devlyn-cli 3.1.2 → 3.1.3
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 +20 -5
- package/bin/devlyn.js +10 -52
- package/bin/instruction-templates.json +52 -0
- package/bin/instructions.js +137 -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/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/spec-verify-check.py +424 -97
- package/config/skills/_shared/state-phase-write.py +202 -32
- package/config/skills/_shared/task-complete.py +9 -1
- 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 +4 -4
- package/config/skills/devlyn:resolve/references/free-form-mode.md +2 -2
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +3 -2
- package/config/skills/devlyn:resolve/references/state-schema.md +1 -1
- package/package.json +1 -1
- package/scripts/lint-skills.sh +2 -2
|
@@ -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
|
|
@@ -1770,7 +1799,16 @@ def authorized_surface_findings(
|
|
|
1770
1799
|
|
|
1771
1800
|
findings: list[dict] = []
|
|
1772
1801
|
seq = finding_start
|
|
1773
|
-
|
|
1802
|
+
paths, paths_error = changed_files(work, state, devlyn_dir)
|
|
1803
|
+
if paths_error is not None:
|
|
1804
|
+
return ([scope_finding(
|
|
1805
|
+
finding_start,
|
|
1806
|
+
"scope.authorized-surface-malformed",
|
|
1807
|
+
f"Cannot read changed files: {paths_error}",
|
|
1808
|
+
".devlyn/external-diff.patch" if (devlyn_dir / "external-diff.patch").is_file() else ".devlyn/pipeline.state.json",
|
|
1809
|
+
"Ensure base_ref.sha is valid and any external-diff.patch is a readable Git patch with a/ and b/ prefixes.",
|
|
1810
|
+
)], finding_start + 1)
|
|
1811
|
+
for path in paths:
|
|
1774
1812
|
if path_matches_surface(path, surface):
|
|
1775
1813
|
continue
|
|
1776
1814
|
findings.append(scope_finding(
|
|
@@ -1859,13 +1897,8 @@ def run_write_untracked_baseline(work: Path, devlyn_dir: Path) -> int:
|
|
|
1859
1897
|
|
|
1860
1898
|
|
|
1861
1899
|
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).
|
|
1900
|
+
"""Validate a spec's authoritative sibling or legacy inline carrier;
|
|
1901
|
+
return 0/2 without staging or execution. This is not generated-mode staging.
|
|
1869
1902
|
"""
|
|
1870
1903
|
if not md_path.is_file():
|
|
1871
1904
|
print(f"[spec-verify --check] error: {md_path} not found", file=sys.stderr)
|
|
@@ -1883,6 +1916,14 @@ def run_check_mode(md_path: Path) -> int:
|
|
|
1883
1916
|
if solo_ceiling_err:
|
|
1884
1917
|
print(f"[spec-verify --check] {md_path}: {solo_ceiling_err}", file=sys.stderr)
|
|
1885
1918
|
return 2
|
|
1919
|
+
expected_path = md_path.with_name("spec.expected.json")
|
|
1920
|
+
if expected_path.is_file():
|
|
1921
|
+
data, err = load_expected_contract(expected_path)
|
|
1922
|
+
err = err or validate_expected_against_sibling_spec(md_path, data)
|
|
1923
|
+
if err:
|
|
1924
|
+
print(f"[spec-verify --check] {md_path}: sibling {expected_path}: {err}", file=sys.stderr)
|
|
1925
|
+
return 2
|
|
1926
|
+
return 0
|
|
1886
1927
|
section_found, block = extract_verification_block(text)
|
|
1887
1928
|
if not section_found:
|
|
1888
1929
|
# Sentinel absent entirely — opt-in nature preserved for ideate (a
|
|
@@ -2060,7 +2101,7 @@ def run_self_test() -> int:
|
|
|
2060
2101
|
+ "\n```\n",
|
|
2061
2102
|
encoding="utf-8",
|
|
2062
2103
|
)
|
|
2063
|
-
staged, inline_error = stage_from_source(
|
|
2104
|
+
_found, staged, inline_error = stage_from_source(
|
|
2064
2105
|
inline_timeout_spec, inline_timeout_devlyn
|
|
2065
2106
|
)
|
|
2066
2107
|
if staged or not inline_error or "timeout_sec" not in inline_error:
|
|
@@ -2080,7 +2121,7 @@ def run_self_test() -> int:
|
|
|
2080
2121
|
+ "\n```\n",
|
|
2081
2122
|
encoding="utf-8",
|
|
2082
2123
|
)
|
|
2083
|
-
staged, inline_error = stage_from_source(
|
|
2124
|
+
_found, staged, inline_error = stage_from_source(
|
|
2084
2125
|
inline_timeout_spec, inline_timeout_devlyn
|
|
2085
2126
|
)
|
|
2086
2127
|
inline_staged = loads_strict_json(
|
|
@@ -3641,6 +3682,56 @@ def run_self_test() -> int:
|
|
|
3641
3682
|
"```\n",
|
|
3642
3683
|
encoding="utf-8",
|
|
3643
3684
|
)
|
|
3685
|
+
generated_raw = generated_criteria.read_bytes()
|
|
3686
|
+
generated_criteria.unlink()
|
|
3687
|
+
missing_source_marker = generated_user / "missing-source-command-ran"
|
|
3688
|
+
for pointer, bench, phase in (
|
|
3689
|
+
({}, False, "build_gate"),
|
|
3690
|
+
({"criteria_path": None}, False, "build_gate"),
|
|
3691
|
+
({"criteria_path": ""}, False, "build_gate"),
|
|
3692
|
+
({"criteria_path": ".devlyn/criteria.generated.md"}, False, "build_gate"),
|
|
3693
|
+
({"criteria_path": str(generated_criteria)}, False, "build_gate"),
|
|
3694
|
+
({"criteria_path": str(generated_user)}, False, "build_gate"),
|
|
3695
|
+
({"criteria_path": str(generated_criteria)}, True, "build_gate"),
|
|
3696
|
+
({"criteria_path": str(generated_criteria)}, False, "verify_mechanical"),
|
|
3697
|
+
):
|
|
3698
|
+
(generated_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3699
|
+
"source": {"type": "generated", "criteria_sha256": hashlib.sha256(generated_raw).hexdigest(), **pointer},
|
|
3700
|
+
"risk_profile": {"risk_probes_enabled": True},
|
|
3701
|
+
}), encoding="utf-8")
|
|
3702
|
+
(generated_devlyn / "spec-verify.json").write_text(json.dumps({
|
|
3703
|
+
"verification_commands": [{"cmd": "printf unexpected > missing-source-command-ran"}],
|
|
3704
|
+
}), encoding="utf-8")
|
|
3705
|
+
(generated_devlyn / "spec-verify.results.json").unlink(missing_ok=True)
|
|
3706
|
+
missing_source_env = dict(os.environ, SPEC_VERIFY_PHASE=phase)
|
|
3707
|
+
if bench:
|
|
3708
|
+
missing_source_env["BENCH_WORKDIR"] = str(generated_user)
|
|
3709
|
+
missing_source_run = subprocess.run(
|
|
3710
|
+
[sys.executable, script_path, "--include-risk-probes"],
|
|
3711
|
+
cwd=generated_user, env=missing_source_env,
|
|
3712
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
3713
|
+
)
|
|
3714
|
+
if (
|
|
3715
|
+
missing_source_run.returncode != 1
|
|
3716
|
+
or "source.criteria_path" not in missing_source_run.stderr
|
|
3717
|
+
or f"declared path: {pointer.get('criteria_path')!r}" not in missing_source_run.stderr
|
|
3718
|
+
):
|
|
3719
|
+
print(f"missing generated source was not rejected: {pointer}, {bench}, {phase}: {missing_source_run.stderr}", file=sys.stderr)
|
|
3720
|
+
return 1
|
|
3721
|
+
missing_findings = [loads_strict_json(line) for line in
|
|
3722
|
+
(generated_devlyn / output_findings_name()).read_text(encoding="utf-8").splitlines()]
|
|
3723
|
+
if (
|
|
3724
|
+
len(missing_findings) != 1
|
|
3725
|
+
or missing_findings[0]["rule_id"] != "correctness.spec-verify-malformed"
|
|
3726
|
+
or missing_findings[0]["severity"] != "CRITICAL"
|
|
3727
|
+
or missing_findings[0]["file"] != ".devlyn/pipeline.state.json"
|
|
3728
|
+
or missing_findings[0]["phase"] != phase
|
|
3729
|
+
or missing_source_marker.exists()
|
|
3730
|
+
or (generated_devlyn / "spec-verify.results.json").exists() != (phase == "build_gate")
|
|
3731
|
+
):
|
|
3732
|
+
print(f"missing generated source lost its finding or executed a stale command: {missing_findings}", file=sys.stderr)
|
|
3733
|
+
return 1
|
|
3734
|
+
generated_criteria.write_bytes(generated_raw)
|
|
3644
3735
|
(generated_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3645
3736
|
"source": {"type": "generated", "criteria_path": str(generated_criteria)}
|
|
3646
3737
|
}), encoding="utf-8")
|
|
@@ -3796,6 +3887,13 @@ def run_self_test() -> int:
|
|
|
3796
3887
|
(real_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3797
3888
|
"source": {"type": "spec", "spec_path": str(real_spec)}
|
|
3798
3889
|
}), encoding="utf-8")
|
|
3890
|
+
sibling_check = subprocess.run(
|
|
3891
|
+
[sys.executable, script_path, "--check", str(real_spec)],
|
|
3892
|
+
cwd=real_user, capture_output=True, text=True, encoding="utf-8",
|
|
3893
|
+
)
|
|
3894
|
+
if sibling_check.returncode != 0 or (real_devlyn / "spec-verify.json").exists():
|
|
3895
|
+
print(f"sibling authoring failed or staged commands: {sibling_check.stderr}", file=sys.stderr)
|
|
3896
|
+
return 1
|
|
3799
3897
|
sibling_run = subprocess.run(
|
|
3800
3898
|
[sys.executable, script_path],
|
|
3801
3899
|
cwd=real_user,
|
|
@@ -3814,51 +3912,137 @@ def run_self_test() -> int:
|
|
|
3814
3912
|
pure_prose = "# Design\n\n<!-- devlyn:verification -->\n## Verification\n\n- No runtime verification commands.\n"
|
|
3815
3913
|
stale_command = {"verification_commands": [{"cmd": "printf unexpected-command"}]}
|
|
3816
3914
|
pure_inline = pure_prose + "\n```json\n" + json.dumps(stale_command) + "\n```\n"
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
("pure-
|
|
3820
|
-
("
|
|
3821
|
-
("
|
|
3822
|
-
("
|
|
3915
|
+
pure_contract = {"pure_design": True, "verification_commands": []}
|
|
3916
|
+
for name, filename, source, contract, expected_rule, source_type in (
|
|
3917
|
+
("pure-prose", "spec.md", pure_prose, {"pure_design": True}, None, "spec"),
|
|
3918
|
+
("pure-inline", "design-notes.md", pure_inline, {"pure_design": True, "verification_commands": []}, None, "spec"),
|
|
3919
|
+
("pure-bad-inline", "design-notes.md", pure_prose + "\n```json\n{broken\n```\n", {"pure_design": True}, None, "spec"),
|
|
3920
|
+
("empty-runtime", "design-notes.md", pure_inline, {"verification_commands": []}, "correctness.spec-verify-malformed", "spec"),
|
|
3921
|
+
("contradictory-pure", "spec.md", pure_inline, {"pure_design": True, **stale_command}, "correctness.spec-verify-malformed", "spec"),
|
|
3922
|
+
("pure-required-file", "spec.md", pure_inline, {"pure_design": True, "required_files": ["missing.md"]}, "correctness.required-file-missing", "spec"),
|
|
3923
|
+
("generated-pure", ".devlyn/criteria.generated.md", pure_contract, None, None, "generated"),
|
|
3924
|
+
("legacy-inline-pure", "design-notes.md", pure_contract, None, None, "spec"),
|
|
3925
|
+
("generated-empty-section", ".devlyn/criteria.generated.md", pure_prose, None, "correctness.spec-verify-malformed", "generated"),
|
|
3926
|
+
("generated-unmarked-empty", ".devlyn/criteria.generated.md", {"verification_commands": []}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3927
|
+
("generated-false-empty", ".devlyn/criteria.generated.md", {**pure_contract, "pure_design": False}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3928
|
+
("generated-missing-list", ".devlyn/criteria.generated.md", {"pure_design": True}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3929
|
+
("generated-nonboolean-pure", ".devlyn/criteria.generated.md", {**pure_contract, "pure_design": 1}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3930
|
+
("generated-contradictory-pure", ".devlyn/criteria.generated.md", {**stale_command, "pure_design": True}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3931
|
+
("generated-unsupported-field", ".devlyn/criteria.generated.md", {**pure_contract, "required_files": ["missing.md"]}, None, "correctness.spec-verify-malformed", "generated"),
|
|
3823
3932
|
):
|
|
3824
3933
|
case_root = work / name
|
|
3825
3934
|
case_root.mkdir()
|
|
3826
3935
|
case_devlyn = case_root / ".devlyn"
|
|
3827
3936
|
case_devlyn.mkdir()
|
|
3828
3937
|
case_spec = case_root / filename
|
|
3938
|
+
if isinstance(source, dict):
|
|
3939
|
+
source = pure_prose + "\n```json\n" + json.dumps(source) + "\n```\n"
|
|
3829
3940
|
case_spec.write_text(source, encoding="utf-8")
|
|
3830
|
-
|
|
3941
|
+
if contract is not None:
|
|
3942
|
+
(case_root / "spec.expected.json").write_text(json.dumps(contract), encoding="utf-8")
|
|
3831
3943
|
(case_devlyn / "spec-verify.json").write_text(json.dumps(stale_command), encoding="utf-8")
|
|
3944
|
+
pointer = "criteria" if source_type == "generated" else "spec"
|
|
3832
3945
|
(case_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3833
|
-
"source": {"type":
|
|
3946
|
+
"source": {"type": source_type, pointer + "_path": str(case_spec),
|
|
3947
|
+
pointer + "_sha256": hashlib.sha256(case_spec.read_bytes()).hexdigest()},
|
|
3834
3948
|
}), encoding="utf-8")
|
|
3949
|
+
case_check = subprocess.run(
|
|
3950
|
+
[sys.executable, script_path, "--check", str(case_spec)],
|
|
3951
|
+
cwd=case_root, capture_output=True, text=True, encoding="utf-8",
|
|
3952
|
+
)
|
|
3953
|
+
if case_check.returncode != (2 if expected_rule == "correctness.spec-verify-malformed" else 0):
|
|
3954
|
+
print(f"{name}: authoring returned {case_check.returncode}: {case_check.stderr}", file=sys.stderr)
|
|
3955
|
+
return 1
|
|
3835
3956
|
case_run = subprocess.run(
|
|
3836
3957
|
[sys.executable, script_path], cwd=case_root,
|
|
3837
3958
|
capture_output=True, text=True, encoding="utf-8",
|
|
3838
3959
|
)
|
|
3839
3960
|
if case_run.returncode != (1 if expected_rule else 0):
|
|
3840
|
-
print(f"{name}:
|
|
3961
|
+
print(f"{name}: contract execution returned {case_run.returncode}: {case_run.stderr}", file=sys.stderr)
|
|
3841
3962
|
return 1
|
|
3842
3963
|
case_findings = [
|
|
3843
3964
|
loads_strict_json(line) for line in
|
|
3844
3965
|
(case_devlyn / output_findings_name()).read_text(encoding="utf-8").splitlines()
|
|
3845
3966
|
]
|
|
3846
3967
|
if [finding["rule_id"] for finding in case_findings] != ([expected_rule] if expected_rule else []):
|
|
3847
|
-
print(f"{name}: unexpected
|
|
3968
|
+
print(f"{name}: unexpected contract findings: {case_findings}", file=sys.stderr)
|
|
3848
3969
|
return 1
|
|
3849
3970
|
case_results = case_devlyn / "spec-verify.results.json"
|
|
3850
3971
|
if expected_rule == "correctness.spec-verify-malformed":
|
|
3851
|
-
|
|
3852
|
-
|
|
3972
|
+
rejected_results = loads_strict_json(case_results.read_text(encoding="utf-8"))
|
|
3973
|
+
if (rejected_results["commands"] != [] or rejected_results["process_evidence"] is not None
|
|
3974
|
+
or "preflight_failure" not in rejected_results):
|
|
3975
|
+
print(f"{name}: malformed contract reached command execution", file=sys.stderr)
|
|
3853
3976
|
return 1
|
|
3854
3977
|
elif (
|
|
3855
3978
|
loads_strict_json(case_results.read_text(encoding="utf-8"))
|
|
3856
3979
|
!= {"commands": [], "process_evidence": None}
|
|
3857
3980
|
or (case_devlyn / "spec-verify.json").exists()
|
|
3858
3981
|
):
|
|
3859
|
-
print(f"{name}: pure-design
|
|
3982
|
+
print(f"{name}: pure-design contract ran or retained a stale command", file=sys.stderr)
|
|
3983
|
+
return 1
|
|
3984
|
+
|
|
3985
|
+
generated_root = work / "generated-pure"
|
|
3986
|
+
generated_devlyn = generated_root / ".devlyn"
|
|
3987
|
+
for prestaged in (False, True):
|
|
3988
|
+
if prestaged:
|
|
3989
|
+
(generated_devlyn / "spec-verify.json").write_text(json.dumps(pure_contract), encoding="utf-8")
|
|
3990
|
+
bench_pure = subprocess.run(
|
|
3991
|
+
[sys.executable, script_path], cwd=generated_root,
|
|
3992
|
+
env=dict(os.environ, BENCH_WORKDIR=str(generated_root)),
|
|
3993
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
3994
|
+
)
|
|
3995
|
+
if bench_pure.returncode != (1 if prestaged else 0):
|
|
3996
|
+
print(f"pure-design benchmark precedence changed: {bench_pure.stderr}", file=sys.stderr)
|
|
3997
|
+
return 1
|
|
3998
|
+
if prestaged and "verification_commands must contain at least one entry" not in bench_pure.stderr:
|
|
3999
|
+
print(f"prestaged empty benchmark did not fail command validation: {bench_pure.stderr}", file=sys.stderr)
|
|
3860
4000
|
return 1
|
|
3861
4001
|
|
|
4002
|
+
generated_state_path = generated_devlyn / "pipeline.state.json"
|
|
4003
|
+
generated_state = loads_strict_json(generated_state_path.read_text(encoding="utf-8"))
|
|
4004
|
+
generated_state["risk_profile"] = {"risk_probes_enabled": True, "risk_probes_explicit": True}
|
|
4005
|
+
generated_state_path.write_text(json.dumps(generated_state), encoding="utf-8")
|
|
4006
|
+
missing_pure_probe = subprocess.run(
|
|
4007
|
+
[sys.executable, script_path, "--include-risk-probes"], cwd=generated_root,
|
|
4008
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
4009
|
+
)
|
|
4010
|
+
if missing_pure_probe.returncode != 1 or "risk probes integrity failed" not in missing_pure_probe.stderr:
|
|
4011
|
+
print(f"pure-design contract bypassed required probes: {missing_pure_probe.stderr}", file=sys.stderr)
|
|
4012
|
+
return 1
|
|
4013
|
+
|
|
4014
|
+
generated_source = generated_devlyn / "criteria.generated.md"
|
|
4015
|
+
generated_source.write_text(
|
|
4016
|
+
generated_source.read_text(encoding="utf-8") + "\n- probe must pass visible marker.\n",
|
|
4017
|
+
encoding="utf-8",
|
|
4018
|
+
)
|
|
4019
|
+
generated_state["source"]["criteria_sha256"] = hashlib.sha256(generated_source.read_bytes()).hexdigest()
|
|
4020
|
+
(generated_devlyn / "probes").mkdir()
|
|
4021
|
+
(generated_devlyn / "probes/P1.py").write_bytes(probe_script.read_bytes())
|
|
4022
|
+
(generated_devlyn / "risk-probes.jsonl").write_text(json.dumps(risk_probe_payload) + "\n", encoding="utf-8")
|
|
4023
|
+
generated_state["risk_probes_digest"], pure_probe_error = risk_probes_digest(generated_devlyn)
|
|
4024
|
+
if pure_probe_error:
|
|
4025
|
+
print(pure_probe_error, file=sys.stderr)
|
|
4026
|
+
return 1
|
|
4027
|
+
generated_state_path.write_text(json.dumps(generated_state), encoding="utf-8")
|
|
4028
|
+
valid_pure_probe = subprocess.run(
|
|
4029
|
+
[sys.executable, script_path, "--include-risk-probes"], cwd=generated_root,
|
|
4030
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
4031
|
+
)
|
|
4032
|
+
if valid_pure_probe.returncode != 0:
|
|
4033
|
+
print(f"pure-design contract did not execute valid probe: {valid_pure_probe.stderr}", file=sys.stderr)
|
|
4034
|
+
return 1
|
|
4035
|
+
pure_probe_results = loads_strict_json((generated_devlyn / "spec-verify.results.json").read_text(encoding="utf-8"))
|
|
4036
|
+
if (
|
|
4037
|
+
len(pure_probe_results["commands"]) != 1
|
|
4038
|
+
or pure_probe_results["commands"][0]["evidence_id"] != "risk-probe-0001"
|
|
4039
|
+
or not pure_probe_results["commands"][0]["pass"]
|
|
4040
|
+
or pure_probe_results["process_evidence"] is None
|
|
4041
|
+
or (generated_devlyn / "spec-verify.json").exists()
|
|
4042
|
+
):
|
|
4043
|
+
print("pure-design probe did not retain exactly one verified process result", file=sys.stderr)
|
|
4044
|
+
return 1
|
|
4045
|
+
|
|
3862
4046
|
malformed = work / "malformed-sibling"
|
|
3863
4047
|
malformed.mkdir()
|
|
3864
4048
|
malformed_devlyn = malformed / ".devlyn"
|
|
@@ -3897,7 +4081,7 @@ def run_self_test() -> int:
|
|
|
3897
4081
|
}), encoding="utf-8")
|
|
3898
4082
|
(devlyn / "spec-verify.json").write_text(json.dumps({
|
|
3899
4083
|
"verification_commands": [
|
|
3900
|
-
{"cmd": "printf bench-staged", "stdout_contains": ["bench-staged"]}
|
|
4084
|
+
{"cmd": "printf bench-staged", "stdout_contains": ["bench-staged"], "contract_refs": ["R1"]}
|
|
3901
4085
|
]
|
|
3902
4086
|
}) + "\n", encoding="utf-8")
|
|
3903
4087
|
bench_pre_staged = subprocess.run(
|
|
@@ -3916,6 +4100,38 @@ def run_self_test() -> int:
|
|
|
3916
4100
|
print("benchmark pre-staged contract was overwritten", file=sys.stderr)
|
|
3917
4101
|
return 1
|
|
3918
4102
|
|
|
4103
|
+
bench_marker = work / "bench-command-ran"
|
|
4104
|
+
bench_command = {"cmd": "printf bad > bench-command-ran; printf bad"}
|
|
4105
|
+
for phase in ("build_gate", "verify_mechanical"):
|
|
4106
|
+
for carrier, diagnostic in (
|
|
4107
|
+
({"verification_commands": [{**bench_command, "stdout_not_contians": ["bad"]}]}, "unknown key(s): stdout_not_contians"),
|
|
4108
|
+
({"verification_commands": [{**bench_command, "contract_refs": [""]}]}, "contract_refs must be a list of non-empty strings"),
|
|
4109
|
+
({"verification_commands": [bench_command], "required_files": ["missing.txt"]}, "unsupported inline key(s): required_files"),
|
|
4110
|
+
({"verification_commands": [bench_command], "pure_design": True}, "requires an explicit empty verification_commands list"),
|
|
4111
|
+
({"verification_commands": [], "pure_design": True}, "must contain at least one entry"),
|
|
4112
|
+
):
|
|
4113
|
+
(devlyn / "spec-verify.json").write_text(json.dumps(carrier), encoding="utf-8")
|
|
4114
|
+
old_bench_results = (devlyn / "spec-verify.results.json").read_bytes()
|
|
4115
|
+
rejected_bench = subprocess.run(
|
|
4116
|
+
[sys.executable, script_path], cwd=work,
|
|
4117
|
+
env={**env, "SPEC_VERIFY_PHASE": phase},
|
|
4118
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
4119
|
+
)
|
|
4120
|
+
rejected_findings = [loads_strict_json(line) for line in
|
|
4121
|
+
(devlyn / output_findings_name()).read_text(encoding="utf-8").splitlines()]
|
|
4122
|
+
if (
|
|
4123
|
+
rejected_bench.returncode != 1 or diagnostic not in rejected_bench.stderr
|
|
4124
|
+
or bench_marker.exists()
|
|
4125
|
+
or (phase == "verify_mechanical" and
|
|
4126
|
+
(devlyn / "spec-verify.results.json").read_bytes() != old_bench_results)
|
|
4127
|
+
or len(rejected_findings) != 1
|
|
4128
|
+
or rejected_findings[0]["rule_id"] != "correctness.spec-verify-malformed"
|
|
4129
|
+
or rejected_findings[0]["severity"] != "CRITICAL"
|
|
4130
|
+
or rejected_findings[0]["phase"] != phase
|
|
4131
|
+
):
|
|
4132
|
+
print(f"benchmark {phase} failed closed incorrectly for {carrier}: {rejected_bench.stderr}", file=sys.stderr)
|
|
4133
|
+
return 1
|
|
4134
|
+
|
|
3919
4135
|
verify_output = work / "verify-output"
|
|
3920
4136
|
verify_output.mkdir()
|
|
3921
4137
|
verify_devlyn = verify_output / ".devlyn"
|
|
@@ -4551,6 +4767,84 @@ def run_self_test() -> int:
|
|
|
4551
4767
|
print("atomic_batch_state without success-order evidence was accepted", file=sys.stderr)
|
|
4552
4768
|
return 1
|
|
4553
4769
|
|
|
4770
|
+
# Literal diff paths must survive Git quoting, renames and both consumers.
|
|
4771
|
+
literal_root = work / "literal-paths"
|
|
4772
|
+
literal_root.mkdir()
|
|
4773
|
+
literal_devlyn = literal_root / ".devlyn"
|
|
4774
|
+
literal_devlyn.mkdir()
|
|
4775
|
+
(literal_root / ".gitignore").write_text(".devlyn/\n", encoding="utf-8")
|
|
4776
|
+
literal_names = ["plain.txt", "설정.txt", "my file.txt"]
|
|
4777
|
+
if os.name != "nt":
|
|
4778
|
+
literal_names += ["a\tb.txt", "a\nb.txt", " leading.txt", "trailing.txt ", 'quote".txt', "back\\slash.txt", "literal*.txt"]
|
|
4779
|
+
for name in literal_names + ["old name.txt"]:
|
|
4780
|
+
(literal_root / name).write_text("original\n", encoding="utf-8")
|
|
4781
|
+
subprocess.run(["git", "init", "-q"], cwd=literal_root, check=True)
|
|
4782
|
+
subprocess.run(["git", "add", "-A"], cwd=literal_root, check=True)
|
|
4783
|
+
subprocess.run(["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "base"], cwd=literal_root, check=True)
|
|
4784
|
+
for name in literal_names:
|
|
4785
|
+
(literal_root / name).write_text("updated\n", encoding="utf-8")
|
|
4786
|
+
subprocess.run(["git", "mv", "old name.txt", "new name.txt"], cwd=literal_root, check=True)
|
|
4787
|
+
literal_names += ["old name.txt", "new name.txt"]
|
|
4788
|
+
literal_state = {"base_ref": {"sha": "HEAD"}}
|
|
4789
|
+
literal_expected = {"forbidden_files": literal_names}
|
|
4790
|
+
(literal_devlyn / "untracked.baseline").write_text("", encoding="utf-8")
|
|
4791
|
+
(literal_devlyn / "plan.md").write_text(
|
|
4792
|
+
"<!-- devlyn:authorized-surface -->\n## Files to touch\n\n```json\n"
|
|
4793
|
+
+ json.dumps({"authorized_surface": literal_names}) + "\n```\n", encoding="utf-8",
|
|
4794
|
+
)
|
|
4795
|
+
literal_patch = subprocess.check_output(
|
|
4796
|
+
["git", "diff", "--binary", "--src-prefix=a/", "--dst-prefix=b/", "HEAD"], cwd=literal_root,
|
|
4797
|
+
)
|
|
4798
|
+
literal_external = literal_devlyn / "external-diff.patch"
|
|
4799
|
+
for external in (False, True):
|
|
4800
|
+
if external:
|
|
4801
|
+
literal_external.write_bytes(literal_patch)
|
|
4802
|
+
hits, _ = expected_contract_findings(literal_expected, None, literal_root, literal_devlyn, literal_state, 1)
|
|
4803
|
+
assert len(hits) == len(literal_names) and all(hit["rule_id"] == "scope.forbidden-file-touched" for hit in hits), hits
|
|
4804
|
+
scope_hits, _ = authorized_surface_findings(literal_root, literal_devlyn, literal_state, 1)
|
|
4805
|
+
assert not scope_hits, scope_hits
|
|
4806
|
+
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"
|
|
4807
|
+
(literal_devlyn / "plan.md").write_text(
|
|
4808
|
+
'<!-- devlyn:authorized-surface -->\n## Files to touch\n\n```json\n{"authorized_surface":["untouched.txt"]}\n```\n', encoding="utf-8",
|
|
4809
|
+
)
|
|
4810
|
+
literal_external.write_bytes(edit)
|
|
4811
|
+
scope_hits, _ = authorized_surface_findings(literal_root, literal_devlyn, literal_state, 1)
|
|
4812
|
+
assert len(scope_hits) == 1 and scope_hits[0]["rule_id"] == "scope.out-of-scope-file", scope_hits
|
|
4813
|
+
literal_external.write_bytes(edit.replace(b"+updated", b"+updated "))
|
|
4814
|
+
subprocess.run(["git", "config", "apply.whitespace", "error"], cwd=literal_root, check=True)
|
|
4815
|
+
try:
|
|
4816
|
+
names, error = changed_files(literal_root, literal_state, literal_devlyn)
|
|
4817
|
+
assert names == ["plain.txt"] and error is None, (names, error)
|
|
4818
|
+
finally:
|
|
4819
|
+
subprocess.run(["git", "config", "--unset", "apply.whitespace"], cwd=literal_root, check=True)
|
|
4820
|
+
for patch, paths in (
|
|
4821
|
+
(b"", []),
|
|
4822
|
+
(edit.replace(b"\n", b"\r\n"), ["plain.txt"]),
|
|
4823
|
+
(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"]),
|
|
4824
|
+
(b"diff --git a/script b/script\nold mode 100644\nnew mode 100755\n", ["script"]),
|
|
4825
|
+
(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"]),
|
|
4826
|
+
):
|
|
4827
|
+
literal_external.write_bytes(patch)
|
|
4828
|
+
names, error = changed_files(literal_root, literal_state, literal_devlyn)
|
|
4829
|
+
assert names == paths and error is None, (names, error)
|
|
4830
|
+
for bad_patch in (
|
|
4831
|
+
b"not a patch\n", b"\n", edit[:-5],
|
|
4832
|
+
edit.replace(b"a/plain.txt", b"plain.txt").replace(b"b/plain.txt", b"plain.txt"),
|
|
4833
|
+
edit.replace(b"a/plain.txt", b"src/plain.txt").replace(b"b/plain.txt", b"src/plain.txt"),
|
|
4834
|
+
edit.replace(b"a/plain.txt", b"i/plain.txt").replace(b"b/plain.txt", b"w/plain.txt"),
|
|
4835
|
+
):
|
|
4836
|
+
literal_external.write_bytes(bad_patch)
|
|
4837
|
+
hits, _ = expected_contract_findings(literal_expected, None, literal_root, literal_devlyn, literal_state, 1)
|
|
4838
|
+
assert len(hits) == 1 and hits[0]["rule_id"] == "correctness.expected-contract-unverifiable", hits
|
|
4839
|
+
scope_hits, _ = authorized_surface_findings(literal_root, literal_devlyn, literal_state, 1)
|
|
4840
|
+
assert len(scope_hits) == 1 and scope_hits[0]["file"] == ".devlyn/external-diff.patch", scope_hits
|
|
4841
|
+
literal_external.unlink()
|
|
4842
|
+
names, error = changed_files(literal_root, {"base_ref": {"sha": "missing-literal-ref"}}, literal_devlyn)
|
|
4843
|
+
assert not names and error, (names, error)
|
|
4844
|
+
assert subprocess.check_output(
|
|
4845
|
+
["git", "diff", "--binary", "--src-prefix=a/", "--dst-prefix=b/", "HEAD"], cwd=literal_root,
|
|
4846
|
+
) == literal_patch
|
|
4847
|
+
|
|
4554
4848
|
# iter-0046: PLAN-declared authorized_surface enforced at BUILD_GATE.
|
|
4555
4849
|
scope_root = work / "scope-gate"
|
|
4556
4850
|
scope_root.mkdir()
|
|
@@ -4965,7 +5259,8 @@ def main() -> int:
|
|
|
4965
5259
|
# continue to legacy source-extract.
|
|
4966
5260
|
# 3. Source-extract reads
|
|
4967
5261
|
# `pipeline.state.json:source.{spec_path | criteria_path}`. If it has
|
|
4968
|
-
# a json block,
|
|
5262
|
+
# a json block, stage its commands or clear stale staging for an
|
|
5263
|
+
# explicit pure-design contract.
|
|
4969
5264
|
# 4. If source has no json block AND source.type=="generated":
|
|
4970
5265
|
# CRITICAL spec-verify-malformed — generated criteria must ship a
|
|
4971
5266
|
# verifiable contract per the generated-criteria output contract.
|
|
@@ -4980,6 +5275,39 @@ def main() -> int:
|
|
|
4980
5275
|
trust_bench_staged = bench_mode and pre_staged
|
|
4981
5276
|
src_type, source_md = read_source(work, devlyn_dir)
|
|
4982
5277
|
state = read_state(devlyn_dir)
|
|
5278
|
+
|
|
5279
|
+
def reject_preflight() -> int:
|
|
5280
|
+
# A completed rejection is not an interrupted checker. Preserve actual
|
|
5281
|
+
# earlier gate observations without inventing an executed command.
|
|
5282
|
+
if validate_risk_probes_only or output_phase() != "build_gate":
|
|
5283
|
+
return 1
|
|
5284
|
+
runner = process_evidence_module()
|
|
5285
|
+
phase, run_id, round_, relative = mechanical_evidence_identity(state, runner)
|
|
5286
|
+
carrier = None
|
|
5287
|
+
try:
|
|
5288
|
+
if os.path.lexists(work / relative):
|
|
5289
|
+
carrier = runner.validate_manifest(
|
|
5290
|
+
work, relative, run_id, phase, round_, require_expectations=False,
|
|
5291
|
+
)
|
|
5292
|
+
findings = devlyn_dir / output_findings_name()
|
|
5293
|
+
result = {
|
|
5294
|
+
"commands": runner.bound_carrier_summary_commands(work, carrier) if carrier else [],
|
|
5295
|
+
"process_evidence": carrier,
|
|
5296
|
+
"preflight_failure": {
|
|
5297
|
+
"run_id": run_id, "phase": phase, "round": round_,
|
|
5298
|
+
"findings": {
|
|
5299
|
+
"path": findings.relative_to(work).as_posix(),
|
|
5300
|
+
"sha256": hashlib.sha256(findings.read_bytes()).hexdigest(),
|
|
5301
|
+
},
|
|
5302
|
+
},
|
|
5303
|
+
}
|
|
5304
|
+
(devlyn_dir / "spec-verify.results.json").write_text(
|
|
5305
|
+
json.dumps(result, indent=2) + "\n", encoding="utf-8",
|
|
5306
|
+
)
|
|
5307
|
+
except (runner.EvidenceError, OSError, UnicodeError, ValueError) as exc:
|
|
5308
|
+
print(f"[spec-verify] rejection evidence could not be finalized: {exc}", file=sys.stderr)
|
|
5309
|
+
return 1
|
|
5310
|
+
|
|
4983
5311
|
external_diff = devlyn_dir / "external-diff.patch"
|
|
4984
5312
|
if external_diff.is_file() and state.get("mode") != "verify-only":
|
|
4985
5313
|
error = (
|
|
@@ -4997,14 +5325,15 @@ def main() -> int:
|
|
|
4997
5325
|
"intentionally verifying an external patch."
|
|
4998
5326
|
),
|
|
4999
5327
|
)
|
|
5000
|
-
return
|
|
5328
|
+
return reject_preflight()
|
|
5001
5329
|
integrity_error = source_integrity_error(src_type, state, source_md)
|
|
5002
5330
|
if integrity_error:
|
|
5003
5331
|
print(f"[spec-verify] carrier malformed: {integrity_error}", file=sys.stderr)
|
|
5004
5332
|
write_malformed_finding(devlyn_dir, integrity_error, source_md)
|
|
5005
|
-
return
|
|
5333
|
+
return reject_preflight()
|
|
5006
5334
|
expected_data: dict | None = None
|
|
5007
5335
|
expected_path: Path | None = None
|
|
5336
|
+
contract_found = False
|
|
5008
5337
|
if validate_risk_probes_only:
|
|
5009
5338
|
_risk_probes, risk_error = load_risk_probes(
|
|
5010
5339
|
devlyn_dir, source_md, require_present=True
|
|
@@ -5012,29 +5341,29 @@ def main() -> int:
|
|
|
5012
5341
|
if risk_error:
|
|
5013
5342
|
print(f"[spec-verify] risk probes malformed: {risk_error}", file=sys.stderr)
|
|
5014
5343
|
write_malformed_finding(devlyn_dir, risk_error, devlyn_dir / "risk-probes.jsonl")
|
|
5015
|
-
return
|
|
5344
|
+
return reject_preflight()
|
|
5016
5345
|
print("[spec-verify] risk probes valid", file=sys.stderr)
|
|
5017
5346
|
return 0
|
|
5018
5347
|
if source_md is not None and not trust_bench_staged:
|
|
5019
5348
|
if src_type == "spec":
|
|
5020
|
-
|
|
5349
|
+
contract_found, _staged, expected_error, expected_path, expected_data = stage_from_expected(
|
|
5021
5350
|
source_md, devlyn_dir
|
|
5022
5351
|
)
|
|
5023
5352
|
if expected_error is not None:
|
|
5024
5353
|
print(f"[spec-verify] carrier malformed: {expected_error}", file=sys.stderr)
|
|
5025
5354
|
write_malformed_finding(devlyn_dir, expected_error, expected_path)
|
|
5026
|
-
return
|
|
5027
|
-
if
|
|
5028
|
-
|
|
5355
|
+
return reject_preflight()
|
|
5356
|
+
if contract_found:
|
|
5357
|
+
error = None
|
|
5029
5358
|
else:
|
|
5030
|
-
|
|
5359
|
+
contract_found, _staged, error = stage_from_source(source_md, devlyn_dir)
|
|
5031
5360
|
else:
|
|
5032
|
-
|
|
5361
|
+
contract_found, _staged, error = stage_from_source(source_md, devlyn_dir)
|
|
5033
5362
|
if error is not None:
|
|
5034
5363
|
print(f"[spec-verify] carrier malformed: {error}", file=sys.stderr)
|
|
5035
5364
|
write_malformed_finding(devlyn_dir, error, source_md)
|
|
5036
|
-
return
|
|
5037
|
-
if not
|
|
5365
|
+
return reject_preflight()
|
|
5366
|
+
if not contract_found:
|
|
5038
5367
|
if src_type == "generated":
|
|
5039
5368
|
msg = (
|
|
5040
5369
|
f"generated {source_md.name} must include a "
|
|
@@ -5043,9 +5372,9 @@ def main() -> int:
|
|
|
5043
5372
|
)
|
|
5044
5373
|
print(f"[spec-verify] {msg}", file=sys.stderr)
|
|
5045
5374
|
write_malformed_finding(devlyn_dir, msg, source_md)
|
|
5046
|
-
return
|
|
5375
|
+
return reject_preflight()
|
|
5047
5376
|
# source.type=="spec", no block in spec markdown.
|
|
5048
|
-
if not bench_mode
|
|
5377
|
+
if not bench_mode:
|
|
5049
5378
|
# Real-user handwritten spec: silent no-op. Drop any stale
|
|
5050
5379
|
# pre-staged file so a killed prior run cannot poison this
|
|
5051
5380
|
# run's gate.
|
|
@@ -5067,11 +5396,10 @@ def main() -> int:
|
|
|
5067
5396
|
|
|
5068
5397
|
commands: list[dict] = []
|
|
5069
5398
|
if not spec_path.exists():
|
|
5070
|
-
#
|
|
5071
|
-
#
|
|
5072
|
-
#
|
|
5073
|
-
|
|
5074
|
-
if expected_data is None:
|
|
5399
|
+
# A declared pure-design contract continues through probes/results.
|
|
5400
|
+
# Missing handwritten/benchmark contracts remain an opt-in no-op;
|
|
5401
|
+
# missing generated contracts were rejected above.
|
|
5402
|
+
if not contract_found:
|
|
5075
5403
|
return 0
|
|
5076
5404
|
else:
|
|
5077
5405
|
try:
|
|
@@ -5080,27 +5408,26 @@ def main() -> int:
|
|
|
5080
5408
|
print(f"[spec-verify] error: cannot parse {spec_path}: {e}", file=sys.stderr)
|
|
5081
5409
|
return 2
|
|
5082
5410
|
|
|
5083
|
-
#
|
|
5084
|
-
#
|
|
5085
|
-
#
|
|
5086
|
-
|
|
5087
|
-
shape_err = validate_shape(spec)
|
|
5411
|
+
# Pre-staged commands need the strict carrier vocabulary too: unknown
|
|
5412
|
+
# expectations were silently ignored. Keep the nonempty shape gate;
|
|
5413
|
+
# a benchmark's staged empty list must not become a pure-design pass.
|
|
5414
|
+
shape_err = validate_shape(spec) or validate_inline_shape(spec)
|
|
5088
5415
|
if shape_err:
|
|
5089
5416
|
print(f"[spec-verify] error: {spec_path}: {shape_err}", file=sys.stderr)
|
|
5090
5417
|
write_malformed_finding(devlyn_dir, f"{spec_path}: {shape_err}", None)
|
|
5091
|
-
return
|
|
5418
|
+
return reject_preflight()
|
|
5092
5419
|
commands = list(spec["verification_commands"])
|
|
5093
5420
|
if include_risk_probes:
|
|
5094
5421
|
risk_state_error = risk_probes_state_error(state)
|
|
5095
5422
|
if risk_state_error:
|
|
5096
5423
|
print(f"[spec-verify] risk probes malformed: {risk_state_error}", file=sys.stderr)
|
|
5097
5424
|
write_malformed_finding(devlyn_dir, risk_state_error, Path("pipeline.state.json"))
|
|
5098
|
-
return
|
|
5425
|
+
return reject_preflight()
|
|
5099
5426
|
integrity_error = risk_probe_integrity_error(state, devlyn_dir)
|
|
5100
5427
|
if integrity_error:
|
|
5101
5428
|
print(f"[spec-verify] risk probes integrity failed: {integrity_error}", file=sys.stderr)
|
|
5102
5429
|
write_risk_probe_integrity_finding(devlyn_dir, integrity_error)
|
|
5103
|
-
return
|
|
5430
|
+
return reject_preflight()
|
|
5104
5431
|
risk_probes, risk_error = load_risk_probes(
|
|
5105
5432
|
devlyn_dir,
|
|
5106
5433
|
source_md,
|
|
@@ -5109,7 +5436,7 @@ def main() -> int:
|
|
|
5109
5436
|
if risk_error:
|
|
5110
5437
|
print(f"[spec-verify] risk probes malformed: {risk_error}", file=sys.stderr)
|
|
5111
5438
|
write_malformed_finding(devlyn_dir, risk_error, devlyn_dir / "risk-probes.jsonl")
|
|
5112
|
-
return
|
|
5439
|
+
return reject_preflight()
|
|
5113
5440
|
commands.extend(risk_probes)
|
|
5114
5441
|
|
|
5115
5442
|
devlyn_dir.mkdir(parents=True, exist_ok=True)
|