Uh oh!
There was an error while loading. Please reload this page.
refactor(service-automation): take the trigger kind from spec's shared resolver - #14994
Conversation
…d resolver (#14328) `AutomationEngine.resolveTriggerBinding` kept a private copy of the chain that decides which trigger kind a flow asks for, in parallel with `@objectstack/spec`'s `resolveFlowTriggerKind` — the authoring-time mirror that `defineStack`'s trigger-capability refusal and `@objectstack/lint`'s `validate-flow-trigger-readiness` already read. Nothing pinned the copies together. The engine now takes the kind from the shared resolver and keeps only the per-kind binding construction. The array-form `triggerType` divergence is PRESERVED as an explicit pre-check before the resolver call: spec answers no kind for it on purpose, and the engine routes it to the record-change trigger only so that trigger can refuse it loudly at bind time (#3457/#3481). Its ordering is load-bearing — array form outranks `timeRelative`, which the resolver would otherwise answer for a start node carrying both. Two new guards close the drift: the per-kind switch is exhaustive over `FlowTriggerKind` with a `never` default, so a kind added to spec fails this package's type-check; and new pins assert every kind in `FLOW_TRIGGER_KINDS` is reachable through the real engine, plus the preserved divergence end-to-end against the real record-change trigger's refusal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…igger-kind-resolver
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 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 b752853db28bfead3687cd69fe5117cc5f415669 && git checkout b752853db28bfead3687cd69fe5117cc5f415669
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55eac1e1304fc187d837e69a0ef459422904a0c3 b7cffa0ec82e354157d793827dd991f3eb3cbe52 && git checkout -B drift-repro 55eac1e1304fc187d837e69a0ef459422904a0c3 && git merge --no-ff b7cffa0ec82e354157d793827dd991f3eb3cbe52
node scripts/docs-audit/affected-docs.mjs --json 55eac1e1304fc187d837e69a0ef459422904a0c3
|
os-dev-report {
"issue": 14328,
"status": "done",
"branch": "claude/issue-14328-trigger-kind-resolver",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14994",
"premise_still_valid": true,
"summary": "AutomationEngine.resolveTriggerBinding now takes its trigger KIND from spec's resolveFlowTriggerKind and keeps only the per-kind binding construction; the array-form triggerType divergence is PRESERVED as an explicit pre-check BEFORE the resolver call, and its ordering is pinned (array form outranks timeRelative, which the resolver would otherwise answer for a start node carrying both). Two drift guards added: the per-kind switch is exhaustive over FlowTriggerKind with a never default (a kind added to spec fails this package's type-check), and a case asserts every kind in FLOW_TRIGGER_KINDS is reachable through the real engine. Clause-2 re-derived from my own diff as 'no' and stated on its own line in the PR body: no exported symbol, no payload key, no accept-set change -- and measured, not argued, by ablation Leg A (reverting the unification reddened NOTHING, so getTriggerBindingAudit and the boot banner report the same kind for every flow, array form included). All five falsifiers checked and none fired: no spec edit needed; resolveFlowTriggerKind confirmed importable from the built @objectstack/spec/automation subpath; the two chains agree on every string-form flow (Leg A is the measurement); the array divergence is fully compatible with taking the kind from the resolver; nobody had wired it in. Three process notes, reported rather than smoothed: (1) I read AGENTS.md AFTER my first file edit, not before -- nothing in it was violated, but the ordering was wrong; (2) my ablation Leg C2 prediction was WRONG and I report it as such, with the diagnosis (see 'tests'); (3) the dispatch said to post this report as a PR comment while the standing os-dev contract says the card comment is the authoritative copy, so I posted it to BOTH (issue 14328 and PR 14994) rather than silently picking a side. Merged origin/main into the branch before deriving gates, because dispatch-gates.mjs reported STALE TREE and named the two files it had read stale copies of.",
"tests": "All on final head 39fcadf65, every exit code captured by redirect-then-read (never across a pipe).\nSUITES: pnpm --filter '@objectstack/service-automation^...' build (closure first -- service-automation resolves @objectstack/spec through exports to dist/), then pnpm --filter service-automation --filter trigger-record-change test => 'Test Files 103 passed (103) / Tests 1223 passed (1223)' and 'Test Files 8 passed (8) / Tests 81 passed (81)'; os-verify-lock VERDICT command-exit 0.\nNEW PINS: 14 cases in packages/services/service-automation/src/flow-trigger-kind-shared-resolver.test.ts (7 precedence cases each asserting a LITERAL kind through the real engine, plus the audit surface and a FLOW_TRIGGER_KINDS reachability case), and 3 in packages/triggers/trigger-record-change/src/array-form-refusal-end-to-end.test.ts (engine -> real RecordChangeTrigger -> the refusal asserted by flow name, 'ARRAY', 'NOT bound / never fire', the record-after-write steer and the 3457 citation, zero hooks registered, plus an anti-vacuity control).\nGATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands => 41 commands, re-derived after the merge commit. 38 exited 0. 3 exited 3 and are NOT MEASURED, quoted from their own verdict text in the PR body: check-test-completeness ('Nothing was measured: this gate exited before parsing a single summary line ... It is NOT a finding'), check:dual-build-cjs-loads ('PREREQUISITE NOT MET -- this gate reads built output, and some package has no dist/ ... This is NOT a pass: nothing was measured'; its own self-test passed first, 93 cases), check:type-check-debt ('This is NOT a pass and NOT a finding: nothing was measured'; check:type-check-coverage exited 0 beside it). No gate exited 1.\nABLATION -- 4 legs, directions written down BEFORE running, driver carried trap ... EXIT INT TERM with absolute paths, and each leg rebuilt what it mutated:\n LEG A (revert the unification, engine.ts -> merge base): predicted NOTHING reddens and said in advance that this leg does NOT discriminate. Measured: 19/19 + 3/3 green, as predicted. This is the Clause-2 measurement.\n LEG B (delete the array-form pre-check): predicted a SPECIFIC red set. Measured exactly it -- '4 failed | 15 passed' in service-automation (3 of my array cases + the existing trigger-dispatch-observability array case) and '2 failed | 1 passed' end-to-end; all 7 precedence cases, the reachability case and the anti-vacuity control stayed GREEN. Discriminating.\n LEG C1 (spec's resolver loses its api branch, spec rebuilt; unified engine): predicted red on the api cases only. Measured '3 failed | 11 passed' -- both api precedence cases failing at test line 134 (the ENGINE half) plus 'reaches EVERY kind spec publishes in FLOW_TRIGGER_KINDS' at line 189.\n LEG C2 (same mutated spec, OLD private chain): I predicted FULLY GREEN and I WAS WRONG -- measured '2 failed | 17 passed'. Diagnosis: the two api cases redden at line 139, the half that queries resolveFlowTriggerKind DIRECTLY, so it reddens on a spec mutation regardless of the engine and cannot be a coupling measurement. The engine half (line 134) and the reachability case both stayed GREEN. C1-vs-C2 is therefore still the leg that measures what this card buys, and the LINE NUMBERS are the measurement: with the unification a spec-side change moves the engine's answer; without it the engine is untouched. No leg reddened everything.\nMUTATION-REACHED-DISK: literal-text counts before/after on the mutated file each leg, each aborting on a miss -- resolveFlowTriggerKind 2 to 0, Array.isArray(config.triggerType) 1 to 0, f.type === 'api' 1 to 0. ONE MARKER WAS VACUOUS AND I REPORT IT: ablation-dist-preflight.mjs with the single-quoted SOURCE spelling matched only sourcemaps, because the bundler re-quotes to double quotes -- the tool caught it itself ('marker found ONLY in 6 sourcemap files ... Treat this run as void'). Leg C was RE-RUN with the bundling-stable marker 'triggerType === \"api\") return \"api\"': present in 6 built files before, 'absent from all 215 built files' after the mutated rebuild, present again after the restore rebuild, and the same 3-case red set reproduced.\nRESTORE PROOF: git hash-object equal to the HEAD blob for both mutated files (engine.ts 00401ee1..., flow-trigger-kind.ts 2e7c8137...), git diff HEAD empty, whole-tree git status --porcelain empty, both packages rebuilt from restored source, and 19/19 + 3/3 green again on the restored tree. The temporary packages/spec mutation was an ablation only -- never committed; the shipped diff touches no spec file.",
"mcp_calls": "0 -- REST via container curl for every GitHub read and write (probe returned 200); no MCP GitHub call was made this run",
"open_questions": [],
"out_of_scope_findings": []
}Generated by Claude Code Generated by Claude Code |
os-sales
commented
Sep 3, 2026
PM ACCEPT — |
Short form: the bot's hypothesis was tested rather than inherited — ablation Leg A reverted the unification with the pins in place and reddened nothing, so no page can describe behaviour this PR moved. All 10 hand-written pages were flagged through the The two places named as able to falsify it have no documentation to go stale: no page in The 3 release-owned pages were read, not touched, and none is wrong — notably Generated by Claude Code Generated by Claude Code |
…tead of casting it away (#14328) The type-check DEBT lane went red on the previous head: `@objectstack/service-automation` measured 4 raw `tsc --noEmit` errors against a ledger entry recording 3. The +1 was mine and it was in the new pin file: src/flow-trigger-kind-shared-resolver.test.ts(250,21): error TS7006: Parameter 'binding' implicitly has an 'any' type. The recording trigger double was written as an object literal cast `as never`. The cast erases the contextual type for `start`, so `binding` had no type to be inferred from. Fixed by typing the double as the real `FlowTrigger` and the capture array as `FlowTriggerBinding[]` — the same shape `engine.test.ts` already uses — which restores the contextual type and removes the cast entirely. This is a TYPE fix only. Both assertion lines in that case are byte-identical; what the pin checks is unchanged, and the array-form divergence it guards is untouched. The ledger entry is NOT raised: re-measured back to exactly the 3 it records, same composition (TS2341 x3 in nested-region-parity.test.ts at 95/151/180, the pre-existing private-`flows` reads). This package has no `typecheck` script, so its `tsc --noEmit` runs only in the debt lane — which is why the error was invisible to the local gate run, where that gate honestly reported PREREQUISITE NOT MET / nothing measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…igger-kind-resolver
Landing conditions MET — held only by API quota, not by anything in this PR
CI on
The patch round, verified at the treeThe two reds were one root cause: The fix (
⛔ Nothing was ledgered. The shrink-only ratchet was not raised, and no ⛔ What is blocking, and it is not this PRThe two remaining actions — mark ready for review, then enable auto-merge (squash) — are both GraphQL-only operations on GitHub. This session's GraphQL endpoint serves only a pinned set of PR-review operations ("This GraphQL query is not enabled for this session"), and REST's Per this seat's standing rule the response is to back off rather than poll or loop-retry. The repo-scoped REST pool is separate and healthy (15000/15000), which is how this comment is being written — but it cannot serve the two remaining operations. Order when quota returns, and the order matters: mark ready first, then arm auto-merge — auto-merge does not survive a draft conversion, so arming first would be undone by the undraft. No carriers to clear: On MERGED, Generated by Claude Code |
os-dev-report Patch round 1 -- supersedes the round-0 report above for gate/test readings; the implementation, pins and ablation are unchanged. {
"issue": 14328,
"status": "done",
"round": "patch round 1",
"branch": "claude/issue-14328-trigger-kind-resolver",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14994",
"head": "b7cffa0ec",
"premise_still_valid": true,
"summary": "Patch round 1: the Type Check debt lane was red on 39fcadf65 and it was mine. Measured rather than guessed -- built the closure, ran the package's own tsc --noEmit, and got 4 errors: 3 TS2341 in src/nested-region-parity.test.ts at 95/151/180 (exactly the pile the ledger entry records and itemises) plus ONE new TS7006 at src/flow-trigger-kind-shared-resolver.test.ts(250,21), 'Parameter binding implicitly has an any type'. Cause: the recording trigger double was an object literal cast 'as never', and the cast erases the contextual type for start, so binding had nothing to infer from. Fixed as a TYPE fix only -- the double is now typed as the real FlowTrigger and the capture array as FlowTriggerBinding[], the same shape engine.test.ts already uses, and the cast is gone. BOTH assertion lines are byte-identical: git diff touches no line containing 'expect', so what the pin checks and the array-form divergence it guards are unchanged. The ledger entry was NOT raised, nothing was ts-expect-error'd, no tsconfig loosened, no test skipped. The second red check (TypeScript Type Check) was the aggregator re-reporting the same lane -- its other three lanes were green on that head -- so one fix cleared both, and the aggregator's narrowing was confirmed by measurement rather than taken on trust: this package has no typecheck script, so its tsc --noEmit runs only in the debt lane, which is exactly why the original local gate run honestly reported that gate as exit 3 / NOT MEASURED and could not have caught it. Merged origin/main again before re-verifying because dispatch-gates.mjs reported STALE TREE and named scripts/check-type-check-coverage.mjs itself -- the gate at issue -- among the stale files; those upstream ledger edits are metadata graduations and the service-automation entry is untouched. Two observations for the seat, neither acted on: (1) the PR read back as draft:false after my body PATCH -- I did not set that and did not touch it, since REST PATCH /pulls does not accept 'draft' and I sent only 'body'; it is consistent with the coordinator's stated intent to do the undrafting and arming. (2) During the union re-run my first 'tail --pid' attached to a stale pid and reported the runner gone at 25/41 while the real runner was still holding the lock; I re-attached to the real pid and completed 41/41 rather than reporting the partial set.",
"tests": "All on final head b7cffa0ec, exit codes captured by redirect-then-read.\nTSC (the actual measurement): cd packages/services/service-automation && tsc --noEmit => exit 2, 3 errors, all TS2341 'Property flows is private' in src/nested-region-parity.test.ts at 95/151/180 -- the ledger's recorded number AND composition. Before the fix the same command reported 4, the extra being src/flow-trigger-kind-shared-resolver.test.ts(250,21) TS7006.\nDEBT GATE (first time it has actually run in this container -- it needed the whole-repo build that made it exit 3 before): pnpm check:type-check-debt => EXIT=0, both halves. Verdict text: '✓ check:type-check-coverage --self-test -- 48 semantic case(s) + 68 observation case(s) + 43 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) + 18 exit-code case(s) hold.' and 'check-type-check-coverage --re-measure: OK -- 18 ledger entr(ies) re-measured in 192.9s, 218 raw tsc error(s) total, none above its recorded number.'\nSIBLING PACKAGE: pnpm --filter '@objectstack/trigger-record-change' typecheck => exit 0 (both halves, tsc --noEmit && tsc --noEmit -p tsconfig.test.json). NOTE: an earlier run of it showed TS2307 'Cannot find module @objectstack/service-automation' in two PRE-EXISTING files; that was an unmet prerequisite in a fresh worktree (service-automation had no built .d.ts because I had built only the '^...' closure, which excludes the package itself), not a finding -- building service-automation cleared it. Reported because it reads exactly like a real failure.\nGATE UNION re-derived and re-run in full on the final head: 41/41 recorded, 40 exit 0, 1 exit 3. With a whole-repo build present, check:dual-build-cjs-loads AND check:type-check-debt now both exit 0 -- they were two of the three NOT MEASURED entries in the pre-patch run. The single remaining NOT MEASURED is 'node scripts/check-test-completeness.mjs' (exit 3), which wants a saved turbo run test log and whose own fix line says to record it as NOT MEASURED when running the family locally.\nWHOLE-REPO BUILD: pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2 => 71 successful, 71 total, exit 0 (run twice: once pre-merge, once post-merge).\nSUITES: service-automation 'Test Files 103 passed (103) / Tests 1223 passed (1223)'; trigger-record-change 'Test Files 8 passed (8) / Tests 81 passed (81)'; os-verify-lock VERDICT command-exit 0.\nNO NEW ABLATION for this round: the change is a test-file type annotation with byte-identical assertions, so there is no behaviour to ablate. The original 4-leg ablation stands unchanged on the engine diff, which this round did not touch.",
"mcp_calls": "0 for this round as well -- REST via container curl for every GitHub read and write; still zero MCP GitHub calls across the whole card",
"open_questions": [],
"out_of_scope_findings": []
}Generated by Claude Code Generated by Claude Code |
Blocked on enqueue — auto-merge accepts and does not persist. Stating it once.
This PR is done on every axis I control. CI job-level on What is actually blockingAuto-merge does not persist. Twice now:
Throughout, ⛔ It is NOT a merge conflict — measured, not assumedThe branch already merged The likeliest reading, offered as a hypothesis rather than a conclusion: What I am doing about it⛔ Not re-arming again — the standing rule is one re-arm, and a third would be the loop-retry the rule exists to prevent. ⛔ Not pushing an empty commit and ⛔ not closing/reopening to kick it; both are forbidden and neither addresses an uncomputed mergeability. This PR stays watched with a check-in scheduled until it merges or the blocker changes. If On MERGED, Generated by Claude Code |
⛔ Correction to my comment above (5529902021) — the diagnosis was wrong
Two reading errors produced it, both mine:
What the evidence actually supportsEnqueued 17:52:29Z; absent from the queue refs at 17:54:04Z, 17:57:26Z and 17:59:23Z; ⛔ I am not now asserting a cause for the dequeue. The candidates I can see — the queue's own CI run on the merged-with-main candidate, or an eviction when What stands from that commentUnchanged and still verified: CI on Also unchanged: ⛔ no empty commit, ⛔ no close-and-reopen, ⛔ no merge commit for a conflict that does not exist. Next action is to re-read the queue and Generated by Claude Code |
Fixes#14328
AutomationEngine.resolveTriggerBindingkept a private copy of the chain that decides which trigger kind a flow asks for, in parallel with@objectstack/spec'sresolveFlowTriggerKind— the authoring-time mirror of the same rule thatdefineStack's trigger-capability refusal and@objectstack/lint'svalidate-flow-trigger-readinessalready read. Nothing pinned the two copies together, so a branch added to one side would leavedefineStackaccepting a stack the runtime leaves inert, or refusing one it would arm.The shape implemented
resolveTriggerBindingnow takes its kind fromresolveFlowTriggerKind(flow)and keeps only the per-kind binding construction — which start-node fields each trigger needs.getTriggerBindingAuditand the boot banner therefore name the kind authoring named, by construction.resolveFlowTriggerKindis one more name on an import statementengine.tsalready had (@objectstack/spec/automation);@objectstack/specis a realdependenciesentry ofservice-automation, so there is no new package edge.Two guards close the drift the card is about, one static and one at run time:
switchis exhaustive overFlowTriggerKindwith aneverdefault, so a kind added to spec fails this package's type-check until its binding shape is written. At run time (a spec build ahead of this one) it falls back to today's behaviour — no binding — rather than throwing inside the boot audit;FLOW_TRIGGER_KINDSis reachable through the real engine. That is the case the ablation shows is the actual coupling measurement.The array-form divergence, and how it is preserved
The ARRAY form of
triggerType(['record-after-create', 'record-after-delete']) resolves to no kind in spec, deliberately, and the resolver's own header says why: multi-event unions are unsupported (#3457), and reading the shape as "asks for a record-change trigger" would havedefineStackdemand a capability the flow can never use and would widen the lint rule's auto-triggered set. The engine routes it to the record-change trigger anyway — a diagnostic route — so that trigger refuses it loudly at bind time (#3481) instead of the flow folding into "manual" and vanishing from every surface.It is kept as an explicit pre-check BEFORE the resolver call, never folded into it. The ordering is load-bearing on its own: array form outranks
timeRelative, which the resolver — blind to the array — would otherwise answer for a start node carrying both. Moving the pre-check after the resolver call silently re-routes that flow and swaps the loud refusal for a sweep, so it has its own pin.Clause-②: noRe-derived from this diff, not inherited. No exported symbol is added or removed (
resolveTriggerBindingisprivate;resolveFlowTriggerKindis imported, not re-exported), no payload key changes, and no accept-set moves.The one place the seat named as able to flip it — whether the public
getTriggerBindingAuditor the boot banner reports anything different for any flow, the array-form case being the candidate — was measured rather than argued, and it does not flip:engine.ts→ the merge base) with the new pins in place. Nothing reddened: 19/19service-automationcases and 3/3trigger-record-changecases green. Old chain and new produce the same answer on every case the pins cover, array form included.eventis still the joined token that maps to no hook,config.triggerTypestill carries the raw array. Pinned directly.service-automationcases and 81trigger-record-changecases pass unchanged.Pins, and what each one catches
flow-trigger-kind-shared-resolver.test.ts, each asserting a literal kind through the real engine (registerFlow→getFlowRuntimeStates())expect(engineKind).toBe(resolveFlowTriggerKind(flow))on its own is satisfied by two wrong answers that agree, and byundefined === undefinedresolveFlowTriggerKindon the flow the engine storednames the same kind on getTriggerBindingAuditreaches EVERY kind spec publishes in FLOW_TRIGGER_KINDSrecord_changethough spec answers no kind; pre-check outrankstimeRelative; raw array + joined event forwarded; an array with norecord-*element is left to the resolver)array-form-refusal-end-to-end.test.ts(new, in@objectstack/trigger-record-change)RecordChangeTrigger→ the warn, asserted by flow name, "ARRAY", "NOT bound / never fire", therecord-after-writesteer and#3457, plus zero hooks registered. Its third case is an anti-vacuity control: a legitimate singlerecord-after-updatetoken draws no warn and armsafterUpdateThe e2e file lives in
trigger-record-changebecause that is the only side of the edge where both halves exist:@objectstack/service-automationis a devDependency there, and the trigger is deliberately not a dependency of the engine. Same split — and samedist/-resolution note — as the existingreentrant-start-condition.test.ts.Ablation
Directions were written down before any leg ran. Resolution facts that shape the legs, measured not assumed:
service-automation's own tests readengine.tsfrom source and@objectstack/specthroughexportstodist/;trigger-record-change's tests read@objectstack/service-automationthroughexportstodist/(both fromcheck:test-source-alias'sKNOWN_UNALIASED_TEST_IMPORTS).@objectstack/specis external in the engine's bundle, so rebuilding spec alone reaches the engine'sdist. Every leg rebuilt what it mutated; the driver carriedtrap … EXIT INT TERMwith absolute paths.engine.ts→ merge base)trigger-dispatch-observabilityarray case + 2 of 3 e2e cases; green: all 7 precedence cases, the reachability case, and the e2e anti-vacuity case4 failed | 15 passedinservice-automation,2 failed | 1 passede2e. Reddened:routes array form to record_change…,keeps the array pre-check AHEAD of the resolver…,hands the record-change trigger the raw array…,hands an array triggerType to the record_change trigger…,refuses the array form by name…,gets there because the ENGINE routed it…apibranch, spec rebuilt; unified engine3 failed | 11 passed: bothapiprecedence cases (failing at line 134, the engine half) andreaches EVERY kind spec publishes in FLOW_TRIGGER_KINDS(line 189)apicases redden here too — but at line 139, the half that queriesresolveFlowTriggerKinddirectly. Line 134, the engine half, stayed green, and the reachability case stayed greenC1 vs C2 is the leg that measures what this card buys, and the line numbers are the measurement. With the unification, a spec-side change moves the engine's answer (line 134 reddens, reachability reddens). Without it, the engine is untouched by the same spec change (line 134 green, reachability green) and only the direct spec query reddens. That is the drift the card describes, demonstrated in both directions.
My C2 prediction was wrong and the reason is worth recording rather than smoothing over: a pin's "and to what spec answers" half asks spec directly, so it reddens on a spec mutation regardless of the engine and cannot be a coupling measurement. The coupling measurements are the engine-half literal and the reachability case. Neither leg reddened everything.
Mutation-reached-disk proof. Literal-text counts before/after on the mutated file each time (
resolveFlowTriggerKind2 → 0;Array.isArray(config.triggerType)1 → 0;f.type === 'api'1 → 0), each aborting the leg on a miss. One marker was initially vacuous and is reported as such:scripts/ablation-dist-preflight.mjswith the single-quoted source spellingf.type === 'api'matched only sourcemaps, because the bundler re-quotes to double quotes — the tool caught it itself (✗ … found ONLY in 6 sourcemap files … Treat this run as void). Leg C was re-run with the bundling-stable markertriggerType === "api") return "api":✓ marker present in 6 built filesbefore,✓ marker absent from all 215 built filesafter the mutated rebuild,✓ presentagain after the restore rebuild, and the same 3-case red set reproduced.Restore proof.
git hash-objectequal to theHEADblob for both mutated files (engine.ts00401ee1…,flow-trigger-kind.ts2e7c8137…),git diff HEADempty, whole-treegit status --porcelainempty, both packages rebuilt from restored source, and 19/19 + 3/3 green again on the restored tree.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandson the merged head (origin/mainmerged in first — the earlier derivation warnedSTALE TREEand named the two files it read stale copies of, so the list below is the one derived from a tree someone is on). Every exit code captured by redirect-then-read, never across a pipe.41 commands, all captured by redirect-then-read. 38 exited 0; 3 exited 3 and are NOT MEASURED — neither green nor red — quoted below from each gate's own verdict text.
Exit 0 (38):
node scripts/check-adr-0087-registration.mjs·node scripts/check-changeset-no-major.mjs·node scripts/check-ci-filter-parity.mjs·node scripts/check-closing-keyword-parity.mjs·node scripts/check-comment-mask-adoption.mjs·node scripts/check-comment-mask-corpus.mjs·node scripts/check-cross-package-test-inputs.mjs·node scripts/check-empty-changeset.mjs·node scripts/check-keyed-text-bounds.mjs·node scripts/check-plugin-teardown-shape.mjs·node scripts/check-shard-attestation.mjs·node scripts/check-system-context-census.mjs·node scripts/check-tenant-audit-census.mjs·node scripts/check-undeclared-dep-imports.mjs·node scripts/docs-audit/check-affected-docs.mjs·node scripts/docs-audit/check-drift-comment.mjs·node scripts/pm/check-half-states.mjs·node scripts/pm/release-rehearsal-clone.mjs --self-test·pnpm check:changeset-gate-self-tests·pnpm check:cross-package-test-inputs·pnpm check:dispatcher-error-vocabulary·pnpm check:doc-authoring·pnpm check:engine-double-contract·pnpm check:logger-receiver-detach·pnpm check:nul-bytes·pnpm check:objectql-double-limit·pnpm check:objectui-changeset·pnpm check:page-declaration-shape·pnpm check:pm-half-states·pnpm check:published-files·pnpm check:query-options-erasure·pnpm check:refd-timer-probe·pnpm check:slot-lookup·pnpm check:test-source-alias·pnpm check:type-check-coverage·pnpm check:type-source-resolution·pnpm check:watch-hint-literal·pnpm check:where-matcherNOT MEASURED (exit 3, 3):
node scripts/check-test-completeness.mjs— exit 3It wants a saved
turbo run testlog; running the family locally, its own fix line says to "record this gate as NOT MEASURED".pnpm check:dual-build-cjs-loads— exit 3Its own self-test passed first (
✓ check-dual-build-cjs-loads self-test: 93 cases pass); only the repo-wide built-output leg is unmet. Needs a whole-repopnpm build, which is CI's run, not mine.pnpm check:type-check-debt— exit 3Same cause — it asks for the closure build
lint.ymldoes first (pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'). Notepnpm check:type-check-coverageexited 0 beside it.Two notes on the derived list rather than trusting it blind:
dispatch-gates.mjskeys entries by script path, so a script CI invokes twice collapses to one entry; and the derived list excludes CI's always-runs tail. Re-derived after the merge commit that changed the file set, and the union above was run on the final head39fcadf65— same tree as the suite run below.docs-drift-checkdisposition — 11 pages opened, zero doc edits, and whyThe bot is advisory, so this is the disposition rather than a skip. Its hypothesis — that nothing needs an edit because the refactor is behaviour-neutral — was tested, not inherited: ablation Leg A reverted the unification with the pins in place and reddened nothing, so no page can be describing behaviour this PR moved. The bot also discloses that it computed on merge commit
f8f059e9a631576a850615bf7116350bba098063rather than this PR head, and that a page stating the rule by its inputs shares no identifier with the emitter and cannot be listed at all — so the list was treated as a starting point, not the population.Its 10 hand-written pages. All flagged through the
record_change/time_relativestring literals inresolveTriggerBinding, which this PR neither renames nor re-values.automation/flows.mdx(flagged twice, viatime_relativetoo):1411"Registration is not arming …defineStackrefuses a stack … naming the flow, the trigger kind it resolved" describes the authoring side, which already readresolveFlowTriggerKindbefore this PR.:1818the single-eventtriggerTypetable — every token still resolves torecord_change.:1742the time-relative sweep, shown with aschedulecadence besidetimeRelative: that is precisely the precedence case, and it is preserved (the resolver rankstime_relativeahead ofschedulefor the same reason the private chain did). This PR makes that documented ordering pinned rather than incidentalpermissions/capabilities.mdx(flagged twice, viatime_relativetoo):28listsrecord_change/schedule/time_relative/apias the flow kinds needingtriggers. That set isFLOW_TRIGGER_KINDS, unchanged — and now asserted reachable through the real engine by a new caseautomation/approvals.mdx,automation/workflows.mdx,concepts/architecture.mdx,getting-started/common-patterns.mdxtype: 'record_change'flows with single-stringtriggerTypetokens. Same kind, same binding, same fieldsautomation/hooks.mdxrecord_changeandschedule/timeRelativeas options. Unchangedapi/plugin-endpoints.mdx,kernel/services-checklist.mdx,protocol/objectql/schema.mdxrecord_changeas a conceptThe blind spot the bot warned about, checked. Searching the docs tree for the rule stated by its inputs (
timeRelative, array-form / multi-event prose) surfaced one page not on the list:content/docs/references/automation/time-relative-trigger.mdx. It describesconfig.timeRelativewith an optionalschedulecadence — accurate under this PR, and in any case it is marked⚠️ AUTO-GENERATED — DO NOT EDIT, generated frompackages/spec/src/automation/time-relative-trigger.zod.ts, which this PR must not touch. No page anywhere incontent/docs/describes array-formtriggerTypehandling, and none mentionsgetTriggerBindingAuditor the boot banner's contents, so the two places the coordinator named as able to falsify the hypothesis have no documentation to go stale.The 3 release-owned pages: read, not touched, and none is wrong.
releases/v17.mdx:1536states "array-formtriggerTypefails loudly instead of silently never firing" — that is exactly the behaviour this PR preserves as an explicit pre-check, so it remains correct.releases/v16.mdx:393(thetime_relativetrigger) and:409(thekernel:bootstrappedbinding audit warning per enabled-but-unbound flow) andreleases/v12.mdx:159are all still accurate. Nothing to file.Patch round 1 — the type-check DEBT lane was red, and it was mine
Two red checks, one root cause.
Type Check · debt ledgerfailed on39fcadf65with@objectstack/service-automation: DEBT records 3 raw tsc error(s), tsc --noEmit now reports 4 (+1), andTypeScript Type Checkwas red only because it aggregates that lane (its other three —typecheck-source-gates,typecheck-workspace,typecheck-consumers— were green on the same head). One fix clears both.Measured, not guessed. Built the closure, then ran the package's
tsc --noEmitdirectly:The three
TS2341are the ledger's recorded pile, at exactly the lines its note itemises (95/151/180). The+1was mine, in the new pin file — consistent with the aggregator's narrowing, since this package has notypecheckscript, so itstsc --noEmitruns only in the debt lane.The cause. The recording trigger double was an object literal cast
as never. The cast erases the contextual type forstart, leavingbindingwith nothing to infer from.The fix is a type fix. The double is now typed as the real
FlowTrigger, the capture array asFlowTriggerBinding[]— the shapeengine.test.tsalready uses — and the cast is gone. Both assertion lines in that case are byte-identical;git difftouches no line containingexpect. What the pin checks, and the array-form divergence it guards, are unchanged.Proof.
⛔ The ledger entry was not raised, nothing was
@ts-expect-error-ed, no tsconfig loosened, no test skipped.@objectstack/trigger-record-change's owntypecheck(both halves,tsc --noEmit && tsc --noEmit -p tsconfig.test.json) also exits 0.Why the local run could not have caught it. This is the same gate the original PR body records as exit 3 / NOT MEASURED (
PREREQUISITE NOT MET … nothing was measured) — an honest reading for a container with no whole-repodist/. CI, with a full build, was the first place it actually ran. Recorded as a lesson: a NOT MEASURED gate is not a quiet pass, and one your diff can move is the one most likely to surprise you in CI.Re-verification on the new head
b7cffa0ec(supersedes the two sections above)origin/mainwas merged again first —dispatch-gates.mjsreportedSTALE TREEand namedscripts/check-type-check-coverage.mjsitself among the files it had read stale copies of, which is the gate at issue. (Those upstream ledger edits are@objectstack/metadatagraduations; theservice-automationentry is untouched.)40exit 0,1NOT MEASURED. With a whole-repo build present,check:dual-build-cjs-loadsandcheck:type-check-debtnow both exit 0 — they were the NOT MEASURED entries in the original run. The single remaining NOT MEASURED isnode scripts/check-test-completeness.mjs(exit 3), which wants a savedturbo run testlog and whose own fix line says to record it as NOT MEASURED when running the family locally.service-automationTest Files 103 passed (103) / Tests 1223 passed (1223);trigger-record-changeTest Files 8 passed (8) / Tests 81 passed (81).Clause-②: no— unchanged and unaffected: a test-file type annotation adds no exported symbol, no payload key and no accept-set change.Deliberately not done
packages/specedit. It is single-owner and this PR is a consumer ofresolveFlowTriggerKind. The change needed none — the temporary spec mutation in Leg C was an ablation, restored with hash proof and never committed.content/docs/releases/**edit. The release-notes input is the changeset.Verification, on the final head
39fcadf65Clause-②: no🤖 Generated with Claude Code
https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Generated by Claude Code