Skip to content

Commit fd5c5c8

Browse files
committed
feat(registry): index AffineScript external specs as verified pointers
Fold the AffineScript v2 standards (.affine / .affex / .affmap) into the estate's authoritative generated registry (.machine_readable/REGISTRY.a2ml + scripts/build-registry.sh + HYP-S006) instead of a parallel registry — owner decision 2026-06-03. These specs are language-coupled: their SSOT is hyperpolymath/affinescript, not this repo. The registry holds a verified POINTER, never a copy, so the normative text is never duplicated (SSOT preserved). - scripts/build-registry.sh: add an EXTERNAL_SPECS table + emission path for kind="external" entries (spec_kind, owning_repo, canonical_url, version_pin, source_hash, source_hash_algo, conformance_level, last_synced, sync_status, media_type, lineage; format_version for .affex, tracked independently of the language version_pin per owner directive). source_hash is RECORDED (sentinel PENDING-FIRST-SYNC until upstream lands) and emitted verbatim — the offline generator never fetches, so --check stays deterministic. Populates the reserved `language` stream with affine/affex/affmap. - .machine_readable/REGISTRY.a2ml + TOPOLOGY.md: regenerated (28 local + 3 external = 31 specs; language stream now rendered). - hypatia-rules/registry-staleness.a2ml (HYP-S006): extend to verify external pointers by re-fetching canonical_url (the network-side check the offline generator and registry-verify.yml can't do); reject md5/sha1; treat the sentinel as awaiting-upstream, not drift; cap external re-pin to :review (owner decision, not an auto-regen). - REGISTRY.adoc: document the external-pointer entry shape + .affcite note. No duplicate HYP-S006 and no parallel top-level REGISTRY.a2ml: this extends main's system rather than competing with it. Workflow CI fixes moved to a separate PR (#363).
1 parent e763d82 commit fd5c5c8

5 files changed

Lines changed: 263 additions & 29 deletions

File tree

.machine_readable/REGISTRY.a2ml

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@
77
# Regenerate with: bash scripts/build-registry.sh (or: just registry)
88
# Source of truth: the SPECS table in scripts/build-registry.sh + the file tree.
99
#
10-
# Each entry's `source_hash` is a sha256 over `git ls-files -s <home>`, so it
11-
# changes whenever any tracked file under the spec's home changes. Hypatia rule
12-
# HYP-S006 (hypatia-rules/registry-staleness.a2ml) recomputes these and emits a
10+
# LOCAL entries: `source_hash` is a sha256 over `git ls-files -s <home>`, so it
11+
# changes whenever any tracked file under the spec's home changes.
12+
# EXTERNAL entries (kind = "external"): a verified POINTER to a spec whose SSOT
13+
# lives in another repo. `source_hash` is RECORDED (sentinel PENDING-FIRST-SYNC
14+
# until upstream lands), not computed locally — the offline generator emits it
15+
# verbatim. Hypatia rule HYP-S006 (hypatia-rules/registry-staleness.a2ml)
16+
# recomputes LOCAL hashes and re-fetches EXTERNAL canonical_urls, emitting a
1317
# `doc.drift` finding (strategy :review) when a recorded hash goes stale.
1418

1519
[registry]
1620
version = "1.0.0"
1721
generator = "scripts/build-registry.sh"
18-
hash_algorithm = "sha256(git ls-files -s <home>)"
19-
entry_count = 28
22+
hash_algorithm = "sha256(git ls-files -s <home>) # local; external: recorded pin"
23+
entry_count = 31
2024

2125
[registry.streams]
2226
foundation = "A2ML format family + K9 + contractiles (Stream 1)"
@@ -278,4 +282,59 @@ canonical_doc = "a2ml-templates/STATE.a2ml.v2.spec.adoc"
278282
source_hash = "sha256:5105bc72621b6214f1adecdf33a1dadf62d1d2b0afd0c2c6a48bbc5e24e9a454"
279283
route = "copy-in templates for the 7 A2ML files"
280284

285+
[[spec]]
286+
id = "affine-spec"
287+
name = "AffineScript .affine (faces / source documents)"
288+
stream = "language"
289+
kind = "external"
290+
spec_kind = "language-coupled"
291+
owning_repo = "hyperpolymath/affinescript"
292+
canonical_url = "https://github.com/hyperpolymath/affinescript/blob/main/spec/affine.adoc"
293+
version_pin = "v2.0.0"
294+
source_hash = "PENDING-FIRST-SYNC"
295+
source_hash_algo = "sha256"
296+
conformance_level = "draft"
297+
last_synced = "never"
298+
sync_status = "awaiting-upstream"
299+
media_type = "application/vnd.affinescript.affine"
300+
lineage = "affinescript:affine@2"
301+
route = "faces, canonical-lowering invariant, canonical islands, idiom packs, mimicry bindings, project face policy"
302+
303+
[[spec]]
304+
id = "affex-manifest"
305+
name = "AffineScript .affex (face-interop manifest)"
306+
stream = "language"
307+
kind = "external"
308+
spec_kind = "language-coupled"
309+
owning_repo = "hyperpolymath/affinescript"
310+
canonical_url = "https://github.com/hyperpolymath/affinescript/blob/main/spec/affex.adoc"
311+
version_pin = "v2.0.0"
312+
source_hash = "PENDING-FIRST-SYNC"
313+
source_hash_algo = "sha256"
314+
conformance_level = "draft"
315+
last_synced = "never"
316+
sync_status = "awaiting-upstream"
317+
media_type = "application/vnd.affinescript.affex"
318+
lineage = "affinescript:affex@2"
319+
route = "derived regenerable manifest; declaration heads not full bodies; format_version bumps independently"
320+
format_version = "2" # tracked independently of version_pin
321+
322+
[[spec]]
323+
id = "affmap-provenance"
324+
name = "AffineScript .affmap (provenance)"
325+
stream = "language"
326+
kind = "external"
327+
spec_kind = "language-coupled"
328+
owning_repo = "hyperpolymath/affinescript"
329+
canonical_url = "https://github.com/hyperpolymath/affinescript/blob/main/spec/affmap.adoc"
330+
version_pin = "v2.0.0"
331+
source_hash = "PENDING-FIRST-SYNC"
332+
source_hash_algo = "sha256"
333+
conformance_level = "draft"
334+
last_synced = "never"
335+
sync_status = "awaiting-upstream"
336+
media_type = "application/vnd.affinescript.affmap"
337+
lineage = "affinescript:affmap@2"
338+
route = "provenance format; own pointer for independent staleness tracking"
339+
281340
### End of REGISTRY.a2ml

REGISTRY.adoc

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,48 @@ The registry is *honest by construction*: the generator only emits a spec
3939
whose `home` directory actually exists, and reports a missing home to stderr
4040
rather than inventing one.
4141

42+
=== External pointers (specs whose SSOT lives in another repo)
43+
44+
Some specs are *language/service-coupled*: their source-of-truth deliberately
45+
lives in another repo whose release cadence this monorepo must not own. The
46+
clearest example is the AffineScript v2 standards — `.affine` (faces / source
47+
documents), `.affex` (face-interop manifest) and `.affmap` (provenance) — whose
48+
SSOT is `hyperpolymath/affinescript`. For these the registry holds a *verified
49+
pointer, never a copy*; duplicating the normative text would create two sources
50+
of truth and guarantee drift.
51+
52+
External entries carry `kind = "external"` and, in place of a local `home`:
53+
54+
[cols="1,3", options="header"]
55+
|===
56+
| Field | Meaning
57+
58+
| `spec_kind` | `language-coupled` or `service-coupled`.
59+
| `owning_repo` | The repo that authors + versions the spec.
60+
| `canonical_url` | The upstream source-of-truth document (what HYP-S006 fetches).
61+
| `version_pin` | The upstream version/tag this pointer is pinned to.
62+
| `source_hash` | The *recorded* upstream hash, or the sentinel `PENDING-FIRST-SYNC` until upstream lands. NEVER fabricated; NEVER fetched at generation time.
63+
| `source_hash_algo` | `sha256` (md5/sha1 are rejected as unverified).
64+
| `media_type` / `lineage` | Provenance fields, consistent with the lineage convention.
65+
| `format_version` | Only for regenerable artefacts (e.g. `.affex`) whose format version is tracked *independently* of `version_pin`.
66+
|===
67+
68+
Because the generator is offline and deterministic, an external entry's
69+
`source_hash` is *recorded* in the `EXTERNAL_SPECS` table in the generator and
70+
emitted verbatim, so `--check` stays a pure function of the committed tree. The
71+
network-side verification (re-fetching `canonical_url`) is HYP-S006's job, and
72+
re-pinning a pointer is capped to `:review` — an owner decision, never an
73+
auto-regen. (`.affcite.a2ml` is a CodeCite-profile A2ML artefact in the
74+
AffineScript repo, not a separate registry pointer.)
75+
4276
== How `source_hash` works (and why it catches drift)
4377

4478
`git ls-files -s <home>` lists every tracked file under a home together with
4579
its blob SHA and path. Hashing that listing yields a fingerprint that changes
4680
whenever *any* tracked file under the spec changes — content, addition, or
4781
removal. Recording it in the registry means a later recompute can prove
48-
whether a spec has moved underneath its documentation.
82+
whether a spec has moved underneath its documentation. (For external pointers,
83+
the recorded hash is instead checked against a fresh fetch of `canonical_url`.)
4984

5085
== Regenerating (the generator)
5186

TOPOLOGY.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
> It cannot freeze: every regeneration re-reads ground truth. Do not edit by hand.
1010
1111
- **Phase:** active &nbsp;|&nbsp; **Maturity:** experimental &nbsp;|&nbsp; **STATE last-updated:** 2026-06-02T18:00:00Z
12-
- **Registry entries:** 28 specs across 6 streams
12+
- **Registry entries:** 31 specs across 6 streams
1313
- **Front door:** human → [README.adoc](README.adoc); machine → [0-AI-MANIFEST.a2ml](0-AI-MANIFEST.a2ml)
1414
- **Registry:** [.machine_readable/REGISTRY.a2ml](.machine_readable/REGISTRY.a2ml) (index + source hashes) · prose: [REGISTRY.adoc](REGISTRY.adoc)
1515

@@ -30,6 +30,14 @@
3030
| PLAYBOOK.a2ml spec | [`playbook-a2ml/`](playbook-a2ml/) | executable operational runbooks |
3131
| ANCHOR.a2ml spec | [`anchor-a2ml/`](anchor-a2ml/) | project-recalibration intervention format |
3232

33+
### Language — AffineScript + language policy (Stream 2)
34+
35+
| Spec | Home | If you want… |
36+
|---|---|---|
37+
| AffineScript .affine (faces / source documents) | [`hyperpolymath/affinescript`](https://github.com/hyperpolymath/affinescript/blob/main/spec/affine.adoc) `@ v2.0.0`| faces, canonical-lowering invariant, canonical islands, idiom packs, mimicry bindings, project face policy |
38+
| AffineScript .affex (face-interop manifest) | [`hyperpolymath/affinescript`](https://github.com/hyperpolymath/affinescript/blob/main/spec/affex.adoc) `@ v2.0.0`| derived regenerable manifest; declaration heads not full bodies; format_version bumps independently |
39+
| AffineScript .affmap (provenance) | [`hyperpolymath/affinescript`](https://github.com/hyperpolymath/affinescript/blob/main/spec/affmap.adoc) `@ v2.0.0`| provenance format; own pointer for independent staleness tracking |
40+
3341
### Protocols
3442

3543
| Spec | Home | If you want… |

hypatia-rules/registry-staleness.a2ml

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
# SPDX-License-Identifier: AGPL-3.0-or-later
22
# HYP-S006 — Registry Staleness / Doc Drift
33
# Detects when the verifiable spec registry (.machine_readable/REGISTRY.a2ml)
4-
# no longer matches the file tree, or when DERIVED docs (TOPOLOGY.md) were
4+
# no longer matches ground truth, or when DERIVED docs (TOPOLOGY.md) were
55
# hand-edited away from their generated form. Drift becomes a routed finding
66
# the moment it lands — not something noticed 60 days later.
77
#
8+
# Covers BOTH registry entry shapes:
9+
# * LOCAL specs — recorded source_hash vs a fresh git-ls-files compute.
10+
# * EXTERNAL specs (kind="external") — verified pointers to specs whose SSOT
11+
# lives in another repo (e.g. AffineScript .affine/.affex/.affmap). The
12+
# recorded source_hash is checked against a fresh fetch of canonical_url;
13+
# re-pinning is capped to :review (owner decision, not an auto-regen).
14+
#
815
# Companion to scripts/build-registry.sh (the generator) and
916
# .github/workflows/registry-verify.yml (the in-repo --check gate). This rule
1017
# is the estate-side half: it lets Hypatia surface the same drift across the
@@ -13,7 +20,7 @@
1320
@rule(version="1.0"):
1421
id: HYP-S006
1522
name: "Registry staleness / doc drift"
16-
description: "Alert when a spec's recorded source_hash in REGISTRY.a2ml no longer matches a fresh compute, or a DERIVED doc (TOPOLOGY.md) drifted from its generator"
23+
description: "Alert when a spec's recorded source_hash in REGISTRY.a2ml no longer matches a fresh compute (local) or a fresh fetch of canonical_url (external pointer), or a DERIVED doc (TOPOLOGY.md) drifted from its generator"
1724
severity: medium
1825
category: StandardsCompliance
1926
auto_fixable: true
@@ -33,23 +40,60 @@ find:
3340
@end
3441

3542
@logic(engine="built-in"):
36-
# Recompute each spec's source_hash the same way the generator does
37-
# (sha256 over `git ls-files -s <home>`) and compare to the recorded value.
43+
# Two entry shapes share one registry:
44+
# * LOCAL (no `kind`) — recompute sha256 over `git ls-files -s <home>`
45+
# exactly as the generator does, compare to the
46+
# recorded value.
47+
# * EXTERNAL (kind="external")— a verified pointer to a spec whose SSOT is
48+
# another repo. Re-fetch canonical_url and
49+
# recompute with source_hash_algo (sha256+;
50+
# md5/sha1 are rejected as UNVERIFIED), compare
51+
# to the recorded source_hash. This is the
52+
# network-side check the offline generator and
53+
# the in-repo registry-verify.yml cannot do.
3854
steps:
3955
- parse_registry: ".machine_readable/REGISTRY.a2ml"
4056
extract:
4157
- key: "home"
4258
as: home
59+
- key: "kind"
60+
as: kind
61+
- key: "canonical_url"
62+
as: canonical_url
63+
- key: "source_hash_algo"
64+
as: algo
65+
- key: "sync_status"
66+
as: sync_status
4367
- key: "source_hash"
4468
as: recorded_hash
45-
- for_each_spec:
69+
# --- LOCAL specs -------------------------------------------------------
70+
- for_each_spec where "kind != external":
4671
recompute: "sha256(git ls-files -s ${home})"
4772
compare_to: recorded_hash
4873
emit_if: "recompute != recorded_hash"
4974
finding_kind: registry-hash-stale
50-
- check_home_exists:
75+
- check_home_exists where "kind != external":
5176
emit_if: "home directory absent"
5277
finding_kind: registry-dead-home
78+
# --- EXTERNAL specs (verified pointers; SSOT in another repo) ----------
79+
- reject_weak_algo where "kind == external":
80+
emit_if: "algo in [md5, sha1]" # security policy: no MD5/SHA1
81+
finding_kind: registry-weak-algo
82+
- for_each_spec where "kind == external && recorded_hash != 'PENDING-FIRST-SYNC'":
83+
fetch: canonical_url
84+
recompute: "${algo}(fetched_content)"
85+
compare_to: recorded_hash
86+
emit_if: "recompute != recorded_hash"
87+
finding_kind: registry-pointer-stale
88+
- for_each_spec where "kind == external && recorded_hash == 'PENDING-FIRST-SYNC'":
89+
# Not drift — upstream has not landed yet. Surface as info, and as a
90+
# low finding only once it has sat unsynced beyond the grace window.
91+
fetch: canonical_url
92+
emit_if: "fetch succeeds"
93+
finding_kind: registry-pointer-ready # ready for first sync (info)
94+
emit_if: "still PENDING beyond ${stale_after_days}d"
95+
finding_kind: registry-pointer-unsynced # low
96+
# --- DERIVED docs ------------------------------------------------------
5397
- check_derived_docs:
5498
command: "bash scripts/build-registry.sh --check"
5599
emit_if: "non-zero exit"
@@ -76,14 +120,18 @@ strategy_caps:
76120
- when: "finding_kind == registry-dead-home"
77121
cap: review
78122
reason: "A missing home is a structural change, not a regen — needs human triage"
123+
- when: "finding_kind in [registry-pointer-stale, registry-pointer-unsynced, registry-weak-algo]"
124+
cap: review
125+
reason: "External pointers reference another repo's SSOT — re-pinning version_pin/source_hash is an owner decision, never an auto-regen (rebuild-registry cannot recompute a cross-repo hash offline)."
79126
@end
80127

81128
@action:
82129
emit_signal: doc.drift
83130
message_template: >-
84-
Registry drift: {finding_kind} for spec '{id}' (home={home}).
85-
Recorded {recorded_hash}, recompute differs. Run 'just registry' to
86-
regenerate, or triage if the home moved. ({age_days}d since generated)
131+
Registry drift: {finding_kind} for spec '{id}' (home={home} url={canonical_url}).
132+
Recorded {recorded_hash}, fresh compute/fetch differs. For LOCAL drift run
133+
'just registry' to regenerate; for EXTERNAL pointers re-pin is an owner
134+
decision (capped :review). ({age_days}d since generated)
87135
recipe: rebuild-registry
88136
severity_escalates_at: 60
89137
halt_on_violation: false

0 commit comments

Comments
 (0)