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..5e43e0d 100644 --- a/.github/agents/living-doc-bdd-copilot.agent.md +++ b/.github/agents/living-doc-bdd-copilot.agent.md @@ -178,7 +178,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. Either way, add `deprecated_at`, `deprecation_reason`, and optionally `superseded_by` | | 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 +186,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 +275,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: ... ``` 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..7de5c05 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -4,12 +4,47 @@ on: pull_request: paths: - "skills/**/*.py" + - "skills/shared/references/schemas/*.schema.json" + - "scripts/**" + - "skills/shared/references/living-doc-glossary.md" - ".github/workflows/test-scripts.yml" permissions: contents: read jobs: + glossary-sync-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract synced ref from the glossary header + id: ref + run: | + set -euo pipefail + header="$(head -n1 skills/shared/references/living-doc-glossary.md)" + ref="$(echo "$header" | sed -nE 's/.*synced from AbsaOSS\/living-doc@([^ ]+).*/\1/p')" + if [ -z "$ref" ]; then + echo "Error: could not find a 'synced from AbsaOSS/living-doc@' header comment on line 1 of the glossary." >&2 + exit 1 + fi + echo "ref=$ref" >> "$GITHUB_OUTPUT" + + - name: Re-run the sync script and diff against the committed copy + env: + GLOSSARY_REF: ${{ steps.ref.outputs.ref }} + run: | + set -euo pipefail + cp skills/shared/references/living-doc-glossary.md /tmp/committed-glossary.md + bash scripts/sync-living-doc-glossary.sh "$GLOSSARY_REF" + if ! diff -u /tmp/committed-glossary.md skills/shared/references/living-doc-glossary.md; then + echo + echo "skills/shared/references/living-doc-glossary.md is out of sync with AbsaOSS/living-doc@${GLOSSARY_REF}." >&2 + echo "Run: scripts/sync-living-doc-glossary.sh ${GLOSSARY_REF}" >&2 + exit 1 + fi + run-tests: runs-on: ubuntu-latest steps: @@ -22,7 +57,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/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..8bb4896 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) +- Status transitions (planned → in_review → active → deprecated) - Ownership changes and notifications - Entity linking workflows - Propagation to downstream skills diff --git a/scripts/sync-living-doc-glossary.sh b/scripts/sync-living-doc-glossary.sh new file mode 100644 index 0000000..bffb2e7 --- /dev/null +++ b/scripts/sync-living-doc-glossary.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# sync-living-doc-glossary.sh — pull the canon glossary from AbsaOSS/living-doc and write the +# local copy that agentic-toolkit's skills load. +# +# The canon lives at living-doc's docs/guides/living-doc-glossary.md today. A later living-doc +# change relocates guides under docs/reference/ — when that lands, update SOURCE_PATH below (and +# the link-rewriting rules, which assume the source and its cross-references live under +# docs/guides/ and docs/examples/). +# +# Usage: +# scripts/sync-living-doc-glossary.sh +# +# is any git ref in AbsaOSS/living-doc: a commit SHA (preferred, for +# reproducibility), a tag, or a branch name. +# +# Writes: skills/shared/references/living-doc-glossary.md +# Exits 1 if the ref is missing, the fetch fails, or the fetched file is empty. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DEST="${REPO_ROOT}/skills/shared/references/living-doc-glossary.md" +SOURCE_PATH="docs/guides/living-doc-glossary.md" +LIVING_DOC_REPO="AbsaOSS/living-doc" + +if [ "${1:-}" = "" ]; then + echo "Usage: $0 " >&2 + echo " : a commit SHA, tag, or branch in ${LIVING_DOC_REPO}" >&2 + exit 1 +fi +REF="$1" + +# 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}")" + +RAW_URL="https://raw.githubusercontent.com/${LIVING_DOC_REPO}/${REF_URL_SAFE}/${SOURCE_PATH}" + +TMP_FILE="$(mktemp)" +trap 'rm -f "${TMP_FILE}"' EXIT + +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 glossary path." >&2 + exit 1 +fi + +if [ ! -s "${TMP_FILE}" ]; then + echo "Error: fetched file is empty — refusing to overwrite ${DEST}" >&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/ (the 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 + cat "${TMP_FILE}" +} > "${DEST}" + +echo "Wrote $(wc -l < "${DEST}") lines to ${DEST#${REPO_ROOT}/}" >&2 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..de32ac8 100644 --- a/skills/living-doc-create-feature/SKILL.md +++ b/skills/living-doc-create-feature/SKILL.md @@ -1,10 +1,10 @@ --- 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, enabling impact analysis and traceability in the living + documentation. Use when documenting a new screen, API, or backend service; mapping surfaces + to User Stories; or resolving Feature naming conflicts. 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", @@ -38,24 +38,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 +74,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 @@ -98,11 +97,10 @@ FUNC entries), leave the array as `[]` and add a warning: | `external_dependencies` | Services or systems this Feature calls (e.g. payment-gateway, order-service) | 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`) -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 +120,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 +130,6 @@ Use this exact output shape for create/document requests: - `name` - `surface_type` - `purpose` - - `status` - `user_stories` - `functionalities` - `owners` @@ -147,9 +144,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 +154,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,9 +165,8 @@ 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) | @@ -185,8 +180,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. | @@ -225,4 +220,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..a037ee7 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" ] }, { diff --git a/skills/living-doc-create-feature/evals/fixture-map.md b/skills/living-doc-create-feature/evals/fixture-map.md index 1734693..6b0dbf2 100644 --- a/skills/living-doc-create-feature/evals/fixture-map.md +++ b/skills/living-doc-create-feature/evals/fixture-map.md @@ -15,13 +15,13 @@ | 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 | diff --git a/skills/living-doc-create-feature/evals/trigger-eval.json b/skills/living-doc-create-feature/evals/trigger-eval.json index 8a37e31..c4718bd 100644 --- a/skills/living-doc-create-feature/evals/trigger-eval.json +++ b/skills/living-doc-create-feature/evals/trigger-eval.json @@ -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, 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..ea7697b 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 @@ -172,11 +171,11 @@ One PageObject ≈ one `UI` Feature. Write the full living-doc header block (see ### 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..e5f087a 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-` | **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..09f4c3d 100644 --- a/skills/living-doc-update/SKILL.md +++ b/skills/living-doc-update/SKILL.md @@ -3,9 +3,9 @@ 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 + an AC, changing a Feature's ownership or deprecation metadata, 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. + Story from planned to active. 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", @@ -30,10 +30,11 @@ 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 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; add `owner_changed_at` (ISO date) and `owner_change_reason` fields; notify the new owner if open User Stories are linked to the Feature | | 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` | +| 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 `deprecated_at`, `deprecation_reason`, and optionally `superseded_by` — never set a `status` field; the surface's retirement is recorded through its Functionalities being deprecated | | Delete a Functionality | Functionality | Do not delete — deprecate it and link to the commit that removed the code | ## Update a User Story — add or modify ACs @@ -42,7 +43,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 +60,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 +97,25 @@ 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 | Feature, Functionality, and User Story | +| `deprecation_reason` | Why it was deprecated | Feature, Functionality, and User Story | +| `deprecated_code_commit` | Commit SHA or URL that removed the backing code (if applicable) | Feature and Functionality | +| `superseded_by` | ID of the replacement entity (if applicable) | Feature, Functionality, and User Story | Rules: - Always deprecate — never delete entities (preserves audit trail) +- 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 - Add `deprecated_code_commit` when the code was removed in a commit - 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 `deprecated_at` / `deprecation_reason` / `superseded_by` as a record of when the surface was retired. - 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 @@ -138,26 +142,29 @@ When a team changes ownership of a Feature, update the `owners` field and set `o ## 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 @@ -206,7 +213,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 +229,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..7ddde2f 100644 --- a/skills/living-doc-update/evals/evals.json +++ b/skills/living-doc-update/evals/evals.json @@ -29,11 +29,11 @@ { "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. Links to the commit that deleted the code if possible. 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", "Flags linked Gherkin scenarios for gherkin-living-doc-sync" @@ -105,13 +105,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,11 +150,11 @@ "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. Add deprecated_at (today's date), deprecation_reason (e.g. 'Replaced by FEAT-002'), and superseded_by: 'FEAT-002' directly on FEAT-003. Never delete the entity — deprecation preserves the audit trail. Flag every Functionality owned by FEAT-003 for deprecation review (setting status: deprecated on each of those, since Functionalities do carry a status). 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 deprecated_at, deprecation_reason, and superseded_by fields on the Feature", "superseded_by points to FEAT-002", "Flags owned Functionalities for deprecation review", "Flags linked tests for update or removal" @@ -195,7 +195,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", diff --git a/skills/living-doc-update/evals/fixture-map.md b/skills/living-doc-update/evals/fixture-map.md index fd8d0ad..2c40a0e 100644 --- a/skills/living-doc-update/evals/fixture-map.md +++ b/skills/living-doc-update/evals/fixture-map.md @@ -10,7 +10,7 @@ | 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)* | +| 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 | 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)* | diff --git a/skills/living-doc-update/scripts/test_validate_entity.py b/skills/living-doc-update/scripts/test_validate_entity.py index 30f3d29..873d894 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,987 @@ 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 deprecation is detected from its markers + directly (is_deprecated's else-branch). One marker present (deprecated_at) but not + the other must still warn only for the missing one.""" + 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) + assert any(i["field"] == "deprecation_reason" for i in issues), issues + assert not [i for i in issues if i["field"] == "deprecated_at"], issues + print("✓ A Feature deprecated via markers (no status field) is still checked for deprecation metadata") + + +def test_deprecated_feature_with_empty_marker_still_warns(): + """A Feature with `deprecated_at: ""` (key present, value falsy) must still be treated + as a deprecation attempt — presence of the key drives is_deprecated, not truthiness. + Before the fix, `entity.get("deprecated_at") or ...` treated an empty string the same + as an absent key, so the whole deprecation-metadata check was skipped and the missing + deprecation_reason went unreported.""" + 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": "", + } + issues = validate(feat) + assert any(i["field"] == "deprecated_at" for i in issues), issues + 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_deprecated_feature_via_deprecated_code_commit_marker_warns(): + """A Feature deprecated via `deprecated_code_commit` alone (no status field, and no + deprecated_at/deprecation_reason/superseded_by) must still be detected as a + deprecation attempt and warned for the missing deprecated_at/deprecation_reason. + SKILL.md's deprecation table lists deprecated_code_commit as applicable to Feature + and Functionality, so is_deprecated's marker check must include it alongside + deprecated_at/deprecation_reason/superseded_by.""" + 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 any(i["field"] == "deprecated_at" for i in issues), issues + assert any(i["field"] == "deprecation_reason" for i in issues), issues + print("✓ A Feature deprecated via deprecated_code_commit alone is still flagged for missing metadata") + + +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_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") + + +# ── 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 +1082,53 @@ 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_deprecated_feature_via_deprecated_code_commit_marker_warns() + test_ac_missing_id_and_description_flagged() + test_ac_unrecognized_state_errors() + 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_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..1cc1d54 100644 --- a/skills/living-doc-update/scripts/validate_entity.py +++ b/skills/living-doc-update/scripts/validate_entity.py @@ -40,11 +40,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 +68,22 @@ 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"], } +# 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"] +# Markers that only signal deprecation for a non-statused entity (Feature) — 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, deprecated_code_commit applies to Feature and Functionality. +FEATURE_DEPRECATION_MARKERS = ("superseded_by", "deprecated_code_commit") VERB_PREFIX_RE = re.compile( r"^(process|handle|manage|do|perform|run|execute|validate|create|update|delete)\b", re.IGNORECASE, @@ -84,19 +97,69 @@ # ── 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 validate(entity: dict, catalog: dict | None = None) -> list[dict]: @@ -147,11 +210,31 @@ 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": + # A Feature carries no `status`, so it is deprecated when it carries any + # deprecation marker directly (deprecated_at / deprecation_reason / superseded_by / + # deprecated_code_commit — see SKILL.md's deprecation table for which entity types + # each marker applies to). + # Presence of the 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". + is_deprecated = ( + status == "deprecated" + if entity_type in STATUSED_ENTITY_TYPES + else any(field in entity for field in (*DEPRECATION_FIELDS, *FEATURE_DEPRECATION_MARKERS)) + ) + if is_deprecated: for dep_field in DEPRECATION_FIELDS: if not entity.get(dep_field): warning( @@ -170,11 +253,40 @@ 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", ) purpose: str = entity.get("purpose", "") if purpose and len(purpose.split()) < 5: @@ -218,12 +330,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): @@ -270,9 +383,15 @@ 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)}", ) @@ -355,7 +474,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..b1f34a2 100644 --- a/skills/shared/references/living-doc-bdd-schemas.md +++ b/skills/shared/references/living-doc-bdd-schemas.md @@ -8,6 +8,10 @@ Templates and schemas for BDD automation files. Load this file when writing or v For entity definitions (IDs, status vocabulary, AC format, relationship diagram) see [living-doc-glossary](./living-doc-glossary.md). +> **Canon source.** This file mirrors `living-doc`'s +> [Living Doc Header Types](https://github.com/AbsaOSS/living-doc/blob/master/docs/guides/living-doc-header-types.md), +> which is the canonical definition of this format. Keep both in sync when either changes. + **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 @@ -42,9 +46,6 @@ paths: # AC state vocabulary as written inside `# AC:` blocks and feature-file headers (lowercase with underscores). ac_states: [planned, in_review, active, deprecated] -# PageObject header `status:` vocabulary (lowercase). -pageobject_statuses: [planned, candidate, active, deprecated] - # Scenario tagging conventions (see "Feature file tags" below). scenario_conventions: feature_tag: true # @US_ID:<id> / @FUNC_ID:<id> on the Feature @@ -63,13 +64,13 @@ manifest_shape: object # manifest is an object with routes array (see Ma | `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 | -> **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. +> **Casing rule:** AC and entity states use **lowercase with underscores** inside `# AC:` blocks, +> entity files, and issue bodies (e.g. `- active`, `- in_review`). 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. There is no PageObject `status:` field — see +> [Feature in a PageObject File](#pageobject-file-header). --- @@ -85,6 +86,9 @@ 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 @@ -94,7 +98,7 @@ Holds all US metadata and is mined during living documentation output generation # # 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 +106,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 +118,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,7 +143,10 @@ 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 | @@ -149,107 +155,22 @@ Feature: <US Title> --- -## 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 | - -**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 FUNC header field — it belongs at scenario level as a tag (e.g. `@test_type:system`). - ---- - ## PageObject File Header 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. 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](./living-doc-glossary.md#feature)); an API surface has no PageObject and carries no header here | | `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) | @@ -261,7 +182,7 @@ 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. | ### Two header formats: Full vs Cross-reference @@ -287,7 +208,6 @@ 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 @@ -306,7 +226,6 @@ 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. | @@ -327,7 +246,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. @@ -337,27 +255,33 @@ The following fields are **intentionally omitted** from the cross-reference head ### 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. | 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). 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]`) -`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. +**`@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 +289,129 @@ 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:` | | `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[]` | + +--- + +## 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 +# 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 +# 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>) ← `(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) | +| `# 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 | + +**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 FUNC header field — it belongs at scenario level as a tag (e.g. `@test_type:system`). --- ## 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. ```yaml # .copilot/bdd/seed.yaml @@ -505,9 +525,10 @@ manifest route's optional `field_constraints[]` (see Manifest schema 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`). Load targeted entries by route +during a session; load the full file 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. diff --git a/skills/shared/references/living-doc-glossary.md b/skills/shared/references/living-doc-glossary.md index 4a7d983..b9ea75c 100644 --- a/skills/shared/references/living-doc-glossary.md +++ b/skills/shared/references/living-doc-glossary.md @@ -1,14 +1,48 @@ -# Living Documentation — Shared Glossary +<!-- synced from AbsaOSS/living-doc@c22a95d4d7515e8db0ce5c08ab979d3313c7c218 — run scripts/sync-living-doc-glossary.sh c22a95d4d7515e8db0ce5c08ab979d3313c7c218 to refresh --> -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/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/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/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/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. See the +> [GitHub issue-body layout](https://github.com/AbsaOSS/living-doc/blob/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/docs/examples/README.md#github-issue-body-layout-canonical) and +> [Living Doc Header Types](https://github.com/AbsaOSS/living-doc/blob/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/docs/guides/living-doc-header-types.md). + ### User Story (US) A business-level requirement expressed from the perspective of a named actor. @@ -23,13 +57,15 @@ 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 -> 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/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/docs/guides/living-doc-header-types.md#1-user-story-in-a-gherkin-feature-file). ### Feature @@ -42,130 +78,47 @@ 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. | - 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`): +- 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 (optional; authored when the surface is retired — the *state* still follows + the Functionalities): - `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 - 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[]` | +> 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/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/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) - 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 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 +136,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/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/docs/guides/living-doc-header-types.md#3-functionality-in-a-gherkin-feature-file). ### Acceptance Criterion (AC) @@ -200,19 +153,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 +200,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 +209,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 +226,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 +269,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/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/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/c22a95d4d7515e8db0ce5c08ab979d3313c7c218/docs/guides/living-doc-header-types.md#manifestjson-exploration-manifest). --- @@ -332,14 +331,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)