Skip to content

Commit ffa6203

Browse files
Joncik91Copilotclaude
authored
feat: v1.2 Diagnostics — step-precision, thin-coverage, round-emission, self-cycle wording (#81)
* feat: step-precision concern + tier3 thin-coverage + walker round emission + self-cycle wording Fix B (walker.py): generate_step_precision_concerns — four structural patterns per step action: pip install without version pin, python -m <pkg> with no subcommand, bare URL with no version token, bare model ID alongside LLM vendor SDK. Each emits an edge-case concern with a per-step idempotency guard. Fix C (llm_judge.py, findings.py): tier3-negative-paths-thin-coverage — when a negative-path-omission finding is present and the step's negative-paths count is < 3, emit an alongside warn finding. Threshold is >= 3 = documented. New kind registered in KNOWN_KINDS and TIER3_CONTRADICTION_SEVERITY. Fix D (walker.py): after each round_count increment in record_answer, emit _status.emit("info", "walker.round", round=N, pending=K) for per-round operator visibility. Wrapped in try/except so test contexts without _status can't break it. Fix M (spec_ast.py): rewrite self-cycle-produces message and suggested_fix. Message now says "references X which it also declares in produces (possible self-cycle)." Suggested fix offers the two-branch operator choice: remove from produces if action only names the path, or assert idempotency if it reads it. Glossary (docs/glossary.md): added tier3-negative-paths-thin-coverage (finding) and walker.round (status) entries with full dev/pm/triggered_by/user_action. Tests: 1896 passed (+28 over 1868 baseline). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(glossary): correct tier3-negative-paths-thin-coverage description The 'Tier-3 demotion alongside-finding' framing was inaccurate. negative-path-omission is info-severity and never enters the faithfulness demotion path (_BLOCK_CONTRADICTION_KINDS in llm_judge.py only contains block-severity kinds). The new check fires alongside the original finding when negative-paths count < 3 — co-occurrence, not demotion. Rewriting dev:, pm:, triggered_by:, and dropping the misleading tier3-unfaithful-contradiction related: entry. Caught in Wave 4 Opus aggregate review. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 762de18 commit ffa6203

9 files changed

Lines changed: 656 additions & 7 deletions

bin/findings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
"excessive-post-ship-iteration",
9696
# v1.1 Fix 3 — behavioral claim with structural-only verification
9797
"verification-too-shallow-for-claim",
98+
# v1.2 Fix C — Tier-3 thin negative-path coverage alongside-finding
99+
"tier3-negative-paths-thin-coverage",
98100
}
99101

100102
# Severity mapping for Tier 3 contradiction tuple kinds (v0.5.2).
@@ -118,6 +120,8 @@
118120
"adversarial-pathway": "block",
119121
# v0.8 — contract-filter audit sentinel
120122
"tier3-filter-applied": "info",
123+
# v1.2 Fix C — thin negative-path coverage alongside demotion
124+
"tier3-negative-paths-thin-coverage": "warn",
121125
}
122126

123127
SEVERITIES = {"block", "warn", "info"}

bin/llm_judge.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,11 +1040,29 @@ def _faithfulness_malformed_finding() -> Finding:
10401040
)
10411041

10421042

1043+
def _thin_coverage_finding(original_finding: Finding) -> Finding:
1044+
"""Return a tier3-negative-paths-thin-coverage warn alongside a demoted negative-path-omission."""
1045+
step_n = original_finding.location.step
1046+
msg = (
1047+
f"Step {step_n} has fewer than 3 negative-paths entries (thin coverage); "
1048+
"consider adding more failure branches."
1049+
)[:findings.MAX_MESSAGE_LEN]
1050+
return Finding(
1051+
tier=3,
1052+
kind="tier3-negative-paths-thin-coverage",
1053+
severity="warn",
1054+
location=original_finding.location,
1055+
message=msg,
1056+
dismissable=True,
1057+
)
1058+
1059+
10431060
def _verify_block_tuples_with_citations(
10441061
tuple_findings: list[Finding],
10451062
step_table: dict,
10461063
*,
10471064
config: JudgeConfig,
1065+
step_objects: list | None = None,
10481066
) -> list[Finding]:
10491067
"""Run a second batched API call to verify block-severity contradiction tuples.
10501068
@@ -1067,15 +1085,41 @@ def _verify_block_tuples_with_citations(
10671085
Returns a new finding list with the same non-block findings plus either
10681086
verified-or-demoted block findings.
10691087
"""
1088+
# Build negative-paths count lookup from step_objects (Fix C).
1089+
# Done first so thin-coverage logic can run even when no block findings exist.
1090+
_neg_paths_count: dict[int, int] = {}
1091+
if step_objects:
1092+
for obj in step_objects:
1093+
if isinstance(obj, dict):
1094+
sn = obj.get("step")
1095+
np = obj.get("negative_paths") or obj.get("negative-paths") or []
1096+
else:
1097+
sn = getattr(obj, "step", None)
1098+
np = getattr(obj, "negative_paths", None) or []
1099+
if sn is not None:
1100+
_neg_paths_count[int(sn)] = len(np) if np else 0
1101+
10701102
# Separate block (verifiable) from non-block (pass through).
10711103
block_indices: list[int] = []
10721104
for idx, f in enumerate(tuple_findings):
10731105
if f.kind in _BLOCK_CONTRADICTION_KINDS and f.severity == "block":
10741106
block_indices.append(idx)
10751107

1076-
# Short-circuit: nothing to verify.
1108+
# Fix C: emit thin-coverage alongside-finding for any negative-path-omission
1109+
# finding whose step has fewer than 3 negative-paths entries.
1110+
# This runs regardless of whether block findings exist.
1111+
thin_coverage_additions: list[Finding] = []
1112+
for f in tuple_findings:
1113+
if f.kind == "negative-path-omission":
1114+
step_n = f.location.step
1115+
np_count = _neg_paths_count.get(step_n, 0) if step_n is not None else 0
1116+
if np_count < 3:
1117+
thin_coverage_additions.append(_thin_coverage_finding(f))
1118+
1119+
# Short-circuit: nothing to verify via faithfulness check.
10771120
if not block_indices:
1078-
return list(tuple_findings)
1121+
result = list(tuple_findings) + thin_coverage_additions
1122+
return result
10791123

10801124
# Build a minimal representation of block findings to send to DeepSeek.
10811125
block_summaries = []
@@ -1184,7 +1228,7 @@ def _verify_block_tuples_with_citations(
11841228
if should_demote:
11851229
result[orig_idx] = _faithfulness_demote_finding(original_finding)
11861230

1187-
return result
1231+
return result + thin_coverage_additions
11881232

11891233

11901234
# ── Deterministic contract-resolution post-filter ────────────────────────────
@@ -1536,4 +1580,6 @@ def evaluate(
15361580

15371581
# Second pass: cite-and-verify for block-severity tuples (v0.6 faithfulness check).
15381582
# Zero extra cost when no block tuples; one batched call otherwise.
1539-
return _verify_block_tuples_with_citations(primary_findings, step_table, config=config)
1583+
return _verify_block_tuples_with_citations(
1584+
primary_findings, step_table, config=config, step_objects=step_objects
1585+
)

bin/spec_ast.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,7 +1641,7 @@ def _check_self_cycle_produces(steps: list[dict]) -> list[_findings.Finding]:
16411641
continue
16421642
# Self-cycle confirmed
16431643
display = tok if len(tok) <= 60 else "..." + tok[-57:]
1644-
msg = f"Step {step_n} action consumes {display!r} which it also produces (self-cycle)."
1644+
msg = f"Step {step_n} action references {display!r} which it also declares in produces (possible self-cycle)."
16451645
if len(msg) > 140:
16461646
msg = msg[:137] + "..."
16471647
results.append(_findings.Finding(
@@ -1653,8 +1653,8 @@ def _check_self_cycle_produces(steps: list[dict]) -> list[_findings.Finding]:
16531653
),
16541654
message=msg,
16551655
suggested_fix=(
1656-
"Move the file to a prior step's produces:, or remove it from "
1657-
"this step's produces: if it is an input, not an output."
1656+
"If action only names the path, remove from produces:. "
1657+
"If action reads X, change verification to assert idempotency."
16581658
)[:140],
16591659
))
16601660

bin/walker.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ def record_answer(state: WalkState, *, concern_id: str, answer: str) -> WalkStat
315315
state.answered[concern_id] = answer
316316
del state.pending[i]
317317
state.round_count += 1
318+
# Fix D: per-round visibility for operators.
319+
try:
320+
from bin import _status as _st
321+
_st.emit("info", "walker.round", round=state.round_count, pending=len(state.pending))
322+
except Exception: # noqa: BLE001
323+
pass
318324
# Flip seed-family flags
319325
if concern_id == "seed-lifecycle":
320326
state.lifecycle_asked = True
@@ -1392,6 +1398,140 @@ def generate_negative_path_concerns(
13921398
return concerns
13931399

13941400

1401+
# ── Step-action precision concerns (Fix B) ───────────────────────────────────
1402+
1403+
# pip install without pinned version: no `==`, no `@`, no `-r` constraint file, no `--constraint`.
1404+
_PRECISION_PIP_UNVERSIONED_RE = re.compile(
1405+
r"\bpip\s+install\b(?!.*(?:==|@\s*\S|(?:-r|-c|--constraint|--requirement)\s+\S))",
1406+
)
1407+
# python -m <pkg> with no subcommand/argument after the package name.
1408+
_PRECISION_PYTHON_M_BARE_RE = re.compile(
1409+
r"\bpython3?\s+-m\s+([\w.]+)\s*$"
1410+
)
1411+
# Bare URL with no version token nearby (http(s):// not followed by a version token on the same line).
1412+
_PRECISION_BARE_URL_RE = re.compile(
1413+
r"https?://\S+"
1414+
)
1415+
_PRECISION_VERSION_TOKEN_RE = re.compile(
1416+
r"(?:==|@\s*v?[\d.]+|/v[\d.]+/|/[\d.]+/)"
1417+
)
1418+
# LLM vendor SDK + bare model identifier patterns.
1419+
_PRECISION_LLM_VENDOR_RE = re.compile(
1420+
r"\b(anthropic|openai|deepseek|ollama|client\.messages\.create|client\.chat\.completions|client\.responses\.create)\b",
1421+
re.IGNORECASE,
1422+
)
1423+
_PRECISION_BARE_MODEL_ID_RE = re.compile(
1424+
r"\b(gpt-\d+[a-z0-9-]*|claude-[a-z0-9-]+|deepseek-[a-z0-9-]+|llama-[a-z0-9-]+|mistral-[a-z0-9-]+|gemma-[a-z0-9-]+|command-[a-z0-9-]+)\b",
1425+
re.IGNORECASE,
1426+
)
1427+
1428+
1429+
def _check_step_precision(step_n: int, action: str) -> list[tuple[str, str]]:
1430+
"""Return list of (concern_id, summary) for vague shapes in *action*.
1431+
1432+
Checks (structural patterns):
1433+
1. pip install without version pin.
1434+
2. python -m <pkg> with no subcommand args.
1435+
3. Bare URL with no version-pin verification nearby.
1436+
4. Bare model ID alongside LLM vendor SDK call.
1437+
"""
1438+
concerns: list[tuple[str, str]] = []
1439+
a = action.strip()
1440+
1441+
# 1. pip install without version pin
1442+
if _PRECISION_PIP_UNVERSIONED_RE.search(a):
1443+
concerns.append((
1444+
f"precision-pip-{step_n}",
1445+
(
1446+
f"Step {step_n} action `{a[:60]}` has `pip install` without a pinned version — "
1447+
"add `==X.Y.Z` or a constraint file (e.g. `-c constraints.txt`) to make the "
1448+
"build reproducible."
1449+
)[:280],
1450+
))
1451+
1452+
# 2. python -m <pkg> with no subcommand args
1453+
m = _PRECISION_PYTHON_M_BARE_RE.search(a)
1454+
if m:
1455+
pkg = m.group(1)
1456+
concerns.append((
1457+
f"precision-python-m-{step_n}",
1458+
(
1459+
f"Step {step_n} action `{a[:60]}` invokes `python -m {pkg}` with no subcommand "
1460+
"or arguments — specify the subcommand (e.g. `python -m myapp serve`) so the "
1461+
"intent is unambiguous."
1462+
)[:280],
1463+
))
1464+
1465+
# 3. Bare URL with no version-pin token in the same action
1466+
url_m = _PRECISION_BARE_URL_RE.search(a)
1467+
if url_m:
1468+
url = url_m.group(0)[:60]
1469+
if not _PRECISION_VERSION_TOKEN_RE.search(a):
1470+
concerns.append((
1471+
f"precision-url-{step_n}",
1472+
(
1473+
f"Step {step_n} action contains a bare URL (`{url}`) with no version pin — "
1474+
"pin to a specific version tag or commit so the download is reproducible."
1475+
)[:280],
1476+
))
1477+
1478+
# 4. Bare model ID alongside LLM vendor SDK
1479+
if _PRECISION_LLM_VENDOR_RE.search(a):
1480+
model_m = _PRECISION_BARE_MODEL_ID_RE.search(a)
1481+
if model_m:
1482+
model_id = model_m.group(0)
1483+
concerns.append((
1484+
f"precision-model-{step_n}",
1485+
(
1486+
f"Step {step_n} action uses bare model ID `{model_id}` alongside a vendor SDK — "
1487+
"document the model selection logic (env var, config key, or version-locked constant) "
1488+
"so the choice is explicit and auditable."
1489+
)[:280],
1490+
))
1491+
1492+
return concerns
1493+
1494+
1495+
def generate_step_precision_concerns(
1496+
state: WalkState,
1497+
steps: list[dict],
1498+
) -> list[Concern]:
1499+
"""Emit edge-case concerns for vague action shapes in each step.
1500+
1501+
Checks four structural patterns per step action (Fix B):
1502+
1. pip install without version pin.
1503+
2. python -m <pkg> with no subcommand.
1504+
3. Bare URL with no version-pin verification.
1505+
4. Bare model ID alongside LLM vendor SDK.
1506+
1507+
Idempotent: never emits a concern whose id already exists in state.
1508+
"""
1509+
existing_ids: set[str] = (
1510+
{c.id for c in state.asked}
1511+
| {c.id for c in state.pending}
1512+
| set(state.answered)
1513+
)
1514+
concerns: list[Concern] = []
1515+
for step in steps:
1516+
step_n = step.get("step")
1517+
if step_n is None:
1518+
continue
1519+
action: str = step.get("action", "") or ""
1520+
if not action:
1521+
continue
1522+
for concern_id, summary in _check_step_precision(step_n, action):
1523+
if concern_id in existing_ids:
1524+
continue
1525+
concerns.append(Concern(
1526+
id=concern_id,
1527+
kind="edge-case",
1528+
receivers=["human"],
1529+
depends_on=[],
1530+
summary=summary,
1531+
))
1532+
return concerns
1533+
1534+
13951535
# ── Scaffold-precondition concern ─────────────────────────────────────────────
13961536

13971537
# Stdlib top-level modules for `python -m <pkg>` heuristic — these do NOT need

docs/glossary.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,3 +1216,21 @@ Status codes: dotted identifiers like `walker.init`. Terms: `term:<noun>` prefix
12161216
- user_action: Run `spectre catalog upgrade-taxonomy --spec <slug> --to <version>` when you want to consider the newer axes, or ignore.
12171217
- related: term:taxonomy-version
12181218
- since: v1.0
1219+
1220+
## tier3-negative-paths-thin-coverage
1221+
- kind: finding
1222+
- dev: Emitted alongside any `negative-path-omission` finding whose step has fewer than 3 `negative-paths:` entries. No demotion is involved — `negative-path-omission` is info-severity and never enters the faithfulness demotion path. The pairing signals "the LLM judge flagged a missing failure branch AND the step's structural coverage is thin." Tier-3 warn, dismissable.
1223+
- pm: A step's failure-branch coverage is thin (fewer than 3 entries) and the automated review flagged a missing failure scenario. Consider adding more failure scenarios to the step's negative-paths section.
1224+
- triggered_by: Co-occurrence of a `negative-path-omission` finding (LLM-judge output) and `< 3` negative-paths entries on the affected step.
1225+
- user_action: Add more negative-paths entries to the flagged step (at least 3 entries covering different failure modes), or dismiss if the step genuinely has only one or two realistic failure branches.
1226+
- related: negative-path-omission
1227+
- since: v1.2
1228+
1229+
## walker.round
1230+
- kind: status
1231+
- dev: Emitted after each concern answer in the walker interview loop. Fields: round=N (1-based count of answered concerns), pending=K (remaining non-stale concerns). Provides per-round visibility into walk progress without exposing convergence decisions.
1232+
- pm: The walker just finished interview round N. There are K questions still to answer.
1233+
- triggered_by: walker.record_answer increments round_count.
1234+
- user_action: No action required. Monitor round and pending counts to gauge walk progress. Operator interpretation only — walker.round does not imply any threshold or convergence signal.
1235+
- related: walker.yield, walker.coverage
1236+
- since: v1.2

0 commit comments

Comments
 (0)