Skip to content

Commit 6b6e2ee

Browse files
Add A2ML Record Dialect specification + conformance vectors (#456)
## What & why The seven canonical descriptile files that every estate repo deploys — `STATE`, `META`, `ECOSYSTEM`, `AGENTIC`, `NEUROSYM`, `PLAYBOOK`, `ANCHOR` — are written in a TOML-like `[section]` / `key = value` surface. Until now that surface was **used but never specified**: ~435 files across the estate conformed to a grammar that existed only by imitation. The existing `a2ml/SPEC.adoc` v1.1.0 specifies the *markup* dialect (`@directive:` blocks) and an `@record`/`key: value` form, but never the `[section]`/`key = value` record surface those files actually use. This PR fills that gap with a normative spec grounded in a **measured survey of the deployed corpus** (1,798 `.a2ml` files), not the abandoned `.scm`-era specs. ## Key design decision The Record Dialect is **not a rival format** — it is a *second concrete surface over the same typed core* as the markup dialect (`SPEC.adoc` §3). It reuses, unchanged: - validation modes (§4) → conformance classes R0/R1/R2 - base invariants (§6) - the Base Record vocabulary (§7) — a base record can be written in either surface and denotes the identical core structure - the profile mechanism (§8) — the shipped `a2ml/*` profiles already target this dialect - the canonical hash form (§9) One abstract model, two surfaces, **no third generation**. This is a structural fix for the audit finding that three format generations (`.scm` → TOML-like → A2ML profiles) coexist with no deprecation story. ## Contents - **`a2ml/RECORD-DIALECT-SPEC.adoc`** — normative spec: RFC-2119 language, EBNF surface grammar (every production grounded in the corpus), the record-tree data model, translation to the A2ML core, R0/R1/R2 conformance classes, a **normative "Divergences from TOML" table** (so an implementer can't conform by piping input through a stock TOML parser), security considerations, IANA note, versioning, and a worked example annotating a real deployed `STATE.a2ml`. - **`a2ml/tests/record-dialect/`** — an executable conformance-vector suite: **8 valid + 12 invalid** vectors, each minimal and mapped to one spec clause + the class at which rejection is required; a **dogfooded** record-dialect `MANIFEST.a2ml` indexing them; and a `README.adoc`. ## Status: Draft (deliberately) Per the no-overclaim doctrine, the spec is marked `Draft` and states it MUST NOT be cited as ratified until a reference reader greens the vectors and a corpus conformance run is published. The path to `Stable` is in the spec's **Appendix D**: 1. Reference reader — a second front-end over the existing core (not a second core). 2. Green the vectors in real CI (no `|| echo SKIP`). 3. Corpus conformance run over all ~435 files; publish the residual non-conforming set (e.g. the observed malformed timestamp `"…ZT00:00:00Z"`) as findings. 4. Registry: the spec lives under the already-registered `a2ml/` home. 5. Owner ratification → bump `Status` to `Stable`. ## Registry regeneration — note for the reviewer `.machine_readable/REGISTRY.a2ml` was regenerated with `scripts/build-registry.sh` (the sanctioned generator — **not** hand-edited). This updates the `a2ml/` home `source_hash` for the new files and **incidentally re-pins the `k9-svc/` home hash**, which was already drifted on `main` since `6df21b1` (the registry wasn't regenerated after that commit changed `k9-svc` files). `build-registry.sh --check` passes after regeneration. Flagging the k9-svc rehash explicitly since it's unrelated to the spec itself. ## Guardrails respected No licence content touched; no SPDX sweeps; new files carry correct SPDX from birth per the repo's MPL-2.0 / CC-BY-SA-4.0 classification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0112RkdER2wtwHdNmbEhThUz --- _Generated by [Claude Code](https://claude.ai/code/session_0112RkdER2wtwHdNmbEhThUz)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 79088f0 commit 6b6e2ee

24 files changed

Lines changed: 1221 additions & 1 deletion

.machine_readable/REGISTRY.a2ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ name = "A2ML — Attested Markup Language"
3636
stream = "foundation"
3737
home = "a2ml/"
3838
canonical_doc = "a2ml/README.adoc"
39-
source_hash = "sha256:9ed18e704be7f0f8f991edf9dcced013691e8b62c8d7cd5a27a3d7a91c0f27dd"
39+
source_hash = "sha256:7c51aab6fe43b04bc795647ae9b3d4813ca82f6182399c2987e17edfd50b36f5"
4040
route = "the typed/verified machine-readable document format"
4141

4242
[[spec]]

a2ml/RECORD-DIALECT-SPEC.adoc

Lines changed: 821 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# SPDX-License-Identifier: CC-BY-SA-4.0
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# MANIFEST.a2ml — conformance-vector index for the A2ML Record Dialect.
5+
# This file is itself a record-dialect document (spec RECORD-DIALECT-SPEC.adoc):
6+
# it dogfoods the format it indexes and MUST parse at R0.
7+
8+
[metadata]
9+
suite = "a2ml-record-dialect"
10+
spec = "RECORD-DIALECT-SPEC.adoc"
11+
spec-version = "1.0.0"
12+
generated-by = "authored"
13+
last-updated = "2026-07-03"
14+
15+
[legend]
16+
expect = "one of: parse | validate | reject"
17+
class = "lowest conformance class at which the expectation is REQUIRED (R0|R1|R2)"
18+
note = "spec section(s) the vector exercises"
19+
20+
# ----------------------------------------------------------------------------
21+
# VALID vectors — MUST parse (R0); MUST validate (R1) where a profile is declared.
22+
# ----------------------------------------------------------------------------
23+
24+
[[vector]]
25+
name = "valid/minimal.a2ml"
26+
expect = "parse"
27+
class = "R0"
28+
spec_section = "5.3, 4.1"
29+
note = "One section, one quoted-string entry — the smallest well-formed document."
30+
31+
[[vector]]
32+
name = "valid/all-scalar-forms.a2ml"
33+
expect = "parse"
34+
class = "R0"
35+
spec_section = "4.1-4.5, D-3"
36+
note = "String, multi-line string, integer with _ separator, float with exponent, booleans."
37+
38+
[[vector]]
39+
name = "valid/arrays-and-inline-tables.a2ml"
40+
expect = "parse"
41+
class = "R0"
42+
spec_section = "4.6, 4.7, 2.3"
43+
note = "Multi-line array with interior comment and trailing comma; array of inline tables."
44+
45+
[[vector]]
46+
name = "valid/dotted-sections.a2ml"
47+
expect = "parse"
48+
class = "R0"
49+
spec_section = "5.4, 6, 7.1"
50+
note = "Dotted key-paths up to four segments; uppercase segment (categories.CHS)."
51+
52+
[[vector]]
53+
name = "valid/array-of-tables.a2ml"
54+
expect = "parse"
55+
class = "R0"
56+
spec_section = "5.3, 6"
57+
note = "Repeated [[entry]] headers build an ordered array of tables."
58+
59+
[[vector]]
60+
name = "valid/quoted-keys.a2ml"
61+
expect = "parse"
62+
class = "R0"
63+
spec_section = "5.5, 3"
64+
note = "Quoted key required for a key containing '-' as first char (\"--browser\")."
65+
66+
[[vector]]
67+
name = "valid/comments-and-preamble.a2ml"
68+
expect = "parse"
69+
class = "R0"
70+
spec_section = "5.1, 2.3"
71+
note = "SPDX/copyright preamble comments; trailing comments after values and headers."
72+
73+
[[vector]]
74+
name = "valid/state-profile.a2ml"
75+
expect = "validate"
76+
class = "R1"
77+
spec_section = "5.2, 9.5, 10"
78+
note = "Declares @profile(id=a2ml/state); carries the three required sections — validates."
79+
80+
# ----------------------------------------------------------------------------
81+
# INVALID vectors — MUST be rejected at the stated class (and every class above).
82+
# ----------------------------------------------------------------------------
83+
84+
[[vector]]
85+
name = "invalid/bare-string-value.a2ml"
86+
expect = "reject"
87+
class = "R0"
88+
spec_section = "4.8, D-2"
89+
note = "Unquoted non-numeric, non-boolean scalar (x = hello) — no bare strings."
90+
91+
[[vector]]
92+
name = "invalid/single-quoted-string.a2ml"
93+
expect = "reject"
94+
class = "R0"
95+
spec_section = "D-5"
96+
note = "Single-quoted 'literal string' does not exist in the record dialect."
97+
98+
[[vector]]
99+
name = "invalid/native-date.a2ml"
100+
expect = "reject"
101+
class = "R0"
102+
spec_section = "D-1, 4.8"
103+
note = "Unquoted date literal — dates are quoted strings, no native date-time."
104+
105+
[[vector]]
106+
name = "invalid/hex-integer.a2ml"
107+
expect = "reject"
108+
class = "R0"
109+
spec_section = "D-3, 4.3"
110+
note = "0x/0o/0b integer bases are not part of the reduced number grammar."
111+
112+
[[vector]]
113+
name = "invalid/leading-dot-float.a2ml"
114+
expect = "reject"
115+
class = "R0"
116+
spec_section = "D-3, 4.4"
117+
note = "Leading-dot float (.5) is not valid; a float MUST have a leading digit."
118+
119+
[[vector]]
120+
name = "invalid/triple-single-quote.a2ml"
121+
expect = "reject"
122+
class = "R0"
123+
spec_section = "D-5, 4.2"
124+
note = "'''…''' multi-line literal string does not exist; only \"\"\"…\"\"\"."
125+
126+
[[vector]]
127+
name = "invalid/unterminated-string.a2ml"
128+
expect = "reject"
129+
class = "R0"
130+
spec_section = "4.1"
131+
note = "String value with no closing quote before end of line."
132+
133+
[[vector]]
134+
name = "invalid/profile-after-section.a2ml"
135+
expect = "reject"
136+
class = "R1"
137+
spec_section = "5.1, 5.2"
138+
note = "@profile declared after a section header — all declarations MUST precede sections."
139+
140+
[[vector]]
141+
name = "invalid/duplicate-key.a2ml"
142+
expect = "reject"
143+
class = "R1"
144+
spec_section = "9.1, D-7"
145+
note = "Same key twice in one table — warning at R0, error at R1."
146+
147+
[[vector]]
148+
name = "invalid/reopened-section.a2ml"
149+
expect = "reject"
150+
class = "R1"
151+
spec_section = "9.2, D-7"
152+
note = "A standard section path opened twice ([a] … [a])."
153+
154+
[[vector]]
155+
name = "invalid/unknown-directive.a2ml"
156+
expect = "reject"
157+
class = "R1"
158+
spec_section = "10"
159+
note = "A base document permits no directive but @profile; @frobnicate is rejected."
160+
161+
[[vector]]
162+
name = "invalid/state-missing-required-section.a2ml"
163+
expect = "reject"
164+
class = "R1"
165+
spec_section = "9.5, 10"
166+
note = "Declares a2ml/state but omits the required maintenance-status section."
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// SPDX-License-Identifier: CC-BY-SA-4.0
2+
= A2ML Record Dialect — Conformance Vectors
3+
:icons: font
4+
:toc: macro
5+
6+
toc::[]
7+
8+
This directory is the *normative test* for
9+
link:../../RECORD-DIALECT-SPEC.adoc[the A2ML Record Dialect specification]. A
10+
reference validator conforms to a class (R0/R1/R2, spec §1.4) if and only if it
11+
produces the expected verdict for every vector whose required class is at or below
12+
the class under test.
13+
14+
== Layout
15+
16+
[cols="1,3",options="header"]
17+
|===
18+
| Path | Contents
19+
20+
| `vectors/valid/`
21+
| Documents that MUST parse (R0) and, where they declare a profile, MUST validate
22+
(R1) against the definitions in `a2ml/profiles/`.
23+
24+
| `vectors/invalid/`
25+
| Documents that MUST be rejected. Each is paired in `MANIFEST.a2ml` with the spec
26+
clause it violates and the lowest class (R0/R1/R2) at which rejection is
27+
REQUIRED.
28+
29+
| `MANIFEST.a2ml`
30+
| The machine-readable index of every vector: expected verdict, required class,
31+
and the spec section exercised. It is itself a record-dialect document, so it
32+
doubles as a valid vector (the suite dogfoods the dialect).
33+
|===
34+
35+
== Expected-verdict model
36+
37+
Each vector has an `expect` of `parse`, `validate`, or `reject`:
38+
39+
* `parse` — R0 MUST build a record tree without error. (Profile-free or
40+
profile-inert.)
41+
* `validate` — R0 MUST parse AND R1 MUST validate against the declared profile.
42+
* `reject` — the validator MUST error at or below `class`; a validator at a higher
43+
class MUST also reject (obligations accumulate downward, spec §1.4).
44+
45+
A vector marked `reject` at `class = R0` is a *syntax* error every conforming
46+
implementation catches. A vector marked `reject` at `class = R1` is a
47+
*well-formedness / profile* error that a pure R0 reader is permitted to accept
48+
(with a warning where the spec says so).
49+
50+
== Running the suite (once a reference validator exists)
51+
52+
The validator is not yet wired (spec Appendix D). The intended contract, so the
53+
suite is runnable the moment the reader lands:
54+
55+
[source,sh]
56+
----
57+
# For each valid vector: exit 0, tree emitted.
58+
# For each invalid vector: non-zero exit, error cites the manifest's clause.
59+
a2ml validate --dialect record --class R1 vectors/valid/<name>.a2ml # → 0
60+
a2ml validate --dialect record --class R1 vectors/invalid/<name>.a2ml # → non-0
61+
----
62+
63+
The CI job that drives this MUST fail loudly on any mismatch — never the
64+
`|| echo SKIP` pattern. A green run of this suite is the precondition (spec §15,
65+
Appendix D) for moving the specification from `Draft` to `Stable`.
66+
67+
== Adding a vector
68+
69+
. Add the `.a2ml` file under `valid/` or `invalid/`.
70+
. Add a matching `[[vector]]` block to `MANIFEST.a2ml` with `name`, `expect`,
71+
`class`, `spec_section`, and a one-line `note`.
72+
. Keep each vector minimal — it should exercise exactly one clause so a failure
73+
localises to one rule.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# INVALID (R0): bare (unquoted) string value. Spec 4.8 / D-2 — no bare strings.
3+
[metadata]
4+
name = hello
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# INVALID (R1): duplicate key in one table. Spec 9.1 / D-7 — warn at R0, error at R1.
3+
[metadata]
4+
version = "1.0.0"
5+
version = "2.0.0"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# INVALID (R0): hexadecimal integer. Spec D-3 / 4.3 — decimal only, no 0x/0o/0b.
3+
[metadata]
4+
mask = 0xFF
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# INVALID (R0): leading-dot float. Spec D-3 / 4.4 — a float MUST have a leading digit.
3+
[metadata]
4+
ratio = .5
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# INVALID (R0): unquoted native date literal. Spec D-1 / 4.8 — dates are quoted strings.
3+
[metadata]
4+
last-updated = 2026-05-15
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# INVALID (R1): @profile declared after a section header. Spec 5.1 / 5.2 —
3+
# all profile declarations MUST precede the first section.
4+
[metadata]
5+
name = "example"
6+
7+
@profile(id=a2ml/state)

0 commit comments

Comments
 (0)