@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.
Files changed (77) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +40 -191
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/design.md +1 -1
  7. package/dist/agent-src/guidelines/design-antipatterns.md +1 -1
  8. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  9. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +2 -0
  10. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  11. package/dist/agent-src/skills/canvas-design/SKILL.md +4 -2
  12. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +13 -10
  13. package/dist/agent-src/skills/fe-design/references/design-patterns.md +49 -2
  14. package/dist/agent-src/skills/frontend-render-security/SKILL.md +9 -0
  15. package/dist/agent-src/skills/iconography/SKILL.md +4 -2
  16. package/dist/agent-src/skills/image-provider-routing/SKILL.md +16 -5
  17. package/dist/agent-src/skills/logo-generation/SKILL.md +12 -8
  18. package/dist/agent-src/skills/svg-authoring/SKILL.md +318 -0
  19. package/dist/agent-src/skills/wireframe/SKILL.md +5 -2
  20. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  21. package/dist/cli/commands/uiRender.js +201 -4
  22. package/dist/cli/commands/uiRenderAsset.js +1308 -0
  23. package/dist/cli/commands/uiRenderAssetCompare.js +752 -0
  24. package/dist/cli/commands/uiRenderAssetGrid.js +265 -0
  25. package/dist/cli/commands/uiRenderAssetGrounds.js +323 -0
  26. package/dist/cli/commands/uiRenderAssetIsolation.js +85 -0
  27. package/dist/cli/commands/uiRenderAssetTypes.js +145 -0
  28. package/dist/cli/main.js +47 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/{chunk-MSUL5NYP.js → chunk-4DBG5K7M.js} +1 -1
  31. package/dist/cli-delegate/{chunk-AED2VQGM.js → chunk-TMWYUOK4.js} +10 -1
  32. package/dist/cli-delegate/cmd_conformance.js +395 -214
  33. package/dist/cli-delegate/cmd_handoff.js +2 -2
  34. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  35. package/dist/discovery/deprecation-report.md +1 -1
  36. package/dist/discovery/discovery-manifest.json +47 -22
  37. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  38. package/dist/discovery/discovery-manifest.summary.md +6 -6
  39. package/dist/discovery/dormancy-report.md +1 -1
  40. package/dist/discovery/orphan-report.md +1 -1
  41. package/dist/discovery/packs.json +19 -15
  42. package/dist/discovery/trust-report.md +2 -2
  43. package/dist/discovery/workspaces.json +13 -9
  44. package/dist/hooks/dispatch.js +1073 -808
  45. package/dist/hooks/dispatch.sha256 +1 -1
  46. package/dist/hooks/svg_rules.chunk.cjs +1626 -0
  47. package/dist/hooks/svg_rules.chunk.cjs.sha256 +1 -0
  48. package/dist/mcp/registry-manifest.json +3 -3
  49. package/dist/mcp/server.json +2 -2
  50. package/dist/mcp/server.mjs +931 -735
  51. package/dist/scripts/_lib/scan_scope.js +105 -0
  52. package/dist/scripts/_lib/svg_dom.js +284 -0
  53. package/dist/scripts/_lib/svg_geometry.js +477 -0
  54. package/dist/scripts/_lib/svg_motion.js +179 -0
  55. package/dist/scripts/_lib/svg_rules.js +1337 -0
  56. package/dist/scripts/_lib/svg_urls.js +67 -0
  57. package/dist/scripts/lint_svg_asset.js +324 -0
  58. package/docs/guidelines/design-antipatterns.md +1 -1
  59. package/package.json +4 -3
  60. package/src/config/gate-violation-baselines.json +3 -3
  61. package/src/config/routing-coverage-seed.json +9 -2
  62. package/src/scripts/_lib/svg_dom.ts +318 -0
  63. package/src/scripts/_lib/svg_geometry.ts +480 -0
  64. package/src/scripts/_lib/svg_motion.ts +208 -0
  65. package/src/scripts/_lib/svg_rules.ts +1453 -0
  66. package/src/scripts/_lib/svg_urls.ts +72 -0
  67. package/src/scripts/_lib/verification_command.ts +59 -0
  68. package/src/scripts/_lib/verify_clause.ts +27 -17
  69. package/src/scripts/hooks/block_speaking_inbox_dir.ts +16 -1
  70. package/src/scripts/hooks/design_pass_hook.ts +157 -3
  71. package/src/scripts/hooks/svg_obligation.ts +497 -0
  72. package/src/scripts/hooks/svg_rules_loader.ts +133 -0
  73. package/src/scripts/hooks/turn_end_gate_hook.ts +5 -3
  74. package/src/scripts/lint_consolidation_lineage.ts +6 -3
  75. package/src/scripts/lint_design_quality.ts +60 -12
  76. package/src/scripts/lint_motion_authority_drift.ts +27 -0
  77. package/src/scripts/lint_svg_asset.ts +365 -0
@@ -0,0 +1,67 @@
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
+ /** Prefixes the document binds to the SVG namespace (`xmlns:svg="…/2000/svg"`). */
8
+ export function svgPrefixes(content) {
9
+ const out = new Set();
10
+ const re = /xmlns:([\w.-]+)\s*=\s*["']http:\/\/www\.w3\.org\/2000\/svg["']/gi;
11
+ let m;
12
+ while ((m = re.exec(content)) !== null)
13
+ out.add((m[1] ?? '').toLowerCase());
14
+ return out;
15
+ }
16
+ /** The SVG local name: `svg:script` is a `script` where `svg` is bound to SVG; `foo:script` is not. */
17
+ export function localName(name, prefixes) {
18
+ const colon = name.indexOf(':');
19
+ return colon !== -1 && prefixes.has(name.slice(0, colon)) ? name.slice(colon + 1) : name;
20
+ }
21
+ /** Attributes read from the raw tag, unquoted values included — HTML parses those. */
22
+ export function rawAttrs(raw) {
23
+ const out = {};
24
+ const re = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"'`=]+))/g;
25
+ let m;
26
+ while ((m = re.exec(raw)) !== null)
27
+ out[(m[1] ?? '').toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? '';
28
+ return out;
29
+ }
30
+ /** Attributes whose value is followed as a URL. */
31
+ export const URL_ATTRS = new Set(['href', 'xlink:href', 'src']);
32
+ /** A URL that executes or opens a document when followed. */
33
+ export const isActiveUrl = (raw) => /^(javascript:|data:text\/html)/i.test(resolvedUrl(raw));
34
+ const NAMED_URL_ENTITIES = {
35
+ colon: ':', sol: '/', tab: '\t', newline: '\n', period: '.', lpar: '(', rpar: ')', amp: '&',
36
+ };
37
+ /**
38
+ * The URL a browser would actually resolve from an attribute value: character
39
+ * references decoded, leading/trailing C0 controls and spaces stripped, tab and
40
+ * newline removed anywhere. The trailing `;` of a reference is optional on
41
+ * purpose: an `.svg` parsed as XML needs it, but the same graphic inlined in an
42
+ * HTML host is parsed by the HTML tokenizer, which decodes `&#106avascript:`
43
+ * too — over-reading XML here costs precision, under-reading HTML costs a miss.
44
+ * The raw value is attacker-chosen, so a scheme test
45
+ * against it is a test of one spelling — ` https:`, `ht\ttps:`, `javascript:`
46
+ * all load, and all passed.
47
+ */
48
+ export function resolvedUrl(raw) {
49
+ const decoded = raw.replace(/&(?:#x([0-9a-f]+)|#(\d+)|([a-z]+));?/gi, (m, hex, dec, name) => {
50
+ const cp = hex ? parseInt(hex, 16) : dec ? parseInt(dec, 10) : NaN;
51
+ if (!Number.isNaN(cp))
52
+ return cp <= 0x10ffff ? String.fromCodePoint(cp) : m;
53
+ return NAMED_URL_ENTITIES[name.toLowerCase()] ?? m;
54
+ });
55
+ return decoded.replace(/^[\u0000- ]+|[\u0000- ]+$/g, '').replace(/[\t\n\r]/g, '');
56
+ }
57
+ /** CSS escapes decoded: `\\68 ` and `\\h` both read `h`. */
58
+ export function cssUnescape(raw) {
59
+ return raw.replace(/\\([0-9a-f]{1,6})\s?|\\(.)/gi, (m, hex, ch) => {
60
+ if (hex) {
61
+ const cp = parseInt(hex, 16);
62
+ return cp <= 0x10ffff ? String.fromCodePoint(cp) : m;
63
+ }
64
+ return ch;
65
+ });
66
+ }
67
+ //# sourceMappingURL=svg_urls.js.map
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint_svg_asset — the command-line surface over the SVG rule set.
4
+ *
5
+ * The rules live in `_lib/svg_rules.ts`, because the design pass imports them
6
+ * into the hook bundle and must not drag a CLI in with them (see that file's
7
+ * header for the measurement). Everything this module adds is the part only a
8
+ * command needs: argument parsing, the directory walk, the project config, the
9
+ * scanned-nothing refusal, and the step 1.2 preservation readout.
10
+ *
11
+ * Inline-ignore (same shape as `lint_design_quality`, works in XML comments):
12
+ * svg-asset-disable <rule-id> [-- reason] (file scope)
13
+ * svg-asset-disable-next-line <rule-id> [-- reason]
14
+ * svg-asset-disable-line <rule-id> [-- reason]
15
+ *
16
+ * Per-project config (`.svg-asset.json` in the project root):
17
+ * { "ignoreRules": ["svg-a11y-generic-id"], "ignoreFiles": ["vendor/**"] }
18
+ *
19
+ * Usage:
20
+ * lint_svg_asset --dir <path> [--delivery inline|img|css-bg|document]
21
+ * lint_svg_asset --file <a.svg> [--file <b.svg>] [--delivery …]
22
+ * lint_svg_asset --file <after.svg> --baseline <before.svg> --allow id,id
23
+ *
24
+ * Exit codes: 0 = clean, 2 = findings, 1 = internal error (including the
25
+ * scanned-nothing refusal — a gate that scanned no file is not a clean gate).
26
+ */
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
30
+ import { DELIVERY_MODES, parseElements, scanHostHtml, scanSvg, } from './_lib/svg_rules.js';
31
+ export * from './_lib/svg_rules.js';
32
+ /**
33
+ * Canonicalised attribute map — number formatting, attribute order and
34
+ * whitespace normalised away, so a re-serialisation that changed nothing reads
35
+ * as nothing changed.
36
+ */
37
+ export function canonicalAttrs(attrs) {
38
+ const out = {};
39
+ for (const key of Object.keys(attrs).sort()) {
40
+ const v = (attrs[key] ?? '')
41
+ .replace(/\s+/g, ' ')
42
+ .trim()
43
+ // Numbers only where a number is what it is. A hex color is a
44
+ // TOKEN, not a quantity: normalising inside `#00ff00` collapses it
45
+ // onto `#0ff0`, and the readout then reports no change for a color
46
+ // edit outside the allowed ids — a surgical-edit witness that
47
+ // cannot see a recolour.
48
+ .replace(/#[0-9a-fA-F]+|-?\d+\.?\d*(?:[eE][-+]?\d+)?/g, (tok) => {
49
+ if (tok.startsWith('#'))
50
+ return tok.toLowerCase();
51
+ const num = Number(tok);
52
+ return Number.isFinite(num) ? String(Number(num.toFixed(4))) : tok;
53
+ });
54
+ out[key] = v;
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Every element outside the allowed set that changed between two revisions.
60
+ *
61
+ * NO PIXEL DIFF decides this. Anti-aliasing and font metrics would make a
62
+ * pixel comparison a per-machine verdict, and the discipline this witnesses —
63
+ * `design-fidelity-mechanics.md` § surgical edits — is about which elements
64
+ * the edit was allowed to touch, which is an attribute fact.
65
+ */
66
+ export function preservationReadout(beforeContent, afterContent, allowedIds) {
67
+ const allow = new Set(allowedIds.map((s) => s.trim()).filter(Boolean));
68
+ /**
69
+ * A stable key per element, and the id chain above it.
70
+ *
71
+ * An element WITHOUT an id is keyed by its nearest id-bearing ancestor plus
72
+ * its tag and its ordinal INSIDE that ancestor — never by a document-wide
73
+ * index. A document-wide index shifts for every element after an allowed
74
+ * deletion, so removing one child of the allowed group reported every
75
+ * un-idded element after it as changed. Measured on the `grouped-transforms`
76
+ * triple: the correct edit reported `rect#7 (element)` and the readout could
77
+ * not tell a clean surgical edit from a dirty one.
78
+ */
79
+ const index = (content) => {
80
+ const map = new Map();
81
+ const stack = [];
82
+ const ordinals = new Map();
83
+ for (const el of parseElements(content)) {
84
+ if (el.closing) {
85
+ if (stack[stack.length - 1]?.name === el.name)
86
+ stack.pop();
87
+ continue;
88
+ }
89
+ const chain = stack.map((f) => f.id).filter((x) => x !== null);
90
+ const ownerId = [...stack].reverse().find((f) => f.id !== null)?.id ?? '';
91
+ const own = el.attrs['id'] ?? null;
92
+ let key;
93
+ if (own) {
94
+ key = own;
95
+ }
96
+ else {
97
+ const bucket = `${ownerId}/${el.name}`;
98
+ const n = (ordinals.get(bucket) ?? 0) + 1;
99
+ ordinals.set(bucket, n);
100
+ key = `${bucket}#${n}`;
101
+ }
102
+ map.set(key, { el, chain: own ? [...chain, own] : chain });
103
+ if (!el.selfClosing)
104
+ stack.push({ name: el.name, id: own });
105
+ }
106
+ return map;
107
+ };
108
+ const before = index(beforeContent);
109
+ const after = index(afterContent);
110
+ const changes = [];
111
+ /** An element inside an allowed subtree is itself allowed. */
112
+ const permitted = (entry) => Boolean(entry && entry.chain.some((id) => allow.has(id)));
113
+ for (const [key, beforeEntry] of before) {
114
+ if (permitted(beforeEntry))
115
+ continue;
116
+ const afterEntry = after.get(key);
117
+ if (!afterEntry) {
118
+ changes.push({
119
+ id: key,
120
+ element: beforeEntry.el.name,
121
+ attribute: '(element)',
122
+ before: 'present',
123
+ after: 'removed',
124
+ });
125
+ continue;
126
+ }
127
+ const a = canonicalAttrs(beforeEntry.el.attrs);
128
+ const b = canonicalAttrs(afterEntry.el.attrs);
129
+ for (const attr of new Set([...Object.keys(a), ...Object.keys(b)])) {
130
+ if ((a[attr] ?? '') === (b[attr] ?? ''))
131
+ continue;
132
+ changes.push({
133
+ id: key,
134
+ element: beforeEntry.el.name,
135
+ attribute: attr,
136
+ before: a[attr] ?? '(absent)',
137
+ after: b[attr] ?? '(absent)',
138
+ });
139
+ }
140
+ }
141
+ for (const [key, afterEntry] of after) {
142
+ if (permitted(afterEntry) || before.has(key))
143
+ continue;
144
+ changes.push({
145
+ id: key,
146
+ element: afterEntry.el.name,
147
+ attribute: '(element)',
148
+ before: 'absent',
149
+ after: 'added',
150
+ });
151
+ }
152
+ return changes;
153
+ }
154
+ export function loadConfig(dir) {
155
+ const configPath = path.join(dir, '.svg-asset.json');
156
+ if (!fs.existsSync(configPath))
157
+ return {};
158
+ try {
159
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
160
+ }
161
+ catch {
162
+ return {};
163
+ }
164
+ }
165
+ function* walkDir(dir, ext) {
166
+ if (!fs.existsSync(dir))
167
+ return;
168
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
169
+ const full = path.join(dir, entry.name);
170
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
171
+ continue;
172
+ if (entry.isDirectory())
173
+ yield* walkDir(full, ext);
174
+ else if (ext.test(entry.name))
175
+ yield full;
176
+ }
177
+ }
178
+ // ---------------------------------------------------------------------------
179
+ // CLI
180
+ // ---------------------------------------------------------------------------
181
+ function readFlag(args, name) {
182
+ const i = args.indexOf(name);
183
+ return i >= 0 ? args[i + 1] : undefined;
184
+ }
185
+ function readAll(args, name) {
186
+ const out = [];
187
+ for (let i = 0; i < args.length; i++)
188
+ if (args[i] === name && args[i + 1])
189
+ out.push(args[i + 1]);
190
+ return out;
191
+ }
192
+ export function main(argv = process.argv.slice(2)) {
193
+ const args = [...argv];
194
+ const jsonMode = args.includes('--json');
195
+ const quiet = args.includes('--quiet');
196
+ const deliveryRaw = readFlag(args, '--delivery');
197
+ if (deliveryRaw && !DELIVERY_MODES.includes(deliveryRaw)) {
198
+ process.stderr.write(`[lint_svg_asset] unknown --delivery "${deliveryRaw}"; expected one of ${DELIVERY_MODES.join(', ')}\n`);
199
+ return 1;
200
+ }
201
+ const delivery = deliveryRaw;
202
+ // --- step 1.2: preservation readout -------------------------------------
203
+ const baseline = readFlag(args, '--baseline');
204
+ if (baseline) {
205
+ const files = readAll(args, '--file');
206
+ const after = files[0];
207
+ if (!after) {
208
+ process.stderr.write('[lint_svg_asset] --baseline needs a --file <after.svg> to compare against\n');
209
+ return 1;
210
+ }
211
+ if (!fs.existsSync(baseline) || !fs.existsSync(after)) {
212
+ process.stderr.write('[lint_svg_asset] --baseline or --file path not found\n');
213
+ return 1;
214
+ }
215
+ const allow = (readFlag(args, '--allow') ?? '').split(',').map((s) => s.trim()).filter(Boolean);
216
+ const changes = preservationReadout(fs.readFileSync(baseline, 'utf-8'), fs.readFileSync(after, 'utf-8'), allow);
217
+ if (jsonMode) {
218
+ process.stdout.write(`${JSON.stringify(changes, null, 2)}\n`);
219
+ return changes.length ? 2 : 0;
220
+ }
221
+ if (!changes.length) {
222
+ if (!quiet) {
223
+ process.stdout.write(`✅ lint_svg_asset --baseline: nothing changed outside ${allow.length} allowed id(s).\n`);
224
+ }
225
+ return 0;
226
+ }
227
+ for (const c of changes) {
228
+ process.stdout.write(`❌ ${after} [svg-preserve-changed-outside-allowed] <${c.element} id="${c.id}"> ` +
229
+ `${c.attribute}: "${c.before}" → "${c.after}"\n`);
230
+ }
231
+ process.stdout.write(`\nlint_svg_asset: ${changes.length} element change(s) outside the allowed set. ` +
232
+ 'A surgical edit touches only what it declared.\n');
233
+ return 2;
234
+ }
235
+ // --- scanning -----------------------------------------------------------
236
+ const explicit = readAll(args, '--file');
237
+ const dirArg = readFlag(args, '--dir');
238
+ const targets = [];
239
+ let roots = [];
240
+ if (explicit.length) {
241
+ targets.push(...explicit.map((f) => path.resolve(f)));
242
+ roots = targets;
243
+ }
244
+ else {
245
+ const scanDir = dirArg ? path.resolve(dirArg) : path.resolve(process.cwd(), 'src');
246
+ roots = [scanDir];
247
+ if (!fs.existsSync(scanDir)) {
248
+ // The two cases are NOT the same, and collapsing them into exit 0
249
+ // made a renamed or mistyped `--dir` report success in CI. An
250
+ // ABSENT DEFAULT root is the opt-in escape — a consumer with no
251
+ // `src/` has nothing to scan and must not red. An EXPLICIT `--dir`
252
+ // that does not resolve is a broken invocation, and this module's
253
+ // own header already says a gate that scanned no file is not a
254
+ // clean gate.
255
+ if (!quiet)
256
+ process.stderr.write(`[lint_svg_asset] scan directory not found: ${scanDir}\n`);
257
+ if (dirArg)
258
+ return 1;
259
+ process.stderr.write('This linter is opt-in. Run with --dir <path> or --file <a.svg> pointing at your assets.\n');
260
+ return 0;
261
+ }
262
+ for (const f of walkDir(scanDir, /\.(svg|html|htm)$/i))
263
+ targets.push(f);
264
+ }
265
+ const config = loadConfig(process.cwd());
266
+ const ignoreRules = new Set((config.ignoreRules ?? []).map((r) => r.toLowerCase()));
267
+ const ignoreGlobs = config.ignoreFiles ?? [];
268
+ const scanned = targets.filter((f) => !ignoreGlobs.some((g) => f.includes(g.replace('**', ''))));
269
+ try {
270
+ assertScanned({ gate: 'lint_svg_asset', scanned: scanned.length, units: 'SVG/HTML file(s)', roots });
271
+ }
272
+ catch (exc) {
273
+ if (exc instanceof DeadScopeError) {
274
+ process.stderr.write(`❌ ${exc.message}\n`);
275
+ return 1;
276
+ }
277
+ throw exc;
278
+ }
279
+ const all = [];
280
+ for (const abs of scanned) {
281
+ const rel = path.relative(process.cwd(), abs);
282
+ try {
283
+ const content = fs.readFileSync(abs, 'utf-8');
284
+ const findings = /\.svg$/i.test(abs)
285
+ ? scanSvg(content, rel, { delivery })
286
+ : scanHostHtml(content, rel, { delivery });
287
+ all.push(...findings.filter((f) => !ignoreRules.has(f.rule.toLowerCase())));
288
+ }
289
+ catch (err) {
290
+ if (!quiet)
291
+ process.stderr.write(`[lint_svg_asset] error: ${abs}: ${err}\n`);
292
+ }
293
+ }
294
+ if (jsonMode) {
295
+ process.stdout.write(`${JSON.stringify(all, null, 2)}\n`);
296
+ return all.length > 0 ? 2 : 0;
297
+ }
298
+ if (!all.length) {
299
+ if (!quiet) {
300
+ process.stdout.write(`✅ lint_svg_asset: clean — ${scanned.length} file(s) checked, no violations.\n`);
301
+ }
302
+ return 0;
303
+ }
304
+ const errors = all.filter((f) => f.severity === 'error');
305
+ const warnings = all.filter((f) => f.severity === 'warning');
306
+ const inert = all.filter((f) => f.severity === 'inert');
307
+ for (const f of all) {
308
+ const icon = f.severity === 'error' ? '❌' : f.severity === 'warning' ? '⚠️ ' : 'ℹ️ ';
309
+ process.stdout.write(`${icon} ${f.file}:${f.line} [${f.rule}] ${f.snippet}\n`);
310
+ }
311
+ process.stdout.write(`\nlint_svg_asset: ${errors.length} error(s), ${warnings.length} warning(s), ${inert.length} inert. ` +
312
+ 'Use svg-asset-disable-next-line <rule> -- reason or .svg-asset.json to suppress; ' +
313
+ 'svg-safety-* findings yield only to .svg-asset.json, never to a marker in the file. ' +
314
+ 'Advisory: an "inert" row is not a clean row — the same file inlined tomorrow is an error.\n');
315
+ return 2;
316
+ }
317
+ /* c8 ignore start — entrypoint guard, exercised by the CLI not by the tests */
318
+ const invokedDirectly = typeof process !== 'undefined' &&
319
+ Array.isArray(process.argv) &&
320
+ /lint_svg_asset\.(ts|js)$/.test(process.argv[1] ?? '');
321
+ if (invokedDirectly)
322
+ process.exit(main());
323
+ /* c8 ignore stop */
324
+ //# sourceMappingURL=lint_svg_asset.js.map
@@ -114,7 +114,7 @@ Within-project invariants + repetition caps. Override = declare the value in
114
114
  | V2 | Glassmorphism as decoration (`backdrop-blur` + `bg-white/10`) with no functional depth hierarchy | Model defaults to it for "premium feel"; result looks like 2021 Material You clip-art | Element genuinely sits above a complex, blurred background layer; use sparingly and only for top-level floating surfaces | craft-presumption | delete → reduce | static |
115
115
  | V3 | Ghost card: `border: 1px solid` + box-shadow ≥ 16px spread together | The combination creates visual noise — the ghost card signals no confident decision about depth | Choose either border OR shadow to indicate depth; never both on the same surface | invariant | delete → reduce | static |
116
116
  | V4 | Over-rounded small cards or badges: `border-radius` > 16px on elements < 200px wide | Excessive rounding on small surfaces looks toy-ish; standard cap is 12–16px | Pill shape (`border-radius: 9999px`) is intentional on single-line tags/chips only | craft-presumption | retune | static |
117
- | V5 | Hand-drawn or illustrated SVG icons mixed with crisp icon-system icons | Visual register collision; instantly reads as "generated asset" | Intentional illustrative section (hero illustration, empty-state art); isolate from the icon system | invariant | replace | render |
117
+ | V5 | Hand-drawn or illustrated SVG icons mixed with crisp icon-system icons | Visual register collision; instantly reads as "generated asset" | Intentional illustrative section (hero illustration, empty-state art); isolate from the icon system. A genuinely missing glyph is authored under [`svg-authoring`](../../src/skills/svg-authoring/SKILL.md) against the incumbent set's own grid | invariant | replace | render |
118
118
  | V6 | Repeating diagonal stripes as a background texture | A cliché background default; reads as generated CSS art | Brand explicitly uses pattern backgrounds (e.g., a textile brand); document the stripe definition | style-preference | delete | static |
119
119
  | V7 | Nested cards: a card inside a card inside a card | Depth hierarchy collapses; user cannot parse elevation intent | A truly three-level hierarchy (e.g., project → board → card); ensure each level has distinct visual weight | invariant | delete → reduce | render |
120
120
  | V9 | Noise or grain texture laid over a gradient as a "premium" surface default | The visual sibling of V6's diagonal stripes — a generated-CSS-art texture reached for to make a flat gradient look considered, applied because it is available rather than because the surface needed a texture | The texture is part of a documented brand surface treatment (a print-inspired or analog-photographic identity) with the grain opacity defined as a token, not chosen per surface | style-preference | delete | render |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
3
  "mcpName": "io.github.event4u-app/agent-config",
4
- "version": "16.4.0",
4
+ "version": "16.5.0",
5
5
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including the counts in these badges.",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -80,7 +80,7 @@
80
80
  "scripts": {
81
81
  "build:cli": "tsc -p tsconfig.json && chmod +x dist/cli/agent-config.js",
82
82
  "build:install-bundle": "esbuild src/scripts/install.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/install/install.mjs --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);\"",
83
- "build:hooks": "esbuild src/scripts/hooks/dispatch_entry.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/hooks/dispatch.js --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url); const __acPath = require('node:path'); const __acFs = require('node:fs'); const __acUrl = require('node:url'); const __acReal = (p) => { try { return __acFs.realpathSync(p); } catch { return p; } }; const __acSelf = __acReal(__acUrl.fileURLToPath(import.meta.url)); if (process.argv[1] !== undefined && __acReal(__acPath.resolve(process.argv[1])) === __acSelf) { globalThis.__AC_HOOKS_BUNDLE_DIRECT = true; process.argv = [process.argv[0], __acSelf + '.__direct__', ...process.argv.slice(2)]; }\" && node src/scripts/write_bundle_digest.mjs",
83
+ "build:hooks": "esbuild src/scripts/hooks/dispatch_entry.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/hooks/dispatch.js --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url); const __acPath = require('node:path'); const __acFs = require('node:fs'); const __acUrl = require('node:url'); const __acReal = (p) => { try { return __acFs.realpathSync(p); } catch { return p; } }; const __acSelf = __acReal(__acUrl.fileURLToPath(import.meta.url)); if (process.argv[1] !== undefined && __acReal(__acPath.resolve(process.argv[1])) === __acSelf) { globalThis.__AC_HOOKS_BUNDLE_DIRECT = true; process.argv = [process.argv[0], __acSelf + '.__direct__', ...process.argv.slice(2)]; }\" && node src/scripts/write_bundle_digest.mjs && npm run build:hook-chunks",
84
84
  "build:mcp-bundle": "esbuild src/scripts/mcp_server/__main__.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/mcp/server.mjs --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url); const __acPath = require('node:path'); const __acFs = require('node:fs'); const __acUrl = require('node:url'); const __acReal = (p) => { try { return __acFs.realpathSync(p); } catch { return p; } }; const __acSelf = __acReal(__acUrl.fileURLToPath(import.meta.url)); if (process.argv[1] !== undefined && __acReal(__acPath.resolve(process.argv[1])) === __acSelf) { globalThis.__AC_MCP_BUNDLE_DIRECT = true; process.argv = [process.argv[0], __acSelf + '.__direct__', ...process.argv.slice(2)]; }\"",
85
85
  "clean:cli-delegate": "rm -rf dist/cli-delegate",
86
86
  "build:delegates": "npm run clean:cli-delegate && npm run build:cli-delegate && npm run build:agent-src-delegate && npm run build:delegate-aux",
@@ -100,7 +100,8 @@
100
100
  "lint:publint": "publint",
101
101
  "prepack": "npm run build && npm run build:discovery && npm run build:mcp-manifest && node src/scripts/prepack-check.mjs",
102
102
  "prepublishOnly": "./scripts-run src/scripts/check_release_includes_discovery",
103
- "prepare": "[ -d .git ] && bash src/scripts/install-hooks.sh || true"
103
+ "prepare": "[ -d .git ] && bash src/scripts/install-hooks.sh || true",
104
+ "build:hook-chunks": "esbuild src/scripts/_lib/svg_rules.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/hooks/svg_rules.chunk.cjs && node src/scripts/write_bundle_digest.mjs dist/hooks/svg_rules.chunk.cjs"
104
105
  },
105
106
  "publishConfig": {
106
107
  "access": "public",
@@ -66,9 +66,9 @@
66
66
  "falsifier": "./scripts-run src/scripts/check_gate_coverage"
67
67
  },
68
68
  "lint_canonical_terms": {
69
- "count": 1004,
70
- "landed": "2026-09-13",
71
- "note": "Lowered 1005 -> 1004 on 2026-09-13 by drain/adversarial-verification, on the same shape and the same discriminator as every entry below. That branch added a routed rule and then paid its per-spawn payload cost by migrating argument prose out of three standing rules (autonomous-execution, delegation-policy, evaluator-independence) into their context and guideline files under the P4 pattern. Nothing was deleted and nothing was swept: the migrated lines were REWRITTEN CANONICAL where the diff authored them, LINE-SCOPED, because `sweep_authorised: false` is still a live constraint. Most were net-neutral -- they replaced deleted lines carrying the same word -- and one is a genuine reduction. The gate reported the tree at 1004 against a baseline of 1005 and asked for exactly this lowering, so the gain cannot be silently given back. LOCAL READING, ADMISSIBLE ON THE SAME DISCRIMINATOR THE ENTRIES BELOW USE: the gate walks only authored prose under src/ and docs/, and a porcelain status over those two roots reported ZERO untracked files, so a clean CI checkout of this commit measures the same 1004. If CI reads anything else, CI's number is the correct one and this note is the record of why. `landed` is refreshed deliberately: checkRatchet returns `improved` BEFORE it evaluates staleness, so an entry left at 1005 while the tree measures 1004 would never expire at any age. PRIOR ENTRY: Lowered 1006 -> 1005 on 2026-09-08 by drain/graph-shipped, on the same shape as the entry below and with the same discriminator. This branch's payload migration moved a See-also line carrying `artefact` from the rule into the code-intelligence skill; keeping the word in both places would have RAISED the count, so both occurrences were written canonical LINE-SCOPED — only lines this diff authored, never a file-wide sweep. One of the two is a genuine reduction. The gate reported the tree at 1005 against a baseline of 1006 and asked for exactly this lowering. `landed` is refreshed for the reason the prior entry records: checkRatchet returns `improved` before it evaluates staleness, so an entry left high never expires. PRIOR ENTRY: Lowered 1007 -> 1006 on 2026-09-07 by drain/observed-learning-signal, and the gain was INCIDENTAL rather than sought: the branch's own new prose introduced five house-dialect occurrences under src/ and docs/ (four `artefact`/`artefacts`, one `behaviour`), which were rewritten LINE-SCOPED — only lines this diff authored, never a file-wide sweep, because `sweep_authorised: false` is still a live constraint. Four of the five were net-neutral (they replaced deleted lines carrying the same word) and one was a genuine reduction. The gate reported the tree at 1006 against a baseline of 1007 and asked for exactly this lowering, so the gain cannot be silently given back. LOCAL READING, ADMISSIBLE ON THE SAME DISCRIMINATOR THIS FILE'S check_source_size_budget ENTRY USES: the gate walks only authored prose under src/ and docs/, and `git status --porcelain -uall -- src docs` reported ZERO untracked files, so a clean CI checkout of this commit measures the same 1006. If CI reads anything else, CI's number is the correct one and this note is the record of why. `landed` is refreshed deliberately: checkRatchet returns `improved` BEFORE it evaluates staleness, so an entry left at 1007 while the tree measures 1006 would never expire at any age. PRIOR ENTRY: House-dialect debt in authored prose under src/ and docs/, measured by the gate this entry governs (road-to-canonical-terms 2.2). NOT a dead-root repair: the gate is new, and the debt is the corpus that predates the dialect decision recorded in src/config/canonical-terms.yml. A hard-zero gate was refused on the same ground the AI council refused an unconditional sweep on 2026-08-25. The ratchet IS the council's staged promotion: report-only for existing debt, blocking for anything new. What remains is dominated by two large-radius pairs — `artefact`->`artifact` and `behaviour`->`behavior` — plus `hand-off`->`handoff` and `licence`->`license`, the last of which the map flags `semantic-care-required` because licence TITLES are protected text. Shrink-only from here. Full classified inventory: agents/evidence/analysis/canonical-terms-inventory-2026-08-26.md.",
69
+ "count": 1003,
70
+ "landed": "2026-10-11",
71
+ "note": "Lowered 1004 -> 1003 on 2026-10-11 by feat/svg-with-a-witness, on the same shape and the same discriminator as every entry below. The branch's own new prose introduced four house-dialect occurrences under src/ and docs/ -- `artefact` in the svg-authoring See-also note, `greys` in the wireframe output line, and `colour`/`centre` in two comments the map does not even carry -- and they were rewritten CANONICAL where the diff authored them, LINE-SCOPED, because `sweep_authorised: false` is still a live constraint. Three were net-neutral and one is a genuine reduction. The gate reported the tree at 1003 against a baseline of 1004 and asked for exactly this lowering BY NAME, so the gain cannot be silently given back. LOCAL READING, ADMISSIBLE ON THE SAME DISCRIMINATOR THE ENTRIES BELOW USE: the gate walks only authored prose under src/ and docs/, and a porcelain status over those two roots reports no untracked file, so a clean CI checkout of this commit measures the same 1003. If CI reads anything else, CI's number is the correct one and this note is the record of why. `landed` is refreshed deliberately: checkRatchet returns `improved` BEFORE it evaluates staleness, so an entry left at 1004 while the tree measures 1003 would never expire at any age. PRIOR ENTRY: Lowered 1005 -> 1004 on 2026-09-13 by drain/adversarial-verification, on the same shape and the same discriminator as every entry below. That branch added a routed rule and then paid its per-spawn payload cost by migrating argument prose out of three standing rules (autonomous-execution, delegation-policy, evaluator-independence) into their context and guideline files under the P4 pattern. Nothing was deleted and nothing was swept: the migrated lines were REWRITTEN CANONICAL where the diff authored them, LINE-SCOPED, because `sweep_authorised: false` is still a live constraint. Most were net-neutral -- they replaced deleted lines carrying the same word -- and one is a genuine reduction. The gate reported the tree at 1004 against a baseline of 1005 and asked for exactly this lowering, so the gain cannot be silently given back. LOCAL READING, ADMISSIBLE ON THE SAME DISCRIMINATOR THE ENTRIES BELOW USE: the gate walks only authored prose under src/ and docs/, and a porcelain status over those two roots reported ZERO untracked files, so a clean CI checkout of this commit measures the same 1004. If CI reads anything else, CI's number is the correct one and this note is the record of why. `landed` is refreshed deliberately: checkRatchet returns `improved` BEFORE it evaluates staleness, so an entry left at 1005 while the tree measures 1004 would never expire at any age. PRIOR ENTRY: Lowered 1006 -> 1005 on 2026-09-08 by drain/graph-shipped, on the same shape as the entry below and with the same discriminator. This branch's payload migration moved a See-also line carrying `artefact` from the rule into the code-intelligence skill; keeping the word in both places would have RAISED the count, so both occurrences were written canonical LINE-SCOPED — only lines this diff authored, never a file-wide sweep. One of the two is a genuine reduction. The gate reported the tree at 1005 against a baseline of 1006 and asked for exactly this lowering. `landed` is refreshed for the reason the prior entry records: checkRatchet returns `improved` before it evaluates staleness, so an entry left high never expires. PRIOR ENTRY: Lowered 1007 -> 1006 on 2026-09-07 by drain/observed-learning-signal, and the gain was INCIDENTAL rather than sought: the branch's own new prose introduced five house-dialect occurrences under src/ and docs/ (four `artefact`/`artefacts`, one `behaviour`), which were rewritten LINE-SCOPED — only lines this diff authored, never a file-wide sweep, because `sweep_authorised: false` is still a live constraint. Four of the five were net-neutral (they replaced deleted lines carrying the same word) and one was a genuine reduction. The gate reported the tree at 1006 against a baseline of 1007 and asked for exactly this lowering, so the gain cannot be silently given back. LOCAL READING, ADMISSIBLE ON THE SAME DISCRIMINATOR THIS FILE'S check_source_size_budget ENTRY USES: the gate walks only authored prose under src/ and docs/, and `git status --porcelain -uall -- src docs` reported ZERO untracked files, so a clean CI checkout of this commit measures the same 1006. If CI reads anything else, CI's number is the correct one and this note is the record of why. `landed` is refreshed deliberately: checkRatchet returns `improved` BEFORE it evaluates staleness, so an entry left at 1007 while the tree measures 1006 would never expire at any age. PRIOR ENTRY: House-dialect debt in authored prose under src/ and docs/, measured by the gate this entry governs (road-to-canonical-terms 2.2). NOT a dead-root repair: the gate is new, and the debt is the corpus that predates the dialect decision recorded in src/config/canonical-terms.yml. A hard-zero gate was refused on the same ground the AI council refused an unconditional sweep on 2026-08-25. The ratchet IS the council's staged promotion: report-only for existing debt, blocking for anything new. What remains is dominated by two large-radius pairs — `artefact`->`artifact` and `behaviour`->`behavior` — plus `hand-off`->`handoff` and `licence`->`license`, the last of which the map flags `semantic-care-required` because licence TITLES are protected text. Shrink-only from here. Full classified inventory: agents/evidence/analysis/canonical-terms-inventory-2026-08-26.md.",
72
72
  "falsifier": "./scripts-run src/scripts/lint_canonical_terms"
73
73
  },
74
74
  "lint_handoffs": {
@@ -10,11 +10,11 @@
10
10
  },
11
11
  "seed": {
12
12
  "rules": 0.9626,
13
- "skills": 0.4147
13
+ "skills": 0.4167
14
14
  },
15
15
  "seed_provenance": {
16
16
  "rules": "103 covered of 107 routed rules on 2026-10-06; was 102 of 106 on 2026-09-13, 101 of 105 on 2026-09-08 and 94 of 105 (tier-1 21/24, tier-2 73/81) on 2026-08-25.",
17
- "skills": "124 of 299 skills carry evals/triggers.json on 2026-10-10; was 123 on 2026-10-09, 119 on 2026-10-07, 118 after the third growth of 2026-10-06, 115 after the second growth that day, 114 earlier that day, 112 on 2026-10-04, 111 on 2026-10-02, 107 on 2026-10-01, 103 earlier that day, 102 on 2026-09-30, 101 on 2026-09-19 and 100 on 2026-08-30.",
17
+ "skills": "125 of 300 skills carry evals/triggers.json on 2026-10-11; was 124 of 299 on 2026-10-10, 123 on 2026-10-09, 119 on 2026-10-07, 118 after the third growth of 2026-10-06, 115 after the second growth that day, 114 earlier that day, 112 on 2026-10-04, 111 on 2026-10-02, 107 on 2026-10-01, 103 earlier that day, 102 on 2026-09-30, 101 on 2026-09-19 and 100 on 2026-08-30.",
18
18
  "finding": "The gap between the two seeds IS the roadmap defect D1, quantified. The rules surface is ~90% covered by a deterministic corpus that can fail a PR; the skills surface -- the one production routes on -- is far lower and is covered only by a harness that is advisory only, never gating (rule_trigger_eval.ts:4). The figure is deliberately not restated here (corrected 2026-10-01): it read `25%` against a seed that had since moved to 0.3445, which is the same count-drift this file's own history entries keep repairing one field away. The live value is `seed.skills`, two keys above."
19
19
  },
20
20
  "history": [
@@ -136,6 +136,13 @@
136
136
  "from": 0.4114,
137
137
  "to": 0.4147,
138
138
  "reason": "road-to-corpus-refresh-cadence-shape step 1.2c re-checked the `database` grounding corpus against PostgreSQL 18 and MySQL 9.7 reference documentation, which edited src/skills/database/data/, and check_routing_coverage requires a corpus for every skill a diff touches (123 -> 124 of 299). Same shape as the 2026-10-01 and 2026-10-04 entries this roadmap's 1.2a and 1.2b wrote, and for the same reason: the file is the gate's own remedy rather than a coverage push. Ten cases: five exemplars including the bare 'this query is slow' phrasing the skill's own description names verbatim and one German, three near-misses -- ORM model work and a migration-only ask, both named in that skill's 'Do NOT use when' block, plus schema-review for a persistence-rule review, which it does not name and which is included because it reads as a database question -- and two counterexamples guarding the 'database' homonym (a vector store as an embedding index, and a hosting/billing question). The seed rises to the live value; it may never be LOWERED. The partition is TRAIN, not holdout: sha256('database')[0:2] is 0x35 = 53, above the holdout ceiling of 51, so BOTH published train-side measurements are re-taken here on the terms the 2026-10-04 and 2026-10-09 entries set -- routing-body-signal-verdict.json (verdict unchanged in kind, still `harmful`, false activation +6.75 pp against the +2.0 pp guard, train corpora 96 -> 97) and delivery-set-measurement-2026-08-31.json -- while the holdout pin's sealed set is untouched, because re-taking it would publish the same measurement under a newer date. ONE entry was removed from trigger_eval_grandfather.json: `database` was NOT red on main -- on main it ships no corpus at all, and check_trigger_eval_presence fires only on an allowlist entry whose skill HAS one, so its entry went stale the moment this change added the corpus. That red is created here and removed here."
139
+ },
140
+ {
141
+ "date": "2026-10-11",
142
+ "scope": "skills",
143
+ "from": 0.4147,
144
+ "to": 0.4167,
145
+ "reason": "`svg-authoring` landed as a new skill on the design family's vector side and shipped its own evals/triggers.json in the same change (124 of 299 -> 125 of 300). This is the FIRST entry on the skills scope where the DENOMINATOR moves: every entry above adds a case to a fixed 299, and this one adds a unit and a case together. The _comment names that as the ratio's other failure direction -- units added without cases -- and the repair it prescribes is the corpus file, not the seed; the file landed with the skill, so the ratio ROSE across a denominator increase (0.4147 -> 0.4167) rather than falling, which is the same shape as the 2026-09-13 and 2026-10-06 entries on the RULES scope. The seed rises to the live value; it may never be LOWERED. The partition is TRAIN, not holdout: sha256('svg-authoring')[0:2] is 0x77 = 119, above the holdout ceiling of 51, so BOTH published train-side measurements are re-taken here on the terms the 2026-10-04, 2026-10-09 and 2026-10-10 entries set. routing-body-signal-verdict.json: verdict unchanged in kind, still `harmful`, false activation +7.50 pp against the +2.0 pp guard, delta-recall 2.360 -> 3.383 pp (still below the 5.0 pp bar), train corpora 97 -> 98, catalogue 299 -> 300. The holdout pin IS touched, for two reasons rather than one: svg-authoring adds a train row, and the same branch EDITED image-provider-routing's existing train corpus -- it splits the vector job shapes between a provider and hand-authoring -- which moves that row's hash while moving no count at all, the case a reader counting files would miss. The sealed holdout set is the same 27 rows and byte-identical, so the ordering claim is untouched. NO entry was removed from trigger_eval_grandfather.json: the skill is new, so it was never allowlisted, and check_trigger_eval_presence fires only on an entry whose skill HAS a corpus."
139
146
  }
140
147
  ]
141
148
  }