devlyn-cli 3.1.1 → 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/process-evidence.py +16 -1
- package/config/skills/_shared/resolve-bootstrap.py +51 -6
- package/config/skills/_shared/spec-verify-check.py +525 -89
- package/config/skills/_shared/state-phase-write.py +202 -32
- package/config/skills/_shared/task-complete.py +260 -16
- 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 +3 -3
- 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 +4 -3
- package/config/skills/devlyn:resolve/references/phases/verify.md +9 -7
- package/config/skills/devlyn:resolve/references/state-schema.md +2 -2
- package/config/skills/devlyn:resolve/references/task-completion.md +27 -0
- 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`.
|
|
@@ -844,10 +844,28 @@ def validate_expected_shape(data) -> str | None:
|
|
|
844
844
|
return None
|
|
845
845
|
|
|
846
846
|
|
|
847
|
-
def
|
|
847
|
+
def validate_inline_shape(data: object) -> str | None:
|
|
848
|
+
if isinstance(data, dict):
|
|
849
|
+
unknown = sorted(set(data) - {"verification_commands", "pure_design"})
|
|
850
|
+
if unknown:
|
|
851
|
+
return (
|
|
852
|
+
f"unsupported inline key(s): {', '.join(unknown)}; inline carriers "
|
|
853
|
+
"support only verification_commands and pure_design. Encode these checks as commands, "
|
|
854
|
+
"or use a real spec with sibling spec.expected.json."
|
|
855
|
+
)
|
|
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)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def validate_expected_against_sibling_spec(spec_path: Path, data: object) -> str | None:
|
|
848
867
|
if not isinstance(data, dict):
|
|
849
868
|
return None
|
|
850
|
-
spec_path = expected_path.with_name("spec.md")
|
|
851
869
|
if not spec_path.is_file():
|
|
852
870
|
return None
|
|
853
871
|
try:
|
|
@@ -881,8 +899,7 @@ def validate_expected_against_sibling_spec(expected_path: Path, data: object) ->
|
|
|
881
899
|
)
|
|
882
900
|
|
|
883
901
|
|
|
884
|
-
def validate_sibling_spec_complexity(
|
|
885
|
-
spec_path = expected_path.with_name("spec.md")
|
|
902
|
+
def validate_sibling_spec_complexity(spec_path: Path) -> str | None:
|
|
886
903
|
if not spec_path.is_file():
|
|
887
904
|
return None
|
|
888
905
|
try:
|
|
@@ -1149,9 +1166,11 @@ def risk_probes_state_error(state: dict) -> str | None:
|
|
|
1149
1166
|
|
|
1150
1167
|
|
|
1151
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 {}
|
|
1152
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}."
|
|
1153
1173
|
return None
|
|
1154
|
-
src = state.get("source") if isinstance(state.get("source"), dict) else {}
|
|
1155
1174
|
if src_type == "generated":
|
|
1156
1175
|
field = "criteria_sha256"
|
|
1157
1176
|
required = True
|
|
@@ -1188,33 +1207,33 @@ def load_expected_contract(expected_path: Path) -> tuple[dict | None, str | None
|
|
|
1188
1207
|
return (data, None)
|
|
1189
1208
|
|
|
1190
1209
|
|
|
1191
|
-
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]:
|
|
1192
1211
|
"""Materialize .devlyn/spec-verify.json from the json block in `md`.
|
|
1193
1212
|
|
|
1194
|
-
Returns (staged, error).
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
contract; a missing fence is a mistake, not a no-op). staged=False,
|
|
1199
|
-
error=None → the sentinel is absent entirely (handwritten spec or
|
|
1200
|
-
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.
|
|
1201
1217
|
"""
|
|
1202
1218
|
section_found, block = extract_verification_block(md.read_text(encoding="utf-8"))
|
|
1203
1219
|
if not section_found:
|
|
1204
|
-
return (False, None)
|
|
1220
|
+
return (False, False, None)
|
|
1205
1221
|
if block is None:
|
|
1206
|
-
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")
|
|
1207
1223
|
try:
|
|
1208
1224
|
data = loads_strict_json(block)
|
|
1209
1225
|
except ValueError as e:
|
|
1210
|
-
return (False, f"`<!-- devlyn:verification -->` ```json``` block in {md} has invalid JSON: {e}")
|
|
1211
|
-
err =
|
|
1226
|
+
return (True, False, f"`<!-- devlyn:verification -->` ```json``` block in {md} has invalid JSON: {e}")
|
|
1227
|
+
err = validate_inline_shape(data)
|
|
1212
1228
|
if err:
|
|
1213
|
-
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)
|
|
1214
1233
|
normalized = {"verification_commands": data["verification_commands"]}
|
|
1215
1234
|
devlyn_dir.mkdir(parents=True, exist_ok=True)
|
|
1216
1235
|
(devlyn_dir / "spec-verify.json").write_text(json.dumps(normalized, indent=2) + "\n", encoding="utf-8")
|
|
1217
|
-
return (True, None)
|
|
1236
|
+
return (True, True, None)
|
|
1218
1237
|
|
|
1219
1238
|
|
|
1220
1239
|
def stage_from_expected(
|
|
@@ -1236,6 +1255,9 @@ def stage_from_expected(
|
|
|
1236
1255
|
if err:
|
|
1237
1256
|
return (True, False, err, expected_path, None)
|
|
1238
1257
|
assert data is not None
|
|
1258
|
+
err = validate_sibling_spec_complexity(md) or validate_expected_against_sibling_spec(md, data)
|
|
1259
|
+
if err:
|
|
1260
|
+
return (True, False, f"{expected_path}: {err}", expected_path, None)
|
|
1239
1261
|
commands = data.get("verification_commands")
|
|
1240
1262
|
if not commands:
|
|
1241
1263
|
spec_path = devlyn_dir / "spec-verify.json"
|
|
@@ -1322,7 +1344,7 @@ def diff_text_for_expected(work: Path, devlyn_dir: Path, state: dict) -> tuple[s
|
|
|
1322
1344
|
external_diff = devlyn_dir / "external-diff.patch"
|
|
1323
1345
|
if external_diff.is_file():
|
|
1324
1346
|
try:
|
|
1325
|
-
return (external_diff.read_text(encoding="utf-8"), None)
|
|
1347
|
+
return (external_diff.read_text(encoding="utf-8", errors="surrogateescape"), None)
|
|
1326
1348
|
except OSError as e:
|
|
1327
1349
|
return ("", f"cannot read {external_diff}: {e}")
|
|
1328
1350
|
base_sha = ((state.get("base_ref") or {}).get("sha") or "").strip()
|
|
@@ -1361,28 +1383,46 @@ def count_deps_added(work: Path, state: dict) -> int:
|
|
|
1361
1383
|
return count
|
|
1362
1384
|
|
|
1363
1385
|
|
|
1364
|
-
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]:
|
|
1365
1387
|
external_diff = devlyn_dir / "external-diff.patch"
|
|
1366
1388
|
if external_diff.is_file():
|
|
1367
1389
|
names: list[str] = []
|
|
1368
1390
|
try:
|
|
1369
|
-
|
|
1370
|
-
except OSError:
|
|
1371
|
-
return []
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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)
|
|
1378
1418
|
base_sha = ((state.get("base_ref") or {}).get("sha") or "").strip()
|
|
1379
|
-
cmd = ["git", "diff", "--name-only"]
|
|
1419
|
+
cmd = ["git", "diff", "--name-only", "-z", "--no-renames"]
|
|
1380
1420
|
if base_sha:
|
|
1381
1421
|
cmd.append(base_sha)
|
|
1382
|
-
proc = subprocess.run(cmd, cwd=str(work), capture_output=True
|
|
1422
|
+
proc = subprocess.run(cmd, cwd=str(work), capture_output=True)
|
|
1383
1423
|
if proc.returncode != 0:
|
|
1384
|
-
return []
|
|
1385
|
-
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)
|
|
1386
1426
|
|
|
1387
1427
|
|
|
1388
1428
|
def expected_contract_findings(
|
|
@@ -1398,6 +1438,8 @@ def expected_contract_findings(
|
|
|
1398
1438
|
findings: list[dict] = []
|
|
1399
1439
|
seq = finding_start
|
|
1400
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
|
|
1401
1443
|
if diff_error and (
|
|
1402
1444
|
expected_data.get("forbidden_patterns") or expected_data.get("forbidden_files")
|
|
1403
1445
|
):
|
|
@@ -1407,12 +1449,12 @@ def expected_contract_findings(
|
|
|
1407
1449
|
"level": "error",
|
|
1408
1450
|
"severity": "CRITICAL",
|
|
1409
1451
|
"confidence": 1.0,
|
|
1410
|
-
"message": f"Cannot compute diff for
|
|
1452
|
+
"message": f"Cannot compute diff for expected contract: {diff_error}",
|
|
1411
1453
|
"file": str(expected_path or "spec.expected.json"),
|
|
1412
1454
|
"line": 1,
|
|
1413
1455
|
"phase": output_phase(),
|
|
1414
|
-
"criterion_ref": "spec.expected.json/forbidden_patterns",
|
|
1415
|
-
"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.",
|
|
1416
1458
|
"blocking": True,
|
|
1417
1459
|
"status": "open",
|
|
1418
1460
|
})
|
|
@@ -1438,7 +1480,7 @@ def expected_contract_findings(
|
|
|
1438
1480
|
"status": "open",
|
|
1439
1481
|
})
|
|
1440
1482
|
seq += 1
|
|
1441
|
-
changed = set(
|
|
1483
|
+
changed = set(paths)
|
|
1442
1484
|
for i, required in enumerate(expected_data.get("required_files", []) or []):
|
|
1443
1485
|
if (work / required).exists():
|
|
1444
1486
|
continue
|
|
@@ -1757,7 +1799,16 @@ def authorized_surface_findings(
|
|
|
1757
1799
|
|
|
1758
1800
|
findings: list[dict] = []
|
|
1759
1801
|
seq = finding_start
|
|
1760
|
-
|
|
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:
|
|
1761
1812
|
if path_matches_surface(path, surface):
|
|
1762
1813
|
continue
|
|
1763
1814
|
findings.append(scope_finding(
|
|
@@ -1846,13 +1897,8 @@ def run_write_untracked_baseline(work: Path, devlyn_dir: Path) -> int:
|
|
|
1846
1897
|
|
|
1847
1898
|
|
|
1848
1899
|
def run_check_mode(md_path: Path) -> int:
|
|
1849
|
-
"""
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
Exit 0: `<!-- devlyn:verification -->` sentinel absent OR section present
|
|
1853
|
-
and well-formed.
|
|
1854
|
-
Exit 2: sentinel present but the section is malformed — no fenced json
|
|
1855
|
-
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.
|
|
1856
1902
|
"""
|
|
1857
1903
|
if not md_path.is_file():
|
|
1858
1904
|
print(f"[spec-verify --check] error: {md_path} not found", file=sys.stderr)
|
|
@@ -1870,6 +1916,14 @@ def run_check_mode(md_path: Path) -> int:
|
|
|
1870
1916
|
if solo_ceiling_err:
|
|
1871
1917
|
print(f"[spec-verify --check] {md_path}: {solo_ceiling_err}", file=sys.stderr)
|
|
1872
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
|
|
1873
1927
|
section_found, block = extract_verification_block(text)
|
|
1874
1928
|
if not section_found:
|
|
1875
1929
|
# Sentinel absent entirely — opt-in nature preserved for ideate (a
|
|
@@ -1892,7 +1946,7 @@ def run_check_mode(md_path: Path) -> int:
|
|
|
1892
1946
|
file=sys.stderr,
|
|
1893
1947
|
)
|
|
1894
1948
|
return 2
|
|
1895
|
-
err =
|
|
1949
|
+
err = validate_inline_shape(data)
|
|
1896
1950
|
if err:
|
|
1897
1951
|
print(f"[spec-verify --check] {md_path}: shape error: {err}", file=sys.stderr)
|
|
1898
1952
|
return 2
|
|
@@ -1915,11 +1969,11 @@ def run_check_expected_mode(expected_path: Path) -> int:
|
|
|
1915
1969
|
if err:
|
|
1916
1970
|
print(f"[spec-verify --check-expected] {expected_path}: shape error: {err}", file=sys.stderr)
|
|
1917
1971
|
return 2
|
|
1918
|
-
complexity_err = validate_sibling_spec_complexity(expected_path)
|
|
1972
|
+
complexity_err = validate_sibling_spec_complexity(expected_path.with_name("spec.md"))
|
|
1919
1973
|
if complexity_err:
|
|
1920
1974
|
print(f"[spec-verify --check-expected] {expected_path}: shape error: {complexity_err}", file=sys.stderr)
|
|
1921
1975
|
return 2
|
|
1922
|
-
sibling_err = validate_expected_against_sibling_spec(expected_path, _data)
|
|
1976
|
+
sibling_err = validate_expected_against_sibling_spec(expected_path.with_name("spec.md"), _data)
|
|
1923
1977
|
if sibling_err:
|
|
1924
1978
|
print(f"[spec-verify --check-expected] {expected_path}: shape error: {sibling_err}", file=sys.stderr)
|
|
1925
1979
|
return 2
|
|
@@ -2047,7 +2101,7 @@ def run_self_test() -> int:
|
|
|
2047
2101
|
+ "\n```\n",
|
|
2048
2102
|
encoding="utf-8",
|
|
2049
2103
|
)
|
|
2050
|
-
staged, inline_error = stage_from_source(
|
|
2104
|
+
_found, staged, inline_error = stage_from_source(
|
|
2051
2105
|
inline_timeout_spec, inline_timeout_devlyn
|
|
2052
2106
|
)
|
|
2053
2107
|
if staged or not inline_error or "timeout_sec" not in inline_error:
|
|
@@ -2067,7 +2121,7 @@ def run_self_test() -> int:
|
|
|
2067
2121
|
+ "\n```\n",
|
|
2068
2122
|
encoding="utf-8",
|
|
2069
2123
|
)
|
|
2070
|
-
staged, inline_error = stage_from_source(
|
|
2124
|
+
_found, staged, inline_error = stage_from_source(
|
|
2071
2125
|
inline_timeout_spec, inline_timeout_devlyn
|
|
2072
2126
|
)
|
|
2073
2127
|
inline_staged = loads_strict_json(
|
|
@@ -3628,6 +3682,56 @@ def run_self_test() -> int:
|
|
|
3628
3682
|
"```\n",
|
|
3629
3683
|
encoding="utf-8",
|
|
3630
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)
|
|
3631
3735
|
(generated_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3632
3736
|
"source": {"type": "generated", "criteria_path": str(generated_criteria)}
|
|
3633
3737
|
}), encoding="utf-8")
|
|
@@ -3691,6 +3795,54 @@ def run_self_test() -> int:
|
|
|
3691
3795
|
print("generated criteria carrier was not staged into .devlyn/spec-verify.json", file=sys.stderr)
|
|
3692
3796
|
return 1
|
|
3693
3797
|
|
|
3798
|
+
inline_marker = generated_user / "inline-command-ran"
|
|
3799
|
+
inline_command = "printf bad > inline-command-ran; printf bad"
|
|
3800
|
+
for source_type in ("generated", "spec"):
|
|
3801
|
+
for contract, diagnostic in (
|
|
3802
|
+
({"verification_commands": [{"cmd": inline_command}],
|
|
3803
|
+
"required_files": ["missing.txt"]}, "unsupported inline key(s): required_files"),
|
|
3804
|
+
({"verification_commands": [{"cmd": inline_command,
|
|
3805
|
+
"stdout_not_contians": ["bad"]}]}, "unknown key(s): stdout_not_contians"),
|
|
3806
|
+
({"verification_commands": [{"cmd": inline_command,
|
|
3807
|
+
"stdout_not_contains": ["bad"]}]}, None),
|
|
3808
|
+
):
|
|
3809
|
+
inline_marker.unlink(missing_ok=True)
|
|
3810
|
+
(generated_devlyn / output_findings_name()).unlink(missing_ok=True)
|
|
3811
|
+
generated_criteria.write_text(
|
|
3812
|
+
"# Inline constraints\n\n<!-- devlyn:verification -->\n## Verification\n\n```json\n"
|
|
3813
|
+
+ json.dumps(contract) + "\n```\n", encoding="utf-8",
|
|
3814
|
+
)
|
|
3815
|
+
prefix = "criteria" if source_type == "generated" else "spec"
|
|
3816
|
+
(generated_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3817
|
+
"source": {
|
|
3818
|
+
"type": source_type,
|
|
3819
|
+
f"{prefix}_path": str(generated_criteria),
|
|
3820
|
+
f"{prefix}_sha256": hashlib.sha256(generated_criteria.read_bytes()).hexdigest(),
|
|
3821
|
+
}
|
|
3822
|
+
}), encoding="utf-8")
|
|
3823
|
+
# A stale valid carrier must not bypass malformed source validation.
|
|
3824
|
+
(generated_devlyn / "spec-verify.json").write_text(json.dumps({
|
|
3825
|
+
"verification_commands": [{"cmd": inline_command}]
|
|
3826
|
+
}), encoding="utf-8")
|
|
3827
|
+
inline_check = subprocess.run(
|
|
3828
|
+
[sys.executable, script_path, "--check", str(generated_criteria)],
|
|
3829
|
+
cwd=generated_user, capture_output=True, text=True, encoding="utf-8",
|
|
3830
|
+
)
|
|
3831
|
+
inline_run = subprocess.run(
|
|
3832
|
+
[sys.executable, script_path], cwd=generated_user,
|
|
3833
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
3834
|
+
)
|
|
3835
|
+
assert inline_check.returncode == (2 if diagnostic else 0)
|
|
3836
|
+
assert inline_run.returncode == 1
|
|
3837
|
+
if diagnostic:
|
|
3838
|
+
assert diagnostic in inline_check.stderr and diagnostic in inline_run.stderr
|
|
3839
|
+
assert not inline_marker.exists(), "malformed inline constraint executed a command"
|
|
3840
|
+
else:
|
|
3841
|
+
assert inline_marker.exists(), "valid inline output guard was not executed"
|
|
3842
|
+
assert "correctness.spec-literal-mismatch" in (
|
|
3843
|
+
generated_devlyn / output_findings_name()
|
|
3844
|
+
).read_text(encoding="utf-8")
|
|
3845
|
+
|
|
3694
3846
|
generated_criteria.write_text(
|
|
3695
3847
|
"# Criteria\n\n## Verification\n\n- generated criteria omitted its machine-readable carrier.\n",
|
|
3696
3848
|
encoding="utf-8",
|
|
@@ -3735,6 +3887,13 @@ def run_self_test() -> int:
|
|
|
3735
3887
|
(real_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3736
3888
|
"source": {"type": "spec", "spec_path": str(real_spec)}
|
|
3737
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
|
|
3738
3897
|
sibling_run = subprocess.run(
|
|
3739
3898
|
[sys.executable, script_path],
|
|
3740
3899
|
cwd=real_user,
|
|
@@ -3750,6 +3909,140 @@ def run_self_test() -> int:
|
|
|
3750
3909
|
print("sibling spec.expected.json was not staged into .devlyn/spec-verify.json", file=sys.stderr)
|
|
3751
3910
|
return 1
|
|
3752
3911
|
|
|
3912
|
+
pure_prose = "# Design\n\n<!-- devlyn:verification -->\n## Verification\n\n- No runtime verification commands.\n"
|
|
3913
|
+
stale_command = {"verification_commands": [{"cmd": "printf unexpected-command"}]}
|
|
3914
|
+
pure_inline = pure_prose + "\n```json\n" + json.dumps(stale_command) + "\n```\n"
|
|
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"),
|
|
3932
|
+
):
|
|
3933
|
+
case_root = work / name
|
|
3934
|
+
case_root.mkdir()
|
|
3935
|
+
case_devlyn = case_root / ".devlyn"
|
|
3936
|
+
case_devlyn.mkdir()
|
|
3937
|
+
case_spec = case_root / filename
|
|
3938
|
+
if isinstance(source, dict):
|
|
3939
|
+
source = pure_prose + "\n```json\n" + json.dumps(source) + "\n```\n"
|
|
3940
|
+
case_spec.write_text(source, encoding="utf-8")
|
|
3941
|
+
if contract is not None:
|
|
3942
|
+
(case_root / "spec.expected.json").write_text(json.dumps(contract), encoding="utf-8")
|
|
3943
|
+
(case_devlyn / "spec-verify.json").write_text(json.dumps(stale_command), encoding="utf-8")
|
|
3944
|
+
pointer = "criteria" if source_type == "generated" else "spec"
|
|
3945
|
+
(case_devlyn / "pipeline.state.json").write_text(json.dumps({
|
|
3946
|
+
"source": {"type": source_type, pointer + "_path": str(case_spec),
|
|
3947
|
+
pointer + "_sha256": hashlib.sha256(case_spec.read_bytes()).hexdigest()},
|
|
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
|
|
3956
|
+
case_run = subprocess.run(
|
|
3957
|
+
[sys.executable, script_path], cwd=case_root,
|
|
3958
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
3959
|
+
)
|
|
3960
|
+
if case_run.returncode != (1 if expected_rule else 0):
|
|
3961
|
+
print(f"{name}: contract execution returned {case_run.returncode}: {case_run.stderr}", file=sys.stderr)
|
|
3962
|
+
return 1
|
|
3963
|
+
case_findings = [
|
|
3964
|
+
loads_strict_json(line) for line in
|
|
3965
|
+
(case_devlyn / output_findings_name()).read_text(encoding="utf-8").splitlines()
|
|
3966
|
+
]
|
|
3967
|
+
if [finding["rule_id"] for finding in case_findings] != ([expected_rule] if expected_rule else []):
|
|
3968
|
+
print(f"{name}: unexpected contract findings: {case_findings}", file=sys.stderr)
|
|
3969
|
+
return 1
|
|
3970
|
+
case_results = case_devlyn / "spec-verify.results.json"
|
|
3971
|
+
if expected_rule == "correctness.spec-verify-malformed":
|
|
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)
|
|
3976
|
+
return 1
|
|
3977
|
+
elif (
|
|
3978
|
+
loads_strict_json(case_results.read_text(encoding="utf-8"))
|
|
3979
|
+
!= {"commands": [], "process_evidence": None}
|
|
3980
|
+
or (case_devlyn / "spec-verify.json").exists()
|
|
3981
|
+
):
|
|
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)
|
|
4000
|
+
return 1
|
|
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
|
+
|
|
3753
4046
|
malformed = work / "malformed-sibling"
|
|
3754
4047
|
malformed.mkdir()
|
|
3755
4048
|
malformed_devlyn = malformed / ".devlyn"
|
|
@@ -3788,7 +4081,7 @@ def run_self_test() -> int:
|
|
|
3788
4081
|
}), encoding="utf-8")
|
|
3789
4082
|
(devlyn / "spec-verify.json").write_text(json.dumps({
|
|
3790
4083
|
"verification_commands": [
|
|
3791
|
-
{"cmd": "printf bench-staged", "stdout_contains": ["bench-staged"]}
|
|
4084
|
+
{"cmd": "printf bench-staged", "stdout_contains": ["bench-staged"], "contract_refs": ["R1"]}
|
|
3792
4085
|
]
|
|
3793
4086
|
}) + "\n", encoding="utf-8")
|
|
3794
4087
|
bench_pre_staged = subprocess.run(
|
|
@@ -3807,6 +4100,38 @@ def run_self_test() -> int:
|
|
|
3807
4100
|
print("benchmark pre-staged contract was overwritten", file=sys.stderr)
|
|
3808
4101
|
return 1
|
|
3809
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
|
+
|
|
3810
4135
|
verify_output = work / "verify-output"
|
|
3811
4136
|
verify_output.mkdir()
|
|
3812
4137
|
verify_devlyn = verify_output / ".devlyn"
|
|
@@ -4442,6 +4767,84 @@ def run_self_test() -> int:
|
|
|
4442
4767
|
print("atomic_batch_state without success-order evidence was accepted", file=sys.stderr)
|
|
4443
4768
|
return 1
|
|
4444
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
|
+
|
|
4445
4848
|
# iter-0046: PLAN-declared authorized_surface enforced at BUILD_GATE.
|
|
4446
4849
|
scope_root = work / "scope-gate"
|
|
4447
4850
|
scope_root.mkdir()
|
|
@@ -4856,7 +5259,8 @@ def main() -> int:
|
|
|
4856
5259
|
# continue to legacy source-extract.
|
|
4857
5260
|
# 3. Source-extract reads
|
|
4858
5261
|
# `pipeline.state.json:source.{spec_path | criteria_path}`. If it has
|
|
4859
|
-
# a json block,
|
|
5262
|
+
# a json block, stage its commands or clear stale staging for an
|
|
5263
|
+
# explicit pure-design contract.
|
|
4860
5264
|
# 4. If source has no json block AND source.type=="generated":
|
|
4861
5265
|
# CRITICAL spec-verify-malformed — generated criteria must ship a
|
|
4862
5266
|
# verifiable contract per the generated-criteria output contract.
|
|
@@ -4871,6 +5275,39 @@ def main() -> int:
|
|
|
4871
5275
|
trust_bench_staged = bench_mode and pre_staged
|
|
4872
5276
|
src_type, source_md = read_source(work, devlyn_dir)
|
|
4873
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
|
+
|
|
4874
5311
|
external_diff = devlyn_dir / "external-diff.patch"
|
|
4875
5312
|
if external_diff.is_file() and state.get("mode") != "verify-only":
|
|
4876
5313
|
error = (
|
|
@@ -4888,14 +5325,15 @@ def main() -> int:
|
|
|
4888
5325
|
"intentionally verifying an external patch."
|
|
4889
5326
|
),
|
|
4890
5327
|
)
|
|
4891
|
-
return
|
|
5328
|
+
return reject_preflight()
|
|
4892
5329
|
integrity_error = source_integrity_error(src_type, state, source_md)
|
|
4893
5330
|
if integrity_error:
|
|
4894
5331
|
print(f"[spec-verify] carrier malformed: {integrity_error}", file=sys.stderr)
|
|
4895
5332
|
write_malformed_finding(devlyn_dir, integrity_error, source_md)
|
|
4896
|
-
return
|
|
5333
|
+
return reject_preflight()
|
|
4897
5334
|
expected_data: dict | None = None
|
|
4898
5335
|
expected_path: Path | None = None
|
|
5336
|
+
contract_found = False
|
|
4899
5337
|
if validate_risk_probes_only:
|
|
4900
5338
|
_risk_probes, risk_error = load_risk_probes(
|
|
4901
5339
|
devlyn_dir, source_md, require_present=True
|
|
@@ -4903,29 +5341,29 @@ def main() -> int:
|
|
|
4903
5341
|
if risk_error:
|
|
4904
5342
|
print(f"[spec-verify] risk probes malformed: {risk_error}", file=sys.stderr)
|
|
4905
5343
|
write_malformed_finding(devlyn_dir, risk_error, devlyn_dir / "risk-probes.jsonl")
|
|
4906
|
-
return
|
|
5344
|
+
return reject_preflight()
|
|
4907
5345
|
print("[spec-verify] risk probes valid", file=sys.stderr)
|
|
4908
5346
|
return 0
|
|
4909
5347
|
if source_md is not None and not trust_bench_staged:
|
|
4910
5348
|
if src_type == "spec":
|
|
4911
|
-
|
|
5349
|
+
contract_found, _staged, expected_error, expected_path, expected_data = stage_from_expected(
|
|
4912
5350
|
source_md, devlyn_dir
|
|
4913
5351
|
)
|
|
4914
5352
|
if expected_error is not None:
|
|
4915
5353
|
print(f"[spec-verify] carrier malformed: {expected_error}", file=sys.stderr)
|
|
4916
5354
|
write_malformed_finding(devlyn_dir, expected_error, expected_path)
|
|
4917
|
-
return
|
|
4918
|
-
if
|
|
4919
|
-
|
|
5355
|
+
return reject_preflight()
|
|
5356
|
+
if contract_found:
|
|
5357
|
+
error = None
|
|
4920
5358
|
else:
|
|
4921
|
-
|
|
5359
|
+
contract_found, _staged, error = stage_from_source(source_md, devlyn_dir)
|
|
4922
5360
|
else:
|
|
4923
|
-
|
|
5361
|
+
contract_found, _staged, error = stage_from_source(source_md, devlyn_dir)
|
|
4924
5362
|
if error is not None:
|
|
4925
5363
|
print(f"[spec-verify] carrier malformed: {error}", file=sys.stderr)
|
|
4926
5364
|
write_malformed_finding(devlyn_dir, error, source_md)
|
|
4927
|
-
return
|
|
4928
|
-
if not
|
|
5365
|
+
return reject_preflight()
|
|
5366
|
+
if not contract_found:
|
|
4929
5367
|
if src_type == "generated":
|
|
4930
5368
|
msg = (
|
|
4931
5369
|
f"generated {source_md.name} must include a "
|
|
@@ -4934,9 +5372,9 @@ def main() -> int:
|
|
|
4934
5372
|
)
|
|
4935
5373
|
print(f"[spec-verify] {msg}", file=sys.stderr)
|
|
4936
5374
|
write_malformed_finding(devlyn_dir, msg, source_md)
|
|
4937
|
-
return
|
|
5375
|
+
return reject_preflight()
|
|
4938
5376
|
# source.type=="spec", no block in spec markdown.
|
|
4939
|
-
if not bench_mode
|
|
5377
|
+
if not bench_mode:
|
|
4940
5378
|
# Real-user handwritten spec: silent no-op. Drop any stale
|
|
4941
5379
|
# pre-staged file so a killed prior run cannot poison this
|
|
4942
5380
|
# run's gate.
|
|
@@ -4958,11 +5396,10 @@ def main() -> int:
|
|
|
4958
5396
|
|
|
4959
5397
|
commands: list[dict] = []
|
|
4960
5398
|
if not spec_path.exists():
|
|
4961
|
-
#
|
|
4962
|
-
#
|
|
4963
|
-
#
|
|
4964
|
-
|
|
4965
|
-
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:
|
|
4966
5403
|
return 0
|
|
4967
5404
|
else:
|
|
4968
5405
|
try:
|
|
@@ -4971,27 +5408,26 @@ def main() -> int:
|
|
|
4971
5408
|
print(f"[spec-verify] error: cannot parse {spec_path}: {e}", file=sys.stderr)
|
|
4972
5409
|
return 2
|
|
4973
5410
|
|
|
4974
|
-
#
|
|
4975
|
-
#
|
|
4976
|
-
#
|
|
4977
|
-
|
|
4978
|
-
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)
|
|
4979
5415
|
if shape_err:
|
|
4980
5416
|
print(f"[spec-verify] error: {spec_path}: {shape_err}", file=sys.stderr)
|
|
4981
5417
|
write_malformed_finding(devlyn_dir, f"{spec_path}: {shape_err}", None)
|
|
4982
|
-
return
|
|
5418
|
+
return reject_preflight()
|
|
4983
5419
|
commands = list(spec["verification_commands"])
|
|
4984
5420
|
if include_risk_probes:
|
|
4985
5421
|
risk_state_error = risk_probes_state_error(state)
|
|
4986
5422
|
if risk_state_error:
|
|
4987
5423
|
print(f"[spec-verify] risk probes malformed: {risk_state_error}", file=sys.stderr)
|
|
4988
5424
|
write_malformed_finding(devlyn_dir, risk_state_error, Path("pipeline.state.json"))
|
|
4989
|
-
return
|
|
5425
|
+
return reject_preflight()
|
|
4990
5426
|
integrity_error = risk_probe_integrity_error(state, devlyn_dir)
|
|
4991
5427
|
if integrity_error:
|
|
4992
5428
|
print(f"[spec-verify] risk probes integrity failed: {integrity_error}", file=sys.stderr)
|
|
4993
5429
|
write_risk_probe_integrity_finding(devlyn_dir, integrity_error)
|
|
4994
|
-
return
|
|
5430
|
+
return reject_preflight()
|
|
4995
5431
|
risk_probes, risk_error = load_risk_probes(
|
|
4996
5432
|
devlyn_dir,
|
|
4997
5433
|
source_md,
|
|
@@ -5000,7 +5436,7 @@ def main() -> int:
|
|
|
5000
5436
|
if risk_error:
|
|
5001
5437
|
print(f"[spec-verify] risk probes malformed: {risk_error}", file=sys.stderr)
|
|
5002
5438
|
write_malformed_finding(devlyn_dir, risk_error, devlyn_dir / "risk-probes.jsonl")
|
|
5003
|
-
return
|
|
5439
|
+
return reject_preflight()
|
|
5004
5440
|
commands.extend(risk_probes)
|
|
5005
5441
|
|
|
5006
5442
|
devlyn_dir.mkdir(parents=True, exist_ok=True)
|