create-sdd-project 0.18.2 → 0.18.4

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/lib/meta.js CHANGED
@@ -12,13 +12,27 @@
12
12
  * warnings on cross-version upgrades — the Codex P1 finding from the
13
13
  * v0.16.10 cross-model review).
14
14
  *
15
- * Core invariant (Codex M1 from plan v1.0 review): a hash in this file
16
- * represents "the last time the tool wrote this file, the content hashed
17
- * to X". Hashes are ONLY written/updated when the tool actually wrote
18
- * canonical output to a file in the current run (replaced, new, or
19
- * --force-template paths). Preserved files leave their hash entry
20
- * untouched otherwise the user's customized content would be hashed and
21
- * silently overwritten on the next upgrade.
15
+ * Core invariant (Codex M1 from plan v1.0 review, refined v0.18.4 G1):
16
+ * a hash in this file represents the canonical TEMPLATE output the tool
17
+ * last attempted to install for this path. Hashes are written/updated in
18
+ * three situations:
19
+ *
20
+ * 1. The tool wrote canonical output (replaced, new file, or
21
+ * --force-template paths) record the new template hash.
22
+ * 2. v0.18.4 G1 — Case 3c fallback preserve writes the template hash to
23
+ * bootstrap a previously-untracked path into the hash-based decision
24
+ * tree for future upgrades. The file on disk is NOT modified (user's
25
+ * customization is preserved + .new is written); only the meta entry
26
+ * is recorded so the next upgrade can enter Case 2 instead of falling
27
+ * through Case 3 again. Records what we "attempted to install", which
28
+ * is the correct anchor for both: (a) the user later accepting .new
29
+ * → user_current == stored → Case 2a clean replace, and (b) the user
30
+ * keeping their customization → user_current != stored → Case 2b
31
+ * preserve (situation (3) below then applies).
32
+ * 3. NEVER on Case 2b preserve — when a stored hash already exists and
33
+ * the current file diverges from it, the prior baseline is kept
34
+ * untouched. Otherwise the user's customized content would be hashed
35
+ * and silently overwritten on the next upgrade.
22
36
  *
23
37
  * File format (schemaVersion: 1):
24
38
  * {
@@ -540,8 +540,21 @@ function generateUpgrade(config) {
540
540
  continue;
541
541
  }
542
542
 
543
- // Content mismatch → preserve. Same rule: no hash update.
543
+ // Content mismatch → preserve.
544
+ //
545
+ // v0.18.4 G1: bootstrap hash AFTER preserve so next upgrade enters
546
+ // the hash-based path (Case 2) instead of falling through Case 3c
547
+ // again. Recording the TEMPLATE hash (not the user's hash) is the
548
+ // correct semantic anchor:
549
+ // - If user later accepts .new → user_current == stored → Case 2a
550
+ // clean replace.
551
+ // - If user keeps customization → user_current != stored → Case 2b
552
+ // preserve (Codex M1 invariant then applies — no further hash
553
+ // updates).
554
+ // This is opt-in at the CALL SITE, not inside preserveFile, so
555
+ // Case 2b callers above remain governed by the M1 invariant.
544
556
  preserveFile(adaptedFullTargetFallback);
557
+ newHashes[posixPath] = computeHash(adaptedFullTargetFallback);
545
558
  }
546
559
  continue;
547
560
  }
@@ -651,7 +664,9 @@ function generateUpgrade(config) {
651
664
  replaced++;
652
665
  continue;
653
666
  }
667
+ // v0.18.4 G1: Case 3c bootstrap (see agents site for full rationale).
654
668
  preserveCmd();
669
+ newHashes[posixPath] = computeHash(rawTemplate);
655
670
  }
656
671
  continue;
657
672
  }
@@ -836,6 +851,8 @@ function generateUpgrade(config) {
836
851
  }
837
852
  modifiedAgentsResults.push({ name: relativePath, modified: true });
838
853
  preserved++;
854
+ // v0.18.4 G1: Case 3c bootstrap (see agents site for full rationale).
855
+ newHashes[posix] = computeHash(adaptedTarget);
839
856
  }
840
857
 
841
858
  // --- d) Handle standards (smart diff) ---
@@ -979,7 +996,9 @@ function generateUpgrade(config) {
979
996
  replaced++;
980
997
  continue;
981
998
  }
999
+ // v0.18.4 G1: Case 3c bootstrap (see agents site for full rationale).
982
1000
  preserveStandard();
1001
+ newHashes[spec.posix] = computeHash(freshAdapted);
983
1002
  }
984
1003
 
985
1004
  step('Updated standards files');
@@ -1055,7 +1074,9 @@ function generateUpgrade(config) {
1055
1074
  newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
1056
1075
  replaced++;
1057
1076
  } else {
1077
+ // v0.18.4 G1: Case 3c bootstrap (see agents site for full rationale).
1058
1078
  preserveAgentsMd();
1079
+ newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
1059
1080
  }
1060
1081
  }
1061
1082
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "description": "Create a new SDD DevFlow project with AI-assisted development workflow",
5
5
  "bin": {
6
6
  "create-sdd-project": "bin/cli.js"
@@ -52,12 +52,21 @@ Run only if `git diff origin/<target-branch>..HEAD --name-only` shows `.json` fi
52
52
 
53
53
  Eleven empirically-validated drift patterns. Failures are NOT blockers for the compliance verdict, but MUST be refreshed before requesting user authorization (the user will otherwise catch them during audit and send the PR back). Each check has a concrete shell recipe — use BSD-grep-compatible regex (no `\K`).
54
54
 
55
- **12. P1 — PR body test count stale.** The PR body's "npm test" line should match the terminal test count in the ticket (AC / DoD / Completion Log last entry). Agents commonly open the PR at Step 4 and add tests during Step 5 review — the PR body number becomes stale.
55
+ **12. P1 — PR body test count stale (v0.18.3 multi-workspace extension — C1).** The PR body's test ratios should all appear in ticket evidence (AC / DoD / Completion Log). Agents commonly open the PR at Step 4 and add tests during Step 5 review — the PR body numbers become stale. In monorepos with multiple workspaces (e.g. api, web, bot, scraper) the PR body may quote several `N/N` ratios; v0.18.3 walks them all instead of comparing only the first. Subset direction: PR ratios ⊆ ticket ratios (the ticket Completion Log is the more comprehensive record and accumulates intermediate per-step ratios that the PR body legitimately omits). Three fallback cases: (a) ≥ 1 ratio on each side → verify each PR ratio appears in ticket; (b) PR has ratios but ticket has none (or vice versa) → emit explicit `P1 N/A` note, no drift flag; (c) neither side has ratios → emit `P1 N/A` note.
56
56
  ```bash
57
- PR_BODY=$(gh pr view --json body -q .body)
58
- PR_TESTS=$(echo "$PR_BODY" | grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" | grep -oE "[0-9]+/[0-9]+" | head -1)
59
- TICKET_TESTS=$(grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | tail -1)
60
- [ -n "$PR_TESTS" ] && [ -n "$TICKET_TESTS" ] && [ "$PR_TESTS" != "$TICKET_TESTS" ] && flag "P1 drift: PR body $PR_TESTS vs ticket $TICKET_TESTS"
57
+ TEST_KW_RE='(npm test|pnpm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])'
58
+ PR_BODY=$(gh pr view --json body -q .body 2>/dev/null || true)
59
+ PR_RATIOS=$(echo "$PR_BODY" | grep -iE "$TEST_KW_RE" | grep -oE "[0-9]+/[0-9]+" | sort -u)
60
+ TICKET_RATIOS=$(grep -iE "$TEST_KW_RE" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | sort -u)
61
+ if [ -z "$PR_RATIOS" ] || [ -z "$TICKET_RATIOS" ]; then
62
+ echo "P1 N/A: no comparable test-count ratios extracted (PR=$(echo "$PR_RATIOS" | tr '\n' ',' ), ticket=$(echo "$TICKET_RATIOS" | tr '\n' ',' ))"
63
+ else
64
+ while IFS= read -r r; do
65
+ [ -z "$r" ] && continue
66
+ echo "$TICKET_RATIOS" | grep -qFx "$r" \
67
+ || flag "P1 drift: PR ratio $r not found in ticket evidence (ticket ratios: $(echo "$TICKET_RATIOS" | tr '\n' ' '))"
68
+ done <<< "$PR_RATIOS"
69
+ fi
61
70
  ```
62
71
 
63
72
  **13. P2 — Merge Checklist Evidence rows aspirational.** Rows marked `[x]` with future-tense Evidence ("will land", "to be created", "pending", "next commit", "TBD") — the row claims done but the work hasn't happened yet.
@@ -94,6 +103,8 @@ for t in docs/tickets/*.md; do
94
103
  status=$(grep -E "^\*\*Status:\*\*" "$t" | head -1 \
95
104
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
96
105
  | sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
106
+ | sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
107
+ | sed -E 's/\*\*[[:space:]]*$//' \
97
108
  | sed -E 's/[[:space:]]+$//')
98
109
  [ "$status" = "Done" ] && continue
99
110
  ticket_id=$(basename "$t" .md | sed -E 's/-[a-z].*//')
@@ -103,12 +114,27 @@ done
103
114
  [ "$FROZEN_COUNT" -eq 1 ] && flag "P5 drift: 1 frozen ticket"
104
115
  ```
105
116
 
106
- **17. P6 — AC count off-by-N.** Merge Checklist Evidence row 1 claim ("all N marked" / "AC: X/Y") diverges from actual count of `[x]` + `[ ]` in `## Acceptance Criteria`.
117
+ **17. P6 — AC count off-by-N.** Merge Checklist Evidence row 1 claim diverges from actual count. Two canonical forms: `all N marked` (N = total, implies all are `[x]`) and `AC: X/Y done` (X = marked, Y = total — supports deferred ACs where Y > X intentionally). For `AC: X/Y` form, compare Y to actual total AND X to actual marked. For `all N marked` form, compare N to actual total.
107
118
  ```bash
108
- ACTUAL=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET" | grep -cE "^- \[[x ]\]")
109
- CLAIMED=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1 | grep -oE "[0-9]+" | head -1)
110
- [ -n "$CLAIMED" ] && [ "$CLAIMED" != "$ACTUAL" ] && [ $((ACTUAL - CLAIMED)) -ge 2 -o $((CLAIMED - ACTUAL)) -ge 2 ] \
111
- && flag "P6 drift: claim '$CLAIMED' vs actual AC count $ACTUAL"
119
+ AC_BLOCK=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
120
+ ACTUAL_TOTAL=$(echo "$AC_BLOCK" | grep -cE "^- \[[x ]\]")
121
+ ACTUAL_MARKED=$(echo "$AC_BLOCK" | grep -cE "^- \[x\]")
122
+ CLAIM_LINE=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1)
123
+ if echo "$CLAIM_LINE" | grep -qE '^AC: [0-9]+/[0-9]+'; then
124
+ CLAIMED_MARKED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
125
+ CLAIMED_TOTAL=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | tail -1)
126
+ [ -n "$CLAIMED_TOTAL" ] && [ "$CLAIMED_TOTAL" != "$ACTUAL_TOTAL" ] \
127
+ && [ $((ACTUAL_TOTAL - CLAIMED_TOTAL)) -ge 2 -o $((CLAIMED_TOTAL - ACTUAL_TOTAL)) -ge 2 ] \
128
+ && flag "P6 drift: claim AC total '$CLAIMED_TOTAL' vs actual total $ACTUAL_TOTAL"
129
+ [ -n "$CLAIMED_MARKED" ] && [ "$CLAIMED_MARKED" != "$ACTUAL_MARKED" ] \
130
+ && [ $((ACTUAL_MARKED - CLAIMED_MARKED)) -ge 2 -o $((CLAIMED_MARKED - ACTUAL_MARKED)) -ge 2 ] \
131
+ && flag "P6 drift: claim AC marked '$CLAIMED_MARKED' vs actual marked $ACTUAL_MARKED"
132
+ elif [ -n "$CLAIM_LINE" ]; then
133
+ CLAIMED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
134
+ [ -n "$CLAIMED" ] && [ "$CLAIMED" != "$ACTUAL_TOTAL" ] \
135
+ && [ $((ACTUAL_TOTAL - CLAIMED)) -ge 2 -o $((CLAIMED - ACTUAL_TOTAL)) -ge 2 ] \
136
+ && flag "P6 drift: 'all $CLAIMED marked' vs actual AC count $ACTUAL_TOTAL"
137
+ fi
112
138
  ```
113
139
 
114
140
  **18. P7 — Test count drift within ticket (final-sections only).** Only flag AC / DoD / tracker Active-Session numbers diverging from Completion Log terminal. Intermediate rows are legitimate.
@@ -157,16 +183,36 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
157
183
  TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
158
184
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
159
185
  | sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
186
+ | sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
187
+ | sed -E 's/\*\*[[:space:]]*$//' \
160
188
  | sed -E 's/[[:space:]]+$//')
161
- FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
162
- TRACKER_STATUS=$(grep -F "$FEATURE_ID" docs/project_notes/product-tracker.md | grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
163
- case "$TICKET_STATUS" in
164
- "Ready for Merge"|"Review"|"In Progress"|"Planning"|"Spec") EXPECTED="in-progress" ;;
165
- "Done") EXPECTED="done" ;;
166
- *) EXPECTED="" ;;
189
+ TICKET_BASENAME=$(basename "$TICKET" .md)
190
+ # v0.18.4 P11-B: sub-scope tickets (-lite / -FU / -FU[0-9]*) close a partial
191
+ # scope of a parent feature; the parent tracker row stays at its parent status
192
+ # (typically `pending` or `in-progress`) while the sub-scope ticket reaches
193
+ # `Done`. P11 must NOT enforce status mapping across this boundary. Pattern
194
+ # scope: empirically derived from fx convention (uppercase -FU + -lite). If
195
+ # future projects introduce -spike / -mini / -aux variants, expand here.
196
+ case "$TICKET_BASENAME" in
197
+ *-lite|*-lite-*|*-FU|*-FU-*|*-FU[0-9]*)
198
+ echo "P11 N/A: $TICKET_BASENAME is a sub-scope ticket — parent tracker row status independent" >&2
199
+ ;;
200
+ *)
201
+ FEATURE_ID=$(echo "$TICKET_BASENAME" | sed -E 's/-[a-z].*//')
202
+ # v0.18.4 P11-B drive-by hardening: anchor tracker lookup to pipe-table row
203
+ # (FEATURE_ID as first cell). Mirrors v0.18.3 P16 idiom — prevents narrative
204
+ # mentions earlier in the tracker from silencing or false-firing the check.
205
+ TRACKER_STATUS=$(grep -E "^\|[[:space:]]*$FEATURE_ID[[:space:]]*\|" docs/project_notes/product-tracker.md \
206
+ | grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
207
+ case "$TICKET_STATUS" in
208
+ "Ready for Merge"|"Review"|"In Progress"|"Planning"|"Spec") EXPECTED="in-progress" ;;
209
+ "Done") EXPECTED="done" ;;
210
+ *) EXPECTED="" ;;
211
+ esac
212
+ [ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
213
+ && flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
214
+ ;;
167
215
  esac
168
- [ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
169
- && flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
170
216
  ```
171
217
 
172
218
  **23. P12 — Tracker HEAD references stale (added v0.18.2).** The `**Last Updated:**` and `**Active Feature:**` lines may embed `HEAD <sha>` or `HEAD: <sha>` references that were correct when written but went stale as further commits landed (empirically observed in fx F-WEB-MENU-VISION-001 audit cycle 2026-05-06: tracker said `HEAD: fd752e4` while `git rev-parse HEAD` was `6fa801e` after the agent's own self-edit commit). Compare each extracted SHA against the active branch HEAD. Bidirectional prefix tolerance: a 7-char tracker SHA matches the full 40-char actual HEAD if it's a prefix; a full 40-char tracker SHA matches if its first 7 chars equal the actual short form. Scoped strictly to the two header lines so narrative SHAs in "Last Completed" prose never false-positive-fire.
@@ -189,9 +235,69 @@ if [ -f "$TRACKER" ]; then
189
235
  fi
190
236
  ```
191
237
 
238
+ **24. P13 — key_facts delta vs ticket atom-count mismatch (added v0.18.3).** When the ticket's Completion Log or MCE quantifies a delta against `key_facts.md` (e.g. `+8 atoms`, `+5 aliases`, `+27 dishes`), the corresponding feature row in `key_facts.md` should record the same delta. Whitespace-safe iteration via `while IFS= read -r`; FEATURE_ID-anchored block scan avoids false-pass on identical deltas elsewhere in the file. English keyword set; Spanish (`átomos`, `platos`) deferred to v0.19.x.
239
+ ```bash
240
+ KEY_FACTS=docs/project_notes/key_facts.md
241
+ if [ -f "$KEY_FACTS" ]; then
242
+ FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
243
+ TICKET_DELTAS=$(grep -oE '\+[0-9]+ (atoms?|aliases?|dishes?|entries|rows)' "$TICKET" | sort -u)
244
+ KEY_FACTS_BLOCK=$(grep -A 3 -F "$FEATURE_ID" "$KEY_FACTS" 2>/dev/null || true)
245
+ while IFS= read -r claim; do
246
+ [ -z "$claim" ] && continue
247
+ if [ -z "$KEY_FACTS_BLOCK" ] || ! echo "$KEY_FACTS_BLOCK" | grep -qF "$claim"; then
248
+ flag "P13 drift: ticket claims '$claim' but key_facts.md block for $FEATURE_ID lacks the same delta"
249
+ fi
250
+ done <<< "$TICKET_DELTAS"
251
+ fi
252
+ ```
253
+
254
+ **25. P14 — MCE Action 1 row stale post-merge (added v0.18.3).** When ticket Status normalizes to `Done` AND the MCE Action 1 row still has `Step 6 [ ]` / `Step 6 [-]`, the row was written pre-merge and not updated post-squash. **Strict scoping**: awk state machine terminates the MCE block at the NEXT `^## ` line of any name (NOT `[^M]` — that incorrectly absorbs subsequent `## M*` sections). **Strict signal**: only `Step 6 [ ]` / `Step 6 [-]` patterns flag; standalone `(this merge)` is omitted because it commonly appears in past-tense narrative ("merged at SHA (this merge)") and produces false positives. Reuses `TICKET_STATUS` defined in P11; do NOT use `$status` from the P5 loop. NIT severity.
255
+ ```bash
256
+ if [ "$TICKET_STATUS" = "Done" ]; then
257
+ MCE_BLOCK=$(awk '
258
+ /^## Merge Checklist Evidence/ { in_mce=1; print; next }
259
+ in_mce && /^## / { in_mce=0 }
260
+ in_mce { print }
261
+ ' "$TICKET")
262
+ if echo "$MCE_BLOCK" | grep -qE 'Step 6 \[[ -]\]'; then
263
+ flag "P14 drift (NIT): MCE Action 1 row contains 'Step 6 [ ]' / 'Step 6 [-]' after merge — flip to [x] and append squash + housekeeping SHAs"
264
+ fi
265
+ fi
266
+ ```
267
+
268
+ **26. P15 — AC with `post-deploy` keyword admitted without Completion Log evidence (added v0.18.3).** ACs containing production-parity keywords (`post-deploy`, `post-merge`, `production parity`, `prod verification`, `on dev API`, `on prod`) are explicit gates; marking them `[x]` without a dated Completion Log row defeats their purpose. Empirical origin: fx F-CATALOG-COV-001 AC-NEW-qa-battery silent-PASS until external audit caught it. Line-safe iteration via `while IFS= read -r`.
269
+ ```bash
270
+ COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
271
+ AC_LINES=$(grep -nE '^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]+AC-[A-Za-z0-9_-]+' "$TICKET" \
272
+ | grep -iE 'post-deploy|post-merge|production parity|prod verification|on dev API|on prod')
273
+ while IFS= read -r line; do
274
+ [ -z "$line" ] && continue
275
+ echo "$line" | grep -q '\[x\]' || continue
276
+ ac_id=$(echo "$line" | grep -oE 'AC-[A-Za-z0-9_-]+' | head -1)
277
+ [ -z "$ac_id" ] && continue
278
+ if ! echo "$COMPLETION" | grep -E "^\|[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}" | grep -qF "$ac_id"; then
279
+ flag "P15 drift: $ac_id marked [x] with post-deploy semantics but no dated Completion Log entry anchoring this AC-ID"
280
+ fi
281
+ done <<< "$AC_LINES"
282
+ ```
283
+
284
+ **27. P16 — Feature missing from Features table (added v0.18.3).** Ticket Status `Ready for Merge` / `Done` should have a row in some `## Features — *` table in `product-tracker.md`. **Strict signal**: requires the feature ID to appear as the first cell of a pipe-table row (`| FEATURE_ID |` with optional whitespace), NOT just anywhere in the tracker — narrative mentions or `**Active Feature:**` references must not silence the drift. NIT severity. Reuses `TICKET_STATUS` and `FEATURE_ID` defined in P11.
285
+ ```bash
286
+ TRACKER=docs/project_notes/product-tracker.md
287
+ case "$TICKET_STATUS" in
288
+ "Ready for Merge"|"Done")
289
+ if [ -f "$TRACKER" ]; then
290
+ if ! grep -qE "^\|[[:space:]]*$FEATURE_ID[[:space:]]*\|" "$TRACKER"; then
291
+ flag "P16 drift (NIT): $FEATURE_ID not in any Features table row (must appear as first cell in '| FEATURE_ID | ... |' form)"
292
+ fi
293
+ fi
294
+ ;;
295
+ esac
296
+ ```
297
+
192
298
  ### Execution discipline (added v0.18.1)
193
299
 
194
- For each of the 12 drift checks (P1–P12), if you declare PASS, **include the literal command output** (or its absence — explicit "no rows matched", "extracted: feature/foo", "FROZEN_COUNT=0") as evidence in your report. A bare "PASS" without supporting output is treated as **NOT EXECUTED** by the auditor — re-run with output captured.
300
+ For each of the 16 drift checks (P1–P16), if you declare PASS, **include the literal command output** (or its absence — explicit "no rows matched", "extracted: feature/foo", "FROZEN_COUNT=0") as evidence in your report. A bare "PASS" without supporting output is treated as **NOT EXECUTED** by the auditor — re-run with output captured.
195
301
 
196
302
  Recommended pattern:
197
303
 
@@ -228,7 +334,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
228
334
 
229
335
  **STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
230
336
 
231
- ### Drift (12-23) — advisory, refresh before user authorization
337
+ ### Drift (12-27) — advisory, refresh before user authorization
232
338
 
233
339
  | # | Pattern | Status | Detail |
234
340
  |---|---------|:------:|--------|
@@ -244,6 +350,10 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
244
350
  | 21 | P10 Duplicate log rows | PASS | no duplicates |
245
351
  | 22 | P11 Tracker status mismatch | PASS | in-progress for Ready for Merge |
246
352
  | 23 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
353
+ | 24 | P13 key_facts delta mismatch | PASS | N/A — no quantified deltas |
354
+ | 25 | P14 MCE Action 1 stale post-merge | PASS | N/A pre-merge / row past-tense |
355
+ | 26 | P15 Post-deploy AC without evidence | PASS | no post-deploy keyword in ACs |
356
+ | 27 | P16 Feature missing from tracker | PASS | feature in Features table |
247
357
 
248
358
  **DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
249
359
 
@@ -263,19 +373,23 @@ Fix them directly:
263
373
  - Merge base diverged → `git merge origin/<target-branch>` and resolve conflicts
264
374
  - Data file issues → fix the data
265
375
 
266
- **Drift advisories (12-22) fixes:**
376
+ **Drift advisories (12-27) fixes:**
267
377
  - **P1 (PR body test count stale)** → edit PR body "Quality Gates" / "npm test" line to match ticket terminal count; add "(+N new tests)" delta note
268
378
  - **P2 (Aspirational Evidence)** → rewrite `[x]` rows with past-tense text + commit SHA + concrete numbers
269
379
  - **P3 (Post-merge action unlogged)** → add a Completion Log row documenting the post-merge execution with date + action + empirical result
270
380
  - **P4 (Remote branch orphan)** → `git push origin --delete <branch>` after confirming merge succeeded
271
381
  - **P5 (Frozen ticket Status)** → update each ticket's `**Status:**` field from "In Progress"/"Ready for Merge" to `Done`; this often belongs in a docs-only tracker-sync PR if the cycle is retroactive
272
- - **P6 (AC count off-by-N)** → recount AC items; update the Merge Checklist Evidence row 1 claim to match actual
382
+ - **P6 (AC count off-by-N)** → recount AC items; update the Merge Checklist Evidence row 1 claim. Use canonical `AC: <marked>/<total>` form (see `merge-checklist.md`) — `total` includes intentionally-deferred ACs
273
383
  - **P7 (Intra-ticket test drift)** → refresh AC / DoD / tracker numbers to match the Completion Log terminal entry
274
384
  - **P8 (Completion Log gap)** → add a Completion Log row per missing Step with agent verdict + commit SHA
275
385
  - **P9 (Tracker header stale)** → update `**Last Updated:**` line step reference to match Active Feature detail
276
386
  - **P10 (Duplicate log rows)** → remove duplicate rows
277
387
  - **P11 (Tracker status mismatch)** → sync tracker Features row status to ticket header Status
278
388
  - **P12 (Tracker HEAD reference stale)** → update `**Last Updated:**` and `**Active Feature:**` `HEAD: <sha>` tokens to match `git rev-parse HEAD`. Use `git rev-parse --short HEAD` for the 7-char form.
389
+ - **P13 (key_facts delta mismatch)** → reconcile: either correct the ticket's claimed delta (`+N atoms`/`+M aliases`) to match the actual key_facts.md row, or update key_facts.md to match the truthful delta
390
+ - **P14 (MCE Action 1 stale post-merge)** → flip Step 6 checkbox to `[x]` and remove the `(this merge)` qualifier; append squash SHA + housekeeping SHA to the Workflow evidence line
391
+ - **P15 (Post-deploy AC without evidence)** → add a dated Completion Log row anchoring the AC-ID with empirical results from the production verification (QA battery output, dev-API smoke result, telemetry confirmation); OR re-mark the AC `[ ]` until verification lands
392
+ - **P16 (Feature missing from tracker)** → add a row to the relevant `## Features — *` table in `product-tracker.md` (or document explicitly in the ticket why standalone is intentional with a tracker-side note)
279
393
 
280
394
  After fixing, re-run the audit to confirm all checks pass.
281
395
 
@@ -72,6 +72,16 @@ In the ticket, fill the `## Merge Checklist Evidence` table. For each action (0
72
72
  | 0. Validate ticket structure | [x] | Sections verified: Spec, Plan, AC, DoD, Workflow, Log, Evidence |
73
73
  | 1. Mark all items | [x] | AC: 12/12, DoD: 7/7, Workflow: 0-5/6 |
74
74
 
75
+ **Canonical form for the AC count claim:** write `AC: <marked>/<total>` — `marked` is the count of `[x]` Acceptance Criteria, `total` is the count of all AC items including any intentionally deferred `[ ]`. When all are checked use the matching form `AC: N/N` (or the shorthand `all N marked`). The `/audit-merge` P6 drift check parses both forms.
76
+
77
+ **Sub-scope ticket naming convention (recognized by `/audit-merge` P11 since v0.18.4):** when a feature is too large to close in one ticket, split it into sub-scope tickets using one of these suffixes on the basename:
78
+
79
+ - `<FEATURE_ID>-lite-<descriptor>.md` — minimal viable closure of a partial scope (e.g. `F116-lite-ci-hardening.md`)
80
+ - `<FEATURE_ID>-FU.md` — single follow-up closing a deferred piece
81
+ - `<FEATURE_ID>-FU<N>.md` — numbered follow-ups (e.g. `F-H7-FU1.md`, `F-H10-FU2.md`)
82
+
83
+ Sub-scope tickets reach `Status: Done` independently while the parent feature's tracker row stays at its parent status (typically `pending` or `in-progress`) until ALL sub-scopes close. `/audit-merge` P11 detects the suffix and emits `P11 N/A` instead of flagging the parent/sub-scope status divergence as drift.
84
+
75
85
  ## Action 9: Run compliance audit
76
86
 
77
87
  Run `/audit-merge` to verify all compliance checks pass automatically. If any check fails, fix it and re-run until all pass.
@@ -52,12 +52,21 @@ Run only if `git diff origin/<target-branch>..HEAD --name-only` shows `.json` fi
52
52
 
53
53
  Eleven empirically-validated drift patterns. Failures are NOT blockers for the compliance verdict, but MUST be refreshed before requesting user authorization. Use BSD-grep-compatible regex (no `\K`).
54
54
 
55
- **12. P1 — PR body test count stale.** Ratio must co-occur with test/pass/green marker to avoid AC/DoD ratios (14/14, 7/7).
55
+ **12. P1 — PR body test count stale (v0.18.3 multi-workspace extension — C1).** All PR-body ratios must appear in ticket evidence. Subset direction: PR ⊆ ticket. Three fallback cases: (a) ratios on both sides → walk each PR ratio; (b)/(c) missing on either side → emit `P1 N/A`.
56
56
  ```bash
57
- PR_BODY=$(gh pr view --json body -q .body)
58
- PR_TESTS=$(echo "$PR_BODY" | grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" | grep -oE "[0-9]+/[0-9]+" | head -1)
59
- TICKET_TESTS=$(grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | tail -1)
60
- [ -n "$PR_TESTS" ] && [ -n "$TICKET_TESTS" ] && [ "$PR_TESTS" != "$TICKET_TESTS" ] && flag "P1 drift: PR body $PR_TESTS vs ticket $TICKET_TESTS"
57
+ TEST_KW_RE='(npm test|pnpm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])'
58
+ PR_BODY=$(gh pr view --json body -q .body 2>/dev/null || true)
59
+ PR_RATIOS=$(echo "$PR_BODY" | grep -iE "$TEST_KW_RE" | grep -oE "[0-9]+/[0-9]+" | sort -u)
60
+ TICKET_RATIOS=$(grep -iE "$TEST_KW_RE" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | sort -u)
61
+ if [ -z "$PR_RATIOS" ] || [ -z "$TICKET_RATIOS" ]; then
62
+ echo "P1 N/A: no comparable test-count ratios extracted (PR=$(echo "$PR_RATIOS" | tr '\n' ',' ), ticket=$(echo "$TICKET_RATIOS" | tr '\n' ',' ))"
63
+ else
64
+ while IFS= read -r r; do
65
+ [ -z "$r" ] && continue
66
+ echo "$TICKET_RATIOS" | grep -qFx "$r" \
67
+ || flag "P1 drift: PR ratio $r not found in ticket evidence (ticket ratios: $(echo "$TICKET_RATIOS" | tr '\n' ' '))"
68
+ done <<< "$PR_RATIOS"
69
+ fi
61
70
  ```
62
71
 
63
72
  **13. P2 — Merge Checklist Evidence aspirational.** `[x]` rows with future-tense text.
@@ -94,6 +103,8 @@ for t in docs/tickets/*.md; do
94
103
  status=$(grep -E "^\*\*Status:\*\*" "$t" | head -1 \
95
104
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
96
105
  | sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
106
+ | sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
107
+ | sed -E 's/\*\*[[:space:]]*$//' \
97
108
  | sed -E 's/[[:space:]]+$//')
98
109
  [ "$status" = "Done" ] && continue
99
110
  ticket_id=$(basename "$t" .md | sed -E 's/-[a-z].*//')
@@ -103,12 +114,27 @@ done
103
114
  [ "$FROZEN_COUNT" -eq 1 ] && flag "P5 drift: 1 frozen ticket"
104
115
  ```
105
116
 
106
- **17. P6 — AC count off-by-N.**
117
+ **17. P6 — AC count off-by-N.** Two claim forms supported: `all N marked` (N = total) and `AC: X/Y done` (X = marked, Y = total — handles deferred ACs where Y > X).
107
118
  ```bash
108
- ACTUAL=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET" | grep -cE "^- \[[x ]\]")
109
- CLAIMED=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1 | grep -oE "[0-9]+" | head -1)
110
- [ -n "$CLAIMED" ] && [ "$CLAIMED" != "$ACTUAL" ] && [ $((ACTUAL - CLAIMED)) -ge 2 -o $((CLAIMED - ACTUAL)) -ge 2 ] \
111
- && flag "P6 drift: claim '$CLAIMED' vs actual AC count $ACTUAL"
119
+ AC_BLOCK=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
120
+ ACTUAL_TOTAL=$(echo "$AC_BLOCK" | grep -cE "^- \[[x ]\]")
121
+ ACTUAL_MARKED=$(echo "$AC_BLOCK" | grep -cE "^- \[x\]")
122
+ CLAIM_LINE=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1)
123
+ if echo "$CLAIM_LINE" | grep -qE '^AC: [0-9]+/[0-9]+'; then
124
+ CLAIMED_MARKED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
125
+ CLAIMED_TOTAL=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | tail -1)
126
+ [ -n "$CLAIMED_TOTAL" ] && [ "$CLAIMED_TOTAL" != "$ACTUAL_TOTAL" ] \
127
+ && [ $((ACTUAL_TOTAL - CLAIMED_TOTAL)) -ge 2 -o $((CLAIMED_TOTAL - ACTUAL_TOTAL)) -ge 2 ] \
128
+ && flag "P6 drift: claim AC total '$CLAIMED_TOTAL' vs actual total $ACTUAL_TOTAL"
129
+ [ -n "$CLAIMED_MARKED" ] && [ "$CLAIMED_MARKED" != "$ACTUAL_MARKED" ] \
130
+ && [ $((ACTUAL_MARKED - CLAIMED_MARKED)) -ge 2 -o $((CLAIMED_MARKED - ACTUAL_MARKED)) -ge 2 ] \
131
+ && flag "P6 drift: claim AC marked '$CLAIMED_MARKED' vs actual marked $ACTUAL_MARKED"
132
+ elif [ -n "$CLAIM_LINE" ]; then
133
+ CLAIMED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
134
+ [ -n "$CLAIMED" ] && [ "$CLAIMED" != "$ACTUAL_TOTAL" ] \
135
+ && [ $((ACTUAL_TOTAL - CLAIMED)) -ge 2 -o $((CLAIMED - ACTUAL_TOTAL)) -ge 2 ] \
136
+ && flag "P6 drift: 'all $CLAIMED marked' vs actual AC count $ACTUAL_TOTAL"
137
+ fi
112
138
  ```
113
139
 
114
140
  **18. P7 — Test count drift within ticket (final-sections only).**
@@ -157,16 +183,36 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
157
183
  TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
158
184
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
159
185
  | sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
186
+ | sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
187
+ | sed -E 's/\*\*[[:space:]]*$//' \
160
188
  | sed -E 's/[[:space:]]+$//')
161
- FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
162
- TRACKER_STATUS=$(grep -F "$FEATURE_ID" docs/project_notes/product-tracker.md | grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
163
- case "$TICKET_STATUS" in
164
- "Ready for Merge"|"Review"|"In Progress"|"Planning"|"Spec") EXPECTED="in-progress" ;;
165
- "Done") EXPECTED="done" ;;
166
- *) EXPECTED="" ;;
189
+ TICKET_BASENAME=$(basename "$TICKET" .md)
190
+ # v0.18.4 P11-B: sub-scope tickets (-lite / -FU / -FU[0-9]*) close a partial
191
+ # scope of a parent feature; the parent tracker row stays at its parent status
192
+ # (typically `pending` or `in-progress`) while the sub-scope ticket reaches
193
+ # `Done`. P11 must NOT enforce status mapping across this boundary. Pattern
194
+ # scope: empirically derived from fx convention (uppercase -FU + -lite). If
195
+ # future projects introduce -spike / -mini / -aux variants, expand here.
196
+ case "$TICKET_BASENAME" in
197
+ *-lite|*-lite-*|*-FU|*-FU-*|*-FU[0-9]*)
198
+ echo "P11 N/A: $TICKET_BASENAME is a sub-scope ticket — parent tracker row status independent" >&2
199
+ ;;
200
+ *)
201
+ FEATURE_ID=$(echo "$TICKET_BASENAME" | sed -E 's/-[a-z].*//')
202
+ # v0.18.4 P11-B drive-by hardening: anchor tracker lookup to pipe-table row
203
+ # (FEATURE_ID as first cell). Mirrors v0.18.3 P16 idiom — prevents narrative
204
+ # mentions earlier in the tracker from silencing or false-firing the check.
205
+ TRACKER_STATUS=$(grep -E "^\|[[:space:]]*$FEATURE_ID[[:space:]]*\|" docs/project_notes/product-tracker.md \
206
+ | grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
207
+ case "$TICKET_STATUS" in
208
+ "Ready for Merge"|"Review"|"In Progress"|"Planning"|"Spec") EXPECTED="in-progress" ;;
209
+ "Done") EXPECTED="done" ;;
210
+ *) EXPECTED="" ;;
211
+ esac
212
+ [ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
213
+ && flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
214
+ ;;
167
215
  esac
168
- [ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
169
- && flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
170
216
  ```
171
217
 
172
218
  **23. P12 — Tracker HEAD references stale (added v0.18.2).** The `**Last Updated:**` and `**Active Feature:**` lines may embed `HEAD <sha>` or `HEAD: <sha>` references that were correct when written but went stale as further commits landed (empirically observed in fx F-WEB-MENU-VISION-001 audit cycle 2026-05-06: tracker said `HEAD: fd752e4` while `git rev-parse HEAD` was `6fa801e` after the agent's own self-edit commit). Compare each extracted SHA against the active branch HEAD. Bidirectional prefix tolerance: a 7-char tracker SHA matches the full 40-char actual HEAD if it's a prefix; a full 40-char tracker SHA matches if its first 7 chars equal the actual short form. Scoped strictly to the two header lines so narrative SHAs in "Last Completed" prose never false-positive-fire.
@@ -189,9 +235,69 @@ if [ -f "$TRACKER" ]; then
189
235
  fi
190
236
  ```
191
237
 
238
+ **24. P13 — key_facts delta vs ticket atom-count mismatch (added v0.18.3).** Whitespace-safe iteration + FEATURE_ID anchoring.
239
+ ```bash
240
+ KEY_FACTS=docs/project_notes/key_facts.md
241
+ if [ -f "$KEY_FACTS" ]; then
242
+ FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
243
+ TICKET_DELTAS=$(grep -oE '\+[0-9]+ (atoms?|aliases?|dishes?|entries|rows)' "$TICKET" | sort -u)
244
+ KEY_FACTS_BLOCK=$(grep -A 3 -F "$FEATURE_ID" "$KEY_FACTS" 2>/dev/null || true)
245
+ while IFS= read -r claim; do
246
+ [ -z "$claim" ] && continue
247
+ if [ -z "$KEY_FACTS_BLOCK" ] || ! echo "$KEY_FACTS_BLOCK" | grep -qF "$claim"; then
248
+ flag "P13 drift: ticket claims '$claim' but key_facts.md block for $FEATURE_ID lacks the same delta"
249
+ fi
250
+ done <<< "$TICKET_DELTAS"
251
+ fi
252
+ ```
253
+
254
+ **25. P14 — MCE Action 1 row stale post-merge (added v0.18.3).** Strict awk state machine terminates MCE block at NEXT `^## ` of any name (not `[^M]`). Strict signal `Step 6 [ ]` / `Step 6 [-]` only — standalone `(this merge)` omitted to prevent false positives on past-tense narrative. Reuses `TICKET_STATUS` from P11. NIT severity.
255
+ ```bash
256
+ if [ "$TICKET_STATUS" = "Done" ]; then
257
+ MCE_BLOCK=$(awk '
258
+ /^## Merge Checklist Evidence/ { in_mce=1; print; next }
259
+ in_mce && /^## / { in_mce=0 }
260
+ in_mce { print }
261
+ ' "$TICKET")
262
+ if echo "$MCE_BLOCK" | grep -qE 'Step 6 \[[ -]\]'; then
263
+ flag "P14 drift (NIT): MCE Action 1 row contains 'Step 6 [ ]' / 'Step 6 [-]' after merge — flip to [x] and append squash + housekeeping SHAs"
264
+ fi
265
+ fi
266
+ ```
267
+
268
+ **26. P15 — AC with post-deploy keyword admitted without Completion Log evidence (added v0.18.3).** Line-safe iteration via `while IFS= read -r`.
269
+ ```bash
270
+ COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
271
+ AC_LINES=$(grep -nE '^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]+AC-[A-Za-z0-9_-]+' "$TICKET" \
272
+ | grep -iE 'post-deploy|post-merge|production parity|prod verification|on dev API|on prod')
273
+ while IFS= read -r line; do
274
+ [ -z "$line" ] && continue
275
+ echo "$line" | grep -q '\[x\]' || continue
276
+ ac_id=$(echo "$line" | grep -oE 'AC-[A-Za-z0-9_-]+' | head -1)
277
+ [ -z "$ac_id" ] && continue
278
+ if ! echo "$COMPLETION" | grep -E "^\|[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}" | grep -qF "$ac_id"; then
279
+ flag "P15 drift: $ac_id marked [x] with post-deploy semantics but no dated Completion Log entry anchoring this AC-ID"
280
+ fi
281
+ done <<< "$AC_LINES"
282
+ ```
283
+
284
+ **27. P16 — Feature missing from Features table (added v0.18.3).** Strict signal: feature ID must appear as first cell of a pipe-table row (`| FEATURE_ID |`) — narrative mentions / `**Active Feature:**` references must not silence the drift. NIT severity. Reuses `TICKET_STATUS` and `FEATURE_ID` from P11.
285
+ ```bash
286
+ TRACKER=docs/project_notes/product-tracker.md
287
+ case "$TICKET_STATUS" in
288
+ "Ready for Merge"|"Done")
289
+ if [ -f "$TRACKER" ]; then
290
+ if ! grep -qE "^\|[[:space:]]*$FEATURE_ID[[:space:]]*\|" "$TRACKER"; then
291
+ flag "P16 drift (NIT): $FEATURE_ID not in any Features table row (must appear as first cell in '| FEATURE_ID | ... |' form)"
292
+ fi
293
+ fi
294
+ ;;
295
+ esac
296
+ ```
297
+
192
298
  ### Execution discipline (added v0.18.1)
193
299
 
194
- For each of the 12 drift checks (P1–P12), if you declare PASS, **include the literal command output** (or its absence — explicit "no rows matched", "extracted: feature/foo", "FROZEN_COUNT=0") as evidence in your report. A bare "PASS" without supporting output is treated as **NOT EXECUTED** by the auditor — re-run with output captured.
300
+ For each of the 16 drift checks (P1–P16), if you declare PASS, **include the literal command output** (or its absence — explicit "no rows matched", "extracted: feature/foo", "FROZEN_COUNT=0") as evidence in your report. A bare "PASS" without supporting output is treated as **NOT EXECUTED** by the auditor — re-run with output captured.
195
301
 
196
302
  Recommended pattern:
197
303
 
@@ -228,7 +334,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
228
334
 
229
335
  **STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
230
336
 
231
- ### Drift (12-23) — advisory, refresh before user authorization
337
+ ### Drift (12-27) — advisory, refresh before user authorization
232
338
 
233
339
  | # | Pattern | Status | Detail |
234
340
  |---|---------|:------:|--------|
@@ -244,6 +350,10 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
244
350
  | 21 | P10 Duplicate log rows | PASS | no duplicates |
245
351
  | 22 | P11 Tracker status mismatch | PASS | status consistent |
246
352
  | 23 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
353
+ | 24 | P13 key_facts delta mismatch | PASS | N/A — no quantified deltas |
354
+ | 25 | P14 MCE Action 1 stale post-merge | PASS | N/A pre-merge / row past-tense |
355
+ | 26 | P15 Post-deploy AC without evidence | PASS | no post-deploy keyword in ACs |
356
+ | 27 | P16 Feature missing from tracker | PASS | feature in Features table |
247
357
 
248
358
  **DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
249
359
 
@@ -263,19 +373,23 @@ Fix them directly:
263
373
  - Merge base diverged → `git merge origin/<target-branch>` and resolve conflicts
264
374
  - Data file issues → fix the data
265
375
 
266
- **Drift advisories (12-22) fixes:**
376
+ **Drift advisories (12-27) fixes:**
267
377
  - **P1** → edit PR body npm test line to match ticket terminal count
268
378
  - **P2** → rewrite `[x]` rows with past-tense + commit SHA
269
379
  - **P3** → add Completion Log row for each post-merge execution
270
380
  - **P4** → `git push origin --delete <branch>` after merge
271
381
  - **P5** → update ticket Status from "In Progress"/"Ready for Merge" to `Done`
272
- - **P6** → recount ACs and update Merge Checklist row 1 claim
382
+ - **P6** → recount ACs; use canonical `AC: <marked>/<total>` form (see `merge-checklist.md`) — `total` includes intentionally-deferred ACs
273
383
  - **P7** → sync AC/DoD/tracker numbers to Completion Log terminal
274
384
  - **P8** → add Completion Log row per missing Step with agent verdict + commit SHA
275
385
  - **P9** → refresh `**Last Updated:**` step reference
276
386
  - **P10** → remove duplicate rows
277
387
  - **P11** → sync tracker Features row status to ticket header Status
278
388
  - **P12 (Tracker HEAD reference stale)** → update `**Last Updated:**` and `**Active Feature:**` `HEAD: <sha>` tokens to match `git rev-parse HEAD`. Use `git rev-parse --short HEAD` for the 7-char form.
389
+ - **P13 (key_facts delta mismatch)** → reconcile ticket delta claim with key_facts.md feature row
390
+ - **P14 (MCE Action 1 stale post-merge)** → flip Step 6 checkbox to `[x]` and remove the `(this merge)` qualifier
391
+ - **P15 (Post-deploy AC without evidence)** → add a dated Completion Log row with empirical post-deploy results, OR re-mark the AC `[ ]` until verification lands
392
+ - **P16 (Feature missing from tracker)** → add a row to the relevant `## Features — *` table in `product-tracker.md`
279
393
 
280
394
  After fixing, re-run the audit to confirm all checks pass.
281
395
 
@@ -72,6 +72,16 @@ In the ticket, fill the `## Merge Checklist Evidence` table. For each action (0
72
72
  | 0. Validate ticket structure | [x] | Sections verified: Spec, Plan, AC, DoD, Workflow, Log, Evidence |
73
73
  | 1. Mark all items | [x] | AC: 12/12, DoD: 7/7, Workflow: 0-5/6 |
74
74
 
75
+ **Canonical form for the AC count claim:** write `AC: <marked>/<total>` — `marked` is the count of `[x]` Acceptance Criteria, `total` is the count of all AC items including any intentionally deferred `[ ]`. When all are checked use the matching form `AC: N/N` (or the shorthand `all N marked`). The `/audit-merge` P6 drift check parses both forms.
76
+
77
+ **Sub-scope ticket naming convention (recognized by `/audit-merge` P11 since v0.18.4):** when a feature is too large to close in one ticket, split it into sub-scope tickets using one of these suffixes on the basename:
78
+
79
+ - `<FEATURE_ID>-lite-<descriptor>.md` — minimal viable closure of a partial scope (e.g. `F116-lite-ci-hardening.md`)
80
+ - `<FEATURE_ID>-FU.md` — single follow-up closing a deferred piece
81
+ - `<FEATURE_ID>-FU<N>.md` — numbered follow-ups (e.g. `F-H7-FU1.md`, `F-H10-FU2.md`)
82
+
83
+ Sub-scope tickets reach `Status: Done` independently while the parent feature's tracker row stays at its parent status (typically `pending` or `in-progress`) until ALL sub-scopes close. `/audit-merge` P11 detects the suffix and emits `P11 N/A` instead of flagging the parent/sub-scope status divergence as drift.
84
+
75
85
  ## Action 9: Run compliance audit
76
86
 
77
87
  Run `/audit-merge` to verify all compliance checks pass automatically. If any check fails, fix it and re-run until all pass.