@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
@@ -13,7 +13,13 @@
13
13
  // --mode cut (Mode 1 — peer pre-staged [Unreleased] or pure stub; STOPS at the release commit)
14
14
  // --mode from-scratch (Mode 2 — promote [Unreleased] → [VERSION]; STOPS at the release commit)
15
15
  // --mode handoff (Mode 3 — the second phase: tag at post-merge main → publish → deploy)
16
- // --mode batch (Mode 4 — multiple merged cuts; pass --version-list)
16
+ //
17
+ // `--mode batch` is NOT implemented here — a batch push tags each version at
18
+ // its OWN release-merge SHA and enforces oldest-first publish ordering, which
19
+ // this single-version orchestrator has no model for. Passing it hard-errors
20
+ // with a pointer to the manual procedure: references/recovery-paths.md
21
+ // §Scenario 2 (Batch push). Silently falling through to the single-version
22
+ // path was the H2 audit finding this replaced.
17
23
  //
18
24
  // REQUIRED:
19
25
  // --version 0.6.22
@@ -25,26 +31,37 @@
25
31
  // --gh-notes-file <path> : GH release notes body (one per cycle)
26
32
  // --substantive "<one-line>" : for the stub block xref
27
33
  // --xref "<changelog-anchor>" : for the stub block
28
- // --substantive-packages a,b,c : packages with [Unreleased] to promote
34
+ // --substantive-packages a,b,c : packages with [Unreleased] to promote (cut AND from-scratch)
29
35
  // --stub-packages a,b,c : packages getting the lockstep stub
30
36
  //
31
37
  // FLAGS:
32
38
  // --dry : preview all commands, no mutation, no prompts
33
- // --yes : skip the 3 operator checkpoints (CI / scripted)
39
+ // --yes : auto-confirm CHECKPOINT 1 ONLY (before tagging). Tag
40
+ // creation is the least destructive of the three
41
+ // mutating boundaries (moving/deleting a local-only
42
+ // tag is free); push and publish each need their own
43
+ // explicit go — --yes does not cover them (H5/defect-2:
44
+ // a single --yes used to flatten all 3 checkpoints).
45
+ // --push : auto-confirm CHECKPOINT 2 (before pushing tags)
46
+ // --publish : auto-confirm CHECKPOINT 3 (before dispatching publishes)
34
47
  // --skip-gates : (DEBUG ONLY) skip pre-flight (don't ship anything!)
35
48
  // --skip-site : skip the EXE deploy step (rare; for offline cycles)
36
49
  //
37
50
  // Part of the adia-release skill. Mechanizes the cut procedure from
38
51
  // references/cut-procedure.md while preserving operator judgment at
39
- // the 3 mutating-checkpoint boundaries.
52
+ // the 3 mutating-checkpoint boundaries. Every checkpoint prints its evidence
53
+ // block BEFORE the prompt (H5) — never a bare y/N.
40
54
 
41
55
  import { execSync } from 'node:child_process';
42
56
  import { createInterface } from 'node:readline/promises';
43
57
  import { stdin, stdout } from 'node:process';
44
58
  import fs from 'node:fs';
59
+ import os from 'node:os';
45
60
  import path from 'node:path';
46
61
  import process from 'node:process';
47
62
  import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
63
+ import { resolvePackageChangelog } from './package-paths.mjs';
64
+ import { GATE_ROSTER } from './gate-roster.mjs';
48
65
 
49
66
  const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
50
67
  const REPO = process.cwd();
@@ -70,7 +87,7 @@ function parseArgs(argv) {
70
87
  versionList: null,
71
88
  host: process.env.ADIA_DEPLOY_HOST || DEFAULT_DEPLOY_HOST,
72
89
  scope: process.env.ADIA_NPM_SCOPE || DEFAULT_NPM_SCOPE,
73
- dry: false, yes: false, skipGates: false, skipSite: false,
90
+ dry: false, yes: false, push: false, publish: false, skipGates: false, skipSite: false,
74
91
  };
75
92
  for (let i = 0; i < argv.length; i++) {
76
93
  const k = argv[i];
@@ -89,6 +106,8 @@ function parseArgs(argv) {
89
106
  else if (k === '--scope') args.scope = argv[++i];
90
107
  else if (k === '--dry') args.dry = true;
91
108
  else if (k === '--yes') args.yes = true;
109
+ else if (k === '--push') args.push = true;
110
+ else if (k === '--publish') args.publish = true;
92
111
  else if (k === '--skip-gates') args.skipGates = true;
93
112
  else if (k === '--skip-site') args.skipSite = true;
94
113
  else if (k === '-h' || k === '--help') { help(); process.exit(0); }
@@ -98,8 +117,17 @@ function parseArgs(argv) {
98
117
  console.error(' see --help');
99
118
  process.exit(2);
100
119
  }
101
- if (!['cut', 'from-scratch', 'handoff', 'batch'].includes(args.mode)) {
102
- console.error(`error: --mode must be cut|from-scratch|handoff|batch (got: ${args.mode})`);
120
+ if (args.mode === 'batch') {
121
+ console.error('error: --mode batch is not implemented — a batch push tags each version at');
122
+ console.error(' its OWN release-merge SHA and enforces oldest-first publish ordering,');
123
+ console.error(' which this single-version orchestrator has no model for. Silently');
124
+ console.error(' falling through to the single-version path was the H2 defect this');
125
+ console.error(' replaced. Follow the manual procedure instead:');
126
+ console.error(' references/recovery-paths.md §Scenario 2 — Batch push');
127
+ process.exit(2);
128
+ }
129
+ if (!['cut', 'from-scratch', 'handoff'].includes(args.mode)) {
130
+ console.error(`error: --mode must be cut|from-scratch|handoff (got: ${args.mode})`);
103
131
  process.exit(2);
104
132
  }
105
133
  return args;
@@ -116,15 +144,28 @@ function help() {
116
144
  --substantive-packages web-components,web-modules,a2ui/corpus \\
117
145
  --stub-packages llm,a2ui/compose,a2ui/mcp,a2ui/retrieval,a2ui/runtime,a2ui/validator
118
146
 
119
- Flags: --dry (preview all) --yes (skip prompts) --skip-site (skip EXE deploy)
147
+ Flags: --dry (preview all) --yes (auto-confirm checkpoint 1 ONLY — tag)
148
+ --push (auto-confirm checkpoint 2 — push tags)
149
+ --publish (auto-confirm checkpoint 3 — dispatch publishes)
150
+ --skip-site (skip EXE deploy)
120
151
  Instance: --host <deploy-host> (default ${DEFAULT_DEPLOY_HOST}; or $ADIA_DEPLOY_HOST)
121
152
  --scope <@org> (npm scope; default ${DEFAULT_NPM_SCOPE}; or $ADIA_NPM_SCOPE)`);
122
153
  }
123
154
 
124
155
  const rl = createInterface({ input: stdin, output: stdout });
125
- async function checkpoint(label, args) {
126
- if (args.yes) {
127
- console.log(`\n[checkpoint] ${label} auto-confirmed (--yes)`);
156
+ // Every checkpoint prints its evidence block BEFORE the y/N prompt (H5)
157
+ // `evidence` is a pre-formatted string (tag list, F-N1 results, registry
158
+ // snapshot); `autoConfirmed` is the SPECIFIC flag that covers THIS checkpoint
159
+ // (--yes for checkpoint 1 only; --push / --publish for 2 and 3 — H5/defect-2:
160
+ // a single --yes used to flatten all three).
161
+ async function checkpoint(label, evidence, autoConfirmed) {
162
+ if (evidence) {
163
+ console.log(`\n--- evidence for: ${label} ---`);
164
+ console.log(evidence);
165
+ console.log('---');
166
+ }
167
+ if (autoConfirmed) {
168
+ console.log(`\n[checkpoint] ${label} — auto-confirmed`);
128
169
  return;
129
170
  }
130
171
  const answer = await rl.question(`\n[CHECKPOINT] ${label} — proceed? [y/N]: `);
@@ -151,6 +192,24 @@ function shQuiet(cmd, args) {
151
192
  return execSync(cmd, { cwd: REPO, encoding: 'utf8' });
152
193
  }
153
194
 
195
+ // Runs a command and CAPTURES its combined output instead of streaming it —
196
+ // used where the output itself becomes evidence in a later checkpoint's
197
+ // evidence block (H5), rather than just scrolling past in the terminal.
198
+ // Never throws: F-N1 legitimately exits non-zero on cosmetic warns; the
199
+ // caller decides what a non-zero code means.
200
+ function shCapture(cmd, args) {
201
+ if (args.dry) {
202
+ console.log(` [dry] ${cmd}`);
203
+ return { code: 0, output: '[dry] (not run)' };
204
+ }
205
+ try {
206
+ const output = execSync(cmd, { cwd: REPO, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
207
+ return { code: 0, output };
208
+ } catch (e) {
209
+ return { code: e.status ?? 1, output: (e.stdout || '') + (e.stderr || '') };
210
+ }
211
+ }
212
+
154
213
  // ── Step 1 — Re-baseline ──────────────────────────────────────────
155
214
  function step1ReBaseline(args) {
156
215
  console.log('\n=== Step 1 — Re-baseline ===');
@@ -173,32 +232,40 @@ function step3PreFlight(args) {
173
232
  console.log('\n=== Step 3 — Pre-flight (SKIPPED via --skip-gates; danger!) ===');
174
233
  return;
175
234
  }
176
- console.log('\n=== Step 3 — Pre-flight gates ===');
177
- const gates = [
178
- 'node scripts/build/components.mjs --verify',
179
- 'npm run verify:traits',
180
- 'npm run check:lockstep',
181
- 'npm run test:unit',
182
- 'npm run typecheck',
183
- 'npm run check:demo-shells',
184
- 'npm run check:lightningcss-build',
185
- 'npm run smoke:engines',
186
- 'npm run smoke:register-engine',
187
- 'npm run check:links',
188
- 'npm run check:embeddings-fresh',
189
- 'npm run verify:corpus',
190
- ];
191
- for (const g of gates) {
192
- console.log(`\n ${g}`);
235
+ console.log(`\n=== Step 3 — Pre-flight gates (${GATE_ROSTER.length}-gate roster — gate-roster.mjs) ===`);
236
+ for (const g of GATE_ROSTER) {
237
+ console.log(`\n ${g.n}/${GATE_ROSTER.length}. ${g.cmd} # ${g.what}`);
193
238
  try {
194
- sh(g, args, { stdio: 'inherit' });
239
+ sh(g.cmd, args, { stdio: 'inherit' });
195
240
  } catch (e) {
196
- console.error(`\n ✗ FAILED: ${g}`);
241
+ console.error(`\n ✗ FAILED (${g.n}/${GATE_ROSTER.length}): ${g.cmd}`);
197
242
  console.error(' Pre-flight aborted. Fix the gate failure before proceeding.');
198
243
  console.error(' See references/gates-catalog.md for failure-mode → recovery.');
199
244
  process.exit(1);
200
245
  }
201
246
  }
247
+ console.log(`\n ✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`);
248
+ }
249
+
250
+ // Read a package's CHANGELOG at repo HEAD (working tree) and report whether a
251
+ // `## [Unreleased]` heading still carries non-empty content — i.e. it rode
252
+ // through un-promoted. Defect 1 (H1-adjacent, audit seed): `--mode cut` never
253
+ // called promote-unreleased.mjs at all, so a peer's hand-authored
254
+ // `## [Unreleased]` section shipped in the tarball unpromoted (bit the
255
+ // v0.8.4 cut — the plugin packages needed hand-promotion because nothing
256
+ // caught it). Returns null when clean, or a short description when dirty.
257
+ function unpromotedUnreleased(repo, pkg) {
258
+ const p = resolvePackageChangelog(repo, pkg);
259
+ if (!fs.existsSync(p)) return null;
260
+ const txt = fs.readFileSync(p, 'utf8');
261
+ const m = txt.match(/^## \[Unreleased\][^\n]*\n([\s\S]*?)(?=^## \[|$(?![\s\S]))/m);
262
+ if (!m) return null;
263
+ // Strip subsection headers (### Added, ### Fixed, …) and blank lines — what's
264
+ // left is real content, not just empty scaffolding.
265
+ const body = m[1].replace(/^###.*$/gm, '').trim();
266
+ if (body.length === 0) return null;
267
+ const firstLine = body.split('\n')[0].slice(0, 100);
268
+ return `${pkg}: "${firstLine}${firstLine.length === 100 ? '…' : ''}"`;
202
269
  }
203
270
 
204
271
  // ── Step 4 — Promote + bump + lockfile ───────────────────────────
@@ -209,7 +276,12 @@ function step4PromoteAndBump(args) {
209
276
  }
210
277
  console.log('\n=== Step 4 — Promote + Bump + Lockfile ===');
211
278
 
212
- if (args.mode === 'from-scratch' && args.substantivePackages) {
279
+ // Promotion runs for BOTH cut and from-scratch now a peer's pre-staged
280
+ // [Unreleased] content needs promoting in either mode; only the CALLER
281
+ // (whether a peer already promoted vs. this cycle authoring fresh) differs,
282
+ // not whether promotion is needed. `--substantive-packages` is the same flag
283
+ // in both modes.
284
+ if (args.substantivePackages) {
213
285
  const cmd = `node ${SCRIPT_DIR}/promote-unreleased.mjs --version ${args.version} --date ${args.date} --packages ${args.substantivePackages.join(',')}`;
214
286
  sh(cmd, args);
215
287
  }
@@ -219,6 +291,23 @@ function step4PromoteAndBump(args) {
219
291
  sh(cmd, args);
220
292
  }
221
293
 
294
+ // Loud guard: after promotion, NO package in the lockstep set (including
295
+ // the 2 plugin packages, which required hand-promotion at v0.8.4) may still
296
+ // carry a non-empty [Unreleased] — that content would ride through the
297
+ // bump unpromoted, undocumented in the tarball's own CHANGELOG.
298
+ if (!args.dry) {
299
+ const dirty = PACKAGES.map((p) => unpromotedUnreleased(REPO, p)).filter(Boolean);
300
+ if (dirty.length > 0) {
301
+ console.error(`\nERROR: ${dirty.length} package(s) still have non-empty [Unreleased] content after promotion:`);
302
+ for (const d of dirty) console.error(` ✗ ${d}`);
303
+ console.error('\n Add the missing package(s) to --substantive-packages (or --stub-packages if');
304
+ console.error(' they are pure ride-alongs) and re-run. See changelog-discipline.md §Detecting');
305
+ console.error(' source-changing packages.');
306
+ process.exit(1);
307
+ }
308
+ console.log(` ✓ no unpromoted [Unreleased] content across all ${PACKAGES.length} packages`);
309
+ }
310
+
222
311
  sh(`node ${SCRIPT_DIR}/bump.mjs --from ${args.previous} --to ${args.version}`, args);
223
312
  sh('npm install --package-lock-only --no-audit --no-fund', args);
224
313
  sh('npm run check:lockstep', args);
@@ -262,23 +351,45 @@ function step5Commit(args) {
262
351
  sh(`git commit -F ${args.commitMessageFile}`, args);
263
352
  }
264
353
 
265
- // ── Step 6 — Tag ─────────────────────────────────────────────────
354
+ // ── Step 6 — Tag ← operator checkpoint 1 (before tagging) ────────
266
355
  async function step6Tag(args) {
267
356
  console.log('\n=== Step 6 — Tag at HEAD ===');
268
- await checkpoint('Ready to create 12 tags (umbrella + 11 per-package)', args);
357
+ const tagList = [`v${args.version}`, ...PACKAGES.map((p) => `${p}-v${args.version}`)];
358
+ const evidence = `Planned tags (${tagList.length} — umbrella + ${PACKAGES.length} per-package):\n` +
359
+ tagList.map((t) => ` ${t}`).join('\n');
360
+ // --yes covers ONLY this checkpoint (defect-2/H5) — moving/deleting a
361
+ // local-only tag is free, so this is the least destructive of the three.
362
+ await checkpoint('Create tags at HEAD — before tagging', evidence, args.yes);
269
363
  sh(`node ${SCRIPT_DIR}/tag-lockstep.mjs --version ${args.version}`, args);
364
+ console.log(` ✓ created ${tagList.length} tags`);
270
365
  }
271
366
 
272
- // ── Step 7 — F-N1 ────────────────────────────────────────────────
367
+ // ── Step 7 — F-N1 ─────────────────────────────────────────────────
368
+ // Captures output (rather than streaming it) so checkpoint 2 can show the
369
+ // per-package F-N1 results as evidence BEFORE the push prompt (H5) — the F-N1
370
+ // output used to scroll past in Step 7 with no connection to Step 8's prompt.
273
371
  function step7Fn1(args) {
274
372
  console.log('\n=== Step 7 — F-N1 release trip-wire ===');
275
- try {
276
- sh('node scripts/release/check-release.mjs --all-pending', args);
277
- } catch (e) {
278
- // F-N1 sometimes exits non-zero on cosmetic warns; show the output and let the operator decide
279
- console.warn('\n F-N1 returned non-zero review the warns above.');
280
- console.warn(' See references/changelog-discipline.md § F-N1 diff-coverage enrichment.');
373
+ const { code, output } = shCapture('node scripts/release/check-release.mjs --all-pending', args);
374
+ console.log(output);
375
+ // CodeRabbit (PR #289): a non-zero exit here (without --strict, which this
376
+ // call doesn't pass) is ALWAYS a real `error` finding check-release.mjs
377
+ // only sets a non-zero code for `error` severity; cosmetic `warn`s leave it
378
+ // 0. Treating non-zero as "just review the warns" let --push auto-confirm
379
+ // straight past a known trip-wire failure. Fail closed: hard-stop HERE,
380
+ // before Step 8 exists at all — no flag can skip this.
381
+ if (code !== 0 && !args.dry) {
382
+ console.error('\n ✗ F-N1 reported an ERROR (not a cosmetic warn) — release-pack stops here.');
383
+ console.error(' Fix the finding above, land it as a new commit through the PR flow (the');
384
+ console.error(' release commit is already merged — see changelog-discipline.md §F-N1');
385
+ console.error(' diff-coverage enrichment), delete + re-create the tags, re-run.');
386
+ process.exit(1);
281
387
  }
388
+ // Cosmetic warns (exit 0, but the output carries `[warn]` lines) are real
389
+ // signal an operator should read before pushing — never let --push silently
390
+ // wave them through; only the interactive prompt may proceed past a warn.
391
+ const hasWarn = /\[warn\]/.test(output);
392
+ return { output, hasWarn };
282
393
  }
283
394
 
284
395
  // ── Step 8 — Push ────────────────────────────────────────────────
@@ -296,7 +407,7 @@ function step7Fn1(args) {
296
407
  // orchestrated cut does NOT rely on push-on-tag firing. The one-at-a-time push
297
408
  // is defensive (a cut aborted after Step 8 still has its publishes triggered)
298
409
  // AND keeps the standalone tag-lockstep.mjs hint honest.
299
- async function step8Push(args) {
410
+ async function step8Push(args, fn1) {
300
411
  console.log('\n=== Step 8 — Push tags (per-tag, NOT batched) ===');
301
412
  // Invariant 3: main reached this state via a merged release PR — never a
302
413
  // direct push from here. Assert we're tagging the pushed history.
@@ -305,14 +416,22 @@ async function step8Push(args) {
305
416
  console.error(`ERROR: HEAD is ${ahead} commit(s) ahead of origin/main — release commits land via PR (invariant 3); merge first, then tag at post-merge main.`);
306
417
  process.exit(1);
307
418
  }
308
- await checkpoint('Ready to push 12 tags (one-at-a-time)', args);
309
419
  const perPkgTags = PACKAGES.map((p) => `${p}-v${args.version}`);
420
+ const evidence = `F-N1 results (Step 7):\n${fn1?.output || '(none captured)'}\n` +
421
+ `Tags to push (${perPkgTags.length + 1} — one push per tag, umbrella last):\n` +
422
+ [...perPkgTags, `v${args.version}`].map((t) => ` ${t}`).join('\n');
423
+ // --push is its OWN flag — NOT covered by --yes (defect-2/H5). CodeRabbit
424
+ // (PR #289): a cosmetic F-N1 warn must still force the interactive prompt —
425
+ // --push may NOT auto-confirm past ANY F-N1 finding, only a fully clean
426
+ // run. (A real F-N1 error never reaches here — step7Fn1 hard-exits first.)
427
+ const autoConfirm = args.push && !fn1?.hasWarn;
428
+ await checkpoint('Push tags — before pushing', evidence, autoConfirm);
310
429
  for (const t of perPkgTags) {
311
430
  sh(`git push origin ${t}`, args);
312
431
  }
313
432
  // Umbrella tag last — triggers no workflow (convention only).
314
433
  sh(`git push origin v${args.version}`, args);
315
- console.log(`\n ${perPkgTags.length} per-package tags + 1 umbrella pushed individually.`);
434
+ console.log(`\n ✓ pushed ${perPkgTags.length} per-package tags + 1 umbrella tag`);
316
435
  console.log(' NOTE: a single `git push origin <all tags>` can skip ALL publish-on-tag');
317
436
  console.log(' triggers (the GitHub batch-push skip). Step 9 dispatches via');
318
437
  console.log(' workflow_dispatch + verifies each triggered — see recovery-paths.md §Scenario 7.');
@@ -324,9 +443,30 @@ async function step8Push(args) {
324
443
  // exist for the <pkg>-vX.Y.Z tags and re-dispatches ONLY the misses — closing the
325
444
  // batch-tag-push skip without double-publishing the ones that already fired.
326
445
  // Idempotent: if all 11 triggered (or none did), it does the right thing.
446
+ // Snapshot the CURRENT registry state — the "ordering evidence" for
447
+ // checkpoint 3 (H5): what's live right now, before anything is dispatched,
448
+ // so the operator can confirm publish order/target against reality rather
449
+ // than a bare y/N.
450
+ function registrySnapshot(args) {
451
+ if (args.dry) return '[dry] (registry snapshot skipped)';
452
+ const lines = [];
453
+ for (const pkg of PACKAGES) {
454
+ let v;
455
+ try { v = execSync(`npm view "${args.scope}/${pkg}" version`, { encoding: 'utf8' }).trim(); }
456
+ catch { v = 'MISSING/E404'; }
457
+ lines.push(` ${pkg}: ${v}${v === args.version ? ' (already at target — idempotent re-dispatch)' : ''}`);
458
+ }
459
+ let latest = 'unknown';
460
+ try { latest = execSync(`npm view ${args.scope}/web-components dist-tags.latest`, { encoding: 'utf8' }).trim(); } catch { /* offline */ }
461
+ lines.push(` dist-tags.latest: ${latest}`);
462
+ return `Registry state BEFORE publish (target = ${args.version}):\n${lines.join('\n')}`;
463
+ }
464
+
327
465
  async function step9Publish(args) {
328
466
  console.log('\n=== Step 9 — Verify-triggered + re-dispatch misses ===');
329
- await checkpoint('Ready to verify the 11 publish triggers + re-dispatch any misses', args);
467
+ const evidence = registrySnapshot(args);
468
+ // --publish is its OWN flag — NOT covered by --yes (defect-2/H5).
469
+ await checkpoint('Dispatch publishes — before publishing', evidence, args.publish);
330
470
  // Give GH a moment to index the runs created by the per-tag push in Step 8.
331
471
  if (!args.dry) sh('sleep 6', args);
332
472
  sh(`node ${SCRIPT_DIR}/dispatch-publish.mjs --version ${args.version} --scope ${args.scope} --verify-triggered`, args);
@@ -335,7 +475,7 @@ async function step9Publish(args) {
335
475
  console.log(' ✓ publishes settled');
336
476
  // Fail-closed: the registry is the verify target (SKILL.md) — a missing or
337
477
  // stale package aborts the cut here, before GH releases + site deploy.
338
- console.log(`\n Verify all ${PACKAGES.length} at ${args.version} + npm latest (fail-closed):`);
478
+ console.log(`\n Post-publish registry verification, all ${PACKAGES.length} at ${args.version} + npm latest (fail-closed):`);
339
479
  sh(
340
480
  `fail=0; ` +
341
481
  `for pkg in ${PACKAGES.join(' ')}; do ` +
@@ -369,8 +509,24 @@ async function step10GhAndSite(args) {
369
509
  console.log(' --skip-site set; skipping EXE deploy');
370
510
  return;
371
511
  }
372
- sh('npm run build:site', args);
373
- sh(`rsync -az --delete dist/ ${args.host}:/srv/adia-ui/dist/`, args);
512
+ // H1 (forge-campaign gh#268 audit): this used to `npm run build:site` +
513
+ // raw `rsync -az --delete` straight to the host — bypassing deploy-site.yml
514
+ // entirely (its build → pre-flight verify → snapshot → rsync → post-deploy
515
+ // verify → auto-rollback pipeline), even though cut-procedure.md §Step 10
516
+ // already documented the workflow-dispatch path as the correct one. A raw
517
+ // local rsync has no snapshot to roll back to on a bad deploy. Dispatch the
518
+ // real pipeline instead — gated behind its own confirm (deploy is
519
+ // outward-facing exactly like publish; --publish covers it, or an
520
+ // interactive confirm if not given).
521
+ const evidence = `Dispatching "Deploy site (${args.host})" (.github/workflows/deploy-site.yml) —\n` +
522
+ `builds from main, runs pre-flight verify → snapshot → hardened rsync →\n` +
523
+ `post-deploy verify → auto-rollback. NOT a raw rsync (the bypass this replaced).`;
524
+ await checkpoint('Deploy site — before dispatching deploy-site.yml', evidence, args.publish);
525
+ sh(`gh workflow run "Deploy site (${args.host})" --repo adiahealth/gen-ui-kit --ref main`, args);
526
+ console.log(' Waiting for the deploy workflow to settle...');
527
+ if (!args.dry) sh('sleep 10', args);
528
+ sh(`until [ "$(gh run list --workflow=deploy-site.yml --limit 1 --json status -q '.[0].status')" = "completed" ]; do sleep 5; done`, args);
529
+ console.log(' ✓ deploy-site.yml settled');
374
530
  // Verify a deployed CONTENT FILE, never an SPA route — the docs site
375
531
  // returns HTTP 200 + a blank shell for ANY unmatched route (v0.7.4 trap).
376
532
  sh(`curl -s -o /dev/null -w "EXE host.css: HTTP %{http_code}\\n" https://${args.host}/packages/web-components/styles/host.css`, args);
@@ -405,16 +561,91 @@ async function main() {
405
561
  return;
406
562
  }
407
563
  await step6Tag(args);
408
- step7Fn1(args);
409
- await step8Push(args);
564
+ const fn1 = step7Fn1(args);
565
+ await step8Push(args, fn1);
410
566
  await step9Publish(args);
411
567
  await step10GhAndSite(args);
412
568
  rl.close();
413
569
  console.log('\n✓ release-pack complete. Verify against the npm registry + GH releases, then author the release notes.');
414
570
  }
415
571
 
416
- main().catch((e) => {
417
- console.error(e);
572
+ // Plan-assembly selftest — spawns THIS script as a real subprocess with
573
+ // --mode cut --dry (fully mutation-free per every step's own dry-guard) and
574
+ // asserts the assembled plan carries the expected markers: the full
575
+ // 17-gate roster (H3 — a subset run must be impossible without editing
576
+ // gate-roster.mjs), and the invariant-3 stop-before-PR message (cut mode
577
+ // must never proceed to tag/push/publish on its own). A fixture temp dir
578
+ // supplies the operator-authored content files (--commit-message-file /
579
+ // --gh-notes-file) so nothing here depends on real repo state.
580
+ function selftest() {
581
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'release-pack-selftest-'));
582
+ try {
583
+ const commitFile = path.join(tmp, 'commit.txt');
584
+ const notesFile = path.join(tmp, 'notes.md');
585
+ fs.writeFileSync(commitFile, 'selftest fixture commit message\n');
586
+ fs.writeFileSync(notesFile, 'selftest fixture notes\n');
587
+
588
+ const scriptPath = fileURLToPathCompat(import.meta.url);
589
+ let out;
590
+ try {
591
+ out = execSync(
592
+ `node "${scriptPath}" --mode cut --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8 ` +
593
+ `--commit-message-file "${commitFile}" --gh-notes-file "${notesFile}" ` +
594
+ `--substantive-packages web-components --stub-packages llm --dry --yes`,
595
+ { cwd: REPO, encoding: 'utf8' },
596
+ );
597
+ } catch (e) {
598
+ console.error('selftest FAIL: --mode cut --dry plan assembly exited non-zero');
599
+ console.error(e.stdout || e.message);
600
+ process.exit(1);
601
+ }
602
+
603
+ const need = [
604
+ `${GATE_ROSTER.length}-gate roster`,
605
+ `✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`,
606
+ 'STOPPING per invariant 3',
607
+ ];
608
+ for (const marker of need) {
609
+ if (!out.includes(marker)) {
610
+ console.error(`selftest FAIL: expected marker not found in plan output: "${marker}"`);
611
+ process.exit(1);
612
+ }
613
+ }
614
+
615
+ // The batch-mode hard-reject (H2) must fire BEFORE any of the above runs.
616
+ let batchOut = '';
617
+ let batchFailed = false;
618
+ try {
619
+ execSync(`node "${scriptPath}" --mode batch --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8`, { cwd: REPO, encoding: 'utf8' });
620
+ } catch (e) {
621
+ batchFailed = true;
622
+ batchOut = (e.stdout || '') + (e.stderr || '');
623
+ }
624
+ if (!batchFailed || !batchOut.includes('Scenario 2')) {
625
+ console.error('selftest FAIL: --mode batch must hard-reject with a pointer to recovery-paths.md §Scenario 2');
626
+ process.exit(1);
627
+ }
628
+
629
+ console.log('selftest OK');
630
+ } finally {
631
+ fs.rmSync(tmp, { recursive: true, force: true });
632
+ }
633
+ }
634
+
635
+ // import.meta.url → filesystem path (works the same on the platforms this
636
+ // skill runs on; avoids pulling in node:url's fileURLToPath just for this).
637
+ function fileURLToPathCompat(url) {
638
+ return new URL(url).pathname;
639
+ }
640
+
641
+ const topArgv = process.argv.slice(2);
642
+ if (topArgv[0] === 'selftest') {
418
643
  rl.close();
419
- process.exit(1);
420
- });
644
+ selftest();
645
+ } else {
646
+ main().catch((e) => {
647
+ console.error(e);
648
+ rl.close();
649
+ process.exit(1);
650
+ });
651
+ }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // tag-lockstep.mjs — create 10 lockstep tags (umbrella vX.Y.Z + 9
3
- // per-package <pkg>-vX.Y.Z) at HEAD or at a specified SHA.
2
+ // tag-lockstep.mjs — create 12 lockstep tags (umbrella vX.Y.Z + 11
3
+ // per-package <pkg>-vX.Y.Z, including the 2 plugin packages that joined
4
+ // the lockstep 2026-07-15) at HEAD or at a specified SHA.
4
5
  //
5
6
  // Usage:
6
7
  // node tag-lockstep.mjs --version 0.6.22
@@ -114,9 +115,30 @@ function main() {
114
115
  }
115
116
  }
116
117
 
117
- try {
118
- main();
119
- } catch (e) {
120
- console.error(`error: ${e.message}`);
121
- process.exit(1);
118
+ function selftest() {
119
+ const tags = buildTagList('1.2.3');
120
+ if (tags.length !== 12) {
121
+ console.error(`selftest FAIL: expected 12 tags (umbrella + 11 per-package), got ${tags.length}`); process.exit(1);
122
+ }
123
+ if (tags[0] !== 'v1.2.3') {
124
+ console.error('selftest FAIL: umbrella tag must be first'); process.exit(1);
125
+ }
126
+ for (const expected of ['web-components-v1.2.3', 'adia-ui-factory-v1.2.3', 'adia-ui-forge-v1.2.3']) {
127
+ if (!tags.includes(expected)) {
128
+ console.error(`selftest FAIL: expected tag '${expected}' missing from buildTagList output`); process.exit(1);
129
+ }
130
+ }
131
+ console.log('selftest OK');
132
+ }
133
+
134
+ const topArgv = process.argv.slice(2);
135
+ if (topArgv[0] === 'selftest') {
136
+ selftest();
137
+ } else {
138
+ try {
139
+ main();
140
+ } catch (e) {
141
+ console.error(`error: ${e.message}`);
142
+ process.exit(1);
143
+ }
122
144
  }
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: adia-ssr
3
+ description: >-
4
+ Answers why an AdiaUI component crashes, drops content, or renders wrong
5
+ under SSR (linkedom/Astro consumers) — the three known failure shapes,
6
+ what's fixed vs open, how to test without a linkedom install. Use when
7
+ asked "does this work under SSR", "why does X crash/disappear when
8
+ server-rendered", or "is this connect-time read safe". ANSWERS only. NOT
9
+ for implementing a fix (adia-author) or consumer host/hydration wiring
10
+ (adia-host, adia-ui-factory plugin).
11
+ disable-model-invocation: false
12
+ user-invocable: false
13
+ ---
14
+
15
+ # adia-ssr — does it work under SSR, and why not if it doesn't
16
+
17
+ Two real consumers (adiav2's `admin-portal-fe` and `factory-dashboard`) server-render
18
+ AdiaUI's light-DOM components via Astro 5 + `custom-elements-ssr`, which runs on
19
+ linkedom — a DOM shim with no layout engine and missing browser APIs the framework's
20
+ base class assumes exist. Every SSR bug that's surfaced maps onto one of three root-cause
21
+ shapes; misclassifying a new report against the wrong shape sends the investigation
22
+ to the wrong fix (or worse, invents a redundant one). This pack answers "which shape is
23
+ this" and "what's the state of each shape's fix" — it never carries the fix itself.
24
+
25
+ ## The three shapes, in one line each
26
+
27
+ 1. **A browser-only API is called unconditionally → crash.** `attachInternals`,
28
+ the four Observer constructors, `document.adoptedStyleSheets`. **Fixed** (gh#285).
29
+ 2. **`connectedCallback` destructively re-stamps existing DOM → silent content loss.**
30
+ The mechanism is real, but **narrowed to a static audit** (gh#284, 2026-07-17) —
31
+ zero shipped components currently pair a non-null template with real light-DOM
32
+ content, so nothing is exposed today; a forward audit catches a future regression.
33
+ 3. **A connect-time layout measurement is treated as confirmed, not unknown.** A zero
34
+ rect (shim, or real pre-layout connect) drives a wrong persistent decision.
35
+ **Fixed for one component** (gh#286); the general pattern is unswept.
36
+
37
+ A fourth item, **property-only components can't seed initial state from SSR HTML**
38
+ (gh#288), is a feature gap explicitly blocked on shape 2's resolution — don't treat it
39
+ as its own independent task.
40
+
41
+ Full symptom → root-cause → status detail, cited to the actual shipped/open code:
42
+ [`references/failure-shapes.md`](references/failure-shapes.md).
43
+
44
+ ## Consult table
45
+
46
+ | Ask | Answer from |
47
+ | --- | --- |
48
+ | "why does `<text-ui>`/`<avatar-ui>`/a container lose its content under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §2 — shape 2, NARROWED (doesn't currently reproduce against any shipped component; see the survey before assuming a new report fits this shape) |
49
+ | "why does this crash / throw at construction under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §1 — shape 1, FIXED; the exact guard shape to copy for a NEW instance is [`guard-patterns.md`](references/guard-patterns.md) §1 |
50
+ | "is this connect-time `getBoundingClientRect()`/measurement read safe" | [`failure-shapes.md`](references/failure-shapes.md) §3 + [`guard-patterns.md`](references/guard-patterns.md) §3 — the fixed component's exact shape, and the "unknown ≠ confirmed" principle to apply elsewhere |
51
+ | "what's fixed vs still open for SSR support" | [`status-ledger.md`](references/status-ledger.md) — re-verify against `gh issue view` before trusting it, it drifts |
52
+ | "how do I test an SSR gap — we have no linkedom installed" | [`test-without-linkedom.md`](references/test-without-linkedom.md) — the delete/try/finally pattern, and what it does NOT prove |
53
+ | "what's the consumer's current workaround, and can they drop it yet" | [`consumer-workarounds.md`](references/consumer-workarounds.md) |
54
+ | "table/chart/select renders empty in the SSR response" | [`failure-shapes.md`](references/failure-shapes.md) §4 — shape 4, blocked on shape 2 |
55
+
56
+ ## Deviation doctrine
57
+
58
+ Every fix pattern this pack cites ([`guard-patterns.md`](references/guard-patterns.md))
59
+ carries the reasoning for why it looks the way it does — the no-op `ElementInternals`
60
+ shim exists because leaving the field `undefined` would relocate a crash, not remove
61
+ it; deletion-based testing exists because this repo has no `linkedom` devDependency.
62
+ If a new case doesn't fit an existing pattern's reasoning, that's a signal to design a
63
+ new pattern. Route it through `adia-author` — don't force-fit the nearest existing
64
+ shape.
65
+
66
+ ## Boundaries
67
+
68
+ - **Implementing any fix — new or matching an existing pattern.** Route to
69
+ `adia-author`. This pack orients and cites; it never edits
70
+ `packages/web-components/core/` or any component source.
71
+ - **Consumer-side SSR/hydration host wiring** (an app's own Astro/Next config, which
72
+ rendering mode to pick, how to structure the hydration boundary). Route to
73
+ `adia-host` (adia-ui-factory plugin) — this pack answers whether the FRAMEWORK's own
74
+ components are SSR-safe, not how a consumer app should be structured around them.
75
+ - **A2UI runtime/generation-pipeline SSR concerns** (if any surface later). Route to
76
+ `adia-a2ui` — this pack is scoped to `UIElement`'s own lifecycle and the
77
+ component/trait/module layer, not the generative pipeline.
78
+ - **Not a general web-components-SSR tutorial.** Every claim here is cited to this
79
+ specific framework's actual code, issues, and PRs — general SSR/custom-elements
80
+ theory that isn't grounded in this repo's own history doesn't belong in this pack.
81
+
82
+ ## Worked example — the answer contract
83
+
84
+ **Ask:** "`<text-ui>Adia Admin</text-ui>` renders as an empty tag in our SSR output —
85
+ is this a known issue?"
86
+
87
+ **Answer:** It matches shape 2's SYMPTOM (`connectedCallback` destructively
88
+ re-stamping existing DOM), but shape 2 was narrowed on 2026-07-17 — verify against
89
+ the CURRENT code before reusing the old answer, because this is exactly the case it
90
+ no longer covers. `text-ui`'s `static template` (`packages/web-components/components/text/text.class.js`)
91
+ is `() => null` — `connectedCallback`'s `if (result) stamp(result, this)` never
92
+ enters the branch, so `stamp()` never touches `<text-ui>`'s children at all. This
93
+ component isn't exposed to shape 2; something else is dropping the text — check
94
+ whether `text-ui` is even registered server-side (a different, structural gap: is
95
+ the tag defined before the SSR pass runs?), or whether another mutation (a parent
96
+ re-render, `innerHTML` elsewhere) is clearing it. **The general lesson, not just
97
+ this one component:** before answering "yes, known issue, shape 2" for ANY new
98
+ report, grep the component's own `static template` — if it's the literal
99
+ `() => null`, shape 2 cannot be the cause, no matter how closely the symptom
100
+ matches the old description. [`failure-shapes.md`](references/failure-shapes.md)
101
+ §2 has the full survey (150 components checked) and cites exactly why every
102
+ current children-accepting component is unaffected.
103
+
104
+ ## Corpus of record
105
+
106
+ Routing-eval corpus: [`evals/routing-corpus.json`](evals/routing-corpus.json).
107
+ This pack is hand-authored from verified session work (not a research-wave corpus) —
108
+ re-sync it per [`status-ledger.md`](references/status-ledger.md)'s own instructions
109
+ whenever an issue referenced here changes state, rather than letting the ledger and
110
+ reality drift apart silently.