@adia-ai/adia-ui-forge 0.8.60 → 0.8.61

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.
@@ -46,7 +46,7 @@ fluctuate).
46
46
  ## Phase 2 — fixes per bucket
47
47
 
48
48
  - **A. Holdout alignment** — map each intent's `expected_chunk` to the actual
49
- top-1; update `packages/gen-ui/engine/corpus/evals/holdout-compose-from-chunks.jsonl`.
49
+ top-1; update `packages/gen-ui/engine/evals/corpus/holdout-compose-from-chunks.jsonl`.
50
50
  - **E. Measurement traps** — PascalCase→kebab (`AgentTrace` → `agent-trace`,
51
51
  not `agenttrace`); substring false positives (`pane` vs `panel`,
52
52
  `textarea-ui` contains `text-ui` — word boundaries); case sensitivity (`/i`).
@@ -82,7 +82,7 @@ the SKILL.md floors hold.
82
82
  The two `check:*-eval-regression` scripts own the floor numbers — read the source
83
83
  before quoting a number elsewhere; SKILL.md only mirrors them and can drift (it
84
84
  once silently regressed to `cov≥40` before the mechanical gate existed). Zettel's
85
- floors are a committed file, `evals/health/zettel-floor.json` (gh#1391)
85
+ floors are a committed file, `packages/gen-ui/engine/evals/health/zettel-floor.json` (gh#1391);
86
86
  `scripts/release/check-zettel-eval-regression.mjs` loads it at runtime and refuses
87
87
  to run without it, so re-baselining is a JSON diff, not a source edit. Free-form's
88
88
  floors are still `ALERT_FLOOR`/`HARD_FLOOR` constants in
@@ -202,7 +202,9 @@ for any constant or decision lives in git and PR descriptions
202
202
  (`THINKING_BUDGET_MONOLITHIC_THINKING`) and `free-form-composer/
203
203
  index.js`'s ingredient-picker call, both its primary pick and its own
204
204
  paraphrase-retry (`THINKING_BUDGET_FREE_FORM`), both starting at the
205
- bridge's own `DEFAULT_THINKING_BUDGET` (10000). `auto` inherits
205
+ bridge's own `DEFAULT_THINKING_BUDGET`, lowered to 4000 (gh#3516 slice
206
+ 3, conductor ruling 2026-09-08) after an n=5 measurement at 10000
207
+ showed no measurable quality gain with real cost. `auto` inherits
206
208
  through the free-form picker call the moment it escalates that far -
207
209
  `monolithic-thinking` itself is never reachable via `auto`'s own
208
210
  escalation ladder. Every other LLM call site (`generate-pro.js`'s four
@@ -86,6 +86,18 @@ shows the results); `--strict` enforcement is deferred to TKT-0010
86
86
  (27% of the corpus currently fails, mostly a schema-generation gap
87
87
  around `data-*`/`span` attributes rather than corpus-content defects).
88
88
 
89
+ ## Provider thinking opt-in kill switch (gh#3516, LLD-3516 slice 1)
90
+
91
+ `monolithic-thinking` and `free-form-composer` opt into provider thinking at
92
+ a fixed strategy-level budget (`THINKING_BUDGET_MONOLITHIC_THINKING`,
93
+ `THINKING_BUDGET_FREE_FORM`, both 4000 as of gh#3516 slice 3) via the shared
94
+ `compose/shared/thinking-opt-in.js` helper. `ADIA_THINKING_OPT_IN=0` drops
95
+ the `thinking`/`thinkingBudget` fields at both call sites entirely (not
96
+ `thinking: false`, the adapter never sees the keys), giving the
97
+ thinking-off baseline a runtime lever instead of a code revert. Unset (or
98
+ `"1"`) keeps current behavior. This is an operations knob, not a caller
99
+ option: no consumer threads it through `generateUI()`.
100
+
89
101
  ## Pitfalls
90
102
 
91
103
  - **`STRONG_MATCH_THRESHOLD` was raised 22 → 40 post-incident.** Lowering it
@@ -17,7 +17,7 @@ the live admin-dashboard example.
17
17
  `<select-ui avatar="…" value="…" variant="ghost">` context switcher —
18
18
  NOT `<menu-ui>` (legacy pattern) — ★ commonly mis-implemented
19
19
  4. Sidebar nav wrap around `<nav-ui>` — `<section>` (per the examples),
20
- `<section-ui>` (card-style chrome), or `<admin-scroll>` all accepted;
20
+ `<section-ui>` (card-style chrome), or `<page-scroll>` all accepted;
21
21
  a bare `<nav-ui>` direct child overflows long lists
22
22
  5. Sidebar `<admin-statusbar slot="footer">` with `<select-ui avatar="…">`
23
23
  (user menu) — ★ commonly missing
@@ -28,17 +28,23 @@ the live admin-dashboard example.
28
28
  ★ spacer + actions commonly missing
29
29
  8. `[data-actions]` contains `<popover-ui>` + `<theme-panel slot="content">`
30
30
  (there is no `<theme-picker-ui>`)
31
- 9. `<admin-scroll>` wrapping optional `<aside data-subnav hidden>` +
32
- `<router-ui>` (or `<admin-page>` directly for non-routed)
33
- **[deprecated 2026-09-01, ADR-0098]** `admin-scroll` is a wholesale
34
- rename to `page-scroll` (both modes carried over); `admin-scroll` stays
35
- as a working compat alias for the deprecation window only.
31
+ 9. `<page-scroll>` wrapping optional `<aside data-subnav hidden>` +
32
+ `<router-ui>` (or `<page-ui band>` directly for non-routed)
33
+ **[deleted, ADR-0098 / gh#3745]** `admin-scroll` was a wholesale
34
+ rename to `page-scroll` (both modes carried over); its one-release
35
+ deprecation window has closed and the module is deleted (gh#3745).
36
+ The `admin-page` family (`admin-page`/`admin-page-header`/
37
+ `admin-page-body`) is deleted outright per ADR-0098, no compat alias.
38
+ `page-scroll` wrapping `page-ui[band]` is the sole successor for the
39
+ routed and non-routed case alike.
36
40
  10. `<admin-page>` with `<admin-page-header>` + `<admin-page-body>`
37
- **[deprecated 2026-09-01, ADR-0098]** The `admin-page` family is
38
- retired deprecate-then-delete in favor of `page-ui[band]` — `page-ui`
39
- is now the one canonical page-chrome primitive; this anatomy still
40
- describes the pre-migration shape for auditing legacy surfaces during
41
- the deprecation window.
41
+ **[deleted, ADR-0098]** The `admin-page` family is deleted outright,
42
+ no compat alias and no open deprecation window, in favor of
43
+ `page-ui[band]`; `page-ui` is now the one canonical page-chrome
44
+ primitive (item 9 above shows the live composition: `page-scroll`
45
+ wrapping `page-ui[band]`). This item is kept only so this anatomy
46
+ still recognizes the retired shape when auditing legacy surfaces
47
+ that predate the migration; never author it in new surfaces.
42
48
  11. `<admin-statusbar>` at content footer (version strip) — ★ commonly missing
43
49
  12. Second `<admin-sidebar slot="trailing">` (inspector rail, hidden by
44
50
  default) — strongly recommended
@@ -64,7 +70,7 @@ the live admin-dashboard example.
64
70
  | content topbar missing `[data-spacer]` / `[data-actions]` | warning — part 7 |
65
71
  | content missing trailing `<admin-statusbar>` | warning — part 11 |
66
72
  | sidebar topbar contains only plain text | warning — part 3 context switcher |
67
- | `<admin-scroll>` missing around `<admin-page>` | critical part 9 (the enforcing script `audit-shell-composition.mjs` tiers this critical: without the scroll+page wrapper the content renders flush with the topbar, no margins) **[deprecated 2026-09-01, ADR-0098]** `admin-scroll`/`admin-page` describe the pre-migration shape; the successor is `page-scroll` wrapping `page-ui[band]` |
73
+ | `<page-scroll>` missing around `<page-ui band>` | critical, part 9 (the enforcing script `audit-shell-composition.mjs` tiers this critical: without the scroll+chrome wrapper the content renders flush with the topbar, no margins). **[deleted, ADR-0098 / gh#3745]** `admin-scroll` and the `admin-page` family are both deleted outright, no compat alias; the successor is `page-scroll` wrapping `page-ui[band]` |
68
74
 
69
75
  ## Opt-out contract
70
76
 
@@ -20,6 +20,7 @@
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 { warnIfStaleCopy } from './stale-copy-warning.mjs';
23
24
  import { PACKAGE_ROSTER } from './package-paths.mjs';
24
25
 
25
26
  // Instance data — fork-configurable. The npm scope the lockstep roster publishes under.
@@ -288,6 +289,13 @@ async function main() {
288
289
  const args = parseArgs(process.argv.slice(2));
289
290
  // Fail-fast guard: refuse to run npm/gh against a non-monorepo directory.
290
291
  assertMonorepoRoot(process.cwd());
292
+ // Stale-plugin-cache self-check (ticket #3943 item 2), same primitive
293
+ // release-pack.mjs's own top-level check now calls.
294
+ warnIfStaleCopy(
295
+ import.meta.url,
296
+ process.cwd(),
297
+ 'packages/plugins/adia-ui-forge/skills/package-release/scripts/dispatch-publish.mjs',
298
+ );
291
299
  if (args.after) checkAfter(args.after, args.scope);
292
300
 
293
301
  // Recovery mode — verify each publish workflow SUCCEEDED for its pushed tag
@@ -103,15 +103,15 @@ export const GATE_ROSTER = [
103
103
  // on the handoff re-run, since the promoted heading was already on main).
104
104
  { n: 26, cmd: 'node scripts/release/check-dx-sweep-freshness.mjs', targetVersionArg: true, what: 'DX sweep fresh + not regression-RED' },
105
105
  // gh#1135 (WS-4 SPEC REQ-06): a fresh free-form eval run feeds the health
106
- // record gate 28 writes without it, "latest" under evals/mcp/runs/ could
106
+ // record gate 28 writes: without it, "latest" under packages/gen-ui/engine/evals/mcp/runs/ could
107
107
  // be a stale leftover from an earlier ad-hoc run, not this cut's own state.
108
- { n: 27, cmd: 'npm run eval:diff -- --engine free-form', what: 'fresh free-form eval run (evals/health input)' },
109
- // REQ-06: commits evals/health/<version>.json the version this record
108
+ { n: 27, cmd: 'npm run eval:diff -- --engine free-form', what: 'fresh free-form eval run (packages/gen-ui/engine/evals/health input)' },
109
+ // REQ-06: commits packages/gen-ui/engine/evals/health/<version>.json; the version this record
110
110
  // names is the CUT's target version from the start, never the
111
111
  // pre/post-bump `hygieneVersion` gate 24 needs (a claim-validation
112
112
  // concern this is not) — hence `targetVersionArg` rather than
113
113
  // `versionArg`, a distinct flag release-pack.mjs's step3PreFlight reads.
114
- { 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 },
114
+ { n: 28, cmd: 'node scripts/release/write-eval-health.mjs', what: 'packages/gen-ui/engine/evals/health/<version>.json committed, AC-01/AC-02 run for real', targetVersionArg: true },
115
115
  // ADR-0048 (gh#1192) estate-split latch. Between P1 (mint @adia-ai/gen-ui)
116
116
  // and P5 (shims + roster) the tree is correct in-repo but deliberately
117
117
  // NOT publishable: the old package dirs are `private: true` stubs the
@@ -76,6 +76,7 @@ import { GATE_ROSTER } from './gate-roster.mjs';
76
76
  // hand-duplicating them — bump.mjs guards its own main()/selftest() behind
77
77
  // an entry-point check specifically so this import is side-effect-free.
78
78
  import { PINNED_REFS, REPO_PINNED_REFS } from './bump.mjs';
79
+ import { warnIfStaleCopy } from './stale-copy-warning.mjs';
79
80
 
80
81
  const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
81
82
  const REPO = process.cwd();
@@ -523,18 +524,18 @@ function step1ReBaseline(args) {
523
524
  // since neither touches (a)'s files). This is the roster's one
524
525
  // CSS-before-JS-shaped file-write ordering dependency: gate 27
525
526
  // (`eval:diff --engine free-form`) writes a fresh
526
- // evals/mcp/runs/<ISO>/ directory; gate 28
527
+ // packages/gen-ui/engine/evals/mcp/runs/<ISO>/ directory; gate 28
527
528
  // (write-eval-health.mjs) reads whichever directory under
528
- // evals/mcp/runs/ SORTS LEXICALLY LAST not specifically the
529
+ // packages/gen-ui/engine/evals/mcp/runs/ SORTS LEXICALLY LAST, not specifically the
529
530
  // latest free-form run — and expects free-form.json there. Gate
530
531
  // 16 (`eval:diff --engine zettel`) writes its OWN fresh directory
531
532
  // under that same root; if 16 and 27 ran concurrently and 16
532
533
  // happened to finish after 27, gate 28 would silently pick 16's
533
534
  // directory (no free-form.json) and commit
534
- // evals/health/<version>.json with `engines.free-form: null` a
535
+ // packages/gen-ui/engine/evals/health/<version>.json with `engines.free-form: null`, a
535
536
  // real, silent coverage loss the roster's fixed serial order
536
537
  // prevents today only by construction. Keeping 16 before 27
537
- // before 28, uninterrupted by any other evals/mcp/runs/ writer,
538
+ // before 28, uninterrupted by any other packages/gen-ui/engine/evals/mcp/runs/ writer,
538
539
  // preserves that guarantee.
539
540
  //
540
541
  // Fail-fast BETWEEN phases (a Phase 1 failure skips Phase 2 outright — no
@@ -1224,7 +1225,7 @@ function step5Commit(args) {
1224
1225
  // targetVersionArg comment above). Under --skip-gates the record was
1225
1226
  // never written (Step 3 returned early), so staging it would make the
1226
1227
  // whole `git add` fail on a missing pathspec (PR #1204 review).
1227
- ...(args.skipGates ? [] : [`evals/health/${args.version}.json`]),
1228
+ ...(args.skipGates ? [] : [`packages/gen-ui/engine/evals/health/${args.version}.json`]),
1228
1229
  ...LOCKSTEP_ROSTER.flatMap(({ name, dir, plugin }) => {
1229
1230
  // Roster-driven (H3): each package stages its manifest + CHANGELOG;
1230
1231
  // plugins also stage .claude-plugin/plugin.json (the /plugin-update
@@ -1265,6 +1266,34 @@ function step5Commit(args) {
1265
1266
  // package.json is already staged above; web-modules' peer-pin entry
1266
1267
  // targets that same package.json).
1267
1268
  ...pinnedRefFiles(PINNED_REFS, REPO_PINNED_REFS),
1269
+ // gh#3946 (v0.8.60 cut hygiene, item 1), root-caused for real at the
1270
+ // v0.8.61 cut: the changelog-fragment assembly step
1271
+ // (assemble-changelog-fragments.mjs) DELETES each consumed
1272
+ // `changes/<pr>.md`/`changes/gh-<issue>.md` fragment (and its
1273
+ // package-suffixed siblings) after folding it into the target
1274
+ // CHANGELOG's own `[Unreleased]` section, a deletion of an
1275
+ // already-tracked file is exactly the 'D' shape Step 5.6's
1276
+ // unstagedTrackedLeftovers() guard blocks on below, same as any other
1277
+ // tracked modification this list must stage first. The v0.8.60 fix
1278
+ // used a shell glob (`changes/*.md`) on the WRONG theory that an
1279
+ // unmatched glob falls through to git's own pathspec wildcard matching:
1280
+ // that only holds when NOTHING in the directory survives. `changes/`
1281
+ // also holds `README.md`, never a fragment, never deleted, so the shell
1282
+ // glob reliably expands to exactly that one survivor and `git add`
1283
+ // never sees a pathspec covering the deleted fragments at all, they
1284
+ // stay unstaged with no error until Step 5.6's guard catches them
1285
+ // downstream (confirmed live at the v0.8.61 cut: 194 fragments deleted,
1286
+ // one survivor, exactly this shape). The correct form names the
1287
+ // DIRECTORY, not a glob inside it: git enumerates the pathspec itself
1288
+ // against its own index rather than asking the shell to enumerate the
1289
+ // working tree first, so it stages every tracked change under
1290
+ // `changes/` including a deletion, regardless of how many (if any)
1291
+ // sibling files still exist on disk. bump.mjs's own PACKAGE_VERSION-
1292
+ // literal pin writes into `packages/web-modules/index.css` too (the
1293
+ // same drift class as the icons-cdn.js entry pinnedRefFiles() already
1294
+ // covers above), so it needs the same explicit, hand-listed entry.
1295
+ 'changes/',
1296
+ 'packages/web-modules/index.css',
1268
1297
  ];
1269
1298
  const dedupedReleaseFiles = [...new Set(releaseFiles)];
1270
1299
  sh(`git add ${dedupedReleaseFiles.join(' ')}`, args);
@@ -1746,24 +1775,21 @@ async function main() {
1746
1775
  // Fail-fast guard: refuse to git/npm/gh/curl against a non-monorepo directory.
1747
1776
  assertMonorepoRoot(REPO);
1748
1777
  console.log(`package-release release-pack — v${args.version} (${args.mode})${args.dry ? ' [DRY]' : ''}`);
1749
- // Stale-plugin-cache self-check (v0.8.32 cut, first failure): the INSTALLED
1750
- // plugin cache only refreshes on a version bump, so running this script from
1751
- // the cache mid-cycle can execute a version that predates its own guards
1752
- // (SKILL.md §Plugin cache). If the repo carries its own copy and the bytes
1753
- // differ, warn loudly — the repo copy is almost always the newer one.
1754
- const repoCopy = path.join(REPO, 'packages/plugins/adia-ui-forge/skills/package-release/scripts/release-pack.mjs');
1755
- const selfPath = new URL(import.meta.url).pathname;
1756
- if (fs.existsSync(repoCopy) && path.resolve(selfPath) !== path.resolve(repoCopy)) {
1757
- const self = fs.readFileSync(selfPath, 'utf8');
1758
- const repo = fs.readFileSync(repoCopy, 'utf8');
1759
- if (self !== repo) {
1760
- console.error('\n⚠ STALE-COPY WARNING: this release-pack.mjs differs from the repo\'s own copy at');
1761
- console.error(` ${repoCopy}`);
1762
- console.error(' The installed plugin cache lags the repo between cuts (SKILL.md §Plugin cache);');
1763
- console.error(' the repo copy is almost certainly newer — prefer running it:');
1764
- console.error(` node ${repoCopy} <same args>\n`);
1765
- }
1766
- }
1778
+ // Stale-plugin-cache self-check (v0.8.32 cut, first failure; generalized
1779
+ // ticket #3943 item 2): the INSTALLED plugin cache only refreshes on a
1780
+ // version bump, so running this script from the cache mid-cycle can
1781
+ // execute a version that predates its own guards (SKILL.md §Plugin
1782
+ // cache). If the repo carries its own copy and the bytes differ, warn
1783
+ // loudly: the repo copy is almost always the newer one. The comparison
1784
+ // itself now lives in this skill's own stale-copy-warning.mjs (package-
1785
+ // local by design, see that file's own header), a small primitive its
1786
+ // sibling scripts share, rather than this inline block being the only
1787
+ // copy of the pattern.
1788
+ warnIfStaleCopy(
1789
+ import.meta.url,
1790
+ REPO,
1791
+ 'packages/plugins/adia-ui-forge/skills/package-release/scripts/release-pack.mjs',
1792
+ );
1767
1793
  // gh#3342: --from-step10 resumes a handoff that already tagged, pushed,
1768
1794
  // and published — skip straight to Step 10 (GH releases + site deploy)
1769
1795
  // rather than a plain re-run, which would re-run the full pre-flight gate
@@ -2030,7 +2056,7 @@ async function selftest() {
2030
2056
  // gh#1197 — Step 5's staging pathspec must carry gate 28's committed
2031
2057
  // health record, or it rides untracked past the release commit
2032
2058
  // (v0.8.35/v0.8.36 both did, PR #1196 swept them by hand).
2033
- 'evals/health/9.9.9.json',
2059
+ 'packages/gen-ui/engine/evals/health/9.9.9.json',
2034
2060
  // gh#1899 — Step 4 must regenerate the Codex plugin manifests post-bump
2035
2061
  // (the v0.8.48 release PR #1897 shipped stale ones without this step).
2036
2062
  'Step 4d.6 — regenerate Codex plugin manifests',
@@ -2075,6 +2101,18 @@ async function selftest() {
2075
2101
  '[dry] npm run build:bundles',
2076
2102
  'packages/web-components/dist',
2077
2103
  'packages/web-modules/dist',
2104
+ // gh#3946 (v0.8.60 cut hygiene, item 1) - bump.mjs and the changelog
2105
+ // assembly step both write to these two paths but neither was in
2106
+ // Step 5's allowlist, so the v0.8.60 cut's real Step 5.6 guard refused
2107
+ // with "244 tracked file(s) modified by this cut are still unstaged"
2108
+ // after all 36 gates had already passed. These markers prove the
2109
+ // `git add` pathspec this --dry run prints actually names both
2110
+ // entries. `changes/` is the directory form (root-caused at the
2111
+ // v0.8.61 cut, see the matching entry's own comment above), never a
2112
+ // glob: a glob that survives one non-fragment file (changes/README.md
2113
+ // always does) hides every deleted fragment from `git add` silently.
2114
+ 'changes/',
2115
+ 'packages/web-modules/index.css',
2078
2116
  // gh#2473 — Step 5.6's unstaged-tracked-files guard must actually run
2079
2117
  // (as a no-op) on a clean --dry plan. Its real leftover-detection
2080
2118
  // logic is unit-tested directly above via unstagedTrackedLeftovers();
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ // stale-copy-warning.mjs: generalizes release-pack.mjs's own STALE-COPY
3
+ // WARNING (ticket #3943 item 2, gh#3764 line 55) into a small helper this
4
+ // skill's own scripts can share.
5
+ //
6
+ // WHY
7
+ //
8
+ // A script reached through an installed plugin cache
9
+ // (`~/.claude/plugins/cache/...`) only refreshes on a version bump, so
10
+ // running it mid-cycle can execute a version that predates the repo's own
11
+ // copy at the same relative path. This is invisible to a repo grep: the
12
+ // grep reads the repo's own, already-corrected file, while the running
13
+ // process reads the cache's stale one: the filer of ticket #3943 hit this
14
+ // exact shape twice in one night (gh#3886, and the filing session itself).
15
+ //
16
+ // This module names ONE detector and ONE formatter, both pure, plus a thin
17
+ // convenience wrapper a script calls once near its own entrypoint. It never
18
+ // errors and never exits the caller's process: per the ticket's own
19
+ // framing, this is a documented check, not a gate: the repo copy is
20
+ // "almost certainly newer," never provably so from bytes alone, so the
21
+ // right response is a loud, named warning, not a failure.
22
+ //
23
+ // PACKAGE-LOCAL BY DESIGN (ticket #3943's own check:pr-ready finding): this
24
+ // file lives here, inside package-release/scripts/, rather than the repo's
25
+ // top-level scripts/lib/, because this skill publishes as its own npm
26
+ // package (`@adia-ai/adia-ui-forge`, PACKAGE_ROSTER). A script inside that
27
+ // package importing a file OUTSIDE its own package root resolves fine in a
28
+ // pnpm worktree but is an unresolvable specifier once packed
29
+ // (`npm run check:packed-imports`, which caught exactly this on the first
30
+ // draft of this change: an escaped-root import out of the tarball). Same
31
+ // reasoning `bump.mjs`'s own inlined `isEntryPoint()` already states for
32
+ // not importing `scripts/lib/is-entry-point.mjs`; this file follows that
33
+ // same precedent (its own inlined entry-point check below, no cross-
34
+ // package import) rather than introducing a second copy of the hazard.
35
+ // A future repo-TOOLING script under the top-level `scripts/` tree (never
36
+ // packaged into any tarball) that wants this same pattern is free to copy
37
+ // this file's own two pure functions there; nothing here assumes only one
38
+ // copy may ever exist, the packaging boundary is exactly why one shared
39
+ // copy cannot serve both sides.
40
+ //
41
+ // USAGE (a script inside this same package, e.g. a sibling in
42
+ // package-release/scripts/):
43
+ //
44
+ // import { warnIfStaleCopy } from './stale-copy-warning.mjs';
45
+ // warnIfStaleCopy(import.meta.url, REPO_ROOT, 'path/relative/to/REPO_ROOT/this-script.mjs');
46
+ //
47
+ // SELFTEST:
48
+ //
49
+ // node packages/plugins/adia-ui-forge/skills/package-release/scripts/stale-copy-warning.mjs selftest
50
+
51
+ import fs from 'node:fs';
52
+ import os from 'node:os';
53
+ import path from 'node:path';
54
+ import { fileURLToPath } from 'node:url';
55
+
56
+ /**
57
+ * Pure: compares `selfPath`'s own bytes against the repo's copy at
58
+ * `repoRoot/relativePath`. Returns `null` when there is nothing to warn
59
+ * about (no repo copy exists to compare against, `selfPath` already IS the
60
+ * repo copy, or the bytes agree); otherwise `{ repoCopyPath, selfPath }`,
61
+ * both resolved absolute paths.
62
+ * @param {{selfPath: string, repoRoot: string, relativePath: string, readFile?: Function, fileExists?: Function}} args
63
+ * @returns {{repoCopyPath: string, selfPath: string} | null}
64
+ */
65
+ export function detectStaleCopy({ selfPath, repoRoot, relativePath, readFile = fs.readFileSync, fileExists = fs.existsSync }) {
66
+ const resolvedSelf = path.resolve(selfPath);
67
+ const repoCopyPath = path.resolve(repoRoot, relativePath);
68
+ if (!fileExists(repoCopyPath)) return null; // nothing at the repo's own path to compare against
69
+ if (resolvedSelf === repoCopyPath) return null; // already running the repo's own copy
70
+ let selfText;
71
+ let repoText;
72
+ try {
73
+ selfText = readFile(selfPath, 'utf8');
74
+ repoText = readFile(repoCopyPath, 'utf8');
75
+ } catch {
76
+ return null; // unreadable on either side; nothing this check can prove
77
+ }
78
+ if (selfText === repoText) return null;
79
+ return { repoCopyPath, selfPath: resolvedSelf };
80
+ }
81
+
82
+ /**
83
+ * Pure: renders `detectStaleCopy`'s own non-null result as the printable
84
+ * warning block, the same shape release-pack.mjs's own inline check used.
85
+ * @param {{repoCopyPath: string, selfPath: string}} hit
86
+ * @returns {string}
87
+ */
88
+ export function formatStaleCopyWarning({ repoCopyPath, selfPath }) {
89
+ return [
90
+ '',
91
+ `⚠ STALE-COPY WARNING: this ${path.basename(selfPath)} differs from the repo's own copy at`,
92
+ ` ${repoCopyPath}`,
93
+ " The installed plugin cache lags the repo between cuts (ticket #3943, gh#3764 line 55);",
94
+ ' the repo copy is almost certainly newer, prefer running it:',
95
+ ` node ${repoCopyPath} <same args>`,
96
+ '',
97
+ ].join('\n');
98
+ }
99
+
100
+ /**
101
+ * Convenience wrapper for a script's own top-level self-check: resolves its
102
+ * own path from `import.meta.url`, compares it against
103
+ * `repoRoot/relativePath`, and logs (never throws, never exits the caller)
104
+ * when the two diverge. Call this once, near the top of a script's own
105
+ * entrypoint, before any real work.
106
+ * @param {string} selfUrl the caller's own `import.meta.url`
107
+ * @param {string} repoRoot the target repo's own root, resolved the same way the caller resolves it for everything else
108
+ * @param {string} relativePath this script's own path, relative to `repoRoot`
109
+ * @param {{log?: Function}} [opts]
110
+ * @returns {boolean} whether a warning was logged
111
+ */
112
+ export function warnIfStaleCopy(selfUrl, repoRoot, relativePath, { log = console.error } = {}) {
113
+ const selfPath = fileURLToPath(selfUrl);
114
+ const hit = detectStaleCopy({ selfPath, repoRoot, relativePath });
115
+ if (hit) log(formatStaleCopyWarning(hit));
116
+ return hit !== null;
117
+ }
118
+
119
+ // ---- selftest -------------------------------------------------------------
120
+
121
+ function selftest() {
122
+ let ran = 0;
123
+ const assert = (cond, msg) => {
124
+ ran += 1;
125
+ if (!cond) throw new Error(`selftest failed: ${msg}`);
126
+ };
127
+
128
+ const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'stale-copy-warning-'));
129
+ try {
130
+ const repoRoot = path.join(sandbox, 'repo');
131
+ const relativePath = path.join('scripts', 'thing.mjs');
132
+ const repoCopyPath = path.join(repoRoot, relativePath);
133
+ const cachePath = path.join(sandbox, 'cache-copy.mjs');
134
+ fs.mkdirSync(path.dirname(repoCopyPath), { recursive: true });
135
+
136
+ // Positive control: cache bytes differ from the repo's own copy.
137
+ fs.writeFileSync(repoCopyPath, 'v2 (repo, newer)\n');
138
+ fs.writeFileSync(cachePath, 'v1 (cache, stale)\n');
139
+ const diverged = detectStaleCopy({ selfPath: cachePath, repoRoot, relativePath });
140
+ assert(diverged !== null, 'differing bytes must be detected');
141
+ assert(diverged.repoCopyPath === path.resolve(repoCopyPath), 'the hit names the resolved repo copy path');
142
+ const warning = formatStaleCopyWarning(diverged);
143
+ assert(warning.includes('STALE-COPY WARNING'), 'the formatted warning names itself');
144
+ assert(warning.includes(repoCopyPath), 'the formatted warning cites the repo copy path');
145
+
146
+ // Negative control: bytes agree.
147
+ fs.writeFileSync(cachePath, 'v2 (repo, newer)\n');
148
+ assert(
149
+ detectStaleCopy({ selfPath: cachePath, repoRoot, relativePath }) === null,
150
+ 'identical bytes must never warn',
151
+ );
152
+
153
+ // Negative control: the "self" path already IS the repo's own copy
154
+ // (release-pack.mjs's own first guard: never warn about itself).
155
+ assert(
156
+ detectStaleCopy({ selfPath: repoCopyPath, repoRoot, relativePath }) === null,
157
+ 'running the repo copy directly must never warn',
158
+ );
159
+
160
+ // Negative control: no repo copy exists at all (nothing to compare
161
+ // against, e.g. a script the repo has since deleted).
162
+ assert(
163
+ detectStaleCopy({ selfPath: cachePath, repoRoot, relativePath: path.join('scripts', 'gone.mjs') }) === null,
164
+ 'a missing repo copy must never warn (nothing provable)',
165
+ );
166
+
167
+ // warnIfStaleCopy: the convenience wrapper logs on divergence and
168
+ // reports it, using a real file:// URL the way a real caller's
169
+ // import.meta.url would.
170
+ fs.writeFileSync(cachePath, 'v1 (cache, stale)\n');
171
+ let logged = '';
172
+ const warned = warnIfStaleCopy(
173
+ new URL(`file://${cachePath}`).href,
174
+ repoRoot,
175
+ relativePath,
176
+ { log: (msg) => { logged += msg; } },
177
+ );
178
+ assert(warned === true, 'warnIfStaleCopy reports true when it warns');
179
+ assert(logged.includes('STALE-COPY WARNING'), 'warnIfStaleCopy actually logs the warning text');
180
+
181
+ // And stays silent (never throws, never exits) when bytes agree.
182
+ fs.writeFileSync(cachePath, 'v2 (repo, newer)\n');
183
+ let loggedAgain = '';
184
+ const clean = warnIfStaleCopy(
185
+ new URL(`file://${cachePath}`).href,
186
+ repoRoot,
187
+ relativePath,
188
+ { log: (msg) => { loggedAgain += msg; } },
189
+ );
190
+ assert(clean === false, 'warnIfStaleCopy reports false when bytes agree');
191
+ assert(loggedAgain === '', 'warnIfStaleCopy logs nothing when bytes agree');
192
+ } finally {
193
+ fs.rmSync(sandbox, { recursive: true, force: true });
194
+ }
195
+
196
+ console.log(`stale-copy-warning selftest, all assertions passed, ${ran} controls ran`);
197
+ return 0;
198
+ }
199
+
200
+ // Realpath-safe entry-point check, inlined rather than imported from the
201
+ // top-level scripts/lib/is-entry-point.mjs (see the package-local-by-design
202
+ // note above); mirrors bump.mjs's own identical inlining in this same dir.
203
+ function isEntryPoint() {
204
+ if (!process.argv[1]) return false;
205
+ try {
206
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ if (isEntryPoint()) {
213
+ const topArgv = process.argv.slice(2);
214
+ process.exit(topArgv[0] === 'selftest' ? selftest() : 0);
215
+ }
@@ -15,6 +15,7 @@
15
15
  import { execSync } from 'node:child_process';
16
16
  import process from 'node:process';
17
17
  import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
18
+ import { warnIfStaleCopy } from './stale-copy-warning.mjs';
18
19
  import { PACKAGE_ROSTER } from './package-paths.mjs';
19
20
 
20
21
  // The roster is single-sourced in package-paths.mjs (H3) — the `adia-ui-*-v*`
@@ -60,6 +61,13 @@ function main() {
60
61
  const args = parseArgs(process.argv.slice(2));
61
62
  // Fail-fast guard: refuse to run git against a non-monorepo directory.
62
63
  assertMonorepoRoot(args.repo);
64
+ // Stale-plugin-cache self-check (ticket #3943 item 2), same primitive
65
+ // release-pack.mjs's own top-level check now calls.
66
+ warnIfStaleCopy(
67
+ import.meta.url,
68
+ args.repo,
69
+ 'packages/plugins/adia-ui-forge/skills/package-release/scripts/tag-lockstep.mjs',
70
+ );
63
71
  const tags = buildTagList(args.version);
64
72
 
65
73
  if (args.deleteMode) {
@@ -30,6 +30,7 @@ file, including the depth references those entries cross-link.
30
30
  ## Demos & doc surfaces
31
31
 
32
32
  - [composite-demo-protocol.md](composite-demo-protocol.md) — what discipline governs any `packages/web-modules/**` demo edit, beyond what the `Pattern source:` gate can see?
33
+ - [examples-structure.md](examples-structure.md): what order do a primitive's own `<name>.examples.html` `<h2 variant="section">` blocks follow, and where does `Typography registers` sit relative to `Edge Cases` and the generated API tail?
33
34
  - [canonical-pattern-index.md](canonical-pattern-index.md) — which canonical `.contents.html` files should I survey for a given UI type? Auto-generated; regenerate via `scripts/build-canonical-pattern-index.mjs`.
34
35
  - [trait-pages.md](trait-pages.md) — which sections, in which order, must a `site/pages/traits/<name>/` detail page carry (ADR-0019 template)?
35
36
 
@@ -0,0 +1,52 @@
1
+ # `<name>.examples.html` section order convention
2
+
3
+ Every primitive's own `packages/web-components/components/<name>/<name>.examples.html`
4
+ follows one binding `<h2 variant="section">` order. gh#4159: a merged-registers sweep
5
+ (gh#3843) found the order inconsistent across five files, `alert` had "Typography
6
+ registers" near the top, `code`/`command` had it after the generated API tail, and
7
+ `accordion`/`divider` had it before Edge Cases, with no reference doc anywhere stating
8
+ which shape was correct. `site-docs-authoring`'s own `intent.md` explicitly declines
9
+ `packages/web-components/components/*/*.examples.html` and hands it here; this file is
10
+ that hand-off's landing spot.
11
+
12
+ ## Required order
13
+
14
+ 1. **Variant/usage sections**, one `<h2 variant="section">` per prop value, state, or
15
+ usage recipe the component demonstrates (`inline`, `block with language`, `grouped`,
16
+ …). Author-ordered; no fixed sequence among these.
17
+ 2. **`Combinations`**, sections composed together in context.
18
+ 3. **`Typography registers`**, the three-register (`scale="ui-sm"` / regular /
19
+ `scale="content-md"`) showcase, when the component carries one. Hand-authored, so it
20
+ sits with the other hand-authored sections above, immediately after `Combinations`
21
+ and before `Edge Cases`, never after the generated API tail.
22
+ 4. **`Edge Cases`**, boundary conditions and unusual content.
23
+ 5. **Generated API sections**, in order: `Properties`, `Events`, `Methods` (when
24
+ applicable), `CSS Tokens`, `Slots`, `A2UI`, `Related`.
25
+
26
+ Rationale for `Typography registers` landing before `Edge Cases` rather than after the
27
+ API tail: it is hand-authored prose+markup like `Combinations`/`Edge Cases`, not a
28
+ generated table like `Properties`/`Events`/`CSS Tokens`; grouping it with the other
29
+ hand-authored sections keeps the generated-vs-authored boundary at one place in the file
30
+ instead of two.
31
+
32
+ ## Why this also matters for `.examples.md`
33
+
34
+ `scripts/build/generate-examples-md.mjs` derives each `<name>.examples.md` from the
35
+ `.examples.html` source. Its main loop picks only the first `MAX_FRAGMENTS` (3)
36
+ `data-section` blocks in document order, so a section sitting far down the file (several
37
+ variant sections deep, as most shipped components run) would normally never be selected.
38
+ `Typography registers` is the one named exception: `processComponent()` guarantees its
39
+ inclusion regardless of ordinal position, so this convention is about authoring
40
+ coherence and readability, not about whether the section survives derivation at all.
41
+
42
+ ## Enforcement
43
+
44
+ `generate-examples-md.mjs`'s own `checkSectionOrder()`, run as part of `npm run
45
+ check:examples-md-fresh` (`--verify` mode) and the plain generate run alike, parses
46
+ every `packages/web-components/components/*/*.examples.html`'s ordered `<h2
47
+ variant="section">` list and reds, naming the file and both indices, when `Typography
48
+ registers` sits after `Edge Cases`. Pre-existing violations outside a given PR's own
49
+ lane are grandfathered via `scripts/build/examples-registers-order-baseline.json` (a
50
+ ratchet: printed as an advisory finding, never silently dropped, and never allowed to
51
+ grow) so the gate never regresses unrelated work; shrink that list to zero as each
52
+ file's own section order gets fixed.
@@ -448,17 +448,18 @@ Consumers (CodeMirror layout, canvas redraw, dependent UI) listen on the shell o
448
448
 
449
449
  ### admin cluster (canonical reference)
450
450
 
451
- **[deprecated 2026-09-01, ADR-0098]** `admin-page`/`admin-page-header`/
452
- `admin-page-body` are retired deprecate-then-delete; `admin-scroll` is
453
- renamed wholesale to `page-scroll`. `page-ui[band]` + `page-scroll` are
454
- now the canonical page-chrome pair this cluster still describes
455
- `admin-shell`'s pre-migration internal composition, valid during the
456
- deprecation window.
457
-
458
- - **3 JS-bearing children** — `<admin-shell>` (host coordinator), `<admin-sidebar>` (resize+collapse+persist), `<admin-command>` (Cmd+K palette)
459
- - **7 CSS-only structural children** `<admin-content>`, `<admin-topbar>`, `<admin-statusbar>`, `<admin-scroll>`, `<admin-page>`, `<admin-page-header>`, `<admin-page-body>`
460
- - **CSS bridge** — `packages/web-modules/shell/admin-shell/css/admin-shell.bespoke.css` (~240 LOC)
461
- - **Tests** — `admin-sidebar.test.js` 10/10, `admin-command.test.js` 9/9
451
+ **[deprecated 2026-09-01, ADR-0098]** The inline child enumeration this section
452
+ used to carry is retired. `admin-page`/`admin-page-header`/`admin-page-body`
453
+ were deleted outright (no compat alias); `admin-scroll` was renamed wholesale
454
+ to `page-scroll`, and its own one-release deprecation window has since closed,
455
+ the module deleted (gh#3745). `page-ui[band]` + `page-scroll` are the canonical
456
+ page-chrome pair.
457
+
458
+ For `admin-shell`'s own live composition, the current bespoke children (the
459
+ `@scope`-per-tag CSS convention landed in gh#4103), and the full 13-part
460
+ canonical anatomy, see
461
+ [admin-shell-anatomy.md](../../demo-audit/references/admin-shell-anatomy.md),
462
+ kept in sync with `site/index.html`'s live example rather than restated here.
462
463
 
463
464
  ### chat cluster (replicated pattern)
464
465