@adia-ai/adia-ui-forge 0.8.4 → 0.8.6
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 +27 -2
- package/README.md +13 -4
- package/agents/a2ui-builder.corpus.json +35 -0
- package/agents/a2ui-builder.md +37 -0
- package/agents/component-author.corpus.json +40 -0
- package/agents/component-author.md +10 -1
- package/agents/framework-reviewer.corpus.json +35 -0
- package/agents/framework-reviewer.md +42 -0
- package/agents/release-builder.corpus.json +39 -0
- package/agents/release-builder.md +39 -0
- package/agents/routing-corpus.json +38 -38
- package/bin/release-pretag-docs-gate +226 -0
- package/commands/release.md +5 -3
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
- package/skills/adia-a2ui/SKILL.md +37 -8
- package/skills/adia-a2ui/evals/routing-corpus.json +36 -29
- package/skills/adia-a2ui/references/pipeline-overview.md +1 -1
- package/skills/adia-a2ui/references/zettel-calibration.md +5 -5
- package/skills/adia-author/SKILL.md +16 -8
- package/skills/adia-author/evals/routing-corpus.json +39 -218
- package/skills/adia-author/references/anti-patterns.md +7 -7
- package/skills/adia-author/references/api-contract.md +9 -9
- package/skills/adia-author/references/authoring-cycle.md +25 -6
- package/skills/adia-author/references/code-style.md +4 -5
- package/skills/adia-author/references/lifecycle-patterns.md +8 -8
- package/skills/adia-author/references/llm-bridge.md +7 -0
- package/skills/adia-author/references/token-contract.md +5 -5
- package/skills/adia-author/references/worked-example.md +34 -18
- package/skills/adia-author/references/yaml-contract.md +1 -1
- package/skills/adia-deploy/SKILL.md +71 -12
- package/skills/adia-deploy/evals/routing-corpus.json +37 -0
- package/skills/adia-deploy/references/deploy-playbooks.md +1 -1
- package/skills/adia-dogfood/SKILL.md +71 -11
- package/skills/adia-dogfood/evals/routing-corpus.json +34 -0
- package/skills/adia-dogfood/references/admin-shell-anatomy.md +1 -1
- package/skills/adia-gen-review/SKILL.md +73 -7
- package/skills/adia-gen-review/evals/routing-corpus.json +34 -0
- package/skills/adia-gen-review/scripts/gen-review-decompose.mjs +5 -0
- package/skills/adia-llm-internals/SKILL.md +10 -6
- package/skills/adia-llm-internals/evals/routing-corpus.json +37 -0
- package/skills/adia-llm-internals/references/bridge-facade.md +2 -2
- package/skills/adia-llm-internals/references/streaming-sse.md +1 -1
- package/skills/adia-release/SKILL.md +43 -13
- package/skills/adia-release/evals/evals.json +24 -0
- package/skills/adia-release/evals/routing-corpus.json +35 -0
- package/skills/adia-release/references/changelog-discipline.md +15 -9
- package/skills/adia-release/references/cut-procedure.md +78 -24
- package/skills/adia-release/references/gates-catalog.md +3 -3
- package/skills/adia-release/references/independent-package-release.md +1 -1
- package/skills/adia-release/references/recovery-paths.md +4 -2
- package/skills/adia-release/scripts/bump.mjs +32 -1
- package/skills/adia-release/scripts/dispatch-publish.mjs +49 -11
- package/skills/adia-release/scripts/gate-roster.mjs +71 -0
- package/skills/adia-release/scripts/insert-stub.mjs +36 -2
- package/skills/adia-release/scripts/promote-unreleased.mjs +35 -1
- package/skills/adia-release/scripts/release-pack.mjs +315 -56
- package/skills/adia-release/scripts/tag-lockstep.mjs +29 -7
- package/skills/adia-site-docs/SKILL.md +6 -1
- package/skills/adia-site-docs/evals/routing-corpus.json +35 -173
- package/skills/adia-ssr/SKILL.md +128 -0
- package/skills/adia-ssr/evals/audit-report.md +63 -0
- package/skills/adia-ssr/evals/routing-corpus.json +34 -0
- package/skills/adia-ssr/references/consumer-workarounds.md +81 -0
- package/skills/adia-ssr/references/failure-shapes.md +164 -0
- package/skills/adia-ssr/references/guard-patterns.md +183 -0
- package/skills/adia-ssr/references/status-ledger.md +33 -0
- package/skills/adia-ssr/references/test-without-linkedom.md +129 -0
- package/agents/a2ui-engineer.md +0 -26
- package/agents/framework-verifier.md +0 -26
- package/agents/release-engineer.md +0 -26
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
// Chains the 5 CLI helpers (bump · promote-unreleased · insert-stub ·
|
|
5
5
|
// tag-lockstep · dispatch-publish) plus the non-helper steps (lockfile ·
|
|
6
6
|
// pre-flight gates · git commit · F-N1 · push · GH releases · site
|
|
7
|
-
// deploy) into one walked sequence with
|
|
8
|
-
// tag, before push, before publish
|
|
7
|
+
// deploy) into one walked sequence with 4 confirmation boundaries (before
|
|
8
|
+
// tag, before push, before publish, before deploy dispatch). Under the
|
|
9
|
+
// single-authorization model (SKILL.md §Authorization, operator ruling
|
|
10
|
+
// 2026-07-17) `--go` auto-confirms all of them — the evidence blocks still
|
|
11
|
+
// print as a log; the granular flags below remain for cautious manual runs.
|
|
9
12
|
//
|
|
10
13
|
// MODES (two-phase since 2026-07-16 — invariant 3: release commits land via
|
|
11
14
|
// PR, so the cut modes stop after the release commit; handoff runs the
|
|
@@ -13,7 +16,13 @@
|
|
|
13
16
|
// --mode cut (Mode 1 — peer pre-staged [Unreleased] or pure stub; STOPS at the release commit)
|
|
14
17
|
// --mode from-scratch (Mode 2 — promote [Unreleased] → [VERSION]; STOPS at the release commit)
|
|
15
18
|
// --mode handoff (Mode 3 — the second phase: tag at post-merge main → publish → deploy)
|
|
16
|
-
//
|
|
19
|
+
//
|
|
20
|
+
// `--mode batch` is NOT implemented here — a batch push tags each version at
|
|
21
|
+
// its OWN release-merge SHA and enforces oldest-first publish ordering, which
|
|
22
|
+
// this single-version orchestrator has no model for. Passing it hard-errors
|
|
23
|
+
// with a pointer to the manual procedure: references/recovery-paths.md
|
|
24
|
+
// §Scenario 2 (Batch push). Silently falling through to the single-version
|
|
25
|
+
// path was the H2 audit finding this replaced.
|
|
17
26
|
//
|
|
18
27
|
// REQUIRED:
|
|
19
28
|
// --version 0.6.22
|
|
@@ -25,26 +34,42 @@
|
|
|
25
34
|
// --gh-notes-file <path> : GH release notes body (one per cycle)
|
|
26
35
|
// --substantive "<one-line>" : for the stub block xref
|
|
27
36
|
// --xref "<changelog-anchor>" : for the stub block
|
|
28
|
-
// --substantive-packages a,b,c : packages with [Unreleased] to promote
|
|
37
|
+
// --substantive-packages a,b,c : packages with [Unreleased] to promote (cut AND from-scratch)
|
|
29
38
|
// --stub-packages a,b,c : packages getting the lockstep stub
|
|
30
39
|
//
|
|
31
40
|
// FLAGS:
|
|
41
|
+
// --go : single-authorization — the operator's cut-start go
|
|
42
|
+
// covers the whole cycle; equals --yes --push --publish.
|
|
43
|
+
// THE standard invocation for an operator-initiated
|
|
44
|
+
// release. An F-N1 warn still refuses auto-confirm at
|
|
45
|
+
// the push boundary (a gate failure, not ceremony).
|
|
32
46
|
// --dry : preview all commands, no mutation, no prompts
|
|
33
|
-
// --yes :
|
|
47
|
+
// --yes : auto-confirm the tag boundary ONLY. Tag creation is
|
|
48
|
+
// the least destructive of the mutating boundaries
|
|
49
|
+
// (moving/deleting a local-only tag is free); push and
|
|
50
|
+
// publish each need their own flag — --yes does not
|
|
51
|
+
// cover them (H5/defect-2: a single --yes used to
|
|
52
|
+
// flatten every boundary).
|
|
53
|
+
// --push : auto-confirm the push-tags boundary
|
|
54
|
+
// --publish : auto-confirm the publish + deploy-dispatch boundaries
|
|
34
55
|
// --skip-gates : (DEBUG ONLY) skip pre-flight (don't ship anything!)
|
|
35
56
|
// --skip-site : skip the EXE deploy step (rare; for offline cycles)
|
|
36
57
|
//
|
|
37
58
|
// Part of the adia-release skill. Mechanizes the cut procedure from
|
|
38
|
-
// references/cut-procedure.md
|
|
39
|
-
// the
|
|
59
|
+
// references/cut-procedure.md. Every confirmation boundary prints its evidence
|
|
60
|
+
// block BEFORE the prompt (H5) — never a bare y/N — whether it waits for input
|
|
61
|
+
// (manual flags) or auto-confirms (--go).
|
|
40
62
|
|
|
41
63
|
import { execSync } from 'node:child_process';
|
|
42
64
|
import { createInterface } from 'node:readline/promises';
|
|
43
65
|
import { stdin, stdout } from 'node:process';
|
|
44
66
|
import fs from 'node:fs';
|
|
67
|
+
import os from 'node:os';
|
|
45
68
|
import path from 'node:path';
|
|
46
69
|
import process from 'node:process';
|
|
47
70
|
import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
|
|
71
|
+
import { resolvePackageChangelog } from './package-paths.mjs';
|
|
72
|
+
import { GATE_ROSTER } from './gate-roster.mjs';
|
|
48
73
|
|
|
49
74
|
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
50
75
|
const REPO = process.cwd();
|
|
@@ -70,7 +95,7 @@ function parseArgs(argv) {
|
|
|
70
95
|
versionList: null,
|
|
71
96
|
host: process.env.ADIA_DEPLOY_HOST || DEFAULT_DEPLOY_HOST,
|
|
72
97
|
scope: process.env.ADIA_NPM_SCOPE || DEFAULT_NPM_SCOPE,
|
|
73
|
-
dry: false, yes: false, skipGates: false, skipSite: false,
|
|
98
|
+
dry: false, yes: false, push: false, publish: false, skipGates: false, skipSite: false,
|
|
74
99
|
};
|
|
75
100
|
for (let i = 0; i < argv.length; i++) {
|
|
76
101
|
const k = argv[i];
|
|
@@ -89,6 +114,16 @@ function parseArgs(argv) {
|
|
|
89
114
|
else if (k === '--scope') args.scope = argv[++i];
|
|
90
115
|
else if (k === '--dry') args.dry = true;
|
|
91
116
|
else if (k === '--yes') args.yes = true;
|
|
117
|
+
else if (k === '--push') args.push = true;
|
|
118
|
+
else if (k === '--publish') args.publish = true;
|
|
119
|
+
// --go: the single-authorization model (operator ruling 2026-07-17,
|
|
120
|
+
// replacing the 4-checkpoint ceremony after the v0.8.5 cut spent ~40
|
|
121
|
+
// minutes on approval relays that produced zero safety). The operator's
|
|
122
|
+
// cut-start instruction covers the WHOLE cycle; the run stops only on
|
|
123
|
+
// gate failures. Equivalent to --yes --push --publish; the F-N1
|
|
124
|
+
// error hard-stop and the warn-blocks-auto-confirm rule still apply —
|
|
125
|
+
// those are gates, not approvals, and no flag skips them.
|
|
126
|
+
else if (k === '--go') { args.yes = true; args.push = true; args.publish = true; }
|
|
92
127
|
else if (k === '--skip-gates') args.skipGates = true;
|
|
93
128
|
else if (k === '--skip-site') args.skipSite = true;
|
|
94
129
|
else if (k === '-h' || k === '--help') { help(); process.exit(0); }
|
|
@@ -98,8 +133,17 @@ function parseArgs(argv) {
|
|
|
98
133
|
console.error(' see --help');
|
|
99
134
|
process.exit(2);
|
|
100
135
|
}
|
|
101
|
-
if (
|
|
102
|
-
console.error(
|
|
136
|
+
if (args.mode === 'batch') {
|
|
137
|
+
console.error('error: --mode batch is not implemented — a batch push tags each version at');
|
|
138
|
+
console.error(' its OWN release-merge SHA and enforces oldest-first publish ordering,');
|
|
139
|
+
console.error(' which this single-version orchestrator has no model for. Silently');
|
|
140
|
+
console.error(' falling through to the single-version path was the H2 defect this');
|
|
141
|
+
console.error(' replaced. Follow the manual procedure instead:');
|
|
142
|
+
console.error(' references/recovery-paths.md §Scenario 2 — Batch push');
|
|
143
|
+
process.exit(2);
|
|
144
|
+
}
|
|
145
|
+
if (!['cut', 'from-scratch', 'handoff'].includes(args.mode)) {
|
|
146
|
+
console.error(`error: --mode must be cut|from-scratch|handoff (got: ${args.mode})`);
|
|
103
147
|
process.exit(2);
|
|
104
148
|
}
|
|
105
149
|
return args;
|
|
@@ -116,15 +160,31 @@ function help() {
|
|
|
116
160
|
--substantive-packages web-components,web-modules,a2ui/corpus \\
|
|
117
161
|
--stub-packages llm,a2ui/compose,a2ui/mcp,a2ui/retrieval,a2ui/runtime,a2ui/validator
|
|
118
162
|
|
|
119
|
-
Flags: --
|
|
163
|
+
Flags: --go (single-authorization: the operator's cut-start go covers the
|
|
164
|
+
whole cycle — auto-confirms ALL checkpoints; stops only on
|
|
165
|
+
gate failures. THE DEFAULT for an operator-initiated release.)
|
|
166
|
+
--dry (preview all) --yes (auto-confirm checkpoint 1 ONLY — tag)
|
|
167
|
+
--push (auto-confirm checkpoint 2 — push tags)
|
|
168
|
+
--publish (auto-confirm checkpoint 3 — dispatch publishes)
|
|
169
|
+
--skip-site (skip EXE deploy)
|
|
120
170
|
Instance: --host <deploy-host> (default ${DEFAULT_DEPLOY_HOST}; or $ADIA_DEPLOY_HOST)
|
|
121
171
|
--scope <@org> (npm scope; default ${DEFAULT_NPM_SCOPE}; or $ADIA_NPM_SCOPE)`);
|
|
122
172
|
}
|
|
123
173
|
|
|
124
174
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
// Every checkpoint prints its evidence block BEFORE the y/N prompt (H5) —
|
|
176
|
+
// `evidence` is a pre-formatted string (tag list, F-N1 results, registry
|
|
177
|
+
// snapshot); `autoConfirmed` is the SPECIFIC flag that covers THIS checkpoint
|
|
178
|
+
// (--yes for checkpoint 1 only; --push / --publish for 2 and 3 — H5/defect-2:
|
|
179
|
+
// a single --yes used to flatten all three).
|
|
180
|
+
async function checkpoint(label, evidence, autoConfirmed) {
|
|
181
|
+
if (evidence) {
|
|
182
|
+
console.log(`\n--- evidence for: ${label} ---`);
|
|
183
|
+
console.log(evidence);
|
|
184
|
+
console.log('---');
|
|
185
|
+
}
|
|
186
|
+
if (autoConfirmed) {
|
|
187
|
+
console.log(`\n[checkpoint] ${label} — auto-confirmed`);
|
|
128
188
|
return;
|
|
129
189
|
}
|
|
130
190
|
const answer = await rl.question(`\n[CHECKPOINT] ${label} — proceed? [y/N]: `);
|
|
@@ -151,6 +211,24 @@ function shQuiet(cmd, args) {
|
|
|
151
211
|
return execSync(cmd, { cwd: REPO, encoding: 'utf8' });
|
|
152
212
|
}
|
|
153
213
|
|
|
214
|
+
// Runs a command and CAPTURES its combined output instead of streaming it —
|
|
215
|
+
// used where the output itself becomes evidence in a later checkpoint's
|
|
216
|
+
// evidence block (H5), rather than just scrolling past in the terminal.
|
|
217
|
+
// Never throws: F-N1 legitimately exits non-zero on cosmetic warns; the
|
|
218
|
+
// caller decides what a non-zero code means.
|
|
219
|
+
function shCapture(cmd, args) {
|
|
220
|
+
if (args.dry) {
|
|
221
|
+
console.log(` [dry] ${cmd}`);
|
|
222
|
+
return { code: 0, output: '[dry] (not run)' };
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const output = execSync(cmd, { cwd: REPO, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
226
|
+
return { code: 0, output };
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return { code: e.status ?? 1, output: (e.stdout || '') + (e.stderr || '') };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
154
232
|
// ── Step 1 — Re-baseline ──────────────────────────────────────────
|
|
155
233
|
function step1ReBaseline(args) {
|
|
156
234
|
console.log('\n=== Step 1 — Re-baseline ===');
|
|
@@ -173,32 +251,40 @@ function step3PreFlight(args) {
|
|
|
173
251
|
console.log('\n=== Step 3 — Pre-flight (SKIPPED via --skip-gates; danger!) ===');
|
|
174
252
|
return;
|
|
175
253
|
}
|
|
176
|
-
console.log(
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
'npm run verify:traits',
|
|
180
|
-
'npm run check:lockstep',
|
|
181
|
-
'npm run test:unit',
|
|
182
|
-
'npm run typecheck',
|
|
183
|
-
'npm run check:demo-shells',
|
|
184
|
-
'npm run check:lightningcss-build',
|
|
185
|
-
'npm run smoke:engines',
|
|
186
|
-
'npm run smoke:register-engine',
|
|
187
|
-
'npm run check:links',
|
|
188
|
-
'npm run check:embeddings-fresh',
|
|
189
|
-
'npm run verify:corpus',
|
|
190
|
-
];
|
|
191
|
-
for (const g of gates) {
|
|
192
|
-
console.log(`\n ${g}`);
|
|
254
|
+
console.log(`\n=== Step 3 — Pre-flight gates (${GATE_ROSTER.length}-gate roster — gate-roster.mjs) ===`);
|
|
255
|
+
for (const g of GATE_ROSTER) {
|
|
256
|
+
console.log(`\n ${g.n}/${GATE_ROSTER.length}. ${g.cmd} # ${g.what}`);
|
|
193
257
|
try {
|
|
194
|
-
sh(g, args, { stdio: 'inherit' });
|
|
258
|
+
sh(g.cmd, args, { stdio: 'inherit' });
|
|
195
259
|
} catch (e) {
|
|
196
|
-
console.error(`\n ✗ FAILED: ${g}`);
|
|
260
|
+
console.error(`\n ✗ FAILED (${g.n}/${GATE_ROSTER.length}): ${g.cmd}`);
|
|
197
261
|
console.error(' Pre-flight aborted. Fix the gate failure before proceeding.');
|
|
198
262
|
console.error(' See references/gates-catalog.md for failure-mode → recovery.');
|
|
199
263
|
process.exit(1);
|
|
200
264
|
}
|
|
201
265
|
}
|
|
266
|
+
console.log(`\n ✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Read a package's CHANGELOG at repo HEAD (working tree) and report whether a
|
|
270
|
+
// `## [Unreleased]` heading still carries non-empty content — i.e. it rode
|
|
271
|
+
// through un-promoted. Defect 1 (H1-adjacent, audit seed): `--mode cut` never
|
|
272
|
+
// called promote-unreleased.mjs at all, so a peer's hand-authored
|
|
273
|
+
// `## [Unreleased]` section shipped in the tarball unpromoted (bit the
|
|
274
|
+
// v0.8.4 cut — the plugin packages needed hand-promotion because nothing
|
|
275
|
+
// caught it). Returns null when clean, or a short description when dirty.
|
|
276
|
+
function unpromotedUnreleased(repo, pkg) {
|
|
277
|
+
const p = resolvePackageChangelog(repo, pkg);
|
|
278
|
+
if (!fs.existsSync(p)) return null;
|
|
279
|
+
const txt = fs.readFileSync(p, 'utf8');
|
|
280
|
+
const m = txt.match(/^## \[Unreleased\][^\n]*\n([\s\S]*?)(?=^## \[|$(?![\s\S]))/m);
|
|
281
|
+
if (!m) return null;
|
|
282
|
+
// Strip subsection headers (### Added, ### Fixed, …) and blank lines — what's
|
|
283
|
+
// left is real content, not just empty scaffolding.
|
|
284
|
+
const body = m[1].replace(/^###.*$/gm, '').trim();
|
|
285
|
+
if (body.length === 0) return null;
|
|
286
|
+
const firstLine = body.split('\n')[0].slice(0, 100);
|
|
287
|
+
return `${pkg}: "${firstLine}${firstLine.length === 100 ? '…' : ''}"`;
|
|
202
288
|
}
|
|
203
289
|
|
|
204
290
|
// ── Step 4 — Promote + bump + lockfile ───────────────────────────
|
|
@@ -209,7 +295,12 @@ function step4PromoteAndBump(args) {
|
|
|
209
295
|
}
|
|
210
296
|
console.log('\n=== Step 4 — Promote + Bump + Lockfile ===');
|
|
211
297
|
|
|
212
|
-
|
|
298
|
+
// Promotion runs for BOTH cut and from-scratch now — a peer's pre-staged
|
|
299
|
+
// [Unreleased] content needs promoting in either mode; only the CALLER
|
|
300
|
+
// (whether a peer already promoted vs. this cycle authoring fresh) differs,
|
|
301
|
+
// not whether promotion is needed. `--substantive-packages` is the same flag
|
|
302
|
+
// in both modes.
|
|
303
|
+
if (args.substantivePackages) {
|
|
213
304
|
const cmd = `node ${SCRIPT_DIR}/promote-unreleased.mjs --version ${args.version} --date ${args.date} --packages ${args.substantivePackages.join(',')}`;
|
|
214
305
|
sh(cmd, args);
|
|
215
306
|
}
|
|
@@ -219,9 +310,35 @@ function step4PromoteAndBump(args) {
|
|
|
219
310
|
sh(cmd, args);
|
|
220
311
|
}
|
|
221
312
|
|
|
313
|
+
// Loud guard: after promotion, NO package in the lockstep set (including
|
|
314
|
+
// the 2 plugin packages, which required hand-promotion at v0.8.4) may still
|
|
315
|
+
// carry a non-empty [Unreleased] — that content would ride through the
|
|
316
|
+
// bump unpromoted, undocumented in the tarball's own CHANGELOG.
|
|
317
|
+
if (!args.dry) {
|
|
318
|
+
const dirty = PACKAGES.map((p) => unpromotedUnreleased(REPO, p)).filter(Boolean);
|
|
319
|
+
if (dirty.length > 0) {
|
|
320
|
+
console.error(`\nERROR: ${dirty.length} package(s) still have non-empty [Unreleased] content after promotion:`);
|
|
321
|
+
for (const d of dirty) console.error(` ✗ ${d}`);
|
|
322
|
+
console.error('\n Add the missing package(s) to --substantive-packages (or --stub-packages if');
|
|
323
|
+
console.error(' they are pure ride-alongs) and re-run. See changelog-discipline.md §Detecting');
|
|
324
|
+
console.error(' source-changing packages.');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
console.log(` ✓ no unpromoted [Unreleased] content across all ${PACKAGES.length} packages`);
|
|
328
|
+
}
|
|
329
|
+
|
|
222
330
|
sh(`node ${SCRIPT_DIR}/bump.mjs --from ${args.previous} --to ${args.version}`, args);
|
|
223
331
|
sh('npm install --package-lock-only --no-audit --no-fund', args);
|
|
224
332
|
sh('npm run check:lockstep', args);
|
|
333
|
+
|
|
334
|
+
// Step 4f — pre-tag F-N1 coverage with auto-fix, run with the AUTHORITATIVE
|
|
335
|
+
// matcher (check-release.mjs itself, not the drifted 9-package
|
|
336
|
+
// check-changelog-coverage.mjs). Appends verified Maintenance bullets for
|
|
337
|
+
// any changed-but-unmentioned directory so the post-tag F-N1 (Step 7) is a
|
|
338
|
+
// formality. The v0.8.5 cut lacked this and paid 3 enrichment PRs + 3
|
|
339
|
+
// delete/retag rounds discovering the same gaps only after tagging.
|
|
340
|
+
console.log('\n Step 4f — pre-tag coverage (+auto-fix) with the authoritative F-N1 matcher:');
|
|
341
|
+
sh(`node scripts/release/check-release.mjs --pending-version ${args.version} --fix`, args);
|
|
225
342
|
}
|
|
226
343
|
|
|
227
344
|
// ── Step 5 — Commit ──────────────────────────────────────────────
|
|
@@ -262,23 +379,45 @@ function step5Commit(args) {
|
|
|
262
379
|
sh(`git commit -F ${args.commitMessageFile}`, args);
|
|
263
380
|
}
|
|
264
381
|
|
|
265
|
-
// ── Step 6 — Tag
|
|
382
|
+
// ── Step 6 — Tag ← operator checkpoint 1 (before tagging) ────────
|
|
266
383
|
async function step6Tag(args) {
|
|
267
384
|
console.log('\n=== Step 6 — Tag at HEAD ===');
|
|
268
|
-
|
|
385
|
+
const tagList = [`v${args.version}`, ...PACKAGES.map((p) => `${p}-v${args.version}`)];
|
|
386
|
+
const evidence = `Planned tags (${tagList.length} — umbrella + ${PACKAGES.length} per-package):\n` +
|
|
387
|
+
tagList.map((t) => ` ${t}`).join('\n');
|
|
388
|
+
// --yes covers ONLY this checkpoint (defect-2/H5) — moving/deleting a
|
|
389
|
+
// local-only tag is free, so this is the least destructive of the three.
|
|
390
|
+
await checkpoint('Create tags at HEAD — before tagging', evidence, args.yes);
|
|
269
391
|
sh(`node ${SCRIPT_DIR}/tag-lockstep.mjs --version ${args.version}`, args);
|
|
392
|
+
console.log(` ✓ created ${tagList.length} tags`);
|
|
270
393
|
}
|
|
271
394
|
|
|
272
|
-
// ── Step 7 — F-N1
|
|
395
|
+
// ── Step 7 — F-N1 ─────────────────────────────────────────────────
|
|
396
|
+
// Captures output (rather than streaming it) so checkpoint 2 can show the
|
|
397
|
+
// per-package F-N1 results as evidence BEFORE the push prompt (H5) — the F-N1
|
|
398
|
+
// output used to scroll past in Step 7 with no connection to Step 8's prompt.
|
|
273
399
|
function step7Fn1(args) {
|
|
274
400
|
console.log('\n=== Step 7 — F-N1 release trip-wire ===');
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
401
|
+
const { code, output } = shCapture('node scripts/release/check-release.mjs --all-pending', args);
|
|
402
|
+
console.log(output);
|
|
403
|
+
// CodeRabbit (PR #289): a non-zero exit here (without --strict, which this
|
|
404
|
+
// call doesn't pass) is ALWAYS a real `error` finding — check-release.mjs
|
|
405
|
+
// only sets a non-zero code for `error` severity; cosmetic `warn`s leave it
|
|
406
|
+
// 0. Treating non-zero as "just review the warns" let --push auto-confirm
|
|
407
|
+
// straight past a known trip-wire failure. Fail closed: hard-stop HERE,
|
|
408
|
+
// before Step 8 exists at all — no flag can skip this.
|
|
409
|
+
if (code !== 0 && !args.dry) {
|
|
410
|
+
console.error('\n ✗ F-N1 reported an ERROR (not a cosmetic warn) — release-pack stops here.');
|
|
411
|
+
console.error(' Fix the finding above, land it as a new commit through the PR flow (the');
|
|
412
|
+
console.error(' release commit is already merged — see changelog-discipline.md §F-N1');
|
|
413
|
+
console.error(' diff-coverage enrichment), delete + re-create the tags, re-run.');
|
|
414
|
+
process.exit(1);
|
|
281
415
|
}
|
|
416
|
+
// Cosmetic warns (exit 0, but the output carries `[warn]` lines) are real
|
|
417
|
+
// signal an operator should read before pushing — never let --push silently
|
|
418
|
+
// wave them through; only the interactive prompt may proceed past a warn.
|
|
419
|
+
const hasWarn = /\[warn\]/.test(output);
|
|
420
|
+
return { output, hasWarn };
|
|
282
421
|
}
|
|
283
422
|
|
|
284
423
|
// ── Step 8 — Push ────────────────────────────────────────────────
|
|
@@ -296,7 +435,7 @@ function step7Fn1(args) {
|
|
|
296
435
|
// orchestrated cut does NOT rely on push-on-tag firing. The one-at-a-time push
|
|
297
436
|
// is defensive (a cut aborted after Step 8 still has its publishes triggered)
|
|
298
437
|
// AND keeps the standalone tag-lockstep.mjs hint honest.
|
|
299
|
-
async function step8Push(args) {
|
|
438
|
+
async function step8Push(args, fn1) {
|
|
300
439
|
console.log('\n=== Step 8 — Push tags (per-tag, NOT batched) ===');
|
|
301
440
|
// Invariant 3: main reached this state via a merged release PR — never a
|
|
302
441
|
// direct push from here. Assert we're tagging the pushed history.
|
|
@@ -305,14 +444,22 @@ async function step8Push(args) {
|
|
|
305
444
|
console.error(`ERROR: HEAD is ${ahead} commit(s) ahead of origin/main — release commits land via PR (invariant 3); merge first, then tag at post-merge main.`);
|
|
306
445
|
process.exit(1);
|
|
307
446
|
}
|
|
308
|
-
await checkpoint('Ready to push 12 tags (one-at-a-time)', args);
|
|
309
447
|
const perPkgTags = PACKAGES.map((p) => `${p}-v${args.version}`);
|
|
448
|
+
const evidence = `F-N1 results (Step 7):\n${fn1?.output || '(none captured)'}\n` +
|
|
449
|
+
`Tags to push (${perPkgTags.length + 1} — one push per tag, umbrella last):\n` +
|
|
450
|
+
[...perPkgTags, `v${args.version}`].map((t) => ` ${t}`).join('\n');
|
|
451
|
+
// --push is its OWN flag — NOT covered by --yes (defect-2/H5). CodeRabbit
|
|
452
|
+
// (PR #289): a cosmetic F-N1 warn must still force the interactive prompt —
|
|
453
|
+
// --push may NOT auto-confirm past ANY F-N1 finding, only a fully clean
|
|
454
|
+
// run. (A real F-N1 error never reaches here — step7Fn1 hard-exits first.)
|
|
455
|
+
const autoConfirm = args.push && !fn1?.hasWarn;
|
|
456
|
+
await checkpoint('Push tags — before pushing', evidence, autoConfirm);
|
|
310
457
|
for (const t of perPkgTags) {
|
|
311
458
|
sh(`git push origin ${t}`, args);
|
|
312
459
|
}
|
|
313
460
|
// Umbrella tag last — triggers no workflow (convention only).
|
|
314
461
|
sh(`git push origin v${args.version}`, args);
|
|
315
|
-
console.log(`\n ${perPkgTags.length} per-package tags + 1 umbrella
|
|
462
|
+
console.log(`\n ✓ pushed ${perPkgTags.length} per-package tags + 1 umbrella tag`);
|
|
316
463
|
console.log(' NOTE: a single `git push origin <all tags>` can skip ALL publish-on-tag');
|
|
317
464
|
console.log(' triggers (the GitHub batch-push skip). Step 9 dispatches via');
|
|
318
465
|
console.log(' workflow_dispatch + verifies each triggered — see recovery-paths.md §Scenario 7.');
|
|
@@ -324,9 +471,30 @@ async function step8Push(args) {
|
|
|
324
471
|
// exist for the <pkg>-vX.Y.Z tags and re-dispatches ONLY the misses — closing the
|
|
325
472
|
// batch-tag-push skip without double-publishing the ones that already fired.
|
|
326
473
|
// Idempotent: if all 11 triggered (or none did), it does the right thing.
|
|
474
|
+
// Snapshot the CURRENT registry state — the "ordering evidence" for
|
|
475
|
+
// checkpoint 3 (H5): what's live right now, before anything is dispatched,
|
|
476
|
+
// so the operator can confirm publish order/target against reality rather
|
|
477
|
+
// than a bare y/N.
|
|
478
|
+
function registrySnapshot(args) {
|
|
479
|
+
if (args.dry) return '[dry] (registry snapshot skipped)';
|
|
480
|
+
const lines = [];
|
|
481
|
+
for (const pkg of PACKAGES) {
|
|
482
|
+
let v;
|
|
483
|
+
try { v = execSync(`npm view "${args.scope}/${pkg}" version`, { encoding: 'utf8' }).trim(); }
|
|
484
|
+
catch { v = 'MISSING/E404'; }
|
|
485
|
+
lines.push(` ${pkg}: ${v}${v === args.version ? ' (already at target — idempotent re-dispatch)' : ''}`);
|
|
486
|
+
}
|
|
487
|
+
let latest = 'unknown';
|
|
488
|
+
try { latest = execSync(`npm view ${args.scope}/web-components dist-tags.latest`, { encoding: 'utf8' }).trim(); } catch { /* offline */ }
|
|
489
|
+
lines.push(` dist-tags.latest: ${latest}`);
|
|
490
|
+
return `Registry state BEFORE publish (target = ${args.version}):\n${lines.join('\n')}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
327
493
|
async function step9Publish(args) {
|
|
328
494
|
console.log('\n=== Step 9 — Verify-triggered + re-dispatch misses ===');
|
|
329
|
-
|
|
495
|
+
const evidence = registrySnapshot(args);
|
|
496
|
+
// --publish is its OWN flag — NOT covered by --yes (defect-2/H5).
|
|
497
|
+
await checkpoint('Dispatch publishes — before publishing', evidence, args.publish);
|
|
330
498
|
// Give GH a moment to index the runs created by the per-tag push in Step 8.
|
|
331
499
|
if (!args.dry) sh('sleep 6', args);
|
|
332
500
|
sh(`node ${SCRIPT_DIR}/dispatch-publish.mjs --version ${args.version} --scope ${args.scope} --verify-triggered`, args);
|
|
@@ -335,7 +503,7 @@ async function step9Publish(args) {
|
|
|
335
503
|
console.log(' ✓ publishes settled');
|
|
336
504
|
// Fail-closed: the registry is the verify target (SKILL.md) — a missing or
|
|
337
505
|
// stale package aborts the cut here, before GH releases + site deploy.
|
|
338
|
-
console.log(`\n
|
|
506
|
+
console.log(`\n Post-publish registry verification, all ${PACKAGES.length} at ${args.version} + npm latest (fail-closed):`);
|
|
339
507
|
sh(
|
|
340
508
|
`fail=0; ` +
|
|
341
509
|
`for pkg in ${PACKAGES.join(' ')}; do ` +
|
|
@@ -369,8 +537,24 @@ async function step10GhAndSite(args) {
|
|
|
369
537
|
console.log(' --skip-site set; skipping EXE deploy');
|
|
370
538
|
return;
|
|
371
539
|
}
|
|
372
|
-
|
|
373
|
-
|
|
540
|
+
// H1 (forge-campaign gh#268 audit): this used to `npm run build:site` +
|
|
541
|
+
// raw `rsync -az --delete` straight to the host — bypassing deploy-site.yml
|
|
542
|
+
// entirely (its build → pre-flight verify → snapshot → rsync → post-deploy
|
|
543
|
+
// verify → auto-rollback pipeline), even though cut-procedure.md §Step 10
|
|
544
|
+
// already documented the workflow-dispatch path as the correct one. A raw
|
|
545
|
+
// local rsync has no snapshot to roll back to on a bad deploy. Dispatch the
|
|
546
|
+
// real pipeline instead — gated behind its own confirm (deploy is
|
|
547
|
+
// outward-facing exactly like publish; --publish covers it, or an
|
|
548
|
+
// interactive confirm if not given).
|
|
549
|
+
const evidence = `Dispatching "Deploy site (${args.host})" (.github/workflows/deploy-site.yml) —\n` +
|
|
550
|
+
`builds from main, runs pre-flight verify → snapshot → hardened rsync →\n` +
|
|
551
|
+
`post-deploy verify → auto-rollback. NOT a raw rsync (the bypass this replaced).`;
|
|
552
|
+
await checkpoint('Deploy site — before dispatching deploy-site.yml', evidence, args.publish);
|
|
553
|
+
sh(`gh workflow run "Deploy site (${args.host})" --repo adiahealth/gen-ui-kit --ref main`, args);
|
|
554
|
+
console.log(' Waiting for the deploy workflow to settle...');
|
|
555
|
+
if (!args.dry) sh('sleep 10', args);
|
|
556
|
+
sh(`until [ "$(gh run list --workflow=deploy-site.yml --limit 1 --json status -q '.[0].status')" = "completed" ]; do sleep 5; done`, args);
|
|
557
|
+
console.log(' ✓ deploy-site.yml settled');
|
|
374
558
|
// Verify a deployed CONTENT FILE, never an SPA route — the docs site
|
|
375
559
|
// returns HTTP 200 + a blank shell for ANY unmatched route (v0.7.4 trap).
|
|
376
560
|
sh(`curl -s -o /dev/null -w "EXE host.css: HTTP %{http_code}\\n" https://${args.host}/packages/web-components/styles/host.css`, args);
|
|
@@ -405,16 +589,91 @@ async function main() {
|
|
|
405
589
|
return;
|
|
406
590
|
}
|
|
407
591
|
await step6Tag(args);
|
|
408
|
-
step7Fn1(args);
|
|
409
|
-
await step8Push(args);
|
|
592
|
+
const fn1 = step7Fn1(args);
|
|
593
|
+
await step8Push(args, fn1);
|
|
410
594
|
await step9Publish(args);
|
|
411
595
|
await step10GhAndSite(args);
|
|
412
596
|
rl.close();
|
|
413
597
|
console.log('\n✓ release-pack complete. Verify against the npm registry + GH releases, then author the release notes.');
|
|
414
598
|
}
|
|
415
599
|
|
|
416
|
-
|
|
417
|
-
|
|
600
|
+
// Plan-assembly selftest — spawns THIS script as a real subprocess with
|
|
601
|
+
// --mode cut --dry (fully mutation-free per every step's own dry-guard) and
|
|
602
|
+
// asserts the assembled plan carries the expected markers: the full
|
|
603
|
+
// 17-gate roster (H3 — a subset run must be impossible without editing
|
|
604
|
+
// gate-roster.mjs), and the invariant-3 stop-before-PR message (cut mode
|
|
605
|
+
// must never proceed to tag/push/publish on its own). A fixture temp dir
|
|
606
|
+
// supplies the operator-authored content files (--commit-message-file /
|
|
607
|
+
// --gh-notes-file) so nothing here depends on real repo state.
|
|
608
|
+
function selftest() {
|
|
609
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'release-pack-selftest-'));
|
|
610
|
+
try {
|
|
611
|
+
const commitFile = path.join(tmp, 'commit.txt');
|
|
612
|
+
const notesFile = path.join(tmp, 'notes.md');
|
|
613
|
+
fs.writeFileSync(commitFile, 'selftest fixture commit message\n');
|
|
614
|
+
fs.writeFileSync(notesFile, 'selftest fixture notes\n');
|
|
615
|
+
|
|
616
|
+
const scriptPath = fileURLToPathCompat(import.meta.url);
|
|
617
|
+
let out;
|
|
618
|
+
try {
|
|
619
|
+
out = execSync(
|
|
620
|
+
`node "${scriptPath}" --mode cut --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8 ` +
|
|
621
|
+
`--commit-message-file "${commitFile}" --gh-notes-file "${notesFile}" ` +
|
|
622
|
+
`--substantive-packages web-components --stub-packages llm --dry --yes`,
|
|
623
|
+
{ cwd: REPO, encoding: 'utf8' },
|
|
624
|
+
);
|
|
625
|
+
} catch (e) {
|
|
626
|
+
console.error('selftest FAIL: --mode cut --dry plan assembly exited non-zero');
|
|
627
|
+
console.error(e.stdout || e.message);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const need = [
|
|
632
|
+
`${GATE_ROSTER.length}-gate roster`,
|
|
633
|
+
`✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`,
|
|
634
|
+
'STOPPING per invariant 3',
|
|
635
|
+
];
|
|
636
|
+
for (const marker of need) {
|
|
637
|
+
if (!out.includes(marker)) {
|
|
638
|
+
console.error(`selftest FAIL: expected marker not found in plan output: "${marker}"`);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// The batch-mode hard-reject (H2) must fire BEFORE any of the above runs.
|
|
644
|
+
let batchOut = '';
|
|
645
|
+
let batchFailed = false;
|
|
646
|
+
try {
|
|
647
|
+
execSync(`node "${scriptPath}" --mode batch --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8`, { cwd: REPO, encoding: 'utf8' });
|
|
648
|
+
} catch (e) {
|
|
649
|
+
batchFailed = true;
|
|
650
|
+
batchOut = (e.stdout || '') + (e.stderr || '');
|
|
651
|
+
}
|
|
652
|
+
if (!batchFailed || !batchOut.includes('Scenario 2')) {
|
|
653
|
+
console.error('selftest FAIL: --mode batch must hard-reject with a pointer to recovery-paths.md §Scenario 2');
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
console.log('selftest OK');
|
|
658
|
+
} finally {
|
|
659
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// import.meta.url → filesystem path (works the same on the platforms this
|
|
664
|
+
// skill runs on; avoids pulling in node:url's fileURLToPath just for this).
|
|
665
|
+
function fileURLToPathCompat(url) {
|
|
666
|
+
return new URL(url).pathname;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const topArgv = process.argv.slice(2);
|
|
670
|
+
if (topArgv[0] === 'selftest') {
|
|
418
671
|
rl.close();
|
|
419
|
-
|
|
420
|
-
}
|
|
672
|
+
selftest();
|
|
673
|
+
} else {
|
|
674
|
+
main().catch((e) => {
|
|
675
|
+
console.error(e);
|
|
676
|
+
rl.close();
|
|
677
|
+
process.exit(1);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// tag-lockstep.mjs — create
|
|
3
|
-
// per-package <pkg>-vX.Y.Z
|
|
2
|
+
// tag-lockstep.mjs — create 12 lockstep tags (umbrella vX.Y.Z + 11
|
|
3
|
+
// per-package <pkg>-vX.Y.Z, including the 2 plugin packages that joined
|
|
4
|
+
// the lockstep 2026-07-15) at HEAD or at a specified SHA.
|
|
4
5
|
//
|
|
5
6
|
// Usage:
|
|
6
7
|
// node tag-lockstep.mjs --version 0.6.22
|
|
@@ -114,9 +115,30 @@ function main() {
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
function selftest() {
|
|
119
|
+
const tags = buildTagList('1.2.3');
|
|
120
|
+
if (tags.length !== 12) {
|
|
121
|
+
console.error(`selftest FAIL: expected 12 tags (umbrella + 11 per-package), got ${tags.length}`); process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (tags[0] !== 'v1.2.3') {
|
|
124
|
+
console.error('selftest FAIL: umbrella tag must be first'); process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
for (const expected of ['web-components-v1.2.3', 'adia-ui-factory-v1.2.3', 'adia-ui-forge-v1.2.3']) {
|
|
127
|
+
if (!tags.includes(expected)) {
|
|
128
|
+
console.error(`selftest FAIL: expected tag '${expected}' missing from buildTagList output`); process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
console.log('selftest OK');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const topArgv = process.argv.slice(2);
|
|
135
|
+
if (topArgv[0] === 'selftest') {
|
|
136
|
+
selftest();
|
|
137
|
+
} else {
|
|
138
|
+
try {
|
|
139
|
+
main();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(`error: ${e.message}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
122
144
|
}
|
|
@@ -35,7 +35,7 @@ skill routes to it and enforces it, it does not restate it.
|
|
|
35
35
|
| Task shape | Do |
|
|
36
36
|
| --- | --- |
|
|
37
37
|
| New page in an owned category | Read the convention doc's template section; copy the shared skeleton from a sibling page in the same category (narrative archetype vs. reference/gallery archetype). |
|
|
38
|
-
| Callout / "meta content" block | `<alert-ui variant="warning\|info\|...">` with `<div slot="content">` (never `<span>` — the transpiler only treats `p`/`div`/`ul`/`ol` as prose; a `<span>` silently reorders mixed inline content on regen) for rich text — never a bare `data-*` attribute. A whole multi-heading section aimed at a different reader (not a short callout) is NOT `alert-ui`'s job either
|
|
38
|
+
| Callout / "meta content" block | `<alert-ui variant="warning\|info\|...">` with `<div slot="content">` (never `<span>` — the transpiler only treats `p`/`div`/`ul`/`ol` as prose; a `<span>` silently reorders mixed inline content on regen) for rich text — never a bare `data-*` attribute. A whole multi-heading section aimed at a different reader (not a short callout) is NOT `alert-ui`'s job either — wrong shape — the convention doc's rule 2 carries the working treatment (eyebrow `<tag-ui>` title-row, TKT-0011). |
|
|
39
39
|
| Tamed admin-shell demo | Add `class="demo-frame"` to the shell instance + a page-local `<style>` block setting only `--demo-frame-height` (and any genuinely page-specific extra). That's `site/site.css`'s `.demo-frame` utility already covering `position`/`border`/`overflow`/`.demo-body` padding. |
|
|
40
40
|
| A surface needs to read as "its own distinct object" against the page background (a chip, a pill) | Reuse `--a-canvas-well-strong` (`packages/web-components/styles/colors/semantics/core.css`) — verified ≥3:1 (WCAG 2.2 SC 1.4.11) in both schemes. `--a-canvas-well` alone is for a subtly-sunken panel; a new candidate token always runs through `color:color-verify` first. |
|
|
41
41
|
| "Is this page consistent?" / review request | Check the page against the convention doc's rules 1–4 by name; a finding names the specific rule violated, not just "this looks off." |
|
|
@@ -63,6 +63,11 @@ A check that cannot run (missing script, no network for a build step) is a
|
|
|
63
63
|
named blocker in the report — flag it and stop; never mark the page done on
|
|
64
64
|
an assumed pass.
|
|
65
65
|
|
|
66
|
+
The fresh-context critic for an authored page is the `framework-reviewer`
|
|
67
|
+
agent (its dogfood visual probe covers rendered site surfaces) — the author
|
|
68
|
+
never certifies their own page's rendered result; the mechanical checks
|
|
69
|
+
above plus that read-only pass together are the review.
|
|
70
|
+
|
|
66
71
|
Done when the touched page(s) pass the checks above and, for a reviewed
|
|
67
72
|
page, every finding cites the specific rule in `site-pages-authoring.md` it
|
|
68
73
|
violates.
|