Skip to content

fix(lint): close three gates that passed what they exist to refuse - #600

Merged
Vivswan merged 1 commit into
mainfrom
fix/lint-gate-holes
Nov 12, 2026
Merged

Vivswan merged 1 commit into
mainfrom
fix/lint-gate-holes

Conversation

@Vivswan

@Vivswan Vivswan commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

Before

Three planted violations, each through the gate's own command on main. All three pass.

$ cat src/sections/planted.ts
/** A planted section module that reaches the library entry through a bare "..". */

import type { RunConfig } from "..";

export type PlantedConfig = RunConfig;
$ bun run lint:arch
lint:arch: src/ imports match architecture.yml
exit 0
$ cat src/sections/planted.ts
/** A planted schema whose whole-object rule is a function written inline in .check(). */

import { z } from "zod";

export const Planted = z.object({ name: z.string(), color: z.string() }).check((ctx) => {
  if (ctx.value.name === ctx.value.color) {
    ctx.issues.push({ code: "custom", message: "name and color must differ", input: ctx.value });
  }
});
$ bun run lint --reporter=github
exit 0
$ tail -n 7 docs/reference/permissions.md   # appended, then committed
<!-- BEGIN GENERATED: retired-grant-table -->

| grant | reads |
| ----- | ----- |
| none  | none  |

<!-- END GENERATED: retired-grant-table -->
$ bun run build:check
wrote src/generated/spec-roles.ts
wrote src/generated/spec-rules.ts
wrote src/generated/spec-enums.ts
wrote src/generated/upstream-gaps.ts (2 gap files)
gen-docs: generated regions already up to date
gen-action-docs: generated regions already up to date
gen-inputs-table: generated regions already up to date
build:check: 14 generated files match their generators
exit 0

After

The same three plants on this branch.

$ bun run lint:arch
lint:arch: 1 problem
  forbidden import sections -> library: src/sections/planted.ts -> src/index.ts; move it or declare the edge
error: script "lint:arch" exited with code 1
exit 1
$ bun run lint --reporter=github
::error title=plugin,file=/repo/src/sections/planted.ts,line=5,endLine=9,col=24,endColumn=3::bare refinement: zod skips it once a nested value failed; wrap it in rule() so it runs beside nested failures
error: script "lint" exited with code 1
exit 1
$ bun run build:check
build:check: docs/reference/permissions.md marks a generated region "retired-grant-table" that no generator registers; register it with a generator, or remove its GENERATED markers
error: script "build:check" exited with code 1
exit 1

How

Gate Refuses now
lint:arch every relative import spelled as a string literal, the bare . and .. included
rule-gate plugin a .check() under src/ handed an inline function
build:check a BEGIN GENERATED marker, in a file the generators own, that no generator registers
  • Library: searched the repository's own tools. arch-lint already parses with oxc-parser, and the plugin is biome's own GritQL.

Proof

  • Tests: 262 green across the 10 test files the change reaches. The 4 cases it adds or extends failed on main first.
  • Gates: lint, lint:arch, and build:check stay green on the tree itself, so nothing committed today is newly refused.
Technical details
  • Scope, arch-lint: a computed specifier was already refused, and an import hidden on purpose is not followed.
  • Scope, rule-gate: a check passed to .check() by name or built by a helper is not followed.
  • Scope, build:check: only pages in the generated-output table are scanned.
  • Outside that scope: a marker in any other tracked file already fails test/scripts/generated.test.ts.
  • Why the runner holds the check: docs/reference/inputs.md is written by two generators (inputs-table, outputs-list), so one generator's loop cannot tell the other's region from an unregistered one.
  • Table: each regions row of GENERATED_OUTPUTS carries the region names its generator registers, read from the registry.
  • Order: the runner refuses before any generator runs, so the refusal is the only output.
  • Scan: hasGeneratedRegion() became regionNames(), which returns the names it finds, and its four assertions in test/scripts/generated.test.ts now compare names.
  • Grammar: a marker whose name falls outside [a-z0-9-]+ is not a marker to the scan, as before.
  • Census, bare specifiers: git grep -n -E "from ['\"]\.\.?['\"]" -- src and git grep -n -E "(import|require)\(['\"]\.\.?['\"]\)" -- src print nothing.
  • Control for that census: git grep -h -E "from ['\"]\.\.?/" -- src | wc -l prints 1011.
  • Census, checks: git grep -n -F '.check(' -- src prints 31 lines, 30 calls and one comment, and the tightened plugin flags none of them (bun run lint exits 0).
  • Census, refinements: git grep -n -E "\.(superRefine|refine)\b" -- src prints one line, rule() itself in the exempt file.
  • Census, regions: git grep -h -o -E "BEGIN GENERATED: [a-z0-9-]+" -- '*.md' '*.yml' ':(exclude)test' | wc -l prints 14, in 10 files.
  • Control for that census: the table registers 14 region names in the same 10 files, and the one-liner below prints build:docs 6, build:action-docs 7, build:inputs-table 1.
bun -e 'import { GENERATED_OUTPUTS as rows } from "./.github/scripts/generated.ts"; const count = new Map(); for (const row of rows) if (row.kind === "regions") count.set(row.generator, (count.get(row.generator) ?? 0) + row.regions.length); console.log([...count].map(([generator, n]) => `${generator} ${n}`).join(", "));'
  • Census, typos: _typos.toml held 21 allow entries, 15 words and 6 patterns.
  • Census command: git grep -l -E -e <entry> -- . ':(exclude)_typos.toml' finds 20 of them in a tracked file and DELETEing in none, so that entry is removed.
  • Comment fix: alikes sits in test/scripts/generated-regions.test.ts alone, so the comment no longer names test/docs/guides.test.ts.
  • Captured blocks: each ref was exported with git archive <ref> | tar -x into a scratch directory, committed there with git init, planted as shown, and run with the command shown.
  • Refs: Before is main at faf72dab, After is this branch.
  • Cut from the gate output: bun's echo of each script line, biome's two closing summary lines, and blank lines.
  • Reporter: --reporter=github gives the plugin's diagnostic as one line.
  • Default reporter: bun run lint fails the same plant with the same message and exit code 1.
  • Resolution: tsc -p . exits 0 on both bare forms, and bun resolves each to the directory's index.ts.
  • Red on main, arch-lint: the two bare-form rows of test/scripts/arch-lint.test.ts receive [].
  • Red on main, plugin: test/lint/rule-gate.test.ts misses its three new diagnostics.
  • Test command: bun test test/scripts/{arch-lint,generated,generated-regions,auto-fix-steps,nightly-steps,gen-docs,gen-action-docs,gen-inputs-table}.test.ts test/lint/rule-gate.test.ts test/docs/coverage.test.ts.
  • Red on main, runner: the new case of test/scripts/generated.test.ts receives status 0 and "14 generated files match their generators".
  • zod, repeatable: the probe below, saved in the repository root and run with bun, prints the type issue alone for the bare check and both issues for the wrapped one.
import { z } from "zod";
import { rule } from "./src/sections/shared/schema-helpers.ts";

const shape = { name: z.string(), color: z.string() };
const bare = z.object(shape).check((ctx) => {
  ctx.issues.push({ code: "custom", message: "whole-object rule ran", input: ctx.value });
});
const wrapped = z.object(shape).check(
  rule((_value, ctx) => ctx.addIssue({ code: "custom", message: "whole-object rule ran" })),
);
for (const schema of [bare, wrapped]) {
  console.log(schema.safeParse({ name: 1, color: "x" }).error?.issues.map((issue) => issue.message));
}
[ "Invalid input: expected string, received number" ]
[ "Invalid input: expected string, received number", "whole-object rule ran" ]
  • Files: .github/scripts/{arch-lint.ts,generated.ts,lib/generated-regions.ts}, lint/rule-gate.grit, test/scripts/{arch-lint,generated}.test.ts, test/lint/rule-gate.test.ts, _typos.toml.

arch-lint dropped the bare relative specifiers. Its filter kept only a specifier with a slash, so an import of "." or ".." (a directory's index, which tsc and bun both resolve) drew no edge, and a forbidden cross-layer import passed. The filter now keeps every relative specifier.

The rule-gate plugin guarded .refine() and .superRefine(), which src/ does not write, and not .check(), which it does. zod skips a function written inline in .check() once a nested value failed, exactly as it skips a bare refinement, so the plugin now refuses that form with the message the existing patterns give. A check passed by name or built by a helper is not followed.

build:check never saw a generated region that no generator registers. Each generator re-renders only the regions in its own registry, so a region dropped from one kept its markers and went stale with the gate green. Each "regions" row of the generated-output table now carries the region names its generator registers, read from the registry, and the runner refuses a BEGIN marker in a registered page that names none of them, naming the page and the region, before any generator runs. The check sits in the runner because docs/reference/inputs.md is written by two generators, and only the table sees both registries. The marker scan now returns the names it finds, so the table test and the runner read one scan.

_typos.toml loses the allow entry for DELETEing, a word no tracked file holds, and its look-alikes comment no longer names a file that does not hold the word.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 03:35
@github-actions

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 6 warning(s).

File Size Tier Cap
src/engine/diff.ts:56 11 comment lines warn 10
src/github/api-error.ts:9 11 comment lines warn 10
src/problem.ts:73 18 comment lines warn 10
src/sections/contract/errors.ts:13 11 comment lines warn 10
src/sections/contract/module.ts:55 12 comment lines warn 10
src/sections/shared/list/decl.ts:265 14 comment lines warn 10

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

5 managed file(s) skipped; repo-platform owns them.

@github-actions

Copy link
Copy Markdown
Contributor

Member shape check

1 finding(s).

File Line Rule
test/sections/contract/validate-hook.test.ts 80 all-or-none

Add the blank lines, make the comment one line directly above the item, comment every field or none, or list the path in .member-shape-allow.local with a # reason.

4 generated file(s) skipped.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues were identified in the changes strengthening architecture, refinement, and generated-region validation.

No open issues 🎉

What changed in this PR

Strengthens three validation gates in GitHub Settings as Code, closing gaps in architecture checks, schema-refinement linting, and generated-region ownership checks.

Changes:

  • Includes bare . and .. imports in architecture enforcement.
  • Flags inline functions passed directly to .check() so refinements use rule().
  • Rejects unregistered generated regions before generators run, accounting for files shared by multiple generators.
File Description
test/​scripts/​generated.test.ts Tests region-name discovery and early rejection of unregistered HTML and YAML regions.
test/​scripts/​arch-lint.test.ts Adds regression cases for bare directory imports.
test/​lint/​rule-gate.test.ts Covers inline check functions and permitted wrapped or named checks.
lint/​rule-gate.grit Detects inline arrow and function expressions passed to .check().
.github/​scripts/​lib/​generated-regions.ts Returns discovered region names using the shared marker grammar.
.github/​scripts/​generated.ts Collects registered region names across generators and checks ownership before generation.
.github/​scripts/​arch-lint.ts Recognizes bare . and .. as relative imports.
_typos.toml Removes an unused spelling allowance and corrects a fixture-location comment.

Review effort: Balanced

@Vivswan
Vivswan marked this pull request as ready for review November 12, 2026 03:53
@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Nov 12, 2026
@Vivswan
Vivswan merged commit 347410e into main Nov 12, 2026
34 checks passed
@Vivswan
Vivswan deleted the fix/lint-gate-holes branch November 12, 2026 05:51
This was referenced Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-when-green Owner approved: merge once every gate is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants