@adia-ai/adia-ui-forge 0.8.59 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +68 -0
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/references/contracts/a2ui-mcp-surface.md +1 -1
- package/scripts/lint-rules.generated.mjs +2418 -359
- package/skills/a2ui-maintenance/references/eval-diagnostics.md +2 -2
- package/skills/a2ui-maintenance/references/pipeline-overview.md +26 -0
- package/skills/a2ui-maintenance/references/strategy-engines.md +12 -0
- package/skills/demo-audit/references/admin-shell-anatomy.md +18 -12
- package/skills/gen-ui-review/scripts/gen-review-decompose.mjs +27 -34
- package/skills/gen-ui-review/scripts/overflow-detect.generated.mjs +101 -0
- package/skills/package-release/references/cut-procedure.md +49 -5
- package/skills/package-release/references/recovery-paths.md +20 -0
- package/skills/package-release/scripts/bump.mjs +36 -6
- package/skills/package-release/scripts/dispatch-publish.mjs +8 -0
- package/skills/package-release/scripts/gate-roster.mjs +4 -4
- package/skills/package-release/scripts/release-pack.mjs +485 -68
- package/skills/package-release/scripts/stale-copy-warning.mjs +215 -0
- package/skills/package-release/scripts/tag-lockstep.mjs +8 -0
- package/skills/primitive-authoring/references/INDEX.md +1 -0
- package/skills/primitive-authoring/references/examples-structure.md +52 -0
- package/skills/primitive-authoring/references/shell-patterns.md +12 -11
- package/skills/primitive-authoring/references/token-contract.md +2 -2
- package/skills/primitive-authoring/references/yaml-contract.md +106 -9
|
@@ -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]**
|
|
452
|
-
`admin-page-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
|
@@ -102,7 +102,7 @@ tag other than its own name needs the same tag-independent scope check.
|
|
|
102
102
|
|
|
103
103
|
- `chart.css` data slots reference `--a-data-0..9` (tokens, not raw)
|
|
104
104
|
- `card.css` mask uses `--a-chrome-light` (mask is opacity-only; value is semantic)
|
|
105
|
-
- Drawer/modal scrims use `--a-chrome-backdrop`
|
|
105
|
+
- Drawer/modal scrims use `--a-scrim-dialog` (aliases.css, gh#373: routes to `--a-chrome-scrim-dialog`, 80% black, not `--a-chrome-backdrop`)
|
|
106
106
|
|
|
107
107
|
If you find a raw value elsewhere, either:
|
|
108
108
|
|
|
@@ -118,7 +118,7 @@ Added in v0.5.0 — use these for UI chrome:
|
|
|
118
118
|
- `--a-chrome-border` — subtle hairline borders
|
|
119
119
|
- `--a-chrome-ring-subtle` — focus rings, outlines
|
|
120
120
|
- `--a-chrome-shadow-soft` — elevation shadows
|
|
121
|
-
- `--a-chrome-backdrop`
|
|
121
|
+
- `--a-chrome-backdrop` - generic 50% chrome overlay, reserved for non-dialog UI chrome; no current consumers. Modal/drawer backdrops use `--a-scrim-dialog` instead (80% black, gh#373, see `--a-chrome-scrim-dialog` in features.css' CHROME block)
|
|
122
122
|
|
|
123
123
|
## Data palette
|
|
124
124
|
|
|
@@ -89,13 +89,26 @@ check) is unaffected either way — it only checks for a slot literally named
|
|
|
89
89
|
## `a2ui.allowedParents:` / `a2ui.allowedChildren:` — composition constraints (SPEC REQ-011, gh#1353)
|
|
90
90
|
|
|
91
91
|
Optional keys inside the `a2ui:` block, alongside `rules:`. Each is a
|
|
92
|
-
non-empty list of catalog `component:` names (NOT tags) naming the
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
non-empty list of catalog `component:` names (NOT tags) naming the parents
|
|
93
|
+
this component may sit under / the direct children it may contain. The
|
|
94
|
+
reserved name `Surface` (the A2UI v1.0 implicit root container) is legal
|
|
95
95
|
only in `allowedParents` and means "may sit at the surface root". **Omitted
|
|
96
96
|
means unconstrained** — never write an empty list (that would mean "allowed
|
|
97
97
|
nowhere"; the build refuses it).
|
|
98
98
|
|
|
99
|
+
`allowedParents` matches the nearest custom-element ancestor, not the
|
|
100
|
+
immediate DOM parent (gh#3310): the generated lint rule's
|
|
101
|
+
`compositionFindings()` walks up past native (non-hyphenated) wrapper
|
|
102
|
+
elements — `section`, `div`, `td`, `tr`, `tbody`, and any other plain HTML
|
|
103
|
+
tag — until it finds a real catalog component tag or reaches the surface
|
|
104
|
+
root. This mirrors Light DOM's own composition reality (AGENTS.md: CSS
|
|
105
|
+
positions by tag + ancestor + DOM order) — a wrapper interposed for layout
|
|
106
|
+
or semantics (a `<section>` inside a card, a `<td>` in a table body) doesn't
|
|
107
|
+
change a component's logical host. `allowedChildren`, by contrast, still
|
|
108
|
+
matches DIRECT children only — a named-slot child that should be exempt
|
|
109
|
+
from the default-slot list is a separate, open gap (gh#3308), not addressed
|
|
110
|
+
by this semantics change.
|
|
111
|
+
|
|
99
112
|
```yaml
|
|
100
113
|
a2ui:
|
|
101
114
|
allowedParents:
|
|
@@ -112,6 +125,35 @@ children). A parent that adopts items through wrappers (menu.class.js's
|
|
|
112
125
|
deliberate descendant query) must NOT constrain — a declared constraint
|
|
113
126
|
stricter than the source is a defect, not documentation.
|
|
114
127
|
|
|
128
|
+
**`allowedChildren` is a default-slot-only check (gh#3308).** A direct
|
|
129
|
+
child carrying ANY `slot=` attribute is a sibling-level named slot, not a
|
|
130
|
+
default-slot member — the lint-side matcher (`compositionFindings`)
|
|
131
|
+
exempts it from `allowedChildren` entirely, regardless of the slot's name.
|
|
132
|
+
This is why menu-ui can declare `allowedChildren: [MenuItem, MenuDivider,
|
|
133
|
+
MenuLabel]` for its default slot while still accepting an arbitrary
|
|
134
|
+
focusable element on `slot="trigger"` ("typically button-ui, but any
|
|
135
|
+
focusable element works") without a false positive. When a named slot
|
|
136
|
+
*should* be constrained too (rare — most named slots exist precisely
|
|
137
|
+
because their content varies), add an `allowedChildrenBySlot:` map
|
|
138
|
+
alongside `allowedChildren:`, keyed by slot name, same catalog-name-list
|
|
139
|
+
shape:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
a2ui:
|
|
143
|
+
allowedChildren:
|
|
144
|
+
- MenuItem
|
|
145
|
+
- MenuDivider
|
|
146
|
+
- MenuLabel
|
|
147
|
+
allowedChildrenBySlot:
|
|
148
|
+
trigger: # only if the trigger slot ITSELF needs constraining
|
|
149
|
+
- ButtonUI
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A slot with no entry in `allowedChildrenBySlot` (or the key omitted
|
|
153
|
+
entirely) stays unconstrained by design — this is the common case, and
|
|
154
|
+
matches "omitted means unconstrained" for `allowedChildren`/`allowedParents`
|
|
155
|
+
above.
|
|
156
|
+
|
|
115
157
|
Pipeline: `components.mjs` validates the shape per-yaml, cross-checks every
|
|
116
158
|
referenced name against the full catalog on a full build, and forwards the
|
|
117
159
|
lists onto `x-adiaui` → `catalog-a2ui_1_0.json`.
|
|
@@ -122,16 +164,71 @@ per gh#2116) across the five opt-out-scoped catalogs (`adia.core.json`,
|
|
|
122
164
|
`adia.shells.json`, gh#2211/ADR-0093), where the vendored `@genui/core`
|
|
123
165
|
validator enforces them (`UNALLOWED_PARENT`/`UNALLOWED_CHILD`). Module-tier
|
|
124
166
|
yamls (web-modules) now carry a v1.0 sidecar too and land in `adia.shells`.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
`allowedChildrenBySlot` (gh#3308) is NOT part of this catalog/sidecar
|
|
168
|
+
pipeline — `components.mjs` never reads it, it never lands in
|
|
169
|
+
`x-adiaui`/`catalog-a2ui_1_0.json`, and the A2UI v1.0 protocol validator
|
|
170
|
+
never enforces it. It exists purely for the lint-side check below.
|
|
171
|
+
|
|
172
|
+
A non-empty `allowedParents`/`allowedChildren`/`allowedChildrenBySlot`
|
|
173
|
+
generates an enforced lint rule (LLD-0016 §C4, gh#2647):
|
|
174
|
+
`scripts/build/gen-composition-rules.mjs` emits `scripts/lint/
|
|
130
175
|
rules/generated/composition/<name>.mjs`, which flags a markup file where
|
|
131
|
-
the tag nests under (or contains) a non-declared tag
|
|
176
|
+
the tag nests under (or contains) a non-declared tag — `allowedChildren`
|
|
177
|
+
checked against default-slot children only, `allowedChildrenBySlot`
|
|
178
|
+
against the matching named-slot children (see above). Advisory (`warn`)
|
|
132
179
|
until a corpus-wide rollout promotes it (`npm run build:composition-rules`
|
|
133
180
|
/ `check:composition-rules-fresh`).
|
|
134
181
|
|
|
182
|
+
### `a2ui.noCompositionConstraint:`: the audited "no contract needed" verdict (lld-0029 C2a, PR #3456)
|
|
183
|
+
|
|
184
|
+
The third disposition a yaml can carry. Where `allowedParents` /
|
|
185
|
+
`allowedChildren` declare a containment edge, `noCompositionConstraint`
|
|
186
|
+
records that the authoring-rule audit above was actually run (read
|
|
187
|
+
`<name>.class.js` for `querySelector` / `closest` / `this.children`
|
|
188
|
+
child-tag expectations, then test any parent/child hypothesis against real
|
|
189
|
+
markup in `apps/`, `site/`, `catalog/`, `packages/web-components/patterns/`)
|
|
190
|
+
and found no constraint worth declaring. Its whole purpose is to let a
|
|
191
|
+
reader, or a gate, tell "audited, categorically needs none" apart from
|
|
192
|
+
"never audited". Do not write it as a shortcut: if the audit turns up a
|
|
193
|
+
real constraint, the fix is a genuine `allowedParents` / `allowedChildren`
|
|
194
|
+
addition, not this marker.
|
|
195
|
+
|
|
196
|
+
Shape (`scripts/schemas/component.yaml.schema.json`, `a2ui.noCompositionConstraint`):
|
|
197
|
+
an object with two required keys and nothing else. `reason` is the verdict
|
|
198
|
+
quoted from the disposition table (schema `minLength: 8`); `ticket` is the
|
|
199
|
+
issue whose table recorded it (`^gh#[0-9]+$`). As with every other schema
|
|
200
|
+
constraint (ADR-0057), the shape is IDE-visible only: `compileComponent()`
|
|
201
|
+
never reads this key, so a malformed block does not stop the build.
|
|
202
|
+
|
|
203
|
+
Real example, `packages/web-components/components/badge/badge.yaml`:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
a2ui:
|
|
207
|
+
noCompositionConstraint:
|
|
208
|
+
reason: 'Referenced generically from a dozen+ unrelated components, not scoped to one parent; own rule says positioning, not ancestry.'
|
|
209
|
+
ticket: 'gh#3270'
|
|
210
|
+
rules:
|
|
211
|
+
- 'Use for small status/count labels attached to another element (notification counts, status pills, version tags).'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Relationship to the two lists. A yaml carries either the marker or a
|
|
215
|
+
containment list, never both. This is now enforced (gh#3497), not just
|
|
216
|
+
convention: `component.yaml.schema.json` declares the two mutually
|
|
217
|
+
exclusive via the `a2ui` object's own `not` clause, and
|
|
218
|
+
`compileCompositionConstraints()` in `scripts/build/components.mjs` throws
|
|
219
|
+
at build time, naming the file and the offending field, when a yaml
|
|
220
|
+
declares both. `npm run check:components-valid` (part of `npm run
|
|
221
|
+
build:components`) is where that throw surfaces.
|
|
222
|
+
|
|
223
|
+
Pipeline: none. The marker is a yaml-only annotation with no sidecar,
|
|
224
|
+
`x-adiaui`, catalog, or generated-lint-rule forwarding (lld-0029 C2a, by
|
|
225
|
+
design), so a PR that only adds it changes no derived artifact and commits
|
|
226
|
+
no regen. The planned `check:composition-coverage` gate (lld-0029 C3,
|
|
227
|
+
`scripts/verify/check-composition-coverage.mjs`, not landed as of
|
|
228
|
+
2026-09-04) fails any yaml whose `a2ui` block has none of `allowedParents`,
|
|
229
|
+
`allowedChildren`, or `noCompositionConstraint`, so the marker counts as
|
|
230
|
+
coverage exactly as either list does.
|
|
231
|
+
|
|
135
232
|
---
|
|
136
233
|
|
|
137
234
|
## `examples:` field — a2ui example ids (semantic-id grammar, gh#2492)
|