Skip to content

fix(lint): waive only a MISSING object beside a dataSource binding - #20454

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20400-datasource-waiver-missing-only
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20400-datasource-waiver-missing-only

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20400

Clause-②: no

What was wrong

validateComponentProps does not report the props schema's required object when the component carries a dataSource.object binding. Two docblocks scope that waiver to absence: DATASOURCE_SUPPLIED_PROP ("the one prop whose absence this rule does NOT report") and suppliedByDataSource ("is this issue 'the required object prop is missing'"). The code matched on the issue's path alone. Any issue at exactly ['object'] was waived whenever dataSource.object was a non-empty string, without looking at properties.object or at the issue. So a present but wrong object beside a binding was silenced, and the same value without a binding was reported.

Before and after (measured)

Each row is one component in a one-page stack, run through validateComponentProps from source with tsx. Before is origin/main 6e3e5462c6. After is this branch at e994035362.

component before after
element:number, dataSource: { object: 'contact' }, properties: { object: 7, aggregate: 'count' } 0 findings 1 component-props-invalid at properties.object
the same, with no dataSource (control) 1 component-props-invalid at properties.object the same, unchanged
object: null beside the binding 0 findings 1 component-props-invalid at properties.object
no object key beside the binding 0 0 (still waived)
object: undefined beside the binding 0 0 (still waived)
the existing picker pin (element:record_picker, binding, labelField only) 0 0

The change

The landing site is the expected one: the body of suppliedByDataSource in packages/lint/src/validate-component-props.ts, plus its test file and a changeset. After the existing path and binding checks, the function reads absence off the component. It waives only when properties.object is not present or is undefined, which is triage's definition of missing. Any other issue at that path passes through as the props row raised it. Both docblocks are unchanged: their contract sentences already say this, and the renderer sentence is out of scope (see Acceptance notes). The call site is unchanged.

Pins (packages/lint/src/validate-component-props.test.ts)

  • The existing does not report the required object prop when dataSource supplies it is unchanged and green.
  • New, run for object: 7 and for object: null: the same element:number bag is judged beside dataSource: { object: 'contact' } and without it. Each is reported as [COMPONENT_PROPS_INVALID, '...properties.object'], and the two finding lists are deep-equal. The assertions are on rule id, path and equality, never on message text.
  • New: object: undefined beside the binding reports nothing. The same bag without the binding reports properties.object, so the silence comes from the waiver and not from the row accepting undefined.

Reverse verification

The fix was committed first (e994035362). The one predicate line was then mutated back to the old behaviour through scripts/ablation-replace.mjs in WRAP mode, with an absolute-path trap restore around it. The line return props?.[DATASOURCE_SUPPLIED_PROP] === undefined; became return true;, which equals the old strName(dataSource?.object) !== undefined once the new early return has run.

  • On-disk proof: anchor count 1 → 0, mutant count 0 → 1, blob f4793c5782 → 52df0d1033.
  • The run: Tests 2 failed | 46 passed (48). The two failures are the object: 7 and object: null pins (expected [] to deeply equal [ [ 'component-props-invalid', …(1) ] ]). The undefined pin and the existing pin stayed green, as expected, because the old code waived both.
  • The restore: blob after restore f4793c5782 equals the HEAD blob, git diff HEAD is 0 bytes, and git status --porcelain is empty.
  • The test imports the rule by relative path (./validate-component-props.js, resolved to src/), so no dist/ leg applies.

Verification (all at e994035362)

  • Build: pnpm --filter '@objectstack/lint^...' build through os-verify-lock.sh, VERDICT command-exit 0.
  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-component-props.test.ts: Tests 48 passed (48).
  • pnpm --filter @objectstack/lint test: Test Files 115 passed (115), Tests 5329 passed | 5 skipped (5334).
  • pnpm --filter @objectstack/lint typecheck: exit 0. tsc --noEmit is clean. check:test-typecheck is OK with the ledger unchanged at 2 files, 6 errors and 2 signatures. --listFiles shows the edited test file is in the tsconfig.test.json program (1 hit) and is not a ledgered file.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 60 commands. Each was run with its exit code captured before any pipe, and the results were reconciled with --ran: 60 derived famil(ies) accounted for — 58 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).
    • 58 exited 0. That count includes check:doc-authoring, check:nul-bytes, check:adr-0087-registration --base origin/main, check:changeset-no-major --base origin/main, check:empty-changeset and check:changeset-gate-self-tests.
    • check:docs-transcript-drift and check:lean-entry-closure first answered PREREQUISITE NOT MET. They exited 0 after pnpm --filter @objectstack/lint build and turbo run build --filter=@objectstack/objectql.
    • NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt. Reason: both refuse without the whole workspace built (84 and 28 packages have no dist/). That build is CI's (Build Core, Lint & Repo Gates), not a local targeted run.
  • The four roster gates the derivation flags for directories this diff touches all exited 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing and check:filter-alias-parity.
  • Published surface: @objectstack/lint ships files: ["dist", "README.md", "CHANGELOG.md"]. After a build, the new line (built spelling return props?.[DATASOURCE_SUPPLIED_PROP] === void 0;) appears once in each of dist/index.js, dist/index.cjs, dist/runtime.js and dist/runtime.cjs. The positive control function suppliedByDataSource(issue, component) also appears once in each. So this publishes, and it takes a patch changeset.
  • Diff size: +84 / -1 across 3 files.

Acceptance notes

  • null is reported. Triage defined missing as "no key, or undefined". object: null is a present value that z.string() rejects, so it now reaches the author, like object: 7. It is pinned beside 7 so the boundary stays explicit.
  • The renderer sentence is untouched. DATASOURCE_SUPPLIED_PROP's docblock says objectui's element renderers "read it FIRST". That is still false for element:number until objectui#10909 lands. objectui#10909 is not addressed here: its fix is that renderer, per triage's out-of-scope list.
  • No per-type table. Restricting the waiver to types whose renderer honours the binding would be a new per-type table on this rule. Triage ruled it out of this card.
  • The two readers now agree on this input. objectui's mirror of this waiver (PR objectui#10908) already refuses object: 7 beside a binding, following the docblock. After this change, this gate refuses it as well.
  • main moved after the base. It gained 7fa3e3e07c and 8cdbe0c6e5, which touch packages/rest and packages/metadata-protocol only. Neither touches packages/lint or packages/spec, so the branch was not merged forward and the merge queue arbitrates.
  • PR feat(spec): ComponentPropsMap rows for action:button/group/menu/icon and element:definition-list/repeater #20420 is text-disjoint. It adds ComponentPropsMap rows that this rule reads, but it does not touch this file.

Generated by Claude Code

`suppliedByDataSource` matched the props row's issue by path alone, so a
present-but-wrong `object` (`object: 7`, `object: null`) beside a
`dataSource.object` binding was silenced, while the same value with no binding
was reported. Its docblocks scope the waiver to absence; the code now reads
absence off the component (no key, or `undefined`) and lets every other issue
at that path through unchanged.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
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 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

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 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 3cf64493899458632f87e661fff1b130bd3a8273 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3cf64493899458632f87e661fff1b130bd3a8273

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e99403536274807c6e527e228be938c3a3100bc9
Local-runs: none

Inputs: card #20400 (body, triage 5866690642, claim 5870756749, os-dev-report 5871456292, ACCEPT 5871502523), PR #20454 (body, its one comment, 3-file list, net diff against main at the head: +84 / -1), and the 34 check-runs on the head. Read-only: git objects and the API only; nothing built, run or re-run.

Checks on the head: all seven required contexts are success (Lint and Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard). Every other check-run is success or skipped (Console Pin Gate, Build Docs, Packed-tarball smoke: skipped by design). No failure. The file list touches no governed surface.

① Derived judgments

  1. Waiver narrowed to absence. suppliedByDataSource now returns true only when the issue sits at path object, dataSource.object is a non-empty string, AND component.properties.object is absent or undefined, read off the component rather than the issue. Right: this is the docblock's own contract and triage's direction verbatim (no key, or undefined); neither docblock is edited.
  2. A present-but-wrong object beside a binding is now reported as component-props-invalid at properties.object, byte-equal to the finding without a binding. Right, and pinned twice (7, null) with a no-binding control and a deep-equality assertion on the two finding lists; assertions are on rule id and path, never message text.
  3. null counts as present. Right: triage defined missing as no key or undefined; null is a value the row's z.string() rejects, so the row's verdict reaches the author. JSON carriers cannot spell undefined, so for them missing equals key-absent, which the code also honours.
  4. object: undefined beside a binding stays waived, with a no-binding control proving the silence is the waiver and not the row. Right.
  5. The existing picker pin (binding present, properties.object absent) is unchanged and stays green. Right.
  6. Rows in reach. Root-level object is declared on element:number (required), element:record_picker (required) and element:metadata_viewer (optional); each is a plain z.string(), none with a min length. The nested object under record:related_list's add-existing picker sits at a deeper path and was never in the waiver. So the accept-set change is exactly: on those three rows, a bound component whose authored object the row rejects now gets one advisory warning. Nothing else moves.
  7. Severity unchanged: ComponentPropsSeverity stays the single literal 'warning'; the rule stays tier: 'advisory', commands: ALL, surfaces: CLI_ONLY. Right, and the changeset's naming of os validate, os lint and os build matches the wiring (validate.ts runs the validate command set, lint.ts the lint set, compile.ts the build set, and build is an alias of compile).
  8. Public surface: no export added, removed or renamed in @objectstack/lint; suppliedByDataSource is module-private and its one call site is untouched; no spec file, no ADR-governed decision reversed (ADR-0078 governs the rule, and the change enforces its declared contract). Right.
  9. Corpus: the one component in the example corpus that binds through dataSource (examples/app-showcase/src/ui/pages/page-variables.page.ts, the showcase record picker) authors no properties.object, so it stays waived and no shipped page gains a finding. Consistent with the green Dogfood gate.
  10. Test hygiene: the new pins import the rule by relative path, read nothing outside the package, and the code comment inside the function carries no tracker number. Right (Lint and Repo Gates green on the head).

Every derived judgment in the code and the pins is right. The one wrong claim is in the changeset prose, judged under ②.

② Semver level

  • Changeset: .changeset/20400-datasource-waiver-missing-only.md, @objectstack/lint: patch. The package publishes (files: dist, README.md, CHANGELOG.md; version 17.4.0 at the head), the change is a bug fix that pulls an advisory rule back to its declared contract, no export moves. patch is the right level; skip-changeset would be wrong and is not used.

  • Declaration: the PR body and the changeset both read Clause-②: no with no direction arm. Right: the diff widens no accept set and enlarges no public surface, and the narrowing is of a defect (the waiver over-matching its docblock), not of a published accept set, so no (narrowing) arm is owed. Check Changeset and the check:changeset-no-major / check:adr-0087-registration gates inside Lint and Repo Gates are green on the head.

  • Prose, judged sentence by sentence against the diff: the title line, the description of the old path-only match, the new definition of missing (no key or an explicit undefined), and the sentence that a written value is judged as written are all accurate. The closing sentence is not:

    WRONG: "The rule stays advisory, so nothing that validated before is refused." The first half is true; the conclusion is false for the documented CI invocations. os validate --strict and os lint --strict (both spelled in content/docs/deployment/cli.mdx, the former as a GitHub Actions step) treat warning-severity findings as failures: in validate.ts this rule's findings go through splitBySeverity into ruleAdvisories, the first member of the warningsSoFar() list the --strict exit reads; in lint.ts the same findings enter issues and failing = errors + (strict ? warnings : 0). A stack carrying a bound component with object: 7 (or null) exited 0 under --strict before this change and exits 1 after it. The @objectstack/lint CHANGELOG already spells this exception whenever a new warning ships, including in a 17.3.0 patch entry ("Under os validate --strict the new warnings are failures, as every warning in this family is"), so the correct wording is customary, not novel. This is the text an upgrading agent greps after a red pipeline, and it would read there that no refusal is possible.

    Fix: one sentence in the changeset, for example replacing the closing sentence with: "The rule stays advisory: without --strict nothing that validated before is refused; under os validate --strict or os lint --strict the new warning fails the run, as every warning does." The level (patch) and the declaration (no) stand as they are.

③ Boundary flags

  • open_questions: none in the report. Nothing to answer.
  • Out-of-scope finding, properties.object: '' beside a binding reports nothing before and after: confirmed from the spec at the head, every root-level object row is a plain z.string() that accepts the empty string, so the row raises no issue for the waiver to keep or release. That is the row's declared accept set, not a contract violation and not a trap with a named producer; the ACCEPT's disposition (noted, not filed) stands. Not this card.
  • Deviation, commit trailer amended before push: the head commit carries Claude-Session: plus Co-authored-by: Claude and no model identifier. Answered.
  • Deviation, main advanced after the base: the paths that moved between the merge base and origin/main are .changeset/, content/docs/, packages/metadata-protocol/ and packages/rest/ only; nothing under packages/lint or packages/spec. No merge-forward owed; the queue rebuilds on current main. Answered.
  • Deviation, two derived gates NOT MEASURED locally (check:dual-build-cjs-loads, check:type-check-debt): both need the whole workspace built, and the head's Lint and Repo Gates, Build Core and Type Check debt ledger runs are green. Answered by the check-runs.
  • Deviation, labels: only the assignee was written by the dev; documentation, tests, tooling, size/s are the path and size labelers' rows. tooling on a packages/lint diff is the labeler's reading, not a lane claim, and triage's domain:spec routing stands. No action.
  • Deviation, scratch probe outside the tree and cleanup order: the file list is the three files named; nothing temporary landed. Answered.
  • Acceptance notes in the PR body: null reported (judged right in ①.3); renderer sentence untouched and no per-type table (right, triage's out-of-scope list, objectui#10909 owns the renderer); the objectui mirror (objectui#10908) is outside this review's inputs and is not judged here; PR feat(spec): ComponentPropsMap rows for action:button/group/menu/icon and element:definition-list/repeater #20420 text-disjoint (the single-writer-path check on the head is green).
  • Escalations: none. The one FAIL reason above is a prose correction inside the dev's own file surface, not a boundary breach.

Implemented-by: claude/issue-20400-datasource-waiver-missing-only
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: FAIL

FAIL reason (one): the changeset's closing sentence claims nothing that validated before is refused, which is false under the documented os validate --strict and os lint --strict invocations; correct that sentence (wording under ②) and re-request the review on the new head. Code, pins, level and declaration are all judged right and need no change.


Generated by Claude Code

The closing sentence said nothing that validated before is refused, which is
false under `os validate --strict` and `os lint --strict`: both fail on any
warning, so a bound component with a wrong-typed `properties.object` passed
there before and fails now. State both halves.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 89ae5540b5b586842cad69c81a695894ba6dcd48
Local-runs: none

Inputs: card #20400 (body; triage 5866690642; claim 5870756749; os-dev-report 5871456292; ACCEPT 5871502523; REWORK 5872106000; round-1 os-dev-report 5872175169), PR #20454 (body, the docs-drift comment 5871406238, the round-0 at-tier record 5872081009 which FAILED one changeset sentence at e994035362, the 3-file list, and the net diff against main at the head over merge base 6e3e5462c6: +86 / -1), and the check-runs on the head. Read-only: git objects and the API only; nothing built, run or re-run.

Round 1 is one commit (89ae5540) that replaces the changeset's closing sentence and nothing else: git diff e994035362 89ae5540b5 -- packages/ is empty, so the rule file and the test file are byte-identical to the round-0 head, and the code and pin judgments below are re-read against the same bytes rather than carried over.

Checks on the head, read 2026-09-28T14:46:25Z: 33 check-runs. success: Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate (rollup and 1/3, 2/3, 3/3), Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Temporal Conformance (live PG + MySQL), Test Core (2/6, 4/6), The card this PR closes must claim this branch, Type Check (consumer gates, debt ledger, source gates, workspace, rollup), filter. skipped by design: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in). Still in_progress at that reading: Lint and Repo Gates, Test Core (1/6, 3/6, 5/6, 6/6). No failure at that reading. The same families were all success on the round-0 head, whose packages/ bytes this head shares. The file list touches no governed surface (Governed Surface Queue Guard success).

① Derived judgments

  1. Waiver narrowed to absence. suppliedByDataSource now returns true only when the issue sits at path object (length 1), dataSource.object is a non-empty string, AND component.properties.object is absent or undefined, read off the component rather than off the issue. Right: this is what both docblocks already declare ("the one prop whose ABSENCE this rule does NOT report"; "is this issue the required object prop is MISSING") and triage's direction verbatim (no key, or undefined); neither docblock is edited, and the renderer sentence stays out as fenced.
  2. A present-but-wrong object beside a binding is now reported as component-props-invalid at properties.object, byte-equal to the finding the same bag gets without a binding. Right, and pinned twice (7, null) with a no-binding control and a deep-equality assertion on the two finding lists; assertions are on rule id, path and equality, never on message text.
  3. null counts as present. Right: triage defined missing as no key or undefined; null is a written value the row's z.string() rejects, so the row's verdict reaches the author. A JSON carrier cannot spell undefined, so for it missing equals key-absent, which the code also honours.
  4. object: undefined beside a binding stays waived, with a no-binding control proving the silence is the waiver and not the row accepting undefined. Right.
  5. The existing picker pin (binding present, properties.object absent) is unchanged and stays green. Right.
  6. Rows in reach at the head. ComponentPropsMap declares a root-level object on exactly five rows: element:number (required z.string(), component.zod.ts:2083), element:metadata_viewer (optional z.string(), :2163), element:record_picker (required z.string(), :2468), and the two tombstones element:filter (:2348) and element:form (:2401), each retiredKey(...). origin/main has moved that file since the merge base (612 lines, the page:tabs / page:card / page:accordion rows among them) and still declares the same five root-level object rows, none new. The nested object under record:related_list's add-existing picker (:1303) sits at a deeper path and was never in the waiver. So the accept-set change is exactly: on the three live rows, a bound component whose written object the row rejects now gets one advisory warning; and on the two tombstone rows, a bound component that writes object at all now receives the retired-key prescription that the path-only match used to swallow (retiredKey is z.never(...).optional(): it fires at path object on any written value and is silent on absence, so the fix and the tombstone agree). Both right; nothing else moves.
  7. Severity unchanged: ComponentPropsSeverity stays the single literal 'warning'; the registry row stays tier: 'advisory', commands: ALL, surfaces: CLI_ONLY. Right, and the changeset's naming of os validate, os lint and os build matches commands: ALL (validate.ts, lint.ts and compile.ts each run authoringRulesFor their own command set).
  8. Public surface: no export added, removed or renamed in @objectstack/lint; suppliedByDataSource is module-private and its one call site is untouched; no spec file changes; no ADR-governed decision reversed (ADR-0078 governs the rule, and the change makes its code match its declared contract). Right.
  9. Corpus: the one component in the tree outside tests, spec and lint that binds through dataSource is the showcase record picker (examples/app-showcase/src/ui/pages/page-variables.page.ts:61), which authors no properties.object, so it stays waived and no shipped page gains a finding. Consistent with the green Dogfood gates on this head.
  10. Test hygiene: the new pins import the rule by relative path, read nothing outside the package, use the file's existing stackWith and invalid helpers, and the new docblocks and the code comment inside the function carry no tracker number. Right.
  11. The new line's props?.[...] fallback (which would return true on a component with no properties bag) is unreachable: the call site skips a component whose properties is not a record before safeParse runs, so the waiver never sees a bag-less component. Right, and harmless.

Every derived judgment in the code and the pins is right.

② Semver level

  • Changeset: .changeset/20400-datasource-waiver-missing-only.md, @objectstack/lint: patch. The package publishes (files: dist, README.md, CHANGELOG.md; version 17.4.0 at the head), the change is a bug fix that pulls an advisory rule back to its declared contract, no export moves. patch is right; skip-changeset would be wrong and is not used; Check Changeset is success on the head.
  • Declaration: the PR body and the changeset both read Clause-②: no with no direction arm. Right: the diff puts no new key on any published payload, removes or renames no authorable key, and leaves ComponentPropsMap's accept set untouched: the rule now relays more of the row's existing verdicts, which is a bug fix under the changeset rule, not a narrowing of a published accept set, so no (narrowing) arm is owed.
  • Prose, judged sentence by sentence against the diff and the CLI source at the head. The title line, the description of the old path-only match, the new definition of missing (no key or an explicit undefined), the sentence that a written value is judged as written and reported at properties.object exactly as without a binding (pinned by deep equality), the effect on the three commands (commands: ALL), and the closing sentence that a binding-only component stays clean (the existing pin) are all accurate. The changeset body carries no tracker number; its filename carries the card number as every sibling in .changeset/ does.
  • The sentence the round-0 record FAILED is replaced by the prescribed wording verbatim: "The rule stays advisory: without --strict nothing that validated before is refused; under os validate --strict or os lint --strict the new warning fails the run, as every warning does." Each half holds. validate.ts: strict is "Treat warnings as errors"; this rule's findings go through splitBySeverity into ruleAdvisories, the first member of warningsSoFar(), and into the text-face warnings list, which both faces read for the exit (exit 1 when flags.strict is set and that list is non-empty, else 0, on the JSON face; Strict mode: warnings treated as errors then exit(1) on the text face). lint.ts: failing = errors.length + (strict ? warnings.length : 0), exit 1 when non-zero, and the flag's own prose says "Fail the run (exit 1) on warning-severity findings too". compile.ts (os build) has no --strict at all, only --strict-body, which gates callable bodies, so without the flag nothing is refused there either. "As every warning does" is read as the class the sentence is about, warning-severity findings of the authoring-rule registry, the only warnings this package ships: on that class it holds on both commands (validate.ts: "Every advisory the registry raised. All of them feed --strict now"; lint.ts counts every warning). The CLI's own diagnostics that validate.ts keeps out of the strict list by design (navigation-contribution, permission-set collision, the JSX-gate parse-level notice, protocolVersionGap) are not lint-package warnings and not this package's CHANGELOG's subject; the same generalisation is already this CHANGELOG's custom ("they fail a run only with --strict, as every other advisory does"). Judged right, with that scope stated here so the reading is auditable.
  • Line wrap: the new sentence spans three lines and leaves "A component that binds" closing a short line. Cosmetic; changesets render as paragraphs.

③ Boundary flags

  • open_questions: none in either report. Nothing to answer.
  • Round-1 deviation, line wrap: answered above, cosmetic.
  • Round-1 deviation, mergeable_state reads blocked: the API reads mergeable: true, draft: true; blocked is the draft flag and the required checks, and the dev's driver-free merge-tree probe against 3cf6449389 was clean. Answered.
  • Round-1 deviation, cleanup order: the file list is the three files named; nothing temporary landed. Answered.
  • Round-0 deviations, re-checked on this head rather than carried over: the head commit's trailers are the model-free pair (Claude-Session: plus Co-authored-by: Claude); main has moved since the merge base in packages/lint (other rules, the JSX baseline) and in component.zod.ts, but not in this rule's file or test and not in the five object rows the rule reads (①.6), so no merge-forward is owed and the queue rebuilds on current main; the two locally NOT-MEASURED gates (check:dual-build-cjs-loads, check:type-check-debt) are answered by Build Core and Type Check debt ledger success on this head; the labels documentation, tests, tooling, size/s are the path and size labelers' rows and triage's domain:spec routing stands. Answered.
  • PR body cites e994035362 as the measured head: round 1 changed no code or test, and the packages/ bytes are identical between the two heads, so every measurement in the body stands for 89ae5540. Answered.
  • Out-of-scope finding, properties.object: '' beside a binding reports nothing before and after: confirmed at the head, the three live object rows are plain z.string() with no minimum length, so the row raises no issue for the waiver to keep or release. That is the row's declared accept set, a spec question with no named producer, not this rule's contract and not this card; the ACCEPT's disposition (noted on the card thread with dedupe words, not filed) stands. Not escalated.
  • Docs: the docs-drift comment lists nothing, and no hand-written page under content/docs names component-props-invalid or the waiver, so no page is owed. Answered.
  • Acceptance notes in the PR body: null reported (judged right in ①.3); renderer sentence untouched and no per-type table (right, triage's out-of-scope list; objectui#10909 owns the renderer); the objectui mirror (objectui#10908) is outside this review's inputs and is not judged here; PR feat(spec): ComponentPropsMap rows for action:button/group/menu/icon and element:definition-list/repeater #20420 text-disjoint (single-writer-path check success on the head).
  • Escalations: none.

Implemented-by: claude/issue-20400-datasource-waiver-missing-only
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS

The one FAIL reason of the round-0 record is closed by the sentence now in the changeset; code, pins, level and declaration were re-judged on this head and are right. Landing still waits on every check green: five check-runs were in progress at the reading above.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 14:57
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit d753744 Sep 28, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20400-datasource-waiver-missing-only branch September 28, 2026 15:23
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