aw-spec-cli 0.1.20 → 0.1.22

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 (62) hide show
  1. package/assets/.appweaver/ORCH-DIALECT.md +8 -3
  2. package/assets/.appweaver/STACK-PROFILES.md +87 -8
  3. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
  4. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +22 -1
  5. package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +6 -6
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +144 -52
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +78 -10
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +283 -25
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +202 -40
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +557 -57
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +3 -3
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +101 -1
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
  22. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +772 -216
  23. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  24. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +708 -209
  25. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  26. package/assets/.appweaver/scripts/python/aw-crstate.py +319 -0
  27. package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
  28. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  29. package/assets/.appweaver/scripts/python/aw-payload-parity.py +19 -0
  30. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +503 -0
  31. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  32. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  33. package/assets/.appweaver/scripts/python/aw-stack.py +257 -28
  34. package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
  35. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  36. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  37. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  38. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +39 -0
  39. package/assets/.appweaver/stack-profiles/java-spring.json +20 -0
  40. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  41. package/assets/.appweaver/stack-profiles/node-express.json +27 -0
  42. package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
  43. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  44. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  45. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  46. package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
  47. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  48. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  49. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
  50. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
  51. package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
  52. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
  53. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
  54. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
  55. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
  56. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
  57. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +114 -0
  58. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
  59. package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
  60. package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
  61. package/package.json +1 -1
  62. package/assets/.appweaver/orchestrator/.state/smoke.state.json +0 -32
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ aw-plangate — the analyze-plan receipt, written and read in one place.
4
+
5
+ THE RECEIPT. `analyze-plan` records which bytes it checked against each other — spec.md,
6
+ the context's domain-model.md and every lane plan on disk — and the verdict it reached.
7
+ `implement` generates from the plans and deliberately does NOT read the spec, on the
8
+ strength of that receipt, so the pair of operations that write and read it is the hinge the
9
+ whole bootstrap path turns on.
10
+
11
+ WHY IT LIVES HERE. Both halves were embedded twice, as a bash heredoc and as a natively
12
+ re-written PowerShell function, and the rule they implement is a set of sha comparisons
13
+ with three deliberate asymmetries in it — an absent spec sha does not block, an absent
14
+ model sha does, and an unrecognised verdict records as `gaps`. That rule changed twice
15
+ (MF-19, then MF-20) and each time it had to be re-derived by hand on the PowerShell side
16
+ and kept in step by eye. One module both shells invoke removes the question: there is
17
+ nothing left to keep in step. Same reason aw-crstate.py owns "has this change merged".
18
+
19
+ NO BASELINE IS NOT "NOTHING MOVED". `spec_moved` / `model_moved` report "" where there is
20
+ nothing to compare against — no receipt, or a receipt that hashed nothing for that
21
+ document. "" is falsy in Python, in PowerShell and to the engine's require_true, so every
22
+ flag computed from them reads exactly as a false did; what it buys is that a reader can
23
+ tell "unchanged" from "unknown", which a bare false cannot express.
24
+
25
+ Usage:
26
+ aw-plangate.py stamp --feature-dir <p> --context-dir <p> --root <p>
27
+ [--spec-version <v>] [--mode <text>]
28
+ [--verdict conformant|routed-gaps|gaps] [--plan-gaps <n>]
29
+ [--routed <json list>]
30
+ -> {file, gated, spec_version, verdict, plan_gaps, routed_gaps, hashed[], count}
31
+ aw-plangate.py check --feature-dir <p> --context-dir <p> --root <p>
32
+ -> {file, tracked, gate_fresh, verdict, may_generate, plan_gaps, routed_gaps[],
33
+ routed_csv, spec_moved, model_moved, plans_changed[], plans_changed_csv,
34
+ gated, spec_version, reason}
35
+ """
36
+
37
+ import argparse
38
+ import datetime
39
+ import hashlib
40
+ import json
41
+ import os
42
+ import sys
43
+
44
+ RECEIPT = ".aw-plan-gate.json"
45
+ VERDICTS = ("conformant", "routed-gaps", "gaps")
46
+
47
+ # Every document the gate READ. A lane that is not declared has no plan on disk and is
48
+ # simply absent from the map — the check treats a name it does not carry as not gated,
49
+ # never as changed, so declaring a second lane later does not read as tampering.
50
+ #
51
+ # THE LEGACY NAMES ARE HERE FOR THE SAME REASON version-stamp CARRIES THEM. feature-paths
52
+ # resolves design.md / design.ui.md under the backend_plan / frontend_plan keys, so a tree
53
+ # that has not been through `plan` since the rename — every brownfield feature `adopt`
54
+ # produces — is GATED on those documents. Hashing only the new names would leave such a
55
+ # feature with an empty plan map: a receipt that looks tracked and fresh while the document
56
+ # implement actually generates from is unwatched.
57
+ PLAN_NAMES = ("backend-plan.md", "frontend-plan.md", "design.md", "design.ui.md")
58
+
59
+ NOTE = ("Written by every master-mode analyze-plan run. It records the bytes that were "
60
+ "checked against each other and the verdict on them, not a version — the plans' "
61
+ "**Version**: headers are stamped in lockstep by close-delta and cannot answer "
62
+ "this. implement reads it through plan-gate-check: moved bytes or verdict `gaps` "
63
+ "refuse; `routed-gaps` lists what /appweaver specify still has to lift.")
64
+
65
+
66
+ def sha(path):
67
+ try:
68
+ with open(path, "rb") as fh:
69
+ return hashlib.sha256(fh.read()).hexdigest()
70
+ except Exception:
71
+ return ""
72
+
73
+
74
+ def stamp(fd, ctx, root, spec_version="", mode="master", verdict="conformant",
75
+ plan_gaps=0, routed=None):
76
+ plans = {}
77
+ for name in PLAN_NAMES:
78
+ h = sha(os.path.join(fd, name))
79
+ if h:
80
+ plans[name] = h
81
+ # An unrecognised verdict is recorded as `gaps`, the one that refuses.
82
+ verdict = (verdict or "").strip()
83
+ if verdict not in VERDICTS:
84
+ verdict = "gaps"
85
+ try:
86
+ plan_gaps = int(plan_gaps or 0)
87
+ except (TypeError, ValueError):
88
+ plan_gaps = 0
89
+ routed = [str(x) for x in (routed or [])]
90
+ rec = {
91
+ "gated": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
92
+ "gate_mode": mode or "master",
93
+ "spec_version": (spec_version or "").strip(),
94
+ "verdict": verdict,
95
+ "plan_gaps": plan_gaps,
96
+ "routed_gaps": routed,
97
+ "spec_sha": sha(os.path.join(fd, "spec.md")),
98
+ "model_sha": sha(os.path.join(ctx, "domain-model.md")),
99
+ "plans": plans,
100
+ "note": NOTE,
101
+ }
102
+ path = os.path.join(fd, RECEIPT)
103
+ with open(path, "w", encoding="utf-8") as fh:
104
+ fh.write(json.dumps(rec, indent=2) + "\n")
105
+ return {"file": os.path.relpath(path, root), "gated": rec["gated"],
106
+ "spec_version": rec["spec_version"], "verdict": verdict,
107
+ "plan_gaps": plan_gaps, "routed_gaps": len(routed),
108
+ "hashed": sorted(plans.keys()), "count": len(plans), "ok": True}
109
+
110
+
111
+ def check(fd, ctx, root):
112
+ path = os.path.join(fd, RECEIPT)
113
+ try:
114
+ rec = json.load(open(path, encoding="utf-8"))
115
+ except Exception:
116
+ rec = None
117
+ out = {"file": os.path.relpath(path, root), "tracked": bool(rec), "gate_fresh": False,
118
+ "verdict": "", "may_generate": True, "plan_gaps": 0, "routed_gaps": [],
119
+ "routed_csv": "", "spec_moved": "", "model_moved": "", "plans_changed": [],
120
+ "plans_changed_csv": "", "gated": "", "spec_version": "", "reason": "", "ok": True}
121
+ if not rec:
122
+ # Never gated. Not a failure and not a stale plan — an unknown, and the only honest
123
+ # thing to report about a tree that has not run analyze-plan since this op existed.
124
+ out["reason"] = ("no .aw-plan-gate.json — this feature has not been through "
125
+ "analyze-plan in master mode since the gate began leaving a receipt")
126
+ return out
127
+
128
+ out["gated"] = rec.get("gated", "")
129
+ out["spec_version"] = rec.get("spec_version", "")
130
+ # A receipt from before the verdict existed was only ever written on a clean pass.
131
+ out["verdict"] = rec.get("verdict") or "conformant"
132
+ out["plan_gaps"] = rec.get("plan_gaps") or 0
133
+ out["routed_gaps"] = [str(x) for x in (rec.get("routed_gaps") or [])]
134
+ out["routed_csv"] = "; ".join(out["routed_gaps"])
135
+
136
+ # A receipt that hashed nothing for a document has no baseline for it either, so the
137
+ # question is unanswerable for that one document and says so.
138
+ spec_base = rec.get("spec_sha") or ""
139
+ out["spec_moved"] = (spec_base != sha(os.path.join(fd, "spec.md"))) if spec_base else ""
140
+ # A model recorded as absent that is still absent has not moved. A context that GAINED a
141
+ # domain model since the gate has moved: check 4 never ran against it — that stays a
142
+ # move, including for a receipt predating model_sha, because a model nothing has been
143
+ # checked against is the condition this field exists to catch. Only a receipt carrying
144
+ # no model_sha with no model on disk either leaves nothing to compare.
145
+ model_base = rec.get("model_sha")
146
+ model_now = sha(os.path.join(ctx, "domain-model.md"))
147
+ out["model_moved"] = ((model_base or "") != model_now
148
+ if (model_base is not None or model_now) else "")
149
+
150
+ changed = []
151
+ for name, was in sorted((rec.get("plans") or {}).items()):
152
+ if sha(os.path.join(fd, name)) != was:
153
+ changed.append(name)
154
+ out["plans_changed"] = changed
155
+ out["plans_changed_csv"] = ", ".join(changed)
156
+ out["gate_fresh"] = not (out["spec_moved"] or out["model_moved"] or changed)
157
+ # Moved bytes refuse whatever the verdict was; unmoved bytes refuse only on `gaps`.
158
+ out["may_generate"] = out["gate_fresh"] and out["verdict"] != "gaps"
159
+
160
+ if not out["gate_fresh"]:
161
+ bits = []
162
+ if out["spec_moved"]:
163
+ bits.append("spec.md has changed")
164
+ if out["model_moved"]:
165
+ bits.append("domain-model.md has changed")
166
+ if changed:
167
+ bits.append("%s changed (a re-derive by `plan main`, or a hand edit)"
168
+ % ", ".join(changed))
169
+ out["reason"] = "since analyze-plan checked them on %s: %s" % (out["gated"],
170
+ "; ".join(bits))
171
+ elif out["verdict"] == "gaps":
172
+ out["reason"] = ("analyze-plan on %s left %s gap(s) a plan rewrite closes, unrepaired"
173
+ % (out["gated"], out["plan_gaps"] or "some"))
174
+ elif out["verdict"] == "routed-gaps":
175
+ out["reason"] = ("the spec, the model and every gated plan are byte-identical to what "
176
+ "analyze-plan checked on %s; it left %d gap(s) only /appweaver specify "
177
+ "can close: %s" % (out["gated"], len(out["routed_gaps"]),
178
+ out["routed_csv"]))
179
+ else:
180
+ out["reason"] = ("the spec, the model and every gated plan are byte-identical to what "
181
+ "analyze-plan passed on %s" % out["gated"])
182
+ return out
183
+
184
+
185
+ def main():
186
+ ap = argparse.ArgumentParser(description="the analyze-plan receipt, written and read")
187
+ sub = ap.add_subparsers(dest="cmd", required=True)
188
+ for name in ("stamp", "check"):
189
+ p = sub.add_parser(name)
190
+ p.add_argument("--feature-dir", required=True)
191
+ p.add_argument("--context-dir", required=True)
192
+ p.add_argument("--root", required=True)
193
+ if name == "stamp":
194
+ p.add_argument("--spec-version", default="")
195
+ p.add_argument("--mode", default="master")
196
+ p.add_argument("--verdict", default="conformant")
197
+ p.add_argument("--plan-gaps", default="0")
198
+ p.add_argument("--routed", default="[]")
199
+ args = ap.parse_args()
200
+
201
+ if args.cmd == "stamp":
202
+ try:
203
+ routed = json.loads(args.routed or "[]")
204
+ except Exception:
205
+ routed = []
206
+ if not isinstance(routed, list):
207
+ routed = []
208
+ out = stamp(args.feature_dir, args.context_dir, args.root, args.spec_version,
209
+ args.mode, args.verdict, args.plan_gaps, routed)
210
+ else:
211
+ out = check(args.feature_dir, args.context_dir, args.root)
212
+ sys.stdout.write(json.dumps(out) + "\n")
213
+
214
+
215
+ if __name__ == "__main__":
216
+ main()
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ aw-principles — enumerate the ratified NON-NEGOTIABLE principles.
4
+
5
+ WHY A SCRIPT AND NOT A JUDGEMENT. `specify` settles [NEEDS CLARIFICATION] markers by
6
+ asking, and a non-interactive run answers with the FIRST option offered — so an agent's
7
+ option ordering becomes project policy unattended. The gate that checks those answers
8
+ against the constitution has to name the principle it is invoking, and an agent asked to
9
+ both find the principle and judge the contradiction can invent the principle it then
10
+ objects to. Splitting it means the factual half is checkable: this module reads the
11
+ headings and nothing else, the agent judges only whether a settled answer departs from
12
+ one of THESE, and a citation outside the list is visibly wrong.
13
+
14
+ It also answers "are there any?" deterministically. A project whose constitution ratifies
15
+ no NON-NEGOTIABLE principle has nothing for the gate to check, and `count` is what lets
16
+ the workflow skip it rather than asking an agent to confirm an empty set.
17
+
18
+ WHAT COUNTS. A principle heading of the form `### <id>. <title> (NON-NEGOTIABLE)`, which
19
+ is what all three constitution templates emit. The id is whatever precedes the first
20
+ period — roman numerals in the domain and technical halves, `UX-`-prefixed in the UI half
21
+ — so a project that renumbers or re-prefixes its series needs no change here.
22
+
23
+ HTML comments are stripped first. The Sync Impact Report at the top of every constitution
24
+ quotes principle headings while recording what a ratification changed, and counting those
25
+ would report principles that are not in force.
26
+
27
+ Usage:
28
+ aw-principles.py list --root <p> [--file <p> ...]
29
+ -> {files[], missing[], principles[{file, id, title, line}], count, titles_csv, ok}
30
+
31
+ Default files are the two halves `specify` loads — constitution.domain.md and
32
+ constitution.ui.md. The technical half is NOT a default: specify never loads it (a
33
+ spec is WHAT, not HOW), so a gate there could cite a principle the authoring agent was
34
+ never shown. A caller that loads it passes --file.
35
+ """
36
+
37
+ import argparse
38
+ import json
39
+ import os
40
+ import re
41
+ import sys
42
+
43
+ DEFAULT_FILES = (".appweaver/memory/constitution.domain.md",
44
+ ".appweaver/memory/constitution.ui.md")
45
+
46
+ # `### XXIII. Clinical Meaning is Bound, Never Free Text (NON-NEGOTIABLE)`
47
+ # `### UX-I. Accessibility Floor (NON-NEGOTIABLE)`
48
+ HEADING = re.compile(r"^###\s+([^.]+)\.\s+(.+?)\s*\(NON-NEGOTIABLE\)\s*$")
49
+ COMMENT = re.compile(r"<!--.*?-->", re.S)
50
+
51
+
52
+ def read(path):
53
+ try:
54
+ with open(path, encoding="utf-8") as fh:
55
+ return fh.read()
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def principles_in(path, rel):
61
+ """[{file, id, title, line}] for one constitution half, in document order."""
62
+ text = read(path)
63
+ if text is None:
64
+ return None
65
+ # Blank the comments rather than deleting them, so reported line numbers still
66
+ # point at the heading in the file a reader opens.
67
+ text = COMMENT.sub(lambda m: "\n" * m.group(0).count("\n"), text)
68
+ out = []
69
+ for n, line in enumerate(text.split("\n"), 1):
70
+ m = HEADING.match(line)
71
+ if m:
72
+ out.append({"file": rel, "id": m.group(1).strip(),
73
+ "title": m.group(2).strip(), "line": n})
74
+ return out
75
+
76
+
77
+ def listing(root, files):
78
+ seen, missing, found = [], [], []
79
+ for rel in files:
80
+ path = rel if os.path.isabs(rel) else os.path.join(root, rel)
81
+ got = principles_in(path, rel)
82
+ if got is None:
83
+ missing.append(rel)
84
+ continue
85
+ seen.append(rel)
86
+ found.extend(got)
87
+ return {"files": seen, "missing": missing, "principles": found,
88
+ "count": len(found),
89
+ "titles_csv": "; ".join("%s (%s %s)" % (p["title"], os.path.basename(p["file"]),
90
+ p["id"]) for p in found),
91
+ "ok": True}
92
+
93
+
94
+ def main():
95
+ ap = argparse.ArgumentParser(description="enumerate ratified NON-NEGOTIABLE principles")
96
+ sub = ap.add_subparsers(dest="cmd", required=True)
97
+ p = sub.add_parser("list")
98
+ p.add_argument("--root", required=True)
99
+ p.add_argument("--file", action="append", default=[],
100
+ help="a constitution half to scan (repeatable; defaults to domain + ui)")
101
+ args = ap.parse_args()
102
+
103
+ out = listing(args.root, args.file or list(DEFAULT_FILES))
104
+ sys.stdout.write(json.dumps(out) + "\n")
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()