factoryline-code-factory 0.5.1__py3-none-any.whl
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.
- factoryline/__init__.py +1 -0
- factoryline/app_builder.py +493 -0
- factoryline/assembly.py +263 -0
- factoryline/attribution.py +113 -0
- factoryline/boundary.py +23 -0
- factoryline/challenge.py +53 -0
- factoryline/cli.py +477 -0
- factoryline/contract.py +169 -0
- factoryline/coverage.py +76 -0
- factoryline/meter.py +160 -0
- factoryline/optimizer.py +152 -0
- factoryline/passport.py +148 -0
- factoryline/proof.py +546 -0
- factoryline/protocol.py +74 -0
- factoryline/refinement.py +81 -0
- factoryline_code_factory-0.5.1.dist-info/METADATA +255 -0
- factoryline_code_factory-0.5.1.dist-info/RECORD +24 -0
- factoryline_code_factory-0.5.1.dist-info/WHEEL +5 -0
- factoryline_code_factory-0.5.1.dist-info/entry_points.txt +2 -0
- factoryline_code_factory-0.5.1.dist-info/licenses/LICENSE +17 -0
- factoryline_code_factory-0.5.1.dist-info/licenses/LICENSE-APACHE +19 -0
- factoryline_code_factory-0.5.1.dist-info/licenses/LICENSE-MIT +22 -0
- factoryline_code_factory-0.5.1.dist-info/licenses/NOTICE +7 -0
- factoryline_code_factory-0.5.1.dist-info/top_level.txt +1 -0
factoryline/assembly.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""factoryline.assembly — line the Lego pieces up.
|
|
2
|
+
|
|
3
|
+
factoryline drives whichever modules are installed by shelling out to their CLIs.
|
|
4
|
+
It hard-depends on none of them. A missing module is simply a stud with nothing
|
|
5
|
+
plugged in — the chain reports it and continues with what's present. This is what
|
|
6
|
+
makes the factory portable: any IDE/agent/OS that can run a subprocess can drive it.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .contract import MODULES, STAGES, ensure_layout, Receipt
|
|
16
|
+
from .meter import MeterLog, StageTiming, stopwatch
|
|
17
|
+
from .attribution import Attribution, FailureClass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ModuleStatus:
|
|
22
|
+
name: str
|
|
23
|
+
cli: str
|
|
24
|
+
installed: bool
|
|
25
|
+
role: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def detect() -> list[ModuleStatus]:
|
|
29
|
+
"""Which Lego pieces are plugged in on this machine."""
|
|
30
|
+
out = []
|
|
31
|
+
for name, meta in MODULES.items():
|
|
32
|
+
out.append(ModuleStatus(
|
|
33
|
+
name=name, cli=meta["cli"],
|
|
34
|
+
installed=shutil.which(meta["cli"]) is not None,
|
|
35
|
+
role=meta["role"]))
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_cli(cli: str, args: list[str], cwd: Path) -> tuple[bool, str]:
|
|
40
|
+
try:
|
|
41
|
+
proc = subprocess.run([cli, *args], cwd=str(cwd),
|
|
42
|
+
capture_output=True, text=True, timeout=300)
|
|
43
|
+
# Parse structured evidence before truncating anything for the receipt.
|
|
44
|
+
return proc.returncode == 0, proc.stdout + proc.stderr
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
return False, f"{cli} not installed"
|
|
47
|
+
except subprocess.TimeoutExpired:
|
|
48
|
+
return False, f"{cli} timed out"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _attribution_from_output(output: str) -> dict | None:
|
|
52
|
+
"""Find a structured attribution block in a CLI's JSON output."""
|
|
53
|
+
decoder = __import__("json").JSONDecoder()
|
|
54
|
+
for offset, char in enumerate(output):
|
|
55
|
+
if char != "{":
|
|
56
|
+
continue
|
|
57
|
+
try:
|
|
58
|
+
payload, _ = decoder.raw_decode(output[offset:])
|
|
59
|
+
except ValueError:
|
|
60
|
+
continue
|
|
61
|
+
if isinstance(payload, dict):
|
|
62
|
+
raw = payload.get("attribution")
|
|
63
|
+
if isinstance(raw, dict):
|
|
64
|
+
Attribution.from_dict(raw)
|
|
65
|
+
return raw
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# The default pipeline: (module, cli-args-template). {f} = feature.
|
|
70
|
+
# Only stages whose module is installed run; UI stage runs only if smoke/<f>.ui exists.
|
|
71
|
+
DEFAULT_CHAIN = [
|
|
72
|
+
("specline", ["strict", "{f}", "--json"]),
|
|
73
|
+
("specline", ["verify-validators", "{f}", "--json"]),
|
|
74
|
+
("specline", ["gate", "spec", "{f}"]),
|
|
75
|
+
("specline", ["tasks", "{f}"]),
|
|
76
|
+
("specline", ["gate", "plan", "{f}"]),
|
|
77
|
+
("forgeline", ["architect", "{f}", "{f}.ssat.yaml"]),
|
|
78
|
+
("forgeline", ["review", "{f}", "{f}.ssat.yaml"]),
|
|
79
|
+
("forgeline", ["arch-gate", "{f}", "{f}.ssat.yaml"]),
|
|
80
|
+
("forgeline", ["verify-tests", "{f}", "{f}.ssat.yaml"]),
|
|
81
|
+
("forgeline", ["smoke", "{f}"]),
|
|
82
|
+
("hsf", ["compile", "specs/{f}.yaml"]),
|
|
83
|
+
("forgeline", ["ship", "{f}"]),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _stage_order(module: str, stage: str) -> tuple[int, str, str]:
|
|
88
|
+
"""Return canonical pipeline order for a module stage.
|
|
89
|
+
|
|
90
|
+
Receipt rollups can arrive in display order, filesystem timestamp order, or
|
|
91
|
+
mixed legacy spellings. Failure diagnosis still follows pipeline order:
|
|
92
|
+
instrument verification precedes trusting runtime smoke output.
|
|
93
|
+
"""
|
|
94
|
+
normalized = stage.replace("_", "-")
|
|
95
|
+
order = {
|
|
96
|
+
(mod, args[0].replace("_", "-")): index
|
|
97
|
+
for index, (mod, args) in enumerate(DEFAULT_CHAIN)
|
|
98
|
+
}
|
|
99
|
+
return (order.get((module, normalized), len(DEFAULT_CHAIN)), module, normalized)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def assemble(root: Path, feature: str, chain=None, dry_run: bool = False) -> dict:
|
|
103
|
+
"""Run the assembly line for a feature. Returns a per-stage report.
|
|
104
|
+
Missing modules are skipped with a clear note (Lego stud left open)."""
|
|
105
|
+
root = Path(root); ensure_layout(root)
|
|
106
|
+
chain = chain or DEFAULT_CHAIN
|
|
107
|
+
installed = {m.name: m for m in detect()}
|
|
108
|
+
meterlog = MeterLog(root)
|
|
109
|
+
report = {"feature": feature, "root": str(root), "stages": [], "dry_run": dry_run}
|
|
110
|
+
|
|
111
|
+
spec_path = root / "specs" / f"{feature}.md"
|
|
112
|
+
if not dry_run and not spec_path.exists() and installed["specline"].installed:
|
|
113
|
+
with stopwatch() as sw:
|
|
114
|
+
ok, out = _run_cli(MODULES["specline"]["cli"], ["new", feature], root)
|
|
115
|
+
Receipt(module="specline", stage="new", feature=feature, ok=ok,
|
|
116
|
+
outputs={"log_tail": out[-2000:]}).write(root)
|
|
117
|
+
report["stages"].append({"module": "specline", "stage": "new",
|
|
118
|
+
"status": "ok" if ok else "failed", "wall_ms": sw.wall_ms})
|
|
119
|
+
if not ok:
|
|
120
|
+
report["halted_at"] = "specline:new"
|
|
121
|
+
else:
|
|
122
|
+
report["paused_at"] = "author_spec"
|
|
123
|
+
report["next_command"] = f"edit specs/{feature}.md and plans/{feature}.md, then rerun factory assemble {feature}"
|
|
124
|
+
report["rollup"] = rollup_attributions(report["stages"])
|
|
125
|
+
return report
|
|
126
|
+
|
|
127
|
+
for module, args_tmpl in chain:
|
|
128
|
+
cli = MODULES[module]["cli"]
|
|
129
|
+
present = installed[module].installed
|
|
130
|
+
args = [a.replace("{f}", feature) for a in args_tmpl]
|
|
131
|
+
stage_name = args[0]
|
|
132
|
+
if not present:
|
|
133
|
+
report["stages"].append({"module": module, "stage": stage_name,
|
|
134
|
+
"status": "skipped", "reason": f"{cli} not installed"})
|
|
135
|
+
continue
|
|
136
|
+
if not dry_run and module == "forgeline" and stage_name == "architect":
|
|
137
|
+
ssat = root / f"{feature}.ssat.yaml"
|
|
138
|
+
state_path = root / ".forge" / feature / "state.json"
|
|
139
|
+
state = None
|
|
140
|
+
if state_path.exists():
|
|
141
|
+
try:
|
|
142
|
+
state = json.loads(state_path.read_text(encoding="utf-8")).get("state")
|
|
143
|
+
except (OSError, ValueError):
|
|
144
|
+
state = None
|
|
145
|
+
if not ssat.exists():
|
|
146
|
+
report["paused_at"] = "architecture_contract"
|
|
147
|
+
report["next_command"] = f"write {feature}.ssat.yaml, then run forge expand {feature}"
|
|
148
|
+
break
|
|
149
|
+
if state in {None, "intent"}:
|
|
150
|
+
ok, out = _run_cli(cli, ["expand", feature], root)
|
|
151
|
+
Receipt(module=module, stage="expand", feature=feature, ok=ok,
|
|
152
|
+
outputs={"log_tail": out[-2000:]}).write(root)
|
|
153
|
+
report["stages"].append({"module": module, "stage": "expand", "status": "ok" if ok else "failed"})
|
|
154
|
+
report["paused_at"] = "architecture_approval"
|
|
155
|
+
report["next_command"] = f"forge gate architected {feature}"
|
|
156
|
+
break
|
|
157
|
+
if state == "expanded":
|
|
158
|
+
report["paused_at"] = "architecture_approval"
|
|
159
|
+
report["next_command"] = f"forge gate architected {feature}"
|
|
160
|
+
break
|
|
161
|
+
if not dry_run and module == "forgeline" and stage_name == "review":
|
|
162
|
+
state_path = root / ".forge" / feature / "state.json"
|
|
163
|
+
if state_path.exists():
|
|
164
|
+
state = json.loads(state_path.read_text(encoding="utf-8")).get("state")
|
|
165
|
+
if state == "scaffolded":
|
|
166
|
+
report["paused_at"] = "implementation_fill"
|
|
167
|
+
report["next_command"] = f"implement the scaffold, then run forge fill {feature} {feature}.ssat.yaml"
|
|
168
|
+
break
|
|
169
|
+
if not dry_run and module == "hsf" and stage_name == "compile" and not (root / f"specs/{feature}.yaml").exists():
|
|
170
|
+
report["stages"].append({"module": module, "stage": stage_name,
|
|
171
|
+
"status": "skipped", "reason": "no deterministic decision spec"})
|
|
172
|
+
continue
|
|
173
|
+
if dry_run:
|
|
174
|
+
report["stages"].append({"module": module, "stage": stage_name,
|
|
175
|
+
"status": "would-run", "cmd": f"{cli} {' '.join(args)}"})
|
|
176
|
+
continue
|
|
177
|
+
with stopwatch() as sw:
|
|
178
|
+
ok, out = _run_cli(cli, args, root)
|
|
179
|
+
attribution_block = _attribution_from_output(out)
|
|
180
|
+
meterlog.record(StageTiming(module, stage_name, sw.wall_ms, 0, 0, 0, ok))
|
|
181
|
+
Receipt(module=module, stage=stage_name, feature=feature, ok=ok,
|
|
182
|
+
outputs={"log_tail": out[-2000:]},
|
|
183
|
+
attribution=attribution_block).write(root)
|
|
184
|
+
report["stages"].append({"module": module, "stage": stage_name,
|
|
185
|
+
"status": "ok" if ok else "failed",
|
|
186
|
+
"wall_ms": sw.wall_ms,
|
|
187
|
+
"attribution": attribution_block})
|
|
188
|
+
if not ok:
|
|
189
|
+
report["halted_at"] = f"{module}:{stage_name}"
|
|
190
|
+
break
|
|
191
|
+
report["rollup"] = rollup_attributions(report["stages"])
|
|
192
|
+
return report
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def rollup_receipts(root: Path, feature: str) -> dict:
|
|
196
|
+
"""Load compatible factory receipts and roll up the latest stage records."""
|
|
197
|
+
receipt_dir = Path(root) / "receipts"
|
|
198
|
+
latest: dict[tuple[str, str], tuple[float, dict]] = {}
|
|
199
|
+
for path in receipt_dir.glob(f"*-{feature}-*.json"):
|
|
200
|
+
try:
|
|
201
|
+
payload = __import__("json").loads(path.read_text(encoding="utf-8"))
|
|
202
|
+
receipt = Receipt.from_dict(payload)
|
|
203
|
+
except (ValueError, TypeError, OSError):
|
|
204
|
+
continue
|
|
205
|
+
latest[(receipt.module, receipt.stage)] = (
|
|
206
|
+
path.stat().st_mtime,
|
|
207
|
+
{
|
|
208
|
+
"module": receipt.module,
|
|
209
|
+
"stage": receipt.stage,
|
|
210
|
+
"status": "ok" if receipt.ok else "failed",
|
|
211
|
+
"attribution": receipt.attribution,
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
stages = [item[1] for item in sorted(latest.values(), key=lambda item: item[0])]
|
|
215
|
+
return rollup_attributions(stages)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def rollup_attributions(stages: list[dict]) -> dict:
|
|
219
|
+
"""Aggregate module attribution with canonical pipeline failure priority.
|
|
220
|
+
|
|
221
|
+
Older receipts without attribution remain visible but do not crash the line.
|
|
222
|
+
The recommendation is always the earliest failing stage, never the worst rate.
|
|
223
|
+
"""
|
|
224
|
+
rows = []
|
|
225
|
+
for stage in stages:
|
|
226
|
+
raw = stage.get("attribution")
|
|
227
|
+
if not raw:
|
|
228
|
+
rows.append({
|
|
229
|
+
**stage,
|
|
230
|
+
"order": _stage_order(stage["module"], stage["stage"])[0],
|
|
231
|
+
"rate": None,
|
|
232
|
+
"dominant_failure_class": None,
|
|
233
|
+
})
|
|
234
|
+
continue
|
|
235
|
+
attr = Attribution.from_dict(raw)
|
|
236
|
+
dominant = attr.dominant_failure_class()
|
|
237
|
+
rows.append({
|
|
238
|
+
**stage,
|
|
239
|
+
"order": _stage_order(stage["module"], stage["stage"])[0],
|
|
240
|
+
"rate": attr.rate,
|
|
241
|
+
"n_checked": attr.n_checked,
|
|
242
|
+
"n_passed": attr.n_passed,
|
|
243
|
+
"dominant_failure_class": dominant.value if dominant else None,
|
|
244
|
+
})
|
|
245
|
+
failures = [
|
|
246
|
+
row for row in rows
|
|
247
|
+
if row.get("status") == "failed" or (row["rate"] is not None and row["rate"] < 1.0)
|
|
248
|
+
]
|
|
249
|
+
first = min(
|
|
250
|
+
failures,
|
|
251
|
+
key=lambda row: _stage_order(row["module"], row["stage"]),
|
|
252
|
+
default=None,
|
|
253
|
+
)
|
|
254
|
+
return {
|
|
255
|
+
"stages": rows,
|
|
256
|
+
"earliest_failing_stage": (
|
|
257
|
+
f"{first['module']}:{first['stage']}" if first else None
|
|
258
|
+
),
|
|
259
|
+
"recommended_edit_class": (
|
|
260
|
+
"structural" if first and first["dominant_failure_class"] else
|
|
261
|
+
"inspect_stage_output" if first else None
|
|
262
|
+
),
|
|
263
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Deterministic, build-time failure attribution shared by factory modules."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FailureClass(str, Enum):
|
|
10
|
+
AMBIGUOUS_REQUIREMENT = "ambiguous_requirement"
|
|
11
|
+
UNTYPED_INPUT = "untyped_input"
|
|
12
|
+
SCOPE_ESCAPE = "scope_escape"
|
|
13
|
+
INVENTED_PARAM = "invented_param"
|
|
14
|
+
SIGNATURE_DRIFT = "signature_drift"
|
|
15
|
+
STUB_UNFILLED = "stub_unfilled"
|
|
16
|
+
COMPLEXITY_EXCEEDED = "complexity_exceeded"
|
|
17
|
+
INCONSISTENT_LOGIC = "inconsistent_logic"
|
|
18
|
+
RUNTIME_CRASH = "runtime_crash"
|
|
19
|
+
RUNTIME_TIMEOUT = "runtime_timeout"
|
|
20
|
+
WRONG_OUTPUT = "wrong_output"
|
|
21
|
+
HOLLOW_TEST = "hollow_test"
|
|
22
|
+
HOLLOW_MANIFEST = "hollow_manifest"
|
|
23
|
+
HOLLOW_VALIDATOR = "hollow_validator"
|
|
24
|
+
HOLLOW_COVERAGE = "hollow_coverage"
|
|
25
|
+
ACCURACY_REGRESSION = "accuracy_regression"
|
|
26
|
+
NONDETERMINISM = "nondeterminism"
|
|
27
|
+
SECURITY_FINDING = "security_finding"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class UnitResult:
|
|
32
|
+
unit: str
|
|
33
|
+
stage: str
|
|
34
|
+
passed: bool
|
|
35
|
+
evidence: str
|
|
36
|
+
failure_class: FailureClass | None = None
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
if not self.unit.strip() or not self.stage.strip():
|
|
40
|
+
raise ValueError("unit and stage are required")
|
|
41
|
+
if not self.passed and self.failure_class is None:
|
|
42
|
+
raise ValueError("failed units require a failure_class")
|
|
43
|
+
if not self.passed and not self.evidence.strip():
|
|
44
|
+
raise ValueError("failed units require concrete evidence")
|
|
45
|
+
if self.passed and self.failure_class is not None:
|
|
46
|
+
raise ValueError("passed units cannot carry a failure_class")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Attribution:
|
|
51
|
+
stage: str
|
|
52
|
+
n_checked: int
|
|
53
|
+
n_passed: int
|
|
54
|
+
units: list[UnitResult]
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
if self.n_checked != len(self.units):
|
|
58
|
+
raise ValueError("n_checked must equal the number of units")
|
|
59
|
+
actual_passed = sum(unit.passed for unit in self.units)
|
|
60
|
+
if self.n_passed != actual_passed:
|
|
61
|
+
raise ValueError("n_passed must equal the number of passing units")
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def rate(self) -> float:
|
|
65
|
+
return self.n_passed / self.n_checked if self.n_checked else 0.0
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def failures(self) -> list[UnitResult]:
|
|
69
|
+
return [unit for unit in self.units if not unit.passed]
|
|
70
|
+
|
|
71
|
+
def dominant_failure_class(self) -> FailureClass | None:
|
|
72
|
+
counts = {failure_class: 0 for failure_class in FailureClass}
|
|
73
|
+
for unit in self.failures:
|
|
74
|
+
counts[unit.failure_class] += 1
|
|
75
|
+
maximum = max(counts.values(), default=0)
|
|
76
|
+
if maximum == 0:
|
|
77
|
+
return None
|
|
78
|
+
return next(kind for kind in FailureClass if counts[kind] == maximum)
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict:
|
|
81
|
+
payload = asdict(self)
|
|
82
|
+
payload["rate"] = self.rate
|
|
83
|
+
dominant = self.dominant_failure_class()
|
|
84
|
+
payload["dominant_failure_class"] = dominant.value if dominant else None
|
|
85
|
+
return payload
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_dict(cls, payload: dict) -> "Attribution":
|
|
89
|
+
units = [
|
|
90
|
+
UnitResult(
|
|
91
|
+
unit=item["unit"],
|
|
92
|
+
stage=item.get("stage", payload["stage"]),
|
|
93
|
+
passed=item["passed"],
|
|
94
|
+
evidence=item.get("evidence", ""),
|
|
95
|
+
failure_class=(
|
|
96
|
+
FailureClass(item["failure_class"])
|
|
97
|
+
if item.get("failure_class")
|
|
98
|
+
else None
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
for item in payload.get("units", [])
|
|
102
|
+
]
|
|
103
|
+
return cls(
|
|
104
|
+
stage=payload["stage"],
|
|
105
|
+
n_checked=payload.get("n_checked", len(units)),
|
|
106
|
+
n_passed=payload.get("n_passed", sum(unit.passed for unit in units)),
|
|
107
|
+
units=units,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def attribution(stage: str, units: Iterable[UnitResult]) -> Attribution:
|
|
112
|
+
materialized = list(units)
|
|
113
|
+
return Attribution(stage, len(materialized), sum(unit.passed for unit in materialized), materialized)
|
factoryline/boundary.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Guards that keep build-time learning state out of deterministic artifacts."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
BUILD_TIME_ONLY = {"attribution", "refine", "rejection_ledger", "edit_selection"}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def assert_no_attribution_in_artifact(artifact_path: Path) -> None:
|
|
8
|
+
source = Path(artifact_path).read_text(encoding="utf-8")
|
|
9
|
+
leaked = sorted(symbol for symbol in BUILD_TIME_ONLY if symbol in source)
|
|
10
|
+
if leaked:
|
|
11
|
+
raise ValueError(f"build-time symbols leaked into artifact: {', '.join(leaked)}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def assert_build_metadata_locations(root: Path) -> None:
|
|
15
|
+
registry = Path(root) / "registry"
|
|
16
|
+
if not registry.exists():
|
|
17
|
+
return
|
|
18
|
+
offenders = [
|
|
19
|
+
path for path in registry.rglob("*")
|
|
20
|
+
if path.is_file() and any(token in path.name for token in BUILD_TIME_ONLY)
|
|
21
|
+
]
|
|
22
|
+
if offenders:
|
|
23
|
+
raise ValueError(f"build metadata found in registry: {offenders}")
|
factoryline/challenge.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Counterfactual integrity challenge for Factoryline proof verification."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import json
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
from .proof import load_trace, verify_trace
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def challenge_trace(trace_path: Path, root: Path | None = None) -> dict:
|
|
13
|
+
trace_path = Path(trace_path)
|
|
14
|
+
trace = load_trace(trace_path)
|
|
15
|
+
baseline = verify_trace(trace_path, root=root)
|
|
16
|
+
mutations = []
|
|
17
|
+
variants = []
|
|
18
|
+
|
|
19
|
+
empty = deepcopy(trace)
|
|
20
|
+
empty["nodes"] = []
|
|
21
|
+
variants.append(("empty_trace", empty))
|
|
22
|
+
|
|
23
|
+
bad_hash = deepcopy(trace)
|
|
24
|
+
if bad_hash.get("nodes"):
|
|
25
|
+
bad_hash["nodes"][0]["receipt_sha256"] = "0" * 64
|
|
26
|
+
variants.append(("receipt_hash_tamper", bad_hash))
|
|
27
|
+
|
|
28
|
+
if len(trace.get("nodes", [])) > 1:
|
|
29
|
+
reordered = deepcopy(trace)
|
|
30
|
+
reordered["nodes"] = list(reversed(reordered["nodes"]))
|
|
31
|
+
variants.append(("stage_reorder", reordered))
|
|
32
|
+
|
|
33
|
+
with tempfile.TemporaryDirectory(prefix="factory-challenge-") as temp:
|
|
34
|
+
for name, variant in variants:
|
|
35
|
+
path = Path(temp) / f"{name}.json"
|
|
36
|
+
path.write_text(json.dumps(variant, indent=2), encoding="utf-8")
|
|
37
|
+
result = verify_trace(path, root=root)
|
|
38
|
+
mutations.append({
|
|
39
|
+
"unit": name,
|
|
40
|
+
"killed": not result["valid"],
|
|
41
|
+
"evidence": "; ".join(result["errors"]) if result["errors"] else "mutant incorrectly verified",
|
|
42
|
+
})
|
|
43
|
+
killed = sum(bool(item["killed"]) for item in mutations)
|
|
44
|
+
return {
|
|
45
|
+
"schema": "factory.challenge.v1",
|
|
46
|
+
"brick": "factoryline",
|
|
47
|
+
"feature": trace.get("feature"),
|
|
48
|
+
"stage": "proof_integrity_counterfactual",
|
|
49
|
+
"passed": baseline["valid"] and killed == len(mutations),
|
|
50
|
+
"mutants_total": len(mutations),
|
|
51
|
+
"mutants_killed": killed,
|
|
52
|
+
"mutations": mutations,
|
|
53
|
+
}
|