@contractspec/example.locale-jurisdiction-gate 3.7.6 → 3.7.10

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 (83) hide show
  1. package/.turbo/turbo-build.log +72 -51
  2. package/AGENTS.md +50 -27
  3. package/CHANGELOG.md +21 -0
  4. package/README.md +87 -42
  5. package/dist/browser/entities/index.js +2 -2
  6. package/dist/browser/entities/models.js +2 -2
  7. package/dist/browser/events.js +1 -1
  8. package/dist/browser/forms/assistant-context.form.js +213 -0
  9. package/dist/browser/forms/index.js +213 -0
  10. package/dist/browser/index.js +362 -40
  11. package/dist/browser/locale-jurisdiction-gate.feature.js +66 -1
  12. package/dist/browser/operations/assistant.js +3 -3
  13. package/dist/browser/operations/index.js +3 -3
  14. package/dist/browser/policy/assistant-gate.policy.js +62 -0
  15. package/dist/browser/policy/index.js +62 -1
  16. package/dist/browser/translations/assistant-gate.en-GB.translation.js +48 -0
  17. package/dist/browser/translations/assistant-gate.en-US.translation.js +50 -0
  18. package/dist/browser/translations/assistant-gate.fr-FR.translation.js +52 -0
  19. package/dist/browser/translations/index.js +148 -0
  20. package/dist/contracts.test.d.ts +1 -0
  21. package/dist/entities/index.js +2 -2
  22. package/dist/entities/models.js +2 -2
  23. package/dist/events.js +1 -1
  24. package/dist/forms/assistant-context.form.d.ts +22 -0
  25. package/dist/forms/assistant-context.form.js +214 -0
  26. package/dist/forms/index.d.ts +1 -0
  27. package/dist/forms/index.js +214 -0
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.js +362 -40
  30. package/dist/locale-jurisdiction-gate.feature.js +66 -1
  31. package/dist/node/entities/index.js +2 -2
  32. package/dist/node/entities/models.js +2 -2
  33. package/dist/node/events.js +1 -1
  34. package/dist/node/forms/assistant-context.form.js +213 -0
  35. package/dist/node/forms/index.js +213 -0
  36. package/dist/node/index.js +362 -40
  37. package/dist/node/locale-jurisdiction-gate.feature.js +66 -1
  38. package/dist/node/operations/assistant.js +3 -3
  39. package/dist/node/operations/index.js +3 -3
  40. package/dist/node/policy/assistant-gate.policy.js +62 -0
  41. package/dist/node/policy/index.js +62 -1
  42. package/dist/node/translations/assistant-gate.en-GB.translation.js +48 -0
  43. package/dist/node/translations/assistant-gate.en-US.translation.js +50 -0
  44. package/dist/node/translations/assistant-gate.fr-FR.translation.js +52 -0
  45. package/dist/node/translations/index.js +148 -0
  46. package/dist/operations/assistant.js +3 -3
  47. package/dist/operations/index.js +3 -3
  48. package/dist/policy/assistant-gate.policy.d.ts +1 -0
  49. package/dist/policy/assistant-gate.policy.js +63 -0
  50. package/dist/policy/index.d.ts +2 -1
  51. package/dist/policy/index.js +62 -1
  52. package/dist/translations/assistant-gate.en-GB.translation.d.ts +1 -0
  53. package/dist/translations/assistant-gate.en-GB.translation.js +49 -0
  54. package/dist/translations/assistant-gate.en-US.translation.d.ts +1 -0
  55. package/dist/translations/assistant-gate.en-US.translation.js +51 -0
  56. package/dist/translations/assistant-gate.fr-FR.translation.d.ts +1 -0
  57. package/dist/translations/assistant-gate.fr-FR.translation.js +53 -0
  58. package/dist/translations/index.d.ts +3 -0
  59. package/dist/translations/index.js +149 -0
  60. package/package.json +105 -7
  61. package/src/contracts.test.ts +32 -0
  62. package/src/docs/locale-jurisdiction-gate.docblock.ts +21 -21
  63. package/src/entities/models.ts +87 -87
  64. package/src/events.ts +55 -55
  65. package/src/example.ts +28 -28
  66. package/src/forms/assistant-context.form.ts +112 -0
  67. package/src/forms/index.ts +1 -0
  68. package/src/handlers/demo.handlers.test.ts +46 -46
  69. package/src/handlers/demo.handlers.ts +133 -133
  70. package/src/index.ts +5 -3
  71. package/src/locale-jurisdiction-gate.feature.ts +40 -34
  72. package/src/operations/assistant.ts +82 -82
  73. package/src/policy/assistant-gate.policy.ts +65 -0
  74. package/src/policy/guard.test.ts +18 -18
  75. package/src/policy/guard.ts +75 -75
  76. package/src/policy/index.ts +2 -1
  77. package/src/policy/types.ts +12 -12
  78. package/src/translations/assistant-gate.en-GB.translation.ts +46 -0
  79. package/src/translations/assistant-gate.en-US.translation.ts +48 -0
  80. package/src/translations/assistant-gate.fr-FR.translation.ts +51 -0
  81. package/src/translations/index.ts +3 -0
  82. package/tsconfig.json +7 -15
  83. package/tsdown.config.js +7 -13
@@ -1,51 +1,51 @@
1
1
  import {
2
- enforceAllowedScope,
3
- enforceCitations,
4
- validateEnvelope,
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
- 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;
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
- 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>;
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
- 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>;
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
- 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
- }
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
- 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
- };
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
- 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
- }
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
- 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
- }
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
- return draft;
141
- }
140
+ return draft;
141
+ }
142
142
 
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
- }
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
- return { answer, explainConcept };
159
+ return { answer, explainConcept };
160
160
  }
package/src/index.ts CHANGED
@@ -5,11 +5,13 @@
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 * from './policy';
9
+ export { default as example } from './example';
10
+ export * from './forms';
11
11
  export * from './handlers';
12
12
  export * from './locale-jurisdiction-gate.feature';
13
- export { default as example } from './example';
13
+ export * from './operations';
14
+ export * from './policy';
15
+ export * from './translations';
14
16
 
15
17
  import './docs';
@@ -1,41 +1,47 @@
1
1
  import { defineFeature } from '@contractspec/lib.contracts-spec';
2
+ import { AssistantGatePolicy } from './policy/assistant-gate.policy';
2
3
 
3
4
  export const LocaleJurisdictionGateFeature = defineFeature({
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
- },
5
+ meta: {
6
+ key: 'locale-jurisdiction-gate',
7
+ version: '1.0.0',
8
+ title: 'Locale + Jurisdiction Gate',
9
+ description:
10
+ 'Fail-closed gating for assistant calls requiring locale/jurisdiction/snapshot/scope and citations.',
11
+ domain: 'knowledge',
12
+ owners: ['@examples'],
13
+ tags: ['assistant', 'policy', 'locale', 'jurisdiction', 'knowledge'],
14
+ stability: 'experimental',
15
+ },
16
+ operations: [
17
+ { key: 'assistant.answer', version: '1.0.0' },
18
+ { key: 'assistant.explainConcept', version: '1.0.0' },
19
+ ],
20
+ events: [
21
+ { key: 'assistant.answer.requested', version: '1.0.0' },
22
+ { key: 'assistant.answer.blocked', version: '1.0.0' },
23
+ { key: 'assistant.answer.delivered', version: '1.0.0' },
24
+ ],
25
+ presentations: [],
26
+ opToPresentation: [],
27
+ presentationsTargets: [],
28
+ capabilities: {
29
+ requires: [{ key: 'knowledge', version: '1.0.0' }],
30
+ },
30
31
 
31
- policies: [{ key: 'locale-jurisdiction-gate.policy.gate', version: '1.0.0' }],
32
+ policies: [
33
+ {
34
+ key: AssistantGatePolicy.meta.key,
35
+ version: AssistantGatePolicy.meta.version,
36
+ },
37
+ ],
32
38
 
33
- knowledge: [
34
- { key: 'locale-jurisdiction-gate.knowledge.rules', version: '1.0.0' },
35
- ],
39
+ knowledge: [
40
+ { key: 'locale-jurisdiction-gate.knowledge.rules', version: '1.0.0' },
41
+ ],
36
42
 
37
- docs: [
38
- 'docs.examples.locale-jurisdiction-gate.goal',
39
- 'docs.examples.locale-jurisdiction-gate.reference',
40
- ],
43
+ docs: [
44
+ 'docs.examples.locale-jurisdiction-gate.goal',
45
+ 'docs.examples.locale-jurisdiction-gate.reference',
46
+ ],
41
47
  });
@@ -1,98 +1,98 @@
1
1
  import { defineCommand } from '@contractspec/lib.contracts-spec';
2
- import { ScalarTypeEnum, defineSchemaModel } from '@contractspec/lib.schema';
2
+ import { defineSchemaModel, ScalarTypeEnum } from '@contractspec/lib.schema';
3
3
 
4
4
  import {
5
- AssistantAnswerIRModel,
6
- LLMCallEnvelopeModel,
5
+ AssistantAnswerIRModel,
6
+ LLMCallEnvelopeModel,
7
7
  } from '../entities/models';
8
8
 
9
9
  const AssistantQuestionInput = defineSchemaModel({
10
- name: 'AssistantQuestionInput',
11
- description: 'Input for assistant calls with mandatory envelope.',
12
- fields: {
13
- envelope: { type: LLMCallEnvelopeModel, isOptional: false },
14
- question: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
15
- },
10
+ name: 'AssistantQuestionInput',
11
+ description: 'Input for assistant calls with mandatory envelope.',
12
+ fields: {
13
+ envelope: { type: LLMCallEnvelopeModel, isOptional: false },
14
+ question: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
15
+ },
16
16
  });
17
17
 
18
18
  const AssistantConceptInput = defineSchemaModel({
19
- name: 'AssistantConceptInput',
20
- description: 'Input for explaining a concept with mandatory envelope.',
21
- fields: {
22
- envelope: { type: LLMCallEnvelopeModel, isOptional: false },
23
- conceptKey: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
24
- },
19
+ name: 'AssistantConceptInput',
20
+ description: 'Input for explaining a concept with mandatory envelope.',
21
+ fields: {
22
+ envelope: { type: LLMCallEnvelopeModel, isOptional: false },
23
+ conceptKey: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
24
+ },
25
25
  });
26
26
 
27
27
  export const AssistantAnswerContract = defineCommand({
28
- meta: {
29
- key: 'assistant.answer',
30
- version: '1.0.0',
31
- stability: 'experimental',
32
- owners: ['@examples'],
33
- tags: ['assistant', 'policy', 'locale', 'jurisdiction', 'knowledge'],
34
- description:
35
- 'Answer a user question using a KB snapshot with strict locale/jurisdiction gating.',
36
- goal: 'Provide policy-safe answers that cite a KB snapshot or refuse.',
37
- context:
38
- 'Called by UI or workflows; must fail-closed if envelope is invalid or citations are missing.',
39
- },
40
- io: {
41
- input: AssistantQuestionInput,
42
- output: AssistantAnswerIRModel,
43
- errors: {
44
- LOCALE_REQUIRED: {
45
- description: 'Locale is required and must be supported',
46
- http: 400,
47
- gqlCode: 'LOCALE_REQUIRED',
48
- when: 'locale is missing or unsupported',
49
- },
50
- JURISDICTION_REQUIRED: {
51
- description: 'Jurisdiction is required',
52
- http: 400,
53
- gqlCode: 'JURISDICTION_REQUIRED',
54
- when: 'jurisdiction is missing',
55
- },
56
- KB_SNAPSHOT_REQUIRED: {
57
- description: 'KB snapshot id is required',
58
- http: 400,
59
- gqlCode: 'KB_SNAPSHOT_REQUIRED',
60
- when: 'kbSnapshotId is missing',
61
- },
62
- CITATIONS_REQUIRED: {
63
- description: 'Answers must include citations to a KB snapshot',
64
- http: 422,
65
- gqlCode: 'CITATIONS_REQUIRED',
66
- when: 'answer has no citations',
67
- },
68
- SCOPE_VIOLATION: {
69
- description:
70
- 'Answer violates allowed scope and must be refused/escalated',
71
- http: 403,
72
- gqlCode: 'SCOPE_VIOLATION',
73
- when: 'output includes forbidden content under the given allowedScope',
74
- },
75
- },
76
- },
77
- policy: { auth: 'user' },
28
+ meta: {
29
+ key: 'assistant.answer',
30
+ version: '1.0.0',
31
+ stability: 'experimental',
32
+ owners: ['@examples'],
33
+ tags: ['assistant', 'policy', 'locale', 'jurisdiction', 'knowledge'],
34
+ description:
35
+ 'Answer a user question using a KB snapshot with strict locale/jurisdiction gating.',
36
+ goal: 'Provide policy-safe answers that cite a KB snapshot or refuse.',
37
+ context:
38
+ 'Called by UI or workflows; must fail-closed if envelope is invalid or citations are missing.',
39
+ },
40
+ io: {
41
+ input: AssistantQuestionInput,
42
+ output: AssistantAnswerIRModel,
43
+ errors: {
44
+ LOCALE_REQUIRED: {
45
+ description: 'Locale is required and must be supported',
46
+ http: 400,
47
+ gqlCode: 'LOCALE_REQUIRED',
48
+ when: 'locale is missing or unsupported',
49
+ },
50
+ JURISDICTION_REQUIRED: {
51
+ description: 'Jurisdiction is required',
52
+ http: 400,
53
+ gqlCode: 'JURISDICTION_REQUIRED',
54
+ when: 'jurisdiction is missing',
55
+ },
56
+ KB_SNAPSHOT_REQUIRED: {
57
+ description: 'KB snapshot id is required',
58
+ http: 400,
59
+ gqlCode: 'KB_SNAPSHOT_REQUIRED',
60
+ when: 'kbSnapshotId is missing',
61
+ },
62
+ CITATIONS_REQUIRED: {
63
+ description: 'Answers must include citations to a KB snapshot',
64
+ http: 422,
65
+ gqlCode: 'CITATIONS_REQUIRED',
66
+ when: 'answer has no citations',
67
+ },
68
+ SCOPE_VIOLATION: {
69
+ description:
70
+ 'Answer violates allowed scope and must be refused/escalated',
71
+ http: 403,
72
+ gqlCode: 'SCOPE_VIOLATION',
73
+ when: 'output includes forbidden content under the given allowedScope',
74
+ },
75
+ },
76
+ },
77
+ policy: { auth: 'user' },
78
78
  });
79
79
 
80
80
  export const AssistantExplainConceptContract = defineCommand({
81
- meta: {
82
- key: 'assistant.explainConcept',
83
- version: '1.0.0',
84
- stability: 'experimental',
85
- owners: ['@examples'],
86
- tags: ['assistant', 'policy', 'knowledge', 'concepts'],
87
- description:
88
- 'Explain a concept using a KB snapshot with strict locale/jurisdiction gating.',
89
- goal: 'Explain concepts with citations or refuse.',
90
- context: 'Same constraints as assistant.answer.',
91
- },
92
- io: {
93
- input: AssistantConceptInput,
94
- output: AssistantAnswerIRModel,
95
- errors: AssistantAnswerContract.io.errors,
96
- },
97
- policy: { auth: 'user' },
81
+ meta: {
82
+ key: 'assistant.explainConcept',
83
+ version: '1.0.0',
84
+ stability: 'experimental',
85
+ owners: ['@examples'],
86
+ tags: ['assistant', 'policy', 'knowledge', 'concepts'],
87
+ description:
88
+ 'Explain a concept using a KB snapshot with strict locale/jurisdiction gating.',
89
+ goal: 'Explain concepts with citations or refuse.',
90
+ context: 'Same constraints as assistant.answer.',
91
+ },
92
+ io: {
93
+ input: AssistantConceptInput,
94
+ output: AssistantAnswerIRModel,
95
+ errors: AssistantAnswerContract.io.errors,
96
+ },
97
+ policy: { auth: 'user' },
98
98
  });
@@ -0,0 +1,65 @@
1
+ import {
2
+ OwnersEnum,
3
+ StabilityEnum,
4
+ TagsEnum,
5
+ } from '@contractspec/lib.contracts-spec/ownership';
6
+ import { definePolicy } from '@contractspec/lib.contracts-spec/policy';
7
+
8
+ export const AssistantGatePolicy = definePolicy({
9
+ meta: {
10
+ key: 'locale-jurisdiction-gate.policy.gate',
11
+ version: '1.0.0',
12
+ title: 'Assistant Locale and Jurisdiction Gate',
13
+ description:
14
+ 'Requires explicit locale, jurisdiction, knowledge snapshot, and allowed scope before assistant requests may proceed.',
15
+ domain: 'assistant',
16
+ scope: 'operation',
17
+ owners: [OwnersEnum.PlatformFinance],
18
+ tags: [TagsEnum.I18n, 'assistant', 'policy', 'jurisdiction'],
19
+ stability: StabilityEnum.Experimental,
20
+ },
21
+ rules: [
22
+ {
23
+ effect: 'deny',
24
+ actions: ['assistant.answer', 'assistant.explainConcept'],
25
+ resource: { type: 'assistant-call' },
26
+ conditions: [
27
+ {
28
+ expression:
29
+ '!context.locale || !context.jurisdiction || !context.kbSnapshotId || !context.allowedScope',
30
+ },
31
+ ],
32
+ reason:
33
+ 'Assistant requests fail closed until locale, jurisdiction, kbSnapshotId, and allowedScope are explicit.',
34
+ },
35
+ {
36
+ effect: 'deny',
37
+ actions: ['assistant.answer', 'assistant.explainConcept'],
38
+ resource: { type: 'assistant-call' },
39
+ conditions: [
40
+ {
41
+ expression:
42
+ "!['en-US', 'en-GB', 'fr-FR'].includes(context.locale ?? '')",
43
+ },
44
+ ],
45
+ reason: 'Only the explicitly reviewed assistant locales are permitted.',
46
+ },
47
+ {
48
+ effect: 'allow',
49
+ actions: ['assistant.answer', 'assistant.explainConcept'],
50
+ resource: { type: 'assistant-call' },
51
+ conditions: [
52
+ {
53
+ expression:
54
+ "['en-US', 'en-GB', 'fr-FR'].includes(context.locale ?? '') && !!context.jurisdiction && !!context.kbSnapshotId && !!context.allowedScope",
55
+ },
56
+ ],
57
+ reason:
58
+ 'Explicit context is present, so the request may continue to citation and scope validation.',
59
+ },
60
+ ],
61
+ pii: {
62
+ fields: ['kbSnapshotId'],
63
+ retentionDays: 30,
64
+ },
65
+ });