From 4e6ed044a9a9c344c4f15f580bd408ca187725d7 Mon Sep 17 00:00:00 2001 From: C-West8 <125926137+C-West8@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:27:51 -0500 Subject: [PATCH] cleanup - removing ai agent for deterministic code and better formatting --- .env.example | 8 - README.md | 13 +- docker-compose.yml | 2 - legacy/SDE_Scope_Lock_Meeting_Suite.html | 13 + package-lock.json | 376 +---------------------- package.json | 1 - src/app.css | 7 + src/lib/breadcrumbs.ts | 292 ++++++++++++++++++ src/lib/server/ai.ts | 83 ----- src/routes/+page.svelte | 20 +- src/routes/api/ai/draft/+server.ts | 28 -- src/routes/scope-lock/+page.svelte | 9 + src/routes/workshop/+page.svelte | 71 +++-- 13 files changed, 396 insertions(+), 527 deletions(-) create mode 100644 src/lib/breadcrumbs.ts delete mode 100644 src/lib/server/ai.ts delete mode 100644 src/routes/api/ai/draft/+server.ts diff --git a/.env.example b/.env.example index d68d01c..dc607d0 100644 --- a/.env.example +++ b/.env.example @@ -5,14 +5,6 @@ # When running via docker-compose, this is set for you in docker-compose.yml. DATABASE_URL=postgres://sde:sde_password@localhost:5432/sde_toolkit -# --- Anthropic API (for the "AI draft" feature) --- -# Get a key from https://console.anthropic.com/ . Without it, the AI button -# fails politely and everything else keeps working. -ANTHROPIC_API_KEY= - -# Model used for AI drafting. -ANTHROPIC_MODEL=claude-sonnet-5 - # --- Server --- PORT=3000 diff --git a/README.md b/README.md index 3bfbd04..8e4c76f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A shared, multi-user web app with two meeting tools: -- **Field Problem Workshop** — capture field problems, cluster them into root problems, dot-vote, build "breadcrumb" trails from problem to measurable outcome, and assign actions. Includes an optional AI draft feature. +- **Field Problem Workshop** — capture field problems, cluster them into root problems, dot-vote, build "breadcrumb" trails from problem to measurable outcome, and assign actions. A "Suggest fields" button fills each breadcrumb from built-in construction-execution methodology (no external service). - **Scope Lock Meeting Suite** — Scope Boundary Board, MVP Priority Ranker, Decision Registry, and Micron Pilot Readiness. Everyone who opens the same link works from the **same data**, stored on a shared server. Edits autosave, and a version check stops two people from silently overwriting each other. The original single-file HTML versions are kept in [`legacy/`](./legacy) for reference. @@ -32,14 +32,14 @@ docker compose logs -f app # watch app logs Data persists in a Docker volume (`db_data`), so it survives restarts. -To enable the **AI draft** feature, provide an Anthropic API key before starting — either export `ANTHROPIC_API_KEY` in your shell or put it in a `.env` file next to `docker-compose.yml`. Without a key the AI button simply fails politely; everything else works. +There are no API keys or external accounts to set up. The "Suggest fields" button runs entirely in-app from a built-in methodology table. ## Local development (without Docker) Needs Node 20+ and a PostgreSQL you can reach. ```bash -cp .env.example .env # edit DATABASE_URL (and ANTHROPIC_API_KEY if wanted) +cp .env.example .env # edit DATABASE_URL npm install psql "$DATABASE_URL" -f db/init.sql # create the schema once npm run dev # http://localhost:5173 @@ -54,8 +54,6 @@ All config is via environment variables — see [`.env.example`](./.env.example) | Variable | Purpose | |---|---| | `DATABASE_URL` | Postgres connection string. Set automatically by docker-compose. | -| `ANTHROPIC_API_KEY` | Enables the "AI draft" button. Blank = feature disabled. | -| `ANTHROPIC_MODEL` | Model for AI drafting (default `claude-sonnet-5`). | | `PORT` | Server port (default 3000). | | `ORIGIN` | Public URL the app is served from; used to validate POST origins. | @@ -67,6 +65,7 @@ src/ app.html page shell lib/ types.ts data shapes for both tools + breadcrumbs.ts methodology table + keyword classifier ("Suggest fields") workspace.svelte.ts client sync: load / autosave / poll / conflict handling toast.svelte.ts shared toast controller components/ Toast.svelte, SyncBadge.svelte @@ -74,7 +73,6 @@ src/ server/ db/index.ts Postgres connection pool (lazy) db/workspaces.ts load / save / reset with optimistic concurrency - ai.ts Anthropic proxy (holds the API key server-side) routes/ +layout.svelte imports global CSS +page.svelte home / tool picker @@ -83,7 +81,6 @@ src/ api/ workspaces/[tool]/[name]/+server.ts GET (load) + PUT (save) workspaces/[tool]/[name]/reset/+server.ts POST (reset to baseline) - ai/draft/+server.ts POST (AI breadcrumb draft) db/init.sql database schema (runs once on first DB startup) static/favicon.svg Dockerfile, docker-compose.yml @@ -108,7 +105,7 @@ It requires, on your Gitea instance: Actions enabled with a registered `act_runn ## The `.skill` file -`legacy/construction-breadcrumbs.skill` is a **Claude skill** (a zip of instructions + a methodology knowledge base). It is not part of the website — it teaches Claude how to draft breadcrumb fields using construction methodology (EVM, CII AWP, Last Planner, first-time quality). The same knowledge is embedded server-side in `src/lib/server/ai.ts`, which powers the app's AI draft button. Install the skill in Claude (via "Save skill") to use the methodology in any chat. +`legacy/construction-breadcrumbs.skill` is a **Claude skill** (a zip of instructions + a methodology knowledge base). It is not part of the website — it teaches Claude how to draft breadcrumb fields using construction methodology (EVM, CII AWP, Last Planner, first-time quality). The same methodology is encoded, deterministically and offline, in [`src/lib/breadcrumbs.ts`](./src/lib/breadcrumbs.ts), which powers the app's "Suggest fields" button. Install the skill in Claude (via "Save skill") to use the methodology in any chat. ## Possible next steps diff --git a/docker-compose.yml b/docker-compose.yml index cb4759c..bf029f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,8 +27,6 @@ services: condition: service_healthy environment: DATABASE_URL: postgres://sde:sde_password@db:5432/sde_toolkit - ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} - ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-sonnet-5} PORT: 3000 ORIGIN: ${ORIGIN:-http://localhost:3000} ports: diff --git a/legacy/SDE_Scope_Lock_Meeting_Suite.html b/legacy/SDE_Scope_Lock_Meeting_Suite.html index 7de1b29..87fcb3e 100644 --- a/legacy/SDE_Scope_Lock_Meeting_Suite.html +++ b/legacy/SDE_Scope_Lock_Meeting_Suite.html @@ -224,6 +224,7 @@
Project SDE | Scope Lock Meeting Suite
Micron Pilot | Innovation Team
+ @@ -658,6 +659,18 @@ document.getElementById('btnAddReady').addEventListener('click', ()=>{ }); document.getElementById('newReadyItem').addEventListener('keydown', e=>{ if(e.key==='Enter') document.getElementById('btnAddReady').click(); }); +/* ================= SAVE JSON (raw data dump for migration/import) ================= */ +document.getElementById('btnSaveJson').addEventListener('click', ()=>{ + const stamp = new Date().toISOString().slice(0,10); + const blob = new Blob([JSON.stringify(S, null, 2)], {type:'application/json'}); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = `SDE_Scope_Lock_state_${stamp}.json`; + a.click(); + URL.revokeObjectURL(a.href); + toast('State saved to JSON'); +}); + /* ================= EXPORT ================= */ document.getElementById('btnExport').addEventListener('click', ()=>{ const d = new Date(); diff --git a/package-lock.json b/package-lock.json index 8a1e33e..9219969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "sde-meeting-toolkit", "version": "1.0.0", "dependencies": { - "@anthropic-ai/sdk": "^0.32.0", "pg": "^8.13.0" }, "devDependencies": { @@ -22,21 +21,6 @@ "vite": "^5.4.0" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz", - "integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==", - "license": "MIT", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1136,21 +1120,12 @@ "version": "18.19.130", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } - }, "node_modules/@types/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", @@ -1177,18 +1152,6 @@ "dev": true, "license": "MIT" }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -1202,18 +1165,6 @@ "node": ">=0.4.0" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/aria-query": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", @@ -1224,12 +1175,6 @@ "node": ">= 0.4" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -1240,19 +1185,6 @@ "node": ">= 0.4" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1279,18 +1211,6 @@ "node": ">=6" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1336,15 +1256,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/devalue": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", @@ -1352,65 +1263,16 @@ "dev": true, "license": "MIT" }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1482,15 +1344,6 @@ "dev": true, "license": "MIT" }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1509,41 +1362,6 @@ } } }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1563,91 +1381,17 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1656,15 +1400,6 @@ "node": ">= 0.4" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -1725,36 +1460,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -1779,6 +1484,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -1800,46 +1506,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2245,12 +1911,6 @@ "node": ">=6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2269,6 +1929,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, "license": "MIT" }, "node_modules/vite": { @@ -2351,31 +2012,6 @@ } } }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2332eec..e89f1ca 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "vite": "^5.4.0" }, "dependencies": { - "@anthropic-ai/sdk": "^0.32.0", "pg": "^8.13.0" } } diff --git a/src/app.css b/src/app.css index 4dac305..2078a4c 100644 --- a/src/app.css +++ b/src/app.css @@ -27,12 +27,19 @@ padding: 0; } +html { + scroll-behavior: smooth; +} + body { font-family: 'IBM Plex Sans', 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); font-size: 16px; line-height: 1.4; + min-height: 100vh; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } button { diff --git a/src/lib/breadcrumbs.ts b/src/lib/breadcrumbs.ts new file mode 100644 index 0000000..2a283e5 --- /dev/null +++ b/src/lib/breadcrumbs.ts @@ -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 + }; +} diff --git a/src/lib/server/ai.ts b/src/lib/server/ai.ts deleted file mode 100644 index ff05e71..0000000 --- a/src/lib/server/ai.ts +++ /dev/null @@ -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 { - 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 { - 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) - }; -} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3b28bca..7ac9e65 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -32,6 +32,9 @@ diff --git a/src/routes/api/ai/draft/+server.ts b/src/routes/api/ai/draft/+server.ts deleted file mode 100644 index 7bbad6f..0000000 --- a/src/routes/api/ai/draft/+server.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { error, json } from '@sveltejs/kit'; -import { draftBreadcrumb } from '$lib/server/ai'; -import type { RequestHandler } from './$types'; - -/** - * POST /api/ai/draft — draft breadcrumb fields from a problem statement. - * Body: { prob: string, related?: string[] } - */ -export const POST: RequestHandler = async ({ request }) => { - let body: { prob?: string; related?: string[] }; - try { - body = await request.json(); - } catch { - throw error(400, 'Body must be JSON'); - } - - const prob = String(body.prob ?? '').trim(); - const related = Array.isArray(body.related) ? body.related.map(String) : []; - - try { - const draft = await draftBreadcrumb(prob, related); - return json(draft); - } catch (e) { - const message = e instanceof Error ? e.message : 'AI draft failed'; - // 422: the request was well-formed but the AI step could not complete. - throw error(422, message); - } -}; diff --git a/src/routes/scope-lock/+page.svelte b/src/routes/scope-lock/+page.svelte index 3a70d6c..40ad7fb 100644 --- a/src/routes/scope-lock/+page.svelte +++ b/src/routes/scope-lock/+page.svelte @@ -404,6 +404,9 @@