@surlymochan/evalid 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +315 -2
- package/RELEASE_NOTES.md +102 -0
- package/atoms/registry.json +758 -0
- package/bin/xflow.js +7208 -0
- package/docs/adoption/README.md +128 -0
- package/docs/adoption/openflow-release-hardening.md +61 -0
- package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
- package/docs/compatibility.md +52 -0
- package/docs/competitive-benchmark.md +108 -0
- package/docs/corps-operator-guide.md +75 -0
- package/docs/demo-proof.md +126 -0
- package/docs/examples-gallery.md +115 -0
- package/docs/fixtures/tracker-item.json +7 -0
- package/docs/goal-vs-codex.md +129 -0
- package/docs/gstack-comparison.md +88 -0
- package/docs/install-upgrade.md +133 -0
- package/docs/integrations.md +49 -0
- package/docs/launch-demo.md +123 -0
- package/docs/launch-dossier.md +180 -0
- package/docs/methodology.md +149 -0
- package/docs/npm-publish-handoff.md +83 -0
- package/docs/openspec-migration.md +106 -0
- package/docs/public-benchmark.md +68 -0
- package/docs/public-release.md +145 -0
- package/docs/quality-assessment.md +139 -0
- package/docs/quickstart.md +156 -0
- package/docs/reviewer-guide.md +43 -0
- package/docs/spec-kit-benchmark.md +73 -0
- package/docs/super-assistant-comparison.md +106 -0
- package/docs/superpowers-comparison.md +82 -0
- package/docs/team-adoption.md +123 -0
- package/docs/tooling-matrix.md +91 -0
- package/docs/walkthrough.md +76 -0
- package/docs/workflow-manual.md +1125 -0
- package/package.json +83 -4
- package/prompts/pencil-draft.md +46 -0
- package/prompts/pencil-refine.md +46 -0
- package/prompts/phase-artifact.md +24 -0
- package/prompts/phase-execution.md +32 -0
- package/schemas/competitor-reconstruction-contract.schema.json +91 -0
- package/schemas/llm-design-review-aggregate.schema.json +83 -0
- package/schemas/visual-benchmark.schema.json +978 -0
- package/schemas/workflow.schema.json +100 -0
- package/scripts/run-tests.mjs +23 -0
- package/scripts/takein_digest.py +139 -0
- package/specs/README.md +118 -0
- package/specs/architecture.md +36 -0
- package/specs/product.md +33 -0
- package/specs/workflow.md +349 -0
- package/src/agent_team/atoms/codex-app-bridge.js +8 -0
- package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
- package/src/agent_team/atoms/design/contract-freeze.js +8 -0
- package/src/agent_team/atoms/design/generation-contract.js +442 -0
- package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
- package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
- package/src/agent_team/atoms/patch-challenge.js +19 -0
- package/src/agent_team/atoms/pencil/accept.js +9 -0
- package/src/agent_team/atoms/pencil/draft.js +365 -0
- package/src/agent_team/atoms/pencil/refine.js +229 -0
- package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
- package/src/agent_team/atoms/team-run.js +380 -0
- package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
- package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
- package/src/agent_team/atoms/visual/benchmark.js +1652 -0
- package/src/agent_team/atoms/visual/design-selection.js +23 -0
- package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
- package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
- package/src/agent_team/atoms/visual/image-diff.js +186 -0
- package/src/agent_team/atoms/visual/layout-competition.js +8 -0
- package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
- package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
- package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
- package/src/agent_team/atoms/visual/review.js +77 -0
- package/src/agent_team/atoms/visual/visual-report.js +197 -0
- package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
- package/src/agent_team/common.js +311 -0
- package/src/core/atoms/agent-invoke.js +684 -0
- package/src/core/atoms/event/ingest.js +30 -0
- package/src/core/atoms/experience/digest.js +55 -0
- package/src/core/atoms/gate/artifact-verify.js +83 -0
- package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
- package/src/core/atoms/gate/evidence-collect.js +64 -0
- package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
- package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
- package/src/core/atoms/gate/workbench.js +79 -0
- package/src/core/atoms/intake/classify.js +15 -0
- package/src/core/atoms/intake/design-skill-select.js +16 -0
- package/src/core/atoms/intake/preview-or-refine.js +19 -0
- package/src/core/atoms/intake/recommend.js +20 -0
- package/src/core/atoms/intake/research-same-category.js +19 -0
- package/src/core/atoms/intervention/create.js +30 -0
- package/src/core/atoms/memory/manage.js +27 -0
- package/src/core/atoms/memory/mount.js +13 -0
- package/src/core/atoms/mission/create.js +19 -0
- package/src/core/atoms/mission/list.js +14 -0
- package/src/core/atoms/mission/show.js +26 -0
- package/src/core/atoms/notify/checkpoint.js +46 -0
- package/src/core/atoms/phase-run/complete.js +42 -0
- package/src/core/atoms/phase-run/fail.js +40 -0
- package/src/core/atoms/phase-run/persist.js +39 -0
- package/src/core/atoms/phase-run/start.js +30 -0
- package/src/core/atoms/plugin/run.js +76 -0
- package/src/core/atoms/policy/gate.js +90 -0
- package/src/core/atoms/policy/resolve.js +11 -0
- package/src/core/atoms/storage/sync.js +57 -0
- package/src/core/atoms/workflow/advance.js +25 -0
- package/src/core/atoms/workflow/guard.js +127 -0
- package/src/core/atoms/workflow/state-load.js +37 -0
- package/src/core/bindings.js +37 -0
- package/src/core/change-artifacts.js +134 -0
- package/src/core/competitive-score.js +265 -0
- package/src/core/concurrency-policy.js +35 -0
- package/src/core/execution-log.js +108 -0
- package/src/core/intake-recommendation.js +134 -0
- package/src/core/memory-state.js +62 -0
- package/src/core/mission-state.js +780 -0
- package/src/core/pending-gates.js +66 -0
- package/src/core/phase-cost.js +116 -0
- package/src/core/phase-run-read.js +27 -0
- package/src/core/policy-overlay.js +180 -0
- package/src/core/state-store.js +201 -0
- package/src/core/workflow-executor.js +998 -0
- package/src/core/workflow-integrity.js +111 -0
- package/src/core/workflow-loader.js +163 -0
- package/src/core/workflow-rerun.js +84 -0
- package/src/core/workflow-state.js +119 -0
- package/src/server.js +1307 -0
- package/workflows/corps.yaml +479 -0
- package/workflows/yolo.yaml +233 -0
- package/xflow/README.md +76 -0
- package/xflow/aha/SKILL.md +82 -0
- package/xflow/aha/scripts/append_aha.py +50 -0
- package/xflow/archive/scripts/archive_change.py +338 -0
- package/xflow/archive/templates/merge-design.md +54 -0
- package/xflow/archive/templates/merge-workflow.md +26 -0
- package/xflow/atoms/__init__.py +6 -0
- package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
- package/xflow/atoms/a2_branch_create_and_link.py +42 -0
- package/xflow/atoms/a3_repo_doctor.py +11 -0
- package/xflow/atoms/a4_project_set_status.py +11 -0
- package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
- package/xflow/atoms/a6_pr_create.py +72 -0
- package/xflow/atoms/b1_change_scaffold.py +11 -0
- package/xflow/atoms/b2_status_read.py +16 -0
- package/xflow/atoms/b2b_status_write.py +20 -0
- package/xflow/atoms/b3_status_transition.py +27 -0
- package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
- package/xflow/atoms/b5_aha_append.py +11 -0
- package/xflow/atoms/b6_takein_digest.py +11 -0
- package/xflow/atoms/b6b_lookback_digest.py +11 -0
- package/xflow/atoms/b7_change_mission_bind.py +43 -0
- package/xflow/atoms/c1_mem.py +54 -0
- package/xflow/atoms/e6_gate_local_precheck.py +23 -0
- package/xflow/atoms/h1_design_lite_gate.py +74 -0
- package/xflow/atoms/i6a_tdd_run.py +104 -0
- package/xflow/atoms/i6b_tdd_proof_validate.py +165 -0
- package/xflow/atoms/i6c_tdd_quality_review.py +357 -0
- package/xflow/atoms/j1_tests_run.py +129 -0
- package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
- package/xflow/atoms/j4b_spec_delta_review.py +316 -0
- package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
- package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
- package/xflow/atoms/j6_cr_review.py +443 -0
- package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
- package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
- package/xflow/atoms/k3_handoff_refresh.py +14 -0
- package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
- package/xflow/atoms/k5_archive_publish.py +21 -0
- package/xflow/atoms/k6_aha_merge.py +42 -0
- package/xflow/atoms/tdd_lib.py +747 -0
- package/xflow/corps/SKILL.md +109 -0
- package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
- package/xflow/goal/SKILL.md +150 -0
- package/xflow/handoff/SKILL.md +95 -0
- package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
- package/xflow/lookback/SKILL.md +66 -0
- package/xflow/lookback/scripts/lookback_digest.py +46 -0
- package/xflow/openissue/scripts/open_issue_flow.py +438 -0
- package/xflow/plan/SKILL.md +38 -0
- package/xflow/ralph/SKILL.md +123 -0
- package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
- package/xflow/scripts/common.py +480 -0
- package/xflow/scripts/doctor.py +278 -0
- package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
- package/xflow/takein/SKILL.md +81 -0
- package/xflow/takein/scripts/takein_digest.py +88 -0
- package/xflow/yolo/SKILL.md +66 -0
- package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
- package/xflow/yolo/scripts/yolo_gate.py +312 -0
- package/xflow/yolo/templates/DESIGN.md +179 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
I6b.tdd.proof_validate — Validate TDD proof structure, binding, and re-execution.
|
|
4
|
+
|
|
5
|
+
Required fields: phase, attempt, command, exit_code, passed, recorded_at.
|
|
6
|
+
Red must prove a failing test. Green/refactor must prove passing tests.
|
|
7
|
+
The binding covers the recorded command, exit code, output hash, failure
|
|
8
|
+
class, and snapshot file hashes. This gate recomputes it and re-runs the
|
|
9
|
+
command in a copy of that snapshot. Green also requires snapshotted test
|
|
10
|
+
files to still match the working tree.
|
|
11
|
+
|
|
12
|
+
tdd_mode refactor does not require a red proof. tdd_mode docs does not
|
|
13
|
+
require a red or green proof. A proof that is present is still checked.
|
|
14
|
+
An agent who rebuilds a self-consistent failing snapshot can still produce
|
|
15
|
+
a binding that verifies; the check raises the bar from "edit the JSON" to
|
|
16
|
+
"possess a tree on which this command really exits as claimed".
|
|
17
|
+
"""
|
|
18
|
+
import sys, os, json, argparse, subprocess
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import tdd_lib
|
|
22
|
+
|
|
23
|
+
REQUIRED_FIELDS = ['phase', 'attempt', 'command', 'exit_code', 'passed', 'recorded_at']
|
|
24
|
+
|
|
25
|
+
def expected_result(phase: str):
|
|
26
|
+
if phase == 'red':
|
|
27
|
+
return False
|
|
28
|
+
if phase in {'green', 'refactor', 'run'}:
|
|
29
|
+
return True
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def emit(payload, code):
|
|
33
|
+
print(json.dumps(payload))
|
|
34
|
+
sys.exit(code)
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
p = argparse.ArgumentParser()
|
|
38
|
+
p.add_argument('--project-root', default='.')
|
|
39
|
+
p.add_argument('--change-id', required=True)
|
|
40
|
+
p.add_argument('--phase', default='run')
|
|
41
|
+
p.add_argument('--attempt', type=int, default=0)
|
|
42
|
+
p.add_argument('--tdd-mode', default='', help='red-green | refactor | docs; empty reads status.json')
|
|
43
|
+
args = p.parse_args()
|
|
44
|
+
|
|
45
|
+
project = Path(args.project_root).resolve()
|
|
46
|
+
mode = tdd_lib.resolve_tdd_mode(project, args.change_id, args.tdd_mode)
|
|
47
|
+
if mode not in tdd_lib.MODES:
|
|
48
|
+
emit({'ok': False, 'verdict': 'invalid_tdd_mode', 'tdd_mode': mode, 'phase': args.phase}, 1)
|
|
49
|
+
|
|
50
|
+
proof_file = os.path.join(
|
|
51
|
+
args.project_root, 'specs', 'changes', args.change_id, 'tdd',
|
|
52
|
+
f'{args.phase}-{args.attempt}.json'
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not os.path.exists(proof_file):
|
|
56
|
+
if mode == 'docs' or (mode == 'refactor' and args.phase == 'red'):
|
|
57
|
+
emit({
|
|
58
|
+
'ok': True,
|
|
59
|
+
'verdict': 'not_required',
|
|
60
|
+
'phase': args.phase,
|
|
61
|
+
'tdd_mode': mode,
|
|
62
|
+
'file': proof_file,
|
|
63
|
+
}, 0)
|
|
64
|
+
emit({'ok': False, 'verdict': 'missing', 'file': proof_file, 'phase': args.phase, 'tdd_mode': mode}, 1)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
proof = json.loads(open(proof_file).read())
|
|
68
|
+
except json.JSONDecodeError as e:
|
|
69
|
+
emit({'ok': False, 'verdict': 'invalid_json', 'error': str(e), 'phase': args.phase}, 1)
|
|
70
|
+
|
|
71
|
+
missing = [f for f in REQUIRED_FIELDS if f not in proof]
|
|
72
|
+
if missing:
|
|
73
|
+
emit({'ok': False, 'verdict': 'schema_error', 'missing_fields': missing, 'phase': args.phase}, 1)
|
|
74
|
+
|
|
75
|
+
if proof.get('phase') != args.phase:
|
|
76
|
+
emit({
|
|
77
|
+
'ok': False,
|
|
78
|
+
'verdict': 'phase_mismatch',
|
|
79
|
+
'expected_phase': args.phase,
|
|
80
|
+
'actual_phase': proof.get('phase'),
|
|
81
|
+
'phase': args.phase,
|
|
82
|
+
}, 1)
|
|
83
|
+
|
|
84
|
+
if not proof.get('binding') or not proof.get('failure_kind') or not isinstance(proof.get('file_hashes'), dict):
|
|
85
|
+
emit({'ok': False, 'verdict': 'binding_missing', 'phase': args.phase}, 1)
|
|
86
|
+
|
|
87
|
+
if tdd_lib.compute_binding(proof) != proof.get('binding'):
|
|
88
|
+
emit({'ok': False, 'verdict': 'binding_mismatch', 'phase': args.phase}, 1)
|
|
89
|
+
|
|
90
|
+
if bool(proof.get('passed')) != (proof.get('exit_code') == 0):
|
|
91
|
+
emit({'ok': False, 'verdict': 'proof_inconsistent', 'phase': args.phase}, 1)
|
|
92
|
+
|
|
93
|
+
destination = tdd_lib.snapshot_dir(project, args.change_id, args.phase, args.attempt)
|
|
94
|
+
found = tdd_lib.snapshot_tree_hashes(destination)
|
|
95
|
+
if found is None or found != proof.get('file_hashes'):
|
|
96
|
+
emit({'ok': False, 'verdict': 'snapshot_mismatch', 'phase': args.phase}, 1)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
actual_exit, actual_text = tdd_lib.reexec_snapshot(destination, proof.get('command') or '', timeout=120)
|
|
100
|
+
except subprocess.TimeoutExpired:
|
|
101
|
+
emit({'ok': False, 'verdict': 'reexec_timeout', 'phase': args.phase}, 1)
|
|
102
|
+
|
|
103
|
+
if actual_exit != proof.get('exit_code'):
|
|
104
|
+
emit({
|
|
105
|
+
'ok': False,
|
|
106
|
+
'verdict': 'reexec_exit_mismatch',
|
|
107
|
+
'phase': args.phase,
|
|
108
|
+
'expected_exit': proof.get('exit_code'),
|
|
109
|
+
'actual_exit': actual_exit,
|
|
110
|
+
}, 1)
|
|
111
|
+
|
|
112
|
+
actual_kind = 'timeout' if proof.get('failure_kind') == 'timeout' else tdd_lib.classify_output(actual_text, actual_exit == 0)
|
|
113
|
+
if proof.get('failure_kind') != actual_kind:
|
|
114
|
+
emit({
|
|
115
|
+
'ok': False,
|
|
116
|
+
'verdict': 'reexec_result_mismatch',
|
|
117
|
+
'phase': args.phase,
|
|
118
|
+
'expected_failure_kind': proof.get('failure_kind'),
|
|
119
|
+
'actual_failure_kind': actual_kind,
|
|
120
|
+
}, 1)
|
|
121
|
+
|
|
122
|
+
if args.phase in {'green', 'refactor', 'run'}:
|
|
123
|
+
mismatched = []
|
|
124
|
+
for rel, digest in proof.get('file_hashes', {}).items():
|
|
125
|
+
if not tdd_lib.is_test_file(rel):
|
|
126
|
+
continue
|
|
127
|
+
path = project / rel
|
|
128
|
+
if not path.is_file() or tdd_lib.sha256_bytes(path.read_bytes()) != digest:
|
|
129
|
+
mismatched.append(rel)
|
|
130
|
+
if mismatched:
|
|
131
|
+
emit({
|
|
132
|
+
'ok': False,
|
|
133
|
+
'verdict': 'green_tree_mismatch',
|
|
134
|
+
'phase': args.phase,
|
|
135
|
+
'files': mismatched,
|
|
136
|
+
}, 1)
|
|
137
|
+
|
|
138
|
+
if proof.get('failure_kind') == 'skipped' or tdd_lib.output_non_result(actual_text):
|
|
139
|
+
emit({'ok': False, 'verdict': 'tests_skipped_or_todo', 'phase': args.phase}, 1)
|
|
140
|
+
|
|
141
|
+
expected_passed = expected_result(args.phase)
|
|
142
|
+
if expected_passed is not None and proof.get('passed') is not expected_passed:
|
|
143
|
+
emit({
|
|
144
|
+
'ok': False,
|
|
145
|
+
'verdict': 'semantic_error',
|
|
146
|
+
'phase': args.phase,
|
|
147
|
+
'expected_passed': expected_passed,
|
|
148
|
+
'actual_passed': proof.get('passed'),
|
|
149
|
+
}, 1)
|
|
150
|
+
|
|
151
|
+
if proof.get('expectation_met') is False:
|
|
152
|
+
emit({'ok': False, 'verdict': 'expectation_not_met', 'phase': args.phase}, 1)
|
|
153
|
+
|
|
154
|
+
emit({
|
|
155
|
+
'ok': True,
|
|
156
|
+
'verdict': 'valid',
|
|
157
|
+
'proof_file': proof_file,
|
|
158
|
+
'passed': proof['passed'],
|
|
159
|
+
'exit_code': proof['exit_code'],
|
|
160
|
+
'phase': args.phase,
|
|
161
|
+
'tdd_mode': mode,
|
|
162
|
+
}, 0)
|
|
163
|
+
|
|
164
|
+
if __name__ == '__main__':
|
|
165
|
+
main()
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
I6c.tdd.quality_review - deterministic review for TDD test quality.
|
|
4
|
+
|
|
5
|
+
This gate checks the parts that red/green proof cannot prove by itself:
|
|
6
|
+
code changes should be accompanied by test changes, changed tests must not
|
|
7
|
+
rely on empty assertions or snapshot-only coverage, and mock-heavy tests are
|
|
8
|
+
rejected when they lack enough real assertions.
|
|
9
|
+
|
|
10
|
+
tdd_mode red-green (default) requires a failing red proof and a passing
|
|
11
|
+
green proof. tdd_mode refactor instead requires the existing tests that
|
|
12
|
+
touch the changed code to pass on the pre-change tree and the current tree,
|
|
13
|
+
with no assertion lines removed. tdd_mode docs requires the diff to stay on
|
|
14
|
+
non-code paths. A semantically wrong but plausible expected value is not
|
|
15
|
+
decidable from the diff in general; gate v2 still catches three decidable
|
|
16
|
+
shapes of it: expected values taken from the implementation's own constants
|
|
17
|
+
(the expectation moves when the constant is edited), assertions inside
|
|
18
|
+
named callbacks that are never referenced again (they execute zero times),
|
|
19
|
+
and self-comparison assertions (x == x). Type-only or existence-only
|
|
20
|
+
assertions are flagged only when they are the sole assertions.
|
|
21
|
+
"""
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import tempfile
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
import tdd_lib
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_git(project_root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
34
|
+
return subprocess.run(
|
|
35
|
+
["git", *args],
|
|
36
|
+
cwd=project_root,
|
|
37
|
+
text=True,
|
|
38
|
+
capture_output=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def changed_files(project_root: Path, diff_ref: str) -> list[str]:
|
|
43
|
+
result = run_git(project_root, ["diff", "--name-only", "--diff-filter=ACMR", diff_ref])
|
|
44
|
+
tracked = [] if result.returncode != 0 else [
|
|
45
|
+
line.strip() for line in result.stdout.splitlines() if line.strip()
|
|
46
|
+
]
|
|
47
|
+
untracked_result = run_git(project_root, ["ls-files", "--others", "--exclude-standard"])
|
|
48
|
+
untracked = [] if untracked_result.returncode != 0 else [
|
|
49
|
+
line.strip() for line in untracked_result.stdout.splitlines() if line.strip()
|
|
50
|
+
]
|
|
51
|
+
return sorted(set(tracked + untracked))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def deleted_files(project_root: Path, diff_ref: str) -> list[str]:
|
|
55
|
+
result = run_git(project_root, ["diff", "--name-only", "--diff-filter=D", diff_ref])
|
|
56
|
+
if result.returncode != 0:
|
|
57
|
+
return []
|
|
58
|
+
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def read_json(path: Path) -> dict:
|
|
62
|
+
try:
|
|
63
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
64
|
+
except (OSError, json.JSONDecodeError):
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def read_text(path: Path) -> str:
|
|
69
|
+
try:
|
|
70
|
+
return path.read_text(encoding="utf-8")
|
|
71
|
+
except OSError:
|
|
72
|
+
return ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def review_test_file(project_root: Path, relative_path: str, implementation_sources: list[str] | None = None) -> list[dict]:
|
|
76
|
+
path = project_root / relative_path
|
|
77
|
+
try:
|
|
78
|
+
content = path.read_text(encoding="utf-8")
|
|
79
|
+
except OSError:
|
|
80
|
+
return [{"severity": "fail", "code": "test_unreadable", "path": relative_path}]
|
|
81
|
+
findings = []
|
|
82
|
+
for finding in tdd_lib.analyze_test(content, implementation_sources=implementation_sources):
|
|
83
|
+
item = dict(finding)
|
|
84
|
+
item["path"] = relative_path
|
|
85
|
+
findings.append(item)
|
|
86
|
+
return findings
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def proof_skipped(proof: dict) -> bool:
|
|
90
|
+
if not proof:
|
|
91
|
+
return False
|
|
92
|
+
if proof.get("failure_kind") == "skipped":
|
|
93
|
+
return True
|
|
94
|
+
blob = f"{proof.get('stdout_tail') or ''}\n{proof.get('stderr_tail') or ''}"
|
|
95
|
+
return tdd_lib.output_non_result(blob)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def symbols_in(project_root: Path, code_files: list[str]) -> set[str]:
|
|
99
|
+
symbols = set()
|
|
100
|
+
for relative in code_files:
|
|
101
|
+
symbols |= tdd_lib.defined_symbols(read_text(project_root / relative))
|
|
102
|
+
return symbols
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def iter_repo_tests(project_root: Path) -> list[str]:
|
|
106
|
+
found = []
|
|
107
|
+
skip = tdd_lib.SKIP_DIR_NAMES
|
|
108
|
+
for dirpath, dirnames, filenames in os_walk(project_root):
|
|
109
|
+
dirnames[:] = [name for name in dirnames if name not in skip]
|
|
110
|
+
for name in filenames:
|
|
111
|
+
full = Path(dirpath) / name
|
|
112
|
+
rel = full.relative_to(project_root).as_posix()
|
|
113
|
+
if tdd_lib.is_test_file(rel):
|
|
114
|
+
found.append(rel)
|
|
115
|
+
return found
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def os_walk(project_root: Path):
|
|
119
|
+
import os
|
|
120
|
+
return os.walk(project_root)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def refactor_findings(project_root: Path, diff_ref: str, code_files: list[str], test_files: list[str], green_proof: dict) -> list[dict]:
|
|
124
|
+
findings = []
|
|
125
|
+
command = green_proof.get("command") or ""
|
|
126
|
+
if not command:
|
|
127
|
+
return findings
|
|
128
|
+
|
|
129
|
+
for relative in code_files:
|
|
130
|
+
if tdd_lib.file_at_ref(project_root, diff_ref, relative) is None:
|
|
131
|
+
findings.append({
|
|
132
|
+
"severity": "fail",
|
|
133
|
+
"code": "refactor_new_code",
|
|
134
|
+
"path": relative,
|
|
135
|
+
"message": "Refactor mode cannot introduce a code file that is not in the baseline.",
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
for relative in deleted_files(project_root, diff_ref):
|
|
139
|
+
if tdd_lib.is_test_file(relative):
|
|
140
|
+
findings.append({
|
|
141
|
+
"severity": "fail",
|
|
142
|
+
"code": "assertions_removed",
|
|
143
|
+
"path": relative,
|
|
144
|
+
"message": "A test file was deleted during a declared refactor.",
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
for relative in test_files:
|
|
148
|
+
previous = tdd_lib.file_at_ref(project_root, diff_ref, relative)
|
|
149
|
+
if previous is None:
|
|
150
|
+
continue
|
|
151
|
+
old_lines = tdd_lib.assertion_lines(previous.decode("utf-8", errors="replace"))
|
|
152
|
+
new_lines = tdd_lib.assertion_lines(read_text(project_root / relative))
|
|
153
|
+
leftover = list(old_lines)
|
|
154
|
+
for line in new_lines:
|
|
155
|
+
if line in leftover:
|
|
156
|
+
leftover.remove(line)
|
|
157
|
+
if leftover:
|
|
158
|
+
findings.append({
|
|
159
|
+
"severity": "fail",
|
|
160
|
+
"code": "assertions_removed",
|
|
161
|
+
"path": relative,
|
|
162
|
+
"message": "Refactor mode removed or rewrote an existing assertion.",
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
symbols = symbols_in(project_root, code_files)
|
|
166
|
+
covering = []
|
|
167
|
+
if symbols:
|
|
168
|
+
for relative in iter_repo_tests(project_root):
|
|
169
|
+
if tdd_lib.references_symbol(read_text(project_root / relative), symbols):
|
|
170
|
+
covering.append(relative)
|
|
171
|
+
if not covering:
|
|
172
|
+
findings.append({
|
|
173
|
+
"severity": "fail",
|
|
174
|
+
"code": "refactor_missing_covering_test",
|
|
175
|
+
"message": "No existing test references a symbol defined in the changed code.",
|
|
176
|
+
})
|
|
177
|
+
if findings:
|
|
178
|
+
return findings
|
|
179
|
+
|
|
180
|
+
temp_root = Path(tempfile.mkdtemp(prefix="evalid-refactor-"))
|
|
181
|
+
try:
|
|
182
|
+
before = temp_root / "before"
|
|
183
|
+
after = temp_root / "after"
|
|
184
|
+
before.mkdir()
|
|
185
|
+
after.mkdir()
|
|
186
|
+
tdd_lib.copy_project(project_root, before)
|
|
187
|
+
tdd_lib.copy_project(project_root, after)
|
|
188
|
+
for relative in code_files:
|
|
189
|
+
baseline = tdd_lib.file_at_ref(project_root, diff_ref, relative)
|
|
190
|
+
if baseline is None:
|
|
191
|
+
continue
|
|
192
|
+
target = before / relative
|
|
193
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
target.write_bytes(baseline)
|
|
195
|
+
try:
|
|
196
|
+
before_run = tdd_lib.run_shell(command, before, timeout=120)
|
|
197
|
+
after_run = tdd_lib.run_shell(command, after, timeout=120)
|
|
198
|
+
except subprocess.TimeoutExpired:
|
|
199
|
+
findings.append({"severity": "fail", "code": "refactor_command_timeout"})
|
|
200
|
+
return findings
|
|
201
|
+
if before_run.returncode != 0:
|
|
202
|
+
findings.append({
|
|
203
|
+
"severity": "fail",
|
|
204
|
+
"code": "refactor_tests_failed_before",
|
|
205
|
+
"message": "Existing tests must pass on the pre-change tree.",
|
|
206
|
+
})
|
|
207
|
+
if after_run.returncode != 0:
|
|
208
|
+
findings.append({
|
|
209
|
+
"severity": "fail",
|
|
210
|
+
"code": "refactor_tests_failed_after",
|
|
211
|
+
"message": "Existing tests must pass after the refactor.",
|
|
212
|
+
})
|
|
213
|
+
finally:
|
|
214
|
+
import shutil
|
|
215
|
+
shutil.rmtree(temp_root, ignore_errors=True)
|
|
216
|
+
return findings
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main() -> None:
|
|
220
|
+
parser = argparse.ArgumentParser()
|
|
221
|
+
parser.add_argument("--project-root", default=".")
|
|
222
|
+
parser.add_argument("--change-id", required=True)
|
|
223
|
+
parser.add_argument("--diff-ref", default="HEAD")
|
|
224
|
+
parser.add_argument("--tdd-mode", default="", help="red-green | refactor | docs; empty reads status.json")
|
|
225
|
+
args = parser.parse_args()
|
|
226
|
+
|
|
227
|
+
project_root = Path(args.project_root).resolve()
|
|
228
|
+
mode = tdd_lib.resolve_tdd_mode(project_root, args.change_id, args.tdd_mode)
|
|
229
|
+
change_dir = project_root / "specs" / "changes" / args.change_id
|
|
230
|
+
proof_dir = change_dir / "tdd"
|
|
231
|
+
red_proof = read_json(proof_dir / "red-0.json")
|
|
232
|
+
green_proof = read_json(proof_dir / "green-0.json")
|
|
233
|
+
status = tdd_lib.read_status(project_root, args.change_id)
|
|
234
|
+
|
|
235
|
+
files = changed_files(project_root, args.diff_ref)
|
|
236
|
+
test_files = [path for path in files if tdd_lib.is_test_file(path)]
|
|
237
|
+
code_files = [path for path in files if tdd_lib.is_code_file(path)]
|
|
238
|
+
|
|
239
|
+
findings = []
|
|
240
|
+
if mode not in tdd_lib.MODES:
|
|
241
|
+
findings.append({"severity": "fail", "code": "invalid_tdd_mode", "tdd_mode": mode})
|
|
242
|
+
|
|
243
|
+
if mode == "docs":
|
|
244
|
+
if code_files or test_files:
|
|
245
|
+
findings.append({
|
|
246
|
+
"severity": "fail",
|
|
247
|
+
"code": "docs_mode_with_code_change",
|
|
248
|
+
"message": "Docs mode was declared, but the diff changes code or tests.",
|
|
249
|
+
"code_files": code_files,
|
|
250
|
+
"test_files": test_files,
|
|
251
|
+
})
|
|
252
|
+
if green_proof and green_proof.get("passed") is not True:
|
|
253
|
+
findings.append({"severity": "fail", "code": "green_proof_not_passing"})
|
|
254
|
+
elif mode == "refactor":
|
|
255
|
+
if status.get("behavior_change") is True:
|
|
256
|
+
findings.append({
|
|
257
|
+
"severity": "fail",
|
|
258
|
+
"code": "refactor_declares_behavior_change",
|
|
259
|
+
"message": "Refactor mode cannot declare a public behavior change.",
|
|
260
|
+
})
|
|
261
|
+
if green_proof.get("passed") is not True:
|
|
262
|
+
findings.append({"severity": "fail", "code": "green_proof_not_passing"})
|
|
263
|
+
else:
|
|
264
|
+
findings.extend(refactor_findings(project_root, args.diff_ref, code_files, test_files, green_proof))
|
|
265
|
+
else:
|
|
266
|
+
if code_files and not test_files:
|
|
267
|
+
findings.append({
|
|
268
|
+
"severity": "fail",
|
|
269
|
+
"code": "code_without_test_change",
|
|
270
|
+
"message": "Code changed without a matching changed test file.",
|
|
271
|
+
"code_files": code_files,
|
|
272
|
+
})
|
|
273
|
+
if red_proof.get("passed") is not False:
|
|
274
|
+
findings.append({"severity": "fail", "code": "red_proof_not_failing"})
|
|
275
|
+
if green_proof.get("passed") is not True:
|
|
276
|
+
findings.append({"severity": "fail", "code": "green_proof_not_passing"})
|
|
277
|
+
if code_files or test_files:
|
|
278
|
+
problem = tdd_lib.command_problem(red_proof.get("command") or "", green_proof.get("command") or "")
|
|
279
|
+
if problem:
|
|
280
|
+
findings.append({
|
|
281
|
+
"severity": "fail",
|
|
282
|
+
"code": problem,
|
|
283
|
+
"message": "Red and green must run the same test, or green must be a superset of the red test.",
|
|
284
|
+
"red_command": red_proof.get("command"),
|
|
285
|
+
"green_command": green_proof.get("command"),
|
|
286
|
+
})
|
|
287
|
+
kind = red_proof.get("failure_kind")
|
|
288
|
+
if kind in {"import_or_syntax", "no_test_evidence", "timeout"}:
|
|
289
|
+
findings.append({
|
|
290
|
+
"severity": "fail",
|
|
291
|
+
"code": "red_failure_not_assertion",
|
|
292
|
+
"message": "The red failure is not an assertion or test error in the changed test.",
|
|
293
|
+
"failure_kind": kind,
|
|
294
|
+
})
|
|
295
|
+
if proof_skipped(red_proof) or proof_skipped(green_proof):
|
|
296
|
+
findings.append({
|
|
297
|
+
"severity": "fail",
|
|
298
|
+
"code": "tests_skipped_or_todo",
|
|
299
|
+
"message": "A recorded run skipped or marked todo the tests it claims to have executed.",
|
|
300
|
+
})
|
|
301
|
+
symbols = symbols_in(project_root, code_files)
|
|
302
|
+
if symbols and test_files:
|
|
303
|
+
for relative in test_files:
|
|
304
|
+
if not tdd_lib.references_symbol(read_text(project_root / relative), symbols):
|
|
305
|
+
findings.append({
|
|
306
|
+
"severity": "fail",
|
|
307
|
+
"code": "test_misses_changed_symbol",
|
|
308
|
+
"path": relative,
|
|
309
|
+
"message": "A changed test does not reference a symbol defined in the changed code.",
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
reviewed = set(test_files)
|
|
313
|
+
if mode == "refactor":
|
|
314
|
+
symbols = symbols_in(project_root, code_files)
|
|
315
|
+
for relative in iter_repo_tests(project_root):
|
|
316
|
+
if symbols and tdd_lib.references_symbol(read_text(project_root / relative), symbols):
|
|
317
|
+
reviewed.add(relative)
|
|
318
|
+
implementation_sources = [read_text(project_root / relative) for relative in code_files]
|
|
319
|
+
for test_file in sorted(reviewed):
|
|
320
|
+
findings.extend(review_test_file(project_root, test_file, implementation_sources))
|
|
321
|
+
|
|
322
|
+
failed = any(finding.get("severity") == "fail" for finding in findings)
|
|
323
|
+
verdict = "failed" if failed else "passed"
|
|
324
|
+
proof = {
|
|
325
|
+
"ok": not failed,
|
|
326
|
+
"verdict": verdict,
|
|
327
|
+
"tdd_mode": mode,
|
|
328
|
+
"diff_ref": args.diff_ref,
|
|
329
|
+
"changed_files": files,
|
|
330
|
+
"code_files": code_files,
|
|
331
|
+
"test_files": test_files,
|
|
332
|
+
"findings": findings,
|
|
333
|
+
"recorded_at": datetime.now(timezone.utc).isoformat(),
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
proof_dir.mkdir(parents=True, exist_ok=True)
|
|
337
|
+
proof_file = proof_dir / "quality-0.json"
|
|
338
|
+
proof_file.write_text(json.dumps(proof, indent=2) + "\n", encoding="utf-8")
|
|
339
|
+
|
|
340
|
+
print(json.dumps({
|
|
341
|
+
"ok": proof["ok"],
|
|
342
|
+
"verdict": verdict,
|
|
343
|
+
"tdd_mode": mode,
|
|
344
|
+
"proof_file": str(proof_file),
|
|
345
|
+
"failures": sum(1 for finding in findings if finding.get("severity") == "fail"),
|
|
346
|
+
"warnings": sum(1 for finding in findings if finding.get("severity") == "warn"),
|
|
347
|
+
"failed_codes": [
|
|
348
|
+
finding.get("code")
|
|
349
|
+
for finding in findings
|
|
350
|
+
if finding.get("severity") == "fail"
|
|
351
|
+
],
|
|
352
|
+
}))
|
|
353
|
+
sys.exit(0 if proof["ok"] else 1)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
if __name__ == "__main__":
|
|
357
|
+
main()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
J1.tests.run — Run planned verification commands, capture pass/fail proof.
|
|
4
|
+
Reads commands from plan.md (lines starting with ` ``` bash` blocks or --commands arg).
|
|
5
|
+
"""
|
|
6
|
+
import sys, os, json, argparse, subprocess, re
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
def extract_commands_from_plan(plan_path):
|
|
10
|
+
"""Extract bash commands from plan.md code blocks."""
|
|
11
|
+
if not os.path.exists(plan_path):
|
|
12
|
+
return []
|
|
13
|
+
content = open(plan_path).read()
|
|
14
|
+
# Find ```bash ... ``` blocks
|
|
15
|
+
blocks = re.findall(r'```bash\n(.*?)```', content, re.DOTALL)
|
|
16
|
+
commands = []
|
|
17
|
+
for block in blocks:
|
|
18
|
+
for line in block.strip().splitlines():
|
|
19
|
+
line = line.strip()
|
|
20
|
+
if line and not line.startswith('#'):
|
|
21
|
+
commands.append(line)
|
|
22
|
+
return commands
|
|
23
|
+
|
|
24
|
+
def normalize_commands(value):
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
command = value.strip()
|
|
27
|
+
return [command] if command else []
|
|
28
|
+
if isinstance(value, list):
|
|
29
|
+
commands = []
|
|
30
|
+
for item in value:
|
|
31
|
+
if isinstance(item, str) and item.strip():
|
|
32
|
+
commands.append(item.strip())
|
|
33
|
+
elif isinstance(item, dict) and isinstance(item.get('command'), str) and item.get('command').strip():
|
|
34
|
+
commands.append(item['command'].strip())
|
|
35
|
+
return commands
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
def extract_commands_from_change_contract(project_root, change_id):
|
|
39
|
+
change_dir = os.path.join(project_root, 'specs', 'changes', change_id)
|
|
40
|
+
candidates = [
|
|
41
|
+
'plan.json',
|
|
42
|
+
'corps-input.json',
|
|
43
|
+
'yolo-input.json',
|
|
44
|
+
'input.json',
|
|
45
|
+
'intake.json',
|
|
46
|
+
]
|
|
47
|
+
command_keys = [
|
|
48
|
+
'verification_commands',
|
|
49
|
+
'test_commands',
|
|
50
|
+
'qa_commands',
|
|
51
|
+
'commands',
|
|
52
|
+
]
|
|
53
|
+
fallback_keys = [
|
|
54
|
+
'tdd_green_command',
|
|
55
|
+
]
|
|
56
|
+
for filename in candidates:
|
|
57
|
+
path = os.path.join(change_dir, filename)
|
|
58
|
+
if not os.path.exists(path):
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
data = json.load(open(path))
|
|
62
|
+
except (OSError, json.JSONDecodeError):
|
|
63
|
+
continue
|
|
64
|
+
for key in command_keys:
|
|
65
|
+
commands = normalize_commands(data.get(key))
|
|
66
|
+
if commands:
|
|
67
|
+
return commands
|
|
68
|
+
for key in fallback_keys:
|
|
69
|
+
commands = normalize_commands(data.get(key))
|
|
70
|
+
if commands:
|
|
71
|
+
return commands
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
def verification_command_env():
|
|
75
|
+
env = os.environ.copy()
|
|
76
|
+
# J1's own orchestration change id should not alter the behavior of the
|
|
77
|
+
# verification commands it runs. Commands that need a CHANGE_ID must set it.
|
|
78
|
+
env.pop('CHANGE_ID', None)
|
|
79
|
+
return env
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
p = argparse.ArgumentParser()
|
|
83
|
+
p.add_argument('--project-root', default='.')
|
|
84
|
+
p.add_argument('--change-id', required=True)
|
|
85
|
+
p.add_argument('--commands', nargs='*', help='Explicit commands to run')
|
|
86
|
+
args = p.parse_args()
|
|
87
|
+
|
|
88
|
+
commands = args.commands
|
|
89
|
+
if not commands:
|
|
90
|
+
plan_md = os.path.join(args.project_root, 'specs', 'changes', args.change_id, 'plan.md')
|
|
91
|
+
commands = extract_commands_from_plan(plan_md)
|
|
92
|
+
if not commands:
|
|
93
|
+
commands = extract_commands_from_change_contract(args.project_root, args.change_id)
|
|
94
|
+
|
|
95
|
+
if not commands:
|
|
96
|
+
print(json.dumps({'ok': False, 'verdict': 'no_commands', 'message': 'No test commands found in plan.md or change contract'}))
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
results = []
|
|
100
|
+
all_passed = True
|
|
101
|
+
command_env = verification_command_env()
|
|
102
|
+
for cmd in commands:
|
|
103
|
+
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, cwd=args.project_root, timeout=120, env=command_env)
|
|
104
|
+
passed = r.returncode == 0
|
|
105
|
+
all_passed = all_passed and passed
|
|
106
|
+
results.append({
|
|
107
|
+
'command': cmd,
|
|
108
|
+
'exit_code': r.returncode,
|
|
109
|
+
'passed': passed,
|
|
110
|
+
'stdout_tail': r.stdout[-1000:] if r.stdout else '',
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
proof = {
|
|
114
|
+
'all_passed': all_passed,
|
|
115
|
+
'results': results,
|
|
116
|
+
'recorded_at': datetime.now(timezone.utc).isoformat(),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
proof_dir = os.path.join(args.project_root, 'specs', 'changes', args.change_id)
|
|
120
|
+
os.makedirs(proof_dir, exist_ok=True)
|
|
121
|
+
proof_file = os.path.join(proof_dir, 'verify_proof.json')
|
|
122
|
+
with open(proof_file, 'w') as f:
|
|
123
|
+
json.dump(proof, f, indent=2)
|
|
124
|
+
|
|
125
|
+
print(json.dumps({'ok': all_passed, 'proof_file': proof_file, 'total': len(results), 'passed': sum(1 for r in results if r['passed'])}))
|
|
126
|
+
sys.exit(0 if all_passed else 1)
|
|
127
|
+
|
|
128
|
+
if __name__ == '__main__':
|
|
129
|
+
main()
|