This page defines the living-doc entity model: the four entities, their IDs and status vocabulary, the
acceptance-criterion grammar, ID uniqueness and how the entities relate. Every living-doc-* repository
works on this model, and so does the agentic layer in agentic/, whose own
formats and skill matrix are in agentic-formats.md.
The layer reads a copy generated from this page, and a rule changed here changes its example in
docs/examples/ in the same PR.
Read before: Authoring formats · Next: Example corpus
Tracker state is never entity status. A GitHub issue being open or closed, or an Azure DevOps work item's
System.State, says where the work item stands in a tracker. The entity status below is authored in the documentation itself and is the only status a living-doc document renders. A collector records the tracker value as provenance (source_ref.tracker_state) and never derives an entity status from it.
Where status and deprecation metadata are authored.
## Status,## Deprecated At,## Deprecation Reasonand## Superseded Byas headings in an issue body;# status:,# deprecated_at:,# deprecation_reason:and# superseded_by:as keys in a feature-file header;deprecation_reason:andsuperseded_by:as keys in a PageObject full header. Of these only the status is required, and only on a User Story and a Functionality — a Feature has no authored status at all, anddeprecated_atexists on a User Story and a Functionality only — see Feature. See the GitHub issue-body layout and Authoring formats.
## Notes— the one place for human context. Every entity may carry an optional## Notessection in an issue body (# notes:in a source-code header): a bullet list, one note per bullet, at entity level only — never on an acceptance criterion. A note is never parsed for semantics: it drives no state, and nothing is derived from it. Anything that must drive behaviour has to be a typed field, not a note.deprecation_reasonstays its own typed field and does not migrate into a note.
A business-level requirement expressed from the perspective of a named actor.
As a <actor>,
I can <capability>,
so that <business outcome>.
- ID format:
US-<nnn>(e.g.US-001) - Name: short imperative title (e.g. "Customer Login")
- Owns: end-to-end Acceptance Criteria (AC)
- Links to: one or more Features (system surfaces the User Story touches)
- Status: authored, and required —
planned | in_review | active | deprecated(lowercase with underscores per the Project Profileac_states). Written as## Statusin the issue body and# status:in the feature-file header. - Deprecation metadata (optional; authored when
status: deprecated):deprecated_at— date the entity was deprecateddeprecation_reason— why it was deprecatedsuperseded_by— ID of the replacement entity
- Notes (optional): human context as a bullet list — see
## Notes— the one place for human context above
Feature file template: see Authoring formats — User Story in a Gherkin Feature File.
A named system surface — the structural layer between User Stories and atomic behaviors.
- ID format:
FEAT-<nnn>(e.g.FEAT-001) - Name: noun phrase identifying the surface (e.g. "Login Page")
- Surface types:
| Type | Description | Test abstraction |
|---|---|---|
UI |
A web page, modal, or named screen | PageObject design pattern — class encapsulating selectors and user interactions for one screen. Selector preference: getByTestId() (resolves to the Project Profile test_id_attribute, default data-cy) > aria-label/role > CSS class. |
API |
A request/response or event-driven service contract: a REST/GraphQL endpoint (or endpoint group), or a message-broker topic (e.g. Kafka) documented via an AsyncAPI (or equivalent) specification. A backend service or event producer/consumer is documented as an API Feature representing its public contract. | Annotated endpoint method or annotated event handler — for request/response, the endpoint method with its API documentation header (OpenAPI annotation, JSDoc, etc.); for event-driven, the producer/consumer handler with its AsyncAPI (or equivalent schema-registry) annotation. Either serves as the living contract anchor. That anchor carries no living-doc header yet, so a project documented in source code cannot document an API Feature; a project documented in GitHub Issues or in Azure DevOps work items can. |
-
Owns: one or more Functionalities
-
Links to: one or more User Stories
-
owners: team or person responsible for this Feature -
external_dependencies: systems this surface calls that are not Features themselves — they have no canonical test-abstraction anchor, so nothing in the catalog can carry their contract. The Features that call such a system record it here, by name; the name resolves to nothing and nothing validates it. When the system gains an anchor it becomes anAPIFeature in its own right, and the entry leaves everyexternal_dependencieslist that named it. -
feature_dependencies(optional): the Features this surface calls — a list ofFEAT-ids, authored by a person or an agent. Whereexternal_dependenciesnames a system with no anchor, this field names a documentedAPIFeature by id. Any Feature may carry it,UIorAPI— anAPIFeature calling anotherAPIFeature is allowed. Three rules:- The target must be an
APIFeature. You can only call what has a contract anchor. - Only the caller authors it. The reverse direction — who depends on this Feature — is not authored.
- Omit it when the Feature calls nothing. There is no
nonevalue.
Written as
## Feature Dependenciesin the issue body andfeature_dependencies:in the PageObject full header, as comma-separated ids in both forms. A PageObject cross-reference header does not carry it.A target the pipeline cannot resolve is reported as
UNRESOLVED_RELATION. In a project documented in source code that is the expected outcome, not a defect: aUIFeature's PageObject header can name anAPIFeature that such a project cannot document, because the contract anchor carries no living-doc header yet. The field resolves fully in a project documented in GitHub Issues or in Azure DevOps work items (both planned sources). - The target must be an
-
Status: derived from its Functionalities — never authored. A Feature is the structural node that names a visible surface; the behaviour that can be planned, reviewed, shipped or retired lives in its Functionalities and their ACs. A hand-written Feature status can therefore only restate them or contradict them, so there is no place to write one: a Feature issue body carries no
## Statusheading, and a PageObject header carries nostatus:field. The pipeline computes the state and marks itderived. -
Deprecation metadata: a Feature has no
deprecated_at— a Feature issue body carries no## Deprecated Atheading and a PageObject header nodeprecated_at:field. What stays authored on a Feature (optional, and neither one drives the state):deprecation_reason— why the surface is being retired; human intent, which nothing can derivesuperseded_by— ID of the replacement entity; a typed entity link the pipeline traverses as a relation edge
-
Notes (optional): human context as a bullet list — see
## Notes— the one place for human context above
PageObject file header schemas (full header, cross-reference, operational notes, common mistakes): see Authoring formats — Feature in a PageObject File.
An atomic, fast-testable behavior — a single verb phrase describing one responsibility.
- ID format:
FUNC-<nnn>(e.g.FUNC-001) - Name:
<parent Feature name> - <behavior phrase>(e.g. "Login Page - Validate Password Strength") - Belongs to: one parent Feature
- Owns: Functionality-level Acceptance Criteria (atomic input to output statements)
- Test anchor: a Functionality feature file under
features/liv_doc_func/— one file per Functionality, containing all AC-linked system-test scenarios once implemented. File name pattern:func-<nnn>-<feature-name-kebab>-<behavior-kebab>.featuree.g.func-001-authentication-screen-credential-based-login.feature - Status: authored, and required —
planned | in_review | active | deprecated(lowercase with underscores per the Project Profileac_states). Written as## Statusin the issue body and# status:in the feature-file header. - Deprecation metadata (optional; authored when
status: deprecated):deprecated_at— date the entity was deprecateddeprecation_reason— why it was deprecatedsuperseded_by— ID of the replacement entity
- Notes (optional): human context as a bullet list — see
## Notes— the one place for human context above
Functionalities differ from User Story ACs: they are atomic and fast-testable, not end-to-end. A single User Story may trigger multiple Functionalities.
| Dimension | User Story | Functionality |
|---|---|---|
| Perspective | End user observing a business outcome | Developer / component behavior |
| Scope | Full E2E flow across one or more surfaces | Single function, method, or UI behavior |
| AC example | "Order is confirmed and confirmation email is sent" | "Returns discounted total when a valid membership tier is applied" |
| Test type | E2E / integration scenario | Unit or fast system test |
| Trigger question | "Would a product owner write this as a business requirement?" → User Story | "Would a developer write this as a function contract?" → Functionality |
When in doubt: if the behavior is observable only by looking at the code or component output (not by a user clicking through the UI), it is a Functionality. If it describes what a user can do or see across one or more screens, it is a User Story.
If an AC belongs to the wrong entity type, redirect:
- AC too atomic / technical inside a US → move to a Functionality
- AC describes a full user journey inside a FUNC → move to a User Story
Feature file template and
func_typevalues: see Authoring formats — Functionality in a Gherkin Feature File.
A binary pass/fail statement that defines a verifiable condition.
Each AC is:
- Atomic — one input condition, one observable outcome
- Binary — clear pass/fail; no "usually" or "typically"
- Single placeholder — at most ONE
{placeholder-name}per AC statement. If two aspects vary independently, write a separate AC for each.
AC identifier and state format (in file header and entity files):
AC:<parent-id>-<nn> (v<version> - <state>)
- <atomic description, with at most one {placeholder-name} for a variable value>
- <placeholder-name>: value1, value2, ... ← optional; or `Aspect: value1, value2, ...` — one of the two
- Rationale: <business context, policy reference, or design decision> ← optional
<parent-id> is the id of the User Story (US-<nnn>) or Functionality (FUNC-<nnn>) that owns the AC — the only two
entity kinds that own acceptance criteria. A Feature owns Functionalities, not criteria, so AC:FEAT-… is not a valid
AC id; neither is any foreign key such as AC:JIRA-12-01.
States — exactly four, lowercase with underscores (the Project Profile ac_states):
| State | Meaning |
|---|---|
planned |
Agreed, not built yet |
in_review |
Built on a branch, not yet accepted into master |
active |
Accepted, part of the shipped solution |
deprecated |
Shipped behaviour on its way out; carries a removal note |
Version rule — a version is required in every state except planned:
| Form | Meaning |
|---|---|
AC:<id> (v<x.y.z> - active) |
shipped in v<x.y.z> (same shape for in_review) |
AC:<id> (v<x.y.z> - planned) |
planned for the target version v<x.y.z> |
AC:<id> (planned) |
backlog: agreed, no target version yet |
AC:<id> (v<x.y.z> - deprecated - removal planned v<x.y.z>) |
deprecated; the removal note is required on a deprecated AC and valid on no other state |
An AC deferred out of the current scope keeps its planned state and gains no extra fields: drop the
target version so it reads AC:<id> (planned), and record why in the AC's Rationale bullet.
Canonical form and normalisation. The canon uses
-(hyphen-minus) in every structural position — AC headers, AC bullets, entity names and the# AC:comment separator. Dash, letter-case and short-version variants an authoring tool may emit (–/—for-,In Reviewforin_review,v1.1forv1.1.0) are rewritten to the canonical form when a collector reads the input; they are never canon themselves. Every generated document shows the canonical form above.
Variants — an AC that must hold for several values (fields, rules, roles) declares them in one bullet, and its scenarios cover them value by value:
- One variant declaration per AC. Either
- Aspect: <value1>, <value2>(the default keyword; the AC text needs no{placeholder-name}), or one named keyword- <placeholder-name>: <value1>, <value2>, matching the one{placeholder-name}in the AC text. The keyword is the other spelling ofAspect:: the same values, the same coverage, a name that reads better. An AC that declares both, two keywords, orAspect:twice isMALFORMED_AC: the pipeline drops the criterion until the author picks one. - The name is lowercase kebab-case and is written the same in the AC text, the bullet and the
scenario tag:
{field},- field:,@AC:<id>/field:<value>. Names are compared case-insensitively, with-,_and space equal, so- Field:still matches{field}.aspectis reserved. - A bullet is a keyword only when the text names it. Apart from
Aspect:andRationale:, a- <name>:bullet whose name is not a{<name>}in the AC text is not a keyword: it is reported asUNPARSED_AC_LINE. - Coverage is per value, the same for a keyword as for
Aspect:. The AC iscoveredwhen every declared value has a scenario,not_coveredwhen none has, andpartially_coveredotherwise, shown as covered/declared — e.g. 1/3 — followed by the per-value breakdown. A bare@AC:<id>tag covers the whole AC, every value at once — a data-driven scenario, for example; review confirms that it really does. A tag value the AC does not declare isSTALE_AC_REF.
The authoring view of the same rule is Authoring formats — AC variants.
Scenario traceability: living-doc scenarios (US and Functionality feature files) carry two
complementary annotations — a human-readable # AC: comment and a machine-readable @AC: tag:
# AC:US-1-01 (v1.0.0 - active) - customer places an order with a saved payment method
@AC:US-1-01
Scenario: Customer successfully places an order
...When a scenario covers only one value of an AC with variants (see Variants above), encode
the value directly in the @AC: tag using the /param:value param syntax — the param is aspect or
the AC's keyword name, whichever the AC declares — and mirror it in the comment:
# AC:US-1-01 (v1.0.0 - active) - displays {required-field} on login screen | required-field: username input
@AC:US-1-01/required-field:username-input
Scenario: Login form shows the username input field
...Multiple ACs — one comment + tag pair per AC:
# AC:US-1-01 (v1.0.0 - active) - invalid credentials show an error message
# AC:US-1-02 (v1.0.0 - active) - account lockout after 3 failed attempts
@AC:US-1-01
@AC:US-1-02
@Regression
Scenario: User is locked out after repeated failed logins
...Tag format: @AC:<id>[/param:value]
| Param | Purpose | Example |
|---|---|---|
/aspect:<kebab-value> |
Names the declared Aspect: value this scenario covers |
@AC:FUNC-001-01/aspect:minimum-length |
/<placeholder-name>:<kebab-value> |
Names the declared value of the AC's keyword this scenario covers; the param is the keyword's name | @AC:US-1-01/required-field:username-input |
A tag carries at most one variant parameter; a tag with two is MALFORMED_AC, and it is skipped.
- The
# AC:comment is human-readable context: the canonical AC header, then-and the description, plus an optional| <param>: <value>suffix for the value the scenario covers (| aspect: <value>or| <placeholder-name>: <value>). The separator is a hyphen-minus, never an en or em dash. - The
@AC:Cucumber tag is machine-readable: drives script scanning, coverage reports, and sync checks. - US scenarios:
@AC:US-<n>-<nn>(e.g.@AC:US-1-01) - Functionality scenarios:
@AC:FUNC-<nnn>-<nn>(e.g.@AC:FUNC-001-01) - Both annotations are required for living-doc feature files (
feature_dirs.user_storyandfeature_dirs.functionality, defaultsfeatures/liv_doc_us/andfeatures/liv_doc_func/). - Feature files outside the living-doc directories (smoke tests, regression suites, exploratory probes,
tutorial walkthroughs) do not require
@AC:tags. - Tutorial walkthroughs are long-run, feature-based walkthroughs kept for tutorial capture, not living
documentation. They live in one or more folders parallel to the living-doc directories (named
tutorials/ortutorial_<group>/), carry a@tutorialscenario flag, and are out of scope for every collector mode — no collector mines them (a post-v1 roadmap item).
User Story AC examples (in the # Acceptance Criteria: file header block):
AC:US-001-01 (v1.0.0 - active)
- The login screen displays {required-field}.
- required-field: username-input, password-input, login-button
- Rationale: Accessibility standard — all interactive controls must be visible on load.
AC:US-001-02 (v1.1.0 - active)
- An inline field validation message is shown when invalid credentials are submitted.
AC:US-001-03 (v2.1.0 - deprecated - removal planned v3.0.0)
- A "Remember me" checkbox retains the session across browser restarts.
- Rationale: Deprecated due to security policy change in v2.0 — persistent sessions no longer permitted.
Functionality AC examples (in the # Acceptance Criteria: file header block):
AC:FUNC-001-01 (v1.0.0 - active)
- Returns valid=true when the password satisfies all complexity rules.
AC:FUNC-001-02 (v1.0.0 - active)
- Raises INVALID_PASSWORD when the password does not match the stored credential.
- Rationale: Distinct error codes per failure reason, required by the global auth error contract.
AC:FUNC-001-03 (v1.0.0 - active)
- Rejects passwords shorter than 8 characters.
AC:FUNC-001-04 (planned)
- Rejects a password found in the breached-password list.
- Rationale: Backlog — no target version; the breach-feed contract is not agreed yet.
One project, one pipeline, one documentation source. A living-doc pipeline documents exactly one project, and it mines the entities of that project from exactly one documentation source — one collector mode over one configured set of repositories, or one Azure DevOps project. The test catalog may come from elsewhere; the technical project may not.
Within that source, entity IDs (US-, FEAT-, FUNC-) and AC IDs are unique. The same ID
appearing twice is always an input error, reported as DUPLICATE_ENTITY_ID (or DUPLICATE_AC_ID)
listing every occurrence. There is no precedence rule and never will be: the pipeline does not pick a
winner, because either choice silently produces a document that is wrong.
Why: a coverage matrix joins the technical project to the test catalog on these IDs. That join is only meaningful if an ID denotes the same entity everywhere it appears. Colliding IDs cannot be reconciled after mining — the merge either collides records or silently mismatches a scenario to the wrong AC, producing a coverage matrix that is wrong in a way no downstream tool can detect.
Across projects, identity is the pair (project_id, entity_id). Two projects may each own a
US-001; they are different entities because project_id differs, and nothing joins them. Do not
prefix or namespace IDs per source to dodge a collision — a collision inside one project means that
project has two entities claiming one ID, and that is the thing to fix.
This mirrors the coverage-matrix prerequisites in Living Doc Document Types.
The rule is living-doc-utilities' One project, one pipeline, one documentation source.
User Story (US)
└── links to: Feature (FEAT)
└── can depend on: Feature (FEAT, surface_type: API)
| feature_dependencies
└── owns: Functionality (FUNC)
└── owns: Functionality ACs
└── maps to: Functionality feature file (system test)
| <feature_dirs.functionality>/func-<nnn>-<kebab>.feature
| @FUNC_ID tag + @AC:FUNC-nnn-nn tagged scenarios
| └── implemented by: Step Definitions
└── can map to: unit/integration tests
└── owns: User Story ACs (in # Acceptance Criteria: header block)
└── linked via: @AC:US-n-nn tags on Scenarios
└── can map to: E2E BDD Scenarios (<feature_dirs.user_story>/*.feature)
@US_ID tag + @AC:US-n-nn tagged scenarios
└── implemented by: Step Definitions
└── delegates to: PageObjects
└── can map to: API coverage / contract tests