Skip to content

fix(lint): narrow data-model rules to the canonical reference, and measure the two readers that stay tolerant - #13322

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-13250-narrow-lint-reference-alias
Aug 30, 2026
Merged

fix(lint): narrow data-model rules to the canonical reference, and measure the two readers that stay tolerant#13322
os-zhuang merged 4 commits into
mainfrom
claude/issue-13250-narrow-lint-reference-alias

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Part of #13250 — this PR closes one of the three doors that card names. The other two are measured and deliberately left alone; the evidence is below, so the card stays open for the maintainer's call on them.

1. What is fixed — the lint reader

packages/lint/src/data-model-rules.ts read:

functionrefOf(def: any): string|undefined{returndef?.reference||def?.reference_to;}

reference_to is a rejected aliasFieldSchema answers it with unrecognized_keys and "Did you mean reference_toreference?" (#11567, "one key, one answer, on both doors"). @objectstack/lint is documented as running over "an in-memory, schema-parsed stack object", so the alias cannot legitimately reach these rules at all: the tolerance was inert. Where it did fire, it made relationship/missing-reference — the rule whose entire job is to catch a relationship with no target — report a valid target for a field that has none.

Narrowed to canonical-only, mirroring the deliberate narrowing already recorded in-file for refOf in packages/lint/src/validate-security-posture.ts (including its typeof r === 'string' guard, which also makes the declared string | undefined return type true — the old || chain returned whatever truthy value it found).

Behaviour delta, only for metadata the spec already refuses:relationship/missing-reference (error) now fires on a relationship field whose only target spelling is reference_to; the rules that need a resolved target (relationship/master-detail-required, rollup/missing-summary, the rest of the relationship family) no longer treat such a field as pointing anywhere. Canonical reference is untouched.

Ablation — the new tests provably fail without the fix

Both legs rebuilt @objectstack/lint and confirmed the mutation on disk and in dist/ before reading any result.

── MUTATION LEG ──
on disk: current-guard=0 (want 0) alias-branch=1 (want 1)
ablation-dist-preflight: marker present in 2 built files -- the ablation is live in the artifact the suite consumes.
mutated-tree vitest exit: 1
Tests 3 failed | 53 passed (56)
FAIL > R1 fires: a relationship whose only target spelling is `reference_to` has no target
FAIL > the resolved target NAME is read from `reference` (not merely truthiness)
FAIL > a non-string `reference` is not a target either
── RESTORE LEG ──
hash-object after restore: f71636000c21b411eff9f89186b566d8e6b14f95 (== HEAD blob)
git diff HEAD -- packages/lint/src/data-model-rules.ts => '' (empty)
on disk: current-guard=1 (want 1) alias-branch=0 (want 0)
ablation-dist-preflight --absent: marker absent from all 14 built files
restored-tree vitest exit: 0
Tests 56 passed (56)

Every alias assertion is paired with a positive control on the canonical spelling (and FieldSchema accepting/refusing each), so a refOf that resolved nothing could not pass.

2. What is NOT fixed, and why — the measurement

⚠️ The card asked whether raw metadata carrying the alias can actually reach the other two readers. It can, at both. Narrowing them is therefore a triage call, not a defect fix, and is left to the maintainer.

Control 0 — the parse door really is shut

FieldSchema.safeParse({... reference_to }) success: false
issue codes: ["unrecognized_keys"]
message: Unrecognized key(s) on this field: `reference_to`. Did you mean `reference_to` → `reference`? …
POSITIVE CONTROL — canonical spelling success: true

So anything observed below arrived through a door that is not the parse door.

Reader A — packages/plugins/plugin-security/src/security-plugin.ts:5846 — REACHABLE

Its population is this.ql.getSchema(object), i.e. SchemaRegistry.getObject. registerObject deliberately skips Zod (#3896 — its own comment: "the registry is the choke point every metadata door goes through, including the ones that skip Zod and lint"), and engine.ts states the consequence outright: a row "came in around the parse seam (a raw registerObject, or a stored/artifact row written before the tightening — the two populations parse-time rejection measurably cannot catch, since the engine registers raw objects and never re-parses)".

Measured, real SchemaRegistry, real round-trip:

[ALIAS ] registerObject -> getObject; field keys: ["type","label","reference_to","required"]
[ALIAS ] reader expression f?.reference ?? f?.reference_to ?? f?.referenceTo => "p_obj"
[CANONICAL] registerObject -> getObject; field keys: ["type","label","reference","required"] ← positive control
[CANONICAL] reader expression f?.reference ?? f?.reference_to ?? f?.referenceTo => "p_obj"

The alias survives registration verbatim and the reader resolves it.

⚠️And narrowing it would not be a silent miss — it would be a loud outage.resolveCbpRelation returning null is fail-closed: the read path returns RLS_DENY_FILTER (0 rows for every non-admin caller) and the write path throws MasterDetailRelationMissingError. So a raw-registered controlled_by_parent object whose relation is spelled with the alias would go from "access derived from its master" to "everything denied / writes throw". That is an availability break on a reachable population, and it is the maintainer's call, not this PR's.

Reader B — packages/verify/src/derive.ts:103 — REACHABLE

Its population is the app config os verify loads. loadConfig() does not parse: it bundleRequires the module and returns mod.default. Parsing happens only if the authored module itself calls a define* helper.

B1. defineStack(cfg) — the DEFAULT strict door:
THROWS: defineStack validation failed … objects.1.fields.parent: Unrecognized key(s) on this field: `reference_to` …
B2. defineStack(cfg, { strict: false }) — the spec's own documented option:
child field keys after load: ["type","label","reference_to","required"]
B3. deriveCrudCases() — did derive READ the alias?
[ALIAS(non-strict)] relationalRefs: [{"field":"parent","target":"p_obj","required":true,"multiple":false}]
[ALIAS(plain object)] relationalRefs: [{"field":"parent","target":"p_obj","required":true,"multiple":false}]
[CANONICAL ] relationalRefs: [{"field":"parent","target":"p_obj","required":true,"multiple":false}] ← positive control

And through the realloadConfig() on a real config file that never calls a define* helper:

loaded: /tmp/os13250-app/objectstack.config.ts
child field keys after loadConfig(): ["type","label","reference_to","required"]
deriveCrudCases -> relationalRefs: [{"field":"parent","target":"p_obj","required":true,"multiple":false}]

If narrowed, such a required relation becomes blocked: 'required lookup field "parent" has no `reference` target' — loud rather than silent, but it converts objects os verify exercises today into objects it reports as blocked. Also a triage call.

3. Verification

Re-verified at 1b9a8834c after the CI-red follow-up (see section 4).

full workspace build exit=0 turbo run build --filter='./packages/*' --filter='./examples/*^...' 66 total
full workspace typecheck exit=0 turbo run typecheck --filter='./packages/*' --filter='./packages/*/*' --filter='./apps/*' 125 total
lint suite exit=0 Test Files 82 passed (82) Tests 2327 passed (2327)
cli dmr suite exit=0 Test Files 1 passed (1) Tests 56 passed (56)
lint typecheck exit=0
cli typecheck exit=0

The entire Lint & Repo Gates job was run locally — all 187 commands, extracted from .github/workflows/lint.yml (single-line run: steps plus every command inside its multi-line run: | blocks). Exit codes captured before any pipe. Exactly one nonzero, and it is not a gate finding:

NONZERO exit=1 node "$RUNNER_TEMP/verify-lanes.mjs"
Error: Cannot find module '/verify-lanes.mjs'

RUNNER_TEMP is a GitHub Actions variable with no value outside a runner, so the path resolved to /verify-lanes.mjs. That is a runner-only step, not a measurement.

Highlights from that sweep, quoting each gate's own verdict line:

pnpm lint exit=0 (eslint . --no-inline-config over the whole repo, silent)
check-reference-carrier-shape exit=0 OK — 5458 file(s) scanned, 508 `reference` site(s).
field-def carrier position: 446 string literal(s), 0 non-string
literal(s) — 11 unjudged (7 non-literal, 4 null/undefined).
check-reference-carrier-shape --self-test exit=0
check:type-check-debt exit=0 OK — 30 ledger entr(ies) re-measured in 374.3s, 1560 raw tsc
error(s) total, none above its recorded number.
surplus: none — every entry sits exactly at its measurement,
so any new error is red.
check:dual-build-cjs-loads exit=0 102 published require entry point(s) across 66 package(s) load;
610 emitted CommonJS file(s) parse.

⚠️check:type-check-debt and check:dual-build-cjs-loads had refused to measure earlier for want of a built workspace; the job's own build steps supplied it, so both are now real greens rather than NOT MEASURED. In particular the ledger re-measure carries surplus: none, which is the direct confirmation that the added test lines move the @objectstack/cli TEST_DEBT ratchet by zero.

Two gates remain NOT MEASURED locally, each by its own refusal text and neither a red: check-test-completeness.mjs (PREREQUISITE NOT MET — this gate grades a saved turbo test log, and no log was named; CI tees one) and the bare check-half-states.mjs (PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential; the --self-test form CI actually runs exits 0).

4. Follow-up: the CI red on 03c514700, and why the fix is a variable binding

node packages/lint/scripts/check-reference-carrier-shape.mjs failed on the new case a non-string reference is not a target either, which wrote reference: { object: 'project' } inline:

packages/cli/test/data-model-rules.test.ts:782
`reference` carries a literal that is not a string: { object: 'project' }

The gate is right about what it sees, and the value must keep being non-string — the whole assertion is that such a value resolves to nothing, which makes the case a deliberate counter-example rather than an authored carrier. So the value is now bound through a variable, with the reasoning recorded at the binding (mirroring the same move in PR #13238).

That is the gate's own doctrine, not an evasion — from its docblock:

- value is a string literal -> PASS under either reading. Immaterial.
- value is not a literal at all -> UNJUDGED under either reading. Immaterial.
- value is a non-string literal -> the readings disagree. REFUSE

Read from the source: classifyValue returns non-literal for an identifier, and both the finding branch and the refuse branch require non-string-literal. ⛔ The case was not deleted, skipped or weakened; the gate was not touched; no path ignore was added (it has no baseline and wants none, by design).

Re-ablated after the binding, because a variable binding is exactly the kind of change that can quietly make an assertion vacuous. With refOf reverted to the || chain, rebuilt, and the mutation confirmed live in dist:

mutated-tree vitest exit: 1 Tests 3 failed | 53 passed (56)
FAIL > R1 fires: a relationship whose only target spelling is `reference_to` has no target
FAIL > a non-string `reference` is not a target either ← still red, so still testing the same thing
FAIL > the resolved target NAME is read from `reference` (not merely truthiness)
restore leg: hash-object == HEAD blob, `git diff HEAD` empty, dist marker absent, 56 passed (56)

⚠️Process note worth keeping. This gate was NOT in the 29 families scripts/pm/dispatch-gates.mjs derived for this diff. Two reasons compounded: it is a package-local script (packages/lint/scripts/…, outside the root check:* namespace), and it is invoked inside a multi-line run: | block. A derived family is a cheap first half, not the farm.

Scope

Generated by Claude Code


Generated by Claude Code

`refOf` in `packages/lint/src/data-model-rules.ts` read
`def?.reference || def?.reference_to`, so a relationship field spelled with
the REJECTED alias resolved a target. #11567 settled that `reference` is the
only relationship spelling `@objectstack/spec` declares and `reference_to` is
answered with `unrecognized_keys` ("one key, one answer, on both doors").
`lintDataModel` runs over a schema-parsed stack, so the alias cannot appear
here — the tolerance was inert, and where it did fire it made
`relationship/missing-reference` report a valid target for a field that has
none: the rule whose job is to catch the misspelling was the one accepting it.
Mirrors the deliberate canonical-only narrowing already recorded in-file for
`refOf` in `packages/lint/src/validate-security-posture.ts`, including its
`typeof r === 'string'` guard — which also makes the declared
`string | undefined` return type true (the old `||` chain returned whatever
truthy value was there).
Tests pair every alias assertion with a positive control on the canonical
spelling, so a `refOf` that resolved nothing could not pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 30, 2026
@github-actions

github-actionsBot commented Aug 30, 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 listnot 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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 5 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 e8ff4274b4e16a06465f05271423a2cc87774475packageMentionDocs.

Which tree this was computed on

This run read content/docs from a9b9af299586123fd3efdfaee0dc73d198d77d36 — the merge of head 1b9a8834c5ae6a846ee6f79ef443511a0928a4f9 into base e8ff4274b4e16a06465f05271423a2cc87774475, 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 a9b9af299586123fd3efdfaee0dc73d198d77d36 && git checkout a9b9af299586123fd3efdfaee0dc73d198d77d36
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e8ff4274b4e16a06465f05271423a2cc87774475 1b9a8834c5ae6a846ee6f79ef443511a0928a4f9 && git checkout -B drift-repro e8ff4274b4e16a06465f05271423a2cc87774475 && git merge --no-ff 1b9a8834c5ae6a846ee6f79ef443511a0928a4f9
node scripts/docs-audit/affected-docs.mjs --json e8ff4274b4e16a06465f05271423a2cc87774475

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

@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM review — accepted for landing (held in draft until CI is complete and green)

Reviewed at head 03c514700, three-dot against the branch's real merge base 56c093c4d (⚠️not the PR's base.sha289cf91c8 — this clone is shallow and a two-dot read here renders commits that landed after the cut as reverts; that trap cost me a false "hot file touched" alarm on #13220 earlier today).

Scope — measured, each grep paired with a positive control

Three files: .changeset/lint-data-model-refof-canonical-reference.md, packages/cli/test/data-model-rules.test.ts, packages/lint/src/data-model-rules.ts (+154/−1).

Zero hits for governed surfaces (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md), content/docs/releases/**, packages/spec/src/** (so no Clause-② path limb, no tier change), packages/drivers/driver-mongodb/**, and this lane's §5 hot file driver-sql/src/sql-driver.ts. The same five patterns matched 5/5 against a synthetic control list, so the zeros are provably real and not a dead grep.

The fix

refOf narrowed to canonical reference with a typeof r === 'string' && r guard. I checked the claimed precedent myself rather than taking it on trust: packages/lint/src/validate-security-posture.ts:170 is character-for-character the same body, and its docblock at :207-213 already records the narrowing as the one DELIBERATE divergence from the runtime's reference ?? reference_to ?? referenceTo. So this is not a new policy invented in this PR — it is the second file adopting a ruling the first file wrote down.

The argument that it applies with more force here is right and worth keeping: relationship/missing-reference exists to tell an author a relationship has no target, and the old || chain made that exact rule the one component that accepted the misspelling.

One behaviour delta the body does not name explicitly, and which I checked: reference: '' no longer falls through to reference_to — it now resolves undefined and R1 fires. That is the correct direction (an empty target is not a target) and the new non-string test covers the adjacent shape.

⚠️ The stop condition fired as designed — and this is a successful dispatch, not a partial one

My dispatch said verbatim: "⛔ If your measurement shows the alias IS reachable at either, do NOT narrow it. Report the finding with the evidence. That is a successful outcome for this dispatch, not a failure." Both other readers measured REACHABLE, both were left alone, and the evidence is on the PR. Exactly the contract.

I got the risk wrong when I triaged #13250. I wrote that narrowing a reachable reader "would turn a tolerated read into a silent miss — the inverse defect." It is not a silent miss. It is louder and worse, and I verified the two lines at the head myself rather than relying on the report:

  • packages/plugins/plugin-security/src/security-plugin.ts:5959if (!rel) return { ...RLS_DENY_FILTER };0 rows for every non-admin caller
  • packages/plugins/plugin-security/src/security-plugin.ts:6152if (!rel) throw new MasterDetailRelationMissingError(object, operation);

So a raw-registered controlled_by_parent object spelled with the alias would flip from "access derived from its master" to everything denied, writes throw — an availability break, not a missed lint. That inverts the shape of the decision for reader A and it is the maintainer's to make, not a seat's.

The reachability pivot corroborates in the repo's own words at packages/objectql/src/registry.ts:1057-1058: the registry is "the one choke point EVERY door goes through … including the doors that skip Zod and lint entirely", and it names #3896 and cloud's rowColor.mapping as shipped instances proving those doors are real. The population is not hypothetical.

Ablation

Both legs proved the mutation live on disk and in dist/ before reading any result — the step that separates a real ablation from a rebuild that silently tested the old artifact. Mutation 3 failed | 53 passed, and the three failures are exactly the three new assertions. Restore leg pinned by hash-object f71636000c21b411eff9f89186b566d8e6b14f95 == the HEAD blob with git diff HEAD empty, then 56/56. Every alias assertion carries a canonical positive control, so a refOf that resolved nothing could not have passed — that is what makes these measurements rather than vacuous passes.

The disclosed coverage gap is honest, and I want it on the record

packages/cli/tsconfig.json is include: ["src"], so packages/cli/test/** sits in no tsc program — measured with tsc --noEmit --listFiles reporting 0 hits for the new file, not assumed. "cli typecheck exit=0" therefore says nothing about the added test. Ad-hoc check: 26 errors in the file, 0 in the added lines, and the TEST_DEBT ledger already carries test/data-model-rules.test.ts x26 — the ratchet moves by zero. Disclosing a gap that a green checkmark would otherwise have papered over is the behaviour I want; the ledger's verdict is still CI's to give.

Four gates read NOT MEASURED locally, each by its own refusal text (PREREQUISITE NOT MET / ⛔ This is NOT a pass: nothing was measured). None is a red, and none is being counted as a pass.

Part of, not Fixes — correct

The card stays open for readers A and B. "Part-of PR must not also close its card" is ✅ green on the head, so this is enforced and not just intended.

Landing posture

Not enqueued. CI is mid-flight (29 checks reported, most in_progress) and total_count still grows as aggregate rollup rows appear — a self-consistent partial read is not "done". The bar is EVERY check completed and green. Holding in draft; I'll flip ready and arm auto-merge when it clears, and verify the landing by content on origin/main, never by the merged boolean.

Nothing to change. Good work — particularly refusing the narrowing you were sent to consider, and bringing back the measurement that made refusing correct.


Generated by Claude Code

`check-reference-carrier-shape` refuses a non-string LITERAL at a `reference`
carrier position, and it is right to: an authored site of that shape is
invisible in both directions at once — refused by `ObjectSchema.safeParse` and
read as `undefined` by every rule that resolves it (#13053). The new
`a non-string reference is not a target either` case wrote one inline, so the
gate reported it as a problem and `Lint & Repo Gates` went red.
The value must keep being non-string — the whole assertion is that such a value
resolves to nothing, which makes the case a deliberate counter-example rather
than an authored carrier. So it is bound through a variable: the gate judges
literals and leaves a non-literal unjudged, which keeps its authored-site sweep
honest while the assertion drives the identical shape. The reasoning is recorded
at the binding, mirroring the same move in PR #13238.
⛔ Not fixed by deleting the case, weakening the gate, or adding a path ignore —
that gate has no baseline and wants none, by design.
Re-ablated after the change: with `refOf` reverted to the `||` chain this case
still goes RED, so the binding did not make the assertion vacuous.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

CI red — diagnosed, this PR's own, fix in flight

Recording the diagnosis so the red is not sitting here unexplained. ⛔ Not an infra failure and not someone else's.

What failed:Lint & Repo Gates (job 99200825836), at check-reference-carrier-shape:

check-reference-carrier-shape: 1 problem(s).
packages/cli/test/data-model-rules.test.ts:782
`reference` carries a literal that is not a string: { object: 'project' }

That is this PR's own new case, a non-string reference is not a target either. The gate's R1 fires correctly — the literal sits at a value position in a fields map, so it is a field-def carrier position, exactly the #13053 shape the gate exists to catch. The gate is right and is not being touched.

No escape hatch, by design. I read the gate (584 lines). Its own docblock: "A baseline here would be a place to put the next defect," and its remedy text: "never by adding a path ignore here." There is no allowlist and there should not be one.

The remedy its own doctrine names. Its refusal-scoping comment classifies three cases:

- value is a string literal -> PASS under either reading. Immaterial.
- value is not a literal at all -> UNJUDGED under either reading. Immaterial.
- value is a non-string literal -> the readings disagree. **REFUSE**

⇒ bind the non-string value through a variable so it is not a literal at the carrier position, with a comment at the binding naming the gate, #13053, and why this site is a deliberate counter-example rather than an authored mistake. House precedent for the same move: PR #13238 bound a field literal through a variable because an inline one would trip TypeScript's excess-property check on the very key that case existed to record, and left a comment at the binding saying so.

⛔ What is not happening: the test case is not being deleted, skipped or weakened. It pins that a non-string reference resolves to undefined rather than being returned as a truthy target — one of the three assertions the ablation proved red, and precisely what the typeof r === 'string' guard bought.

The fix must also re-run the ablation for that one case: a variable binding is exactly the kind of change that can quietly make an assertion vacuous, so with refOf reverted to the || chain it must still go red.

⚠️ Why this got past local verification — filed as #13333

dispatch-gates.mjs derived 29 gates for this diff and all 29 ran green locally. Measured: grep -c 'reference-carrier' scripts/pm/dispatch-gates.mjs0, with where-matcher12 on the same file as a positive control. Meanwhile .github/workflows/lint.yml:3281-3284 runs this gate with no path filter, on every PR.

So "I derived the gate family mechanically and it was all green" is not the coverage claim it reads as. Filed as #13333 — deliberately as another instance of a recurring class (#12205, #12850, #13126 closed; #12956 open p1), not as a novel finding, so triage can decide whether to fix the instance or treat it as the fourth data point that the class needs a structural answer.

The review above still stands; nothing about the substance of the change is in question. Only the new test's spelling needs to move.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 30, 2026 05:32
@os-zhuang
os-zhuang enabled auto-merge August 30, 2026 05:32
@os-zhuang
os-zhuang added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5228e52Aug 30, 2026
37 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-13250-narrow-lint-reference-alias branch August 30, 2026 06:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude