Files
doormilxpress_astryx/scripts/owliver-capture.mjs
2026-08-20 18:18:10 +05:30

159 lines
5.9 KiB
JavaScript

/**
* Today's Owliver behaviour, captured as data.
*
* The Agent layer is additive: opening a Krow page must produce exactly the
* Owliver it produces now. That is a claim about eleven page contexts, their
* skills, their suggestions, their prompts and their intent routing — far more
* than anyone can hold in their head while editing, and all of it silent when
* it breaks. So it is recorded here as a value, committed, and compared on
* every run.
*
* One module, two callers: `owliver-baseline.mjs` writes the snapshot and
* `skill-check.mjs` asserts against it. A second implementation of "what we
* measure" would be a second definition of the contract.
*
* Everything below is pinned. `TODAY` is fixed rather than `new Date()`,
* because `buildFacts` windows records by date and a baseline that drifts
* daily is not a baseline. Lists are sorted; nothing depends on registry
* ordering or on the clock.
*/
import { join } from 'node:path';
/** Where the committed snapshot lives. Named here rather than in the CLI so a
* reader can import the path without also running a Vite server. */
export const BASELINE_PATH = join(process.cwd(), 'scripts/__baseline__/owliver-baseline.json');
/** Fixed so the snapshot means the same thing tomorrow. */
export const TODAY = new Date('2026-08-20T09:00:00.000Z');
/**
* The contexts under contract.
*
* The eight Krow pages the brief protects, plus the three further Admin
* contexts the placement table carries — Create Position, Candidates Analysis
* and Profile. They are page-scoped by exactly the same mechanism, so leaving
* them out would protect most of the boundary and quietly not the rest.
*/
export const CONTEXTS = [
'admin.controlCenter',
'admin.positions',
'admin.createPosition',
'admin.candidatesList',
'admin.candidates',
'admin.hiredHistory',
'admin.talentPool',
'admin.forge',
'admin.analytics',
'admin.activity',
'admin.profile',
];
/**
* What gets asked, per context.
*
* Three kinds on purpose, because they exercise three different branches of
* `resolveIntent` and a change to any one of them is a regression a reader
* would notice: a question the page owns, a question another page owns (which
* must route, not answer), and a question nothing owns (which must decline).
*/
export const QUESTIONS = [
'What needs my attention?',
'Summarize this page',
'Show hiring activity',
'Which positions need attention?',
'Where are candidates dropping off?',
'What are my permissions?',
'What is the weather today?',
];
const sorted = (list) => [...list].sort();
/** Block types only — structure is the contract, wording is not. */
const docShape = (document) =>
Array.isArray(document?.blocks) ? document.blocks.map((b) => b?.type ?? null) : null;
/**
* Loads the real module graph and reads today's behaviour off it.
*
* `ssrLoadModule` rather than a mock, for the reason `skill-check.mjs` gives:
* the pipeline is what is under test, and a mock of it would reproduce none of
* the failures this exists to catch.
*/
export async function captureBaseline(server) {
const registry = await server.ssrLoadModule('/src/lib/skills/registry.js');
const placement = await server.ssrLoadModule('/src/components/ai-assistant/placement.js');
const resolver = await server.ssrLoadModule('/src/lib/skills/owliverResolver.js');
const dynamic = await server.ssrLoadModule('/src/components/ai-assistant/dynamic.js');
const routing = await server.ssrLoadModule('/src/components/ai-assistant/routing.js');
const insights = await server.ssrLoadModule('/src/components/ai-assistant/insights.js');
const seed = await server.ssrLoadModule('/src/api/seed.js');
/* The fact sheet the panel builds, from the shipped seed at a fixed instant. */
const facts = insights.buildFacts({
applications: seed.seedData.JobApplication,
postings: seed.seedData.JobPosting,
interviews: seed.seedData.AIInterview,
staff: seed.seedData.Staff,
profiles: seed.seedData.WorkerProfile,
activity: seed.seedData.UserActivity,
courses: seed.seedData.Course,
profile: null,
user: seed.DEMO_USER,
today: TODAY,
});
const contexts = {};
for (const contextId of CONTEXTS) {
const pageKey = registry.pageKeyForContext(contextId);
contexts[contextId] = {
pageKey,
route: pageKey ? registry.routeForPageKey(pageKey) : null,
/* The page boundary itself: which skills this page carries with no
agent, no account customization and nothing disabled. */
skills: sorted(registry.skillsForContext(contextId, [], []).map((s) => s.id)),
suggestions: sorted(
resolver.owliverSuggestions(contextId, [], [], {}).map((s) => s.label)
),
prompts: (dynamic.buildPrompts(contextId, facts, null) || []).map((p) => p.label),
intents: QUESTIONS.map((question) => {
const matched = registry.matchSkill(question, contextId, [], []);
const intent = routing.resolveIntent({ question, contextId });
return {
question,
matchedSkill: matched?.id ?? null,
kind: intent?.kind ?? null,
skill: intent?.skill?.id ?? null,
capability: intent?.capability ?? null,
action: intent?.action?.name ?? null,
destination: intent?.destination?.contextId ?? null,
doc: docShape(intent?.doc),
};
}),
};
}
/* Route → context, for every placement the product declares. An agent must
never become an input to this. */
const routes = {};
for (const route of Object.keys(placement.PLACEMENT_ROUTES).sort()) {
routes[route] = placement.resolveAssistantContext('admin', route)?.id ?? null;
}
return {
/* Bumped only when the shape of what we measure changes — never to make a
failing comparison pass. */
schema: 1,
today: TODAY.toISOString(),
skillIds: sorted(registry.SKILLS.map((s) => s.id)),
diagnostics: registry.readSkillRegistry([]).diagnostics,
routes,
contexts,
};
}