@metasession.co/devaudit-cli 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^0.8.2",
36
- "@metasession.co/devaudit-plugin-sdk": "^0.1.39",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.1.41",
37
37
  "commander": "^12.1.0",
38
38
  "consola": "^3.2.3",
39
39
  "env-paths": "^3.0.0",
@@ -58,6 +58,38 @@ Both `ci.yml` and `compliance-evidence.yml` call the same helper, so a feature s
58
58
 
59
59
  If you need a separate release container for a sub-piece of work — e.g. carving REQ-038 out of an in-flight feature — give it its own REQ-ID and tag the commits accordingly.
60
60
 
61
+ ## Two release shapes
62
+
63
+ A release is classified by its version pattern. **Most of this doc walks the tracked path** (a REQ-XXX-tagged release for a single requirement). For develop pushes that don't carry a REQ tag — typically `docs:`, `chore:`, `ci:`, `build:`, `test:`, `compliance:`, `revert:` — the version-deriver falls back to a bare date and the release is **housekeeping**.
64
+
65
+ | Shape | Version pattern | Triggered by | Per-REQ ceremony |
66
+ |---|---|---|---|
67
+ | **Tracked** | `REQ-037`, `REQ-046-FIX`, etc. | A `feat`/`fix`/`refactor`/`perf` commit (REQ tag required by commitlint) | Yes — full Steps 1-9 |
68
+ | **Housekeeping** | `v2026.06.04` (bare date, optionally `.N`) | A push containing only REQ-exempt commit types | **No** per-REQ artefacts; the portal auto-skips test-scope, test-plan, implementation-plan, and test-execution-summary completeness checks |
69
+
70
+ ### Housekeeping releases — what's required
71
+
72
+ Housekeeping releases **skip** the per-requirement evidence (no REQ → no `compliance/evidence/REQ-XXX/` folder) but **still produce two release-scoped artefacts**:
73
+
74
+ | Artefact | Tracked path | Housekeeping path | Auto-generated by CI? |
75
+ |---|---|---|---|
76
+ | Release ticket | `compliance/pending-releases/RELEASE-TICKET-REQ-XXX.md` (Step 8) | `compliance/pending-releases/RELEASE-TICKET-<version>.md` (e.g. `RELEASE-TICKET-v2026.06.04.md`) | **Yes** — `generate-housekeeping-release-ticket.sh` runs after `derive-release-version.sh` and emits a stub the operator reviews + signs off |
77
+ | Security summary | `compliance/evidence/REQ-XXX/security-summary.md` (Step 4) | `compliance/security-summary-<version>.md` at the compliance root (release-scoped, not REQ-scoped) | **Yes** — `generate-security-summary.sh` scrapes the SAST + dep-audit gate JSON and emits a stub with an operator sign-off block |
78
+
79
+ **The portal evidence check is lenient on filename.** Any file whose name (case-insensitive) starts with `release-ticket` satisfies the release-ticket check, and any file named exactly `security-summary.md` satisfies the security-summary check. The version-suffixed conventions above are recommended for **searchability + audit trail** when a project has many housekeeping releases stacked up — they keep the artefacts distinct per release without needing folder scoping.
80
+
81
+ **What housekeeping operators do, end to end:**
82
+
83
+ 1. Push the `docs:` / `chore:` / `ci:` commits to develop. CI runs the four gates as usual.
84
+ 2. CI's `compliance-evidence.yml` workflow auto-opens a PR (`chore/housekeeping-release-<version>`) containing the two stubs.
85
+ 3. Review the stubs — confirm the commit-summary list in the release ticket is sensible, confirm the SAST + dep-audit summary reads correctly, fill in the operator sign-off block on each.
86
+ 4. Merge that PR. The next CI run picks up the artefacts and the portal's release-completeness checklist flips both items to ✓.
87
+ 5. Submit for UAT review on the portal. The approval flow is identical to the tracked path — same four-eyes rules (per project risk tier), same `draft → uat_review → uat_approved → prod_review → prod_approved → released` state machine.
88
+
89
+ **No auto-approval.** Housekeeping releases still require operator action to advance through the gates. The CI-generated stubs replace the operator's authoring effort, not the operator's review.
90
+
91
+ > **Versions of the framework before 2026-06 produced housekeeping stubs by hand.** Older release records may have `security-summary.md` under a REQ-XXX-shaped folder or no release ticket at all — leave those in DRAFT for the audit trail; backfilling isn't required.
92
+
61
93
  ## Steps
62
94
 
63
95
  ### Step 0: Confirm CI Is Green
@@ -45,7 +45,7 @@ A worked end-to-end example for a zero-risk change (a typo, a dependency bump, a
45
45
  3. **Commit with a housekeeping type.** `docs:` / `chore:` / `ci:` / `build:` / `test:` / `revert:` are **exempt** from the `[REQ-XXX]` rule — e.g. `git commit -m "docs: fix typo in README"`. A `feat` / `fix` / `refactor` / `perf` subject without a `[REQ-XXX]` or `Ref: REQ-XXX` is **rejected** by commitlint and `validate-commits.sh` — if that fires, you picked the wrong type and the change isn't trivial.
46
46
  4. **Run the gates locally — not optional.** `npx tsc --noEmit`, lint, and the test suite must pass before you push. Trivial ≠ unverified.
47
47
  5. **Push and open a PR.** CI runs the same quality gates. `compliance-validation.yml` finds no `REQ-XXX` and **skips** artifact validation; no release ticket, no RTM row, no evidence pack is required.
48
- 6. **Merge once CI is green** and a reviewer approves the PR. There's **no** portal release record to approve, no UAT/Production gate, and no close-out a housekeeping push produces at most a bare-date release (`vYYYY.MM.DD`), which carries no approval gate. (Contrast the tracked-change flow below, which produces a `REQ-XXX` release that goes through four-eyes.)
48
+ 6. **Merge once CI is green** and a reviewer approves the PR. A housekeeping push **does** produce a portal release record a bare-date release (`vYYYY.MM.DD`) and it **does** go through the same UAT → production four-eyes flow as a tracked release. What the portal **auto-skips for housekeeping** is the four per-REQ completeness items (test-scope / test-plan / implementation-plan / test-execution-summary) — those have nothing to evaluate without a `REQ-XXX`. What's still required: all four CI gates green, a release ticket (`compliance/pending-releases/RELEASE-TICKET-<version>.md`), and a security summary (`compliance/security-summary-<version>.md`). **CI auto-generates both stubs as an operator-sign-off PR** (DevAudit-Installer v0.1.41+, scripts `generate-housekeeping-release-ticket.sh` + `generate-security-summary.sh`). Review the stubs + replace the `REPLACE — …` markers + merge → next CI run uploads them → matrix flips both items to ✓ → submit for UAT review on the portal. See [stage-3 housekeeping section](./3-compile-evidence.md) for the full walkthrough.
49
49
 
50
50
  If at any step it stops feeling trivial — it changes behaviour, touches auth/payments/data, or an auditor would ask about it — switch to a tracked change and run `sdlc-implementer`. When unsure, it's not trivial.
51
51
 
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # generate-housekeeping-release-ticket.sh
3
+ #
4
+ # Writes a housekeeping release-ticket stub for a bare-date release
5
+ # (`v2026.06.04` etc.). Synced into the consumer's `scripts/` by
6
+ # `devaudit update`; invoked from `compliance-evidence.yml` after
7
+ # `derive-release-version.sh` when the version pattern is bare-date AND
8
+ # no ticket file already exists.
9
+ #
10
+ # Usage:
11
+ # bash scripts/generate-housekeeping-release-ticket.sh <version> > <out>
12
+ #
13
+ # Where <version> is the bare-date version (e.g. `v2026.06.04`) and
14
+ # <out> is the target path (typically `compliance/pending-releases/
15
+ # RELEASE-TICKET-<version>.md`).
16
+ #
17
+ # The stub carries:
18
+ # - Frontmatter with version + generated_at + last_reviewed_at (defaults
19
+ # to today; operator updates on sign-off)
20
+ # - Commit summaries since the previous release tag (or last 20 commits
21
+ # if no prior tag exists)
22
+ # - An operator sign-off block with `REPLACE — …` markers the operator
23
+ # must fill in before merging the auto-PR
24
+ #
25
+ # Companion to `generate-security-summary.sh`. Both are invoked together
26
+ # by the CI workflow.
27
+ #
28
+ # DevAudit-Installer#116 WS3.
29
+
30
+ set -euo pipefail
31
+
32
+ VERSION="${1:-}"
33
+ if [ -z "$VERSION" ]; then
34
+ echo "usage: $(basename "$0") <version>" >&2
35
+ exit 2
36
+ fi
37
+
38
+ # Validate bare-date pattern (matches `classifyReleaseShape` in META-COMPLY).
39
+ if ! [[ "$VERSION" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
40
+ echo "error: '$VERSION' is not a bare-date version (expected vYYYY.MM.DD[.N])" >&2
41
+ exit 2
42
+ fi
43
+
44
+ TODAY=$(date -u +%Y-%m-%d)
45
+
46
+ # Last 20 commits with subject lines. Skip the version-deriver fallback
47
+ # from itself (avoid recursion if this script is re-run on a branch
48
+ # that already includes its own auto-commit).
49
+ COMMIT_LINES=$(git log -20 --pretty=format:'- `%h` %s' 2>/dev/null \
50
+ | grep -v 'chore: housekeeping release-ticket stub' \
51
+ | head -15)
52
+ if [ -z "$COMMIT_LINES" ]; then
53
+ COMMIT_LINES="- _(no commits found in `git log` — this stub was generated outside a git context)_"
54
+ fi
55
+
56
+ cat <<EOF
57
+ ---
58
+ version: "$VERSION"
59
+ release_shape: housekeeping
60
+ generated_at: "$TODAY"
61
+ last_reviewed_at: "$TODAY"
62
+ generated_by: "generate-housekeeping-release-ticket.sh (DevAudit-Installer#116)"
63
+ ---
64
+
65
+ > ⚠️ **AUTO-GENERATED STUB — REPLACE BEFORE MERGE**
66
+ >
67
+ > This release ticket was auto-generated by CI when develop received a
68
+ > housekeeping push (no \`REQ-XXX\` in commit subjects, version derived
69
+ > as bare-date \`$VERSION\`).
70
+ >
71
+ > **The operator must:**
72
+ > 1. Confirm the **Summary** below describes the actual intent of these
73
+ > commits — replace the placeholder if the auto-summary is generic.
74
+ > 2. Fill in the **Sign-off** block with the operator's name + date +
75
+ > risk assessment.
76
+ > 3. Merge this PR. The next \`compliance-evidence.yml\` run will upload
77
+ > this file as \`release_artifact\` evidence, satisfying the
78
+ > portal's release-ticket completeness check.
79
+
80
+ # Release Ticket: $VERSION (housekeeping)
81
+
82
+ **Status:** TESTED - PENDING SIGN-OFF
83
+ **Date:** $TODAY
84
+ **Release Shape:** Housekeeping (bare-date, no REQ tag)
85
+ **Version:** $VERSION
86
+
87
+ ---
88
+
89
+ ## Summary
90
+
91
+ REPLACE — one or two sentences describing what these housekeeping commits delivered. Examples: *"Documentation refresh for the v0.1.39 governance changes; no code paths touched."* / *"Dependency bumps (vitest 4.0.5 → 4.0.6, prettier 4.3.0 → 4.3.1) caught by Dependabot; no behavioural change."* / *"CI workflow housekeeping: retry-on-flake threshold raised, runner image pinned."*
92
+
93
+ ## Commits in this release
94
+
95
+ $COMMIT_LINES
96
+
97
+ ## Risk Assessment
98
+
99
+ REPLACE — confirm risk class for this release (typically LOW for housekeeping):
100
+
101
+ - [ ] **LOW** — documentation, dependency bumps, CI tweaks, internal refactors. No user-visible behaviour change.
102
+ - [ ] **MEDIUM** — touches code paths an end-user reaches; user-visible change is small and well-contained.
103
+ - [ ] **HIGH** — should not be a housekeeping release. If this is checked, **stop and re-tag the commits with \`REQ-XXX\`** so the release is tracked properly.
104
+
105
+ ## Test Evidence
106
+
107
+ | Test Type | Status | Source |
108
+ |---|---|---|
109
+ | TypeScript | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`typecheck\` gate |
110
+ | SAST | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`SAST\` gate |
111
+ | Dependency audit | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`dependency_audit\` gate |
112
+ | E2E | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`e2e\` gate |
113
+ | Test reports | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`test_report\` gate |
114
+
115
+ > Companion artefact: \`compliance/security-summary-$VERSION.md\` (auto-generated by \`generate-security-summary.sh\` in the same workflow run; contains the SAST + dep-audit findings detail).
116
+
117
+ ## Acceptance Criteria
118
+
119
+ - [x] All four compliance gates green on the CI run that produced this stub
120
+ - [x] No \`REQ-XXX\`-tagged commits in this release (housekeeping by definition)
121
+ - [ ] Operator-reviewed and signed off — see Sign-off block below
122
+
123
+ ## Post-Deploy Actions
124
+
125
+ | Type | Script / Command | Target | Required | Notes |
126
+ |------|-----------------|--------|----------|-------|
127
+ | — | None | — | — | Housekeeping releases typically have no post-deploy actions |
128
+
129
+ <!-- Replace the "None" row above if this release requires post-deploy work. -->
130
+
131
+ ---
132
+
133
+ ## Sign-off
134
+
135
+ | Role | Name | Date | Notes |
136
+ |---|---|---|---|
137
+ | Submitter | REPLACE | $TODAY | Auto-generated by CI; reviewed + edited |
138
+ | Reviewer (independent if project risk_tier ≠ low) | REPLACE | REPLACE | REPLACE |
139
+
140
+ ## Audit Trail
141
+
142
+ | Date | Action | Actor | Notes |
143
+ |------|--------|-------|-------|
144
+ | $TODAY | Stub auto-generated | devaudit-bot | Bare-date version $VERSION |
145
+ | REPLACE | Operator sign-off | REPLACE | REPLACE |
146
+ | REPLACE | Submitted for UAT review | REPLACE | Via portal |
147
+ EOF
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env bash
2
+ # generate-security-summary.sh
3
+ #
4
+ # Writes a security-summary stub for a release. Resolves the dangling
5
+ # reference in META-COMPLY's release-checklist (the UI hint mentioned a
6
+ # generator script that didn't exist).
7
+ #
8
+ # Usage:
9
+ # bash scripts/generate-security-summary.sh <version> > <out>
10
+ #
11
+ # - For housekeeping releases (bare-date version), the canonical
12
+ # <out> is `compliance/security-summary-<version>.md` at the
13
+ # compliance root.
14
+ # - For tracked releases (REQ-XXX version), the canonical <out> is
15
+ # `compliance/evidence/REQ-XXX/security-summary.md`. The script
16
+ # outputs the same body shape; only the caller decides the path.
17
+ #
18
+ # Scrapes:
19
+ # - `sast-results.json` (Semgrep) — high/critical findings count
20
+ # - `dependency-audit.json` (npm audit / pip-audit) — high/critical
21
+ # vulnerability count
22
+ # - `gate-outcomes.json` (DevAudit-Installer v0.1.29) — per-gate
23
+ # pass/fail/skip status
24
+ #
25
+ # Each source is optional — the stub gracefully reports "not found"
26
+ # rather than erroring out. The operator fills in any missing detail
27
+ # in the sign-off block before merging the auto-PR.
28
+ #
29
+ # DevAudit-Installer#116 WS4.
30
+
31
+ set -euo pipefail
32
+
33
+ VERSION="${1:-}"
34
+ if [ -z "$VERSION" ]; then
35
+ echo "usage: $(basename "$0") <version>" >&2
36
+ exit 2
37
+ fi
38
+
39
+ TODAY=$(date -u +%Y-%m-%d)
40
+
41
+ # Detect release shape from the version.
42
+ SHAPE="unknown"
43
+ if [[ "$VERSION" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
44
+ SHAPE="housekeeping"
45
+ elif [[ "$VERSION" =~ ^REQ- ]]; then
46
+ SHAPE="tracked"
47
+ fi
48
+
49
+ # Helper: scrape Semgrep JSON for high/critical counts. Soft-fails when
50
+ # the file is absent or jq isn't available.
51
+ sast_summary() {
52
+ if [ ! -f sast-results.json ]; then
53
+ echo "REPLACE — \`sast-results.json\` not present at CWD; check that the SAST gate ran on this commit"
54
+ return
55
+ fi
56
+ if ! command -v jq >/dev/null 2>&1; then
57
+ echo "REPLACE — \`jq\` not available; manually inspect \`sast-results.json\`"
58
+ return
59
+ fi
60
+ local HIGH CRITICAL TOTAL
61
+ HIGH=$(jq -r '[.results[]? | select(.extra.severity == "WARNING" or .extra.severity == "ERROR")] | length' sast-results.json 2>/dev/null || echo "?")
62
+ CRITICAL=$(jq -r '[.results[]? | select(.extra.severity == "ERROR")] | length' sast-results.json 2>/dev/null || echo "?")
63
+ TOTAL=$(jq -r '[.results[]?] | length' sast-results.json 2>/dev/null || echo "?")
64
+ echo "$TOTAL total finding(s) · $HIGH high/warning · $CRITICAL critical/error"
65
+ }
66
+
67
+ # Helper: scrape npm audit JSON. Tolerant of both `npm audit --json`
68
+ # shape (vulnerabilities object) and pip-audit shape (vulnerabilities
69
+ # array).
70
+ dep_audit_summary() {
71
+ if [ ! -f dependency-audit.json ]; then
72
+ echo "REPLACE — \`dependency-audit.json\` not present at CWD; check that the dependency-audit gate ran on this commit"
73
+ return
74
+ fi
75
+ if ! command -v jq >/dev/null 2>&1; then
76
+ echo "REPLACE — \`jq\` not available; manually inspect \`dependency-audit.json\`"
77
+ return
78
+ fi
79
+ # npm audit shape: .vulnerabilities is an object keyed by package
80
+ # name with .severity per row.
81
+ local HIGH CRITICAL TOTAL
82
+ HIGH=$(jq -r '[.vulnerabilities | (if type == "object" then to_entries[] | .value else .[]? end) | select(.severity == "high")] | length' dependency-audit.json 2>/dev/null || echo "?")
83
+ CRITICAL=$(jq -r '[.vulnerabilities | (if type == "object" then to_entries[] | .value else .[]? end) | select(.severity == "critical")] | length' dependency-audit.json 2>/dev/null || echo "?")
84
+ TOTAL=$(jq -r '[.vulnerabilities | (if type == "object" then to_entries[] | .value else .[]? end)] | length' dependency-audit.json 2>/dev/null || echo "?")
85
+ echo "$TOTAL total vulnerability/ies · $HIGH high · $CRITICAL critical"
86
+ }
87
+
88
+ # Helper: gate-outcomes.json (DevAudit-Installer v0.1.29 onwards).
89
+ gate_outcomes_summary() {
90
+ if [ ! -f gate-outcomes.json ]; then
91
+ echo "REPLACE — \`gate-outcomes.json\` not present at CWD"
92
+ return
93
+ fi
94
+ if ! command -v jq >/dev/null 2>&1; then
95
+ echo "REPLACE — \`jq\` not available; manually inspect \`gate-outcomes.json\`"
96
+ return
97
+ fi
98
+ jq -r 'to_entries[] | "- **\(.key)** — \(.value // "?")"' gate-outcomes.json 2>/dev/null \
99
+ || echo "REPLACE — could not parse gate-outcomes.json"
100
+ }
101
+
102
+ SAST_SUMMARY=$(sast_summary)
103
+ DEP_SUMMARY=$(dep_audit_summary)
104
+ GATES=$(gate_outcomes_summary)
105
+
106
+ cat <<EOF
107
+ ---
108
+ version: "$VERSION"
109
+ release_shape: $SHAPE
110
+ generated_at: "$TODAY"
111
+ last_reviewed_at: "$TODAY"
112
+ generated_by: "generate-security-summary.sh (DevAudit-Installer#116)"
113
+ ---
114
+
115
+ > ⚠️ **AUTO-GENERATED STUB — REVIEW BEFORE MERGE**
116
+ >
117
+ > This security summary was auto-generated by CI from the SAST and
118
+ > dependency-audit gate JSON. The operator should:
119
+ >
120
+ > 1. Confirm the **findings summary** matches what they saw on the
121
+ > gate panel for this release.
122
+ > 2. Replace any \`REPLACE — …\` markers below (typically the access-
123
+ > control + audit-log assessment, which the gates can't infer).
124
+ > 3. Sign off in the **Sign-off** block before this PR merges.
125
+
126
+ # Security Summary — $VERSION
127
+
128
+ **Release shape:** $SHAPE
129
+ **Generated:** $TODAY
130
+ **Source data:** \`sast-results.json\` + \`dependency-audit.json\` + \`gate-outcomes.json\` (this CI run)
131
+
132
+ ## SAST findings (Semgrep)
133
+
134
+ $SAST_SUMMARY
135
+
136
+ > **Policy:** the SAST gate fails the build at \`high\` or \`critical\` severity. If this release shipped, both are zero.
137
+
138
+ ## Dependency vulnerabilities
139
+
140
+ $DEP_SUMMARY
141
+
142
+ > **Policy:** the dependency-audit gate fails the build at \`high\` or \`critical\` severity. If this release shipped, both are zero.
143
+
144
+ ## Gate outcomes (per CI run)
145
+
146
+ $GATES
147
+
148
+ ## Access control + audit log
149
+
150
+ | Check | Result | Notes |
151
+ |---|---|---|
152
+ | Access control unchanged | REPLACE — yes/no | If yes, no further work. If no, document the auth/RBAC delta and confirm it landed an audit event. |
153
+ | Audit log append-only invariant preserved | REPLACE — yes/no | If yes, no further work. If no, document why and confirm the change has independent review. |
154
+ | Sensitive data exposure | REPLACE — yes/no | If yes, escalate to the GDPR triage in \`compliance/governance/dpia.md\` before merging. |
155
+
156
+ ## Risk Assessment
157
+
158
+ REPLACE — one paragraph summarising the security posture of this release. For housekeeping releases the typical wording is *"No code paths touched; security posture unchanged from the previous release."* For tracked releases name the touched modules + threat model assessment.
159
+
160
+ ---
161
+
162
+ ## Sign-off
163
+
164
+ | Role | Name | Date | Notes |
165
+ |---|---|---|---|
166
+ | Author | devaudit-bot (auto-generated) | $TODAY | Stub generated from CI gate JSON |
167
+ | Reviewer | REPLACE | REPLACE | REPLACE |
168
+
169
+ Once reviewed + signed off, this file is uploaded as evidence by the next \`compliance-evidence.yml\` run; the portal's release-completeness checklist flips the security-summary item to ✓.
170
+ EOF
@@ -327,7 +327,7 @@ test('AC1: edit dialog opens with fields pre-filled', async ({ page }) => {
327
327
  - Call `evidenceShot` **immediately after** the AC-proving assertion, before navigating, closing dialogs, or any further interaction.
328
328
  - AC number is a separate argument (`ac: number`) — the helper composes the filename `REQ-XXX-AC<n>-<slug>.png`. The slug describes what the screenshot proves (`edit-dialog-prefilled`), NOT the AC number.
329
329
  - Slug is kebab-case lowercase (`[a-z0-9-]+`). Capitalised slugs, underscores, or spaces throw.
330
- - One screenshot per AC, not per test.
330
+ - One **canonical** screenshot per AC; additional stage screenshots are tier-gated — see *Screenshot density per spec role* below.
331
331
  - Failure forensics stays untouched (`screenshot: 'only-on-failure'` + `trace: 'on-first-retry'`).
332
332
 
333
333
  The helper is shipped automatically into `e2e/helpers/evidence.ts` by the SDLC sync (node-stack consumers). Output lands at `compliance/evidence/<REQ-ID>/screenshots/REQ-XXX-AC<n>-<slug>.png` — commit these PNGs as part of the evidence pack so reviewers can corroborate the test-plan AC mapping.
@@ -336,6 +336,43 @@ The helper also writes a sidecar `<filename>.meta.json` containing the AC mappin
336
336
 
337
337
  The canonical helper source lives at `references/evidence.ts` in this skill.
338
338
 
339
+ ### Screenshot density per spec role
340
+
341
+ The number of `evidenceShot` calls per spec should scale to the spec's role:
342
+
343
+ - **While the spec is a feature artefact** (newly authored on the branch, before merge to develop): capture multiple stages — every meaningful transition or state the AC documents. The dense evidence is what reviewers use to verify the AC was met end-to-end during the feature cycle.
344
+ - **Once the spec joins the regression pack** (post-merge, `git diff --diff-filter=A` no longer matches it): capture only the canonical "this still works" anchor per AC. Re-running the dense journey on every regression cycle is noise and inflates CI artefact storage with little signal.
345
+
346
+ The `EvidenceShotOrigin` signal (`'feature' | 'regression'`) auto-detects from `E2E_NEW_SPECS`. Mark stage screenshots with `{ tier: 'feature' }`; the helper auto-suppresses them on regression runs. The canonical anchor uses the default tier (`'always'`).
347
+
348
+ ```ts
349
+ test('AC7: stock dial completes the transition', async ({ page }) => {
350
+ // Stage screenshots — fire while the spec is a feature artefact;
351
+ // auto-suppress once it graduates into the regression pack.
352
+ await openStockDial(page, item.id);
353
+ await evidenceShot(page, 'REQ-066', 7, 'dial-open', { tier: 'feature' });
354
+ await advanceDial(page);
355
+ await evidenceShot(page, 'REQ-066', 7, 'in-progress', { tier: 'feature' });
356
+
357
+ // Canonical anchor — always fires (default tier: 'always').
358
+ // This is the artefact every future regression run re-captures as
359
+ // proof the AC still holds.
360
+ await expect(dial.getByRole('status')).toHaveText('Completed');
361
+ await evidenceShot(page, 'REQ-066', 7, 'completed');
362
+ });
363
+ ```
364
+
365
+ A reasonable default per AC:
366
+
367
+ - 1× canonical "completed / final state" shot (tier `'always'`).
368
+ - 1–3× stage shots covering the meaningful intermediate transitions (tier `'feature'`).
369
+
370
+ When to deviate:
371
+
372
+ - **Single-shot ACs** (one assertion that's its own proof — e.g. *"the form submits and returns to the list"*) need only the canonical anchor. Don't manufacture stages just to hit the 1–3 band.
373
+ - **Long flows** (>3 meaningful transitions) keep all stages tier `'feature'`. The post-merge regression run still has the canonical anchor to corroborate the AC; the dense journey is on the feature PR for reviewers and in the audit-pack download for that release forever.
374
+ - **Reviewer pushback that evidence feels thin** (single-shot per AC across a HIGH-risk REQ) almost always means tier `'feature'` stages are missing — add them on the feature branch where they actually fire, not after.
375
+
339
376
  ---
340
377
 
341
378
  ## Principles
@@ -7,6 +7,23 @@
7
7
 
8
8
  export type EvidenceShotOrigin = 'feature' | 'regression';
9
9
 
10
+ /**
11
+ * Capture density tier. Lets spec authors mark intermediate-state
12
+ * screenshots that only matter while the spec is being authored on a
13
+ * feature branch — once the spec joins the regression pack, those
14
+ * become noise and inflate CI artefact storage.
15
+ *
16
+ * - `'always'` (default) — capture on every run. Use for the canonical
17
+ * "this still works" anchor per AC; the artefact reviewers rely on
18
+ * to corroborate the test-plan mapping across every release.
19
+ * - `'feature'` — capture only when the spec's origin is `feature`
20
+ * (i.e. the spec was added on the current branch per
21
+ * `E2E_NEW_SPECS`). Auto-suppressed once the spec graduates into
22
+ * the regression pack. Use for stage screenshots covering meaningful
23
+ * intermediate transitions reviewers want during the feature cycle.
24
+ */
25
+ export type EvidenceShotTier = 'always' | 'feature';
26
+
10
27
  export interface EvidenceShotSidecar {
11
28
  readonly origin: EvidenceShotOrigin;
12
29
  readonly reqId: string;
@@ -16,6 +33,20 @@ export interface EvidenceShotSidecar {
16
33
  readonly capturedAt: string;
17
34
  }
18
35
 
36
+ /**
37
+ * Pure decision: should the capture be suppressed?
38
+ *
39
+ * The only suppression case is `tier='feature'` × `origin='regression'`
40
+ * — a stage screenshot whose spec has graduated into the regression
41
+ * pack. Every other (tier, origin) combination captures.
42
+ */
43
+ export function shouldSuppressEvidenceShot(
44
+ tier: EvidenceShotTier,
45
+ origin: EvidenceShotOrigin,
46
+ ): boolean {
47
+ return tier === 'feature' && origin === 'regression';
48
+ }
49
+
19
50
  const REQ_ID_RE = /^REQ-[A-Z0-9-]+$/;
20
51
  const SLUG_RE = /^[a-z0-9-]+$/;
21
52
 
@@ -4,12 +4,14 @@ import { test, type Page } from '@playwright/test';
4
4
  import {
5
5
  autoDetectEvidenceShotOrigin,
6
6
  composeScreenshotFilename,
7
+ shouldSuppressEvidenceShot,
7
8
  validateEvidenceShotInputs,
8
9
  type EvidenceShotOrigin,
9
10
  type EvidenceShotSidecar,
11
+ type EvidenceShotTier,
10
12
  } from './evidence-shot-core';
11
13
 
12
- export type { EvidenceShotOrigin };
14
+ export type { EvidenceShotOrigin, EvidenceShotTier };
13
15
 
14
16
  export interface EvidenceShotOptions {
15
17
  /** Capture the full page rather than the viewport. Default: true. */
@@ -21,6 +23,14 @@ export interface EvidenceShotOptions {
21
23
  * the calling spec's file appears in that list, else `regression`.
22
24
  */
23
25
  readonly origin?: EvidenceShotOrigin;
26
+ /**
27
+ * Capture density tier. Default: `'always'`. Set to `'feature'` for
28
+ * intermediate-state screenshots that should only fire while the
29
+ * spec is on a feature branch — they auto-suppress once the spec
30
+ * graduates into the regression pack. See the SKILL.md "Screenshot
31
+ * density per spec role" section for the density policy.
32
+ */
33
+ readonly tier?: EvidenceShotTier;
24
34
  }
25
35
 
26
36
  /**
@@ -59,15 +69,15 @@ export async function evidenceShot(
59
69
  opts: EvidenceShotOptions = {},
60
70
  ): Promise<void> {
61
71
  validateEvidenceShotInputs(reqId, ac, slug);
72
+ const tier: EvidenceShotTier = opts.tier ?? 'always';
73
+ const specFile = resolveSpecFile();
74
+ const origin = opts.origin ?? autoDetectEvidenceShotOrigin(specFile, process.env.E2E_NEW_SPECS);
75
+ if (shouldSuppressEvidenceShot(tier, origin)) return;
62
76
  const fileName = composeScreenshotFilename(reqId, ac, slug);
63
77
  const dir = path.join(process.cwd(), 'compliance/evidence', reqId, 'screenshots');
64
78
  const pngPath = path.join(dir, fileName);
65
79
  const sidecarPath = `${pngPath}.meta.json`;
66
-
67
80
  await page.screenshot({ path: pngPath, fullPage: opts.fullPage ?? true });
68
-
69
- const specFile = resolveSpecFile();
70
- const origin = opts.origin ?? autoDetectEvidenceShotOrigin(specFile, process.env.E2E_NEW_SPECS);
71
81
  const sidecar: EvidenceShotSidecar = {
72
82
  origin,
73
83
  reqId,
@@ -101,6 +101,87 @@ jobs:
101
101
  echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
102
102
  echo "Release version: ${VERSION}"
103
103
 
104
+ # Housekeeping artefact auto-generation (DevAudit-Installer#116 WS3+WS4)
105
+ #
106
+ # Housekeeping releases (bare-date version) need two operator-authored
107
+ # artefacts to clear the portal's release-completeness checklist:
108
+ # `RELEASE-TICKET-<version>.md` and `security-summary-<version>.md`.
109
+ # When the version resolves to a bare-date AND those artefacts don't
110
+ # already exist on develop, generate stubs from the CI gate JSON and
111
+ # open a PR that the operator reviews + signs off + merges. Once
112
+ # merged, the next compliance-evidence.yml run uploads them as
113
+ # evidence and the matrix flips both items to ✓.
114
+ #
115
+ # Mirrors the `incident-export.yml` pattern: auto-PR, operator fills
116
+ # in sign-off, merge to land.
117
+ - name: Auto-generate housekeeping stubs (if needed)
118
+ if: steps.resolve.outputs.skip != 'true'
119
+ env:
120
+ GH_TOKEN: ${{ secrets.DEVAUDIT_USER_TOKEN || github.token }}
121
+ VERSION: ${{ steps.version.outputs.version }}
122
+ run: |
123
+ # Guard: only fire for bare-date (housekeeping) versions.
124
+ if ! [[ "$VERSION" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
125
+ echo "Version ${VERSION} is not housekeeping; skipping stub generation."
126
+ exit 0
127
+ fi
128
+ TICKET_PATH="compliance/pending-releases/RELEASE-TICKET-${VERSION}.md"
129
+ SECSUM_PATH="compliance/security-summary-${VERSION}.md"
130
+ # Skip when both stubs already exist (this branch already carries
131
+ # them, or a prior run already opened the PR and the artefacts
132
+ # have since merged).
133
+ if [ -f "$TICKET_PATH" ] && [ -f "$SECSUM_PATH" ]; then
134
+ echo "Housekeeping stubs already present for ${VERSION}; nothing to generate."
135
+ exit 0
136
+ fi
137
+ chmod +x scripts/generate-housekeeping-release-ticket.sh 2>/dev/null || true
138
+ chmod +x scripts/generate-security-summary.sh 2>/dev/null || true
139
+ mkdir -p compliance/pending-releases
140
+ # Generate any missing stubs. Each generator is independent so a
141
+ # partial-state branch (e.g. ticket already merged but security-
142
+ # summary still missing) only writes the missing half.
143
+ if [ ! -f "$TICKET_PATH" ]; then
144
+ bash scripts/generate-housekeeping-release-ticket.sh "$VERSION" > "$TICKET_PATH"
145
+ echo "Wrote $TICKET_PATH"
146
+ fi
147
+ if [ ! -f "$SECSUM_PATH" ]; then
148
+ bash scripts/generate-security-summary.sh "$VERSION" > "$SECSUM_PATH"
149
+ echo "Wrote $SECSUM_PATH"
150
+ fi
151
+ # Open the auto-PR. force-with-lease so re-runs on the same
152
+ # bare-date version update the same branch in place.
153
+ BRANCH="chore/housekeeping-release-${VERSION}"
154
+ git config user.name 'devaudit-bot'
155
+ git config user.email 'devaudit-bot@users.noreply.github.com'
156
+ git fetch origin develop
157
+ git checkout -B "${BRANCH}" origin/develop
158
+ # Re-write under the new branch (the checkout above resets the
159
+ # working tree — regenerate so the commit captures the stubs).
160
+ if [ ! -f "$TICKET_PATH" ]; then
161
+ bash scripts/generate-housekeeping-release-ticket.sh "$VERSION" > "$TICKET_PATH"
162
+ fi
163
+ if [ ! -f "$SECSUM_PATH" ]; then
164
+ bash scripts/generate-security-summary.sh "$VERSION" > "$SECSUM_PATH"
165
+ fi
166
+ git add "$TICKET_PATH" "$SECSUM_PATH"
167
+ if git diff --cached --quiet; then
168
+ echo "No change — stubs already on develop."
169
+ exit 0
170
+ fi
171
+ git commit -m "chore: housekeeping release-ticket stub + security-summary for ${VERSION}" \
172
+ -m "Auto-generated by compliance-evidence.yml (DevAudit-Installer#116)." \
173
+ -m "" \
174
+ -m "REPLACE markers in the Risk Assessment + Sign-off blocks require operator review before merge."
175
+ git push --force-with-lease origin "${BRANCH}"
176
+ EXISTING=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number' || true)
177
+ if [ -z "$EXISTING" ]; then
178
+ gh pr create --base develop --head "${BRANCH}" \
179
+ --title "chore(housekeeping): release-ticket + security-summary stubs for ${VERSION}" \
180
+ --body "Auto-generated by the \`Compliance Evidence Upload\` workflow when develop received a housekeeping push (no \`REQ-XXX\` in commit subjects; version derived as bare-date \`${VERSION}\`).\n\n**Files added:**\n- \`${TICKET_PATH}\` — release ticket stub with commit summary + sign-off block\n- \`${SECSUM_PATH}\` — security summary stub scraping SAST + dep-audit gate JSON\n\n**Required before merge:**\n- [ ] Replace \`REPLACE — …\` markers in the **Summary**, **Risk Assessment**, and **Sign-off** blocks on both files\n- [ ] Confirm the auto-scraped SAST + dep-audit counts match what the gate panel shows\n- [ ] Add reviewer sign-off (operator name + date)\n\nOnce merged, the next \`compliance-evidence.yml\` run uploads both artefacts as evidence and the portal's release-completeness checklist flips the **Security summary present** + **Release ticket present** items to ✓.\n\nSee [stage-3 SDLC doc \"Housekeeping releases\" section](../SDLC/3-compile-evidence.md) for the full operator walkthrough."
181
+ else
182
+ echo "PR #${EXISTING} already open for ${VERSION} — branch updated in place."
183
+ fi
184
+
104
185
  - name: Upload compliance documents
105
186
  if: steps.resolve.outputs.skip != 'true'
106
187
  run: |