code-factory-2-forge 0.9.2__tar.gz → 0.10.1__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.9.2/code_factory_2_forge.egg-info → code_factory_2_forge-0.10.1}/PKG-INFO +41 -3
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/README.md +40 -2
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1/code_factory_2_forge.egg-info}/PKG-INFO +41 -3
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/code_factory_2_forge.egg-info/SOURCES.txt +2 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/__init__.py +1 -1
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/adoption.py +2 -4
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/attribution.py +3 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/cli.py +31 -5
- code_factory_2_forge-0.10.1/forgeline/gates/adversary.py +66 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/judge.py +8 -7
- code_factory_2_forge-0.10.1/forgeline/gates/qa_audit.py +157 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/typescript_mutants.py +5 -1
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/orchestrator.py +22 -5
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/pr_optimizer.py +5 -5
- code_factory_2_forge-0.10.1/forgeline/provenance.py +52 -0
- code_factory_2_forge-0.10.1/forgeline/source_scope.py +197 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/ssat.py +93 -6
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/pyproject.toml +1 -1
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/tests/test_forgeline.py +107 -0
- code_factory_2_forge-0.9.2/forgeline/gates/adversary.py +0 -45
- code_factory_2_forge-0.9.2/forgeline/gates/qa_audit.py +0 -134
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/LICENSE-APACHE +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/LICENSE-MIT +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/NOTICE +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/code_factory_2_forge.egg-info/dependency_links.txt +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/code_factory_2_forge.egg-info/entry_points.txt +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/code_factory_2_forge.egg-info/requires.txt +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/code_factory_2_forge.egg-info/top_level.txt +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/adapters.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/demo.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/demo_learning.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/__init__.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/reverse_classical.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/runtime_smoke.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/skill_check.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/intent_thread.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/learning.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/refinement.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/run_store.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/skill_memory.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/states.py +0 -0
- {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/setup.cfg +0 -0
{code_factory_2_forge-0.9.2/code_factory_2_forge.egg-info → code_factory_2_forge-0.10.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-factory-2-forge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
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
|
|
@@ -207,7 +207,7 @@ the build. Full PRD-to-production traceability, enforced — not documented.
|
|
|
207
207
|
|
|
208
208
|
ForgeLine now audits quality quantitatively and **learns from its own runs**:
|
|
209
209
|
|
|
210
|
-
**Deep QA audit** (`forge qa
|
|
210
|
+
**Deep QA audit** (`forge qa <feature> --ssat <feature.ssat.yaml>`) grades the declared feature slice on coverage-intent (do tests
|
|
211
211
|
actually call the functions?), cyclomatic complexity, a scored security surface
|
|
212
212
|
(eval/exec/shell/secrets/weak-crypto), and documentation — a composite A–F grade
|
|
213
213
|
that gates shipping. A pretty build with untested, over-complex, or insecure
|
|
@@ -230,6 +230,44 @@ cycle in 15 seconds.
|
|
|
230
230
|
(normal → elevated "fix all findings" → final "human review required") instead
|
|
231
231
|
of a flat retry cap.
|
|
232
232
|
|
|
233
|
+
## v0.10 - Safe feature QA and provenance
|
|
234
|
+
|
|
235
|
+
ForgeLine now treats an SSAT as the ownership boundary for a gate. `forge review`
|
|
236
|
+
uses only its declared source modules for adversarial and QA findings; unrelated
|
|
237
|
+
repository observations do not decide a feature result. Run the same scoped
|
|
238
|
+
check directly with:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
forge qa my-feature --ssat specs/my-feature.ssat.yaml --root . --strict
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`forge qa --repo-wide --root .` remains available for an explicit inventory,
|
|
245
|
+
but is labeled `repo_wide` in its receipt and is not a substitute for reviewed
|
|
246
|
+
feature evidence. Dependency, build, cache, virtual-environment, and generated
|
|
247
|
+
receipt trees are pruned before either scan. Disappearing optional paths are
|
|
248
|
+
reported as skipped instead of crashing the gate.
|
|
249
|
+
|
|
250
|
+
Source parsing is extension-aware: Python uses its AST; JavaScript and
|
|
251
|
+
TypeScript use Node/TypeScript when available. An unavailable parser is reported
|
|
252
|
+
as `parser_unsupported`, never misreported as Python syntax. Complexity above
|
|
253
|
+
the hard threshold (10) cannot receive a passing grade.
|
|
254
|
+
|
|
255
|
+
Regex invariants must declare a reviewed bounded scope: SSAT module, symbol,
|
|
256
|
+
and `max_lines`. An unscoped pattern blocks as `E_INVARIANT_SCOPE`; a match in
|
|
257
|
+
the bounded symbol blocks as `E_INVARIANT`. This prevents a broad regex from
|
|
258
|
+
accidentally judging unrelated code.
|
|
259
|
+
|
|
260
|
+
Every installed CLI can identify the running package without guessing from a
|
|
261
|
+
checkout:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
forge --version --json
|
|
265
|
+
forge version --json
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The JSON includes package/version, install origin, Python version, and explicit
|
|
269
|
+
`null` values when a source commit or build hash was not embedded in the wheel.
|
|
270
|
+
|
|
233
271
|
## The five-brick factory
|
|
234
272
|
|
|
235
273
|
| Repo | Tier | Owns |
|
|
@@ -339,7 +377,7 @@ the mutant is the real generated stub.
|
|
|
339
377
|
## Failure attribution and refinement
|
|
340
378
|
|
|
341
379
|
ForgeLine 0.5 reports review, architecture, QA, smoke, and intent failures at
|
|
342
|
-
their smallest actionable unit. `forge qa --
|
|
380
|
+
their smallest actionable unit. `forge qa <feature> --ssat <feature.ssat.yaml>` includes function-level
|
|
343
381
|
metrics and attribution in its JSON output.
|
|
344
382
|
|
|
345
383
|
The refinement engine accepts exactly one proposed edit at a time. Structural
|
|
@@ -189,7 +189,7 @@ the build. Full PRD-to-production traceability, enforced — not documented.
|
|
|
189
189
|
|
|
190
190
|
ForgeLine now audits quality quantitatively and **learns from its own runs**:
|
|
191
191
|
|
|
192
|
-
**Deep QA audit** (`forge qa
|
|
192
|
+
**Deep QA audit** (`forge qa <feature> --ssat <feature.ssat.yaml>`) grades the declared feature slice on coverage-intent (do tests
|
|
193
193
|
actually call the functions?), cyclomatic complexity, a scored security surface
|
|
194
194
|
(eval/exec/shell/secrets/weak-crypto), and documentation — a composite A–F grade
|
|
195
195
|
that gates shipping. A pretty build with untested, over-complex, or insecure
|
|
@@ -212,6 +212,44 @@ cycle in 15 seconds.
|
|
|
212
212
|
(normal → elevated "fix all findings" → final "human review required") instead
|
|
213
213
|
of a flat retry cap.
|
|
214
214
|
|
|
215
|
+
## v0.10 - Safe feature QA and provenance
|
|
216
|
+
|
|
217
|
+
ForgeLine now treats an SSAT as the ownership boundary for a gate. `forge review`
|
|
218
|
+
uses only its declared source modules for adversarial and QA findings; unrelated
|
|
219
|
+
repository observations do not decide a feature result. Run the same scoped
|
|
220
|
+
check directly with:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
forge qa my-feature --ssat specs/my-feature.ssat.yaml --root . --strict
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`forge qa --repo-wide --root .` remains available for an explicit inventory,
|
|
227
|
+
but is labeled `repo_wide` in its receipt and is not a substitute for reviewed
|
|
228
|
+
feature evidence. Dependency, build, cache, virtual-environment, and generated
|
|
229
|
+
receipt trees are pruned before either scan. Disappearing optional paths are
|
|
230
|
+
reported as skipped instead of crashing the gate.
|
|
231
|
+
|
|
232
|
+
Source parsing is extension-aware: Python uses its AST; JavaScript and
|
|
233
|
+
TypeScript use Node/TypeScript when available. An unavailable parser is reported
|
|
234
|
+
as `parser_unsupported`, never misreported as Python syntax. Complexity above
|
|
235
|
+
the hard threshold (10) cannot receive a passing grade.
|
|
236
|
+
|
|
237
|
+
Regex invariants must declare a reviewed bounded scope: SSAT module, symbol,
|
|
238
|
+
and `max_lines`. An unscoped pattern blocks as `E_INVARIANT_SCOPE`; a match in
|
|
239
|
+
the bounded symbol blocks as `E_INVARIANT`. This prevents a broad regex from
|
|
240
|
+
accidentally judging unrelated code.
|
|
241
|
+
|
|
242
|
+
Every installed CLI can identify the running package without guessing from a
|
|
243
|
+
checkout:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
forge --version --json
|
|
247
|
+
forge version --json
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The JSON includes package/version, install origin, Python version, and explicit
|
|
251
|
+
`null` values when a source commit or build hash was not embedded in the wheel.
|
|
252
|
+
|
|
215
253
|
## The five-brick factory
|
|
216
254
|
|
|
217
255
|
| Repo | Tier | Owns |
|
|
@@ -321,7 +359,7 @@ the mutant is the real generated stub.
|
|
|
321
359
|
## Failure attribution and refinement
|
|
322
360
|
|
|
323
361
|
ForgeLine 0.5 reports review, architecture, QA, smoke, and intent failures at
|
|
324
|
-
their smallest actionable unit. `forge qa --
|
|
362
|
+
their smallest actionable unit. `forge qa <feature> --ssat <feature.ssat.yaml>` includes function-level
|
|
325
363
|
metrics and attribution in its JSON output.
|
|
326
364
|
|
|
327
365
|
The refinement engine accepts exactly one proposed edit at a time. Structural
|
{code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1/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.
|
|
3
|
+
Version: 0.10.1
|
|
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
|
|
@@ -207,7 +207,7 @@ the build. Full PRD-to-production traceability, enforced — not documented.
|
|
|
207
207
|
|
|
208
208
|
ForgeLine now audits quality quantitatively and **learns from its own runs**:
|
|
209
209
|
|
|
210
|
-
**Deep QA audit** (`forge qa
|
|
210
|
+
**Deep QA audit** (`forge qa <feature> --ssat <feature.ssat.yaml>`) grades the declared feature slice on coverage-intent (do tests
|
|
211
211
|
actually call the functions?), cyclomatic complexity, a scored security surface
|
|
212
212
|
(eval/exec/shell/secrets/weak-crypto), and documentation — a composite A–F grade
|
|
213
213
|
that gates shipping. A pretty build with untested, over-complex, or insecure
|
|
@@ -230,6 +230,44 @@ cycle in 15 seconds.
|
|
|
230
230
|
(normal → elevated "fix all findings" → final "human review required") instead
|
|
231
231
|
of a flat retry cap.
|
|
232
232
|
|
|
233
|
+
## v0.10 - Safe feature QA and provenance
|
|
234
|
+
|
|
235
|
+
ForgeLine now treats an SSAT as the ownership boundary for a gate. `forge review`
|
|
236
|
+
uses only its declared source modules for adversarial and QA findings; unrelated
|
|
237
|
+
repository observations do not decide a feature result. Run the same scoped
|
|
238
|
+
check directly with:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
forge qa my-feature --ssat specs/my-feature.ssat.yaml --root . --strict
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`forge qa --repo-wide --root .` remains available for an explicit inventory,
|
|
245
|
+
but is labeled `repo_wide` in its receipt and is not a substitute for reviewed
|
|
246
|
+
feature evidence. Dependency, build, cache, virtual-environment, and generated
|
|
247
|
+
receipt trees are pruned before either scan. Disappearing optional paths are
|
|
248
|
+
reported as skipped instead of crashing the gate.
|
|
249
|
+
|
|
250
|
+
Source parsing is extension-aware: Python uses its AST; JavaScript and
|
|
251
|
+
TypeScript use Node/TypeScript when available. An unavailable parser is reported
|
|
252
|
+
as `parser_unsupported`, never misreported as Python syntax. Complexity above
|
|
253
|
+
the hard threshold (10) cannot receive a passing grade.
|
|
254
|
+
|
|
255
|
+
Regex invariants must declare a reviewed bounded scope: SSAT module, symbol,
|
|
256
|
+
and `max_lines`. An unscoped pattern blocks as `E_INVARIANT_SCOPE`; a match in
|
|
257
|
+
the bounded symbol blocks as `E_INVARIANT`. This prevents a broad regex from
|
|
258
|
+
accidentally judging unrelated code.
|
|
259
|
+
|
|
260
|
+
Every installed CLI can identify the running package without guessing from a
|
|
261
|
+
checkout:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
forge --version --json
|
|
265
|
+
forge version --json
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The JSON includes package/version, install origin, Python version, and explicit
|
|
269
|
+
`null` values when a source commit or build hash was not embedded in the wheel.
|
|
270
|
+
|
|
233
271
|
## The five-brick factory
|
|
234
272
|
|
|
235
273
|
| Repo | Tier | Owns |
|
|
@@ -339,7 +377,7 @@ the mutant is the real generated stub.
|
|
|
339
377
|
## Failure attribution and refinement
|
|
340
378
|
|
|
341
379
|
ForgeLine 0.5 reports review, architecture, QA, smoke, and intent failures at
|
|
342
|
-
their smallest actionable unit. `forge qa --
|
|
380
|
+
their smallest actionable unit. `forge qa <feature> --ssat <feature.ssat.yaml>` includes function-level
|
|
343
381
|
metrics and attribution in its JSON output.
|
|
344
382
|
|
|
345
383
|
The refinement engine accepts exactly one proposed edit at a time. Structural
|
{code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/code_factory_2_forge.egg-info/SOURCES.txt
RENAMED
|
@@ -20,9 +20,11 @@ forgeline/intent_thread.py
|
|
|
20
20
|
forgeline/learning.py
|
|
21
21
|
forgeline/orchestrator.py
|
|
22
22
|
forgeline/pr_optimizer.py
|
|
23
|
+
forgeline/provenance.py
|
|
23
24
|
forgeline/refinement.py
|
|
24
25
|
forgeline/run_store.py
|
|
25
26
|
forgeline/skill_memory.py
|
|
27
|
+
forgeline/source_scope.py
|
|
26
28
|
forgeline/ssat.py
|
|
27
29
|
forgeline/states.py
|
|
28
30
|
forgeline/gates/__init__.py
|
|
@@ -7,13 +7,11 @@ import re
|
|
|
7
7
|
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
IGNORED = {".git", "node_modules", "dist", "build", ".next", "coverage", ".forge", ".venv", "venv"}
|
|
10
|
+
from .source_scope import iter_source_files
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def _files(root: Path, suffixes: set[str]) -> list[Path]:
|
|
15
|
-
return
|
|
16
|
-
and not any(part in IGNORED for part in path.parts)]
|
|
14
|
+
return iter_source_files(root, suffixes=suffixes)
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
def _functions(path: Path, language: str) -> list[dict]:
|
|
@@ -10,6 +10,9 @@ class FailureClass(str, Enum):
|
|
|
10
10
|
INVENTED_PARAM = "invented_param"
|
|
11
11
|
SIGNATURE_DRIFT = "signature_drift"
|
|
12
12
|
STUB_UNFILLED = "stub_unfilled"
|
|
13
|
+
PARSER_UNSUPPORTED = "parser_unsupported"
|
|
14
|
+
SYNTAX_ERROR = "syntax_error"
|
|
15
|
+
INVARIANT_VIOLATION = "invariant_violation"
|
|
13
16
|
COMPLEXITY_EXCEEDED = "complexity_exceeded"
|
|
14
17
|
INCONSISTENT_LOGIC = "inconsistent_logic"
|
|
15
18
|
RUNTIME_CRASH = "runtime_crash"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""forge CLI — the state-machine driver."""
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
import argparse, json
|
|
3
|
+
import argparse, json, sys
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from .states import State, TRANSITIONS, HUMAN_GATES
|
|
6
6
|
from .run_store import RunStore
|
|
@@ -20,10 +20,20 @@ NEXT_ACTION = {
|
|
|
20
20
|
State.SHIPPED: "done ✓",
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
def _emit_version(as_json: bool) -> None:
|
|
24
|
+
from .provenance import provenance
|
|
25
|
+
payload = provenance()
|
|
26
|
+
print(json.dumps(payload, indent=2, sort_keys=True) if as_json else f"forge {payload['version']}")
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
def main(argv=None):
|
|
30
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
31
|
+
if argv and argv[0] == "--version":
|
|
32
|
+
_emit_version("--json" in argv)
|
|
33
|
+
return
|
|
24
34
|
p = argparse.ArgumentParser(prog="forge", description="ForgeLine — autonomous software factory outer loop")
|
|
25
35
|
sub = p.add_subparsers(required=True, dest="cmd")
|
|
26
|
-
for name in ["init","adopt","status","expand","gate","architect","fill","review","arch-gate","verify-tests","verify-tests-ts","challenge","smoke","ship","handoff","agent","lessons","policy","qa","optimize-pr","demo","demo-learning"]:
|
|
36
|
+
for name in ["init","adopt","status","expand","gate","architect","fill","review","arch-gate","verify-tests","verify-tests-ts","challenge","smoke","ship","handoff","agent","lessons","policy","qa","optimize-pr","demo","demo-learning","version"]:
|
|
27
37
|
sp = sub.add_parser(name)
|
|
28
38
|
if name == "init": sp.add_argument("--root", default=".")
|
|
29
39
|
elif name in {"architect","fill","review","arch-gate","verify-tests"}:
|
|
@@ -43,15 +53,21 @@ def main(argv=None):
|
|
|
43
53
|
elif name == "handoff": sp.add_argument("feature"); sp.add_argument("spec"); sp.add_argument("--root", default=".")
|
|
44
54
|
elif name == "lessons": sp.add_argument("--root", default=".")
|
|
45
55
|
elif name == "policy": sp.add_argument("--root", default=".")
|
|
46
|
-
elif name == "qa":
|
|
56
|
+
elif name == "qa":
|
|
57
|
+
sp.add_argument("feature", nargs="?"); sp.add_argument("--ssat", default=None)
|
|
58
|
+
sp.add_argument("--repo-wide", action="store_true")
|
|
59
|
+
sp.add_argument("--root", default="."); sp.add_argument("--strict", action="store_true")
|
|
47
60
|
elif name == "optimize-pr": sp.add_argument("feature", nargs="?"); sp.add_argument("--root", default="."); sp.add_argument("--base", default="main")
|
|
61
|
+
elif name == "version": sp.add_argument("--json", action="store_true")
|
|
48
62
|
elif name == "demo": pass
|
|
49
63
|
elif name == "demo-learning": pass
|
|
50
64
|
elif name == "demo-learning": pass
|
|
51
65
|
else: sp.add_argument("feature", nargs="?"); sp.add_argument("--root", default=".")
|
|
52
66
|
a = p.parse_args(argv); root = Path(getattr(a, "root", "."))
|
|
53
67
|
|
|
54
|
-
if a.cmd == "
|
|
68
|
+
if a.cmd == "version":
|
|
69
|
+
_emit_version(a.json)
|
|
70
|
+
elif a.cmd == "init":
|
|
55
71
|
(root/".forge").mkdir(parents=True, exist_ok=True)
|
|
56
72
|
(root/"skills").mkdir(exist_ok=True)
|
|
57
73
|
print("ForgeLine initialized. Next: forge agent claude|codex, then forge expand <feature>")
|
|
@@ -138,7 +154,17 @@ def main(argv=None):
|
|
|
138
154
|
print(json.dumps(LearningKernel(root).policy_summary(), indent=2))
|
|
139
155
|
elif a.cmd == "qa":
|
|
140
156
|
from .gates.qa_audit import qa_audit
|
|
141
|
-
|
|
157
|
+
from .source_scope import declared_paths
|
|
158
|
+
from .ssat import load_ssat
|
|
159
|
+
if not a.repo_wide and not a.ssat:
|
|
160
|
+
print(json.dumps({"passed": False, "error": {
|
|
161
|
+
"code": "E_SCOPE_REQUIRED",
|
|
162
|
+
"message": "feature QA requires --ssat; use --repo-wide only for an explicitly non-gating inventory",
|
|
163
|
+
"next": "forge qa <feature> --ssat <feature.ssat.yaml> --root .",
|
|
164
|
+
}}, indent=2))
|
|
165
|
+
raise SystemExit(2)
|
|
166
|
+
source_paths = None if a.repo_wide else declared_paths(root, load_ssat(Path(a.ssat)))
|
|
167
|
+
r = qa_audit(root, source_paths=source_paths)
|
|
142
168
|
print(json.dumps({"grade": r.grade, "passed": r.passed, "metrics": r.metrics,
|
|
143
169
|
"findings": r.findings,
|
|
144
170
|
"attribution": r.attribution.to_dict()}, indent=2))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The grumpy adversary — assumes the code is broken/insecure and makes the
|
|
2
|
+
generator prove otherwise. Executable heuristics (no LLM needed to be
|
|
3
|
+
useful); an optional LLM adversary can be layered behind the same interface.
|
|
4
|
+
It does NOT need to be right — it needs to force proof (tests + arch completeness)."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import ast, re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
|
|
10
|
+
from ..source_scope import SOURCE_SUFFIXES, analyze_source, iter_source_files
|
|
11
|
+
|
|
12
|
+
DANGER = {
|
|
13
|
+
"eval(": "A_EVAL arbitrary eval",
|
|
14
|
+
"exec(": "A_EXEC arbitrary exec",
|
|
15
|
+
"os.system": "A_SHELL shell execution",
|
|
16
|
+
"subprocess": "A_SUBPROC subprocess use",
|
|
17
|
+
"pickle.load": "A_PICKLE unsafe deserialization",
|
|
18
|
+
"verify=False": "A_TLS TLS verification disabled",
|
|
19
|
+
"shell=True": "A_SHELL_TRUE shell=True injection surface",
|
|
20
|
+
}
|
|
21
|
+
SECRET = re.compile(r"(?i)(api[_-]?key|secret|password|token)\s*=\s*['\"][A-Za-z0-9/\+_-]{12,}['\"]")
|
|
22
|
+
|
|
23
|
+
def grumpy_review(
|
|
24
|
+
src_dir: Path,
|
|
25
|
+
require_tests: bool = True,
|
|
26
|
+
*,
|
|
27
|
+
source_paths: Iterable[Path] | None = None,
|
|
28
|
+
test_paths: Iterable[Path] | None = None,
|
|
29
|
+
) -> tuple[bool, list[str]]:
|
|
30
|
+
"""Returns (satisfied, complaints). Grumpy is satisfied only when it can
|
|
31
|
+
find NO obvious sin AND the generator supplied tests to prove correctness."""
|
|
32
|
+
src_dir = Path(src_dir).resolve(); complaints = []
|
|
33
|
+
inventory = (
|
|
34
|
+
iter_source_files(src_dir, paths=source_paths)
|
|
35
|
+
if source_paths is not None
|
|
36
|
+
else iter_source_files(src_dir, suffixes=SOURCE_SUFFIXES)
|
|
37
|
+
)
|
|
38
|
+
for p in inventory:
|
|
39
|
+
if p.name.startswith("test_") or p.parent.name in {"tests", "test", "__tests__"} or ".test." in p.name or ".spec." in p.name:
|
|
40
|
+
continue
|
|
41
|
+
parsed = analyze_source(p, src_dir)
|
|
42
|
+
text = parsed["text"]
|
|
43
|
+
for needle, msg in DANGER.items():
|
|
44
|
+
if needle in text:
|
|
45
|
+
complaints.append(f"{msg} in {p.relative_to(src_dir).as_posix()}")
|
|
46
|
+
if SECRET.search(text):
|
|
47
|
+
complaints.append(f"A_SECRET hard-coded credential in {p.relative_to(src_dir).as_posix()}")
|
|
48
|
+
# bare except = hiding failure, which grumpy hates
|
|
49
|
+
if parsed["language"] == "python" and parsed["status"] == "ok":
|
|
50
|
+
for node in ast.walk(parsed["tree"]):
|
|
51
|
+
if isinstance(node, ast.ExceptHandler) and node.type is None:
|
|
52
|
+
complaints.append(f"A_BARE_EXCEPT swallowed error in {p.relative_to(src_dir).as_posix()}")
|
|
53
|
+
elif parsed["status"] == "syntax_error":
|
|
54
|
+
complaints.append(f"A_SYNTAX {p.relative_to(src_dir).as_posix()} does not parse")
|
|
55
|
+
elif parsed["status"] == "parser_unsupported":
|
|
56
|
+
complaints.append(f"A_PARSER_UNSUPPORTED {p.relative_to(src_dir).as_posix()}: {parsed.get('reason', parsed['language'])}")
|
|
57
|
+
if require_tests:
|
|
58
|
+
test_inventory = (
|
|
59
|
+
iter_source_files(src_dir, paths=test_paths)
|
|
60
|
+
if test_paths is not None
|
|
61
|
+
else iter_source_files(src_dir, suffixes=SOURCE_SUFFIXES)
|
|
62
|
+
)
|
|
63
|
+
test_files = [p for p in test_inventory if p.name.startswith("test_") or p.parent.name in {"tests", "test", "__tests__"} or ".test." in p.name or ".spec." in p.name]
|
|
64
|
+
if not test_files:
|
|
65
|
+
complaints.append("A_NO_PROOF no tests supplied — prove it works")
|
|
66
|
+
return (len(complaints) == 0, complaints)
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Checks the filled code matches the SSAT scaffold: no stubs left, every
|
|
3
3
|
declared function implemented, imports resolve within the tree."""
|
|
4
4
|
from __future__ import annotations
|
|
5
|
-
import ast
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from ..ssat import check_erosion
|
|
7
|
+
from ..source_scope import analyze_source
|
|
8
8
|
|
|
9
9
|
def judge_consistency(ssat: dict, src_dir: Path) -> tuple[bool, list[str]]:
|
|
10
10
|
findings = []
|
|
@@ -13,13 +13,14 @@ def judge_consistency(ssat: dict, src_dir: Path) -> tuple[bool, list[str]]:
|
|
|
13
13
|
p = src_dir/mod["path"]
|
|
14
14
|
if not p.exists():
|
|
15
15
|
findings.append(f"J_MISSING {mod['path']}"); continue
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
parsed = analyze_source(p, src_dir)
|
|
17
|
+
text = parsed["text"]
|
|
18
|
+
if "raise NotImplementedError" in text or "# FILL" in text:
|
|
18
19
|
findings.append(f"J_STUB unfilled body in {mod['path']}")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
findings.append(f"
|
|
20
|
+
if parsed["status"] == "syntax_error":
|
|
21
|
+
findings.append(f"J_SYNTAX {mod['path']}: {parsed['error']}")
|
|
22
|
+
elif parsed["status"] == "parser_unsupported":
|
|
23
|
+
findings.append(f"J_PARSER_UNSUPPORTED {mod['path']}: {parsed.get('reason', parsed['language'])}")
|
|
23
24
|
# structural erosion is a judge concern too
|
|
24
25
|
for v in check_erosion(ssat, src_dir):
|
|
25
26
|
findings.append(f"J_ARCH {v.code} {v.message}")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Feature-scoped, language-aware QA evidence with explicit hard thresholds."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
|
|
10
|
+
from ..source_scope import SOURCE_SUFFIXES, analyze_source, iter_source_files, read_text
|
|
11
|
+
|
|
12
|
+
COMPLEXITY_LIMIT = 10
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class QAReport:
|
|
17
|
+
coverage_intent: float = 0.0
|
|
18
|
+
max_complexity: int = 0
|
|
19
|
+
security_score: int = 100
|
|
20
|
+
doc_ratio: float = 0.0
|
|
21
|
+
grade: str = "F"
|
|
22
|
+
findings: list[str] = field(default_factory=list)
|
|
23
|
+
metrics: dict = field(default_factory=dict)
|
|
24
|
+
function_metrics: list[dict] = field(default_factory=list)
|
|
25
|
+
skipped_paths: list[str] = field(default_factory=list)
|
|
26
|
+
scope: dict = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def passed(self) -> bool:
|
|
30
|
+
return self.grade in ("A", "B") and self.security_score >= 80 and self.max_complexity <= COMPLEXITY_LIMIT
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def attribution(self):
|
|
34
|
+
from ..attribution import Attribution, FailureClass, UnitResult
|
|
35
|
+
units = []
|
|
36
|
+
for metric in self.function_metrics:
|
|
37
|
+
failures = []
|
|
38
|
+
failure_class = metric.get("failure_class")
|
|
39
|
+
if metric["complexity"] > COMPLEXITY_LIMIT:
|
|
40
|
+
failures.append(f"complexity={metric['complexity']}; threshold={COMPLEXITY_LIMIT}; policy=hard")
|
|
41
|
+
failure_class = FailureClass.COMPLEXITY_EXCEEDED
|
|
42
|
+
if not metric["tested"]:
|
|
43
|
+
failures.append("coverage_intent=0; required=1")
|
|
44
|
+
failure_class = failure_class or FailureClass.INCONSISTENT_LOGIC
|
|
45
|
+
units.append(UnitResult(
|
|
46
|
+
unit=f"qa_audit:{metric['function']}", stage="qa_audit", passed=not failures,
|
|
47
|
+
evidence="metrics within thresholds" if not failures else "; ".join(failures),
|
|
48
|
+
failure_class=failure_class,
|
|
49
|
+
))
|
|
50
|
+
for finding in self.findings:
|
|
51
|
+
if finding.startswith("QA_PARSER_UNSUPPORTED"):
|
|
52
|
+
units.append(UnitResult(f"qa_audit:{finding.split()[1]}", "qa_audit", False, finding, FailureClass.PARSER_UNSUPPORTED))
|
|
53
|
+
elif finding.startswith("QA_SYNTAX"):
|
|
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
|
+
return Attribution("qa_audit", len(units), sum(unit.passed for unit in units), units)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _complexity(node: ast.FunctionDef) -> int:
|
|
62
|
+
value = 1
|
|
63
|
+
for child in ast.walk(node):
|
|
64
|
+
if isinstance(child, (ast.If, ast.For, ast.While, ast.ExceptHandler, ast.With, ast.Assert)):
|
|
65
|
+
value += 1
|
|
66
|
+
elif isinstance(child, ast.BoolOp):
|
|
67
|
+
value += len(child.values) - 1
|
|
68
|
+
elif isinstance(child, ast.IfExp):
|
|
69
|
+
value += 1
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
SEC_PATTERNS = {
|
|
74
|
+
r"\beval\(": ("CRITICAL", 40, "eval() - arbitrary code execution"),
|
|
75
|
+
r"\bexec\(": ("CRITICAL", 40, "exec() - arbitrary code execution"),
|
|
76
|
+
r"shell\s*=\s*True": ("HIGH", 25, "shell=True - command injection surface"),
|
|
77
|
+
r"pickle\.loads?\(": ("HIGH", 20, "pickle - unsafe deserialization"),
|
|
78
|
+
r"verify\s*=\s*False": ("HIGH", 20, "TLS verification disabled"),
|
|
79
|
+
r"(?i)(password|secret|api_key|token)\s*=\s*['\"][^'\"]{8,}": ("CRITICAL", 40, "hard-coded credential"),
|
|
80
|
+
r"md5\(": ("MEDIUM", 10, "MD5 - weak hash"),
|
|
81
|
+
r"random\.random\(\)": ("LOW", 5, "non-cryptographic randomness"),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_test(path: Path) -> bool:
|
|
86
|
+
return path.name.startswith("test_") or path.parent.name in {"tests", "test", "__tests__"} or ".test." in path.name or ".spec." in path.name
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _metric(report: QAReport, path: Path, name: str, complexity: int, documented: bool, test_text: str, root: Path) -> None:
|
|
90
|
+
tested = bool(re.search(rf"\b{re.escape(name)}\b", test_text)) and not name.startswith("<anonymous")
|
|
91
|
+
report.function_metrics.append({
|
|
92
|
+
"function": f"{path.relative_to(root).as_posix()}:{name}", "complexity": complexity,
|
|
93
|
+
"tested": tested, "documented": documented,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def qa_audit(src_dir: Path, *, source_paths: Iterable[Path] | None = None) -> QAReport:
|
|
98
|
+
"""Grade a reviewed feature slice, or an explicitly requested pruned repository scan."""
|
|
99
|
+
root = Path(src_dir).resolve()
|
|
100
|
+
report = QAReport(scope={"kind": "feature" if source_paths is not None else "repo_wide"})
|
|
101
|
+
inventory = (
|
|
102
|
+
iter_source_files(root, paths=source_paths, skipped=report.skipped_paths)
|
|
103
|
+
if source_paths is not None
|
|
104
|
+
else iter_source_files(root, suffixes=SOURCE_SUFFIXES, skipped=report.skipped_paths)
|
|
105
|
+
)
|
|
106
|
+
code_files = [path for path in inventory if not _is_test(path)]
|
|
107
|
+
test_files = [path for path in iter_source_files(root, suffixes=SOURCE_SUFFIXES, skipped=report.skipped_paths) if _is_test(path)]
|
|
108
|
+
report.scope["code_files"] = [path.relative_to(root).as_posix() for path in code_files]
|
|
109
|
+
test_text = "\n".join(text for path in test_files if (text := read_text(path, report.skipped_paths)) is not None)
|
|
110
|
+
|
|
111
|
+
for path in code_files:
|
|
112
|
+
parsed = analyze_source(path, root)
|
|
113
|
+
status = parsed["status"]
|
|
114
|
+
if status == "parser_unsupported":
|
|
115
|
+
report.findings.append(f"QA_PARSER_UNSUPPORTED {path.relative_to(root).as_posix()}: {parsed.get('reason', parsed['language'])}")
|
|
116
|
+
continue
|
|
117
|
+
if status == "syntax_error":
|
|
118
|
+
report.findings.append(f"QA_SYNTAX {path.relative_to(root).as_posix()}: {parsed['error']}")
|
|
119
|
+
continue
|
|
120
|
+
text = parsed["text"]
|
|
121
|
+
if parsed["language"] == "python":
|
|
122
|
+
for node in ast.walk(parsed["tree"]):
|
|
123
|
+
if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
|
|
124
|
+
_metric(report, path, node.name, _complexity(node), bool(ast.get_docstring(node)), test_text, root)
|
|
125
|
+
else:
|
|
126
|
+
for function in parsed.get("functions", []):
|
|
127
|
+
_metric(report, path, function["name"], function["complexity"], bool(function["documented"]), test_text, root)
|
|
128
|
+
for pattern, (severity, deduction, message) in SEC_PATTERNS.items():
|
|
129
|
+
if re.search(pattern, text):
|
|
130
|
+
report.security_score -= deduction
|
|
131
|
+
report.findings.append(f"QA_SEC[{severity}] {message} in {path.relative_to(root).as_posix()}")
|
|
132
|
+
|
|
133
|
+
count = len(report.function_metrics) or 1
|
|
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)
|
|
136
|
+
report.max_complexity = max((metric["complexity"] for metric in report.function_metrics), default=0)
|
|
137
|
+
report.security_score = max(report.security_score, 0)
|
|
138
|
+
for metric in report.function_metrics:
|
|
139
|
+
if metric["complexity"] > COMPLEXITY_LIMIT:
|
|
140
|
+
report.findings.append(f"QA_COMPLEXITY {metric['function']} complexity {metric['complexity']} > {COMPLEXITY_LIMIT}; policy=hard")
|
|
141
|
+
|
|
142
|
+
score = 35 * report.coverage_intent
|
|
143
|
+
score += 25 * (1 if report.max_complexity <= COMPLEXITY_LIMIT else max(0, 1 - (report.max_complexity - COMPLEXITY_LIMIT) / 10))
|
|
144
|
+
score += 25 * (report.security_score / 100)
|
|
145
|
+
score += 15 * report.doc_ratio
|
|
146
|
+
report.grade = "A" if score >= 85 else "B" if score >= 70 else "C" if score >= 55 else "D" if score >= 40 else "F"
|
|
147
|
+
if report.max_complexity > COMPLEXITY_LIMIT and report.grade in {"A", "B"}:
|
|
148
|
+
report.grade = "C"
|
|
149
|
+
if any(finding.startswith(("QA_SYNTAX", "QA_PARSER_UNSUPPORTED")) for finding in report.findings):
|
|
150
|
+
report.grade = "F"
|
|
151
|
+
report.metrics = {
|
|
152
|
+
"coverage_intent": report.coverage_intent, "max_complexity": report.max_complexity,
|
|
153
|
+
"security_score": report.security_score, "doc_ratio": report.doc_ratio,
|
|
154
|
+
"composite": round(score, 1), "complexity_policy": "hard",
|
|
155
|
+
"scope": report.scope, "skipped_paths": report.skipped_paths,
|
|
156
|
+
}
|
|
157
|
+
return report
|
{code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/typescript_mutants.py
RENAMED
|
@@ -24,7 +24,11 @@ def verify_typescript_tests(root: Path, feature: str, manifest_path: Path | None
|
|
|
24
24
|
return GateResult(False, Attribution("verify_tests_ts", 1, 0, [UnitResult("typescript:manifest", "verify_tests_ts", False, "TypeScript mutant manifest declares no reviewed mutants", FailureClass.HOLLOW_MANIFEST)]))
|
|
25
25
|
tmp = Path(tempfile.mkdtemp(prefix="forge-ts-mutant-"))
|
|
26
26
|
try:
|
|
27
|
-
shutil.copytree(root, tmp, dirs_exist_ok=True, ignore=shutil.ignore_patterns(
|
|
27
|
+
shutil.copytree(root, tmp, dirs_exist_ok=True, ignore=shutil.ignore_patterns(
|
|
28
|
+
".git", ".pnpm", "node_modules", "dist", "build", ".next", ".nuxt",
|
|
29
|
+
"coverage", ".forge", ".cache", "cache", "out", "output", "target",
|
|
30
|
+
".venv", "venv", "__pycache__",
|
|
31
|
+
))
|
|
28
32
|
units = []
|
|
29
33
|
for mutant in mutants:
|
|
30
34
|
required = ("name", "path", "replace_regex", "command")
|