cleanup - removing ai agent for deterministic code and better formatting
This commit is contained in:
292
src/lib/breadcrumbs.ts
Normal file
292
src/lib/breadcrumbs.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
// Deterministic breadcrumb suggestions — no external AI.
|
||||
//
|
||||
// This replaces the old Anthropic-backed "AI draft" feature. The methodology it
|
||||
// encodes is exactly what mattered: seven construction-execution domains, each
|
||||
// with a canonical value driver, capability, metric, method, and baseline drawn
|
||||
// from established practice (Earned Value Management, CII Advanced Work
|
||||
// Packaging, Last Planner System, PMBOK change control, first-time quality,
|
||||
// CII lessons-learned). A problem statement is classified into a domain by
|
||||
// keyword, then that domain's template fills the breadcrumb fields. The team
|
||||
// then reviews and edits — same as before, minus the API key, tokens, and
|
||||
// network round-trip.
|
||||
//
|
||||
// The metric text is always written as an outcome, never adoption language, so
|
||||
// it passes the adoption gate the UI already enforces.
|
||||
|
||||
export const BREADCRUMB_FIELDS = ['driver', 'cap', 'metric', 'method', 'baseline'] as const;
|
||||
export type BreadcrumbField = (typeof BREADCRUMB_FIELDS)[number];
|
||||
|
||||
export interface Domain {
|
||||
id: string;
|
||||
label: string;
|
||||
/** Lowercased keywords/phrases that steer classification toward this domain. */
|
||||
keywords: string[];
|
||||
driver: string;
|
||||
cap: string;
|
||||
metric: string;
|
||||
method: string;
|
||||
baseline: string;
|
||||
}
|
||||
|
||||
export interface Suggestion {
|
||||
domain: string;
|
||||
label: string;
|
||||
driver: string;
|
||||
cap: string;
|
||||
metric: string;
|
||||
method: string;
|
||||
baseline: string;
|
||||
}
|
||||
|
||||
// The catch-all used when nothing matches or the user picks "not sure". It fills
|
||||
// the fields with the gates themselves, so the card is still useful guidance.
|
||||
const GENERAL: Domain = {
|
||||
id: 'general',
|
||||
label: 'General / not sure',
|
||||
keywords: [],
|
||||
driver:
|
||||
'The business end this should improve — state it in cost, schedule, quality, safety, or predictability terms, not as a tool.',
|
||||
cap: 'The capability needed to move that outcome — a function, not a product or vendor name.',
|
||||
metric: 'An outcome ratio or rate that moves — never "implemented", "adopted", "rolled out", or "active users".',
|
||||
method: 'Data source + comparison + cadence for measuring the metric.',
|
||||
baseline:
|
||||
'A number, or the cheapest defensible pre-pilot study that produces one (poll < record trace < log audit < 2-week coding < sampling study). Set targets after the baseline, not before.'
|
||||
};
|
||||
|
||||
export const DOMAINS: Domain[] = [
|
||||
{
|
||||
id: 'progress-visibility',
|
||||
label: 'Progress visibility',
|
||||
keywords: [
|
||||
'progress',
|
||||
'percent complete',
|
||||
'% complete',
|
||||
'percent-complete',
|
||||
'claiming',
|
||||
'reported',
|
||||
'rules of credit',
|
||||
'forecast',
|
||||
'earned value',
|
||||
's-curve',
|
||||
'roll-up',
|
||||
'rollup',
|
||||
'status'
|
||||
],
|
||||
driver:
|
||||
'Trustworthy progress information so cost and schedule forecasts hold up and decisions are not made on inflated percent-complete.',
|
||||
cap: 'Objective progress measurement with rules of credit tied to verifiable field milestones.',
|
||||
metric: 'Claiming accuracy — reported % versus field-verified % within tolerance, by phase.',
|
||||
method: 'Monthly spot audit of 30+ sampled assets: compare reported % against a field walkdown.',
|
||||
baseline: 'The first audit establishes the baseline claiming gap.'
|
||||
},
|
||||
{
|
||||
id: 'data-freshness',
|
||||
label: 'Data freshness / entry burden',
|
||||
keywords: [
|
||||
'data entry',
|
||||
'stale',
|
||||
'out of date',
|
||||
'duplicate entry',
|
||||
're-enter',
|
||||
'reenter',
|
||||
'double entry',
|
||||
'paper',
|
||||
'spreadsheet',
|
||||
'manual entry',
|
||||
'latency',
|
||||
'single source',
|
||||
'transcribe',
|
||||
'rekey'
|
||||
],
|
||||
driver:
|
||||
'Decisions made on current field reality, with data captured once instead of burdening crews with duplicate entry.',
|
||||
cap: 'A single source of truth with data entered once at the point of work.',
|
||||
metric: 'Entry latency — % of updates recorded within 24 hours of the work completing.',
|
||||
method: 'Compare system timestamp against actual completion date, sampled weekly.',
|
||||
baseline: 'Audit the current paper-to-digital lag on this project before the pilot.'
|
||||
},
|
||||
{
|
||||
id: 'constraint-readiness',
|
||||
label: 'Constraint readiness / work release',
|
||||
keywords: [
|
||||
'constraint',
|
||||
'ready to work',
|
||||
'work package',
|
||||
'iwp',
|
||||
'material',
|
||||
'tools',
|
||||
'access',
|
||||
'permit',
|
||||
'scaffolding',
|
||||
'release',
|
||||
'prerequisite',
|
||||
'work front',
|
||||
'stand-down',
|
||||
'stand down'
|
||||
],
|
||||
driver:
|
||||
'Crews start only constraint-free work, so time is not lost to missing material, tools, access, or prerequisites.',
|
||||
cap: 'A release gate that verifies every constraint is cleared before a work package is issued.',
|
||||
metric: '% of work packages released constraint-free (no later constraint-driven stop).',
|
||||
method: 'Weekly release-gate checklist audit; a later constraint stop counts as a miss.',
|
||||
baseline: 'Audit the last month of work starts from the daily logs.'
|
||||
},
|
||||
{
|
||||
id: 'planning-resources',
|
||||
label: 'Planning / resources / idle time',
|
||||
keywords: [
|
||||
'plan',
|
||||
'planning',
|
||||
'commitment',
|
||||
'schedule reliability',
|
||||
'idle',
|
||||
'waiting',
|
||||
'wait',
|
||||
'crew',
|
||||
'resource',
|
||||
'look ahead',
|
||||
'lookahead',
|
||||
'promise',
|
||||
'weekly plan',
|
||||
'ppc',
|
||||
'churn'
|
||||
],
|
||||
driver: 'Predictable weekly output, so crews are not idle and downstream work is not disrupted.',
|
||||
cap: 'Collaborative short-interval planning with commitments tracked and variances learned from.',
|
||||
metric: 'PPC — % of weekly commitments completed as promised.',
|
||||
method: 'Log commitments at weekly planning, score the following week, publish the trend.',
|
||||
baseline: 'The first 4 weeks of logging establish the baseline PPC.'
|
||||
},
|
||||
{
|
||||
id: 'scope-change',
|
||||
label: 'Scope control / change',
|
||||
keywords: [
|
||||
'scope',
|
||||
'change order',
|
||||
'unauthorized',
|
||||
'extra work',
|
||||
't&m',
|
||||
'backcharge',
|
||||
'out of scope',
|
||||
'variation',
|
||||
'change log',
|
||||
'co log'
|
||||
],
|
||||
driver: 'Changes are authorized and captured before work proceeds, protecting margin and schedule.',
|
||||
cap: 'A change-control loop that logs and prices scope changes at the time they occur.',
|
||||
metric: 'Unauthorized-work hours or dollars performed before a change was authorized.',
|
||||
method: 'Monthly reconciliation of field records against the change-order log.',
|
||||
baseline: 'Run the reconciliation on the current project to establish the baseline.'
|
||||
},
|
||||
{
|
||||
id: 'quality-rework',
|
||||
label: 'Quality / rework',
|
||||
keywords: [
|
||||
'quality',
|
||||
'rework',
|
||||
'defect',
|
||||
'punch',
|
||||
'punchlist',
|
||||
'punch list',
|
||||
'redo',
|
||||
'first pass',
|
||||
'first-time',
|
||||
'inspection',
|
||||
'qc',
|
||||
'qa',
|
||||
'out of rev',
|
||||
'out-of-rev',
|
||||
'revision',
|
||||
'nonconformance',
|
||||
'ncr',
|
||||
'out of spec',
|
||||
'wrong'
|
||||
],
|
||||
driver: 'Work done right the first time, cutting the craft hours lost to rework.',
|
||||
cap: 'First-time-quality controls with rework captured and its causes coded.',
|
||||
metric: 'Rework as a % of craft hours.',
|
||||
method: 'A dedicated rework cost code with reason coding, reviewed monthly.',
|
||||
baseline: 'Create the cost code; the first month of coding is the baseline.'
|
||||
},
|
||||
{
|
||||
id: 'knowledge-feedback',
|
||||
label: 'Knowledge / feedback',
|
||||
keywords: [
|
||||
'lessons learned',
|
||||
'lesson learned',
|
||||
'closeout',
|
||||
'close-out',
|
||||
'handover',
|
||||
'handoff',
|
||||
'knowledge',
|
||||
'feedback',
|
||||
'estimate',
|
||||
'historical',
|
||||
'punch closure',
|
||||
'as-built',
|
||||
'as built',
|
||||
'documentation',
|
||||
'chain of custody',
|
||||
'design intent'
|
||||
],
|
||||
driver: 'Lessons and actuals feed the next project, so estimates and plans improve over time.',
|
||||
cap: 'A closeout and feedback loop that captures actuals and routes them to estimating.',
|
||||
metric: 'Closeout compliance — % of closeouts completed within 60 days.',
|
||||
method: 'Closeout checklist audit against project completion dates.',
|
||||
baseline: 'Audit the last 4–6 projects to establish the baseline.'
|
||||
}
|
||||
];
|
||||
|
||||
/** Domain options for a UI picker: the seven domains plus the catch-all. */
|
||||
export const DOMAIN_OPTIONS: { id: string; label: string }[] = [
|
||||
...DOMAINS.map((d) => ({ id: d.id, label: d.label })),
|
||||
{ id: GENERAL.id, label: GENERAL.label }
|
||||
];
|
||||
|
||||
function domainById(id: string): Domain | undefined {
|
||||
if (id === GENERAL.id) return GENERAL;
|
||||
return DOMAINS.find((d) => d.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a problem into a domain by counting keyword hits. Returns the domain
|
||||
* id with the most matches, or 'general' if nothing matches. `related` issues
|
||||
* are weighted lightly so they can break ties but not overrule the statement.
|
||||
*/
|
||||
export function classifyDomain(prob: string, related: string[] = []): string {
|
||||
const main = String(prob).toLowerCase();
|
||||
const rel = related.map((r) => String(r).toLowerCase()).join(' \n ');
|
||||
|
||||
let best = GENERAL.id;
|
||||
let bestScore = 0;
|
||||
for (const d of DOMAINS) {
|
||||
let score = 0;
|
||||
for (const kw of d.keywords) {
|
||||
if (main.includes(kw)) score += 2;
|
||||
if (rel.includes(kw)) score += 1;
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = d.id;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the five breadcrumb fields from methodology. If `forceDomain` is a real
|
||||
* domain id, that domain's template is used; otherwise the problem is classified
|
||||
* automatically. The result is a starting draft for the team to edit.
|
||||
*/
|
||||
export function suggestBreadcrumb(prob: string, related: string[] = [], forceDomain = ''): Suggestion {
|
||||
const domain = domainById(forceDomain) ?? domainById(classifyDomain(prob, related)) ?? GENERAL;
|
||||
return {
|
||||
domain: domain.id,
|
||||
label: domain.label,
|
||||
driver: domain.driver,
|
||||
cap: domain.cap,
|
||||
metric: domain.metric,
|
||||
method: domain.method,
|
||||
baseline: domain.baseline
|
||||
};
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Server-side "AI draft" for breadcrumb fields. The Anthropic API key lives here
|
||||
// (from an env var) and never reaches the browser.
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export const REQUIRED_FIELDS = ['driver', 'cap', 'metric', 'method', 'baseline'] as const;
|
||||
export const ADOPTION_WORDS =
|
||||
/implement|adopt|roll ?out|have a tool|tool exists|usage|active users?|users? active/i;
|
||||
|
||||
export interface DraftResult {
|
||||
domain: string;
|
||||
driver: string;
|
||||
cap: string;
|
||||
metric: string;
|
||||
method: string;
|
||||
baseline: string;
|
||||
}
|
||||
|
||||
const METHOD_KB = `Domain playbook (anchor methodology -> standard metrics/methods/baselines):
|
||||
1 PROGRESS VISIBILITY - Earned Value Mgmt, rules of credit. Metrics: claiming accuracy (reported % vs field-verified % within tolerance, by phase; method: monthly spot audit of 30+ sampled assets; baseline: first audit); forecast reliability (forecast vs actual completion; baseline: last project's reports). Never use "% of assets with a status" (adoption in disguise).
|
||||
2 DATA FRESHNESS / ENTRY BURDEN - lean data flow, single source of truth. Metrics: entry latency (% of updates within 24h of completion; method: system timestamp vs completion date, weekly; baseline: audit paper-to-digital lag on current project); touch count (systems the same record is entered into; method: trace 10 records end to end; baseline: pre-pilot trace); non-productive data minutes/person/day (method+baseline: structured field poll pre-pilot, repeat midpoint).
|
||||
3 CONSTRAINT READINESS / WORK RELEASE - CII AWP IR 272-2, constraint-free IWP release (drawings, material, tools, labor, access, predecessors, permits, quality docs, scaffolding). Metrics: % releases issued constraint-free (method: release-gate checklist audit weekly, later constraint stop = miss; baseline: audit last month of work starts from daily logs); stand-down count/week (method: foreman log with fixed reason list; baseline: 2-week logging exercise); tool time % (method: activity sampling study; baseline: pre-pilot study; propose only when stakes justify cost).
|
||||
4 PLANNING / RESOURCES / IDLE - Last Planner System. Metrics: PPC (% weekly commitments completed as promised; method: log at weekly planning, score next week, publish trend; baseline: first 4 weeks of logging); reasons-for-variance distribution (fixed-list coding of misses); idle hours/crew-week (timecard or foreman reason codes; baseline: 2-week coding).
|
||||
5 SCOPE CONTROL / CHANGE - PMBOK change control, CII change research. Metrics: unauthorized-work hours/dollars at time of performance (method: monthly reconciliation of field records vs CO log; baseline: run reconciliation on current project); change capture latency days (field record date vs log date); change cycle time (log timestamps; baseline: last project's log).
|
||||
6 QUALITY / REWORK - first-time quality, CII rework research (2-6% direct cost where measured). Metrics: rework % of craft hours (method: dedicated rework cost code + reason coding, monthly; baseline: create the code, first month is baseline); first-pass yield at QC gates; out-of-rev incidents (QC walkdown coding).
|
||||
7 KNOWLEDGE / FEEDBACK - CII lessons-learned, estimate feedback. Metrics: closeout compliance % within 60 days (method: closeout checklist audit; baseline: audit last 4-6 projects); % estimates referencing prior actuals (estimating file audit); punch closure median days (issue log timestamps; if untimestamped, that gap is the finding).
|
||||
GATES: driver = business end in cost/schedule/quality/safety/predictability terms, never a tool. capability = function not product, no vendor names. metric = outcome ratio/rate, NEVER "implemented/adopted/rolled out/active users/have a tool". method = data source + comparison + cadence. baseline = a number or the named pre-pilot study that produces one (cheapest defensible: poll < record trace < log audit < 2-week coding < sampling study). Set targets after baseline, not before. Each field under ~30 words except method/baseline.`;
|
||||
|
||||
function parseJson(text: string): Record<string, unknown> {
|
||||
const clean = String(text).replace(/```json|```/g, '').trim();
|
||||
const candidates = ['[', '{'].map((ch) => {
|
||||
const i = clean.indexOf(ch);
|
||||
return i === -1 ? Infinity : i;
|
||||
});
|
||||
const start = Math.min(...candidates);
|
||||
return JSON.parse(start === Infinity ? clean : clean.slice(start));
|
||||
}
|
||||
|
||||
/** True when the AI feature is configured. Used to disable the button gracefully. */
|
||||
export function aiConfigured(): boolean {
|
||||
return Boolean(env.ANTHROPIC_API_KEY);
|
||||
}
|
||||
|
||||
export async function draftBreadcrumb(prob: string, related: string[]): Promise<DraftResult> {
|
||||
const apiKey = env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) throw new Error('AI is not configured on this server (missing ANTHROPIC_API_KEY).');
|
||||
if (!prob.trim()) throw new Error('Write the problem statement first.');
|
||||
|
||||
const client = new Anthropic({ apiKey });
|
||||
const rel = related.length
|
||||
? `\nRelated issues in this cluster:\n${related.map((r) => '- ' + r).join('\n')}`
|
||||
: '';
|
||||
const prompt = `You populate breadcrumb trails for construction field problems at an industrial I&C contractor, using best-in-class execution methodology.\n\n${METHOD_KB}\n\nProblem statement:\n${prob}${rel}\n\nClassify the problem into one domain above, then draft the five fields per that domain's standard metrics, methods, and baselines, obeying every GATE. Respond ONLY with a JSON object, no prose, no markdown fences:\n{"domain":"...","driver":"...","cap":"...","metric":"...","method":"...","baseline":"..."}`;
|
||||
|
||||
const message = await client.messages.create({
|
||||
model: env.ANTHROPIC_MODEL || 'claude-sonnet-5',
|
||||
max_tokens: 1000,
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const text = message.content
|
||||
.filter((block): block is Anthropic.TextBlock => block.type === 'text')
|
||||
.map((block) => block.text)
|
||||
.join('\n');
|
||||
|
||||
const obj = parseJson(text);
|
||||
for (const f of REQUIRED_FIELDS) {
|
||||
if (!obj[f]) throw new Error('AI returned an incomplete draft; try again.');
|
||||
}
|
||||
if (ADOPTION_WORDS.test(String(obj.metric))) {
|
||||
throw new Error('Draft failed the adoption gate; try again.');
|
||||
}
|
||||
|
||||
return {
|
||||
domain: String(obj.domain ?? 'methodology'),
|
||||
driver: String(obj.driver),
|
||||
cap: String(obj.cap),
|
||||
metric: String(obj.metric),
|
||||
method: String(obj.method),
|
||||
baseline: String(obj.baseline)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user