code-factory-2-forge 0.10.2__tar.gz → 0.10.4__tar.gz
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.
- {code_factory_2_forge-0.10.2/code_factory_2_forge.egg-info → code_factory_2_forge-0.10.4}/PKG-INFO +21 -4
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/README.md +20 -3
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4/code_factory_2_forge.egg-info}/PKG-INFO +21 -4
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/__init__.py +1 -1
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/qa_audit.py +15 -10
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/orchestrator.py +50 -6
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/provenance.py +41 -4
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/run_store.py +14 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/source_scope.py +58 -11
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/ssat.py +52 -9
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/pyproject.toml +1 -1
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/tests/test_forgeline.py +62 -1
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/LICENSE-APACHE +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/LICENSE-MIT +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/NOTICE +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/code_factory_2_forge.egg-info/SOURCES.txt +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/code_factory_2_forge.egg-info/dependency_links.txt +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/code_factory_2_forge.egg-info/entry_points.txt +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/code_factory_2_forge.egg-info/requires.txt +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/code_factory_2_forge.egg-info/top_level.txt +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/adapters.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/adoption.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/attribution.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/cli.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/demo.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/demo_learning.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/__init__.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/adversary.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/judge.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/reverse_classical.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/runtime_smoke.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/skill_check.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/typescript_mutants.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/intent_thread.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/learning.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/pr_optimizer.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/refinement.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/skill_memory.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/states.py +0 -0
- {code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/setup.cfg +0 -0
{code_factory_2_forge-0.10.2/code_factory_2_forge.egg-info → code_factory_2_forge-0.10.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-factory-2-forge
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.4
|
|
4
4
|
Summary: ForgeLine — the autonomous outer loop for AI software factories. A CLI-backed state machine that drives intent -> spec -> plan -> code -> adversarial review -> ship, orchestrating SpecLine (spec governance) and Harness Software Factory (compiled decisions), with self-improving skills and architecture-as-a-CI-gate.
|
|
5
5
|
License-Expression: MIT OR Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/zrk222/code-factory-2-forge
|
|
@@ -135,9 +135,24 @@ files and includes before/after SHA-256 values. Existing targets are reported as
|
|
|
135
135
|
conflicts and leave both the files and feature state unchanged. An intentional
|
|
136
136
|
replacement requires `--force`; ForgeLine first writes timestamped backups under
|
|
137
137
|
`.forge/scaffold-backups/`, validates every generated file, and restores every
|
|
138
|
-
modified target if any replacement fails. Python,
|
|
139
|
-
use extension-specific generators and
|
|
140
|
-
are rejected rather than receiving
|
|
138
|
+
modified target if any replacement fails. Python, JavaScript (including ESM
|
|
139
|
+
`.mjs`), TypeScript, and TSX scaffolds use extension-specific generators and
|
|
140
|
+
structure checks; unsupported extensions are rejected rather than receiving
|
|
141
|
+
Python syntax.
|
|
142
|
+
|
|
143
|
+
**Feature-scoped, language-aware QA.** `forge qa <feature> --ssat <contract>`
|
|
144
|
+
only grades files declared by that contract. Dependency, build, cache, VCS, and
|
|
145
|
+
reparse-point trees are pruned. Python is parsed with Python; JavaScript/ESM is
|
|
146
|
+
syntax-checked by Node and inventories exported and local functions; TypeScript
|
|
147
|
+
uses its matching parser. An unavailable parser is reported as
|
|
148
|
+
`PARSER_UNSUPPORTED`, never disguised as a syntax error. A slice without
|
|
149
|
+
discoverable functions is explicitly inventory-only, not behavioral-proof green.
|
|
150
|
+
|
|
151
|
+
**Fresh proof inputs.** `forge verify-tests` hashes its SSAT, declared source,
|
|
152
|
+
smoke manifests, tests, command, and tool version. Any mismatch invalidates the
|
|
153
|
+
prior reverse-classical receipt and reruns the proof. Inspect the active binary
|
|
154
|
+
with `forge version --json`; it reports version, origin, build hash, runtime,
|
|
155
|
+
and whether source identity is complete.
|
|
141
156
|
|
|
142
157
|
**The grumpy adversary.** A review agent that *assumes your code is broken and
|
|
143
158
|
insecure* and makes the generator prove otherwise. Executable heuristics catch
|
|
@@ -182,6 +197,8 @@ forge arch-gate <feature> <ssat> architecture CI gate
|
|
|
182
197
|
forge verify-tests <feature> <ssat> prove smoke checks fail on generated stubs
|
|
183
198
|
forge verify-tests-ts <feature> prove existing TypeScript tests fail on reviewed source mutants
|
|
184
199
|
forge challenge <feature> <ssat> write a Factory Passport challenge receipt
|
|
200
|
+
forge qa <feature> --ssat <ssat> --strict run scoped language-aware QA
|
|
201
|
+
forge version --json print install and build provenance
|
|
185
202
|
forge smoke <feature> runtime behavior gate
|
|
186
203
|
forge ship <feature> seal it
|
|
187
204
|
forge handoff <feature> <spec> route decision tables to HSF
|
|
@@ -117,9 +117,24 @@ files and includes before/after SHA-256 values. Existing targets are reported as
|
|
|
117
117
|
conflicts and leave both the files and feature state unchanged. An intentional
|
|
118
118
|
replacement requires `--force`; ForgeLine first writes timestamped backups under
|
|
119
119
|
`.forge/scaffold-backups/`, validates every generated file, and restores every
|
|
120
|
-
modified target if any replacement fails. Python,
|
|
121
|
-
use extension-specific generators and
|
|
122
|
-
are rejected rather than receiving
|
|
120
|
+
modified target if any replacement fails. Python, JavaScript (including ESM
|
|
121
|
+
`.mjs`), TypeScript, and TSX scaffolds use extension-specific generators and
|
|
122
|
+
structure checks; unsupported extensions are rejected rather than receiving
|
|
123
|
+
Python syntax.
|
|
124
|
+
|
|
125
|
+
**Feature-scoped, language-aware QA.** `forge qa <feature> --ssat <contract>`
|
|
126
|
+
only grades files declared by that contract. Dependency, build, cache, VCS, and
|
|
127
|
+
reparse-point trees are pruned. Python is parsed with Python; JavaScript/ESM is
|
|
128
|
+
syntax-checked by Node and inventories exported and local functions; TypeScript
|
|
129
|
+
uses its matching parser. An unavailable parser is reported as
|
|
130
|
+
`PARSER_UNSUPPORTED`, never disguised as a syntax error. A slice without
|
|
131
|
+
discoverable functions is explicitly inventory-only, not behavioral-proof green.
|
|
132
|
+
|
|
133
|
+
**Fresh proof inputs.** `forge verify-tests` hashes its SSAT, declared source,
|
|
134
|
+
smoke manifests, tests, command, and tool version. Any mismatch invalidates the
|
|
135
|
+
prior reverse-classical receipt and reruns the proof. Inspect the active binary
|
|
136
|
+
with `forge version --json`; it reports version, origin, build hash, runtime,
|
|
137
|
+
and whether source identity is complete.
|
|
123
138
|
|
|
124
139
|
**The grumpy adversary.** A review agent that *assumes your code is broken and
|
|
125
140
|
insecure* and makes the generator prove otherwise. Executable heuristics catch
|
|
@@ -164,6 +179,8 @@ forge arch-gate <feature> <ssat> architecture CI gate
|
|
|
164
179
|
forge verify-tests <feature> <ssat> prove smoke checks fail on generated stubs
|
|
165
180
|
forge verify-tests-ts <feature> prove existing TypeScript tests fail on reviewed source mutants
|
|
166
181
|
forge challenge <feature> <ssat> write a Factory Passport challenge receipt
|
|
182
|
+
forge qa <feature> --ssat <ssat> --strict run scoped language-aware QA
|
|
183
|
+
forge version --json print install and build provenance
|
|
167
184
|
forge smoke <feature> runtime behavior gate
|
|
168
185
|
forge ship <feature> seal it
|
|
169
186
|
forge handoff <feature> <spec> route decision tables to HSF
|
{code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4/code_factory_2_forge.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-factory-2-forge
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.4
|
|
4
4
|
Summary: ForgeLine — the autonomous outer loop for AI software factories. A CLI-backed state machine that drives intent -> spec -> plan -> code -> adversarial review -> ship, orchestrating SpecLine (spec governance) and Harness Software Factory (compiled decisions), with self-improving skills and architecture-as-a-CI-gate.
|
|
5
5
|
License-Expression: MIT OR Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/zrk222/code-factory-2-forge
|
|
@@ -135,9 +135,24 @@ files and includes before/after SHA-256 values. Existing targets are reported as
|
|
|
135
135
|
conflicts and leave both the files and feature state unchanged. An intentional
|
|
136
136
|
replacement requires `--force`; ForgeLine first writes timestamped backups under
|
|
137
137
|
`.forge/scaffold-backups/`, validates every generated file, and restores every
|
|
138
|
-
modified target if any replacement fails. Python,
|
|
139
|
-
use extension-specific generators and
|
|
140
|
-
are rejected rather than receiving
|
|
138
|
+
modified target if any replacement fails. Python, JavaScript (including ESM
|
|
139
|
+
`.mjs`), TypeScript, and TSX scaffolds use extension-specific generators and
|
|
140
|
+
structure checks; unsupported extensions are rejected rather than receiving
|
|
141
|
+
Python syntax.
|
|
142
|
+
|
|
143
|
+
**Feature-scoped, language-aware QA.** `forge qa <feature> --ssat <contract>`
|
|
144
|
+
only grades files declared by that contract. Dependency, build, cache, VCS, and
|
|
145
|
+
reparse-point trees are pruned. Python is parsed with Python; JavaScript/ESM is
|
|
146
|
+
syntax-checked by Node and inventories exported and local functions; TypeScript
|
|
147
|
+
uses its matching parser. An unavailable parser is reported as
|
|
148
|
+
`PARSER_UNSUPPORTED`, never disguised as a syntax error. A slice without
|
|
149
|
+
discoverable functions is explicitly inventory-only, not behavioral-proof green.
|
|
150
|
+
|
|
151
|
+
**Fresh proof inputs.** `forge verify-tests` hashes its SSAT, declared source,
|
|
152
|
+
smoke manifests, tests, command, and tool version. Any mismatch invalidates the
|
|
153
|
+
prior reverse-classical receipt and reruns the proof. Inspect the active binary
|
|
154
|
+
with `forge version --json`; it reports version, origin, build hash, runtime,
|
|
155
|
+
and whether source identity is complete.
|
|
141
156
|
|
|
142
157
|
**The grumpy adversary.** A review agent that *assumes your code is broken and
|
|
143
158
|
insecure* and makes the generator prove otherwise. Executable heuristics catch
|
|
@@ -182,6 +197,8 @@ forge arch-gate <feature> <ssat> architecture CI gate
|
|
|
182
197
|
forge verify-tests <feature> <ssat> prove smoke checks fail on generated stubs
|
|
183
198
|
forge verify-tests-ts <feature> prove existing TypeScript tests fail on reviewed source mutants
|
|
184
199
|
forge challenge <feature> <ssat> write a Factory Passport challenge receipt
|
|
200
|
+
forge qa <feature> --ssat <ssat> --strict run scoped language-aware QA
|
|
201
|
+
forge version --json print install and build provenance
|
|
185
202
|
forge smoke <feature> runtime behavior gate
|
|
186
203
|
forge ship <feature> seal it
|
|
187
204
|
forge handoff <feature> <spec> route decision tables to HSF
|
|
@@ -14,10 +14,10 @@ COMPLEXITY_LIMIT = 10
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
16
16
|
class QAReport:
|
|
17
|
-
coverage_intent: float = 0.0
|
|
17
|
+
coverage_intent: float | None = 0.0
|
|
18
18
|
max_complexity: int = 0
|
|
19
19
|
security_score: int = 100
|
|
20
|
-
doc_ratio: float = 0.0
|
|
20
|
+
doc_ratio: float | None = 0.0
|
|
21
21
|
grade: str = "F"
|
|
22
22
|
findings: list[str] = field(default_factory=list)
|
|
23
23
|
metrics: dict = field(default_factory=dict)
|
|
@@ -52,9 +52,6 @@ class QAReport:
|
|
|
52
52
|
units.append(UnitResult(f"qa_audit:{finding.split()[1]}", "qa_audit", False, finding, FailureClass.PARSER_UNSUPPORTED))
|
|
53
53
|
elif finding.startswith("QA_SYNTAX"):
|
|
54
54
|
units.append(UnitResult(f"qa_audit:{finding.split()[1]}", "qa_audit", False, finding, FailureClass.SYNTAX_ERROR))
|
|
55
|
-
if not units:
|
|
56
|
-
units.append(UnitResult("qa_audit:<no-public-functions>", "qa_audit", False,
|
|
57
|
-
"no supported public functions were available to grade", FailureClass.PARSER_UNSUPPORTED))
|
|
58
55
|
return Attribution("qa_audit", len(units), sum(unit.passed for unit in units), units)
|
|
59
56
|
|
|
60
57
|
|
|
@@ -130,20 +127,26 @@ def qa_audit(src_dir: Path, *, source_paths: Iterable[Path] | None = None) -> QA
|
|
|
130
127
|
report.security_score -= deduction
|
|
131
128
|
report.findings.append(f"QA_SEC[{severity}] {message} in {path.relative_to(root).as_posix()}")
|
|
132
129
|
|
|
133
|
-
count = len(report.function_metrics)
|
|
134
|
-
report.coverage_intent = round(sum(metric["tested"] for metric in report.function_metrics) / count, 2)
|
|
135
|
-
report.doc_ratio = round(sum(metric["documented"] for metric in report.function_metrics) / count, 2)
|
|
130
|
+
count = len(report.function_metrics)
|
|
131
|
+
report.coverage_intent = round(sum(metric["tested"] for metric in report.function_metrics) / count, 2) if count else None
|
|
132
|
+
report.doc_ratio = round(sum(metric["documented"] for metric in report.function_metrics) / count, 2) if count else None
|
|
136
133
|
report.max_complexity = max((metric["complexity"] for metric in report.function_metrics), default=0)
|
|
137
134
|
report.security_score = max(report.security_score, 0)
|
|
138
135
|
for metric in report.function_metrics:
|
|
139
136
|
if metric["complexity"] > COMPLEXITY_LIMIT:
|
|
140
137
|
report.findings.append(f"QA_COMPLEXITY {metric['function']} complexity {metric['complexity']} > {COMPLEXITY_LIMIT}; policy=hard")
|
|
141
138
|
|
|
142
|
-
|
|
139
|
+
# A source slice with no declared functions has no coverage *claim*. It is
|
|
140
|
+
# syntax/security inventory only, rather than a synthetic zero-coverage D.
|
|
141
|
+
score = 35 * report.coverage_intent if report.coverage_intent is not None else 35
|
|
143
142
|
score += 25 * (1 if report.max_complexity <= COMPLEXITY_LIMIT else max(0, 1 - (report.max_complexity - COMPLEXITY_LIMIT) / 10))
|
|
144
143
|
score += 25 * (report.security_score / 100)
|
|
145
|
-
score += 15 * report.doc_ratio
|
|
144
|
+
score += 15 * report.doc_ratio if report.doc_ratio is not None else 15
|
|
146
145
|
report.grade = "A" if score >= 85 else "B" if score >= 70 else "C" if score >= 55 else "D" if score >= 40 else "F"
|
|
146
|
+
# No executable symbols means no behavioral proof, only a syntax/security
|
|
147
|
+
# inventory. Do not surface that inventory as an aggregate green result.
|
|
148
|
+
if not count and report.grade == "A":
|
|
149
|
+
report.grade = "B"
|
|
147
150
|
if report.max_complexity > COMPLEXITY_LIMIT and report.grade in {"A", "B"}:
|
|
148
151
|
report.grade = "C"
|
|
149
152
|
if any(finding.startswith(("QA_SYNTAX", "QA_PARSER_UNSUPPORTED")) for finding in report.findings):
|
|
@@ -152,6 +155,8 @@ def qa_audit(src_dir: Path, *, source_paths: Iterable[Path] | None = None) -> QA
|
|
|
152
155
|
"coverage_intent": report.coverage_intent, "max_complexity": report.max_complexity,
|
|
153
156
|
"security_score": report.security_score, "doc_ratio": report.doc_ratio,
|
|
154
157
|
"composite": round(score, 1), "complexity_policy": "hard",
|
|
158
|
+
"coverage_assessment": "measured" if count else "not_applicable_no_declared_functions",
|
|
159
|
+
"behavioral_proof_status": "available" if count else "unavailable_no_declared_functions",
|
|
155
160
|
"scope": report.scope, "skipped_paths": report.skipped_paths,
|
|
156
161
|
}
|
|
157
162
|
return report
|
|
@@ -3,7 +3,7 @@ governance and HSF for decision compilation when a spec carries a decision
|
|
|
3
3
|
table. Everything is a receipt; every gate failure records a skill lesson and
|
|
4
4
|
routes to the refine loop."""
|
|
5
5
|
from __future__ import annotations
|
|
6
|
-
import shutil, subprocess, sys
|
|
6
|
+
import hashlib, json, shutil, subprocess, sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from .states import State, can_transition, IllegalTransition, HUMAN_GATES
|
|
9
9
|
from .run_store import RunStore
|
|
@@ -14,6 +14,7 @@ from .source_scope import declared_paths
|
|
|
14
14
|
from .skill_memory import record_lesson, inject_lessons_block, lessons_for
|
|
15
15
|
from .learning import LearningKernel
|
|
16
16
|
from .attribution import Attribution, FailureClass, UnitResult
|
|
17
|
+
from . import __version__
|
|
17
18
|
|
|
18
19
|
MAX_REFINE = 3
|
|
19
20
|
|
|
@@ -60,6 +61,29 @@ def _judge_failure_class(findings: list[str]) -> FailureClass:
|
|
|
60
61
|
return failure_class
|
|
61
62
|
return FailureClass.INCONSISTENT_LOGIC
|
|
62
63
|
|
|
64
|
+
|
|
65
|
+
def _verify_tests_fingerprint(root: Path, feature: str, ssat_path: Path) -> dict:
|
|
66
|
+
"""Hash every verified input so stale reverse-classical receipts cannot pass."""
|
|
67
|
+
from .source_scope import iter_source_files
|
|
68
|
+
root = Path(root).resolve()
|
|
69
|
+
ssat_path = Path(ssat_path).resolve()
|
|
70
|
+
ssat = load_ssat(ssat_path)
|
|
71
|
+
components: dict[str, str] = {"ssat": hashlib.sha256(Path(ssat_path).read_bytes()).hexdigest()}
|
|
72
|
+
for path in declared_paths(root, ssat):
|
|
73
|
+
key = f"source:{path.relative_to(root).as_posix()}"
|
|
74
|
+
components[key] = hashlib.sha256(path.read_bytes()).hexdigest() if path.exists() else "MISSING"
|
|
75
|
+
smoke_paths = [root / "smoke" / f"{feature}.json", root / "smoke" / "smoke.json"]
|
|
76
|
+
for path in smoke_paths:
|
|
77
|
+
if path.exists():
|
|
78
|
+
components[f"smoke:{path.relative_to(root).as_posix()}"] = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
79
|
+
for path in iter_source_files(root):
|
|
80
|
+
if path.name.startswith("test_") or path.parent.name in {"tests", "test", "__tests__"} or ".test." in path.name or ".spec." in path.name:
|
|
81
|
+
components[f"test:{path.relative_to(root).as_posix()}"] = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
82
|
+
components["command"] = "forge verify-tests"
|
|
83
|
+
components["tool_version"] = __version__
|
|
84
|
+
canonical = json.dumps(components, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
85
|
+
return {"sha256": hashlib.sha256(canonical).hexdigest(), "components": components}
|
|
86
|
+
|
|
63
87
|
class Orchestrator:
|
|
64
88
|
def __init__(self, root: Path, feature: str):
|
|
65
89
|
self.root = Path(root); self.feature = feature
|
|
@@ -274,17 +298,35 @@ class Orchestrator:
|
|
|
274
298
|
def verify_tests(self, ssat_path: Path) -> dict:
|
|
275
299
|
"""Verify smoke checks can fail before trusting smoke results."""
|
|
276
300
|
from .gates.reverse_classical import verify_tests
|
|
301
|
+
fingerprint = _verify_tests_fingerprint(self.root, self.feature, Path(ssat_path))
|
|
277
302
|
if self.store.state == State.SHIPPED:
|
|
278
|
-
|
|
303
|
+
previous = self.store.latest_receipt("verify_tests")
|
|
304
|
+
if previous and previous.get("input_fingerprint") == fingerprint["sha256"]:
|
|
305
|
+
return {"verified": True, "note": "already shipped", "input_fingerprint": fingerprint["sha256"]}
|
|
306
|
+
return {
|
|
307
|
+
"verified": False,
|
|
308
|
+
"reason": "shipped artifact has stale verify-tests evidence; open a new feature run",
|
|
309
|
+
"input_fingerprint": fingerprint["sha256"],
|
|
310
|
+
"previous_fingerprint": previous.get("input_fingerprint") if previous else None,
|
|
311
|
+
}
|
|
279
312
|
if self.store.state in {State.TESTS_VERIFIED, State.SMOKED}:
|
|
280
|
-
|
|
313
|
+
previous = self.store.latest_receipt("verify_tests")
|
|
314
|
+
if previous and previous.get("input_fingerprint") == fingerprint["sha256"]:
|
|
315
|
+
return {"verified": True, "note": "already verified", "input_fingerprint": fingerprint["sha256"]}
|
|
316
|
+
# Source, tests, SSAT, command, or tool version changed. Invalidate
|
|
317
|
+
# downstream proof before rerunning rather than returning stale green.
|
|
318
|
+
self.store.set_state(State.ARCH_GATED, "verify-tests inputs changed; stale receipt invalidated")
|
|
319
|
+
self.store.receipt(phase="verify_tests_cache", verified=False,
|
|
320
|
+
reason="input fingerprint changed", input_fingerprint=fingerprint["sha256"],
|
|
321
|
+
previous_fingerprint=previous.get("input_fingerprint") if previous else None)
|
|
281
322
|
if self.store.state not in {State.ARCH_GATED, State.BLOCKED}:
|
|
282
323
|
return {"verified": False,
|
|
283
324
|
"reason": "architecture gate not passed - run `forge arch-gate` first"}
|
|
284
325
|
gate = verify_tests(self.root, self.feature, Path(ssat_path))
|
|
285
326
|
attr = gate.attribution.to_dict()
|
|
286
327
|
if not gate.passed:
|
|
287
|
-
self.store.receipt(phase="verify_tests", verified=False, attribution=attr
|
|
328
|
+
self.store.receipt(phase="verify_tests", verified=False, attribution=attr,
|
|
329
|
+
input_fingerprint=fingerprint["sha256"], input_components=fingerprint["components"])
|
|
288
330
|
if self.store.state != State.BLOCKED:
|
|
289
331
|
self._advance(State.BLOCKED, "reverse-classical test verification failed")
|
|
290
332
|
return {"verified": False,
|
|
@@ -292,8 +334,10 @@ class Orchestrator:
|
|
|
292
334
|
"attribution": attr}
|
|
293
335
|
self._advance(State.TESTS_VERIFIED, "smoke checks proven non-hollow",
|
|
294
336
|
attribution=attr)
|
|
295
|
-
self.store.receipt(phase="verify_tests", verified=True, attribution=attr
|
|
296
|
-
|
|
337
|
+
self.store.receipt(phase="verify_tests", verified=True, attribution=attr,
|
|
338
|
+
input_fingerprint=fingerprint["sha256"], input_components=fingerprint["components"])
|
|
339
|
+
return {"verified": True, "checks": gate.attribution.n_checked, "attribution": attr,
|
|
340
|
+
"input_fingerprint": fingerprint["sha256"]}
|
|
297
341
|
|
|
298
342
|
def refine(self, evaluate, propose, apply, revert, max_iters: int = 6) -> dict:
|
|
299
343
|
"""Run deterministic localized refinement.
|
|
@@ -3,14 +3,45 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import importlib.metadata
|
|
5
5
|
import json
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
6
9
|
import sys
|
|
7
10
|
from pathlib import Path
|
|
8
11
|
|
|
9
12
|
from . import __version__
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
def _source_commit(module_dir: Path) -> str | None:
|
|
16
|
+
"""Return a checked-out source revision when one is actually available."""
|
|
17
|
+
source_root = module_dir.parent
|
|
18
|
+
manifest = source_root / "pyproject.toml"
|
|
19
|
+
if not (source_root / ".git").exists() or not manifest.exists():
|
|
20
|
+
return None
|
|
21
|
+
if 'name = "code-factory-2-forge"' not in manifest.read_text(encoding="utf-8"):
|
|
22
|
+
return None
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
["git", "rev-parse", "HEAD"], cwd=source_root, capture_output=True,
|
|
26
|
+
text=True, timeout=3, check=False,
|
|
27
|
+
)
|
|
28
|
+
except OSError:
|
|
29
|
+
return None
|
|
30
|
+
return result.stdout.strip() if result.returncode == 0 else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_hash(module_dir: Path) -> str:
|
|
34
|
+
"""Stable hash of the installed Python package payload, not a claimed commit."""
|
|
35
|
+
digest = hashlib.sha256()
|
|
36
|
+
for path in sorted(module_dir.rglob("*.py")):
|
|
37
|
+
digest.update(path.relative_to(module_dir).as_posix().encode("utf-8"))
|
|
38
|
+
digest.update(path.read_bytes())
|
|
39
|
+
return digest.hexdigest()
|
|
40
|
+
|
|
41
|
+
|
|
12
42
|
def provenance() -> dict:
|
|
13
43
|
"""Return only facts available from the active installed distribution."""
|
|
44
|
+
module_dir = Path(__file__).resolve().parent
|
|
14
45
|
install_origin = "unknown"
|
|
15
46
|
direct_url: dict | None = None
|
|
16
47
|
try:
|
|
@@ -19,17 +50,19 @@ def provenance() -> dict:
|
|
|
19
50
|
if metadata_path.name.endswith(".egg-info"):
|
|
20
51
|
install_origin = "source-tree"
|
|
21
52
|
installed_module = Path(distribution.locate_file("forgeline")).resolve()
|
|
22
|
-
if installed_module !=
|
|
53
|
+
if installed_module != module_dir:
|
|
23
54
|
return {
|
|
24
55
|
"schema": "forgeline.provenance.v1",
|
|
25
56
|
"package": "code-factory-2-forge",
|
|
26
57
|
"version": __version__,
|
|
27
58
|
"source_commit": None,
|
|
28
|
-
"build_hash":
|
|
59
|
+
"build_hash": _build_hash(module_dir),
|
|
29
60
|
"install_origin": "source-tree",
|
|
30
61
|
"direct_url": None,
|
|
31
62
|
"python": sys.version.split()[0],
|
|
63
|
+
"runtime": {"python": sys.version.split()[0], "implementation": sys.implementation.name},
|
|
32
64
|
"receipt_schema": "forge.receipt.v1",
|
|
65
|
+
"identity_complete": False,
|
|
33
66
|
}
|
|
34
67
|
direct_url_text = distribution.read_text("direct_url.json")
|
|
35
68
|
if direct_url_text:
|
|
@@ -39,14 +72,18 @@ def provenance() -> dict:
|
|
|
39
72
|
install_origin = "site-packages"
|
|
40
73
|
except importlib.metadata.PackageNotFoundError:
|
|
41
74
|
pass
|
|
75
|
+
source_commit = _source_commit(module_dir)
|
|
76
|
+
build_hash = _build_hash(module_dir)
|
|
42
77
|
return {
|
|
43
78
|
"schema": "forgeline.provenance.v1",
|
|
44
79
|
"package": "code-factory-2-forge",
|
|
45
80
|
"version": __version__,
|
|
46
|
-
"source_commit":
|
|
47
|
-
"build_hash":
|
|
81
|
+
"source_commit": source_commit,
|
|
82
|
+
"build_hash": build_hash,
|
|
48
83
|
"install_origin": install_origin,
|
|
49
84
|
"direct_url": direct_url.get("url") if direct_url else None,
|
|
50
85
|
"python": sys.version.split()[0],
|
|
86
|
+
"runtime": {"python": sys.version.split()[0], "implementation": sys.implementation.name},
|
|
51
87
|
"receipt_schema": "forge.receipt.v1",
|
|
88
|
+
"identity_complete": bool(source_commit and build_hash),
|
|
52
89
|
}
|
|
@@ -41,3 +41,17 @@ class RunStore:
|
|
|
41
41
|
fields = {"h": hashlib.sha256(line.encode()).hexdigest()[:12], **fields}
|
|
42
42
|
with self.receipts.open("a") as f:
|
|
43
43
|
f.write(json.dumps(fields, sort_keys=True) + "\n")
|
|
44
|
+
|
|
45
|
+
def latest_receipt(self, phase: str) -> dict | None:
|
|
46
|
+
"""Return the most recent receipt for one phase without trusting state."""
|
|
47
|
+
if not self.receipts.exists():
|
|
48
|
+
return None
|
|
49
|
+
latest = None
|
|
50
|
+
for line in self.receipts.read_text(encoding="utf-8").splitlines():
|
|
51
|
+
try:
|
|
52
|
+
item = json.loads(line)
|
|
53
|
+
except json.JSONDecodeError:
|
|
54
|
+
continue
|
|
55
|
+
if item.get("phase") == phase:
|
|
56
|
+
latest = item
|
|
57
|
+
return latest
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import ast
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
7
8
|
import shutil
|
|
8
9
|
import subprocess
|
|
9
10
|
from pathlib import Path
|
|
@@ -167,6 +168,20 @@ def analyze_source(path: Path, root: Path) -> dict:
|
|
|
167
168
|
node = shutil.which("node")
|
|
168
169
|
if node is None:
|
|
169
170
|
return {"status": "parser_unsupported", "language": language, "text": text, "reason": "node is unavailable"}
|
|
171
|
+
# Node's parser is authoritative for JavaScript/ESM and does not need the
|
|
172
|
+
# optional TypeScript package. That makes .mjs support deterministic in a
|
|
173
|
+
# normal Node installation instead of silently producing zero symbols.
|
|
174
|
+
if language == "javascript":
|
|
175
|
+
try:
|
|
176
|
+
checked = subprocess.run(
|
|
177
|
+
[node, "--check", str(path)], cwd=Path(root), capture_output=True,
|
|
178
|
+
text=True, timeout=10, check=False,
|
|
179
|
+
)
|
|
180
|
+
except (OSError, subprocess.TimeoutExpired) as error:
|
|
181
|
+
return {"status": "parser_unsupported", "language": language, "text": text, "reason": type(error).__name__}
|
|
182
|
+
if checked.returncode != 0:
|
|
183
|
+
return {"status": "syntax_error", "language": language, "text": text, "error": checked.stderr.strip()}
|
|
184
|
+
return {"status": "ok", "language": language, "text": text, "functions": _javascript_functions(text), "parser": "node-check"}
|
|
170
185
|
try:
|
|
171
186
|
completed = subprocess.run(
|
|
172
187
|
[node, "-e", _NODE_TYPESCRIPT_AST, str(path), language], cwd=Path(root),
|
|
@@ -183,15 +198,47 @@ def analyze_source(path: Path, root: Path) -> dict:
|
|
|
183
198
|
if payload["errors"]:
|
|
184
199
|
return {"status": "syntax_error", "language": language, "text": text, "error": "; ".join(payload["errors"])}
|
|
185
200
|
return {"status": "ok", "language": language, "text": text, "functions": payload["functions"]}
|
|
186
|
-
if language == "javascript":
|
|
187
|
-
try:
|
|
188
|
-
checked = subprocess.run(
|
|
189
|
-
[node, "--check", str(path)], cwd=Path(root), capture_output=True,
|
|
190
|
-
text=True, timeout=10, check=False,
|
|
191
|
-
)
|
|
192
|
-
except (OSError, subprocess.TimeoutExpired) as error:
|
|
193
|
-
return {"status": "parser_unsupported", "language": language, "text": text, "reason": type(error).__name__}
|
|
194
|
-
if checked.returncode == 0:
|
|
195
|
-
return {"status": "ok", "language": language, "text": text, "functions": [], "parser": "node-check"}
|
|
196
|
-
return {"status": "syntax_error", "language": language, "text": text, "error": checked.stderr.strip()}
|
|
197
201
|
return {"status": "parser_unsupported", "language": language, "text": text, "reason": completed.stderr.strip() or "typescript parser is unavailable"}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _javascript_functions(source: str) -> list[dict]:
|
|
205
|
+
"""Extract named ESM/local functions after Node has validated the source.
|
|
206
|
+
|
|
207
|
+
This is intentionally an inventory, not a second syntax parser. Node owns
|
|
208
|
+
syntax validity; the conservative scanner supplies stable symbols and
|
|
209
|
+
branch counts for feature-scoped QA without requiring an npm dependency.
|
|
210
|
+
"""
|
|
211
|
+
patterns = (
|
|
212
|
+
re.compile(r"(?:^|\n)\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)[^{]*\{", re.MULTILINE),
|
|
213
|
+
re.compile(r"(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>", re.MULTILINE),
|
|
214
|
+
)
|
|
215
|
+
functions: list[dict] = []
|
|
216
|
+
seen: set[str] = set()
|
|
217
|
+
for pattern in patterns:
|
|
218
|
+
for match in pattern.finditer(source):
|
|
219
|
+
name = match.group(1)
|
|
220
|
+
if name in seen:
|
|
221
|
+
continue
|
|
222
|
+
seen.add(name)
|
|
223
|
+
start = match.start()
|
|
224
|
+
brace = source.find("{", match.end() - 1)
|
|
225
|
+
end = len(source)
|
|
226
|
+
if brace >= 0:
|
|
227
|
+
depth = 0
|
|
228
|
+
for index in range(brace, len(source)):
|
|
229
|
+
if source[index] == "{":
|
|
230
|
+
depth += 1
|
|
231
|
+
elif source[index] == "}":
|
|
232
|
+
depth -= 1
|
|
233
|
+
if depth == 0:
|
|
234
|
+
end = index + 1
|
|
235
|
+
break
|
|
236
|
+
body = source[start:end]
|
|
237
|
+
complexity = 1 + len(re.findall(r"\b(?:if|for|while|case|catch)\b|&&|\|\||\?", body))
|
|
238
|
+
prefix = source[max(0, start - 400):start]
|
|
239
|
+
functions.append({
|
|
240
|
+
"name": name,
|
|
241
|
+
"complexity": complexity,
|
|
242
|
+
"documented": bool(re.search(r"/\*\*[^*]*(?:\*(?!/)[^*]*)*\*/\s*$", prefix, re.DOTALL)),
|
|
243
|
+
})
|
|
244
|
+
return functions
|
|
@@ -80,7 +80,15 @@ class ScaffoldReport:
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
_LANGUAGE_BY_SUFFIX = {
|
|
83
|
+
_LANGUAGE_BY_SUFFIX = {
|
|
84
|
+
".py": "python",
|
|
85
|
+
".js": "javascript",
|
|
86
|
+
".jsx": "javascript",
|
|
87
|
+
".mjs": "javascript",
|
|
88
|
+
".cjs": "javascript",
|
|
89
|
+
".ts": "typescript",
|
|
90
|
+
".tsx": "typescript",
|
|
91
|
+
}
|
|
84
92
|
|
|
85
93
|
|
|
86
94
|
def load_ssat(path: Path) -> dict:
|
|
@@ -147,11 +155,39 @@ def _render_typescript(module: dict) -> str:
|
|
|
147
155
|
return "\n".join(lines)
|
|
148
156
|
|
|
149
157
|
|
|
158
|
+
def _javascript_args(args: list[str]) -> str:
|
|
159
|
+
"""Remove TypeScript-only annotations while preserving ordinary JS syntax."""
|
|
160
|
+
cleaned: list[str] = []
|
|
161
|
+
for arg in args:
|
|
162
|
+
value = arg.split(":", 1)[0].strip().rstrip("?")
|
|
163
|
+
if not value:
|
|
164
|
+
raise ValueError("JavaScript SSAT arguments must include a parameter name")
|
|
165
|
+
cleaned.append(value)
|
|
166
|
+
return ", ".join(cleaned)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _render_javascript(module: dict) -> str:
|
|
170
|
+
lines = ["/** AUTO-SCAFFOLD from SSAT. Fill bodies only; do not change signatures. */"]
|
|
171
|
+
lines.extend(_typescript_import(imp) for imp in module.get("imports", []))
|
|
172
|
+
lines.append("")
|
|
173
|
+
for function in module.get("functions", []):
|
|
174
|
+
lines.extend([
|
|
175
|
+
f"/** {function.get('doc', 'TODO')} */",
|
|
176
|
+
f"export function {function['name']}({_javascript_args(function.get('args', []))}) {{",
|
|
177
|
+
' throw new Error("NotImplementedError: FILL");',
|
|
178
|
+
"}",
|
|
179
|
+
"",
|
|
180
|
+
])
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
|
|
183
|
+
|
|
150
184
|
def _render_module(module: dict, target: Path) -> str:
|
|
151
185
|
language = _language_for(target)
|
|
152
186
|
if language == "python":
|
|
153
187
|
return _render_python(module)
|
|
154
|
-
|
|
188
|
+
if language == "typescript":
|
|
189
|
+
return _render_typescript(module)
|
|
190
|
+
return _render_javascript(module)
|
|
155
191
|
|
|
156
192
|
|
|
157
193
|
def _validate_generated_source(source: str, target: Path) -> None:
|
|
@@ -160,10 +196,17 @@ def _validate_generated_source(source: str, target: Path) -> None:
|
|
|
160
196
|
ast.parse(source)
|
|
161
197
|
return
|
|
162
198
|
if re.search(r"^\s*def\s+", source, flags=re.MULTILINE) or source.count("{") != source.count("}"):
|
|
163
|
-
raise ValueError(f"generated invalid
|
|
199
|
+
raise ValueError(f"generated invalid {language} for {target}")
|
|
164
200
|
for line in source.splitlines():
|
|
165
|
-
if "export function "
|
|
166
|
-
|
|
201
|
+
if "export function " not in line:
|
|
202
|
+
continue
|
|
203
|
+
signature = (
|
|
204
|
+
r"export function \w+\(.*\):\s*[^\s]+\s*\{"
|
|
205
|
+
if language == "typescript"
|
|
206
|
+
else r"export function \w+\(.*\)\s*\{"
|
|
207
|
+
)
|
|
208
|
+
if not re.search(signature, line):
|
|
209
|
+
raise ValueError(f"generated invalid {language} signature for {target}")
|
|
167
210
|
|
|
168
211
|
|
|
169
212
|
def scaffold_from_ssat(
|
|
@@ -268,8 +311,8 @@ def _module_of(path: str, ssat: dict) -> str | None:
|
|
|
268
311
|
return None
|
|
269
312
|
|
|
270
313
|
|
|
271
|
-
def
|
|
272
|
-
"""A TypeScript
|
|
314
|
+
def _javascript_erosion(module: dict, path: Path, names: dict, allowed: set[tuple[str, str]]) -> list[ArchViolation]:
|
|
315
|
+
"""A JavaScript/TypeScript structure check. Python's AST is never used here."""
|
|
273
316
|
source = path.read_text(encoding="utf-8")
|
|
274
317
|
violations: list[ArchViolation] = []
|
|
275
318
|
if re.search(r"^\s*def\s+", source, flags=re.MULTILINE) or source.count("{") != source.count("}"):
|
|
@@ -401,8 +444,8 @@ def check_erosion(ssat: dict, src_dir: Path) -> list[ArchViolation]:
|
|
|
401
444
|
except ValueError as exc:
|
|
402
445
|
violations.append(ArchViolation("E_UNSUPPORTED_LANGUAGE", str(exc), module["path"]))
|
|
403
446
|
continue
|
|
404
|
-
if language
|
|
405
|
-
violations.extend(
|
|
447
|
+
if language in {"typescript", "javascript"}:
|
|
448
|
+
violations.extend(_javascript_erosion(module, path, names, allowed))
|
|
406
449
|
continue
|
|
407
450
|
try:
|
|
408
451
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-factory-2-forge"
|
|
7
|
-
version = "0.10.
|
|
7
|
+
version = "0.10.4"
|
|
8
8
|
description = "ForgeLine — the autonomous outer loop for AI software factories. A CLI-backed state machine that drives intent -> spec -> plan -> code -> adversarial review -> ship, orchestrating SpecLine (spec governance) and Harness Software Factory (compiled decisions), with self-improving skills and architecture-as-a-CI-gate."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -105,6 +105,19 @@ def test_typescript_ssat_generates_typescript_and_compiles_when_tsc_is_available
|
|
|
105
105
|
assert completed.returncode == 0, completed.stdout + completed.stderr
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
def test_mjs_ssat_generates_valid_esm_without_python_syntax(proj):
|
|
109
|
+
report = scaffold_from_ssat(_typescript_ssat(["src/memory.mjs"]), proj)
|
|
110
|
+
target = proj / "src" / "memory.mjs"
|
|
111
|
+
source = target.read_text(encoding="utf-8")
|
|
112
|
+
assert len(report) == 1
|
|
113
|
+
assert "export function render(name)" in source
|
|
114
|
+
assert "def render" not in source
|
|
115
|
+
node = shutil.which("node")
|
|
116
|
+
if node is not None:
|
|
117
|
+
completed = subprocess.run([node, "--check", str(target)], capture_output=True, text=True)
|
|
118
|
+
assert completed.returncode == 0, completed.stdout + completed.stderr
|
|
119
|
+
|
|
120
|
+
|
|
108
121
|
def test_mixed_language_ssat_generates_each_language_and_rejects_unknown_extensions(proj):
|
|
109
122
|
report = scaffold_from_ssat(_typescript_ssat(["src/memory.ts", "src/worker.py"]), proj)
|
|
110
123
|
assert len(report.created) == 2
|
|
@@ -317,6 +330,39 @@ def test_qa_feature_slice_ignores_unrelated_python_and_never_python_parses_mjs(p
|
|
|
317
330
|
assert not any("bad.py" in finding for finding in report.findings)
|
|
318
331
|
|
|
319
332
|
|
|
333
|
+
def test_mjs_qa_extracts_esm_and_local_symbols_with_measured_coverage(proj):
|
|
334
|
+
from forgeline.gates.qa_audit import qa_audit
|
|
335
|
+
|
|
336
|
+
feature = proj / "services" / "memory.mjs"
|
|
337
|
+
feature.parent.mkdir()
|
|
338
|
+
feature.write_text(
|
|
339
|
+
"/** Recall a saved value. */\n"
|
|
340
|
+
"export function recall(id) { if (!id) return null; return id; }\n"
|
|
341
|
+
"const normalize = (value) => value.trim();\n",
|
|
342
|
+
encoding="utf-8",
|
|
343
|
+
)
|
|
344
|
+
tests = proj / "services" / "memory.test.mjs"
|
|
345
|
+
tests.write_text("import { recall } from './memory.mjs';\nrecall('x'); normalize?.(' x ');\n", encoding="utf-8")
|
|
346
|
+
|
|
347
|
+
report = qa_audit(proj, source_paths=[feature])
|
|
348
|
+
names = {metric["function"].split(":")[-1] for metric in report.function_metrics}
|
|
349
|
+
assert {"recall", "normalize"} <= names
|
|
350
|
+
assert report.coverage_intent is not None and report.coverage_intent > 0
|
|
351
|
+
assert report.metrics["coverage_assessment"] == "measured"
|
|
352
|
+
assert not any("PARSER_UNSUPPORTED" in finding or "QA_SYNTAX" in finding for finding in report.findings)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_mjs_invalid_syntax_is_not_misattributed_as_python_error(proj):
|
|
356
|
+
from forgeline.source_scope import analyze_source
|
|
357
|
+
|
|
358
|
+
feature = proj / "services" / "broken.mjs"
|
|
359
|
+
feature.parent.mkdir()
|
|
360
|
+
feature.write_text("export function broken( {\n", encoding="utf-8")
|
|
361
|
+
parsed = analyze_source(feature, proj)
|
|
362
|
+
assert parsed["language"] == "javascript"
|
|
363
|
+
assert parsed["status"] == "syntax_error"
|
|
364
|
+
|
|
365
|
+
|
|
320
366
|
def test_qa_complexity_threshold_is_hard_and_cannot_pass_with_grade_b(proj):
|
|
321
367
|
from forgeline.gates.qa_audit import qa_audit
|
|
322
368
|
|
|
@@ -390,7 +436,7 @@ def test_cli_requires_feature_scope_and_reports_machine_provenance(proj, capsys)
|
|
|
390
436
|
main(["version", "--json"])
|
|
391
437
|
provenance = json.loads(capsys.readouterr().out)
|
|
392
438
|
assert provenance["package"] == "code-factory-2-forge"
|
|
393
|
-
assert provenance["version"] == "0.10.
|
|
439
|
+
assert provenance["version"] == "0.10.4"
|
|
394
440
|
assert {"source_commit", "build_hash", "install_origin", "python"} <= provenance.keys()
|
|
395
441
|
|
|
396
442
|
def test_learning_kernel_promotes_recurring_lessons(proj):
|
|
@@ -590,6 +636,21 @@ def test_verify_tests_passes_real_behavioral_check(proj):
|
|
|
590
636
|
assert r["attribution"]["rate"] == 1.0
|
|
591
637
|
|
|
592
638
|
|
|
639
|
+
def test_verify_tests_invalidates_receipt_when_source_changes(proj):
|
|
640
|
+
o = _to_arch_gated(proj)
|
|
641
|
+
write_smoke_manifest(proj, passing=True)
|
|
642
|
+
first = o.verify_tests(proj / "notifier.ssat.yaml")
|
|
643
|
+
assert first["verified"] is True
|
|
644
|
+
target = proj / "slices" / "notifier" / "formatter.py"
|
|
645
|
+
target.write_text(target.read_text(encoding="utf-8") + "\n# source changed\n", encoding="utf-8")
|
|
646
|
+
|
|
647
|
+
second = o.verify_tests(proj / "notifier.ssat.yaml")
|
|
648
|
+
assert second["verified"] is True
|
|
649
|
+
assert second["input_fingerprint"] != first["input_fingerprint"]
|
|
650
|
+
cache = o.store.latest_receipt("verify_tests_cache")
|
|
651
|
+
assert cache is not None and cache["reason"] == "input fingerprint changed"
|
|
652
|
+
|
|
653
|
+
|
|
593
654
|
def test_verify_tests_catches_assert_true_hollow_check(proj):
|
|
594
655
|
from forgeline.states import State
|
|
595
656
|
o = _to_arch_gated(proj)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/reverse_classical.py
RENAMED
|
File without changes
|
{code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/runtime_smoke.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_factory_2_forge-0.10.2 → code_factory_2_forge-0.10.4}/forgeline/gates/typescript_mutants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|