@adia-ai/adia-ui-forge 0.8.34 → 0.8.35
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 +20 -0
- package/README.md +12 -4
- package/agents/a2ui-builder.md +12 -8
- package/agents/component-author.md +17 -12
- package/agents/framework-reviewer.md +18 -12
- package/agents/release-builder.md +15 -10
- package/commands/deploy.md +2 -0
- package/commands/dogfood.md +2 -0
- package/commands/gen-review.md +2 -0
- package/commands/release.md +3 -1
- package/package.json +4 -1
- package/scripts/forge-lint +38 -1
- package/scripts/release-pretag-docs-gate +45 -3
- package/skills/adia-a2ui/SKILL.md +2 -2
- package/skills/adia-author/SKILL.md +1 -1
- package/skills/adia-author/references/anti-patterns.md +1 -0
- package/skills/adia-author/references/code-style.md +3 -3
- package/skills/adia-author/references/worked-example.md +3 -3
- package/skills/adia-author/references/yaml-contract.md +42 -0
- package/skills/adia-deploy/SKILL.md +10 -11
- package/skills/adia-dogfood/SKILL.md +5 -4
- package/skills/adia-dogfood/references/admin-shell-anatomy.md +5 -2
- package/skills/adia-dogfood/references/app-shell-pitfalls.md +10 -3
- package/skills/adia-dogfood/scripts/analyze.mjs +2 -0
- package/skills/adia-gen-review/SKILL.md +10 -3
- package/skills/adia-gen-review/references/loop-protocol.md +15 -8
- package/skills/adia-gen-review/references/rubric-score.md +5 -2
- package/skills/adia-gen-review/scripts/gen-review-status.mjs +8 -6
- package/skills/adia-llm-internals/SKILL.md +1 -1
- package/skills/adia-release/SKILL.md +2 -2
- package/skills/adia-release/references/cut-procedure.md +4 -0
- package/skills/adia-release/references/gates-catalog.md +3 -1
- package/skills/adia-release/references/independent-package-release.md +2 -2
- package/skills/adia-release/scripts/bump.mjs +7 -3
- package/skills/adia-release/scripts/dispatch-publish.mjs +5 -2
- package/skills/adia-release/scripts/gate-roster.mjs +29 -0
- package/skills/adia-release/scripts/package-paths.mjs +41 -13
- package/skills/adia-release/scripts/pr-bridge.mjs +36 -2
- package/skills/adia-release/scripts/promote-unreleased.mjs +27 -7
- package/skills/adia-release/scripts/release-pack.mjs +24 -11
- package/skills/adia-release/scripts/tag-lockstep.mjs +10 -8
- package/skills/adia-site-docs/SKILL.md +8 -6
- package/skills/adia-site-docs/intent.md +1 -1
- package/skills/adia-ssr/SKILL.md +1 -1
- package/skills/adia-ssr/references/failure-shapes.md +8 -5
- package/skills/adia-ssr/references/guard-patterns.md +1 -1
- package/skills/adia-site-docs/evals/audit-report.md +0 -30
- package/skills/adia-ssr/evals/audit-report.md +0 -63
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
Script: `scripts/dev/audit-shell-composition.mjs` (repo-local), npm gates
|
|
4
4
|
`audit:shell-composition{,:strict,:all}`. Static AST walk over
|
|
5
5
|
`apps/**/*.html`, `playgrounds/**/*.html`, `catalog/page-shells/**/*.html` —
|
|
6
|
-
no browser needed; pre-commit fast. The script
|
|
7
|
-
|
|
6
|
+
no browser needed; pre-commit fast. The script's own `PARTS` array is the
|
|
7
|
+
mechanical census — it has grown past this list (count it in
|
|
8
|
+
`scripts/dev/audit-shell-composition.mjs`, never from a hand-typed number
|
|
9
|
+
here); this numbered list is the human review standard, and the findings
|
|
10
|
+
table below samples common symptoms rather than the script's full roster.
|
|
8
11
|
Canonical source: the monorepo's `site/index.html` — the markup that renders
|
|
9
12
|
the live admin-dashboard example.
|
|
10
13
|
|
|
@@ -20,10 +20,17 @@ may stall — wait 30s, re-run.
|
|
|
20
20
|
| 4 | Top-level `await` without async setup wrap | `[setup-failed]` console error | `export default async function setup(host) { … }` |
|
|
21
21
|
| 5 | Vite import-analysis 500 on dynamic import | `[network-4xx] 500` for `./<name>.contents.js` | add `/* @vite-ignore */` to the dynamic import |
|
|
22
22
|
| 6 | icon-ui not imported despite `<icon-ui>` / `[icon=…]` / icon-rendering composites | `[icon-ui-missing]` | `import "/packages/web-components/components/icon/icon.js"` |
|
|
23
|
+
| 7 | `<admin-page-body>` emitted without its `<admin-page>` ancestor (gh#981) | *(no audit-app-shells.mjs tag — apps/-only script, doesn't sweep this surface)* | wrap in `<admin-page>` — `admin-page > admin-page-body { flex:1; … }` (`admin-shell.bespoke.css:144`) is a direct-child selector; without that literal parent, `admin-page-body` falls back to UA `display:inline` |
|
|
23
24
|
|
|
24
|
-
Secondary signals: `[collapsed-element]` (registered but 0px tall
|
|
25
|
-
`
|
|
26
|
-
`
|
|
25
|
+
Secondary signals: `[collapsed-element]` (registered but 0px tall —
|
|
26
|
+
`audit-app-shells.mjs`'s own threshold is <4px, so a shallower-but-still-broken
|
|
27
|
+
collapse won't trip it), `[network-4xx]` (typoed stylesheet href, missing
|
|
28
|
+
contents.html, stale `import.meta.url`). Row 7's specific case (gh#981, in
|
|
29
|
+
`site/site.js`'s router, not swept by this script at all): the 150px isn't
|
|
30
|
+
`admin-page-body`'s own height — it's a *replaced child* (an `<iframe>`)
|
|
31
|
+
whose `height:100%` can't resolve, so it falls back to the browser's
|
|
32
|
+
intrinsic default (300×150). The observable is a body rendering ~150px tall
|
|
33
|
+
with no console error, regardless of real content height.
|
|
27
34
|
|
|
28
35
|
## Composite → internal primitives (undiscoverable from the markup)
|
|
29
36
|
|
|
@@ -571,6 +571,8 @@ function renderReport(results, base) {
|
|
|
571
571
|
lines.push('4. **Synonym-attr / synonym-slot drift** — markers documented in `.claude/docs/conventions/attribute-api-migration.md`.');
|
|
572
572
|
lines.push('5. **Alert flex-row** — `alert-ui` with multiple bare `<text-ui>` children (need `<col-ui slot="content">` wrap).');
|
|
573
573
|
lines.push('6. **Console** — every `console.error` and `console.warn` during page load + 800ms settling.');
|
|
574
|
+
lines.push('7. **Missing component CSS** — a rendered `*-ui` element with no resolved `--{prefix}-*` custom property, meaning its per-component stylesheet never loaded (controls render unstyled).');
|
|
575
|
+
lines.push('8. **Unstyled popover** — an OPEN `[popover]` with transparent background AND zero padding (transparent-host components like `feed-ui` allowlisted).');
|
|
574
576
|
lines.push('');
|
|
575
577
|
|
|
576
578
|
return lines.join('\n');
|
|
@@ -35,8 +35,15 @@ directives are findings.
|
|
|
35
35
|
3. Zero P1 cosmetic findings (overflow entries auto-count as P1) — checked
|
|
36
36
|
mechanically.
|
|
37
37
|
4. Zero RENDER_FAILURE prompts; cycle `status: COMPLETE` — checked
|
|
38
|
-
mechanically
|
|
39
|
-
5. Human QA
|
|
38
|
+
mechanically.
|
|
39
|
+
5. **Human QA is per-sweep, not per-cycle** (spec-factory-dx-ws6-measurement.md
|
|
40
|
+
REQ-11, gh#1137 — retired from this exit gate 2026-08-12; the per-cycle
|
|
41
|
+
`humanQA` ledger field never held: null in 12 of 13 rows, and the one
|
|
42
|
+
populated row still said "gate pending"). The 5-sampled-prompts,
|
|
43
|
+
`passCount ≥ 4` bar is unchanged, but it now feeds `qa/dx/`'s R1 sweep
|
|
44
|
+
record (D5's induced-failure sample + D8's gallery term) instead of
|
|
45
|
+
blocking a single cycle's own exit condition. `gen-review-status.mjs
|
|
46
|
+
--check-exit` no longer reads `humanQA` off the ledger row.
|
|
40
47
|
|
|
41
48
|
## The Gen-Review Scorecard — the per-prompt output contract
|
|
42
49
|
|
|
@@ -182,7 +189,7 @@ map + FAILS/WORKS transpilation pairs:
|
|
|
182
189
|
| New primitive / native tag in canvas | `TAG_TO_COMPONENT` in the decompose script (`Native*` prefix for natives); coverage-audit must go green |
|
|
183
190
|
| New root-cause code | rubric-score.md table + `scores.schema.json` enum + `schemaVersion` bump |
|
|
184
191
|
| New safe attribute | `ATTR_ALLOWLIST` — static enums only, never freeform-text attrs |
|
|
185
|
-
| Threshold recalibration | rubric-score.md §Thresholds (pass-flip sweep) + the exit gate above |
|
|
192
|
+
| Threshold recalibration | rubric-score.md §Thresholds (pass-flip sweep) + the exit gate above + `EXCELLENCE_THRESHOLD` at `scripts/gen-review-status.mjs:31` — the only mechanical enforcer; skip it and `--check-exit` silently keeps the old floor |
|
|
186
193
|
|
|
187
194
|
## Posture
|
|
188
195
|
|
|
@@ -161,12 +161,16 @@ never get fix plans. Reads ONLY the decomposed file.
|
|
|
161
161
|
skill does not perform the edits.
|
|
162
162
|
3. **Regenerate**: `npm run gallery:generate`; confirm 0 console
|
|
163
163
|
errors/warnings in the canvas output.
|
|
164
|
-
4. **Human QA
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
4. **Human QA sample — per-sweep, not per-cycle** (retired from this
|
|
165
|
+
cycle's own COMPLETE gate 2026-08-12, spec-factory-dx-ws6-measurement.md
|
|
166
|
+
REQ-11, gh#1137). Operator reviews 5 random PASSING prompts against:
|
|
167
|
+
(a) serves the user's task? (b) right primary primitive? (c) would ship
|
|
168
|
+
unchanged? Record the result in the next `qa/dx/` sweep record's
|
|
169
|
+
`humanQA.{sampledPrompts,pass,fail}` (`node scripts/qa/dx-status.mjs`),
|
|
170
|
+
not on this cycle's ledger row. `failCount ≥ 2` still means the
|
|
171
|
+
thresholds are miscalibrated — recalibrate rubric-score.md §Thresholds
|
|
172
|
+
against the human judgments — it just no longer blocks THIS cycle's own
|
|
173
|
+
`status: COMPLETE`.
|
|
170
174
|
5. **Schema gate** (must exit 0 before touching the ledger):
|
|
171
175
|
|
|
172
176
|
```bash
|
|
@@ -175,8 +179,11 @@ never get fix plans. Reads ONLY the decomposed file.
|
|
|
175
179
|
|
|
176
180
|
6. **Update ledger** (`review/cycle-ledger.json`): `cycleNumber`,
|
|
177
181
|
`completedAt`, `engine`, `status`, `aggregate`
|
|
178
|
-
(passingCount/failingCount/renderFailureCount/meanScore/Δ)
|
|
179
|
-
|
|
182
|
+
(passingCount/failingCount/renderFailureCount/meanScore/Δ). `humanQA` is
|
|
183
|
+
RETIRED from this per-cycle row (step 4) — do not populate it here; a
|
|
184
|
+
stray value is harmless (ignored) but the field's home is now the
|
|
185
|
+
per-sweep `qa/dx/sweeps/*.json` record. Remove the `cycle-{N}.lock`
|
|
186
|
+
sentinel.
|
|
180
187
|
7. **Exit condition**:
|
|
181
188
|
|
|
182
189
|
```bash
|
|
@@ -22,8 +22,11 @@ Max score 105 (D1–D5 at 20 each = 100; D6 mechanical = 0 or +5).
|
|
|
22
22
|
**Recalibrating** (when human QA contradicts mechanical scores across 2+
|
|
23
23
|
cycles): sweep candidate threshold values and read the pass-flips at each step
|
|
24
24
|
against the human verdicts — never pick a round number by feel. A threshold
|
|
25
|
-
change must also update the exit gate in SKILL.md
|
|
26
|
-
|
|
25
|
+
change must also update the exit gate in SKILL.md AND the
|
|
26
|
+
`EXCELLENCE_THRESHOLD` constant at `scripts/gen-review-status.mjs:31` — that
|
|
27
|
+
constant is the only mechanical enforcer (`--check-exit` reads nothing else;
|
|
28
|
+
the schema carries no threshold), so missing it leaves the old floor silently
|
|
29
|
+
enforced. New cause codes / dimensions bump `schemaVersion` in
|
|
27
30
|
[scores.schema.json](scores.schema.json).
|
|
28
31
|
|
|
29
32
|
---
|
|
@@ -17,9 +17,14 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Exit-condition (from scores.schema.json + SKILL.md §ExitCondition):
|
|
19
19
|
* - Every prompt PASSING (p1Count = 0 AND rubricScore.score ≥ 92)
|
|
20
|
-
* - humanQA.passCount ≥ 4 (of 5 sampled)
|
|
21
20
|
* - No RENDER_FAILURE prompts
|
|
22
21
|
* - Cycle status = COMPLETE (not OPEN or INTERRUPTED)
|
|
22
|
+
*
|
|
23
|
+
* humanQA is RETIRED from this per-cycle exit condition (spec-factory-dx-
|
|
24
|
+
* ws6-measurement.md REQ-11, gh#1137, 2026-08-12): the per-cycle ledger
|
|
25
|
+
* field never held (null in 12/13 rows), so it moved to a per-sweep
|
|
26
|
+
* obligation — `qa/dx/sweeps/*.json`'s `humanQA` field (dx-status.mjs)
|
|
27
|
+
* reads a 5-prompt sample per sweep instead of per cycle.
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
30
|
import { readFileSync, existsSync } from 'node:fs';
|
|
@@ -85,11 +90,8 @@ function checkExitCondition() {
|
|
|
85
90
|
reasons.push(`Cycle ${latest.cycleNumber} status is "${latest.status}" (need COMPLETE)`);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} else if (latest.humanQA.passCount < 4) {
|
|
91
|
-
reasons.push(`Human QA pass count ${latest.humanQA.passCount}/5 (need ≥ 4)`);
|
|
92
|
-
}
|
|
93
|
+
// humanQA is RETIRED from this per-cycle check (REQ-11, gh#1137) — it's a
|
|
94
|
+
// per-sweep obligation now (qa/dx/sweeps/*.json), not a per-cycle gate.
|
|
93
95
|
|
|
94
96
|
const agg = latest.aggregate;
|
|
95
97
|
if (agg?.failingCount > 0) {
|
|
@@ -25,7 +25,7 @@ Consumers depend on: the `StreamChunk` union, `ChatResult` (`text` / `usage` / `
|
|
|
25
25
|
|
|
26
26
|
Three invariants override any cleanup instinct:
|
|
27
27
|
|
|
28
|
-
1. **
|
|
28
|
+
1. **Never collapse `stopReason` truncation values — and never invent a NEW normalization.** The one sanctioned mapping is OpenAI's own `finish_reason === 'stop'` → `end` (`openai.ts` `parseResponse`; adapter-contract.md §stopReason documents it as correct); everything else propagates raw. Providers emit `end` / `stop` / `max_tokens` / `length` / `MAX_TOKENS` / `tool_use`; the downstream truncation detector reads the raw value, so collapsing `max_tokens`/`length`/`MAX_TOKENS` to `end` hides truncation — a defect, not a cleanup.
|
|
29
29
|
2. **`buildRequest()` is the single source of upstream shape** for direct AND passthrough-proxy mode; the dispatcher swaps only the URL. Never fork it per proxy flavor.
|
|
30
30
|
3. **No real API key reaches the browser on a production host.** The same-origin passthrough proxy injects the key server-side; the sentinel-key + one-shot-warning path in `createAdapter()` must survive any refactor.
|
|
31
31
|
|
|
@@ -93,9 +93,9 @@ assumed pass, or "published" without the actual `npm view` output.
|
|
|
93
93
|
|
|
94
94
|
## Mechanization (bundled `scripts/`, stdlib Node, all support `--dry`)
|
|
95
95
|
|
|
96
|
-
`release-pack.mjs` walks the cycle in two phases per invariant 3 — `--mode cut` / `--mode from-scratch` promote `[Unreleased]` (BOTH modes now — a peer's hand-authored section must be promoted whichever mode cut it, and a loud guard blocks the bump if any roster package still carries non-empty `[Unreleased]` content after promotion), run Step 4f (pre-tag coverage `--fix` with the authoritative F-N1 matcher), and stop at the release commit (PR → merge); `--mode handoff` tags/publishes/deploys from post-merge main. **`--go` is the standard invocation for an operator-initiated release** — the single authorization covers the cycle; evidence blocks print as a log; granular `--yes`/`--push`/`--publish` remain for cautious manual runs (`--push` still refuses to auto-fire past any F-N1 finding). `--mode batch` is not implemented — it hard-rejects with a pointer to [`recovery-paths.md`](references/recovery-paths.md) §Scenario 2 rather than silently misbehaving; a real batch tags each version at its own release-merge SHA, which this single-version orchestrator has no model for. Pre-flight (Step 3) runs the full
|
|
96
|
+
`release-pack.mjs` walks the cycle in two phases per invariant 3 — `--mode cut` / `--mode from-scratch` promote `[Unreleased]` (BOTH modes now — a peer's hand-authored section must be promoted whichever mode cut it, and a loud guard blocks the bump if any roster package still carries non-empty `[Unreleased]` content after promotion), run Step 4f (pre-tag coverage `--fix` with the authoritative F-N1 matcher), and stop at the release commit (PR → merge); `--mode handoff` tags/publishes/deploys from post-merge main. **`--go` is the standard invocation for an operator-initiated release** — the single authorization covers the cycle; evidence blocks print as a log; granular `--yes`/`--push`/`--publish` remain for cautious manual runs (`--push` still refuses to auto-fire past any F-N1 finding). `--mode batch` is not implemented — it hard-rejects with a pointer to [`recovery-paths.md`](references/recovery-paths.md) §Scenario 2 rather than silently misbehaving; a real batch tags each version at its own release-merge SHA, which this single-version orchestrator has no model for. Pre-flight (Step 3) runs the full 28-gate roster from the single shared list in `scripts/gate-roster.mjs` — the same list [`cut-procedure.md`](references/cut-procedure.md) §3.1 transcribes, so a subset run requires editing that one file, not two.
|
|
97
97
|
|
|
98
|
-
The pieces are callable alone: `bump.mjs` (version fields + the factory `.mcp.json` a2ui-mcp pin, invariant 8), `promote-unreleased.mjs` / `insert-stub.mjs` (CHANGELOG blocks), `tag-lockstep.mjs` (
|
|
98
|
+
The pieces are callable alone: `bump.mjs` (version fields + the factory `.mcp.json` a2ui-mcp pin, invariant 8), `promote-unreleased.mjs` / `insert-stub.mjs` (CHANGELOG blocks), `tag-lockstep.mjs` (umbrella + one tag per roster package — 14 today, `--delete` to move), `dispatch-publish.mjs` (publish workflows; `--verify-triggered` re-dispatches missing AND dead — cancelled/failed/timed-out — runs, each gated behind an npm-registry check so an already-published version is never re-dispatched (gh#763); `--after` enforces batch ordering — dispatch output states what WAS done, never a to-do phrased as if it hadn't fired), and `pr-bridge.mjs` (the two-phase bridge: pushes the release branch, opens the PR, waits out CI, and merges ONLY on all-non-fail-soft-checks-green + zero unresolved threads + no changes-requested; any other state stops with evidence — §5.7 mechanized, `--dry` to preview the decision). All fail fast outside a monorepo root (`assert-monorepo-root.mjs`); fork-configurable via `--host` / `--scope`. All 6 CLI scripts (plus `scripts/release/check-release.mjs`) have a `selftest` mode wired into CI's Plugin bin selftests step; the 3 helper modules (`assert-monorepo-root`, `gate-roster`, `package-paths`) are exercised through the CLIs' selftests, not their own.
|
|
99
99
|
|
|
100
100
|
Site deploy (Step 10 of the handoff) dispatches `deploy-site.yml` (its own pre-flight verify → snapshot → hardened rsync → post-deploy verify → auto-rollback), never a raw local `rsync` — H1 (forge-campaign gh#268 audit): the script had drifted into bypassing that pipeline even though this doc already taught the workflow-dispatch path. The dispatch rides the cycle's single authorization (`--go`, or `--publish` on a manual run); the workflow's own GitHub environment gate is the human stop for the deploy itself.
|
|
101
101
|
|
|
@@ -113,6 +113,10 @@ npm run check:token-semantics-sync # 21 adia-tokens generated refs v
|
|
|
113
113
|
npm run check:demo-routes # 22 demo surfaces routed + patterns indexed
|
|
114
114
|
npm run check:brand-assets # 23 brand mark token-driven, not baked raster
|
|
115
115
|
node scripts/release/check-cut-hygiene.mjs --version <mode-dependent> # 24 README CDN-pin + version currency — CUT modes pre-flight at the PREVIOUS version (the README claim is exact-match and only moves at the Step-4 bump; Step 4g re-proves at the cut version), HANDOFF pre-flights at the CUT version (post-merge, the claim already moved)
|
|
116
|
+
python3 packages/plugins/adia-ui-factory/scripts/adia-scaffold selftest # 25 scaffold specifiers resolve in packed @adia-ai/web-components + @adia-ai/web-modules tarballs (gh#1132)
|
|
117
|
+
node scripts/release/check-dx-sweep-freshness.mjs # 26 DX sweep fresh + not regression-RED (gh#1137 REQ-05 — FLAGGED, needs adia-release confirmation; see the script's own header)
|
|
118
|
+
npm run eval:diff -- --engine free-form # 27 fresh free-form eval run (evals/health input, gh#1135)
|
|
119
|
+
node scripts/release/write-eval-health.mjs --version <cut> # 28 evals/health/<version>.json committed, AC-01/AC-02 run for real (gh#1135, WS-4 SPEC REQ-06)
|
|
116
120
|
```
|
|
117
121
|
|
|
118
122
|
Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source, re-run the narrowest gate, then re-run the full sequence. The canonical miss: a cut that ran 7 of the gates shipped a stale-embeddings defect that surfaced a day later and cost a tag-move recovery.
|
|
@@ -153,6 +153,8 @@ The two `audit:*` probes are soft-fail pre-cut (exit-1 only on criticals); findi
|
|
|
153
153
|
- **What:** the vitest suite (~1000+ tests).
|
|
154
154
|
- **Stale-test failure:** the test asserts a behavior a peer deliberately changed (CHANGELOG-documented) without updating the assertion. Tell it apart by reading assertion vs CHANGELOG vs source: if the code matches the CHANGELOG's described behavior, the test is stale — update the assertion (see [`recovery-paths.md`](recovery-paths.md) §Scenario 5). Real regression → fix the regression.
|
|
155
155
|
- **Parallel-contention flake:** heavy web-modules composite suites can fail under full parallelism on a loaded machine (`signals: drain loop exceeded 100 iterations`). Load-dependent, not a code defect. Sequential is the source of truth, and **the roster gate now runs it directly** (`npm run test:unit:serial` = `vitest run --no-file-parallelism`, gate 4 since 2026-07-19) — a parallel `test:unit` flake outside the roster still isn't a blocker, and don't raise the drain guard.
|
|
156
|
+
- **Exit 143 (SIGTERM):** the vitest process was KILLED (resource pressure / a stray terminator), not a test failure — re-run the gate directly (`npx vitest run --no-file-parallelism`) before diagnosing anything; a clean re-run means transient, proceed (v0.8.34 handoff hit this).
|
|
157
|
+
- **First-time-at-cut failures are REAL (v0.8.34):** this serial suite runs tests PR CI never does — `exit-gate.corpus` (dialect-catalog conformance over real harvested chunks) surfaces yaml-schema drift (e.g. an array prop missing `items.type`, the command/combobox `DynamicStringList` mis-map) only HERE, potentially weeks after the yaml edit merged green. Treat such a failure as a genuine latent defect to root-cause at the yaml SoT, never as release-blocking noise. (Structural fix — promoting the corpus batch into PR CI — tracked as a follow-up.)
|
|
156
158
|
|
|
157
159
|
### `npm run eval:diff -- --engine zettel`
|
|
158
160
|
|
|
@@ -202,7 +204,7 @@ npm run check:demo-shells
|
|
|
202
204
|
|
|
203
205
|
Add `verify:corpus` + `check:embeddings-fresh` if chunks were touched; `check:lightningcss-build` if CSS was touched; F-N1 if unpushed release tags exist.
|
|
204
206
|
|
|
205
|
-
**Full pre-cut sweep** — the
|
|
207
|
+
**Full pre-cut sweep** — the 28-gate roster in [`cut-procedure.md`](cut-procedure.md) §Step 3, sourced from `` `${CLAUDE_PLUGIN_ROOT}/skills/adia-release/scripts/gate-roster.mjs` `` (the ONE list; `release-pack.mjs` imports and runs it in full — a subset run is impossible without editing that file). ~90s wall time.
|
|
206
208
|
|
|
207
209
|
**Omnibus** — `npm run check` invokes everything. Heavy; use when re-baselining a stale checkout.
|
|
208
210
|
|
|
@@ -39,11 +39,11 @@ Substrate fixes (release scripts / lockfile / CHANGELOG), landed once as a norma
|
|
|
39
39
|
4. **Tag at the published commit** — `git tag <pkg>-vX.Y.Z` per package; versions differ; no umbrella. Dry-run the trip-wire over the tags (`node scripts/release/check-release.mjs <tags>`) before pushing. If the shared working tree is on a peer's branch, push tags from a throwaway worktree on `origin/main`.
|
|
40
40
|
5. **Push the tags** → the per-package workflows run `npm ci → verify:plugins → npm publish --access public`.
|
|
41
41
|
6. **Verify against npm, not the workflow:** `npm view @adia-ai/<pkg> version` returns the new independent version for each package. Watch runs (`gh run watch <id> --exit-status`), but the registry is the source of truth.
|
|
42
|
-
7. **Marketplace cut-over**
|
|
42
|
+
7. **Marketplace cut-over** — per ADR-0045 the distribution channel is the npm-sourced manifest package `@adia-ai/adia-plugins` (no public git marketplace repo — that shape was considered and rejected). Bump/publish the manifest package so its `marketplace.json` entries reference the new plugin versions; consumers add the marketplace from npm, never from a git repo.
|
|
43
43
|
|
|
44
44
|
## §Verify target
|
|
45
45
|
|
|
46
|
-
The published package(s) on the **npm registry** at their independent versions, plus
|
|
46
|
+
The published package(s) on the **npm registry** at their independent versions, plus the npm-published `@adia-ai/adia-plugins` manifest referencing the new versions (ADR-0045). NOT "all 11 + the demo site" — that's class A.
|
|
47
47
|
|
|
48
48
|
## §Gotchas (from the first independent cut, factory v0.2.2 + forge v0.1.2)
|
|
49
49
|
|
|
@@ -20,17 +20,21 @@
|
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import process from 'node:process';
|
|
23
|
-
import {
|
|
23
|
+
import { PACKAGE_ROSTER } from './package-paths.mjs';
|
|
24
24
|
|
|
25
25
|
// Single-sourced roster (H3, package-paths.mjs). Each plugin also carries a
|
|
26
26
|
// .claude-plugin/plugin.json whose version MUST move with package.json —
|
|
27
27
|
// plugin.json is the /plugin-update cache key (verify:plugins gates the sync).
|
|
28
|
-
|
|
28
|
+
// Filtered to `lockstep !== false`: a class-B package (gh#1133's
|
|
29
|
+
// adia-plugins) bumps on its own independent version line, never via this
|
|
30
|
+
// lockstep --from/--to sweep. See package-paths.mjs's `lockstep` field note.
|
|
31
|
+
const LOCKSTEP_ROSTER = PACKAGE_ROSTER.filter((p) => p.lockstep !== false);
|
|
32
|
+
const PACKAGES = LOCKSTEP_ROSTER.map((p) => p.dir);
|
|
29
33
|
|
|
30
34
|
// Sibling manifests bumped in the same pass as package.json (path relative to
|
|
31
35
|
// the package dir) — derived from the roster's plugin flag.
|
|
32
36
|
const SIBLING_MANIFESTS = Object.fromEntries(
|
|
33
|
-
|
|
37
|
+
LOCKSTEP_ROSTER.filter((p) => p.plugin).map((p) => [p.dir, '.claude-plugin/plugin.json']),
|
|
34
38
|
);
|
|
35
39
|
|
|
36
40
|
// Cross-package EXACT pins bumped in the same pass — a different shape from a
|
|
@@ -20,14 +20,17 @@
|
|
|
20
20
|
import { execFileSync } from 'node:child_process';
|
|
21
21
|
import process from 'node:process';
|
|
22
22
|
import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
|
|
23
|
-
import {
|
|
23
|
+
import { PACKAGE_ROSTER } from './package-paths.mjs';
|
|
24
24
|
|
|
25
25
|
// Instance data — fork-configurable. The npm scope the lockstep roster publishes under.
|
|
26
26
|
// Default preserves @adia-ai behavior; override via --scope or $ADIA_NPM_SCOPE.
|
|
27
27
|
const DEFAULT_SCOPE = '@adia-ai';
|
|
28
28
|
|
|
29
29
|
// Single-sourced roster (H3, package-paths.mjs); order = dispatch order.
|
|
30
|
-
|
|
30
|
+
// Filtered to `lockstep !== false`: a class-B package (gh#1133's
|
|
31
|
+
// adia-plugins) publishes via its own manual cut (independent-package-
|
|
32
|
+
// release.md), never this lockstep dispatch loop.
|
|
33
|
+
const PACKAGES = PACKAGE_ROSTER.filter((p) => p.lockstep !== false).map((p) => p.name);
|
|
31
34
|
|
|
32
35
|
function parseArgs(argv) {
|
|
33
36
|
const args = {
|
|
@@ -64,6 +64,35 @@ export const GATE_ROSTER = [
|
|
|
64
64
|
// v0.7.13-retro hygiene checker — README CDN pins / "Current version"
|
|
65
65
|
// claims vs the cut version; branch/worktree limbo prints as warns (H2).
|
|
66
66
|
{ n: 24, cmd: 'node scripts/release/check-cut-hygiene.mjs', what: 'README CDN-pin + version currency', versionArg: true },
|
|
67
|
+
// gh#1132 (factory-dx-ws1-boot SPEC, REQ-04): npm-packs BOTH
|
|
68
|
+
// @adia-ai/web-components and @adia-ai/web-modules and resolves every
|
|
69
|
+
// specifier the scaffold templates emit against the extracted tarballs'
|
|
70
|
+
// real exports maps — the same blast radius gh#296's
|
|
71
|
+
// exports-wildcard-resolution.mjs gate covers, applied to the factory
|
|
72
|
+
// plugin's own templates instead of a consumer's hand-written import.
|
|
73
|
+
{ n: 25, cmd: 'python3 packages/plugins/adia-ui-factory/scripts/adia-scaffold selftest', what: 'scaffold specifiers resolve in packed tarballs' },
|
|
74
|
+
// gh#1137 (factory-dx-ws6-measurement SPEC, REQ-05) — FLAGGED FOR
|
|
75
|
+
// CONFIRMATION: this entry was authored by the WS-6 build without a live
|
|
76
|
+
// adia-release sign-off (no operator was available mid-build to confirm
|
|
77
|
+
// the exact freshness window / regression semantics). Asserts a
|
|
78
|
+
// qa/dx/sweeps/ record exists dated within this cut's window AND its
|
|
79
|
+
// verdict is not a regression-RED (a plain floors<=1 baseline RED does
|
|
80
|
+
// NOT block a cut — only a floor that DROPPED since the prior sweep
|
|
81
|
+
// does). CONFIRMED by adia-release on the first live cut (v0.8.35,
|
|
82
|
+
// 2026-08-13): semantics hold, with one correction — the previous-cut
|
|
83
|
+
// comparison excludes the in-flight target version (self-reference fired
|
|
84
|
+
// on the handoff re-run, since the promoted heading was already on main).
|
|
85
|
+
{ n: 26, cmd: 'node scripts/release/check-dx-sweep-freshness.mjs', targetVersionArg: true, what: 'DX sweep fresh + not regression-RED' },
|
|
86
|
+
// gh#1135 (WS-4 SPEC REQ-06): a fresh free-form eval run feeds the health
|
|
87
|
+
// record gate 28 writes — without it, "latest" under evals/mcp/runs/ could
|
|
88
|
+
// be a stale leftover from an earlier ad-hoc run, not this cut's own state.
|
|
89
|
+
{ n: 27, cmd: 'npm run eval:diff -- --engine free-form', what: 'fresh free-form eval run (evals/health input)' },
|
|
90
|
+
// REQ-06: commits evals/health/<version>.json — the version this record
|
|
91
|
+
// names is the CUT's target version from the start, never the
|
|
92
|
+
// pre/post-bump `hygieneVersion` gate 24 needs (a claim-validation
|
|
93
|
+
// concern this is not) — hence `targetVersionArg` rather than
|
|
94
|
+
// `versionArg`, a distinct flag release-pack.mjs's step3PreFlight reads.
|
|
95
|
+
{ n: 28, cmd: 'node scripts/release/write-eval-health.mjs', what: 'evals/health/<version>.json committed, AC-01/AC-02 run for real', targetVersionArg: true },
|
|
67
96
|
];
|
|
68
97
|
|
|
69
98
|
// -- CLI ------------------------------------------------------------------
|
|
@@ -42,24 +42,52 @@ import path from 'node:path';
|
|
|
42
42
|
// Order is the canonical publish/tag order (release-pack's historical order);
|
|
43
43
|
// consumers needing a different ordering (check-lockstep sorts by path for
|
|
44
44
|
// stable output) re-sort locally.
|
|
45
|
+
//
|
|
46
|
+
// `lockstep` (added gh#1133, onboarding the first class-B member since the
|
|
47
|
+
// 2026-07-15 policy emptied that class — independent-package-release.md):
|
|
48
|
+
// `true` (or omitted — every pre-2026-08 entry defaults true below) means the
|
|
49
|
+
// package rides the coordinated cut — version-match/caret gate
|
|
50
|
+
// (check-lockstep.mjs), the umbrella tag, the --pending-version sweep, bump.mjs's
|
|
51
|
+
// --from/--to pass, the dispatch-publish loop, and generate-release-notes.mjs's
|
|
52
|
+
// notes. `false` means class B: its own version line, its own manual tag +
|
|
53
|
+
// publish (independent-package-release.md), NEVER the umbrella tag or a
|
|
54
|
+
// lockstep bump/dispatch/notes pass. The ONE exception: the F-N1 release
|
|
55
|
+
// trip-wire (check-release.mjs's per-tag `checkTag` lookup) reads the FULL
|
|
56
|
+
// roster unfiltered — every pushed tag, lockstep or not, still needs its
|
|
57
|
+
// package.json/CHANGELOG validated, so it must recognize a class-B tag too.
|
|
58
|
+
// Every lockstep-scoped consumer must filter `p.lockstep !== false` itself
|
|
59
|
+
// (this module intentionally does NOT pre-filter PACKAGE_NAMES/PACKAGE_DIRS —
|
|
60
|
+
// they stay full-roster projections so a consumer's own filter is visible at
|
|
61
|
+
// its point of use, not hidden upstream).
|
|
45
62
|
export const PACKAGE_ROSTER = [
|
|
46
|
-
{ name: 'web-components', dir: 'packages/web-components' },
|
|
47
|
-
{ name: 'web-modules', dir: 'packages/web-modules' },
|
|
48
|
-
{ name: 'llm', dir: 'packages/llm' },
|
|
49
|
-
{ name: 'agent', dir: 'packages/agent' },
|
|
50
|
-
{ name: 'persona', dir: 'packages/persona' },
|
|
51
|
-
{ name: 'a2ui-runtime', dir: 'packages/a2ui/runtime' },
|
|
52
|
-
{ name: 'a2ui-compose', dir: 'packages/a2ui/compose' },
|
|
53
|
-
{ name: 'a2ui-corpus', dir: 'packages/a2ui/corpus' },
|
|
54
|
-
{ name: 'a2ui-mcp', dir: 'packages/a2ui/mcp' },
|
|
55
|
-
{ name: 'a2ui-retrieval', dir: 'packages/a2ui/retrieval' },
|
|
56
|
-
{ name: 'a2ui-validator', dir: 'packages/a2ui/validator' },
|
|
63
|
+
{ name: 'web-components', dir: 'packages/web-components', lockstep: true },
|
|
64
|
+
{ name: 'web-modules', dir: 'packages/web-modules', lockstep: true },
|
|
65
|
+
{ name: 'llm', dir: 'packages/llm', lockstep: true },
|
|
66
|
+
{ name: 'agent', dir: 'packages/agent', lockstep: true },
|
|
67
|
+
{ name: 'persona', dir: 'packages/persona', lockstep: true },
|
|
68
|
+
{ name: 'a2ui-runtime', dir: 'packages/a2ui/runtime', lockstep: true },
|
|
69
|
+
{ name: 'a2ui-compose', dir: 'packages/a2ui/compose', lockstep: true },
|
|
70
|
+
{ name: 'a2ui-corpus', dir: 'packages/a2ui/corpus', lockstep: true },
|
|
71
|
+
{ name: 'a2ui-mcp', dir: 'packages/a2ui/mcp', lockstep: true },
|
|
72
|
+
{ name: 'a2ui-retrieval', dir: 'packages/a2ui/retrieval', lockstep: true },
|
|
73
|
+
{ name: 'a2ui-validator', dir: 'packages/a2ui/validator', lockstep: true },
|
|
57
74
|
// Plugins joined the lockstep 2026-07-15 (check-lockstep.mjs POLICY note);
|
|
58
75
|
// each carries a .claude-plugin/plugin.json that bumps with package.json.
|
|
59
|
-
{ name: 'adia-ui-factory', dir: 'packages/plugins/adia-ui-factory', plugin: true },
|
|
60
|
-
{ name: 'adia-ui-forge', dir: 'packages/plugins/adia-ui-forge',
|
|
76
|
+
{ name: 'adia-ui-factory', dir: 'packages/plugins/adia-ui-factory', lockstep: true, plugin: true },
|
|
77
|
+
{ name: 'adia-ui-forge', dir: 'packages/plugins/adia-ui-forge', lockstep: true, plugin: true },
|
|
78
|
+
// adia-plugins is class B (gh#1133, ADR-0045/spec-factory-dx-ws2): the npm-
|
|
79
|
+
// sourced marketplace manifest package. No lockstep content (a roster of
|
|
80
|
+
// npm sources, no code) and REQ-03's unpinned sources mean it republishes
|
|
81
|
+
// only on its own roster change — cuts independently, own version line,
|
|
82
|
+
// never the umbrella tag. Not a Claude Code plugin itself (no `plugin.json`
|
|
83
|
+
// sidecar), so no `plugin: true`.
|
|
84
|
+
{ name: 'adia-plugins', dir: 'packages/plugins/adia-plugins', lockstep: false },
|
|
61
85
|
];
|
|
62
86
|
|
|
87
|
+
// Full-roster projections — every registered package, lockstep or not. A
|
|
88
|
+
// lockstep-scoped consumer (bump, tag-lockstep, dispatch-publish, the
|
|
89
|
+
// version-match gate, the notes generator, the docs gate) must filter
|
|
90
|
+
// `.lockstep !== false` itself; see the `lockstep` field note above.
|
|
63
91
|
export const PACKAGE_NAMES = PACKAGE_ROSTER.map((p) => p.name);
|
|
64
92
|
export const PACKAGE_DIRS = PACKAGE_ROSTER.map((p) => p.dir);
|
|
65
93
|
|
|
@@ -81,8 +81,29 @@ function help() {
|
|
|
81
81
|
Flags: --poll-interval <s=30> --timeout <s=2400> --dry`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Transient-network failure signatures (v0.8.34 cut: three bridge runs died
|
|
85
|
+
// on momentary TLS-interception blips — `x509: certificate signed by unknown
|
|
86
|
+
// authority` — that a 20s retry sailed past). Deliberately narrow: anything
|
|
87
|
+
// NOT matching rethrows immediately so real failures stay loud.
|
|
88
|
+
const TRANSIENT_NET = /tls:|x509:|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|Could not resolve host|connection reset|TLS handshake/i;
|
|
89
|
+
const NET_RETRIES = 3;
|
|
90
|
+
const NET_BACKOFF_MS = 20_000;
|
|
91
|
+
|
|
92
|
+
function withNetRetry(fn, label) {
|
|
93
|
+
for (let attempt = 1; ; attempt++) {
|
|
94
|
+
try { return fn(); }
|
|
95
|
+
catch (e) {
|
|
96
|
+
const msg = `${e.message || ''}\n${e.stderr || ''}\n${e.stdout || ''}`;
|
|
97
|
+
if (attempt >= NET_RETRIES || !TRANSIENT_NET.test(msg)) throw e;
|
|
98
|
+
console.error(`[pr-bridge] transient network failure on ${label} (attempt ${attempt}/${NET_RETRIES}) — retrying in ${NET_BACKOFF_MS / 1000}s`);
|
|
99
|
+
execFileSync('sleep', [String(NET_BACKOFF_MS / 1000)]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
function sh(cmd) {
|
|
85
|
-
return
|
|
105
|
+
return withNetRetry(
|
|
106
|
+
() => execSync(cmd, { cwd: REPO, encoding: 'utf8' }).trim(), cmd.slice(0, 40));
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
// No-shell variant for every call that carries an operator-supplied value
|
|
@@ -90,7 +111,9 @@ function sh(cmd) {
|
|
|
90
111
|
// `$(…)`/backticks live inside double quotes and broke on metachar branch
|
|
91
112
|
// names. execFileSync + arg array = no shell, no expansion, no quoting bugs.
|
|
92
113
|
function shFile(cmd, argv) {
|
|
93
|
-
return
|
|
114
|
+
return withNetRetry(
|
|
115
|
+
() => execFileSync(cmd, argv, { cwd: REPO, encoding: 'utf8' }).trim(),
|
|
116
|
+
`${cmd} ${argv[0] ?? ''}`);
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
// ── The decision predicate — pure, selftest-locked ────────────────
|
|
@@ -208,6 +231,17 @@ async function main() {
|
|
|
208
231
|
if (existing.length > 0) {
|
|
209
232
|
prNumber = existing[0].number;
|
|
210
233
|
console.log(`[pr-bridge] reusing open PR #${prNumber} for ${args.branch} ("${existing[0].title}")`);
|
|
234
|
+
// Push any LOCAL-AHEAD commits before judging CI (v0.8.34 cut: a fix
|
|
235
|
+
// committed locally but never pushed left the bridge judging the STALE
|
|
236
|
+
// failed run — permanently, since the remote never got the fix). Only
|
|
237
|
+
// meaningful when the branch exists in this checkout; a bridge run from
|
|
238
|
+
// a checkout without the branch (handoff-only) skips silently.
|
|
239
|
+
if (!args.dry) {
|
|
240
|
+
try {
|
|
241
|
+
shFile('git', ['rev-parse', '--verify', `refs/heads/${args.branch}`]);
|
|
242
|
+
shFile('git', ['push', 'origin', args.branch]);
|
|
243
|
+
} catch { /* no local branch — nothing to push */ }
|
|
244
|
+
}
|
|
211
245
|
} else if (args.dry) {
|
|
212
246
|
console.error(`[pr-bridge] --dry: no open PR for ${args.branch}; nothing to report`);
|
|
213
247
|
process.exit(1);
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// in N package CHANGELOGs. The special target `root` is the repo-root
|
|
4
4
|
// CHANGELOG.md — same swap, no package resolution.
|
|
5
5
|
//
|
|
6
|
-
// Idempotent
|
|
7
|
-
//
|
|
6
|
+
// Idempotent — including across PARTIAL runs: a target already promoted to
|
|
7
|
+
// exactly [vX.Y.Z] with no [Unreleased] remaining counts as done (skip).
|
|
8
|
+
// Stops only if a target carries BOTH headings (its [Unreleased] content
|
|
9
|
+
// would silently miss the release) — merge those by hand.
|
|
8
10
|
//
|
|
9
11
|
// Usage:
|
|
10
12
|
// node promote-unreleased.mjs --version 0.6.22 --date 2026-05-22 \
|
|
@@ -17,7 +19,7 @@
|
|
|
17
19
|
//
|
|
18
20
|
// Exits:
|
|
19
21
|
// 0 — all targets promoted (or --dry succeeded)
|
|
20
|
-
// 1 —
|
|
22
|
+
// 1 — a target missing [Unreleased], or carrying BOTH [Unreleased] and [vX.Y.Z]
|
|
21
23
|
// 2 — bad args / file I/O error
|
|
22
24
|
|
|
23
25
|
import fs from 'node:fs';
|
|
@@ -67,7 +69,17 @@ function promote(repo, pkg, version, date, dry) {
|
|
|
67
69
|
const txt = fs.readFileSync(p, 'utf8');
|
|
68
70
|
const newHeading = `## [${version}] — ${date}`;
|
|
69
71
|
if (txt.includes(newHeading)) {
|
|
70
|
-
|
|
72
|
+
// TRUE idempotency (v0.8.34 cut, gh#976): a partial Step-4 failure
|
|
73
|
+
// leaves some targets promoted; re-running must treat "already carries
|
|
74
|
+
// exactly this heading AND no [Unreleased] remains" as the DONE state,
|
|
75
|
+
// not an error — the error branch forced a hand-driven recovery.
|
|
76
|
+
// Having BOTH headings is still an error: the [Unreleased] content
|
|
77
|
+
// would silently miss the release.
|
|
78
|
+
if (!txt.includes('## [Unreleased]')) {
|
|
79
|
+
console.log(` ok ${pkg} — already promoted to ${newHeading} (idempotent skip)`);
|
|
80
|
+
return 'promoted';
|
|
81
|
+
}
|
|
82
|
+
console.error(` ERROR ${pkg} — has BOTH ${newHeading} and [Unreleased]: merge the [Unreleased] block into the promoted section by hand`);
|
|
71
83
|
return 'already-promoted';
|
|
72
84
|
}
|
|
73
85
|
if (!txt.includes('## [Unreleased]')) {
|
|
@@ -111,10 +123,18 @@ function selftest() {
|
|
|
111
123
|
if (out.includes('## [Unreleased]')) { console.error('selftest FAIL: [Unreleased] heading should have been renamed, not left behind'); process.exit(1); }
|
|
112
124
|
if (!out.includes('### Fixed\n- x')) { console.error('selftest FAIL: content under the heading must survive the rename'); process.exit(1); }
|
|
113
125
|
|
|
114
|
-
// Idempotency: re-running against
|
|
115
|
-
//
|
|
126
|
+
// Idempotency across partial runs (v0.8.34 cut): re-running against a
|
|
127
|
+
// fully-promoted target is the DONE state — 'promoted', file untouched.
|
|
128
|
+
const beforeRerun = fs.readFileSync(clPath, 'utf8');
|
|
116
129
|
const r2 = promote(tmp, 'web-components', '0.2.0', '2026-02-02', false);
|
|
117
|
-
if (r2 !== '
|
|
130
|
+
if (r2 !== 'promoted') { console.error(`selftest FAIL: expected idempotent 'promoted' on re-run, got '${r2}'`); process.exit(1); }
|
|
131
|
+
if (fs.readFileSync(clPath, 'utf8') !== beforeRerun) { console.error('selftest FAIL: idempotent re-run must not modify the file'); process.exit(1); }
|
|
132
|
+
|
|
133
|
+
// BOTH headings present = the one loud stop left: the [Unreleased]
|
|
134
|
+
// content would silently miss the release.
|
|
135
|
+
fs.writeFileSync(clPath, '# C\n\n## [Unreleased]\n\n### Fixed\n- late fix\n\n## [0.2.0] — 2026-02-02\n\n- x\n');
|
|
136
|
+
const r2b = promote(tmp, 'web-components', '0.2.0', '2026-02-02', false);
|
|
137
|
+
if (r2b !== 'already-promoted') { console.error(`selftest FAIL: expected 'already-promoted' for BOTH-headings case, got '${r2b}'`); process.exit(1); }
|
|
118
138
|
|
|
119
139
|
// `root` target resolves to <repo>/CHANGELOG.md, no package resolution.
|
|
120
140
|
const rootPath = path.join(tmp, 'CHANGELOG.md');
|