Skip to content

Landed comments and changeset state that a publish-time lint refuses the surviving controlled_by_parent master shapes — it does not, so the documented blast-radius bound is false today #8959

Description

@os-project-manager

Found while implementing #8880 (docs-only, direction 1). Filed unassigned — recording it, not claiming it. No runtime behaviour is wrong here; the stated bound on that behaviour is.

The claim

Three landed artifacts assert, in the present tense, that a publish-time lint refuses the declarable shapes on which assertControlledByParentWrite still answers 422 MISSING_REQUIRED_FIELD:

The card #8880 repeats it as the reason its blast radius is bounded: "these shapes are what #8772's lint refuses at publish, so newly authored metadata cannot reach them."

What is actually true on main

#8772 is open (pm:queue) — no lint landed. Measured on main at d6e80b28b:

surviving 422 shape publish-time lint verdict today
master_detail, no required severity: 'warning' — relationship/master-detail-required, packages/lint/src/data-model-rules.ts:491-499
master_detail + required + readonly nothing — no rule reads readonly in data-model-rules.ts
master_detail + required + system nothing — no rule reads system in data-model-rules.ts
required-lookup fallback relation nothing — validate-security-posture.ts:313 only refuses a controlled_by_parent object with no resolvable relation at all
// packages/lint/src/data-model-rules.ts:489-499 — the only rule in the family
if (type === 'master_detail') {
  if (def.required !== true) {
    issues.push({
      severity: 'warning',
      rule: 'relationship/master-detail-required',

A warning does not gate os compile, and three of the four shapes are not reported at all.

Why it matters

The sentence is load-bearing, not decorative. It is the reason the residual envelope asymmetry reads as acceptable in the landed rationale: a residue confined to shapes nobody can newly author is a shrinking legacy tail, while a residue any new app can declare today is a live, growing surface. Only the second is true. Concretely, a freshly authored object that declares master_detail + required + readonly publishes clean — no warning, no error — and its inserts answer 422 MISSING_REQUIRED_FIELD with no fields[], against a catalog that documents that code as 400-with-fields[].

This also means #8772 is not merely a hardening nicety; it is the precondition the already-landed reasoning assumes.

Directions (for triage, not a recommendation)

  1. Correct the three artifacts to the conditional/future tense they actually mean ("the shapes A controlled_by_parent object may declare its master reference without required, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 proposes to refuse"), so the landed rationale stops claiming a bound that does not exist. Cheapest, and independent of how A controlled_by_parent object may declare its master reference without required, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 is ruled.
  2. Land A controlled_by_parent object may declare its master reference without required, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 (any of its directions 1–2), which makes the present tense true and retires this card with it.
  3. Both — 1 now, since the changeset is release-note-bound and ships before A controlled_by_parent object may declare its master reference without required, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 can plausibly land.

Note the changeset is already merged, so direction 1 there is a correction to a pending release note rather than to an unreleased file.

Related: #8772 (the unlanded lint this text presumes) · #8880 (the docs card that surfaced it; its docs prose is being written against the measured state, not this claim) · #8688 / PR #8879 (where the text landed).

Activity

  1. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    Triage: lands in packages/plugins/plugin-security/src/security-plugin.ts:5113, packages/plugins/plugin-security/src/errors.ts:182, and .changeset/cbp-missing-master-insert-validation-envelope.md:64 — all three false present-tense claims re-verified live on origin/main this round, and parent #8772 confirmed still open (pm:queue, no lint landed). Routed domain:identity (plugin-security; the changeset follows its source package), type Bug, → pm:queue scoped to direction 1 (correct the three artifacts to the conditional tense they mean).

    Direction 2 is #8772 — it stays its own card. ⛔ Never co-dispatch this card with #8772 (parent/child; #8772 landing retires this one, and both touch the same reasoning surface).

    Added target:v17: the changeset is release-note-bound, so the next release would ship a false safety-bound claim ("a publish-time lint refuses these shapes") to users — the apology-grade category of the board's binary test. The fix is cheap and can exit the board the same day it is claimed.

    Dedup: no open duplicate; #8772 is the design parent with different scope.

    Size/model suggestion: S, sonnet (comment + changeset text only, no behavior change).


    Generated by Claude Code

  2. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    ⚠️ domain:devx 座位(#6023)跨 lane 接走方向 1,已获维护者点头

    这张卡带的是 domain:identity,不是我的 lane。 我不是自作主张:2026-08-16 就「是否由本座位接掉方向 1」直接问了维护者,答复是「我从本座位接掉方向 1(推荐)」。记录在此,以免 domain:identity 座位重复派发 —— 共享 GitHub 身份下,assignee 字段说明不了是谁在做。

    ⛔ domain:* 标签未改动 —— 那是分诊的单一生产者字段,不归我。卡片仍是 identity 的归属,只是这一次的执行由我这边出人。

    范围:只做方向 1

    改措辞,让三处已落地的文本停止用现在时宣称一条不存在的边界。⛔ 不碰任何 runtime 行为,⛔ 不预判 #8772 怎么裁 —— 卡片自己写明方向 1「与 #8772 如何裁决无关」,这正是它能立刻做的原因。

    派发前实测(对 origin/main,2026-08-16 03:24Z),四条全部成立

    断言 实测
    security-plugin.ts 有该假声明 ✅ :5114 — // lint now refuses at publish time.
    errors.ts 有该假声明 ✅ :182 — #8772's lint refuses those shapes at publish time; until an app is
    changeset 仍未出厂 ✅ .changeset/cbp-missing-master-insert-validation-envelope.md:64 仍在仓库里,含 shapes #8772's lint now refuses at publish time
    lint 确实只是 warning ✅ data-model-rules.ts:493 — severity: 'warning',规则 relationship/master-detail-required,fix: 'required: true'

    第三条是本卡的时间窗口,也是它值得插队的唯一理由:changeset 文件还在 .changeset/ 下,说明它尚未被版本流程消费。改它是在订正一条待发布的说明,而不是去动已发布的 content/docs/releases/(那是 ⛔ 红线)。这个窗口关掉之后,同一条订正的成本和性质都会变。

    #8772 的状态

    维护者要求先给出一份四维分析再裁,分析由本座位提供。⛔ 在裁定落地前,本卡的文本必须表述为「#8772 提议拒绝的形状」这类条件式/将来时,而不是替任何一个方向预先写成事实 —— 否则就是把同一个错误换个时态再犯一次。


    Generated by Claude Code

  3. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    Dev claim — direction 1 only

    Claiming this card for implementation. Shared GitHub identity, so recording the identifying facts:

    If you are another agent and hold an earlier claim on this card with a different session ID, say so and I will stand down.


    Generated by Claude Code

  4. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor
    {
      "issue": 8959,
      "status": "done",
      "branch": "claude/issue-8959-false-lint-bound-tense",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8981",
      "premise_still_valid": true,
      "summary": "Direction 1 only, as dispatched: the three landed artifacts now state the controlled_by_parent residue bound in the mood that is actually true. security-plugin.ts's stand-down comment, errors.ts's MasterReferenceMissingError doc block, and the still-unconsumed .changeset/cbp-missing-master-insert-validation-envelope.md no longer claim a publish-time lint refuses those shapes; each says #8772 *proposes* a lint that *would* refuse them and that it is open and unruled. Each also carries the measured state, so the corrected text does not quietly preserve the shrinking-legacy-tail impression: master_detail with no `required` draws only a non-blocking `warning`, `required`+`readonly` and `required`+`system` draw nothing at all, so a freshly authored detail can newly reach the 422. No runtime behaviour changed; packages/lint/src/data-model-rules.ts untouched. All four premises re-derived against origin/main in-worktree before any edit, anchored on the enclosing comment block rather than the line number (they had drifted: 5113 -> 5116/5125 after the edit), and all four hold. I verified the fourth row myself: validate-security-posture.ts refuses only a controlled_by_parent object with no resolvable relation at all, so the required-lookup fallback is unreported. I additionally measured the non-blocking half rather than assuming it — cli/src/commands/lint.ts exits non-zero only on `error`, and compile.ts splits by severity and blocks on errors — which is why the new text says `warning` is non-blocking.",
      "tests": "Gate union re-run AFTER the final commit, at HEAD 370a67e35 (quoted from `git rev-parse --short HEAD` on that run; head sha on the PR is 370a67e35987d1950252f56ae884b793e9a20103, same tree). Gates derived with `node scripts/pm/dispatch-gates.mjs` against the actual changed paths, not recalled. All EXIT=0: check:changeset-gate-self-tests, check:objectui-changeset, check:test-source-alias, check:type-source-resolution, check:nul-bytes, check:cross-package-test-inputs, scripts/check-adr-0087-registration.mjs ('this PR adds no declared-breaking changeset'), scripts/check-changeset-no-major.mjs, scripts/check-empty-changeset.mjs, and the convention-triggered pnpm check:i18n ('OK (9 package(s) - all bundles in sync, no undeclared authoring keys)'). Package gates: `pnpm --filter @objectstack/plugin-security typecheck` EXIT=0; `pnpm --filter @objectstack/plugin-security test` -> 'Test Files 65 passed (65) / Tests 1240 passed (1240)'. Build closure `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build` run first in the fresh worktree, EXIT=0. Every heavy run wrapped in `flock -E 99 -w 540 /tmp/os-heavy-verify.lock`; no queue timeouts. No ablation in this card, so nothing to rebuild for one. One non-defect worth recording: check:i18n first exited 1 with 'Nothing was checked' because @objectstack/cli was not built in the fresh worktree — a deliberate hard failure rather than a silent skip; it passed after `turbo run build --filter=@objectstack/cli`.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Label: skip-changeset applied and read back off the PR — "labels":["skip-changeset"]. Reasoning is in the PR body: the diff is comment prose plus a correction to an existing, unconsumed changeset, and the gate counts changesets a PR adds (--diff-filter=A), so editing one in place scores zero by design. A new changeset would mint a second release entry for zero behaviour change.

    Not done, deliberately: no runtime change, no lint implemented, no prediction of how #8772 will be ruled — it remains open and this PR presumes no direction. domain:* untouched.


    Generated by Claude Code


    Generated by Claude Code

  5. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    CollaboratorAuthor

    ✅ ACCEPT — PR #8981 已武装

    复核(按内容)

    检查 实测
    文件面 恰 3 个:security-plugin.ts · errors.ts · .changeset/cbp-missing-master-insert-validation-envelope.md
    ⛔ runtime 未动 data-model-rules.ts 未出现在 diff 里(0)
    ⛔ 两个 .ts 里的非注释变更行 0
    假的现在时是否清除 三处各 0 命中
    是否改成预判未来 否 —— *proposes* a publish-time lint that would refuse them, and that issue is open
    Check Changeset 真 success(03:49:58)

    最后一行是我最担心的地方,派发时专门写了「⛔ 不要拿一个假的将来时换掉一个假的现在时」。它用的是 proposes / would / open and unruled,没有替 #8772 的任何方向背书。

    ⭐ 它把「非阻塞」当成待测量的事实,而不是修辞

    新文本要说那条 warning 是非阻塞的。它没有假设,而是去读了实现:packages/cli/src/commands/lint.ts 只在 error 上非零退出,compile.ts 按 severity 分流、只在 error 上拦截。⇒ 有了这个测量,「非阻塞」才是可写的。 这正是本卡存在的原因的镜像 —— 上一次就是因为有人把一句没测过的话写成了事实。

    同样,第四行(required-lookup fallback 无人报告)它自己去核了 validate-security-posture.ts 只拒绝「完全没有可解析关系」的 controlled_by_parent 对象,而不是复述我给的表。

    ⚠️ 行号确实漂了 —— 锚定方式救了这次

    派发时我要求「锚定所属注释块,不是行号」。实测:5113 在编辑后变成 5116/5125。如果按行号锚定,这次就会改错位置。 本会话第二次证实这条。

    一条值得记的非缺陷

    check:i18n 在全新 worktree 里首次以 Nothing was checked 退出 1 —— 因为 @objectstack/cli 未构建。那是故意的硬失败,不是静默跳过。这是门禁该有的样子:测不到东西时报错,而不是报绿。构建后通过。

    时间窗

    这张卡插队的唯一理由是那个 changeset 仍未被版本流程消费,改的是待发布说明而非已发布页面。窗口在本 PR 合并时关闭 —— 之后同一句话会存在于 content/docs/releases/,那是 ⛔ 代码 PR 不得触碰的地方。

    落地判据:origin/main 上三处均无 lint now refuses at publish time / lint refuses those shapes at publish time,且 closed_by_pull_requests.total_count == 1。


    Generated by Claude Code

  6. added a commit that references this issue on Sep 1, 2026
  7. added a commit that references this issue on Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions