Skip to content

Commit a2aadab

Browse files
feat(spec)!: FlowSchema refuses a create_record, update_record or delete_record node whose static objectName is a stored-metadata table, with the runtime's prescription (#21654) (#21687)
Fixes #21654 Clause-②: yes (narrowing) The save-time half of #21624, route A as ruled (triage `5972908953`, the `domain:services` seat's answer `5974847898`). #21624 remains open until both halves have landed; its seat owns that card. The run-time half is PR #21649 (`f40bb3217f`). ## What changes - **The refusal.** `flowNodeConfigRefusals` (`packages/spec/src/automation/flow-node-config-refusals.ts`), the one judge `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share, gains a third arm beside the executor-contract arm and the decision arm. A `create_record`, `update_record` or `delete_record` node whose `config.objectName` is a **static string** naming a stored-metadata table, judged by `isStoredMetadataBodyObject` by exact name, is refused at `nodes.N.config.objectName` (any depth, ADR-0031 regions included). The message names the node type and the table, uses the run-time refusal's verbs (create a record in / update / delete from) and ends on the run-time prescription. - **A new closed-set code.** `write-node-stored-metadata-target` joins `FLOW_SLOT_REFUSAL_CODES` with `params: { nodeType, objectName }` (`flow-node-expression-paths.ts`: the code table the judge's return type requires). - **Out of reach on purpose.** A dynamic `objectName` (a `{token}` template, an expression envelope) is not judged at save: the run judges the name it hands the data engine. A `get_record` node is not judged by this arm: a read is not a write. - **One prescription sentence.** The hook refusal's private `STORED_METADATA_BODY_PRESCRIPTION` moved, byte for byte, to the import-free leaf `kernel/stored-metadata-body-objects.ts` as an export. `data/hook.zod.ts` and the new flow arm both import it, and `kernel/metadata-type-redaction.ts` re-exports it beside the family set, so `@objectstack/spec/kernel` publishes it (`api-surface/kernel.json`, `export-origins/kernel.json` regenerated). In `hook.zod.ts` only the import line, the constant and the three comment lines describing it changed. The `handler` doc region that #21604 holds is untouched. - **The ADR-0087 kit.** The D3 semantic entry `entries/semantic/18.flow-write-node-stored-metadata-target-refused.ts` (its prescription names the metadata protocol), its step-18 rationale fragment at order 74, the next free order on `main` at `7d0781482d` (71 to 73 are taken), and the regenerated `registry.ts` regions. No tombstone (no key is removed) and no D2 conversion (a refused node carries no intent a rewrite could keep). One BREAKING `@objectstack/spec` changeset with the `registered` disposition and the `Clause-②` line. ## Wording, set against the run-time refusal - Run time (`service-automation`, `storedMetadataWriteRefusal`): "create_record: refusing to create a record in 'sys_metadata': it holds stored metadata, and a flow may not write it directly, so the write was not run." Then the prescription. - Save time (this PR): "This `create_record` node's `objectName` is 'sys_metadata', so it would create a record in a table that holds stored metadata, and a flow may not write it directly: every run that reaches the node refuses it before anything is written, and re-running changes nothing." Then the prescription. - One residual difference, in the prescription itself: the run-time node spells "Elevation (`runAs: 'system'`)", while the shared constant (the hook refusal's, now exported) spells "Elevation (`runAs`, a system context)". Both say elevation does not change the outcome. Importing the exported sentence in `service-automation` makes them identical; that is a named follow-up, not done here. ## Census, before any edit At `417443eb27`: **0** write nodes aimed at either family table outside tests, across `packages/**`, `examples/**`, `skills/**`, `content/docs/**` and `docs/**` (229 write-node declarations). The only hits are the run-time half's own pins, `write-nodes-stored-metadata-family-refusal.integration.test.ts` (a static target at lines 270 and 271, and a parameterized one through `configFor`). Those pins are a test of the run-time refusal, not a writer. This PR re-expresses them (next section). The positive control fired: the same windowed search finds those pins and this PR's new tests. ## The run-time pins, re-expressed (claim revision `5977090032`, open question 1 answered A) The run-time half's pins (`packages/services/service-automation/src/builtin/write-nodes-stored-metadata-family-refusal.integration.test.ts`, from PR #21649) registered static family-target flows through `registerFlow` in order to run them. `registerFlow` parses first, so the save-time refusal turned 12 of their 17 cases red. The PM revised the claim to add this one file, test-only (claim revision `5977090032`; cross-lane declaration `5977094605` on #21118). No other `service-automation` file changes, and the engine, `crud-nodes.ts` and the runtime are untouched. **The edit.** One new harness step, `registerForRun(def)`, replaces the two direct `registerFlow` calls (`runWatched` and `codeAsAFlowReadsIt`). - A definition with no static family target registers exactly as before. That covers the variable-target cases and the ordinary-object controls. - A definition carrying one is first judged at save. `registerFlow` must throw, with exactly one `custom` issue per family write node at its path: `nodes.1.config.objectName`, or `nodes.1.config.try.nodes.0.config.objectName` for the `try_catch` region flow. Each issue's message must name the metadata protocol. `getFlow(name)` must answer `null` (nothing was registered), and the target table's snapshot must be unchanged. - It then reaches the run-time guard with a definition the parse never judged. The same definition is registered aimed at a stand-in object (`pin_stand_in_target`, which does not exist). The family table is then put back on the parsed definition `registerFlow` returned. `getFlow(name)` must answer the family table at every original path before the run starts. - Every existing run-time assertion is unchanged, byte for byte: the run fails, nothing downstream runs, no engine write reaches the family, the table is unchanged, the flow reads `PERMISSION_DENIED`, a fault edge does not route, and all of it under both identities and both compositions. No assertion was removed or loosened. **The engine behaviour the route relies on, and why it is stable.** Two facts, neither touched by this PR, which changes no `service-automation` source file: - `AutomationEngine.registerFlow` stores the parsed definition it returns, by reference: `this.flows.set(name, parsed)`, then `return parsed`. - `execute` runs `this.flows.get(name)` as stored and never re-parses it. The engine documents `this.flows` as holding only `FlowSchema.parse` output. Both are read back rather than assumed. The `getFlow(name)` assertion above must see the family table at every retargeted path. If the engine ever copied, froze or re-parsed the stored definition, the retarget would fail loudly: a copy or a re-parse leaves the stand-in in place, so the read-back goes red and the run fails with not-found instead of the family refusal; a frozen definition throws on the assignment itself. The route cannot pass silently. **Evidence** (at `a9d2d5d453`): - The pins file: `Tests 17 passed (17)`. - The full `service-automation` suite: `Test Files 170 passed (170)`, `Tests 2098 passed (2098)`, 0 failed. `pnpm --filter @objectstack/service-automation typecheck` exits 0, and `check:test-typecheck` is OK. eslint on the file gives 0 errors and 0 warnings. - **Run-time guard ablation** (`crud-nodes.ts`, predicted 12 red / 5 green). `scripts/ablation-replace.mjs` pointed `storedMetadataWriteRefusal`'s family check at a name nothing matches. The anchor went 1 to 0 and the blob `b9bb559a0c` to `ffe005789b`. No build was needed: the pins reach it through relative `src` imports. Observed `Tests 12 failed | 5 passed (17)`. Every first failure is a run-time assertion ("the run must fail: expected true to be false"), and 0 are save-time assertions. So the save step passed, and the run then wrote, which the pins catch. Restore: blob after restore `b9bb559a0c` equals HEAD's, and `git diff HEAD` is empty; the script's own trap re-confirmed it with 0 diff lines. The first attempt used a one-line anchor that hits twice in the file (once in the `get_record` read refusal). The tool refused it (`ANCHOR AMBIGUOUS`, exit 3) and wrote nothing. The rerun used a longer anchor unique to the write refusal. - **Save-time arm ablation** (spec, predicted 12 red / 5 green). The arm's push became a `globalThis` marker assignment. The anchor went 1 to 0 and the blob `82a173479e` to `ee1614818e`. `@objectstack/spec` was rebuilt, and `ablation-dist-preflight.mjs` proved the marker reached `packages/spec/dist`, because `service-automation` resolves `@objectstack/spec` through `dist`. Observed `Tests 12 failed | 5 passed (17)`. Every failure is the new save-time assertion ("registerFlow must refuse a static family target at save: expected undefined to be defined"). Restore: blob `82a173479e` equals HEAD's and `git diff HEAD` is empty. The rebuilt `dist` was proven free of the marker (`--absent`, exit 0), and the rerun gave `17 passed`. ## Doors, tested and probed (all at `f7d1216a8f` unless noted) - `FlowSchema.parse`: refused at `nodes.1.config.objectName` for each write node and each family table, and at `nodes.1.config.try.nodes.0.config.objectName` inside a region. The message is the judge's own and ends on the leaf's prescription. - `defineStack`: `STACK_SCHEMA_INVALID` / 422 at `flows.1.nodes.1.config.objectName`. `ObjectStackDefinitionSchema` (the stack parse `objectstack validate` runs) refuses at `flows.0.nodes.1.config.objectName`. The registered `flow` type schema (the metadata save door's) and an artifact's parse refuse too. - `objectstack validate`, the real CLI on a temporary fixture (deleted afterwards): with a `create_record` node on `sys_metadata` it gave exit 1, `"code": "STACK_SCHEMA_INVALID"`, and an error naming `flows.0.nodes.1.config.objectName` and the prescription. The same stack aimed at an ordinary object gave exit 0, `"valid": true`. - `registerFlow`: refuses, because it parses first, and a committed pin now says so. The re-expressed run-time pins assert the throw, its path and the unchanged table for every static case (above). The throw comes from `FlowSchema.parse` inside `canonicalizeStoredFlow` (`engine.ts:4346`), which `registerFlow` (`engine.ts:4368`) calls. - Lit controls: each write node on an ordinary object passes. A dynamic `objectName` (`{record.target}`, `{target}`, and the envelope `{ dialect: 'cel', source: ... }`) passes at save, and the judge returns nothing for it. `get_record` on either family table passes. The refused set equals the predicate's by exact name (`SYS_METADATA`, `sys_metadata_draft`, ` sys_metadata` with a leading space, `sys_meta`, `metadata` all pass). ## Ablation of the spec pins (one-shot, not kept) At `f7d1216a8f`, with the implementation committed, `scripts/ablation-replace.mjs` replaced the arm's push line with a no-op plus a marker. On disk the anchor went 1 to 0, the marker 0 to 1, and the blob `82a173479e` to `a832964b45`. No dist rebuild was needed: the tests reach the judge through relative `src` imports. Predicted 17 red / 29 green over the two pin files; observed `Tests 17 failed | 29 passed (46)`. Restore: the tool reported blob after restore equal to the HEAD blob (`82a173479e`) and `git diff HEAD` empty. The script's own EXIT/INT/TERM trap, using `git checkout HEAD --` on the absolute path plus a hash compare, re-confirmed it with 0 diff lines. The rerun gave 46 passed. An earlier run at `8f5adb6ad6` (before the stack-parse door test existed) read 16 / 29, as predicted. ## Verification Spec-side readings at `f7d1216a8f`. `packages/spec` has not changed since; round 2 touched only the `service-automation` test file. - `@objectstack/spec`, full local project: `Test Files 611 passed (611)`, `Tests 18141 passed | 1 todo`. - `pnpm --filter @objectstack/spec typecheck`: exit 0. `check:test-typecheck` is OK, and `tsc -p tsconfig.test.json --listFiles` lists both edited test files. - `check:generated`: all 15 artifacts up to date, against a dist the run built. - `@objectstack/lint` (the judge's other caller, `validateStackExpressions`): `Test Files 119 passed`, `Tests 5627 passed`. - Lint, a proven narrowing (`pnpm lint` itself belongs to CI). eslint's config lints 9 of the 12 round-1 paths: 0 errors and 0 warnings from `--format json --no-inline-config`. It ignores the 3 that are `.md` / `.json`. The config sets no `parserOptions.project` and enables no typed rules, so this diff cannot move an untouched file's verdict. Readings at `a9d2d5d453` (the head): - `@objectstack/service-automation`: `Test Files 170 passed (170)`, `Tests 2098 passed (2098)`. Typecheck exits 0. - eslint on the round-2 file: 0 errors and 0 warnings. The control-byte scan over the 13 changed paths found none. - `dispatch-gates --commands` (no paths), derived fresh at this head: 94 families, the round-1 92 plus `check-tenant-audit-census` and its self-test. All 94 were run fresh on this head, each exited 0, and `--ran` with exit codes recorded reads 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN. `origin/main` was merged twice through `scripts/pm/os-regen-merge.sh` (no rebase): once for #21668 / #21673, which landed `registry.ts` changes, and once for `7d0781482d`. Each merge regenerated and re-checked the artefacts afterwards. The sibling entries are all present (each id counted the same on `origin/main` and here), and this branch's `registry.ts` delta against `main` is +61 / -0. ## Acceptance notes - The comment above the `flowNodeConfigRefusals` walk in `packages/spec/src/automation/flow.zod.ts` still says "Two arms" and lists two. With this PR there are three. The file is outside the claim's surface, so it is noted, not edited. The judge's own docblock in `flow-node-config-refusals.ts` states all three arms. - `packages/lint/src/validate-expressions.ts` describes its call into the judge as "a key its contract requires, left out, and a `decision` branch list it cannot read". That list is now incomplete, not false: the call also emits the new refusal, as `error`. - Follow-up, not done here: `service-automation`'s `storedMetadataWriteRefusal` and the runtime body boundary's private `PRESCRIPTION` can import `STORED_METADATA_BODY_PRESCRIPTION` from `@objectstack/spec/kernel`, which makes the sentence one. Body refreshed 2026-10-04T06:42Z (round 2: the run-time pins re-expressed). The docs-drift advisory on this PR was read: it is advisory, and a spot-check of the hand-written pages naming `sys_metadata` with flows found none that this change falsifies. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fe10172 commit a2aadab

13 files changed

Lines changed: 658 additions & 23 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
A flow `create_record`, `update_record` or `delete_record` node whose `objectName` is `sys_metadata` or `sys_metadata_history` is refused at parse, with the runtime's prescription: change metadata through the metadata API.
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered flow-write-node-stored-metadata-target-refused -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
**Why.** App-authored work may not write the two stored-metadata tables: the metadata protocol is their only writer, where a change is validated and its provenance is recorded, and a flow is app-authored automation. The runtime already enforces that at the node: the three write nodes refuse such a target before they resolve a filter, compute a field or call the data engine, under every run identity. But `FlowSchema` still accepted the flow, so `objectstack validate` passed it, the metadata save door answered 200 for it and `registerFlow` registered it, and the author learned otherwise only at its first run.
14+
15+
**What is refused.** A `create_record`, `update_record` or `delete_record` node, at any depth including an ADR-0031 region body, whose `config.objectName` is a string naming `sys_metadata` or `sys_metadata_history`. The issue's `code` is `custom`, at `nodes.N.config.objectName`, and its message names the node type and the table and ends with the runtime's prescription. The judge is `flowNodeConfigRefusals`, the one `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share, and its membership test is the kernel's own `isStoredMetadataBodyObject`, the predicate the runtime judges by. That covers `FlowSchema`, `defineFlow()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `flows.N.nodes.M.config.objectName`), `os validate`, an artifact's parse, `registerFlow` and the metadata save door (`422 INVALID_METADATA`). The refusal joins the closed flow slot refusal set as `write-node-stored-metadata-target`, with `params: { nodeType, objectName }`.
16+
17+
**What stays accepted, byte for byte.** A `get_record` node on those tables (a read is not a write; the runtime judges its reach at the run), a write node whose `objectName` is dynamic (a `{token}` template or an expression envelope: the parse cannot read it as a name, and the runtime judges the name it hands the data engine), and every write node on any other object.
18+
19+
**One prescription sentence.** `@objectstack/spec/kernel` now exports `STORED_METADATA_BODY_PRESCRIPTION`, the sentence the hook refusal and this flow refusal both end on. It was the hook refusal's private constant, moved unchanged.
20+
21+
## FROM → TO
22+
23+
| you wrote | write instead |
24+
|:--|:--|
25+
| a `create_record` / `update_record` / `delete_record` node with `objectName: 'sys_metadata'` or `objectName: 'sys_metadata_history'` | change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) instead, and delete the node |
26+
| a write node on any other object, a `get_record` node, or a dynamic `objectName` | unchanged |
27+
28+
**The one-line fix: delete the node, or point its `objectName` at the object the flow really means to write, and make the metadata change through the metadata API.** The runtime never ran such a write, so removing it changes nothing a flow does.
29+
30+
**Who is affected, measured.** No authored flow writes either table in this repository's `packages/**`, `examples/**`, `skills/**`, `content/docs/**` or `docs/**` at `417443eb27` (229 write-node declarations); the only hits are the runtime's own tests of its node refusal. Deployed metadata was not measured. Where such a node already sits in a stored flow, the whole flow is refused at registration: at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register.
31+
32+
### The kit
33+
34+
- **The refusal.** A third arm of `flowNodeConfigRefusals` (`automation/flow-node-config-refusals.ts`), beside the executor-contract arm and the decision arm.
35+
- **The ledger.** The D3 semantic entry `flow-write-node-stored-metadata-target-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: a refused node carries no intent a rewrite could keep.

‎packages/services/service-automation/src/builtin/write-nodes-stored-metadata-family-refusal.integration.test.ts‎

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@
3333
* a flow variable leaves the family table unchanged, and the three nodes write
3434
* an ordinary object exactly as before.
3535
*
36+
* [#21654] The save-time half. `FlowSchema` now refuses a write node whose
37+
* STATIC `objectName` names a family table, and `registerFlow` parses first,
38+
* so a flow carrying one is refused before it can run. Every static case here
39+
* therefore asserts that refusal first (`registerFlow` throws, the issue sits at
40+
* the node's `config.objectName`, the table is unchanged), and only then reaches
41+
* the run-time guard, with a definition the parse never judged: see
42+
* {@link registerForRun}. A dynamic target (`{record.target}`) is not judged at
43+
* save and registers as before.
44+
*
3645
* Composition: `ObjectKernel`, `ObjectQLPlugin`, `driver-sql` on
3746
* better-sqlite3 `:memory:` and the real `AutomationServicePlugin`, the stack
3847
* the family read pins boot; the secured composition adds the real
@@ -60,6 +69,49 @@ type RunAs = 'system' | 'user';
6069
const STORED_BODY = JSON.stringify({ name: 'pin_body', label: 'Pin body' });
6170
const BODY_FRAGMENT = '"label":"Pin body"';
6271

72+
/**
73+
* [#21654] The target a static family case is registered under, so that the
74+
* parse lets it through; {@link registerForRun} then puts the family table back
75+
* on the registered definition. No object of this name exists: a definition
76+
* whose retarget did not land fails its run with a not-found error, never with
77+
* the family refusal its case asserts.
78+
*/
79+
const STAND_IN_TARGET = 'pin_stand_in_target';
80+
81+
/** One write node in a flow definition aimed at a family table, and where it sits. */
82+
interface FamilyTarget {
83+
readonly path: string;
84+
readonly object: string;
85+
}
86+
87+
/**
88+
* Every write node in `def`, at any depth (a `try_catch` region's nodes
89+
* included), whose `config.objectName` is `match` — or, with no `match`, is a
90+
* family table by name. Paths in the parse's dotted spelling
91+
* (`nodes.1.config.objectName`).
92+
*/
93+
function writeTargetsIn(def: unknown, match?: string): Array<FamilyTarget & { readonly node: Record<string, unknown> }> {
94+
const found: Array<FamilyTarget & { readonly node: Record<string, unknown> }> = [];
95+
const visit = (value: unknown, path: string[]): void => {
96+
if (Array.isArray(value)) {
97+
value.forEach((item, i) => visit(item, [...path, String(i)]));
98+
return;
99+
}
100+
if (value === null || typeof value !== 'object') return;
101+
const rec = value as Record<string, unknown>;
102+
const config = rec.config as Record<string, unknown> | undefined;
103+
if ((WRITE_NODES as readonly unknown[]).includes(rec.type) && config && typeof config.objectName === 'string') {
104+
const object = config.objectName;
105+
if (match === undefined ? (FAMILY as readonly string[]).includes(object) : object === match) {
106+
found.push({ path: [...path, 'config', 'objectName'].join('.'), object, node: config });
107+
}
108+
}
109+
for (const [key, child] of Object.entries(rec)) visit(child, [...path, key]);
110+
};
111+
visit(def, []);
112+
return found;
113+
}
114+
63115
/** An ordinary object: the non-family control. */
64116
const PLAIN_OBJECT = {
65117
name: 'pin_write_plain',
@@ -169,9 +221,71 @@ function harness(ql: ObjectQL, automation: AutomationEngine) {
169221
return { objectName: object, filter: { id } };
170222
}
171223

224+
/**
225+
* [#21654] Register `def` so that it can RUN. A definition with no static
226+
* family target registers as it always did. One that carries such a target is
227+
* refused by the parse at save, now that `FlowSchema` judges it, so this first
228+
* asserts that refusal — `registerFlow` throws, the issue is a `custom` one at
229+
* each such node's `config.objectName` carrying the metadata-protocol
230+
* prescription, nothing is registered under the name, and the target table is
231+
* unchanged — and only then reaches the run-time guard with a definition the
232+
* parse never judged: the same definition registered with
233+
* {@link STAND_IN_TARGET} in place of each family table, after which the
234+
* family table is put back on the definition the engine holds.
235+
*
236+
* The engine behaviour this leans on, none of which the save-time refusal
237+
* changes: `registerFlow` stores the parsed definition it returns, by
238+
* reference (`this.flows.set(name, parsed)`, then `return parsed`), and
239+
* `execute` runs `this.flows.get(name)` as stored, never re-parsing it. Both
240+
* are read back here rather than assumed: `getFlow(name)` must answer the
241+
* family table at every retargeted path before the run. Were either to stop
242+
* holding — a copy, a freeze, a re-parse — the retarget would fail to land,
243+
* that read-back would go red, and the run would refuse nothing for the family
244+
* reason; a frozen definition throws on the write itself.
245+
*/
246+
async function registerForRun(def: { name: string }): Promise<void> {
247+
const targets = writeTargetsIn(def);
248+
if (targets.length === 0) {
249+
automation.registerFlow(def.name, def as any);
250+
return;
251+
}
252+
253+
// Save time: refused, located at each family target, nothing registered, the table unchanged.
254+
const tables = [...new Set(targets.map((t) => t.object))];
255+
const before = await Promise.all(tables.map((object) => snapshot(object)));
256+
let thrown: { issues?: Array<{ code: string; path: PropertyKey[]; message: string }> } | undefined;
257+
try {
258+
automation.registerFlow(def.name, def as any);
259+
} catch (err) {
260+
thrown = err as typeof thrown;
261+
}
262+
expect(thrown, `${def.name}: registerFlow must refuse a static family target at save`).toBeDefined();
263+
expect(
264+
(thrown!.issues ?? []).map((i) => ({ code: i.code, path: i.path.join('.') })),
265+
`${def.name}: the save-time refusal's issues`,
266+
).toEqual(targets.map((t) => ({ code: 'custom', path: t.path })));
267+
for (const issue of thrown!.issues ?? []) expect(issue.message).toContain('the metadata protocol');
268+
expect(await automation.getFlow(def.name), `${def.name}: a refused flow was registered`).toBeNull();
269+
expect(await Promise.all(tables.map((object) => snapshot(object))), `${def.name}: the save-time refusal changed a table`)
270+
.toEqual(before);
271+
272+
// Run time: a definition the parse never judged — registered aimed at the stand-in, then retargeted.
273+
const standIn = JSON.parse(JSON.stringify(def)) as { name: string };
274+
for (const target of writeTargetsIn(standIn)) target.node.objectName = STAND_IN_TARGET;
275+
const registered = automation.registerFlow(def.name, standIn as any);
276+
const placeholders = writeTargetsIn(registered, STAND_IN_TARGET);
277+
expect(placeholders.map((p) => p.path), `${def.name}: the stand-in sits where the family targets did`)
278+
.toEqual(targets.map((t) => t.path));
279+
placeholders.forEach((placeholder, i) => {
280+
placeholder.node.objectName = targets[i]!.object;
281+
});
282+
expect(writeTargetsIn(await automation.getFlow(def.name)), `${def.name}: the engine holds the retargeted definition`)
283+
.toEqual(targets.map((t) => expect.objectContaining({ path: t.path, object: t.object })));
284+
}
285+
172286
/** Run `def` with the engine's write verbs watched; count the calls aimed at the family. */
173287
async function runWatched(def: { name: string }, trigger: Record<string, unknown>) {
174-
automation.registerFlow(def.name, def as any);
288+
await registerForRun(def);
175289
const insert = vi.spyOn(ql, 'insert');
176290
const update = vi.spyOn(ql, 'update');
177291
const remove = vi.spyOn(ql, 'delete');
@@ -193,7 +307,7 @@ function harness(ql: ObjectQL, automation: AutomationEngine) {
193307
async function codeAsAFlowReadsIt(runAs: RunAs, node: Record<string, unknown>, trigger: Record<string, unknown>) {
194308
const name = `pin_code_${seq++}`;
195309
captured.length = 0;
196-
automation.registerFlow(name, {
310+
await registerForRun({
197311
name, label: name, type: 'autolaunched', runAs,
198312
nodes: [
199313
{ id: 'start', type: 'start', label: 'Start' },
@@ -208,7 +322,7 @@ function harness(ql: ObjectQL, automation: AutomationEngine) {
208322
{ id: 'end', type: 'end', label: 'End' },
209323
],
210324
edges: [{ id: 'e1', source: 'start', target: 'guarded' }, { id: 'e2', source: 'guarded', target: 'end' }],
211-
} as any);
325+
} as { name: string });
212326
await automation.execute(name, { ...trigger } as any);
213327
expect(captured, 'the catch region must have run once').toHaveLength(1);
214328
return captured[0]!;

‎packages/spec/api-surface/kernel.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@
375375
"SEMVER_2_0_0_VERSION_PATTERN (const)",
376376
"STORED_METADATA_BODY_COLUMN (const)",
377377
"STORED_METADATA_BODY_OBJECTS (const)",
378+
"STORED_METADATA_BODY_PRESCRIPTION (const)",
378379
"STORED_METADATA_TYPE_COLUMN (const)",
379380
"SandboxConfig (type)",
380381
"SandboxConfigParsed (type)",

‎packages/spec/export-origins/kernel.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@
373373
"SEMVER_2_0_0_VERSION_PATTERN": "src/kernel/version-grammar.ts#SEMVER_2_0_0_VERSION_PATTERN (const)",
374374
"STORED_METADATA_BODY_COLUMN": "src/kernel/metadata-type-redaction.ts#STORED_METADATA_BODY_COLUMN (const)",
375375
"STORED_METADATA_BODY_OBJECTS": "src/kernel/stored-metadata-body-objects.ts#STORED_METADATA_BODY_OBJECTS (const)",
376+
"STORED_METADATA_BODY_PRESCRIPTION": "src/kernel/stored-metadata-body-objects.ts#STORED_METADATA_BODY_PRESCRIPTION (const)",
376377
"STORED_METADATA_TYPE_COLUMN": "src/kernel/metadata-type-redaction.ts#STORED_METADATA_TYPE_COLUMN (const)",
377378
"SandboxConfig": "src/kernel/plugin-security-advanced.zod.ts#SandboxConfig (type)",
378379
"SandboxConfigParsed": "src/kernel/plugin-security-advanced.zod.ts#SandboxConfigParsed (type)",

0 commit comments

Comments
 (0)