Seed data triggers recurring related_to_required validation warnings on dev server #1203
Description
Observed
At 5d46177f, pnpm dev logs recurring validation warnings while serving seed data:
Validation rule 'related_to_required' (warning): At least one related record should be selected
At least one seed record (likely in the activity/event family) is missing its related-record reference, so every boot logs the warning repeatedly. Warning level only — runtime, UI and read paths are unaffected.
Expected
Seed data passes all authored validation rules silently: a clean pnpm dev boot produces zero validation warnings.
Repro
git checkout 5d46177f
pnpm dev
# watch the server log during boot / while browsing seeded records
Recorded as an unassigned finding (not claiming). Surfaced in the platform-overview promo-film run (video-studio work order steedos-labs/video-studio#11, read-only carrier at the pinned SHA); relayed by the coordinating PM session on behalf of the producing session, which has no GitHub scope on this repository. Duplicate search: no existing issue mentions related_to_required or seed validation warnings.
Activity
huangyiirene commentedon Aug 25, 2026
定级(首触)→
pm:queue,并补type语义标签bug。按本席常设授权定级。⚠️ 本仓无
finding同笔摘除。(type生产者 —— 加的是bug标签,不是 GitHub issue type,与既有惯例一致。)为什么这不只是一条日志噪音
判据是本车道宪章的第二条常设承诺:
种子数据违反本应用自己编写的校验规则,是这条的直接反例:应用一边声明「至少要选一条关联记录」,一边发货一批不满足它的数据。任何 clone 这个仓的人,第一次
pnpm dev就会在日志里看到自己的示范应用在抱怨自己的示范数据。而且它还有第二重代价:一条恒常出现的 warning 会训练所有人无视 warning 流。本轮已经在别处付过这个账(objectstack#11529 的 50 条截断上限、
objectstack build的 advisory 告警 —— 都是「告警没人看」促成的)。范围与判据
related_to_required规则的谓词,读它然后反查种子集。pnpm dev一次干净启动 零related_to_required告警。crm_*种子记录都满足其对象上声明的 validations」的测试,否则下一条种子记录还会静默复发。这一条是本席加的要求,不在卡片里。前提刷新要求
卡片的基线是⚠️ 派发时先在当前 main 上复现(
5d46177f,本仓现在在6ed7b8d。pnpm dev,看启动日志)。若已不复现 —— 例如某次种子改动顺手修掉了 —— 那是合法交付:premise_still_valid: false+pr: null,关卡了事。本轮已有三张卡是这么收的。出处
来自平台总览宣传片的制作流程(video-studio 工单,只读载体,pinned SHA),由协调 PM 会话代为转报 —— 生产会话对本仓无 GitHub 权限。查重已做:无既有 issue 提及
related_to_required或种子校验告警。Generated by Claude Code
hotlong commentedon Aug 25, 2026
Claim: PM loop round 1
Session:
session_01R5spVzEKtmowdQQ3r6xNRMBranch:
claude/issue-1203-seed-validation-warningsWorktree:
hotcrm-issue-1203Domain: (hotcrm has no
domain:*taxonomy — repo-wide seat, objectstack#10282)File surface:
src/data/*.seed.ts, and only if the measurement demands itsrc/objects/event.object.ts/src/objects/task.object.ts, plus one test file (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusClause-②: no — app-side seed data and possibly one validation rule. No contract accept/reject behaviour changes and no public surface widens; this repo ships no packages.
Serial constraints cleared: 4 open PRs, all dependabot (#658, #1058, #1178, #1264), none touching
src/. Batch siblings: #1133 takessrc/objects/case.hook.tsandsrc/objects/lead.hook.ts— ⛔ both are fenced out of this card by name, andevent.object.ts/task.object.tsare fenced out of that one; #1136 takescontent/docs/sales/**— disjoint.Read the full issue body and every comment on GitHub yourself before starting, and verify the body is not truncated.
The card says a seed record is "likely in the activity/event family" missing its related-record reference, and expects "a clean
pnpm devboot produces zero validation warnings."Measured on
origin/main@413d964,src/data/service.seed.ts:140-141carries this, in the tree, today:At least one source of this warning is deliberate and documented as such. So the card's "Expected" is an assumption, not an established requirement, and the obvious fix — parent the row — would delete a deliberate piece of seed data whose comment explains itself.
related_to_requiredis declaredseverity: 'warning'on bothsrc/objects/event.object.ts:248andsrc/objects/task.object.ts:289.Ruling (⛔ not re-adjudicable)
related_to_required, across both objects and all ofsrc/data/. The card says "recurring" and "repeatedly"; the one row I found is documented as intentional. How many rows, in which files, and which are deliberate is the deliverable — a card that turns out to be one intentional row is a complete and valuable answer.needs-user-decisioncard. ⛔ Never decided inside this PR.src/objects/case.hook.tsorsrc/objects/lead.hook.ts— Guest-submission sanitisation never reaches storage:delete input.xin a beforeInsert hook is a no-op while assignments on the same object land #1133 is in flight on both in this same batch.PM mechanism assumptions (measured this round — falsify them and say so)
related_to_requiredisseverity: 'warning'on both objects, so it is noise, not breakage — the card says so too ("runtime, UI and read paths are unaffected").5d46177f, which is stale —origin/mainis now413d964, and seed data has moved since.main, not at that SHA. If the warnings no longer appear at all,premise_still_valid: falseis the right answer.conditiononevent.object.tsis aPCEL envelope guarding all fiverelated_to_*columns with!has(...) || isBlank(...).daysBetween(null, …)abort — it greps ordering operators, and its totality run short-circuits before the call #1115's whole subject, and it is queued.PM's suggested route (optional — measurement beats it)
The cheapest honest outcome is probably: parent the accidental rows, leave the deliberate one, and land a test that pins the expected warning count so the next boot's noise level is a measured constant rather than a vibe.⚠️ But that presumes there are accidental rows, which is exactly what has not been measured. Do not start from my route.
Non-negotiable
package.jsononorigin/mainat dispatch time):pnpm verify=validate→typecheck→lint→lint:i18n-gate→hygiene→hygiene:tokens→build→test. Exit codes captured after redirect, never through a pipe.hygiene:tokensis a shrink-only ratchet, but notesrc/data/is excluded from its measured surface by the maintainer ruling of 2026-08-17 quoted inscripts/check-source-token-ratchet.mjs— so seed rows are free.src/objects/is not excluded. ⛔ Never raise a ceiling; stop and report with numbers.✗ i18n lint gate: … has no issues arrayand✗ source token ratchet failed. Both are the gates' own failure paths driven with fixtures..changeset/*.mdversus the base sha, or theskip-changesetlabel, or empty frontmatter.private: truedoes not exempt it.git checkout HEAD --with the path, never the bare form, which restores from the index; verify the tree afterwards).draft, basemain, first body lineFixes #1203if the boot is clean afterwards;Part of #1203if you deliberately leave deliberate rows warning — and say which in the report.Report
Terminal JSON report as a comment on this issue first (first line the literal
os-dev-report), then as the return message. Machine-checkable fields only. ⛔ Do not restate the PR body's narrative. Leave your own claim comment with your own session ID and branch — the one above identifies the PM session, not you.premise_still_valid: falsewithpr: nullis a legitimate delivery, and on this card it is a likely one. I would rather have a measured "the premise does not hold" than a PR that deletes a deliberate seed row to quiet a log line.Generated by Claude Code
hotlong commentedon Aug 25, 2026
Claim: os-dev seat, dispatched for this card
Session:
session_01R5spVzEKtmowdQQ3r6xNRM(same id as the PM claim above — I run as the dispatched dev subagent inside that session, so the id does not distinguish us; the branch and worktree below do)Branch:
claude/issue-1203-seed-validation-warningsWorktree:
hotcrm-issue-1203(dedicated, offorigin/main@413d964)PR: #1291 (draft)
Files touched:
test/seed-validation-warnings.test.ts(new),.changeset/seed-validation-warning-set-pinned.md(new). No file undersrc/was modified — neither seed data nor any validation rule. The fenced-outsrc/objects/case.hook.tsandsrc/objects/lead.hook.ts(#1133) were not read or touched.Outcome in one line: the card's premise does not hold — there is no accidentally unparented seed row. Full report follows in the next comment.
Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 25, 2026
os-dev-report
{ "issue": 1203, "status": "needs_decision", "branch": "claude/issue-1203-seed-validation-warnings", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1291", "premise_still_valid": false, "summary": "Enumerated every seed row against the related_to_required predicate across both objects that declare it (crm_event, crm_task) and all of src/data/: exactly ONE row trips it, the crm_task row 'Update CRM pipeline report', which is deliberately unparented and documented as such at src/data/service.seed.ts:140-142. Zero of the 27 crm_event rows can trip it (EventSpec makes relatedType/relatedKey mandatory and the builder always writes the matching column), confirmed by a SQL sweep of the seeded DB returning that one row and no others. The card's premise fails on two counts: the warning is NOT recurring (clean-DB first boot logs it twice — seed insert plus demo_bootstrap's claim sweep re-writing the same row to stamp owner_id — and every subsequent boot logs it ZERO times, measured over 135s), and its 'zero validation warnings' expectation is an assumption reachable only by parenting a deliberate row or editing the rule, both forbidden by the ruling. No accidental row exists, so no seed data and no rule changed; the PR is test-only, pinning the measured set so a genuinely accidental row cannot land silently later.", "tests": "Repro on current origin/main @ 413d964 (not the card's stale 5d46177f). Clean-DB boot (rm -rf .objectstack/data; objectstack dev -p 4051): boot log 'Boot diagnostics — 4 warnings', of which exactly 2 are \"WARN Validation rule 'related_to_required' (warning): At least one related record should be selected\" at 15:04:03.396Z and 15:04:05.738Z. Second boot on the already-seeded DB, observed 135s: related_to_required occurrences = 0, total WARN = 1 (an unrelated Seeder budget line). DB sweep via better-sqlite3 over both objects for rows with all five related_to_* NULL/empty: crm_task total=7 TRIPPING=1 ('Update CRM pipeline report'); crm_event total=27 TRIPPING=0. ABLATION (enumeration completeness): temporarily added related_to_type+related_to_account to that one row; mutation proven on disk BEFORE the run by marker grep count 3->4 and blob hash 4059119010da... -> 2c80f31001c8..., then REBUILT with pnpm build (exit 0) and the mutation proven present in dist/objectstack.json by reading the row's artifact context, then clean-DB boot on port 4052: related_to_required warnings = 0 (down from 2), confirming no other row contributes. Restore leg ran via trap with absolute paths using 'git checkout HEAD -- <abs path>' and was proven byte-identical (HEAD blob == working blob, git diff HEAD empty). REVERSE-VERIFICATION of the new pin, from the committed state a7ae812, both legs red as predicted: leg A parent the deliberate row -> 'AssertionError: expected [] to deeply equal [ Array(1) ]'; leg B add a new unparented row -> 'expected [ ...(2) ] to deeply equal [ Array(1) ]' naming the probe row; restore byte-identical again. GATES, all eight of pnpm verify, run on the exact pushed tree a7ae812 with exit codes captured after redirect (never through a pipe): validate EXIT=0, typecheck EXIT=0, lint EXIT=0, lint:i18n-gate EXIT=0, hygiene EXIT=0, hygiene:tokens EXIT=0 (gate's own verdict line 'source token ratchet clean', authored total ~135,376 vs ceiling ~140,000, no ceiling raised, src/ untouched), build EXIT=0 ('Build complete'), test EXIT=0 ('Test Files 134 passed (134)', 'Tests 2925 passed | 1 skipped (2926)'). New file alone: 4 passed.", "open_questions": [ { "question": "Should related_to_required fire at all on an internal task that legitimately hangs off no customer record? The rule is severity 'warning' on both crm_task and crm_event and its predicate has no exemption, so the shipped exemplar permanently demonstrates its own seed data failing its own authored rule on first boot. Per ruling 3 this is product semantics and is NOT decided in this PR.", "options": [ "A. Leave exactly as-is. One warning line on a first boot only; the seed comment already explains it. Zero risk, and the rule keeps its full reach.", "B. Narrow the predicate so an explicitly-internal task is exempt — needs a real discriminator on crm_task (e.g. an is_internal flag or a task type value); today none exists, so this is a new authorable field, not a predicate tweak.", "C. Parent the housekeeping row to an account anyway. Reaches 'zero warnings' but deletes the only demonstration that a warning-severity rule is survivable, and contradicts the comment in the seed.", "D. Drop the rule to a UI-only hint / remove it. Largest reach change; declared-equals-enforced argues against it." ], "recommendation": "A, leave as-is. On real business need there is no evidence anyone is harmed: the rule is a warning by design, runtime/UI/read paths are unaffected, and it costs one log line per fresh database, not per boot. On long-term soundness, B is the only option that fixes the semantics honestly, but it requires inventing a crm_task discriminator with no current consumer — implementation-first capability growth on a startup-scoped repo, and C/D are both gate-weakening dressed as cleanup. On making AI-authored metadata hard to get wrong, the current state is actively instructive: an exemplar that shows a warning-severity rule firing, tolerated, and explained teaches the right lesson, whereas C teaches that the fix for a warning is to bend the data until it stops. On startup scope discipline, A costs nothing. If the maintainer nonetheless wants zero warnings on a fresh boot, B is the only route that does not weaken a rule — and it should be its own card with the field design decided first." } ], "out_of_scope_findings": [ "REPORTED TO PM, NOT FILED (os-dev seat gets HTTP 403 from api.github.com; REST PR create also 403'd, PR was opened via the GitHub MCP tool). Finding 1 — authored seed records cannot be validated against date predicates, and doing so returns confident wrong answers: seeded dates are unresolved CEL envelopes ({dialect:'cel',source:'daysFromNow(30)'}) until the loader resolves them at boot, and handing one to crm_opportunity.close_date_future made it 'fire' for TEN opportunities whose close dates are all in the future, a rule the real boot logs zero times. Any future test, script or tooling that sweeps seed records through evaluateValidationRules will hit this. My new test defends against it explicitly (skips such pairs as unmeasurable, plus an anti-vacuity guard), but nothing repo-wide does.", "REPORTED TO PM, NOT FILED. Finding 2 — pre-existing boot warning unrelated to this card, seen on every clean-DB boot: '[metadata-protocol] seed/API tenancy split detected but the target organization is not derivable — backfill skipped (#8686)'. It states crm_account.account_number, crm_product.product_code and crm_campaign.campaign_code each run TWO autonumber counters (global lastValue 9/13/7 vs organization 0) and 'can mint the same \"unique\" identifier twice'. It names upstream #8686. This is a louder correctness signal than the one this card is about and is worth its own triage.", "REPORTED TO PM, NOT FILED. Finding 3 — demo_bootstrap's claim sweep re-validates every row it stamps, so any seed row that trips a warning rule warns a second time on a fresh database purely because owner_id is being written. Harmless today (it doubles one line), but it means warning counts from boot logs are per-write, not per-row, and anyone counting them will over-count by the number of claimed objects." ] }Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 25, 2026
PM review — ACCEPT. PR #1291.
premise_still_valid: falseis the correct verdict and the right delivery.Reviewed against the repo and against GitHub, not against the report.
Checklist: draft ✓ · base
main✓ · first body linePart of #1203✓ (correct — the card is not closed by this) · 2 files =test/seed-validation-warnings.test.ts+ changeset, no seed data and no rule touched, exactly as ruled ✓ · empty-frontmatter changeset for a test-only PR ✓ ·mergeable_state: clean✓ · all 9 checks green,Build and Test (22.x)confirmed by NAME ✓ · path fork:test/+.changeset/— neither governed ⇒ loop-mergeable ✓I verified the enumeration myself, because everything else rests on it
The claim that exactly one seed row trips
related_to_requiredis the load-bearing one — it is what makes "the card's defect does not exist" true rather than merely unrefuted. Sweptsrc/data/service.seed.tsindependently for subject-bearing record literals carrying norelated_to_*and norelatedKey:Nine hits, eight of them
crm_case— an object with norelated_to_*columns and norelated_to_requireddeclaration, so they are not candidates at all. The one task row is the whole population. AndEventSpec.relatedType/relatedKeyare declared non-optional (readonly relatedType: EventRelatedType;), which is why nocrm_eventrow can trip. Independently confirmed.The ablation is the stronger proof and I am glad it was run rather than argued: parenting that single row took a clean-DB boot from 2 warnings to 0, with the mutation proven on disk by blob hash and proven present in the built artifact before the boot. That closes enumeration completeness from the other side.
The premise fails on two counts, and the second one matters more
⭐ The best thing in this PR is the hazard it refused to pin
Sweeping all 12 warning rules naively produced ten false positives:
close_date_future"fired" for ten opportunities whose close dates are all in the future. Seeded dates are unresolved CEL envelopes (cel\daysFromNow(30)``) until the loader resolves them at boot; hand a predicate the envelope and it returns a confident wrong answer rather than failing. The real boot logs that rule zero times.The dev skipped those pairs as unmeasurable instead of pinning fictions, and added an anti-vacuity guard asserting
related_to_requireditself was never skipped — so the pin cannot go green while measuring nothing. A test that quietly fell into its own skip path would have read green forever. That guard is the difference between a pin and a decoration.Also correct: pinning the row to its reason (the test fails if the
deliberately unparentedcomment is deleted), so the pin cannot outlive the rationale it depends on.Disposition — the card stays open, and its remaining scope is a decision
Part of, so this does not close #1203, which is right. What is left is the question the dev correctly refused to answer inside a PR: shouldrelated_to_requiredfire at all on an internal task that hangs off no customer record? Moving this card to the decision inbox with a four-prism block in the next comment.Three out-of-scope findings were reported rather than filed (403), and I am filing the one that outranks this card. Landing now.
Generated by Claude Code
hotlong commentedon Aug 25, 2026
Re-triage after PR #1291 →
needs-user-decision. The defect half is answered; what is left is a product call.Assignee cleared,
pm:dispatcheddropped. The card as filed — "at least one seed record is missing its related-record reference" — is premise-false and now pinned as such. What survives is a question the dev correctly refused to answer inside a PR.选项
demo_bootstrap认领扫描回写owner_id),之后每次启动 0 行。种子里的注释已经解释了原因。crm_task今天没有可用的判别字段(没有is_internal,也没有可承担此意的 type 值),所以这不是改一个谓词,而是新增一个可声明字段。四棱分析
① 实际业务需求 —— 没有任何人受损的证据,这一点是实测的而非推测:规则声明即为⚠️ 反向也要诚实:样板是给人看的,首次启动就打自己规则的 warning,确实会让第一次跑起来的人愣一下 —— 这是真实但很小的观感成本,不是功能缺陷。
severity: 'warning',运行时、UI 与读路径均不受影响(卡片自己也这么说),代价是每个全新数据库一行日志,不是每次启动。② 项目长远合理性 —— B 是唯一在语义上诚实的修法(真正区分「内部任务」与「忘了挂父记录」),但它要求先设计一个
crm_task判别字段,而该字段今天没有任何消费者 —— 这正是本仓反复拒绝的 implementation-first 形态:先造声明面,再去找用途。C 与 D 都是「把规则弯到数据上」或「把规则拆掉」,属于门禁削弱换绿,方向错误。A 不欠任何债。③ 防 AI 写错 —— 这一轴反直觉地支持 A,是本次判断的关键:当前状态是有教学价值的。样板演示了「一条 warning 级规则触发了、被容忍了、并且旁边写明了为什么」,这正是 AI 作者需要学会的形态。而 C 教给照抄者的是「日志有 warning,就把数据改到它不响为止」—— 这是最坏的一课,且它恰好是这张卡原本会引向的动作。⭐ PR #1291 还把这一课钉住了:测试同时锁定该行与解释它的注释,任何一方被删都会红。
④ 创业阶段不扩散需求 —— A 零成本。B 是新增字段 + 迁移形状 + 四语言包 + 文档行,为一行日志付这个价,在创业阶段很难辩护。
推荐:A(维持现状),与实施该卡的 dev 独立得出的结论一致
四棱在 A 上收敛,③ 是决定性的一轴。⚠️ 推荐是输入不是放行。
若维护者仍希望「全新启动零 warning」,唯一不削弱规则的路线是 B,且它应当另立一卡、先定字段设计,⛔ 不应挂在这张卡下顺手做掉 —— 新增可声明字段属于功能新增,是人工地板上的动作。
⛔ C 与 D 本席不建议采纳,理由在③与②;但决定权在维护者,本席只供证据。
Generated by Claude Code
huangyiirene commentedon Aug 31, 2026
裁决:定性平台问题,上行 objectstack#13889 —— 本卡收口(维护者 2026-08-31)
项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· hotcrm 决裁批 #18 ①维护者原话(逐字):「#1203 种子数据触发 related_to_required 警告,这种也是平台的问题啊」
裁决内容
severity: 'warning'的规则文案是给表单中的人看的劝导,而平台没有受众/通道概念 —— UI 级劝导在 seed / bootstrap 等机器写入路径上照样求值并以 WARN 打进启动日志,且按写入次数而非按行计数(认领回写重复求值,本卡 dev 发现 3)。应用侧的行与规则都合法,噪音是平台的;状态
needs-user-decision摘除,本卡关闭(completed) —— 现象与测量记录完结,活问题由 objectstack#13889 承载。dev 发现 1(CEL 信封离线求值)与发现 2(双 autonumber,#8686)已在上行卡记录为相关不并卡。Generated by Claude Code