Long-term correct fix for the drift algorithm. Closes the archaeology wrinkle exposed by the new path-filtered SHA approach (see the close comment on #1). Tracks the design work; not urgent because the path-filtered SHA in n3ary/actions#3 + n3ary/standards#3 stopped the user-visible false positives.
Why this still matters
The current drift algorithm uses "latest commit on main touching standards/" as the canonical SHA. That kills false positives (a .gitignore-only commit on n3ary/standards/main doesn't move the canonical SHA), but it has a residual quirk:
- Two non-content commits (e.g.
a21f890 and 7f9f83c) can sit between the last content commit and HEAD.
- A new vendor run would re-stamp the vendored files with the content-touching SHA (
063866e today) - backwards from HEAD (7f9f83c). The vendor side and drift side agree, but a human reading <!-- synced from n3ary/standards@063866e --> cannot infer "this content is also in 7f9f83c" without running the path-filter lookup by hand.
- Today's n3ary/gtfs-adapters#19 / n3ary/gtfs-adapters#23 dance (
7f9f83c then 063866e) was a direct consequence.
Proposed shape
Stamp a content hash into the vendored file header alongside (or instead of) the commit SHA, and have the drift check compare on content rather than SHA.
Header format
<!-- synced from n3ary/standards@<sha> on <date> -->
<!-- content-sha256: <hash> -->
<!-- do not edit locally; run scripts/vendor-standards.mjs to update -->
<standard content>
Where <hash> is sha256 of the concatenated standards/*.md file contents (sorted by filename, separated by \n---\n or similar unambiguous delimiter). Deterministic across vendors and consumers.
Drift check
Replace the SHA comparison:
LATEST_CONTENT_HASH=$(gh api .../standards/contents | ... sha256sum ...)
VENDORED_HASH=$(grep -oP 'content-sha256:\s*\K\S+' "$f")
[ "$VENDORED_HASH" != "$LATEST_CONTENT_HASH" ] && DRIFT=1
Note: the canonical content lives in standards/*.md on n3ary/standards, so the hash can be computed from a single fetch (concatenated raw content) rather than needing a local checkout.
What this buys
- The vendored header is content-addressable:
content-sha256: abc123 is the same no matter which commit produced it. Drift detection focuses on actual content changes - not git history.
- Two non-content commits (
a21f890, 7f9f83c) no longer wedge the canonical SHA out of sync with what the vendored files actually carry.
- A first-time vendor of
docs/standards/* into a fresh consumer produces a header that already passes drift-check on the next day, with no manual SHA bumps.
- The "synced from @" header stays useful for
git blame archaeology - it's a stable pointer to the commit that produced this content, not a moving target.
What this costs
Cross-repo change, three pieces:
n3ary/standards/scripts/vendor-standards.mjs:
- New
lastStandardsContentHash() helper (compute from standards/*.md).
- Header emits both SHA and content hash.
- Keep the path-filtered SHA lookup so the "synced from @" stays meaningful (and so daily sync still triggers via
sync-standards.yml).
n3ary/actions/.github/workflows/check-standards-drift.yml:
- Switch the comparison to content hash.
LATEST_CONTENT_HASH computed once per run from standards/* raw content.
- Consumer repos:
[n3ary/app](https://github.com/n3ary/app), [n3ary/gtfs-publisher](https://github.com/n3ary/gtfs-publisher), [n3ary/gtfs-adapters](https://github.com/n3ary/gtfs-adapters), plus any future adopter:
- On first deploy of the new algorithm, a one-time migration: vendor-standards.mjs re-runs and emits the new
content-sha256: header. Drift-check still passes because the SHA in the header also gets refreshed in the same run.
Migration plan
- Land the new algorithm in
n3ary/actions first (drift-check accepts both SHA and content hash during transition, prefers hash when present).
- Land the new header format in
vendor-standards.mjs next; have it emit both old and new fields during a single sync run, so consumers automatically pick up the new header on the next sync.
- Once all consumers have
content-sha256: ... in their headers, simplify drift-check to compare hash only.
- Document the new header format in
standards/README.md so future consumer authors know what the second comment is.
Open questions
- Hash input ordering: alphabetical by filename is the obvious choice, but the standards directory has a
README.md and SHARED-STANDARDS.md that are excluded by the vendor - does the hash include them, or only what's vendored?
- Hash algorithm:
sha256 is fine, but if we ever want Git-blame-style traceability, git hash-object-equivalent over a tree could work. Probably overkill for "did the content change?" - lean on sha256.
- Should the path-filtered SHA lookup be retained at all? Today it serves two purposes: (1) the human-readable "synced from @" label, (2) the daily
sync-standards.yml trigger source. If we move (2) to a paths: [standards/**] workflow filter alone, the SHA lookup becomes purely informational and could be simplified to "HEAD of main" again. Worth a follow-up sub-decision.
Related
Long-term correct fix for the drift algorithm. Closes the archaeology wrinkle exposed by the new path-filtered SHA approach (see the close comment on #1). Tracks the design work; not urgent because the path-filtered SHA in n3ary/actions#3 + n3ary/standards#3 stopped the user-visible false positives.
Why this still matters
The current drift algorithm uses "latest commit on main touching
standards/" as the canonical SHA. That kills false positives (a.gitignore-only commit onn3ary/standards/maindoesn't move the canonical SHA), but it has a residual quirk:a21f890and7f9f83c) can sit between the last content commit and HEAD.063866etoday) - backwards from HEAD (7f9f83c). The vendor side and drift side agree, but a human reading<!-- synced from n3ary/standards@063866e -->cannot infer "this content is also in 7f9f83c" without running the path-filter lookup by hand.7f9f83cthen063866e) was a direct consequence.Proposed shape
Stamp a content hash into the vendored file header alongside (or instead of) the commit SHA, and have the drift check compare on content rather than SHA.
Header format
Where
<hash>issha256of the concatenatedstandards/*.mdfile contents (sorted by filename, separated by\n---\nor similar unambiguous delimiter). Deterministic across vendors and consumers.Drift check
Replace the SHA comparison:
Note: the canonical content lives in
standards/*.mdonn3ary/standards, so the hash can be computed from a single fetch (concatenated raw content) rather than needing a local checkout.What this buys
content-sha256: abc123is the same no matter which commit produced it. Drift detection focuses on actual content changes - not git history.a21f890,7f9f83c) no longer wedge the canonical SHA out of sync with what the vendored files actually carry.docs/standards/*into a fresh consumer produces a header that already passes drift-check on the next day, with no manual SHA bumps.git blamearchaeology - it's a stable pointer to the commit that produced this content, not a moving target.What this costs
Cross-repo change, three pieces:
n3ary/standards/scripts/vendor-standards.mjs:lastStandardsContentHash()helper (compute fromstandards/*.md).sync-standards.yml).n3ary/actions/.github/workflows/check-standards-drift.yml:LATEST_CONTENT_HASHcomputed once per run fromstandards/*raw content.[n3ary/app](https://github.com/n3ary/app),[n3ary/gtfs-publisher](https://github.com/n3ary/gtfs-publisher),[n3ary/gtfs-adapters](https://github.com/n3ary/gtfs-adapters), plus any future adopter:content-sha256:header. Drift-check still passes because the SHA in the header also gets refreshed in the same run.Migration plan
n3ary/actionsfirst (drift-check accepts both SHA and content hash during transition, prefers hash when present).vendor-standards.mjsnext; have it emit both old and new fields during a single sync run, so consumers automatically pick up the new header on the next sync.content-sha256: ...in their headers, simplify drift-check to compare hash only.standards/README.mdso future consumer authors know what the second comment is.Open questions
README.mdandSHARED-STANDARDS.mdthat are excluded by the vendor - does the hash include them, or only what's vendored?sha256is fine, but if we ever want Git-blame-style traceability,git hash-object-equivalent over a tree could work. Probably overkill for "did the content change?" - lean onsha256.sync-standards.ymltrigger source. If we move (2) to apaths: [standards/**]workflow filter alone, the SHA lookup becomes purely informational and could be simplified to "HEAD of main" again. Worth a follow-up sub-decision.Related