@adia-ai/adia-ui-forge 0.8.31 → 0.8.33
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 +22 -0
- package/package.json +1 -1
- package/skills/adia-author/SKILL.md +6 -8
- package/skills/adia-author/references/INDEX.md +41 -0
- package/skills/adia-author/references/anti-patterns.md +24 -0
- package/skills/adia-author/references/api-contract.md +15 -0
- package/skills/adia-author/references/css-patterns.md +19 -0
- package/skills/adia-author/references/lifecycle-patterns.md +20 -0
- package/skills/adia-author/references/token-contract.md +15 -1
- package/skills/adia-author/references/trait-pages.md +38 -0
- package/skills/adia-gen-review/references/corpus-html-patterns.md +1 -1
- package/skills/adia-release/SKILL.md +1 -1
- package/skills/adia-release/references/cut-procedure.md +41 -17
- package/skills/adia-release/references/recovery-paths.md +1 -1
- package/skills/adia-release/scripts/dispatch-publish.mjs +217 -54
- package/skills/adia-release/scripts/release-pack.mjs +365 -20
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Part of the adia-release skill. Mechanizes the dispatch loop
|
|
18
18
|
// + handles the npm-latest ordering rule for batch pushes.
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { execFileSync } from 'node:child_process';
|
|
21
21
|
import process from 'node:process';
|
|
22
22
|
import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
|
|
23
23
|
import { PACKAGE_NAMES } from './package-paths.mjs';
|
|
@@ -49,10 +49,14 @@ function parseArgs(argv) {
|
|
|
49
49
|
console.log('--after Verify npm latest is at the given version before dispatching this one.');
|
|
50
50
|
console.log(' Used in batch push to ensure publish ordering.');
|
|
51
51
|
console.log('--verify-triggered Instead of dispatching, check whether each per-package publish-<pkg>.yml');
|
|
52
|
-
console.log('
|
|
53
|
-
console.log('
|
|
54
|
-
console.log('
|
|
55
|
-
console.log('
|
|
52
|
+
console.log(' has a SUCCESSFUL run for the <pkg>-vX.Y.Z tag, and re-dispatch the misses.');
|
|
53
|
+
console.log(' Conclusion-aware (gh#763): a cancelled/failed/timed-out run is a miss, not');
|
|
54
|
+
console.log(' a pass; in-progress/queued runs get a bounded wait. Every re-dispatch is');
|
|
55
|
+
console.log(' gated behind an npm-registry check — if <pkg>@<version> is already on the');
|
|
56
|
+
console.log(' registry the publish succeeded regardless of run conclusion (a run can');
|
|
57
|
+
console.log(' publish, then be stamped cancelled by the job clock), and re-dispatching');
|
|
58
|
+
console.log(' would only 403. Covers the batch-tag-push skip (recovery-paths.md');
|
|
59
|
+
console.log(' §Scenario 7) AND dead runs from runner starvation. Idempotent.');
|
|
56
60
|
console.log(`--scope npm scope the packages publish under (default: ${DEFAULT_SCOPE};`);
|
|
57
61
|
console.log(' or set $ADIA_NPM_SCOPE). The --after npm-latest check uses this scope.');
|
|
58
62
|
process.exit(0);
|
|
@@ -65,18 +69,23 @@ function parseArgs(argv) {
|
|
|
65
69
|
return args;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
// All gh/npm invocations go through execFileSync with ARGUMENT ARRAYS —
|
|
73
|
+
// never a shell string. --version/--scope are CLI-attacker-controlled;
|
|
74
|
+
// interpolating them into a shell command is command injection (PR #774
|
|
75
|
+
// review). `run` logs a printable form in dry mode and execs the array.
|
|
76
|
+
function run(bin, argv, dry, opts = {}) {
|
|
77
|
+
const printable = `${bin} ${argv.join(' ')}`;
|
|
69
78
|
if (dry) {
|
|
70
|
-
console.log(` [dry] ${
|
|
79
|
+
console.log(` [dry] ${printable}`);
|
|
71
80
|
return '';
|
|
72
81
|
}
|
|
73
|
-
return
|
|
82
|
+
return execFileSync(bin, argv, { encoding: 'utf8', ...opts });
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
function checkAfter(afterVersion, scope) {
|
|
77
86
|
console.log(`Checking npm latest is at ${afterVersion} before dispatching...`);
|
|
78
87
|
try {
|
|
79
|
-
const latest =
|
|
88
|
+
const latest = run('npm', ['view', `${scope}/web-components`, 'dist-tags.latest'], false).trim();
|
|
80
89
|
if (latest !== afterVersion) {
|
|
81
90
|
console.error(`error: npm ${scope}/web-components latest is '${latest}', expected '${afterVersion}'`);
|
|
82
91
|
console.error(' This means the previous batch hasn\'t completed publishing.');
|
|
@@ -92,9 +101,8 @@ function checkAfter(afterVersion, scope) {
|
|
|
92
101
|
|
|
93
102
|
function dispatch(pkg, version, dry) {
|
|
94
103
|
const ref = `${pkg}-v${version}`;
|
|
95
|
-
const cmd = `gh workflow run "publish-${pkg}.yml" --ref "${ref}"`;
|
|
96
104
|
try {
|
|
97
|
-
const out = run(
|
|
105
|
+
const out = run('gh', ['workflow', 'run', `publish-${pkg}.yml`, '--ref', ref], dry);
|
|
98
106
|
if (!dry && out) {
|
|
99
107
|
const url = out.split('\n').find((l) => l.startsWith('http')) || '';
|
|
100
108
|
console.log(` ✓ dispatched ${pkg} (${url.trim()})`);
|
|
@@ -114,53 +122,163 @@ async function sleep(ms) {
|
|
|
114
122
|
return new Promise((res) => setTimeout(res, ms));
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
//
|
|
125
|
+
// Classify a run's {status, conclusion} into what --verify-triggered should do
|
|
126
|
+
// with it. Pure — selftest exercises it with negative controls.
|
|
127
|
+
// 'success' — completed + success: the publish run is satisfied.
|
|
128
|
+
// 'pending' — queued / in_progress / waiting / requested: recheck (bounded wait).
|
|
129
|
+
// 'dead' — completed but NOT success (cancelled / failure / timed_out /
|
|
130
|
+
// skipped / anything unknown): re-dispatch candidate. gh#763: the
|
|
131
|
+
// v0.8.29 starvation wave stamped runs `cancelled` — one of them
|
|
132
|
+
// had already published to npm, which is why every re-dispatch is
|
|
133
|
+
// additionally gated behind the registry check below.
|
|
134
|
+
function classifyRun(status, conclusion) {
|
|
135
|
+
if (status !== 'completed') return 'pending';
|
|
136
|
+
return conclusion === 'success' ? 'success' : 'dead';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Latest run of publish-<pkg>.yml for the <pkg>-vX.Y.Z tag. Tag-triggered runs
|
|
118
140
|
// report the tag in the `headBranch` field, so we filter the run list by it.
|
|
119
|
-
// Returns
|
|
141
|
+
// Returns 'none' | 'success' | 'pending' | 'dead' | 'unknown' — existence
|
|
142
|
+
// alone is NOT satisfaction (gh#763: a cancelled run used to count as
|
|
143
|
+
// "triggered"), and a FAILED QUERY is NOT verified absence (PR #774 review:
|
|
144
|
+
// fail-open here read a gh rate limit as "no run" and re-dispatched over a
|
|
145
|
+
// possibly in-flight publish).
|
|
120
146
|
function triggeredForTag(pkg, version, dry) {
|
|
121
147
|
const tag = `${pkg}-v${version}`;
|
|
122
|
-
const
|
|
148
|
+
const argv = ['run', 'list', `--workflow=publish-${pkg}.yml`, '--branch', tag, '--limit', '1', '--json', 'status,conclusion', '-q', '.[0] // empty'];
|
|
123
149
|
if (dry) {
|
|
124
|
-
console.log(` [dry] ${
|
|
125
|
-
return
|
|
150
|
+
console.log(` [dry] gh ${argv.join(' ')}`);
|
|
151
|
+
return 'none'; // dry preview assumes not-triggered so the re-dispatch path is shown
|
|
126
152
|
}
|
|
127
153
|
try {
|
|
128
|
-
const out =
|
|
129
|
-
|
|
154
|
+
const out = execFileSync('gh', argv, { encoding: 'utf8' }).trim();
|
|
155
|
+
if (!out) return 'none'; // gh answered: genuinely zero runs for the tag
|
|
156
|
+
const run = JSON.parse(out);
|
|
157
|
+
return classifyRun(run.status, run.conclusion);
|
|
130
158
|
} catch {
|
|
131
|
-
// gh error (auth / rate limit / missing workflow) —
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
return false;
|
|
159
|
+
// gh error (auth / rate limit / missing workflow) — we could not observe
|
|
160
|
+
// the runs. Fail CLOSED: 'unknown' stops the loop with a nonzero result
|
|
161
|
+
// instead of dispatching blind.
|
|
162
|
+
return 'unknown';
|
|
136
163
|
}
|
|
137
164
|
}
|
|
138
165
|
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
166
|
+
// Registry truth: is <scope>/<pkg>@<version> already published? A workflow
|
|
167
|
+
// run's conclusion is NOT publish truth — the v0.8.29 compose run published
|
|
168
|
+
// successfully, then got stamped `cancelled` by the 10m job clock. If the
|
|
169
|
+
// version is on the registry, the publish succeeded, full stop; re-dispatching
|
|
170
|
+
// would only hit npm's publish-over-published 403.
|
|
171
|
+
// Returns true (published) | false (npm answered: version absent, E404) |
|
|
172
|
+
// null (query FAILED — network/auth; absence NOT verified, fail closed:
|
|
173
|
+
// PR #774 review — a transient npm failure must not re-open the 403).
|
|
174
|
+
function onRegistry(pkg, version, scope, dry) {
|
|
175
|
+
const argv = ['view', `${scope}/${pkg}@${version}`, 'version'];
|
|
176
|
+
if (dry) {
|
|
177
|
+
console.log(` [dry] npm ${argv.join(' ')}`);
|
|
178
|
+
return false; // dry preview assumes not-published so the re-dispatch path is shown
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
return execFileSync('npm', argv, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() === version;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
const stderr = `${e.stderr || ''}`;
|
|
184
|
+
if (stderr.includes('E404') || stderr.includes('404 Not Found')) return false; // definitive: not published
|
|
185
|
+
return null; // npm/network error — absence unverified
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// What to do with a package whose latest run is not a clean success, given
|
|
190
|
+
// registry truth (`published`: true | false | null-unverified). Pure —
|
|
191
|
+
// selftest proves the 403-avoidance and fail-closed invariants here.
|
|
192
|
+
// published === true → 'satisfied' — npm has the version; the publish
|
|
193
|
+
// succeeded whatever the run conclusion says. NEVER
|
|
194
|
+
// re-dispatch over an already-published version (403).
|
|
195
|
+
// either query failed→ 'unknown' — run state or registry state was NOT
|
|
196
|
+
// observed (rate limit, network). Fail closed: stop
|
|
197
|
+
// with a nonzero result, never dispatch blind.
|
|
198
|
+
// pending, absent → 'wait' — a run may still be publishing; do not
|
|
199
|
+
// race it with a second dispatch.
|
|
200
|
+
// dead/none, absent → 'redispatch' — no run, or a cancelled/failed/
|
|
201
|
+
// timed-out one, and the registry lacks the version.
|
|
202
|
+
function decidePackage(state, published) {
|
|
203
|
+
if (published === true) return 'satisfied';
|
|
204
|
+
if (state === 'unknown' || published === null) return 'unknown';
|
|
205
|
+
if (state === 'pending') return 'wait';
|
|
206
|
+
return 'redispatch';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Bounded wait for in-progress/queued runs before judging them: a healthy
|
|
210
|
+
// publish run finishes in ~30s; 6 × 20s covers a slow queue without stalling
|
|
211
|
+
// the recovery loop forever.
|
|
212
|
+
const PENDING_RECHECKS = 6;
|
|
213
|
+
const PENDING_RECHECK_SECONDS = 20;
|
|
214
|
+
|
|
215
|
+
// The publish-run recovery: verify each per-package publish workflow has a
|
|
216
|
+
// SUCCESSFUL run for its tag (batch-tag-push skip AND dead runs — cancelled /
|
|
217
|
+
// failed / timed_out, e.g. runner starvation), wait out in-progress runs, and
|
|
218
|
+
// re-dispatch only true misses — never over a version the registry already has.
|
|
219
|
+
// Idempotent — re-dispatches nothing if all publishes landed.
|
|
220
|
+
async function verifyTriggeredAndRedispatch(version, scope, dry) {
|
|
221
|
+
console.log(`\nVerifying publish-on-tag succeeded for all ${PACKAGES.length} packages (v${version}):`);
|
|
222
|
+
let pending = PACKAGES;
|
|
223
|
+
const unresolved = new Map(); // pkg → state, after the bounded wait
|
|
224
|
+
for (let attempt = 0; attempt <= PENDING_RECHECKS && pending.length > 0; attempt++) {
|
|
225
|
+
if (attempt > 0) {
|
|
226
|
+
console.log(` … ${pending.length} run(s) still pending — recheck ${attempt}/${PENDING_RECHECKS} in ${PENDING_RECHECK_SECONDS}s`);
|
|
227
|
+
await sleep(PENDING_RECHECK_SECONDS * 1000);
|
|
228
|
+
}
|
|
229
|
+
const stillPending = [];
|
|
230
|
+
for (const pkg of pending) {
|
|
231
|
+
const state = triggeredForTag(pkg, version, dry);
|
|
232
|
+
if (state === 'success') {
|
|
233
|
+
console.log(` ✓ ${pkg} — successful publish run for ${pkg}-v${version}`);
|
|
234
|
+
} else if (state === 'pending' && attempt < PENDING_RECHECKS && !dry) {
|
|
235
|
+
stillPending.push(pkg);
|
|
236
|
+
} else {
|
|
237
|
+
unresolved.set(pkg, state);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
pending = stillPending;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Registry gate: for every non-success, npm is the arbiter — already
|
|
244
|
+
// published → satisfied regardless of run conclusion; absent → re-dispatch.
|
|
144
245
|
const misses = [];
|
|
145
|
-
for (const pkg of
|
|
146
|
-
|
|
147
|
-
|
|
246
|
+
for (const [pkg, state] of unresolved) {
|
|
247
|
+
const decision = decidePackage(state, onRegistry(pkg, version, scope, dry));
|
|
248
|
+
if (decision === 'satisfied') {
|
|
249
|
+
console.log(` ✓ ${pkg} — run state '${state}' but ${scope}/${pkg}@${version} IS on the registry → published, no re-dispatch`);
|
|
250
|
+
} else if (decision === 'unknown') {
|
|
251
|
+
console.log(` ! ${pkg} — run or registry state could NOT be observed (gh/npm query failed) → NOT dispatching blind; fix gh/npm access and re-run --verify-triggered`);
|
|
252
|
+
misses.push({ pkg, redispatch: false });
|
|
253
|
+
} else if (decision === 'wait') {
|
|
254
|
+
console.log(` ? ${pkg} — run still pending after bounded wait and not on the registry → NOT re-dispatching over an in-flight run; re-run --verify-triggered once it settles`);
|
|
255
|
+
misses.push({ pkg, redispatch: false });
|
|
148
256
|
} else {
|
|
149
|
-
|
|
150
|
-
|
|
257
|
+
const why = state === 'none' ? 'NO publish run (batch-push skip)' : 'run cancelled/failed/timed-out';
|
|
258
|
+
console.log(` ✗ ${pkg} — ${why} for ${pkg}-v${version}, not on the registry → will re-dispatch`);
|
|
259
|
+
misses.push({ pkg, redispatch: true });
|
|
151
260
|
}
|
|
152
261
|
}
|
|
153
262
|
if (misses.length === 0) {
|
|
154
|
-
console.log(`\n[verify-triggered] all ${PACKAGES.length}
|
|
263
|
+
console.log(`\n[verify-triggered] all ${PACKAGES.length} publishes verified (successful run or on-registry) — nothing to re-dispatch.`);
|
|
155
264
|
return 0;
|
|
156
265
|
}
|
|
157
|
-
|
|
266
|
+
const toDispatch = misses.filter((m) => m.redispatch);
|
|
158
267
|
let redispatched = 0;
|
|
159
|
-
|
|
160
|
-
|
|
268
|
+
if (toDispatch.length > 0) {
|
|
269
|
+
console.log(`\n[verify-triggered] ${toDispatch.length} miss(es) — re-dispatching via workflow_dispatch:`);
|
|
270
|
+
for (const { pkg } of toDispatch) {
|
|
271
|
+
if (dispatch(pkg, version, dry)) redispatched++;
|
|
272
|
+
}
|
|
273
|
+
console.log(`\n[verify-triggered] re-dispatched ${redispatched}/${toDispatch.length} ${dry ? '(dry)' : ''}`);
|
|
274
|
+
if (!dry) {
|
|
275
|
+
console.log(' Registry is the final arbiter — confirm each with:');
|
|
276
|
+
console.log(` npm view ${scope}/<pkg>@${version} version`);
|
|
277
|
+
}
|
|
161
278
|
}
|
|
162
|
-
|
|
163
|
-
|
|
279
|
+
const unresolvedCount = misses.length - toDispatch.length;
|
|
280
|
+
if (unresolvedCount > 0) console.log(`\n[verify-triggered] ${unresolvedCount} package(s) unresolved (run still pending, or gh/npm query failed) — re-run --verify-triggered once they settle / access is restored.`);
|
|
281
|
+
return redispatched === toDispatch.length && unresolvedCount === 0 ? 0 : 1;
|
|
164
282
|
}
|
|
165
283
|
|
|
166
284
|
async function main() {
|
|
@@ -169,10 +287,11 @@ async function main() {
|
|
|
169
287
|
assertMonorepoRoot(process.cwd());
|
|
170
288
|
if (args.after) checkAfter(args.after, args.scope);
|
|
171
289
|
|
|
172
|
-
// Recovery mode — verify each publish workflow
|
|
173
|
-
// re-dispatch only
|
|
290
|
+
// Recovery mode — verify each publish workflow SUCCEEDED for its pushed tag
|
|
291
|
+
// (or the version is on the registry), re-dispatch only true misses (batch-
|
|
292
|
+
// tag-push skip + dead runs, gh#763). Skips the blind dispatch.
|
|
174
293
|
if (args.verifyTriggered) {
|
|
175
|
-
const rc = verifyTriggeredAndRedispatch(args.version, args.dry);
|
|
294
|
+
const rc = await verifyTriggeredAndRedispatch(args.version, args.scope, args.dry);
|
|
176
295
|
process.exit(rc);
|
|
177
296
|
}
|
|
178
297
|
|
|
@@ -211,31 +330,75 @@ async function main() {
|
|
|
211
330
|
if (succeeded !== PACKAGES.length) process.exit(1);
|
|
212
331
|
}
|
|
213
332
|
|
|
214
|
-
// dry mode never shells out (run()/dispatch()
|
|
215
|
-
//
|
|
216
|
-
|
|
333
|
+
// dry mode never shells out (run()/dispatch()/triggeredForTag()/onRegistry()
|
|
334
|
+
// short-circuit before execFileSync), so this exercises the real functions
|
|
335
|
+
// without touching gh/npm.
|
|
336
|
+
async function selftest() {
|
|
337
|
+
const fail = (msg) => { console.error(`selftest FAIL: ${msg}`); process.exit(1); };
|
|
338
|
+
|
|
217
339
|
let allOk = true;
|
|
218
340
|
for (const pkg of PACKAGES) {
|
|
219
341
|
if (!dispatch(pkg, '9.9.9', true /* dry */)) allOk = false;
|
|
220
342
|
}
|
|
221
|
-
if (!allOk)
|
|
343
|
+
if (!allOk) fail('dry dispatch() reported a failure for at least one package');
|
|
344
|
+
|
|
345
|
+
// classifyRun() — the gh#763 core: conclusion decides, existence does not.
|
|
346
|
+
if (classifyRun('completed', 'success') !== 'success') fail("classifyRun(completed,success) must be 'success'");
|
|
347
|
+
// Negative controls: the exact states the v0.8.29 starvation wave produced
|
|
348
|
+
// must NOT satisfy the verify — a cancelled run counting as "triggered" is
|
|
349
|
+
// the defect this script no longer has.
|
|
350
|
+
for (const dead of ['cancelled', 'failure', 'timed_out', 'skipped', null]) {
|
|
351
|
+
if (classifyRun('completed', dead) !== 'dead') fail(`classifyRun(completed,${dead}) must be 'dead', never satisfied`);
|
|
352
|
+
}
|
|
353
|
+
for (const status of ['queued', 'in_progress', 'waiting', 'requested']) {
|
|
354
|
+
if (classifyRun(status, null) !== 'pending') fail(`classifyRun(${status}) must be 'pending' (bounded recheck)`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// decidePackage() — the registry gate. Negative controls: the v0.8.29
|
|
358
|
+
// a2ui-compose shape (run cancelled, version PUBLISHED) must never
|
|
359
|
+
// re-dispatch — that's the publish-over-published 403.
|
|
360
|
+
if (decidePackage('dead', true) !== 'satisfied') fail('decidePackage(dead, published) must be satisfied — never re-dispatch over a published version');
|
|
361
|
+
if (decidePackage('none', true) !== 'satisfied') fail('decidePackage(none, published) must be satisfied');
|
|
362
|
+
if (decidePackage('pending', true) !== 'satisfied') fail('decidePackage(pending, published) must be satisfied');
|
|
363
|
+
if (decidePackage('dead', false) !== 'redispatch') fail('decidePackage(dead, absent) must re-dispatch');
|
|
364
|
+
if (decidePackage('none', false) !== 'redispatch') fail('decidePackage(none, absent) must re-dispatch');
|
|
365
|
+
if (decidePackage('pending', false) !== 'wait') fail('decidePackage(pending, absent) must wait, never race an in-flight run');
|
|
366
|
+
// Fail-closed negative controls (PR #774 review): a FAILED query is not
|
|
367
|
+
// verified absence — 'unknown' must never dispatch. A gh rate limit could
|
|
368
|
+
// hide an in-flight run; a transient npm failure could hide an
|
|
369
|
+
// already-published version (the 403 again).
|
|
370
|
+
for (const state of ['unknown', 'none', 'dead', 'pending']) {
|
|
371
|
+
if (decidePackage(state, null) !== 'unknown') fail(`decidePackage(${state}, registry-unverified) must be 'unknown' — never dispatch blind`);
|
|
372
|
+
}
|
|
373
|
+
for (const published of [false, null]) {
|
|
374
|
+
if (decidePackage('unknown', published) !== 'unknown') fail(`decidePackage(run-unknown, ${published}) must be 'unknown' — never dispatch blind`);
|
|
375
|
+
}
|
|
376
|
+
// Positive registry truth still wins even when the run query failed:
|
|
377
|
+
if (decidePackage('unknown', true) !== 'satisfied') fail('decidePackage(unknown, published) must be satisfied — registry is the arbiter');
|
|
378
|
+
|
|
379
|
+
// triggeredForTag() in dry mode must conservatively report 'none' — the
|
|
380
|
+
// whole point is re-dispatching misses; silently assuming "triggered"
|
|
381
|
+
// would let a real miss through.
|
|
382
|
+
if (triggeredForTag('web-components', '9.9.9', true) !== 'none') {
|
|
383
|
+
fail("dry triggeredForTag() must report 'none' (conservative default)");
|
|
384
|
+
}
|
|
222
385
|
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
if (
|
|
227
|
-
|
|
386
|
+
// onRegistry() in dry mode must report not-published — assuming published
|
|
387
|
+
// would suppress the re-dispatch preview AND, inverted, the real registry
|
|
388
|
+
// gate is what prevents the publish-over-published 403.
|
|
389
|
+
if (onRegistry('web-components', '9.9.9', '@adia-ai', true) !== false) {
|
|
390
|
+
fail('dry onRegistry() must report false (conservative default)');
|
|
228
391
|
}
|
|
229
392
|
|
|
230
|
-
const rc = verifyTriggeredAndRedispatch('9.9.9', true);
|
|
231
|
-
if (rc !== 0)
|
|
393
|
+
const rc = await verifyTriggeredAndRedispatch('9.9.9', '@adia-ai', true);
|
|
394
|
+
if (rc !== 0) fail(`dry verifyTriggeredAndRedispatch() should report rc=0, got ${rc}`);
|
|
232
395
|
|
|
233
396
|
console.log('selftest OK');
|
|
234
397
|
}
|
|
235
398
|
|
|
236
399
|
const topArgv = process.argv.slice(2);
|
|
237
400
|
if (topArgv[0] === 'selftest') {
|
|
238
|
-
selftest();
|
|
401
|
+
selftest().catch((e) => { console.error(`selftest FAIL: ${e.message}`); process.exit(1); });
|
|
239
402
|
} else {
|
|
240
403
|
main().catch((e) => {
|
|
241
404
|
console.error(`error: ${e.message}`);
|