Files
krow_backend/scripts/oracle.mjs
2026-08-28 12:21:44 +05:30

201 lines
7.7 KiB
JavaScript

/**
* The JS parser, as an oracle.
*
* Runs the REAL frontend module graph through Vite — `import.meta.glob`, the
* `@/` alias and raw Markdown loading behave exactly as they do in the app, the
* same technique the frontend's own `scripts/skill-check.mjs` uses (that file
* lives in krow-demo, not here). A mock of the registry would reproduce none of
* the behaviour this file exists to capture.
*
* Emits one JSON document: for every shipped definition and every adversarial
* case, what the JS parser did with it. That document is the fixture the Go
* conformance suite asserts against, so "the Go parser agrees with the JS
* parser" is a comparison against the JS parser's actual output rather than
* against anybody's description of it.
*/
import { readFileSync, readdirSync, writeFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
/* The frontend checkout. Overridable so this runs anywhere the two repos are
checked out side by side, which is the layout it defaults to. */
/* fileURLToPath, not .pathname: a URL percent-encodes, so a checkout under a
directory with a space in it resolved to "/Users/.../Krow%20Project%20/..."
— a path that does not exist. Vite then started with a root pointing nowhere
and failed on the first import, which reads as a missing source file rather
than a broken path. The effect was that this script could not run at all on
such a checkout, and the conformance test went on passing against whatever
oracle happened to be committed. */
const FRONTEND = process.env.KROW_FRONTEND
|| fileURLToPath(new URL('../../krow-demo', import.meta.url));
const { createServer } = await import(join(FRONTEND, 'node_modules/vite/dist/node/index.js'));
const { CASES } = await import(new URL('./cases.mjs', import.meta.url).href);
const server = await createServer({
root: FRONTEND, server: { middlewareMode: true }, appType: 'custom', logLevel: 'error',
});
const skillReg = await server.ssrLoadModule('/src/lib/skills/registry.js');
const agentReg = await server.ssrLoadModule('/src/lib/agents/registry.js');
/** Every .md under a directory, recursively, repo-relative. */
function walk(dir) {
const out = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) out.push(...walk(full));
else if (entry.endsWith('.md')) out.push(full);
}
return out.sort();
}
/** The fields the backend contract actually projects out of a definition. */
const projectSkill = (s) => ({
id: s.id,
name: s.name,
description: s.description,
status: s.status,
pages: s.pages,
kind: s.kind,
category: s.category,
actions: s.actions,
triggers: s.triggers,
declaredTriggers: s.declaredTriggers,
prompt: s.prompt ?? null,
facets: s.facets,
skillId: s.skillId ?? null,
});
const projectAgent = (a) => ({
id: a.id,
name: a.name,
description: a.description,
status: a.status,
version: a.version,
pages: a.pages,
icon: a.icon,
reasoning: a.reasoning,
trigger: a.trigger,
webSearch: a.webSearch,
skills: a.skills,
/* Capability, and the field the two parsers most need to agree on: the
backend resolves an agent's tools from exactly this list, so a divergence
here is an agent that can do something in one process and not the other. */
tools: a.tools,
subagents: a.subagents,
starters: a.starters,
permissions: a.permissions,
errors: a.errors,
});
/** One definition, as the JS side sees it end to end. */
function observe(raw, kind) {
const out = { kind };
/* Layer 1 — the fence. */
try {
out.hasFrontmatter = skillReg.hasFrontmatter(raw);
const { data, body } = skillReg.parseFrontmatter(raw);
out.frontmatter = { ok: true, data, body };
} catch (error) {
out.hasFrontmatter = (() => { try { return skillReg.hasFrontmatter(raw); } catch { return null; } })();
out.frontmatter = { ok: false, error: String(error?.message ?? error) };
}
/* Layer 2 — the definition. */
try {
const parsed = kind === 'agent'
? agentReg.parseAgent(raw, { custom: true })
: skillReg.parseSkill(raw, { custom: true });
out.parse = { ok: true };
out.normalized = kind === 'agent' ? projectAgent(parsed) : projectSkill(parsed);
out.markdownVerbatim = parsed.markdown === raw;
} catch (error) {
out.parse = { ok: false, error: String(error?.message ?? error) };
out.normalized = null;
out.markdownVerbatim = null;
}
/* Layer 3 — the save gate. This is the accept/reject contract. */
const problem = kind === 'agent'
? agentReg.validateAgentSource(raw)
: skillReg.validateSkillSource(raw);
out.accepted = problem === null;
out.rejection = problem;
return out;
}
/* ── The shipped corpus ───────────────────────────────────────────────────── */
const corpus = [];
const groups = [
{ dir: join(FRONTEND, 'src/agents'), type: 'agent', kind: 'agent' },
{ dir: join(FRONTEND, 'src/skills'), type: 'skill', kind: 'skill' },
{ dir: join(FRONTEND, 'skill-examples'), type: 'example', kind: 'skill' },
];
for (const { dir, type, kind } of groups) {
for (const file of walk(dir)) {
const raw = readFileSync(file, 'utf8');
corpus.push({
path: relative(FRONTEND, file),
type,
rawBase64: Buffer.from(raw, 'utf8').toString('base64'),
bytes: Buffer.byteLength(raw, 'utf8'),
...observe(raw, kind),
});
}
}
/* ── The adversarial cases ────────────────────────────────────────────────── */
const cases = CASES.map((c) => ({
name: c.name,
rawBase64: Buffer.from(c.raw, 'utf8').toString('base64'),
bytes: Buffer.byteLength(c.raw, 'utf8'),
...observe(c.raw, c.kind),
}));
/* ── The closed vocabularies, read out of the JS tables themselves ────────── */
const surfaces = await server.ssrLoadModule('/src/lib/skills/surfaces.js');
const agentVocab = await server.ssrLoadModule('/src/lib/agents/vocabulary.js');
const vocabulary = {
pages: surfaces.SKILL_SURFACES.map((s) => ({ id: s.id, aliases: s.aliases || [] })),
agentStatuses: agentVocab.AGENT_STATUSES,
defaultAgentStatus: agentVocab.DEFAULT_AGENT_STATUS,
reasoning: agentVocab.SUPPORTED_REASONING,
defaultReasoning: agentVocab.DEFAULT_REASONING,
icons: agentVocab.AGENT_ICONS,
defaultIcon: agentVocab.DEFAULT_AGENT_ICON,
knowledgeKinds: agentVocab.KNOWLEDGE_KINDS,
defaultKnowledgeKind: agentVocab.DEFAULT_KNOWLEDGE_KIND,
access: agentVocab.AGENT_ACCESS,
defaultAccess: agentVocab.DEFAULT_AGENT_ACCESS,
roles: agentVocab.PERMISSION_ROLES,
defaultRole: agentVocab.DEFAULT_PERMISSION_ROLE,
};
await server.close();
const doc = {
generatedBy: 'scripts/oracle.mjs against the frontend module graph',
frontendParser: {
yaml: 'src/lib/skills/yaml.js (hand-written YAML subset, no dependency)',
frontmatter: 'src/lib/skills/registry.js — normalizeDefinition / hasFrontmatter / parseFrontmatter',
skill: 'src/lib/skills/registry.js — parseSkill / validateSkillSource',
agent: 'src/lib/agents/registry.js — parseAgent / validateAgentSource',
},
vocabulary,
corpus,
cases,
};
const target = process.argv[2];
writeFileSync(target, JSON.stringify(doc, null, 2) + '\n');
const acc = (xs) => xs.filter((x) => x.accepted).length;
console.log(`corpus ${corpus.length} files — ${acc(corpus)} accepted, ${corpus.length - acc(corpus)} rejected`);
console.log(`cases ${cases.length} — ${acc(cases)} accepted, ${cases.length - acc(cases)} rejected`);
console.log(`markdown verbatim: ${[...corpus, ...cases].every((x) => x.markdownVerbatim !== false)}`);
console.log(`written: ${target}`);