Skip to content

Commit 87db28e

Browse files
clkaoclaude
andauthored
status --ac-scan: enumerate ACs annotated inside the bold (#447)
Broaden acHeadingRe so an asterisk-free trailing label inside the bold span is allowed and discarded — **AC-1 (VALUE)** enumerates as AC-1, exactly as bare **AC-1**, so the value AC the README ideation policy recommends is no longer silently dropped from the gate AC cross-check. The [^*]* label excludes *, so it cannot span a ** boundary: two headings on one line still enumerate separately. TestACScanEnumeratesAnnotatedAC + annotated-ac-fixture.md: RED on the bare matcher (AC-1 absent), GREEN after; enumerated id set is exactly {AC-1, AC-2} (no over-match, prose 'see AC-3' not enumerated). README validation cross-check prose gains the value-annotation clause. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2ef249e commit 87db28e

4 files changed

Lines changed: 97 additions & 3 deletions

File tree

docs/dev/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ A task moves to validation after implementation is complete. The work here is to
128128
- **Outputs:**
129129
- Run applicable tests from the Testing Resources section and report results.
130130
- Verify each acceptance criterion with evidence.
131-
- Pull every `**AC-N**` item from the entity body's `## Acceptance criteria` section; reproduce the evidence cited in each "Verified by" clause; flag any AC without evidence.
131+
- Pull every `**AC-N**` item (including a value annotation inside the bold, e.g. `**AC-1 (VALUE)**`) from the entity body's `## Acceptance criteria` section; reproduce the evidence cited in each "Verified by" clause; flag any AC without evidence.
132132
- Reproduce each AC's cited evidence; reject any AC whose evidence is self-referential, or whose only deliverable is a decision with nothing shipped (that belongs in the roadmap, not a terminal dev task). Dev-workflow policy: an AC's proof is code, command, or state. A non-development workflow's AC proof may legitimately be a published artifact, a metric, or a human review.
133133
- Check that the task body, acceptance criteria, implementation, and tests reflect the latest captain feedback.
134134
- Reject when tests pass but prove an obsolete, over-specified, or wrong target behavior.

internal/status/cycle3_extract_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,69 @@ func TestACScanScopeIsLoadBearing(t *testing.T) {
557557
}
558558
}
559559

560+
// annotatedACFixturePath returns the committed fixture whose ## Acceptance
561+
// criteria section annotates the value AC inside the bold (**AC-1 (VALUE)**),
562+
// alongside a bare **AC-2** and a prose-only "see AC-3 above" mention — the
563+
// over-match discriminator for the broadened heading matcher.
564+
func annotatedACFixturePath(t *testing.T) string {
565+
t.Helper()
566+
p, err := filepath.Abs(filepath.Join("testdata", "section-reader", "annotated-ac-fixture.md"))
567+
if err != nil {
568+
t.Fatal(err)
569+
}
570+
return p
571+
}
572+
573+
// TestACScanEnumeratesAnnotatedAC (AC-1 + AC-2) asserts --ac-scan enumerates an
574+
// AC whose bold span carries an annotation: **AC-1 (VALUE)** lists with id AC-1,
575+
// exactly as the bare **AC-2** does, so the value AC the README ideation policy
576+
// recommends is no longer silently dropped from the gate cross-check. The
577+
// enumerated id set is EXACTLY {AC-1, AC-2}: AC-1 once, AC-2 once (no
578+
// over-match), AC-3 absent (the prose "see AC-3 above" is not a heading, proving
579+
// the **…** delimiter requirement survived the broadening). RED on the bare
580+
// matcher (AC-1 absent), GREEN after acHeadingRe gains the trailing-label form.
581+
func TestACScanEnumeratesAnnotatedAC(t *testing.T) {
582+
root, err := filepath.Abs(filepath.Join("testdata", "seq-workflow"))
583+
if err != nil {
584+
t.Fatal(err)
585+
}
586+
env := pinnedEnv(t)
587+
fixture := annotatedACFixturePath(t)
588+
589+
out, stderr, code := runNative(t, root, env, "--workflow-dir", root, "--read", fixture, "--stage", "ideation", "--ac-scan", "--json")
590+
if code != 0 {
591+
t.Fatalf("exit=%d stderr=%q", code, stderr)
592+
}
593+
var doc acScanEnvelope
594+
if err := json.Unmarshal([]byte(out), &doc); err != nil {
595+
t.Fatalf("not JSON: %v\n%s", err, out)
596+
}
597+
// Count each enumerated id's occurrences: the set must be exactly {AC-1, AC-2}.
598+
counts := map[string]int{}
599+
for _, ac := range doc.ACs {
600+
counts[ac.ID]++
601+
}
602+
if counts["AC-1"] != 1 {
603+
t.Errorf("AC-1 enumerated %d time(s), want exactly 1 (the annotated value AC must list as AC-1)", counts["AC-1"])
604+
}
605+
if counts["AC-2"] != 1 {
606+
t.Errorf("AC-2 enumerated %d time(s), want exactly 1 (bare AC, no over-match)", counts["AC-2"])
607+
}
608+
if counts["AC-3"] != 0 {
609+
t.Errorf("AC-3 enumerated %d time(s), want 0 (the prose 'see AC-3 above' is not a heading)", counts["AC-3"])
610+
}
611+
if len(doc.ACs) != 2 {
612+
t.Fatalf("enumerated %d ACs, want exactly 2 ({AC-1, AC-2}): %v\n%s", len(doc.ACs), counts, out)
613+
}
614+
// AC-1's evidence flag is exercised: its DONE evidence line cites AC-1, so a
615+
// correctly-enumerated AC-1 reports unevidenced=false (not merely "present").
616+
for _, ac := range doc.ACs {
617+
if ac.ID == "AC-1" && ac.Unevidenced != "false" {
618+
t.Errorf("AC-1 unevidenced = %q, want \"false\" (cited in the ideation DONE evidence)", ac.Unevidenced)
619+
}
620+
}
621+
}
622+
560623
// TestGateModeLoudFailures (AC-5) asserts the gate modes fail loudly with a
561624
// non-zero exit and a named diagnostic, never a partial/silent emit: missing
562625
// --stage, a --stage matching no report (no silent positional-last fallback),

internal/status/gate_extract.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,13 @@ var checklistBulletRe = regexp.MustCompile(`^-\s+(DONE|SKIPPED|FAILED):\s*(.*)$`
4848

4949
// acHeadingRe matches an `**AC-N**` acceptance-criteria heading and captures the
5050
// AC id. The id is tokenized on the heading boundary (AC- followed by an
51-
// alphanumeric run), never split on `-` (the spike's AC-id boundary finding).
52-
var acHeadingRe = regexp.MustCompile(`\*\*(AC-[0-9A-Za-z]+)\*\*`)
51+
// alphanumeric run), never split on `-` (the spike's AC-id boundary finding). An
52+
// asterisk-free trailing label inside the bold span is allowed and discarded, so
53+
// `**AC-1 (VALUE)**` enumerates as `AC-1` exactly as bare `**AC-1**` — the value
54+
// AC the README ideation policy recommends is no longer dropped. The `[^*]*`
55+
// label excludes `*`, so it cannot span a `**` boundary: two headings on one line
56+
// (`**AC-1** … **AC-2**`) still enumerate separately and never merge.
57+
var acHeadingRe = regexp.MustCompile(`\*\*(AC-[0-9A-Za-z]+)[^*]*\*\*`)
5358

5459
// acTokenRe matches an AC-N token anywhere in a line, for citation scanning.
5560
var acTokenRe = regexp.MustCompile(`\bAC-[0-9A-Za-z]+\b`)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
id: annotated-ac-fixture-id
3+
title: annotated acceptance-criteria fixture
4+
status: ideation
5+
---
6+
7+
Intro prose owned by no section.
8+
9+
## Acceptance criteria
10+
11+
- **AC-1 (VALUE)** the value criterion, with its annotation INSIDE the bold span.
12+
13+
(see AC-3 above for the prose-only control — a bare mention, never a heading.)
14+
15+
- **AC-2** the bare second criterion, no annotation.
16+
17+
## Stage Report: ideation
18+
19+
- DONE: prove the value criterion
20+
AC-1 evidenced here in the ideation DONE line
21+
- SKIPPED: the live drive
22+
deferred to a later member
23+
24+
### Summary
25+
26+
Ideation summary prose; AC-3 is named only in passing, never as a heading.

0 commit comments

Comments
 (0)