Skip to content

fix(lint): check the autonumber format the runtime resolves, not a local copy - #19787

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-19772-lint-autonumber-resolver
Sep 23, 2026
Merged

os-warren merged 4 commits into
mainfrom
claude/issue-19772-lint-autonumber-resolver

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #19772
Clause-②: no

The defect

lintAutonumberFormats chose the format it checks with its own copy of the precedence rule. It took autonumberFormat whenever that was a string, '' included, and skipped the field when the string was empty. resolveAutonumberFormat (@objectstack/spec/data) is what the engine (engine.ts:5051) and driver-sql (sql-driver.ts:10639) mint through, and it counts only a non-empty string as declared. So { type: 'autonumber', autonumberFormat: '', format: '{nope}{000}' } parses through FieldSchema and passes the lint. At runtime it resolves to {nope}{000}, which names no field, so every create throws. The fix replaces the local rule with a call to resolveAutonumberFormat. That function is already exported from @objectstack/spec/data, which the lint already imported from. No export is added and packages/spec is not touched.

Undeclared fields: the lint now checks the contract default

A field that declares neither key resolves to DEFAULT_AUTONUMBER_FORMAT ({0000}). The lint now checks that format instead of skipping the field. The reasons:

  • Skipping would need the lint to decide whether a key is declared. That decision is the resolver's non-empty-string rule, so skipping would keep a second copy of the rule this PR removes.
  • Measured: {0000} parses to one sequence token, with no field references and no literal brace groups, so none of the four finding arms can fire. I tried six undeclared spellings ({}, '' on either key, both empty, null, a non-string). Base and head both return 0 findings for each, so no message names a format the author did not write.
  • If the contract default changes one day to a format that does draw a finding, the runtime will mint with it too, so reporting it is correct. The undeclared-field test below goes red on that day, so the change gets noticed.

What was measured: base 4b23e4fab5 against head 26c2a1d762

  • Premises, on the built dist at base. The lint on the document above returns []. The control { format: '{nope}{000}' } returns autonumber-references-unknown-field. resolveAutonumberFormat returns {nope}{000}, and missingFieldValues on that format with an empty record returns ['nope']. FieldSchema.safeParse succeeds and keeps both keys, and the lint on the parsed field is [] too (the rule's input tier is parsed). The engine's own suite, engine-autonumber-default-format.test.ts, pins the shorthand winning over an empty canonical key. It passes 11/11 locally.
  • Base against head, over 121 spellings of the two keys (11 values each: absent, null, '', a number, an object and six formats). 4 spellings changed. All 4 are autonumberFormat: '' with a non-empty format, and each now draws the finding the shorthand draws on its own: unknown field, optional field, self-reference or unrecognised token. Head matches the resolver on all 121.
  • Runtime publish gate (runRuntimeAuthoringRules, type object). Base publishes the document with errors: []. Head refuses it with autonumber-references-unknown-field. An undeclared field gets [] on both.

Tests

lint-autonumber-formats.test.ts gains five tests:

  1. An empty canonical key lints the shorthand. This is the test written to fail first.
  2. A non-empty canonical key wins over the shorthand.
  3. The shorthand is linted when the canonical key is absent.
  4. With neither key declared, the findings equal those for the declared default, and are [].
  5. A table over 25 spellings asserts the findings equal those for the format the resolver returns.

On f395eafd6f (the tests alone, without the fix): 2 failed, 12 passed. The failures are test 1 and the table, and the table's only divergent spelling is {"autonumberFormat":"","format":"{nope}{000}"}. On head: 14/14 pass.

Ablations start from the committed fix and go through scripts/ablation-replace.mjs. In every leg the anchor went 1 to 0 and the blob changed. The restore was proven by blob == HEAD and an empty git diff HEAD.

  • Revert the fix (put the old copy back): 2 red, test 1 and the table.
  • Swap the precedence: 2 red, test 2 and the table.
  • Ignore the shorthand: 3 red, tests 1 and 3 and the table.
  • Make the default draw a finding: 1 red, test 4.

Verification, at head 26c2a1d762

  • @objectstack/lint tests: base has 108 files and 4111 tests, head has 108 and 4116. The +5 are the new tests. Typecheck exits 0 on both. The test layer keeps its 6 ledgered errors in 2 other files, and --listFiles shows the edited test file is compiled. The build exits 0, and both the ESM and CJS dist carry the resolver call. The CJS build, loaded and run on the document, returns autonumber-references-unknown-field.
  • dispatch-gates --repo objectstack-ai/objectstack: 59 derived and 57 exited 0. Two are NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt exit 3 until the whole workspace is built. CI builds it and runs both. --ran with the exit codes recorded reconciles 59 derived, 57 run, 2 NOT MEASURED, 0 unrun. check:lean-entry-closure first exited 3 as well. After building @objectstack/objectql it exited 0.
  • Consumers. @objectstack/cli runs this rule through authoringRulesFor in compile.ts:382 and validate.ts:331, and the rule table is unchanged. test/authoring-rule-command-parity.test.ts exercises this rule through the lint dist. Its 10 in-process cases pass, including autonumber-references-unknown-field gates on all three commands. Its one spawned case, an approval-flow repro that does not touch this rule, was filtered out locally and is left to CI's integration tier. @objectstack/metadata-protocol protocol.runtime-authoring-gate.test.ts passes 35/35.
  • Hygiene: the three changed files, the commit messages and this body have 0 control bytes and 0 model identifiers. Both positive controls fired.

Acceptance notes

  • The sweep found no other copy of the autonumber precedence in packages/lint/src. Among non-test sources, lint-autonumber-formats.ts is the only file that names autonumberFormat.
  • A field whose autonumberFormat is '' and whose format is broken now fails os validate, os build and the runtime publish gate. Before, it passed all three and then failed on every create. No field in this repository's sources declares autonumberFormat: ''. Outside tests, it appears only in changelog and comment prose.

Generated by Claude Code

Red on this commit: with `autonumberFormat: ''` and a `format` shorthand,
the lint takes the empty canonical key and checks nothing, while
`resolveAutonumberFormat` (which the engine and the SQL driver mint
through) treats the empty key as undeclared and renders the shorthand.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
…cal copy

`lintAutonumberFormats` chose the format with its own precedence rule,
which took `autonumberFormat: ''` as declared and skipped the field. The
engine and the SQL driver resolve the same field to its `format`
shorthand and mint through it, so a shorthand naming an unknown field
passed the lint and then failed every create.

The lint now asks `resolveAutonumberFormat`, the resolver both minting
paths call, so there is no second copy of the rule to drift. A field
declaring neither key is checked against the contract default it mints
with; that default draws no finding.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 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 531689c230af594abcefd2775ea076609272b12a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7522d42541ce7297821279f46eeaf44562b1b206 — the merge of head 26c2a1d7623c16bf6817d2acd56612928be2ee64 into base 531689c230af594abcefd2775ea076609272b12a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7522d42541ce7297821279f46eeaf44562b1b206 && git checkout 7522d42541ce7297821279f46eeaf44562b1b206
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 531689c230af594abcefd2775ea076609272b12a 26c2a1d7623c16bf6817d2acd56612928be2ee64 && git checkout -B drift-repro 531689c230af594abcefd2775ea076609272b12a && git merge --no-ff 26c2a1d7623c16bf6817d2acd56612928be2ee64

node scripts/docs-audit/affected-docs.mjs --json 531689c230af594abcefd2775ea076609272b12a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

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 size/s tests tooling

Projects

None yet

2 participants