Repository navigation
test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read - #21935
Merged
objectstack-fleet[bot] merged 1 commit intoOct 6, 2026
Conversation
…cratch-dir scan can read The dispatch-gates self-test refuses a mkdtempSync whose base it cannot read, and the per-file cwd took its base from inject(): a path handed over from the globalSetup. Each file's directory is now made as mkdtempSync(join(tmpdir(), TAG-file-)), where TAG is the run's reserved name (only a name crosses inject(), and a separator in it is refused). The globalSetup sweeps that prefix at run end, then removes the reservation, so the shared boot's handles still live until the run ends and a concurrent run's directories are never touched. Isolation, the leftover guard and the stale-dir clearing are unchanged. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
objectstack-fleet
Bot
deleted the
claude/issue-21924-per-file-cwd-readable-base
branch
October 6, 2026 02:54
This was referenced Oct 6, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…rries the credential mask and omits internal fields (objectstack-ai#21928) Fixes objectstack-ai#21867 Clause-②: no ## What this changes Ruling A on objectstack-ai#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 (objectstack-ai#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 objectstack-ai#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 objectstack-ai#21919. It is a no-op once objectstack-ai#21935 lands. --- _Generated by [Claude Code](https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
… metadata layer holds; layering can only narrow intake (objectstack-ai#21864) Fixes objectstack-ai#21835 Clause-②: yes (widening) Fixes a regression introduced after 17.6.0 (with objectstack-ai#21420); it should land before 17.7.0 is cut. ## What Per the rulings recorded on objectstack-ai#21835: a public form's withdrawal is a kill switch, layering can only narrow anonymous intake, a withdrawal closes the **same form** only, and only an **explicit** withdrawal counts. - **Anonymous doors (`GET /forms/:slug`, `POST /forms/:slug/submit`).** Both use one resolver and judge by the name of the view item they serve. When an organization is resolved, the env-wide view list beneath it is read as well. A form is served only when the env-wide item of the same name does not explicitly withdraw a form in the same slot (nested form, the same `formViews` key, or the flattened config) or with the same slug. Other views that share the public slug never close each other. - **What counts as a withdrawal.** A sharing that keeps its `publicLink` and sets `enabled: false` or `allowAnonymous: false`. Only an explicit false counts. Not a withdrawal: an absent switch, a sharing with no link (raw, or schema-parsed), a cleared link, a removed sharing block, or no body of the view at that layer. The public data collection docs page has a "Withdraw a public form" section with these rules. - **Write door (save and publish).** An org-scoped `view` save or draft promotion in the organization the doors read is refused with `403 NOT_OVERRIDABLE` when it would leave open a form the env-wide definition explicitly withdraws. It judges by the stored row: the body is compared with the env-wide body of the row it is keyed by (the active env-wide row, else the package artifact), matched by slot or by slug. So renamed `formViews` keys, `form.name`, slot moves and listViews collision renames are the same form. It is also judged against the env-wide view list the way the doors read it, with container bodies expanded. Re-saving an overlay that was open before the withdrawal is refused. The message names both remedies. - **Package-shipped forms.** A package artifact is part of the env-wide definition, not a separate layer. A package artifact parsed by the stack schema (strict `defineStack`, the default) carries the schema's default `enabled: false`, so a shipped form that keeps its link without switching `enabled` on is an explicit withdrawal and fails closed. An artifact loaded without that parse (`defineStack(..., { strict: false })` or a hand-built manifest) is judged as written: a switch it omits is absent, which is not a withdrawal. The env-wide definition is the administrator's switch, so an env-wide save may open a form the package ships closed. - **Known limit: packages and names.** A withdrawal of a view name closes that name in every package: when two packages ship a view of the same name, one package's withdrawal also closes the other package's form of that name. It may over-close, never under-close. Per-package precision is tracked in objectstack-ai#21934. A publish judges the draft it promotes under the same package key (the stated one, else the resolved draft row's own), so with two packages holding a draft of the same view in one organization, each draft is judged on its own publish. - **Intentional reversal.** The earlier behaviour in which an organization overlay re-published a form the package had withdrawn is reversed. A form with no env-wide word on it stays organization-publishable (objectstack-ai#21420), and the objectstack-ai#21473 anchors and objectstack-ai#21566 field allowlist are unchanged. - **Public surface:** `@objectstack/metadata-core` adds one export, `anonymousFormIntakeWithdrawnIn` (`minor`). `@objectstack/rest` and `@objectstack/metadata-protocol` are `patch`. - **Known limit (ruled to stay as is).** The doors match by served item name, and the write door runs only on an org-scoped save or publish. An organization overlay stored before the env-wide withdrawal, or restored by rollback or commit revert, can still be served if it keeps the form open under a different key or slot than the env-wide definition. Withdrawing the form in that overlay closes it. Stated in the changeset and the docs. ## Tests The first bullet is round 6, the second round 5, the third round 4; the bullets after them were measured at `e8778acb96` (round 2): - Round 6 at `7882eef683` (merged origin/main `9dce635337`, merge commit `46d08189a7`): metadata-core 18 files, 411 passed; metadata-protocol 216 files (3 skipped), 27940 passed, 19 skipped; rest 260 files, 4912 passed, 326 skipped; objectql 375 files, 7469 passed (suites at `4d5f6c4e61`; the later commits touch docs, the changeset and three ported dogfood files only). Typecheck green for metadata-core, metadata-protocol, rest and objectql, test layers included. 97 of 97 derived gates green at `7882eef683`, reconciled with `dispatch-gates --ran`; `dispatch-gates --self-test` 1976 cases pass. The cross-package skip of round 5 is removed per the ruling, so a withdrawal of a view name closes it in every package again. Pins: another package's withdrawal of the same name closes this package's form too (metadata-core and the doors); with two packages shipping the same view name, a row-anchored rename by a package-bound org save is refused (metadata-protocol), with a withdrawn-save control. Ablation: the two edited sources set back to their round-5 blobs and rebuilt, markers proved in `dist/`: 1 red in each of metadata-protocol, metadata-core and rest; restored to HEAD (`git diff HEAD` empty), rebuilt, markers proved absent. - Round 5 at `d8657b5c19`: metadata-core 18 files, 411 passed; metadata-protocol 216 files (3 skipped), 27938 passed, 19 skipped; rest 260 files, 4911 passed, 326 skipped; objectql 374 files, 7464 passed. Typecheck green for metadata-core, metadata-protocol, rest and objectql, test layers included. 97 of 97 derived gates green, reconciled with `dispatch-gates --ran`. New pins: two packages' drafts of one view in one organization are each judged on their own publish; one package's withdrawal of a name closes its own form (metadata-core and both doors; the cross-package half was inverted in round 6). Ablation: removing the package key from the publish gate's draft read turned the two-package pin red, and removing the package comparison turned the cross-package pin red; both restored to HEAD (`git diff HEAD` empty). - Round 4 at `79b847042d` (targeted): metadata-core `anonymous-form-intake.test.ts` 39/39, metadata-protocol `protocol.org-scoped-write-refused.test.ts` 41/41, rest `public-form-withdrawal` + `public-form-intake-availability` 43/43. Typecheck green for metadata-core and metadata-protocol. Docs and changeset gates green. New pins: a package parsed `false` is a withdrawal; an env-wide save opens a package-closed form. - `@objectstack/metadata-core`: 18 files, 394 passed. - `@objectstack/rest`: 260 files, 4906 passed, 326 skipped. - `@objectstack/metadata-protocol`: 214 files (3 skipped), 27752 passed, 19 skipped. - Typecheck green for all three and dogfood. - Dogfood (real showcase boot): the layered-withdrawal suite 5/5 (including the re-save refusal) and the five sibling public-form suites 20/20. - Coverage: two views sharing a slug do not close each other (one read, with and without an organization, and across layers); a cleared link is not a withdrawal; a parsed link-less sharing is not a withdrawal; re-saving an already-open overlay is refused; a container-shaped save is judged after expansion. - Ablation (source set back to the base blobs, packages rebuilt): 3 / 4 / 4 tests red across metadata-core / rest / metadata-protocol; restored to HEAD. - Gates: 92 of 95 derived run green; `check:skill-examples`, `check:dual-build-cjs-loads` and `check:type-check-debt` are NOT MEASURED locally (workspace-wide prerequisites) and left to CI. ## Acceptance notes - The known limit above (an overlay stored before the withdrawal, or restored by rollback or revert, with its form under a different key or slot) is accepted per the ruling on objectstack-ai#21835. No provenance or new protocol query was added. - Two installed apps publishing the same slug is a separate concern (slug collision), out of scope here. - A package artifact loaded without the stack schema's parse (`strict: false`, a hand-built manifest) is judged as written; giving every load path the schema's sharing defaults is left as a possible follow-up (see the round 5 report on objectstack-ai#21835). - An objectql test double now answers the publish gate's draft-row read (`protocol-publish-package-drafts.test.ts`); that is test-only. - Maintainer ruling, 2026-10-06: 「撤掉跨包那一改,合并」. The cross-package skip is removed; per-package precision is tracked in objectstack-ai#21934. - This branch carries three dogfood files ported unchanged from objectstack-ai#21935 (`packages/qa/dogfood/test/per-file-cwd.setup.ts`, `packages/qa/dogfood/test/per-file-cwd.global-setup.ts`, `packages/qa/dogfood/vitest.config.ts`) so the dispatch-gates self-test is green here; they merge away once objectstack-ai#21935 lands. --- _Generated by [Claude Code](https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…asses the explicit system opt-in instead of no principal (objectstack-ai#21940) Fixes objectstack-ai#21913 Clause-②: yes (widening) This is a slice of objectstack-ai#21908: the services-lane producers. objectstack-ai#21908 stays open, because it builds the deny itself, last. ## What changes Every engine call in the card's named functions now passes the explicit system opt-in that exists today: `{ isSystem: true }` on the call's context. These calls used to reach the data engine with no context at all, so they had no principal and no opt-in. They got past the security middleware only through its principal-less hand-off (ADR-0096 E1), which objectstack-ai#21908 retires. This PR adds no new elevation API, changes nothing any door authorizes, and does not build the deny. | Row | Package | Function | Engine calls that now carry the opt-in | | :-- | :-- | :-- | :-- | | 7 | service-settings | `SettingsService.loadRows` | `find` on `sys_setting` | | 8 | service-settings | `SettingsService.upsertRow` | existence-probe `find` and `insert` on `sys_setting` (its `update` already had the opt-in) | | 8 | service-settings | `buildSettingAuditWriter` `write` | `insert` on `sys_setting_audit` | | 11 | service-datasource | `loadDatasourceRows`, `loadDatasourceRow` | `find` / `findOne` on `sys_metadata` | | 11 | service-datasource | `persistDatasourceRow`, `deleteDatasourceRow` | `findOne` + `insert` / `update` / `delete` on `sys_metadata` | | 11 | service-datasource | secret binder `bind` / `unbind` / `resolve` | `insert` / `delete` / `find` on `sys_secret` | | 12 | plugin-webhooks | `AutoEnqueuer.doRefresh` | `find` on `sys_webhook` | | 12 | plugin-webhooks | `createWebhookRedeliverGuard` | `findOne` on `sys_webhook` | | 13 | service-messaging | `SqlNotificationOutbox.claim` / `claimDigest` / `reapExpired` | candidate `find`, claiming `update`, read-back `find`; the reap `update` | | 13 | service-messaging | `SqlHttpOutbox.claim` / `reapExpired` | candidate `find`, claiming `update`, read-back `find`; the reap `update` | | 14 | service-messaging | `MessagingService.writeEvent` | `insert` on `sys_notification` | | 14 | service-messaging | inbox channel `send` + `writeDeliveredReceipt` | `insert` on `sys_inbox_message`, the recipient-locale `findOne` on `sys_user` (a helper only `send` calls), `insert` on `sys_notification_receipt` | | 14 | service-messaging | `PreferenceResolver.loadRows` | both `find`s on `sys_notification_preference` | | 14 | service-messaging | `RecipientResolver.resolveEmail` | `findOne` on `sys_user` | IDataEngine reads pass the opt-in in the trailing options argument, which is where the contract puts a read's context. Two package-local surfaces have a single options bag, and the opt-in goes there: `SettingsEngine`, and the `sys_secret` binder's engine slice. `SettingsEngine.find` and `.insert` and `SecretStoreEngineLike.delete` now declare the `context` they receive. No symbol is new on any package entry. The shared constants (`FAN_OUT_SYSTEM_CONTEXT`, `DISPATCHER_SYSTEM_CONTEXT`) live in package-internal modules. Rows 15 and 16 are not in this slice and wait for the maintainer. ## Measurement **Instrument (H2).** A local, uncommitted instrument sat at the security middleware. It recorded each principal-less, non-system context that reached the hand-off, with its stack. It recorded whether any of the six gates before the hand-off threw on such a call, and which of them matched the call's object and verb. It also recorded the outcome after `next()`: the result type, row count, key set, a hash of the non-volatile values, or the error code. In the AFTER leg it recorded the same outcome for each `isSystem` call whose stack ran through these four packages. Both legs covered the whole dogfood suite (206 files, 1590 tests passed, 9 skipped, identical in both legs) and a booted showcase dev composition. The boot covered seed-admin, a settings read plus two writes, a runtime datasource create / patch / read / delete, and admin and anonymous requests, then sat idle for 65 seconds so the dispatchers and the webhook refresh ticked. The instrument was then reverted, and the file's blob equals HEAD (`5b4ab28045af`). The plugin-security dist was rebuilt clean: `ablation-dist-preflight --absent` passes, and the marker had 3 hits in the instrumented dist. **Before and after, per function.** Columns: principal-less records BEFORE, principal-less records AFTER, and `isSystem` records AFTER. | Function | dogfood before / after / after-system | boot before / after / after-system | | :-- | --: | --: | | `SettingsService.loadRows` | 2090 / 0 / 2090 | 42 / 0 / 42 | | `SettingsService.upsertRow` (probe + insert) | 7 / 0 / 7 | 3 / 0 / 3 | | setting-audit `write` | 4 / 0 / 4 | 2 / 0 / 2 | | `loadDatasourceRows` | — | 1 / 0 / 1 | | `persistDatasourceRow` | — | 4 / 0 / 4 | | `deleteDatasourceRow` | — | 2 / 0 / 2 | | `AutoEnqueuer.doRefresh` | — | 3 / 0 / 3 | | `SqlNotificationOutbox.claim` | 8 / 0 / 8 | 56 / 0 / 56 | | `SqlNotificationOutbox.claimDigest` | 8 / 0 / 8 | 56 / 0 / 56 | | `SqlNotificationOutbox.reapExpired` | 1 / 0 / 1 | 7 / 0 / 7 | | `SqlHttpOutbox.claim` | 8 / 0 / 8 | 56 / 0 / 56 | | `SqlHttpOutbox.reapExpired` | 1 / 0 / 1 | 7 / 0 / 7 | | `MessagingService.writeEvent` | 8 / 0 / 8 | — | | inbox `send` (row insert) | 8 / 0 / 8 | — | | `writeDeliveredReceipt` | 8 / 0 / 8 | — | | `PreferenceResolver.loadRows` | 16 / 0 / 16 | — | | `RecipientResolver.resolveEmail` | 1 / 0 / 1 | — | Principal-less totals moved 35245 → 33077 in dogfood (Δ 2168) and 422 → 183 at boot (Δ 239). Each delta is exactly the sum of the rows above. No principal-less record attributed to any moved function remains. The hand-off still sees row 15 and every other lane's producers. No run reached these, so each is held by its unit pin instead: `loadDatasourceRow`, the secret binder (this repo wires it into no composition), the redeliver guard, the inbox recipient-locale read (template path), and the claim path's `update` and read-back (no pending rows in any run). **Gates before the hand-off (Zone 1).** Across 35245 dogfood and 422 boot principal-less records, the "gate threw" record fired 0 times. The package-managed, system-row, curated-capability and audience-anchor gates never matched an object or verb these producers touch. Neither did the delegated-administration gate. The engine-owned guard matched the bucket on the writes to engine-owned objects. On a context with no user id, its own `isUserContextWrite` predicate returns before it can refuse. **No producer is held back.** **What each call answers is unchanged.** Per function, call counts per object and verb are equal before and after. So are the outcome shapes (result type, row count, key set). There were 0 errors in either leg. Content hashes are equal for 11 of 14 functions in dogfood and 7 of 9 at boot. The rest differ only on values that change every run: the receipt's `at` timestamp (all 8), and inbox and notification payloads that carry a per-run record id or date (2 of 8 and 3 of 8, from the approval and sweep tests). At boot, the probe's own per-phase file path sits in the stored datasource record. The plugin-audit rows these writes produce (`sys_audit_log`, `sys_activity`) are written in equal numbers before and after. **H6, `loadRows`.** The call count is the same (2090 + 42), and the returned settings have equal hashes on every call. The opt-in adds one frozen context object. The middleware now exits at its system short-circuit instead of running the six gates and the hand-off. No wall-clock figure is quoted, because the container is shared. **H7, reads on another principal's behalf.** What these reads return (a user id for an address, a locale, preference rows) is consumed inside the fan-out. `emit()` answers the notification id, counts and per-delivery outcomes. Its three in-repo callers (approvals, the flow notify node and comment mentions) relay counts and the id only. The opt-in changes none of this, because the principal-less read returned the same rows. **One engine branch keyed on the flag stops running on these writes.** It is row 23 of the `isSystem` census page: the dangling-reference check is skipped for an `isSystem` write. Before the move, it ran 10 times nested under these producers (`writeEvent` 2, inbox `send` 2, setting-audit `write` 6), on the `actor_id` lookups, and resolved every time. After the move it does not run. A local probe (real ObjectQL and SQLite, deleted after the run) showed what that means for an `actor_id` that names no user. With no context, today's path refuses with `VALIDATION_FAILED` ("Actor: no sys_user record has id …"). Under `isSystem` the row is written. A real user is written both ways. That `actor_id` comes from `emit()`'s `actorId`, which a flow notify node can author. So the behaviour on measured traffic is unchanged, and a latent difference remains for an `actorId` that names no user. The Acceptance notes carry it. **H4 pins and ablations.** There is one pin per package. The engine double sits behind the package's real call path, proves the population ran, and asserts `isSystem` on every call. Each pin was ablated by dropping the opt-in through `scripts/ablation-replace.mjs` (the anchor must hit). Seven legs ran: settings `loadRows`, the fan-out constant, the dispatcher constant, the datasource `sys_metadata` constant, the secret-binder constant, and the two webhook constants. Every leg went red under the mutation, and the failure names the call, for example "find on sys_setting: expected undefined to deeply equal { isSystem: true }". Every leg was restored with blob equal to HEAD and an empty `git diff HEAD`, and went green again. The pins are package-local, imported from `src` with no dist in the path. **Census pages (H3).** The `isSystem` census (`check-system-context-census`) is OK, and `--fix` changed nothing: this change adds no elevation read site. The tenant-audit census did move, because the write sites now thread a context. It was regenerated with `tenant-audit-census.mjs --write`. On its page, the hand-written figures follow the census: the provable no-context, tenancy-enabled count went 9 → 2, unreadable 67 → 60, decidably elevated 114 → 121. **Serial (H5).** `origin/main` was merged twice. It now includes objectstack-ai#21906's squash, and the merge was clean. A `git merge-tree` against objectstack-ai#21877's head (`5c405846`, now closed as a draft) is clean. This PR edits neither PR's region: `datasource-admin-plugin.ts` and `datasource-secret-binder.ts` only, in `service-datasource`. ## Tests - At `10e77fef6f`, after merging `origin/main` `faf8dce482`. The next merge (`9dce635337`, which brings this PR to `62960ffa1a`) touches no file in these four packages. Typecheck of the four packages: exit 0. - Unit suites: service-settings 614 passed, service-messaging 510, service-datasource 743, plugin-webhooks 165. All exit 0, unchanged apart from the new pins and tests that came in from `main`. - ESLint, narrowed to the 19 changed TS files with `--no-inline-config --format json`: 19 files, 0 errors, 0 warnings. Those files are inside the config's own `packages/**/*.{ts,…}` population, and the config enables no type-aware linting, so this diff cannot move a verdict on any untouched file. The full `pnpm lint` run belongs to CI. - At `62960ffa1a`, the head this PR opens with, every one of the 105 commands `dispatch-gates --commands --repo objectstack-ai/objectstack` derives exited 0. `dispatch-gates --ran` reports: "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED". In an earlier pass, four of these went red on this branch, and they are now fixed. `check:tenant-audit-census` needed the census regenerated. `check:engine-double-contract` and `check:objectql-double-limit` needed the pin doubles routed through the shared dispatch asserts and holding a find's bound, with the ledger recording the new pinned coverage. `check:dual-build-cjs-loads` needed eight unrelated packages built first. ## Acceptance notes - **Producers in these packages that the card does not name.** A static read finds that they still reach the engine with no context. No run exercised them, so the measured table never listed them. Without a route, objectstack-ai#21908's deny breaks each one, so they are listed for the seat's closure rather than moved here: - service-settings: the `sys_secret` store the plugin builds (`insert` / `get` / `update`), and `SettingsService.readStoredHandle`. - service-messaging: `SqlNotificationOutbox` and `SqlHttpOutbox` `enqueue`, `ack` and `list`; the email and SMS channels' recipient reads; `RecipientResolver.resolveRole` / `resolveTeam` / `resolveOwnerOf`; the emit dedup lookup; the template renderer's read. - `resolveOwnerOf` reads a business object, and its posture is not neutral. Today the sharing middleware answers a principal-less read of a `private` object with a deny-all filter, so an `owner_of:` recipient on such an object resolves to nobody. Under the opt-in, that filter would be bypassed. - **Request-door producers that act on the caller's own rows, like rows 15 and 16** (report-only, for the maintainer's ruling): the inbox unread count, and mark-read / mark-all-read (`unreadNotificationIds`, `upsertReadReceipt`, `notificationOrganization`). - **The row-23 difference above:** the dangling-reference check stops running on the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`. - One posture question was noted on a request-door read and is held off-thread. It was not measured. ## Seat's append: patch round 1 at `57f738dfb1` (written by `domain:services` seat 1 from the dev's report `6009307655`; the dev does not edit this body) **What changed in the patch round** (seat verdict `6008259054`). The sections above describe `62960ffa1a`; where they differ, this append is current. - **`Clause-②: yes (widening)`.** The exported `SettingsEngine` (`find`, `insert`) and `SecretStoreEngineLike` (`delete`) gain an optional `context`, so `@objectstack/service-settings` and `@objectstack/service-datasource` take a `minor`. `service-messaging` and `plugin-webhooks` stay `patch`. Line 2 above, the changeset and the claim (`6003840075`) moved together. Nothing accepted or refused at any door changes. - **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write and has no option to keep it. So each producer that writes a user reference does one guarded `sys_user` read by id under the opt-in, then refuses an unknown id with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. - The checked references are the `actor_id` of `sys_notification` (`writeEvent`), of `sys_inbox_message` (the inbox send) and of `sys_setting_audit` (the setting-audit writer), and the `user_id` of a user-scope `sys_setting` row on `SettingsService.upsertRow`'s insert. The last is the same difference, which this PR's opt-in introduced on that insert. - The refusal is built by `validationFailure` from `@objectstack/types`, already a runtime dependency of both packages, so neither package stamps the code itself and `check:error-code-provenance` is green with no spec row and no waiver. It is shape-identical to the engine's refusal but not `instanceof` objectql's `ValidationError`; the callers on these paths read the message or the code, and every door maps the shape to `400 VALIDATION_FAILED`. - A write that names no user is unchanged. A read that cannot run lets the write through, as the engine's check does. The cost is one extra `sys_user` read per write that names a user. - Differential pins over a real engine hold each producer's answer equal to the engine's own refusal of a context-less insert. Four ablations went red and were restored with blob equal to HEAD. - **objectstack-ai#21935 merged in** (`a3c2209a68`). `check:pm-dispatch-gates` exits 0. - **Gates at `57f738dfb1`:** 105 derived, 105 run, all exit 0. The 54 roster families: 51 exit 0, and 3 are NOT WIRED locally (they need a pull-request context; CI runs them). - **`service-settings/vitest.config.ts`** gains one anchored alias (`platform-objects/identity` → `src`) for the new pin, which `check:test-source-alias` asks for. **Carried, not filed here:** - Producers in these packages that the card does not name are recorded on objectstack-ai#21908's census (rows 24 onward). `resolveOwnerOf` is not neutral to move. - The inbox unread count and mark-read / mark-all-read join the maintainer's open ruling on rows 15 and 16. - One request-door posture question is held off-thread, at class level only. --- _Generated by [Claude Code](https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 9, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 9, 2026
…der tmpdir() and removes them itself (objectstack-ai#22416) Fixes objectstack-ai#22400 Clause-②: no ## What this changes One test file, `packages/qa/dogfood/test/security-catalog-cold-boot-environment-holder.dogfood.test.ts`, in the private `@objectstack/dogfood` package: - `databaseFile()` takes its `mkdtempSync` base from `join(tmpdir(), 'catalog-cold-boot-')`. Before, the base was `join(process.cwd(), 'catalog-cold-boot-')`. - The per-file cwd sweep no longer covers these directories, so the file records every root `databaseFile()` creates and removes them in a new `afterAll`. That hook runs after `afterEach` has stopped the last kernel. A refused boot leaves no kernel to stop, but its directory is still removed. This follows the harness's own `BootOptions.databaseFile` contract: "Callers own the file's lifetime (create it under a temp dir, delete it after)". - The header comment now describes the new placement and why the base is spelled `tmpdir()`. The old text ("the files live in this test file's own working directory, which the dogfood run removes at its end") is no longer true. Why: `scripts/pm/dispatch-gates.mjs`'s scratch-directory scan must be able to read every `mkdtempSync` base. `process.cwd()` is not a base it reads, so the site came back UNRESOLVED and the `pm dispatch-gates self-test` case failed on `main`. This uses the same remedy as PR objectstack-ai#21935, which fixed the same mechanism on `per-file-cwd.setup.ts`: the base is made readable at the site. Per triage, `dispatch-gates.mjs` is not touched (the guard is not loosened, and `process.cwd()` is not taught to the scan), and `per-file-cwd.setup.ts` is not touched. ## Evidence ### `pnpm check:pm-dispatch-gates` (run detached, followed with `tail --pid`) | tree | final line | |---|---| | `main` `83e7ae93a` (unfixed, in a separate worktree pinned there) | `✗ dispatch-gates self-test: 1 of 2011 case(s) failed.` (the failing case: `✗ no mkdtempSync site in this tree takes a base the scan cannot read — UNRESOLVED: packages/qa/dogfood/test/security-catalog-cold-boot-environment-holder.dogfood.test.ts:108 (a base this scan cannot read: process.cwd())`) | | this branch `d8eee2191` | `✓ dispatch-gates self-test: 2011 cases pass.` | On this branch the case itself reads `✓ no mkdtempSync site in this tree takes a base the scan cannot read` (log line 1533 in both runs). The wrapper's own battery reads `✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.` on both trees. The unfixed run ends `ELIFECYCLE Command failed with exit code 1.`, and the fixed run prints no such line. Battery time on this shared box: 781.7s before and 774.7s after. The same scan, read directly through `exposedScratchDirs()` on both trees: `sites=1625 inTree=63 exposed=0`, with `unresolved(mkdtempSync)` going from 1 to 0 and `unresolved(all)` from 289 to 288. Site and in-tree counts are unchanged, so the one site moved to "outside", not out of the scan. ### The dogfood file itself (through `scripts/pm/os-verify-lock.sh`, on `d8eee2191`) `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/security-catalog-cold-boot-environment-holder.dogfood.test.ts` ``` Test Files 1 passed (1) Tests 4 passed (4) os-verify-lock: VERDICT command-exit 0 ``` A `catalog-cold-boot-*` listing under `tmpdir()` (`/tmp` here), taken before and after the run, with a poller sampling `/tmp` every 100 ms during it: ``` before (2026-10-09T04:02:10Z): count=0 seen during the run: 2026-10-09T04:02:32.669Z /tmp/catalog-cold-boot-6eM9D3 2026-10-09T04:02:36.183Z /tmp/catalog-cold-boot-KGxFei 2026-10-09T04:02:37.484Z /tmp/catalog-cold-boot-eGpgIr 2026-10-09T04:02:39.290Z /tmp/catalog-cold-boot-PzYAkQ (distinct seen=4, one per case) after (2026-10-09T04:02:41Z): count=0 ``` So the four roots really are created under `tmpdir()` and none survives the file. Nothing else removes them: the dogfood globalSetup teardown only sweeps its own `os-dogfood-run-TAG-file-` prefix. `pnpm --filter @objectstack/dogfood typecheck` gives `VERDICT command-exit 0`. The package's `tsconfig.json` includes `test/**/*`, and `tsc --noEmit --listFiles` names this file once. ### Derived gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 53 commands from this diff (1 path vs merge base `83e7ae93a`). Each was run on `d8eee2191` with its exit code recorded, then reconciled with `--ran`: ``` Run reconciliation — 53 derived, 53 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 53 derived famil(ies) accounted for — 53 run, 0 NOT-MEASURED (a DERIVED zero — all 53 recorded an exit code and none of them is 3). ``` On its first pass, `pnpm check:dual-build-cjs-loads` exited 3 (`PREREQUISITE NOT MET`): eight packages outside the dogfood closure had no `dist/`. They were built through the lock (44 of 44 turbo tasks, all cache hits), and the re-run exited 0: `✓ check:dual-build-cjs-loads — 107 published require entry point(s) across 66 package(s) load`. The record above carries that re-run's code. The other 52 exited 0 on the first pass. ESLint, narrowed to the one changed file: `eslint --no-inline-config --format json` gives 1 file, 0 errors, 0 warnings, and the file is not ignored. The resolved config sets neither `parserOptions.project` nor `projectService`, so linting is not type-aware and this diff cannot change the verdict on any file it does not touch. The repo-wide `pnpm lint` is CI's. ## Acceptance notes - No changeset: `@objectstack/dogfood` is `private: true` and the diff is one test file. - The full dogfood suite is not run locally. CI's `Dogfood Regression Gate` runs it. Locally this file ran on its own, as above. - The workflow-valued families the derivation names as NOT MEASURED (`check-shard-attestation --emit …`, `check-test-completeness "$RUNNER_TEMP/…"`, `check-issue-citations --census`) take a value that exists only in a CI run. They are CI's. --- _Generated by [Claude Code](https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #21924
Clause-②: no
What changes
main's hourlyLint & Type Checkrun is red on onedispatch-gatesself-test case (run37394652870atbe97cf3c93,Lint & Repo Gates, stepPM dispatch-gates self-test):PR #21919 added that site for #21914. The guard is right: a
mkdtempSyncbase handed over throughinject()is an expression the tree's scratch-directory scan cannot read. So the fix is at the site, and nothing in the guard or the scan changes.packages/qa/dogfood/test/per-file-cwd.setup.ts: each file's directory is nowmkdtempSync(join(tmpdir(), perFileDirPrefix(runTag))). The base istmpdir(), outside the tree by construction, and the scan reads it as such.inject()(key renameddogfoodCwdRoottodogfoodRunTag), and only as a name component...is refused.packages/qa/dogfood/test/per-file-cwd.global-setup.ts:.objectstack(unchanged). It reserves the run's tag asmkdtempSync(join(tmpdir(), 'os-dogfood-run-'))(unchanged expression) and provides its basename.perFileDirPrefix, exported here and imported by the setup module, so the prefix has one spelling), then the reservation.shared-showcaseboot keeps its handles until the run ends. A concurrent run carries another tag and is never touched.packages/qa/dogfood/vitest.config.ts: one comment line ("removes the run's per-file directories at the end").Unchanged from #21914: every file still runs in its own temporary cwd; the cwd is restored in
afterAll; and the throwingafterAllguard onpackages/qa/dogfood/.objectstack/datais untouched.Out of scope here: the rule, the case and
scripts/pm/dispatch-gates.mjsare untouched. Nothing is skipped or recorded as an exception.Evidence
origin/mainfaf8dce4with no change:pnpm check:pm-dispatch-gatesgave✗ dispatch-gates self-test: 1 of 1976 case(s) failed.. The one red case is the line quoted above, atper-file-cwd.setup.ts:60. The battery took 821.9s.2edc5d59:pnpm check:pm-dispatch-gatesgave✓ no mkdtempSync site in this tree takes a base the scan cannot readand✓ dispatch-gates self-test: 1976 cases pass.(780.2s).2edc5d59, andscripts/ablation-replace.mjs(wrap mode) replaced the site withprocess.chdir(mkdtempSync(join(inject('dogfoodRunTag'), 'file-')));. The anchor went from 1 to 0, and the blob fromdc1d3de3to51346b9f.✗ dispatch-gates self-test: 1 of 1976 case(s) failed., and the red case was exactly... UNRESOLVED: packages/qa/dogfood/test/per-file-cwd.setup.ts:73 (a base this scan cannot read: inject('dogfoodRunTag')).dc1d3de3),git diff HEADis empty, andgit status --porcelainis empty. The second worktree was then removed.2edc5d59:pnpm --filter @objectstack/dogfood test:Test Files 208 passed | 1 skipped (209),Tests 1606 passed | 9 skipped (1615). Afterwardspackages/qa/dogfood/.objectstackdoes not exist. Zero/tmp/os-dogfood-run-*entries before the run and zero after it.scripts/ablation-replace.mjs: the centralprocess.chdir(...)was dropped (blobdc1d3de3toad6684f2) and two files were run,showcase-external-autoconnectandshowcase-search. Result:Test Files 2 failed (2). Both failed in the guard:.../packages/qa/dogfood/.objectstack/data exists after this test file ran. Entries: showcase_external.db(plus-shm/-walon the shared-showcase file).git diff HEADempty). The restored leg gave2 passed, exit 0, and left nothing in the package dir. Zero temp entries were left after each leg, the red one included.dispatch-gates --commandsover the 3 changed paths derived 47 families. All 47 ran at2edc5d59, anddispatch-gates --ranreported47 derived famil(ies) accounted for — 47 run, 0 NOT-MEASURED.check:dual-build-cjs-loadsfirst exited 3 (prerequisite:packages/apps/studio/distmissing), then exited 0 after building it.pnpm lintexits 0, andpnpm --filter @objectstack/dogfood typecheckis green.check:pm-dispatch-gatesis path-scoped, so this PR's CI may not schedule it, which is how the red reachedmain. It was run here in full at the head, as above.Acceptance notes
packages/qa/dogfood/vitest.config.ts, in theisolatedproject's alias block (about line 295). This PR changes one comment line near line 146. The hunks do not overlap.main: test(dogfood): every test file runs in its own temporary working directory #21919's own CI did not schedulecheck:pm-dispatch-gates, because that battery is path-scoped and the battery's live-tree half reads every tracked source. This is noted, not filed: the scoping is the merge-group ruling thatlint.ymldocuments, and the hourly full run caught the red as designed.Generated by Claude Code