mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Merge release/v1.39.0 into feat/ai-routes-insights-coach
Brings in the jobs, stored-text and consent package and the extraction routes. Conflicts resolved: - The digest keeps its capability-driven reaction line and check-in, now through the shared `aiCapabilityForRecord`; the local copy of it is removed. - The route inventories classify the extraction routes (documents, labs OCR, medication text, fenced chat and attachments) as actions; a capability call counts as the gate for the module inventory. - With every caller moved, `requireAssistantSurface` and `AssistantDisabledError` are deleted. The `assistant.disabled.*` codes stay on the capability refusal, and the guard now asserts the retired gate is gone. - `aiCapabilityToServe` answers whether stored model text may be shown from the record's own state, apart from who is reading. Refs #975
This commit is contained in:
commit
c509afeb2e
256 files changed
+13048
-4642
No files matched your search
@@ -45,13 +45,18 @@ function code(path: string): string {
|
||||
}
|
||||
|
||||
/** Whitespace-tolerant: a call split across lines still matches. */
|
||||
const GATE_CALL = /\b(requireAiCapability|getAiCapability)\s*\(\s*"(\w+)"/g;
|
||||
const ANY_GATE = /\b(?:requireAiCapability|getAiCapability)\s*\(/;
|
||||
const GATE_CALL =
|
||||
/\b(?:(requireAiCapability|getAiCapability)\s*\(\s*"(\w+)"|(aiCapabilityToServe)\s*\([^,()]*,\s*"(\w+)")/g;
|
||||
const ANY_GATE =
|
||||
/\b(?:requireAiCapability|getAiCapability|aiCapabilityToServe)\s*\(/;
|
||||
const REQUIRE_GATE = /\brequireAiCapability\s*\(/;
|
||||
const RETIRED_GATE = /\brequireAssistantSurface\s*\(/;
|
||||
|
||||
function gateCalls(text: string): Array<{ fn: string; key: string }> {
|
||||
return [...text.matchAll(GATE_CALL)].map((m) => ({ fn: m[1], key: m[2] }));
|
||||
return [...text.matchAll(GATE_CALL)].map((m) => ({
|
||||
fn: m[1] ?? m[3],
|
||||
key: m[2] ?? m[4],
|
||||
}));
|
||||
}
|
||||
|
||||
function routeFiles(): string[] {
|
||||
@@ -81,6 +86,9 @@ describe("AI capability route inventory", () => {
|
||||
{ fn: "requireAiCapability", key: "coach" },
|
||||
]);
|
||||
expect(ANY_GATE.test("getAiCapability (\n")).toBe(true);
|
||||
expect(
|
||||
gateCalls('aiCapabilityToServe(\n user.id,\n "statusText",\n )'),
|
||||
).toEqual([{ fn: "aiCapabilityToServe", key: "statusText" }]);
|
||||
});
|
||||
|
||||
it("finds the routes it is meant to judge", () => {
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
*
|
||||
* - `action` routes call `requireAiCapability(key)` and refuse with the
|
||||
* capability envelope;
|
||||
* - `mixed` reads call `getAiCapability(key)`, answer 200, null the model
|
||||
* text and say why in `ai`. They never refuse for an AI reason, so they
|
||||
* never call `requireAiCapability`.
|
||||
* - `mixed` reads call `getAiCapability(key)` (the viewer's view, for work
|
||||
* they would start) or `aiCapabilityToServe(recordId, key)` (the record's
|
||||
* own view, for stored model text they would show), answer 200, null the
|
||||
* model text and say why in `ai`. They never refuse for an AI reason, so
|
||||
* they never call `requireAiCapability`.
|
||||
*
|
||||
* Read by two guards: `ai-capability-route-inventory.test.ts` (the whole API
|
||||
* tree) and `insights/__tests__/coach-route-gate-inventory.test.ts` (every
|
||||
@@ -29,6 +31,67 @@ export interface AiRouteEntry {
|
||||
}
|
||||
|
||||
export const AI_ROUTES: Readonly<Record<string, AiRouteEntry>> = {
|
||||
// Reading documents, lab scans and medication text.
|
||||
"src/app/api/documents/inbound/[id]/chat/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["documentAi"],
|
||||
why: "POST asks a model about one document; the history GET asks nothing.",
|
||||
},
|
||||
"src/app/api/documents/inbound/[id]/extract/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["documentAi"],
|
||||
why: "Reads a document's text with a model.",
|
||||
},
|
||||
"src/app/api/documents/inbound/[id]/index/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["documentAi"],
|
||||
why: "Indexes a document's content through a provider.",
|
||||
},
|
||||
"src/app/api/documents/inbound/[id]/suggest/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["documentAi"],
|
||||
why: "Suggests a title and category from the document with a model.",
|
||||
},
|
||||
"src/app/api/documents/inbound/[id]/summary/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["documentAi"],
|
||||
why: "Summarises a document with a model.",
|
||||
},
|
||||
"src/app/api/documents/inbound/reindex/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["documentAi"],
|
||||
why: "Re-indexes the vault through a provider.",
|
||||
},
|
||||
"src/app/api/documents/inbound/route.ts": {
|
||||
kind: "mixed",
|
||||
capabilities: ["documentAi"],
|
||||
why: "The upload is data and always accepted; only the AI work queued after it follows the capability.",
|
||||
},
|
||||
"src/app/api/auth/me/documents-auto-ai-read/route.ts": {
|
||||
kind: "mixed",
|
||||
capabilities: ["documentAi"],
|
||||
why: "The preference is stored whatever AI says; the catch-up it queues follows the capability.",
|
||||
},
|
||||
"src/app/api/insights/chat/fenced/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["coach", "documentAi"],
|
||||
why: "Document chat inside a Coach conversation.",
|
||||
},
|
||||
"src/app/api/insights/chat/[id]/attachments/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["coach", "documentAi"],
|
||||
why: "Attaching a document to a Coach conversation for the model to read.",
|
||||
},
|
||||
"src/app/api/labs/ocr/extract/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["labsOcr"],
|
||||
why: "Reads a lab report image with a model.",
|
||||
},
|
||||
"src/app/api/medications/extract/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["medicationExtract"],
|
||||
why: "Turns typed medication text into a schedule with a model.",
|
||||
},
|
||||
"src/app/api/insights/chat/route.ts": {
|
||||
kind: "action",
|
||||
capabilities: ["coach"],
|
||||
@@ -189,23 +252,19 @@ export const DATA_ROUTES: Readonly<Record<string, string>> = {
|
||||
"src/app/api/coach/reminders/[id]/route.ts": "One stored Coach reminder.",
|
||||
"src/app/api/coach/reminder-suggestions/route.ts":
|
||||
"Deterministic reminder suggestions.",
|
||||
"src/app/api/insights/chat/[id]/attachments/[documentId]/route.ts":
|
||||
"Detaching a document: removal of one's own data.",
|
||||
"src/app/api/coach/suggested-actions/route.ts":
|
||||
"Confirms a proposed action from a closed allowlist; no model call.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Routes in the AI trees whose AI gating another change owns. Each still has
|
||||
* Routes in the AI trees whose AI gating another change still owns. Each has
|
||||
* to be classified; the entry names who moves it. An entry that starts
|
||||
* calling a capability gate fails the guard until it moves to `AI_ROUTES`.
|
||||
* Empty: every route in these trees is classified.
|
||||
*/
|
||||
export const PENDING_ROUTES: Readonly<Record<string, string>> = {
|
||||
"src/app/api/insights/chat/fenced/route.ts":
|
||||
"Document chat inside a conversation: moves to `coach` and `documentAi` with the document-extraction routes.",
|
||||
"src/app/api/insights/chat/[id]/attachments/route.ts":
|
||||
"Attaching a document to a conversation: moves to `coach` and `documentAi` with the document-extraction routes.",
|
||||
"src/app/api/insights/chat/[id]/attachments/[documentId]/route.ts":
|
||||
"Detaching a document: removal of one's own data, stays ungated once the document routes move.",
|
||||
};
|
||||
export const PENDING_ROUTES: Readonly<Record<string, string>> = {};
|
||||
|
||||
/** The trees in which every route has to be classified above. */
|
||||
export const AI_ROUTE_TREES = [
|
||||
|
||||
@@ -42,9 +42,6 @@ import { MODULE_KEYS, type ModuleKey } from "@/lib/modules/registry";
|
||||
* module is the AI analysis opt-out and gates only AI parts;
|
||||
* the data routes that happen to live under
|
||||
* `/api/insights` are EXEMPT below.
|
||||
* - coach → `requireAssistantSurface("coach")`, the retired gate,
|
||||
* still recognised for the one route outside these trees
|
||||
* that has not moved to its capability yet.
|
||||
*
|
||||
* 3. EXEMPT — an explicit, COMMENTED allowlist of routes that serve a
|
||||
* toggleable domain but are deliberately NOT gated, each with the
|
||||
@@ -253,10 +250,9 @@ const EXEMPT_ROUTES: ReadonlyArray<string> = [
|
||||
// affordance), not the row store. The extract route is read-only vision
|
||||
// assistance, the commit route writes the user's own confirmed lab rows,
|
||||
// and the capability probe is an infra availability check carrying no
|
||||
// module data. All three are owner-scoped and AI-gated (consent / budget /
|
||||
// rate); the module toggle hiding the surface does not need to wedge them.
|
||||
// module data. The extract route is not listed: it names the `labsOcr` AI
|
||||
// capability, which carries the labs module, so it counts as gated.
|
||||
"src/app/api/labs/ocr/capability/route.ts",
|
||||
"src/app/api/labs/ocr/extract/route.ts",
|
||||
"src/app/api/labs/ocr/commit/route.ts",
|
||||
// ── DATA LAYER (medications) ──────────────────────────────────────
|
||||
// v1.18.1 (D3) — medications graduated from CORE to a toggleable module,
|
||||
@@ -277,9 +273,9 @@ const EXEMPT_ROUTES: ReadonlyArray<string> = [
|
||||
// per-medication importer below: it writes intake rows and reads its own job.
|
||||
"src/app/api/medications/intake/dose-history-import/route.ts",
|
||||
"src/app/api/medications/intake/dose-history-import/[jobId]/status/route.ts",
|
||||
// NB: `medications/extract` is NOT exempt — it gates on
|
||||
// `requireAssistantSurface("coach")` (the NL-extraction is an assistant
|
||||
// surface), so the inventory already counts it as a delegated gate.
|
||||
// `medications/extract` is not listed: it is model work under the
|
||||
// `medicationExtract` AI capability (no owning module; medications is a
|
||||
// core domain for extraction), and a capability call counts as the gate.
|
||||
"src/app/api/medications/[id]/route.ts",
|
||||
"src/app/api/medications/[id]/api-endpoint/route.ts",
|
||||
"src/app/api/medications/[id]/cadence/route.ts",
|
||||
@@ -421,10 +417,10 @@ const EXEMPT_ROUTES: ReadonlyArray<string> = [
|
||||
|
||||
const MODULE_GATE_NEEDLE = "requireModuleEnabled(";
|
||||
const CYCLE_GATE_NEEDLE = "requireCycleEnabled(";
|
||||
const COACH_GATE_NEEDLE = 'requireAssistantSurface("coach")';
|
||||
// An AI capability resolves its owning modules and the AI opt-out, so a route
|
||||
// that names one is gated on them.
|
||||
const AI_CAPABILITY_GATE = /\b(?:requireAiCapability|getAiCapability)\s*\(/;
|
||||
const AI_CAPABILITY_GATE =
|
||||
/\b(?:requireAiCapability|getAiCapability|aiCapabilityToServe)\s*\(/;
|
||||
// v1.18.1 — the illness journal's thin gate wrapper. `requireIllnessEnabled`
|
||||
// delegates to `requireModuleEnabled(userId, "illness")` and re-stamps the
|
||||
// illness-specific errorCode, exactly mirroring how `cycle` delegates to
|
||||
@@ -679,7 +675,6 @@ describe("module API route gate inventory", () => {
|
||||
|
||||
if (fileHasCall(text, MODULE_GATE_NEEDLE)) continue;
|
||||
if (fileHasCall(text, CYCLE_GATE_NEEDLE)) continue;
|
||||
if (fileHasCall(text, COACH_GATE_NEEDLE)) continue;
|
||||
if (fileHasCall(text, ILLNESS_GATE_NEEDLE)) continue;
|
||||
if (fileHasCapabilityGate(text)) continue;
|
||||
|
||||
@@ -801,7 +796,6 @@ describe("module API route gate inventory", () => {
|
||||
if (
|
||||
fileHasCall(text, MODULE_GATE_NEEDLE) ||
|
||||
fileHasCall(text, CYCLE_GATE_NEEDLE) ||
|
||||
fileHasCall(text, COACH_GATE_NEEDLE) ||
|
||||
fileHasCall(text, ILLNESS_GATE_NEEDLE) ||
|
||||
fileHasCapabilityGate(text)
|
||||
) {
|
||||
|
||||
Reference in new issue
Block a user