Skip to content

docs(comments): re-scope the #8959 CBP residue note to what #9138 actually closed - #14132

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13722-9138-9139-residue-rescope
Sep 1, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-13722-9138-9139-residue-rescope

Conversation

@claude

@claude claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #13722

Comments only, one paragraph, one file. Final commit 1d1263f26.

The false sentence

packages/plugins/plugin-security/src/security-plugin.ts — the [#8959] paragraph at the controlled_by_parent stand-down site in assertControlledByParentWrite — said the #8772 ramp has "two code legs and neither has landed yet", and that #9138 "is dispatchable now but not yet merged".

Located by symbol and text, never by line number: the card cites :6351-6367, and after #14049 (09e4b0ece) landed upstream in the same file the paragraph is at :6484-6500. Anchored on the literal string neither has landed yet.

Falsified before rewriting — and the card's suggested rewrite did not survive intact

Read directly from the API rather than from the card's paraphrase:

state today reading
#9138 (Direction 2, builder force) closed / completed 2026-08-17, PR #9206 the "neither has landed" sentence is false
#9139 (Direction 1, lint promotion) open, pm:on-hold, target:v18 that half of the sentence is still true

The card then proposed re-scoping the residue claim "to the raw .parse() path". Measured against today's code, that would have replaced one false sentence with another. It is true for one of the three residue shapes and false for the other two.

forceCbpMasterDetailRequired (packages/spec/src/data/object.zod.ts) returns early on required === true and never inspects readonly / system. So the two provenance-flagged shapes are still newly authorable through ObjectSchema.create() itself — not merely through raw parse. Measured by running the built dist of @objectstack/spec and @objectstack/lint:

===== A. builder path: ObjectSchema.create() =====
M1   CBP + master_detail, `required` OMITTED          -> ok=true required=true   (forced)
M1c  CONTROL same shape, sharingModel private          -> ok=true required=false  (not forced)
M2   CBP + master_detail + `required: false`           -> ok=false  (refused, located error)
M2c  CONTROL same shape, sharingModel private          -> ok=true required=false  (accepted)
M3   CBP + master_detail + required:true + READONLY    -> ok=true  ACCEPTED
M4   CBP + master_detail + required:true + SYSTEM      -> ok=true  ACCEPTED
M5   CBP + master_detail + READONLY, required OMITTED  -> ok=true required=true readonly=true
M5b  CBP + master_detail + SYSTEM,   required OMITTED  -> ok=true required=true system=true

===== B. raw parse path: ObjectSchema.safeParse() =====
M6   safeParse CBP + `required: false`   -> success=true  required=false
M7   safeParse CBP + required OMITTED    -> success=true  required=false   (not forced at parse)
M7c  CONTROL safeParse with alias `isRequired` -> success=false unrecognized_keys@fields.order_id

===== C. lint relationship/master-detail-required =====
M8   required OMITTED            -> ["warning"]   | also fired: delete-behavior, line-items-inline-edit
M9   required:true + READONLY    -> []            | also fired: delete-behavior, line-items-inline-edit
M10  required:true + SYSTEM      -> []            | also fired: delete-behavior, line-items-inline-edit
M11  required:false              -> ["warning"]   | also fired: delete-behavior, line-items-inline-edit

Every zero above is a reading, not an absence:

  • M9 / M10 report no master-detail-required finding, and in the same lint call two other rules do fire — so the linter is live on that object and the zero is about the predicate, which is still def.required !== true at severity: 'warning'.
  • M7c is the control for the parse leg: the same safeParse refuses an alias key, so M6/M7's success=true is a decision and not a comparator that always says yes.
  • M1c / M2c are the controls for the builder leg: the same shapes under sharingModel: 'private' are not forced and not refused, so the force is CBP-scoped rather than unconditional.

M5 / M5b are the sharpest reading: the force mints the two flagged shapes. A readonly or system master reference authored with required omitted comes out of create() as required: true plus the flag — which is exactly a shape validateRecord skips (if (def.system || def.readonly) continue;), leaving the security gate the sole refusal. That matches omissionRefusedByValidation's own predicate, def?.type === 'master_detail' && !!def?.required && !def?.readonly && !def?.system.

The rewritten paragraph states all of that, plus the parts of the old text that re-measured true (#8772 ruled; #9139 held for v18; main still 17.x — packages/spec is 17.2.0).

The non-comment diff is empty — with a live comparator

Both versions transpiled with removeComments: true and compared two independent ways (output text and ts.createScanner token stream), plus two controls:

raw byte lengths: BASE=398918  WORKING=400382  (raw sources differ: true)

### SUBJECT - BASE vs WORKING TREE (this PR diff)
  stripped-TEXT : A sha=ee8e85d25ba0ae6b (136185 B)  B sha=ee8e85d25ba0ae6b (136185 B)  -> IDENTICAL
  TOKEN stream  : A n=11864 sha=799a1855bdacd000  B n=11864 sha=799a1855bdacd000  -> IDENTICAL

### REVERSE CONTROL - WORKING TREE + one-token CODE change ('insert' -> 'update')
  stripped-TEXT : ... -> DIFFERENT   first diff line 2241
  TOKEN stream  : ... -> DIFFERENT   first diff index 10817: A="11:'insert'" B="11:'update'"

### CONTROL 2 - WORKING TREE + comment-TEXT change
  stripped-TEXT : -> IDENTICAL
  TOKEN stream  : -> IDENTICAL

subject non-comment diff EMPTY     : true
reverse control reports NON-empty  : true
comment-only control reports EMPTY : true
OVERALL: PASS

Exit code captured before any pipe: node noncomment-diff.cjs ... ; echo "EXIT=$?" gave 0. An earlier run exited 9 because control 2's anchor matched 0 times — reported as NOT RUN, not as a pass; the anchor was fixed and the control then actually ran.

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no path arguments — the script took the change set from the merge base itself), both output sections read whole. 21 matched families plus 2 convention-triggered i18n families. All run at 1d1263f26:

  • 22 PASS — including check:comment-mask-adoption, check:doc-authoring, check:keyed-text-bounds, check:system-context-census (whose declared population is literally this file), check:tenant-audit-census, check:published-files, check:test-source-alias, check:type-source-resolution, check:nul-bytes, and check:i18n-stale-fill.
  • pnpm check:i18n first exited 1 with its own verdict line reading PREREQUISITE NOT MET — the workspace CLI is not built / "Nothing was checked". Not recorded as a red: the named build closure was run and the gate then passed, reporting plugins/plugin-security in sync (7 bundle(s)) and OK (9 package(s)).
  • 2 NOT MEASURED, both exit 3, neither a pass and neither a red:
    • check-test-completeness — grades a saved turbo run test log; the gate's own text says to record the local reading as NOT MEASURED. CI supplies the log.
    • check:dual-build-cjs-loads — needs a full pnpm build; still reports missing dist/ for ~73 packages after the i18n closure. CI runs it.

Beyond the family: pnpm lint (eslint . --no-inline-config) run repo-wide in the foreground, exit 0, 5623 files linted, 0 findings — and this PR's file is confirmed present in that population at errors=0 warnings=0, so the zero is a measurement rather than a file eslint never opened. pnpm --filter @objectstack/plugin-security typecheck exit 0 (it runs tsconfig.test.json too). @objectstack/plugin-security vitest: 94 files / 1764 tests passed.

No changeset: comments-only, publishes nothing. Same disposition as #14049, the reference PR for this shape, which carried skip-changeset for a diff that included this same file.

Out of scope, filed not fixed

#14130 — the adjacent [#9137] freeze note cites record-validator.ts:993 / :1003 for the provenance skip; both anchors have rotted (the guard is at :1037 / :1047, and :993/:1003 land on unrelated docblock prose while still being in range). Filed unassigned rather than folded in, because #13722's ruling fences this card to the one paragraph. Same class as #13556, different carrier.

The repo-wide sweep of similar stale comments is not addressed here; that surface belongs to #14046, which stays open.


Generated by Claude Code

…ually closed

The paragraph at the `controlled_by_parent` stand-down site said the #8772 ramp
had "two code legs and neither has landed yet". #9138 (Direction 2) merged on
2026-08-17, so that sentence is false.

Correcting it rather than deleting it: #9138 narrows exactly ONE of the three
residue shapes, and only on the builder path. The note now states, measured:

- `master_detail` with no `required` is no longer newly authorable through
  `ObjectSchema.create()` (`forceCbpMasterDetailRequired` forces `required: true`
  and refuses an explicit `required: false`), but survives on the raw
  `.parse()`/`.safeParse()` path #9138 deliberately left untouched.
- `required: true` + `readonly` and `required: true` + `system` are STILL newly
  authorable through `create()` itself -- the force returns early on
  `required === true` and never inspects `readonly`/`system`, and on an omitted
  `required` it mints those two shapes.
- The lint leg (#9139) has not landed: still open, `pm:on-hold`, held for v18;
  predicate still `def.required !== true` at `severity: 'warning'`, so the two
  flagged shapes draw no finding at any severity.

Comments only -- the non-comment diff is empty (TypeScript `removeComments`
text + scanner token stream both identical), with a reverse control proving the
comparator reports non-empty on a one-token code change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actions github-actions Bot added the size/s label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9e1b2de73ef5d7ce5f3f665612a12731d3608725 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f617e60f0418fe7b3929a1266af8019763f94946 — the merge of head 1d1263f26ac406d8fe8dbacabebb560d852995a3 into base 9e1b2de73ef5d7ce5f3f665612a12731d3608725, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f617e60f0418fe7b3929a1266af8019763f94946 && git checkout f617e60f0418fe7b3929a1266af8019763f94946
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e1b2de73ef5d7ce5f3f665612a12731d3608725 1d1263f26ac406d8fe8dbacabebb560d852995a3 && git checkout -B drift-repro 9e1b2de73ef5d7ce5f3f665612a12731d3608725 && git merge --no-ff 1d1263f26ac406d8fe8dbacabebb560d852995a3

node scripts/docs-audit/affected-docs.mjs --json 9e1b2de73ef5d7ce5f3f665612a12731d3608725

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@os-steve
os-steve marked this pull request as ready for review September 1, 2026 06:56
@os-steve
os-steve enabled auto-merge September 1, 2026 06:56
@os-steve
os-steve added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit fa06ed5 Sep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13722-9138-9139-residue-rescope branch September 1, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant