yadflow 3.14.0 → 3.15.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 +16 -0
- package/cli/manifest.mjs +1 -1
- package/cli/setup.mjs +5 -2
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +1 -1
- package/skills/yad-backfill/SKILL.md +3 -2
- package/skills/yad-backfill/templates/checks/backfill-check.sh +26 -2
- package/skills/yad-checks/SKILL.md +5 -0
- package/skills/yad-checks/references/check-gates.md +43 -2
- package/skills/yad-checks/templates/checks/commit-message.sh +24 -1
- package/skills/yad-checks/templates/checks/contract-check.sh +70 -11
- package/skills/yad-checks/templates/checks/epic-open.sh +24 -1
- package/skills/yad-checks/templates/checks/ledger-guard.sh +24 -1
- package/skills/yad-checks/templates/checks/lineage-check.sh +24 -1
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +24 -1
- package/skills/yad-checks/templates/checks/spec-link.sh +24 -1
- package/skills/yad-checks/templates/checks/verified-commits.sh +24 -1
- package/skills/yad-connect-testing/SKILL.md +7 -3
- package/skills/yad-connect-testing/references/testing-context.md +6 -0
- package/skills/yad-connect-testing/references/testing-registry.md +2 -2
- package/skills/yad-docs/templates/app/package-lock.json +3 -3
- package/skills/yad-test-cases/SKILL.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [3.15.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.15.0...v3.15.1) (2026-08-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **checks:** close the silent-PASS holes the [#161](https://github.com/abdelrahmannasr/yadflow/issues/161) fix left open ([0f180ab](https://github.com/abdelrahmannasr/yadflow/commit/0f180abc72618fcd793ef6717fe1e128aa5ccfa5))
|
|
7
|
+
* **checks:** pin every changed contract slice, not just the first ([a79a946](https://github.com/abdelrahmannasr/yadflow/commit/a79a946aa37c08615741082bc638d006f7e80b76)), closes [#161](https://github.com/abdelrahmannasr/yadflow/issues/161)
|
|
8
|
+
* **checks:** read hub.json and the contract lock across line breaks ([43a618d](https://github.com/abdelrahmannasr/yadflow/commit/43a618d0822ed8576b9a164889bc53a52bbdf713)), closes [#161](https://github.com/abdelrahmannasr/yadflow/issues/161)
|
|
9
|
+
|
|
10
|
+
# [3.15.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.14.0...v3.15.0) (2026-08-10)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **testing:** add maestro as a testing-tool adapter ([7718b50](https://github.com/abdelrahmannasr/yadflow/commit/7718b502814ee8e0eb46f5e6f20342fc2471dbd7))
|
|
16
|
+
|
|
1
17
|
# [3.14.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.13.2...v3.14.0) (2026-08-10)
|
|
2
18
|
|
|
3
19
|
|
package/cli/manifest.mjs
CHANGED
|
@@ -135,7 +135,7 @@ export const DESIGN_PRIMARY = 'figma';
|
|
|
135
135
|
// is the fallback `registerTesting`/setup use when an unknown tool is named, and `none` is the explicit
|
|
136
136
|
// artifacts-only choice. (doctor does NOT fall back — an unknown tool there is a hard YAD-CFG-003 fail,
|
|
137
137
|
// mirroring the design-tool YAD-CFG-002.)
|
|
138
|
-
export const TESTING_TOOLS = ['playwright', 'cypress', 'pytest'];
|
|
138
|
+
export const TESTING_TOOLS = ['playwright', 'cypress', 'pytest', 'maestro'];
|
|
139
139
|
export const TESTING_PRIMARY = 'playwright';
|
|
140
140
|
|
|
141
141
|
// Supported learning-tool adapters (mirrors skills/sdlc/config.yaml `learning.tools`); `LEARNING_PRIMARY`
|
package/cli/setup.mjs
CHANGED
|
@@ -590,9 +590,12 @@ export async function runSetup(root, opts = {}) {
|
|
|
590
590
|
}
|
|
591
591
|
|
|
592
592
|
// Connect a testing tool (Playwright-first, pluggable; the test-cases step implements automation here)
|
|
593
|
-
|
|
593
|
+
// Banner + guide read TESTING_TOOLS rather than spelling the adapters out: this is the list that
|
|
594
|
+
// actually grows (maestro joined it), and a hardcoded copy here would quietly offer the user fewer
|
|
595
|
+
// tools than the prompt below accepts.
|
|
596
|
+
S(`Connect a testing tool (${TESTING_TOOLS.join(' / ')} / none)`);
|
|
594
597
|
guide([
|
|
595
|
-
|
|
598
|
+
`Where yad-test-cases generates automation. ${TESTING_TOOLS.join('/')}, or none for artifacts-only.`,
|
|
596
599
|
'Skipping is safe — test-cases authors test-cases.md only.',
|
|
597
600
|
]);
|
|
598
601
|
if (exists(testingPath) && !(await askYesNo('testing.json exists — reconfigure?', false))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.1",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 38 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
package/skills/sdlc/config.yaml
CHANGED
|
@@ -198,7 +198,7 @@ design:
|
|
|
198
198
|
# (test-links.json) is written by yad-test-cases per epic.
|
|
199
199
|
testing:
|
|
200
200
|
registry: "{project-root}/.sdlc/testing.json" # project-wide testing connection (NOT per-epic)
|
|
201
|
-
tools: [playwright, cypress, pytest]
|
|
201
|
+
tools: [playwright, cypress, pytest, maestro] # supported adapters; an unknown tool falls back to `primary`
|
|
202
202
|
primary: playwright # the default/named provider
|
|
203
203
|
degrade: artifacts-only # no tool / no MCP => yad-test-cases authors test-cases.md only
|
|
204
204
|
links: "{project-root}/epics/EP-<slug>/.sdlc/test-links.json" # per-epic case->test map (yad-test-cases)
|
|
@@ -72,8 +72,9 @@ the approver(s) + date. Only a `verified: true` backfill spec counts as real.
|
|
|
72
72
|
|
|
73
73
|
### Step 5 — `gate` (block changes per touched feature)
|
|
74
74
|
`bash checks/backfill-check.sh <base>` blocks a change that touches a feature being backfilled until
|
|
75
|
-
that feature's spec is `verified: true`.
|
|
76
|
-
|
|
75
|
+
that feature's spec is `verified: true`. `<base>` is optional and resolves like every other gate's —
|
|
76
|
+
see "Resolving `<base>`" in `../yad-checks/references/check-gates.md`. It is **per touched feature** —
|
|
77
|
+
a change touching feature A is not blocked by an unverified feature B. Forward-spec'd features (those with their own `specs/<story>/`)
|
|
77
78
|
are not this gate's concern.
|
|
78
79
|
|
|
79
80
|
### Step 6 — `promote` (flip a stub epic → real, once its spec is approved)
|
|
@@ -5,13 +5,37 @@
|
|
|
5
5
|
# forward-spec'd (their own specs/<story>/) or not yet being backfilled are not this gate's concern.
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
9
|
+
# --- is duplicated, not sourced) ---
|
|
10
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
11
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
12
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
13
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
14
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
15
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
16
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
17
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
18
|
+
# command substitution in an assignment aborts the script.
|
|
19
|
+
resolve_base() {
|
|
20
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
21
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
22
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
23
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
24
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
25
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
26
|
+
done
|
|
27
|
+
printf '%s' origin/main
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
31
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [backfill]: no base given — diffing against '${BASE}'."
|
|
9
32
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
10
33
|
echo "FAIL [backfill]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
11
34
|
exit 1
|
|
12
35
|
fi
|
|
13
36
|
|
|
14
|
-
|
|
37
|
+
# quotePath off so a non-ASCII feature directory still matches src/<feature>/ below.
|
|
38
|
+
changed="$(git -c core.quotePath=false diff --name-only "${BASE}..HEAD")"
|
|
15
39
|
# Feature = a directory under src/ (src/<feature>/...). Top-level src/*.js files are deliberately NOT
|
|
16
40
|
# gated here (they belong to no single feature); only src/<feature>/ changes are checked.
|
|
17
41
|
feats="$(printf '%s\n' "$changed" | sed -nE 's#^src/([^/]+)/.*#\1#p' | sort -u)"
|
|
@@ -15,6 +15,7 @@ in CI on every PR/MR and must pass before merge (build plan §C). Each is a smal
|
|
|
15
15
|
`specs/<story>/contracts/`) without a `Contract-Change: yes` trailer **and** an updated, re-locked
|
|
16
16
|
contract upstream, it **FAILS and routes back to the architecture gate**. The shared surface is
|
|
17
17
|
never widened from inside a code repo (Phase 2 contract representation: delimited block + SHA-256 lock).
|
|
18
|
+
Every story whose slice the diff touches is checked, not just the first — see `references/check-gates.md`.
|
|
18
19
|
3. **build/test/lint** — standard quality stage; tests must actually exercise new behavior, not just pass.
|
|
19
20
|
The CI job sets `YAD_TEST_MAX_WORKERS` (default `2`); the gate caps jest/vitest test concurrency at
|
|
20
21
|
that and is a no-op for other runners (see `references/check-gates.md`).
|
|
@@ -141,6 +142,10 @@ bash checks/spec-link.sh "<base>"
|
|
|
141
142
|
bash checks/contract-check.sh "<base>"
|
|
142
143
|
bash checks/build-test-lint.sh
|
|
143
144
|
```
|
|
145
|
+
`<base>` is optional for the gates that take one (`build-test-lint` takes none) — omitted, the gate
|
|
146
|
+
resolves the trunk (configured `default_branch`, else `origin/HEAD`, else `origin/main`) and prints
|
|
147
|
+
the base it chose; pass it (or `SDLC_BASE`) when the PR/MR targets another branch.
|
|
148
|
+
|
|
144
149
|
A non-zero exit is a FAIL. Summarize which gates passed and, for any failure, the exact remediation
|
|
145
150
|
(spec-link: add the `Task:` trailer / spec; contract-check: route back to the architecture gate and
|
|
146
151
|
re-lock the contract; build/test/lint: fix the failing lint/test).
|
|
@@ -19,6 +19,27 @@ repo uses. Each reads conventions established by earlier steps — it invents no
|
|
|
19
19
|
| pr-title | the PR/MR title (from the CI event payload) | `yad-pr-template` (`config.yaml build.pr_title_style`) |
|
|
20
20
|
| pr-template | the PR/MR body (from the CI event payload) | `yad-pr-template` (the committed PR/MR template) |
|
|
21
21
|
|
|
22
|
+
## Resolving `<base>` (every gate that takes one)
|
|
23
|
+
|
|
24
|
+
The `<base>` argument is **optional**. The order is: the **argument**, else `SDLC_BASE`, else the
|
|
25
|
+
**configured** `default_branch` (`.sdlc/hub.json`, or `SDLC_HUB_CONFIG`), else the remote's
|
|
26
|
+
**published default branch** (`git symbolic-ref refs/remotes/origin/HEAD`), else `origin/main` —
|
|
27
|
+
the same order the CLI resolves (`cli/hubcommit.mjs`, `cli/repo.mjs`), so a gate never diffs a
|
|
28
|
+
different range than the `yad` commands run beside it. Each candidate must actually **resolve**
|
|
29
|
+
before it is used, so a *dangling* `origin/HEAD` (trunk renamed, the old remote-tracking ref pruned)
|
|
30
|
+
falls through instead of failing the gate on a fully-fetched repo. CI always passes the base
|
|
31
|
+
explicitly (`origin/<PR base>` / `origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`), so this governs
|
|
32
|
+
local runs.
|
|
33
|
+
|
|
34
|
+
Hardcoding `origin/main` diffed the wrong range on a repo whose trunk is `develop`/`master` — either
|
|
35
|
+
failing closed for the wrong reason, or, where a stale `main` still existed, silently diffing a range
|
|
36
|
+
that both mis-reports the surface and drags unrelated stories into contract-check's per-story fidelity
|
|
37
|
+
pass (issue #161). An auto-resolved base is **printed as a note**, so the range a local run gated is
|
|
38
|
+
never implicit. Like the `product-repo` block below, this one is duplicated verbatim across the
|
|
39
|
+
scripts (they are standalone by design) and pinned byte-identical by a test — which covers every
|
|
40
|
+
base-taking gate, including `yad-backfill`'s `backfill-check.sh` and the installed copies this repo's
|
|
41
|
+
own CI runs, plus an assertion that each one actually *assigns* `BASE` from it.
|
|
42
|
+
|
|
22
43
|
## 1. spec-link (`templates/checks/spec-link.sh`)
|
|
23
44
|
|
|
24
45
|
- Checks every non-merge commit in `<base>..HEAD` **per commit** (not aggregated across the range),
|
|
@@ -38,11 +59,13 @@ repo uses. Each reads conventions established by earlier steps — it invents no
|
|
|
38
59
|
- An empty range (no non-merge commits) **PASSes**.
|
|
39
60
|
- Portable across bash 3.2 (macOS) and 4+ (no `mapfile`).
|
|
40
61
|
- **Fails closed** when `<base>` can't be resolved (so a shallow clone / wrong base never PASSes blind).
|
|
62
|
+
`<base>` is optional — see [Resolving `<base>`](#resolving-base-every-gate-that-takes-one).
|
|
41
63
|
|
|
42
64
|
## 2. contract-check (`templates/checks/contract-check.sh`)
|
|
43
65
|
|
|
44
66
|
- **Fails closed** if `<base>` can't be resolved — an undiffable range must never report "no surface
|
|
45
|
-
change" and silently green-light a bypass.
|
|
67
|
+
change" and silently green-light a bypass. `<base>` is optional — see
|
|
68
|
+
[Resolving `<base>`](#resolving-base-every-gate-that-takes-one).
|
|
46
69
|
- Computes the changed files in `<base>..HEAD`.
|
|
47
70
|
- If **nothing** under `specs/*/contracts/**` changed → **PASS** (normal implementation only *consumes*
|
|
48
71
|
the contract).
|
|
@@ -52,6 +75,19 @@ repo uses. Each reads conventions established by earlier steps — it invents no
|
|
|
52
75
|
require `link.md`'s pinned `contract-lock` hash to match the product repo's current
|
|
53
76
|
`contract-lock.json`. A claimed change that still pins the **old** lock **FAILS** — re-run
|
|
54
77
|
`yad-spec` so the slice matches the re-locked contract.
|
|
78
|
+
- The fidelity check runs for **every story whose slice the diff touches**, and **aggregates**: each
|
|
79
|
+
story reports (matched / stale / deferred), and any stale pin fails the gate. `git diff
|
|
80
|
+
--name-only` is path-sorted, so reading one story off the first changed path validated whichever
|
|
81
|
+
story sorted first and left the rest unpinned — a later story pinning a stale hash passed, and a
|
|
82
|
+
first story with no `link.md` deferred the whole check before the stale one was ever read
|
|
83
|
+
(issue #161). One clean-or-deferred story never masks another's stale pin, the same rule spec-link
|
|
84
|
+
applies per commit.
|
|
85
|
+
- A lock the gate can **read but not parse** (truncated, half-written, or a changed schema — no
|
|
86
|
+
`"hash": "sha256:…"`) **FAILS**. It used to short-circuit the comparison into the "hash matches"
|
|
87
|
+
note, i.e. the gate affirmatively reported a match it never made.
|
|
88
|
+
- The changed-file list is read with `core.quotePath=false`. With git's default, a path holding a
|
|
89
|
+
non-ASCII byte comes back quoted and octal-escaped, so a slice like `specs/EP-démo-S01/contracts/…`
|
|
90
|
+
never matched the surface pattern and an undeclared widening passed untouched.
|
|
55
91
|
- This enforces the Phase 2 rule: the shared surface is owned upstream and is never widened from inside
|
|
56
92
|
a code repo. The hash recipe is in `../yad-architecture/references/contract-format.md`.
|
|
57
93
|
|
|
@@ -129,6 +165,7 @@ non-merge commit in `<base>..HEAD`:
|
|
|
129
165
|
- **Profiles** (`--profile code|hub`): the subject rule is identical on both; the gate never requires
|
|
130
166
|
the `Task:` trailer (spec-link owns that on code repos; hub commits are not task-scoped).
|
|
131
167
|
- **Fails closed** when `<base>` can't be resolved.
|
|
168
|
+
`<base>` is optional — see [Resolving `<base>`](#resolving-base-every-gate-that-takes-one).
|
|
132
169
|
|
|
133
170
|
## 6. pr-title (`templates/checks/pr-title.sh`)
|
|
134
171
|
|
|
@@ -319,9 +356,13 @@ line). Code repos run the same three with `--profile code` inside the main `yad-
|
|
|
319
356
|
|
|
320
357
|
## Running by hand (Phase 3 is manual)
|
|
321
358
|
|
|
322
|
-
From inside the code repo, against the PR/MR base (e.g. `master`)
|
|
359
|
+
From inside the code repo, against the PR/MR base (e.g. `master`). For the gates that take one, the
|
|
360
|
+
base argument is optional — omit it and the gate resolves the trunk in the order above (configured
|
|
361
|
+
`default_branch`, else `origin/HEAD`, else `origin/main`), printing the base it chose; pass it (or
|
|
362
|
+
`export SDLC_BASE=…`) whenever the PR targets something else. `build-test-lint` takes no base at all.
|
|
323
363
|
|
|
324
364
|
```bash
|
|
365
|
+
bash checks/spec-link.sh # -> diffs the resolved trunk, and says which one
|
|
325
366
|
bash checks/spec-link.sh master
|
|
326
367
|
bash checks/contract-check.sh master
|
|
327
368
|
bash checks/build-test-lint.sh
|
|
@@ -25,7 +25,30 @@ while [ $# -gt 0 ]; do
|
|
|
25
25
|
done
|
|
26
26
|
case "$PROFILE" in code|hub) ;; *) echo "FAIL [commit-message]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
29
|
+
# --- is duplicated, not sourced) ---
|
|
30
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
31
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
32
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
33
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
34
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
35
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
36
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
37
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
38
|
+
# command substitution in an assignment aborts the script.
|
|
39
|
+
resolve_base() {
|
|
40
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
41
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
42
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
43
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
44
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
45
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
46
|
+
done
|
|
47
|
+
printf '%s' origin/main
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
BASE="${ARGS[0]:-${SDLC_BASE:-$(resolve_base)}}"
|
|
51
|
+
[ -n "${ARGS[0]:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [commit-message]: no base given — diffing against '${BASE}'."
|
|
29
52
|
|
|
30
53
|
# Fail closed if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
|
|
31
54
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
@@ -8,7 +8,30 @@
|
|
|
8
8
|
# architecture gate. Normal implementation that only CONSUMES the contract passes untouched.
|
|
9
9
|
set -euo pipefail
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
12
|
+
# --- is duplicated, not sourced) ---
|
|
13
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
14
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
15
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
16
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
17
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
18
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
19
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
20
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
21
|
+
# command substitution in an assignment aborts the script.
|
|
22
|
+
resolve_base() {
|
|
23
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
24
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
25
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
26
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
27
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
28
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
29
|
+
done
|
|
30
|
+
printf '%s' origin/main
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
34
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [contract-check]: no base given — diffing against '${BASE}'."
|
|
12
35
|
|
|
13
36
|
# Fail CLOSED if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
|
|
14
37
|
# Never let an undiffable range silently report "no surface change" — that would green-light a bypass.
|
|
@@ -18,7 +41,10 @@ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
|
18
41
|
fi
|
|
19
42
|
RANGE="${BASE}..HEAD"
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
# core.quotePath=false: with the default ON, git wraps any path holding a non-ASCII byte in quotes
|
|
45
|
+
# and octal-escapes it, so a slice like specs/EP-démo-S01/contracts/api.md never matches the pattern
|
|
46
|
+
# below — the surface change would be invisible to the gate it exists to stop.
|
|
47
|
+
changed="$(git -c core.quotePath=false diff --name-only "$RANGE")"
|
|
22
48
|
surface="$(printf '%s\n' "$changed" | grep -E '^specs/[^/]+/contracts/' || true)"
|
|
23
49
|
|
|
24
50
|
if [ -z "$surface" ]; then
|
|
@@ -72,9 +98,22 @@ resolve_product() {
|
|
|
72
98
|
|
|
73
99
|
# Fidelity check (best-effort): when the product repo is reachable, the story's link.md must pin the
|
|
74
100
|
# CURRENT product lock — proof the contract was actually updated/re-locked upstream, not just flagged.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
#
|
|
102
|
+
# Checked for EVERY story whose slice the diff touches, not just the first one. `git diff --name-only`
|
|
103
|
+
# is path-sorted, so reading a single story off `head -1` validated whichever story sorted first and
|
|
104
|
+
# left the rest unpinned: a second story pinning a STALE hash passed, and a first story with no
|
|
105
|
+
# link.md deferred the whole check before the stale one was ever read (issue #161). Failures are
|
|
106
|
+
# AGGREGATED — every story reports, so one clean-or-deferred story never masks another's stale pin
|
|
107
|
+
# (the same rule spec-link applies per commit).
|
|
108
|
+
stories="$(printf '%s\n' "$surface" | sed -E 's#^specs/([^/]+)/contracts/.*#\1#' | sort -u)"
|
|
109
|
+
rc=0
|
|
110
|
+
while IFS= read -r story; do
|
|
111
|
+
[ -z "$story" ] && continue
|
|
112
|
+
link="specs/${story}/link.md"
|
|
113
|
+
if [ ! -f "$link" ]; then
|
|
114
|
+
echo "note [contract-check]: no ${link} — fidelity check deferred (spec-link gates the link itself)."
|
|
115
|
+
continue
|
|
116
|
+
fi
|
|
78
117
|
product_rel="$(link_val product-repo "$link")"
|
|
79
118
|
pinned="$(printf '%s' "$(link_val contract-lock "$link")" | sed -E 's/^sha256:([0-9a-f]+).*$/\1/')"
|
|
80
119
|
epic="$(printf '%s' "$story" | sed -E 's/-S[0-9]+$//')" # story EP-<slug>-S0N -> epic EP-<slug>
|
|
@@ -85,20 +124,40 @@ if [ -f "$link" ]; then
|
|
|
85
124
|
lock=""
|
|
86
125
|
[ -n "$prod" ] && lock="${prod}/epics/${epic}/.sdlc/contract-lock.json"
|
|
87
126
|
if [ -n "$product_rel" ] && [ -f "$lock" ]; then
|
|
88
|
-
|
|
89
|
-
|
|
127
|
+
# Newline-tolerant, first-match: `"hash":` and its value may legally sit on separate lines, and an
|
|
128
|
+
# unparseable lock is a FAIL below — so a formatting choice must not become a gate failure.
|
|
129
|
+
# `|| current=""` is load-bearing: under `pipefail` a no-match grep fails the whole pipeline, which
|
|
130
|
+
# under `set -e` would abort the gate instead of reaching the unparseable-lock FAIL below.
|
|
131
|
+
current="$(tr '\n' ' ' < "$lock" | grep -oE '"hash"[[:space:]]*:[[:space:]]*"sha256:[0-9a-f]+"' | head -1 | sed -E 's/.*sha256:([0-9a-f]+)"$/\1/')" || current=""
|
|
132
|
+
# A lock we can READ but cannot PARSE proves nothing, and an empty `current` used to short-circuit
|
|
133
|
+
# the comparison below straight into the "hash matches" note — the gate affirmatively reporting a
|
|
134
|
+
# match it never made. Fail closed instead: a truncated, half-written or schema-changed lock is a
|
|
135
|
+
# broken lock, and a Contract-Change is being claimed against it.
|
|
136
|
+
if [ -z "$current" ]; then
|
|
137
|
+
echo "FAIL [contract-check]: ${lock} has no readable \"hash\": \"sha256:…\" value —"
|
|
138
|
+
echo " the lock cannot prove ${link}'s pin. Re-lock the contract upstream (yad-architecture Step 5)."
|
|
139
|
+
rc=1
|
|
140
|
+
continue
|
|
141
|
+
fi
|
|
142
|
+
if [ "$current" != "$pinned" ]; then
|
|
90
143
|
echo "FAIL [contract-check]: Contract-Change claimed, but ${link} still pins ${pinned:0:12}…"
|
|
91
144
|
echo " while the product lock is ${current:0:12}… — re-run yad-spec so the slice matches the re-locked contract."
|
|
92
|
-
|
|
145
|
+
rc=1
|
|
146
|
+
continue
|
|
93
147
|
fi
|
|
94
|
-
echo "note [contract-check]: link
|
|
148
|
+
echo "note [contract-check]: ${link} hash matches the product lock (${current:0:12}…)."
|
|
95
149
|
else
|
|
96
150
|
# Say so. A skipped fidelity check used to be indistinguishable from a passed one, which is how a
|
|
97
151
|
# mis-resolved product-repo could turn a stale-pin FAIL into a silent PASS (issue #149).
|
|
98
152
|
echo "note [contract-check]: product lock not reachable at ${lock:-<no product-repo in link.md>} — fidelity check deferred."
|
|
99
153
|
fi
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
done <<EOF
|
|
155
|
+
$stories
|
|
156
|
+
EOF
|
|
157
|
+
|
|
158
|
+
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."
|
|
160
|
+
exit 1
|
|
102
161
|
fi
|
|
103
162
|
|
|
104
163
|
echo "PASS [contract-check]: surface change accompanied by Contract-Change: yes (and an updated contract)."
|
|
@@ -11,7 +11,30 @@
|
|
|
11
11
|
# Fails CLOSED on an unresolvable base.
|
|
12
12
|
set -euo pipefail
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
15
|
+
# --- is duplicated, not sourced) ---
|
|
16
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
17
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
18
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
19
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
20
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
21
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
22
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
23
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
24
|
+
# command substitution in an assignment aborts the script.
|
|
25
|
+
resolve_base() {
|
|
26
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
27
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
28
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
29
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
30
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
31
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
32
|
+
done
|
|
33
|
+
printf '%s' origin/main
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
37
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [epic-open]: no base given — diffing against '${BASE}'."
|
|
15
38
|
|
|
16
39
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
17
40
|
echo "FAIL [epic-open]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
@@ -32,7 +32,30 @@ if [ ! -f "$HUB" ] || ! grep -Eq '"(bridge_enabled|bridge)"[[:space:]]*:[[:space
|
|
|
32
32
|
exit 0
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
36
|
+
# --- is duplicated, not sourced) ---
|
|
37
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
38
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
39
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
40
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
41
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
42
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
43
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
44
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
45
|
+
# command substitution in an assignment aborts the script.
|
|
46
|
+
resolve_base() {
|
|
47
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
48
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
49
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
50
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
51
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
52
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
53
|
+
done
|
|
54
|
+
printf '%s' origin/main
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
58
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [ledger-guard]: no base given — diffing against '${BASE}'."
|
|
36
59
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
37
60
|
echo "FAIL [ledger-guard]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
38
61
|
exit 1
|
|
@@ -10,7 +10,30 @@
|
|
|
10
10
|
# best-effort: the commit PASSes with a note (spec-link already proved the story link).
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
14
|
+
# --- is duplicated, not sourced) ---
|
|
15
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
16
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
17
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
18
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
19
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
20
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
21
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
22
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
23
|
+
# command substitution in an assignment aborts the script.
|
|
24
|
+
resolve_base() {
|
|
25
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
26
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
27
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
28
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
29
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
30
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
31
|
+
done
|
|
32
|
+
printf '%s' origin/main
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
36
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [lineage-check]: no base given — diffing against '${BASE}'."
|
|
14
37
|
|
|
15
38
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
16
39
|
echo "FAIL [lineage-check]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
@@ -10,7 +10,30 @@
|
|
|
10
10
|
# ci/chore/build/test exempt. Fails CLOSED on an unresolvable base.
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
14
|
+
# --- is duplicated, not sourced) ---
|
|
15
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
16
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
17
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
18
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
19
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
20
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
21
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
22
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
23
|
+
# command substitution in an assignment aborts the script.
|
|
24
|
+
resolve_base() {
|
|
25
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
26
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
27
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
28
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
29
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
30
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
31
|
+
done
|
|
32
|
+
printf '%s' origin/main
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
36
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [reconcile-debt]: no base given — diffing against '${BASE}'."
|
|
14
37
|
|
|
15
38
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
16
39
|
echo "FAIL [reconcile-debt]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
@@ -10,7 +10,30 @@
|
|
|
10
10
|
# range), so the report names every offending commit.
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
14
|
+
# --- is duplicated, not sourced) ---
|
|
15
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
16
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
17
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
18
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
19
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
20
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
21
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
22
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
23
|
+
# command substitution in an assignment aborts the script.
|
|
24
|
+
resolve_base() {
|
|
25
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
26
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
27
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
28
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
29
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
30
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
31
|
+
done
|
|
32
|
+
printf '%s' origin/main
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
36
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [spec-link]: no base given — diffing against '${BASE}'."
|
|
14
37
|
|
|
15
38
|
# Fail closed if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
|
|
16
39
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
@@ -26,7 +26,30 @@
|
|
|
26
26
|
# GITLAB_TOKEN (or SDLC_API_TOKEN) with read_api scope; see the pipeline fragment header.
|
|
27
27
|
set -euo pipefail
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
30
|
+
# --- is duplicated, not sourced) ---
|
|
31
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
32
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
33
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
34
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
35
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
36
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
37
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
38
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
39
|
+
# command substitution in an assignment aborts the script.
|
|
40
|
+
resolve_base() {
|
|
41
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
42
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
43
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
44
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
45
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
46
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
47
|
+
done
|
|
48
|
+
printf '%s' origin/main
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
52
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [verified-commits]: no base given — diffing against '${BASE}'."
|
|
30
53
|
|
|
31
54
|
# Fail closed if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
|
|
32
55
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
@@ -21,7 +21,7 @@ connected, `yad-test-cases` runs artifacts-only exactly as before.
|
|
|
21
21
|
- `{project-root}` resolves from the project working directory (the **product hub**).
|
|
22
22
|
- The integration is **Playwright-first but pluggable** (`config.yaml` `testing.tools`): a testing-tool
|
|
23
23
|
*adapter*, like the GitHub/GitLab platform adapter or the design-tool adapter. Playwright is the
|
|
24
|
-
primary provider; `cypress` and `
|
|
24
|
+
primary provider; `cypress`, `pytest` and `maestro` are second providers; `none` → artifacts-only.
|
|
25
25
|
- **The testing tool is reached through its MCP** (a harness MCP server), NOT a subprocess CLI — the
|
|
26
26
|
same shape as the design tool's MCP, not Repomix's `npx`. The skill detects the MCP and degrades when
|
|
27
27
|
it is absent; it never installs an MCP server.
|
|
@@ -34,7 +34,7 @@ connected, `yad-test-cases` runs artifacts-only exactly as before.
|
|
|
34
34
|
## Inputs
|
|
35
35
|
|
|
36
36
|
- `action` — `connect` (default) | `refresh` | `list` | `disconnect`.
|
|
37
|
-
- `tool` — `playwright` | `cypress` | `pytest` | another adapter id (`config.yaml` `testing.tools`).
|
|
37
|
+
- `tool` — `playwright` | `cypress` | `pytest` | `maestro` | another adapter id (`config.yaml` `testing.tools`).
|
|
38
38
|
`none` records a deliberate artifacts-only project.
|
|
39
39
|
- `project_url` — the testing tool's project/config reference (e.g. a `playwright.config.ts` path or a
|
|
40
40
|
test-runner project URL). Optional — a connection with no suite yet is valid; `yad-test-cases` can
|
|
@@ -51,9 +51,13 @@ way `registerRepo` falls back on an unknown platform). Then **detect the tool's
|
|
|
51
51
|
- **playwright** → a Playwright MCP server (drives a browser, generates/runs E2E + API specs).
|
|
52
52
|
- **cypress** → the Cypress MCP (generate/run Cypress specs).
|
|
53
53
|
- **pytest** → a pytest MCP (generate/run service-layer tests).
|
|
54
|
+
- **maestro** → the Maestro MCP, bundled in the Maestro CLI and started as `maestro mcp` over STDIO
|
|
55
|
+
(drives iOS simulators, Android emulators and Chromium; generates/runs Maestro flows). Mobile E2E,
|
|
56
|
+
where Playwright has no reach.
|
|
54
57
|
- another adapter → its named MCP.
|
|
55
58
|
|
|
56
|
-
Record `provider` (the concrete MCP, e.g. `playwright-mcp` | `cypress-mcp` | `pytest-mcp`
|
|
59
|
+
Record `provider` (the concrete MCP, e.g. `playwright-mcp` | `cypress-mcp` | `pytest-mcp` |
|
|
60
|
+
`maestro-mcp`) and whether
|
|
57
61
|
it is available. **Auth is the local user's own** — the user's authenticated MCP session. The skill
|
|
58
62
|
**stores no tokens**; `project_url`/`suites` are plain references, never credentials.
|
|
59
63
|
|
|
@@ -15,6 +15,7 @@ Detection is best-effort against the user's own authenticated MCP session:
|
|
|
15
15
|
| `playwright` | a Playwright MCP | **generate** — author + run E2E/API specs against the app |
|
|
16
16
|
| `cypress` | the Cypress MCP | **generate** — author + run Cypress specs |
|
|
17
17
|
| `pytest` | a pytest MCP | **generate** — author + run service-layer tests |
|
|
18
|
+
| `maestro` | the Maestro MCP (`maestro mcp`, bundled in the CLI, STDIO) | **generate** — author + run Maestro flows on iOS/Android/Chromium |
|
|
18
19
|
| any | a read-only runner MCP | **link** — reference an existing suite and read results back |
|
|
19
20
|
| other | the adapter's named MCP | per that adapter |
|
|
20
21
|
|
|
@@ -34,6 +35,11 @@ the cases `test-cases.md` enumerates and the acceptance criteria the stories def
|
|
|
34
35
|
the code-maps from `yad-test-cases` Step 2b), one spec per high-priority (P0/P1) case, and runs them
|
|
35
36
|
via the MCP to confirm they execute.
|
|
36
37
|
- **Cypress / pytest** — the lens authors the equivalent specs in that framework's layout.
|
|
38
|
+
- **Maestro** — the lens authors `.yaml` flows under the repo's Maestro directory (one flow per
|
|
39
|
+
high-priority case), targeting the app id / screen elements the code-maps name rather than invented
|
|
40
|
+
ones, and runs them via the MCP against a simulator/emulator to confirm they execute. Mobile E2E is
|
|
41
|
+
the top of that pyramid: keep the flow count small and push detail down to the unit/integration
|
|
42
|
+
levels the repo already has.
|
|
37
43
|
|
|
38
44
|
Reuse what already exists: load the connected code repos' code-maps (`yad-test-cases` Step 2b) so
|
|
39
45
|
generated tests target real endpoints/components, not invented ones, and prefer the lowest useful test
|
|
@@ -14,8 +14,8 @@ root, not under any `epics/EP-<slug>/.sdlc/`.
|
|
|
14
14
|
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
17
|
-
"tool": "playwright", // playwright | cypress | pytest | <adapter id> | none
|
|
18
|
-
"provider": "playwright-mcp", // the concrete MCP: playwright-mcp | cypress-mcp | pytest-mcp | null
|
|
17
|
+
"tool": "playwright", // playwright | cypress | pytest | maestro | <adapter id> | none
|
|
18
|
+
"provider": "playwright-mcp", // the concrete MCP: playwright-mcp | cypress-mcp | pytest-mcp | maestro-mcp | null
|
|
19
19
|
"project_url": "tests/playwright.config.ts", // project/config reference; null if none yet
|
|
20
20
|
"auth": "user", // ALWAYS the user's own MCP session — never a token
|
|
21
21
|
"suites": { "backend": null, "mobile": null }, // optional default suite refs per repo
|
|
@@ -2382,9 +2382,9 @@
|
|
|
2382
2382
|
"license": "MIT"
|
|
2383
2383
|
},
|
|
2384
2384
|
"node_modules/js-yaml": {
|
|
2385
|
-
"version": "4.3.
|
|
2386
|
-
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.
|
|
2387
|
-
"integrity": "sha512-
|
|
2385
|
+
"version": "4.3.1",
|
|
2386
|
+
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
|
2387
|
+
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
|
2388
2388
|
"dev": true,
|
|
2389
2389
|
"funding": [
|
|
2390
2390
|
{
|
|
@@ -94,7 +94,8 @@ Read `{project-root}/.sdlc/testing.json` (`config.yaml` `testing.registry`). Dec
|
|
|
94
94
|
the Markdown artifact only and record `testing: none` in the frontmatter with a one-line note
|
|
95
95
|
(mirrors the `design: none` degrade). Skip to Step 4.
|
|
96
96
|
- **A tool is connected and its MCP is available:** adopt the `test architect` lens and, using the
|
|
97
|
-
provider recorded in `testing.json` (Playwright via a Playwright MCP, Cypress/pytest via
|
|
97
|
+
provider recorded in `testing.json` (Playwright via a Playwright MCP, Cypress/pytest/Maestro via
|
|
98
|
+
theirs — Maestro authors mobile flows where Playwright has no reach) drive
|
|
98
99
|
`bmad-testarch-automate`:
|
|
99
100
|
- **Generate** — when the provider is write-capable, author one automation test per high-priority
|
|
100
101
|
(P0/P1) case into the connected code repo(s) for the repos in `epic.repos`, reusing the code-maps
|
|
@@ -120,7 +121,7 @@ status: draft
|
|
|
120
121
|
owner: <inherit from epic.md owner> # the epic owner carries through; not retyped
|
|
121
122
|
repos: [<inherit from epic>]
|
|
122
123
|
code-context: { repos: [], loaded: <YYYY-MM-DD or none> } # code-maps that informed the tests (Step 2b)
|
|
123
|
-
testing: <none | { tool: <playwright|cypress|pytest|…>, direction: <generated|linked>, suite: <url/path>, tests: <N> }> # the connected testing tool (Step 3b)
|
|
124
|
+
testing: <none | { tool: <playwright|cypress|pytest|maestro|…>, direction: <generated|linked>, suite: <url/path>, tests: <N> }> # the connected testing tool (Step 3b)
|
|
124
125
|
---
|
|
125
126
|
|
|
126
127
|
## Test strategy & risk
|