@pilotspace/add 1.4.0 → 1.6.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 +63 -0
- package/docs/14-foundation.md +11 -0
- package/docs/16-releasing.md +182 -0
- package/docs/README.md +3 -0
- package/docs/appendix-c-glossary.md +19 -1
- package/package.json +1 -1
- package/skill/add/SKILL.md +12 -0
- package/skill/add/design.md +92 -0
- package/skill/add/phases/0-setup.md +2 -1
- package/skill/add/phases/1-specify.md +3 -1
- package/skill/add/release.md +114 -0
- package/tooling/add.py +357 -0
- package/tooling/templates/kit.sample.css +53 -0
- package/tooling/templates/settings.sample.html +25 -0
- package/tooling/templates/tokens.sample.css +23 -0
- package/tooling/templates/udd-wireframe.md +118 -0
- package/tooling/templates/welcome.sample.html +25 -0
- package/tooling/templates/wireframe.sample.txt +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,69 @@ All notable changes to the ADD method (`@pilotspace/add` on npm,
|
|
|
4
4
|
`pilotspace-add` on PyPI) are documented here. The format follows
|
|
5
5
|
[Keep a Changelog](https://keepachangelog.com/); versions follow semver.
|
|
6
6
|
|
|
7
|
+
## [1.6.0] — 2026-06-16
|
|
8
|
+
|
|
9
|
+
The releasing release: shipping a versioned cut is now a first-class **5th ADD
|
|
10
|
+
scope level**, not an ad-hoc ritual. The AI gathers the inventory and drafts
|
|
11
|
+
evidence-backed notes, the engine records the cut behind a security-hard-stop
|
|
12
|
+
readiness floor, and the human owns the tag and publish. All additive; no
|
|
13
|
+
breaking changes (SemVer MINOR).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **The RELEASE scope level (`release.md`)** — the on-demand guide for the 7-step
|
|
17
|
+
flow `cue → gather → draft notes → readiness floor → human confirms → cut → watch`,
|
|
18
|
+
orthogonal to stage: bundle one or more closed milestones into a versioned,
|
|
19
|
+
watched cut. Cross-referenced from `SKILL.md`.
|
|
20
|
+
- **`add.py release-report`** — a read-only gather of the five record-sets (closed
|
|
21
|
+
milestones · their consolidated deltas · riding `RISK-ACCEPTED` waivers · open
|
|
22
|
+
security `HARD-STOP` · scenarios → monitors), with `--json`, plus the
|
|
23
|
+
`→ releasable: N milestone(s)` status cue.
|
|
24
|
+
- **`add.py release <version>`** — a guarded, record-only cut: it prepends the
|
|
25
|
+
`CHANGELOG.md` block, appends an append-only `RELEASES.md` ledger row
|
|
26
|
+
(newest-first), and attributes the bundled milestones — behind a four-code
|
|
27
|
+
readiness floor (`release_security_open` · `release_tests_red` ·
|
|
28
|
+
`release_no_closed_milestone` · `release_undisclosed_waiver`). The security stop
|
|
29
|
+
is **un-forceable**: `--force` can override the other three, never that one.
|
|
30
|
+
- **`RELEASES.md`** — the append-only release ledger (date · version · milestones ·
|
|
31
|
+
waivers · evidence); membership is the attribution source, so the cue never has
|
|
32
|
+
to read a compacted milestone file.
|
|
33
|
+
- **Book chapter 16 (`16-releasing.md`)** + five glossary entries (Release · Release
|
|
34
|
+
scope level · Readiness floor · RELEASES.md ledger · Hotfix release) + the
|
|
35
|
+
`test_release_docs_accord` guard that keeps the book in accord with `release.md`.
|
|
36
|
+
|
|
37
|
+
### Notes
|
|
38
|
+
- **The engine records; the human ships.** `add.py release` writes the changelog +
|
|
39
|
+
ledger + attribution; it never bumps a version source, tags, publishes, or
|
|
40
|
+
deploys. The outward act stays human-owned and tool-agnostic — exactly the
|
|
41
|
+
human-gated `git tag` that cut this very release.
|
|
42
|
+
|
|
43
|
+
## [1.5.0] — 2026-06-16
|
|
44
|
+
|
|
45
|
+
The UDD design-loop release: defining the design *before* the code is now a
|
|
46
|
+
guided, evidence-backed loop inside the method. A new `design.md` drives the UDD
|
|
47
|
+
beats to a confirmed screen, a wireframe + HTML-mock recipe renders a real screen
|
|
48
|
+
the human approves before build, and the engine measures that the confirmation
|
|
49
|
+
was actually captured. All additive; no breaking changes (SemVer MINOR).
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- **UDD design-definition loop (`design.md`)** — turns the foundation's UDD
|
|
53
|
+
concern into a runnable loop: a low-fi structural wireframe → a self-contained
|
|
54
|
+
HTML mock (resolve semantic tokens → one kit class per component → compose the
|
|
55
|
+
prototype tree → populate with mock data) → a captured screen the human
|
|
56
|
+
confirms *before* any build. Wired into `0-setup` and `1-specify`.
|
|
57
|
+
- **Wireframe + HTML-mock recipe (`udd-wireframe.md`) + sample templates** — a
|
|
58
|
+
zero-dependency, any-stack floor for rendering a prototype tree into a real
|
|
59
|
+
screen, with a worked sample set (`tokens.sample.css`, `kit.sample.css`,
|
|
60
|
+
`welcome.sample.html`, `settings.sample.html` reusing the kit,
|
|
61
|
+
`wireframe.sample.txt`). One semantic-token flip re-themes every screen by
|
|
62
|
+
construction. An optional `@json-render/image` (Satori → PNG/SVG, no browser)
|
|
63
|
+
fast path is noted for JS-ecosystem projects.
|
|
64
|
+
- **Capture-evidence convention + `missing_capture` WARN** — design captures live
|
|
65
|
+
at `.add/design/captures/<name>.<ext>`; `add.py check` emits a never-red
|
|
66
|
+
`missing_capture` warning for any prototype lacking a capture (silent when
|
|
67
|
+
absent, so non-UI projects stay clean). The engine *measures* capture presence;
|
|
68
|
+
it never renders.
|
|
69
|
+
|
|
7
70
|
## [1.4.0] — 2026-06-15
|
|
8
71
|
|
|
9
72
|
The guided-onboarding release: starting and running an ADD project is now guided
|
package/docs/14-foundation.md
CHANGED
|
@@ -47,6 +47,17 @@ the method already names, made explicit as three concerns.
|
|
|
47
47
|
test that behaviour; the foundation keeps the design intent that makes a screen
|
|
48
48
|
worth building.
|
|
49
49
|
|
|
50
|
+
When a milestone has screens, UDD is not only a static `DESIGN.md` — it runs a
|
|
51
|
+
**design-definition loop** that turns the domain into a screen the human has *seen
|
|
52
|
+
and confirmed before build*. Its four beats are `review-domain → research-components
|
|
53
|
+
→ wireframe → render-capture-confirm`: read the domain into screens and regions,
|
|
54
|
+
research and reuse components before inventing them, wireframe the structure low-fi,
|
|
55
|
+
then render a real screen and **capture** it. That capture is the **design-confirm**
|
|
56
|
+
evidence — a real image the person approves *before* implementation, so the build
|
|
57
|
+
matches the layout instead of discovering it. The book keeps the *why*; the
|
|
58
|
+
operational recipe (the wireframe format, the token-bound mock, the capture engines)
|
|
59
|
+
lives in the `add` skill's `design.md` and `udd-wireframe.md`.
|
|
60
|
+
|
|
50
61
|
These three foundation competencies, together with the **TDD ⇄ ADD** engine of
|
|
51
62
|
[Part II](./02-the-flow.md), are ADD's five. The first four feed context to the
|
|
52
63
|
fifth, where the AI executes on it:
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# 16 · Releasing
|
|
2
|
+
|
|
3
|
+
[← 15 Foundations & Lineage](./15-foundations-and-lineage.md) · [Contents](./README.md) · Next: [Appendix A Templates →](./appendix-a-templates.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
The flow chapters ([03](./03-step-1-specify.md)–[08](./08-step-6-verify.md)) take one feature from
|
|
8
|
+
spec to verified. The loop chapter ([09](./09-the-loop.md)) keeps a milestone going until its goal
|
|
9
|
+
is met. The stages chapter ([10](./10-setup-and-stages.md)) graduates the project's rigor. None of
|
|
10
|
+
them *ship*. This chapter names the act every project eventually performs and that the method, until
|
|
11
|
+
now, never formalized: bundling closed milestones into a versioned, user-facing release whose notes
|
|
12
|
+
are evidence-backed, whose risk is disclosed, and whose behaviour is then watched.
|
|
13
|
+
|
|
14
|
+
Releasing is the **fifth scope level** — after the task, the milestone, the foundation/setup level,
|
|
15
|
+
and stage graduation. Like every scope level it runs the same shape: **gather → propose → the human
|
|
16
|
+
confirms → the engine records and enforces a floor.** And like graduation, it ends with an outward
|
|
17
|
+
act the human owns. The operational recipe lives in the `release.md` skill guide; this chapter is the
|
|
18
|
+
*why* behind it.
|
|
19
|
+
|
|
20
|
+
## 16.1 · Why release is its own scope level
|
|
21
|
+
|
|
22
|
+
A number bump is not a release. A release is the moment one or more **closed milestones** become a
|
|
23
|
+
versioned cut that real users can run. Three distinctions make it its own scope level:
|
|
24
|
+
|
|
25
|
+
- **Milestone ≠ release.** A milestone is *feature-complete and consolidated* — its goal is met and
|
|
26
|
+
its lessons are gathered into the foundation (see [14 · The foundation](./14-foundation.md) and the
|
|
27
|
+
`fold.md` retrospective consolidation). A release is *shipped and watched*. The first is an internal
|
|
28
|
+
state; the second faces outward.
|
|
29
|
+
- **Graduation ≠ release.** Stage graduation moves the project's *rigor* (mvp → production); a release
|
|
30
|
+
ships a *version*. The two axes are orthogonal: you cut a prototype preview, an mvp beta, and a
|
|
31
|
+
production GA, each at its own stage. You release at every stage, not only at the end.
|
|
32
|
+
- **A release bundles; it does not equal.** One version may attribute several milestones — "we shipped
|
|
33
|
+
after a couple of milestones closed" is the normal case, not the exception. Forcing one release per
|
|
34
|
+
milestone is the anti-pattern; the decoupling is the whole point.
|
|
35
|
+
|
|
36
|
+
So release sits beside the other scope levels rather than inside any of them. The granularity ladder
|
|
37
|
+
is now complete: intake → milestone → task, with stage graduation and release as the two cross-cutting
|
|
38
|
+
levels that change *rigor* and ship *versions* respectively (see the **Scope level** entry in
|
|
39
|
+
[Appendix C](./appendix-c-glossary.md)).
|
|
40
|
+
|
|
41
|
+
## 16.2 · The cue and the inventory — gather, never judge
|
|
42
|
+
|
|
43
|
+
The trigger is a status line. When at least one milestone is `done`, archived, and not yet attributed
|
|
44
|
+
to any release, `add.py status` prints:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
→ releasable: N milestone(s) closed since last release
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That line is a **tally**, never a verdict. It counts archived-but-unreleased milestones; it is silent
|
|
51
|
+
for a project that has never released or has already shipped everything (grandfathered — zero change).
|
|
52
|
+
It says *there is something to consider*, not *you are ready*.
|
|
53
|
+
|
|
54
|
+
To gather the cut's evidence, run `add.py release-report`. It clusters five labeled record-sets: the
|
|
55
|
+
closed milestones since the last release · their **consolidated deltas** (the "what changed" record) ·
|
|
56
|
+
the open `RISK-ACCEPTED` waivers riding into the release · any open security `HARD-STOP` (a blocker) ·
|
|
57
|
+
the §2 scenarios to take live as monitors. The report **gathers; it does not judge** — there is no
|
|
58
|
+
readiness score to read off, because a tally that pretended to be a verdict would invite reading the
|
|
59
|
+
number instead of the evidence.
|
|
60
|
+
|
|
61
|
+
The reuse claim, stated plainly: **the consolidated deltas are the changelog source.** You do not write
|
|
62
|
+
release notes from memory. The foundation already recorded what changed when each milestone was
|
|
63
|
+
consolidated (`fold.md`); the release surfaces it. This is why release runs *after* consolidation, not
|
|
64
|
+
before. The lifecycle order is one line:
|
|
65
|
+
`milestone-done → fold → compact → archive → (repeat ≥1×) → release → watch`.
|
|
66
|
+
|
|
67
|
+
## 16.3 · Drafting the notes and the version — the proposal
|
|
68
|
+
|
|
69
|
+
From those record-sets you draft a [Keep a Changelog](https://keepachangelog.com/) entry: group the
|
|
70
|
+
changes under Added / Changed / Fixed and name the headline capabilities concretely, in the user's
|
|
71
|
+
language, not the commit's. Each bundled milestone's goal anchors one or more entries.
|
|
72
|
+
|
|
73
|
+
Then propose the version. Semver is a decision the evidence informs but does not make for you: a
|
|
74
|
+
breaking change is a MAJOR, a new capability a MINOR, a fix-only cut a PATCH. You propose the bump; the
|
|
75
|
+
**human confirms it** — the version is a judgement, not a default the tool fills in.
|
|
76
|
+
|
|
77
|
+
Both the notes and the version are *shown before they are asked about*. Present the drafted entry, the
|
|
78
|
+
proposed version, and the waivers shipping in this cut via the report template — opening with the ARC
|
|
79
|
+
(goal · done · plan) — and let the human approve once. The question is a summary to decide on; the
|
|
80
|
+
artifact itself is rendered first, never pre-stamped.
|
|
81
|
+
|
|
82
|
+
## 16.4 · The floor — what the engine enforces
|
|
83
|
+
|
|
84
|
+
`add.py release <version>` is **guarded**. Before it records anything it enforces a readiness floor,
|
|
85
|
+
refusing with a non-zero exit and leaving every file byte-unchanged on any of four conditions:
|
|
86
|
+
|
|
87
|
+
- `release_security_open` — an open security `HARD-STOP` exists. This is the non-negotiable one: a
|
|
88
|
+
security finding is never shipped. Resolve it first, as a change request back to Specify.
|
|
89
|
+
- `release_tests_red` — the suite is not green. A release ships on evidence, not on a plausible diff.
|
|
90
|
+
- `release_no_closed_milestone` — nothing new since the last release. The cut would be a no-op; do not
|
|
91
|
+
bump a version to mark time.
|
|
92
|
+
- `release_undisclosed_waiver` — a `RISK-ACCEPTED` waiver rides into the release but is missing from the
|
|
93
|
+
notes. Disclosure *is* the floor: a shipped risk the user cannot read about is a hidden risk.
|
|
94
|
+
|
|
95
|
+
The security stop is **un-forceable.** `--force` exists for grandfathered and brownfield first-cuts —
|
|
96
|
+
the same authority valve as `stage --force` ([10 · Setup and stages](./10-setup-and-stages.md)) — and
|
|
97
|
+
it can override the other three rejects, but it can never override `release_security_open`. This mirrors
|
|
98
|
+
the verify gate exactly ([08 · Verify](./08-step-6-verify.md)) and the governance ceiling that no
|
|
99
|
+
autonomy level may lift ([11 · Governance](./11-governance.md)): a security `HARD-STOP` is the one
|
|
100
|
+
outcome the method refuses to auto-pass, at verify and again at the cut.
|
|
101
|
+
|
|
102
|
+
## 16.5 · The cut versus the ship — the engine records, the human ships
|
|
103
|
+
|
|
104
|
+
Only after the human confirms do you run the cut. And here is the line that keeps releasing honest:
|
|
105
|
+
**the engine records; the human ships.**
|
|
106
|
+
|
|
107
|
+
`add.py release <version>` **records** the marker. It prepends the entry to `CHANGELOG.md`, stamps one
|
|
108
|
+
append-only row (newest-first, like the §Key Decisions log) into `RELEASES.md` — date · version ·
|
|
109
|
+
milestones · waivers shipped · evidence — and attributes the bundled milestones to this version, so the
|
|
110
|
+
cue stops firing for them. The ledger is the attribution source: a milestone is "released" because a
|
|
111
|
+
`RELEASES.md` row says so, never because a file was edited to claim it.
|
|
112
|
+
|
|
113
|
+
What the engine does **not** do is act outward. It **never tags, publishes, or deploys.** The outward
|
|
114
|
+
act — `git tag`, `npm publish`, the deploy pipeline — is the human's, tool-agnostic, exactly as the
|
|
115
|
+
method tool "never renders" in design and "never spawns" a subagent. Design-for-failure — timeouts,
|
|
116
|
+
retries, rollback, a tested revert path — belongs in the pipeline the human owns, not in a method tool
|
|
117
|
+
that has no business holding deploy credentials. The tag is the human-gated trigger; the record is the
|
|
118
|
+
engine's receipt that the floor was met.
|
|
119
|
+
|
|
120
|
+
> **A caveat worth one paragraph.** `add.py release` writes `CHANGELOG.md` at the **project root**. That
|
|
121
|
+
> is the right default for most repositories. But a repo with a different changelog convention — for
|
|
122
|
+
> instance a **nested-package** layout whose root `CHANGELOG.md` is a deliberate pointer to a package's
|
|
123
|
+
> own changelog — will get release blocks *prepended above* its existing content rather than replacing
|
|
124
|
+
> it (the writer preserves what is there; it does not clobber). Reconcile per repo: either let the root
|
|
125
|
+
> file carry the canonical log, or point the human's publish step at the package changelog the team
|
|
126
|
+
> treats as the source of truth.
|
|
127
|
+
|
|
128
|
+
## 16.6 · Watch and the hotfix path — re-entering observe
|
|
129
|
+
|
|
130
|
+
A release is not the finish line; it is where the most reliable information finally appears. The §2
|
|
131
|
+
scenarios that were pass/fail cases at build time become **live monitors** for the released version, and
|
|
132
|
+
error-budget burn feeds the next loop. Live-registry and deploy confirmation are post-cut *evidence*,
|
|
133
|
+
gathered after the tag — not unit tests pretending to be one.
|
|
134
|
+
|
|
135
|
+
The unhappy path is first-class. A regression found in the wild re-enters at Specify as a **change
|
|
136
|
+
request**, which narrows to a **hotfix release** — the same seven-step flow, scoped to the fix, cut as a
|
|
137
|
+
PATCH. Releasing does not have a separate emergency mode; it has the ordinary flow at a tighter scope.
|
|
138
|
+
|
|
139
|
+
Depth follows the stage, as everywhere in the method:
|
|
140
|
+
|
|
141
|
+
- **prototype / poc** — a one-line preview note and a tag; no deploy ceremony. The point is feedback.
|
|
142
|
+
- **mvp** — full notes, a tag, a guarded publish; watch the headline scenarios.
|
|
143
|
+
- **production** — every step at full rigor: notes, tag, a deploy behind a rollback-tested pipeline,
|
|
144
|
+
live scenario monitors, and error-budget watch. The hotfix path is a routine capability here, not a
|
|
145
|
+
fire drill.
|
|
146
|
+
|
|
147
|
+
## 16.7 · The flow, in one arc
|
|
148
|
+
|
|
149
|
+
One arc, seven steps:
|
|
150
|
+
|
|
151
|
+
**cue → gather → draft notes → readiness floor → human confirms → cut → watch**
|
|
152
|
+
|
|
153
|
+
1. **cue** — `add.py status` prints `→ releasable: N` when an archived milestone is unreleased.
|
|
154
|
+
2. **gather** — `add.py release-report` clusters the five record-sets (gather, never judge).
|
|
155
|
+
3. **draft notes** — a Keep-a-Changelog entry drawn from the consolidated deltas; propose the version.
|
|
156
|
+
4. **readiness floor** — the four guarded rejects; the security stop is un-forceable.
|
|
157
|
+
5. **human confirms** — the notes, version, and waivers are shown, then approved once.
|
|
158
|
+
6. **cut** — `add.py release <version>` records the CHANGELOG block + the `RELEASES.md` row + attribution.
|
|
159
|
+
7. **watch** — the scenarios become monitors; a wild regression becomes a PATCH hotfix release.
|
|
160
|
+
|
|
161
|
+
The recipe for each step — flags, the report's `--json` shape, the exact ledger row — lives in the
|
|
162
|
+
`release.md` skill guide. This chapter is the reasoning; the guide is the procedure.
|
|
163
|
+
|
|
164
|
+
## 16.8 · Worked example — this method's own 1.5.0
|
|
165
|
+
|
|
166
|
+
The repository already runs this by hand, which is the best evidence the flow is real. The
|
|
167
|
+
`udd-design-loop` milestone closed (4/4) and consolidated into `foundation-version 33`. From those
|
|
168
|
+
deltas the human drafted the `## [1.5.0]` changelog entry, bumped the three version sources in
|
|
169
|
+
lockstep, and a forward-pinned `test_release_1_5_0.py` asserted in-repo readiness: the versions agree,
|
|
170
|
+
the changelog lineage survives, the feature anchors are named, and the engine is untouched by the
|
|
171
|
+
release. The cut itself — the `git tag` that triggers the npm and PyPI publish — stayed human-gated,
|
|
172
|
+
and the live-registry confirmation was gathered *after* the tag as verify evidence, never as a unit
|
|
173
|
+
test.
|
|
174
|
+
|
|
175
|
+
That ritual is what this chapter formalizes. `release-report` gathers the inventory, the floor enforces
|
|
176
|
+
the security stop and the disclosures, `add.py release` records the cut, and the human still owns the
|
|
177
|
+
tag. The method releases itself the way it asks every project to release: gather the evidence, disclose
|
|
178
|
+
the risk, record the marker, and let a person make the outward call.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
[← 15 Foundations & Lineage](./15-foundations-and-lineage.md) · [Contents](./README.md) · Next: [Appendix A Templates →](./appendix-a-templates.md)
|
package/docs/README.md
CHANGED
|
@@ -54,6 +54,9 @@ For every feature, before AI writes any code, you write four short artifacts in
|
|
|
54
54
|
**Lineage**
|
|
55
55
|
- [15 · Foundations & Lineage](./15-foundations-and-lineage.md)
|
|
56
56
|
|
|
57
|
+
**Releasing**
|
|
58
|
+
- [16 · Releasing](./16-releasing.md)
|
|
59
|
+
|
|
57
60
|
**Part IV — Reference**
|
|
58
61
|
- [Appendix A · Templates](./appendix-a-templates.md)
|
|
59
62
|
- [Appendix B · Prompt library](./appendix-b-prompts.md)
|
|
@@ -72,10 +72,20 @@
|
|
|
72
72
|
|
|
73
73
|
**Baseline approval** (formerly "the lock-down") — the single human gate ending autonomous setup: an explicit yes that freezes the foundation, first scope, and first contract together; runs as `add.py lock --by <name>`.
|
|
74
74
|
|
|
75
|
-
**Scope level** (formerly "altitude") — the granularity a decision lives at: intake level (request → versioned scope) · milestone level · setup/foundation level · task level. (A cross-stage decision lives one level out, at the **stage-graduation** loop — which `graduate.md` also numbers as a scope level; see **Stage graduation**.) One ⚠-assumption notation is shared across every scope level.
|
|
75
|
+
**Scope level** (formerly "altitude") — the granularity a decision lives at: intake level (request → versioned scope) · milestone level · setup/foundation level · task level · release level (≥1 closed milestone → a versioned, watched cut; see **Release scope level**). (A cross-stage decision lives one level out, at the **stage-graduation** loop — which `graduate.md` also numbers as a scope level; see **Stage graduation**.) One ⚠-assumption notation is shared across every scope level.
|
|
76
76
|
|
|
77
77
|
**Autonomy level** (formerly "autonomy dial") — the explicit per-task setting (`autonomy: manual | conservative | auto`, an ordered ladder manual < conservative < auto) choosing who resolves Verify: `auto` auto-PASSes on complete evidence, `conservative` keeps a human at the gate, `manual` is the strict floor (the human owns the gate; nothing auto-resolves). A high-risk scope refuses an unguarded `auto` — it must be lowered to `manual` or `conservative`. New tasks seed a visible, overridable `autonomy: auto`; a live task with no level warns (`implicit_autonomy`), a token outside the set is rejected (`unknown_autonomy_level`).
|
|
78
78
|
|
|
79
|
+
**Release** — a versioned, user-facing cut that bundles one or more closed milestones into something real users can run; its notes are evidence-backed, its risk is disclosed, and its behaviour is then watched. Recorded with `add.py release <version>`, which writes the changelog block and the ledger row but never tags, publishes, or deploys — the outward act stays human-owned. See [16 · Releasing](./16-releasing.md).
|
|
80
|
+
|
|
81
|
+
**Release scope level** — the fifth scope level: releasing as its own granularity, orthogonal to the stage. A release bundles ≥1 closed milestone (never forced one-per-milestone) and may be cut at any stage — prototype preview, mvp beta, production GA. Distinct from milestone-close (feature-complete + consolidated) and from stage graduation (which changes rigor, not version). See **Scope level** and [16 · Releasing](./16-releasing.md).
|
|
82
|
+
|
|
83
|
+
**Readiness floor** — the engine-enforced pre-cut gate `add.py release` applies before it records anything: a green suite, zero open security `HARD-STOP`, a closed-and-unreleased milestone to bundle, and every riding `RISK-ACCEPTED` waiver disclosed in the notes. Its four rejects are `release_security_open` (un-forceable) · `release_tests_red` · `release_no_closed_milestone` · `release_undisclosed_waiver`; `--force` may override every reject except the security stop.
|
|
84
|
+
|
|
85
|
+
**RELEASES.md ledger** — the append-only, newest-first trail of release rows at the project root (date · version · milestones · waivers shipped · evidence). Like the §Key Decisions log it is never rewritten; a superseded or yanked version is recorded as a new row. The ledger is the attribution source — a milestone is "released" because a row says so — which is why the `→ releasable` cue never has to read a milestone file.
|
|
86
|
+
|
|
87
|
+
**Hotfix release** — a narrowed PATCH cut that re-enters at Specify as a change request when a regression is found in a released version. It runs the same seven-step release flow scoped to the fix; releasing has no separate emergency mode, only the ordinary flow at a tighter scope.
|
|
88
|
+
|
|
79
89
|
**Auto-ready goal** — a milestone goal whose every exit criterion **cites a verifier** (`(verify: <test|command|metric>)`), so the engine can self-verify the result against the goal without human judgment. It is the prerequisite by which **autonomy is earned by goal-clarity**: the **autonomy level** governs *who* resolves Verify, but a clarified, machine-checkable goal is what makes a self-verifying run meaningful. `add.py check` raises a `goal_not_auto_ready` **WARN** (never red) for the active milestone until it has an auto-ready goal (≥1 exit criterion and every one cited), and `status` surfaces it (`goal-ready: auto-ready ✓` / cited-of-total); a zero-criteria goal reads not-auto-ready and is milestone-shaping's nudge, not this warning's. The lint forces a citation *slot* per criterion — it raises the floor but **cannot prove the citation is real** (a human can write `(verify: it works)`): citation-theater is the accepted irreducible floor, and the freeze gate and autonomy behavior are unchanged by it.
|
|
80
90
|
|
|
81
91
|
**Automated quality gate** (formerly "evidence auto-gate") — the Verify resolver under `autonomy: auto`: a run may auto-PASS on complete evidence, recorded as *auto-resolved*; a security finding always escalates (`HARD-STOP`).
|
|
@@ -108,6 +118,14 @@
|
|
|
108
118
|
|
|
109
119
|
**Newest-first append-only** — every append-only foundation sequence prepends the newest record at the top; the rolled-up settled line anchors at the bottom (the oldest end), so compaction collapses upward.
|
|
110
120
|
|
|
121
|
+
**Wireframe** — the Stage-A low-fidelity, *structural* map of one screen: its regions and the component **slots** inside them, derived from `prototypes/<name>.json` *before* any color, type, or spacing — it answers "what goes where", not "what it looks like". Beat 3 of the UDD **design-definition loop**; the low-fi half of the two-stage fidelity that ends in a confirmed capture. See the `add` skill's `udd-wireframe.md` (Stage A).
|
|
122
|
+
|
|
123
|
+
**Design mock** — the Stage-B high-fidelity, **self-contained** HTML render of a screen: the `catalog.json` components as a reusable token-bound kit, bound to `tokens.json` and populated with mock data, openable offline and screenshot-able. The human-facing *visible* evidence the human confirms (the frozen `prototypes/<name>.json` tree is its machine-checkable twin). Beat 4's hi-fi artifact; the recipe lives in the `add` skill's `udd-wireframe.md` (Stage B).
|
|
124
|
+
|
|
125
|
+
**Capture** — the real rendered image (PNG/SVG) of a design mock: the **design-confirm evidence** artifact. Captures live at `.add/design/captures/<name>.<ext>` (one per prototype) and are attached or mentioned in the feature's `TASK.md`; `@json-render/image` (Satori → PNG/SVG, no browser) is the named default capture engine, otherwise the self-contained mock is screenshot headless. The engine never renders — it only MEASURES presence: `add.py check` raises a never-red `missing_capture` WARN for a prototype with no capture.
|
|
126
|
+
|
|
127
|
+
**Design-confirm** — the human touchpoint closing the UDD **design-definition loop** (`review-domain → research-components → wireframe → render-capture-confirm`, beat 4 of the `add` skill's `design.md`): approving the captured screen image **before build**, show-before-ask, so the implementation matches the layout the human has already seen instead of discovering it.
|
|
128
|
+
|
|
111
129
|
---
|
|
112
130
|
|
|
113
131
|
## Optional mapping to formal phase names
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilotspace/add",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "ADD (AI-Driven Development) — a minimal, state-tracked Claude Code skill that drives every feature through Specify → Scenarios → Contract → Tests → Build → Verify → Observe. Ships the AIDD book as its trust layer.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"add": "bin/cli.js"
|
package/skill/add/SKILL.md
CHANGED
|
@@ -106,6 +106,11 @@ template, the tier pick — read `advisor.md`; and at any decision point self-sc
|
|
|
106
106
|
(0–1 across six dimensions, refine if any < 0.9) with `confidence.md`. Both are advisory: the
|
|
107
107
|
engine never spawns, and the self-score is never a gate.
|
|
108
108
|
|
|
109
|
+
When a **UI feature** reaches specify, run the **design-definition loop** in `design.md` (UDD):
|
|
110
|
+
review the domain → research and reuse components → wireframe → render a real captured screen the
|
|
111
|
+
human confirms **before** build — so the build matches the expected layout. Tool-agnostic; the
|
|
112
|
+
engine never renders.
|
|
113
|
+
|
|
109
114
|
When a milestone's tasks are all done but its **goal** (the `MILESTONE.md` exit criteria) is not
|
|
110
115
|
yet met, `milestone-done` holds the milestone open — read `loop.md` for the dynamic loop that turns
|
|
111
116
|
open deltas + extras into the next tasks, proposed by you and confirmed by the human, until the goal is met.
|
|
@@ -116,6 +121,13 @@ orchestration: gather `graduation-report` analytics → co-specify interview →
|
|
|
116
121
|
milestone → human confirm → then (and only then) `stage production`. The flip is guarded
|
|
117
122
|
(`stage_no_roadmap`) and is the FINAL step — never a bare label change.
|
|
118
123
|
|
|
124
|
+
When one or more milestones have closed since the last release, `add.py status` prints
|
|
125
|
+
**`→ releasable: N milestone(s) closed since last release`** — read `release.md` for the 5th scope
|
|
126
|
+
level: gather `release-report` → draft notes from the consolidated deltas → meet the readiness floor
|
|
127
|
+
(security HARD-STOP is un-forceable) → human confirms → `add.py release <version>` records the cut
|
|
128
|
+
(CHANGELOG + `RELEASES.md` ledger + milestone attribution) → watch. The engine records; the human runs
|
|
129
|
+
the tag / publish / deploy. A release bundles ≥1 milestone and is orthogonal to stage.
|
|
130
|
+
|
|
119
131
|
## Non-negotiable rules (from the method)
|
|
120
132
|
|
|
121
133
|
<constraints>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Design — the design-definition loop (UDD)
|
|
2
|
+
|
|
3
|
+
When a **UI feature** reaches specify, design it before you build it. This loop takes the
|
|
4
|
+
feature from the **domain** to a screen the human has **seen and confirmed** — a real captured
|
|
5
|
+
image — *before* any implementation, so the build matches the expected layout instead of
|
|
6
|
+
discovering it. It is loaded on demand (like `advisor.md` / `confidence.md`); the engine never
|
|
7
|
+
runs it for you.
|
|
8
|
+
|
|
9
|
+
Design before code is the UDD half of the method. The token + component foundation a UI project
|
|
10
|
+
draws from already exists — `tokens.json` (the compact-DTCG dialect, `udd-tokens.md`),
|
|
11
|
+
`catalog.json` + `prototypes/<name>.json` (`udd-catalog.md`). This loop is how you *fill* that
|
|
12
|
+
foundation for a feature and earn the human's sign-off on the look before build.
|
|
13
|
+
|
|
14
|
+
## The loop — four beats
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
review-domain → research-components → wireframe → render-capture-confirm
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Run the beats in order. Each feeds the next; the last ends at a human design-confirm.
|
|
21
|
+
|
|
22
|
+
### 1 · review-domain
|
|
23
|
+
Start from the **domain**, not from a blank canvas. Read the domain model — entities, flows, the
|
|
24
|
+
ubiquitous language in `PROJECT.md` / `GLOSSARY.md` — and derive **which screens** the feature
|
|
25
|
+
needs and the **regions** each screen holds. Map each domain entity to a *presentational*
|
|
26
|
+
component (it shows state; it does not own a domain decision). The screen list + per-screen
|
|
27
|
+
regions are this beat's output, and they are grounded in the domain you just read.
|
|
28
|
+
|
|
29
|
+
### 2 · research-components (reuse before you invent)
|
|
30
|
+
Check `catalog.json` **first** and **reuse** the components already there — that is what keeps
|
|
31
|
+
screens consistent. Research a reference UI (a websearch over pattern galleries / component
|
|
32
|
+
libraries for this kind of screen) only to fill a **genuine gap**, and propose a **new** catalog
|
|
33
|
+
component for that gap with a **cited** reference. Reuse before invention; a new component is the
|
|
34
|
+
exception, named and justified, not the default.
|
|
35
|
+
|
|
36
|
+
### 3 · wireframe
|
|
37
|
+
Draw a **low-fi**, **structural** layout for each screen — regions and component slots, no
|
|
38
|
+
styling, no color. This is the cheapest possible artifact that shows the *expected layout*, so
|
|
39
|
+
the human can correct the structure before a pixel is styled. Confirm the wireframe, then move on.
|
|
40
|
+
|
|
41
|
+
### 4 · render-capture-confirm
|
|
42
|
+
Render the screen as a **self-contained HTML mock** — the project's component library via CDN,
|
|
43
|
+
bound to `tokens.json` (resolved to CSS variables), composed from the **reusable per-component
|
|
44
|
+
kit** (one token-bound partial per catalog component), populated with realistic **mock** data.
|
|
45
|
+
**Capture** a real image of it (a headless screenshot) and present that image to the human for
|
|
46
|
+
**design-confirm** — show-before-ask, **before build**. On confirm, record the layout back to
|
|
47
|
+
`prototypes/<name>.json` + `catalog.json`, save the captured image to
|
|
48
|
+
`.add/design/captures/<name>.<ext>`, and **attach or mention it in the feature's `TASK.md`**
|
|
49
|
+
(alongside the §6 evidence) — so the screen the human approved is traceable from the task that
|
|
50
|
+
builds it. The HTML mock is the *visible evidence*; the json-render tree is the
|
|
51
|
+
*machine-checkable* record of the same screen.
|
|
52
|
+
|
|
53
|
+
## Tool-agnostic capture
|
|
54
|
+
|
|
55
|
+
How you render and capture is **your** choice, not the engine's: a headless browser
|
|
56
|
+
(Playwright / Puppeteer), an `html2image`-style renderer, a browser-automation skill, a design
|
|
57
|
+
tool, or a hosted screenshot service — whatever the agent has. The recommended default is the
|
|
58
|
+
self-contained HTML mock above, captured headless, because it needs no app build yet still wears
|
|
59
|
+
the project's real tokens and component vocabulary. For a project that renders json-render, the
|
|
60
|
+
recommended default is **`@json-render/image`** (Satori → PNG/SVG, no browser) — a deterministic
|
|
61
|
+
`Spec` → image. The captured image is a **design-confirm evidence** artifact the human approves;
|
|
62
|
+
it is never an engine output, and the engine never renders. This keeps the loop tool-agnostic and
|
|
63
|
+
the method renderer-free.
|
|
64
|
+
|
|
65
|
+
Captures live at **`.add/design/captures/<name>.<ext>`** (one per prototype) and are
|
|
66
|
+
attached/mentioned in the feature's `TASK.md`. `add.py check` raises a never-red
|
|
67
|
+
`missing_capture` WARN for any prototype under `.add/design/prototypes/` that still lacks a
|
|
68
|
+
capture — a nudge to render + confirm it, never a blocker.
|
|
69
|
+
|
|
70
|
+
The loop **binds** the existing UDD contracts **read-only**: `tokens.json`, `catalog.json`, and
|
|
71
|
+
`prototypes/<name>.json` are read and composed, never reshaped — the `prototypes/<name>.json`
|
|
72
|
+
data contract stays **unchanged** (a change to it is a change request, not a design step). And
|
|
73
|
+
**identity** values — brand color, palette, typeface — stay **human-owned**: surface them for the
|
|
74
|
+
human to decide, never auto-pick a brand value (`udd-tokens.md`).
|
|
75
|
+
|
|
76
|
+
## The hard rules
|
|
77
|
+
|
|
78
|
+
<constraints>
|
|
79
|
+
- **Domain first.** A screen is derived from the domain (beat 1), never sketched blind.
|
|
80
|
+
- **Reuse before invent.** Beat 2 checks the catalog first; a new component is a justified,
|
|
81
|
+
cited exception — never the reflex.
|
|
82
|
+
- **Confirm before build.** The captured image is approved by the human *before* implementation;
|
|
83
|
+
a design-confirm placed at or after build defeats the loop.
|
|
84
|
+
- **The engine never renders.** Capture is a recommended, tool-agnostic recipe run by the
|
|
85
|
+
agent's own tools; the image is evidence, not an engine artifact.
|
|
86
|
+
- **Bind, don't break.** The loop reads `tokens.json` / `catalog.json` / `prototypes/<name>.json`
|
|
87
|
+
read-only; the data contract is unchanged, and identity values stay human-owned.
|
|
88
|
+
</constraints>
|
|
89
|
+
|
|
90
|
+
> Used at specify for a UI feature: `phases/0-setup.md` scaffolds `DESIGN.md`, and
|
|
91
|
+
> `phases/1-specify.md` points here when the feature has a screen — run the four beats, then
|
|
92
|
+
> carry the confirmed layout into the contract.
|
|
@@ -74,7 +74,8 @@ gate: auto-complete NEVER skips the human baseline approval — the `lock` (§4)
|
|
|
74
74
|
1. **Fill the living documentation** (it outlives all code): `.add/PROJECT.md` (the foundation — Domain · Spec/active
|
|
75
75
|
milestone · UI/UX · Key Decisions, one screen), `CONVENTIONS.md`, `GLOSSARY.md`, `MODEL_REGISTRY.md`,
|
|
76
76
|
`dependencies.allowlist`, and — for a UI project — `DESIGN.md` (the design source of truth: identity ·
|
|
77
|
-
principles · screens · the named-set foundation pointers + render recipe; delete it if there's no UI
|
|
77
|
+
principles · screens · the named-set foundation pointers + render recipe; delete it if there's no UI;
|
|
78
|
+
the design-definition loop that fills it — domain → components → wireframe → a captured screen confirmed before build — is `design.md`).
|
|
78
79
|
Brownfield: from the code. Greenfield: from the interview, gaps flagged `guessed`.
|
|
79
80
|
2. **Propose, then size it.** You just read the codebase (brownfield) or interviewed (greenfield) — so
|
|
80
81
|
don't silently draft. First float a **kickoff suggestion** for the first milestone the human reacts to:
|
|
@@ -18,7 +18,9 @@ understand the feature — that is information, not an obstacle. Stop and ask.
|
|
|
18
18
|
**Identity is direction, not default (UDD).** For UI/design work, identity values — the brand
|
|
19
19
|
color, the core palette, the typeface — are human-owned. Surface them for discussion during
|
|
20
20
|
Diverge; never assume a brand value. The UDD token dialect checks a token's *shape*; its *value*
|
|
21
|
-
is the user's call (`udd-tokens.md`).
|
|
21
|
+
is the user's call (`udd-tokens.md`). For a UI feature with a screen, run the design-definition
|
|
22
|
+
loop in `design.md` — domain → research and reuse components → wireframe → a real captured screen
|
|
23
|
+
the human confirms before build — and carry the confirmed layout into the contract.
|
|
22
24
|
|
|
23
25
|
## Produce (in TASK.md §1)
|
|
24
26
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Release — cut a versioned ship, never an unwatched flip
|
|
2
|
+
|
|
3
|
+
A project does not "release" because someone bumped a number. It releases when one or more
|
|
4
|
+
**closed milestones** are bundled into a versioned, user-facing cut whose notes are evidence-backed,
|
|
5
|
+
whose risk is disclosed, and whose behaviour is then watched. This guide is the **5th scope level** —
|
|
6
|
+
after setup (`phases/0-setup.md`), intake (`intake.md` / `scope.md`), the milestone loop (`loop.md`),
|
|
7
|
+
and stage graduation (`graduate.md`). It is a different verb from each of them:
|
|
8
|
+
|
|
9
|
+
- a **milestone** is *feature-complete and consolidated*; a **release** is *shipped + watched*.
|
|
10
|
+
- **graduation** changes the project's *rigor* (mvp→production); a **release** ships a *version*.
|
|
11
|
+
The axes are orthogonal — you cut releases at every stage (prototype preview · mvp beta · prod GA).
|
|
12
|
+
|
|
13
|
+
So a release is its own scope level: it bundles **≥1** closed milestone, and it may bundle several.
|
|
14
|
+
You (the AI) **gather and propose**; the **human confirms and judges**; the engine records the cut
|
|
15
|
+
and enforces a floor — it **never tags, publishes, or deploys** (the same stance as "the engine never
|
|
16
|
+
renders" in `design.md` and "never spawns" in `advisor.md`). The outward act is the human's.
|
|
17
|
+
|
|
18
|
+
## The cue (what starts this)
|
|
19
|
+
|
|
20
|
+
When ≥1 milestone is `done` AND archived AND not yet attributed to a release, `add.py status` prints:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
→ releasable: N milestone(s) closed since last release
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That line is the trigger. It is a **tally** over unreleased-but-archived milestones — never a
|
|
27
|
+
readiness judgment. It is silent until the first such milestone exists (a project that has never
|
|
28
|
+
released, or has released everything, sees nothing here — grandfathered, zero change).
|
|
29
|
+
|
|
30
|
+
## The flow
|
|
31
|
+
|
|
32
|
+
One arc, seven steps: **cue → gather → draft notes → readiness floor → human confirms → cut → watch.**
|
|
33
|
+
|
|
34
|
+
1. **Gather the release inventory** — run `add.py release-report` (add `--json` to branch on it). It
|
|
35
|
+
clusters the cut's evidence into labeled record-sets: the closed milestones since the last release ·
|
|
36
|
+
their **consolidated deltas** (the "what changed" record) · open RISK-ACCEPTED waivers riding into
|
|
37
|
+
the release · any open **security HARD-STOP** (a blocker) · the §2 scenarios to take live as monitors.
|
|
38
|
+
It **gathers, never judges** — there is no readiness verdict to read; the records are what you reason from.
|
|
39
|
+
2. **Draft the release notes** — write a [Keep a Changelog](https://keepachangelog.com/) entry **from the
|
|
40
|
+
consolidated deltas + each milestone's goal** (reuse — those consolidated deltas ARE the changelog source;
|
|
41
|
+
you are not inventing the story, you are surfacing what the foundation already recorded). Group Added / Changed /
|
|
42
|
+
Fixed; name the headline capabilities concretely. Propose the **semver bump** — breaking→MAJOR,
|
|
43
|
+
feature→MINOR, fix-only→PATCH — and let the human confirm it (the version is a decision, not a default).
|
|
44
|
+
3. **Meet the readiness floor** — before the cut the engine enforces a floor (see below): the suite is
|
|
45
|
+
green, **zero** open security HARD-STOP, and every RISK-ACCEPTED waiver shipping in this release is
|
|
46
|
+
signed AND disclosed in the notes. A security finding is a HARD-STOP here exactly as in verify —
|
|
47
|
+
never auto-passed, never shipped silent.
|
|
48
|
+
4. **Human confirms the cut** — present via `report-template.md`, opening with the ARC (goal · done · plan):
|
|
49
|
+
the version you are shipping, the milestones + evidence that earn it, and the rollout + watch plan that
|
|
50
|
+
follows. Show the drafted notes, the version, and the waivers being shipped. The human approves once
|
|
51
|
+
(the decision point) — never pre-stamped; you surface a summary to decide on, not the artifact itself.
|
|
52
|
+
5. **Cut — record the marker** — only now run `add.py release <version> --notes <file>`. The engine
|
|
53
|
+
**records**: it prepends the CHANGELOG entry, stamps one append-only row (newest-first) in
|
|
54
|
+
`RELEASES.md` (date · version · milestones · waivers shipped · evidence), and attributes the bundled
|
|
55
|
+
milestones to this version (so the cue stops firing for them).
|
|
56
|
+
6. **Ship — the human's outward act** — the engine has recorded the cut; the **human runs the tag /
|
|
57
|
+
publish / deploy** (`git tag`, `npm publish`, the deploy pipeline — tool-agnostic, whatever this project
|
|
58
|
+
ships through). The tag is the human-gated trigger, exactly the dogfooded recipe today. The engine never
|
|
59
|
+
performs it: design-for-failure lives in the pipeline the human owns, not in the method tool.
|
|
60
|
+
7. **Watch — re-enter observe at the release scope level** — the §2 scenarios become live monitors for the
|
|
61
|
+
*released* version; live-registry / deploy confirmation is post-cut **evidence**, not a unit test. A
|
|
62
|
+
regression found in the wild re-enters at Specify as a **change request** → a narrowed **PATCH hotfix
|
|
63
|
+
release** (this same flow, scoped to the fix). Release is not the finish line — it is where the most
|
|
64
|
+
reliable information appears (`phases/7-observe.md`).
|
|
65
|
+
|
|
66
|
+
## The floor (what the engine enforces)
|
|
67
|
+
|
|
68
|
+
`add.py release <version>` is **guarded** — it refuses (non-zero exit, state byte-unchanged) on:
|
|
69
|
+
|
|
70
|
+
<reject_codes>
|
|
71
|
+
- `release_security_open` — an open security HARD-STOP exists. The non-negotiable; a security finding is
|
|
72
|
+
never shipped. Resolve it (a change request back to Specify) before the cut. `--force` does NOT override this.
|
|
73
|
+
- `release_tests_red` — the suite is not green. Evidence, not a plausible diff, is what a release ships on.
|
|
74
|
+
- `release_no_closed_milestone` — nothing new since the last release. The cut is a no-op; do **not** bump.
|
|
75
|
+
- `release_undisclosed_waiver` — a RISK-ACCEPTED waiver rides into the release but is absent from the notes.
|
|
76
|
+
Disclosure is the floor: a shipped risk the user can't read about is a hidden risk. Add it to the notes.
|
|
77
|
+
</reject_codes>
|
|
78
|
+
|
|
79
|
+
`--force` preserves human authority for grandfathered / edge cases (e.g. a first release of a brownfield
|
|
80
|
+
adopt), mirroring `stage --force` — but it never overrides `release_security_open`. Use it deliberately,
|
|
81
|
+
not as the normal path.
|
|
82
|
+
|
|
83
|
+
## Invariants (never break these)
|
|
84
|
+
|
|
85
|
+
- **The engine records; the human ships.** `add.py release` writes the CHANGELOG + ledger + attribution;
|
|
86
|
+
it never tags, publishes, or deploys. The outward act stays human-owned and tool-agnostic.
|
|
87
|
+
- **Security is a HARD-STOP at the cut**, not just at verify. No `--force`, no waiver, no exception.
|
|
88
|
+
- **Notes draw from consolidated deltas** — release after `fold.md` has run, so the changelog surfaces
|
|
89
|
+
consolidated learnings, not raw open lessons. The lifecycle order is one line:
|
|
90
|
+
`milestone-done → fold → compact → archive → (repeat ≥1×) → release → watch`.
|
|
91
|
+
- **The ledger is append-only (newest-first)** — like §Key Decisions, a release row is never rewritten;
|
|
92
|
+
a superseded or yanked version is recorded with a new row, never edited away.
|
|
93
|
+
- **A release bundles, it does not equal.** One version may attribute several milestones; never force a
|
|
94
|
+
release per milestone.
|
|
95
|
+
|
|
96
|
+
## Depth and reuse
|
|
97
|
+
|
|
98
|
+
The shape is constant; the depth follows the stage (read it from `add.py status`):
|
|
99
|
+
|
|
100
|
+
- **prototype / poc** — a one-line preview note + a tag; no deploy ceremony. The point is feedback, not GA.
|
|
101
|
+
- **mvp** — full notes + tag + a guarded publish; watch the headline scenarios.
|
|
102
|
+
- **production** — every step at full rigor: notes + tag + deploy behind a rollback-tested pipeline +
|
|
103
|
+
live scenario monitors + error-budget watch. The hotfix path (step 7) is first-class here.
|
|
104
|
+
|
|
105
|
+
## Worked example (this method's own 1.5.0)
|
|
106
|
+
|
|
107
|
+
The repo already runs this by hand. The `udd-design-loop` milestone closed (4/4) and consolidated into
|
|
108
|
+
`foundation-version 33`; the human then drafted the `## [1.5.0]` CHANGELOG entry from those deltas,
|
|
109
|
+
bumped the three version sources in lockstep, and the forward-pinned `test_release_1_5_0.py` asserted
|
|
110
|
+
in-repo readiness (versions agree · changelog lineage survives · feature anchors named · engine
|
|
111
|
+
untouched). The cut itself — the `git tag` that triggers the npm/PyPI publish — stayed human-gated, and
|
|
112
|
+
the live-registry confirmation was gathered *after* the tag as verify evidence, never a unit test. This
|
|
113
|
+
guide makes that ritual first-class: `release-report` gathers it, the floor enforces it, `add.py release`
|
|
114
|
+
records it, and the human still owns the tag.
|
package/tooling/add.py
CHANGED
|
@@ -36,6 +36,14 @@ STAGES = ("prototype", "poc", "mvp", "production")
|
|
|
36
36
|
# v22 stage-graduation: the read-only cue `status` shows when the MVP is covered.
|
|
37
37
|
# Worded as the ACTION (never a file) so it stands before graduate.md exists.
|
|
38
38
|
GRADUATION_CUE = "MVP covered → propose graduation"
|
|
39
|
+
# release-altitude: the read-only cue `status` shows when ≥1 closed milestone is
|
|
40
|
+
# unreleased. The 5th scope level (release.md). `{n}` is filled at print time; the
|
|
41
|
+
# wording matches SKILL.md's "Beyond the bundle" cross-ref byte-for-byte.
|
|
42
|
+
RELEASABLE_CUE = "releasable: {n} milestone(s) closed since last release"
|
|
43
|
+
# the append-only release ledger lives at the PROJECT ROOT (the dir containing .add/),
|
|
44
|
+
# a sibling of CHANGELOG.md — NOT inside .add/. The ledger IS the attribution source:
|
|
45
|
+
# a milestone is "released" iff its slug appears on a `milestones:` row.
|
|
46
|
+
RELEASES_FILE = "RELEASES.md"
|
|
39
47
|
PHASES = ("ground", "specify", "scenarios", "contract", "tests", "build", "verify", "observe", "done")
|
|
40
48
|
GATES = ("none", "PASS", "RISK-ACCEPTED", "HARD-STOP")
|
|
41
49
|
# heal-then-escalate (verify-integrity): the bounded self-heal loop cap. A CONFIRMED cheat
|
|
@@ -1055,6 +1063,14 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
1055
1063
|
print(f"archived: {n} milestone{'s' if n != 1 else ''} "
|
|
1056
1064
|
f"({m_tasks} task{'s' if m_tasks != 1 else ''})")
|
|
1057
1065
|
|
|
1066
|
+
# release cue (release-altitude): project-global + read-only. Fires when ≥1 CLOSED
|
|
1067
|
+
# milestone (live-done OR archived) is not yet attributed to a RELEASES.md row — so it
|
|
1068
|
+
# stands even with no live milestones. Additive: a line solely when releasable; the
|
|
1069
|
+
# ledger read is fail-open (a vanished ledger never silences the cue). See release.md.
|
|
1070
|
+
_rel = _releasable(root, state)
|
|
1071
|
+
if _rel:
|
|
1072
|
+
print(f" → {RELEASABLE_CUE.format(n=len(_rel))}")
|
|
1073
|
+
|
|
1058
1074
|
print(f"active : {active or '(none)'}")
|
|
1059
1075
|
# surface the active task's autonomy level (task explicit-autonomy-dial) so the human
|
|
1060
1076
|
# reads the throttle every session; "unset" when no explicit `autonomy:` line is present.
|
|
@@ -1606,6 +1622,27 @@ def _udd_named_set_checks(root: Path) -> list[tuple[bool, str, str]]:
|
|
|
1606
1622
|
return out
|
|
1607
1623
|
|
|
1608
1624
|
|
|
1625
|
+
_CAPTURE_EXTS = ("png", "svg", "jpg", "jpeg", "webp")
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
def _missing_captures(root: Path) -> list[str]:
|
|
1629
|
+
"""Prototype names under `.add/design/prototypes/` lacking a design-confirm capture.
|
|
1630
|
+
|
|
1631
|
+
A prototype `<name>.json` is CAPTURED iff a file `.add/design/captures/<name>.<ext>`
|
|
1632
|
+
exists (ext in _CAPTURE_EXTS). Returns the uncaptured names in document (sorted) order.
|
|
1633
|
+
PURE · TOTAL (missing dirs -> []) · READ-ONLY (never writes, never renders): the engine
|
|
1634
|
+
MEASURES capture presence; producing the image is the agent's tool-agnostic choice
|
|
1635
|
+
(design.md beat 4; default `@json-render/image`). [] == every prototype captured / none exist.
|
|
1636
|
+
"""
|
|
1637
|
+
proto_dir = root / "design" / "prototypes"
|
|
1638
|
+
cap_dir = root / "design" / "captures"
|
|
1639
|
+
if not proto_dir.is_dir():
|
|
1640
|
+
return []
|
|
1641
|
+
names = sorted(p.stem for p in proto_dir.glob("*.json") if p.is_file())
|
|
1642
|
+
return [n for n in names
|
|
1643
|
+
if not any((cap_dir / f"{n}.{ext}").is_file() for ext in _CAPTURE_EXTS)]
|
|
1644
|
+
|
|
1645
|
+
|
|
1609
1646
|
def cmd_check(args: argparse.Namespace) -> None:
|
|
1610
1647
|
"""Read-only integrity check of the .add project. Exit 1 if anything fails."""
|
|
1611
1648
|
as_json = getattr(args, "json", False)
|
|
@@ -1772,6 +1809,16 @@ def cmd_check(args: argparse.Namespace) -> None:
|
|
|
1772
1809
|
# Silent when absent; read-only; fail-closed on malformed JSON.
|
|
1773
1810
|
checks.extend(_udd_named_set_checks(root))
|
|
1774
1811
|
|
|
1812
|
+
# capture-evidence: a never-red WARN naming each prototype with no design-confirm capture
|
|
1813
|
+
# at .add/design/captures/<name>.<ext>. Measure-never-block — rides `warnings`, NEVER
|
|
1814
|
+
# `checks` (so never feeds `failed`); silent-when-absent (no prototypes -> []). The engine
|
|
1815
|
+
# MEASURES capture presence; producing the image is the agent's tool-agnostic choice.
|
|
1816
|
+
for _pname in _missing_captures(root):
|
|
1817
|
+
warnings.append(("missing_capture",
|
|
1818
|
+
f"prototype '{_pname}' has no design-confirm capture at "
|
|
1819
|
+
f".add/design/captures/{_pname}.<png|svg|…> — render + confirm it "
|
|
1820
|
+
"before build (design.md beat 4)"))
|
|
1821
|
+
|
|
1775
1822
|
passed = sum(1 for ok, _, _ in checks if ok)
|
|
1776
1823
|
failed = len(checks) - passed
|
|
1777
1824
|
if as_json:
|
|
@@ -4082,6 +4129,296 @@ def cmd_graduation_report(args: argparse.Namespace) -> None:
|
|
|
4082
4129
|
print("\n".join(L))
|
|
4083
4130
|
|
|
4084
4131
|
|
|
4132
|
+
def _releases_path(root: Path) -> Path:
|
|
4133
|
+
"""The append-only release ledger — at the PROJECT ROOT (root IS the .add dir, so its
|
|
4134
|
+
parent), a sibling of CHANGELOG.md. NOT inside .add/."""
|
|
4135
|
+
return root.parent / RELEASES_FILE
|
|
4136
|
+
|
|
4137
|
+
|
|
4138
|
+
def _released_milestones(root: Path) -> set[str]:
|
|
4139
|
+
"""Slugs already attributed to a release — the union of every `milestones:` row in
|
|
4140
|
+
RELEASES.md. Fail-OPEN: a missing/unreadable/malformed ledger yields the empty set, so
|
|
4141
|
+
every closed milestone reads as still-releasable (a vanished ledger never hides work).
|
|
4142
|
+
READ-ONLY."""
|
|
4143
|
+
try:
|
|
4144
|
+
text = _releases_path(root).read_text(encoding="utf-8")
|
|
4145
|
+
except OSError:
|
|
4146
|
+
return set() # no ledger (or a dir at the path) -> nothing released yet
|
|
4147
|
+
out: set[str] = set()
|
|
4148
|
+
for line in text.splitlines():
|
|
4149
|
+
st = line.strip()
|
|
4150
|
+
if st.lower().startswith("milestones:"):
|
|
4151
|
+
for tok in re.split(r"[,\s]+", st.split(":", 1)[1]):
|
|
4152
|
+
tok = tok.strip()
|
|
4153
|
+
if tok and tok.lower() != "none":
|
|
4154
|
+
out.add(tok)
|
|
4155
|
+
return out
|
|
4156
|
+
|
|
4157
|
+
|
|
4158
|
+
def _closed_milestones(state: dict) -> list[dict]:
|
|
4159
|
+
"""Every CLOSED milestone (its milestone-done gate passed): LIVE done milestones
|
|
4160
|
+
(status == 'done', still in state) + ARCHIVED milestones (all were PASS-done before
|
|
4161
|
+
archive — see _archived_task_slugs). Each: {slug, title, tier}."""
|
|
4162
|
+
out: list[dict] = []
|
|
4163
|
+
for slug, m in (state.get("milestones") or {}).items():
|
|
4164
|
+
if m.get("status") == "done":
|
|
4165
|
+
out.append({"slug": slug, "title": m.get("title", slug), "tier": "live"})
|
|
4166
|
+
for rec in state.get("archived") or []:
|
|
4167
|
+
if rec.get("slug"):
|
|
4168
|
+
out.append({"slug": rec["slug"], "title": rec.get("title", rec["slug"]),
|
|
4169
|
+
"tier": "archived"})
|
|
4170
|
+
return out
|
|
4171
|
+
|
|
4172
|
+
|
|
4173
|
+
def _releasable(root: Path, state: dict) -> list[dict]:
|
|
4174
|
+
"""Closed milestones NOT yet attributed to any RELEASES.md row — the cut's candidate
|
|
4175
|
+
bundle. Drives BOTH the `→ releasable: N` status cue and release-report. READ-ONLY."""
|
|
4176
|
+
released = _released_milestones(root)
|
|
4177
|
+
return [m for m in _closed_milestones(state) if m["slug"] not in released]
|
|
4178
|
+
|
|
4179
|
+
|
|
4180
|
+
def _key_decisions_for(root: Path, slug: str) -> list[str]:
|
|
4181
|
+
"""Best-effort §Key-Decisions rows from PROJECT.md that NAME this milestone slug — the
|
|
4182
|
+
consolidated decisions the changelog can cite. Fail-open: a missing section / unreadable
|
|
4183
|
+
foundation / no slug match -> [] (a gather never raises). READ-ONLY."""
|
|
4184
|
+
try:
|
|
4185
|
+
text = (root / "PROJECT.md").read_text(encoding="utf-8")
|
|
4186
|
+
except OSError:
|
|
4187
|
+
return []
|
|
4188
|
+
m = re.search(r"^#{1,6}[^\n]*key decision[^\n]*$(.*?)(?=^#{1,6}\s|\Z)", text, re.S | re.M | re.I)
|
|
4189
|
+
if not m:
|
|
4190
|
+
return []
|
|
4191
|
+
return [st.lstrip("-* ").strip() for st in (ln.strip() for ln in m.group(1).splitlines())
|
|
4192
|
+
if st.startswith(("-", "*")) and slug in st]
|
|
4193
|
+
|
|
4194
|
+
|
|
4195
|
+
def release_data(root: Path, state: dict) -> dict:
|
|
4196
|
+
"""The single source of FACTS for a release cut — PURE, NO writes (mirrors graduation_data).
|
|
4197
|
+
Both the `release-report` text dashboard and `--json` render from this one dict, so the human
|
|
4198
|
+
view and the machine view can never disagree.
|
|
4199
|
+
|
|
4200
|
+
GATHER, never JUDGE: every value is a RECORD the human verifies by looking; there is no
|
|
4201
|
+
readiness/score/ranking field by construction. Five record-sets feed the release.md flow:
|
|
4202
|
+
releasable — closed-but-unreleased milestones (the bundle candidate; the cue's count)
|
|
4203
|
+
changed — per releasable milestone: RETRO path + carried-delta count + §Key-Decisions rows
|
|
4204
|
+
waivers — open RISK-ACCEPTED riding into the cut (soonest expiry first)
|
|
4205
|
+
blockers — open HARD-STOP gate records (the security stop the floor will refuse on)
|
|
4206
|
+
monitors — declared §7 Watch lines to carry into the post-cut watch step
|
|
4207
|
+
A source is read fail-closed (skip on error); the ledger is read fail-OPEN (see _releasable)."""
|
|
4208
|
+
tasks = state.get("tasks") or {}
|
|
4209
|
+
releasable = _releasable(root, state)
|
|
4210
|
+
|
|
4211
|
+
# changed — the consolidated learning trail per releasable milestone (the changelog source)
|
|
4212
|
+
changed = []
|
|
4213
|
+
for m in releasable:
|
|
4214
|
+
slug = m["slug"]
|
|
4215
|
+
retro = None
|
|
4216
|
+
for sub in ("milestones", "archive"):
|
|
4217
|
+
cand = root / sub / slug / "RETRO.md"
|
|
4218
|
+
if cand.is_file(): # a directory at the path is not a ledger (fail-closed)
|
|
4219
|
+
retro = str(cand.relative_to(root))
|
|
4220
|
+
break
|
|
4221
|
+
changed.append({"milestone": slug, "key_decisions": _key_decisions_for(root, slug),
|
|
4222
|
+
"retro": retro,
|
|
4223
|
+
"carried_deltas": _retro_carried(root / retro) if retro else 0})
|
|
4224
|
+
|
|
4225
|
+
# waivers — open RISK-ACCEPTED riding into the cut, soonest expiry first (mirrors graduation_data)
|
|
4226
|
+
waivers = []
|
|
4227
|
+
for slug, t in tasks.items():
|
|
4228
|
+
if t.get("gate") == "RISK-ACCEPTED" and t.get("waiver"):
|
|
4229
|
+
w = t["waiver"]
|
|
4230
|
+
waivers.append({"slug": slug, "owner": w.get("owner", "?"),
|
|
4231
|
+
"ticket": w.get("ticket", "?"), "expires": w.get("expires", "?")})
|
|
4232
|
+
|
|
4233
|
+
def _exp_key(wv):
|
|
4234
|
+
try:
|
|
4235
|
+
return (0, date.fromisoformat(wv["expires"]).isoformat())
|
|
4236
|
+
except (ValueError, TypeError):
|
|
4237
|
+
return (1, "") # unparseable/missing -> after every real date
|
|
4238
|
+
waivers.sort(key=_exp_key)
|
|
4239
|
+
|
|
4240
|
+
# blockers — open HARD-STOP gate records (the un-forceable security stop the floor enforces)
|
|
4241
|
+
blockers = [{"slug": s, "gate": t.get("gate")} for s, t in tasks.items()
|
|
4242
|
+
if t.get("gate") == "HARD-STOP"]
|
|
4243
|
+
|
|
4244
|
+
# monitors — declared §7 Watch lines (filled, not the `<…>` template) for the watch step
|
|
4245
|
+
monitors = []
|
|
4246
|
+
for slug in tasks:
|
|
4247
|
+
try:
|
|
4248
|
+
text = (root / "tasks" / slug / "TASK.md").read_text(encoding="utf-8")
|
|
4249
|
+
except OSError:
|
|
4250
|
+
continue # unreadable TASK.md -> skip this task's monitor record
|
|
4251
|
+
for line in text.splitlines():
|
|
4252
|
+
st = line.strip()
|
|
4253
|
+
if st.startswith("Watch") and "<" not in st and st != "Watch":
|
|
4254
|
+
monitors.append({"slug": slug, "watch": st})
|
|
4255
|
+
break
|
|
4256
|
+
|
|
4257
|
+
return {
|
|
4258
|
+
"releasable": releasable,
|
|
4259
|
+
"changed": changed,
|
|
4260
|
+
"waivers": waivers,
|
|
4261
|
+
"blockers": blockers,
|
|
4262
|
+
"monitors": monitors,
|
|
4263
|
+
"summary": {
|
|
4264
|
+
"releasable": len(releasable), "changed": len(changed), "waivers": len(waivers),
|
|
4265
|
+
"blockers": len(blockers), "monitors": len(monitors),
|
|
4266
|
+
},
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
|
|
4270
|
+
def cmd_release_report(args: argparse.Namespace) -> None:
|
|
4271
|
+
"""Read-only: GATHER the release inventory into five labeled record-sets for the release.md
|
|
4272
|
+
flow. text (default) or --json (the frozen JSON facts interface). Exit 0 ALWAYS — a gather,
|
|
4273
|
+
not a gate; the ONLY non-zero exit is no_project. Judges nothing. NO writes."""
|
|
4274
|
+
root = find_root()
|
|
4275
|
+
if root is None: # frozen contract: fail-closed with a no_project signal
|
|
4276
|
+
_die("no_project: no .add/ project found. Run `add.py init` first.")
|
|
4277
|
+
state = load_state(root)
|
|
4278
|
+
d = release_data(root, state)
|
|
4279
|
+
|
|
4280
|
+
if getattr(args, "json", False):
|
|
4281
|
+
print(json.dumps(d, ensure_ascii=False, indent=2))
|
|
4282
|
+
return
|
|
4283
|
+
|
|
4284
|
+
s = d["summary"]
|
|
4285
|
+
L = ["RELEASE REPORT — release inventory (gather, not judge)", ""]
|
|
4286
|
+
L.append(f"Releasable ({s['releasable']}) — closed milestones not yet in {RELEASES_FILE}:")
|
|
4287
|
+
for m in d["releasable"]:
|
|
4288
|
+
L.append(f" - {m['slug']} [{m['tier']}]: {m['title']}")
|
|
4289
|
+
L.append("")
|
|
4290
|
+
L.append(f"Changed ({s['changed']}) — the consolidated learning trail per milestone:")
|
|
4291
|
+
for c in d["changed"]:
|
|
4292
|
+
L.append(f" - {c['milestone']}: {c['retro'] or '(no RETRO record)'} "
|
|
4293
|
+
f"({c['carried_deltas']} carried · {len(c['key_decisions'])} key decision(s))")
|
|
4294
|
+
L.append("")
|
|
4295
|
+
L.append(f"Waivers ({s['waivers']}) — open RISK-ACCEPTED riding into the cut, soonest expiry first:")
|
|
4296
|
+
for w in d["waivers"]:
|
|
4297
|
+
L.append(f" - {w['slug']}: {w['owner']} · {w['ticket']} · expires {w['expires']}")
|
|
4298
|
+
L.append("")
|
|
4299
|
+
L.append(f"Blockers ({s['blockers']}) — open HARD-STOP (the un-forceable security stop):")
|
|
4300
|
+
for b in d["blockers"]:
|
|
4301
|
+
L.append(f" - {b['slug']}: {b['gate']}")
|
|
4302
|
+
L.append("")
|
|
4303
|
+
L.append(f"Monitors ({s['monitors']}) — declared §7 Watch lines to carry into the watch step:")
|
|
4304
|
+
for mo in d["monitors"]:
|
|
4305
|
+
L.append(f" - {mo['slug']}: {mo['watch']}")
|
|
4306
|
+
print("\n".join(L))
|
|
4307
|
+
|
|
4308
|
+
|
|
4309
|
+
def _build_in_flight(state: dict) -> bool:
|
|
4310
|
+
"""release_tests_red proxy (PURE): is any ACTIVE task mid-build without a recorded green gate
|
|
4311
|
+
— phase ∈ {build, verify} AND gate == 'none'? The tool-agnostic engine never runs the suite,
|
|
4312
|
+
so an entered-but-ungated build is the recorded-evidence stand-in for 'the suite is red'."""
|
|
4313
|
+
return any(t.get("phase") in ("build", "verify") and t.get("gate") == "none"
|
|
4314
|
+
for t in (state.get("tasks") or {}).values())
|
|
4315
|
+
|
|
4316
|
+
|
|
4317
|
+
def _prepend_block(existing: str, header: str, block: str) -> str:
|
|
4318
|
+
"""Newest-first prepend: insert `block` directly under the top H1 `header`, creating the
|
|
4319
|
+
header when `existing` is empty / headerless. Existing content is preserved VERBATIM
|
|
4320
|
+
(append-only). `block` is expected to end in a blank-line separator."""
|
|
4321
|
+
if not existing.strip():
|
|
4322
|
+
return f"{header}\n\n{block}"
|
|
4323
|
+
if existing.lstrip().startswith(header):
|
|
4324
|
+
after = existing.split(header, 1)[1].lstrip("\n")
|
|
4325
|
+
return f"{header}\n\n{block}{after}"
|
|
4326
|
+
return f"{block}{existing}" # no recognized header -> block goes on top, verbatim tail
|
|
4327
|
+
|
|
4328
|
+
|
|
4329
|
+
def _render_changelog_block(version: str, day: str, bundle: list[dict],
|
|
4330
|
+
changed_by_slug: dict) -> str:
|
|
4331
|
+
"""A CHANGELOG block: `## <version> — <date>` + one bullet per bundled milestone (title +
|
|
4332
|
+
carried-delta / key-decision counts from release_data['changed'])."""
|
|
4333
|
+
lines = [f"## {version} — {day}", ""]
|
|
4334
|
+
if bundle:
|
|
4335
|
+
for m in bundle:
|
|
4336
|
+
c = changed_by_slug.get(m["slug"], {})
|
|
4337
|
+
lines.append(f"- {m['title']} — {c.get('carried_deltas', 0)} carried · "
|
|
4338
|
+
f"{len(c.get('key_decisions', []))} key decision(s)")
|
|
4339
|
+
else:
|
|
4340
|
+
lines.append("- (no milestone bundled)")
|
|
4341
|
+
return "\n".join(lines) + "\n\n"
|
|
4342
|
+
|
|
4343
|
+
|
|
4344
|
+
def _render_releases_row(version: str, day: str, bundle: list[dict],
|
|
4345
|
+
waiver_slugs: list[str], evidence: str | None) -> str:
|
|
4346
|
+
"""One append-only RELEASES.md row — the attribution source (`milestones:` membership)."""
|
|
4347
|
+
ms = ", ".join(m["slug"] for m in bundle) if bundle else "none"
|
|
4348
|
+
wv = ", ".join(waiver_slugs) if waiver_slugs else "none"
|
|
4349
|
+
return (f"## {version} — {day}\n"
|
|
4350
|
+
f"milestones: {ms}\n"
|
|
4351
|
+
f"waivers: {wv}\n"
|
|
4352
|
+
f"evidence: {evidence or 'recorded by add.py release'}\n\n")
|
|
4353
|
+
|
|
4354
|
+
|
|
4355
|
+
def cmd_release(args: argparse.Namespace) -> None:
|
|
4356
|
+
"""GUARDED, record-only: cut a version. Enforce the 4-code readiness floor, then RECORD by
|
|
4357
|
+
prepending CHANGELOG.md + an append-only RELEASES.md row (whose `milestones:` line attributes
|
|
4358
|
+
the bundle). The engine RECORDS; it NEVER tags / publishes / deploys / bumps a version source /
|
|
4359
|
+
writes state.json. Validate-before-write: a reject leaves both files + state.json byte-unchanged.
|
|
4360
|
+
A failed second write rolls back the first (release_write_failed)."""
|
|
4361
|
+
root = find_root()
|
|
4362
|
+
if root is None: # frozen contract: fail-closed with a no_project signal
|
|
4363
|
+
_die("no_project: no .add/ project found. Run `add.py init` first.")
|
|
4364
|
+
state = load_state(root)
|
|
4365
|
+
d = release_data(root, state)
|
|
4366
|
+
forced = getattr(args, "force", False)
|
|
4367
|
+
disclosed = getattr(args, "with_waivers", False)
|
|
4368
|
+
|
|
4369
|
+
# ── FLOOR — all checks BEFORE any write (validate-before-write) ──────────────────────────
|
|
4370
|
+
if d["blockers"]: # the UN-FORCEABLE reject — security is never shipped
|
|
4371
|
+
_die("release_security_open: an open HARD-STOP blocks the cut — a security finding is "
|
|
4372
|
+
"never shipped. Resolve it (a change request back to Specify) before releasing. "
|
|
4373
|
+
"--force does NOT override this.")
|
|
4374
|
+
if not forced and _build_in_flight(state):
|
|
4375
|
+
_die("release_tests_red: a build is in flight without a recorded green gate — finish and "
|
|
4376
|
+
"gate it first, or pass --force to override.")
|
|
4377
|
+
bundle = _releasable(root, state)
|
|
4378
|
+
if not forced and not bundle:
|
|
4379
|
+
_die("release_no_closed_milestone: nothing closed-and-unreleased to bundle — the cut "
|
|
4380
|
+
"would be a no-op. Close a milestone first, or pass --force to override.")
|
|
4381
|
+
if not forced and d["waivers"] and not disclosed:
|
|
4382
|
+
_die("release_undisclosed_waiver: a RISK-ACCEPTED waiver rides into this release — pass "
|
|
4383
|
+
"--with-waivers to disclose it in the notes, or --force to override.")
|
|
4384
|
+
|
|
4385
|
+
# ── RECORD — build both contents in memory, then write CHANGELOG, then RELEASES (commit) ──
|
|
4386
|
+
day = date.today().isoformat()
|
|
4387
|
+
changed_by_slug = {c["milestone"]: c for c in d["changed"]}
|
|
4388
|
+
waiver_slugs = [w["slug"] for w in d["waivers"]] if disclosed else []
|
|
4389
|
+
changelog_path = root.parent / "CHANGELOG.md"
|
|
4390
|
+
releases_path = _releases_path(root)
|
|
4391
|
+
cl_before = changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
|
|
4392
|
+
rel_before = releases_path.read_text(encoding="utf-8") if releases_path.exists() else ""
|
|
4393
|
+
new_cl = _prepend_block(cl_before or "", "# Changelog",
|
|
4394
|
+
_render_changelog_block(args.version, day, bundle, changed_by_slug))
|
|
4395
|
+
new_rel = _prepend_block(rel_before, "# Releases",
|
|
4396
|
+
_render_releases_row(args.version, day, bundle, waiver_slugs,
|
|
4397
|
+
getattr(args, "evidence", None)))
|
|
4398
|
+
_atomic_write(changelog_path, new_cl)
|
|
4399
|
+
try:
|
|
4400
|
+
_atomic_write(releases_path, new_rel) # the attribution commit point
|
|
4401
|
+
except OSError as e:
|
|
4402
|
+
if cl_before is not None: # ROLLBACK (design-for-failure)
|
|
4403
|
+
_atomic_write(changelog_path, cl_before)
|
|
4404
|
+
else:
|
|
4405
|
+
try:
|
|
4406
|
+
changelog_path.unlink()
|
|
4407
|
+
except OSError:
|
|
4408
|
+
pass
|
|
4409
|
+
_die(f"release_write_failed: the ledger write failed ({e}); CHANGELOG was rolled back — "
|
|
4410
|
+
"nothing was recorded. Retry the release.")
|
|
4411
|
+
|
|
4412
|
+
# NO save_state — attribution lives in RELEASES.md (the cue re-reads it), never state.json
|
|
4413
|
+
ms = ", ".join(m["slug"] for m in bundle) if bundle else "none"
|
|
4414
|
+
print(f"released {args.version} — recorded {len(bundle)} milestone(s): {ms}")
|
|
4415
|
+
print(" CHANGELOG.md + RELEASES.md updated (project root). The engine records; "
|
|
4416
|
+
"you run the tag / publish / deploy.")
|
|
4417
|
+
if forced:
|
|
4418
|
+
print(" (--force: forceable floor rejects were bypassed — release_security_open is never bypassable)")
|
|
4419
|
+
print(_next_footer(root, state))
|
|
4420
|
+
|
|
4421
|
+
|
|
4085
4422
|
def cmd_deltas(args: argparse.Namespace) -> None:
|
|
4086
4423
|
"""Read-only: report all open lessons learned grouped by competency.
|
|
4087
4424
|
|
|
@@ -4394,6 +4731,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
4394
4731
|
pgr.add_argument("--plain", action="store_true", help="ASCII/pipe-safe text (output is plain by default)")
|
|
4395
4732
|
pgr.set_defaults(func=cmd_graduation_report)
|
|
4396
4733
|
|
|
4734
|
+
prr = sub.add_parser("release-report",
|
|
4735
|
+
help="read-only: gather the release inventory (releasable milestones · "
|
|
4736
|
+
"changed/RETROs · waivers · HARD-STOP blockers · monitors) for a "
|
|
4737
|
+
"release cut — gathers, never judges")
|
|
4738
|
+
prr.add_argument("--json", action="store_true", help="emit the frozen JSON facts interface")
|
|
4739
|
+
prr.add_argument("--plain", action="store_true", help="ASCII/pipe-safe text (output is plain by default)")
|
|
4740
|
+
prr.set_defaults(func=cmd_release_report)
|
|
4741
|
+
|
|
4742
|
+
prl = sub.add_parser("release",
|
|
4743
|
+
help="guarded, record-only: cut a version — enforce the readiness floor, "
|
|
4744
|
+
"then prepend CHANGELOG.md + an append-only RELEASES.md row (the "
|
|
4745
|
+
"engine records; you tag/publish). Security HARD-STOP is un-forceable")
|
|
4746
|
+
prl.add_argument("version", help="the version string to cut (free-form: semver / calver / any)")
|
|
4747
|
+
prl.add_argument("--force", action="store_true",
|
|
4748
|
+
help="override the forceable floor rejects (NEVER release_security_open)")
|
|
4749
|
+
prl.add_argument("--with-waivers", action="store_true", dest="with_waivers",
|
|
4750
|
+
help="disclose riding RISK-ACCEPTED waivers (records them on the ledger row)")
|
|
4751
|
+
prl.add_argument("--evidence", default=None, help="the RELEASES.md row's evidence line")
|
|
4752
|
+
prl.set_defaults(func=cmd_release)
|
|
4753
|
+
|
|
4397
4754
|
pau = sub.add_parser("audit",
|
|
4398
4755
|
help="read-only: verify recorded human decision points left well-formed records "
|
|
4399
4756
|
"(exit 1 on findings — the CI enforcement gate)")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* kit.sample.css — one class per catalog.sample.json component (the reusable kit).
|
|
2
|
+
Every visual value resolves a var(--…) from tokens.sample.css — no literals here, so
|
|
3
|
+
a token flip in tokens.sample.css restyles every screen that composes from this kit.
|
|
4
|
+
Screens use these classes and style nothing inline. */
|
|
5
|
+
|
|
6
|
+
/* Screen — the root viewport container */
|
|
7
|
+
.screen {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
align-items: center;
|
|
11
|
+
min-height: 100vh;
|
|
12
|
+
gap: var(--semantic-space-inset-md);
|
|
13
|
+
padding: var(--semantic-space-inset-md);
|
|
14
|
+
background: var(--semantic-color-surface);
|
|
15
|
+
font-family: var(--primitive-font-family-sans);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Card — a surface that groups related content */
|
|
19
|
+
.card {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: var(--semantic-space-inset-sm);
|
|
23
|
+
width: 100%;
|
|
24
|
+
max-width: 360px;
|
|
25
|
+
padding: var(--semantic-space-inset-md);
|
|
26
|
+
background: var(--semantic-color-surface);
|
|
27
|
+
border-radius: var(--semantic-space-inset-sm);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Text — a run of styled text */
|
|
31
|
+
.text {
|
|
32
|
+
margin: 0;
|
|
33
|
+
line-height: 1.5;
|
|
34
|
+
color: var(--semantic-color-text);
|
|
35
|
+
font-family: var(--primitive-font-family-sans);
|
|
36
|
+
}
|
|
37
|
+
.text--bold { font-weight: var(--primitive-font-weight-bold); }
|
|
38
|
+
.text--regular { font-weight: var(--primitive-font-weight-regular); }
|
|
39
|
+
|
|
40
|
+
/* Button — a primary call-to-action */
|
|
41
|
+
.button {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
padding: var(--semantic-space-inset-sm) var(--semantic-space-inset-md);
|
|
46
|
+
border: 0;
|
|
47
|
+
border-radius: var(--semantic-space-inset-sm);
|
|
48
|
+
background: var(--semantic-color-accent);
|
|
49
|
+
color: var(--semantic-color-surface);
|
|
50
|
+
font-family: var(--primitive-font-family-sans);
|
|
51
|
+
font-weight: var(--primitive-font-weight-bold);
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!-- settings.sample.html — a SECOND screen that proves REUSE. It composes from the SAME
|
|
3
|
+
kit classes (screen/card/text/button) and links the SAME tokens.sample.css +
|
|
4
|
+
kit.sample.css as welcome. It redefines nothing — no per-screen stylesheet, no inline
|
|
5
|
+
styles — so a single semantic-token flip in tokens.sample.css re-renders BOTH screens
|
|
6
|
+
identically. This is consistency by construction. -->
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<title>Settings — design mock</title>
|
|
12
|
+
<link rel="stylesheet" href="tokens.sample.css">
|
|
13
|
+
<link rel="stylesheet" href="kit.sample.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<!-- reuses the welcome kit: Screen > Card > {Text, Text, Button} -->
|
|
17
|
+
<main class="screen">
|
|
18
|
+
<section class="card">
|
|
19
|
+
<h1 class="text text--bold">Settings</h1>
|
|
20
|
+
<p class="text text--regular">Manage your workspace preferences.</p>
|
|
21
|
+
<button class="button" type="button">Save changes</button>
|
|
22
|
+
</section>
|
|
23
|
+
</main>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* tokens.sample.css — tokens.sample.json resolved to CSS custom properties.
|
|
2
|
+
The SINGLE source of token values. Literals live ONLY here; the kit and the
|
|
3
|
+
screens reference var(--semantic-*). Flip one --semantic-* line below and EVERY
|
|
4
|
+
screen that links this file re-renders the same way. */
|
|
5
|
+
:root {
|
|
6
|
+
/* primitive — raw values (the only literals in the whole sample) */
|
|
7
|
+
--primitive-color-blue-500: #3B82F6;
|
|
8
|
+
--primitive-color-slate-900: #0F172A;
|
|
9
|
+
--primitive-color-white: #FFFFFF;
|
|
10
|
+
--primitive-space-2: 8px;
|
|
11
|
+
--primitive-space-4: 16px;
|
|
12
|
+
--primitive-font-family-sans: "Inter", system-ui, sans-serif;
|
|
13
|
+
--primitive-font-weight-regular: 400;
|
|
14
|
+
--primitive-font-weight-bold: 700;
|
|
15
|
+
|
|
16
|
+
/* semantic — alias the primitives; THIS is the layer screens bind to.
|
|
17
|
+
Repoint one of these to re-theme every screen at once. */
|
|
18
|
+
--semantic-color-accent: var(--primitive-color-blue-500);
|
|
19
|
+
--semantic-color-text: var(--primitive-color-slate-900);
|
|
20
|
+
--semantic-color-surface: var(--primitive-color-white);
|
|
21
|
+
--semantic-space-inset-sm: var(--primitive-space-2);
|
|
22
|
+
--semantic-space-inset-md: var(--primitive-space-4);
|
|
23
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# UDD wireframe + HTML mock — make the layout visible before build
|
|
2
|
+
|
|
3
|
+
`udd-tokens.md` says what a UI is *made of*; `udd-catalog.md` says what it is *built
|
|
4
|
+
from*. Both are abstract JSON. This doc closes the last gap design.md names: turn a
|
|
5
|
+
prototype tree into a **real, captured screen the human confirms BEFORE build** — so
|
|
6
|
+
the build matches the expected layout instead of discovering it.
|
|
7
|
+
|
|
8
|
+
Two stages, low-fi → hi-fi:
|
|
9
|
+
|
|
10
|
+
- **Stage A — wireframe**: a fast, low-fi *structural* map. No styling.
|
|
11
|
+
- **Stage B — HTML mock**: a self-contained screen the human (or a headless tool)
|
|
12
|
+
actually renders and screenshots. The image is the design-confirm evidence.
|
|
13
|
+
|
|
14
|
+
The engine never renders. This is a recommendation + a worked sample, tool-agnostic
|
|
15
|
+
(consistent with `udd-catalog.md`'s render recipe). A worked pair ships beside it —
|
|
16
|
+
`welcome.sample.html` + `settings.sample.html` — that you can open in any browser.
|
|
17
|
+
|
|
18
|
+
## Stage A — wireframe (low-fi, structural)
|
|
19
|
+
|
|
20
|
+
A **wireframe** is a low-fidelity, *structural* map of one screen — regions and the
|
|
21
|
+
component **slots** inside them — derived directly from the prototype tree, **before**
|
|
22
|
+
any color, type scale, or spacing is decided. It answers "what goes where", not "what
|
|
23
|
+
it looks like".
|
|
24
|
+
|
|
25
|
+
Derive it mechanically from `prototypes/<name>.json`: each container element is a
|
|
26
|
+
region, each leaf element is a slot, the tree's nesting is the layout nesting. Keep it
|
|
27
|
+
in plain text or boxes — the point is speed and a shared structural picture, not paint.
|
|
28
|
+
|
|
29
|
+
Worked example: `wireframe.sample.txt` is the Stage-A map of the welcome screen
|
|
30
|
+
(`Screen > Card > {Text, Text, Button}`), with each slot mapped 1:1 to its prototype
|
|
31
|
+
element. No colors, no pixel values — those arrive in Stage B.
|
|
32
|
+
|
|
33
|
+
## Stage B — HTML mock (hi-fi, self-contained, screenshot-able)
|
|
34
|
+
|
|
35
|
+
The mock is a real screen built from the design foundation in **four moves**:
|
|
36
|
+
|
|
37
|
+
1. **Resolve `tokens.json`'s `semantic` layer → CSS custom properties.** Each
|
|
38
|
+
`{semantic.dotted.path}` alias becomes a `--semantic-dotted-path` variable in a
|
|
39
|
+
`:root` block. This file is the single source of token values — see
|
|
40
|
+
`tokens.sample.css`.
|
|
41
|
+
2. **One kit class per `catalog.json` component.** Each component (Screen, Card, Text,
|
|
42
|
+
Button, …) gets exactly one CSS class whose every visual value is a `var(--…)` from
|
|
43
|
+
step 1 — never a literal. This is the reusable **kit** — see `kit.sample.css`.
|
|
44
|
+
3. **Compose the prototype tree into HTML using the kit classes.** Walk
|
|
45
|
+
`prototypes/<name>.json`: each element becomes an HTML node carrying its
|
|
46
|
+
component's kit class; nesting follows `children`. Style nothing inline.
|
|
47
|
+
4. **Populate with mock data.** Fill the `Text`/`Button` content from the prototype's
|
|
48
|
+
props (or representative stand-ins). The result is a **self-contained** screen —
|
|
49
|
+
*no network*: it links only local sibling files — that opens offline and a
|
|
50
|
+
**headless** tool can **screenshot** directly (Playwright, `html2image`,
|
|
51
|
+
an agent browser, or a SaaS renderer — pick one; the engine never renders).
|
|
52
|
+
|
|
53
|
+
`welcome.sample.html` is the worked Stage-B mock of `prototype.sample.json`.
|
|
54
|
+
|
|
55
|
+
## Fast path — render via json-render (for JS-ecosystem projects)
|
|
56
|
+
|
|
57
|
+
Your `prototypes/<name>.json` is already a **json-render `Spec`** — the exact schema
|
|
58
|
+
`udd-catalog.md` pins. If your project renders json-render (React, Vue, Svelte, Solid,
|
|
59
|
+
React Native, …) you can skip the hand-authored kit and render the prototype through
|
|
60
|
+
your *real* catalog, so the mock **is** the product and cannot drift from it:
|
|
61
|
+
|
|
62
|
+
- **Render** — feed `prototype.json` into your `defineCatalog(…)` (see `udd-catalog.md`'s
|
|
63
|
+
"Render recipe"), or use a pre-built catalog such as `@json-render/shadcn` (36
|
|
64
|
+
shadcn/ui components) — no hand-written components for those stacks.
|
|
65
|
+
- **Capture** — `@json-render/image` renders a `Spec` straight to **PNG / SVG** (Satori,
|
|
66
|
+
no browser), a deterministic captured image with no headless-screenshot step. That
|
|
67
|
+
image is the same design-confirm evidence (see "Capture is evidence").
|
|
68
|
+
|
|
69
|
+
Trade-off — this is the *fast path*, not the floor. json-render needs the JS/npm
|
|
70
|
+
toolchain + a renderer package, so it does NOT cover a native (SwiftUI / Flutter / …)
|
|
71
|
+
project, and it has **no wireframe mode** (Stage A stays a separate, renderer-agnostic
|
|
72
|
+
structural artifact). When there is no toolchain, or the stack is not JS, the
|
|
73
|
+
self-contained HTML + CSS mock above is the universal default.
|
|
74
|
+
|
|
75
|
+
## Reuse & consistency (by construction)
|
|
76
|
+
|
|
77
|
+
Screens compose **only** from the shared kit + token vars — never hand-styled per
|
|
78
|
+
screen. Because every screen links the *same* `tokens.sample.css` + `kit.sample.css`,
|
|
79
|
+
a single **semantic-token flip** (one line in `tokens.sample.css`) re-renders **every**
|
|
80
|
+
screen the same way. Reuse before invent: pull an existing kit class first; add a new
|
|
81
|
+
one only for a genuine new `catalog.json` component.
|
|
82
|
+
|
|
83
|
+
`settings.sample.html` is a SECOND screen that **reuses** the welcome kit classes
|
|
84
|
+
(screen/card/text/button) without redefining them — proving the consistency: flip
|
|
85
|
+
`--semantic-color-surface` once and both screens change together.
|
|
86
|
+
|
|
87
|
+
## Capture is evidence
|
|
88
|
+
|
|
89
|
+
The screenshot of a Stage-B mock is **design-confirm evidence** — the human-facing
|
|
90
|
+
proof of beat 4 in `design.md`. Captures live at **`.add/design/captures/<name>.<ext>`**
|
|
91
|
+
(one per prototype) and are **attached or mentioned in the feature's `TASK.md`**
|
|
92
|
+
(alongside the §6 evidence), so the screen the human approved stays traceable from the
|
|
93
|
+
task that builds it. The recommended default capture engine for a json-render project is
|
|
94
|
+
**`@json-render/image`** (Satori → PNG/SVG, no browser); otherwise capture the
|
|
95
|
+
self-contained HTML mock headless. The engine never produces this image — it only
|
|
96
|
+
MEASURES presence (`add.py check` raises a never-red `missing_capture` WARN for a
|
|
97
|
+
prototype with no capture); the capture recipe is a recommendation, not an engine feature.
|
|
98
|
+
|
|
99
|
+
## Binds (read-only)
|
|
100
|
+
|
|
101
|
+
This recipe **consumes** the UDD contracts and never reshapes them:
|
|
102
|
+
|
|
103
|
+
- `tokens.json` — the semantic layer is the source of the CSS variables (unchanged).
|
|
104
|
+
- `catalog.json` — its component set is the source of the kit classes (unchanged).
|
|
105
|
+
- `prototypes/<name>.json` — the json-render tree is the structure both stages walk
|
|
106
|
+
(unchanged; the frozen data contract is read-only — a change there is a separate
|
|
107
|
+
change-request).
|
|
108
|
+
|
|
109
|
+
The mock (`*.sample.html`) is the human-facing *visible* view; the frozen
|
|
110
|
+
`prototypes/<name>.json` tree is the *machine-checkable* record. They are two views of
|
|
111
|
+
one screen.
|
|
112
|
+
|
|
113
|
+
## Ships byte-identical
|
|
114
|
+
|
|
115
|
+
`udd-wireframe.md` and its five sample files ship **byte-identical** in both template
|
|
116
|
+
trees (canonical `tooling/templates/` and the `_bundled` mirror) — enforced by
|
|
117
|
+
`test_bundle_parity`. A file present in one tree but missing from the other is a
|
|
118
|
+
`parity_break`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!-- welcome.sample.html — Stage-B HTML mock of prototype.sample.json (the welcome screen).
|
|
3
|
+
Self-contained (no network): links only the local shared stylesheets. Composes ONLY
|
|
4
|
+
from the kit classes in kit.sample.css and styles nothing inline. A headless tool
|
|
5
|
+
screenshots this file directly; that image is the design-confirm evidence
|
|
6
|
+
(see udd-wireframe.md, "Capture is evidence"). -->
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<title>Welcome — design mock</title>
|
|
12
|
+
<link rel="stylesheet" href="tokens.sample.css">
|
|
13
|
+
<link rel="stylesheet" href="kit.sample.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<!-- prototype tree: Screen > welcome-card(Card) > {title(Text), subtitle(Text), cta(Button)} -->
|
|
17
|
+
<main class="screen">
|
|
18
|
+
<section class="card">
|
|
19
|
+
<h1 class="text text--bold">Welcome to ADD</h1>
|
|
20
|
+
<p class="text text--regular">Spec-and-tests-first, render-ready.</p>
|
|
21
|
+
<button class="button" type="button">Get started</button>
|
|
22
|
+
</section>
|
|
23
|
+
</main>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
WIREFRAME — welcome screen (Stage A · low-fi · pre-styling)
|
|
2
|
+
derived from: prototypes/welcome.json (Screen > Card > Text, Text, Button)
|
|
3
|
+
|
|
4
|
+
+--------------------------------------------------+
|
|
5
|
+
| SCREEN [region: full viewport] |
|
|
6
|
+
| |
|
|
7
|
+
| +--------------------------------------+ |
|
|
8
|
+
| | CARD [region: centered group] | |
|
|
9
|
+
| | | |
|
|
10
|
+
| | [ Text slot ] title (bold) | |
|
|
11
|
+
| | [ Text slot ] subtitle | |
|
|
12
|
+
| | [ Button slot ] primary CTA | |
|
|
13
|
+
| | | |
|
|
14
|
+
| +--------------------------------------+ |
|
|
15
|
+
| |
|
|
16
|
+
+--------------------------------------------------+
|
|
17
|
+
|
|
18
|
+
slots map one-to-one to prototype elements:
|
|
19
|
+
SCREEN -> Screen (root)
|
|
20
|
+
CARD -> Card (welcome-card)
|
|
21
|
+
Text -> Text (title, subtitle)
|
|
22
|
+
Button -> Button (cta)
|
|
23
|
+
|
|
24
|
+
notes:
|
|
25
|
+
- structure only — no color, no type scale, no spacing values yet
|
|
26
|
+
- styling arrives in Stage B (kit.sample.css bound to tokens.sample.css)
|
|
27
|
+
- confirm the STRUCTURE here is right before investing in the hi-fi mock
|