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.
Files changed (42) hide show
  1. {code_factory_2_forge-0.9.2/code_factory_2_forge.egg-info → code_factory_2_forge-0.10.1}/PKG-INFO +41 -3
  2. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/README.md +40 -2
  3. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1/code_factory_2_forge.egg-info}/PKG-INFO +41 -3
  4. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/code_factory_2_forge.egg-info/SOURCES.txt +2 -0
  5. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/__init__.py +1 -1
  6. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/adoption.py +2 -4
  7. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/attribution.py +3 -0
  8. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/cli.py +31 -5
  9. code_factory_2_forge-0.10.1/forgeline/gates/adversary.py +66 -0
  10. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/judge.py +8 -7
  11. code_factory_2_forge-0.10.1/forgeline/gates/qa_audit.py +157 -0
  12. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/typescript_mutants.py +5 -1
  13. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/orchestrator.py +22 -5
  14. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/pr_optimizer.py +5 -5
  15. code_factory_2_forge-0.10.1/forgeline/provenance.py +52 -0
  16. code_factory_2_forge-0.10.1/forgeline/source_scope.py +197 -0
  17. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/ssat.py +93 -6
  18. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/pyproject.toml +1 -1
  19. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/tests/test_forgeline.py +107 -0
  20. code_factory_2_forge-0.9.2/forgeline/gates/adversary.py +0 -45
  21. code_factory_2_forge-0.9.2/forgeline/gates/qa_audit.py +0 -134
  22. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/LICENSE-APACHE +0 -0
  23. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/LICENSE-MIT +0 -0
  24. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/NOTICE +0 -0
  25. {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
  26. {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
  27. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/code_factory_2_forge.egg-info/requires.txt +0 -0
  28. {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
  29. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/adapters.py +0 -0
  30. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/demo.py +0 -0
  31. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/demo_learning.py +0 -0
  32. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/__init__.py +0 -0
  33. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/reverse_classical.py +0 -0
  34. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/runtime_smoke.py +0 -0
  35. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/gates/skill_check.py +0 -0
  36. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/intent_thread.py +0 -0
  37. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/learning.py +0 -0
  38. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/refinement.py +0 -0
  39. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/run_store.py +0 -0
  40. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/skill_memory.py +0 -0
  41. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/forgeline/states.py +0 -0
  42. {code_factory_2_forge-0.9.2 → code_factory_2_forge-0.10.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-factory-2-forge
3
- Version: 0.9.2
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`) grades every build on coverage-intent (do tests
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 --root .` includes function-level
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`) grades every build on coverage-intent (do tests
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 --root .` includes function-level
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-factory-2-forge
3
- Version: 0.9.2
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`) grades every build on coverage-intent (do tests
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 --root .` includes function-level
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
@@ -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
@@ -11,4 +11,4 @@ hard CI gate, and learning from every run via an evolving skill memory.
11
11
  └──────── refine loop ─────────┘
12
12
  skill memory learns each pass
13
13
  """
14
- __version__ = "0.9.1"
14
+ __version__ = "0.10.1"
@@ -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 [path for path in root.rglob("*") if path.is_file() and path.suffix in suffixes
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": sp.add_argument("--root", default="."); sp.add_argument("--strict", action="store_true")
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 == "init":
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
- r = qa_audit(root)
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
- text = p.read_text()
17
- if "raise NotImplementedError" in text or "# FILL" in text or "TODO" in text:
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
- try:
20
- ast.parse(text)
21
- except SyntaxError as e:
22
- findings.append(f"J_SYNTAX {mod['path']}: {e}")
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
@@ -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(".git", "node_modules", "dist", "build", ".next", "coverage", ".forge"))
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")