converted to proper npm sveltekit project

This commit is contained in:
C-West8
2026-07-22 14:52:35 -05:00
parent ba0a445aaf
commit 086542169c
37 changed files with 6018 additions and 14 deletions

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import type { SyncStatus } from '$lib/workspace.svelte';
let { status, updatedAt }: { status: SyncStatus; updatedAt: string | null } = $props();
const labels: Record<SyncStatus, string> = {
loading: 'Loading…',
idle: 'Saved',
saving: 'Saving…',
saved: 'Saved',
conflict: 'Reloaded (teammate edit)',
error: 'Save error'
};
let time = $derived(
updatedAt ? new Date(updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''
);
</script>
<span class="sync sync-{status}" title={time ? `Last saved ${time}` : ''}>
<span class="dot"></span>{labels[status]}
</span>
<style>
.sync {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #c6c6c6;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6f6f6f;
}
.sync-saved .dot,
.sync-idle .dot {
background: var(--success);
}
.sync-saving .dot {
background: var(--warning);
}
.sync-error .dot,
.sync-conflict .dot {
background: var(--danger);
}
</style>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import { toast } from '$lib/toast.svelte';
</script>
<div class="toast" class:show={toast.visible}>{toast.message}</div>
<style>
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--text);
color: #fff;
padding: 12px 22px;
font-size: 14px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
z-index: 50;
}
.toast.show {
opacity: 1;
}
</style>

33
src/lib/defaults/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { ToolId } from '$lib/types';
import { workshopDefaultState } from './workshop';
import { scopeLockDefaultState } from './scope-lock';
/** The tools known to the app, with display metadata for the home page. */
export const TOOLS: { id: ToolId; title: string; blurb: string }[] = [
{
id: 'workshop',
title: 'Field Problem Workshop',
blurb: 'Capture field problems, cluster them into root problems, vote, build breadcrumb trails to measurable outcomes, and assign actions.'
},
{
id: 'scope-lock',
title: 'Scope Lock Meeting Suite',
blurb: 'Scope Boundary Board, MVP Priority Ranker, Decision Registry, and Micron Pilot Readiness in one place.'
}
];
/** Return a fresh copy of the baseline state for a tool. */
export function defaultStateFor(tool: ToolId): unknown {
switch (tool) {
case 'workshop':
return workshopDefaultState();
case 'scope-lock':
return scopeLockDefaultState();
default:
throw new Error(`Unknown tool: ${tool satisfies never}`);
}
}
export function isToolId(value: string): value is ToolId {
return value === 'workshop' || value === 'scope-lock';
}

View File

@@ -0,0 +1,90 @@
// Baseline state for the Scope Lock Meeting Suite (Micron pilot starting point).
import type { ScopeLockState } from '$lib/types';
export const DECISION_TYPES = ['Scope', 'Technical', 'Process', 'Data', 'Pilot', 'Other'];
export const DECISION_OWNERS = [
'Nick',
'Tim',
'Cameron',
'Clinton',
'Jason',
'Wyman',
'Ryan',
'Brendan',
'Lee',
'Team',
'Other'
];
export const DECISION_STATUSES = ['Locked', 'Provisional', 'Revisit'] as const;
// Readiness status labels, indexed by ScopeLock ready item `s`.
export const READY_STATES = ['Not started', 'In progress', 'Ready', 'Blocked'];
export function scopeLockDefaultState(): ScopeLockState {
return {
scope: [
// Locked pre-decisions surfaced so the room sees them, not to relitigate them
{ id: 's1', t: 'IO list (types + instances, COIN schema)', b: 'mvp', lock: true },
{ id: 's2', t: 'Cable list', b: 'mvp', lock: true },
{ id: 's3', t: 'Wire list', b: 'mvp', lock: true },
{ id: 's4', t: 'Construction status tracking (asset level)', b: 'mvp', lock: true },
{ id: 's5', t: 'Commissioning / Cx workflows and signoffs', b: 'out', lock: true },
{ id: 's6', t: 'Blender / 3D visualization app', b: 'later', lock: true },
// Open items, unsorted
{ id: 's7', t: 'Mobile field entry (phone / tablet)', b: '', lock: false },
{ id: 's8', t: 'Offline mode with sync', b: '', lock: false },
{ id: 's9', t: 'Photo attachments on status updates', b: '', lock: false },
{ id: 's10', t: 'Progress rollups by system / area', b: '', lock: false },
{ id: 's11', t: 'Weighted progress rules (per asset class / stage)', b: '', lock: false },
{ id: 's12', t: 'Plan vs actual variance reporting', b: '', lock: false },
{ id: 's13', t: 'Standup / weekly progress report export', b: '', lock: false },
{ id: 's14', t: 'Role based access (PM / Field / QC / Admin)', b: '', lock: false },
{ id: 's15', t: 'QC hold points inside construction tracking', b: '', lock: false },
{ id: 's16', t: 'IWP / work package linkage (AWP alignment)', b: '', lock: false },
{ id: 's17', t: 'controls.dev API read/write for field updates', b: '', lock: false },
{ id: 's18', t: 'PrimeFlow import (legacy IO data)', b: '', lock: false },
{ id: 's19', t: 'Acumatica integration (cost codes / hours)', b: '', lock: false },
{ id: 's20', t: 'Parts / device library management', b: '', lock: false },
{ id: 's21', t: 'Panel / termination level granularity', b: '', lock: false },
{ id: 's22', t: 'Dashboards with R/Y/G indicators', b: '', lock: false },
{ id: 's23', t: 'Labor / crew hours tracking', b: '', lock: false },
{ id: 's24', t: 'GC / owner tool data exchange (Micron side)', b: '', lock: false },
{ id: 's25', t: 'Project templates and cloning', b: '', lock: false },
{ id: 's26', t: 'Audit trail (user / timestamp on every update)', b: '', lock: false },
{ id: 's27', t: 'Change / revision handling for engineering data', b: '', lock: false },
{ id: 's28', t: 'Excel import/export round trip', b: '', lock: false },
{ id: 's29', t: 'Training materials and quick start guides', b: '', lock: false },
{ id: 's30', t: 'In app notifications / @mentions', b: '', lock: false }
],
rank: [
{ id: 'r1', t: 'Mobile field entry', n: 'Named top priority in prior sessions' },
{ id: 'r2', t: 'Reporting (standup + rollups)', n: '' },
{ id: 'r3', t: 'Integration and phasing (controls.dev sync)', n: '' },
{ id: 'r4', t: 'Role based access', n: '' },
{ id: 'r5', t: 'Ease of training and adoption', n: '' },
{ id: 'r6', t: 'Plan vs actual variance', n: '' },
{ id: 'r7', t: 'Weighted progress rules', n: '' }
],
cutAfter: 5,
decisions: [],
ready: [
{ id: 'd1', g: 'Data', t: 'Micron IO list source identified and format confirmed', w: 'Which system of record, which export format, who provides it', o: '', s: 0 },
{ id: 'd2', g: 'Data', t: 'Cable and wire list data availability confirmed', w: '', o: '', s: 0 },
{ id: 'd3', g: 'Data', t: 'Asset hierarchy agreed (Building > System > Panel > Device > Termination)', w: 'Depth of tracking granularity for the pilot', o: '', s: 0 },
{ id: 'd4', g: 'Data', t: 'Import format and refresh cadence agreed with engineering', w: 'One time load vs live sync for the pilot', o: '', s: 0 },
{ id: 'd5', g: 'Platform', t: 'controls.dev instance provisioned for Micron project', w: '', o: 'Cameron', s: 0 },
{ id: 'd6', g: 'Platform', t: 'API endpoints ready for field status writes', w: '', o: 'Cameron', s: 0 },
{ id: 'd7', g: 'Platform', t: 'Role definitions configured (PM / Field / QC / Admin)', w: '', o: '', s: 0 },
{ id: 'd8', g: 'Field', t: 'Field devices confirmed (tablets / phones, count and type)', w: '', o: 'Wyman', s: 0 },
{ id: 'd9', g: 'Field', t: 'Fab site network / device security constraints understood', w: 'Semiconductor site restrictions on devices, cameras, connectivity', o: 'Wyman', s: 0 },
{ id: 'd10', g: 'Field', t: 'Field team availability windows confirmed with Micron schedule', w: '', o: 'Wyman', s: 0 },
{ id: 'd11', g: 'People', t: 'Pilot cohort named, including at least one named skeptic', w: 'Change management framework requirement', o: 'Nick', s: 0 },
{ id: 'd12', g: 'People', t: 'Friction routing loop defined (who owns fixing what the field reports)', w: '', o: '', s: 0 },
{ id: 'd13', g: 'People', t: 'Training approach and materials scoped', w: '', o: '', s: 0 },
{ id: 'd14', g: 'Governance', t: 'Pilot success metrics defined (behavioral, not sentiment)', w: 'Update compliance, entry latency, duplicate entry eliminated; not satisfaction scores', o: 'Nick', s: 0 },
{ id: 'd15', g: 'Governance', t: 'Explicit stop path defined (what triggers pausing the pilot)', w: '', o: '', s: 0 },
{ id: 'd16', g: 'Governance', t: 'Baseline current state process documented before go live', w: 'Avoids reconciliation gap with change management framework', o: '', s: 0 }
]
};
}

View File

@@ -0,0 +1,189 @@
// Baseline state and static reference data for the Field Problem Workshop.
// This is the "July 21 session" starting point the tool resets to.
import type {
Breadcrumb,
Cluster,
Problem,
ProblemType,
WorkshopState
} from '$lib/types';
export const PEOPLE = [
'Nick',
'Tim',
'Jason',
'Brendan',
'Lee',
'Cody',
'Clinton',
'Wyman',
'Cameron',
'Ryan',
'Team'
];
export const TYPES: ProblemType[] = ['Problem', 'Question', 'Idea'];
// Theme-analysis suggestions shown as "ghost" rows inside each cluster.
export const SUGGESTIONS: { cluster: string; t: string }[] = [
{ cluster: 'c1', t: 'There is no shared definition of what counts as complete for each asset stage, so reported percent complete means different things by team' },
{ cluster: 'c1', t: 'Percent complete is count based, not effort based: a 2 hour device and a 2 day panel move the number the same amount' },
{ cluster: 'c1', t: 'Turnover readiness by system is not visible, so incomplete predecessor work is discovered at commissioning' },
{ cluster: 'c2', t: 'The field gets no feedback that their updates were used, so the discipline to enter data decays' },
{ cluster: 'c2', t: 'As-built and redline data is not captured at completion, so closeout becomes a reconstruction project' },
{ cluster: 'c2', t: 'Each project invents its own tracker (Excel, paper, whiteboard), so nothing rolls up across projects' },
{ cluster: 'c3', t: 'Permits, LOTO, and energization requirements are not confirmed before work is released, causing same day stand downs' },
{ cluster: 'c3', t: 'Work areas are not confirmed released by the GC or other trades before crews are sent' },
{ cluster: 'c3', t: 'There is no defined readiness check before work is released; each foreman decides on their own what to verify' },
{ cluster: 'c4', t: 'Crews do not see a lookahead: the plan lives with the PM or GF, so the next assignment travels verbally' },
{ cluster: 'c4', t: 'Commitments made at standup are not tracked, so we never know what percent of promised work actually completed' },
{ cluster: 'c4', t: 'There is no visibility to which crews or skills free up when, so reassignment is reactive' },
{ cluster: 'c5', t: 'Extra work and T&M are not captured at the time they are performed, so recoverable cost is lost' },
{ cluster: 'c5', t: 'There is no single owner of the change log; pending changes live in email threads' },
{ cluster: 'c5', t: 'Scope splits at contract boundaries are not documented anywhere the field can check' },
{ cluster: 'c6', t: 'There is no traceable record of who installed or verified a termination, so failure investigations start from zero' },
{ cluster: 'c6', t: 'The field cannot tell whether the drawing in hand is the current revision without calling the office' },
{ cluster: 'c6', t: 'QA hold points are not enforced by the process; skipping a gate has no visible consequence until rework' },
{ cluster: 'c7', t: 'Estimating does not receive field actuals in a usable form, so estimates cannot learn from performance' }
];
function cluster(id: string, name: string, root: string, ws = ''): Cluster {
return { id, name, root, ws, open: false, dots: 0, promoted: false };
}
function problem(
id: string,
t: string,
who: string,
cluster: string,
type: ProblemType = 'Problem'
): Problem {
return { id, t, who, cluster, type, sugg: false };
}
function breadcrumb(o: Partial<Breadcrumb> & { id: string }): Breadcrumb {
return {
example: false,
type: 'Outcome',
prob: '',
related: [],
driver: '',
cap: '',
metric: '',
method: '',
baseline: '',
target: '',
...o
};
}
export function workshopDefaultState(): WorkshopState {
return {
clusters: [
cluster('c1', 'Progress Visibility', 'We cannot answer where a project stands: no trustworthy, phase level percent complete exists', 'Tracking MVP (pilot)'),
cluster('c2', 'Data Freshness & Entry Burden', 'Field data is captured late, on paper, or more than once, so the record is stale and expensive to maintain'),
cluster('c3', 'Constraint Readiness', 'Work is released before it is ready: material, tools, and information are not confirmed available before crews start', 'IWP Tool Suite'),
cluster('c4', 'Planning & Resources', 'Crews and planners lack visibility to backlog, priorities, and resource availability, producing idle time and churn'),
cluster('c5', 'Scope Control', 'Scope changes and gaps move faster than our change control: work happens without approval or falls out entirely', 'AWP Process RFP'),
cluster('c6', 'Quality & Spec Conformance', 'The field cannot reliably build to the approved design: specs, revisions, and QA gates do not reach the point of work', 'AWP Process RFP'),
cluster('c7', 'Knowledge & Integration', 'What we learn does not travel: knowledge stays with individuals and external platforms stay closed to us')
],
problems: [
problem('p01', 'No one can reliably answer "where are we at / what percent complete" on a project', 'Jason', 'c1'),
problem('p05', 'Percent complete is needed by phase (engineering, BIM, tray/conduit), not just one overall number', 'Jason', 'c1'),
problem('p27', 'Work started but not finished. (ie field hits constraint and forgets to return to the scope)', 'Cody', 'c1'),
problem('p17', 'Constraints (ie trade stacking) not being captured in real time. No visibility to GF / Super loosing the ability to better communicate at stand up meetings', 'Nick', 'c1'),
problem('p34', 'Install at risk - BIM has no visibility to what has been installed without being captured in the model.', 'Team', 'c1'),
problem('p02', 'Field data goes stale because updates are not entered when the work completes', 'Tim', 'c2'),
problem('p37', 'Data is being captured in the field on paper and brought back to the office to be converted to digital (ie QA check lists, Time sheets, daily log)', 'Team', 'c2'),
problem('p08', 'Crews walk back to the trailer to turn in daily logs and forms', 'Tim', 'c2'),
problem('p09', 'Getting assets, IO, and cable lists into a workable relational database takes heavy manual entry', 'Jason', 'c2'),
problem('p10', 'Double entry between our tools and the GC platform on every project', 'Jason', 'c2'),
problem('p36', 'cable pull list has duplicate or missing cable', 'Team', 'c2'),
problem('p28', 'material is not available when the scope is executed.', 'Nick', 'c3'),
problem('p29', 'Equipment / tools are not available when the scope is being executed (ladders, lift)', 'Team', 'c3'),
problem('p06', 'We overspend on material because nobody knows where material is', 'Nick', 'c3'),
problem('p18', 'working with incomplete data sets (ie installing with no BIM)', 'Cody', 'c3'),
problem('p22', 'poor visibility to predecessors / requirements for the work being done. (ie if a peice of equipment needs to be commissioned but it requires PTP backbone to be operational on the other side of the building))', 'Tim', 'c3'),
problem('p04', 'Idle time: people stand around waiting for their next assignment', 'Tim', 'c4'),
problem('p14', 'Foreman dont have good visibility to work backlog causing Idle time.', 'Nick', 'c4'),
problem('p16', 'Bim starts work on priority 1, then told to stop and work on priority 3, this repeats', 'Cody', 'c4'),
problem('p19', 'Resource availability is not availible to project teams (ie if a PM needs a tech, where can they get one?)', 'Nick', 'c4'),
problem('p42', 'Progress halts due to loss of resource (ie pto, terminated)', 'Clinton', 'c4'),
problem('p30', 'Scope takes longer than expected (could this be mitigated by including the field team in estimates)', 'Team', 'c4'),
problem('p03', 'scope gap - scope performed without approved change order.', 'Clinton', 'c5'),
problem('p40', 'scope gap - in scope not performed as it was lost in pre-contruction activities (ie dropped from the io list)', 'Clinton', 'c5'),
problem('p41', 'Scope Gap - who terminates the back bone fiber?', 'Clinton', 'c5'),
problem('p35', 'Field team taking direction from external sources causing rework as it was not approved through the proper channels.', 'Team', 'c5'),
problem('p33', 'When field team deviates from plan failure to communicate that back to design (ie redlines during the course of work rather than at the end)', 'Team', 'c5'),
problem('p07', 'Field rework happens because crews do not have the latest drawings', 'Nick', 'c6'),
problem('p15', 'Rework caused by construction tracking not having the correct data', 'Cody', 'c6'),
problem('p23', 'Failure in sequencing, missing the QA gate (ie terminating a cable that is TSP when it should have been a 4 Conductor)', 'Cody', 'c6'),
problem('p24', 'wrong type of conduit installed due to environmental requirements ( ie Class 1 div 2 area had EMT installed) failure mode: out of spec', 'Clinton', 'c6'),
problem('p20', 'installed equipment doesnt match submittals', 'Brendan', 'c6'),
problem('p21', 'install material used was does not match whats approved in the submittal', 'Nick', 'c6'),
problem('p25', 'Specs not accessible to field installation teams', 'Clinton', 'c6'),
problem('p31', 'visability to chain of custody of project scope, drawings, design intent (who do I ask when constructability issue arises)', 'Team', 'c6'),
problem('p32', 'field teams fail to follow the designed workflow/ install.', 'Team', 'c6'),
problem('p13', 'Punch list / issue follow-through is not measured; time to close is unknown', 'Nick', 'c7'),
problem('p43', 'Tribal knowledge is lost after someone leaves or is on PTO', 'Clinton', 'c7'),
problem('p38', 'Lessons learned - Construction methods could be more efficient with one team and this is not shared with the other.', 'Team', 'c7'),
problem('p39', 'What are all of the ways we are reinventing the wheel?', 'Team', 'c7', 'Question'),
problem('p26', 'Suggestion improvement: create info graphics and other simplified ways of communicating project specs', 'Clinton', 'c7', 'Idea'),
problem('p11', 'GC platform sync/access requests get denied; we have never reached the people who actually own the decision', 'Tim', 'c7'),
problem('p12', 'Platform changes GC to GC, so crews face retraining on a new tool nearly every project', 'Brendan', 'c7')
],
breadcrumbs: [
breadcrumb({
id: 'b1',
example: true,
type: 'Outcome',
prob: 'Crews walk back to the trailer to turn in daily logs and forms',
driver: 'Minimize non productive field time; keep craft hours on the work face',
cap: 'Mobile field entry from the work area (phone / tablet, offline capable)',
metric: '% of daily logs submitted from the work area on the same day',
method: 'System timestamps on log submission vs work date',
baseline: 'Field poll of minutes per day spent on paperwork and walking, taken before pilot'
}),
breadcrumb({
id: 'b2',
type: 'Outcome',
prob: 'No one can reliably answer "where are we at / what percent complete" on a project',
driver: 'man power / work planning',
cap: 'construction tracking / resource planning',
metric: 'do we have a tool, tool has been implemented and adopted, ~>80% visibility to the project progress'
}),
breadcrumb({ id: 'b3', type: 'Outcome', prob: 'Field data goes stale because updates are not entered when the work completes' }),
breadcrumb({ id: 'b4', type: 'Outcome', prob: 'scope gap - scope performed without approved change order.' }),
breadcrumb({
id: 'b5',
type: 'Outcome',
prob: 'Double entry between our tools and the GC platform on every project',
driver: 'Reduced duplicate entry',
cap: 'Single system of entry with automated propagation',
metric: 'Number of systems requiring manual entry of the same record',
method: 'Trace 10 representative records end to end',
baseline: 'Count on a current project before pilot',
target: '1 internal system of entry'
}),
breadcrumb({
id: 'b6',
type: 'Adoption',
prob: 'Supporting adoption measure for the pilot',
driver: 'Field usage',
cap: 'Pilot tool in daily field use',
metric: '% of assigned field users active weekly',
method: 'Platform usage analytics',
baseline: 'n/a, starts at zero'
})
],
actions: [
{ id: 'a1', t: 'Review cluster assignments and root problem statements as a team; move issues that landed in the wrong cluster', owner: '', due: '', done: false },
{ id: 'a2', t: 'Complete breadcrumb: Field data goes stale because updates are not entered when the work completes', owner: '', due: '', done: false },
{ id: 'a3', t: 'Complete breadcrumb: scope gap - scope performed without approved change order.', owner: '', due: '', done: false },
{ id: 'a4', t: 'Rework the percent complete breadcrumb: current metric is an adoption statement, needs a real outcome metric with method and baseline', owner: '', due: '', done: false },
{ id: 'a5', t: 'Assign workstreams to the three unmapped clusters (Data Freshness, Planning & Resources, Knowledge & Integration)', owner: '', due: '', done: false }
]
};
}

83
src/lib/server/ai.ts Normal file
View File

@@ -0,0 +1,83 @@
// 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)
};
}

View File

@@ -0,0 +1,27 @@
// Postgres connection pool. Created lazily on first use so the module can be
// imported during build/analysis without DATABASE_URL being present.
import pg from 'pg';
import { env } from '$env/dynamic/private';
let pool: pg.Pool | null = null;
function getPool(): pg.Pool {
if (pool) return pool;
const connectionString = env.DATABASE_URL;
if (!connectionString) {
// Fail loudly on first query rather than mysteriously later.
throw new Error(
'DATABASE_URL is not set. Copy .env.example to .env (or set it in docker-compose) and try again.'
);
}
pool = new pg.Pool({ connectionString });
return pool;
}
/** Small helper so call sites read as `query(sql, params)`. */
export function query<T extends pg.QueryResultRow = pg.QueryResultRow>(
text: string,
params?: unknown[]
): Promise<pg.QueryResult<T>> {
return getPool().query<T>(text, params);
}

View File

@@ -0,0 +1,107 @@
// Repository for reading and writing tool workspaces.
//
// Concurrency model: optimistic locking on an integer `version`. A client loads
// a workspace (getting its version), edits locally, then saves with that same
// version as `baseVersion`. If nobody else saved in between, the row's version
// still matches and the write wins (bumping version). If someone else saved
// first, the versions differ, no row updates, and we report a conflict so the
// client can reload instead of clobbering their teammate.
import { query } from './index';
import { defaultStateFor } from '$lib/defaults';
import type { ToolId, WorkspaceEnvelope } from '$lib/types';
interface Row {
tool: ToolId;
name: string;
state: unknown;
version: number;
updated_at: Date;
updated_by: string | null;
}
function toEnvelope<T>(row: Row): WorkspaceEnvelope<T> {
return {
tool: row.tool,
name: row.name,
state: row.state as T,
version: row.version,
updatedAt: row.updated_at.toISOString(),
updatedBy: row.updated_by
};
}
/**
* Load a workspace. If it does not exist yet, seed it from the tool's default
* baseline state so the first visitor sees a populated, useful tool.
*/
export async function getOrCreateWorkspace<T>(
tool: ToolId,
name: string
): Promise<WorkspaceEnvelope<T>> {
const existing = await query<Row>(
'SELECT tool, name, state, version, updated_at, updated_by FROM workspaces WHERE tool = $1 AND name = $2',
[tool, name]
);
if (existing.rows.length > 0) {
return toEnvelope<T>(existing.rows[0]);
}
const seed = defaultStateFor(tool);
// ON CONFLICT guards against a race where two first-visitors insert at once.
const inserted = await query<Row>(
`INSERT INTO workspaces (tool, name, state)
VALUES ($1, $2, $3)
ON CONFLICT (tool, name) DO UPDATE SET tool = EXCLUDED.tool
RETURNING tool, name, state, version, updated_at, updated_by`,
[tool, name, seed]
);
return toEnvelope<T>(inserted.rows[0]);
}
export type SaveResult<T> =
| { ok: true; workspace: WorkspaceEnvelope<T> }
| { ok: false; conflict: WorkspaceEnvelope<T> };
/** Save state with optimistic concurrency. */
export async function saveWorkspace<T>(
tool: ToolId,
name: string,
state: T,
baseVersion: number,
updatedBy: string | null
): Promise<SaveResult<T>> {
const updated = await query<Row>(
`UPDATE workspaces
SET state = $3, version = version + 1, updated_at = now(), updated_by = $5
WHERE tool = $1 AND name = $2 AND version = $4
RETURNING tool, name, state, version, updated_at, updated_by`,
[tool, name, state, baseVersion, updatedBy]
);
if (updated.rows.length === 1) {
return { ok: true, workspace: toEnvelope<T>(updated.rows[0]) };
}
// No row updated: either the workspace does not exist, or the version moved.
// Return the current server copy so the client can reconcile.
const current = await getOrCreateWorkspace<T>(tool, name);
return { ok: false, conflict: current };
}
/** Reset a workspace back to the tool's baseline. */
export async function resetWorkspace<T>(
tool: ToolId,
name: string
): Promise<WorkspaceEnvelope<T>> {
const seed = defaultStateFor(tool);
const reset = await query<Row>(
`INSERT INTO workspaces (tool, name, state)
VALUES ($1, $2, $3)
ON CONFLICT (tool, name)
DO UPDATE SET state = EXCLUDED.state, version = workspaces.version + 1, updated_at = now(), updated_by = NULL
RETURNING tool, name, state, version, updated_at, updated_by`,
[tool, name, seed]
);
return toEnvelope<T>(reset.rows[0]);
}

15
src/lib/toast.svelte.ts Normal file
View File

@@ -0,0 +1,15 @@
// Tiny shared toast controller. Import `toast` anywhere and call toast.show('...').
class ToastController {
message = $state('');
visible = $state(false);
#timer: ReturnType<typeof setTimeout> | null = null;
show(msg: string, ms = 2200): void {
this.message = msg;
this.visible = true;
if (this.#timer) clearTimeout(this.#timer);
this.#timer = setTimeout(() => (this.visible = false), ms);
}
}
export const toast = new ToastController();

115
src/lib/types.ts Normal file
View File

@@ -0,0 +1,115 @@
// Shared data types for both meeting tools. These describe exactly what gets
// stored as the JSON "state" of a workspace in the database.
export type ToolId = 'workshop' | 'scope-lock';
// ---------- Field Problem Workshop ----------
export type ProblemType = 'Problem' | 'Question' | 'Idea';
export interface Problem {
id: string;
t: string; // the problem text
who: string; // who raised it
cluster: string; // cluster id, or '' if unclustered
type: ProblemType;
sugg: boolean; // came from theme-analysis suggestions
}
export interface Cluster {
id: string;
name: string;
root: string; // root problem statement
ws: string; // workstream label
open: boolean;
dots: number; // dot-voting count
promoted: boolean;
}
export type BreadcrumbType = 'Outcome' | 'Adoption';
export interface Breadcrumb {
id: string;
example: boolean;
type: BreadcrumbType;
prob: string;
related: string[];
driver: string;
cap: string;
metric: string;
method: string;
baseline: string;
target: string;
}
export interface WorkshopAction {
id: string;
t: string;
owner: string;
due: string;
done: boolean;
}
export interface WorkshopState {
clusters: Cluster[];
problems: Problem[];
breadcrumbs: Breadcrumb[];
actions: WorkshopAction[];
}
// ---------- Scope Lock Suite ----------
export type ScopeBucket = '' | 'mvp' | 'later' | 'out' | 'park';
export interface ScopeItem {
id: string;
t: string;
b: ScopeBucket; // bucket
lock: boolean; // locked pre-decision
}
export interface RankItem {
id: string;
t: string;
n: string; // note
}
export type DecisionStatus = 'Locked' | 'Provisional' | 'Revisit';
export interface Decision {
text: string;
type: string; // Scope / Priority / Technical / Process / ...
owner: string;
status: DecisionStatus;
why: string; // rationale
trade: string; // trade-off accepted
at: string; // time logged, e.g. "02:15 PM"
}
export interface ReadyItem {
id: string;
g: string; // group (Data / Platform / Field / People / Governance)
t: string; // item text
w: string; // "what it means" note
o: string; // owner
s: number; // status (0 = not started, etc.)
}
export interface ScopeLockState {
scope: ScopeItem[];
rank: RankItem[];
cutAfter: number;
decisions: Decision[];
ready: ReadyItem[];
}
// ---------- Workspace envelope (what the API returns) ----------
export interface WorkspaceEnvelope<T> {
tool: ToolId;
name: string;
state: T;
version: number;
updatedAt: string;
updatedBy: string | null;
}

138
src/lib/workspace.svelte.ts Normal file
View File

@@ -0,0 +1,138 @@
// Reactive client-side workspace synchronizer.
//
// Usage in a component:
// const ws = new Workspace<WorkshopState>('workshop', 'default');
// onMount(() => ws.init());
// ... read/write ws.state ...; call ws.touch() after any mutation.
//
// It debounces saves, uses the version returned by the server for optimistic
// concurrency, and polls so another person's saves appear here too.
import type { ToolId, WorkspaceEnvelope } from './types';
export type SyncStatus = 'loading' | 'idle' | 'saving' | 'saved' | 'conflict' | 'error';
const SAVE_DEBOUNCE_MS = 700;
const POLL_INTERVAL_MS = 6000;
export class Workspace<T> {
tool: ToolId;
name: string;
state = $state<T | null>(null);
version = $state(0);
status = $state<SyncStatus>('loading');
updatedAt = $state<string | null>(null);
message = $state('');
#saveTimer: ReturnType<typeof setTimeout> | null = null;
#pollTimer: ReturnType<typeof setInterval> | null = null;
#dirty = false; // local edits not yet saved
#saving = false;
constructor(tool: ToolId, name = 'default') {
this.tool = tool;
this.name = name;
}
get base(): string {
return `/api/workspaces/${this.tool}/${encodeURIComponent(this.name)}`;
}
async init(): Promise<void> {
await this.load();
this.#pollTimer = setInterval(() => this.#poll(), POLL_INTERVAL_MS);
}
destroy(): void {
if (this.#saveTimer) clearTimeout(this.#saveTimer);
if (this.#pollTimer) clearInterval(this.#pollTimer);
}
async load(): Promise<void> {
this.status = 'loading';
try {
const res = await fetch(this.base);
if (!res.ok) throw new Error(`load failed (${res.status})`);
const env = (await res.json()) as WorkspaceEnvelope<T>;
this.#adopt(env);
this.status = 'idle';
} catch (e) {
this.status = 'error';
this.message = e instanceof Error ? e.message : 'Could not load';
}
}
#adopt(env: WorkspaceEnvelope<T>): void {
this.state = env.state;
this.version = env.version;
this.updatedAt = env.updatedAt;
this.#dirty = false;
}
/** Call after mutating `state`. Schedules a debounced save. */
touch(): void {
this.#dirty = true;
this.status = 'saving';
if (this.#saveTimer) clearTimeout(this.#saveTimer);
this.#saveTimer = setTimeout(() => this.save(), SAVE_DEBOUNCE_MS);
}
async save(): Promise<void> {
if (this.#saving || this.state === null) return;
this.#saving = true;
try {
const res = await fetch(this.base, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ state: $state.snapshot(this.state), baseVersion: this.version })
});
if (res.status === 409) {
// Someone else saved first. Adopt their copy so we don't clobber it.
const { current } = (await res.json()) as { current: WorkspaceEnvelope<T> };
this.#adopt(current);
this.status = 'conflict';
this.message = 'A teammate updated this workspace; their changes were loaded.';
return;
}
if (!res.ok) throw new Error(`save failed (${res.status})`);
const env = (await res.json()) as WorkspaceEnvelope<T>;
this.version = env.version;
this.updatedAt = env.updatedAt;
this.#dirty = false;
this.status = 'saved';
} catch (e) {
this.status = 'error';
this.message = e instanceof Error ? e.message : 'Could not save';
} finally {
this.#saving = false;
}
}
async reset(): Promise<void> {
const res = await fetch(`${this.base}/reset`, { method: 'POST' });
if (res.ok) {
this.#adopt((await res.json()) as WorkspaceEnvelope<T>);
this.status = 'idle';
}
}
async #poll(): Promise<void> {
// Don't stomp on unsaved local edits or an in-flight save.
if (this.#dirty || this.#saving) return;
try {
const res = await fetch(this.base);
if (!res.ok) return;
const env = (await res.json()) as WorkspaceEnvelope<T>;
if (env.version > this.version) this.#adopt(env);
} catch {
// Ignore transient poll failures.
}
}
}
export function uid(): string {
return 'x' + Math.random().toString(36).slice(2, 9);
}