@adia-ai/adia-ui-factory 0.2.2 → 0.2.3

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 (94) hide show
  1. package/.claude-plugin/plugin.json +12 -3
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +24 -1
  4. package/bin/adia-scaffold +93 -13
  5. package/package.json +2 -2
  6. package/references/shell-embed.md +10 -0
  7. package/references/spa-architecture.md +7 -0
  8. package/skills/adia-ui-kit/CHANGELOG.md +41 -0
  9. package/skills/adia-ui-kit/SKILL.md +442 -0
  10. package/skills/adia-ui-kit/assets/figma-make/guidelines/Guidelines.md +87 -0
  11. package/skills/adia-ui-kit/assets/figma-make/guidelines/components.md +142 -0
  12. package/skills/adia-ui-kit/assets/figma-make/guidelines/styles.md +118 -0
  13. package/skills/adia-ui-kit/assets/figma-make/guidelines/tokens.md +120 -0
  14. package/skills/adia-ui-kit/assets/team-scaffold/archive-README.md +30 -0
  15. package/skills/adia-ui-kit/assets/team-scaffold/drafts-README.md +57 -0
  16. package/skills/adia-ui-kit/assets/team-scaffold/inbox-README.md +76 -0
  17. package/skills/adia-ui-kit/assets/team-scaffold/outbox-README.md +46 -0
  18. package/skills/adia-ui-kit/assets/team-scaffold/team-root.README.md +51 -0
  19. package/skills/adia-ui-kit/assets/team-scaffold/tickets-README.md +208 -0
  20. package/skills/adia-ui-kit/assets/team-scaffold/tickets-REGISTRY.md +81 -0
  21. package/skills/adia-ui-kit/assets/templates/AGENTS.template.md +128 -0
  22. package/skills/adia-ui-kit/assets/templates/CLAUDE.template.md +1 -0
  23. package/skills/adia-ui-kit/assets/templates/FEEDBACK-OMISSIONS-DEFENSE.template.md +150 -0
  24. package/skills/adia-ui-kit/assets/templates/FEEDBACK.template.md +213 -0
  25. package/skills/adia-ui-kit/assets/templates/RESPONSE.template.md +114 -0
  26. package/skills/adia-ui-kit/assets/templates/figma-make-guidelines.md +90 -0
  27. package/skills/adia-ui-kit/evals/adversarial-content-trust.json +83 -0
  28. package/skills/adia-ui-kit/evals/evals.json +55 -0
  29. package/skills/adia-ui-kit/evals/routing-corpus.json +121 -0
  30. package/skills/adia-ui-kit/evals/teach-routing-cases.json +92 -0
  31. package/skills/adia-ui-kit/references/capabilities.md +245 -0
  32. package/skills/adia-ui-kit/references/catalog-lookup.md +186 -0
  33. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v1.md +8 -0
  34. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v2.md +3585 -0
  35. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v3.md +1899 -0
  36. package/skills/adia-ui-kit/references/common-gotchas-consumer.md +522 -0
  37. package/skills/adia-ui-kit/references/component-literacy.md +291 -0
  38. package/skills/adia-ui-kit/references/composition.md +245 -0
  39. package/skills/adia-ui-kit/references/correction-loop.md +462 -0
  40. package/skills/adia-ui-kit/references/css-policy.md +343 -0
  41. package/skills/adia-ui-kit/references/decision-model.md +158 -0
  42. package/skills/adia-ui-kit/references/diagnostic-output.md +57 -0
  43. package/skills/adia-ui-kit/references/docs-catalog.md +239 -0
  44. package/skills/adia-ui-kit/references/exclusions.md +225 -0
  45. package/skills/adia-ui-kit/references/feedback-authoring.md +466 -0
  46. package/skills/adia-ui-kit/references/figma-make.md +244 -0
  47. package/skills/adia-ui-kit/references/flows-auth.md +307 -0
  48. package/skills/adia-ui-kit/references/flows-content.md +206 -0
  49. package/skills/adia-ui-kit/references/flows-data.md +348 -0
  50. package/skills/adia-ui-kit/references/flows-interaction.md +181 -0
  51. package/skills/adia-ui-kit/references/flows-shells.md +336 -0
  52. package/skills/adia-ui-kit/references/gap-detection.md +266 -0
  53. package/skills/adia-ui-kit/references/harness.md +284 -0
  54. package/skills/adia-ui-kit/references/loading-protocol.md +406 -0
  55. package/skills/adia-ui-kit/references/local-link.md +198 -0
  56. package/skills/adia-ui-kit/references/mcp.md +401 -0
  57. package/skills/adia-ui-kit/references/migration.md +116 -0
  58. package/skills/adia-ui-kit/references/mission.md +194 -0
  59. package/skills/adia-ui-kit/references/mode-routing.md +298 -0
  60. package/skills/adia-ui-kit/references/patterns-agent.md +223 -0
  61. package/skills/adia-ui-kit/references/patterns-data.md +1066 -0
  62. package/skills/adia-ui-kit/references/patterns-forms.md +223 -0
  63. package/skills/adia-ui-kit/references/patterns-layout.md +326 -0
  64. package/skills/adia-ui-kit/references/patterns-recipes.md +1416 -0
  65. package/skills/adia-ui-kit/references/patterns-shells.md +1097 -0
  66. package/skills/adia-ui-kit/references/patterns-traits.md +259 -0
  67. package/skills/adia-ui-kit/references/patterns-wiring.md +339 -0
  68. package/skills/adia-ui-kit/references/pev-rationale.md +137 -0
  69. package/skills/adia-ui-kit/references/reasoning-gate.md +146 -0
  70. package/skills/adia-ui-kit/references/reasoning-methodologies.md +112 -0
  71. package/skills/adia-ui-kit/references/recon.md +192 -0
  72. package/skills/adia-ui-kit/references/rendering-model.md +431 -0
  73. package/skills/adia-ui-kit/references/response-authoring.md +398 -0
  74. package/skills/adia-ui-kit/references/session-log-schema.md +203 -0
  75. package/skills/adia-ui-kit/references/session-log-template.md +118 -0
  76. package/skills/adia-ui-kit/references/setup.md +652 -0
  77. package/skills/adia-ui-kit/references/skill-hygiene.md +135 -0
  78. package/skills/adia-ui-kit/references/spec-to-ui-reasoning.md +394 -0
  79. package/skills/adia-ui-kit/references/state.md +229 -0
  80. package/skills/adia-ui-kit/references/teach-protocol.md +421 -0
  81. package/skills/adia-ui-kit/references/theme.md +142 -0
  82. package/skills/adia-ui-kit/references/version-check.md +69 -0
  83. package/skills/adia-ui-kit/scripts/audit-decision-trace.mjs +198 -0
  84. package/skills/adia-ui-kit/scripts/audit-kit-roster.mjs +366 -0
  85. package/skills/adia-ui-kit/scripts/changelog-archive.mjs +126 -0
  86. package/skills/adia-ui-kit/scripts/check-adia-versions.py +404 -0
  87. package/skills/adia-ui-kit/scripts/run-skill-evals.mjs +358 -0
  88. package/skills/adia-ui-kit/scripts/validate-session-log.mjs +242 -0
  89. package/skills/adia-ui-kit/skill.json +102 -0
  90. package/skills/adia-ui-migration/CHANGELOG.md +47 -0
  91. package/skills/adia-ui-migration/SKILL.md +381 -0
  92. package/skills/adia-ui-migration/skill.json +29 -0
  93. package/skills/adia-ui-spa/SKILL.md +1 -1
  94. package/skills/adia-ui-verify/SKILL.md +2 -2
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * run-skill-evals.mjs (bundled — adia-ui-kit v2.30.0+)
4
+ *
5
+ * Portable eval runner for adia-ui-kit. Runs three bundled corpora
6
+ * against the kit:
7
+ *
8
+ * 1. evals/routing-corpus.json
9
+ * Routing-accuracy corpus (10 trigger phrases + 5 adversarial).
10
+ * Scores kit's description + trigger frontmatter via TF-IDF token
11
+ * overlap. Trigger phrases SHOULD activate kit; adversarial
12
+ * phrases should NOT.
13
+ *
14
+ * 2. evals/evals.json
15
+ * Behavioral-eval corpus (instruction-following + composition
16
+ * checks). Manually reviewable for now; the runner reports
17
+ * counts + structure only.
18
+ *
19
+ * 3. evals/adversarial-content-trust.json
20
+ * Content-trust adversarial corpus (5 injection cases). Manually
21
+ * reviewable — the runner reports case count + structure only;
22
+ * actual PASS/FAIL requires presenting each case to a fresh
23
+ * agent session and observing behavior. See REFACTOR-SPEC §10
24
+ * OQ2 — promotion to a CI gate happens after 3 months of stable
25
+ * PASS rates.
26
+ *
27
+ * Cross-skill mode (substrate-only)
28
+ * When invoked from inside the chat-ui substrate (detected by
29
+ * presence of `scripts/skills/run-skill-evals.mjs`), this runner
30
+ * delegates the routing eval to the substrate's cross-skill version
31
+ * for higher fidelity (compares kit against all 5 senior skills).
32
+ *
33
+ * Standalone mode (consumer-repo default)
34
+ * When invoked from a consumer repo (no substrate runner detected),
35
+ * this runner scores kit alone — does the phrase activate kit at
36
+ * all (score ≥ MIN_SCORE_THRESHOLD)?
37
+ *
38
+ * Usage:
39
+ * # From the kit itself:
40
+ * node scripts/run-skill-evals.mjs
41
+ *
42
+ * # From a consumer repo (skill installed as a dep):
43
+ * cd <consumer-repo>
44
+ * node ./node_modules/adia-ui-kit/scripts/run-skill-evals.mjs --kit-dir=./node_modules/adia-ui-kit
45
+ *
46
+ * # JSON output:
47
+ * node run-skill-evals.mjs --json
48
+ *
49
+ * # Strict mode — exit 1 if routing F1 < 0.85:
50
+ * node run-skill-evals.mjs --strict
51
+ *
52
+ * Per REFACTOR-SPEC P3.
53
+ */
54
+
55
+ import fs from 'node:fs';
56
+ import path from 'node:path';
57
+ import process from 'node:process';
58
+ import { fileURLToPath } from 'node:url';
59
+ import * as childProcess from 'node:child_process';
60
+
61
+ const __filename = fileURLToPath(import.meta.url);
62
+ const SCRIPT_DIR = path.dirname(__filename);
63
+ // Default kit-dir is the parent of scripts/ (i.e. SKILL.md's dir).
64
+ // Override with --kit-dir=<path> when running from outside the kit.
65
+ const KIT_FLAG = process.argv.find(a => a.startsWith('--kit-dir='));
66
+ const KIT_DIR = KIT_FLAG
67
+ ? path.resolve(KIT_FLAG.slice('--kit-dir='.length))
68
+ : path.dirname(SCRIPT_DIR);
69
+
70
+ const JSON_MODE = process.argv.includes('--json');
71
+ const STRICT_MODE = process.argv.includes('--strict');
72
+ const STRICT_F1_FLOOR = 0.85;
73
+
74
+ const MIN_SCORE_THRESHOLD = 2.0; // matches substrate runner
75
+ const MIN_MATCH_COUNT = 2;
76
+
77
+ // ─────────────────────────────────────────────────────────────────
78
+ // Token overlap scorer (compatible with substrate runner)
79
+ // ─────────────────────────────────────────────────────────────────
80
+
81
+ const STOPWORDS = new Set([
82
+ 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'do', 'for',
83
+ 'from', 'has', 'have', 'i', 'if', 'in', 'is', 'it', 'just', 'me', 'my',
84
+ 'no', 'not', 'of', 'on', 'or', 'our', 's', 't', 'that', 'the', 'this',
85
+ 'to', 'too', 'us', 'use', 'we', 'what', 'with', 'you', 'your', 'how',
86
+ 'why', 'when', 'where', 'which', 'who', 'will', 'would', 'could', 'should',
87
+ 'can', 'may', 'might', 'shall', 'must', 'about', 'into', 'over', 'than',
88
+ 'up', 'down', 'out', 'off',
89
+ ]);
90
+
91
+ function tokenize(text) {
92
+ return new Set(
93
+ text
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9-/]+/g, ' ')
96
+ .split(/\s+/)
97
+ .filter(t => t.length >= 2 && !STOPWORDS.has(t))
98
+ );
99
+ }
100
+
101
+ function loadKitFrontmatter(kitDir) {
102
+ const skillPath = path.join(kitDir, 'SKILL.md');
103
+ if (!fs.existsSync(skillPath)) {
104
+ throw new Error(`SKILL.md not found at ${skillPath}. Use --kit-dir=<path> to point at the kit's root.`);
105
+ }
106
+ const raw = fs.readFileSync(skillPath, 'utf8');
107
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
108
+ if (!fmMatch) {
109
+ throw new Error(`No YAML frontmatter found in ${skillPath}.`);
110
+ }
111
+ const fm = fmMatch[1];
112
+ // Extract description (single-line or > folded) + trigger block.
113
+ // We're not running a full YAML parser — keep it minimal.
114
+ const descMatch = fm.match(/^description:\s*(?:>-?\s*\n((?: [^\n]*\n?)+)|([^\n]+))/m);
115
+ const trigMatch = fm.match(/^trigger:\s*(?:>-?\s*\n((?: [^\n]*\n?)+)|([^\n]+))/m);
116
+ const description = (descMatch?.[1] || descMatch?.[2] || '').trim();
117
+ const trigger = (trigMatch?.[1] || trigMatch?.[2] || '').trim();
118
+ return { description, trigger, combined: `${description} ${trigger}` };
119
+ }
120
+
121
+ function scorePhrase(phrase, kitTokens, kitTextTokens) {
122
+ const phraseTokens = [...tokenize(phrase)];
123
+ let score = 0;
124
+ let matchCount = 0;
125
+ const matchedTokens = [];
126
+ for (const t of phraseTokens) {
127
+ if (kitTokens.has(t)) {
128
+ // Simple IDF approximation — uncommon tokens score higher.
129
+ // In standalone mode we don't have a corpus-wide IDF, so use 1.0
130
+ // for any matched token. The substrate runner does real TF-IDF.
131
+ score += 1.0;
132
+ matchCount += 1;
133
+ matchedTokens.push(t);
134
+ }
135
+ }
136
+ const activates = score >= MIN_SCORE_THRESHOLD && matchCount >= MIN_MATCH_COUNT;
137
+ return { score, matchCount, matchedTokens, activates };
138
+ }
139
+
140
+ // ─────────────────────────────────────────────────────────────────
141
+ // Eval 1 — routing corpus
142
+ // ─────────────────────────────────────────────────────────────────
143
+
144
+ function runRoutingEval(kitDir) {
145
+ const corpusPath = path.join(kitDir, 'evals/routing-corpus.json');
146
+ if (!fs.existsSync(corpusPath)) {
147
+ return { error: `routing-corpus.json not found at ${corpusPath}`, skipped: true };
148
+ }
149
+ const corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
150
+ const { combined } = loadKitFrontmatter(kitDir);
151
+ const kitTokens = tokenize(combined);
152
+
153
+ const results = corpus.phrases.map(p => {
154
+ const { score, matchCount, matchedTokens, activates } = scorePhrase(p.phrase, kitTokens);
155
+ const expected = p.should_route_to_kit;
156
+ const pass = activates === expected;
157
+ return { ...p, score, matchCount, matchedTokens, activates, pass };
158
+ });
159
+
160
+ const tp = results.filter(r => r.should_route_to_kit && r.activates).length;
161
+ const fp = results.filter(r => !r.should_route_to_kit && r.activates).length;
162
+ const fn = results.filter(r => r.should_route_to_kit && !r.activates).length;
163
+ const tn = results.filter(r => !r.should_route_to_kit && !r.activates).length;
164
+
165
+ const precision = tp + fp === 0 ? 0 : tp / (tp + fp);
166
+ const recall = tp + fn === 0 ? 0 : tp / (tp + fn);
167
+ const f1 = precision + recall === 0 ? 0 : (2 * precision * recall) / (precision + recall);
168
+
169
+ return { results, tp, fp, fn, tn, precision, recall, f1, total: results.length };
170
+ }
171
+
172
+ // ─────────────────────────────────────────────────────────────────
173
+ // Eval 2 — behavioral corpus (structural-only report)
174
+ // ─────────────────────────────────────────────────────────────────
175
+
176
+ function runBehavioralEval(kitDir) {
177
+ const corpusPath = path.join(kitDir, 'evals/evals.json');
178
+ if (!fs.existsSync(corpusPath)) {
179
+ return { error: `evals.json not found at ${corpusPath}`, skipped: true };
180
+ }
181
+ const corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
182
+ // evals.json shape varies; report what we find.
183
+ const cases = corpus.cases || corpus.tests || corpus.examples || corpus.evals || [];
184
+ return {
185
+ case_count: Array.isArray(cases) ? cases.length : 0,
186
+ note: 'Behavioral evals are structurally counted only — actual PASS/FAIL requires running each case against a fresh agent session.',
187
+ };
188
+ }
189
+
190
+ // ─────────────────────────────────────────────────────────────────
191
+ // Eval 3 — adversarial content-trust (structural-only report)
192
+ // ─────────────────────────────────────────────────────────────────
193
+
194
+ function runAdversarialEval(kitDir) {
195
+ const corpusPath = path.join(kitDir, 'evals/adversarial-content-trust.json');
196
+ if (!fs.existsSync(corpusPath)) {
197
+ return { error: `adversarial-content-trust.json not found at ${corpusPath}`, skipped: true };
198
+ }
199
+ const corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
200
+ const cases = corpus.cases || [];
201
+ const severities = cases.reduce((acc, c) => {
202
+ acc[c.severity || 'unknown'] = (acc[c.severity || 'unknown'] || 0) + 1;
203
+ return acc;
204
+ }, {});
205
+ return {
206
+ case_count: cases.length,
207
+ severities,
208
+ note: 'Adversarial content-trust evals are structurally counted only — actual PASS/FAIL requires presenting each case to a fresh agent session and observing behavior. See REFACTOR-SPEC §10 OQ2.',
209
+ };
210
+ }
211
+
212
+ // ─────────────────────────────────────────────────────────────────
213
+ // Eval 4 — §Teach routing (mechanical, scored against teach-route.mjs)
214
+ // ─────────────────────────────────────────────────────────────────
215
+
216
+ function runTeachRoutingEval(kitDir) {
217
+ const corpusPath = path.join(kitDir, 'evals/teach-routing-cases.json');
218
+ const scriptPath = path.join(kitDir, 'scripts/teach-route.mjs');
219
+ if (!fs.existsSync(corpusPath)) {
220
+ return { error: `teach-routing-cases.json not found at ${corpusPath}`, skipped: true };
221
+ }
222
+ if (!fs.existsSync(scriptPath)) {
223
+ return { error: `teach-route.mjs not found at ${scriptPath}`, skipped: true };
224
+ }
225
+ // Run teach-route in --eval mode and parse the Pass: X/Y summary line.
226
+ try {
227
+ const { execSync } = childProcess;
228
+ const out = execSync(`node "${scriptPath}" --eval="${corpusPath}"`, { encoding: 'utf8' });
229
+ const m = out.match(/Pass:\s*(\d+)\/(\d+)/);
230
+ if (m) {
231
+ const passed = parseInt(m[1], 10);
232
+ const total = parseInt(m[2], 10);
233
+ return { passed, total, pass_rate: total === 0 ? 0 : passed / total };
234
+ }
235
+ return { error: 'Could not parse teach-route eval output', raw: out.slice(0, 200) };
236
+ } catch (err) {
237
+ return { error: `teach-route eval failed: ${err.message}` };
238
+ }
239
+ }
240
+
241
+ // ─────────────────────────────────────────────────────────────────
242
+ // Substrate cross-skill mode
243
+ // ─────────────────────────────────────────────────────────────────
244
+
245
+ function detectSubstrate(cwd) {
246
+ // We're in the substrate if scripts/skills/run-skill-evals.mjs exists
247
+ // at cwd (or any ancestor).
248
+ let dir = cwd;
249
+ while (dir !== path.dirname(dir)) {
250
+ if (fs.existsSync(path.join(dir, 'scripts/skills/run-skill-evals.mjs'))) {
251
+ return dir;
252
+ }
253
+ dir = path.dirname(dir);
254
+ }
255
+ return null;
256
+ }
257
+
258
+ // ─────────────────────────────────────────────────────────────────
259
+ // Main
260
+ // ─────────────────────────────────────────────────────────────────
261
+
262
+ function main() {
263
+ const substrate = detectSubstrate(process.cwd());
264
+ const mode = substrate ? 'substrate (kit-only standalone scoring)' : 'standalone';
265
+
266
+ const routing = runRoutingEval(KIT_DIR);
267
+ const behavioral = runBehavioralEval(KIT_DIR);
268
+ const adversarial = runAdversarialEval(KIT_DIR);
269
+ const teachRouting = runTeachRoutingEval(KIT_DIR);
270
+
271
+ const summary = {
272
+ runner_version: '1.1.0',
273
+ runner_mode: mode,
274
+ kit_dir: KIT_DIR,
275
+ substrate_detected: substrate ? path.relative(process.cwd(), substrate) || '.' : null,
276
+ routing,
277
+ behavioral,
278
+ adversarial,
279
+ teach_routing: teachRouting,
280
+ };
281
+
282
+ if (JSON_MODE) {
283
+ console.log(JSON.stringify(summary, null, 2));
284
+ } else {
285
+ printHumanReport(summary);
286
+ }
287
+
288
+ if (STRICT_MODE && routing.f1 < STRICT_F1_FLOOR) {
289
+ console.error(`\n✗ STRICT mode: routing F1 ${routing.f1.toFixed(2)} < ${STRICT_F1_FLOOR}`);
290
+ process.exit(1);
291
+ }
292
+ if (STRICT_MODE && teachRouting && !teachRouting.skipped && !teachRouting.error && teachRouting.pass_rate < 1.0) {
293
+ console.error(`\n✗ STRICT mode: teach-routing pass rate ${(teachRouting.pass_rate * 100).toFixed(1)}% < 100%`);
294
+ process.exit(1);
295
+ }
296
+ process.exit(0);
297
+ }
298
+
299
+ function printHumanReport(summary) {
300
+ console.log(`\n══ adia-ui-kit bundled eval runner ══`);
301
+ console.log(`Mode: ${summary.runner_mode}`);
302
+ console.log(`Kit dir: ${summary.kit_dir}`);
303
+ if (summary.substrate_detected) console.log(`Substrate detected at: ${summary.substrate_detected}`);
304
+ console.log();
305
+
306
+ console.log(`─── Routing corpus (10 trigger + 5 adversarial) ───`);
307
+ if (summary.routing.error) {
308
+ console.log(` ✗ ${summary.routing.error}`);
309
+ } else {
310
+ for (const r of summary.routing.results) {
311
+ const mark = r.pass ? '✓' : '✗';
312
+ const expected = r.should_route_to_kit ? 'should activate' : 'should NOT activate';
313
+ const got = r.activates ? 'activated' : 'did not activate';
314
+ console.log(` ${mark} [${r.id}] "${r.phrase}"`);
315
+ console.log(` expected ${expected}; ${got} (score=${r.score.toFixed(1)} matches=${r.matchCount} [${r.matchedTokens.slice(0, 5).join(', ')}${r.matchedTokens.length > 5 ? '...' : ''}])`);
316
+ }
317
+ console.log();
318
+ console.log(` TP=${summary.routing.tp} FP=${summary.routing.fp} FN=${summary.routing.fn} TN=${summary.routing.tn}`);
319
+ console.log(` Precision: ${(summary.routing.precision * 100).toFixed(1)}%`);
320
+ console.log(` Recall: ${(summary.routing.recall * 100).toFixed(1)}%`);
321
+ console.log(` F1: ${(summary.routing.f1 * 100).toFixed(1)}%`);
322
+ }
323
+ console.log();
324
+
325
+ console.log(`─── Behavioral evals ───`);
326
+ if (summary.behavioral.error) {
327
+ console.log(` ✗ ${summary.behavioral.error}`);
328
+ } else {
329
+ console.log(` Cases: ${summary.behavioral.case_count}`);
330
+ console.log(` ${summary.behavioral.note}`);
331
+ }
332
+ console.log();
333
+
334
+ console.log(`─── Adversarial content-trust evals ───`);
335
+ if (summary.adversarial.error) {
336
+ console.log(` ✗ ${summary.adversarial.error}`);
337
+ } else {
338
+ console.log(` Cases: ${summary.adversarial.case_count}`);
339
+ const sev = summary.adversarial.severities;
340
+ console.log(` Severities: ${Object.entries(sev).map(([k, v]) => `${k}=${v}`).join(', ')}`);
341
+ console.log(` ${summary.adversarial.note}`);
342
+ }
343
+ console.log();
344
+
345
+ console.log(`─── §Teach routing evals (mechanical, scored against teach-route.mjs) ───`);
346
+ const tr = summary.teach_routing;
347
+ if (!tr) {
348
+ console.log(` (skipped — runner version mismatch)`);
349
+ } else if (tr.skipped || tr.error) {
350
+ console.log(` ✗ ${tr.error || 'skipped'}`);
351
+ } else {
352
+ const pct = (tr.pass_rate * 100).toFixed(1);
353
+ const icon = tr.pass_rate >= 1.0 ? '✓' : tr.pass_rate >= 0.8 ? '⚠' : '✗';
354
+ console.log(` ${icon} Pass: ${tr.passed}/${tr.total} (${pct}%)`);
355
+ }
356
+ }
357
+
358
+ main();
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-session-log.mjs (adia-ui-kit v3.1.0+)
4
+ *
5
+ * Lints a JSONL session log against the schema documented in
6
+ * `references/session-log-schema.md`.
7
+ *
8
+ * Per REFACTOR-SPEC P5(a).
9
+ *
10
+ * Usage:
11
+ * node validate-session-log.mjs <path-to-log.jsonl>
12
+ * node validate-session-log.mjs <path-to-log.jsonl> --strict
13
+ * node validate-session-log.mjs <path-to-log.jsonl> --json
14
+ *
15
+ * Default mode: prints per-line findings + final summary; exits 0 if no
16
+ * findings, 1 if any findings.
17
+ *
18
+ * Strict mode: fails on any warning OR error (default fails only on
19
+ * errors — warnings are missing-recommended-field hints).
20
+ *
21
+ * JSON mode: prints `{ summary, findings }` JSON for programmatic use.
22
+ */
23
+
24
+ import fs from 'node:fs';
25
+ import process from 'node:process';
26
+
27
+ const SCHEMA_VERSION = '1';
28
+
29
+ const REQUIRED_FIELDS = ['schema_version', 'session_id', 'timestamp', 'skill', 'skill_version', 'event_type', 'payload'];
30
+
31
+ const KNOWN_EVENT_TYPES = new Set([
32
+ 'skill_loaded',
33
+ 'mode_entered',
34
+ 'decision_made',
35
+ 'component_named',
36
+ 'verify_target_run',
37
+ 'correction_applied',
38
+ 'teach_landing',
39
+ 'session_closed',
40
+ ]);
41
+
42
+ // Per-event-type required payload fields
43
+ const PAYLOAD_REQUIREMENTS = {
44
+ skill_loaded: ['trigger'],
45
+ mode_entered: ['mode_id', 'mode_name'],
46
+ decision_made: ['decision', 'required_signal', 'possible_actions', 'chosen_action'],
47
+ component_named: ['tag'],
48
+ verify_target_run: ['verify_target', 'exit_code'],
49
+ correction_applied: ['feedback_ticket', 'skill_section'],
50
+ teach_landing: ['input_payload', 'branch', 'landing_target', 'confidence'],
51
+ session_closed: ['outcome'],
52
+ };
53
+
54
+ const KNOWN_BRANCHES = new Set(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'NONE']);
55
+ const KNOWN_CONFIDENCES = new Set(['high', 'medium', 'low', 'none']);
56
+ const KNOWN_OUTCOMES = new Set(['completed', 'interrupted', 'error']);
57
+
58
+ function validateLine(obj, lineNum) {
59
+ const findings = [];
60
+ const error = (msg) => findings.push({ line: lineNum, severity: 'error', msg });
61
+ const warn = (msg) => findings.push({ line: lineNum, severity: 'warning', msg });
62
+
63
+ // Required top-level fields
64
+ for (const field of REQUIRED_FIELDS) {
65
+ if (!(field in obj)) {
66
+ error(`Missing required field: ${field}`);
67
+ }
68
+ }
69
+
70
+ // Schema version
71
+ if (obj.schema_version && obj.schema_version !== SCHEMA_VERSION) {
72
+ warn(`schema_version="${obj.schema_version}" — validator only knows v${SCHEMA_VERSION}`);
73
+ }
74
+
75
+ // Timestamp validation (ISO 8601, UTC)
76
+ if (obj.timestamp) {
77
+ if (typeof obj.timestamp !== 'string') {
78
+ error(`timestamp must be a string, got ${typeof obj.timestamp}`);
79
+ } else if (!obj.timestamp.endsWith('Z')) {
80
+ error(`timestamp must be UTC (end with Z): "${obj.timestamp}"`);
81
+ } else {
82
+ const d = new Date(obj.timestamp);
83
+ if (Number.isNaN(d.getTime())) {
84
+ error(`timestamp is not valid ISO 8601: "${obj.timestamp}"`);
85
+ }
86
+ }
87
+ }
88
+
89
+ // session_id sanity
90
+ if (obj.session_id && (typeof obj.session_id !== 'string' || obj.session_id.length < 4)) {
91
+ error(`session_id must be a non-trivial string`);
92
+ }
93
+
94
+ // event_type validity
95
+ if (obj.event_type) {
96
+ if (!KNOWN_EVENT_TYPES.has(obj.event_type)) {
97
+ error(`Unknown event_type "${obj.event_type}". Known: ${[...KNOWN_EVENT_TYPES].join(', ')}`);
98
+ }
99
+ }
100
+
101
+ // Payload requirements per event_type
102
+ if (obj.event_type && obj.payload && PAYLOAD_REQUIREMENTS[obj.event_type]) {
103
+ const required = PAYLOAD_REQUIREMENTS[obj.event_type];
104
+ for (const field of required) {
105
+ if (!(field in obj.payload)) {
106
+ error(`event_type=${obj.event_type} missing required payload field: ${field}`);
107
+ }
108
+ }
109
+ }
110
+
111
+ // Event-specific value validation
112
+ if (obj.event_type === 'teach_landing' && obj.payload) {
113
+ if (obj.payload.branch && !KNOWN_BRANCHES.has(obj.payload.branch)) {
114
+ error(`teach_landing.branch="${obj.payload.branch}" — must be one of A-G or NONE`);
115
+ }
116
+ if (obj.payload.confidence && !KNOWN_CONFIDENCES.has(obj.payload.confidence)) {
117
+ error(`teach_landing.confidence="${obj.payload.confidence}" — must be high/medium/low/none`);
118
+ }
119
+ }
120
+
121
+ if (obj.event_type === 'session_closed' && obj.payload) {
122
+ if (obj.payload.outcome && !KNOWN_OUTCOMES.has(obj.payload.outcome)) {
123
+ error(`session_closed.outcome="${obj.payload.outcome}" — must be completed/interrupted/error`);
124
+ }
125
+ if (obj.payload.summary && obj.payload.summary.length > 500) {
126
+ warn(`session_closed.summary > 500 chars (${obj.payload.summary.length}) — consider trimming`);
127
+ }
128
+ }
129
+
130
+ if (obj.event_type === 'decision_made' && obj.payload) {
131
+ if (obj.payload.possible_actions && !Array.isArray(obj.payload.possible_actions)) {
132
+ error(`decision_made.possible_actions must be an array`);
133
+ }
134
+ if (obj.payload.possible_actions && Array.isArray(obj.payload.possible_actions)
135
+ && obj.payload.possible_actions.length < 2) {
136
+ warn(`decision_made.possible_actions has < 2 entries — a "decision" with only one option isn't really a decision`);
137
+ }
138
+ if (obj.payload.possible_actions && obj.payload.chosen_action
139
+ && Array.isArray(obj.payload.possible_actions)
140
+ && !obj.payload.possible_actions.includes(obj.payload.chosen_action)) {
141
+ error(`decision_made.chosen_action "${obj.payload.chosen_action}" is not in possible_actions`);
142
+ }
143
+ }
144
+
145
+ return findings;
146
+ }
147
+
148
+ function validateFile(path) {
149
+ if (!fs.existsSync(path)) {
150
+ return { error: `File not found: ${path}`, findings: [], lineCount: 0 };
151
+ }
152
+ const content = fs.readFileSync(path, 'utf8');
153
+ const lines = content.split('\n').filter(l => l.trim().length > 0);
154
+ const allFindings = [];
155
+ let parsedCount = 0;
156
+ const eventTypeCounts = {};
157
+ let sessionIds = new Set();
158
+
159
+ lines.forEach((line, idx) => {
160
+ const lineNum = idx + 1;
161
+ let obj;
162
+ try {
163
+ obj = JSON.parse(line);
164
+ } catch (err) {
165
+ allFindings.push({ line: lineNum, severity: 'error', msg: `Invalid JSON: ${err.message}` });
166
+ return;
167
+ }
168
+ parsedCount += 1;
169
+ if (obj.event_type) {
170
+ eventTypeCounts[obj.event_type] = (eventTypeCounts[obj.event_type] || 0) + 1;
171
+ }
172
+ if (obj.session_id) {
173
+ sessionIds.add(obj.session_id);
174
+ }
175
+ const findings = validateLine(obj, lineNum);
176
+ allFindings.push(...findings);
177
+ });
178
+
179
+ const errors = allFindings.filter(f => f.severity === 'error').length;
180
+ const warnings = allFindings.filter(f => f.severity === 'warning').length;
181
+
182
+ return {
183
+ path,
184
+ lineCount: lines.length,
185
+ parsedCount,
186
+ errors,
187
+ warnings,
188
+ sessions: sessionIds.size,
189
+ event_type_counts: eventTypeCounts,
190
+ findings: allFindings,
191
+ };
192
+ }
193
+
194
+ function main() {
195
+ const args = process.argv.slice(2);
196
+ const json = args.includes('--json');
197
+ const strict = args.includes('--strict');
198
+ const path = args.find(a => !a.startsWith('--'));
199
+
200
+ if (!path) {
201
+ console.error('Usage: validate-session-log.mjs <path-to-log.jsonl> [--strict] [--json]');
202
+ process.exit(2);
203
+ }
204
+
205
+ const result = validateFile(path);
206
+
207
+ if (result.error) {
208
+ console.error(`✗ ${result.error}`);
209
+ process.exit(1);
210
+ }
211
+
212
+ if (json) {
213
+ console.log(JSON.stringify(result, null, 2));
214
+ } else {
215
+ console.log(`\n══ validate-session-log — ${result.path} ══`);
216
+ console.log(` Lines: ${result.lineCount}`);
217
+ console.log(` Parsed: ${result.parsedCount}`);
218
+ console.log(` Sessions: ${result.sessions}`);
219
+ console.log(` Event types:`);
220
+ for (const [et, count] of Object.entries(result.event_type_counts)) {
221
+ console.log(` ${et}: ${count}`);
222
+ }
223
+ console.log(` Errors: ${result.errors}`);
224
+ console.log(` Warnings: ${result.warnings}`);
225
+
226
+ if (result.findings.length > 0) {
227
+ console.log(`\nFindings:`);
228
+ for (const f of result.findings) {
229
+ const icon = f.severity === 'error' ? '✗' : '⚠';
230
+ console.log(` ${icon} line ${f.line}: [${f.severity}] ${f.msg}`);
231
+ }
232
+ } else {
233
+ console.log(`\n✓ No findings.`);
234
+ }
235
+ }
236
+
237
+ if (result.errors > 0) process.exit(1);
238
+ if (strict && result.warnings > 0) process.exit(1);
239
+ process.exit(0);
240
+ }
241
+
242
+ main();