devlyn-cli 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -6
- package/CLAUDE.md +7 -7
- package/README.md +34 -13
- package/bin/devlyn.js +44 -65
- package/bin/instruction-templates.json +82 -0
- package/bin/instructions.js +187 -0
- package/config/skills/_shared/adapters/claude.md +1 -5
- package/config/skills/_shared/adapters/codex.md +1 -5
- package/config/skills/_shared/adapters/omp.md +0 -4
- package/config/skills/_shared/archive_run.py +5 -1
- package/config/skills/_shared/codex-config.md +3 -14
- package/config/skills/_shared/engine-preflight.md +3 -3
- package/config/skills/_shared/expected.schema.json +2 -2
- package/config/skills/_shared/platform-support.py +4 -1
- package/config/skills/_shared/resolve-bootstrap.py +22 -2
- package/config/skills/_shared/role-config.py +14 -2
- package/config/skills/_shared/spec-verify-check.py +427 -101
- package/config/skills/_shared/state-phase-write.py +497 -49
- package/config/skills/_shared/task-complete.py +46 -5
- package/config/skills/_shared/terminal-claim-check.py +2 -2
- package/config/skills/devlyn:ideate/SKILL.md +1 -1
- package/config/skills/devlyn:ideate/references/from-spec-mode.md +1 -1
- package/config/skills/devlyn:ideate/references/spec-template.md +2 -2
- package/config/skills/devlyn:resolve/SKILL.md +29 -51
- package/config/skills/devlyn:resolve/references/free-form-mode.md +2 -2
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +8 -7
- package/config/skills/devlyn:resolve/references/phases/cleanup.md +5 -11
- package/config/skills/devlyn:resolve/references/phases/implement.md +1 -0
- package/config/skills/devlyn:resolve/references/phases/plan.md +3 -3
- package/config/skills/devlyn:resolve/references/state-schema.md +10 -9
- package/package.json +2 -1
- package/scripts/lint-skills.sh +14 -4
- package/scripts/test-owner-phases.py +193 -0
|
@@ -21,7 +21,8 @@ Default mode (BUILD_GATE invocation, no args):
|
|
|
21
21
|
(3) For generated criteria and legacy handwritten specs without a sibling,
|
|
22
22
|
source markdown extract reads `pipeline.state.json:
|
|
23
23
|
source.{spec_path | criteria_path}` and extracts a `## Verification`
|
|
24
|
-
```json``` block.
|
|
24
|
+
```json``` block. Stage executable commands; an explicit pure-design
|
|
25
|
+
contract removes stale `.devlyn/spec-verify.json` instead.
|
|
25
26
|
(4) If no json block in source AND source.type=="generated": emit
|
|
26
27
|
CRITICAL `correctness.spec-verify-malformed` so the fix-loop reruns
|
|
27
28
|
BUILD.
|
|
@@ -38,14 +39,13 @@ Default mode (BUILD_GATE invocation, no args):
|
|
|
38
39
|
run-fixture.sh's combined-stream matching semantics.
|
|
39
40
|
|
|
40
41
|
Check mode (`--check <markdown_path>`):
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- Exits
|
|
45
|
-
to
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`complexity` value.
|
|
42
|
+
- Spec-source authoring preflight: a present sibling `spec.expected.json`
|
|
43
|
+
takes precedence; otherwise validate the legacy inline carrier. Validate
|
|
44
|
+
metadata against the given Markdown path, including non-canonical names.
|
|
45
|
+
- Exits 2 on malformed carrier or metadata, without falling back from a bad
|
|
46
|
+
sibling to inline content. An absent sibling and sentinel retain opt-in
|
|
47
|
+
success. Never stages or executes commands. Generated-source runtime has
|
|
48
|
+
its own inline validation and must not use this sibling-precedence route.
|
|
49
49
|
|
|
50
50
|
Expected-contract check mode (`--check-expected <json_path>`):
|
|
51
51
|
- Used by /devlyn:ideate after writing sibling `spec.expected.json`.
|
|
@@ -846,14 +846,21 @@ def validate_expected_shape(data) -> str | None:
|
|
|
846
846
|
|
|
847
847
|
def validate_inline_shape(data: object) -> str | None:
|
|
848
848
|
if isinstance(data, dict):
|
|
849
|
-
unknown = sorted(set(data) - {"verification_commands"})
|
|
849
|
+
unknown = sorted(set(data) - {"verification_commands", "pure_design"})
|
|
850
850
|
if unknown:
|
|
851
851
|
return (
|
|
852
852
|
f"unsupported inline key(s): {', '.join(unknown)}; inline carriers "
|
|
853
|
-
"support only verification_commands. Encode these checks as commands, "
|
|
853
|
+
"support only verification_commands and pure_design. Encode these checks as commands, "
|
|
854
854
|
"or use a real spec with sibling spec.expected.json."
|
|
855
855
|
)
|
|
856
|
-
|
|
856
|
+
error = validate_expected_shape(data)
|
|
857
|
+
if error:
|
|
858
|
+
return error
|
|
859
|
+
if isinstance(data, dict) and data.get("pure_design") is True:
|
|
860
|
+
if data.get("verification_commands") != []:
|
|
861
|
+
return "inline pure_design: true requires an explicit empty verification_commands list"
|
|
862
|
+
return None
|
|
863
|
+
return validate_shape(data)
|
|
857
864
|
|
|
858
865
|
|
|
859
866
|
def validate_expected_against_sibling_spec(spec_path: Path, data: object) -> str | None:
|
|
@@ -1159,9 +1166,11 @@ def risk_probes_state_error(state: dict) -> str | None:
|
|
|
1159
1166
|
|
|
1160
1167
|
|
|
1161
1168
|
def source_integrity_error(src_type: str | None, state: dict, source_md: Path | None) -> str | None:
|
|
1169
|
+
src = state.get("source") if isinstance(state.get("source"), dict) else {}
|
|
1162
1170
|
if source_md is None:
|
|
1171
|
+
if src_type == "generated":
|
|
1172
|
+
return f"source.criteria_path must identify an existing generated criteria file; declared path: {src.get('criteria_path')!r}."
|
|
1163
1173
|
return None
|
|
1164
|
-
src = state.get("source") if isinstance(state.get("source"), dict) else {}
|
|
1165
1174
|
if src_type == "generated":
|
|
1166
1175
|
field = "criteria_sha256"
|
|
1167
1176
|
required = True
|
|
@@ -1198,33 +1207,33 @@ def load_expected_contract(expected_path: Path) -> tuple[dict | None, str | None
|
|
|
1198
1207
|
return (data, None)
|
|
1199
1208
|
|
|
1200
1209
|
|
|
1201
|
-
def stage_from_source(md: Path, devlyn_dir: Path) -> tuple[bool, str | None]:
|
|
1210
|
+
def stage_from_source(md: Path, devlyn_dir: Path) -> tuple[bool, bool, str | None]:
|
|
1202
1211
|
"""Materialize .devlyn/spec-verify.json from the json block in `md`.
|
|
1203
1212
|
|
|
1204
|
-
Returns (staged, error).
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
contract; a missing fence is a mistake, not a no-op). staged=False,
|
|
1209
|
-
error=None → the sentinel is absent entirely (handwritten spec or
|
|
1210
|
-
generated source missing the contract).
|
|
1213
|
+
Returns (found, staged, error). found=False means no sentinel. A present
|
|
1214
|
+
sentinel without a valid fenced JSON contract returns an error. A valid
|
|
1215
|
+
pure-design contract removes stale staging and returns (True, False, None);
|
|
1216
|
+
executable commands return (True, True, None) after writing spec-verify.json.
|
|
1211
1217
|
"""
|
|
1212
1218
|
section_found, block = extract_verification_block(md.read_text(encoding="utf-8"))
|
|
1213
1219
|
if not section_found:
|
|
1214
|
-
return (False, None)
|
|
1220
|
+
return (False, False, None)
|
|
1215
1221
|
if block is None:
|
|
1216
|
-
return (False, f"`<!-- devlyn:verification -->` section in {md} has no fenced ```json``` block")
|
|
1222
|
+
return (True, False, f"`<!-- devlyn:verification -->` section in {md} has no fenced ```json``` block")
|
|
1217
1223
|
try:
|
|
1218
1224
|
data = loads_strict_json(block)
|
|
1219
1225
|
except ValueError as e:
|
|
1220
|
-
return (False, f"`<!-- devlyn:verification -->` ```json``` block in {md} has invalid JSON: {e}")
|
|
1226
|
+
return (True, False, f"`<!-- devlyn:verification -->` ```json``` block in {md} has invalid JSON: {e}")
|
|
1221
1227
|
err = validate_inline_shape(data)
|
|
1222
1228
|
if err:
|
|
1223
|
-
return (False, f"`<!-- devlyn:verification -->` ```json``` block in {md}: {err}")
|
|
1229
|
+
return (True, False, f"`<!-- devlyn:verification -->` ```json``` block in {md}: {err}")
|
|
1230
|
+
if data["verification_commands"] == []:
|
|
1231
|
+
(devlyn_dir / "spec-verify.json").unlink(missing_ok=True)
|
|
1232
|
+
return (True, False, None)
|
|
1224
1233
|
normalized = {"verification_commands": data["verification_commands"]}
|
|
1225
1234
|
devlyn_dir.mkdir(parents=True, exist_ok=True)
|
|
1226
1235
|
(devlyn_dir / "spec-verify.json").write_text(json.dumps(normalized, indent=2) + "\n", encoding="utf-8")
|
|
1227
|
-
return (True, None)
|
|
1236
|
+
return (True, True, None)
|
|
1228
1237
|
|
|
1229
1238
|
|
|
1230
1239
|
def stage_from_expected(
|
|
@@ -1335,7 +1344,7 @@ def diff_text_for_expected(work: Path, devlyn_dir: Path, state: dict) -> tuple[s
|
|
|
1335
1344
|
external_diff = devlyn_dir / "external-diff.patch"
|
|
1336
1345
|
if external_diff.is_file():
|
|
1337
1346
|
try:
|
|
1338
|
-
return (external_diff.read_text(encoding="utf-8"), None)
|
|
1347
|
+
return (external_diff.read_text(encoding="utf-8", errors="surrogateescape"), None)
|
|
1339
1348
|
except OSError as e:
|
|
1340
1349
|
return ("", f"cannot read {external_diff}: {e}")
|
|
1341
1350
|
base_sha = ((state.get("base_ref") or {}).get("sha") or "").strip()
|
|
@@ -1374,28 +1383,46 @@ def count_deps_added(work: Path, state: dict) -> int:
|
|
|
1374
1383
|
return count
|
|
1375
1384
|
|
|
1376
1385
|
|
|
1377
|
-
def changed_files(work: Path, state: dict, devlyn_dir: Path) -> list[str]:
|
|
1386
|
+
def changed_files(work: Path, state: dict, devlyn_dir: Path) -> tuple[list[str], str | None]:
|
|
1378
1387
|
external_diff = devlyn_dir / "external-diff.patch"
|
|
1379
1388
|
if external_diff.is_file():
|
|
1380
1389
|
names: list[str] = []
|
|
1381
1390
|
try:
|
|
1382
|
-
|
|
1383
|
-
except OSError:
|
|
1384
|
-
return []
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
+
external_bytes = external_diff.read_bytes()
|
|
1392
|
+
except OSError as error:
|
|
1393
|
+
return ([], f"cannot read {external_diff}: {error}")
|
|
1394
|
+
if not external_bytes:
|
|
1395
|
+
return ([], None)
|
|
1396
|
+
# Prefix depth is otherwise ambiguous (src/x becomes x with -p1).
|
|
1397
|
+
headers = [line for line in external_bytes.split(b"\n") if line.startswith(b"diff --git ")]
|
|
1398
|
+
if not headers or any(not re.fullmatch(
|
|
1399
|
+
rb'diff --git (a/[^"\r]*|"a/(?:[^"\\]|\\.)*") (b/[^"\r]*|"b/(?:[^"\\]|\\.)*")\r?', line,
|
|
1400
|
+
) for line in headers):
|
|
1401
|
+
return ([], "external diff requires Git a/ and b/ path prefixes; regenerate with git diff --binary --src-prefix=a/ --dst-prefix=b/")
|
|
1402
|
+
# Statistics mode never applies the patch. Reverse parsing includes
|
|
1403
|
+
# rename sources as well as destinations without parsing quoted headers.
|
|
1404
|
+
for direction in ([], ["--reverse"]):
|
|
1405
|
+
proc = subprocess.run(
|
|
1406
|
+
["git", "apply", "--numstat", "-z", "-p1", "--whitespace=nowarn", *direction], cwd=str(work),
|
|
1407
|
+
input=external_bytes, capture_output=True,
|
|
1408
|
+
)
|
|
1409
|
+
if proc.returncode != 0:
|
|
1410
|
+
return ([], (proc.stderr or proc.stdout).decode("utf-8", "replace").strip() or "git apply --numstat failed")
|
|
1411
|
+
for record in proc.stdout.split(b"\0"):
|
|
1412
|
+
if record:
|
|
1413
|
+
fields = record.split(b"\t", 2)
|
|
1414
|
+
if len(fields) != 3 or not fields[2]:
|
|
1415
|
+
return ([], "invalid git apply --numstat path record")
|
|
1416
|
+
names.append(fields[2].decode("utf-8", "surrogateescape"))
|
|
1417
|
+
return (list(dict.fromkeys(names)), None)
|
|
1391
1418
|
base_sha = ((state.get("base_ref") or {}).get("sha") or "").strip()
|
|
1392
|
-
cmd = ["git", "diff", "--name-only"]
|
|
1419
|
+
cmd = ["git", "diff", "--name-only", "-z", "--no-renames"]
|
|
1393
1420
|
if base_sha:
|
|
1394
1421
|
cmd.append(base_sha)
|
|
1395
|
-
proc = subprocess.run(cmd, cwd=str(work), capture_output=True
|
|
1422
|
+
proc = subprocess.run(cmd, cwd=str(work), capture_output=True)
|
|
1396
1423
|
if proc.returncode != 0:
|
|
1397
|
-
return []
|
|
1398
|
-
return [
|
|
1424
|
+
return ([], (proc.stderr or proc.stdout).decode("utf-8", "replace").strip() or "git diff --name-only failed")
|
|
1425
|
+
return ([path.decode("utf-8", "surrogateescape") for path in proc.stdout.split(b"\0") if path], None)
|
|
1399
1426
|
|
|
1400
1427
|
|
|
1401
1428
|
def expected_contract_findings(
|
|
@@ -1411,6 +1438,8 @@ def expected_contract_findings(
|
|
|
1411
1438
|
findings: list[dict] = []
|
|
1412
1439
|
seq = finding_start
|
|
1413
1440
|
diff_text, diff_error = diff_text_for_expected(work, devlyn_dir, state)
|
|
1441
|
+
paths, paths_error = changed_files(work, state, devlyn_dir) if expected_data.get("forbidden_files") else ([], None)
|
|
1442
|
+
diff_error = diff_error or paths_error
|
|
1414
1443
|
if diff_error and (
|
|
1415
1444
|
expected_data.get("forbidden_patterns") or expected_data.get("forbidden_files")
|
|
1416
1445
|
):
|
|
@@ -1420,12 +1449,12 @@ def expected_contract_findings(
|
|
|
1420
1449
|
"level": "error",
|
|
1421
1450
|
"severity": "CRITICAL",
|
|
1422
1451
|
"confidence": 1.0,
|
|
1423
|
-
"message": f"Cannot compute diff for
|
|
1452
|
+
"message": f"Cannot compute diff for expected contract: {diff_error}",
|
|
1424
1453
|
"file": str(expected_path or "spec.expected.json"),
|
|
1425
1454
|
"line": 1,
|
|
1426
1455
|
"phase": output_phase(),
|
|
1427
|
-
"criterion_ref": "spec.expected.json/forbidden_patterns",
|
|
1428
|
-
"fix_hint": "Ensure
|
|
1456
|
+
"criterion_ref": "spec.expected.json/forbidden_files" if paths_error else "spec.expected.json/forbidden_patterns",
|
|
1457
|
+
"fix_hint": "Ensure base_ref.sha is valid and any external-diff.patch is a readable Git patch with a/ and b/ prefixes.",
|
|
1429
1458
|
"blocking": True,
|
|
1430
1459
|
"status": "open",
|
|
1431
1460
|
})
|
|
@@ -1451,7 +1480,7 @@ def expected_contract_findings(
|
|
|
1451
1480
|
"status": "open",
|
|
1452
1481
|
})
|
|
1453
1482
|
seq += 1
|
|
1454
|
-
changed = set(
|
|
1483
|
+
changed = set(paths)
|
|
1455
1484
|
for i, required in enumerate(expected_data.get("required_files", []) or []):
|
|
1456
1485
|
if (work / required).exists():
|
|
1457
1486
|
continue
|
|
@@ -1725,10 +1754,9 @@ def authorized_surface_findings(
|
|
|
1725
1754
|
to catch. A persistent finding exhausts the existing BUILD_GATE
|
|
1726
1755
|
fix-loop budget and halts for user/orchestrator review instead.
|
|
1727
1756
|
|
|
1728
|
-
Not re-run at VERIFY MECHANICAL time
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
CLEANUP's own sanctioned changes.
|
|
1757
|
+
Not re-run at VERIFY MECHANICAL time: historical worker CLEANUP can
|
|
1758
|
+
license paths outside PLAN. Owner CLEANUP instead enforces an unchanged
|
|
1759
|
+
source checkpoint and the final untracked baseline before VERIFY.
|
|
1732
1760
|
"""
|
|
1733
1761
|
surface, surface_error = load_authorized_surface(devlyn_dir)
|
|
1734
1762
|
if surface_error is not None:
|
|
@@ -1770,7 +1798,16 @@ def authorized_surface_findings(
|
|
|
1770
1798
|
|
|
1771
1799
|
findings: list[dict] = []
|
|
1772
1800
|
seq = finding_start
|
|
1773
|
-
|
|
1801
|
+
paths, paths_error = changed_files(work, state, devlyn_dir)
|
|
1802
|
+
if paths_error is not None:
|
|
1803
|
+
return ([scope_finding(
|
|
1804
|
+
finding_start,
|
|
1805
|
+
"scope.authorized-surface-malformed",
|
|
1806
|
+
f"Cannot read changed files: {paths_error}",
|
|
1807
|
+
".devlyn/external-diff.patch" if (devlyn_dir / "external-diff.patch").is_file() else ".devlyn/pipeline.state.json",
|
|
1808
|
+
"Ensure base_ref.sha is valid and any external-diff.patch is a readable Git patch with a/ and b/ prefixes.",
|
|
1809
|
+
)], finding_start + 1)
|
|
1810
|
+
for path in paths:
|
|
1774
1811
|
if path_matches_surface(path, surface):
|
|
1775
1812
|
continue
|
|
1776
1813
|
findings.append(scope_finding(
|
|
@@ -1859,13 +1896,8 @@ def run_write_untracked_baseline(work: Path, devlyn_dir: Path) -> int:
|
|
|
1859
1896
|
|
|
1860
1897
|
|
|
1861
1898
|
def run_check_mode(md_path: Path) -> int:
|
|
1862
|
-
"""
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
Exit 0: `<!-- devlyn:verification -->` sentinel absent OR section present
|
|
1866
|
-
and well-formed.
|
|
1867
|
-
Exit 2: sentinel present but the section is malformed — no fenced json
|
|
1868
|
-
block, invalid JSON, or bad shape (so ideate can re-prompt).
|
|
1899
|
+
"""Validate a spec's authoritative sibling or legacy inline carrier;
|
|
1900
|
+
return 0/2 without staging or execution. This is not generated-mode staging.
|
|
1869
1901
|
"""
|
|
1870
1902
|
if not md_path.is_file():
|
|
1871
1903
|
print(f"[spec-verify --check] error: {md_path} not found", file=sys.stderr)
|
|
@@ -1883,6 +1915,14 @@ def run_check_mode(md_path: Path) -> int:
|
|
|
1883
1915
|
if solo_ceiling_err:
|
|
1884
1916
|
print(f"[spec-verify --check] {md_path}: {solo_ceiling_err}", file=sys.stderr)
|
|
1885
1917
|
return 2
|
|
1918
|
+
expected_path = md_path.with_name("spec.expected.json")
|
|
1919
|
+
if expected_path.is_file():
|
|
1920
|
+
data, err = load_expected_contract(expected_path)
|
|
1921
|
+
err = err or validate_expected_against_sibling_spec(md_path, data)
|
|
1922
|
+
if err:
|
|
1923
|
+
print(f"[spec-verify --check] {md_path}: sibling {expected_path}: {err}", file=sys.stderr)
|
|
1924
|
+
return 2
|
|
1925
|
+
return 0
|
|
1886
1926
|
section_found, block = extract_verification_block(text)
|
|
1887
1927
|
if not section_found:
|
|
1888
1928
|
# Sentinel absent entirely — opt-in nature preserved for ideate (a
|
|
@@ -2060,7 +2100,7 @@ def run_self_test() -> int:
|
|
|
2060
2100
|
+ "\n```\n",
|
|
2061
2101
|
encoding="utf-8",
|
|
2062
2102
|
)
|
|
2063
|
-
staged, inline_error = stage_from_source(
|
|
2103
|
+
_found, staged, inline_error = stage_from_source(
|
|
2064
2104
|
inline_timeout_spec, inline_timeout_devlyn
|
|
2065
2105
|
)
|
|
2066
2106
|
if staged or not inline_error or "timeout_sec" not in inline_error:
|
|
@@ -2080,7 +2120,7 @@ def run_self_test() -> int:
|
|
|
2080
2120
|
+ "\n```\n",
|
|
2081
2121
|
encoding="utf-8",
|
|
2082
2122
|
)
|
|
2083
|
-
staged, inline_error = stage_from_source(
|
|
2123
|
+
_found, staged, inline_error = stage_from_source(
|
|
2084
2124
|
inline_timeout_spec, inline_timeout_devlyn
|
|
2085
2125
|
)
|
|
2086
2126
|
inline_staged = loads_strict_json(
|
|
@@ -3641,6 +3681,56 @@ def run_self_test() -> int:
|
|
|
3641
3681
|
"```\n",
|
|
3642
3682
|
encoding="utf-8",
|
|
3643
3683
|
)
|
|
3684
|
+
generated_raw = generated_criteria.read_bytes()
|
|
3685
|
+
generated_criteria.unlink()
|
|
3686
|
+
missing_source_marker = generated_user / "missing-source-command-ran"
|
|
3687
|
+
for pointer, bench, phase in (
|
|
3688
|
+
({}, False, "build_gate"),
|
|
3689
|
+
({"criteria_path": None}, False, "build_gate"),
|
|
3690
|
+
({"criteria_path": ""}, False, "build_gate"),
|
|
3691
|
+
({"criteria_path": ".devlyn/criteria.generated.md"}, False, "build_gate"),
|
|
3692
|
+
({"criteria_path": str(generated_criteria)}, False, "build_gate"),
|
|
3693
|
+
({"criteria_path": str(generated_user)}, False, "build_gate"),
|
|
3694
|
+
({"criteria_path": str(generated_criteria)}, True, "build_gate"),
|
|
3695
|
+
({"criteria_path": str(generated_criteria)}, False, "verify_mechanical"),
|
|
3696
|
+
):
|
|
3697
|
+
(generated_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3698
|
+
"source": {"type": "generated", "criteria_sha256": hashlib.sha256(generated_raw).hexdigest(), **pointer},
|
|
3699
|
+
"risk_profile": {"risk_probes_enabled": True},
|
|
3700
|
+
}), encoding="utf-8")
|
|
3701
|
+
(generated_devlyn / "spec-verify.json").write_text(json.dumps({
|
|
3702
|
+
"verification_commands": [{"cmd": "printf unexpected > missing-source-command-ran"}],
|
|
3703
|
+
}), encoding="utf-8")
|
|
3704
|
+
(generated_devlyn / "spec-verify.results.json").unlink(missing_ok=True)
|
|
3705
|
+
missing_source_env = dict(os.environ, SPEC_VERIFY_PHASE=phase)
|
|
3706
|
+
if bench:
|
|
3707
|
+
missing_source_env["BENCH_WORKDIR"] = str(generated_user)
|
|
3708
|
+
missing_source_run = subprocess.run(
|
|
3709
|
+
[sys.executable, script_path, "--include-risk-probes"],
|
|
3710
|
+
cwd=generated_user, env=missing_source_env,
|
|
3711
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
3712
|
+
)
|
|
3713
|
+
if (
|
|
3714
|
+
missing_source_run.returncode != 1
|
|
3715
|
+
or "source.criteria_path" not in missing_source_run.stderr
|
|
3716
|
+
or f"declared path: {pointer.get('criteria_path')!r}" not in missing_source_run.stderr
|
|
3717
|
+
):
|
|
3718
|
+
print(f"missing generated source was not rejected: {pointer}, {bench}, {phase}: {missing_source_run.stderr}", file=sys.stderr)
|
|
3719
|
+
return 1
|
|
3720
|
+
missing_findings = [loads_strict_json(line) for line in
|
|
3721
|
+
(generated_devlyn / output_findings_name()).read_text(encoding="utf-8").splitlines()]
|
|
3722
|
+
if (
|
|
3723
|
+
len(missing_findings) != 1
|
|
3724
|
+
or missing_findings[0]["rule_id"] != "correctness.spec-verify-malformed"
|
|
3725
|
+
or missing_findings[0]["severity"] != "CRITICAL"
|
|
3726
|
+
or missing_findings[0]["file"] != ".devlyn/pipeline.state.json"
|
|
3727
|
+
or missing_findings[0]["phase"] != phase
|
|
3728
|
+
or missing_source_marker.exists()
|
|
3729
|
+
or (generated_devlyn / "spec-verify.results.json").exists() != (phase == "build_gate")
|
|
3730
|
+
):
|
|
3731
|
+
print(f"missing generated source lost its finding or executed a stale command: {missing_findings}", file=sys.stderr)
|
|
3732
|
+
return 1
|
|
3733
|
+
generated_criteria.write_bytes(generated_raw)
|
|
3644
3734
|
(generated_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3645
3735
|
"source": {"type": "generated", "criteria_path": str(generated_criteria)}
|
|
3646
3736
|
}), encoding="utf-8")
|
|
@@ -3796,6 +3886,13 @@ def run_self_test() -> int:
|
|
|
3796
3886
|
(real_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3797
3887
|
"source": {"type": "spec", "spec_path": str(real_spec)}
|
|
3798
3888
|
}), encoding="utf-8")
|
|
3889
|
+
sibling_check = subprocess.run(
|
|
3890
|
+
[sys.executable, script_path, "--check", str(real_spec)],
|
|
3891
|
+
cwd=real_user, capture_output=True, text=True, encoding="utf-8",
|
|
3892
|
+
)
|
|
3893
|
+
if sibling_check.returncode != 0 or (real_devlyn / "spec-verify.json").exists():
|
|
3894
|
+
print(f"sibling authoring failed or staged commands: {sibling_check.stderr}", file=sys.stderr)
|
|
3895
|
+
return 1
|
|
3799
3896
|
sibling_run = subprocess.run(
|
|
3800
3897
|
[sys.executable, script_path],
|
|
3801
3898
|
cwd=real_user,
|
|
@@ -3814,51 +3911,137 @@ def run_self_test() -> int:
|
|
|
3814
3911
|
pure_prose = "# Design\n\n<!-- devlyn:verification -->\n## Verification\n\n- No runtime verification commands.\n"
|
|
3815
3912
|
stale_command = {"verification_commands": [{"cmd": "printf unexpected-command"}]}
|
|
3816
3913
|
pure_inline = pure_prose + "\n```json\n" + json.dumps(stale_command) + "\n```\n"
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
("pure-
|
|
3820
|
-
("
|
|
3821
|
-
("
|
|
3822
|
-
("
|
|
3914
|
+
pure_contract = {"pure_design": True, "verification_commands": []}
|
|
3915
|
+
for name, filename, source, contract, expected_rule, source_type in (
|
|
3916
|
+
("pure-prose", "spec.md", pure_prose, {"pure_design": True}, None, "spec"),
|
|
3917
|
+
("pure-inline", "design-notes.md", pure_inline, {"pure_design": True, "verification_commands": []}, None, "spec"),
|
|
3918
|
+
("pure-bad-inline", "design-notes.md", pure_prose + "\n```json\n{broken\n```\n", {"pure_design": True}, None, "spec"),
|
|
3919
|
+
("empty-runtime", "design-notes.md", pure_inline, {"verification_commands": []}, "correctness.spec-verify-malformed", "spec"),
|
|
3920
|
+
("contradictory-pure", "spec.md", pure_inline, {"pure_design": True, **stale_command}, "correctness.spec-verify-malformed", "spec"),
|
|
3921
|
+
("pure-required-file", "spec.md", pure_inline, {"pure_design": True, "required_files": ["missing.md"]}, "correctness.required-file-missing", "spec"),
|
|
3922
|
+
("generated-pure", ".devlyn/criteria.generated.md", pure_contract, None, None, "generated"),
|
|
3923
|
+
("legacy-inline-pure", "design-notes.md", pure_contract, None, None, "spec"),
|
|
3924
|
+
("generated-empty-section", ".devlyn/criteria.generated.md", pure_prose, None, "correctness.spec-verify-malformed", "generated"),
|
|
3925
|
+
("generated-unmarked-empty", ".devlyn/criteria.generated.md", {"verification_commands": []}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3926
|
+
("generated-false-empty", ".devlyn/criteria.generated.md", {**pure_contract, "pure_design": False}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3927
|
+
("generated-missing-list", ".devlyn/criteria.generated.md", {"pure_design": True}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3928
|
+
("generated-nonboolean-pure", ".devlyn/criteria.generated.md", {**pure_contract, "pure_design": 1}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3929
|
+
("generated-contradictory-pure", ".devlyn/criteria.generated.md", {**stale_command, "pure_design": True}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3930
|
+
("generated-unsupported-field", ".devlyn/criteria.generated.md", {**pure_contract, "required_files": ["missing.md"]}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3823
3931
|
):
|
|
3824
3932
|
case_root = work / name
|
|
3825
3933
|
case_root.mkdir()
|
|
3826
3934
|
case_devlyn = case_root / ".devlyn"
|
|
3827
3935
|
case_devlyn.mkdir()
|
|
3828
3936
|
case_spec = case_root / filename
|
|
3937
|
+
if isinstance(source, dict):
|
|
3938
|
+
source = pure_prose + "\n```json\n" + json.dumps(source) + "\n```\n"
|
|
3829
3939
|
case_spec.write_text(source, encoding="utf-8")
|
|
3830
|
-
|
|
3940
|
+
if contract is not None:
|
|
3941
|
+
(case_root / "spec.expected.json").write_text(json.dumps(contract), encoding="utf-8")
|
|
3831
3942
|
(case_devlyn / "spec-verify.json").write_text(json.dumps(stale_command), encoding="utf-8")
|
|
3943
|
+
pointer = "criteria" if source_type == "generated" else "spec"
|
|
3832
3944
|
(case_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3833
|
-
"source": {"type":
|
|
3945
|
+
"source": {"type": source_type, pointer + "_path": str(case_spec),
|
|
3946
|
+
pointer + "_sha256": hashlib.sha256(case_spec.read_bytes()).hexdigest()},
|
|
3834
3947
|
}), encoding="utf-8")
|
|
3948
|
+
case_check = subprocess.run(
|
|
3949
|
+
[sys.executable, script_path, "--check", str(case_spec)],
|
|
3950
|
+
cwd=case_root, capture_output=True, text=True, encoding="utf-8",
|
|
3951
|
+
)
|
|
3952
|
+
if case_check.returncode != (2 if expected_rule == "correctness.spec-verify-malformed" else 0):
|
|
3953
|
+
print(f"{name}: authoring returned {case_check.returncode}: {case_check.stderr}", file=sys.stderr)
|
|
3954
|
+
return 1
|
|
3835
3955
|
case_run = subprocess.run(
|
|
3836
3956
|
[sys.executable, script_path], cwd=case_root,
|
|
3837
3957
|
capture_output=True, text=True, encoding="utf-8",
|
|
3838
3958
|
)
|
|
3839
3959
|
if case_run.returncode != (1 if expected_rule else 0):
|
|
3840
|
-
print(f"{name}:
|
|
3960
|
+
print(f"{name}: contract execution returned {case_run.returncode}: {case_run.stderr}", file=sys.stderr)
|
|
3841
3961
|
return 1
|
|
3842
3962
|
case_findings = [
|
|
3843
3963
|
loads_strict_json(line) for line in
|
|
3844
3964
|
(case_devlyn / output_findings_name()).read_text(encoding="utf-8").splitlines()
|
|
3845
3965
|
]
|
|
3846
3966
|
if [finding["rule_id"] for finding in case_findings] != ([expected_rule] if expected_rule else []):
|
|
3847
|
-
print(f"{name}: unexpected
|
|
3967
|
+
print(f"{name}: unexpected contract findings: {case_findings}", file=sys.stderr)
|
|
3848
3968
|
return 1
|
|
3849
3969
|
case_results = case_devlyn / "spec-verify.results.json"
|
|
3850
3970
|
if expected_rule == "correctness.spec-verify-malformed":
|
|
3851
|
-
|
|
3852
|
-
|
|
3971
|
+
rejected_results = loads_strict_json(case_results.read_text(encoding="utf-8"))
|
|
3972
|
+
if (rejected_results["commands"] != [] or rejected_results["process_evidence"] is not None
|
|
3973
|
+
or "preflight_failure" not in rejected_results):
|
|
3974
|
+
print(f"{name}: malformed contract reached command execution", file=sys.stderr)
|
|
3853
3975
|
return 1
|
|
3854
3976
|
elif (
|
|
3855
3977
|
loads_strict_json(case_results.read_text(encoding="utf-8"))
|
|
3856
3978
|
!= {"commands": [], "process_evidence": None}
|
|
3857
3979
|
or (case_devlyn / "spec-verify.json").exists()
|
|
3858
3980
|
):
|
|
3859
|
-
print(f"{name}: pure-design
|
|
3981
|
+
print(f"{name}: pure-design contract ran or retained a stale command", file=sys.stderr)
|
|
3982
|
+
return 1
|
|
3983
|
+
|
|
3984
|
+
generated_root = work / "generated-pure"
|
|
3985
|
+
generated_devlyn = generated_root / ".devlyn"
|
|
3986
|
+
for prestaged in (False, True):
|
|
3987
|
+
if prestaged:
|
|
3988
|
+
(generated_devlyn / "spec-verify.json").write_text(json.dumps(pure_contract), encoding="utf-8")
|
|
3989
|
+
bench_pure = subprocess.run(
|
|
3990
|
+
[sys.executable, script_path], cwd=generated_root,
|
|
3991
|
+
env=dict(os.environ, BENCH_WORKDIR=str(generated_root)),
|
|
3992
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
3993
|
+
)
|
|
3994
|
+
if bench_pure.returncode != (1 if prestaged else 0):
|
|
3995
|
+
print(f"pure-design benchmark precedence changed: {bench_pure.stderr}", file=sys.stderr)
|
|
3996
|
+
return 1
|
|
3997
|
+
if prestaged and "verification_commands must contain at least one entry" not in bench_pure.stderr:
|
|
3998
|
+
print(f"prestaged empty benchmark did not fail command validation: {bench_pure.stderr}", file=sys.stderr)
|
|
3860
3999
|
return 1
|
|
3861
4000
|
|
|
4001
|
+
generated_state_path = generated_devlyn / "pipeline.state.json"
|
|
4002
|
+
generated_state = loads_strict_json(generated_state_path.read_text(encoding="utf-8"))
|
|
4003
|
+
generated_state["risk_profile"] = {"risk_probes_enabled": True, "risk_probes_explicit": True}
|
|
4004
|
+
generated_state_path.write_text(json.dumps(generated_state), encoding="utf-8")
|
|
4005
|
+
missing_pure_probe = subprocess.run(
|
|
4006
|
+
[sys.executable, script_path, "--include-risk-probes"], cwd=generated_root,
|
|
4007
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
4008
|
+
)
|
|
4009
|
+
if missing_pure_probe.returncode != 1 or "risk probes integrity failed" not in missing_pure_probe.stderr:
|
|
4010
|
+
print(f"pure-design contract bypassed required probes: {missing_pure_probe.stderr}", file=sys.stderr)
|
|
4011
|
+
return 1
|
|
4012
|
+
|
|
4013
|
+
generated_source = generated_devlyn / "criteria.generated.md"
|
|
4014
|
+
generated_source.write_text(
|
|
4015
|
+
generated_source.read_text(encoding="utf-8") + "\n- probe must pass visible marker.\n",
|
|
4016
|
+
encoding="utf-8",
|
|
4017
|
+
)
|
|
4018
|
+
generated_state["source"]["criteria_sha256"] = hashlib.sha256(generated_source.read_bytes()).hexdigest()
|
|
4019
|
+
(generated_devlyn / "probes").mkdir()
|
|
4020
|
+
(generated_devlyn / "probes/P1.py").write_bytes(probe_script.read_bytes())
|
|
4021
|
+
(generated_devlyn / "risk-probes.jsonl").write_text(json.dumps(risk_probe_payload) + "\n", encoding="utf-8")
|
|
4022
|
+
generated_state["risk_probes_digest"], pure_probe_error = risk_probes_digest(generated_devlyn)
|
|
4023
|
+
if pure_probe_error:
|
|
4024
|
+
print(pure_probe_error, file=sys.stderr)
|
|
4025
|
+
return 1
|
|
4026
|
+
generated_state_path.write_text(json.dumps(generated_state), encoding="utf-8")
|
|
4027
|
+
valid_pure_probe = subprocess.run(
|
|
4028
|
+
[sys.executable, script_path, "--include-risk-probes"], cwd=generated_root,
|
|
4029
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
4030
|
+
)
|
|
4031
|
+
if valid_pure_probe.returncode != 0:
|
|
4032
|
+
print(f"pure-design contract did not execute valid probe: {valid_pure_probe.stderr}", file=sys.stderr)
|
|
4033
|
+
return 1
|
|
4034
|
+
pure_probe_results = loads_strict_json((generated_devlyn / "spec-verify.results.json").read_text(encoding="utf-8"))
|
|
4035
|
+
if (
|
|
4036
|
+
len(pure_probe_results["commands"]) != 1
|
|
4037
|
+
or pure_probe_results["commands"][0]["evidence_id"] != "risk-probe-0001"
|
|
4038
|
+
or not pure_probe_results["commands"][0]["pass"]
|
|
4039
|
+
or pure_probe_results["process_evidence"] is None
|
|
4040
|
+
or (generated_devlyn / "spec-verify.json").exists()
|
|
4041
|
+
):
|
|
4042
|
+
print("pure-design probe did not retain exactly one verified process result", file=sys.stderr)
|
|
4043
|
+
return 1
|
|
4044
|
+
|
|
3862
4045
|
malformed = work / "malformed-sibling"
|
|
3863
4046
|
malformed.mkdir()
|
|
3864
4047
|
malformed_devlyn = malformed / ".devlyn"
|
|
@@ -3897,7 +4080,7 @@ def run_self_test() -> int:
|
|
|
3897
4080
|
}), encoding="utf-8")
|
|
3898
4081
|
(devlyn / "spec-verify.json").write_text(json.dumps({
|
|
3899
4082
|
"verification_commands": [
|
|
3900
|
-
{"cmd": "printf bench-staged", "stdout_contains": ["bench-staged"]}
|
|
4083
|
+
{"cmd": "printf bench-staged", "stdout_contains": ["bench-staged"], "contract_refs": ["R1"]}
|
|
3901
4084
|
]
|
|
3902
4085
|
}) + "\n", encoding="utf-8")
|
|
3903
4086
|
bench_pre_staged = subprocess.run(
|
|
@@ -3916,6 +4099,38 @@ def run_self_test() -> int:
|
|
|
3916
4099
|
print("benchmark pre-staged contract was overwritten", file=sys.stderr)
|
|
3917
4100
|
return 1
|
|
3918
4101
|
|
|
4102
|
+
bench_marker = work / "bench-command-ran"
|
|
4103
|
+
bench_command = {"cmd": "printf bad > bench-command-ran; printf bad"}
|
|
4104
|
+
for phase in ("build_gate", "verify_mechanical"):
|
|
4105
|
+
for carrier, diagnostic in (
|
|
4106
|
+
({"verification_commands": [{**bench_command, "stdout_not_contians": ["bad"]}]}, "unknown key(s): stdout_not_contians"),
|
|
4107
|
+
({"verification_commands": [{**bench_command, "contract_refs": [""]}]}, "contract_refs must be a list of non-empty strings"),
|
|
4108
|
+
({"verification_commands": [bench_command], "required_files": ["missing.txt"]}, "unsupported inline key(s): required_files"),
|
|
4109
|
+
({"verification_commands": [bench_command], "pure_design": True}, "requires an explicit empty verification_commands list"),
|
|
4110
|
+
({"verification_commands": [], "pure_design": True}, "must contain at least one entry"),
|
|
4111
|
+
):
|
|
4112
|
+
(devlyn / "spec-verify.json").write_text(json.dumps(carrier), encoding="utf-8")
|
|
4113
|
+
old_bench_results = (devlyn / "spec-verify.results.json").read_bytes()
|
|
4114
|
+
rejected_bench = subprocess.run(
|
|
4115
|
+
[sys.executable, script_path], cwd=work,
|
|
4116
|
+
env={**env, "SPEC_VERIFY_PHASE": phase},
|
|
4117
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
4118
|
+
)
|
|
4119
|
+
rejected_findings = [loads_strict_json(line) for line in
|
|
4120
|
+
(devlyn / output_findings_name()).read_text(encoding="utf-8").splitlines()]
|
|
4121
|
+
if (
|
|
4122
|
+
rejected_bench.returncode != 1 or diagnostic not in rejected_bench.stderr
|
|
4123
|
+
or bench_marker.exists()
|
|
4124
|
+
or (phase == "verify_mechanical" and
|
|
4125
|
+
(devlyn / "spec-verify.results.json").read_bytes() != old_bench_results)
|
|
4126
|
+
or len(rejected_findings) != 1
|
|
4127
|
+
or rejected_findings[0]["rule_id"] != "correctness.spec-verify-malformed"
|
|
4128
|
+
or rejected_findings[0]["severity"] != "CRITICAL"
|
|
4129
|
+
or rejected_findings[0]["phase"] != phase
|
|
4130
|
+
):
|
|
4131
|
+
print(f"benchmark {phase} failed closed incorrectly for {carrier}: {rejected_bench.stderr}", file=sys.stderr)
|
|
4132
|
+
return 1
|
|
4133
|
+
|
|
3919
4134
|
verify_output = work / "verify-output"
|
|
3920
4135
|
verify_output.mkdir()
|
|
3921
4136
|
verify_devlyn = verify_output / ".devlyn"
|
|
@@ -4551,6 +4766,84 @@ def run_self_test() -> int:
|
|
|
4551
4766
|
print("atomic_batch_state without success-order evidence was accepted", file=sys.stderr)
|
|
4552
4767
|
return 1
|
|
4553
4768
|
|
|
4769
|
+
# Literal diff paths must survive Git quoting, renames and both consumers.
|
|
4770
|
+
literal_root = work / "literal-paths"
|
|
4771
|
+
literal_root.mkdir()
|
|
4772
|
+
literal_devlyn = literal_root / ".devlyn"
|
|
4773
|
+
literal_devlyn.mkdir()
|
|
4774
|
+
(literal_root / ".gitignore").write_text(".devlyn/\n", encoding="utf-8")
|
|
4775
|
+
literal_names = ["plain.txt", "설정.txt", "my file.txt"]
|
|
4776
|
+
if os.name != "nt":
|
|
4777
|
+
literal_names += ["a\tb.txt", "a\nb.txt", " leading.txt", "trailing.txt ", 'quote".txt', "back\\slash.txt", "literal*.txt"]
|
|
4778
|
+
for name in literal_names + ["old name.txt"]:
|
|
4779
|
+
(literal_root / name).write_text("original\n", encoding="utf-8")
|
|
4780
|
+
subprocess.run(["git", "init", "-q"], cwd=literal_root, check=True)
|
|
4781
|
+
subprocess.run(["git", "add", "-A"], cwd=literal_root, check=True)
|
|
4782
|
+
subprocess.run(["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "base"], cwd=literal_root, check=True)
|
|
4783
|
+
for name in literal_names:
|
|
4784
|
+
(literal_root / name).write_text("updated\n", encoding="utf-8")
|
|
4785
|
+
subprocess.run(["git", "mv", "old name.txt", "new name.txt"], cwd=literal_root, check=True)
|
|
4786
|
+
literal_names += ["old name.txt", "new name.txt"]
|
|
4787
|
+
literal_state = {"base_ref": {"sha": "HEAD"}}
|
|
4788
|
+
literal_expected = {"forbidden_files": literal_names}
|
|
4789
|
+
(literal_devlyn / "untracked.baseline").write_text("", encoding="utf-8")
|
|
4790
|
+
(literal_devlyn / "plan.md").write_text(
|
|
4791
|
+
"<!-- devlyn:authorized-surface -->\n## Files to touch\n\n```json\n"
|
|
4792
|
+
+ json.dumps({"authorized_surface": literal_names}) + "\n```\n", encoding="utf-8",
|
|
4793
|
+
)
|
|
4794
|
+
literal_patch = subprocess.check_output(
|
|
4795
|
+
["git", "diff", "--binary", "--src-prefix=a/", "--dst-prefix=b/", "HEAD"], cwd=literal_root,
|
|
4796
|
+
)
|
|
4797
|
+
literal_external = literal_devlyn / "external-diff.patch"
|
|
4798
|
+
for external in (False, True):
|
|
4799
|
+
if external:
|
|
4800
|
+
literal_external.write_bytes(literal_patch)
|
|
4801
|
+
hits, _ = expected_contract_findings(literal_expected, None, literal_root, literal_devlyn, literal_state, 1)
|
|
4802
|
+
assert len(hits) == len(literal_names) and all(hit["rule_id"] == "scope.forbidden-file-touched" for hit in hits), hits
|
|
4803
|
+
scope_hits, _ = authorized_surface_findings(literal_root, literal_devlyn, literal_state, 1)
|
|
4804
|
+
assert not scope_hits, scope_hits
|
|
4805
|
+
edit = b"diff --git a/plain.txt b/plain.txt\n--- a/plain.txt\n+++ b/plain.txt\n@@ -1 +1 @@\n-original\n+updated\n"
|
|
4806
|
+
(literal_devlyn / "plan.md").write_text(
|
|
4807
|
+
'<!-- devlyn:authorized-surface -->\n## Files to touch\n\n```json\n{"authorized_surface":["untouched.txt"]}\n```\n', encoding="utf-8",
|
|
4808
|
+
)
|
|
4809
|
+
literal_external.write_bytes(edit)
|
|
4810
|
+
scope_hits, _ = authorized_surface_findings(literal_root, literal_devlyn, literal_state, 1)
|
|
4811
|
+
assert len(scope_hits) == 1 and scope_hits[0]["rule_id"] == "scope.out-of-scope-file", scope_hits
|
|
4812
|
+
literal_external.write_bytes(edit.replace(b"+updated", b"+updated "))
|
|
4813
|
+
subprocess.run(["git", "config", "apply.whitespace", "error"], cwd=literal_root, check=True)
|
|
4814
|
+
try:
|
|
4815
|
+
names, error = changed_files(literal_root, literal_state, literal_devlyn)
|
|
4816
|
+
assert names == ["plain.txt"] and error is None, (names, error)
|
|
4817
|
+
finally:
|
|
4818
|
+
subprocess.run(["git", "config", "--unset", "apply.whitespace"], cwd=literal_root, check=True)
|
|
4819
|
+
for patch, paths in (
|
|
4820
|
+
(b"", []),
|
|
4821
|
+
(edit.replace(b"\n", b"\r\n"), ["plain.txt"]),
|
|
4822
|
+
(b"diff --git a/logo.png b/logo.png\nindex 1111111..2222222 100644\nBinary files a/logo.png and b/logo.png differ\n", ["logo.png"]),
|
|
4823
|
+
(b"diff --git a/script b/script\nold mode 100644\nnew mode 100755\n", ["script"]),
|
|
4824
|
+
(b"diff --git a/keep.py b/new.py\nsimilarity index 100%\ncopy from keep.py\ncopy to new.py\n", ["new.py", "keep.py"]),
|
|
4825
|
+
):
|
|
4826
|
+
literal_external.write_bytes(patch)
|
|
4827
|
+
names, error = changed_files(literal_root, literal_state, literal_devlyn)
|
|
4828
|
+
assert names == paths and error is None, (names, error)
|
|
4829
|
+
for bad_patch in (
|
|
4830
|
+
b"not a patch\n", b"\n", edit[:-5],
|
|
4831
|
+
edit.replace(b"a/plain.txt", b"plain.txt").replace(b"b/plain.txt", b"plain.txt"),
|
|
4832
|
+
edit.replace(b"a/plain.txt", b"src/plain.txt").replace(b"b/plain.txt", b"src/plain.txt"),
|
|
4833
|
+
edit.replace(b"a/plain.txt", b"i/plain.txt").replace(b"b/plain.txt", b"w/plain.txt"),
|
|
4834
|
+
):
|
|
4835
|
+
literal_external.write_bytes(bad_patch)
|
|
4836
|
+
hits, _ = expected_contract_findings(literal_expected, None, literal_root, literal_devlyn, literal_state, 1)
|
|
4837
|
+
assert len(hits) == 1 and hits[0]["rule_id"] == "correctness.expected-contract-unverifiable", hits
|
|
4838
|
+
scope_hits, _ = authorized_surface_findings(literal_root, literal_devlyn, literal_state, 1)
|
|
4839
|
+
assert len(scope_hits) == 1 and scope_hits[0]["file"] == ".devlyn/external-diff.patch", scope_hits
|
|
4840
|
+
literal_external.unlink()
|
|
4841
|
+
names, error = changed_files(literal_root, {"base_ref": {"sha": "missing-literal-ref"}}, literal_devlyn)
|
|
4842
|
+
assert not names and error, (names, error)
|
|
4843
|
+
assert subprocess.check_output(
|
|
4844
|
+
["git", "diff", "--binary", "--src-prefix=a/", "--dst-prefix=b/", "HEAD"], cwd=literal_root,
|
|
4845
|
+
) == literal_patch
|
|
4846
|
+
|
|
4554
4847
|
# iter-0046: PLAN-declared authorized_surface enforced at BUILD_GATE.
|
|
4555
4848
|
scope_root = work / "scope-gate"
|
|
4556
4849
|
scope_root.mkdir()
|
|
@@ -4965,7 +5258,8 @@ def main() -> int:
|
|
|
4965
5258
|
# continue to legacy source-extract.
|
|
4966
5259
|
# 3. Source-extract reads
|
|
4967
5260
|
# `pipeline.state.json:source.{spec_path | criteria_path}`. If it has
|
|
4968
|
-
# a json block,
|
|
5261
|
+
# a json block, stage its commands or clear stale staging for an
|
|
5262
|
+
# explicit pure-design contract.
|
|
4969
5263
|
# 4. If source has no json block AND source.type=="generated":
|
|
4970
5264
|
# CRITICAL spec-verify-malformed — generated criteria must ship a
|
|
4971
5265
|
# verifiable contract per the generated-criteria output contract.
|
|
@@ -4980,6 +5274,39 @@ def main() -> int:
|
|
|
4980
5274
|
trust_bench_staged = bench_mode and pre_staged
|
|
4981
5275
|
src_type, source_md = read_source(work, devlyn_dir)
|
|
4982
5276
|
state = read_state(devlyn_dir)
|
|
5277
|
+
|
|
5278
|
+
def reject_preflight() -> int:
|
|
5279
|
+
# A completed rejection is not an interrupted checker. Preserve actual
|
|
5280
|
+
# earlier gate observations without inventing an executed command.
|
|
5281
|
+
if validate_risk_probes_only or output_phase() != "build_gate":
|
|
5282
|
+
return 1
|
|
5283
|
+
runner = process_evidence_module()
|
|
5284
|
+
phase, run_id, round_, relative = mechanical_evidence_identity(state, runner)
|
|
5285
|
+
carrier = None
|
|
5286
|
+
try:
|
|
5287
|
+
if os.path.lexists(work / relative):
|
|
5288
|
+
carrier = runner.validate_manifest(
|
|
5289
|
+
work, relative, run_id, phase, round_, require_expectations=False,
|
|
5290
|
+
)
|
|
5291
|
+
findings = devlyn_dir / output_findings_name()
|
|
5292
|
+
result = {
|
|
5293
|
+
"commands": runner.bound_carrier_summary_commands(work, carrier) if carrier else [],
|
|
5294
|
+
"process_evidence": carrier,
|
|
5295
|
+
"preflight_failure": {
|
|
5296
|
+
"run_id": run_id, "phase": phase, "round": round_,
|
|
5297
|
+
"findings": {
|
|
5298
|
+
"path": findings.relative_to(work).as_posix(),
|
|
5299
|
+
"sha256": hashlib.sha256(findings.read_bytes()).hexdigest(),
|
|
5300
|
+
},
|
|
5301
|
+
},
|
|
5302
|
+
}
|
|
5303
|
+
(devlyn_dir / "spec-verify.results.json").write_text(
|
|
5304
|
+
json.dumps(result, indent=2) + "\n", encoding="utf-8",
|
|
5305
|
+
)
|
|
5306
|
+
except (runner.EvidenceError, OSError, UnicodeError, ValueError) as exc:
|
|
5307
|
+
print(f"[spec-verify] rejection evidence could not be finalized: {exc}", file=sys.stderr)
|
|
5308
|
+
return 1
|
|
5309
|
+
|
|
4983
5310
|
external_diff = devlyn_dir / "external-diff.patch"
|
|
4984
5311
|
if external_diff.is_file() and state.get("mode") != "verify-only":
|
|
4985
5312
|
error = (
|
|
@@ -4997,14 +5324,15 @@ def main() -> int:
|
|
|
4997
5324
|
"intentionally verifying an external patch."
|
|
4998
5325
|
),
|
|
4999
5326
|
)
|
|
5000
|
-
return
|
|
5327
|
+
return reject_preflight()
|
|
5001
5328
|
integrity_error = source_integrity_error(src_type, state, source_md)
|
|
5002
5329
|
if integrity_error:
|
|
5003
5330
|
print(f"[spec-verify] carrier malformed: {integrity_error}", file=sys.stderr)
|
|
5004
5331
|
write_malformed_finding(devlyn_dir, integrity_error, source_md)
|
|
5005
|
-
return
|
|
5332
|
+
return reject_preflight()
|
|
5006
5333
|
expected_data: dict | None = None
|
|
5007
5334
|
expected_path: Path | None = None
|
|
5335
|
+
contract_found = False
|
|
5008
5336
|
if validate_risk_probes_only:
|
|
5009
5337
|
_risk_probes, risk_error = load_risk_probes(
|
|
5010
5338
|
devlyn_dir, source_md, require_present=True
|
|
@@ -5012,29 +5340,29 @@ def main() -> int:
|
|
|
5012
5340
|
if risk_error:
|
|
5013
5341
|
print(f"[spec-verify] risk probes malformed: {risk_error}", file=sys.stderr)
|
|
5014
5342
|
write_malformed_finding(devlyn_dir, risk_error, devlyn_dir / "risk-probes.jsonl")
|
|
5015
|
-
return
|
|
5343
|
+
return reject_preflight()
|
|
5016
5344
|
print("[spec-verify] risk probes valid", file=sys.stderr)
|
|
5017
5345
|
return 0
|
|
5018
5346
|
if source_md is not None and not trust_bench_staged:
|
|
5019
5347
|
if src_type == "spec":
|
|
5020
|
-
|
|
5348
|
+
contract_found, _staged, expected_error, expected_path, expected_data = stage_from_expected(
|
|
5021
5349
|
source_md, devlyn_dir
|
|
5022
5350
|
)
|
|
5023
5351
|
if expected_error is not None:
|
|
5024
5352
|
print(f"[spec-verify] carrier malformed: {expected_error}", file=sys.stderr)
|
|
5025
5353
|
write_malformed_finding(devlyn_dir, expected_error, expected_path)
|
|
5026
|
-
return
|
|
5027
|
-
if
|
|
5028
|
-
|
|
5354
|
+
return reject_preflight()
|
|
5355
|
+
if contract_found:
|
|
5356
|
+
error = None
|
|
5029
5357
|
else:
|
|
5030
|
-
|
|
5358
|
+
contract_found, _staged, error = stage_from_source(source_md, devlyn_dir)
|
|
5031
5359
|
else:
|
|
5032
|
-
|
|
5360
|
+
contract_found, _staged, error = stage_from_source(source_md, devlyn_dir)
|
|
5033
5361
|
if error is not None:
|
|
5034
5362
|
print(f"[spec-verify] carrier malformed: {error}", file=sys.stderr)
|
|
5035
5363
|
write_malformed_finding(devlyn_dir, error, source_md)
|
|
5036
|
-
return
|
|
5037
|
-
if not
|
|
5364
|
+
return reject_preflight()
|
|
5365
|
+
if not contract_found:
|
|
5038
5366
|
if src_type == "generated":
|
|
5039
5367
|
msg = (
|
|
5040
5368
|
f"generated {source_md.name} must include a "
|
|
@@ -5043,9 +5371,9 @@ def main() -> int:
|
|
|
5043
5371
|
)
|
|
5044
5372
|
print(f"[spec-verify] {msg}", file=sys.stderr)
|
|
5045
5373
|
write_malformed_finding(devlyn_dir, msg, source_md)
|
|
5046
|
-
return
|
|
5374
|
+
return reject_preflight()
|
|
5047
5375
|
# source.type=="spec", no block in spec markdown.
|
|
5048
|
-
if not bench_mode
|
|
5376
|
+
if not bench_mode:
|
|
5049
5377
|
# Real-user handwritten spec: silent no-op. Drop any stale
|
|
5050
5378
|
# pre-staged file so a killed prior run cannot poison this
|
|
5051
5379
|
# run's gate.
|
|
@@ -5067,11 +5395,10 @@ def main() -> int:
|
|
|
5067
5395
|
|
|
5068
5396
|
commands: list[dict] = []
|
|
5069
5397
|
if not spec_path.exists():
|
|
5070
|
-
#
|
|
5071
|
-
#
|
|
5072
|
-
#
|
|
5073
|
-
|
|
5074
|
-
if expected_data is None:
|
|
5398
|
+
# A declared pure-design contract continues through probes/results.
|
|
5399
|
+
# Missing handwritten/benchmark contracts remain an opt-in no-op;
|
|
5400
|
+
# missing generated contracts were rejected above.
|
|
5401
|
+
if not contract_found:
|
|
5075
5402
|
return 0
|
|
5076
5403
|
else:
|
|
5077
5404
|
try:
|
|
@@ -5080,27 +5407,26 @@ def main() -> int:
|
|
|
5080
5407
|
print(f"[spec-verify] error: cannot parse {spec_path}: {e}", file=sys.stderr)
|
|
5081
5408
|
return 2
|
|
5082
5409
|
|
|
5083
|
-
#
|
|
5084
|
-
#
|
|
5085
|
-
#
|
|
5086
|
-
|
|
5087
|
-
shape_err = validate_shape(spec)
|
|
5410
|
+
# Pre-staged commands need the strict carrier vocabulary too: unknown
|
|
5411
|
+
# expectations were silently ignored. Keep the nonempty shape gate;
|
|
5412
|
+
# a benchmark's staged empty list must not become a pure-design pass.
|
|
5413
|
+
shape_err = validate_shape(spec) or validate_inline_shape(spec)
|
|
5088
5414
|
if shape_err:
|
|
5089
5415
|
print(f"[spec-verify] error: {spec_path}: {shape_err}", file=sys.stderr)
|
|
5090
5416
|
write_malformed_finding(devlyn_dir, f"{spec_path}: {shape_err}", None)
|
|
5091
|
-
return
|
|
5417
|
+
return reject_preflight()
|
|
5092
5418
|
commands = list(spec["verification_commands"])
|
|
5093
5419
|
if include_risk_probes:
|
|
5094
5420
|
risk_state_error = risk_probes_state_error(state)
|
|
5095
5421
|
if risk_state_error:
|
|
5096
5422
|
print(f"[spec-verify] risk probes malformed: {risk_state_error}", file=sys.stderr)
|
|
5097
5423
|
write_malformed_finding(devlyn_dir, risk_state_error, Path("pipeline.state.json"))
|
|
5098
|
-
return
|
|
5424
|
+
return reject_preflight()
|
|
5099
5425
|
integrity_error = risk_probe_integrity_error(state, devlyn_dir)
|
|
5100
5426
|
if integrity_error:
|
|
5101
5427
|
print(f"[spec-verify] risk probes integrity failed: {integrity_error}", file=sys.stderr)
|
|
5102
5428
|
write_risk_probe_integrity_finding(devlyn_dir, integrity_error)
|
|
5103
|
-
return
|
|
5429
|
+
return reject_preflight()
|
|
5104
5430
|
risk_probes, risk_error = load_risk_probes(
|
|
5105
5431
|
devlyn_dir,
|
|
5106
5432
|
source_md,
|
|
@@ -5109,7 +5435,7 @@ def main() -> int:
|
|
|
5109
5435
|
if risk_error:
|
|
5110
5436
|
print(f"[spec-verify] risk probes malformed: {risk_error}", file=sys.stderr)
|
|
5111
5437
|
write_malformed_finding(devlyn_dir, risk_error, devlyn_dir / "risk-probes.jsonl")
|
|
5112
|
-
return
|
|
5438
|
+
return reject_preflight()
|
|
5113
5439
|
commands.extend(risk_probes)
|
|
5114
5440
|
|
|
5115
5441
|
devlyn_dir.mkdir(parents=True, exist_ok=True)
|