Skip to content

docs(spec): state what the map renderer actually does with an undeclared key in map - #19102

Merged
os-bill merged 3 commits into
mainfrom
claude/issue-18910-listmapconfig-docblock-truth
Sep 18, 2026
Merged

os-bill merged 3 commits into
mainfrom
claude/issue-18910-listmapconfig-docblock-truth

Conversation

@os-bill

@os-bill os-bill commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18910

Clause-②: no — docblock prose only. The accept set, the key list and strictObject are 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:

it validates schema.map against a local zod schema with exactly these keys, so an extra key here would be dropped there

I re-derived every reading first-hand at the pin — .objectui-sha reads 53ded82bf7a494f54e344e19099dbf00854b8694, and I checked that commit out into its own detached worktree (git worktree add --detach, HEAD 53ded82bf7a494f54e344e19099dbf00854b8694), never objectui main. 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.

# reading at the pin lit control control result
1 ObjectMapConfigSchema (packages/types/src/zod/objectql.zod.ts:562) is a plain z.object: safeParse({…, extraKey}) → success=true, zero issues, no warning (a) the same extracted declaration with .strict() appended, same input; (b) the real declaration with zoom: 'far' (a) success=false → unrecognized_keys at the ROOT path; (b) success=false → invalid_type@zoom. The probe can see closure, and it reaches the schema
2 getMapConfig (packages/plugin-map/src/ObjectMap.tsx:373-376) consults that safeParse only to pick a console.warn: with an undeclared key, 0 warn calls same call with zoom: 'far' exactly 1 warn, [ObjectMap] Invalid map configuration: — the warn path is reachable and my capture works
3 :378 returns a spread of the AUTHORED block: getMapConfig({map:{locationField,extraKey}}) → {"locationField":"loc","extraKey":"x"} — carried through the failing-parse call above still returns {"locationField":"loc","zoom":"far"} the parse result is provably not the value
4 New. On the path this block actually takes, the key IS dropped: resolveListMapConfig (packages/plugin-list/src/ListView.tsx:113) → {"locationField":"loc","titleField":"name"} — extraKey gone, and style with it the declared locationField / titleField in the same call both survive — the instrument transmits, so the two absences are readings

Per 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.ts declares nothing strict" — is printed too: its only strict hit in that file is a comment at :392 (「Note UserActionsConfigSchema is NOT .strict()」), and the lit control that the search would find a real one is packages/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 map is neither flagged nor dropped by the renderer" — is false for the path this block governs. A spec list view's map block never reaches getMapConfig as a map block: ListView and ObjectView flatten it through the hand-listed FLAT_MAP_CONFIG_KEYS whitelist first, and both emit flat props ("neither flattener emits a map key 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 passthrough buys 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:

Closed (strict) from the start: the map renderer's read set is itself closed — it validates schema.map against a local zod schema with exactly these keys, so an extra key here would be dropped there.

After (opening sentence; the full replacement is in the diff):

Closed (strict) from the start, and the strictness stands on its own: it does NOT rest on the renderer refusing an undeclared key, because nothing downstream refuses one.

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 — map is not in objectui's LIST_VIEW_LOCAL_OVERRIDES, so objectui's own ListViewSchema imports this block by reference and the document check on that side is this same schema.

⛔ strictObject is 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-citations already 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, and ASSERTED_ANCHOR_FLOOR moves 0 → 7 with its note rewritten (that ratchet is an equality in both directions).

Verified, not assumed: check:objectui-pin-citations prints 7 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:

✗ packages/spec/src/ui/view.zod.ts:1764 — the asserted first line does not match
  `packages/types/src/zod/objectql.zod.ts:562` at this pin.
      asserted: export const ObjectMapConfigSchema = z.strictObject({
      actually: export const ObjectMapConfigSchema = z.object({

Restore was verified on disk, not by an exit code: blob after restore 358aed5fc71d99c6… == blob at HEAD 358aed5fc71d99c6…, git diff HEAD empty.

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 (includes check:scripts-typecheck, which covers the edited gate script, and check:test-typecheck).
  • pnpm --filter @objectstack/spec build && … check:generated — all 16 generated artifacts up to date; no regeneration was owed.
  • Gate families derived with 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 with PREREQUISITE NOT MET against a dist older than the edit, and were green on re-run after pnpm --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.

  1. ListMapConfigSchema.style does not reach the renderer on the list-view path at this pin. #18406 added style because getMapConfig reads schema.map?.style. Measured: FLAT_MAP_CONFIG_KEYS is this block's keys minus style, resolveListMapConfig drops it (printed above), baseProps carries no style/mapStyle, and mapStyle appears in neither flattener (printed hits; lit control: locationField, which IS forwarded, hits in both files). Radius of the instrument: the two flatten sites plus baseProps in packages/plugin-list/src/ListView.tsx and packages/plugin-view/src/ObjectView.tsx at 53ded82b — it does not cover an object-map component node written by hand, where map.style does reach getMapConfig. 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.
  2. This docblock's opening still claims "every key here has a live read there". True as written for style (the read is in getMapConfig) 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

…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>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:ui tooling labels Sep 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/ui/view.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/ui/view.zod.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 07c6f822edd9c4c48a7ce34767de420c6096b9c3 → packageMentionDocs.

@os-bill
os-bill marked this pull request as ready for review September 18, 2026 20:00
@os-bill
os-bill added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 8e8ea99 Sep 18, 2026
37 checks passed
@os-bill
os-bill deleted the claude/issue-18910-listmapconfig-docblock-truth branch September 18, 2026 20:23
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/s tooling

Projects

None yet

2 participants