diff --git a/.github/agents/evals/living-doc-bdd-copilot/evals.json b/.github/agents/evals/living-doc-bdd-copilot/evals.json index 350587f..761ac17 100644 --- a/.github/agents/evals/living-doc-bdd-copilot/evals.json +++ b/.github/agents/evals/living-doc-bdd-copilot/evals.json @@ -34,11 +34,12 @@ "id": 3, "category": "happy-path", "prompt": "Generate Gherkin scenarios for US-007 — Place an Online Order. ACs: (1) active — happy path: customer places order with saved payment. (2) active — error: order rejected when card declined.", - "expected_output": "Agent generates a .feature file named us-007-place-an-online-order.feature. Feature header uses the As-a/I-can/so-that narrative from US-007. Two scenarios generated — one per active AC. Each Scenario: is immediately preceded by a '# AC: US-007-0n (v1.0.0 – active) — ...' traceability tag. Step text uses domain language (no HTTP calls, selectors, or DB). For steps without a matching step definition, generates stubs: Case A (PageObject method exists) = full stub; Case B (no PageObject method) = stub with NotImplementedError and flag to extend the PageObject.", + "expected_output": "Agent generates a .feature file named us-007-place-an-online-order.feature. Feature header uses the As-a/I-can/so-that narrative from US-007. Two scenarios generated - one per active AC. Each Scenario: is immediately preceded by a '# AC:US-007-0n (v1.0.0 - active) - ...' comment AND carries the matching '@AC:US-007-0n' tag directly above the Scenario: line — the agent contract requires both the human-readable comment and the machine-readable tag on every scenario. Step text uses domain language (no HTTP calls, selectors, or DB). For steps without a matching step definition, generates stubs: Case A (PageObject method exists) = full stub; Case B (no PageObject method) = stub with NotImplementedError and flag to extend the PageObject.", "files": [], "expectations": [ "Feature file named us-007-place-an-online-order.feature", "Each Scenario: immediately preceded by a # AC: traceability comment", + "Each Scenario: also carries the matching @AC: tag — comment alone is not sufficient", "Skips planned and deprecated ACs — only active ACs drive generation", "Step text in domain language — no implementation details", "Case A stubs delegate to PageObject methods", @@ -160,13 +161,14 @@ "id": 12, "category": "output-format", "prompt": "After scanning /login and generating a LoginPage, show me what the manifest.json entry for /login looks like.", - "expected_output": "The manifest.json entry for /login includes: pageobject_path (path to the generated LoginPage file), feature_id (FEAT- or FEAT-UNKNOWN if unlinked), last_scanned (ISO timestamp), elements (list of discovered elements with data_cy and tag), coverage_gaps (empty list initially), and navigation_context with prerequisites, navigation_steps, data_requirements, auth_role, and notes. The feature_id is FEAT-UNKNOWN if no matching Feature entity exists in the living doc — flag this route as 'needs Feature entity' and load `living-doc-create-feature` skill to create it.", + "expected_output": "The manifest.json entry for /login includes: pageobject_path (path to the generated LoginPage file), feature_id (FEAT- or FEAT-UNKNOWN if unlinked), last_scanned (ISO timestamp), elements (list of discovered elements, each with the normalized test_id key and tag — not data_cy), coverage_gaps (empty list initially), and navigation_context as a prose STRING describing how to reach the route (e.g. prerequisites, navigation steps, auth role) — not an object. The feature_id is FEAT-UNKNOWN if no matching Feature entity exists in the living doc — flag this route as 'needs Feature entity' and load `living-doc-create-feature` skill to create it.", "files": [], "expectations": [ "manifest.json entry has: pageobject_path, feature_id, last_scanned, elements, coverage_gaps, navigation_context", "feature_id is FEAT-UNKNOWN if no matching Feature entity exists", "last_scanned is an ISO timestamp", - "navigation_context includes prerequisites, navigation_steps, auth_role", + "elements use the normalized 'test_id' key, not 'data_cy'", + "navigation_context is a prose string (not an object) describing prerequisites, navigation steps, and auth role", "Missing Feature entity triggers living-doc-create-feature skill load" ] }, @@ -215,7 +217,7 @@ "id": 16, "category": "skill-dispatch", "prompt": "The @AC: traceability tags in checkout.feature are out of sync with the living doc. Sync them.", - "expected_output": "Agent loads the `gherkin-living-doc-sync` skill. Runs scan_ac_links.py to audit the @AC: tags and # AC: comments across checkout.feature. For each scenario: (1) verifies the @AC: tag matches a live AC in the catalog; (2) checks the # AC: comment format is canonical; (3) flags stale or missing links. Produces a sync diff showing what changed. Does not generate new scenarios — routes new-scenario requests to living-doc-scenario-creator.", + "expected_output": "Agent loads the `gherkin-living-doc-sync` skill. Runs scan_ac_links.py to audit the @AC: tags and # AC: comments across checkout.feature. For each scenario: (1) verifies the @AC: tag matches a live AC in the catalog; (2) checks the # AC: comment format is canonical; (3) flags stale or missing links. Produces a sync diff showing what changed. Does not generate new scenarios - routes new-scenario requests to living-doc-scenario-creator.", "files": [], "expectations": [ "Loads gherkin-living-doc-sync skill", @@ -257,11 +259,11 @@ "id": 19, "category": "skill-dispatch", "prompt": "Delete all BDD artifacts linked to the deprecated FEAT-012 feature — clean up feature files, step definitions, and PageObjects.", - "expected_output": "Agent loads the `bdd-maintain` skill in REMOVE mode. Identifies all .feature file scenarios whose # AC: tags reference FEAT-012 User Stories. Runs find_unused_steps.py to find step definitions only used by those scenarios. Runs find_unused_po_methods.py and find_unused_po_components.py to identify exclusively-used PageObject artifacts. Presents the full deletion list to the user for confirmation before touching any file. Also checks fixtures.ts for PageObject imports to remove. After confirmation, removes the identified files.", + "expected_output": "Agent loads the `bdd-maintain` skill in REMOVE mode. Identifies all .feature file scenarios whose @AC: tags reference FEAT-012 User Stories. Runs find_unused_steps.py to find step definitions only used by those scenarios. Runs find_unused_po_methods.py and find_unused_po_components.py to identify exclusively-used PageObject artifacts. Presents the full deletion list to the user for confirmation before touching any file. Also checks fixtures.ts for PageObject imports to remove. After confirmation, removes the identified files.", "files": [], "expectations": [ "Loads bdd-maintain skill", - "REMOVE mode — identifies scenarios via # AC: tags linked to deprecated Feature", + "REMOVE mode - identifies scenarios via @AC: tags linked to deprecated Feature", "Runs all three audit scripts to identify exclusively-used artifacts", "Checks fixtures.ts for PageObject imports", "Presents deletion list for confirmation before touching any file" @@ -271,12 +273,13 @@ "id": 20, "category": "skill-dispatch", "prompt": "Document the Notifications Service as a Feature entity in the living doc — it exposes a REST API at /api/notifications.", - "expected_output": "Agent loads the `living-doc-create-feature` skill. Assigns the next sequential FEAT-nnn ID using next_id.py. Creates a Feature entity JSON with: id, name ('Notifications Service'), type ('api'), route ('/api/notifications'), status ('active'), owners (asks user), and empty functionalities and user_stories arrays. Adds the Feature to feature_registry.json. Prompts: 'Do you want to create Functionality entities for specific behaviors of this service?'", + "expected_output": "Agent loads the `living-doc-create-feature` skill. Assigns the next sequential FEAT-nnn ID using next_id.py. Creates a Feature entity JSON with: id, name ('Notifications Service'), surface_type ('API'), purpose, owners (asks user), external_dependencies, and empty `functionalities`/`user_stories` arrays since the prompt supplies no relationships — no `status` field, since a Feature's state is derived from its Functionalities. Repeats the orphan warning outside the JSON (no traceable business value until linked to a User Story; reported as `ORPHAN_FEATURE` by living-doc-gap-finder) rather than fabricating placeholder IDs. Adds the Feature to feature_registry.json. Prompts: 'Do you want to create Functionality entities for specific behaviors of this service?'", "files": [], "expectations": [ "Loads living-doc-create-feature skill", "Assigns next FEAT-nnn ID using next_id.py", - "Creates Feature entity JSON with required fields", + "Creates Feature entity JSON with required fields and empty functionalities/user_stories arrays — no fabricated placeholder IDs", + "Repeats the ORPHAN_FEATURE warning outside the JSON", "Adds entry to feature_registry.json", "Prompts for Functionality creation as next step" ] @@ -314,12 +317,13 @@ "id": 23, "category": "skill-dispatch", "prompt": "Generate BDD scenarios for US-007 — Place an Online Order. It has 3 active ACs and 1 planned AC.", - "expected_output": "Agent loads the `living-doc-scenario-creator` skill. Generates a .feature file for US-007 with 3 scenarios — one per active AC. The planned AC is skipped (not generated until active). Each scenario is preceded by a '# AC: US-007-0n (v1.0.0 – active)' traceability comment. Merge policy: if a scenario already exists for an AC, applies the 4-row decision table (skip if intent matches, update if GWT is stale, propose replacement if deprecated, flag if multiple scenarios exist per AC).", + "expected_output": "Agent loads the `living-doc-scenario-creator` skill. Generates a .feature file for US-007 with 3 scenarios - one per active AC. The planned AC is skipped (not generated until active). Each scenario is preceded by a '# AC:US-007-0n (v1.0.0 - active)' comment AND carries the matching '@AC:US-007-0n' tag directly above the Scenario: line — both are required. Merge policy: if a scenario already exists for an AC, applies the 4-row decision table (skip if intent matches, update if GWT is stale, propose replacement if deprecated, flag if multiple scenarios exist per AC).", "files": [], "expectations": [ "Loads living-doc-scenario-creator skill", "Generates scenarios only for active ACs — skips planned", "Each scenario preceded by # AC: traceability comment", + "Each scenario also carries the matching @AC: tag — comment alone is not sufficient", "Applies merge policy decision table for existing scenarios", "Output is a named .feature file: us-007-place-an-online-order.feature" ] diff --git a/.github/agents/living-doc-bdd-copilot.agent.md b/.github/agents/living-doc-bdd-copilot.agent.md index 720bf9b..77336d6 100644 --- a/.github/agents/living-doc-bdd-copilot.agent.md +++ b/.github/agents/living-doc-bdd-copilot.agent.md @@ -122,6 +122,7 @@ Load **one** skill per session. Do not pre-load skills for modes not yet trigger | Create Functionality (atomic behavior) | `living-doc-create-functionality` | | Update / deprecate entity or AC | `living-doc-update` | | Promote entity to ACTIVE | `living-doc-update` | +| Change a Feature's dependencies — add, drop, or promote an external dependency to a Feature | `living-doc-update` (it hands the new Feature's creation to `living-doc-create-feature`) | | PR impact analysis / trace affected entities | `living-doc-impact-analysis` | | Catalog gaps / AUDIT mode / PLAN mode | `living-doc-gap-finder` | @@ -178,7 +179,7 @@ When a User Story or Feature is deprecated, three skills fire in sequence. Compl | Step | Skill | Action | |---|---|---| -| 1 | `living-doc-update` | Set entity `status: deprecated`; add `deprecated_at`, `deprecation_reason`, and optionally `superseded_by` | +| 1 | `living-doc-update` | For a User Story, set `status: deprecated`. A Feature has no `status` field — its state is derived from its Functionalities; deprecate the Feature by deprecating every Functionality it owns instead. A User Story or Functionality gets `deprecated_at`, `deprecation_reason`, and optionally `superseded_by`; the Feature itself gets only `deprecation_reason` and optionally `superseded_by` — its `deprecated_at` is derived with its state, never authored | | 2 | `gherkin-living-doc-sync` | Find all scenarios tagged `@AC:` for the deprecated entity's ACs; add `@deprecated` and `@review-needed` | | 3 | `bdd-maintain` (REMOVE) | Confirm file deletion list with user; remove confirmed `.feature` files, PageObjects, and step definitions; update `manifest.json` | @@ -186,7 +187,7 @@ Do not skip steps or run them out of order. Complete catalog changes (step 1) be **Manifest loading rule:** Use targeted line ranges for the current route(s). Load full manifest only for RE-SCAN. `seed.yaml`: always load in full. When PageObject generation discovers a route with no linked Feature entity, set `feature_id: FEAT-UNKNOWN`, flag the route as needing a Feature entity, and cross-load `living-doc-create-feature` to create it before continuing. -**living-doc-bdd-schemas:** Load [skills/shared/references/living-doc-bdd-schemas.md](skills/shared/references/living-doc-bdd-schemas.md) only when generating or validating feature file headers, PageObject headers, ExplorationFixture entries, seed.yaml form_fixtures, or manifest.json route entries. +**living-doc-bdd-schemas:** Load [skills/shared/references/living-doc-bdd-schemas.md](skills/shared/references/living-doc-bdd-schemas.md) only when generating or validating feature file headers, PageObject headers, seed.yaml form_fixtures, or manifest.json route entries. --- @@ -275,12 +276,12 @@ Full model: [living-doc-glossary](skills/shared/references/living-doc-glossary.m **Entity IDs:** `US-` · `FEAT-` · `FUNC-` -**AC reference format:** `AC:- (v – ) — ` +**AC reference format:** `AC:- (v - ) - `. A backlog AC with no target version yet uses `(planned)` instead of a version. A `deprecated` AC requires a removal note: `(v - deprecated - removal planned v)`. State: `planned | in_review | active | deprecated` **Gherkin traceability:** every scenario in the living-doc feature directories (`feature_dirs.user_story` and `feature_dirs.functionality` from the Project Profile, defaults `features/liv_doc_us/` and `features/liv_doc_func/`) requires: ```gherkin -# AC:US-1-01 (v1.0.0 - active) — +# AC:US-1-01 (v1.0.0 - active) - @AC:US-1-01 Scenario: ... ``` @@ -288,7 +289,7 @@ Aspect variant: `@AC:US-1-01/aspect:username-input`. The `@AC:` tag is the singl **Surface types:** `UI` → PageObject (locators via `getByTestId()`, resolving to the profile `test_id_attribute`, default `data-cy`). `API` → contract test layer only. -**ACTIVE ACs** drive scenario generation. DEPRECATED ACs require `deprecated_at`, `deprecation_reason`, optionally `superseded_by`. +**ACTIVE ACs** drive scenario generation. A DEPRECATED AC carries the removal note above and no other deprecation fields — `deprecated_at`, `deprecation_reason`, and `superseded_by` belong to the owning User Story or Functionality when the entity itself is deprecated. **Catalog layer healing boundary:** catalog changes (AC states, traceability links, entity deprecation) and automation changes (PageObjects, step definitions, Gherkin files) are separate steps — complete catalog changes before moving to automation updates in the same session. @@ -303,7 +304,7 @@ Aspect variant: `@AC:US-1-01/aspect:username-input`. The `@AC:` tag is the singl | `living-doc-create-user-story` | Create US with business-level ACs | `skills/living-doc-create-user-story/SKILL.md` | New US or narrative request | | `living-doc-create-feature` | Document a system surface | `skills/living-doc-create-feature/SKILL.md` | New Feature or inbound surface from EXPLORE mode | | `living-doc-create-functionality` | Define an atomic, testable behaviour | `skills/living-doc-create-functionality/SKILL.md` | New Functionality or atomic-behaviour AC request | -| `living-doc-update` | Amend or deprecate entities | `skills/living-doc-update/SKILL.md` | Updating, promoting, or deprecating an entity or AC | +| `living-doc-update` | Amend or deprecate entities | `skills/living-doc-update/SKILL.md` | Updating, promoting, or deprecating an entity or AC; changing a Feature's dependencies | | `living-doc-impact-analysis` | Trace which entities a code change affects | `skills/living-doc-impact-analysis/SKILL.md` | PR review or change-trace request | | `living-doc-gap-finder` | Find catalog gaps (top-down) and uncovered ACs (bottom-up) | `skills/living-doc-gap-finder/SKILL.md` | HEALING mode, gap audit, or scenario gap detection | diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2a5fa56..6dbb966 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,6 +8,22 @@ See [Token-Saving Skill](../docs/guides/token-saving.md) for details, including --- +## BDD Artifacts + +When working with living documentation and BDD automation, use these standard artifact locations: +- **Business Seed:** `.copilot/bdd/seed.yaml` — App configuration, routes, test users, form fixtures +- **Exploration Manifest:** `.copilot/bdd/manifest.json` — Discovered surfaces, PageObject paths, UI elements +- **Project Profile:** `.copilot/bdd/.project-profile.yaml` — BDD conventions for this project +- **Feature Files (User Story):** `features/liv_doc_us/` — E2E scenarios linked to User Stories +- **Feature Files (Functionality):** `features/liv_doc_func/` — System-test scenarios linked to Functionalities +- **PageObjects:** `playwright/pages/` — Locators and page interaction methods +- **Step Definitions:** `playwright/steps/` — Gherkin step implementations +- **Living Doc Catalog:** `docs/living-doc/` — User Stories, Features, Functionalities, and Acceptance Criteria + +These paths are configurable in `.copilot/bdd/.project-profile.yaml`. + +--- + ## PR Review When reviewing a pull request, load and apply: diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index 671f537..d3da9b9 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -4,12 +4,72 @@ on: pull_request: paths: - "skills/**/*.py" + - "skills/shared/references/schemas/*.schema.json" + - "scripts/**" + - "skills/shared/references/living-doc-glossary.md" + - "skills/shared/references/living-doc-bdd-schemas.md" - ".github/workflows/test-scripts.yml" permissions: contents: read jobs: + canon-sync-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Every canon copy under skills/shared/references/ is generated by + # scripts/sync-living-doc-reference.sh and none is hand-maintained. For each target the + # script declares, re-read the SHA its committed copy pins, re-run the sync against that + # SHA, and fail on any difference — a hand edit to either copy cannot survive this. + - name: Re-run the sync for every target and diff against the committed copies + run: | + set -euo pipefail + + failures=() + while IFS=$'\t' read -r target source_path dest; do + echo "::group::${target} → ${dest}" + + header="$(head -n1 "$dest")" + ref="$(echo "$header" | sed -nE 's/.*synced from AbsaOSS\/living-doc@([^ ]+).*/\1/p')" + if [ -z "$ref" ]; then + echo "Error: no 'synced from AbsaOSS/living-doc@' header comment on line 1 of ${dest}." >&2 + failures+=("$dest (missing provenance header)") + echo "::endgroup::" + continue + fi + + # The pinned ref must be an immutable 40-character commit SHA. A branch name moves + # underneath the copy, so re-running the sync against it proves nothing. + if ! printf '%s' "$ref" | grep -qE '^[0-9a-f]{40}$'; then + echo "Error: ${dest} pins '${ref}', which is not a 40-character commit SHA." >&2 + echo "Re-sync against an immutable SHA: scripts/sync-living-doc-reference.sh ${target} " >&2 + failures+=("$dest (ref '${ref}' is not a 40-char SHA)") + echo "::endgroup::" + continue + fi + + cp "$dest" "/tmp/committed-${target}.md" + bash scripts/sync-living-doc-reference.sh "$target" "$ref" + if ! diff -u "/tmp/committed-${target}.md" "$dest"; then + echo + echo "${dest} is out of sync with AbsaOSS/living-doc@${ref} (${source_path})." >&2 + echo "Run: scripts/sync-living-doc-reference.sh ${target} ${ref}" >&2 + failures+=("$dest (out of sync)") + fi + echo "::endgroup::" + done < <(bash scripts/sync-living-doc-reference.sh --list) + + echo + if [ "${#failures[@]}" -ne 0 ]; then + echo "Canon sync check FAILED:" + printf ' %s\n' "${failures[@]}" + exit 1 + fi + echo "All canon copies match their pinned living-doc SHA." + run-tests: runs-on: ubuntu-latest steps: @@ -22,7 +82,7 @@ jobs: python-version: "3.11" - name: Install dependencies - run: pip install pyyaml + run: pip install pyyaml jsonschema - name: Discover and run test_*.py scripts env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7db30e8..ad0ce43 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,22 @@ skills/ - **`skills/shared/references/`** — shared docs used by multiple skills (e.g., schemas, glossaries, common API specs) If a reference file is used by 2+ skills, place it in `skills/shared/references/` instead of duplicating it. See [living-doc-bdd-schemas.md](skills/shared/references/living-doc-bdd-schemas.md) and [living-doc-glossary.md](skills/shared/references/living-doc-glossary.md) for examples. + +> **Generated canon copies — do not edit by hand.** Both +> `skills/shared/references/living-doc-glossary.md` and +> `skills/shared/references/living-doc-bdd-schemas.md` mirror canon documents in +> [`AbsaOSS/living-doc`](https://github.com/AbsaOSS/living-doc). They are written by +> `scripts/sync-living-doc-reference.sh`, each pins the canon commit SHA it was generated from in +> a line-1 provenance comment, and `test-scripts.yml` re-runs the sync for every target and fails +> on any diff. To pick up a canon change, re-run the sync against the new SHA: +> +> ```bash +> scripts/sync-living-doc-reference.sh --list # targets and their canon paths +> scripts/sync-living-doc-reference.sh all # refresh every copy +> ``` +> +> Toolkit-local content that has to appear inside a generated copy (for example the pointer to +> `schemas/`) lives in the script's `local_addendum` function, not in the copy itself. --- ## 2. Frontmatter schema diff --git a/docs/guides/agent-design.md b/docs/guides/agent-design.md index 538ec16..7a2baff 100644 --- a/docs/guides/agent-design.md +++ b/docs/guides/agent-design.md @@ -234,12 +234,12 @@ IDs are stable — never change an ID after creation. Bump the `version` field f ### Gherkin traceability tag format ```gherkin -# AC:US-007-01 (v1.0.0 - active) — +# AC:US-007-01 (v1.0.0 - active) - @AC:US-007-01 Scenario: ... ``` -One `# AC:` + `@AC:` pair per AC. The `@AC:` tag is the machine-readable traceability anchor — never delete or rename it without syncing the catalog entity. +One `# AC:` + `@AC:` pair per AC. The `@AC:` tag is the machine-readable traceability anchor - never delete or rename it without syncing the catalog entity. ### Cooperating agent boundary diff --git a/docs/guides/data-cy-instrument.md b/docs/guides/data-cy-instrument.md index 9869a1b..1860feb 100644 --- a/docs/guides/data-cy-instrument.md +++ b/docs/guides/data-cy-instrument.md @@ -54,7 +54,7 @@ coverage gaps ## Testing Evals -This skill has been validated with **11 test cases** covering: +This skill has been validated with **12 test cases** covering: - Gap audit workflows - Angular/React/Vue template instrumentation - PageObject selector updates diff --git a/docs/guides/gherkin-living-doc-sync.md b/docs/guides/gherkin-living-doc-sync.md index d826cb8..f423952 100644 --- a/docs/guides/gherkin-living-doc-sync.md +++ b/docs/guides/gherkin-living-doc-sync.md @@ -12,7 +12,7 @@ The skill audits traceability and produces sync reports: |--------|-------------| | **AC link audit** | Finds missing `@AC:` tags, stale links, orphaned scenarios | | **Step text drift** | Flags scenarios where step text diverged from AC descriptions | -| **Deprecation tagging** | Tags scenarios `@deprecated` when ACs are descoped | +| **Deprecation / descope tagging** | Tags scenarios `@deprecated` + `@review-needed` when their AC is deprecated, or `@wip` + `@review-needed` when their AC is descoped back to `planned` | | **AC change propagation** | Maps AC edits back to affected scenarios | --- diff --git a/docs/guides/living-doc-create-feature.md b/docs/guides/living-doc-create-feature.md index 2908db4..1194964 100644 --- a/docs/guides/living-doc-create-feature.md +++ b/docs/guides/living-doc-create-feature.md @@ -1,6 +1,6 @@ # Living Doc Create Feature Skill -The `living-doc-create-feature` skill helps you document a system surface (UI screen, API endpoint, worker, module) as a Feature entity. It establishes ownership, enables impact analysis, and links User Stories to the surfaces they exercise. +The `living-doc-create-feature` skill helps you document a system surface (UI screen or API endpoint, including a backend service's public contract) as a Feature entity. It establishes ownership, enables impact analysis, and links User Stories to the surfaces they exercise. --- @@ -34,12 +34,14 @@ feature dependencies ## Feature types -| Type | Example | -|------|---------| -| **UI Screen** | Login, Dashboard, Checkout | -| **API Endpoint** | POST /orders, GET /users/:id | -| **Worker/Service** | Order processor, Email sender | -| **Module** | Payment library, Auth service | +Only two surface types exist — each requires the matching test abstraction to actually exist for this surface: + +| Type | Example | Eventual test-abstraction anchor | +|------|---------|------------------| +| **UI** | Login, Dashboard, Checkout | A PageObject for the screen (create together with the Feature if it doesn't exist yet, e.g. via `living-doc-pageobject-scan`) | +| **API** | POST /orders, GET /users/:id, including a backend service's public contract (REST/GraphQL or an annotated message-broker contract) | An annotated endpoint method, or an annotated event handler | + +A worker, module, or service with neither anchor achievable is not a Feature yet — record it as an `external_dependencies` entry on the Feature(s) that interact with it. See [living-doc-glossary](../../skills/shared/references/living-doc-glossary.md) for details. --- @@ -63,7 +65,7 @@ One Python utility available in `skills/living-doc-create-feature/scripts/`: This skill has been validated with **17 test cases** covering: - Feature entity creation -- Multiple Feature types (UI, API, service, module) +- Feature types (UI, API) - Ownership and dependency tracking - Feature Registry integration - User Story linkage diff --git a/docs/guides/living-doc-create-user-story.md b/docs/guides/living-doc-create-user-story.md index 86efb21..6339ba2 100644 --- a/docs/guides/living-doc-create-user-story.md +++ b/docs/guides/living-doc-create-user-story.md @@ -13,7 +13,7 @@ The skill produces a complete User Story: | **Narrative** | As-a/I-can/so-that structure with business context | | **ACs** | Well-formed acceptance criteria | | **Feature links** | Which system surfaces this User Story exercises | -| **Status** | draft → ready → in_review → deprecated progression | +| **Status** | planned → in_review → active → deprecated progression | | **Validation** | Checks narrative and AC clarity | --- @@ -73,4 +73,4 @@ This skill has been validated with **18 test cases** covering: - Narrative elicitation (As-a/I-can/so-that) - Acceptance Criteria definition and validation - Feature linkage workflows -- Status progression (draft → ready) +- Status progression (planned → active) diff --git a/docs/guides/living-doc-scenario-creator.md b/docs/guides/living-doc-scenario-creator.md index 1e0a81b..956d311 100644 --- a/docs/guides/living-doc-scenario-creator.md +++ b/docs/guides/living-doc-scenario-creator.md @@ -59,7 +59,7 @@ One Python utility available in `skills/living-doc-scenario-creator/scripts/`: ## Testing Evals -This skill has been validated with **21 test cases** covering: +This skill has been validated with **22 test cases** covering: - Entity mode: US and Functionality inputs - Standalone mode: free-form scenarios - Scenario Outline generation for variations diff --git a/docs/guides/living-doc-update.md b/docs/guides/living-doc-update.md index 41410dc..f93edaa 100644 --- a/docs/guides/living-doc-update.md +++ b/docs/guides/living-doc-update.md @@ -11,7 +11,7 @@ The skill updates and validates entity changes: | Operation | Effect | |-----------|--------| | **Add/modify/remove AC** | Version tracked; may trigger scenario sync | -| **Change status** | draft → ready → in_review → deprecated | +| **Change status** | planned → in_review → active → deprecated (User Story / Functionality only — a Feature has no status field) | | **Change ownership** | Update team responsible for entity | | **Link entities** | Connect User Stories to Features, Functionalities to Features | | **Deprecate entity** | Tag dependent scenarios `@deprecated`; cleanup triggered | @@ -40,9 +40,9 @@ change entity status |------|------------------| | **Add AC** | None — create scenarios via [Living Doc Scenario Creator](./living-doc-scenario-creator.md) | | **Modify AC** | [Gherkin ↔ Living Doc Sync](./gherkin-living-doc-sync.md) to sync scenarios | -| **Descope AC** | Gherkin sync tags scenarios `@deprecated` | +| **Descope AC** | AC drops back to `planned` (no `descoped` state); Gherkin sync tags scenarios `@wip` + `@review-needed` | | **Deprecate entity** | [BDD Maintain](./bdd-maintain.md) removes test files | -| **Change status to ready** | Scenario creation enabled | +| **Change status to active** | Scenario creation enabled | --- @@ -66,7 +66,7 @@ One Python utility available in `skills/living-doc-update/scripts/`: This skill has been validated with **16 test cases** covering: - Adding/modifying/removing ACs -- Status transitions (draft → ready → deprecated) -- Ownership changes and notifications +- Status transitions (planned → in_review → active → deprecated) +- Ownership changes and the in-flight User Story handoff list - Entity linking workflows - Propagation to downstream skills diff --git a/scripts/sync-living-doc-reference.sh b/scripts/sync-living-doc-reference.sh new file mode 100755 index 0000000..28bb129 --- /dev/null +++ b/scripts/sync-living-doc-reference.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# sync-living-doc-reference.sh — pull a canon reference document from AbsaOSS/living-doc and +# write the local copy that agentic-toolkit's skills load. +# +# One script, one copy of the fetch / link-rewrite / provenance-header logic, driven by the +# TARGETS table below. Every file under skills/shared/references/ that mirrors living-doc canon +# is generated by this script and verified in CI (.github/workflows/test-scripts.yml) — none of +# them is hand-maintained, and a hand edit fails the build. +# +# Both canon sources live at living-doc's docs/guides/ today. A later living-doc change relocates +# guides under docs/reference/ — when that lands, update the source paths in TARGETS below (and +# the link-rewriting rules, which assume each source and its cross-references live under +# docs/guides/ and docs/examples/). The relocation affects BOTH targets, not just one. +# +# Usage: +# scripts/sync-living-doc-reference.sh +# scripts/sync-living-doc-reference.sh all +# scripts/sync-living-doc-reference.sh --list +# +# is a key in TARGETS below (glossary | bdd-schemas), or "all" for every target. +# is any git ref in AbsaOSS/living-doc: a commit SHA (required for a committed +# copy, see the 40-hex check in CI), a tag, or a branch name. +# +# Exits 1 if the target or ref is missing, the fetch fails, or the fetched file is empty. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIVING_DOC_REPO="AbsaOSS/living-doc" +REFERENCES_DIR="skills/shared/references" + +# ── Target table: canon path → local path ──────────────────────────────────── +# Each row is "||". +# A plain indexed array rather than an associative one: macOS ships bash 3.2, which has no +# `declare -A`, and contributors run this script locally as well as in CI. +TARGETS=( + "glossary|docs/guides/living-doc-glossary.md|${REFERENCES_DIR}/living-doc-glossary.md" + "bdd-schemas|docs/guides/living-doc-header-types.md|${REFERENCES_DIR}/living-doc-bdd-schemas.md" +) + +target_names() { + local row + for row in "${TARGETS[@]}"; do + printf '%s\n' "${row%%|*}" + done +} + +# Echo the "|" pair for a target name, or nothing if it is unknown. +target_row() { + local row name="$1" + for row in "${TARGETS[@]}"; do + if [ "${row%%|*}" = "${name}" ]; then + printf '%s' "${row#*|}" + return 0 + fi + done + return 1 +} + +# ── Toolkit-local addenda ──────────────────────────────────────────────────── +# Content that is NOT canon but that the local copy needs — pointers to files that exist only +# in this repo. Emitted verbatim at the end of the generated file, so it is reproduced byte for +# byte on every re-sync and CI's diff stays clean. Keep this strictly to toolkit-local pointers: +# anything that restates canon belongs upstream in living-doc, not here. +local_addendum() { + case "$1" in + bdd-schemas) + cat <<'ADDENDUM' + +--- + +## Toolkit-local additions + +*Not part of the canon above — machine-readable contracts that live only in `agentic-toolkit`.* + +**Schema reference:** for JSON schema validation, see the [schemas/](./schemas/) directory: + +- [project-profile.schema.json](./schemas/project-profile.schema.json) — Project Profile validation +- [seed.schema.json](./schemas/seed.schema.json) — seed.yaml validation +- [manifest.schema.json](./schemas/manifest.schema.json) — manifest.json validation +ADDENDUM + ;; + *) : ;; + esac +} + +usage() { + echo "Usage: $0 " >&2 + echo " $0 all " >&2 + echo " $0 --list" >&2 + echo " : one of: $(target_names | tr '\n' ' ')| all" >&2 + echo " : a commit SHA, tag, or branch in ${LIVING_DOC_REPO}" >&2 +} + +if [ "${1:-}" = "--list" ]; then + printf '%s\n' "${TARGETS[@]}" | tr '|' '\t' + exit 0 +fi + +if [ "${1:-}" = "" ] || [ "${2:-}" = "" ]; then + usage + exit 1 +fi + +TARGET="$1" +REF="$2" + +if [ "${TARGET}" != "all" ] && ! target_row "${TARGET}" >/dev/null; then + echo "Error: unknown target '${TARGET}'" >&2 + usage + exit 1 +fi + +# Percent-encode REF for safe use inside a URL path segment, preserving '/' so refs that +# contain one (e.g. "feature/foo") still address the right nested path. REF is attacker/PR +# controlled (any branch, tag, or SHA in AbsaOSS/living-doc) and is dropped unescaped into +# both the curl fetch URL and the doc links rewritten below. Reserved URL characters such as +# '#' are delimiters — curl (and a browser, for the links below) treats everything from '#' +# onward as a fragment and never sends it — so a ref containing one silently truncates the +# request instead of failing loudly. Percent-encoding leaves only unreserved characters and +# '/' in the result, so the sed replacements further down need no separate escaping either. +url_encode_ref() { + local LC_ALL=C ref="$1" i c encoded="" + for (( i = 0; i < ${#ref}; i++ )); do + c="${ref:i:1}" + case "$c" in + [a-zA-Z0-9._~-]|/) encoded+="$c" ;; + *) encoded+=$(printf '%%%02X' "'$c") ;; + esac + done + printf '%s' "$encoded" +} + +REF_URL_SAFE="$(url_encode_ref "${REF}")" + +if ! [[ "${REF}" =~ ^[0-9a-f]{40}$ ]]; then + echo "Warning: '${REF}' is not a 40-character commit SHA. A committed copy must pin an" >&2 + echo "immutable SHA — a branch name moves underneath the copy and can never be verified." >&2 + echo "CI rejects any committed reference file whose header pins a non-SHA ref." >&2 +fi + +TMP_FILE="$(mktemp)" +trap 'rm -f "${TMP_FILE}" "${TMP_FILE}.rewritten"' EXIT + +sync_one() { + local target="$1" row source_path dest_rel dest raw_url blob_base + row="$(target_row "${target}")" + source_path="${row%%|*}" + dest_rel="${row#*|}" + dest="${REPO_ROOT}/${dest_rel}" + + raw_url="https://raw.githubusercontent.com/${LIVING_DOC_REPO}/${REF_URL_SAFE}/${source_path}" + + echo "Fetching ${source_path} from ${LIVING_DOC_REPO}@${REF} ..." >&2 + if ! curl -fsSL --connect-timeout 10 --max-time 30 --retry 2 "${raw_url}" -o "${TMP_FILE}"; then + echo "Error: failed to fetch ${raw_url}" >&2 + echo "Check that the ref exists and ${source_path} is still the canon path for '${target}'." >&2 + exit 1 + fi + + if [ ! -s "${TMP_FILE}" ]; then + echo "Error: fetched file is empty — refusing to overwrite ${dest_rel}" >&2 + exit 1 + fi + + # Rewrite the canon's relative links to absolute living-doc URLs so the synced copy reads + # correctly from inside agentic-toolkit. Same-page anchors (#section) are left untouched, as + # are already-absolute links (http/https) — none of the patterns below can match those, since + # each requires "(" to be followed immediately by a slash-free, colon-free relative segment. + # Handled shapes, generic rather than a fixed filename list so a newly-added sibling doc or + # example asset in the upstream source needs no change here: + # - "(../foo)" — parent-relative, resolves under docs/ (each source lives in docs/guides/) + # - "(./foo)" — explicit same-dir relative, resolves under docs/guides/ + # - "(foo.md)" — bare sibling filename (no path separator), resolves under docs/guides/ + # + # REF_URL_SAFE is already percent-encoded (see above), so it contains only unreserved URL + # characters and '/' — none of which are special to sed's s|...|...| syntax — and can be + # dropped straight into the replacement text below with no separate escaping pass. + blob_base="https://github.com/${LIVING_DOC_REPO}/blob/${REF_URL_SAFE}" + sed -E \ + -e "s|\(\.\./([^)]+)\)|(${blob_base}/docs/\1)|g" \ + -e "s|\(\./([^)]+)\)|(${blob_base}/docs/guides/\1)|g" \ + -e "s|\(([A-Za-z0-9_.-]+\.md)(#[^)]*)?\)|(${blob_base}/docs/guides/\1\2)|g" \ + "${TMP_FILE}" > "${TMP_FILE}.rewritten" + mv "${TMP_FILE}.rewritten" "${TMP_FILE}" + + { + echo "" + echo "" + echo + cat "${TMP_FILE}" + local_addendum "${target}" + } > "${dest}" + + echo "Wrote $(wc -l < "${dest}") lines to ${dest_rel}" >&2 +} + +if [ "${TARGET}" = "all" ]; then + for t in $(target_names); do + sync_one "$t" + done +else + sync_one "${TARGET}" +fi diff --git a/skills/bdd-maintain/SKILL.md b/skills/bdd-maintain/SKILL.md index 7d5a59c..d1f823f 100644 --- a/skills/bdd-maintain/SKILL.md +++ b/skills/bdd-maintain/SKILL.md @@ -117,7 +117,7 @@ All three scripts exit `0` on clean, `1` on findings, `2` on bad arguments — s **Recommended script order for a full audit:** run in the sequence steps → PO methods → PO components. Deleting unused steps can expose unused PO methods; deleting unused PO methods can then expose unused PO classes. Running in this order ensures each pass builds on the previous one rather than missing transitively dead code. **Unused step def — distinguish before deleting:** -- If the step belongs to a **deprecated entity** (the US/Feature has `status: deprecated` in the catalog), delete it — the coverage it provided is no longer needed. +- If the step belongs to a **deprecated entity** — a User Story with `status: deprecated`, a Functionality with `status: deprecated`, or a Feature whose Functionalities are all deprecated (a Feature carries no `status` field itself) — delete it, the coverage it provided is no longer needed. - If the step belongs to an **active entity** but has no exercising scenario, it is a stale draft or an orphan; flag it for team review before deleting. Someone may be about to add a scenario for it. - Never delete without first verifying the step is not imported or re-exported by another step file — grep for the step file name as an import target as well. diff --git a/skills/data-cy-instrument/SKILL.md b/skills/data-cy-instrument/SKILL.md index c12ebd8..c198dc1 100644 --- a/skills/data-cy-instrument/SKILL.md +++ b/skills/data-cy-instrument/SKILL.md @@ -204,14 +204,13 @@ When answering a locator-conversion question, explicitly say: replace the old lo If all four checks pass and the gap remains, add or update a `WORK_LOG.md` §4 row so the remaining blocker is tracked. **Update PageObject header comments:** -- Change `status: candidate` → `status: active` if all locators for the page are now resolved. -- Remove `stub-reason:` line if no un-instrumented elements remain. +- Remove the `stub-reason:` line if no un-instrumented elements remain — a PageObject header carries no `status:` field to flip; the surface is instrumented once `stub-reason:` is gone. --- ## Phase 6 · Living Doc Promotion -For each Functionality whose `status: planned` was solely due to missing `data-cy`, act only after **Instrument** (template instrumentation) and **Sync** (PageObject update) are complete. +For each Functionality whose `status: planned` was solely due to missing `data-cy`, act only after **Instrument** (template instrumentation) and **Sync** (PageObject update) are complete. Resolving the `data-cy` gap removes one blocker but is not by itself sufficient to promote — `living-doc-update` still requires every AC on the Functionality to have a passing `test_coverage` entry (see `living-doc-update/SKILL.md` § Promote a Functionality from planned to active) before the status actually flips. 1. Open `/func-{NNN}-*.feature` (e.g. `aul-ui/playwright/features/liv_doc_func/`). 2. Change `# status: planned` → `# status: active` in the comment header. @@ -220,9 +219,9 @@ For each Functionality whose `status: planned` was solely due to missing `data-c Only promote if the data-cy attributes required by that Functionality's ACs were all added during **Instrument**. If a Functionality depends on multiple elements and only some were instrumented, leave it as `planned` and add a comment listing the remaining blockers. -Primary downstream action: `living-doc-update` changes the matching catalog entity from `planned` to `active`. If the task also updates the BDD feature-file header, keep it in sync. For promotion questions, answer in routing form: after Instrument and Sync, load `living-doc-update`. Do **not** lead with manual feature-file edits. +Primary downstream action: `living-doc-update` changes the matching catalog entity from `planned` to `active`, provided its own promotion gate is satisfied (every AC has a passing `test_coverage` entry) — clearing the `data-cy` gap is necessary but not sufficient on its own. If the task also updates the BDD feature-file header, keep it in sync. For promotion questions, answer in routing form: after Instrument and Sync, load `living-doc-update`. Do **not** lead with manual feature-file edits. -Preferred promotion wording: `After Instrument and Sync complete, invoke living-doc-update and change FUNC-001 status from 'planned' to 'active'.` +Preferred promotion wording: `After Instrument and Sync complete, invoke living-doc-update, which changes FUNC-001 status from 'planned' to 'active' only once every AC has a passing test_coverage entry.` --- @@ -268,7 +267,7 @@ Report the following at the end of the run: | `living-doc-pageobject-scan` | Upstream — produces `manifest.json` with `coverage_gaps` and may leave `⚠️ PROPOSED` locator comments when test-id attributes are missing. `data-cy-instrument` consumes both signals, adds the attributes to templates, and updates PageObjects to use `getByTestId()`. | | `living-doc-pageobject-scan` RE-SCAN scope | Upstream — re-generates `coverage_gaps` after a UI change. Trigger this skill after RE-SCAN if new gaps appear. | | `living-doc-scenario-creator` | Downstream — after Functionalities are promoted from `planned` to `active`, generate Gherkin scenarios for them. | -| `living-doc-update` | Downstream — if PageObject header `status` changes, the corresponding Feature entity in the living doc may also need a status update. | +| `living-doc-update` | Downstream — after `stub-reason:` is removed from a PageObject header, `living-doc-update` promotes the matching Functionality from `planned` to `active` once its own promotion gate is also satisfied (every AC has a passing `test_coverage` entry — a Feature has no status field to update). | When describing the relationship, state it in this order: `living-doc-pageobject-scan` is upstream, `data-cy-instrument` resolves missing test-id gaps and `⚠️ PROPOSED` locators, and `living-doc-scenario-creator` is downstream and uses the stable locators. diff --git a/skills/data-cy-instrument/evals/evals.json b/skills/data-cy-instrument/evals/evals.json index adba856..4fec94b 100644 --- a/skills/data-cy-instrument/evals/evals.json +++ b/skills/data-cy-instrument/evals/evals.json @@ -54,11 +54,12 @@ "id": 5, "category": "happy-path", "prompt": "FUNC-001 has status 'planned' because the data-cy attributes are missing. After Instrument and Sync are complete, what do I do to promote it?", - "expected_output": "After data-cy attributes are added and PageObjects updated: load living-doc-update and change FUNC-001 status from 'planned' to 'active'. This promotion is the downstream step — data-cy-instrument invokes living-doc-update for the status change.", + "expected_output": "After data-cy attributes are added and PageObjects updated: load living-doc-update, which changes FUNC-001 status from 'planned' to 'active' only if every AC on the Functionality already has a passing test_coverage entry — clearing the data-cy gap is necessary but not sufficient on its own. This promotion is the downstream step — data-cy-instrument invokes living-doc-update for the status change.", "files": [], "expectations": [ "Routes to living-doc-update for status promotion", - "Status changes from planned to active", + "Status changes from planned to active only once every AC has a passing test_coverage entry", + "Does not treat Instrument+Sync completion alone as sufficient for promotion", "Only after Instrument and Sync are complete" ] }, @@ -138,6 +139,18 @@ "getByTestId() resolves to the profile test_id_attribute", "Does not hardcode data-cy when a profile is present" ] + }, + { + "id": 12, + "category": "regression", + "prompt": "A PageObject header has no remaining un-instrumented elements — every locator now uses getByTestId(). The header still carries a `stub-reason:` line. What do I update, and do I write a status field?", + "expected_output": "Remove the `stub-reason:` line from the PageObject header now that no un-instrumented elements remain — its absence is what marks the surface as instrumented. Do not add, write, or promote any `status:` field on the PageObject header: a PageObject header carries no `status:` field at all, since Feature/PageObject state is derived from Functionalities, never authored.", + "files": [], + "expectations": [ + "Removes the stub-reason: line once no un-instrumented elements remain", + "Does not write or promote a status field on the PageObject header", + "States a PageObject header carries no status field" + ] } ] } \ No newline at end of file diff --git a/skills/gherkin-living-doc-sync/SKILL.md b/skills/gherkin-living-doc-sync/SKILL.md index 4f2c1e6..48a143b 100644 --- a/skills/gherkin-living-doc-sync/SKILL.md +++ b/skills/gherkin-living-doc-sync/SKILL.md @@ -54,34 +54,34 @@ Use the report in this repair order: broken links first, then missing links, the |---|---|---| | New `.feature` file added | Feature file to living doc | Link each scenario to an AC; create AC if missing | | User Story AC modified or added | Living doc to feature file | Update or add the corresponding scenario | -| UI refactored (selector / method renamed) | Step text to PageObject | Update step text and `@AC:` tag if scenario intent changed; for the PageObject side of the rename (method signature or locator), load `living-doc-pageobject-scan` HEALING scope — this skill owns only the Gherkin step text, not the PageObject code | +| UI refactored (selector / method renamed) | Step text to PageObject | Update step text and `@AC:` tag if scenario intent changed; for the PageObject side of the rename (method signature or locator), load `living-doc-pageobject-scan` HEALING scope - this skill owns only the Gherkin step text, not the PageObject code | | US deprecated | Living doc to feature file | Emit one sync action per linked scenario; add `@deprecated`, record the reason, and flag `@review-needed` | | Scenario added without an `@AC:` tag | Feature file to living doc | Propose an AC and add the `@AC:` tag | --- -## Step 2 — Audit `@AC:` traceability tags +## Step 2 - Audit `@AC:` traceability tags -> **Authoritative source:** The `@AC:` format is defined in `living-doc-scenario-creator`. The spec below is a reference copy for sync validation — load `living-doc-scenario-creator` for the canonical definition. +> **Authoritative source:** The `@AC:` format is defined in `living-doc-scenario-creator`. The spec below is a reference copy for sync validation - load `living-doc-scenario-creator` for the canonical definition. **Required traceability format** for living-doc feature files (from the glossary): ```gherkin -# AC:US-1-01 (v1.0.0 - active) — customer places an order with a saved payment method +# 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 ``` -With aspect param — when the scenario covers only one aspect of a multi-aspect AC: +With aspect param - when the scenario covers only one aspect of a multi-aspect AC: ```gherkin -# AC:US-1-01 (v1.0.0 - active) — displays {required field} on login screen | aspect: username input +# AC:US-1-01 (v1.0.0 - active) - displays {required field} on login screen | aspect: username input @AC:US-1-01/aspect:username-input Scenario: Login form shows the username input field ``` -- `# AC:` comment: human-readable context — ID, version, state, description, optional aspect. -- `@AC:` Cucumber tag: `@AC:[/param:value...]` — machine-readable link. The `/param:value` format is extensible. +- `# AC:` comment: human-readable context - ID, version, state, description, optional aspect. +- `@AC:` Cucumber tag: `@AC:[/param:value...]` - machine-readable link. The `/param:value` format is extensible. - The `@AC:` tag(s) must appear on the lines immediately above `Scenario:` or `Scenario Outline:`. Additional tags (e.g. `@Regression`, `@skip`) may appear in the same block. - Full AC details (version, state, description) live in the file's `# Acceptance Criteria:` header block. @@ -119,7 +119,7 @@ comment line: ```text SYNC ACTION: checkout.feature:14 — Missing AC link header Scenario: "Customer successfully places an order" - Proposed link: # AC:US-001-01 (v1.0.0 - active) — customer places an order with a saved payment method + Proposed link: # AC:US-001-01 (v1.0.0 - active) - customer places an order with a saved payment method Apply change? (y/n) ``` @@ -131,7 +131,7 @@ missing AC link headers, then emit 3 separate `SYNC ACTION` blocks and propose a each scenario from the living doc catalog. Do not hedge with “for example” or generic placeholders in that answer — show the concrete proposed mappings directly in the blocks. -If only one of `@AC:` or `# AC:` is present, that is still a sync issue — raise a SYNC ACTION and +If only one of `@AC:` or `# AC:` is present, that is still a sync issue - raise a SYNC ACTION and repair the missing side rather than treating the scenario as already synced. ## Step 3 — Detect step text drift @@ -164,11 +164,11 @@ DRIFT DETECTED: checkout.feature:17 Apply the minimum necessary change per action: - **Add missing `@AC:` tag**: insert `@AC:` above `Scenario:` -- **Update stale AC reference**: this is **living doc → feature file** sync. Update the file header's `# Acceptance Criteria:` block entry; the `@AC:` tag on the scenario stays unchanged. Show the exact change as `OLD:` and `NEW:` lines, and mirror the current version/state text from the living doc in the `NEW:` line (for example `v1.0.0` → `v1.1.0` when the AC version changed). If the revised AC intent changed materially, add an explicit `Step text review:` note so the linked step wording can be checked before any scenario restructuring. +- **Update stale AC reference**: this is **living doc → feature file** sync. Update the `# AC:` comment directly ABOVE the affected scenario to reflect the new version, state, and description from the living doc. The `@AC:` tag on the scenario stays unchanged. Show the exact change as `OLD:` and `NEW:` lines, and mirror the current version/state text from the living doc in the `NEW:` line (for example `v1.0.0` → `v1.1.0` when the AC version changed). If the revised AC intent changed materially, add an explicit `Step text review:` note so the linked step wording can be checked before any scenario restructuring. Also update the matching `AC:` entry in the file header's `# acceptance_criteria:` block (see `living-doc-bdd-schemas.md`) to the same version, state, and description in the same sync action — the header entry is mined US/Functionality metadata, not decorative, and a stale copy there lets a collector read outdated AC data even after the scenario-level comment is fixed. - **Update scenario to match revised AC**: update step text; keep the `@AC:` tag unchanged - **Fix broken step text**: prefer updating the `.feature` file to match the existing step definition and PageObject method; only update the step definition regex when the business wording genuinely changed - **Mark deprecated scenarios**: add `@deprecated` and `@review-needed`, plus a comment with the date and reason. Emit one action per affected scenario with file and line number. -- **Mark descoped scenarios**: add `@wip` or `@pending` and `@review-needed`, plus a comment with the descope reason and target-release reference. Preserve the scenario — never delete it — so it can be reinstated when the AC is promoted back to `active`. Emit one SYNC ACTION per affected scenario. +- **Mark descoped scenarios**: there is no `descoped` state — the AC keeps its `planned` state and drops any target version, reading `AC: (planned)`. Update the existing `# AC:` comment's header to `AC: (planned)` while keeping its description unchanged — the description is required and must still match the catalog AC — then add a separate `# Rationale: ...` comment line mirroring the AC's `- Rationale:` bullet. Also update the matching `AC:` entry in the file header's `# acceptance_criteria:` block (see `living-doc-bdd-schemas.md`) in the same sync action — drop its target version to match `AC: (planned)` and add the `- Rationale:` bullet there too — the header entry is mined US/Functionality metadata, not decorative, and a stale copy there lets a collector keep mining the old target version even after the scenario-level comment is fixed. Add `@wip` and `@review-needed` to the scenario. Preserve the scenario — never delete it — so it can be reinstated when the AC is re-targeted to a release. Emit one SYNC ACTION per affected scenario. - **Broken AC reference**: never silently remove the `@AC:` tag. Either relink it to the correct AC ID, or create the missing living doc entity with `living-doc-create-user-story` / `living-doc-create-functionality`, then update the tag. - **AC split into multiple ACs**: update the existing scenario's `@AC:` tag to the primary AC; emit a separate `SYNC ACTION` proposing each additional scenario, including the required `# AC:` header and `@AC:` tag for the new AC. Developer confirmation is still required before any new scenario is created. - **Aspect mismatch** (`@AC:.../aspect:...` present but comment missing the aspect): raise a SYNC ACTION and update the comment to include the human-readable `| aspect: ...` suffix. Confirm before applying. @@ -211,9 +211,9 @@ Summary: 2 missing AC links, 1 step text drift detected — apply changes? (y/n For deprecated or descoped entities, emit one `SYNC ACTION` per affected scenario, each with its own file path and line number, plus the added tags/comments (`@deprecated` + `@review-needed`, or -`@wip`/`@pending` + descope reason). +`@wip` + `@review-needed` mirroring the AC's `- Rationale:` bullet). For missing-link report examples, include the full proposed `# AC:` comment (ID, version, state, -description, and aspect if relevant) — not just the `@AC:` tag. +description, and aspect if relevant) - not just the `@AC:` tag. --- @@ -221,7 +221,7 @@ description, and aspect if relevant) — not just the `@AC:` tag. | Anti-pattern | Flag | |---|---| -| Scenario with no `@AC:` tag | Missing traceability — add tag or create AC | +| Scenario with no `@AC:` tag | Missing traceability - add tag or create AC | | Two scenarios linked to the same AC | Usually a duplicate — review | | AC linked from a scenario in a different User Story's feature file | Passive cross-US coverage — permitted but note it in the sync report. Only flag if the scenario's primary intent belongs to a different User Story (misplaced scenario) | | Step text describes implementation (selector, endpoint) | Gherkin business-language violation — refer to `living-doc-scenario-creator` | diff --git a/skills/gherkin-living-doc-sync/evals/evals.json b/skills/gherkin-living-doc-sync/evals/evals.json index 7cc8ed8..cbaccda 100644 --- a/skills/gherkin-living-doc-sync/evals/evals.json +++ b/skills/gherkin-living-doc-sync/evals/evals.json @@ -5,7 +5,7 @@ "id": 1, "category": "happy-path", "prompt": "checkout.feature has 3 scenarios but none of them have a # AC: comment above them. What should I do?", - "expected_output": "Agent identifies all three scenarios as missing AC link headers (sync direction: feature file → living doc). For each scenario, it proposes a matching AC from the living doc catalog and outputs a SYNC ACTION block per scenario showing the proposed # AC: line to insert. Format: 'SYNC ACTION: checkout.feature: — Missing AC link header — Proposed link: # AC: ( – ) — '. Asks the developer to confirm each mapping before applying.", + "expected_output": "Agent identifies all three scenarios as missing AC link headers (sync direction: feature file → living doc). For each scenario, it proposes a matching AC from the living doc catalog and outputs a SYNC ACTION block per scenario showing the proposed # AC: line to insert. Format: 'SYNC ACTION: checkout.feature: - Missing AC link header - Proposed link: # AC: (v - ) - '. Asks the developer to confirm each mapping before applying.", "files": [], "expectations": [ "Identifies all scenarios missing # AC: comments", @@ -19,12 +19,13 @@ "id": 2, "category": "happy-path", "prompt": "The product owner changed the description of AC:US-001-01 in the living doc. The linked scenario in checkout.feature still has the old # AC: description text. How do I fix this?", - "expected_output": "Sync direction is living doc → feature file. Agent updates the # AC: comment text above the linked scenario to reflect the new AC description. The AC ID itself is never changed — only the description text in the comment. Outputs: OLD: '# AC: US-001-01 (v1.0.0 – Active) — ' and NEW: '# AC: US-001-01 (v1.1.0 – Active) — '. Flags any step text that may also need updating to match the revised AC intent.", + "expected_output": "Sync direction is living doc → feature file. Agent updates the # AC: comment text above the linked scenario to reflect the new AC description. The AC ID itself is never changed - only the description text in the comment. Outputs: OLD: '# AC:US-001-01 (v1.0.0 - active) - ' and NEW: '# AC:US-001-01 (v1.1.0 - active) - '. In the same sync action, also updates the matching AC:US-001-01 entry in the file header's `# acceptance_criteria:` block to the same version, state, and description — that entry is mined US metadata and must not be left stale. Flags any step text that may also need updating to match the revised AC intent.", "files": [], "expectations": [ "Sync direction: living doc → feature file", "Updates comment description text only — AC ID remains stable", "Shows old and new comment text clearly labelled", + "Also updates the matching AC:US-001-01 entry in the file header's # acceptance_criteria: block to the same version/state/description", "Flags step text for review if the AC intent changed significantly", "Does not change the scenario structure" ] @@ -85,7 +86,7 @@ "id": 7, "category": "edge-case", "prompt": "A scenario has '# AC: US-099-01' in its header but US-099 does not exist in the living doc catalog. What should I do?", - "expected_output": "This is a broken AC reference. Agent outputs a SYNC ACTION flagging the scenario with the broken link. Resolution options: (1) find the correct AC ID in the catalog that matches this scenario's intent and update the # AC: comment; (2) if the behavior is new and has no AC, invoke living-doc-create-user-story or living-doc-create-functionality to create the missing entity, then link the scenario. Never silently removes the # AC: comment — that would destroy the traceability intent.", + "expected_output": "This is a broken AC reference. Agent outputs a SYNC ACTION flagging the scenario with the broken link. Resolution options: (1) find the correct AC ID in the catalog that matches this scenario's intent and update the # AC: comment; (2) if the behavior is new and has no AC, invoke living-doc-create-user-story or living-doc-create-functionality to create the missing entity, then link the scenario. Never silently removes the # AC: comment - that would destroy the traceability intent.", "files": [], "expectations": [ "Detects the broken AC reference (AC ID not in catalog)", @@ -98,7 +99,7 @@ "id": 8, "category": "output-format", "prompt": "Show me what the output of a sync run looks like when there are 2 missing AC links and 1 step text drift.", - "expected_output": "Output contains: (1) a SYNC ACTION block for each missing AC link — showing the feature file path, line number, scenario title, and proposed # AC: header; (2) a DRIFT DETECTED block for the broken step — showing the old step text, the expected step text, the PageObject method, and the suggested fix. Each block has a clear header (SYNC ACTION / DRIFT DETECTED). A summary line at the end: '2 missing AC links, 1 step text drift detected — apply changes? (y/n per action)'.", + "expected_output": "Output contains: (1) a SYNC ACTION block for each missing AC link - showing the feature file path, line number, scenario title, and proposed # AC: header; (2) a DRIFT DETECTED block for the broken step - showing the old step text, the expected step text, the PageObject method, and the suggested fix. Each block has a clear header (SYNC ACTION / DRIFT DETECTED). A summary line at the end: '2 missing AC links, 1 step text drift detected - apply changes? (y/n per action)'.", "files": [], "expectations": [ "SYNC ACTION blocks for each missing AC link with file, line, scenario, and proposed fix", @@ -112,7 +113,7 @@ "id": 9, "category": "happy-path", "prompt": "What is the difference between the '@AC:US-001-01' Cucumber tag and the '# AC:US-001-01 ...' comment above a scenario? Do I need both?", - "expected_output": "Yes — both serve distinct purposes. The '# AC: ...' comment provides human-readable context: the AC version, state, description, and optional aspect annotation, all visible at a glance in the feature file. The '@AC:' Cucumber tag provides machine-readable traceability: coverage scripts, gap finders, and CI tools use it to link scenarios to ACs programmatically. Both must be present on every scenario for full traceability. The sync skill ensures both are aligned — if only one is present, a SYNC ACTION is raised.", + "expected_output": "Yes - both serve distinct purposes. The '# AC: ...' comment provides human-readable context: the AC version, state, description, and optional aspect annotation, all visible at a glance in the feature file. The '@AC:' Cucumber tag provides machine-readable traceability: coverage scripts, gap finders, and CI tools use it to link scenarios to ACs programmatically. Both must be present on every scenario for full traceability. The sync skill ensures both are aligned - if only one is present, a SYNC ACTION is raised.", "files": [], "expectations": [ "Explains # AC: comment as human-readable context with version, state, and description", @@ -125,7 +126,7 @@ "id": 10, "category": "happy-path", "prompt": "I want to run a full audit of all AC link headers across all feature files before starting a sync. How do I use scan_ac_links.py?", - "expected_output": "Run: 'python skills/gherkin-living-doc-sync/scripts/scan_ac_links.py --us-dir --func-dir --catalog '. The script scans the living-doc feature directories for '# AC:' comments and '@AC:' tags, validates each AC ID against the living doc catalog, and outputs a report listing: (1) missing # AC: comments (scenarios with no link header), (2) stale AC IDs (IDs not found in catalog), (3) mismatched comment/tag pairs (one present but not the other). The --us-dir and --func-dir flags accept the profile feature_dirs.user_story and feature_dirs.functionality values (defaults: features/liv_doc_us/ and features/liv_doc_func/). Use this report as input to the sync workflow — address broken links first, then missing links, then mismatches.", + "expected_output": "Run: 'python skills/gherkin-living-doc-sync/scripts/scan_ac_links.py --us-dir --func-dir --catalog '. The script scans the living-doc feature directories for '# AC:' comments and '@AC:' tags, validates each AC ID against the living doc catalog, and outputs a report listing: (1) missing # AC: comments (scenarios with no link header), (2) stale AC IDs (IDs not found in catalog), (3) mismatched comment/tag pairs (one present but not the other). The --us-dir and --func-dir flags accept the profile feature_dirs.user_story and feature_dirs.functionality values (defaults: features/liv_doc_us/ and features/liv_doc_func/). Use this report as input to the sync workflow - address broken links first, then missing links, then mismatches.", "files": [], "expectations": [ "Names the correct command with --us-dir and --func-dir flags (profile feature_dirs paths)", @@ -138,8 +139,8 @@ { "id": 11, "category": "regression", - "prompt": "A scenario has '@AC:US-001-01/aspect:username-input' but the # AC: comment above it just reads '# AC:US-001-01 (v1.0.0 – Active) — displays login form fields'. Is this a sync issue?", - "expected_output": "Yes — this is a SYNC ACTION. The @AC: tag encodes an aspect param (/aspect:username-input) but the # AC: comment does not mirror it. Per the skill spec, the comment must include '| aspect: username input' to match the tag. Apply: change the comment to '# AC:US-001-01 (v1.0.0 – Active) — displays login form fields | aspect: username input'. The comment mirrors the human-readable form of the tag's /aspect: param. Confirm before applying.", + "prompt": "A scenario has '@AC:US-001-01/aspect:username-input' but the # AC: comment above it just reads '# AC:US-001-01 (v1.0.0 - Active) - displays login form fields'. Is this a sync issue?", + "expected_output": "Yes - this is a SYNC ACTION. The @AC: tag encodes an aspect param (/aspect:username-input) but the # AC: comment does not mirror it. Per the skill spec, the comment must include '| aspect: username input' to match the tag. Apply: change the comment to '# AC:US-001-01 (v1.0.0 - active) - displays login form fields | aspect: username input'. The comment mirrors the human-readable form of the tag's /aspect: param. Confirm before applying.", "files": [], "expectations": [ "Identifies mismatch between @AC: tag aspect param and # AC: comment as a SYNC ACTION", @@ -151,22 +152,24 @@ { "id": 12, "category": "edge-case", - "prompt": "I have a scenario linked to AC:US-042-03 which has just been descoped in the living doc (status: descoped). What should the sync skill do?", - "expected_output": "Sync direction: living doc → feature file. When an AC is descoped, the linked scenario should be tagged with @wip or @pending to indicate it is not active. Add a comment above the scenario explaining the descope: '# Descoped: AC:US-042-03 descoped on 2026-05-15 — reason: promo stacking deferred to sprint-52'. Never delete the scenario — it preserves the intent for when the AC is reinstated. Flag the scenario for @pending or @wip tagging in the CI pipeline.", + "prompt": "I have a scenario linked to AC:US-042-03 which has just been descoped in the living doc — it dropped back to AC:US-042-03 (planned) with a Rationale bullet. What should the sync skill do?", + "expected_output": "Sync direction: living doc → feature file. There is no `descoped` state — the AC keeps its `planned` state and dropped its target version. Update the existing '# AC:US-042-03' comment's header to '(planned)' while keeping its original description unchanged, e.g. '# AC:US-042-03 (planned) - customer can stack multiple promo codes at checkout' — the canonical # AC: comment always carries the AC's description, never the Rationale in its place. Add a separate comment line mirroring the AC's Rationale bullet: '# Rationale: promo stacking deferred to a future release'. In the same sync action, also update the matching AC:US-042-03 entry in the file header's `# acceptance_criteria:` block — drop its target version so it reads 'AC:US-042-03 (planned)' and add a '- Rationale: promo stacking deferred to a future release' bullet there too, since a collector mining that block would otherwise still see the old target version. Tag the scenario with @wip and @review-needed to indicate it is not active. Never delete the scenario - it preserves the intent for when the AC is re-targeted to a release.", "files": [], "expectations": [ "Sync direction: living doc → feature file", - "Tags scenario with @wip or @pending — does not delete it", - "Adds a comment with the descope reason and date", - "Preserves the scenario for when the AC is reinstated", - "Flags for @pending tagging in CI" + "Does not use a 'descoped' state — the AC is 'planned' with no target version", + "Tags scenario with @wip and @review-needed — does not delete it", + "Keeps the # AC: comment's description unchanged, only updating the version/state header", + "Adds the Rationale as a separate comment line rather than replacing the description", + "Also updates the matching AC:US-042-03 entry in the file header's # acceptance_criteria: block — drops the target version and adds the Rationale bullet", + "Preserves the scenario for when the AC is re-targeted to a release" ] }, { "id": 13, "category": "edge-case", "prompt": "AC:US-001-02 was split into two separate ACs: AC:US-001-02 (happy path) and AC:US-001-04 (alt path: guest checkout). The feature file still has one scenario linked to AC:US-001-02. What does the sync output look like?", - "expected_output": "Two SYNC ACTIONs are emitted. (1) Existing scenario: the '@AC:US-001-02' tag is updated to the primary (happy path) AC — it stays unchanged since it was always the primary AC. (2) A SYNC ACTION proposes creating a new scenario for AC:US-001-04 with the required '# AC: US-001-04' header and '@AC:US-001-04' tag. The existing scenario is never modified or deleted. Developer confirms both actions before any change is applied.", + "expected_output": "Two SYNC ACTIONs are emitted. (1) Existing scenario: the '@AC:US-001-02' tag is updated to the primary (happy path) AC - it stays unchanged since it was always the primary AC. (2) A SYNC ACTION proposes creating a new scenario for AC:US-001-04 with the required '# AC:US-001-04' header and '@AC:US-001-04' tag. The existing scenario is never modified or deleted. Developer confirms both actions before any change is applied.", "files": [], "expectations": [ "Existing scenario's @AC: tag is updated to point to the primary AC (US-001-02)", @@ -179,7 +182,7 @@ "id": 14, "category": "happy-path", "prompt": "During a sync pass I discover that the step text for AC:US-007-03 changed AND the PageObject method that backs it was renamed. What should I do for each part?", - "expected_output": "Split the work: (1) gherkin-living-doc-sync updates the step text and the @AC: tag in the .feature file — this skill handles Gherkin text sync. (2) The PageObject method rename (signature and locator) is owned by living-doc-pageobject-scan HEALING scope — load that skill for the PO side. Do not attempt to rename PageObject methods inside this sync skill.", + "expected_output": "Split the work: (1) gherkin-living-doc-sync updates the step text and the @AC: tag in the .feature file - this skill handles Gherkin text sync. (2) The PageObject method rename (signature and locator) is owned by living-doc-pageobject-scan HEALING scope - load that skill for the PO side. Do not attempt to rename PageObject methods inside this sync skill.", "files": [], "expectations": [ "Correctly splits Gherkin-side vs PageObject-side work", diff --git a/skills/gherkin-living-doc-sync/evals/fixture-map.md b/skills/gherkin-living-doc-sync/evals/fixture-map.md index bb7c892..e49e82b 100644 --- a/skills/gherkin-living-doc-sync/evals/fixture-map.md +++ b/skills/gherkin-living-doc-sync/evals/fixture-map.md @@ -19,7 +19,7 @@ No fixture files for this skill. All evals are conversational — the skill oper | 9 | happy-path | _(none)_ | @AC: Cucumber tag vs # AC: comment — both required, each serves a distinct purpose | | 10 | happy-path | _(none)_ | scan_ac_links.py audit command and output interpretation | | 11 | regression | _(none)_ | Aspect param mismatch: @AC: tag has /aspect: but # AC: comment does not mirror it | -| 12 | edge-case | _(none)_ | Descoped AC: tag scenario @wip/@pending, add comment, never delete | +| 12 | edge-case | _(none)_ | Descoped AC (dropped back to `planned`): tag scenario @wip + @review-needed, mirror the Rationale bullet, never delete | ## Trigger eval summary diff --git a/skills/living-doc-create-feature/SKILL.md b/skills/living-doc-create-feature/SKILL.md index d34b871..d103f6a 100644 --- a/skills/living-doc-create-feature/SKILL.md +++ b/skills/living-doc-create-feature/SKILL.md @@ -1,19 +1,18 @@ --- name: living-doc-create-feature description: > - Define a system surface (UI screen, API endpoint, service, or module) as a Feature entity, - enabling impact analysis and traceability in the living documentation. Use when documenting - a new screen, API, service, or module; mapping surfaces to User Stories; or resolving - Feature naming conflicts. + Define a system surface (UI screen, or API endpoint including a backend service's public + contract) as a Feature entity for impact analysis and traceability. Use when documenting a + new screen, API, or backend service, or mapping surfaces to User Stories. Triggers on: "document a new feature", "create a feature entity", "new screen documentation", - "document an API endpoint", "feature registry", "what feature owns this", "map user story to - feature", "system surface documentation", "feature owners", "feature dependencies", - "duplicate feature name", "resolve feature naming", "rename feature". - Does NOT trigger for: creating User Stories (use living-doc-create-user-story); defining - behaviors (use living-doc-create-functionality); scanning PageObjects (use - living-doc-pageobject-scan); deprecating (use living-doc-update). - Pairs with living-doc-create-functionality and living-doc-create-user-story. - After creating, add a feature_registry entry for living-doc-impact-analysis. + "document an API endpoint", "system surface documentation", "what feature owns this", + "map user story to feature", "add feature registry entry", "feature owners", + "new feature's dependencies", "duplicate feature name", "resolve feature naming". + NOT for: User Stories (living-doc-create-user-story), behaviors + (living-doc-create-functionality), PageObject scans (living-doc-pageobject-scan), changing + an existing Feature: owners, dependencies, rename, registry, deprecation (living-doc-update). + Pairs with living-doc-create-functionality and living-doc-create-user-story; after creating, + add a feature_registry entry for living-doc-impact-analysis. license: Apache-2.0 --- @@ -38,24 +37,22 @@ When information is missing, phrase the discovery as a short numbered checklist 5. external dependencies 6. surface type, if still ambiguous -When details are missing but the surface name makes the domain obvious, infer a sensible starter draft instead of blocking. If the prompt does **not** explicitly say the links are unknown, seed provisional `US-...` / `FUNC-...` references instead of leaving both arrays empty. Common examples: -- `Checkout Page` → `surface_type: "UI"`; dependencies often include `payment-gateway` and `order-service`; starter Functionalities can be `FUNC-001`, `FUNC-002`, `FUNC-003`. +When details are missing but the surface name makes the domain obvious, infer a sensible starter draft instead of blocking — purpose, `surface_type`, and `external_dependencies` can be inferred confidently from the surface name. `user_stories` and `functionalities` are the exception: leave them `[]` unless the catalog already contains the linked entity (see Step 3 and Step 4). Common examples: +- `Checkout Page` → `surface_type: "UI"`; dependencies often include `payment-gateway` and `order-service`. - `Orders API` / REST controller → `surface_type: "API"`; dependencies often include `order-db` and `notification-service`. -- `Notification Service` / notification worker → default to `surface_type: "Worker"` (or `API` if it is clearly a synchronous contract surface); do **not** simply mirror the word "Service" into `surface_type`; dependencies often include `smtp-relay` and `template-store`. -- `PaymentEventProcessor` / event consumer → `surface_type: "Worker"`; include the Kafka topic or event stream in `external_dependencies`. +- `Notification Service` / async notification worker → if it exposes a REST/GraphQL endpoint with an annotated endpoint method, or a message-broker contract (e.g. a Kafka topic) with an AsyncAPI (or equivalent schema-registry) annotation on the producer/consumer handler, `surface_type: "API"` documenting that contract; do **not** simply mirror the word "Service" into `surface_type` — there is no separate `Service` or `Worker` type. If it has neither an annotated endpoint method nor an annotated event handler, it has no canonical test-abstraction anchor yet (see table below) — do **not** create a Feature for it; instead record it as an `external_dependencies` entry on the Feature(s) that publish to or consume it. Dependencies often include `smtp-relay` and `template-store`. +- `PaymentEventProcessor` / event consumer on a Kafka topic → ask whether the producer/consumer handler carries an AsyncAPI (or equivalent schema-registry) annotation. If it does, that annotated handler is the `API` contract anchor — create the Feature with `surface_type: "API"`. If the topic contract is not yet annotated, it has no canonical test-abstraction anchor: do **not** create a Feature for it; record the topic as an `external_dependencies` entry on the Feature(s) that publish or consume it, and note that annotating the handler (AsyncAPI or equivalent) is what unlocks Feature-level tracking. Ask only for what is missing: *What system surface does this Feature represent?* -Select the surface type: +Select the surface type — only two exist, and each is paired with a matching test abstraction that anchors it as a Feature: -| Type | Examples | -|---|---| -| `UI` | A web page, modal, or named screen (e.g. Checkout Page, Login Screen) | -| `API` | A REST/GraphQL endpoint or endpoint group, including a backend service's public API contract (e.g. Orders API, Payment Gateway API) | -| `Service` | A named backend/service surface with its own contract (e.g. Customer Profile Service) | -| `Worker` | An asynchronous/background processor (e.g. Notification Worker) | -| `Module` | A distinct internal module with a stable contract or bounded responsibility | -| `Library` | A substantial shared internal library that is intentionally tracked as its own surface | +| Type | Examples | Eventual test-abstraction anchor | +|---|---|---| +| `UI` | A web page, modal, or named screen (e.g. Checkout Page, Login Screen) | A PageObject for the screen — create it together with the Feature (e.g. via `living-doc-pageobject-scan`) if it doesn't exist yet | +| `API` | A REST/GraphQL endpoint or endpoint group, including a backend service's public contract (e.g. Orders API, Payment Gateway API), or a message-broker topic (e.g. Kafka) documented via an AsyncAPI (or equivalent) specification | An **annotated endpoint method** (OpenAPI annotation, JSDoc, etc.) for request/response, or an **annotated event handler** (AsyncAPI or equivalent schema-registry annotation on the producer/consumer) for event-driven — either serves as the living contract anchor, see [living-doc-glossary](../shared/references/living-doc-glossary.md) | + +A surface where the anchor is not even achievable — for example a pure event consumer or async worker whose topic/queue contract carries no AsyncAPI (or equivalent) annotation — is **not** a Feature yet. Document it as an `external_dependencies` entry on the Feature(s) that interact with it until the contract is formally annotated. A UI or API surface whose anchor simply hasn't been built yet is still a valid Feature — create the Feature and its anchor together rather than blocking on sequencing. Feature names should be **noun phrases** that name the surface. If it could plausibly be a PageObject or service/module class name (for example `PaymentPage`), it is usually a good Feature name. @@ -76,8 +73,9 @@ Ask: *Which User Stories rely on this Feature?* If unknown at creation time, leave empty `[]` but warn: > "An orphaned Feature (not linked to any User Story) contributes no traceable business value. -> Link at least one User Story or mark this as exploratory with status: 'candidate'. -> Orphaned Features are surfaced as gaps in living-doc-gap-finder reports." +> Link at least one User Story once one exists — a Feature has no `status` field to mark it +> exploratory with. +> Orphaned Features are reported as an `ORPHAN_FEATURE` condition in living-doc-gap-finder reports." ## Step 4 — Enumerate Functionalities @@ -95,14 +93,17 @@ FUNC entries), leave the array as `[]` and add a warning: | Field | What to capture | |---|---| | `owners` | Team name(s) or individual(s) responsible for this surface | -| `external_dependencies` | Services or systems this Feature calls (e.g. payment-gateway, order-service) | +| `external_dependencies` | Services or systems this Feature calls that are **not** documented Features (e.g. payment-gateway, order-service) | + +`external_dependencies` is no longer the only place a dependency is recorded. Before writing a name into it, check whether the catalog already documents that system as a Feature: +- **Not a documented Feature** — no canonical anchor, so nothing in the catalog can carry its contract. Record it here, by name. +- **Already a documented `API` Feature** — leave it out of `external_dependencies`. The call is a `feature_dependencies` edge, authored on the Functionality that makes the call, not on this Feature (whose value is derived from its Functionalities). Note the target's `FEAT-` for `living-doc-create-functionality`, or for `living-doc-update` if the calling Functionality already exists. See [living-doc-glossary — Functionality](../shared/references/living-doc-glossary.md#functionality-func). For starter drafts, prefer **provisional inferred values** over empty strings when the domain is obvious: -- `user_stories`: use starter IDs such as `US-checkout`, `US-order-management`, `US-payment-processing` -- `functionalities`: use starter IDs such as `FUNC-001`, `FUNC-002`, `FUNC-003` - `owners`: always emit an array, even for one owner: `["team-identity"]` +- `external_dependencies`: infer the systems this kind of surface typically calls (e.g. `payment-gateway`, `order-service`), minus any the catalog already documents as a Feature -Use `[]` only when the relationship is truly unknown and you cannot infer a sensible starter link. +`user_stories` and `functionalities` are never fabricated: use `[]` unless the catalog already contains the linked entity (see Step 3 and Step 4), and pair `[]` with the orphan/candidate warnings from those steps. ## Step 6 — Output canonical Feature entity @@ -122,7 +123,7 @@ Use `[]` only when the relationship is truly unknown and you cannot infer a sens > `Ran: python scripts/next_id.py --type FEAT --catalog catalog.json -> FEAT-012` > and use that returned ID in the JSON. -Output the entity as a **single fenced `json` code block** whenever you have enough information to draft it. The block must contain **only** the JSON object — no prose, no bullets, no warnings inside the fence. The literal first line of the block must be ````json` and the closing line must be ``` . Code fences are required plain text, not optional formatting. Keep any warnings or follow-up questions **outside** the code block. If the user gives a named surface but not all metadata, ask the missing questions and still include a starter draft in the same reply, using inferred purpose/surface type, `status: "planned"`, and `[]` only where nothing sensible can be inferred. If the request explicitly asks to create the entity from the given details, emit the draft immediately. +Output the entity as a **single fenced `json` code block** whenever you have enough information to draft it. The block must contain **only** the JSON object — no prose, no bullets, no warnings inside the fence. The literal first line of the block must be ````json` and the closing line must be ``` . Code fences are required plain text, not optional formatting. Keep any warnings or follow-up questions **outside** the code block. If the user gives a named surface but not all metadata, ask the missing questions and still include a starter draft in the same reply, using inferred purpose/surface type/external_dependencies, and `[]` for `user_stories`/`functionalities` unless the catalog already backs the link. A Feature never carries a `status` field — its state is derived from its Functionalities, never authored. If the request explicitly asks to create the entity from the given details, emit the draft immediately. Use this exact output shape for create/document requests: - Optional brief line with only the missing questions. @@ -132,7 +133,6 @@ Use this exact output shape for create/document requests: - `name` - `surface_type` - `purpose` - - `status` - `user_stories` - `functionalities` - `owners` @@ -147,9 +147,8 @@ Use this exact output shape for create/document requests: "name": "Example Surface", "surface_type": "UI", "purpose": "Business-language summary of the surface responsibility.", - "status": "planned", - "user_stories": ["US-example"], - "functionalities": ["FUNC-001"], + "user_stories": [], + "functionalities": [], "owners": ["team-example"], "external_dependencies": ["example-service"] } @@ -158,9 +157,9 @@ Use this exact output shape for create/document requests: Do not replace the fenced block with raw JSON. Do not emit `owners` as a string. Use a spaced noun phrase for the `name` field (for example `Payment Event Processor`, not `PaymentEventProcessor`). Worked starter patterns: -- `Checkout Page` starter links: explicitly ask *What user interactions does it own? Which User Stories rely on it? What Functionalities does it own? Who owns it? What external dependencies does it call?* Run next_id.py to get the numeric ID, then use `id: "FEAT-001"` (or next number), `user_stories: ["US-001"]`, `functionalities: ["FUNC-001", "FUNC-002", "FUNC-003"]`, `owners: ["team-checkout"]`, `external_dependencies: ["payment-gateway", "order-service"]` -- `Orders API` starter links: `user_stories: ["US-001"]`, `functionalities: ["FUNC-001", "FUNC-002", "FUNC-003"]` -- `Notification Service` starter draft: emit a Feature JSON even when the user asks "where do I start?", but explicitly ask: *What type of surface is it (API, Worker, or UI)? Which User Stories rely on it? What Functionalities does it own? Who owns it? What are the external dependencies (SMTP relay, template store, etc.)?* If the prompt still sounds like asynchronous alert delivery after those questions, run next_id.py to get the numeric ID, then use `id: "FEAT-001"` (or next number), `surface_type: "Worker"`, `user_stories: ["US-001"]`, `functionalities: ["FUNC-001", "FUNC-002"]`, `owners: ["team-notifications"]`, and `external_dependencies: ["smtp-relay", "template-store"]` +- `Checkout Page` starter links: explicitly ask *What user interactions does it own? Which User Stories rely on it? What Functionalities does it own? Who owns it? What external dependencies does it call?* Run next_id.py to get the numeric ID, then use `id: "FEAT-001"` (or next number), `user_stories: []` and `functionalities: []` unless the catalog already contains the linked entities (repeat the Step 3/Step 4 warnings), `owners: ["team-checkout"]`, `external_dependencies: ["payment-gateway", "order-service"]` +- `Orders API` starter links: `user_stories: []`, `functionalities: []` unless the catalog already contains the linked entities +- `Notification Service` starter draft: explicitly ask: *Does it expose a REST/GraphQL endpoint (e.g. a trigger-notification call), or a message-broker topic with an annotated event handler (a producer/consumer documented via AsyncAPI or an equivalent schema-registry annotation)? Which User Stories rely on it? What Functionalities does it own? Who owns it? What are the external dependencies (SMTP relay, template store, message broker, etc.)?* If it exposes either an annotated endpoint method or an annotated event handler, run next_id.py to get the numeric ID, then emit a Feature JSON with `surface_type: "API"` documenting that contract (the REST/GraphQL endpoint, or the AsyncAPI-annotated producer/consumer handler), `user_stories: []` and `functionalities: []` unless the catalog already contains the linked entities, `owners: ["team-notifications"]`, and `external_dependencies: ["smtp-relay", "template-store"]`. Only if the prompt still describes purely asynchronous alert delivery with **neither** an annotated endpoint **nor** an annotated event handler (no documented AsyncAPI/schema-registry contract at all) after those questions, do **not** create a Feature — explain there is no `UI`/`API` test-abstraction anchor for it, and record it as an `external_dependencies` entry on the Feature(s) that publish to or consume it instead. Canonical JSON fields: @@ -169,13 +168,12 @@ Canonical JSON fields: | `type` | Yes | `Feature` | | `id` | Yes | `FEAT-NNN` (zero-padded numeric ID from catalog, e.g. `FEAT-001`) | | `name` | Yes | Noun phrase (e.g. "Login Page") | -| `surface_type` | Yes | `UI` \| `API` \| `Service` \| `Worker` \| `Module` \| `Library` | +| `surface_type` | Yes | `UI` \| `API` | | `purpose` | Yes | One-to-two sentence description in business language | -| `status` | Yes | `planned` \| `active` \| `candidate` \| `deprecated` | | `user_stories` | Yes | List of `US-<...>` IDs (use `[]` if unknown) | | `functionalities` | Yes | List of `FUNC-<...>` IDs (use `[]` if unknown or still only candidates) | | `owners` | Yes | Team name(s) | -| `external_dependencies` | Yes | Names of services or systems this Feature calls | +| `external_dependencies` | Yes | Names of services or systems this Feature calls that are not documented Features — a documented `API` target is a `feature_dependencies` edge on the calling Functionality instead | If `user_stories` is `[]`, repeat the orphan warning from Step 3 outside the JSON. If `functionalities` is `[]` because they are still just candidate notes, repeat the formal-definition warning from Step 4 outside the JSON. @@ -185,8 +183,8 @@ If `user_stories` is `[]`, repeat the orphan warning from Step 3 outside the JSO |---|---| | Feature covers multiple unrelated screens | Split into one Feature per distinct screen | | Feature name is a verb (e.g. "Process Payment") | Feature names should be nouns — name the surface. Verb phrases describe *what the surface does*, which belongs in a Functionality entity (use **living-doc-create-functionality**). If it could be a PageObject or service/module class name, it is usually a better Feature name. | -| Feature has no User Stories and no Functionalities | Orphan Feature — it contributes no traceable business value. Link at least one User Story, mark it as `candidate` if it is still exploratory, or delete it if it is no longer relevant. Orphan Features will be surfaced as gaps in living-doc-gap-finder reports. | -| Shared utility library documented as a Feature | By default, a shared utility library is not a Feature — document it as an `external_dependency` on the consumer Features. Only create a standalone Feature when the library is substantial enough to be treated as a distinct shared surface; in that case use `surface_type: "Library"` and mark it as a shared internal dependency. Features should map 1:1 to distinct/deployable surfaces. | +| Feature has no User Stories and no Functionalities | Orphan Feature — it contributes no traceable business value. Link at least one User Story once one exists, or delete it if it is no longer relevant. A Feature has no `status` field to flag it as exploratory with; living-doc-gap-finder reports it as an `ORPHAN_FEATURE` condition instead. | +| Shared utility library documented as a Feature | A shared utility library is never a Feature — there is no `surface_type` for it (only `UI` and `API` exist). Document it as an `external_dependency` on the consumer Features instead, however substantial it is. Features should map 1:1 to distinct/deployable UI or API surfaces. | | Feature name encodes implementation technology (e.g. "React Login Component", "Spring Payment Controller") | Feature names describe the business surface, not the stack. Use "Login Screen" (UI) or "Payment API" (API) — technology choice is an implementation detail that changes without the surface changing. | | `surface_type` is `UI` for a backend REST controller or service | A REST endpoint group is an `API` surface. `UI` is reserved for screens a human interacts with directly. Misclassification breaks impact analysis routing between frontend and backend changes. | | Feature shares a name with an existing Feature | Check for duplicates before creating. Identical names indicate a merge candidate or a scope overlap — clarify the boundary before proceeding. | @@ -207,8 +205,9 @@ If `user_stories` is `[]`, repeat the orphan warning from Step 3 outside the JSO | Link to an existing User Story | **living-doc-update** (add Feature to the User Story's `features` list) | | Generate BDD PageObjects for a UI Feature | **living-doc-pageobject-scan** | | Update feature_registry for impact traceability | **living-doc-impact-analysis** (see Feature registry format in that skill) | +| The new Feature was an `external_dependencies` entry on other Features | **living-doc-update** — "Change a Feature's dependencies" moves the entry off every Feature that named it | -> **Renaming a Feature:** Changing a Feature's `id` or `name` requires cascading updates. Load `living-doc-update` and follow the "Rename a Feature" workflow there. The minimum cascade is: (1) update the Feature entity itself, (2) update every linked Functionality `feature_id`, (3) update the `feature_registry` entry, (4) update `manifest.json`, (5) update `seed.yaml`, (6) update PageObject file headers, (7) update Gherkin feature file `# Feature:` headers, then run **living-doc-gap-finder** to confirm no orphan references remain. +> **Renaming a Feature:** Changing a Feature's `id` or `name` requires cascading updates. Load `living-doc-update` and follow the "Rename a Feature" workflow there. The minimum cascade is: (1) update the Feature entity itself, (2) update every linked Functionality `parent_feature`, (3) update the `feature_registry` entry, (4) update `manifest.json`, (5) update `seed.yaml`, (6) update PageObject file headers, (7) update Gherkin feature file `# Feature:` headers, then run **living-doc-gap-finder** to confirm no orphan references remain. ## Script — `validate_entity.py` @@ -225,4 +224,4 @@ python skills/living-doc-update/scripts/validate_entity.py entity.json --catalog python skills/living-doc-update/scripts/validate_entity.py entity.json --profile .copilot/bdd/.project-profile.yaml ``` -Exits 0 if valid (warnings are non-blocking). Exits 1 if any required field is missing, the ID format is wrong, or the status or `surface_type` value is invalid. +Exits 0 if valid (warnings are non-blocking). Exits 1 if any required field is missing, the ID format is wrong, or the `surface_type` value is invalid. diff --git a/skills/living-doc-create-feature/evals/evals.json b/skills/living-doc-create-feature/evals/evals.json index 7a87dff..b28aa07 100644 --- a/skills/living-doc-create-feature/evals/evals.json +++ b/skills/living-doc-create-feature/evals/evals.json @@ -5,7 +5,7 @@ "id": 1, "category": "happy-path", "prompt": "I want to document the Checkout Page as a Feature entity.", - "expected_output": "Agent identifies surface type as UI. Asks for: purpose and scope (what user interactions it owns), linked User Stories, known Functionalities, owners, external dependencies. Runs next_id.py to get the next Feature ID from the catalog. Outputs a canonical Feature JSON with id=FEAT-001 (or next numeric ID), surface_type=UI, at least one US link, at least two Functionality references, owners, and external_dependencies including payment-gateway and order-service.", + "expected_output": "Agent identifies surface type as UI. Asks for: purpose and scope (what user interactions it owns), linked User Stories, known Functionalities, owners, external dependencies. Runs next_id.py to get the next Feature ID from the catalog. Since the prompt supplies no catalog relationship data, outputs a canonical Feature JSON with id=FEAT-001 (or next numeric ID), surface_type=UI, user_stories=[] and functionalities=[] with the orphan/candidate warnings, owners, and external_dependencies including payment-gateway and order-service.", "files": [], "expectations": [ "Identifies surface_type as UI for a web page", @@ -14,6 +14,7 @@ "Asks for Functionalities owned by this Feature", "Asks for owners and external dependencies", "Runs next_id.py to get numeric Feature ID from catalog", + "Does not fabricate user_stories or functionalities links absent catalog evidence", "Notes: 1 Feature ≈ 1 PageObject" ] }, @@ -33,12 +34,12 @@ "id": 3, "category": "regression", "prompt": "My Feature entity has no User Stories linked and no Functionalities. Is that OK?", - "expected_output": "Agent warns: an orphaned Feature with no User Stories and no Functionalities contributes no traceable business value. Either link to at least one User Story (or flag as 'candidate' status if it is exploratory), or delete it if it is no longer relevant. Notes that orphan Features will appear in living-doc-gap-finder reports as a gap.", + "expected_output": "Agent warns: an orphaned Feature with no User Stories and no Functionalities contributes no traceable business value. A Feature has no `status` field to flag it as exploratory with. Either link to at least one User Story once one exists, or delete it if it is no longer relevant. Notes that orphan Features are reported by living-doc-gap-finder as an ORPHAN_FEATURE condition.", "files": [], "expectations": [ "Warns about orphaned Feature", - "Suggests linking to at least one User Story or setting status to 'candidate'", - "Notes orphan Features appear in gap reports" + "Does not suggest a Feature status field (Features have none)", + "Notes orphan Features are reported as an ORPHAN_FEATURE condition in gap reports" ] }, { @@ -68,25 +69,27 @@ "id": 6, "category": "paraphrase", "prompt": "I want to add the Notification Service to the living doc as a system component. Where do I start?", - "expected_output": "Agent identifies this as a Feature entity creation (system surface). Asks: what type of surface is it (API, Worker, UI)? What User Stories does it enable? What Functionalities does it own? Who are the owners? What are the external dependencies (SMTP relay, template store)? Runs next_id.py to get the numeric ID from the catalog. Outputs a canonical Feature JSON with id=FEAT-001 (or next numeric ID), surface_type=Worker or API, at least one User Story link, owners, and external_dependencies.", + "expected_output": "Agent identifies this as a Feature entity creation (system surface). Asks: only UI and API exist — does it expose a REST/GraphQL endpoint with an annotated endpoint method, or a message-broker topic (e.g. Kafka) with an AsyncAPI (or equivalent) annotation on the producer/consumer handler (either is its API contract anchor), or is it purely topic/queue-driven with no such annotation at all? What User Stories does it enable? What Functionalities does it own? Who are the owners? What are the external dependencies (SMTP relay, template store)? If it exposes an annotated endpoint or an annotated event handler, runs next_id.py and outputs a canonical Feature JSON with id=FEAT-001 (or next numeric ID), surface_type=API documenting that contract, owners, and external_dependencies — with user_stories=[] and functionalities=[] plus the orphan/candidate warnings, since the prompt supplies no catalog relationship data. If it is purely topic/queue-driven with no annotation at all, explains it has no UI/API test-abstraction anchor yet and should instead be recorded as an external_dependencies entry on the Features that use it, rather than created as a Feature itself.", "files": [], "expectations": [ "Identifies this as a Feature creation request despite 'system component' phrasing", - "Asks for surface_type (Worker/API for a notification service)", + "Asks for surface_type, noting only UI and API exist", + "Distinguishes an annotated REST/GraphQL endpoint or an AsyncAPI-annotated event handler (surface_type=API) from an unannotated topic/queue-driven contract (no Feature)", "Asks for User Story links, owners, and external dependencies", - "Outputs valid canonical Feature JSON" + "Outputs valid canonical Feature JSON only when an annotated endpoint or event handler exists", + "Does not fabricate a User Story link absent catalog evidence" ] }, { "id": 7, "category": "edge-case", "prompt": "We have a shared utility library used by three different services. Should it be documented as a Feature entity?", - "expected_output": "A shared utility library is not a Feature — it is not a system surface with its own UI, API, or event stream. Document it as an external_dependency in the Feature entities of the services that consume it. If the library is substantial enough to warrant its own living doc entry, create it as a Feature with surface_type=Library and explicitly mark it as a shared internal dependency. Note: Features map 1:1 to deployable/distinct surfaces — shared libraries are infrastructure, not surfaces.", + "expected_output": "A shared utility library is never a Feature — only UI and API surface_type values exist, and a library has neither a screen nor a public API contract. Document it as an external_dependency in the Feature entities of the services that consume it, however substantial the library is. Note: Features map 1:1 to deployable/distinct UI or API surfaces — shared libraries are infrastructure, not surfaces.", "files": [], "expectations": [ - "Advises against creating a Feature for a shared utility library by default", + "Advises against creating a Feature for a shared utility library, regardless of size", "Recommends listing it as external_dependency in consumer Feature entities", - "Notes the surface_type=Library option for substantial libraries", + "Does not offer a surface_type=Library option", "Explains the 1:1 Feature-to-surface mapping rule" ] }, @@ -94,13 +97,14 @@ "id": 8, "category": "output-format", "prompt": "Create a Feature entity for the 'User Profile' screen in our banking app. It is owned by team-identity and depends on the customer-service API.", - "expected_output": "The output contains a single fenced ```json code block with a valid Feature entity. The JSON object includes all required fields: type, id, name, surface_type, purpose, status, user_stories, functionalities, owners, external_dependencies. The id field is a numeric ID from the catalog (e.g. FEAT-001). The surface_type value is one of: UI, API, Service, Worker, Module, Library. No prose appears inside the JSON code block.", + "expected_output": "The output contains a single fenced ```json code block with a valid Feature entity. The JSON object includes all required fields: type, id, name, surface_type, purpose, user_stories, functionalities, owners, external_dependencies. There is no `status` field — a Feature's state is derived from its Functionalities, never authored. The id field is a numeric ID from the catalog (e.g. FEAT-001). The surface_type value is UI or API — this is a screen, so surface_type=UI. No prose appears inside the JSON code block.", "files": [], "expectations": [ "Single fenced ```json code block", - "All required fields present: type, id, name, surface_type, purpose, status, user_stories, functionalities, owners, external_dependencies", + "All required fields present: type, id, name, surface_type, purpose, user_stories, functionalities, owners, external_dependencies", + "No `status` field in the JSON", "id is a numeric ID from the catalog (FEAT-001, FEAT-002, etc.)", - "surface_type is one of UI/API/Service/Worker/Module/Library", + "surface_type is UI or API (surface_type=UI for this screen)", "No prose inside the code block" ] }, @@ -150,14 +154,14 @@ "id": 12, "category": "happy-path", "prompt": "Document a Feature for our PaymentEventProcessor — an asynchronous worker that listens on a Kafka topic and processes payment events in the background.", - "expected_output": "Agent identifies surface_type as Worker (asynchronous/background processor). Purpose describes the business contract: processes payment events asynchronously from the Kafka topic. Runs next_id.py to get the numeric ID from the catalog. Outputs Feature JSON with id=FEAT-001 (or next numeric ID), surface_type=Worker, purpose in business language describing the event processing responsibility, and external_dependencies including the Kafka topic and any downstream services.", + "expected_output": "Agent explains that only UI and API surface types exist, and that a message-broker topic now qualifies as an API contract when its producer/consumer handler carries an AsyncAPI (or equivalent schema-registry) annotation — a bare topic name alone is not a canonical anchor. Agent asks whether PaymentEventProcessor's Kafka consumer handler is documented via AsyncAPI (or equivalent). If yes, agent treats that annotated handler as the API contract anchor and drafts a Feature with surface_type='API' (e.g. 'Payment Event Processor API'). If no such annotation exists yet, agent does NOT create a Feature entity for it, and instead recommends recording the Kafka topic as an external_dependencies entry on the Feature(s) that publish to or consume it, noting that annotating the handler is what would unlock Feature-level tracking.", "files": [], "expectations": [ - "Identifies surface_type as Worker", - "Feature name is a noun phrase: 'Payment Event Processor'", - "Purpose describes the business contract, not the technology", - "external_dependencies includes the Kafka topic or downstream services", - "Outputs valid canonical Feature JSON" + "Recognizes a Kafka/message-broker topic documented via AsyncAPI (or equivalent) as a valid API contract anchor, not just REST/GraphQL", + "Asks whether the producer/consumer handler carries an AsyncAPI (or equivalent) annotation", + "If annotated: drafts a Feature with surface_type=API anchored on that handler", + "If not annotated: does not create a Feature and recommends recording the topic as an external_dependencies entry instead", + "Does not treat the bare topic name alone (unannotated) as a canonical anchor" ] }, { @@ -222,7 +226,7 @@ "expectations": [ "Notes that numeric IDs are stable and should not change", "Directs to update 'name' field only, not the 'id' field", - "Notes feature_registry and Functionality.feature_id must be updated", + "Notes feature_registry and Functionality.parent_feature must be updated", "Recommends gap-finder run at the end to confirm clean state" ] } diff --git a/skills/living-doc-create-feature/evals/fixture-map.md b/skills/living-doc-create-feature/evals/fixture-map.md index 1734693..5b2e98a 100644 --- a/skills/living-doc-create-feature/evals/fixture-map.md +++ b/skills/living-doc-create-feature/evals/fixture-map.md @@ -15,24 +15,33 @@ | 3 | regression | _(none)_ | Orphan Feature warning (no User Stories, no Functionalities) | | 4 | happy-path | _(none)_ | Anti-pattern: verb-phrase Feature name (Process Payment) | | 5 | negative | _(none)_ | Routing: User Story creation → living-doc-create-user-story | -| 6 | paraphrase | _(none)_ | Notification Service — surface type (Worker/API) identification | +| 6 | paraphrase | _(none)_ | Notification Service — surface type (API-only) identification | | 7 | edge-case | _(none)_ | Shared utility library — external_dependency vs Feature entity | | 8 | output-format | _(none)_ | Canonical JSON output: all required fields, FEAT-kebab id, surface_type enum | | 9 | file-based | `raw-feature-notes.md` | Notifications Centre — extract surface from rough notes | | 10 | regression | _(none)_ | Anti-pattern: technology-encoded Feature name (Spring Payment Controller) | | 11 | edge-case | _(none)_ | Anti-pattern: surface_type=UI for a REST controller | -| 12 | happy-path | _(none)_ | Worker surface type: PaymentEventProcessor | +| 12 | happy-path | _(none)_ | Kafka-topic worker (PaymentEventProcessor) — asks for AsyncAPI annotation on the handler; anchors an API Feature if annotated, else declined and recorded as external_dependencies | | 13 | regression | _(none)_ | Candidate Functionalities not formally defined — leave functionalities=[] | | 14 | regression | _(none)_ | Duplicate Feature name conflict resolution | ## Trigger eval summary -18 entries: 13 `should_trigger=true`, 5 `should_trigger=false` +29 entries: 13 `should_trigger=true`, 16 `should_trigger=false` | Routes to | Query count | |---|---| | living-doc-create-user-story | 1 | -| living-doc-create-functionality | 1 | +| living-doc-create-functionality | 2 | | living-doc-pageobject-scan | 1 | -| living-doc-scenario-creator | 1 | -| living-doc-update | 1 | +| living-doc-scenario-creator | 2 | +| living-doc-update | 4 (deprecate, rename, drop a dependency, promote a dependency) | +| gherkin-step | 1 | +| bdd-maintain | 1 | +| living-doc-gap-finder | 1 | +| gherkin-living-doc-sync | 1 | +| living-doc-impact-analysis | 1 | +| data-cy-instrument | 1 | + +Creation vs change: this skill owns a Feature that does not exist yet. Any change to an existing +Feature — owners, dependencies, rename, registry entry, deprecation — routes to `living-doc-update`. diff --git a/skills/living-doc-create-feature/evals/trigger-eval.json b/skills/living-doc-create-feature/evals/trigger-eval.json index 8a37e31..0eb51c6 100644 --- a/skills/living-doc-create-feature/evals/trigger-eval.json +++ b/skills/living-doc-create-feature/evals/trigger-eval.json @@ -27,7 +27,7 @@ "id": 5, "query": "Update the feature registry with the new notification service", "should_trigger": true, - "reason": "'feature registry' trigger keyword" + "reason": "'add feature registry entry' trigger phrase — the entry is for a new surface; changing an existing Feature's registry entry routes to living-doc-update" }, { "id": 6, @@ -87,7 +87,7 @@ "id": 15, "query": "Register the notification background worker in the living doc as a system surface", "should_trigger": true, - "reason": "Documenting a background worker as a system surface — Feature creation (surface_type=Worker)" + "reason": "Documenting a background worker as a system surface — Feature creation workflow triggers to assess whether it has a REST/GraphQL endpoint or an AsyncAPI-annotated event handler (either anchors surface_type=API), or is an unannotated topic/queue worker with no Feature anchor yet (see evals.json id 12)" }, { "id": 16, @@ -99,7 +99,7 @@ "id": 17, "query": "Document the Orders Service — it exposes a REST API to place and cancel orders", "should_trigger": true, - "reason": "Documenting a backend service surface — Feature creation (surface_type=Service or API)" + "reason": "Documenting a backend service surface — Feature creation (surface_type=API)" }, { "id": 18, @@ -158,7 +158,19 @@ { "id": 27, "query": "I need to rename a Feature because the domain structure changed — what are all the cascade steps?", - "should_trigger": true, - "reason": "Feature renaming guidance — create-feature owns the Feature schema and triggers rename/update guidance via living-doc-update" + "should_trigger": false, + "reason": "Renaming an existing Feature — the cascade is owned by living-doc-update ('Rename a Feature'), not by creation" + }, + { + "id": 28, + "query": "Checkout Page no longer calls the fraud-check service — remove that dependency from the living doc.", + "should_trigger": false, + "reason": "Dropping a dependency from an existing Feature — routes to living-doc-update" + }, + { + "id": 29, + "query": "The payment-events topic got an AsyncAPI annotation, so it became a feature. Update every Feature that lists it as an external dependency.", + "should_trigger": false, + "reason": "Promoting an external dependency is a cascade across existing Features — living-doc-update runs it and calls living-doc-create-feature for the new Feature itself" } ] \ No newline at end of file diff --git a/skills/living-doc-create-functionality/SKILL.md b/skills/living-doc-create-functionality/SKILL.md index 40844ac..0763d9d 100644 --- a/skills/living-doc-create-functionality/SKILL.md +++ b/skills/living-doc-create-functionality/SKILL.md @@ -2,17 +2,17 @@ name: living-doc-create-functionality description: > Define an atomic, testable behavior (Functionality) with Acceptance Criteria for unit or - integration tests. Use when writing Functionality-level ACs, - choosing test_type, identifying reuse candidates, or reviewing a Functionality. - Triggers on: "create a functionality", "document an atomic behavior", "functionality AC", - "unit-testable behavior", "define component behavior", "atomic acceptance criteria", - "document a business rule", "create a functionality entity", "functionality acceptance criteria", - "test_type", "unit vs integration test", "choose test type", "link functionality to feature", - "review this functionality", "reuse candidate", "what ACs should I write for". - Does NOT trigger for: E2E User Stories (use living-doc-create-user-story); system - surfaces (use living-doc-create-feature); generating BDD scenarios (use - living-doc-scenario-creator). - Pairs with living-doc-create-feature and living-doc-scenario-creator. After creating, + integration tests: write Functionality-level ACs, choose func_type or test_type, record the + Features it calls, flag reuse candidates, or review a Functionality. + Triggers on: "create a functionality", "document an atomic behavior", "document a business + rule", "functionality AC", "atomic acceptance criteria", "unit-testable behavior", + "define component behavior", "link functionality to feature", "func_type", + "which func_type", "component_state vs button_action", "behavior category", "test_type", + "unit vs integration test", "review this functionality", "reuse candidate", + "what ACs should I write for". + NOT for: E2E User Stories (living-doc-create-user-story), system surfaces + (living-doc-create-feature), BDD scenarios (living-doc-scenario-creator). + Pairs with living-doc-create-feature and living-doc-scenario-creator; after creating, update the parent Feature's functionalities[] array. license: Apache-2.0 --- @@ -28,22 +28,34 @@ Before asking, **scan the conversation context** for a behavior phrase and paren Ask only for what is missing: *What is the atomic behavior to document?* -Express the Functionality `name` as a **verb phrase only** — one atomic responsibility, with no Feature prefix. Keep the owning Feature separate in `feature_id`. +Express the behavior as a **verb phrase** — one atomic responsibility. The Functionality `name` is that phrase prefixed with the parent Feature's name: ` - `, joined by a plain hyphen (never an en or em dash). This is the canon form — see [living-doc-glossary — Functionality](../shared/references/living-doc-glossary.md#functionality-func) — and it is what appears in the feature-file title line and the issue title. The Feature ID itself still goes only in `parent_feature`. ``` -✅ "Validate cart contains at least one in-stock item" -✅ "Apply gold member discount on qualifying orders" -✅ "Deduct voucher discount before tax is calculated" - -❌ "Handle checkout" (too broad — split into multiple Functionalities) -❌ "The payment page" (that is a Feature, not a Functionality) +✅ "Checkout Page - Validate cart contains at least one in-stock item" +✅ "Checkout Page - Apply gold member discount on qualifying orders" +✅ "Checkout Page - Deduct voucher discount before tax is calculated" + +❌ "Validate cart contains at least one in-stock item" (no Feature name prefix) +❌ "Checkout Page — Validate cart contents" (em dash — use a plain hyphen) +❌ "Checkout Page - Handle checkout" (too broad — split into multiple Functionalities) +❌ "The payment page" (that is a Feature, not a Functionality) ``` +Until the parent Feature is known (Step 2), work with the verb phrase alone; add the prefix once the Feature's name is confirmed from the catalog. + ## Step 2 — Identify the parent Feature Ask: *Which Feature (system surface) owns this behavior?* only if it is not already obvious from the prompt. -A Functionality must belong to at least one Feature. If the user clearly names the surface or domain (for example checkout, basket, login, pricing), infer a provisional `feature_id` by asking for the catalog to look up the numeric ID. If the catalog is not available, note that the Feature must be created formally via `living-doc-create-feature` first (which will assign a numeric ID). Do **not** invent a slug-based Feature ID; all Feature IDs are numeric and catalog-assigned. +A Functionality belongs to exactly one parent Feature, recorded in `parent_feature` — the toolkit JSON name for the canon's `# parent:` header key (see [living-doc-bdd-schemas](../shared/references/living-doc-bdd-schemas.md#3-functionality-in-a-gherkin-feature-file)). If the user clearly names the surface or domain (for example checkout, basket, login, pricing), infer a provisional `parent_feature` by asking for the catalog to look up the numeric ID. If the catalog is not available, note that the Feature must be created formally via `living-doc-create-feature` first (which will assign a numeric ID). Do **not** invent a slug-based Feature ID; all Feature IDs are numeric and catalog-assigned. + +### Features this behaviour calls + +Ask: *Does this behaviour call another documented Feature — an API endpoint or an annotated event contract?* + +Record each one as a `FEAT-` id in `feature_dependencies`. The field is authored **here**, on the Functionality that makes the call, and derived upward to the parent Feature — never written on the Feature itself. Which targets are allowed is defined once, in [living-doc-glossary — Functionality](../shared/references/living-doc-glossary.md#functionality-func); read the rules there rather than a copy here. + +A system the behaviour calls that is **not** a documented Feature is not a target. It stays a free-form `external_dependencies` name on the parent Feature, which `living-doc-create-feature` owns. If nothing is called, emit `"feature_dependencies": []`. ## Step 3 — Elicit Functionality-level Acceptance Criteria @@ -67,6 +79,35 @@ Write **3-7 ACs** for one coherent behavior. If a Functionality needs around **1 Warn if only happy-path ACs are present. +### Choosing `func_type` + +`func_type` is **required** on every Functionality. It is not a label applied after the fact: it +decides which PageObject locator the behaviour anchors to, and it drives the one-FUNC-one-cause +scoping rules — so getting it wrong changes how the behaviour must be split, not just how it reads. + +The seven allowed values, what each documents, and its PageObject anchor are tabulated once, in +[living-doc-bdd-schemas — `func_type` values](../shared/references/living-doc-bdd-schemas.md#3-functionality-in-a-gherkin-feature-file); +the scoping rules follow it in the same section. Read the table there — it is not repeated here. + +To pick a value, ask **what causes the behaviour**: + +| The behaviour is caused by… | Value | +|---|---| +| the page simply loading, or a data-bound component rendering a data state | `component_state` | +| an interaction inside one self-contained component, with no discrete control and no system side effect | `component_action` | +| a specific discrete control being triggered (including form submit) | `button_action` | +| a rule enforced on one field's value | `field_validation` | +| a value derived from inputs and displayed without a submission | `calculation` | +| a runtime condition (role, prior action, data presence, config) gating an element | `visibility` | +| a routing decision with its own precondition or business rule | `navigation_rule` | + +Two tie-breaks worth stating explicitly, because they are the usual mistakes: + +- A derived value that only appears **after a submit** is an AC on the `button_action` FUNC, not a + `calculation`. +- A redirect that is always the result of a button press is an AC on that `button_action` FUNC, not + a separate `navigation_rule`. + ### Choosing `test_type` - Use **`unit`** when the behavior can be verified in isolation as a pure calculation, validation rule, or deterministic transformation. @@ -117,19 +158,22 @@ Use this canonical shape: ```json { "type": "Functionality", - "id": "FUNC-", - "name": "", + "id": "FUNC-", + "name": " - ", "description": "", - "feature_id": "FEAT-", + "parent_feature": "FEAT-", + "func_type": "", + "feature_dependencies": ["FEAT-"], "user_stories": ["US-"], "acceptance_criteria": [ - "When , ", - "When , validation returns INVALID with code ", - "When , " + {"id": "AC:FUNC--01", "description": "When , "}, + {"id": "AC:FUNC--02", "description": "When , validation returns INVALID with code "}, + {"id": "AC:FUNC--03", "description": "When , "} ], "test_coverage": [ - {"ac": "AC:FUNC-001-01", "test_type": "unit", "justification": "Pure validation rule"}, - {"ac": "AC:FUNC-001-02", "test_type": "unit", "justification": "Pure validation rule"} + {"ac": "AC:FUNC--01", "test_type": "unit", "justification": "Pure validation rule"}, + {"ac": "AC:FUNC--02", "test_type": "unit", "justification": "Pure validation rule"}, + {"ac": "AC:FUNC--03", "test_type": "unit", "justification": "Pure validation rule"} ], "status": "planned" } @@ -140,24 +184,44 @@ Rules: - If the prompt states an existing catalog range (for example `FUNC-001` through `FUNC-007`), reflect the allocator call in the answer as `Ran: python scripts/next_id.py --type FUNC --catalog catalog.json -> FUNC-008` and use that returned numeric ID in the JSON. - If the prompt explicitly says the catalog is missing / `next_id.py` cannot run for a real save operation, use the stable placeholder `FUNC-PENDING` until an allocator can assign the final ID. - Otherwise, for starter drafts and normal create/document requests where no catalog state is given, use the readable draft form `FUNC-`. -- `name` stays a verb phrase only. +- `name` is ` - `: the parent Feature's name, a plain hyphen, then the atomic verb phrase. +- Each acceptance criterion is an object with `id` and `description`. The `id` is + `AC:-`, numbered from `01` in order — the same ID the AC carries in the + feature-file header or issue body (see [living-doc-glossary — Acceptance Criterion](../shared/references/living-doc-glossary.md#acceptance-criterion-ac)). + Never emit an AC as a bare string: it has no ID, so nothing can link a test or a scenario to it. + When a placeholder ID (`FUNC-`, `FUNC-PENDING`) is replaced, renumber the AC IDs and + the `test_coverage` references with it. - `description` and `acceptance_criteria` must stay in plain business language with **no implementation details**. - Every acceptance criterion must state an exact outcome; error cases must include the explicit error code. -- `test_coverage` must cover every AC and record `unit` or `integration` consistently with Step 3. +- `func_type` is **required** and must be one of the seven allowed values — pick it from what causes + the behaviour, per [Choosing `func_type`](#choosing-func_type). Never omit it and never invent a + value: `validate_entity.py` rejects both, and without it the entity cannot be authored into a + Functionality feature-file header. +- `feature_dependencies` lists the documented Features this behaviour calls, per + [Features this behaviour calls](#features-this-behaviour-calls) — `[]` when it calls none. Never + put a free-form system name in it. +- `test_coverage` must cover every AC — one entry per AC `id`, referenced by that exact ID — and record `unit` or `integration` consistently with Step 3. + +> **`test_coverage` is toolkit-local.** It is a test-planning structure used by this toolkit's +> skills to reason about how each AC will be verified; it is **never authored into documentation** — +> neither `func-*.feature` headers nor issue bodies carry it, and the living-doc pipeline neither +> reads nor emits it. `test_type` lives only inside it, for the same reason (see +> [Choosing `test_type`](#choosing-test_type)). Starter example for a normal draft: ```json { "type": "Functionality", "id": "FUNC-001", - "name": "Apply gold member discount on qualifying orders", + "name": "Checkout Page - Apply gold member discount on qualifying orders", "description": "Applies the gold-member discount when the order satisfies the minimum qualifying threshold.", - "feature_id": "FEAT-pricing", + "parent_feature": "FEAT-001", + "func_type": "calculation", "user_stories": ["US-001"], "acceptance_criteria": [ - "When the customer is a gold member and the order total is greater than £50, the discount applied is 20% of the subtotal.", - "When the order total is exactly £50, no gold-member discount is applied.", - "When the customer is not a gold member, no gold-member discount is applied." + {"id": "AC:FUNC-001-01", "description": "When the customer is a gold member and the order total is greater than £50, the discount applied is 20% of the subtotal."}, + {"id": "AC:FUNC-001-02", "description": "When the order total is exactly £50, no gold-member discount is applied."}, + {"id": "AC:FUNC-001-03", "description": "When the customer is not a gold member, no gold-member discount is applied."} ], "test_coverage": [ {"ac": "AC:FUNC-001-01", "test_type": "unit", "justification": "Pure pricing rule"}, @@ -173,15 +237,16 @@ Fallback example when the prompt explicitly says the catalog is missing: { "type": "Functionality", "id": "FUNC-PENDING", - "name": "Validate discount code expiry", + "name": "Checkout Page - Validate discount code expiry", "description": "Rejects expired discount codes before they are applied to the order.", - "feature_id": "FEAT-001", + "parent_feature": "FEAT-001", + "func_type": "field_validation", "user_stories": ["US-001"], "acceptance_criteria": [ - "When the discount code is expired, validation returns INVALID with code DISCOUNT_EXPIRED." + {"id": "AC:FUNC-PENDING-01", "description": "When the discount code is expired, validation returns INVALID with code DISCOUNT_EXPIRED."} ], "test_coverage": [ - {"ac": "AC:FUNC-002-01", "test_type": "unit", "justification": "Pure date comparison rule"} + {"ac": "AC:FUNC-PENDING-01", "test_type": "unit", "justification": "Pure date comparison rule"} ], "status": "planned" } @@ -191,11 +256,12 @@ Fallback example when the prompt explicitly says the catalog is missing: > **Parent Feature sync:** After saving this entity, load `living-doc-update` and append this `FUNC-` to the parent Feature's `"functionalities"` array. An unlinked Functionality will be flagged as `ORPHAN_FUNCTIONALITY` by `living-doc-gap-finder`. -Common create requests that should produce JSON immediately: -- cart validation rule ("Validate cart contains at least one in-stock item") → explicitly ask: *What happens when the cart is empty? when all items are out of stock? when only some items are in stock? when an item has zero quantity?* Then emit JSON with those cases covered, including an explicit zero-quantity error such as `INVALID_QUANTITY`; keep all ACs as `unit` when validating an already-provided cart snapshot -- discount rule ("Apply 20% discount to gold member orders over £50") → infer `feature_id: "FEAT-pricing"` or the named parent Feature, ask about non-gold members, exactly £50, under £50, and promo-code stacking, then emit the full JSON **including a stacking/precedence AC in the `acceptance_criteria` array** -- voucher calculation rule ("Deduct voucher discount before tax is calculated") → ask which Feature (checkout/pricing) owns this, or ask for the catalog to look up the Feature ID, then emit the full JSON -- checkout stock rule ("Reject order when all items are out of stock") → ask which Feature (checkout) owns this, or ask for the catalog to look up the Feature ID, then emit the full JSON +Common create requests that should produce JSON immediately (each names the `func_type` that fits +its behaviour — never carry one value across from the previous example): +- cart validation rule ("Validate cart contains at least one in-stock item") → `func_type: "field_validation"` (a rule enforced on the cart's contents); explicitly ask: *What happens when the cart is empty? when all items are out of stock? when only some items are in stock? when an item has zero quantity?* Then emit JSON with those cases covered, including an explicit zero-quantity error such as `INVALID_QUANTITY`; keep all ACs as `unit` when validating an already-provided cart snapshot +- discount rule ("Apply 20% discount to gold member orders over £50") → `func_type: "calculation"` (a value derived from the order); look up the pricing (or named) Feature's numeric ID in the catalog for `parent_feature`, ask about non-gold members, exactly £50, under £50, and promo-code stacking, then emit the full JSON **including a stacking/precedence AC in the `acceptance_criteria` array** +- voucher calculation rule ("Deduct voucher discount before tax is calculated") → `func_type: "calculation"` (an ordering rule on a derived total); ask which Feature (checkout/pricing) owns this, or ask for the catalog to look up the Feature ID, then emit the full JSON +- checkout stock rule ("Reject order when all items are out of stock") → `func_type: "button_action"` if the rejection is observable only after the order is submitted, `field_validation` if the cart itself blocks first; ask which Feature (checkout) owns this, or ask for the catalog to look up the Feature ID, then emit the full JSON For the gold-member discount pattern, first write a short `Completeness checklist:` line that explicitly mentions non-gold members, exactly £50, under £50, and promo-code stacking, then end with the full JSON artifact, not a sentence saying to output JSON later. @@ -204,16 +270,17 @@ Gold-member discount starter draft example: { "type": "Functionality", "id": "FUNC-003", - "name": "Apply gold member discount on qualifying orders", + "name": "Checkout Page - Apply gold member discount on qualifying orders", "description": "Applies the gold-member discount when an order meets the qualifying threshold.", - "feature_id": "FEAT-001", + "parent_feature": "FEAT-001", + "func_type": "calculation", "user_stories": ["US-001"], "acceptance_criteria": [ - "When the customer is a gold member and the order total is greater than £50, a 20% discount is applied to the order subtotal.", - "When the customer is a gold member and the order total is exactly £50, the threshold outcome is applied exactly as specified by the business rule.", - "When the customer is a gold member and the order total is below £50, no gold-member discount is applied.", - "When the customer is not a gold member, no gold-member discount is applied regardless of order total.", - "When a promo code is combined with the gold-member discount, the stacking or precedence rule is applied exactly as specified." + {"id": "AC:FUNC-003-01", "description": "When the customer is a gold member and the order total is greater than £50, a 20% discount is applied to the order subtotal."}, + {"id": "AC:FUNC-003-02", "description": "When the customer is a gold member and the order total is exactly £50, the threshold outcome is applied exactly as specified by the business rule."}, + {"id": "AC:FUNC-003-03", "description": "When the customer is a gold member and the order total is below £50, no gold-member discount is applied."}, + {"id": "AC:FUNC-003-04", "description": "When the customer is not a gold member, no gold-member discount is applied regardless of order total."}, + {"id": "AC:FUNC-003-05", "description": "When a promo code is combined with the gold-member discount, the stacking or precedence rule is applied exactly as specified."} ], "test_coverage": [ {"ac": "AC:FUNC-003-01", "test_type": "unit", "justification": "Pure pricing rule"}, @@ -241,7 +308,7 @@ python skills/living-doc-update/scripts/validate_entity.py entity.json --catalog python skills/living-doc-update/scripts/validate_entity.py entity.json --profile .copilot/bdd/.project-profile.yaml ``` -Exits 0 if valid (warnings are non-blocking). Exits 1 if any required field is missing, the ID format is wrong, `parent_feature` does not match `FEAT-*`, or the status value is invalid. +Exits 0 if valid (warnings are non-blocking). Exits 1 if any required field is missing, the ID format is wrong, `parent_feature` does not match `FEAT-*`, `func_type` is outside the seven canonical values, or the status value is invalid. With `--catalog` it also rejects a `feature_dependencies` target that is not an `API` Feature or is the Functionality's own Feature, and warns on one that is missing from the catalog or deprecated. ## Distinguishing Functionality ACs from User Story ACs @@ -258,7 +325,8 @@ redirect to `living-doc-create-user-story`. | Anti-pattern | Warning | |---|---| -| Functionality name is a noun (e.g. "Password Validation") | Names must be verb phrases expressing the atomic behavior — e.g. "Validate Password Strength". | +| Functionality name is a noun (e.g. "Password Validation") | The behavior part of the name must be a verb phrase expressing the atomic behavior — e.g. "Login Page - Validate Password Strength". | +| Functionality name has no Feature prefix (e.g. "Validate Password Strength") | Canon names a Functionality ` - ` — prefix the parent Feature's name with a plain hyphen: "Login Page - Validate Password Strength". `validate_entity.py` warns on a name without the separator. | | Functionality name is broad (e.g. "Handle checkout") | That is not atomic. Split it into smaller behaviors such as validation, pricing, payment authorization, or order submission. | | Functionality AC describes a full user journey (e.g. "User logs in and sees their dashboard") | That is a User Story AC — redirect to **living-doc-create-user-story**. Functionality ACs describe a single behavior's input to output or side effect. | | Functionality has only happy-path ACs | Edge cases (null input, boundary values, partial validity, error codes) are missing. Run through the completeness checklist in Step 3 before confirming. | diff --git a/skills/living-doc-create-functionality/evals/evals.json b/skills/living-doc-create-functionality/evals/evals.json index 1cf44fc..c4f4eb5 100644 --- a/skills/living-doc-create-functionality/evals/evals.json +++ b/skills/living-doc-create-functionality/evals/evals.json @@ -5,15 +5,16 @@ "id": 1, "category": "happy-path", "prompt": "I want to document the atomic behavior: validate that a cart contains at least one in-stock item before checkout.", - "expected_output": "Agent forms verb-phrase name: 'Validate cart contains at least one in-stock item'. Asks which Feature (Checkout Page / FEAT-001 or equivalent) owns this behavior. Runs completeness checklist: asks about null/empty cart, out-of-stock items, partially in-stock carts, zero-quantity items. Produces at least 4 ACs: empty cart → CART_EMPTY error, all items out of stock → OUT_OF_STOCK error, valid cart → VALID, zero-quantity item → INVALID_QUANTITY. All test_type=unit. Outputs canonical Functionality JSON with feature_id set to the numeric Feature ID.", + "expected_output": "Agent forms the name ' - ': 'Checkout Page - Validate cart contains at least one in-stock item' (or the confirmed Feature's name as prefix). Asks which Feature (Checkout Page / FEAT-001 or equivalent) owns this behavior. Runs completeness checklist: asks about null/empty cart, out-of-stock items, partially in-stock carts, zero-quantity items. Produces at least 4 ACs: empty cart → CART_EMPTY error, all items out of stock → OUT_OF_STOCK error, valid cart → VALID, zero-quantity item → INVALID_QUANTITY. All test_type=unit. Outputs canonical Functionality JSON with parent_feature set to the numeric Feature ID. The emitted JSON carries func_type='field_validation' — the behaviour is a rule enforced on the cart's contents, not a derived value or a control press.", "files": [], "expectations": [ - "Name is a verb phrase (not a noun)", + "Name is ' - ' with a plain hyphen; the behavior part is a verb phrase (not a noun)", "Asks about empty cart, out-of-stock, and boundary conditions", "Runs the completeness checklist", "Produces at least 3 ACs covering error codes explicitly", "All ACs have test_type=unit", - "Outputs valid canonical Functionality JSON" + "Outputs valid canonical Functionality JSON", + "Emitted JSON includes func_type, set to field_validation" ] }, { @@ -71,14 +72,15 @@ "id": 6, "category": "paraphrase", "prompt": "I need to capture the business rule: a gold member gets a 20% discount on all orders over £50. How do I document this in the living doc?", - "expected_output": "Agent identifies this as a Functionality entity. Forms verb-phrase name: 'Apply gold member discount on qualifying orders'. Runs completeness checklist: asks about non-gold members, orders exactly £50 (boundary), orders under £50, combination with promo codes. Produces ACs: order>£50 and gold member → 20% discount applied; order<=£50 → no discount; non-gold member → no discount; combined with promo → define stacking rule. All ACs test_type=unit. Outputs canonical Functionality JSON.", + "expected_output": "Agent identifies this as a Functionality entity. Forms the name ' - Apply gold member discount on qualifying orders' (e.g. 'Checkout Page - Apply gold member discount on qualifying orders'). Runs completeness checklist: asks about non-gold members, orders exactly £50 (boundary), orders under £50, combination with promo codes. Produces ACs: order>£50 and gold member → 20% discount applied; order<=£50 → no discount; non-gold member → no discount; combined with promo → define stacking rule. All ACs test_type=unit. Outputs canonical Functionality JSON. The emitted JSON carries func_type='calculation' — a discount is a value derived from the order and displayed independently of a submission.", "files": [], "expectations": [ "Identifies as Functionality (atomic business rule), not User Story", - "Verb-phrase name captures the business rule precisely", + "Name is ' - ' and the verb phrase captures the business rule precisely", "Boundary value at £50 is included in the completeness checklist", "At least 3 ACs with explicit expected outcomes", - "Outputs valid canonical Functionality JSON" + "Outputs valid canonical Functionality JSON", + "Emitted JSON includes func_type, set to calculation" ] }, { @@ -98,26 +100,27 @@ "id": 8, "category": "output-format", "prompt": "Document the atomic behavior: 'The system deducts the applied voucher discount from the basket total before tax is calculated.'", - "expected_output": "The output contains a fenced ```json code block with a Functionality entity. Required fields: type ('Functionality'), id (FUNC-), name, description, feature_id, acceptance_criteria (array), test_coverage (array), status. Acceptance criteria items are phrased in plain business English. No implementation details appear in the JSON values.", + "expected_output": "The output contains a fenced ```json code block with a Functionality entity. Required fields: type ('Functionality'), id (FUNC-), name, description, parent_feature, acceptance_criteria (array of objects, each with id AC:- and description), test_coverage (array, one entry per AC id), status. Acceptance criteria items are phrased in plain business English. No implementation details appear in the JSON values. Required fields also include func_type, one of the seven canonical values (component_state, component_action, button_action, field_validation, calculation, visibility, navigation_rule) — here calculation, since the voucher deduction is a derived basket total.", "files": [], "expectations": [ "Single fenced ```json code block", "type field is 'Functionality'", "id follows FUNC- convention", - "acceptance_criteria is an array of business-language strings", + "acceptance_criteria is an array of objects with id (AC:-) and a business-language description", "test_coverage array covers every AC with unit or integration classification", - "No implementation details in JSON values" + "No implementation details in JSON values", + "func_type is present and is one of the seven canonical values (calculation here)" ] }, { "id": 9, "category": "regression", "prompt": "I want to document a Functionality called 'Password Validation'. Is that a good name?", - "expected_output": "'Password Validation' is a noun phrase — Functionality names must be verb phrases expressing what the behavior does. Rename it to 'Validate Password Strength' or 'Validate Password Against Policy'. The noun phrase 'Password Validation' names a module or component, which would be a Feature, not a Functionality. Correct format: verb + object, describing the single atomic action the Functionality performs.", + "expected_output": "'Password Validation' is a noun phrase — Functionality names must be verb phrases expressing what the behavior does. Rename it to ' - Validate Password Strength' (e.g. 'Login Page - Validate Password Strength') or ' - Validate Password Against Policy'. The noun phrase 'Password Validation' names a module or component, which would be a Feature, not a Functionality. Correct format: verb + object, describing the single atomic action the Functionality performs.", "files": [], "expectations": [ "Flags 'Password Validation' as a noun phrase anti-pattern", - "Provides a correct verb-phrase rename (e.g. 'Validate Password Strength')", + "Provides a correct rename in the form ' - ' (e.g. 'Login Page - Validate Password Strength')", "Explains: Functionality names must be verb phrases", "Notes noun phrases describe modules/components (Features), not behaviors" ] @@ -126,24 +129,25 @@ "id": 10, "category": "happy-path", "prompt": "I need to document this atomic behavior for the checkout domain: 'Reject order when all items in the cart are out of stock'. The Feature hasn't been created yet.", - "expected_output": "Agent forms verb-phrase name: 'Reject order when all items are out of stock'. Asks which Feature (Checkout Page / FEAT-001 or equivalent) owns this behavior. Runs completeness checklist: boundary conditions (all items out of stock vs. some items out of stock), partial cart availability, zero-quantity items. Notes that the parent Feature must already exist in the catalog with a numeric ID. Outputs canonical Functionality JSON with feature_id set to the correct numeric ID.", + "expected_output": "Agent forms the name ' - ': 'Checkout Page - Reject order when all items are out of stock' (or the confirmed Feature's name as prefix). Asks which Feature (Checkout Page / FEAT-001 or equivalent) owns this behavior. Runs completeness checklist: boundary conditions (all items out of stock vs. some items out of stock), partial cart availability, zero-quantity items. Notes that the parent Feature must already exist in the catalog with a numeric ID. Outputs canonical Functionality JSON with parent_feature set to the correct numeric ID. The emitted JSON carries a func_type fitting the behaviour: button_action if the rejection is observable only once the order is submitted, field_validation if the cart blocks first. The agent states which it chose and why.", "files": [], "expectations": [ "Asks for the parent Feature ID (numeric, from catalog) instead of inventing a slug", - "Forms a verb-phrase name", + "Forms a ' - ' name", "Runs completeness checklist: all out of stock vs. partial", "Notes that the parent Feature must already exist in the catalog with a numeric ID", - "Outputs valid Functionality JSON with feature_id populated" + "Outputs valid Functionality JSON with parent_feature populated", + "Emitted JSON includes func_type with a stated justification for the value chosen" ] }, { "id": 11, "category": "regression", "prompt": "A Functionality has no parent Feature — I forgot to link it. Is that a problem?", - "expected_output": "Yes. A Functionality without a parent Feature is untraceable — it cannot be reached via the entity hierarchy, will be surfaced as an ORPHAN_FUNCTIONALITY gap by living-doc-gap-finder, and will be missed in impact analyses. Identify or create the owning Feature first (living-doc-create-feature), then set feature_id in the Functionality entity. Do not leave feature_id empty or null in a finalized entity.", + "expected_output": "Yes. A Functionality without a parent Feature is untraceable — it cannot be reached via the entity hierarchy, will be surfaced as an ORPHAN_FUNCTIONALITY gap by living-doc-gap-finder, and will be missed in impact analyses. Identify or create the owning Feature first (living-doc-create-feature), then set parent_feature in the Functionality entity. Do not leave parent_feature empty or null in a finalized entity.", "files": [], "expectations": [ - "Flags missing feature_id as a traceability problem", + "Flags missing parent_feature as a traceability problem", "Names the gap type: ORPHAN_FUNCTIONALITY", "Notes it will be missed in impact analyses", "Directs to living-doc-create-feature to create the parent Feature first" @@ -166,27 +170,29 @@ "id": 14, "category": "regression", "prompt": "Create a new Functionality entity 'Validate discount code expiry'. The catalog already contains FUNC-001 through FUNC-007.", - "expected_output": "Before assigning an ID, the agent runs: python scripts/next_id.py --type FUNC --catalog catalog.json. The script returns FUNC-008. The agent assigns id='FUNC-008'. It does NOT invent an ID, reuse an existing one, or leave a placeholder such as FUNC-XXX or FUNC-. The final JSON has a fully populated id field before being presented to the user.", + "expected_output": "Before assigning an ID, the agent runs: python scripts/next_id.py --type FUNC --catalog catalog.json. The script returns FUNC-008. The agent assigns id='FUNC-008'. It does NOT invent an ID, reuse an existing one, or leave a placeholder such as FUNC-XXX or FUNC-. The final JSON has a fully populated id field before being presented to the user. The JSON also carries func_type='field_validation' — an expiry rule enforced on the discount-code field.", "files": [], "expectations": [ "Runs next_id.py --type FUNC before assigning the ID", "Assigns the ID returned by the script (e.g. FUNC-008)", "Does not invent, guess, or reuse an ID", "Does not leave a placeholder (FUNC-XXX, FUNC-, FUNC-unknown)", - "Final JSON has a fully populated id field" + "Final JSON has a fully populated id field", + "Emitted JSON includes func_type, set to field_validation" ] }, { "id": 15, "category": "regression", "prompt": "Create a Functionality entity but the catalog file is missing — next_id.py cannot be run. What should the agent do?", - "expected_output": "Agent cannot auto-assign an ID. It outputs the entity with id='FUNC-PENDING' and explicitly states: 'Catalog not available — ID could not be assigned. Run next_id.py --type FUNC once the catalog is present and update this field before saving.' It does NOT invent a numeric ID such as FUNC-001 without catalog evidence. The placeholder makes the gap visible rather than hiding it with a guessed value.", + "expected_output": "Agent cannot auto-assign an ID. It outputs the entity with id='FUNC-PENDING' and explicitly states: 'Catalog not available — ID could not be assigned. Run next_id.py --type FUNC once the catalog is present and update this field before saving.' It does NOT invent a numeric ID such as FUNC-001 without catalog evidence. The placeholder makes the gap visible rather than hiding it with a guessed value. The entity still carries a real func_type ('field_validation') — an unavailable catalog blocks ID assignment only, never the behaviour category.", "files": [], "expectations": [ "Uses FUNC-PENDING placeholder when catalog is unavailable", "Explicitly warns the user that the ID must be assigned before saving", "Does not invent a numeric ID without catalog evidence", - "Placeholder is visibly distinct — not a real FUNC-nnn value" + "Placeholder is visibly distinct — not a real FUNC-nnn value", + "func_type is populated even though the ID is a placeholder" ] }, { @@ -213,6 +219,32 @@ "Flags lack of explicit outcome as non-testable", "Provides a rewritten AC with an explicit error code" ] + }, + { + "id": 17, + "category": "happy-path", + "prompt": "What func_type should I use for a Functionality that shows the 'Delete' button only to the record's owner?", + "expected_output": "Agent answers func_type='visibility' — an element's presence gated on a runtime condition (here the viewer's role relative to the record). It distinguishes visibility from component_state (always true on load) and from component_action (a response to an interaction), and notes that the owning condition is descriptive context in the AC, not a separate field. It points at the func_type values table in living-doc-bdd-schemas rather than restating the whole table.", + "files": [], + "expectations": [ + "Recommends visibility", + "Distinguishes visibility from component_state and component_action", + "Notes the condition is AC context, not a separate field", + "References the func_type values table in living-doc-bdd-schemas instead of restating it" + ] + }, + { + "id": 18, + "category": "regression", + "prompt": "Here is my Functionality entity with \"func_type\": \"form_validation\". Is it valid?", + "expected_output": "No. 'form_validation' is not one of the seven canonical values. The agent names the correct one — field_validation for a rule enforced on a field's value, or button_action if the behaviour is the outcome of pressing submit — and states that validate_entity.py reports an out-of-vocabulary func_type as an error (exit 1), because the value is authored verbatim into a '# func_type:' header line where only the seven are valid. It does not silently accept or auto-correct the value without saying so.", + "files": [], + "expectations": [ + "Rejects form_validation as outside the seven canonical values", + "Names field_validation (or button_action) as the correct value, with the distinction", + "States that validate_entity.py reports this as an error, not a warning", + "Does not invent an eighth value or silently accept the input" + ] } ] -} \ No newline at end of file +} diff --git a/skills/living-doc-create-functionality/evals/fixture-map.md b/skills/living-doc-create-functionality/evals/fixture-map.md index 32e3c02..0307482 100644 --- a/skills/living-doc-create-functionality/evals/fixture-map.md +++ b/skills/living-doc-create-functionality/evals/fixture-map.md @@ -18,7 +18,7 @@ | 6 | paraphrase | _(none)_ | Gold member discount business rule — Functionality elicitation | | 7 | edge-case | _(none)_ | 12 ACs → non-atomic scope signal; recommend split | | 8 | output-format | _(none)_ | Canonical Functionality JSON: all required fields, test_coverage array | -| 9 | regression | _(none)_ | Anti-pattern: noun name ('Password Validation') → verb phrase required | +| 9 | regression | _(none)_ | Anti-pattern: noun name ('Password Validation') → ' - ' required | | 10 | happy-path | _(none)_ | Feature inference from context ('checkout domain') | | 11 | regression | _(none)_ | Missing parent Feature: ORPHAN_FUNCTIONALITY anti-pattern | | 12 | edge-case | _(none)_ | Vague AC ('validates') — non-testable; rewrite with explicit When/Then + error code | diff --git a/skills/living-doc-create-functionality/evals/trigger-eval.json b/skills/living-doc-create-functionality/evals/trigger-eval.json index 36752c3..5d4083a 100644 --- a/skills/living-doc-create-functionality/evals/trigger-eval.json +++ b/skills/living-doc-create-functionality/evals/trigger-eval.json @@ -33,7 +33,7 @@ "id": 6, "query": "Create a functionality entity for the payment retry logic", "should_trigger": true, - "reason": "'create a functionality entity' trigger phrase" + "reason": "'create a functionality' trigger phrase" }, { "id": 7, @@ -105,7 +105,7 @@ "id": 18, "query": "Help me choose test type for the loyalty points calculation — it calls no external services", "should_trigger": true, - "reason": "'choose test type' trigger phrase" + "reason": "'test_type' / 'unit vs integration test' trigger phrases" }, { "id": 19, diff --git a/skills/living-doc-create-user-story/SKILL.md b/skills/living-doc-create-user-story/SKILL.md index 1e9b891..f923155 100644 --- a/skills/living-doc-create-user-story/SKILL.md +++ b/skills/living-doc-create-user-story/SKILL.md @@ -27,9 +27,9 @@ Before asking, **scan the conversation context** for an actor, capability, or bu When the user asks to **create**, **document**, or **draft** a User Story, do not stop at elicitation questions alone. Ask the missing questions first as three distinct numbered questions (**Who? What? Why?**), then explicitly ask **Which Feature(s) does this story touch?**, then emit a starter User Story JSON draft in the same reply. For bare prompts like *"Create a new User Story for password reset"* or *"I want to create a new User Story for the password reset capability"*, do **not** wait for confirmation before drafting — assume a sensible starter narrative and output the JSON immediately after the questions. If the user names a common capability but leaves some details implicit, infer a sensible starter draft instead of blocking. Common patterns: -- password reset → actor usually `registered customer`; likely Feature `FEAT-001` (login); ask the three narrative questions plus the Feature question, then still output starter JSON immediately with at least one happy-path AC and 2 error-path ACs +- password reset → actor usually `registered customer`; likely touches the login Feature — use its catalog ID only if the catalog already has one, otherwise note it as `[NEW: Login]` per Step 2; ask the three narrative questions plus the Feature question, then still output starter JSON immediately with at least one happy-path AC and 2 error-path ACs - password reset via SMS → add `[NEW: SMS Authentication]` or the known SMS/login Feature if not yet created -- customer service agent views customer order history → actor `customer service agent`; likely Feature `FEAT-order-management`; include customer-not-found and permission error ACs +- customer service agent views customer order history → actor `customer service agent`; likely touches an order-management Feature — use its catalog ID only if the catalog already has one, otherwise note it as `[NEW: Order Management]` per Step 2; include customer-not-found and permission error ACs - two different actors in one narrative → split into two separate User Stories and call out that shared Functionalities can be linked to both For create flows, explicitly note that if the user provides only happy-path ACs, you will warn them and add at least one failure or alternative-path AC before treating the story as ready. @@ -145,9 +145,9 @@ When creating a new User Story, output **one fenced `json` code block** using th ```json { - "type": "UserStory", + "type": "User Story", "id": "US-001", - "title": "Reset password via SMS", + "name": "Reset password via SMS", "status": "planned", "as_a": "registered customer", "i_want": "reset my password via SMS", @@ -171,7 +171,7 @@ When creating a new User Story, output **one fenced `json` code block** using th ``` Rules: -- Use `title` rather than `name` +- Use `type: "User Story"` (with the space) and `name` — matching `validate_entity.py`'s `REQUIRED_FIELDS`/entity-type vocabulary, the same as Feature and Functionality - Use `as_a`, `i_want`, and `so_that` - Every AC object must have `id` in `AC:US--` format and a plain-language `description` - Write AC descriptions in plain language — no structured language keywords in JSON values @@ -181,9 +181,9 @@ Rules: Starter example for a create request: ```json { - "type": "UserStory", + "type": "User Story", "id": "US-001", - "title": "Reset password via SMS", + "name": "Reset password via SMS", "status": "planned", "as_a": "registered customer", "i_want": "reset my password via SMS", @@ -208,7 +208,7 @@ Starter example for a create request: ``` For password-reset stories, the starter JSON should already include error-path ACs such as unregistered contact detail and expired/already-used reset token or code. -For a generic password-reset prompt with no channel specified, default the starter draft to `title: "Reset password"`, `features: ["FEAT-001"]`, and still emit the JSON immediately after the four questions. +For a generic password-reset prompt with no channel specified, default the starter draft to `name: "Reset password"`, `features: ["FEAT-"]` if the catalog already has a Login Feature, otherwise `features: ["[NEW: Login]"]` per line 30, and still emit the JSON immediately after the four questions. > **Next steps after creation:** The User Story is created with `status: "planned"`. When all ACs are finalised and at least one Feature is linked, use `living-doc-update` to promote it to `active`. After promotion, use `living-doc-scenario-creator` to generate BDD feature files for each `active` AC. diff --git a/skills/living-doc-create-user-story/evals/evals.json b/skills/living-doc-create-user-story/evals/evals.json index 8aeb8da..6426fae 100644 --- a/skills/living-doc-create-user-story/evals/evals.json +++ b/skills/living-doc-create-user-story/evals/evals.json @@ -13,7 +13,7 @@ "Asks which Feature(s) this story touches", "Elicits at least one error-path AC (e.g. unregistered email, expired token)", "Warns if only happy-path AC is provided", - "Outputs valid canonical UserStory JSON" + "Outputs valid canonical User Story JSON" ] }, { @@ -82,7 +82,7 @@ "Forms As-a/I-can/so-that narrative", "Elicits at least 3 ACs covering happy path and error paths", "Asks actor, capability, and business value as distinct questions", - "Outputs valid canonical UserStory JSON" + "Outputs valid canonical User Story JSON" ] }, { @@ -102,11 +102,11 @@ "id": 8, "category": "output-format", "prompt": "Create a user story: 'A customer service agent needs to view the full order history for any customer to resolve disputes.'", - "expected_output": "The output contains a fenced ```json code block with a UserStory entity. Required fields: type ('UserStory'), id (US-), title, as_a, i_want, so_that, acceptance_criteria (array of objects with id and description). The as_a/i_want/so_that fields follow the standard user story template. Each AC has a unique id in the format AC:US--. No Gherkin syntax appears inside the JSON values.", + "expected_output": "The output contains a fenced ```json code block with a User Story entity. Required fields: type ('User Story'), id (US-), name, as_a, i_want, so_that, acceptance_criteria (array of objects with id and description). The as_a/i_want/so_that fields follow the standard user story template. Each AC has a unique id in the format AC:US--. No Gherkin syntax appears inside the JSON values.", "files": [], "expectations": [ "Single fenced ```json code block", - "type field is 'UserStory'", + "type field is 'User Story'", "id follows US- convention", "as_a/i_want/so_that follow standard template", "Each AC has id in AC:US-- format", diff --git a/skills/living-doc-create-user-story/evals/files/incomplete-user-story.json b/skills/living-doc-create-user-story/evals/files/incomplete-user-story.json index 4826bef..9015202 100644 --- a/skills/living-doc-create-user-story/evals/files/incomplete-user-story.json +++ b/skills/living-doc-create-user-story/evals/files/incomplete-user-story.json @@ -1,13 +1,11 @@ { - "type": "UserStory", + "type": "User Story", "id": "US-042", - "title": "Reset account password", - "status": "draft", - "narrative": { - "as_a": "registered customer", - "i_can": "reset my password using my email address", - "so_that": "I can regain access to my account if I forget my password" - }, + "name": "Reset account password", + "status": "planned", + "as_a": "registered customer", + "i_want": "reset my password using my email address", + "so_that": "I can regain access to my account if I forget my password", "features": ["FEAT-001"], "acceptance_criteria": [ { diff --git a/skills/living-doc-create-user-story/evals/fixture-map.md b/skills/living-doc-create-user-story/evals/fixture-map.md index df18fc3..e079fec 100644 --- a/skills/living-doc-create-user-story/evals/fixture-map.md +++ b/skills/living-doc-create-user-story/evals/fixture-map.md @@ -17,7 +17,7 @@ | 5 | negative | _(none)_ | Routing: atomic behavior → living-doc-create-functionality | | 6 | paraphrase | _(none)_ | SMS password reset — full elicitation with happy path + error paths | | 7 | edge-case | _(none)_ | Two actors for same capability → two separate User Stories | -| 8 | output-format | _(none)_ | Canonical UserStory JSON: as_a/i_want/so_that, AC:US-- format | +| 8 | output-format | _(none)_ | Canonical User Story JSON: as_a/i_want/so_that, AC:US-- format | | 9 | regression | _(none)_ | Anti-pattern: 'I want' clause with 'and' — two capabilities bundled | | 10 | regression | _(none)_ | Anti-pattern: single-value placeholder {error type} | | 11 | regression | _(none)_ | Anti-pattern: non-observable outcome (background job) | diff --git a/skills/living-doc-gap-finder/evals/files/catalog-snapshot.json b/skills/living-doc-gap-finder/evals/files/catalog-snapshot.json index f5765aa..73c7149 100644 --- a/skills/living-doc-gap-finder/evals/files/catalog-snapshot.json +++ b/skills/living-doc-gap-finder/evals/files/catalog-snapshot.json @@ -2,9 +2,9 @@ "generated_at": "2026-05-15T08:00:00Z", "catalog": { "user_stories": [ - {"id": "US-001", "title": "Place an online order", "status": "ready", "ac_count": 3}, - {"id": "US-002", "title": "View order history", "status": "ready", "ac_count": 2}, - {"id": "US-007", "title": "Apply a promotional discount", "status": "ready", "ac_count": 4} + {"id": "US-001", "title": "Place an online order", "status": "active", "ac_count": 3}, + {"id": "US-002", "title": "View order history", "status": "active", "ac_count": 2}, + {"id": "US-007", "title": "Apply a promotional discount", "status": "active", "ac_count": 4} ], "features": [ {"id": "FEAT-001", "name": "Checkout Page", "user_stories": ["US-001"]}, diff --git a/skills/living-doc-impact-analysis/SKILL.md b/skills/living-doc-impact-analysis/SKILL.md index 8eea29b..c9399db 100644 --- a/skills/living-doc-impact-analysis/SKILL.md +++ b/skills/living-doc-impact-analysis/SKILL.md @@ -171,9 +171,9 @@ IMPACT MAP — PR #217: "Refactor promo validation to support stacked discounts" User Stories: US-042 (high impact), US-067 (high impact), US-089 (medium impact) ACs requiring review: - AC:US-042-01 — Happy path: single promo applied correctly - AC:US-042-03 — Stacked promos applied in priority order ← NEW BEHAVIOUR - AC:US-067-02 — Expired promo returns 422 + AC:US-042-01 - Happy path: single promo applied correctly + AC:US-042-03 - Stacked promos applied in priority order ← NEW BEHAVIOUR + AC:US-067-02 - Expired promo returns 422 Recommended actions: 1. Update living-doc: add AC for stacked discount priority order (AC:US-042-03 is new) diff --git a/skills/living-doc-pageobject-scan/SKILL.md b/skills/living-doc-pageobject-scan/SKILL.md index 3f1160d..4cae411 100644 --- a/skills/living-doc-pageobject-scan/SKILL.md +++ b/skills/living-doc-pageobject-scan/SKILL.md @@ -89,7 +89,7 @@ Repeat until coverage plateau — no new surfaces in the last full iteration. ### Step 3 — Form traversal (deep exploration) -Resolve field values using the **ExplorationFixture sourcing cascade** (see [living-doc-bdd-schemas](../shared/references/living-doc-bdd-schemas.md#explorationfixture)): (1) `seed.yaml form_fixtures`; (2) copied/derived value from an existing entity; (3) inferred `fake` value from label + placeholder + tooltip; (4) user-assist pause for `real-world` fields and record `source: user_provided`. +Resolve field values using the **`form_fixtures` sourcing cascade** (see [living-doc-bdd-schemas](../shared/references/living-doc-bdd-schemas.md#form_fixtures-sourcing-cascade-how-the-agent-resolves-a-field-value)): (1) `seed.yaml form_fixtures`; (2) copied/derived value from an existing entity; (3) inferred `fake` value from label + placeholder + tooltip; (4) user-assist pause for `real-world` fields and record `source: user_provided`. Skip `condition`-gated fields until the controlling field has the required value. After submit, probe each text input for special characters (`<>'"&\``), oversized input (200+ chars), wrong type, and duplicate value; after each probe, run the core scan to capture error elements (in the profile `test_id_attribute`) visible only in error state and record them in `field_constraints[]`. @@ -111,7 +111,7 @@ After interacting with a required field (e.g. `cps-radio-group`), re-check wheth ### Step 4 — Generate PageObject skeleton -Create one class per screen named `Page`. Every PageObject file starts with the full living-doc header block (`surface_type`, `route`, `owners`, `status`, `purpose`, `user_stories`, `functionalities`, `external_dependencies`, `page-object`) — see [living-doc-bdd-schemas — PageObject File Header](../shared/references/living-doc-bdd-schemas.md#pageobject-file-header). Secondary files sharing one Feature use the cross-reference header. Locators use `getByTestId()`. +Create one class per screen named `Page`. Every PageObject file starts with the full living-doc header block (`surface_type`, `route`, `owners`, `purpose`, `user_stories`, `functionalities`, `external_dependencies`, `page-object`) — see [living-doc-bdd-schemas — PageObject File Header](../shared/references/living-doc-bdd-schemas.md#pageobject-file-header). The header carries no `status` field. Secondary files sharing one Feature use the cross-reference header. Locators use `getByTestId()`. For starter bootstrap answers, say **Create mode** and emit real code, not pseudocode. For `/checkout`, materialise concrete members/methods for the promo input, confirm-order button, and error banner (`enterPromoCode` / `enter_promo_code`, `confirmOrder` / `confirm_order`, `assertErrorVisible` / `assert_error_visible`) in the class body — never as TODOs. If no matching Feature exists in the catalog, explicitly propose drafting it via `living-doc-create-feature`. @@ -120,7 +120,6 @@ For starter bootstrap answers, say **Create mode** and emit real code, not pseud * surface_type: UI * route: /checkout * owners: - * status: active * purpose: Checkout screen to confirm and pay for an order. * user_stories: US-7 * functionalities: FUNC-005 @@ -165,18 +164,18 @@ Flag fragile selectors with `# FRAGILE`, recommend adding the profile `test_id_a ### Step 5 — Map PageObjects to Feature entities -One PageObject ≈ one `UI` Feature. Write the full living-doc header block (see schema ref) with `feature_id`/`route`, and record `feature_id` in the manifest. +One PageObject ≈ one `UI` Feature. Write the full living-doc header block (see schema ref) — the Feature ID goes in its title line (`LIVING DOC — FEAT- · `), not in a `feature_id:` field — with `route`, and record `feature_id` in the manifest. - Feature exists → add header and manifest entry. - No Feature → write `FEAT-UNKNOWN`, flag **"needs Feature entity"** in the scan report, and propose drafting `FEAT-` via `living-doc-create-feature`; replace `FEAT-UNKNOWN` in the header and manifest after the Feature exists. Do not auto-create it from this skill. ### Step 6 — Generate Functionality stubs -For each discovered behavior, propose a stub named ` – `: +For each discovered behavior, propose a stub named ` - `: -- Button → `"Checkout Page – Confirm Order"` -- Form → `"Login Page – Submit Credentials"` -- Table → `"Order History Page – Display Order List"` +- Button → `"Checkout Page - Confirm Order"` +- Form → `"Login Page - Submit Credentials"` +- Table → `"Order History Page - Display Order List"` Output to `/func--.feature` (default `features/liv_doc_func/`) with `@FUNC_ID:FUNC-UNKNOWN`. Promote via `living-doc-create-functionality` once IDs are assigned. diff --git a/skills/living-doc-pageobject-scan/evals/evals.json b/skills/living-doc-pageobject-scan/evals/evals.json index 49d2fd9..161b774 100644 --- a/skills/living-doc-pageobject-scan/evals/evals.json +++ b/skills/living-doc-pageobject-scan/evals/evals.json @@ -100,11 +100,12 @@ "id": 8, "category": "output-format", "prompt": "Show me the expected structure of a generated CheckoutPage PageObject in Python for a screen at /checkout linked to FEAT-003.", - "expected_output": "Output is a Python code block. Line 1: '# living-doc: FEAT-003 | /checkout'. The class is named CheckoutPage. Selector constants are ALL_CAPS class attributes with data-testid values. The __init__ takes a page parameter and assigns self.page. Methods are named in snake_case, each wrapping a single page action or assertion. No inline selectors in method bodies — selectors are referenced only via the class constants. Methods include at least one action method and one assertion method (assert_*).", + "expected_output": "Output is a Python code block. It opens with the full living-doc header as a `#`-commented block naming FEAT-003 · Checkout, followed by every required field: surface_type: UI, route: /checkout, owners, purpose, user_stories, functionalities, external_dependencies, and page-object naming the file. The header carries no `status:` field. The class is named CheckoutPage. Selector constants are ALL_CAPS class attributes with data-testid values. The __init__ takes a page parameter and assigns self.page. Methods are named in snake_case, each wrapping a single page action or assertion. No inline selectors in method bodies — selectors are referenced only via the class constants. Methods include at least one action method and one assertion method (assert_*).", "files": [], "expectations": [ "Output is a Python code block", - "File-level comment: # living-doc: FEAT-003 | /checkout", + "Full header comment block with surface_type, route, owners, purpose, user_stories, functionalities, external_dependencies, page-object", + "Header carries no status field", "Class named CheckoutPage", "Selector constants are ALL_CAPS class attributes", "Methods reference class-level selector constants — no inline selectors", @@ -115,11 +116,11 @@ "id": 9, "category": "happy-path", "prompt": "During a Create mode scan of the checkout page, the agent discovers a 'Confirm Order' button and an 'Apply Promo Code' form input. What Functionality stubs should be generated?", - "expected_output": "Agent generates two Functionality stubs: (1) 'Checkout Page – Confirm Order' (for the button) and (2) 'Checkout Page – Apply Promo Code' (for the form input). Both are output to the profile Functionality feature directory (feature_dirs.functionality, default features/liv_doc_func/) as func--.feature with @FUNC_ID:FUNC-UNKNOWN placeholder tags for team review. The agent notes these are candidates requiring formal definition via living-doc-create-functionality before FUNC- IDs are assigned.", + "expected_output": "Agent generates two Functionality stubs: (1) 'Checkout Page - Confirm Order' (for the button) and (2) 'Checkout Page - Apply Promo Code' (for the form input). Both are output to the profile Functionality feature directory (feature_dirs.functionality, default features/liv_doc_func/) as func--.feature with @FUNC_ID:FUNC-UNKNOWN placeholder tags for team review. The agent notes these are candidates requiring formal definition via living-doc-create-functionality before FUNC- IDs are assigned.", "files": [], "expectations": [ "Generates one Functionality stub per distinct behavior (button → action, form input → action)", - "Stub names follow: ' – '", + "Stub names follow: ' - '", "Stubs output to the profile feature_dirs.functionality directory (default features/liv_doc_func/)", "Stubs use @FUNC_ID:FUNC-UNKNOWN placeholder tags", "Notes formal definition via living-doc-create-functionality is the next step" @@ -129,11 +130,12 @@ "id": 10, "category": "output-format", "prompt": "Show me the expected structure of a generated CheckoutPage PageObject in TypeScript for a screen at /checkout linked to FEAT-003.", - "expected_output": "Output is a TypeScript code block. First line: '// living-doc: FEAT-003 | /checkout'. Import line: 'import { type Page, type Locator, expect } from \"@playwright/test\"'. Class is declared 'export class CheckoutPage'. Locator fields are declared as 'readonly'. The constructor takes 'readonly page: Page' and initialises each Locator via page.getByTestId(). Methods are async, named in camelCase, each wrapping a single interaction or assertion. No inline selectors in method bodies — only the class Locator fields are used.", + "expected_output": "Output is a TypeScript code block. It opens with the full living-doc header as a block comment naming FEAT-003 · Checkout, followed by every required field: surface_type: UI, route: /checkout, owners, purpose, user_stories, functionalities, external_dependencies, and page-object naming the file. The header carries no `status:` field. Import line: 'import { type Page, type Locator, expect } from \"@playwright/test\"'. Class is declared 'export class CheckoutPage'. Locator fields are declared as 'readonly'. The constructor takes 'readonly page: Page' and initialises each Locator via page.getByTestId(). Methods are async, named in camelCase, each wrapping a single interaction or assertion. No inline selectors in method bodies — only the class Locator fields are used.", "files": [], "expectations": [ "Output is a TypeScript code block", - "File-level comment: // living-doc: FEAT-003 | /checkout", + "Full header block comment with surface_type, route, owners, purpose, user_stories, functionalities, external_dependencies, page-object", + "Header carries no status field", "Import from @playwright/test (Page, Locator, expect)", "Class exported: export class CheckoutPage", "Locator fields declared as readonly", @@ -188,12 +190,12 @@ "id": 14, "category": "output-format", "prompt": "Show the structure of the manifest.json a Create-mode scan should write for the /login route.", - "expected_output": "Output is a JSON object whose top level has 'generated', 'scan_version', and 'routes'. 'routes' is a JSON ARRAY of route objects, not an object keyed by path. Each element in 'elements[]' uses the literal key 'data-cy' (not snake_case 'data_cy'), and each coverage_gap uses camelCase 'suggestedDataCy' (not 'suggested_data_cy'). 'navigation_context' is a prose STRING, not an object. The closeout step validates the file with validate_artifacts.py manifest --canonicalize, which sorts routes by url and rewrites with sorted keys for stable diffs.", + "expected_output": "Output is a JSON object whose top level has 'generated', 'scan_version', and 'routes'. 'routes' is a JSON ARRAY of route objects, not an object keyed by path. Each element in 'elements[]' uses the normalized key 'test_id' (not attribute-specific 'data-cy' or 'data_cy'), and each coverage_gap uses 'suggested_test_id' (not 'suggestedDataCy' or 'suggested_data_cy'). The root-level 'test_id_attribute' tells downstream generators what HTML attribute (e.g. data-cy) these normalized keys map to. 'navigation_context' is a prose STRING, not an object. The closeout step validates the file with validate_artifacts.py manifest --canonicalize, which sorts routes by url and rewrites with sorted keys for stable diffs.", "files": [], "expectations": [ - "routes is a JSON array of route objects (manifest_shape: array)", - "Element keys use literal 'data-cy', not 'data_cy'", - "Gap keys use camelCase 'suggestedDataCy', not 'suggested_data_cy'", + "routes is a JSON array of route objects, inside an object-shaped manifest root (manifest_shape: object)", + "Element keys use normalized 'test_id', not 'data-cy' or 'data_cy'", + "Gap keys use normalized 'suggested_test_id', not 'suggestedDataCy' or 'suggested_data_cy'", "navigation_context is a string, not an object", "References validating/canonicalizing via validate_artifacts.py before closeout" ] @@ -202,12 +204,12 @@ "id": 15, "category": "output-format", "prompt": "Before you report the scan complete, what validation do you run on the artifacts?", - "expected_output": "Runs the mandatory closeout validation gate: 'python skills/living-doc-pageobject-scan/scripts/validate_artifacts.py manifest /manifest.json --canonicalize' and 'validate_artifacts.py seed /seed.yaml' (and 'validate_artifacts.py profile' if a profile is present). The manifest check rejects snake_case keys and an object navigation_context; --canonicalize sorts routes/elements and rewrites with sorted keys. The seed check rejects inline credential literals. The session is not reported complete until every validator exits 0.", + "expected_output": "Runs the mandatory closeout validation gate: 'python skills/living-doc-pageobject-scan/scripts/validate_artifacts.py manifest /manifest.json --canonicalize' and 'validate_artifacts.py seed /seed.yaml' (and 'validate_artifacts.py profile' if a profile is present). The manifest check rejects non-canonical element/gap keys (e.g. 'data-cy', 'data_cy', 'suggestedDataCy' instead of the normalized 'test_id' / 'suggested_test_id') and an object navigation_context; --canonicalize sorts routes/elements and rewrites with sorted keys. The seed check rejects inline credential literals. The session is not reported complete until every validator exits 0.", "files": [], "expectations": [ "Runs validate_artifacts.py on the manifest with --canonicalize", "Runs validate_artifacts.py on the seed", - "Notes the manifest check rejects snake_case keys and object navigation_context", + "Notes the manifest check rejects non-canonical keys (e.g. data-cy/data_cy/suggestedDataCy) and an object navigation_context", "Notes the seed check rejects inline credential literals", "Does not report done until validators exit 0" ] diff --git a/skills/living-doc-pageobject-scan/evals/fixture-map.md b/skills/living-doc-pageobject-scan/evals/fixture-map.md index ed2fcc3..9e55840 100644 --- a/skills/living-doc-pageobject-scan/evals/fixture-map.md +++ b/skills/living-doc-pageobject-scan/evals/fixture-map.md @@ -19,6 +19,10 @@ No fixture files for this skill. All evals are conversational or reference live | 9 | happy-path | _(none)_ | Create mode Step 5: Functionality stubs from discovered behaviors | | 10 | output-format | _(none)_ | TypeScript CheckoutPage: readonly Locators, async methods, living-doc header | | 11 | edge-case | _(none)_ | Maintain mode: multi-step auth route — navigation_context string with sequential steps | +| 12 | negative | _(none)_ | MCP pre-flight check: Create mode stops when Playwright MCP server is unavailable | +| 13 | negative | _(none)_ | MCP pre-flight check: Maintain mode stops when Playwright MCP server is unavailable | +| 14 | output-format | _(none)_ | manifest.json contract: object-shaped root, normalized `test_id`/`suggested_test_id` keys, string navigation_context | +| 15 | output-format | _(none)_ | Closeout validation gate: validate_artifacts.py on manifest (--canonicalize) and seed before reporting done | ## Trigger eval summary diff --git a/skills/living-doc-scenario-creator/SKILL.md b/skills/living-doc-scenario-creator/SKILL.md index 53841ee..5615bdc 100644 --- a/skills/living-doc-scenario-creator/SKILL.md +++ b/skills/living-doc-scenario-creator/SKILL.md @@ -30,8 +30,8 @@ license: Apache-2.0 | Mode | When to use | |---|---| -| **Entity mode** | A User Story or Functionality entity exists — generate full feature file with header, `@AC:` tags, and step bodies. | -| **Standalone mode** | No US/FUNC entity — write Gherkin directly from business descriptions. Use `@AC:STANDALONE` as tag; `gherkin-living-doc-sync` will note it but not flag a traceability gap. | +| **Entity mode** | A User Story or Functionality entity exists - generate full feature file with header, `@AC:` tags, and step bodies. | +| **Standalone mode** | No US/FUNC entity - write Gherkin directly from business descriptions. Use `@AC:STANDALONE` as tag; `gherkin-living-doc-sync` will note it but not flag a traceability gap. | --- @@ -57,7 +57,7 @@ An AC is uncovered if no `.feature` file carries `@AC:`. Use `living-doc-gap | Existing scenario state | Action | |---|---| | Matches AC intent; GWT correct | **Skip** — record `already covered` in the coverage report | -| Step text stale or AC description changed | **Update** — rewrite GWT in-place; keep `@AC:` tag and title stable | +| Step text stale or AC description changed | **Update** - rewrite GWT in-place; keep `@AC:` tag and title stable | | Tagged `@deprecated` or `@review-needed` | **Propose replacement** — draft new scenario; confirm with user before overwriting | | Multiple scenarios for the same AC | **Flag** — list them; ask user: valid aspect split or consolidate? | @@ -73,7 +73,7 @@ For each `active` AC, output `# AC:` comment, `@AC:` tag, `Scenario:` title, and **Traceability format** (authoritative — `gherkin-living-doc-sync` validates against this definition): ```gherkin -# AC:US-1-01 (v1.0.0 - active) — customer places an order with a saved payment method +# 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 Given the customer has items in their cart @@ -84,16 +84,16 @@ Scenario: Customer successfully places an order Aspect variant (when one scenario covers only one aspect of a multi-aspect AC): ```gherkin -# AC:US-1-01 (v1.0.0 - active) — displays {required field} on login screen | aspect: username input +# AC:US-1-01 (v1.0.0 - active) - displays {required field} on login screen | aspect: username input @AC:US-1-01/aspect:username-input Scenario: Login form shows the username input field ``` -Multiple ACs per scenario — one comment + tag pair per AC: +Multiple ACs per scenario - one comment + tag pair per AC: ```gherkin -# 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 (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 @@ -107,25 +107,32 @@ AC tag prefix matches the parent entity: `@AC:US--` for User Story, `@AC: | Type | Location (default) | Feature block | |---|---|---| | User Story (E2E) | `/us--.feature` (e.g. `features/liv_doc_us/`) | `Feature: ` with As-a/I-can/so-that + `@US_ID:US-` | -| Functionality | `/func--.feature` (e.g. `features/liv_doc_func/`) | `Feature: — ` + `@FUNC_ID:FUNC-` | +| Functionality | `/func--.feature` (e.g. `features/liv_doc_func/`) | `Feature: - ` + `@FUNC_ID:FUNC-` — the Functionality entity's `name` already has this canonical form (e.g. `Login Page - Validate Password Strength`), so use it as-is; never prefix the Feature name a second time | **Feature-level and scenario tags** (when `scenario_conventions` enables them in the profile): - Feature-level: the entity tag (`@US_ID:US-` / `@FUNC_ID:FUNC-`) plus an optional domain tag, e.g. `@domain_create`. - Scenario-level: the `@AC:` tag(s), plus optional suite tags such as `@Regression`. - Section banners may group scenarios, e.g. `# *** Happy day scenarios ***` and `# *** Negative scenarios ***`. -**US feature file example** — copy this skeleton verbatim and replace only the `<...>` placeholders and AC lines; do not reorder the header blocks or rename the tags: +**US feature file example** — copy this skeleton verbatim and replace only the `<...>` placeholders and AC lines; do not reorder the header blocks or rename the tags. The header comment block follows the canonical schema in [living-doc-bdd-schemas.md](../shared/references/living-doc-bdd-schemas.md#us-feature-file-header) exactly — do not shorten it: ```gherkin -# us-001-place-an-online-order.feature - -# Source: /us/US-001 -# Business Value: +# ============================================================================= +# LIVING DOC — US-001 · Place an online order +# ============================================================================= +# source: /us/US-001 +# status: active +# business_value: # - Customers can complete an order without calling support. - -# Acceptance Criteria: -# AC:US-001-01 (v1.0.0 - active) — customer places an order with a saved payment method. -# AC:US-001-02 (v1.0.0 - active) — order is rejected when the payment card is declined. +# +# acceptance_criteria: +# +# AC:US-001-01 (v1.0.0 - active) +# - customer places an order with a saved payment method +# +# AC:US-001-02 (v1.0.0 - active) +# - order is rejected when the payment card is declined +# ============================================================================= @US_ID:US-001 @domain_orders @@ -136,7 +143,7 @@ Feature: Place an online order # *** Happy day scenarios *** - # AC:US-001-01 (v1.0.0 - active) — customer places an order with a saved payment method + # AC:US-001-01 (v1.0.0 - active) - customer places an order with a saved payment method @AC:US-001-01 @Regression Scenario: Customer successfully places an order @@ -146,7 +153,7 @@ Feature: Place an online order # *** Negative scenarios *** - # AC:US-001-02 (v1.0.0 - active) — order is rejected when the payment card is declined + # AC:US-001-02 (v1.0.0 - active) - order is rejected when the payment card is declined @AC:US-001-02 Scenario: Order rejected when payment card is declined Given the customer has items in their cart @@ -154,13 +161,26 @@ Feature: Place an online order Then an error message is shown and the order is not placed ``` -**Functionality feature file example:** +**Functionality feature file example** — the header comment block follows the canonical schema in [living-doc-bdd-schemas.md](../shared/references/living-doc-bdd-schemas.md#functionality-feature-file-header) exactly — do not start the file at the `@FUNC_ID:` tag: ```gherkin +# ============================================================================= +# LIVING DOC — FUNC-001 · Login Page - Validate Password Strength +# ============================================================================= +# status: active +# parent: FEAT-010 +# func_type: field_validation +# +# acceptance_criteria: +# +# AC:FUNC-001-01 (v1.0.0 - active) +# - returns valid=true when password satisfies all rules +# ============================================================================= + @FUNC_ID:FUNC-001 -Feature: Login Page — Validate Password Strength +Feature: Login Page - Validate Password Strength - # AC:FUNC-001-01 (v1.0.0 - active) — returns valid=true when password satisfies all rules + # AC:FUNC-001-01 (v1.0.0 - active) - returns valid=true when password satisfies all rules @AC:FUNC-001-01 Scenario: Password meets all complexity rules Given a password with at least 8 characters, one uppercase, one lowercase, and one number @@ -177,7 +197,7 @@ AC COVERAGE REPORT — US-001 AC:US-001-01 (active): ✅ covered AC:US-001-02 (active): ✅ covered AC:US-001-03 (active): ❌ NOT COVERED - AC:US-001-04 (planned): ⏭ skipped — not yet active + AC:US-001-04 (planned): ⏭ skipped - not yet active ``` Coverage-report closeout rules: @@ -317,7 +337,7 @@ Scenario: Admin can create a new user | Assertions in Given/When | Move all assertions to `Then` | | Scenario depends on prior scenario state | Make every scenario fully self-contained | -When reviewing an existing scenario, check for a missing `@AC:` tag above each `Scenario:` — call that out as a traceability defect. +When reviewing an existing scenario, check for a missing `@AC:` tag above each `Scenario:` - call that out as a traceability defect. --- @@ -327,7 +347,7 @@ When no User Story or Functionality entity exists, generate scenarios directly f - Apply all GWT rules and ubiquitous language rules above. - Use `@AC:STANDALONE` as an optional tag to signal intentionally unlinked scenarios. -- Omit the header block (`# Business Value:`, `# Acceptance Criteria:`, `@US_ID:`) — start directly with `Feature:`. +- Omit the header block (`# status:`, `# business_value:`, `# acceptance_criteria:`, `@US_ID:`) — start directly with `Feature:`. - File location is at the user's discretion; `gherkin-living-doc-sync` will note `@AC:STANDALONE` but not flag a traceability gap. --- diff --git a/skills/living-doc-scenario-creator/evals/evals.json b/skills/living-doc-scenario-creator/evals/evals.json index 106c9b1..2acd2bb 100644 --- a/skills/living-doc-scenario-creator/evals/evals.json +++ b/skills/living-doc-scenario-creator/evals/evals.json @@ -4,8 +4,8 @@ { "id": 1, "category": "happy-path", - "prompt": "Generate BDD scenarios for US-007 — Place an Online Order.\n\nACs:\n- AC:US-007-01 (v1.0.0 – active) — happy_path: Customer places an order with a saved payment method\n- AC:US-007-02 (v1.0.0 – active) — error: Order is rejected when the payment card is declined\n- AC:US-007-03 (v1.0.0 – active) — alternative: Customer places an order using a guest checkout", - "expected_output": "Outputs a .feature file named 'us-007-place-an-online-order.feature'. Feature header uses the As-a/I-can/so-that narrative. Three Scenarios generated — one per active AC. Each Scenario: is preceded immediately by a '# AC: US-007-0n (v1.0.0 – active) — ...' comment. Scenario names follow the AC type conventions: US-007-01 is a Scenario: (happy_path), US-007-02 is titled 'Order rejected when payment card is declined' (error), US-007-03 is titled 'Place an Online Order — guest checkout' (alternative). All step text is domain language with no implementation details.", + "prompt": "Generate BDD scenarios for US-007 - Place an Online Order.\n\nACs:\n- AC:US-007-01 (v1.0.0 - active) - happy_path: Customer places an order with a saved payment method\n- AC:US-007-02 (v1.0.0 - active) - error: Order is rejected when the payment card is declined\n- AC:US-007-03 (v1.0.0 - active) - alternative: Customer places an order using a guest checkout", + "expected_output": "Outputs a .feature file named 'us-007-place-an-online-order.feature'. Feature header uses the As-a/I-can/so-that narrative. Three Scenarios generated - one per active AC. Each Scenario: is preceded immediately by a '# AC:US-007-0n (v1.0.0 - active) - ...' comment. Scenario names follow the AC type conventions: US-007-01 is a Scenario: (happy_path), US-007-02 is titled 'Order rejected when payment card is declined' (error), US-007-03 is titled 'Place an Online Order - guest checkout' (alternative). All step text is domain language with no implementation details.", "files": [], "expectations": [ "Feature file named us-007-place-an-online-order.feature", @@ -19,8 +19,8 @@ { "id": 2, "category": "happy-path", - "prompt": "Generate scenarios for US-007 which has these ACs:\n- AC:US-007-01 (active) — happy_path\n- AC:US-007-04 (deprecated) — error: old payment timeout path\n- AC:US-007-05 (planned) — alternative: loyalty points checkout\n\nWhat scenarios get generated and what appears in the coverage report?", - "expected_output": "Only AC:US-007-01 is active — one Scenario is generated for it. AC:US-007-04 (deprecated) and AC:US-007-05 (planned) are excluded from generation. Coverage report shows: AC:US-007-01 (active): generated. AC:US-007-04 (deprecated): skipped — deprecated AC. AC:US-007-05 (planned): skipped — not yet active. No scenarios are generated for deprecated or planned ACs.", + "prompt": "Generate scenarios for US-007 which has these ACs:\n- AC:US-007-01 (v1.0.0 - active) - happy_path\n- AC:US-007-04 (v1.2.0 - deprecated - removal planned v2.0.0) - error: old payment timeout path\n- AC:US-007-05 (planned) - alternative: loyalty points checkout\n\nWhat scenarios get generated and what appears in the coverage report?", + "expected_output": "Only AC:US-007-01 is active - one Scenario is generated for it. AC:US-007-04 (deprecated) and AC:US-007-05 (planned) are excluded from generation. Coverage report shows: AC:US-007-01 (v1.0.0 - active): generated. AC:US-007-04 (v1.2.0 - deprecated - removal planned v2.0.0): skipped - deprecated AC. AC:US-007-05 (planned): skipped - not yet active. No scenarios are generated for deprecated or planned ACs.", "files": [], "expectations": [ "Only active ACs drive scenario generation", @@ -98,7 +98,7 @@ "id": 8, "category": "output-format", "prompt": "Show the expected structure of a .feature file generated for US-007 with two active ACs: one happy_path and one error AC.", - "expected_output": "Output is a gherkin code block. Filename in a comment or header: us-007-place-an-online-order.feature. Feature: block with As-a/I-can/so-that narrative from the User Story. First scenario: '# AC: US-007-01 (v1.0.0 – active) — ...' immediately above 'Scenario: '. Second scenario: '# AC: US-007-02 (v1.0.0 – active) — ...' immediately above 'Scenario: — '. All steps use domain language. A coverage table appears after the feature file block.", + "expected_output": "Output is a gherkin code block. Filename in a comment or header: us-007-place-an-online-order.feature. Feature: block with As-a/I-can/so-that narrative from the User Story. First scenario: '# AC:US-007-01 (v1.0.0 - active) - ...' immediately above 'Scenario: '. Second scenario: '# AC:US-007-02 (v1.0.0 - active) - ...' immediately above 'Scenario: - '. All steps use domain language. A coverage table appears after the feature file block.", "files": [], "expectations": [ "Output is a gherkin code block with Feature: header", @@ -111,8 +111,8 @@ { "id": 9, "category": "edge-case", - "prompt": "AC:US-010-01 is: 'The login form displays the username input field and the password input field.' I need to write two scenarios — one for each field. How should the @AC: tags look?", - "expected_output": "Since each scenario covers only one aspect of the multi-aspect AC, encode the aspect using the /aspect:value param on the tag: first scenario uses '@AC:US-010-01/aspect:username-input' with comment '# AC:US-010-01 (v1.0.0 - active) — ... | aspect: username input'. Second scenario uses '@AC:US-010-01/aspect:password-input' with comment mirroring the aspect. The /aspect:value format allows coverage scripts to map each scenario to its specific aspect while the base AC ID keeps overall traceability intact.", + "prompt": "AC:US-010-01 is: 'The login form displays the username input field and the password input field.' I need to write two scenarios - one for each field. How should the @AC: tags look?", + "expected_output": "Since each scenario covers only one aspect of the multi-aspect AC, encode the aspect using the /aspect:value param on the tag: first scenario uses '@AC:US-010-01/aspect:username-input' with comment '# AC:US-010-01 (v1.0.0 - active) - ... | aspect: username input'. Second scenario uses '@AC:US-010-01/aspect:password-input' with comment mirroring the aspect. The /aspect:value format allows coverage scripts to map each scenario to its specific aspect while the base AC ID keeps overall traceability intact.", "files": [], "expectations": [ "Uses @AC:US-010-01/aspect:username-input for the first scenario", @@ -126,10 +126,10 @@ "id": 10, "category": "output-format", "prompt": "What tags and header blocks should appear at the Feature level (not the Scenario level) in a generated .feature file for US-007?", - "expected_output": "The feature file starts with: (1) a file-level comment block showing the source URL, business value statement, and the full AC list with versions and states; (2) a '@US_ID:US-007' tag immediately before the 'Feature:' keyword; (3) the 'Feature: ' block with the As-a/I-can/so-that narrative. The @US_ID: tag provides machine-readable file-level traceability distinct from the per-scenario @AC: tags. The # AC: scenario-level comments appear only immediately above each individual Scenario: block.", + "expected_output": "The feature file starts with the canonical US header comment block: '# status: active', the source URL, the business value statement, and the full acceptance_criteria list with versions and states; (2) a '@US_ID:US-007' tag immediately before the 'Feature:' keyword; (3) the 'Feature: <title>' block with the As-a/I-can/so-that narrative. The @US_ID: tag provides machine-readable file-level traceability distinct from the per-scenario @AC: tags. The # AC: scenario-level comments appear only immediately above each individual Scenario: block.", "files": [], "expectations": [ - "File-level comment block with source URL, business value, and AC list", + "File-level comment block includes '# status:' plus source URL, business value, and acceptance_criteria list", "@US_ID:US-007 tag immediately before Feature: keyword", "Feature: block with As-a/I-can/so-that narrative", "@US_ID: tag is distinct from per-scenario @AC: tags", @@ -140,7 +140,7 @@ "id": 11, "category": "regression", "prompt": "I ran coverage_report.py and it shows AC:US-007-03 is not covered. But I have a scenario for that AC. What might be wrong?", - "expected_output": "The most likely causes: (1) the @AC:US-007-03 Cucumber tag is missing or misspelled on the scenario — the script matches on the machine-readable @AC: tag, not the # AC: comment; (2) the tag uses the wrong ID format (e.g. @AC:US-7-03 instead of @AC:US-007-03); (3) the feature file is not in the directory the script was pointed at. Check the @AC: tag on the scenario exactly matches the AC ID in the living doc catalog. Run: scripts/coverage_report.py <living_doc_dir> <features_dir>.", + "expected_output": "The most likely causes: (1) the @AC:US-007-03 Cucumber tag is missing or misspelled on the scenario - the script matches on the machine-readable @AC: tag, not the # AC: comment; (2) the tag uses the wrong ID format (e.g. @AC:US-7-03 instead of @AC:US-007-03); (3) the feature file is not in the directory the script was pointed at. Check the @AC: tag on the scenario exactly matches the AC ID in the living doc catalog. Run: scripts/coverage_report.py <living_doc_dir> <features_dir>.", "files": [], "expectations": [ "Identifies @AC: tag mismatch/missing as the most likely cause", @@ -163,8 +163,8 @@ { "id": 13, "category": "regression", - "prompt": "The existing scenario for AC:US-007-02 has stale Given/When/Then steps — the step text no longer matches the current AC wording. What is the merge policy?", - "expected_output": "Update — the scenario intent still matches but the GWT is stale. Rewrite the Given/When/Then steps to match the current AC wording. Do not create a new scenario; update the existing one in place. Load gherkin-living-doc-sync if the @AC: tag or # AC: comment also needs updating.", + "prompt": "The existing scenario for AC:US-007-02 has stale Given/When/Then steps - the step text no longer matches the current AC wording. What is the merge policy?", + "expected_output": "Update - the scenario intent still matches but the GWT is stale. Rewrite the Given/When/Then steps to match the current AC wording. Do not create a new scenario; update the existing one in place. Load gherkin-living-doc-sync if the @AC: tag or # AC: comment also needs updating.", "files": [], "expectations": [ "Decision: Update (rewrite stale GWT)", @@ -201,10 +201,10 @@ "id": 16, "category": "regression", "prompt": "Run the coverage report for US-007 which has three active ACs. The features directory contains one scenario with '@AC:US-007-01' and one scenario that has a '# AC: US-007-02' comment but no @AC: tag. What does the report show?", - "expected_output": "Coverage report shows: AC:US-007-01 ✅ covered (matched by @AC: tag). AC:US-007-02 ❌ NOT COVERED — the # AC: comment is human-readable only; coverage is determined solely by the machine-readable @AC: tag. The comment alone does not count as coverage. AC:US-007-03 ❌ NOT COVERED. Summary: 1 covered, 2 gaps. The report does NOT claim AC:US-007-02 is covered just because a comment references it.", + "expected_output": "Coverage report shows: AC:US-007-01 ✅ covered (matched by @AC: tag). AC:US-007-02 ❌ NOT COVERED - the # AC: comment is human-readable only; coverage is determined solely by the machine-readable @AC: tag. The comment alone does not count as coverage. AC:US-007-03 ❌ NOT COVERED. Summary: 1 covered, 2 gaps. The report does NOT claim AC:US-007-02 is covered just because a comment references it.", "files": [], "expectations": [ - "Coverage is based exclusively on @AC: tags — not on # AC: comments", + "Coverage is based exclusively on @AC: tags - not on # AC: comments", "AC:US-007-01 is marked ✅ covered", "AC:US-007-02 is marked ❌ NOT COVERED despite the # AC: comment", "Summary correctly shows 1 covered, 2 gaps", @@ -229,7 +229,7 @@ "id": 18, "category": "regression", "prompt": "The coverage report is run for US-010. AC:US-010-02 has a scenario tagged '@AC:US-10-02' (missing leading zero in the US number). Is it counted as covered?", - "expected_output": "No — AC:US-010-02 is NOT covered. The @AC: tag '@AC:US-10-02' does not match the canonical ID 'US-010-02' because the ID format must match exactly (zero-padded). The report shows AC:US-010-02 ❌ NOT COVERED. A warning or note is shown: '@AC:US-10-02 found in <filename> — does not match any known AC ID; check zero-padding'. The report does not guess or fuzzy-match AC IDs.", + "expected_output": "No - AC:US-010-02 is NOT covered. The @AC: tag '@AC:US-10-02' does not match the canonical ID 'US-010-02' because the ID format must match exactly (zero-padded). The report shows AC:US-010-02 ❌ NOT COVERED. A warning or note is shown: '@AC:US-10-02 found in <filename> - does not match any known AC ID; check zero-padding'. The report does not guess or fuzzy-match AC IDs.", "files": [], "expectations": [ "AC:US-010-02 is marked ❌ NOT COVERED", @@ -270,7 +270,7 @@ "id": 21, "category": "output-format", "prompt": "For regression automation, generate the happy-day scenario for US-007 (AC:US-007-01 is the active happy_path AC) and show how it is tagged and where the closeout gate confirms coverage.", - "expected_output": "Generates exactly one happy-day scenario for AC:US-007-01 under a '# *** Happy day scenarios ***' banner. The scenario carries both the '@AC:US-007-01' traceability tag and a '@Regression' suite tag, with the '# AC:US-007-01 (v1.0.0 - active) — ...' comment immediately above it. At closeout the skill runs scan_ac_links.py (exits 0) and coverage_report.py, which must show 0 NOT COVERED happy-path ACs before the session closes. Scenarios are emitted in ascending AC-id order so regeneration is byte-stable.", + "expected_output": "Generates exactly one happy-day scenario for AC:US-007-01 under a '# *** Happy day scenarios ***' banner. The scenario carries both the '@AC:US-007-01' traceability tag and a '@Regression' suite tag, with the '# AC:US-007-01 (v1.0.0 - active) - ...' comment immediately above it. At closeout the skill runs scan_ac_links.py (exits 0) and coverage_report.py, which must show 0 NOT COVERED happy-path ACs before the session closes. Scenarios are emitted in ascending AC-id order so regeneration is byte-stable.", "files": [], "expectations": [ "One happy-day scenario for the active happy_path AC under the '# *** Happy day scenarios ***' banner", @@ -279,6 +279,21 @@ "Closeout runs scan_ac_links.py (exit 0) and coverage_report.py with 0 NOT COVERED happy-path ACs", "Scenarios emitted in ascending AC-id order for byte-stable regeneration" ] + }, + { + "id": 22, + "category": "regression", + "prompt": "Generate a Functionality feature file for FUNC-001 (parent Feature: FEAT-001 'Login Page') with one active AC:\n- AC:FUNC-001-01 (v1.0.0 - active) - happy_path: returns valid=true when password satisfies all rules", + "expected_output": "Outputs a .feature file named 'func-001-<kebab>.feature'. The file opens with the canonical Functionality header comment block (status, parent: FEAT-001, func_type: field_validation, and the acceptance_criteria listing for AC:FUNC-001-01) before '@FUNC_ID:FUNC-001' immediately above 'Feature: Login Page - Validate Password Strength' - the separator between the Feature name and the Functionality name is a plain hyphen ' - ', never an en dash '–' or em dash '—'. One Scenario is generated for AC:FUNC-001-01, preceded immediately by its '# AC:' comment and '@AC:FUNC-001-01' tag. Step text is domain language with no implementation details.", + "files": [], + "expectations": [ + "Feature file named func-001-<kebab>.feature", + "Canonical header comment block (status, parent, func_type, acceptance_criteria) precedes the @FUNC_ID: tag", + "@FUNC_ID:FUNC-001 tag immediately before the Feature: line", + "Feature: line uses a plain hyphen ' - ' between Feature name and Functionality name - not an em dash or en dash", + "One Scenario for AC:FUNC-001-01 with # AC: comment and @AC: tag immediately above it", + "Business language steps — no HTTP, selectors, or DB" + ] } ] } \ No newline at end of file diff --git a/skills/living-doc-scenario-creator/evals/fixture-map.md b/skills/living-doc-scenario-creator/evals/fixture-map.md index 2cdf967..6b26a8f 100644 --- a/skills/living-doc-scenario-creator/evals/fixture-map.md +++ b/skills/living-doc-scenario-creator/evals/fixture-map.md @@ -19,6 +19,17 @@ No fixture files for this skill. All evals use inline User Story/AC definitions | 9 | edge-case | _(none)_ | /aspect:value param encoding for multi-aspect ACs | | 10 | output-format | _(none)_ | Feature-level @US_ID: tag vs. per-scenario @AC: tags | | 11 | regression | _(none)_ | coverage_report.py: @AC: tag mismatch causing false "not covered" result | +| 12 | happy-path | _(none)_ | Merge policy: existing scenario already matches AC intent exactly → skip, no duplicate; AC marked covered | +| 13 | regression | _(none)_ | Merge policy: existing scenario intent matches but GWT text is stale → update in place, no new scenario; routes @AC: tag sync to gherkin-living-doc-sync if needed | +| 14 | edge-case | _(none)_ | Merge policy: existing scenario is deprecated/review-needed → propose replacement, mark old as superseded, human review before delete | +| 15 | edge-case | _(none)_ | Merge policy: AC already has 3+ linked scenarios → flag for review instead of adding another; user picks the canonical scenario | +| 16 | regression | _(none)_ | coverage_report.py: @AC: tag counts as coverage, bare # AC: comment does not; mixed set reports 1 covered / 2 gaps | +| 17 | regression | _(none)_ | coverage_report.py: empty features directory → all ACs NOT COVERED, exit code 1, no hallucinated coverage | +| 18 | regression | _(none)_ | coverage_report.py: zero-padding mismatch (@AC:US-10-02 vs US-010-02) → NOT COVERED + unrecognised-tag warning, no fuzzy matching | +| 19 | happy-path | _(none)_ | coverage_report.py: all ACs tag-matched → 100% covered, real file names listed, exit code 0 | +| 20 | happy-path | _(none)_ | Gap-to-generation handoff: coverage report gaps become the scenario-creator queue, processed in report order | +| 21 | output-format | _(none)_ | Regression closeout: single happy-day scenario tagged @AC: + @Regression; scan_ac_links.py and coverage_report.py must show 0 NOT COVERED happy-path ACs before close | +| 22 | regression | _(none — inline FUNC/AC definition in prompt)_ | Functionality feature file: @FUNC_ID: tag, Feature: name uses plain hyphen separator (not em/en dash), # AC:/@AC: pairing | ## Trigger eval summary diff --git a/skills/living-doc-update/SKILL.md b/skills/living-doc-update/SKILL.md index d025a66..bb7b1c6 100644 --- a/skills/living-doc-update/SKILL.md +++ b/skills/living-doc-update/SKILL.md @@ -1,18 +1,19 @@ --- name: living-doc-update description: > - Update, amend, or deprecate existing living documentation entities (User Stories, Features, - Functionalities). Use when adding new ACs to an existing User Story, descoping or removing - an AC, changing a Feature's ownership or status, updating the Feature Registry after a team - restructure, deprecating a Functionality whose code has been deleted, or promoting a User - Story from draft to ready. - Triggers on: "update user story", "add AC to user story", "descope AC", "deprecate feature", - "mark US ready", "change feature owner", "update functionality", "deprecate functionality", - "living doc update", "update living doc entity", "mark feature deprecated", "update AC", - "change status of user story", "update feature registry". - Does NOT trigger for: creating new entities (use living-doc-create-*), finding gaps - (use living-doc-gap-finder), generating scenarios (use living-doc-scenario-creator). - Pairs with gherkin-living-doc-sync (propagate AC changes) and bdd-maintain (cleanup after deprecation). + Update or deprecate existing living-doc entities (User Stories, Features, Functionalities): + add, modify, or descope an AC; change status or promote planned to active; change a + Feature's owners, deprecation metadata, or dependencies; rename a Feature; update the + Feature Registry; deprecate a Functionality whose code was deleted. + Triggers on: "update user story", "add AC to user story", "update AC", "descope AC", + "mark US ready", "change status of user story", "update functionality", + "deprecate functionality", "deprecate feature", "mark feature deprecated", + "change feature owner", "update feature registry", "change feature dependencies", + "remove external dependency", "dependency became a feature", "promote external dependency", + "living doc update". + NOT for: creating entities (living-doc-create-*), finding gaps (living-doc-gap-finder), + generating scenarios (living-doc-scenario-creator). + Pairs with gherkin-living-doc-sync (AC changes) and bdd-maintain (post-deprecation cleanup). license: Apache-2.0 --- @@ -30,11 +31,13 @@ If the user says "update the story" but the substance is a newly discovered edge |---|---|---| | Add a new AC | User Story / Functionality | Append a new AC entry with the next sequential AC ID | | Modify AC description | User Story / Functionality | Edit the description; keep the AC ID stable | -| Change status | Any entity | Update `status` field; record the transition event | -| Change owner | Feature | Update `owners` field; add `owner_changed_at` (ISO date) and `owner_change_reason` fields; notify the new owner if open User Stories are linked to the Feature | +| Change status | User Story / Functionality | Update `status` field; record the transition event — a Feature has no `status` field to change; its state is derived from its Functionalities | +| Change owner | Feature | Update `owners` field — there are no ownership-change metadata fields; list the Feature's non-`deprecated` User Stories in the change summary so the outgoing owner can hand them over | | Add a linked User Story | Feature | Append to `user_stories` | -| Deprecate an entity | Any entity | Set `status: deprecated`; add `deprecated_at`, `deprecation_reason`, and optionally `superseded_by` | -| Delete a Functionality | Functionality | Do not delete — deprecate it and link to the commit that removed the code | +| Add, drop, or promote a dependency | Feature / Functionality | See [Change a Feature's dependencies](#change-a-features-dependencies) — a promotion cascades across every Feature that names the dependency | +| Deprecate a User Story or Functionality | User Story / Functionality | Set `status: deprecated`; add `deprecated_at`, `deprecation_reason`, and optionally `superseded_by` | +| Deprecate a Feature | Feature | Add `deprecation_reason` and optionally `superseded_by` — never set a `status` field, and never author `deprecated_at`; the surface's retirement is recorded through its Functionalities being deprecated, and its state and date are derived from them | +| Delete a Functionality | Functionality | Do not delete — deprecate it, and record the commit that removed the backing code as a `## Notes` bullet | ## Update a User Story — add or modify ACs @@ -42,7 +45,7 @@ When adding a new AC to an existing User Story: 1. Load the existing User Story entity 2. Assign the next sequential AC ID in the canonical `AC:<parent-id>-<nn>` format - (for example `AC:US-042-04`) — see [living-doc-glossary](../shared/references/living-doc-glossary.md#acceptance-criterion-ac) + (for example `AC:US-042-04`) - see [living-doc-glossary](../shared/references/living-doc-glossary.md#acceptance-criterion-ac) 3. Elicit the new AC using the same completeness checklist as `living-doc-create-user-story` and capture it in `description`, `given`, `when`, `then` form: - Happy path covered? @@ -59,10 +62,11 @@ When modifying an existing AC **keep the AC ID stable** — changing the ID brea to linked tests. Only update the `description`, `given`, `when`, `then`, or state fields. If the changed AC text affects linked tests, flag them for update. -**AC versioning:** ACs carry a `(vMAJOR.MINOR.PATCH – state)` annotation. +**AC versioning:** Once an AC is targeted at a version, it carries a `(vMAJOR.MINOR.PATCH - state)` annotation. A backlog AC with no target version yet stays `(planned)` — do not invent a version for it. - Bump the **minor** version for any business-rule change to an `active` AC (e.g. `v1.0.0 → v1.1.0`). - Bump the **patch** version for a wording clarification that does not change the rule (e.g. `v1.0.0 → v1.0.1`). -- The version must appear in the `# AC:` comment in linked Gherkin feature files — trigger `gherkin-living-doc-sync` to propagate the new version into those comments. +- Deprecating an AC requires a removal note: `(v<version> - deprecated - removal planned v<version>)`. +- The version must appear in the `# AC:` comment in linked Gherkin feature files - trigger `gherkin-living-doc-sync` to propagate the new version into those comments. ## Promote a Functionality from planned to active @@ -95,23 +99,29 @@ When promotion is blocked because only a happy-path AC exists, give a concrete e After promoting a User Story to `active`, trigger `living-doc-scenario-creator` to generate BDD feature files for each `active` AC if they do not yet exist. -## Deprecate a Feature or Functionality +## Deprecate a Feature, Functionality, or User Story Use this workflow when code backing an entity is deleted or a business capability is retired. Set the relevant fields in the project's Storage Profile format: -| Field | Value | -|---|---| -| `status` | `deprecated` | -| `deprecated_at` | Date of deprecation | -| `deprecation_reason` | Why it was deprecated | -| `deprecated_code_commit` | Commit SHA or URL that removed the backing code (if applicable) | -| `superseded_by` | ID of the replacement entity (if applicable) | +| Field | Value | Applies to | +|---|---|---| +| `status` | `deprecated` | Functionality and User Story only — a Feature has no `status` field | +| `deprecated_at` | Date of deprecation | Functionality and User Story only — on a Feature it is **derived** alongside the state, never authored | +| `deprecation_reason` | Why it was deprecated | Feature, Functionality, and User Story | +| `superseded_by` | ID of the replacement entity (if applicable) | Feature, Functionality, and User Story | Rules: - Always deprecate — never delete entities (preserves audit trail) -- Add `deprecated_code_commit` when the code was removed in a commit +- A Functionality or User Story being deprecated gets `status: deprecated` — never leave it on its prior status while adding deprecation metadata, or it reads as still active +- When the backing code was removed in a commit, record that commit as a **`## Notes` bullet** on + the entity (feature-file header key `notes:`, issue-body section `## Notes`) — for example + `- Backing code removed in abc1234 (PR #412).` There is no typed field for it: *deprecated is not + removed*, and the commit is a pointer to where the removal actually happened, which is context, + not schema. Do not invent a typed field for it — neither the canon nor `living-doc-utilities` + defines one, and `validate_entity.py` treats any such key as unrecognised. - Add `superseded_by` when a replacement entity exists +- A Feature never gets a `status` field, deprecated or otherwise — its state is derived from its Functionalities. Retiring a Feature means deprecating every Functionality it owns; the Feature entity itself only gains `deprecation_reason` / `superseded_by` as a record of why the surface was retired. Its `deprecated_at` is derived with the state, so do not author one — `validate_entity.py` flags a Feature that carries it. - If a deprecated Feature owns Functionalities, flag every owned Functionality for deprecation review before closing the change. - Flag any tests linked to the deprecated entity for update or removal - If the deprecated entity has `ACTIVE` ACs with linked Gherkin scenarios, trigger @@ -124,40 +134,125 @@ Rules: Changing a Feature's `id` or `name` requires these cascading updates: 1. Update the Feature entity (`id`, `name`, and any self-referencing fields). -2. Update `feature_id` in every Functionality linked to this Feature. +2. Update `parent_feature` in every Functionality linked to this Feature. 3. Update the `feature_registry` entry in `catalog.json` (change the `feature_id` key and any path comments). 4. Search `manifest.json` and `seed.yaml` for the old name or ID and update. 5. Search PageObject file headers for the old Feature reference and update. 6. If Gherkin feature files have a `# Feature:` header with the old name, update those headers. 7. Run `living-doc-gap-finder` to confirm no `ORPHAN_FUNCTIONALITY` gaps remain after the rename. -## Update Feature ownership or dependencies +## Update Feature ownership + +When a team changes ownership of a Feature, update the `owners` field and nothing else. There are no +ownership-change metadata fields — no canonical layout places one, no contract model holds one, and +an authored `owner_changed_at` or `owner_change_reason` is silently dropped by the pipeline. The +tracked file's git history already records when the transfer happened and why. + +A transfer leaves work in flight, so list the Feature's User Stories whose `status` is not +`deprecated` in the change summary. That is the handoff list for the two owners to work through — +this skill edits documentation and sends no messages, so do not claim the new owner was notified. + +## Change a Feature's dependencies + +Use this when the code behind a Feature starts calling a system, stops calling one, or when a system +it already calls gets documented as a Feature of its own. The job is to leave each call recorded in +the one place the canon puts it: + +- A system with **no canonical anchor** is an `external_dependencies` entry on the calling Feature, + by name — see [living-doc-glossary — Feature](../shared/references/living-doc-glossary.md#feature). +- An **`API` Feature** is a `feature_dependencies` entry on the **Functionality that makes the call** — + see [living-doc-glossary — Functionality](../shared/references/living-doc-glossary.md#functionality-func). + +A Feature's own `feature_dependencies` is **derived** from its Functionalities and never authored. +Never write the field onto a Feature — not to mirror a Functionality, and not as a place to park an +id while the calling Functionality is missing. + +### A dependency is added + +Record it in the one place above and stop — nothing cascades. Then ask one question: does the new +call bring behaviour that no existing Functionality covers yet? If it does, that is a +`living-doc-create-functionality` request, separate from this change. + +### A dependency is dropped + +Remove the entry from the list that held it. Then scan `catalog.json`: if no other +`external_dependencies` or `feature_dependencies` list still names it, say so — any test double, +fixture, or `manifest.json` entry standing in for it is now unreferenced. List what you find; this +skill does not delete them. + +### A dependency graduates into its own Feature + +An `external_dependencies` entry becomes a Feature when the system gains a contract anchor. From that +point the call is an edge to a Feature, and it moves off every Feature that named it: + +1. **Confirm the trigger.** The dependency now has a canonical anchor — an annotated endpoint method, + or a producer/consumer handler carrying an AsyncAPI (or equivalent schema-registry) annotation. If + it has neither, stop: it stays an `external_dependencies` entry. The anchor rules belong to + `living-doc-create-feature` — route there to decide, do not restate them here. +2. **Create the Feature** via `living-doc-create-feature`. It assigns the id, the `surface_type`, and + the `feature_registry` entry in `catalog.json`. Come back with the new `FEAT-<nnn>`. +3. **Find every Feature that names the dependency.** Scan the `external_dependencies` of every Feature + in `catalog.json`, not only the one the user mentioned. Entries are free-form names: match + case-insensitively, treat a near miss (`payment-gateway` vs `payments-gateway`) as a question for + the user rather than an automatic edit, and list what you found before changing anything. +4. **Move each occurrence.** Remove the `external_dependencies` entry from the Feature, and add the new + `FEAT-<nnn>` to `feature_dependencies` on the Functionality that makes the call — never on the + Feature, whose value is derived. If the caller has no Functionality describing that call, say so and + offer `living-doc-create-functionality`; do not invent a Functionality, and do not park the id on + the Feature. +5. **Leave the Functionalities where they are.** A Functionality describes the behaviour of the Feature + that owns it, not of the system it calls, so the promotion never re-parents one. Flag the + Functionalities whose tests stubbed the dependency for test review — they can now test against a + documented contract. +6. **Expect the new Feature to be an orphan.** It owns no Functionalities yet, so + `living-doc-gap-finder` reports `ORPHAN_FEATURE`, and its derived state is `planned` until its first + Functionality is documented. Report that; do not invent placeholder Functionalities to silence it. +7. **Validate** the new Feature and every edited entity: + `python scripts/validate_entity.py <entity>.json --catalog catalog.json`. With the catalog, the + validator checks each `feature_dependencies` target exists, is an `API` Feature, is not deprecated, + and is not the Functionality's own Feature. +8. **Emit the change summary** — the new Feature id, every entity the entry moved on, and the + Functionalities flagged for test review: -When a team changes ownership of a Feature, update the `owners` field and set `owner_changed_at` -(date) and `owner_change_reason`. If the Feature has open User Stories, notify the new owner. +``` +LIVING DOC UPDATE — 2026-09-29 + Promoted: payment-events → FEAT-007 Payment Events API (surface_type: API) + Changes: + - FEAT-001 Checkout Page external_dependencies: removed payment-events + + FUNC-003 feature_dependencies: added FEAT-007 + - FEAT-002 Orders API external_dependencies: removed payment-events + + FUNC-005 feature_dependencies: added FEAT-007 + Flagged for test review (stubbed payment-events): + FUNC-003, FUNC-005 + Downstream flags: + FEAT-007 is ORPHAN_FEATURE and derives as planned until its first Functionality is documented +``` ## Descope an AC mid-sprint -When an AC is moved out of the current sprint but not permanently removed: +There is no `descoped` state. When an AC is moved out of the current sprint but not permanently +removed, it keeps its `planned` state and gains no extra fields — do not delete the AC (preserves +audit trail and reinstating intent): -- Set `status: descoped` — do not delete the AC (preserves audit trail and reinstating intent) -- Add `descoped_at` (date) and `descoped_reason` fields -- Add `future_release` field if the work is planned for a later sprint -- Flag any linked Gherkin scenarios for `@wip` or `@pending` tagging via `gherkin-living-doc-sync` - -``` +- Drop the target version so the AC reads `AC:<id> (planned)` - backlog, agreed, no target version yet +- Record why on the AC's `- Rationale:` bullet +- Flag any linked Gherkin scenarios for `@wip` + `@review-needed` tagging via `gherkin-living-doc-sync` For **business-rule changes to an active AC**, first show the AC side-by-side for confirmation, then apply the version bump: ``` -OLD: AC:US-042-01 (v1.0.0 - active) — Minimum order value is £50. -NEW: AC:US-042-01 (v1.1.0 - active) — Minimum order value is £75. +OLD: AC:US-042-01 (v1.0.0 - active) - Minimum order value is £50. +NEW: AC:US-042-01 (v1.1.0 - active) - Minimum order value is £75. +``` + +For an AC **descoped** out of the current release (was targeting `v1.2.0`, no longer is): + ``` -AC:US-042-03 (v1.2.0 – descoped) - – Promo codes can be stacked and applied in defined priority order. - – descoped_at: 2026-05-15 - – descoped_reason: Promo stacking rule deferred — too complex for current sprint - – future_release: sprint-52 +OLD: AC:US-042-03 (v1.2.0 - planned) + - Promo codes can be stacked and applied in defined priority order. +NEW: AC:US-042-03 (planned) + - Promo codes can be stacked and applied in defined priority order. + - Rationale: Promo stacking rule deferred - too complex for current sprint; reinstate when re-prioritised. ``` ## Out-of-scope routing @@ -176,7 +271,9 @@ AC:US-042-03 (v1.2.0 – descoped) After updating any entity, run this script to validate the result against the canonical schema. It checks required fields, ID format, status values, AC structure, and (with `--catalog`) -referential integrity against the full catalog. +referential integrity against the full catalog — including every `feature_dependencies` edge: +the target exists, is an `API` Feature, is not deprecated, and is not the Functionality's own +Feature. Without `--catalog` it reports none of those four and says the basis is unavailable. ```bash # Validate a single entity file @@ -206,7 +303,7 @@ new values clearly labelled, and list any linked tests that need updating: LIVING DOC UPDATE — 2026-05-15 Entity: US-042 — Customer applies a promotional discount Changes: - + Added AC AC:US-042-04 (state: planned) — Promo code expired returns 422 with error message + + Added AC AC:US-042-04 (planned) - Promo code expired returns 422 with error message ~ Modified AC AC:US-042-01: OLD: "Payment must complete within 3 seconds under normal load (p99 SLA)" NEW: "Payment must complete within 2 seconds under normal load (p99 SLA)" @@ -222,7 +319,7 @@ For **added ACs**, use the same summary pattern rather than ending with validati LIVING DOC UPDATE — 2026-05-15 Entity: US-089 — Delivery restrictions Changes: - + Added AC AC:US-089-04 (state: planned) + + Added AC AC:US-089-04 (planned) GIVEN a customer enters an address outside the shipping zone WHEN they place the order THEN the order is blocked with SHIPPING_ZONE_EXCLUDED and a clear message diff --git a/skills/living-doc-update/evals/evals.json b/skills/living-doc-update/evals/evals.json index 51d8040..4454276 100644 --- a/skills/living-doc-update/evals/evals.json +++ b/skills/living-doc-update/evals/evals.json @@ -29,13 +29,14 @@ { "id": 3, "category": "regression", - "prompt": "The `LegacyPaymentGatewayService` has been deleted from the codebase. How do I handle this in the living doc?", - "expected_output": "Agent sets status to deprecated on the entity — never deletes the entity file. Adds deprecated_at and deprecation_reason fields. Links to the commit that deleted the code if possible. Flags any linked Gherkin scenarios for gherkin-living-doc-sync.", + "prompt": "The `FUNC-018` Functionality (Legacy Payment Gateway Retry Logic) has had its backing code deleted from the codebase. How do I handle this in the living doc?", + "expected_output": "Agent sets status to deprecated on the Functionality entity — never deletes the entity file. Adds deprecated_at and deprecation_reason fields. Records the commit that deleted the backing code as a `## Notes` bullet on the entity (header key `notes:`), not as a typed field — there is none, and deprecated is not removed. Flags any linked Gherkin scenarios for gherkin-living-doc-sync.", "files": [], "expectations": [ - "Sets status: deprecated — never deletes the entity file", + "Sets status: deprecated on the Functionality — never deletes the entity file", "Adds deprecated_at and deprecation_reason fields", - "Links to the commit that deleted the code if possible", + "Records the removing commit as a `## Notes` bullet, not as a typed field", + "Does not invent a deprecation field for the commit reference", "Flags linked Gherkin scenarios for gherkin-living-doc-sync" ] }, @@ -43,12 +44,13 @@ "id": 4, "category": "regression", "prompt": "The checkout feature is now owned by team-payments-v2 instead of team-checkout. How do I update this in the living doc?", - "expected_output": "Agent updates the owners array in the Feature entity JSON to team-payments-v2. Adds owner_changed_at and owner_change_reason fields. Notifies the new owner if open User Stories are linked to the Feature.", + "expected_output": "Agent updates the owners array in the Feature entity JSON to team-payments-v2 and adds no other field — there are no ownership-change metadata fields. Lists the Feature's non-deprecated User Stories in the change summary as the handoff list, and does not claim to have notified anyone.", "files": [], "expectations": [ "Updates owners array in Feature entity JSON", - "Adds owner_changed_at and owner_change_reason fields", - "Notifies new owner if open User Stories exist" + "Adds no owner_changed_at or owner_change_reason field", + "Lists the Feature's non-deprecated User Stories as a handoff list", + "Does not claim the new owner was notified" ] }, { @@ -105,13 +107,13 @@ "id": 9, "category": "edge-case", "prompt": "We decided during the sprint to descope AC:US-042-03 — the promo stacking rule is moving to a future release. How do I handle this in the living doc without losing the work?", - "expected_output": "Agent sets the AC status to 'descoped' — does not delete the AC. Adds descoped_at and descoped_reason fields. Adds a future_release reference if the work is planned for a later sprint. Flags any linked Gherkin scenarios for @wip or @pending tagging via gherkin-living-doc-sync.", + "expected_output": "There is no 'descoped' state. Agent keeps the AC's 'planned' state and drops the target version, so it reads AC:US-042-03 (planned) — does not delete the AC. Records why on the AC's '- Rationale:' bullet (e.g. 'Promo stacking rule deferred - too complex for current sprint'). Flags any linked Gherkin scenarios for @wip and @review-needed tagging via gherkin-living-doc-sync.", "files": [], "expectations": [ - "Sets AC status to 'descoped' — does not delete the AC", - "Adds descoped_at and descoped_reason fields", - "Adds a future_release reference if planned for a later sprint", - "Flags linked Gherkin scenarios for @wip or @pending tagging via gherkin-living-doc-sync" + "Does not set a 'descoped' status — keeps the AC 'planned' with no target version", + "Does not delete the AC", + "Records the reason on the AC's '- Rationale:' bullet, not a descoped_reason field", + "Flags linked Gherkin scenarios for @wip and @review-needed tagging via gherkin-living-doc-sync" ] }, { @@ -150,13 +152,14 @@ "type": "happy-path", "description": "Developer deprecates a Feature that has been superseded by a new Feature.", "prompt": "The 'Legacy Payment Widget' (FEAT-003) has been replaced by the new 'Payment Page' (FEAT-002). How do I deprecate the old Feature in the living doc?", - "expected_output": "Set status: deprecated on FEAT-003. Add deprecated_at (today's date), deprecation_reason (e.g. 'Replaced by FEAT-002'), and superseded_by: 'FEAT-002'. Never delete the entity — deprecation preserves the audit trail. Flag any Functionalities owned by FEAT-003 for deprecation review. Flag any tests linked to those Functionalities for update or removal.", + "expected_output": "FEAT-003 never gets a status field — a Feature's state is derived from its Functionalities, never authored. It gets no deprecated_at either: a Feature's deprecation date is derived alongside its state. Add deprecation_reason (e.g. 'Replaced by FEAT-002') and superseded_by: 'FEAT-002' directly on FEAT-003, and nothing else. Never delete the entity — deprecation preserves the audit trail. Deprecate every Functionality owned by FEAT-003 (setting status: deprecated on each, since Functionalities do carry a status) — that, not a field on the Feature, is what makes the surface deprecated. Flag any tests linked to those Functionalities for update or removal.", "files": [], "expectations": [ - "Sets status: deprecated — never deletes the entity", - "Adds deprecated_at, deprecation_reason, and superseded_by fields", + "Does not set a status field on the Feature — never deletes the entity", + "Adds deprecation_reason and superseded_by on the Feature", + "Does not author deprecated_at on the Feature — it is derived with the state", "superseded_by points to FEAT-002", - "Flags owned Functionalities for deprecation review", + "Deprecates every owned Functionality as the actual mechanism of retiring the surface", "Flags linked tests for update or removal" ], "category": "regression" @@ -195,7 +198,7 @@ "id": 15, "category": "regression", "prompt": "AC:US-042-01 is currently ACTIVE at v1.0.0. The discount threshold rule changed — the minimum order value is now £75 instead of £50. I need to update the AC.", - "expected_output": "Agent shows OLD and NEW AC side by side for confirmation. Updates the AC description to reflect the new threshold. Bumps the version from v1.0.0 to v1.1.0 — required for all business-rule changes to ACTIVE ACs. The AC ID stays unchanged. Any linked Gherkin scenarios annotated with '# AC: US-042-01' are flagged as potentially stale and handed to gherkin-living-doc-sync.", + "expected_output": "Agent shows OLD and NEW AC side by side for confirmation. Updates the AC description to reflect the new threshold. Bumps the version from v1.0.0 to v1.1.0 — required for all business-rule changes to ACTIVE ACs. The AC ID stays unchanged. Any linked Gherkin scenarios annotated with '# AC:US-042-01' are flagged as potentially stale and handed to gherkin-living-doc-sync.", "files": [], "expectations": [ "Version bumped from v1.0.0 to v1.1.0", @@ -212,12 +215,31 @@ "files": [], "expectations": [ "Lists all 7 cascade steps", - "Step 2: update feature_id in Functionality entities", + "Step 2: update parent_feature in Functionality entities", "Step 3: update feature_registry.json", "Step 5: update PageObject file headers", "Step 6: update Gherkin # Feature: headers", "Step 7: run gap-finder to confirm clean state" ] + }, + { + "id": 17, + "category": "happy-path", + "prompt": "The payment-events Kafka topic finally has an AsyncAPI annotation on its producer handler. Checkout Page (FEAT-001) lists payment-events as an external dependency — promote it to a proper Feature and update the living doc. Catalog: evals/files/dependency-promotion-catalog.json", + "expected_output": "Agent confirms the trigger (the producer handler now carries an AsyncAPI annotation, so payment-events has a canonical anchor) and routes creation of the new API Feature to living-doc-create-feature rather than restating its anchor rules, returning with the new FEAT id (FEAT-004). It scans external_dependencies on every Feature in the catalog, not only FEAT-001, and finds both FEAT-001 Checkout Page ('payment-events') and FEAT-002 Orders API ('Payment-Events', matched case-insensitively), listing them before editing; FEAT-003 is left untouched. For each, it removes the external_dependencies entry and adds FEAT-004 to feature_dependencies on the Functionality that makes the call (FUNC-001 on Checkout Page, FUNC-003 on Orders API) — never on a Feature, whose feature_dependencies is derived. FUNC-001 and FUNC-003 keep their parent_feature; nothing is re-parented to FEAT-004. Both are flagged for test review because their tests stubbed payment-events and can now test against the documented contract. It reports that FEAT-004 owns no Functionalities yet, so living-doc-gap-finder flags it ORPHAN_FEATURE and its derived state is planned, without inventing placeholder Functionalities. It runs validate_entity.py with --catalog on the new Feature and every edited entity, and emits a change summary naming the new Feature id, every entity the entry moved on, and the Functionalities flagged for test review.", + "files": [ + "evals/files/dependency-promotion-catalog.json" + ], + "expectations": [ + "Routes creation of the new Feature to living-doc-create-feature instead of restating its anchor rules", + "Finds both FEAT-001 and FEAT-002 as Features naming payment-events, not only the FEAT-001 the prompt mentions", + "Removes payment-events from external_dependencies on both Features", + "Adds the new FEAT id to feature_dependencies on the calling Functionalities (FUNC-001, FUNC-003), never on a Feature", + "Does not re-parent FUNC-001 or FUNC-003 to the new Feature", + "Flags the Functionalities that stubbed payment-events for test review", + "Names ORPHAN_FEATURE and/or the derived planned state for the new Feature without inventing placeholder Functionalities", + "Emits a change summary with the new Feature id, every entity the entry moved on, and the Functionalities flagged for test review" + ] } ] -} \ No newline at end of file +} diff --git a/skills/living-doc-update/evals/files/dependency-promotion-catalog.json b/skills/living-doc-update/evals/files/dependency-promotion-catalog.json new file mode 100644 index 0000000..c795cd5 --- /dev/null +++ b/skills/living-doc-update/evals/files/dependency-promotion-catalog.json @@ -0,0 +1,102 @@ +{ + "generated_at": "2026-09-29T08:00:00Z", + "catalog": { + "user_stories": [ + { + "id": "US-001", + "name": "Place an online order", + "status": "active", + "features": ["FEAT-001", "FEAT-002"] + }, + { + "id": "US-002", + "name": "View account overview", + "status": "active", + "features": ["FEAT-003"] + } + ], + "features": [ + { + "id": "FEAT-001", + "name": "Checkout Page", + "surface_type": "UI", + "purpose": "Lets a customer review the basket, pay, and place the order.", + "user_stories": ["US-001"], + "functionalities": ["FUNC-001", "FUNC-002"], + "owners": ["team-checkout"], + "external_dependencies": ["payment-events", "order-service"] + }, + { + "id": "FEAT-002", + "name": "Orders API", + "surface_type": "API", + "purpose": "Creates orders and keeps their payment status in step with the payment provider.", + "user_stories": ["US-001"], + "functionalities": ["FUNC-003"], + "owners": ["team-orders"], + "external_dependencies": ["order-db", "Payment-Events"] + }, + { + "id": "FEAT-003", + "name": "Account Dashboard", + "surface_type": "UI", + "purpose": "Shows a customer their balances, recent orders, and saved payment methods.", + "user_stories": ["US-002"], + "functionalities": ["FUNC-004"], + "owners": ["team-accounts"], + "external_dependencies": ["accounts-api"] + } + ], + "functionalities": [ + { + "id": "FUNC-001", + "name": "Checkout Page - Publish payment requested event", + "parent_feature": "FEAT-001", + "func_type": "button_action", + "status": "active", + "acceptance_criteria": [ + {"id": "AC:FUNC-001-01", "description": "When the customer confirms payment, a payment-requested event is published with the order id and amount", "state": "active"}, + {"id": "AC:FUNC-001-02", "description": "When publishing fails, the order is not placed and the error PAYMENT_UNAVAILABLE is shown", "state": "active"} + ], + "notes": ["Integration tests stub payment-events with an in-memory fake publisher."] + }, + { + "id": "FUNC-002", + "name": "Checkout Page - Show order total", + "parent_feature": "FEAT-001", + "func_type": "calculation", + "status": "active", + "acceptance_criteria": [ + {"id": "AC:FUNC-002-01", "description": "The order total equals the sum of line totals plus delivery", "state": "active"} + ] + }, + { + "id": "FUNC-003", + "name": "Orders API - Reconcile payment status", + "parent_feature": "FEAT-002", + "func_type": "button_action", + "status": "active", + "acceptance_criteria": [ + {"id": "AC:FUNC-003-01", "description": "When a payment-settled event arrives, the order moves to PAID", "state": "active"}, + {"id": "AC:FUNC-003-02", "description": "When a payment-failed event arrives, the order moves to PAYMENT_FAILED", "state": "active"} + ], + "notes": ["Tests feed hand-built payment-events messages; no contract backs their shape."] + }, + { + "id": "FUNC-004", + "name": "Account Dashboard - Show recent orders", + "parent_feature": "FEAT-003", + "func_type": "component_state", + "status": "active", + "acceptance_criteria": [ + {"id": "AC:FUNC-004-01", "description": "The dashboard lists the five most recent orders, newest first", "state": "active"} + ] + } + ] + }, + "feature_registry": [ + {"feature_id": "FEAT-001", "paths": ["web/src/pages/checkout/**"]}, + {"feature_id": "FEAT-002", "paths": ["services/orders/src/api/**"]}, + {"feature_id": "FEAT-003", "paths": ["web/src/pages/account/**"]} + ] +} diff --git a/skills/living-doc-update/evals/fixture-map.md b/skills/living-doc-update/evals/fixture-map.md index fd8d0ad..d077407 100644 --- a/skills/living-doc-update/evals/fixture-map.md +++ b/skills/living-doc-update/evals/fixture-map.md @@ -10,22 +10,26 @@ | 6 | negative | *(no file — create US redirect to living-doc-create-user-story)* | | 7 | negative | *(no file — gap-finding redirect to living-doc-gap-finder)* | | 8 | paraphrase | *(no file — add AC phrased as "update the story")* | -| 9 | edge-case | *(no file — descope AC mid-sprint: state=deprecated, descoped_at/descoped_reason metadata preserved, AC not deleted)* | -| 10 | output-format | *(no file — AC text change: OLD/NEW diff + linked scenario list)* | -| 11 | file-based | `payment-living-doc.md` | AC-2 SLA change from 3 s to 1 s (p99) | -| 12 | happy-path | *(no file — Feature deprecation with superseded_by field)* | +| 9 | edge-case | *(no file — descope AC mid-sprint: AC stays 'planned' with no target version, reason recorded on a '- Rationale:' bullet, AC not deleted)* | +| 10 | happy-path | *(no file — AC text change: OLD/NEW diff + linked scenario list)* | +| 11 | happy-path | `payment-living-doc.md` — AC-2 SLA change from 3 s to 1 s (p99) | +| 12 | regression | *(no file — Feature deprecation with superseded_by field)* | | 13 | regression | *(no file — validate_entity.py post-update validation)* | -| 14 | regression | *(no file — US promotion blocked by missing error-path AC)* | +| 14 | edge-case | *(no file — US promotion blocked by missing error-path AC)* | +| 15 | regression | *(no file — business-rule change to an active AC: minor version bump, ID stable)* | +| 16 | regression | *(no file — Feature rename cascade)* | +| 17 | happy-path | `dependency-promotion-catalog.json` — `payment-events` promoted to an API Feature; named in `external_dependencies` by FEAT-001 *and* FEAT-002 (different casing), prompt mentions only FEAT-001 | ## Coverage summary -- happy-path: 2 (add AC to User Story, Feature deprecation with superseded_by) -- regression: 5 (US promotion check, deprecate Functionality, Feature ownership, AC ID stability, validate after update) +- happy-path: 4 (add AC to User Story, AC diff format, payment living doc SLA update, dependency promotion cascade) +- regression: 8 (US promotion check, deprecate Functionality, Feature ownership, AC ID stability, Feature deprecation with superseded_by, validate after update, active-AC version bump, Feature rename cascade) - negative: 2 (create US redirect, gap-finder redirect) - paraphrase: 1 (add AC phrased as "update the story") -- edge-case: 1 (descope AC mid-sprint) -- output-format: 1 (AC diff format) -- file-based: 1 (payment living doc SLA update) +- edge-case: 2 (descope AC mid-sprint, US promotion blocked by missing error-path AC) + +A dependency that is only added or only dropped has no eval of its own — nothing about either +cascades. The promotion (17) is the one situation that does. ## Rules exercised @@ -35,10 +39,13 @@ | US promotion invariants check | 2, 14 | | Deprecate entity — never delete | 3, 12 | | Feature ownership update in JSON + registry | 4 | -| AC ID stability when modifying description | 5 | +| AC ID stability when modifying description | 5, 15 | | Out-of-scope: create US → living-doc-create-user-story | 6 | | Out-of-scope: find gaps → living-doc-gap-finder | 7 | | Descope AC mid-sprint | 9 | | Change summary format with OLD/NEW diff | 10, 11 | | superseded_by field on Feature deprecation | 12 | | validate_entity.py post-update check | 13 | +| Minor version bump for a business-rule change to an active AC | 15 | +| Feature rename cascade | 16 | +| Dependency promotion: route creation to living-doc-create-feature, scan every Feature's `external_dependencies`, record the edge on the calling Functionality, no re-parenting, `ORPHAN_FEATURE` on the new Feature | 17 | diff --git a/skills/living-doc-update/evals/trigger-eval.json b/skills/living-doc-update/evals/trigger-eval.json index c761884..6930f70 100644 --- a/skills/living-doc-update/evals/trigger-eval.json +++ b/skills/living-doc-update/evals/trigger-eval.json @@ -148,5 +148,23 @@ "query": "I need to update a Feature's name because the domain split — walk me through all the cascade steps", "should_trigger": true, "reason": "Feature update cascade — living-doc-update owns the workflow for renaming/updating Features" + }, + { + "id": "t26-drop-dependency", + "query": "Checkout Page no longer calls the legacy fraud-check service — remove that external dependency from the living doc.", + "should_trigger": true, + "reason": "'remove external dependency' is a listed trigger phrase; dropping a dependency is covered by 'Change a Feature's dependencies'." + }, + { + "id": "t27-promote-dependency", + "query": "The payment-events topic got an AsyncAPI annotation, so that dependency became a feature. Update every Feature that lists it.", + "should_trigger": true, + "reason": "'dependency became a feature' / 'promote external dependency' are listed trigger phrases; the promotion cascade lives in living-doc-update." + }, + { + "id": "t28-not-create-unannotated", + "query": "Create a Feature for our inventory-updates Kafka topic — the consumer handler has no AsyncAPI annotation yet.", + "should_trigger": false, + "reason": "Creating a Feature, and deciding that an un-annotated topic has no anchor and stays an external_dependencies entry, belongs to living-doc-create-feature." } ] \ No newline at end of file diff --git a/skills/living-doc-update/scripts/test_validate_entity.py b/skills/living-doc-update/scripts/test_validate_entity.py index 30f3d29..ea107e2 100644 --- a/skills/living-doc-update/scripts/test_validate_entity.py +++ b/skills/living-doc-update/scripts/test_validate_entity.py @@ -11,9 +11,15 @@ """ __test__ = False # pytest: ignore this helper script +import json +import subprocess import sys +import tempfile +from pathlib import Path -from validate_entity import ID_PATTERNS, AC_ID_PATTERN, validate +from validate_entity import ID_PATTERNS, AC_ID_PATTERN, validate, load_ac_states_from_profile, format_report + +VALIDATE_ENTITY_PY = Path(__file__).parent / "validate_entity.py" def test_single_digit_entity_ids_accepted(): @@ -76,7 +82,6 @@ def test_end_to_end_slug_feature_id_flagged(): "name": "Checkout Page", "surface_type": "UI", "purpose": "Lets a customer complete a purchase", - "status": "active", "user_stories": ["US-1"], "functionalities": [], "owners": ["Team"], @@ -87,6 +92,1364 @@ def test_end_to_end_slug_feature_id_flagged(): print("✓ End-to-end: a slug Feature ID is flagged as an error") +def test_feature_with_status_field_rejected(): + """A Feature has no status field — its state is derived from its Functionalities.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Checkout Page", + "surface_type": "UI", + "purpose": "Lets a customer complete a purchase", + "status": "active", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + } + issues = validate(feat) + status_errors = [i for i in issues if i["field"] == "status" and i["severity"] == "error"] + assert status_errors, f"expected a status error for a Feature carrying 'status', got: {issues}" + print("✓ A Feature carrying a 'status' field is flagged as an error") + + +def test_feature_surface_type_canonical_set_only(): + """VALID_SURFACE_TYPES must match the canonical living-doc-glossary exactly: UI and API + are the only surface types. Service/Worker/Module/Library are not canon and must be + rejected, even though an older living-doc-create-feature/SKILL.md draft once documented + them — that skill has since been realigned to UI/API only.""" + def make_feature(surface_type: str) -> dict: + return { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Order Processor", + "surface_type": surface_type, + "purpose": "Processes orders in the background", + "user_stories": ["US-1"], + "functionalities": [], + "owners": ["Team"], + } + + for surface_type in ("UI", "API"): + issues = validate(make_feature(surface_type)) + surface_errors = [i for i in issues if i["field"] == "surface_type"] + assert not surface_errors, f"expected no surface_type error for '{surface_type}', got: {surface_errors}" + + for surface_type in ("Service", "Worker", "Module", "Library", "Frontend"): + issues = validate(make_feature(surface_type)) + surface_errors = [i for i in issues if i["field"] == "surface_type" and i["severity"] == "error"] + assert surface_errors, f"expected a surface_type error for '{surface_type}', got: {issues}" + print("✓ Feature surface_type accepts only the canonical UI/API set and rejects everything else") + + +def test_orphan_feature_reported_distinctly(): + """A Feature with no linked User Stories and no Functionalities is an ORPHAN_FEATURE, + not a 'candidate' status — there is no status field to tag it with.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-2", + "name": "Exploratory Page", + "surface_type": "UI", + "purpose": "An exploratory surface with no confirmed links yet", + "user_stories": [], + "functionalities": [], + "owners": ["Team"], + } + issues = validate(feat) + orphan_issues = [i for i in issues if i["field"] == "ORPHAN_FEATURE"] + assert orphan_issues, f"expected an ORPHAN_FEATURE warning, got: {issues}" + print("✓ A Feature with no User Stories and no Functionalities is flagged as ORPHAN_FEATURE") + + +def test_orphan_feature_with_functionalities_still_reported(): + """gap-finder's ORPHAN_FEATURE gap (compute_gaps.py) fires on a missing User Story link + alone. A Feature that owns Functionalities but has no User Stories must still be flagged + as ORPHAN_FEATURE, not downgraded to a generic 'user_stories' warning.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-3", + "name": "Reporting Engine", + "surface_type": "UI", + "purpose": "Generates account activity reports for internal review", + "user_stories": [], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + } + issues = validate(feat) + orphan_issues = [i for i in issues if i["field"] == "ORPHAN_FEATURE"] + assert orphan_issues, f"expected an ORPHAN_FEATURE warning, got: {issues}" + func_issues = [i for i in issues if i["field"] == "functionalities"] + assert not func_issues, f"did not expect a functionalities warning, got: {func_issues}" + print("✓ A Feature with Functionalities but no User Stories is still flagged as ORPHAN_FEATURE") + + +def test_orphan_feature_suppressed_by_catalog_back_link(): + """A Feature with an empty own `user_stories` list, but referenced via a User + Story's forward `features` link in the catalog, must not be flagged ORPHAN_FEATURE — + matches compute_gaps.py's Gap 3, which honours both link directions.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-4", + "name": "Back-linked Surface", + "surface_type": "UI", + "purpose": "A surface linked only from the User Story side", + "user_stories": [], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + } + catalog = { + "catalog": { + "user_stories": [{"id": "US-1", "name": "Do a thing", "features": ["FEAT-4"]}], + "features": [], + "functionalities": [], + }, + } + issues = validate(feat, catalog) + orphan_issues = [i for i in issues if i["field"] == "ORPHAN_FEATURE"] + assert not orphan_issues, f"did not expect ORPHAN_FEATURE for a catalog back-linked Feature, got: {orphan_issues}" + print("✓ A Feature linked only via a catalog User Story's forward link is not flagged orphan") + + +def test_functionality_warning_suppressed_by_catalog_back_link(): + """A Feature with an empty own `functionalities` list, but referenced via a + Functionality's `parent_feature` back-link in the catalog, must not get the + 'functionalities' warning — matches compute_gaps.py's EMPTY_FEATURE gap, which unions + a Feature's forward `functionalities` list with the catalog back-link + (feature_func_counts), the same treatment ORPHAN_FEATURE already gets for user_stories.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-5", + "name": "Back-linked Surface", + "surface_type": "UI", + "purpose": "A surface whose Functionality is linked only from the Functionality side", + "user_stories": ["US-1"], + "functionalities": [], + "owners": ["Team"], + } + catalog = { + "catalog": { + "user_stories": [], + "features": [], + "functionalities": [{"id": "FUNC-1", "parent_feature": "FEAT-5"}], + }, + } + issues = validate(feat, catalog) + func_issues = [i for i in issues if i["field"] == "functionalities"] + assert not func_issues, f"did not expect a functionalities warning for a catalog back-linked Feature, got: {func_issues}" + print("✓ A Feature linked only via a catalog Functionality's parent_feature back-link is not flagged") + + +def test_functionality_warning_still_reported_without_back_link(): + """The catalog back-link suppression must not swallow a genuine gap — a Feature with + an empty `functionalities` list and no matching catalog back-link still warns.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-6", + "name": "Genuinely Empty Surface", + "surface_type": "UI", + "purpose": "A surface with no Functionalities anywhere in the catalog", + "user_stories": ["US-1"], + "functionalities": [], + "owners": ["Team"], + } + catalog = { + "catalog": { + "user_stories": [], + "features": [], + "functionalities": [{"id": "FUNC-1", "parent_feature": "FEAT-OTHER"}], + }, + } + issues = validate(feat, catalog) + func_issues = [i for i in issues if i["field"] == "functionalities"] + assert func_issues, f"expected a functionalities warning when no catalog back-link matches, got: {issues}" + print("✓ A Feature with no Functionalities and no matching catalog back-link is still flagged") + + +def test_profile_missing_pyyaml_hard_fails(): + """Missing pyyaml must hard-fail with a clear message, not silently fall back to + unrestricted canonical validation as if --profile had been omitted. Regression: a + prior version caught ImportError in the same except clause as 'profile file not + found' and returned None with just a stderr warning, so a profile narrowing + ac_states would be silently ignored in any environment without pyyaml installed.""" + original = sys.modules.get("yaml", "__absent__") + sys.modules["yaml"] = None # forces `import yaml` to raise ImportError + try: + try: + load_ac_states_from_profile("irrelevant-path.yaml") + except SystemExit as exc: + assert exc.code == 1, f"expected exit code 1, got: {exc.code}" + else: + raise AssertionError( + "expected load_ac_states_from_profile to exit(1) when pyyaml is unavailable" + ) + finally: + if original == "__absent__": + del sys.modules["yaml"] + else: + sys.modules["yaml"] = original + print("✓ Missing pyyaml hard-fails instead of silently falling back to unrestricted validation") + + +def _run_validate_entity(*args: str) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(VALIDATE_ENTITY_PY), *args], + capture_output=True, + text=True, + encoding="utf-8", + ) + + +def test_profile_ac_states_subset_accepted(): + """A profile's ac_states that is a subset of the canonical set narrows validation — + an AC state inside that narrower subset validates clean, and a state that is still + canonical but excluded by the profile must be flagged AND must actually block: + `valid: false` and a nonzero exit code, not just an advisory issue entry. Regression + for a bug where the excluded-state check was a warning, not an error, so it produced + a state issue but still returned exit code 0 / valid: true — silently failing to + enforce the configured vocabulary (PR #40 review 5234392828).""" + def make_us(state: str) -> dict: + return { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": ["FEAT-1"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": state}, + ], + } + + with tempfile.TemporaryDirectory() as tmp: + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: [planned, active]\n", encoding="utf-8") + + in_subset_path = Path(tmp) / "entity-in-subset.json" + in_subset_path.write_text(json.dumps(make_us("active")), encoding="utf-8") + result = _run_validate_entity(str(in_subset_path), "--profile", str(profile_path), "--json") + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["valid"], payload + state_issues = [i for i in payload["issues"] if i["field"].endswith(".state")] + assert not state_issues, f"did not expect a state issue for 'active' inside the narrowed profile, got: {state_issues}" + + excluded_path = Path(tmp) / "entity-excluded.json" + excluded_path.write_text(json.dumps(make_us("in_review")), encoding="utf-8") + result = _run_validate_entity(str(excluded_path), "--profile", str(profile_path), "--json") + payload = json.loads(result.stdout) + state_issues = [i for i in payload["issues"] if i["field"].endswith(".state")] + assert state_issues, f"expected a state issue for 'in_review' excluded by the narrowed profile, got: {payload}" + assert not payload["valid"], ( + f"a profile-excluded AC state must make the entity invalid, got valid=True: {payload}" + ) + assert result.returncode == 1, ( + f"a profile-excluded AC state must exit nonzero, got {result.returncode}: {payload}" + ) + assert state_issues[0]["severity"] == "error", ( + f"a profile-excluded AC state must be an error, not a warning, got: {state_issues}" + ) + + print("✓ --profile ac_states that is a canonical subset narrows validation as expected, and actually blocks") + + +def test_profile_narrows_entity_status_too(): + """--profile must narrow the authored entity `status` field (User Story / Functionality), + not just AC `state` — both are the same canonical vocabulary (living-doc-bdd-schemas.md). + Regression for a bug where --profile reassigned only VALID_AC_STATUSES, so a profile with + ac_states: [planned, active] still accepted an entity with status: in_review.""" + + def make_us(status: str) -> dict: + return { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": status, + "features": ["FEAT-1"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": "active"}, + ], + } + + with tempfile.TemporaryDirectory() as tmp: + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: [planned, active]\n", encoding="utf-8") + + in_subset_path = Path(tmp) / "entity-in-subset.json" + in_subset_path.write_text(json.dumps(make_us("active")), encoding="utf-8") + result = _run_validate_entity(str(in_subset_path), "--profile", str(profile_path), "--json") + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + status_issues = [i for i in payload["issues"] if i["field"] == "status"] + assert not status_issues, f"did not expect a status issue for 'active' inside the narrowed profile, got: {status_issues}" + + excluded_path = Path(tmp) / "entity-excluded.json" + excluded_path.write_text(json.dumps(make_us("in_review")), encoding="utf-8") + result = _run_validate_entity(str(excluded_path), "--profile", str(profile_path), "--json") + payload = json.loads(result.stdout) + status_issues = [i for i in payload["issues"] if i["field"] == "status"] + assert status_issues, f"expected a status issue for 'in_review' excluded by the narrowed profile, got: {payload}" + + print("✓ --profile ac_states narrows the entity 'status' field, not just AC 'state'") + + +def test_profile_ac_states_outside_canon_rejected(): + """A profile's ac_states containing a value outside the canonical four states must be + rejected up front, not silently substituted for the canonical set — otherwise an AC in + a made-up state (e.g. 'done') would validate successfully.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: [done]\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "not a subset of the canonical" in result.stderr, result.stderr + print("✓ --profile ac_states outside the canonical set is rejected instead of applied") + + +def test_profile_ac_states_empty_list_rejected(): + """A profile's `ac_states: []` is schema-invalid (minItems: 1) — it must be rejected + up front, not treated the same as an omitted field. Regression for a bug where + load_ac_states_from_profile() returned None for both cases, so --profile silently + fell back to the canonical defaults instead of enforcing the configured vocabulary.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: []\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "invalid `ac_states`" in result.stderr, result.stderr + print("✓ --profile ac_states: [] is rejected instead of silently falling back to canonical defaults") + + +def test_profile_ac_states_wrong_type_rejected(): + """A profile's `ac_states` that isn't an array (e.g. a string) is schema-invalid — + it must be rejected up front rather than silently ignored like an omitted field.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: active\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "invalid `ac_states`" in result.stderr, result.stderr + print("✓ --profile ac_states of the wrong type is rejected instead of silently applied") + + +def test_profile_ac_states_non_string_item_rejected(): + """A profile's `ac_states` list containing a non-string item (e.g. a YAML mapping) is + schema-invalid — it must be rejected up front with a diagnostic, not reach `set(states)` + and crash with an unhandled TypeError on the unhashable item.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: [{foo: bar}, active]\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "invalid `ac_states`" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ --profile ac_states with a non-string item is rejected instead of crashing") + + +def test_profile_ac_states_duplicate_rejected(): + """A profile's `ac_states` list containing a duplicate value is schema-invalid — it must + be rejected up front through the CLI, not just through the separate JSON Schema check, so + a regression in load_ac_states_from_profile()'s own duplicate check would fail this test + even if the schema-level test still passed.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: [active, active]\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "invalid `ac_states`" in result.stderr, result.stderr + assert "duplicates" in result.stderr, result.stderr + print("✓ --profile ac_states: [active, active] is rejected instead of silently applied") + + +def test_profile_scalar_root_rejected(): + """A profile whose YAML root is a scalar (e.g. `42`) is schema-invalid — it must be + rejected up front with a diagnostic, not reach the `"ac_states" not in profile` + membership test and crash with an unhandled TypeError.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("42\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "must be a mapping" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ --profile with a scalar YAML root is rejected instead of crashing") + + +def test_profile_list_root_rejected(): + """A profile whose YAML root is a list is schema-invalid — it must be rejected up + front, not silently treated as though `ac_states` were omitted (the `in` membership + test on a list checks its elements, not mapping keys, so it never matches).""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("- active\n- planned\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "must be a mapping" in result.stderr, result.stderr + print("✓ --profile with a list YAML root is rejected instead of silently ignored") + + +def test_profile_missing_file_hard_fails(): + """A --profile path that doesn't exist must hard-fail with a clear diagnostic, not + fall back to unrestricted canonical validation as though --profile were omitted. + Regression for the bug the FileNotFoundError/ValueError except clause used to hide: + it printed a warning and returned None, which main() cannot distinguish from a + validly-omitted `ac_states`.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + missing_profile = Path(tmp) / "does-not-exist.yaml" + result = _run_validate_entity(str(entity_path), "--profile", str(missing_profile)) + assert result.returncode == 1, result.stdout + assert "could not load profile" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ A missing --profile file hard-fails instead of silently validating unrestricted") + + +def test_profile_directory_path_hard_fails(): + """A --profile path that points at a directory raises PermissionError on Windows + (IsADirectoryError on POSIX) — neither is a FileNotFoundError, so the except clause + must catch OSError broadly, not just FileNotFoundError, or this crashes with a raw + traceback instead of a clean exit(1).""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_dir = Path(tmp) / "a-directory.yaml" + profile_dir.mkdir() + result = _run_validate_entity(str(entity_path), "--profile", str(profile_dir)) + assert result.returncode == 1, result.stdout + assert "could not load profile" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ A --profile path pointing at a directory hard-fails instead of crashing") + + +def test_profile_invalid_encoding_hard_fails(): + """A profile file that isn't valid UTF-8 raises UnicodeDecodeError (a ValueError + subclass) while reading, before yaml.safe_load ever runs — must hard-fail cleanly, + not crash with a raw traceback.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_bytes(b"ac_states: [\xff\xfe]") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "could not load profile" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ A non-UTF-8 --profile file hard-fails instead of crashing") + + +def test_profile_invalid_yaml_syntax_hard_fails(): + """A profile with a YAML syntax error (as opposed to a wrong-typed but syntactically + valid root) must hit the yaml.YAMLError branch and hard-fail cleanly. This branch + was added in commit 748b767 with no dedicated test, so a regression here would have + gone unnoticed.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: [planned\n active\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "invalid YAML" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ A profile with a YAML syntax error hard-fails instead of crashing") + + +def test_profile_empty_file_treated_as_omitted(): + """A genuinely empty profile file (yaml.safe_load returns None) is not malformed — + it must be treated the same as an omitted `ac_states`, i.e. unrestricted canonical + validation, not rejected as a non-mapping root.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text( + json.dumps({ + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "in_review", + "features": ["FEAT-1"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": "in_review"}, + ], + }), + encoding="utf-8", + ) + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path), "--json") + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["valid"], payload + print("✓ An empty --profile file is treated as an omitted ac_states, not a malformed root") + + +def test_profile_falsey_non_mapping_roots_rejected(): + """A profile YAML root that is falsey but not a mapping (false, 0, an empty string, + an empty list) must still be rejected as malformed. Regression for the bug in + `yaml.safe_load(f) or {}`: any falsey root — not just None — was silently coerced + into an empty dict, bypassing the 'root must be a mapping' check entirely. An empty + list is the sharpest case: `test_profile_list_root_rejected` above only covers a + *non-empty* list, which is truthy and was never affected by the `or {}` bug.""" + for content in ["false\n", "0\n", '""\n', "[]\n"]: + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text(content, encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, f"content={content!r}, stdout={result.stdout}" + assert "must be a mapping" in result.stderr, f"content={content!r}, stderr={result.stderr}" + print("✓ Falsey non-mapping --profile roots (false/0/\"\"/[]) are rejected, not silently treated as {}") + + +def test_profile_null_ac_states_rejected(): + """`ac_states: null` is present but not a list — it must be rejected the same way + as any other wrong-typed `ac_states`, not treated as 'omitted' since the key is + present in the mapping.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + profile_path = Path(tmp) / ".project-profile.yaml" + profile_path.write_text("ac_states: null\n", encoding="utf-8") + result = _run_validate_entity(str(entity_path), "--profile", str(profile_path)) + assert result.returncode == 1, result.stdout + assert "invalid `ac_states`" in result.stderr, result.stderr + print("✓ --profile ac_states: null is rejected instead of treated as omitted") + + +# ── validate(): entity_type handling ──────────────────────────────────────────── + + +def test_missing_entity_type_flagged(): + """An entity with neither 'entity_type' nor 'type' must be rejected up front with a + single, specific error — and must short-circuit rather than fall through into field + checks that assume a known entity shape.""" + issues = validate({"id": "US-1", "name": "Nothing"}) + assert len(issues) == 1, f"expected exactly one issue for a missing entity_type, got: {issues}" + assert issues[0]["field"] == "entity_type" and issues[0]["severity"] == "error" + print("✓ An entity with no entity_type/type is flagged and short-circuits further checks") + + +def test_unknown_entity_type_flagged(): + """An entity_type outside {User Story, Feature, Functionality} must be rejected — + REQUIRED_FIELDS[entity_type] would otherwise KeyError deeper in validate().""" + issues = validate({"entity_type": "Bug", "id": "BUG-1"}) + assert len(issues) == 1, f"expected exactly one issue for an unknown entity_type, got: {issues}" + assert issues[0]["field"] == "entity_type" + print("✓ An unrecognised entity_type is flagged instead of crashing") + + +def test_type_alias_accepted(): + """The 'type' key is accepted as an alias for 'entity_type' via + `entity.get("entity_type") or entity.get("type")`.""" + us = { + "type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": ["FEAT-1"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": "active"}, + ], + } + issues = validate(us) + assert not [i for i in issues if i["field"] == "entity_type"], issues + print("✓ The 'type' key is accepted as an alias for 'entity_type'") + + +# ── validate(): required fields, deprecation, AC and per-type checks ─────────── + + +def test_required_field_missing_flagged(): + """Each entity type's REQUIRED_FIELDS must actually be enforced when a field is + entirely omitted (not just left empty) — mirrors the existing empty-string case.""" + us = { + "entity_type": "User Story", + "id": "US-1", + "status": "active", + "features": ["FEAT-1"], + "acceptance_criteria": [{"id": "AC:US-1-01", "description": "fails on invalid input"}], + } # missing 'name' + assert any(i["field"] == "name" and i["severity"] == "error" for i in validate(us)), validate(us) + + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Checkout Page", + "surface_type": "UI", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + } # missing 'purpose' + assert any(i["field"] == "purpose" and i["severity"] == "error" for i in validate(feat)), validate(feat) + + func = { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "Login Page - Validate Password", + "parent_feature": "FEAT-1", + "acceptance_criteria": [{"id": "AC:FUNC-1-01", "description": "fails on invalid input"}], + } # missing 'status' + assert any(i["field"] == "status" and i["severity"] == "error" for i in validate(func)), validate(func) + print("✓ A missing (not just empty) required field is flagged for every entity type") + + +def test_deprecated_user_story_missing_metadata_warns(): + """A User Story with status: deprecated but no deprecated_at/deprecation_reason must + warn for both — deprecation metadata is required for the audit trail.""" + us = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "deprecated", + "features": ["FEAT-1"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": "deprecated"}, + ], + } + dep_fields = {i["field"] for i in validate(us) if i["field"] in ("deprecated_at", "deprecation_reason")} + assert dep_fields == {"deprecated_at", "deprecation_reason"}, f"expected both deprecation warnings, got: {dep_fields}" + print("✓ A deprecated User Story missing deprecation metadata warns for both fields") + + +def test_deprecated_feature_via_markers_warns(): + """A Feature carries no 'status' field, so its deprecation is derived from its + Functionalities — which needs the catalog. Given one, a Feature whose Functionalities + are all deprecated is deprecated; one marker present (deprecated_at) but not the other + must still warn only for the missing one. + + Updated from the pre-P35-AG2 behaviour, where the same assertions held with no catalog + at all because the authored markers alone were read as the Feature's state.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Legacy Reports", + "surface_type": "UI", + "purpose": "Generates legacy account reports, replaced by the new dashboard", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + "deprecated_at": "2026-01-01", + } + issues = validate(feat, _feature_catalog(["deprecated"])) + assert any(i["field"] == "deprecation_reason" for i in issues), issues + # deprecated_at is now flagged for the opposite reason: a Feature must not author it. + authored_at = [i for i in issues if i["field"] == "deprecated_at"] + assert authored_at and "must not author" in authored_at[0]["message"], issues + print("✓ A Feature derived as deprecated is still checked for deprecation metadata") + + +def test_deprecated_feature_with_empty_marker_still_warns(): + """A Feature with `deprecation_reason: ""` (key present, value falsy) must still have + the empty value reported — presence of the key is a deprecation attempt with malformed + metadata, not an absent key. Before the original fix, `entity.get(...) or ...` treated + the two the same and the malformed metadata went unreported. + + The marker under test moved from `deprecated_at` to `deprecation_reason` because a + Feature no longer authors `deprecated_at` at all; the truthiness point is unchanged.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Legacy Reports", + "surface_type": "UI", + "purpose": "Generates legacy account reports, replaced by the new dashboard", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + "deprecation_reason": "", + } + issues = validate(feat, _feature_catalog(["deprecated"])) + assert any(i["field"] == "deprecation_reason" for i in issues), issues + print("✓ A Feature with an empty-string deprecation marker is still flagged for missing metadata") + + +def test_feature_authoring_deprecated_at_is_flagged(): + """Canon derives a Feature's `deprecated_at` with its state; a Feature issue body + carries no `## Deprecated At` heading and a PageObject header no `deprecated_at:` + field. Authoring it must be reported — and never reported the other way round, as a + *missing* field, which would instruct the agent to author a derived value.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Legacy Reports", + "surface_type": "UI", + "purpose": "Generates legacy account reports, replaced by the new dashboard", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + "deprecation_reason": "Replaced by the new reporting dashboard", + } + # Derived as deprecated, authoring nothing it shouldn't: no deprecated_at complaint. + clean = validate(feat, _feature_catalog(["deprecated"])) + assert not [i for i in clean if i["field"] == "deprecated_at"], clean + + feat["deprecated_at"] = "2026-01-01" + issues = validate(feat, _feature_catalog(["deprecated"])) + flagged = [i for i in issues if i["field"] == "deprecated_at"] + assert flagged and "must not author" in flagged[0]["message"], issues + print("✓ A Feature authoring 'deprecated_at' is flagged; a Feature omitting it is not") + + +def test_unknown_deprecation_key_is_not_a_deprecation_marker(): + """`deprecated_code_commit` used to be a recognised Feature deprecation marker here. + It is in neither the canon nor living-doc-utilities' contract — the removing commit is + now recorded as a `## Notes` bullet instead — so an entity carrying it must NOT be + treated as deprecated on that basis. This test previously pinned the opposite + behaviour; it is updated, not deleted, so the removal stays covered. + + The key is simply unrecognised: no deprecation detection, and therefore none of the + deprecated_at/deprecation_reason missing-metadata warnings.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Legacy Reports", + "surface_type": "UI", + "purpose": "Generates legacy account reports, replaced by the new dashboard", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + "deprecated_code_commit": "abc1234", + } + issues = validate(feat) + assert not [i for i in issues if i["field"] == "deprecated_at"], issues + assert not [i for i in issues if i["field"] == "deprecation_reason"], issues + assert not [i for i in issues if i["field"] == "deprecation"], issues + print("✓ deprecated_code_commit is no longer a deprecation marker") + + +def test_feature_state_not_claimed_without_catalog(): + """A Feature has no authored status — its state is derived from its Functionalities, + which a single entity does not contain. Without --catalog the validator must not claim + a state; when the Feature authors deprecation metadata it reports the basis explicitly + as an authored marker, and points at --catalog.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Legacy Reports", + "surface_type": "UI", + "purpose": "Generates legacy account reports, replaced by the new dashboard", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + "superseded_by": "FEAT-2", + } + issues = validate(feat) + basis = [i for i in issues if i["field"] == "deprecation"] + assert basis, issues + assert "authored marker" in basis[0]["message"], basis + assert "--catalog" in basis[0]["message"], basis + # No derived state was claimed, so no missing-metadata warnings were emitted either. + assert not [i for i in issues if i["field"] in ("deprecated_at", "deprecation_reason")], issues + print("✓ Without a catalog, a Feature's state is not claimed and the basis is named") + + +def test_feature_with_no_markers_and_no_catalog_is_silent(): + """A Feature that authors nothing and is validated without a catalog gets no + deprecation commentary at all — not 'deprecated', and not 'not deprecated'.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Reports Page", + "surface_type": "UI", + "purpose": "Generates account reports for finance and support teams", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1"], + "owners": ["Team"], + } + issues = validate(feat) + assert not [ + i for i in issues + if i["field"] in ("deprecation", "deprecated_at", "deprecation_reason") + ], issues + print("✓ A Feature with no deprecation metadata and no catalog draws no state claim") + + +def _feature_catalog(func_statuses: list[str]) -> dict: + """Catalog whose FUNC-1..n all have FEAT-1 as parent, with the given statuses.""" + return { + "features": [{"id": "FEAT-1"}], + "user_stories": [{"id": "US-1", "features": ["FEAT-1"]}], + "functionalities": [ + {"id": f"FUNC-{i + 1}", "parent_feature": "FEAT-1", "status": status} + for i, status in enumerate(func_statuses) + ], + } + + +def _legacy_feature(**extra) -> dict: + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Legacy Reports", + "surface_type": "UI", + "purpose": "Generates legacy account reports, replaced by the new dashboard", + "user_stories": ["US-1"], + "functionalities": ["FUNC-1", "FUNC-2"], + "owners": ["Team"], + } + feat.update(extra) + return feat + + +def test_feature_deprecation_derived_from_catalog(): + """With --catalog the Feature's state is derived the canonical way: every owned + Functionality deprecated means the Feature is deprecated, and the missing + deprecation_reason is then reported against a state that was actually determined.""" + catalog = _feature_catalog(["deprecated", "deprecated"]) + issues = validate(_legacy_feature(superseded_by="FEAT-2"), catalog) + assert any(i["field"] == "deprecation_reason" for i in issues), issues + # deprecated_at is derived for a Feature, so it is never reported as missing. + assert not [i for i in issues if i["field"] == "deprecated_at"], issues + # The state was derived, so no authored-marker basis disclaimer is emitted. + assert not [i for i in issues if i["field"] == "deprecation"], issues + print("✓ With a catalog, a Feature's deprecation is derived from its Functionalities") + + +def test_feature_not_deprecated_when_a_functionality_is_live(): + """One non-deprecated Functionality means the Feature is not deprecated, however much + deprecation metadata it authors — and that contradiction is reported.""" + catalog = _feature_catalog(["deprecated", "active"]) + issues = validate(_legacy_feature(deprecation_reason="Replaced by the new dashboard"), catalog) + contradiction = [i for i in issues if i["field"] == "deprecation"] + assert contradiction, issues + assert "not deprecated" in contradiction[0]["message"], contradiction + # Not deprecated, so no missing-metadata warnings for a state it does not have. + assert not [i for i in issues if i["field"] == "deprecation_reason"], issues + print("✓ A Feature with a live Functionality is not reported as deprecated") + + +def test_feature_with_no_owned_functionalities_claims_no_state(): + """A catalog that gives the Feature no Functionalities leaves nothing to derive from. + The EMPTY_FEATURE warning already covers that; the validator must not additionally + invent a deprecation state.""" + catalog = {"features": [{"id": "FEAT-1"}], "user_stories": [], "functionalities": []} + issues = validate(_legacy_feature(functionalities=[], superseded_by="FEAT-2"), catalog) + basis = [i for i in issues if i["field"] == "deprecation"] + assert basis, issues + assert "authored marker" in basis[0]["message"], basis + assert not [i for i in issues if i["field"] in ("deprecated_at", "deprecation_reason")], issues + print("✓ A Feature with no owned Functionalities is not assigned a derived state") + + +def test_functionality_missing_func_type_flagged(): + """`func_type` is required on a Functionality — canon marks `# func_type:` Yes, and + without it the entity cannot be authored into a feature-file header at all.""" + func = { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "Login Page - Validate Password Strength", + "parent_feature": "FEAT-1", + "status": "planned", + "acceptance_criteria": [ + {"id": "AC:FUNC-1-01", "description": "When the password is under 8 characters, " + "validation returns INVALID with code PWD_TOO_SHORT"}, + ], + } + issues = validate(func) + assert any( + i["field"] == "func_type" and i["severity"] == "error" for i in issues + ), issues + print("✓ A Functionality without func_type is an error") + + +def test_functionality_invalid_func_type_flagged(): + """A value outside the seven canonical categories must be an error, not a warning — + it goes straight into a `# func_type:` header line where only those seven are valid.""" + func = { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "Login Page - Validate Password Strength", + "parent_feature": "FEAT-1", + "func_type": "form_validation", # not one of the seven — the real value is field_validation + "status": "planned", + "acceptance_criteria": [ + {"id": "AC:FUNC-1-01", "description": "When the password is under 8 characters, " + "validation returns INVALID with code PWD_TOO_SHORT"}, + ], + } + issues = validate(func) + bad = [i for i in issues if i["field"] == "func_type"] + assert bad and bad[0]["severity"] == "error", issues + assert "form_validation" in bad[0]["message"], bad + print("✓ A func_type outside the seven canonical values is an error") + + +def test_functionality_all_canonical_func_types_accepted(): + """Each of the seven canonical values must pass — a narrowed set here would silently + block a legitimate Functionality.""" + for value in ( + "component_state", "component_action", "button_action", + "field_validation", "calculation", "visibility", "navigation_rule", + ): + func = { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "Login Page - Validate Password Strength", + "parent_feature": "FEAT-1", + "func_type": value, + "status": "planned", + "acceptance_criteria": [ + {"id": "AC:FUNC-1-01", "description": "When the password is under 8 characters, " + "validation returns INVALID with code PWD_TOO_SHORT"}, + ], + } + assert not [i for i in validate(func) if i["field"] == "func_type"], value + print("✓ All seven canonical func_type values are accepted") + + +def test_ac_missing_id_and_description_flagged(): + """An AC missing 'id' or 'description' must each raise their own error — + _validate_ac() checks them independently.""" + us = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": ["FEAT-1"], + "acceptance_criteria": [{}], + } + fields = {i["field"] for i in validate(us)} + assert "acceptance_criteria[0].id" in fields, fields + assert "acceptance_criteria[0].description" in fields, fields + print("✓ An AC missing 'id' and 'description' is flagged for both, independently") + + +def test_ac_unrecognized_state_errors(): + """An AC 'state' outside the canonical vocabulary must be an error, not a warning — + it is the same vocabulary as the entity-level `status` field (already an error), and + only an error actually blocks validation for a --profile-narrowed subset.""" + us = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": ["FEAT-1"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": "done"}, + ], + } + issues = validate(us) + state_issues = [i for i in issues if i["field"] == "acceptance_criteria[0].state"] + assert state_issues, issues + assert state_issues[0]["severity"] == "error", f"expected an error, got: {state_issues}" + print("✓ An AC with an unrecognised state is flagged as an error") + + +def test_ac_bare_string_errors_instead_of_crashing(): + """A bare-string AC has no ID, so nothing can link a test or scenario to it. It must be + reported as an error on that AC — for a Functionality and a User Story alike — not raise + AttributeError on the first `.get`.""" + func = { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "Checkout Page - Validate discount code expiry", + "parent_feature": "FEAT-1", + "func_type": "field_validation", + "status": "planned", + "acceptance_criteria": [ + "When the discount code is expired, validation returns INVALID with code DISCOUNT_EXPIRED.", + {"id": "AC:FUNC-1-02", "description": "When the code is valid, it is accepted"}, + ], + } + us = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "planned", + "features": ["FEAT-1"], + "acceptance_criteria": ["Order fails when payment is invalid"], + } + for entity, parent in ((func, "FUNC-1"), (us, "US-1")): + issues = validate(entity) + bare = [i for i in issues if i["field"] == "acceptance_criteria[0]"] + assert len(bare) == 1 and bare[0]["severity"] == "error", issues + assert f"AC:{parent}-01" in bare[0]["message"], bare + # The well-formed AC beside it is still validated normally, with no issue of its own. + assert not [i for i in validate(func) if i["field"].startswith("acceptance_criteria[1]")] + print("✓ A bare-string AC is an error with an example of the object form, not a crash") + + +def test_functionality_name_missing_separator_warns(): + """A Functionality name without the canonical ' - ' separator must warn — that + pattern is how a Functionality stays traceable to its parent Feature by name.""" + func = { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "ValidatePasswordStrength", + "parent_feature": "FEAT-1", + "status": "active", + "acceptance_criteria": [ + {"id": "AC:FUNC-1-01", "description": "Fails when the password is too short", "state": "active"}, + ], + } + issues = validate(func) + assert any(i["field"] == "name" for i in issues), issues + print("✓ A Functionality name without the canonical ' - ' separator is flagged") + + +def test_functionality_parent_feature_bad_format_flagged(): + """A Functionality's parent_feature must look like a Feature ID (FEAT-nnn) — anything + else is an error, since it breaks the Feature -> Functionality link.""" + func = { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "Login Page - Validate Password Strength", + "parent_feature": "US-1", + "status": "active", + "acceptance_criteria": [ + {"id": "AC:FUNC-1-01", "description": "Fails when the password is too short", "state": "active"}, + ], + } + issues = validate(func) + assert any(i["field"] == "parent_feature" and i["severity"] == "error" for i in issues), issues + print("✓ A Functionality parent_feature that isn't a FEAT- id is flagged as an error") + + +def test_user_story_no_features_warns(): + """A User Story with an empty 'features' list must warn — orphan User Stories are + reported by gap-finder.""" + us = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": [], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": "active"}, + ], + } + issues = validate(us) + assert any(i["field"] == "features" for i in issues), issues + print("✓ A User Story with no linked Features is flagged") + + +def test_user_story_no_acceptance_criteria_errors(): + """A User Story with zero ACs must error — it cannot be validated or implemented + without at least one.""" + us = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": ["FEAT-1"], + "acceptance_criteria": [], + } + issues = validate(us) + assert any(i["field"] == "acceptance_criteria" and i["severity"] == "error" for i in issues), issues + print("✓ A User Story with no Acceptance Criteria is flagged as an error") + + +def test_user_story_missing_error_path_warns(): + """A User Story whose ACs are all happy-path (no error/failure keywords) must warn — + at least one failure case is required.""" + us = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": ["FEAT-1"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order succeeds when payment is valid", "state": "active"}, + ], + } + issues = validate(us) + assert any(i["field"] == "acceptance_criteria" and i["severity"] == "warning" for i in issues), issues + print("✓ A User Story with only happy-path ACs is flagged for a missing error path") + + +# ── validate(): referential integrity against a --catalog ────────────────────── + + +def test_reference_missing_feature_for_user_story_warns(): + """A User Story referencing a Feature ID absent from the catalog must warn — this + exercises _validate_references(), which no prior test invoked directly (the existing + catalog-based tests all target the ORPHAN_FEATURE/functionalities back-link logic, + not this function).""" + us = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": ["FEAT-999"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": "active"}, + ], + } + catalog = {"catalog": {"features": [], "user_stories": [], "functionalities": []}} + issues = validate(us, catalog) + assert any( + i["field"] == "features" and "not found in catalog" in i["message"] for i in issues + ), issues + print("✓ A User Story referencing a Feature missing from the catalog is flagged") + + +def test_reference_missing_links_for_feature_warns(): + """A Feature referencing User Story / Functionality IDs absent from the catalog must + warn for each, independently.""" + feat = { + "entity_type": "Feature", + "id": "FEAT-1", + "name": "Checkout Page", + "surface_type": "UI", + "purpose": "Lets a customer complete a purchase", + "user_stories": ["US-999"], + "functionalities": ["FUNC-999"], + "owners": ["Team"], + } + catalog = {"catalog": {"features": [], "user_stories": [], "functionalities": []}} + issues = validate(feat, catalog) + assert any( + i["field"] == "user_stories" and "not found in catalog" in i["message"] for i in issues + ), issues + assert any( + i["field"] == "functionalities" and "not found in catalog" in i["message"] for i in issues + ), issues + print("✓ A Feature referencing a User Story and Functionality missing from the catalog is flagged for both") + + +def test_reference_missing_parent_feature_for_functionality_warns(): + """A Functionality whose parent_feature isn't in the catalog must warn — distinct + from the FEAT- format check, which only validates the ID's shape, not its existence.""" + func = { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "Login Page - Validate Password Strength", + "parent_feature": "FEAT-999", + "status": "active", + "acceptance_criteria": [ + {"id": "AC:FUNC-1-01", "description": "Fails when the password is too short", "state": "active"}, + ], + } + catalog = {"catalog": {"features": [], "user_stories": [], "functionalities": []}} + issues = validate(func, catalog) + assert any( + i["field"] == "parent_feature" and "not found in catalog" in i["message"] for i in issues + ), issues + print("✓ A Functionality whose parent_feature is missing from the catalog is flagged") + + +# ── feature_dependencies ────────────────────────────────────────────────────── + + +def _calling_functionality(targets) -> dict: + """FUNC-1 on FEAT-1 (Checkout Page), declaring the given feature_dependencies.""" + return { + "entity_type": "Functionality", + "id": "FUNC-1", + "name": "Checkout Page - Submit Payment", + "parent_feature": "FEAT-1", + "func_type": "button_action", + "status": "active", + "feature_dependencies": targets, + "acceptance_criteria": [ + {"id": "AC:FUNC-1-01", "description": "Rejects the order when payment fails", "state": "active"}, + ], + } + + +def _dependency_catalog(**target_overrides) -> dict: + """FEAT-1 is the caller's UI surface; FEAT-2 is an API Feature owning one live + Functionality. target_overrides patch FEAT-2; `func_status` sets its Functionality.""" + func_status = target_overrides.pop("func_status", "active") + target = {"id": "FEAT-2", "name": "Payment Events API", "surface_type": "API"} + target.update(target_overrides) + return { + "features": [ + {"id": "FEAT-1", "name": "Checkout Page", "surface_type": "UI"}, + target, + ], + "user_stories": [], + "functionalities": [ + {"id": "FUNC-1", "parent_feature": "FEAT-1", "status": "active"}, + {"id": "FUNC-2", "parent_feature": "FEAT-2", "status": func_status}, + ], + } + + +def _dependency_issues(issues: list[dict]) -> list[dict]: + return [i for i in issues if i["field"].startswith("feature_dependencies")] + + +def test_feature_dependency_valid_api_target_is_clean(): + """A live API Feature that is not the caller's own is a valid edge — no issue at all.""" + issues = validate(_calling_functionality(["FEAT-2"]), _dependency_catalog()) + assert not _dependency_issues(issues), issues + print("✓ A feature_dependencies edge to a live API Feature validates clean") + + +def test_feature_dependency_target_missing_from_catalog_warns(): + """Rule 1 — the target exists. A warning, not an error: canon expects an unresolved + target in a project documented in source code, where no API Feature can exist yet.""" + issues = validate(_calling_functionality(["FEAT-99"]), _dependency_catalog()) + deps = _dependency_issues(issues) + assert len(deps) == 1, issues + assert deps[0]["severity"] == "warning" and "not found in catalog" in deps[0]["message"], deps + print("✓ A feature_dependencies target missing from the catalog is flagged") + + +def test_feature_dependency_non_api_target_errors(): + """Rule 2 — the target is an API Feature. A UI target is an error: only a Feature with + a contract anchor can be called.""" + issues = validate(_calling_functionality(["FEAT-2"]), _dependency_catalog(surface_type="UI")) + deps = _dependency_issues(issues) + assert any(i["severity"] == "error" and "'UI' Feature" in i["message"] for i in deps), issues + print("✓ A feature_dependencies target that is not an API Feature is an error") + + +def test_feature_dependency_deprecated_target_warns(): + """Rule 3 — the target is not deprecated. Its state is derived from its Functionalities, + and the warning points at the replacement when the target names one.""" + catalog = _dependency_catalog(func_status="deprecated", superseded_by="FEAT-3", + deprecation_reason="Replaced by the v2 events API") + deps = _dependency_issues(validate(_calling_functionality(["FEAT-2"]), catalog)) + assert len(deps) == 1, deps + assert deps[0]["severity"] == "warning", deps + assert "is deprecated" in deps[0]["message"] and "FEAT-3" in deps[0]["message"], deps + # A live target with the same markers is not reported as deprecated — markers are not state. + live = _dependency_catalog(superseded_by="FEAT-3") + assert not _dependency_issues(validate(_calling_functionality(["FEAT-2"]), live)) + print("✓ A deprecated feature_dependencies target is flagged, derived from its Functionalities") + + +def test_feature_dependency_on_own_parent_feature_errors(): + """Rule 4 — the target is not the declaring Functionality's own Feature. Reported once, + as the self-edge, without a second surface-type error stacked on top (FEAT-1 is UI).""" + deps = _dependency_issues(validate(_calling_functionality(["FEAT-1"]), _dependency_catalog())) + assert len(deps) == 1, deps + assert deps[0]["severity"] == "error" and "own parent Feature" in deps[0]["message"], deps + print("✓ A feature_dependencies target that is the Functionality's own Feature is an error") + + +def test_feature_dependency_rules_not_reported_without_catalog(): + """Without --catalog none of the four states can be determined, so none is reported — + only that the basis is unavailable. A UI or self target passes unflagged here.""" + deps = _dependency_issues(validate(_calling_functionality(["FEAT-1", "FEAT-99"]))) + assert len(deps) == 1, deps + assert deps[0]["severity"] == "warning" and "--catalog" in deps[0]["message"], deps + for claim in ("not found", "own parent", "'UI'", "deprecated —"): + assert claim not in deps[0]["message"], deps + print("✓ Without a catalog, no dependency rule is reported — only the missing basis") + + +def test_feature_dependency_malformed_entries_error(): + """The field's shape is checkable without a catalog: a list of FEAT- ids. A free-form + name is an external_dependencies entry on the Feature, not a target.""" + deps = _dependency_issues(validate(_calling_functionality(["payment-events", "FEAT-2"]))) + assert any( + i["severity"] == "error" and i["field"] == "feature_dependencies[0]" + and "external_dependencies" in i["message"] for i in deps + ), deps + deps = _dependency_issues(validate(_calling_functionality("FEAT-2"))) + assert any(i["severity"] == "error" and "must be a list" in i["message"] for i in deps), deps + print("✓ A malformed feature_dependencies value or entry is an error") + + +def test_feature_authoring_feature_dependencies_warns(): + """A Feature's feature_dependencies is derived from its Functionalities and never + authored — carrying it is flagged with a pointer to the calling Functionality.""" + issues = validate(_legacy_feature(feature_dependencies=["FEAT-2"])) + deps = _dependency_issues(issues) + assert len(deps) == 1 and deps[0]["severity"] == "warning", issues + assert "derived" in deps[0]["message"] and "Functionality" in deps[0]["message"], deps + print("✓ A Feature authoring feature_dependencies is flagged") + + +# ── format_report() ───────────────────────────────────────────────────────────── + + +def test_format_report_no_issues_and_with_issues(): + """format_report() must produce a clean pass message with zero issues, and an + accurate error/warning count with a line per issue when there are some.""" + clean = format_report("US-1", []) + assert "US-1" in clean and "no issues found" in clean, clean + + issues = [ + {"severity": "error", "field": "name", "message": "Required field 'name' is missing or empty"}, + {"severity": "warning", "field": "owners", "message": "Feature has no owners"}, + ] + report = format_report("FEAT-1", issues) + assert "1 error(s)" in report and "1 warning(s)" in report, report + assert "name" in report and "owners" in report, report + print("✓ format_report() reports a clean pass and an accurate error/warning breakdown") + + +# ── main() / CLI-level error handling ─────────────────────────────────────────── + + +def test_cli_entity_file_not_found(): + """A nonexistent entity path must fail cleanly through the CLI, not with a raw + traceback.""" + with tempfile.TemporaryDirectory() as tmp: + missing_path = Path(tmp) / "does-not-exist.json" + result = _run_validate_entity(str(missing_path)) + assert result.returncode == 1, result.stdout + assert "Error reading entity" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ A missing entity file fails cleanly through the CLI") + + +def test_cli_entity_invalid_json(): + """Malformed JSON in the entity file must fail cleanly, not with a raw traceback.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text("{not valid json", encoding="utf-8") + result = _run_validate_entity(str(entity_path)) + assert result.returncode == 1, result.stdout + assert "Error reading entity" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ Malformed entity JSON fails cleanly through the CLI") + + +def test_cli_catalog_file_not_found(): + """A nonexistent --catalog path must fail cleanly, independent of the entity file + being valid.""" + with tempfile.TemporaryDirectory() as tmp: + entity_path = Path(tmp) / "entity.json" + entity_path.write_text(json.dumps({"entity_type": "User Story"}), encoding="utf-8") + missing_catalog = Path(tmp) / "does-not-exist.json" + result = _run_validate_entity(str(entity_path), "--catalog", str(missing_catalog)) + assert result.returncode == 1, result.stdout + assert "Error reading catalog" in result.stderr, result.stderr + assert "Traceback" not in result.stderr, result.stderr + print("✓ A missing catalog file fails cleanly through the CLI") + + +def test_cli_stdin_input(): + """Passing '-' as the entity argument must read the entity from stdin instead of + treating '-' as a file path.""" + entity = { + "entity_type": "User Story", + "id": "US-1", + "name": "Place an order", + "status": "active", + "features": ["FEAT-1"], + "acceptance_criteria": [ + {"id": "AC:US-1-01", "description": "Order fails when payment is invalid", "state": "active"}, + ], + } + result = subprocess.run( + [sys.executable, str(VALIDATE_ENTITY_PY), "-", "--json"], + input=json.dumps(entity), + capture_output=True, + text=True, + encoding="utf-8", + ) + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["valid"], payload + print("✓ '-' reads the entity from stdin") + + if __name__ == "__main__": try: test_single_digit_entity_ids_accepted() @@ -96,6 +1459,71 @@ def test_end_to_end_slug_feature_id_flagged(): test_feat_parent_ac_id_rejected() test_end_to_end_user_story_with_single_digit_id_validates_clean() test_end_to_end_slug_feature_id_flagged() + test_feature_with_status_field_rejected() + test_feature_surface_type_canonical_set_only() + test_orphan_feature_reported_distinctly() + test_orphan_feature_with_functionalities_still_reported() + test_orphan_feature_suppressed_by_catalog_back_link() + test_functionality_warning_suppressed_by_catalog_back_link() + test_functionality_warning_still_reported_without_back_link() + test_profile_missing_pyyaml_hard_fails() + test_profile_ac_states_subset_accepted() + test_profile_narrows_entity_status_too() + test_profile_ac_states_outside_canon_rejected() + test_profile_ac_states_empty_list_rejected() + test_profile_ac_states_wrong_type_rejected() + test_profile_ac_states_non_string_item_rejected() + test_profile_ac_states_duplicate_rejected() + test_profile_scalar_root_rejected() + test_profile_list_root_rejected() + test_profile_missing_file_hard_fails() + test_profile_directory_path_hard_fails() + test_profile_invalid_encoding_hard_fails() + test_profile_invalid_yaml_syntax_hard_fails() + test_profile_empty_file_treated_as_omitted() + test_profile_falsey_non_mapping_roots_rejected() + test_profile_null_ac_states_rejected() + test_missing_entity_type_flagged() + test_unknown_entity_type_flagged() + test_type_alias_accepted() + test_required_field_missing_flagged() + test_deprecated_user_story_missing_metadata_warns() + test_deprecated_feature_via_markers_warns() + test_deprecated_feature_with_empty_marker_still_warns() + test_feature_authoring_deprecated_at_is_flagged() + test_unknown_deprecation_key_is_not_a_deprecation_marker() + test_feature_state_not_claimed_without_catalog() + test_feature_with_no_markers_and_no_catalog_is_silent() + test_feature_deprecation_derived_from_catalog() + test_feature_not_deprecated_when_a_functionality_is_live() + test_feature_with_no_owned_functionalities_claims_no_state() + test_functionality_missing_func_type_flagged() + test_functionality_invalid_func_type_flagged() + test_functionality_all_canonical_func_types_accepted() + test_ac_missing_id_and_description_flagged() + test_ac_unrecognized_state_errors() + test_ac_bare_string_errors_instead_of_crashing() + test_functionality_name_missing_separator_warns() + test_functionality_parent_feature_bad_format_flagged() + test_user_story_no_features_warns() + test_user_story_no_acceptance_criteria_errors() + test_user_story_missing_error_path_warns() + test_reference_missing_feature_for_user_story_warns() + test_reference_missing_links_for_feature_warns() + test_reference_missing_parent_feature_for_functionality_warns() + test_feature_dependency_valid_api_target_is_clean() + test_feature_dependency_target_missing_from_catalog_warns() + test_feature_dependency_non_api_target_errors() + test_feature_dependency_deprecated_target_warns() + test_feature_dependency_on_own_parent_feature_errors() + test_feature_dependency_rules_not_reported_without_catalog() + test_feature_dependency_malformed_entries_error() + test_feature_authoring_feature_dependencies_warns() + test_format_report_no_issues_and_with_issues() + test_cli_entity_file_not_found() + test_cli_entity_invalid_json() + test_cli_catalog_file_not_found() + test_cli_stdin_input() print("\n✓ All tests passed!") except AssertionError as e: print(f"✗ Test failed: {e}") diff --git a/skills/living-doc-update/scripts/validate_entity.py b/skills/living-doc-update/scripts/validate_entity.py index 6201936..544592a 100644 --- a/skills/living-doc-update/scripts/validate_entity.py +++ b/skills/living-doc-update/scripts/validate_entity.py @@ -4,7 +4,8 @@ Validates a living doc entity JSON against the canonical schema from the glossary. Checks required fields, ID formats, status values, AC structure, and (optionally) -referential integrity against the full catalog. +referential integrity against the full catalog — including the four rules every +`feature_dependencies` edge on a Functionality must satisfy, which only the catalog can decide. Usage: python validate_entity.py entity.json @@ -40,11 +41,15 @@ # ── Canonical constraints (from living-doc-glossary.md) ─────────────────────── -VALID_STATUSES = {"planned", "active", "deprecated"} VALID_SURFACE_TYPES = {"UI", "API"} -# AC state vocabulary — lowercase with underscores per the Project Profile `ac_states`. -# Override at runtime with --profile to read the project's own ac_states list. -VALID_AC_STATUSES = {"planned", "in_review", "active", "deprecated"} +# Status/AC-state vocabulary — lowercase with underscores per the Project Profile `ac_states`. +# The same vocabulary backs both an authored entity `status` (User Story / Functionality) and +# an AC `state` (see living-doc-bdd-schemas.md) — they are not independent sets. CANONICAL_STATUSES +# never changes; VALID_STATUSES and VALID_AC_STATUSES may both be narrowed together at runtime +# with --profile to a subset of it (see main()) — a profile can restrict, never extend, the canon. +CANONICAL_STATUSES = {"planned", "in_review", "active", "deprecated"} +VALID_STATUSES = set(CANONICAL_STATUSES) +VALID_AC_STATUSES = set(CANONICAL_STATUSES) # Numeric only, any digit count (US-1 and US-001 are both valid — matches # living_doc_id.py's ENTITY_TYPE_MAP and scan_ac_links.py). Feature IDs are numeric @@ -64,13 +69,47 @@ REQUIRED_FIELDS: dict[str, list[str]] = { "User Story": ["id", "name", "status", "features", "acceptance_criteria"], "Feature": [ - "id", "name", "surface_type", "purpose", "status", + "id", "name", "surface_type", "purpose", "user_stories", "functionalities", "owners", ], - "Functionality": ["id", "name", "parent_feature", "status", "acceptance_criteria"], + "Functionality": [ + "id", "name", "parent_feature", "func_type", "status", "acceptance_criteria", + ], +} + +# The seven categories of behaviour a Functionality can document. Canon: living-doc's +# "Functionality in a Gherkin Feature File" marks `# func_type:` required, and the values table +# there is the single definition of what each one means and which PageObject locator it anchors +# to — see skills/shared/references/living-doc-bdd-schemas.md. Never widened locally: a value +# outside this set cannot be authored into a feature-file header. +VALID_FUNC_TYPES = { + "component_state", + "component_action", + "button_action", + "field_validation", + "calculation", + "visibility", + "navigation_rule", } +# Entity types that carry an authored `status` field. A Feature has none — its state +# is derived from its Functionalities and must never be authored. +STATUSED_ENTITY_TYPES = {"User Story", "Functionality"} + DEPRECATION_FIELDS = ["deprecated_at", "deprecation_reason"] +# What a *deprecated Feature* should carry. Not the same list: canon derives a Feature's +# `deprecated_at` alongside its state, after parsing, and says a Feature issue body carries +# no `## Deprecated At` heading and a PageObject header no `deprecated_at:` field (see the +# glossary's Feature entry and Header Types' "Common mistakes"). Warning a Feature for a +# missing `deprecated_at` would therefore instruct the agent to author a derived field. +FEATURE_AUTHORED_DEPRECATION_FIELDS = ["deprecation_reason"] +# Markers a Feature can carry as a record that the surface is being retired — never +# required, so they stay out of DEPRECATION_FIELDS' missing-field warning loop. +# Per SKILL.md's deprecation table, superseded_by applies to Feature/Functionality/ +# User Story. These are *authored* markers: their presence records an intent, it does +# not establish the Feature's state — that is derived from the Feature's Functionalities +# and needs the catalog (see _feature_deprecation_state). +FEATURE_DEPRECATION_MARKERS = ("superseded_by",) VERB_PREFIX_RE = re.compile( r"^(process|handle|manage|do|perform|run|execute|validate|create|update|delete)\b", re.IGNORECASE, @@ -84,19 +123,130 @@ # ── Validation logic ─────────────────────────────────────────────────────────── def load_ac_states_from_profile(profile_path: str) -> set[str] | None: - """Read `ac_states` from a Project Profile YAML. Returns None if unavailable.""" + """Read `ac_states` from a Project Profile YAML. Returns None if the field is + omitted (valid — the schema allows omitting it) or the file is genuinely empty. + Exits nonzero for anything that means the requested profile could not actually be + consulted — missing/unreadable file, invalid YAML, a non-mapping root, or a + present-but-schema-invalid `ac_states` — instead of silently falling back to the + unrestricted canonical set as if `--profile` had never been passed. Also exits + nonzero if pyyaml itself is unavailable, for the same reason.""" try: import yaml # noqa: PLC0415 — optional, only needed when --profile is passed + except ImportError as exc: + print( + f"Error: --profile requires pyyaml, which is not installed: {exc}\n" + "Install it with: pip install pyyaml", + file=sys.stderr, + ) + sys.exit(1) + try: with open(profile_path, encoding="utf-8") as f: - profile = yaml.safe_load(f) or {} - except (FileNotFoundError, ImportError, ValueError) as exc: - print(f"Warning: could not load profile '{profile_path}': {exc}", file=sys.stderr) + profile = yaml.safe_load(f) + except (OSError, ValueError) as exc: + print(f"Error: could not load profile '{profile_path}': {exc}", file=sys.stderr) + sys.exit(1) + except yaml.YAMLError as exc: + print( + f"Error: profile '{profile_path}' is malformed — invalid YAML: {exc}", + file=sys.stderr, + ) + sys.exit(1) + if profile is None: return None - states = profile.get("ac_states") - if isinstance(states, list) and states: - return {str(s) for s in states} - return None + if not isinstance(profile, dict): + print( + f"Error: profile '{profile_path}' is malformed — " + f"root must be a mapping, got: {type(profile).__name__}", + file=sys.stderr, + ) + sys.exit(1) + if "ac_states" not in profile: + return None + states = profile["ac_states"] + if not isinstance(states, list) or not states: + print( + f"Error: profile '{profile_path}' has an invalid `ac_states` — " + f"must be a non-empty array of strings, got: {states!r}", + file=sys.stderr, + ) + sys.exit(1) + if not all(isinstance(s, str) for s in states): + print( + f"Error: profile '{profile_path}' has an invalid `ac_states` — " + f"all items must be strings, got: {states!r}", + file=sys.stderr, + ) + sys.exit(1) + if len(set(states)) != len(states): + print( + f"Error: profile '{profile_path}' has an invalid `ac_states` — " + f"must not contain duplicates, got: {states!r}", + file=sys.stderr, + ) + sys.exit(1) + return {str(s) for s in states} + + +def _authored_feature_markers(entity: dict) -> list[str]: + """The deprecation keys a Feature actually carries, in a stable order. + + Presence, not truthiness — `"deprecation_reason": ""` is still an authored key and a + malformed one, which the missing-metadata warnings downstream are there to catch.""" + return [ + field + for field in (*DEPRECATION_FIELDS, *FEATURE_DEPRECATION_MARKERS) + if field in entity + ] + + +def _feature_deprecation_state( + entity: dict, catalog: dict | None +) -> tuple[str, str | None]: + """Derive a Feature's deprecation state the way the canon defines it: from its + Functionalities. + + A Feature has no authored `status`; living-doc derives its state from the + Functionalities it owns. A validator handed one entity at a time therefore *cannot* + know a Feature's state unless it is also given the catalog — so rather than inferring + one from whatever the Feature happens to carry, this returns "unknown" and lets the + caller report the honest basis. + + Returns ``(state, basis)`` where: + + - ``("deprecated", "derived")`` — every owned Functionality is deprecated. + - ``("active", "derived")`` — at least one owned Functionality is not deprecated. + - ``("unknown", "authored_marker")`` — not derivable, but the Feature authors + deprecation metadata; the caller reports that the basis is an authored marker. + - ``("unknown", None)`` — not derivable and nothing authored; say nothing. + + Ownership is the union of the Feature's forward ``functionalities`` list and the + back-link from each Functionality's ``parent_feature``, matching how the + ORPHAN_FEATURE / EMPTY_FEATURE checks below resolve the same relationship.""" + authored = _authored_feature_markers(entity) + fallback_basis = "authored_marker" if authored else None + + if catalog is None: + return "unknown", fallback_basis + + inner = catalog.get("catalog", catalog) + entity_id = entity.get("id", "") + forward = set(entity.get("functionalities") or []) + owned = [ + fn + for fn in inner.get("functionalities", []) + if fn.get("id") in forward or (entity_id and fn.get("parent_feature") == entity_id) + ] + + # A Feature that owns no Functionality has no children to derive from. The + # EMPTY_FEATURE warning below already reports that; claiming a state on top of it + # would be the exact inference this function exists to avoid. + if not owned: + return "unknown", fallback_basis + + if all(fn.get("status") == "deprecated" for fn in owned): + return "deprecated", "derived" + return "active", "derived" def validate(entity: dict, catalog: dict | None = None) -> list[dict]: @@ -147,12 +297,69 @@ def warning(field: str, message: str) -> None: # ── Status ─────────────────────────────────────────────────────────────── status: str = entity.get("status", "") - if status and status not in VALID_STATUSES: + if entity_type not in STATUSED_ENTITY_TYPES: + if "status" in entity: + error( + "status", + f"{entity_type} must not carry a 'status' field — " + "its state is derived from its Functionalities, never authored", + ) + elif status and status not in VALID_STATUSES: error("status", f"Invalid status '{status}'. Must be one of: {VALID_STATUSES}") # ── Deprecation metadata ───────────────────────────────────────────────── - if status == "deprecated": - for dep_field in DEPRECATION_FIELDS: + # A User Story or Functionality authors its own `status`, so its deprecation is + # read straight off the entity. Presence of a deprecation key is what signals + # deprecation, not truthiness — an empty-string value (e.g. `"deprecated_at": ""`) + # is still a deprecation attempt with malformed metadata, and must fall through to + # the missing-field warnings below rather than being silently treated as "not + # deprecated". + # + # A Feature has no `status` at all, and its state is *derived from its + # Functionalities* — a single entity handed to this validator does not contain it. + # _feature_deprecation_state() therefore derives it from --catalog where it can, and + # returns "unknown" where it cannot. A Feature's authored markers record an intent to + # retire the surface; they are not the state, and the report below says so. + if entity_type in STATUSED_ENTITY_TYPES: + is_deprecated = status == "deprecated" + else: + feature_state, basis = _feature_deprecation_state(entity, catalog) + is_deprecated = feature_state == "deprecated" + authored = _authored_feature_markers(entity) + # `deprecated_at` is derived for a Feature, alongside its state — authoring it here + # duplicates a generated value and invites the two to disagree. Canon lists it under + # "Common mistakes" ("Remove it"), so this is a warning to remediate, not a rejection. + if "deprecated_at" in entity: + warning( + "deprecated_at", + "Feature must not author 'deprecated_at' — a Feature's deprecation date is " + "derived with its state from its Functionalities. Remove it; keep " + "'deprecation_reason' / 'superseded_by' if the surface is being retired", + ) + if feature_state == "unknown" and basis == "authored_marker": + warning( + "deprecation", + f"Feature carries authored deprecation metadata ({', '.join(authored)}), " + "but a Feature has no status of its own — its state is derived from its " + "Functionalities and cannot be determined from a single entity. The basis " + "reported here is an authored marker, not a derived state; re-run with " + "--catalog to derive the actual state.", + ) + elif feature_state == "active" and authored: + warning( + "deprecation", + f"Feature carries authored deprecation metadata ({', '.join(authored)}), " + "but its state derived from the catalog is not deprecated — at least one " + "owned Functionality is still non-deprecated. Deprecate every owned " + "Functionality, or drop the deprecation metadata.", + ) + if is_deprecated: + required_dep_fields = ( + DEPRECATION_FIELDS + if entity_type in STATUSED_ENTITY_TYPES + else FEATURE_AUTHORED_DEPRECATION_FIELDS + ) + for dep_field in required_dep_fields: if not entity.get(dep_field): warning( dep_field, @@ -170,11 +377,50 @@ def warning(field: str, message: str) -> None: ) if isinstance(entity.get("owners"), list) and not entity["owners"]: warning("owners", "Feature has no owners — assign a team or individual") - if isinstance(entity.get("user_stories"), list) and not entity["user_stories"]: + no_user_stories = isinstance(entity.get("user_stories"), list) and not entity["user_stories"] + no_functionalities = isinstance(entity.get("functionalities"), list) and not entity["functionalities"] + # gap-finder's ORPHAN_FEATURE gap fires on the absence of a linked User Story alone + # (compute_gaps.py), regardless of Functionality links — so mirror that here rather + # than requiring both lists to be empty. compute_gaps.py also honours the back-link + # from a User Story's own `features` list (features_linked_from_us), not just the + # Feature's forward `user_stories` — mirror that here too so a Feature linked only + # from the User Story side isn't falsely flagged when --catalog is supplied. gap-finder's + # EMPTY_FEATURE gap applies the same union-of-forward-and-back-link treatment to + # Functionality.parent_feature (feature_func_counts) — mirror that here too. + catalog_inner = catalog.get("catalog", catalog) if catalog is not None else None + linked_from_catalog_us = False + if no_user_stories and catalog_inner is not None: + linked_from_catalog_us = any( + entity_id in (us.get("features") or []) + for us in catalog_inner.get("user_stories", []) + ) + if no_user_stories and not linked_from_catalog_us: warning( - "user_stories", + "ORPHAN_FEATURE", "Feature has no linked User Stories — " - "orphan Features appear in gap-finder reports", + "reported as an ORPHAN_FEATURE condition by living-doc-gap-finder", + ) + linked_from_catalog_func = False + if no_functionalities and catalog_inner is not None: + linked_from_catalog_func = any( + fn.get("parent_feature") == entity_id + for fn in catalog_inner.get("functionalities", []) + ) + if no_functionalities and not linked_from_catalog_func: + warning( + "functionalities", + "Feature has no Functionalities — " + "a Feature should own at least one Functionality", + ) + # A Feature's `feature_dependencies` is derived — the union of its Functionalities' + # targets — and canon gives it no authored place (no issue-body heading, no PageObject + # header field). Same remediation shape as an authored `deprecated_at` above. + if "feature_dependencies" in entity: + warning( + "feature_dependencies", + "Feature must not author 'feature_dependencies' — a Feature's value is derived " + "from its Functionalities. Remove it and declare each target on the " + "Functionality that makes the call", ) purpose: str = entity.get("purpose", "") if purpose and len(purpose.split()) < 5: @@ -205,6 +451,7 @@ def warning(field: str, message: str) -> None: else: has_error_path = any( ERROR_KEYWORDS_RE.search(ac.get("description", "")) for ac in acs + if isinstance(ac, dict) ) if not has_error_path: warning( @@ -218,12 +465,13 @@ def warning(field: str, message: str) -> None: # ── Functionality-specific ─────────────────────────────────────────────── if entity_type == "Functionality": name = entity.get("name", "") - if name and " – " not in name and " - " not in name: + if name and " - " not in name: warning( "name", "Functionality name should follow the pattern " - "'<Feature name> – <behavior phrase>' " - "(e.g. 'Login Page – Validate Password Strength')", + "'<Feature name> - <behavior phrase>' " + "(e.g. 'Login Page - Validate Password Strength'). " + "The canonical separator is a plain hyphen, never an en or em dash.", ) parent: str = entity.get("parent_feature", "") if parent and not re.match(r"^FEAT-", parent): @@ -231,6 +479,16 @@ def warning(field: str, message: str) -> None: "parent_feature", f"parent_feature '{parent}' does not look like a valid Feature ID (expected FEAT-nnn)", ) + # An out-of-vocabulary func_type is an error, not a warning, for the same reason an + # out-of-vocabulary AC state is: the value goes straight into a `# func_type:` header + # line, where only these seven are authorable. A missing one is already caught by the + # REQUIRED_FIELDS loop above. + func_type: str = entity.get("func_type", "") + if func_type and func_type not in VALID_FUNC_TYPES: + error( + "func_type", + f"Invalid func_type '{func_type}'. Must be one of: {sorted(VALID_FUNC_TYPES)}", + ) acs = entity.get("acceptance_criteria") or [] if not acs: warning( @@ -240,10 +498,11 @@ def warning(field: str, message: str) -> None: ) for i, ac in enumerate(acs): _validate_ac(ac, entity_id, i, error, warning) + _validate_feature_dependency_shape(entity, catalog, error, warning) # ── Referential integrity ──────────────────────────────────────────────── if catalog is not None: - _validate_references(entity, entity_type, catalog, warning) + _validate_references(entity, entity_type, catalog, error, warning) return issues @@ -257,6 +516,16 @@ def _validate_ac( ) -> None: """Validate a single Acceptance Criterion entry.""" field_prefix = f"acceptance_criteria[{index}]" + # An AC is an object carrying its own `id` — the same ID it has in the feature-file header + # or issue body. A bare string has no ID, so no test or scenario can link to it: report + # that as an error rather than failing on the first attribute access. + if not isinstance(ac, dict): + error_fn( + field_prefix, + f"AC must be an object with 'id' and 'description', got {type(ac).__name__}: " + f"{ac!r} — e.g. {{\"id\": \"AC:{parent_id}-{index + 1:02d}\", \"description\": \"…\"}}", + ) + return ac_id: str = ac.get("id", "") if not ac_id: error_fn(f"{field_prefix}.id", "AC is missing an 'id' field") @@ -270,15 +539,127 @@ def _validate_ac( error_fn(f"{field_prefix}.description", "AC is missing a 'description'") ac_status = ac.get("state", "") if ac_status and ac_status not in VALID_AC_STATUSES: - warning_fn( + # Error, not warning — an AC `state` and an entity `status` are the same + # vocabulary (see the VALID_STATUSES/VALID_AC_STATUSES comment above) and must + # be enforced identically. As a warning this was non-blocking regardless of + # whether VALID_AC_STATUSES was the full canonical set or a --profile-narrowed + # subset, so a profile-excluded AC state still produced exit code 0 / valid: + # true — defeating the entire purpose of `--profile` (restrict, never extend). + error_fn( f"{field_prefix}.state", - f"Unrecognised AC state '{ac_status}'. " + f"Invalid AC state '{ac_status}'. " f"Expected one of: {sorted(VALID_AC_STATUSES)}", ) +def _feature_dependency_targets(entity: dict) -> list[str]: + """The well-formed `feature_dependencies` targets a Functionality declares, in order. + Malformed entries are reported by _validate_feature_dependency_shape and skipped here.""" + targets = entity.get("feature_dependencies") + if not isinstance(targets, list): + return [] + return [t for t in targets if isinstance(t, str) and ID_PATTERNS["Feature"].match(t)] + + +def _validate_feature_dependency_shape( + entity: dict, catalog: dict | None, error_fn, warning_fn +) -> None: + """What can be checked about a Functionality's `feature_dependencies` without a catalog: + it is a list of `FEAT-<n>` ids. Whether each target exists, is an `API` Feature, is live, + and is not the Functionality's own Feature needs the catalog — without one, say the basis + is unavailable instead of reporting any of those states.""" + if "feature_dependencies" not in entity: + return + targets = entity["feature_dependencies"] + if not isinstance(targets, list): + error_fn( + "feature_dependencies", + f"'feature_dependencies' must be a list of Feature IDs, got: {targets!r}", + ) + return + for i, target in enumerate(targets): + if not isinstance(target, str) or not ID_PATTERNS["Feature"].match(target): + error_fn( + f"feature_dependencies[{i}]", + f"'{target}' is not a Feature ID (expected FEAT-nnn) — a system with no " + "canonical anchor is not a target; record it in the Feature's " + "'external_dependencies' instead", + ) + if catalog is None and _feature_dependency_targets(entity): + warning_fn( + "feature_dependencies", + "Dependency targets not checked — whether each target exists, is an 'API' " + "Feature, is not deprecated, and is not this Functionality's own Feature cannot " + "be determined from a single entity. Re-run with --catalog to check them.", + ) + + +def _validate_feature_dependency_targets( + entity: dict, inner: dict, catalog: dict, error_fn, warning_fn +) -> None: + """The four rules a declared `feature_dependencies` edge must satisfy, checked against + the catalog — the same four living-doc's examples_check enforces on the canon corpus. + + - Unresolved target: a warning, as for every other missing reference here. Canon expects + it in a project documented in source code, where no `API` Feature can exist yet. + - Own parent Feature, or a non-`API` target: errors — canon rules the edge invalid. + - Deprecated target: a warning — the target's retirement is a lifecycle event outside + the caller, and the remediation (repoint at the replacement) is a follow-up, not a + reason to refuse the rest of the entity.""" + features = {f.get("id"): f for f in inner.get("features", [])} + parent = entity.get("parent_feature") + for target in _feature_dependency_targets(entity): + if target not in features: + warning_fn( + "feature_dependencies", + f"Dependency target '{target}' not found in catalog — reported as " + "UNRESOLVED_RELATION by the pipeline", + ) + continue + if parent and target == parent: + error_fn( + "feature_dependencies", + f"Dependency target '{target}' is this Functionality's own parent Feature — " + "a Functionality cannot depend on the surface it belongs to", + ) + continue + target_feature = features[target] + surface_type = target_feature.get("surface_type") + if surface_type is None: + warning_fn( + "feature_dependencies", + f"Dependency target '{target}' has no surface_type in the catalog — cannot " + "confirm it is an 'API' Feature", + ) + elif surface_type != "API": + error_fn( + "feature_dependencies", + f"Dependency target '{target}' is a '{surface_type}' Feature — only an 'API' " + "Feature has a contract anchor and can be a dependency target", + ) + state, basis = _feature_deprecation_state(target_feature, catalog) + replacement = target_feature.get("superseded_by") + repoint = ( + f"point the dependency at '{replacement}'" if replacement + else "point the dependency at the replacement surface" + ) + if state == "deprecated": + warning_fn( + "feature_dependencies", + f"Dependency target '{target}' is deprecated — every Functionality it owns " + f"is deprecated; {repoint}", + ) + elif state == "unknown" and basis == "authored_marker": + warning_fn( + "feature_dependencies", + f"Dependency target '{target}' carries authored deprecation metadata, but " + "owns no Functionality in the catalog, so its state cannot be derived; if " + f"it is being retired, {repoint}", + ) + + def _validate_references( - entity: dict, entity_type: str, catalog: dict, warning_fn + entity: dict, entity_type: str, catalog: dict, error_fn, warning_fn ) -> None: """Check that referenced IDs exist in the catalog (referential integrity).""" inner = catalog.get("catalog", catalog) @@ -306,6 +687,7 @@ def _validate_references( "parent_feature", f"Parent Feature '{parent}' not found in catalog", ) + _validate_feature_dependency_targets(entity, inner, catalog, error_fn, warning_fn) # ── Output formatting ────────────────────────────────────────────────────────── @@ -355,7 +737,16 @@ def main() -> None: if args.profile: profile_states = load_ac_states_from_profile(args.profile) if profile_states: - global VALID_AC_STATUSES + invalid_states = profile_states - CANONICAL_STATUSES + if invalid_states: + print( + f"Error: profile ac_states {sorted(invalid_states)} are not a subset of the " + f"canonical states {sorted(CANONICAL_STATUSES)}", + file=sys.stderr, + ) + sys.exit(1) + global VALID_STATUSES, VALID_AC_STATUSES + VALID_STATUSES = profile_states VALID_AC_STATUSES = profile_states try: diff --git a/skills/shared/references/living-doc-bdd-schemas.md b/skills/shared/references/living-doc-bdd-schemas.md index d5f3f95..45e7606 100644 --- a/skills/shared/references/living-doc-bdd-schemas.md +++ b/skills/shared/references/living-doc-bdd-schemas.md @@ -1,79 +1,58 @@ -# Living Documentation — BDD Schemas +<!-- synced from AbsaOSS/living-doc@86c756d88e04184b724d3326b27ae001bbb261f6 — run scripts/sync-living-doc-reference.sh bdd-schemas 86c756d88e04184b724d3326b27ae001bbb261f6 to refresh --> +<!-- Generated file — do not edit by hand. CI re-runs the sync and fails on any diff. --> -Templates and schemas for BDD automation files. Load this file when writing or validating: -- The **Project Profile** (`.project-profile.yaml`) — the per-project config that supplies all conventions below -- US or Functionality **feature file headers** -- **PageObject file headers** (full header or cross-reference header) -- **seed.yaml** (Business Seed) and **manifest.json** (Exploration Manifest) +# Living Doc Header Types -For entity definitions (IDs, status vocabulary, AC format, relationship diagram) see [living-doc-glossary](./living-doc-glossary.md). +Templates and schemas for BDD automation files. Two kinds of file are covered here: -**Schema reference:** For JSON schema validation, see [schemas/](./schemas/) directory: -- [project-profile.schema.json](./schemas/project-profile.schema.json) — Project Profile validation -- [seed.schema.json](./schemas/seed.schema.json) — seed.yaml validation -- [manifest.schema.json](./schemas/manifest.schema.json) — manifest.json validation +- **File headers you author** — the header blocks that carry a **User Story**, a **Feature**, or a **Functionality**. A human can write any of these by hand (no AI agent required — see [agentic-toolkit](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/projects/agentic-toolkit.md)) as long as the file follows the format below. +- **Tooling files the AI agent manages** — the Project Profile (`.project-profile.yaml`), `seed.yaml`, and `manifest.json`. The `agentic-toolkit` agent creates and maintains these as configuration and durable local memory between runs; they are not written by hand. ---- +For entity definitions (IDs, status vocabulary, AC format, relationship diagram), see [Living Doc Glossary](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md). -## Project Profile (config-driven conventions) +> **Source of truth.** This page is the canonical definition of this format. `agentic-toolkit`'s `skills/shared/references/living-doc-bdd-schemas.md` is synced from it. -**Every value a skill could otherwise hardcode — directory names, the test-id attribute, -state-casing, tag conventions — lives in one Project Profile file.** Skills read the profile at -session start and never assume defaults. This keeps the toolkit portable across projects that use -different directory layouts or naming. +> **Worked examples.** A minimal, copyable example of every format on this page lives in +> [`docs/examples/`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/README.md). **Sync obligation:** when a field or rule on this page +> changes, the matching example in `docs/examples/` changes in the same PR. -**Location:** `<bdd_artifacts_dir>/.project-profile.yaml` (default `.copilot/bdd/.project-profile.yaml`). -The agent creates it on first run from the defaults below and confirms each value with the user. -If it already exists, load it and skip the prompt. +--- -```yaml -# .copilot/bdd/.project-profile.yaml — defaults shown match the reference (AUL) project. -test_id_attribute: data-cy # what page.getByTestId() resolves to (Playwright testIdAttribute) +## Contents -feature_dirs: - user_story: features/liv_doc_us # E2E User Story feature files - functionality: features/liv_doc_func # Functionality (system-test) feature files +**File headers you author** — human-editable, one per entity: -paths: - bdd_artifacts: .copilot/bdd # seed.yaml, manifest.json, breaking-changes.md - pageobjects: playwright/pages - steps: playwright/steps +1. [User Story in a Gherkin Feature File](#1-user-story-in-a-gherkin-feature-file) +2. [Feature in a PageObject File](#2-feature-in-a-pageobject-file) +3. [Functionality in a Gherkin Feature File](#3-functionality-in-a-gherkin-feature-file) -# AC state vocabulary as written inside `# AC:` blocks and feature-file headers (lowercase with underscores). -ac_states: [planned, in_review, active, deprecated] +**Tooling files the AI agent manages** — not authored by hand: -# PageObject header `status:` vocabulary (lowercase). -pageobject_statuses: [planned, candidate, active, deprecated] +- [Project Profile](#project-profile-config-driven-conventions) — config-driven conventions every skill reads +- [seed.yaml](#seedyaml-business-seed) — the agent's business-surface memory between runs +- [manifest.json](#manifestjson-exploration-manifest) — the agent's record of every scanned surface -# Scenario tagging conventions (see "Feature file tags" below). -scenario_conventions: - feature_tag: true # @US_ID:<id> / @FUNC_ID:<id> on the Feature - domain_tag: true # optional second feature-level tag, e.g. @domain_create - suite_tags: ["@Regression"] # additional scenario-level tags allowed beside @AC: - section_banners: true # "# *** Happy day scenarios ***" / "# *** Negative scenarios ***" +See also: [Repository adoption scope](#repository-adoption-scope). -manifest_shape: object # manifest is an object with routes array (see Manifest schema) -``` +--- -| Field | Used by | Default (AUL) | +## Repository adoption scope + +A repository does not have to adopt all three entity types. Pick the depth that matches how much +structure the team wants to maintain — each scope is a superset of the one above it: + +| Scope | Headers you maintain | What it gives you | |---|---|---| -| `test_id_attribute` | pageobject-scan, data-cy-instrument, gherkin-step | `data-cy` | -| `feature_dirs.user_story` | scenario-creator, gherkin-living-doc-sync, gap-finder, scripts | `features/liv_doc_us` | -| `feature_dirs.functionality` | scenario-creator, pageobject-scan, gap-finder, scripts | `features/liv_doc_func` | -| `paths.bdd_artifacts` | pageobject-scan, data-cy-instrument | `.copilot/bdd` | -| `paths.pageobjects` / `paths.steps` | pageobject-scan, gherkin-step | `playwright/pages` / `playwright/steps` | -| `ac_states` | all catalog skills, scenario-creator, gherkin-living-doc-sync | `active` etc. (lowercase with underscores) | -| `pageobject_statuses` | pageobject-scan | `active` etc. (lowercase) | -| `scenario_conventions` | scenario-creator, gherkin-living-doc-sync | as shown | +| **User Stories only** | User Story feature file headers | Business requirements and their end-to-end acceptance criteria, tracked by version and status. No mapping to concrete system surfaces. | +| **User Stories + Features** | + Feature (PageObject) file headers | Every User Story is traced to the UI/API surfaces that satisfy it. Orphaned surfaces and uncovered User Stories show up in gap reports. | +| **User Stories + Features + Functionalities** | + Functionality feature file headers | Full atomic-behavior coverage: each surface is broken into fast-testable Functionalities, each with its own ACs and system-test scenarios. | -> **Casing rule:** AC states use **lowercase with underscores** inside `# AC:` blocks and entity files -> (e.g. `- active`, `- in_review`). PageObject header `status:` is also lowercase (`status: active`). -> Wherever this document or a skill shows `ACTIVE`/`PLANNED` in upper-case prose, it refers to the -> *logical* state; the *written* form is always lowercase with underscores. +Moving up a scope never invalidates what you already wrote — adding Features later does not change +existing User Story headers, and adding Functionalities does not change Feature headers. --- -## US Feature File Header +## 1. User Story in a Gherkin Feature File Header comment block at the top of every User Story feature file — `<feature_dirs.user_story>/us-<nnn>-<kebab>.feature` (default `features/liv_doc_us/`). @@ -85,16 +64,21 @@ Holds all US metadata and is mined during living documentation output generation # ============================================================================= # source: https://github.com/<org>/<repo>/issues/<n> ← optional # status: active ← one of profile `ac_states` (planned | in_review | active | deprecated) +# deprecated_at: <date> ← optional; with status: deprecated +# deprecation_reason: <why the User Story was deprecated> ← optional; with status: deprecated +# superseded_by: US-<n> ← optional; replacement entity # business_value: # - <bullet describing the business outcome> # preconditions: ← optional; inherited by all ACs # - <system state required before test> # not_in_scope: ← optional; inherited by all ACs # - <item excluded from this US> +# notes: ← optional; human context +# - <a fact worth recording that drives nothing> # # acceptance_criteria: # -# AC:US-<n>-01 (v<version> - <State>) +# AC:US-<n>-01 (v<version> - <state>) ← `(planned)` with no version = backlog # - <description of the AC> # - Aspect: <value1>, <value2> ← optional; default keyword — no {placeholder} needed in AC text # preconditions: ← optional; extends feature-level preconditions for this AC only @@ -102,11 +86,10 @@ Holds all US metadata and is mined during living documentation output generation # not_in_scope: ← optional; extends feature-level not_in_scope for this AC only # - <AC-specific exclusion> # -# AC:US-<n>-02 (v<version> - <State>) +# AC:US-<n>-02 (v<version> - <state>) # - <description of the AC with optional {placeholder-name} for parameterised variants> # - <placeholder-name>: <value1>, <value2> ← optional; custom keyword — matches {placeholder-name} in AC text; ALL values must be covered # ============================================================================= -``` @US_ID:US-<n> Feature: <US Title> @@ -115,17 +98,17 @@ Feature: <US Title> Background: ← optional Given <shared precondition> - # AC:US-<n>-01 (v<version> - <State>) — <AC description> + # AC:US-<n>-01 (v<version> - <state>) - <AC description> @AC:US-<n>-01 Scenario: <scenario title> ← single scenario = full AC coverage (no aspect split) ... - # — when Aspect values are declared and need individual scenarios: + # - when Aspect values are declared and need individual scenarios: @AC:US-<n>-01/aspect:<value1> Scenario: <scenario title for value1 branch> ... - # — when a custom {placeholder-name} keyword is used (both US and Func): + # - when a custom {placeholder-name} keyword is used (both US and Func): @AC:US-<n>-02/<placeholder-name>:<value1> Scenario: <scenario title for value1> ... @@ -140,120 +123,45 @@ Feature: <US Title> | Field | Required | Purpose | |---|---|---| | `# source:` | Optional | Link to the original issue tracker entry or the pre-BDD living doc location | -| `# status:` | Yes | `planned` · `in_review` · `active` · `deprecated` (lowercase with underscores per profile `ac_states`) | +| `# status:` | Yes | `planned` · `in_review` · `active` · `deprecated` (lowercase with underscores per profile `ac_states`). A User Story always carries an authored status. | +| `# deprecated_at:` | Optional | Date the User Story was deprecated; authored with `status: deprecated` | +| `# deprecation_reason:` | Optional | Why it was deprecated | +| `# superseded_by:` | Optional | ID of the replacement entity | | `# business_value:` | Yes | Why this User Story exists (bullets) | | `# preconditions:` | Optional | System-level state required before test execution; inherited and extended by all ACs | | `# not_in_scope:` | Optional | Explicit exclusions at US level; inherited and extended by all ACs | +| `# notes:` | Optional | Human context at User Story level — a bullet list, one note per bullet; see [Living Doc Glossary — Core entities](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#core-entities) | | `# acceptance_criteria:` | Yes | Full AC listing with IDs, versions, and states; each AC may extend inherited preconditions and not_in_scope | | `@US_ID:US-<n>` tag | Yes | Machine-parseable User Story ID (feature-level tag) | ---- - -## Functionality Feature File Header - -Header comment block at the top of every Functionality feature file — -`<feature_dirs.functionality>/func-<nnn>-<kebab>.feature` (default `features/liv_doc_func/`). - -```gherkin -# ============================================================================= -# LIVING DOC — FUNC-<nnn> · <Feature Name> — <Functionality Name> -# ============================================================================= -# source: https://github.com/<org>/<repo>/issues/<n> ← optional -# status: planned | in_review | active | deprecated -# parent: FEAT-<nnn> -# func_type: component_state | component_action | button_action | -# field_validation | calculation | visibility | navigation_rule -# rationale: ← optional -# - <why this FUNC is scoped this way — business or design decision context> -# preconditions: ← optional; inherited by all ACs -# - <system state required before test> -# not_in_scope: ← optional; inherited by all ACs -# - <exclusion> -# -# acceptance_criteria: -# -# AC:FUNC-<nnn>-01 (v<version> - <State>) -# - <description in business language — no data-cy IDs in AC text> -# - Aspect: <value1>, <value2> ← optional; default keyword — no {placeholder} needed -# preconditions: ← optional; extends feature-level preconditions for this AC only -# - <AC-specific precondition> -# not_in_scope: ← optional; extends feature-level not_in_scope for this AC only -# - <AC-specific exclusion> -# -# AC:FUNC-<nnn>-02 (v<version> - <State>) -# - <description — may contain a {placeholder-name} for parameterised variants> -# - <placeholder-name>: <value1>, <value2> ← optional; custom keyword — matches {placeholder-name} in AC text; ALL values must be covered -# ============================================================================= -``` - -@FUNC_ID:FUNC-<nnn> -Feature: <Feature Name> — <Functionality Name> - <Purpose: one-to-two sentences describing what this FUNC covers, in business - language. Present only when purpose adds context beyond the title.> ← optional - - # No scenarios yet — uncovered ACs flagged by coverage_report.py. - # When adding scenarios: include # AC:<id> comment and @AC:<id> or @AC:<id>/<placeholder-name>:<value> tag above each Scenario. - # ACs with a {placeholder-name}: one scenario per declared value is required — partial coverage is a gap. -``` - -**Header fields:** - -| Field | Required | Purpose | -|---|---|---| -| `# source:` | Optional | Link to the original issue tracker entry or the pre-BDD living doc location | -| `# status:` | Yes | `planned` · `in_review` · `active` · `deprecated` (lowercase with underscores per profile `ac_states`) | -| `# parent:` | Yes | Parent Feature ID (`FEAT-<nnn>`) | -| `# func_type:` | Yes | Category of behavior this Functionality represents (see table below) | -| `# rationale:` | Optional | **Why** this FUNC is scoped the way it is — business context, a deliberate design decision, or a constraint that explains the boundary. Not for implementation notes. | -| `# preconditions:` | Optional | System-level state required before test execution; inherited and extended by all ACs | -| `# not_in_scope:` | Optional | Explicit exclusions at FUNC level; inherited and extended by all ACs | -| `# acceptance_criteria:` | Yes | Full AC listing in business language — do not include `data-cy` IDs or implementation names in AC text; each AC may extend inherited preconditions and not_in_scope | -| `@FUNC_ID:FUNC-<nnn>` tag | Yes | Machine-parseable Functionality ID (feature-level tag) | -| Feature description (below `Feature:`) | Optional | One-to-two sentence purpose in business language. Use when the title alone is not self-explanatory. | - -**`func_type` values:** - -| Value | What it documents | PageObject anchor | -|---|---|---| -| `component_state` | Visible state of elements on load (presence, enabled/disabled, default text) AND what a data-bound component renders per data state (populated, empty, error) | `constructor` locators, data-bearing locators | -| `component_action` | Observable response within a self-contained component to an internal interaction — no discrete button, no system-level side effect (e.g. live search, autocomplete, accordion, carousel, tab content) | Component input/state locators | -| `button_action` | Observable outcome(s) after a specific discrete control is triggered — may span multiple resulting steps (e.g. redirect, entity created, dialog opened) | `btn-*` locators | -| `field_validation` | Rule enforced on a single field's value — inline error, enabled state, accepted/rejected input | `input-*` locators | -| `calculation` | Value computed and displayed from one or more inputs, independent of form submission | Display-only locators | -| `visibility` | Element presence, content, or enabled state conditional on a runtime state — condition is optional context and may be role, prior action, data presence, or config (e.g. owner sees action buttons, section appears after step complete) | Any conditional locator | -| `navigation_rule` | When and where the app routes, driven by action or system state — only when routing has a distinct precondition or business rule | Route assertion | +**Example:** [`docs/examples/gherkin/liv_doc_us/us-001-customer-login.feature`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/gherkin/liv_doc_us/us-001-customer-login.feature) — required fields, one covered AC and one uncovered AC, plus the one optional extension for that file (AC-level `preconditions`). -**Scoping rules:** +--- -- **One FUNC, one cause.** If two behaviors share a trigger, they are one FUNC with two ACs. If two behaviors have different triggers, they are two FUNCs. -- **`component_state`** — scope to a logical group, not individual elements. "Login form controls on load" is one FUNC. Do not write one FUNC per locator. For data-bound components, each distinct data state (populated / empty / error) is an AC on the same FUNC, not a separate FUNC. -- **`component_action`** — one FUNC per distinct component behavior. If the same component has multiple independent internal behaviors (live search AND column sort), they are separate FUNCs. -- **`button_action`** — one FUNC per distinct button. A button that produces multiple observable steps is still one FUNC; the steps become multiple ACs. Two buttons = two FUNCs. Form submission is `button_action` — the trigger is the submit control. -- **`field_validation`** — one FUNC per distinct validation rule, not one per field. The same rule applied to multiple fields = one FUNC with a `{field}` placeholder AC. -- **`calculation`** — only when the derived value is observable independently of a submission. If the result only appears after a form submit, it is an AC on the `button_action` FUNC. -- **`visibility`** — use when an element's presence or state depends on a condition. The condition is descriptive context in the AC, not a required field. Distinct from `component_state` (always-true on load) and `component_action` (response to interaction). -- **`navigation_rule`** — only for routing behaviors with a distinct precondition or business rule. A redirect that is always the result of a button action is an AC on that `button_action` FUNC, not a separate `navigation_rule`. +## 2. Feature in a PageObject File -> `test_type` (unit vs integration vs system) is NOT a FUNC header field — it belongs at scenario level as a tag (e.g. `@test_type:system`). +Every PageObject file opens with a living-doc header block. Use this format so each file is self-describing and traceable without opening a separate registry. ---- +**In this section:** [Required fields](#required-fields) · [Full vs cross-reference headers](#two-header-formats-full-vs-cross-reference) · [Maintaining the header](#maintaining-a-pageobject-header) · [Where operational notes belong](#where-operational-notes-belong) · [Common mistakes](#common-mistakes) -## PageObject File Header +**Example:** [`docs/examples/pageobject/LoginPage.ts`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/pageobject/LoginPage.ts) — full header with the `stub-reason:` optional field. -Every PageObject file opens with a living-doc header block. Use this format so each file is self-describing and traceable without opening a separate registry. +> **No surface status.** A PageObject header carries **no `status:` field**. The Feature it documents +> has no authored status either — a Feature's state is derived from its Functionalities (see +> [Living Doc Glossary — Feature](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#feature)). A surface that is known but not +> yet instrumented for test automation says so with `stub-reason:`, not with a status value. ### Required fields | Field | Canonical values | |---|---| -| `surface_type` | `UI` · `API` · `Service` · `Worker` · `Module` · `Library` | +| `surface_type` | `UI` — a PageObject is the test abstraction for a UI surface only (see [Living Doc Glossary — Feature](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#feature)); an API surface has no PageObject and [carries no header here](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#feature) | | `route` | URL path — use `{param}` for dynamic segments | | `owners` | Team name(s), comma-separated | -| `status` | `active` · `planned` · `candidate` · `deprecated` | | `purpose` | One-to-two sentence description in business language | | `user_stories` | `US-N` IDs, comma-separated — or `none` (triggers orphan warning in gap reports) | | `functionalities` | `FUNC-N` IDs, comma-separated — or `none` (triggers a reminder to define FUNCs) | -| `external_dependencies` | Service or API names this surface calls — or `none` | +| `external_dependencies` | Names of systems this surface calls that are **not** Features themselves — or `none`. A system that has a canonical anchor is an `API` Feature instead, and the call is declared on the Functionality as `feature_dependencies` (see [Living Doc Glossary — Feature](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#feature)) | | `page-object` | Filename of this PageObject | **Optional fields:** @@ -261,7 +169,8 @@ Every PageObject file opens with a living-doc header block. Use this format so e | Field | When | |---|---| | `wizard-steps` | Multi-step wizard UI — list the named steps in order | -| `stub-reason` | `status: candidate` — one-to-two sentence statement of **why** the surface is not yet fully instrumented; treated as tech-debt resolvable by instrumenting the template and re-scanning | +| `stub-reason` | The surface is documented but not yet fully instrumented — one-to-two sentence statement of **why**; treated as tech-debt resolvable by instrumenting the template and re-scanning. Its presence *is* the marker; there is no status value for this. | +| `notes` | Human context at Feature level — a bullet list under the key, one note per bullet; see [Living Doc Glossary — Core entities](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#core-entities). Not a scan diary — the rows under [Where operational notes belong](#where-operational-notes-belong) keep their homes. | ### Two header formats: Full vs Cross-reference @@ -287,13 +196,14 @@ A PageObject file uses one of two formats depending on whether it is the **prima * surface_type: UI * route: /app/accounts/setup * owners: Platform Team - * status: active * wizard-steps: Profile · Preferences · Review · Confirm * purpose: Multi-step wizard for creating and configuring a new account. * user_stories: US-10, US-12 * functionalities: FUNC-005, FUNC-006 * external_dependencies: accounts-api * page-object: AccountSetupWizardPage.ts + * notes: + * - Step order is fixed; the review step cannot be skipped even for a returning customer. * ============================================================================= */ ``` @@ -306,13 +216,18 @@ A PageObject file uses one of two formats depending on whether it is the **prima | `parent-feat` | `FEAT-<nnn>` — ID of the primary Feature that owns this surface. **Required.** | | `route` | URL path of this specific sub-surface — use `{param}` for dynamic segments | | `owners` | Team name(s), comma-separated | -| `status` | `active` · `planned` · `candidate` · `deprecated` | | `purpose` | One sentence: what this step or sub-surface does, in business language — no FEAT IDs | | `page-object` | Filename of this PageObject | | `functionalities` | Optional: `FUNC-<nnn>, ...` — subset of parent Feature's Functionalities that this step implements. Omit if all sub-pages equally implement all parent Feature Functionalities. | The following fields are **intentionally omitted** from the cross-reference header — they belong only on the primary Feature file: `surface_type`, `user_stories`, `external_dependencies`. +`feature_dependencies` is **intentionally omitted from both** PageObject header formats, for a different +reason: a Feature's value is derived from its Functionalities and is never authored, so writing it on a +surface would be a second source of truth for a derived value. It is authored on the Functionality — see +[Living Doc Glossary — Functionality](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#functionality-func) and +[§3](#3-functionality-in-a-gherkin-feature-file). + **Optional inclusion of `functionalities`:** You may list this field in a cross-reference header to scope step-specific atomic behaviors to that sub-page. Use this when a step implements distinct Functionalities not shared across the entire Feature. If the sub-page's Functionality list is identical to the parent Feature's, omit this field to avoid duplication and keep the primary Feature as the authoritative source. **Cross-reference header example:** @@ -327,7 +242,6 @@ The following fields are **intentionally omitted** from the cross-reference head * parent-feat: FEAT-042 * route: /app/accounts/setup (wizard stays on this URL) * owners: Platform Team - * status: active * functionalities: FUNC-005 ← Step 1 (Profile) implements profile-specific field validation * purpose: Step 1 (Profile) — user profile fields: display name, email address, * and role selection. @@ -335,29 +249,57 @@ The following fields are **intentionally omitted** from the cross-reference head * ============================================================================= */ ``` +### Maintaining a PageObject header + +**By hand.** The header block *is* the registry — there is no separate file to keep in sync. Edit +the fields in place as the surface changes: keep `owners:`, `user_stories:`, and +`functionalities:` current, and when a surface is known but its template carries no `data-cy` +attributes yet, add a one-line `stub-reason:`. None of this needs +`seed.yaml` or `manifest.json` — those back the scan workflow only. Maintained by hand, the rule for +scan-style detail (scan dates, gap lists, open-issue references) is simply: it has no home in the +file — leave it out. + +**With the `agentic-toolkit` skills.** The +[`living-doc-bdd-copilot`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/projects/agentic-toolkit.md) skills are large maintenance helpers here +and produce the identical header format — nothing about a scanned header differs from a hand-written +one. `living-doc-pageobject-scan` writes and refreshes the header and locators from a live scan of +the running app; `data-cy-instrument` adds the missing `data-cy` attributes a `stub-reason:` surface +is waiting on; `living-doc-gap-finder` reports orphaned surfaces and uncovered User Stories. They are +accelerators, not a requirement. + ### Where operational notes belong -The PageObject **header block and class JSDoc are living-doc contracts** — they encode identity, traceability, and status. They are not a changelog, scan diary, or issue tracker. +The PageObject **header block and class JSDoc are living-doc contracts** — they encode identity and traceability. They are not a changelog, scan diary, or issue tracker. + +When the scan workflow *is* in use, scan-derived detail has a home outside the header — the table +below says where. (Without the scan workflow, none of these rows apply: the data simply is not +produced, and the header stays clean by default.) | Information type | Correct location | NOT in | |---|---|---| | Missing `data-cy` attributes discovered during a scan | `manifest.json` → `coverage_gaps[]` | Header or class JSDoc | | Reason a surface is not yet fully instrumented | Header field `stub-reason:` (one or two lines) | Free-text NOTE block | -| Proposed `data-cy` names for missing elements | `manifest.json` → `coverage_gaps[].suggestedDataCy` | Header or class body | +| Proposed `data-cy` names for missing elements | `manifest.json` → `coverage_gaps[].suggested_test_id` (normalized; maps to root `test_id_attribute`) | Header or class body | | Open issue reference (e.g. OI-08, P1) | `manifest.json` → `coverage_gaps[].note` | Header or class JSDoc | | Scan date or scan session tag | `manifest.json` → `last_scanned` | Header or class JSDoc | -| `@stub` / `@pending` JSDoc tags on the class | — (use `status: candidate` + `stub-reason:`) | Class JSDoc | +| `@stub` / `@pending` JSDoc tags on the class | — (use `stub-reason:`) | Class JSDoc | | Implementation note explaining a locator strategy | Inline code comment on the locator or method | Header block | -**`status: candidate` and `stub-reason:` as resolvable tech-debt** +**`stub-reason:` as resolvable tech-debt** -A `status: candidate` surface is **not a permanent state**. The surface is known, documented, and linked to User Stories; what is missing is template instrumentation (`data-cy` attributes). Resolution path: +A surface carrying `stub-reason:` is **not in a permanent state** — it is a living-doc tech-debt item. The surface is known, documented, and linked to User Stories; what is missing is template instrumentation (`data-cy` attributes) that would allow full PageObject locators to be written. `stub-reason:` is the instrumentation marker: while it is present the surface is not fully instrumented, and removing it is what records that it now is. The resolution path is always: 1. Instrument the component template with the `data-cy` values listed in `manifest.json` `coverage_gaps[]` (use the `data-cy-instrument` skill). 2. Re-scan — the scan session updates the PageObject locators. -3. Promote `status: candidate` → `status: active` and remove `stub-reason:`. +3. Remove `stub-reason:` — the surface is instrumented. -`stub-reason:` records the factual state at time of discovery (≤ two lines). Must not contain: internal tool or file references, data-cy attribute names, action items, or scan session tags except as a factual date anchor. +`stub-reason:` records the factual state at time of discovery (≤ two lines). The value must be free of: +- internal tool or file references (e.g. `issue-missing-data-cy.md`) +- data-cy attribute names or implementation detail +- action items ("raise with dev team", "will resolve once…") +- scan session tags except as a factual date anchor (e.g. `discovered [scan: 2026-05-28-b]`) + +**`@pending` JSDoc on an individual locator property** is acceptable — it explains why that specific locator uses a fallback strategy and what resolves it. It is implementation-level, not an operational note on the surface as a whole. ### Common mistakes @@ -365,33 +307,200 @@ A `status: candidate` surface is **not a permanent state**. The surface is known |---|---| | `type: screen` | `surface_type: UI` | | `owner: Team` | `owners: Team` (plural key) | -| `status: ACTIVE` | `status: active` (lowercase) | -| `status: STUB` | `status: candidate` + `stub-reason:` field | +| `status:` on a PageObject header (any value) | Remove it — a surface has no status; a Feature's state is derived from its Functionalities | +| `status: STUB` / `status: candidate` | Remove `status`, keep `stub-reason:` | +| `deprecated_at:` on a PageObject header | Remove it — a Feature's deprecation date is derived with its state; keep `deprecation_reason:` / `superseded_by:` if the surface is being retired | +| `feature_dependencies:` on a PageObject header | Remove it — a Feature's value is derived from its Functionalities; author `# feature_dependencies:` on the Functionality that makes the call | | `functionalities:` omitted | `functionalities: none` | | `user_stories:` omitted | `user_stories: none` | | `external_dependencies:` omitted | `external_dependencies: none` | | `parent-feat:` omitted from cross-reference file | Every secondary file for a shared Feature must declare `parent-feat` | -| `page-object:` omitted from cross-reference file | `page-object:` is required in both formats | -| `user_stories:` duplicated in cross-reference file | These fields live only on the primary Feature file | -| Multiple files claiming the same Feature without `[cross-reference]` tag | Only one file carries the full header | +| `page-object:` omitted from cross-reference file | `page-object:` is required in both formats — it names the file being read | +| `user_stories:` duplicated in cross-reference file | These fields live only on the primary Feature file; omit from cross-references | +| Multiple files claiming the same Feature without `[cross-reference]` tag | Only one file carries the full header; all others must use `[cross-reference]` format | | NOTE block in header about missing `data-cy` or open issues | Move to `manifest.json` `coverage_gaps[]`; keep only `stub-reason:` in the header | -| `@stub` or `@pending` on the class JSDoc | Use `status: candidate` + `stub-reason:` in the header instead | -| `purpose:` contains FEAT IDs | `purpose` must not contain FEAT IDs — ID is already in the title line and `parent-feat` | +| `@stub` or `@pending` on the class JSDoc | Use `stub-reason:` in the header instead | +| `purpose: Step 1 of FEAT-006 — ...` | `purpose` must not contain FEAT IDs — use `Step 1 (About) — ...` instead; the ID is already in the title line and `parent-feat` | | `purpose:` contains "NOT a …" or "Accessed via …" | Purpose describes what the surface does; exclude defensive statements and navigation instructions | -| `route:` contains a `data-cy` attribute name | `route:` is a URL path; locator IDs belong in the PageObject body | +| `route:` contains a `data-cy` attribute name (e.g. `btn-import-domain`) | `route:` is a URL path or "modal overlay — no dedicated URL"; locator IDs belong in the PageObject body | | `wizard-steps:` contains `[scan: …]` tag | `wizard-steps:` is a clean ordered list; scan provenance belongs in `manifest.json` | -| Non-spec field added to header | Only use fields defined in the Required or Optional tables | -| Cross-reference prose mentions FUNC IDs or file names | Keep to one human-readable sentence: which step/sub-surface this file implements and where the authoritative header lives | -| `stub-reason:` contains action items or internal tool refs | `stub-reason:` states only the factual reason (≤ two lines) | +| Non-spec field added to header (e.g. `query_params:`) | Only use fields defined in the Required or Optional tables; extra fields are ignored by miners and silently dropped | +| Cross-reference prose mentions FUNC IDs or file names | Cross-reference prose is mined as-is — keep it to one human-readable sentence: which step/sub-surface this file implements and where the authoritative header lives | +| `stub-reason:` contains action items, internal tool refs, or data-cy names | `stub-reason:` states only the factual reason (≤ two lines); action items go in `manifest.json` `coverage_gaps[]` | + +--- + +## 3. Functionality in a Gherkin Feature File + +Header comment block at the top of every Functionality feature file — +`<feature_dirs.functionality>/func-<nnn>-<kebab>.feature` (default `features/liv_doc_func/`). + +**Example:** [`docs/examples/gherkin/liv_doc_func/func-001-validate-password-strength.feature`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/gherkin/liv_doc_func/func-001-validate-password-strength.feature) — required fields plus the one optional extension for that file (`Aspect:` on an AC), split across two scenarios (covered), and one uncovered AC. [`func-002-reject-breached-password.feature`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/gherkin/liv_doc_func/func-002-reject-breached-password.feature) shows `# feature_dependencies:`, on a `planned` Functionality with no scenario yet. + +```gherkin +# ============================================================================= +# LIVING DOC — FUNC-<nnn> · <Feature Name> - <Functionality Name> +# ============================================================================= +# source: https://github.com/<org>/<repo>/issues/<n> ← optional +# status: planned | in_review | active | deprecated +# deprecated_at: <date> ← optional; with status: deprecated +# deprecation_reason: <why this Functionality was deprecated> ← optional; with status: deprecated +# superseded_by: FUNC-<nnn> ← optional; replacement entity +# parent: FEAT-<nnn> +# func_type: component_state | component_action | button_action | +# field_validation | calculation | visibility | navigation_rule +# feature_dependencies: FEAT-<nnn>, FEAT-<nnn> ← optional; API Features this behaviour calls +# rationale: ← optional +# - <why this FUNC is scoped this way — business or design decision context> +# preconditions: ← optional; inherited by all ACs +# - <system state required before test> +# not_in_scope: ← optional; inherited by all ACs +# - <exclusion> +# notes: ← optional; human context +# - <a fact worth recording that drives nothing> +# +# acceptance_criteria: +# +# AC:FUNC-<nnn>-01 (v<version> - <state>) ← `(planned)` with no version = backlog +# - <description in business language — no data-cy IDs in AC text> +# - Aspect: <value1>, <value2> ← optional; default keyword — no {placeholder} needed +# preconditions: ← optional; extends feature-level preconditions for this AC only +# - <AC-specific precondition> +# not_in_scope: ← optional; extends feature-level not_in_scope for this AC only +# - <AC-specific exclusion> +# +# AC:FUNC-<nnn>-02 (v<version> - <state>) +# - <description — may contain a {placeholder-name} for parameterised variants> +# - <placeholder-name>: <value1>, <value2> ← optional; custom keyword — matches {placeholder-name} in AC text; ALL values must be covered +# ============================================================================= + +@FUNC_ID:FUNC-<nnn> +Feature: <Feature Name> - <Functionality Name> + <Purpose: one-to-two sentences describing what this FUNC covers, in business + language. Present only when purpose adds context beyond the title.> ← optional + + # No scenarios yet — uncovered ACs flagged by coverage_report.py. + # When adding scenarios: include # AC:<id> comment and @AC:<id> or @AC:<id>/<placeholder-name>:<value> tag above each Scenario. + # ACs with a {placeholder-name}: one scenario per declared value is required — partial coverage is a gap. +``` + +**Header fields:** + +| Field | Required | Purpose | +|---|---|---| +| `# source:` | Optional | Link to the original issue tracker entry or the pre-BDD living doc location | +| `# status:` | Yes | `planned` · `in_review` · `active` · `deprecated` (lowercase with underscores per profile `ac_states`). A Functionality always carries an authored status. | +| `# deprecated_at:` | Optional | Date the Functionality was deprecated; authored with `status: deprecated` | +| `# deprecation_reason:` | Optional | Why it was deprecated | +| `# superseded_by:` | Optional | ID of the replacement entity | +| `# parent:` | Yes | Parent Feature ID (`FEAT-<nnn>`) | +| `# func_type:` | Yes | Category of behavior this Functionality represents (see table below) | +| `# feature_dependencies:` | Optional | The Features this behaviour calls — `FEAT-<nnn>` IDs, comma-separated. Each target must be an `API` Feature, and only the caller writes the field; the reverse direction is derived. Authored here and derived upward to the parent Feature — see [Living Doc Glossary — Functionality](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#functionality-func). A system with no canonical anchor is not a target: it is a Feature-level `external_dependencies` entry instead. | +| `# rationale:` | Optional | **Why** this FUNC is scoped the way it is — business context, a deliberate design decision, or a constraint that explains the boundary. Not for implementation notes. | +| `# preconditions:` | Optional | System-level state required before test execution; inherited and extended by all ACs | +| `# not_in_scope:` | Optional | Explicit exclusions at FUNC level; inherited and extended by all ACs | +| `# notes:` | Optional | Human context at Functionality level — a bullet list, one note per bullet; see [Living Doc Glossary — Core entities](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-glossary.md#core-entities) | +| `# acceptance_criteria:` | Yes | Full AC listing in business language — do not include `data-cy` IDs or implementation names in AC text; each AC may extend inherited preconditions and not_in_scope | +| `@FUNC_ID:FUNC-<nnn>` tag | Yes | Machine-parseable Functionality ID (feature-level tag) | +| Feature description (below `Feature:`) | Optional | One-to-two sentence purpose in business language. Use when the title alone is not self-explanatory. | + +**`func_type` values:** + +| Value | What it documents | PageObject anchor | +|---|---|---| +| `component_state` | Visible state of elements on load (presence, enabled/disabled, default text) AND what a data-bound component renders per data state (populated, empty, error) | `constructor` locators, data-bearing locators | +| `component_action` | Observable response within a self-contained component to an internal interaction — no discrete button, no system-level side effect (e.g. live search, autocomplete, accordion, carousel, tab content) | Component input/state locators | +| `button_action` | Observable outcome(s) after a specific discrete control is triggered — may span multiple resulting steps (e.g. redirect, entity created, dialog opened) | `btn-*` locators | +| `field_validation` | Rule enforced on a single field's value — inline error, enabled state, accepted/rejected input | `input-*` locators | +| `calculation` | Value computed and displayed from one or more inputs, independent of form submission | Display-only locators | +| `visibility` | Element presence, content, or enabled state conditional on a runtime state — condition is optional context and may be role, prior action, data presence, or config (e.g. owner sees action buttons, section appears after step complete) | Any conditional locator | +| `navigation_rule` | When and where the app routes, driven by action or system state — only when routing has a distinct precondition or business rule | Route assertion | + +**Scoping rules:** + +- **One FUNC, one cause.** If two behaviors share a trigger, they are one FUNC with two ACs. If two behaviors have different triggers, they are two FUNCs. +- **`component_state`** — scope to a logical group, not individual elements. "Login form controls on load" is one FUNC. Do not write one FUNC per locator. For data-bound components, each distinct data state (populated / empty / error) is an AC on the same FUNC, not a separate FUNC. +- **`component_action`** — one FUNC per distinct component behavior. If the same component has multiple independent internal behaviors (live search AND column sort), they are separate FUNCs. +- **`button_action`** — one FUNC per distinct button. A button that produces multiple observable steps is still one FUNC; the steps become multiple ACs. Two buttons = two FUNCs. Form submission is `button_action` — the trigger is the submit control. +- **`field_validation`** — one FUNC per distinct validation rule, not one per field. The same rule applied to multiple fields = one FUNC with a `{field}` placeholder AC. +- **`calculation`** — only when the derived value is observable independently of a submission. If the result only appears after a form submit, it is an AC on the `button_action` FUNC. +- **`visibility`** — use when an element's presence or state depends on a condition. The condition is descriptive context in the AC, not a required field. Distinct from `component_state` (always-true on load) and `component_action` (response to interaction). +- **`navigation_rule`** — only for routing behaviors with a distinct precondition or business rule. A redirect that is always the result of a button action is an AC on that `button_action` FUNC, not a separate `navigation_rule`. + +> `test_type` (unit vs integration vs system) is not a living-doc field — not on a FUNC header, and not a +> tag this documentation defines. How a test is executed is a test-framework concern. A `@test_type:` tag +> written by a test framework may appear in a scenario file; the pipeline does not interpret it — it +> travels with the scenario's other tags, and nothing in living-doc reads it. + +--- + +## Project Profile (config-driven conventions) + +**Every value a skill could otherwise hardcode — directory names, the test-id attribute, +state-casing, tag conventions — lives in one Project Profile file.** Tooling reads the profile at +session start and never assumes defaults. This keeps the format portable across projects that use +different directory layouts or naming. `agentic-toolkit`'s agent creates it on first run from the +defaults below (confirming each value with the user) and loads it as-is thereafter — it is not a file +you are expected to write from scratch. + +**Location:** `<bdd_artifacts_dir>/.project-profile.yaml` (default `.copilot/bdd/.project-profile.yaml`). + +**Example:** [`docs/examples/project-profile/.project-profile.yaml`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/project-profile/.project-profile.yaml). + +```yaml +# .copilot/bdd/.project-profile.yaml — defaults shown match the reference (AUL) project. +test_id_attribute: data-cy # what page.getByTestId() resolves to (Playwright testIdAttribute) + +feature_dirs: + user_story: features/liv_doc_us # E2E User Story feature files + functionality: features/liv_doc_func # Functionality (system-test) feature files + +paths: + bdd_artifacts: .copilot/bdd # seed.yaml, manifest.json, breaking-changes.md + pageobjects: playwright/pages + steps: playwright/steps + +# AC state vocabulary as written inside `# AC:` blocks and feature-file headers (lowercase with underscores). +ac_states: [planned, in_review, active, deprecated] + +# Scenario tagging conventions (see "Feature file tags" below). +scenario_conventions: + feature_tag: true # @US_ID:<id> / @FUNC_ID:<id> on the Feature + domain_tag: true # optional second feature-level tag, e.g. @domain_create + suite_tags: ["@Regression"] # additional scenario-level tags allowed beside @AC: + section_banners: true # "# *** Happy day scenarios ***" / "# *** Negative scenarios ***" + +manifest_shape: object # manifest is an object with routes array (see Manifest schema) +``` + +| Field | Used by | Default (AUL) | +|---|---|---| +| `test_id_attribute` | pageobject-scan, data-cy-instrument, gherkin-step | `data-cy` | +| `feature_dirs.user_story` | scenario-creator, gherkin-living-doc-sync, gap-finder, scripts | `features/liv_doc_us` | +| `feature_dirs.functionality` | scenario-creator, pageobject-scan, gap-finder, scripts | `features/liv_doc_func` | +| `paths.bdd_artifacts` | pageobject-scan, data-cy-instrument | `.copilot/bdd` | +| `paths.pageobjects` / `paths.steps` | pageobject-scan, gherkin-step | `playwright/pages` / `playwright/steps` | +| `ac_states` | all catalog skills, scenario-creator, gherkin-living-doc-sync | `active` etc. (lowercase with underscores) | +| `scenario_conventions` | scenario-creator, gherkin-living-doc-sync | as shown | + +> **Casing rule:** AC and entity states use **lowercase with underscores** inside `# AC:` blocks, +> entity files and issue bodies (e.g. `active`, `in_review`). Wherever a skill shows `ACTIVE` or +> `PLANNED` in upper-case prose, it refers to the *logical* state; the *written* form is always +> lowercase with underscores. There is no PageObject `status:` field — see +> [Feature in a PageObject File](#2-feature-in-a-pageobject-file). --- ## seed.yaml (Business Seed) -`seed.yaml` lives at `<paths.bdd_artifacts>/seed.yaml` (default `.copilot/bdd/seed.yaml`). It is the -durable, human-curated input to every scan session: app entry point, business domains → routes, -known entities for parameterised routes, test-user roles, and pre-declared form values. **Re-read it -in full at the start of every scan session.** The agent appends to it as it discovers entities. +`seed.yaml` lives at `<paths.bdd_artifacts>/seed.yaml` (default `.copilot/bdd/seed.yaml`). It is **the +agent's local memory, not a human-authored file** — its durable record of the app's business surface +between scan sessions: app entry point, business domains → routes, known entities for parameterised +routes, test-user roles, and pre-declared form values. The agent creates it, re-reads it in full at +the start of every scan session, and appends to it as it discovers entities. A human may pre-seed +known values or correct them, but is not expected to write the file. + +**Example:** [`docs/examples/project-profile/seed.yaml`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/project-profile/seed.yaml) — required keys plus one optional `form_fixtures` entry. ```yaml # .copilot/bdd/seed.yaml @@ -443,7 +552,7 @@ known_entities: name: "test csv" # form_fixtures — pre-declared field values for form/wizard traversal, keyed by route path. -# The agent reads these before falling back to the sourcing cascade. +# Read before falling back to the sourcing cascade below. form_fixtures: /auth/all-domains/create-domain/about: - field: domain-name @@ -469,15 +578,15 @@ form_fixtures: | `form_fixtures` | No | Map of `route → [ {field, value, source, note?} ]`. `source`: `seed_declared` \| `user_provided` \| `agent_observed` | **Credential rule:** credentials are referenced via `credentials_source` (an env file) or `env:VAR_NAME` -placeholders. A literal credential value anywhere in `seed.yaml` is a **security violation** — refuse to -proceed until it is replaced. +placeholders. A literal credential value anywhere in `seed.yaml` is a **security violation** — the file must be +corrected before proceeding. -### form_fixtures sourcing cascade (how the agent resolves a field value) +### form_fixtures sourcing cascade (how a field value is resolved) 1. **`seed.yaml form_fixtures`** — pre-declared (`source: seed_declared`) or written in a prior session (`agent_observed`). 2. **Existing app entities** — navigate to the entity list; read a real field value; copy it, or derive (e.g. append a suffix to avoid duplicate rejection). 3. **Field context inference** — read label + placeholder + tooltip → infer a plausible value (`"Domain name"` → `"E2E Test Domain"`, `email` → `"test@example.com"`). -4. **User-assist pause** — for a value that must exist in the real environment and none of the above suffices → show the user the form, request the value, record it back with `source: user_provided`. +4. **User-assist pause** — for a value that must exist in the real environment and none of the above suffices → show the form, request the value, record it back with `source: user_provided`. > **Optional advanced fields.** For complex forms a fixture entry may also carry `value_class` > (`copyable` / `derived` / `fake` / `real-world`), a `values[]` array of labelled traversal branches @@ -498,16 +607,17 @@ to capture error `data-cy` elements visible only in the invalid state: | Duplicate detection | A known existing entity name | Duplicate-rejected error and its `data-cy` | Findings become source material for `field_validation` Functionality stubs and are recorded in the -manifest route's optional `field_constraints[]` (see Manifest schema below). +manifest route's optional `field_constraints[]` (see below). --- ## manifest.json (Exploration Manifest) `manifest.json` lives at `<paths.bdd_artifacts>/manifest.json` (default `.copilot/bdd/manifest.json`). -It is the machine record of every scanned surface. The manifest is a JSON object; **`routes` is a JSON array** of route objects -(profile `manifest_shape: object`). Load targeted entries by route during a session; load the full file -only for a RE-SCAN. +It is **the agent's local memory, not a human-authored file** — its machine record of every scanned +surface, written and read by the tooling across runs. The manifest is a JSON object; **`routes` is a JSON array** of route objects +(profile `manifest_shape: object`). Targeted entries are loaded by route during a session; the full file +is loaded only for a RE-SCAN. The manifest uses **normalized test_id keys** (not attribute-specific). The root-level `test_id_attribute` metadata tells downstream generators (PageObject, data-cy-instrument) how to map these normalized keys to the actual HTML attribute. @@ -571,9 +681,21 @@ tells downstream generators (PageObject, data-cy-instrument) how to map these no | Event | What happens | |---|---| -| First form encountered | Agent applies the sourcing cascade; fills the form; probes validation | +| First form encountered | The sourcing cascade is applied; the form is filled; validation is probed | | `real-world` value missing | User-assist pause → value saved to `form_fixtures` with `source: user_provided` | | Validation probe finds a new `data-cy` | Added to the route `elements[]`; flagged as a `field_validation` Functionality candidate | -| Next scan session | Agent reads `form_fixtures`; skips the cascade for pre-declared fields | -| Constraint changes (e.g. max length) | Agent updates `field_constraints`; flags the change in `breaking-changes.md` | -| Element missing on re-scan | Flag `BREAKING CHANGE`; never auto-delete the locator | +| Next scan session | `form_fixtures` is read; the cascade is skipped for pre-declared fields | +| Constraint changes (e.g. max length) | `field_constraints` is updated; the change is flagged in `breaking-changes.md` | +| Element missing on re-scan | Flagged `BREAKING CHANGE`; the locator is never auto-deleted | + +--- + +## Toolkit-local additions + +*Not part of the canon above — machine-readable contracts that live only in `agentic-toolkit`.* + +**Schema reference:** for JSON schema validation, see the [schemas/](./schemas/) directory: + +- [project-profile.schema.json](./schemas/project-profile.schema.json) — Project Profile validation +- [seed.schema.json](./schemas/seed.schema.json) — seed.yaml validation +- [manifest.schema.json](./schemas/manifest.schema.json) — manifest.json validation diff --git a/skills/shared/references/living-doc-glossary.md b/skills/shared/references/living-doc-glossary.md index 4a7d983..b3a0cf8 100644 --- a/skills/shared/references/living-doc-glossary.md +++ b/skills/shared/references/living-doc-glossary.md @@ -1,14 +1,57 @@ -# Living Documentation — Shared Glossary +<!-- synced from AbsaOSS/living-doc@86c756d88e04184b724d3326b27ae001bbb261f6 — run scripts/sync-living-doc-reference.sh glossary 86c756d88e04184b724d3326b27ae001bbb261f6 to refresh --> +<!-- Generated file — do not edit by hand. CI re-runs the sync and fails on any diff. --> -Core entity contracts: IDs, status vocabulary, relationships, and AC format. -All living-doc-* skills operate on this canonical entity model. +# Living Doc Glossary -For BDD file templates and schemas (feature file headers, PageObject headers, ExplorationFixture, seed.yaml), load [living-doc-bdd-schemas](./living-doc-bdd-schemas.md). +Core entity contracts: IDs, status vocabulary, relationships, and AC format. Every `living-doc-*` repo and `agentic-toolkit`'s `living-doc-bdd-copilot` agent operate on this canonical entity model. + +For the file-header schemas that carry these entities (feature file headers, PageObject headers, Project Profile, seed.yaml, manifest.json), see [Living Doc Header Types](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-header-types.md). + +> **Source of truth.** This page is the canonical definition of the entity model. `agentic-toolkit`'s `skills/shared/references/living-doc-glossary.md` is synced from it. + +> **Worked examples & sync obligation.** A minimal, copyable example of each entity lives in +> [`docs/examples/`](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/README.md). When a field or rule on this page changes, the matching +> example changes in the same PR. + +--- + +## Contents + +- [Core entities](#core-entities) + - [User Story (US)](#user-story-us) + - [Feature](#feature) + - [Functionality (FUNC)](#functionality-func) + - [Acceptance Criterion (AC)](#acceptance-criterion-ac) + - [ID uniqueness](#id-uniqueness) +- [Relationship diagram](#relationship-diagram) +- [What each `living-doc-bdd-copilot` skill creates or consumes](#what-each-living-doc-bdd-copilot-skill-creates-or-consumes) --- ## Core entities +> **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 Reason` and `## Superseded By` as headings in an issue body; `# status:`, +> `# deprecated_at:`, `# deprecation_reason:` and `# superseded_by:` as keys in a feature-file +> 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, and `deprecated_at` is authored on a User Story and a +> Functionality only — see [Feature](#feature) for what a Feature derives instead. +> See the [GitHub issue-body layout](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/examples/README.md#github-issue-body-layout-canonical) and +> [Living Doc Header Types](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-header-types.md). + +> **`## Notes` — the one place for human context.** Every entity may carry an optional `## Notes` +> section 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_reason` stays its own typed field and does not +> migrate into a note. + ### User Story (US) A business-level requirement expressed from the perspective of a named actor. @@ -23,13 +66,17 @@ so that <business outcome>. - 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: `planned | in_review | active | deprecated` (lowercase with underscores per the Project Profile `ac_states`) -- Deprecation metadata (set when `status: deprecated`): +- Status: **authored**, and required — `planned | in_review | active | deprecated` (lowercase with + underscores per the Project Profile `ac_states`). Written as `## Status` in the issue body and + `# status:` in the feature-file header. +- Deprecation metadata (optional; authored when `status: deprecated`): - `deprecated_at` — date the entity was deprecated - `deprecation_reason` — why it was deprecated - - `superseded_by` — ID of the replacement entity (optional) + - `superseded_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 [living-doc-bdd-schemas — US Feature File Header](./living-doc-bdd-schemas.md#us-feature-file-header). +> Feature file template: see [Living Doc Header Types — User Story in a Gherkin Feature File](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-header-types.md#1-user-story-in-a-gherkin-feature-file). ### Feature @@ -42,130 +89,77 @@ A named system surface — the structural layer between User Stories and atomic | 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 REST/GraphQL endpoint or endpoint group. A backend service is documented as an API Feature representing its public contract. | **Annotated endpoint method** — the endpoint method with its API documentation header (OpenAPI annotation, JSDoc, etc.) serves as the living contract anchor. | +| `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 -- Status: `planned | in_review | active | deprecated` (lowercase with underscores per the Project Profile `ac_states`) -- Deprecation metadata (set when `status: deprecated`): - - `deprecated_at` — date the entity was deprecated - - `deprecation_reason` — why it was deprecated - - `superseded_by` — ID of the replacement entity (optional) -- Ownership change metadata (set when `owners` changes): - - `owner_changed_at` — date of ownership transfer - - `owner_change_reason` — reason for the transfer - -> PageObject file header schemas (full header, cross-reference, operational notes, common mistakes): see [living-doc-bdd-schemas — PageObject File Header](./living-doc-bdd-schemas.md#pageobject-file-header). - -### Functionality (FUNC) -| `parent-feat` | `FEAT-<nnn>` — ID of the primary Feature that owns this surface. **Required.** | -| `route` | URL path of this specific sub-surface — use `{param}` for dynamic segments | -| `owners` | Team name(s), comma-separated | -| `status` | `active` · `planned` · `candidate` · `deprecated` | -| `purpose` | One sentence: what this step or sub-surface does, in business language — no FEAT IDs, no internal references | -| `page-object` | Filename of this PageObject | - -The following fields are **intentionally omitted** from the cross-reference header — they belong only on the primary Feature file: `surface_type`, `user_stories`, `external_dependencies`. - -**Optional inclusion of `functionalities`:** You may list this field in a cross-reference header to scope step-specific atomic behaviors to that sub-page. Use this when a step implements distinct Functionalities not shared across the entire Feature. If the sub-page's Functionality list is identical to the parent Feature's, omit this field to avoid duplication and keep the primary Feature as the authoritative source. - -**Cross-reference header example:** - -```typescript -/* ============================================================================= - * LIVING DOC — FEAT-042 · Account Setup Wizard [cross-reference] - * ============================================================================= - * This file implements Step 1 (Profile) of the Account Setup Wizard. - * The authoritative Feature header is in AccountSetupWizardPage.ts. - * - * parent-feat: FEAT-042 - * route: /app/accounts/setup (wizard stays on this URL) - * owners: Platform Team - * status: active - * functionalities: FUNC-005 ← Step 1 (Profile) implements profile-specific field validation - * purpose: Step 1 (Profile) — user profile fields: display name, email address, - * and role selection. - * page-object: AccountSetupWizardProfilePage.ts - * ============================================================================= */ -``` - ---- - -#### Where operational notes belong - -The PageObject **header block and class JSDoc are living-doc contracts** — they encode identity, traceability, and status. They are not a changelog, scan diary, or issue tracker. - -| Information type | Correct location | NOT in | -|---|---|---| -| Missing `data-cy` attributes discovered during a scan | `manifest.json` → `coverage_gaps[]` | Header or class JSDoc | -| Reason a surface is not yet fully instrumented | Header field `stub-reason:` (one or two lines) | Free-text NOTE block | -| Proposed test-id names for missing elements | `manifest.json` → `coverage_gaps[].suggested_test_id` (normalized; maps to root `test_id_attribute`) | Header or class body | -| Open issue reference (e.g. OI-08, P1) | `manifest.json` → `coverage_gaps[].note` | Header or class JSDoc | -| Scan date or scan session tag | `manifest.json` → `last_scanned` | Header or class JSDoc | -| `@stub` / `@pending` JSDoc tags on the class | — (use `status: candidate` + `stub-reason:`) | Class JSDoc | -| Implementation note explaining a locator strategy | Inline code comment on the locator or method | Header block | - -**`status: candidate` and `stub-reason:` as resolvable tech-debt** - -A `status: candidate` surface is **not a permanent state** — it is a living-doc tech-debt item. The surface is known, documented, and linked to User Stories; what is missing is template instrumentation (`data-cy` attributes) that would allow full PageObject locators to be written. The resolution path is always: - -1. Instrument the component template with the `data-cy` values listed in `manifest.json` `coverage_gaps[]` (use the `data-cy-instrument` skill). -2. Re-scan — the scan session updates the PageObject locators. -3. Promote `status: candidate` → `status: active` and remove `stub-reason:`. - -`stub-reason:` records the factual state at time of discovery (≤ two lines). The value must be free of: -- internal tool or file references (e.g. `issue-missing-data-cy.md`) -- data-cy attribute names or implementation detail -- action items ("raise with dev team", "will resolve once…") -- scan session tags except as a factual date anchor (e.g. `discovered [scan: 2026-05-28-b]`) - -**`@pending` JSDoc on an individual locator property** is acceptable — it explains why that specific locator uses a fallback strategy and what resolves it. It is implementation-level, not an operational note on the surface as a whole. - ---- - -**Common mistakes:** - -| Anti-pattern | Correct | -|---|---| -| `type: screen` | `surface_type: UI` | -| `owner: Team` | `owners: Team` (plural key) | -| `status: ACTIVE` | `status: active` (lowercase) | -| `status: STUB` | `status: candidate` + `stub-reason:` field | -| `functionalities:` omitted | `functionalities: none` | -| `user_stories:` omitted | `user_stories: none` | -| `external_dependencies:` omitted | `external_dependencies: none` | -| `parent-feat:` omitted from cross-reference file | Every secondary file for a shared Feature must declare `parent-feat` | -| `page-object:` omitted from cross-reference file | `page-object:` is required in both formats — it names the file being read | -| `user_stories:` duplicated in cross-reference file | These fields live only on the primary Feature file; omit from cross-references | -| Multiple files claiming the same Feature without `[cross-reference]` tag | Only one file carries the full header; all others must use `[cross-reference]` format | -| NOTE block in header about missing `data-cy` or open issues | Move to `manifest.json` `coverage_gaps[]`; keep only `stub-reason:` in the header | -| `@stub` or `@pending` on the class JSDoc | Use `status: candidate` + `stub-reason:` in the header instead | -| `purpose: Step 1 of FEAT-006 — ...` | `purpose` must not contain FEAT IDs — use `Step 1 (About) — ...` instead; the ID is already in the title line and `parent-feat` | -| `purpose:` contains "NOT a …" or "Accessed via …" | Purpose describes what the surface does; exclude defensive statements and navigation instructions | -| `route:` contains a `data-cy` attribute name (e.g. `btn-import-domain`) | `route:` is a URL path or "modal overlay — no dedicated URL"; locator IDs belong in the PageObject body | -| `wizard-steps:` contains `[scan: …]` tag | `wizard-steps:` is a clean ordered list; scan provenance belongs in `manifest.json` | -| Non-spec field added to header (e.g. `query_params:`) | Only use fields defined in the Required or Optional tables; extra fields are ignored by miners and silently dropped | -| Cross-reference prose mentions FUNC IDs or file names | Cross-reference prose is mined as-is — keep it to one human-readable sentence: which step/sub-surface this file implements and where the authoritative header lives | -| `stub-reason:` contains action items, internal tool refs, or data-cy names | `stub-reason:` states only the factual reason (≤ two lines); action items go in `manifest.json` `coverage_gaps[]` | +- `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 an `API` Feature in its own right, and the entry leaves every + `external_dependencies` list that named it. +- `feature_dependencies`: **derived from its Functionalities — never authored.** The value is the union of + its Functionalities' targets, for the same reason the status is derived: the Feature is a structural node, + and what calls another surface is a behaviour. A Feature issue body carries no `## Feature Dependencies` + heading and a PageObject header no `feature_dependencies:` field. See + [Functionality](#functionality-func) for where it *is* written. +- 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 `## Status` + heading, and a PageObject header carries no `status:` field. The pipeline computes the state and + marks it `derived`. +- Deprecation metadata: `deprecated_at` is **derived, never authored** — it is generated after parsing, + alongside the state, and follows the Functionalities like the state does. A Feature issue body + carries no `## Deprecated At` heading and a PageObject header no `deprecated_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 derive + - `superseded_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 [Living Doc Header Types — Feature in a PageObject File](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-header-types.md#2-feature-in-a-pageobject-file). ### Functionality (FUNC) 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") +- 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) +- `feature_dependencies` (optional): the Features this behaviour calls — a list of `FEAT-` ids, + **authored here** and derived upward to the parent Feature (see [Feature](#feature)). Three rules: + - **The target must be an `API` Feature.** You can only call what has a contract anchor. A `UI` target is + an error today; the rule can be loosened later if UI composition needs it, which is cheaper than the + reverse. + - **Only the caller authors it.** The reverse direction — who depends on this Feature — is derived by the + pipeline and never written by hand. + - **It is authored on the Functionality, not on the Feature.** Coverage of a dependency edge is described + by a Functionality and its ACs — roughly one endpoint of the called surface — and scenarios already link + to ACs by `@AC:` tag. Written where the ACs are, a declared dependency with no linked scenario is a + computable untested integration point, with no second "test → edge" concept. Written at Feature level it + would be a second source of truth for a derived value. + + 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: no `API` Feature can exist there, because the + contract anchor carries no living-doc header yet (see [Feature](#feature)). The field resolves fully in a + project documented in GitHub Issues or in Azure DevOps work items. - 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>.feature` e.g. `func-001-authentication-screen-credential-based-login.feature` -- Status: `planned | in_review | active | deprecated` (lowercase with underscores per the Project Profile `ac_states`) -- Deprecation metadata (set when `status: deprecated`): +- Status: **authored**, and required — `planned | in_review | active | deprecated` (lowercase with + underscores per the Project Profile `ac_states`). Written as `## Status` in the issue body and + `# status:` in the feature-file header. +- Deprecation metadata (optional; authored when `status: deprecated`): - `deprecated_at` — date the entity was deprecated - `deprecation_reason` — why it was deprecated - - `superseded_by` — ID of the replacement entity (optional) + - `superseded_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. @@ -183,10 +177,10 @@ A single User Story may trigger multiple Functionalities. **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** (`living-doc-create-functionality`) -- AC describes a full user journey inside a FUNC → move to a **User Story** (`living-doc-create-user-story`) +- 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_type` values: see [living-doc-bdd-schemas — Functionality Feature File Header](./living-doc-bdd-schemas.md#functionality-feature-file-header). +> Feature file template and `func_type` values: see [Living Doc Header Types — Functionality in a Gherkin Feature File](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-header-types.md#3-functionality-in-a-gherkin-feature-file). ### Acceptance Criterion (AC) @@ -200,19 +194,44 @@ Each AC is: **AC identifier and state format** (in file header and entity files): ``` -AC:<parent-id>-<nn> (v<version> - <State>) +AC:<parent-id>-<nn> (v<version> - <state>) - <atomic description, with at most one {placeholder} for a variable value> - <Placeholder>: value1, value2, ... - Rationale: <business context, policy reference, or design decision> ← optional ``` -State values: `planned | in_review | active | deprecated` (lowercase with underscores per the Project Profile `ac_states`). +**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 Review` for +> `in_review`, `v1.1` for `v1.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. **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: ```gherkin -# AC:US-1-01 (v1.0.0 - active) — customer places an order with a saved payment method +# 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 ... @@ -222,7 +241,7 @@ When a scenario covers only **one aspect** of a multi-aspect AC, encode the aspe the `@AC:` tag using the `/param:value` param syntax, and mirror it in the comment: ```gherkin -# AC:US-1-01 (v1.0.0 - active) — displays {required field} on login screen | aspect: username input +# AC:US-1-01 (v1.0.0 - active) - displays {required field} on login screen | aspect: username input @AC:US-1-01/aspect:username-input Scenario: Login form shows the username input field ... @@ -231,8 +250,8 @@ Scenario: Login form shows the username input field Multiple ACs — one comment + tag pair per AC: ```gherkin -# 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 (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 @@ -248,29 +267,19 @@ Scenario: User is locked out after repeated failed logins Additional `/param:value` segments can be appended as needed — the format is open for extension. -- The `# AC:` comment is human-readable context: AC ID, version, state, description, optional aspect. +- The `# AC:` comment is human-readable context: the canonical AC header, then ` - ` and the + description, plus an optional `| aspect: <value>` suffix. 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_story` and `feature_dirs.functionality`, defaults `features/liv_doc_us/` and `features/liv_doc_func/`). -- Feature files outside the living-doc directories (smoke tests, regression suites, exploratory probes) - do not require `@AC:` tags. - -Deprecated ACs include a removal note: - -``` -AC:<parent-id>-<nn> (v<version> – DEPRECATED – removal planned v<version>) -``` - -**Descoped ACs** (deferred mid-sprint — state stays `PLANNED`): - -``` -AC:<parent-id>-<nn> (v<version> – PLANNED) - – <description> - – descoped_at: <date> ← date AC was deferred out of the current sprint - – descoped_reason: <text> - – future_release: <sprint/tag> ← optional; target sprint or release -``` +- 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/` or `tutorial_<group>/`), carry a `@tutorial` scenario 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): @@ -301,11 +310,42 @@ AC:FUNC-001-02 (v1.0.0 - active) 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. ``` +### ID uniqueness + +**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](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-document-types.md#coverage-matrix) +and the `Data Flows & Schemas` spec §8 ("Multiple sources and multiple generators"). The toolkit +[`coverage_matrix` service README](https://github.com/AbsaOSS/living-doc-toolkit/blob/master/packages/services/coverage_matrix/README.md) +describes the false-gap failure mode when the two sides of the join do not describe the same system. + --- -> ExplorationFixture taxonomy, seed.yaml schema, and manifest.field_constraints schema: see [living-doc-bdd-schemas — ExplorationFixture](./living-doc-bdd-schemas.md#explorationfixture). +> `seed.yaml` and `manifest.json` schemas: see [Living Doc Header Types — manifest.json (Exploration Manifest)](https://github.com/AbsaOSS/living-doc/blob/86c756d88e04184b724d3326b27ae001bbb261f6/docs/guides/living-doc-header-types.md#manifestjson-exploration-manifest). --- @@ -321,6 +361,9 @@ User Story (US) | @FUNC_ID tag + @AC:FUNC-nnn-nn tagged scenarios | └── implemented by: Step Definitions └── can map to: unit/integration tests + └── can depend on: Feature (FEAT, surface_type: API) + feature_dependencies - authored here, + derived upward to the parent Feature └── 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) @@ -332,14 +375,13 @@ User Story (US) --- -## What each skill creates or consumes +## What each `living-doc-bdd-copilot` skill creates or consumes | Skill | Creates | Reads | |---|---|---| | `living-doc-create-user-story` | User Story entity | Feature entities | | `living-doc-create-feature` | Feature entity | User Story entities | | `living-doc-create-functionality` | Functionality entity + Functionality feature file stub | Feature entity | -| `living-doc-pageobject-scan` | PageObject files + Functionality feature file stubs + `ExplorationFixture` entries in `seed.yaml` | App URL or test suite; `seed.yaml form_fixtures` | +| `living-doc-pageobject-scan` | PageObject files + Functionality feature file stubs + fixture entries in `seed.yaml` | App URL or test suite; `seed.yaml form_fixtures` | | `living-doc-scenario-creator` | E2E BDD scenario files (US) + Functionality feature files (FUNC) | US / FUNC entities, PageObjects | -| `living-doc-tutorial-creator` | Tutorial documents | BDD scenario files, User Story entities | | `living-doc-gap-finder` | Gap report | All of the above | diff --git a/skills/shared/references/schemas/project-profile.schema.json b/skills/shared/references/schemas/project-profile.schema.json index 26611e6..806a7b0 100644 --- a/skills/shared/references/schemas/project-profile.schema.json +++ b/skills/shared/references/schemas/project-profile.schema.json @@ -7,11 +7,12 @@ "required": [ "test_id_attribute", "feature_dirs", - "paths", - "ac_states", - "pageobject_statuses" + "paths" ], "additionalProperties": true, + "not": { + "required": ["pageobject_statuses"] + }, "properties": { "test_id_attribute": { "type": "string", @@ -40,14 +41,12 @@ "ac_states": { "type": "array", "minItems": 1, - "items": { "type": "string", "minLength": 1 }, - "description": "AC state vocabulary. Canonical form is lowercase with underscores (e.g. planned, in_review, active, deprecated). Legacy Title Case values (e.g. 'In Review') are normalized on load by scripts such as coverage_report.py." - }, - "pageobject_statuses": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 }, - "description": "PageObject header 'status:' vocabulary, lowercase, e.g. [planned, candidate, active, deprecated]." + "items": { + "type": "string", + "enum": ["planned", "in_review", "active", "deprecated"] + }, + "uniqueItems": true, + "description": "AC state vocabulary — a subset of the four canonical states, lowercase with underscores. Omitting this field is allowed; no other spelling or extra state is accepted. There is no PageObject status vocabulary — PageObject headers carry no `status:` field at all." }, "scenario_conventions": { "type": "object", diff --git a/skills/shared/references/schemas/test_project_profile_schema.py b/skills/shared/references/schemas/test_project_profile_schema.py new file mode 100644 index 0000000..3679668 --- /dev/null +++ b/skills/shared/references/schemas/test_project_profile_schema.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +""" +Regression coverage for project-profile.schema.json's ac_states / pageobject_statuses +constraints (living-doc canon: ac_states is a subset of the four canonical states; +there is no PageObject status vocabulary at all). +""" +__test__ = False # pytest: ignore this helper script + +import json +import sys +from pathlib import Path + +try: + import jsonschema +except ImportError: + print( + "Error: the 'jsonschema' package is required to run this test.\n" + "Install it with: pip install jsonschema", + file=sys.stderr, + ) + sys.exit(1) + +SCHEMA_PATH = Path(__file__).parent / "project-profile.schema.json" +SCHEMA = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + +BASE_PROFILE = { + "test_id_attribute": "data-cy", + "feature_dirs": {"user_story": "features/liv_doc_us", "functionality": "features/liv_doc_func"}, + "paths": {"bdd_artifacts": ".copilot/bdd", "pageobjects": "playwright/pages", "steps": "playwright/steps"}, +} + + +def is_valid(profile: dict) -> bool: + try: + jsonschema.validate(profile, SCHEMA) + return True + except jsonschema.ValidationError: + return False + + +def test_canonical_ac_states_accepted(): + profile = {**BASE_PROFILE, "ac_states": ["planned", "in_review", "active", "deprecated"]} + assert is_valid(profile), "canonical ac_states must be accepted" + print("✓ Canonical ac_states list is accepted") + + +def test_ac_states_field_omission_allowed(): + assert is_valid(dict(BASE_PROFILE)), "omitting ac_states must be allowed" + print("✓ Omitting ac_states is allowed") + + +def test_ac_states_title_case_rejected(): + profile = {**BASE_PROFILE, "ac_states": ["In Review"]} + assert not is_valid(profile), "ac_states: [In Review] must be rejected" + print("✓ ac_states: [In Review] is rejected") + + +def test_ac_states_unknown_value_rejected(): + profile = {**BASE_PROFILE, "ac_states": ["done"]} + assert not is_valid(profile), "ac_states: [done] must be rejected" + print("✓ ac_states: [done] is rejected") + + +def test_ac_states_duplicate_rejected(): + profile = {**BASE_PROFILE, "ac_states": ["active", "active"]} + assert not is_valid(profile), "ac_states: [active, active] must be rejected" + print("✓ ac_states: [active, active] is rejected") + + +def test_pageobject_statuses_key_rejected(): + profile = {**BASE_PROFILE, "pageobject_statuses": ["planned", "candidate", "active", "deprecated"]} + assert not is_valid(profile), "a pageobject_statuses key must be rejected" + print("✓ A pageobject_statuses key is rejected") + + +if __name__ == "__main__": + try: + test_canonical_ac_states_accepted() + test_ac_states_field_omission_allowed() + test_ac_states_title_case_rejected() + test_ac_states_unknown_value_rejected() + test_ac_states_duplicate_rejected() + test_pageobject_statuses_key_rejected() + print("\n✓ All tests passed!") + except AssertionError as e: + print(f"✗ Test failed: {e}") + sys.exit(1) + except Exception as e: + print(f"✗ Unexpected error: {e}") + import traceback + traceback.print_exc() + sys.exit(1)