Skip to content

fix(vectors): pin conformance vectors to LF with .gitattributes (#1160) - #1177

Merged
miles-on-nightshift merged 2 commits into
plur-ai:mainfrom
amasen02:fix/pin-conformance-vectors-lf-1160
Sep 16, 2026
Merged

miles-on-nightshift merged 2 commits into
plur-ai:mainfrom
amasen02:fix/pin-conformance-vectors-lf-1160

Conversation

@amasen02

@amasen02 amasen02 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves #1160.

The conformance vector corpus (spec/vectors) provides byte-exact fixtures (§5.5 integrity hashes and §6.7 capsules) for multi-language implementations.

As observed in #1160:

  1. The repository had no .gitattributes. When cloned with core.autocrlf=true (common default on Windows), text files under spec/vectors received CRLF line endings, mutating all raw-byte SHA-256 integrity hashes (§5.5) and reporting 11 false-positive MISMATCH notices across the vector corpus during verification.
  2. In spec/vectors/build.py:220,225,530 and build_capsules.py:288, INTEGRITY, index.json, and capsules.json were written with Path.write_text without encoding="utf-8" or newline="". On Windows, Python translated \n to \r\n, causing build.py --check to report drift on files it generated itself.

Proposed Changes

  1. .gitattributes:
    • Add repository-wide * text=auto eol=lf to ensure LF normalization on commit and checkout across all platforms.
    • Add spec/vectors/** -text to preserve exact byte stability across platforms for conformance vector fixtures.
  2. spec/vectors/build.py:
    • Pass encoding="utf-8", newline="" to (d / "INTEGRITY").write_text(...) and INDEX.write_text(...) so generated fixtures use universal LF newlines and explicit UTF-8 on Windows.
  3. spec/vectors/build_capsules.py:
    • Pass encoding="utf-8", newline="" to INDEX.write_text(...).

Verification

  • python spec/vectors/build.py --check: Vectors and index.json match what this script builds. (Exit code 0, 0 drift).
  • python spec/vectors/build_capsules.py --check: Capsules and capsules.json match what this script builds. (Exit code 0, 0 drift).
  • python spec/vectors/verify.py --index spec/vectors/index.json --capsules spec/vectors/capsules.json: 26 fixture(s) checked, 0 failure(s) (all false-positive mismatches eliminated).
  • npx vitest run spec-vectors: 102/102 passed.
  • packages/core builds cleanly via tsup.

…-ai#1160)

* Add repository-level .gitattributes to auto-detect text files with eol=lf
  and preserve exact binary bytes for spec/vectors/** (-text).
* In spec/vectors/build.py, pass encoding="utf-8" and newline="" to
  (d / "INTEGRITY").write_text() and INDEX.write_text() to prevent CRLF
  emission and locale encoding dependency on Windows.
* In spec/vectors/build_capsules.py, pass encoding="utf-8" and newline=""
  to INDEX.write_text().
* Resolves false-positive integrity mismatches when repository is checked
  out on platforms with core.autocrlf enabled.

Signed-off-by: Ama Senevirathne <amasenevirathne7@gmail.com>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

ℹ️ Could not auto-assign #1160 to @amasen02 — GitHub only allows assigning users with repo access or prior interaction on the issue. Maintainer: please assign it manually so the claim is recorded. This is not a problem with the PR.

@miles-on-nightshift miles-on-nightshift 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.

Correct fix for the Windows CRLF issue. .gitattributes with * text=auto eol=lf normalises all text files on checkout, and spec/vectors/** -text keeps the binary/byte-exact fixtures untouched. The build.py change pins encoding and newline explicitly so regeneration on any OS produces identical output. CI passing. @plur9 ready for merge.

@amasen02

amasen02 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the approval! Both this and #1176 are green on all checks — happy to make any final adjustments if needed, otherwise ready to merge whenever suits.

@crtahlin crtahlin 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.

Requesting changes — the generator fix uses a Python 3.10 API, so build.py --check now crashes on Python 3.9, which is what macOS ships as /usr/bin/python3.

Thank you for this. The diagnosis is right, the two-rule .gitattributes is right, and you correctly saw that attributes alone would not have been enough — git governs checkout and commit, not what Python writes on a Windows machine. The blocking issue is one line each and the file already contains the idiom that fixes it.

Blocking

  • spec/vectors/build.py:220,225,530 and spec/vectors/build_capsules.py:288 — Path.write_text() gained its newline keyword only in Python 3.10. On 3.9 all four calls raise TypeError: write_text() got an unexpected keyword argument 'newline'. Verified on two separate interpreters: this machine's /usr/bin/python3 (3.9.6) and a clean 3.9.25 — python3 spec/vectors/build.py --check crashes at line 220, and a full regeneration crashes at the same place. Both scripts run clean on 3.9 before this change.

    That silently raises the toolchain floor from 3.9 to 3.10 for a directory the README describes as "Python, stdlib only" and whose CI comment says the gates must "run anywhere". CI cannot catch it: ci.yml:118-123 uses the runner's bare python3 with no setup-python step.

    The file already has the answer — build.py:211,212,231 and build_capsules.py:242 use write_bytes. So:

    (d / "INTEGRITY").write_bytes((correct + "\n").encode("utf-8"))

    Byte-exact, no version floor, and more obviously correct than relying on a text-mode newline flag.

Should fix

  • .gitattributes:5 — spec/vectors/** is broader than the fixture corpus. git check-attr confirms build.py, build_capsules.py, verify.py and README.md also get -text, so the very scripts that enforce byte stability are themselves exempt from LF normalization and a Windows contributor could commit CRLF into them. Either narrow to spec/vectors/packs/**, spec/vectors/capsules/** plus the two JSON indexes, or add spec/vectors/*.py text eol=lf after line 5.

Notes

  • The fix protects fresh clones only. Upgrading an existing core.autocrlf=true clone from main to this commit leaves 47 vector files still CRLF, verify.py still reporting 34 failures, and git status reporting the tree clean — so nothing points at the cause. Recovery is rm -rf spec/vectors && git checkout -- spec/vectors. Filed as #1181 rather than asked of you, since it is a README and CONTRIBUTING change rather than part of this fix.
  • build.py:517, build_capsules.py:276 — the writes are newline-pinned but the matching --check reads still use Path.read_text(), which applies universal-newline translation and would silently accept a CRLF index.json as matching. Not a regression, arguably the tolerant choice, but the drift gate cannot detect a CRLF index on its own.
  • The undeclared interpreter floor that let this through is filed as #1182 — a CI gap rather than something to fix here.

Verified

The original defect and the fix were both reproduced end to end. A clone of main with core.autocrlf=true rewrites 49 of 62 vector files to CRLF and verify.py reports 34 failures; the same clone at this commit keeps all 62 files LF, and all three Python gates plus the 102-test spec-vectors suite pass.

The repository-wide rule is safe here, and I checked rather than assumed: git ls-files --eol shows 933 of 955 files already stored LF and nothing stored CRLF, git add --renormalize . on a fresh checkout stages nothing, there are no .bat/.cmd/.ps1 files and no CRLF-asserting fixtures. The two rules also interact correctly — git check-attr -a reports text: unset for SKILL.md, engrams.yaml, INTEGRITY, .jsonld and the .plur capsules, so -text wins over * text=auto for every vector path. Choosing -text rather than binary was right: binary would also have unset the diff attribute, and vector diffs stay readable.

Tests: PASS on Python 3.13 (all three gates green, 102/102 vitest). FAIL on Python 3.9, which is the blocking finding above.

…e corpus

- Replace Path.write_text(newline="") (3.10+) with write_bytes at the four
  write sites; byte-identical output, no interpreter floor.
- Narrow spec/vectors/** -text to packs/**, capsules/** and the two JSON
  indexes so the gate scripts and README stay under LF normalization.
@amasen02

amasen02 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @crtahlin — excellent review, both points addressed in 87fa481:

Blocking (Py 3.9): all four write_text(..., newline="") calls replaced with the file's own write_bytes idiom — (correct + "\n").encode("utf-8") etc. No version floor left; stdlib-only works on 3.9 again.

Should-fix (.gitattributes scope): narrowed spec/vectors/** -text to packs/**, capsules/** and the two JSON indexes; the gate scripts and README now get text: auto eol=lf like the rest of the repo.

Verified locally (Python 3.12): build.py --check ✓, build_capsules.py --check ✓, verify.py 13 fixtures / 0 failures ✓. Regeneration is byte-identical (index.json + capsules.json sha256 unchanged after full rm -rf packs capsules + rebuild), so the committed corpus is untouched by the write-path change. I can't easily run a clean 3.9 here (Windows host), but the changed lines now use only .encode() / write_bytes, which are 3.0-era APIs — happy to re-run if you want a 3.9 CI confirmation on top of #1182.

@amasen02

amasen02 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Confirming CI on 87fa481 is fully green: test (20), test (22), test (24), test (26), dedup-calibration, and smoke-packaged all pass (run 34352272721). @crtahlin whenever you have a moment, a re-review would be much appreciated — thanks again for the exceptionally thorough review, it made the fix straightforward.

@amasen02

Copy link
Copy Markdown
Contributor Author

Thank you for merging #1176! 🎉

This one (#1177) is also approved by @crtahlin, has the review feedback addressed in 87fa481, and CI is fully green (run 34352272721). Whenever you have a moment, a merge would be greatly appreciated — happy to make any final adjustments if needed.

@crtahlin crtahlin 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.

Approved. Both points from my earlier review are resolved, exactly as suggested — thank you for the quick and careful turnaround.

All four write_text(..., newline="") calls now use the file's own write_bytes(....encode("utf-8")) idiom, which is byte-exact and carries no interpreter floor. And -text is narrowed to the generated corpus — packs/**, capsules/**, index.json and capsules.json — so the gate scripts and the README fall back under LF normalization with the rest of the repository.

You mentioned you could not test on Python 3.9 from a Windows host, so I did:

  • Python 3.9. The three gates pass on the macOS system interpreter, 3.9.6, and on a separate 3.9.25 build. Restoring the old write_text(newline=) at build.py:220 makes 3.9.6 fail with the original TypeError, so the green run genuinely exercises the fixed line.
  • Byte-identical output. A full regeneration on 3.9.6 of all 13 packs and 13 capsules leaves 0 files differing from the committed corpus.
  • Other interpreters. The gates also pass on 3.11, 3.13 and 3.14. spec-vectors passes 102 of 102.
  • The original defect. Under core.autocrlf=true, main converts 49 of 62 vector files on checkout and verify.py reports 34 failures. This head converts 0 of 62 and passes, with build.py --check and build_capsules.py --check both matching.
  • Coverage of the narrowed rule. Every tracked file under spec/vectors/ was checked with git check-attr. The only ones outside the four -text patterns are README.md and the three scripts, which is the intent.

issue-guard does not appear on the latest commit because it triggers only on opened, edited and reopened, not on a push. That is expected, not a failure.

The two related items from the earlier review — documenting how to repair a clone already checked out as CRLF (#1181), and enforcing the interpreter floor the gates depend on (#1182) — are tracked separately and are not needed here.

@amasen02

Copy link
Copy Markdown
Contributor Author

Friendly 48h follow-up on this PR — the requested changes are implemented and CI is green (evidence posted above). Happy to adjust further if anything else is needed. Thanks for the review!

crtahlin added a commit that referenced this pull request Sep 11, 2026
…the record (#1164 review)

Second round of review fixes, following the maintainer's re-review.

The guard now enforces only when the dismissed review had requested changes.
The `review_dismissed` timeline event carries the review's state before
dismissal in `dismissed_review.state`. An approved or commented review passes,
and an unknown state is enforced rather than waved through. This matters
because the `dismiss_stale_reviews` rule removes an approval on every push,
recording the pusher as actor with no message. At the previous head that would
have failed on #1177 with "@amasen02 dismissed a review without a message" — a
public red check against an external contributor for something nobody chose to
do. Removing an approval never bypasses review, so nothing is lost by not
enforcing there.

A missing timeline event still fails closed. Every observed case (#1122, #1138,
and one of the two on #1017) was a manual dismissal of a changes-requested
review, which is exactly the case this guard exists for, so passing on a
missing event would reopen the hole. The earlier suggestion to treat "no event,
and the review's commit is not the head" as an automatic dismissal was wrong
for the same reason: it would have passed all three.

The header no longer says the empty dismissals were people skipping a reason.
A census by previous review state, 2026-08-18 to 2026-09-11, finds fourteen
dismissals: six empty and eight with a message. All six empty ones were of
approvals, and no changes-requested review was dismissed empty. The guard is
preventive, and the header now says so. The unverified fork explanation is
gone.

The header no longer suggests adding this as a required status check. The job
runs only on a dismissal, so a pull request without one never reports it, and
its result attaches to the head at dismissal time, so the next push leaves it
behind. A gate would need a `pull_request` trigger that re-evaluates every
dismissal.

The remediation text now names what actually clears a failure — ask the
reviewer to re-review, or push a commit — because a dismissed review cannot be
dismissed again. A dismissal made only of invisible characters no longer
passes: `trim()` leaves zero-width spaces, byte-order marks and directional
controls in place. The class that strips them is written with escapes, because
a literal U+2028 or U+2029 breaks both the script and the YAML.

Verified: 24 cases against stubs, all passing, four of them replayed from the
live timeline. #1177 and #929 (approvals dismissed with no message) pass, #946
(changes requested, with a message) passes, and #1138 (changes requested, no
event) fails closed. Removing the prior-state gate turns five cases red,
including both live approval replays; replacing the strip with plain `trim()`
turns three red.
@amasen02

Copy link
Copy Markdown
Contributor Author

Gentle second follow-up (48h since my last note): this is still approved, CI-green on 87fa481, and ready to merge whenever a slot opens — congrats again on landing #1176! Happy to address anything further. Thanks!

@miles-on-nightshift miles-on-nightshift 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.

Verified:

  • CI: all 4 Node matrix variants pass (test 20/22/24/26, dedup-calibration, smoke-packaged)
  • .gitattributes: * text=auto eol=lf normalises text files; spec/vectors/packs/**, spec/vectors/capsules/**, and the two index JSON files are scoped -text so their raw bytes are not touched on checkout — exactly the path the §5.5 and §6.7 integrity hashes cover
  • write_bytes in build.py and build_capsules.py: correct fix for Python write_text inserting CRLF on Windows even with an explicit encoding arg; INDEX.write_bytes and both INTEGRITY writes now encode UTF-8 explicitly before write, matching the spec's byte-stability requirement
  • Sibling check: all three INTEGRITY write sites and both index writes in both build scripts are covered

@miles-on-nightshift
miles-on-nightshift merged commit b17b35b into plur-ai:main Sep 16, 2026
6 checks 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.

Pin the conformance vectors to LF with .gitattributes

4 participants