// SPDX-License-Identifier: CC-BY-SA-4.0
= A2ML v1.1.0 Specification
:version: 1.1.0
:date: 2026-06-03
:status: Stable
:era: v1
:toc: left
:toclevels: 3
:icons: font
:sectnums:

[abstract]
== Abstract

A2ML (Attested Markup Language) is a lightweight markup format that compiles to a
typed, verifiable core with formal proof obligations. This specification defines
the v1.1 surface grammar, typed-core semantics, validation modes (lax, checked,
attested), the *profile mechanism*, the *base record vocabulary*, *citation
primitives*, and the *canonical hash form* for opaque payloads and documents.

A2ML v1.1 is *additive* over v1.0: every conforming v1.0 document remains a
conforming v1.1 document. The new surface in v1.1 — profiles, the base record
vocabulary, citation primitives, and canonical hashing — is inert under the base
modes and acquires meaning only when a profile selects it.

[cols="1,3"]
|===
| Media Type    | `application/vnd.a2ml` (registration pending)
| File Extension| `.a2ml`
| Version       | 1.1.0 (Stable)
| Era           | v1
| Date          | 2026-06-03
| Lineage       | See `VERSIONS.adoc`
|===

== 1. Introduction

=== 1.1 Position In The Stack

A2ML is a *foundation*, not a consumer. It defines neutral primitives that other
formats and tools build on. A2ML names no downstream consumer normatively: the
primitives in this document are deliberately generic so that independent record
formats can reuse them without coupling back to A2ML's release cadence.

This independence is load-bearing. The single most common failure in a layered
estate is *desync*: two artefacts that should describe the same thing drift apart
because each re-invents identity, provenance, and integrity. A2ML's answer is the
*base record vocabulary* (Section 7) — one neutral shape for "a thing that can be
cited, hashed, and attributed" — reused verbatim by every downstream format.

=== 1.2 Design Principles

* *Lightweight authoring*: a Djot-like surface syntax.
* *Strong guarantees on demand*: Idris2 dependent types for the typed core.
* *Progressive strictness*: lax (warnings) → checked (errors) → attested (proofs).
* *Non-negotiable fidelity*: opaque payloads preserved byte-for-byte.
* *Inert-by-default extension*: new directives carry no semantics until a profile
  selects them. Nothing in v1.1 changes the meaning of a v1.0 document.
* *One home per concept*: identity, provenance, and integrity are defined once
  (Section 7) and referenced, never duplicated.

=== 1.3 Conformance Language

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT,
RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in RFC 2119 and
RFC 8174 when, and only when, they appear in all capitals.

=== 1.4 Conformance Classes

A conforming A2ML v1.1 implementation MUST:

* Parse the surface grammar (Section 2).
* Translate the surface AST to the typed core (Section 3).
* Support lax and checked modes (Section 4).
* Preserve opaque payloads byte-for-byte (Section 5).
* Implement the base invariant checks (Section 6).
* Recognise the base record vocabulary directives and admit them without error,
  even when no profile is active (Section 7).
* Compute the canonical hash form (Section 9) when asked.

The following are RECOMMENDED but not required for base conformance:

* Attested mode (proof obligations, Section 4.3).
* Profile resolution and enforcement (Section 8). An implementation that does not
  resolve profiles MUST treat an `@profile` declaration as an inert annotation and
  MUST NOT reject a document for declaring one.

== 2. Surface Grammar (Module 0)

=== 2.1 Lexical Rules

[source,ebnf]
----
Line         ::= CharSeq LF | CharSeq EOF
BlankLine    ::= Whitespace* LF
Identifier   ::= [A-Za-z][A-Za-z0-9:_-]*
QualifiedId  ::= Identifier ( '/' Identifier )*
String       ::= '"' ( '\\' Any | [^"] )* '"'
               | "'" ( '\\' Any | [^'] )* "'"
Number       ::= '-'? [0-9]+ ( '.' [0-9]+ )?
Whitespace   ::= ' ' | '\t'
LF           ::= '\n'
----

`QualifiedId` is new in v1.1 and is used for profile identifiers and reference
classes. It is a strict superset of `Identifier`; an unqualified identifier is a
`QualifiedId` of length one.

=== 2.2 Document Structure

[source,ebnf]
----
Document     ::= Preamble? Block*
Preamble     ::= ProfileDecl+        // zero or more; see Section 8.1

Block        ::= Heading
              | DirectiveBlock
              | InlineDirective
              | List
              | CodeBlock
              | Paragraph
              | BlankLine+

Heading      ::= HeadingMarker SP+ TextLine LF
HeadingMarker::= '#'{1,5}
HeadingLevel ::= length(HeadingMarker)

DirectiveBlock ::= '@' Identifier Attrs? ':' LF DirectiveBody '@end' LF
InlineDirective::= '@' Identifier Attrs LF       // self-closing; no body, no @end
Attrs        ::= '(' Attr ( ',' Attr )* ')'
Attr         ::= Identifier '=' ( QualifiedId | String | Number )

DirectiveBody ::= Block*

List         ::= ListItem ( LF ListItem )*
ListItem     ::= ( '-' | '*' ) SP+ Inline

Paragraph    ::= Inline ( LF Inline )* LF

CodeBlock    ::= '```' LangSpec? LF RawLine* '```' LF
LangSpec     ::= [A-Za-z0-9_+-]+
RawLine      ::= ( [^\n] )* LF
----

==== 2.2.1 Inline (self-closing) directives — new in v1.1

A *self-closing directive* is a directive that carries all of its content in its
attribute list and has no body and no `@end`. It is written on a single line:

[source,a2ml]
----
@profile(id=a2ml/state)
@cite(key="rfc2119", span="L10-L14")
----

The parser distinguishes the two directive forms by lookahead: a directive line
ending in `:` opens a `DirectiveBlock` and MUST be matched by `@end`; a directive
line that ends after its closing `)` (or its name, if no attributes) is an
`InlineDirective`. A directive name is either block-form or inline-form per this
specification (Section 7.3, Section 8.1, Appendix C); it MUST NOT be both.

=== 2.3 Inline Syntax

[source,ebnf]
----
Inline       ::= InlineAtom ( SP InlineAtom )*
InlineAtom   ::= Emph | Strong | Link | Ref | Text
Emph         ::= '*' TextInline '*'
Strong       ::= '**' TextInline '**'
Link         ::= '[' TextInline ']' '(' URL ')'
Ref          ::= '@ref(' QualifiedId ')'
TextInline   ::= ( TextChar | '\\' Any )+
TextChar     ::= [^*\[\]@\n]
URL          ::= [^ )\n]+
----

=== 2.4 Core Directives (Module 0)

[cols="2,3,2",options="header"]
|===
| Directive | Purpose | Attributes

| `@abstract` | Document abstract        | `id` (optional)
| `@refs`     | Reference list           | `id` (optional)
| `@fig`      | Figure block             | `id`, `ref` (optional)
| `@table`    | Table block              | `id` (optional)
| `@opaque`   | Byte-preserved payload   | `id`, `lang` (optional)
| `@requires` | Structural dependency    | `section` (required)
|===

Profile, base-vocabulary, and citation directives are specified in Sections 7, 8
and Appendix C.

== 3. Typed Core Semantics

=== 3.1 Core Types (Idris2)

The normative Idris2 model lives in `src/A2ML/`. The shape below is informative;
the source files are authoritative.

[source,idris]
----
record Id where
  constructor MkId
  raw : String

data Block
  = Section Sec
  | Para String
  | Bullet (List String)
  | Figure Fig
  | Table Tbl
  | Refs (List Ref)
  | Opaque Payload
  | ProfileDecl ProfileId          -- Section 8
  | Record BaseRecord              -- Section 7
  | Citation CiteNode              -- Appendix C

record Doc where
  constructor MkDoc
  profiles : List ProfileId        -- declared profiles, in source order
  blocks   : List Block

record Payload where
  constructor MkPayload
  id    : Maybe Id
  lang  : Maybe String
  bytes : String                   -- preserved exactly; see Section 5
----

=== 3.2 Translation Rules

1. *Headings* → `Section` with generated or explicit `id`.
2. *Block directives* → typed blocks by directive name.
3. *Self-closing directives* → typed nodes (`ProfileDecl`, `Record`, `Citation`).
4. *Lists* → `Bullet`.
5. *Paragraphs* → `Para`.
6. *`@opaque`* → `Payload`, bytes preserved.

Unknown directives are retained as opaque structural nodes in lax mode and
rejected in checked mode (Section 4.2), *except* that an implementation which does
not resolve profiles MUST still accept the directives named in this specification.

== 4. Validation Modes

=== 4.1 Lax Mode

Parses all valid surface syntax; emits warnings for missing IDs and unresolved
references; never errors unless the syntax itself is invalid. Use for authoring.

=== 4.2 Checked Mode

All IDs REQUIRED on `@fig`, `@table`, and sections; all `@ref(...)` MUST resolve;
unknown directives are rejected. Use for production documents.

=== 4.3 Attested Mode

All checked-mode requirements, plus the proof obligations of Section 6 MUST
discharge. When a profile is active and declares a minimum attestation level
(Section 8.2), attested mode additionally discharges the profile's obligations.

=== 4.4 Mode And Profile Are Orthogonal

A document's *mode* is chosen by the validator invocation; its *profile(s)* are
declared in the document. A profile MAY raise the effective floor (e.g. "this
profile requires at least checked mode") but a profile MUST NOT lower it. Modes
and profiles compose by taking the stricter of the two at every axis.

== 5. Opaque Payloads (Non-Negotiable)

Opaque payloads MUST be preserved byte-for-byte in the typed core. Parsers and
renderers:

* MUST NOT modify opaque payload bytes.
* MUST preserve encoding (UTF-8 default).
* MAY transform for an output format (e.g. PDF escaping) but MUST report every
  transformation.
* MUST support round-trip: parse → serialize → parse yields an identical payload.

The integrity of an opaque payload is witnessed by its canonical hash (Section 9).

== 6. Base Invariants

=== 6.1 Unique IDs

All `id` attributes in a document MUST be unique.

[source,idris]
----
UniqueIds : Doc -> Type
uniqueIdsDec : (doc : Doc) -> Dec (UniqueIds doc)
----

=== 6.2 Resolved References

All `@ref(id)` and `ref="id"` attributes MUST reference existing IDs.

[source,idris]
----
RefsResolve : Doc -> Type
refsResolveDec : (doc : Doc) -> Dec (RefsResolve doc)
----

=== 6.3 Required Sections (configurable)

Specific sections MAY be required by policy or by an active profile.

=== 6.4 Opaque Payload Fidelity

The round-trip property of Section 5 is an invariant, witnessed by hash equality
under the canonical hash form (Section 9).

== 7. Base Record Vocabulary

NOTE: This section is the *anti-desync primitive*. It defines one neutral shape
for "a thing that can be identified, located, attributed, and integrity-checked".
Downstream record formats REUSE this vocabulary verbatim rather than re-inventing
it. A2ML core defines the shape and names no consumer; consumers reference this
section.

=== 7.1 Rationale

When two artefacts describe the same underlying thing — a span of source, a proof
obligation, an attestation receipt — they desync if each invents its own notion of
identity, provenance, and integrity. The base record vocabulary fixes a single
neutral shape so that "the same thing" is *literally the same fields* everywhere.

=== 7.2 The Base Record

A *base record* is a directive `@record` whose body is a set of fields. The field
set is fixed by this specification:

[cols="2,1,4",options="header"]
|===
| Field | Cardinality | Meaning

| `id`           | REQUIRED | Document-unique identifier (`Identifier`). Subject to the uniqueness invariant (Section 6.1).
| `source_span`  | REQUIRED | The located region this record is about. See Section 7.4.
| `canonical_node` | OPTIONAL | A stable structural address into a canonical syntax tree (e.g. an AffineScript node path). OPTIONAL so that non-AffineScript and source-free projects conform without it.
| `hash`         | REQUIRED | Integrity digest of the referenced artefact, `sha256:<hex>`. See Section 9. This is the desync tripwire: equal things hash equal.
| `provenance`   | REQUIRED | Who/what produced the record. See Section 7.5.
| `timestamp`    | REQUIRED | RFC 3339 UTC instant of record creation.
| `artefact_ref` | REQUIRED | A reference to the artefact the record is about (URI, repo-relative path, or content address).
| `profile_decl` | OPTIONAL | The profile under which this record was authored (`QualifiedId`), echoing the document's `@profile` (Section 8). Lets a detached record carry its own profile context.
|===

A base record with all REQUIRED fields present and well-formed is *base-valid*.
No field of the base record carries domain semantics: a base record asserts only
"this identified, located artefact had this hash, produced by this provenance, at
this time". Domain meaning is added exclusively by profiles (Section 8) and by
downstream formats.

=== 7.3 Surface Form

`@record` is a block directive. Fields are `key: value` lines; nested records
(`provenance`, `source_span`) are written as indented `key: value` groups.

[source,a2ml]
----
@record:
id: rec-001
source_span: { file: "src/Lex.affine", start: { line: 12, col: 1 }, end: { line: 14, col: 9 } }
canonical_node: "module/Lex/fn:tokenize/expr:3"
hash: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
provenance: { author: "j.d.a.jewell", tool: "a2ml-cli@1.1.0", kind: human }
timestamp: "2026-06-03T11:00:00Z"
artefact_ref: "repo:standards/a2ml/SPEC.adoc"
profile_decl: a2ml/state
@end
----

=== 7.4 `source_span`

A `source_span` locates a region. Its shape:

[cols="2,1,4",options="header"]
|===
| Field | Cardinality | Meaning
| `file`  | REQUIRED | Repo-relative path or URI of the source artefact.
| `start` | REQUIRED | `{ line, col }`, both 1-based.
| `end`   | REQUIRED | `{ line, col }`, both 1-based; `end` ≥ `start` lexicographically.
|===

A *point* span sets `end = start`. Byte-offset spans MAY be carried additionally in
an OPTIONAL `bytes: { start, end }` sub-field, but line/col is the REQUIRED form so
that text tools without a byte model still conform.

=== 7.5 `provenance`

[cols="2,1,4",options="header"]
|===
| Field | Cardinality | Meaning
| `author` | REQUIRED | Human or organisational identity responsible.
| `tool`   | REQUIRED | Producing tool, `name@version`.
| `kind`   | REQUIRED | One of the closed set `{ human, ai, mechanical }`.
| `agent`  | OPTIONAL | When `kind = ai`, the agent/model identity. SHOULD be present when `kind = ai`.
|===

`kind` is a *closed* enumeration: exactly `human`, `ai`, or `mechanical`. A
validator MUST reject any other value in checked mode. `mechanical` denotes a
deterministic, non-AI process (a script, a compiler pass); `ai` denotes a
generative/inferential agent; `human` denotes direct human authorship.

=== 7.6 Validation Status Of The Base Record

In *base* A2ML (no profile active), `@record` is recognised and its REQUIRED
fields are checked for presence and well-formedness, but no field is given domain
meaning and no cross-record constraint is imposed. A profile (Section 8) MAY add:
required additional records, cross-record constraints, or a narrower `kind` set.
A profile MUST NOT remove a REQUIRED base field.

== 8. Profile Mechanism

NOTE: A profile is a *named validation bundle*. It is non-executable: it adds
required structure and tightens checks; it NEVER grants execution, network, or
filesystem capability. Profiles increase validation strictness only.

=== 8.1 Declaring A Profile

A document declares a profile with a self-closing directive in its preamble:

[source,a2ml]
----
@profile(id=a2ml/state)
----

* The `id` is a `QualifiedId` of the form `<namespace>/<name>` (e.g. `a2ml/state`).
* A document MAY declare more than one profile; declared profiles compose by
  taking the union of their constraints (the stricter of any overlapping axis).
* `@profile` declarations MUST appear before the first non-preamble block. A
  declaration appearing later is a checked-mode error.
* `@profile` is inert under base conformance: an implementation that does not
  resolve profiles MUST accept the declaration and ignore it (Section 1.4).

=== 8.2 What A Profile Definition May Constrain

A profile *definition* is itself an A2ML document (Section 8.4). It MAY constrain
exactly these axes, and no others:

[cols="2,4",options="header"]
|===
| Axis | A profile MAY…

| *Required sections* | name headings/sections that MUST be present (by title or id).
| *Allowed directives* | restrict the directive set: a closed allow-list, a
  deny-list, or "base + these". A directive not permitted by an active profile is
  a checked-mode error.
| *Required directives* | name directives (e.g. specific `@record` shapes) that
  MUST appear, with min/max cardinality.
| *Reference classes* | constrain `@ref` targets by class (`QualifiedId`
  namespaces), e.g. "every `@ref(state/*)` MUST resolve to a `Section`".
| *Attestation level* | declare the minimum mode (`lax` < `checked` < `attested`)
  under which the document is considered valid for this profile.
|===

A profile MUST NOT:

* grant or imply execution, network access, or filesystem writes;
* relax a base invariant (Section 6) or remove a REQUIRED base-record field;
* change surface grammar or token meaning.

These prohibitions are normative and are what keeps "active"/"attested" variants
(sometimes called A2MLa) safe: A2ML remains read-only; a profile only raises the
bar.

=== 8.3 Resolution

Given `@profile(id=ns/name)`, a validator resolves the definition as follows:

1. Look up `ns/name` in the *profile registry* (Section 8.5).
2. If found, load the referenced definition and pin it by its `version` and `hash`
   (Section 9). A registry entry without a hash is usable only in lax mode.
3. If not found, behaviour depends on mode: lax → warn and continue inert; checked
   → error `unknown-profile`; attested → error.

Resolution is *offline and deterministic*: the registry and definitions are files
in the repository (or a content-addressed store), never a network fetch at
validation time.

=== 8.4 Profile Definition Format

A profile definition is an A2ML document declaring `@profile-def`:

[source,a2ml]
----
@profile-def(id=example/illustrative, version="1.0.0"):
required_sections:
  - metadata
  - position
allowed_directives: base + [ record ]
required_directives:
  - { directive: record, min: 1, max: 1 }
reference_classes:
  - { class: state, target: Section, required: true }
min_attestation: checked
@end
----

This example is illustrative of the *format* and exercises all five axes.
The real `a2ml/*` profiles shipped by this repository are in
`a2ml/profiles/` (Section 11); each one constrains only the axes it needs.
The five fields map one-to-one onto the typed-core `ProfileDef`
(`src/A2ML/Profiles.idr`): `required_sections`, `allowed_directives`
(a `DirectivePolicy`), `required_directives` (each a `RequiredDirective`
with `min`/`max` cardinality), `reference_classes`, and `min_attestation`
(a `Mode`).

The `@profile-def` directive and its fields are themselves base A2ML; a profile
definition is therefore self-describing and can be validated by the same engine.

=== 8.5 Where Definitions Live, And Registration

* *Home (one per profile):* each profile definition lives at
  `a2ml/profiles/<namespace>-<name>/PROFILE.a2ml` (e.g.
  `a2ml/profiles/a2ml-state/PROFILE.a2ml`). This is the single source of truth for
  that profile; its text is not duplicated elsewhere.
* *Registry (verifiable index):* `a2ml/profiles/REGISTRY.a2ml` lists every
  known profile as a base record (Section 7) whose `artefact_ref` points at the
  definition file and whose `hash` pins it. The registry is the *verifiable* index:
  a consumer trusts a profile only if the definition's computed hash equals the
  registry's pinned hash.
* *Namespacing:* the `a2ml/*` namespace is reserved for profiles shipped by this
  repository (Section 11). Downstream consumers (which A2ML core does not name)
  register their own profiles under their own namespaces, in their own repos, and
  publish their own registries. A2ML core ships only neutral primitives and the
  `a2ml/*` profiles.

This realises SSOT + verifiable registry: one home per profile, a hash-pinned
index, and no duplicated normative text.

== 9. Canonical Hash Form (Hashing Now, Signing Later)

=== 9.1 Scope

v1.1 specifies *hashing* normatively and *reserves* signing for a later minor
version. Implementations MUST compute hashes as below; signature fields are
defined as reserved and inert.

=== 9.2 Opaque Payload Hash

The canonical hash of an opaque payload is:

----
opaque_hash = "sha256:" + lowerhex( SHA-256( payload_bytes ) )
----

where `payload_bytes` are the exact bytes preserved under Section 5, with no
normalisation whatsoever (no newline translation, no trimming, no re-encoding).

=== 9.3 Document Canonical Hash

The canonical hash of a *document* is computed over a canonical serialization of
its typed core (Section 3), not its surface bytes, so that semantically identical
documents that differ only in incidental surface whitespace hash equal. The
canonical serialization is:

1. Translate surface → typed core.
2. Serialize the typed core as canonical JSON per RFC 8785 (JCS): keys sorted,
   no insignificant whitespace, UTF-8, shortest number forms.
3. For every `Opaque` node, substitute the field `bytes` with its `opaque_hash`
   (Section 9.2) so that payload bytes are committed by reference, not inlined.
4. `doc_hash = "sha256:" + lowerhex( SHA-256( canonical_json ) )`.

This makes the document hash stable under surface reformatting while remaining
sensitive to any change in structure, identifiers, references, or payloads.

=== 9.4 Record And Profile Hashes

* A base record's `hash` field (Section 7.2) is the `opaque_hash` (Section 9.2) of
  the artefact named by `artefact_ref`, computed over that artefact's exact bytes.
* A profile registry entry's `hash` (Section 8.5) pins the definition by the
  `opaque_hash` (Section 9.2) of the definition file's *exact bytes*. (Pinning the
  file bytes — rather than the `doc_hash` of Section 9.3 — keeps the registry
  verifiable with a stock `sha256sum` before the document canonicaliser is
  deployed. The registry MUST state which form it uses; v1.1 registries use the
  byte hash.)

=== 9.5 Reserved: Signing

The following are RESERVED and MUST be treated as inert in v1.1:

[source,idris]
----
record Signature where
  constructor MkSignature
  alg     : String          -- reserved; "ed25519" anticipated
  pubkey  : String          -- reserved (base64)
  sig     : String          -- reserved (base64); over a doc_hash
  signed_at : Maybe String  -- reserved (RFC 3339)
----

A v1.1 validator MUST NOT require a signature and MUST NOT reject a document that
carries reserved signature fields. Signing semantics (what is signed, trust roots,
rotation) are deferred to a later minor version. What is signed is already pinned:
a signature, when specified, will sign a `doc_hash`.

== 10. Citation Primitives

NOTE: Citation primitives are *representable in base A2ML* but carry *no semantics
by default*. They are REQUIRED and validated only under a citation profile, which
A2ML core does not ship (consumers define their own). This keeps citation
machinery available to everyone without making any document "about citations"
unless it opts in.

=== 10.1 The Primitives

[cols="2,1,4",options="header"]
|===
| Directive | Form | Carries

| `@cite`         | inline | `key` (reference key) and OPTIONAL `span` (a `source_span`, Section 7.4).
| `@rationale`    | block  | free prose explaining *why* a cited claim is relied upon.
| `@origin`       | inline | provenance of a claim: `artefact_ref` + OPTIONAL `hash`.
| `@source_span`  | inline | a standalone `source_span` (Section 7.4) not attached to a record.
| `@canonical_node` | inline | a standalone `canonical_node` address (Section 7.2).
|===

`@source_span` and `@canonical_node` are the same located-region and structural-
address notions as the base record's fields (Section 7), surfaced as standalone
directives so that prose can point at code without wrapping a whole `@record`.

=== 10.2 Default (Profile-Free) Semantics

With no citation profile active:

* All five directives parse and are retained in the typed core.
* `@cite(key=…)` does *not* require `key` to resolve to a `@refs` entry.
* No cross-reference, completeness, or provenance check is applied.

They are, in base mode, structured annotations — nothing more.

=== 10.3 Under A Citation Profile

A citation profile (defined by a consumer, not by A2ML core) MAY require, for
example: that every `@cite(key)` resolves; that every cited claim carries an
`@origin`; that spans are well-formed and in-bounds. Such a profile is an ordinary
profile (Section 8) and is subject to all profile prohibitions (no execution, no
relaxation of base invariants).

== 11. Reserved Namespaces

The `a2ml/*` profile namespace is reserved by this repository. v1.1 ships the
seven `a2ml/*` profiles for the 6a2 metadata family:

`a2ml/state`, `a2ml/meta`, `a2ml/ecosystem`, `a2ml/agentic`, `a2ml/neurosym`,
`a2ml/playbook`, `a2ml/anchor`.

Their definitions live under `a2ml/profiles/` and are indexed in
`a2ml/profiles/REGISTRY.a2ml`.

Other components within this repository MAY register additional `a2ml/*`
profiles in their own subtrees, each with its own definition file and registry
entry. A2ML core neither enumerates nor depends on them: it ships only the
mechanism (Section 8) and the seven 6a2 profiles above. No namespace other than
`a2ml/*` is defined by A2ML core.

== 12. Security Considerations

=== 12.1 Injection

Opaque payloads may contain arbitrary content. Renderers MUST escape/sanitize for
the target format (HTML: `<script>`, `<iframe>`; LaTeX: `\input`, `\write`).

=== 12.2 Resource Exhaustion

Implementations SHOULD limit directive nesting depth (recommended: 100) and opaque
payload size.

=== 12.3 Reference And Profile Cycles

Checked mode MUST detect `@requires` cycles. Profile resolution (Section 8.3) MUST
detect cycles in profile composition and reject them.

=== 12.4 Hash Agility

Only SHA-256 is defined in v1.1, written `sha256:`. The prefix is a hash-agility
hook: future minor versions MAY define additional algorithms behind new prefixes.
A validator MUST reject an unrecognised prefix in checked mode rather than guessing.

=== 12.5 Profiles Cannot Escalate

By construction (Section 8.2) a profile cannot grant capability. A consumer that
treats a profile as an authorisation token is misusing A2ML; profile presence is
evidence of *validation intent*, never of *granted capability*.

== 13. IANA Considerations

Proposed media type `application/vnd.a2ml`; file extension `.a2ml`; no magic number;
fragment identifier `#section-id`. See the separate IANA registration document.

== 14. Interoperability

Reference implementation: the Idris2 core under `src/A2ML/`. Third-party
implementations MUST pass the conformance suite (`docs/CONFORMANCE.adoc`,
`tests/vectors/`). Rendering targets (HTML5, LaTeX/PDF, Markdown) are unchanged
from v1.0; Markdown remains lossy (proof obligations and hashes not preserved).

== 15. Versioning

* SemVer for specs. Additive changes (new inert directives, new profiles, new
  invariants that do not invalidate prior-conforming documents) are MINOR bumps.
* Breaking changes are a MAJOR bump and an *era* change; "v2" is a roadmap heading,
  not a near-term plan.
* Lineage convention per spec directory: one `SPEC.adoc` (current normative),
  `archive/` (frozen prior versions), `VERSIONS.adoc` (lineage + era). See
  `VERSIONS.adoc`.

== Appendix A: Grammar Summary

The complete EBNF is in `docs/GRAMMAR.adoc`; Section 2 is normative where the two
differ.

== Appendix B: Change Log

=== From v1.0.0 to v1.1.0 (additive)

* Add the *profile mechanism*: `@profile` declaration, `@profile-def` definition
  format, resolution, and the SSOT + hash-pinned registry (Section 8, Section 11).
* Add the *base record vocabulary* `@record` — the anti-desync primitive
  (Section 7).
* Add *citation primitives* `@cite`, `@rationale`, `@origin`, `@source_span`,
  `@canonical_node` — inert without a citation profile (Section 10).
* Add the *canonical hash form* for opaque payloads and documents; reserve signing
  (Section 9).
* Add self-closing (inline) directive syntax and `QualifiedId` (Section 2).
* No change invalidates a v1.0-conforming document.

== Appendix C: Directive Index (v1.1)

[cols="2,1,1,3",options="header"]
|===
| Directive | Form | Since | Section
| `@abstract`     | block  | v1.0 | 2.4
| `@refs`         | block  | v1.0 | 2.4
| `@fig`          | block  | v1.0 | 2.4
| `@table`        | block  | v1.0 | 2.4
| `@opaque`       | block  | v1.0 | 2.4 / 5
| `@requires`     | block  | v1.0 | 2.4
| `@ref(...)`     | inline | v1.0 | 2.3
| `@profile`      | inline | v1.1 | 8.1
| `@profile-def`  | block  | v1.1 | 8.4
| `@record`       | block  | v1.1 | 7
| `@cite`         | inline | v1.1 | 10
| `@rationale`    | block  | v1.1 | 10
| `@origin`       | inline | v1.1 | 10
| `@source_span`  | inline | v1.1 | 10 / 7.4
| `@canonical_node` | inline | v1.1 | 10 / 7.2
|===

---

*Version*: 1.1.0 +
*Status*: Stable +
*Era*: v1 +
*Date*: 2026-06-03 +
*Lineage*: `VERSIONS.adoc`
