yadflow 2.17.0 → 2.18.0
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 +2 -2
- package/README.md +36 -3
- package/bin/yad.mjs +24 -0
- package/cli/doctor.mjs +31 -1
- package/cli/epic-state.mjs +152 -0
- package/cli/manifest.mjs +6 -0
- package/cli/thread.mjs +174 -0
- package/package.json +5 -4
- package/skills/sdlc/config.yaml +46 -1
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +4 -0
- package/skills/yad-change/SKILL.md +174 -0
- package/skills/yad-change/references/triage.md +102 -0
- package/skills/yad-checks/SKILL.md +13 -1
- package/skills/yad-checks/references/check-gates.md +27 -0
- package/skills/yad-checks/templates/checks/epic-open.sh +98 -0
- package/skills/yad-checks/templates/checks/lineage-check.sh +97 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +105 -0
- package/skills/yad-checks/templates/github/yad-checks.yml +25 -0
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +20 -0
- package/skills/yad-defects/SKILL.md +79 -0
- package/skills/yad-epic/references/state-schema.md +106 -0
- package/skills/yad-reconcile/SKILL.md +75 -0
- package/skills/yad-timeline/SKILL.md +78 -0
|
@@ -30,3 +30,7 @@ SDLC Workflow,yad-connect-docs,Connect Docs Target,DX,"Setup/maintenance: connec
|
|
|
30
30
|
SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive documentation SPA (animated flow canvas + role-based stakeholder doc pages) from the epic's approved artifacts (epic, architecture, locked contract, ui-design, stories, code-context, test-cases), themed by the connected design system, into epics/EP-<slug>/docs-site/. Copies the vendored React/Vite/Tailwind shell verbatim and generates src/data/*.ts deterministically; drives the yad docs build/deploy CLI to publish to Pages (or build-only). An OUTPUT ENRICHMENT — never a gate; never mutates state.json, approvals, or the contract lock.",,{epic: EP-<slug>} {action: generate|refresh|deploy} {login_gate: true|false},,yad-review-gate,,false,epics/EP-<slug>/docs-site/,docs-site/ docs-build.json
|
|
31
31
|
SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
|
|
32
32
|
SDLC Workflow,yad-docs-sync,Docs Sync,DY,"Maintenance/CI: keep the generated doc sites fresh. Detect staleness (a content hash of the approved artifacts + the connected repos' HEAD shas + the doc-shell version vs each site's build manifest), report which sites drifted and why, regenerate + redeploy the stale ones, and wire a CI job that rebuilds on push (carrying [skip ci] + a concurrency group to prevent deploy loops). Generalizes the rule that feature work must hand-update docs/index.html + diagrams + skill counts. Refresh is always a human/CI decision; never a gate.",,{action: check|refresh|wire} {epic: EP-<slug>},,,,false,epics/EP-<slug>/.sdlc/,docs-build.json yad-docs.yml
|
|
33
|
+
SDLC Workflow,yad-change,Change/Defect Intake,CH,"Phase 6 post-lock change management: the INTAKE + TRIAGE step of a feature thread. Classifies the change DEPTH (defect-fix / behavioral-no-surface / contract-surface / new-capability), seeds a NEW EP-<slug> change-epic threaded to its parent (lineage frontmatter kind/parent/thread/inherits/supersedes + a state.json whose inherited steps are pre-marked done and only the changed steps run; a pointer-lock contract-lock.json when architecture is inherited), and records the intake in change.json (escape_stage + root_cause for defects). For hotfixes it records the ship-first exception and opens reconcile-debt.json. Never auto-advances — hands off to the normal authoring skills + yad-review-gate.",,{parent: EP-<slug>} {title: one-line} {kind: change|defect|hotfix} {origin: production|staging|qa|review} {severity: sev1..sev4} {description: text} {affected: artifacts},1-front,,yad-review-gate,false,epics/EP-<slug>/,epic.md state.json change.json reconcile-debt.json contract-lock.json
|
|
34
|
+
SDLC Workflow,yad-timeline,Feature Timeline,TL,"Render a feature THREAD (its linked epics, genesis->changes->defects) as an evolution view (the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary) AND resolve the inheritance chain into the authoritative current artifact set (thread-resolved.md: the winning source per artifact + the resolved contract-lock hash) — the composed source-of-truth AI/humans read for the next change. Reads frontmatter lineage + each change.json + build-log.json. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<genesis>/,timeline-site/ thread-resolved.md TIMELINE.md
|
|
35
|
+
SDLC Workflow,yad-defects,Quality-Gap Report,DF,"Generate a per-epic AND per-thread defect/bug report (same vendored shell + DEFECTS.md). Walks the thread for every kind:defect change-epic + each change.json defect block + shipped regressions in build-log.json, aggregates by escape_stage (the SDLC gate that should have caught it) and root_cause, and visualizes WHERE quality gaps systematically come from (e.g. % of thread defects that escaped at the test-cases gate) so the team can harden the originating stage. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{epic: EP-<slug> | thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<slug>/,defects-site/ DEFECTS.md
|
|
36
|
+
SDLC Workflow,yad-reconcile,Change Reconciler,RE,"Maintenance/CI (mirrors yad-docs-sync — never a gate): detect post-lock DRIFT/ORPHANS — shipped code or a repo HEAD advance (the repos.json syncedHead-vs-current-HEAD rule) with NO owning change-epic in any thread — plus open hotfix reconcile debt, and report which thread drifted and why. refresh scaffolds a reconcile change-epic stub (hands to yad-change) — never silent; wire commits advisory CI ([skip ci] + concurrency, like yad-docs-sync). The actual merge BLOCK is the lineage-check / reconcile-debt gates; this only discovers.",,{action: check|refresh|wire} {thread: EP-<genesis>},,,,false,epics/EP-<genesis>/.sdlc/,(report) reconcile-debt.json yad-reconcile.yml
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yad-change
|
|
3
|
+
description: 'Phase 6 post-lock change management — the change-request/defect INTAKE + TRIAGE step of a feature thread. After the contract locks and code ships, a change must NOT mutate a locked artifact; it becomes a NEW epic threaded to its parent. This skill classifies the change DEPTH (defect-fix / behavioral-no-surface / contract-surface / new-capability), seeds a new EP-<slug> change-epic threaded to its parent (lineage frontmatter kind/parent/thread/inherits/supersedes + a state.json whose inherited steps are pre-marked done and only the changed steps run; a pointer-lock contract-lock.json when architecture is inherited), and records the intake in change.json (escape_stage + root_cause for defects). For hotfixes it records the ship-first exception and opens reconcile-debt.json. Never auto-advances — hands off to the normal authoring skills + the team review gate. Use when the user says "log a change request", "file a defect", "thread a change off EP-…", "open a hotfix", or after a shipped feature needs a fix.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SDLC — Change/Defect Intake + Triage (Phase 6, the entry of a feature thread)
|
|
7
|
+
|
|
8
|
+
**Goal:** Turn a post-lock change request, defect, or hotfix into a **new epic threaded to its parent**,
|
|
9
|
+
so the feature's locked artifacts are never mutated — only *superseded*. The change-epic **inherits**
|
|
10
|
+
the front artifacts it does not change (by reference) and **re-authors** only the ones it does, so the
|
|
11
|
+
thread head always describes current behaviour and the SDLC stays a trusted source of truth for the next
|
|
12
|
+
change. This skill does the **intake + triage + seeding** and then hands off to the normal authoring
|
|
13
|
+
skills + `yad-review-gate`. It is a **front state**: human-confirmed, **never auto-advances**.
|
|
14
|
+
|
|
15
|
+
This is the answer to "the front/spec docs go stale after the contract locks": a behavioural change can
|
|
16
|
+
no longer ship through the build half against an old story — `epic-open` seals a fully-shipped epic, so
|
|
17
|
+
new behaviour must enter here, and its re-authored stories/test-cases describe the change.
|
|
18
|
+
|
|
19
|
+
## Conventions
|
|
20
|
+
|
|
21
|
+
- `{project-root}` resolves from the product hub.
|
|
22
|
+
- Artifacts live under `{project-root}/epics/EP-<slug>/` — the change-epic gets its OWN `EP-<slug>`
|
|
23
|
+
(assigned here, never renamed) and its own `stories/EP-<slug>-S0N`, so every existing gate, the bridge,
|
|
24
|
+
`yad next`, and the build-half traceability keep working unchanged.
|
|
25
|
+
- The thread is **derived** from `parent:` frontmatter (no registry); `thread:` is a cache that must
|
|
26
|
+
equal the computed root (`yad doctor` flags a mismatch). Thread id = the genesis epic's id.
|
|
27
|
+
- Lineage frontmatter, the inherited-step shape, the pointer-lock, `change.json`, and
|
|
28
|
+
`reconcile-debt.json` are all defined in `../yad-epic/references/state-schema.md` (Phase 6 section).
|
|
29
|
+
- Genesis epics authored before Phase 6 must be **migrated once** (`kind: feature`, `thread: <self>` in
|
|
30
|
+
their `epic.md`) before a change threads off them — see `references/triage.md`.
|
|
31
|
+
- Speak in the configured `communication_language`; write documents in `document_output_language`.
|
|
32
|
+
|
|
33
|
+
## Inputs
|
|
34
|
+
|
|
35
|
+
- `parent` — **required.** The `EP-<slug>` this change evolves (the thread predecessor; usually the
|
|
36
|
+
feature's current tip).
|
|
37
|
+
- `title` — **required.** One line describing the change.
|
|
38
|
+
- `kind` — `change` | `defect` | `hotfix` (default `change`). `feature` is reserved for a genesis epic
|
|
39
|
+
(use `yad-epic`, not this skill).
|
|
40
|
+
- `origin` — defect/hotfix only: `production` | `staging` | `qa` | `review`.
|
|
41
|
+
- `severity` — defect/hotfix only: `sev1`..`sev4`.
|
|
42
|
+
- `escape_stage` — defect/hotfix only: the SDLC gate that *should* have caught it (`stories`,
|
|
43
|
+
`test-cases`, `architecture`, …). Feeds the `yad-defects` quality report.
|
|
44
|
+
- `root_cause` — defect/hotfix only: a short tag (e.g. `missing-negative-test`).
|
|
45
|
+
- `description` — free text: what is wrong / what must change.
|
|
46
|
+
- `affected` — the artifacts the requester believes change (the triage confirms/adjusts this).
|
|
47
|
+
|
|
48
|
+
## On Activation
|
|
49
|
+
|
|
50
|
+
### Step 1 — Resolve the parent + thread (validate; STOP on a broken lineage)
|
|
51
|
+
Confirm `parent` exists (`epics/<parent>/epic.md` + `.sdlc/state.json`). Read its lineage and resolve
|
|
52
|
+
the thread root (`yad thread <parent>` / `resolveThread`). **STOP** if the parent is missing, or its
|
|
53
|
+
lineage is broken (a cycle, or a `thread` cache ≠ the computed root) — fix the parent first. If the
|
|
54
|
+
parent is a **genesis epic not yet migrated** (no `kind:`), migrate it now: add `kind: feature` and
|
|
55
|
+
`thread: <its own id>` to its `epic.md` (a one-line, non-gated frontmatter add).
|
|
56
|
+
|
|
57
|
+
The new epic's `thread` = the parent's thread (the genesis id). Its `parent` = the given `parent` (the
|
|
58
|
+
immediate predecessor — usually the current tip; if the parent is not the tip, see "concurrent changes"
|
|
59
|
+
in `references/triage.md`).
|
|
60
|
+
|
|
61
|
+
### Step 2 — Gather the change + triage the DEPTH (auto-propose, human-confirm)
|
|
62
|
+
With the requester, classify the change into one **depth** (the `yad-backfill` discipline: auto-propose,
|
|
63
|
+
human-confirm). The depth decides which front states are **re-authored** vs **inherited**:
|
|
64
|
+
|
|
65
|
+
| depth | re-authors (active) | inherits (pre-done, by reference) | first step |
|
|
66
|
+
|-------|---------------------|-----------------------------------|-----------|
|
|
67
|
+
| **defect-fix** — the spec was right, code/coverage was wrong | `stories` (a regression story), `test-cases` (the missing case) | epic, architecture, contract, ui-design | `stories` |
|
|
68
|
+
| **behavioral-no-surface** — behaviour changes, contract surface unchanged | epic (delta), `stories`, `test-cases` (+ ui-design if visible) | architecture, **contract (no re-lock)** | `stories` (or `ui-design`) |
|
|
69
|
+
| **contract-surface** — the shared cross-repo surface changes | **architecture + contract (RE-LOCK)**, `stories`, `test-cases` (+ epic/ui as needed) | whatever is genuinely untouched | `architecture` |
|
|
70
|
+
| **new-capability** — not a change to this feature, a new one | full chain (`epic`…`test-cases`) | lineage/context only | `epic` |
|
|
71
|
+
|
|
72
|
+
Print the chosen depth and the **re-author vs inherit** split; **get explicit confirmation** before
|
|
73
|
+
seeding. A `new-capability` is usually a *new genesis epic* (`yad-epic`) — only thread it when it truly
|
|
74
|
+
extends this feature's evolution.
|
|
75
|
+
|
|
76
|
+
### Step 3 — Derive the change-epic id + open the authoring branch
|
|
77
|
+
Derive a distinct `EP-<slug>` from the title (2–4 lowercase words; check `epics/` for collisions, append
|
|
78
|
+
a word if needed). Create `{project-root}/epics/EP-<slug>/`. Open the `change/EP-<slug>` authoring branch
|
|
79
|
+
per the shared "Authoring branches" procedure (git/greenfield-safe).
|
|
80
|
+
|
|
81
|
+
### Step 4 — Write `epic.md` (the change brief + lineage frontmatter)
|
|
82
|
+
Write a thin brief carrying the lineage frontmatter:
|
|
83
|
+
|
|
84
|
+
```markdown
|
|
85
|
+
---
|
|
86
|
+
id: EP-<slug>
|
|
87
|
+
status: draft
|
|
88
|
+
kind: <change|defect|hotfix>
|
|
89
|
+
parent: <EP-parent>
|
|
90
|
+
thread: <EP-genesis>
|
|
91
|
+
inherits: [<the inherited bases>]
|
|
92
|
+
supersedes: [<parent story ids this replaces, optional>]
|
|
93
|
+
owner:
|
|
94
|
+
repos: [<inherit from the resolved current truth / parent>]
|
|
95
|
+
# defect/hotfix only:
|
|
96
|
+
origin: <…>
|
|
97
|
+
severity: <sevN>
|
|
98
|
+
escape_stage: <stage>
|
|
99
|
+
root_cause: <tag>
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Change
|
|
103
|
+
<!-- what is wrong / what must change, and why now -->
|
|
104
|
+
|
|
105
|
+
## Resolved current truth (input)
|
|
106
|
+
<!-- run `yad thread <parent>`: which epic currently owns each artifact this change builds on -->
|
|
107
|
+
|
|
108
|
+
## Re-authored vs inherited
|
|
109
|
+
<!-- the Step 2 split, for the reviewers -->
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For a **contract-surface** depth, do NOT inherit `architecture` — it will be re-authored (and re-locked)
|
|
113
|
+
by `yad-architecture` downstream.
|
|
114
|
+
|
|
115
|
+
### Step 5 — Seed `state.json` (inherited steps pre-done; only the changed steps run)
|
|
116
|
+
Create `.sdlc/state.json` with the **same 10-step chain** as `yad-epic` (so `advanceState`/`nextAction`/
|
|
117
|
+
`gatePredicate`/the bridge run unchanged), but:
|
|
118
|
+
- **Inherited** authoring steps **and their review gates**: `status: "done"`, `"inherited": true`,
|
|
119
|
+
`"inheritedFrom": "<owning epic from the resolved truth>"`, `"boundHash": "<that artifact's current
|
|
120
|
+
hash>"`.
|
|
121
|
+
- The **first re-authored** authoring step: `status: "in_progress"`; its review: `status: "in_review"`
|
|
122
|
+
only once authored — seed it `blocked` and let the authoring skill open it. Set `currentStep` to the
|
|
123
|
+
first re-authored authoring step.
|
|
124
|
+
- Remaining re-authored steps: `blocked`.
|
|
125
|
+
|
|
126
|
+
Seed `.sdlc/approvals.json` with one **provenance** record per inherited gate (NOT a forged approval):
|
|
127
|
+
`{ "artifact": "<art>", "step": "<…-review>", "status": "inherited", "from": "<epic>", "boundHash": "<hash>", "date": "<today>" }`.
|
|
128
|
+
Seed `.sdlc/comments.json` = `[]` and create `reviews/`.
|
|
129
|
+
|
|
130
|
+
When `architecture` is **inherited**, materialize the **pointer-lock** `.sdlc/contract-lock.json`:
|
|
131
|
+
`{ "artifact": "contract.md", "hash": "<parent surface hash, verbatim>", "lockedAt": "<today>", "inheritedFrom": "<epic>", "ref": "../../<epic>/.sdlc/contract-lock.json" }`.
|
|
132
|
+
There is no `contract.md` in the change-epic, so the surface cannot drift, and `contract-check` passes
|
|
133
|
+
unchanged because the hash is identical. (Exact recipe + field shapes: `references/triage.md`.)
|
|
134
|
+
|
|
135
|
+
### Step 6 — Write `.sdlc/change.json` (intake + triage record)
|
|
136
|
+
Record the intake: `epicId`, `thread`, `parent`, `kind`, `depth`, `intakeBy`, `intakeDate`, `title`,
|
|
137
|
+
`description`, `affectedArtifacts`, `reauthors`, `inherits`, and for a defect/hotfix the `defect` block
|
|
138
|
+
(`origin`, `severity`, `escape_stage`, `root_cause`). This is what `yad-defects` reads to attribute the
|
|
139
|
+
defect to the gate that should have caught it.
|
|
140
|
+
|
|
141
|
+
### Step 7 — Hotfix only: record the ship-first exception + open reconcile debt
|
|
142
|
+
If `kind: hotfix`, the build half MAY run before these front gates approve (severity demands it). Record
|
|
143
|
+
`hotfix: { "shipFirst": true }` in `change.json` and **append** to `.sdlc/reconcile-debt.json`:
|
|
144
|
+
`{ "thread": "<…>", "epicId": "<…>", "openedDate": "<today>", "reason": "<why>", "requires": ["artifacts-updated","regression-test"], "status": "open", "paidDate": null, "paidBy": null, "evidence": { "artifacts": [], "regressionTest": "" } }`.
|
|
145
|
+
Tell the user the debt **freezes the next normal change** on this thread (`reconcile-debt` gate) until it
|
|
146
|
+
is paid (front artifacts updated **and** a regression test added, then `status: "paid"`).
|
|
147
|
+
|
|
148
|
+
### Step 8 — Stop; hand off (NO auto-advance)
|
|
149
|
+
Report: the new `EP-<slug>`, its thread + parent, the re-author-vs-inherit split, the seeded
|
|
150
|
+
`currentStep`, and the next skill — `yad-architecture` (contract-surface), else `yad-stories` /
|
|
151
|
+
`yad-test-cases` — followed by `yad-review-gate`. Front states do not auto-advance. Suggest
|
|
152
|
+
`yad thread <thread>` to see the evolution and `yad-timeline` / `yad-defects` to render it.
|
|
153
|
+
|
|
154
|
+
## Hard rules
|
|
155
|
+
|
|
156
|
+
- **Never mutate a locked artifact.** A change is a new threaded epic, not an edit to a shipped one.
|
|
157
|
+
- **A change MUST thread to a real parent.** Validate the parent + thread first; STOP on a broken lineage.
|
|
158
|
+
- **Inherit by reference, never copy.** Inherited steps are pre-done with `inherited: true` + a
|
|
159
|
+
`boundHash`; the pointer-lock carries the parent hash verbatim. The gate never re-reviews them.
|
|
160
|
+
- **Contract-surface ⇒ re-author architecture.** Omitting `architecture` from `inherits` is the ONLY way
|
|
161
|
+
to change the surface; it re-locks (new hash) and routes through the escalated architecture review —
|
|
162
|
+
the same mechanism as the build-half `Contract-Change` route, unified.
|
|
163
|
+
- **A hotfix opens debt, never waives it.** Ship-first is allowed once; the thread freezes for new work
|
|
164
|
+
until the debt is paid.
|
|
165
|
+
- **Never auto-advances.** This skill seeds + records; humans author and approve via the normal gates.
|
|
166
|
+
|
|
167
|
+
## Reference
|
|
168
|
+
- Depth triage details, the exact seeding shape, the pointer-lock recipe, genesis migration, and the
|
|
169
|
+
concurrent-change (re-parent) rule: `references/triage.md`.
|
|
170
|
+
- The lineage frontmatter + ledger schemas: `../yad-epic/references/state-schema.md` (Phase 6).
|
|
171
|
+
- The authoring skills this hands off to: `../yad-architecture/`, `../yad-stories/`, `../yad-test-cases/`,
|
|
172
|
+
and the gate `../yad-review-gate/`.
|
|
173
|
+
- The thread view + reports: `yad thread`, `../yad-timeline/`, `../yad-defects/`.
|
|
174
|
+
- The gates that enforce it: `../yad-checks/` (lineage-check, epic-open, reconcile-debt).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Change triage — depth, seeding, the pointer-lock, migration, concurrency
|
|
2
|
+
|
|
3
|
+
This is the detail behind `yad-change`. The lineage frontmatter + ledger schemas live in
|
|
4
|
+
`../../yad-epic/references/state-schema.md` (Phase 6 section); this file is the *how*.
|
|
5
|
+
|
|
6
|
+
## Depth → re-author vs inherit (the triage table, expanded)
|
|
7
|
+
|
|
8
|
+
The depth is the single decision that drives everything else. Pick the SHALLOWEST depth that honestly
|
|
9
|
+
describes the change — a deeper depth re-authors (and re-reviews) more than necessary.
|
|
10
|
+
|
|
11
|
+
| depth | when | re-authors | inherits | first runnable step |
|
|
12
|
+
|-------|------|-----------|----------|---------------------|
|
|
13
|
+
| **defect-fix** | the design was right; the code or its coverage was wrong. No behaviour the spec promised changes. | `stories` (a regression story stating the correct behaviour), `test-cases` (the case that would have caught it) | epic, architecture, contract, ui-design | `stories` |
|
|
14
|
+
| **behavioral-no-surface** | observable behaviour changes (validation, an edge case, a non-surface field), but the **cross-repo contract surface** does not | `epic` (a delta), `stories`, `test-cases`; `ui-design` too if the change is visible | architecture, **contract (inherited, NO re-lock)** | `stories` (or `ui-design`) |
|
|
15
|
+
| **contract-surface** | the shared API/event/data-model surface itself changes | **architecture + contract (re-author + RE-LOCK)**, `stories`, `test-cases`; `epic`/`ui-design` as needed | only what is genuinely untouched | `architecture` |
|
|
16
|
+
| **new-capability** | this is not a change to the feature — it is a new feature | the full chain | lineage/context only | `epic` |
|
|
17
|
+
|
|
18
|
+
**Heuristics for the surface question** (defect-fix / behavioral-no-surface vs contract-surface): does
|
|
19
|
+
the change alter an endpoint's path/method, a request/response field, an event name/payload, or a shared
|
|
20
|
+
enum/identifier in the genesis (or current-truth) `contract.md` `CONTRACT-SURFACE` block? If yes →
|
|
21
|
+
contract-surface. If it only changes internal behaviour, validation, or a repo-private field → not the
|
|
22
|
+
surface.
|
|
23
|
+
|
|
24
|
+
## The seeded `state.json` (worked shape)
|
|
25
|
+
|
|
26
|
+
Same 10 steps as `yad-epic`. For a **defect-fix** that inherits epic/architecture/ui-design and
|
|
27
|
+
re-authors stories+test-cases:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"epicId": "EP-<slug>", "createdAt": "<today>", "currentStep": "stories",
|
|
32
|
+
"steps": [
|
|
33
|
+
{ "id": "epic", "type": "author", "artifact": "epic.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
|
|
34
|
+
{ "id": "epic-review", "type": "review+approve", "artifact": "epic.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
|
|
35
|
+
{ "id": "architecture", "type": "author", "artifact": "architecture.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
|
|
36
|
+
{ "id": "architecture-review", "type": "review+approve", "artifact": "architecture.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": ["contract"] },
|
|
37
|
+
{ "id": "ui-design", "type": "author", "artifact": "ui-design.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
|
|
38
|
+
{ "id": "ui-design-review", "type": "review+approve", "artifact": "ui-design.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
|
|
39
|
+
{ "id": "stories", "type": "author", "artifact": "stories/", "assistance": "review", "automation": "human_approve", "locked": true, "status": "in_progress", "risk_tags": [] },
|
|
40
|
+
{ "id": "stories-review", "type": "review+approve", "artifact": "stories/", "assistance": "review", "automation": "human_approve", "locked": true, "status": "blocked", "risk_tags": [] },
|
|
41
|
+
{ "id": "test-cases", "type": "author", "artifact": "test-cases.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "blocked", "risk_tags": [] },
|
|
42
|
+
{ "id": "test-cases-review", "type": "review+approve", "artifact": "test-cases.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "blocked", "risk_tags": [] }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`boundHash` is the inherited artifact's **current** hash from the owning epic — the same hashes
|
|
48
|
+
`cli/epic-state.mjs` computes: `contractSurfaceHash` for `architecture`, `storiesHash` for `stories`,
|
|
49
|
+
the file bytes otherwise. The gate predicate treats an `inherited` step as satisfied (never re-reviewed)
|
|
50
|
+
as long as `boundHash` matches the thread's current hash for that artifact — which it always does,
|
|
51
|
+
because the artifact lives in the parent and cannot be edited from the child.
|
|
52
|
+
|
|
53
|
+
`approvals.json` provenance record per inherited gate (append-only, NOT an approval that the predicate
|
|
54
|
+
counts — it just documents where the sign-off lives):
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "artifact": "architecture.md", "step": "architecture-review", "status": "inherited",
|
|
58
|
+
"from": "EP-<genesis>", "boundHash": "sha256:…", "date": "<today>" }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## The pointer-lock (when `architecture` is inherited)
|
|
62
|
+
|
|
63
|
+
Write `.sdlc/contract-lock.json` with the parent's hash **verbatim**:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{ "artifact": "contract.md", "hash": "sha256:<parent hash, copied byte-for-byte>", "lockedAt": "<today>",
|
|
67
|
+
"inheritedFrom": "EP-<genesis>", "ref": "../../EP-<genesis>/.sdlc/contract-lock.json" }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Get the parent hash from the owning epic's `.sdlc/contract-lock.json` `hash` field (do NOT recompute — copy
|
|
71
|
+
it). `contract-check.sh` reads only `hash`, so a build-half story in the change-epic pins this identical
|
|
72
|
+
hash via its `link.md` and the gate passes unchanged. There is no `contract.md` in the change-epic, so
|
|
73
|
+
the surface physically cannot drift.
|
|
74
|
+
|
|
75
|
+
**To CHANGE the surface instead:** do not inherit `architecture`. Then `yad-architecture` re-authors
|
|
76
|
+
`contract.md` in the change-epic between fresh `CONTRACT-SURFACE` markers, computes a **new** hash, and
|
|
77
|
+
writes a real (non-pointer) `contract-lock.json`. `architecture-review` carries `risk_tags: ["contract"]`
|
|
78
|
+
→ the escalated domain-owner review. This is the same re-lock-invalidates-approvals behaviour the front
|
|
79
|
+
half already has, relocated from "edit the locked file" to "author a contract-surface change-epic".
|
|
80
|
+
|
|
81
|
+
## Genesis migration (one-time, per feature)
|
|
82
|
+
|
|
83
|
+
A feature epic authored before Phase 6 has no lineage frontmatter. Before a change threads off it, add
|
|
84
|
+
to its `epic.md` frontmatter:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
kind: feature
|
|
88
|
+
thread: <its own id>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is a non-gated, idempotent frontmatter add (no `parent`, since a genesis epic is the thread root).
|
|
92
|
+
`epicLineage` already defaults an absent `kind` to `feature`, so an un-migrated genesis still behaves as
|
|
93
|
+
a root — migration just makes the `thread` cache explicit and lets `yad thread` group it.
|
|
94
|
+
|
|
95
|
+
## Concurrent changes on one feature (forward-only resolution)
|
|
96
|
+
|
|
97
|
+
Two change-epics threaded off the same tip that re-author the same artifact are a **fork**: the resolver
|
|
98
|
+
sees two non-inherited owners of one base at the same depth, and `yad doctor` / `yad reconcile` warn.
|
|
99
|
+
Resolution is forward-only — the second to merge **re-parents** onto the first (set `parent` to the new
|
|
100
|
+
tip) and re-inherits; no artifact is mutated, no lock conflicts. The contract is the natural
|
|
101
|
+
serialization point: only one re-lock can win, and `contract-check`'s pinned-hash fidelity check fails
|
|
102
|
+
the loser until it re-specs against the new tip.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yad-checks
|
|
3
|
-
description: 'Build-half Step C of the gated SDLC — the production-safety check gates. Wire and run the CI gates on a code repo: spec-link (every change links a real story/spec via its Task trailer), contract-check (a diff that changes the contract surface without a Contract-Change + an updated, re-locked contract FAILS and routes back to the architecture gate), build/test/lint,
|
|
3
|
+
description: 'Build-half Step C of the gated SDLC — the production-safety check gates. Wire and run the CI gates on a code repo: spec-link (every change links a real story/spec via its Task trailer), contract-check (a diff that changes the contract surface without a Contract-Change + an updated, re-locked contract FAILS and routes back to the architecture gate), build/test/lint, verified-commits (no unverified commits from unverified users — platform-Verified signature + roster-allowlisted author, on the hub and every repo), and the Phase 6 feature-thread gates lineage-check / epic-open / reconcile-debt (a change links a real threaded epic; a sealed epic refuses new behaviour; a thread with open hotfix debt is frozen until paid). The gates are CI-agnostic bash, invoked by GitHub Actions and GitLab CI. Use when the user says "wire the check gates", "run the gates", "require signed commits", or "set up CI checks" for a repo.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SDLC — Check Gates (build-half Step C)
|
|
@@ -24,6 +24,18 @@ in CI on every PR/MR and must pass before merge (build plan §C). Each is a smal
|
|
|
24
24
|
**product hub and every connected repo**; runs on PRs/MRs only, so the gate-sync bot's direct
|
|
25
25
|
ledger pushes are unaffected (never replace it with a default-branch push rule — see
|
|
26
26
|
`references/check-gates.md` §4).
|
|
27
|
+
5. **lineage-check** (Phase 6) — the change's owning epic is a valid node in a **feature thread**: a
|
|
28
|
+
`change`/`defect`/`hotfix` epic must thread to a real `parent`. Builds on spec-link's story→epic
|
|
29
|
+
resolution; the every-code-change-has-a-threaded-epic enforcement.
|
|
30
|
+
6. **epic-open** (Phase 6, the staleness preventer) — an epic is **sealed** once every story is
|
|
31
|
+
`shipped`; a commit targeting a sealed epic **FAILS**, forcing new behaviour into a new threaded
|
|
32
|
+
change-epic (so the front artifacts can never go stale).
|
|
33
|
+
7. **reconcile-debt** (Phase 6) — a hotfix that shipped first opens debt; the **next** change on its
|
|
34
|
+
thread **FAILS** until the debt is paid (artifacts updated + a regression test added).
|
|
35
|
+
|
|
36
|
+
The Phase 6 gates read the owning epic in the **product hub** via `specs/<story>/link.md`'s
|
|
37
|
+
`product-repo` path (like contract-check), and degrade to a PASS-with-note when the hub is not reachable
|
|
38
|
+
from CI. See `references/check-gates.md` and `skills/yad-change`.
|
|
27
39
|
|
|
28
40
|
The gates are **CI-agnostic bash** in `checks/`; thin pipeline configs invoke them on GitHub Actions
|
|
29
41
|
and GitLab CI. This step is **by hand** in Phase 3 — run the gates with the skill or let CI run them;
|
|
@@ -11,6 +11,9 @@ repo uses. Each reads conventions established by earlier steps — it invents no
|
|
|
11
11
|
| spec-link | the `Task: <story>-<task>` commit trailer; `specs/<story>/link.md` | `yad-implement` (trailer), `yad-spec` (link.md) |
|
|
12
12
|
| contract-check | changed files under `specs/<story>/contracts/`; the `Contract-Change: yes` trailer; `link.md`'s pinned `contract-lock`; the product repo's `contract-lock.json` | `yad-architecture` (lock), `yad-spec` (slice + link), `yad-implement` (trailer) |
|
|
13
13
|
| build/test/lint | the repo's `npm run lint` / `npm run build` / `npm test` | the repo |
|
|
14
|
+
| lineage-check | the `Task:` trailer → `link.md` (`epic` + `product-repo`); the owning epic's `kind`/`parent` frontmatter in the hub | `yad-spec` (link.md), `yad-change` (lineage frontmatter) |
|
|
15
|
+
| epic-open | the `Task:` trailer → `link.md` → the hub epic's `stories/*.md` `status:` (sealed = all `shipped`) | `yad-engineer-review` (story status), `yad-change` (the change-epic) |
|
|
16
|
+
| reconcile-debt | the `Task:` trailer → `link.md` → the hub epic's `thread`; every thread epic's `reconcile-debt.json` | `yad-change` (opens hotfix debt) |
|
|
14
17
|
| verified-commits | each commit's platform signature-verification status; the author email vs `.sdlc/verified-authors` | hub roster `email` fields (`yad check --fix` generates the allowlist) |
|
|
15
18
|
| commit-message | each non-merge commit's subject + trailer block | `yad-commit` / `CONTRIBUTING.md` (`config.yaml build.commit_subject_style`) |
|
|
16
19
|
| pr-title | the PR/MR title (from the CI event payload) | `yad-pr-template` (`config.yaml build.pr_title_style`) |
|
|
@@ -149,6 +152,28 @@ catches a free-form description that bypassed it:
|
|
|
149
152
|
(`epics/**`, detected from the CI-supplied `--changed <file>` list) **FAILS** — artifact changes
|
|
150
153
|
must go through a `review/EP-*` PR.
|
|
151
154
|
|
|
155
|
+
## 8. Phase 6 — feature-thread gates (`lineage-check.sh`, `epic-open.sh`, `reconcile-debt-check.sh`)
|
|
156
|
+
|
|
157
|
+
After the contract locks and code ships, a change must not mutate a locked artifact — it becomes a new
|
|
158
|
+
epic threaded to its parent (`config.yaml` `change:`). These three gates keep that discipline. All three
|
|
159
|
+
resolve the owning epic the same way: `Task:` trailer → `specs/<story>/link.md` (`epic` + `product-repo`)
|
|
160
|
+
→ the hub epic. All **fail closed** on an unresolvable base; all are **per commit**; `ci|chore|build|test`
|
|
161
|
+
commits are exempt. When the **product hub is not reachable** from CI (the usual case for a code-repo
|
|
162
|
+
PR), each degrades to a **PASS-with-note** — the hub-side check (`yad doctor` / `yad reconcile`) covers
|
|
163
|
+
that path, and spec-link still proves the story link.
|
|
164
|
+
|
|
165
|
+
- **lineage-check** — reads the hub epic's `kind`/`parent` frontmatter. A `feature` (genesis) epic
|
|
166
|
+
passes. A `change`/`defect`/`hotfix` epic **FAILS** unless it declares a `parent:` that resolves to a
|
|
167
|
+
real `epics/<parent>/` in the hub (no orphan threads). This is the "every code change has an owning
|
|
168
|
+
epic in a thread" enforcement, layered on spec-link.
|
|
169
|
+
- **epic-open** — an epic is **sealed** iff it has ≥1 story and **every** `stories/*.md` `status:` is
|
|
170
|
+
`shipped`. A commit whose owning epic is sealed **FAILS**: new behaviour cannot mutate a shipped epic;
|
|
171
|
+
it must land in a new threaded change-epic. This is what stops the front artifacts from going stale.
|
|
172
|
+
- **reconcile-debt** — resolves the epic's `thread` (its `thread:` frontmatter, else the epic id) and
|
|
173
|
+
scans every thread epic's `reconcile-debt.json`. An **open** entry the current epic does not own
|
|
174
|
+
**FAILS** the change (the thread is frozen until the hotfix debt is paid: artifacts updated + a
|
|
175
|
+
regression test added, then `status: paid`). Thread-scoped — only the affected thread freezes.
|
|
176
|
+
|
|
152
177
|
## CI wiring (both platforms)
|
|
153
178
|
|
|
154
179
|
The gates run identically under either CI; the config just invokes the scripts with the PR/MR base.
|
|
@@ -158,6 +183,8 @@ The gates run identically under either CI; the config just invokes the scripts w
|
|
|
158
183
|
(verified-commits also gets a read-only `GH_TOKEN` for the Verified-badge lookup). The pattern jobs
|
|
159
184
|
read the title/body from the event payload: `pr-title` takes `${{ github.event.pull_request.title }}`
|
|
160
185
|
and `pr-template` writes `${{ github.event.pull_request.body }}` to a temp file. All `--profile code`.
|
|
186
|
+
The Phase 6 thread gates (`lineage-check`, `epic-open`, `reconcile-debt`) run as their own jobs with
|
|
187
|
+
`fetch-depth: 0`, the same `origin/${{ github.base_ref }}` base.
|
|
161
188
|
- **GitLab CI** — `templates/gitlab/yad-checks.gitlab-ci.yml` → `.gitlab/ci/yad-checks.yml`, pulled in
|
|
162
189
|
by the root `.gitlab-ci.yml`'s `include:`. The jobs run on `merge_request_event` with `GIT_DEPTH: 0`,
|
|
163
190
|
passing `origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`; the pattern jobs read `$CI_MERGE_REQUEST_TITLE`
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# epic-open gate (Phase 6 — the staleness preventer). An epic is SEALED once every one of its stories
|
|
3
|
+
# is `shipped`. A SEALED epic's artifacts are the final, approved description of shipped behaviour — so
|
|
4
|
+
# new behaviour must NOT be added to it; it belongs in a NEW threaded change-epic whose re-authored
|
|
5
|
+
# stories/test-cases describe the change. This gate FAILs any non-maintenance commit whose owning epic
|
|
6
|
+
# is sealed, forcing the front half to stay current (staleness becomes unshippable).
|
|
7
|
+
#
|
|
8
|
+
# The owning epic lives in the PRODUCT repo (via specs/<story>/link.md `product-repo`). When it is not
|
|
9
|
+
# reachable from CI, the seal cannot be read, so the commit PASSes with a note (degraded, fail-open here
|
|
10
|
+
# because lineage/spec-link still gate the link itself). Per commit; ci/chore/build/test exempt.
|
|
11
|
+
# Fails CLOSED on an unresolvable base.
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
BASE="${1:-${SDLC_BASE:-origin/main}}"
|
|
15
|
+
|
|
16
|
+
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
17
|
+
echo "FAIL [epic-open]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
RANGE="${BASE}..HEAD"
|
|
21
|
+
EXEMPT='ci|chore|build|test'
|
|
22
|
+
|
|
23
|
+
# Read one frontmatter value from the FIRST --- … --- block only (awk stops at the first closing fence).
|
|
24
|
+
fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f && index($0, k":")==1 {sub("^" k ":[ \t]*", ""); print; exit}' "$2" 2>/dev/null | tr -d '\r'; }
|
|
25
|
+
|
|
26
|
+
# Is the epic SEALED? true iff it has >=1 story and EVERY stories/*.md frontmatter status is `shipped`.
|
|
27
|
+
epic_sealed() {
|
|
28
|
+
ep_dir="$1"
|
|
29
|
+
sdir="${ep_dir}/stories"
|
|
30
|
+
[ -d "$sdir" ] || return 1
|
|
31
|
+
found=0
|
|
32
|
+
for f in "$sdir"/*.md; do
|
|
33
|
+
[ -e "$f" ] || continue
|
|
34
|
+
found=1
|
|
35
|
+
st="$(fm_val status "$f")"
|
|
36
|
+
[ "$st" = "shipped" ] || return 1
|
|
37
|
+
done
|
|
38
|
+
[ "$found" = "1" ] || return 1
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
commits="$(git rev-list --no-merges "$RANGE")"
|
|
43
|
+
if [ -z "$commits" ]; then
|
|
44
|
+
echo "PASS [epic-open]: no non-merge commits in ${RANGE}"
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
rc=0
|
|
49
|
+
while IFS= read -r sha; do
|
|
50
|
+
[ -z "$sha" ] && continue
|
|
51
|
+
short="$(git log -1 --format=%h "$sha")"
|
|
52
|
+
subject="$(git log -1 --format=%s "$sha")"
|
|
53
|
+
if printf '%s' "$subject" | grep -qE "^(${EXEMPT})(\([a-z0-9._-]+\))?!?: "; then
|
|
54
|
+
echo "PASS [epic-open]: ${short} '${subject}' — maintenance commit (exempt)"
|
|
55
|
+
continue
|
|
56
|
+
fi
|
|
57
|
+
task="$(git log -1 --format='%(trailers:key=Task,valueonly)' "$sha" | sed '/^$/d' | head -1)"
|
|
58
|
+
if ! printf '%s' "$task" | grep -qE '.+-T[0-9]+$'; then
|
|
59
|
+
echo "note [epic-open]: ${short} has no resolvable Task trailer — deferring to spec-link."
|
|
60
|
+
continue
|
|
61
|
+
fi
|
|
62
|
+
story="$(printf '%s' "$task" | sed -E 's/-T[0-9]+$//')"
|
|
63
|
+
link="specs/${story}/link.md"
|
|
64
|
+
[ -f "$link" ] || { echo "note [epic-open]: ${short} ${task} — link.md missing (spec-link will FAIL)."; continue; }
|
|
65
|
+
product_rel="$(fm_val product-repo "$link")"
|
|
66
|
+
epic="$(fm_val epic "$link")"
|
|
67
|
+
# A malformed link.md (empty product-repo, or an epic that is not a real EP-<slug>) must FAIL, not
|
|
68
|
+
# slip through as "not reachable" — an empty epic would collapse ep_dir to <product>/epics/ (a real
|
|
69
|
+
# dir) and pass the seal check as if the epic were open.
|
|
70
|
+
if [ -z "$product_rel" ] || ! printf '%s' "$epic" | grep -qE '^EP-[a-z0-9-]+$'; then
|
|
71
|
+
echo "FAIL [epic-open]: ${short} ${task} — link.md has no valid product-repo/epic metadata."
|
|
72
|
+
rc=1
|
|
73
|
+
continue
|
|
74
|
+
fi
|
|
75
|
+
# product-repo is relative to the link.md's directory (specs/<story>/), so join it there.
|
|
76
|
+
prod="specs/${story}/${product_rel}"
|
|
77
|
+
ep_dir="${prod}/epics/${epic}"
|
|
78
|
+
if [ ! -d "$prod" ]; then
|
|
79
|
+
echo "PASS [epic-open]: ${short} ${task} -> ${epic} (product repo not reachable — seal check deferred)."
|
|
80
|
+
continue
|
|
81
|
+
fi
|
|
82
|
+
if [ ! -d "$ep_dir" ]; then
|
|
83
|
+
echo "FAIL [epic-open]: ${short} ${task} -> epic ${epic} does not exist in the product repo (orphan story link)."
|
|
84
|
+
rc=1
|
|
85
|
+
continue
|
|
86
|
+
fi
|
|
87
|
+
if epic_sealed "$ep_dir"; then
|
|
88
|
+
echo "FAIL [epic-open]: ${short} ${task} targets SEALED epic ${epic} (all stories shipped)."
|
|
89
|
+
echo " -> New behaviour cannot mutate a shipped epic. Open a threaded change-epic with yad-change"
|
|
90
|
+
echo " (kind: change|defect|hotfix, parent: ${epic}) and implement against ITS stories instead."
|
|
91
|
+
rc=1
|
|
92
|
+
continue
|
|
93
|
+
fi
|
|
94
|
+
echo "PASS [epic-open]: ${short} ${task} -> ${epic} (epic is open — has unshipped stories)."
|
|
95
|
+
done <<EOF
|
|
96
|
+
$commits
|
|
97
|
+
EOF
|
|
98
|
+
exit "$rc"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lineage-check gate (Phase 6 — feature threads). Builds on spec-link: every NON-MAINTENANCE commit
|
|
3
|
+
# must link a real story (spec-link enforces that), and the OWNING epic must be a valid node in a
|
|
4
|
+
# feature thread — a change/defect/hotfix epic MUST thread to a real parent. This is the
|
|
5
|
+
# "every code change has an owning epic in a thread" enforcement. Per commit; maintenance commits
|
|
6
|
+
# (ci/chore/build/test) are exempt. Fails CLOSED on an unresolvable base.
|
|
7
|
+
#
|
|
8
|
+
# The owning epic lives in the PRODUCT repo (reached via specs/<story>/link.md's `product-repo` path,
|
|
9
|
+
# exactly like contract-check). When the product repo is not reachable from CI, lineage is verified
|
|
10
|
+
# best-effort: the commit PASSes with a note (spec-link already proved the story link).
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
BASE="${1:-${SDLC_BASE:-origin/main}}"
|
|
14
|
+
|
|
15
|
+
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
16
|
+
echo "FAIL [lineage-check]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
RANGE="${BASE}..HEAD"
|
|
20
|
+
EXEMPT='ci|chore|build|test'
|
|
21
|
+
|
|
22
|
+
# Read one frontmatter value from the FIRST --- … --- block only. awk bounds to the first block (stops
|
|
23
|
+
# at the first closing fence), so a body `---` or an absent key can never leak a body line. Plain
|
|
24
|
+
# scalars only.
|
|
25
|
+
fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f && index($0, k":")==1 {sub("^" k ":[ \t]*", ""); print; exit}' "$2" 2>/dev/null | tr -d '\r'; }
|
|
26
|
+
|
|
27
|
+
commits="$(git rev-list --no-merges "$RANGE")"
|
|
28
|
+
if [ -z "$commits" ]; then
|
|
29
|
+
echo "PASS [lineage-check]: no non-merge commits in ${RANGE}"
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
rc=0
|
|
34
|
+
while IFS= read -r sha; do
|
|
35
|
+
[ -z "$sha" ] && continue
|
|
36
|
+
short="$(git log -1 --format=%h "$sha")"
|
|
37
|
+
subject="$(git log -1 --format=%s "$sha")"
|
|
38
|
+
if printf '%s' "$subject" | grep -qE "^(${EXEMPT})(\([a-z0-9._-]+\))?!?: "; then
|
|
39
|
+
echo "PASS [lineage-check]: ${short} '${subject}' — maintenance commit (exempt)"
|
|
40
|
+
continue
|
|
41
|
+
fi
|
|
42
|
+
task="$(git log -1 --format='%(trailers:key=Task,valueonly)' "$sha" | sed '/^$/d' | head -1)"
|
|
43
|
+
# No / malformed Task trailer is spec-link's job to FAIL; here we only skip what we can't resolve.
|
|
44
|
+
if ! printf '%s' "$task" | grep -qE '.+-T[0-9]+$'; then
|
|
45
|
+
echo "note [lineage-check]: ${short} has no resolvable Task trailer — deferring to spec-link."
|
|
46
|
+
continue
|
|
47
|
+
fi
|
|
48
|
+
story="$(printf '%s' "$task" | sed -E 's/-T[0-9]+$//')"
|
|
49
|
+
link="specs/${story}/link.md"
|
|
50
|
+
if [ ! -f "$link" ]; then
|
|
51
|
+
echo "note [lineage-check]: ${short} ${task} — specs/${story}/link.md missing (spec-link will FAIL)."
|
|
52
|
+
continue
|
|
53
|
+
fi
|
|
54
|
+
product_rel="$(fm_val product-repo "$link")"
|
|
55
|
+
epic="$(fm_val epic "$link")"
|
|
56
|
+
if [ -z "$epic" ]; then
|
|
57
|
+
echo "FAIL [lineage-check]: ${short} ${task} — link.md has no 'epic:' (cannot place it in a thread)."
|
|
58
|
+
rc=1
|
|
59
|
+
continue
|
|
60
|
+
fi
|
|
61
|
+
# product-repo is relative to the link.md's directory (specs/<story>/), so join it there.
|
|
62
|
+
prod="specs/${story}/${product_rel}"
|
|
63
|
+
epicmd="${prod}/epics/${epic}/epic.md"
|
|
64
|
+
# Defer ONLY when the product checkout itself is unreachable. A reachable hub whose epic is missing is
|
|
65
|
+
# an orphaned story link — FAIL, do not pass it off as "not reachable".
|
|
66
|
+
if [ -z "$product_rel" ] || [ ! -d "$prod" ]; then
|
|
67
|
+
echo "PASS [lineage-check]: ${short} ${task} -> epic ${epic} (product repo not reachable — lineage check deferred)."
|
|
68
|
+
continue
|
|
69
|
+
fi
|
|
70
|
+
if [ ! -f "$epicmd" ]; then
|
|
71
|
+
echo "FAIL [lineage-check]: ${short} ${task} -> epic ${epic} does not exist in the product repo (orphan story link)."
|
|
72
|
+
rc=1
|
|
73
|
+
continue
|
|
74
|
+
fi
|
|
75
|
+
kind="$(fm_val kind "$epicmd")"
|
|
76
|
+
[ -z "$kind" ] && kind="feature"
|
|
77
|
+
if [ "$kind" = "feature" ]; then
|
|
78
|
+
echo "PASS [lineage-check]: ${short} ${task} -> ${epic} (genesis feature epic)."
|
|
79
|
+
continue
|
|
80
|
+
fi
|
|
81
|
+
# A change/defect/hotfix epic MUST thread to a real parent.
|
|
82
|
+
parent="$(fm_val parent "$epicmd")"
|
|
83
|
+
if [ -z "$parent" ]; then
|
|
84
|
+
echo "FAIL [lineage-check]: ${short} ${task} -> ${epic} is kind:${kind} but declares no 'parent:' — a change-epic must thread to its predecessor."
|
|
85
|
+
rc=1
|
|
86
|
+
continue
|
|
87
|
+
fi
|
|
88
|
+
if [ ! -f "${prod}/epics/${parent}/epic.md" ]; then
|
|
89
|
+
echo "FAIL [lineage-check]: ${short} ${task} -> ${epic} threads to '${parent}', but epics/${parent}/ does not exist in the hub (orphan thread)."
|
|
90
|
+
rc=1
|
|
91
|
+
continue
|
|
92
|
+
fi
|
|
93
|
+
echo "PASS [lineage-check]: ${short} ${task} -> ${epic} (kind:${kind} threaded to ${parent})."
|
|
94
|
+
done <<EOF
|
|
95
|
+
$commits
|
|
96
|
+
EOF
|
|
97
|
+
exit "$rc"
|