Repository navigation
fix(lint)!: the object save door gives the build's field-rule-slot verdict (#22032 pass 2) - #22117
Conversation
…rdict runStackExpressionPasses no longer skips the field-rule slots on an object write: requiredWhen / readonlyWhen / conditionalRequired / visibleWhen run with their parent gate, the requiredWhen null guard and the reference traversal refusal, at the build's own position in the field walk. The per-option visibleWhen loop keeps its own fence (pass 3), as do the object's action predicates (pass 4). Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…the fence pin to passes 3-4 Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…real save, promotion and package publish doors Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…ect save door judges field-rule slots Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 4 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 214ae2a8506eee1873e1989e90ba316b3f0c8b10 && git checkout 214ae2a8506eee1873e1989e90ba316b3f0c8b10
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ace0a53852c4f34547eca8df973faa9fb8d68c97 ab17f41aa77ff026bffaed596c81f4ca6723d043 && git checkout -B drift-repro ace0a53852c4f34547eca8df973faa9fb8d68c97 && git merge --no-ff ab17f41aa77ff026bffaed596c81f4ca6723d043
node scripts/docs-audit/affected-docs.mjs --json ace0a53852c4f34547eca8df973faa9fb8d68c97
|
Contract reviewServed-tier: PR #22117 (card #22032, pass 2 of 4), head ① Derived judgments
② Semver level
③ Boundary flagsFrom the os-dev-report
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #22032
Clause-②: no (narrowing)
This is pass 2 of #22032: the field-rule slots. Passes 3 and 4 stay fenced, and the card stays open for them: option
visibleWhen, and the object's action predicates.What changes
The object save door gives the build's verdict on a field's rule slots.
formulas.mdxsays "the samevalidateExpressionvalidator backsos buildand metadata registration". After pass 1 the object door judged formula fields and validation-rule predicates, and fenced the field-rule slots off by name. So an object whose field carried a barerequiredWhen: 'amount > 1'(the card's measured body) still saved with a 200, whileos buildrefused it at error.runStackExpressionPasses(packages/lint/src/validate-expressions.ts) the field walk no longer starts withif (objectWrite) { judgeFieldFormula(fname, f); continue; }. On an object write the walk now runs, at the build's own position:requiredWhen,readonlyWhen,conditionalRequired,visibleWhen) asrecord-scoped predicates, with the root verdict;parentgate (areadonlyWhen/requiredWhenreadingparenton an object without exactly onemaster_detail);requiredWhen;requiredWhen/readonlyWhenread through a reference field;continuealso skipped the per-optionvisibleWhenloop, which sits inside the same iteration. That loop now reads(objectWrite ? [] : recordsOf(f.options)), the same guard shape as the flow, action, sharing-rule and hook loops. Deleting thecontinuealone would have lifted passes 2 and 3 together.validateStackExpressionsentry declaresruntimeTypes: ['flow', 'action', 'hook', 'object'](authoring-rules.ts), andruntimeAuthoringRulesFor('object')(runtime-gate.ts) dispatches it.runtime-gate.tsis untouched.StackExpressionOptions.runtimeWriteTypenow names three admitted passes and two fenced ones.AuthoringRuleContext.runtimeWriteTypeinauthoring-rules.ts, the one line that reaches a built.d.ts, names the field-rule-slot pass. The function-head comment, thejudgeFieldFormuladocblock, the registry entry's measurement comment and the object roster comment inruntime-gate.object-writes.test.tsmove with it.runAuthoringRules('build', …)give the same rule (expression-invalid), location (object 'fx_field' · field 'name' requiredWhen), path, message and hint. The pins compare these key by key.packages/metadata-protocol. Only its test file gains the door-level pins.Pins
packages/lint/src/runtime-gate.object-field-rule-writes.test.ts(new, 12 tests).requiredWhen, an unregistered function inreadonlyWhenand invisibleWhen, a bareconditionalRequired, the root verdict (current_user), theparentgate, therequiredWhennull guard, and the traversal refusal (record.account.name). Each is located at the slot and asserted on its named subject.parent-scoped detail with its master stored beside it, are clean at the door and at the build.packages/lint/src/runtime-gate.object-formula-writes.test.ts. The fenced sites are now passes 3 and 4 only (an optionvisibleWhen, an actionvisible). The lifted sites are the validation rule and therequiredWhen. The build flags all four; the object door flags the two lifted sites, in the build's order, andrunStackExpressionPasseson an object write returns exactly the build's findings for the admitted passes.packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the realsaveMetaItem,publishMetaItemandpublishPackageDrafts:requiredWhen, an unregistered function invisibleWhen, and aparentread with no master inreadonlyWhenare each refused on an active save with a 422INVALID_METADATAcarrying the build's located finding, and nothing lands;outcome: 'refused',failednaming the object withINVALID_METADATA, the row left a draft); the draft saves themselves still succeed;os buildgive the same finding on rule, where, path, message and hint.Reverse verification (one-off, from committed HEAD
ab17f41aa)scripts/ablation-replace.mjs --holdput the fence back at the head of the field loop:const ablationFence22032 = objectWrite; if (ablationFence22032) { judgeFieldFormula(fname, f); continue; }. The anchor was hit once, 1 to 0, and the blob went758396f8e4e4to0f007b9ed735. The script carriedtrap restore EXIT INT TERM.@objectstack/lintwas rebuilt, andablation-dist-preflightfound the marker in 4 built files.758396f8e4e4equals HEAD, andgit diff HEADis empty. Lint was rebuilt, and--absentfound the marker gone from all 14 built files with a clean tree. Both suites went green again: lint 29 of 29, and the protocol file 86 of 86.Measurements
*.object.tsunderpackages/**andexamples/**(111 files), plus the twoapp-multi-packagesub-stacks. That is 118 objects in 18 groups, at the raw shape and at theObjectSchema.parseshape, each with its own group as context.showcase_invoice4 (issued_on.requiredWhen,tax_rate.readonlyWhen,paid_on.requiredWhen,paid_on.visibleWhen),showcase_invoice_line4 (product,quantityandunit_pricereadonlyWhenreadparent;description.requiredWhen), andsys_permission_set.name.readonlyWhen.bafb58bb0: 0 build errors and 0 build warnings for the pass (raw, parsed, and throughrunAuthoringRules('build')), and 0 door findings.runRuntimeAuthoringRuleswith typeobject, at both shapes.conditionalRequiredcannot reach this gate through the save door. Measured through the realsaveMetaItemwith a scratch test that was deleted afterwards: the per-type spec step refuses it as a key retired in protocol 17 (422INVALID_METADATA, on a draft save and on a publish save) before the gate runs. The lint pin judges it because the build does.Clause-② (measured)
@objectstack/lintone doc comment moves (AuthoringRuleContext.runtimeWriteType).StackExpressionOptionsandrunStackExpressionPassesare not in the built declarations. No exported signature moves..changeset/22032-object-save-door-field-rule-slots.mdcovers@objectstack/lintand@objectstack/metadata-protocol:minor,fix(lint)!, BREAKING, with the remedy, and ADR-0087not-required (no-migration-prescription)..changeset/pre.jsonis absent onorigin/mainbafb58bb0, sominorwith the BREAKING banner, as pass 1.Tests and gates (all at
ab17f41aa)@objectstack/lint: 122 files, 5658 tests passed;typecheckexit 0, its test-typecheck included (--listFilesshows the three touched lint test files in thetsconfig.test.jsonprogram).@objectstack/metadata-protocol: 221 files passed and 3 skipped; 28234 tests passed and 19 skipped;typecheckexit 0 (--listFilesshows the door test file in the program).rest^...andobjectql^...closure.@objectstack/rest: everymeta-object-*file andmeta-publish-package-scope, 11 files, 197 tests passed.@objectstack/objectql:save-meta-response-conformance,publish-meta-response-conformanceandplugin.integration, 3 files, 67 tests passed.dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands at this head. All 63 end at exit 0, and--ranreconciles 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, with an exit code recorded for each.check-plugin-teardown-shape.mjs --self-testfirst exited 3: its pinned fixture commit was outside this shallow clone. Aftergit fetch --depth=1of that one commit it exited 0.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: no full build). Afterpnpm build(72 tasks, 71 cached) it exited 0.--no-inline-config --format json): 6 files, 0 errors and 0 warnings. Each file is matched byeslint.config.mjs(--print-config), none was ignored, and the config enables no type-aware linting (noparserOptions.project), so this diff cannot move the verdict on any untouched file.Acceptance notes
saveMetaItemwith a scratch test that was deleted afterwards. The stored detailfx_detailhas onemaster_detailtofx_master, and areadonlyWhenreadingparent.acct.name, whereacctis a lookup on the master. Re-saving the master with only its label changed answers 422, with the issue located atobject 'fx_detail' · field 'qty' readonlyWhen. Without the detail, the same save resolves.parentholder needs the master's field types.validateObjectFieldRefsonmaindoes the same: re-saving a master is refused for a stored detail whose lookup'slookupColumnsnames a column the master lacks.os buildand faults at runtime. The changeset states the consequence and the remedy. It is reported on the card for the seat.parent.REF.FIELDread only when the master is in the write's context; a detail saved without its master in the package closure gets no traversal verdict there, where the build, holding the whole stack, gives one. The door stays a subset of the build.then/otherwisepredicate gap (lint: a conditional validation rule's nestedthen/otherwisepredicate is never validated, soos buildpasses and the object save door stores a predicate the top-level rule would refuse #22042) is not addressed here; it is serial behind this pass.formulas.mdxcould name the object save door: a docs addition, not a false line.Generated by Claude Code