Uh oh!
There was an error while loading. Please reload this page.
feat(spec): assignment values may be a CEL value envelope — the expression ledger gains the value role - #15113
Conversation
…ontract, pins, docs, changeset Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…Schema; array-form prescription on the record type error Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…sion ledger gains the value role
FLOW_NODE_EXPRESSION_PATHS gains the assignment entry at assignments.* with the
new value role; ledger paths accept a * segment (every key of an object, the
sibling of []); resolveFlowNodeExpressions emits only envelope-shaped objects
for a value slot, so a plain string stays {token} interpolation and every
pre-existing entry resolves byte-identically. AssignmentConfigSchema /
AssignmentValueSchema / AssignmentExpressionValueSchema declare the assignment
node's value contract (ExpressionSchema, safeExtended to the cel dialect,
beside interpolation and literals); a malformed envelope is refused at the
variable's path with a fixed leading sentence; the map value carries
xExpression: value as the ledger's declaration channel. Docs page, generated
artifacts, and a spec minor changeset. The executor half (evaluation,
validateExpression value checks, the reconciliation ratchet's marker table)
is service-automation's and is reported, not edited.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i…R-0090 reserved word in the flows page Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…ledger ratchet; ratchet learns the value marker and the additionalProperties walk Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…-declared node config map Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…signment-value-role
…rged tree The os-regen merge driver deferred docs/audits/2026-07-unknown-key-strictness-ledger.counts.md (changed on both sides since the merge base); regenerated with pnpm --filter @objectstack/spec gen:strictness-ledger on the merged tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
📓 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 — 128 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 3846d99734cadb1d55c8684a0314db977d5cb752 && git checkout 3846d99734cadb1d55c8684a0314db977d5cb752
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f594e70d796cbdd8e751c30105ede566acfec584 d25a6606530ba3051129d8efce9c66a41d4be83c && git checkout -B drift-repro f594e70d796cbdd8e751c30105ede566acfec584 && git merge --no-ff d25a6606530ba3051129d8efce9c66a41d4be83c
node scripts/docs-audit/affected-docs.mjs --json f594e70d796cbdd8e751c30105ede566acfec584
|
Contract review (Clause ②) — PASS · ACCEPTReviewer of record: the Head and window (readings at 2026-09-04T02:09Z)
The widening, verified in the diff
Report checklist
Follow-up cards this seat files at landing
LandingAt all-green on
Generated by Claude Code |
Landing provenance — Landing window readings, all on head
Sequence: ready → squash auto-merge → Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14149
Clause ②: yes — path limb: every edited source path is under
packages/spec/src/**(automation/flow-node-expression-paths.ts,automation/builtin-node-config.zod.ts,automation/node-executor.zod.tsplus their pins;node scripts/pm/dispatch-gates.mjs --tierat the final head: "no path-derived mandate", each path ⇢ the contract surface); content limb yes: a published node schema widens what it accepts — anassignmentvalue may now be a CEL value envelope beside{token}interpolation — and the exportedFlowNodeExpressionRoleunion widens byvalue.Maintainer ruling 2026-09-02 (card comment 5507504961, verbatim reply 「同意」 to recommendation A): the rendering half only —
FLOW_NODE_EXPRESSION_PATHSgains a value role for theassignmentnode, no new vocabulary, the grouping half deferred,scriptstays the workaround until A lands. This PR is the SPEC half of that ruling, exactly as dispatched by thedomain:specseat (card comment 5532987091 / 5533002333), plus the ONE file the seat widened the surface by in its ruling on the first report's blocker — card comment 5533840057 (2026-09-04T00:18Z, option A with A2, same PR): the ledger's reconciliation ratchetpackages/services/service-automation/src/builtin/config-expression-ledger.test.ts. Nothing else underpackages/services/service-automation/src/**is touched; the executor half is reported below, not implemented. Final head after the last commit:d25a6606—origin/main(f594e70d) merged in by the merge commit7ed4fb54(scripts/pm/os-regen-merge.sh, no rebase, no amend, no force-push) and the one artifact changed on both sides since the merge base, the generateddocs/audits/2026-07-unknown-key-strictness-ledger.counts.md, regenerated from the merged tree withpnpm --filter @objectstack/spec gen:strictness-ledgerind25a6606(the os-regen marker cleared on that commit). The commits afterd902edfdkeep theirwip/choretitles — no history rewrite.What changed
packages/spec/src/automation/flow-node-expression-paths.tsFlowNodeExpressionRolegains'value', with a docblock: a slot whose authored value may be a{ dialect: 'cel', source }expression envelope evaluated by the expression engine to the value the variable takes — not a predicate, not a template; the slot's SHAPE decides the dialect (a plain string stays{token}interpolation, an envelope is CEL).FLOW_NODE_EXPRESSION_PATHSgains one entry:{ nodeType: 'assignment', path: 'assignments.*', role: 'value', label: 'assignment value' }.*segment means "every own key of this object" — the sibling of[]("every element of this array"). The resolver understood only[]before, so this is the smallest extension: one new branch inwalk()guarded byhead === '*'; every existing entry resolves byte-identically because no pre-existing path carries*and no pre-existing entry has thevaluerole (pinned with the ratchet's own fixtures restated inflow-node-expression-paths.test.ts).resolveFlowNodeExpressionsemits, for avalueentry, only envelope-shaped objects; for the two existing roles the rule is unchanged (non-empty strings).isExpressionEnvelopeShaped(a plain object with a stringdialect) is exported as the one recognizer both halves discriminate on — deliberately looser than "a valid envelope", so{ dialect: 'cel' }with nosourcereaches the validator and is refused there instead of being stored as a literal.AssignmentConfigSchemais exposed through the JSON mapgetSchemalessNodeConfigJsonSchemas()— the walk the ratchet already performs — by a NEW sibling mapLEDGER_DECLARED_NODE_CONFIG_SCHEMASinschemaless-node-config.zod.ts, deliberately not by addingassignmenttoSCHEMALESS_NODE_CONFIG_SCHEMAS: that const means "publishes no descriptor" andpackages/metadata-protocol/src/reference-sites.ts:480walks it for exactly that reason (limb 2 of reference-site attribution), so its key set and its readers stay untouched. Exported typesLedgerDeclaredNodeType/ReconciledNodeConfigType; the function's return type widens accordingly. Pinned spec-side:SCHEMALESS_NODE_CONFIG_SCHEMASkeys are still exactlydecision / script / subflow,LEDGER_DECLARED_NODE_CONFIG_SCHEMASkeys are exactlyassignment, and the projectedassignmentJSON carriesproperties.assignments.additionalProperties.xExpression === 'value'.assignments.*and only that: theassignmentnode's canonical config is theassignmentsmap its descriptor (logic-nodes.ts,additionalProperties: true) and the Studio keyValue editor declare, and every bundled example writes. The two legacy shapes the executor also normalizes — the bare{ variable: value }config and theassignments: [{ variable, value }]array — are read-compatibility the descriptor does not declare and the form↔Zod ledger pins as "not offered for new authoring"; their values keep today's meaning (an envelope-shaped object there is the literal it always was). Declaring a top-level*entry would also misreadconfig.assignmentsitself as a value slot whenever an author names a variabledialect.packages/spec/src/automation/builtin-node-config.zod.tsAssignmentExpressionValueSchema=ExpressionSchema.safeExtend({ dialect: z.literal('cel') })— the canonical{ dialect, source, ast?, meta? }envelope fromshared/expression.zod.ts(the spellingvalidateExpressionalready reads:packages/formula/src/validate.ts,toSource()), with its ownsource-or-astrefinement kept and the dialect narrowed at the type level. Not a second spelling..extend()is refused by Zod on a refined object;safeExtendis the sanctioned form — measured.AssignmentValueSchema—z.unknown()refined: anything that is not envelope-shaped passes untouched (strings, scalars, arrays, plain objects — the preservation half); an envelope-shaped value must parse as the CEL envelope, else acustomissue at the value's path whose message starts with the fixed sentenceASSIGNMENT_VALUE_ENVELOPE_REFUSAL. Carries.meta({ xExpression: 'value' })— the ledger's declaration channel for this slot; measured to ridez.toJSONSchemaonto the map'sadditionalProperties(pinned). Built eagerly rather than throughlazySchema:.meta()registers by schema identity and the lazy Proxy is not that identity, so a lazily wrapped marker never reaches the JSON Schema (measured; the sibling markers all sit on eager inner schemas).AssignmentConfigSchema—{ assignments?: Record(name → AssignmentValueSchema) }with an open catchall for the bare legacy keys; the array form is refused as a type error on the record carryingASSIGNMENT_ARRAY_FORM_PRESCRIPTION(an object-level refinement never runs once a property failed its type — Zod aborts the object; measured). Plus the ADR-0122…Parsedaliases.assignmentmoves from "deliberately absent" to "described by its VALUES, not by a key set".packages/spec/src/automation/node-executor.zod.ts— theconfigSchemadocblock names the third role and its declaration channel.flow-node-expression-paths.test.ts(new, 17 cases) and a newdescribeinbuiltin-node-config.test.ts(10 cases): the entry and role; resolution on a two-variable fixture (owner_name: '{manager.name}',digest: envelope) yields exactlyassignments.digestwith the envelope verbatim;{token}strings and literals skipped; a malformed envelope still resolved; legacy shapes resolve nothing; the ratchet'sconditionpin kept; every pre-existing entry byte-identical; envelope accepted (the ruling'sjoinNonEmpty(overdue_tasks.map(t, t.subject), "\n")source, verbatim); interpolation and every other literal still accepted; malformed envelopes refused withcode+path+ the leading sentence (nosource, emptysource, non-stringsource, unknown /template/crondialect); the array form's prescription; thexExpression: 'value'marker in the JSON Schema.content/docs/automation/flows.mdxgains an "Assignment (set variables)" example with both forms and the digest, plus a warn callout stating the runtime status honestly (the executor half has not landed; ascriptnode is the workaround today). Generated:content/docs/references/automation/builtin-node-config.mdx,api-surface/,export-origins/,declaration-map/,authorable-surface/,json-schema.manifest/(automation shards), the strictness ledger counts..changeset/assignment-value-role-cel-envelope.md—@objectstack/specminor.Measured, not edited
validateExpression's roles live inpackages/formula/src/validate.ts:41—FieldRole = 'predicate' | 'value' | 'template'— so avaluerole already exists there (line 553 onward: "predicate | value → CEL"). Nothing inpackages/specholds a role table; the ledger'sFlowNodeExpressionRoleis the declared-and-pinned side. Probed on the built engine at this head:validateExpression('value', { dialect: 'cel', source: DIGEST })→ok: true; atemplateenvelope → "expected a CEL expression but got atemplatedialect.";celEngine.evaluate({ dialect: 'cel', source: DIGEST }, { extra: vars, record: vars })→"Renew contract\nCall ACME"for three rows with one empty subject — themapmacro andjoinNonEmptyboth work, so the ruling's example is a real digest body. One seam for the executor half:validateExpression('value', { dialect: 'cel' })(nosource) answersok: true— a missing source reads as "not authored" there — soAssignmentConfigSchemais the only gate that refuses{ dialect: 'cel' }, and the executor half must parse the config against it (theparseNodeConfigseam) before validating.pnpm --filter @objectstack/spec check:livenessgreen; the gate is registry-rooted over metadata types at one-level granularity andflow.nodes.configcarries a single blanketliveverdict (liveness/flow.json,props.nodes.children.config), so node-config values are not walked and noplannedentry is owed; the executor pointer sits in the role's docblock.@objectstack/linttypecheck→ 0 andsrc/validate-expressions.test.tsgreen (its ledger consumer guards withrole !== 'predicate', not an exhaustive switch).@objectstack/service-automationhas NOtypecheckscript (ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT);tsc --noEmit -p tsconfig.jsonin the package → 3 errors, allsrc/nested-region-parity.test.ts(Property 'flows' is private), byte-identical onorigin/main, none naming the role union — pre-existing test-layer debt, not this change.The widening — seat ruling 5533840057, one test file
packages/services/service-automation/src/builtin/config-expression-ledger.test.ts(the #4027 ratchet) is the only edit outside the declared surface, on the ruled four-condition adjacent-fix path. What it gains, and what it does not:ROLE_BY_MARKER.value = 'value'— a third row besideexpression → predicateandtemplate → flow-template, with the docblock stating that it ISvalidateExpression'svaluerole and what makes it a distinct ledger role (the slot's shape rule).collectExpressionPropsdescends an object-valuedadditionalPropertiesas a*segment — the sibling ofitems→[], the same spelling the spec resolver gained. A bareadditionalProperties: true(theassignmentDESCRIPTOR's own shape) declares nothing, so the descriptor channel and the spec channel cannot double-declare the slot.assignment.assignments.*is in the ledger with rolevalue, reaches the ratchet through the schemaless-config JSON channel specifically and NOT through the descriptor channel, and the marker is mapped (ROLE_BY_MARKER.value === 'value'). The existing pins are untouched, including the line-270 pin (assignmentwith a bareconditionresolves nothing).@objectstack/spec/automationthroughdist/, so each leg rebuilt spec withOS_SKIP_DTS=1and proved the mutation reacheddist/withscripts/ablation-dist-preflight.mjs; source restored bygit checkout HEAD -- ABS_PATHand proven by blob hash + emptygit diff HEAD; a final FULL build restoreddist/):assignmentremoved fromLEDGER_DECLARED_NODE_CONFIG_SCHEMAS(source occurrences 0, dist preflight--absentexit 0) →Tests 2 failed | 10 passed (12):the ledger carries no path a builtin no longer declareswith the verdictstale ledger entries — no descriptor or schemaless schema declares these: expected [ 'assignment.assignments.* (value)' ] to deeply equal [], plus the new positive pin;xExpression: 'value'→'valeu'on the map value (injected count 1, dist preflight present exit 0) →Tests 5 failed | 7 passed (12):every xExpression property a builtin declares is in the ledger, the schemaless-channel derivation, the stale check, the 表达式 ledger 只认 descriptor configSchema,所以「刻意 schemaless」的节点上声明的 CEL 槽位没有任何 build-time 校验器 #4439 decision pin and the new pin, with the verdictassignment.assignments.* declares an unknown xExpression marker 'valeu' — add it to ROLE_BY_MARKER and teach the validators which dialect it takes;Tests 12 passed (12).d902edfd(first report) the file ranTest Files 1 failed (1)/Tests 1 failed | 10 passed (11)— the stale-entries assertion; at11e61c06it runsTest Files 1 passed (1)/Tests 12 passed (12)(the full file, every describe).service-automationchangeset: a test-only edit publishes no bytes. The specminorchangeset stays.Executor half (read-and-report,
packages/services/service-automation)src/engine.ts:6900-6901(invalidateFlowExpressions, the ledger consumer):for (const found of resolveFlowNodeExpressions(node.type, node.config)) { if (found.entry.role !== 'predicate') continue; …— every non-predicateslot is resolved and skipped, so the newvalueentry is walked and ignored atregisterFlow; nothing callsvalidateExpression('value', …).src/builtin/logic-nodes.ts:112-137(theassignmentexecutor): normalizes the three shapes to pairs and runsvariables.set(key, interpolate(value, variables, context))(line 135).src/builtin/template.ts:346-368(interpolate): for an object it recurses into every property and returns a copy — so an envelope{ dialect: 'cel', source: '…' }is written into the variable VERBATIM as a literal object (itssourcestring gets{token}interpolation on the way, so a CEL map literal inside it would even be mangled), never evaluated;notify-node.ts:259-260then renders{digest}throughstringifyForTemplate→ the JSON text of the envelope. The executor half therefore needs: evaluate envelope-shaped values in the canonical map through the expression engine ({ extra: vars, record: vars }is the context shapeengine.ts:7757already uses), parse the config againstAssignmentConfigSchemaat execute time, checkvalueslots withvalidateExpression('value', …)atregisterFlow(and the lint passpackages/lint/src/validate-expressions.ts:1138, another lane), and extend the ratchet as above.Studio form (read-and-report, objectui
origin/maine304a4ef)packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:475-479:assignment: [cfg('assignments', 'Assignments', 'keyValue', …)]— the descriptor's free-form map rendered byFlowKeyValueField.tsx.FlowKeyValueField.tsx:47-63parseValue: a value cell that starts with{and ends with}isJSON.parsed on commit (arrays likewise), non-JSON falls through to a plain string, andtoRawrenders a stored object back as its JSON text — so the form ALREADY accepts a non-string value and an author CAN type{"dialect":"cel","source":"joinNonEmpty(...)"}and have it stored as the envelope object. What it lacks is an affordance: no per-row "CEL value" toggle or CEL editor, novalidateExpression('value', …)feedback (theFlowExprIssueit imports is for template strings), and the simulator (previews/simulator/flow-simulator.ts:352-360,interpolateValue) returns a non-string value as-is, mirroring the runtime's verbatim write.json-schema-to-fields.ts:243-254readsxExpressiononly on string properties ('expression' | 'template'), so the new marker onadditionalPropertieschanges no editor and breaks nothing. Verdict: usable without an editor change; a consumer-side card is warranted for the affordance (a per-row envelope toggle + value-role validation + simulator evaluation), not required for the contract.Verification record at the merged head
d25a6606Exits captured by redirect; verdict lines the gates' own.
pnpm --filter @objectstack/spec check:generated --fixon the merged tree regenerated nothing (✓ All 15 generated artifacts are up to date.), so the head did not move after the ledger commit.packages/services/service-automation/src/builtin/config-expression-ledger.test.ts→Test Files 1 passed (1)/Tests 12 passed (12).Test Files 3 passed (3)/Tests 87 passed (87).pnpm --filter @objectstack/spec build→ exit 0;pnpm --filter @objectstack/spec typecheck→ exit 0.check:generated→✓ All 15 generated artifacts are up to date.;check:strictness-ledger→✓ docs/audits/2026-07-unknown-key-strictness-ledger.counts.md is current — 440 site(s) measured, 1 authorable strip site(s) left.node scripts/check-system-context-census.mjs→OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.assignments: [{ variable, value }]array-form refusal noted by the director's audit, card comment 5534406255, is left for the executor-half card the seat files at ACCEPT).Verification record at the pre-merge head
11e61c06Every exit captured by redirect before any read; the verdict lines quoted are the gates' own. Order of events at this head: the union chunks and the locked spec runs first on the quiescent committed tree; then the two dist-resolved ratchet legs above (which rebuild spec); then a final full
pnpm --filter @objectstack/spec build(exit 0) and, on that rebuilt dist, the dist-reading gates again.Test Files 3 passed (3)/Tests 87 passed (87).Test Files 1 passed (1)/Tests 12 passed (12).pnpm --filter @objectstack/spec build(the final full build, after the legs) → exit 0;pnpm --filter @objectstack/spec typecheck(after it) → exit 0;pnpm --filter @objectstack/lint typecheck→ exit 0.check:generatedafter the final rebuild →✓ All 15 generated artifacts are up to date.(check:generated --fixbefore the last commit regeneratedapi-surface/andexport-origins/for the three new automation exports and nothing else).Test Files 467 passed (467)/Tests 12463 passed (12463).node scripts/check-system-context-census.mjsafter the last edit →OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.pnpm exec eslint --no-inline-configover the seven edited TypeScript files (six spec, the ratchet) → exit 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsat this head: 92 commands (the 90 derived at the previous head plusnode scripts/check-tenant-audit-census.mjsand its--self-test, both exit 0). Run one by one on the quiescent tree: 89 exit 0 —check:skill-examplesafter building the@objectstack/client-reactclosure it reads (✅ 256 prose examples type-check across 3 surface(s)); 3 answer NOT MEASURED by their own verdict —node scripts/check-test-completeness.mjs(exit 3,PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named),pnpm check:dual-build-cjs-loads(exit 3,PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/),pnpm check:type-check-debt(exit 3,PREREQUISITE NOT MET— the whole-workspace built closure is CI's;check:type-check-coverageinside it reportsOK — 72/79 workspace packages type-checked). After the final rebuild the dist-reading members were re-run and are 0 again:check:generated,check:api-surface,check:export-origins,check:docs,check:authorable-surface,check:liveness,check:exported-any,check:dual-source-exports,check:entry-nameability,check:browser-reachable-entries,check:strictness-ledger,check:role-word,check:spec-parsed-alias.Record at the previous head
d902edfd(the two files the reverse-verification legs below mutate changed only in comment lines since —git diff d902edfd 11e61c06on them shows no code line — so those legs stand):Test Files 3 passed (3)/Tests 86 passed (86)(87 at11e61c06, one new channel pin).pnpm --filter @objectstack/spec build→ exit 0 (DTS emitted;check-dts-emittedandcheck-dev-prereqs --stamppassed inside it).pnpm --filter @objectstack/spec typecheck→ exit 0 (tsc, scripts program, test program — the test files are insidetsconfig.test.json'ssrc/**include, so the new pins are type-checked).pnpm --filter @objectstack/spec check:generated --fixthencheck:generated→✓ All 15 generated artifacts are up to date.vitest run --maxWorkers=2under the shared verify lock:Test Files 467 passed (467)/Tests 12462 passed (12462).check:authorable-surface,check:liveness,check:docs,check:api-surface,check:export-origins,check:strictness-ledger→ exit 0 (inside the union below).pnpm exec eslint --no-inline-configover the five edited TypeScript files → exit 0.node scripts/check-system-context-census.mjsafter the last edit →OK — 106 elevation read sites in 20 packages across 45 files, all anchored.pnpm check:role-word→ 0 andpnpm check:spec-parsed-alias→OKafter the two findings they raised on an earlier head were fixed ind902edfd(ADR-0090 reserved word in the flows page; ADR-0122…Parsedaliases).node scripts/pm/dispatch-gates.mjs --commands(no paths passed; derived from the merge base at this head): 90 commands run one by one; 87 exit 0 (check:skill-examplesafter building the@objectstack/client-reactclosure it reads:✅ 256 prose examples type-check across 3 surface(s)); 3 answer NOT MEASURED by their own verdict —node scripts/check-test-completeness.mjs(exit 3,PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named),pnpm check:dual-build-cjs-loads(exit 3,PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/),pnpm check:type-check-debt(exit 3,PREREQUISITE NOT MET— the whole-workspace built closure is CI's;check:type-check-coverageinside it reportsOK — 72/79 workspace packages type-checked).Reverse verification (from the committed state, restore by blob hash)
Script with
trap restore EXIT INT TERM, absolute paths undergit rev-parse --show-toplevel; the pins import the modules under test by relative source specifier, so nodist/is involved and a mutation is live the moment it is on disk.assignmentledger entry deleted: on-disk proofgrep -c "path: 'assignments.*'"= 0 (expected 0),git diff --stat1 file / 23 deletions → pins6 failed | 53 passed (59)(the entry, the role union, the two-variable resolution, the malformed-envelope resolution, the authoring-order resolution, the five-entry ledger pin). Restore:git checkout HEAD -- FILE;git hash-object=500b8a57…=HEAD:blob;git diff HEADempty.if (result.success) return;arm replaced by an unconditional refusal; injected marker count 1, original anchor count 0,git diff --stat1 file / +1 −1) → pins3 failed | 56 passed (59)(the accept pin with the ruling's example, the two-forms pin, the "wherever it sits" pin whose green key now reds). Restore proven the same way: blob8a5c42d2…=HEAD:;git diff HEADempty.59 passed (59).Notes for review
check-dev-prereqs.mjsat this head still names 32 unbuilt app packages — the dev-server precondition, not a gate in the union.packages/services/service-automation/src/**outside the ratchet test (logic-nodes.tsincluded — the descriptor keepsadditionalProperties: trueand the form↔Zod ledger's pin on it is untouched),packages/formula/src/validate.ts, objectui.🤖 Generated with Claude Code
https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
Generated by Claude Code