Skip to content

fix(spec,lint): one-line functional-completeness verdicts; os explain RULE_ID carries their reasoning - #22383

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22161-s2-functional-completeness
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22161-s2-functional-completeness

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #22161
Clause-②: no

Stage 2 of the card, first slice: the rule ids whose message lives in packages/spec/src/kernel/functional-completeness.ts. The card stays open for the later slices listed at the end.

What changes

  • One verdict line per finding. Each of the nine rule ids the ADR-0078 completeness predicate emits now prints a message of one verdict sentence, at most 200 characters. The longest was 793. The verdict still names the runtime site that makes it true, because ADR-0078 §6 (the calibration section) keeps that citation in the finding's own message. The fix is unchanged. It is already one pastable line, and the registry's boot log prints it, so leaving it alone keeps that door's text as it was.
  • The reasoning moves to RULE_EXPLANATIONS (packages/lint/src/rule-explanations.ts): nine new entries, one per rule id. os explain RULE_ID prints them, and the CLI's rule: line now ends with — `os explain RULE_ID` for … for these ids. No CLI file changes. explainPointer() and os explain resolve any id the table holds. That was measured through the real printer and packages/cli/test/explain-rule-id.test.ts, which iterates every key (below).
  • Every fact the old text stated is still reachable. Each fact sits in the verdict, the fix: line or the explanation. For view/layout-without-binding, the keys the old per-type body said to declare are on the fix: line, which prints directly under the verdict. Their required/optional status and each type's measured renderer path are in the explanation.
  • Nothing a rule accepts or refuses changes. No severity, rule id, path or fix changes either.
  • Tests.
    • functional-completeness.test.ts (spec): every firing variant (18 fixtures, all nine ids) prints one line of at most 200 characters, with no newline in message or fix. The calendar/gantt/timeline/map key pins now read the fix line. Every other existing message pin still holds: the runtime file or symbol, the refusal screen names, the offending key = "value" entries, the cross-reference to view/row-color-without-colors, no manual fire path exists, isActive.
    • validate-functional-completeness.test.ts (lint): every id in FUNCTIONAL_COMPLETENESS_RULES has an explanation, and each explanation names what its verdict stopped saying.
    • rule-explanations.test.ts accepts the spec predicate's ids as rule id constants (they live in @objectstack/spec/kernel, not the lint barrel).

Census (re-taken at 6a53564b9, before the change)

Every firing variant of every rule in the file, run through the predicate. Lengths are message alone; the printed line adds where and : . All nine ids exceed 200 characters, and all nine are changed here. Stage 1's list counted 8. It measured over the packages/lint suite only, where view/row-color-unresolvable-value (726) never fires. That id fires only in the spec suite.

rule id firing variant (fixture) severity before after
field/summary-without-operations summary error 319 159
field/formula-without-expression formula error 259 176
field/relationship-without-reference lookup error 239 166
field/relationship-without-reference master_detail error 246 173
field/choice-without-options select error 250 170
field/choice-without-options radio error 249 169
field/choice-without-options checkboxes warning 291 175
view/layout-without-binding kanban (no block) warning 248 142
view/layout-without-binding calendar (no block) warning 636 170
view/layout-without-binding gantt (no block) warning 673 155
view/layout-without-binding timeline (no block) warning 568 151
view/layout-without-binding map (no block) warning 638 152
view/layout-without-binding tree (no block) warning 244 138
view/layout-without-binding map (block, no coords) warning 570 189
view/tree-without-parent-field tree (no parent) warning 592 190
view/row-color-without-colors grid rowColor field=status, no colors warning 793 172
view/row-color-unresolvable-value grid rowColor 1 hex value warning 726 194
webhook/without-triggers webhook error 594 182

Doors that print the new text

Measured in-process at b3598b011: runAuthoringRules / runRuntimeAuthoringRules from the rebuilt @objectstack/lint dist, printed through packages/cli/src/utils/format.ts's real printers. The protocol ordering was read from source.

door which ids what changes
os validate, os build (os compile, which os dev runs per compile), os lint all nine (commands: ALL) 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
os verify, os init scaffold check same registry rules (build) same printers
runtime publish gate, object writes (Studio, REST /meta, MCP) field/* only (runtimeTypes: ['object']; the gate's context holds objects, permissions and books, no views or webhooks) summary and formula errors: the 422 issue message, and the hatch's refusal log line. checkboxes warning: the 2xx advisories message, and the deduped [Protocol] authoring advisory log line. hint unchanged
same gate, lookup/master_detail without reference, select/radio without options never reached saveMetaItem runs the schema safeParse (protocol.ts :20704) before runAuthoringGate (:20890), and field.zod.ts refuses both shapes with its own message, unchanged here
registry boot line (warnFunctionalCompleteness, packages/objectql/src/registry.ts) field/* unchanged: it prints rule id and fix, never message
webhook auto-enqueuer boot warning webhook/without-triggers unchanged: its own text

Measured printed shape (os build text face, rebuilt dist):

  • object "my_app_ticket" › fields.total: A `summary` field with no `summaryOperations` reads 0/null everywhere: the engine's summary index skips it (`engine.ts` — `if (!d.summaryOperations) continue`)
      fix: summaryOperations: { object: 'CHILD_OBJECT', field: 'CHILD_FIELD', function: 'sum' }
      rule: field/summary-without-operations  at objects[0].fields.total.summaryOperations — `os explain field/summary-without-operations` for why a summary field needs summaryOperations
  ⚠ view container [0] › list: A `calendar` view with no `calendar` block shows the "Calendar configuration required" screen on every object (`ObjectCalendar.tsx` — `getCalendarConfig` resolves `null`)
    fix: calendar: { startDateField: 'DATE_FIELD', titleField: 'TEXT_FIELD' }
    rule: view/layout-without-binding  at views[0].list.calendar — `os explain view/layout-without-binding` for what each view type renders without its block

(The fix: placeholders are printed in angle brackets; they are spelled in capitals here because GitHub strips angle-bracket fragments from bodies.)

Before and after, per rule

field/summary-without-operations

summary — before 319, after 159:

before: A `summary` field with no `summaryOperations` computes nothing: the engine's summary index skips it (`engine.ts` — `if (!d.summaryOperations) continue`), so it reads 0/null everywhere and anything derived from it is stuck at 0 — while every authoring surface reports success. This is the shape ADR-0078 was written for.
after:  A `summary` field with no `summaryOperations` reads 0/null everywhere: the engine's summary index skips it (`engine.ts` — `if (!d.summaryOperations) continue`)

field/formula-without-expression

formula — before 259, after 176:

before: A `formula` field with no `expression` never computes: the engine builds its formula plan only from fields that HAVE one (`engine.ts` — `if (def?.type === 'formula' && def.expression)`), so this field is permanently empty while parsing and publishing succeed.
after:  A `formula` field with no `expression` is always empty: the engine plans formulas only for fields that have one (`engine.ts` — `if (def?.type === 'formula' && def.expression)`)

field/relationship-without-reference

lookup — before 239, after 166:

before: A `lookup` field with no `reference` is a relationship to nowhere: `$expand` silently skips it (`engine.ts` — `if (!referenceObject) continue`) and the record picker has no object to search, so the column stores raw ids that never resolve.
after:  A `lookup` field with no `reference` points nowhere: `$expand` skips it (`engine.ts` — `if (!referenceObject) continue`) and the record picker has no object to search

master_detail — before 246, after 173:

before: A `master_detail` field with no `reference` is a relationship to nowhere: `$expand` silently skips it (`engine.ts` — `if (!referenceObject) continue`) and the record picker has no object to search, so the column stores raw ids that never resolve.
after:  A `master_detail` field with no `reference` points nowhere: `$expand` skips it (`engine.ts` — `if (!referenceObject) continue`) and the record picker has no object to search

field/choice-without-options

select — before 250, after 170:

before: A `select` field with no `options` is a choice with nothing to choose: the form control is empty AND server-side value validation is disabled (`record-validator.ts` skips the check when the allowed list is empty), so any value writes through the API.
after:  A `select` field with no `options` offers nothing to pick and validates nothing: `record-validator.ts` skips the value check on an empty list, so any value writes through

radio — before 249, after 169:

before: A `radio` field with no `options` is a choice with nothing to choose: the form control is empty AND server-side value validation is disabled (`record-validator.ts` skips the check when the allowed list is empty), so any value writes through the API.
after:  A `radio` field with no `options` offers nothing to pick and validates nothing: `record-validator.ts` skips the value check on an empty list, so any value writes through

checkboxes — before 291, after 175:

before: A `checkboxes` field with no `options` renders zero checkboxes. The validator's multi-value branch tolerates it as free-form (the `multiselect` tags mode), but a checkbox group is almost never meant to be free-form — declare the boxes, or use `multiselect` if free-form tags were the intent.
after:  A `checkboxes` field with no `options` renders zero checkboxes (`record-validator.ts` accepts it as free-form tags); declare the boxes, or use `multiselect` if tags were meant

view/layout-without-binding

kanban (no block) — before 248, after 142:

before: A `kanban` view with no `kanban` block is bound to nothing: the renderer falls back to literal default field names, which works only if the object happens to declare them — on any other object the view renders empty while authoring reports success.
after:  A `kanban` view with no `kanban` block falls back to literal default field names, so it renders empty on any object that does not declare them

calendar (no block) — before 636, after 170:

before: A `calendar` view with no `calendar` block declares no date axis, and the renderer does not invent one: objectui's `ListView.tsx` calendar branch forwards only the bindings the view DECLARED, so `getCalendarConfig` (objectui `ObjectCalendar.tsx`) resolves `null` and the view renders its "Calendar configuration required" refusal screen instead of records — it parses and publishes clean, then shows no event on any object, not just on one that happens to lack a field. Declare `calendar.startDateField`, the block's one required key; the event title resolves through the ADR-0079 record display-name chain when `titleField` is omitted.
after:  A `calendar` view with no `calendar` block shows the "Calendar configuration required" screen on every object (`ObjectCalendar.tsx` — `getCalendarConfig` resolves `null`)

gantt (no block) — before 673, after 155:

before: A `gantt` view with no `gantt` block declares no date axis, and the renderer does not invent one: objectui's `ListView.tsx` gantt branch forwards only the bindings the view DECLARED, so `getGanttConfig` (objectui `ObjectGantt.tsx`) resolves `null` without both dates and the view renders its "Gantt configuration required" refusal screen instead of tasks — it parses and publishes clean, then shows no task on any object, not just on one that happens to lack a field. Declare `gantt.startDateField`, `gantt.endDateField` and `gantt.titleField`, the three keys `GanttConfigSchema` requires; `progressField` and `dependenciesField` are optional and stay unbound when omitted.
after:  A `gantt` view with no `gantt` block shows the "Gantt configuration required" screen on every object (`ObjectGantt.tsx` — `getGanttConfig` resolves `null`)

timeline (no block) — before 568, after 151:

before: A `timeline` view with no `timeline` block declares no date axis, and the renderer does not invent one: objectui's `ListView.tsx` timeline branch forwards a start date only when the view declared one, so `ObjectTimeline.tsx` resolves no date field and renders its "Timeline date axis required" refusal instead of records — it parses and publishes clean, then shows no item on any object. Only the title has a renderer default (`name`); the date axis never does. Declare `timeline.startDateField` and `timeline.titleField`, the two keys `TimelineConfigSchema` requires.
after:  A `timeline` view with no `timeline` block shows the "Timeline date axis required" screen on every object (`ObjectTimeline.tsx` resolves no date field)

map (no block) — before 638, after 152:

before: A `map` view with no `map` block declares no coordinate binding, and the renderer does not guess one: objectui's `ListView.tsx` map branch forwards only the keys the view declared, and `ObjectMap.tsx` no longer guesses `location` / `latitude` / `longitude` field names, so its `hasCoordinateBinding` gate fails and the view renders its "Map configuration required" refusal instead of markers — it parses and publishes clean, then plots nothing on any object. Declare `map.locationField`, or both `map.latitudeField` and `map.longitudeField`; `ListMapConfigSchema` requires neither form, so this warning is where the requirement is stated.
after:  A `map` view with no `map` block shows the "Map configuration required" screen on every object (`ObjectMap.tsx` — its `hasCoordinateBinding` gate fails)

tree (no block) — before 244, after 138:

before: A `tree` view with no `tree` block is bound to nothing: the renderer falls back to literal default field names, which works only if the object happens to declare them — on any other object the view renders empty while authoring reports success.
after:  A `tree` view with no `tree` block falls back to literal default field names, so it renders empty on any object that does not declare them

map (block, no coords) — before 570, after 189:

before: A `map` view whose `map` block declares neither `locationField` nor the `latitudeField`/`longitudeField` pair is bound to nothing, and the renderer does not guess: objectui `ObjectMap.tsx` applies its `hasCoordinateBinding` gate to a declared block exactly as to an absent one, so the view renders its "Map configuration required" refusal instead of markers — while the block parses and publishes clean, because `ListMapConfigSchema` requires neither form. Declare `map.locationField`, or both `map.latitudeField` and `map.longitudeField` (half a pair is not a binding).
after:  A `map` block with neither `map.locationField` nor both `map.latitudeField` and `map.longitudeField` shows the "Map configuration required" screen (`ObjectMap.tsx` — `hasCoordinateBinding`)

view/tree-without-parent-field

tree (no parent) — before 592, after 190:

before: A `tree` view with no resolvable parent pointer renders FLAT, not empty: `parentField` is undeclared and the bound object declares neither a `tree` field (with no `reference`, or one naming this object) nor a lookup/master_detail back to itself, so the renderer's auto-detection finds nothing (objectui `ObjectTree.tsx` — `detectParentField`) and `buildForest` makes every record a root at depth 0. The result is a complete, correct-looking table with an expand slot that never opens, while authoring reports success. Declare `tree.parentField`, or add a self-referencing field to the object.
after:  A `tree` view with no resolvable parent pointer renders flat, every record at depth 0: `parentField` is undeclared and `detectParentField` (objectui `ObjectTree.tsx`) finds no self-reference

view/row-color-without-colors

grid rowColor field=status, no colors — before 793, after 172:

before: A `grid` view whose `rowColor` binds `status` and declares no `colors` map never colours a row: the grid's row-className resolver returns before it reads a record (objectui `useRowColor.ts` — `if (!config?.field || !config.colors) return undefined`), so every row keeps the default background while parsing and publishing report success. An empty `colors: {}` is the same dead shape spelled out — it passes that guard and then matches no value. The map is what does the colouring; the field only says which value to look up. Each value is a colour NAME from the resolver's own vocabulary (`red`, `blue`, `slate`, …) or a complete Tailwind background class (`bg-red-200`) — a hex parses, publishes and silences this very rule while still colouring nothing (`view/row-color-unresolvable-value`).
after:  A `grid` view whose `rowColor` binds `status` and declares no `colors` map never colours a row (`useRowColor.ts` — `if (!config?.field || !config.colors) return undefined`)

view/row-color-unresolvable-value

grid rowColor 1 hex value — before 726, after 194:

before: A `grid` view whose `rowColor` binds `status` declares 1 colour value the renderer resolves to nothing: `open` = "#ff0000". objectui `useRowColor.ts` — `colorToClass` — hands a `bg-`-prefixed literal through untouched and otherwise looks the lower-cased, trimmed value up in its own closed vocabulary of colour NAMES, returning `undefined` for everything else; Tailwind v4 has no runtime, so no class can be fabricated from a hex. A map like this CLEARS the `!config.colors` guard, so `view/row-color-without-colors` goes quiet, and every row still keeps its default background while parsing and publishing report success. Write a colour name (`red`, `blue`, `slate`, …) or a complete Tailwind background class (`bg-red-200`).
after:  A `grid` view's `rowColor` on `status` has 1 colour value the renderer resolves to nothing (`useRowColor.ts` — `colorToClass`), which silences `view/row-color-without-colors`: `open` = "#ff0000"

webhook/without-triggers

webhook — before 594, after 182:

before: A webhook with no `triggers` never fires on any path. The auto-enqueuer drops it while building its subscription cache (`auto-enqueuer.ts` — `if (triggers.size === 0) … return null`), and there is no manual fire path to reach it either: `webhook.zod.ts` records that the `api` trigger was removed because "no manual fire path exists — the only webhook HTTP surface re-queues already-failed deliveries". The webhook materializes into `sys_webhook`, looks armed in Setup, and delivers nothing. To disable a webhook use `isActive: false`; an empty `triggers` is not an off switch, just a dead one.
after:  A webhook with no `triggers` never fires: `auto-enqueuer.ts` drops it (`if (triggers.size === 0) … return null`) and no manual fire path exists; to turn it off, use `isActive: false`

Tests and gates (all local; this branch)

  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2: Test Files 680 passed (680), Tests 19655 passed | 1 todo (19656), run at 7386db03f. The only later commit, b3598b011, adds the changeset.
  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: Test Files 128 passed (128), Tests 5863 passed (5863), run at 7386db03f.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 test/explain-rule-id.test.ts: 12 passed, against the rebuilt lint dist. every rule the pointer can name resolves through the command to its own explanation iterates all 11 keys, including the nine new ones.
  • pnpm --filter @objectstack/lint run typecheck and pnpm --filter @objectstack/spec run typecheck: exit 0. check:test-typecheck held at 2/6/2 (lint) and 52/246/135 (spec).
  • Builds: turbo run build --filter=@objectstack/lint... (spec, formula, sdui-parser, lint), then --filter=@objectstack/core... and --filter=@objectstack/objectql... for the dist-reading gates. git status stayed clean after each, with no generated artifact moved. The moved strings feed no generated file or docs table: no .describe() and no export changed, and git grep finds no copy of the old message text outside the predicate and its own tests.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at b3598b011 derived 86 commands. The printed artifact-roster block adds 51 more (one duplicate), so 136 ran, each with its exit code captured before any pipe.
    • 131 exited 0.
    • check:dual-build-cjs-loads and check:lean-entry-closure first exited 3 (PREREQUISITE NOT MET, missing dists). After the builds above, both exited 0 on rerun (107 entries / 66 packages; 15 packages for objectql/core).
    • check-partof-closing-keyword exited 2 (NOT WIRED, no PR context). It was rerun with PR_BODY set to this body: exit 0, "this PR carries no Part-of/closing-keyword contradiction".
    • check-closing-target-claim and check-single-claim-paths exited 2 (NOT WIRED: they need PR_NUMBER and a token). NOT MEASURED locally; their guard workflows run them on this PR.
    • dispatch-gates --ran: exit 0, "86 derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED". The two NOT-WIRED roster guards are recorded as NOT-MEASURED rows outside the derived 86
  • ESLint, narrowed to the diff: npx eslint --no-inline-config --format json over the 5 changed .ts files. The JSON report lists 5 files, 0 errors, 0 warnings, and none is ignored. eslint.config.mjs never enables type-aware linting (its comment at :327), so this diff cannot change the verdict on any untouched file.
  • Control-character scan of every changed file: no match.

Next slices left on #22161

The card stays open. These are named here, not built:

  • 124 more author-time rule ids in packages/lint whose fired message exceeds about 200 characters (stage 1's "Second stage" list, with measured lengths).
  • 19 in validate-expressions.ts, lint-flow-patterns.ts and validate-flow-template-paths.ts, serial behind open PRs.
  • The action-governance boot-log lines in packages/objectql/src/action-governance.ts (:568 / :584).

Acceptance notes

  • ADR-0078 §6 is kept, not reversed. The ADR says each rule names its runtime skip site in the finding's own message, and its tests pin that. Every verdict here keeps that citation, so no superseding ADR is needed.
  • Runtime 422 coverage was read from source, not booted. The lookup / select refusal comes from the object schema's parse before the authoring gate. That conclusion is a source reading of protocol.ts and field.zod.ts. The lint-level gate itself still produces those findings, as measured with runRuntimeAuthoringRules.
  • The rule: line is still long. With the pointer it runs about 150 to 190 characters for these ids, for example rule: view/layout-without-binding at views[0].list.calendar — .... That is stage 1's printer shape, unchanged here. Noted, not filed.

Generated by Claude Code

claude added 2 commits October 8, 2026 23:46
…ning moves to `os explain RULE_ID`

The nine rule ids the ADR-0078 completeness predicate emits printed a
239-793 character message on every `os validate` / `os build` / `os dev`
run. Each finding now carries one verdict sentence that still names the
runtime site making it true (ADR-0078 §6) and its unchanged one-line fix;
the long reasoning is each id's entry in RULE_EXPLANATIONS, which
`os explain RULE_ID` prints and the `rule:` line points at.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m 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

15 anchor(s) derived from 2 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
  • 7 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 — 139 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 bb4f5cc0058d1b787120ebd5f505ada79ec5a865 → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 01:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 01:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 9fe7448 Oct 9, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22161-s2-functional-completeness branch October 9, 2026 02:13
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants