Skip to content

test(cli): sweep every os explain catalog entry against its spec schema - #15197

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-14811-explain-catalog-schema-guard
Sep 4, 2026
Merged

test(cli): sweep every os explain catalog entry against its spec schema#15197
os-litant merged 1 commit into
mainfrom
claude/issue-14811-explain-catalog-schema-guard

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #14811

Generalises the parse-the-example guard from one catalog entry to the whole
os explain catalog, and reports what that turns up rather than repairing it.

The catalog is 11 entries, not 12

Triage refused to publish a count and required it be derived programmatically,
which was the right call: iterating SCHEMAS yields 11 keys —
object, field, view, flow, agent, app, query, dashboard,
action, workflow, trigger.

The card says 12 and lists fields alongside field. There is no fields
entry in the catalog. Triage's step 2 named fields as one of the two entries
with nothing to parse against — that half of the instruction is about an entry
that does not exist, and the two real unbound entries turned out to be
workflow and trigger instead. Nothing was hand-counted to establish this:
the guard's own classification test compares its table against
Object.keys(SCHEMAS) and goes red on any disagreement.

What the sweep does

Each entry's example is evaluated and parsed against the schema it names,
resolved by name from the spec. Measured while wiring it up: the package
root @objectstack/spec exports none of these Zod schemas (129 root
exports, no ObjectSchema / FieldSchema / … among them), so every binding
names its subpath — /data, /ui, /ai, /automation.

Three properties the block is built to have:

  • No entry escapes. The table is checked against Object.keys(SCHEMAS), so
    a catalog entry added tomorrow makes this red until it is classified. A
    filter would have been the exact shape of the defect being closed.
  • Nothing is skipped silently. The two entries with no schema get tests
    that assert their reason instead of being absent.
  • it.fails cannot go green by accident. Vitest's xfail passes on any
    failure, so a broken subpath export or an unevaluable example would keep six
    xfails green while measuring nothing. A separate assertion resolves every
    bound schema to a real safeParse and every bound example to an object —
    that one is a plain it, and it is what makes the six xfails load-bearing.

Results — 3 parse, 6 do not, 2 have nothing to parse against

entry schema verdict
field FieldSchema parses
flow FlowSchema parses (pinned earlier at #14782)
query QuerySchema parses
object ObjectSchema xfail — see #15170
view ViewSchema xfail — see #15171
agent AgentSchema xfail — see #15172
app AppSchema xfail — see #15173
dashboard DashboardSchema xfail — see #15174
action ActionSchema xfail — see #15175
workflow none unbound: no standalone Workflow type (ADR-0019)
trigger none unbound: no TriggerSchema at all — see #15176

No catalog entry is repaired here. Rewriting one rewrites operator-facing
output and is a separate change with a separate review question; the ruling on
#14811 put that out of scope. Each broken entry is an it.fails naming its own
card, and the day one is corrected its xfail reports "Expect test to fail" —
that is the prompt to promote it to a plain assertion.

The sharpest of the six is agent: it still documents and demonstrates
tools, a key removed in @objectstack/spec 17 whose own rejection
message says there is no key the value moves to (ADR-0064). The trigger
entry is the odd one out and arguably worse than a broken example — it
documents a metadata type the spec does not have, and its sample is not a
Hook either, so the guard rules that candidate out explicitly rather than
assuming it.

One correction outside the new block, named here

This file's own comment claimed it "sits outside every tsc program — see the
TEST_DEBT ledger". That is no longer true and the new code's safety argument
depends on the opposite, so it is corrected in place rather than left to
mislead the next editor. Evidence: #14710 landed packages/cli/tsconfig.test.json
with include: ["test/**/*"]; tsc --noEmit --listFiles -p tsconfig.test.json
resolves packages/cli/test/commands.test.ts (1 hit), and the file carries no
row in test-typecheck-debt.json, so any diagnostic it gains is red on arrival.
Confirmed by pnpm --filter @objectstack/cli typecheck passing with the ledger
held at 3 files / 28 errors / 6 pinned signatures, unchanged.

⛔ The object.ownership exact-token assertion (#3244, widened at #5678) is
untouched — it guards a different axis and this parse check does not subsume it.
packages/cli/src/commands/explain.ts is unmodified.

Reverse verification

Four ablations, each mutating the tree, proving the mutation landed on disk by
a marker that was absent before, running the suite, restoring via
git checkout HEAD -- <abs path>, and proving the restore by comparing
git hash-object against the HEAD blob (an empty hash read as failure, not as
"nothing to compare"). No rebuild leg is involved or needed: the test imports
../src/commands/explain as source through vitest, not through a dist/
export.

mutation predicted observed
break the field example (type: 'not_a_field_type') sweep goes red red — AssertionError: os explain field: its example must parse as FieldSchema
add a brand_new_entry to SCHEMAS classification goes red red — expected [ …(8) ] to deeply equal [ …(9) ]
move a passing entry into the xfail set xfail inverts red — Error: Expect test to fail
drop object's xfail marker red, proving the parse really fails red — AssertionError: os explain object: its example must parse as ObjectSchema

Both restore legs verified clean after every case (git status empty, blob
hashes equal to HEAD).

Verification — all at e5a680b6, the final commit

pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/commands.test.ts
Test Files 1 passed (1) · Tests 25 passed | 6 expected fail (31).

pnpm --filter @objectstack/cli typecheck — green, both legs. The test-layer
program reports 3 file(s) / 28 error(s) / 6 pinned signature(s) held in test-typecheck-debt.json, i.e. unchanged: this file is in that program and
gained nothing.

Gate union derived, never hand-listed:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands
on a clean tree — 35 commands from 1 changed path. Each exit code captured
before any pipe. 34 pass, 1 NOT MEASURED:

  • ⚠️ pnpm check:dual-build-cjs-loadsPREREQUISITE NOT MET … ⛔ This is NOT a pass: nothing was measured (exit 3). It reads built output and 12 packages
    have no dist/ in this worktree, which built only the CLI's dependency
    closure. Not a red and not a green; CI builds everything and runs it. A
    test-only diff ships nothing into any dist/.
  • node scripts/check-plugin-teardown-shape.mjs --self-test first exited 1 with
    cannot read the positive control at 621a4876… — the shallow-clone
    prerequisite, not a red. After git fetch --depth=1 origin 621a4876… (the
    object only, so the shared origin/main pointer was not moved) it passes:
    ✓ … 47 cases pass.

Repo-wide pnpm lint was narrowed, and the narrowing is declared rather than
silent.
Run: npx eslint --no-inline-config --format json over the changed
path — 0 errors, 0 warnings, and the JSON reports 1 file linted, so the
file was really processed rather than skipped by an ignore rule. Why 1 is the
whole population that can move: the changed-path set is derived from git by
dispatch-gates.mjs (1 path vs merge base 25a59bd10), not hand-written; and
this repo runs one eslint.config.mjs which "never enables type-aware linting
(no parserOptions.project, no typed @typescript-eslint rules) for ANY file"
(its own measured note, eslint.config.mjs), so no untouched file's verdict
can move because of this diff. CI runs the full scan regardless.

⚠️ pnpm --filter @objectstack/cli exec vitest run (the whole package) is
NOT MEASURED.
It was started, held the shared verify lock for 745s while
writing nothing for the last 6.5 minutes of that, with a sibling agent queued
behind it for 500s — so it was stopped to free the lock rather than left to
hog it. Recorded as not measured rather than quietly dropped. The signal it
would have added is thin: this diff changes one test file and no source, so no
other test's behaviour can move; CI runs the suite.


Generated by Claude Code

…hema

The `SCHEMAS` catalog in `packages/cli/src/commands/explain.ts` is
hand-maintained and derives from nothing; until now one field of one entry
was guarded by an exact-token assertion, and one entry's example was pinned
by parsing it against the real schema.

This generalises that parse technique across the whole catalog, deriving the
entry set from `SCHEMAS` itself so a future entry cannot be added without
being classified. Entries whose example does not parse today land as
`it.fails` xfails naming the card filed for each; rewriting a catalog entry
is operator-facing output and a separate review question, so nothing is
fixed here. The two entries with no schema to parse against assert that
reason rather than being silently absent.

Also corrects a stale claim in this file's own comments: it is no longer
outside every tsc program. `packages/cli/tsconfig.test.json` includes
`test/**/*`, and this file carries no row in `test-typecheck-debt.json`, so
a diagnostic it gains is red on arrival.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
@github-actions github-actions Bot added the size/m label Sep 4, 2026
@os-litant os-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 919beca43b2901320cea39afd912b7f662ddd3b1packageMentionDocs.

@github-actions github-actions Bot added the tests label Sep 4, 2026
@os-litant
os-litant marked this pull request as ready for review September 4, 2026 06:05
@os-litant
os-litant enabled auto-merge September 4, 2026 06:05
@os-litant
os-litant added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 8ad872b Sep 4, 2026
39 checks passed
@os-litant
os-litant deleted the claude/issue-14811-explain-catalog-schema-guard branch September 4, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] os explain's 11 other catalog entries are hand-maintained against no schema — the flow entry's sample was unparseable and nothing said so

2 participants