yadflow 3.19.0-next.2 → 4.0.0-next.3
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/CHANGELOG.md +371 -0
- package/README.md +160 -22
- package/bin/commands.mjs +46 -0
- package/bin/yad.mjs +506 -117
- package/cli/artifact-status.mjs +30 -11
- package/cli/capture.mjs +417 -0
- package/cli/checkpoint.mjs +47 -27
- package/cli/claims.mjs +165 -0
- package/cli/codeowners-command.mjs +170 -0
- package/cli/codeowners.mjs +397 -0
- package/cli/commit.mjs +13 -9
- package/cli/dial.mjs +183 -0
- package/cli/docs.mjs +88 -32
- package/cli/doctor.mjs +1682 -90
- package/cli/epic-state.mjs +3460 -211
- package/cli/epic.mjs +506 -0
- package/cli/errors.mjs +3 -0
- package/cli/fold.mjs +247 -0
- package/cli/gate-local.mjs +296 -0
- package/cli/gate.mjs +977 -183
- package/cli/history.mjs +556 -0
- package/cli/hook.mjs +264 -53
- package/cli/hubcommit.mjs +9 -19
- package/cli/index-command.mjs +87 -0
- package/cli/ledger.mjs +54 -4
- package/cli/lib.mjs +161 -9
- package/cli/manifest.mjs +376 -38
- package/cli/migrate.mjs +683 -52
- package/cli/mode.mjs +170 -0
- package/cli/next.mjs +365 -86
- package/cli/openpr.mjs +193 -34
- package/cli/owners.mjs +266 -0
- package/cli/people.mjs +657 -0
- package/cli/plan.mjs +466 -125
- package/cli/platform.mjs +109 -128
- package/cli/product-index.mjs +287 -0
- package/cli/protection.mjs +706 -0
- package/cli/reconcile.mjs +40 -12
- package/cli/repo-publish.mjs +24 -26
- package/cli/repo.mjs +23 -14
- package/cli/report.mjs +24 -14
- package/cli/review.mjs +19 -22
- package/cli/riskmap-command.mjs +289 -0
- package/cli/riskmap.mjs +373 -0
- package/cli/setup.mjs +127 -279
- package/cli/ship.mjs +6 -5
- package/cli/skill.mjs +180 -0
- package/cli/skip.mjs +211 -30
- package/cli/thread.mjs +41 -16
- package/cli/tidy.mjs +18 -18
- package/cli/update-commit.mjs +21 -21
- package/cli/usage.mjs +115 -109
- package/package.json +3 -3
- package/skills/sdlc/config.yaml +149 -70
- package/skills/sdlc/module-help.csv +24 -24
- package/skills/yad-analysis/SKILL.md +121 -59
- package/skills/yad-architecture/SKILL.md +30 -18
- package/skills/yad-architecture/references/contract-format.md +8 -5
- package/skills/yad-backfill/SKILL.md +10 -4
- package/skills/yad-backfill/references/backfill.md +8 -1
- package/skills/yad-backfill/templates/checks/backfill-check.sh +10 -2
- package/skills/yad-change/SKILL.md +119 -42
- package/skills/yad-change/references/triage.md +42 -27
- package/skills/yad-checks/SKILL.md +68 -35
- package/skills/yad-checks/references/check-gates.md +310 -85
- package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
- package/skills/yad-checks/templates/checks/contract-check.sh +130 -9
- package/skills/yad-checks/templates/checks/epic-open.sh +2 -2
- package/skills/yad-checks/templates/checks/ledger-guard.sh +55 -8
- package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +1 -1
- package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
- package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
- package/skills/yad-checks/templates/github/yad-checks.yml +13 -2
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
- package/skills/yad-checks/templates/github/yad-update-guard.yml +7 -5
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +10 -1
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +3 -3
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +37 -6
- package/skills/yad-checks/templates/hooks/yad-capture.sh +53 -0
- package/skills/yad-commit/SKILL.md +4 -4
- package/skills/yad-connect-design/SKILL.md +5 -5
- package/skills/yad-connect-design/references/design-context.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +2 -2
- package/skills/yad-connect-docs/SKILL.md +11 -11
- package/skills/yad-connect-docs/references/docs-registry.md +1 -1
- package/skills/yad-connect-learning/SKILL.md +5 -5
- package/skills/yad-connect-learning/references/learning-registry.md +2 -2
- package/skills/yad-connect-repos/SKILL.md +66 -45
- package/skills/yad-connect-repos/references/code-context.md +4 -4
- package/skills/yad-connect-repos/references/hub-config.md +47 -51
- package/skills/yad-connect-repos/references/repos-registry.md +8 -7
- package/skills/yad-connect-repos/references/risk-map.md +81 -0
- package/skills/yad-connect-testing/SKILL.md +5 -5
- package/skills/yad-connect-testing/references/testing-context.md +3 -4
- package/skills/yad-connect-testing/references/testing-registry.md +2 -2
- package/skills/yad-defects/SKILL.md +7 -7
- package/skills/yad-discovery/SKILL.md +131 -94
- package/skills/yad-discovery/references/discovery-schema.md +22 -6
- package/skills/yad-discovery/references/foundation-schema.md +374 -0
- package/skills/yad-docs/SKILL.md +13 -8
- package/skills/yad-docs/references/data-mapping.md +9 -7
- package/skills/yad-docs/templates/app/package-lock.json +3 -3
- package/skills/yad-docs-overview/SKILL.md +30 -15
- package/skills/yad-docs-overview/references/pipeline-model.md +33 -20
- package/skills/yad-docs-sync/SKILL.md +10 -5
- package/skills/yad-docs-sync/references/staleness.md +8 -7
- package/skills/yad-engineer-review/SKILL.md +81 -17
- package/skills/yad-engineer-review/references/ship-and-record.md +17 -8
- package/skills/yad-epic/SKILL.md +173 -92
- package/skills/yad-epic/references/state-schema.md +610 -94
- package/skills/yad-hub-bridge/SKILL.md +57 -38
- package/skills/yad-hub-bridge/references/bridge.md +94 -67
- package/skills/yad-hub-bridge/references/login-roster.md +163 -70
- package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
- package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
- package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +21 -11
- package/skills/yad-implement/SKILL.md +27 -13
- package/skills/yad-implement/references/implement-conventions.md +2 -2
- package/skills/yad-learn/SKILL.md +9 -9
- package/skills/yad-learn/references/learning-state.md +2 -2
- package/skills/yad-open-pr/SKILL.md +61 -26
- package/skills/yad-pair-review/SKILL.md +7 -5
- package/skills/yad-pair-review/references/session-state.md +1 -1
- package/skills/yad-pr-template/SKILL.md +47 -26
- package/skills/yad-pr-template/references/risk-routing.md +97 -24
- package/skills/yad-pr-template/templates/checks/pr-template.sh +34 -11
- package/skills/yad-pr-template/templates/checks/pr-title.sh +32 -9
- package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
- package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
- package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
- package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +14 -13
- package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +14 -12
- package/skills/yad-reconcile/SKILL.md +3 -3
- package/skills/yad-report/SKILL.md +5 -5
- package/skills/yad-review-companion/SKILL.md +6 -3
- package/skills/yad-review-gate/SKILL.md +203 -93
- package/skills/yad-review-gate/references/gating.md +232 -54
- package/skills/yad-run/SKILL.md +81 -51
- package/skills/yad-run/references/run-loop.md +61 -39
- package/skills/yad-ship/SKILL.md +15 -11
- package/skills/yad-spec/SKILL.md +21 -6
- package/skills/yad-spec/references/spec-handoff.md +17 -5
- package/skills/yad-status/SKILL.md +115 -50
- package/skills/yad-stories/SKILL.md +37 -21
- package/skills/yad-stories/references/story-schema.md +7 -6
- package/skills/yad-stub/SKILL.md +58 -47
- package/skills/yad-sync-repos/SKILL.md +2 -2
- package/skills/yad-test-cases/SKILL.md +32 -23
- package/skills/yad-test-cases/references/test-cases-schema.md +7 -4
- package/skills/yad-timeline/SKILL.md +8 -7
- package/skills/yad-ui/SKILL.md +41 -18
- package/cli/roster.mjs +0 -164
- package/skills/sdlc/install.sh +0 -68
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
# - any trailers appear in the fixed order Task -> Contract-Change -> Co-Authored-By.
|
|
8
8
|
# Keep the type list in sync with cli/manifest.mjs COMMIT_TYPES and config.yaml build.commit_subject_style.
|
|
9
9
|
#
|
|
10
|
-
# Profiles (--profile code|hub, default code): the subject rule is identical on the
|
|
10
|
+
# Profiles (--profile code|hub|product, default code): the subject rule is identical on the Product and on
|
|
11
11
|
# code repos (both follow CONTRIBUTING). The Task trailer is NOT required here (the spec-link gate owns
|
|
12
|
-
# that on code repos;
|
|
12
|
+
# that on code repos; Product commits are not task-scoped) — this gate only checks SHAPE and ORDER.
|
|
13
13
|
#
|
|
14
14
|
# Merge/squash commits (2+ parents) are skipped: their platform-generated subjects are not authored.
|
|
15
15
|
set -euo pipefail
|
|
@@ -23,7 +23,21 @@ while [ $# -gt 0 ]; do
|
|
|
23
23
|
*) ARGS+=("$1"); shift ;;
|
|
24
24
|
esac
|
|
25
25
|
done
|
|
26
|
-
case "$PROFILE" in code|hub) ;; *) echo "FAIL [commit-message]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
|
|
26
|
+
case "$PROFILE" in code|hub|product) ;; *) echo "FAIL [commit-message]: unknown --profile '$PROFILE' (code|hub|product)."; exit 1 ;; esac
|
|
27
|
+
# `product` is the new name for the `hub` profile and BOTH are accepted.
|
|
28
|
+
#
|
|
29
|
+
# Not because the two sides update separately — they do not: this script and the workflow that passes
|
|
30
|
+
# the flag are both in PRODUCT_WIRING (cli/manifest.mjs) and land together on one `yad update`. It is
|
|
31
|
+
# the plain add-before-remove ladder instead: accept the new spelling now, switch the workflow
|
|
32
|
+
# templates to emit it in a later release, drop the old one after that. Every shipped template still
|
|
33
|
+
# passes `--profile hub` today, so this arm is dead weight until that switch — which is the point.
|
|
34
|
+
#
|
|
35
|
+
# Normalised to `hub` immediately, so nothing below has to know there are two spellings. That is
|
|
36
|
+
# load-bearing in pr-title.sh and pr-template.sh: leave `$PROFILE` as `product` and the `= hub`
|
|
37
|
+
# branch is skipped, taking the whole review-branch arm with it — including the guard that stops a
|
|
38
|
+
# plain code title carrying an artifact change past its review.
|
|
39
|
+
[ "$PROFILE" = product ] && PROFILE=hub
|
|
40
|
+
|
|
27
41
|
|
|
28
42
|
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
29
43
|
# --- is duplicated, not sourced) ---
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
# (link.md's pinned hash must match the product lock). Otherwise FAIL and route back to the
|
|
8
8
|
# architecture gate. Normal implementation that only CONSUMES the contract passes untouched.
|
|
9
9
|
set -euo pipefail
|
|
10
|
+
# Bytes, not characters (E114). The changed list below carries raw path bytes, and a path that is not
|
|
11
|
+
# valid UTF-8 read under a UTF-8 locale is one GNU grep and sed may skip or leave unmatched — one such
|
|
12
|
+
# file in a diff would hide every other path from the patterns (Linux CI). On macOS, `tr` and `sed` stop
|
|
13
|
+
# on it with "Illegal byte sequence" instead, so the gate fails with a message that names nothing.
|
|
14
|
+
export LC_ALL=C
|
|
10
15
|
|
|
11
16
|
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
12
17
|
# --- is duplicated, not sourced) ---
|
|
@@ -41,14 +46,89 @@ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
|
41
46
|
fi
|
|
42
47
|
RANGE="${BASE}..HEAD"
|
|
43
48
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
# No symlink and no submodule anywhere under specs/ (E115). The gate reads PATHS: a link lets the
|
|
50
|
+
# content live somewhere the paths below never name. A symlink at `specs`, `specs/<story>` or
|
|
51
|
+
# `specs/<story>/contracts` hid the slice from every rule here, and so did every later edit to the
|
|
52
|
+
# link's target; a symlinked slice file was seen once, when added, and never again. A link.md behind a
|
|
53
|
+
# link redirects the lock pin the fidelity check reads. yad-spec writes real files, so nothing
|
|
54
|
+
# legitimate lives there as a link.
|
|
55
|
+
#
|
|
56
|
+
# Read from the TREE at HEAD, not from the diff, and before the "no surface" PASS below: a link merged
|
|
57
|
+
# before this check existed fails every PR until one PR removes it — no diff would ever show it again.
|
|
58
|
+
# 120000 is a symlink, 160000 a submodule; `-r` lists `specs` itself when `specs` is the link.
|
|
59
|
+
#
|
|
60
|
+
# The WHOLE tree, from the repo root (--full-tree), matched without case: on macOS and Windows `Specs/`
|
|
61
|
+
# IS `specs/`, and git's path filter matches exact bytes only (`:(icase)` is refused by ls-tree). So a
|
|
62
|
+
# link under `Specs/` got past a `-- specs` read, and a plain file at `Specs/<story>/contracts/…` was
|
|
63
|
+
# never on the surface below, which is spelled in lowercase. A top folder spelled any other way than
|
|
64
|
+
# `specs` is refused too, once, by its own name. The name printed is everything after the first tab.
|
|
65
|
+
# `tolower` under LC_ALL=C folds ASCII only, so the one other letter the file systems fold into "specs"
|
|
66
|
+
# is mapped by hand: `ſ` (long s, U+017F, bytes 305 277) — APFS reads `ſpecs/` as `specs/`. That is
|
|
67
|
+
# `fold` below.
|
|
68
|
+
#
|
|
69
|
+
# The same holds one and two folders down (review 3): `specs/<story>/Contracts/` IS `contracts/` on a
|
|
70
|
+
# Mac, and `specs/EP-x-S01/` and `specs/ep-x-s01/` are one folder there, while every rule below reads
|
|
71
|
+
# exact bytes. So a `contracts` spelled any other way is refused, and so is a second spelling of a
|
|
72
|
+
# story folder — each once, by name. So is a FILE named `specs`, where the folder has to go. `fold`
|
|
73
|
+
# knows ASCII case and the long s only: `EP-démo` beside `EP-DÉMO` (or an NFC/NFD twin) is not caught.
|
|
74
|
+
# That costs a skipped lock check at most — the surface grep takes any story spelling — and refusing
|
|
75
|
+
# every non-ASCII story name would refuse real repos.
|
|
76
|
+
links="$(git ls-tree -r -z --full-tree HEAD | tr '\0' '\n' | awk '
|
|
77
|
+
function fold(x) { x = tolower(x); gsub(/\305\277/, "s", x); return x }
|
|
78
|
+
function once(k, msg) { if (!(k in said)) { said[k] = 1; print " " msg } }
|
|
79
|
+
{ t = index($0, "\t"); p = (t ? substr($0, t + 1) : $0); m = (t ? substr($0, 1, t - 1) : ""); lp = fold(p) }
|
|
80
|
+
lp !~ /^specs(\/|$)/ { next }
|
|
81
|
+
m ~ /^120000 / { print " " p " (symlink)"; next }
|
|
82
|
+
m ~ /^160000 / { print " " p " (submodule)"; next }
|
|
83
|
+
p !~ /^specs(\/|$)/ { top = p; sub(/\/.*/, "", top); once(top, (top == p ? top " (a file" : top "/ (a folder") " spelled other than specs — the same name on macOS and Windows)"); next }
|
|
84
|
+
p == "specs" { print " specs (a file, where the specs/ folder goes)"; next }
|
|
85
|
+
{ n = split(p, c, "/"); fs = fold(c[2]) }
|
|
86
|
+
(fs in story) && story[fs] != c[2] { once("s/" c[2], "specs/" story[fs] (n == 2 ? " and specs/" c[2] " (one file" : "/ and specs/" c[2] "/ (one folder") " on macOS and Windows)") }
|
|
87
|
+
!(fs in story) { story[fs] = c[2] }
|
|
88
|
+
n >= 3 && c[3] != "contracts" && fold(c[3]) == "contracts" { once("c/" c[2] "/" c[3], "specs/" c[2] "/" c[3] (n == 3 ? " (a file" : "/ (a folder") " spelled other than contracts — the same name on macOS and Windows)") }
|
|
89
|
+
')" || { echo "FAIL [contract-check]: could not read the tree at HEAD — the links under specs/ cannot be checked."; exit 1; }
|
|
90
|
+
if [ -n "$links" ]; then
|
|
91
|
+
echo "FAIL [contract-check]: specs/ holds a symlink, a submodule or a second spelling — this gate cannot see what it points at:"
|
|
92
|
+
printf '%s\n' "$links"
|
|
93
|
+
echo " -> replace each link with the real files (yad-spec writes real files), and keep ONE spelling"
|
|
94
|
+
echo " of each folder: specs/, specs/<story>/, specs/<story>/contracts/. Until then every PR in this"
|
|
95
|
+
echo " repo fails here, because the contract surface cannot be checked. Removing a link or merging"
|
|
96
|
+
echo " a spelling is itself a change to specs/: under contracts/ it needs 'Contract-Change: yes'."
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# How the changed list is built, and why each flag is there (E114):
|
|
101
|
+
# --no-renames a rename is listed by BOTH paths, as a delete and an add. Without it git names a
|
|
102
|
+
# rename by its NEW path only, so `git mv specs/S1/contracts/api.md docs/api.md`
|
|
103
|
+
# took the slice off the surface and the gate never saw it — no trailer needed.
|
|
104
|
+
# -z | tr NUL-separated, so git never quotes a path. By default git wraps a path holding a
|
|
105
|
+
# non-ASCII byte in quotes and octal-escapes it, and it still quotes one holding a `"`
|
|
106
|
+
# or a tab with core.quotePath off — either way a slice like
|
|
107
|
+
# specs/EP-démo-S01/contracts/api.md never matched the pattern below.
|
|
108
|
+
# Both lists (this one and `deleted` below) are built the SAME way: the no-lock escape hatch compares
|
|
109
|
+
# them line for line.
|
|
110
|
+
changed="$(git diff --no-renames --name-only -z "$RANGE" | tr '\0' '\n')"
|
|
111
|
+
surface="$(printf '%s\n' "$changed" | grep -E '^specs/[^/]+/contracts(/|$)' || true)"
|
|
112
|
+
|
|
113
|
+
# Slice paths this diff DELETES. `--name-only` above lists a deleted file exactly like a changed one,
|
|
114
|
+
# which is right for the trailer rule — removing an agreed endpoint IS a surface change — but it
|
|
115
|
+
# creates a dead end for the one epic that should never have had a slice in the first place.
|
|
116
|
+
#
|
|
117
|
+
# A short-lane epic has no lock and never will (E40). Once a `specs/<story>/contracts/` file exists
|
|
118
|
+
# under one, the unflagged rule below fails the commit, the no-lock rule further down fails the
|
|
119
|
+
# flagged one, and the cleanup commit that DELETES the file fails too — so there is no diff that
|
|
120
|
+
# passes, including the correct one. An escape hatch is not optional there.
|
|
121
|
+
#
|
|
122
|
+
# Deleting your own copy of a surface that nothing upstream has locked cannot widen it: with no lock
|
|
123
|
+
# there is no agreed shape for the deletion to contradict. So a delete-only diff is allowed through
|
|
124
|
+
# the no-lock branch (and only that branch — where a lock EXISTS, a deletion is still a real surface
|
|
125
|
+
# change and every rule below applies to it unchanged).
|
|
126
|
+
# --no-renames matters here too: a slice MOVED out of contracts/ is a delete of the old path, and
|
|
127
|
+
# without it git reports an `R`, which --diff-filter=D never matches — the hatch would refuse the move.
|
|
128
|
+
deleted="$(git diff --no-renames --diff-filter=D --name-only -z "$RANGE" | tr '\0' '\n' | grep -E '^specs/[^/]+/contracts(/|$)' || true)"
|
|
49
129
|
|
|
50
130
|
if [ -z "$surface" ]; then
|
|
51
|
-
echo "PASS [contract-check]: diff does not touch the contract surface (specs/*/contracts
|
|
131
|
+
echo "PASS [contract-check]: diff does not touch the contract surface (specs/*/contracts and everything under it)."
|
|
52
132
|
exit 0
|
|
53
133
|
fi
|
|
54
134
|
|
|
@@ -59,6 +139,8 @@ cc="$(git log "$RANGE" --format='%(trailers:key=Contract-Change,valueonly)' | se
|
|
|
59
139
|
if ! printf '%s\n' "$cc" | grep -qx 'yes'; then
|
|
60
140
|
echo "FAIL [contract-check]: contract surface changed without a 'Contract-Change: yes' trailer."
|
|
61
141
|
echo " -> Route back to the architecture gate: update + re-lock contract.md in the product repo,"
|
|
142
|
+
echo " or, if this epic is on a short lane (chore/spike) and has no architecture gate, move the"
|
|
143
|
+
echo " surface change to a new epic on the classic route,"
|
|
62
144
|
echo " re-run yad-spec, then implement with Contract-Change: yes. The surface is never widened"
|
|
63
145
|
echo " from inside a code repo."
|
|
64
146
|
exit 1
|
|
@@ -74,7 +156,7 @@ fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f &&
|
|
|
74
156
|
# Same, for a link.md field. yad-spec writes link.md WITH frontmatter, but code repos still carry
|
|
75
157
|
# pre-frontmatter ones that contract-check used to read with a whole-file scan — so fall back to that
|
|
76
158
|
# rather than silently reading an empty value and skipping the check it guards. Deliberately separate
|
|
77
|
-
# from fm_val:
|
|
159
|
+
# from fm_val: Product artifacts (epic.md, stories/*.md) stay bounded to their first block.
|
|
78
160
|
link_val() {
|
|
79
161
|
_v="$(fm_val "$1" "$2")"
|
|
80
162
|
[ -n "$_v" ] || _v="$(sed -nE "s/^$1:[[:space:]]*(.*)\$/\1/p" "$2" 2>/dev/null | head -1 | tr -d '\r' | sed -E 's/[[:space:]]+$//')"
|
|
@@ -105,7 +187,7 @@ resolve_product() {
|
|
|
105
187
|
# link.md deferred the whole check before the stale one was ever read (issue #161). Failures are
|
|
106
188
|
# AGGREGATED — every story reports, so one clean-or-deferred story never masks another's stale pin
|
|
107
189
|
# (the same rule spec-link applies per commit).
|
|
108
|
-
stories="$(printf '%s\n' "$surface" | sed -E 's#^specs/([^/]+)/contracts
|
|
190
|
+
stories="$(printf '%s\n' "$surface" | sed -E 's#^specs/([^/]+)/contracts(/.*)?$#\1#' | sort -u)"
|
|
109
191
|
rc=0
|
|
110
192
|
while IFS= read -r story; do
|
|
111
193
|
[ -z "$story" ] && continue
|
|
@@ -146,6 +228,45 @@ while IFS= read -r story; do
|
|
|
146
228
|
continue
|
|
147
229
|
fi
|
|
148
230
|
echo "note [contract-check]: ${link} hash matches the product lock (${current:0:12}…)."
|
|
231
|
+
elif [ -n "$prod" ] && [ -d "${prod}/epics/${epic}/.sdlc" ]; then
|
|
232
|
+
# THE PRODUCT REPO RESOLVED AND THERE IS STILL NO LOCK. That is a different fact from "not
|
|
233
|
+
# reachable", and it must not share its answer. A `Contract-Change: yes` is being claimed against
|
|
234
|
+
# an epic that has no locked surface at all, so there is nothing the claim could be true of.
|
|
235
|
+
#
|
|
236
|
+
# This is the steady state of the short lanes (E40): `chore` and `spike` carry no architecture
|
|
237
|
+
# step, so `contract.md` and `contract-lock.json` never exist on them. Deferring here meant every
|
|
238
|
+
# short-lane epic could move the shared cross-repo surface forever, with a note that reads like a
|
|
239
|
+
# pass. The guard used to be prose in the authoring skills; this is the gate.
|
|
240
|
+
#
|
|
241
|
+
# Narrow on purpose, and the guard is the EPIC'S LEDGER DIRECTORY, not the product path — because
|
|
242
|
+
# `resolve_product` ALWAYS returns some path and never verifies that the one it picked exists. (Its
|
|
243
|
+
# own two `-d` tests only choose BETWEEN the story-relative and repo-root joins; neither rejects a
|
|
244
|
+
# miss.) Requiring `epics/<epic>/.sdlc/` to exist is what separates "this epic is real and has no
|
|
245
|
+
# lock" from "nothing is checked out here". A CI job that does not check the
|
|
246
|
+
# product repo out still defers below, exactly as before — that case proves nothing either way,
|
|
247
|
+
# and failing it would break every setup that has always run this way.
|
|
248
|
+
# The escape hatch described at the top: with no lock upstream there is no agreed shape for a
|
|
249
|
+
# deletion to contradict, so REMOVING a slice is always allowed here. This is what keeps a
|
|
250
|
+
# short-lane story that should never have had a `contracts/` folder from being unfixable.
|
|
251
|
+
story_surface="$(printf '%s\n' "$surface" | grep -E "^specs/${story}/contracts(/|$)" || true)"
|
|
252
|
+
story_deleted="$(printf '%s\n' "$deleted" | grep -E "^specs/${story}/contracts(/|$)" || true)"
|
|
253
|
+
if [ -n "$story_surface" ] && [ "$story_surface" = "$story_deleted" ]; then
|
|
254
|
+
echo "note [contract-check]: ${story} only REMOVES contract slice files and ${epic} has no lock —"
|
|
255
|
+
echo " nothing upstream is being contradicted, so the removal is allowed."
|
|
256
|
+
continue
|
|
257
|
+
fi
|
|
258
|
+
echo "FAIL [contract-check]: Contract-Change claimed, but ${epic} has no contract lock at all."
|
|
259
|
+
echo " Expected ${lock} — the epic's ledger directory is there and the lock file is not."
|
|
260
|
+
echo " Three ways to be here, with three different fixes:"
|
|
261
|
+
echo " - a SHORT LANE (chore/spike): it has no architecture step and never locks a surface, so it"
|
|
262
|
+
echo " carries no specs/<story>/contracts/ at all. DELETE the slice; a delete-only diff passes."
|
|
263
|
+
echo " A real surface change belongs to a new epic on the classic route."
|
|
264
|
+
echo " - a STUB / lightly-promoted brownfield epic: the feature was never documented, so there is"
|
|
265
|
+
echo " nothing to lock yet. Run yad-backfill and promote it fully (which locks contract.md)."
|
|
266
|
+
echo " - a CLASSIC epic that has not reached its architecture gate: author and lock contract.md"
|
|
267
|
+
echo " first (yad-architecture Step 5)."
|
|
268
|
+
rc=1
|
|
269
|
+
continue
|
|
149
270
|
else
|
|
150
271
|
# Say so. A skipped fidelity check used to be indistinguishable from a passed one, which is how a
|
|
151
272
|
# mis-resolved product-repo could turn a stale-pin FAIL into a silent PASS (issue #149).
|
|
@@ -156,7 +277,7 @@ $stories
|
|
|
156
277
|
EOF
|
|
157
278
|
|
|
158
279
|
if [ "$rc" != 0 ]; then
|
|
159
|
-
echo "FAIL [contract-check]: a changed slice pins a stale contract lock (see above) — the surface was not re-locked upstream for every story in this diff."
|
|
280
|
+
echo "FAIL [contract-check]: a changed slice pins a stale or absent contract lock (see above) — the surface was not re-locked upstream for every story in this diff."
|
|
160
281
|
exit 1
|
|
161
282
|
fi
|
|
162
283
|
|
|
@@ -53,7 +53,7 @@ fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f &&
|
|
|
53
53
|
# Same, for a link.md field. yad-spec writes link.md WITH frontmatter, but code repos still carry
|
|
54
54
|
# pre-frontmatter ones that contract-check used to read with a whole-file scan — so fall back to that
|
|
55
55
|
# rather than silently reading an empty value and skipping the check it guards. Deliberately separate
|
|
56
|
-
# from fm_val:
|
|
56
|
+
# from fm_val: Product artifacts (epic.md, stories/*.md) stay bounded to their first block.
|
|
57
57
|
link_val() {
|
|
58
58
|
_v="$(fm_val "$1" "$2")"
|
|
59
59
|
[ -n "$_v" ] || _v="$(sed -nE "s/^$1:[[:space:]]*(.*)\$/\1/p" "$2" 2>/dev/null | head -1 | tr -d '\r' | sed -E 's/[[:space:]]+$//')"
|
|
@@ -142,7 +142,7 @@ while IFS= read -r sha; do
|
|
|
142
142
|
if epic_sealed "$ep_dir"; then
|
|
143
143
|
echo "FAIL [epic-open]: ${short} ${task} targets SEALED epic ${epic} (all stories shipped)."
|
|
144
144
|
echo " -> New behaviour cannot mutate a shipped epic. Open a threaded change-epic with yad-change"
|
|
145
|
-
echo " (
|
|
145
|
+
echo " (type change|defect|hotfix, parent: ${epic}) and implement against ITS stories instead."
|
|
146
146
|
rc=1
|
|
147
147
|
continue
|
|
148
148
|
fi
|
|
@@ -6,8 +6,21 @@
|
|
|
6
6
|
# the ledger" a mechanical guarantee instead of a convention.
|
|
7
7
|
#
|
|
8
8
|
# Protected (gate-state, machine-written):
|
|
9
|
-
# epics/*/.sdlc/state.json, approvals.json, comments.json, hub-prs.json
|
|
9
|
+
# epics/*/.sdlc/state.json, approvals.json, comments.json, product-prs.json, hub-prs.json
|
|
10
|
+
# foundation/.sdlc/… and foundation/reviews/*.md — the same files for the Product level (E75), whose
|
|
11
|
+
# ledger lives in its own top-level folder. A copy of this script from before that release does NOT
|
|
12
|
+
# guard them; `yad doctor` says so on a verified Product that has a Foundation, and `yad update`
|
|
13
|
+
# refreshes this file.
|
|
14
|
+
#
|
|
15
|
+
# BOTH names of the PR ledger are guarded. It was renamed `hub-prs.json` -> `product-prs.json`, and
|
|
16
|
+
# the engine writes both for one major so that a copy of THIS script which predates the rename still
|
|
17
|
+
# finds the file it knows. Guarding only one name would leave the other open to a hand-edit on a
|
|
18
|
+
# verified product — the exact thing this gate exists to refuse.
|
|
10
19
|
# epics/*/reviews/*.md
|
|
20
|
+
# .sdlc/index.json — the Product index (E19): derived from the ledgers above, and rebuilt by CI in the
|
|
21
|
+
# same commit that records a merge. Exactly that one path: the files beside it (product.json, hub.json,
|
|
22
|
+
# repos.json…) are a person's to edit, and a wider arm would refuse `yad mode` and `yad setup` commits.
|
|
23
|
+
# It is never a seed, so no carve-out applies to it.
|
|
11
24
|
# NOT protected:
|
|
12
25
|
# epics/*/.sdlc/contract-lock.json — artifact-side: the architect locks the contract surface in
|
|
13
26
|
# `gate open`, so a human legitimately commits it alongside the architecture artifact.
|
|
@@ -23,7 +36,7 @@
|
|
|
23
36
|
# Scope: enforced ONLY when the ledger is verified — hub.json carries BOTH a `platform` and either
|
|
24
37
|
# `ledger: "verified"` or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or the
|
|
25
38
|
# legacy `bridge`) true. That is the same predicate `isVerifiedLedger` (cli/manifest.mjs) and
|
|
26
|
-
# `
|
|
39
|
+
# `productActions` (cli/plan.mjs) apply. With a local ledger — or a platform-less Product — humans
|
|
27
40
|
# legitimately write the ledger themselves, so the gate is a no-op.
|
|
28
41
|
#
|
|
29
42
|
# Degradation: a base ref that cannot be resolved FAILs closed; no platform (cannot read the Verified
|
|
@@ -32,8 +45,8 @@ set -euo pipefail
|
|
|
32
45
|
|
|
33
46
|
# ---- bridge gate: only CI-owned ledgers are guarded -------------------------------------------
|
|
34
47
|
# The predicate is BOTH a platform and the verified ledger flag, exactly as `isVerifiedLedger` (`cli/manifest.mjs`) and
|
|
35
|
-
# `
|
|
36
|
-
# other ledger reader (issue #186): a
|
|
48
|
+
# `productActions` (cli/plan.mjs) define it. Requiring the flag alone put this gate out of step with every
|
|
49
|
+
# other ledger reader (issue #186): a Product carrying `bridge_enabled: true` with no `platform` would
|
|
37
50
|
# have its human ledger commits rejected here while the CLI, reading the same file, called it
|
|
38
51
|
# local and kept the LOCAL write path — no CI writer and no permitted human writer, so no gate
|
|
39
52
|
# could advance. Reachable through a stale install (platform set, script wired, platform later
|
|
@@ -45,7 +58,7 @@ set -euo pipefail
|
|
|
45
58
|
#
|
|
46
59
|
# Matched at the ROOT LEVEL only. The shared `default_branch` read below is depth-blind, and that is
|
|
47
60
|
# survivable there — a false match yields a bogus branch name and the gate fails loudly. Here it is
|
|
48
|
-
# not: a nested `"bridge": true` (say under `review`) would silently ENABLE this gate on a
|
|
61
|
+
# not: a nested `"bridge": true` (say under `review`) would silently ENABLE this gate on a Product whose
|
|
49
62
|
# `isVerifiedLedger` is false, recreating the exact no-writer deadlock #186 is about, from the other side. So
|
|
50
63
|
# the nesting is stripped rather than ignored: peel the outermost braces, then delete innermost
|
|
51
64
|
# objects/arrays until none remain, leaving only root-level pairs to match against. Not a JSON parser
|
|
@@ -229,15 +242,32 @@ is_seeding() { # $1 = epic slug; 0 when that epic has no le
|
|
|
229
242
|
_s="${_p#epics/}"; _s="${_s%%/*}"
|
|
230
243
|
base_slugs[${#base_slugs[@]}]="$(fold "$_s")"
|
|
231
244
|
;;
|
|
245
|
+
# The Product level (E75) has ONE ledger, in `foundation/`, under the fixed id EP-foundation.
|
|
246
|
+
foundation/.sdlc/state.json)
|
|
247
|
+
base_slugs[${#base_slugs[@]}]="ep-foundation"
|
|
248
|
+
;;
|
|
232
249
|
esac
|
|
233
|
-
done < <(git -c core.quotePath=false ls-tree -r --name-only -z "${BASE}" -- epics 2>/dev/null || true)
|
|
250
|
+
done < <(git -c core.quotePath=false ls-tree -r --name-only -z "${BASE}" -- epics foundation 2>/dev/null || true)
|
|
251
|
+
# A product has ONE product level. When its OLD spelling (epics/EP-discovery) is already on BASE, a
|
|
252
|
+
# Foundation ledger is not a new product level being created — it is a second one replacing a
|
|
253
|
+
# CI-owned ledger, and on a verified Product no CI path writes one. So it counts as seeded, and a
|
|
254
|
+
# human commit adding it is a mutation like any other.
|
|
255
|
+
if in_list "ep-discovery" "${base_slugs[@]}"; then
|
|
256
|
+
base_slugs[${#base_slugs[@]}]="ep-foundation"
|
|
257
|
+
fi
|
|
258
|
+
# …and the mirror: with a Foundation on BASE, a fresh epics/EP-discovery ledger is a second product
|
|
259
|
+
# level too, not a new epic.
|
|
260
|
+
if in_list "ep-foundation" "${base_slugs[@]}"; then
|
|
261
|
+
base_slugs[${#base_slugs[@]}]="ep-discovery"
|
|
262
|
+
fi
|
|
234
263
|
base_slugs_loaded=1
|
|
235
264
|
fi
|
|
236
265
|
_f="$(fold "$1")"
|
|
237
266
|
in_list "$_f" "${base_slugs[@]}" && return 1
|
|
238
267
|
in_list "$_f" "${noted_slugs[@]}" && return 0
|
|
239
268
|
noted_slugs[${#noted_slugs[@]}]="$_f"
|
|
240
|
-
|
|
269
|
+
if [ "$_f" = "ep-foundation" ]; then _where="foundation"; else _where="epics/$1"; fi
|
|
270
|
+
echo "note [ledger-guard]: ${_where} has no ledger on ${BASE} — new epic, its seed is exempt (creation, not mutation)."
|
|
241
271
|
return 0
|
|
242
272
|
}
|
|
243
273
|
|
|
@@ -250,13 +280,30 @@ for sha in $commits; do
|
|
|
250
280
|
while IFS= read -r -d '' f; do
|
|
251
281
|
[ -n "$f" ] || continue
|
|
252
282
|
case "$f" in
|
|
283
|
+
.sdlc/index.json) # the Product index (E19) — derived, CI-written, never a seed
|
|
284
|
+
touches_ledger=1
|
|
285
|
+
echo " ${sha} (author $(git show -s --format='%an' "$sha")) → $f"
|
|
286
|
+
;;
|
|
253
287
|
epics/*/.sdlc/contract-lock.json) ;; # artifact-side — allowed
|
|
254
|
-
epics/*/.sdlc/state.json|epics/*/.sdlc/approvals.json|epics/*/.sdlc/comments.json|epics/*/.sdlc/hub-prs.json|epics/*/reviews/*.md)
|
|
288
|
+
epics/*/.sdlc/state.json|epics/*/.sdlc/approvals.json|epics/*/.sdlc/comments.json|epics/*/.sdlc/product-prs.json|epics/*/.sdlc/hub-prs.json|epics/*/reviews/*.md)
|
|
255
289
|
_slug="${f#epics/}"; _slug="${_slug%%/*}"
|
|
256
290
|
is_seeding "$_slug" && continue # a new epic's seed — not a mutation of a CI-owned ledger
|
|
257
291
|
touches_ledger=1
|
|
258
292
|
echo " ${sha} (author $(git show -s --format='%an' "$sha")) → $f"
|
|
259
293
|
;;
|
|
294
|
+
# The Foundation's ledger (E75) — the same files, in the Product level's own folder, under the fixed
|
|
295
|
+
# id EP-foundation. Matched on the path BELOW `foundation/` with a leading `/` put back, so the
|
|
296
|
+
# same `*/.sdlc/<file>` test covers the ledger itself and a nested copy (the epic arms' `*` spans
|
|
297
|
+
# `/` too) without also catching a section file that merely ends in `.sdlc` or `reviews`.
|
|
298
|
+
foundation/*)
|
|
299
|
+
case "/${f#foundation/}" in
|
|
300
|
+
*/.sdlc/state.json|*/.sdlc/approvals.json|*/.sdlc/comments.json|*/.sdlc/product-prs.json|*/.sdlc/hub-prs.json|*/reviews/*.md)
|
|
301
|
+
is_seeding "EP-foundation" && continue
|
|
302
|
+
touches_ledger=1
|
|
303
|
+
echo " ${sha} (author $(git show -s --format='%an' "$sha")) → $f"
|
|
304
|
+
;;
|
|
305
|
+
esac
|
|
306
|
+
;;
|
|
260
307
|
esac
|
|
261
308
|
done < <(git -c core.quotePath=false diff-tree --no-commit-id --name-only -r -z "$sha")
|
|
262
309
|
if [ "$touches_ledger" = 1 ] && ! trusted_bot "$sha"; then
|
|
@@ -52,7 +52,7 @@ fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f &&
|
|
|
52
52
|
# Same, for a link.md field. yad-spec writes link.md WITH frontmatter, but code repos still carry
|
|
53
53
|
# pre-frontmatter ones that contract-check used to read with a whole-file scan — so fall back to that
|
|
54
54
|
# rather than silently reading an empty value and skipping the check it guards. Deliberately separate
|
|
55
|
-
# from fm_val:
|
|
55
|
+
# from fm_val: Product artifacts (epic.md, stories/*.md) stay bounded to their first block.
|
|
56
56
|
link_val() {
|
|
57
57
|
_v="$(fm_val "$1" "$2")"
|
|
58
58
|
[ -n "$_v" ] || _v="$(sed -nE "s/^$1:[[:space:]]*(.*)\$/\1/p" "$2" 2>/dev/null | head -1 | tr -d '\r' | sed -E 's/[[:space:]]+$//')"
|
|
@@ -113,7 +113,7 @@ while IFS= read -r sha; do
|
|
|
113
113
|
fi
|
|
114
114
|
prod="$(resolve_product "$product_rel" "$story")"
|
|
115
115
|
epicmd="${prod}/epics/${epic}/epic.md"
|
|
116
|
-
# Defer ONLY when the product checkout itself is unreachable. A reachable
|
|
116
|
+
# Defer ONLY when the product checkout itself is unreachable. A reachable Product whose epic is missing is
|
|
117
117
|
# an orphaned story link — FAIL, do not pass it off as "not reachable".
|
|
118
118
|
if [ -z "$product_rel" ] || [ ! -d "$prod" ]; then
|
|
119
119
|
echo "PASS [lineage-check]: ${short} ${task} -> epic ${epic} (product repo not reachable — lineage check deferred)."
|
|
@@ -124,25 +124,39 @@ while IFS= read -r sha; do
|
|
|
124
124
|
rc=1
|
|
125
125
|
continue
|
|
126
126
|
fi
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
# The work-item TYPE. Two names are alive: `kind:` is the original and is still the one that
|
|
128
|
+
# counts; `type:` is the name from shape 5 on, written beside it. READ ORDER IS OLD FIRST, and it
|
|
129
|
+
# is the same order `workItemType` uses in cli/epic-state.mjs — cli/test-checks.mjs runs a table of
|
|
130
|
+
# frontmatter through both and asserts they agree, because two readers is two ways to drift.
|
|
131
|
+
# Absent from both means `feature`: an epic authored before types existed is its own thread root.
|
|
132
|
+
wtype="$(fm_val kind "$epicmd")"
|
|
133
|
+
[ -z "$wtype" ] && wtype="$(fm_val type "$epicmd")"
|
|
134
|
+
[ -z "$wtype" ] && wtype="feature"
|
|
135
|
+
# `feature` and `chore` may stand alone. Upkeep — a dependency bump, a CI move — usually has no
|
|
136
|
+
# feature to hang off, and an invented parent is worse than none: every thread rollup walks
|
|
137
|
+
# `parent:` and would file the upkeep under a feature it has nothing to do with.
|
|
138
|
+
#
|
|
139
|
+
# NOTE the asymmetry, which `feature` has always had: this PASSES before it looks at `parent:` at
|
|
140
|
+
# all, so a genesis type that DOES declare a dangling parent is not checked here. `yad doctor`
|
|
141
|
+
# catches that one (threadChecks skips only the parent-LESS case), so it is covered — just not by
|
|
142
|
+
# this gate.
|
|
143
|
+
if [ "$wtype" = "feature" ] || [ "$wtype" = "chore" ]; then
|
|
144
|
+
echo "PASS [lineage-check]: ${short} ${task} -> ${epic} (genesis ${wtype} epic)."
|
|
131
145
|
continue
|
|
132
146
|
fi
|
|
133
147
|
# A change/defect/hotfix epic MUST thread to a real parent.
|
|
134
148
|
parent="$(fm_val parent "$epicmd")"
|
|
135
149
|
if [ -z "$parent" ]; then
|
|
136
|
-
echo "FAIL [lineage-check]: ${short} ${task} -> ${epic} is
|
|
150
|
+
echo "FAIL [lineage-check]: ${short} ${task} -> ${epic} is type:${wtype} but declares no 'parent:' — a change-epic must thread to its predecessor."
|
|
137
151
|
rc=1
|
|
138
152
|
continue
|
|
139
153
|
fi
|
|
140
154
|
if [ ! -f "${prod}/epics/${parent}/epic.md" ]; then
|
|
141
|
-
echo "FAIL [lineage-check]: ${short} ${task} -> ${epic} threads to '${parent}', but epics/${parent}/ does not exist in the
|
|
155
|
+
echo "FAIL [lineage-check]: ${short} ${task} -> ${epic} threads to '${parent}', but epics/${parent}/ does not exist in the Product (orphan thread)."
|
|
142
156
|
rc=1
|
|
143
157
|
continue
|
|
144
158
|
fi
|
|
145
|
-
echo "PASS [lineage-check]: ${short} ${task} -> ${epic} (
|
|
159
|
+
echo "PASS [lineage-check]: ${short} ${task} -> ${epic} (type:${wtype} threaded to ${parent})."
|
|
146
160
|
done <<EOF
|
|
147
161
|
$commits
|
|
148
162
|
EOF
|
|
@@ -52,7 +52,7 @@ fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f &&
|
|
|
52
52
|
# Same, for a link.md field. yad-spec writes link.md WITH frontmatter, but code repos still carry
|
|
53
53
|
# pre-frontmatter ones that contract-check used to read with a whole-file scan — so fall back to that
|
|
54
54
|
# rather than silently reading an empty value and skipping the check it guards. Deliberately separate
|
|
55
|
-
# from fm_val:
|
|
55
|
+
# from fm_val: Product artifacts (epic.md, stories/*.md) stay bounded to their first block.
|
|
56
56
|
link_val() {
|
|
57
57
|
_v="$(fm_val "$1" "$2")"
|
|
58
58
|
[ -n "$_v" ] || _v="$(sed -nE "s/^$1:[[:space:]]*(.*)\$/\1/p" "$2" 2>/dev/null | head -1 | tr -d '\r' | sed -E 's/[[:space:]]+$//')"
|