Skip to content

ldd-lint: a gap linter for file length and package size, one set of thresholds - #100

Merged
buzzdan merged 6 commits into
mainfrom
claude/gap-linter
Oct 18, 2026
Merged

buzzdan merged 6 commits into
mainfrom
claude/gap-linter

Conversation

@buzzdan

@buzzdan buzzdan commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

What

Every plugin gets scripts/ldd-lint.sh, a gap linter. It measures two design triggers:

  • file-length: lines per source file.
  • package-size: source files per directory. No linter in any of the four ecosystems counts these.

The script never edits a file. The linter is the authority for every threshold it owns, and the gap linter fills only the gaps. The first output line names the source of each threshold.

  • File length. A linter config at the repository root can name a file-length rule (revive file-length-limit, sonarjs/max-lines, ESLint max-lines, pylint max-module-lines). Then the linter holds file length, and the script measures none. The linter's limit, its way of counting and whether the rule is on are the repository's decision. Otherwise the script counts code lines against .ldd-lint.yaml, else the default of 450. With the default, it prints a linter setting to propose to the repository's owners.
  • Package size. The limit comes from .ldd-lint.yaml, else from the defaults: a warning at 8 source files per directory and the limit at 13.

--tsv writes findings as path TAB 0 TAB message rows. A later PR adds the Detect-script detect-line kind, which reads these rows.

Changes

  • Profile scalars. The profiles get file_max_lines, package_warn_files and package_max_files. The generator rejects a value that is not a positive integer. It also rejects a warning size at or above the limit.
  • Linter table. A new core include, scripts/ldd-lint-langs.sh, is keyed by language id. It holds the comment syntax, the files that a directory count leaves out, and a check that a linter config names a file-length rule. It is the one place in core that names linters, and it carries a residue-exempt: marker. The table does not read any limit or counting mode out of a config.
  • Shared thresholds. The skills, the routing tables and the lint-fixer tables use the scalars. They no longer state 400, 450, 600, 8–12 or 13. The package-size procedure runs the gap linter in place of a hand-written find.
  • Removed. The Go plugin's hooks/check-package-sizes.sh is deleted. It was never registered, so it never ran.
  • Tests. The fixture matrix ldd-lint_test.sh runs in task test-gate for every plugin, with 22 to 23 cases per fixture row. The cases include a named rule, a rule with a similar name and a commented-out rule.

Departures from the plan

  • Hook registration moved to a separate PR. Every eval fixture has directories at or over the limit:

    Fixture Directory Source files
    go-mini internal/models 14
    py-mini internal/models 14
    ts-react-mini src/types 17
    ts-react-mini src/services 15

    A registered hook would speak to the agent during eval cases, so it needs its own eval gate.

  • No config parsing. The plan read each linter's limit and copied its way of counting. That was a second copy of the linters' rules to maintain and test. The script now only checks whether a config names the rule.

  • No new field in the language block. The per-language data goes into a shared table, the same way as the language table. The generic plugin then covers every language id with no extra work.

Text the agents read that changed

The review rules did not change, except for one question. Three texts changed:

  • The refactoring package-and-file procedure. It runs ldd-lint.sh now. On the eval fixtures, the script measures file length only on py-mini, where it finds no file over 450 code lines. go-mini and ts-react-mini name a file-length rule, so the linter holds file length there. All three fixtures report the package-size zones in the table above.
  • The lint-fixer and refactoring routing rows. They name the gap linter's findings.
  • R5 Q2 in every binding. Main added a mechanical lead for the god file to this question, with fixed limits: 450 lines for Go and Python, 600 for ts-react. The merge keeps the lead. The question now checks each long file against the repository linter's rule where the repository sets one, else 450 code lines. The hunters read this text.

The package-size zones are not new. The same procedure counted them before, with a find command. I propose to merge without an eval run.

Checks

  • task check, task lint-core, task docs:check, and the generator's tests and lint all pass.
  • The ldd-scope, ldd-detect and ldd-lint matrices pass for all four plugins on macOS. CI on Ubuntu passed on the earlier head, which ran the counting awk under mawk. The check-repo-brain matrix fails two Q7 cases, the same as on main.
  • I mutated the rendered script eight ways. A named case caught each mutation.

🤖 Generated with Claude Code

buzzdan and others added 3 commits October 18, 2026 17:51
…hresholds

Add scripts/ldd-lint.sh to every plugin. The script measures lines per
source file (file-length) and source files per directory (package-size),
where the repository's linter does not hold them. It never edits a file.

The threshold comes from the first source that applies:
- the repository's linter config, counted the way that linter counts
  (revive file-length-limit, sonarjs/max-lines, ESLint max-lines,
  pylint max-module-lines);
- .ldd-lint.yaml at the repository root;
- the profile defaults: 450 code lines per file, a warning at 8 source
  files per directory, the limit at 13.

The first output line names the threshold and its source. With the
defaults, it also prints the setting that makes the linter hold the limit.

- Three profile scalars hold the thresholds. The generator rejects a value
  that is not a positive integer, and a warning at or above the limit.
- A core include, scripts/ldd-lint-langs.sh, is the linter table, keyed by
  language id. It is the one place core names linters (residue-exempt).
- ldd-lint_test.sh runs 20 cases per fixture row in every plugin. The
  bindings' fixture rows add the source suffix, the comment forms, a test
  file name, the file a count leaves out, and a linter config.
- The skills and routing tables use the scalars in place of 400, 450, 600,
  8-12 and 13, and the package-size procedure runs the gap linter.
- Remove the Go plugin's hooks/check-package-sizes.sh. It was never
  registered, so it never ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ig forms

- The threshold line offers the linter setting as a proposal ("a setting to
  propose, never to add on your own"). An agent that runs the script does
  not edit a linter config, as the house rules require.
- Every path the script gives to find, head or awk starts with "./", and a
  directory is cut from a path by parameter expansion. A file or directory
  whose name starts with "-" is measured and never read as an option. A
  matrix case covers it; dirname failed that case before this change.
- The fixture rows pin more config forms in CI: revive's bare-number form
  (Go plugin), its map form (generic plugin's Go row), revive with the skip
  flags and sonarjs/max-lines, which count only code lines.
- docs/token-budget.md keeps the planned Go analyzer apart from the gap
  linter's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gap linter read each linter's limit and copied each linter's way of
counting. That code was a second copy of the linters' rules to maintain and
test. The linter is the authority for every threshold it owns, so the
script now asks one question: does a linter config at the repository root
name a file-length rule (revive file-length-limit, sonarjs/max-lines,
ESLint max-lines, pylint max-module-lines) on a line that is not a comment?

- Yes: the threshold line names the rule, and the script measures no file
  length. The rule's limit, its counting and whether it is on are the
  repository's decision.
- No: the script counts code lines against .ldd-lint.yaml or the plugin
  default, as before.

The config readers, the counting modes and their fixture builders are
removed (about 120 lines). The matrix pins the new behavior: a named rule,
a rule of a similar name (argument-limit, max-args, max-lines-per-function)
and a commented-out rule, for every fixture row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

🟡 Changes recommended

Option parsing can hang, and comment handling and file counting can produce incorrect lint results.

5 open issues
What changed in this PR

Adds a shared gap linter to the generated plugins, covering file length and package size while deferring to repository linter rules.

Changes:

  • Adds configurable thresholds, validation, and machine-readable findings.
  • Updates refactoring guidance and removes the unused Go package-size hook.
  • Adds fixture matrices to the test gate.
File Description
ts-react-linter-driven-development/​skills/​refactoring/​SKILL.md Routes gap-linter findings.
ts-react-linter-driven-development/​skills/​refactoring/​reference.md Uses gap-linter measurements.
ts-react-linter-driven-development/​skills/​code-designing/​SKILL.md Updates file-length guidance.
ts-react-linter-driven-development/​scripts/​ldd-scope_test.sh Expands shared fixtures.
ts-react-linter-driven-development/​scripts/​ldd-lint_test.sh Adds lint fixture matrix.
ts-react-linter-driven-development/​scripts/​ldd-detect_test.sh Expands shared fixtures.
ts-react-linter-driven-development/​rules/​R5-vertical-slice.md Removes example-specific threshold.
ts-react-linter-driven-development/​CHANGELOG.md Documents gap linter.
ts-react-linter-driven-development/​agents/​lint-fixer.md Routes gap-linter findings.
tools/​ldd-gen/​main_test.go Adds fixture thresholds.
tools/​ldd-gen/​internal/​render/​render_test.go Adds fixture thresholds.
tools/​ldd-gen/​internal/​profile/​profile.go Validates threshold scalars.
tools/​ldd-gen/​internal/​profile/​profile_test.go Tests threshold validation.
tools/​ldd-gen/​internal/​gen/​gen_test.go Adds fixture thresholds.
tools/​ldd-gen/​internal/​binding/​binding_test.go Adds fixture thresholds.
Taskfile.yaml Registers lint matrices.
python-linter-driven-development/​skills/​refactoring/​SKILL.md Routes file-length findings.
python-linter-driven-development/​skills/​refactoring/​reference.md Uses gap-linter measurements.
python-linter-driven-development/​skills/​code-designing/​SKILL.md Updates file-length guidance.
python-linter-driven-development/​scripts/​ldd-scope_test.sh Expands shared fixtures.
python-linter-driven-development/​scripts/​ldd-lint.sh Adds Python gap linter.
python-linter-driven-development/​scripts/​ldd-lint_test.sh Adds lint fixture matrix.
python-linter-driven-development/​scripts/​ldd-detect_test.sh Expands shared fixtures.
python-linter-driven-development/​CHANGELOG.md Documents gap linter.
linter-driven-development/​skills/​refactoring/​reference.md Uses gap-linter measurements.
linter-driven-development/​scripts/​ldd-scope_test.sh Expands shared fixtures.
linter-driven-development/​scripts/​ldd-lint_test.sh Adds multilingual lint matrix.
linter-driven-development/​scripts/​ldd-detect_test.sh Expands shared fixtures.
linter-driven-development/​README.md Removes obsolete hook reference.
linter-driven-development/​CHANGELOG.md Documents gap linter.
lang/​ts-react/​skills/​refactoring/​routing-table.md Routes gap-linter findings.
lang/​ts-react/​skills/​refactoring/​file-and-package-routing.md Uses thresholds and measurements.
lang/​ts-react/​skills/​code-designing/​linter-triggers.md Uses threshold scalar.
lang/​ts-react/​scripts/​ldd-fixture.sh Adds TypeScript lint fixtures.
lang/​ts-react/​rules/​R5/​falsifying-questions.md Removes example-specific threshold.
lang/​ts-react/​profile.yaml Defines threshold defaults.
lang/​ts-react/​passthrough/​CHANGELOG.md Documents gap linter.
lang/​ts-react/​agents/​lint-fixer/​routing-table.md Routes gap-linter findings.
lang/​python/​skills/​refactoring/​routing-table.md Routes file-length findings.
lang/​python/​skills/​refactoring/​file-and-package-routing.md Uses thresholds and measurements.
lang/​python/​skills/​code-designing/​linter-triggers.md Uses threshold scalar.
lang/​python/​scripts/​ldd-fixture.sh Adds Python lint fixtures.
lang/​python/​profile.yaml Defines threshold defaults.
lang/​python/​passthrough/​CHANGELOG.md Documents gap linter.
lang/​go/​skills/​refactoring/​routing-table.md Replaces hook routing.
lang/​go/​skills/​refactoring/​file-and-package-routing.md Uses thresholds and measurements.
lang/​go/​skills/​code-designing/​linter-triggers.md Uses threshold scalar.
lang/​go/​scripts/​ldd-fixture.sh Adds Go lint fixtures.
lang/​go/​profile.yaml Defines threshold defaults.
lang/​go/​passthrough/​README.md Lists gap linter instead of hook.
lang/​go/​passthrough/​hooks/​hooks.json Removes empty hook configuration.
lang/​go/​passthrough/​hooks/​check-package-sizes.sh Removes unused hook source.
lang/​go/​passthrough/​CHANGELOG.md Documents linter and hook removal.
lang/​go/​agents/​lint-fixer/​routing-table.md Replaces hook routing.
lang/​generic/​scripts/​ldd-fixture.sh Adds multilingual lint fixtures.
lang/​generic/​profile.yaml Defines threshold defaults.
lang/​generic/​passthrough/​README.md Removes obsolete hook reference.
lang/​generic/​passthrough/​CHANGELOG.md Documents gap linter.
go-linter-driven-development/​skills/​refactoring/​SKILL.md Replaces hook routing.
go-linter-driven-development/​skills/​refactoring/​reference.md Uses gap-linter measurements.
go-linter-driven-development/​skills/​code-designing/​SKILL.md Updates file-length guidance.
go-linter-driven-development/​scripts/​ldd-scope_test.sh Expands shared fixtures.
go-linter-driven-development/​scripts/​ldd-lint.sh Adds Go gap linter.
go-linter-driven-development/​scripts/​ldd-lint_test.sh Adds lint fixture matrix.
go-linter-driven-development/​scripts/​ldd-detect_test.sh Expands shared fixtures.
go-linter-driven-development/​README.md Lists gap linter instead of hook.
go-linter-driven-development/​hooks/​hooks.json Removes empty hook configuration.
go-linter-driven-development/​hooks/​check-package-sizes.sh Removes unused generated hook.
go-linter-driven-development/​CHANGELOG.md Documents linter and hook removal.
go-linter-driven-development/​agents/​lint-fixer.md Replaces hook routing.
docs/​token-budget.md Distinguishes planned analyzer; removes hook claim.
docs/​language-residue.md Describes shared linter table.
docs/​generator.md Removes obsolete hook rationale.
docs/​eval-fixture.md Updates fixture-layout rationale.
core/​skills/​refactoring/​reference.md Uses package threshold scalars.
core/​scripts/​ldd-lint.sh Implements shared gap-linter template.
core/​scripts/​ldd-lint_test.sh Adds shared lint matrix template.
core/​README.md Documents scalars and lint templates.
core/​includes/​skills/​refactoring/​file-and-package-routing.md Uses shared measurements and thresholds.
core/​includes/​scripts/​ldd-lint-langs.sh Defines language-specific lint handling.

Review effort: Balanced

Comment thread core/includes/scripts/ldd-lint-langs.sh Outdated
Comment thread core/scripts/ldd-lint.sh Outdated
Comment thread core/scripts/ldd-lint.sh Outdated
Comment thread core/scripts/ldd-lint.sh Outdated
Comment thread core/includes/scripts/ldd-lint-langs.sh Outdated
…t inventory

Fix five review findings:

- A rule name inside a block or trailing comment of a linter config no
  longer counts as a named rule. A new LINT_STRIP_AWK removes comments one
  character at a time and keeps string literals. The config check and the
  line count both use it.
- A block comment that opens after code on a line now ends the code there.
  The lines inside the block are not counted.
- --root, --files, --glob or --test-re as the last argument is a usage
  error (exit 2). Before, the loop never ended.
- The directory count reads the repository inventory (git ls-files when the
  root is a repository). A git-ignored sibling no longer counts, and a
  --files run counts each directory it names in full.
- *.stories.ts and the other story files are left out of the TypeScript
  directory count again.

Each fix has a matrix case that fails on the previous script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buzzdan and others added 2 commits October 18, 2026 21:43
Resolve the conflict in ts-react R5 Q2. Keep main's mechanical lead for
the god file (the longest scope files by wc -l). Replace the limits it
names with the shared threshold: the repository linter's rule where the
repository sets it, else {{.FileMaxLines}} code lines. Apply the same
change to the Go, Python and core R5 Q2 text, which named 450 lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	go-linter-driven-development/CHANGELOG.md
#	lang/generic/passthrough/CHANGELOG.md
#	lang/go/passthrough/CHANGELOG.md
#	lang/python/passthrough/CHANGELOG.md
#	lang/ts-react/passthrough/CHANGELOG.md
#	linter-driven-development/CHANGELOG.md
#	python-linter-driven-development/CHANGELOG.md
#	ts-react-linter-driven-development/CHANGELOG.md
@buzzdan
buzzdan marked this pull request as ready for review October 18, 2026 19:10
@buzzdan
buzzdan merged commit a604fce into main Oct 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants