Repository navigation
[finding] suggest / distance ("did you mean?") is hand-copied four times across packages/lint — the same shape #4330 already fixed for SYSTEM_FIELDS #14268
Description
Activity
Triage (R+89, triage seat, session
session_019kDRpB7D2XzVzkaLp57T5D): graded →pm:queue·priority:p3·domain:devx· type Task.findingremoved.Lands in:
packages/lint/src— pointvalidate-object-references.ts,validate-sortable-fields.tsandvalidate-widget-bindings.tsat the helperobject-graph.tsalready exports on the barrel, and delete the three private copies. The one decision the consolidation forces is ruled here so the implementer does not have to: the containment pre-pass becomes the shared helper's behaviour for every caller. Rationale: it only ever adds a suggestion where the edit-distance budget returns none (suggestion text; no accept set moves), and "a different quality of hint depending on which position you typo'd" is the drift the card exists to end.validate-widget-bindings.test.ts'ssum_amountpin stays as the regression net; the other three rules' suggestion assertions must stay green byte-for-byte except where the pre-pass now supplies a hint that was previously absent — list those in the PR body.Size/model suggestion: S mechanical,
sonnet/opus.
Generated by Claude Code
Claim: session
session_01WLJQhde67SeTccsmnBVarV(domain:devx execution seat, seat post #6023) — R1 wave 8.- Branch:
claude/issue-14268-lint-suggest-consolidation - Worktree:
../objectstack-14268(dedicated per-task worktree, offorigin/main) - Domain:
domain:devxper the triage (packages/lintis a devx package; nopackages/specchange) - File surface (the three rules the ruling names + the helper + their tests):
packages/lint/src/object-graph.ts(nearestNamegains the containment pre-pass — the ruled shared behaviour; itsdistancestays the one edit-distance body),packages/lint/src/validate-object-references.ts,packages/lint/src/validate-sortable-fields.ts,packages/lint/src/validate-widget-bindings.ts(each importssuggestNamefrom./object-graphand deletes its privatesuggest/distance/levenshtein/didYouMean), plus their.test.tsfiles andobject-graph.test.tsonly where the ruling's "previously absent hint now supplied" clause applies (every such assertion listed in the PR body). ⛔packages/lint/src/index.tsunchanged (nearestName/suggestNameare already on the barrel at:533-534). ⛔ The other private copies inpackages/lint/src(see serial constraints) are NOT in this card's surface — report them, do not consolidate them here. - Premise re-verified on
origin/mainfed4fa409:object-graph.ts:367export function nearestName,:381export function suggestName,:386privatedistance, barrelindex.ts:533-534; private copies atvalidate-object-references.ts:130/:145,validate-sortable-fields.ts:342/:356,validate-widget-bindings.ts:299/:322/:339(the containment pre-pass isdidYouMean:322-337:input.length >= 3 && (c.includes(input) || input.includes(c))scored by length delta, else Levenshtein withinmax(2, floor(len/3))scored100 + d); thesum_amountpin isvalidate-widget-bindings.test.ts:134-135(Did you mean "sum_amount"?foramount). Premise holds. - Container & model: subagent lane (S), sonnet.
- Clause ②: not reached — suggestion text only; no accept set moves (the ruling says so explicitly).
- Serial constraints: no in-flight PR of this seat touches
packages/lint/src(PR fix(lint): wire test layer into check:test-typecheck (#14173) #14494, the last one that did, merged asfa7292c43). Census note for the PR body / report:git grep -n "^function suggest\|^function distance\|^function levenshtein" origin/main -- packages/lint/srcalso finds copies invalidate-action-name-refs.ts,validate-ai-tool-references.ts,validate-chart-bindings.ts,validate-searchable-fields.ts,validate-translation-references.ts— the card counted four, the tree has more; those stay out of scope (the ruling names three rules) and go intoout_of_scope_findings. Changeset:@objectstack/lintpublishes ⇒ apatchchangeset is required (hint text may gain a suggestion where none was offered before).
Generated by Claude Code
- Branch:
- added a commit that references this issue
on Sep 2, 2026 os-dev-report
{ "issue": 14268, "status": "done", "branch": "claude/issue-14268-lint-suggest-consolidation", "pr": "https://github.com/objectstack-ai/objectstack/pull/14575", "premise_still_valid": true, "summary": "Ported the containment pre-pass (from validate-widget-bindings.ts's private didYouMean) into object-graph.ts's nearestName, per the ruling's decision that it becomes the shared helper's behaviour for every caller. Deleted the three private suggest/distance (or didYouMean/levenshtein) copies in validate-object-references.ts, validate-sortable-fields.ts and validate-widget-bindings.ts; all three now import suggestName from ./object-graph. index.ts unchanged (nearestName/suggestName already on the barrel). A/B test comparison on the pre-merge base tree (fed4fa409): full @objectstack/lint suite run with the four files at origin/main state (93 files / 2812 passed, 5 skipped) and again with the port applied -- byte-identical results, confirmed by restoring via `git checkout HEAD -- PATHS` after the A run (git diff HEAD empty). Zero suggestion assertions flipped, so the 'Hints newly supplied' list in the PR body is empty -- none of the three rules' existing fixtures happen to exercise a containment-eligible typo the edit-distance budget didn't already catch. Re-verified on the final merged tree (after two origin/main merges, one of which touched packages/spec's platform-object-names registry validate-object-references.ts reads): 93 files / 2817 passed, 0 skipped, clean build and typecheck.", "tests": "pnpm --filter @objectstack/lint build -- PASS (dist emitted, check-dts-emitted 4/4). pnpm --filter @objectstack/lint typecheck -- PASS (tsc --noEmit + check:test-typecheck). pnpm --filter @objectstack/lint test -- PASS, 93 files / 2817 tests passed, 0 skipped, on final merged HEAD 17d9e56a0. A/B reverse-verification: same suite run against the four files restored to origin/main (fed4fa409) state -- 93 files / 2812 passed / 5 skipped, byte-identical to the post-port run on the same base; restoration confirmed via `git diff HEAD` empty after `git checkout HEAD -- PATHS`. git grep -n \"function suggest|function distance|function levenshtein|function didYouMean\" on the three rule files -- zero hits. pnpm lint narrowed to the 5 touched files (--format json): 0 errors, 1 informational warning (.changeset/*.md has no ESLint config, not a finding). eslint.config.mjs enables no type-aware linting anywhere (no parserOptions.project), so the narrowed run is a complete reading of this diff's lint surface. Note: the gate union run, build/typecheck/test cycle and eslint run were all performed with the changeset file already present on disk (git-add'ed and committed immediately after, with no content change) -- HEAD at gate-run time was 4eaea4919, final HEAD after the trivial changeset-commit is 17d9e56a0; no tracked content differs between the two besides the changeset file's git status.", "mcp_calls": 0, "gates": { "node scripts/check-adr-0087-registration.mjs": "RAN-PASS (exit 0)", "node scripts/check-changeset-no-major.mjs": "RAN-PASS (exit 0)", "node scripts/check-ci-filter-parity.mjs": "RAN-PASS (exit 0)", "node scripts/check-comment-mask-adoption.mjs": "RAN-PASS (exit 0)", "node scripts/check-cross-package-test-inputs.mjs": "RAN-PASS (exit 0)", "node scripts/check-empty-changeset.mjs": "RAN-PASS (exit 0)", "node scripts/check-keyed-text-bounds.mjs": "RAN-PASS (exit 0)", "node scripts/check-plugin-teardown-shape.mjs": "RAN-PASS (exit 0)", "node scripts/check-shard-attestation.mjs": "RAN-PASS (exit 0)", "node scripts/check-system-context-census.mjs": "RAN-PASS (exit 0)", "node scripts/check-test-completeness.mjs": "NOT-MEASURED (exit 3, PREREQUISITE NOT MET -- needs a saved `turbo run test` log CI produces; unreachable locally by design)", "node scripts/check-undeclared-dep-imports.mjs": "RAN-PASS (exit 0)", "node scripts/docs-audit/check-affected-docs.mjs": "RAN-PASS (exit 0)", "node scripts/docs-audit/check-drift-comment.mjs": "RAN-PASS (exit 0)", "node scripts/pm/check-half-states.mjs": "RAN-PASS (exit 0)", "node scripts/pm/release-rehearsal-clone.mjs --self-test": "RAN-PASS (exit 0)", "pnpm check:changeset-gate-self-tests": "RAN-PASS (exit 0)", "pnpm check:cross-package-test-inputs": "RAN-PASS (exit 0)", "pnpm check:doc-authoring": "RAN-PASS (exit 0)", "pnpm check:dual-build-cjs-loads": "NOT-MEASURED (exit 3, PREREQUISITE NOT MET -- needs a full monorepo `pnpm build` of ~78 packages' dist/; out of this PR's local scope)", "pnpm check:logger-receiver-detach": "RAN-PASS (exit 0)", "pnpm check:objectui-changeset": "RAN-PASS (exit 0)", "pnpm check:page-declaration-shape": "RAN-PASS (exit 0)", "pnpm check:pm-half-states": "RAN-PASS (exit 0)", "pnpm check:published-files": "RAN-PASS (exit 0)", "pnpm check:slot-lookup": "RAN-PASS (exit 0)", "pnpm check:test-source-alias": "RAN-PASS (exit 0)", "pnpm check:type-source-resolution": "RAN-PASS (exit 0)", "pnpm lint (narrowed to 5 touched files)": "RAN-PASS (0 errors, 1 informational warning on .changeset/*.md -- no ESLint config for .md, not a finding)" }, "files_touched": [ "packages/lint/src/object-graph.ts", "packages/lint/src/validate-object-references.ts", "packages/lint/src/validate-sortable-fields.ts", "packages/lint/src/validate-widget-bindings.ts", ".changeset/lint-suggest-name-consolidation.md" ], "changeset": "lint-suggest-name-consolidation.md -- @objectstack/lint: patch", "open_questions": [], "out_of_scope_findings": [ "packages/lint/src/validate-action-name-refs.ts:84 (distance) / :101 (suggest) -- private copy, not in this card's surface per the ruling", "packages/lint/src/validate-ai-tool-references.ts:70 (distance) / :87 (suggest) -- same", "packages/lint/src/validate-chart-bindings.ts:83 (distance) / :100 (suggest) -- same", "packages/lint/src/validate-searchable-fields.ts:275 (suggest) / :289 (distance) -- same", "packages/lint/src/validate-translation-references.ts:159 (distance) / :186 (suggest) -- same", "packages/lint/src/validate-react-page-props.ts:167 (nearestKnown) -- same shape, different name", "packages/lint/src/validate-rule-schema-formats.ts:266 (nearestRegisteredFormat) -- same shape, different name" ] }Generated by Claude Code
Generated by Claude Code
ACCEPT — PR #14575 merged to
mainasd7548298a(out of the merge queue 14:13:52Z;added_to_merge_queue13:48:01Z). Sessionsession_01WLJQhde67SeTccsmnBVarV(domain:devx execution seat, seat post #6023), R1 wave 8.Close-out probes on
origin/main996cb1d48(verify worktree,d7548298aan ancestor):git grep -n "function suggest\|function distance\|function levenshtein\|function didYouMean" origin/main -- packages/lint/src/validate-object-references.ts packages/lint/src/validate-sortable-fields.ts packages/lint/src/validate-widget-bindings.ts→ 0 hits (the three private copies are gone).git grep -n "suggestName" origin/main -- packages/lint/src/…→object-graph.ts:394export function suggestName(target, known)(the shared helper, with the containment pre-pass insidenearestName); imported atvalidate-object-references.ts:72,validate-sortable-fields.ts:237,validate-widget-bindings.ts:13and called at every former private-helper site (:175/:195,:440,:642/:645/:809/:855/:857/:905).patchchangeset for@objectstack/lintin the merge; thesum_amountpin survives per the PR's test run.
Against triage 5502887598: the three rules go through the barrel helper; the containment pre-pass is now the SHARED behaviour; no accept-set change. The five remaining private copies elsewhere in
packages/lint/srcare #14577's (filed as the follow-up, awaiting triage). Zero rework.pm:dispatchedremoved with this comment; the card closes via the PR'sFixes #14268.
Generated by Claude Code
- added a commit that references this issue
on Sep 2, 2026
Recorded while implementing #14105 (PR #14267). Not fixed there: consolidating it touches rules that card does not otherwise change, so it is filed rather than ridden along.
What was measured
Four independent copies of the same Levenshtein "did you mean?" helper live in
packages/lint/src, none of them exported, each re-deriving the same edit-distance budget:validate-object-references.tssuggest(target, known)+distance(a, b), budgetmax(2, floor(len/3))validate-sortable-fields.tssuggest(target, known)+distance(a, b), byte-for-byte the same budgetvalidate-widget-bindings.tsdidYouMean(input, candidates)+levenshtein(a, b)+suggest(...), with an extra containment pre-pass for the ADR-0021amounttosum_amountdriftobject-graph.ts(added by #14267)nearestName(target, known)+suggestName(...)+distance(a, b), the same budget againThe fourth copy is mine, and I wrote it as a copy deliberately rather than importing one of the three: none of them exports its helper, and widening one rule's private function into a cross-rule dependency was outside #14105's declared file surface. The copy is annotated at its definition in
object-graph.tspointing here.Why this is worth a card rather than a shrug
This is exactly the shape #4330 recorded and fixed one constant over — "five hand-copied
SYSTEM_FIELDSlists in packages/lint should derive from the spec's declarations" — and the argument carries: with N copies, the next author improves one of N and the survivors keep the old behaviour. That is not hypothetical here, because the copies have already diverged:validate-widget-bindings.tsscores containment ahead of edit distance, so it suggestssum_amountforamount(11 edits apart, obvious to a human) where the other three suggest nothing at all. Whether that pre-pass is right is a real question — but it is currently a per-rule accident rather than a decision, and an author gets a different quality of suggestion depending on which position they typo'd.Two more copies exist and were checked rather than assumed to be the same question:
@objectstack/formula's unknown-function suggestion and the spec's strict-object alias map are keyed on curated tables, not edit distance, so they are deliberately out of this finding's scope.Shape of the fix
object-graph.tsalready exportsnearestName/suggestNameon the package barrel, so the mechanical route is to point the other three at it and delete their private copies — plus one decision the consolidation forces into the open: whether the containment pre-pass becomes everyone's behaviour or stays declared as a widget-binding-specific one. Each rule's existing suggestion assertions are the regression net;validate-widget-bindings.test.tsin particular pins thesum_amountsuggestion, so a naive merge that drops the pre-pass fails loudly rather than silently degrading messages.No behaviour is wrong today; this is drift-shaped, so it is filed as an observation and left unassigned for triage rather than graded here.