@contractspec/example.policy-safe-knowledge-assistant 1.46.1 → 1.48.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.
Files changed (50) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +50 -30
  2. package/.turbo/turbo-build.log +49 -29
  3. package/CHANGELOG.md +66 -0
  4. package/README.md +7 -8
  5. package/dist/example.d.ts +2 -2
  6. package/dist/example.d.ts.map +1 -1
  7. package/dist/example.js +5 -3
  8. package/dist/example.js.map +1 -1
  9. package/dist/handlers/index.d.ts +2 -0
  10. package/dist/handlers/index.js +3 -0
  11. package/dist/handlers/policy-safe-knowledge-assistant.handlers.d.ts +127 -0
  12. package/dist/handlers/policy-safe-knowledge-assistant.handlers.d.ts.map +1 -0
  13. package/dist/handlers/policy-safe-knowledge-assistant.handlers.js +264 -0
  14. package/dist/handlers/policy-safe-knowledge-assistant.handlers.js.map +1 -0
  15. package/dist/index.d.ts +5 -2
  16. package/dist/index.js +5 -2
  17. package/dist/orchestrator/buildAnswer.js.map +1 -1
  18. package/dist/policy-safe-knowledge-assistant.feature.d.ts +7 -0
  19. package/dist/policy-safe-knowledge-assistant.feature.d.ts.map +1 -0
  20. package/dist/{feature.js → policy-safe-knowledge-assistant.feature.js} +6 -4
  21. package/dist/policy-safe-knowledge-assistant.feature.js.map +1 -0
  22. package/dist/seeders/index.d.ts +10 -0
  23. package/dist/seeders/index.d.ts.map +1 -0
  24. package/dist/seeders/index.js +16 -0
  25. package/dist/seeders/index.js.map +1 -0
  26. package/dist/ui/PolicySafeKnowledgeAssistantDashboard.d.ts +7 -0
  27. package/dist/ui/PolicySafeKnowledgeAssistantDashboard.d.ts.map +1 -0
  28. package/dist/ui/PolicySafeKnowledgeAssistantDashboard.js +231 -0
  29. package/dist/ui/PolicySafeKnowledgeAssistantDashboard.js.map +1 -0
  30. package/dist/ui/hooks/usePolicySafeKnowledgeAssistant.d.ts +55 -0
  31. package/dist/ui/hooks/usePolicySafeKnowledgeAssistant.d.ts.map +1 -0
  32. package/dist/ui/hooks/usePolicySafeKnowledgeAssistant.js +193 -0
  33. package/dist/ui/hooks/usePolicySafeKnowledgeAssistant.js.map +1 -0
  34. package/dist/ui/index.d.ts +2 -0
  35. package/dist/ui/index.js +3 -0
  36. package/package.json +27 -15
  37. package/src/example.ts +4 -4
  38. package/src/handlers/index.ts +1 -0
  39. package/src/handlers/policy-safe-knowledge-assistant.handlers.ts +476 -0
  40. package/src/index.ts +3 -1
  41. package/src/{feature.ts → policy-safe-knowledge-assistant.feature.ts} +3 -3
  42. package/src/seeders/index.ts +20 -0
  43. package/src/ui/PolicySafeKnowledgeAssistantDashboard.tsx +206 -0
  44. package/src/ui/hooks/usePolicySafeKnowledgeAssistant.ts +229 -0
  45. package/src/ui/index.ts +1 -0
  46. package/tsconfig.tsbuildinfo +1 -1
  47. package/IMPLEMENTATION_SKETCH.md +0 -40
  48. package/dist/feature.d.ts +0 -7
  49. package/dist/feature.d.ts.map +0 -1
  50. package/dist/feature.js.map +0 -1
@@ -0,0 +1,264 @@
1
+ import { buildPolicySafeAnswer } from "../orchestrator/buildAnswer.js";
2
+ import { web } from "@contractspec/lib.runtime-sandbox";
3
+
4
+ //#region src/handlers/policy-safe-knowledge-assistant.handlers.ts
5
+ const { generateId } = web;
6
+ function parseJsonArray(value) {
7
+ if (!value) return [];
8
+ try {
9
+ const parsed = JSON.parse(value);
10
+ return Array.isArray(parsed) ? parsed.filter((v) => typeof v === "string") : [];
11
+ } catch {
12
+ return [];
13
+ }
14
+ }
15
+ function nowIso() {
16
+ return (/* @__PURE__ */ new Date()).toISOString();
17
+ }
18
+ function createPolicySafeKnowledgeAssistantHandlers(db) {
19
+ async function getUserContext(input) {
20
+ const row = (await db.query(`SELECT * FROM psa_user_context WHERE "projectId" = $1 LIMIT 1`, [input.projectId])).rows[0];
21
+ if (!row) return {
22
+ projectId: input.projectId,
23
+ locale: "en-GB",
24
+ jurisdiction: "EU",
25
+ allowedScope: "education_only",
26
+ kbSnapshotId: null
27
+ };
28
+ return {
29
+ projectId: row.projectId,
30
+ locale: row.locale,
31
+ jurisdiction: row.jurisdiction,
32
+ allowedScope: row.allowedScope,
33
+ kbSnapshotId: row.kbSnapshotId
34
+ };
35
+ }
36
+ async function setUserContext(input) {
37
+ if ((await db.query(`SELECT "projectId" FROM psa_user_context WHERE "projectId" = $1 LIMIT 1`, [input.projectId])).rows.length) await db.execute(`UPDATE psa_user_context SET locale = $1, jurisdiction = $2, "allowedScope" = $3 WHERE "projectId" = $4`, [
38
+ input.locale,
39
+ input.jurisdiction,
40
+ input.allowedScope,
41
+ input.projectId
42
+ ]);
43
+ else await db.execute(`INSERT INTO psa_user_context ("projectId", locale, jurisdiction, "allowedScope", "kbSnapshotId") VALUES ($1, $2, $3, $4, $5)`, [
44
+ input.projectId,
45
+ input.locale,
46
+ input.jurisdiction,
47
+ input.allowedScope,
48
+ null
49
+ ]);
50
+ return await getUserContext({ projectId: input.projectId });
51
+ }
52
+ async function createRule(input) {
53
+ const id = generateId("psa_rule");
54
+ await db.execute(`INSERT INTO psa_rule (id, "projectId", jurisdiction, "topicKey") VALUES ($1, $2, $3, $4)`, [
55
+ id,
56
+ input.projectId,
57
+ input.jurisdiction,
58
+ input.topicKey
59
+ ]);
60
+ return {
61
+ id,
62
+ ...input
63
+ };
64
+ }
65
+ async function upsertRuleVersion(input) {
66
+ if (!input.sourceRefs.length) throw new Error("SOURCE_REFS_REQUIRED");
67
+ const rule = (await db.query(`SELECT * FROM psa_rule WHERE id = $1 LIMIT 1`, [input.ruleId])).rows[0];
68
+ if (!rule) throw new Error("RULE_NOT_FOUND");
69
+ const maxResult = await db.query(`SELECT MAX(version) as maxVersion FROM psa_rule_version WHERE "ruleId" = $1`, [input.ruleId]);
70
+ const version = Number(maxResult.rows[0]?.maxVersion ?? 0) + 1;
71
+ const id = generateId("psa_rv");
72
+ const createdAt = nowIso();
73
+ await db.execute(`INSERT INTO psa_rule_version (id, "ruleId", jurisdiction, "topicKey", version, content, status, "sourceRefsJson", "approvedBy", "approvedAt", "createdAt")
74
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [
75
+ id,
76
+ input.ruleId,
77
+ rule.jurisdiction,
78
+ rule.topicKey,
79
+ version,
80
+ input.content,
81
+ "draft",
82
+ JSON.stringify(input.sourceRefs),
83
+ null,
84
+ null,
85
+ createdAt
86
+ ]);
87
+ return {
88
+ id,
89
+ ruleId: input.ruleId,
90
+ jurisdiction: rule.jurisdiction,
91
+ topicKey: rule.topicKey,
92
+ version,
93
+ content: input.content,
94
+ status: "draft",
95
+ sourceRefs: input.sourceRefs,
96
+ createdAt: new Date(createdAt)
97
+ };
98
+ }
99
+ async function approveRuleVersion(input) {
100
+ const approvedAt = nowIso();
101
+ await db.execute(`UPDATE psa_rule_version SET status = $1, "approvedBy" = $2, "approvedAt" = $3 WHERE id = $4`, [
102
+ "approved",
103
+ input.approver,
104
+ approvedAt,
105
+ input.ruleVersionId
106
+ ]);
107
+ return {
108
+ ruleVersionId: input.ruleVersionId,
109
+ status: "approved"
110
+ };
111
+ }
112
+ async function publishSnapshot(input) {
113
+ const includedIds = (await db.query(`SELECT id FROM psa_rule_version WHERE jurisdiction = $1 AND status = 'approved' ORDER BY id ASC`, [input.jurisdiction])).rows.map((r) => r.id).filter(Boolean);
114
+ if (!includedIds.length) throw new Error("NO_APPROVED_RULES");
115
+ const id = generateId("psa_snap");
116
+ const publishedAt = nowIso();
117
+ await db.execute(`INSERT INTO psa_snapshot (id, jurisdiction, "asOfDate", "includedRuleVersionIdsJson", "publishedAt")
118
+ VALUES ($1, $2, $3, $4, $5)`, [
119
+ id,
120
+ input.jurisdiction,
121
+ input.asOfDate.toISOString(),
122
+ JSON.stringify(includedIds),
123
+ publishedAt
124
+ ]);
125
+ await db.execute(`UPDATE psa_user_context SET "kbSnapshotId" = $1 WHERE "projectId" = $2`, [id, input.projectId]);
126
+ return {
127
+ id,
128
+ jurisdiction: input.jurisdiction,
129
+ includedRuleVersionIds: includedIds
130
+ };
131
+ }
132
+ async function searchKb(input) {
133
+ const snap = (await db.query(`SELECT * FROM psa_snapshot WHERE id = $1 LIMIT 1`, [input.snapshotId])).rows[0];
134
+ if (!snap) throw new Error("SNAPSHOT_NOT_FOUND");
135
+ if (snap.jurisdiction !== input.jurisdiction) throw new Error("JURISDICTION_MISMATCH");
136
+ const includedIds = parseJsonArray(snap.includedRuleVersionIdsJson);
137
+ const tokens = input.query.toLowerCase().split(/\s+/).map((t) => t.trim()).filter(Boolean);
138
+ const items = [];
139
+ for (const id of includedIds) {
140
+ const rv = (await db.query(`SELECT * FROM psa_rule_version WHERE id = $1 LIMIT 1`, [id])).rows[0];
141
+ if (!rv) continue;
142
+ const hay = rv.content.toLowerCase();
143
+ if (!(tokens.length ? tokens.every((t) => hay.includes(t)) : true)) continue;
144
+ items.push({
145
+ ruleVersionId: rv.id,
146
+ excerpt: rv.content.slice(0, 140)
147
+ });
148
+ }
149
+ return { items };
150
+ }
151
+ async function answer(input) {
152
+ const ctx = await getUserContext({ projectId: input.projectId });
153
+ if (!ctx.kbSnapshotId) return await buildPolicySafeAnswer({
154
+ envelope: {
155
+ traceId: generateId("trace"),
156
+ locale: ctx.locale,
157
+ kbSnapshotId: "",
158
+ allowedScope: ctx.allowedScope,
159
+ regulatoryContext: { jurisdiction: ctx.jurisdiction }
160
+ },
161
+ question: input.question,
162
+ kbSearch: async () => ({ items: [] })
163
+ });
164
+ return await buildPolicySafeAnswer({
165
+ envelope: {
166
+ traceId: generateId("trace"),
167
+ locale: ctx.locale,
168
+ kbSnapshotId: ctx.kbSnapshotId,
169
+ allowedScope: ctx.allowedScope,
170
+ regulatoryContext: { jurisdiction: ctx.jurisdiction }
171
+ },
172
+ question: input.question,
173
+ kbSearch: async (q) => await searchKb(q)
174
+ });
175
+ }
176
+ async function createChangeCandidate(input) {
177
+ const id = generateId("psa_change");
178
+ await db.execute(`INSERT INTO psa_change_candidate (id, "projectId", jurisdiction, "detectedAt", "diffSummary", "riskLevel", "proposedRuleVersionIdsJson")
179
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
180
+ id,
181
+ input.projectId,
182
+ input.jurisdiction,
183
+ nowIso(),
184
+ input.diffSummary,
185
+ input.riskLevel,
186
+ JSON.stringify(input.proposedRuleVersionIds)
187
+ ]);
188
+ return { id };
189
+ }
190
+ async function createReviewTask(input) {
191
+ const candidate = (await db.query(`SELECT * FROM psa_change_candidate WHERE id = $1 LIMIT 1`, [input.changeCandidateId])).rows[0];
192
+ if (!candidate) throw new Error("CHANGE_CANDIDATE_NOT_FOUND");
193
+ const assignedRole = candidate.riskLevel === "high" ? "expert" : "curator";
194
+ const id = generateId("psa_review");
195
+ await db.execute(`INSERT INTO psa_review_task (id, "changeCandidateId", status, "assignedRole", decision, "decidedAt", "decidedBy")
196
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
197
+ id,
198
+ input.changeCandidateId,
199
+ "open",
200
+ assignedRole,
201
+ null,
202
+ null,
203
+ null
204
+ ]);
205
+ return {
206
+ id,
207
+ assignedRole
208
+ };
209
+ }
210
+ async function submitDecision(input) {
211
+ const task = (await db.query(`SELECT * FROM psa_review_task WHERE id = $1 LIMIT 1`, [input.reviewTaskId])).rows[0];
212
+ if (!task) throw new Error("REVIEW_TASK_NOT_FOUND");
213
+ const candidate = (await db.query(`SELECT * FROM psa_change_candidate WHERE id = $1 LIMIT 1`, [task.changeCandidateId])).rows[0];
214
+ if (!candidate) throw new Error("CHANGE_CANDIDATE_NOT_FOUND");
215
+ if (candidate.riskLevel === "high" && input.decision === "approve" && input.decidedByRole !== "expert") throw new Error("FORBIDDEN_ROLE");
216
+ const decidedAt = nowIso();
217
+ await db.execute(`UPDATE psa_review_task SET status = $1, decision = $2, "decidedAt" = $3, "decidedBy" = $4 WHERE id = $5`, [
218
+ "decided",
219
+ input.decision,
220
+ decidedAt,
221
+ input.decidedBy,
222
+ input.reviewTaskId
223
+ ]);
224
+ return {
225
+ id: input.reviewTaskId,
226
+ status: "decided"
227
+ };
228
+ }
229
+ async function publishIfReady(input) {
230
+ const openResult = await db.query(`SELECT COUNT(*) as count FROM psa_review_task WHERE status != 'decided'`, []);
231
+ if (Number(openResult.rows[0]?.count ?? 0) > 0) throw new Error("NOT_READY");
232
+ const decidedResult = await db.query(`SELECT * FROM psa_review_task`, []);
233
+ for (const row of decidedResult.rows) {
234
+ if (row.decision !== "approve") continue;
235
+ const cand = (await db.query(`SELECT * FROM psa_change_candidate WHERE id = $1 LIMIT 1`, [row.changeCandidateId])).rows[0];
236
+ if (!cand) continue;
237
+ if (cand.jurisdiction !== input.jurisdiction) continue;
238
+ const proposedIds = parseJsonArray(cand.proposedRuleVersionIdsJson);
239
+ for (const rvId of proposedIds) {
240
+ const rvQueryResult = await db.query(`SELECT status FROM psa_rule_version WHERE id = $1 LIMIT 1`, [rvId]);
241
+ if (String(rvQueryResult.rows[0]?.status ?? "") !== "approved") throw new Error("NOT_READY");
242
+ }
243
+ }
244
+ return { published: true };
245
+ }
246
+ return {
247
+ getUserContext,
248
+ setUserContext,
249
+ createRule,
250
+ upsertRuleVersion,
251
+ approveRuleVersion,
252
+ publishSnapshot,
253
+ searchKb,
254
+ answer,
255
+ createChangeCandidate,
256
+ createReviewTask,
257
+ submitDecision,
258
+ publishIfReady
259
+ };
260
+ }
261
+
262
+ //#endregion
263
+ export { createPolicySafeKnowledgeAssistantHandlers };
264
+ //# sourceMappingURL=policy-safe-knowledge-assistant.handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-safe-knowledge-assistant.handlers.js","names":[],"sources":["../../src/handlers/policy-safe-knowledge-assistant.handlers.ts"],"sourcesContent":["/**\n * Runtime-local handlers for the Policy-safe Knowledge Assistant template.\n *\n * These handlers are intentionally minimal and deterministic:\n * - No external LLM calls\n * - No web fetching as primary truth\n * - Answers are derived from KB snapshots and must include citations\n */\nimport type { DatabasePort, DbRow } from '@contractspec/lib.runtime-sandbox';\nimport { web } from '@contractspec/lib.runtime-sandbox';\nconst { generateId } = web;\n\nimport { buildPolicySafeAnswer } from '../orchestrator/buildAnswer';\n\ntype AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';\ntype RiskLevel = 'low' | 'medium' | 'high';\ntype ReviewRole = 'curator' | 'expert';\n\ninterface UserContextRow extends Record<string, unknown> {\n projectId: string;\n locale: string;\n jurisdiction: string;\n allowedScope: AllowedScope;\n kbSnapshotId: string | null;\n}\n\ninterface RuleRow extends Record<string, unknown> {\n id: string;\n projectId: string;\n jurisdiction: string;\n topicKey: string;\n}\n\ninterface RuleVersionRow extends Record<string, unknown> {\n id: string;\n ruleId: string;\n jurisdiction: string;\n topicKey: string;\n version: number;\n content: string;\n status: string;\n sourceRefsJson: string;\n approvedBy: string | null;\n approvedAt: string | null;\n createdAt: string;\n}\n\ninterface SnapshotRow extends Record<string, unknown> {\n id: string;\n jurisdiction: string;\n asOfDate: string;\n includedRuleVersionIdsJson: string;\n publishedAt: string;\n}\n\ninterface ChangeCandidateRow extends Record<string, unknown> {\n id: string;\n projectId: string;\n jurisdiction: string;\n detectedAt: string;\n diffSummary: string;\n riskLevel: RiskLevel;\n proposedRuleVersionIdsJson: string;\n}\n\ninterface ReviewTaskRow extends Record<string, unknown> {\n id: string;\n changeCandidateId: string;\n status: string;\n assignedRole: ReviewRole;\n decision: string | null;\n decidedAt: string | null;\n decidedBy: string | null;\n}\n\nfunction parseJsonArray(value: string | null): string[] {\n if (!value) return [];\n try {\n const parsed = JSON.parse(value) as unknown;\n return Array.isArray(parsed)\n ? parsed.filter((v) => typeof v === 'string')\n : [];\n } catch {\n return [];\n }\n}\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\nexport function createPolicySafeKnowledgeAssistantHandlers(db: DatabasePort) {\n async function getUserContext(input: { projectId: string }) {\n const result = await db.query(\n `SELECT * FROM psa_user_context WHERE \"projectId\" = $1 LIMIT 1`,\n [input.projectId]\n );\n const row = result.rows[0] as UserContextRow | undefined;\n if (!row) {\n return {\n projectId: input.projectId,\n locale: 'en-GB',\n jurisdiction: 'EU',\n allowedScope: 'education_only' as const,\n kbSnapshotId: null as string | null,\n };\n }\n return {\n projectId: row.projectId,\n locale: row.locale,\n jurisdiction: row.jurisdiction,\n allowedScope: row.allowedScope,\n kbSnapshotId: row.kbSnapshotId,\n };\n }\n\n async function setUserContext(input: {\n projectId: string;\n locale: string;\n jurisdiction: string;\n allowedScope: AllowedScope;\n }) {\n const existing = await db.query(\n `SELECT \"projectId\" FROM psa_user_context WHERE \"projectId\" = $1 LIMIT 1`,\n [input.projectId]\n );\n if (existing.rows.length) {\n await db.execute(\n `UPDATE psa_user_context SET locale = $1, jurisdiction = $2, \"allowedScope\" = $3 WHERE \"projectId\" = $4`,\n [input.locale, input.jurisdiction, input.allowedScope, input.projectId]\n );\n } else {\n await db.execute(\n `INSERT INTO psa_user_context (\"projectId\", locale, jurisdiction, \"allowedScope\", \"kbSnapshotId\") VALUES ($1, $2, $3, $4, $5)`,\n [\n input.projectId,\n input.locale,\n input.jurisdiction,\n input.allowedScope,\n null,\n ]\n );\n }\n return await getUserContext({ projectId: input.projectId });\n }\n\n async function createRule(input: {\n projectId: string;\n jurisdiction: string;\n topicKey: string;\n }) {\n const id = generateId('psa_rule');\n await db.execute(\n `INSERT INTO psa_rule (id, \"projectId\", jurisdiction, \"topicKey\") VALUES ($1, $2, $3, $4)`,\n [id, input.projectId, input.jurisdiction, input.topicKey]\n );\n return { id, ...input };\n }\n\n async function upsertRuleVersion(input: {\n projectId: string;\n ruleId: string;\n content: string;\n sourceRefs: { sourceDocumentId: string; excerpt?: string }[];\n }) {\n if (!input.sourceRefs.length) throw new Error('SOURCE_REFS_REQUIRED');\n const rulesResult = await db.query(\n `SELECT * FROM psa_rule WHERE id = $1 LIMIT 1`,\n [input.ruleId]\n );\n const rule = rulesResult.rows[0] as RuleRow | undefined;\n if (!rule) throw new Error('RULE_NOT_FOUND');\n\n const maxResult = await db.query(\n `SELECT MAX(version) as maxVersion FROM psa_rule_version WHERE \"ruleId\" = $1`,\n [input.ruleId]\n );\n const maxVersion = Number(\n (maxResult.rows[0] as Record<string, unknown>)?.maxVersion ?? 0\n );\n const version = maxVersion + 1;\n const id = generateId('psa_rv');\n const createdAt = nowIso();\n await db.execute(\n `INSERT INTO psa_rule_version (id, \"ruleId\", jurisdiction, \"topicKey\", version, content, status, \"sourceRefsJson\", \"approvedBy\", \"approvedAt\", \"createdAt\")\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,\n [\n id,\n input.ruleId,\n rule.jurisdiction,\n rule.topicKey,\n version,\n input.content,\n 'draft',\n JSON.stringify(input.sourceRefs),\n null,\n null,\n createdAt,\n ]\n );\n return {\n id,\n ruleId: input.ruleId,\n jurisdiction: rule.jurisdiction,\n topicKey: rule.topicKey,\n version,\n content: input.content,\n status: 'draft',\n sourceRefs: input.sourceRefs,\n createdAt: new Date(createdAt),\n };\n }\n\n async function approveRuleVersion(input: {\n ruleVersionId: string;\n approver: string;\n }) {\n const approvedAt = nowIso();\n await db.execute(\n `UPDATE psa_rule_version SET status = $1, \"approvedBy\" = $2, \"approvedAt\" = $3 WHERE id = $4`,\n ['approved', input.approver, approvedAt, input.ruleVersionId]\n );\n return { ruleVersionId: input.ruleVersionId, status: 'approved' as const };\n }\n\n async function publishSnapshot(input: {\n projectId: string;\n jurisdiction: string;\n asOfDate: Date;\n }) {\n const approvedResult = await db.query(\n `SELECT id FROM psa_rule_version WHERE jurisdiction = $1 AND status = 'approved' ORDER BY id ASC`,\n [input.jurisdiction]\n );\n const includedIds = approvedResult.rows\n .map((r: DbRow) => (r as { id: string }).id)\n .filter(Boolean);\n if (!includedIds.length) throw new Error('NO_APPROVED_RULES');\n const id = generateId('psa_snap');\n const publishedAt = nowIso();\n await db.execute(\n `INSERT INTO psa_snapshot (id, jurisdiction, \"asOfDate\", \"includedRuleVersionIdsJson\", \"publishedAt\")\n VALUES ($1, $2, $3, $4, $5)`,\n [\n id,\n input.jurisdiction,\n input.asOfDate.toISOString(),\n JSON.stringify(includedIds),\n publishedAt,\n ]\n );\n // update user context snapshot pointer (single-profile demo)\n await db.execute(\n `UPDATE psa_user_context SET \"kbSnapshotId\" = $1 WHERE \"projectId\" = $2`,\n [id, input.projectId]\n );\n return {\n id,\n jurisdiction: input.jurisdiction,\n includedRuleVersionIds: includedIds,\n };\n }\n\n async function searchKb(input: {\n snapshotId: string;\n jurisdiction: string;\n query: string;\n }) {\n const snapResult = await db.query(\n `SELECT * FROM psa_snapshot WHERE id = $1 LIMIT 1`,\n [input.snapshotId]\n );\n const snap = snapResult.rows[0] as SnapshotRow | undefined;\n if (!snap) throw new Error('SNAPSHOT_NOT_FOUND');\n if (snap.jurisdiction !== input.jurisdiction)\n throw new Error('JURISDICTION_MISMATCH');\n\n const includedIds = parseJsonArray(snap.includedRuleVersionIdsJson);\n const tokens = input.query\n .toLowerCase()\n .split(/\\s+/)\n .map((t) => t.trim())\n .filter(Boolean);\n\n const items: { ruleVersionId: string; excerpt?: string }[] = [];\n for (const id of includedIds) {\n const rvResult = await db.query(\n `SELECT * FROM psa_rule_version WHERE id = $1 LIMIT 1`,\n [id]\n );\n const rv = rvResult.rows[0] as RuleVersionRow | undefined;\n if (!rv) continue;\n const hay = rv.content.toLowerCase();\n const match = tokens.length ? tokens.every((t) => hay.includes(t)) : true;\n if (!match) continue;\n items.push({ ruleVersionId: rv.id, excerpt: rv.content.slice(0, 140) });\n }\n return { items };\n }\n\n async function answer(input: { projectId: string; question: string }) {\n const ctx = await getUserContext({ projectId: input.projectId });\n if (!ctx.kbSnapshotId) {\n // fail closed: no snapshot id => refuse via gate\n const refused = await buildPolicySafeAnswer({\n envelope: {\n traceId: generateId('trace'),\n locale: ctx.locale,\n kbSnapshotId: '',\n allowedScope: ctx.allowedScope,\n regulatoryContext: { jurisdiction: ctx.jurisdiction },\n },\n question: input.question,\n kbSearch: async () => ({ items: [] }),\n });\n return refused;\n }\n return await buildPolicySafeAnswer({\n envelope: {\n traceId: generateId('trace'),\n locale: ctx.locale,\n kbSnapshotId: ctx.kbSnapshotId,\n allowedScope: ctx.allowedScope,\n regulatoryContext: { jurisdiction: ctx.jurisdiction },\n },\n question: input.question,\n kbSearch: async (q) => await searchKb(q),\n });\n }\n\n async function createChangeCandidate(input: {\n projectId: string;\n jurisdiction: string;\n diffSummary: string;\n riskLevel: RiskLevel;\n proposedRuleVersionIds: string[];\n }) {\n const id = generateId('psa_change');\n await db.execute(\n `INSERT INTO psa_change_candidate (id, \"projectId\", jurisdiction, \"detectedAt\", \"diffSummary\", \"riskLevel\", \"proposedRuleVersionIdsJson\")\n VALUES ($1, $2, $3, $4, $5, $6, $7)`,\n [\n id,\n input.projectId,\n input.jurisdiction,\n nowIso(),\n input.diffSummary,\n input.riskLevel,\n JSON.stringify(input.proposedRuleVersionIds),\n ]\n );\n return { id };\n }\n\n async function createReviewTask(input: { changeCandidateId: string }) {\n const candResult = await db.query(\n `SELECT * FROM psa_change_candidate WHERE id = $1 LIMIT 1`,\n [input.changeCandidateId]\n );\n const candidate = candResult.rows[0] as ChangeCandidateRow | undefined;\n if (!candidate) throw new Error('CHANGE_CANDIDATE_NOT_FOUND');\n const assignedRole: ReviewRole =\n candidate.riskLevel === 'high' ? 'expert' : 'curator';\n const id = generateId('psa_review');\n await db.execute(\n `INSERT INTO psa_review_task (id, \"changeCandidateId\", status, \"assignedRole\", decision, \"decidedAt\", \"decidedBy\")\n VALUES ($1, $2, $3, $4, $5, $6, $7)`,\n [id, input.changeCandidateId, 'open', assignedRole, null, null, null]\n );\n return { id, assignedRole };\n }\n\n async function submitDecision(input: {\n reviewTaskId: string;\n decision: 'approve' | 'reject';\n decidedByRole: ReviewRole;\n decidedBy: string;\n }) {\n const reviewResult = await db.query(\n `SELECT * FROM psa_review_task WHERE id = $1 LIMIT 1`,\n [input.reviewTaskId]\n );\n const task = reviewResult.rows[0] as ReviewTaskRow | undefined;\n if (!task) throw new Error('REVIEW_TASK_NOT_FOUND');\n\n const candidateResult = await db.query(\n `SELECT * FROM psa_change_candidate WHERE id = $1 LIMIT 1`,\n [task.changeCandidateId]\n );\n const candidate = candidateResult.rows[0] as ChangeCandidateRow | undefined;\n if (!candidate) throw new Error('CHANGE_CANDIDATE_NOT_FOUND');\n if (\n candidate.riskLevel === 'high' &&\n input.decision === 'approve' &&\n input.decidedByRole !== 'expert'\n ) {\n throw new Error('FORBIDDEN_ROLE');\n }\n\n const decidedAt = nowIso();\n await db.execute(\n `UPDATE psa_review_task SET status = $1, decision = $2, \"decidedAt\" = $3, \"decidedBy\" = $4 WHERE id = $5`,\n [\n 'decided',\n input.decision,\n decidedAt,\n input.decidedBy,\n input.reviewTaskId,\n ]\n );\n return { id: input.reviewTaskId, status: 'decided' as const };\n }\n\n async function publishIfReady(input: { jurisdiction: string }) {\n const openResult = await db.query(\n `SELECT COUNT(*) as count FROM psa_review_task WHERE status != 'decided'`,\n []\n );\n const openCount = Number(\n (openResult.rows[0] as Record<string, unknown>)?.count ?? 0\n );\n if (openCount > 0) throw new Error('NOT_READY');\n\n // Ensure for each approved review, all proposed rule versions are approved in KB.\n const decidedResult = await db.query(`SELECT * FROM psa_review_task`, []);\n for (const row of decidedResult.rows as ReviewTaskRow[]) {\n if (row.decision !== 'approve') continue;\n const candQueryResult = await db.query(\n `SELECT * FROM psa_change_candidate WHERE id = $1 LIMIT 1`,\n [row.changeCandidateId]\n );\n const cand = candQueryResult.rows[0] as ChangeCandidateRow | undefined;\n if (!cand) continue;\n if (cand.jurisdiction !== input.jurisdiction) continue;\n const proposedIds = parseJsonArray(cand.proposedRuleVersionIdsJson);\n for (const rvId of proposedIds) {\n const rvQueryResult = await db.query(\n `SELECT status FROM psa_rule_version WHERE id = $1 LIMIT 1`,\n [rvId]\n );\n const status = String(\n (rvQueryResult.rows[0] as Record<string, unknown> | undefined)\n ?.status ?? ''\n );\n if (status !== 'approved') throw new Error('NOT_READY');\n }\n }\n return { published: true as const };\n }\n\n return {\n // Onboarding\n getUserContext,\n setUserContext,\n\n // KB\n createRule,\n upsertRuleVersion,\n approveRuleVersion,\n publishSnapshot,\n searchKb,\n\n // Assistant\n answer,\n\n // Pipeline\n createChangeCandidate,\n createReviewTask,\n submitDecision,\n publishIfReady,\n };\n}\n\nexport type PolicySafeKnowledgeAssistantHandlers = ReturnType<\n typeof createPolicySafeKnowledgeAssistantHandlers\n>;\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import example from "./example.js";
2
- import { PolicySafeKnowledgeAssistantFeature } from "./feature.js";
2
+ import { PolicySafeKnowledgeAssistantFeature } from "./policy-safe-knowledge-assistant.feature.js";
3
3
  import { DEMO_FIXTURES } from "./seed/fixtures.js";
4
4
  import { AssistantAnswerIR, BuildAnswerInput, buildPolicySafeAnswer } from "./orchestrator/buildAnswer.js";
5
- export { AssistantAnswerIR, BuildAnswerInput, DEMO_FIXTURES, PolicySafeKnowledgeAssistantFeature, buildPolicySafeAnswer, example };
5
+ import { PolicySafeKnowledgeAssistantDashboard } from "./ui/PolicySafeKnowledgeAssistantDashboard.js";
6
+ import "./ui/index.js";
7
+ import { PolicySafeKnowledgeAssistantHandlers, createPolicySafeKnowledgeAssistantHandlers } from "./handlers/policy-safe-knowledge-assistant.handlers.js";
8
+ export { AssistantAnswerIR, BuildAnswerInput, DEMO_FIXTURES, PolicySafeKnowledgeAssistantDashboard, PolicySafeKnowledgeAssistantFeature, PolicySafeKnowledgeAssistantHandlers, buildPolicySafeAnswer, createPolicySafeKnowledgeAssistantHandlers, example };
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import example_default from "./example.js";
2
- import { PolicySafeKnowledgeAssistantFeature } from "./feature.js";
2
+ import { PolicySafeKnowledgeAssistantFeature } from "./policy-safe-knowledge-assistant.feature.js";
3
3
  import { DEMO_FIXTURES } from "./seed/fixtures.js";
4
4
  import { buildPolicySafeAnswer } from "./orchestrator/buildAnswer.js";
5
+ import { createPolicySafeKnowledgeAssistantHandlers } from "./handlers/policy-safe-knowledge-assistant.handlers.js";
6
+ import { PolicySafeKnowledgeAssistantDashboard } from "./ui/PolicySafeKnowledgeAssistantDashboard.js";
7
+ import "./ui/index.js";
5
8
  import "./docs/index.js";
6
9
 
7
- export { DEMO_FIXTURES, PolicySafeKnowledgeAssistantFeature, buildPolicySafeAnswer, example_default as example };
10
+ export { DEMO_FIXTURES, PolicySafeKnowledgeAssistantDashboard, PolicySafeKnowledgeAssistantFeature, buildPolicySafeAnswer, createPolicySafeKnowledgeAssistantHandlers, example_default as example };
@@ -1 +1 @@
1
- {"version":3,"file":"buildAnswer.js","names":["draft: AssistantAnswerIR"],"sources":["../../src/orchestrator/buildAnswer.ts"],"sourcesContent":["import {\n enforceAllowedScope,\n enforceCitations,\n validateEnvelope,\n} from '@contractspec/example.locale-jurisdiction-gate/policy/guard';\n\ntype AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';\n\nexport interface AssistantAnswerIR {\n locale: string;\n jurisdiction: string;\n allowedScope: AllowedScope;\n sections: { heading: string; body: string }[];\n citations: {\n kbSnapshotId: string;\n sourceType: string;\n sourceId: string;\n title?: string;\n excerpt?: string;\n }[];\n disclaimers?: string[];\n riskFlags?: string[];\n refused?: boolean;\n refusalReason?: string;\n}\n\nexport interface BuildAnswerInput {\n envelope: {\n traceId: string;\n locale: string;\n kbSnapshotId: string;\n allowedScope: AllowedScope;\n regulatoryContext: { jurisdiction: string };\n };\n question: string;\n kbSearch: (input: {\n snapshotId: string;\n jurisdiction: string;\n query: string;\n }) => Promise<{ items: { ruleVersionId: string; excerpt?: string }[] }>;\n}\n\n/**\n * Build a policy-safe assistant answer derived from KB search results.\n *\n * Deterministic: no LLM calls; if search yields no results, it refuses.\n */\nexport async function buildPolicySafeAnswer(\n input: BuildAnswerInput\n): Promise<AssistantAnswerIR> {\n const env = validateEnvelope(input.envelope);\n if (!env.ok) {\n return {\n locale: input.envelope.locale ?? 'en-GB',\n jurisdiction: input.envelope.regulatoryContext?.jurisdiction ?? 'UNKNOWN',\n allowedScope: input.envelope.allowedScope ?? 'education_only',\n sections: [{ heading: 'Request blocked', body: env.error.message }],\n citations: [],\n disclaimers: ['This system refuses to answer without a valid envelope.'],\n riskFlags: [env.error.code],\n refused: true,\n refusalReason: env.error.code,\n };\n }\n\n const results = await input.kbSearch({\n snapshotId: env.value.kbSnapshotId,\n jurisdiction: env.value.regulatoryContext?.jurisdiction ?? 'UNKNOWN',\n query: input.question,\n });\n\n const citations = results.items.map((item) => ({\n kbSnapshotId: env.value.kbSnapshotId,\n sourceType: 'ruleVersion',\n sourceId: item.ruleVersionId,\n title: 'Curated rule version',\n excerpt: item.excerpt,\n }));\n\n const draft: AssistantAnswerIR = {\n locale: env.value.locale,\n jurisdiction: env.value.regulatoryContext?.jurisdiction ?? 'UNKNOWN',\n allowedScope: env.value.allowedScope,\n sections: [\n {\n heading: 'Answer (KB-derived)',\n body:\n results.items.length > 0\n ? `This answer is derived from ${results.items.length} curated rule version(s) in the referenced snapshot.`\n : 'No curated knowledge found in the referenced snapshot.',\n },\n ],\n citations,\n disclaimers: ['Educational demo only.'],\n riskFlags: [],\n };\n\n const scope = enforceAllowedScope(env.value.allowedScope, draft);\n if (!scope.ok) {\n return {\n ...draft,\n sections: [{ heading: 'Escalation required', body: scope.error.message }],\n refused: true,\n refusalReason: scope.error.code,\n riskFlags: [...(draft.riskFlags ?? []), scope.error.code],\n };\n }\n\n const cited = enforceCitations(draft);\n if (!cited.ok) {\n return {\n ...draft,\n sections: [{ heading: 'Request blocked', body: cited.error.message }],\n citations: [],\n refused: true,\n refusalReason: cited.error.code,\n riskFlags: [...(draft.riskFlags ?? []), cited.error.code],\n };\n }\n\n return draft;\n}\n"],"mappings":";;;;;;;;AA+CA,eAAsB,sBACpB,OAC4B;CAC5B,MAAM,MAAM,iBAAiB,MAAM,SAAS;AAC5C,KAAI,CAAC,IAAI,GACP,QAAO;EACL,QAAQ,MAAM,SAAS,UAAU;EACjC,cAAc,MAAM,SAAS,mBAAmB,gBAAgB;EAChE,cAAc,MAAM,SAAS,gBAAgB;EAC7C,UAAU,CAAC;GAAE,SAAS;GAAmB,MAAM,IAAI,MAAM;GAAS,CAAC;EACnE,WAAW,EAAE;EACb,aAAa,CAAC,0DAA0D;EACxE,WAAW,CAAC,IAAI,MAAM,KAAK;EAC3B,SAAS;EACT,eAAe,IAAI,MAAM;EAC1B;CAGH,MAAM,UAAU,MAAM,MAAM,SAAS;EACnC,YAAY,IAAI,MAAM;EACtB,cAAc,IAAI,MAAM,mBAAmB,gBAAgB;EAC3D,OAAO,MAAM;EACd,CAAC;CAEF,MAAM,YAAY,QAAQ,MAAM,KAAK,UAAU;EAC7C,cAAc,IAAI,MAAM;EACxB,YAAY;EACZ,UAAU,KAAK;EACf,OAAO;EACP,SAAS,KAAK;EACf,EAAE;CAEH,MAAMA,QAA2B;EAC/B,QAAQ,IAAI,MAAM;EAClB,cAAc,IAAI,MAAM,mBAAmB,gBAAgB;EAC3D,cAAc,IAAI,MAAM;EACxB,UAAU,CACR;GACE,SAAS;GACT,MACE,QAAQ,MAAM,SAAS,IACnB,+BAA+B,QAAQ,MAAM,OAAO,wDACpD;GACP,CACF;EACD;EACA,aAAa,CAAC,yBAAyB;EACvC,WAAW,EAAE;EACd;CAED,MAAM,QAAQ,oBAAoB,IAAI,MAAM,cAAc,MAAM;AAChE,KAAI,CAAC,MAAM,GACT,QAAO;EACL,GAAG;EACH,UAAU,CAAC;GAAE,SAAS;GAAuB,MAAM,MAAM,MAAM;GAAS,CAAC;EACzE,SAAS;EACT,eAAe,MAAM,MAAM;EAC3B,WAAW,CAAC,GAAI,MAAM,aAAa,EAAE,EAAG,MAAM,MAAM,KAAK;EAC1D;CAGH,MAAM,QAAQ,iBAAiB,MAAM;AACrC,KAAI,CAAC,MAAM,GACT,QAAO;EACL,GAAG;EACH,UAAU,CAAC;GAAE,SAAS;GAAmB,MAAM,MAAM,MAAM;GAAS,CAAC;EACrE,WAAW,EAAE;EACb,SAAS;EACT,eAAe,MAAM,MAAM;EAC3B,WAAW,CAAC,GAAI,MAAM,aAAa,EAAE,EAAG,MAAM,MAAM,KAAK;EAC1D;AAGH,QAAO"}
1
+ {"version":3,"file":"buildAnswer.js","names":[],"sources":["../../src/orchestrator/buildAnswer.ts"],"sourcesContent":["import {\n enforceAllowedScope,\n enforceCitations,\n validateEnvelope,\n} from '@contractspec/example.locale-jurisdiction-gate/policy/guard';\n\ntype AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';\n\nexport interface AssistantAnswerIR {\n locale: string;\n jurisdiction: string;\n allowedScope: AllowedScope;\n sections: { heading: string; body: string }[];\n citations: {\n kbSnapshotId: string;\n sourceType: string;\n sourceId: string;\n title?: string;\n excerpt?: string;\n }[];\n disclaimers?: string[];\n riskFlags?: string[];\n refused?: boolean;\n refusalReason?: string;\n}\n\nexport interface BuildAnswerInput {\n envelope: {\n traceId: string;\n locale: string;\n kbSnapshotId: string;\n allowedScope: AllowedScope;\n regulatoryContext: { jurisdiction: string };\n };\n question: string;\n kbSearch: (input: {\n snapshotId: string;\n jurisdiction: string;\n query: string;\n }) => Promise<{ items: { ruleVersionId: string; excerpt?: string }[] }>;\n}\n\n/**\n * Build a policy-safe assistant answer derived from KB search results.\n *\n * Deterministic: no LLM calls; if search yields no results, it refuses.\n */\nexport async function buildPolicySafeAnswer(\n input: BuildAnswerInput\n): Promise<AssistantAnswerIR> {\n const env = validateEnvelope(input.envelope);\n if (!env.ok) {\n return {\n locale: input.envelope.locale ?? 'en-GB',\n jurisdiction: input.envelope.regulatoryContext?.jurisdiction ?? 'UNKNOWN',\n allowedScope: input.envelope.allowedScope ?? 'education_only',\n sections: [{ heading: 'Request blocked', body: env.error.message }],\n citations: [],\n disclaimers: ['This system refuses to answer without a valid envelope.'],\n riskFlags: [env.error.code],\n refused: true,\n refusalReason: env.error.code,\n };\n }\n\n const results = await input.kbSearch({\n snapshotId: env.value.kbSnapshotId,\n jurisdiction: env.value.regulatoryContext?.jurisdiction ?? 'UNKNOWN',\n query: input.question,\n });\n\n const citations = results.items.map((item) => ({\n kbSnapshotId: env.value.kbSnapshotId,\n sourceType: 'ruleVersion',\n sourceId: item.ruleVersionId,\n title: 'Curated rule version',\n excerpt: item.excerpt,\n }));\n\n const draft: AssistantAnswerIR = {\n locale: env.value.locale,\n jurisdiction: env.value.regulatoryContext?.jurisdiction ?? 'UNKNOWN',\n allowedScope: env.value.allowedScope,\n sections: [\n {\n heading: 'Answer (KB-derived)',\n body:\n results.items.length > 0\n ? `This answer is derived from ${results.items.length} curated rule version(s) in the referenced snapshot.`\n : 'No curated knowledge found in the referenced snapshot.',\n },\n ],\n citations,\n disclaimers: ['Educational demo only.'],\n riskFlags: [],\n };\n\n const scope = enforceAllowedScope(env.value.allowedScope, draft);\n if (!scope.ok) {\n return {\n ...draft,\n sections: [{ heading: 'Escalation required', body: scope.error.message }],\n refused: true,\n refusalReason: scope.error.code,\n riskFlags: [...(draft.riskFlags ?? []), scope.error.code],\n };\n }\n\n const cited = enforceCitations(draft);\n if (!cited.ok) {\n return {\n ...draft,\n sections: [{ heading: 'Request blocked', body: cited.error.message }],\n citations: [],\n refused: true,\n refusalReason: cited.error.code,\n riskFlags: [...(draft.riskFlags ?? []), cited.error.code],\n };\n }\n\n return draft;\n}\n"],"mappings":";;;;;;;;AA+CA,eAAsB,sBACpB,OAC4B;CAC5B,MAAM,MAAM,iBAAiB,MAAM,SAAS;AAC5C,KAAI,CAAC,IAAI,GACP,QAAO;EACL,QAAQ,MAAM,SAAS,UAAU;EACjC,cAAc,MAAM,SAAS,mBAAmB,gBAAgB;EAChE,cAAc,MAAM,SAAS,gBAAgB;EAC7C,UAAU,CAAC;GAAE,SAAS;GAAmB,MAAM,IAAI,MAAM;GAAS,CAAC;EACnE,WAAW,EAAE;EACb,aAAa,CAAC,0DAA0D;EACxE,WAAW,CAAC,IAAI,MAAM,KAAK;EAC3B,SAAS;EACT,eAAe,IAAI,MAAM;EAC1B;CAGH,MAAM,UAAU,MAAM,MAAM,SAAS;EACnC,YAAY,IAAI,MAAM;EACtB,cAAc,IAAI,MAAM,mBAAmB,gBAAgB;EAC3D,OAAO,MAAM;EACd,CAAC;CAEF,MAAM,YAAY,QAAQ,MAAM,KAAK,UAAU;EAC7C,cAAc,IAAI,MAAM;EACxB,YAAY;EACZ,UAAU,KAAK;EACf,OAAO;EACP,SAAS,KAAK;EACf,EAAE;CAEH,MAAM,QAA2B;EAC/B,QAAQ,IAAI,MAAM;EAClB,cAAc,IAAI,MAAM,mBAAmB,gBAAgB;EAC3D,cAAc,IAAI,MAAM;EACxB,UAAU,CACR;GACE,SAAS;GACT,MACE,QAAQ,MAAM,SAAS,IACnB,+BAA+B,QAAQ,MAAM,OAAO,wDACpD;GACP,CACF;EACD;EACA,aAAa,CAAC,yBAAyB;EACvC,WAAW,EAAE;EACd;CAED,MAAM,QAAQ,oBAAoB,IAAI,MAAM,cAAc,MAAM;AAChE,KAAI,CAAC,MAAM,GACT,QAAO;EACL,GAAG;EACH,UAAU,CAAC;GAAE,SAAS;GAAuB,MAAM,MAAM,MAAM;GAAS,CAAC;EACzE,SAAS;EACT,eAAe,MAAM,MAAM;EAC3B,WAAW,CAAC,GAAI,MAAM,aAAa,EAAE,EAAG,MAAM,MAAM,KAAK;EAC1D;CAGH,MAAM,QAAQ,iBAAiB,MAAM;AACrC,KAAI,CAAC,MAAM,GACT,QAAO;EACL,GAAG;EACH,UAAU,CAAC;GAAE,SAAS;GAAmB,MAAM,MAAM,MAAM;GAAS,CAAC;EACrE,WAAW,EAAE;EACb,SAAS;EACT,eAAe,MAAM,MAAM;EAC3B,WAAW,CAAC,GAAI,MAAM,aAAa,EAAE,EAAG,MAAM,MAAM,KAAK;EAC1D;AAGH,QAAO"}
@@ -0,0 +1,7 @@
1
+ import * as _contractspec_lib_contracts0 from "@contractspec/lib.contracts";
2
+
3
+ //#region src/policy-safe-knowledge-assistant.feature.d.ts
4
+ declare const PolicySafeKnowledgeAssistantFeature: _contractspec_lib_contracts0.FeatureModuleSpec;
5
+ //#endregion
6
+ export { PolicySafeKnowledgeAssistantFeature };
7
+ //# sourceMappingURL=policy-safe-knowledge-assistant.feature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-safe-knowledge-assistant.feature.d.ts","names":[],"sources":["../src/policy-safe-knowledge-assistant.feature.ts"],"sourcesContent":[],"mappings":";;;cAEa,qCAuDX,4BAAA,CAvD8C"}
@@ -1,5 +1,7 @@
1
- //#region src/feature.ts
2
- const PolicySafeKnowledgeAssistantFeature = {
1
+ import { defineFeature } from "@contractspec/lib.contracts";
2
+
3
+ //#region src/policy-safe-knowledge-assistant.feature.ts
4
+ const PolicySafeKnowledgeAssistantFeature = defineFeature({
3
5
  meta: {
4
6
  key: "policy-safe-knowledge-assistant",
5
7
  version: "1.0.0",
@@ -141,8 +143,8 @@ const PolicySafeKnowledgeAssistantFeature = {
141
143
  version: "1.0.0"
142
144
  }
143
145
  ] }
144
- };
146
+ });
145
147
 
146
148
  //#endregion
147
149
  export { PolicySafeKnowledgeAssistantFeature };
148
- //# sourceMappingURL=feature.js.map
150
+ //# sourceMappingURL=policy-safe-knowledge-assistant.feature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-safe-knowledge-assistant.feature.js","names":[],"sources":["../src/policy-safe-knowledge-assistant.feature.ts"],"sourcesContent":["import { defineFeature } from '@contractspec/lib.contracts';\n\nexport const PolicySafeKnowledgeAssistantFeature = defineFeature({\n meta: {\n key: 'policy-safe-knowledge-assistant',\n version: '1.0.0',\n title: 'Policy-safe Knowledge Assistant',\n description:\n 'All-in-one example composing locale/jurisdiction gate + versioned KB + HITL pipeline + learning hub.',\n domain: 'knowledge',\n owners: ['@examples'],\n tags: ['assistant', 'knowledge', 'policy', 'hitl', 'learning'],\n stability: 'experimental',\n },\n operations: [\n // Gate\n { key: 'assistant.answer', version: '1.0.0' },\n { key: 'assistant.explainConcept', version: '1.0.0' },\n // KB\n { key: 'kb.ingestSource', version: '1.0.0' },\n { key: 'kb.upsertRuleVersion', version: '1.0.0' },\n { key: 'kb.approveRuleVersion', version: '1.0.0' },\n { key: 'kb.publishSnapshot', version: '1.0.0' },\n { key: 'kb.search', version: '1.0.0' },\n // Pipeline\n { key: 'kbPipeline.runWatch', version: '1.0.0' },\n { key: 'kbPipeline.createReviewTask', version: '1.0.0' },\n { key: 'kbPipeline.submitDecision', version: '1.0.0' },\n { key: 'kbPipeline.publishIfReady', version: '1.0.0' },\n ],\n events: [\n { key: 'assistant.answer.requested', version: '1.0.0' },\n { key: 'assistant.answer.blocked', version: '1.0.0' },\n { key: 'assistant.answer.delivered', version: '1.0.0' },\n { key: 'kb.source.ingested', version: '1.0.0' },\n { key: 'kb.ruleVersion.created', version: '1.0.0' },\n { key: 'kb.ruleVersion.approved', version: '1.0.0' },\n { key: 'kb.snapshot.published', version: '1.0.0' },\n { key: 'kb.change.detected', version: '1.0.0' },\n { key: 'kb.review.requested', version: '1.0.0' },\n { key: 'kb.review.decided', version: '1.0.0' },\n ],\n presentations: [],\n opToPresentation: [],\n presentationsTargets: [],\n capabilities: {\n requires: [\n { key: 'identity', version: '1.0.0' },\n { key: 'audit-trail', version: '1.0.0' },\n { key: 'notifications', version: '1.0.0' },\n { key: 'jobs', version: '1.0.0' },\n { key: 'feature-flags', version: '1.0.0' },\n { key: 'files', version: '1.0.0' },\n { key: 'metering', version: '1.0.0' },\n { key: 'learning-journey', version: '1.0.0' },\n ],\n },\n});\n"],"mappings":";;;AAEA,MAAa,sCAAsC,cAAc;CAC/D,MAAM;EACJ,KAAK;EACL,SAAS;EACT,OAAO;EACP,aACE;EACF,QAAQ;EACR,QAAQ,CAAC,YAAY;EACrB,MAAM;GAAC;GAAa;GAAa;GAAU;GAAQ;GAAW;EAC9D,WAAW;EACZ;CACD,YAAY;EAEV;GAAE,KAAK;GAAoB,SAAS;GAAS;EAC7C;GAAE,KAAK;GAA4B,SAAS;GAAS;EAErD;GAAE,KAAK;GAAmB,SAAS;GAAS;EAC5C;GAAE,KAAK;GAAwB,SAAS;GAAS;EACjD;GAAE,KAAK;GAAyB,SAAS;GAAS;EAClD;GAAE,KAAK;GAAsB,SAAS;GAAS;EAC/C;GAAE,KAAK;GAAa,SAAS;GAAS;EAEtC;GAAE,KAAK;GAAuB,SAAS;GAAS;EAChD;GAAE,KAAK;GAA+B,SAAS;GAAS;EACxD;GAAE,KAAK;GAA6B,SAAS;GAAS;EACtD;GAAE,KAAK;GAA6B,SAAS;GAAS;EACvD;CACD,QAAQ;EACN;GAAE,KAAK;GAA8B,SAAS;GAAS;EACvD;GAAE,KAAK;GAA4B,SAAS;GAAS;EACrD;GAAE,KAAK;GAA8B,SAAS;GAAS;EACvD;GAAE,KAAK;GAAsB,SAAS;GAAS;EAC/C;GAAE,KAAK;GAA0B,SAAS;GAAS;EACnD;GAAE,KAAK;GAA2B,SAAS;GAAS;EACpD;GAAE,KAAK;GAAyB,SAAS;GAAS;EAClD;GAAE,KAAK;GAAsB,SAAS;GAAS;EAC/C;GAAE,KAAK;GAAuB,SAAS;GAAS;EAChD;GAAE,KAAK;GAAqB,SAAS;GAAS;EAC/C;CACD,eAAe,EAAE;CACjB,kBAAkB,EAAE;CACpB,sBAAsB,EAAE;CACxB,cAAc,EACZ,UAAU;EACR;GAAE,KAAK;GAAY,SAAS;GAAS;EACrC;GAAE,KAAK;GAAe,SAAS;GAAS;EACxC;GAAE,KAAK;GAAiB,SAAS;GAAS;EAC1C;GAAE,KAAK;GAAQ,SAAS;GAAS;EACjC;GAAE,KAAK;GAAiB,SAAS;GAAS;EAC1C;GAAE,KAAK;GAAS,SAAS;GAAS;EAClC;GAAE,KAAK;GAAY,SAAS;GAAS;EACrC;GAAE,KAAK;GAAoB,SAAS;GAAS;EAC9C,EACF;CACF,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { DatabasePort } from "@contractspec/lib.runtime-sandbox";
2
+
3
+ //#region src/seeders/index.d.ts
4
+ declare function seedPolicyKnowledgeAssistant(params: {
5
+ projectId: string;
6
+ db: DatabasePort;
7
+ }): Promise<void>;
8
+ //#endregion
9
+ export { seedPolicyKnowledgeAssistant };
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/seeders/index.ts"],"sourcesContent":[],"mappings":";;;iBAEsB,4BAAA;;EAAA,EAAA,EAEhB,YAFgB;IAGrB"}
@@ -0,0 +1,16 @@
1
+ //#region src/seeders/index.ts
2
+ async function seedPolicyKnowledgeAssistant(params) {
3
+ const { projectId, db } = params;
4
+ if ((await db.query(`SELECT COUNT(*) as count FROM psa_user_context WHERE "projectId" = $1`, [projectId])).rows[0]?.count > 0) return;
5
+ await db.execute(`INSERT INTO psa_user_context ("projectId", locale, jurisdiction, "allowedScope")
6
+ VALUES ($1, $2, $3, $4)`, [
7
+ projectId,
8
+ "en-GB",
9
+ "EU",
10
+ "education_only"
11
+ ]);
12
+ }
13
+
14
+ //#endregion
15
+ export { seedPolicyKnowledgeAssistant };
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/seeders/index.ts"],"sourcesContent":["import type { DatabasePort } from '@contractspec/lib.runtime-sandbox';\n\nexport async function seedPolicyKnowledgeAssistant(params: {\n projectId: string;\n db: DatabasePort;\n}) {\n const { projectId, db } = params;\n\n const existing = await db.query(\n `SELECT COUNT(*) as count FROM psa_user_context WHERE \"projectId\" = $1`,\n [projectId]\n );\n if ((existing.rows[0]?.count as number) > 0) return;\n\n await db.execute(\n `INSERT INTO psa_user_context (\"projectId\", locale, jurisdiction, \"allowedScope\")\n VALUES ($1, $2, $3, $4)`,\n [projectId, 'en-GB', 'EU', 'education_only']\n );\n}\n"],"mappings":";AAEA,eAAsB,6BAA6B,QAGhD;CACD,MAAM,EAAE,WAAW,OAAO;AAM1B,MAJiB,MAAM,GAAG,MACxB,yEACA,CAAC,UAAU,CACZ,EACa,KAAK,IAAI,QAAmB,EAAG;AAE7C,OAAM,GAAG,QACP;+BAEA;EAAC;EAAW;EAAS;EAAM;EAAiB,CAC7C"}
@@ -0,0 +1,7 @@
1
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+
3
+ //#region src/ui/PolicySafeKnowledgeAssistantDashboard.d.ts
4
+ declare function PolicySafeKnowledgeAssistantDashboard(): react_jsx_runtime0.JSX.Element;
5
+ //#endregion
6
+ export { PolicySafeKnowledgeAssistantDashboard };
7
+ //# sourceMappingURL=PolicySafeKnowledgeAssistantDashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PolicySafeKnowledgeAssistantDashboard.d.ts","names":[],"sources":["../../src/ui/PolicySafeKnowledgeAssistantDashboard.tsx"],"sourcesContent":[],"mappings":";;;iBAyBgB,qCAAA,CAAA,GAAqC,kBAAA,CAAA,GAAA,CAAA"}