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.
- package/assets/.appweaver/ORCH-DIALECT.md +8 -3
- package/assets/.appweaver/STACK-PROFILES.md +87 -8
- package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
- package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +22 -1
- package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +6 -6
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +144 -52
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +78 -10
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +283 -25
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +202 -40
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +557 -57
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +3 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +101 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +772 -216
- package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +708 -209
- package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
- package/assets/.appweaver/scripts/python/aw-crstate.py +319 -0
- package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
- package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
- package/assets/.appweaver/scripts/python/aw-payload-parity.py +19 -0
- package/assets/.appweaver/scripts/python/aw-plan-coverage.py +503 -0
- package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
- package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
- package/assets/.appweaver/scripts/python/aw-stack.py +257 -28
- package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
- package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
- package/assets/.appweaver/stack-profiles/angular.json +26 -0
- package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +39 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +20 -0
- package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
- package/assets/.appweaver/stack-profiles/node-express.json +27 -0
- package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
- package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
- package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
- package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
- package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
- package/assets/.appweaver/templates/ui-model-template.md +67 -0
- package/assets/.appweaver/templates/workflow-model-template.md +62 -0
- package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
- package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
- package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
- package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
- package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
- package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
- package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
- package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +114 -0
- package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
- package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
- package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
- package/package.json +1 -1
- 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()
|