159 lines
5.9 KiB
JavaScript
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,
|
|
};
|
|
}
|