@adia-ai/adia-ui-forge 0.8.3 → 0.8.5

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.
Files changed (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/agents/framework-verifier.md +13 -5
  4. package/bin/release-pretag-docs-gate +226 -0
  5. package/hooks/hooks.json +10 -0
  6. package/package.json +1 -1
  7. package/skills/adia-a2ui/SKILL.md +10 -2
  8. package/skills/adia-a2ui/references/pipeline-overview.md +1 -1
  9. package/skills/adia-author/SKILL.md +1 -0
  10. package/skills/adia-author/references/anti-patterns.md +7 -7
  11. package/skills/adia-author/references/api-contract.md +9 -9
  12. package/skills/adia-author/references/authoring-cycle.md +6 -6
  13. package/skills/adia-author/references/code-style.md +4 -4
  14. package/skills/adia-author/references/lifecycle-patterns.md +8 -8
  15. package/skills/adia-author/references/token-contract.md +5 -5
  16. package/skills/adia-author/references/worked-example.md +16 -16
  17. package/skills/adia-author/references/yaml-contract.md +1 -1
  18. package/skills/adia-deploy/SKILL.md +44 -0
  19. package/skills/adia-deploy/evals/routing-corpus.json +146 -0
  20. package/skills/adia-deploy/references/deploy-playbooks.md +1 -1
  21. package/skills/adia-dogfood/SKILL.md +64 -11
  22. package/skills/adia-dogfood/evals/routing-corpus.json +146 -0
  23. package/skills/adia-gen-review/SKILL.md +55 -0
  24. package/skills/adia-gen-review/evals/routing-corpus.json +146 -0
  25. package/skills/adia-llm-internals/evals/routing-corpus.json +146 -0
  26. package/skills/adia-llm-internals/references/adapter-contract.md +1 -1
  27. package/skills/adia-llm-internals/references/add-a-provider.md +1 -0
  28. package/skills/adia-llm-internals/references/bridge-facade.md +1 -1
  29. package/skills/adia-llm-internals/references/browser-proxy-boundary.md +8 -6
  30. package/skills/adia-llm-internals/references/model-registry.md +1 -1
  31. package/skills/adia-release/SKILL.md +12 -5
  32. package/skills/adia-release/evals/evals.json +24 -0
  33. package/skills/adia-release/references/changelog-discipline.md +4 -4
  34. package/skills/adia-release/references/cut-procedure.md +45 -11
  35. package/skills/adia-release/references/gates-catalog.md +3 -3
  36. package/skills/adia-release/references/recovery-paths.md +4 -2
  37. package/skills/adia-release/scripts/bump.mjs +32 -1
  38. package/skills/adia-release/scripts/dispatch-publish.mjs +49 -11
  39. package/skills/adia-release/scripts/gate-roster.mjs +71 -0
  40. package/skills/adia-release/scripts/insert-stub.mjs +35 -1
  41. package/skills/adia-release/scripts/promote-unreleased.mjs +35 -1
  42. package/skills/adia-release/scripts/release-pack.mjs +284 -53
  43. package/skills/adia-release/scripts/tag-lockstep.mjs +29 -7
  44. package/skills/adia-ssr/SKILL.md +110 -0
  45. package/skills/adia-ssr/evals/audit-report.md +63 -0
  46. package/skills/adia-ssr/evals/routing-corpus.json +138 -0
  47. package/skills/adia-ssr/references/consumer-workarounds.md +63 -0
  48. package/skills/adia-ssr/references/failure-shapes.md +122 -0
  49. package/skills/adia-ssr/references/guard-patterns.md +142 -0
  50. package/skills/adia-ssr/references/status-ledger.md +35 -0
  51. package/skills/adia-ssr/references/test-without-linkedom.md +113 -0
@@ -9,10 +9,10 @@
9
9
 
10
10
  Two entry variants, converging at Step 5:
11
11
 
12
- - **Variant A — deploy handoff:** a peer pre-cut the release commit + CHANGELOG + bump + lockfile. Re-baseline, verify, then resume at Step 6 (tag).
12
+ - **Variant A — deploy handoff:** a peer pre-cut the release commit + CHANGELOG + bump + lockfile. Re-baseline, verify, run **Step 4e** (release docs + team notes — the peer's cut may not have generated them, and the pretag gate blocks Step 6 without them), then resume at Step 6 (tag).
13
13
  - **Variant B — author from scratch:** source landed under `## [Unreleased]` with no bump. Do Step 4 (promotion + bump + lockfile), then the full tail.
14
14
 
15
- `scripts/release-pack.mjs` (bundled) mechanizes the sequence in two phases per invariant 3 — the cut modes stop at the release commit (PR → CI → merge happens between), `--mode handoff` runs tag→publish→deploy from post-merge main — with the three operator checkpoints preserved; the steps below are the manual/diagnostic form.
15
+ `` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/release-pack.mjs` `` (bundled) mechanizes the sequence in two phases per invariant 3 — the cut modes stop at the release commit (PR → CI → merge happens between), `--mode handoff` runs tag→publish→deploy from post-merge main — with the three operator checkpoints preserved; the steps below are the manual/diagnostic form.
16
16
 
17
17
  | Step | Action | Mutates? |
18
18
  | --- | --- | --- |
@@ -27,7 +27,7 @@ Two entry variants, converging at Step 5:
27
27
  | 7 | F-N1 release trip-wire; enrich CHANGELOGs on warns | If warns |
28
28
  | 8 | Push tags (one per push; `main` already merged) — **checkpoint 2 first** | Push |
29
29
  | 9 | Dispatch publish workflows; wait; verify registry — **checkpoint 3 first** | Publish |
30
- | 10 | GH releases + site deploy | Deploy |
30
+ | 10 | GH releases + site deploy — **checkpoint 4 first** | Deploy |
31
31
  | 11 | Author release notes (default, not optional) | No |
32
32
 
33
33
  ---
@@ -96,9 +96,11 @@ npm run dogfood:status # 17 P0/P1 dogfood floor
96
96
 
97
97
  Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source, re-run the narrowest gate, then re-run the full sequence. The canonical miss: a cut that ran 7 of the gates shipped a stale-embeddings defect that surfaced a day later and cost a tag-move recovery.
98
98
 
99
- ## §Step 4 — (Variant B) Promote, bump, lockfile
99
+ ## §Step 4 — (Variant B, or ANY variant with uncommitted `[Unreleased]` content) Promote, bump, lockfile
100
100
 
101
- **4a. Promote** `## [Unreleased]` `## [vX.Y.Z] YYYY-MM-DD` per package (`scripts/promote-unreleased.mjs`); author fresh blocks for changed-but-unlogged packages; stub the pure ride-alongs (`scripts/insert-stub.mjs`). Classification recipe + shapes: [`changelog-discipline.md`](changelog-discipline.md).
101
+ **Run 4a whenever a hand-authored `## [Unreleased]` section is still sitting uncommitted, not only on a strict Variant B.** `release-pack.mjs --mode cut` (a peer's pre-staged content, not yet promoted) needs it exactly as much as `--mode from-scratch` does the v0.8.4 near-miss was `--mode cut` skipping this step entirely because the doc (and the script) only associated promotion with "from scratch". Both modes now run it and both hard-fail before the bump if any of the 11 packages still carries non-empty `[Unreleased]` content afterward.
102
+
103
+ **4a. Promote** `## [Unreleased]` → `## [vX.Y.Z] — YYYY-MM-DD` per package (`` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/promote-unreleased.mjs` ``); author fresh blocks for changed-but-unlogged packages; stub the pure ride-alongs (`` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/insert-stub.mjs` ``). Classification recipe + shapes: [`changelog-discipline.md`](changelog-discipline.md).
102
104
 
103
105
  **4b. Bump.** PATCH vs MINOR: **MINOR is reserved for API-surface breaks only** (removed/renamed prop, attribute, slot, event, token, or tag). Visible behavior changes, re-scalings, and opt-in features stay PATCH; a CHANGELOG bullet saying "(MINOR behavior change)" is prose, not a semver directive. Unqualified "bump version" = PATCH; don't round-trip to ask. `node "${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/bump.mjs" --from X.Y.Z-1 --to X.Y.Z`. On a MINOR cut, also bump the internal `@adia-ai/*` `^ranges` separately (bump.mjs touches `"version"` fields only) — and a MINOR cut owes a MIGRATION GUIDE section ([`migration-guide-authoring.md`](migration-guide-authoring.md)).
104
106
 
@@ -106,6 +108,23 @@ Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source,
106
108
 
107
109
  **4d.** `npm run check:lockstep` → `OK — all packages at X.Y.Z, all internal ranges at ^X.Y.0`.
108
110
 
111
+ **4e. Release docs + team notes** (gated, not optional): review the entry
112
+ files the release touches (root README/CHANGELOG, per-package READMEs —
113
+ content currency is YOUR judgment; the gate only proves presence), then
114
+ generate the team notes and land them IN the release commit:
115
+
116
+ ```bash
117
+ node scripts/release/generate-release-notes.mjs --version X.Y.Z --write # → docs/releases/vX.Y.Z.md
118
+ node scripts/release/check-release-docs.mjs --version X.Y.Z # must print OK
119
+ ```
120
+
121
+ `check-release-docs` is enforced twice downstream — the
122
+ `release-pretag-docs-gate` Claude Code hook (adia-forge plugin) denies
123
+ tag-creation/tag-push/publish-dispatch commands until it passes, and
124
+ `.githooks/pre-push` blocks release-tag pushes the same way (v0.8.4
125
+ shipped tarballs with unpromoted `[Unreleased]` CHANGELOG headers because
126
+ this class of check ran only after tagging).
127
+
109
128
  ## §Step 5 — Stage and commit (on a release branch)
110
129
 
111
130
  The release commit lands via PR, never a direct push to `main` (repo
@@ -118,7 +137,8 @@ allowlist (never `git add -A`; if peers may have pre-staged files,
118
137
  git -C "$REPO" checkout -b "release/vX.Y.Z"
119
138
  git -C "$REPO" reset HEAD >/dev/null 2>&1
120
139
  git -C "$REPO" add package-lock.json packages/*/package.json packages/*/CHANGELOG.md \
121
- packages/a2ui/*/package.json packages/a2ui/*/CHANGELOG.md # + in-scope source + Step-3.0 regen outputs
140
+ packages/a2ui/*/package.json packages/a2ui/*/CHANGELOG.md \
141
+ docs/releases/vX.Y.Z.md # + in-scope source + Step-3.0 regen outputs
122
142
  git -C "$REPO" diff --cached --stat | tail -3 # count must match the allowlist
123
143
  ```
124
144
 
@@ -152,9 +172,22 @@ post-merge HEAD, so anything merged before tagging ships in the tarball.
152
172
  If unrelated PRs merged between yours and the tag step, that is fine: tag
153
173
  at HEAD is the invariant, and the window closes at the tag.
154
174
 
175
+ ## §Operator checkpoints — evidence-first, not a bare y/N
176
+
177
+ Four checkpoints total (corrected on PR #289 review — this section previously said "three", missing the Step-10 deploy-dispatch confirm that already existed in the script). Each prints its evidence block BEFORE the prompt, never a bare confirm (H5, forge-campaign gh#268 audit — the prior shape asked "proceed? [y/N]" with nothing to judge it against). The prompt itself is a plain two-way gate, honestly: **yes proceeds** (the mutating step for that checkpoint runs); **anything else aborts** — the script exits cleanly having done nothing past that point. There is no in-script "enrich" path (a CodeRabbit finding on PR #289 caught this doc previously over-promising a three-way go/enrich/abort choice the code never implemented) — to enrich, abort, fix the thing the evidence surfaced (a CHANGELOG entry, a version mismatch), then re-run from that checkpoint; the evidence re-prints fresh against the fixed state.
178
+
179
+ | Checkpoint | Evidence shown BEFORE the prompt | Why this evidence (not F-N1 at cp1) |
180
+ | --- | --- | --- |
181
+ | 1 — before tagging | The planned tag list (umbrella + 11 per-package) | F-N1 inspects tag commits — it has nothing to check until Step 6 creates them |
182
+ | 2 — before pushing | The Step 7 F-N1 output (now real, against the just-created tags) + the tag list again | This is the FIRST point F-N1 evidence exists — showing it here, not at cp1, is why the step order (tag → F-N1 → cp2) is fixed |
183
+ | 3 — before publishing | The current registry snapshot (per-package versions + `dist-tags.latest`) | Publish ordering is judged against what's LIVE right now, not an assumption |
184
+ | 4 — before deploying the site | The `deploy-site.yml` dispatch itself as evidence (never a raw rsync) | Deploy is its own irreversible, outward-facing action distinct from publish |
185
+
186
+ `release-pack.mjs` implements this: `checkpoint(label, evidence, autoConfirmed)` always prints `evidence` first, then `[CHECKPOINT] <label> — proceed? [y/N]`; `--yes` auto-confirms checkpoint 1 ONLY, `--push` gates checkpoint 2, `--publish` gates checkpoints 3 AND 4 (a single `--yes` used to flatten all of them — the defect this replaced). **F-N1 errors (not warns) hard-stop before checkpoint 2 even appears — no flag, including `--push`, skips this.** A cosmetic F-N1 warn does not hard-stop, but it also blocks `--push` from auto-confirming checkpoint 2 — only the interactive prompt may proceed past a warn (never past an error, which already exited).
187
+
155
188
  ## §Step 6 — Tag ← operator checkpoint 1 (before tagging)
156
189
 
157
- Show the operator the planned tag list + the F-N1 preview; proceed only on their go. Then tag **at `main`'s post-merge HEAD** (post-bump fixes belong in the tarball; the window's last merge is the tag point — exception: batch push tags each version at its own release-merge SHA):
190
+ Show the operator the planned tag list; proceed only on their go (F-N1 can't run yet see the table above). Then tag **at `main`'s post-merge HEAD** (post-bump fixes belong in the tarball; the window's last merge is the tag point — exception: batch push tags each version at its own release-merge SHA):
158
191
 
159
192
  ```bash
160
193
  node "${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/tag-lockstep.mjs" \
@@ -172,7 +205,8 @@ Per-package tags must be `✓ clean`; the umbrella-tag error is expected noise.
172
205
  ## §Step 8 — Push tags ← operator checkpoint 2 (before pushing tags)
173
206
 
174
207
  `main` is already on the remote (the release PR merged in Step 5.7) —
175
- only the tags push here. Show the tag list + confirm
208
+ only the tags push here. Show the Step 7 F-N1 results + the tag list (the
209
+ evidence block for this checkpoint — see the table above) + confirm
176
210
  `git rev-list --count origin/main..HEAD` is 0 (a non-zero count means
177
211
  local commits bypassed the PR flow — stop and route them through a PR
178
212
  first); on go:
@@ -189,7 +223,7 @@ git -C "$REPO" push origin vX.Y.Z # umbrella last; triggers nothing
189
223
 
190
224
  ## §Step 9 — Publish ← operator checkpoint 3 (before publishing)
191
225
 
192
- For batch pushes, confirm ordering with the operator: **oldest version publishes and settles first** — `npm dist-tag latest` is set by publish order. Then:
226
+ Show the current registry snapshot (per-package versions + `dist-tags.latest`) as this checkpoint's evidence. For batch pushes, confirm ordering with the operator against that snapshot: **oldest version publishes and settles first** — `npm dist-tag latest` is set by publish order. Then:
193
227
 
194
228
  ```bash
195
229
  node "${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/dispatch-publish.mjs" \
@@ -208,7 +242,7 @@ npm view @adia-ai/web-components dist-tags.latest # must equal X.Y.Z
208
242
 
209
243
  Zero workflows fired after a tag push → [`recovery-paths.md`](recovery-paths.md) §Scenario 7.
210
244
 
211
- ## §Step 10 — GH releases + site deploy
245
+ ## §Step 10 — GH releases + site deploy ← operator checkpoint 4 (before deploying)
212
246
 
213
247
  ```bash
214
248
  for pkg in <the 9>; do
@@ -220,7 +254,7 @@ gh workflow run "Deploy site (ui-kit.exe.xyz)" --repo adiahealth/gen-ui-kit --re
220
254
 
221
255
  Deploy discipline (the release tenant of the demo-site host; VM/service ops belong to the deploy skill, `adia-deploy`):
222
256
 
223
- - `deploy-site.yml` owns build → pre-flight verify → snapshot → rsync → post-deploy verify → auto-rollback; it builds from `main`, so the release commit must already be merged (§Step 5.7 guarantees this). A raw local rsync bypasses every one of those gates.
257
+ - `deploy-site.yml` owns build → pre-flight verify → snapshot → rsync → post-deploy verify → auto-rollback; it builds from `main`, so the release commit must already be merged (§Step 5.7 guarantees this). A raw local rsync bypasses every one of those gates — `release-pack.mjs` dispatches the workflow (never rsyncs directly) behind its own confirm (H1, forge-campaign gh#268 audit: the script had drifted from this already-documented procedure).
224
258
  - **Verify deployed FILES, never SPA routes.** The docs site returns HTTP 200 + the same ~5 KB shell for *every* path; an unmatched route renders blank with no error. Curl a content file and grep for real bytes:
225
259
 
226
260
  ```bash
@@ -16,7 +16,7 @@ Row layout per gate: **What** · **Typical failure** · **Recovery**.
16
16
 
17
17
  - **What:** all 11 `@adia-ai/*` packages declare the same `version`; internal `@adia-ai/*` dep ranges match policy (`^X.Y.0` during PATCH cycles, bumped at MINOR).
18
18
  - **Typical failure:** one package forgot to bump; a peer edited an internal range mid-PATCH; a `^0.0.x` range slipped in.
19
- - **Recovery:** version drift → `scripts/bump.mjs` (bundled with this skill); range drift → `npm run check:lockstep:fix` auto-aligns, then re-run.
19
+ - **Recovery:** version drift → `` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/bump.mjs` ``; range drift → `npm run check:lockstep:fix` auto-aligns, then re-run.
20
20
  - **Why `^0.0.x` is forbidden:** npm pre-1.0 semver only widens the caret when major+minor aren't both zero — `^0.0.6` resolves to `>=0.0.6 <0.0.7`, locked to exactly 0.0.6. An internal dep pinned that way silently installs a *stale* sibling on every fresh `npm i` (this shipped a real ~4-day-latent bug before the lockstep policy). The `^X.Y.0` floor (Y≥1) widens correctly across patches; trust the gate, don't reason about caret semantics by hand. Moot at 1.0.0.
21
21
 
22
22
  ### `node scripts/release/check-release.mjs --all-pending` (F-N1, the release trip-wire)
@@ -24,7 +24,7 @@ Row layout per gate: **What** · **Typical failure** · **Recovery**.
24
24
  - **What:** for every unpushed tag, verifies CHANGELOG coverage of the diff between this tag and the package's previous tag.
25
25
  - **Cosmetic failure:** "diff `packages/<pkg>/components/` touched but CHANGELOG `[X.Y.Z]` doesn't mention 'components'" — the entry names the component (`table-ui`) but not the literal path keyword. Regex miss, change IS documented.
26
26
  - **Real failure:** a touched directory has NO matching CHANGELOG entry at all — the cycle missed documenting a change.
27
- - **Recovery:** cosmetic → add the path keyword to a relevant entry (`table.yaml` → `components/table/table.yaml`); real → author the missing entry. Either way: amend the release commit, delete + re-create the tags at the new SHA, re-run. Full discipline: [`changelog-discipline.md`](changelog-discipline.md) §F-N1 enrichment.
27
+ - **Recovery:** cosmetic → add the path keyword to a relevant entry (`table.yaml` → `components/table/table.yaml`); real → author the missing entry. Either way: land it as a new commit through the PR flow (the release commit is already merged — `--amend` is not possible post-invariant-3), delete + re-create the tags at the new post-merge SHA, re-run. Full discipline: [`changelog-discipline.md`](changelog-discipline.md) §F-N1 enrichment.
28
28
  - The umbrella tag's "doesn't match `<pkg>-v<version>`" **error is expected — ignore it**; only per-package warns need action.
29
29
 
30
30
  ### `npm run check:changelog-coverage`
@@ -207,7 +207,7 @@ npm run check:demo-shells
207
207
 
208
208
  Add `verify:corpus` + `check:embeddings-fresh` if chunks were touched; `check:lightningcss-build` if CSS was touched; F-N1 if unpushed release tags exist.
209
209
 
210
- **Full pre-cut sweep** — the 17-gate roster in [`cut-procedure.md`](cut-procedure.md) §Step 3. ~90s wall time.
210
+ **Full pre-cut sweep** — the 17-gate roster in [`cut-procedure.md`](cut-procedure.md) §Step 3, sourced from `` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/gate-roster.mjs` `` (the ONE list; `release-pack.mjs` imports and runs it in full — a subset run is impossible without editing that file). ~90s wall time.
211
211
 
212
212
  **Omnibus** — `npm run check` invokes everything. Heavy; use when re-baselining a stale checkout.
213
213
 
@@ -24,6 +24,8 @@
24
24
 
25
25
  **Shape:** multiple unpushed `release(*): vX.Y.Z` commits since the last published tag; operator asks to "publish what's accumulated."
26
26
 
27
+ `release-pack.mjs --mode batch` hard-rejects and points here rather than attempting this — each version tags at its OWN release-commit SHA (not `main`'s single post-merge HEAD) and publish order must be enforced across versions, which the single-version orchestrator has no model for. This IS the manual procedure; there is no mechanized alternative.
28
+
27
29
  **Resolution:**
28
30
 
29
31
  1. Order the release commits oldest → newest.
@@ -36,7 +38,7 @@
36
38
 
37
39
  **Shape:** source + CHANGELOG entries landed under `## [Unreleased]`, no bump, no release commit.
38
40
 
39
- **Resolution:** [`cut-procedure.md`](cut-procedure.md) Variant B; `scripts/promote-unreleased.mjs` mechanizes the heading swap; fresh blocks per [`changelog-discipline.md`](changelog-discipline.md) §Authoring.
41
+ **Resolution:** [`cut-procedure.md`](cut-procedure.md) Variant B; `` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/promote-unreleased.mjs` `` mechanizes the heading swap; fresh blocks per [`changelog-discipline.md`](changelog-discipline.md) §Authoring.
40
42
 
41
43
  ## §Scenario 4 — `[Unreleased]` extension (early cut + entangled fix)
42
44
 
@@ -72,7 +74,7 @@ node "${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/dispatch-publish.mjs" \
72
74
  --version X.Y.Z --verify-triggered # re-dispatches ONLY packages with no run; idempotent
73
75
  ```
74
76
 
75
- For a batch, preserve npm-latest ordering (`--after <prev>`). Verify against the registry, not the workflows. **Prevention:** push tags one-at-a-time ([`cut-procedure.md`](cut-procedure.md) §Step 8); `release-pack.mjs` does this automatically and follows with `--verify-triggered`.
77
+ For a batch, preserve npm-latest ordering (`--after <prev>`). Verify against the registry, not the workflows. **Prevention:** push tags one-at-a-time ([`cut-procedure.md`](cut-procedure.md) §Step 8); `` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/release-pack.mjs` `` does this automatically and follows with `--verify-triggered`.
76
78
 
77
79
  ## §Scenario 8 — Cut on the wrong branch
78
80
 
@@ -140,4 +140,35 @@ function main() {
140
140
  }
141
141
  }
142
142
 
143
- main();
143
+ // Fixture-locked proof of the core transform, entirely in-memory (no real
144
+ // package.json files touched) — the `dry: true` path below asserts the
145
+ // script never calls fs.writeFileSync by pointing at a path whose directory
146
+ // doesn't exist; a latent write bug would throw ENOENT instead of passing.
147
+ function selftest() {
148
+ const fake = [{
149
+ pkg: 'packages/web-components',
150
+ path: '/nonexistent-fixture-dir/package.json',
151
+ txt: '{\n "name": "@adia-ai/web-components",\n "version": "0.8.4"\n}\n',
152
+ version: '0.8.4',
153
+ }];
154
+ if (!validateAllAtFrom(fake, '0.8.4')) {
155
+ console.error('selftest FAIL: validateAllAtFrom rejected a matching version'); process.exit(1);
156
+ }
157
+ if (validateAllAtFrom(fake, '0.8.3')) {
158
+ console.error('selftest FAIL: validateAllAtFrom accepted a mismatched version'); process.exit(1);
159
+ }
160
+ const bumped = bumpAll(fake, '0.8.4', '0.8.5', true /* dry */);
161
+ if (bumped !== fake.length) {
162
+ console.error('selftest FAIL: bumpAll(dry) did not report all packages bumped'); process.exit(1);
163
+ }
164
+ // Corrupted package.json (regex has nothing to match) must be caught, not silently skipped.
165
+ const corrupted = [{ pkg: 'packages/x', path: '/nonexistent-fixture-dir/x/package.json', txt: '{}', version: '0.8.4' }];
166
+ if (bumpAll(corrupted, '0.8.4', '0.8.5', true) !== -1) {
167
+ console.error('selftest FAIL: corrupted package.json (no version match) not caught'); process.exit(1);
168
+ }
169
+ console.log('selftest OK');
170
+ }
171
+
172
+ const topArgv = process.argv.slice(2);
173
+ if (topArgv[0] === 'selftest') selftest();
174
+ else main();
@@ -199,19 +199,57 @@ async function main() {
199
199
  }
200
200
  console.log(`\n[dispatch] ${succeeded}/${PACKAGES.length} dispatched ${args.dry ? '(dry)' : ''}`);
201
201
 
202
- if (!args.dry) {
203
- console.log(`\n[next] wait for workflows to settle:`);
204
- console.log(` until [ "$(gh run list --workflow=publish-a2ui-validator.yml --limit 1 --json status -q '.[0].status')" = "completed" ]; do sleep 5; done`);
205
- console.log(`\n[next] verify all 11 succeeded:`);
206
- console.log(` for pkg in ${PACKAGES.join(' ')}; do`);
207
- console.log(` gh run list --workflow=publish-$pkg.yml --limit 1 --json conclusion -q '.[0].conclusion'`);
208
- console.log(` done`);
202
+ // defect-2 (forge-campaign gh#268 audit): this used to print a "[next] wait
203
+ // for / verify" block phrased as a to-do EVEN THOUGH the dispatch above
204
+ // already fired an operator's agent read it as "publish hasn't happened
205
+ // yet" and went back to refine CHANGELOGs post-publish. State what WAS
206
+ // done; offer monitoring as optional, not required.
207
+ if (!args.dry && succeeded > 0) {
208
+ // CodeRabbit (PR #289): "RUNNING" overclaimed — `gh workflow run` only
209
+ // confirms the dispatch was SUBMITTED; the run may still queue, or fail
210
+ // to materialize (bad ref, disabled workflow). "dispatched" is what we
211
+ // actually know happened; no further action is still true either way —
212
+ // the point stands, the verb doesn't overreach.
213
+ console.log(`\n✓ ${succeeded} publish workflow(s) dispatched above — no further action required.`);
214
+ console.log(' Optional — monitor until they settle:');
215
+ console.log(` until [ "$(gh run list --workflow=publish-a2ui-validator.yml --limit 1 --json status -q '.[0].status')" = "completed" ]; do sleep 5; done`);
216
+ console.log(' Optional — check each conclusion once settled:');
217
+ console.log(` for pkg in ${PACKAGES.join(' ')}; do`);
218
+ console.log(` gh run list --workflow=publish-$pkg.yml --limit 1 --json conclusion -q '.[0].conclusion'`);
219
+ console.log(` done`);
209
220
  }
210
221
 
211
222
  if (succeeded !== PACKAGES.length) process.exit(1);
212
223
  }
213
224
 
214
- main().catch((e) => {
215
- console.error(`error: ${e.message}`);
216
- process.exit(1);
217
- });
225
+ // dry mode never shells out (run()/dispatch() short-circuit before
226
+ // execSync), so this exercises the real functions without touching gh/npm.
227
+ function selftest() {
228
+ let allOk = true;
229
+ for (const pkg of PACKAGES) {
230
+ if (!dispatch(pkg, '9.9.9', true /* dry */)) allOk = false;
231
+ }
232
+ if (!allOk) { console.error('selftest FAIL: dry dispatch() reported a failure for at least one package'); process.exit(1); }
233
+
234
+ // triggeredForTag() in dry mode must conservatively report "not yet
235
+ // triggered" — verifyTriggeredAndRedispatch's whole point is to re-dispatch
236
+ // misses; silently assuming "triggered" would let a real miss through.
237
+ if (triggeredForTag('web-components', '9.9.9', true) !== false) {
238
+ console.error('selftest FAIL: dry triggeredForTag() must report false (conservative default)'); process.exit(1);
239
+ }
240
+
241
+ const rc = verifyTriggeredAndRedispatch('9.9.9', true);
242
+ if (rc !== 0) { console.error(`selftest FAIL: dry verifyTriggeredAndRedispatch() should report rc=0, got ${rc}`); process.exit(1); }
243
+
244
+ console.log('selftest OK');
245
+ }
246
+
247
+ const topArgv = process.argv.slice(2);
248
+ if (topArgv[0] === 'selftest') {
249
+ selftest();
250
+ } else {
251
+ main().catch((e) => {
252
+ console.error(`error: ${e.message}`);
253
+ process.exit(1);
254
+ });
255
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // gate-roster.mjs — the ONE ordered list of the pre-cut gate roster.
3
+ //
4
+ // H3 (forge-campaign gh#268 audit): release-pack.mjs's Step 3 pre-flight ran
5
+ // only 12 of the 17 gates cut-procedure.md §3.1 declares as "every gate runs;
6
+ // a subset = pre-flight failure" — the roster had drifted into two places
7
+ // (the script's own hardcoded array + the doc's table) and only one of them
8
+ // got the memo when gates were added. This module is the single source: the
9
+ // doc's §3.1 table transcribes it, release-pack.mjs's step3PreFlight()
10
+ // imports and runs it in full, and nothing else declares its own subset — a
11
+ // gate can only be dropped by editing this file, which is the change the
12
+ // doc and the script both need anyway.
13
+ //
14
+ // Usage:
15
+ // import { GATE_ROSTER } from './gate-roster.mjs';
16
+ // node gate-roster.mjs --list # human-readable numbered roster (docs/CI use)
17
+ // node gate-roster.mjs --count # just the count (drift check)
18
+
19
+ import { fileURLToPath } from 'node:url';
20
+ import { realpathSync } from 'node:fs';
21
+
22
+ export const GATE_ROSTER = [
23
+ { n: 1, cmd: 'node scripts/build/components.mjs --verify', what: 'yaml ↔ sidecar ↔ .d.ts' },
24
+ { n: 2, cmd: 'npm run verify:traits', what: 'trait coverage' },
25
+ { n: 3, cmd: 'npm run check:lockstep', what: 'version coherence' },
26
+ { n: 4, cmd: 'npm run test:unit', what: 'vitest' },
27
+ { n: 5, cmd: 'npm run typecheck', what: 'tsc --noEmit' },
28
+ { n: 6, cmd: 'npm run check:demo-shells', what: 'demo imports cover composes:' },
29
+ { n: 7, cmd: 'npm run check:lightningcss-build', what: 'CSS minifies' },
30
+ { n: 8, cmd: 'npm run check:css-bundles-fresh', what: 'dist CSS matches source' },
31
+ { n: 9, cmd: 'npm run check:js-bundles-fresh', what: 'dist JS matches source' },
32
+ { n: 10, cmd: 'npm run smoke:engines', what: 'gen-UI engines' },
33
+ { n: 11, cmd: 'npm run smoke:register-engine', what: 'register-engine 11/11' },
34
+ { n: 12, cmd: 'npm run verify:corpus', what: 'corpus 0 errors' },
35
+ { n: 13, cmd: 'npm run check:chunks-fresh', what: 'chunk index vs sources' },
36
+ { n: 14, cmd: 'npm run check:embeddings-fresh', what: 'embeddings vs chunk index' },
37
+ { n: 15, cmd: 'npm run check:links', what: 'intra-repo links' },
38
+ { n: 16, cmd: 'npm run eval:diff -- --engine zettel', what: 'eval floors' },
39
+ { n: 17, cmd: 'npm run dogfood:status', what: 'P0/P1 dogfood floor' },
40
+ ];
41
+
42
+ // -- CLI ------------------------------------------------------------------
43
+ // Only runs when invoked directly (not when imported by release-pack.mjs).
44
+ // realpathSync BOTH sides (CodeRabbit, PR #289): a naive string/URL compare
45
+ // breaks under a symlinked invocation — this skill is normally reached
46
+ // through `.claude/skills/adia-release` -> `packages/plugins/adia-ui-forge/
47
+ // skills/adia-release`, and Node resolves `import.meta.url` through that
48
+ // symlink to the real file while `process.argv[1]` keeps the as-invoked
49
+ // path, so an unresolved compare never matches even when this IS the entry
50
+ // point. Verified directly (see embeddings-chunks.mjs's identical guard).
51
+ const isMain = (() => {
52
+ if (!process.argv[1]) return false;
53
+ try {
54
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
55
+ } catch {
56
+ return false;
57
+ }
58
+ })();
59
+ if (isMain) {
60
+ const args = new Set(process.argv.slice(2));
61
+ if (args.has('--count')) {
62
+ console.log(GATE_ROSTER.length);
63
+ } else if (args.has('--list') || args.size === 0) {
64
+ console.log(`${GATE_ROSTER.length}-gate pre-cut roster (source of truth — cut-procedure.md §3.1 transcribes this):\n`);
65
+ for (const g of GATE_ROSTER) {
66
+ console.log(` ${String(g.n).padStart(2)}. ${g.cmd.padEnd(46)} # ${g.what}`);
67
+ }
68
+ } else {
69
+ console.log('Usage: node gate-roster.mjs [--list|--count]');
70
+ }
71
+ }
@@ -16,6 +16,8 @@
16
16
  // cycles.
17
17
 
18
18
  import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
19
21
  import process from 'node:process';
20
22
  import { resolvePackageChangelog } from './package-paths.mjs';
21
23
 
@@ -126,4 +128,36 @@ function main() {
126
128
  }
127
129
  }
128
130
 
129
- main();
131
+ // Fixture-locked proof against a real (throwaway) temp directory.
132
+ function selftest() {
133
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'insert-stub-selftest-'));
134
+ try {
135
+ const dir = path.join(tmp, 'packages', 'llm');
136
+ fs.mkdirSync(dir, { recursive: true });
137
+ const clPath = path.join(dir, 'CHANGELOG.md');
138
+ const anchor = '## [0.1.0] — 2026-01-01';
139
+ fs.writeFileSync(clPath, `# C\n\n${anchor}\n\n- prior\n`);
140
+
141
+ const stub = buildStub('0.2.0', '2026-02-02', '@adia-ai/web-components', 'packages/web-components/CHANGELOG.md#020--2026-02-02');
142
+ const r1 = insertStub(tmp, 'llm', anchor, stub, '0.2.0', false);
143
+ if (r1 !== 'inserted') { console.error(`selftest FAIL: expected 'inserted', got '${r1}'`); process.exit(1); }
144
+ const out = fs.readFileSync(clPath, 'utf8');
145
+ if (!out.includes('## [0.2.0]')) { console.error('selftest FAIL: stub heading not written'); process.exit(1); }
146
+ if (out.indexOf('## [0.2.0]') > out.indexOf(anchor)) {
147
+ console.error('selftest FAIL: stub must land ABOVE the previous-version anchor, not below'); process.exit(1);
148
+ }
149
+
150
+ // Idempotency: re-inserting against an already-stubbed target must fail
151
+ // loudly, not create a duplicate [0.2.0] block.
152
+ const r2 = insertStub(tmp, 'llm', anchor, stub, '0.2.0', false);
153
+ if (r2 !== 'already-present') { console.error(`selftest FAIL: expected 'already-present' on re-run, got '${r2}'`); process.exit(1); }
154
+
155
+ console.log('selftest OK');
156
+ } finally {
157
+ fs.rmSync(tmp, { recursive: true, force: true });
158
+ }
159
+ }
160
+
161
+ const topArgv = process.argv.slice(2);
162
+ if (topArgv[0] === 'selftest') selftest();
163
+ else main();
@@ -20,6 +20,8 @@
20
20
  // 2 — bad args / file I/O error
21
21
 
22
22
  import fs from 'node:fs';
23
+ import os from 'node:os';
24
+ import path from 'node:path';
23
25
  import process from 'node:process';
24
26
  import { resolvePackageChangelog } from './package-paths.mjs';
25
27
 
@@ -86,4 +88,36 @@ function main() {
86
88
  }
87
89
  }
88
90
 
89
- main();
91
+ // Fixture-locked proof against a real (throwaway) temp directory — promote()
92
+ // does real fs.readFileSync/writeFileSync via resolvePackageChangelog, so a
93
+ // pure in-memory test can't exercise it; a temp dir keeps this <1s and
94
+ // touches nothing in the real repo.
95
+ function selftest() {
96
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'promote-unreleased-selftest-'));
97
+ try {
98
+ const dir = path.join(tmp, 'packages', 'web-components');
99
+ fs.mkdirSync(dir, { recursive: true });
100
+ const clPath = path.join(dir, 'CHANGELOG.md');
101
+ fs.writeFileSync(clPath, '# C\n\n## [Unreleased]\n\n### Fixed\n- x\n\n## [0.1.0] — 2026-01-01\n\n- y\n');
102
+
103
+ const r1 = promote(tmp, 'web-components', '0.2.0', '2026-02-02', false);
104
+ if (r1 !== 'promoted') { console.error(`selftest FAIL: expected 'promoted', got '${r1}'`); process.exit(1); }
105
+ const out = fs.readFileSync(clPath, 'utf8');
106
+ if (!out.includes('## [0.2.0] — 2026-02-02')) { console.error('selftest FAIL: new heading not written'); process.exit(1); }
107
+ if (out.includes('## [Unreleased]')) { console.error('selftest FAIL: [Unreleased] heading should have been renamed, not left behind'); process.exit(1); }
108
+ if (!out.includes('### Fixed\n- x')) { console.error('selftest FAIL: content under the heading must survive the rename'); process.exit(1); }
109
+
110
+ // Idempotency: re-running against an already-promoted target must fail
111
+ // loudly (a silent no-op or a duplicate block would both be wrong).
112
+ const r2 = promote(tmp, 'web-components', '0.2.0', '2026-02-02', false);
113
+ if (r2 !== 'already-promoted') { console.error(`selftest FAIL: expected 'already-promoted' on re-run, got '${r2}'`); process.exit(1); }
114
+
115
+ console.log('selftest OK');
116
+ } finally {
117
+ fs.rmSync(tmp, { recursive: true, force: true });
118
+ }
119
+ }
120
+
121
+ const topArgv = process.argv.slice(2);
122
+ if (topArgv[0] === 'selftest') selftest();
123
+ else main();