@konneal/engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +13 -0
- package/dist/admin.d.ts +26 -0
- package/dist/ai.d.ts +6 -0
- package/dist/anchors.d.ts +6 -0
- package/dist/answercache.d.ts +22 -0
- package/dist/ask.d.ts +5 -0
- package/dist/auth.d.ts +12 -0
- package/dist/bubble.d.ts +14 -0
- package/dist/chunk-LLWPT2XV.js +49 -0
- package/dist/chunk-MB74PTRM.js +114 -0
- package/dist/chunk-WOGQM7DJ.js +197 -0
- package/dist/chunk-WWNCWKKC.js +42 -0
- package/dist/completion.d.ts +5 -0
- package/dist/config.d.ts +154 -0
- package/dist/config.js +37 -0
- package/dist/context.d.ts +115 -0
- package/dist/conversations.d.ts +5 -0
- package/dist/drafts.d.ts +129 -0
- package/dist/env.d.ts +57 -0
- package/dist/faithfulness.d.ts +5 -0
- package/dist/grader.d.ts +3 -0
- package/dist/graph.d.ts +13 -0
- package/dist/hybrid.d.ts +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5373 -0
- package/dist/internal_gateway.d.ts +14 -0
- package/dist/lexical.d.ts +7 -0
- package/dist/livedata.d.ts +77 -0
- package/dist/memories.d.ts +10 -0
- package/dist/modelplane.d.ts +61 -0
- package/dist/oidc.d.ts +73 -0
- package/dist/pipeline.d.ts +57 -0
- package/dist/profile.d.ts +2 -0
- package/dist/profile.gen.d.ts +70 -0
- package/dist/profile.js +8 -0
- package/dist/projects.d.ts +8 -0
- package/dist/prompts/conversational.md +8 -0
- package/dist/prompts/enrichment.md +3 -0
- package/dist/prompts/faithfulness.md +1 -0
- package/dist/prompts/grader.md +5 -0
- package/dist/prompts/listwise.md +3 -0
- package/dist/prompts/precision.md +1 -0
- package/dist/prompts/reflect.md +1 -0
- package/dist/prompts/relevancy.md +1 -0
- package/dist/prompts/research.md +10 -0
- package/dist/prompts/section-summary.md +5 -0
- package/dist/prompts/summarize.md +1 -0
- package/dist/prompts/system.md +18 -0
- package/dist/prompts/understanding.md +17 -0
- package/dist/quota.d.ts +13 -0
- package/dist/reflect.d.ts +5 -0
- package/dist/refs.d.ts +40 -0
- package/dist/refusal.d.ts +9 -0
- package/dist/refusal.js +9 -0
- package/dist/requestScope.d.ts +26 -0
- package/dist/requestScope.js +10 -0
- package/dist/research.d.ts +8 -0
- package/dist/search.d.ts +4 -0
- package/dist/selfquery.d.ts +7 -0
- package/dist/session.d.ts +1 -0
- package/dist/share.d.ts +2 -0
- package/dist/structural.d.ts +27 -0
- package/dist/tablecontext.d.ts +11 -0
- package/dist/understand.d.ts +11 -0
- package/dist/understandContract.d.ts +29 -0
- package/dist/verdict.d.ts +24 -0
- package/docs/API.md +451 -0
- package/docs/ARCHITECTURE.md +302 -0
- package/docs/AUDIT-2026-08-24.md +71 -0
- package/docs/CONTRIBUTOR-AUDIT-2026-08-25.md +147 -0
- package/docs/INGEST-ARCHITECTURE.md +158 -0
- package/docs/MCP.md +92 -0
- package/docs/METANORMA-AI-SERIALIZATION.md +247 -0
- package/docs/MKO-EXPORT-PIPELINE.md +147 -0
- package/docs/REDESIGN-NORMATIVE-RAG-ETSI.md +485 -0
- package/docs/RESEARCH-SOTA-2026.md +243 -0
- package/docs/ROADMAP-SOTA.md +130 -0
- package/docs/SOTA-STAGE-SPECS.md +509 -0
- package/docs/annealment/F1-verdict.md +27 -0
- package/docs/annealment/F10-notes.md +19 -0
- package/docs/annealment/F11-composition.md +17 -0
- package/docs/annealment/F12-passport.md +17 -0
- package/docs/annealment/F2-counterfactual.md +20 -0
- package/docs/annealment/F3-absence.md +21 -0
- package/docs/annealment/F4-instance.md +18 -0
- package/docs/annealment/F5-workflow.md +21 -0
- package/docs/annealment/F6-impact.md +21 -0
- package/docs/annealment/F7-editions.md +18 -0
- package/docs/annealment/F8-selfverify.md +19 -0
- package/docs/annealment/F9-projection-qa.md +17 -0
- package/docs/annealment/L0-locate.md +19 -0
- package/docs/annealment/L1-extract.md +18 -0
- package/docs/annealment/L2-nomenclature.md +22 -0
- package/docs/annealment/L3-geometry.md +23 -0
- package/docs/annealment/L4-composition.md +21 -0
- package/docs/annealment/L5-cross-standard.md +20 -0
- package/docs/annealment/L6-diachrony.md +21 -0
- package/docs/annealment/L7-perception.md +20 -0
- package/docs/annealment/L8-computation.md +22 -0
- package/docs/annealment/L9-instance-process.md +23 -0
- package/docs/annealment/README.md +10 -0
- package/docs/guidelines-metanorma-ai-programme.md +279 -0
- package/docs/identity-onboarding-rag.md +65 -0
- package/docs/identity-service.md +219 -0
- package/docs/knowledge-annealment.md +273 -0
- package/docs/konneal-extraction-plan.md +481 -0
- package/docs/metanorma-for-ai.md +270 -0
- package/docs/mirror-plan.md +36 -0
- package/docs/multi-sdo-architecture.md +191 -0
- package/docs/paper-annealment-comparison.md +259 -0
- package/docs/paper-assets/architecture.svg +94 -0
- package/docs/paper-assets/contract-v2.svg +94 -0
- package/docs/paper-assets/mko-ingest.svg +91 -0
- package/docs/paper-oiml-bulletin.md +402 -0
- package/docs/paper-oiml-bulletin.mdx +419 -0
- package/docs/product-branding-options.md +172 -0
- package/docs/projects-design.md +88 -0
- package/docs/sota-mechanisms.md +184 -0
- package/docs/spec-api.md +77 -0
- package/docs/spec-pipeline.md +126 -0
- package/docs/vector-adapter.md +88 -0
- package/package.json +70 -0
- package/profile/corpora.yaml +5 -0
- package/profile/datasets.yaml +14 -0
- package/profile/prompts.yaml +5 -0
- package/profile/publisher.yaml +17 -0
- package/profile/retrieval.yaml +1 -0
- package/profile/sources.yaml +5 -0
- package/profile/ui.yaml +7 -0
- package/scripts/gen_profile.mjs +33 -0
- package/workers/shared/ai.ts +21 -0
- package/workers/shared/auth.ts +16 -0
- package/workers/shared/chunk.ts +108 -0
- package/workers/shared/oidc.ts +312 -0
- package/workers/shared/router.ts +45 -0
- package/workers/shared/session.ts +104 -0
- package/workers/worker_internal/src/index.ts +157 -0
- package/workers/worker_internal/tsconfig.json +15 -0
- package/workers/worker_internal/wrangler.toml +32 -0
- package/workers/worker_mcp/src/index.ts +175 -0
- package/workers/worker_mcp/tsconfig.json +13 -0
- package/workers/worker_mcp/wrangler.toml +18 -0
- package/workers/worker_public/migrations/0002_conversations.sql +22 -0
- package/workers/worker_public/migrations/0003_shared_conversations.sql +9 -0
- package/workers/worker_public/migrations/0004_graph.sql +16 -0
- package/workers/worker_public/migrations/0005_documents.sql +19 -0
- package/workers/worker_public/migrations/0006_conversation_entities.sql +11 -0
- package/workers/worker_public/migrations/0007_chunks_fts.sql +43 -0
- package/workers/worker_public/migrations/0008_unit_payloads.sql +15 -0
- package/workers/worker_public/migrations/0009_chunks_unit.sql +8 -0
- package/workers/worker_public/migrations/0009_message_context.sql +7 -0
- package/workers/worker_public/migrations/0010_model_nodes.sql +33 -0
- package/workers/worker_public/migrations/0011_schema_union.sql +38 -0
- package/workers/worker_public/migrations/0012_memories.sql +15 -0
- package/workers/worker_public/migrations/0013_projects.sql +21 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.d.ts +16373 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.ts +16328 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.d.ts +16382 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.ts +16337 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.d.ts +16383 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.ts +16338 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.d.ts +16403 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.ts +16358 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.d.ts +16408 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.ts +16363 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.d.ts +16414 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.ts +16369 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.d.ts +16414 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.ts +16369 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/README.md +135 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/entrypoints.svg +53 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.d.ts +17095 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.ts +17050 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.d.ts +16447 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.ts +16402 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/package.json +11 -0
- package/workers/worker_public/package.json +13 -0
- package/workers/worker_public/prompts/conversational.md +8 -0
- package/workers/worker_public/prompts/enrichment.md +3 -0
- package/workers/worker_public/prompts/faithfulness.md +1 -0
- package/workers/worker_public/prompts/grader.md +5 -0
- package/workers/worker_public/prompts/listwise.md +3 -0
- package/workers/worker_public/prompts/precision.md +1 -0
- package/workers/worker_public/prompts/reflect.md +1 -0
- package/workers/worker_public/prompts/relevancy.md +1 -0
- package/workers/worker_public/prompts/research.md +10 -0
- package/workers/worker_public/prompts/section-summary.md +5 -0
- package/workers/worker_public/prompts/summarize.md +1 -0
- package/workers/worker_public/prompts/system.md +18 -0
- package/workers/worker_public/prompts/understanding.md +17 -0
- package/workers/worker_public/public/app.js +166 -0
- package/workers/worker_public/public/index.html +48 -0
- package/workers/worker_public/public/style.css +147 -0
- package/workers/worker_public/schema.sql +248 -0
- package/workers/worker_public/src/admin.ts +358 -0
- package/workers/worker_public/src/ai.ts +71 -0
- package/workers/worker_public/src/anchors.ts +41 -0
- package/workers/worker_public/src/answercache.ts +72 -0
- package/workers/worker_public/src/ask.ts +1094 -0
- package/workers/worker_public/src/auth.ts +252 -0
- package/workers/worker_public/src/bubble.ts +111 -0
- package/workers/worker_public/src/completion.ts +75 -0
- package/workers/worker_public/src/config.ts +238 -0
- package/workers/worker_public/src/context.ts +238 -0
- package/workers/worker_public/src/conversations.ts +162 -0
- package/workers/worker_public/src/drafts.ts +497 -0
- package/workers/worker_public/src/env.ts +90 -0
- package/workers/worker_public/src/faithfulness.ts +63 -0
- package/workers/worker_public/src/grader.ts +89 -0
- package/workers/worker_public/src/graph.ts +63 -0
- package/workers/worker_public/src/hybrid.ts +77 -0
- package/workers/worker_public/src/index.ts +441 -0
- package/workers/worker_public/src/internal_gateway.ts +41 -0
- package/workers/worker_public/src/lexical.ts +86 -0
- package/workers/worker_public/src/lib/hit.ts +4 -0
- package/workers/worker_public/src/lib/http.ts +83 -0
- package/workers/worker_public/src/lib/router.ts +4 -0
- package/workers/worker_public/src/livedata.ts +334 -0
- package/workers/worker_public/src/memories.ts +81 -0
- package/workers/worker_public/src/modelplane.ts +213 -0
- package/workers/worker_public/src/oidc.ts +333 -0
- package/workers/worker_public/src/pipeline.ts +377 -0
- package/workers/worker_public/src/ports/blobs.ts +7 -0
- package/workers/worker_public/src/ports/cloudflare/adapters.ts +177 -0
- package/workers/worker_public/src/ports/kv.ts +8 -0
- package/workers/worker_public/src/ports/model.ts +28 -0
- package/workers/worker_public/src/ports/runtime.ts +13 -0
- package/workers/worker_public/src/ports/store.ts +20 -0
- package/workers/worker_public/src/ports/vector.ts +26 -0
- package/workers/worker_public/src/profile.gen.ts +101 -0
- package/workers/worker_public/src/profile.ts +16 -0
- package/workers/worker_public/src/projects.ts +108 -0
- package/workers/worker_public/src/prompts.d.ts +6 -0
- package/workers/worker_public/src/quota.ts +54 -0
- package/workers/worker_public/src/reflect.ts +67 -0
- package/workers/worker_public/src/refs.ts +107 -0
- package/workers/worker_public/src/refusal.ts +65 -0
- package/workers/worker_public/src/requestScope.ts +71 -0
- package/workers/worker_public/src/research.ts +126 -0
- package/workers/worker_public/src/search.ts +56 -0
- package/workers/worker_public/src/selfquery.ts +25 -0
- package/workers/worker_public/src/session.ts +4 -0
- package/workers/worker_public/src/share.ts +53 -0
- package/workers/worker_public/src/stages/conceptGraph.ts +68 -0
- package/workers/worker_public/src/stages/conceptSteer.ts +39 -0
- package/workers/worker_public/src/stages/corpusScope.ts +25 -0
- package/workers/worker_public/src/stages/dedup.ts +10 -0
- package/workers/worker_public/src/stages/dense.ts +73 -0
- package/workers/worker_public/src/stages/diversity.ts +33 -0
- package/workers/worker_public/src/stages/editionCover.ts +63 -0
- package/workers/worker_public/src/stages/editionSteer.ts +88 -0
- package/workers/worker_public/src/stages/familyBoost.ts +22 -0
- package/workers/worker_public/src/stages/federate.ts +22 -0
- package/workers/worker_public/src/stages/glossary.ts +65 -0
- package/workers/worker_public/src/stages/graphLane.ts +31 -0
- package/workers/worker_public/src/stages/hyde.ts +29 -0
- package/workers/worker_public/src/stages/index.ts +69 -0
- package/workers/worker_public/src/stages/lexicalUnion.ts +21 -0
- package/workers/worker_public/src/stages/multiQuery.ts +57 -0
- package/workers/worker_public/src/stages/overviewDemote.ts +14 -0
- package/workers/worker_public/src/stages/poolOpen.ts +10 -0
- package/workers/worker_public/src/stages/propagate.ts +15 -0
- package/workers/worker_public/src/stages/rerank.ts +47 -0
- package/workers/worker_public/src/stages/seal.ts +16 -0
- package/workers/worker_public/src/stages/sectionDescent.ts +61 -0
- package/workers/worker_public/src/stages/stdRefNudge.ts +35 -0
- package/workers/worker_public/src/stages/subQuery.ts +42 -0
- package/workers/worker_public/src/stages/termNudge.ts +24 -0
- package/workers/worker_public/src/stages/typedPin.ts +131 -0
- package/workers/worker_public/src/stages/types.ts +112 -0
- package/workers/worker_public/src/stages/windowFloor.ts +23 -0
- package/workers/worker_public/src/structural.ts +171 -0
- package/workers/worker_public/src/tablecontext.ts +41 -0
- package/workers/worker_public/src/understand.ts +72 -0
- package/workers/worker_public/src/understandContract.ts +67 -0
- package/workers/worker_public/src/verdict.ts +255 -0
- package/workers/worker_public/tsconfig.json +18 -0
- package/workers/worker_public/wrangler.toml +104 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Internal-tier gateway: fetches FEDERATED passages (OIML + ISO/IEC) from
|
|
2
|
+
// rag-internal via the service binding. rag-public holds NO Vectorize
|
|
3
|
+
// binding to the internal index — this is the only path, member-gated.
|
|
4
|
+
// rag-internal is retrieval-only; the serving pipeline (rerank, grading,
|
|
5
|
+
// generation) stays here — one pipeline, both audiences.
|
|
6
|
+
|
|
7
|
+
import type { Hit } from "./pipeline";
|
|
8
|
+
|
|
9
|
+
interface ServiceFetcher {
|
|
10
|
+
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Ask rag-internal for RRF-fused passages for the query. Returns [] on
|
|
14
|
+
* any failure — the public-only results then serve the member.
|
|
15
|
+
* `auth` forwards the caller's session in WHATEVER form it arrived
|
|
16
|
+
* (cookie same-origin, Bearer via the bubble bridge) — the internal
|
|
17
|
+
* worker's readSession accepts both. */
|
|
18
|
+
export async function retrieveInternal(
|
|
19
|
+
service: ServiceFetcher,
|
|
20
|
+
auth: { cookie: string; authorization: string },
|
|
21
|
+
query: string,
|
|
22
|
+
): Promise<Hit[]> {
|
|
23
|
+
try {
|
|
24
|
+
const res = await service.fetch("https://internal/retrieve", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "content-type": "application/json", cookie: auth.cookie, authorization: auth.authorization },
|
|
27
|
+
body: JSON.stringify({ query }),
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) return [];
|
|
30
|
+
const data: any = await res.json();
|
|
31
|
+
if (!Array.isArray(data?.hits)) return [];
|
|
32
|
+
return data.hits.map((h: any) => ({
|
|
33
|
+
id: h.id,
|
|
34
|
+
score: h.score,
|
|
35
|
+
metadata: h.metadata ?? {},
|
|
36
|
+
text: h.text ?? "",
|
|
37
|
+
}));
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// G-ETSI-1: full-corpus BM25 prefilter via D1 FTS5 (arXiv:2604.09868 §II-B5).
|
|
2
|
+
// Returns Hits ranked by bm25; caller unions with dense and RRF-fuses.
|
|
3
|
+
// Fail-open: any D1/FTS error → empty list (dense path stands alone).
|
|
4
|
+
|
|
5
|
+
import type { Hit, ChunkMeta } from "./pipeline";
|
|
6
|
+
|
|
7
|
+
const LEXICAL_K = 40;
|
|
8
|
+
|
|
9
|
+
/** Build a safe FTS5 MATCH query from user text: alphanumeric tokens,
|
|
10
|
+
* joined with OR so jargon hits don't require full-phrase match. */
|
|
11
|
+
export function ftsMatchQuery(query: string): string | null {
|
|
12
|
+
const terms = query
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^\p{L}\p{N}\s_-]/gu, " ")
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.map((t) => t.trim())
|
|
17
|
+
.filter((t) => t.length >= 2 && t.length <= 40)
|
|
18
|
+
// drop pure stop-ish tokens that blow up OR-queries on standards text
|
|
19
|
+
.filter((t) => !STOP.has(t));
|
|
20
|
+
const uniq = [...new Set(terms)].slice(0, 12);
|
|
21
|
+
if (!uniq.length) return null;
|
|
22
|
+
// quote each term; FTS5 bare tokens are fine for alnum, quotes for safety
|
|
23
|
+
return uniq.map((t) => `"${t.replace(/"/g, "")}"`).join(" OR ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STOP = new Set([
|
|
27
|
+
"the", "a", "an", "of", "and", "or", "to", "in", "for", "on", "is", "are",
|
|
28
|
+
"was", "were", "be", "by", "with", "as", "at", "from", "that", "this",
|
|
29
|
+
"what", "how", "when", "where", "which", "who", "does", "do", "did",
|
|
30
|
+
"can", "could", "should", "would", "may", "might", "shall", "must",
|
|
31
|
+
"about", "into", "than", "then", "its", "it", "their", "there",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export async function lexicalPrefilter(env: { DB: D1Database }, query: string, k = LEXICAL_K): Promise<Hit[]> {
|
|
35
|
+
const match = ftsMatchQuery(query);
|
|
36
|
+
if (!match) return [];
|
|
37
|
+
try {
|
|
38
|
+
// bm25() lower = better; multiply by -1 for descending score convention
|
|
39
|
+
const res = await env.DB.prepare(
|
|
40
|
+
`SELECT c.id, c.doc_id, c.docidentifier, c.doctype, c.doc_number, c.edition,
|
|
41
|
+
c.language, c.clause_anchor, c.clause_title, c.status, c.superseded_by,
|
|
42
|
+
c.corpus, c.tier, c.text, c.unit_id, c.block, bm25(chunks_fts) AS rank
|
|
43
|
+
FROM chunks_fts
|
|
44
|
+
JOIN chunks c ON c.rowid = chunks_fts.rowid
|
|
45
|
+
WHERE chunks_fts MATCH ?1
|
|
46
|
+
ORDER BY rank
|
|
47
|
+
LIMIT ?2`,
|
|
48
|
+
)
|
|
49
|
+
.bind(match, k)
|
|
50
|
+
.all();
|
|
51
|
+
const rows = res.results ?? [];
|
|
52
|
+
return rows.map((r: any, i: number) => {
|
|
53
|
+
const meta: ChunkMeta = {
|
|
54
|
+
doc_id: String(r.doc_id ?? ""),
|
|
55
|
+
docidentifier: String(r.docidentifier ?? ""),
|
|
56
|
+
doctype: String(r.doctype ?? ""),
|
|
57
|
+
doc_number: String(r.doc_number ?? ""),
|
|
58
|
+
edition: String(r.edition ?? ""),
|
|
59
|
+
language: String(r.language ?? "en"),
|
|
60
|
+
clause_anchor: String(r.clause_anchor ?? ""),
|
|
61
|
+
clause_title: String(r.clause_title ?? ""),
|
|
62
|
+
tier: String(r.tier ?? ""),
|
|
63
|
+
corpus: String(r.corpus ?? ""),
|
|
64
|
+
text_ref: "",
|
|
65
|
+
status: String(r.status ?? "unknown"),
|
|
66
|
+
superseded_by: String(r.superseded_by ?? ""),
|
|
67
|
+
// contract v2 over the lexical lane: typed chunks arriving via BM25
|
|
68
|
+
// keep their unit identity ([[u:…]] refs, typed pin, retyping check)
|
|
69
|
+
unit_id: String(r.unit_id ?? "") || undefined,
|
|
70
|
+
block: String(r.block ?? "") || undefined,
|
|
71
|
+
};
|
|
72
|
+
// rank is bm25 (lower better) → convert to positive score that RRF can ignore
|
|
73
|
+
// (RRF uses rank position, not score). score kept for logging only.
|
|
74
|
+
const bm25 = typeof r.rank === "number" ? r.rank : i;
|
|
75
|
+
return {
|
|
76
|
+
id: String(r.id),
|
|
77
|
+
score: 1 / (1 + Math.max(0, bm25)),
|
|
78
|
+
metadata: meta,
|
|
79
|
+
text: String(r.text ?? ""),
|
|
80
|
+
} satisfies Hit;
|
|
81
|
+
});
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.log("lexical prefilter failed:", String(e).slice(0, 200));
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// HTTP wire helpers + request validation + API-key authentication —
|
|
2
|
+
// the vocabulary every route handler speaks (TODO.impl/23).
|
|
3
|
+
import { LIMITS, sha256Hex } from "../config";
|
|
4
|
+
import type { Env } from "../env";
|
|
5
|
+
|
|
6
|
+
export const json = (body: unknown, status = 200, extra: Record<string, string> = {}) =>
|
|
7
|
+
new Response(JSON.stringify(body), {
|
|
8
|
+
status,
|
|
9
|
+
headers: { "content-type": "application/json", ...extra },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const err = (status: number, code: string, message: string) =>
|
|
13
|
+
json({ error: { code, message } }, status);
|
|
14
|
+
|
|
15
|
+
export function corsHeaders(req: Request): Record<string, string> {
|
|
16
|
+
const origin = req.headers.get("origin") ?? "";
|
|
17
|
+
const allowed =
|
|
18
|
+
origin === "https://oimlsmart.org" ||
|
|
19
|
+
/^https:\/\/[a-z0-9-]+\.oimlsmart\.org$/.test(origin) ||
|
|
20
|
+
// the local dev posture: the platform and the minisites develop on
|
|
21
|
+
// localhost ports against the live service (the bubble bridge admits
|
|
22
|
+
// the same class; anon quota is per-IP, member auth needs the token)
|
|
23
|
+
/^http:\/\/localhost(:\d{1,5})?$/.test(origin) ||
|
|
24
|
+
/^http:\/\/127\.0\.0\.1(:\d{1,5})?$/.test(origin);
|
|
25
|
+
return allowed
|
|
26
|
+
? {
|
|
27
|
+
"access-control-allow-origin": origin,
|
|
28
|
+
// PATCH + DELETE: the conversations API speaks them (rename,
|
|
29
|
+
// delete) — the embedded panel preflights cross-origin.
|
|
30
|
+
"access-control-allow-methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
31
|
+
"access-control-allow-headers": "authorization, content-type",
|
|
32
|
+
"access-control-max-age": "86400",
|
|
33
|
+
}
|
|
34
|
+
: {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** CORS-complete a handler's response: the browser surface grew
|
|
38
|
+
* piecemeal (the SSE ask paths carried the headers; the JSON + error
|
|
39
|
+
* paths and the conversations API did not), which an embedded
|
|
40
|
+
* cross-origin client reads as opaque network failures. One wrap at
|
|
41
|
+
* the router keeps every browser-facing answer readable. */
|
|
42
|
+
export function withCors(res: Response, cors: Record<string, string>): Response {
|
|
43
|
+
if (!cors["access-control-allow-origin"]) return res;
|
|
44
|
+
const headers = new Headers(res.headers);
|
|
45
|
+
for (const [k, v] of Object.entries(cors)) headers.set(k, v);
|
|
46
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ApiKey {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
day_limit: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function authenticate(env: Env, req: Request): Promise<ApiKey | null> {
|
|
56
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
57
|
+
const m = auth.match(/^Bearer\s+(.+)$/i);
|
|
58
|
+
if (!m) return null;
|
|
59
|
+
const keyHash = await sha256Hex(m[1].trim());
|
|
60
|
+
const row = await env.DB.prepare(
|
|
61
|
+
"SELECT id, name, day_limit FROM api_keys WHERE key_hash = ?1 AND revoked = 0",
|
|
62
|
+
)
|
|
63
|
+
.bind(keyHash)
|
|
64
|
+
.first<ApiKey>();
|
|
65
|
+
return row ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function readJson(req: Request): Promise<any | null> {
|
|
69
|
+
try {
|
|
70
|
+
const body = await req.json();
|
|
71
|
+
if (!body || typeof body !== "object") return null;
|
|
72
|
+
return body;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function validateQuery(body: any): { query: string; lang?: string } | null {
|
|
79
|
+
const query = typeof body?.query === "string" ? body.query.trim() : "";
|
|
80
|
+
if (!query || query.length > LIMITS.maxInputChars) return null;
|
|
81
|
+
const lang = typeof body?.lang === "string" && /^[a-z]{2}$/.test(body.lang) ? body.lang : undefined;
|
|
82
|
+
return { query, lang };
|
|
83
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The router lives with the shared cross-worker modules
|
|
2
|
+
// (workers/shared/router.ts) — re-exported for worker_public's imports.
|
|
3
|
+
export { matchRoute } from "../../../shared/router.ts";
|
|
4
|
+
export type { RouteContext, Route, RouteHandler } from "../../../shared/router.ts";
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// The live-data tool (TODO.ai-platform/03) — the "my account" context's
|
|
2
|
+
// service half. The user opted the message into their OWN live platform
|
|
3
|
+
// data; the service reads the platform EXACTLY as the user, never wider:
|
|
4
|
+
//
|
|
5
|
+
// 1. The sign-in (auth.ts) retains the OP's access token for the
|
|
6
|
+
// SESSION's window only (KV, keyed by the session token's hash,
|
|
7
|
+
// TTL = the OP token's own expiry — never D1, never past it).
|
|
8
|
+
// 2. The exchange (the identity service's RFC 8693 session delegation,
|
|
9
|
+
// RP guide §9b) trades that subject token for a narrowly-scoped OP
|
|
10
|
+
// JWT: `<platform>:read`, the account's standing RE-JUDGED at the
|
|
11
|
+
// exchange (a role lost mid-session narrows the next answer
|
|
12
|
+
// honestly), the actor claim naming THIS service.
|
|
13
|
+
// 3. The platform reads ride that JWT; the platform enforces the cones
|
|
14
|
+
// exactly as for the user's own browser (smart's bearer cone). THE
|
|
15
|
+
// MUST-NOT IS STRUCTURAL HERE: this module maps the platform's
|
|
16
|
+
// answers 1:1 into records — it cannot invent a row the platform
|
|
17
|
+
// did not return, and a store the cone refuses simply contributes
|
|
18
|
+
// nothing.
|
|
19
|
+
//
|
|
20
|
+
// The exchanged JWT is cached for its own short life (KV, TTL-bounded)
|
|
21
|
+
// so an ask burst costs one exchange; nothing outlives the session's
|
|
22
|
+
// exchange window.
|
|
23
|
+
|
|
24
|
+
/** SHA-256 hex — config.ts's helper, inlined so this module stays
|
|
25
|
+
* dependency-free (context.ts's pattern: the unit tests load it on
|
|
26
|
+
* plain node's type stripping, which never resolves extensionless
|
|
27
|
+
* relative imports). */
|
|
28
|
+
async function sha256Hex(s: string): Promise<string> {
|
|
29
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
|
|
30
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── configuration ────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface LiveDataConfig {
|
|
36
|
+
/** The platform instance's API base (the hub's origin). */
|
|
37
|
+
platformApi: string;
|
|
38
|
+
/** The platform's client id at the OP (the delegation's scope target
|
|
39
|
+
* + the exchanged token's audience). */
|
|
40
|
+
platformClientId: string;
|
|
41
|
+
/** The OP's issuer (the exchange endpoint lives there). */
|
|
42
|
+
issuer: string;
|
|
43
|
+
/** THIS service's client credentials (the delegation's caller auth —
|
|
44
|
+
* the subject token binds to the client it was issued to). */
|
|
45
|
+
clientId: string;
|
|
46
|
+
clientSecret?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function liveDataConfig(env: any): LiveDataConfig | null {
|
|
50
|
+
const platformApi = (env.SMART_PLATFORM_API ?? "").trim().replace(/\/$/, "");
|
|
51
|
+
const platformClientId = (env.SMART_PLATFORM_CLIENT_ID ?? "").trim();
|
|
52
|
+
const issuer = (env.OIDC_ISSUER ?? "https://id.oimlsmart.org").trim().replace(/\/$/, "");
|
|
53
|
+
const clientId = (env.OIDC_CLIENT_ID ?? "").trim();
|
|
54
|
+
if (!platformApi || !platformClientId || !clientId) return null;
|
|
55
|
+
return { platformApi, platformClientId, issuer, clientId, clientSecret: env.OIDC_CLIENT_SECRET };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── the session's exchange window (the retained OP access token) ─────
|
|
59
|
+
|
|
60
|
+
const SUBJECT_KEY = (sessionHash: string) => `opat:${sessionHash}`;
|
|
61
|
+
const EXCHANGED_KEY = (sessionHash: string) => `ossx:${sessionHash}`;
|
|
62
|
+
|
|
63
|
+
/** Retain the sign-in's OP access token for the session's exchange
|
|
64
|
+
* window (the OP token's own TTL, minus a small margin). Called ONCE
|
|
65
|
+
* per sign-in from the auth callback; the token never persists past
|
|
66
|
+
* the window. */
|
|
67
|
+
export async function retainOpAccessToken(
|
|
68
|
+
env: any,
|
|
69
|
+
sessionRaw: string,
|
|
70
|
+
opAccessToken: string,
|
|
71
|
+
expiresInSec: number,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const ttl = Math.max(30, Math.floor(expiresInSec) - 30);
|
|
74
|
+
try {
|
|
75
|
+
await env.CACHE.put(SUBJECT_KEY(await sha256Hex(sessionRaw)), JSON.stringify({ token: opAccessToken }), { expirationTtl: ttl });
|
|
76
|
+
} catch {
|
|
77
|
+
// a KV hiccup degrades the live window honestly (the chip answers
|
|
78
|
+
// "live data unavailable"), never blocks the sign-in
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Drop the retained subject + the cached exchanged token (the sign-out
|
|
83
|
+
* closes the window deliberately, never by expiry alone). */
|
|
84
|
+
export async function dropOpAccessToken(env: any, sessionRaw: string): Promise<void> {
|
|
85
|
+
const h = await sha256Hex(sessionRaw);
|
|
86
|
+
try {
|
|
87
|
+
await env.CACHE.delete(SUBJECT_KEY(h));
|
|
88
|
+
await env.CACHE.delete(EXCHANGED_KEY(h));
|
|
89
|
+
} catch {
|
|
90
|
+
/* the TTL closes the window regardless */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── the RFC 8693 delegation exchange ─────────────────────────────────
|
|
95
|
+
|
|
96
|
+
export type LiveTokenVerdict =
|
|
97
|
+
| { ok: true; token: string }
|
|
98
|
+
| { ok: false; reason: "not_configured" | "window_expired" | "refused" | "op_unreachable" };
|
|
99
|
+
|
|
100
|
+
/** Exchange the session's retained OP access token for the
|
|
101
|
+
* platform-scoped JWT (identity's §9b). The exchanged token caches for
|
|
102
|
+
* its own short life; the refusal lattice is honest per leg. */
|
|
103
|
+
export async function exchangeForLiveToken(env: any, sessionRaw: string): Promise<LiveTokenVerdict> {
|
|
104
|
+
const cfg = liveDataConfig(env);
|
|
105
|
+
if (!cfg) return { ok: false, reason: "not_configured" };
|
|
106
|
+
const h = await sha256Hex(sessionRaw);
|
|
107
|
+
|
|
108
|
+
const cached = await env.CACHE.get(EXCHANGED_KEY(h));
|
|
109
|
+
if (cached) return { ok: true, token: cached };
|
|
110
|
+
|
|
111
|
+
const subjectRow = await env.CACHE.get(SUBJECT_KEY(h), "json") as { token?: string } | null;
|
|
112
|
+
if (!subjectRow?.token) return { ok: false, reason: "window_expired" };
|
|
113
|
+
|
|
114
|
+
const body = new URLSearchParams({
|
|
115
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
116
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
|
|
117
|
+
subject_token: subjectRow.token,
|
|
118
|
+
scope: `${cfg.platformClientId}:read`,
|
|
119
|
+
});
|
|
120
|
+
const headers: Record<string, string> = { "content-type": "application/x-www-form-urlencoded" };
|
|
121
|
+
if (cfg.clientSecret) {
|
|
122
|
+
headers.authorization = `Basic ${btoa(`${encodeURIComponent(cfg.clientId)}:${encodeURIComponent(cfg.clientSecret)}`)}`;
|
|
123
|
+
} else {
|
|
124
|
+
body.set("client_id", cfg.clientId);
|
|
125
|
+
}
|
|
126
|
+
let res: Response;
|
|
127
|
+
try {
|
|
128
|
+
res = await fetch(`${cfg.issuer}/op/token`, { method: "POST", headers, body });
|
|
129
|
+
} catch {
|
|
130
|
+
return { ok: false, reason: "op_unreachable" };
|
|
131
|
+
}
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
// The refusal's code is logged, NEVER the subject token: a refused
|
|
134
|
+
// exchange narrows the answer honestly (the window lapsed, the
|
|
135
|
+
// standing fell away, or the deployment refuses).
|
|
136
|
+
const code = await res.json().then((j: any) => j?.error ?? "unknown").catch(() => "unknown");
|
|
137
|
+
console.log("live-data exchange refused:", res.status, code);
|
|
138
|
+
return { ok: false, reason: code === "invalid_grant" ? "window_expired" : "refused" };
|
|
139
|
+
}
|
|
140
|
+
const granted = (await res.json()) as { access_token?: string; expires_in?: number; scope?: string };
|
|
141
|
+
if (!granted.access_token) return { ok: false, reason: "refused" };
|
|
142
|
+
// The exchange's own answer states the granted scope — read it, never
|
|
143
|
+
// assume it (the standing re-judgment may have narrowed honestly).
|
|
144
|
+
const ttl = Math.max(30, Math.floor(granted.expires_in ?? 300) - 60);
|
|
145
|
+
try {
|
|
146
|
+
await env.CACHE.put(EXCHANGED_KEY(h), granted.access_token, { expirationTtl: ttl });
|
|
147
|
+
} catch {
|
|
148
|
+
/* the exchange simply re-runs next ask */
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, token: granted.access_token };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── the platform reads (the records the answer grounds in) ───────────
|
|
154
|
+
|
|
155
|
+
export interface LiveRecord {
|
|
156
|
+
/** the platform store the record came from */
|
|
157
|
+
store: string;
|
|
158
|
+
id: string;
|
|
159
|
+
/** the display label ("Application APP-2026-001 — R 60") */
|
|
160
|
+
label: string;
|
|
161
|
+
/** the platform page the record links to */
|
|
162
|
+
url: string;
|
|
163
|
+
status?: string;
|
|
164
|
+
date?: string;
|
|
165
|
+
/** the coarse one-line state ("evaluation in progress", "3 test
|
|
166
|
+
* requests dispatched") — the progress projection's summary */
|
|
167
|
+
detail?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type LiveRead =
|
|
171
|
+
| { ok: true; records: LiveRecord[]; stores: string[]; readAt: string }
|
|
172
|
+
| { ok: false; reason: "platform_refused" | "platform_unreachable" };
|
|
173
|
+
|
|
174
|
+
/** The platform page for a record — the role family decides the console
|
|
175
|
+
* (the same page the user's own browser would open). */
|
|
176
|
+
function recordUrl(cfg: LiveDataConfig, roleFamily: string, store: string, row: any): string {
|
|
177
|
+
const std = typeof row.standard_id === "string" ? row.standard_id.replace(/^oiml-/, "") : null;
|
|
178
|
+
if (store === "certificates") {
|
|
179
|
+
if (roleFamily === "applicant") return `${cfg.platformApi}/app/portal/certificates/${row.id}`;
|
|
180
|
+
if (std) return `${cfg.platformApi}/app/standards/${std}/certificates/${row.id}`;
|
|
181
|
+
return `${cfg.platformApi}/app/portal/certificates/${row.id}`;
|
|
182
|
+
}
|
|
183
|
+
// applications + testRequests resolve to their application page (the
|
|
184
|
+
// request is visible within its project on every console).
|
|
185
|
+
const appId = store === "applications" ? row.id : (row.application_id ?? row.id);
|
|
186
|
+
if (roleFamily === "ia") return `${cfg.platformApi}/app/ia/applications/${appId}`;
|
|
187
|
+
if (roleFamily === "lab") return `${cfg.platformApi}/app/lab/projects/${appId}`;
|
|
188
|
+
return `${cfg.platformApi}/app/portal/applications/${appId}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The exchanged JWT's primary platform role (the service's OWN fetched
|
|
192
|
+
* token — decoded, never re-validated) decides the console family. */
|
|
193
|
+
function roleFamilyOf(token: string, platformClientId: string): string {
|
|
194
|
+
try {
|
|
195
|
+
const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
196
|
+
const roles: string[] = payload?.service_roles?.[platformClientId] ?? [];
|
|
197
|
+
const primary = roles[0] ?? "";
|
|
198
|
+
if (["ia_officer", "case_officer", "certification_officer", "signatory"].includes(primary)) return "ia";
|
|
199
|
+
if (primary === "tl_operator") return "lab";
|
|
200
|
+
return "applicant";
|
|
201
|
+
} catch {
|
|
202
|
+
return "applicant";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const MAX_RECORDS = 12;
|
|
207
|
+
const PROGRESS_FOR = 3;
|
|
208
|
+
|
|
209
|
+
/** Read the account's live surface: the bounded store lists (the cone
|
|
210
|
+
* filters server-side — a refused store contributes NOTHING, never an
|
|
211
|
+
* error into the answer) + the progress projection for the freshest
|
|
212
|
+
* applications. Every record maps 1:1 from a platform row. */
|
|
213
|
+
export async function readMyAccount(_env: any, cfg: LiveDataConfig, token: string): Promise<LiveRead> {
|
|
214
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
215
|
+
const readAt = new Date().toISOString();
|
|
216
|
+
const family = roleFamilyOf(token, cfg.platformClientId);
|
|
217
|
+
const records: LiveRecord[] = [];
|
|
218
|
+
const storesRead: string[] = [];
|
|
219
|
+
|
|
220
|
+
async function readStore(store: string): Promise<any[]> {
|
|
221
|
+
let res: Response;
|
|
222
|
+
try {
|
|
223
|
+
res = await fetch(`${cfg.platformApi}/api/entities/${store}`, { headers: auth });
|
|
224
|
+
} catch {
|
|
225
|
+
throw new Error("unreachable");
|
|
226
|
+
}
|
|
227
|
+
if (!res.ok) {
|
|
228
|
+
// The cone's honest refusal (403) or an unauthenticated read (401):
|
|
229
|
+
// the store contributes nothing — the records stay exactly what the
|
|
230
|
+
// account may see.
|
|
231
|
+
console.log(`live-data: ${store} answered ${res.status} — skipped`);
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
storesRead.push(store);
|
|
235
|
+
const rows = (await res.json()) as any[];
|
|
236
|
+
return Array.isArray(rows) ? rows : [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let applications: any[] = [];
|
|
240
|
+
try {
|
|
241
|
+
applications = await readStore("applications");
|
|
242
|
+
const certificates = await readStore("certificates");
|
|
243
|
+
const requests = await readStore("testRequests");
|
|
244
|
+
for (const row of applications) {
|
|
245
|
+
records.push({
|
|
246
|
+
store: "applications",
|
|
247
|
+
id: String(row.id),
|
|
248
|
+
label: `Application ${row.application_number ?? row.id}${row.standard_id ? ` — ${String(row.standard_id).replace(/^oiml-/, "").toUpperCase().replace(/^R(\d)/, "R $1")}` : ""}`,
|
|
249
|
+
url: recordUrl(cfg, family, "applications", row),
|
|
250
|
+
status: row.status,
|
|
251
|
+
date: row.submitted_date ?? row.date_of_application,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
for (const row of certificates) {
|
|
255
|
+
records.push({
|
|
256
|
+
store: "certificates",
|
|
257
|
+
id: String(row.id),
|
|
258
|
+
label: `Certificate ${row.certificate_number ?? row.id}`,
|
|
259
|
+
url: recordUrl(cfg, family, "certificates", row),
|
|
260
|
+
status: row.status,
|
|
261
|
+
date: row.issue_date ?? row.registered_copy_of?.registered_date,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
for (const row of requests) {
|
|
265
|
+
records.push({
|
|
266
|
+
store: "testRequests",
|
|
267
|
+
id: String(row.id),
|
|
268
|
+
label: `Test request ${row.request_number ?? row.id}`,
|
|
269
|
+
url: recordUrl(cfg, family, "testRequests", row),
|
|
270
|
+
status: row.status,
|
|
271
|
+
date: row.issued_date,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
return { ok: false, reason: "platform_unreachable" };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// The progress projection for the freshest applications (the "where is
|
|
279
|
+
// my application" answer's own surface — the same coarse projection the
|
|
280
|
+
// portal renders, never the work-in-flight stores).
|
|
281
|
+
const freshest = applications
|
|
282
|
+
.slice()
|
|
283
|
+
.sort((a, b) => String(b.submitted_date ?? b.date_of_application ?? "").localeCompare(String(a.submitted_date ?? a.date_of_application ?? "")))
|
|
284
|
+
.slice(0, PROGRESS_FOR);
|
|
285
|
+
for (const row of freshest) {
|
|
286
|
+
try {
|
|
287
|
+
const res = await fetch(`${cfg.platformApi}/api/entities/applications/${encodeURIComponent(row.id)}/progress`, { headers: auth });
|
|
288
|
+
if (!res.ok) continue;
|
|
289
|
+
const p = (await res.json()) as any;
|
|
290
|
+
const rec = records.find((r) => r.store === "applications" && r.id === String(row.id));
|
|
291
|
+
if (rec) {
|
|
292
|
+
const parts: string[] = [];
|
|
293
|
+
if (p.evaluation?.state === "concluded") parts.push(`evaluation concluded${p.evaluation.decision ? ` (${p.evaluation.decision})` : ""}`);
|
|
294
|
+
else if (p.evaluation?.state === "in_progress") parts.push("evaluation in progress");
|
|
295
|
+
else parts.push("evaluation not started");
|
|
296
|
+
if (Array.isArray(p.requests) && p.requests.length) parts.push(`${p.requests.length} test request${p.requests.length === 1 ? "" : "s"} dispatched`);
|
|
297
|
+
if (p.certificate) parts.push(`certificate ${p.certificate.certificate_number ?? ""} ${p.certificate.status ?? ""}`.trim());
|
|
298
|
+
rec.detail = parts.join("; ");
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
/* the projection is additive — the record stands without it */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The freshest surface first, bounded (the newest dates lead; a record
|
|
306
|
+
// without a date sorts last).
|
|
307
|
+
records.sort((a, b) => String(b.date ?? "").localeCompare(String(a.date ?? "")));
|
|
308
|
+
return { ok: true, records: records.slice(0, MAX_RECORDS), stores: storesRead, readAt };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── the ask path's composed resolution ───────────────────────────────
|
|
312
|
+
|
|
313
|
+
export type LiveAccount =
|
|
314
|
+
| { status: "ok"; records: LiveRecord[]; stores: string[]; readAt: string }
|
|
315
|
+
| { status: "unavailable"; reason: "sign_in_required" | "not_configured" | "window_expired" | "refused" | "op_unreachable" | "platform_refused" | "platform_unreachable" };
|
|
316
|
+
|
|
317
|
+
/** Resolve the "my account" context for one ask: the member's session →
|
|
318
|
+
* the exchange → the platform reads. Every failure is an honest reason
|
|
319
|
+
* the context line can state — never a silent widening, never an
|
|
320
|
+
* invented record. */
|
|
321
|
+
export async function resolveLiveAccount(
|
|
322
|
+
env: any,
|
|
323
|
+
sessionRaw: string | null,
|
|
324
|
+
member: { sub: string } | null,
|
|
325
|
+
): Promise<LiveAccount> {
|
|
326
|
+
if (!member || !sessionRaw) return { status: "unavailable", reason: "sign_in_required" };
|
|
327
|
+
const cfg = liveDataConfig(env);
|
|
328
|
+
if (!cfg) return { status: "unavailable", reason: "not_configured" };
|
|
329
|
+
const exchanged = await exchangeForLiveToken(env, sessionRaw);
|
|
330
|
+
if (!exchanged.ok) return { status: "unavailable", reason: exchanged.reason };
|
|
331
|
+
const read = await readMyAccount(env, cfg, exchanged.token);
|
|
332
|
+
if (!read.ok) return { status: "unavailable", reason: read.reason };
|
|
333
|
+
return { status: "ok", records: read.records, stores: read.stores, readAt: read.readAt };
|
|
334
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Personalized memory files (#171): per-member context documents the
|
|
2
|
+
// ask path injects when selected. Member-scoped CRUD — every query is
|
|
3
|
+
// owner-filtered by the session's sub; content is user-authored text,
|
|
4
|
+
// never corpus. Caps: 10 files, 8k chars each (the ask path further
|
|
5
|
+
// bounds the injected total).
|
|
6
|
+
|
|
7
|
+
import { json, err } from "./lib/http";
|
|
8
|
+
|
|
9
|
+
const MAX_FILES = 10;
|
|
10
|
+
const MAX_CONTENT = 8_000;
|
|
11
|
+
const MAX_NAME = 64;
|
|
12
|
+
const ID_RE = /^m:[a-f0-9]{16}$/;
|
|
13
|
+
|
|
14
|
+
const newId = () => "m:" + [...crypto.getRandomValues(new Uint8Array(8))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
|
|
16
|
+
export async function handleMemories(env: any, sub: string, req: Request, route: { method: string; id?: string }): Promise<Response> {
|
|
17
|
+
const { method, id } = route;
|
|
18
|
+
if (method === "GET") {
|
|
19
|
+
const rows = (await env.DB.prepare("SELECT id, name, content, enabled, updated_at FROM memories WHERE sub = ?1 ORDER BY updated_at DESC").bind(sub).all()).results ?? [];
|
|
20
|
+
return json({ memories: rows });
|
|
21
|
+
}
|
|
22
|
+
if (method === "DELETE") {
|
|
23
|
+
if (!id || !ID_RE.test(id)) return err(400, "invalid_input", "bad memory id");
|
|
24
|
+
await env.DB.prepare("DELETE FROM memories WHERE id = ?1 AND sub = ?2").bind(id, sub).run();
|
|
25
|
+
return json({ ok: true });
|
|
26
|
+
}
|
|
27
|
+
if (method === "POST") {
|
|
28
|
+
const body: any = await req.json().catch(() => null);
|
|
29
|
+
const name = typeof body?.name === "string" ? body.name.trim().slice(0, MAX_NAME) : "";
|
|
30
|
+
const content = typeof body?.content === "string" ? body.content.slice(0, MAX_CONTENT) : "";
|
|
31
|
+
if (!name || !content.trim()) return err(400, "invalid_input", "name and content are required");
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (typeof body?.id === "string" && ID_RE.test(body.id)) {
|
|
34
|
+
const r = await env.DB.prepare(
|
|
35
|
+
"UPDATE memories SET name = ?1, content = ?2, updated_at = ?3 WHERE id = ?4 AND sub = ?5",
|
|
36
|
+
).bind(name, content, now, body.id, sub).run();
|
|
37
|
+
if (!r.meta?.changes) return err(404, "not_found", "no such memory");
|
|
38
|
+
return json({ ok: true, id: body.id });
|
|
39
|
+
}
|
|
40
|
+
const count = Number((await env.DB.prepare("SELECT COUNT(*) AS n FROM memories WHERE sub = ?1").bind(sub).first() as any)?.n ?? 0);
|
|
41
|
+
if (count >= MAX_FILES) return err(400, "quota_exceeded", `at most ${MAX_FILES} memory files`);
|
|
42
|
+
const mid = newId();
|
|
43
|
+
await env.DB.prepare(
|
|
44
|
+
"INSERT INTO memories (id, sub, name, content, enabled, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, 1, ?5, ?5)",
|
|
45
|
+
).bind(mid, sub, name, content, now).run();
|
|
46
|
+
return json({ ok: true, id: mid });
|
|
47
|
+
}
|
|
48
|
+
return err(405, "method_not_allowed", "GET, POST or DELETE");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The ask path's memory injection: fetch the SELECTED, OWNED files and
|
|
52
|
+
* render them as one bounded note. Ids may be personal (m:…) or the
|
|
53
|
+
* user's project files (pf:…) — ownership resolves both ways, and a
|
|
54
|
+
* foreign id silently drops (a stale selection is never an error, and
|
|
55
|
+
* never another user's memory). Returns [note, idsActuallyUsed]. */
|
|
56
|
+
export async function memoryNote(env: any, sub: string, ids: string[]): Promise<[string | null, string[]]> {
|
|
57
|
+
const wanted = [...new Set(ids.filter((x) => typeof x === "string" && (ID_RE.test(x) || /^pf:[a-f0-9]{16}$/.test(x))))].slice(0, 4);
|
|
58
|
+
if (!wanted.length) return [null, []];
|
|
59
|
+
const ph = wanted.map((_, i) => `?${i + 2}`).join(",");
|
|
60
|
+
const rows = [
|
|
61
|
+
...((await env.DB.prepare(`SELECT id, name, content FROM memories WHERE sub = ?1 AND id IN (${ph})`).bind(sub, ...wanted).all()).results ?? []),
|
|
62
|
+
...((await env.DB.prepare(
|
|
63
|
+
`SELECT f.id, f.name, f.content FROM project_files f JOIN projects p ON p.id = f.project_id WHERE p.sub = ?1 AND f.id IN (${ph})`,
|
|
64
|
+
).bind(sub, ...wanted).all()).results ?? []),
|
|
65
|
+
];
|
|
66
|
+
if (!rows.length) return [null, []];
|
|
67
|
+
let budget = 4_000;
|
|
68
|
+
const parts: string[] = [];
|
|
69
|
+
const used: string[] = [];
|
|
70
|
+
for (const r of rows as any[]) {
|
|
71
|
+
if (budget <= 200) break;
|
|
72
|
+
const body = String(r.content).slice(0, budget);
|
|
73
|
+
budget -= body.length;
|
|
74
|
+
parts.push(`### ${r.name}\n${body}`);
|
|
75
|
+
used.push(String(r.id));
|
|
76
|
+
}
|
|
77
|
+
return [
|
|
78
|
+
"The user's own memory files — their stated context. Treat as trusted user facts (their lab, instruments, preferences); blend with the passages, never contradict them silently:\n" + parts.join("\n\n"),
|
|
79
|
+
used,
|
|
80
|
+
];
|
|
81
|
+
}
|