From df38d6c895178fff32440561649604bf908b6a38 Mon Sep 17 00:00:00 2001 From: James Juniper <217263268+jjuniper-dev@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:51:53 -0400 Subject: [PATCH] [PCA-209] Make skill hashes newline-portable --- docs/wf60-integration.md | 2 +- evidence/portability-proof.json | 4 +-- registry.yaml | 50 ++++++++++++++++----------------- tests/test_skill_contract.py | 11 ++++++++ tools/build_registry.py | 7 +++-- tools/resolve_skill.py | 5 ++-- tools/text_hash.py | 9 ++++++ tools/validate_repo.py | 5 ++-- 8 files changed, 58 insertions(+), 35 deletions(-) create mode 100644 tools/text_hash.py diff --git a/docs/wf60-integration.md b/docs/wf60-integration.md index 38e9945..bb7e819 100644 --- a/docs/wf60-integration.md +++ b/docs/wf60-integration.md @@ -14,7 +14,7 @@ A governed consumer resolves a skill to a binding containing: "source": { "repository": "https://github.com/jjuniper-dev/skills", "path": "skills/governed-execution/SKILL.md", - "revision": "v2.0.0", + "revision": "v2.0.1", "sha256": "" }, "authority": { diff --git a/evidence/portability-proof.json b/evidence/portability-proof.json index 11d2961..1c387a8 100644 --- a/evidence/portability-proof.json +++ b/evidence/portability-proof.json @@ -20,7 +20,7 @@ "source": { "repository": "https://github.com/jjuniper-dev/skills", "path": "skills/governed-execution/SKILL.md", - "revision": "v2.0.0", + "revision": "v2.0.1", "sha256": "29893ae60b4bb1af30ddbc166dcbdf5fb308e021ac20a7066e16a765bc5c7bc8" }, "contract_hash": "e5ca0983ef9259edcd5543a8c8bd5e58294212f3229733aa37ce9458e3813d33", @@ -48,7 +48,7 @@ "source": { "repository": "https://github.com/jjuniper-dev/skills", "path": "skills/governed-execution/SKILL.md", - "revision": "v2.0.0", + "revision": "v2.0.1", "sha256": "29893ae60b4bb1af30ddbc166dcbdf5fb308e021ac20a7066e16a765bc5c7bc8" }, "contract_hash": "e5ca0983ef9259edcd5543a8c8bd5e58294212f3229733aa37ce9458e3813d33", diff --git a/registry.yaml b/registry.yaml index e854ee5..51cb7e1 100644 --- a/registry.yaml +++ b/registry.yaml @@ -1,6 +1,6 @@ schema_version: '2.0' repository: https://github.com/jjuniper-dev/skills -release_revision: v2.0.0 +release_revision: v2.0.1 skills: - id: accessible-pptx name: Accessible PowerPoint (PPTX) @@ -8,7 +8,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/accessible-pptx/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 838cc7127448e5c62486cf7be723baf560d6a2d275fdc314daefb59fac42622d runtimes: - chatgpt @@ -27,7 +27,7 @@ skills: contract_version: '2.0' source_path: skills/agent-dispatch/SKILL.md contract_path: skills/agent-dispatch/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 74028141fcfc8e71535d2124a3154d7275c6ca2bf053981e38126ab040c3ebff runtimes: - chatgpt @@ -50,7 +50,7 @@ skills: contract_version: '2.0' source_path: skills/architecture-diagram/SKILL.md contract_path: skills/architecture-diagram/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 0b1700be8eebdbb219cb12069be8bf1dcdb78c35770e742c563a0ebb1571ac7e runtimes: - chatgpt @@ -71,7 +71,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/briefing-note/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: c126d0abf31640ee3cba5ebf77105dc766025bc01f2bc0fbc8af5079dd12c6bf runtimes: - chatgpt @@ -90,7 +90,7 @@ skills: contract_version: '2.0' source_path: skills/credential-authority/SKILL.md contract_path: skills/credential-authority/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: bb372414a5ae748e923f2cc3653c3a6821b3b3491746abd83e8fc85d9da00309 runtimes: - chatgpt @@ -114,7 +114,7 @@ skills: contract_version: '2.0' source_path: skills/ea-delivery-pipeline/SKILL.md contract_path: skills/ea-delivery-pipeline/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 7af8d24b14fdf9c7c0a324c509aa63b5102ef5969570ff947299d2e8790d0d74 runtimes: - chatgpt @@ -138,7 +138,7 @@ skills: contract_version: '2.0' source_path: skills/evidence-engineering/SKILL.md contract_path: skills/evidence-engineering/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: e252538c20f65b97f88bcabc5565640a73fcf91421320ad6ba0ca4ae11ecaa57 runtimes: - chatgpt @@ -160,7 +160,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/gctranslate-integration/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 5460e54b08609a89afd13c5ab64c84a37ee04bd8aa90d2c30a0ee25ceb5361c8 runtimes: - chatgpt @@ -179,7 +179,7 @@ skills: contract_version: '2.0' source_path: skills/governed-execution/SKILL.md contract_path: skills/governed-execution/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 29893ae60b4bb1af30ddbc166dcbdf5fb308e021ac20a7066e16a765bc5c7bc8 runtimes: - chatgpt @@ -202,7 +202,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/hc-phac-intelligence-stack/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: f2a8df4c031ba1c86a91e02c109acf14257f4f58615d6631fa1b6337a5d8fb58 runtimes: - chatgpt @@ -220,7 +220,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/intelligence-monitoring-platform/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 4ff92e2b936130dcba27151d8c57377505e4d5ded4c14a88435aac39afe41eee runtimes: - chatgpt @@ -238,7 +238,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/n8n-intelligence-automation/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 5e4e6945aa9980e21f17e641d179129d72575110a8fce6a983831b5fefc91919 runtimes: - chatgpt @@ -256,7 +256,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/neo4j-graph-builder/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 2b43b718bfef246262fb3c7827d5e9d5b28bda284fdb782e05e03b98d294c6a1 runtimes: - chatgpt @@ -274,7 +274,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/news-gathering-integration/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: a71b34bba046b2b4d568389b1a59f1db190f898b17a685eca3e41e2c59b83667 runtimes: - chatgpt @@ -293,7 +293,7 @@ skills: contract_version: '2.0' source_path: skills/outcome-verification/SKILL.md contract_path: skills/outcome-verification/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: ee61af5616c64fec6893da4d46b032f494f09a1fbcbf160b8f5600655ba69509 runtimes: - chatgpt @@ -316,7 +316,7 @@ skills: status: deprecated contract_version: '1.0' source_path: skills/powerpoint-arb-deck/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 6ce8010e41b6723a1f843180c6b4e0893e01500e3b9277f7fbdd152e0ce66df2 runtimes: - chatgpt @@ -334,7 +334,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/presentation-deck/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 3c3f6cfac88579ff1f9050fe0b7bff6fa2fc483d5f1300eeda9e17e4e758fb1e runtimes: - chatgpt @@ -352,7 +352,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/prompts/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 9782f3222ea581dfcac129bc9d6832a5de01c185217e7c7d353f769dc29f6947 runtimes: - chatgpt @@ -371,7 +371,7 @@ skills: contract_version: '2.0' source_path: skills/qa-recording-evidence/SKILL.md contract_path: skills/qa-recording-evidence/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: ccbe91a2500247a620693206db90e9ff9546699e6fc7e037d3cf2475ed7b6e27 runtimes: - chatgpt @@ -394,7 +394,7 @@ skills: contract_version: '2.0' source_path: skills/runtime-evidence/SKILL.md contract_path: skills/runtime-evidence/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 29e8cb45580a57229c3f892ae4e7befb45fa2bd8fe65e53448af3e229671afde runtimes: - chatgpt @@ -419,7 +419,7 @@ skills: contract_version: '2.0' source_path: skills/session-handover/SKILL.md contract_path: skills/session-handover/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: bfba050c3adad7ff4efffd55fc8fd75968ac9961eea1893b375ddb815ab79686 runtimes: - chatgpt @@ -441,7 +441,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/social-feed-news-integration/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 1dc56b75689c8e8457a30d5605e687769f106f6db297b4b778fb3de3b55b3d4a runtimes: - chatgpt @@ -459,7 +459,7 @@ skills: status: legacy contract_version: '1.0' source_path: skills/strategic-screening/SKILL.md - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 8a17a233383e96dc86cbd063b6330dc80524a7900a1c060a0a688d8b1310e9a8 runtimes: - chatgpt @@ -478,7 +478,7 @@ skills: contract_version: '2.0' source_path: skills/work-package-orchestration/SKILL.md contract_path: skills/work-package-orchestration/skill.yaml - source_revision: v2.0.0 + source_revision: v2.0.1 source_sha256: 85310d141d6241bec0830c60d14eb608f5d830966c659c04d57bec8ea2eaf22e runtimes: - chatgpt diff --git a/tests/test_skill_contract.py b/tests/test_skill_contract.py index 153e627..b82ddeb 100644 --- a/tests/test_skill_contract.py +++ b/tests/test_skill_contract.py @@ -1,4 +1,5 @@ import sys +import tempfile from pathlib import Path import unittest @@ -6,6 +7,7 @@ sys.path.insert(0, str(ROOT / "tools")) from resolve_skill import resolve +from text_hash import canonical_text_sha256 from validate_repo import validate class SkillContractTests(unittest.TestCase): @@ -27,5 +29,14 @@ def test_wf60_binding_is_explicit(self): self.assertTrue(binding["source"]["path"].startswith("skills/")) self.assertEqual(len(binding["source"]["sha256"]), 64) + def test_text_hash_is_portable_across_line_endings(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + lf = root / "lf.md" + crlf = root / "crlf.md" + lf.write_bytes(b"# Skill\nline two\n") + crlf.write_bytes(b"# Skill\r\nline two\r\n") + self.assertEqual(canonical_text_sha256(lf), canonical_text_sha256(crlf)) + if __name__ == "__main__": unittest.main() diff --git a/tools/build_registry.py b/tools/build_registry.py index 68117c8..31d142c 100644 --- a/tools/build_registry.py +++ b/tools/build_registry.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 -import hashlib import re from pathlib import Path import yaml +from text_hash import canonical_text_sha256 + ROOT = Path(__file__).resolve().parents[1] SKILLS = ROOT / "skills" -RELEASE = "v2.0.0" +RELEASE = "v2.0.1" def sha256(path): - return hashlib.sha256(path.read_bytes()).hexdigest() + return canonical_text_sha256(path) def skill_name(text, slug): match = re.search(r"^# Skill:\s*(.+)$", text, re.MULTILINE) diff --git a/tools/resolve_skill.py b/tools/resolve_skill.py index 1f3bc96..066ab57 100644 --- a/tools/resolve_skill.py +++ b/tools/resolve_skill.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 import argparse -import hashlib import json from pathlib import Path import sys import yaml +from text_hash import canonical_text_sha256 + ROOT = Path(__file__).resolve().parents[1] REPOSITORY = "https://github.com/jjuniper-dev/skills" def sha256_file(path): - return hashlib.sha256(path.read_bytes()).hexdigest() + return canonical_text_sha256(path) def load_registry(): return yaml.safe_load((ROOT / "registry.yaml").read_text(encoding="utf-8")) diff --git a/tools/text_hash.py b/tools/text_hash.py new file mode 100644 index 0000000..df768d1 --- /dev/null +++ b/tools/text_hash.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import hashlib +from pathlib import Path + + +def canonical_text_sha256(path: Path) -> str: + text = path.read_text(encoding="utf-8") + normalized = text.replace("\r\n", "\n").replace("\r", "\n") + return hashlib.sha256(normalized.encode("utf-8")).hexdigest() diff --git a/tools/validate_repo.py b/tools/validate_repo.py index ac5a345..e26b17b 100644 --- a/tools/validate_repo.py +++ b/tools/validate_repo.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 import argparse -import hashlib import json from pathlib import Path import jsonschema import yaml +from text_hash import canonical_text_sha256 + ROOT = Path(__file__).resolve().parents[1] def repo_files(pattern): return [p for p in ROOT.rglob(pattern) if ".git" not in p.parts and "__pycache__" not in p.parts] def sha256_file(path): - return hashlib.sha256(path.read_bytes()).hexdigest() + return canonical_text_sha256(path) def load_yaml(path): return yaml.safe_load(path.read_text(encoding="utf-8"))