@adia-ai/adia-ui-forge 0.8.4 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -2
- package/agents/framework-verifier.md +13 -5
- package/bin/release-pretag-docs-gate +226 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
- package/skills/adia-a2ui/SKILL.md +10 -2
- package/skills/adia-a2ui/references/pipeline-overview.md +1 -1
- package/skills/adia-author/SKILL.md +1 -0
- package/skills/adia-author/references/anti-patterns.md +7 -7
- package/skills/adia-author/references/api-contract.md +9 -9
- package/skills/adia-author/references/authoring-cycle.md +6 -6
- package/skills/adia-author/references/code-style.md +4 -4
- package/skills/adia-author/references/lifecycle-patterns.md +8 -8
- package/skills/adia-author/references/token-contract.md +5 -5
- package/skills/adia-author/references/worked-example.md +16 -16
- package/skills/adia-author/references/yaml-contract.md +1 -1
- package/skills/adia-deploy/SKILL.md +44 -0
- package/skills/adia-deploy/evals/routing-corpus.json +146 -0
- package/skills/adia-deploy/references/deploy-playbooks.md +1 -1
- package/skills/adia-dogfood/SKILL.md +64 -11
- package/skills/adia-dogfood/evals/routing-corpus.json +146 -0
- package/skills/adia-gen-review/SKILL.md +55 -0
- package/skills/adia-gen-review/evals/routing-corpus.json +146 -0
- package/skills/adia-llm-internals/evals/routing-corpus.json +146 -0
- package/skills/adia-release/SKILL.md +12 -5
- package/skills/adia-release/evals/evals.json +24 -0
- package/skills/adia-release/references/changelog-discipline.md +4 -4
- package/skills/adia-release/references/cut-procedure.md +45 -11
- package/skills/adia-release/references/gates-catalog.md +3 -3
- package/skills/adia-release/references/recovery-paths.md +4 -2
- package/skills/adia-release/scripts/bump.mjs +32 -1
- package/skills/adia-release/scripts/dispatch-publish.mjs +49 -11
- package/skills/adia-release/scripts/gate-roster.mjs +71 -0
- package/skills/adia-release/scripts/insert-stub.mjs +35 -1
- package/skills/adia-release/scripts/promote-unreleased.mjs +35 -1
- package/skills/adia-release/scripts/release-pack.mjs +284 -53
- package/skills/adia-release/scripts/tag-lockstep.mjs +29 -7
- package/skills/adia-ssr/SKILL.md +110 -0
- package/skills/adia-ssr/evals/audit-report.md +63 -0
- package/skills/adia-ssr/evals/routing-corpus.json +138 -0
- package/skills/adia-ssr/references/consumer-workarounds.md +63 -0
- package/skills/adia-ssr/references/failure-shapes.md +122 -0
- package/skills/adia-ssr/references/guard-patterns.md +142 -0
- package/skills/adia-ssr/references/status-ledger.md +35 -0
- 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
|
-
//
|
|
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 :
|
|
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 (
|
|
102
|
-
console.error(
|
|
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 (
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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(
|
|
177
|
-
const
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
373
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
// per-package <pkg>-vX.Y.Z
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|