Part 6. The package

AFDS user guide

Packaging a design system so that it arrives intact

Everything the earlier sections describe is a set of facts about a design system: what a component promises, what it refuses to promise, which keys operate it, what was observed on which engine, and what nobody has checked yet. This section is about the last mile. It answers how those facts get from the team that wrote them to the team that relies on them, without any of them being lost, reordered, or quietly rewritten on the way.

Three readers are served here. A designer wants to know what a package is and what has to be in one before it can be handed over; a developer wants the field lists, because a producer writes them and a consumer reads them; a tester or QA engineer wants the verification algorithm and the security rules, because those are the parts that can be run as a check. The field-by-field parts are marked as such, and a designer can skim them.

How to read the requirements quoted in this section

The specification uses capitalised MUST, MUST NOT, SHOULD, MAY and their relatives in the sense RFC 2119 gives them, and clause 4.1 sets out the force of each: MUST is an absolute requirement, SHOULD is a strong expectation that a departing party has to justify and owns the consequences of, and MAY is genuinely optional, so a consumer cannot assume the optional behaviour is present. Clause 4.1 also settles a question a reader will otherwise ask: the same words in lower case, in the specification's informative clauses, carry no requirement at all.

This guide is informative and does not issue requirements in its own voice. Where something is required, the sentence says that the specification requires it and names the clause, so you can read the normative text for yourself.

Two roles carry the obligations, and clause 4.2 defines them. A producer is any tool or person that creates a package. A consumer is any tool or person that reads a package and relies on its contents. A single tool may be both, and clause 4.2 requires that when it is, it satisfies both sets of obligations independently. An adapter is always both, which is the reason Part IV gives adapters a clause of their own.

Why the facts travel as one file

A design system that arrives as a directory to be assembled arrives differently for each recipient. Files go missing in transfer, relationships between artefacts become ambiguous once a folder has been copied twice, integrity is hard to check, and a consumer cannot reliably tell which folder or which revision was meant to be the complete system. The Part IV preamble puts the consequence plainly: a contract that is reassembled is a contract that can be reassembled wrongly.

So a package is a single file, and everything in clauses 25 to 33 follows from wanting one file that a consumer can verify before trusting a word of its contents. The container is a ZIP archive with the .afds extension, because ZIP is widely supported, cross-platform, compressible, and inspectable with ordinary tools, and because it keeps several specialised representations together without pretending they are one format.

The costs are worth stating before anyone adopts this. A package is less convenient for line-by-line collaboration than a live repository, and editing one artefact means unpacking or using package-aware tooling. An inventory of digests proves that bytes did not change; it proves nothing about who produced them, which clause 32.3 states in terms. And an archive from somewhere else is an attack surface, which is why clause 32 exists at all.

Part IV also declines to do two things on purpose: it defines no signature format, for the reason clause 32.3 gives, and it defines no adapter for any particular external target, because the moment one adapter is canonical the format has a preferred toolchain and the portability claim is weaker than it looks.

The container rules

Clause 25.1 states ten container requirements, and every row of the table below is a MUST in that clause. The wording here is indicative for readability; the normative wording is at clause 25.1.

RequirementWhat a conforming package does (clause 25.1)
ZIP syntaxUses ZIP syntax, and is readable by an ordinary ZIP reader
ExtensionUses the .afds extension
No enclosing directoryDoes not wrap its contents in a single enclosing top-level directory; afds-manifest.json sits at the archive root
Root manifestIncludes an entry named exactly afds-manifest.json at the archive root
Root inventoryIncludes an entry named exactly afds-inventory.json at the archive root
Normalised relative pathsGives every entry a normalised relative path using / as the separator
No absolute pathsHas no entry path beginning with /, and none containing a drive letter or UNC prefix
No traversalHas no entry path containing a .. segment or a . segment
UTF-8 textStores text content as UTF-8, and emits no byte-order mark
No encryptionContains no encrypted entries when it is intended for portable interchange

Clause 25.2 explains four of these, and each note is operational rather than decorative.

The no-enclosing-directory rule exists so that a consumer can find the manifest without guessing. Many archive tools add a wrapper directory by default, and clause 25.2 requires a producer to check its output rather than trusting the tool — the single most common way a first package fails.

The path restrictions are there for security as much as tidiness, and clause 32 names the attack. Clause 25.2 requires a consumer to reject a non-conforming path rather than sanitising it, because sanitising silently changes what the package says.

The encryption prohibition applies to portable interchange, which is the only case Part IV specifies. Clause 25.2 permits a producer to encrypt a package for private transfer by wrapping the conforming .afds file in some other envelope, and requires that the .afds file inside that envelope is itself unencrypted.

Directory entries are permitted but carry no meaning. Clause 25.2 states that a consumer "MUST NOT rely on the presence of an explicit directory entry, and a producer SHOULD omit them", and that directory entries "MUST NOT appear in the inventory, because they have no content to digest". That last rule is what makes the entry arithmetic later in this section come out: an archive's entry count and its inventory's record count differ by exactly one, the inventory itself, and not by however many folder markers the packing tool decided to write.

Identifying a package on the wire

The underlying registered media type is application/zip, and clause 26 says so. AFDS has no dedicated IANA media-type registration, and obtaining one is recorded as an open question in the project open-questions register.

The operative half of clause 26 is a prohibition that is easy to miss. A consumer is required not to rely on a media type of application/afds+zip or similar being present, because no such type is registered. Clause 26 instead recommends that a consumer identify a package by opening it and finding a parseable root manifest whose afdsFormat field is afds-package. A producer may advertise application/afds+zip in a private context where both ends agree, and clause 26 requires that it not treat that as a registered type.

For a tester, that turns into a concrete check: identification by extension or served type is a smell, and identification by opening the file and reading afdsFormat is what the specification asks for.

What the container borrows from the Open Packaging Conventions

The obvious question about a ZIP-based container is why AFDS did not reuse one that already exists. Annex A answers it, and Annex A is informative, so nothing in it is a requirement.

Open Packaging Conventions, standardised as ECMA-376 Part 2 and ISO/IEC 29500-2, is a formal ZIP-based multi-part container. An OPC package holds parts, each with a name and a content type; content types are declared in a [Content_Types].xml part at the package root; and relationships between parts are declared in separate XML relationship parts under _rels directories, so that a consumer discovers the structure by walking relationships from a package-level root rather than by convention. OOXML uses that machinery to collect the many related parts of one document into a single logical file, and other formats reuse it: Annex A notes that ECMA-388 states the OpenXPS format requirements "are an extension of the packaging requirements described in the Open Packaging Conventions (OPC) Standard".

Annex A summarises the position in one sentence: "AFDS borrows the principle and rejects the machinery."

What it borrows, at A.1, is the principle that a package is one logical object made of related parts: a consumer receives one file, can identify it, and can enumerate its contents without hunting through a folder tree. A.1 credits OPC with demonstrating that a ZIP archive is a sound basis for exactly that.

What it rejects, at A.2, is the rest.

OPC mechanismAFDS position (A.2)Reason given
XML parts as the content modelRejectedAFDS content is JSON and Markdown centred, and wrapping JSON in XML parts adds a representation nobody needs
[Content_Types].xmlRejectedThe inventory already carries a media type per entry, in the same file that carries the digest
_rels relationship partsRejectedThe manifest already supplies the relationship map, in one place, in the format the rest of the package uses
Part-naming grammarRejectedNormalised relative ZIP paths are sufficient and are what ordinary tools already show
Relationship-walking discoveryRejectedA consumer reads two known root files, and discovery by convention is simpler and easier to verify
Single logical object made of related partsAdoptedThis is the principle worth keeping

A.2 also states the cost rather than hiding it. AFDS gains no benefit from existing OPC tooling, and a developer who already knows OPC has to learn a second set of conventions. The judgement recorded there is that OPC's XML parts and relationship model add complexity without improving a JSON and Markdown centred representation, and that a manifest a person can read in a text editor is worth more to this project than reuse of an XML relationship library.

If you have met OPC before, the two root files are the mapping to hold on to: afds-inventory.json does the work of [Content_Types].xml and adds digests, and afds-manifest.json does the work of _rels and adds identity, licensing and profile claims.

What is in a package

A package declares a fixed hierarchy, so that a consumer knows where each kind of artefact lives without consulting a directory listing (clause 27). The full hierarchy is below. Nothing in it is a suggestion: clause 27.2 attaches a requirement level to every path, and clause 27.3 states what a producer may not do with it.


package root
├── afds-manifest.json        REQUIRED  what the package is, and where its canonical sources are
├── afds-inventory.json       REQUIRED  what the package contains, byte for byte
├── LICENSES.md               RECOMMENDED  licence summary (the only other file the spec defines at root)
├── tokens/                   REQUIRED in every profile
├── components/               REQUIRED in the components and full profiles
│   └── <component>/                    one subdirectory per component: contract plus prose specification
├── patterns/                 OPTIONAL   multi-component flow documentation, and the reserved registry.json
├── manifests/                OPTIONAL   generated interface manifests, for example a Custom Elements Manifest
├── evidence/                 REQUIRED in the full profile
├── adapters/                 OPTIONAL
│   └── <target>/                       declaration, transform report, and for an export adapter its output
├── docs/                     RECOMMENDED  human-readable package documentation
├── schemas/                  OPTIONAL   JSON Schema documents for the package's own machine-readable artefacts
└── stories/                  OPTIONAL   executable examples and test fixtures

Two facts about that tree are worth saying out loud, because a reader skimming a folder listing will not infer them.

At the archive root sit exactly two required files (clause 27.1): afds-manifest.json, which states what the package is and where its canonical sources are, and afds-inventory.json, which states what the package contains, byte for byte. Beneath the root sit up to nine directories, and clause 27.1 names all nine: tokens/, components/, patterns/, manifests/, evidence/, adapters/, docs/, schemas/, stories/.

A licence summary, LICENSES.md, may sit at the root (clause 27.1). No other root-level file is defined by the specification, and clause 27.1 says a producer "SHOULD NOT add one". That is a SHOULD NOT rather than a MUST NOT, so adding a root-level README.md is a departure you have to be able to justify rather than an automatic failure.

The requirement level of each path, from the clause 27.2 table:

PathKindRequiredContents
afds-manifest.jsonFileREQUIREDPackage identity, version, licences, profile, and canonical source declarations
afds-inventory.jsonFileREQUIREDOne record per package entry except itself, with length, media type, role, and digest
tokens/DirectoryREQUIRED in every profileDesign-token files validating against the declared Design Tokens Format Module version
components/DirectoryREQUIRED in the components and full profilesOne subdirectory per component
patterns/DirectoryOPTIONALMulti-component flow and guidance documentation
manifests/DirectoryOPTIONALGenerated interface manifests, for example a Custom Elements Manifest
evidence/DirectoryREQUIRED in the full profileEngine-qualified evidence records and known-limitations prose
adapters/DirectoryOPTIONALAdapter declarations, transform reports, and export output
docs/DirectoryRECOMMENDEDHuman-readable package documentation
schemas/DirectoryOPTIONALJSON Schema documents for the package's machine-readable artefacts
stories/DirectoryOPTIONALExecutable examples and test fixtures
LICENSES.mdFileRECOMMENDEDHuman-readable statement of the licensing arrangement

The per-profile entries in that column are the only place the hierarchy depends on which completeness profile a package declares. The profiles themselves, and what each requires, are at clause 34 and are covered elsewhere in this guide.

Clause 27.3 adds three prohibitions and one habit.

A producer may not place a canonical token file outside tokens/, may not place a component contract outside components/, and may not place adapter output or a transform report outside adapters/ (clause 27.3). Each of those is a separate MUST NOT, and together they are what allow a consumer to find a kind of artefact without a search.

The habit concerns absence. An empty optional directory carries no information, so clause 27.3 asks a producer to omit an optional directory rather than shipping it empty, and requires it to "declare the absence in the manifest where the manifest has a corresponding field". That qualifier matters and is easy to drop: manifests/ and docs/ have no corresponding manifest field, so there is nothing to declare for them. Where a field does exist, clause 27.3 states that an empty array in the manifest is a positive declaration of absence and is preferable to omitting the field.

The distinction is the same one the whole format keeps making: an empty array says somebody considered the question and answered it, and a missing field says nothing at all.

Who owns a fact: the artefact roles

Every inventoried entry has exactly one role, and clause 28 gives the role a job: it records who owns the fact the entry carries. That is the mechanism which keeps the accessibility contract portable rather than leaking into a build output.

Clause 28.1 is headed "The six roles" and defines six.

RoleMeaning (clause 28.1)
canonicalThe authoritative source of the facts it carries. Nothing else in the package may contradict it.
derivedGenerated from one or more canonical artefacts and reproducible from them.
adapterProduced by an adapter for a specific external target, and shaped by that target's limits.
evidenceA record of observation: what was tested, on which engine and assistive technology, on what date, with what result.
documentationHuman-readable prose explaining canonical artefacts. Explanatory, not authoritative.
schemaA machine-readable schema that other artefacts in the package validate against.

Six is the number clause 28.1 gives, and the identifiers are exactly those six lower-case strings.

The ownership rule

Clause 28.2 states the rule in one line: a derived or adapter artefact must not be the only source of a fact owned by a canonical artefact.

The rest of the clause is what "owned" means in practice. A token value is owned by the canonical token file. A component's semantic model, derivation, keyboard contract, Reflow behaviour, WCAG mapping, guarantees, non-guarantees, assertions, and uncertainty are owned by the canonical component contract — nine things, and clause 28.2 lists all nine. An observation of assistive-technology behaviour is owned by an evidence record.

A guarantee's substantiation status is owned by neither. Clause 28.2 states that it "is computed from the two together and MUST NOT be written into either, as clause 14.3 requires". This is a rule a producer breaks by being helpful: caching a computed substantiated flag into a contract, or into an evidence record, is prohibited, because the cached value can then disagree with the two artefacts it was computed from.

The reason the rule exists is stated as a failure mode rather than a principle. If a fact exists only in a generated stylesheet, a design-tool library, or a platform resource bundle, the fact has left the portable bundle, and at that point the package no longer carries the accessibility contract — which clause 28.2 calls "the exact failure the format exists to prevent".

Two consequences are testable, which is why a QA engineer should care about this clause.

The first is that any derived or adapter artefact must be regenerable from the canonical artefacts in the same package alone (clause 28.2). If regeneration loses a fact, the fact was only in the derived artefact, and clause 28.2 says the package does not conform. Clause 33.4 states the single exception, which is an import report.

The second is that a consumer may discard every derived and adapter entry and still hold a complete design system (clause 28.2). That is hard to check directly, so clause 28.2 offers an approximation a verifier can implement: confirm that no canonical artefact references a derived or adapter path as its source. It is the only mechanisable form of the ownership rule, and it is worth building.

Documentation is not authoritative

A documentation artefact explains a canonical artefact, and clause 28.3 requires that it introduce no normative fact of its own. Where prose and contract disagree, clause 28.3 states that the contract wins and the prose is a defect to be corrected.

The clause explains why it needed saying: a reader naturally trusts the readable file over the machine-readable one, and in this format that instinct is wrong. The worked example at the end of this section contains a live instance of exactly this, in a package that is otherwise careful.

The manifest, field by field

This subsection is a schema definition. A designer can read the first three paragraphs and skip the tables; a producer implementer needs all of them.

The manifest states what the package is, who may use it and under what terms, which profile it claims, and where every canonical source lives (clause 29). Clause 29.1 sets out the fields, shows nesting with dotted paths, and states the reading rule: "A field marked REQUIRED MUST be present; a field marked OPTIONAL MAY be omitted, and a consumer MUST NOT infer a default beyond the one stated."

There is no implicit default anywhere in this table: if a field is absent and no default is stated, a consumer does not get to guess one.

Clause 29.1 defines twenty-six fields, of which eighteen are required. Two of those eighteen are required only in particular completeness profiles, and are marked as such below.

FieldTypeRequiredWhat it carriesClause
afdsFormatStringREQUIREDFormat identifier; must be the exact string afds-package29.1
afdsVersionStringREQUIREDVersion of the package format, as semantic versioning; 1.0.0 for this specification29.1, 35
packageIdStringREQUIREDStable identifier, unique within the publisher's namespace; reverse-DNS form is RECOMMENDED29.1
packageVersionStringREQUIREDSemantic version of the package payload, independent of afdsVersion29.1, 35
titleStringREQUIREDHuman-readable package title29.1
descriptionStringREQUIREDProse description of what the package contains and is for29.1
createdStringREQUIREDCreation date of this package version, as an ISO 8601 date29.1
conformanceProfileStringREQUIREDThe declared completeness profile identifier from clause 34, and nothing else29.1, 34
methodProfilesArray of stringsREQUIRED, may be emptyMethod profile identifiers claimed; an empty array declares that no method is claimed29.1, 20.2
targetConformanceLevelStringREQUIREDDefault target WCAG level, one of A, AA, AAA; not inferable from any other field29.1, 12.4
licences.codeStringREQUIREDSPDX identifier for code and machine-readable artefacts29.1
licences.documentationStringREQUIREDSPDX identifier for prose29.1
publisher.nameStringREQUIREDName of the person or organisation publishing the package29.1
publisher.projectStringOPTIONALProject the package belongs to29.1
publisher.uriStringOPTIONALPublisher URI; informational only, and it proves nothing about provenance29.1
tokens.dtcgVersionStringREQUIREDVersion of the Design Tokens Format Module the token files validate against29.1
tokens.canonicalSourcesArray of source objectsREQUIREDCanonical token files; at least one entry in every profile29.1
components.canonicalSourcesArray of component objectsREQUIRED in the components and full profilesCanonical component declarations29.1
patterns.canonicalSourcesArray of source objectsOPTIONALCanonical pattern documentation, and the registry where clause 29.4 requires one; an empty array declares absence29.1, 29.4
localProfilesArray of local profile objectsOPTIONALMethod profiles defined by this package rather than by Part III29.1, 29.3
evidence.canonicalSourcesArray of source objectsREQUIRED in the full profileCanonical evidence records29.1
schemas.canonicalSourcesArray of source objectsOPTIONALSchema documents shipped in the package29.1
documentation.sourcesArray of source objectsOPTIONALDocumentation artefacts worth enumerating29.1
adaptersArray of adapter objectsREQUIREDDeclared adapters; an empty array declares that the package ships none29.1, 33.5
storiesArray of source objectsOPTIONALExecutable examples and fixtures29.1
notesArray of stringsOPTIONALStatements a consumer should read before relying on the package29.1

Note the shape of adapters: it is required even in a package that ships no adapter at all, and the empty array is the declaration, which is clause 27.3's positive-declaration rule showing up as a required field.

A source object appears wherever the manifest points at a single artefact (clause 29.1).

FieldTypeRequiredWhat it carries
idStringREQUIREDIdentifier unique within its array
pathStringREQUIREDPackage-relative path to the artefact; it must appear in the inventory
roleStringREQUIREDOne of the six roles in clause 28
descriptionStringRECOMMENDEDWhat the artefact carries

The path rule is a cross-check between the two root files: a source object naming a path that no inventory record covers makes the package non-conforming (clause 29.1).

A component object replaces path with two paths, because clause 29.1 takes it as given that a component always has both a contract and a prose specification.

FieldTypeRequiredWhat it carries
idStringREQUIREDStable component identifier
nameStringREQUIREDHuman-readable component name
kindStringREQUIREDComponent kind, for example layout-primitive or interactive-component
specificationStringREQUIREDPath to the component specification
documentationStringREQUIREDPath to the component documentation
roleStringREQUIREDMust be canonical

That last row is not decoration: a component object declaring any other role is non-conforming, because a component contract is by definition the authoritative source of the facts it carries (clauses 29.1, 28.2).

An adapter object is specified at clause 33.5, and is set out later in this section.

Where a conformance claim actually lives

Clause 4.4 requires a conformance claim to state three things: "the format version, the completeness profile, and the set of method profiles claimed, which MAY be empty". All three travel in the manifest, and it is worth knowing which field carries which, because the three are routinely confused.

Part of the claim (clause 4.4)Manifest fieldNotes
Format versionafdsVersionThe version of the package format, not of the design system
Completeness profileconformanceProfileExactly one identifier from clause 34; clause 29.1 states that "the value it carries is a completeness profile and nothing else"
Method profiles claimedmethodProfilesAn array, possibly empty; an empty array is a claim of no method, not an omission

A fourth field is often mistaken for part of the claim and is not. targetConformanceLevel states the WCAG level the package targets by default (clause 12.4), and clause 29.1 says in terms that it is "not inferable from any other field". Clause 4.5 requires the completeness axis and the method axis to be declared separately and prohibits a consumer from inferring either from the other, and clause 34 extends the same independence to the target level. The completeness profiles themselves are covered at clause 34, elsewhere in this guide.

Clause 4.4 also states two prohibitions about how a claim may be worded, and they belong with the manifest because the manifest is where a claim becomes machine-readable. A claim may not be expressed as conformance to an informative document or to a guide that has no conformance model; in particular, clause 4.4 states that a package "MUST NOT claim that a component conforms to the ARIA Authoring Practices Guide, because that guide is informative and has no conformance model to conform to". What a package may publish about a component is the accessibility criteria met, the semantics used, and the recorded assistive-technology results. And a conformance claim is a claim about a package, not about a service built from it: clause 4.4 prohibits a producer from presenting one as evidence that an assembled service is accessible.

Declaring a method profile the specification does not define

Clause 20.4 defines four method profiles and permits an organisation to define its own, requiring a local identifier to be namespaced with a prefix that is not afds-. Clause 29.3 says how such a profile travels.

A package that lists an identifier in methodProfiles which is not defined in clause 20.4 is required to declare that profile in a localProfiles array (clause 29.3). The reverse is prohibited in two directions: a localProfiles entry may not use an identifier defined in clause 20.4, because a package cannot redefine a profile the specification defines, and a package may not supply a provenance object for a specification-defined profile either (clause 29.3).

A local profile object has five fields (clause 29.3).

FieldTypeRequiredWhat it carries
idStringREQUIREDProfile identifier; must not begin with afds-
titleStringREQUIREDHuman-readable profile name
statementStringREQUIREDThe profile's statement, as clause 20.1 requires of every profile
specificationStringOPTIONALPackage-relative path to the profile's full text; where present it must appear in the inventory
provenanceProvenance objectREQUIREDThe profile's provenance, per clauses 20.6 and 29.3.1

The provenance object is where a local profile says what it took from elsewhere and what it invented. Clause 29.3.1 fixes its serialised form: four members, of which three are required arrays.

MemberTypeRequiredContent
adoptedArray of adopted entriesREQUIRED, may be emptyWhat the profile takes from work outside the package
changedArray of changed entriesREQUIRED, may be emptyWhat the profile alters about an adopted idea
originatesArray of originates entriesREQUIRED, must not be emptyWhat the profile asserts on its own authority
statementStringOPTIONALProse accompanying the structured members

originates is the one that cannot be empty (clause 29.3.1): a profile that adopts everything and originates nothing is not a profile, it is a citation.

An adopted entry carries id, what, source.author, source.title, and source.uri where one exists, with everything but the URI required unconditionally (clause 29.3.1). A changed entry carries adoptedRef, which must match an id in the same adopted array, what, and direction, which is one of stricter, weaker, or different (clause 29.3.1). An originates entry carries what and appliesTo, both required (clause 29.3.1).

Four checks are mechanical, and clause 29.3.1 permits a verifier to run them: every adoptedRef resolves within the same adopted array, every direction is one of the three permitted values, originates is not empty, and every adopted entry carries an author and a title.

Then comes the sentence a tool author has to respect. Clause 29.3.1 states that no check establishes that an attribution is truthful, and that "a tool MUST NOT report a passing structural check as a verified provenance". Clause 20.5 makes attributing a requirement to a source that does not support it a conformance failure, and detecting that failure requires reading the source. A green tick on a provenance object means the shape is right, not that the citation is honest, and a report implying otherwise is itself a defect.

The reserved pattern-registry path

Clause 24.2 requires a package claiming afds-patterns-native-first to carry a package-level registry of component and pattern statuses. Clause 29.4 binds it to a path.

A package claiming that profile is required to carry the registry at patterns/registry.json, to declare it in patterns.canonicalSources with role canonical, and to include it in the inventory (clause 29.4). The path is reserved: clause 29.4 prohibits using patterns/registry.json for anything other than a registry satisfying clause 24.2, whether or not the package claims the profile.

The registry is canonical rather than derived even though its component entries restate a fact each component contract already carries. Clause 29.4 gives the reason, and it is the same honesty argument that runs through the format: the registry's prohibition entries record a pattern the package has declined, and no component contract can supply that, "because a decision not to build something leaves no component behind to declare it".

The inventory, field by field

The inventory is what makes a package verifiable (clause 30). It lists every entry with enough information to detect any change between production and consumption.

Clause 30.1 states what it covers: exactly one record for every entry in the archive, with one exception, which is that it must not contain a record for itself. That exclusion is necessary rather than stylistic, because a record of the inventory inside the inventory could never hold a correct digest — writing the digest would change the bytes it describes. Directory entries are also excluded, as clause 25.2 states, because they have no content.

Clause 30.1 then places an obligation on the reader of a package, not its writer. A consumer is required to verify the inventory before relying on any package content, and the clause spells out what that means: "before parsing a token file, before reading a component contract, and before extracting anything to disk". This is not an affordance a consumer may take up if convenient; parsing a token file first and verifying afterwards does not conform.

Ten top-level fields, from clause 30.2.

FieldTypeRequiredWhat it carries
afdsFormatStringREQUIREDMust be the exact string afds-inventory
afdsVersionStringREQUIREDPackage-format version, matching the manifest
packageIdStringREQUIREDMust match the manifest's packageId
packageVersionStringREQUIREDMust match the manifest's packageVersion
digestAlgorithmStringREQUIREDMust be the exact string SHA-256
digestEncodingStringREQUIREDMust be the exact string lowercase-hex
excludesSelfBooleanREQUIREDMust be true, stating explicitly that the inventory omits itself
entryCountNumberREQUIREDNumber of records; must equal the length of records
descriptionStringRECOMMENDEDProse statement of what the inventory does and does not prove
recordsArray of record objectsREQUIREDOne record per inventoried entry

These are obligations on the producer, not merely things a verifier happens to look at, and four of them are fixed strings or a fixed boolean, which makes them the cheapest possible check on whether a file is an AFDS inventory at all.

Each record object has five required fields (clause 30.2).

FieldTypeRequiredWhat it carries
pathStringREQUIREDPackage-relative normalised path of the entry
mediaTypeStringREQUIREDMedia type of the entry's content, including a charset parameter for text
byteLengthNumberREQUIREDExact uncompressed length of the entry in bytes
roleStringREQUIREDOne of the six roles in clause 28
sha256StringREQUIREDSHA-256 digest of the entry's exact uncompressed bytes, as lowercase hexadecimal

Clause 30.2 asks that records be sorted by path in ascending byte order, as a SHOULD, and gives review convenience as the reason: a rebuilt inventory then produces a diff showing only genuine changes rather than a reshuffle.

One rule about digest format is easy to skip and expensive to get wrong. Clause 30.3 states that a sha256 value "MUST be the full 64 lowercase hexadecimal characters, and a consumer MUST reject a truncated, uppercase, or base-64 digest rather than attempting to interpret it". A consumer that upper-cases and compares is being helpful in a way the specification prohibits, because a package whose digests are the wrong shape is not a package whose integrity has been established.

Verifying a package, step by step

Clause 31 gives the procedure a conforming consumer implements, in ten numbered steps. The order is not editorial. Clause 31 states that the steps are ordered "so that a cheap check never runs after an expensive one it could have prevented, and so that nothing is parsed before the container is known to be safe". Implement them in this order, and number your report against these numbers, so that a producer reading a failure can find the clause.

  1. Open as ZIP. Open the file using ZIP syntax. If it is not a readable ZIP archive, report a container failure and stop.
  2. Check paths. For every entry, confirm the path is a normalised relative path, contains no .. or . segment, does not begin with /, and carries no drive letter or UNC prefix. Confirm no single enclosing top-level directory wraps the contents. Report each violation and stop, and do not sanitise.
  3. Check encryption and limits. Confirm no entry is encrypted. Apply the configured limits from clause 32 for entry count, total compressed size, total uncompressed size, per-entry decompression ratio, nesting depth, and path length. Report each violation and stop.
  4. Locate and parse the manifest. Confirm afds-manifest.json exists at the archive root, decode it as UTF-8, parse it as JSON, and confirm afdsFormat is afds-package. Read afdsVersion and apply the version rules in clause 35.
  5. Locate and parse the inventory. Confirm afds-inventory.json exists at the archive root, decode it as UTF-8, and parse it as JSON. Confirm afdsFormat is afds-inventory, digestAlgorithm is SHA-256, digestEncoding is lowercase-hex, and excludesSelf is true. Confirm packageId and packageVersion match the manifest.
  6. Confirm completeness in both directions. Confirm that every archive entry other than the inventory itself and other than directory entries has exactly one inventory record, and that every inventory record names an entry that exists. Confirm the inventory holds no record for itself. Confirm entryCount equals the number of records. Report every unmatched name in both directions.
  7. Compare byte lengths. For each record, compare the entry's uncompressed length with byteLength. Report every mismatch.
  8. Recompute and compare digests. For each record, compute the SHA-256 digest of the entry's exact uncompressed bytes and compare it with sha256 as lowercase hexadecimal. Report every mismatch. Clause 31 states that if any digest fails, the consumer "MUST NOT rely on any package content".
  9. Validate token files. For each canonical token source named in the manifest, decode it as UTF-8, parse it as JSON, and validate it against the Design Tokens Format Module version declared in tokens.dtcgVersion. Report every validation failure. A consumer that cannot validate against the declared version is required to report that it did not validate, rather than passing the step silently.
  10. Report. Emit a single report giving a pass or fail verdict, the count of entries checked, and every individual problem found. Clause 31 requires that a consumer "MUST NOT report a pass when any step failed, and MUST distinguish 'checked and passed' from 'not checked'".

Step 4 is the one most often written short: reading afdsVersion is half of it, and applying the clause 35 version rules is the other half, which decides whether the package may be processed at all. Clause 35 is covered elsewhere in this guide.

Clause 31 names two properties of the procedure as deliberate, and both are worth preserving in an implementation.

Steps 2 and 3 run before anything is parsed or extracted, so a hostile archive is rejected before its content is touched. Steps 6 to 9 gather all problems rather than stopping at the first, because a partial report causes a producer to fix one defect at a time.

Step 9 deserves a second look from a tester, because it is the only step whose honest outcome may be "I did not check this": a consumer with no validator for the declared Design Tokens Format Module version cannot pass it quietly, which is the same distinction step 10 requires the report to carry throughout.

Security requirements

A package arrives from somewhere else, so clause 32 treats it as untrusted input. There are three concerns and they are separable.

Path traversal

A ZIP archive stores a path for each entry, and clause 32.1 notes that nothing in ZIP syntax prevents that path being absolute or containing .. segments. A naive extractor that joins the entry path onto an output directory can therefore be made to write outside that directory, overwriting arbitrary files.

Clause 32.1 states three requirements, and each is separate. A consumer is required to reject any entry whose path is absolute, contains a .. or . segment, or is not normalised. A consumer is required to perform this check before extracting anything. And a consumer is prohibited from rewriting an offending path into a safe one, "because that silently changes what the package says and hides the attack".

The third is the one a developer argues with: sanitising feels like defence, and clause 32.1 treats it as concealment.

Decompression limits

A small archive can expand to an enormous volume of data, exhausting memory or disk, and clause 32.2 notes that nesting archives inside archives multiplies the effect.

Clause 32.2 requires a consumer to enforce configured limits and to fail rather than continuing when a limit is reached. Six limits are named, with suggested defaults.

LimitPurposeSuggested default (clause 32.2)
Entry countBound the number of records and file handles5000 entries
Total compressed sizeBound the input read32 MiB
Total uncompressed sizeBound memory and disk consumption256 MiB
Per-entry decompression ratioDetect a single highly compressible entry200 to 1
Nesting depthBound path recursion and nested archives16 path segments
Path lengthBound filesystem interaction255 characters

Clause 32.2 says the defaults are suggestions, not requirements. What is required is the mechanism around them: a consumer is required to make its limits configurable and to report which limit was exceeded, "so that a legitimately large package can be handled by raising a named limit rather than by disabling the checks".

There is also an ordering expectation. Clause 32.2 states that a consumer "SHOULD compute the uncompressed total from the archive's own metadata first and reject an over-large package before decompressing anything, then enforce the same limit again during decompression, because the declared metadata may lie" — two passes, because the first is cheap and the second is the one that cannot be fooled.

Integrity is not authenticity

Inventory integrity is not a digital signature, and clause 32.3 is unusually direct about it.

SHA-256 digests detect that content changed between the moment the inventory was written and the moment it was verified. Clause 32.3 grants that this is genuinely useful, because it catches truncated downloads, corrupted media, accidental edits, and careless repackaging.

What it does not do is enumerated, and clause 32.3 states that "a consumer MUST NOT claim otherwise".

PropertyProvided by the inventory? (clause 32.3)
Detects accidental or in-transit changeYes
Detects a change made after the inventory was writtenYes
Identifies who produced the packageNo
Proves the package came from the claimed publisherNo
Prevents an attacker rewriting content and rebuilding the inventoryNo
Establishes a chain of custodyNo

The reason is arithmetic rather than cryptographic. An attacker who can alter the content can also recompute the digests and rewrite the inventory, and nothing in the package binds it to a key, so nothing in it can be attributed. Clause 32.3 draws the conclusion that follows for the manifest: "The publisher object in the manifest is a claim, not evidence."

A future signature mechanism is therefore needed for trusted distribution, and the project open-questions register records it as open. Until such a mechanism exists, clause 32.3 requires that "trust in a package MUST come from the channel it arrived on rather than from the package itself".

For anyone writing a verification report, that turns into a wording rule: "integrity verified" is accurate, while "package verified" invites the reader to hear authenticity, which clause 32.3 prohibits claiming.

Adapters, and honest transforms

An adapter moves information between the canonical artefacts of a package and the representation an external tool or platform uses (clause 33). Figma, Penpot, CSS custom properties, native platform resources, and Electron shells are all adapter targets.

An adapter reads a package and writes something, or reads something and drafts a package, so it is a consumer and a producer at once. Clause 4.2 says so directly — "An adapter is always both, which is why Part IV gives it its own clause" — and requires a tool that is both to satisfy both sets of obligations independently. That is the sentence to keep in mind through the rest of this subsection: an adapter does not get a relaxed version of either role.

An export adapter reads canonical artefacts and writes the representation a target expects. An import adapter reads a target's representation and drafts the artefacts an AFDS package requires.

Both directions are in scope, and clause 33 records why: a format that can only export can be adopted only by a design system that began in it, and no established design system did. An adopter arrives holding a design-tool library, a token file, a component library, and a good deal of knowledge nobody wrote down. Leaving import undefined would not stop anyone importing; it would push the work into hand transcription and one-off scripts whose output lands in a package with nothing recording which facts were real and which were guessed.

The two directions do not carry the same obligations, and clause 33 explains the asymmetry. An export knows the full set of facts it is permitted to state, because it reads artefacts that own them, so its whole problem is what the target refuses to accept. An import does not know, "because the representation it reads was never obliged to carry an accessibility contract at all".

Direction

Clause 33.1 requires each element of the manifest's adapters array to declare exactly one direction, either export or import. A target supported in both directions is required to be declared as two adapters sharing a target value (clause 33.1).

The reason clause 33.1 gives is about discharge of obligations: the two directions produce different artefacts and different reports, and a single object describing both would leave a consumer unable to determine which obligations had been discharged.

What both directions owe

Clause 33.2 requires an adapter to report its mappings and its warnings, and to report whatever it could not carry. It prohibits silently flattening meaning.

Silent flattening is the more dangerous behaviour of the two, because the output looks complete. Clause 33.2 gives three examples, and each is a real limit rather than an illustration: a ch-based measure has no direct native analogue, a forced-colours boundary has no equivalent in a target that has no concept of a user-forced colour palette, and a keyboard contract has no representation at all in a token pipeline. In each case, clause 33.2 says the honest output is a recorded finding, not an approximation presented as an equivalent.

No adapter in either direction may produce an artefact with the role canonical (clause 33.2), and the reason is the ownership rule at clause 28.2: an artefact shaped by a target's limits cannot own a fact.

Export adapters (clause 33.3)

Clause 33.3 is short and entirely operational. Export output is required to carry the role adapter or derived, never canonical, is required to be regenerable from the canonical artefacts alone as clause 28.2 requires, and a producer is required to place it under adapters/<target>/out/. That path is a real requirement rather than a convention, and it is the one export rule a producer discovers late, after the output has been written somewhere more convenient.

What an import may not do

An import adapter is prohibited from writing an artefact with the role canonical (clause 33.4).

The output of an import is a draft, and a draft is not a contract. Clause 33.4 states that a draft becomes canonical only when a person reviews it, supplies what the source could not, and accepts responsibility for the accessibility claims the artefact then makes. The specification calls that act promotion, and requires that promotion be performed by a person and not by a transform, "because a canonical artefact asserts a contract that somebody has to be willing to defend".

Two consequences follow for what may ship.

Import output is not itself a package artefact, and clause 33.4 prohibits a producer from shipping an unpromoted draft in a conforming package, because once a draft is inside a package it is indistinguishable from a contract to whoever relies on it.

What the package retains from an import is the import report, which is the provenance of every artefact promoted from that import. Clause 33.4 requires an import report to carry the role adapter, and exempts it from the regenerability consequence stated in clause 28.2. The exemption is narrow and structural: an import reads a source that lies outside the package by definition, so no package can regenerate it. Clause 33.4 notes that the alternative to the exemption is discarding the provenance of every imported artefact, which is a worse outcome than a stated exception.

Clause 33.4 then closes the gap a reader would otherwise find. Every gaps entry in an import report is required to appear in the promoted artefact as an uncertainty record or as a declared non-guarantee. An import that could not discover a component's keyboard behaviour has not thereby excused the package from declaring that the keyboard behaviour is unknown.

There is also a rule about how an import runs, not only about what it produces. Clause 33.4 requires an import to be a discrete run that produces a dated report, and prohibits it from being a live read-through dependency on an external tool's model. A read-through dependency makes the external tool the effective owner of whatever it supplies, which is the failure clause 28.2 exists to prevent, and it leaves no report a reviewer can examine.

The adapter declaration

Each element of the manifest's adapters array is an adapter object, and clause 33.5 gives it nine fields.

FieldTypeRequiredWhat it carries
idStringREQUIREDAdapter identifier, unique within the package
directionStringREQUIREDEither export or import
targetStringREQUIREDThe external tool or platform, for example figma or css-custom-properties
adapterVersionStringREQUIREDSemantic version of the adapter that produced the output
declarationStringREQUIREDPath to the adapter's own declaration file
reportStringREQUIREDPath to the transform report
inputsArray of stringsREQUIREDFor export, paths of the canonical artefacts consumed; for import, identifiers of the external sources read, which are not package paths
outputsArray of stringsREQUIREDFor export, paths of the generated artefacts; for import, an empty array, because import output is not a package artefact
promotedArray of stringsREQUIRED for importPaths of the canonical artefacts promoted from this import, and an empty array where nothing has yet been promoted

inputs and outputs are where the two directions stop looking alike. For an export, both are package paths; for an import, inputs are external identifiers that no inventory record will ever match, and outputs is empty by rule. A verifier that checks every inputs entry against the inventory will therefore report false failures on import declarations, and clause 33.5 is the clause that says why it should not.

The transform report

A transform report records what the adapter did, what it could not do, and what it wants a reader to notice (clause 33.6). It is where the honesty becomes checkable rather than aspirational.

Eight fields are required in both directions (clause 33.6).

FieldTypeWhat it carries
adapterIdStringIdentifier of the adapter that produced this report
adapterVersionStringVersion of the adapter
directionStringEither export or import, matching the adapter declaration
targetStringThe external tool or platform
runDateStringISO 8601 date of the transform run
validationStatusStringOne of passed, passed-with-warnings, or failed
mappingsArray of mapping objectsOne record per fact carried across
warningsArray of finding objectsFacts carried across with a caveat; an empty array if none

An export report additionally requires two arrays (clause 33.6).

FieldTypeWhat it carries
lossesArray of finding objectsFacts the target could not accept; an empty array if none
unsupportedArray of finding objectsSource features the target has no concept of; an empty array if none

An import report additionally requires the two that face the other way (clause 33.6).

FieldTypeWhat it carries
gapsArray of finding objectsFacts that an AFDS artefact requires and the source could not supply; an empty array if none
unmappedArray of finding objectsSource content for which AFDS has no representation; an empty array if none

A mapping object has source, sourceKind, targetName, and fidelity, where fidelity is one of exact, approximate, or partial (clause 33.6). A finding object has source, severity, statement, and consumerAction, where severity is one of info, warning, or error and consumerAction says plainly what a person consuming the output must do about it (clause 33.6).

Take the literal token values from those lists. The status vocabulary is passed, passed-with-warnings, failed, hyphenated as shown, and the severity vocabulary is info, warning, error — not "passed with warnings" and not "information", and an adapter emitting the prose forms emits invalid values.

Every array is required even when empty, and clause 33.6 explains why in terms that generalise beyond adapters. An empty losses array is a positive claim that nothing was lost, which a reviewer can challenge; an omitted losses field is merely silence. The same reasoning applies to gaps, where clause 33.6 notes that an empty array claims the source supplied every fact an AFDS artefact requires, "which is a strong claim and rarely a true one".

Two status rules are mechanical. An export report containing a losses or unsupported entry with severity error is required to set validationStatus to failed (clause 33.6). An import report containing a gaps entry with severity error is required to do the same (clause 33.6).

Clause 33.6 then says something a reader will otherwise misread as a bug. A failed import report is not a malfunction, and for most targets it is the expected result: it states that the source cannot yield a conforming artefact without human authorship, which is information a person needs before deciding how much work an adoption will cost.

Round-tripping

An export followed by an import is not a round trip in any sense that returns what was sent.

An export is a projection, and a projection discards. Running it backwards does not restore what it dropped, because the information is not in the target to be read. A system exported to a token pipeline and imported back is a system with no keyboard contracts, no evidence, and no non-guarantees, because a token pipeline never held any of those. Clause 33 states the underlying reason: an import reads a representation that "was never obliged to carry an accessibility contract at all".

The value of a defined import path is not that it makes round-tripping work. It is that the returned system arrives saying so, in a dated report with a gaps array, instead of arriving looking complete (clauses 33.4, 33.6).

What survives is whatever the target's representation can hold — token values, usually, and names. What does not survive is everything Part II adds: the semantic model, the keyboard contract, the Reflow behaviour, the WCAG mapping, the guarantees and non-guarantees, the assertions, the evidence, and the uncertainty. Those have to be re-authored by a person, which is what promotion means (clause 33.4).

A worked example: the sample package

The repository ships a small complete package at afds-sample/, and this subsection walks through it, quoting the files as they stand. The sample is a real work-in-progress package rather than an idealised one. Where a point in it is known to depart from the specification, the departure is named in the prose here; the full list is kept as a defects register alongside the repository rather than in this guide, so that this guide stays a description of the specification.

The source tree contains eleven files, of which ten become inventory records.


afds-sample/
├── afds-manifest.json
├── afds-inventory.json          not recorded in itself
├── LICENSES.md
├── adapters/README.md
├── components/stack/stack.md
├── components/stack/stack.spec.json
├── docs/PACKAGE.md
├── evidence/at-matrix.json
├── evidence/known-limitations.md
├── patterns/registry.json
├── tokens/core.tokens.json
├── README.md                    repository-side, excluded from the package
└── tools/build-inventory.py     repository-side, excluded from the package

The last two are not package entries: tools/build-inventory.py excludes them explicitly, with EXCLUDED_TOP_LEVEL = {"tools", "README.md"} and the comment "Repository-side helpers that are not part of the distributable package." That is consistent with clause 27.1, which defines no root-level file beyond the two required ones and LICENSES.md.

It is worth being clear about what kind of rule that is. The specification says nothing about the directory a package is built from, so nothing in it makes those two files excludable and nothing in it would be violated if they were included. The boundary is the sample's own, stated in a table in its README.md that marks every path in the directory as package content or not, and the verify command checks that table against the package it builds so the two cannot drift apart. If you publish from a working directory that holds more than the package, the same is true of you: the boundary is yours to draw and yours to write down. Open question H7 records that the specification is silent on the matter.

What the manifest says

The specification reproduces this manifest in full at clause 29.2, generated from the file rather than transcribed so that the example cannot drift from what it describes. The excerpts below are drawn from the same file and are quoted in the order the fields appear, so you can read either against the field table at clause 29.1.

afds-manifest.json opens with the four identity fields and then the three-part claim.


{
  "afdsFormat": "afds-package",
  "afdsVersion": "1.0.0",
  "packageId": "com.a11ybob.abd.afds-sample",
  "packageVersion": "1.0.0",
  "title": "AFDS Sample",
  "description": "A minimal but complete Accessibility Focused Design System package demonstrating the declared hierarchy, canonical source declarations, a DTCG token sample, one layout-primitive component contract, structured assistive-technology evidence, adapter guidance, and dual licensing.",
  "created": "2026-08-29",
  "conformanceProfile": "afds-components",
  "methodProfiles": [
    "afds-patterns-native-first"
  ],
  "targetConformanceLevel": "AA"
}

Read those last three lines against clause 4.4: the format version is 1.0.0, the completeness profile is afds-components, and the set of method profiles claimed is ["afds-patterns-native-first"]. That is the whole conformance claim, and targetConformanceLevel of AA is a separate statement made under clause 12.4. docs/PACKAGE.md puts the four together in one sentence: "complete at the component level, claiming the pattern method, not claiming the layout method, targeting Level AA."

The licences and publisher blocks carry the two required SPDX identifiers and the required publisher.name, plus both optional publisher fields.


{
  "licences": {
    "code": "GPL-3.0-only",
    "documentation": "CC-BY-SA-4.0"
  },
  "publisher": {
    "name": "Bob Dodd",
    "project": "Accessible by Design",
    "uri": "https://a11ybob.com/"
  }
}

The tokens block carries the required dtcgVersion and one source object with all four of its fields.


{
  "tokens": {
    "dtcgVersion": "2025.10",
    "canonicalSources": [
      {
        "id": "core",
        "path": "tokens/core.tokens.json",
        "role": "canonical",
        "description": "Core spacing, typography, measure, and colour tokens for the sample."
      }
    ]
  }
}

dtcgVersion is the field that makes step 9 of the verification algorithm possible at all, because a validator otherwise has to guess which version of the token format applies (clauses 29.1, 31).

The components block carries one component object, and its role is canonical, as clause 29.1 requires.


{
  "components": {
    "canonicalSources": [
      {
        "id": "stack",
        "name": "Stack",
        "kind": "layout-primitive",
        "specification": "components/stack/stack.spec.json",
        "documentation": "components/stack/stack.md",
        "role": "canonical"
      }
    ]
  }
}

The patterns block is where the method-profile claim costs something. Because the package claims afds-patterns-native-first, clause 29.4 requires the registry at patterns/registry.json, declared in patterns.canonicalSources with role canonical, and present in the inventory. All three hold here.


{
  "patterns": {
    "canonicalSources": [
      {
        "id": "pattern-registry",
        "path": "patterns/registry.json",
        "role": "canonical",
        "description": "Package-level pattern registry required of a package claiming afds-patterns-native-first, at specification clause 24.2. Records the status of every component and every pattern this package has declined."
      }
    ]
  }
}

The evidence block declares one source, the JSON matrix, with role evidence. The package's known-limitations prose sits in the same directory but is declared in documentation.sources with role documentation, because clause 28.1 defines evidence as a record of observation carrying an engine, an assistive technology, a date, and a result, and narrative prose carries none of those. The directory and the role are independent: clause 27.2 assigns known-limitations prose to evidence/, and the role describes what the artefact is rather than where it sits. Then the empty declarations, which are the part a reader most often misreads.


{
  "schemas": {
    "canonicalSources": []
  }
}

That is an object containing an empty array, not an empty array, whereas adapters and stories are themselves empty arrays.


{
  "adapters": [],
  "stories": []
}

adapters is required by clause 29.1 even when a package ships none, and the empty array is the declaration that it ships none. stories is optional, and the empty array is clause 27.3's positive declaration of absence in preference to omitting the field.

The documentation.sources array enumerates four prose artefacts, and each carries the id that clause 29.1 marks as REQUIRED of every source object.


{
  "documentation": {
    "sources": [
      {
        "id": "package-doc",
        "path": "docs/PACKAGE.md",
        "role": "documentation",
        "description": "What this sample package demonstrates."
      },
      {
        "id": "licences-doc",
        "path": "LICENSES.md",
        "role": "documentation",
        "description": "Dual licensing arrangement for code and documentation."
      },
      {
        "id": "adapters-readme",
        "path": "adapters/README.md",
        "role": "documentation",
        "description": "Adapter guidance and the no-adapter-is-canonical rule."
      },
      {
        "id": "known-limitations",
        "path": "evidence/known-limitations.md",
        "role": "documentation",
        "description": "Narrative account of known limitations, non-guarantees, and uncertainty. Explanatory only; the records it discusses are canonical."
      }
    ]
  }
}

That id field is the one most often left out, because a path already looks like an identifier. It is not one: a path can change when a file is moved without the artefact changing what it is or what role it plays, which is why clause 29.1 requires an identifier that is stable independently of location and unique within its own array.

The notes array closes the file with the three statements the package wants read first.


{
  "notes": [
    "AFDS 1.0.0 is a project draft, not a W3C standard.",
    "Inventory integrity is not a digital signature and does not prove provenance.",
    "No assistive-technology test results in this package are real; every result field is marked not-yet-tested."
  ]
}

The second of those is clause 32.3 written into the package itself, which is the right place for it: a consumer that reads only the manifest still learns that the digests prove nothing about provenance.

Two things are absent from the manifest and should be, given what this package is. There is no localProfiles array, because the one profile claimed is defined at clause 20.4 and clause 29.3 prohibits declaring a specification-defined profile locally. And afds-layout-intrinsic is not claimed, even though Stack is built the way that profile describes, because clause 21.4 requires forced-colours evidence and this package has no real evidence at all. docs/PACKAGE.md makes the reasoning explicit: "A profile claim asserting a method the package cannot show it followed would be exactly the kind of unearned claim the format exists to prevent."

What the inventory says

afds-inventory.json carries all ten top-level fields from clause 30.2, and declares ten records.


{
  "afdsFormat": "afds-inventory",
  "afdsVersion": "1.0.0",
  "packageId": "com.a11ybob.abd.afds-sample",
  "packageVersion": "1.0.0",
  "digestAlgorithm": "SHA-256",
  "digestEncoding": "lowercase-hex",
  "excludesSelf": true,
  "entryCount": 10,
  "description": "Inventory of every entry in this package except this inventory itself. A consumer must verify every record before relying on package content. These digests detect transfer changes; they are not a digital signature and do not identify a signer or prove provenance.",
  "records": []
}

The arithmetic is worth doing once, because it is the check most easily got wrong: ten records, plus the inventory itself, which clause 30.1 requires the inventory to omit, gives eleven entries in a packed archive. entryCount is 10 and records has length 10, as clause 30.2 requires, and excludesSelf is true.

The first and last records show the shape, and the digests are the full sixty-four lowercase hexadecimal characters clause 30.3 requires.


{
  "path": "LICENSES.md",
  "mediaType": "text/markdown; charset=utf-8",
  "byteLength": 2180,
  "role": "documentation",
  "sha256": "bedd3036d453487186e2f516a70368969d0c6e75466c85eed9a909e322651e35"
}

{
  "path": "tokens/core.tokens.json",
  "mediaType": "application/json",
  "byteLength": 3055,
  "role": "canonical",
  "sha256": "b45bb732e28f4c3f906bb37231442e7051fb2ffe34ef6b57753b29dc68c7a29b"
}

The ten records run in ascending byte order by path: LICENSES.md, adapters/README.md, afds-manifest.json, components/stack/stack.md, components/stack/stack.spec.json, docs/PACKAGE.md, evidence/at-matrix.json, evidence/known-limitations.md, patterns/registry.json, tokens/core.tokens.json. That satisfies the sorting SHOULD at clause 30.2. Note that afds-manifest.json appears in the inventory and afds-inventory.json does not, which is clause 30.1's one exception in practice.

Note also the roles the sample assigns. afds-manifest.json, the token file, the pattern registry and stack.spec.json are canonical; the two evidence files are evidence; stack.md, docs/PACKAGE.md, LICENSES.md and adapters/README.md are documentation. The pairing on the component is the one to notice: the component specification is canonical and its component documentation is documentation, which is clause 28.3's ordering made visible in the inventory.

What the tokens say

tokens/core.tokens.json is the smallest artefact in the package and the best place to see the alias mechanism. It declares a modular scale in rem, then defines spacing as aliases of scale steps rather than as independent values.


{
  "space": {
    "$type": "dimension",
    "$description": "Spacing tokens are aliases of scale steps rather than independent values, so spacing cannot drift away from the type scale.",
    "tight": {
      "$value": "{scale.step-minus-1}",
      "$description": "Alias reference to the scale step below the seed."
    },
    "default": {
      "$value": "{scale.step-0}",
      "$description": "Alias reference to the seed step. This is the default Stack gap."
    },
    "loose": {
      "$value": "{scale.step-1}",
      "$description": "Alias reference to one step above the seed."
    }
  }
}

space.default is the token to follow, because it is the one the Stack contract and assertion stack-a1 both refer to. It resolves to scale.step-0, which is { "value": 1, "unit": "rem" }, seeded on one line of body text.

The colour group carries a warning that matters for anyone expecting tokens to carry accessibility facts.


{
  "colour": {
    "$type": "color",
    "$description": "Colour tokens are candidates for contrast pairing, not guarantees. DTCG carries values, not contrast relationships, so the pairing constraint lives in the component specification."
  }
}

That is the ownership rule from clause 28.2 seen from the token side: a contrast relationship is not a value, so it cannot live in a token file, and it lives instead in the contract that owns it.

What the component contract says

components/stack/stack.spec.json carries seven identity fields — afdsSpecVersion, id, name, kind, version, status, and summary — and then ten fields that are the substance: semanticModel, derivation, keyboardContract, reflowBehaviour, wcagMapping, guarantees, nonGuarantees, assertions, uncertainty, and tests.

Nine of those ten are the facts clause 28.2 says the canonical component contract owns; tests is the tenth, and points at fixtures.

The semantic model is a statement of restraint rather than of capability.


{
  "semanticModel": {
    "role": "none",
    "implicitElement": "div",
    "accessibleName": "none",
    "rationale": "A layout primitive cannot know whether its children form a list, a group, a set of landmarks, or unrelated blocks. Only the consumer knows, so Stack adds no ARIA role, no accessible name, and no state.",
    "domOrderIsReadingOrder": true
  }
}

The keyboard contract is declared as absent rather than omitted, and says why.


{
  "keyboardContract": {
    "hasKeyboardContract": false,
    "statement": "Stack has no keyboard contract. It is stated explicitly rather than omitted so that a reviewer cannot mistake absence for oversight."
  }
}

The derivation status is native-first, with deviations empty.


{
  "derivation": {
    "status": "native-first",
    "rationale": "Stack is a flex column on a plain div. No published interaction pattern applies to it, because it has no interaction. The native element and one CSS declaration fully supply the behaviour, so no custom pattern is derived and none is needed.",
    "deviations": [],
    "supportDependent": false
  }
}

guarantees carries six entries, stack-g1 to stack-g6, each naming the assertions that would substantiate it. nonGuarantees carries seven items, and they are the part a developer should read first, because each one is a responsibility that remains theirs.


{
  "nonGuarantees": [
    "Stack does not provide list semantics.",
    "Stack does not provide a grouping role or an accessible name.",
    "Stack does not provide a heading structure or landmark.",
    "Stack does not enforce the measure; that is the Center primitive's responsibility.",
    "Stack does not manage focus, focus order, focus trapping, or focus return.",
    "Stack does not guarantee contrast between any pair of colour tokens.",
    "Stack does not provide a basis for claiming the WCAG 1.4.10 two-dimensional exception."
  ]
}

Seven non-guarantees, so seven responsibilities left with the consumer.

assertions carries six entries, stack-a1 to stack-a6, three automated and three manual. uncertainty carries four records, not two, and the four are worth naming because together they are the honest boundary of the component.

RecordSubjectStatus
stack-u1Screen-reader announcement of a bare grouping containernot-yet-tested
stack-u2Interaction of rem-anchored gaps with operating-system font scaling in Electronnot-yet-tested
stack-u3Reflow of a Stack at 320 CSS pixels of available inline size and at 400% zoomnot-yet-tested
stack-u4Voice-driven targeting of content placed inside a Stacknot-yet-tested

stack-u3 shows what an uncertainty record is for when an assertion already exists.


{
  "id": "stack-u3",
  "subject": "Reflow of a Stack at 320 CSS pixels of available inline size and at 400% zoom",
  "statement": "Whether a Stack clips content or produces a page-level horizontal scrollbar at 320 CSS pixels of available inline size, or at 400% zoom, has not been tested for this sample. Assertion stack-a4 states the expected behaviour but is manual, and no observation has been recorded against it.",
  "status": "not-yet-tested",
  "evidenceRef": "evidence/at-matrix.json#stack"
}

An assertion states what should be true; an uncertainty record states that nobody has looked. The contract carries both, which is the mechanism by which "nobody has checked" becomes visible rather than absent.

The tests field names two fixtures the package does not ship, and says so.


{
  "tests": {
    "isolated": "stories/stack.isolated.md",
    "realisticPage": "stories/stack.in-page.md",
    "note": "This sample does not ship the story fixtures. The paths record where they belong in a complete package and a consumer MUST treat them as absent here."
  }
}

What the evidence file says

evidence/at-matrix.json carries three preamble fields — afdsEvidenceVersion, description, resultVocabulary — and then nine records.

The five-value result vocabulary is carried in the file itself rather than assumed: not-yet-tested, supported, partial, unsupported, not-applicable. The description explains that not-applicable carries two distinct senses, one as a result value and one in any other field, where it means the field does not apply to that record.

Every record has the same shape.


{
  "id": "stack-nvda-chromium",
  "componentId": "stack",
  "assertionRef": [
    "stack-a3"
  ],
  "claim": "The Stack container element itself is not announced as an additional structural object.",
  "engine": "Blink",
  "engineVersion": "not-yet-tested",
  "browser": "Chrome",
  "browserVersion": "not-yet-tested",
  "at": "NVDA",
  "atVersion": "not-yet-tested",
  "platform": "Windows",
  "device": "desktop",
  "startingViewport": "not-applicable",
  "zoom": "not-applicable",
  "date": "not-yet-tested",
  "result": "not-yet-tested",
  "observation": "not-yet-tested",
  "tester": "not-yet-tested",
  "uncertaintyRef": "stack-u1"
}

Two reference fields tie the record into the rest of the package, and their literal names matter: assertionRef is an array naming the assertion or assertions the record evaluates, and uncertaintyRef names the uncertainty record the observation would resolve.

The nine records cover four screen-reader and engine combinations (NVDA/Blink on Windows, JAWS/Blink on Windows, VoiceOver/WebKit on macOS, Orca/Gecko on Linux), one Electron font-scaling record, two Reflow records at a 320 by 640 starting viewport and at 400% zoom, and two voice-control records on Blink and WebKit.

Now do the arithmetic the contract invites. Across the nine records, assertionRef names three of the six assertions — stack-a3, stack-a4, and stack-a5 — so stack-a1, stack-a2, and stack-a6 have no evidence record at all, and every one of the nine records reads "result": "not-yet-tested".

So all six guarantees compute as unsubstantiated, and none of them is written as such anywhere in the package. That is clause 28.2 and clause 14.3 working as intended: the status is computed from the contract and the evidence together, and prohibited from being written into either. The package's own notes array says the same thing in prose, and evidence/known-limitations.md says why: "Fabricated evidence is worse than absent evidence, because absent evidence is visible as a gap while fabricated evidence looks like a guarantee."

What the pattern registry says

patterns/registry.json exists because the package claims afds-patterns-native-first, and clauses 24.2 and 29.4 require it. It carries three entries: one for Stack, with status native-first, and two prohibitions.

The prohibitions are the reason the artefact is canonical rather than derived, and the registry says so itself: "This package contains one component, so the registry is mostly prohibitions. That is the expected shape for a small package and is the reason clause 24.2 requires the artefact at all: a decision not to build something leaves no component behind to declare it."

The menubar entry is worth reading for its shape.


{
  "status": "prohibited",
  "notMisuse": "The ARIA Authoring Practices Guide ships a navigation menubar example demonstrating site navigation, so that use is sanctioned by its publisher. This entry is a convention of this package and is not a claim that the pattern is being misused."
}

That is clause 24.5 respected in a package artefact: the practice is declined as a local convention with a stated cost, and explicitly not described as a misuse.

Rebuilding and verifying the sample

tools/build-inventory.py implements the producer and consumer halves of clause 30 and part of clause 31, in about two hundred lines. It takes three commands.


python3 tools/build-inventory.py build      Regenerate afds-inventory.json.
python3 tools/build-inventory.py verify     Verify afds-inventory.json.
python3 tools/build-inventory.py pack PATH  Write a .afds ZIP to PATH.

Running build walks the source tree, excludes tools/, README.md and the inventory itself, sorts the paths, and writes one record per remaining entry with its media type, byte length, role, and SHA-256 digest. Running verify recomputes everything and reports.


inventory: 10 records, 10 entries digest-checked
boundary: README.md agrees with the exclusions
VERIFY PASSED: every entry is inventoried, lengths and SHA-256 digests match

The checks it performs map onto steps 5 to 8 of clause 31, with one addition of its own. The boundary line has no counterpart in clause 31, because the boundary it checks is not a specification rule; an unpacked archive carries no README.md, so the check reports that it was skipped and the verification still passes. It confirms digestAlgorithm is SHA-256 and excludesSelf is true; it confirms the inventory holds no record for itself; it reports entries present but not inventoried and entries inventoried but not present, in both directions rather than stopping at the first; it compares byteLength, sha256, mediaType and role for every entry; and it confirms entryCount matches the number of records. Reporting both directions separately is clause 31 step 6 taken literally, and gathering every problem before reporting is the second of clause 31's two deliberate properties.

pack refuses to write the archive inside the source tree, then runs the whole verification and refuses to pack if it fails.


FAIL: refusing to pack an unverified source tree

That ordering is the point of the tool: a package is packed from a verified tree or not at all. The eleven entries it writes are the ten inventoried files plus the inventory, and the archive has no enclosing top-level directory, as clause 25.1 requires.

The tool stops short of a full clause 31 consumer, and honestly so. It verifies a source tree rather than opening a .afds archive and verifying it as delivered; it does not apply the clause 32.2 decompression limits, because it is not decompressing untrusted input; and it does not validate the token file against the declared dtcgVersion, which is step 9. For a tester, those three are the gap between this script and a conforming consumer.

One failure mode is worth naming here, because it is the one the inventory cannot catch and the one this sample has already been through. A published archive can fall behind the source tree it was built from. When that happens nothing in the archive is corrupt: its digests still match its own records, so a consumer verifying it reports a pass, and it is entitled to. What has changed is the source, and no digest inside a package can detect that, because the package has no way to refer to something outside itself. A package proves that its bytes are the bytes its inventory describes, and clause 32.3 is careful never to claim more. Detecting the other kind of staleness is a release-process problem rather than a format problem, which is why the packing tool refuses to build from an unverified tree.