@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,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
J4a.spec_consistency.structural — Deterministic structural drift check.
|
|
4
|
+
Extends doctor.py with proposal/plan/AHA consistency checks.
|
|
5
|
+
Catches ~80% of drifts deterministically. Semantic drift stays LLM-driven (J4b).
|
|
6
|
+
"""
|
|
7
|
+
import sys, os, json, argparse, re
|
|
8
|
+
|
|
9
|
+
def load_json_if_exists(path):
|
|
10
|
+
if os.path.exists(path):
|
|
11
|
+
try:
|
|
12
|
+
return json.loads(open(path).read())
|
|
13
|
+
except Exception:
|
|
14
|
+
return None
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
def load_text_if_exists(path):
|
|
18
|
+
if os.path.exists(path):
|
|
19
|
+
return open(path).read()
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def extract_section_headings(md_text):
|
|
23
|
+
"""Extract all ## and ### headings from markdown."""
|
|
24
|
+
if not md_text:
|
|
25
|
+
return []
|
|
26
|
+
return re.findall(r'^#{1,3} (.+)$', md_text, re.MULTILINE)
|
|
27
|
+
|
|
28
|
+
def extract_task_items(md_text):
|
|
29
|
+
"""Extract task checklist items from markdown."""
|
|
30
|
+
if not md_text:
|
|
31
|
+
return []
|
|
32
|
+
return re.findall(r'- \[[ x]\] (.+)', md_text)
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
p = argparse.ArgumentParser()
|
|
36
|
+
p.add_argument('--project-root', default='.')
|
|
37
|
+
p.add_argument('--change-id', required=True)
|
|
38
|
+
p.add_argument('--json', action='store_true')
|
|
39
|
+
args = p.parse_args()
|
|
40
|
+
|
|
41
|
+
change_dir = os.path.join(args.project_root, 'specs', 'changes', args.change_id)
|
|
42
|
+
issues = []
|
|
43
|
+
warnings = []
|
|
44
|
+
|
|
45
|
+
# 1. Required files exist
|
|
46
|
+
for fname in ['proposal.md', 'plan.md', 'tasks.md', 'status.json']:
|
|
47
|
+
fpath = os.path.join(change_dir, fname)
|
|
48
|
+
if not os.path.exists(fpath):
|
|
49
|
+
issues.append(f'Missing required file: {fpath}')
|
|
50
|
+
|
|
51
|
+
# 2. status.json fields
|
|
52
|
+
status = load_json_if_exists(os.path.join(change_dir, 'status.json'))
|
|
53
|
+
if status:
|
|
54
|
+
for field in ['change_id', 'title', 'change_type', 'status', 'current_stage']:
|
|
55
|
+
if not status.get(field):
|
|
56
|
+
issues.append(f'status.json missing field: {field}')
|
|
57
|
+
if status.get('change_id') != args.change_id:
|
|
58
|
+
issues.append(f'status.json change_id mismatch: {status.get("change_id")} != {args.change_id}')
|
|
59
|
+
|
|
60
|
+
# 3. proposal.md has minimum structure
|
|
61
|
+
proposal_text = load_text_if_exists(os.path.join(change_dir, 'proposal.md'))
|
|
62
|
+
if proposal_text:
|
|
63
|
+
headings = extract_section_headings(proposal_text)
|
|
64
|
+
for required_h in ['Problem', 'Scope']:
|
|
65
|
+
if not any(required_h.lower() in h.lower() for h in headings):
|
|
66
|
+
warnings.append(f'proposal.md missing expected section: "{required_h}" (found: {headings})')
|
|
67
|
+
else:
|
|
68
|
+
issues.append('proposal.md is empty or missing')
|
|
69
|
+
|
|
70
|
+
# 4. plan.md may coexist with tasks.md, but plan itself stays reusable
|
|
71
|
+
plan_text = load_text_if_exists(os.path.join(change_dir, 'plan.md'))
|
|
72
|
+
tasks_text = load_text_if_exists(os.path.join(change_dir, 'tasks.md'))
|
|
73
|
+
if plan_text and len(plan_text.strip()) < 100:
|
|
74
|
+
warnings.append('plan.md seems too short (< 100 chars)')
|
|
75
|
+
if tasks_text:
|
|
76
|
+
task_items = extract_task_items(tasks_text)
|
|
77
|
+
if len(task_items) == 0:
|
|
78
|
+
warnings.append('tasks.md has no checklist items')
|
|
79
|
+
|
|
80
|
+
# 5. AHA.md keyword collision check (if AHA.md exists at root)
|
|
81
|
+
aha_path = os.path.join(args.project_root, 'AHA.md')
|
|
82
|
+
aha_text = load_text_if_exists(aha_path)
|
|
83
|
+
if aha_text and proposal_text:
|
|
84
|
+
# Simple heuristic: look for any "must not" or "avoid" rules in AHA that match keywords in proposal
|
|
85
|
+
must_not = re.findall(r'(?:must not|avoid|do not)\s+(.+?)(?:\.|$)', aha_text, re.IGNORECASE)
|
|
86
|
+
for rule in must_not:
|
|
87
|
+
rule_lower = rule.lower().strip()
|
|
88
|
+
if len(rule_lower) > 10 and rule_lower in proposal_text.lower():
|
|
89
|
+
warnings.append(f'Proposal may conflict with AHA rule: "{rule_lower}"')
|
|
90
|
+
|
|
91
|
+
ok = len(issues) == 0
|
|
92
|
+
result = {
|
|
93
|
+
'ok': ok,
|
|
94
|
+
'gate': 'J4a.spec_consistency.structural',
|
|
95
|
+
'verdict': 'pass' if ok else 'fail',
|
|
96
|
+
'issues': issues,
|
|
97
|
+
'warnings': warnings,
|
|
98
|
+
'change_id': args.change_id,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
print(json.dumps(result, indent=2))
|
|
102
|
+
sys.exit(0 if ok else 1)
|
|
103
|
+
|
|
104
|
+
if __name__ == '__main__':
|
|
105
|
+
main()
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
J4b.spec_delta.review - Generate a deterministic spec delta review.
|
|
4
|
+
|
|
5
|
+
This is intentionally not an LLM judge. It creates a reviewer-facing summary of
|
|
6
|
+
proposal, plan, tasks, verification commands, and merge-spec snippets so humans
|
|
7
|
+
and agents can inspect the change at the spec layer before reading code.
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import tdd_lib
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ROOT_SPEC_FILES = ["product.md", "architecture.md", "workflow.md", "README.md"]
|
|
22
|
+
MERGE_SPEC_FILES = [
|
|
23
|
+
"merge-product.md",
|
|
24
|
+
"merge-architecture.md",
|
|
25
|
+
"merge-workflow.md",
|
|
26
|
+
"merge-aha.md",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_text(path: Path) -> str:
|
|
31
|
+
try:
|
|
32
|
+
return path.read_text(encoding="utf-8")
|
|
33
|
+
except OSError:
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def headings(text: str) -> list[str]:
|
|
38
|
+
return [match.strip() for match in re.findall(r"^#{1,4}\s+(.+)$", text, re.MULTILINE)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def bullets(text: str) -> list[str]:
|
|
42
|
+
return [match.strip() for match in re.findall(r"^\s*[-*]\s+(.+)$", text, re.MULTILINE)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def requirement_statements(text: str) -> list[dict]:
|
|
46
|
+
statements = []
|
|
47
|
+
for line_number, raw in enumerate(text.splitlines(), start=1):
|
|
48
|
+
line = raw.strip()
|
|
49
|
+
if not line:
|
|
50
|
+
continue
|
|
51
|
+
normalized = re.sub(r"^\s*(?:[-*]|\d+\.)\s+", "", line)
|
|
52
|
+
normalized = re.sub(r"^\[[ xX]\]\s+", "", normalized).strip()
|
|
53
|
+
if not normalized:
|
|
54
|
+
continue
|
|
55
|
+
marker_match = re.search(
|
|
56
|
+
r"\b(MUST|SHOULD|REQUIRED|REQUIRES|ACCEPTANCE|VERIFY|VALIDATE|PROOF|GATE)\b",
|
|
57
|
+
normalized,
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
if marker_match:
|
|
61
|
+
statements.append({
|
|
62
|
+
"line": line_number,
|
|
63
|
+
"marker": marker_match.group(1).upper(),
|
|
64
|
+
"text": normalized,
|
|
65
|
+
})
|
|
66
|
+
return statements
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def tasks(text: str) -> list[dict]:
|
|
70
|
+
items = []
|
|
71
|
+
for done, body in re.findall(r"^\s*[-*]\s+\[([ xX])\]\s+(.+)$", text, re.MULTILINE):
|
|
72
|
+
items.append({"done": done.lower() == "x", "text": body.strip()})
|
|
73
|
+
return items
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def verification_commands(text: str) -> list[str]:
|
|
77
|
+
commands = []
|
|
78
|
+
for block in re.findall(r"```(?:bash|sh|shell)?\n(.*?)```", text, re.DOTALL):
|
|
79
|
+
for line in block.splitlines():
|
|
80
|
+
candidate = line.strip()
|
|
81
|
+
if candidate and not candidate.startswith("#"):
|
|
82
|
+
commands.append(candidate)
|
|
83
|
+
return commands
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def first_nonempty_lines(text: str, limit: int = 5) -> list[str]:
|
|
87
|
+
lines = []
|
|
88
|
+
for line in text.splitlines():
|
|
89
|
+
stripped = line.strip()
|
|
90
|
+
if stripped:
|
|
91
|
+
lines.append(stripped)
|
|
92
|
+
if len(lines) >= limit:
|
|
93
|
+
break
|
|
94
|
+
return lines
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def spec_touchpoints(change_dir: Path) -> list[dict]:
|
|
98
|
+
touchpoints = []
|
|
99
|
+
for filename in MERGE_SPEC_FILES:
|
|
100
|
+
path = change_dir / filename
|
|
101
|
+
if not path.exists():
|
|
102
|
+
continue
|
|
103
|
+
text = read_text(path)
|
|
104
|
+
touchpoints.append({
|
|
105
|
+
"file": filename,
|
|
106
|
+
"headings": headings(text),
|
|
107
|
+
"bullets": bullets(text)[:12],
|
|
108
|
+
"requirements": requirement_statements(text)[:12],
|
|
109
|
+
"preview": first_nonempty_lines(text, 4),
|
|
110
|
+
})
|
|
111
|
+
return touchpoints
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def root_spec_presence(project_root: Path) -> list[dict]:
|
|
115
|
+
specs_dir = project_root / "specs"
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
"file": f"specs/{filename}",
|
|
119
|
+
"exists": (specs_dir / filename).exists(),
|
|
120
|
+
}
|
|
121
|
+
for filename in ROOT_SPEC_FILES
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def requirements_delta(proposal_text: str, plan_text: str, touchpoints: list[dict]) -> dict:
|
|
126
|
+
proposal_requirements = requirement_statements(proposal_text)
|
|
127
|
+
plan_requirements = requirement_statements(plan_text)
|
|
128
|
+
touchpoint_requirements = [
|
|
129
|
+
{
|
|
130
|
+
"file": touchpoint["file"],
|
|
131
|
+
"line": requirement["line"],
|
|
132
|
+
"marker": requirement["marker"],
|
|
133
|
+
"text": requirement["text"],
|
|
134
|
+
}
|
|
135
|
+
for touchpoint in touchpoints
|
|
136
|
+
for requirement in touchpoint.get("requirements", [])
|
|
137
|
+
]
|
|
138
|
+
return {
|
|
139
|
+
"proposal": proposal_requirements[:20],
|
|
140
|
+
"plan": plan_requirements[:20],
|
|
141
|
+
"touchpoints": touchpoint_requirements[:20],
|
|
142
|
+
"counts": {
|
|
143
|
+
"proposal": len(proposal_requirements),
|
|
144
|
+
"plan": len(plan_requirements),
|
|
145
|
+
"touchpoints": len(touchpoint_requirements),
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def render_markdown(review: dict) -> str:
|
|
151
|
+
lines = [
|
|
152
|
+
"# Spec Delta Review",
|
|
153
|
+
"",
|
|
154
|
+
f"- Change: `{review['change_id']}`",
|
|
155
|
+
f"- Verdict: `{review['verdict']}`",
|
|
156
|
+
f"- Recorded: `{review['recorded_at']}`",
|
|
157
|
+
"",
|
|
158
|
+
"## Summary",
|
|
159
|
+
"",
|
|
160
|
+
f"- Proposal sections: {', '.join(review['proposal']['headings']) or '(none)'}",
|
|
161
|
+
f"- Plan sections: {', '.join(review['plan']['headings']) or '(none)'}",
|
|
162
|
+
f"- Task count: {review['tasks']['total']} ({review['tasks']['done']} done)",
|
|
163
|
+
f"- Verification commands: {len(review['plan']['verification_commands'])}",
|
|
164
|
+
f"- Merge spec snippets: {len(review['spec_touchpoints'])}",
|
|
165
|
+
f"- Requirement statements: {review['requirements_delta']['counts']['proposal']} proposal / {review['requirements_delta']['counts']['plan']} plan / {review['requirements_delta']['counts']['touchpoints']} touchpoint",
|
|
166
|
+
"",
|
|
167
|
+
"## Issues",
|
|
168
|
+
"",
|
|
169
|
+
]
|
|
170
|
+
if review["issues"]:
|
|
171
|
+
lines.extend([f"- {issue}" for issue in review["issues"]])
|
|
172
|
+
else:
|
|
173
|
+
lines.append("- None")
|
|
174
|
+
|
|
175
|
+
lines.extend(["", "## Warnings", ""])
|
|
176
|
+
if review["warnings"]:
|
|
177
|
+
lines.extend([f"- {warning}" for warning in review["warnings"]])
|
|
178
|
+
else:
|
|
179
|
+
lines.append("- None")
|
|
180
|
+
|
|
181
|
+
lines.extend(["", "## Spec Touchpoints", ""])
|
|
182
|
+
if review["spec_touchpoints"]:
|
|
183
|
+
for touchpoint in review["spec_touchpoints"]:
|
|
184
|
+
lines.append(f"### {touchpoint['file']}")
|
|
185
|
+
if touchpoint["bullets"]:
|
|
186
|
+
lines.extend([f"- {bullet}" for bullet in touchpoint["bullets"]])
|
|
187
|
+
else:
|
|
188
|
+
lines.append("- No bullet summary found.")
|
|
189
|
+
lines.append("")
|
|
190
|
+
else:
|
|
191
|
+
lines.append("- No merge spec snippets found.")
|
|
192
|
+
|
|
193
|
+
lines.extend(["", "## Requirements Delta", ""])
|
|
194
|
+
for bucket in ["proposal", "plan", "touchpoints"]:
|
|
195
|
+
lines.append(f"### {bucket.title()}")
|
|
196
|
+
items = review["requirements_delta"][bucket]
|
|
197
|
+
if items:
|
|
198
|
+
for item in items:
|
|
199
|
+
source = f"{item['file']}:" if "file" in item else ""
|
|
200
|
+
lines.append(f"- {source}L{item['line']} `{item['marker']}` {item['text']}")
|
|
201
|
+
else:
|
|
202
|
+
lines.append("- None")
|
|
203
|
+
lines.append("")
|
|
204
|
+
|
|
205
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main() -> None:
|
|
209
|
+
parser = argparse.ArgumentParser()
|
|
210
|
+
parser.add_argument("--project-root", default=".")
|
|
211
|
+
parser.add_argument("--change-id", required=True)
|
|
212
|
+
args = parser.parse_args()
|
|
213
|
+
|
|
214
|
+
project_root = Path(args.project_root).resolve()
|
|
215
|
+
change_dir = project_root / "specs" / "changes" / args.change_id
|
|
216
|
+
proposal_text = read_text(change_dir / "proposal.md")
|
|
217
|
+
plan_text = read_text(change_dir / "plan.md")
|
|
218
|
+
tasks_text = read_text(change_dir / "tasks.md")
|
|
219
|
+
task_items = tasks(tasks_text)
|
|
220
|
+
|
|
221
|
+
issues = []
|
|
222
|
+
warnings = []
|
|
223
|
+
if not proposal_text.strip():
|
|
224
|
+
issues.append("proposal.md is missing or empty")
|
|
225
|
+
if not plan_text.strip():
|
|
226
|
+
issues.append("plan.md is missing or empty")
|
|
227
|
+
if task_items and not any(item["done"] for item in task_items):
|
|
228
|
+
warnings.append("tasks.md has checklist items, but none are marked done")
|
|
229
|
+
if not verification_commands(plan_text):
|
|
230
|
+
warnings.append("plan.md has no fenced verification commands")
|
|
231
|
+
|
|
232
|
+
touchpoints = spec_touchpoints(change_dir)
|
|
233
|
+
req_delta = requirements_delta(proposal_text, plan_text, touchpoints)
|
|
234
|
+
if not touchpoints:
|
|
235
|
+
warnings.append("No merge spec snippets found; archive may have no durable spec delta")
|
|
236
|
+
if req_delta["counts"]["proposal"] and not req_delta["counts"]["plan"]:
|
|
237
|
+
warnings.append("proposal.md has requirement statements, but plan.md has no requirement-level mapping")
|
|
238
|
+
|
|
239
|
+
clause_groups = [
|
|
240
|
+
("proposal.md", proposal_text),
|
|
241
|
+
("plan.md", plan_text),
|
|
242
|
+
("tasks.md", tasks_text),
|
|
243
|
+
]
|
|
244
|
+
for filename in MERGE_SPEC_FILES:
|
|
245
|
+
clause_groups.append((filename, read_text(change_dir / filename)))
|
|
246
|
+
diff = subprocess.run(
|
|
247
|
+
["git", "diff", "--name-only", "HEAD"],
|
|
248
|
+
cwd=project_root,
|
|
249
|
+
text=True,
|
|
250
|
+
capture_output=True,
|
|
251
|
+
)
|
|
252
|
+
untracked = subprocess.run(
|
|
253
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
254
|
+
cwd=project_root,
|
|
255
|
+
text=True,
|
|
256
|
+
capture_output=True,
|
|
257
|
+
)
|
|
258
|
+
relatives = []
|
|
259
|
+
if diff.returncode == 0:
|
|
260
|
+
relatives.extend(line.strip() for line in diff.stdout.splitlines() if line.strip())
|
|
261
|
+
if untracked.returncode == 0:
|
|
262
|
+
relatives.extend(line.strip() for line in untracked.stdout.splitlines() if line.strip())
|
|
263
|
+
for relative in relatives:
|
|
264
|
+
if tdd_lib.is_test_file(relative):
|
|
265
|
+
clause_groups.append((relative, read_text(project_root / relative)))
|
|
266
|
+
issues.extend(tdd_lib.contradictory_requirements(clause_groups))
|
|
267
|
+
|
|
268
|
+
review = {
|
|
269
|
+
"ok": len(issues) == 0,
|
|
270
|
+
"gate": "J4b.spec_delta.review",
|
|
271
|
+
"verdict": "pass" if len(issues) == 0 else "fail",
|
|
272
|
+
"change_id": args.change_id,
|
|
273
|
+
"proposal": {
|
|
274
|
+
"exists": bool(proposal_text.strip()),
|
|
275
|
+
"headings": headings(proposal_text),
|
|
276
|
+
"bullets": bullets(proposal_text)[:12],
|
|
277
|
+
},
|
|
278
|
+
"plan": {
|
|
279
|
+
"exists": bool(plan_text.strip()),
|
|
280
|
+
"headings": headings(plan_text),
|
|
281
|
+
"verification_commands": verification_commands(plan_text),
|
|
282
|
+
},
|
|
283
|
+
"tasks": {
|
|
284
|
+
"exists": bool(tasks_text.strip()),
|
|
285
|
+
"total": len(task_items),
|
|
286
|
+
"done": sum(1 for item in task_items if item["done"]),
|
|
287
|
+
"items": task_items[:20],
|
|
288
|
+
},
|
|
289
|
+
"root_specs": root_spec_presence(project_root),
|
|
290
|
+
"spec_touchpoints": touchpoints,
|
|
291
|
+
"requirements_delta": req_delta,
|
|
292
|
+
"issues": issues,
|
|
293
|
+
"warnings": warnings,
|
|
294
|
+
"recorded_at": datetime.now(timezone.utc).isoformat(),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
change_dir.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
json_path = change_dir / "spec_delta_review.json"
|
|
299
|
+
md_path = change_dir / "spec_delta_review.md"
|
|
300
|
+
json_path.write_text(json.dumps(review, indent=2) + "\n", encoding="utf-8")
|
|
301
|
+
md_path.write_text(render_markdown(review), encoding="utf-8")
|
|
302
|
+
|
|
303
|
+
print(json.dumps({
|
|
304
|
+
"ok": review["ok"],
|
|
305
|
+
"verdict": review["verdict"],
|
|
306
|
+
"json_file": str(json_path),
|
|
307
|
+
"markdown_file": str(md_path),
|
|
308
|
+
"warnings": len(warnings),
|
|
309
|
+
"issues": len(issues),
|
|
310
|
+
"issue_texts": issues,
|
|
311
|
+
}))
|
|
312
|
+
sys.exit(0 if review["ok"] else 1)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
main()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
J4c.openspec.migration_map - Map an OpenSpec project to evalid surfaces.
|
|
4
|
+
|
|
5
|
+
This atom is intentionally read-only. It reports how `openspec/specs` and
|
|
6
|
+
`openspec/changes` would map into `specs/` and `specs/changes/` without copying
|
|
7
|
+
or rewriting user files.
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def slugify(value: str) -> str:
|
|
19
|
+
value = value.strip().lower()
|
|
20
|
+
value = re.sub(r"[^a-z0-9]+", "-", value)
|
|
21
|
+
return re.sub(r"-+", "-", value).strip("-") or "change"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def markdown_files(root: Path) -> list[Path]:
|
|
25
|
+
if not root.exists():
|
|
26
|
+
return []
|
|
27
|
+
return sorted(path for path in root.rglob("*.md") if path.is_file())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def map_change(change_dir: Path, openspec_root: Path) -> dict:
|
|
31
|
+
files = {path.name: path for path in markdown_files(change_dir)}
|
|
32
|
+
change_id = slugify(change_dir.name)
|
|
33
|
+
proposal = files.get("proposal.md")
|
|
34
|
+
design = files.get("design.md")
|
|
35
|
+
tasks = files.get("tasks.md")
|
|
36
|
+
spec_files = [
|
|
37
|
+
str(path.relative_to(openspec_root))
|
|
38
|
+
for path in markdown_files(change_dir)
|
|
39
|
+
if "spec" in path.name.lower() or "specs" in path.parts
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"openspec_change": str(change_dir.relative_to(openspec_root)),
|
|
44
|
+
"xflow_change_id": change_id,
|
|
45
|
+
"target_dir": f"specs/changes/{change_id}",
|
|
46
|
+
"proposal": str(proposal.relative_to(openspec_root)) if proposal else None,
|
|
47
|
+
"design": str(design.relative_to(openspec_root)) if design else None,
|
|
48
|
+
"tasks": str(tasks.relative_to(openspec_root)) if tasks else None,
|
|
49
|
+
"spec_files": spec_files,
|
|
50
|
+
"recommended_xflow_files": {
|
|
51
|
+
"proposal": f"specs/changes/{change_id}/proposal.md",
|
|
52
|
+
"plan": f"specs/changes/{change_id}/plan.md",
|
|
53
|
+
"tasks": f"specs/changes/{change_id}/tasks.md",
|
|
54
|
+
"spec_delta_review": f"specs/changes/{change_id}/spec_delta_review.json",
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> None:
|
|
60
|
+
parser = argparse.ArgumentParser()
|
|
61
|
+
parser.add_argument("--project-root", default=".")
|
|
62
|
+
parser.add_argument("--openspec-root", default="openspec")
|
|
63
|
+
args = parser.parse_args()
|
|
64
|
+
|
|
65
|
+
project_root = Path(args.project_root).resolve()
|
|
66
|
+
openspec_root = (project_root / args.openspec_root).resolve()
|
|
67
|
+
specs_root = openspec_root / "specs"
|
|
68
|
+
changes_root = openspec_root / "changes"
|
|
69
|
+
|
|
70
|
+
issues = []
|
|
71
|
+
if not openspec_root.exists():
|
|
72
|
+
issues.append(f"OpenSpec root not found: {openspec_root}")
|
|
73
|
+
|
|
74
|
+
root_specs = [
|
|
75
|
+
{
|
|
76
|
+
"openspec_file": str(path.relative_to(openspec_root)),
|
|
77
|
+
"recommended_target": f"specs/{path.name}",
|
|
78
|
+
}
|
|
79
|
+
for path in markdown_files(specs_root)
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
changes = [
|
|
83
|
+
map_change(path, openspec_root)
|
|
84
|
+
for path in sorted(changes_root.iterdir()) if changes_root.exists() and path.is_dir()
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
report = {
|
|
88
|
+
"ok": len(issues) == 0,
|
|
89
|
+
"gate": "J4c.openspec.migration_map",
|
|
90
|
+
"project_root": str(project_root),
|
|
91
|
+
"openspec_root": str(openspec_root),
|
|
92
|
+
"root_specs": root_specs,
|
|
93
|
+
"changes": changes,
|
|
94
|
+
"command_mapping": {
|
|
95
|
+
"/opsx:new <change>": "xflow init --project-root .; xflow:plan",
|
|
96
|
+
"/opsx:ff": "xflow:plan creates/refreshes plan.md",
|
|
97
|
+
"/opsx:apply": "xflow workflow run yolo|corps --project-root .",
|
|
98
|
+
"/opsx:archive": "xflow archive phase via A5.archive.commit_push_close",
|
|
99
|
+
"openspec validate": "xflow workflow validate yolo|corps plus J4a/J4b gates",
|
|
100
|
+
},
|
|
101
|
+
"issues": issues,
|
|
102
|
+
"recorded_at": datetime.now(timezone.utc).isoformat(),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
output_dir = project_root / ".as-xflow"
|
|
106
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
output_path = output_dir / "openspec-migration.json"
|
|
108
|
+
output_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
print(json.dumps({
|
|
111
|
+
"ok": report["ok"],
|
|
112
|
+
"report_file": str(output_path),
|
|
113
|
+
"root_specs": len(root_specs),
|
|
114
|
+
"changes": len(changes),
|
|
115
|
+
"issues": issues,
|
|
116
|
+
}))
|
|
117
|
+
sys.exit(0 if report["ok"] else 1)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
main()
|