Skip to content

fix(lint): one-line verdicts for the hook, action and flow record-write rules; os explain RULE_ID carries their reasoning - #22548

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22161-s2-lint-slice-3
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22161-s2-lint-slice-3

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #22161
Clause-②: no

Stage 2 of the card, slice 3: the 14 rule ids of the six record-write rules in packages/lint, six whole source files. The card stays open for the later slices listed under "Remaining" below.

What changes

  • One verdict line per finding. Each finding of the 14 ids prints a message of one verdict sentence. Every finding the rules' own suites fire is now 196 characters or fewer, and the CLI suite's handler-hook variants 195 or fewer (the longest of each id was 212 to 792 before). The author's values still close each verdict: the field, the object, the ctx.api call and the run identity. A hook lowered from an inline handler keeps its location suffix " (judged on the metadata body lowered from the inline handler)", and the bound holds with it.
  • The reasoning moves to RULE_EXPLANATIONS (packages/lint/src/rule-explanations.ts), 14 new entries, so os explain RULE_ID prints it and the CLI's rule: line ends with the pointer for these ids. No CLI file changes: explainPointer() and os explain resolve any key the table holds (packages/cli/test/explain-rule-id.test.ts iterates every key; run below).
  • Nothing else moves. Rule ids, severities, path, hint (the fix: line) and what each rule accepts or refuses are unchanged. No condition, branch, dedupe key or skip moved; every hunk in the six rule files is a message expression, a comment, an import, or a shared verdict helper.
  • .changeset/22161-lint-slice-3-one-line.md: @objectstack/lint patch, naming every door that prints the new text (below). Clause-②: no follows the contract review on slice 1 (its section ②): RULE_EXPLANATIONS entries are data in an existing export.

Shared prose: written once (the dispatch's H2)

H2 holds. Three families of sentences were shared across the action, hook and flow surfaces. Each was typed out separately in each rule, or concatenated from one long helper. Each now lives in one place for the verdict and one place for the explanation, and every id that uses it references it:

shared sentence ids verdict clause (rule files) explanation (rule-explanations.ts)
the unprovisioned-anchor cause and write consequence (was unprovisionedAnchorCause() plus the 5-sentence unprovisionedAnchorWriteConsequence()) the three *-write-unprovisioned-anchor ids unprovisionedAnchorWriteVerdict() in validate-hook-body-writes.ts, over slice 2's one-clause unprovisionedAnchorVerdict() (system-fields.ts, unchanged) UNPROVISIONED_ANCHOR_WRITE (3 paragraphs)
the declared-field door ("scoped handle on the running engine … INVALID_FIELD / 400, identically on every driver, before any statement is built"), typed twice verbatim plus a flow variant the three *-write-unknown-field ids UNDECLARED_FIELD_WRITE_REFUSAL and undeclaredApiWriteVerdict() in validate-hook-body-writes.ts DECLARED_FIELD_DOOR
the parse-failure sentence, typed twice verbatim hook-body-source-unparseable, action-body-source-unparseable bodyParseFailureVerdict() in validate-hook-body-writes.ts BODY_PARSE_FAILURE (2 paragraphs)
the static readonly strip and the conditional readonlyWhen strip the five readonly ids READONLY_WHEN_STRIP_SCOPE, READONLY_INSERT_STRIP_OUTCOME in validate-readonly-flow-writes.ts (already the module the other two import buildReadonlyIndex from) READONLY_STATIC_STRIP, READONLY_WHEN_STRIP (2 paragraphs), plus FLOW_FIELDS_CALLER_PAYLOAD and HOOK_API_CALLER_PAYLOAD for the two sibling pairs

unprovisionedAnchorWriteConsequence() had exactly these three callers and is not exported from the package barrel, so it is replaced, not left dead. unprovisionedAnchorCause() keeps its other callers in the files later slices will shorten.

The shape, as os lint's printer wrote it in packages/cli/test/lint-hook-rules-reach-handler-hooks.test.ts against the rebuilt @objectstack/lint dist (where: message, recorded by the census preload below):

hook "typo" › body: body calls ctx.api.object('crm_case').update(…) writing undeclared field 'is_escalatd', so the write is refused (INVALID_FIELD / 400) (judged on the metadata body lowered from the inline handler)
hook "escalate" > body: body's ctx.api.object('crm_case').update(...) writes readonly field 'is_escalated', silently stripped on a non-system trigger
hook "hold" > body: body's ctx.api.object('crm_case').update(...) writes readonlyWhen field 'credit_hold', silently stripped where its predicate is TRUE (judged on the metadata body lowered from the inline handler)

The other verdict forms, one each, as the lint suite fired them:

'organization_id' is an injected column with no storage on external object 'wh_order', so the body's updateById(…) write can never land
create_record writes 'stagee', but object 'deal' declares no such field, so the write is refused (INVALID_FIELD / 400), the record is never created and the step fails the run
body assigns ctx.record.amount, but an action's ctx.record is a snapshot the runtime never writes back, so the assignment is discarded while the action returns success
L2 body did not parse (line 1, column 15: Expression expected.; 2 syntax errors in total), so writes in its unread part go unchecked
writes readonly field 'approval_status' of object 'crm_opportunity', which a runAs:'user' INSERT silently strips, so the row is created WITHOUT this column

Census (taken first, before any edit, at the base faf6348508)

Method: a scratch preload (NODE_OPTIONS=--import, never committed) patched Array.prototype.push to record every finding-shaped object (rule + message) of the 14 ids, deduped by (rule, message), with its push site, in every vitest worker and every process a test spawns. Lengths are message alone; the printed line adds where and : . Rows from os lint's own printer (commands/lint.ts), whose message is the printed where: message line, are excluded, as in slice 2.

  • packages/lint suite at the base: 131 files, 6,010 tests passed. All 14 ids fired, every one over 200.
  • packages/cli suite, unit and integration, against the base's lint dist: 376 files, 373 passed, 4,961 tests passed and 35 skipped. Three files failed before any test: published-subpath-console.pin, published-subpath-hook-body.pin and dev-standalone-self-heal.integration. Each refused because packages/cli itself had no dist/ (the census built the cli's dependency closure, not the cli). None of the three calls a lint rule (grep: no lintConfig, runAuthoringRules, ctx.api or rule id), so the census is complete for these ids. The cli suite fires three of the 14, all from test/lint-hook-rules-reach-handler-hooks.test.ts with the lowered-handler suffix: hook-body-write-unknown-field 527 and hook-api-update-readonly-when-field 323 (both longer than the lint suite's), and hook-api-update-readonly-field 419 (the lint suite's 464 is longer).
  • H1 holds: every id is over 200 on some variant, so none is dropped. The shortest were the two *-source-unparseable ids at 212, carried by the parse diagnostic plus "; 2 syntax errors in total".
  • No producer outside the six files: every one of the 14 messages is built in its own rule file, from system-fields.ts (unchanged) and checked-parse.ts (unchanged). No id quotes packages/spec refusal prose, so none was dropped for that reason.
rule id file severity before: lint suite messages · longest after: lint suite messages · shortest–longest cli suite before → after
hook-body-write-unprovisioned-anchor validate-hook-body-writes.ts warning 2 · 792 2 · 127–135 —
action-body-write-unprovisioned-anchor validate-action-body-writes.ts warning 2 · 778 2 · 124–128 —
flow-node-write-unprovisioned-anchor validate-flow-node-writes.ts warning 1 · 739 1 · 122–122 —
hook-body-write-unknown-field validate-hook-body-writes.ts warning 15 · 465 15 · 118–195 527 → 195
hook-api-update-readonly-field validate-readonly-hook-writes.ts error 14 · 464 14 · 119–196 419 → 187
action-body-write-unknown-field validate-action-body-writes.ts warning 11 · 433 11 · 120–135 —
flow-node-write-unknown-field validate-flow-node-writes.ts error 21 · 421 21 · 141–176 —
flow-update-readonly-field validate-readonly-flow-writes.ts error 13 · 410 13 · 96–155 —
action-api-update-readonly-when-field validate-readonly-action-writes.ts warning 5 · 396 5 · 173–182 —
hook-api-update-readonly-when-field validate-readonly-hook-writes.ts warning 1 · 267 1 · 135–135 323 → 194
flow-update-readonly-when-field validate-readonly-flow-writes.ts warning 3 · 317 3 · 135–146 —
action-record-write-discarded validate-action-body-writes.ts warning 3 · 293 3 · 165–167 —
hook-body-source-unparseable validate-hook-body-writes.ts warning 2 · 212 2 · 99–132 —
action-body-source-unparseable validate-action-body-writes.ts warning 2 · 212 2 · 99–132 —

The per-id message counts are unchanged, so no two variants of an id collapsed into one sentence. The cli "after" column is the four cli files that call these rules (run below), not the whole suite again.

Doors that print the new text

Read from the registry (authoring-rules.ts: the reference-integrity suite entry is commands: ALL, runtimeTypes: ['flow', 'view', 'object', 'dataset', 'report']), the suite's per-member runtimeTypes (reference-integrity-suite.ts: the six members carry the frozen ['flow'] default) and the runtime gate (lint/src/runtime-gate.ts, metadata-protocol/src/runtime-authoring-gate.ts):

door ids what changes
os validate, os build (and os compile, which os dev runs per compile), os lint, os verify, os init scaffold check all 14 the text-face verdict line; the rule: line gains the os explain pointer; os validate --json warnings and os build --json author-time issues carry the new message
runtime publish gate, flow writes (Studio, REST /meta, MCP) the 4 flow ids errors (flow-node-write-unknown-field, flow-update-readonly-field): the 422 issue message and the OS_ALLOW_UNLINTED_METADATA_WRITES refusal log line. Warnings (flow-node-write-unprovisioned-anchor, flow-update-readonly-when-field): the 2xx advisories message and the deduped [Protocol] authoring advisory log line
never at the runtime gate the 10 hook and action ids a hook or action write does not dispatch the suite (the body members parse JavaScript, which runtime-lazy-deps.test.ts pins off the publish path), and a flow snapshot carries no hook or action body
unchanged every hint; every other rule id

Every row is named in the changeset.

Tests (head 77821c2e1d)

  • Each rule's own suite pins the new shape, as in slice 2. The six test files wrap their rule import and record every finding their cases fire for the shortened ids. A final case holds each recorded verdict to one line of at most 200 characters, behind a coverage control that each shortened id fired at least once. The two hook files also require that a lowered-handler variant (with the suffix) is among the recorded ones, so the bound covers it. A second block pins, per id, that explainRule(id) exists and still names what the verdict stopped saying. The measured-refusal pins (INVALID_FIELD / 400, "identically on every driver", "before any statement is built", "ordinary CALLER write", what fails) moved from the message to the explanation. The retired driver-split negatives now hold over both the message and the explanation. Every refusal assertion, rule id, severity, path, where and hint pin is unchanged.
  • Lint suite: pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 gave Test Files 131 passed (131), Tests 6,036 passed (6,036); lock VERDICT command-exit 0. The first run, at 4c78055635, was red in exactly one case: rule-explanations.test.ts refuses a dot in covers, and action-record-write-discarded's phrase read "ctx.record". 77821c2e1d rewords it.
  • Lint typecheck: pnpm --filter @objectstack/lint run typecheck gave VERDICT command-exit 0. check:test-typecheck OK: 2 files, 6 errors and 2 pinned signatures held.
  • cli, against the rebuilt @objectstack/lint dist (marker grep: 1 hit each in dist/rule-explanations.js and .cjs): pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/explain-rule-id.test.ts test/commands.test.ts test/lint-hook-rules-reach-handler-hooks.test.ts test/validate-build-gate-parity.test.ts src/flow-node-undeclared-field-write.integration.test.ts test/lint-hook-rules-reach-handler-hooks.e2e.test.ts gave Test Files 5 passed (5), Tests 107 passed (107). Of the six named paths, vitest list selects the first five, four unit and one integration. The flow-node file pins the runtime refusal the verdict names, not the message. The sixth, the .e2e file, selected nothing: it is nightly-tier. explain-rule-id.test.ts iterates every RULE_EXPLANATIONS key, so all 14 new ids resolve through os explain and explainPointer. The nightly-tier file then ran on its own after pnpm turbo run build --filter=@objectstack/cli: OS_TEST_TIERS=nightly … vitest run test/lint-hook-rules-reach-handler-hooks.e2e.test.ts gave 1 file, 10 passed. It spawns os build / os lint, and every hook-api-update-readonly-field verdict it fired is at most 187 characters.
  • Spec repo project (the dispatch's H3): pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2 gave Test Files 54 passed (54), Tests 915 passed (915); lock VERDICT command-exit 0. It walks packages/lint/src as a text corpus. The new explanations carry no os migrate meta sentence (grep: 0), no security-* rule id constant, and no retired key.
  • Nothing else moves. The six rule files' only non-message hunks are imports, comments and the shared helpers. unprovisionedAnchorWriteConsequence(), the one removed function, had exactly the three in-slice callers (grep at the base) and is not on the package barrel. system-fields.ts and checked-parse.ts are untouched, so no other rule's message can move. The per-id message counts in the census table are equal before and after.
  • Ablation (one-shot, from the committed state 77821c2e1d, through scripts/ablation-replace.mjs wrap mode, trap-armed). The tests import the rule source, so there is no dist leg. action-record-write-discarded's pre-slice message was restored verbatim through the anchor of its new two-line message: anchor x1 to x0, blob 134c72868e97 to 1a67d89f7b73. src/validate-action-body-writes.test.ts then gave Test Files 1 failed (1), Tests 1 failed | 44 passed (45). The one red case was the bound pin, "every verdict the cases above fired for those ids is one line of at most 200 characters" (the restored ctx.record.stage verdict, 291 characters). Restored: blob after restore 134c72868e97 == blob at HEAD, git diff HEAD empty, git status --porcelain empty.
  • ESLint, narrowed: npx eslint --no-inline-config --format json over the 13 changed .ts files (git diff --name-only faf6348508..HEAD) gave 13 files in the report, 0 errors, 0 warnings, none ignored. eslint.config.mjs never enables type-aware linting (its comment at :327), so no untouched file's verdict can move. A control-character scan of every changed file found no match, and check:nul-bytes exits 0.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the actual diff (14 paths vs merge base faf634850) derived 62 commands, identical to the dispatch's 62. I also ran the 4 artifact-roster gates it flags as rostered in a directory this diff touches: check-changeset-fixed, check:authz-resolver, check:error-code-casing and check:filter-alias-parity. All 66 ran sequentially, each with its own log and an exit code captured before any pipe, at 77821c2e1d; 64 exited 0. Two exited 3 with PREREQUISITE NOT MET, which is not a measurement. check:dual-build-cjs-loads lacked eight packages' dist/ and exited 0 after building them. check:type-check-debt's re-measure build had a message-less tsup DTS-worker exit in plugin-approvals on the shared box, and its re-run exited 0 ("1 ledger entr(ies) re-measured … none above its recorded number"). --ran gave ✓ dispatch-gates --ran: 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED.

Remaining for later slices — 80 ids in packages/lint, by file

Slice 2's list (PR #22448, census at 05c7c3fa3b; longest fired message, "cli" marks a length only the cli suite reached) minus this slice's six files and 14 ids. The lengths are slice 2's, not re-measured here.

  • data-model-rules.ts (6): unique/legacy-organization-composite 480 cli, relationship/master-detail-required 462, unique/unscoped-declared-index 427, unique/double-declaration 402 cli, rollup/non-numeric-aggregand 364, relationship/delete-behavior 201 cli
  • validate-flow-trigger-readiness.ts (5): flow-time-relative-descriptor-invalid 796, flow-time-relative-descriptor-unroutable 532, flow-trigger-unroutable 518, flow-api-trigger-secret-missing 336, flow-trigger-unknown-event 222
  • validate-react-page-props.ts (5): react-chart-drilldown-invalid 784, react-chart-aggregate-invalid 507, react-chart-field-unprovisioned 429, react-block-needs-record-context 267, react-page-source-unparseable 211
  • validate-rls-predicate-enforceability.ts (5): rls-predicate-unparseable 2056, rls-predicate-unenforceable 1679, rls-predicate-unknown-user-variable 1544, rls-predicate-unknown-field 1399, rls-predicate-over-budget 1259
  • validate-sharing-rule-enforceability.ts (4): sharing-rule-unlowerable-condition 1093, sharing-rule-object-not-shareable 774, sharing-rule-object-controlled-by-parent 660, sharing-rule-runtime-variable-condition 492
  • validate-ai-agent-authoring.ts (3): default-agent-legacy-alias 466, default-agent-outside-roster 388, agent-authoring-withdrawn 352
  • validate-predicate-path-refs.ts (3): predicate-rhs-path-shaped 648, predicate-path-unrooted 434, predicate-path-unresolved 371
  • validate-searchable-fields.ts (3): searchable-field-unprovisioned 512, searchable-field-unsearchable 449, searchable-field-unknown 295
  • validate-sortable-fields.ts (3): sort-field-unprovisioned 844, sort-field-unsortable 369, sort-field-unknown 287
  • lint-view-refs.ts (2): view-ref-nav-view-missing 437, view-key-collision 278 cli
  • validate-approval-approvers.ts (2): approval-approvers-may-resolve-empty 451, approval-approver-not-membership-tier 272
  • validate-chart-bindings.ts (2): chart-measure-unknown 442, chart-axis-not-selected 327
  • validate-dashboard-action-refs.ts (2): dashboard-action-route-unresolved 242, dashboard-action-target-undefined 239
  • validate-dataset-measure-aggregates.ts (2): measure-aggregate-field-type-refused 809, dimension-json-stored-field-refused 531
  • validate-empty-combinators.ts (2): filter-empty-combinator 352, filter-empty-node 226
  • validate-list-view-field-refs.ts (2): list-view-field-dotted 445, list-view-field-unknown 355
  • validate-rule-compilability.ts (2): validation-rule-json-schema-uncompilable 517, validation-rule-regex-uncompilable 482
  • validate-translation-references.ts (2): translation-target-unknown 345, translation-option-key-unknown 230
  • lint-flow-credential-literals.ts (1): flow-credential-literal 390
  • validate-action-dispatch-contract.ts (1): action-dispatch-contract-mismatch 927
  • validate-action-name-refs.ts (1): action-name-undefined 409
  • validate-ai-surface-affinity.ts (1): ai-skill-surface-mismatch 281
  • validate-ai-tool-references.ts (1): ai-skill-tool-unresolved 441
  • validate-capability-references.ts (1): capability-reference-unknown 219
  • validate-component-types.ts (1): component-type-unknown 807
  • validate-flow-filter-tokens.ts (1): flow-filter-token-unknown 278
  • validate-managed-api-methods.ts (1): object/managed-api-method-unaffordable 412
  • validate-mapping-target-fields.ts (1): mapping-target-field-unknown 466
  • validate-nav-access.ts (1): nav-object-ungranted 353
  • validate-nav-object-servability.ts (1): nav-object-unservable 528
  • validate-nav-target-refs.ts (1): nav-target-unresolved 426
  • validate-object-field-refs.ts (1): object-field-ref-unknown 320
  • validate-object-references.ts (1): object-reference-unregistered-platform 324
  • validate-org-axis-red-lines.ts (1): org-axis-cross-org-bu-grant 365
  • validate-page-visualization-bindings.ts (1): page/visualization-without-binding 629
  • validate-preset-comparands.ts (1): filter-preset-comparand 669
  • validate-retired-permission-residue.ts (1): permission-retired-lifecycle-residue 235
  • validate-rule-schema-formats.ts (1): validation-rule-json-schema-unknown-format 1049
  • validate-seed-replay-safety.ts (1): seed-insert-mode-duplicates-on-replay 222
  • validate-seed-state-machine.ts (1): seed-value-outside-state-machine 320
  • validate-semantic-roles.ts (1): semantic-role-field-unprovisioned 291
  • validate-translatable-sections.ts (1): translation-section-name-missing 553
  • validate-view-containers.ts (1): view-container-shape 290

The 26 ids slice 2 fenced, the 9 ids owned by packages/cli, and the action-governance.ts boot-log lines stay as PR #22448's body lists them. This slice touched none of them.

Acceptance notes

  • The fix: lines are unchanged and several stay long. The dispatch holds hints as they are. The readonly hook hints run to several hundred characters (the runAs: 'system' / own-hook-stamp / sudo() remedy), and so does the flow readonlyWhen hint. Whether hints get the same one-line budget is the card's call, as slice 2's report already noted. Some explanation paragraphs restate reasoning those long hints also carry (elevation does not waive readonlyWhen, sudo() is not marshalled into the sandbox). Slice 2's entries overlap their hints the same way.
  • Author values and the lowered-handler suffix extend a verdict. The fixed text of every verdict is short. The suffix is location, not reasoning: it explains why path is hooks[i].handler, and packages/cli pins its wording, so it stays verbatim. Every combination the suites fire is at most 196. A combination no suite fires can pass 200: a hook lowered from a handler that INSERTs a long-named readonly field (239 with the fixtures' names). The changeset says a verdict over a long name grows with it.
  • unprovisionedAnchorCause() keeps five callers (validate-flow-template-paths.ts, validate-page-field-bindings.ts, validate-react-page-props.ts, validate-searchable-fields.ts, validate-sortable-fields.ts). They converge on unprovisionedAnchorVerdict() when their slices shorten them, as slice 2's docblock states.
  • os lint prints where: message. For a lowered hook its printed line reaches 215 characters with the where prefix. That is the printer's shape, stage 1's, carried as before.
  • Process: the cli census of the whole suite held the shared verify lock for 35m32s, as slice 2's did (34m39s). A later slice whose ids the cli suite does not fire can scope that half of its census to the files that call its rules.

Generated by Claude Code

claude added 3 commits October 9, 2026 21:39
…RULE_ID` carries their reasoning

Stage 2 of the card, slice 3: the 14 rule ids of the hook-body,
action-body and flow-node write rules and the three readonly write
rules each print one verdict sentence. The reasoning moves into 14
RULE_EXPLANATIONS entries; the prose the three write surfaces shared is
written once there (and the verdict clauses once in the rule files) so
the families cannot drift apart. Rule ids, severities, paths, hints and
accept/refuse behaviour are unchanged.

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

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 36 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/ai/connect-mcp.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/api/index.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/automation/flows.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/automation/hook-bodies.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/automation/jobs.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/concepts/architecture.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/data-modeling/fields.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/deployment/cli.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/deployment/validating-metadata.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/getting-started/build-with-claude-code.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/releases/v17/17-4.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/releases/v17/17-5.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))
  • content/docs/releases/v17/17-7.mdx (via create_record (literal, a string literal in validateFlowNodeWrites))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 faf6348508519197c6047b46fb30b6ae8910f6b2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 94e4033eb0d28b79e38bb96bb4f5004193311085 — the merge of head 77821c2e1d2094a8f11dd25fb22dd6f4880228e2 into base faf6348508519197c6047b46fb30b6ae8910f6b2, 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 94e4033eb0d28b79e38bb96bb4f5004193311085 && git checkout 94e4033eb0d28b79e38bb96bb4f5004193311085
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin faf6348508519197c6047b46fb30b6ae8910f6b2 77821c2e1d2094a8f11dd25fb22dd6f4880228e2 && git checkout -B drift-repro faf6348508519197c6047b46fb30b6ae8910f6b2 && git merge --no-ff 77821c2e1d2094a8f11dd25fb22dd6f4880228e2

node scripts/docs-audit/affected-docs.mjs --json faf6348508519197c6047b46fb30b6ae8910f6b2

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs faf6348508519197c6047b46fb30b6ae8910f6b2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants