Skip to content

fix(metadata-core,metadata): split the form-view predicate root vocabulary per surface so a field-level current_user test is not false-flagged - #13005

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-12915-current-user-vocabulary
Aug 28, 2026
Merged

fix(metadata-core,metadata): split the form-view predicate root vocabulary per surface so a field-level current_user test is not false-flagged#13005
hotlong merged 2 commits into
mainfrom
claude/issue-12915-current-user-vocabulary

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Part of #12915 — a same-day correction to the boot notice that landed in PR #12989. The card stays open (scope A remains deferred and recorded on it).

What was wrong

#12989 judged every form-view predicate against one vocabulary — record / previous / parent / data — sourced faithfully from the contract prose. For the field-level slot that prose was stale.

current_user, and its ADR-0068 alias roots (user, ctx.user, os.user), resolve on a field-level visibleWhen since objectui#6010. Three spec text sites still said otherwise until PR #12930 re-measured them, and one of those sites was the exact sentence the policy was written against — it landed on main at 12:47, after the fresh read and while #12989 was in flight.

Consequence in the shipped detector: a legacy artifact carrying a legitimate current_user.role == "admin" field predicate was reported as faulting open. That is the cry-wolf class the card explicitly forbids, and the module header additionally stated a measurably false contract claim.

The fix

The vocabulary is now per surface, which is what the contract says. Verified by fresh-reading current origin/main rather than taking the hand-off at its word:

  • Field-levelBOUND_FORM_FIELD_PREDICATE_ROOTS: the shared base plus current_user, user, ctx, os. Silent on all of them.
  • Section-levelBOUND_FORM_VIEW_PREDICATE_ROOTS, unchanged in both name and value: the base alone. current_user is still flagged there, because FormSectionSchema.visibleWhen states it is unbound at that level and the predicate faults open.

The traversal already knew which slot it was scanning, so the split is a parameter rather than a second scanner; findings now carry a surface field.

Two limits of the field binding deliberately do not change the answer, and the module says why: it is a rendering rule rather than authorization (an authoring hazard, not a version-drift one), and the host scope is empty on the console's public /f/:slug route — equally true of a freshly built current artifact, so it says nothing about the artifact's ERA, which is the only thing this notice claims to detect.

Warn-line wording. Printing one flat list would now either understate the field vocabulary (reading to an operator as "your legitimate current_user predicate is broken") or quote a section rule at someone with no section findings. It now prints the bound roots per surface, and only for the surfaces the findings actually implicate.

Stale quotes corrected in the module header, the vocabulary docblock and the door method's docblock, each citing #12930 and objectui#6010, and noting the mid-flight drift so the next reader knows why a merged PR needed a same-day correction.

unboundRootsInCelSource takes the vocabulary as an optional second argument. Its default is unchanged (the stricter base), so any existing caller behaves exactly as before; the default is deliberately the strict one, so a caller that forgets to say gets a findable false positive rather than a silent miss.

Verification

Union re-run on the final commit 330e4b85b.

  • pnpm --filter @objectstack/metadata-core --filter @objectstack/metadata test — 15/15 files, 260/260 tests (was 255: +5 new); 37/37 files, 674/674 tests (was 672: +2 new). Every pre-existing pin unchanged and still green.
  • New pins: field-level predicates rooted at each of current_user / user / ctx / os produce zero notices, at both the policy and the door level; a section-level current_user predicate is still flagged; findings carry the deciding surface; the same predicate is judged differently per surface; the warn line prints only the implicated surface's vocabulary.
  • pnpm --filter @objectstack/metadata-core typecheck exit 0, with tsc -p tsconfig.test.json --listFiles confirming it still reaches both changed metadata-core files (1 hit each). packages/metadata has no typecheck script (DEBT ledger entry, dts build is its type gate): pnpm --filter @objectstack/metadata build exit 0.
  • node scripts/check-type-check-coverage.mjs --re-measure under NODE_OPTIONS=--max-old-space-size=4096none above its recorded number, surplus: none.
  • Gate families re-derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack and diffed against the previous round: the family set is identical, so no newly-implicated gate went unrun. All 21 path-matched plus the 6 convention-triggered families exit 0; check:dispatcher-error-vocabulary exit 0 (no new SCREAMING_SNAKE code literal).
  • pnpm lint — full repo, exit 0.
  • Not measured (unchanged from the previous round): a bare node scripts/pm/check-half-states.mjs exits 3 with PREREQUISITE NOT MET — the anonymous API rate limit is exhausted for this egress IP. A transport refusal, not a red gate; the form CI runs (check:pm-half-states = --self-test) exits 0.

Ablation, targeting exactly what this patch adds: collapse the field vocabulary back onto the section base — reintroducing the defect — rebuild, and confirm the new pins go red. packages/metadata/vitest.config.ts carries no alias for @objectstack/metadata-core, so the door suite resolves it through dist/; both legs therefore rebuilt and both proved what reached dist/. Predicted direction was stated before running (the field-level current_user silence pins go red). Observed: door 2 failed | 7 passed, policy 4 failed | 22 passed, and every failure is a per-surface-split pin — no incidental collateral. Mutation confirmed on disk by counting the injected marker (1) and the replaced anchor (0) before the rebuild, then in dist/ by scripts/ablation-dist-preflight.mjs (present in 2 built files). Restore leg: git checkout HEAD -- PATH, git diff HEAD empty, restored blob byte-identical to the HEAD blob dcc5963d525c5769dc05835a30564ab16d11d8ad, rebuild, preflight --absent clean, both suites green again at 9/9 and 26/26.

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh could not take the shared verify lock on this host: no usable flock. The shared verify lock is declared Linux-only (flock is util-linux, and a stock macOS does not ship it), so the commands were run directly, without the lock — a declared narrowing, not a silent one. No serialization guarantee held for these runs, nor for any sibling agent in this container while they ran.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-core, @objectstack/metadata, touching 12 documentable anchor(s).

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json feab4bff9d0c01e0dbf7db52f5b6e57eb3b03f2b.

6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 11 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 feab4bff9d0c01e0dbf7db52f5b6e57eb3b03f2bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 1de1916065f64e3e3a1424b0d63a4c1b49af4e47 — the merge of head 330e4b85bf23470017a120ad44f3b9f9a5b20b42 into base feab4bff9d0c01e0dbf7db52f5b6e57eb3b03f2b, 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 1de1916065f64e3e3a1424b0d63a4c1b49af4e47 && git checkout 1de1916065f64e3e3a1424b0d63a4c1b49af4e47
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin feab4bff9d0c01e0dbf7db52f5b6e57eb3b03f2b 330e4b85bf23470017a120ad44f3b9f9a5b20b42 && git checkout -B drift-repro feab4bff9d0c01e0dbf7db52f5b6e57eb3b03f2b && git merge --no-ff 330e4b85bf23470017a120ad44f3b9f9a5b20b42
node scripts/docs-audit/affected-docs.mjs --json feab4bff9d0c01e0dbf7db52f5b6e57eb3b03f2b

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs feab4bff9d0c01e0dbf7db52f5b6e57eb3b03f2b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 28, 2026
@hotlong
hotlong marked this pull request as ready for review August 28, 2026 14:25
@hotlong
hotlong enabled auto-merge August 28, 2026 14:25
@hotlong
hotlong added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit 2852accAug 28, 2026
34 checks passed
@hotlong
hotlong deleted the claude/issue-12915-current-user-vocabulary branch August 28, 2026 14:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hotlong