yadflow 3.18.1 → 4.0.0-next.1
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 +355 -0
- package/README.md +79 -26
- package/bin/commands.mjs +41 -0
- package/bin/yad.mjs +437 -124
- package/cli/artifact-status.mjs +34 -15
- package/cli/checkpoint.mjs +69 -49
- package/cli/codeowners-command.mjs +170 -0
- package/cli/codeowners.mjs +397 -0
- package/cli/commit.mjs +13 -9
- package/cli/companion.mjs +2 -2
- package/cli/dial.mjs +183 -0
- package/cli/docs.mjs +88 -32
- package/cli/doctor.mjs +1472 -97
- package/cli/epic-state.mjs +3478 -232
- package/cli/epic.mjs +506 -0
- package/cli/errors.mjs +4 -1
- package/cli/gate.mjs +1002 -209
- package/cli/history.mjs +556 -0
- package/cli/hook.mjs +266 -55
- package/cli/hubcommit.mjs +6 -17
- package/cli/index-command.mjs +87 -0
- package/cli/ledger.mjs +57 -7
- package/cli/lib.mjs +184 -18
- package/cli/manifest.mjs +367 -56
- package/cli/migrate.mjs +726 -53
- package/cli/mode.mjs +170 -0
- package/cli/next.mjs +349 -90
- package/cli/openpr.mjs +191 -39
- package/cli/people.mjs +654 -0
- package/cli/plan.mjs +417 -132
- package/cli/platform.mjs +110 -129
- package/cli/product-index.mjs +287 -0
- package/cli/protection.mjs +706 -0
- package/cli/reconcile.mjs +38 -12
- package/cli/repo-publish.mjs +24 -26
- package/cli/repo.mjs +23 -14
- package/cli/report.mjs +21 -15
- package/cli/review.mjs +24 -27
- package/cli/riskmap-command.mjs +289 -0
- package/cli/riskmap.mjs +373 -0
- package/cli/setup.mjs +139 -287
- package/cli/ship.mjs +7 -6
- package/cli/skill.mjs +180 -0
- package/cli/skip.mjs +211 -30
- package/cli/thread.mjs +42 -17
- package/cli/tidy.mjs +20 -20
- package/cli/update-commit.mjs +22 -22
- package/cli/usage.mjs +115 -109
- package/package.json +3 -3
- package/skills/sdlc/config.yaml +166 -87
- package/skills/sdlc/module-help.csv +35 -35
- package/skills/yad-analysis/SKILL.md +125 -65
- package/skills/yad-architecture/SKILL.md +34 -23
- package/skills/yad-architecture/references/contract-format.md +10 -8
- package/skills/yad-backfill/SKILL.md +14 -8
- package/skills/yad-backfill/references/backfill.md +1 -1
- package/skills/yad-change/SKILL.md +127 -52
- package/skills/yad-change/references/triage.md +42 -28
- package/skills/yad-checks/SKILL.md +89 -45
- package/skills/yad-checks/references/check-gates.md +315 -92
- package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
- package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
- package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
- package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
- package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
- package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
- package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
- package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
- 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 +37 -5
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
- package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
- package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
- 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 +4 -4
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
- package/skills/yad-commit/SKILL.md +6 -6
- package/skills/yad-connect-design/SKILL.md +6 -6
- 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 +12 -12
- 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 +92 -54
- package/skills/yad-connect-repos/references/code-context.md +6 -6
- package/skills/yad-connect-repos/references/hub-config.md +68 -58
- package/skills/yad-connect-repos/references/repos-registry.md +10 -9
- package/skills/yad-connect-repos/references/risk-map.md +81 -0
- package/skills/yad-connect-testing/SKILL.md +6 -6
- 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 +8 -8
- package/skills/yad-discovery/SKILL.md +130 -94
- package/skills/yad-discovery/references/discovery-schema.md +23 -7
- package/skills/yad-discovery/references/foundation-schema.md +374 -0
- package/skills/yad-docs/SKILL.md +16 -11
- 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 +32 -17
- package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
- 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 +88 -24
- package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
- package/skills/yad-epic/SKILL.md +178 -100
- package/skills/yad-epic/references/state-schema.md +626 -117
- package/skills/yad-hub-bridge/SKILL.md +66 -48
- package/skills/yad-hub-bridge/references/bridge.md +110 -83
- 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 +22 -12
- package/skills/yad-implement/SKILL.md +29 -15
- 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 +64 -29
- package/skills/yad-pair-review/SKILL.md +18 -16
- package/skills/yad-pair-review/references/session-state.md +4 -4
- package/skills/yad-pr-template/SKILL.md +48 -27
- package/skills/yad-pr-template/references/risk-routing.md +97 -24
- package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
- package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
- 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 +15 -14
- package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
- package/skills/yad-reconcile/SKILL.md +3 -3
- package/skills/yad-report/SKILL.md +5 -5
- package/skills/yad-review-companion/SKILL.md +12 -9
- package/skills/yad-review-gate/SKILL.md +198 -79
- package/skills/yad-review-gate/references/gating.md +230 -54
- package/skills/yad-run/SKILL.md +86 -56
- package/skills/yad-run/references/run-loop.md +67 -45
- package/skills/yad-ship/SKILL.md +18 -14
- package/skills/yad-spec/SKILL.md +31 -17
- package/skills/yad-spec/references/spec-handoff.md +17 -5
- package/skills/yad-status/SKILL.md +114 -56
- package/skills/yad-stories/SKILL.md +42 -27
- package/skills/yad-stories/references/story-schema.md +10 -9
- package/skills/yad-stub/SKILL.md +59 -48
- package/skills/yad-sync-repos/SKILL.md +3 -3
- package/skills/yad-test-cases/SKILL.md +37 -30
- package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
- package/skills/yad-timeline/SKILL.md +8 -7
- package/skills/yad-ui/SKILL.md +46 -25
- package/cli/roster.mjs +0 -164
- package/skills/sdlc/install.sh +0 -68
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# build / test / lint gate (Phase 3 build plan §C).
|
|
3
3
|
# Standard quality stage: lint, build, and tests that actually exercise behavior (not just pass).
|
|
4
|
-
# Delegates to the
|
|
4
|
+
# Delegates to the package manager named by package.json so each repo owns the specifics.
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
7
|
+
YAD_CHECKS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
readonly YAD_CHECKS_DIR
|
|
9
|
+
# shellcheck source-path=SCRIPTDIR
|
|
10
|
+
# shellcheck source=package-manager.sh
|
|
11
|
+
source "$YAD_CHECKS_DIR/package-manager.sh"
|
|
12
|
+
# Two assignments on purpose: nested inside another $(...), a failing yad_package_manager_spec would
|
|
13
|
+
# be swallowed, and the gate would fall back to lockfile detection and PASS on a package.json it
|
|
14
|
+
# had just rejected. Each step fails the gate on its own under `set -e`.
|
|
15
|
+
package_manager_spec="$(yad_package_manager_spec)"
|
|
16
|
+
package_manager="$(yad_detect_package_manager "$package_manager_spec")"
|
|
17
|
+
# `npm`, `pnpm`, or `corepack npm` for a pinned npm — see yad_package_manager_command.
|
|
18
|
+
run_with_words="$(yad_package_manager_command "$package_manager_spec" "$package_manager")"
|
|
19
|
+
read -r -a run_with <<< "$run_with_words"
|
|
20
|
+
|
|
7
21
|
echo "[build/test/lint] lint…"
|
|
8
|
-
|
|
22
|
+
"${run_with[@]}" run --silent lint
|
|
9
23
|
echo "[build/test/lint] build…"
|
|
10
|
-
|
|
24
|
+
"${run_with[@]}" run --silent build
|
|
11
25
|
|
|
12
26
|
# Worker cap: when YAD_TEST_MAX_WORKERS is set AND the repo's test script is jest/vitest (the
|
|
13
27
|
# runners that accept --maxWorkers), forward it to bound CI test concurrency. For any other runner
|
|
14
28
|
# (node --test, mocha, …) it is a deliberate no-op so the gate never breaks on an unknown flag.
|
|
29
|
+
# npm needs the `--` separator to hand the flag to the script; pnpm (7+) forwards a literal `--` to
|
|
30
|
+
# the script instead, where jest reads `--maxWorkers=N` as a test-path pattern and finds no tests.
|
|
15
31
|
extra=""
|
|
16
32
|
if [ -n "${YAD_TEST_MAX_WORKERS:-}" ]; then
|
|
17
|
-
case "$(
|
|
18
|
-
*jest*|*vitest*)
|
|
33
|
+
case "$(node --input-type=module -e 'import fs from "node:fs"; const p=JSON.parse(fs.readFileSync("package.json", "utf8").replace(/^\uFEFF/, "")); process.stdout.write(p.scripts?.test || "")' 2>/dev/null || true)" in
|
|
34
|
+
*jest*|*vitest*)
|
|
35
|
+
if [ "$package_manager" = "pnpm" ]; then extra="--maxWorkers=${YAD_TEST_MAX_WORKERS}"; else extra="-- --maxWorkers=${YAD_TEST_MAX_WORKERS}"; fi
|
|
36
|
+
;;
|
|
19
37
|
esac
|
|
20
38
|
fi
|
|
21
39
|
echo "[build/test/lint] test…"
|
|
22
|
-
# Intentional word-splitting: $extra is
|
|
40
|
+
# Intentional word-splitting: $extra is empty, `-- --maxWorkers=N` (npm), or `--maxWorkers=N` (pnpm).
|
|
23
41
|
# shellcheck disable=SC2086
|
|
24
|
-
|
|
42
|
+
"${run_with[@]}" run --silent test $extra
|
|
25
43
|
|
|
26
44
|
echo "PASS [build/test/lint]: lint, build, and tests all green."
|
|
@@ -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) ---
|
|
@@ -47,6 +47,21 @@ RANGE="${BASE}..HEAD"
|
|
|
47
47
|
changed="$(git -c core.quotePath=false diff --name-only "$RANGE")"
|
|
48
48
|
surface="$(printf '%s\n' "$changed" | grep -E '^specs/[^/]+/contracts/' || true)"
|
|
49
49
|
|
|
50
|
+
# Slice paths this diff DELETES. `--name-only` above lists a deleted file exactly like a changed one,
|
|
51
|
+
# which is right for the trailer rule — removing an agreed endpoint IS a surface change — but it
|
|
52
|
+
# creates a dead end for the one epic that should never have had a slice in the first place.
|
|
53
|
+
#
|
|
54
|
+
# A short-lane epic has no lock and never will (E40). Once a `specs/<story>/contracts/` file exists
|
|
55
|
+
# under one, the unflagged rule below fails the commit, the no-lock rule further down fails the
|
|
56
|
+
# flagged one, and the cleanup commit that DELETES the file fails too — so there is no diff that
|
|
57
|
+
# passes, including the correct one. An escape hatch is not optional there.
|
|
58
|
+
#
|
|
59
|
+
# Deleting your own copy of a surface that nothing upstream has locked cannot widen it: with no lock
|
|
60
|
+
# there is no agreed shape for the deletion to contradict. So a delete-only diff is allowed through
|
|
61
|
+
# the no-lock branch (and only that branch — where a lock EXISTS, a deletion is still a real surface
|
|
62
|
+
# change and every rule below applies to it unchanged).
|
|
63
|
+
deleted="$(git -c core.quotePath=false diff --diff-filter=D --name-only "$RANGE" | grep -E '^specs/[^/]+/contracts/' || true)"
|
|
64
|
+
|
|
50
65
|
if [ -z "$surface" ]; then
|
|
51
66
|
echo "PASS [contract-check]: diff does not touch the contract surface (specs/*/contracts/**)."
|
|
52
67
|
exit 0
|
|
@@ -59,6 +74,8 @@ cc="$(git log "$RANGE" --format='%(trailers:key=Contract-Change,valueonly)' | se
|
|
|
59
74
|
if ! printf '%s\n' "$cc" | grep -qx 'yes'; then
|
|
60
75
|
echo "FAIL [contract-check]: contract surface changed without a 'Contract-Change: yes' trailer."
|
|
61
76
|
echo " -> Route back to the architecture gate: update + re-lock contract.md in the product repo,"
|
|
77
|
+
echo " or, if this epic is on a short lane (chore/spike) and has no architecture gate, move the"
|
|
78
|
+
echo " surface change to a new epic on the classic route,"
|
|
62
79
|
echo " re-run yad-spec, then implement with Contract-Change: yes. The surface is never widened"
|
|
63
80
|
echo " from inside a code repo."
|
|
64
81
|
exit 1
|
|
@@ -74,7 +91,7 @@ fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f &&
|
|
|
74
91
|
# Same, for a link.md field. yad-spec writes link.md WITH frontmatter, but code repos still carry
|
|
75
92
|
# pre-frontmatter ones that contract-check used to read with a whole-file scan — so fall back to that
|
|
76
93
|
# rather than silently reading an empty value and skipping the check it guards. Deliberately separate
|
|
77
|
-
# from fm_val:
|
|
94
|
+
# from fm_val: Product artifacts (epic.md, stories/*.md) stay bounded to their first block.
|
|
78
95
|
link_val() {
|
|
79
96
|
_v="$(fm_val "$1" "$2")"
|
|
80
97
|
[ -n "$_v" ] || _v="$(sed -nE "s/^$1:[[:space:]]*(.*)\$/\1/p" "$2" 2>/dev/null | head -1 | tr -d '\r' | sed -E 's/[[:space:]]+$//')"
|
|
@@ -146,6 +163,45 @@ while IFS= read -r story; do
|
|
|
146
163
|
continue
|
|
147
164
|
fi
|
|
148
165
|
echo "note [contract-check]: ${link} hash matches the product lock (${current:0:12}…)."
|
|
166
|
+
elif [ -n "$prod" ] && [ -d "${prod}/epics/${epic}/.sdlc" ]; then
|
|
167
|
+
# THE PRODUCT REPO RESOLVED AND THERE IS STILL NO LOCK. That is a different fact from "not
|
|
168
|
+
# reachable", and it must not share its answer. A `Contract-Change: yes` is being claimed against
|
|
169
|
+
# an epic that has no locked surface at all, so there is nothing the claim could be true of.
|
|
170
|
+
#
|
|
171
|
+
# This is the steady state of the short lanes (E40): `chore` and `spike` carry no architecture
|
|
172
|
+
# step, so `contract.md` and `contract-lock.json` never exist on them. Deferring here meant every
|
|
173
|
+
# short-lane epic could move the shared cross-repo surface forever, with a note that reads like a
|
|
174
|
+
# pass. The guard used to be prose in the authoring skills; this is the gate.
|
|
175
|
+
#
|
|
176
|
+
# Narrow on purpose, and the guard is the EPIC'S LEDGER DIRECTORY, not the product path — because
|
|
177
|
+
# `resolve_product` ALWAYS returns some path and never verifies that the one it picked exists. (Its
|
|
178
|
+
# own two `-d` tests only choose BETWEEN the story-relative and repo-root joins; neither rejects a
|
|
179
|
+
# miss.) Requiring `epics/<epic>/.sdlc/` to exist is what separates "this epic is real and has no
|
|
180
|
+
# lock" from "nothing is checked out here". A CI job that does not check the
|
|
181
|
+
# product repo out still defers below, exactly as before — that case proves nothing either way,
|
|
182
|
+
# and failing it would break every setup that has always run this way.
|
|
183
|
+
# The escape hatch described at the top: with no lock upstream there is no agreed shape for a
|
|
184
|
+
# deletion to contradict, so REMOVING a slice is always allowed here. This is what keeps a
|
|
185
|
+
# short-lane story that should never have had a `contracts/` folder from being unfixable.
|
|
186
|
+
story_surface="$(printf '%s\n' "$surface" | grep -E "^specs/${story}/contracts/" || true)"
|
|
187
|
+
story_deleted="$(printf '%s\n' "$deleted" | grep -E "^specs/${story}/contracts/" || true)"
|
|
188
|
+
if [ -n "$story_surface" ] && [ "$story_surface" = "$story_deleted" ]; then
|
|
189
|
+
echo "note [contract-check]: ${story} only REMOVES contract slice files and ${epic} has no lock —"
|
|
190
|
+
echo " nothing upstream is being contradicted, so the removal is allowed."
|
|
191
|
+
continue
|
|
192
|
+
fi
|
|
193
|
+
echo "FAIL [contract-check]: Contract-Change claimed, but ${epic} has no contract lock at all."
|
|
194
|
+
echo " Expected ${lock} — the epic's ledger directory is there and the lock file is not."
|
|
195
|
+
echo " Three ways to be here, with three different fixes:"
|
|
196
|
+
echo " - a SHORT LANE (chore/spike): it has no architecture step and never locks a surface, so it"
|
|
197
|
+
echo " carries no specs/<story>/contracts/ at all. DELETE the slice; a delete-only diff passes."
|
|
198
|
+
echo " A real surface change belongs to a new epic on the classic route."
|
|
199
|
+
echo " - a STUB / lightly-promoted brownfield epic: the feature was never documented, so there is"
|
|
200
|
+
echo " nothing to lock yet. Run yad-backfill and promote it fully (which locks contract.md)."
|
|
201
|
+
echo " - a CLASSIC epic that has not reached its architecture gate: author and lock contract.md"
|
|
202
|
+
echo " first (yad-architecture Step 5)."
|
|
203
|
+
rc=1
|
|
204
|
+
continue
|
|
149
205
|
else
|
|
150
206
|
# Say so. A skipped fidelity check used to be indistinguishable from a passed one, which is how a
|
|
151
207
|
# mis-resolved product-repo could turn a stale-pin FAIL into a silent PASS (issue #149).
|
|
@@ -156,7 +212,7 @@ $stories
|
|
|
156
212
|
EOF
|
|
157
213
|
|
|
158
214
|
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."
|
|
215
|
+
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
216
|
exit 1
|
|
161
217
|
fi
|
|
162
218
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# is `shipped`. A SEALED epic's artifacts are the final, approved description of shipped behaviour — so
|
|
4
4
|
# new behaviour must NOT be added to it; it belongs in a NEW threaded change-epic whose re-authored
|
|
5
5
|
# stories/test-cases describe the change. This gate FAILs any non-maintenance commit whose owning epic
|
|
6
|
-
# is sealed, forcing
|
|
6
|
+
# is sealed, forcing Shape to stay current (staleness becomes unshippable).
|
|
7
7
|
#
|
|
8
8
|
# The owning epic lives in the PRODUCT repo (via specs/<story>/link.md `product-repo`). When it is not
|
|
9
9
|
# reachable from CI, the seal cannot be read, so the commit PASSes with a note (degraded, fail-open here
|
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Deterministic CI dependency installation selected by package.json#packageManager.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
YAD_CHECKS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
readonly YAD_CHECKS_DIR
|
|
7
|
+
# shellcheck source-path=SCRIPTDIR
|
|
8
|
+
# shellcheck source=package-manager.sh
|
|
9
|
+
source "$YAD_CHECKS_DIR/package-manager.sh"
|
|
10
|
+
|
|
11
|
+
package_manager_spec="$(yad_package_manager_spec)"
|
|
12
|
+
package_manager="$(yad_detect_package_manager "$package_manager_spec")"
|
|
13
|
+
|
|
14
|
+
case "$package_manager" in
|
|
15
|
+
npm)
|
|
16
|
+
if [ ! -f "$YAD_NPM_LOCKFILE" ] && [ ! -f "$YAD_NPM_SHRINKWRAP" ]; then
|
|
17
|
+
echo "FAIL [install-deps]: npm requires package-lock.json or npm-shrinkwrap.json." >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
if [[ "$package_manager_spec" == npm@* ]]; then
|
|
21
|
+
yad_require_corepack
|
|
22
|
+
corepack enable
|
|
23
|
+
yad_corepack_prepare "$package_manager_spec"
|
|
24
|
+
# npm's shim is not enabled by Corepack, so dispatch it explicitly through Corepack — the same
|
|
25
|
+
# `corepack npm` the gate uses for lint/build/test, so package.json#packageManager selects the
|
|
26
|
+
# npm version end to end instead of the Node image's ambient npm binary.
|
|
27
|
+
corepack npm ci
|
|
28
|
+
else
|
|
29
|
+
npm ci
|
|
30
|
+
fi
|
|
31
|
+
;;
|
|
32
|
+
pnpm)
|
|
33
|
+
if [ ! -f "$YAD_PNPM_LOCKFILE" ]; then
|
|
34
|
+
echo "FAIL [install-deps]: pnpm requires pnpm-lock.yaml." >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
if [[ "$package_manager_spec" != pnpm@* ]]; then
|
|
38
|
+
echo "FAIL [install-deps]: pnpm CI requires an exact package.json#packageManager value (for example pnpm@9.15.0)." >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
yad_require_corepack
|
|
42
|
+
corepack enable
|
|
43
|
+
yad_corepack_prepare "$package_manager_spec"
|
|
44
|
+
pnpm install --frozen-lockfile
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ledger-guard gate.
|
|
3
|
-
# In
|
|
3
|
+
# In verified mode the gate ledger is CI-owned: only the yad gate-sync bot may change the
|
|
4
4
|
# machine-written gate-state files. A commit on a review PR by anyone else that modifies them is
|
|
5
5
|
# rejected — the human keeps the artifact, CI keeps the ledger. This makes "CI is the sole writer of
|
|
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.
|
|
@@ -20,21 +33,22 @@
|
|
|
20
33
|
# distinguishes CI-generated commits. A spoofed-author commit that is not Verified is treated as a
|
|
21
34
|
# human edit and rejected.
|
|
22
35
|
#
|
|
23
|
-
# Scope: enforced ONLY when the
|
|
24
|
-
# `
|
|
25
|
-
# `
|
|
26
|
-
#
|
|
36
|
+
# Scope: enforced ONLY when the ledger is verified — hub.json carries BOTH a `platform` and either
|
|
37
|
+
# `ledger: "verified"` or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or the
|
|
38
|
+
# legacy `bridge`) true. That is the same predicate `isVerifiedLedger` (cli/manifest.mjs) and
|
|
39
|
+
# `productActions` (cli/plan.mjs) apply. With a local ledger — or a platform-less Product — humans
|
|
40
|
+
# legitimately write the ledger themselves, so the gate is a no-op.
|
|
27
41
|
#
|
|
28
42
|
# Degradation: a base ref that cannot be resolved FAILs closed; no platform (cannot read the Verified
|
|
29
43
|
# badge) WARNs and waives the signature half — the same stance verified-commits takes.
|
|
30
44
|
set -euo pipefail
|
|
31
45
|
|
|
32
46
|
# ---- bridge gate: only CI-owned ledgers are guarded -------------------------------------------
|
|
33
|
-
# The predicate is BOTH a platform and the
|
|
34
|
-
# `
|
|
35
|
-
# other
|
|
47
|
+
# The predicate is BOTH a platform and the verified ledger flag, exactly as `isVerifiedLedger` (`cli/manifest.mjs`) and
|
|
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
|
|
36
50
|
# have its human ledger commits rejected here while the CLI, reading the same file, called it
|
|
37
|
-
#
|
|
51
|
+
# local and kept the LOCAL write path — no CI writer and no permitted human writer, so no gate
|
|
38
52
|
# could advance. Reachable through a stale install (platform set, script wired, platform later
|
|
39
53
|
# nulled), not through `yad setup`, which derives both from one value.
|
|
40
54
|
#
|
|
@@ -44,8 +58,8 @@ set -euo pipefail
|
|
|
44
58
|
#
|
|
45
59
|
# Matched at the ROOT LEVEL only. The shared `default_branch` read below is depth-blind, and that is
|
|
46
60
|
# survivable there — a false match yields a bogus branch name and the gate fails loudly. Here it is
|
|
47
|
-
# not: a nested `"bridge": true` (say under `review`) would silently ENABLE this gate on a
|
|
48
|
-
# `
|
|
61
|
+
# not: a nested `"bridge": true` (say under `review`) would silently ENABLE this gate on a Product whose
|
|
62
|
+
# `isVerifiedLedger` is false, recreating the exact no-writer deadlock #186 is about, from the other side. So
|
|
49
63
|
# the nesting is stripped rather than ignored: peel the outermost braces, then delete innermost
|
|
50
64
|
# objects/arrays until none remain, leaving only root-level pairs to match against. Not a JSON parser
|
|
51
65
|
# — a value containing a literal brace would confuse it — but hub.json is machine-written and the
|
|
@@ -67,9 +81,37 @@ done
|
|
|
67
81
|
# SIGPIPE-under-pipefail problem this avoids).
|
|
68
82
|
hub_str() { sed -nE "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p" <<< "$HUB_ROOT"; }
|
|
69
83
|
hub_true() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*true" <<< "$HUB_ROOT"; }
|
|
84
|
+
# Is the key present with a STRING value at all, empty included? `hub_str` cannot answer this: it
|
|
85
|
+
# returns nothing both for a missing key and for an empty one.
|
|
86
|
+
hub_has_str() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*\"" <<< "$HUB_ROOT"; }
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
# READ ORDER, identical to `isVerifiedLedger` (cli/manifest.mjs) — the two must never disagree:
|
|
89
|
+
# 1. `ledger`, if hub.json carries it — shape 2 and later. "verified" and nothing else.
|
|
90
|
+
# 2. otherwise the old booleans `bridge_enabled` (canonical) or `bridge` (older still).
|
|
91
|
+
# A platform is required either way, for the reason in the header.
|
|
92
|
+
#
|
|
93
|
+
# Step 2 is NOT dead weight. This script is committed inside the user's repo and refreshed by
|
|
94
|
+
# `yad update`, which is a separate act from `yad migrate` — so an un-migrated hub.json (no `ledger`
|
|
95
|
+
# key at all) will be read by this version of the script, and it has to keep saying "verified".
|
|
96
|
+
# cli/test-checks.mjs runs a table of hub.json variants through this script AND through the JS
|
|
97
|
+
# reader and asserts they agree on every row.
|
|
98
|
+
# Written as if/else rather than `cmd; verified=$?`: under the `set -e` above, a bare failing test
|
|
99
|
+
# would EXIT the script instead of recording a false — and exiting mid-guard is indistinguishable
|
|
100
|
+
# from passing, so the gate would silently stop guarding.
|
|
101
|
+
# PRESENCE of the key decides which branch runs, not whether its value is non-empty. `"ledger": ""`
|
|
102
|
+
# is a present key with a string value: the JS reader stops there and answers "local", so this must
|
|
103
|
+
# too. Testing `-n` instead sent an empty value down to the old booleans and the two readers gave
|
|
104
|
+
# OPPOSITE answers — the guard rejecting human writes while the CLI kept the local path open, which
|
|
105
|
+
# is the no-writer deadlock of #186 reached from a third direction.
|
|
106
|
+
verified=no
|
|
107
|
+
if hub_has_str ledger; then
|
|
108
|
+
if [ "$(hub_str ledger)" = "verified" ]; then verified=yes; fi
|
|
109
|
+
elif hub_true bridge_enabled || hub_true bridge; then
|
|
110
|
+
verified=yes
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ ! -f "$HUB" ] || [ -z "$(hub_str platform)" ] || [ "$verified" != yes ]; then
|
|
114
|
+
echo "PASS [ledger-guard]: the ledger is locally owned (ledger: local) — nothing to guard."
|
|
73
115
|
exit 0
|
|
74
116
|
fi
|
|
75
117
|
|
|
@@ -159,7 +201,7 @@ trusted_bot() {
|
|
|
159
201
|
# A brand-new epic's ledger has no CI author. `gate ci` only ADVANCES an existing chain — it bails on
|
|
160
202
|
# a missing state.json ("the review branch is cut from the default branch, so it should carry it") and
|
|
161
203
|
# writes only at merge, on the default branch — and the engine itself reads a missing state.json as
|
|
162
|
-
# "not seeded yet". `gate open` writes nothing in
|
|
204
|
+
# "not seeded yet". `gate open` writes nothing in verified mode, and `checkpoint` stages Build
|
|
163
205
|
# ledgers only. So the seed the authoring skills write (yad-epic / yad-change / yad-analysis /
|
|
164
206
|
# yad-discovery / yad-stub) can reach the trunk ONLY through the first review PR/MR — the one place
|
|
165
207
|
# this gate runs. Guarding it there makes the documented flow unshippable on a protected trunk, so an
|
|
@@ -200,15 +242,32 @@ is_seeding() { # $1 = epic slug; 0 when that epic has no le
|
|
|
200
242
|
_s="${_p#epics/}"; _s="${_s%%/*}"
|
|
201
243
|
base_slugs[${#base_slugs[@]}]="$(fold "$_s")"
|
|
202
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
|
+
;;
|
|
203
249
|
esac
|
|
204
|
-
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
|
|
205
263
|
base_slugs_loaded=1
|
|
206
264
|
fi
|
|
207
265
|
_f="$(fold "$1")"
|
|
208
266
|
in_list "$_f" "${base_slugs[@]}" && return 1
|
|
209
267
|
in_list "$_f" "${noted_slugs[@]}" && return 0
|
|
210
268
|
noted_slugs[${#noted_slugs[@]}]="$_f"
|
|
211
|
-
|
|
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)."
|
|
212
271
|
return 0
|
|
213
272
|
}
|
|
214
273
|
|
|
@@ -221,13 +280,30 @@ for sha in $commits; do
|
|
|
221
280
|
while IFS= read -r -d '' f; do
|
|
222
281
|
[ -n "$f" ] || continue
|
|
223
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
|
+
;;
|
|
224
287
|
epics/*/.sdlc/contract-lock.json) ;; # artifact-side — allowed
|
|
225
|
-
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)
|
|
226
289
|
_slug="${f#epics/}"; _slug="${_slug%%/*}"
|
|
227
290
|
is_seeding "$_slug" && continue # a new epic's seed — not a mutation of a CI-owned ledger
|
|
228
291
|
touches_ledger=1
|
|
229
292
|
echo " ${sha} (author $(git show -s --format='%an' "$sha")) → $f"
|
|
230
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
|
+
;;
|
|
231
307
|
esac
|
|
232
308
|
done < <(git -c core.quotePath=false diff-tree --no-commit-id --name-only -r -z "$sha")
|
|
233
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
|