aw-spec-cli 0.1.19 → 0.1.21

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 (75) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +17 -9
  2. package/assets/.appweaver/ORCH-DIALECT.md +8 -3
  3. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +26 -3
  4. package/assets/.appweaver/orchestrator/orchestrator.py +65 -3
  5. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +1 -1
  6. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +4 -2
  7. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +1 -1
  8. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +1 -1
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +121 -42
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +7 -7
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +145 -52
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +18 -10
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +306 -28
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +1 -1
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +1 -1
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +1 -1
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +4 -4
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +1 -1
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +114 -30
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +140 -36
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +385 -61
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +217 -174
  23. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +407 -107
  24. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +1 -1
  25. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +49 -11
  26. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +1 -1
  27. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +1 -1
  28. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +29 -23
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +47 -23
  30. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +19 -11
  31. package/assets/.appweaver/scripts/bash/aw-inventory.sh +15 -0
  32. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +683 -92
  33. package/assets/.appweaver/scripts/bash/aw-xref.sh +3 -0
  34. package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +15 -0
  35. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +625 -64
  36. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +3 -0
  37. package/assets/.appweaver/scripts/python/aw-crstate.py +194 -0
  38. package/assets/.appweaver/scripts/python/aw-foundation.py +151 -0
  39. package/assets/.appweaver/scripts/python/aw-inventory.py +1829 -63
  40. package/assets/.appweaver/scripts/python/aw-payload-parity.py +3 -0
  41. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +423 -0
  42. package/assets/.appweaver/scripts/python/aw-stack.py +251 -28
  43. package/assets/.appweaver/scripts/python/aw-testlog.py +148 -0
  44. package/assets/.appweaver/scripts/python/aw-xref.py +500 -81
  45. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +1 -0
  46. package/assets/.appweaver/stack-profiles/java-spring.json +1 -0
  47. package/assets/.appweaver/stack-profiles/node-express.json +1 -0
  48. package/assets/.appweaver/stack-profiles/python-fastapi.json +1 -0
  49. package/assets/.appweaver/templates/constitution-domain-template.md +17 -2
  50. package/assets/.appweaver/templates/delta-design-template.md +1 -1
  51. package/assets/.appweaver/templates/delta-domain-model-template.md +1 -1
  52. package/assets/.appweaver/templates/delta-spec-template.md +4 -3
  53. package/assets/.appweaver/templates/design-template.md +16 -11
  54. package/assets/.appweaver/templates/okf-concept-template.md +11 -1
  55. package/assets/.appweaver/templates/proposed-inventory-template.md +127 -0
  56. package/assets/.appweaver/templates/source-map-template.md +5 -3
  57. package/assets/.appweaver/templates/spec-template.md +17 -17
  58. package/assets/.appweaver/templates/ui-delta-design-template.md +1 -1
  59. package/assets/.appweaver/templates/ui-design-template.md +4 -4
  60. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +4489 -361
  61. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2274 -125
  62. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +10 -6
  63. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +4 -0
  64. package/assets/guardrails/technology/dotnet-webapi/auditing.md +3 -1
  65. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +5 -3
  66. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +5 -1
  67. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +13 -5
  68. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +2 -0
  69. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +6 -2
  70. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +70 -0
  71. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +3 -0
  72. package/assets/guardrails/technology/dotnet-webapi/testing.md +10 -0
  73. package/assets/guardrails/technology/nextjs/ui-constitution.md +8 -1
  74. package/package.json +1 -1
  75. package/assets/.appweaver/orchestrator/.state/smoke.state.json +0 -32
@@ -9,6 +9,9 @@
9
9
  # aw-xref.ps1 concept-upsert --aggregate X [--source-files a,b,c] [--description D] [--origin ...] [--root DIR]
10
10
  # aw-xref.ps1 concept-overview --aggregate X (--text PROSE | --text-file F) [--root DIR]
11
11
  # aw-xref.ps1 concept-paths --aggregate X | --feature-dir <dir> [--root DIR] [--bundle knowledge]
12
+ # aw-xref.ps1 feature-aggregates --feature-dir <dir> [--root DIR]
13
+ # (the aggregate(s) a feature acts on: its spec's Acts-on line,
14
+ # then the boundary map, then the feature name itself)
12
15
  $ErrorActionPreference = "Stop"
13
16
  $core = Join-Path $PSScriptRoot "..\python\aw-xref.py"
14
17
  & python3 $core @args
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ aw-crstate — ONE definition of "this change has been merged", for every reader of it.
4
+
5
+ Three places ask the question and each answered it differently: cr-merge-check's
6
+ merged-open fence, cr-recall-check's already-merged guard, and close-delta through it.
7
+ All three tested `cr_id in spec.md` — the id ANYWHERE in the file, including quoted in a
8
+ rationale, and matching a prefix (`CR-001` inside `CR-0011`). A guard that fires on prose
9
+ is a guard nobody can trust, and three copies of it drift.
10
+
11
+ THE EVIDENCE IS A CHANGE HISTORY ROW. finalize-delta appends one when it merges, carrying
12
+ the change's id in the row's CR cell. So a change is merged when its id is a CELL of such a
13
+ row — matched whole, in a table row, never as a substring of a paragraph.
14
+
15
+ WHERE IT LOOKS. `spec.md` is per feature, but `domain-model.md` is shared by every feature
16
+ in the bounded context: a sibling feature's merge moves a master THIS delta was authored
17
+ against and leaves nothing in this feature's spec. So the scan covers the context's model
18
+ and every feature's spec, and reports which document carried the evidence.
19
+
20
+ Importable (the bash payloads load it by path) and a CLI (the PowerShell twins call it), so
21
+ both shells answer from the same code.
22
+
23
+ Usage:
24
+ aw-crstate.py merged --cr <id> --doc <path> [--doc <path>…]
25
+ -> {"merged": bool, "evidence": "<the row>", "document": "<path>"}
26
+ aw-crstate.py merged-open --cr <id> --feature-dir <p> --context-dir <p>
27
+ -> {"merged_open": [{cr_id, feature, document, evidence, aggregates, shared, same_feature}], …}
28
+ """
29
+
30
+ import argparse
31
+ import glob
32
+ import json
33
+ import os
34
+ import re
35
+ import sys
36
+
37
+ CR_RX = re.compile(r"CR-\d{8}-\d{6}-[0-9A-Za-z]+")
38
+ TICK_RX = re.compile(r"`([^`]+)`")
39
+
40
+
41
+ def read(path):
42
+ try:
43
+ return open(path, encoding="utf-8").read()
44
+ except Exception:
45
+ return ""
46
+
47
+
48
+ def _cells(line):
49
+ s = line.strip()
50
+ if not s.startswith("|"):
51
+ return []
52
+ return [c.strip() for c in s.strip("|").split("|")]
53
+
54
+
55
+ def merged_row(text, cr_id):
56
+ """The Change History row whose CR cell IS this change, or "" when there is none.
57
+
58
+ The cell may carry the bare id or the id with a link or a note beside it, so the test is
59
+ "the cell contains this id as a whole token" — not "the row mentions it", which is what
60
+ made a cited id in prose read as a merge.
61
+ """
62
+ if not text or not cr_id:
63
+ return ""
64
+ for line in text.split("\n"):
65
+ cells = _cells(line)
66
+ if len(cells) < 2 or set("".join(cells)) <= set("-: "):
67
+ continue
68
+ for cell in cells[1:]: # never the leading Version cell
69
+ if cr_id in CR_RX.findall(cell):
70
+ return line.strip()
71
+ return ""
72
+
73
+
74
+ def merged_in(paths, cr_id):
75
+ """(row, document) for the first document whose Change History records this change."""
76
+ for p in paths:
77
+ row = merged_row(read(p), cr_id)
78
+ if row:
79
+ return row, p
80
+ return "", ""
81
+
82
+
83
+ def aggregates(text):
84
+ """The `**Aggregates touched**:` header — the list finalize-delta opens sections for."""
85
+ m = re.search(r"^[ \t]*(?:[-*][ \t]+)?\*\*Aggregates touched(?:\*\*:|:\*\*)[ \t]*(.*)$",
86
+ text or "", re.M)
87
+ if not m:
88
+ return set()
89
+ value = re.sub(r"<!--.*?(?:-->|$)", "", m.group(1)).strip()
90
+ names = TICK_RX.findall(value)
91
+ if not names:
92
+ parts = [p.strip() for p in re.split(r"[,;/&]|\band\b", value) if p.strip()]
93
+ names = [re.match(r"[A-Za-z_][A-Za-z0-9_]*", n).group(0)
94
+ for n in parts if re.match(r"[A-Za-z_][A-Za-z0-9_]*", n)]
95
+ return {n for n in names if not n.startswith(("<", "[")) and not re.match(r"(?i)none\b", n)}
96
+
97
+
98
+ def model_delta_of(ctx, cr_id):
99
+ """A change's model half: beside the master while open, in its archive once closed."""
100
+ p = os.path.join(ctx, "domain-model.%s.delta.md" % cr_id)
101
+ if os.path.isfile(p):
102
+ return read(p)
103
+ for feat in sorted(os.listdir(ctx)) if os.path.isdir(ctx) else []:
104
+ a = os.path.join(ctx, feat, "changes", "archive", cr_id, "domain-model.delta.md")
105
+ if os.path.isfile(a):
106
+ return read(a)
107
+ return ""
108
+
109
+
110
+ def open_changes(ctx):
111
+ """{cr_id: feature} for every change OPEN in the context — the directory is the
112
+ authority, and a model delta with no change directory still counts."""
113
+ found = {}
114
+ if not os.path.isdir(ctx):
115
+ return found
116
+ for feat in sorted(os.listdir(ctx)):
117
+ ch = os.path.join(ctx, feat, "changes")
118
+ if not os.path.isdir(ch):
119
+ continue
120
+ for name in sorted(os.listdir(ch)):
121
+ if name != "archive" and os.path.isdir(os.path.join(ch, name)):
122
+ found.setdefault(name, feat)
123
+ for f in sorted(os.listdir(ctx)):
124
+ m = re.match(r"^domain-model\.(.+)\.delta\.md$", f)
125
+ if m:
126
+ found.setdefault(m.group(1), "")
127
+ return found
128
+
129
+
130
+ def merged_open(ctx, feature_dir, cr_id):
131
+ """Changes that are MERGED into a master and still OPEN — the state in which the masters
132
+ carry a change the version does not yet reflect.
133
+
134
+ A merge in THIS feature moved this delta's own spec, so it blocks whatever it touched.
135
+ A SIBLING feature's merge moved the shared context model, so it blocks only where it
136
+ touched an aggregate this delta also names; `shared` carries that answer and a reader
137
+ that cannot read a delta's aggregates is told so (`unknown`) rather than cleared.
138
+ """
139
+ this_feat = os.path.basename(os.path.normpath(feature_dir))
140
+ mine = aggregates(model_delta_of(ctx, cr_id))
141
+ docs = {}
142
+ model_master = os.path.join(ctx, "domain-model.md")
143
+ if os.path.isfile(model_master):
144
+ docs[model_master] = ""
145
+ for spec in sorted(glob.glob(os.path.join(glob.escape(ctx), "*", "spec.md"))):
146
+ docs[spec] = os.path.basename(os.path.dirname(spec))
147
+
148
+ out = []
149
+ for other, feat in sorted(open_changes(ctx).items()):
150
+ if other == cr_id:
151
+ continue
152
+ row, doc = merged_in(list(docs), other)
153
+ if not row:
154
+ continue
155
+ theirs = aggregates(model_delta_of(ctx, other))
156
+ same_feature = (feat or docs.get(doc, "")) == this_feat
157
+ shared = sorted(mine & theirs)
158
+ out.append({
159
+ "cr_id": other, "feature": feat or docs.get(doc, ""),
160
+ "document": os.path.relpath(doc, ctx), "evidence": row,
161
+ "aggregates": sorted(theirs), "shared": shared,
162
+ "unknown": not theirs, "same_feature": same_feature,
163
+ # A sibling's merge blocks only where it moved something this delta also names.
164
+ "blocking": same_feature or bool(shared) or not theirs,
165
+ })
166
+ return out
167
+
168
+
169
+ def main():
170
+ ap = argparse.ArgumentParser(description="one definition of a merged change")
171
+ sub = ap.add_subparsers(dest="cmd", required=True)
172
+ m = sub.add_parser("merged")
173
+ m.add_argument("--cr", required=True)
174
+ m.add_argument("--doc", action="append", default=[])
175
+ o = sub.add_parser("merged-open")
176
+ o.add_argument("--cr", required=True)
177
+ o.add_argument("--feature-dir", required=True)
178
+ o.add_argument("--context-dir", required=True)
179
+ args = ap.parse_args()
180
+
181
+ if args.cmd == "merged":
182
+ row, doc = merged_in(args.doc, args.cr)
183
+ out = {"merged": bool(row), "evidence": row, "document": doc}
184
+ else:
185
+ rows = merged_open(args.context_dir, args.feature_dir, args.cr)
186
+ blocking = [r for r in rows if r["blocking"]]
187
+ out = {"merged_open": rows, "blocking": blocking,
188
+ "blocking_count": len(blocking),
189
+ "blocking_ids": ", ".join(r["cr_id"] for r in blocking)}
190
+ sys.stdout.write(json.dumps(out, separators=(",", ":")) + "\n")
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ aw-foundation — the part of scaffolding that depends on the LANE MODEL.
4
+
5
+ A foundation pack is a tree of files and an unpacker that writes it under one output
6
+ directory. That output directory is now a LANE'S SOURCE PATH, so everything a pack
7
+ carries lands inside the subtree its lane owns, and nothing a scaffold produces can
8
+ appear outside a declared lane.
9
+
10
+ A lane owns TWO subtrees, though — its source path and its test path — and for most
11
+ backend stacks they are siblings rather than nested: .NET keeps `tests/` beside `src/`,
12
+ not inside it. A pack that carries its own test project therefore has one directory
13
+ that does NOT belong under the source path, and unpacking it verbatim would bury the
14
+ tests at `src/tests/`, where the lane does not own them, `dotnet test` does not look for
15
+ them, and every later phase that resolves the lane's test path finds nothing.
16
+
17
+ So the test tree is LIFTED: whatever the pack placed at `<source path>/<test dir>` is
18
+ merged into the lane's declared test path. The directory name comes from the lane's own
19
+ test path rather than from a list here, which is what makes a lane with a custom
20
+ `test_root` work and a stack whose tests live inside its source a no-op — the two paths
21
+ are then the same subtree and there is nothing to lift.
22
+
23
+ Called as a CLI by both the bash and the PowerShell actions, so the two answer from one
24
+ implementation rather than from two that have to be kept in step.
25
+
26
+ Usage:
27
+ aw-foundation.py redirect-tests --out <lane source path> --test-path <lane test path>
28
+ [--repo <root>] [--force]
29
+ -> {"moved": n, "conflicts": [...], "from": "...", "to": "...", "message": "..."}
30
+ """
31
+
32
+ import argparse
33
+ import json
34
+ import os
35
+ import shutil
36
+ import sys
37
+
38
+
39
+ def norm(path):
40
+ """A repo-relative directory, '/'-separated and '/'-terminated; "" when absent.
41
+
42
+ Mirrors aw-stack's `norm_root` for the shapes this reads, so the paths compared here
43
+ are the same strings the lane model stores. "./" — the repo root as a source path —
44
+ normalises to "" so that joining it yields the repo itself.
45
+ """
46
+ p = (path or "").replace("\\", "/").strip()
47
+ while p.startswith("./"):
48
+ p = p[2:]
49
+ p = p.lstrip("/")
50
+ if p in ("", "."):
51
+ return ""
52
+ return p if p.endswith("/") else p + "/"
53
+
54
+
55
+ def emit(payload):
56
+ sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n")
57
+
58
+
59
+ def merge_move(src, dest, force):
60
+ """Move every file under `src` to the matching place under `dest`, keeping what is
61
+ already there unless `force`. Returns (moved, conflicts) as repo-relative-ish names.
62
+
63
+ A merge rather than a rename because the destination legitimately already exists: a
64
+ second foundation unpacked into the same lane, or a reference pack following the
65
+ primary one, both add to a test tree rather than replace it.
66
+ """
67
+ moved, conflicts = [], []
68
+ for dirpath, _dirnames, filenames in os.walk(src):
69
+ for name in sorted(filenames):
70
+ s = os.path.join(dirpath, name)
71
+ rel = os.path.relpath(s, src).replace("\\", "/")
72
+ d = os.path.join(dest, rel)
73
+ if os.path.exists(d):
74
+ if not force:
75
+ conflicts.append(rel)
76
+ continue
77
+ try:
78
+ os.remove(d)
79
+ except OSError:
80
+ conflicts.append(rel)
81
+ continue
82
+ os.makedirs(os.path.dirname(d) or ".", exist_ok=True)
83
+ shutil.move(s, d)
84
+ moved.append(rel)
85
+ # Leave nothing behind but the directories a conflict kept a file in.
86
+ for dirpath, _dirnames, _filenames in sorted(os.walk(src), reverse=True):
87
+ try:
88
+ os.rmdir(dirpath)
89
+ except OSError:
90
+ pass
91
+ return moved, conflicts
92
+
93
+
94
+ def redirect_tests(repo, out, test_path, force=False):
95
+ out_rel, test_rel = norm(out), norm(test_path)
96
+
97
+ def skip(why):
98
+ return {"moved": 0, "conflicts": [], "from": "", "to": test_rel,
99
+ "redirected": False, "message": why}
100
+
101
+ if not test_rel:
102
+ return skip("the lane declares no test path, so nothing is lifted")
103
+ # Nested or identical paths mean the pack's own layout is already the lane's: the
104
+ # tests it carries are inside the subtree they belong in, and moving them would
105
+ # take them out of it.
106
+ if out_rel and (test_rel == out_rel or test_rel.startswith(out_rel)):
107
+ return skip("this lane keeps its tests inside its source path (%s), "
108
+ "so the pack's layout is already right" % out_rel)
109
+
110
+ # WHAT the pack would have called its test tree, had it placed one: the last segment
111
+ # of the path the lane keeps tests in. Reading it from the lane rather than from a
112
+ # fixed list is what makes a custom `test_root` work.
113
+ seg = test_rel.rstrip("/").rsplit("/", 1)[-1]
114
+ src = os.path.join(repo, out_rel, seg) if out_rel else os.path.join(repo, seg)
115
+ dest = os.path.join(repo, test_rel)
116
+
117
+ if os.path.abspath(src) == os.path.abspath(dest):
118
+ return skip("the pack already writes its tests to %s" % test_rel)
119
+ if not os.path.isdir(src):
120
+ return skip("this pack carries no %s/ directory, so there are no tests to lift" % seg)
121
+
122
+ os.makedirs(dest, exist_ok=True)
123
+ moved, conflicts = merge_move(src, dest, force)
124
+ src_rel = "%s%s/" % (out_rel, seg)
125
+ if conflicts:
126
+ msg = ("lifted %d test file(s) from %s to the lane's test path %s; %d left in place "
127
+ "because %s already holds them" % (len(moved), src_rel, test_rel,
128
+ len(conflicts), test_rel))
129
+ else:
130
+ msg = ("lifted %d test file(s) from %s to the lane's test path %s"
131
+ % (len(moved), src_rel, test_rel))
132
+ return {"moved": len(moved), "conflicts": conflicts, "from": src_rel, "to": test_rel,
133
+ "redirected": bool(moved), "message": msg}
134
+
135
+
136
+ def main():
137
+ ap = argparse.ArgumentParser(description="lane-aware scaffolding helpers")
138
+ ap.add_argument("--repo", default=os.getcwd())
139
+ sub = ap.add_subparsers(dest="cmd", required=True)
140
+ r = sub.add_parser("redirect-tests")
141
+ r.add_argument("--out", required=True, help="the lane's source path, where the pack was unpacked")
142
+ r.add_argument("--test-path", default="", help="the lane's declared test path")
143
+ r.add_argument("--force", action="store_true")
144
+ args = ap.parse_args()
145
+
146
+ if args.cmd == "redirect-tests":
147
+ emit(redirect_tests(args.repo, args.out, args.test_path, args.force))
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()