aw-spec-cli 0.1.5 → 0.1.7

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 (205) hide show
  1. package/assets/.agents/agent.json +1 -1
  2. package/assets/.agents/skills/{aw-spec-orch → appweaver}/SKILL.md +10 -10
  3. package/assets/.agents/skills/{aw-spec-orch-demo → appweaver-demo}/SKILL.md +9 -9
  4. package/assets/.claude/agent.json +1 -1
  5. package/assets/.claude/skills/{aw-spec-orch → appweaver}/SKILL.md +10 -10
  6. package/assets/.claude/skills/{aw-spec-orch-demo → appweaver-demo}/SKILL.md +9 -9
  7. package/assets/.kilocode/agent.json +1 -1
  8. package/assets/.kilocode/workflows/{aw-spec-orch-demo.md → appweaver-demo.md} +9 -9
  9. package/assets/.kilocode/workflows/{aw-spec-orch.md → appweaver.md} +10 -10
  10. package/assets/docs/pharma-constitution.my-ref.md +86 -0
  11. package/dist/utils/paths.js +1 -1
  12. package/dist/utils/paths.js.map +1 -1
  13. package/package.json +1 -1
  14. package/assets/.aw-spec/DESIGN-FORMAT.md +0 -151
  15. package/assets/.aw-spec/ORCH-DIALECT.md +0 -156
  16. package/assets/.aw-spec/RULE-PARTITION.md +0 -126
  17. package/assets/.aw-spec/domain.json +0 -3
  18. package/assets/.aw-spec/extensions/.registry +0 -19
  19. package/assets/.aw-spec/extensions/agent-context/README.md +0 -57
  20. package/assets/.aw-spec/extensions/agent-context/agent-context-config.yml +0 -4
  21. package/assets/.aw-spec/extensions/agent-context/commands/aw-spec.agent-context.update.md +0 -26
  22. package/assets/.aw-spec/extensions/agent-context/extension.yml +0 -37
  23. package/assets/.aw-spec/extensions/agent-context/scripts/bash/update-agent-context.sh +0 -201
  24. package/assets/.aw-spec/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +0 -237
  25. package/assets/.aw-spec/extensions.yml +0 -16
  26. package/assets/.aw-spec/feature.json +0 -3
  27. package/assets/.aw-spec/init-options.json +0 -9
  28. package/assets/.aw-spec/integration.json +0 -15
  29. package/assets/.aw-spec/integrations/aw-spec.manifest.json +0 -9
  30. package/assets/.aw-spec/interpreter/run-skill.py +0 -1158
  31. package/assets/.aw-spec/memory/constitution.md +0 -50
  32. package/assets/.aw-spec/orchestrator/AGENT-PROFILE.md +0 -94
  33. package/assets/.aw-spec/orchestrator/README.md +0 -300
  34. package/assets/.aw-spec/orchestrator/adapters/claude_agent_sdk.py +0 -74
  35. package/assets/.aw-spec/orchestrator/adapters/google_antigravity.py +0 -74
  36. package/assets/.aw-spec/orchestrator/modules/delta-design.orch.yaml +0 -105
  37. package/assets/.aw-spec/orchestrator/modules/delta-spec.orch.yaml +0 -79
  38. package/assets/.aw-spec/orchestrator/modules/domain-select.orch.yaml +0 -81
  39. package/assets/.aw-spec/orchestrator/modules/tech-select.orch.yaml +0 -75
  40. package/assets/.aw-spec/orchestrator/orchestrator.py +0 -1721
  41. package/assets/.aw-spec/orchestrator/rev-templates/ai-rules.reference.md +0 -40
  42. package/assets/.aw-spec/orchestrator/rev-templates/comandrulemap.skeleton.md +0 -59
  43. package/assets/.aw-spec/orchestrator/rev-templates/commandtechrulemap.skeleton.md +0 -97
  44. package/assets/.aw-spec/orchestrator/rev-templates/domain-constitution.skeleton.md +0 -61
  45. package/assets/.aw-spec/orchestrator/rev-templates/domain-requirements.skeleton.md +0 -48
  46. package/assets/.aw-spec/orchestrator/rev-templates/domain-rule-file.skeleton.md +0 -39
  47. package/assets/.aw-spec/orchestrator/rev-templates/technical-constitution.skeleton.md +0 -106
  48. package/assets/.aw-spec/orchestrator/rev-templates/technical-rule-file.skeleton.md +0 -54
  49. package/assets/.aw-spec/orchestrator/workflows/_smoke.orch.yaml +0 -160
  50. package/assets/.aw-spec/orchestrator/workflows/aw-spec-add-component.orch.yaml +0 -244
  51. package/assets/.aw-spec/orchestrator/workflows/aw-spec-adopt.orch.yaml +0 -775
  52. package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +0 -248
  53. package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +0 -261
  54. package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +0 -425
  55. package/assets/.aw-spec/orchestrator/workflows/aw-spec-build.orch.yaml +0 -188
  56. package/assets/.aw-spec/orchestrator/workflows/aw-spec-characterize.orch.yaml +0 -474
  57. package/assets/.aw-spec/orchestrator/workflows/aw-spec-close-delta.orch.yaml +0 -181
  58. package/assets/.aw-spec/orchestrator/workflows/aw-spec-constitution.orch.yaml +0 -284
  59. package/assets/.aw-spec/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -103
  60. package/assets/.aw-spec/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +0 -69
  61. package/assets/.aw-spec/orchestrator/workflows/aw-spec-deploy.orch.yaml +0 -85
  62. package/assets/.aw-spec/orchestrator/workflows/aw-spec-design.orch.yaml +0 -382
  63. package/assets/.aw-spec/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +0 -382
  64. package/assets/.aw-spec/orchestrator/workflows/aw-spec-foundation.orch.yaml +0 -246
  65. package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +0 -231
  66. package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement.orch.yaml +0 -258
  67. package/assets/.aw-spec/orchestrator/workflows/aw-spec-inventory.orch.yaml +0 -236
  68. package/assets/.aw-spec/orchestrator/workflows/aw-spec-onboard.orch.yaml +0 -949
  69. package/assets/.aw-spec/orchestrator/workflows/aw-spec-parity.orch.yaml +0 -293
  70. package/assets/.aw-spec/orchestrator/workflows/aw-spec-run.orch.yaml +0 -177
  71. package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-domain.orch.yaml +0 -79
  72. package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-feature.orch.yaml +0 -128
  73. package/assets/.aw-spec/orchestrator/workflows/aw-spec-specify.orch.yaml +0 -412
  74. package/assets/.aw-spec/scripts/bash/aw-inventory.sh +0 -14
  75. package/assets/.aw-spec/scripts/bash/aw-parity.sh +0 -13
  76. package/assets/.aw-spec/scripts/bash/aw-rev-constitution.sh +0 -13
  77. package/assets/.aw-spec/scripts/bash/aw-spec-actions.sh +0 -1899
  78. package/assets/.aw-spec/scripts/bash/aw-xref.sh +0 -15
  79. package/assets/.aw-spec/scripts/bash/common.sh +0 -533
  80. package/assets/.aw-spec/scripts/powershell/aw-inventory.ps1 +0 -14
  81. package/assets/.aw-spec/scripts/powershell/aw-parity.ps1 +0 -13
  82. package/assets/.aw-spec/scripts/powershell/aw-rev-constitution.ps1 +0 -13
  83. package/assets/.aw-spec/scripts/powershell/aw-spec-actions.ps1 +0 -1563
  84. package/assets/.aw-spec/scripts/powershell/aw-xref.ps1 +0 -15
  85. package/assets/.aw-spec/scripts/python/aw-inventory.py +0 -683
  86. package/assets/.aw-spec/scripts/python/aw-parity.py +0 -380
  87. package/assets/.aw-spec/scripts/python/aw-rev-constitution.py +0 -312
  88. package/assets/.aw-spec/scripts/python/aw-xref.py +0 -1330
  89. package/assets/.aw-spec/scripts/python/validate_okf.py +0 -349
  90. package/assets/.aw-spec/templates/constitution-domain-template.md +0 -75
  91. package/assets/.aw-spec/templates/constitution-technical-template.md +0 -70
  92. package/assets/.aw-spec/templates/delta-design-template.md +0 -69
  93. package/assets/.aw-spec/templates/delta-spec-template.md +0 -60
  94. package/assets/.aw-spec/templates/design-template.md +0 -383
  95. package/assets/.aw-spec/templates/okf-concept-template.md +0 -103
  96. package/assets/.aw-spec/templates/parity-manifest-template.json +0 -49
  97. package/assets/.aw-spec/templates/source-map-template.md +0 -85
  98. package/assets/.aw-spec/templates/spec-template.md +0 -252
  99. package/assets/docs/ModularMonolith/ModularMonolith.Reference.pack.txt +0 -25248
  100. package/assets/docs/ModularMonolith/ModularMonolith.pack.txt +0 -5726
  101. package/assets/docs/ModularMonolith/pack-foundation.ps1 +0 -145
  102. package/assets/docs/ModularMonolith/pack-foundation.sh +0 -196
  103. package/assets/docs/ModularMonolith/pack-reference-foundation.ps1 +0 -157
  104. package/assets/docs/ModularMonolith/pack-reference-foundation.sh +0 -209
  105. package/assets/docs/ModularMonolith/unpack-foundation.ps1 +0 -126
  106. package/assets/docs/ModularMonolith/unpack-foundation.sh +0 -217
  107. package/assets/docs/ModularMonolith/unpack-reference-foundation.ps1 +0 -129
  108. package/assets/docs/ModularMonolith/unpack-reference-foundation.sh +0 -218
  109. package/assets/docs/README.md +0 -106
  110. package/assets/docs/VerticalSlice/PACKAGING.md +0 -185
  111. package/assets/docs/VerticalSlice/VerticalSlice.Reference.pack.txt +0 -19318
  112. package/assets/docs/VerticalSlice/VerticalSlice.pack.txt +0 -2833
  113. package/assets/docs/VerticalSlice/pack-foundation.ps1 +0 -145
  114. package/assets/docs/VerticalSlice/pack-foundation.sh +0 -196
  115. package/assets/docs/VerticalSlice/pack-reference-foundation.ps1 +0 -157
  116. package/assets/docs/VerticalSlice/pack-reference-foundation.sh +0 -209
  117. package/assets/docs/VerticalSlice/unpack-foundation.ps1 +0 -126
  118. package/assets/docs/VerticalSlice/unpack-foundation.sh +0 -217
  119. package/assets/docs/VerticalSlice/unpack-reference-foundation.ps1 +0 -129
  120. package/assets/docs/VerticalSlice/unpack-reference-foundation.sh +0 -218
  121. package/assets/docs/architecture/clean-architecture.md +0 -71
  122. package/assets/docs/architecture/cqrs.md +0 -141
  123. package/assets/docs/architecture/eventing.md +0 -83
  124. package/assets/docs/architecture/folder-structure.md +0 -61
  125. package/assets/docs/architecture/modular-monolith.md +0 -47
  126. package/assets/docs/architecture/persistence.md +0 -269
  127. package/assets/docs/architecture/testing.md +0 -55
  128. package/assets/docs/architecture/vertical-slices.md +0 -201
  129. package/assets/docs/components/Email/README.md +0 -119
  130. package/assets/docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +0 -84
  131. package/assets/docs/components/Email/integration.md +0 -300
  132. package/assets/docs/components/Jobs/README.md +0 -113
  133. package/assets/docs/components/Jobs/integration.md +0 -259
  134. package/assets/docs/components/QrGenerator/README.md +0 -85
  135. package/assets/docs/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +0 -57
  136. package/assets/docs/components/QrGenerator/integration.md +0 -230
  137. package/assets/docs/components/README.md +0 -74
  138. package/assets/docs/domain/domain-driven-design.md +0 -212
  139. package/assets/docs/domainspecs/healthcare/ai-rules.domain.md +0 -34
  140. package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +0 -17
  141. package/assets/docs/domainspecs/healthcare/auditing.domain.md +0 -97
  142. package/assets/docs/domainspecs/healthcare/authorization.domain.md +0 -26
  143. package/assets/docs/domainspecs/healthcare/clinical-terms.md +0 -20
  144. package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +0 -26
  145. package/assets/docs/domainspecs/healthcare/comandrulemap.md +0 -100
  146. package/assets/docs/domainspecs/healthcare/commandtechrulemap.md +0 -119
  147. package/assets/docs/domainspecs/healthcare/consent.md +0 -19
  148. package/assets/docs/domainspecs/healthcare/cqrs.domain.md +0 -12
  149. package/assets/docs/domainspecs/healthcare/data-classification.domain.md +0 -28
  150. package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +0 -11
  151. package/assets/docs/domainspecs/healthcare/dicom.md +0 -15
  152. package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +0 -28
  153. package/assets/docs/domainspecs/healthcare/domain-requirements.md +0 -55
  154. package/assets/docs/domainspecs/healthcare/eventing.domain.md +0 -18
  155. package/assets/docs/domainspecs/healthcare/fhir.md +0 -36
  156. package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +0 -23
  157. package/assets/docs/domainspecs/healthcare/hipaa.md +0 -34
  158. package/assets/docs/domainspecs/healthcare/hl7.md +0 -15
  159. package/assets/docs/domainspecs/healthcare/integration-terms.md +0 -21
  160. package/assets/docs/domainspecs/healthcare/persistence.domain.md +0 -20
  161. package/assets/docs/domainspecs/healthcare/terminology.md +0 -30
  162. package/assets/docs/domainspecs/healthcare/testing.domain.md +0 -15
  163. package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +0 -18
  164. package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +0 -12
  165. package/assets/docs/domainspecs/pharma/ai-rules.domain.md +0 -32
  166. package/assets/docs/domainspecs/pharma/api-versioning.domain.md +0 -20
  167. package/assets/docs/domainspecs/pharma/atc.md +0 -22
  168. package/assets/docs/domainspecs/pharma/auditing.domain.md +0 -90
  169. package/assets/docs/domainspecs/pharma/authorization.domain.md +0 -25
  170. package/assets/docs/domainspecs/pharma/coding-standards.domain.md +0 -28
  171. package/assets/docs/domainspecs/pharma/comandrulemap.md +0 -108
  172. package/assets/docs/domainspecs/pharma/commandtechrulemap.md +0 -119
  173. package/assets/docs/domainspecs/pharma/cqrs.domain.md +0 -16
  174. package/assets/docs/domainspecs/pharma/data-classification.domain.md +0 -24
  175. package/assets/docs/domainspecs/pharma/data-provenance.md +0 -21
  176. package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +0 -11
  177. package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +0 -36
  178. package/assets/docs/domainspecs/pharma/domain-requirements.md +0 -99
  179. package/assets/docs/domainspecs/pharma/eventing.domain.md +0 -18
  180. package/assets/docs/domainspecs/pharma/fhir.md +0 -31
  181. package/assets/docs/domainspecs/pharma/folder-structure.domain.md +0 -24
  182. package/assets/docs/domainspecs/pharma/hl7.md +0 -17
  183. package/assets/docs/domainspecs/pharma/idmp.md +0 -26
  184. package/assets/docs/domainspecs/pharma/integration-terms.md +0 -21
  185. package/assets/docs/domainspecs/pharma/ndc.md +0 -28
  186. package/assets/docs/domainspecs/pharma/persistence.domain.md +0 -23
  187. package/assets/docs/domainspecs/pharma/pharma-terms.md +0 -27
  188. package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +0 -30
  189. package/assets/docs/domainspecs/pharma/regulatory-compliance.md +0 -19
  190. package/assets/docs/domainspecs/pharma/terminology.md +0 -32
  191. package/assets/docs/domainspecs/pharma/testing.domain.md +0 -19
  192. package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +0 -17
  193. package/assets/docs/domainspecs/pharma/zero-trust.domain.md +0 -12
  194. package/assets/docs/glossary/ai-rules.md +0 -59
  195. package/assets/docs/governance/api-versioning.md +0 -99
  196. package/assets/docs/governance/coding-standards.md +0 -87
  197. package/assets/docs/governance/dependency-policy.md +0 -40
  198. package/assets/docs/interop/ihe-conformance.md +0 -31
  199. package/assets/docs/requirement-spec.txt +0 -27
  200. package/assets/docs/security/auditing.md +0 -160
  201. package/assets/docs/security/authorization-matrix.md +0 -14
  202. package/assets/docs/security/authorization.md +0 -87
  203. package/assets/docs/security/data-classification.md +0 -38
  204. package/assets/docs/security/zero-trust.md +0 -76
  205. package/assets/docs/technology-constitution.md +0 -172
@@ -1,380 +0,0 @@
1
- #!/usr/bin/env python3
2
- """aw-parity.py — the Brownfield Pipeline PARITY runner (the cutover gate).
3
-
4
- Parity is the migration-grade oracle: run every case in a parity manifest against
5
- BOTH the running LEGACY system and the new CANDIDATE, and diff observable output
6
- (response status + body, and optional resulting-state probes). The seam cuts over
7
- to the candidate only when parity HOLDS — i.e. no unaccepted diffs. Approved
8
- intended differences are whitelisted in the manifest's `expected_deviations` (each
9
- must become a MODIFIED spec item via the CR loop — parity never lets behaviour
10
- drift silently). See Brownfield-Pipeline.my-ref.md.
11
-
12
- Unlike Characterize (a frozen snapshot of behaviour, in CI), Parity compares
13
- against the LIVE legacy system, so it catches behaviour no static test captured.
14
-
15
- Stdlib only (urllib for http, subprocess for shell adapters). Subcommands print a
16
- single JSON object as the LAST stdout line (the orchestrator binds it); human
17
- detail goes to STDERR.
18
-
19
- scaffold --feature-dir <dir> -> {manifest_path, created} (copy the template)
20
- check --manifest <path> -> {ready, sides_configured, case_count, error}
21
- run --manifest <path> [--out <report.md>]
22
- -> {ok, ready, total, matched, mismatched, accepted, errors, report_path}
23
-
24
- A side is invoked by `kind`:
25
- http : { "kind":"http", "base_url":"http://host:port" } → base_url + case.request.path
26
- shell : { "kind":"shell", "command":"./adapter.sh" } → request JSON on stdin, output JSON on stdout
27
- """
28
-
29
- import argparse
30
- import json
31
- import os
32
- import re
33
- import subprocess
34
- import sys
35
- import urllib.error
36
- import urllib.request
37
-
38
- HERE = os.path.dirname(os.path.abspath(__file__))
39
- TEMPLATE = os.path.join(HERE, "..", "..", "templates", "parity-manifest-template.json")
40
- PLACEHOLDER = re.compile(r"REPLACE_WITH", re.IGNORECASE)
41
-
42
-
43
- # --------------------------------------------------------------------------- IO
44
- def load_manifest(path):
45
- with open(path, encoding="utf-8") as f:
46
- return json.load(f)
47
-
48
-
49
- def strip_comments(obj):
50
- """Drop `_comment*` keys (documentation) recursively so they never diff."""
51
- if isinstance(obj, dict):
52
- return {k: strip_comments(v) for k, v in obj.items() if not str(k).startswith("_comment")}
53
- if isinstance(obj, list):
54
- return [strip_comments(x) for x in obj]
55
- return obj
56
-
57
-
58
- # ------------------------------------------------------------------- normalize
59
- def _drop_path(obj, parts):
60
- """Remove one ignore-path (list of segments, '*' = wildcard) from obj in place."""
61
- if not parts or obj is None:
62
- return
63
- seg, rest = parts[0], parts[1:]
64
- if isinstance(obj, dict):
65
- keys = list(obj.keys()) if seg == "*" else ([seg] if seg in obj else [])
66
- for k in keys:
67
- if not rest:
68
- obj.pop(k, None)
69
- else:
70
- _drop_path(obj[k], rest)
71
- elif isinstance(obj, list):
72
- items = obj if seg in ("*", "[*]") else []
73
- if not items and seg.isdigit() and int(seg) < len(obj):
74
- items = [obj[int(seg)]]
75
- for it in items:
76
- if rest:
77
- _drop_path(it, rest)
78
-
79
-
80
- def _parse_path(p):
81
- # "$.a.b[*].c" / "$.*.timestamp" -> ["a","b","*","c"]
82
- p = p.strip()
83
- p = p[2:] if p.startswith("$.") else (p[1:] if p.startswith("$") else p)
84
- p = p.replace("[*]", ".*")
85
- return [seg for seg in p.split(".") if seg != ""]
86
-
87
-
88
- def normalize(body, ignore_paths):
89
- """Return a deep copy of body with ignore_paths removed (JSON only)."""
90
- if not isinstance(body, (dict, list)):
91
- return body
92
- clone = json.loads(json.dumps(body))
93
- for ip in ignore_paths or []:
94
- _drop_path(clone, _parse_path(ip))
95
- return clone
96
-
97
-
98
- # ------------------------------------------------------------------ deep diff
99
- def deep_diff(a, b, path="$"):
100
- """List of {path, legacy, candidate} where a (legacy) and b (candidate) differ."""
101
- out = []
102
- if isinstance(a, dict) and isinstance(b, dict):
103
- for k in sorted(set(a) | set(b)):
104
- if k not in a:
105
- out.append({"path": f"{path}.{k}", "legacy": None, "candidate": b[k]})
106
- elif k not in b:
107
- out.append({"path": f"{path}.{k}", "legacy": a[k], "candidate": None})
108
- else:
109
- out += deep_diff(a[k], b[k], f"{path}.{k}")
110
- elif isinstance(a, list) and isinstance(b, list):
111
- if len(a) != len(b):
112
- out.append({"path": f"{path}.length", "legacy": len(a), "candidate": len(b)})
113
- for i in range(min(len(a), len(b))):
114
- out += deep_diff(a[i], b[i], f"{path}[{i}]")
115
- elif a != b:
116
- out.append({"path": path, "legacy": a, "candidate": b})
117
- return out
118
-
119
-
120
- # ------------------------------------------------------------------- invoke
121
- def invoke(side, request, timeout):
122
- kind = (side or {}).get("kind", "http")
123
- if kind == "http":
124
- return _invoke_http(side, request, timeout)
125
- if kind == "shell":
126
- return _invoke_shell(side, request, timeout)
127
- return {"error": f"unknown side kind '{kind}'"}
128
-
129
-
130
- def _invoke_http(side, request, timeout):
131
- base = (side.get("base_url") or "").rstrip("/")
132
- url = base + "/" + str(request.get("path", "")).lstrip("/")
133
- method = str(request.get("method", "GET")).upper()
134
- headers = request.get("headers", {}) or {}
135
- data = None
136
- if request.get("body") is not None and method in ("POST", "PUT", "PATCH", "DELETE"):
137
- data = json.dumps(request["body"]).encode("utf-8")
138
- headers.setdefault("Content-Type", "application/json")
139
- req = urllib.request.Request(url, data=data, method=method, headers=headers)
140
- try:
141
- with urllib.request.urlopen(req, timeout=timeout) as resp:
142
- raw, status = resp.read().decode("utf-8", "replace"), resp.status
143
- except urllib.error.HTTPError as e:
144
- raw, status = e.read().decode("utf-8", "replace"), e.code
145
- except Exception as e:
146
- return {"error": f"{type(e).__name__}: {e}", "url": url}
147
- try:
148
- body = json.loads(raw) if raw.strip() else ""
149
- except json.JSONDecodeError:
150
- body = raw
151
- return {"status": status, "body": body}
152
-
153
-
154
- def _invoke_shell(side, request, timeout):
155
- cmd = side.get("command")
156
- if not cmd:
157
- return {"error": "shell side has no 'command'"}
158
- try:
159
- proc = subprocess.run(cmd, shell=True, input=json.dumps(request),
160
- capture_output=True, text=True, timeout=timeout)
161
- except subprocess.TimeoutExpired:
162
- return {"error": f"timeout after {timeout}s"}
163
- if proc.returncode != 0:
164
- return {"error": (proc.stderr or "").strip()[:500], "status": proc.returncode}
165
- out = (proc.stdout or "").strip()
166
- try:
167
- body = json.loads(out) if out else ""
168
- except json.JSONDecodeError:
169
- body = out
170
- return {"status": 0, "body": body}
171
-
172
-
173
- # ------------------------------------------------------------------- compare
174
- def compare_observation(legacy, candidate, ignore_paths, ignore_status):
175
- """Diff a single observation (one request's outputs) across the two sides."""
176
- if "error" in legacy or "error" in candidate:
177
- return [{"path": "$", "legacy": legacy.get("error", legacy.get("status")),
178
- "candidate": candidate.get("error", candidate.get("status")),
179
- "invoke_error": True}]
180
- diffs = []
181
- if not ignore_status and legacy.get("status") != candidate.get("status"):
182
- diffs.append({"path": "$.status", "legacy": legacy.get("status"),
183
- "candidate": candidate.get("status")})
184
- diffs += deep_diff(normalize(legacy.get("body"), ignore_paths),
185
- normalize(candidate.get("body"), ignore_paths), "$.body")
186
- return diffs
187
-
188
-
189
- def _accepted(case_id, path, deviations):
190
- for d in deviations or []:
191
- if d.get("case") in (case_id, "*") and (d.get("path") == path or d.get("path") == "*"):
192
- return d.get("reason", "approved deviation")
193
- return None
194
-
195
-
196
- # ------------------------------------------------------------------- commands
197
- def cmd_scaffold(args):
198
- parity_dir = os.path.join(args.feature_dir, "parity")
199
- os.makedirs(parity_dir, exist_ok=True)
200
- manifest = os.path.join(parity_dir, "parity-manifest.json")
201
- created = False
202
- if not os.path.exists(manifest):
203
- with open(TEMPLATE, encoding="utf-8") as src:
204
- content = src.read()
205
- agg = os.path.basename(os.path.normpath(args.feature_dir))
206
- content = content.replace('"AGGREGATE"', json.dumps(agg))
207
- with open(manifest, "w", encoding="utf-8") as dst:
208
- dst.write(content)
209
- created = True
210
- print(f"parity manifest {'created' if created else 'exists'}: {manifest}", file=sys.stderr)
211
- print(json.dumps({"manifest_path": manifest, "created": "true" if created else "false"}))
212
- return 0
213
-
214
-
215
- def _sides_configured(m):
216
- for side in (m.get("sides") or {}).values():
217
- side = strip_comments(side)
218
- target = side.get("base_url") if side.get("kind", "http") == "http" else side.get("command")
219
- if not target or PLACEHOLDER.search(str(target)):
220
- return False
221
- return bool(m.get("sides"))
222
-
223
-
224
- def cmd_check(args):
225
- try:
226
- m = strip_comments(load_manifest(args.manifest))
227
- except FileNotFoundError:
228
- print(json.dumps({"ready": "false", "sides_configured": "false", "case_count": 0,
229
- "error": f"manifest not found: {args.manifest}"}))
230
- return 0
231
- except json.JSONDecodeError as e:
232
- print(json.dumps({"ready": "false", "sides_configured": "false", "case_count": 0,
233
- "error": f"manifest is not valid JSON: {e}"}))
234
- return 0
235
- cases = [c for c in (m.get("cases") or []) if not PLACEHOLDER.search(json.dumps(c))
236
- and not str(c.get("id", "")).endswith("EXAMPLE")]
237
- configured = _sides_configured(m)
238
- ready = configured and len(cases) > 0
239
- print(json.dumps({"ready": "true" if ready else "false",
240
- "sides_configured": "true" if configured else "false",
241
- "case_count": len(cases), "error": ""}))
242
- return 0
243
-
244
-
245
- def cmd_run(args):
246
- try:
247
- m = strip_comments(load_manifest(args.manifest))
248
- except (FileNotFoundError, json.JSONDecodeError) as e:
249
- print(json.dumps({"ok": "false", "ready": "false", "total": 0, "matched": 0,
250
- "mismatched": 0, "accepted": 0, "errors": 1, "report_path": "",
251
- "error": str(e)}))
252
- return 0
253
- sides = m.get("sides") or {}
254
- legacy_side, cand_side = strip_comments(sides.get("legacy") or {}), strip_comments(sides.get("candidate") or {})
255
- comp = strip_comments(m.get("compare") or {})
256
- ignore_paths = comp.get("ignore_paths") or []
257
- ignore_status = bool(comp.get("ignore_status"))
258
- timeout = float(comp.get("timeout_seconds") or 30)
259
- deviations = [strip_comments(d) for d in (m.get("expected_deviations") or [])]
260
- cases = [strip_comments(c) for c in (m.get("cases") or [])
261
- if not str(c.get("id", "")).endswith("EXAMPLE")]
262
-
263
- # Don't attempt calls against unconfigured (placeholder) sides — the workflow's
264
- # readiness gate normally stops earlier, but guard here too.
265
- if not _sides_configured(m):
266
- print("parity: sides not configured (placeholder base_url/command) — nothing run",
267
- file=sys.stderr)
268
- print(json.dumps({"ok": "false", "ready": "false", "total": len(cases), "matched": 0,
269
- "mismatched": 0, "accepted": 0, "errors": 0, "report_path": "",
270
- "error": "sides not configured"}))
271
- return 0
272
-
273
- results, n_match, n_mismatch, n_accept, n_err = [], 0, 0, 0, 0
274
- for case in cases:
275
- cid = case.get("id", "?")
276
- case_diffs, accepted, invoke_err = [], [], False
277
- observations = [("request", case.get("request"))]
278
- if case.get("state_probe"):
279
- observations.append(("state", case["state_probe"]))
280
- for label, request in observations:
281
- if not request:
282
- continue
283
- lg = invoke(legacy_side, request, timeout)
284
- cd = invoke(cand_side, request, timeout)
285
- for d in compare_observation(lg, cd, ignore_paths, ignore_status):
286
- d["observation"] = label
287
- if d.get("invoke_error"):
288
- invoke_err = True
289
- case_diffs.append(d)
290
- continue
291
- reason = _accepted(cid, d["path"], deviations)
292
- if reason:
293
- d["accepted_reason"] = reason
294
- accepted.append(d)
295
- else:
296
- case_diffs.append(d)
297
- if invoke_err:
298
- n_err += 1
299
- status = "error"
300
- elif case_diffs:
301
- n_mismatch += 1
302
- status = "mismatch"
303
- else:
304
- n_match += 1
305
- status = "match"
306
- n_accept += len(accepted)
307
- results.append({"id": cid, "description": case.get("description", ""),
308
- "spec_ids": case.get("spec_ids", []), "status": status,
309
- "diffs": case_diffs, "accepted": accepted})
310
-
311
- ok = (n_mismatch == 0 and n_err == 0 and len(cases) > 0)
312
- report_path = args.out or os.path.join(os.path.dirname(args.manifest), "parity-report.md")
313
- _write_report(report_path, m, results, n_match, n_mismatch, n_accept, n_err)
314
-
315
- print(f"parity: {n_match} match · {n_mismatch} mismatch · {n_err} error · "
316
- f"{n_accept} accepted-deviation(s) over {len(cases)} case(s)", file=sys.stderr)
317
- print(json.dumps({"ok": "true" if ok else "false", "ready": "true" if cases else "false",
318
- "total": len(cases), "matched": n_match, "mismatched": n_mismatch,
319
- "accepted": n_accept, "errors": n_err, "report_path": report_path}))
320
- return 0
321
-
322
-
323
- def _write_report(path, m, results, n_match, n_mismatch, n_accept, n_err):
324
- from datetime import datetime, timezone
325
- lines = [f"# Parity Report — {m.get('aggregate', '?')}",
326
- f"_generated {datetime.now(timezone.utc).date().isoformat()} · "
327
- f"baseline {m.get('baseline_commit', '-')}_", "",
328
- f"**verdict:** {'PARITY HOLDS ✓' if (n_mismatch == 0 and n_err == 0) else 'PARITY FAILS ✗'} · "
329
- f"{n_match} match · {n_mismatch} mismatch · {n_err} error · {n_accept} accepted deviation(s)", "",
330
- "| case | spec ids | status | diffs |", "|---|---|---|---|"]
331
- for r in results:
332
- lines.append(f"| {r['id']} {r['description']} | {', '.join(r['spec_ids'])} | "
333
- f"{r['status']} | {len(r['diffs'])} |")
334
- fails = [r for r in results if r["diffs"]]
335
- if fails:
336
- lines += ["", "## Unaccepted diffs (block cutover)"]
337
- for r in fails:
338
- lines.append(f"### {r['id']} — {r['description']}")
339
- for d in r["diffs"]:
340
- tag = " (INVOKE ERROR)" if d.get("invoke_error") else ""
341
- lines.append(f"- `{d['path']}`{tag} [{d.get('observation','request')}]: "
342
- f"legacy=`{json.dumps(d['legacy'])}` vs candidate=`{json.dumps(d['candidate'])}`")
343
- accepted = [(r, d) for r in results for d in r["accepted"]]
344
- if accepted:
345
- lines += ["", "## Accepted deviations (intended — must be MODIFIED spec items via CR)"]
346
- for r, d in accepted:
347
- lines.append(f"- {r['id']} `{d['path']}`: {d.get('accepted_reason')}")
348
- lines += ["", "## How to proceed",
349
- "- Each unaccepted diff is either a REGRESSION (fix the candidate to match legacy) or an "
350
- "INTENDED change (add it to `expected_deviations` AND record it as a MODIFIED spec item via "
351
- "/aw-spec-orch specify). Then re-run /aw-spec-orch parity.",
352
- "- Parity holds → the seam is cutover-ready: route it to the candidate; roll back by re-routing."]
353
- os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
354
- with open(path, "w", encoding="utf-8") as f:
355
- f.write("\n".join(lines) + "\n")
356
-
357
-
358
- def main():
359
- ap = argparse.ArgumentParser(description="Brownfield parity runner (cutover gate).")
360
- sub = ap.add_subparsers(dest="cmd", required=True)
361
-
362
- ps = sub.add_parser("scaffold", help="Create specs/<Agg>/parity/parity-manifest.json from the template.")
363
- ps.add_argument("--feature-dir", required=True)
364
- ps.set_defaults(func=cmd_scaffold)
365
-
366
- pc = sub.add_parser("check", help="Report whether the manifest is ready to run (sides configured + cases).")
367
- pc.add_argument("--manifest", required=True)
368
- pc.set_defaults(func=cmd_check)
369
-
370
- pr = sub.add_parser("run", help="Run every case against both sides, diff, write the report.")
371
- pr.add_argument("--manifest", required=True)
372
- pr.add_argument("--out", help="Report path (default: <manifest dir>/parity-report.md).")
373
- pr.set_defaults(func=cmd_run)
374
-
375
- args = ap.parse_args()
376
- return args.func(args)
377
-
378
-
379
- if __name__ == "__main__":
380
- sys.exit(main())