Skip to content

fix(service-automation)!: a switched-off packaged caller guards its subflow's disable only while it holds a parked run - #20724

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20678-subflow-disable-parked-run
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20678-subflow-disable-parked-run

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20678
Clause-②: yes (narrowing)

This completes the card. Stage 1 (PR #20711, merged as 679f95ec) landed the enable half. This PR lands the disable half, as triage's answer A (comment 5898655174 on #20678) reads ADR-0126 §7.3: a switched-off packaged caller guards the disable of its packaged subflow only while it holds a parked run. resume() and resumeInternal() are unchanged, and no ADR text is touched.

What changed (packages/services/service-automation/src/engine.ts)

toggleFlow(name, false) now judges each packaged caller of name (the flowName of a subflow or map node) on reachability:

  • Enabled (isFlowEnabled): it guards, as before. The refusal names it, with the step "disable the calling flow first".
  • Disabled, by the activation ledger or by its definition's status, and holding a parked run: it guards. The refusal names each parked run id and the operator cancel door, POST /automation/CALLER/runs/:runId/cancel (ADR-0044): "cancel it, or let it finish".
  • Disabled with no parked run: it no longer guards. So "disable the caller, then the callee" completes.

The envelope is unchanged: DELETE_RESTRICTED / 409, and subflowCallers, which now lists exactly the callers that guard. No new error code, gate, state or structured key.

One helper. parkedRunsOf(flowNames) answers "holds a parked run". The verdict and the refusal's run list both come from its single answer, so they cannot disagree. toggleFlow reads it only when a switched-off caller is being judged. A flow whose callers are all armed never touches the run stores, and every other disable flips exactly as before: synchronously, up to its durable write. A first version awaited the read on every disable. That moved the flip one microtask later, and flow-terminal-messages.test.ts, which calls toggleFlow without awaiting it, measured the shift (fixed in d59c97a50).

B6 rider (enable direction). In disabledPackagedSubflows the ledger-cycle exemption is now asked only of a child whose status does not also disable it. A child disabled both ways inside a ledger-disabled cycle is named, with both reasons and both steps, including its publish remedy.

The mechanism premises, measured (B1 to B8)

  • B1, the reader. listSuspendedRunsDurable() already answers from both stores. It merges store.list() (the durable rows, including runs a previous process parked and this one never loaded) with this process's hot map, and the durable row wins an id collision. Its body moves unchanged into a private readSuspendedRuns(onEnumerationFailure). The public listing calls it with 'degrade', so its behaviour and its warning are unchanged. parkedRunsOf calls it with 'throw'. The reason: under 'degrade' a store outage answers from the cache alone, and for this guard an absent run means "accept the disable". That would fail open onto exactly the run §7.3 protects. Pinned both ways: a run only in the durable store guards, and an unlistable store refuses with its own error while writing nothing.

  • B2, a map parent. It is an ordinary suspended run parked AT its map node. The measured durable row is flowName: parent, nodeId: call, nodeType: map, correlation: map:CHILDRUNID. No distinct state exists. Terminal runs are in neither store, so they never count. The subflow-pair pin runs the caller to completion first.

  • B3, status-disabled callers. A status-disabled caller's parked run still resumes. The pin republishes the caller obsolete, gets the refusal naming the run, then resume() answers success. The ledger-disabled case is the control pin. An enabled caller guards as before, and a customer-authored caller is still not counted.

  • B4, the cancel door. The route file is packages/runtime/src/domains/automation.ts, arm POST /:name/runs/:runId/cancel, gated by isRunLifecycleWrite on the platform-operator rung. It calls cancelRun(runId, reason). The map-pair pin cancels the named run and the disable completes. This was also measured live, below. The run list is not bounded, because no reason to bound it was measured (the shipped map pair parks one run per release).

  • B5, the text. Here is one refusal as measured live:

    Flow 'showcase_one_task_signoff' cannot be disabled while 1 packaged flow still calls it as a subflow: 'showcase_release_signoff'. Disabling it would break that caller mid-run at its subflow node with a late, inexplicable failure (ADR-0126 §7.3). 'showcase_release_signoff' is disabled but still holds a parked run that resumes into its subflow node ('run_b2bf74fc-0125-4284-8728-6223bf59dd5f'): cancel it through the operator cancel door, POST /automation/showcase_release_signoff/runs/:runId/cancel (ADR-0044), or let it finish — or leave this one armed.

    An enabled caller reads "Disable the calling flow 'X' first — or leave this one armed." When both kinds are present, the steps are joined with ;.

  • B6 is above. It is pinned, and ablation M4 turns it red.

  • B7, the registerFlow door. Measured, not built. See Acceptance notes.

  • B8, Clause-②. Measured against this diff:

    • Widening: a disable of a packaged subflow whose packaged callers are all switched off and hold no parked run was refused, and is now accepted.
    • Narrowing: re-enabling a ledger-disabled packaged caller whose packaged subflow is disabled both ways inside a ledger-disabled cycle was accepted (the child was skipped whole), and is now refused. This is the B6 corner.
    • So the declaration is yes (narrowing), BREAKING, and minor under the launch-window convention. The claim's yes (widening) misses the B6 narrowing.
    • One refusal changes shape without changing the accept set. When the durable store cannot be listed while a switched-off caller is judged, the disable was refused 409 DELETE_RESTRICTED (every caller guarded). It is now refused with the store's own error. It is refused either way.

Pins (flow-activation-ledger.test.ts)

Every refusal pin asserts code: 'DELETE_RESTRICTED', status: 409, subflowCallers, and the named run id with the cancel door.

  1. The subflow pair: a completed caller run does not count, and caller-then-callee completes at once.
  2. The map pair: while a per-item sign-off is parked, the refusal names the caller's run. After cancelRun, the disable completes.
  3. Control: a switched-off caller with an enabled callee resumes its parked run to success. Once that run has finished, the disable completes.
  4. A status-disabled caller with a parked run (B3).
  5. A run in the durable store only, parked by a previous engine over the same store, with an empty hot cache (B1).
  6. An unlistable durable store: the disable throws the store's own error and writes nothing.
  7. Enable direction: a child disabled both ways inside a ledger cycle is named with its publish remedy, and the remedy completes (B6).

Red first: 97222b887 ran against the unfixed engine: Tests 7 failed | 40 passed (47). Each failure was the intended one: the run id or the new step missing, the store's error masked by the old refusal, and the B6 enable accepted. The fix is 172680217, and d59c97a50 is the head. #20711's enable-half pins are unchanged and green.

Ablations. Each leg used scripts/ablation-replace.mjs in wrap mode. The anchor hit exactly once, the mutation was proven on disk (anchor 1 to 0, blob changed), and the restore was proven: ok restored: blob == HEAD (7d48c737834c) and git diff HEAD is empty. An outer trap on EXIT/INT/TERM restored by absolute path. There is no dist/ leg: the test imports ./engine.js relatively, so it resolves src/. All legs were re-run from committed d59c97a50.

leg mutation red
M1 every packaged caller guards (the pre-fix rule) 5: pins 1 to 5
M2 the reader sees the hot cache only 2: pins 5 and 6
M3 the guard takes the 'degrade' posture 1: pin 6
M4 the cycle exemption asked of a status-disabled child 1: pin 7
M5 the refusal names no run ids 4: pins 2 to 5
M6 a disabled caller never guards (the plain skip) 4: pins 2 to 5
M7 "disabled" read from the ledger bit alone 1: pin 4

Live measurement (showcase, pnpm dev -- --fresh -p 41863, built at d59c97a50)

  • The card's own reproduction. Disabling showcase_notify_owner answered 409, naming its armed caller. Disabling showcase_task_done_notify_owner answered 200. Disabling showcase_notify_owner again answered 200, and /_status read enabled: false for both.
  • The map pair. POST /api/v1/automation/showcase_release_signoff/trigger over two seeded tasks answered paused, with run run_b2bf74fc-….
    • Disabling showcase_one_task_signoff answered 409, naming the armed caller.
    • Disabling showcase_release_signoff answered 200.
    • Disabling showcase_one_task_signoff answered 409, naming the run and the door (the text quoted above).
    • POST /api/v1/automation/showcase_release_signoff/runs/RUNID/cancel answered 200 cancelled: true.
    • Disabling showcase_one_task_signoff then answered 200.
  • The server was stopped by its recorded process group, and the worktree is clean.

Tests and gates (at d59c97a50)

  • pnpm --filter @objectstack/service-automation test: Test Files 155 passed (155), Tests 1942 passed (1942).
  • pnpm --filter @objectstack/service-automation run typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: OK). --listFiles counts the test file once in tsconfig.json and once in tsconfig.test.json.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gives 62 commands, the same 62 as the dispatch-time list. Every command ran with its exit code captured before any pipe. --ran gives 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED.
    • 61 exited 0. check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET). They were re-run after a full package build (turbo run build, 71 of 71 tasks) and both exited 0.
    • One exit is red by design: check-empty-changeset.mjs --base origin/main exits 1, as a DELIBERATE CORRECTION of a pending release note. See Deviations.
  • The four roster families that dispatch-gates prints outside its runnable list exited 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing ("no unlisted lowercase error codes in 6989 scanned file(s)"), and pnpm check:filter-alias-parity. pnpm check:durability-log-level and pnpm check:startup-registry-verdict also exited 0.
  • check-changeset-no-major.mjs and check-adr-0087-registration.mjs were run against a synthetic pull_request payload that carries this body. Both exited 0.
  • Lint, a declared narrowing (a repo-wide pnpm lint is CI's run): eslint --no-inline-config --format json over the 4 changed files gave 4 files, 0 errors and 2 warnings. Both warnings are the .md changesets: "File ignored because no matching configuration was supplied". The population comes from eslint.config.mjs (files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']). The config never enables type-aware linting (its own comment, and no parserOptions.project), so this diff cannot move a verdict on an untouched file.
  • pnpm check:nul-bytes: exit 0. A control-byte self-scan of the 4 files found nothing.
  • NOT MEASURED: the 6 value-bearing CI invocations that dispatch-gates marks as not runnable locally (check-issue-citations --census, the three shard attestations, and the two check-test-completeness runs). Their argv comes from the workflow.

Deviations

  • An unreleased changeset from stage 1 is amended in place. This is .changeset/20678-subflow-disable-sequence.md, outside the declared surface. This diff makes two of its sentences false. The first is "Disabling a subflow is unchanged". The second is the cycle bullet under "Not refused", which the B6 corner now contradicts. Per AGENTS.md, a release note is corrected in its own entry, never by an erratum in a later one. So the cycle bullet gains the status corner, and the closing sentence now points at this PR's entry. check-empty-changeset stays red on this by design until the seat confirms it on this PR. If the seat declines, revert that one file. This PR's own changeset then still states the narrowing.
  • readSuspendedRuns is extracted from listSuspendedRunsDurable, in engine.ts but outside the functions the claim names. That is the one-reader requirement (B1). One paragraph of the catch-arm comment is corrected, because it claimed the method has no production consumer. plugin-approvals reads it, and fails closed on an absent entry.
  • A moved envelope comment re-added a citation that check-issue-citations reports as allocated-but-absent. It now cites the toggle ruling's commit (266436a7f) and its in-tree record, isFlowAuthoringWrite.

Acceptance notes

  • B7, the registerFlow door (class a: finding, for the seat to file).
    • Engine seam: registerFlow arms a new packaged caller onto a ledger-disabled packaged subflow (bound: true), and a run then fails at the subflow node with the composed FLOW_DISABLED. The same holds for a subflow republished obsolete under an armed packaged caller.
    • reach, a public door measured live: a create request through the automation create door that asserts package provenance answered 200 [request detail redacted by the domain:services seat under the security-family disclosure rule; the finding is carried abstractly by automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761]. The body called the ledger-disabled showcase_one_task_signoff. The engine treats the new flow as packaged: its re-enable is refused 409 RESOURCE_CONFLICT by the enable guard. Yet it was registered enabled: true, and POST /api/v1/automation/NAME/trigger answered 400 FLOW_FAILED "subflow 'showcase_one_task_signoff' failed: Flow 'showcase_one_task_signoff' is disabled …".
    • The toggle door refuses that same state. So the §7.3 invariant holds on toggleFlow only.
    • The real producer, a package upgrade that adds a caller while its child is switched off, is not named or measured. DELETE /api/v1/automation/NAME (unregisterFlow) on a packaged subflow is the same invariant's third door. That door was read, not measured.
    • Dedupe words: registerFlow subflow guard, packaged caller armed disabled subflow, automation create door FLOW_DISABLED, ADR-0126 7.3 door.
  • Toggling a customer-authored flow (class a: finding, for the seat to file). Measured incidentally, on the same live boot. POST /api/v1/automation/NAME/toggle on a flow created without package provenance answered 400 VALIDATION_FAILED "Package is required" (field package_id) for both enabled: false and enabled: true. The caller sent no package field: the activation row is written with an empty package id. This behaviour predates this PR, which does not change it. Dedupe words: toggle Package is required, activation ledger customer flow toggle, sys_metadata_activation package_id.
  • Boundary, the listing cap. ObjectStoreSuspendedRunStore.list() reads at most 1000 paused rows. Beyond that, deployment-wide, a caller's parked run can be missing from the enumeration, and the disable would be accepted. Not measured. Carrier: none.
  • Boundary, runs in flight. A caller run executing (not parked) at the instant of the disable is in neither store, and can still reach its subflow node. "Holds a parked run" is triage's rule, and this PR does not build around it. Carrier: none.
  • Boundary, restored strands. A failed caller run with a restorable consumed suspension (the restore-suspension door) is terminal and does not count. Restoring it after the child's disable re-parks it into a failure. Read, not measured. Carrier: none.
  • ADR-0126 §7.3's heading, "attached to disable". It now under-describes the guard, as the seat recorded after fix(service-automation)!: refuse re-enabling a packaged flow onto a disabled packaged subflow #20711. docs/adr/** is not touched here, and the seat raises it with the maintainer.

Generated by Claude Code

… reachability (red)

The disable direction of the ADR-0126 7.3 guard, as triage's answer A on
#20678 reads it: a switched-off packaged caller guards its packaged subflow
only while it holds a parked run, read from both run stores, and the
refusal names those runs and the operator cancel door (ADR-0044).

Pins, red against the unfixed engine:
- the subflow pair completes the sequence at once;
- the map pair: the refusal names the parked caller run, and after a
  cancel the disable completes;
- control: a switched-off caller with an enabled callee resumes to
  success, and once it finishes the disable completes;
- a status-disabled caller with a parked run guards the same way;
- a run parked by a previous process (durable store only) guards;
- an unlistable durable store refuses with its own failure;
- enable direction: a subflow disabled both ways inside a ledger cycle
  is still named with its publish remedy.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…bflow only while it holds a parked run

The disable direction of the ADR-0126 7.3 subflow guard now reads
reachability, per triage's answer A on #20678. A packaged caller guards
the disable of a packaged subflow when it is enabled (step: disable it),
or when it is disabled, by the ledger or by its status, and holds a
parked run (step: cancel each named run through the ADR-0044 operator
cancel door, or let it finish). A disabled caller with no parked run no
longer guards, so "disable the caller, then the callee" completes.

"Holds a parked run" is one helper, parkedRunsOf, over the one reader of
both run stores (readSuspendedRuns, the body listSuspendedRunsDurable
already served). The listing keeps its degrade-on-outage posture; the
guard takes a throw posture, so an unlistable store propagates its own
failure instead of reading as "no parked run". The refusal keeps
DELETE_RESTRICTED / 409 and subflowCallers.

Rider: in disabledPackagedSubflows the ledger-cycle exemption is asked
only of a child whose status does not also disable it, so a child
disabled both ways inside a ledger cycle is named with its publish
remedy.

resume() / resumeInternal() are unchanged.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…ard on parked runs

Clause-②: yes (narrowing). The disable direction widens (a disable
refused while every packaged caller was switched off and held no parked
run is now accepted); the enable direction narrows in one corner (a
subflow disabled both ways inside a ledger-disabled cycle is now named
instead of skipped). ADR-0087: not-required (no-migration-prescription).

The enable-half entry of the same card is still unreleased; two of its
sentences this change makes false are amended in place: the cycle
bullet gains the status corner, and "Disabling a subflow is unchanged"
points at the new entry instead.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
… a number that no longer resolves

Moving the DELETE_RESTRICTED envelope comment into the new disable-guard
method re-added a citation check-issue-citations reports as
allocated-but-absent. The comment now names the ruling's commit and the
in-tree docblock that records it.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…ller is judged

The disable direction awaited the parked-run read on every disable, which
moved the flip one microtask later even for a flow with no switched-off
packaged caller. flow-terminal-messages.test.ts calls toggleFlow without
awaiting it and measured that shift. toggleFlow now reads, and awaits,
the run stores only when a switched-off caller is being judged; every
other disable flips exactly as before, synchronously up to its durable
write. The refusal itself is a synchronous function of the callers and
that one read.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation, touching 10 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via DELETE_RESTRICTED (literal, a string literal in refuseDisableUnderReachingCallers; a string literal in toggleFlow))
  • content/docs/api/declarative-endpoints.mdx (via /api/v1/automation/:name/trigger (route, bridged from symbol toggleFlow — its route source's handler names it))
  • content/docs/api/error-catalog.mdx (via DELETE_RESTRICTED (literal, a string literal in refuseDisableUnderReachingCallers; a string literal in toggleFlow))
  • content/docs/api/error-handling-server.mdx (via DELETE_RESTRICTED (literal, a string literal in refuseDisableUnderReachingCallers; a string literal in toggleFlow))
  • content/docs/automation/flows.mdx (via /api/v1/automation/:name/trigger (route, bridged from symbol toggleFlow — its route source's handler names it), /runs/:runId/cancel (route, a path literal in refuseDisableUnderReachingCallers))
  • content/docs/kernel/contracts/data-engine.mdx (via DELETE_RESTRICTED (literal, a string literal in refuseDisableUnderReachingCallers; a string literal in toggleFlow))
  • content/docs/protocol/kernel/error-handling.mdx (via DELETE_RESTRICTED (literal, a string literal in refuseDisableUnderReachingCallers; a string literal in toggleFlow))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/automation/:name/trigger (route, bridged from symbol toggleFlow — its route source's handler names it))
  • content/docs/protocol/objectql/types.mdx (via DELETE_RESTRICTED (literal, a string literal in refuseDisableUnderReachingCallers; a string literal in toggleFlow))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 6 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 3711e0b763d4bb597a52cb08b04950cace6821b1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc2de572cf758c6c577181b4ad5838a3ee36f7d4 — the merge of head d59c97a50565f16fc75adaac526a4e4303bbafd8 into base 3711e0b763d4bb597a52cb08b04950cace6821b1, 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 dc2de572cf758c6c577181b4ad5838a3ee36f7d4 && git checkout dc2de572cf758c6c577181b4ad5838a3ee36f7d4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3711e0b763d4bb597a52cb08b04950cace6821b1 d59c97a50565f16fc75adaac526a4e4303bbafd8 && git checkout -B drift-repro 3711e0b763d4bb597a52cb08b04950cace6821b1 && git merge --no-ff d59c97a50565f16fc75adaac526a4e4303bbafd8

node scripts/docs-audit/affected-docs.mjs --json 3711e0b763d4bb597a52cb08b04950cace6821b1

⚠️ 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 3711e0b763d4bb597a52cb08b04950cace6821b1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d59c97a50565f16fc75adaac526a4e4303bbafd8
Local-runs: none

PR #20724, a draft onto main closing #20678 (Fixes #20678, line 1; Clause-②: yes (narrowing), line 2): 4 files, +419 / −48, merge base 679f95ec5 (stage 1's landing), read as the net diff against main over refs fetched into this checkout's object store under an owned name (refs/review/pr-20724) — a read, no worktree, nothing built or run. No governed path; head repo = base repo; 467 changed lines. Inputs: the card's body and all ten comments (triage 5895784128, claim 5897077531, stage-1 report 5898095119, acceptance 5898156412, retriage 5898181940, triage's answer A 5898655174, the landing note 5898894424, the stage-2 claim 5898944490, the dev report 5900020200, the seat's acceptance 5900132104), stage 1's at-tier record 5898430659 on PR #20711, the PR body and file list, and the head's check-runs. The dispatch order and the dispatching seat's conclusions were not inputs; where this record agrees with them the source agrees.

Check-runs on the head, latest run per name, read 2026-09-29T22:27Z (34 runs; Lint & Repo Gates was the last to conclude, at 22:27): 0 in progress, 1 failure — Check Changeset, red by design (below). It is the only non-success, non-skipped check. All seven required contexts conclude success: Lint & Repo Gates, TypeScript Type Check, Test Core (rollup and six shards), Dogfood Regression Gate (rollup and three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Also success: Dogfood Verify CLI, the four Type Check · jobs, Part-of PR must not also close its card, The card this PR closes must claim this branch, the two same-issue / single-writer claim checks, Check Documentation Links, Flag docs affected by code changes, Auto Label, Check PR Size, filter. skipped by design: Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in). The Check Changeset job log reads: 1 changeset added (route 1 satisfied), No empty-frontmatter changeset introduced, then the foreign-changeset rule (scripts/check-empty-changeset.mjs rule 2) refusing on .changeset/20678-subflow-disable-sequence.md — "present on the merge base and CHANGED by this PR" — and printing the two-class remedy. The job stops there, so the check-changeset-no-major and check-adr-0087-registration steps behind it did not run on this head; ② below judges both against their own rules instead.

① Derived judgments

Read at source on the head: engine.ts (toggleFlow, packagedSubflowCallers, parkedRunsOf, refuseDisableUnderReachingCallers, readSuspendedRuns, listSuspendedRunsDurable, isFlowEnabled, disabledPackagedSubflows, ledgerDisabledChainReaches, refuseEnableOntoDisabledSubflow, cancelRun), the hunk map of the diff against the definitions of resume (:6199) and resumeInternal (:6705), suspended-run-store.ts (both stores' list()), the cancel arm and isRunLifecycleWrite in packages/runtime/src/domains/automation.ts, plugin-approvals' listSuspendedRunsDurable consumer, ADR-0126 §7.2 / §7.3 / §9, ADR-0044's cancelRun primitive, and all 7 new pins in flow-activation-ledger.test.ts. Each judgment is against triage's answer A (5898655174).

  1. The toggleFlow(name, false) accept set WIDENS, on reachability — right, and it is answer A. packagedSubflowCallers still reports every packaged caller whatever its state (the scan is unchanged; only its docblock moved). toggleFlow splits them by isFlowEnabled: an armed caller guards as before; a switched-off caller guards only if parkedRunsOf names a run for it; a switched-off caller with no parked run no longer guards. So "disable the caller, then the callee" completes — the card's own reproduction, and pin 1 (the subflow pair) proves it at the engine. The refusal, when it fires, is thrown before flowActivationStore.setActive, so a refusal moves nothing (pins assert no ledger row).
  2. "Disabled" is read from BOTH dimensions — right (answer A: "by the ledger or by its status alike"). switchedOff is !isFlowEnabled(c), and isFlowEnabled is the composition of flowStatusDisabled and flowLedgerDisabled. Pin 4 (a caller republished obsolete, no ledger row) and ablation M7 stand on exactly this; an armed caller and a customer-authored caller are handled as before (the describeFlowContender(...).source !== 'package' skip is untouched).
  3. One helper, both stores — right (answer A: "Read both run stores… One helper… so they can't disagree"). parkedRunsOf is the single source for the verdict (parked.has(c)) and the refusal's run list ([...parked]), so the runs named are the runs that refused. It reads through readSuspendedRuns, which is listSuspendedRunsDurable's body moved verbatim (the store.list() merge, durable row wins an id collision, hot-map entries reconciled per #15832) with one parameter added. Pin 5 proves a run held only in the durable store, over a restarted engine with an empty hot cache, still guards; ablation M2 turns it red.
  4. The store-listing failure posture — right, and the one place this diff is stricter than the listing. readSuspendedRuns('throw') rethrows the enumeration error before the listing's warn-and-degrade branch; 'degrade' keeps the listing byte-for-byte as it was, warning included. For the guard, an outage read as "no parked run" would be a fail-open onto the exact mid-run failure §7.3 refuses; refusing with the store's own error writes nothing (pin 6: the thrown value IS the outage error, no ledger row, the callee still executes). This changes a refusal's shape without moving the accept set: with a switched-off caller and an unlistable store the disable was refused 409 DELETE_RESTRICTED (every caller guarded) and is now refused with the store's error — refused either way, and the new answer is the honest one (an outage is not a dependency conflict). A flow whose callers are all armed never reaches the store, so it flips exactly as before; the dev's microtask fix (d59c97a50) is the await moved behind the switchedOff.length test, and flow-terminal-messages.test.ts (which does not await toggleFlow) is the pin that measured it — Test Core is green on it. AGENTS.md's degradation rule is met on both arms: the 'throw' arm hands the failure to the caller (no log owed), the 'degrade' arm keeps its recorded warn verdict, and the corrected catch-arm paragraph is now true (plugin-approvals does consume the listing and refuses on an absent entry, at approval-service.ts:5478–5486).
  5. The refusal keeps DELETE_RESTRICTED / 409 and names the run ids and the ADR-0044 door — right (answer A: "No new error code, gate or state… text extended by the run ids"). Envelope unchanged: code, status, and the subflowCallers key, which now lists exactly the guarding callers (the REST DELETE_RESTRICTED arm relays none of it, per stage 1's ①.10, so no wire shape moves). The text names each parked run id, the step per caller (disable … first for an armed one; cancel it through the operator cancel door, POST /automation/CALLER/runs/:runId/cancel (ADR-0044), or let it finish for a parked one), joined with ; when both kinds are present, and ends — or leave this one armed. The door is real: POST /:name/runs/:runId/cancel in the automation domain, gated by isRunLifecycleWrite, calling cancelRun — ADR-0044's run-cancel primitive. The domain-relative spelling (POST /automation/…, no /api/v1) matches the file's two existing refusal spellings of the resume door (:3278, :6414). No tracker number in the new refusal text; the ADR ids are the precedent's.
  6. resume() and resumeInternal() are untouched — right. The engine diff's twelve hunks lie in 4621–4930 and 8831–8999; resume is defined at :6199 and resumeInternal at :6705, and neither consults isFlowEnabled (the premise triage's answer rests on, still true at the head). Pin 3 (the control) proves the run the refusal protected then resumes THROUGH its subflow node to completed, and that once it has finished the disable completes — "or let it finish" is a real branch.
  7. The map pair — right (answer A's second pin). A parent parked at its map node is an ordinary suspended run (nodeId: 'call'), so it needs no special case; pin 2 parks one, gets the refusal naming the run and the door, cancels it through cancelRun, and the disable completes. Terminal runs are in neither store, so they never count (pin 1 runs the caller to completion first).
  8. The B6 cycle rider NARROWS the toggleFlow(name, true) accept set in one corner — right, and it is the corner stage 1's ③.4 carried in. In disabledPackagedSubflows the status is now read before the ledger-cycle exemption is asked, so if (ledger && !statusDisabled && ledgerDisabledChainReaches(...)) continue; skips only a child the ledger ALONE holds off. A child disabled both ways inside a ledger-disabled cycle was skipped whole (the caller was armed onto a child no enable order could re-arm — every run would fail at the node); it is now named with both reasons and both remedies. The exemption's own rationale ("every order is refused, so no sequence completes") never applied to a status: the publish door does not run through this guard. Pin 7 follows the remedy through (republish active, enable the child — the ledger-only cycle now exempts it — enable the caller) and ablation M4 turns it red. Within the stage-2 claim's file surface (disabledPackagedSubflows is named there), no new code or state, same RESOURCE_CONFLICT / 409.
  9. No public-surface change — right. All new members are private; listSuspendedRunsDurable keeps its signature and behaviour; IAutomationService in packages/spec is untouched; no export, spec key, route, query parameter or env var moves. TypeScript Type Check and its four sub-jobs are green.
  10. Test order and ablations — read from the commit list, not re-run. 97222b887 (7 pins, red) → 172680217 (fix) → 2fe58600a (changeset) → 5db7319c4 (the #10243 citation replaced by the ruling commit 266436a7f, which resolves, and isFlowAuthoringWrite, which exists) → d59c97a50 (the microtask fix). The seven ablation legs and the live showcase run are the dev's readings, consistent with the code and with the pins, and taken as such under Local-runs: none; the Test Core shards are the gate verdict on the pins.
  11. No hand-written page restates the old rule. content/docs, apps/docs and skills carry 0 hits for the disable refusal's prose or packagedSubflowCallers; the Docs Drift rows are the generic DELETE_RESTRICTED catalog entries and the trigger route, and stay accurate. Hygiene: no model identifier in title, body, either changeset or the diff; commits carry the model-free trailer pair.

② Semver level

  • The changeset .changeset/20678-subflow-disable-parked-run.md grades @objectstack/service-automation minor (a published package), carries the BREAKING banner, Clause-②: yes (narrowing), and exactly one ADR-0087 marker, not-required (no-migration-prescription). The PR body's line 2 carries the same declaration, as the post-task checklist requires.
  • Clause-②: yes (narrowing) is RIGHT; the claim's yes (widening) was wrong. Per scripts/pm/clause2-line.mjs the value answers whether the card widens an accept set or the public surface — it does (①.1: disables refused before are accepted) — and the arm names a narrowing that ALSO rides in the same diff — it does (①.8: an enable accepted before is refused). That is the grammar's own third case, "a diff that widens one surface and narrows another. Both facts are true and both are read." (widening) would have left the B6 narrowing undeclared, the [finding] An accept-set narrowing owes a **BREAKING** banner in core but not in platform-objects — and the ADR-0087 classifier reads the banner #16421 failure the arm exists to remove. The store-outage shape change (①.4) is neither: refused either way.
  • minor is RIGHT. AGENTS.md: yes takes at least minor; (narrowing) is BREAKING and, under the launch-window convention check-changeset-no-major.mjs enforces, ships minor with the banner and the disposition as the carriers. The level axis (yes ⇒ at least one moved package graded minor or above) is satisfied by the one moved package. That gate's step did not run in CI behind the foreign-changeset red (above); the dev's synthetic-payload run and this reading agree.
  • The ADR-0087 disposition is RIGHT for this shape. Nothing an author writes is removed or renamed, so no FROM → TO mapping and no tombstone is owed; no-migration-prescription's one mechanical refusal is a migration prescription in the body, and the body carries none (operator steps — cancel, publish, enable — not consumer code rewrites). That gate's step did not run in CI either; judged against its documented rule.
  • The deliberate correction of .changeset/20678-subflow-disable-sequence.md — CONFIRMED, sentence by sentence. This is stage 1's PENDING release note, present on the merge base, modified here (+2 / −2). Both notes are pending in the same .changeset/ and will be compiled into the same release, so the stage-1 note would otherwise describe, in that release, behaviour the same release changes; AGENTS.md's guardrail corrects a release note in its own entry, never by an erratum in a later one.
    1. Old: "Disabling a subflow is unchanged." Made false by this diff — ①.1 and ①.5 change exactly the disable direction (its accept set and its refusal text).
    2. New: "The disable direction of the same guard is described in its own entry, "disabling a packaged subflow completes once its packaged callers are switched off and hold no parked run"." True of the delivered tree: the quoted words are, verbatim, the opening of the new note's title line, and the new note is where the disable direction is described.
    3. Old (the cycle bullet under "Not refused"): "A subflow in a cycle of switched-off flows with the flow being enabled, including a flow that calls itself. Each flow in such a cycle would refuse the others, so no order could complete." Made false by this diff for the corner ①.8 narrows: unqualified, it promises a both-ways-disabled child in a ledger cycle is not refused, and it now is.
    4. New (appended to that bullet): "A subflow in such a cycle whose definition's status also disables it is still named, with its publish remedy: no enable order changes a status." True of the delivered behaviour: disabledPackagedSubflows reports it with ledger: true and its status, the refusal reads "switched off in the activation ledger, and its definition's status is 'invalid'" and prescribes "Publish 'pong' with status 'active' … and enable 'pong' first" (pin 7 asserts both strings), and toggleFlow moves the ledger bit only.
      Neither rewritten sentence is wrong. The Check Changeset red is this correction and nothing else — the log names the file and the foreign-changeset rule, the added changeset passed the empty-frontmatter rule — and it is read as red by design: route 0 in .github/workflows/pr-automation.yml and the gate's own two-class remedy say to leave it red and confirm on the PR; the workflow runs on pull_request only, not merge_group; and this comment is the PR-thread record of the gate and the reason. The skip-changeset label is correctly absent (it is refused for this class).

③ Boundary flags

  1. Open question 1 (confirm the correction) — ANSWERED: A, keep it. ② above is the confirmation the landing rule requires: the note is named and each rewritten sentence judged.
  2. Open question 2 (confirm yes (narrowing) + minor + BREAKING + not-required (no-migration-prescription)) — ANSWERED: A, keep as delivered. ② above. The dev measured instead of copying the claim line, as the order asked.
  3. Deviation: readSuspendedRuns extracted outside the claim's named functions — accepted. It is answer A's "one helper" requirement applied at the reader, the public method is a one-line wrapper, and the moved body is unchanged. The corrected catch-arm paragraph is true (①.4).
  4. Deviation: the citation fix — accepted. #10243 is replaced by the ruling commit 266436a7f and the in-tree record isFlowAuthoringWrite; both resolve at the head. Lint & Repo Gates (which carries check-issue-citations) is green.
  5. Deviation: the microtask fix — accepted, and the right side to fix. ①.4.
  6. The registerFlow / unregisterFlow doors (B7, measured live through the create door) — FILED as service-automation: ADR-0126 §7.3's packaged-subflow guard holds on the toggle door only — creating, republishing or deleting a flow can still arm a packaged caller onto a disabled packaged subflow #20725 by the seat. Out of this card's scope (triage's answer is toggleFlow's); the invariant now holds on the toggle door in both directions and on no other.
  7. The toggle door's 400 "Package is required" on a customer-authored flow — FILED as automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726. Predates this PR; not changed by it.
  8. Boundary, the listing cap — ESCALATED to the seat for a carrier; not blocking. ObjectStoreSuspendedRunStore.list() reads at most 1000 paused rows deployment-wide. Past that, a switched-off caller's parked run can be absent from the enumeration and the guard accepts the disable — the same fail-open class ①.4 closes for an outage, arriving silently. It is pre-existing store behaviour shared by the listing and by the boot's wait re-arm, and triage's answer names this reader, so it is not a defect of this PR; but "Carrier: none" undersells it. Recommend the seat file it (or fold it into service-automation: ADR-0126 §7.3's packaged-subflow guard holds on the toggle door only — creating, republishing or deleting a flow can still arm a packaged caller onto a disabled packaged subflow #20725's family): a per-flow, bounded question to the store would close it without touching this guard's shape.
  9. Boundary, a run executing (not parked) at the instant of the disable — by ruling. In neither store; triage ruled on "holds a parked run". Noted, not a flag against this PR.
  10. Boundary, a parked run parked AFTER its subflow node — by ruling, in the conservative direction. "Holds a parked run" counts a run that can no longer reach the node; the refusal it produces still names a completable step. Over-refusal, never fail-open.
  11. Boundary, restored strands — read, not measured, by the dev; accepted as noted.
  12. ADR-0126 §7.3's heading, "attached to disable" — carried by the seat to the maintainer. Extended, not reversed (stage 1's ①.8 and ③.5); no ADR text is touched here and none is owed by this PR.
  13. Base drift: main has moved 6 commits past 679f95ec5; measured over the fetched refs, none touches packages/services/service-automation, either changeset, the automation domain or error-response.ts. The queue's merge-ref build is the second CI round.
  14. UI rendering of the extended 409 text on the Setup packaged-automation board — NOT MEASURED, declared. No route or body shape changed; the board renders the server message.

Implemented-by: claude/issue-20678-subflow-disable-parked-run
Reviewed-by: session_01XY5uCwTjZj7884yYtyur4H

VERDICT: PASS

Landing: every check on this head is green or an expected skip except Check Changeset, which is red by design and confirmed above under the three conditions; this record covers the head; the owning seat lands it through the queue. The merge closes #20678.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing with one red check, by design · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-29T22:29Z

The gate: Check Changeset (.github/workflows/pr-automation.yml, job changeset-check). It is the only non-success, non-skipped check on d59c97a5. The seven required contexts are green.

The reason: this PR corrects .changeset/20678-subflow-disable-sequence.md, stage 1's PENDING release note, which exists on the merge base. Two of its sentences are made false by this diff.

  • That is the DELIBERATE CORRECTION class of scripts/check-empty-changeset.mjs's foreign-changeset rule.
  • By that gate's own design it stays red for this class, whatever the author does ("this gate stays red either way"; route 0 in the workflow).

The confirmation: the at-tier contract review PASS on this same head, 5900305615. It names the note and judges each rewritten sentence: both old sentences are made false by the diff, and both new sentences are true of the delivered behaviour.

The three conditions for landing with it red, each met:

  1. The gate's source says it is red by design on the pushed branch: pr-automation.yml route 0, and check-empty-changeset.mjs's DELIBERATE CORRECTION remedy.
  2. It does not run on merge_group. pr-automation.yml triggers on pull_request only.
  3. This comment records the gate and the reason.

Also recorded: because that job stops at this step, check-changeset-no-major and check-adr-0087-registration did not run in CI on this head.

  • The dev ran both locally against a synthetic payload carrying this PR body, and both exited 0.
  • The review judged both against their own rules.
  • Neither gate's verdict is read from silence.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 22:30
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 36d043b Sep 29, 2026
35 of 36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20678-subflow-disable-parked-run branch September 29, 2026 22:49
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… commits and ADR that decided them (objectstack-ai#20816)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the fourteenth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-automation/src/**` and nothing else. By the
seat's claim (`5905919247`), it is the largest package left in the lane,
and it was free once the `engine.ts` work of the previous holder landed
as `c8111a575`. Later stages cover the other packages, so this PR says
`Part of` and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by the method of stages 1 to 13 (the latest is PR
objectstack-ai#20789, landed as `8acdae9d8`). That is **73 sites on 73 lines in 27
files, covering 14 numbers**:

- 44 census sites (every census site this package has at the base);
- 24 sites in test comments, which the census defers; 3 of their numbers
(`objectstack-ai#11504`, `objectstack-ai#16709`, `objectstack-ai#8778`) stand only in test files here, and each
was read on its own and answers 404;
- 5 sites the census grammar cannot see: the `objectstack-ai#13398-class` spelling (a
hyphen after the digits), 2 in `engine.ts` and 3 in test files.

Each rewritten line now cites the record in this repository that decided
what the line describes, and says in its own words what was decided:
**15 distinct commit shas, plus ADR-0126 §7.2** on 2 lines, per ruling
C's order (the ADR first where it records the decision; see the
per-number table). No number was dropped.

Only comments changed. Every touched source file keeps its line count
(76 lines out, 76 in, over 27 files), so no line citation into these
files moves. 73 of the 76 changed lines carried a dead citation; the
other three are listed under Wordings. No code token moves (see the
guard below).

**No citation number is added.** The only tracker numbers on added lines
are the live `objectstack-ai#14095`, `objectstack-ai#14456`, `objectstack-ai#8287` and the cross-repo
`hotcrm#1206`, each once and each on the line it already stood on. No
number is new to the diff, no number grew, and no PR number is the
citation on an added line.

17 dead sites are left on purpose: 16 test titles and 1 runtime string
(see the list below).

One more file: a `patch` changeset for
`@objectstack/service-automation`, because the rewritten prose ships
(see Changeset below).

## Census: `service-automation`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/services/service-automation/`. Each run counts as a reading
only because its board frontier equals the newest issue or pull-request
number, read by a separate request just before and just after the run.
In two of the three runs a new number was opened while the run was
enumerating; each frontier equals the newest number at the run's end,
which is the criterion (stages 7, 11 and 13 met the same shape).

| reading | tree | board | whole-repo `allocated-but-absent` |
service-automation sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `8acdae9d8`, run 2026-09-30T07:03:52Z to 07:07:25Z |
enumerated, 187 pages, frontier objectstack-ai#20798 (newest objectstack-ai#20796 before, objectstack-ai#20798
after) | 796 | **44** | 44 | 11 | 11 |
| after | `3511e88cc` (comments and changeset), run 07:32:02Z to
07:35:27Z | enumerated, 187 pages, frontier objectstack-ai#20803 (newest objectstack-ai#20803 before
and after) | 752 | **0** | 0 | 0 | 0 |
| after, final head | head `a602c4003`, run 07:58:50Z to 08:02:19Z |
enumerated, 187 pages, frontier objectstack-ai#20809 (newest objectstack-ai#20807 before, objectstack-ai#20809
after) | 752 | **0** | 0 | 0 | 0 |

The whole-repo drop is 44, exactly this diff's census sites. The
`resolves` tally is 33,096 in all three runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (1,003) did
not move either. No run was truncated or discarded: all three
enumerations read 187 pages at the newest frontier.

The seat's census counted 45 here at `6bff748b`. The difference is one
`objectstack-ai#10243` comment line that `36d043be1` (PR objectstack-ai#20724) removed from
`engine.ts` in the meantime; the other seven commits since then add or
remove no dead site in this package's non-test `src`.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `namesThisRepository`
over every `.ts` file under `service-automation/src` (191 files). It
takes its verdicts from the before census's own board reading rather
than from a second enumeration: a number is dead when that census
reported it `allocated-but-absent`, and alive when the gate's own
census-scope extraction judged it and the census did not report it. 39
numbers are covered by neither, because they stand only in test files or
strings here; each was read on its own through the read-only tools (2
answer 404: `objectstack-ai#11504` and `objectstack-ai#16709`; 18 answer 200 as issues; 19 answer
200 as pull requests, 18 of them also the squash suffix of a commit on
`main`).

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `8acdae9d8` | 3,019 | **85** | 44 | 24 | 1 | 16 |
| after, `a602c4003` | 2,951 | **17** | 0 | 0 | 1 | 16 |

Its src-comment column equals the census's 44, which is the control on
the second instrument. The 2,874 live citations and 60 cross-repo
citations are the same in both readings, and the drop of 68 citations is
exactly the rewritten sites the gate's grammar can see. A third, raw
reading (every `#` followed by 2 to 6 digits, whatever surrounds it)
finds 3,078 occurrences before and 3,005 after: the 68, plus the 5
`objectstack-ai#13398-class` sites only this reading sees.

## Per-number table

Sites and files count every dead occurrence in scope at the base
(comments and strings, tests included, and the 5 hyphen-joined sites).
`rewritten / left` counts the sites rewritten and the sites left. Each
anchor was read in its message and diff, not only its subject.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#11060` | 17/4 | 12/5 | `815585513` (PR objectstack-ai#11347): flow value
expressions gain exactly `round` / `floor` / `ceil` / `abs` / `min` /
`max`, mirrored 1:1 from the CEL stdlib, and an unknown name in call
position becomes the loud `FlowExpressionFunctionError` instead of a
silent `null`. Its message records the maintainer ruling on `objectstack-ai#11060`
(2026-08-23, option A) and its diff names the number 18 times; `git
blame` puts 11 of the 12 lines in it, and the twelfth
(`end-node-refused-outcome.test.ts:333`) was written later. The lint
lane's anchor for the same number |
| `objectstack-ai#10243` | 14/5 | 13/1 | Three rungs, per line. **ADR-0126 §7.2** on 2
lines (`engine.ts:2315`, `flow-activation-ledger.test.ts:1024`): the
durable ledger row replaces the process-local `flowEnabled` map,
"retiring the objectstack-ai#10243 leak's mechanism rather than refining it"
(`docs/adr/0126-packaged-metadata-customization-model.md:339-340`),
which is what both lines say is the point. **`02b41232d`** (PR objectstack-ai#10996)
on 9 lines, the ones that say the map "measured" leaking: the recorded
measurement that tenant A's toggle answered 200 and tenant B and the
platform admin read the flow back off. **`266436a7f`** (PR objectstack-ai#11660) on 2
lines: it implements the maintainer ruling on `objectstack-ai#10243` (option A, toggle
joins the `manage_metadata` write set), which is the gate
`flow-activation-store.ts:67` says the difference "turned on", and it
wrote the "mitigating but not exculpating" record that
`flow-activation-ledger.test.ts:272` quotes. The runtime lane's anchors
for the same number |
| `objectstack-ai#14419` | 14/4 | 11/3 | `c5a7448d5` (PR objectstack-ai#14948): `create_record`
surfaces `engine.insert`'s classified `DUPLICATE_RECORD` code on the
node result, the engine copies it onto `$error`, and `try_catch`
preserves it across its own binding; deliberately scoped to
`create_record`. Its message's last line names `objectstack-ai#14419` as its card, its
diff names the number 13 times, and `git blame` puts 8 of the 11 lines
in it (the other 3 were written by later commits it precedes). The spec
lane's anchor for the same number |
| `objectstack-ai#13398` | 9/4 | 9/0 | `e238c79f0` (PR objectstack-ai#13592): the earliest text in
this repository that records the maintainer's published-sink ruling as
made — raising a log level by widening a published sink that declares no
`error` is "refused as actively harmful". Every line here states that
ruling ("forbids raising a site to `error` where doing so means GROWING
`error?` onto a published sink"). Stage 3's anchor, and the one the
stage-3 review recommended for this package |
| `objectstack-ai#16659` | 9/3 | 9/0 | `ecdfc9411` (PR objectstack-ai#17334): a time-triggered flow
declares its acting organization on its start node, the engine lifts it
onto the binding, and the triggers run the flow as it. `git blame` puts
the 4 `engine.ts` / `suspended-run-store.ts` lines in it. The 5 lines in
`notify-zero-delivery-visibility.integration.test.ts` were written by
`ae6dcf6a4`, an ancestor of `ecdfc9411`, in the future tense ("once
objectstack-ai#16659 lands"); they now name the landed commit (see Wordings). Stage
12's anchor |
| `objectstack-ai#13648` | 9/3 | 6/3 | `7307191db` (PR objectstack-ai#14388): the public `resume`
door normalises an absent signal to `{}`, and the chokepoints take a
non-optional signal, so a signal-less resume is held to the screen
contract. Its diff names `objectstack-ai#13648` 8 times; `git blame` puts 5 of the 6
lines in it |
| `objectstack-ai#13681` | 6/4 | 4/2 | `18d816a50` (PR objectstack-ai#14452): the spec half of the
contained-failure contract, which declares the run-level `failed`, the
loop iteration through `try` / `catch`, and row identity on `$error`.
Its subject names `objectstack-ai#13681`. The lines were written by the services
halves (`d30ccb9bd`, `b7225477b`), both descendants. The spec lane's
anchor for the same sentence ("one level up") |
| `objectstack-ai#17123` | 4/2 | 2/2 | `ae6dcf6a4` (PR objectstack-ai#17339): a `notify` node
reports `selected`, the recipients it addressed, so a zero-delivery run
stops reading like a run with nothing to notify. Its diff writes
`objectstack-ai#17123` 4 times, and `git blame` puts both lines in it. New to the
sweep |
| `objectstack-ai#8707` | 2/2 | 2/0 | `1408fe385` (PR objectstack-ai#8777): audit rows are stamped
from the record's own organization. Stage 7's anchor |
| `objectstack-ai#16709` | 2/1 | 1/1 | `8c7cca1ce` (PR objectstack-ai#16739): its item 1 pins the
restore verb's drop of a stale hot consumed-suspension copy, and it
created this test file. Stage 7's anchor |
| `objectstack-ai#10062` | 1/1 | 1/0 | `fa5d137ab` (PR objectstack-ai#12942): published `src` may
import only declared workspace dependencies; its diff moved this import
to `@objectstack/metadata-core`. Subject names it |
| `objectstack-ai#14390` | 1/1 | 1/0 | `9d7f7259f` (PR objectstack-ai#14603): both driver exits of
`engine.update` answer a unique violation with the `DUPLICATE_RECORD`
envelope. Subject names it. The runtime and rest lanes' anchor |
| `objectstack-ai#11504` | 1/1 | 1/0 | `f90e82024` (PR objectstack-ai#12611): registers
`FLOW_INPUT_SCHEMA_INVALID`, the line's subject; its diff names `objectstack-ai#11504`
5 times. The spec and runtime lanes' anchor |
| `objectstack-ai#8778` | 1/1 | 1/0 | `7901b2dd2` (PR objectstack-ai#8905): the stamp-only
`tenancy.organizationField` declaration the line names. Stage 6's anchor
|

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 15), and all 15 are ancestors
of the base (`merge-base --is-ancestor`, exit 0 for each; reverse leg,
base against each anchor, exit 1 for each; control legs exit 0: stage
1's landing `422db788a`, and the repository's root commit, which lies
deeper than every anchor; the history is complete,
`--is-shallow-repository` false, 15,178 commits). Each of the 14 numbers
answers 404 on the issues endpoint, which serves pull requests too, read
one by one.

No ADR, `scripts/adr-anchors/` file or other `docs/` page records the
decision of any of the 14 (a `docs/audits` census row names `objectstack-ai#10062` as
a gate's origin, and `docs/qa` checklist rows name `objectstack-ai#10243`; neither is
a decision record), except ADR-0126, which records the retirement
`objectstack-ai#10243`'s measurement led to (§7.2) and the operator gate (§5). The
`objectstack-ai#10243` lines that describe the measurement or the ruling cite those
commits; the two lines that describe the retirement cite the ADR.

## Wordings to check

- **Tag swaps in brackets or parentheses.** 「[objectstack-ai#16659]」 became 「[commit
ecdfc94]」 on 4 lines, 「[objectstack-ai#10062]」 became 「[commit fa5d137]」, and
every parenthesised `(#N)` became `(commit SHA)` in place.
- **Headings.** 「objectstack-ai#14419 —」, 「objectstack-ai#17123 —」, 「objectstack-ai#11060 —」, 「objectstack-ai#16709 item 1 —」 at
the head of a docblock or comment became 「Commit c5a7448 —」 and so on,
the form stage 13 used.
- **Two headers name the card or issue by its commit.**
`notify-zero-delivery-visibility.integration.test.ts:4` now opens 「The
card behind commit ae6dcf6:」, and
`flow-field-expression-scale.integration.test.ts:4` 「The end-to-end
oracle for the issue behind commit 8155855」. The docblocks below them
go on speaking of 「the card's reading」, 「the card's ⭐」 and 「the third
value-producing surface the issue names」; the headers keep that
antecedent. This landed as its own commit, `a602c4003`, and every
reading was re-run on it.
- **`objectstack-ai#13398`.** 「a objectstack-ai#13398-class raise: that ruling forbids」 became 「a
raise under the published-sink ruling (commit e238c79): that ruling
forbids」; 「outside objectstack-ai#13398's class」 became 「outside the sink ruling's
(commit e238c79) class」; 「(objectstack-ai#13398-class)」 became 「(the published-sink
ruling, commit e238c79)」.
- **`objectstack-ai#10243`.** 「the map objectstack-ai#10243 measured leaking」 became 「the map commit
02b4123 measured leaking」 (and alike on 9 lines); 「the whole point of
objectstack-ai#10243」 became 「the whole point of ADR-0126 §7.2」; 「the one objectstack-ai#10243
turned on」 became 「the one the toggle ruling (commit 266436a) turned
on」; 「objectstack-ai#10243 — the retired mechanism is GONE」 became 「ADR-0126 §7.2 —
the retired mechanism is GONE」.
- **`flow-activation-ledger.test.ts:271-272`.** 「the objectstack-ai#10243 map's "cold
boot reads enabled: true again" was recorded as
mitigating-but-not-exculpating」 became 「the retired map's … was recorded
(commit 266436a) as mitigating-but-not-exculpating」. The anchor moved
one line down, onto the verb it dates; `:272` is one of the three
changed lines that carried no dead number.
- **`crud-nodes.ts:439-441`, a statement that was stale when it
landed.** 「`engine.update` still leaks the raw driver error (objectstack-ai#14390, not
yet fixed) — those node results have nothing structured to surface yet」
became 「`engine.update` gained the same `DUPLICATE_RECORD` envelope for
a unique violation (commit 9d7f725), but those node results are
untouched here — this repair was scoped to `create_record` alone.」
`9d7f7259f` is an ancestor of `c5a7448d5`, the commit that wrote the
sentence, so the update door already carried the envelope when "not yet
fixed" landed; `c5a7448d5`'s message states the `create_record`-only
scope. `:439` and `:441` are the other two changed lines with no dead
number.
- **`objectstack-ai#16659` in the notify test, future tense.** 「Why objectstack-ai#16659 landing
does not close this」 became 「Why commit ecdfc94 does not close this」;
「the shape a scheduled flow has once objectstack-ai#16659 lands」 became 「the shape a
scheduled flow takes under commit ecdfc94」; 「as it fires once objectstack-ai#16659
lands」 became 「as it fires under commit ecdfc94」.
- **`objectstack-ai#13681`.** 「the measurement these tests reproduce (objectstack-ai#13681) found」
became 「the measurement behind commit 18d816a, reproduced here,
found」: the measurement was the card's, and `18d816a50` is the contract
that answered it.
- **`objectstack-ai#11060`.** 「the LOUD half of the objectstack-ai#11060 ruling」 became 「the LOUD
half of the ruling commit 8155855 records」; 「the exact silence objectstack-ai#11060
removes」 became 「the exact silence commit 8155855 removed」; 「before
objectstack-ai#11060 this wrote the field as undefined」 became 「before commit
8155855 …」.
- **`objectstack-ai#14419`.** 「a different door than objectstack-ai#14419's original bug」 became 「a
different door than the bug commit c5a7448 fixed」; 「the whole point of
objectstack-ai#14419」 became 「the whole point of commit c5a7448」.
- **`objectstack-ai#16709`.** 「objectstack-ai#16709 item 1 —」 became 「Commit 8c7cca1, item 1 —」:
the item numbering is that commit's own.

## The 17 sites left

- **Test strings, 16 sites on 16 lines**, all `describe` / `it` titles,
left as stages 1 to 13 left theirs:
`contained-failure-visibility.test.ts:184` and
`loop-dying-body-steps.test.ts:298` (`objectstack-ai#13681`);
`create-record-duplicate-code.test.ts:51`, `:133`, `:255` (`objectstack-ai#14419`);
`notify-zero-delivery-visibility.integration.test.ts:403`, `:535`
(`objectstack-ai#17123`); `screen-resume-signal-less.test.ts:81`, `:134`, `:187`
(`objectstack-ai#13648`); `template-functions.test.ts:44`, `:162`, `:202` and
`flow-field-expression-scale.integration.test.ts:83` (`objectstack-ai#11060`);
`flow-activation-ledger.test.ts:1027` (`objectstack-ai#10243`);
`stale-hot-consumed-suspension.test.ts:157` (`objectstack-ai#16709`).
- **One runtime string**: `builtin/template.ts:192`, the tail of the
unknown-function refusal ("(Before objectstack-ai#11060 this name was silently
rewritten to null …)"). A runtime string takes form D, not this card's
comment-only form C, and the shrink-only `doc-authoring-prose-id`
baseline already holds it (`template.ts`: `objectstack-ai#11060: 1`), so
`check:doc-authoring` sees no growth.
- No operator log string, assertion message, quoted maintainer ruling or
generated file in this package carries a dead number.
- **Outside `src`, listed and left, not edited in this stage:** the
shipping `README.md` names no dead number (`objectstack-ai#4336`, `objectstack-ai#4414`, both live).
`tsconfig.test.json` names the dead `objectstack-ai#13176` on 2 lines (5, 73) and the
dead `objectstack-ai#14916` on 1 line (54); its other numbers are live.
`vitest.config.ts` names only live numbers. The release-owned
`CHANGELOG.md` names 7 of the 14 numbers on 13 lines.

## Mechanical guard: no code token moves

The guard compares, base `8acdae9d8` against head, over all 27 touched
`.ts` files:

- **Reading 1**, the TypeScript parser's leaf nodes (a `forEachChild`
walk, so comments are trivia and JSDoc nodes are never visited). String
and template literals are therefore read in full.
- **Reading 2**, the full token stream in parser context (a
`getChildren` walk, so punctuation and keywords are included; JSDoc
nodes skipped).

Results:

- Real run at the final head `a602c4003`: 47,982 base leaf tokens, **0
files with a token change** on either reading (exit 0).
- Comment control in `engine.ts` (「which folds nothing and rejects
nothing.」 to 「which folds nothing and refuses nothing.」): 0 files
changed, as expected (exit 0).
- Positive control, a code token renamed in `engine.ts` (`function
applyResumeSignal(` to `function applyResumeSignalX(`): DIFFER on the
identifier (exit 1).
- Positive control, one digit changed inside a kept test title
(`flow-activation-ledger.test.ts:1027`, `objectstack-ai#10243` to `objectstack-ai#10244`): DIFFER on
the string literal (exit 1).

Every mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path, and each landed
(anchor 1 to 0, blob changed). Each restore was proven byte-identical to
the HEAD blob (`26e9be7dc174`, `e78e505fa3be`), with `git diff HEAD`
empty and a clean tree afterwards. The controls ran on `3511e88cc` and
again on `a602c4003`.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-automation`
(`.changeset/20596-service-automation-provenance-anchors.md`) is
included. Its body is stage 4's (`plugin-security`, the other stage with
an ADR anchor), word for word, with the package name changed.

Measured on the built package (A3), after a full workspace build in
which this package was a cache miss (71 of 71 tasks, at `9b0d34213`,
which holds every source-line change): `files[]` is `dist`, `README.md`
and `CHANGELOG.md`, and the package is not private.

- The declaration files `dist/index.d.ts` and `dist/index.d.cts` carry
the rewritten docblocks at `engine.ts:354`, `:362`, `:529`, `:2112`,
`:2119`, `:2315`, `:2331`, `:5224`, `:7984` and
`flow-activation-store.ts:67` (e.g. `02b41232d` 4 times, `c5a7448d5`
twice, `ecdfc9411` and `7307191db` once each).
- The JS entries `dist/index.js` and `dist/index.cjs` carry the kept
comments at `engine.ts:2315`, `:2331`, `:3565`, `:3581`, `:5224`,
`:7984`, `notify-node.ts:449`, `suspended-run-store.ts:714` and
`sys-automation-run.object.ts:112`.
- The other rewrites are stripped by the bundle or sit in test files.
- Positive controls, unchanged lines beside the rewrites, land exactly
where their neighbours do: the line before `engine.ts:2112` and before
`:2119` once in each declaration file and 0 in the JS; the
`FlowActivationStore` docblock opener beside
`flow-activation-store.ts:67` once in each declaration file; and the
neighbours of three stripped rewrites (`crud-nodes.ts:438`,
`suspended-run-store.ts:952`, `template.ts:24`) 0 everywhere.
- A never-written negative phrase appears nowhere in `dist`.
- None of the rewritten numbers is left in `dist`; the one `objectstack-ai#11060` in
each JS entry is the kept runtime string at `template.ts:192`.

The two commits after `9b0d34213` add the changeset and change two
test-file comment lines, which the bundle does not include.

## Gates (final head `a602c4003`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
exits 0 (self-test, 114 cases, 8 batteries). `node
scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged
the 2 citations the change adds on its surface (the live `objectstack-ai#14095` and
`objectstack-ai#8287`, each on the line it already stood on), and both resolve.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0 (the
sibling-package prose-id baseline holds, no growth).
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `a602c4003` (after a fresh fetch)
derived 63 commands. They are the 64 derived at dispatch less `pnpm
check:error-code-casing`, which the derivation now lists as a roster
family (below).
- Each ran with its exit code captured before any pipe, and all 63 exit
0; none exited 3.
- `--ran`, fed each command with its exit code, reports 63 run, 0 NOT
MEASURED (a derived zero), 0 unrun, and exits 0.
- A full `turbo run build` of `./packages/*` and `./packages/*/*` ran
first under the shared verify lock (71 of 71 tasks, exit 0), so no gate
hit an unbuilt workspace.
- The same 63 had also all exited 0 on `3511e88cc` before the referent
commit.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock, at `a602c4003`:**
- `pnpm --filter @objectstack/service-automation test`: 157 files pass
and 1,974 tests pass, every tracked test file under `src/`, the 16
touched ones included.
- `pnpm --filter @objectstack/service-automation typecheck` exits 0
(`tsc --noEmit` plus the test-layer check on `tsconfig.test.json`). `tsc
--listFiles` puts all 27 touched files in both programs.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 27 touched `.ts` files, gives 27 files, 0 errors and 0 warnings
(its `--format json` output). All 27 are in eslint's own population
(none reported ignored; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), so a comment
edit here cannot move the verdict on any untouched file. The repo-wide
`pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 28 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked** (objectstack-ai#20636).
At the base: `#N-word` 11 lines, of which the 5 `objectstack-ai#13398-class` lines
were dead and are rewritten here; the 6 left are live (`objectstack-ai#5912` twice,
`objectstack-ai#5048`, `objectstack-ai#5186`) or cross-repo (`hotcrm#548` twice). `#A/#B` 13 lines
of the number-slash-number shape, plus 6 lines where the slash follows
`ADR-0049` (`ADR-0049/objectstack-ai#1888`); every second number is live. `option #N`
none. URL-spelled none. So the claim's 11 / 13 / 0 / 0 hold, and at the
head the first is 6.
- **A sibling of the `option #N` position, dormant.**
`NON_CITATION_HEADS` also excuses a number after 「clause」, and
`suspended-run-store-consume-log-cause.test.ts:408` reads 「The
consequence clause objectstack-ai#6299 asked for」, a real citation of the live
`objectstack-ai#6299`. Across `packages/**/src` there are 4 such sites, all in test
files, a surface the gate defers anyway, so nothing dead hides there
today. Recorded for objectstack-ai#20636's family, not filed.
- **Two drifts left as they are, wording only.**
`notify-zero-delivery-visibility.integration.test.ts:72` still calls the
organization-less cron tick 「the shape production builds now」, written
before `ecdfc9411` landed; it carries no number and lost no referent
here. `suspended-run-store.test.ts:904` names
`tenancy.organizationField`, which `502f179cc` has since retired from
the authorable surface (stage 7 recorded the same drift in
`plugin-approvals`).
- **`update_record` still surfaces no `code`.** The corrected sentence
at `crud-nodes.ts:439-441` is now true that `engine.update` carries the
`DUPLICATE_RECORD` envelope while `update_record` does not surface it.
That is an observation with no reported pull, recorded here.
- **「The card」 phrases.** 134 comment lines in 50 files of this package
speak of 「the card」 or 「this card」. They carry no number and neither
instrument sees them. The two whose antecedent this diff would have
removed are handled above; the rest are unchanged, as in stages 8 to 13.
- **The census instrument did not truncate in this stage.** All three
enumerations read 187 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#11060` →
`815585513`; `objectstack-ai#10243` → `02b41232d` (the measurement), `266436a7f` (the
ruling) or ADR-0126 §7.2 (the retirement), per line; `objectstack-ai#14419` →
`c5a7448d5`; `objectstack-ai#13648` → `7307191db`; `objectstack-ai#13681` → `18d816a50`; `objectstack-ai#17123` →
`ae6dcf6a4`; `objectstack-ai#14390` → `9d7f7259f`; `objectstack-ai#11504` → `f90e82024`; `objectstack-ai#10062` →
`fa5d137ab`; and the reused `objectstack-ai#13398` → `e238c79f0`, `objectstack-ai#16659` →
`ecdfc9411`, `objectstack-ai#16709` → `8c7cca1ce`, `objectstack-ai#8778` → `7901b2dd2`, `objectstack-ai#8707` →
`1408fe385`.
- **Base.** The branch is on `main` at `8acdae9d8`. `main` has since
moved five commits (`41dcf1188`, `96e724475`, `df67985b0`, `cfa931535`,
`5bed1f6ca`). None touches `packages/services/service-automation/src`,
`scripts/check-issue-citations.mjs` or `.changeset/config.json`, and
none is a path in this diff. Two of them move gate inputs
(`scripts/doc-authoring-prose-id.baseline.json`, whose change is
`driver-sql` rows only, and one `scripts/adr-anchors/` file for
`packages/spec`), so those families ran here against the base's copies;
this diff moves no code token and no runtime string, so nothing here can
interact with them. No merge was taken; the merge queue rebuilds on the
merged generation.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ion run proved stale (objectstack-ai#21339)

Docs-only checklist revision from the 17.6.0 release-verification run
objectstack-ai#21330 (subject `617f25f8`, Console pin `31971ff1e28f`). Every change
follows the checklist README's lifecycle rule: the item's `revision`
bumps and one `history` entry says what changed, why, and cites the run.
No product code, no `content/docs/**`, no generated file.

## Stale clauses the run proved (each a FAIL in objectstack-ai#21330 with disposition
stale-clause / assertion-defect)

| item | rev | evidence | changed by |
|---|---|---|---|
| `access-security.audit-log-browser` | 2 → 3 | admin `GET
/data/sys_audit_log?filter={"action":"delete"}` → 0 rows; the row is
stored with correct attribution | `30c530e5` (objectstack-ai#21194): the ledger serves
a non-system reader, admins included, only rows about records it can
read |
| `api-backend.filter-comparand-conformance` | 2 → 3 | POST `/query` →
400 `VALIDATION_FAILED` at `query.where.f_number.$eq`; GET `$filter` and
engine → 400 `INVALID_FILTER`; no door returns rows | objectstack-ai#20116 (`cfc3bcf1`
objectstack-ai#20247, `dd1b8031` objectstack-ai#20325) — the split query-contract-matrix rev 3
already records |
| `api-backend.date-range-preset-matrix` | 1 → 2 | equality
`{"signed_on":"today"}` → 400 `INVALID_FILTER` (temporal door);
`$gte:"this_week"` → 400 with `bareDateRangePresetComparandMessage` | by
design: `18.filter-preset-ordering-comparand-refused.ts` judges ordering
positions only |
| `records-forms.import-transform-matrix` | 1 → 2 | 400
`UNSUPPORTED_TRANSFORM` names the missing sandbox, 0 rows — but no
`framework#2611` | `f115b1f` (objectstack-ai#21188): refusals state decisions in
words, not tracker numbers |
| `studio-authoring.view-authoring-live` | 1 → 2 | `GET
/meta/view?object=repair_asset` serves `repair_asset.default` /
`repair_asset.form` with the authored config; container name 0 hits | by
design: `expandViewContainer` (objectstack-ai#7163, objectstack-ai#7736, objectstack-ai#13407) |

## Expected-fail notes 17.6.0 has made pass (clauses held in objectstack-ai#21330;
only their framing was stale)

| item | rev | measured | fixed by |
|---|---|---|---|
| `automation.packaged-flow-subflow-disable-refusal` | 1 → 2 | caller
off → child's disable retry 200, ledger `active=false`; caller-first
enable 409 `RESOURCE_CONFLICT` | `36d043b` objectstack-ai#20724, `0d9349f` objectstack-ai#20759; the
enable guard is `679f95e` objectstack-ai#20711 (step 6 now enables the child first) |
| `automation.packaged-flow-clone-contract` | 1 → 2 | clone survives a
cold restart and fires; still unreachable from Studio | durability
`cb4c31d` objectstack-ai#20907; reachability now filed as objectstack-ai#21332 (clause unchanged,
still expected to fail) |
| `access-security.packaged-flow-write-door-parity` | 1 → 2 | `PUT` /
`DELETE /automation/showcase_urgent_task_alert` → 403 `NOT_OVERRIDABLE`,
flow unchanged | `4b45afae` (objectstack-ai#20817); knownGap names the existing pin
`packaged-flow-write-door-parity.dogfood.test.ts` |

No clause was weakened: each still refuses the original failure mode
(rows returned, a served delete row, a 200-with-zero-rows), and the
clone clause keeps its expected fail.

## Validation

- `node scripts/check-platform-checklist.mjs` → `OK — 15 areas, 269
items (265 active, 2 planned)`; symbol anchors and line-citation sweep
green.
- `api-backend.json` is re-serialized in its existing canonical 2-space
form; the other four files are edited in place in their existing mixed
formatting.

Not in this PR (listed on objectstack-ai#21330's close-out instead): the other
checklist-accuracy findings the run collected, and the two `planned`
picklist items, which can only be promoted by a run in which they pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants