its-magic 0.1.2-9 → 0.1.3-0
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/README.md +1602 -755
- package/bin/its-magic.js +121 -11
- package/bin/postinstall.js +93 -0
- package/installer.ps1 +759 -0
- package/installer.py +1036 -0
- package/installer.sh +649 -0
- package/package.json +18 -14
- package/scripts/check_intake_template_parity.py +312 -0
- package/scripts/doc_profile_lib.py +415 -0
- package/scripts/guard_installer_publish.py +88 -0
- package/scripts/intake_bug_routing_guard.py +67 -0
- package/scripts/intake_evidence_lib.py +802 -0
- package/scripts/intake_evidence_validate.py +73 -0
- package/scripts/materialize_codebase_map.py +184 -0
- package/scripts/remote_config_summary.py +243 -0
- package/template/.cursor/agents/curator.mdc +35 -0
- package/template/.cursor/agents/dev.mdc +29 -0
- package/template/.cursor/agents/po.mdc +141 -0
- package/template/.cursor/agents/qa.mdc +28 -0
- package/template/.cursor/agents/release.mdc +28 -0
- package/template/.cursor/agents/security.mdc +98 -0
- package/template/.cursor/agents/tech-lead.mdc +57 -0
- package/template/.cursor/commands/architecture.md +127 -0
- package/template/.cursor/commands/ask.md +55 -0
- package/template/.cursor/commands/auto.md +533 -0
- package/template/.cursor/commands/discovery.md +47 -0
- package/template/.cursor/commands/execute.md +343 -0
- package/template/.cursor/commands/intake.md +323 -0
- package/template/.cursor/commands/map-codebase.md +46 -0
- package/template/.cursor/commands/memory-audit.md +175 -0
- package/template/.cursor/commands/milestone-complete.md +51 -0
- package/template/.cursor/commands/milestone-start.md +59 -0
- package/template/.cursor/commands/pause.md +64 -0
- package/{.cursor/commands/gsd-phase-context.md → template/.cursor/commands/phase-context.md} +6 -2
- package/template/.cursor/commands/plan-verify.md +34 -0
- package/template/.cursor/commands/qa.md +226 -0
- package/template/.cursor/commands/quick.md +41 -0
- package/template/.cursor/commands/refresh-context.md +82 -0
- package/template/.cursor/commands/release.md +523 -0
- package/template/.cursor/commands/research.md +49 -0
- package/template/.cursor/commands/resume.md +67 -0
- package/template/.cursor/commands/security-review.md +81 -0
- package/template/.cursor/commands/sprint-plan.md +103 -0
- package/template/.cursor/commands/status-reconcile.md +95 -0
- package/template/.cursor/commands/verify-work.md +152 -0
- package/template/.cursor/dev-environment.json.example +22 -0
- package/{.cursor → template/.cursor}/hooks/README.md +3 -2
- package/{.cursor/hooks/gsd-hook.py → template/.cursor/hooks/hook.py} +15 -6
- package/template/.cursor/hooks.json +26 -0
- package/template/.cursor/remote.json +31 -0
- package/template/.cursor/rules/caveman.mdc +184 -0
- package/template/.cursor/rules/coding-standards.mdc +39 -0
- package/template/.cursor/rules/core.mdc +111 -0
- package/template/.cursor/rules/handoffs.mdc +27 -0
- package/template/.cursor/rules/quality.mdc +49 -0
- package/template/.cursor/scratchpad.local.example.md +323 -0
- package/template/.cursor/scratchpad.md +324 -0
- package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/SKILL.md +2 -2
- package/template/.cursorignore +6 -0
- package/template/.env.example +28 -0
- package/template/.github/workflows/ci.yml +194 -0
- package/{.github → template/.github}/workflows/deploy.yml +5 -2
- package/template/.its-magic-version +1 -0
- package/template/CHANGELOG.md +11 -0
- package/template/README.md +1602 -0
- package/template/decisions/DEC-0001.md +11 -0
- package/template/decisions/DEC-0002.md +11 -0
- package/template/docs/developer/README.md +44 -0
- package/template/docs/engineering/architecture.md +9 -0
- package/template/docs/engineering/artifact-ordering-policy.md +48 -0
- package/template/docs/engineering/artifact-ownership-policy.md +57 -0
- package/template/docs/engineering/auto-orchestration-reference.md +1211 -0
- package/{docs → template/docs}/engineering/codebase-map.md +0 -5
- package/template/docs/engineering/compatibility-report.md +20 -0
- package/template/docs/engineering/compatibility-signals.md +7 -0
- package/template/docs/engineering/component-scope-report.md +16 -0
- package/template/docs/engineering/component-scope.md +19 -0
- package/template/docs/engineering/context/installer-owned-paths.manifest +96 -0
- package/template/docs/engineering/context/readme-section-affinity.json +30 -0
- package/template/docs/engineering/decisions.md +16 -0
- package/template/docs/engineering/legacy-drift-audit.md +21 -0
- package/template/docs/engineering/manifests/components/api-gateway.manifest.yaml +12 -0
- package/template/docs/engineering/manifests/registry.manifest.yaml +20 -0
- package/template/docs/engineering/manifests/repo.manifest.yaml +11 -0
- package/template/docs/engineering/phase-context.md +16 -0
- package/template/docs/engineering/release-targets.json +123 -0
- package/template/docs/engineering/research.md +29 -0
- package/template/docs/engineering/runbook.md +2391 -0
- package/template/docs/engineering/runtime-connectivity.md +81 -0
- package/template/docs/engineering/security-review.md +33 -0
- package/template/docs/engineering/spec-pack/README.md +20 -0
- package/template/docs/engineering/state-archive/README.md +15 -0
- package/template/docs/engineering/state.md +20 -0
- package/template/docs/engineering/status-normalization-report.md +19 -0
- package/template/docs/engineering/token-cost-parity-manifest.md +16 -0
- package/template/docs/engineering/us-0084-remote-e2e.md +44 -0
- package/template/docs/product/acceptance.md +1 -0
- package/template/docs/product/backlog.md +5 -0
- package/template/docs/product/vision.md +11 -0
- package/template/docs/user-guides/README.md +40 -0
- package/{handoffs → template/handoffs}/dev_to_qa.md +0 -3
- package/{handoffs → template/handoffs}/po_to_tl.md +0 -3
- package/{handoffs → template/handoffs}/qa_to_dev.md +1 -2
- package/template/handoffs/release_notes.md +51 -0
- package/template/handoffs/release_queue.md +47 -0
- package/template/handoffs/release_to_dev.md +31 -0
- package/template/handoffs/releases/Sxxxx-release-notes.md +62 -0
- package/template/handoffs/releases/vX.Y.Z-release-notes.md.example +21 -0
- package/template/handoffs/resume_brief.md +14 -0
- package/{handoffs → template/handoffs}/tl_to_dev.md +0 -2
- package/template/handoffs/token_cost_runs/README.md +26 -0
- package/template/its_magic/.its-magic-version +1 -0
- package/template/its_magic/README.md +9 -0
- package/template/scripts/auto_outer_driver.py +521 -0
- package/template/scripts/caveman_compress_input.py +903 -0
- package/template/scripts/check_downstream_ci_guard.py +67 -0
- package/template/scripts/check_intake_template_parity.py +312 -0
- package/template/scripts/check_token_cost_parity.py +69 -0
- package/template/scripts/dev_environment_lib.py +601 -0
- package/template/scripts/doc_profile_lib.py +415 -0
- package/template/scripts/downstream_ci_guard_lib.py +222 -0
- package/template/scripts/enforce-triad-hot-surface.py +753 -0
- package/template/scripts/guard_installer_publish.py +88 -0
- package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
- package/template/scripts/intake_bug_routing_guard.py +67 -0
- package/template/scripts/intake_evidence_lib.py +802 -0
- package/template/scripts/intake_evidence_validate.py +73 -0
- package/template/scripts/materialize_codebase_map.py +184 -0
- package/template/scripts/pack_json_validate.py +130 -0
- package/template/scripts/project_readme_coverage_lib.py +417 -0
- package/template/scripts/readme_feature_coverage_lib.py +608 -0
- package/{scripts → template/scripts}/release-all.sh +27 -4
- package/template/scripts/release_changelog_backfill.py +153 -0
- package/template/scripts/release_changelog_lib.py +544 -0
- package/template/scripts/release_changelog_validate.py +134 -0
- package/template/scripts/remote_config_summary.py +243 -0
- package/template/scripts/sync_push_gates.py +198 -0
- package/template/scripts/token_cost_compare.py +40 -0
- package/template/scripts/token_cost_lib.py +108 -0
- package/template/scripts/uat_probe_lib.py +868 -0
- package/template/scripts/validate-and-push.ps1 +280 -0
- package/template/scripts/validate-and-push.sh +243 -0
- package/template/scripts/validate_doc_profile.py +103 -0
- package/template/scripts/validate_project_readme_coverage.py +151 -0
- package/template/scripts/validate_readme_feature_coverage.py +140 -0
- package/template/sprints/S0001/progress.md +1 -0
- package/template/sprints/S0001/qa-findings.md +9 -0
- package/template/sprints/S0001/release-findings.md +24 -0
- package/template/sprints/S0001/sprint.md +9 -0
- package/template/sprints/S0001/summary.md +7 -0
- package/template/sprints/S0001/tasks.md +1 -0
- package/template/sprints/S0001/uat.json +6 -0
- package/template/sprints/S0001/uat.md +5 -0
- package/template/sprints/quick/Q0001/summary.md +1 -0
- package/{sprints → template/sprints}/quick/Q0001/task.json +0 -1
- package/.cursor/agents/curator.mdc +0 -21
- package/.cursor/agents/dev.mdc +0 -20
- package/.cursor/agents/po.mdc +0 -19
- package/.cursor/agents/qa.mdc +0 -19
- package/.cursor/agents/release.mdc +0 -19
- package/.cursor/agents/tech-lead.mdc +0 -21
- package/.cursor/commands/gsd-architecture.md +0 -29
- package/.cursor/commands/gsd-auto.md +0 -27
- package/.cursor/commands/gsd-discovery.md +0 -27
- package/.cursor/commands/gsd-execute.md +0 -32
- package/.cursor/commands/gsd-intake.md +0 -28
- package/.cursor/commands/gsd-map-codebase.md +0 -25
- package/.cursor/commands/gsd-milestone-complete.md +0 -24
- package/.cursor/commands/gsd-milestone-start.md +0 -26
- package/.cursor/commands/gsd-pause.md +0 -25
- package/.cursor/commands/gsd-plan-verify.md +0 -26
- package/.cursor/commands/gsd-qa.md +0 -28
- package/.cursor/commands/gsd-quick.md +0 -24
- package/.cursor/commands/gsd-refresh-context.md +0 -26
- package/.cursor/commands/gsd-release.md +0 -29
- package/.cursor/commands/gsd-research.md +0 -28
- package/.cursor/commands/gsd-resume.md +0 -26
- package/.cursor/commands/gsd-sprint-plan.md +0 -30
- package/.cursor/commands/gsd-verify-work.md +0 -25
- package/.cursor/hooks.json +0 -26
- package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +0 -57
- package/.cursor/remote.json +0 -18
- package/.cursor/rules/gsd-core.mdc +0 -18
- package/.cursor/rules/gsd-handoffs.mdc +0 -10
- package/.cursor/rules/gsd-quality.mdc +0 -15
- package/.cursor/scratchpad.md +0 -34
- package/.github/workflows/ci.yml +0 -47
- package/decisions/DEC-0001.md +0 -21
- package/decisions/DEC-0002.md +0 -21
- package/docs/engineering/architecture.md +0 -354
- package/docs/engineering/decisions.md +0 -6
- package/docs/engineering/research.md +0 -11
- package/docs/engineering/runbook.md +0 -32
- package/docs/engineering/state.md +0 -33
- package/docs/product/acceptance.md +0 -6
- package/docs/product/backlog.md +0 -7
- package/docs/product/vision.md +0 -46
- package/gsd-installer.ps1 +0 -189
- package/gsd-installer.py +0 -195
- package/gsd-installer.sh +0 -201
- package/handoffs/release_notes.md +0 -14
- package/handoffs/resume_brief.md +0 -8
- package/milestones/M0001/milestone.json +0 -7
- package/milestones/M0001/phases.json +0 -9
- package/milestones/M0001/progress.md +0 -3
- package/milestones/M0001/summary.md +0 -3
- package/scripts/generate-release-notes.ps1 +0 -74
- package/scripts/generate-release-notes.sh +0 -63
- package/scripts/release-all.ps1 +0 -423
- package/sprints/S0001/progress.md +0 -4
- package/sprints/S0001/qa-findings.md +0 -113
- package/sprints/S0001/sprint.md +0 -70
- package/sprints/S0001/summary.md +0 -46
- package/sprints/S0001/tasks.md +0 -35
- package/sprints/S0001/uat.json +0 -8
- package/sprints/S0001/uat.md +0 -8
- package/sprints/quick/Q0001/summary.md +0 -3
- /package/{.cursor/rules/gsd-escalation.mdc → template/.cursor/rules/escalation.mdc} +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/milestone.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/phase-context.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/plan-verify.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/uat.json +0 -0
- /package/{docs → template/docs}/engineering/context/phase-template.json +0 -0
- /package/{docs → template/docs}/engineering/dependencies.json +0 -0
- /package/{sprints → template/sprints}/S0001/plan-verify.json +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validate release changelog / version-doc consistency (US-0100 / DEC-0085).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
_REPO_ROOT = os.path.normpath(os.path.join(_SCRIPT_DIR, ".."))
|
|
15
|
+
if _REPO_ROOT not in sys.path:
|
|
16
|
+
sys.path.insert(0, _REPO_ROOT)
|
|
17
|
+
if _SCRIPT_DIR not in sys.path:
|
|
18
|
+
sys.path.insert(0, _SCRIPT_DIR)
|
|
19
|
+
|
|
20
|
+
import release_changelog_lib as rcl # noqa: E402
|
|
21
|
+
|
|
22
|
+
REMEDIATION = {
|
|
23
|
+
rcl.RELEASE_CHANGELOG_VERSION_MISSING: "Set explicit semver on queue row or pass valid --semver.",
|
|
24
|
+
rcl.RELEASE_CHANGELOG_DUPLICATE_VERSION: "Resolve fingerprint mismatch; do not reuse semver for different work items.",
|
|
25
|
+
rcl.RELEASE_CHANGELOG_WORK_ITEM_GAP: "Run derive/build_version_doc or complete sprint notes + backlog refs.",
|
|
26
|
+
rcl.RELEASE_CHANGELOG_ORDER_INVALID: "Reorder CHANGELOG.md semver sections newest-first.",
|
|
27
|
+
rcl.RELEASE_CHANGELOG_UNRELEASED_MISSING: "Add mandatory ## [Unreleased] header at top of CHANGELOG.md.",
|
|
28
|
+
rcl.RELEASE_CHANGELOG_QUEUE_DRIFT: "Re-run bind_queue_release_version or reconcile queue release_version.",
|
|
29
|
+
rcl.RELEASE_CHANGELOG_VERSION_DOC_MISSING: "Run build_version_doc or release_changelog_backfill before gh -F attach.",
|
|
30
|
+
rcl.RELEASE_CHANGELOG_SPRINT_ORPHAN: "Include released sprint in semver doc or [Unreleased] via backfill.",
|
|
31
|
+
rcl.RELEASE_CHANGELOG_BACKFILL_AMBIGUOUS: "Fix manifest collision; one semver intent per coalesce group.",
|
|
32
|
+
rcl.RELEASE_CHANGELOG_IDEMPOTENCY_VIOLATION: "Remove duplicate work-item bullets for same version; re-run derive.",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _emit(code: str, detail: str = "") -> None:
|
|
37
|
+
msg = code if not detail else f"{code}: {detail}"
|
|
38
|
+
print(msg, file=sys.stderr)
|
|
39
|
+
rem = REMEDIATION.get(code, "See docs/engineering/runbook.md § Version-scoped release docs.")
|
|
40
|
+
print(f"Remediation: {rem}", file=sys.stderr)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate(repo_root: str, enforce: bool) -> int:
|
|
44
|
+
errors: list[str] = []
|
|
45
|
+
cl_path = rcl.changelog_path(repo_root)
|
|
46
|
+
if not os.path.isfile(cl_path):
|
|
47
|
+
errors.append(rcl.RELEASE_CHANGELOG_UNRELEASED_MISSING)
|
|
48
|
+
else:
|
|
49
|
+
text = rcl.read_utf8(cl_path)
|
|
50
|
+
if "## [Unreleased]" not in text:
|
|
51
|
+
errors.append(rcl.RELEASE_CHANGELOG_UNRELEASED_MISSING)
|
|
52
|
+
# semver order newest-first (skip Unreleased)
|
|
53
|
+
versions: list[str] = []
|
|
54
|
+
for m in re.finditer(r"^##\s+\[(.+?)\]", text, re.MULTILINE):
|
|
55
|
+
v = m.group(1)
|
|
56
|
+
if v.lower() == "unreleased":
|
|
57
|
+
continue
|
|
58
|
+
try:
|
|
59
|
+
versions.append(rcl.normalize_semver(v))
|
|
60
|
+
except rcl.ReleaseChangelogError:
|
|
61
|
+
errors.append(rcl.RELEASE_CHANGELOG_VERSION_MISSING)
|
|
62
|
+
# Simple order check: compare as strings after normalization (pre-release aware minimal)
|
|
63
|
+
if len(versions) > 1:
|
|
64
|
+
for i in range(len(versions) - 1):
|
|
65
|
+
if versions[i] < versions[i + 1]:
|
|
66
|
+
errors.append(rcl.RELEASE_CHANGELOG_ORDER_INVALID)
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
rows = rcl.parse_queue_rows(repo_root)
|
|
70
|
+
for row in rows:
|
|
71
|
+
if row.status != "released":
|
|
72
|
+
continue
|
|
73
|
+
rv = row.release_version.strip()
|
|
74
|
+
if not rv:
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
norm = rcl.normalize_semver(rv)
|
|
78
|
+
except rcl.ReleaseChangelogError:
|
|
79
|
+
errors.append(rcl.RELEASE_CHANGELOG_VERSION_MISSING)
|
|
80
|
+
continue
|
|
81
|
+
vdoc = rcl.version_doc_path(repo_root, norm)
|
|
82
|
+
if not os.path.isfile(vdoc):
|
|
83
|
+
errors.append(rcl.RELEASE_CHANGELOG_VERSION_DOC_MISSING)
|
|
84
|
+
section = rcl.extract_changelog_section(norm, repo_root)
|
|
85
|
+
if section is None:
|
|
86
|
+
errors.append(rcl.RELEASE_CHANGELOG_VERSION_MISSING)
|
|
87
|
+
# work item gap: story_refs should appear in version doc
|
|
88
|
+
if os.path.isfile(vdoc):
|
|
89
|
+
doc_text = rcl.read_utf8(vdoc)
|
|
90
|
+
for token in re.split(r"[,;\s]+", row.story_refs):
|
|
91
|
+
token = token.strip()
|
|
92
|
+
if token and token not in doc_text:
|
|
93
|
+
errors.append(rcl.RELEASE_CHANGELOG_WORK_ITEM_GAP)
|
|
94
|
+
|
|
95
|
+
# queue drift: coalesce groups vs bound semver
|
|
96
|
+
groups = rcl.coalesce_sprints_by_semver(rows, repo_root)
|
|
97
|
+
for semver, sids in groups.items():
|
|
98
|
+
for sid in sids:
|
|
99
|
+
row = next((r for r in rows if r.sprint_id == sid), None)
|
|
100
|
+
if row and row.release_version.strip():
|
|
101
|
+
try:
|
|
102
|
+
if rcl.normalize_semver(row.release_version) != semver:
|
|
103
|
+
errors.append(rcl.RELEASE_CHANGELOG_QUEUE_DRIFT)
|
|
104
|
+
except rcl.ReleaseChangelogError:
|
|
105
|
+
errors.append(rcl.RELEASE_CHANGELOG_QUEUE_DRIFT)
|
|
106
|
+
|
|
107
|
+
seen_errors = sorted(set(errors))
|
|
108
|
+
for code in seen_errors:
|
|
109
|
+
_emit(code)
|
|
110
|
+
if seen_errors and enforce:
|
|
111
|
+
return 1
|
|
112
|
+
if not seen_errors:
|
|
113
|
+
print("[RELEASE_CHANGELOG_VALIDATE_OK]")
|
|
114
|
+
return 0
|
|
115
|
+
if not enforce:
|
|
116
|
+
print("[RELEASE_CHANGELOG_VALIDATE_WARN]")
|
|
117
|
+
return 0
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main() -> int:
|
|
122
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
123
|
+
parser.add_argument("--repo", default=_REPO_ROOT, help="Repository root")
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--enforce",
|
|
126
|
+
action="store_true",
|
|
127
|
+
help="Exit non-zero on any fail code (release gate / release-all.sh).",
|
|
128
|
+
)
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
return validate(os.path.abspath(args.repo), args.enforce)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Summarize .cursor/remote.json for operators (US-0084 / US-0064 alignment).
|
|
4
|
+
|
|
5
|
+
Stdout: non-secret labels and env-reference names only (no key material).
|
|
6
|
+
Stderr: errors and skip reasons.
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
0 OK or REMOTE_EXECUTION=0 skip (DEC-0070)
|
|
10
|
+
1 usage / CLI error
|
|
11
|
+
2 config missing or unreadable
|
|
12
|
+
3 invalid JSON
|
|
13
|
+
4 schema / contract mismatch vs runbook remote.json contract
|
|
14
|
+
5 reserved (unused; DEC-0070 maps REMOTE_EXECUTION=0 to 0)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
ENV_VAR_NAME = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
28
|
+
ALLOWED_TYPES = frozenset({"docker", "ssh", "vm"})
|
|
29
|
+
AUTH_MODES = frozenset({"none", "env"})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _truthy_remote_execution(raw: str | None) -> bool:
|
|
33
|
+
if raw is None:
|
|
34
|
+
return False
|
|
35
|
+
return raw.strip() in {"1", "true", "TRUE", "yes", "YES", "on", "ON"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
|
|
39
|
+
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], add_help=True)
|
|
40
|
+
p.add_argument(
|
|
41
|
+
"--config",
|
|
42
|
+
default=None,
|
|
43
|
+
help="Path to remote JSON (default: REMOTE_CONFIG env or .cursor/remote.json)",
|
|
44
|
+
)
|
|
45
|
+
return p.parse_known_args(argv)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _config_path(explicit: str | None) -> Path:
|
|
49
|
+
if explicit:
|
|
50
|
+
return Path(explicit).expanduser()
|
|
51
|
+
env = os.environ.get("REMOTE_CONFIG")
|
|
52
|
+
if env:
|
|
53
|
+
return Path(env).expanduser()
|
|
54
|
+
return Path(".cursor/remote.json")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate_env_ref(name: str, value: Any, prefix: str) -> str | None:
|
|
58
|
+
if not isinstance(value, str):
|
|
59
|
+
return f"{prefix}: {name} must be a string env-reference name"
|
|
60
|
+
if not ENV_VAR_NAME.match(value):
|
|
61
|
+
return f"{prefix}: {name} must look like an env var name (A-Z_0-9)"
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def validate_and_summarize(path: Path, data: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
66
|
+
"""Return (stdout_lines, error_messages)."""
|
|
67
|
+
errs: list[str] = []
|
|
68
|
+
out: list[str] = []
|
|
69
|
+
out.append(f"remote_config={path.as_posix()}")
|
|
70
|
+
|
|
71
|
+
if not isinstance(data.get("version"), int):
|
|
72
|
+
errs.append("[REMOTE_CONFIG_ERROR] root.version: expected integer")
|
|
73
|
+
if not isinstance(data.get("defaultTarget"), str) or not data["defaultTarget"]:
|
|
74
|
+
errs.append("[REMOTE_CONFIG_ERROR] root.defaultTarget: expected non-empty string")
|
|
75
|
+
targets = data.get("targets")
|
|
76
|
+
if not isinstance(targets, list) or not targets:
|
|
77
|
+
errs.append("[REMOTE_CONFIG_ERROR] root.targets: expected non-empty array")
|
|
78
|
+
|
|
79
|
+
if errs:
|
|
80
|
+
return out, errs
|
|
81
|
+
|
|
82
|
+
by_id: dict[str, dict[str, Any]] = {}
|
|
83
|
+
for i, t in enumerate(targets):
|
|
84
|
+
prefix = f"targets[{i}]"
|
|
85
|
+
if not isinstance(t, dict):
|
|
86
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}: expected object")
|
|
87
|
+
continue
|
|
88
|
+
tid = t.get("id")
|
|
89
|
+
if not isinstance(tid, str) or not tid:
|
|
90
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.id: expected non-empty string")
|
|
91
|
+
ttype = t.get("type")
|
|
92
|
+
if ttype not in ALLOWED_TYPES:
|
|
93
|
+
errs.append(
|
|
94
|
+
f"[REMOTE_CONFIG_ERROR] {prefix}.type: expected one of {sorted(ALLOWED_TYPES)}"
|
|
95
|
+
)
|
|
96
|
+
if not isinstance(t.get("enabled"), bool):
|
|
97
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.enabled: expected boolean")
|
|
98
|
+
if not isinstance(t.get("host"), str) or not t["host"]:
|
|
99
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.host: expected non-empty string")
|
|
100
|
+
port = t.get("port")
|
|
101
|
+
if not isinstance(port, int) or not (1 <= port <= 65535):
|
|
102
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.port: expected integer 1..65535")
|
|
103
|
+
if not isinstance(t.get("workspaceRoot"), str) or not t["workspaceRoot"]:
|
|
104
|
+
errs.append(
|
|
105
|
+
f"[REMOTE_CONFIG_ERROR] {prefix}.workspaceRoot: expected non-empty string"
|
|
106
|
+
)
|
|
107
|
+
auth = t.get("auth")
|
|
108
|
+
if auth is not None:
|
|
109
|
+
if not isinstance(auth, dict):
|
|
110
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.auth: expected object")
|
|
111
|
+
else:
|
|
112
|
+
mode = auth.get("mode")
|
|
113
|
+
if mode not in AUTH_MODES:
|
|
114
|
+
errs.append(
|
|
115
|
+
f"[REMOTE_CONFIG_ERROR] {prefix}.auth.mode: expected one of {sorted(AUTH_MODES)}"
|
|
116
|
+
)
|
|
117
|
+
elif mode == "env":
|
|
118
|
+
env_keys = (
|
|
119
|
+
"tokenEnv",
|
|
120
|
+
"passwordEnv",
|
|
121
|
+
"privateKeyPathEnv",
|
|
122
|
+
"usernameEnv",
|
|
123
|
+
)
|
|
124
|
+
any_ref = False
|
|
125
|
+
for k in env_keys:
|
|
126
|
+
if k not in auth:
|
|
127
|
+
continue
|
|
128
|
+
any_ref = True
|
|
129
|
+
err = _validate_env_ref(k, auth[k], f"{prefix}.auth")
|
|
130
|
+
if err:
|
|
131
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {err}")
|
|
132
|
+
if not any_ref:
|
|
133
|
+
errs.append(
|
|
134
|
+
f"[REMOTE_CONFIG_ERROR] {prefix}.auth: mode=env requires at least one *Env field"
|
|
135
|
+
)
|
|
136
|
+
if isinstance(tid, str) and tid:
|
|
137
|
+
by_id[tid] = t
|
|
138
|
+
|
|
139
|
+
default = data["defaultTarget"]
|
|
140
|
+
if default not in by_id:
|
|
141
|
+
errs.append(
|
|
142
|
+
f"[REMOTE_CONFIG_ERROR] defaultTarget={default!r} not found in targets[].id"
|
|
143
|
+
)
|
|
144
|
+
elif not by_id[default].get("enabled"):
|
|
145
|
+
errs.append(
|
|
146
|
+
f"[REMOTE_CONFIG_ERROR] defaultTarget={default!r} must reference an enabled target"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if errs:
|
|
150
|
+
return out, errs
|
|
151
|
+
|
|
152
|
+
out.append(f"defaultTarget={data['defaultTarget']}")
|
|
153
|
+
for t in targets:
|
|
154
|
+
assert isinstance(t, dict)
|
|
155
|
+
tid = t["id"]
|
|
156
|
+
parts = [
|
|
157
|
+
f"id={tid}",
|
|
158
|
+
f"type={t['type']}",
|
|
159
|
+
f"enabled={t['enabled']!s}",
|
|
160
|
+
f"host={t['host']}",
|
|
161
|
+
f"port={t['port']}",
|
|
162
|
+
f"workspaceRoot={t['workspaceRoot']}",
|
|
163
|
+
]
|
|
164
|
+
auth = t.get("auth")
|
|
165
|
+
if isinstance(auth, dict):
|
|
166
|
+
parts.append(f"auth.mode={auth.get('mode')}")
|
|
167
|
+
for k in (
|
|
168
|
+
"tokenEnv",
|
|
169
|
+
"passwordEnv",
|
|
170
|
+
"privateKeyPathEnv",
|
|
171
|
+
"usernameEnv",
|
|
172
|
+
):
|
|
173
|
+
if k in auth and isinstance(auth[k], str):
|
|
174
|
+
parts.append(f"auth.{k}={auth[k]}")
|
|
175
|
+
out.append("target:" + " ".join(parts))
|
|
176
|
+
|
|
177
|
+
return out, []
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main(argv: list[str] | None = None) -> int:
|
|
181
|
+
args, rest = _parse_args(argv or sys.argv[1:])
|
|
182
|
+
if rest:
|
|
183
|
+
print(
|
|
184
|
+
f"[REMOTE_CONFIG_SUMMARY] unknown arguments: {' '.join(rest)}",
|
|
185
|
+
file=sys.stderr,
|
|
186
|
+
)
|
|
187
|
+
return 1
|
|
188
|
+
if not _truthy_remote_execution(os.environ.get("REMOTE_EXECUTION")):
|
|
189
|
+
print(
|
|
190
|
+
"[REMOTE_CONFIG_SUMMARY] REMOTE_EXECUTION!=1: skip validation/summary "
|
|
191
|
+
"(zero overhead; DEC-0070 / US-0084).",
|
|
192
|
+
file=sys.stderr,
|
|
193
|
+
)
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
path = _config_path(args.config)
|
|
197
|
+
if not path.is_file():
|
|
198
|
+
print(
|
|
199
|
+
f"[REMOTE_CONFIG_ERROR] {path}: file missing or unreadable. "
|
|
200
|
+
"Fix: create config or set REMOTE_EXECUTION=0.",
|
|
201
|
+
file=sys.stderr,
|
|
202
|
+
)
|
|
203
|
+
return 2
|
|
204
|
+
try:
|
|
205
|
+
raw = path.read_text(encoding="utf-8")
|
|
206
|
+
except OSError as e:
|
|
207
|
+
print(
|
|
208
|
+
f"[REMOTE_CONFIG_ERROR] {path}: read failed ({e}). Fix: permissions/path.",
|
|
209
|
+
file=sys.stderr,
|
|
210
|
+
)
|
|
211
|
+
return 2
|
|
212
|
+
try:
|
|
213
|
+
data = json.loads(raw)
|
|
214
|
+
except json.JSONDecodeError as e:
|
|
215
|
+
print(
|
|
216
|
+
f"[REMOTE_CONFIG_ERROR] {path}: invalid JSON ({e}). Fix: syntax.",
|
|
217
|
+
file=sys.stderr,
|
|
218
|
+
)
|
|
219
|
+
return 3
|
|
220
|
+
if not isinstance(data, dict):
|
|
221
|
+
print(
|
|
222
|
+
f"[REMOTE_CONFIG_ERROR] {path}: expected JSON object at root.",
|
|
223
|
+
file=sys.stderr,
|
|
224
|
+
)
|
|
225
|
+
return 4
|
|
226
|
+
|
|
227
|
+
lines, errs = validate_and_summarize(path, data)
|
|
228
|
+
if errs:
|
|
229
|
+
for line in errs:
|
|
230
|
+
print(line, file=sys.stderr)
|
|
231
|
+
print(
|
|
232
|
+
f"[REMOTE_CONFIG_ERROR] {path}: contract check failed. "
|
|
233
|
+
"Fix: align with docs/engineering/runbook.md remote contract (US-0084).",
|
|
234
|
+
file=sys.stderr,
|
|
235
|
+
)
|
|
236
|
+
return 4
|
|
237
|
+
for line in lines:
|
|
238
|
+
print(line)
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared sync / push eligibility evaluation for validate-and-push (merged scratchpad).
|
|
4
|
+
|
|
5
|
+
Imports installer.merge_scratchpad_layers / validate_merged_scratchpad only — no
|
|
6
|
+
duplicate DEC-0055 precedence logic. Emits machine-readable JSON on stdout; reason
|
|
7
|
+
codes only (no planning-shaped tokens) on stderr for errors.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import fnmatch
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Tuple
|
|
20
|
+
|
|
21
|
+
# Repo root (parent of scripts/)
|
|
22
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
23
|
+
if str(_ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(_ROOT))
|
|
25
|
+
|
|
26
|
+
import installer # noqa: E402
|
|
27
|
+
|
|
28
|
+
_VALID_MODES = frozenset(
|
|
29
|
+
{"disabled", "manual", "by_phase", "by_milestone", "custom_phase_list"}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _parse_csv(val: str) -> List[str]:
|
|
33
|
+
if not val or not val.strip():
|
|
34
|
+
return []
|
|
35
|
+
return [p.strip() for p in val.split(",") if p.strip()]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _merge_and_validate(root: str) -> Tuple[dict, List[str]]:
|
|
39
|
+
ok, diags = installer.validate_merged_scratchpad(root)
|
|
40
|
+
if not ok:
|
|
41
|
+
return {}, diags
|
|
42
|
+
merged, _paths = installer.merge_scratchpad_layers(root)
|
|
43
|
+
return merged, []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def eval_policy(root: str, _branch: str) -> Tuple[bool, str, List[str]]:
|
|
47
|
+
"""
|
|
48
|
+
Pre-test gate: merged scratchpad validation + SYNC_POLICY_MODE + ALLOW_AUTO_PUSH
|
|
49
|
+
+ custom_phase_list boundary (SYNC_PHASE_BOUNDARY env).
|
|
50
|
+
Returns (ok, reason_code, scratchpad_diagnostics).
|
|
51
|
+
"""
|
|
52
|
+
merged, diags = _merge_and_validate(root)
|
|
53
|
+
if diags:
|
|
54
|
+
return False, "SCRATCHPAD_MERGE_ERROR", diags
|
|
55
|
+
|
|
56
|
+
mode = (merged.get("SYNC_POLICY_MODE") or "").strip().lower()
|
|
57
|
+
if not mode or mode not in _VALID_MODES:
|
|
58
|
+
mode = "manual"
|
|
59
|
+
if mode == "disabled":
|
|
60
|
+
return False, "SYNC_DISABLED", []
|
|
61
|
+
if mode == "manual":
|
|
62
|
+
return False, "MANUAL_MODE_NO_AUTO", []
|
|
63
|
+
|
|
64
|
+
allow = (merged.get("ALLOW_AUTO_PUSH") or "").strip()
|
|
65
|
+
if allow != "1":
|
|
66
|
+
return False, "AUTO_PUSH_NOT_ENABLED", []
|
|
67
|
+
|
|
68
|
+
if mode == "custom_phase_list":
|
|
69
|
+
phases = [p.strip().lower() for p in _parse_csv(merged.get("SYNC_CUSTOM_PHASES", ""))]
|
|
70
|
+
boundary = (os.environ.get("SYNC_PHASE_BOUNDARY") or "").strip().lower()
|
|
71
|
+
if not boundary:
|
|
72
|
+
return False, "SYNC_TRIGGER_NOT_ELIGIBLE", []
|
|
73
|
+
if not phases or boundary not in phases:
|
|
74
|
+
return False, "SYNC_TRIGGER_NOT_ELIGIBLE", []
|
|
75
|
+
|
|
76
|
+
# by_phase / by_milestone: invocation counts as eligible boundary (DEC-0058 §4).
|
|
77
|
+
return True, "", []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _branch_allowed(branch: str, allowlist_csv: str) -> bool:
|
|
81
|
+
patterns = _parse_csv(allowlist_csv)
|
|
82
|
+
if not patterns:
|
|
83
|
+
return False
|
|
84
|
+
for pat in patterns:
|
|
85
|
+
if fnmatch.fnmatchcase(branch, pat):
|
|
86
|
+
return True
|
|
87
|
+
if branch == pat:
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _scan_qa_findings_blocking(repo: Path) -> bool:
|
|
93
|
+
"""True if blocking markers found (DEC-0058 §6)."""
|
|
94
|
+
sprints = repo / "sprints"
|
|
95
|
+
if not sprints.is_dir():
|
|
96
|
+
return False
|
|
97
|
+
for p in sorted(sprints.iterdir()):
|
|
98
|
+
if not p.is_dir() or len(p.name) != 5 or not p.name.startswith("S"):
|
|
99
|
+
continue
|
|
100
|
+
if not p.name[1:].isdigit():
|
|
101
|
+
continue
|
|
102
|
+
qf = p / "qa-findings.md"
|
|
103
|
+
if not qf.is_file():
|
|
104
|
+
continue
|
|
105
|
+
text = qf.read_text(encoding="utf-8", errors="replace")
|
|
106
|
+
if "BLOCKING_QA_FINDINGS" in text:
|
|
107
|
+
return True
|
|
108
|
+
in_blocking = False
|
|
109
|
+
for line in text.splitlines():
|
|
110
|
+
if re.match(r"^##\s+blocking\s*$", line, re.IGNORECASE):
|
|
111
|
+
in_blocking = True
|
|
112
|
+
continue
|
|
113
|
+
if re.match(r"^##\s+\S", line):
|
|
114
|
+
in_blocking = False
|
|
115
|
+
if in_blocking and re.search(r"^-\s+\[\s+\]", line):
|
|
116
|
+
return True
|
|
117
|
+
if re.search(r"^-\s+\[\s+\]", line) and re.search(
|
|
118
|
+
r"BLOCKING|FAIL", line, re.IGNORECASE
|
|
119
|
+
):
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _count_sprint_qa_findings(repo: Path) -> int:
|
|
125
|
+
n = 0
|
|
126
|
+
sprints = repo / "sprints"
|
|
127
|
+
if not sprints.is_dir():
|
|
128
|
+
return 0
|
|
129
|
+
for p in sprints.iterdir():
|
|
130
|
+
if not p.is_dir() or len(p.name) != 5 or not p.name.startswith("S"):
|
|
131
|
+
continue
|
|
132
|
+
if not p.name[1:].isdigit():
|
|
133
|
+
continue
|
|
134
|
+
if (p / "qa-findings.md").is_file():
|
|
135
|
+
n += 1
|
|
136
|
+
return n
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def eval_post_test(root: str, branch: str) -> Tuple[bool, str, List[str]]:
|
|
140
|
+
"""
|
|
141
|
+
After tests: branch allowlist + PRE_QA + BLOCKING_QA_FINDINGS scan.
|
|
142
|
+
"""
|
|
143
|
+
merged, diags = _merge_and_validate(root)
|
|
144
|
+
if diags:
|
|
145
|
+
return False, "SCRATCHPAD_MERGE_ERROR", diags
|
|
146
|
+
|
|
147
|
+
allow_csv = merged.get("AUTO_PUSH_BRANCH_ALLOWLIST") or ""
|
|
148
|
+
if not _branch_allowed(branch, allow_csv):
|
|
149
|
+
return False, "BRANCH_NOT_ALLOWLISTED", []
|
|
150
|
+
|
|
151
|
+
b = branch.strip()
|
|
152
|
+
if b not in ("main", "master"):
|
|
153
|
+
if _count_sprint_qa_findings(Path(root)) == 0:
|
|
154
|
+
return False, "PRE_QA_AUTOPUSH_FORBIDDEN", []
|
|
155
|
+
|
|
156
|
+
if _scan_qa_findings_blocking(Path(root)):
|
|
157
|
+
return False, "BLOCKING_QA_FINDINGS", []
|
|
158
|
+
|
|
159
|
+
return True, "", []
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main() -> int:
|
|
163
|
+
parser = argparse.ArgumentParser(description="Sync/push gate evaluation for validate-and-push.")
|
|
164
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
165
|
+
|
|
166
|
+
p_pol = sub.add_parser("policy", help="Pre-test merged scratchpad + sync policy gate.")
|
|
167
|
+
p_pol.add_argument("--root", required=True)
|
|
168
|
+
p_pol.add_argument("--branch", required=True)
|
|
169
|
+
|
|
170
|
+
p_post = sub.add_parser("post", help="Post-test allowlist + QA scan gate.")
|
|
171
|
+
p_post.add_argument("--root", required=True)
|
|
172
|
+
p_post.add_argument("--branch", required=True)
|
|
173
|
+
|
|
174
|
+
args = parser.parse_args()
|
|
175
|
+
root = os.path.abspath(args.root)
|
|
176
|
+
|
|
177
|
+
if args.cmd == "policy":
|
|
178
|
+
ok, reason, diags = eval_policy(root, args.branch)
|
|
179
|
+
if diags:
|
|
180
|
+
for d in diags:
|
|
181
|
+
print(d, file=sys.stderr)
|
|
182
|
+
print(json.dumps({"ok": False, "reason_code": "SCRATCHPAD_MERGE_ERROR"}))
|
|
183
|
+
return 2
|
|
184
|
+
print(json.dumps({"ok": ok, "reason_code": reason or None}))
|
|
185
|
+
return 0 if ok else 2
|
|
186
|
+
|
|
187
|
+
ok, reason, pdiags = eval_post_test(root, args.branch)
|
|
188
|
+
if pdiags:
|
|
189
|
+
for d in pdiags:
|
|
190
|
+
print(d, file=sys.stderr)
|
|
191
|
+
print(json.dumps({"ok": False, "reason_code": "SCRATCHPAD_MERGE_ERROR"}))
|
|
192
|
+
return 2
|
|
193
|
+
print(json.dumps({"ok": ok, "reason_code": reason or None}))
|
|
194
|
+
return 0 if ok else 2
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare two token-cost run JSON records for AC-2 (same run_class_hash, 50% cache read)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
if _SCRIPT_DIR not in sys.path:
|
|
14
|
+
sys.path.insert(0, _SCRIPT_DIR)
|
|
15
|
+
|
|
16
|
+
import token_cost_lib # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
21
|
+
p.add_argument("baseline", type=Path, help="JSON file with run_class_hash + totals")
|
|
22
|
+
p.add_argument("target", type=Path, help="JSON file with run_class_hash + totals")
|
|
23
|
+
p.add_argument(
|
|
24
|
+
"--reduction-fraction",
|
|
25
|
+
type=float,
|
|
26
|
+
default=0.5,
|
|
27
|
+
help="Required fractional reduction in cache_read_tokens (default 0.5)",
|
|
28
|
+
)
|
|
29
|
+
args = p.parse_args()
|
|
30
|
+
base = json.loads(args.baseline.read_text(encoding="utf-8"))
|
|
31
|
+
tgt = json.loads(args.target.read_text(encoding="utf-8"))
|
|
32
|
+
ok, msg = token_cost_lib.compare_cache_read_reduction(
|
|
33
|
+
base, tgt, reduction_fraction=args.reduction_fraction
|
|
34
|
+
)
|
|
35
|
+
print(msg)
|
|
36
|
+
return 0 if ok else 2
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sys.exit(main())
|