@deftai/directive 0.55.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.
Files changed (261) hide show
  1. package/dist/bin.d.ts +3 -0
  2. package/dist/bin.js +5 -0
  3. package/dist/branch-parity.d.ts +47 -0
  4. package/dist/branch-parity.js +263 -0
  5. package/dist/cache-parity.d.ts +36 -0
  6. package/dist/cache-parity.js +165 -0
  7. package/dist/cache.d.ts +4 -0
  8. package/dist/cache.js +10 -0
  9. package/dist/capacity-backfill.d.ts +3 -0
  10. package/dist/capacity-backfill.js +9 -0
  11. package/dist/capacity-show.d.ts +3 -0
  12. package/dist/capacity-show.js +9 -0
  13. package/dist/check.d.ts +10 -0
  14. package/dist/check.js +60 -0
  15. package/dist/cli-router/index.d.ts +8 -0
  16. package/dist/cli-router/index.js +37 -0
  17. package/dist/cli-router/route-argv.d.ts +32 -0
  18. package/dist/cli-router/route-argv.js +226 -0
  19. package/dist/codebase-default-extractor.d.ts +3 -0
  20. package/dist/codebase-default-extractor.js +9 -0
  21. package/dist/codebase-map-fresh.d.ts +3 -0
  22. package/dist/codebase-map-fresh.js +9 -0
  23. package/dist/codebase-map.d.ts +3 -0
  24. package/dist/codebase-map.js +9 -0
  25. package/dist/codebase-parity.d.ts +30 -0
  26. package/dist/codebase-parity.js +294 -0
  27. package/dist/codebase-projection-registry.d.ts +3 -0
  28. package/dist/codebase-projection-registry.js +9 -0
  29. package/dist/codebase-provider.d.ts +3 -0
  30. package/dist/codebase-provider.js +9 -0
  31. package/dist/content-validate-cli/validate-links.d.ts +11 -0
  32. package/dist/content-validate-cli/validate-links.js +37 -0
  33. package/dist/content-validate-cli/validate-strategy-output.d.ts +13 -0
  34. package/dist/content-validate-cli/validate-strategy-output.js +56 -0
  35. package/dist/content-validate-cli/verify-capacity.d.ts +12 -0
  36. package/dist/content-validate-cli/verify-capacity.js +54 -0
  37. package/dist/dispatch.d.ts +26 -0
  38. package/dist/dispatch.js +569 -0
  39. package/dist/doctor-parity.d.ts +38 -0
  40. package/dist/doctor-parity.js +151 -0
  41. package/dist/doctor.d.ts +3 -0
  42. package/dist/doctor.js +10 -0
  43. package/dist/gates-cli/_helpers.d.ts +18 -0
  44. package/dist/gates-cli/_helpers.js +71 -0
  45. package/dist/index.d.ts +11 -0
  46. package/dist/index.js +15 -0
  47. package/dist/init-cli/constants.d.ts +5 -0
  48. package/dist/init-cli/constants.js +5 -0
  49. package/dist/init-cli/init.d.ts +3 -0
  50. package/dist/init-cli/init.js +11 -0
  51. package/dist/init-cli/resolve-binary.d.ts +34 -0
  52. package/dist/init-cli/resolve-binary.js +78 -0
  53. package/dist/init-cli/run-deft-install.d.ts +23 -0
  54. package/dist/init-cli/run-deft-install.js +71 -0
  55. package/dist/init-cli/update.d.ts +3 -0
  56. package/dist/init-cli/update.js +11 -0
  57. package/dist/install-cli/coverage-map.d.ts +14 -0
  58. package/dist/install-cli/coverage-map.js +111 -0
  59. package/dist/intake-parity.d.ts +30 -0
  60. package/dist/intake-parity.js +203 -0
  61. package/dist/lifecycle-cli/coverage-map.d.ts +16 -0
  62. package/dist/lifecycle-cli/coverage-map.js +254 -0
  63. package/dist/lifecycle-cli/helpers.d.ts +8 -0
  64. package/dist/lifecycle-cli/helpers.js +20 -0
  65. package/dist/lifecycle-packs-parity.d.ts +30 -0
  66. package/dist/lifecycle-packs-parity.js +377 -0
  67. package/dist/orchestration-cli/coverage-map.d.ts +21 -0
  68. package/dist/orchestration-cli/coverage-map.js +225 -0
  69. package/dist/orchestration-cli/helpers.d.ts +11 -0
  70. package/dist/orchestration-cli/helpers.js +35 -0
  71. package/dist/orchestration-parity.d.ts +38 -0
  72. package/dist/orchestration-parity.js +364 -0
  73. package/dist/parity.d.ts +36 -0
  74. package/dist/parity.js +176 -0
  75. package/dist/platform-parity.d.ts +26 -0
  76. package/dist/platform-parity.js +309 -0
  77. package/dist/policy-parity.d.ts +35 -0
  78. package/dist/policy-parity.js +214 -0
  79. package/dist/policy.d.ts +27 -0
  80. package/dist/policy.js +316 -0
  81. package/dist/pr-closing-keywords-parity.d.ts +45 -0
  82. package/dist/pr-closing-keywords-parity.js +259 -0
  83. package/dist/pr-closing-keywords.d.ts +3 -0
  84. package/dist/pr-closing-keywords.js +10 -0
  85. package/dist/pr-merge-readiness-parity.d.ts +44 -0
  86. package/dist/pr-merge-readiness-parity.js +296 -0
  87. package/dist/pr-merge-readiness.d.ts +3 -0
  88. package/dist/pr-merge-readiness.js +10 -0
  89. package/dist/pr-monitor-parity.d.ts +44 -0
  90. package/dist/pr-monitor-parity.js +283 -0
  91. package/dist/pr-monitor.d.ts +3 -0
  92. package/dist/pr-monitor.js +10 -0
  93. package/dist/pr-protected-issues-parity.d.ts +41 -0
  94. package/dist/pr-protected-issues-parity.js +220 -0
  95. package/dist/pr-protected-issues.d.ts +3 -0
  96. package/dist/pr-protected-issues.js +10 -0
  97. package/dist/pr-wait-mergeable-parity.d.ts +45 -0
  98. package/dist/pr-wait-mergeable-parity.js +340 -0
  99. package/dist/pr-wait-mergeable.d.ts +3 -0
  100. package/dist/pr-wait-mergeable.js +10 -0
  101. package/dist/preflight-cache.d.ts +16 -0
  102. package/dist/preflight-cache.js +106 -0
  103. package/dist/preflight-gh.d.ts +12 -0
  104. package/dist/preflight-gh.js +188 -0
  105. package/dist/probe-session.d.ts +3 -0
  106. package/dist/probe-session.js +10 -0
  107. package/dist/release-e2e-parity.d.ts +31 -0
  108. package/dist/release-e2e-parity.js +114 -0
  109. package/dist/release-e2e.d.ts +3 -0
  110. package/dist/release-e2e.js +10 -0
  111. package/dist/release-parity.d.ts +40 -0
  112. package/dist/release-parity.js +223 -0
  113. package/dist/release-publish-parity.d.ts +36 -0
  114. package/dist/release-publish-parity.js +138 -0
  115. package/dist/release-publish.d.ts +3 -0
  116. package/dist/release-publish.js +10 -0
  117. package/dist/release-rollback-parity.d.ts +37 -0
  118. package/dist/release-rollback-parity.js +161 -0
  119. package/dist/release-rollback.d.ts +3 -0
  120. package/dist/release-rollback.js +10 -0
  121. package/dist/release.d.ts +3 -0
  122. package/dist/release.js +10 -0
  123. package/dist/render-cli/deft-ts-runner.d.ts +20 -0
  124. package/dist/render-cli/deft-ts-runner.js +35 -0
  125. package/dist/render-cli/prd-render-cli.d.ts +2 -0
  126. package/dist/render-cli/prd-render-cli.js +7 -0
  127. package/dist/render-cli/project-render-cli.d.ts +2 -0
  128. package/dist/render-cli/project-render-cli.js +6 -0
  129. package/dist/render-cli/spec-render-cli.d.ts +2 -0
  130. package/dist/render-cli/spec-render-cli.js +6 -0
  131. package/dist/render-cli/spec-validate-cli.d.ts +2 -0
  132. package/dist/render-cli/spec-validate-cli.js +6 -0
  133. package/dist/render-parity.d.ts +35 -0
  134. package/dist/render-parity.js +383 -0
  135. package/dist/scm-parity.d.ts +39 -0
  136. package/dist/scm-parity.js +181 -0
  137. package/dist/scope-lifecycle-parity.d.ts +35 -0
  138. package/dist/scope-lifecycle-parity.js +177 -0
  139. package/dist/scope-lifecycle.d.ts +4 -0
  140. package/dist/scope-lifecycle.js +29 -0
  141. package/dist/session-parity.d.ts +39 -0
  142. package/dist/session-parity.js +260 -0
  143. package/dist/slice-parity.d.ts +36 -0
  144. package/dist/slice-parity.js +304 -0
  145. package/dist/slice.d.ts +4 -0
  146. package/dist/slice.js +29 -0
  147. package/dist/story-ready-parity.d.ts +49 -0
  148. package/dist/story-ready-parity.js +254 -0
  149. package/dist/subagent-monitor.d.ts +3 -0
  150. package/dist/subagent-monitor.js +10 -0
  151. package/dist/swarm-parity.d.ts +28 -0
  152. package/dist/swarm-parity.js +326 -0
  153. package/dist/toolchain-check.d.ts +3 -0
  154. package/dist/toolchain-check.js +14 -0
  155. package/dist/triage-actions-parity.d.ts +33 -0
  156. package/dist/triage-actions-parity.js +201 -0
  157. package/dist/triage-actions.d.ts +17 -0
  158. package/dist/triage-actions.js +144 -0
  159. package/dist/triage-aux-a-parity.d.ts +35 -0
  160. package/dist/triage-aux-a-parity.js +172 -0
  161. package/dist/triage-aux-b-parity.d.ts +32 -0
  162. package/dist/triage-aux-b-parity.js +308 -0
  163. package/dist/triage-bootstrap-parity.d.ts +44 -0
  164. package/dist/triage-bootstrap-parity.js +226 -0
  165. package/dist/triage-bootstrap.d.ts +24 -0
  166. package/dist/triage-bootstrap.js +172 -0
  167. package/dist/triage-bulk.d.ts +17 -0
  168. package/dist/triage-bulk.js +154 -0
  169. package/dist/triage-classify-parity.d.ts +40 -0
  170. package/dist/triage-classify-parity.js +227 -0
  171. package/dist/triage-classify.d.ts +13 -0
  172. package/dist/triage-classify.js +76 -0
  173. package/dist/triage-help.d.ts +3 -0
  174. package/dist/triage-help.js +10 -0
  175. package/dist/triage-queue-parity.d.ts +59 -0
  176. package/dist/triage-queue-parity.js +363 -0
  177. package/dist/triage-queue.d.ts +17 -0
  178. package/dist/triage-queue.js +174 -0
  179. package/dist/triage-reconcile.d.ts +11 -0
  180. package/dist/triage-reconcile.js +76 -0
  181. package/dist/triage-refresh.d.ts +7 -0
  182. package/dist/triage-refresh.js +37 -0
  183. package/dist/triage-scope-drift.d.ts +12 -0
  184. package/dist/triage-scope-drift.js +117 -0
  185. package/dist/triage-scope-parity.d.ts +34 -0
  186. package/dist/triage-scope-parity.js +218 -0
  187. package/dist/triage-scope.d.ts +4 -0
  188. package/dist/triage-scope.js +29 -0
  189. package/dist/triage-smoketest.d.ts +3 -0
  190. package/dist/triage-smoketest.js +43 -0
  191. package/dist/triage-subscribe.d.ts +15 -0
  192. package/dist/triage-subscribe.js +157 -0
  193. package/dist/triage-summary-parity.d.ts +50 -0
  194. package/dist/triage-summary-parity.js +306 -0
  195. package/dist/triage-summary.d.ts +14 -0
  196. package/dist/triage-summary.js +77 -0
  197. package/dist/triage-welcome.d.ts +12 -0
  198. package/dist/triage-welcome.js +78 -0
  199. package/dist/ts-check-lane.d.ts +9 -0
  200. package/dist/ts-check-lane.js +49 -0
  201. package/dist/validate-content-parity.d.ts +33 -0
  202. package/dist/validate-content-parity.js +356 -0
  203. package/dist/vbrief-activate-parity.d.ts +39 -0
  204. package/dist/vbrief-activate-parity.js +216 -0
  205. package/dist/vbrief-activate.d.ts +3 -0
  206. package/dist/vbrief-activate.js +7 -0
  207. package/dist/vbrief-build-parity.d.ts +28 -0
  208. package/dist/vbrief-build-parity.js +399 -0
  209. package/dist/vbrief-build.d.ts +3 -0
  210. package/dist/vbrief-build.js +8 -0
  211. package/dist/vbrief-preflight-parity.d.ts +34 -0
  212. package/dist/vbrief-preflight-parity.js +163 -0
  213. package/dist/vbrief-preflight.d.ts +12 -0
  214. package/dist/vbrief-preflight.js +58 -0
  215. package/dist/vbrief-reconcile-parity.d.ts +22 -0
  216. package/dist/vbrief-reconcile-parity.js +600 -0
  217. package/dist/vbrief-reconcile.d.ts +3 -0
  218. package/dist/vbrief-reconcile.js +7 -0
  219. package/dist/vbrief-validate-parity.d.ts +27 -0
  220. package/dist/vbrief-validate-parity.js +122 -0
  221. package/dist/vbrief-validate.d.ts +3 -0
  222. package/dist/vbrief-validate.js +8 -0
  223. package/dist/vbrief-validation-parity.d.ts +28 -0
  224. package/dist/vbrief-validation-parity.js +645 -0
  225. package/dist/vbrief-validation.d.ts +3 -0
  226. package/dist/vbrief-validation.js +8 -0
  227. package/dist/verify-branch.d.ts +14 -0
  228. package/dist/verify-branch.js +81 -0
  229. package/dist/verify-encoding.d.ts +15 -0
  230. package/dist/verify-encoding.js +86 -0
  231. package/dist/verify-env-parity.d.ts +28 -0
  232. package/dist/verify-env-parity.js +272 -0
  233. package/dist/verify-hooks-installed.d.ts +10 -0
  234. package/dist/verify-hooks-installed.js +48 -0
  235. package/dist/verify-investigation.d.ts +3 -0
  236. package/dist/verify-investigation.js +10 -0
  237. package/dist/verify-judgment-gates.d.ts +3 -0
  238. package/dist/verify-judgment-gates.js +10 -0
  239. package/dist/verify-no-task-runtime.d.ts +3 -0
  240. package/dist/verify-no-task-runtime.js +16 -0
  241. package/dist/verify-session-ritual.d.ts +13 -0
  242. package/dist/verify-session-ritual.js +78 -0
  243. package/dist/verify-source-cli/rule-ownership-lint.d.ts +12 -0
  244. package/dist/verify-source-cli/rule-ownership-lint.js +61 -0
  245. package/dist/verify-source-cli/verify-content-manifest.d.ts +12 -0
  246. package/dist/verify-source-cli/verify-content-manifest.js +61 -0
  247. package/dist/verify-source-cli/verify-scm-boundary.d.ts +13 -0
  248. package/dist/verify-source-cli/verify-scm-boundary.js +66 -0
  249. package/dist/verify-source-cli/verify-stubs.d.ts +11 -0
  250. package/dist/verify-source-cli/verify-stubs.js +27 -0
  251. package/dist/verify-source-parity.d.ts +26 -0
  252. package/dist/verify-source-parity.js +178 -0
  253. package/dist/verify-story-ready.d.ts +16 -0
  254. package/dist/verify-story-ready.js +138 -0
  255. package/dist/verify-tools.d.ts +13 -0
  256. package/dist/verify-tools.js +67 -0
  257. package/dist/verify-wip-cap.d.ts +13 -0
  258. package/dist/verify-wip-cap.js +62 -0
  259. package/dist/wip-cap-parity.d.ts +39 -0
  260. package/dist/wip-cap-parity.js +209 -0
  261. package/package.json +50 -0
@@ -0,0 +1,600 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Golden-output parity harness (#1782 s4): runs BOTH the frozen Python oracles
4
+ * and the ported TS vbrief-reconcile CLI over shared fixtures (fake gh on PATH,
5
+ * cache-off), then diffs exit codes and byte-identical stdout/stderr.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { PARITY_SCENARIO_NAMES } from "@deftai/directive-core/vbrief-reconcile";
13
+ const FAKE_GH_PY = `import json
14
+ import os
15
+ import sys
16
+
17
+ STATE = {"labels": {}, "comments": {}, "next_id": 1000}
18
+
19
+ def classify(cmd):
20
+ joined = " ".join(cmd)
21
+ if "issue" in cmd and "view" in cmd and "--json" in cmd and "labels" in joined:
22
+ return "issue-labels"
23
+ if "issue" in cmd and "edit" in cmd:
24
+ return "issue-edit"
25
+ if "/issues/" in joined and "/comments" in joined and "-X" not in cmd:
26
+ return "list-comments"
27
+ if "-X" in cmd and "PATCH" in cmd and "/issues/comments/" in joined:
28
+ return "patch-comment"
29
+ if "-X" in cmd and "POST" in joined and "/issues/" in joined and "/comments" in joined:
30
+ return "post-comment"
31
+ return "unknown"
32
+
33
+ def handle(label, cmd):
34
+ responses = json.loads(os.environ.get("DEFT_FAKE_GH_RESPONSES", "{}"))
35
+ if label in responses:
36
+ return responses[label]
37
+ if label == "issue-labels":
38
+ repo = cmd[cmd.index("--repo") + 1]
39
+ num = int(cmd[cmd.index("view") + 1])
40
+ labels = STATE["labels"].get((repo, num), [])
41
+ return {"returncode": 0, "stdout": json.dumps({"labels": [{"name": n} for n in labels]})}
42
+ if label == "issue-edit":
43
+ repo = cmd[cmd.index("--repo") + 1]
44
+ num = int(cmd[cmd.index("edit") + 1])
45
+ key = (repo, num)
46
+ cur = set(STATE["labels"].get(key, []))
47
+ i = 0
48
+ while i < len(cmd):
49
+ if cmd[i] == "--add-label":
50
+ cur.add(cmd[i + 1]); i += 2
51
+ elif cmd[i] == "--remove-label":
52
+ cur.discard(cmd[i + 1]); i += 2
53
+ else:
54
+ i += 1
55
+ STATE["labels"][key] = sorted(cur)
56
+ return {"returncode": 0, "stdout": ""}
57
+ if label == "list-comments":
58
+ parts = [p for p in cmd[-1].split("/") if p]
59
+ repo = "/".join(parts[1:3])
60
+ num = int(parts[3])
61
+ comments = STATE["comments"].get((repo, num), [])
62
+ return {"returncode": 0, "stdout": json.dumps(comments)}
63
+ if label == "post-comment":
64
+ parts = [p for p in cmd if "/issues/" in p][0].split("/")
65
+ repo = "/".join(parts[1:3])
66
+ num = int(parts[3])
67
+ body = json.loads(sys.stdin.read())["body"]
68
+ cid = STATE["next_id"]; STATE["next_id"] += 1
69
+ STATE["comments"].setdefault((repo, num), []).append({"id": cid, "body": body})
70
+ return {"returncode": 0, "stdout": json.dumps({"id": cid})}
71
+ if label == "patch-comment":
72
+ cid = int([p for p in cmd if "/issues/comments/" in p][0].split("/")[-1])
73
+ body = json.loads(sys.stdin.read())["body"]
74
+ for bucket in STATE["comments"].values():
75
+ for c in bucket:
76
+ if c["id"] == cid:
77
+ c["body"] = body
78
+ return {"returncode": 0, "stdout": ""}
79
+ return {"returncode": 1, "stderr": f"unexpected gh call: {label}", "stdout": ""}
80
+
81
+ label = classify(sys.argv[1:])
82
+ resp = handle(label, sys.argv[1:])
83
+ stdout = resp.get("stdout", "")
84
+ stderr = resp.get("stderr", "")
85
+ if stdout:
86
+ sys.stdout.write(stdout if stdout.endswith("\\n") else stdout)
87
+ if stderr:
88
+ sys.stderr.write(stderr)
89
+ sys.exit(int(resp.get("returncode", 0)))
90
+ `;
91
+ const PYTHON_DRIVER = `import json, os, sys
92
+ from datetime import datetime, timezone
93
+ from pathlib import Path
94
+
95
+ ROOT = Path(os.environ["DEFT_ROOT"])
96
+ sys.path.insert(0, str(ROOT / "scripts"))
97
+
98
+ from _vbrief_reconciliation import (
99
+ build_spec_task_index,
100
+ format_reconciliation_markdown,
101
+ parse_overrides_yaml,
102
+ reconcile_scope_items,
103
+ )
104
+ from vbrief_reconcile_umbrellas import parse_current_shape
105
+ import _vbrief_reconciliation as _vr_mod
106
+
107
+ FIXED_REPORT_NOW = datetime(2026, 6, 19, 12, 0, 0, tzinfo=timezone.utc)
108
+
109
+ OVERRIDES_SAMPLE = """overrides:
110
+ t2.4.1:
111
+ status: completed
112
+ body_source: spec
113
+ roadmap-9:
114
+ drop: true
115
+ """
116
+
117
+ PARSE_SHAPE_BODY = (
118
+ "## Current shape (as of pass-4)\\n"
119
+ "Last updated: 2026-06-19T00:00:00Z \\n"
120
+ "Last pass type:\\tverify\\t\\n"
121
+ "Child-count history: pass-1: 2, pass-2: 3, pass-3: 5\\n"
122
+ "Trailing field with empty value: \\n"
123
+ "Child-count history: pass-9: 9"
124
+ )
125
+
126
+ def spec_with(items):
127
+ return {
128
+ "vBRIEFInfo": {"version": "0.5", "description": "spec"},
129
+ "plan": {"title": "Spec", "status": "approved", "narratives": {}, "items": items},
130
+ }
131
+
132
+ def run_scenario(name):
133
+ if name == "reconcile-overrides":
134
+ return {"scenario": name, "ok": True, "payload": parse_overrides_yaml(OVERRIDES_SAMPLE)}
135
+ if name == "reconcile-spec-index":
136
+ spec = spec_with([
137
+ {"id": "t1.1", "title": "One", "status": "pending"},
138
+ {
139
+ "id": "phase-1",
140
+ "title": "Phase 1: Foundation",
141
+ "status": "pending",
142
+ "subItems": [{"id": "t1.1.1", "title": "Deep task", "status": "pending"}],
143
+ },
144
+ ])
145
+ index = build_spec_task_index(spec)
146
+ return {
147
+ "scenario": name,
148
+ "ok": True,
149
+ "payload": {"keys": sorted(index.keys()), "deepPhase": index["t1.1.1"].spec_phase},
150
+ }
151
+ if name == "reconcile-scope-clean":
152
+ spec = spec_with([{"id": "t1", "title": "Task one", "status": "pending"}])
153
+ items, report = reconcile_scope_items(
154
+ roadmap_active=[{"number": "", "task_id": "t1", "title": "Task one", "phase": "Phase 1"}],
155
+ roadmap_completed=[],
156
+ spec_vbrief=spec,
157
+ )
158
+ return {"scenario": name, "ok": True, "payload": {"items": items, "hasDisagreement": report.has_disagreement()}}
159
+ if name == "reconcile-scope-orphan":
160
+ spec = spec_with([{"id": "t1", "title": "One", "status": "pending"}])
161
+ items, report = reconcile_scope_items(
162
+ roadmap_active=[{"number": "9", "title": "Orphan task", "phase": "Phase 1", "synthetic_id": "roadmap-9"}],
163
+ roadmap_completed=[],
164
+ spec_vbrief=spec,
165
+ )
166
+ return {"scenario": name, "ok": True, "payload": {"items": items, "orphans": report.orphans}}
167
+ if name == "reconcile-report":
168
+ spec = spec_with([{"id": "t2.4.1", "title": "Repo indexer", "status": "pending"}])
169
+ _, report = reconcile_scope_items(
170
+ roadmap_active=[],
171
+ roadmap_completed=[{"number": "", "task_id": "t2.4.1", "title": "Repo indexer", "phase": "Completed"}],
172
+ spec_vbrief=spec,
173
+ )
174
+ class _FixedDatetime:
175
+ UTC = timezone.utc
176
+
177
+ @staticmethod
178
+ def now(tz=None):
179
+ return FIXED_REPORT_NOW
180
+
181
+ prior = _vr_mod.datetime
182
+ _vr_mod.datetime = _FixedDatetime
183
+ try:
184
+ payload = format_reconciliation_markdown(report)
185
+ finally:
186
+ _vr_mod.datetime = prior
187
+ return {"scenario": name, "ok": True, "payload": payload}
188
+ if name == "reconcile-parse-shape":
189
+ shape = parse_current_shape(PARSE_SHAPE_BODY)
190
+ return {
191
+ "scenario": name,
192
+ "ok": True,
193
+ "payload": {
194
+ "passN": shape.pass_n,
195
+ "history": [[n, c] for n, c in shape.history],
196
+ "lastUpdated": shape.last_updated,
197
+ "lastPassType": shape.last_pass_type,
198
+ },
199
+ }
200
+ raise SystemExit(f"unknown library scenario: {name}")
201
+
202
+ if __name__ == "__main__":
203
+ name = sys.argv[sys.argv.index("--scenario") + 1] if "--scenario" in sys.argv else None
204
+ if not name:
205
+ raise SystemExit("usage: driver --scenario NAME")
206
+ print(json.dumps(run_scenario(name), indent=2))
207
+ `;
208
+ const LIBRARY_SCENARIOS = new Set([
209
+ "reconcile-overrides",
210
+ "reconcile-spec-index",
211
+ "reconcile-scope-clean",
212
+ "reconcile-scope-orphan",
213
+ "reconcile-report",
214
+ "reconcile-parse-shape",
215
+ ]);
216
+ function installFakeGh() {
217
+ const binDir = mkdtempSync(join(tmpdir(), "deft-vbrief-reconcile-fake-gh-"));
218
+ for (const name of ["gh", "ghx"]) {
219
+ const bin = join(binDir, name);
220
+ writeFileSync(bin, `#!/usr/bin/env python3\n${FAKE_GH_PY}`, "utf8");
221
+ chmodSync(bin, 0o755);
222
+ }
223
+ return { binDir, cleanup: () => rmSync(binDir, { recursive: true, force: true }) };
224
+ }
225
+ function runCapture(cmd, args, cwd, env) {
226
+ const result = spawnSync(cmd, args, {
227
+ cwd,
228
+ encoding: "utf8",
229
+ env: { ...process.env, ...env },
230
+ stdio: ["ignore", "pipe", "pipe"],
231
+ });
232
+ return {
233
+ name: "",
234
+ exitCode: result.status ?? 2,
235
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
236
+ stderr: typeof result.stderr === "string" ? result.stderr : "",
237
+ };
238
+ }
239
+ function resolveDeftRoot() {
240
+ if (process.env.DEFT_ROOT)
241
+ return resolve(process.env.DEFT_ROOT);
242
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
243
+ }
244
+ function installPythonDriver() {
245
+ const dir = mkdtempSync(join(tmpdir(), "deft-vbrief-reconcile-driver-"));
246
+ const driverPath = join(dir, "vbrief_reconcile_parity_driver.py");
247
+ writeFileSync(driverPath, PYTHON_DRIVER, "utf8");
248
+ chmodSync(driverPath, 0o755);
249
+ return { driverPath, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
250
+ }
251
+ function fakeGhResponsesForScenario(name) {
252
+ if (name === "labels-blocked-dry-run" || name === "labels-utf8-dry-run") {
253
+ return {
254
+ "issue-labels": { returncode: 0, stdout: "[]\n" },
255
+ };
256
+ }
257
+ if (name === "umbrellas-create-dry-run") {
258
+ return { "list-comments": { returncode: 0, stdout: "[]\n" } };
259
+ }
260
+ if (name === "umbrellas-unchanged") {
261
+ return { "list-comments": { returncode: 0, stdout: "[]\n" } };
262
+ }
263
+ return {};
264
+ }
265
+ function writeGraphFixtures(root, name) {
266
+ const writeBrief = (storyId, folder, dependsOn = []) => {
267
+ const dir = join(root, "vbrief", folder);
268
+ mkdirSync(dir, { recursive: true });
269
+ const statusMap = {
270
+ proposed: "proposed",
271
+ completed: "completed",
272
+ };
273
+ writeFileSync(join(dir, `2026-05-21-${storyId}.vbrief.json`), `${JSON.stringify({
274
+ vBRIEFInfo: { version: "0.6" },
275
+ plan: {
276
+ id: storyId,
277
+ title: storyId,
278
+ status: statusMap[folder] ?? "pending",
279
+ narratives: {
280
+ Description: `${storyId} description.`,
281
+ ImplementationPlan: `1. Do ${storyId}.`,
282
+ UserStory: `As a user, I want ${storyId}.`,
283
+ Traces: "FR-1",
284
+ },
285
+ items: [
286
+ {
287
+ id: `${storyId}-a1`,
288
+ title: "Acceptance item 1",
289
+ status: "pending",
290
+ narrative: { Acceptance: `Given X when ${storyId} then Y.` },
291
+ },
292
+ ],
293
+ metadata: {
294
+ kind: "story",
295
+ swarm: {
296
+ readiness: "ready",
297
+ parallel_safe: true,
298
+ file_scope: [`src/${storyId}.py`],
299
+ verify_commands: [`pytest ${storyId}`],
300
+ expected_outputs: ["tests pass"],
301
+ depends_on: dependsOn,
302
+ conflict_group: "reconcile-suite",
303
+ size: "small",
304
+ file_scope_confidence: "high",
305
+ model_tier: "standard",
306
+ },
307
+ },
308
+ },
309
+ }, null, 2)}\n`, "utf8");
310
+ };
311
+ if (name === "graph-dry-run") {
312
+ mkdirSync(join(root, "vbrief", "proposed"), { recursive: true });
313
+ writeBrief("dep-done", "completed");
314
+ writeBrief("child", "proposed", ["dep-done"]);
315
+ }
316
+ else if (name === "graph-cycle") {
317
+ mkdirSync(join(root, "vbrief", "proposed"), { recursive: true });
318
+ writeBrief("a", "proposed", ["b"]);
319
+ writeBrief("b", "proposed", ["a"]);
320
+ }
321
+ else if (name === "graph-missing-proposed") {
322
+ mkdirSync(join(root, "vbrief"), { recursive: true });
323
+ }
324
+ }
325
+ function runGraphCliParity(deftRoot, name, fixture, env) {
326
+ writeGraphFixtures(fixture, name);
327
+ const py = runCapture("uv", [
328
+ "run",
329
+ "python",
330
+ join(deftRoot, "scripts", "vbrief_reconcile_graph.py"),
331
+ "--project-root",
332
+ fixture,
333
+ "--dry-run",
334
+ ], deftRoot, env);
335
+ const ts = runCapture("node", [
336
+ join(deftRoot, "packages", "cli", "dist", "vbrief-reconcile.js"),
337
+ "graph",
338
+ "--project-root",
339
+ fixture,
340
+ "--dry-run",
341
+ ], deftRoot, env);
342
+ return {
343
+ python: { name, exitCode: py.exitCode, stdout: py.stdout, stderr: py.stderr },
344
+ ts: { name, exitCode: ts.exitCode, stdout: ts.stdout, stderr: ts.stderr },
345
+ };
346
+ }
347
+ function writeLabelsUmbrellasFixtures(root, name) {
348
+ const writeBrief = (storyId, folder, extra = {}) => {
349
+ const dir = join(root, "vbrief", folder);
350
+ mkdirSync(dir, { recursive: true });
351
+ const statusMap = {
352
+ proposed: "proposed",
353
+ pending: "pending",
354
+ active: "running",
355
+ completed: "completed",
356
+ cancelled: "cancelled",
357
+ };
358
+ writeFileSync(join(dir, `2026-05-21-${storyId}.vbrief.json`), `${JSON.stringify({
359
+ vBRIEFInfo: { version: "0.6" },
360
+ plan: {
361
+ id: storyId,
362
+ title: storyId,
363
+ status: statusMap[folder] ?? "pending",
364
+ narratives: {
365
+ Description: `${storyId} description.`,
366
+ ImplementationPlan: `1. Do ${storyId}.`,
367
+ UserStory: `As a user, I want ${storyId}.`,
368
+ Traces: "FR-1",
369
+ },
370
+ items: [
371
+ {
372
+ id: `${storyId}-a1`,
373
+ title: "Acceptance item 1",
374
+ status: "pending",
375
+ narrative: { Acceptance: `Given X when ${storyId} then Y.` },
376
+ },
377
+ ],
378
+ metadata: {
379
+ kind: "story",
380
+ swarm: {
381
+ readiness: "ready",
382
+ parallel_safe: true,
383
+ file_scope: [`src/${storyId}.py`],
384
+ verify_commands: [`pytest ${storyId}`],
385
+ expected_outputs: ["tests pass"],
386
+ depends_on: [],
387
+ conflict_group: "reconcile-suite",
388
+ size: "small",
389
+ file_scope_confidence: "high",
390
+ model_tier: "standard",
391
+ },
392
+ },
393
+ references: [],
394
+ ...extra,
395
+ },
396
+ }, null, 2)}\n`, "utf8");
397
+ };
398
+ if (name === "labels-blocked-dry-run") {
399
+ writeBrief("blk", "active", {
400
+ status: "blocked",
401
+ references: [
402
+ {
403
+ uri: "https://github.com/deftai/directive/issues/10",
404
+ type: "x-vbrief/github-issue",
405
+ title: "Issue #10",
406
+ },
407
+ ],
408
+ });
409
+ }
410
+ else if (name === "labels-utf8-dry-run") {
411
+ writeBrief("utf8", "active", {
412
+ references: [
413
+ {
414
+ uri: "https://github.com/deftai/directive/issues/11",
415
+ type: "x-vbrief/github-issue",
416
+ title: "Issue #11 — smart “quotes”",
417
+ },
418
+ ],
419
+ });
420
+ }
421
+ else if (name === "umbrellas-create-dry-run") {
422
+ writeBrief("child-a", "active", { metadata: { kind: "story", swarm: { depends_on: [] } } });
423
+ writeBrief("epic-1", "active", {
424
+ metadata: { kind: "epic", swarm: { depends_on: [] } },
425
+ references: [
426
+ { uri: "active/2026-05-21-child-a.vbrief.json", type: "x-vbrief/plan", title: "child-a" },
427
+ {
428
+ uri: "https://github.com/deftai/directive/issues/1284",
429
+ type: "x-vbrief/github-issue",
430
+ title: "Issue #1284",
431
+ },
432
+ ],
433
+ });
434
+ }
435
+ else if (name === "umbrellas-unchanged") {
436
+ writeBrief("child-b", "active", { metadata: { kind: "story", swarm: { depends_on: [] } } });
437
+ writeBrief("epic-2", "active", {
438
+ metadata: { kind: "epic", swarm: { depends_on: [] } },
439
+ references: [
440
+ { uri: "active/2026-05-21-child-b.vbrief.json", type: "x-vbrief/plan", title: "child-b" },
441
+ {
442
+ uri: "https://github.com/deftai/directive/issues/1285",
443
+ type: "x-vbrief/github-issue",
444
+ title: "Issue #1285",
445
+ },
446
+ ],
447
+ });
448
+ }
449
+ }
450
+ function runLabelsUmbrellasParity(deftRoot, name, env) {
451
+ const pyFixture = mkdtempSync(join(tmpdir(), "deft-vbrief-reconcile-py-"));
452
+ const tsFixture = mkdtempSync(join(tmpdir(), "deft-vbrief-reconcile-ts-"));
453
+ const script = name.startsWith("labels-")
454
+ ? "vbrief_reconcile_labels.py"
455
+ : "vbrief_reconcile_umbrellas.py";
456
+ const verb = name.startsWith("labels-") ? "labels" : "umbrellas";
457
+ try {
458
+ writeLabelsUmbrellasFixtures(pyFixture, name);
459
+ writeLabelsUmbrellasFixtures(tsFixture, name);
460
+ if (name === "umbrellas-unchanged") {
461
+ runCapture("uv", ["run", "python", join(deftRoot, "scripts", script), "--project-root", pyFixture], deftRoot, env);
462
+ runCapture("node", [
463
+ join(deftRoot, "packages", "cli", "dist", "vbrief-reconcile.js"),
464
+ verb,
465
+ "--project-root",
466
+ tsFixture,
467
+ ], deftRoot, env);
468
+ }
469
+ const pyArgs = [
470
+ "run",
471
+ "python",
472
+ join(deftRoot, "scripts", script),
473
+ "--project-root",
474
+ pyFixture,
475
+ "--dry-run",
476
+ ];
477
+ const tsArgs = [
478
+ join(deftRoot, "packages", "cli", "dist", "vbrief-reconcile.js"),
479
+ verb,
480
+ "--project-root",
481
+ tsFixture,
482
+ "--dry-run",
483
+ ];
484
+ const py = runCapture("uv", pyArgs, deftRoot, env);
485
+ const ts = runCapture("node", tsArgs, deftRoot, env);
486
+ return {
487
+ python: { name, exitCode: py.exitCode, stdout: py.stdout, stderr: py.stderr },
488
+ ts: { name, exitCode: ts.exitCode, stdout: ts.stdout, stderr: ts.stderr },
489
+ };
490
+ }
491
+ finally {
492
+ rmSync(pyFixture, { recursive: true, force: true });
493
+ rmSync(tsFixture, { recursive: true, force: true });
494
+ }
495
+ }
496
+ export function runParity() {
497
+ const deftRoot = resolveDeftRoot();
498
+ const driver = installPythonDriver();
499
+ const fake = installFakeGh();
500
+ const scenarios = [];
501
+ try {
502
+ for (const name of PARITY_SCENARIO_NAMES) {
503
+ const env = {
504
+ DEFT_CACHE_DISABLE: "1",
505
+ PYTHONUTF8: "1",
506
+ PATH: `${fake.binDir}:${process.env.PATH ?? ""}`,
507
+ DEFT_ROOT: deftRoot,
508
+ DEFT_FAKE_GH_RESPONSES: JSON.stringify(fakeGhResponsesForScenario(name)),
509
+ };
510
+ let ran;
511
+ if (LIBRARY_SCENARIOS.has(name)) {
512
+ const pyFixture = mkdtempSync(join(tmpdir(), "deft-vbrief-reconcile-lib-py-"));
513
+ const tsFixture = mkdtempSync(join(tmpdir(), "deft-vbrief-reconcile-lib-ts-"));
514
+ try {
515
+ const py = runCapture("uv", ["run", "python", driver.driverPath, "--scenario", name], deftRoot, env);
516
+ const ts = runCapture("node", [
517
+ join(deftRoot, "packages", "cli", "dist", "vbrief-reconcile.js"),
518
+ "parity",
519
+ "--scenario",
520
+ name,
521
+ "--fixture-root",
522
+ tsFixture,
523
+ ], deftRoot, env);
524
+ ran = {
525
+ python: { name, exitCode: py.exitCode, stdout: py.stdout, stderr: py.stderr },
526
+ ts: { name, exitCode: ts.exitCode, stdout: ts.stdout, stderr: ts.stderr },
527
+ };
528
+ }
529
+ finally {
530
+ rmSync(pyFixture, { recursive: true, force: true });
531
+ rmSync(tsFixture, { recursive: true, force: true });
532
+ }
533
+ }
534
+ else if (name.startsWith("graph-")) {
535
+ const fixture = mkdtempSync(join(tmpdir(), "deft-vbrief-reconcile-graph-"));
536
+ try {
537
+ ran = runGraphCliParity(deftRoot, name, fixture, env);
538
+ }
539
+ finally {
540
+ rmSync(fixture, { recursive: true, force: true });
541
+ }
542
+ }
543
+ else {
544
+ ran = runLabelsUmbrellasParity(deftRoot, name, env);
545
+ }
546
+ scenarios.push({
547
+ name,
548
+ pythonExit: ran.python.exitCode,
549
+ tsExit: ran.ts.exitCode,
550
+ exitMismatch: ran.python.exitCode !== ran.ts.exitCode,
551
+ outputMismatch: ran.python.stdout !== ran.ts.stdout || ran.python.stderr !== ran.ts.stderr,
552
+ pythonOutput: ran.python.stdout || ran.python.stderr,
553
+ tsOutput: ran.ts.stdout || ran.ts.stderr,
554
+ });
555
+ }
556
+ }
557
+ finally {
558
+ driver.cleanup();
559
+ fake.cleanup();
560
+ }
561
+ const ok = scenarios.every((s) => !s.exitMismatch && !s.outputMismatch);
562
+ return { ok, scenarios };
563
+ }
564
+ export function renderReport(result) {
565
+ if (result.ok) {
566
+ return `vbrief_reconcile parity: CLEAN -- Python and TS agree on ${result.scenarios.length} scenario(s).`;
567
+ }
568
+ const lines = ["vbrief_reconcile parity: DIVERGENCE"];
569
+ for (const s of result.scenarios) {
570
+ if (s.exitMismatch || s.outputMismatch) {
571
+ lines.push(` scenario: ${s.name}`);
572
+ if (s.exitMismatch)
573
+ lines.push(` exit mismatch: python=${s.pythonExit} ts=${s.tsExit}`);
574
+ if (s.outputMismatch) {
575
+ lines.push(` python (${s.pythonOutput.length} bytes):`);
576
+ lines.push(s.pythonOutput.slice(0, 500));
577
+ lines.push(` ts (${s.tsOutput.length} bytes):`);
578
+ lines.push(s.tsOutput.slice(0, 500));
579
+ }
580
+ }
581
+ }
582
+ return lines.join("\n");
583
+ }
584
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
585
+ try {
586
+ const result = runParity();
587
+ if (result.ok) {
588
+ process.stdout.write(`${renderReport(result)}\n`);
589
+ process.exit(0);
590
+ }
591
+ process.stderr.write(`${renderReport(result)}\n`);
592
+ process.exit(1);
593
+ }
594
+ catch (err) {
595
+ const msg = String(err).replace(/\r?\n/g, " ");
596
+ process.stderr.write(`vbrief_reconcile parity: harness error -- ${msg}\n`);
597
+ process.exit(2);
598
+ }
599
+ }
600
+ //# sourceMappingURL=vbrief-reconcile-parity.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=vbrief-reconcile.d.ts.map
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ import { cmdVbriefReconcile } from "@deftai/directive-core/vbrief-reconcile";
4
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
5
+ process.exit(cmdVbriefReconcile(process.argv.slice(2)));
6
+ }
7
+ //# sourceMappingURL=vbrief-reconcile.js.map
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ export interface GateCapture {
3
+ readonly name: string;
4
+ readonly exitCode: number;
5
+ readonly stdout: string;
6
+ readonly stderr: string;
7
+ }
8
+ export interface GateParity {
9
+ readonly name: string;
10
+ readonly exitMismatch: boolean;
11
+ readonly stdoutMismatch: boolean;
12
+ readonly stderrMismatch: boolean;
13
+ readonly pythonExit: number;
14
+ readonly tsExit: number;
15
+ readonly pythonStdout: string;
16
+ readonly tsStdout: string;
17
+ readonly pythonStderr: string;
18
+ readonly tsStderr: string;
19
+ }
20
+ export interface ParityResult {
21
+ readonly ok: boolean;
22
+ readonly gates: readonly GateParity[];
23
+ }
24
+ export declare function diffGate(python: GateCapture, ts: GateCapture): GateParity;
25
+ export declare function runParity(deftRoot?: string): ParityResult;
26
+ export declare function renderReport(result: ParityResult): string;
27
+ //# sourceMappingURL=vbrief-validate-parity.d.ts.map