Skip to content

feat(spec): declare AutomationContext.recordLoadDenied, the flow face of the caller-scope record-load signal - #15143

Merged
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-14244-automation-context-record-load-denied
Sep 4, 2026
Merged

feat(spec): declare AutomationContext.recordLoadDenied, the flow face of the caller-scope record-load signal#15143
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-14244-automation-context-record-load-denied

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #14244

The ruling (inherited from #14143, quoted where operative)

Triage 5502970354 (2026-09-02):

an additive key on the flow run's context that mirrors the handler face's signal, so a runAs: 'system' flow has something to guard on; ⛔ no narrowing of dispatchFlowAction, ⛔ no claim that runAs: 'system' is wrong. If the implementer finds a real semantic difference (e.g. the only viable fix is to refuse the start), that is a fork report back to the decision inbox, not a unilateral choice.

Unlock scan 5503608656 (2026-09-02):

the key must mirror the producer's spelling rather than invent a second one, and that spelling is now on main

Clause-②: yes — AutomationContext is a published @objectstack/spec contract, so this PR widens a published surface; needs:contract-review is hung on it for the in-seat contract review. No semantic fork found: the additive key is viable, nothing requires refusing the start, and dispatchFlowAction is not narrowed.

Scope: the spec half only

This PR declares the key on the contract, pins its shape, documents it, and ships a changeset. The runtime half — dispatchFlowAction and the REST /actions door actually passing the producer's signal into the run's context — is a separate domain:cli follow-up card the PM files from the read-and-report at the end of this body. ⛔ packages/runtime/**, packages/services/**, objectui and content/docs/releases/** are untouched.

Premise re-verified on origin/main (2cc46103, the branch base)

  • Producerpackages/runtime/src/action-execution.ts:1278: export function actionRecordLoadSignal(load: ActionSubjectRecordLoad): { recordLoadDenied?: true } (body at :1279: return load.recordLoadDenied ? { recordLoadDenied: true } : {};). Built by loadActionSubjectRecord at :1245:1270 (recordLoadDenied = true at :1260 and :1263, returned at :1269).
  • Contractpackages/spec/src/contracts/automation-service.ts:25: export interface AutomationContext { declares record / previous / object / event / userId / positions / permissions / tenantId / runAs / flowName / flowRunId / params and no recordLoadDenied (git grep -n recordLoadDenied packages/spec/src → 0 hits before this PR). Premise holds.
  • Handler-face wording mirrored from content/docs/ui/actions.mdx:341:371 ("Read it as ctx.recordLoadDenied === true. The key is absent, never false, when nothing was refused") and content/docs/automation/hook-bodies.mdx:241.

What changed (4 files, +201)

  1. packages/spec/src/contracts/automation-service.tsrecordLoadDenied?: true; on AutomationContext, directly under record: optional, literal true, absent otherwise, never false — exactly the producer's shape. The TSDoc states what it means (the dispatcher's caller-scope load of the subject row did not deliver it and record is the stamped id stub), who sets it (the runtime's action dispatch, both doors — REST POST /api/v1/actions/... and the MCP run_action bridge — through the one producer, cited by file and line on 2cc46103), what a flow guards on (runAs: 'system' flows especially; a runAs: 'user' flow re-derives the caller's scope and the stub resolves to nothing), what it does not claim (an RLS-hidden row and an id that names nothing both arrive as RECORD_NOT_FOUND, deliberately), and — plainly — that it is declared, NOT YET POPULATED on the flow face until the runtime half lands.
  2. packages/spec/src/contracts/automation-context-record-load-denied.pin.test.ts (new, beside the family's automation-result-status.pin.test.ts, same Eq / Assert form) — type-level identity AutomationContext['recordLoadDenied']true | undefined; identity with the producer's { recordLoadDenied?: true }; an additive positive control (a context literal without the key type-checks); @ts-expect-error on recordLoadDenied: false; a runtime anti-vacuity read; a service double guarding with === true without a cast; and a JSDoc-reading pin requiring the doc block to name loadActionSubjectRecord, actionRecordLoadSignal, both doors, run_action, runAs: 'system', dispatchFlowAction, "never false" and NOT YET POPULATED — so the day the runtime half lands, the test names the sentence to retire. It is inside the compiled test program: tsconfig.test.json includes src/**, and test-typecheck-debt.json lists the file nowhere, so check:test-typecheck requires it to compile with zero errors (it does).
  3. content/docs/ui/actions.mdx — "Authorization inside an action" gains a Flow actions paragraph: same key, same spelling, declared on AutomationContext, declared but not yet populated on the flow face; a runAs: 'user' flow needs no guard. node scripts/docs-audit/affected-docs.mjs at the final head lists exactly two pages: this one (edited) and content/docs/releases/v15.mdx (release-owned — ⛔ never edited in a code PR). No hand-written page lists AutomationContext's keys; outside the generated references/ tree the name appears only in ADRs, CHANGELOGs, READMEs and changesets.
  4. .changeset/automation-context-record-load-denied.md@objectstack/spec: minor (additive public-surface key). ADR-0087 marker: none owedscripts/check-adr-0087-registration.mjs judges only a changeset that declares a breaking change (its breakingDeclaration() matches **BREAKING** / major / feat!:); a minor additive key declares none, so none of the five marker spellings is used. Its --self-test and check-changeset-no-major --self-test are in the gate union below.

Generated products: pnpm --filter @objectstack/spec check:generated → "✓ All 15 generated artifacts are up to date." on the first run; --fix regenerated nothing (an optional interface member moves neither api-surface/ — "@objectstack/spec public API surface + factory signatures unchanged ✓" — nor export-origins/, declaration-map/, nor content/docs/references/**, which does not render contract interfaces); confirming run at 6c14ba889 exit 0, same line.

Measurements at 6c14ba889 (git rev-parse --short HEAD after the last commit; every exit captured by redirect before any pipe; verdict lines quoted from the gate's own output)

step command verdict
spec build os-verify-lock.sh -c 'pnpm --filter @objectstack/spec build' check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present. · os-verify-lock: VERDICT command-exit 0 · held the lock 102s
pin file os-verify-lock.sh -c 'pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/contracts/automation-context-record-load-denied.pin.test.ts' Test Files 1 passed (1) · Tests 3 passed (3) · VERDICT command-exit 0
spec typecheck + ledger os-verify-lock.sh -c 'pnpm --filter @objectstack/spec typecheck' (tsc --noEmit, check:scripts-typecheck, check:test-typecheck) check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json; 54 file(s) / 261 error(s) / 145 pinned signature(s) held in test-typecheck-debt.json · VERDICT command-exit 0
check:generated pnpm --filter @objectstack/spec check:generated ✓ All 15 generated artifacts are up to date. · exit 0 (first run, --fix, and the confirming run at the final head)
eslint, edited TS files node --stack-size=4000 node_modules/eslint/bin/eslint.js --no-inline-config over the two TS files exit 0, no findings
repo-wide lint pnpm lint (foreground) exit 0, zero findings, in full (eslint . --no-inline-config, 59s wall clock on the shared box) — no narrowing to prove
check:doc-authoring pnpm check:doc-authoring ✓ doc authoring guard: 393 files clean — no bare metadata literals. (+ 3 more ✓ lines) · exit 0
check:nul-bytes pnpm check:nul-bytes check-nul-bytes: OK (scanned 8232 text file(s) -- 8232 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes). · exit 0
downstream runtime os-verify-lock.sh -c "pnpm turbo run typecheck --filter=@objectstack/runtime --concurrency=2" (closure built via ^build) @objectstack/runtime:typecheck: cache miss, executing · check:test-typecheck: OK — @objectstack/runtime's test layer compiles under packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned signature(s) · Tasks: 30 successful, 30 total · VERDICT command-exit 0 · held the lock 270s — unaffected
downstream service-automation os-verify-lock.sh -c "pnpm --filter @objectstack/service-automation typecheck" (direct, cache-bypassing) NOT MEASURED as a gate — the package has no typecheck script (ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT; it is ledgered type-check debt). Raw npx tsc --noEmit --pretty false at 6c14ba889 under the lock: 3 errors, all src/nested-region-parity.test.ts: error TS2341: Property 'flows' is private and only accessible within class 'AutomationEngine'., none naming recordLoadDenied or AutomationContext; the base-commit count was not measured, so this is a raw reading, not a verdict
census node scripts/check-system-context-census.mjs (after the last edit) check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read. · exit 0
gate union node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands gate list derived from the tree of 'objectstack-ai/objectstack' at commit 6c14ba889 · change set 4 paths vs merge base e37456eb0 (committed 4, working tree 0, untracked 0) · 81 commands — see below

Gate union (81 commands at 6c14ba889): 79 exit 0 at the final state, 2 NOT MEASURED, reported in their own words:

  • pnpm check:dual-build-cjs-loads → exit 3: "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/. … Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." (38 packages are unbuilt in this worktree; CI's full build owns it.)
  • pnpm check:type-check-debt → exit 3: "check-type-check-coverage: PREREQUISITE NOT MET … ⛔ This is NOT a pass and NOT a finding: nothing was measured" (same reason: measures with every package's closure built).
  • Two runs that first said "Build first" and were then measured green: check:skill-examples needed @objectstack/client-react built (built under the lock, then "✅ 257 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them", exit 0); check:api-surface, check:browser-reachable-entries, check:dual-source-exports, check:entry-nameability reported "packages/spec/dist/**/*.d.ts is OLDER than packages/spec/src" because the reverse-verification touch (below) had left the source file newer than dist (bytes identical, hash-proven) — the mtime probe is working as designed (gen:api-surface / gen:export-origins read the dist without asserting it is FRESH — a stale dist launders a phantom breaking removal into the committed baseline #7122); spec was rebuilt and all four re-ran green ("@objectstack/spec public API surface + factory signatures unchanged ✓").
  • check:react-declaration-paritycheck:generated reports it "Cannot run here" (needs objectui's manifest); unchanged posture.

Reverse verification (one leg, from the committed state at 6c14ba889)

Precondition git status --porcelain empty. Mutation: delete only the recordLoadDenied?: true; line (TSDoc kept, so the JSDoc-reading pin fails on the missing declaration, not on missing prose). Landed on disk: grep -c 'recordLoadDenied?: true;' 1 → 0; git diff --stat: 1 file changed, 1 deletion(-).

  • tsc --noEmit --pretty false -p tsconfig.test.json → exit 2 (the whole test program, which always carries the ledgered debt); the pin file contributes 7 new errors: error TS2339: Property 'recordLoadDenied' does not exist on type 'AutomationContext'. ×6 (lines 53, 57, 68, 69, 77, 79) and error TS2353: Object literal may only specify known properties, and 'recordLoadDenied' does not exist in type 'AutomationContext'. (line 62). Under check:test-typecheck an unledgered file with errors is red.
  • vitest run on the pin → Tests 1 failed | 2 passed (3): the JSDoc-reading pin fails with AssertionError: expected -1 to be greater than -1 (declaration string absent). The two runtime-only tests stay green by design — esbuild strips types; the type-level pins are tsc's.
  • Direction observed: red, as predicted.
  • Restore by trap: git checkout HEAD -- ABSOLUTE_PATH (index and tree) exit 0. Proof: git hash-object of the file = 1bb555b762fbdd2a5db25e1f98d64acb419a93ac = git rev-parse HEAD:packages/spec/src/contracts/automation-service.ts; git diff HEAD --stat empty; git status --porcelain empty; declaration count back to 1.

Deviations

  • Merged origin/main (e37456eb0, one commit: packages/metadata-protocol + scripts/engine-double-contract.pinned.json + its changeset — none of this PR's files, nothing under packages/spec, packages/runtime or packages/services) before opening, because dispatch-gates flagged the pinned ledger as a stale derivation input; the final head is the merge commit and every measurement above was taken there. No conflicts.
  • The runtime typecheck was run through turbo (^build builds the closure); the service-automation typecheck was additionally run directly because turbo answered it from the shared .turbo/cache (a replay is not a measurement of this tree).

Read-and-report for the runtime follow-up card (no edits; lines on 2cc46103, runtime and services unchanged by the merge)

Where dispatchFlowAction receives record. packages/runtime/src/action-execution.ts:791wiring: { objectName; record; params; recordId?; ec; envId? } (:794:801); it hands automation.execute(action.target, {…}) the context literal at :820:828: record, object (unless object-less), userId, positions, permissions, tenantId, params: seedFlowActionParams(…) (:827). No load signal anywhere in that literal.

Where the producer's recordLoadDenied is in scope at each call site.

  • MCP run_action arm: packages/runtime/src/domains/mcp.ts:649actionExec.invokeBusinessAction(...) (action-execution.ts:1300); inside it const subject = await loadActionSubjectRecord(objectName, recordId, …) at :1383, const record = subject.record; at :1385; the flow branch if (action.type === 'flow') at :1406 calls dispatchFlowAction(deps, requestContext, action, { objectName, record, params, recordId, ec, envId }) at :1407subject is in scope and its signal is dropped; the handler face two branches down spreads ...actionRecordLoadSignal(subject) at :1435.
  • REST /actions door: packages/runtime/src/domains/actions.ts:623 const subject = await actionExec.loadActionSubjectRecord(…), :625 const record = subject.record;; the handler actionContext spreads ...actionExec.actionRecordLoadSignal(subject) at :657; the flow branch if (actionType === 'flow') at :682 calls actionExec.dispatchFlowAction(deps, _context, actionDef, { objectName, record, params: reqParams, recordId, ec, envId }) at :685 — same drop.
  • Natural wiring for the card: extend dispatchFlowAction's wiring with the ActionSubjectRecordLoad (or its signal) and spread ...actionRecordLoadSignal(load) into the context literal at :820 — never onto record, and never subject.recordLoadDenied raw (ActionSubjectRecordLoad.recordLoadDenied at :1203 is the internal boolean; the optional-true form is the contract).

Does seedFlowActionParams copy record keys into params? Yes — action-execution.ts:618; const seeded = { ...record }; at :628 copies every record key, then the id aliases, then return { ...seeded, ...params }; at :661. So the key leaks into params only if it is put on record; carried as a sibling context key (as the producer's actionRecordLoadSignal already does on the handler face) it never reaches params — and therefore never reaches seedDeclaredVariables' isInput bindings either.

What automation.execute(target, ctx) does with an unknown context key today. packages/services/service-automation/src/engine.ts:4082: it reads context?.record for the re-entrancy key (:4130); seedRunVariables (:8176) seeds only record (and its flattened fields), previous, $runId / $flowName / $flowLabel, on top of seedDeclaredVariables (:8108, which reads context.params[v.name] for isInput variables) — an unknown context key is never seeded into the variable environment, so no start-node condition or template can see it until the engine seeds it deliberately; resolveRunContext (:3936) spreads ...(context ?? {}) at :3943 into the run context — carried verbatim into resolveRunDataContext and into persistence: suspended-run-store.ts:654 and :704 write context_json: JSON.stringify(run.context ?? {}), restored at :672 / :752. Today, then: carried and persisted, read by nothing. The follow-up owes a decision on whether the flow face reads it from the context (a runAs-aware guard in the engine) or seeds it as a variable so authors can guard in CEL — the contract as declared supports either.

Do any flow-side test doubles or fixtures construct AutomationContext in a way a new optional key breaks? No. git grep over packages/ for keyof AutomationContext, Required / Omit / Pick of AutomationContext, and satisfies AutomationContext → 0 hits. Literal constructions: service-automation/src/builtin/crud-runas.test.ts:145 (const trigger: AutomationContext = { userId: 'u1' }), loop-dying-body-steps.test.ts:161 / :391 ({ event: 'schedule' } as AutomationContext), map-node.ts:111 / :156 / :172 and loop-node.ts:98 / :137 ({} as AutomationContext fallbacks), qa/dogfood/test/flow-runas-schedule.dogfood.test.ts:40, runtime/src/endpoint-executor.ts:536 (cast from buildAutomationContext, which returns a plain record — domains/automation.ts:80). All optional-key tolerant; confirmed by the @objectstack/runtime and @objectstack/service-automation typechecks above.

Not in this PR

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i


Generated by Claude Code

… of the record-load signal

Additive key `recordLoadDenied?: true` on `AutomationContext`, mirroring the
producer's spelling (`actionRecordLoadSignal` in the runtime's
`action-execution.ts` returns `{ recordLoadDenied?: true }`), with a type-level
pin (`true | undefined`, additive positive control, `false` refused) and a
minor changeset. Declared, not yet populated on the flow face — the runtime
half is a separate card.

Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
Co-authored-by: Claude <noreply@anthropic.com>
…tomationContext, not yet populated

Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
Co-authored-by: Claude <noreply@anthropic.com>
…t-record-load-denied

Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/ui/actions.mdx (via AutomationContext (symbol, a top-level interface), recordLoadDenied (symbol, a field of interface AutomationContext), /api/v1/actions/... (route, a path literal in AutomationContext))

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

  • content/docs/releases/v15.mdx (via AutomationContext (symbol, a top-level interface))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 4, 2026
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Contract review (Clause ②) — PASS · ACCEPT

Reviewer of record: the domain:spec PM seat, session session_0174WZTU6XcFcS7g2kykC53i (seat post #6017). Tier fuse: get_session read at 2026-09-04T03:22Z — both model fields equal CONTRACT_REVIEW_TIER. Gate: check-clause2-carriers.mjs --pair 15143 exit 0 at 03:22Z (the Clause-②: declaration is in the dev's own claim comment 5534863694; both carriers hung). Report 5535186894 (03:21Z) read against GitHub and the fetched branch, not against its own prose.

Head and window (readings at 2026-09-04T03:23Z)

  • Report head 6c14ba88 = PR head.sha = branch tip (the branch carries a merge of origin/main e37456eb taken before the PR opened — none of the PR's four files touched by it). Trial merge onto origin/main 1876d5df: clean. Governed test: 0 of 4 changed files. No content/docs/releases/ edits (the release page the docs-drift advisory named is read-only and untouched). H17 index: no hit.
  • CI on 6c14ba88 at 03:22Z: 35 checks — 13 success, 5 skipped, 17 running, 0 red. Landing waits for all-green.

The widening, verified in the diff

  • The key: AutomationContext.recordLoadDenied?: true (packages/spec/src/contracts/automation-service.ts, +36 lines: one member and its TSDoc) — exactly the producer's shape (actionRecordLoadSignal(load): { recordLoadDenied?: true }, packages/runtime/src/action-execution.ts:1278 on 2cc46103, built in loadActionSubjectRecord), mirrored, not respelled; true when the caller-scope load did not deliver the row (record is the stamped { id } stub), absent — never false — otherwise; both doors named; the honesty clause "declared, NOT YET POPULATED on the flow face" and dispatchFlowAction named as the runtime half. The inherited ruling's two ⛔ hold: no narrowing of dispatchFlowAction, no claim that runAs: 'system' is wrong; no fork report was needed (the additive key is viable).
  • Pins (automation-context-record-load-denied.pin.test.ts, inside tsconfig.test.json's program, unledgered so it must compile with zero errors): type-level Eq of the key with true | undefined and with the producer's ProducerSignal['recordLoadDenied']; an additive positive control (a pre-finding: a flow-type action's AutomationContext gets the same stamped record stub when the caller cannot read the row — and the flow face has no recordLoadDenied #14244 context literal); @ts-expect-error on recordLoadDenied: false; the documented predicate === true exercised through an IAutomationService implementation without a cast; a JSDoc-reading pin that requires the producer, both doors, runAs: 'system', the not-yet-populated clause and the absence semantics to stay in the doc block — and exactly one declaration of the key in the file.
  • Docs: content/docs/ui/actions.mdx gains a "Flow actions" paragraph with the same honesty (the one hand-written page the drift check named; no page lists AutomationContext's keys). Generated products unchanged by construction (api-surface records export names; references do not render contract interfaces) — check:generated 15/15, --fix regenerated nothing.

Report checklist

  • Reverse verification: one leg from the committed state — the one declaration line deleted (grep 1→0, --stat 1 deletion); tsc -p tsconfig.test.json exit 2 with seven new errors all in the pin file (TS2339 ×6, TS2353 ×1, lines quoted); vitest 1 failed | 2 passed (the JSDoc pin); restore proven by git hash-object = HEAD blob 1bb555b7…, empty git diff HEAD / porcelain. Direction red, as predicted.
  • Gate readings at 6c14ba88: spec build (check-dts-emitted 34/34), pin file 3/3, spec typecheck + check:test-typecheck ledger unchanged (261 / 145 — the new file adds zero errors), check:generated 15/15, eslint 0, the full pnpm lint in the foreground 0 findings, doc-authoring clean, nul-bytes OK, census OK; downstream @objectstack/runtime typecheck through turbo (30 tasks) exit 0 — unaffected; service-automation raw tsc 3 pre-existing errors in nested-region-parity.test.ts, none naming the key; dispatch-gates --commands 81 derived, 79 exit 0, NOT MEASURED by their own words: dual-build CJS loads, type-check debt (both need the whole-repo build); check:skill-examples green after building client-react; four dist-reading spec gates re-run green after a rebuild (the reverse leg's touch had left src newer than dist).
  • Changeset: @objectstack/spec published, changeset present — minor, additive; no ADR-0087 marker owed (the gate judges only declared-breaking changesets; none of the marker spellings used — stated). Accepted.
  • Scope: 4 files = the declared surface (contract + pin + one docs page + changeset); no packages/runtime/**, packages/services/**, objectui edits. Closing keyword, two reads (03:22Z): first line Fixes #14244; no other closing keyword beside a card number. Fixes is correct — this card is the spec half as triaged (domain:spec, the contract surface); the runtime half is the card below.
  • MCP-call accounting: 0 GitHub MCP + 1 subscribe_pr_activity. Head pinning: report head = PR head; the local gate readings are live.
  • Read-and-report accepted (file:line in the PR body): dispatchFlowAction :791 / context built :820–:828 without the signal; MCP run_action arm (domains/mcp.ts:649invokeBusinessAction :1300, subject :1383, dropped :1407, handler spread :1435); REST door (domains/actions.ts:623 / :657 / :685); seedFlowActionParams :628 copies record keys into params (:661) — carry the key as a sibling context key, not on record; engine.execute seeds only record/previous/params into variables (engine.ts:4082, :8176, :8108), resolveRunContext :3943 spreads unknown context keys verbatim, persisted by suspended-run-store.ts:654/:704; no exhaustive AutomationContext construction anywhere (grep 0 hits).

Seat ruling on the report's open question (open to the maintainer's veto)

Runtime half — A first: context-only, mirroring the handler face exactly; the engine (or a runAs: 'system'-aware guard in it) reads context.recordLoadDenied. Seeding a run variable for a start-node CEL condition (B) is a separate authoring-surface decision, stated on the runtime card as an open option, not taken there by default.

Follow-up card this seat files at landing

domain:cli — the runtime half: dispatchFlowAction on both doors spreads actionRecordLoadSignal(subject) into the run's context as a sibling key (never onto record); the engine reads it; a pin that a flow started on an unreadable row sees recordLoadDenied === true and a readable one sees it absent; the JSDoc's "NOT YET POPULATED" sentence retired in the same stroke (the spec pin tells the author which sentence).

Landing

At all-green on 6c14ba88: provenance → ready → squash auto-merge → added_to_merge_queue. On MERGED: strip pm:dispatched, probe contracts/automation-service.ts on origin/main, landing note, the runtime card filed.

needs:contract-review is cleared on this PR and on #14244 in this stroke (read-modify-write, both read back). If the head moves after this clear, the gate's C3 reading re-hangs it and the delta is reviewed before it is cleared again.


Generated by Claude Code

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Landing provenancedomain:spec PM seat, session session_0174WZTU6XcFcS7g2kykC53i (seat post #6017). Contract review (Clause ②) PASS · ACCEPT: comment 5535204682; needs:contract-review cleared on both carriers in that stroke.

Landing window readings, all on head 6c14ba88 at 2026-09-04T03:50Z (latest run per check name):

  • CI: all green — {('completed', 'success'): 30, ('completed', 'skipped'): 4} (0 red, 0 running); the required floor (Lint & Repo Gates, Type Check · workspace / · source gates / · consumer gates / · debt ledger, Check Changeset, Build Core, Temporal Conformance (live PG + MySQL)) all success on this head.
  • Trial merge onto origin/main ebb082204: clean.
  • check-clause2-carriers.mjs --pair 15143: exit 0 (head unchanged since the clear — no C3). No content/docs/releases/ paths in the PR's file list.
  • Closing keyword, two reads: first line Fixes #14244; no other closing keyword beside a card number in the body (read at review time; body unchanged since).

Sequence: ready → squash auto-merge → added_to_merge_queue. On MERGED: strip pm:dispatched from #14244, probe on origin/main, landing note on the card, the follow-up card named in the review filed.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 4, 2026 03:51
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 63cd487 Sep 4, 2026
43 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-14244-automation-context-record-load-denied branch September 4, 2026 04:33
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