🔧 Share one Oxlint policy between the gate and the sensor - #401
Conversation
The repository lint gate and the review sensor each carried a full copy of the Oxlint catalog, and the copies had drifted: the sensor turned pedantic and style on, so it reported 96 diagnostics over PR #375's files, nearly all generic style noise. The two also ran different Oxlint versions, so the profiles could not be compared at all. oxlint.shared.json becomes the one built-in policy. Both profiles extend it through a native JSON extends path, so a profile can no longer enable a rule the shared policy disables. The gate adds only its local JavaScript plugin and blocking rules; the sensor adds nothing. Oxlint and tsgolint are pinned to the exact versions the production review sensor already provisions, so a routine lock refresh cannot expand the rule set without changing the sensor. SENSOR_RULES in categories.ts becomes the one TypeScript catalog, and Doctor derives its available and missing rules from it. scripts/tests/oxlint-policy.test.ts proves inheritance, catalog ownership, the sensor-subset relation, override survival, and version agreement under Deno, Node and Bun. Closes #395
PR #401: 🔧 Share one Oxlint policy between the gate and the sensor14 files, +882 / -310 Scope🔴 PR has 1192 lines changed. Split into focused PRs. 🟡 1192 lines changed. PRs under 400 receive more thorough review. 🟡 Changes span 6 directories. 🟡 PR mixes config and source changes. Structural✅ No structural bloat detected. Slop
Static AnalysisOxlint: 1 diagnostic across 1 file (1 rule) no-array-sort (1): packages/code-review-agent/src/doctor.ts CorrectnessNo extraneous code patterns detected. All changes directly support the shared Oxlint policy implementation, toolchain alignment, and conformance verification as described. The repeated plugin lists, version pins, and test suite are all required for the stated purpose. |
The specifications claimed extends made a profile-level override impossible. Oxlint merges first to last and the later configuration wins, so a profile that redeclared an inherited rule would override it. The real invariant is that the committed sensor profile declares no built-in rule of its own and that its active built-in set stays a subset of the gate's, with OP2 and OP5 failing verification when either stops holding. runOxlint gave stdout and stderr one shared TextDecoder, so a multi-byte character split across chunks on one pipe could take its continuation bytes from the other, and a character left pending at exit was dropped. Each pipe now has its own decoder, and both are flushed after the process settles. The conformance suite takes its directory and file operations from @effectionx/fs, keeping node:fs only for mkdtempSync, which that package does not provide.
| test override. The sensor adds nothing: | ||
|
|
||
| ```jsonc | ||
| // .oxlintrc.json |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // .oxlintrc.json |
| ] | ||
| } | ||
|
|
||
| // .reviews/.oxlintrc.json |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // .reviews/.oxlintrc.json |
Why
The repository lint gate and the review sensor each carried a full copy of the Oxlint catalog, and the copies had drifted apart. The sensor turned
pedanticandstyleon; the gate turned them off. The sensor's test override was missing**/tests/**. The two also ran different Oxlint versions — 1.56 installed, 1.74 downloaded by the review job — so the profiles could not meaningfully be compared at all.The visible cost is on PR #375's own review: the sensor reported almost entirely generic style noise, which is exactly the signal the density model is supposed to keep clean.
Closes #395.
What changes
oxlint.shared.jsonbecomes the one project-owned built-in policy, and both profiles extend it instead of carrying a catalog each.Before: over the three representative files plus the 17 TypeScript files PR #375 changed, the sensor reports 96 diagnostics across 19 rules —
func-names(37),no-ternary(13),id-length(12),max-lines-per-function(9),max-statements(8), and so on.After: the same file set reports 2 diagnostics:
Both come from the shared
correctness/suspiciousbaseline. Neither is fixed here — this PR does not touch source to lower its own count.The gate moves too, in the same direction.
deno task lintgoes from 1411 advisory warnings to 369 under the same Oxlint 1.74. The difference is exactly the two rules the settled policy now denies:require-yield(1039) andpreserve-caught-error(3). No rule appears that did not appear before.How it works
Oxlint resolves a JSON
extendspath relative to the declaring configuration and merges the shared source under whatever the profile adds, including merging inheritedoverridesarrays. The gate therefore ends with two test-override blocks (the shared one plus its ownlocal/no-redundant-test-scope), and the sensor with one.Toolchain versions are pinned exactly to what
.reviews/components/EnsureOxlint.mdalready provisions — Oxlint 1.74.0, tsgolint 0.25.0 — so both uses evaluate the same policy with the same linter.Review guide
Start with:
oxlint.shared.jsonThen review:
.oxlintrc.jsonand.reviews/.oxlintrc.json— what each profile still ownsdeno.json/package.json— the exact pinspackages/code-review-agent/src/categories.tsanddoctor.ts— one catalog, no duplicatescripts/tests/oxlint-policy.test.ts— the conformance suitespecs/oxlint-sensor-spec.md§4 and §14,specs/code-review-agent-spec.md§13.1 and §14Look carefully at:
"plugins". Oxlint 1.74 reads the plugin set from the entry configuration only: a list reached throughextendsis additive over Oxlint's defaults rather than replacing them, so a profile that inherited plugins silently gained the 17oxc/*rules the current configs exclude. Repeating the list preserves today's rule set exactly; the alternative would have adopted 17 rules Unify Oxlint gate and review sensor policy #395 explicitly excludes.oxlint.shared.jsonkeeps the list as the canonical declaration, and OP2/OP4 assert all three agree.What must stay true
local/*rule, no error severity; checked by OP8.SENSOR_RULESincategories.ts; the native JSON must match it, checked by OP2, OP6, and OP12.EnsureOxlint.md,deno.json,package.json, and the executed binary. Two of the four are additionally protected by the frozen lockfile, which refuses to run at all on a drifted manifest.How to verify it
scripts/tests/oxlint-policy.test.tsruns under Deno, Node, and Bun (14 cases, OP1–OP14). Each was mutation-tested: the mutation was applied to a file copy, the suite run, the file restored.extendsduplicatedunicorn/filename-casedenial deletedpedanticpluginsdeclarationno-debugger**/tests/**EnsureOxlint.mdpins a different Oxlint tagpackage.jsondrifts to^1.74.0A tenth mutation checks the invariant this PR actually relies on: making the sensor profile redeclare
unicorn/filename-case: "warn", overriding a shared denial. It fails OP2, OP5, OP7, and OP8. That is the mechanism, sinceextendsdoes not itself forbid the override.OP11 is the one that needed a real lint rather than
--print-config: the printed config keeps override blocks separate instead of flattening them into top-levelrules, so a printedno-console: warnproves nothing about a test path. It lints atests/file and a sibling in a scratch directory and asserts only the sibling reportsno-console.Local run:
Because this PR moves dependency state,
deno task verify:cleanran against the committed revision. It passed on7073d12: the release-target preparation held, every build phase stayed cache-pure, and the full battery ran with the site pair applying.Production evidence
The
reviewjob on this PR runs the real sensor path — the downloaded Oxlint 1.74.0 binary against.reviews/.oxlintrc.json— and reports:Issue #395 recorded 424 diagnostics for the state this replaces. The one surviving diagnostic comes from the shared
correctness/suspiciousbaseline and is deliberately not fixed here.The same report flags scope: 1167 lines changed across 6 directories, mixing config and source. That is the settled shape of this change. Of those lines, 430 are the new conformance suite and 287 are regenerated lockfiles — 61% between them. Only 27 lines of package source change. The plan for #395 established one atomic PR because configuration, toolchain versions, catalogs, and specifications must move together: splitting them would leave either the running sensor or the documented policy knowingly contradictory on
main.Scope
Included
SENSOR_RULESas the single TypeScript catalog; Doctor's duplicate removedIntentionally unchanged
.reviews/components/EnsureOxlint.md— its pins and checksums are the production authority this PR matches, not something to editno-unsafe-*,preserve-caught-error,no-duplicate-imports) stay explicitly off, pending individual adoptionDoctorResult, density calibration, diagnostic category names, normalized output, review workflow lifecycle, component protocols, and all public package APIsNew abstractions
SENSOR_RULES(categories.ts) exists because Doctor and the native JSON both needed the curated catalog and each kept its own copy. Consumers:doctor.tsand the conformance suite. It is internal policy structure —mod.tsis not widened and no changeset is added.runOxlint(config, args)(scripts/tests/oxlint.ts) generalizes the existing helper to any config and returns the exit code and stderr, which the conformance suite needs to fail loudly on a broken invocation.oxlint()now delegates to it; the rule tests are unchanged.Generated or mechanical changes
deno.lock,pnpm-lock.yaml, andbun.lockwere regenerated bydeno install --frozen=false,pnpm install, andbun install --lockfile-only. The churn is confined to oxlint and tsgolint resolutions and their platform-binding integrity hashes; no unrelated package moved.Revision at
60a6fdfReview feedback on
7073d12, addressed without changing the settled policy design:extendsmade a profile-level override impossible. Oxlint merges first to last and the later configuration wins, so it does not.specs/oxlint-sensor-spec.md§4.1,specs/code-review-agent-spec.md§13.1, and this description now state the real invariant — the committed sensor profile declares no built-in rule of its own, its active built-in set stays a subset of the gate's, and OP2/OP5 are what make that binding. The new tenth mutation above demonstrates it.runOxlintshared oneTextDecoderbetween stdout and stderr. A multi-byte character split across chunks on one pipe could take its continuation bytes from the other, and a character still pending at exit was dropped. Each pipe now has its own decoder and both are flushed after the process settles. Thescopedlifecycle and captured exit status are unchanged.@effectionx/fs. Directory and file operations now useensureDir,writeTextFile, andrm;node:fsremains only formkdtempSync, which that package does not provide, with the rationale stated at the call site. One comment that narrated ordinary code is gone.Re-verified at this head:
fmt,lint(exit 0, 369 advisory warnings),check,check:jsr, the policy and helper-dependent suites under Deno (8 files, 64 steps, 0 failed), and the policy suite under Node 22 and Bun (34/34 each).Risks and limitations
npx --yes oxlint, matching the existing rule tests; it resolves the localnode_modules/.bincopy on a prepared worktree.oxlint.shared.jsonalone — both profiles extend it.Scope confirmation