@contractspec/example.locale-jurisdiction-gate 0.0.0-canary-20260113162409
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/.turbo/turbo-build$colon$bundle.log +56 -0
- package/.turbo/turbo-build.log +57 -0
- package/CHANGELOG.md +302 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/docs/index.d.ts +1 -0
- package/dist/docs/index.js +1 -0
- package/dist/docs/locale-jurisdiction-gate.docblock.d.ts +1 -0
- package/dist/docs/locale-jurisdiction-gate.docblock.js +53 -0
- package/dist/docs/locale-jurisdiction-gate.docblock.js.map +1 -0
- package/dist/entities/index.d.ts +2 -0
- package/dist/entities/index.js +3 -0
- package/dist/entities/models.d.ts +186 -0
- package/dist/entities/models.d.ts.map +1 -0
- package/dist/entities/models.js +168 -0
- package/dist/entities/models.js.map +1 -0
- package/dist/events.d.ts +69 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +123 -0
- package/dist/events.js.map +1 -0
- package/dist/example.d.ts +7 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +47 -0
- package/dist/example.js.map +1 -0
- package/dist/handlers/demo.handlers.d.ts +59 -0
- package/dist/handlers/demo.handlers.d.ts.map +1 -0
- package/dist/handlers/demo.handlers.js +86 -0
- package/dist/handlers/demo.handlers.js.map +1 -0
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/locale-jurisdiction-gate.feature.d.ts +7 -0
- package/dist/locale-jurisdiction-gate.feature.d.ts.map +1 -0
- package/dist/locale-jurisdiction-gate.feature.js +53 -0
- package/dist/locale-jurisdiction-gate.feature.js.map +1 -0
- package/dist/operations/assistant.d.ts +245 -0
- package/dist/operations/assistant.d.ts.map +1 -0
- package/dist/operations/assistant.js +115 -0
- package/dist/operations/assistant.js.map +1 -0
- package/dist/operations/index.d.ts +2 -0
- package/dist/operations/index.js +3 -0
- package/dist/policy/guard.d.ts +27 -0
- package/dist/policy/guard.d.ts.map +1 -0
- package/dist/policy/guard.js +73 -0
- package/dist/policy/guard.js.map +1 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +3 -0
- package/dist/policy/types.d.ts +16 -0
- package/dist/policy/types.d.ts.map +1 -0
- package/dist/policy/types.js +0 -0
- package/example.ts +1 -0
- package/package.json +77 -0
- package/src/docs/index.ts +1 -0
- package/src/docs/locale-jurisdiction-gate.docblock.ts +46 -0
- package/src/entities/index.ts +1 -0
- package/src/entities/models.ts +110 -0
- package/src/events.ts +74 -0
- package/src/example.ts +34 -0
- package/src/handlers/demo.handlers.test.ts +54 -0
- package/src/handlers/demo.handlers.ts +160 -0
- package/src/handlers/index.ts +1 -0
- package/src/index.ts +15 -0
- package/src/locale-jurisdiction-gate.feature.ts +30 -0
- package/src/operations/assistant.ts +98 -0
- package/src/operations/index.ts +1 -0
- package/src/policy/guard.test.ts +25 -0
- package/src/policy/guard.ts +102 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/types.ts +18 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.js +17 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { GateResult } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/policy/guard.d.ts
|
|
4
|
+
interface EnvelopeLike {
|
|
5
|
+
locale?: string;
|
|
6
|
+
kbSnapshotId?: string;
|
|
7
|
+
allowedScope?: 'education_only' | 'generic_info' | 'escalation_required';
|
|
8
|
+
regulatoryContext?: {
|
|
9
|
+
jurisdiction?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
interface AnswerLike {
|
|
13
|
+
citations?: unknown[];
|
|
14
|
+
sections?: {
|
|
15
|
+
heading: string;
|
|
16
|
+
body: string;
|
|
17
|
+
}[];
|
|
18
|
+
refused?: boolean;
|
|
19
|
+
refusalReason?: string;
|
|
20
|
+
allowedScope?: 'education_only' | 'generic_info' | 'escalation_required';
|
|
21
|
+
}
|
|
22
|
+
declare function validateEnvelope(envelope: EnvelopeLike): GateResult<Required<EnvelopeLike>>;
|
|
23
|
+
declare function enforceCitations(answer: AnswerLike): GateResult<AnswerLike>;
|
|
24
|
+
declare function enforceAllowedScope(allowedScope: EnvelopeLike['allowedScope'], answer: AnswerLike): GateResult<AnswerLike>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { enforceAllowedScope, enforceCitations, validateEnvelope };
|
|
27
|
+
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","names":[],"sources":["../../src/policy/guard.ts"],"sourcesContent":[],"mappings":";;;UAEU,YAAA;;EAAA,YAAA,CAAA,EAAY,MAAA;EAOZ,YAAA,CAAU,EAAA,gBAAA,GAAA,cAAA,GAAA,qBAAA;EAcJ,iBAAA,CAAA,EAAgB;IACpB,YAAA,CAAA,EAAA,MAAA;EACW,CAAA;;UAhBb,UAAA,CAgBP;EAAU,SAAA,CAAA,EAAA,OAAA,EAAA;EA4BG,QAAA,CAAA,EAAA;IAAyB,OAAA,EAAA,MAAA;IAAwB,IAAA,EAAA,MAAA;EAAX,CAAA,EAAA;EAAU,OAAA,CAAA,EAAA,OAAA;EAoBhD,aAAA,CAAA,EAAA,MAAmB;EACnB,YAAA,CAAA,EAAA,gBAAA,GAAA,cAAA,GAAA,qBAAA;;AAEF,iBArDE,gBAAA,CAqDF,QAAA,EApDF,YAoDE,CAAA,EAnDX,UAmDW,CAnDA,QAmDA,CAnDS,YAmDT,CAAA,CAAA;AAAX,iBAvBa,gBAAA,CAuBb,MAAA,EAvBsC,UAuBtC,CAAA,EAvBmD,UAuBnD,CAvB8D,UAuB9D,CAAA;AAAU,iBAHG,mBAAA,CAGH,YAAA,EAFG,YAEH,CAAA,cAAA,CAAA,EAAA,MAAA,EADH,UACG,CAAA,EAAV,UAAU,CAAC,UAAD,CAAA"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/policy/guard.ts
|
|
2
|
+
const SUPPORTED_LOCALES = new Set([
|
|
3
|
+
"en-US",
|
|
4
|
+
"en-GB",
|
|
5
|
+
"fr-FR"
|
|
6
|
+
]);
|
|
7
|
+
function err(code, message) {
|
|
8
|
+
return {
|
|
9
|
+
code,
|
|
10
|
+
message
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function validateEnvelope(envelope) {
|
|
14
|
+
if (!envelope.locale || !SUPPORTED_LOCALES.has(envelope.locale)) return {
|
|
15
|
+
ok: false,
|
|
16
|
+
error: err("LOCALE_REQUIRED", "locale is required and must be supported")
|
|
17
|
+
};
|
|
18
|
+
if (!envelope.regulatoryContext?.jurisdiction) return {
|
|
19
|
+
ok: false,
|
|
20
|
+
error: err("JURISDICTION_REQUIRED", "jurisdiction is required")
|
|
21
|
+
};
|
|
22
|
+
if (!envelope.kbSnapshotId) return {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: err("KB_SNAPSHOT_REQUIRED", "kbSnapshotId is required")
|
|
25
|
+
};
|
|
26
|
+
if (!envelope.allowedScope) return {
|
|
27
|
+
ok: false,
|
|
28
|
+
error: err("SCOPE_VIOLATION", "allowedScope is required")
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
value: envelope
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function enforceCitations(answer) {
|
|
36
|
+
const citations = answer.citations ?? [];
|
|
37
|
+
if (!Array.isArray(citations) || citations.length === 0) return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: err("CITATIONS_REQUIRED", "answers must include at least one citation")
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
value: answer
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const EDUCATION_ONLY_FORBIDDEN_PATTERNS = [
|
|
47
|
+
/\b(buy|sell)\b/i,
|
|
48
|
+
/\b(should\s+buy|should\s+sell)\b/i,
|
|
49
|
+
/\b(guarantee(d)?|promise(d)?)\b/i
|
|
50
|
+
];
|
|
51
|
+
function enforceAllowedScope(allowedScope, answer) {
|
|
52
|
+
if (!allowedScope) return {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: err("SCOPE_VIOLATION", "allowedScope is required")
|
|
55
|
+
};
|
|
56
|
+
if (allowedScope !== "education_only") return {
|
|
57
|
+
ok: true,
|
|
58
|
+
value: answer
|
|
59
|
+
};
|
|
60
|
+
const bodies = (answer.sections ?? []).map((s) => s.body).join("\n");
|
|
61
|
+
if (EDUCATION_ONLY_FORBIDDEN_PATTERNS.some((re) => re.test(bodies))) return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: err("SCOPE_VIOLATION", "answer violates education_only scope (contains actionable or promotional language)")
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
value: answer
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { enforceAllowedScope, enforceCitations, validateEnvelope };
|
|
73
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","names":[],"sources":["../../src/policy/guard.ts"],"sourcesContent":["import type { GateError, GateResult } from './types';\n\ninterface EnvelopeLike {\n locale?: string;\n kbSnapshotId?: string;\n allowedScope?: 'education_only' | 'generic_info' | 'escalation_required';\n regulatoryContext?: { jurisdiction?: string };\n}\n\ninterface AnswerLike {\n citations?: unknown[];\n sections?: { heading: string; body: string }[];\n refused?: boolean;\n refusalReason?: string;\n allowedScope?: 'education_only' | 'generic_info' | 'escalation_required';\n}\n\nconst SUPPORTED_LOCALES = new Set<string>(['en-US', 'en-GB', 'fr-FR']);\n\nfunction err(code: GateError['code'], message: string): GateError {\n return { code, message };\n}\n\nexport function validateEnvelope(\n envelope: EnvelopeLike\n): GateResult<Required<EnvelopeLike>> {\n if (!envelope.locale || !SUPPORTED_LOCALES.has(envelope.locale)) {\n return {\n ok: false,\n error: err('LOCALE_REQUIRED', 'locale is required and must be supported'),\n };\n }\n if (!envelope.regulatoryContext?.jurisdiction) {\n return {\n ok: false,\n error: err('JURISDICTION_REQUIRED', 'jurisdiction is required'),\n };\n }\n if (!envelope.kbSnapshotId) {\n return {\n ok: false,\n error: err('KB_SNAPSHOT_REQUIRED', 'kbSnapshotId is required'),\n };\n }\n if (!envelope.allowedScope) {\n return {\n ok: false,\n error: err('SCOPE_VIOLATION', 'allowedScope is required'),\n };\n }\n return { ok: true, value: envelope as Required<EnvelopeLike> };\n}\n\nexport function enforceCitations(answer: AnswerLike): GateResult<AnswerLike> {\n const citations = answer.citations ?? [];\n if (!Array.isArray(citations) || citations.length === 0) {\n return {\n ok: false,\n error: err(\n 'CITATIONS_REQUIRED',\n 'answers must include at least one citation'\n ),\n };\n }\n return { ok: true, value: answer };\n}\n\nconst EDUCATION_ONLY_FORBIDDEN_PATTERNS: RegExp[] = [\n /\\b(buy|sell)\\b/i,\n /\\b(should\\s+buy|should\\s+sell)\\b/i,\n /\\b(guarantee(d)?|promise(d)?)\\b/i,\n];\n\nexport function enforceAllowedScope(\n allowedScope: EnvelopeLike['allowedScope'],\n answer: AnswerLike\n): GateResult<AnswerLike> {\n if (!allowedScope) {\n return {\n ok: false,\n error: err('SCOPE_VIOLATION', 'allowedScope is required'),\n };\n }\n if (allowedScope !== 'education_only') {\n return { ok: true, value: answer };\n }\n\n const bodies = (answer.sections ?? []).map((s) => s.body).join('\\n');\n const violations = EDUCATION_ONLY_FORBIDDEN_PATTERNS.some((re) =>\n re.test(bodies)\n );\n if (violations) {\n return {\n ok: false,\n error: err(\n 'SCOPE_VIOLATION',\n 'answer violates education_only scope (contains actionable or promotional language)'\n ),\n };\n }\n return { ok: true, value: answer };\n}\n"],"mappings":";AAiBA,MAAM,oBAAoB,IAAI,IAAY;CAAC;CAAS;CAAS;CAAQ,CAAC;AAEtE,SAAS,IAAI,MAAyB,SAA4B;AAChE,QAAO;EAAE;EAAM;EAAS;;AAG1B,SAAgB,iBACd,UACoC;AACpC,KAAI,CAAC,SAAS,UAAU,CAAC,kBAAkB,IAAI,SAAS,OAAO,CAC7D,QAAO;EACL,IAAI;EACJ,OAAO,IAAI,mBAAmB,2CAA2C;EAC1E;AAEH,KAAI,CAAC,SAAS,mBAAmB,aAC/B,QAAO;EACL,IAAI;EACJ,OAAO,IAAI,yBAAyB,2BAA2B;EAChE;AAEH,KAAI,CAAC,SAAS,aACZ,QAAO;EACL,IAAI;EACJ,OAAO,IAAI,wBAAwB,2BAA2B;EAC/D;AAEH,KAAI,CAAC,SAAS,aACZ,QAAO;EACL,IAAI;EACJ,OAAO,IAAI,mBAAmB,2BAA2B;EAC1D;AAEH,QAAO;EAAE,IAAI;EAAM,OAAO;EAAoC;;AAGhE,SAAgB,iBAAiB,QAA4C;CAC3E,MAAM,YAAY,OAAO,aAAa,EAAE;AACxC,KAAI,CAAC,MAAM,QAAQ,UAAU,IAAI,UAAU,WAAW,EACpD,QAAO;EACL,IAAI;EACJ,OAAO,IACL,sBACA,6CACD;EACF;AAEH,QAAO;EAAE,IAAI;EAAM,OAAO;EAAQ;;AAGpC,MAAM,oCAA8C;CAClD;CACA;CACA;CACD;AAED,SAAgB,oBACd,cACA,QACwB;AACxB,KAAI,CAAC,aACH,QAAO;EACL,IAAI;EACJ,OAAO,IAAI,mBAAmB,2BAA2B;EAC1D;AAEH,KAAI,iBAAiB,iBACnB,QAAO;EAAE,IAAI;EAAM,OAAO;EAAQ;CAGpC,MAAM,UAAU,OAAO,YAAY,EAAE,EAAE,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;AAIpE,KAHmB,kCAAkC,MAAM,OACzD,GAAG,KAAK,OAAO,CAChB,CAEC,QAAO;EACL,IAAI;EACJ,OAAO,IACL,mBACA,qFACD;EACF;AAEH,QAAO;EAAE,IAAI;EAAM,OAAO;EAAQ"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/policy/types.d.ts
|
|
2
|
+
type AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';
|
|
3
|
+
interface GateError {
|
|
4
|
+
code: 'LOCALE_REQUIRED' | 'JURISDICTION_REQUIRED' | 'KB_SNAPSHOT_REQUIRED' | 'CITATIONS_REQUIRED' | 'SCOPE_VIOLATION';
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
type GateResult<T> = {
|
|
8
|
+
ok: true;
|
|
9
|
+
value: T;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: GateError;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { AllowedScope, GateError, GateResult };
|
|
16
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/policy/types.ts"],"sourcesContent":[],"mappings":";KAAY,YAAA;AAAA,UAKK,SAAA,CALO;EAKP,IAAA,EAAA,iBAAS,GAAA,uBAAA,GAAA,sBAAA,GAAA,oBAAA,GAAA,iBAAA;EAUd,OAAA,EAAA,MAAU;;KAAV;;SACW;;;SACC"}
|
|
File without changes
|
package/example.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './src/example';
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contractspec/example.locale-jurisdiction-gate",
|
|
3
|
+
"version": "0.0.0-canary-20260113162409",
|
|
4
|
+
"description": "Example: enforce locale + jurisdiction + kbSnapshotId + allowed scope for assistant calls (fail-closed).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./docs": "./dist/docs/index.js",
|
|
10
|
+
"./docs/locale-jurisdiction-gate.docblock": "./dist/docs/locale-jurisdiction-gate.docblock.js",
|
|
11
|
+
"./entities": "./dist/entities/index.js",
|
|
12
|
+
"./entities/models": "./dist/entities/models.js",
|
|
13
|
+
"./events": "./dist/events.js",
|
|
14
|
+
"./example": "./dist/example.js",
|
|
15
|
+
"./handlers": "./dist/handlers/index.js",
|
|
16
|
+
"./handlers/demo.handlers": "./dist/handlers/demo.handlers.js",
|
|
17
|
+
"./locale-jurisdiction-gate.feature": "./dist/locale-jurisdiction-gate.feature.js",
|
|
18
|
+
"./operations": "./dist/operations/index.js",
|
|
19
|
+
"./operations/assistant": "./dist/operations/assistant.js",
|
|
20
|
+
"./policy": "./dist/policy/index.js",
|
|
21
|
+
"./policy/guard": "./dist/policy/guard.js",
|
|
22
|
+
"./policy/types": "./dist/policy/types.js",
|
|
23
|
+
"./*": "./*"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
27
|
+
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
28
|
+
"build": "bun build:types && bun build:bundle",
|
|
29
|
+
"build:bundle": "tsdown",
|
|
30
|
+
"build:types": "tsc --noEmit",
|
|
31
|
+
"dev": "bun build:bundle --watch",
|
|
32
|
+
"clean": "rimraf dist .turbo",
|
|
33
|
+
"lint": "bun lint:fix",
|
|
34
|
+
"lint:fix": "eslint src --fix",
|
|
35
|
+
"lint:check": "eslint src",
|
|
36
|
+
"test": "bun test"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@contractspec/lib.contracts": "0.0.0-canary-20260113162409",
|
|
40
|
+
"@contractspec/lib.schema": "0.0.0-canary-20260113162409"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@contractspec/tool.tsdown": "0.0.0-canary-20260113162409",
|
|
44
|
+
"@contractspec/tool.typescript": "0.0.0-canary-20260113162409",
|
|
45
|
+
"tsdown": "^0.19.0",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"exports": {
|
|
51
|
+
".": "./dist/index.js",
|
|
52
|
+
"./contracts": "./dist/contracts/index.js",
|
|
53
|
+
"./contracts/assistant": "./dist/contracts/assistant.js",
|
|
54
|
+
"./docs": "./dist/docs/index.js",
|
|
55
|
+
"./docs/locale-jurisdiction-gate.docblock": "./dist/docs/locale-jurisdiction-gate.docblock.js",
|
|
56
|
+
"./entities": "./dist/entities/index.js",
|
|
57
|
+
"./entities/models": "./dist/entities/models.js",
|
|
58
|
+
"./events": "./dist/events.js",
|
|
59
|
+
"./example": "./dist/example.js",
|
|
60
|
+
"./handlers": "./dist/handlers/index.js",
|
|
61
|
+
"./handlers/demo.handlers": "./dist/handlers/demo.handlers.js",
|
|
62
|
+
"./locale-jurisdiction-gate.feature": "./dist/locale-jurisdiction-gate.feature.js",
|
|
63
|
+
"./policy": "./dist/policy/index.js",
|
|
64
|
+
"./policy/guard": "./dist/policy/guard.js",
|
|
65
|
+
"./policy/types": "./dist/policy/types.js",
|
|
66
|
+
"./*": "./*"
|
|
67
|
+
},
|
|
68
|
+
"registry": "https://registry.npmjs.org/"
|
|
69
|
+
},
|
|
70
|
+
"license": "MIT",
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "https://github.com/lssm-tech/contractspec.git",
|
|
74
|
+
"directory": "packages/examples/locale-jurisdiction-gate"
|
|
75
|
+
},
|
|
76
|
+
"homepage": "https://contractspec.io"
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './locale-jurisdiction-gate.docblock';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DocBlock } from '@contractspec/lib.contracts/docs';
|
|
2
|
+
import { registerDocBlocks } from '@contractspec/lib.contracts/docs';
|
|
3
|
+
|
|
4
|
+
const docBlocks: DocBlock[] = [
|
|
5
|
+
{
|
|
6
|
+
id: 'docs.examples.locale-jurisdiction-gate.goal',
|
|
7
|
+
title: 'Locale/Jurisdiction Gate — Goal',
|
|
8
|
+
summary:
|
|
9
|
+
'Fail-closed gate that forces locale + jurisdiction + kbSnapshotId + allowedScope for assistant calls.',
|
|
10
|
+
kind: 'goal',
|
|
11
|
+
visibility: 'public',
|
|
12
|
+
route: '/docs/examples/locale-jurisdiction-gate/goal',
|
|
13
|
+
tags: ['assistant', 'policy', 'locale', 'jurisdiction', 'knowledge'],
|
|
14
|
+
body: `## Why it matters
|
|
15
|
+
- Forces all assistant behavior to be bound to explicit inputs (no guessing).
|
|
16
|
+
- Requires KB snapshot citations to make answers traceable and regenerable.
|
|
17
|
+
|
|
18
|
+
## Guardrails
|
|
19
|
+
- Missing locale/jurisdiction/snapshot/scope => refuse (structured).
|
|
20
|
+
- Missing citations => refuse.
|
|
21
|
+
- Scope violations under education_only => refuse/escalate.`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'docs.examples.locale-jurisdiction-gate.reference',
|
|
25
|
+
title: 'Locale/Jurisdiction Gate — Reference',
|
|
26
|
+
summary: 'Contracts, models, and events exposed by the gate example.',
|
|
27
|
+
kind: 'reference',
|
|
28
|
+
visibility: 'public',
|
|
29
|
+
route: '/docs/examples/locale-jurisdiction-gate',
|
|
30
|
+
tags: ['assistant', 'policy', 'reference'],
|
|
31
|
+
body: `## Contracts
|
|
32
|
+
- assistant.answer (v1)
|
|
33
|
+
- assistant.explainConcept (v1)
|
|
34
|
+
|
|
35
|
+
## Models
|
|
36
|
+
- LLMCallEnvelope (locale, regulatoryContext, kbSnapshotId, allowedScope, traceId)
|
|
37
|
+
- AssistantAnswerIR (sections, citations, disclaimers, riskFlags)
|
|
38
|
+
|
|
39
|
+
## Events
|
|
40
|
+
- assistant.answer.requested
|
|
41
|
+
- assistant.answer.blocked
|
|
42
|
+
- assistant.answer.delivered`,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
registerDocBlocks(docBlocks);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './models';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ScalarTypeEnum,
|
|
3
|
+
defineEnum,
|
|
4
|
+
defineSchemaModel,
|
|
5
|
+
} from '@contractspec/lib.schema';
|
|
6
|
+
|
|
7
|
+
export const AllowedScopeEnum = defineEnum('AllowedScope', [
|
|
8
|
+
'education_only',
|
|
9
|
+
'generic_info',
|
|
10
|
+
'escalation_required',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export const UserProfileModel = defineSchemaModel({
|
|
14
|
+
name: 'UserProfile',
|
|
15
|
+
description: 'User profile inputs used to derive regulatory context.',
|
|
16
|
+
fields: {
|
|
17
|
+
preferredLocale: {
|
|
18
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
19
|
+
isOptional: true,
|
|
20
|
+
},
|
|
21
|
+
residencyCountry: {
|
|
22
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
23
|
+
isOptional: true,
|
|
24
|
+
},
|
|
25
|
+
taxResidenceCountry: {
|
|
26
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
27
|
+
isOptional: true,
|
|
28
|
+
},
|
|
29
|
+
clientType: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const RegulatoryContextModel = defineSchemaModel({
|
|
34
|
+
name: 'RegulatoryContext',
|
|
35
|
+
description: 'Explicit regulatory context (no guessing).',
|
|
36
|
+
fields: {
|
|
37
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
38
|
+
region: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
39
|
+
clientType: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
40
|
+
allowedScope: { type: AllowedScopeEnum, isOptional: false },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const LLMCallEnvelopeModel = defineSchemaModel({
|
|
45
|
+
name: 'LLMCallEnvelope',
|
|
46
|
+
description:
|
|
47
|
+
'Mandatory envelope for assistant calls. All fields are explicit and required for policy gating.',
|
|
48
|
+
fields: {
|
|
49
|
+
traceId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
50
|
+
locale: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
51
|
+
regulatoryContext: { type: RegulatoryContextModel, isOptional: false },
|
|
52
|
+
kbSnapshotId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
53
|
+
allowedScope: { type: AllowedScopeEnum, isOptional: false },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const AssistantCitationModel = defineSchemaModel({
|
|
58
|
+
name: 'AssistantCitation',
|
|
59
|
+
description:
|
|
60
|
+
'Citation referencing a KB snapshot + a specific item within it.',
|
|
61
|
+
fields: {
|
|
62
|
+
kbSnapshotId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
63
|
+
sourceType: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
64
|
+
sourceId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
65
|
+
title: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
66
|
+
excerpt: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const AssistantAnswerSectionModel = defineSchemaModel({
|
|
71
|
+
name: 'AssistantAnswerSection',
|
|
72
|
+
description: 'Structured answer section.',
|
|
73
|
+
fields: {
|
|
74
|
+
heading: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
75
|
+
body: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const AssistantAnswerIRModel = defineSchemaModel({
|
|
80
|
+
name: 'AssistantAnswerIR',
|
|
81
|
+
description:
|
|
82
|
+
'Structured assistant answer with mandatory citations and explicit locale/jurisdiction.',
|
|
83
|
+
fields: {
|
|
84
|
+
locale: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
85
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
86
|
+
allowedScope: { type: AllowedScopeEnum, isOptional: false },
|
|
87
|
+
sections: {
|
|
88
|
+
type: AssistantAnswerSectionModel,
|
|
89
|
+
isArray: true,
|
|
90
|
+
isOptional: false,
|
|
91
|
+
},
|
|
92
|
+
citations: {
|
|
93
|
+
type: AssistantCitationModel,
|
|
94
|
+
isArray: true,
|
|
95
|
+
isOptional: false,
|
|
96
|
+
},
|
|
97
|
+
disclaimers: {
|
|
98
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
99
|
+
isArray: true,
|
|
100
|
+
isOptional: true,
|
|
101
|
+
},
|
|
102
|
+
riskFlags: {
|
|
103
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
104
|
+
isArray: true,
|
|
105
|
+
isOptional: true,
|
|
106
|
+
},
|
|
107
|
+
refused: { type: ScalarTypeEnum.Boolean(), isOptional: true },
|
|
108
|
+
refusalReason: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
109
|
+
},
|
|
110
|
+
});
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineEvent, defineSchemaModel } from '@contractspec/lib.contracts';
|
|
2
|
+
import { ScalarTypeEnum } from '@contractspec/lib.schema';
|
|
3
|
+
|
|
4
|
+
const AssistantAnswerRequestedPayload = defineSchemaModel({
|
|
5
|
+
name: 'AssistantAnswerRequestedPayload',
|
|
6
|
+
description: 'Emitted when an assistant answer is requested (pre-gate).',
|
|
7
|
+
fields: {
|
|
8
|
+
traceId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
9
|
+
locale: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
10
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
11
|
+
kbSnapshotId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
12
|
+
allowedScope: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const AssistantAnswerRequestedEvent = defineEvent({
|
|
17
|
+
meta: {
|
|
18
|
+
key: 'assistant.answer.requested',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
description: 'Assistant answer requested (policy gate will run).',
|
|
21
|
+
stability: 'experimental',
|
|
22
|
+
owners: ['@examples'],
|
|
23
|
+
tags: ['assistant', 'policy'],
|
|
24
|
+
},
|
|
25
|
+
payload: AssistantAnswerRequestedPayload,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const AssistantAnswerBlockedPayload = defineSchemaModel({
|
|
29
|
+
name: 'AssistantAnswerBlockedPayload',
|
|
30
|
+
description: 'Emitted when a request is blocked by the gate.',
|
|
31
|
+
fields: {
|
|
32
|
+
traceId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
33
|
+
reasonCode: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
34
|
+
reason: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const AssistantAnswerBlockedEvent = defineEvent({
|
|
39
|
+
meta: {
|
|
40
|
+
key: 'assistant.answer.blocked',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
description: 'Assistant answer blocked (fail-closed).',
|
|
43
|
+
stability: 'experimental',
|
|
44
|
+
owners: ['@examples'],
|
|
45
|
+
tags: ['assistant', 'policy', 'blocked'],
|
|
46
|
+
},
|
|
47
|
+
payload: AssistantAnswerBlockedPayload,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const AssistantAnswerDeliveredPayload = defineSchemaModel({
|
|
51
|
+
name: 'AssistantAnswerDeliveredPayload',
|
|
52
|
+
description: 'Emitted when a structured, cited answer is delivered.',
|
|
53
|
+
fields: {
|
|
54
|
+
traceId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
55
|
+
locale: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
56
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
57
|
+
kbSnapshotId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
58
|
+
allowedScope: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
59
|
+
citationsCount: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const AssistantAnswerDeliveredEvent = defineEvent({
|
|
64
|
+
meta: {
|
|
65
|
+
key: 'assistant.answer.delivered',
|
|
66
|
+
version: '1.0.0',
|
|
67
|
+
description:
|
|
68
|
+
'Assistant answer delivered (must include KB snapshot citations).',
|
|
69
|
+
stability: 'experimental',
|
|
70
|
+
owners: ['@examples'],
|
|
71
|
+
tags: ['assistant', 'policy', 'delivered'],
|
|
72
|
+
},
|
|
73
|
+
payload: AssistantAnswerDeliveredPayload,
|
|
74
|
+
});
|
package/src/example.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineExample } from '@contractspec/lib.contracts';
|
|
2
|
+
|
|
3
|
+
const example = defineExample({
|
|
4
|
+
meta: {
|
|
5
|
+
key: 'locale-jurisdiction-gate',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
title: 'Locale / Jurisdiction Gate',
|
|
8
|
+
description:
|
|
9
|
+
'Fail-closed gating for assistant calls: locale + jurisdiction + kbSnapshotId + allowedScope must be explicit, answers must cite a snapshot.',
|
|
10
|
+
kind: 'knowledge',
|
|
11
|
+
visibility: 'public',
|
|
12
|
+
stability: 'experimental',
|
|
13
|
+
owners: ['@platform.core'],
|
|
14
|
+
tags: ['policy', 'locale', 'jurisdiction', 'assistant', 'gating'],
|
|
15
|
+
},
|
|
16
|
+
docs: {
|
|
17
|
+
rootDocId: 'docs.examples.locale-jurisdiction-gate',
|
|
18
|
+
},
|
|
19
|
+
entrypoints: {
|
|
20
|
+
packageName: '@contractspec/example.locale-jurisdiction-gate',
|
|
21
|
+
feature: './feature',
|
|
22
|
+
contracts: './contracts',
|
|
23
|
+
handlers: './handlers',
|
|
24
|
+
docs: './docs',
|
|
25
|
+
},
|
|
26
|
+
surfaces: {
|
|
27
|
+
templates: true,
|
|
28
|
+
sandbox: { enabled: true, modes: ['markdown', 'specs'] },
|
|
29
|
+
studio: { enabled: true, installable: true },
|
|
30
|
+
mcp: { enabled: true },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default example;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { createDemoAssistantHandlers } from './demo.handlers';
|
|
4
|
+
|
|
5
|
+
describe('@contractspec/example.locale-jurisdiction-gate demo handlers', () => {
|
|
6
|
+
it('blocks when locale is missing', async () => {
|
|
7
|
+
const handlers = createDemoAssistantHandlers();
|
|
8
|
+
const result = await handlers.answer({
|
|
9
|
+
envelope: {
|
|
10
|
+
traceId: 't1',
|
|
11
|
+
locale: '',
|
|
12
|
+
kbSnapshotId: 'snap_1',
|
|
13
|
+
allowedScope: 'education_only',
|
|
14
|
+
regulatoryContext: { jurisdiction: 'EU' },
|
|
15
|
+
},
|
|
16
|
+
question: 'What is a snapshot?',
|
|
17
|
+
});
|
|
18
|
+
expect(result.refused).toBeTrue();
|
|
19
|
+
expect(result.refusalReason).toBe('LOCALE_REQUIRED');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('blocks when kbSnapshotId is missing', async () => {
|
|
23
|
+
const handlers = createDemoAssistantHandlers();
|
|
24
|
+
const result = await handlers.answer({
|
|
25
|
+
envelope: {
|
|
26
|
+
traceId: 't2',
|
|
27
|
+
locale: 'en-GB',
|
|
28
|
+
kbSnapshotId: '',
|
|
29
|
+
allowedScope: 'education_only',
|
|
30
|
+
regulatoryContext: { jurisdiction: 'EU' },
|
|
31
|
+
},
|
|
32
|
+
question: 'What is a snapshot?',
|
|
33
|
+
});
|
|
34
|
+
expect(result.refused).toBeTrue();
|
|
35
|
+
expect(result.refusalReason).toBe('KB_SNAPSHOT_REQUIRED');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('blocks education_only answers that include buy/sell language', async () => {
|
|
39
|
+
const handlers = createDemoAssistantHandlers();
|
|
40
|
+
const result = await handlers.answer({
|
|
41
|
+
envelope: {
|
|
42
|
+
traceId: 't3',
|
|
43
|
+
locale: 'en-GB',
|
|
44
|
+
kbSnapshotId: 'snap_1',
|
|
45
|
+
allowedScope: 'education_only',
|
|
46
|
+
regulatoryContext: { jurisdiction: 'EU' },
|
|
47
|
+
},
|
|
48
|
+
question: 'Should I buy now?',
|
|
49
|
+
});
|
|
50
|
+
// demo handler echoes question; question includes forbidden phrase \"buy\"
|
|
51
|
+
expect(result.refused).toBeTrue();
|
|
52
|
+
expect(result.refusalReason).toBe('SCOPE_VIOLATION');
|
|
53
|
+
});
|
|
54
|
+
});
|