@event4u/agent-config 16.4.0 → 16.5.0
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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +40 -191
- package/README.md +1 -1
- package/dist/agent-src/commands/design.md +1 -1
- package/dist/agent-src/guidelines/design-antipatterns.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +2 -0
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/canvas-design/SKILL.md +4 -2
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +13 -10
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +49 -2
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +9 -0
- package/dist/agent-src/skills/iconography/SKILL.md +4 -2
- package/dist/agent-src/skills/image-provider-routing/SKILL.md +16 -5
- package/dist/agent-src/skills/logo-generation/SKILL.md +12 -8
- package/dist/agent-src/skills/svg-authoring/SKILL.md +318 -0
- package/dist/agent-src/skills/wireframe/SKILL.md +5 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/uiRender.js +201 -4
- package/dist/cli/commands/uiRenderAsset.js +1308 -0
- package/dist/cli/commands/uiRenderAssetCompare.js +752 -0
- package/dist/cli/commands/uiRenderAssetGrid.js +265 -0
- package/dist/cli/commands/uiRenderAssetGrounds.js +323 -0
- package/dist/cli/commands/uiRenderAssetIsolation.js +85 -0
- package/dist/cli/commands/uiRenderAssetTypes.js +145 -0
- package/dist/cli/main.js +47 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/{chunk-MSUL5NYP.js → chunk-4DBG5K7M.js} +1 -1
- package/dist/cli-delegate/{chunk-AED2VQGM.js → chunk-TMWYUOK4.js} +10 -1
- package/dist/cli-delegate/cmd_conformance.js +395 -214
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +47 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +19 -15
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +13 -9
- package/dist/hooks/dispatch.js +1073 -808
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/svg_rules.chunk.cjs +1626 -0
- package/dist/hooks/svg_rules.chunk.cjs.sha256 +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +931 -735
- package/dist/scripts/_lib/scan_scope.js +105 -0
- package/dist/scripts/_lib/svg_dom.js +284 -0
- package/dist/scripts/_lib/svg_geometry.js +477 -0
- package/dist/scripts/_lib/svg_motion.js +179 -0
- package/dist/scripts/_lib/svg_rules.js +1337 -0
- package/dist/scripts/_lib/svg_urls.js +67 -0
- package/dist/scripts/lint_svg_asset.js +324 -0
- package/docs/guidelines/design-antipatterns.md +1 -1
- package/package.json +4 -3
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/routing-coverage-seed.json +9 -2
- package/src/scripts/_lib/svg_dom.ts +318 -0
- package/src/scripts/_lib/svg_geometry.ts +480 -0
- package/src/scripts/_lib/svg_motion.ts +208 -0
- package/src/scripts/_lib/svg_rules.ts +1453 -0
- package/src/scripts/_lib/svg_urls.ts +72 -0
- package/src/scripts/_lib/verification_command.ts +59 -0
- package/src/scripts/_lib/verify_clause.ts +27 -17
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +16 -1
- package/src/scripts/hooks/design_pass_hook.ts +157 -3
- package/src/scripts/hooks/svg_obligation.ts +497 -0
- package/src/scripts/hooks/svg_rules_loader.ts +133 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +5 -3
- package/src/scripts/lint_consolidation_lineage.ts +6 -3
- package/src/scripts/lint_design_quality.ts +60 -12
- package/src/scripts/lint_motion_authority_drift.ts +27 -0
- package/src/scripts/lint_svg_asset.ts +365 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a browser reads the URLs and names in an SVG, for the safety rules in
|
|
3
|
+
* `svg_rules.ts`: references decoded, controls stripped, CSS escapes resolved,
|
|
4
|
+
* and an element prefix stripped only where it is bound to the SVG namespace.
|
|
5
|
+
* Its own module so `svg_rules.ts` stays under the source-size ceiling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Prefixes the document binds to the SVG namespace (`xmlns:svg="…/2000/svg"`). */
|
|
9
|
+
export function svgPrefixes(content: string): Set<string> {
|
|
10
|
+
const out = new Set<string>();
|
|
11
|
+
const re = /xmlns:([\w.-]+)\s*=\s*["']http:\/\/www\.w3\.org\/2000\/svg["']/gi;
|
|
12
|
+
let m: RegExpExecArray | null;
|
|
13
|
+
while ((m = re.exec(content)) !== null) out.add((m[1] ?? '').toLowerCase());
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The SVG local name: `svg:script` is a `script` where `svg` is bound to SVG; `foo:script` is not. */
|
|
18
|
+
export function localName(name: string, prefixes: ReadonlySet<string>): string {
|
|
19
|
+
const colon = name.indexOf(':');
|
|
20
|
+
return colon !== -1 && prefixes.has(name.slice(0, colon)) ? name.slice(colon + 1) : name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Attributes read from the raw tag, unquoted values included — HTML parses those. */
|
|
24
|
+
export function rawAttrs(raw: string): Record<string, string> {
|
|
25
|
+
const out: Record<string, string> = {};
|
|
26
|
+
const re = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"'`=]+))/g;
|
|
27
|
+
let m: RegExpExecArray | null;
|
|
28
|
+
while ((m = re.exec(raw)) !== null) out[(m[1] ?? '').toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? '';
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Attributes whose value is followed as a URL. */
|
|
33
|
+
export const URL_ATTRS = new Set(['href', 'xlink:href', 'src']);
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/** A URL that executes or opens a document when followed. */
|
|
37
|
+
export const isActiveUrl = (raw: string): boolean => /^(javascript:|data:text\/html)/i.test(resolvedUrl(raw));
|
|
38
|
+
|
|
39
|
+
const NAMED_URL_ENTITIES: Record<string, string> = {
|
|
40
|
+
colon: ':', sol: '/', tab: '\t', newline: '\n', period: '.', lpar: '(', rpar: ')', amp: '&',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The URL a browser would actually resolve from an attribute value: character
|
|
45
|
+
* references decoded, leading/trailing C0 controls and spaces stripped, tab and
|
|
46
|
+
* newline removed anywhere. The trailing `;` of a reference is optional on
|
|
47
|
+
* purpose: an `.svg` parsed as XML needs it, but the same graphic inlined in an
|
|
48
|
+
* HTML host is parsed by the HTML tokenizer, which decodes `javascript:`
|
|
49
|
+
* too — over-reading XML here costs precision, under-reading HTML costs a miss.
|
|
50
|
+
* The raw value is attacker-chosen, so a scheme test
|
|
51
|
+
* against it is a test of one spelling — ` https:`, `ht\ttps:`, `javascript:`
|
|
52
|
+
* all load, and all passed.
|
|
53
|
+
*/
|
|
54
|
+
export function resolvedUrl(raw: string): string {
|
|
55
|
+
const decoded = raw.replace(/&(?:#x([0-9a-f]+)|#(\d+)|([a-z]+));?/gi, (m, hex, dec, name) => {
|
|
56
|
+
const cp = hex ? parseInt(hex, 16) : dec ? parseInt(dec, 10) : NaN;
|
|
57
|
+
if (!Number.isNaN(cp)) return cp <= 0x10ffff ? String.fromCodePoint(cp) : m;
|
|
58
|
+
return NAMED_URL_ENTITIES[(name as string).toLowerCase()] ?? m;
|
|
59
|
+
});
|
|
60
|
+
return decoded.replace(/^[\u0000- ]+|[\u0000- ]+$/g, '').replace(/[\t\n\r]/g, '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** CSS escapes decoded: `\\68 ` and `\\h` both read `h`. */
|
|
64
|
+
export function cssUnescape(raw: string): string {
|
|
65
|
+
return raw.replace(/\\([0-9a-f]{1,6})\s?|\\(.)/gi, (m, hex, ch) => {
|
|
66
|
+
if (hex) {
|
|
67
|
+
const cp = parseInt(hex, 16);
|
|
68
|
+
return cp <= 0x10ffff ? String.fromCodePoint(cp) : m;
|
|
69
|
+
}
|
|
70
|
+
return ch as string;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -271,6 +271,28 @@ function segmentVerifies(rawWords: readonly string[]): boolean {
|
|
|
271
271
|
// the old token-anywhere regex accepted it by accident and neither
|
|
272
272
|
// explicit list named it, so the union seeding missed it too.
|
|
273
273
|
return rest.some((a) => a === 'test' || a.startsWith('--test'));
|
|
274
|
+
case 'agent-config':
|
|
275
|
+
// NEITHER `ui:render` NOR `xmllint` verifies here, and the first
|
|
276
|
+
// half of that was true, then false, and is true again.
|
|
277
|
+
//
|
|
278
|
+
// Step 3.5 of `road-to-svg-with-a-witness` taught this predicate
|
|
279
|
+
// `ui:render --profile asset`, reasoning that it is the one
|
|
280
|
+
// subcommand which opens the delivered file in a real engine. Step
|
|
281
|
+
// 2.1(b) of `road-to-svg-review-followups` removed it again on
|
|
282
|
+
// review claim C2, which is narrower than the first argument and
|
|
283
|
+
// refutes it: the turn-end gate's TRANSCRIPT path matches a command
|
|
284
|
+
// and never sees an exit code, so a call that rendered nothing —
|
|
285
|
+
// the ordinary case on a machine with no Playwright, where the
|
|
286
|
+
// command also exited 0 until the same day — discharged the
|
|
287
|
+
// obligation from its text alone.
|
|
288
|
+
//
|
|
289
|
+
// What replaces it reads the RENDER instead of the call:
|
|
290
|
+
// `hooks/svg_obligation.ts` matches a manifest to the file's own
|
|
291
|
+
// digest, and accepts a renderer-absent manifest only together with
|
|
292
|
+
// a clean static witness on those same bytes. `xmllint --noout`
|
|
293
|
+
// stays out for its original reason (S6): a file can parse and
|
|
294
|
+
// still be the wrong picture.
|
|
295
|
+
return false;
|
|
274
296
|
case 'scripts-run': {
|
|
275
297
|
// `./scripts-run src/scripts/lint_x` verifies; `…/rule_activation_census`
|
|
276
298
|
// writes a report and asserts nothing, which is why this keys on the
|
|
@@ -322,3 +344,40 @@ const _WIDE_CANDIDATE_RE =
|
|
|
322
344
|
export function mightBeVerification(command: string): boolean {
|
|
323
345
|
return _WIDE_CANDIDATE_RE.test(command);
|
|
324
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* `agent-config ui:render … --profile asset`, by shape.
|
|
350
|
+
*
|
|
351
|
+
* Moved here from `hooks/svg_obligation.ts` by finding 3 of this change's
|
|
352
|
+
* SECOND completion review: two detectors need it and one of them is in
|
|
353
|
+
* `_lib`, so a hook module was the wrong home. Re-exported there, so no
|
|
354
|
+
* caller moved.
|
|
355
|
+
*/
|
|
356
|
+
export function isAssetRenderCommand(command: string): boolean {
|
|
357
|
+
return /\bui:render\b/.test(command) && /--profile[= ]\s*asset\b/.test(command);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Did this command ATTEMPT to verify something — as opposed to: did it produce
|
|
362
|
+
* a verdict this gate may read?
|
|
363
|
+
*
|
|
364
|
+
* The distinction is the whole point, and the first review round and the
|
|
365
|
+
* second each saw one half of it. Round one (claim C2) was right that
|
|
366
|
+
* `isVerificationCommand` must NOT learn the asset render: the transcript path
|
|
367
|
+
* never sees an exit code, so the command's TEXT would have discharged the SVG
|
|
368
|
+
* obligation on a machine where the render produced nothing. Round two
|
|
369
|
+
* (finding 3) was right that removing it left detector C — "did any
|
|
370
|
+
* verification command follow the last edit" — unable to see the one command
|
|
371
|
+
* this package PRESCRIBES for an `.svg`, so the prescribed turn was refused.
|
|
372
|
+
*
|
|
373
|
+
* Both hold because they are about different detectors. Detector C asks
|
|
374
|
+
* whether the author tried; it is satisfied by `eslint` and must be satisfied
|
|
375
|
+
* by a render too. The SVG obligation asks what the render FOUND, and still
|
|
376
|
+
* reads the manifest and the file's digest — an attempt that produced nothing
|
|
377
|
+
* discharges nothing there, with a far better reason than detector C's.
|
|
378
|
+
*
|
|
379
|
+
* So: one predicate per question, and `isVerificationCommand` is unchanged.
|
|
380
|
+
*/
|
|
381
|
+
export function isVerificationAttempt(command: string): boolean {
|
|
382
|
+
return isVerificationCommand(command) || isAssetRenderCommand(command);
|
|
383
|
+
}
|
|
@@ -126,25 +126,35 @@ function lastMatch(re: RegExp, text: string): RegExpExecArray | null {
|
|
|
126
126
|
* carries `(?!`)`: a label followed by a backtick is the delimited form, which
|
|
127
127
|
* branch one already owns.
|
|
128
128
|
*
|
|
129
|
-
* The tail is
|
|
130
|
-
* this parser a whole step BLOCK already joined
|
|
131
|
-
* the line" can mean "the rest of the step"
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
129
|
+
* The tail is bounded by the EXPECTATION'S OWN DELIMITERS, not by the end of
|
|
130
|
+
* the line. A caller may hand this parser a whole step BLOCK already joined
|
|
131
|
+
* into one line, so "the rest of the line" can mean "the rest of the step": an
|
|
132
|
+
* end-anchored tail would lose the expectation of any step that carries prose
|
|
133
|
+
* after its clause. A slash-delimited regex therefore runs to the first
|
|
134
|
+
* UNESCAPED closing `/` — one not preceded by an odd number of backslashes —
|
|
135
|
+
* that is followed by whitespace or the end of the text, and an exit code is
|
|
136
|
+
* one non-space token; both self-terminating, so the step may continue and
|
|
137
|
+
* `-> /[1-9]/ and then note the result` still yields `[1-9]`.
|
|
137
138
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* the
|
|
144
|
-
*
|
|
145
|
-
*
|
|
139
|
+
* The word UNESCAPED is load-bearing and was added on 2026-10-11 (step 1.1 of
|
|
140
|
+
* `road-to-svg-review-followups`, review claim C4). The tail used to be a lazy
|
|
141
|
+
* `\/.*?\/(?=\s|$)`, which stops at ANY `/` followed by a space — so a clause
|
|
142
|
+
* ending in ``-> /a\/ b/`` was cut at the escaped slash and parsed to the
|
|
143
|
+
* regex source `a\`. A pattern that deliberately matches a path separator is
|
|
144
|
+
* exactly the case that spells it `\/`, and the truncation was silent: a
|
|
145
|
+
* shorter regex is still a regex, so nothing reported the loss.
|
|
146
|
+
*
|
|
147
|
+
* It was capped at a single non-space token until 2026-10-11, which cost this
|
|
148
|
+
* form every expectation containing a space: `-> /Icon-only button/` parsed as
|
|
149
|
+
* a clause with NO expectation, and nothing reported the loss — the step simply
|
|
150
|
+
* read as unfalsifiable. The two forms now agree on every expectation rather
|
|
151
|
+
* than only the space-free ones; {@link VERIFY_ARROW_SOURCE} stays the single
|
|
152
|
+
* `$`-anchored decision about what an expectation IS, and this regex decides
|
|
153
|
+
* only where the tail ENDS. A bare word stays refused by the constant, so
|
|
154
|
+
* widening the boundary widened no grammar.
|
|
146
155
|
*/
|
|
147
|
-
const BACKTICKED_RE =
|
|
156
|
+
const BACKTICKED_RE =
|
|
157
|
+
/(?:`verify:`|verify:(?!`))\s*`([^`]+)`[ \t]*((?:->|→)[ \t]*(?:\/(?:[^\\/]|\\.)*\/(?=\s|$)|\S+))?/g;
|
|
148
158
|
|
|
149
159
|
/** Prose after the label. The MANUAL form; the body is bounded separately. */
|
|
150
160
|
const PROSE_RE = /(?:`verify:`|verify:(?!`))[ \t]*/g;
|
|
@@ -149,11 +149,26 @@ function isGlobOverAcceptableInboxDir(name: string): boolean {
|
|
|
149
149
|
if (!/[*?[\]]/.test(name)) {
|
|
150
150
|
return false;
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
// Only a generic class is stripped. Any other bracket payload stays in the
|
|
153
|
+
// literal and is judged: `[acme]` names a source, and so does a run of
|
|
154
|
+
// singleton ranges like `[a-a][c-c]`.
|
|
155
|
+
const literal = name.replace(/[*?]|\[(?:a-z|0-9|a-f|0-9a-f|a-z0-9)\]/gi, '');
|
|
156
|
+
if (OPAQUE_FAMILY_WILDCARD_RE.test(name)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
153
159
|
const trimmed = literal.replace(/-+$/, '');
|
|
154
160
|
return trimmed !== '' && isAcceptableInboxDir(trimmed);
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
/**
|
|
164
|
+
* The fixed head of an opaque identifier family followed by wildcards only.
|
|
165
|
+
* `round-*` lists hex rounds; as a literal it would create `round-*`, which
|
|
166
|
+
* carries no source. Brackets are excluded because their payload survives as
|
|
167
|
+
* literal text: `round-[acme]-*` must not pass by stripping to `round`.
|
|
168
|
+
*/
|
|
169
|
+
const OPAQUE_FAMILY_WILDCARD_RE =
|
|
170
|
+
/^(?:(?:inbox|round|src-set|source-set)(?:-\d{4}(?:-\d{2})?)?|set)-[*?]+$/i;
|
|
171
|
+
|
|
157
172
|
/**
|
|
158
173
|
* The paths a redirect inside one segment would create.
|
|
159
174
|
*
|
|
@@ -48,6 +48,37 @@
|
|
|
48
48
|
* reason instead of passing silently. `src/rules/design-review-after-ui-write.md`
|
|
49
49
|
* already required that in prose with nothing to read it; this emits it.
|
|
50
50
|
*
|
|
51
|
+
* SVG ROUTING, AND HOW TO REVERT IT. Step 1.3 of
|
|
52
|
+
* `road-to-svg-rules-in-the-dispatch-path`. An `.svg` write reaches
|
|
53
|
+
* `lint_svg_asset`'s rules from here, inside or outside a UI tree, through
|
|
54
|
+
* `svg_rules_loader` — a CJS chunk beside the dispatch bundle, required only
|
|
55
|
+
* for a path ending in `.svg`. The review that withdrew the first attempt
|
|
56
|
+
* named the gap plainly: a statically bundled rule set has no off switch, so
|
|
57
|
+
* the triggers and the mechanism are written down before they are needed.
|
|
58
|
+
*
|
|
59
|
+
* REVERT WHEN any of these is read:
|
|
60
|
+
* · the composition gate exceeds its ceiling attributable to this routing
|
|
61
|
+
* (`check_hook_bundle_composition`; measured 2,128 B on 2026-10-11, with
|
|
62
|
+
* 16,910 B of headroom left);
|
|
63
|
+
* · a non-`.svg` hook event is observed failing with a stack that passes
|
|
64
|
+
* through the chunk — the containment case
|
|
65
|
+
* `design_pass_svg.test.ts > rule-failure-does-not-reach-an-unrelated-event`
|
|
66
|
+
* exists to make impossible, so one observation is enough;
|
|
67
|
+
* · an operator reports SVG false positives on files they accept, at a rate
|
|
68
|
+
* that would make them switch the carrier off — Risk 1 of the parent
|
|
69
|
+
* roadmap, and the failure this whole concern is shaped around.
|
|
70
|
+
*
|
|
71
|
+
* THE MECHANISM, in increasing order of blast radius:
|
|
72
|
+
* 1. Delete `dist/hooks/svg_rules.chunk.cjs`. The loader returns `null`,
|
|
73
|
+
* the pass reports `verification: degraded` naming the missing file,
|
|
74
|
+
* and every other finding family is unaffected. This is the per-install
|
|
75
|
+
* off switch the static import could not have had.
|
|
76
|
+
* 2. Drop `isSvgAsset` from `isPassSurface` below. An `.svg` outside a UI
|
|
77
|
+
* tree stops reaching the pass at all; one inside it reads as it did
|
|
78
|
+
* before this step.
|
|
79
|
+
* 3. Turn the concern off entirely (`hooks.design_pass.enabled: false`),
|
|
80
|
+
* which was already the default.
|
|
81
|
+
*
|
|
51
82
|
* Default-OFF (`hooks.design_pass.enabled`), fail_closed: false.
|
|
52
83
|
*
|
|
53
84
|
* Exit codes (dispatcher contract): 0 allow · 1 block · 2 warn + JSON reason.
|
|
@@ -62,6 +93,7 @@ import { trigger_matches, type Trigger } from '../_lib/router_match.js';
|
|
|
62
93
|
import { isUiPath, isUiTreePath } from '../_lib/ui_surface.js';
|
|
63
94
|
import { loadDesignContext, scanFile } from '../lint_design_slop.js';
|
|
64
95
|
import { readHookStdin } from './hook_stdin.js';
|
|
96
|
+
import { CHUNK_BASENAME, loadSvgScan } from './svg_rules_loader.js';
|
|
65
97
|
import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
|
|
66
98
|
|
|
67
99
|
const SETTINGS_FILE = '.agent-settings.yml';
|
|
@@ -129,8 +161,42 @@ export function enabled(root: string, key = 'design_pass'): boolean {
|
|
|
129
161
|
return false;
|
|
130
162
|
}
|
|
131
163
|
|
|
164
|
+
/**
|
|
165
|
+
* True for a path this pass should look at.
|
|
166
|
+
*
|
|
167
|
+
* `.svg` is admitted HERE and nowhere else — step 1.2 of
|
|
168
|
+
* `road-to-svg-rules-in-the-dispatch-path`. The obvious way to reach an `.svg`
|
|
169
|
+
* from this pass was to add `svg` to `UI_EXTENSIONS`, and that is the thing the
|
|
170
|
+
* parent roadmap's decision E10 and `design_pass_svg.test.ts` forbid: the same
|
|
171
|
+
* predicate backs `ui_route_nudge_hook`'s `isUiWrite`, so widening it would
|
|
172
|
+
* draw a UI routing nudge and its tokens on every icon dropped into `public/`.
|
|
173
|
+
* A local disjunct reaches the asset without touching the shared predicate, in
|
|
174
|
+
* either direction.
|
|
175
|
+
*/
|
|
176
|
+
export const isSvgAsset = (p: string): boolean => /\.svg$/i.test(p);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* UNCHANGED, and that is the point of the predicate below it.
|
|
180
|
+
*
|
|
181
|
+
* Finding 6 of this change's own completion review: widening THIS predicate to
|
|
182
|
+
* admit every `.svg` would have moved the UI-turn denominator the
|
|
183
|
+
* consultation-rate measurement is defined against — which the
|
|
184
|
+
* `HANDOVER_TRIGGERS` note further down reserves, in writing, as a different
|
|
185
|
+
* decision than any routing change. It was widened and is widened no longer.
|
|
186
|
+
*/
|
|
132
187
|
export const isUiSurface = (p: string): boolean => isUiPath(p) || isUiTreePath(p);
|
|
133
188
|
|
|
189
|
+
/**
|
|
190
|
+
* What this PASS looks at — a UI surface, or any `.svg`.
|
|
191
|
+
*
|
|
192
|
+
* The routing step 1.2 of `road-to-svg-rules-in-the-dispatch-path` asked for,
|
|
193
|
+
* in the one place it belongs: the pass's own entry gate. Keeping it out of
|
|
194
|
+
* `isUiSurface` leaves every other reading that predicate feeds — the
|
|
195
|
+
* conformance verdict's noteworthy test, the denominator above — reading
|
|
196
|
+
* exactly what it read before.
|
|
197
|
+
*/
|
|
198
|
+
export const isPassSurface = (p: string): boolean => isUiSurface(p) || isSvgAsset(p);
|
|
199
|
+
|
|
134
200
|
/**
|
|
135
201
|
* The shadow read of `ui-conformance.json` — Phase 5 of
|
|
136
202
|
* `road-to-behaviour-evidence-over-pixels`.
|
|
@@ -441,6 +507,53 @@ function renderAvailable(root: string): boolean {
|
|
|
441
507
|
}
|
|
442
508
|
}
|
|
443
509
|
|
|
510
|
+
/**
|
|
511
|
+
* SVG severity → this pass's severity.
|
|
512
|
+
*
|
|
513
|
+
* `error` and `warning` become P1 and P2; `inert` is dropped. Deliberately NOT
|
|
514
|
+
* P0: the P0 set above is a closed list of objective floors and it is the only
|
|
515
|
+
* thing that computes a block at stop. Routing a new rule family into it would
|
|
516
|
+
* change the block surface, which is a different decision than making the
|
|
517
|
+
* findings visible — and the one Risk 1 of the parent roadmap warns about.
|
|
518
|
+
*/
|
|
519
|
+
export function svgSeverity(s: 'error' | 'warning' | 'inert'): Finding['severity'] | null {
|
|
520
|
+
if (s === 'error') return 'P1';
|
|
521
|
+
if (s === 'warning') return 'P2';
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export interface SvgRuleFinding {
|
|
526
|
+
rule: string;
|
|
527
|
+
file: string;
|
|
528
|
+
line: number;
|
|
529
|
+
snippet: string;
|
|
530
|
+
severity: 'error' | 'warning' | 'inert';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Map the SVG rule set's findings onto this pass's shape.
|
|
535
|
+
*
|
|
536
|
+
* Pure, and exported, so the routing can be proven without building a bundle:
|
|
537
|
+
* the half that is expensive to exercise end-to-end is the chunk load, and that
|
|
538
|
+
* has its own test in `svg_rules_loader`.
|
|
539
|
+
*/
|
|
540
|
+
export function svgFindings(raw: readonly SvgRuleFinding[], rel: string): Finding[] {
|
|
541
|
+
const out: Finding[] = [];
|
|
542
|
+
for (const f of raw) {
|
|
543
|
+
const severity = svgSeverity(f.severity);
|
|
544
|
+
if (severity === null) continue;
|
|
545
|
+
out.push({
|
|
546
|
+
file: rel,
|
|
547
|
+
severity,
|
|
548
|
+
catalogId: 'SVG',
|
|
549
|
+
rule: f.rule,
|
|
550
|
+
line: f.line,
|
|
551
|
+
message: `${f.rule}${f.snippet ? ` — ${f.snippet}` : ''}`,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
|
|
444
557
|
/** Extracts the written path from a tool payload, whichever key the host uses. */
|
|
445
558
|
export function targetPath(payload: unknown): string | null {
|
|
446
559
|
if (typeof payload !== 'object' || payload === null) return null;
|
|
@@ -499,7 +612,7 @@ function main(): number {
|
|
|
499
612
|
targets = state.touched;
|
|
500
613
|
} else {
|
|
501
614
|
const t = targetPath(payload);
|
|
502
|
-
if (!t || !
|
|
615
|
+
if (!t || !isPassSurface(t)) return EXIT_ALLOW;
|
|
503
616
|
targets = [t];
|
|
504
617
|
if (!state.touched.includes(t)) state.touched.push(t);
|
|
505
618
|
}
|
|
@@ -508,6 +621,7 @@ function main(): number {
|
|
|
508
621
|
// Findings come from the shipped registry, via the same entrypoint CI uses.
|
|
509
622
|
const findings: Finding[] = [];
|
|
510
623
|
let scanFailed: string | null = null;
|
|
624
|
+
let svgRulesUnavailable = false;
|
|
511
625
|
try {
|
|
512
626
|
const ctx = loadDesignContext(root);
|
|
513
627
|
for (const rel of targets) {
|
|
@@ -517,6 +631,17 @@ function main(): number {
|
|
|
517
631
|
// itself rather than handing over a path, which is also what lets a
|
|
518
632
|
// post-write pass see the bytes that were just written.
|
|
519
633
|
const content = fs.readFileSync(abs, 'utf-8');
|
|
634
|
+
if (isSvgAsset(rel)) {
|
|
635
|
+
// The conditional chunk. Loaded only here, so a hook event that
|
|
636
|
+
// never touched an `.svg` pays none of its bytes and runs none
|
|
637
|
+
// of its code — the containment `design_pass_svg.test.ts` pins.
|
|
638
|
+
// A null load is reported through `svgRulesUnavailable`, never
|
|
639
|
+
// thrown: a consumer whose install is missing the chunk must
|
|
640
|
+
// lose the SVG findings, not the whole dispatch.
|
|
641
|
+
const scan = loadSvgScan();
|
|
642
|
+
if (scan === null) svgRulesUnavailable = true;
|
|
643
|
+
else findings.push(...svgFindings(scan(content, rel) as SvgRuleFinding[], rel));
|
|
644
|
+
}
|
|
520
645
|
for (const f of scanFile(content, rel, ctx)) {
|
|
521
646
|
findings.push({
|
|
522
647
|
file: rel,
|
|
@@ -532,11 +657,40 @@ function main(): number {
|
|
|
532
657
|
scanFailed = String(err);
|
|
533
658
|
}
|
|
534
659
|
|
|
535
|
-
|
|
536
|
-
|
|
660
|
+
// The audit question is asked of UI SURFACES, never of the pass surface.
|
|
661
|
+
// Findings 1 and 2 of the third completion review, both reproduced by
|
|
662
|
+
// running the hook: round one split the entry gate from the UI predicate,
|
|
663
|
+
// and `targets` carried the `.svg` past the split anyway. So a clean
|
|
664
|
+
// conformant icon write exited WARN with "no ui-audit.json newer than
|
|
665
|
+
// public/icon.svg" — an asset has no component inventory to be audited
|
|
666
|
+
// against — and, worse, `recordAuditDischarge` returns early on a non-empty
|
|
667
|
+
// `auditMissing`, so writing ONE `.svg` in a turn silently suppressed the
|
|
668
|
+
// ui-audit discharge row for every real UI file in that same turn.
|
|
669
|
+
const auditTargets = targets.filter((t) => isUiSurface(t));
|
|
670
|
+
const auditMissing = auditTargets.filter((t) => !auditIsFresh(root, t));
|
|
671
|
+
recordAuditDischarge(root, auditTargets, auditMissing);
|
|
537
672
|
let result = decide(slot, findings, auditMissing, new Set(state.surfaced), renderAvailable(root));
|
|
538
673
|
if (scanFailed) {
|
|
539
674
|
result = { ...result, verification: 'unverified', degradation_reason: `detector unavailable: ${scanFailed}` };
|
|
675
|
+
} else if (svgRulesUnavailable) {
|
|
676
|
+
// Degraded, not unverified: the rest of the pass ran. An absent chunk
|
|
677
|
+
// is reported rather than silently producing a clean SVG verdict —
|
|
678
|
+
// "absent is not clean" is the same rule the conformance verdict keeps.
|
|
679
|
+
// APPENDED, never overwritten. Finding 6 of the fourth completion
|
|
680
|
+
// review: `decide` may already have set a reason — the renderer being
|
|
681
|
+
// absent is the one it sets — and assigning here threw it away, so a
|
|
682
|
+
// turn where both degradations held reported only the newer one. Two
|
|
683
|
+
// degradations are two facts; the operator needs both or they fix one
|
|
684
|
+
// and are surprised by the other.
|
|
685
|
+
const svgReason = `the SVG rule set could not be loaded (expected dist/hooks/${CHUNK_BASENAME} beside the dispatch bundle) — the .svg files in this turn were not checked`;
|
|
686
|
+
result = {
|
|
687
|
+
...result,
|
|
688
|
+
verification: 'degraded',
|
|
689
|
+
degradation_reason:
|
|
690
|
+
result.degradation_reason === undefined || result.degradation_reason === ''
|
|
691
|
+
? svgReason
|
|
692
|
+
: `${result.degradation_reason}; ${svgReason}`,
|
|
693
|
+
};
|
|
540
694
|
}
|
|
541
695
|
|
|
542
696
|
for (const f of result.findings) state.surfaced.push(`${f.file}::${f.rule}::${f.line}`);
|