@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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +16 -2
  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-release/SKILL.md +12 -5
  27. package/skills/adia-release/evals/evals.json +24 -0
  28. package/skills/adia-release/references/changelog-discipline.md +4 -4
  29. package/skills/adia-release/references/cut-procedure.md +45 -11
  30. package/skills/adia-release/references/gates-catalog.md +3 -3
  31. package/skills/adia-release/references/recovery-paths.md +4 -2
  32. package/skills/adia-release/scripts/bump.mjs +32 -1
  33. package/skills/adia-release/scripts/dispatch-publish.mjs +49 -11
  34. package/skills/adia-release/scripts/gate-roster.mjs +71 -0
  35. package/skills/adia-release/scripts/insert-stub.mjs +35 -1
  36. package/skills/adia-release/scripts/promote-unreleased.mjs +35 -1
  37. package/skills/adia-release/scripts/release-pack.mjs +284 -53
  38. package/skills/adia-release/scripts/tag-lockstep.mjs +29 -7
  39. package/skills/adia-ssr/SKILL.md +110 -0
  40. package/skills/adia-ssr/evals/audit-report.md +63 -0
  41. package/skills/adia-ssr/evals/routing-corpus.json +138 -0
  42. package/skills/adia-ssr/references/consumer-workarounds.md +63 -0
  43. package/skills/adia-ssr/references/failure-shapes.md +122 -0
  44. package/skills/adia-ssr/references/guard-patterns.md +142 -0
  45. package/skills/adia-ssr/references/status-ledger.md +35 -0
  46. package/skills/adia-ssr/references/test-without-linkedom.md +113 -0
@@ -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();