From 05189caf4c9ad93a5d2d76ca64936de54915f3fe Mon Sep 17 00:00:00 2001 From: Craig Truitt Date: Wed, 14 Oct 2026 10:06:58 -0500 Subject: [PATCH 1/3] test(conflict-guards): pin the recursive declared-conflicts check The depth-2 declaration-model agreement area capped its reference resolver at one nested segment and built its event stream from a top-level node to its immediate child only, so a regression that flattened the declared-conflict check back to two levels would pass the suite unnoticed. Widen the resolver to the recursive canonical reference grammar `NNNN-slug(/MMMM-slug)*`, replace the hardcoded top-level loops with a recursive walk that descends into a child holding a manifest, and add the additive `tests/fixtures/recursive-conflicts/` tree three levels deep. New `AC23 recursive-*` assertions pin the reference-vs-path discriminator, sibling-first resolution below the root, recursion into a decomposing child, the own union parent-cell union and its drift discrepancy, the cross-level pair predicate, shipped-counterpart exclusion, and malformed/unresolved reporting. The shipped depth-2 fixture and its `AC23` blocks stay unchanged as controls. Refs: work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard --- tests/checks/85-conflict-guards.sh | 368 +++++++++++++++++- .../items/0002-grand/spec.md | 14 + .../items/0009-solo/spec.md | 12 + .../0001-nested/0001-leaf/.gitkeep | 0 .../0001-nested/0002-grand/design.md | 15 + .../0001-nested/0003-shipped/ship.md | 13 + .../0001-nested/0004-refs/design.md | 15 + .../0001-nested/0005-precedence/design.md | 14 + .../0001-nested/0006-bad/design.md | 15 + .../items/9201-root-a/0001-nested/manifest.md | 47 +++ .../items/9201-root-a/0002-sibling/.gitkeep | 0 .../9201-root-a/0003-legacy/0001-old/.gitkeep | 0 .../items/9201-root-a/0003-legacy/manifest.md | 35 ++ .../items/9201-root-a/manifest.md | 43 ++ .../items/9202-root-b/0001-namer/.gitkeep | 0 .../items/9202-root-b/0002-grand/.gitkeep | 0 .../items/9202-root-b/manifest.md | 37 ++ .../items/9203-shared/.gitkeep | 0 .../items/9210-surface-shared/design.md | 13 + .../items/9210-surface-shared/spec.md | 13 + .../items/9211-names-shared/design.md | 13 + .../items/9211-names-shared/spec.md | 13 + 22 files changed, 669 insertions(+), 11 deletions(-) create mode 100644 tests/fixtures/recursive-conflicts/items/0002-grand/spec.md create mode 100644 tests/fixtures/recursive-conflicts/items/0009-solo/spec.md create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0001-leaf/.gitkeep create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0002-grand/design.md create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0003-shipped/ship.md create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0004-refs/design.md create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0005-precedence/design.md create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0006-bad/design.md create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/manifest.md create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0002-sibling/.gitkeep create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0003-legacy/0001-old/.gitkeep create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/0003-legacy/manifest.md create mode 100644 tests/fixtures/recursive-conflicts/items/9201-root-a/manifest.md create mode 100644 tests/fixtures/recursive-conflicts/items/9202-root-b/0001-namer/.gitkeep create mode 100644 tests/fixtures/recursive-conflicts/items/9202-root-b/0002-grand/.gitkeep create mode 100644 tests/fixtures/recursive-conflicts/items/9202-root-b/manifest.md create mode 100644 tests/fixtures/recursive-conflicts/items/9203-shared/.gitkeep create mode 100644 tests/fixtures/recursive-conflicts/items/9210-surface-shared/design.md create mode 100644 tests/fixtures/recursive-conflicts/items/9210-surface-shared/spec.md create mode 100644 tests/fixtures/recursive-conflicts/items/9211-names-shared/design.md create mode 100644 tests/fixtures/recursive-conflicts/items/9211-names-shared/spec.md diff --git a/tests/checks/85-conflict-guards.sh b/tests/checks/85-conflict-guards.sh index 10044c6..03f37e2 100644 --- a/tests/checks/85-conflict-guards.sh +++ b/tests/checks/85-conflict-guards.sh @@ -12,8 +12,11 @@ # command, is invoked by no framework command, and never reads live work/**. # Item references are resolved inside the fixture-local tree # tests/fixtures/declared-conflicts/items/, which shadows the live work/ root; -# surface paths resolve against the repository copy root. The suite is read-only -# and fresh-clone-clean. +# surface paths resolve against the repository copy root. A second, additive +# fixture root tests/fixtures/recursive-conflicts/items/ carries a depth-≥3 tree +# with a decomposing child so the recursive grammar and the recursion into that +# child's own Children rows are pinned too. The suite is read-only and +# fresh-clone-clean. # # It also asserts the shipped reporting vocabulary (finding grammar, codes, and # classes) on the live authority and the operative prompts, so a dropped code, @@ -26,12 +29,18 @@ # tests/run.sh, so it must not call exit. Prints ok/FAIL lines labelled AC23 plus # a sub-area, so a mutation self-check (tests/mutation.sh) can name the guard it # trips: column-layout, positional-parse, cell-resolution, pair-predicate, -# reporting-vocabulary. +# reporting-vocabulary, and the depth-≥3 recursive sub-areas +# recursive-fixture-existence, recursive-read-only-contract, +# recursive-column-layout, recursive-traversal, recursive-reference, +# recursive-sibling-precedence, recursive-union, recursive-drift, +# recursive-pair, recursive-shipped, recursive-unresolved. echo "== AC23 declared-conflict declaration model and check guards ==" GUARD_ROOT="tests/fixtures/declared-conflicts" GUARD_ITEMS="$GUARD_ROOT/items" +RC_ROOT="tests/fixtures/recursive-conflicts" # additive depth-≥3 fixture root +RC_ITEMS="$RC_ROOT/items" # its fixture-local shadow root GUARD_CANON_HEADER='| Local id | Title | Scope | Depends on | conflicts-with | Canonical reference |' GUARD_LEGACY_HEADER='| Local id | Title | Scope | Depends on | Canonical reference |' @@ -170,7 +179,7 @@ guard_malformed() { # $1 token, $2 entity ref, $3 local id # "unresolved|". $2 is the parent ref (empty for standalone), $3 the local id, # $4 the parent's parsed Children rows. guard_resolve() { - if printf '%s' "$1" | grep -qE '^[0-9]{4}-[a-z0-9-]+(/[0-9]{4}-[a-z0-9-]+)?$'; then + if printf '%s' "$1" | grep -qE '^[0-9]{4}-[a-z0-9-]+(/[0-9]{4}-[a-z0-9-]+)*$'; then case "$1" in */*) if [ -d "$GUARD_ITEMS/$1" ]; then printf 'item|%s\n' "$1"; else printf 'unresolved|\n'; fi @@ -281,22 +290,40 @@ T|$ref|resolved|$rkind|$rid|$ctoken" done <<< "$cand" } +# Recursively emit the entities of one decomposing node: every immediate child +# directory is an entity, and a child that itself holds a manifest recurses so +# its own Children rows enter the compared set at arbitrary depth, keyed by their +# full canonical reference. A decomposing child is therefore both an entity (from +# its parent's row) and the source of further entities. +guard_walk_node() { # $1 node-ref, $2 node-dir, $3 node-rows + local node_ref="$1" node_dir="$2" node_rows="$3" + local cd lname cman child_rows + for cd in "$node_dir"/*/; do + [ -d "$cd" ] || continue + lname="${cd%/}"; lname="${lname##*/}" + case "$lname" in [0-9][0-9][0-9][0-9]-*) : ;; *) continue ;; esac + guard_emit_entity "$node_ref/$lname" "$cd" "$node_ref" "$lname" "$node_rows" + if cman="$(guard_manifest "$cd")"; then + child_rows="$(guard_children_rows "$cman")" + guard_walk_node "$node_ref/$lname" "$cd" "$child_rows" + fi + done +} + guard_build_events() { GUARD_EVENTS="" - local pd pname pman rows cd lname sd sname + local pd pname pman rows sd sname + # Top level: a directory holding a manifest is a container walked recursively + # (never emitted as an entity itself). for pd in "$GUARD_ITEMS"/*/; do [ -d "$pd" ] || continue pname="${pd%/}"; pname="${pname##*/}" case "$pname" in [0-9][0-9][0-9][0-9]-*) : ;; *) continue ;; esac pman="$(guard_manifest "$pd")" || continue rows="$(guard_children_rows "$pman")" - for cd in "$pd"/*/; do - [ -d "$cd" ] || continue - lname="${cd%/}"; lname="${lname##*/}" - case "$lname" in [0-9][0-9][0-9][0-9]-*) : ;; *) continue ;; esac - guard_emit_entity "$pname/$lname" "$cd" "$pname" "$lname" "$rows" - done + guard_walk_node "$pname" "$pd" "$rows" done + # Top level: a directory with spec.md and no manifest is a standalone entity. for sd in "$GUARD_ITEMS"/*/; do [ -d "$sd" ] || continue sname="${sd%/}"; sname="${sname##*/}" @@ -307,6 +334,15 @@ guard_build_events() { done } +# Build the event stream for one items root, saving and restoring the active +# root so both fixtures are produced by the same code and analyzers. +guard_collect() { # $1 = items root + local saved="$GUARD_ITEMS" + GUARD_ITEMS="$1" + guard_build_events + GUARD_ITEMS="$saved" +} + # --- analyzers -------------------------------------------------------------- # guard_emit_entity appends to GUARD_EVENTS from the current shell (its token # loop is fed by a here-string, not a pipe), so the analyzers below see the @@ -324,6 +360,13 @@ fixture_drift() { printf '%s\n' "$GUARD_EVENTS" | awk -F'|' '$1=="D"{ print $2 }' | LC_ALL=C sort -u } +# The compared-set entity stream: "|" per entity, +# sorted by canonical reference (a plain path order, so a node sorts before its +# own children), so the recursive fixture's traversal can be asserted directly. +fixture_entities() { + printf '%s\n' "$GUARD_EVENTS" | awk -F'|' '$1=="E"{ print $2"|"$3 }' | LC_ALL=C sort -t'|' -k1,1 +} + fixture_conflicts() { printf '%s\n' "$GUARD_EVENTS" | awk -F'|' ' $1 == "E" { shipped[$2] = $3; ents[$2] = 1; next } @@ -587,6 +630,309 @@ else guard_show_diff "(empty)" "$guard_empty_shipped" fi +# --- AC23 recursive-* : depth-≥3 declared-conflict guard -------------------- +# +# The additive fixture root tests/fixtures/recursive-conflicts/items/ carries a +# depth-≥3 tree with a decomposing child. Each recursive rule gets its own +# labelled assertion so a single targeted mutation names the rule it trips. + +guard_collect "$RC_ITEMS" + +RC_ENTITIES="$(fixture_entities)" +RC_RESOLUTION="$(fixture_resolution)" +RC_UNRESOLVED="$(fixture_unresolved)" +RC_DRIFT="$(fixture_drift)" +RC_CONFLICTS="$(fixture_conflicts)" + +# --- AC23 recursive-fixture-existence --------------------------------------- + +if [ -d "$RC_ITEMS" ]; then + ok "AC23 recursive-fixture-existence: recursive-conflicts fixture tree exists" +else + bad "AC23 recursive-fixture-existence: recursive-conflicts fixture tree missing at $RC_ITEMS" +fi + +for rc_file in \ + "$RC_ITEMS/9201-root-a/manifest.md" \ + "$RC_ITEMS/9201-root-a/0001-nested/manifest.md" \ + "$RC_ITEMS/9201-root-a/0003-legacy/manifest.md" \ + "$RC_ITEMS/9201-root-a/0001-nested/0003-shipped/ship.md" \ + "$RC_ITEMS/9210-surface-shared/design.md" \ + "$RC_ITEMS/9211-names-shared/design.md"; do + if [ -f "$rc_file" ]; then + ok "AC23 recursive-fixture-existence: $rc_file exists" + else + bad "AC23 recursive-fixture-existence: $rc_file missing" + fi +done + +# --- AC23 recursive-read-only-contract -------------------------------------- + +if [ -d "$RC_ITEMS" ] && ! grep -rq 'work/' "$RC_ROOT" 2>/dev/null; then + ok "AC23 recursive-read-only-contract: recursive fixture references no live work/ path" +else + bad "AC23 recursive-read-only-contract: recursive fixture references a live work/ path" +fi + +# --- AC23 recursive-column-layout (AC9) ------------------------------------- + +if [ -f "$RC_ITEMS/9201-root-a/manifest.md" ]; then + need "$RC_ITEMS/9201-root-a/manifest.md" "$GUARD_CANON_HEADER" \ + "AC23 recursive-column-layout: 9201-root-a has the 6-column Children header" + need "$RC_ITEMS/9201-root-a/0001-nested/manifest.md" "$GUARD_CANON_HEADER" \ + "AC23 recursive-column-layout: the depth-2 decomposing child has the 6-column Children header" + need "$RC_ITEMS/9201-root-a/0003-legacy/manifest.md" "$GUARD_LEGACY_HEADER" \ + "AC23 recursive-column-layout: the depth-2 legacy manifest has the 5-column Children header" +else + bad "AC23 recursive-column-layout: recursive fixture manifest missing" +fi + +rc_hdr_line="" +if [ -f "$RC_ITEMS/9201-root-a/0001-nested/manifest.md" ]; then + rc_hdr_line="$(grep -F -- "$GUARD_CANON_HEADER" "$RC_ITEMS/9201-root-a/0001-nested/manifest.md" | head -n1)" +fi +rc_parse="$(printf '%s' "$rc_hdr_line" | awk -F'|' '{ gsub(/^[ \t]+|[ \t]+$/, "", $5); gsub(/^[ \t]+|[ \t]+$/, "", $6); print $5 "|" $6 }')" +if [ "$rc_parse" = "Depends on|conflicts-with" ]; then + ok "AC23 recursive-column-layout: depth-2 header places Depends on at pipe-field 5 and conflicts-with field 6" +else + bad "AC23 recursive-column-layout: depth-2 header does not place Depends on at pipe-field 5 (got '$rc_parse')" +fi + +guard_contains "$RC_ENTITIES" '9201-root-a/0003-legacy/0001-old|0' \ + "AC23 recursive-column-layout: a 5-column legacy child is an entity from its parent row" +if printf '%s\n' "$RC_RESOLUTION" | grep -q '^9201-root-a/0003-legacy/0001-old|'; then + bad "AC23 recursive-column-layout: a 5-column legacy row declared a target" +else + ok "AC23 recursive-column-layout: a 5-column legacy row declares nothing" +fi + +# --- AC23 recursive-traversal (AC3) ----------------------------------------- + +EXP_RC_ENTITIES="$(cat <<'EOF' +0002-grand|0 +0009-solo|0 +9201-root-a/0001-nested|0 +9201-root-a/0001-nested/0001-leaf|0 +9201-root-a/0001-nested/0002-grand|0 +9201-root-a/0001-nested/0003-shipped|1 +9201-root-a/0001-nested/0004-refs|0 +9201-root-a/0001-nested/0005-precedence|0 +9201-root-a/0001-nested/0006-bad|0 +9201-root-a/0002-sibling|0 +9201-root-a/0003-legacy|0 +9201-root-a/0003-legacy/0001-old|0 +9202-root-b/0001-namer|0 +9202-root-b/0002-grand|0 +9210-surface-shared|0 +9211-names-shared|0 +EOF +)" + +if [ "$RC_ENTITIES" = "$EXP_RC_ENTITIES" ]; then + ok "AC23 recursive-traversal: compared-set entity stream matches the expected block" +else + bad "AC23 recursive-traversal: compared-set entity stream diverged from the expected block" + guard_show_diff "$EXP_RC_ENTITIES" "$RC_ENTITIES" +fi + +# The decomposing child is both an entity (from its parent's row) and the source +# of further entities keyed by full canonical reference. +guard_contains "$RC_ENTITIES" '9201-root-a/0001-nested|0' \ + "AC23 recursive-traversal: the decomposing child is an entity from its parent row" +guard_contains "$RC_ENTITIES" '9201-root-a/0001-nested/0001-leaf|0' \ + "AC23 recursive-traversal: the decomposing child's own row is an entity at depth 3" + +# --- AC23 recursive-reference (AC1) ----------------------------------------- + +rc_refs_resolution="$(printf '%s\n' "$RC_RESOLUTION" | awk -F'|' '$1=="9201-root-a/0001-nested/0004-refs"{ print }')" +EXP_RC_REFS="$(cat <<'EOF' +9201-root-a/0001-nested/0004-refs|9201-root-a/0001-nested/0001-leaf|resolved|item|9201-root-a/0001-nested/0001-leaf +9201-root-a/0001-nested/0004-refs|9201-root-a/0001-nested/0003-shipped|resolved|item|9201-root-a/0001-nested/0003-shipped +9201-root-a/0001-nested/0004-refs|tests/fixtures|resolved|surface|tests/fixtures +EOF +)" + +if [ "$rc_refs_resolution" = "$EXP_RC_REFS" ]; then + ok "AC23 recursive-reference: the depth-3 reference block matches the expected resolution" +else + bad "AC23 recursive-reference: the depth-3 reference block diverged from the expected resolution" + guard_show_diff "$EXP_RC_REFS" "$rc_refs_resolution" +fi + +# A multi-segment canonical reference is an item, never a surface path; a token +# outside the recursive grammar is classified as a surface path. +guard_contains "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0004-refs|9201-root-a/0001-nested/0001-leaf|resolved|item|9201-root-a/0001-nested/0001-leaf' \ + "AC23 recursive-reference: a three-segment reference resolves as an item" +guard_contains "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0004-refs|tests/fixtures|resolved|surface|tests/fixtures' \ + "AC23 recursive-reference: a non-reference token resolves as a surface path" + +# --- AC23 recursive-sibling-precedence (AC2) -------------------------------- + +rc_prec_resolution="$(printf '%s\n' "$RC_RESOLUTION" | awk -F'|' '$1=="9201-root-a/0001-nested/0005-precedence"{ print }')" +EXP_RC_PREC="$(cat <<'EOF' +9201-root-a/0001-nested/0005-precedence|0002-grand|resolved|item|9201-root-a/0001-nested/0002-grand +9201-root-a/0001-nested/0005-precedence|0009-solo|resolved|item|0009-solo +9201-root-a/0001-nested/0005-precedence|9203-shared|resolved|item|9203-shared +EOF +)" + +if [ "$rc_prec_resolution" = "$EXP_RC_PREC" ]; then + ok "AC23 recursive-sibling-precedence: the below-root resolution block matches the expected resolution" +else + bad "AC23 recursive-sibling-precedence: the below-root resolution block diverged from the expected resolution" + guard_show_diff "$EXP_RC_PREC" "$rc_prec_resolution" +fi + +guard_contains "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0005-precedence|0002-grand|resolved|item|9201-root-a/0001-nested/0002-grand' \ + "AC23 recursive-sibling-precedence: a bare MMMM-slug resolves to its sibling row first below the root" +guard_absent "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0005-precedence|0002-grand|resolved|item|0002-grand' \ + "AC23 recursive-sibling-precedence: the sibling row wins over the same-named top-level decoy" +guard_contains "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0005-precedence|0009-solo|resolved|item|0009-solo' \ + "AC23 recursive-sibling-precedence: a bare token matching no sibling row falls to the root fallback" + +# --- AC23 recursive-union (AC4) --------------------------------------------- + +rc_grand_resolution="$(printf '%s\n' "$RC_RESOLUTION" | awk -F'|' '$1=="9201-root-a/0001-nested/0002-grand"{ print }')" +EXP_RC_GRAND="$(cat <<'EOF' +9201-root-a/0001-nested/0002-grand|docs/customization.md|resolved|surface|docs/customization.md +9201-root-a/0001-nested/0002-grand|docs/workflow.md|resolved|surface|docs/workflow.md +EOF +)" + +if [ "$rc_grand_resolution" = "$EXP_RC_GRAND" ]; then + ok "AC23 recursive-union: the own ∪ parent-cell union matches the expected resolution" +else + bad "AC23 recursive-union: the own ∪ parent-cell union diverged from the expected resolution" + guard_show_diff "$EXP_RC_GRAND" "$rc_grand_resolution" +fi + +guard_contains "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0002-grand|docs/workflow.md|resolved|surface|docs/workflow.md' \ + "AC23 recursive-union: the union includes the child's own declaration" +guard_contains "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0002-grand|docs/customization.md|resolved|surface|docs/customization.md' \ + "AC23 recursive-union: the union includes the parent cell's declaration" +# A one-sided record (only the parent cell) still declares its own targets. +guard_contains "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0003-shipped|docs/workflow.md|resolved|surface|docs/workflow.md' \ + "AC23 recursive-union: a one-sided parent cell declares its target" + +# --- AC23 recursive-drift (AC5) --------------------------------------------- + +EXP_RC_DRIFT="$(cat <<'EOF' +9201-root-a/0001-nested/0002-grand +EOF +)" + +if [ "$RC_DRIFT" = "$EXP_RC_DRIFT" ]; then + ok "AC23 recursive-drift: the drift set matches the expected block" +else + bad "AC23 recursive-drift: the drift set diverged from the expected block" + guard_show_diff "$EXP_RC_DRIFT" "$RC_DRIFT" +fi + +guard_contains "$RC_DRIFT" '9201-root-a/0001-nested/0002-grand' \ + "AC23 recursive-drift: a both-present-unequal declaration at depth is reported" +guard_absent "$RC_DRIFT" '9201-root-a/0001-nested/0003-shipped' \ + "AC23 recursive-drift: a one-sided record is not reported as a discrepancy" + +# Edge (spec): both-present-and-equal at depth — an own declaration equal to its +# parent cell declares the union with no discrepancy. The committed fixture only +# carries the unequal case, so this drives guard_emit_entity directly with a +# synthetic parent-cell value (the real design.md is read; nothing is written) +# and asserts the equal set emits no D event while the unequal set still does. +guard_saved_events2="$GUARD_EVENTS" +GUARD_EVENTS="" +guard_emit_entity "9201-root-a/0001-nested/0004-refs" \ + "$RC_ITEMS/9201-root-a/0001-nested/0004-refs" "9201-root-a/0001-nested" "0004-refs" \ + "0004-refs|—|9201-root-a/0001-nested/0001-leaf, tests/fixtures, 9201-root-a/0001-nested/0003-shipped" +guard_equal_drift="$(fixture_drift)" +GUARD_EVENTS="" +guard_emit_entity "9201-root-a/0001-nested/0004-refs" \ + "$RC_ITEMS/9201-root-a/0001-nested/0004-refs" "9201-root-a/0001-nested" "0004-refs" \ + "0004-refs|—|tests/fixtures" +guard_unequal_drift="$(fixture_drift)" +GUARD_EVENTS="$guard_saved_events2" +if [ -z "$guard_equal_drift" ] && + [ "$guard_unequal_drift" = "9201-root-a/0001-nested/0004-refs" ]; then + ok "AC23 recursive-drift: a both-present-equal declaration is a no-drift union" +else + bad "AC23 recursive-drift: a both-present-equal declaration was mismatched (equal drift='$guard_equal_drift', unequal drift='$guard_unequal_drift')" +fi + +# --- AC23 recursive-pair (AC6) ---------------------------------------------- + +EXP_RC_CONFLICTS="$(cat <<'EOF' +0009-solo|9201-root-a/0001-nested/0005-precedence|(b) +9201-root-a/0001-nested/0001-leaf|9201-root-a/0001-nested/0004-refs|(b) +9201-root-a/0001-nested/0002-grand|9201-root-a/0001-nested/0005-precedence|(b) +9201-root-a/0001-nested/0002-grand|9210-surface-shared|(a) +9201-root-a/0001-nested/0005-precedence|9211-names-shared|(b) +9202-root-b/0001-namer|9202-root-b/0002-grand|(b) +EOF +)" + +if [ "$RC_CONFLICTS" = "$EXP_RC_CONFLICTS" ]; then + ok "AC23 recursive-pair: cross-level declared conflict pairs match the expected block" +else + bad "AC23 recursive-pair: cross-level declared conflict pairs diverged from the expected block" + guard_show_diff "$EXP_RC_CONFLICTS" "$RC_CONFLICTS" +fi + +guard_contains "$RC_CONFLICTS" \ + '9201-root-a/0001-nested/0002-grand|9210-surface-shared|(a)' \ + "AC23 recursive-pair: a cross-level shared surface target is a class (a) conflict" +guard_contains "$RC_CONFLICTS" \ + '9201-root-a/0001-nested/0005-precedence|9211-names-shared|(b)' \ + "AC23 recursive-pair: a cross-level shared item target is a class (b) conflict" +guard_contains "$RC_CONFLICTS" \ + '9201-root-a/0001-nested/0001-leaf|9201-root-a/0001-nested/0004-refs|(b)' \ + "AC23 recursive-pair: one-sided naming of an otherwise silent counterpart is a conflict" +guard_absent "$RC_CONFLICTS" \ + '9201-root-a/0001-nested/0005-precedence|9202-root-b/0001-namer' \ + "AC23 recursive-pair: equal text resolving to different items is not a shared target" + +# --- AC23 recursive-shipped (AC7) ------------------------------------------- + +guard_contains "$RC_RESOLUTION" \ + '9201-root-a/0001-nested/0004-refs|9201-root-a/0001-nested/0003-shipped|resolved|item|9201-root-a/0001-nested/0003-shipped' \ + "AC23 recursive-shipped: a shipped target still resolves for the declaring entity" +guard_absent "$RC_CONFLICTS" '9201-root-a/0001-nested/0003-shipped' \ + "AC23 recursive-shipped: a shipped entity is excluded as a conflict counterpart" +guard_contains "$RC_ENTITIES" '9201-root-a/0001-nested/0003-shipped|1' \ + "AC23 recursive-shipped: the shipped entity carries the shipped signal" + +# --- AC23 recursive-unresolved (AC8) ---------------------------------------- + +EXP_RC_UNRESOLVED="$(cat <<'EOF' +9201-root-a/0001-nested/0006-bad| +9201-root-a/0001-nested/0006-bad|../escape +9201-root-a/0001-nested/0006-bad|/abs.md +9201-root-a/0001-nested/0006-bad|9201-root-a/0001-nested/0006-bad +9201-root-a/0001-nested/0006-bad|9999-nope +9201-root-a/0001-nested/0006-bad|docs/*.md +9201-root-a/0001-nested/0006-bad|docs/no-such-file.md +EOF +)" + +if [ "$RC_UNRESOLVED" = "$EXP_RC_UNRESOLVED" ]; then + ok "AC23 recursive-unresolved: the malformed/unresolved block matches the expected block" +else + bad "AC23 recursive-unresolved: the malformed/unresolved block diverged from the expected block" + guard_show_diff "$EXP_RC_UNRESOLVED" "$RC_UNRESOLVED" +fi + +for rc_bad in '' '../escape' '/abs.md' 'docs/*.md' '9999-nope' \ + 'docs/no-such-file.md' '9201-root-a/0001-nested/0006-bad'; do + guard_contains "$RC_UNRESOLVED" "9201-root-a/0001-nested/0006-bad|$rc_bad" \ + "AC23 recursive-unresolved: the malformed/unresolved token '$rc_bad' is reported, not dropped" +done + # --- AC23 reporting-vocabulary (AC8) ---------------------------------------- GUARD_AUTH="$(awk ' diff --git a/tests/fixtures/recursive-conflicts/items/0002-grand/spec.md b/tests/fixtures/recursive-conflicts/items/0002-grand/spec.md new file mode 100644 index 0000000..727e41d --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/0002-grand/spec.md @@ -0,0 +1,14 @@ +--- +feature: 0002-grand +phase: spec +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Fixture — top-level `0002-grand` decoy + +A committed test fixture, not a real work item. It is a top-level item whose name +collides with a bare `MMMM-slug` token used below the root, so the guard can prove +sibling-first resolution prefers the depth-2 parent's row over this top-level +decoy. diff --git a/tests/fixtures/recursive-conflicts/items/0009-solo/spec.md b/tests/fixtures/recursive-conflicts/items/0009-solo/spec.md new file mode 100644 index 0000000..57c82ba --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/0009-solo/spec.md @@ -0,0 +1,12 @@ +--- +feature: 0009-solo +phase: spec +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Fixture — top-level `0009-solo` root fallback + +A committed test fixture, not a real work item. It is the root-fallback target for +a bare `MMMM-slug` below the root that matches no sibling row. diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0001-leaf/.gitkeep b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0001-leaf/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0002-grand/design.md b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0002-grand/design.md new file mode 100644 index 0000000..4da6ab1 --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0002-grand/design.md @@ -0,0 +1,15 @@ +--- +feature: 9201-root-a/0001-nested/0002-grand +phase: design +status: final +created: 2026-10-14 +updated: 2026-10-14 +conflicts-with: "docs/workflow.md" +--- + +# Design — Fixture drift/union child + +A committed test fixture. Its own `conflicts-with` value is `docs/workflow.md` +while its parent row declares `docs/customization.md`; both are present and +unequal, so the declared set is their union and the guard reports a declaration +discrepancy. diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0003-shipped/ship.md b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0003-shipped/ship.md new file mode 100644 index 0000000..c20e051 --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0003-shipped/ship.md @@ -0,0 +1,13 @@ +--- +feature: 9201-root-a/0001-nested/0003-shipped +phase: ship +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Ship record — Fixture shipped child + +A committed test fixture, not a real work item. Its presence is the shipped +signal: the guard proves a shipped entity still resolves as a target but is +excluded as a conflict counterpart. diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0004-refs/design.md b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0004-refs/design.md new file mode 100644 index 0000000..0d11ad9 --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0004-refs/design.md @@ -0,0 +1,15 @@ +--- +feature: 9201-root-a/0001-nested/0004-refs +phase: design +status: final +created: 2026-10-14 +updated: 2026-10-14 +conflicts-with: "9201-root-a/0001-nested/0001-leaf, tests/fixtures, 9201-root-a/0001-nested/0003-shipped" +--- + +# Design — Fixture recursive-reference child + +A committed test fixture. Its targets pin the recursive reference grammar: a +three-segment canonical reference resolves to the fixture item at that path, a +non-reference token (`tests/fixtures`) resolves as a surface path, and a shipped +item's reference still resolves even though it is excluded as a counterpart. diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0005-precedence/design.md b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0005-precedence/design.md new file mode 100644 index 0000000..e3c01bf --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0005-precedence/design.md @@ -0,0 +1,14 @@ +--- +feature: 9201-root-a/0001-nested/0005-precedence +phase: design +status: final +created: 2026-10-14 +updated: 2026-10-14 +conflicts-with: "0002-grand, 0009-solo, 9203-shared" +--- + +# Design — Fixture sibling-first child + +A committed test fixture. `0002-grand` names a sibling row of its immediate +parent and so resolves to that sibling, not the same-named top-level item; +`0009-solo` and `9203-shared` match no sibling row and fall to the root fallback. diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0006-bad/design.md b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0006-bad/design.md new file mode 100644 index 0000000..28ee27f --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/0006-bad/design.md @@ -0,0 +1,15 @@ +--- +feature: 9201-root-a/0001-nested/0006-bad +phase: design +status: final +created: 2026-10-14 +updated: 2026-10-14 +conflicts-with: "../escape, /abs.md, docs/*.md, 9999-nope, docs/no-such-file.md, 9201-root-a/0001-nested/0006-bad, " +--- + +# Design — Fixture malformed and unresolved targets + +A committed test fixture. Every target is unresolved and reported: `../escape` +traverses, `/abs.md` is absolute, `docs/*.md` is a glob, `9999-nope` resolves to +no sibling row and no item, `docs/no-such-file.md` resolves to no repository +path, the self-reference is malformed, and the trailing empty token is blank. diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/manifest.md b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/manifest.md new file mode 100644 index 0000000..4866184 --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9201-root-a/0001-nested/manifest.md @@ -0,0 +1,47 @@ +--- +feature: 9201-root-a/0001-nested +phase: manifest +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Manifest — Recursive fixture nested decomposing child + +## Initiative + +A committed test fixture, not a real work item. This depth-2 directory itself +holds a manifest, so it is both an entity (from `9201-root-a`'s row) and the +source of further entities keyed by their full canonical reference. Its rows +exercise sibling-first resolution below the root, the recursive reference +grammar, the own ∪ parent-cell union and its drift, shipped-counterpart +exclusion, and malformed/unresolved reporting. + +## Assumptions + +- The fixture is read-only committed test data; it is never shipped and never + executed by `/status`. + +## Children + +| Local id | Title | Scope | Depends on | conflicts-with | Canonical reference | +| -------- | ----- | ----- | ---------- | -------------- | ------------------- | +| 0001-leaf | Silent leaf | fixture | — | — | 9201-root-a/0001-nested/0001-leaf | +| 0002-grand | Drift/union child | fixture | — | docs/customization.md | 9201-root-a/0001-nested/0002-grand | +| 0003-shipped | Shipped child | fixture | — | docs/workflow.md | 9201-root-a/0001-nested/0003-shipped | +| 0004-refs | Recursive-reference child | fixture | — | — | 9201-root-a/0001-nested/0004-refs | +| 0005-precedence | Sibling-first child | fixture | — | — | 9201-root-a/0001-nested/0005-precedence | +| 0006-bad | Malformed-target child | fixture | — | — | 9201-root-a/0001-nested/0006-bad | + +## Sequencing + +1. 0001-leaf +2. 0002-grand +3. 0003-shipped +4. 0004-refs +5. 0005-precedence +6. 0006-bad + +## Open issues + +- None. The fixture is deliberate, acyclic test data. diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0002-sibling/.gitkeep b/tests/fixtures/recursive-conflicts/items/9201-root-a/0002-sibling/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0003-legacy/0001-old/.gitkeep b/tests/fixtures/recursive-conflicts/items/9201-root-a/0003-legacy/0001-old/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/0003-legacy/manifest.md b/tests/fixtures/recursive-conflicts/items/9201-root-a/0003-legacy/manifest.md new file mode 100644 index 0000000..c7a8789 --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9201-root-a/0003-legacy/manifest.md @@ -0,0 +1,35 @@ +--- +feature: 9201-root-a/0003-legacy +phase: manifest +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Manifest — Recursive fixture legacy (5-column) nested child + +## Initiative + +A committed test fixture, not a real work item. It is a depth-2 decomposing child +that carries the legacy five-column `Children` header (no `conflicts-with` +column), so the guard can prove an absent column is treated as no declarations at +depth, not only at the top level, while the child itself remains an entity. + +## Assumptions + +- The fixture is read-only committed test data; it is never shipped and never + executed by `/status`. + +## Children + +| Local id | Title | Scope | Depends on | Canonical reference | +| -------- | ----- | ----- | ---------- | ------------------- | +| 0001-old | Legacy leaf | fixture | — | 9201-root-a/0003-legacy/0001-old | + +## Sequencing + +1. 0001-old + +## Open issues + +- None. The fixture is deliberate, acyclic test data. diff --git a/tests/fixtures/recursive-conflicts/items/9201-root-a/manifest.md b/tests/fixtures/recursive-conflicts/items/9201-root-a/manifest.md new file mode 100644 index 0000000..a95ae39 --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9201-root-a/manifest.md @@ -0,0 +1,43 @@ +--- +feature: 9201-root-a +phase: manifest +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Manifest — Recursive declared-conflict fixture root A + +## Initiative + +A committed test fixture, not a real work item. Its tree is three levels deep +(`9201-root-a/0001-nested/...`) so the declared-conflict guard in +`tests/checks/85-conflict-guards.sh` (`AC23 recursive-*`) can pin the recursive +`conflicts-with` target grammar and reference-vs-path discriminator, the +sibling-first-then-root resolution precedence below the root, the own ∪ +parent-cell union and its drift at depth, the pair predicate across levels, and +malformed/unresolved reporting. It lives under `tests/`, is read-only, and +allocates no real sequence number. + +## Assumptions + +- The fixture is read-only committed test data; it is never shipped and never + executed by `/status`. + +## Children + +| Local id | Title | Scope | Depends on | conflicts-with | Canonical reference | +| -------- | ----- | ----- | ---------- | -------------- | ------------------- | +| 0001-nested | Nested decomposing child | fixture | — | — | 9201-root-a/0001-nested | +| 0002-sibling | Silent sibling | fixture | — | — | 9201-root-a/0002-sibling | +| 0003-legacy | Legacy 5-column decomposing child | fixture | — | — | 9201-root-a/0003-legacy | + +## Sequencing + +1. 0001-nested +2. 0002-sibling +3. 0003-legacy + +## Open issues + +- None. The fixture is deliberate, acyclic test data. diff --git a/tests/fixtures/recursive-conflicts/items/9202-root-b/0001-namer/.gitkeep b/tests/fixtures/recursive-conflicts/items/9202-root-b/0001-namer/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/recursive-conflicts/items/9202-root-b/0002-grand/.gitkeep b/tests/fixtures/recursive-conflicts/items/9202-root-b/0002-grand/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/recursive-conflicts/items/9202-root-b/manifest.md b/tests/fixtures/recursive-conflicts/items/9202-root-b/manifest.md new file mode 100644 index 0000000..2a2aa1f --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9202-root-b/manifest.md @@ -0,0 +1,37 @@ +--- +feature: 9202-root-b +phase: manifest +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Manifest — Recursive declared-conflict fixture root B + +## Initiative + +A committed test fixture, not a real work item. It is the cross-parent control for +the recursive declared-conflict guard: its bare `0002-grand` token and the one in +`9201-root-a/0001-nested` share the same text but resolve to different items, so +they must not be reported as a shared target. + +## Assumptions + +- The fixture is read-only committed test data; it is never shipped and never + executed by `/status`. + +## Children + +| Local id | Title | Scope | Depends on | conflicts-with | Canonical reference | +| -------- | ----- | ----- | ---------- | -------------- | ------------------- | +| 0002-grand | Silent grand | fixture | — | — | 9202-root-b/0002-grand | +| 0001-namer | Namer of a silent grand | fixture | — | 0002-grand | 9202-root-b/0001-namer | + +## Sequencing + +1. 0002-grand +2. 0001-namer + +## Open issues + +- None. The fixture is deliberate, acyclic test data. diff --git a/tests/fixtures/recursive-conflicts/items/9203-shared/.gitkeep b/tests/fixtures/recursive-conflicts/items/9203-shared/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/recursive-conflicts/items/9210-surface-shared/design.md b/tests/fixtures/recursive-conflicts/items/9210-surface-shared/design.md new file mode 100644 index 0000000..613c13c --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9210-surface-shared/design.md @@ -0,0 +1,13 @@ +--- +feature: 9210-surface-shared +phase: design +status: final +created: 2026-10-14 +updated: 2026-10-14 +conflicts-with: "docs/workflow.md" +--- + +# Design — Fixture shared surface target partner + +A committed test fixture declaring the same surface path as the depth-3 +drift/union child. diff --git a/tests/fixtures/recursive-conflicts/items/9210-surface-shared/spec.md b/tests/fixtures/recursive-conflicts/items/9210-surface-shared/spec.md new file mode 100644 index 0000000..2a5a808 --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9210-surface-shared/spec.md @@ -0,0 +1,13 @@ +--- +feature: 9210-surface-shared +phase: spec +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Fixture — shared surface target partner + +A committed test fixture, not a real work item. It shares the surface path +`docs/workflow.md` with a depth-3 entity, so the guard reports a class (a) +declared conflict across levels. diff --git a/tests/fixtures/recursive-conflicts/items/9211-names-shared/design.md b/tests/fixtures/recursive-conflicts/items/9211-names-shared/design.md new file mode 100644 index 0000000..c3d2ad7 --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9211-names-shared/design.md @@ -0,0 +1,13 @@ +--- +feature: 9211-names-shared +phase: design +status: final +created: 2026-10-14 +updated: 2026-10-14 +conflicts-with: "9203-shared" +--- + +# Design — Fixture shared item target partner + +A committed test fixture declaring the same resolved item as the depth-3 +sibling-first child. diff --git a/tests/fixtures/recursive-conflicts/items/9211-names-shared/spec.md b/tests/fixtures/recursive-conflicts/items/9211-names-shared/spec.md new file mode 100644 index 0000000..b568fbf --- /dev/null +++ b/tests/fixtures/recursive-conflicts/items/9211-names-shared/spec.md @@ -0,0 +1,13 @@ +--- +feature: 9211-names-shared +phase: spec +status: final +created: 2026-10-14 +updated: 2026-10-14 +--- + +# Fixture — shared item target partner + +A committed test fixture, not a real work item. It shares the resolved item target +`9203-shared` with a depth-3 entity, so the guard reports a class (b) declared +conflict across levels. From 1758e1246702108306514327a7c341c9e341ea98 Mon Sep 17 00:00:00 2001 From: Craig Truitt Date: Wed, 14 Oct 2026 10:07:09 -0500 Subject: [PATCH 2/3] docs(work): record build, verification, and review for 0015-recursive-workflow-guards/0003-recursive-conflicts-guard Tick the nine completed tasks, add verify.md recording the green suite (551 passed, 0 failed), the read-only fresh-clone run, and the AC12 mutation spot check, and add review.md recording the approving verdict with no surviving finding. Refs: work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard --- .../0003-recursive-conflicts-guard/review.md | 121 +++++++++++++++++ .../0003-recursive-conflicts-guard/tasks.md | 18 +-- .../0003-recursive-conflicts-guard/verify.md | 126 ++++++++++++++++++ 3 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/review.md create mode 100644 work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/verify.md diff --git a/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/review.md b/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/review.md new file mode 100644 index 0000000..d9fd9b4 --- /dev/null +++ b/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/review.md @@ -0,0 +1,121 @@ +--- +feature: 0015-recursive-workflow-guards/0003-recursive-conflicts-guard +phase: review +status: final +created: 2026-10-14 +updated: 2026-10-14 +parent: 0015-recursive-workflow-guards +--- + +# Review — Recursive declared-conflicts guard + +## Verdict + +**approve** — every acceptance criterion is met by a committed, mutation-sensitive +assertion; the diff widens the shipped resolver and event builder exactly as the +design specifies, adds only the additive fixture root, and leaves the depth-2 +control untouched. No finding of any severity survives. + +## Diff reviewed + +The item's plan (`spec.md`, `design.md`, `tasks.md`) is already committed on +origin/main at the plan commit, so the build diff is the uncommitted working tree +on top of `HEAD`: + +- `git merge-base HEAD origin/main` → `8d1a35ebd57b968df6f66c8a5219f1bcc53ff053` +- `git rev-parse HEAD origin/main` → `8d1a35e` (branch tip) / `5bf6dfe` (main) +- `git diff HEAD --stat` → `tests/checks/85-conflict-guards.sh` (+368/-20), + `work/…0003…/tasks.md` (9 boxes ticked) +- `git status --short -- tests/ work/` → untracked `tests/fixtures/recursive-conflicts/` + and `work/…0003…/verify.md` + +`HEAD` (`8d1a35e`) is the item's own plan commit and an ancestor of origin/main; +reviewing against origin/main directly would show spurious deletions of the +sibling `0001` artifacts that main carries and this branch does not, so the +correct base is `HEAD`. The new fixture root is untracked and must be `git add`ed +by `/ship`. + +## Acceptance criteria + +| AC | Result | Evidence | +| -- | ------ | -------- | +| AC1 recursive reference grammar + ref-vs-path | **met** | `guard_resolve` regex widened to `(/…)*` at `tests/checks/85-conflict-guards.sh:182`; `AC23 recursive-reference` asserts the 3-segment `9201-root-a/0001-nested/0001-leaf` resolves as `item` and `tests/fixtures` as `surface` (`:745-769`). | +| AC2 sibling-first below root, then root fallback | **met** | `AC23 recursive-sibling-precedence` asserts `0002-grand` → the sibling `9201-root-a/0001-nested/0002-grand` (not the top-level decoy) and `0009-solo`/`9203-shared` → root fallback (`:771-796`). | +| AC3 recursion into a decomposing child | **met** | `guard_walk_node` recurses when a child dir holds a manifest (`:298-311`); `AC23 recursive-traversal` pins the full entity set including `9201-root-a/0001-nested` and its own depth-3 rows (`:709-743`). | +| AC4 own ∪ parent-cell union | **met** | `AC23 recursive-union` pins both `docs/workflow.md` (own) and `docs/customization.md` (cell) for `0002-grand`, plus the one-sided `0003-shipped` cell (`:798-823`). | +| AC5 drift discrepancy; one-sided and equal not drift | **met** | `AC23 recursive-drift` pins the drift set and adds the both-present-equal synthetic no-drift case (`:825-866`). | +| AC6 pair predicate across levels | **met** | `AC23 recursive-pair` pins the six-pair block, the `(a)` cross-level surface pair, the `(b)` cross-level item pair, one-sided naming, and the equal-text/different-target absence (`:868-898`). | +| AC7 shipped resolves but is excluded | **met** | `AC23 recursive-shipped` asserts the `…/0003-shipped` target still resolves, is absent from the conflict block, and carries the shipped flag (`:900-908`). | +| AC8 malformed/unresolved reporting | **met** | `AC23 recursive-unresolved` pins the seven-token block and asserts each token is reported, none dropped (`:910-934`). | +| AC9 legacy 5-column header at depth | **met** | `AC23 recursive-column-layout` pins the 5-column header on `9201-root-a/0003-legacy`, the depth-2 positional parse, and `0001-old` declaring nothing while remaining an entity (`:677-707`). | +| AC10 depth-2 control unchanged | **met** | The only removed lines are the header comment, the `?`-capped regex, and the replaced non-recursive loop (`git diff HEAD`); every `EXP_*` block and `AC23` assertion is byte-identical, and `declared-conflicts` has no nested manifest to change the stream. | +| AC11 fresh clone, read-only, no live `work/` | **met** | `$RC_ROOT` grep is clean (I confirmed no `work/` literal under `tests/fixtures/recursive-conflicts/`), the guard is fixture-scoped, and `verify.md` records a no-`work/**` suite pass. | +| AC12 one targeted change trips the matching label | **met** | Every rule has a distinct `AC23 recursive-*` label; each block is a full-equality or contains/absent assertion tied to one rule; the tester's scratch mutation table shows each label independently trippable. Permanent jobs are `0005`'s deliverable, as the spec mandates. | + +## Findings + +### Blockers + +None. + +### Major + +None. + +### Minor + +None. + +### Nits + +None. + +## Adversarial checks performed + +- **Resolver widening is behavior-preserving at depth ≤2.** No `conflicts-with` + value in `tests/fixtures/declared-conflicts/` contains a 3-segment reference + (all are 1- or 2-segment), so the `?`→`*` change cannot reclassify any existing + token; the depth-2 `ACT_*` blocks are unchanged. +- **Every expected block is self-consistent with the implementation.** I traced + the walk/resolve/union/drift/pair pipeline over the whole fixture by hand: + the 16-entity set, the three resolution sub-blocks, the single drift line, the + seven unresolved lines, and the six conflict pairs all match the committed + constants, including LC_ALL=C sort order. +- **The recursion is genuinely pinned, not vacuously.** Removing the recursion + loses all depth-3 entities → `recursive-traversal` full-block equality fails; + reverting the regex sends the 3-segment target to the surface check (path + absent) → `recursive-reference` fails. +- **Shipped exclusion is meaningful.** `0003-shipped` declares `docs/workflow.md`, + which `0002-grand` and `9210-surface-shared` also declare, so including it as a + counterpart would add two `(a)` pairs — the assertion would catch that. +- **Fixtures are pure data.** No symlinks, no CRLF, no `roadmap.md` that would + trip `80-cycle-fixture.sh:66` (`find tests/fixtures -name roadmap.md`), and the + entity ids allocate no real `work/` number. +- **Scope.** Only `tests/checks/85-conflict-guards.sh`, the new fixture root, and + the item's own `tasks.md` changed; no production/authority surface + (`docs/**`, `.opencode/**`, `README.md`, `AGENTS.md`, `tests/README.md`, + `tests/mutation.sh`) was touched, matching the spec's non-goals. +- **Bash 3.2 / portability.** New code uses only `local`, here-strings already + present in the file, `awk`/`grep`/`sort` — no new runtime dependency. + +## Not reviewed + +- **The suite was not executed in this environment.** My bash allowlist denies + arbitrary program execution (`bash tests/run.sh` is blocked here), so I could + not independently reproduce `verify.md`'s `551 passed` / `97 passed` figures. I + validated the guard statically against the fixture and could not find a + divergence; if the reviewer-of-record can run `bash tests/run.sh`, that is the + one remaining independent check. +- **`bash tests/mutation.sh` is expected to fail until `0005` lands.** The + mutation harness stages only the three pre-existing fixture roots + (`tests/mutation.sh:119-121`), so its whole-suite step does not see + `tests/fixtures/recursive-conflicts/` and the new `recursive-fixture-existence` + assertions `bad` on the copy. This is not a defect of this item: the spec's + non-goals explicitly assign the `tests/mutation.sh` staged-copy entry and + mutation jobs to `0005-guard-registration-sweep`, the parent manifest does the + same (`manifest.md:99-102,127`), and `design.md` records the transient window. + `bash tests/run.sh` — the canonical, CI-run command — is unaffected. Flagged so + the human confirms `0005` is merged in the same roadmap; it is not a finding + against this diff. +- The live `/conflicts`, `/status`, and `/build`-gate model runs remain a + documented manual residual, unchanged from the shipped depth-2 area. diff --git a/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/tasks.md b/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/tasks.md index 600b992..6d9af49 100644 --- a/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/tasks.md +++ b/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/tasks.md @@ -15,7 +15,7 @@ Ordered, dependency-aware. One task ≈ one focused commit. Every task edits onl not touch `tests/README.md`, `tests/mutation.sh`, `tests/fixtures/declared-conflicts/`, or any production/authority surface — those are out of scope (`spec.md:50-68`). -- [ ] **T1** — Widen `guard_resolve` in `tests/checks/85-conflict-guards.sh` to the +- [x] **T1** — Widen `guard_resolve` in `tests/checks/85-conflict-guards.sh` to the recursive reference grammar `^[0-9]{4}-[a-z0-9-]+(/[0-9]{4}-[a-z0-9-]+)*$` (the trailing group is `?` today), keeping the root-relative reference branch, the sibling-row-first bare-token branch, and the surface fallback @@ -25,7 +25,7 @@ or any production/authority surface — those are out of scope (`spec.md:50-68`) the shipped `AC23 column-layout`/`positional-parse`/`cell-resolution`/ `pair-predicate` blocks are unchanged. -- [ ] **T2** — Make the event build recursive and parameterized: add +- [x] **T2** — Make the event build recursive and parameterized: add `guard_walk_node(node_ref, node_dir, node_rows)` replacing the two hardcoded top-level loops in `guard_build_events`, recursing into a child directory that holds `manifest.md` or `roadmap.md`; add @@ -36,7 +36,7 @@ or any production/authority surface — those are out of scope (`spec.md:50-68`) the `declared-conflicts` fixture (no decomposing child) produces an unchanged event stream, so all shipped `AC23` blocks still match. -- [ ] **T3** — Author the committed fixture root `tests/fixtures/recursive-conflicts/` +- [x] **T3** — Author the committed fixture root `tests/fixtures/recursive-conflicts/` exactly as specified in `design.md` → "Fixture": the seven top-level entries, the 6-column `9201-root-a` and `9201-root-a/0001-nested` manifests, the 5-column legacy `9201-root-a/0003-legacy` manifest, the 6-column @@ -48,7 +48,7 @@ or any production/authority surface — those are out of scope (`spec.md:50-68`) Glob/Read tools); `grep -r 'work/' tests/fixtures/recursive-conflicts` returns no match. -- [ ] **T4** — Add the structural recursive assertions: `AC23 recursive-fixture-existence` +- [x] **T4** — Add the structural recursive assertions: `AC23 recursive-fixture-existence` (the root and its key files exist; a missing fixture `bad`s, never skips), `AC23 recursive-read-only-contract` (no live `work/` path under `$RC_ROOT`), and `AC23 recursive-column-layout` (the 6-column header on `9201-root-a` and @@ -59,7 +59,7 @@ or any production/authority surface — those are out of scope (`spec.md:50-68`) temporarily renaming `tests/fixtures/recursive-conflicts` makes the area exit non-zero naming `recursive-fixture-existence` (restore immediately). -- [ ] **T5** — Add `AC23 recursive-traversal`, `AC23 recursive-reference`, and +- [x] **T5** — Add `AC23 recursive-traversal`, `AC23 recursive-reference`, and `AC23 recursive-sibling-precedence`, using the expected blocks in `design.md` → "Expected recursive event blocks" and the analyzers `fixture_entities` / `fixture_resolution` (filtered by owner ref). @@ -67,13 +67,13 @@ or any production/authority surface — those are out of scope (`spec.md:50-68`) Verify: `FRAMEWORK_TEST_AREAS=85-conflict-guards bash tests/run.sh` exits 0 with all three labels `ok`. -- [ ] **T6** — Add `AC23 recursive-union` (the expected two-line resolution subset +- [x] **T6** — Add `AC23 recursive-union` (the expected two-line resolution subset for `9201-root-a/0001-nested/0002-grand`) and `AC23 recursive-drift` (the expected `fixture_drift` block). [AC4] [AC5] [depends: T4] Verify: `FRAMEWORK_TEST_AREAS=85-conflict-guards bash tests/run.sh` exits 0 with both labels `ok`. -- [ ] **T7** — Add `AC23 recursive-pair` (the expected six-line `fixture_conflicts` +- [x] **T7** — Add `AC23 recursive-pair` (the expected six-line `fixture_conflicts` block) with the absence assertions that no pair names `9201-root-a/0001-nested/0003-shipped` and that `9201-root-a/0001-nested/0005-precedence` is not paired with @@ -85,7 +85,7 @@ or any production/authority surface — those are out of scope (`spec.md:50-68`) adding an unshipped copy makes the pair block diverge and fail (restore immediately). -- [ ] **T8** — Add `AC23 recursive-unresolved` with the expected +- [x] **T8** — Add `AC23 recursive-unresolved` with the expected `fixture_unresolved` block for `0006-bad`, asserting every malformed and unresolved token is reported and none is dropped. [AC8] [depends: T4] Verify: `FRAMEWORK_TEST_AREAS=85-conflict-guards bash tests/run.sh` exits 0; @@ -93,7 +93,7 @@ or any production/authority surface — those are out of scope (`spec.md:50-68`) `9201-root-a/0001-nested/0006-bad/design.md` makes the block diverge and fail (restore immediately). -- [ ] **T9** — Run the full suite and the mutation-sensitivity spot check. +- [x] **T9** — Run the full suite and the mutation-sensitivity spot check. [AC10] [AC12] [depends: T5] [depends: T6] [depends: T7] [depends: T8] Verify: `bash tests/run.sh` exits 0 with every area green (AC10). On a scratch copy of the repo, for each of the eight labels diff --git a/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/verify.md b/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/verify.md new file mode 100644 index 0000000..c643bfd --- /dev/null +++ b/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/verify.md @@ -0,0 +1,126 @@ +--- +feature: 0015-recursive-workflow-guards/0003-recursive-conflicts-guard +phase: test +status: final +created: 2026-10-14 +updated: 2026-10-14 +parent: 0015-recursive-workflow-guards +--- + +# Verification — Recursive declared-conflicts guard + +## Commands run + +| Command | Result | Notes | +| ------- | ------ | ----- | +| `bash tests/run.sh` | PASS | `TOTAL: 551 passed, 0 failed, 0 skipped` (full suite; includes the 1 assertion added by this pass). | +| `FRAMEWORK_TEST_AREAS=85-conflict-guards bash tests/run.sh` | PASS | `TOTAL: 97 passed, 0 failed, 0 skipped`; all 11 `AC23 recursive-*` labels `ok`. | +| `bash tests/run.sh /tmp/opencode/mut` (fresh clone with **no** `work/**`) | PASS | `TOTAL: 551 passed, 0 failed, 0 skipped` — proves the area reads no live `work/` path. | +| `grep -rn 'work/' tests/fixtures/recursive-conflicts/` | no match | The recursive fixture references no live `work/` path. | +| `git diff tests/checks/85-conflict-guards.sh` removals audit | PASS | The only removed lines are the header comment, the old `?`-capped regex, and the replaced non-recursive inner loop. No depth-2 `EXP_*` block or depth-2 assertion was removed or altered. | +| 11 targeted mutations on a scratch copy (AC12) | PASS | Each recursive label is independently trippable; see "Mutation spot-check (AC12)". | + +Baseline was established before editing (full suite `550 passed, 0 failed, 0 skipped`). +The implementation diff under test: `tests/checks/85-conflict-guards.sh` (resolver, +recursive walk, analyzers, new assertions) plus the new committed fixture root +`tests/fixtures/recursive-conflicts/`. `tasks.md` T1–T9 are all `[x]`. + +## Acceptance coverage + +The guard *is* the test (design → "Test strategy"). Each criterion is pinned by a +committed assertion in `tests/checks/85-conflict-guards.sh`, run by +`bash tests/run.sh`. + +| Criterion | Test(s) | Result | +| --------- | ------- | ------ | +| AC1 recursive reference grammar + ref-vs-path | `AC23 recursive-reference: the depth-3 reference block matches the expected resolution`; `... a three-segment reference resolves as an item`; `... a non-reference token resolves as a surface path` | PASS | +| AC2 sibling-first, then root fallback, below the root | `AC23 recursive-sibling-precedence: the below-root resolution block matches the expected resolution`; `... a bare MMMM-slug resolves to its sibling row first below the root`; `... the sibling row wins over the same-named top-level decoy`; `... a bare token matching no sibling row falls to the root fallback` | PASS | +| AC3 recursion into a decomposing child | `AC23 recursive-traversal: compared-set entity stream matches the expected block`; `... the decomposing child is an entity from its parent row`; `... the decomposing child's own row is an entity at depth 3` | PASS | +| AC4 own ∪ parent-cell union | `AC23 recursive-union: the own ∪ parent-cell union matches the expected resolution`; `... the union includes the child's own declaration`; `... the union includes the parent cell's declaration`; `... a one-sided parent cell declares its target` | PASS | +| AC5 drift discrepancy; one-sided is not drift | `AC23 recursive-drift: the drift set matches the expected block`; `... a both-present-unequal declaration at depth is reported`; `... a one-sided record is not reported as a discrepancy`; **added:** `... a both-present-equal declaration is a no-drift union` | PASS | +| AC6 pair predicate across levels | `AC23 recursive-pair: cross-level declared conflict pairs match the expected block`; `... a cross-level shared surface target is a class (a) conflict`; `... a cross-level shared item target is a class (b) conflict`; `... one-sided naming of an otherwise silent counterpart is a conflict`; `... equal text resolving to different items is not a shared target` | PASS | +| AC7 shipped resolves but is excluded | `AC23 recursive-shipped: a shipped target still resolves for the declaring entity`; `... a shipped entity is excluded as a conflict counterpart`; `... the shipped entity carries the shipped signal` | PASS | +| AC8 malformed/unresolved reporting | `AC23 recursive-unresolved: the malformed/unresolved block matches the expected block` + 7 per-token `... reported, not dropped` assertions (blank, `../escape`, `/abs.md`, `docs/*.md`, `9999-nope`, `docs/no-such-file.md`, self-reference) | PASS | +| AC9 legacy 5-column header at depth | `AC23 recursive-column-layout: the depth-2 legacy manifest has the 5-column Children header`; `... depth-2 header places Depends on at pipe-field 5 and conflicts-with field 6`; `... a 5-column legacy child is an entity from its parent row`; `... a 5-column legacy row declares nothing` | PASS | +| AC10 depth-2 control unchanged | Existing `AC23 column-layout` / `positional-parse` / `cell-resolution` / `pair-predicate` / `reporting-vocabulary` blocks present, unedited (removals audit above), and green in the full run | PASS | +| AC11 fixture existence + read-only, fresh clone | `AC23 recursive-fixture-existence` (7 files); `AC23 recursive-read-only-contract`; fresh-clone run with no `work/**` | PASS | +| AC12 one targeted change trips the matching label | Mutation spot-check below (11 labels) | PASS (scratch); permanent jobs owned by `0005` | + +### Edge cases + +| Edge case | Coverage | Result | +| --------- | -------- | ------ | +| Empty compared set at depth | `AC23 pair-predicate: an empty compared set with no declarations yields no findings` (depth-2 synthetic stream; the `fixture_conflicts` analyzer is depth-agnostic, so nesting adds no distinct behavior) | PASS | +| All-shipped compared set | `AC23 pair-predicate: an all-shipped compared set yields no findings` (same analyzer) | PASS | +| `.gitkeep`-only nested child remains a counterpart | Entity set + pairs: `0001-leaf` and `9203-shared` are named targets with no declaration of their own (`recursive-traversal` / `recursive-pair`) | PASS | +| Equal text, different targets | `AC23 recursive-pair: equal text resolving to different items is not a shared target` (`0002-grand` in `9201-root-a/0001-nested` vs `9202-root-b/0001-namer`) | PASS | +| Both-present-and-equal | **Added test:** `AC23 recursive-drift: a both-present-equal declaration is a no-drift union` (mutation-sensitive; fails when the equal case emits drift) | PASS | +| Reference-vs-path ambiguity | `AC23 recursive-reference` item vs surface classification | PASS | +| Legacy header at depth | `AC23 recursive-column-layout` (`9201-root-a/0003-legacy`) | PASS | +| Depth boundary (`.gitkeep` leaf) | `9201-root-a/0001-nested/0001-leaf` is an entity with no declaration (`recursive-traversal`) | PASS | +| No live `work/**` | `AC23 recursive-read-only-contract` + `grep` + fresh-clone run | PASS | + +## Mutation spot-check (AC12) + +Independent targeted mutations on a scratch copy (no `work/`), each run with +`FRAMEWORK_TEST_AREAS=85-conflict-guards bash tests/run.sh `. Every label's +own mutation produced a `FAIL` line naming that label (`exit=1`). + +| Label | Targeted change | FAILed own label | +| ----- | --------------- | ---------------- | +| `recursive-reference` | reverted the resolver regex to the one-`/`-segment `?` form | yes | +| `recursive-sibling-precedence` | resolved root-first instead of sibling-row-first | yes | +| `recursive-traversal` | removed the recursion into a decomposing child | yes | +| `recursive-union` | dropped the parent-cell candidate contribution | yes | +| `recursive-drift` | disabled the drift condition | yes | +| `recursive-pair` | removed the one-sided item-naming clause | yes | +| `recursive-shipped` | included shipped entities as counterparts | yes | +| `recursive-unresolved` | disabled structural malformation detection | yes | +| `recursive-fixture-existence` | moved the fixture root aside | yes | +| `recursive-read-only-contract` | injected a `work/` literal into a fixture file | yes | +| `recursive-column-layout` | changed the depth-2 header to the 5-column form | yes | + +The mutations also tripped the depth-2 control labels when they shared logic +(e.g. the resolver), which is expected: the depth-2 control and the recursive +area share one resolver and one analyzer set, exactly as the spec requires. + +## Changes made by this pass + +- `tests/checks/85-conflict-guards.sh` — added one focused, read-only assertion + (`AC23 recursive-drift: a both-present-equal declaration is a no-drift union`). + It drives `guard_emit_entity` with a synthetic parent-cell value against the + committed `0004-refs` `design.md` and asserts the equal set emits no `D` event + while an unequal set still does. No fixture file, expected block, analyzer, or + production surface was modified. Verified mutation-sensitive: forcing the + emitter to always report drift makes the assertion `FAIL`. + +## Gaps and residual risk + +- **Live model runs are not executable in CI.** The actual `/conflicts`, + `/status`, and `/build`-gate prompts are not run by the suite; this residual is + unchanged from the shipped depth-2 area. Manual step: in an interactive session + run `/conflicts 0015-recursive-workflow-guards/0003-recursive-conflicts-guard` + and `/status 0015-recursive-workflow-guards/0003-recursive-conflicts-guard` and + confirm the findings/readiness rendered for the fixture-shaped declarations. +- **Permanent mutation jobs and `tests/README.md` registration are deferred.** + `tests/mutation.sh` does not yet stage `tests/fixtures/recursive-conflicts/` and + has no jobs for the new labels; AC12 is proven here only by a scratch + targeted-mutation spot check. This is the recorded sibling dependency owned by + `0005-guard-registration-sweep` (`spec.md` non-goals), not a regression in + `bash tests/run.sh`. +- **Harness observation (pre-existing, out of scope).** `tests/run.sh` sources each + check with `.` and does not inspect the source's return status, so a syntax + error inside a check file makes that entire area's assertions silently not run + while the suite still exits 0 (reproduced: an unmatched `if` at the top of + `85-conflict-guards.sh` yielded `exit=0`, `0 failed`, and 454 instead of 551 + total assertions). This affects every area and predates this item; it is a + follow-up suggestion for the harness, not a defect of this work item. +- **No flakiness observed.** The fixture is pure committed data; assertions are + deterministic and order-independent, and the fixture ids stay in the + non-colliding `92xx` fixture space, allocating no real `work/` number. + +## Verdict + +Every acceptance criterion and every spec edge case is covered by a passing, +mutation-sensitive committed assertion. No implementation defect found; no +backtrack required. From 031ce5a86a1864b7e1f6cc395f770f7f6a63aae5 Mon Sep 17 00:00:00 2001 From: Craig Truitt Date: Wed, 14 Oct 2026 10:08:20 -0500 Subject: [PATCH 3/3] docs(work): record ship state for 0015-recursive-workflow-guards/0003-recursive-conflicts-guard Record the branch, commits, PR #93, and the reconcile result (reconciled by fast-forward; no conflicts) so the shipped signal travels with the branch. Refs: work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard --- .../0003-recursive-conflicts-guard/ship.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/ship.md diff --git a/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/ship.md b/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/ship.md new file mode 100644 index 0000000..4a7631f --- /dev/null +++ b/work/0015-recursive-workflow-guards/0003-recursive-conflicts-guard/ship.md @@ -0,0 +1,38 @@ +--- +feature: 0015-recursive-workflow-guards/0003-recursive-conflicts-guard +phase: ship +status: final +created: 2026-10-14 +updated: 2026-10-14 +parent: 0015-recursive-workflow-guards +--- + +# Ship record — Recursive declared-conflicts guard + +- Branch: `test/0015-recursive-workflow-guards+0003-recursive-conflicts-guard` +- PR: https://github.com/49115566/opencode-framework/pull/93 +- Commits: + - `05189ca` test(conflict-guards): pin the recursive declared-conflicts check + - `1758e12` docs(work): record build, verification, and review for 0015-recursive-workflow-guards/0003-recursive-conflicts-guard + +## Reconcile + +- Result: reconciled (fast-forward, no conflicts) — the item branch was a strict ancestor of + `origin/main` (merge base `8d1a35e` == HEAD); `git merge --no-edit origin/main` + fast-forwarded to `43382b1`, adding only the sibling plan artifacts under + `work/0015-recursive-workflow-guards/{0001,0002,0004}/`. The merge touched + `work/` paths, so the `work/` artifact reconcile re-ran: no duplicate + top-level `NNNN`, no duplicate per-parent `MMMM` at any decomposing node, and + every manifest `Depends on` resolves within its own `Children` table with an + acyclic stored graph. +- Resolved paths: + - none — the merge was a fast-forward with no conflict markers, preserving both + branches' records in the merged tree. +- Re-verification: `bash tests/run.sh` → `551 passed, 0 failed, 0 skipped`; + `FRAMEWORK_TEST_AREAS=85-conflict-guards bash tests/run.sh` → `97 passed, + 0 failed, 0 skipped` (all 11 `AC23 recursive-*` labels `ok`). +- Pre-flight: no conflicts detected. `git merge-tree --write-tree HEAD + origin/main` reported no conflicted paths (classes (a)/(b)); no duplicate + top-level or per-parent sequence prefixes (class (c)); no manifest graph fault + (class (b)); duplicated inventory/count facts agree (class (d)) — confirmed by + the green full suite.