mirror of
https://github.com/Strategic-Automation/violin.git
synced 2026-08-14 12:33:37 +02:00
214 lines
7.0 KiB
Python
214 lines
7.0 KiB
Python
"""Structured finding creation from guarded execution receipts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from hashlib import sha256
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from . import hypotheses, state
|
|
from .skill_receipts import get_review_readiness
|
|
|
|
_FINDING_ID_RE = re.compile(r"FIND-(\d{3,})$")
|
|
_SEVERITIES = {"critical", "high", "medium", "low", "info"}
|
|
|
|
|
|
def _next_finding_id(directory: Path) -> str:
|
|
numbers = []
|
|
for path in directory.glob("FIND-*.md"):
|
|
match = _FINDING_ID_RE.fullmatch(path.stem)
|
|
if match:
|
|
numbers.append(int(match.group(1)))
|
|
return f"FIND-{max(numbers, default=0) + 1:03d}"
|
|
|
|
|
|
def _batch_evidence(eng_dir: Path, pending: dict[str, Any]) -> list[str]:
|
|
unmatched = {str(item.get("command") or "") for item in pending.get("commands") or []}
|
|
evidence: list[str] = []
|
|
manifests = sorted(
|
|
(eng_dir / "evidence" / "executions").glob("*.json"),
|
|
key=lambda path: path.stat().st_mtime_ns,
|
|
reverse=True,
|
|
)
|
|
for manifest in manifests:
|
|
receipt = state.read_json(manifest)
|
|
command = str(receipt.get("command") or "")
|
|
if command not in unmatched:
|
|
continue
|
|
unmatched.remove(command)
|
|
for value in (receipt.get("evidence_paths") or {}).values():
|
|
relative = str(value or "").strip()
|
|
if relative and (eng_dir / relative).is_file() and relative not in evidence:
|
|
evidence.append(relative)
|
|
return evidence
|
|
|
|
|
|
def _existing_batch_finding(directory: Path, batch_id: str) -> Path | None:
|
|
marker = f"- **Batch:** {batch_id}"
|
|
for path in sorted(directory.glob("FIND-*.md")):
|
|
try:
|
|
if marker in path.read_text(encoding="utf-8").splitlines():
|
|
return path
|
|
except OSError:
|
|
continue
|
|
return None
|
|
|
|
|
|
def _validate_from_pending_batch(
|
|
eng_dir: str | Path,
|
|
pending: dict[str, Any],
|
|
*,
|
|
title: str,
|
|
severity: str,
|
|
description: str,
|
|
impact: str,
|
|
remediation: str,
|
|
finding_id: str = "",
|
|
hypothesis_id: str = "",
|
|
) -> dict[str, Any]:
|
|
engagement = state.resolve_eng_dir(eng_dir)
|
|
values = {
|
|
"title": title.strip(),
|
|
"description": description.strip(),
|
|
"impact": impact.strip(),
|
|
"remediation": remediation.strip(),
|
|
}
|
|
severity_key = severity.strip().lower()
|
|
if not all(values.values()):
|
|
raise ValueError("title, description, impact, and remediation must be non-empty")
|
|
if severity_key not in _SEVERITIES:
|
|
raise ValueError("severity must be one of Critical, High, Medium, Low, or Info")
|
|
identifier = finding_id.strip().upper()
|
|
if identifier and not _FINDING_ID_RE.fullmatch(identifier):
|
|
raise ValueError("finding_id must use FIND-NNN format")
|
|
if not identifier:
|
|
raise ValueError("finding_id is required so fp-check review can bind the evidence set")
|
|
evidence = _batch_evidence(engagement, pending)
|
|
if not evidence:
|
|
raise ValueError("the current batch has no completed execution receipts to cite")
|
|
normalized_hypothesis = hypothesis_id.strip().upper().removeprefix("H-").zfill(3)
|
|
hypothesis = next(
|
|
(
|
|
item
|
|
for item in hypotheses.parse_hypotheses(engagement / "hypotheses.md")
|
|
if item.id == normalized_hypothesis
|
|
),
|
|
None,
|
|
)
|
|
if (
|
|
not hypothesis
|
|
or hypothesis.canonical_status() != "Validated"
|
|
or not hypothesis.runtime_evidence
|
|
):
|
|
raise ValueError("a finding requires a linked Validated hypothesis with runtime_evidence")
|
|
evidence_digest = "sha256:" + sha256("\n".join(sorted(evidence)).encode()).hexdigest()
|
|
review = get_review_readiness(
|
|
engagement, finding_id=identifier, evidence_digest=evidence_digest
|
|
)
|
|
if not review:
|
|
raise ValueError("fp-check review must be prepared for this evidence on an earlier turn")
|
|
return {
|
|
**values,
|
|
"severity": severity_key,
|
|
"finding_id": identifier,
|
|
"evidence_paths": evidence,
|
|
"hypothesis_id": f"H-{hypothesis.id}",
|
|
}
|
|
|
|
|
|
def _create_from_pending_batch(
|
|
eng_dir: str | Path,
|
|
*,
|
|
title: str,
|
|
severity: str,
|
|
description: str,
|
|
impact: str,
|
|
remediation: str,
|
|
finding_id: str = "",
|
|
hypothesis_id: str = "",
|
|
pending: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
engagement = state.resolve_eng_dir(eng_dir)
|
|
pending = pending or state.get_pending_sync(engagement)
|
|
if not pending:
|
|
raise ValueError("no current execution batch; run guarded validation commands first")
|
|
draft = _validate_from_pending_batch(
|
|
engagement,
|
|
pending,
|
|
title=title,
|
|
severity=severity,
|
|
description=description,
|
|
impact=impact,
|
|
remediation=remediation,
|
|
finding_id=finding_id,
|
|
hypothesis_id=hypothesis_id,
|
|
)
|
|
|
|
directory = engagement / "evidence" / "findings"
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
batch_id = str(pending.get("batch_id") or "")
|
|
existing = _existing_batch_finding(directory, batch_id)
|
|
if existing:
|
|
if draft["finding_id"] and draft["finding_id"] != existing.stem:
|
|
raise ValueError(
|
|
f"batch {batch_id} already has finding {existing.stem}; "
|
|
f"refusing requested {draft['finding_id']}"
|
|
)
|
|
return {
|
|
"finding_id": existing.stem,
|
|
"path": existing.relative_to(engagement).as_posix(),
|
|
"evidence_paths": draft["evidence_paths"],
|
|
"batch_id": batch_id,
|
|
"reused": True,
|
|
}
|
|
|
|
identifier = draft["finding_id"] or _next_finding_id(directory)
|
|
output = directory / f"{identifier}.md"
|
|
if output.exists():
|
|
raise ValueError(f"finding already exists: {output}")
|
|
|
|
commands = [str(item.get("command") or "") for item in pending.get("commands") or []]
|
|
lines = [
|
|
f"# {identifier}: {draft['title']}",
|
|
"",
|
|
f"- **Severity:** {draft['severity'].title()}",
|
|
f"- **Batch:** {batch_id or 'unknown'}",
|
|
f"- **PTT task:** {pending.get('ptt_task_id') or 'unknown'}",
|
|
f"- **Phase:** {pending.get('phase') or 'unknown'}",
|
|
f"- **Hypothesis:** {draft['hypothesis_id']}",
|
|
"",
|
|
"## Description",
|
|
"",
|
|
draft["description"],
|
|
"",
|
|
"## Impact",
|
|
"",
|
|
draft["impact"],
|
|
"",
|
|
"## Evidence",
|
|
"",
|
|
*[f"- `{path}`" for path in draft["evidence_paths"]],
|
|
"",
|
|
"## Reproduction commands",
|
|
"",
|
|
"```text",
|
|
*commands,
|
|
"```",
|
|
"",
|
|
"## Remediation",
|
|
"",
|
|
draft["remediation"],
|
|
"",
|
|
]
|
|
temporary = output.with_suffix(output.suffix + ".tmp")
|
|
temporary.write_text("\n".join(lines), encoding="utf-8")
|
|
temporary.replace(output)
|
|
return {
|
|
"finding_id": identifier,
|
|
"path": output.relative_to(engagement).as_posix(),
|
|
"evidence_paths": draft["evidence_paths"],
|
|
"batch_id": batch_id,
|
|
"reused": False,
|
|
}
|