Repository navigation
docs(spec): state what the map renderer actually does with an undeclared key in map - #19102
Merged
Merged
Conversation
…red key in `map` `ListMapConfigSchema`'s docblock claimed the renderer "validates `schema.map` against a local zod schema with exactly these keys, so an extra key here would be dropped there". Re-measured at the `.objectui-sha` pin 53ded82b by executing the pinned declarations: that local schema (`ObjectMapConfigSchema`) is a plain `z.object`, not strict, and `getMapConfig` uses its `safeParse` only to decide whether to `console.warn`, returning a spread of the authored block. What does drop an undeclared key on this block's path is a different instrument altogether -- the hand-listed `FLAT_MAP_CONFIG_KEYS` whitelist in `ListView` / `ObjectView` -- and it drops it silently. The strictness is unchanged. Only the rationale is corrected, and it is restated so it stands on its own: nothing downstream reports an undeclared key, so this parse is the only diagnostic an author ever gets. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
The seven objectui anchors in `ListMapConfigSchema`'s record now quote the line they were read at, so `check:objectui-pin-citations` verifies their CONTENT against objectui at `.objectui-sha` rather than only checking that the sha label matches. ASSERTED_ANCHOR_FLOOR moves 0 -> 7 with its note rewritten, as that ratchet's equality requires. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…tion The docblock ships: `@objectstack/spec`'s `files[]` carries `src/**/*.zod.ts` and the text also lands in `dist/*.d.ts` (measured by grep against the built tree, with a pre-existing phrase as the positive control), so this is published text, not a `skip-changeset` diff. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 18, 2026
os-bill
marked this pull request as ready for review
September 18, 2026 20:00
This was referenced Sep 18, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…nd-grep remedy is prescribed (objectstack-ai#19110) Fixes objectstack-ai#19093 Clause-②: no — the diff is workflow prose plus its pin. No schema, no accept set, no public surface. ## What this lands The Docs Drift Check's fold already carries one unconditional boundary line, and it ends by **prescribing a remedy**: "a rule your change carries has to be re-read by hand in the pages that restate it". That remedy is a name-based search, and this card is about the fact that a name does not identify a key. So the remedy's own failure mode now renders **beside the remedy**, on every run, with the censused names as its payload and the discriminating parse as its fix — plus a byte-exact pin in `scripts/docs-audit/check-drift-comment.mjs`, the same shape the neighbouring unconditional line takes. ## The census — first deliverable, and it chose the site Measured on this branch by walking the liveness ledger's governed types through the same registry and the same shape helpers `check-liveness.mts` uses, and reading each key's `[REMOVED]` marker. **Names, never a count** — on this card a count is structurally not a reading, because the whole defect is that a count cannot tell two identically-named keys apart. A key **tombstoned on one governed type and authorable on another**: | name | tombstoned on | authorable on | |---|---|---| | `active` | `flow` | `agent`, `skill`, `datasource`, `app`, `email_template`, `validation`, `sharing_rule` | | `aria` | `dashboard`, `app` | `action`, `page` | | `joins` | `query` | `analytics_cube` | | `objects` | `app` | `permission`, `translation`, `manifest` | | `template` | `flow` | `page` | | `tools` | `agent` | `skill` | | `version` | `app` | `flow`, `manifest` | So `tools` is **one of seven**, not a singleton — which settles the card's "what is NOT established: how many other keys carry the same collision". **Radius of the instrument.** Top-level keys of the governed types, resolved through `getMetadataTypeSchema()` plus the `SPEC_ONLY_SCHEMAS` overrides, tombstone detected as a `[REMOVED]` description prefix. Two things follow from that radius and both are stated in the landed text: - Drilling the same walk to depth 2 widens the class well past these seven (`timeoutMs` is tombstoned at `flow.nodes.waitEventConfig.timeoutMs` while `flow.nodes.timeoutMs` is authorable — a collision between **siblings on one type**); depth 3 and beyond adds only `query.expand.*` self-recursion, so depth 2 is where the widening stops being new. - **One known target outside the radius, measured:** a key retired through the `.strict()` guidance-map route instead of `retiredKey()` leaves **no tombstone in the walked shape at all**, so this instrument cannot see it in either direction. `tool.category` is retired that way (`TOOL_RETIRED_KEY_GUIDANCE` in `packages/spec/src/ai/tool.zod.ts`) while `AIToolDefinition.category` in `packages/spec/src/contracts/ai-service.ts` stays live and is documented as carrying `action.ai.category` — the identical trap, invisible here. The same route hides a second instance of a name this census already flagged: `tool.active`. ## The discriminating parse, re-derived first-hand The card records that the dual-parse was measured by the reporting dev and **not re-run** by the filing seat. It has now been re-run on this branch, against `SkillSchema` and `AgentSchema` imported from source, with the object literal taken verbatim from `content/docs/ai/agents.mdx:155-162`. - as a **`SkillSchema`**: **PASS**. - as an **`AgentSchema`**: **FAIL**, with an issue at `["tools"]` whose message opens, verbatim: > `agent.tools` was removed in @objectstack/spec 17 — use `skills`. An agent reaches exactly > the tools its surface-compatible skills declare (ADR-0064), so move each reference into a > skill: a platform tool by its registered name, or `action_ACTIONNAME` for one of your own > AI-exposed Actions. This is NOT a rename — there is no key the value moves to: the migration > DELETES the key and emits a notice naming each tool that was listed, and you re-declare each > one in a skill by hand. (One edit to that quotation: the message spells its placeholder with angle brackets, which this repo's body-sanitizer discipline keeps out of a PR body — it is written `action_ACTIONNAME` above. Everything else is byte-for-byte what the parse emitted.) **One correction to the card, from that re-run.** The card says the agent parse fails "with exactly one issue". On the literal **as written on the page** it fails with **two**: `["tools"]` and `["role"]` (`invalid_type`, required and absent — `role` is required on `AgentSchema` and the page's skill block naturally has none). The "exactly one" reading holds only once every `AgentSchema`-required key is supplied, which is presumably what the reporting dev did. Two controls settle it: - **Control A** — literal + `role`, parsed as an agent: **FAIL, exactly 1 issue, at `["tools"]`**. - **Control B** — the same object with `tools` deleted, parsed as an agent: **PASS, 0 issues**. So `tools` is the sole cause, and the direction is the one the card claims. The substance is confirmed; only the issue count was off. **Control C**, on the card's claimed hard discriminator: `SkillSchema` declares `triggerConditions` and `AgentSchema` does not. Also worth recording because it is the mechanism of the whole trap: `AgentSchema` **does** declare `tools` — the tombstone keeps the key in the walked shape, which is exactly why a name-based search finds it there. **Page structure, re-read first-hand:** the page's only `tools:` authoring occurrence is `:161`, inside the `defineSkill({` block opened at `:155`; the four `defineAgent(` blocks at `:256`, `:292`, `:325`, `:361` write zero `tools`. ## Why this site, and why not the other The card offers two candidate sites, marked as input and not a prescription. This PR takes the Docs Drift Check blind-spot text and deliberately does **not** write the liveness ledger's `_note`. 1. **The discriminator has to be on the path the trapped reader is already walking.** The measured instance is a reviewer of PR objectstack-ai#19059 who grepped `content/docs` and read the hit. That reviewer meets the Docs Drift Check comment on the PR, unconditionally, at the moment the hand-grep method is prescribed to them. They do not open a ledger file to decide whether a grep hit is real. 2. **The free half of the ledger site is the wrong half.** A reader alarmed about `agent.tools` opens `agent.json`. `agent.json` is held by open PR objectstack-ai#19059 and is fenced for this card, so the only writable ledger site is `skill.json` — the half that reader has no reason to open. A discriminator there is a warning posted on the door nobody walks through. 3. **The remedy and its failure mode belong in one place.** The existing unconditional line is what sends a reader to grep by hand. Documenting that method's known failure anywhere else splits a prescription from its caveat. 4. `.github/workflows/**` is not a PD-14 governed surface, and — measured — it publishes nothing. `packages/spec/liveness` **is** in `@objectstack/spec`'s `files[]`, so the ledger route would also have shipped this text into the npm tarball and taken a changeset. Per the card's fence, exactly one site is written. ## Class Reviewer/tooling trap, **not class (c)**. An author cannot reach the tombstoned key: the parse door refuses it (the FAIL above **is** that door), and the ADR-0087 `agent-tools-to-skills` conversion deletes it at rehydration. Nothing here is written up as a live authoring trap. ## Verification - `node scripts/docs-audit/check-drift-comment.mjs` :: exit 0 — `✓ check-drift-comment: 66 cases pass across 5 fixture diff(s)`. The new assertion pair runs on all 5 fixture cases, evidenced by the ablations below failing exactly 5 of the 66. - **Ablation, both legs, mutation proven on disk and restored byte-exact** via `scripts/ablation-replace.mjs` (anchor must hit; blob hashes compared against HEAD): - alter one censused name in the rendered line (`joins` → `JOINS`) — mutation landed (`anchor 1 -> 0, blob f050c7c -> 79f4b52e58d0`), pin **RED** on all 5 cases, restore verified `blob == HEAD (f050c7c)` and `git diff HEAD` empty. - make the line conditional (`if (false) limits.push(...)`) — mutation landed, pin **RED** on all 5 cases, restored byte-exact. - Expected direction was "turns red", and that is what both legs produced. - Gate families derived from the real change set with `node scripts/pm/dispatch-gates.mjs --commands` (no hand-written path list): **47 derived, 47 run**, reconciled with `--ran`. 46 exit 0. `pnpm check:pm-dispatch-gates` is the checker's own repo-wide self-test suite and ran past two foreground budgets without reaching a verdict — recorded as NOT MEASURED, left to CI, never read as green. - Control-byte self-scan over both edited files beyond `check:nul-bytes`: no hits. - All readings taken at `549936fe47`. ## Acceptance notes Out of scope for this PR, noted and not filed: - The census instrument is a one-shot measurement, not a shipped script. A census script with no gate reading it is the "code `grep` finds and the runtime never runs" shape from Route & surface ownership §1, and wiring a new gate would be a new verification surface this card does not authorize. The names are durable in the landed text and in the pin; the method is written down here. Successor: none — no open PR or queued card walks this file. - `packages/spec/liveness/agent.json` and `packages/spec/liveness/README.md` were **not written**, per the card's fence: both are held by open PR objectstack-ai#19059 and both carry `merge=os-regen`, where a conflict resolves silently. Independently re-derived here over all 32 open PRs (371 file rows; lit control: objectstack-ai#19102 holds exactly 3 files) — those two are the only held paths among this card's candidates. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ 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 #18910
Clause-②: no — docblock prose only. The accept set, the key list and
strictObjectare byte-identical; no public surface moves.What the card said, and what I measured
The card said
ListMapConfigSchema's docblock asserts a mechanism the renderer does not perform:I re-derived every reading first-hand at the pin —
.objectui-shareads53ded82bf7a494f54e344e19099dbf00854b8694, and I checked that commit out into its own detached worktree (git worktree add --detach, HEAD53ded82bf7a494f54e344e19099dbf00854b8694), never objectuimain. Rather than reading the source and believing it, I extracted the pinned declarations byte-exact by line range (anchor-verified, sha256-stamped) and executed them under zod 4.4.3 — the version the pin's own lockfile resolves. Every zero/negative reading below carries a same-subject control that HITS.ObjectMapConfigSchema(packages/types/src/zod/objectql.zod.ts:562) is a plainz.object:safeParse({…, extraKey})→success=true, zero issues, no warning.strict()appended, same input; (b) the real declaration withzoom: 'far'success=false→unrecognized_keysat the ROOT path; (b)success=false→invalid_type@zoom. The probe can see closure, and it reaches the schemagetMapConfig(packages/plugin-map/src/ObjectMap.tsx:373-376) consults thatsafeParseonly to pick aconsole.warn: with an undeclared key, 0 warn callszoom: 'far'[ObjectMap] Invalid map configuration:— the warn path is reachable and my capture works:378returns a spread of the AUTHORED block:getMapConfig({map:{locationField,extraKey}})→{"locationField":"loc","extraKey":"x"}— carried through{"locationField":"loc","zoom":"far"}resolveListMapConfig(packages/plugin-list/src/ListView.tsx:113) →{"locationField":"loc","titleField":"name"}—extraKeygone, andstylewith itlocationField/titleFieldin the same callPer the dispatcher's warning about counting rather than printing: every negative above is a printed value, not a count. The one search-shaped reading — "
objectql.zod.tsdeclares nothing strict" — is printed too: its onlystricthit in that file is a comment at:392(「NoteUserActionsConfigSchemais NOT.strict()」), and the lit control that the search would find a real one ispackages/types/src/zod/complex.zod.ts:635→}).strict();.What turned out false — in the card, not in the fix
The card's three readings are all accurate, and its line numbers still land. But its conclusion — "an undeclared key inside
mapis neither flagged nor dropped by the renderer" — is false for the path this block governs. A spec list view'smapblock never reachesgetMapConfigas amapblock:ListViewandObjectViewflatten it through the hand-listedFLAT_MAP_CONFIG_KEYSwhitelist first, and both emit flat props ("neither flattener emits amapkey at all"). So the docblock's outcome word — "would be dropped" — happens to hold; its mechanism — a closed local zod schema — does not, anywhere.That distinction is the whole fix, because the mechanism is what a future editor would act on. Believing "the renderer validates it" and relaxing this block to
passthroughbuys an author no check at all: the key dies in a whitelist without a word, and the one schema that could have reported it is open and warn-only.The edit
Before:
After (opening sentence; the full replacement is in the diff):
followed by the two measured bullets, and by the reason the strictness survives losing its old argument: nothing downstream reports an undeclared key, so this parse is the only place an author is told anywhere —
mapis not in objectui'sLIST_VIEW_LOCAL_OVERRIDES, so objectui's ownListViewSchemaimports this block by reference and the document check on that side is this same schema.⛔
strictObjectis untouched. No key was added, removed or renamed.The anchors are now machine-checked, not just asserted
This defect is a cross-repo claim that rotted because nothing re-read it.
check:objectui-pin-citationsalready has the instrument for that — an anchor that quotes its first line is verified against objectui at the pin — and it was sitting at zero coverage. The seven anchors in this record now carry their quoted line, so the gate reds on the next pin bump instead of rotting, andASSERTED_ANCHOR_FLOORmoves0 → 7with its note rewritten (that ratchet is an equality in both directions).Verified, not assumed:
check:objectui-pin-citationsprints7 anchor content assertion(s) verified against objectui at 53ded82bf.Ablation — the new assertions can fail. Committed the change first, then mutated one quoted line through
scripts/ablation-replace.mjs(on-disk anchor hit + blob hashes, not an editor's exit code):export const ObjectMapConfigSchema = z.object({→… z.strictObject({. The gate went exit 1 and named it:Restore was verified on disk, not by an exit code:
blob after restore 358aed5fc71d99c6…==blob at HEAD 358aed5fc71d99c6…,git diff HEADempty.Tests and gates
pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/view.test.ts src/ui/component.test.ts src/ui/view-gantt-tree-config-closed-15469.test.ts src/ui/view-strictness-batch18.test.ts— 4 files, 878 tests passed.pnpm --filter @objectstack/spec typecheck— green (includescheck:scripts-typecheck, which covers the edited gate script, andcheck:test-typecheck).pnpm --filter @objectstack/spec build && … check:generated— all 16 generated artifacts up to date; no regeneration was owed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(its own change-set derivation, not a hand-written list) and run; six dist-reading gates first refused withPREREQUISITE NOT METagainst adistolder than the edit, and were green on re-run afterpnpm --filter @objectstack/spec build— recorded as NOT MEASURED → measured, never as findings.Acceptance notes
Noted here rather than filed or fixed — ⛔ no diff was widened for any of them.
ListMapConfigSchema.styledoes not reach the renderer on the list-view path at this pin.#18406addedstylebecausegetMapConfigreadsschema.map?.style. Measured:FLAT_MAP_CONFIG_KEYSis this block's keys minusstyle,resolveListMapConfigdrops it (printed above),basePropscarries nostyle/mapStyle, andmapStyleappears in neither flattener (printed hits; lit control:locationField, which IS forwarded, hits in both files). Radius of the instrument: the two flatten sites plusbasePropsinpackages/plugin-list/src/ListView.tsxandpackages/plugin-view/src/ObjectView.tsxat53ded82b— it does not cover anobject-mapcomponent node written by hand, wheremap.styledoes reachgetMapConfig. A style URL authored through this face parses green and the map renders on the default demo tiles, silently. The landing repo would be objectui, and the docblock sentence it touches is#18406's ruling text, so it is a card, not a rider.style(the read is ingetMapConfig) but not on the path this block takes — see 1. Left byte-identical: correcting it is the same card as 1.Generated by Claude Code