Skip to content

Commit 322ff1e

Browse files
docs(spec): RuntimeAuthoringIssueSchema.path states the name-keying rule for every collection-resident write type (#22784)
Fixes #22759 Clause-②: no ## What changed `RuntimeAuthoringIssueSchema.path` (`packages/spec/src/api/protocol.zod.ts`) is the describe of the `path` on a runtime publish-gate finding, on both the 422 `issues[]` and the 2xx `advisories[]`. It named `object` / `permission` / `book` as the only collection-resident write types and called every other write type positional. A `dataset` write's findings have been name-keyed too since #19143. The describe now states the rule instead of a list, per triage's grade `6105640249` under ruling `6104584601` on #22636: - a collection-resident write type is one whose collection the gate's private per-write snapshot fills with the tenant's other stored items beside the written one, and there the top-level entry is keyed by NAME; - every other write type is the sole member of its own collection in that snapshot, so its `[0]` stays positional; - the members are derived, not listed. The text names where they are derived: `@objectstack/lint`'s runtime gate, `NAME_KEYED_STACK_KEYS`, the collections it carries as resolution context that a gated write type also lands in. There is no import of lint into spec. The only members the text names are two examples marked `for example` (`object`, `dataset`). The true sentences are kept: the submitted-body framing, the empty path, and nested positions staying positional. One sentence is added because "never by an array index" was not true without it: `nameKeyFindingPath` keeps the index when the entry's name cannot be spliced into a dotted path. Describe text only. No schema shape, accepted value, default or type moves. `content/docs/references/api/protocol.mdx` is regenerated by `gen:docs`, the one artifact `check:generated` proved stale. The changeset `.changeset/22759-runtime-authoring-issue-path-rule.md` is a `patch` for `@objectstack/spec`. ## Readings (at `origin/main` `e84aeb36ce`, the merge base; origin/main has not moved since) **The old text.** `protocol.zod.ts:560`–`:571`: "For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME … Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`)". **The derived set.** `packages/lint/src/runtime-gate.ts:905` derives `NAME_KEYED_STACK_KEYS` = `deriveNameKeyedStackKeys(CONTEXT_STACK_KEYS, WRITTEN_STACK_KEYS)` (`:796`, `:550`, `:845`). At `e84aeb36ce` it is `objects`, `permissions`, `books`, `datasets`. The write types that map into those keys (`TYPE_TO_STACK_KEY`, `:94`; `dataset: 'datasets'` at `:146`) are `object`, `permission`, `book` and `dataset`. - `page` is NOT in the set today. The `packages/lint` changelog (`CHANGELOG.md:5287`) records `pages` joining it, but it left again when `pages` left the context with `validateViewPageRefs`. `runtime-gate.derived-name-keys.test.ts:53`–`:68` pins `['objects', 'permissions', 'books', 'datasets']`, and its comment records the `pages` exit. - Pins: `runtime-gate.dataset-writes.test.ts:310` expects `datasets.acme_invoice_metrics.dimensions[0].field`, and `:313` refuses `datasets[N]`. `runtime-gate.derived-name-keys.test.ts:85` asks, for each context collection, that it is name-keyed exactly when a write type maps into it. Both files ran green here: 2 files, 29 tests. **Is "every other write type is the sole member of its own collection" still true?** Yes, for all 17 gated write types. They are the union of `runtimeTypes` over `AUTHORING_RULES`. Measured by driving `buildRuntimeWriteSnapshots` and `nameKeyFindingPath` from `packages/lint/src`, with every context collection filled: | write type | stack key | members in the candidate | wire spelling | |:--|:--|:--|:--| | `object` | `objects` | 3 | name-keyed | | `permission` | `permissions` | 2 | name-keyed | | `book` | `books` | 2 | name-keyed | | `dataset` | `datasets` | 2 | name-keyed | | `action`, `app`, `dashboard`, `datasource`, `email_template`, `flow`, `hook`, `mapping`, `page`, `position`, `report`, `seed` (key `data`), `view` | own key | 1 | positional | `agent` is mapped in `TYPE_TO_STACK_KEY` but no rule declares it in `runtimeTypes`, so the gate dispatches nothing for it. Its snapshot collection would also hold one member. ## The family (Zone 2 item 4) `git grep` over `packages/spec` and `content/docs` for the closed list (`object` / `permission` / `book`), the "sole member" sentence, "collection-resident", "trivially stable" and the `objects.acme_invoice` example: - **In `packages/spec`, edited:** one hit, this describe (`protocol.zod.ts:560`–`:571`). - **In `packages/spec`, not edited:** `packages/spec/CHANGELOG.md:31982`, the released entry for commit def0d3e. It is release-owned and accurate about what shipped then. - **Generated:** `content/docs/references/api/protocol.mdx`. The describe appears three times (`:2371`, `:2597`, `:2651`), all regenerated. - **Outside `packages/spec`, reported only, nothing wrong:** - `packages/lint/src/runtime-gate.ts:612`–`:617` already names `dataset` beside the other three. - `packages/lint/src/validate-security-posture.runtime-surface.test.ts:168` is a historical note about the card that declared those three types for that rule block. - `authoring-rules.ts:202`, `data-model-rules.ts:66` and `scripts/bench/runtime-publish-gate.bench.mts:495` use `objects.…` as an example, not a list. ## The pin: not added No spec test holds the describe free of a closed list. The new text carries no enumeration whose membership can drift. Its two members are marked examples, and both stay true while `objects` and `datasets` are context collections a write lands in. The set itself is already pinned where it is derived (`runtime-gate.derived-name-keys.test.ts`). A spec-side test could only assert that one particular string is absent, which guards against re-adding exactly this list and nothing else. A cross-package pin would have to enumerate the members, which is the closed list the ruling refuses. ## Verification (head `498abe794e`) - `pnpm --filter @objectstack/spec build`: exit 0. Then `check:generated` named `check:docs` stale. `gen:docs` rewrote only `api/protocol.mdx` (+3/−3). - `pnpm --filter @objectstack/spec check:generated`: "✓ All 14 generated artifacts are up to date". `check:docs`: "✅ 225 generated files in sync with packages/spec". - `vitest run --project local` on `@objectstack/spec`: 645 files passed, 19222 tests passed, 1 todo. - `pnpm --filter @objectstack/spec typecheck`: exit 0. `check:test-typecheck` OK. - `dispatch-gates --commands --repo objectstack-ai/objectstack` (3 paths) printed 103 commands. The full tally is in the report on the card. - Not run: `check:dual-build-cjs-loads`, by dispatch (no whole-workspace build). - NOT MEASURED, prerequisite builds outside this diff's closure, left to CI: - `check:skill-examples` needs `client-react` built; - `check:lean-entry-closure` needs `objectql` built. ## Acceptance notes - At-tier review: the accept set does not move. The diff touches one `.describe()` string, its generated rendering and a changeset. No governed surface is touched. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 680a86b commit 322ff1e

3 files changed

Lines changed: 35 additions & 12 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `RuntimeAuthoringIssueSchema.path` states the name-keying rule for every collection-resident write type, so a `dataset` write's name-keyed paths are no longer described as positional
6+
7+
Clause-②: no
8+
9+
The description of `path` on a runtime publish-gate finding (the 422 `issues[]` and the 2xx `advisories[]`) named `object`, `permission` and `book` as the only write types whose top-level collection entry is keyed by name, and called every other write type positional. A `dataset` write's findings have been name-keyed too (for example `datasets.acme_invoice_metrics.dimensions[0].field`), so a consumer that followed the text would have expected `datasets[N]` and never received it.
10+
11+
The description now states the rule instead of a list: a write type is collection-resident when the gate's per-write snapshot holds the tenant's other stored items of that type beside the written one, and there the top-level entry is keyed by name. Every other write type is the only member of its collection in that snapshot, so its `[0]` stays positional. The set is derived inside `@objectstack/lint`'s runtime gate, and the description names that derivation rather than copying its members. It also states the fallback the gate already applies: an entry whose name cannot be spliced into a dotted path keeps its index.
12+
13+
Description text only: the schema's shape, accepted values and types are unchanged. The generated `content/docs/references/api/protocol.mdx` is regenerated.

‎content/docs/references/api/protocol.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2368,7 +2368,7 @@ Installed package with runtime lifecycle state
23682368
| Property | Type | Required | Description |
23692369
| :--- | :--- | :--- | :--- |
23702370
| **rule** | `string` | ✅ | Stable diagnostic rule id (`flow-multi-write-unfiltered`, `approval-expression-invalid`, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
2371-
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`), as do nested positions inside one named item (`objects.acme_invoice.indexes[1]`), which index the author's own document. |
2371+
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example `objects.acme_invoice.sharingModel` (an `object` write) or `datasets.acme_invoice_metrics.dimensions[0].field` (a `dataset` write). Every other write type is the sole member of its own collection in that snapshot, so its `[0]` IS the written item and stays positional (for example `flows[0]...`). Which write types are collection-resident is derived, never listed: `@objectstack/lint`'s runtime gate computes the set (`NAME_KEYED_STACK_KEYS`) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (`objects.acme_invoice.indexes[1]`): they index the author's own document. |
23722372
| **where** | `string` | ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
23732373
| **message** | `string` | ✅ | What is wrong, in the rule author's own words. |
23742374
| **hint** | `string` | ✅ | How to fix it. |
@@ -2594,7 +2594,7 @@ Installed package with runtime lifecycle state
25942594
| Property | Type | Required | Description |
25952595
| :--- | :--- | :--- | :--- |
25962596
| **rule** | `string` | ✅ | Stable diagnostic rule id (`flow-multi-write-unfiltered`, `approval-expression-invalid`, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
2597-
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`), as do nested positions inside one named item (`objects.acme_invoice.indexes[1]`), which index the author's own document. |
2597+
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example `objects.acme_invoice.sharingModel` (an `object` write) or `datasets.acme_invoice_metrics.dimensions[0].field` (a `dataset` write). Every other write type is the sole member of its own collection in that snapshot, so its `[0]` IS the written item and stays positional (for example `flows[0]...`). Which write types are collection-resident is derived, never listed: `@objectstack/lint`'s runtime gate computes the set (`NAME_KEYED_STACK_KEYS`) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (`objects.acme_invoice.indexes[1]`): they index the author's own document. |
25982598
| **where** | `string` | ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
25992599
| **message** | `string` | ✅ | What is wrong, in the rule author's own words. |
26002600
| **hint** | `string` | ✅ | How to fix it. |
@@ -2648,7 +2648,7 @@ Installed package with runtime lifecycle state
26482648
| Property | Type | Required | Description |
26492649
| :--- | :--- | :--- | :--- |
26502650
| **rule** | `string` | ✅ | Stable diagnostic rule id (`flow-multi-write-unfiltered`, `approval-expression-invalid`, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
2651-
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`), as do nested positions inside one named item (`objects.acme_invoice.indexes[1]`), which index the author's own document. |
2651+
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example `objects.acme_invoice.sharingModel` (an `object` write) or `datasets.acme_invoice_metrics.dimensions[0].field` (a `dataset` write). Every other write type is the sole member of its own collection in that snapshot, so its `[0]` IS the written item and stays positional (for example `flows[0]...`). Which write types are collection-resident is derived, never listed: `@objectstack/lint`'s runtime gate computes the set (`NAME_KEYED_STACK_KEYS`) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (`objects.acme_invoice.indexes[1]`): they index the author's own document. |
26522652
| **where** | `string` | ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
26532653
| **message** | `string` | ✅ | What is wrong, in the rule author's own words. |
26542654
| **hint** | `string` | ✅ | How to fix it. |

‎packages/spec/src/api/protocol.zod.ts‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -560,15 +560,25 @@ export const RuntimeAuthoringIssueSchema = lazySchema(() => z.object({
560560
path: z.string().describe(
561561
'Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), '
562562
+ 'so an editor can jump to the offending key. May be empty when the '
563-
+ 'finding is about the item as a whole. For the collection-resident '
564-
+ 'write types (`object` / `permission` / `book`) the TOP-LEVEL collection '
565-
+ 'entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by '
566-
+ 'an array index — the gate evaluates against a private per-write '
567-
+ 'snapshot whose indexes no caller can resolve. Every other '
568-
+ 'write type is the sole member of its own collection, so its `[0]` is '
569-
+ 'trivially stable and stays positional (`flows[0]...`), as do nested '
570-
+ 'positions inside one named item (`objects.acme_invoice.indexes[1]`), '
571-
+ 'which index the author\'s own document.',
563+
+ 'finding is about the item as a whole. The TOP-LEVEL collection entry '
564+
+ 'follows one rule. The gate evaluates against a private per-write '
565+
+ 'snapshot whose indexes no caller can resolve; a collection-resident '
566+
+ 'write type is one whose collection that snapshot fills with the '
567+
+ 'tenant\'s other stored items beside the written one, and there the '
568+
+ 'entry is keyed by NAME, never by an array index — for example '
569+
+ '`objects.acme_invoice.sharingModel` (an `object` write) or '
570+
+ '`datasets.acme_invoice_metrics.dimensions[0].field` (a `dataset` '
571+
+ 'write). Every other write type is the sole member of its own '
572+
+ 'collection in that snapshot, so its `[0]` IS the written item and '
573+
+ 'stays positional (for example `flows[0]...`). Which write types are '
574+
+ 'collection-resident is derived, never listed: `@objectstack/lint`\'s '
575+
+ 'runtime gate computes the set (`NAME_KEYED_STACK_KEYS`) as the '
576+
+ 'collections it carries as resolution context that a gated write type '
577+
+ 'also lands in, so it changes with the gate, not with this text. An '
578+
+ 'entry whose name cannot be spliced into a dotted path keeps its index. '
579+
+ 'Nested positions inside one named item stay positional '
580+
+ '(`objects.acme_invoice.indexes[1]`): they index the author\'s own '
581+
+ 'document.',
572582
),
573583
where: z.string().describe(
574584
'Human-readable location — `flow "leave_approval" · node "approve"`. Prose '

0 commit comments

Comments
 (0)