Skip to content

Read the Line-Ending Pins Against the Tree, Not Just Each Other - #634

Merged
ptr727 merged 1 commit into
developfrom
feature/eol-coverage-check
Aug 9, 2026
Merged

Read the Line-Ending Pins Against the Tree, Not Just Each Other#634
ptr727 merged 1 commit into
developfrom
feature/eol-coverage-check

Conversation

@ptr727

Copy link
Copy Markdown
Owner

Closes#633.

The gap

repo_gate.py --check eol compares .gitattributes against .editorconfig and never compares either against the repository. That direction is correct and stays unchanged. What it cannot see is the tree: both documents can agree perfectly and both be wrong about the repository they describe, and spec/files.json marks .gitattributes as intent, so what stood in the gap was an agent reading the file for meaning during an audit.

Measured, not argued

ptr727/Blog at 392de22 carried both shapes. Run against that commit:

--- eol, before and after this change ---
[ok ] eol 0 issue(s)
note: .editorconfig sets end_of_line = lf globally, so that default satisfies
every pin and nothing here read pin content. Use eol-coverage instead.
--- eol-coverage, new ---
[FAIL] eol-coverage 3 issue(s)
ops/vps-backup-pull: a shebang script git resolves to `eol: unspecified`, not `lf`
.gitattributes pins `deploy/blog-deploy-shell` to LF and no tracked file matches it
.gitattributes pins `deploy/authorized_keys` to LF and no tracked file matches it

All three reported defects caught, and Blog's other six pins raise nothing. ops/vps-backup-pull is run unattended by systemd on a backup host, so a CRLF checkout hands it a broken interpreter line.

The dead pin is the worse half. The comment above Blog's two asserted the extensionless case was handled, so the one real instance twenty lines up read as covered by everyone who opened the file. A dead pin does not merely fail to bind, it reads as coverage.

What ships

  • eol-coverage, a new check rather than a widened eol. Widening in place would make the docstring, scripts/README.md, and TestEol wrong at once, and would destroy the ability to ask the config question alone.
  • unpinned: a tracked file opening #! that git does not resolve to eol=lf. Delegates to git check-attr, so it cannot disagree with what a checkout applies. The shebang decides rather than the mode bit, since the two move independently. This starts enforcing the GOVERNANCE.md "Line Endings" sentence "any repo whose tooling ships extensionless scripts adds the matching path pin", which had nothing behind it. This repo passes with zero slack: 16 tracked #! files, 16 resolving to lf.
  • dead: a pin matching no tracked file, unless its block is marked forward-declared.
  • A vacuity note on eol. Where .editorconfig sets [*] end_of_line = lf, the override the check looks for is satisfied by the global default for any path, an absent one included, so the check asserts nothing about pin content. Blog is shaped that way, as are the repos declaring lineEndings: lf in registry/repos.json. It does not fail there, it says what it did not read.

The exemption decision

Three of this repo's pins match nothing today and all three are correct: uv.lock, Dockerfile, *.Dockerfile. .gitattributes is carried, so those go live the moment a consumer adds a lockfile or a Dockerfile, which the file already said in prose. The forward-declared token makes that prose machine-readable and travels with the carried copy, so a python repo holding the baseline without a lockfile stays exempt too.

Two alternatives rejected: an exception list inside the script restates data the scanned repository already carries, and diffing against the hub's own .gitattributes makes a repo gate change verdict when another repository changes and cannot run standalone.

What it trades away is stated rather than left to be found: the mark reaches to the next blank line, so a pin appended under a marked block inherits an exemption nobody wrote for it, and that fails open. TestGitattributesCoupling therefore names the three pins the marking is for and fails the moment a fourth arrives, rather than the parser growing a second rule.

Two implementation notes

Matching is gitattributes matching, not pathspec matching.git ls-files -- <pattern> is the cheap way to ask whether a pin binds anything and is a different language: there * crosses a /, so capture/*.py also matches capture/sub/x.py and a dead pin reads as live. attr_glob handles basename-versus-anchored, the leading slash, ** as a whole segment, and character classes, with a 20-case table.

The shebang floor is a suite case rather than a runtime finding. A source-only configuration repo shipping no scripts is legitimately clean, so failing an empty scan would be a false finding in the common fleet case. What must not go unnoticed is this repo's own scan going quiet, which TestCoverageFloors holds instead. Every run still prints its counters, for the reason sha-pin does.

Deliberately out of scope

The symmetric .editorconfig reading, a path-specific section naming files that do not exist. This repo's [.github/workflows/*] and [catalog/snippets/workflows/*] sections are legitimately broad, so the exemption needs measuring against the live corpus before a gate is built on it. Recorded in TODO.md under Standalone Chores.

TODO.md also gains a ready Work Cluster for the general case this issue is one instance of: an observer that reads merged fleet pull requests and classifies each changed path against what the hub declares it owns. Nothing detected the downstream editing hub-managed CI files here except the maintainer noticing.

Verification

  • 496 script self-tests pass, test_repo_gate.py going 46 to 70 cases, harness floor raised to 68.
  • Each direction was watched failing on the reintroduced defect in a scratch clone, and passing again on restore, rather than trusted from a green run.
  • spec/validate.py, spec/audit.py --selftest, all three repo gates, whole-tree prose_lint.py, markdownlint-cli2 and editorconfig-checker all clean. CRLF preserved in the three Markdown and dotfile edits, LF in both scripts.

`repo_gate.py --check eol` compares .gitattributes with .editorconfig and
never compares either against the repository, so both documents can agree
perfectly and both be wrong. ptr727/Blog carried two defects in that gap
while the gate reported clean: an extensionless shebang script systemd runs
unattended, matched by no pin, and two pins naming paths never tracked
there. The dead pins are what hid the live one, since a pin that binds
nothing still reads as coverage.
`eol-coverage` adds the filesystem-facing direction as a separate check, so
`--check eol` keeps meaning exactly what it means today. A pin block marked
`forward-declared` is exempt from the dead reading, because in a carried
baseline a pattern matching nothing is a declaration for whichever consumer
adds the file, and the mark travels with the copy.
Raised as #633 by a downstream agent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 9, 2026 03:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new filesystem-facing line-ending gate (eol-coverage) so the existing eol check is no longer “document-to-document only” and can detect cases where .gitattributes/.editorconfig agree with each other but are still wrong for the tracked tree (including dead pins and unpinned shebang scripts).

Changes:

  • Add eol-coverage to scripts/repo_gate.py, including forward-declared pin marking, shebang detection, and gitattributes-style glob matching for dead-pin detection.
  • Expand scripts/test_repo_gate.py with unit/integration-style tests covering the new check, the forward-declared marker behavior, and matching semantics.
  • Update documentation and carried baseline prose to make the new behavior and the forward-declared convention explicit (scripts/README.md, .gitattributes, TODO.md).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
TODO.mdRecords follow-up work and links related to the newly closed gap and future symmetric .editorconfig coverage reading.
scripts/test_repo_gate.pyAdds/extends tests for eol-coverage, forward-declared pin parsing, gitattributes glob semantics, and non-vacuity floors.
scripts/repo_gate.pyImplements the new eol-coverage check plus shared pin parsing, shebang scanning, and dead-pin detection.
scripts/README.mdDocuments the new check, its rationale, and the forward-declared exemption mechanism.
.gitattributesMarks the lockfile/Dockerfile LF pins as forward-declared so dead-pin detection can distinguish carried-baseline pins from genuinely dead ones.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ptr727
ptr727 merged commit 0bb43c5 into developAug 9, 2026
7 checks passed
@ptr727
ptr727 deleted the feature/eol-coverage-check branch August 9, 2026 03:26
ptr727 added a commit that referenced this pull request Aug 9, 2026
…ate (#635)
Promotion of develop at 2082547, six squashes since the last one. Closes#633.
#631 routes the README by reader and documents the GH_WRITE_GUARD_ALLOW grant where a denied cross-owner write puts the reader. #632 moves readme.sections from intent to letter with four checks beside it, backed by spec/readme-sections.json and spec/third-party-tools.json, and settles the tagline rule. #634 adds repo_gate.py --check eol-coverage, reading the line-ending pins against the tree rather than only against .editorconfig.
#636 and #637 repair two readers Copilot found on this pull request, both defects develop already carried: a tool row required both outer table pipes that GitHub's Markdown makes optional, and a retired badge written as an inline image was invisible to a scan that read reference definitions alone. #638 turns the host contract's presence check into a version gate, and retires two gh workarounds that were artifacts of a stale distribution package, re-tested on an upgraded host rather than inferred.
Four carried files owe a downstream re-vendor and none is recorded in the TODO.md entry yet. GOVERNANCE.md Repository Details is verbatim, so the audit reports it, and it propagates a rule: the About description is the tagline alone, and Docker Hub receives it from the About panel rather than from the README. CODESTYLE.md item 4 and .gitattributes are intent, so nothing reports them, and the second couples to the new gate through the forward-declared mark. .github/copilot-instructions.md is intent and propagates a correction rather than a refresh, so a repo left on the old copy is wrong rather than merely stale.
ptr727 added a commit that referenced this pull request Aug 23, 2026
Adds a cluster to TODO.md tracking #931: whether Blog's \`-text\` plus
explicit named \`eol=lf\` pins plus a rotted-pin gate is an accepted
alternative to the fleet's \`text=auto eol=lf\` default.
Reflects the issue's own follow-up comment, which changes the original
post's position: option 3 (ask Blog to adopt \`text=auto eol=lf\`) is
now ruled out, since it would defeat \`eol-coverage\` (the check #634
built for exactly this failure mode) for the one repo whose bug caused
that check to exist. The comment now leans toward option 1 (document the
alternative in \`line-endings.md\`) over option 2 (a plain registry
driftNote), and raises a secondary open question about whether
\`eol-coverage\` runs in Blog's own CI.
No decision made here, this is the pointer per TODO.md's own convention,
the actual call is explicitly the maintainer's per the issue.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Added a decision record documenting Blog’s divergence from the
standard `.gitattributes` pattern.
* Linked the decision to the relevant tracking issue for future
reference.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for freeto 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

@ptr727