Skip to content

Commit 2abc0d3

Browse files
objectstack-fleet[bot]claudemig-builds
authored
docs(spec): the guardOf docblock and its test title state the has() rule that holds now (#25367)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoAbFvsoZdKGPS2souyWMqfZ)_ Fixes #25282 Clause-②: no Metadata-protocol: none ## What changes Two prose edits in `packages/spec/src/automation/flow-value-slot-template.ts` and its test. No behaviour change. - The `guardOf` docblock said `has()` refuses an index anywhere in its argument (`has(a[0].b)`, `has(vars["$x"].y)`). Since PR #25275 that is no longer true: `has()` refuses an index only as the **last** step of its argument, and an index before it is a presence test. The docblock now says that, and gives the reason the guard still skips a path with a numeric segment, a `$`-named head or a keyword segment: the guard tests every step of the path, each of those segments is read by index, so one of the guard's tests would end on that index. - The test title "an index anywhere in the path leaves it unguarded — `has()` refuses one at run time" now reads "an index in the path leaves it unguarded — a step-at-a-time guard would end a `has()` on it, which `has()` refuses at run time". The assertions are unchanged. ## The rule, measured Through `celEngine.evaluate` in `@objectstack/formula` at this head (a throwaway probe test, deleted after the run), with `vars = { rows: [{ name: 'a' }], $x: { y: 1 }, record: { in: 1 } }`: | source | answer | |---|---| | `has(vars.rows[0].name)` | `ok: true`, `true` | | `has(vars.rows[0].nope)` | `ok: true`, `false` | | `has(vars["$x"].y)` | `ok: true`, `true` | | `has(vars.rows[0])` | refused, `has() invalid argument` | | `has(vars["$x"])` | refused, `has() invalid argument` | | `has(vars.record["in"])` | refused, `has() invalid argument` | | `has(vars.rows) && has(vars.rows[0]) && has(vars.rows[0].name)` (the guard's shape) | refused, `has() invalid argument` | So the docblock's old examples are now accepted, and the guard the function would build for such a path still faults on its middle step. The behaviour (no guard) stays right; only the stated reason changed. ## Changeset `skip-changeset`. The file is `.ts`, not one of the `src/**/*.zod.ts` sources `@objectstack/spec` ships in `files[]`, and `guardOf` is module-private. Measured on the built `dist/` before the edit: the old docblock sentence ("takes a field selection over names alone") has 0 hits, while the same file's `ABSENT_SENTENCE` text (positive control) is present in `dist/automation/index.js`. The comment does not ship. ## Tests and gates (head d3abc1c) - `pnpm --filter @objectstack/spec build`, then `vitest run src/automation/flow-value-slot-template.test.ts` (Test Files 1 passed, Tests 172 passed), then `pnpm --filter @objectstack/spec typecheck`: lock VERDICT command-exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 79 commands. All were run with exit codes captured before any pipe; `--ran` reconciles: 79 derived, 78 run, 1 NOT-MEASURED, 0 UNRUN. All 78 run exited 0. Three first exited 3 on a missing prerequisite and were 0 on a rerun: `check-plugin-teardown-shape --self-test` (after fetching its pinned fixture commit), `@objectstack/lint check:doc-formula-expressions` (after building formula and lint) and `check:lean-entry-closure` (after building objectql). NOT MEASURED: `check:dual-build-cjs-loads`, reason: it needs every package's `dist/`; left to CI. - Dogfood: n/a (no `!` in the title, no `(narrowing)` arm). ## Acceptance notes None. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NxRRuV3TrQJLgFHt1pL6JA --- _Generated by [Claude Code](https://claude.ai/code/session_01NxRRuV3TrQJLgFHt1pL6JA)_ Credit: mig-builds proposed the same fix in fork PR #25284, closed in favour of this PR per ruling 6358845679. Co-authored-by: mig-builds <54319294+mig-builds@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: mig-builds <54319294+mig-builds@users.noreply.github.com>
1 parent b22ab8d commit 2abc0d3

2 files changed

Lines changed: 7 additions & 5 deletions

File tree

‎packages/spec/src/automation/flow-value-slot-template.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ describe('a head CEL claims is read through `vars`, the route a `$`-named head t
198198
expect(refusalOf('{record.true}')).not.toContain('the guarded form');
199199
});
200200

201-
it('an index anywhere in the path leaves it unguarded — `has()` refuses one at run time', () => {
201+
it('an index in the path leaves it unguarded — a step-at-a-time guard would end a `has()` on it, which `has()` refuses at run time', () => {
202202
expect(refusalOf('{rows.0.name}')).toContain("source: 'rows[0].name' }");
203203
expect(refusalOf('{rows.0.name}')).not.toContain('the guarded form');
204204
});

‎packages/spec/src/automation/flow-value-slot-template.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,12 @@ type Write = (source: string) => string;
315315

316316
/**
317317
* A `has()` guard for a path of keys — the absent-variable and absent-key
318-
* remedy. `has()` takes a field selection over names alone: an index anywhere
319-
* in its argument (`has(a[0].b)`, `has(vars["$x"].y)`) is refused when it
320-
* runs, so a path with a numeric segment, a `$`-named head or a keyword
321-
* segment gets no guard.
318+
* remedy. `has()` refuses an index as the last step of its argument
319+
* (`has(a[0])`, `has(vars["$x"])`) when it runs; an index before the last
320+
* step is a presence test like any other (`has(a[0].b)`). The guard tests
321+
* every step of the path, and a numeric segment, a `$`-named head or a keyword
322+
* segment is read by index, so one of its tests would end on that index: such
323+
* a path gets no guard.
322324
*
323325
* The guard tests one step at a time, from `vars`, which holds only the
324326
* variables the run has bound: `has(vars.source) && has(vars.source.id) ?

0 commit comments

Comments
 (0)