Uh oh!
There was an error while loading. Please reload this page.
feat(metadata-core,metadata): warn when a pre-current-era artifact carries fault-open form-view predicates - #12989
Conversation
📓 Docs Drift CheckThis PR changes 2 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 890d15a88e8c8558801b0af3aae733007c1a4bf8 && git checkout 890d15a88e8c8558801b0af3aae733007c1a4bf8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2b4178aa53ca62089f43e2cfae0b7838cf340dd1 8ee7e0793a67e9c4a57210cfe84d698bd5bc5964 && git checkout -B drift-repro 2b4178aa53ca62089f43e2cfae0b7838cf340dd1 && git merge --no-ff 8ee7e0793a67e9c4a57210cfe84d698bd5bc5964
node scripts/docs-audit/affected-docs.mjs --json 2b4178aa53ca62089f43e2cfae0b7838cf340dd1
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #12915 — scope C only. The card stays open: scope A (the expression-rewriting ADR-0087 conversion) is deferred by the maintainer ruling recorded on the card and is not implemented here.
What this does
A form-view predicate binds
record(+previous,parent) in runtime record forms, ordatain metadata-editing forms. The contract states the failure mode beside the vocabulary (packages/spec/src/ui/view.zod.ts,FormFieldSchema.visibleWhen/FormSectionSchema.visibleWhen): a bare identifier is unbound, the predicate faults, andvisibleWhen's fault fallback istrue— so a field the predicate was authored to hide renders for everyone.That is quiet alone and lethal in combination with the authoring pattern it serves. On the reported deployment an artifact built by released
@objectstack/cli17.1.0 authors{ field: 'disqualification_reason', required: true, visibleWhen: 'status == "unqualified"' }— the era's working spelling. On a 17.2 runtime the predicate faults open, the conditionally hidden field renders, and its unconditionalrequired: trueblocks every record creation through the console, while the same payload POSTs 201 through REST. Nothing refused and nothing logged, so the operator — the only person who can rebuild the artifact — had no signal at all.The framework artifact door now emits one deduped
warnline per artifact naming the authoredengines.protocolfloor and the runtime spec version, how many predicates on which views (first path as anchor), the fault-open consequence, and the remedy (os build).packages/metadata-core/src/form-predicate-root-policy.ts— the detection policy, exported (detectUnboundFormViewPredicateRoots,BOUND_FORM_VIEW_PREDICATE_ROOTS) so a composed door can reuse one policy rather than fork it.packages/metadata/src/plugin.ts— wiring in_convertArtifactForward, reusing the existingartifactConversionWarneddedupe set so the HMR replay does not re-announce.No behaviour change: no refusal, no rewrite, no
packages/specorpackages/metadata-protocoledit. A pin asserts the registered view still carries the authored predicate sources verbatim, bare root and all.Scoped to legacy artifacts by construction. The notice fires only inside the versioned window the forward conversion already opens, and it reads that pass's own
verdictrather than recomputing the comparison, so the two can never disagree about which artifacts are old. Verified against the window code rather than assumed:converted-forward(declared floor below the running spec) andconverted-undeclared(no declared range, grandfathered as data at rest) open it;authored-currentandruntime-version-unknowndo not. An artifact declaring the current or a newer floor gets zero notices even when it carries bare roots.Precision — the notice must never cry wolf
A notice that fires on a healthy current artifact is worse than no notice, so every judgement call resolves toward silence: string literals are stripped before the scan; only root position counts (
record.statusnames one root); a name followed by(is a call, not a scope root; a comprehension macro (.exists(,.all(,.map(,.filter(,.exists_one() binds its own iteration variable and is therefore skipped whole; AST-only envelopes pass.A tokenizer rather than a CEL parse is the established shape for this exact question here — the spec's own enforced
checkFormViewPredicateFeaturesRootscans the source string the same way. A real CEL parse lives inpackages/formula, whichmetadata-corecannot reach without breaking its spec-only dependency edge; spec itself exposes no parse. Measured, not assumed.One premise correction
The dispatch brief expected the form-view predicate key set to be
visibleWhen/requiredWhen/readonlyWhen/visibleOn. Read off the schema instead: a form-view field entry and a section each carry exactlyvisibleWhenplus the deprecatedvisibleOnalias.requiredWhen/readonlyWhenare declared on the OBJECT field schema (packages/spec/src/data/field.zod.ts), a different surface with its own binding scope, and are not scanned here. Both keys are read because this scan runs before the parse that folds the alias — same reason the legacygroupssection bucket is read alongsidesections.Per-option
visibleWhenis deliberately out of scope: options are evaluated by a different evaluator that also bindscurrent_user, so this vocabulary would be the wrong yardstick there.Verification
Union re-run on the final commit
8ee7e0793.pnpm --filter @objectstack/metadata-core --filter @objectstack/metadata test— 15/15 files, 255/255 tests; 37/37 files, 672/672 tests.pnpm --filter @objectstack/metadata-core typecheck— exit 0.--listFilesconfirmstsconfig.test.jsonreally reaches both new files (1 hit each), so the green is not a green over source nothing read.packages/metadatadeclares notypecheckscript — it is a DEBT ledger entry whose dts build is the type gate.pnpm --filter @objectstack/metadata buildexit 0, andcheck-type-check-coverage --re-measureholds its entry at exactly its recorded 89 (surplus: none — every entry sits exactly at its measurement), which is what covers the new test file there.node scripts/check-type-check-coverage.mjs --re-measureunderNODE_OPTIONS=--max-old-space-size=4096—OK — 31 ledger entr(ies) re-measured, 1570 raw tsc error(s) total, none above its recorded number.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(not from a hand-built path list): all 21 path-matched families plus the 6 convention-triggered ones (two new test files) exit 0, includingcheck:cross-package-test-inputs,check:engine-double-contract,check:where-matcher,check:stack-collection-maps,check:durability-log-level,check:test-source-alias,check:nul-bytes.check:dispatcher-error-vocabularyalso exit 0 (no new SCREAMING_SNAKE code literal is introduced).pnpm lint— full repo,eslint . --no-inline-config, exit 0. No narrowing claimed on this axis.node scripts/pm/check-half-states.mjsexited 3 withPREREQUISITE NOT MET — the anonymous API rate limit is exhausted for this egress IP. That is a transport refusal, not a red gate; the form CI runs (check:pm-half-states=--self-test) exits 0.Ablation.
packages/metadata/vitest.config.tscarries no alias for@objectstack/metadata-core, so the door suite resolves it throughdist/— both legs therefore rebuilt and proved what reacheddist/. Predicted direction stated before running: the two notice-asserting pins go red, the five silence pins stay green. Observed exactly that —2 failed | 5 passed, both failuresexpected [] to have a length of 1. The mutation was confirmed on disk by counting the injected marker (1) and the anchor (still 1) before the rebuild, then byscripts/ablation-dist-preflight.mjsreporting it present in 2 built files. Restore leg:git checkout HEAD -- PATH,git diff HEADempty, restored blob byte-identical to the HEAD blob (0d828bb8f3fe8d36ca2e3bb6b4885e5ab522e991), rebuild, preflight--absentclean, suite green again at 7/7.A first attempt at this ablation was a void run and is reported rather than quietly retried: the marker was written as a comment, esbuild stripped it, and it reached only the sourcemap — the preflight refused to count it and the run was discarded before any conclusion was drawn from it.
Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usableflock. The shared verify lock is declared Linux-only (flockis 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