aw-spec-cli 0.1.4 → 0.1.5
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/.agents/agent.json +28 -0
- package/assets/.agents/skills/aw-spec-orch/SKILL.md +56 -0
- package/assets/.agents/skills/aw-spec-orch-demo/SKILL.md +59 -0
- package/assets/.aw-spec/DESIGN-FORMAT.md +151 -0
- package/assets/.aw-spec/ORCH-DIALECT.md +156 -0
- package/assets/.aw-spec/RULE-PARTITION.md +126 -0
- package/assets/.aw-spec/extensions/agent-context/extension.yml +12 -9
- package/assets/.aw-spec/extensions.yml +12 -19
- package/assets/.aw-spec/integrations/aw-spec.manifest.json +1 -9
- package/assets/.aw-spec/interpreter/run-skill.py +1158 -0
- package/assets/.aw-spec/orchestrator/AGENT-PROFILE.md +94 -0
- package/assets/.aw-spec/orchestrator/README.md +300 -0
- package/assets/.aw-spec/orchestrator/adapters/claude_agent_sdk.py +74 -0
- package/assets/.aw-spec/orchestrator/adapters/google_antigravity.py +74 -0
- package/assets/.aw-spec/orchestrator/modules/delta-design.orch.yaml +105 -0
- package/assets/.aw-spec/orchestrator/modules/delta-spec.orch.yaml +79 -0
- package/assets/.aw-spec/orchestrator/modules/domain-select.orch.yaml +81 -0
- package/assets/.aw-spec/orchestrator/modules/tech-select.orch.yaml +75 -0
- package/assets/.aw-spec/orchestrator/orchestrator.py +1721 -0
- package/assets/.aw-spec/orchestrator/rev-templates/ai-rules.reference.md +40 -0
- package/assets/.aw-spec/orchestrator/rev-templates/comandrulemap.skeleton.md +59 -0
- package/assets/.aw-spec/orchestrator/rev-templates/commandtechrulemap.skeleton.md +97 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-constitution.skeleton.md +61 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-requirements.skeleton.md +48 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-rule-file.skeleton.md +39 -0
- package/assets/.aw-spec/orchestrator/rev-templates/technical-constitution.skeleton.md +106 -0
- package/assets/.aw-spec/orchestrator/rev-templates/technical-rule-file.skeleton.md +54 -0
- package/assets/.aw-spec/orchestrator/workflows/_smoke.orch.yaml +160 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-add-component.orch.yaml +244 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-adopt.orch.yaml +775 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +248 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +261 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +425 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-build.orch.yaml +188 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-characterize.orch.yaml +474 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-close-delta.orch.yaml +181 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-constitution.orch.yaml +284 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-demo.orch.yaml +103 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +69 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-deploy.orch.yaml +85 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-design.orch.yaml +382 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +382 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-foundation.orch.yaml +246 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +231 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement.orch.yaml +258 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-inventory.orch.yaml +236 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-onboard.orch.yaml +949 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-parity.orch.yaml +293 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-run.orch.yaml +177 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-domain.orch.yaml +79 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-feature.orch.yaml +128 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-specify.orch.yaml +412 -0
- package/assets/.aw-spec/scripts/bash/aw-inventory.sh +14 -0
- package/assets/.aw-spec/scripts/bash/aw-parity.sh +13 -0
- package/assets/.aw-spec/scripts/bash/aw-rev-constitution.sh +13 -0
- package/assets/.aw-spec/scripts/bash/aw-spec-actions.sh +1899 -0
- package/assets/.aw-spec/scripts/bash/aw-xref.sh +15 -0
- package/assets/.aw-spec/scripts/bash/common.sh +11 -60
- package/assets/.aw-spec/scripts/powershell/aw-inventory.ps1 +14 -0
- package/assets/.aw-spec/scripts/powershell/aw-parity.ps1 +13 -0
- package/assets/.aw-spec/scripts/powershell/aw-rev-constitution.ps1 +13 -0
- package/assets/.aw-spec/scripts/powershell/aw-spec-actions.ps1 +1563 -0
- package/assets/.aw-spec/scripts/powershell/aw-xref.ps1 +15 -0
- package/assets/.aw-spec/scripts/python/aw-inventory.py +683 -0
- package/assets/.aw-spec/scripts/python/aw-parity.py +380 -0
- package/assets/.aw-spec/scripts/python/aw-rev-constitution.py +312 -0
- package/assets/.aw-spec/scripts/python/aw-xref.py +1330 -0
- package/assets/.aw-spec/scripts/python/validate_okf.py +349 -0
- package/assets/.aw-spec/templates/constitution-domain-template.md +75 -0
- package/assets/.aw-spec/templates/constitution-technical-template.md +70 -0
- package/assets/.aw-spec/templates/delta-design-template.md +69 -0
- package/assets/.aw-spec/templates/delta-spec-template.md +60 -0
- package/assets/.aw-spec/templates/design-template.md +383 -0
- package/assets/.aw-spec/templates/okf-concept-template.md +103 -0
- package/assets/.aw-spec/templates/parity-manifest-template.json +49 -0
- package/assets/.aw-spec/templates/source-map-template.md +85 -0
- package/assets/.aw-spec/templates/spec-template.md +192 -71
- package/assets/.claude/agent.json +27 -0
- package/assets/.claude/skills/aw-spec-orch/SKILL.md +55 -0
- package/assets/.claude/skills/aw-spec-orch-demo/SKILL.md +59 -0
- package/assets/.kilocode/agent.json +28 -0
- package/assets/.kilocode/workflows/aw-spec-orch-demo.md +59 -0
- package/assets/.kilocode/workflows/aw-spec-orch.md +55 -0
- package/assets/docs/architecture/clean-architecture.md +1 -1
- package/assets/docs/architecture/cqrs.md +1 -1
- package/assets/docs/architecture/eventing.md +1 -1
- package/assets/docs/architecture/folder-structure.md +1 -1
- package/assets/docs/architecture/modular-monolith.md +1 -1
- package/assets/docs/architecture/persistence.md +1 -1
- package/assets/docs/architecture/testing.md +1 -1
- package/assets/docs/architecture/vertical-slices.md +1 -1
- package/assets/docs/components/Email/README.md +1 -1
- package/assets/docs/components/Jobs/README.md +1 -1
- package/assets/docs/components/QrGenerator/README.md +1 -1
- package/assets/docs/components/README.md +1 -1
- package/assets/docs/domain/domain-driven-design.md +1 -1
- package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/auditing.domain.md +86 -18
- package/assets/docs/domainspecs/healthcare/authorization.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/clinical-terms.md +1 -1
- package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/comandrulemap.md +58 -124
- package/assets/docs/domainspecs/healthcare/commandtechrulemap.md +119 -0
- package/assets/docs/domainspecs/healthcare/consent.md +1 -1
- package/assets/docs/domainspecs/healthcare/cqrs.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/data-classification.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/dicom.md +1 -1
- package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/eventing.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/fhir.md +1 -1
- package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/hipaa.md +1 -1
- package/assets/docs/domainspecs/healthcare/hl7.md +1 -1
- package/assets/docs/domainspecs/healthcare/integration-terms.md +1 -1
- package/assets/docs/domainspecs/healthcare/persistence.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/terminology.md +1 -1
- package/assets/docs/domainspecs/healthcare/testing.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/api-versioning.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/atc.md +1 -1
- package/assets/docs/domainspecs/pharma/auditing.domain.md +80 -16
- package/assets/docs/domainspecs/pharma/authorization.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/coding-standards.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/comandrulemap.md +64 -142
- package/assets/docs/domainspecs/pharma/commandtechrulemap.md +119 -0
- package/assets/docs/domainspecs/pharma/cqrs.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/data-classification.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/data-provenance.md +1 -1
- package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/eventing.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/fhir.md +1 -1
- package/assets/docs/domainspecs/pharma/folder-structure.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/hl7.md +1 -1
- package/assets/docs/domainspecs/pharma/idmp.md +1 -1
- package/assets/docs/domainspecs/pharma/integration-terms.md +1 -1
- package/assets/docs/domainspecs/pharma/ndc.md +1 -1
- package/assets/docs/domainspecs/pharma/persistence.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/pharma-terms.md +1 -1
- package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/regulatory-compliance.md +1 -1
- package/assets/docs/domainspecs/pharma/terminology.md +1 -1
- package/assets/docs/domainspecs/pharma/testing.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/zero-trust.domain.md +1 -1
- package/assets/docs/governance/api-versioning.md +1 -1
- package/assets/docs/governance/coding-standards.md +1 -1
- package/assets/docs/governance/dependency-policy.md +1 -1
- package/assets/docs/requirement-spec.txt +27 -0
- package/assets/docs/security/auditing.md +54 -11
- package/assets/docs/security/authorization.md +1 -1
- package/assets/docs/security/data-classification.md +1 -1
- package/assets/docs/security/zero-trust.md +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -54
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/paths.js +2 -2
- package/dist/utils/paths.js.map +1 -1
- package/package.json +1 -1
- package/assets/.agents/skills/aw-spec-addcomponent/SKILL.md +0 -129
- package/assets/.agents/skills/aw-spec-agent-context-update/SKILL.md +0 -31
- package/assets/.agents/skills/aw-spec-analyze/SKILL.md +0 -296
- package/assets/.agents/skills/aw-spec-checklist/SKILL.md +0 -374
- package/assets/.agents/skills/aw-spec-clarify/SKILL.md +0 -320
- package/assets/.agents/skills/aw-spec-constitution/SKILL.md +0 -194
- package/assets/.agents/skills/aw-spec-fast-implement/SKILL.md +0 -268
- package/assets/.agents/skills/aw-spec-foundation/SKILL.md +0 -358
- package/assets/.agents/skills/aw-spec-implement/SKILL.md +0 -279
- package/assets/.agents/skills/aw-spec-minimal-implementation/SKILL.md +0 -209
- package/assets/.agents/skills/aw-spec-plan/SKILL.md +0 -221
- package/assets/.agents/skills/aw-spec-select-domain/SKILL.md +0 -73
- package/assets/.agents/skills/aw-spec-specify/SKILL.md +0 -382
- package/assets/.agents/skills/aw-spec-tasks/SKILL.md +0 -259
- package/assets/.agents/skills/aw-spec-taskstoissues/SKILL.md +0 -107
- package/assets/.aw-spec/integrations/claude.manifest.json +0 -17
- package/assets/.aw-spec/scripts/bash/check-prerequisites.sh +0 -189
- package/assets/.aw-spec/scripts/bash/create-new-feature.sh +0 -299
- package/assets/.aw-spec/scripts/bash/setup-plan.sh +0 -84
- package/assets/.aw-spec/scripts/bash/setup-tasks.sh +0 -91
- package/assets/.aw-spec/templates/checklist-template.md +0 -40
- package/assets/.aw-spec/templates/constitution-template.md +0 -57
- package/assets/.aw-spec/templates/plan-template.md +0 -113
- package/assets/.aw-spec/templates/tasks-template.md +0 -252
- package/assets/.aw-spec/workflows/aw-spec/workflow.yml +0 -77
- package/assets/.aw-spec/workflows/workflow-registry.json +0 -13
- package/assets/.claude/skills/aw-spec-addcomponent/SKILL.md +0 -129
- package/assets/.claude/skills/aw-spec-agent-context-update/SKILL.md +0 -31
- package/assets/.claude/skills/aw-spec-analyze/SKILL.md +0 -296
- package/assets/.claude/skills/aw-spec-checklist/SKILL.md +0 -374
- package/assets/.claude/skills/aw-spec-clarify/SKILL.md +0 -320
- package/assets/.claude/skills/aw-spec-constitution/SKILL.md +0 -194
- package/assets/.claude/skills/aw-spec-fast-implement/SKILL.md +0 -268
- package/assets/.claude/skills/aw-spec-foundation/SKILL.md +0 -358
- package/assets/.claude/skills/aw-spec-implement/SKILL.md +0 -279
- package/assets/.claude/skills/aw-spec-minimal-implementation/SKILL.md +0 -209
- package/assets/.claude/skills/aw-spec-plan/SKILL.md +0 -221
- package/assets/.claude/skills/aw-spec-select-domain/SKILL.md +0 -73
- package/assets/.claude/skills/aw-spec-specify/SKILL.md +0 -382
- package/assets/.claude/skills/aw-spec-tasks/SKILL.md +0 -259
- package/assets/.claude/skills/aw-spec-taskstoissues/SKILL.md +0 -107
- package/assets/.kilocode/rules/specify-rules.md +0 -4
- package/assets/.kilocode/workflows/aw-spec-addcomponent.md +0 -129
- package/assets/.kilocode/workflows/aw-spec-agent-context.update.md +0 -31
- package/assets/.kilocode/workflows/aw-spec-analyze.md +0 -296
- package/assets/.kilocode/workflows/aw-spec-checklist.md +0 -374
- package/assets/.kilocode/workflows/aw-spec-clarify.md +0 -320
- package/assets/.kilocode/workflows/aw-spec-constitution.md +0 -194
- package/assets/.kilocode/workflows/aw-spec-fast-implement.md +0 -263
- package/assets/.kilocode/workflows/aw-spec-foundation.md +0 -358
- package/assets/.kilocode/workflows/aw-spec-implement.md +0 -279
- package/assets/.kilocode/workflows/aw-spec-minimal-implementation.md +0 -204
- package/assets/.kilocode/workflows/aw-spec-plan.md +0 -221
- package/assets/.kilocode/workflows/aw-spec-select-domain.md +0 -73
- package/assets/.kilocode/workflows/aw-spec-specify.md +0 -382
- package/assets/.kilocode/workflows/aw-spec-tasks.md +0 -259
- package/assets/.kilocode/workflows/aw-spec-taskstoissues.md +0 -107
- package/assets/docs/UI.Visualization/PACKAGING.md +0 -191
- package/assets/docs/UI.Visualization/UI.Visualization.Reference.pack.txt +0 -11768
- package/assets/docs/UI.Visualization/UI.Visualization.pack.txt +0 -11214
- package/assets/docs/UI.Visualization/pack-foundation.ps1 +0 -165
- package/assets/docs/UI.Visualization/pack-foundation.sh +0 -206
- package/assets/docs/UI.Visualization/pack-reference-foundation.ps1 +0 -159
- package/assets/docs/UI.Visualization/pack-reference-foundation.sh +0 -205
- package/assets/docs/UI.Visualization/unpack-foundation.ps1 +0 -126
- package/assets/docs/UI.Visualization/unpack-foundation.sh +0 -217
- package/assets/docs/UI.Visualization/unpack-reference-foundation.ps1 +0 -129
- package/assets/docs/UI.Visualization/unpack-reference-foundation.sh +0 -218
- package/assets/docs/command.txt +0 -295
- package/assets/docs/domainspecs/UIVisualization/chart-bubble.md +0 -70
- package/assets/docs/domainspecs/UIVisualization/chart-dimred.md +0 -58
- package/assets/docs/domainspecs/UIVisualization/chart-heatmap.md +0 -62
- package/assets/docs/domainspecs/UIVisualization/chart-network.md +0 -69
- package/assets/docs/domainspecs/UIVisualization/chart-parallel.md +0 -75
- package/assets/docs/domainspecs/UIVisualization/chart-scurve.md +0 -66
- package/assets/docs/domainspecs/UIVisualization/chart-survival.md +0 -69
- package/assets/docs/domainspecs/UIVisualization/chart-trellis.md +0 -68
- package/assets/docs/domainspecs/UIVisualization/chart-violin.md +0 -61
- package/assets/docs/domainspecs/UIVisualization/chart-volcano.md +0 -63
- package/assets/docs/domainspecs/UIVisualization/charting-standards.domain.md +0 -80
- package/assets/docs/domainspecs/UIVisualization/clean-architecture.domain.md +0 -87
- package/assets/docs/domainspecs/UIVisualization/coding-standards.domain.md +0 -47
- package/assets/docs/domainspecs/UIVisualization/comandrulemap.md +0 -130
- package/assets/docs/domainspecs/UIVisualization/component-contracts.domain.md +0 -67
- package/assets/docs/domainspecs/UIVisualization/data-source.domain.md +0 -68
- package/assets/docs/domainspecs/UIVisualization/domain-requirements.md +0 -110
- package/assets/docs/domainspecs/UIVisualization/folder-structure.domain.md +0 -55
- package/assets/docs/domainspecs/UIVisualization/state-management.domain.md +0 -67
- package/assets/docs/domainspecs/UIVisualization/testing.domain.md +0 -52
- package/assets/docs/domainspecs/UIVisualization/ui-viz-terms.md +0 -39
- 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.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())
|