@contractspec/example.locale-jurisdiction-gate 3.7.6 → 3.7.7
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.log +3 -3
- package/AGENTS.md +50 -27
- package/README.md +57 -24
- package/dist/browser/entities/index.js +2 -2
- package/dist/browser/entities/models.js +2 -2
- package/dist/browser/events.js +1 -1
- package/dist/browser/index.js +42 -41
- package/dist/browser/operations/assistant.js +3 -3
- package/dist/browser/operations/index.js +3 -3
- package/dist/entities/index.js +2 -2
- package/dist/entities/models.js +2 -2
- package/dist/events.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +42 -41
- package/dist/node/entities/index.js +2 -2
- package/dist/node/entities/models.js +2 -2
- package/dist/node/events.js +1 -1
- package/dist/node/index.js +42 -41
- package/dist/node/operations/assistant.js +3 -3
- package/dist/node/operations/index.js +3 -3
- package/dist/operations/assistant.js +3 -3
- package/dist/operations/index.js +3 -3
- package/dist/policy/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/docs/locale-jurisdiction-gate.docblock.ts +21 -21
- package/src/entities/models.ts +87 -87
- package/src/events.ts +55 -55
- package/src/example.ts +28 -28
- package/src/handlers/demo.handlers.test.ts +46 -46
- package/src/handlers/demo.handlers.ts +133 -133
- package/src/index.ts +3 -3
- package/src/locale-jurisdiction-gate.feature.ts +34 -34
- package/src/operations/assistant.ts +82 -82
- package/src/policy/guard.test.ts +18 -18
- package/src/policy/guard.ts +75 -75
- package/src/policy/index.ts +1 -1
- package/src/policy/types.ts +12 -12
- package/tsconfig.json +7 -15
- package/tsdown.config.js +7 -13
package/src/example.ts
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import { defineExample } from '@contractspec/lib.contracts-spec';
|
|
2
2
|
|
|
3
3
|
const example = defineExample({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
32
|
});
|
|
33
33
|
|
|
34
34
|
export default example;
|
|
@@ -3,52 +3,52 @@ import { describe, expect, it } from 'bun:test';
|
|
|
3
3
|
import { createDemoAssistantHandlers } from './demo.handlers';
|
|
4
4
|
|
|
5
5
|
describe('@contractspec/example.locale-jurisdiction-gate demo handlers', () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
54
|
});
|
|
@@ -1,51 +1,51 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
enforceAllowedScope,
|
|
3
|
+
enforceCitations,
|
|
4
|
+
validateEnvelope,
|
|
5
5
|
} from '../policy/guard';
|
|
6
6
|
|
|
7
7
|
type AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';
|
|
8
8
|
|
|
9
9
|
interface AssistantAnswerIR {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
locale: string;
|
|
11
|
+
jurisdiction: string;
|
|
12
|
+
allowedScope: AllowedScope;
|
|
13
|
+
sections: { heading: string; body: string }[];
|
|
14
|
+
citations: {
|
|
15
|
+
kbSnapshotId: string;
|
|
16
|
+
sourceType: string;
|
|
17
|
+
sourceId: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
excerpt?: string;
|
|
20
|
+
}[];
|
|
21
|
+
disclaimers?: string[];
|
|
22
|
+
riskFlags?: string[];
|
|
23
|
+
refused?: boolean;
|
|
24
|
+
refusalReason?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface DemoAssistantHandlers {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
answer(input: {
|
|
29
|
+
envelope: {
|
|
30
|
+
traceId: string;
|
|
31
|
+
locale: string;
|
|
32
|
+
kbSnapshotId: string;
|
|
33
|
+
allowedScope: AllowedScope;
|
|
34
|
+
regulatoryContext: { jurisdiction: string };
|
|
35
|
+
};
|
|
36
|
+
question: string;
|
|
37
|
+
}): Promise<AssistantAnswerIR>;
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
explainConcept(input: {
|
|
40
|
+
envelope: {
|
|
41
|
+
traceId: string;
|
|
42
|
+
locale: string;
|
|
43
|
+
kbSnapshotId: string;
|
|
44
|
+
allowedScope: AllowedScope;
|
|
45
|
+
regulatoryContext: { jurisdiction: string };
|
|
46
|
+
};
|
|
47
|
+
conceptKey: string;
|
|
48
|
+
}): Promise<AssistantAnswerIR>;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
@@ -56,105 +56,105 @@ export interface DemoAssistantHandlers {
|
|
|
56
56
|
* - Enforces allowedScope (education_only blocks actionable language)
|
|
57
57
|
*/
|
|
58
58
|
export function createDemoAssistantHandlers(): DemoAssistantHandlers {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
59
|
+
async function answer(input: {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
envelope: DemoAssistantHandlers['answer'] extends (a: infer A) => any
|
|
62
|
+
? A extends { envelope: infer E }
|
|
63
|
+
? E
|
|
64
|
+
: never
|
|
65
|
+
: never;
|
|
66
|
+
question: string;
|
|
67
|
+
}): Promise<AssistantAnswerIR> {
|
|
68
|
+
const env = validateEnvelope(input.envelope);
|
|
69
|
+
if (!env.ok) {
|
|
70
|
+
return {
|
|
71
|
+
locale: input.envelope.locale ?? 'en-US',
|
|
72
|
+
jurisdiction:
|
|
73
|
+
input.envelope.regulatoryContext?.jurisdiction ?? 'UNKNOWN',
|
|
74
|
+
allowedScope: input.envelope.allowedScope ?? 'education_only',
|
|
75
|
+
sections: [
|
|
76
|
+
{
|
|
77
|
+
heading: 'Request blocked',
|
|
78
|
+
body: env.error.message,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
citations: [],
|
|
82
|
+
disclaimers: [
|
|
83
|
+
'This system refuses to answer without a valid envelope.',
|
|
84
|
+
],
|
|
85
|
+
riskFlags: [env.error.code],
|
|
86
|
+
refused: true,
|
|
87
|
+
refusalReason: env.error.code,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
91
|
+
const draft: AssistantAnswerIR = {
|
|
92
|
+
locale: env.value.locale,
|
|
93
|
+
jurisdiction: env.value.regulatoryContext?.jurisdiction ?? 'UNKNOWN',
|
|
94
|
+
allowedScope: env.value.allowedScope ?? 'education_only',
|
|
95
|
+
sections: [
|
|
96
|
+
{
|
|
97
|
+
heading: 'Answer (demo)',
|
|
98
|
+
body: `You asked: "${input.question}". This demo answer is derived from the KB snapshot only.`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
citations: [
|
|
102
|
+
{
|
|
103
|
+
kbSnapshotId: env.value.kbSnapshotId ?? 'unknown',
|
|
104
|
+
sourceType: 'ruleVersion',
|
|
105
|
+
sourceId: 'rv_demo',
|
|
106
|
+
title: 'Demo rule version',
|
|
107
|
+
excerpt: 'Demo excerpt',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
disclaimers: ['Educational demo only.'],
|
|
111
|
+
riskFlags: [],
|
|
112
|
+
};
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
114
|
+
const scope = enforceAllowedScope(env.value.allowedScope, draft);
|
|
115
|
+
if (!scope.ok) {
|
|
116
|
+
return {
|
|
117
|
+
...draft,
|
|
118
|
+
sections: [
|
|
119
|
+
{ heading: 'Escalation required', body: scope.error.message },
|
|
120
|
+
],
|
|
121
|
+
citations: draft.citations,
|
|
122
|
+
refused: true,
|
|
123
|
+
refusalReason: scope.error.code,
|
|
124
|
+
riskFlags: [...(draft.riskFlags ?? []), scope.error.code],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
128
|
+
const cited = enforceCitations(draft);
|
|
129
|
+
if (!cited.ok) {
|
|
130
|
+
return {
|
|
131
|
+
...draft,
|
|
132
|
+
sections: [{ heading: 'Request blocked', body: cited.error.message }],
|
|
133
|
+
citations: [],
|
|
134
|
+
refused: true,
|
|
135
|
+
refusalReason: cited.error.code,
|
|
136
|
+
riskFlags: [...(draft.riskFlags ?? []), cited.error.code],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
return draft;
|
|
141
|
+
}
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
143
|
+
async function explainConcept(input: {
|
|
144
|
+
envelope: DemoAssistantHandlers['explainConcept'] extends (
|
|
145
|
+
a: infer A
|
|
146
|
+
) => any // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
147
|
+
? A extends { envelope: infer E }
|
|
148
|
+
? E
|
|
149
|
+
: never
|
|
150
|
+
: never;
|
|
151
|
+
conceptKey: string;
|
|
152
|
+
}): Promise<AssistantAnswerIR> {
|
|
153
|
+
return await answer({
|
|
154
|
+
envelope: input.envelope,
|
|
155
|
+
question: `Explain concept: ${input.conceptKey}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
return { answer, explainConcept };
|
|
160
160
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* allowedScope must be explicit, and answers must cite a KB snapshot.
|
|
6
6
|
*/
|
|
7
7
|
export * from './entities';
|
|
8
|
-
export * from './operations';
|
|
9
8
|
export * from './events';
|
|
10
|
-
export
|
|
9
|
+
export { default as example } from './example';
|
|
11
10
|
export * from './handlers';
|
|
12
11
|
export * from './locale-jurisdiction-gate.feature';
|
|
13
|
-
export
|
|
12
|
+
export * from './operations';
|
|
13
|
+
export * from './policy';
|
|
14
14
|
|
|
15
15
|
import './docs';
|
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
import { defineFeature } from '@contractspec/lib.contracts-spec';
|
|
2
2
|
|
|
3
3
|
export const LocaleJurisdictionGateFeature = defineFeature({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 requiring locale/jurisdiction/snapshot/scope and citations.',
|
|
10
|
+
domain: 'knowledge',
|
|
11
|
+
owners: ['@examples'],
|
|
12
|
+
tags: ['assistant', 'policy', 'locale', 'jurisdiction', 'knowledge'],
|
|
13
|
+
stability: 'experimental',
|
|
14
|
+
},
|
|
15
|
+
operations: [
|
|
16
|
+
{ key: 'assistant.answer', version: '1.0.0' },
|
|
17
|
+
{ key: 'assistant.explainConcept', version: '1.0.0' },
|
|
18
|
+
],
|
|
19
|
+
events: [
|
|
20
|
+
{ key: 'assistant.answer.requested', version: '1.0.0' },
|
|
21
|
+
{ key: 'assistant.answer.blocked', version: '1.0.0' },
|
|
22
|
+
{ key: 'assistant.answer.delivered', version: '1.0.0' },
|
|
23
|
+
],
|
|
24
|
+
presentations: [],
|
|
25
|
+
opToPresentation: [],
|
|
26
|
+
presentationsTargets: [],
|
|
27
|
+
capabilities: {
|
|
28
|
+
requires: [{ key: 'knowledge', version: '1.0.0' }],
|
|
29
|
+
},
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
policies: [{ key: 'locale-jurisdiction-gate.policy.gate', version: '1.0.0' }],
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
knowledge: [
|
|
34
|
+
{ key: 'locale-jurisdiction-gate.knowledge.rules', version: '1.0.0' },
|
|
35
|
+
],
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
docs: [
|
|
38
|
+
'docs.examples.locale-jurisdiction-gate.goal',
|
|
39
|
+
'docs.examples.locale-jurisdiction-gate.reference',
|
|
40
|
+
],
|
|
41
41
|
});
|