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.
- package/assets/.appweaver/DESIGN-FORMAT.md +200 -0
- package/assets/.appweaver/ORCH-DIALECT.md +156 -0
- package/assets/.appweaver/RULE-PARTITION.md +175 -0
- package/assets/.appweaver/STACK-PROFILES.md +461 -0
- package/assets/.appweaver/ai-rules.md +180 -0
- package/assets/.appweaver/domain.json +1 -0
- package/assets/.appweaver/extensions/.registry +19 -0
- package/assets/.appweaver/extensions/agent-context/README.md +57 -0
- package/assets/.appweaver/extensions/agent-context/agent-context-config.yml +4 -0
- package/assets/.appweaver/extensions/agent-context/commands/aw-spec.agent-context.update.md +26 -0
- package/assets/.appweaver/extensions/agent-context/extension.yml +37 -0
- package/assets/.appweaver/extensions/agent-context/scripts/bash/update-agent-context.sh +201 -0
- package/assets/.appweaver/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +237 -0
- package/assets/.appweaver/extensions.yml +16 -0
- package/assets/.appweaver/feature.json +3 -0
- package/assets/.appweaver/init-options.json +9 -0
- package/assets/.appweaver/integration.json +15 -0
- package/assets/.appweaver/integrations/aw-spec.manifest.json +9 -0
- package/assets/.appweaver/interpreter/run-skill.py +1176 -0
- package/assets/.appweaver/memory/constitution.md +50 -0
- package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +6 -0
- package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +94 -0
- package/assets/.appweaver/orchestrator/README.md +300 -0
- package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +74 -0
- package/assets/.appweaver/orchestrator/adapters/google_antigravity.py +74 -0
- package/assets/.appweaver/orchestrator/modules/delta-design.orch.yaml +105 -0
- package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +101 -0
- package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +90 -0
- package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +126 -0
- package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +123 -0
- package/assets/.appweaver/orchestrator/orchestrator.py +1721 -0
- package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +40 -0
- package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +101 -0
- package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +245 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +63 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +48 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +59 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-file.skeleton.md +39 -0
- package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +63 -0
- package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +116 -0
- package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +106 -0
- package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +54 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +125 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +123 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +294 -0
- package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +160 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +270 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +934 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +251 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +265 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +437 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +197 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +475 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +181 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +727 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +100 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +69 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +85 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-design-ui.orch.yaml +386 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-design.orch.yaml +382 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +462 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +265 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +253 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-ui.orch.yaml +267 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +287 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +238 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1822 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +293 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +194 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +83 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +128 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +296 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +439 -0
- package/assets/.appweaver/scripts/bash/aw-inventory.sh +17 -0
- package/assets/.appweaver/scripts/bash/aw-parity.sh +13 -0
- package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +13 -0
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +2626 -0
- package/assets/.appweaver/scripts/bash/aw-xref.sh +15 -0
- package/assets/.appweaver/scripts/bash/common.sh +533 -0
- package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +17 -0
- package/assets/.appweaver/scripts/powershell/aw-parity.ps1 +13 -0
- package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +13 -0
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +2232 -0
- package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +15 -0
- package/assets/.appweaver/scripts/python/aw-component-catalog.py +121 -0
- package/assets/.appweaver/scripts/python/aw-inventory.py +1180 -0
- package/assets/.appweaver/scripts/python/aw-parity.py +380 -0
- package/assets/.appweaver/scripts/python/aw-rev-constitution.py +329 -0
- package/assets/.appweaver/scripts/python/aw-stack.py +1251 -0
- package/assets/.appweaver/scripts/python/aw-xref.py +1330 -0
- package/assets/.appweaver/scripts/python/validate_okf.py +352 -0
- package/assets/.appweaver/stack-profiles/angular.json +16 -0
- package/assets/.appweaver/stack-profiles/blazor-wasm.json +17 -0
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +15 -0
- package/assets/.appweaver/stack-profiles/go-chi.json +14 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +16 -0
- package/assets/.appweaver/stack-profiles/nextjs.json +16 -0
- package/assets/.appweaver/stack-profiles/node-express.json +15 -0
- package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
- package/assets/.appweaver/stack-profiles/react-vite.json +17 -0
- package/assets/.appweaver/stack-profiles/sveltekit.json +16 -0
- package/assets/.appweaver/stack-profiles/vue-vite.json +17 -0
- package/assets/.appweaver/stacks.json +3 -0
- package/assets/.appweaver/templates/constitution-domain-template.md +116 -0
- package/assets/.appweaver/templates/constitution-technical-template.md +108 -0
- package/assets/.appweaver/templates/constitution-ui-template.md +116 -0
- package/assets/.appweaver/templates/delta-design-template.md +69 -0
- package/assets/.appweaver/templates/delta-spec-template.md +63 -0
- package/assets/.appweaver/templates/design-template.md +383 -0
- package/assets/.appweaver/templates/okf-concept-template.md +114 -0
- package/assets/.appweaver/templates/parity-manifest-template.json +49 -0
- package/assets/.appweaver/templates/source-map-template.md +85 -0
- package/assets/.appweaver/templates/spec-template.md +309 -0
- package/assets/.appweaver/templates/ui-design-template.md +273 -0
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.pack.txt +25248 -0
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +5726 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-foundation.ps1 +145 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-foundation.sh +196 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.ps1 +157 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.sh +209 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.ps1 +126 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.sh +217 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.ps1 +129 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.sh +218 -0
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.pack.txt +19318 -0
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2833 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-foundation.ps1 +145 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-foundation.sh +196 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.ps1 +157 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.sh +209 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.ps1 +126 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.sh +217 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.ps1 +129 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.sh +218 -0
- package/assets/foundations/dotnet/components/Email/README.md +119 -0
- package/assets/foundations/dotnet/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +84 -0
- package/assets/foundations/dotnet/components/Email/integration.md +300 -0
- package/assets/foundations/dotnet/components/Jobs/README.md +113 -0
- package/assets/foundations/dotnet/components/Jobs/integration.md +259 -0
- package/assets/foundations/dotnet/components/QrGenerator/README.md +85 -0
- package/assets/foundations/dotnet/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +57 -0
- package/assets/foundations/dotnet/components/QrGenerator/integration.md +230 -0
- package/assets/foundations/dotnet/components/README.md +74 -0
- package/assets/guardrails/README.md +222 -0
- package/assets/guardrails/domain/healthcare/ai-rules.domain.md +34 -0
- package/assets/guardrails/domain/healthcare/api-versioning.domain.md +17 -0
- package/assets/guardrails/domain/healthcare/auditing.domain.md +97 -0
- package/assets/guardrails/domain/healthcare/authorization.domain.md +26 -0
- package/assets/guardrails/domain/healthcare/clinical-terms.md +20 -0
- package/assets/guardrails/domain/healthcare/coding-standards.domain.md +26 -0
- package/assets/guardrails/domain/healthcare/consent.md +19 -0
- package/assets/guardrails/domain/healthcare/cqrs.domain.md +12 -0
- package/assets/guardrails/domain/healthcare/data-classification.domain.md +28 -0
- package/assets/guardrails/domain/healthcare/dependency-policy.domain.md +11 -0
- package/assets/guardrails/domain/healthcare/dicom.md +15 -0
- package/assets/guardrails/domain/healthcare/domain-constitution.md +119 -0
- package/assets/guardrails/domain/healthcare/domain-driven-design.domain.md +28 -0
- package/assets/guardrails/domain/healthcare/eventing.domain.md +18 -0
- package/assets/guardrails/domain/healthcare/fhir.md +36 -0
- package/assets/guardrails/domain/healthcare/folder-structure.domain.md +25 -0
- package/assets/guardrails/domain/healthcare/hipaa.md +34 -0
- package/assets/guardrails/domain/healthcare/hl7.md +15 -0
- package/assets/guardrails/domain/healthcare/ihe-conformance.md +31 -0
- package/assets/guardrails/domain/healthcare/integration-terms.md +21 -0
- package/assets/guardrails/domain/healthcare/persistence.domain.md +20 -0
- package/assets/guardrails/domain/healthcare/terminology.md +30 -0
- package/assets/guardrails/domain/healthcare/testing.domain.md +15 -0
- package/assets/guardrails/domain/healthcare/ux/accessibility-standards.md +36 -0
- package/assets/guardrails/domain/healthcare/ux/content-design.md +32 -0
- package/assets/guardrails/domain/healthcare/ux/interaction-states.md +44 -0
- package/assets/guardrails/domain/healthcare/ux/journey-mapping.md +40 -0
- package/assets/guardrails/domain/healthcare/vertical-slices.domain.md +18 -0
- package/assets/guardrails/domain/healthcare/zero-trust.domain.md +12 -0
- package/assets/guardrails/domain/pharma/ai-rules.domain.md +32 -0
- package/assets/guardrails/domain/pharma/api-versioning.domain.md +20 -0
- package/assets/guardrails/domain/pharma/atc.md +22 -0
- package/assets/guardrails/domain/pharma/auditing.domain.md +90 -0
- package/assets/guardrails/domain/pharma/authorization.domain.md +25 -0
- package/assets/guardrails/domain/pharma/coding-standards.domain.md +28 -0
- package/assets/guardrails/domain/pharma/cqrs.domain.md +16 -0
- package/assets/guardrails/domain/pharma/data-classification.domain.md +24 -0
- package/assets/guardrails/domain/pharma/data-provenance.md +21 -0
- package/assets/guardrails/domain/pharma/dependency-policy.domain.md +11 -0
- package/assets/guardrails/domain/pharma/domain-constitution.md +113 -0
- package/assets/guardrails/domain/pharma/domain-driven-design.domain.md +36 -0
- package/assets/guardrails/domain/pharma/domain-requirements.sample.md +103 -0
- package/assets/guardrails/domain/pharma/eventing.domain.md +18 -0
- package/assets/guardrails/domain/pharma/fhir.md +31 -0
- package/assets/guardrails/domain/pharma/folder-structure.domain.md +26 -0
- package/assets/guardrails/domain/pharma/hl7.md +17 -0
- package/assets/guardrails/domain/pharma/idmp.md +26 -0
- package/assets/guardrails/domain/pharma/integration-terms.md +21 -0
- package/assets/guardrails/domain/pharma/ndc.md +28 -0
- package/assets/guardrails/domain/pharma/persistence.domain.md +23 -0
- package/assets/guardrails/domain/pharma/pharma-terms.md +27 -0
- package/assets/guardrails/domain/pharma/product-lifecycle.domain.md +30 -0
- package/assets/guardrails/domain/pharma/regulatory-compliance.md +19 -0
- package/assets/guardrails/domain/pharma/terminology.md +32 -0
- package/assets/guardrails/domain/pharma/testing.domain.md +19 -0
- package/assets/guardrails/domain/pharma/vertical-slices.domain.md +17 -0
- package/assets/guardrails/domain/pharma/zero-trust.domain.md +12 -0
- package/assets/guardrails/projects/dotnet-default-project/domain/domain-requirements.md +58 -0
- package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +113 -0
- package/assets/guardrails/projects/dotnet-default-project/frontend/ui-requirements.md +43 -0
- package/assets/guardrails/projects/dotnet-default-project/service/technical-requirements.md +40 -0
- package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +263 -0
- package/assets/guardrails/requirement-spec.txt +27 -0
- package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +184 -0
- package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +44 -0
- package/assets/guardrails/technology/dotnet-webapi/auditing.md +96 -0
- package/assets/guardrails/technology/dotnet-webapi/authorization-matrix.md +14 -0
- package/assets/guardrails/technology/dotnet-webapi/authorization.md +39 -0
- package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +54 -0
- package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +87 -0
- package/assets/guardrails/technology/dotnet-webapi/cqrs.md +63 -0
- package/assets/guardrails/technology/dotnet-webapi/data-classification.md +38 -0
- package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +40 -0
- package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +91 -0
- package/assets/guardrails/technology/dotnet-webapi/eventing.md +57 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/api-versioning.md +75 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/auditing.md +84 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/authorization.md +68 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/clean-architecture.md +37 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +97 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +141 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/eventing.md +46 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/persistence.md +207 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/vertical-slices.md +151 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/zero-trust.md +48 -0
- package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +61 -0
- package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +47 -0
- package/assets/guardrails/technology/dotnet-webapi/persistence.md +82 -0
- package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +162 -0
- package/assets/guardrails/technology/dotnet-webapi/testing.md +55 -0
- package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +70 -0
- package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +48 -0
- package/assets/guardrails/technology/java-spring/README.md +62 -0
- package/assets/guardrails/technology/nextjs/accessibility-implementation.md +56 -0
- package/assets/guardrails/technology/nextjs/api-client.md +56 -0
- package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +174 -0
- package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +203 -0
- package/assets/guardrails/technology/nextjs/auth-session.md +68 -0
- package/assets/guardrails/technology/nextjs/client-security.md +69 -0
- package/assets/guardrails/technology/nextjs/component-architecture.md +60 -0
- package/assets/guardrails/technology/nextjs/component-conventions.md +58 -0
- package/assets/guardrails/technology/nextjs/design-system.md +60 -0
- package/assets/guardrails/technology/nextjs/error-handling.md +72 -0
- package/assets/guardrails/technology/nextjs/forms-and-validation.md +75 -0
- package/assets/guardrails/technology/nextjs/frontend-testing.md +94 -0
- package/assets/guardrails/technology/nextjs/i18n.md +57 -0
- package/assets/guardrails/technology/nextjs/observability.md +67 -0
- package/assets/guardrails/technology/nextjs/performance-budgets.md +66 -0
- package/assets/guardrails/technology/nextjs/routing-navigation.md +52 -0
- package/assets/guardrails/technology/nextjs/state-management.md +70 -0
- package/assets/guardrails/technology/nextjs/ui-constitution.md +166 -0
- package/package.json +1 -1
- 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())
|