aw-spec-cli 0.1.13 → 0.1.15

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 (30) hide show
  1. package/assets/.appweaver/STACK-PROFILES.md +1 -1
  2. package/assets/.appweaver/orchestrator/orchestrator.py +5 -0
  3. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +10 -3
  4. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +38 -11
  5. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +2 -1
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +607 -123
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +625 -66
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-ui.orch.yaml +407 -134
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +452 -143
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +78 -14
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan-ui.orch.yaml +597 -0
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +688 -0
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +3 -3
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +23 -23
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-story-plan.orch.yaml +945 -0
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-sync-stories.orch.yaml +1040 -0
  17. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +2069 -28
  18. package/assets/.appweaver/scripts/bash/aw-xref.sh +2 -1
  19. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +1973 -13
  20. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +2 -1
  21. package/assets/.appweaver/scripts/python/aw-xref.py +246 -20
  22. package/assets/.appweaver/templates/design-template.md +88 -54
  23. package/assets/.appweaver/templates/spec-template.md +12 -10
  24. package/assets/.appweaver/templates/story-template.md +48 -8
  25. package/assets/.appweaver/templates/ui-design-template.md +71 -39
  26. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +19 -7
  27. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +35 -26
  28. package/package.json +1 -1
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-design-ui.orch.yaml +0 -386
  30. package/assets/.appweaver/orchestrator/workflows/aw-spec-design.orch.yaml +0 -382
@@ -6,9 +6,10 @@
6
6
  #
7
7
  # aw-xref.ps1 record --using X --design <design.md> [--origin ...] [--date YYYY-MM-DD] [--root DIR]
8
8
  # aw-xref.ps1 list --aggregate Y [--root DIR] [--bundle knowledge]
9
- # aw-xref.ps1 concept-upsert --aggregate X [--source-files a,b,c] [--description D] [--origin ...] [--root DIR]
9
+ # aw-xref.ps1 concept-upsert --aggregate X [--source-files a,b,c] [--lane backend|frontend] [--merge] [--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 concept-coverage --aggregate X | --feature-dir <dir> [--ids "FR-001,BR-002"] [--max-list N] [--root DIR] [--bundle knowledge]
12
13
  $ErrorActionPreference = "Stop"
13
14
  $core = Join-Path $PSScriptRoot "..\python\aw-xref.py"
14
15
  & python3 $core @args
@@ -39,14 +39,19 @@ summary to STDERR so it never corrupts the JSON contract):
39
39
  list --aggregate Y [--root DIR] [--bundle knowledge]
40
40
  -> Y's blast radius (who uses Y). {aggregate, concept, users[], count, root}
41
41
 
42
- concept-upsert --aggregate X [--source-files a,b,c] [--merge] [--title T] [--description D]
42
+ concept-upsert --aggregate X [--source-files a,b,c] [--lane backend|frontend] [--merge]
43
+ [--title T] [--description D]
43
44
  [--origin greenfield|reverse-engineered] [--root DIR] [--bundle knowledge]
44
- -> create/update X's OWN concept (frontmatter type/title/source_files), PRESERVING
45
- its body — never clobbers the Cross-References other slices wrote. --merge UNIONs
46
- source_files (CR deltas) instead of replacing. source_files is written as a FLAT
47
- list, path-grouped by the code's OWN folders via YAML comment headers (adaptive —
48
- see _group_source_files), so the listing mirrors the real architecture.
49
- {aggregate, concept, created, source_file_count, root}
45
+ -> create/update X's OWN concept (frontmatter type/title/file lists), PRESERVING
46
+ its body — never clobbers the Cross-References other slices wrote. --lane picks
47
+ WHICH list this run owns: backend -> `source_files`, frontend -> `ui_source_files`.
48
+ Both keys are always present (an absent one claims "never implemented", which is
49
+ not the same as empty). --merge UNIONs that lane's list instead of replacing it —
50
+ what one story's slice, or a CR delta, must do so a second run does not drop the
51
+ first's files. Each list is written FLAT, path-grouped by the code's OWN folders
52
+ via YAML comment headers (adaptive — see _group_source_files), so the listing
53
+ mirrors the real architecture.
54
+ {aggregate, concept, created, lane, key, source_file_count, root}
50
55
 
51
56
  concept-overview --aggregate X (--text PROSE | --text-file F) [--root DIR] [--bundle knowledge]
52
57
  -> splice model-authored prose into X's `## Overview` ONLY (the one section a
@@ -63,6 +68,21 @@ summary to STDERR so it never corrupts the JSON contract):
63
68
  {aggregate, concept, found, paths[], count, csv, declared[], declared_count,
64
69
  missing[], missing_count, root}
65
70
 
71
+ concept-coverage --aggregate X | --feature-dir <dir> [--ids CSV] [--max-list N] [--root DIR] [--bundle knowledge]
72
+ -> READ-ONLY: what CODE EXISTS for one feature, split by LANE — `source_files`
73
+ (service) and `ui_source_files` (UI) as separate lists, plus the concept's
74
+ forward edges (Depends-On) and reverse edges (Cross-References, the blast
75
+ radius). The answer a STORY DESIGN needs, where concept-paths' one flat
76
+ backend list would hand a UI pass the service tree. `--ids` splits the
77
+ story's requirements into anchored/unanchored against the feature's
78
+ source-map.md where one exists; with no map every id is unanchored, which
79
+ is reported as "no index", never as "no code". `digest` is the rendered
80
+ block a design pass reads. Never stubs a concept.
81
+ {aggregate, concept, found, origin, backend_paths[]/_csv/_count,
82
+ frontend_paths[]/_csv/_count, has_backend_code, has_frontend_code,
83
+ backend_missing[], frontend_missing[], missing_count, depends_on[]/_csv,
84
+ used_by[]/_csv, source_map, anchored_csv, unanchored_csv, digest, root}
85
+
66
86
  validate [--prune] [--root DIR] [--bundle knowledge]
67
87
  -> drift check beyond reconcile-on-write. reverse-orphan = a Cross-References row with
68
88
  no backing Depends-On (stale; --prune removes it); forward-orphan = a Depends-On
@@ -478,13 +498,19 @@ def _resolve_concept(name, bundle_abs):
478
498
  return best
479
499
 
480
500
 
481
- def _skeleton_concept(name, origin, source_files, description):
501
+ def _skeleton_concept(name, origin, source_files, description, lane="backend"):
502
+ """A new concept always carries BOTH lane keys — `source_files` for the service
503
+ tree and `ui_source_files` for the UI one — with the run's files in the lane it
504
+ generated and an empty list in the other. Whichever lane implements first, the
505
+ other's key is there for it to fill; the template's rule is to leave it empty on a
506
+ single-lane slice, never to delete it."""
482
507
  fm = {
483
508
  "type": "aggregate",
484
509
  "title": name,
485
510
  "description": description or f"{name} aggregate.",
486
511
  "origin": origin or "greenfield",
487
- "source_files": list(source_files or []),
512
+ "source_files": list(source_files or []) if lane == "backend" else [],
513
+ "ui_source_files": list(source_files or []) if lane == "frontend" else [],
488
514
  }
489
515
  overview = f"[overview of the {name} aggregate]"
490
516
  body = (
@@ -788,13 +814,25 @@ def cmd_concept_upsert(args):
788
814
  _emit({"error": "aggregate-required", "root": root})
789
815
  return 2
790
816
  source_files = _parse_source_files(args.source_files, root)
817
+ # WHICH LANE'S FILE LIST THIS RUN OWNS. A feature has ONE concept across both lanes,
818
+ # and the two trees live under different keys: `source_files` is the service lane's,
819
+ # `ui_source_files` is the UI lane's. Writing UI files into the service key would
820
+ # collapse the concept resolver's module_dir to the repo root and hand every later
821
+ # service reader a tree of components — which is exactly what concept-coverage's
822
+ # lane split exists to prevent.
823
+ lane = (args.lane or "backend").strip().lower()
824
+ if lane not in ("backend", "frontend"):
825
+ print("[xref] concept-upsert: --lane must be backend or frontend, not %r" % lane, file=sys.stderr)
826
+ _emit({"error": "bad-lane", "lane": lane, "root": root})
827
+ return 2
828
+ key = "source_files" if lane == "backend" else "ui_source_files"
791
829
 
792
830
  path = _resolve_concept(aggregate, bundle_abs)
793
831
  created = path is None
794
832
  if created:
795
833
  _ensure_bundle(bundle_abs)
796
834
  path = _canonical_concept_path(bundle_abs, aggregate)
797
- _write(path, _skeleton_concept(aggregate, origin, source_files, args.description or ""))
835
+ _write(path, _skeleton_concept(aggregate, origin, source_files, args.description or "", lane))
798
836
  _ensure_dir_index(path, aggregate)
799
837
  else:
800
838
  # Update frontmatter ONLY (source_files/description/origin); PRESERVE the body,
@@ -804,18 +842,23 @@ def cmd_concept_upsert(args):
804
842
  fm["title"] = fm.get("title") or aggregate
805
843
  if source_files:
806
844
  if args.merge:
807
- # CR mode: {{files}} is only the delta's changed files — UNION with the
808
- # slice's existing source_files, never replace (that would drop the rest).
809
- existing = fm.get("source_files") if isinstance(fm.get("source_files"), list) else []
845
+ # INCREMENTAL mode: {{files}} is only what THIS run wrote — one story's
846
+ # slice, or a delta's changed files — so UNION with what the lane already
847
+ # records, never replace. Replacing would drop every file the feature's
848
+ # earlier stories generated the moment a second story implements.
849
+ existing = fm.get(key) if isinstance(fm.get(key), list) else []
810
850
  merged = list(existing)
811
851
  for s in source_files:
812
852
  if s not in merged:
813
853
  merged.append(s)
814
- fm["source_files"] = merged
854
+ fm[key] = merged
815
855
  else:
816
- fm["source_files"] = source_files
817
- elif "source_files" not in fm:
818
- fm["source_files"] = []
856
+ fm[key] = source_files
857
+ # Both keys always present: an absent one reads as "this lane was never
858
+ # implemented" to every consumer, which is a different claim from "empty".
859
+ for k in ("source_files", "ui_source_files"):
860
+ if k not in fm:
861
+ fm[k] = []
819
862
  if args.description:
820
863
  fm["description"] = args.description
821
864
  elif "description" not in fm:
@@ -828,9 +871,10 @@ def cmd_concept_upsert(args):
828
871
  _ensure_dir_index(path, aggregate)
829
872
 
830
873
  print(f"[xref] concept {aggregate}: {'created' if created else 'updated'} "
831
- f"({len(source_files)} source file(s)) — {_rel(path, root)}", file=sys.stderr)
874
+ f"({len(source_files)} {lane} source file(s) into {key}) — {_rel(path, root)}",
875
+ file=sys.stderr)
832
876
  _emit({"aggregate": aggregate, "concept": _rel(path, root), "created": "true" if created else "false",
833
- "source_file_count": len(source_files), "root": root})
877
+ "lane": lane, "key": key, "source_file_count": len(source_files), "root": root})
834
878
  return 0
835
879
 
836
880
 
@@ -935,6 +979,177 @@ def cmd_concept_paths(args):
935
979
  return 0
936
980
 
937
981
 
982
+ def cmd_concept_coverage(args):
983
+ """CODE COVERAGE for one feature, read from its OKF concept — what code EXISTS
984
+ today for the aggregate a story is about to design against, SPLIT BY LANE.
985
+
986
+ concept-paths answers "which files is this slice made of" for the executable
987
+ phases, as one flat backend list. A story design needs the same facts shaped
988
+ differently: the service design reads `source_files`, the frontend design reads
989
+ `ui_source_files`, and neither should be handed the other's tree — a UI pass
990
+ told to read twelve C# files designs around them. It also needs the two things
991
+ a path list drops: the aggregate's forward edges (Depends-On), and its REVERSE
992
+ edges (Cross-References) — the blast radius a slice inherits the moment it
993
+ changes a published surface.
994
+
995
+ COVERAGE, HONESTLY. A greenfield concept holds no requirement→file index: code
996
+ carries no FR ids. A feature that was ADOPTED does — the source map's `Spec ids`
997
+ column — so the ids given in --ids come back split into ANCHORED (some artefact
998
+ claims the id) and UNANCHORED. With no source map every id is unanchored, which
999
+ is the true answer, and it is NEVER reported as uncovered: absent evidence is not
1000
+ evidence of absence, and a design pass told "FR-003 has no code" when the index
1001
+ simply does not exist would re-plan work that already ships.
1002
+
1003
+ STRICTLY READ-ONLY, the same contract as concept-paths: a query never stubs a
1004
+ concept, and a feature with no concept at all — greenfield, before the first
1005
+ implement — is a legitimate EMPTY answer with `found: false`, not an error."""
1006
+ root = _root(args.root)
1007
+ bundle_abs = os.path.join(root, args.bundle)
1008
+ aggregate = args.aggregate or _agg_from_feature_dir(args.feature_dir)
1009
+ if not aggregate:
1010
+ print("[xref] concept-coverage: --aggregate or --feature-dir required", file=sys.stderr)
1011
+ _emit({"error": "aggregate-required", "root": root})
1012
+ return 2
1013
+
1014
+ path = _resolve_concept(aggregate, bundle_abs)
1015
+ fm = {}
1016
+ if path:
1017
+ fm, _ = _read_fm(_read(path))
1018
+
1019
+ def lane_files(key):
1020
+ """One frontmatter key's paths, split into on-disk and declared-but-missing.
1021
+ _read_fm gives a list for a block list and "" for an empty scalar; the
1022
+ `# Layer` group headers are comments and are already skipped by it."""
1023
+ raw = fm.get(key)
1024
+ items = raw if isinstance(raw, list) else ([raw] if isinstance(raw, str) and raw else [])
1025
+ seen, existing, missing = set(), [], []
1026
+ for s in items:
1027
+ s = (s or "").strip().strip('"').strip("'").replace(os.sep, "/")
1028
+ if not s or s in seen:
1029
+ continue
1030
+ seen.add(s)
1031
+ target = s if os.path.isabs(s) else os.path.join(root, s)
1032
+ (existing if os.path.isfile(target) else missing).append(s)
1033
+ return existing, missing
1034
+
1035
+ be, be_missing = lane_files("source_files")
1036
+ fe, fe_missing = lane_files("ui_source_files")
1037
+
1038
+ # ---- requirement anchors, where an index exists ---------------------------
1039
+ ids = [i for i in re.split(r'[,\s]+', (args.ids or "").strip()) if i]
1040
+ smap = ""
1041
+ anchors = {}
1042
+ if args.feature_dir:
1043
+ cand = os.path.join(args.feature_dir, "source-map.md")
1044
+ if os.path.isfile(cand):
1045
+ smap = _rel(cand, root)
1046
+ text = _read(cand)
1047
+ lines = text.splitlines()
1048
+ start = next((i for i, ln in enumerate(lines) if ln.startswith("## Artefact")), None)
1049
+ if start is not None:
1050
+ end = next((j for j in range(start + 1, len(lines))
1051
+ if lines[j].startswith("## ")), len(lines))
1052
+ for r in _table_data_rows("\n".join(lines[start:end])):
1053
+ # | # | Legacy artefact | Kind | Spec ids | Design element | ... |
1054
+ if len(r) < 4:
1055
+ continue
1056
+ artefact = r[1].strip().strip("`")
1057
+ for rid in re.findall(r'\b[A-Z]{2}-\d+', r[3]):
1058
+ anchors.setdefault(rid, [])
1059
+ if artefact and artefact not in anchors[rid]:
1060
+ anchors[rid].append(artefact)
1061
+ anchored = [i for i in ids if anchors.get(i)]
1062
+ unanchored = [i for i in ids if not anchors.get(i)]
1063
+
1064
+ deps = [r[0].strip() for r in (_read_deps_rows(path) if path else [])
1065
+ if r and _IDENT_RE.match((r[0] or "").strip())]
1066
+ users = sorted(set(_xref_users(aggregate, bundle_abs))) if path else []
1067
+
1068
+ # ---- the digest — the block a design pass reads --------------------------
1069
+ cap = args.max_list if args.max_list and args.max_list > 0 else 60
1070
+
1071
+ def block(label, files, missing, empty_note):
1072
+ if not files:
1073
+ return ["%s: %s" % (label, empty_note)]
1074
+ out = ["%s: %d file(s) on disk" % (label, len(files))]
1075
+ shown = 0
1076
+ for group, items in _group_source_files(files):
1077
+ if shown >= cap:
1078
+ break
1079
+ out.append(" %s" % group)
1080
+ for f in items:
1081
+ if shown >= cap:
1082
+ break
1083
+ out.append(" %s" % f)
1084
+ shown += 1
1085
+ if len(files) > shown:
1086
+ out.append(" … and %d more (the full list is in the lane's csv)" % (len(files) - shown))
1087
+ if missing:
1088
+ out.append(" DRIFT — declared in the concept, not on disk (%d): %s"
1089
+ % (len(missing), ", ".join(missing[:12])
1090
+ + (" …" if len(missing) > 12 else "")))
1091
+ return out
1092
+
1093
+ lines = []
1094
+ if not path:
1095
+ lines.append("No OKF concept for %s — nothing has been implemented for this feature yet. "
1096
+ "Every lane below is GREENFIELD: design from a clean slate." % aggregate)
1097
+ lines.append("")
1098
+ else:
1099
+ lines.append("%s — code coverage from the OKF concept (%s, origin: %s)"
1100
+ % (aggregate, _rel(path, root), (fm.get("origin") or "unknown")))
1101
+ lines.append("")
1102
+ lines += block("BACKEND", be, be_missing,
1103
+ "no service code recorded for this aggregate — greenfield on this lane")
1104
+ lines.append("")
1105
+ lines += block("FRONTEND", fe, fe_missing,
1106
+ "no UI code recorded for this aggregate — greenfield on this lane")
1107
+ lines.append("")
1108
+ lines.append("DEPENDS ON (this aggregate reaches into): %s" % (", ".join(deps) or "nothing"))
1109
+ lines.append("USED BY (change this aggregate's surface and these break): %s"
1110
+ % (", ".join(users) or "nothing recorded"))
1111
+ if ids:
1112
+ lines.append("")
1113
+ if not smap:
1114
+ lines.append("REQUIREMENT ANCHORS: none available — this feature has no source-map.md, "
1115
+ "so no id can be traced to a file. Treat the files above as the whole of "
1116
+ "what exists and read them; do NOT read an id's absence here as proof "
1117
+ "that no code implements it.")
1118
+ else:
1119
+ lines.append("REQUIREMENT ANCHORS (%s):" % smap)
1120
+ for i in anchored:
1121
+ lines.append(" %-10s %s" % (i, ", ".join(anchors[i][:6])
1122
+ + (" …" if len(anchors[i]) > 6 else "")))
1123
+ if unanchored:
1124
+ lines.append(" not anchored to any artefact: %s — the map claims no code for "
1125
+ "them, so design them as new work unless the files above say otherwise."
1126
+ % ", ".join(unanchored))
1127
+ digest = "\n".join(lines)
1128
+
1129
+ print("[xref] concept-coverage %s: backend %d, frontend %d, drift %d, used-by %d"
1130
+ % (aggregate, len(be), len(fe), len(be_missing) + len(fe_missing), len(users)),
1131
+ file=sys.stderr)
1132
+ _emit({"aggregate": aggregate, "concept": _rel(path, root) if path else "",
1133
+ "found": bool(path), "origin": (fm.get("origin") or ""),
1134
+ "backend_paths": be, "backend_csv": ",".join(be), "backend_count": len(be),
1135
+ "frontend_paths": fe, "frontend_csv": ",".join(fe), "frontend_count": len(fe),
1136
+ "has_backend_code": bool(be), "has_frontend_code": bool(fe),
1137
+ "backend_missing": be_missing, "frontend_missing": fe_missing,
1138
+ "missing_count": len(be_missing) + len(fe_missing),
1139
+ # THE UNION, for a reader whose question spans both lanes. A gate that traces a
1140
+ # requirement to code has to see the UI half of it: an FR- with a user-facing
1141
+ # surface is realised in two trees, and `source_files` alone is the service one.
1142
+ "all_paths": be + fe, "all_csv": ",".join(be + fe), "all_count": len(be) + len(fe),
1143
+ "all_missing": be_missing + fe_missing,
1144
+ "all_missing_csv": ", ".join(be_missing + fe_missing),
1145
+ "depends_on": deps, "depends_on_csv": ", ".join(deps),
1146
+ "used_by": users, "used_by_csv": ", ".join(users),
1147
+ "source_map": smap, "anchored_csv": ", ".join(anchored),
1148
+ "unanchored_csv": ", ".join(unanchored),
1149
+ "digest": digest, "root": root})
1150
+ return 0
1151
+
1152
+
938
1153
  # ─────────────────────────────── validate ──────────────────────────────────
939
1154
  def cmd_validate(args):
940
1155
  """Cross-reference drift check (the safety net BEYOND reconcile-on-write). Pairs the
@@ -1273,7 +1488,8 @@ def main(argv):
1273
1488
  c.add_argument("--aggregate", default="")
1274
1489
  c.add_argument("--feature-dir", dest="feature_dir", default="")
1275
1490
  c.add_argument("--source-files", default="")
1276
- c.add_argument("--merge", action="store_true", help="union source_files with existing (CR delta) instead of replacing")
1491
+ c.add_argument("--merge", action="store_true", help="union the lane's file list with existing (one story's slice, a CR delta) instead of replacing")
1492
+ c.add_argument("--lane", default="backend", help="backend -> source_files, frontend -> ui_source_files")
1277
1493
  c.add_argument("--title", default="")
1278
1494
  c.add_argument("--description", default="")
1279
1495
  c.add_argument("--origin", default="greenfield")
@@ -1297,6 +1513,16 @@ def main(argv):
1297
1513
  cp.add_argument("--bundle", default="knowledge")
1298
1514
  cp.set_defaults(func=cmd_concept_paths)
1299
1515
 
1516
+ cc = sub.add_parser("concept-coverage", help="read-only: lane-split code coverage for one feature (story design)")
1517
+ cc.add_argument("--aggregate", default="")
1518
+ cc.add_argument("--feature-dir", dest="feature_dir", default="")
1519
+ cc.add_argument("--ids", default="", help="requirement ids to report anchors for (csv)")
1520
+ cc.add_argument("--max-list", dest="max_list", type=int, default=60,
1521
+ help="cap on files listed per lane in the digest (the csv stays complete)")
1522
+ cc.add_argument("--root", default="")
1523
+ cc.add_argument("--bundle", default="knowledge")
1524
+ cc.set_defaults(func=cmd_concept_coverage)
1525
+
1300
1526
  vv = sub.add_parser("validate", help="report cross-reference drift (orphan rows)")
1301
1527
  vv.add_argument("--prune", action="store_true", help="remove stale reverse orphans (safe self-heal)")
1302
1528
  vv.add_argument("--root", default="")
@@ -2,40 +2,56 @@
2
2
 
3
3
  **Aggregate (feature)**: `[Aggregate]` <!-- the feature = the aggregate, e.g. Order -->
4
4
 
5
- **Design home**: `specs/[Aggregate]/design.md`
5
+ **Design home**: `specs/[Aggregate]/backend-plan.md` <!-- the service lane; the frontend twin is frontend-plan.md, owned by /appweaver plan-ui -->
6
6
 
7
7
  **Source spec**: `specs/[Aggregate]/spec.md`
8
8
 
9
- **Version**: v1.0 <!-- moves in lockstep with the spec; every applied change request bumps this — see Change History -->
9
+ **Version**: v1.0 <!-- SCRIPT-OWNED: plan-stamp sets this to the spec version this plan derives from. Never edit it by hand. -->
10
10
 
11
11
  **Status**: Draft
12
12
 
13
13
  <!--
14
14
  WHAT THIS DOCUMENT IS
15
15
  =====================
16
- The single, living High-Level Design (HLD) for ONE aggregate. It is the
17
- MEDIATOR between spec.md (WHAT/WHY, tech-agnostic) and the generated code
18
- (HOW, tech-specific). It is NOT code: it carries structure, contracts,
19
- behaviour, and decisions — enough for ANY capable LLM to generate correct,
20
- spec-satisfying code on ANY target stack, but no complete method bodies.
16
+ The single, living High-Level Design (HLD) for ONE aggregate — the FEATURE-LEVEL
17
+ ARCHITECTURAL CONFIRMATION. It is the MEDIATOR between spec.md (WHAT/WHY,
18
+ tech-agnostic) and the generated code (HOW, tech-specific). It is NOT code: it
19
+ carries structure, contracts, behaviour, and decisions — enough for ANY capable LLM
20
+ to generate correct, spec-satisfying code on ANY target stack, but no complete
21
+ method bodies.
21
22
 
22
- spec.md ──▶ design.md ──▶ code (+ tests)
23
- behaviour structure & generated by `implement`
24
- (FR/BR/US/SC) decisions (HLD) (stack-bound at generation time)
23
+ spec.md ──▶ backend-plan.md ──▶ story-backend-plan.md ──▶ code (+ tests)
24
+ behaviour structure & the story's slice generated by `implement`
25
+ (FR/BR/CN/SC) decisions (HLD) (concrete, ephemeral)
26
+
27
+ It is LONG-LIVED and moves FORWARD with the spec: /appweaver plan authors it, and
28
+ conforms it whenever the spec's Change History says something it realises has moved.
29
+ Story designs READ it and never write back — nothing merges into this document.
25
30
 
26
31
  ANCHORING RULES
27
- - Every design element traces UP to a spec id (FR-/BR-/CN-/DC-/IC-/US-/SC-) and
28
- the whole spec is covered DOWN here (see §6 Traceability). No design element
29
- without a spec reason; no spec requirement without a design home.
32
+ - Every design element traces UP to a spec id (FR-/BR-/CN-/DC-/IC-/SC-) and the
33
+ whole spec is covered DOWN here (see §6 Traceability). No design element without
34
+ a spec reason; no spec requirement without a design home.
30
35
  - THE SPEC IS THE ONLY DOMAIN INPUT. Design reads the spec + the technical/
31
36
  architecture standards — NOT the domain rule files. The domain reached this doc
32
37
  only as spec ids: business invariants (BR-), domain-derived constraints (CN- —
33
38
  retention/immutability/audit/limits), data classification (DC-), and interop
34
39
  obligations (IC-). Realise each of those; invent no domain rule the spec omits
35
40
  (if one seems missing, record it as an Assumption in §7, never smuggle it in).
36
- - Feature = aggregate. Commands (CreateOrder, UpdateOrder…) are USER STORIES,
37
- not features. One aggregate = one design.md = one owning design for its code
38
- files. A later requirement is a delta on THIS doc, never a new one.
41
+ - THE CODE IS THE WHAT-EXISTS INPUT, and it is a different authority from the spec.
42
+ Where code already realises this feature, the plan describes and ADJUSTS TO the
43
+ structure that is actually there rather than an idealised one, and names any
44
+ deliberate divergence as a §8 WA-n. The code never overrides the spec on
45
+ BEHAVIOUR, and it may not introduce a domain rule the spec omits — code that
46
+ contradicts the spec is a finding to report, not a design to bless.
47
+ - NO STORY IDS ANYWHERE IN THIS DOCUMENT. Stories live in stories/ and cite the
48
+ requirement ids they deliver; this design traces to FR-/BR-, which is what keeps
49
+ it stable while the backlog churns, is reprioritised and is re-raised.
50
+ - Feature = aggregate. Commands (CreateOrder, UpdateOrder…) are REQUIREMENTS
51
+ (FR-n) in the spec, not features and not stories. One aggregate = one
52
+ backend-plan.md = one owning design for its code files. A later requirement is
53
+ CONFORMED into this document by /appweaver plan's update mode — never a new
54
+ design doc, and never a delta document.
39
55
 
40
56
  WHAT LIVES ELSEWHERE (do NOT repeat it here — it is identical for every feature)
41
57
  - The clean-architecture layering + dependency rule (API → Application → Domain
@@ -47,6 +63,13 @@
47
63
  - Physical persistence (PKs/FKs, columns, indexes, JSON shapes, migration SQL)
48
64
  belongs to a technical-data-model. This doc states WHAT is stored and WHICH
49
65
  port persists it, not the schema.
66
+ - THE PUBLISHED SURFACE belongs to `interfaces.md`, this feature's APPEND-ONLY
67
+ interface log — the anchor the frontend lane designs against. /appweaver plan
68
+ publishes each entry there (the script assigns IF-nnn, the date and
69
+ status: proposed); /appweaver plan-ui and the FE story designs READ the latest
70
+ entry per id. §3 here is the INDEX that cites those ids, never a second copy of
71
+ the contract: two copies drift, and only the log has a lifecycle and a
72
+ supersedes: chain.
50
73
 
51
74
  TECHNOLOGY-AGNOSTIC (mandatory)
52
75
  - Express behaviour as SIGNATURES + pre/post/invariant/raises pseudocode and
@@ -128,7 +151,7 @@ trade-off. Layering follows the project architecture standard (constitution).]
128
151
 
129
152
  | Event | Raised by | Trigger | Key payload | Serves |
130
153
  |---|---|---|---|---|
131
- | `[EntityStateChanged]` | `[Root/Entity]` | [what causes it] | `[id, key fields]` | FR-00X · US-Y |
154
+ | `[EntityStateChanged]` | `[Root/Entity]` | [what causes it] | `[id, key fields]` | FR-00X · BR-00Z |
132
155
 
133
156
  ### 1.3 Entities
134
157
 
@@ -162,7 +185,7 @@ trade-off. Layering follows the project architecture standard (constitution).]
162
185
  Invariant: [what must hold — maps to a spec BR-nnn]
163
186
  Post-condition: [resulting state / outputs]
164
187
  Raises: [DomainEvent]
165
- Serves: FR-00X · US-Y · BR-00Z
188
+ Serves: FR-00X · BR-00Z
166
189
  ```
167
190
 
168
191
  **Invariants** *(always-true; each maps to a spec BR-nnn — do not add rules absent from the spec)*
@@ -182,13 +205,13 @@ trade-off. Layering follows the project architecture standard (constitution).]
182
205
  Purpose: [why this can't live on one entity]
183
206
  Steps: 1. … 2. … 3. …
184
207
  Touches: [aggregates/ports coordinated]
185
- Serves: FR-00X · US-Y
208
+ Serves: FR-00X
186
209
  ```
187
210
 
188
211
  ## 2. Application Layer
189
212
 
190
213
  <!--
191
- Use-cases: one command/query per user-story action, its handler's orchestration,
214
+ Use-cases: one command/query per requirement action (FR-n), its handler's orchestration,
192
215
  the ports it depends on, application-level rules (authorization, input validation,
193
216
  transaction + event publication), and the request/response DTO shapes. Consumes
194
217
  Domain types and the PORTS the Domain/Application defines; NEVER touches
@@ -206,9 +229,9 @@ trade-off. Layering follows the project architecture standard (constitution).]
206
229
 
207
230
  ### 2.2 Use-cases (commands / queries)
208
231
 
209
- <!-- One block per use-case = one user-story action. This is the LLM's unit of work. -->
232
+ <!-- One block per use-case = one requirement action. This is the LLM's unit of work. -->
210
233
 
211
- #### `[Command/Query]` — Serves US-Y · FR-00X
234
+ #### `[Command/Query]` — Serves FR-00X
212
235
 
213
236
  - **Type**: command (writes) | query (reads)
214
237
  - **Request (DTO)**: `{ [field]: [type], … }`
@@ -224,33 +247,41 @@ trade-off. Layering follows the project architecture standard (constitution).]
224
247
  4. [persist via port] (single transaction boundary)
225
248
  5. [publish domain events]
226
249
  6. return [Response]
227
- On failure: [which error → which API status in §3]
250
+ On failure: [which error → which published-interface status; the contract is the
251
+ IF- entry cited in §3]
228
252
  ```
229
253
  - **Emits**: `[DomainEvent]` (from §1.2)
230
254
 
231
- ## 3. API Layer
255
+ ## 3. API Layer — the published surface (INDEX; the wording lives in `interfaces.md`)
232
256
 
233
257
  <!--
234
- The contract other systems couple to. Describe it at PROTOCOL level (REST assumed
235
- per the constitution — adapt for gRPC/GraphQL/CLI if the stack differs), NOT as
236
- controller code. Pin route, verb, auth, request/response shape, status codes, and
237
- the error contract. Delegates to an Application use-case; no business logic here.
258
+ The contract other systems and the frontend couple to. Describe it at PROTOCOL level
259
+ (REST assumed per the constitution — adapt for gRPC/GraphQL/CLI if the stack differs),
260
+ NEVER as controller code. Delegates to an Application use-case; no business logic here.
261
+
262
+ WHERE IT LIVES. Each published interface is an entry in `specs/[Aggregate]/interfaces.md`,
263
+ the append-only interface log: route/verb, auth, request and response shapes, status
264
+ codes and the error contract. `plan` publishes those entries — the script assigns the
265
+ IF-nnn id, the date and status: proposed — and `plan-ui`, the FE story designs and any
266
+ consuming feature read the LATEST entry per id. This section is the INDEX only.
267
+
268
+ HOW TO FILL IT
269
+ - An interface that ALREADY has an entry in the log (it is in context on an update
270
+ run): cite its IF- id here, and describe nothing.
271
+ - An interface being published for the FIRST TIME in this run: describe it in the
272
+ "Interface" cell in one line and leave the IF- id cell as `(this run)` — the log
273
+ assigns the real id after this document is written, and the next update run cites
274
+ it. Never invent an IF- number: the script is the only id authority.
275
+ - CHANGING a published interface is a NEW entry that supersedes the old one, never
276
+ an edit to the existing entry and never a change to this table alone.
277
+ - Every row cites the spec ids it serves and the use-case that implements it, so
278
+ §6 Traceability and the analyze gate can follow the chain both ways.
238
279
  -->
239
280
 
240
- ### `[VERB] [/route]` — [purpose] · Serves US-Y · FR-00X
241
-
242
- - **Auth**: [required role/scope]
243
- - **Request**: `[path/query params]` + body `{ [field]: [type], … }`
244
- - **Success**: `2xx` → `{ [response shape] }`
245
- - **Errors** *(the error contract)*:
246
-
247
- | Status | When | Body shape |
248
- |---|---|---|
249
- | 400 | [validation failure] | `{ errors: [{ field, message }] }` |
250
- | 403 | [unauthorized] | `{ error }` |
251
- | 409 | [conflict / duplicate — maps to a BR] | `{ error }` |
252
-
253
- - **Maps to**: `[Command/Query]` (§2.2)
281
+ | Interface | IF- id | Serves | Implemented by |
282
+ |---|---|---|---|
283
+ | `[VERB] [/route]` — [purpose] | IF-00n | FR-00X | `[Command/Query]` (§2.2) |
284
+ | `[DomainEvent]` published outside this feature | (this run) | FR-00X · BR-00X | §2.2 emits, §1.2 |
254
285
 
255
286
  ## 4. Infrastructure Layer
256
287
 
@@ -316,7 +347,7 @@ trade-off. Layering follows the project architecture standard (constitution).]
316
347
  ## 6. Traceability (the spine)
317
348
 
318
349
  <!--
319
- The two-way guarantee: every FR/BR/CN/DC/IC/US/SC has a design home, and every
350
+ The two-way guarantee: every FR/BR/CN/DC/IC/SC has a design home, and every
320
351
  design element has a spec reason. This is what makes impact analysis exact and lets
321
352
  the analyze gate verify coverage. Keep it complete — EVERY CN-/DC-/IC- in the spec
322
353
  MUST appear as a row here (that is how the analyze gate proves no lifted constraint
@@ -331,7 +362,6 @@ trade-off. Layering follows the project architecture standard (constitution).]
331
362
  | CN-001 | constraint | [where realised — §1 invariant / §4 persistence / §5 cross-cutting] | [layer] |
332
363
  | DC-001 | classification | [exposure enforcement — §2 projection / §5 data-protection] | App·Infra |
333
364
  | IC-001 | contract | [adapter/port realising it — §4.2] | Infra |
334
- | US-1 | story | [end-to-end elements realising it] | all |
335
365
  | SC-001 | success | [where it is met + how verified] | [layer] |
336
366
 
337
367
  ## 7. Assumptions (to clarify later)
@@ -369,15 +399,19 @@ trade-off. Layering follows the project architecture standard (constitution).]
369
399
  ## 9. Change History
370
400
 
371
401
  <!--
372
- The living-truth ledger, in lockstep with spec.md. Every applied change request
373
- appends a row here, carrying the version it ships as. finalize-delta writes the row
374
- when it merges the delta; close-delta then archives the CR, bumps the minor version
375
- in active_version.json and stamps that same number into this document's **Version**
376
- header — one mechanism, one stage, so header, row and active_version.json never
377
- disagree. A change to this aggregate is a DELTA on THIS document (delta-design:
378
- ADDED / MODIFIED / REMOVED sections), never a new design doc. Newest last.
402
+ The living-truth ledger, in lockstep with spec.md — MANDATORY, and the one section
403
+ whose loss is refused rather than absorbed: plan-stamp writes no baseline at all if
404
+ this section went missing, so a dropped ledger can never be silently stamped over.
405
+
406
+ SCRIPT-OWNED. `plan-stamp` is the only writer: it appends one row per update naming
407
+ the requirement ids conformed, and moves the **Version** header to the spec version
408
+ this plan now derives from. The model writes NO version and NO row, ever — not here,
409
+ not in the header. Newest last.
410
+
411
+ Three columns, matching exactly what the stamp writes: the derived spec version, the
412
+ date, and what changed (the conformed ids are carried in that cell).
379
413
  -->
380
414
 
381
- | Version | Date | Change | User stories | CR |
382
- |---------|------|--------|--------------|-----|
383
- | v1.0 | [DATE] | Initial design | US-1 | — |
415
+ | Version | Date | Change |
416
+ |---------|------|--------|
417
+ | v1.0 | [DATE] | Initial design |