aw-spec-cli 0.1.21 → 0.1.23
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/STACK-PROFILES.md +87 -8
- package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
- package/assets/.appweaver/orchestrator/orchestrator.py +47 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +41 -8
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +62 -2
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +18 -5
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +4 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +115 -27
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +5 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +225 -27
- package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +4 -15
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1 -1
- 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-domain.orch.yaml +3 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +104 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +159 -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 +153 -0
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +203 -212
- package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +191 -224
- package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
- package/assets/.appweaver/scripts/python/aw-crstate.py +127 -2
- package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
- package/assets/.appweaver/scripts/python/aw-payload-parity.py +17 -1
- package/assets/.appweaver/scripts/python/aw-plan-coverage.py +84 -4
- 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 +6 -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 +38 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +19 -0
- package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
- package/assets/.appweaver/stack-profiles/node-express.json +26 -0
- package/assets/.appweaver/stack-profiles/python-fastapi.json +15 -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/ui-model-template.md +67 -0
- package/assets/.appweaver/templates/workflow-model-template.md +62 -0
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +3 -3
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +5 -123
- package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +784 -0
- package/assets/foundations/java/Hexagonal/pack-foundation.sh +357 -0
- package/assets/foundations/java/Hexagonal/unpack-foundation.sh +399 -0
- package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +44 -0
- package/package.json +1 -1
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
# version-read [--feature-dir <path>] -> {feature, version, active_change, exists, bootstrap_pending}
|
|
50
50
|
# version-init [--feature-dir <path>] [--bootstrap] -> {…, created, bootstrap_pending} (create active_version.json v1.0; --bootstrap marks a FIRST draft in progress)
|
|
51
51
|
# version-bootstrap-done [--feature-dir <path>] -> {…, cleared} (the ONLY clearer of the --bootstrap marker; call it when the first draft is finished)
|
|
52
|
-
# feature-paths -> {feature, feature_dir, spec_file, backend_plan, frontend_plan, *_exists,
|
|
52
|
+
# feature-paths -> {feature, feature_slug, feature_dir, spec_file, backend_plan, frontend_plan, *_exists,
|
|
53
53
|
# design_file/ui_design_file (aliases of the two plans), spec_exists, bootstrap_pending, needs_bootstrap}
|
|
54
54
|
# checklist-read --file <path> -> {exists, total, checked, items[]}
|
|
55
55
|
# checklist-apply --file <path> --results <json>-> {exists, before_checked, after_checked, total, newly_passing[], regressions[]}
|
|
@@ -1100,9 +1100,12 @@ _cr_id_token() { # $1 = raw --cr-id value
|
|
|
1100
1100
|
}
|
|
1101
1101
|
|
|
1102
1102
|
# One change's wave, read from the documents that carry it: the delta-spec first, then
|
|
1103
|
-
# the model delta. Both are stamped together, so either answers
|
|
1104
|
-
#
|
|
1105
|
-
|
|
1103
|
+
# the model delta. Both are stamped together, so either answers. EMPTY when neither carries
|
|
1104
|
+
# a readable one — `**Wave**: [N]` is the unfilled template line, so "no wave" is a state
|
|
1105
|
+
# that happens, and a caller who WRITES the answer somewhere must not write a number nobody
|
|
1106
|
+
# derived. A phantom wave is worse than none: the merge and recall fences can see that a
|
|
1107
|
+
# wave is missing and say so, but a 1 that was invented downstream reads to them as fact.
|
|
1108
|
+
_wave_of_readable() { # $1 = abs feature dir, $2 = cr id, $3 = repo_root
|
|
1106
1109
|
local fd="$1" cr="$2" repo_root="$3" w=""
|
|
1107
1110
|
local dspec="$fd/changes/$cr/delta-spec.md"
|
|
1108
1111
|
local ctx_dir; ctx_dir=$(_context_dir_for_feature "$fd" "$repo_root")
|
|
@@ -1112,11 +1115,20 @@ _wave_of() { # $1 = abs feature dir, $2 = cr id, $3 = repo_root
|
|
|
1112
1115
|
w=$(sed -n -E 's/^\*\*Wave\*\*:[[:space:]]*([0-9]+).*$/\1/p' "$f" | head -1)
|
|
1113
1116
|
[ -n "$w" ] && break
|
|
1114
1117
|
done
|
|
1115
|
-
case "$w" in ''|*[!0-9]*) w=
|
|
1116
|
-
[ "$w"
|
|
1118
|
+
case "$w" in ''|*[!0-9]*) w="" ;; esac
|
|
1119
|
+
if [ -n "$w" ]; then
|
|
1120
|
+
[ "$w" -ge 1 ] 2>/dev/null || w=""
|
|
1121
|
+
fi
|
|
1117
1122
|
printf '%s' "$w"
|
|
1118
1123
|
}
|
|
1119
1124
|
|
|
1125
|
+
# The same reading with the floor applied, for the readers that must show a number — a
|
|
1126
|
+
# listing has a column to fill and 1 is the floor for a change opened before waves existed.
|
|
1127
|
+
_wave_of() { # $1 = abs feature dir, $2 = cr id, $3 = repo_root
|
|
1128
|
+
local w; w=$(_wave_of_readable "$1" "$2" "$3")
|
|
1129
|
+
printf '%s' "${w:-1}"
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1120
1132
|
# ─────────────────────────────── context-paths ─────────────────────────────
|
|
1121
1133
|
# Everything specify needs to know about the domain model of the context it is working
|
|
1122
1134
|
# in: where the master is, whether it exists yet, and EVERY open delta model beside it.
|
|
@@ -1664,6 +1676,16 @@ cmd_feature_paths() {
|
|
|
1664
1676
|
fi
|
|
1665
1677
|
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
1666
1678
|
local feature; feature=$(basename "$fd") # the aggregate name (feature = specs/<Feature>)
|
|
1679
|
+
# feature_slug is the URL-safe form of that name, and it is what the UI stages use
|
|
1680
|
+
# as a route segment and a directory name (app/<slug>/page.tsx). It is derived here
|
|
1681
|
+
# rather than per workflow so every reader gets the same segment for one feature.
|
|
1682
|
+
# CamelCase splits on the case boundary before the non-alphanumeric squeeze, so
|
|
1683
|
+
# OrderManagement and "Order Management" both reach order-management.
|
|
1684
|
+
local feature_slug
|
|
1685
|
+
feature_slug=$(printf '%s' "$feature" \
|
|
1686
|
+
| sed -E 's/([a-z0-9])([A-Z])/\1-\2/g; s/([A-Z]+)([A-Z][a-z])/\1-\2/g' \
|
|
1687
|
+
| tr '[:upper:]' '[:lower:]' \
|
|
1688
|
+
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')
|
|
1667
1689
|
# The master documents of a feature: spec.md (WHAT/WHY) + one LANE PLAN per declared
|
|
1668
1690
|
# lane (each lane's HOW) — backend-plan.md for the service lane, frontend-plan.md for
|
|
1669
1691
|
# the frontend lane. `plan` writes them; nothing else does.
|
|
@@ -1696,8 +1718,8 @@ cmd_feature_paths() {
|
|
|
1696
1718
|
local needs_bootstrap=false
|
|
1697
1719
|
[ "$spec_exists" = false ] && needs_bootstrap=true
|
|
1698
1720
|
[ "$bootstrap_pending" = true ] && needs_bootstrap=true
|
|
1699
|
-
printf '{"feature":"%s","feature_dir":"%s","spec_file":"%s","backend_plan":"%s","frontend_plan":"%s","backend_plan_exists":%s,"frontend_plan_exists":%s,"design_file":"%s","ui_design_file":"%s","spec_exists":%s,"design_exists":%s,"ui_design_exists":%s,"bootstrap_pending":%s,"needs_bootstrap":%s}\n' \
|
|
1700
|
-
"$(json_escape "$feature")" "$(json_escape "$fd")" "$(json_escape "$spec")" \
|
|
1721
|
+
printf '{"feature":"%s","feature_slug":"%s","feature_dir":"%s","spec_file":"%s","backend_plan":"%s","frontend_plan":"%s","backend_plan_exists":%s,"frontend_plan_exists":%s,"design_file":"%s","ui_design_file":"%s","spec_exists":%s,"design_exists":%s,"ui_design_exists":%s,"bootstrap_pending":%s,"needs_bootstrap":%s}\n' \
|
|
1722
|
+
"$(json_escape "$feature")" "$(json_escape "$feature_slug")" "$(json_escape "$fd")" "$(json_escape "$spec")" \
|
|
1701
1723
|
"$(json_escape "$design")" "$(json_escape "$ui_design")" "$design_exists" "$ui_design_exists" \
|
|
1702
1724
|
"$(json_escape "$design")" "$(json_escape "$ui_design")" "$spec_exists" "$design_exists" "$ui_design_exists" \
|
|
1703
1725
|
"$bootstrap_pending" "$needs_bootstrap"
|
|
@@ -2564,6 +2586,25 @@ print(json.dumps({"ok": ok, "exists": True, "issues": issues,
|
|
|
2564
2586
|
PY
|
|
2565
2587
|
}
|
|
2566
2588
|
|
|
2589
|
+
# ─────────────────────────────── principles-list ───────────────────────────
|
|
2590
|
+
# The ratified NON-NEGOTIABLE principles. A thin wrapper: the scan lives in
|
|
2591
|
+
# python/aw-principles.py, which BOTH shells invoke, so the list a gate cites cannot
|
|
2592
|
+
# differ between them. Defaults to the two halves specify loads; --file overrides.
|
|
2593
|
+
cmd_principles_list() {
|
|
2594
|
+
local rest=()
|
|
2595
|
+
while [ $# -gt 0 ]; do
|
|
2596
|
+
case "$1" in
|
|
2597
|
+
--help|-h) echo "principles-list [--file <path>]... -> {files[], missing[], principles[], count, titles_csv}"; return 0 ;;
|
|
2598
|
+
*) rest+=("$1"); shift ;;
|
|
2599
|
+
esac
|
|
2600
|
+
done
|
|
2601
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
2602
|
+
local core="$SCRIPT_DIR/../python/aw-principles.py"
|
|
2603
|
+
if [ ! -f "$core" ]; then echo "principles-list: core not found at $core" >&2; return 1; fi
|
|
2604
|
+
if ! command -v python3 >/dev/null 2>&1; then echo "principles-list: python3 is required" >&2; return 1; fi
|
|
2605
|
+
python3 "$core" list --root "$repo_root" "${rest[@]+"${rest[@]}"}"
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2567
2608
|
# ────────────────────────── agent-context-update ───────────────────────────
|
|
2568
2609
|
# Run the agent-context extension's updater (refreshes the managed AW-Spec block).
|
|
2569
2610
|
cmd_agent_context_update() {
|
|
@@ -2924,7 +2965,14 @@ cmd_delta_plan_scaffold() {
|
|
|
2924
2965
|
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$target"; else touch "$target"; fi
|
|
2925
2966
|
created="true"
|
|
2926
2967
|
fi
|
|
2927
|
-
|
|
2968
|
+
# The lane plan carries the change's wave so a reader of the plan alone can see where it
|
|
2969
|
+
# sits. It is COPIED from the delta-spec, never decided here — so when the spec has no
|
|
2970
|
+
# readable wave the template's `[N]` is left standing, which is what the merge and recall
|
|
2971
|
+
# fences read as "not ordered yet" and refuse on, with the command that stamps it.
|
|
2972
|
+
local scaffold_wave; scaffold_wave=$(_wave_of_readable "$fd" "$cr_id" "$repo_root")
|
|
2973
|
+
if [ -n "$scaffold_wave" ]; then
|
|
2974
|
+
_stamp_header "$target" "Wave" "$scaffold_wave"
|
|
2975
|
+
fi
|
|
2928
2976
|
local override_active="false"
|
|
2929
2977
|
[ -f "$repo_root/.appweaver/templates/overrides/${template_arg}.md" ] && override_active="true"
|
|
2930
2978
|
local template_source; template_source=$(resolve_template "$template_arg" "$repo_root") || true
|
|
@@ -2997,13 +3045,17 @@ cmd_test_run() {
|
|
|
2997
3045
|
# feature's aggregate name (feature-dir basename) plus any --also
|
|
2998
3046
|
# referrer names — the per-feature integrity scope. .NET only; npm/none
|
|
2999
3047
|
# ignore it and run the full suite.
|
|
3048
|
+
# --also is REPEATABLE and the lists UNION. A merge's blast radius has two halves that
|
|
3049
|
+
# are computed separately — the referrer closure and the features that co-own the
|
|
3050
|
+
# aggregates the change touches — and the dialect cannot concatenate two variables into
|
|
3051
|
+
# one argument, so the caller passes each list as its own --also.
|
|
3000
3052
|
local feature_dir="" scope="repo" also=""
|
|
3001
3053
|
while [ $# -gt 0 ]; do
|
|
3002
3054
|
case "$1" in
|
|
3003
3055
|
--feature-dir) feature_dir="$2"; shift 2 ;;
|
|
3004
3056
|
--scope) scope="$2"; shift 2 ;;
|
|
3005
|
-
--also) also="$2"; shift 2 ;;
|
|
3006
|
-
--help|-h) echo "test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]"; return 0 ;;
|
|
3057
|
+
--also) also="${also:+$also,}$2"; shift 2 ;;
|
|
3058
|
+
--help|-h) echo "test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]... (--also repeatable; lists union)"; return 0 ;;
|
|
3007
3059
|
*) echo "test-run: unknown arg '$1'" >&2; return 1 ;;
|
|
3008
3060
|
esac
|
|
3009
3061
|
done
|
|
@@ -3493,8 +3545,12 @@ required = max([d["wave"] for d in known], default=0) + 1
|
|
|
3493
3545
|
current = wave_of(cr)
|
|
3494
3546
|
ok = current is not None and current >= required
|
|
3495
3547
|
|
|
3548
|
+
# A change with NO readable wave is stamped too, not only one sitting too low. The merge
|
|
3549
|
+
# and recall fences refuse on an unordered change and send the operator here, so the one
|
|
3550
|
+
# command they are given has to be able to write the first wave as well as correct a wrong
|
|
3551
|
+
# one — a remedy that declines the case it was named for is no remedy.
|
|
3496
3552
|
stamped = []
|
|
3497
|
-
if stamp and current is
|
|
3553
|
+
if stamp and (current is None or current < required):
|
|
3498
3554
|
line = "**Wave**: %d" % required
|
|
3499
3555
|
pat = re.compile(r"^\*\*Wave\*\*:.*$", re.M)
|
|
3500
3556
|
for p in present:
|
|
@@ -3505,13 +3561,16 @@ if stamp and current is not None and current < required:
|
|
|
3505
3561
|
if stamped:
|
|
3506
3562
|
current, ok = required, True
|
|
3507
3563
|
|
|
3508
|
-
if
|
|
3509
|
-
msg = "%s declares no predecessor, so wave %d is the floor; it carries %s" % (
|
|
3510
|
-
cr, required, current if current is not None else "no wave")
|
|
3511
|
-
elif stamped:
|
|
3564
|
+
if stamped and deps:
|
|
3512
3565
|
msg = "%s depends on %s, so it was raised to wave %d in %s" % (
|
|
3513
3566
|
cr, ", ".join("%s (wave %s)" % (d["cr_id"], d["wave"]) for d in known),
|
|
3514
3567
|
required, ", ".join(stamped))
|
|
3568
|
+
elif stamped:
|
|
3569
|
+
msg = "%s declares no predecessor, so it was stamped at wave %d — the floor — in %s" % (
|
|
3570
|
+
cr, required, ", ".join(stamped))
|
|
3571
|
+
elif not deps:
|
|
3572
|
+
msg = "%s declares no predecessor, so wave %d is the floor; it carries %s" % (
|
|
3573
|
+
cr, required, current if current is not None else "no wave")
|
|
3515
3574
|
elif ok:
|
|
3516
3575
|
msg = "%s is at wave %s, at or above the %d its `## Depends on` requires" % (
|
|
3517
3576
|
cr, current, required)
|
|
@@ -4434,7 +4493,7 @@ cmd_cr_merge_check() {
|
|
|
4434
4493
|
case "$1" in
|
|
4435
4494
|
--cr-id) cr_id="$2"; shift 2 ;;
|
|
4436
4495
|
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
4437
|
-
--help|-h) echo "cr-merge-check --cr-id <id> [--feature-dir <path>] -> {rebase_needed, base_version, current_version, blockers[], blocker_count, wave, …}"; return 0 ;;
|
|
4496
|
+
--help|-h) echo "cr-merge-check --cr-id <id> [--feature-dir <path>] -> {rebase_needed, base_version, current_version, blockers[], blocker_count, wave, wave_ordering_ok, unordered[], …}"; return 0 ;;
|
|
4438
4497
|
*) echo "cr-merge-check: unknown arg '$1'" >&2; return 1 ;;
|
|
4439
4498
|
esac
|
|
4440
4499
|
done
|
|
@@ -4462,14 +4521,8 @@ def norm(v):
|
|
|
4462
4521
|
if not v: return ""
|
|
4463
4522
|
return v if v.startswith("v") else "v" + v
|
|
4464
4523
|
|
|
4465
|
-
# THIS change's own spec half
|
|
4524
|
+
# THIS change's own spec half.
|
|
4466
4525
|
my_spec = os.path.join(fd, "changes", cr, "delta-spec.md")
|
|
4467
|
-
def wave_of(p, default=1):
|
|
4468
|
-
try: t = open(p, encoding="utf-8").read()
|
|
4469
|
-
except Exception: return default
|
|
4470
|
-
m = re.search(r"^\*\*Wave\*\*:[ \t]*(\d+)", t, re.M)
|
|
4471
|
-
return int(m.group(1)) if m else default
|
|
4472
|
-
wave = wave_of(my_spec)
|
|
4473
4526
|
base_version = norm(header(my_spec, "Base version"))
|
|
4474
4527
|
|
|
4475
4528
|
# The master's CURRENT version, from the ledger — the same authority version-read reports,
|
|
@@ -4486,52 +4539,65 @@ if not current:
|
|
|
4486
4539
|
# absence of evidence and is not treated as a move.
|
|
4487
4540
|
rebase_needed = bool(base_version and current and base_version != current)
|
|
4488
4541
|
|
|
4489
|
-
#
|
|
4490
|
-
#
|
|
4491
|
-
# a
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
"feature": feat})
|
|
4501
|
-
for f in sorted(os.listdir(ctx)) if os.path.isdir(ctx) else []:
|
|
4502
|
-
m = re.match(r"^domain-model\.(.+)\.delta\.md$", f)
|
|
4503
|
-
if not m: continue
|
|
4504
|
-
other = m.group(1)
|
|
4505
|
-
opens.setdefault(other, {"cr_id": other, "wave": wave_of(os.path.join(ctx, f)), "feature": ""})
|
|
4506
|
-
|
|
4507
|
-
blockers = [v for k, v in sorted(opens.items()) if k != cr and v["wave"] < wave]
|
|
4508
|
-
|
|
4509
|
-
# THE MASTERS CAN MOVE WITHOUT THE VERSION MOVING. finalize-delta merges and close-delta
|
|
4510
|
-
# bumps, so between the two the masters carry a change the number does not yet reflect — and
|
|
4511
|
-
# a second delta sharing the same base_version would compare equal and merge onto masters
|
|
4512
|
-
# that had already shifted under it. The proof is the Change History row finalize-delta
|
|
4513
|
-
# appends, carrying the id in its CR cell.
|
|
4542
|
+
# BOTH CONTEXT-WIDE QUESTIONS COME FROM aw-crstate, for every reader of them and for both
|
|
4543
|
+
# shells: which open changes this one is ordered against, and which of them have already
|
|
4544
|
+
# merged. Each was once a copy of the scan living in this payload and another in its
|
|
4545
|
+
# PowerShell twin, which is how the two came to answer the same question differently.
|
|
4546
|
+
#
|
|
4547
|
+
# PREDECESSORS. Every open change in the context, both halves — changes/<id>/ under each
|
|
4548
|
+
# feature is the authority for "open", and the pending model deltas are unioned in so a
|
|
4549
|
+
# model-only artifact of a half-built change is still seen. A change whose `**Wave**:` is
|
|
4550
|
+
# unreadable is reported as unordered rather than assumed to be wave 1, which is what let an
|
|
4551
|
+
# unstamped delta pose as a predecessor and let an unstamped SUBJECT skip this fence
|
|
4552
|
+
# entirely.
|
|
4514
4553
|
#
|
|
4515
|
-
#
|
|
4516
|
-
#
|
|
4517
|
-
#
|
|
4518
|
-
#
|
|
4519
|
-
#
|
|
4520
|
-
#
|
|
4521
|
-
|
|
4554
|
+
# MERGED-OPEN. The masters can move without the version moving: finalize-delta merges and
|
|
4555
|
+
# close-delta bumps, so between the two the masters carry a change the number does not yet
|
|
4556
|
+
# reflect, and a second delta sharing the same base_version would compare equal and merge
|
|
4557
|
+
# onto masters that had already shifted under it. The proof is the Change History row
|
|
4558
|
+
# finalize-delta appends, carrying the id in its CR cell — which replaced `cr in spec.md`,
|
|
4559
|
+
# a test that fired on an id quoted in prose and matched a prefix of a longer id. It looks
|
|
4560
|
+
# where a merge can actually show: this feature's spec AND the context model every feature
|
|
4561
|
+
# shares, so a SIBLING feature's merged-unclosed change is no longer invisible. A sibling
|
|
4562
|
+
# blocks only where it touched an aggregate this delta also names (`blocking`); a merge in
|
|
4563
|
+
# this feature blocks outright, because it moved this delta's own master.
|
|
4564
|
+
_m, _why = None, ""
|
|
4522
4565
|
try:
|
|
4523
4566
|
import importlib.util
|
|
4524
4567
|
_mod_path = os.path.join(root, ".appweaver", "scripts", "python", "aw-crstate.py")
|
|
4525
4568
|
_spec = importlib.util.spec_from_file_location("aw_crstate", _mod_path)
|
|
4526
|
-
|
|
4527
|
-
_spec.loader.exec_module(
|
|
4528
|
-
|
|
4569
|
+
_loaded = importlib.util.module_from_spec(_spec)
|
|
4570
|
+
_spec.loader.exec_module(_loaded)
|
|
4571
|
+
_m = _loaded
|
|
4529
4572
|
except Exception as _e:
|
|
4530
|
-
|
|
4531
|
-
|
|
4573
|
+
_why = str(_e)
|
|
4574
|
+
|
|
4575
|
+
order, merged_open, merged_text = None, None, ""
|
|
4576
|
+
if _m is not None:
|
|
4577
|
+
try:
|
|
4578
|
+
order = _m.wave_order(ctx, fd, cr, "lower", "context")
|
|
4579
|
+
merged_open = [r for r in _m.merged_open(ctx, fd, cr) if r["blocking"]]
|
|
4580
|
+
merged_text = _m.merged_open_text(merged_open)
|
|
4581
|
+
except Exception as _e:
|
|
4582
|
+
order, merged_open, _why = None, None, str(_e)
|
|
4583
|
+
|
|
4584
|
+
# Fail CLOSED on an unreadable helper: report the state as blocking rather than clear, and
|
|
4585
|
+
# say which check could not run rather than which condition was found.
|
|
4586
|
+
if order is None:
|
|
4587
|
+
order = {"wave": None, "wave_known": False, "related": [], "related_count": 0,
|
|
4588
|
+
"related_ids": "", "unknown": [], "unknown_count": 0, "unknown_ids": "",
|
|
4589
|
+
"unknown_blocks": True, "wave_ordering_ok": False,
|
|
4590
|
+
"wave_problem": "the wave reader could not be run (%s), so no open change in "
|
|
4591
|
+
"this context is ordered against %s at all" % (_why, cr)}
|
|
4592
|
+
if merged_open is None:
|
|
4593
|
+
merged_open = [{"cr_id": "(merged-open check failed: %s)" % _why, "blocking": True}]
|
|
4594
|
+
merged_text = merged_open[0]["cr_id"]
|
|
4595
|
+
blockers = order["related"]
|
|
4532
4596
|
|
|
4533
4597
|
print(json.dumps({
|
|
4534
|
-
"ok": True, "cr_id": cr,
|
|
4598
|
+
"ok": True, "cr_id": cr,
|
|
4599
|
+
"wave": order["wave"] if order["wave"] is not None else "",
|
|
4600
|
+
"wave_known": order["wave_known"],
|
|
4535
4601
|
"exists": os.path.isdir(os.path.join(fd, "changes", cr)),
|
|
4536
4602
|
# REBASE half — the masters moved because a change CLOSED and bumped the number.
|
|
4537
4603
|
"base_version": base_version, "current_version": current,
|
|
@@ -4540,14 +4606,20 @@ print(json.dumps({
|
|
|
4540
4606
|
# PREDECESSOR half. Lower wave = committed to land first.
|
|
4541
4607
|
"blockers": blockers, "blocker_count": len(blockers),
|
|
4542
4608
|
"has_blockers": len(blockers) > 0,
|
|
4543
|
-
"blocker_ids":
|
|
4609
|
+
"blocker_ids": order["related_ids"],
|
|
4610
|
+
# The changes this one cannot be ordered against at all, in either direction.
|
|
4611
|
+
"unordered": order["unknown"], "unordered_count": order["unknown_count"],
|
|
4612
|
+
"unordered_ids": order["unknown_ids"],
|
|
4613
|
+
"wave_ordering_ok": order["wave_ordering_ok"],
|
|
4614
|
+
"wave_problem": order["wave_problem"],
|
|
4544
4615
|
# The other way the masters move: a change MERGED and has not closed yet.
|
|
4545
4616
|
"merged_open": merged_open, "merged_open_count": len(merged_open),
|
|
4546
4617
|
"has_merged_open": len(merged_open) > 0,
|
|
4547
|
-
"merged_open_ids":
|
|
4548
|
-
# All
|
|
4549
|
-
# and a gate that must read
|
|
4550
|
-
"merge_ok": (not rebase_needed) and
|
|
4618
|
+
"merged_open_ids": merged_text,
|
|
4619
|
+
# All four refusals as ONE positive flag, because the engine takes no compound condition
|
|
4620
|
+
# and a gate that must read four booleans would need four passes over one question.
|
|
4621
|
+
"merge_ok": (not rebase_needed) and order["wave_ordering_ok"] and len(blockers) == 0
|
|
4622
|
+
and len(merged_open) == 0,
|
|
4551
4623
|
}))
|
|
4552
4624
|
PY
|
|
4553
4625
|
}
|
|
@@ -4572,7 +4644,7 @@ cmd_cr_recall_check() {
|
|
|
4572
4644
|
case "$1" in
|
|
4573
4645
|
--cr-id) cr_id="$2"; shift 2 ;;
|
|
4574
4646
|
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
4575
|
-
--help|-h) echo "cr-recall-check --cr-id <id> [--feature-dir <path>] -> {merged, dependants[], dependant_count, wave, …}"; return 0 ;;
|
|
4647
|
+
--help|-h) echo "cr-recall-check --cr-id <id> [--feature-dir <path>] -> {merged, dependants[], dependant_count, wave, wave_ordering_ok, unordered[], …}"; return 0 ;;
|
|
4576
4648
|
*) echo "cr-recall-check: unknown arg '$1'" >&2; return 1 ;;
|
|
4577
4649
|
esac
|
|
4578
4650
|
done
|
|
@@ -4588,21 +4660,13 @@ root = os.environ.get("AW_ROOT", ""); ctx = os.environ.get("AW_CTX", "")
|
|
|
4588
4660
|
changes = os.path.join(fd, "changes")
|
|
4589
4661
|
cr_dir = os.path.join(changes, cr)
|
|
4590
4662
|
|
|
4591
|
-
def wave_of(d):
|
|
4592
|
-
for name in ("delta-spec.md",):
|
|
4593
|
-
p = os.path.join(d, name)
|
|
4594
|
-
if os.path.isfile(p):
|
|
4595
|
-
m = re.search(r'^\*\*Wave\*\*:[ \t]*(\d+)', open(p, encoding="utf-8").read(), re.M)
|
|
4596
|
-
if m:
|
|
4597
|
-
return int(m.group(1))
|
|
4598
|
-
return 1
|
|
4599
|
-
|
|
4600
4663
|
# MERGED = a Change History row whose CR cell IS this change, in this feature's spec or in
|
|
4601
4664
|
# the context model every feature shares — aw-crstate owns that test for all three readers
|
|
4602
4665
|
# of it. The old test was `cr in line`, which fired on an id quoted in prose and matched a
|
|
4603
4666
|
# prefix of a longer id.
|
|
4604
4667
|
spec = os.path.join(fd, "spec.md")
|
|
4605
4668
|
merged, evidence, merged_doc = False, "", ""
|
|
4669
|
+
order, _why = None, ""
|
|
4606
4670
|
try:
|
|
4607
4671
|
import importlib.util
|
|
4608
4672
|
_mod_path = os.path.join(root, ".appweaver", "scripts", "python", "aw-crstate.py")
|
|
@@ -4611,29 +4675,39 @@ try:
|
|
|
4611
4675
|
_spec.loader.exec_module(_m)
|
|
4612
4676
|
evidence, merged_doc = _m.merged_in([spec, os.path.join(ctx, "domain-model.md")], cr)
|
|
4613
4677
|
merged = bool(evidence)
|
|
4614
|
-
|
|
4678
|
+
# DEPENDANTS, from the same reader the merge fence uses in the opposite direction. An
|
|
4679
|
+
# unreadable `**Wave**:` is not wave 1 here either: it used to mean `w > wave` was false
|
|
4680
|
+
# and the change was not counted, so recall withdrew the ground from under a dependant
|
|
4681
|
+
# it could not see. Waves have no ceiling, so any unordered change could sit above this
|
|
4682
|
+
# one and every one of them holds recall.
|
|
4683
|
+
order = _m.wave_order(ctx, fd, cr, "higher", "feature")
|
|
4684
|
+
except Exception as _e:
|
|
4615
4685
|
# Fail CLOSED: an unreadable helper must not report "never merged", which is the
|
|
4616
4686
|
# answer that lets close-delta archive a change nobody merged.
|
|
4617
|
-
merged, evidence = True, "(merged check unavailable — treated as merged)"
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
w = wave_of(d)
|
|
4627
|
-
if w > wave:
|
|
4628
|
-
dependants.append({"cr_id": name, "wave": w})
|
|
4687
|
+
merged, evidence, _why = True, "(merged check unavailable — treated as merged)", str(_e)
|
|
4688
|
+
|
|
4689
|
+
if order is None:
|
|
4690
|
+
order = {"wave": None, "wave_known": False, "related": [], "related_count": 0,
|
|
4691
|
+
"related_ids": "", "unknown": [], "unknown_count": 0, "unknown_ids": "",
|
|
4692
|
+
"unknown_blocks": True, "wave_ordering_ok": False,
|
|
4693
|
+
"wave_problem": "the wave reader could not be run (%s), so no open change in "
|
|
4694
|
+
"this feature is ordered against %s at all" % (_why, cr)}
|
|
4695
|
+
dependants = order["related"]
|
|
4629
4696
|
|
|
4630
4697
|
print(json.dumps({
|
|
4631
|
-
"ok": True, "cr_id": cr, "exists": os.path.isdir(cr_dir),
|
|
4698
|
+
"ok": True, "cr_id": cr, "exists": os.path.isdir(cr_dir),
|
|
4699
|
+
"wave": order["wave"] if order["wave"] is not None else "",
|
|
4700
|
+
"wave_known": order["wave_known"],
|
|
4632
4701
|
"merged": merged, "merge_evidence": evidence,
|
|
4633
4702
|
"dependants": dependants,
|
|
4634
|
-
"dependant_ids":
|
|
4703
|
+
"dependant_ids": order["related_ids"],
|
|
4635
4704
|
"dependant_count": len(dependants),
|
|
4636
4705
|
"has_dependants": len(dependants) > 0,
|
|
4706
|
+
# The changes this one cannot be ordered against at all, in either direction.
|
|
4707
|
+
"unordered": order["unknown"], "unordered_count": order["unknown_count"],
|
|
4708
|
+
"unordered_ids": order["unknown_ids"],
|
|
4709
|
+
"wave_ordering_ok": order["wave_ordering_ok"],
|
|
4710
|
+
"wave_problem": order["wave_problem"],
|
|
4637
4711
|
}))
|
|
4638
4712
|
PY
|
|
4639
4713
|
}
|
|
@@ -4641,6 +4715,12 @@ PY
|
|
|
4641
4715
|
# ──────────────────── plan-gate-stamp / plan-gate-check ────────────────────
|
|
4642
4716
|
# THE RECEIPT THAT analyze-plan PASSED, and over WHICH BYTES.
|
|
4643
4717
|
#
|
|
4718
|
+
# THE RULE ITSELF LIVES IN python/aw-plangate.py, which both shells invoke — these two are
|
|
4719
|
+
# thin wrappers that resolve the feature and hand over. It is a set of sha comparisons with
|
|
4720
|
+
# three deliberate asymmetries in it, and while it was embedded twice (a heredoc here, a
|
|
4721
|
+
# native function there) every change to it had to be re-derived by hand on the other side
|
|
4722
|
+
# and kept in step by eye. It changed twice that way. Now there is nothing to keep in step.
|
|
4723
|
+
#
|
|
4644
4724
|
# implement generates from the lane plans and deliberately does NOT read the spec — its
|
|
4645
4725
|
# own header says "analyze-plan already conformed the plans to it". That sentence was an
|
|
4646
4726
|
# assumption about the order the operator ran things in, not a fact anything checked: the
|
|
@@ -4719,75 +4799,23 @@ PY
|
|
|
4719
4799
|
}
|
|
4720
4800
|
|
|
4721
4801
|
cmd_plan_gate_stamp() {
|
|
4722
|
-
|
|
4802
|
+
case "${1:-}" in
|
|
4803
|
+
--help|-h) echo "plan-gate-stamp [--feature-dir <path>] [--spec-version <v>] [--mode <text>] [--verdict conformant|routed-gaps|gaps] [--plan-gaps <n>] [--routed <json list>] -> {file, gated, verdict, hashed[], count}"; return 0 ;;
|
|
4804
|
+
esac
|
|
4805
|
+
local feature_dir_arg="" rest=()
|
|
4723
4806
|
while [ $# -gt 0 ]; do
|
|
4724
4807
|
case "$1" in
|
|
4725
4808
|
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
4726
|
-
|
|
4727
|
-
--mode) mode="$2"; shift 2 ;;
|
|
4728
|
-
--verdict) verdict="$2"; shift 2 ;;
|
|
4729
|
-
--plan-gaps) plan_gaps="$2"; shift 2 ;;
|
|
4730
|
-
--routed) routed="$2"; shift 2 ;;
|
|
4731
|
-
--help|-h) echo "plan-gate-stamp [--feature-dir <path>] [--spec-version <v>] [--mode <text>] [--verdict conformant|routed-gaps|gaps] [--plan-gaps <n>] [--routed <json list>] -> {file, gated, verdict, hashed[], count}"; return 0 ;;
|
|
4732
|
-
*) echo "plan-gate-stamp: unknown arg '$1'" >&2; return 1 ;;
|
|
4809
|
+
*) rest+=("$1"); shift ;;
|
|
4733
4810
|
esac
|
|
4734
4811
|
done
|
|
4735
4812
|
local repo_root; repo_root=$(get_repo_root)
|
|
4736
4813
|
local fd; fd=$(_resolve_feature_dir "$feature_dir_arg" "$repo_root") || { echo "plan-gate-stamp: no feature set and no --feature-dir" >&2; return 1; }
|
|
4737
4814
|
local ctx_dir; ctx_dir=$(_context_dir_for_feature "$fd" "$repo_root")
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
def sha(p):
|
|
4743
|
-
try:
|
|
4744
|
-
with open(p, "rb") as f: return hashlib.sha256(f.read()).hexdigest()
|
|
4745
|
-
except Exception: return ""
|
|
4746
|
-
def rel(p): return os.path.relpath(p, root)
|
|
4747
|
-
# Every document the gate READ. A lane that is not declared has no plan on disk and is
|
|
4748
|
-
# simply absent from the map — the check treats a name it does not carry as not gated,
|
|
4749
|
-
# never as changed, so declaring a second lane later does not read as tampering.
|
|
4750
|
-
#
|
|
4751
|
-
# THE LEGACY NAMES ARE HERE FOR THE SAME REASON version-stamp CARRIES THEM. feature-paths
|
|
4752
|
-
# resolves design.md / design.ui.md under the backend_plan / frontend_plan keys, so a tree
|
|
4753
|
-
# that has not been through `plan` since the rename — every brownfield feature `adopt`
|
|
4754
|
-
# produces — is GATED on those documents. Hashing only the new names would leave such a
|
|
4755
|
-
# feature with an empty plan map: a receipt that looks tracked and fresh while the document
|
|
4756
|
-
# implement actually generates from is unwatched.
|
|
4757
|
-
plans = {}
|
|
4758
|
-
for name in ("backend-plan.md", "frontend-plan.md", "design.md", "design.ui.md"):
|
|
4759
|
-
h = sha(os.path.join(fd, name))
|
|
4760
|
-
if h: plans[name] = h
|
|
4761
|
-
# An unrecognised verdict is recorded as `gaps`, the one that refuses.
|
|
4762
|
-
verdict = os.environ.get("AW_VERDICT", "").strip()
|
|
4763
|
-
if verdict not in ("conformant", "routed-gaps", "gaps"):
|
|
4764
|
-
verdict = "gaps"
|
|
4765
|
-
try: plan_gaps = int(os.environ.get("AW_PLAN_GAPS", "0") or 0)
|
|
4766
|
-
except ValueError: plan_gaps = 0
|
|
4767
|
-
try: routed = [str(x) for x in json.loads(os.environ.get("AW_ROUTED") or "[]")]
|
|
4768
|
-
except Exception: routed = []
|
|
4769
|
-
rec = {
|
|
4770
|
-
"gated": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
4771
|
-
"gate_mode": os.environ.get("AW_MODE", "master"),
|
|
4772
|
-
"spec_version": os.environ.get("AW_VER", "").strip(),
|
|
4773
|
-
"verdict": verdict,
|
|
4774
|
-
"plan_gaps": plan_gaps,
|
|
4775
|
-
"routed_gaps": routed,
|
|
4776
|
-
"spec_sha": sha(os.path.join(fd, "spec.md")),
|
|
4777
|
-
"model_sha": sha(os.path.join(ctx, "domain-model.md")),
|
|
4778
|
-
"plans": plans,
|
|
4779
|
-
"note": ("Written by every master-mode analyze-plan run. It records the bytes that were "
|
|
4780
|
-
"checked against each other and the verdict on them, not a version — the plans' "
|
|
4781
|
-
"**Version**: headers are stamped in lockstep by close-delta and cannot answer "
|
|
4782
|
-
"this. implement reads it through plan-gate-check: moved bytes or verdict `gaps` "
|
|
4783
|
-
"refuse; `routed-gaps` lists what /appweaver specify still has to lift."),
|
|
4784
|
-
}
|
|
4785
|
-
p = os.path.join(fd, ".aw-plan-gate.json")
|
|
4786
|
-
open(p, "w", encoding="utf-8").write(json.dumps(rec, indent=2) + "\n")
|
|
4787
|
-
print(json.dumps({"file": rel(p), "gated": rec["gated"], "spec_version": rec["spec_version"],
|
|
4788
|
-
"verdict": verdict, "plan_gaps": plan_gaps, "routed_gaps": len(routed),
|
|
4789
|
-
"hashed": sorted(plans.keys()), "count": len(plans), "ok": True}))
|
|
4790
|
-
PY
|
|
4815
|
+
local core="$SCRIPT_DIR/../python/aw-plangate.py"
|
|
4816
|
+
if [ ! -f "$core" ]; then echo "plan-gate-stamp: core not found at $core" >&2; return 1; fi
|
|
4817
|
+
if ! command -v python3 >/dev/null 2>&1; then echo "plan-gate-stamp: python3 is required" >&2; return 1; fi
|
|
4818
|
+
python3 "$core" stamp --feature-dir "$fd" --context-dir "$ctx_dir" --root "$repo_root" "${rest[@]+"${rest[@]}"}"
|
|
4791
4819
|
}
|
|
4792
4820
|
|
|
4793
4821
|
cmd_plan_gate_check() {
|
|
@@ -4802,64 +4830,25 @@ cmd_plan_gate_check() {
|
|
|
4802
4830
|
local repo_root; repo_root=$(get_repo_root)
|
|
4803
4831
|
local fd; fd=$(_resolve_feature_dir "$feature_dir_arg" "$repo_root") || { echo "plan-gate-check: no feature set and no --feature-dir" >&2; return 1; }
|
|
4804
4832
|
local ctx_dir; ctx_dir=$(_context_dir_for_feature "$fd" "$repo_root")
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
"in master mode since the gate began leaving a receipt")
|
|
4825
|
-
else:
|
|
4826
|
-
out["gated"] = rec.get("gated", "")
|
|
4827
|
-
out["spec_version"] = rec.get("spec_version", "")
|
|
4828
|
-
# A receipt from before the verdict existed was only ever written on a clean pass.
|
|
4829
|
-
out["verdict"] = rec.get("verdict") or "conformant"
|
|
4830
|
-
out["plan_gaps"] = rec.get("plan_gaps") or 0
|
|
4831
|
-
out["routed_gaps"] = [str(x) for x in (rec.get("routed_gaps") or [])]
|
|
4832
|
-
out["routed_csv"] = "; ".join(out["routed_gaps"])
|
|
4833
|
-
out["spec_moved"] = bool(rec.get("spec_sha")) and rec["spec_sha"] != sha(os.path.join(fd, "spec.md"))
|
|
4834
|
-
# A model recorded as absent that is still absent has not moved. A context that
|
|
4835
|
-
# GAINED a domain model since the gate has moved: check 4 never ran against it.
|
|
4836
|
-
out["model_moved"] = rec.get("model_sha", "") != sha(os.path.join(ctx, "domain-model.md"))
|
|
4837
|
-
changed = []
|
|
4838
|
-
for name, was in sorted((rec.get("plans") or {}).items()):
|
|
4839
|
-
if sha(os.path.join(fd, name)) != was:
|
|
4840
|
-
changed.append(name)
|
|
4841
|
-
out["plans_changed"] = changed
|
|
4842
|
-
out["plans_changed_csv"] = ", ".join(changed)
|
|
4843
|
-
out["gate_fresh"] = not (out["spec_moved"] or out["model_moved"] or changed)
|
|
4844
|
-
# Moved bytes refuse whatever the verdict was; unmoved bytes refuse only on `gaps`.
|
|
4845
|
-
out["may_generate"] = out["gate_fresh"] and out["verdict"] != "gaps"
|
|
4846
|
-
if not out["gate_fresh"]:
|
|
4847
|
-
bits = []
|
|
4848
|
-
if out["spec_moved"]: bits.append("spec.md has changed")
|
|
4849
|
-
if out["model_moved"]: bits.append("domain-model.md has changed")
|
|
4850
|
-
if changed: bits.append("%s changed (a re-derive by `plan main`, or a hand edit)" % ", ".join(changed))
|
|
4851
|
-
out["reason"] = "since analyze-plan checked them on %s: %s" % (out["gated"], "; ".join(bits))
|
|
4852
|
-
elif out["verdict"] == "gaps":
|
|
4853
|
-
out["reason"] = ("analyze-plan on %s left %s gap(s) a plan rewrite closes, unrepaired"
|
|
4854
|
-
% (out["gated"], out["plan_gaps"] or "some"))
|
|
4855
|
-
elif out["verdict"] == "routed-gaps":
|
|
4856
|
-
out["reason"] = ("the spec, the model and every gated plan are byte-identical to what "
|
|
4857
|
-
"analyze-plan checked on %s; it left %d gap(s) only /appweaver specify "
|
|
4858
|
-
"can close: %s" % (out["gated"], len(out["routed_gaps"]), out["routed_csv"]))
|
|
4859
|
-
else:
|
|
4860
|
-
out["reason"] = "the spec, the model and every gated plan are byte-identical to what analyze-plan passed on %s" % out["gated"]
|
|
4861
|
-
print(json.dumps(out))
|
|
4862
|
-
PY
|
|
4833
|
+
local core="$SCRIPT_DIR/../python/aw-plangate.py"
|
|
4834
|
+
if [ ! -f "$core" ]; then echo "plan-gate-check: core not found at $core" >&2; return 1; fi
|
|
4835
|
+
if ! command -v python3 >/dev/null 2>&1; then echo "plan-gate-check: python3 is required" >&2; return 1; fi
|
|
4836
|
+
python3 "$core" check --feature-dir "$fd" --context-dir "$ctx_dir" --root "$repo_root"
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4839
|
+
# ─────────────────────────────── gap-route ─────────────────────────────────
|
|
4840
|
+
# Split a gate's findings by the command that CLOSES each one. The checks already tag
|
|
4841
|
+
# every finding with `fix`; this turns that tag into the routing a gate message prints,
|
|
4842
|
+
# so a plan defect is never sent to `implement` — which generates FROM the plan and would
|
|
4843
|
+
# reproduce it. Thin wrapper: the logic is in aw-gaps.py, called identically by both shells.
|
|
4844
|
+
cmd_gap_route() {
|
|
4845
|
+
case "${1:-}" in
|
|
4846
|
+
--help|-h) echo "gap-route --gaps '<json>'... [--cr <id>] -> {total, high_count, <route>_count, <route>_refs, <route>_text, summary, routes}"; return 0 ;;
|
|
4847
|
+
esac
|
|
4848
|
+
local core="$SCRIPT_DIR/../python/aw-gaps.py"
|
|
4849
|
+
if [ ! -f "$core" ]; then echo "gap-route: core not found at $core" >&2; return 1; fi
|
|
4850
|
+
if ! command -v python3 >/dev/null 2>&1; then echo "gap-route: python3 is required" >&2; return 1; fi
|
|
4851
|
+
python3 "$core" route "$@"
|
|
4863
4852
|
}
|
|
4864
4853
|
|
|
4865
4854
|
# ──────────────────────────────── plan-coverage ─────────────────────────────
|
|
@@ -4870,7 +4859,7 @@ PY
|
|
|
4870
4859
|
# (python/aw-plan-coverage.py), called identically by both shells.
|
|
4871
4860
|
cmd_plan_coverage() {
|
|
4872
4861
|
case "${1:-}" in
|
|
4873
|
-
--help|-h) echo "plan-coverage --plan <path> --kind backend|frontend [--mode bootstrap|delta] [--source-path <p>] [--test-path <p>] [--reported <json list>]... [--list-only] -> {ok, total, names[], names_csv, elements[], gaps[], gap_names[], gap_count, gaps_text, message}"; return 0 ;;
|
|
4862
|
+
--help|-h) echo "plan-coverage --plan <path> --kind backend|frontend [--mode bootstrap|delta] [--source-path <p>] [--test-path <p>] [--reported <json list>]... [--generated <json list>] [--list-only] -> {ok, outcome, total, names[], names_csv, elements[], gaps[], gap_names[], gap_count, gaps_text, message}"; return 0 ;;
|
|
4874
4863
|
esac
|
|
4875
4864
|
local repo_root; repo_root=$(get_repo_root)
|
|
4876
4865
|
local core="$SCRIPT_DIR/../python/aw-plan-coverage.py"
|
|
@@ -5039,6 +5028,7 @@ case "$sub" in
|
|
|
5039
5028
|
plan-gate-stamp) cmd_plan_gate_stamp "$@" ;;
|
|
5040
5029
|
plan-gate-check) cmd_plan_gate_check "$@" ;;
|
|
5041
5030
|
plan-coverage) cmd_plan_coverage "$@" ;;
|
|
5031
|
+
gap-route) cmd_gap_route "$@" ;;
|
|
5042
5032
|
checklist-read) cmd_checklist_read "$@" ;;
|
|
5043
5033
|
checklist-apply) cmd_checklist_apply "$@" ;;
|
|
5044
5034
|
detect-project) cmd_detect_project "$@" ;;
|
|
@@ -5054,6 +5044,7 @@ case "$sub" in
|
|
|
5054
5044
|
setup-constitution) cmd_setup_constitution "$@" ;;
|
|
5055
5045
|
compose-constitution) cmd_compose_constitution "$@" ;;
|
|
5056
5046
|
constitution-verify) cmd_constitution_verify "$@" ;;
|
|
5047
|
+
principles-list) cmd_principles_list "$@" ;;
|
|
5057
5048
|
agent-context-update) cmd_agent_context_update "$@" ;;
|
|
5058
5049
|
""|--help|-h)
|
|
5059
5050
|
sed -n '2,37p' "$0" | sed 's/^#\{0,1\} \{0,1\}//' ;;
|