Repository navigation
fix(trigger-record-change)!: a record-change flow's trigger record carries the credential mask and omits internal fields - #21928
Conversation
…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>
…igger record 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>
…te fixture 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>
…on checklist Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6 Co-authored-by: Claude <noreply@anthropic.com>
…r the mask pin Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
… 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>
Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: 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
|
The fix is open as #21935. I ported the same three files ( Generated by Claude Code |
Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…jectstack-ai#21943) Part of objectstack-ai#21932 Clause-②: no ## What changes The platform checklist gains items for the rules the 17.7 pre-release security follow-up landed, and two re-checks from the card are resolved. All edits are in `docs/qa/platform-checklist/areas/*.json`. `automation.json` is untouched (open PR objectstack-ai#21928 holds it). | Card row | Disposition | Item | |---|---|---| | objectstack-ai#21792 (PR objectstack-ai#21809) settings audit and secret-valued settings | new item | `platform-core.settings-audit-secret-fingerprint` | | objectstack-ai#21846 (PR objectstack-ai#21872) implicit account linking | new item | `identity-auth.implicit-account-linking-ownership` | | objectstack-ai#21839 (PR objectstack-ai#21890) share-link password | three clauses added, rev 4 to 5 | `access-security.share-link-capability-tokens` | | objectstack-ai#21836 (PR objectstack-ai#21879) global search skips unreadable objects, plus the two cases objectstack-ai#21880 lists | new item | `search.global-search-skips-unreadable` | | re-check 1: A2 / A7 and the plugin-driver boundary | rev 2 to 3 | `integration-system.datasource-credential-refusal-matrix` | | re-check 2: the objectstack-ai#21845 CLI and quorum N1 notes | already applied by objectstack-ai#21891, no edit | `cli.scaffold-first-run`, `cli.scaffold-console-first-paint`, `approvals.quorum-m-of-n` | Each item states rules, not reproductions. Withheld security detail stays out. ### Grounding, per row - **Settings audit fingerprint.** Both ledgers record the keyed digest for a secret-valued setting, or no fingerprint when none is available, and never the value or an unkeyed hash. Grounded in `settings-service.ts#secretAuditDigest`, `config-change-audit.ts#CONFIG_CHANGE_ACTION` and the contract text at `crypto-provider.ts#keyedDigest`. The pin is `settings-audit-secret-digest.test.ts` (7 cases). The offline check carries a positive control: the non-secret key's unkeyed digest IS found, so a no-hit on the secret rows means something. The no-keyed-digest arm cannot be reached on a stock boot, so that clause is scored from the pin. - **Implicit account linking.** Four rules: no implicit link to an unverified local user; an unlink is honoured; an explicit, signed-in link still works and lifts the refusal; the platform IdP exception holds only on its OAuth path. Grounded in `implicit-account-linking.ts` (`decideImplicitLink`, `IMPLICIT_LINK_REFUSED`, `PLATFORM_IDP_PROVIDER_ID`, `recordUnlinkTombstone`, `refuseImplicitAccountLink`) and the published `sso.mdx` section. The pin is `implicit-account-linking.test.ts`. The item reuses the local OIDC provider recipe from `identity-auth.linked-accounts-social`. The platform-IdP clause and the operator override are pin-scored, and knownGaps says why. - **Share-link password.** The stored hash leaves on no exit (mint, list, redemption). The password is accepted from the `X-Share-Password` header, the query form is still accepted, and the default CORS allow-list carries the header. Both public routes answer `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, and the authenticated routes do not. Grounded in `share-link-service.ts#withoutPasswordHash`, `share-link-routes.ts#SHARE_LINK_PUBLIC_RESPONSE_HEADERS`, the runtime `share-links.ts#PUBLIC_RESPONSE_HEADERS` and `adapter.ts#DEFAULT_CORS_ALLOW_HEADERS`. The pins are the `[objectstack-ai#21839]` blocks in `share-link-password.test.ts`, `share-links-public-cache-headers.test.ts` and the hono-plugin CORS case. Existing clause indices are unchanged. - **Global search.** An unreadable object is never queried, named or counted. An explicit `objects=` naming one answers exactly as a name that matches no object. The object stays refused at its own door. Row scope still narrows a searched object, and a term found only in a field hidden from the caller yields no hit. Grounded in `protocol.ts#searchAll` (the `canReadObject` pre-filter and the `getQueryableFields` narrowing). The pins are the dogfood `search-skip-unreadable.dogfood.test.ts` and the 12 unit cases in `protocol.search-skip-unreadable.test.ts`. The two objectstack-ai#21880 cases have no end-to-end pin yet, and knownGaps says so. The open pinyin-companion finding on objectstack-ai#21880 is recorded as a knownGap with a flag-off instruction, at class level only. The persona reuses the area recipe `qa-contributor-bound-member`. - **Datasource credential matrix.** A2 / A7 (`acceptance[1]` and `acceptance[6]`) are recorded as a known environment gap. They need a reachable credential-protected database of a shipped driver, which no run has had. No recipe is claimed, because none is proven. A successful publish alone may not score them, and the stored-credential half of A7 can be read as a partial reading. Separately, the unknown-driver clause, step 7, its negative and the title now state the ruled boundary from objectstack-ai#21921 and the docs note objectstack-ai#21927. For a plugin driver, only the fixed spellings are redacted (the canonical keys, the former aliases and URL credentials). A non-canonical key served as written is the boundary, not a FAIL. Grounded in `common.zod.ts#CANONICAL_CREDENTIAL_KEYS` and `datasource-credential-redaction.ts#redactableConfigKeys`. ### Re-check 2 evidence (no edit) At the claim ref `9dce635337`: - `cli.scaffold-first-run` (rev 3) step 0 and `cli.scaffold-console-first-paint` (rev 3) step 0 both drop the trailing `npm install` and warn against adding it. Their rev 3 history entries cite objectstack-ai#21845. No other `npm install` step remains in `cli.json`. - `approvals.quorum-m-of-n` (rev 4) `negative[0]` requires a NON-PRIVILEGED repeat actor and names the documented admin override (objectstack-ai#3424) as never a distinctness FAIL. ## Remaining on objectstack-ai#21932 (held, not in this PR) - The objectstack-ai#21864 row (public-form withdrawal layering). Its PR is still open. - The objectstack-ai#21928 row (run-state trigger record mask). That PR adds its own item in `automation.json`. objectstack-ai#21932 remains open for these two rows. ## Validation (at `a72b827e43`) - `pnpm check:platform-checklist`: exit 0. It reports 15 areas and 273 items (269 active, 2 planned). The baseline was 270. Symbol anchors resolve 674 of 684 (baseline 657 of 667): all 17 new anchors resolve, and the objectstack-ai#16898 residual is unchanged at 10. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 13 commands, and all 13 exit 0. `check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` were not built). After building them it exited 0. `--ran` reconciliation: 13 derived, 13 run, 0 unrun. - No package source changed, so there is no package build, test or typecheck. No changeset: `docs/qa/**` publishes nothing. ## Acceptance notes - Source citations name test cases and symbols, never line numbers, because `check:platform-checklist` refuses a `file:line` pin. - `content/docs/data-modeling/drivers.mdx` says a plugin driver's `config` is "stored and served to administrators as written". The read redactor still withholds the canonical spellings (`password`, `authToken`), the former aliases and URL credentials for such a driver (`redactableConfigKeys`). So the docs sentence is slightly broader than the code, and the code is the more protective of the two. The checklist follows the code. This is noted only, with no card. Carrier: none. - A run of `search.global-search-skips-unreadable` picks the walled object and the hidden-field value on the live boot, behind premise guards. The item names likely candidates and does not assume them. --- _Generated by [Claude Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_ Co-authored-by: Claude <noreply@anthropic.com>
…cision in words instead of a tracker number (stage 23) (objectstack-ai#21947) Part of objectstack-ai#20749 Clause-②: no Stage 23 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the last name-ordered `ui/` group: the 16 id-bearing test files directly under `packages/spec/src/ui/` from `view-item-config-type.test.ts` to `widget.test.ts`. Those files carried 100 messages and 106 tracker ids, citing 53 records. All 106 now either state what their record decided, in words (form D), or are dropped where the title already says it. No needle sits in this group. Text only: no assertion, identifier, test count or code comment changes, and no file is renamed. ## Census at the base (`9e33ee7c59`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages 10 to 22 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. The worktree was cut from `origin/main` at `9e33ee7c59`, the claim's base. Both instruments read **571 messages / 604 ids in 127 files**, the seat's reading and stage 22's head reading. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `system/` | 34 | 154 / 167 | 128 / 138 | 26 / 29 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ui/` (this PR: 16 of the 21 files) | 21 | 107 / 113 | 97 / 103 | 10 / 10 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **127** | **571 / 604** | **523 / 553** | **48 / 51** | The group reads **100 messages / 106 ids in 16 files**, the seat's figures file for file: | file (under `ui/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `view-item-config-type.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view-metadata-schema.test.ts` | 8 / 8 | 8 / 8 | 0 | | `view-metadata-type.test.ts` | 2 / 2 | 2 / 2 | 0 | | `view-overlay-options-bag.test.ts` | 6 / 6 | 6 / 6 | 0 | | `view-overlay-options-type.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view-overlay-owner-hidden-retirement.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view-overlay-viewkind-arm.test.ts` | 10 / 10 | 10 / 10 | 0 | | `view-overlay-viewkind-type.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view-strictness-batch18.test.ts` | 10 / 11 | 10 / 11 | 0 | | `view-submit-redirect-url.test.ts` | 5 / 5 | 5 / 5 | 0 | | `view-union-branch-focus.test.ts` | 7 / 7 | 6 / 6 | 1 / 1 | | `view-union-diagnostics.test.ts` | 4 / 5 | 4 / 5 | 0 | | `view-union-retirement-prescription.test.ts` | 1 / 1 | 1 / 1 | 0 | | `view.test.ts` | 39 / 43 | 38 / 42 | 1 / 1 | | `widget-i18n-retirement.test.ts` | 3 / 3 | 2 / 2 | 1 / 1 | | `widget.test.ts` | 1 / 1 | 1 / 1 | 0 | | **16 files** | **100 / 106** | **97 / 103** | **3 / 3** | Three more test files sit in the same name range and carry no id (`view-item-owner-hidden-retirement.test.ts`, `view-list-tabs-retirement.test.ts`, `vocabulary-derivation.test.ts`). The three "other" strings are rewritten and declared to the text-only tool: the table label at `view-union-branch-focus.test.ts:139`, which prints inside two `for … of` test titles, and the expect messages at `view.test.ts:4099` and `widget-i18n-retirement.test.ts:135`. - **Controls.** Lit: `ui/notification.test.ts` and `api/api-error-code-type.test.ts`, outside the group, read 1 id each at the base and at the head. Dark: `view.test.ts` reads 0 at the head while 92 of its comment lines still carry a number. Planted in a scratch tree: an id put into a `widget.test.ts` title reads 1 / 1 (`title:describe`), and an id put into a `view-metadata-type.test.ts` comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same as the gate pattern in 15 of the 16 files at the base. `view.test.ts` reads 2 more, and keeps them at the head: the CSS colours `'#00cc00'` (`:2770`) and `'#22c55e'` (`:3537`), fixture values that cite nothing. - **At the head:** 471 messages / 498 ids in 111 files. The 16 files read 0 / 0, `ui/` reads 7 / 7, and no other file moved. ## How the area was chosen `ui/` has no subdirectory test file with an id, so it is taken in name-ordered file groups near the ~100-id bound. Stage 22's re-cut named this group at 106 ids, and this census reads 106, so no re-cut was needed. `view.test.ts` (43 ids) is one file inside one text-only proof here, so it is not split. **`ui/` after this PR** reads 7 / 7, all kept items: stage 20's `component-props-unknown-members.pin.test.ts:322`, stage 21's four colour literals (`dashboard-chart-structure-refusal.test.ts:94`, `dashboard.test.ts:124`), and stage 22's two needles (`notification.test.ts:123`, `strictness-batch14.test.ts:395`). **Named for the next stages** (cut from the head census, 471 / 498): - **`api/`, 201 ids in 40 files**, with no subdirectory. Its first name-ordered group near the bound is `ai-agents-envelope.test.ts` through `package-lifecycle.test.ts`: 27 files, 100 messages / 106 ids (95 / 101 titles, 5 / 5 other: `auth.test.ts`, `discovery-environment-subset.pin.test.ts` and three in `export-job-family-retirement.test.ts`). The second is `plugin-rest-api.handler-status-retirement.test.ts` through `zod-issues-to-fields.test.ts`: 13 files, 89 / 95, `protocol.test.ts` alone 50. - `system/` 167, two stages. The files directly in `src/`, 120, one. - The needles: the three docblock needles, the kept `:322` and stage 22's two. One stage, with an at-tier review. ## What each id became - **25 literals (27 ids)** now state a decision in words. - **20 literals (22 ids)** get their subject back in words, where the number stood for a thing. - **55 literals (57 ids)** drop a number the title already explains. Every cited record was fetched with all its comments through REST, and its decision was read from its ruling, ACCEPT and landing comments: 53 records, 51 answer 200 and 2 answer 404. Five citations are objectui's and were read from objectui: `objectui#5233`, `objectui#2231` (cited bare at `view-strictness-batch18.test.ts:309`), `objectui#6237` (cited bare at `view.test.ts:949`), `objectui#5435` and `objectui#3289`. Three same-number records in the other repository were fetched first and set aside: `objectstack#2231` is a version-packages PR, `objectstack#6237` a datasource PR, and `objectui#2998` a form PR; `framework#1894 / objectstack-ai#2998` are this repository's objectstack-ai#1894 and objectstack-ai#2998 under its old name. The two that answer 404 were read from what landed: - **objectstack-ai#9933**, from its landing commit `d5552ca13f` ("admit columnState as an explicitly runtime-only view-overlay key") and the CHANGELOG entry for `d5552ca`; - **objectstack-ai#11195**, from PR objectstack-ai#11458, the PR that closed it ("UserActionsConfigSchema adopts group / hideFields / rowColor (ruled A on objectui#5435)"). One citation names the wrong record, and the titles now state what landed instead. `objectstack-ai#3896 close-out` (twice in `view.test.ts`) names objectstack-ai#3896, the sharing-rule criteria card, which records no decision about these keys; the two titles state the decision from the landed tombstones of `form.defaultSort` and `view.responsive` / `view.performance`, as stage 20 did for `action.test.ts`. Where a record's first decision was corrected later, the title follows the correction: - **objectstack-ai#6926:** its first triage direction retired the `groups` alias; the measurement found live consumers, and the maintainer re-ruled A, a fold at the producer. The two titles say "the producer-side `groups` fold" and "folds onto `sections`". - **objectstack-ai#7025 and objectstack-ai#7741:** objectstack-ai#7025 froze the acceptance face; objectstack-ai#7741's ruling then moved it, and later retirements moved it again, each pinned. The title says "frozen by the diagnostics work; every move since is deliberate and pinned", not "moved only once". - **objectstack-ai#7510:** the "[objectstack-ai#7510] ⛔ the acceptance face did not move" describe sits beside two ruled moves recorded in its own comments, so the title now names what did not move it: "⛔ the branch focusing did not move the acceptance face". **Stated in words:** | record | literal (under `ui/`) | now reads | the decision | |:--|:--|:--|:--| | objectstack-ai#5599 | `view-metadata-schema.test.ts:95` | "REJECTS a bare `{}` — the pin this line used to make, reversed by the identity precondition" | Maintainer ruling 2026-08-06, direction B: a minimal identity precondition ahead of the union's four members; each member's `.strip()` is untouched. | | objectstack-ai#7741 | `view-metadata-schema.test.ts:125` | "… NO object binding — a row no read path could serve, with located guidance" | Maintainer ruling 2026-08-12, direction B: a row that cannot be expanded or served by any read path is not stored and badged valid; the inline arm requires the binding, refused with `defineView`'s guidance. | | objectstack-ai#5599 | `view-metadata-schema.test.ts:215` | "identity precondition — a body must read as a view before any member judges it" | The same direction B. | | `objectui#5233` | `view-metadata-schema.test.ts:413` | "… a `columnState`-only patch (the patch-only write the console persists)" | Maintainer ruling 2026-08-12 (on objectstack-ai#7494): `persistViewPatch` stores the patch only, not the merged base. | | objectstack-ai#17152 | `view-overlay-owner-hidden-retirement.test.ts:335` | "… names the family's D2 conversion (ruled: a D3 entry per family, even beside a lossless D2)" | Ruling B (director seat, 2026-09-10, upheld 2026-09-11): one D3 semantic entry per retired family, beside its D2 conversion even when D2 is lossless. | | objectstack-ai#7494 | `view-overlay-viewkind-arm.test.ts:102` | "the console %s toggle (a patch-only write, as ruled) is ACCEPTED on listOverlay" | Maintainer ruling 2026-08-12: the overlay store is org-wide, and the toolbar write stores the patch only. | | objectstack-ai#4001 | `view-strictness-batch18.test.ts:91` | "批 18, unknown keys refused — the doors these shapes are reachable through" | The strictness campaign: an unknown key on the authorable surface is refused, not silently stripped. | | objectstack-ai#15469 | `view-strictness-batch18.test.ts:149` | "… a CLOSED entry, and since the renderer-ahead `.passthrough()` was removed a CLOSED parent too" | Maintainer ruling A (decision batch objectstack-ai#41, 2026-09-05): every key the gantt and tree renderers read is declared, and both `.passthrough()` calls go. | | objectstack-ai#5074 | `view-strictness-batch18.test.ts:364` | "[RESOLVED by the ruled split] ViewItemSchema SPLIT — …" | Maintainer ruling A (2026-08-04): split — a strict authoring `ViewItemSchema` and a reopened wire member in the union. | | objectstack-ai#5074 | `view-strictness-batch18.test.ts:403` | "[RESOLVED with the ruled split] ListViewSchema.sort CLOSED — …" | The split's scope addendum: the wire door strips the console's decoration keys before validating, so `sort[]` closed again with no declared `id`. | | objectstack-ai#7025, objectstack-ai#7741 | `view-union-diagnostics.test.ts:246` | "the acceptance face of ViewMetadataSchema — frozen by the diagnostics work; every move since is deliberate and pinned" | objectstack-ai#7025's sweep rule: the diagnostic face improves, the acceptance face does not move; objectstack-ai#7741's ruled binding requirement is the first pinned move since. | | objectstack-ai#9463 | `view.test.ts:342` | "viewMode — the granularities the gantt renderer honours, measured" | Declare `viewMode` with exactly the granularities objectui's `GanttView` honours, measured, not invented (the spec half of objectui#5074's ruling). | | objectstack-ai#17053 | `view.test.ts:441` | "the legacy string `sort` clause is retired — one spelling, the array" | objectui's ruling (director batch objectstack-ai#77, option B): one spelling, the array; the spec stops producing the string. | | objectstack-ai#13704 | `view.test.ts:873` | "wizard tightening — sections are the steps, the inert step keys are refused, no key is added" | The ruled shape of objectstack-ai#13622 (2026-08-31): sections are the steps, the wizard-inert step keys are refused at parse, zero new keys. | | `objectui#6237` | `view.test.ts:949` | "… stay accepted on tabbed/simple (the ruled split confines it to wizard steps)" | Maintainer ruling 2026-08-30 (director batch objectstack-ai#3): `FormSectionConfig` is split, so tabbed sections take a predicate and wizard steps carry none. | | `objectui#2231` | `view.test.ts:2876` | "ListColumnSchema summary object form and prefix — spec-owned, no longer an objectui-local extension" | The derive-by-reference unification: `677b591` moved `prefix` and the `{ type, field }` `summary` form into the spec, closing objectui's local `.extend()`. | | objectstack-ai#3896 (see above) | `view.test.ts:3144` | "FormViewSchema — retired defaultSort (audit close-out: nothing read it)" | The landed tombstone: `form.defaultSort` was removed because nothing read it. | | `objectui#5435` | `view.test.ts:3386` | "… defaults asymmetry, copied from what the renderer reads" | Ruling A (2026-08-22): the spec adopts `group` / `hideFields` / `rowColor`, with the defaults copied from `ListView`'s reads. | | objectstack-ai#3896 (see above) | `view.test.ts:3826` | "ListViewSchema — retired responsive/performance (audit close-out: no renderer read them)" | The landed tombstones: no renderer or runtime read either key. | | objectstack-ai#7176 | `view.test.ts:3847` | "ListViewSchema — retired striped/bordered/virtualScroll (every reader only passed them through)" | Maintainer ruling 2026-08-10: retire under ADR-0049, since every measured reader copied the keys forward and none applied them. | | objectstack-ai#5832 | `view.test.ts:4099` (expect message) | "`HttpMethodType` was renamed to `HttpMethodSubset`" | Maintainer ruling 2026-08-06: rename the 5-value subset; the 7-value `HttpMethod` keeps its name and its wire contract. | | objectstack-ai#16577, objectstack-ai#13817 | `view.test.ts:4708` | "… the `type: 'calendar'` axis is NOT gated by the `allowedVisualizations` check (ruled: a completeness warning)" | Ruling B (director seat, 2026-09-11): the objectstack-ai#13817 guard keeps gating `allowedVisualizations` only; the `type: 'calendar'` route is carried at warning by `checkViewCompleteness`. | | objectstack-ai#19228 | `view.test.ts:4793` | "view row bound — `pagination.pageSize` is the one bound; no per-kind `limit` on the view configs" | Maintainer ruling D (2026-09-23): one row bound per view, `pagination.pageSize`; the per-kind `limit` was removed before it shipped. | | objectstack-ai#5055 | `widget-i18n-retirement.test.ts:70` | "ui/ widget + i18n family retirement — doorless vocabularies removed, not tightened" | Maintainer ruling A (2026-08-06): ADR-0049 enforce-or-remove retires the unreachable widget and locale vocabularies; closing them would only dress a dead slot as a checked one. | | `objectui#3289` | `widget-i18n-retirement.test.ts:196` | "the surviving `error` slot is exactly the one objectui renamed its own slot onto, with no alias" | objectui followed the spec: its widget `errorMessage` slot became the spec's `error`, with no alias, and the form renderer produces it. | **Subject back in words** (20 literals): "(binding pair, objectstack-ai#7741)" becomes "(the object + viewKind binding pair)"; "union error behaviour (objectstack-ai#5014)" becomes "union error behaviour (where a branch prescription gets buried)"; the five `objectstack-ai#7496` prefixes become "the ruled redirect `url` shape —" (twice) and "ruled bullet 1 / 2 / 3 —", the file's own name for the ruling's three bullets; "the pre-objectstack-ai#7510 ranking" becomes "the pre-fix ranking"; "the objectstack-ai#4001 wrap prescription" becomes "the `defineView` wrap prescription"; the acceptance-face describe at `view-union-branch-focus.test.ts:261` (above); "the objectstack-ai#6926 fold" becomes "the producer-side `groups` fold"; "(objectstack-ai#7025 membership)" becomes "and the union corpus pins it accepted"; "(objectstack-ai#8321/objectstack-ai#12174)" becomes "(a negative or fractional scale)", what that test probes; "the exact declaration objectstack-ai#9340 exists to make legal" and "the gap objectstack-ai#9340 closes" name "this block"; "(acceptance criterion, objectstack#11195)" becomes "(the acceptance criterion for adopting the three keys)"; "(objectstack-ai#7176 rides …)" becomes "(the retirement rides …)"; "the axis objectstack-ai#13817 does not gate" becomes "the axis the `allowedVisualizations` check does not gate"; "zero holders after objectstack-ai#5055" becomes "after the widget + i18n retirement"; "objectstack-ai#5055 — the one surviving shape" becomes "the widget retirement — the one surviving shape". **Dropped where already stated** (55 literals, 57 ids). A number goes only where the title already says its decision. Examples: the four `[objectstack-ai#19920]` prefixes ("… typed by its arm, not unknown", "… a parsed view body, not unknown") and `[objectstack-ai#19871]`; the six `[objectstack-ai#20051]` describes on the `options` bag and the one in `view-union-retirement-prescription.test.ts`; the eight `objectstack-ai#20186` describes ("a column-less list PATCH is judged by the list member", "each member judges ONE viewKind", …); the three `[objectstack-ai#6391]` describes, three `[objectstack-ai#7510]` titles and the `[objectstack-ai#21180]` table label ("the retired `publicPicker` key itself"); "(objectstack-ai#3095)", "(objectstack-ai#5074)" after "`.strip()` round-tripping is untouched", "(objectstack-ai#9933)" after "runtime-only overlay key", and the `view.test.ts` tails `(objectstack-ai#15469)`, `(objectstack-ai#6926)`, `(objectstack-ai#12174)`, `(objectstack-ai#19088)`, `(objectstack-ai#7084)`, `(objectstack-ai#9340 — …)`, `(objectstack-ai#17499)`, `(objectstack-ai#18791)`, `(framework#1894 / objectstack-ai#2998)`, `(objectstack-ai#5073 — …)` x2, `(objectstack-ai#8010)`, `[objectstack-ai#4688]`, `[objectstack-ai#4691]`, `(objectstack-ai#6416 / objectstack-ai#6619)`, `(objectstack-ai#17063)`, `(objectstack-ai#16885)`, `(objectstack-ai#13817)` and `(objectstack-ai#16577)`. The batch label `批 18` stays, in stage 20's "批 19, unknown keys refused" form on the file's first describe and bare on the other four. `W2` stays: the file's own header defines W1 and W2. The commit `ce70876e` stays in "(measured on origin/main ce70876)": a commit, not a tracker id. **No file is renamed.** ## Readers - **Test-name filters:** none. No tracked script, workflow or package config passes `-t` / `--testNamePattern` (the 3 hits are `docker build -t`, `type -t` and `lsof -t`). - **Snapshots:** none. No `__snapshots__` directory is tracked under `packages/spec`, and none of the 16 files calls a snapshot matcher. - **Projects:** none of the 16 files is in the `repo` project (`packages/spec/vitest.repo-tests.json`); all run in `local`. - **By substring:** every old literal, its id-bearing fragment and a window around each id (299 needles) was searched with `git grep` at the base, across the tracked tree outside its own file. No gate, doc, filter, snapshot, QA checklist entry or `scripts/check-*.mjs` self-test reads one. The 16 hits are windows that share wording with code comments and one CHANGELOG line: 15 comments in the migration registry, its semantic entries and `view-list-tabs-retirement.test.ts` read "(ruling B on objectstack-ai#17152)", and `packages/spec/CHANGELOG.md:30342` reads "runtime-only overlay key (objectstack-ai#9933)". - **Cross-references by id:** two places name the `columnState` section of `view-metadata-schema.test.ts` as "§objectstack-ai#9933": a code comment at `packages/spec/scripts/strictness-ledger.test.ts:380` and the `view.zod.ts` row of `docs/audits/2026-07-unknown-key-strictness-ledger.md`. Neither matches a string; both point a reader at the section, which still carries "columnState — runtime-only overlay key" in its title and its banner comment. A code comment and an audit record are not this card's share, so neither is edited. ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. This stage declares three lines: `view-union-branch-focus.test.ts:139`, `view.test.ts:4099` and `widget-i18n-retirement.test.ts:135`. - **Result:** 16 of 16 files SAME on all three legs, with the per-file counts predicted in writing before the run. - **Totals:** 100 changed string leaves in 100 literals: 97 titles and 3 declared. The diff's `+` and `-` lines are exactly the 100 planned lines as multisets, and every file keeps its line count. - **Controls (14 of 14 as predicted on the first run, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; an `it.each` row given an id VIOLATION; an undeclared expect message changed VIOLATION; a title re-split into a `+` chain DIFF; a declared expect message reverted to base SAME; a declared expect message given a new id VIOLATION; the declared table label given a new id VIOLATION; a template-literal title given a new id VIOLATION. - **Templates and tables:** one `.each` title changes, `view-overlay-viewkind-arm.test.ts:102`, a `%s` template whose placeholder and rows are untouched. The table label at `view-union-branch-focus.test.ts:139` feeds two `for … of` template titles, which print it whole. The template-literal title at `:173` changes only its text before `${label}`. **Test counts:** the 16 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 862 tests in 16 files, all passed, with the same count and status sequence per file in 16 of 16. 574 full test names change, and each changed name equals the base name with the planned replacements applied (0 mismatches). No full name repeats on either side. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 16 touched files are in it, and no `*.test.ts` at all. The controls `src/ui/view.zod.ts`, `src/ui/widget.zod.ts` and `dist/index.mjs` are in it. - In the built `dist/`, a new phrase and an old one each read in 0 files. The control `Unrecognized key` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `75022207b3`) - `pnpm turbo run build` over all packages: 71 / 71, through the shared verify lock (`VERDICT command-exit 0`). - `@objectstack/spec`: - `vitest run --project local`: 619 files, 18471 passed, 1 todo. - `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 16 group files, counted by path with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date, against the `dist/` the build above wrote. - **Gates:** `dispatch-gates --commands` derived 79 families, the same set as stage 22, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. The five roster families whose rosters sit under a touched directory were also run, and each exits 0: `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **ESLint, a proven narrowing:** `--no-inline-config` over the 16 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 16 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 200 changed lines (+100 / -100). - A control-byte scan over the 16 changed files finds none. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was four commits past the base (`1f0469655f`: objectstack-ai#21939, objectstack-ai#21937, objectstack-ai#21943, objectstack-ai#21928). They touch 32 files, none of the 16; two are under `packages/spec` (a step-18 semantic migration entry and the migration registry, neither a test file). So `main` was not merged. The census on that tree still reads 571 / 604 in test files and 0 elsewhere. `git merge-tree` onto `1f0469655f` is clean, and none of the 8 open PRs touches any of the 16 files. ## Acceptance notes - **The `{{record.FIELD}}` title.** `view-submit-redirect-url.test.ts:187` keeps its literal placeholder after "ruled bullet 2 —"; this body spells it with `FIELD` because the platform strips angle-bracket fragments from PR text. - **Same-id test titles in this card's later stages** go with those stages: 5 lines in `packages/spec/src`, `api/api-error-code-type.test.ts:71` ("[objectstack-ai#19920] …"), `stack.test.ts:1510` ("(objectstack-ai#17063)"), `system/stack-server.test.ts:93` and `system/translation.test.ts:672` / `:767` ("(objectstack-ai#4001)"). - **Same-id test titles in other packages** stay: 46 lines in 11 packages (`metadata-protocol` 11, `objectql` 8, `spec/scripts` 8, `lint` 6, `rest` 4, `plugin-auth` 3, `cli` 2, and one each in `plugin-security`, `plugin-sharing`, `qa/dogfood` and `service-automation`), each package's share under the objectstack-ai#20513 lane children. The three `plugin-auth` titles cite objectstack's objectstack-ai#5233, a different record from `objectui#5233`. - **Code comments with live ids** remain in these files and their sources, among them the "§objectstack-ai#9933" cross-references above, the `[objectstack-ai#7741]` / `[objectstack-ai#21180]` corpus notes in `view-union-branch-focus.test.ts` and `view-union-diagnostics.test.ts`, and the `objectstack-ai#5055` banners in `widget-i18n-retirement.test.ts`. Code comments are not this card's share. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
…curity follow-up) (objectstack-ai#21964) Fixes objectstack-ai#21932 Clause-②: no ## What This PR adds one checklist item, `access-security.public-form-withdrawal-layers`, to `docs/qa/platform-checklist/areas/access-security.json`. It sits right after `access-security.public-form-intake`. Its fields are rev 1, `since: v17.7`, P1, surface `api`. No other file changes. This delivers the last two rows of objectstack-ai#21932. The first five rows and both re-checks landed in PR objectstack-ai#21943. ### The objectstack-ai#21835 / PR objectstack-ai#21864 row: public-form withdrawal layering The item is written against what PR objectstack-ai#21864 landed on `main`. Its merge, `3c7785d4ab`, is an ancestor of this branch's base `01e0f71a`. Each rule on the card maps to a clause: | Card rule | Where in the item | Oracle | Code anchor | |---|---|---|---| | An env-wide withdrawal is not re-opened by an org overlay | acceptance[0]: both doors answer 404 `FORM_NOT_FOUND` and no row lands. acceptance[1]: an org-scoped save that would leave the form open answers 403 `NOT_OVERRIDABLE` | api | `rest-server.ts#registerFormEndpoints`, `anonymous-form-intake.ts#anonymousFormIntakeWithdrawnIn`, `protocol.ts#anonymousFormIntakeReopenRefusal` | | Only an explicit false withdraws | acceptance[2]: with an absent `allowAnonymous`, or no `publicLink`, env-wide, the org save that opens the form is accepted and both doors serve it | api | `anonymous-form-intake.ts#anonymousFormExplicitWithdrawals`. Premise: `protocol.ts#projectStorableViewBody` | | A package's shipped false withdraws | acceptance[4] | test | `anonymousFormExplicitWithdrawals`. Pins: `protocol.org-scoped-write-refused.test.ts` ('single: a package-shipped form') and `anonymous-form-intake.test.ts` | | The env-wide definition may open a package-closed form | acceptance[5] | test | `protocol.ts#envWideRawViewRows`, with the same protocol pin | | The ruled known limit is recorded as a known gap | `fixtures.knownGaps[0]`, plus a negative saying it is not a FAIL | none | The doors match by served item name. The save check runs only from `saveMetaItem` and the draft promotion, never from `rollbackMetaItem` or `revertCommit` | acceptance[3] is the control pair, which the dogfood also pins: - An organization can always withdraw the form for itself. - A form open at both layers is served, and its row lands in the organization. `automated.ref` leads with `packages/qa/dogfood/test/showcase-public-form-withdrawal-layers.dogfood.test.ts`. That dogfood covers acceptance[0], [1] and [3] end to end. The ref also names the rest, metadata-protocol and metadata-core unit pins. The steps drive the stock showcase form, `showcase_inquiry.contact` at `/forms/contact-us`. The admin saves it at two scopes: env-wide, and in the Default Organization the doors read. Both doors are probed anonymously. ### Where the code is narrower than the card's wording Where they differ, the item follows the code: - **A package's shipped false.** This holds only for an artifact the stack schema parsed (strict `defineStack`, the default). There the schema default `enabled: false` counts as an explicit false. An artifact loaded unparsed (`strict: false`, or a hand-built manifest) is judged as written, so a switch it omits is absent and withdraws nothing. acceptance[4] says so. - **Only an explicit false.** The false must sit on a sharing that keeps a non-empty `publicLink`. A sharing with no link withdraws nothing, even with both switches false. acceptance[2] says so. - **A second documented limit.** `main` carries "Known limit: packages and names" besides the ruled one. Cases where two packages ship the same view name are outside this item's fixture. The item points at that docs section as it reads at the run's commit, rather than restating it. ### The objectstack-ai#21867 / PR objectstack-ai#21928 row Confirmed on `main` with no change. The item is `automation.paused-run-trigger-record-masked` in `docs/qa/platform-checklist/areas/automation.json`, at rev 1, `status: active`. Its `automated.ref` is `packages/qa/dogfood/test/flow-trigger-record-credential-mask.dogfood.test.ts`, which is on disk. PR objectstack-ai#21928 merged as `1f0469655f`, an ancestor of this branch's base. ### Overlap with objectstack-ai#21934 objectstack-ai#21934 is not addressed here. Its PR objectstack-ai#21962 was an open, unmerged draft when this PR was opened, so the item is written against `main` as it stands. - **The ruled known limit does not depend on objectstack-ai#21934.** PR objectstack-ai#21962 leaves the docs page's "Known limit." paragraph and the doors' name-based identity unchanged. - **The multi-package line holds either way.** PR objectstack-ai#21962 rewrites the "Known limit: packages and names" section. This item points at that section as it reads at the run's commit and names objectstack-ai#21934, so its line stays true whether or not PR objectstack-ai#21962 lands. - **The `envWideRawViewRows` note holds either way.** It is scoped to "a form one package ships", which is true before and after PR objectstack-ai#21962. That PR keeps the symbol and resolves it per package. - **No shared files.** This PR touches only the checklist JSON. It changes neither `content/docs/ui/public-data-collection.mdx` nor any package source. ## Tests All results are at head `2d51effa`. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 13 commands, the same 13 the dispatch named. All 13 exited 0, with each exit code captured before any pipe. The `--ran` reconciliation reads 13 derived, 13 run, 0 NOT-MEASURED, 0 UNRUN. - **Checklist gate.** `pnpm check:platform-checklist` answered `OK — 15 areas, 275 items (271 active, 2 planned)` with 690/700 symbol anchors resolved. At the base `01e0f71a` it read 274 items and 676/686. All 14 new anchors resolve, and the 10 that do not are the named objectstack-ai#16898 residual. - **Formula gate.** `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT MET, because formula and lint were unbuilt), so that run measured nothing. I built both under `os-verify-lock` (VERDICT command-exit 0), and the gate then exited 0. - **Not owed.** No package source changed, so no build, test, typecheck or lint is owed. The cited test-case names were read from the test files on `main`. The pins themselves were not re-run here; they ran in CI on PR objectstack-ai#21864 and PR objectstack-ai#21928. ## Acceptance notes - **`coverage.json` is untouched.** The claim's file surface is `areas/*.json`, and the `view` kind is already mapped. Mapping the new item to `view` is optional, and is left to whoever next owns `coverage.json`. - **A stale clause in the sibling item.** `access-security.public-form-intake` clause 7 says "republishing restores service" but does not name the scope of the republish. With layering, republishing in an organization over an env-wide withdrawal is refused with a 403. The new item covers that case. The old item is unchanged, with no revision bump, to keep this PR to the card's rows. - **No changeset.** The diff touches only `docs/qa/platform-checklist/areas/access-security.json`, which no published package ships: the root package is private, and no package `files` entry names `docs/qa`. `skip-changeset` applies. --- _Generated by [Claude Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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,recordandprevious, through the one helperomitInternalFieldsFromWriteResponse(@objectstack/core,packages/core/src/utils/internal-write-response.ts), with the trigger object's definition. A credential-class field (everysecretfield, and everypasswordfield outside the exemptmanagedBybuckets, per ADR-0100 andisMaskedOnReadFieldType) carriesSECRET_MASK, ornullwhen unset. A field declaredinternal: trueis omitted.paramsis the same object asrecord, so it inherits the projection.ctx.result/ctx.previous/ctx.input, which are shared with every other binding and hook on the write, are never touched.readObjectDefinitionreads the engine's optionalgetObjectaccessor. When the definition cannot be resolved (accessor absent, no answer, or a throw), the flow still dispatches unmasked, andreadObjectDefinitionlogs 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.record,$record,previous),SuspendedRun.context, the persistedvariables_json/context_json, the run read doors, and the run a resume rehydrates, in-process and after a restart. ⛔ No mask inservice-automationor 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/maindcb11c2ec9)buildContext.this.engine.getObjectis already read there for materialisation. Re-check grep: 9 hits inrecord-change-trigger.ts.decoupleFromEngineStateon both roots, then the return. The projection sits between the decoupling and the return.examples/**/*flow*andexamples/**/flows/**returns zero hits (git grepexit 1). Control: the same paths carryrecord.FIELDreads in 4 files, so the zero is not a dead pattern. The only example object withpassword/secretfields isshowcase_field_zoo. Its one record-change flow (showcase_approver_bindings,status: 'draft') reads neither field.get_recordand the other CRUD nodes (service-automation/src/builtin/crud-nodes.ts) read throughdata.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):passwordandsecretcarry the mask onrecordand onprevious, and theinternalfield is omitted.paramsis the same object asrecord.null.better-auth-managedpasswordkeeps the read path's exemption.afterDelete(record from the prior row) andbeforeUpdate(payload over the prior row) are masked on both roots.packages/qa/dogfood/test/flow-trigger-record-credential-mask.dogfood.test.ts. A real boot:bootStackwith automation, a file-backed database, the real crypto provider and the record-change trigger. It uses one object with an ordinary field, apasswordfield, asecretfield and aninternalfield, and onerecord-after-updateflow that pauses at ascreennode. The cases:variables_jsonandcontext_jsoncarry the mask for both credential fields and omit the internal field (record,$record,previous), with no stored credential spelling anywhere in either column.GET /automation/:name/runs/:runIdshows the same.record.CREDENTIAL_FIELD/previous.CREDENTIAL_FIELDstores the mask, while the ordinary field stores its value.resolveSecretFieldpath still returns the plaintext.automation.paused-run-trigger-record-maskedindocs/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.refnames the dogfood pin, and aknownGapsline says the pin reads as the admin.Upgrade text
.changeset/21867-flow-trigger-record-credential-mask.md:@objectstack/trigger-record-changeminor,@objectstack/specpatch. 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.packages/spec/src/migrations/entries/semantic/18.flow-trigger-record-credential-masked.ts, a sibling of18.by-id-write-unreadable-row-not-found. It is registered throughgen:migration-registry(registry.ts) and declared in the changeset asregistered 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/dataand@objectstack/coreto source; thecheck-test-source-aliasregistry entry for this package drops@objectstack/core.Verification
Round 1 readings are at head
67ce8a46a2unless marked. Round 2 readings are in their own block below, at head4f287e072f.scripts/ablation-replace.mjs, wrap mode, with an EXIT/INT/TERM restore. On-disk proof: anchor 1 → 0, marker 0 → 1, blobd0702684cb19→27a41720a0ab. The dogfood project aliases@objectstack/trigger-record-changeto source, and the plugin is passed inextraPluginsfrom that import, so no dist hop applies.paramsidentity, unset reads null, hook objects whole. All four hold without a mask too.d0702684cb19, andgit diff HEADis empty.@objectstack/trigger-record-changepnpm test: 11 files, 108 tests, green at67ce8a46a2.@objectstack/corepnpm test: 77 files, 2177 tests, green.@objectstack/service-automationvitest: 173 files, 2112 tests, green.48e0b1cc35(trigger source unchanged since).@objectstack/specsrc/migrations: 3 files, 179 tests, green.@objectstack/trigger-record-changetypecheck, includingtsconfig.test.json: green.--listFilescounts the new test file once.@objectstack/dogfoodtypecheck: green, and it covers the new file.@objectstack/spectypecheck(src, scripts, test layer): green.@objectstack/speccheck: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..tsfiles, undereslint --no-inline-config --format json: 6 files, 0 errors, 0 warnings..changeset/*.mdandautomation.json) answer "File ignored because no matching configuration was supplied".--print-configshowsparserOptionswithoutproject, so type-aware linting is off. This diff cannot move any untouched file's verdict.Round 2, at head
4f287e072forigin/mainwas merged in as a merge commit (baa4b2fe6c; the branch was 9 behind).pnpm --workspace-concurrency=2 --filter '@objectstack/trigger-record-change...' build: exit 0.@objectstack/trigger-record-changepnpm test: 11 files, 114 tests, green. The mask file has 13 cases.@objectstack/trigger-record-changetypecheck(tsc --noEmit && tsc --noEmit -p tsconfig.test.json): exit 0 for both.scripts/ablation-replace.mjs, an early return was planted inomitInternalFieldsFromWriteResponse(packages/core/src/utils/internal-write-response.ts), with coredistnot rebuilt (marker: 0 hits inpackages/core/dist). Landed: anchor 1 → 0, blob2a6a48c04fdb→d51283c8f5e6. Result: 5 red, 8 green; the red ones are the masking cases, the newafterDeleteandbeforeUpdateincluded. Restore: blob == HEAD2a6a48c04fdb,git diff HEADempty. 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.false. Landed: anchor 1 → 0. Result: 3 red (the absent, no-answer and throw cases), 10 green. Restore: blob == HEAD04e3ca86825f,git diff HEADempty.check:adr-0087-registration(reads[BREAKING+bang] registered flow-trigger-record-credential-masked;--self-test441 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/speccheck:generatedafter the main merge: all 15 generated artifacts up to date, against the specdistbuilt post-merge.check:console-injection. It skipped, because there is nopackages/console/distin this worktree.dispatch-gatesderivation (109 commands over the whole PR diff, mostly round-1 spec and dogfood families) was not re-run this round; CI owns it.record-change-trigger.ts,trigger-record-credential-mask.test.ts,vitest.config.ts,scripts/check-test-source-alias.mjs), undereslint --no-inline-config --format json: 4 files, 0 errors, 0 warnings. All 4 are in eslint's own config, per--print-config, which also showsparserOptions.projectandprojectServiceundefined, so type-aware linting is off and this diff cannot move any untouched file's verdict.Acceptance notes
packages/triggers/trigger-record-change/src. This PR also touches that package'svitest.config.ts(the alias above) and adds one dogfood test file underpackages/qa/dogfood/test/, as the dispatch asked. Round 2 also touchesscripts/check-test-source-alias.mjs, a registry narrowing only (this package's entry drops@objectstack/core).packages/plugins/plugin-approvals/distandpackages/plugins/plugin-auth/distwere found without.d.ts(written mid-pass).check:dts-closureandcheck:dual-build-cjs-loadswent 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.buildContext, the materialisation read ofgetObject(gated on ground truth) is not wrapped in try/catch. AgetObjectthat throws therefore fails the dispatch before the mask runs, and the handler logs "execution failed". SoreadObjectDefinition'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 fromgetObject.48c162ed06ports 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 thePM dispatch-gates self-testred thatmainhas 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