Skip to content

fix(trigger-record-change)!: a record-change flow's trigger record carries the credential mask and omits internal fields - #21928

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-21867-run-state-trigger-record-mask
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-21867-run-state-trigger-record-mask

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21867
Clause-②: no

What this changes

Ruling A on #21867 (director's record 5995381726, alignment note 6005796816): mask at the source. RecordChangeTrigger.buildContext (packages/triggers/trigger-record-change/src/record-change-trigger.ts) now projects both roots it hands a flow, record and previous, through the one helper omitInternalFieldsFromWriteResponse (@objectstack/core, packages/core/src/utils/internal-write-response.ts), with the trigger object's definition. A credential-class field (every secret field, and every password field outside the exempt managedBy buckets, per ADR-0100 and isMaskedOnReadFieldType) carries SECRET_MASK, or null when unset. A field declared internal: true is omitted. params is the same object as record, so it inherits the projection.

  • Applied last. The projection runs after hydration, after declared-field materialisation and after the decoupling copy, so no later layer brings a clear value back. It runs in place on the decoupled copies only, so the engine's ctx.result / ctx.previous / ctx.input, which are shared with every other binding and hook on the write, are never touched.
  • The definition is read regardless of ground truth. Materialisation is gated on persisted state; the mask is not. A new private readObjectDefinition reads the engine's optional getObject accessor. When the definition cannot be resolved (accessor absent, no answer, or a throw), the flow still dispatches unmasked, and readObjectDefinition logs that once per object at error through the plugin logger, naming the object. The bind-time existence probe only warns and still binds; nothing upstream refuses an unknown object.
  • Downstream inherits it, with no second copy. The variables map (record, $record, previous), SuspendedRun.context, the persisted variables_json / context_json, the run read doors, and the run a resume rehydrates, in-process and after a restart. ⛔ No mask in service-automation or in the suspended-run store. ⛔ No other variable is filtered (finding: the whole /automation read domain is gated only by "authenticated" — run-detail returns the triggering record's fields without that record's own FLS #7900 stands).

Premises verified before writing (at origin/main dcb11c2ec9)

  1. The definition is reachable in buildContext. this.engine.getObject is already read there for materialisation. Re-check grep: 9 hits in record-change-trigger.ts.
  2. The projection is the last overlay. The last layers are materialisation, then decoupleFromEngineState on both roots, then the return. The projection sits between the decoupling and the return.
  3. No shipped flow reads a credential-class field off its trigger record. The card's grep over examples/**/*flow* and examples/**/flows/** returns zero hits (git grep exit 1). Control: the same paths carry record.FIELD reads in 4 files, so the zero is not a dead pattern. The only example object with password / secret fields is showcase_field_zoo. Its one record-change flow (showcase_approver_bindings, status: 'draft') reads neither field.
  4. Only the trigger's own record enters here. get_record and the other CRUD nodes (service-automation/src/builtin/crud-nodes.ts) read through data.find / data.findOne, the engine's generic read path, which ADR-0100 already masks. Nothing here touches those nodes.

Pins

  • packages/triggers/trigger-record-change/src/trigger-record-credential-mask.test.ts (unit, fake engine, 13 cases):
    • password and secret carry the mask on record and on previous, and the internal field is omitted.
    • An ordinary field keeps its value, and params is the same object as record.
    • An unset credential reads null.
    • The engine's hook objects stay whole.
    • Insert events are masked too.
    • A better-auth-managed password keeps the read path's exemption.
    • afterDelete (record from the prior row) and beforeUpdate (payload over the prior row) are masked on both roots.
    • Each of the three unresolved-definition shapes (accessor absent, no answer, a throw) logs one error naming the object, while the flow runs on both writes.
    • A resolved definition logs no error.
  • packages/qa/dogfood/test/flow-trigger-record-credential-mask.dogfood.test.ts. A real boot: bootStack with automation, a file-backed database, the real crypto provider and the record-change trigger. It uses one object with an ordinary field, a password field, a secret field and an internal field, and one record-after-update flow that pauses at a screen node. The cases:
    • The scene is armed: the engine write result holds the stored values.
    • The paused row's variables_json and context_json carry the mask for both credential fields and omit the internal field (record, $record, previous), with no stored credential spelling anywhere in either column.
    • The data door over that row serves the same.
    • GET /automation/:name/runs/:runId shows the same.
    • After the resume, a node reading record.CREDENTIAL_FIELD / previous.CREDENTIAL_FIELD stores the mask, while the ordinary field stores its value.
    • The privileged resolveSecretField path still returns the plaintext.
    • A second suite pauses, stops the kernel, cold-boots a second kernel over the same file and resumes there. The post-pause node again stores the mask.
  • QA checklist: automation.paused-run-trigger-record-masked in docs/qa/platform-checklist/areas/automation.json. This is the item triage named as missing on the path "approvals and automation — flows run: errors, pauses and schedules". It covers reading a paused run's stored state as a non-privileged holder. automated.ref names the dogfood pin, and a knownGaps line says the pin reads as the admin.

Upgrade text

  • Changeset .changeset/21867-flow-trigger-record-credential-mask.md: @objectstack/trigger-record-change minor, @objectstack/spec patch. It carries the ! banner, FROM → TO and the one-line handling: a flow that needs a credential uses a privileged binder, never the trigger record.
    • It names the record-vs-previous credential comparison: a condition comparing the two sees two equal masks whenever the field is set on both sides, so a credential change is detected through a privileged binder.
    • It carries a "Runs stored before this release" paragraph: paused runs, and terminal runs that keep a restorable snapshot, created before the upgrade are resumed, cancelled or purged after upgrading. There is no migration and no scrub.
  • ADR-0087 semantic entry packages/spec/src/migrations/entries/semantic/18.flow-trigger-record-credential-masked.ts, a sibling of 18.by-id-write-unreadable-row-not-found. It is registered through gen:migration-registry (registry.ts) and declared in the changeset as registered flow-trigger-record-credential-masked.
  • packages/triggers/trigger-record-change/vitest.config.ts: the alias moves to the anchored array form and gains @objectstack/spec/data and @objectstack/core to source; the check-test-source-alias registry entry for this package drops @objectstack/core.

Verification

Round 1 readings are at head 67ce8a46a2 unless marked. Round 2 readings are in their own block below, at head 4f287e072f.

  • Ablation. The two projection calls were replaced via scripts/ablation-replace.mjs, wrap mode, with an EXIT/INT/TERM restore. On-disk proof: anchor 1 → 0, marker 0 → 1, blob d0702684cb19 → 27a41720a0ab. The dogfood project aliases @objectstack/trigger-record-change to source, and the plugin is passed in extraPlugins from that import, so no dist hop applies.
    • Unit pin: 3 red, 4 green. The four that stay green: ordinary value, params identity, unset reads null, hook objects whole. All four hold without a mask too.
    • Dogfood pin: 5 red, 2 green. The two that stay green: armed scene, privileged path.
    • Restore: blob == HEAD d0702684cb19, and git diff HEAD is empty.
  • Tests.
    • @objectstack/trigger-record-change pnpm test: 11 files, 108 tests, green at 67ce8a46a2.
    • @objectstack/core pnpm test: 77 files, 2177 tests, green.
    • @objectstack/service-automation vitest: 173 files, 2112 tests, green.
    • Dogfood pin: 7/7 green, at 48e0b1cc35 (trigger source unchanged since).
    • @objectstack/spec src/migrations: 3 files, 179 tests, green.
  • Typecheck.
    • @objectstack/trigger-record-change typecheck, including tsconfig.test.json: green. --listFiles counts the new test file once.
    • @objectstack/dogfood typecheck: green, and it covers the new file.
    • @objectstack/spec typecheck (src, scripts, test layer): green.
  • Gates.
    • @objectstack/spec check:generated: all 15 artifacts up to date.
    • check:adr-0087-registration: green. It reads the changeset as [BREAKING+bang] registered flow-trigger-record-credential-masked.
    • check:platform-checklist: green.
    • dispatch-gates.mjs --ran: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN.
  • Lint. The run was narrowed to the 6 changed .ts files, under eslint --no-inline-config --format json: 6 files, 0 errors, 0 warnings.
    • The population comes from eslint's own config: the two non-code files (.changeset/*.md and automation.json) answer "File ignored because no matching configuration was supplied".
    • --print-config shows parserOptions without project, so type-aware linting is off. This diff cannot move any untouched file's verdict.

Round 2, at head 4f287e072f

origin/main was merged in as a merge commit (baa4b2fe6c; the branch was 9 behind).

  • Build and tests.
    • Closure build pnpm --workspace-concurrency=2 --filter '@objectstack/trigger-record-change...' build: exit 0.
    • @objectstack/trigger-record-change pnpm test: 11 files, 114 tests, green. The mask file has 13 cases.
    • @objectstack/trigger-record-change typecheck (tsc --noEmit && tsc --noEmit -p tsconfig.test.json): exit 0 for both.
  • Ablation 1, the core alias resolves to source. Via scripts/ablation-replace.mjs, an early return was planted in omitInternalFieldsFromWriteResponse (packages/core/src/utils/internal-write-response.ts), with core dist not rebuilt (marker: 0 hits in packages/core/dist). Landed: anchor 1 → 0, blob 2a6a48c04fdb → d51283c8f5e6. Result: 5 red, 8 green; the red ones are the masking cases, the new afterDelete and beforeUpdate included. Restore: blob == HEAD 2a6a48c04fdb, git diff HEAD empty. A first attempt was refused by the tool as a no-op (the replacement contained the anchor); it measured nothing and was redone with a non-overlapping replacement.
  • Ablation 2, the log pin can fail. The error branch's condition was replaced with false. Landed: anchor 1 → 0. Result: 3 red (the absent, no-answer and throw cases), 10 green. Restore: blob == HEAD 04e3ca86825f, git diff HEAD empty.
  • Gates, each exit 0. check:adr-0087-registration (reads [BREAKING+bang] registered flow-trigger-record-credential-masked; --self-test 441 assertions), check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:changeset-gate-self-tests, check:test-source-alias (73 packages with tests scanned, 60 registered), check:nul-bytes, check-scripts-symbol-anchors, check-published-list-mirrors, check:cross-package-test-inputs, check:doc-authoring, check:issue-citations, check:logger-receiver-detach, check-changeset-fixed, check:published-files.
    • @objectstack/spec check:generated after the main merge: all 15 generated artifacts up to date, against the spec dist built post-merge.
    • NOT MEASURED: check:console-injection. It skipped, because there is no packages/console/dist in this worktree.
    • The rest of the dispatch-gates derivation (109 commands over the whole PR diff, mostly round-1 spec and dogfood families) was not re-run this round; CI owns it.
  • Lint. Narrowed to the 4 files changed this round (record-change-trigger.ts, trigger-record-credential-mask.test.ts, vitest.config.ts, scripts/check-test-source-alias.mjs), under eslint --no-inline-config --format json: 4 files, 0 errors, 0 warnings. All 4 are in eslint's own config, per --print-config, which also shows parserOptions.project and projectService undefined, so type-aware linting is off and this diff cannot move any untouched file's verdict.

Acceptance notes

  • The claim's file surface names packages/triggers/trigger-record-change/src. This PR also touches that package's vitest.config.ts (the alias above) and adds one dogfood test file under packages/qa/dogfood/test/, as the dispatch asked. Round 2 also touches scripts/check-test-source-alias.mjs, a registry narrowing only (this package's entry drops @objectstack/core).
  • During the second full gate pass, packages/plugins/plugin-approvals/dist and packages/plugins/plugin-auth/dist were found without .d.ts (written mid-pass). check:dts-closure and check:dual-build-cjs-loads went red as a result. A rebuild of those two packages restored them, and both gates read green. Neither package is in this diff. Which step wrote them was not established.
  • Carrier: none; noted here only, not filed. In buildContext, the materialisation read of getObject (gated on ground truth) is not wrapped in try/catch. A getObject that throws therefore fails the dispatch before the mask runs, and the handler logs "execution failed". So readObjectDefinition's throw branch is reachable only on an update or delete with no prior row. This behaviour predates the PR and was left untouched, because the dispatch said dispatch behaviour must not change. No public entry point is shown to throw from getObject.
  • Of the three operator actions for runs stored before this release, purging is the only one that leaves no clear value behind; resuming an old paused run can still write its clear values into the run's step log. A follow-up edit to the changeset should list purge first.
  • 48c162ed06 ports the three dogfood test-infra files of open PR test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read #21935 (packages/qa/dogfood/test/per-file-cwd.setup.ts, per-file-cwd.global-setup.ts, packages/qa/dogfood/vitest.config.ts), byte-identical, to clear the PM dispatch-gates self-test red that main has carried since test(dogfood): every test file runs in its own temporary working directory #21919. It is a no-op once test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read #21935 lands.

Generated by Claude Code

claude added 8 commits October 5, 2026 23:56
…us values carry the credential mask and omit internal fields

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
…masked, hot and after a cold boot

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
…r the masked flow trigger record

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 6, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/dogfood, @objectstack/spec, @objectstack/trigger-record-change, touching 5 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/qa/dogfood/vitest.config.ts, packages/triggers/trigger-record-change/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v16.mdx (via RecordChangeTrigger (symbol, a top-level class))
  • content/docs/releases/v17/17-4.mdx (via buildContext (symbol, a method of class RecordChangeTrigger))

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
  • 2 changed file(s) yielded no anchor (packages/qa/dogfood/vitest.config.ts, packages/triggers/trigger-record-change/vitest.config.ts) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 139 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 9dce635337c2cc42a4149aa49289ad77d172363d → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 9dce635337c2cc42a4149aa49289ad77d172363d

⚠️ 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 9dce635337c2cc42a4149aa49289ad77d172363d → 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: 67ce8a46a2ec2fda09767043d93f7d8e00f81151
Local-runs: none

Inputs: card #21867 (body, triage 5993882228, director ruling 5995381726 letter A, PM note 6005739672, alignment note 6005796816, claim 6005816377, os-dev-report 6007440350); PR #21928 body, its 8-file list, and the net diff origin/main...67ce8a46a2 (700+/3-); the 36 check-runs on this head. No earlier ## Contract review record exists on the card.

① Derived judgments

  • Accept-set / behaviour change: right. RecordChangeTrigger.buildContext now passes both flow-facing roots (isolatedRecord, isolatedPrevious) through omitInternalFieldsFromWriteResponse from @objectstack/core, using the trigger object's definition. This is ruling A as written: the mask is applied at the source, through the one helper, with no second mask in service-automation or in the suspended-run store. I checked four things in the code:
    • Order. The calls sit after hydration, after materializeDeclaredFields and after decoupleFromEngineState, directly before the return (premise 2). No later layer can bring a clear value back.
    • In place, on copies only. decoupleFromEngineState always deep-copies objects, arrays and dates through a WeakMap. The helper deletes and masks in place on those copies. So ctx.result, ctx.previous and ctx.input.data, which are shared with other bindings, stay whole. The unit pin "hook objects stay whole" asserts this.
    • Same projection on every derived surface. params is the same object as record, so it carries the same projection. buildContext is the only builder: its one caller is the hook handler at line 289. $record, SuspendedRun.context, variables_json / context_json and the rehydrated run all derive from it, and the dogfood pin covers them hot and after a cold boot.
    • Same rule as the read path. The helper asks isMaskedOnReadFieldType with the object's managedBy, so the better-auth password exemption carries over (unit pin case 7). An unset credential reads null. internal: true is omitted after masking.
  • readObjectDefinition: right. It reads the engine's optional getObject without the groundTruth gate, so insert events are masked too (unit pin case 6). If the accessor is missing or throws, nothing is masked. That matches the helper's documented "no schema strips nothing" contract and the existing object-existence gate.
  • finding: the whole /automation read domain is gated only by "authenticated" — run-detail returns the triggering record's fields without that record's own FLS #7900 untouched: right. Only the two record-shaped roots of a known object get the type-based mask. No other variable is filtered, and no field-level-security logic is added.
  • Write-back hazard checked: none. A flow that echoes record.CREDENTIAL_FIELD back through update_record sends SECRET_MASK. encryptSecretFields in the objectql engine drops a masked field whose value equals the mask, for every field the read path masks. So an echo-back means "unchanged" and cannot overwrite the stored credential.
  • Public surface: right, unchanged. No package.json is touched.
    • @objectstack/trigger-record-change exports only ., and no export is added or removed.
    • @objectstack/core is already a declared dependency, and omitInternalFieldsFromWriteResponse is already exported from its root (src/index.ts:83).
    • @objectstack/spec gains one data entry in the ./migrations registry (step 18): flow-trigger-record-credential-masked, which is byte-identical to the new entries/semantic/18.*.ts file. No Zod schema or ADR-0100 table changes.
  • vitest alias: right. The alias moves to the anchored array form and adds @objectstack/spec/data → source. Test config only, nothing published.
  • QA item automation.paused-run-trigger-record-masked: right. This is the item triage named as missing. automated.ref points to the dogfood pin, and the knownGaps line says honestly that the pin reads every surface as the admin.
  • Pins: they cover every surface the ruling names. That is the paused row's two columns (record, $record, previous), the data door, GET /automation/:name/runs/:runId, the post-pause node hot and after a cold boot, the ordinary field keeping its value, and resolveSecretField unchanged. The dev's ablation report (unit pin 3 red, dogfood pin 5 red) is consistent with which cases depend on the mask.

② Semver level

  • Changeset .changeset/21867-flow-trigger-record-credential-mask.md: right.
    • @objectstack/trigger-record-change gets minor with the ! banner, under the launch-window convention for a changed answer.
    • @objectstack/spec gets patch for the added ADR-0087 semantic registry entry. This is the same shape as its sibling 21771-write-door-unreadable-is-not-found.md (minor plus spec patch, registered marker).
    • It carries FROM → TO, the one-line handling (read a credential through a privileged binder, never off the trigger record) and the adr-0087: registered flow-trigger-record-credential-masked marker.
  • Clause-②: no: right. It matches the director's ruling 5995381726 ("No Zod or ADR-0100 table change") and the claim. The change alters a value a flow reads, not an accept-set of a published schema.
  • Non-blocking note on the upgrade text: a condition that compares record.X with previous.X for a credential field (for example, "the password changed") now always sees two equal masks. The FROM → TO line implies this, but the handling paragraph names only the comparison with a literal. Per premise 3, no shipped flow does this. Worth one more sentence in a later edit; it does not block.

③ Boundary flags

  • open_questions: []. Nothing to answer.
  • out_of_scope_findings[0]: packages/plugins/plugin-approvals/dist and packages/plugins/plugin-auth/dist lost their .d.ts mid-pass in the dev's worktree. Answered: local-only, not this diff. Neither package is in the diff. check:dts-closure and check:dual-build-cjs-loads are judged on this head by CI (Type Check · consumer gates / Build Core), not by the dev's local reading.
  • out_of_scope_findings[1]: the diff touches more than the declared file surface (vitest.config.ts, plus the dogfood pin under packages/qa/dogfood/test/). Answered: accepted. The alias is required by check:test-source-alias for the new value import. The dogfood pin is the end-to-end pin the ruling asked for. Neither publishes anything.
  • Premises 1–4 of the ruling were each reported as holding at dcb11c2ec9, and the diff is consistent with 1, 2 and 4. Premise 3 is a grep result I take from the report and did not re-run. No fork is owed.
  • Withholding (RUNNER rule 2): the PR body, the changeset, the semantic entry and the QA item stay at classes and positions. None of them contains reproduction steps.
  • Gate verdicts at this head, when this record was written: the completed check-runs are success or skipped (15 success, 6 skipped, Check Changeset and Dogfood Verify CLI among the successes). These were still in_progress and are not judged here: Build Core, Lint & Repo Gates, Type Check (workspace, consumer gates, debt ledger), Test Core 1–6, Dogfood Regression Gate 1–3, Temporal Conformance. Under the landing rule, this PASS lifts the label only once every check is green.

Implemented-by: claude/issue-21867-run-state-trigger-record-mask
Reviewed-by: session_018zT8d8NpiQ1ExhuNd5TxY6

VERDICT: PASS

claude added 4 commits October 6, 2026 01:38
… is logged at error, once per object

The mask in buildContext needs the object's definition. The comment
claimed an unknown object is refused upstream; the bind-time probe only
warns and still binds, so the comment is corrected and the unresolved
case (accessor absent, no answer, or a throw) now logs at error through
the plugin's logger, naming the object. Dispatch is unchanged. Pins the
log and adds afterDelete and beforeUpdate mask cases.

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
… mask helper is tested from the checkout

Narrows the KNOWN_UNALIASED_TEST_IMPORTS entry for this package to the
three dependencies still resolved through dist.

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
…d the runs stored before the release

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4f287e072f7b95aa19fd13e5298e19ea658885d7
Local-runs: none

Inputs: card #21867 (body; triage 5993882228; director ruling 5995381726, letter A; PM note 6005739672; alignment note 6005796816; claim 6005816377; round-1 os-dev-report 6007440350; round-2 os-dev-report 6007718665). PR #21928: its body, its 9-file list, and the net diff origin/main...4f287e072f (855+/8-). The 32 check-runs on this head. This record supersedes the PASS record 6007494437, which was written for 67ce8a46a2.

① Derived judgments

  • Behaviour change in RecordChangeTrigger.buildContext: right. This part is unchanged from round 1. Both flow-facing roots go through omitInternalFieldsFromWriteResponse from @objectstack/core, using the trigger object's definition. The calls come after hydration, materialisation and decoupleFromEngineState, directly before the return. They run in place on the decoupled copies only. params is the same object as record. There is no second mask in service-automation or in the store, and no other variable is filtered, so ruling A holds as written and finding: the whole /automation read domain is gated only by "authenticated" — run-detail returns the triggering record's fields without that record's own FLS #7900 is untouched.
  • Round 2: readObjectDefinition logs when the definition is unresolved. Right. The three unresolved shapes are a missing accessor, an empty answer and a throw. Each one logs once per object per trigger, at error, falling back to warn, which is the file's existing execution-failure pattern. The log names the object, the flow, the consequence and the fix. Dispatch is unchanged, as the dispatch required.
    • The corrected code comment is accurate. The bind-time probe in start() only warns and still binds, and the diff no longer claims an upstream refusal.
    • The remaining fail-open case (no definition, so nothing is masked) is the helper's documented "no schema strips nothing" contract. It is now loud instead of silent.
    • When object is absent, no definition is read and nothing is logged. That is correct: such a binding has no object to describe.
  • The dev's carrier-none finding: right. The materialisation getObject read is gated on ground truth and is not wrapped. A throw there fails the dispatch before the mask runs, so that path fails closed (the flow does not run with clear values). The behaviour predates this PR, and the dispatch said not to change dispatch behaviour.
  • Unit pin, now 13 cases: right. It adds afterDelete (the record comes from the prior row) and beforeUpdate (the payload layered over the prior row), masked on both roots. It also adds one case per unresolved shape (one error naming the object, and the flow runs on both writes) and a control where a resolved definition logs nothing.
    • The dev's ablations are consistent with which cases depend on the mask and on the log branch: an early return in the core helper turned 5 cases red, and removing the error branch turned 3 red.
  • Dogfood pin and QA item: right. Both are unchanged from round 1, and each covers a surface the ruling names.
  • Test-config changes: right. They publish nothing.
    • vitest.config.ts adds an anchored alias that points @objectstack/core to source, so the mask pins read the helper from source, not from dist/.
    • scripts/check-test-source-alias.mjs removes @objectstack/core from this package's KNOWN_UNALIASED_TEST_IMPORTS entry. Removing it is required once the alias exists, because the gate reports the entry as stale. This narrows the registry; it does not widen it.
  • Public surface (package exports maps): unchanged. Right.
    • No package.json is touched.
    • @objectstack/trigger-record-change still exports only ., and its files list is dist, README and CHANGELOG.
    • @objectstack/core was already a declared dependency, and the helper was already a root export.
    • @objectstack/spec's ./migrations export gains one data entry, step 18 flow-trigger-record-credential-masked. It is byte-identical to the new entries/semantic/18.*.ts file.
    • @objectstack/dogfood is private: true.
    • None of the 9 paths is a governed surface.

② Semver level

  • Changeset .changeset/21867-flow-trigger-record-credential-mask.md: right.
    • @objectstack/trigger-record-change is minor with the ! banner, under the launch-window convention for a changed answer.
    • @objectstack/spec is patch, for the additive registry entry.
    • It carries FROM → TO, the one-line handling and the marker adr-0087: registered flow-trigger-record-credential-masked.
    • Round 2 closes the earlier record's non-blocking note. The handling paragraph now names the record-vs-previous comparison (two equal masks; detect a change through a privileged binder). It also gains a "Runs stored before this release" paragraph: there is no migration and no scrub, and the operator drains pre-upgrade runs.
    • Nothing else in the diff publishes. The scripts, the vitest config, docs/qa and the private dogfood package all ship nothing.
  • Clause-②: no: right. The same line is in the changeset, the PR body, the claim and the director's ruling ("No Zod or ADR-0100 table change"). A value a flow reads changes. No published schema's accept-set changes.
  • Non-blocking note on the "Runs stored before this release" paragraph. Of the three remedies it names, only purge is certain to leave nothing behind. A pre-upgrade paused run that is resumed continues from its stored, unmasked context, and the terminal row's steps_json (which the paragraph itself lists) records what the post-resume nodes saw. "Resume" therefore drains the paused row but can carry clear values into the history row. A later edit should rank purge first. This does not block, because the paragraph's main claim (no migration or scrub; stored runs keep their values) is accurate.

③ Boundary flags

  • open_questions: [] in both rounds. Nothing to answer.
  • Round-2 out_of_scope_findings[0] (the unwrapped materialisation getObject read): Answered under ① as fail-closed and predating this PR. Nothing is owed here.
  • Round-2 pr_body_edits_needed (four edits): Answered: owed, non-blocking. The PR body still describes round 1: "7 cases", "tolerates a missing accessor or a throw", readings at 67ce8a46a2, and no scripts/check-test-source-alias.mjs row. The verdict is judged on the diff and the changeset, which are what ship, and the body's contract lines (Fixes #21867, Clause-②: no) are correct. The owning seat applies the four edits the dev listed before it lands the PR.
  • Round-1 flags (the local dist .d.ts loss, and the file-surface extension): answered in 6007494437. They still hold at this head.
  • The round-2 report's NOT MEASURED item, check:console-injection, is local only. CI's Lint & Repo Gates judges it on this head.
  • Withholding (RUNNER rule 2): the PR body, the changeset, the semantic entry, the code comments, the new log text and the QA item stay at classes and positions. None of them contains reproduction steps.
  • Gate verdicts at this head when this record was written:
    • 22 success and 3 skipped (Packed-tarball smoke, Console Pin Gate, Build Docs). The successes include Build Core, Check Changeset, Governed Surface Queue Guard, Type Check · source gates / consumer gates / debt ledger, Dogfood Regression Gate (1/3, 2/3, 3/3), Temporal Conformance (live PG + MySQL) and Test Core (5/6).
    • Still in_progress and not judged here: Lint & Repo Gates, Type Check · workspace, and Test Core (1/6, 2/6, 3/6, 4/6, 6/6).
    • Under the landing rule, this PASS supports landing only once every required check is green.

Implemented-by: claude/issue-21867-run-state-trigger-record-mask
Reviewed-by: session_018zT8d8NpiQ1ExhuNd5TxY6

VERDICT: PASS

main is red on the dispatch-gates self-test since the per-file cwd setup
landed: its mkdtempSync base is not readable by the scratch-dir scan. This
ports the same three-file change as the open fix PR, so this branch's lint
lane goes green; it is a no-op once main carries that fix.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Lint & Repo Gates failed at 4f287e072f, in the step PM dispatch-gates self-test. This failure is not caused by this PR. The same step is red on main (faf8dce482, 9dce635337) since #21919 landed. That commit's per-file cwd setup creates its temporary base where the scratch-dir scan cannot read it ("no mkdtempSync site in this tree takes a base the scan cannot read").

The fix is open as #21935. I ported the same three files (packages/qa/dogfood/test/per-file-cwd.setup.ts, per-file-cwd.global-setup.ts, vitest.config.ts) in 48c162ed06. Locally, dispatch-gates --self-test on that tree passes all 1976 cases, including the scan above. The ported change becomes a no-op once #21935 lands.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 48c162ed0680a540fbef54b2beb27cec619dde2b
Local-runs: none

Inputs: card #21867 (body; triage 5993882228; director ruling 5995381726, letter A; PM note 6005739672; alignment note 6005796816; claim 6005816377; os-dev-reports 6007440350 and 6007718665). PR #21928: its body, its 12-file list, its comments (the earlier records 6007494437 and 6007858445, and the port note 6008303479), the commit diff 4f287e072f..48c162ed06, and the net diff origin/main...48c162ed06 (914+/32-). The 36 check-runs on this head. This record supersedes the PASS record 6007858445, written for 4f287e072f.

① Derived judgments

② Semver level

  • Changeset .changeset/21867-flow-trigger-record-credential-mask.md: right, and unchanged by this commit.
    • @objectstack/trigger-record-change is minor with the ! banner, under the launch-window convention.
    • @objectstack/spec is patch, for the additive registry entry.
    • It carries FROM → TO, the handling (including the record-vs-previous comparison), the "Runs stored before this release" paragraph and the marker adr-0087: registered flow-trigger-record-credential-masked.
    • The ported files belong to @objectstack/dogfood, which is private: true and publishes nothing, so no changeset entry is owed for them.
  • Clause-②: no: right. It appears in the changeset, the PR body, the claim and the director's ruling. A value a flow reads changes. No accept-set of a published schema changes, and the port touches no schema.
  • Non-blocking, carried from 6007858445: still open. The "Runs stored before this release" paragraph still lists "resume, cancel or purge" without ranking purge first. The PR's own Acceptance notes name this as a follow-up edit. It does not block.

③ Boundary flags

  • open_questions: [] in both rounds. Nothing to answer.
  • File-surface extension, round 3: answered, accepted. The claim's file surface does not name packages/qa/dogfood/test/per-file-cwd.* or packages/qa/dogfood/vitest.config.ts. The extension is explained in 6008303479, and it equals test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read #21935's diff. The No other open PR may claim the same single-writer path check is success on this head.
  • PR body: owed, non-blocking. The body does not yet mention the port (0 hits for per-file-cwd or 21935). The owning seat should add one Acceptance-notes line naming 48c162ed06, the three paths, test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read #21935 and "no-op once test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read #21935 lands" before landing. The round-2 body edits the earlier record asked for have been applied (13 cases, the unresolved-definition wording, the round-2 block).
  • Earlier flags (the local dist .d.ts loss, the round-1 and round-2 file-surface extensions, and the unwrapped materialisation getObject read, which fails closed and predates this PR): answered in 6007494437 and 6007858445. They still hold at this head.
  • Withholding (RUNNER rule 2): the PR body, the changeset, the semantic entry, the code comments, the log text, the QA item and the port note stay at classes and positions. None of them contains reproduction steps.
  • Gate verdicts at this head when this record was written:
    • 12 success and 6 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke ×2). Auto Label and Check PR Size also each have a success run. Among the successes are Check Changeset, Governed Surface Queue Guard, Spec property liveness and the three claim guards.
    • Still in_progress and not judged here: Build Core, Check Changeset (a second run), Dogfood Regression Gate (1/3, 2/3, 3/3), Dogfood Verify CLI, Lint & Repo Gates, Temporal Conformance (live PG + MySQL), Test Core (1/6 to 6/6), and Type Check · consumer gates / debt ledger / source gates / workspace.
    • Lint & Repo Gates is the lane this commit exists to clear. Under the landing rule, this PASS supports landing only once every required check is green.

Implemented-by: claude/issue-21867-run-state-trigger-record-mask
Reviewed-by: session_018zT8d8NpiQ1ExhuNd5TxY6

VERDICT: PASS

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/l tests tooling

Projects

None yet

2 participants