aw-spec-cli 0.1.8 → 0.1.9

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 (257) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +200 -0
  2. package/assets/.appweaver/ORCH-DIALECT.md +156 -0
  3. package/assets/.appweaver/RULE-PARTITION.md +175 -0
  4. package/assets/.appweaver/STACK-PROFILES.md +461 -0
  5. package/assets/.appweaver/ai-rules.md +180 -0
  6. package/assets/.appweaver/domain.json +1 -0
  7. package/assets/.appweaver/extensions/.registry +19 -0
  8. package/assets/.appweaver/extensions/agent-context/README.md +57 -0
  9. package/assets/.appweaver/extensions/agent-context/agent-context-config.yml +4 -0
  10. package/assets/.appweaver/extensions/agent-context/commands/aw-spec.agent-context.update.md +26 -0
  11. package/assets/.appweaver/extensions/agent-context/extension.yml +37 -0
  12. package/assets/.appweaver/extensions/agent-context/scripts/bash/update-agent-context.sh +201 -0
  13. package/assets/.appweaver/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +237 -0
  14. package/assets/.appweaver/extensions.yml +16 -0
  15. package/assets/.appweaver/feature.json +3 -0
  16. package/assets/.appweaver/init-options.json +9 -0
  17. package/assets/.appweaver/integration.json +15 -0
  18. package/assets/.appweaver/integrations/aw-spec.manifest.json +9 -0
  19. package/assets/.appweaver/interpreter/run-skill.py +1176 -0
  20. package/assets/.appweaver/memory/constitution.md +50 -0
  21. package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +6 -0
  22. package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +94 -0
  23. package/assets/.appweaver/orchestrator/README.md +300 -0
  24. package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +74 -0
  25. package/assets/.appweaver/orchestrator/adapters/google_antigravity.py +74 -0
  26. package/assets/.appweaver/orchestrator/modules/delta-design.orch.yaml +105 -0
  27. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +101 -0
  28. package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +90 -0
  29. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +126 -0
  30. package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +123 -0
  31. package/assets/.appweaver/orchestrator/orchestrator.py +1721 -0
  32. package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +40 -0
  33. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +101 -0
  34. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +245 -0
  35. package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +63 -0
  36. package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +48 -0
  37. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +59 -0
  38. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-file.skeleton.md +39 -0
  39. package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +63 -0
  40. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +116 -0
  41. package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +106 -0
  42. package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +54 -0
  43. package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +125 -0
  44. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +123 -0
  45. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +294 -0
  46. package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +160 -0
  47. package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +270 -0
  48. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +934 -0
  49. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +251 -0
  50. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +265 -0
  51. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +437 -0
  52. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +197 -0
  53. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +475 -0
  54. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +181 -0
  55. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +727 -0
  56. package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +100 -0
  57. package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +69 -0
  58. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +85 -0
  59. package/assets/.appweaver/orchestrator/workflows/aw-spec-design-ui.orch.yaml +386 -0
  60. package/assets/.appweaver/orchestrator/workflows/aw-spec-design.orch.yaml +382 -0
  61. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +462 -0
  62. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +265 -0
  63. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +253 -0
  64. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-ui.orch.yaml +267 -0
  65. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +287 -0
  66. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +238 -0
  67. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1822 -0
  68. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +293 -0
  69. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +194 -0
  70. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +83 -0
  71. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +128 -0
  72. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +296 -0
  73. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +439 -0
  74. package/assets/.appweaver/scripts/bash/aw-inventory.sh +17 -0
  75. package/assets/.appweaver/scripts/bash/aw-parity.sh +13 -0
  76. package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +13 -0
  77. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +2626 -0
  78. package/assets/.appweaver/scripts/bash/aw-xref.sh +15 -0
  79. package/assets/.appweaver/scripts/bash/common.sh +533 -0
  80. package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +17 -0
  81. package/assets/.appweaver/scripts/powershell/aw-parity.ps1 +13 -0
  82. package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +13 -0
  83. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +2232 -0
  84. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +15 -0
  85. package/assets/.appweaver/scripts/python/aw-component-catalog.py +121 -0
  86. package/assets/.appweaver/scripts/python/aw-inventory.py +1180 -0
  87. package/assets/.appweaver/scripts/python/aw-parity.py +380 -0
  88. package/assets/.appweaver/scripts/python/aw-rev-constitution.py +329 -0
  89. package/assets/.appweaver/scripts/python/aw-stack.py +1251 -0
  90. package/assets/.appweaver/scripts/python/aw-xref.py +1330 -0
  91. package/assets/.appweaver/scripts/python/validate_okf.py +352 -0
  92. package/assets/.appweaver/stack-profiles/angular.json +16 -0
  93. package/assets/.appweaver/stack-profiles/blazor-wasm.json +17 -0
  94. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +15 -0
  95. package/assets/.appweaver/stack-profiles/go-chi.json +14 -0
  96. package/assets/.appweaver/stack-profiles/java-spring.json +16 -0
  97. package/assets/.appweaver/stack-profiles/nextjs.json +16 -0
  98. package/assets/.appweaver/stack-profiles/node-express.json +15 -0
  99. package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
  100. package/assets/.appweaver/stack-profiles/react-vite.json +17 -0
  101. package/assets/.appweaver/stack-profiles/sveltekit.json +16 -0
  102. package/assets/.appweaver/stack-profiles/vue-vite.json +17 -0
  103. package/assets/.appweaver/stacks.json +3 -0
  104. package/assets/.appweaver/templates/constitution-domain-template.md +116 -0
  105. package/assets/.appweaver/templates/constitution-technical-template.md +108 -0
  106. package/assets/.appweaver/templates/constitution-ui-template.md +116 -0
  107. package/assets/.appweaver/templates/delta-design-template.md +69 -0
  108. package/assets/.appweaver/templates/delta-spec-template.md +63 -0
  109. package/assets/.appweaver/templates/design-template.md +383 -0
  110. package/assets/.appweaver/templates/okf-concept-template.md +114 -0
  111. package/assets/.appweaver/templates/parity-manifest-template.json +49 -0
  112. package/assets/.appweaver/templates/source-map-template.md +85 -0
  113. package/assets/.appweaver/templates/spec-template.md +309 -0
  114. package/assets/.appweaver/templates/ui-design-template.md +273 -0
  115. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.pack.txt +25248 -0
  116. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +5726 -0
  117. package/assets/foundations/dotnet/ModularMonolith/pack-foundation.ps1 +145 -0
  118. package/assets/foundations/dotnet/ModularMonolith/pack-foundation.sh +196 -0
  119. package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.ps1 +157 -0
  120. package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.sh +209 -0
  121. package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.ps1 +126 -0
  122. package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.sh +217 -0
  123. package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.ps1 +129 -0
  124. package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.sh +218 -0
  125. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.pack.txt +19318 -0
  126. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2833 -0
  127. package/assets/foundations/dotnet/VerticalSlice/pack-foundation.ps1 +145 -0
  128. package/assets/foundations/dotnet/VerticalSlice/pack-foundation.sh +196 -0
  129. package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.ps1 +157 -0
  130. package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.sh +209 -0
  131. package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.ps1 +126 -0
  132. package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.sh +217 -0
  133. package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.ps1 +129 -0
  134. package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.sh +218 -0
  135. package/assets/foundations/dotnet/components/Email/README.md +119 -0
  136. package/assets/foundations/dotnet/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +84 -0
  137. package/assets/foundations/dotnet/components/Email/integration.md +300 -0
  138. package/assets/foundations/dotnet/components/Jobs/README.md +113 -0
  139. package/assets/foundations/dotnet/components/Jobs/integration.md +259 -0
  140. package/assets/foundations/dotnet/components/QrGenerator/README.md +85 -0
  141. package/assets/foundations/dotnet/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +57 -0
  142. package/assets/foundations/dotnet/components/QrGenerator/integration.md +230 -0
  143. package/assets/foundations/dotnet/components/README.md +74 -0
  144. package/assets/guardrails/README.md +222 -0
  145. package/assets/guardrails/domain/healthcare/ai-rules.domain.md +34 -0
  146. package/assets/guardrails/domain/healthcare/api-versioning.domain.md +17 -0
  147. package/assets/guardrails/domain/healthcare/auditing.domain.md +97 -0
  148. package/assets/guardrails/domain/healthcare/authorization.domain.md +26 -0
  149. package/assets/guardrails/domain/healthcare/clinical-terms.md +20 -0
  150. package/assets/guardrails/domain/healthcare/coding-standards.domain.md +26 -0
  151. package/assets/guardrails/domain/healthcare/consent.md +19 -0
  152. package/assets/guardrails/domain/healthcare/cqrs.domain.md +12 -0
  153. package/assets/guardrails/domain/healthcare/data-classification.domain.md +28 -0
  154. package/assets/guardrails/domain/healthcare/dependency-policy.domain.md +11 -0
  155. package/assets/guardrails/domain/healthcare/dicom.md +15 -0
  156. package/assets/guardrails/domain/healthcare/domain-constitution.md +119 -0
  157. package/assets/guardrails/domain/healthcare/domain-driven-design.domain.md +28 -0
  158. package/assets/guardrails/domain/healthcare/eventing.domain.md +18 -0
  159. package/assets/guardrails/domain/healthcare/fhir.md +36 -0
  160. package/assets/guardrails/domain/healthcare/folder-structure.domain.md +25 -0
  161. package/assets/guardrails/domain/healthcare/hipaa.md +34 -0
  162. package/assets/guardrails/domain/healthcare/hl7.md +15 -0
  163. package/assets/guardrails/domain/healthcare/ihe-conformance.md +31 -0
  164. package/assets/guardrails/domain/healthcare/integration-terms.md +21 -0
  165. package/assets/guardrails/domain/healthcare/persistence.domain.md +20 -0
  166. package/assets/guardrails/domain/healthcare/terminology.md +30 -0
  167. package/assets/guardrails/domain/healthcare/testing.domain.md +15 -0
  168. package/assets/guardrails/domain/healthcare/ux/accessibility-standards.md +36 -0
  169. package/assets/guardrails/domain/healthcare/ux/content-design.md +32 -0
  170. package/assets/guardrails/domain/healthcare/ux/interaction-states.md +44 -0
  171. package/assets/guardrails/domain/healthcare/ux/journey-mapping.md +40 -0
  172. package/assets/guardrails/domain/healthcare/vertical-slices.domain.md +18 -0
  173. package/assets/guardrails/domain/healthcare/zero-trust.domain.md +12 -0
  174. package/assets/guardrails/domain/pharma/ai-rules.domain.md +32 -0
  175. package/assets/guardrails/domain/pharma/api-versioning.domain.md +20 -0
  176. package/assets/guardrails/domain/pharma/atc.md +22 -0
  177. package/assets/guardrails/domain/pharma/auditing.domain.md +90 -0
  178. package/assets/guardrails/domain/pharma/authorization.domain.md +25 -0
  179. package/assets/guardrails/domain/pharma/coding-standards.domain.md +28 -0
  180. package/assets/guardrails/domain/pharma/cqrs.domain.md +16 -0
  181. package/assets/guardrails/domain/pharma/data-classification.domain.md +24 -0
  182. package/assets/guardrails/domain/pharma/data-provenance.md +21 -0
  183. package/assets/guardrails/domain/pharma/dependency-policy.domain.md +11 -0
  184. package/assets/guardrails/domain/pharma/domain-constitution.md +113 -0
  185. package/assets/guardrails/domain/pharma/domain-driven-design.domain.md +36 -0
  186. package/assets/guardrails/domain/pharma/domain-requirements.sample.md +103 -0
  187. package/assets/guardrails/domain/pharma/eventing.domain.md +18 -0
  188. package/assets/guardrails/domain/pharma/fhir.md +31 -0
  189. package/assets/guardrails/domain/pharma/folder-structure.domain.md +26 -0
  190. package/assets/guardrails/domain/pharma/hl7.md +17 -0
  191. package/assets/guardrails/domain/pharma/idmp.md +26 -0
  192. package/assets/guardrails/domain/pharma/integration-terms.md +21 -0
  193. package/assets/guardrails/domain/pharma/ndc.md +28 -0
  194. package/assets/guardrails/domain/pharma/persistence.domain.md +23 -0
  195. package/assets/guardrails/domain/pharma/pharma-terms.md +27 -0
  196. package/assets/guardrails/domain/pharma/product-lifecycle.domain.md +30 -0
  197. package/assets/guardrails/domain/pharma/regulatory-compliance.md +19 -0
  198. package/assets/guardrails/domain/pharma/terminology.md +32 -0
  199. package/assets/guardrails/domain/pharma/testing.domain.md +19 -0
  200. package/assets/guardrails/domain/pharma/vertical-slices.domain.md +17 -0
  201. package/assets/guardrails/domain/pharma/zero-trust.domain.md +12 -0
  202. package/assets/guardrails/projects/dotnet-default-project/domain/domain-requirements.md +58 -0
  203. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +113 -0
  204. package/assets/guardrails/projects/dotnet-default-project/frontend/ui-requirements.md +43 -0
  205. package/assets/guardrails/projects/dotnet-default-project/service/technical-requirements.md +40 -0
  206. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +263 -0
  207. package/assets/guardrails/requirement-spec.txt +27 -0
  208. package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +184 -0
  209. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +44 -0
  210. package/assets/guardrails/technology/dotnet-webapi/auditing.md +96 -0
  211. package/assets/guardrails/technology/dotnet-webapi/authorization-matrix.md +14 -0
  212. package/assets/guardrails/technology/dotnet-webapi/authorization.md +39 -0
  213. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +54 -0
  214. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +87 -0
  215. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +63 -0
  216. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +38 -0
  217. package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +40 -0
  218. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +91 -0
  219. package/assets/guardrails/technology/dotnet-webapi/eventing.md +57 -0
  220. package/assets/guardrails/technology/dotnet-webapi/examples/api-versioning.md +75 -0
  221. package/assets/guardrails/technology/dotnet-webapi/examples/auditing.md +84 -0
  222. package/assets/guardrails/technology/dotnet-webapi/examples/authorization.md +68 -0
  223. package/assets/guardrails/technology/dotnet-webapi/examples/clean-architecture.md +37 -0
  224. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +97 -0
  225. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +141 -0
  226. package/assets/guardrails/technology/dotnet-webapi/examples/eventing.md +46 -0
  227. package/assets/guardrails/technology/dotnet-webapi/examples/persistence.md +207 -0
  228. package/assets/guardrails/technology/dotnet-webapi/examples/vertical-slices.md +151 -0
  229. package/assets/guardrails/technology/dotnet-webapi/examples/zero-trust.md +48 -0
  230. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +61 -0
  231. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +47 -0
  232. package/assets/guardrails/technology/dotnet-webapi/persistence.md +82 -0
  233. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +162 -0
  234. package/assets/guardrails/technology/dotnet-webapi/testing.md +55 -0
  235. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +70 -0
  236. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +48 -0
  237. package/assets/guardrails/technology/java-spring/README.md +62 -0
  238. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +56 -0
  239. package/assets/guardrails/technology/nextjs/api-client.md +56 -0
  240. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +174 -0
  241. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +203 -0
  242. package/assets/guardrails/technology/nextjs/auth-session.md +68 -0
  243. package/assets/guardrails/technology/nextjs/client-security.md +69 -0
  244. package/assets/guardrails/technology/nextjs/component-architecture.md +60 -0
  245. package/assets/guardrails/technology/nextjs/component-conventions.md +58 -0
  246. package/assets/guardrails/technology/nextjs/design-system.md +60 -0
  247. package/assets/guardrails/technology/nextjs/error-handling.md +72 -0
  248. package/assets/guardrails/technology/nextjs/forms-and-validation.md +75 -0
  249. package/assets/guardrails/technology/nextjs/frontend-testing.md +94 -0
  250. package/assets/guardrails/technology/nextjs/i18n.md +57 -0
  251. package/assets/guardrails/technology/nextjs/observability.md +67 -0
  252. package/assets/guardrails/technology/nextjs/performance-budgets.md +66 -0
  253. package/assets/guardrails/technology/nextjs/routing-navigation.md +52 -0
  254. package/assets/guardrails/technology/nextjs/state-management.md +70 -0
  255. package/assets/guardrails/technology/nextjs/ui-constitution.md +166 -0
  256. package/package.json +1 -1
  257. package/assets/docs/pharma-constitution.my-ref.md +0 -86
@@ -0,0 +1,380 @@
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.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
+ "/appweaver specify). Then re-run /appweaver 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())