Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .machine_readable/REGISTRY.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ name = "A2ML — Attested Markup Language"
stream = "foundation"
home = "a2ml/"
canonical_doc = "a2ml/README.adoc"
source_hash = "sha256:9ed18e704be7f0f8f991edf9dcced013691e8b62c8d7cd5a27a3d7a91c0f27dd"
source_hash = "sha256:7c51aab6fe43b04bc795647ae9b3d4813ca82f6182399c2987e17edfd50b36f5"
route = "the typed/verified machine-readable document format"

[[spec]]
Expand Down
821 changes: 821 additions & 0 deletions a2ml/RECORD-DIALECT-SPEC.adoc

Large diffs are not rendered by default.

166 changes: 166 additions & 0 deletions a2ml/tests/record-dialect/MANIFEST.a2ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# SPDX-License-Identifier: CC-BY-SA-4.0
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# MANIFEST.a2ml — conformance-vector index for the A2ML Record Dialect.
# This file is itself a record-dialect document (spec RECORD-DIALECT-SPEC.adoc):
# it dogfoods the format it indexes and MUST parse at R0.

[metadata]
suite = "a2ml-record-dialect"
spec = "RECORD-DIALECT-SPEC.adoc"
spec-version = "1.0.0"
generated-by = "authored"
last-updated = "2026-07-03"

[legend]
expect = "one of: parse | validate | reject"
class = "lowest conformance class at which the expectation is REQUIRED (R0|R1|R2)"
note = "spec section(s) the vector exercises"

# ----------------------------------------------------------------------------
# VALID vectors — MUST parse (R0); MUST validate (R1) where a profile is declared.
# ----------------------------------------------------------------------------

[[vector]]
name = "valid/minimal.a2ml"
expect = "parse"
class = "R0"
spec_section = "5.3, 4.1"
note = "One section, one quoted-string entry — the smallest well-formed document."

[[vector]]
name = "valid/all-scalar-forms.a2ml"
expect = "parse"
class = "R0"
spec_section = "4.1-4.5, D-3"
note = "String, multi-line string, integer with _ separator, float with exponent, booleans."

[[vector]]
name = "valid/arrays-and-inline-tables.a2ml"
expect = "parse"
class = "R0"
spec_section = "4.6, 4.7, 2.3"
note = "Multi-line array with interior comment and trailing comma; array of inline tables."

[[vector]]
name = "valid/dotted-sections.a2ml"
expect = "parse"
class = "R0"
spec_section = "5.4, 6, 7.1"
note = "Dotted key-paths up to four segments; uppercase segment (categories.CHS)."

[[vector]]
name = "valid/array-of-tables.a2ml"
expect = "parse"
class = "R0"
spec_section = "5.3, 6"
note = "Repeated [[entry]] headers build an ordered array of tables."

[[vector]]
name = "valid/quoted-keys.a2ml"
expect = "parse"
class = "R0"
spec_section = "5.5, 3"
note = "Quoted key required for a key containing '-' as first char (\"--browser\")."

[[vector]]
name = "valid/comments-and-preamble.a2ml"
expect = "parse"
class = "R0"
spec_section = "5.1, 2.3"
note = "SPDX/copyright preamble comments; trailing comments after values and headers."

[[vector]]
name = "valid/state-profile.a2ml"
expect = "validate"
class = "R1"
spec_section = "5.2, 9.5, 10"
note = "Declares @profile(id=a2ml/state); carries the three required sections — validates."

# ----------------------------------------------------------------------------
# INVALID vectors — MUST be rejected at the stated class (and every class above).
# ----------------------------------------------------------------------------

[[vector]]
name = "invalid/bare-string-value.a2ml"
expect = "reject"
class = "R0"
spec_section = "4.8, D-2"
note = "Unquoted non-numeric, non-boolean scalar (x = hello) — no bare strings."

[[vector]]
name = "invalid/single-quoted-string.a2ml"
expect = "reject"
class = "R0"
spec_section = "D-5"
note = "Single-quoted 'literal string' does not exist in the record dialect."

[[vector]]
name = "invalid/native-date.a2ml"
expect = "reject"
class = "R0"
spec_section = "D-1, 4.8"
note = "Unquoted date literal — dates are quoted strings, no native date-time."

[[vector]]
name = "invalid/hex-integer.a2ml"
expect = "reject"
class = "R0"
spec_section = "D-3, 4.3"
note = "0x/0o/0b integer bases are not part of the reduced number grammar."

[[vector]]
name = "invalid/leading-dot-float.a2ml"
expect = "reject"
class = "R0"
spec_section = "D-3, 4.4"
note = "Leading-dot float (.5) is not valid; a float MUST have a leading digit."

[[vector]]
name = "invalid/triple-single-quote.a2ml"
expect = "reject"
class = "R0"
spec_section = "D-5, 4.2"
note = "'''…''' multi-line literal string does not exist; only \"\"\"…\"\"\"."

[[vector]]
name = "invalid/unterminated-string.a2ml"
expect = "reject"
class = "R0"
spec_section = "4.1"
note = "String value with no closing quote before end of line."

[[vector]]
name = "invalid/profile-after-section.a2ml"
expect = "reject"
class = "R1"
spec_section = "5.1, 5.2"
note = "@profile declared after a section header — all declarations MUST precede sections."

[[vector]]
name = "invalid/duplicate-key.a2ml"
expect = "reject"
class = "R1"
spec_section = "9.1, D-7"
note = "Same key twice in one table — warning at R0, error at R1."

[[vector]]
name = "invalid/reopened-section.a2ml"
expect = "reject"
class = "R1"
spec_section = "9.2, D-7"
note = "A standard section path opened twice ([a] … [a])."

[[vector]]
name = "invalid/unknown-directive.a2ml"
expect = "reject"
class = "R1"
spec_section = "10"
note = "A base document permits no directive but @profile; @frobnicate is rejected."

[[vector]]
name = "invalid/state-missing-required-section.a2ml"
expect = "reject"
class = "R1"
spec_section = "9.5, 10"
note = "Declares a2ml/state but omits the required maintenance-status section."
73 changes: 73 additions & 0 deletions a2ml/tests/record-dialect/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
= A2ML Record Dialect — Conformance Vectors
:icons: font
:toc: macro

toc::[]

This directory is the *normative test* for
link:../../RECORD-DIALECT-SPEC.adoc[the A2ML Record Dialect specification]. A
reference validator conforms to a class (R0/R1/R2, spec §1.4) if and only if it
produces the expected verdict for every vector whose required class is at or below
the class under test.

== Layout

[cols="1,3",options="header"]
|===
| Path | Contents

| `vectors/valid/`
| Documents that MUST parse (R0) and, where they declare a profile, MUST validate
(R1) against the definitions in `a2ml/profiles/`.

| `vectors/invalid/`
| Documents that MUST be rejected. Each is paired in `MANIFEST.a2ml` with the spec
clause it violates and the lowest class (R0/R1/R2) at which rejection is
REQUIRED.

| `MANIFEST.a2ml`
| The machine-readable index of every vector: expected verdict, required class,
and the spec section exercised. It is itself a record-dialect document, so it
doubles as a valid vector (the suite dogfoods the dialect).
|===

== Expected-verdict model

Each vector has an `expect` of `parse`, `validate`, or `reject`:

* `parse` — R0 MUST build a record tree without error. (Profile-free or
profile-inert.)
* `validate` — R0 MUST parse AND R1 MUST validate against the declared profile.
* `reject` — the validator MUST error at or below `class`; a validator at a higher
class MUST also reject (obligations accumulate downward, spec §1.4).

A vector marked `reject` at `class = R0` is a *syntax* error every conforming
implementation catches. A vector marked `reject` at `class = R1` is a
*well-formedness / profile* error that a pure R0 reader is permitted to accept
(with a warning where the spec says so).

== Running the suite (once a reference validator exists)

The validator is not yet wired (spec Appendix D). The intended contract, so the
suite is runnable the moment the reader lands:

[source,sh]
----
# For each valid vector: exit 0, tree emitted.
# For each invalid vector: non-zero exit, error cites the manifest's clause.
a2ml validate --dialect record --class R1 vectors/valid/<name>.a2ml # → 0
a2ml validate --dialect record --class R1 vectors/invalid/<name>.a2ml # → non-0
----

The CI job that drives this MUST fail loudly on any mismatch — never the
`|| echo SKIP` pattern. A green run of this suite is the precondition (spec §15,
Appendix D) for moving the specification from `Draft` to `Stable`.

== Adding a vector

. Add the `.a2ml` file under `valid/` or `invalid/`.
. Add a matching `[[vector]]` block to `MANIFEST.a2ml` with `name`, `expect`,
`class`, `spec_section`, and a one-line `note`.
. Keep each vector minimal — it should exercise exactly one clause so a failure
localises to one rule.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R0): bare (unquoted) string value. Spec 4.8 / D-2 — no bare strings.
[metadata]
name = hello
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R1): duplicate key in one table. Spec 9.1 / D-7 — warn at R0, error at R1.
[metadata]
version = "1.0.0"
version = "2.0.0"
4 changes: 4 additions & 0 deletions a2ml/tests/record-dialect/vectors/invalid/hex-integer.a2ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R0): hexadecimal integer. Spec D-3 / 4.3 — decimal only, no 0x/0o/0b.
[metadata]
mask = 0xFF
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R0): leading-dot float. Spec D-3 / 4.4 — a float MUST have a leading digit.
[metadata]
ratio = .5
4 changes: 4 additions & 0 deletions a2ml/tests/record-dialect/vectors/invalid/native-date.a2ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R0): unquoted native date literal. Spec D-1 / 4.8 — dates are quoted strings.
[metadata]
last-updated = 2026-05-15
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R1): @profile declared after a section header. Spec 5.1 / 5.2 —
# all profile declarations MUST precede the first section.
[metadata]
name = "example"

@profile(id=a2ml/state)
10 changes: 10 additions & 0 deletions a2ml/tests/record-dialect/vectors/invalid/reopened-section.a2ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R1): a standard section path opened twice. Spec 9.2 / D-7.
[metadata]
name = "example"

[position]
phase = "maintenance"

[metadata]
version = "1.0.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R0): single-quoted 'literal string'. Spec D-5 — only double quotes.
[metadata]
name = 'hello'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R1): declares a2ml/state but omits the required maintenance-status
# section. Spec 9.5 / 10 — required_sections = [metadata, position, maintenance-status].
@profile(id=a2ml/state)

[metadata]
project = "example"
version = "1.0.0"

[position]
phase = "maintenance"
maturity = "production"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R0): triple-single-quoted string. Spec D-5 / 4.2 — only """...""" exists.
[metadata]
blurb = '''hello
world'''
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R1): a base document permits no directive but @profile. Spec 10 —
# allowed_directives is closed [ profile ] unless a profile widens it.
@frobnicate(id=nope)

[metadata]
name = "example"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: MPL-2.0
# INVALID (R0): string value with no closing quote. Spec 4.1.
[metadata]
name = "unterminated
14 changes: 14 additions & 0 deletions a2ml/tests/record-dialect/vectors/valid/all-scalar-forms.a2ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-License-Identifier: MPL-2.0
# Exercises every scalar value form (spec 4.1-4.5).
[scalars]
a-string = "hello world"
a-multiline = """line one
line two"""
an-integer = 42
a-negative = -17
a-grouped-integer = 1_000_000
a-float = 3.14
a-float-exp = 6.022e23
a-signed-float = -1.5e-3
a-true = true
a-false = false
13 changes: 13 additions & 0 deletions a2ml/tests/record-dialect/vectors/valid/array-of-tables.a2ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: MPL-2.0
# Repeated [[header]] builds an ordered array of tables (spec 5.3, 6).
[[vector]]
name = "first"
order = 1

[[vector]]
name = "second"
order = 2

[[vector]]
name = "third"
order = 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: MPL-2.0
# Multi-line array with interior comment + trailing comma (4.6); array of inline tables (4.7).
[route-to-mvp]
milestones = [
"alpha",
"beta",
# trailing comma below is permitted
"ga",
]

[architecture-decisions]
decisions = [
{ id = "ADR-001", status = "accepted", ref = "CODEOWNERS-POLICY.adoc" },
{ id = "ADR-002", status = "proposed" },
]

[nested]
matrix = [ [ 1, 2 ], [ 3, 4 ] ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# comments-and-preamble.a2ml — preamble comments plus trailing comments (spec 2.3, 5.1).

[position] # trailing comment after a section header
phase = "maintenance" # design | implementation | testing | maintenance | archived
maturity = "production" # experimental | alpha | beta | production | lts
note = "a # inside a string is literal, not a comment"
Loading
Loading