agnets done
This commit is contained in:
65
scripts/__baseline__/README.md
Normal file
65
scripts/__baseline__/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Owliver behaviour baseline
|
||||
|
||||
`owliver-baseline.json` records how the panel routed and answered before the
|
||||
agent layer existed. `skill-check.mjs` asserts against it on every run.
|
||||
|
||||
Regenerating it is a deliberate act, and the reason belongs here.
|
||||
|
||||
## 2026-08-27 — the seed gained the three statuses nothing exercised
|
||||
|
||||
`application_status` has seven values. The fixture produced four: `applied`,
|
||||
`ai_screened`, `interview`, `hired`. `shortlisted`, `rejected` and `assigned`
|
||||
existed only in the schema, and `Assignment` shipped empty by design.
|
||||
|
||||
That gap was hiding real defects, all found the same week and none visible
|
||||
against the old data:
|
||||
|
||||
- `atOrBeyond` ranks a status by its index in `STAGE_ORDER`, which listed five
|
||||
of the seven. `rejected` and `assigned` scored -1 and dropped out of *every*
|
||||
bucket including `applied`, so a position's funnel lost people and a role
|
||||
whose candidates had all been assigned read as unfilled.
|
||||
- The agent's `candidates_awaiting` excluded `hired` and `rejected` from "still
|
||||
in the running", so somebody already working a shift was offered as a person
|
||||
to chase.
|
||||
- `insights.stalled` counted "screened and waiting on a decision" as
|
||||
`status === 'ai_screened'` only, silently dropping the shortlisted — the
|
||||
people that phrase most describes. Caught by this regeneration, not before it.
|
||||
|
||||
**The change:** three existing applications were converted, not added, so every
|
||||
regression anchor holds — 24 applications, 9 scored averaging 76, 3 staff,
|
||||
8 postings, all unchanged.
|
||||
|
||||
app_kevin applied → rejected (never scored; a hard requirement)
|
||||
app_sofia ai_screened → shortlisted (the strongest screened candidate)
|
||||
app_marco hired → assigned (hired, then rostered)
|
||||
|
||||
Plus one `Assignment` for Marco — the minimum that makes `assigned` real. Eight
|
||||
of the nine positions still have none, so every reader still meets the empty
|
||||
case.
|
||||
|
||||
**What drifted, verified before regenerating:** one number, in four prompts.
|
||||
Kevin left `applied`, so the unscored count reads 9 where it read 10:
|
||||
|
||||
"Show the 10 applications waiting for review" → "…9 applications…"
|
||||
"10 have no score" / "10 unscored applicants" / "10 unscored applications"
|
||||
|
||||
Nothing else moved. `1 waiting on a decision` briefly became a fallback prompt
|
||||
and that was the `stalled` defect above — fixed in `insights.js` rather than
|
||||
absorbed into the baseline, and the prompt came back on its own.
|
||||
|
||||
## 2026-08-27 — the local simulator was removed
|
||||
|
||||
The panel used to answer from two places: an agent, and ~3,300 lines of
|
||||
browser-side templates. Two paths behind one avatar meant the same question got
|
||||
different answers depending on phrasing, so the templates were deleted.
|
||||
|
||||
**What actually drifted, verified before regenerating:** one thing. The
|
||||
"I do not have that on this page" decline used to list the page's capability
|
||||
labels as bullets — those labels were the simulator's canned readings and went
|
||||
with it. It now names the page's topics in a sentence.
|
||||
|
||||
doc shape: ["text","list","note"] → ["text","text","note"]
|
||||
|
||||
Every intent `kind` was unchanged. No routing moved, no skill matching changed.
|
||||
That is why this regeneration was safe: the diff was read first, and it was one
|
||||
cosmetic change on a path that only runs when no agent is configured at all.
|
||||
@@ -2,16 +2,29 @@
|
||||
"schema": 1,
|
||||
"today": "2026-08-20T09:00:00.000Z",
|
||||
"skillIds": [
|
||||
"activity-analysis",
|
||||
"analytics-insights",
|
||||
"anomaly-detection",
|
||||
"attendance-analysis",
|
||||
"bartending-training",
|
||||
"candidate-analysis",
|
||||
"candidate-search",
|
||||
"create-position",
|
||||
"customer-service-training",
|
||||
"executive-summary",
|
||||
"food-safety-training",
|
||||
"forge-skill-management",
|
||||
"hiring-activity-assistant",
|
||||
"hiring-history-analysis",
|
||||
"hiring-pulse-analysis",
|
||||
"leadership-training",
|
||||
"server-training"
|
||||
"learning-analysis",
|
||||
"operational-risk",
|
||||
"overtime-analysis",
|
||||
"server-training",
|
||||
"staffing-risk",
|
||||
"talent-pool-analysis",
|
||||
"workforce-analytics"
|
||||
],
|
||||
"diagnostics": [],
|
||||
"routes": {
|
||||
@@ -24,15 +37,35 @@
|
||||
"/admin/positions": "admin.positions",
|
||||
"/admin/positions/new": "admin.createPosition",
|
||||
"/admin/profile": "admin.profile",
|
||||
"/admin/settings": "admin.settings",
|
||||
"/admin/talent-pool": "admin.talentPool",
|
||||
"/admin/university": "admin.forge"
|
||||
"/admin/university": "admin.forge",
|
||||
"/admin/workspace": "admin.workspace",
|
||||
"/admin/workspace/agents": "admin.workspaceAgents",
|
||||
"/admin/workspace/agents/new": "admin.agentConfigure",
|
||||
"/admin/workspace/skill-development": "admin.skillDevelopment",
|
||||
"/admin/workspace/skills": "admin.workspaceSkills",
|
||||
"/admin/workspace/skills/new": "admin.skillConfigure"
|
||||
},
|
||||
"contexts": {
|
||||
"admin.controlCenter": {
|
||||
"pageKey": "control-center",
|
||||
"route": "/admin",
|
||||
"skills": [],
|
||||
"suggestions": [],
|
||||
"skills": [
|
||||
"anomaly-detection",
|
||||
"attendance-analysis",
|
||||
"executive-summary",
|
||||
"hiring-pulse-analysis",
|
||||
"operational-risk",
|
||||
"overtime-analysis",
|
||||
"staffing-risk"
|
||||
],
|
||||
"suggestions": [
|
||||
"Compare attendance by person",
|
||||
"How is attendance?",
|
||||
"Is anything unusual?",
|
||||
"Show the signals"
|
||||
],
|
||||
"prompts": [
|
||||
"What needs attention?",
|
||||
"Platform health",
|
||||
@@ -118,7 +151,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -129,16 +162,19 @@
|
||||
"route": "/admin/positions",
|
||||
"skills": [
|
||||
"create-position",
|
||||
"hiring-activity-assistant"
|
||||
"hiring-activity-assistant",
|
||||
"staffing-risk"
|
||||
],
|
||||
"suggestions": [
|
||||
"Show hiring activity as a flow",
|
||||
"Summarize hiring activity for this position"
|
||||
"Summarize hiring activity for this position",
|
||||
"Summarize staffing risk",
|
||||
"Which roles are at risk?"
|
||||
],
|
||||
"prompts": [
|
||||
"What needs attention?",
|
||||
"Which position has the strongest pipeline?",
|
||||
"Show the 10 applications waiting for review",
|
||||
"Show the 9 applications waiting for review",
|
||||
"Summarize hiring activity across all positions"
|
||||
],
|
||||
"intents": [
|
||||
@@ -162,7 +198,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -226,7 +262,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -254,7 +290,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -278,7 +314,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -328,7 +364,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -338,14 +374,19 @@
|
||||
"pageKey": "candidates",
|
||||
"route": "/admin/candidates",
|
||||
"skills": [
|
||||
"candidate-analysis",
|
||||
"candidate-search"
|
||||
],
|
||||
"suggestions": [],
|
||||
"suggestions": [
|
||||
"How strong is the candidate pool?",
|
||||
"Score bands",
|
||||
"Show candidates by score"
|
||||
],
|
||||
"prompts": [
|
||||
"1 waiting on a decision",
|
||||
"Who are the strongest candidates?",
|
||||
"Which candidates are ready for interview?",
|
||||
"10 have no score",
|
||||
"9 have no score",
|
||||
"Summarize the candidate pipeline",
|
||||
"Identify candidate risks"
|
||||
],
|
||||
@@ -380,7 +421,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -427,7 +468,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -437,13 +478,18 @@
|
||||
"pageKey": "candidates-analysis",
|
||||
"route": "/admin/candidates-analysis",
|
||||
"skills": [
|
||||
"candidate-analysis",
|
||||
"candidate-search"
|
||||
],
|
||||
"suggestions": [],
|
||||
"suggestions": [
|
||||
"How strong is the candidate pool?",
|
||||
"Score bands",
|
||||
"Show candidates by score"
|
||||
],
|
||||
"prompts": [
|
||||
"Compare Chef & Marcus",
|
||||
"1 missing credentials",
|
||||
"10 unscored applicants",
|
||||
"9 unscored applicants",
|
||||
"Recommend next actions",
|
||||
"Recruitment insights"
|
||||
],
|
||||
@@ -458,7 +504,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -472,7 +518,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -486,7 +532,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -536,7 +582,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -545,8 +591,13 @@
|
||||
"admin.hiredHistory": {
|
||||
"pageKey": "hired-history",
|
||||
"route": "/admin/hired",
|
||||
"skills": [],
|
||||
"suggestions": [],
|
||||
"skills": [
|
||||
"hiring-history-analysis"
|
||||
],
|
||||
"suggestions": [
|
||||
"How is hiring performance?",
|
||||
"Who did we hire recently?"
|
||||
],
|
||||
"prompts": [
|
||||
"3 hires unrated",
|
||||
"Bartender leads on hires",
|
||||
@@ -565,7 +616,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -579,7 +630,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -593,7 +644,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -646,7 +697,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -655,8 +706,14 @@
|
||||
"admin.talentPool": {
|
||||
"pageKey": "talent-pool",
|
||||
"route": "/admin/talent-pool",
|
||||
"skills": [],
|
||||
"suggestions": [],
|
||||
"skills": [
|
||||
"talent-pool-analysis"
|
||||
],
|
||||
"suggestions": [
|
||||
"How healthy is the talent pool?",
|
||||
"Strongest people in the pool",
|
||||
"Who is in the pool?"
|
||||
],
|
||||
"prompts": [
|
||||
"Why does Maria rank first?",
|
||||
"4 profiles unscored",
|
||||
@@ -675,7 +732,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -699,7 +756,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -752,7 +809,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -762,9 +819,13 @@
|
||||
"pageKey": "krow-forge",
|
||||
"route": "/admin/university",
|
||||
"skills": [
|
||||
"forge-skill-management"
|
||||
"forge-skill-management",
|
||||
"learning-analysis"
|
||||
],
|
||||
"suggestions": [
|
||||
"How is training progressing?",
|
||||
"What does the library hold?"
|
||||
],
|
||||
"suggestions": [],
|
||||
"prompts": [
|
||||
"Create a skill training",
|
||||
"40 published",
|
||||
@@ -783,7 +844,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -797,7 +858,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -811,7 +872,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -864,7 +925,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -874,16 +935,25 @@
|
||||
"pageKey": "analytics",
|
||||
"route": "/admin/analytics",
|
||||
"skills": [
|
||||
"analytics-insights"
|
||||
"analytics-insights",
|
||||
"attendance-analysis",
|
||||
"hiring-pulse-analysis",
|
||||
"overtime-analysis",
|
||||
"workforce-analytics"
|
||||
],
|
||||
"suggestions": [
|
||||
"Attendance over recent periods",
|
||||
"Compare attendance by person",
|
||||
"How is attendance?",
|
||||
"What is the hiring pulse?"
|
||||
],
|
||||
"suggestions": [],
|
||||
"prompts": [
|
||||
"What is the hiring trend?",
|
||||
"Which department is performing best?",
|
||||
"Biggest drop at Hired",
|
||||
"Which positions are converting best?",
|
||||
"Summarize hiring performance",
|
||||
"10 unscored applications"
|
||||
"9 unscored applications"
|
||||
],
|
||||
"intents": [
|
||||
{
|
||||
@@ -906,7 +976,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -920,7 +990,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -970,7 +1040,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -979,8 +1049,17 @@
|
||||
"admin.activity": {
|
||||
"pageKey": "activity",
|
||||
"route": "/admin/activity",
|
||||
"skills": [],
|
||||
"suggestions": [],
|
||||
"skills": [
|
||||
"activity-analysis",
|
||||
"anomaly-detection",
|
||||
"operational-risk"
|
||||
],
|
||||
"suggestions": [
|
||||
"Activity over recent periods",
|
||||
"Is anything unusual?",
|
||||
"Summarize workspace activity",
|
||||
"What kinds of event are there?"
|
||||
],
|
||||
"prompts": [
|
||||
"3 unusual patterns",
|
||||
"Summarize today's activity",
|
||||
@@ -999,7 +1078,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -1013,7 +1092,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -1076,7 +1155,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
@@ -1105,7 +1184,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -1119,7 +1198,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
},
|
||||
@@ -1179,7 +1258,7 @@
|
||||
"destination": null,
|
||||
"doc": [
|
||||
"text",
|
||||
"list",
|
||||
"text",
|
||||
"note"
|
||||
]
|
||||
}
|
||||
|
||||
72
scripts/seed-fixture.mjs
Normal file
72
scripts/seed-fixture.mjs
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Writes the backend's seed fixture from the frontend's seed module.
|
||||
*
|
||||
* node scripts/seed-fixture.mjs # check: exits non-zero if stale
|
||||
* node scripts/seed-fixture.mjs --write # regenerate
|
||||
*
|
||||
* `src/api/seed.js` is the authored source: it has the helpers, the reasoning in
|
||||
* comments, and the values a person edits. `seed/fixtures/seed.json` is what the
|
||||
* Go seeder loads. They used to be maintained in parallel with nothing checking
|
||||
* them, which is a duplication that only fails quietly — the demo and the API
|
||||
* would answer the same question differently, and the first symptom would be a
|
||||
* number on a page disagreeing with a number from an agent.
|
||||
*
|
||||
* `ShiftRecord` is deliberately not carried across. Its dates are anchored to
|
||||
* the day the seeder runs rather than to this file's fixed calendar, so the Go
|
||||
* side generates it (internal/seeder/shifts.go, a port of attendanceSeed.js).
|
||||
* A frozen snapshot of it would read as permanently empty a fortnight later.
|
||||
*/
|
||||
import { createServer } from 'vite';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
export const FIXTURE_PATH = join(process.cwd(), '..', 'krow-backend', 'seed', 'fixtures', 'seed.json');
|
||||
|
||||
/** The fixture the frontend seed implies. Pure — takes the loaded module. */
|
||||
export const GENERATED_NOTE =
|
||||
'Generated from krow-demo/src/api/seed.js — do not edit by hand. ' +
|
||||
'Regenerate with: npm run seed:fixture';
|
||||
|
||||
export function fixtureFrom(seedModule) {
|
||||
const { ShiftRecord, ...entities } = seedModule.seedData;
|
||||
/* First key, so it is the first thing anyone opening the file reads. The Go
|
||||
loader unmarshals into a struct of demoUser + entities and ignores the
|
||||
rest, so this costs nothing on the reading side. */
|
||||
return { _generated: GENERATED_NOTE, demoUser: seedModule.DEMO_USER, entities };
|
||||
}
|
||||
|
||||
/** Serialised exactly as the committed file is: 2-space indent, no trailing newline. */
|
||||
export const serialise = (fixture) => JSON.stringify(fixture, null, 2);
|
||||
|
||||
export async function buildFixture(server) {
|
||||
return serialise(fixtureFrom(await server.ssrLoadModule('/src/api/seed.js')));
|
||||
}
|
||||
|
||||
/* ── CLI ──────────────────────────────────────────────────────────────────── */
|
||||
/* pathToFileURL, not string concatenation: this repo lives under a path with a
|
||||
space in it, and import.meta.url percent-encodes it while `file://${path}`
|
||||
does not — the guard silently never fires and the script does nothing. */
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const server = await createServer({
|
||||
root: process.cwd(), server: { middlewareMode: true }, appType: 'custom', logLevel: 'error',
|
||||
});
|
||||
const built = await buildFixture(server);
|
||||
await server.close();
|
||||
|
||||
if (process.argv.includes('--write')) {
|
||||
writeFileSync(FIXTURE_PATH, built);
|
||||
const { entities } = JSON.parse(built);
|
||||
const n = Object.values(entities).reduce((s, v) => s + v.length, 0);
|
||||
console.log(`seed.json written — ${Object.keys(entities).length} collections, ${n} records.`);
|
||||
} else if (!existsSync(FIXTURE_PATH)) {
|
||||
console.error('seed.json is missing. Run: node scripts/seed-fixture.mjs --write');
|
||||
process.exit(1);
|
||||
} else if (readFileSync(FIXTURE_PATH, 'utf8') !== built) {
|
||||
console.error('seed.json is stale — it no longer matches src/api/seed.js.');
|
||||
console.error('Run: node scripts/seed-fixture.mjs --write');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('seed.json is in step with src/api/seed.js.');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user