@unson/brainbase-mcp 0.43.0 → 0.44.1
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/contracts/canonical-task/operation-schema.sql +58 -0
- package/contracts/canonical-task/search-indexes.sql +10 -0
- package/contracts/canonical-task/store-schema.sql +77 -0
- package/dist/automation-run.d.ts +213 -0
- package/dist/automation-run.js +479 -0
- package/dist/canonical-task-operations.d.ts +205 -0
- package/dist/canonical-task-operations.js +700 -0
- package/dist/canonical-task-postgres.d.ts +139 -0
- package/dist/canonical-task-postgres.js +554 -0
- package/dist/canonical-task-service.d.ts +13 -0
- package/dist/canonical-task-service.js +55 -3
- package/dist/canonical-task-store-config.d.ts +31 -0
- package/dist/canonical-task-store-config.js +53 -0
- package/dist/config-parser.d.ts +298 -0
- package/dist/config-parser.js +593 -0
- package/dist/connected-onboarding.d.ts +57 -0
- package/dist/connected-onboarding.js +37 -0
- package/dist/durable-wait-http.d.ts +3 -1
- package/dist/durable-wait-http.js +73 -6
- package/dist/durable-waits.d.ts +93 -3
- package/dist/durable-waits.js +320 -8
- package/dist/honcho.d.ts +25 -0
- package/dist/honcho.js +81 -0
- package/dist/judgment-event-adapter.d.ts +72 -0
- package/dist/judgment-event-adapter.js +114 -0
- package/dist/judgment-event-outbox.d.ts +182 -0
- package/dist/judgment-event-outbox.js +568 -0
- package/dist/judgment-execution-outcome.d.ts +46 -0
- package/dist/judgment-execution-outcome.js +149 -0
- package/dist/judgment-host.d.ts +18 -7
- package/dist/judgment-host.js +82 -34
- package/dist/judgment-lifecycle-core.d.ts +198 -0
- package/dist/judgment-lifecycle-core.js +464 -0
- package/dist/judgment-node-evidence.d.ts +57 -0
- package/dist/judgment-node-evidence.js +297 -0
- package/dist/judgment-runtime-resolution.d.ts +264 -0
- package/dist/judgment-runtime-resolution.js +1248 -0
- package/dist/judgment-runtime-resolution.public.d.ts +255 -0
- package/dist/knowledge-event.d.ts +28 -0
- package/dist/knowledge-event.js +114 -0
- package/dist/mcp-runtime.d.ts +75 -0
- package/dist/mcp-runtime.js +188 -0
- package/dist/meeting-automation-runtime.d.ts +102 -0
- package/dist/meeting-automation-runtime.js +249 -0
- package/dist/meeting-automation.d.ts +155 -0
- package/dist/meeting-automation.js +341 -0
- package/dist/meeting-minutes-context-receipt.d.ts +91 -0
- package/dist/meeting-minutes-context-receipt.js +280 -0
- package/dist/meeting-source-sync.d.ts +89 -0
- package/dist/meeting-source-sync.js +230 -0
- package/dist/mesh.d.ts +58 -0
- package/dist/mesh.js +134 -0
- package/dist/onboarding-runtime.d.ts +47 -0
- package/dist/onboarding-runtime.js +4719 -0
- package/dist/routine-cycle.d.ts +51 -0
- package/dist/routine-cycle.js +153 -0
- package/dist/routine-liveness.d.ts +49 -0
- package/dist/routine-liveness.js +227 -0
- package/dist/routine-runtime.d.ts +101 -0
- package/dist/routine-runtime.js +237 -0
- package/dist/routines.d.ts +3 -0
- package/dist/routines.js +3 -0
- package/dist/run-receipt.d.ts +40 -0
- package/dist/run-receipt.js +635 -0
- package/dist/runtime-host.d.ts +64 -0
- package/dist/runtime-host.js +70 -0
- package/dist/world-work.d.ts +16 -0
- package/package.json +115 -3
- package/ui/meeting-minutes.css +122 -0
- package/ui/meeting-minutes.js +316 -0
- package/ui/objective-editor.js +47 -11
- package/ui/world/world-district.js +18 -3
- package/ui/world/world-placement.js +13 -0
- package/ui/world/world-work-rail.js +7 -1
|
@@ -0,0 +1,1248 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Stage3a migration exception: the mechanically extracted legacy helper body
|
|
3
|
+
// remains unchecked internally. The public adapter contract is declared and
|
|
4
|
+
// strict-checked in judgment-runtime-resolution.public.d.ts instead of using
|
|
5
|
+
// this exception to weaken the normalized input/output boundary.
|
|
6
|
+
/**
|
|
7
|
+
* Pure judgment runtime resolver core.
|
|
8
|
+
*
|
|
9
|
+
* This module is intentionally free of filesystem, transport, authentication,
|
|
10
|
+
* tenant, Host binding, policy authorization, TaskGraph persistence, and
|
|
11
|
+
* receipt I/O. Callers must validate and authorize those inputs in an adapter,
|
|
12
|
+
* then pass only the normalized request and already-applicable policies here.
|
|
13
|
+
* The implementation is kept behaviorally aligned with the former Unson
|
|
14
|
+
* service so the Host can opt into it without changing live behavior.
|
|
15
|
+
*/
|
|
16
|
+
import { JudgmentResolutionError, canonicalJson, computeRequestDigest, sha256Hex } from './judgment-resolution-contract.js';
|
|
17
|
+
export { JudgmentResolutionError, canonicalJson, computeRequestDigest, sha256Hex };
|
|
18
|
+
const INPUT_FIELDS = new Set(['request', 'turn_id', 'project_code', 'conversation_context', 'model_interpretation']);
|
|
19
|
+
const CLASSIFICATION_FIELDS = new Set(['intent', 'domains', 'action_kind', 'risk', 'confidence', 'signals']);
|
|
20
|
+
const CONVERSATION_CONTEXT_FIELDS = new Set([
|
|
21
|
+
'schema_version', 'session_ref', 'messages', 'prior_receipts', 'runtime',
|
|
22
|
+
'instruction_bindings', 'completeness', 'source_digest'
|
|
23
|
+
]);
|
|
24
|
+
const MESSAGE_FIELDS = new Set(['sequence', 'turn_id', 'role', 'phase', 'text']);
|
|
25
|
+
const PRIOR_RECEIPT_FIELDS = new Set([
|
|
26
|
+
'turn_id', 'resolution_id', 'request_digest', 'context_digest', 'plan_digest',
|
|
27
|
+
'classification', 'selected_dag_ids'
|
|
28
|
+
]);
|
|
29
|
+
const RUNTIME_FIELDS = new Set(['host', 'entrypoint', 'model', 'permission_mode', 'project_binding']);
|
|
30
|
+
const INSTRUCTION_BINDING_FIELDS = new Set(['scope', 'source_ref', 'digest']);
|
|
31
|
+
const INTENTS = new Set(['answer', 'investigate', 'diagnose', 'design', 'implement', 'review', 'operate']);
|
|
32
|
+
const INTENT_ORDER = ['operate', 'implement', 'review', 'diagnose', 'design', 'investigate', 'answer'];
|
|
33
|
+
const DOMAINS = new Set(['general', 'knowledge', 'personal_judgment', 'engineering', 'organization', 'operations']);
|
|
34
|
+
const ACTIONS = ['none', 'read', 'write', 'external'];
|
|
35
|
+
const RISKS = ['low', 'medium', 'high', 'critical'];
|
|
36
|
+
const CONFIDENCES = new Set(['confirmed', 'inferred', 'unknown']);
|
|
37
|
+
const SIGNALS = new Set(['cumulative_effect', 'complexity_growth', 'threshold_proposal', 'parallel_exploration', 'authority_boundary', 'problem_frame_uncertain', 'external_outcome']);
|
|
38
|
+
const CONTENT_TYPES = new Set(['canonical_fact', 'team_document', 'source_document', 'personal_knowledge', 'operational_state', 'unknown']);
|
|
39
|
+
const SCOPE_SPECIFICITY = { global: 0, organization: 1, project: 2, owner: 3 };
|
|
40
|
+
const POLICY_VISIBILITIES = new Set(['organization', 'owner']);
|
|
41
|
+
const NODE_KINDS = new Set(['common', 'judgment', 'capability', 'constraint', 'fail_closed']);
|
|
42
|
+
const DAG_KINDS = new Set(['domain', 'constraint', 'fail_closed']);
|
|
43
|
+
const DOMAIN_MATCHERS = new Set(['engineering', 'knowledge', 'personal_judgment', 'organization', 'operations']);
|
|
44
|
+
const SAFETY_MATCHERS = new Set(['write', 'external', 'critical']);
|
|
45
|
+
const AUTONOMY_DECISIONS = new Set(['continue', 'escalate']);
|
|
46
|
+
const AUTONOMY_REASON_CODES = new Set([
|
|
47
|
+
'routine_in_scope', 'classification_missing', 'policy_conflict', 'risk_or_external'
|
|
48
|
+
]);
|
|
49
|
+
const RUNTIME_ESCALATION_REASONS = new Set([
|
|
50
|
+
'irreversible_action', 'missing_authority', 'owner_value_choice',
|
|
51
|
+
'required_input_unavailable', 'evidenced_terminal_blocker'
|
|
52
|
+
]);
|
|
53
|
+
const INTENT_MATCHERS = new Set(INTENT_ORDER);
|
|
54
|
+
const ADAPTER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
55
|
+
const ADAPTER_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
|
|
56
|
+
const RUNTIME_HOSTS = new Set(['codex', 'claude-code']);
|
|
57
|
+
const RUNTIME_ENTRYPOINTS = new Set(['codex', 'claude_code', 'mana', 'company_os', 'unknown']);
|
|
58
|
+
function compareCodePoints(left, right) {
|
|
59
|
+
const a = Array.from(left, (value) => value.codePointAt(0));
|
|
60
|
+
const b = Array.from(right, (value) => value.codePointAt(0));
|
|
61
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
|
|
62
|
+
if (a[index] !== b[index])
|
|
63
|
+
return a[index] - b[index];
|
|
64
|
+
}
|
|
65
|
+
return a.length - b.length;
|
|
66
|
+
}
|
|
67
|
+
function fail(message, code = 'judgment_resolution_input_invalid', status = 400) {
|
|
68
|
+
throw new JudgmentResolutionError(code, message, status);
|
|
69
|
+
}
|
|
70
|
+
function exactFields(value, allowed, name) {
|
|
71
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
72
|
+
fail(`${name} must be an object`);
|
|
73
|
+
for (const key of Object.keys(value)) {
|
|
74
|
+
if (!allowed.has(key))
|
|
75
|
+
fail(`${name}.${key} is not allowed`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function requiredString(value, name) {
|
|
79
|
+
if (typeof value !== 'string' || !value.trim())
|
|
80
|
+
fail(`${name} is required`);
|
|
81
|
+
return value.trim();
|
|
82
|
+
}
|
|
83
|
+
function requiredText(value, name) {
|
|
84
|
+
if (typeof value !== 'string' || !value.trim())
|
|
85
|
+
fail(`${name} is required`);
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function uniqueEnumArray(value, name, allowed, { optional = false } = {}) {
|
|
89
|
+
if (value === undefined && optional)
|
|
90
|
+
return [];
|
|
91
|
+
if (!Array.isArray(value) || (!optional && value.length === 0))
|
|
92
|
+
fail(`${name} must be a non-empty array`);
|
|
93
|
+
const output = [];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const entry of value) {
|
|
96
|
+
if (typeof entry !== 'string' || !allowed.has(entry))
|
|
97
|
+
fail(`${name} contains an invalid value`);
|
|
98
|
+
if (seen.has(entry))
|
|
99
|
+
fail(`${name} contains duplicate value '${entry}'`);
|
|
100
|
+
seen.add(entry);
|
|
101
|
+
output.push(entry);
|
|
102
|
+
}
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
function uniqueStringArray(value, name) {
|
|
106
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
107
|
+
fail(`${name} must be a non-empty array`);
|
|
108
|
+
const output = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (const entry of value) {
|
|
111
|
+
const normalized = requiredString(entry, name);
|
|
112
|
+
if (/[\u0000-\u001f\u007f]/u.test(normalized))
|
|
113
|
+
fail(`${name} contains an invalid value`);
|
|
114
|
+
if (seen.has(normalized))
|
|
115
|
+
fail(`${name} contains duplicate value '${normalized}'`);
|
|
116
|
+
seen.add(normalized);
|
|
117
|
+
output.push(normalized);
|
|
118
|
+
}
|
|
119
|
+
return output;
|
|
120
|
+
}
|
|
121
|
+
function stringArray(value, name, { allowEmpty = false } = {}) {
|
|
122
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0))
|
|
123
|
+
fail(`${name} must be a${allowEmpty ? '' : ' non-empty'} array`);
|
|
124
|
+
return value.map((entry) => requiredString(entry, name));
|
|
125
|
+
}
|
|
126
|
+
function enumValue(value, name, allowed) {
|
|
127
|
+
if (typeof value !== 'string' || !allowed.has(value))
|
|
128
|
+
fail(`${name} contains an invalid value`);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function indexFloor(order, left, right) {
|
|
132
|
+
return order[Math.max(order.indexOf(left), order.indexOf(right))];
|
|
133
|
+
}
|
|
134
|
+
function actionFloor(intent) {
|
|
135
|
+
if (intent === 'implement' || intent === 'operate')
|
|
136
|
+
return 'write';
|
|
137
|
+
if (intent === 'investigate' || intent === 'diagnose' || intent === 'review')
|
|
138
|
+
return 'read';
|
|
139
|
+
return 'none';
|
|
140
|
+
}
|
|
141
|
+
function escapeRegExp(value) {
|
|
142
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
143
|
+
}
|
|
144
|
+
function includesTerm(request, terms) {
|
|
145
|
+
const normalized = request.toLocaleLowerCase('ja');
|
|
146
|
+
return terms.some((term) => {
|
|
147
|
+
const normalizedTerm = term.toLocaleLowerCase('ja');
|
|
148
|
+
if (/^[a-z0-9_.-]+$/u.test(normalizedTerm)) {
|
|
149
|
+
return new RegExp(`(?<![a-z0-9_])${escapeRegExp(normalizedTerm)}(?![a-z0-9_])`, 'u').test(normalized);
|
|
150
|
+
}
|
|
151
|
+
return normalized.includes(normalizedTerm);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function includesPositiveCommandClause(request, terms) {
|
|
155
|
+
const japaneseTerms = terms.filter((term) => !/^[a-z0-9_.-]+$/iu.test(term));
|
|
156
|
+
const normalizedRequest = request.toLocaleLowerCase('ja');
|
|
157
|
+
const includesJapaneseCommand = japaneseTerms.some((term) => {
|
|
158
|
+
const normalizedTerm = term.toLocaleLowerCase('ja');
|
|
159
|
+
let offset = normalizedRequest.indexOf(normalizedTerm);
|
|
160
|
+
while (offset >= 0) {
|
|
161
|
+
const remainder = normalizedRequest.slice(offset + normalizedTerm.length);
|
|
162
|
+
const endsCommand = /^(?:$|[\s、,;;。!?!?])/u.test(remainder)
|
|
163
|
+
|| /^(?:(?:ください|下さい|くれ|ほしい|欲しい|おけ|みろ)|(?:おいて|みて)(?:ください|下さい)?|(?:もらえ|いただけ)(?:ます(?:か|でしょうか)|ません(?:か|でしょうか)|ないですか)|(?:もらいたい|いただきたい))(?=$|[\s、,;;。!?!?])/u.test(remainder);
|
|
164
|
+
if (endsCommand) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
offset = normalizedRequest.indexOf(normalizedTerm, offset + normalizedTerm.length);
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
});
|
|
171
|
+
if (includesJapaneseCommand)
|
|
172
|
+
return true;
|
|
173
|
+
const englishTerms = terms.filter((term) => /^[a-z0-9_.-]+$/iu.test(term));
|
|
174
|
+
if (englishTerms.length === 0)
|
|
175
|
+
return false;
|
|
176
|
+
const commandPattern = englishTerms.map(escapeRegExp).join('|');
|
|
177
|
+
const match = new RegExp(`^(?:please\\s+|(?:can|could|would|will)\\s+you\\s+(?:please\\s+)?|you\\s+must\\s+)?(?:${commandPattern})\\b`, 'iu').exec(request.trim());
|
|
178
|
+
if (!match)
|
|
179
|
+
return false;
|
|
180
|
+
const remainder = request.trim().slice(match[0].length);
|
|
181
|
+
return !/^\s+(?:is|are|was|were|has|have|had)\b/iu.test(remainder);
|
|
182
|
+
}
|
|
183
|
+
function includesRequestedEffectTerm(request, terms) {
|
|
184
|
+
const normalized = request.toLocaleLowerCase('ja');
|
|
185
|
+
return terms.some((term) => {
|
|
186
|
+
const normalizedTerm = term.toLocaleLowerCase('ja');
|
|
187
|
+
let offset = 0;
|
|
188
|
+
while (offset < normalized.length) {
|
|
189
|
+
const matcher = /^[a-z0-9_.-]+$/u.test(normalizedTerm)
|
|
190
|
+
? new RegExp(`(?<![a-z0-9_])${escapeRegExp(normalizedTerm)}(?![a-z0-9_])`, 'gu')
|
|
191
|
+
: null;
|
|
192
|
+
if (matcher)
|
|
193
|
+
matcher.lastIndex = offset;
|
|
194
|
+
const match = matcher?.exec(normalized);
|
|
195
|
+
const index = matcher ? (match?.index ?? -1) : normalized.indexOf(normalizedTerm, offset);
|
|
196
|
+
if (index < 0)
|
|
197
|
+
return false;
|
|
198
|
+
const continuation = normalized.slice(index + normalizedTerm.length).trimStart();
|
|
199
|
+
const isConditionalTeForm = normalizedTerm.endsWith('して')
|
|
200
|
+
&& /^(?:も(?!ら)|しま|いる|いた|ある|あった|おり|はいけ|はなら|よい|良い|いい|問題ない|可能|でき)/u.test(continuation);
|
|
201
|
+
const sentenceTail = continuation.split(/[。!?\n]/u, 1)[0];
|
|
202
|
+
const negation = /(?:は|を|も)?(?:実行|実施)?(?:しない(?:でください)?|しません|行わない(?:でください)?|禁止)/u.exec(sentenceTail);
|
|
203
|
+
const listPrefix = negation ? sentenceTail.slice(0, negation.index) : '';
|
|
204
|
+
const isNegated = /(?:do not|don't|must not)\s*$/u.test(normalized.slice(Math.max(0, index - 16), index))
|
|
205
|
+
|| Boolean(negation && !/(?:して|し、|し,|した|する|してください|せよ|しろ)/u.test(listPrefix));
|
|
206
|
+
if (!isConditionalTeForm && !isNegated)
|
|
207
|
+
return true;
|
|
208
|
+
offset = index + normalizedTerm.length;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Bounds keep the check linear in the request length. Past a bound the term
|
|
214
|
+
// stays critical, so a bound can keep an escalation but never remove one.
|
|
215
|
+
const PROHIBITION_WINDOW = 200;
|
|
216
|
+
const MAX_PROHIBITED_OCCURRENCES = 32;
|
|
217
|
+
// A prohibition predicate ends its clause. A negation that continues into a
|
|
218
|
+
// condition, reason, or question (「行わないと」「しないので」「しませんか」) is not one.
|
|
219
|
+
const JAPANESE_PROHIBITION_PREDICATE = /(?:(?:しない|行わない)(?:でください|こと)?|しません|行いません|するな|避けてください|(?:して|って|んで|て|で)?は(?:いけ|なら)(?:ない|ません)|禁止(?:です|されています)?|不可(?:です)?)(?=[ねよ]?\s*[))」』]*\s*(?:$|[、,,;;。.!!]))/u;
|
|
220
|
+
const NOUN = String.raw `(?:(?!等|以外)[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{Nd}ー々_-])+`;
|
|
221
|
+
const ACTION_HEAD = '(?:外部|本番|自動|手動|一括)?(?:変更|操作|処理|作業|実行|送信|送付|更新|反映|書き込み|書込み|投稿|通知|対応|行為|取引|手続き|アクション)';
|
|
222
|
+
const COORDINATOR = '(?:・|や|と|および|及び|または|又は|もしくは|並びに|ならびに)';
|
|
223
|
+
// All that may separate a critical term from the prohibition governing it: an
|
|
224
|
+
// action noun it modifies, the rest of a noun list, and one final particle. A
|
|
225
|
+
// comma list must close with など/等, since 「本番削除、確認はしない」 can also
|
|
226
|
+
// mean "delete, without confirming". Anything else (「以外の」「のみ行い、」
|
|
227
|
+
// 「は後回しに」「の停止は」) means the prohibition governs something other than
|
|
228
|
+
// the term.
|
|
229
|
+
const PROHIBITED_TERM_TAIL = new RegExp(`^(?:の?${ACTION_HEAD})?(?:(?:\\s*${COORDINATOR}\\s*${NOUN})*(?:など|等)?(?:の${ACTION_HEAD})?|(?:\\s*(?:[、,,]|${COORDINATOR})\\s*${NOUN})+(?:など|等)(?:の${ACTION_HEAD})?)[はをもが]?(?:一切|絶対に|決して)?$`, 'u');
|
|
230
|
+
// Before a list comma the term must start its item or follow a noun modifier;
|
|
231
|
+
// 「請求書を支払い、」「すぐ支払い、」「至急支払い、」 use it as a verb.
|
|
232
|
+
const LIST_ITEM_HEAD = /^(?:(?!等|以外)[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{Nd}ー々_\- ]|の|や|と|への|での|からの|までの)*$/u;
|
|
233
|
+
const LIST_ITEM_HEAD_END = /(?:^|の|や|と)\s*$/u;
|
|
234
|
+
function isProhibitedObject(text, index, length) {
|
|
235
|
+
const before = text.slice(Math.max(0, index - PROHIBITION_WINDOW), index);
|
|
236
|
+
const clause = before.slice(Math.max(...['。', '.', '!', '?', '!', '?', '\n'].map((mark) => before.lastIndexOf(mark))) + 1);
|
|
237
|
+
const clauseHead = clause.slice(Math.max(...['、', ',', ',', ';', ';'].map((mark) => clause.lastIndexOf(mark))) + 1);
|
|
238
|
+
const tailEnd = index + length + PROHIBITION_WINDOW + 1;
|
|
239
|
+
const sentenceTail = /^[^。.!?!?\n]*[。.!?!?\n]?/u.exec(text.slice(index + length, tailEnd))[0];
|
|
240
|
+
// A question, or a sentence too long to judge, is not a prohibition.
|
|
241
|
+
if (sentenceTail.length > PROHIBITION_WINDOW || /[??]$/u.test(sentenceTail))
|
|
242
|
+
return false;
|
|
243
|
+
const prohibition = JAPANESE_PROHIBITION_PREDICATE.exec(sentenceTail);
|
|
244
|
+
if (!prohibition)
|
|
245
|
+
return false;
|
|
246
|
+
// A quoted or nominalized prohibition (「支払いはしない、という制限は外して」
|
|
247
|
+
// 「支払いはしない、は撤回」) is being discussed, not imposed.
|
|
248
|
+
const predicateEnd = index + length + prohibition.index + prohibition[0].length;
|
|
249
|
+
if (/^[ねよ]?[\s))」』]*(?:[、,,;;。.!!]?\s*(?:と|って)|[、,,]\s*[はをがもの])/u.test(text.slice(predicateEnd, predicateEnd + 8)))
|
|
250
|
+
return false;
|
|
251
|
+
const between = sentenceTail.slice(0, prohibition.index);
|
|
252
|
+
if (/^\s*[、,,]/u.test(between) && !(LIST_ITEM_HEAD.test(clauseHead) && LIST_ITEM_HEAD_END.test(clauseHead)))
|
|
253
|
+
return false;
|
|
254
|
+
return PROHIBITED_TERM_TAIL.test(between);
|
|
255
|
+
}
|
|
256
|
+
// Polite prohibitions are already stripped from the positive request. A term
|
|
257
|
+
// that survives still counts unless a clause-final prohibition governs it
|
|
258
|
+
// directly, such as the plain form 「…支払い…などの外部変更は行わない。」.
|
|
259
|
+
function includesUnprohibitedTerm(request, terms) {
|
|
260
|
+
const normalized = request.toLocaleLowerCase('ja');
|
|
261
|
+
return terms.some((term) => {
|
|
262
|
+
const normalizedTerm = term.toLocaleLowerCase('ja');
|
|
263
|
+
if (/^[a-z0-9_.-]+$/u.test(normalizedTerm))
|
|
264
|
+
return includesTerm(request, [term]);
|
|
265
|
+
let prohibited = 0;
|
|
266
|
+
for (let index = normalized.indexOf(normalizedTerm); index >= 0; index = normalized.indexOf(normalizedTerm, index + normalizedTerm.length)) {
|
|
267
|
+
if (prohibited === MAX_PROHIBITED_OCCURRENCES || !isProhibitedObject(normalized, index, normalizedTerm.length))
|
|
268
|
+
return true;
|
|
269
|
+
prohibited += 1;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function responseAnnotationCommands(request) {
|
|
275
|
+
const commands = [];
|
|
276
|
+
for (const match of request.matchAll(/<response-annotations>([\s\S]*?)<\/response-annotations>/giu)) {
|
|
277
|
+
let annotations;
|
|
278
|
+
try {
|
|
279
|
+
annotations = JSON.parse(match[1]);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (!Array.isArray(annotations))
|
|
285
|
+
continue;
|
|
286
|
+
for (const entry of annotations) {
|
|
287
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
288
|
+
continue;
|
|
289
|
+
if (typeof entry.annotation !== 'string' || !entry.annotation.trim())
|
|
290
|
+
continue;
|
|
291
|
+
commands.push(entry.annotation.trim());
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return commands;
|
|
295
|
+
}
|
|
296
|
+
function classificationRequest(request) {
|
|
297
|
+
const annotationCommands = responseAnnotationCommands(request);
|
|
298
|
+
const withoutStructuredMaterial = request
|
|
299
|
+
.replace(/<response-annotations>[\s\S]*?<\/response-annotations>/giu, ' ')
|
|
300
|
+
.replace(/```[\s\S]*?```/gu, ' ')
|
|
301
|
+
.replace(/^>.*$/gmu, ' ');
|
|
302
|
+
const paragraphs = withoutStructuredMaterial.split(/\n\s*\n/u);
|
|
303
|
+
const materialStart = paragraphs.findIndex((paragraph, index) => index > 0 && (/(?:^|\n)\s*(?:添付|引用|会話ログ|ログ|参考|資料)\s*[::]/u.test(paragraph)
|
|
304
|
+
|| /(?:^|\n)\s*(?:\d{1,2}:\d{2}|\d{4}[./-]\d{1,2}[./-]\d{1,2}|\d{4}\.\d{2}\.\d{2}\s+.+曜日)/u.test(paragraph)));
|
|
305
|
+
const commandParagraphs = materialStart < 0 ? paragraphs : paragraphs.slice(0, materialStart);
|
|
306
|
+
return [...commandParagraphs, ...annotationCommands].join('\n\n').trim();
|
|
307
|
+
}
|
|
308
|
+
function positiveClassificationRequest(request, manifest) {
|
|
309
|
+
const positiveCommandTerms = manifest.semantic_matchers.positive_commands;
|
|
310
|
+
return request
|
|
311
|
+
.split(/(?<=[。!?.!?\n])/u)
|
|
312
|
+
.map((sentence) => {
|
|
313
|
+
const japaneseBoundary = /(?:しないでください|行わないでください|実行しないでください|しないこと|するな|避けてください|しません|行いません|実行しません|禁止(?:です|されています)?(?=[、,;;。!?\s]|$)|不可(?:です)?(?=[、,;;。!?\s]|$))/u.exec(sentence);
|
|
314
|
+
if (japaneseBoundary) {
|
|
315
|
+
const beforeNegation = sentence.slice(0, japaneseBoundary.index);
|
|
316
|
+
const clauseBoundary = Math.max(beforeNegation.lastIndexOf('、'), beforeNegation.lastIndexOf(','), beforeNegation.lastIndexOf(';'), beforeNegation.lastIndexOf(';'));
|
|
317
|
+
const positivePrefix = clauseBoundary >= 0
|
|
318
|
+
? beforeNegation.slice(0, clauseBoundary).trim()
|
|
319
|
+
: '';
|
|
320
|
+
const keepPrefix = includesPositiveCommandClause(positivePrefix, positiveCommandTerms)
|
|
321
|
+
? positivePrefix
|
|
322
|
+
: '';
|
|
323
|
+
const afterNegation = sentence.slice(japaneseBoundary.index + japaneseBoundary[0].length);
|
|
324
|
+
const tailBoundary = /^[、,;;]/u.exec(afterNegation);
|
|
325
|
+
const tailCandidate = tailBoundary
|
|
326
|
+
? afterNegation.slice(tailBoundary[0].length).replace(/^[、,;;。!?\s]+/u, '')
|
|
327
|
+
: '';
|
|
328
|
+
const positiveTail = includesPositiveCommandClause(tailCandidate, positiveCommandTerms)
|
|
329
|
+
? tailCandidate
|
|
330
|
+
: '';
|
|
331
|
+
return [keepPrefix, positiveTail].filter(Boolean).join('。');
|
|
332
|
+
}
|
|
333
|
+
const englishBoundary = /\b(?:do not|don't|must not|never|no(?!-)|is prohibited|are prohibited|is forbidden|are forbidden)\b/iu.exec(sentence);
|
|
334
|
+
if (englishBoundary) {
|
|
335
|
+
const beforeNegation = sentence.slice(0, englishBoundary.index);
|
|
336
|
+
const suffixProhibition = /^(?:is|are) (?:prohibited|forbidden)$/iu.test(englishBoundary[0]);
|
|
337
|
+
const clauseBoundary = Math.max(beforeNegation.lastIndexOf(','), beforeNegation.lastIndexOf(';'));
|
|
338
|
+
const positiveMaterial = suffixProhibition
|
|
339
|
+
? (clauseBoundary >= 0 ? beforeNegation.slice(0, clauseBoundary) : '')
|
|
340
|
+
: beforeNegation;
|
|
341
|
+
const positivePrefix = positiveMaterial
|
|
342
|
+
.replace(/\b(?:but|and)\s*$/iu, '')
|
|
343
|
+
.replace(/[,;:\s]+$/u, '')
|
|
344
|
+
.trim();
|
|
345
|
+
const keepPrefix = includesPositiveCommandClause(positivePrefix, positiveCommandTerms)
|
|
346
|
+
? positivePrefix
|
|
347
|
+
: '';
|
|
348
|
+
const afterNegation = sentence.slice(englishBoundary.index + englishBoundary[0].length);
|
|
349
|
+
const tailBoundary = /(?:[;;]|,\s*(?:but|and(?: then)?)\s+)/iu.exec(afterNegation);
|
|
350
|
+
const tailCandidate = tailBoundary
|
|
351
|
+
? afterNegation.slice(tailBoundary.index + tailBoundary[0].length).replace(/^[,.;:!?\s]+/u, '')
|
|
352
|
+
: '';
|
|
353
|
+
const positiveTail = includesPositiveCommandClause(tailCandidate, positiveCommandTerms)
|
|
354
|
+
? tailCandidate
|
|
355
|
+
: '';
|
|
356
|
+
return [keepPrefix, positiveTail].filter(Boolean).join('. ');
|
|
357
|
+
}
|
|
358
|
+
return sentence;
|
|
359
|
+
})
|
|
360
|
+
.join('')
|
|
361
|
+
.trim();
|
|
362
|
+
}
|
|
363
|
+
function sortByOrder(values, order) {
|
|
364
|
+
const indexes = new Map(order.map((value, index) => [value, index]));
|
|
365
|
+
return [...values].sort((left, right) => (indexes.get(left) ?? Number.MAX_SAFE_INTEGER) - (indexes.get(right) ?? Number.MAX_SAFE_INTEGER));
|
|
366
|
+
}
|
|
367
|
+
function validatedClassification(value, name, manifest) {
|
|
368
|
+
exactFields(value, CLASSIFICATION_FIELDS, name);
|
|
369
|
+
const domains = uniqueEnumArray(value.domains, `${name}.domains`, DOMAINS);
|
|
370
|
+
return {
|
|
371
|
+
intent: enumValue(value.intent, `${name}.intent`, INTENTS),
|
|
372
|
+
domains: sortByOrder(domains, manifest.selectors.domain_order),
|
|
373
|
+
action_kind: enumValue(value.action_kind, `${name}.action_kind`, new Set(ACTIONS)),
|
|
374
|
+
risk: enumValue(value.risk, `${name}.risk`, new Set(RISKS)),
|
|
375
|
+
confidence: enumValue(value.confidence, `${name}.confidence`, CONFIDENCES),
|
|
376
|
+
signals: sortByOrder(uniqueEnumArray(value.signals, `${name}.signals`, SIGNALS, { optional: true }), manifest.selectors.signal_order)
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function validatePolicy(policy, seen) {
|
|
380
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy) || typeof policy.id !== 'string' || !policy.id || seen.has(policy.id))
|
|
381
|
+
throw new TypeError('judgment manifest has duplicate or invalid policy id');
|
|
382
|
+
seen.add(policy.id);
|
|
383
|
+
if (typeof policy.version !== 'string' || !policy.version)
|
|
384
|
+
throw new TypeError(`policy ${policy.id} version is invalid`);
|
|
385
|
+
if (!Number.isInteger(policy.priority))
|
|
386
|
+
throw new TypeError(`policy ${policy.id} priority must be an integer`);
|
|
387
|
+
if (!['hard', 'soft'].includes(policy.strength))
|
|
388
|
+
throw new TypeError(`policy ${policy.id} strength is invalid`);
|
|
389
|
+
if (!policy.scope || !(policy.scope.type in SCOPE_SPECIFICITY))
|
|
390
|
+
throw new TypeError(`policy ${policy.id} scope is invalid`);
|
|
391
|
+
if (policy.scope.type === 'global' ? policy.scope.id !== null : typeof policy.scope.id !== 'string' || !policy.scope.id)
|
|
392
|
+
throw new TypeError(`policy ${policy.id} scope id is invalid`);
|
|
393
|
+
if (!POLICY_VISIBILITIES.has(policy.visibility))
|
|
394
|
+
throw new TypeError(`policy ${policy.id} visibility is invalid`);
|
|
395
|
+
if (policy.owner_person_id !== null && (typeof policy.owner_person_id !== 'string' || !policy.owner_person_id))
|
|
396
|
+
throw new TypeError(`policy ${policy.id} owner is invalid`);
|
|
397
|
+
if (policy.visibility === 'owner' && typeof policy.owner_person_id !== 'string')
|
|
398
|
+
throw new TypeError(`policy ${policy.id} owner visibility requires an owner`);
|
|
399
|
+
if (typeof policy.evidence_requirement !== 'string' || !policy.evidence_requirement)
|
|
400
|
+
throw new TypeError(`policy ${policy.id} evidence requirement is invalid`);
|
|
401
|
+
if (!policy.effect || typeof policy.effect.target !== 'string' || !policy.effect.target)
|
|
402
|
+
throw new TypeError(`policy ${policy.id} effect target is invalid`);
|
|
403
|
+
const decision = policy.effect?.decision;
|
|
404
|
+
if (policy.strength === 'hard' && !['require', 'forbid'].includes(decision))
|
|
405
|
+
throw new TypeError(`policy ${policy.id} hard effect is invalid`);
|
|
406
|
+
if (policy.strength === 'soft' && decision !== 'prefer')
|
|
407
|
+
throw new TypeError(`policy ${policy.id} soft effect is invalid`);
|
|
408
|
+
if (typeof policy.instruction !== 'string' || !policy.instruction)
|
|
409
|
+
throw new TypeError(`policy ${policy.id} instruction is invalid`);
|
|
410
|
+
if (policy.human_approval !== undefined) {
|
|
411
|
+
const approval = policy.human_approval;
|
|
412
|
+
if (!approval || typeof approval !== 'object' || Array.isArray(approval))
|
|
413
|
+
throw new TypeError(`policy ${policy.id} human_approval is invalid`);
|
|
414
|
+
const keys = Object.keys(approval);
|
|
415
|
+
if (keys.length === 0 || keys.some((key) => !['action_kinds', 'risks'].includes(key)))
|
|
416
|
+
throw new TypeError(`policy ${policy.id} human_approval is invalid`);
|
|
417
|
+
if (approval.action_kinds !== undefined && (!Array.isArray(approval.action_kinds) || approval.action_kinds.length === 0 || approval.action_kinds.some((kind) => !ACTIONS.includes(kind)))) {
|
|
418
|
+
throw new TypeError(`policy ${policy.id} human_approval action_kinds is invalid`);
|
|
419
|
+
}
|
|
420
|
+
if (approval.risks !== undefined && (!Array.isArray(approval.risks) || approval.risks.length === 0 || approval.risks.some((risk) => !RISKS.includes(risk)))) {
|
|
421
|
+
throw new TypeError(`policy ${policy.id} human_approval risks is invalid`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function validateStringTerms(value, name, { allowEmpty = false } = {}) {
|
|
426
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0) || value.some((entry) => typeof entry !== 'string' || !entry)) {
|
|
427
|
+
throw new TypeError(`${name} terms are invalid`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function validateExactKeys(value, expected, name) {
|
|
431
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
432
|
+
throw new TypeError(`${name} is invalid`);
|
|
433
|
+
const actual = Object.keys(value);
|
|
434
|
+
if (actual.length !== expected.size || actual.some((key) => !expected.has(key)))
|
|
435
|
+
throw new TypeError(`${name} references an unsupported selector or matcher`);
|
|
436
|
+
}
|
|
437
|
+
function validateSelectableGraphs(manifest) {
|
|
438
|
+
const signalDagIds = Object.values(manifest.selectors.signal_dags);
|
|
439
|
+
const commonDagIds = [...signalDagIds, manifest.selectors.authority_dag];
|
|
440
|
+
const domainGroups = [
|
|
441
|
+
[manifest.selectors.domain_dags.general],
|
|
442
|
+
manifest.selectors.domain_order
|
|
443
|
+
.filter((domain) => domain !== 'general')
|
|
444
|
+
.map((domain) => manifest.selectors.domain_dags[domain])
|
|
445
|
+
];
|
|
446
|
+
if (manifest.selectors.engineering_implementation_dag) {
|
|
447
|
+
domainGroups.push(domainGroups[1].map((id) => id === manifest.selectors.domain_dags.engineering
|
|
448
|
+
? manifest.selectors.engineering_implementation_dag : id));
|
|
449
|
+
}
|
|
450
|
+
for (const domainDagIds of domainGroups) {
|
|
451
|
+
try {
|
|
452
|
+
buildGraph([...new Set([...domainDagIds, ...commonDagIds])], manifest);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
if (error instanceof TypeError && error.message === 'active judgment graph contains a cycle') {
|
|
456
|
+
throw new TypeError('selectable judgment graph contains a cycle');
|
|
457
|
+
}
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
export function validateManifestLock(lock, previous = null, current = null) {
|
|
463
|
+
if (!lock || lock.schema_version !== 'brainbase-judgment-manifest-lock-v1' || !Array.isArray(lock.entries) || lock.entries.length === 0) {
|
|
464
|
+
throw new TypeError('judgment manifest lock schema is invalid');
|
|
465
|
+
}
|
|
466
|
+
const versions = new Set();
|
|
467
|
+
for (const entry of lock.entries) {
|
|
468
|
+
if (!entry || typeof entry.runtime_version !== 'string' || !entry.runtime_version)
|
|
469
|
+
throw new TypeError('judgment manifest lock runtime version is invalid');
|
|
470
|
+
if (versions.has(entry.runtime_version))
|
|
471
|
+
throw new TypeError(`judgment manifest lock duplicate runtime version '${entry.runtime_version}'`);
|
|
472
|
+
versions.add(entry.runtime_version);
|
|
473
|
+
if (typeof entry.manifest_digest !== 'string' || !/^[a-f0-9]{64}$/.test(entry.manifest_digest))
|
|
474
|
+
throw new TypeError('judgment manifest lock digest is invalid');
|
|
475
|
+
}
|
|
476
|
+
if (previous) {
|
|
477
|
+
if (previous.schema_version !== lock.schema_version || !Array.isArray(previous.entries) || lock.entries.length < previous.entries.length) {
|
|
478
|
+
throw new TypeError('judgment manifest lock is append-only');
|
|
479
|
+
}
|
|
480
|
+
for (let index = 0; index < previous.entries.length; index += 1) {
|
|
481
|
+
if (canonicalJson(previous.entries[index]) !== canonicalJson(lock.entries[index]))
|
|
482
|
+
throw new TypeError('judgment manifest lock is append-only');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (current) {
|
|
486
|
+
const last = lock.entries.at(-1);
|
|
487
|
+
if (last.runtime_version !== current.runtimeVersion || last.manifest_digest !== current.manifestDigest)
|
|
488
|
+
throw new TypeError('judgment manifest lock current pair does not match');
|
|
489
|
+
}
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
export function validateManifest(manifest, lock) {
|
|
493
|
+
if (!manifest || manifest.schema_version !== 'brainbase-judgment-runtime-v1' || typeof manifest.runtime_version !== 'string' || !manifest.runtime_version)
|
|
494
|
+
throw new TypeError('judgment manifest schema is invalid');
|
|
495
|
+
if (!Array.isArray(manifest.host_bindings) || !Array.isArray(manifest.policies) || !Array.isArray(manifest.nodes) || !Array.isArray(manifest.dags) || !Array.isArray(manifest.composition_edges))
|
|
496
|
+
throw new TypeError('judgment manifest collections are invalid');
|
|
497
|
+
const hostIds = new Set();
|
|
498
|
+
for (const host of manifest.host_bindings) {
|
|
499
|
+
if (!host || !ADAPTER_ID_PATTERN.test(host.adapter_id) || !ADAPTER_VERSION_PATTERN.test(host.adapter_version))
|
|
500
|
+
throw new TypeError('judgment manifest host binding is invalid');
|
|
501
|
+
const key = `${host.adapter_id}:${host.adapter_version}`;
|
|
502
|
+
if (hostIds.has(key))
|
|
503
|
+
throw new TypeError('judgment manifest has duplicate host binding');
|
|
504
|
+
hostIds.add(key);
|
|
505
|
+
}
|
|
506
|
+
const policies = new Set();
|
|
507
|
+
manifest.policies.forEach((policy) => validatePolicy(policy, policies));
|
|
508
|
+
const nodes = new Set();
|
|
509
|
+
for (const node of manifest.nodes) {
|
|
510
|
+
if (!node || typeof node.id !== 'string' || !node.id || nodes.has(node.id))
|
|
511
|
+
throw new TypeError('judgment manifest has duplicate or invalid node id');
|
|
512
|
+
nodes.add(node.id);
|
|
513
|
+
if (node.execution_contract !== undefined && node.execution_contract !== 'judgment-node-evidence-v1')
|
|
514
|
+
throw new TypeError(`judgment node ${node.id} execution contract is invalid`);
|
|
515
|
+
if (!NODE_KINDS.has(node.kind))
|
|
516
|
+
throw new TypeError(`judgment node ${node.id} kind is invalid`);
|
|
517
|
+
if (typeof node.instruction !== 'string' || !node.instruction)
|
|
518
|
+
throw new TypeError(`judgment node ${node.id} instruction is invalid`);
|
|
519
|
+
if (node.required_capability_template !== null && (typeof node.required_capability_template !== 'string' || !node.required_capability_template))
|
|
520
|
+
throw new TypeError(`judgment node ${node.id} capability reference is invalid`);
|
|
521
|
+
if (node.kind === 'capability' && typeof node.required_capability_template !== 'string')
|
|
522
|
+
throw new TypeError(`judgment node ${node.id} capability reference is required`);
|
|
523
|
+
}
|
|
524
|
+
const dags = new Set();
|
|
525
|
+
for (const dag of manifest.dags) {
|
|
526
|
+
if (!dag || typeof dag.id !== 'string' || dags.has(dag.id) || !Array.isArray(dag.path) || dag.path.length === 0)
|
|
527
|
+
throw new TypeError('judgment manifest has duplicate, invalid, or empty DAG');
|
|
528
|
+
dags.add(dag.id);
|
|
529
|
+
if (!DAG_KINDS.has(dag.kind) || !Array.isArray(dag.policy_ids))
|
|
530
|
+
throw new TypeError(`judgment DAG ${dag.id} schema is invalid`);
|
|
531
|
+
if (new Set(dag.path).size !== dag.path.length)
|
|
532
|
+
throw new TypeError(`judgment DAG ${dag.id} contains a cycle`);
|
|
533
|
+
if (new Set(dag.policy_ids).size !== dag.policy_ids.length)
|
|
534
|
+
throw new TypeError(`judgment DAG ${dag.id} contains duplicate policy references`);
|
|
535
|
+
for (const nodeId of dag.path)
|
|
536
|
+
if (!nodes.has(nodeId))
|
|
537
|
+
throw new TypeError(`judgment DAG ${dag.id} references missing node ${nodeId}`);
|
|
538
|
+
for (const policyId of dag.policy_ids)
|
|
539
|
+
if (!policies.has(policyId))
|
|
540
|
+
throw new TypeError(`judgment DAG ${dag.id} references missing policy ${policyId}`);
|
|
541
|
+
}
|
|
542
|
+
const compositionEdges = new Set();
|
|
543
|
+
for (const edge of manifest.composition_edges) {
|
|
544
|
+
if (!Array.isArray(edge) || edge.length !== 2 || edge.some((nodeId) => typeof nodeId !== 'string' || !nodes.has(nodeId))) {
|
|
545
|
+
throw new TypeError('judgment composition edge references a missing or invalid node');
|
|
546
|
+
}
|
|
547
|
+
if (edge[0] === edge[1])
|
|
548
|
+
throw new TypeError('judgment composition edge cannot reference itself');
|
|
549
|
+
const key = `${edge[0]}\u0000${edge[1]}`;
|
|
550
|
+
if (compositionEdges.has(key))
|
|
551
|
+
throw new TypeError('judgment manifest has duplicate composition edge');
|
|
552
|
+
compositionEdges.add(key);
|
|
553
|
+
}
|
|
554
|
+
const selectors = manifest.selectors;
|
|
555
|
+
validateStringTerms(selectors?.domain_order, 'judgment domain order');
|
|
556
|
+
validateStringTerms(selectors?.signal_order, 'judgment signal order');
|
|
557
|
+
if (new Set(selectors.domain_order).size !== selectors.domain_order.length || selectors.domain_order.length !== DOMAINS.size || selectors.domain_order.some((value) => !DOMAINS.has(value)))
|
|
558
|
+
throw new TypeError('judgment domain order is invalid');
|
|
559
|
+
if (new Set(selectors.signal_order).size !== selectors.signal_order.length || selectors.signal_order.length !== SIGNALS.size || selectors.signal_order.some((value) => !SIGNALS.has(value)))
|
|
560
|
+
throw new TypeError('judgment signal order is invalid');
|
|
561
|
+
validateExactKeys(selectors.domain_dags, DOMAINS, 'judgment domain selectors');
|
|
562
|
+
validateExactKeys(selectors.signal_dags, SIGNALS, 'judgment signal selectors');
|
|
563
|
+
const selectorDagIds = [
|
|
564
|
+
...Object.values(selectors.domain_dags),
|
|
565
|
+
...Object.values(selectors.signal_dags),
|
|
566
|
+
selectors.authority_dag,
|
|
567
|
+
selectors.clarification_dag
|
|
568
|
+
];
|
|
569
|
+
if (Object.hasOwn(selectors, 'engineering_implementation_dag'))
|
|
570
|
+
selectorDagIds.push(selectors.engineering_implementation_dag);
|
|
571
|
+
for (const dagId of selectorDagIds)
|
|
572
|
+
if (!dags.has(dagId))
|
|
573
|
+
throw new TypeError(`judgment selector references missing DAG ${dagId}`);
|
|
574
|
+
const matchers = manifest.semantic_matchers;
|
|
575
|
+
validateExactKeys(matchers?.intents, INTENT_MATCHERS, 'judgment intent matchers');
|
|
576
|
+
validateExactKeys(matchers?.domains, DOMAIN_MATCHERS, 'judgment domain matchers');
|
|
577
|
+
validateExactKeys(matchers?.signals, SIGNALS, 'judgment signal matchers');
|
|
578
|
+
validateExactKeys(matchers?.safety, SAFETY_MATCHERS, 'judgment safety matchers');
|
|
579
|
+
for (const [key, terms] of Object.entries(matchers.intents))
|
|
580
|
+
validateStringTerms(terms, `judgment intent matcher ${key}`);
|
|
581
|
+
for (const [key, terms] of Object.entries(matchers.domains))
|
|
582
|
+
validateStringTerms(terms, `judgment domain matcher ${key}`);
|
|
583
|
+
for (const [key, terms] of Object.entries(matchers.signals))
|
|
584
|
+
validateStringTerms(terms, `judgment signal matcher ${key}`);
|
|
585
|
+
for (const [key, terms] of Object.entries(matchers.safety))
|
|
586
|
+
validateStringTerms(terms, `judgment safety matcher ${key}`);
|
|
587
|
+
validateStringTerms(matchers.positive_commands, 'judgment positive command matcher');
|
|
588
|
+
validateStringTerms(matchers.follow_up, 'judgment follow-up matcher');
|
|
589
|
+
const autonomy = manifest.autonomy;
|
|
590
|
+
if (!autonomy || autonomy.schema_version !== 'brainbase-autonomy-policy-v1')
|
|
591
|
+
throw new TypeError('judgment autonomy policy is invalid');
|
|
592
|
+
validateStringTerms(autonomy.continue_risks, 'judgment autonomy continue risks');
|
|
593
|
+
validateStringTerms(autonomy.escalate_risks, 'judgment autonomy escalate risks', { allowEmpty: true });
|
|
594
|
+
validateStringTerms(autonomy.escalate_action_kinds, 'judgment autonomy escalate action kinds', { allowEmpty: true });
|
|
595
|
+
validateStringTerms(autonomy.runtime_escalation_reasons, 'judgment autonomy runtime escalation reasons');
|
|
596
|
+
// escalate_risks / escalate_action_kinds are no longer consulted by autonomyResolution
|
|
597
|
+
// (classification alone never auto-escalates); they stay schema-validated so a stale
|
|
598
|
+
// manifest cannot silently reintroduce a hidden auto-escalation source.
|
|
599
|
+
if (canonicalJson(autonomy.continue_risks) !== canonicalJson(['low', 'medium'])
|
|
600
|
+
|| autonomy.escalate_risks.some((risk) => !RISKS.includes(risk))
|
|
601
|
+
|| autonomy.escalate_action_kinds.some((kind) => !ACTIONS.includes(kind))
|
|
602
|
+
|| autonomy.runtime_escalation_reasons.length !== RUNTIME_ESCALATION_REASONS.size
|
|
603
|
+
|| autonomy.runtime_escalation_reasons.some((reason) => !RUNTIME_ESCALATION_REASONS.has(reason))) {
|
|
604
|
+
throw new TypeError('judgment autonomy policy boundary is invalid');
|
|
605
|
+
}
|
|
606
|
+
validateSelectableGraphs(manifest);
|
|
607
|
+
const digest = sha256Hex(canonicalJson(manifest));
|
|
608
|
+
validateManifestLock(lock, null, { runtimeVersion: manifest.runtime_version, manifestDigest: digest });
|
|
609
|
+
return digest;
|
|
610
|
+
}
|
|
611
|
+
// Policies whose `human_approval` rule matches this turn's classification, in
|
|
612
|
+
// deterministic policy-id order. A non-empty result is the only source of a
|
|
613
|
+
// risk_or_external escalation now — classification alone never auto-escalates.
|
|
614
|
+
function humanApprovalPolicyIds(classification, applicablePolicies) {
|
|
615
|
+
const matched = [];
|
|
616
|
+
for (const policy of applicablePolicies) {
|
|
617
|
+
const rule = policy?.human_approval;
|
|
618
|
+
if (!rule)
|
|
619
|
+
continue;
|
|
620
|
+
const riskMatch = Array.isArray(rule.risks) && rule.risks.includes(classification.risk);
|
|
621
|
+
const actionMatch = Array.isArray(rule.action_kinds) && rule.action_kinds.includes(classification.action_kind);
|
|
622
|
+
if (riskMatch || actionMatch)
|
|
623
|
+
matched.push(policy.id);
|
|
624
|
+
}
|
|
625
|
+
return matched.sort(compareCodePoints);
|
|
626
|
+
}
|
|
627
|
+
export function autonomyResolution(status, classification, manifest, applicablePolicies = []) {
|
|
628
|
+
let decision;
|
|
629
|
+
let reasonCode;
|
|
630
|
+
let policyIds = [];
|
|
631
|
+
if (status === 'needs_classification') {
|
|
632
|
+
decision = 'escalate';
|
|
633
|
+
reasonCode = 'classification_missing';
|
|
634
|
+
}
|
|
635
|
+
else if (status === 'needs_policy_resolution') {
|
|
636
|
+
decision = 'escalate';
|
|
637
|
+
reasonCode = 'policy_conflict';
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
policyIds = humanApprovalPolicyIds(classification, applicablePolicies);
|
|
641
|
+
if (policyIds.length > 0) {
|
|
642
|
+
decision = 'escalate';
|
|
643
|
+
reasonCode = 'risk_or_external';
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
decision = 'continue';
|
|
647
|
+
reasonCode = 'routine_in_scope';
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (!AUTONOMY_DECISIONS.has(decision) || !AUTONOMY_REASON_CODES.has(reasonCode)) {
|
|
651
|
+
throw new TypeError('judgment autonomy resolution is invalid');
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
autonomy_decision: decision,
|
|
655
|
+
autonomy_reason_code: reasonCode,
|
|
656
|
+
allowed_runtime_escalation_reasons: decision === 'continue'
|
|
657
|
+
? [...manifest.autonomy.runtime_escalation_reasons]
|
|
658
|
+
: [],
|
|
659
|
+
autonomy_policy_ids: policyIds
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
export function validateRuntimeInput(rawInput, manifest) {
|
|
663
|
+
exactFields(rawInput, INPUT_FIELDS, 'input');
|
|
664
|
+
const request = requiredText(rawInput.request, 'request');
|
|
665
|
+
const turnId = requiredString(rawInput.turn_id, 'turn_id');
|
|
666
|
+
if (turnId.length > 128 || /[\u0000-\u001f\u007f]/u.test(turnId))
|
|
667
|
+
fail('turn_id is invalid');
|
|
668
|
+
if (rawInput.conversation_context === undefined) {
|
|
669
|
+
fail('conversation_context is required');
|
|
670
|
+
}
|
|
671
|
+
let conversationContext = null;
|
|
672
|
+
{
|
|
673
|
+
exactFields(rawInput.conversation_context, CONVERSATION_CONTEXT_FIELDS, 'conversation_context');
|
|
674
|
+
if (rawInput.conversation_context.schema_version !== 'brainbase-conversation-context-v1') {
|
|
675
|
+
fail('conversation_context.schema_version is invalid');
|
|
676
|
+
}
|
|
677
|
+
const messages = rawInput.conversation_context.messages;
|
|
678
|
+
if (!Array.isArray(messages))
|
|
679
|
+
fail('conversation_context.messages must be an array');
|
|
680
|
+
const validatedMessages = messages.map((message, index) => {
|
|
681
|
+
exactFields(message, MESSAGE_FIELDS, `conversation_context.messages[${index}]`);
|
|
682
|
+
if (message.sequence !== index)
|
|
683
|
+
fail('conversation_context.messages sequence is invalid');
|
|
684
|
+
if (!['user', 'assistant'].includes(message.role))
|
|
685
|
+
fail('conversation_context.messages role is invalid');
|
|
686
|
+
if (message.turn_id !== null && (typeof message.turn_id !== 'string' || !message.turn_id))
|
|
687
|
+
fail('conversation_context.messages turn_id is invalid');
|
|
688
|
+
if (message.phase !== null && (typeof message.phase !== 'string' || !message.phase))
|
|
689
|
+
fail('conversation_context.messages phase is invalid');
|
|
690
|
+
return {
|
|
691
|
+
sequence: index,
|
|
692
|
+
turn_id: message.turn_id,
|
|
693
|
+
role: message.role,
|
|
694
|
+
phase: message.phase,
|
|
695
|
+
text: requiredText(message.text, `conversation_context.messages[${index}].text`)
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
const priorReceipts = rawInput.conversation_context.prior_receipts;
|
|
699
|
+
if (!Array.isArray(priorReceipts))
|
|
700
|
+
fail('conversation_context.prior_receipts must be an array');
|
|
701
|
+
const validatedPriorReceipts = priorReceipts.map((receipt, index) => {
|
|
702
|
+
exactFields(receipt, PRIOR_RECEIPT_FIELDS, `conversation_context.prior_receipts[${index}]`);
|
|
703
|
+
const digestFields = ['request_digest', 'plan_digest'];
|
|
704
|
+
for (const field of digestFields)
|
|
705
|
+
if (!/^[a-f0-9]{64}$/u.test(String(receipt[field])))
|
|
706
|
+
fail(`conversation_context.prior_receipts[${index}].${field} is invalid`);
|
|
707
|
+
if (receipt.context_digest !== null && !/^[a-f0-9]{64}$/u.test(String(receipt.context_digest)))
|
|
708
|
+
fail(`conversation_context.prior_receipts[${index}].context_digest is invalid`);
|
|
709
|
+
return {
|
|
710
|
+
turn_id: requiredString(receipt.turn_id, `conversation_context.prior_receipts[${index}].turn_id`),
|
|
711
|
+
resolution_id: requiredString(receipt.resolution_id, `conversation_context.prior_receipts[${index}].resolution_id`),
|
|
712
|
+
request_digest: receipt.request_digest,
|
|
713
|
+
context_digest: receipt.context_digest,
|
|
714
|
+
plan_digest: receipt.plan_digest,
|
|
715
|
+
classification: validatedClassification(receipt.classification, `conversation_context.prior_receipts[${index}].classification`, manifest),
|
|
716
|
+
selected_dag_ids: stringArray(receipt.selected_dag_ids, `conversation_context.prior_receipts[${index}].selected_dag_ids`)
|
|
717
|
+
};
|
|
718
|
+
});
|
|
719
|
+
exactFields(rawInput.conversation_context.runtime, RUNTIME_FIELDS, 'conversation_context.runtime');
|
|
720
|
+
const runtime = rawInput.conversation_context.runtime;
|
|
721
|
+
if (!RUNTIME_HOSTS.has(runtime.host))
|
|
722
|
+
fail('conversation_context.runtime.host is invalid');
|
|
723
|
+
if (runtime.entrypoint !== undefined && !RUNTIME_ENTRYPOINTS.has(runtime.entrypoint)) {
|
|
724
|
+
fail('conversation_context.runtime.entrypoint is invalid');
|
|
725
|
+
}
|
|
726
|
+
for (const nullable of ['model', 'permission_mode', 'project_binding']) {
|
|
727
|
+
if (runtime[nullable] !== null && (typeof runtime[nullable] !== 'string' || !runtime[nullable]))
|
|
728
|
+
fail(`conversation_context.runtime.${nullable} is invalid`);
|
|
729
|
+
}
|
|
730
|
+
const instructionBindings = rawInput.conversation_context.instruction_bindings;
|
|
731
|
+
if (!Array.isArray(instructionBindings))
|
|
732
|
+
fail('conversation_context.instruction_bindings must be an array');
|
|
733
|
+
const validatedInstructionBindings = instructionBindings.map((binding, index) => {
|
|
734
|
+
exactFields(binding, INSTRUCTION_BINDING_FIELDS, `conversation_context.instruction_bindings[${index}]`);
|
|
735
|
+
if (!/^[a-f0-9]{64}$/u.test(String(binding.digest)))
|
|
736
|
+
fail(`conversation_context.instruction_bindings[${index}].digest is invalid`);
|
|
737
|
+
return {
|
|
738
|
+
scope: requiredString(binding.scope, `conversation_context.instruction_bindings[${index}].scope`),
|
|
739
|
+
source_ref: requiredString(binding.source_ref, `conversation_context.instruction_bindings[${index}].source_ref`),
|
|
740
|
+
digest: binding.digest
|
|
741
|
+
};
|
|
742
|
+
});
|
|
743
|
+
if (!['complete', 'partial'].includes(rawInput.conversation_context.completeness))
|
|
744
|
+
fail('conversation_context.completeness is invalid');
|
|
745
|
+
if (!/^[a-f0-9]{64}$/u.test(String(rawInput.conversation_context.session_ref)))
|
|
746
|
+
fail('conversation_context.session_ref is invalid');
|
|
747
|
+
if (!/^[a-f0-9]{64}$/u.test(String(rawInput.conversation_context.source_digest)))
|
|
748
|
+
fail('conversation_context.source_digest is invalid');
|
|
749
|
+
const currentMessages = validatedMessages.filter((message) => message.turn_id === turnId);
|
|
750
|
+
const currentMessage = validatedMessages.at(-1);
|
|
751
|
+
if (currentMessages.length !== 1 || currentMessage?.role !== 'user' || currentMessage.turn_id !== turnId || currentMessage.text !== request) {
|
|
752
|
+
fail('conversation_context must end with the exact current request exactly once');
|
|
753
|
+
}
|
|
754
|
+
const { source_digest: _sourceDigest, ...sourceContext } = rawInput.conversation_context;
|
|
755
|
+
if (sha256Hex(canonicalJson(sourceContext)) !== rawInput.conversation_context.source_digest) {
|
|
756
|
+
fail('conversation_context.source_digest does not match canonical context');
|
|
757
|
+
}
|
|
758
|
+
if (rawInput.project_code !== undefined && runtime.project_binding !== rawInput.project_code) {
|
|
759
|
+
fail('conversation_context.runtime.project_binding must match project_code');
|
|
760
|
+
}
|
|
761
|
+
const normalizedRuntime = {
|
|
762
|
+
host: runtime.host,
|
|
763
|
+
model: runtime.model,
|
|
764
|
+
permission_mode: runtime.permission_mode,
|
|
765
|
+
project_binding: runtime.project_binding
|
|
766
|
+
};
|
|
767
|
+
if (runtime.entrypoint !== undefined)
|
|
768
|
+
normalizedRuntime.entrypoint = runtime.entrypoint;
|
|
769
|
+
conversationContext = {
|
|
770
|
+
schema_version: rawInput.conversation_context.schema_version,
|
|
771
|
+
session_ref: rawInput.conversation_context.session_ref,
|
|
772
|
+
messages: validatedMessages,
|
|
773
|
+
prior_receipts: validatedPriorReceipts,
|
|
774
|
+
runtime: normalizedRuntime,
|
|
775
|
+
instruction_bindings: validatedInstructionBindings,
|
|
776
|
+
completeness: rawInput.conversation_context.completeness,
|
|
777
|
+
source_digest: rawInput.conversation_context.source_digest
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
request,
|
|
782
|
+
turn_id: turnId,
|
|
783
|
+
project_code: rawInput.project_code === undefined ? null : requiredString(rawInput.project_code, 'project_code'),
|
|
784
|
+
conversation_context: conversationContext,
|
|
785
|
+
model_interpretation: rawInput.model_interpretation === undefined
|
|
786
|
+
? null
|
|
787
|
+
: validatedClassification(rawInput.model_interpretation, 'model_interpretation', manifest)
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
export function reconcileModelInterpretation(input, manifest) {
|
|
791
|
+
if (!input.model_interpretation) {
|
|
792
|
+
return {
|
|
793
|
+
status: 'needs_classification', classification: null, assurance: 'unknown',
|
|
794
|
+
reasons: ['model_interpretation_missing'],
|
|
795
|
+
evidence: { source: 'resolver', source_turn_ids: [], matcher_ids: [] }
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// Host matchers are monotonic safety rails: they may only add risk,
|
|
799
|
+
// action floors, signals, and capabilities. They never replace or weaken
|
|
800
|
+
// the model's semantic interpretation.
|
|
801
|
+
const hardSignals = classify(input, manifest);
|
|
802
|
+
const hardClassification = hardSignals.classification;
|
|
803
|
+
const hardIsSpecific = hardClassification
|
|
804
|
+
&& !(hardClassification.intent === 'answer' && hardClassification.domains.length === 1 && hardClassification.domains[0] === 'general');
|
|
805
|
+
const classification = {
|
|
806
|
+
...input.model_interpretation,
|
|
807
|
+
intent: input.model_interpretation.intent,
|
|
808
|
+
domains: hardIsSpecific
|
|
809
|
+
? (input.model_interpretation.domains.length === 1 && input.model_interpretation.domains[0] === 'general'
|
|
810
|
+
? hardClassification.domains
|
|
811
|
+
: sortByOrder([...new Set([...input.model_interpretation.domains, ...hardClassification.domains])], manifest.selectors.domain_order))
|
|
812
|
+
: input.model_interpretation.domains,
|
|
813
|
+
action_kind: hardClassification
|
|
814
|
+
? indexFloor(ACTIONS, input.model_interpretation.action_kind, hardClassification.action_kind)
|
|
815
|
+
: input.model_interpretation.action_kind,
|
|
816
|
+
risk: hardClassification
|
|
817
|
+
? indexFloor(RISKS, input.model_interpretation.risk, hardClassification.risk)
|
|
818
|
+
: input.model_interpretation.risk,
|
|
819
|
+
signals: sortByOrder([
|
|
820
|
+
...new Set([
|
|
821
|
+
...input.model_interpretation.signals,
|
|
822
|
+
...(hardClassification?.signals ?? [])
|
|
823
|
+
])
|
|
824
|
+
], manifest.selectors.signal_order)
|
|
825
|
+
};
|
|
826
|
+
// Project discovery belongs to knowledge retrieval. An unbound task still
|
|
827
|
+
// has a valid semantic classification; this receipt grants no data access.
|
|
828
|
+
return {
|
|
829
|
+
status: 'resolved',
|
|
830
|
+
classification,
|
|
831
|
+
assurance: classification.confidence === 'confirmed' ? 'verified' : 'bounded',
|
|
832
|
+
reasons: hardSignals.reasons,
|
|
833
|
+
evidence: {
|
|
834
|
+
source: 'current_request', source_turn_ids: [input.turn_id],
|
|
835
|
+
matcher_ids: [...new Set(['model_interpretation', ...(hardSignals.evidence?.matcher_ids ?? [])])]
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
function matchingKeys(text, matchers, order) {
|
|
840
|
+
return order.filter((key) => includesTerm(text, matchers[key]));
|
|
841
|
+
}
|
|
842
|
+
// Write/operate intent matchers must describe a requested command. State or
|
|
843
|
+
// temporal mentions still remain available to the independent safety floors.
|
|
844
|
+
function intentCommandTerms(terms) {
|
|
845
|
+
return terms.flatMap((term) => {
|
|
846
|
+
if (/^[a-z0-9_.-]+$/iu.test(term))
|
|
847
|
+
return [term];
|
|
848
|
+
if (/[、,;;。!?!?]$/u.test(term) || /(?:して|って|んで|いて|せよ|しろ)$/u.test(term)) {
|
|
849
|
+
return [term];
|
|
850
|
+
}
|
|
851
|
+
return [`${term}して`, `${term}しろ`, `${term}せよ`];
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function includesRequestedIntentTerm(request, terms) {
|
|
855
|
+
return includesPositiveCommandClause(request, intentCommandTerms(terms));
|
|
856
|
+
}
|
|
857
|
+
function matchingIntent(text, manifest) {
|
|
858
|
+
return INTENT_ORDER.find((intent) => {
|
|
859
|
+
const terms = manifest.semantic_matchers.intents[intent];
|
|
860
|
+
return ['implement', 'operate'].includes(intent)
|
|
861
|
+
? includesRequestedIntentTerm(text, terms)
|
|
862
|
+
: includesTerm(text, terms);
|
|
863
|
+
}) || null;
|
|
864
|
+
}
|
|
865
|
+
function includesFollowUpTerm(text, terms) {
|
|
866
|
+
const normalized = text.toLocaleLowerCase('ja');
|
|
867
|
+
return terms.some((term) => {
|
|
868
|
+
const normalizedTerm = term.toLocaleLowerCase('ja');
|
|
869
|
+
if (normalizedTerm === 'これ' || normalizedTerm === 'こちら') {
|
|
870
|
+
return new RegExp(`${escapeRegExp(normalizedTerm)}(?:は(?:[??]|どう|何|どれ|どこ|いつ|誰|なぜ)|(?!は))`, 'u').test(normalized);
|
|
871
|
+
}
|
|
872
|
+
if (normalizedTerm === 'では') {
|
|
873
|
+
return /^(?:では)(?:[、,\s]|$)/u.test(normalized.trimStart());
|
|
874
|
+
}
|
|
875
|
+
return includesTerm(normalized, [normalizedTerm]);
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
function classificationFromPriorContext(input, manifest) {
|
|
879
|
+
const context = input.conversation_context;
|
|
880
|
+
if (!context)
|
|
881
|
+
return null;
|
|
882
|
+
const priorReceipts = [...context.prior_receipts].reverse().filter((receipt) => receipt.turn_id !== input.turn_id);
|
|
883
|
+
const domainReceipt = priorReceipts.find((receipt) => receipt.classification.domains.some((domain) => domain !== 'general'));
|
|
884
|
+
if (domainReceipt)
|
|
885
|
+
return { classification: domainReceipt.classification, turn_ids: [domainReceipt.turn_id], source: 'prior_receipt' };
|
|
886
|
+
for (let index = context.messages.length - 1; index >= 0; index -= 1) {
|
|
887
|
+
const message = context.messages[index];
|
|
888
|
+
if (message.role !== 'user' || message.turn_id === input.turn_id)
|
|
889
|
+
continue;
|
|
890
|
+
const domains = matchingKeys(message.text, manifest.semantic_matchers.domains, manifest.selectors.domain_order.filter((domain) => domain !== 'general'));
|
|
891
|
+
if (domains.length === 0)
|
|
892
|
+
continue;
|
|
893
|
+
const signals = matchingKeys(message.text, manifest.semantic_matchers.signals, manifest.selectors.signal_order);
|
|
894
|
+
const intent = matchingIntent(message.text, manifest) || 'answer';
|
|
895
|
+
return {
|
|
896
|
+
classification: {
|
|
897
|
+
intent,
|
|
898
|
+
domains,
|
|
899
|
+
action_kind: actionFloor(intent),
|
|
900
|
+
risk: actionFloor(intent) === 'write' ? 'medium' : 'low',
|
|
901
|
+
confidence: 'inferred',
|
|
902
|
+
signals
|
|
903
|
+
},
|
|
904
|
+
turn_ids: message.turn_id ? [message.turn_id] : [],
|
|
905
|
+
source: 'prior_message'
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
const fallbackReceipt = priorReceipts[0];
|
|
909
|
+
if (fallbackReceipt)
|
|
910
|
+
return { classification: fallbackReceipt.classification, turn_ids: [fallbackReceipt.turn_id], source: 'prior_receipt' };
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
export function classify(input, manifest) {
|
|
914
|
+
const matchers = manifest.semantic_matchers;
|
|
915
|
+
const request = positiveClassificationRequest(classificationRequest(input.request), manifest);
|
|
916
|
+
const detectedDomains = matchingKeys(request, matchers.domains, manifest.selectors.domain_order.filter((domain) => domain !== 'general'));
|
|
917
|
+
const detectedSignals = matchingKeys(request, matchers.signals, manifest.selectors.signal_order);
|
|
918
|
+
const detectedIntent = matchingIntent(request, manifest);
|
|
919
|
+
const followsPrior = includesFollowUpTerm(request, matchers.follow_up);
|
|
920
|
+
const prior = followsPrior ? classificationFromPriorContext(input, manifest) : null;
|
|
921
|
+
const inheritedDomains = detectedDomains.length === 0 && prior ? prior.classification.domains.filter((domain) => domain !== 'general') : [];
|
|
922
|
+
const inheritedSignals = detectedSignals.length === 0 && prior ? prior.classification.signals : [];
|
|
923
|
+
const domains = detectedDomains.length > 0
|
|
924
|
+
? detectedDomains
|
|
925
|
+
: inheritedDomains.length > 0
|
|
926
|
+
? inheritedDomains
|
|
927
|
+
: followsPrior && !prior
|
|
928
|
+
? []
|
|
929
|
+
: ['general'];
|
|
930
|
+
const signals = detectedSignals.length > 0 ? detectedSignals : inheritedSignals;
|
|
931
|
+
const intent = detectedIntent || (prior ? prior.classification.intent : followsPrior ? null : 'answer');
|
|
932
|
+
if (!intent || domains.length === 0) {
|
|
933
|
+
return {
|
|
934
|
+
status: 'needs_classification', classification: null, assurance: 'unknown',
|
|
935
|
+
reasons: ['conversation_referent_missing'],
|
|
936
|
+
evidence: { source: 'resolver', source_turn_ids: [], matcher_ids: [] }
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const detectedAction = includesRequestedEffectTerm(request, matchers.safety.external)
|
|
940
|
+
? 'external'
|
|
941
|
+
: includesRequestedEffectTerm(request, matchers.safety.write)
|
|
942
|
+
? 'write'
|
|
943
|
+
: 'none';
|
|
944
|
+
const minimumAction = indexFloor(ACTIONS, actionFloor(intent), detectedAction);
|
|
945
|
+
const minimumRisk = includesUnprohibitedTerm(request, matchers.safety.critical)
|
|
946
|
+
? 'critical'
|
|
947
|
+
: minimumAction === 'external'
|
|
948
|
+
? 'high'
|
|
949
|
+
: minimumAction === 'write'
|
|
950
|
+
? 'medium'
|
|
951
|
+
: 'low';
|
|
952
|
+
const inherited = detectedDomains.length === 0 && Boolean(prior);
|
|
953
|
+
const matcherIds = [
|
|
954
|
+
...(detectedIntent ? [`intent:${detectedIntent}`] : []),
|
|
955
|
+
...detectedDomains.map((domain) => `domain:${domain}`),
|
|
956
|
+
...detectedSignals.map((signal) => `signal:${signal}`),
|
|
957
|
+
...(detectedAction !== 'none' ? [`effect:${detectedAction}`] : [])
|
|
958
|
+
];
|
|
959
|
+
return {
|
|
960
|
+
status: 'resolved',
|
|
961
|
+
classification: {
|
|
962
|
+
intent,
|
|
963
|
+
domains: sortByOrder(domains, manifest.selectors.domain_order),
|
|
964
|
+
action_kind: minimumAction,
|
|
965
|
+
risk: minimumRisk,
|
|
966
|
+
confidence: inherited ? 'inferred' : 'confirmed',
|
|
967
|
+
signals: sortByOrder(signals, manifest.selectors.signal_order)
|
|
968
|
+
},
|
|
969
|
+
assurance: inherited ? 'bounded' : 'verified',
|
|
970
|
+
reasons: inherited ? ['classification_inherited_from_prior_turn'] : [],
|
|
971
|
+
evidence: {
|
|
972
|
+
source: inherited ? prior.source : 'current_request',
|
|
973
|
+
source_turn_ids: inherited ? prior.turn_ids : [input.turn_id],
|
|
974
|
+
matcher_ids: matcherIds
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function policyScopeId(policy, ownerPersonId) {
|
|
979
|
+
return policy.scope.id === '$personal_owner' ? ownerPersonId : policy.scope.id;
|
|
980
|
+
}
|
|
981
|
+
export function materializePolicy(policy, ownerPersonId) {
|
|
982
|
+
return {
|
|
983
|
+
...policy,
|
|
984
|
+
scope: { ...policy.scope, id: policyScopeId(policy, ownerPersonId) },
|
|
985
|
+
owner_person_id: policy.owner_person_id === '$personal_owner' ? ownerPersonId : policy.owner_person_id
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function policyComparator(left, right) {
|
|
989
|
+
if (left.strength !== right.strength)
|
|
990
|
+
return left.strength === 'hard' ? -1 : 1;
|
|
991
|
+
if (left.priority !== right.priority)
|
|
992
|
+
return right.priority - left.priority;
|
|
993
|
+
const scopeDelta = SCOPE_SPECIFICITY[right.scope.type] - SCOPE_SPECIFICITY[left.scope.type];
|
|
994
|
+
if (scopeDelta !== 0)
|
|
995
|
+
return scopeDelta;
|
|
996
|
+
return compareCodePoints(left.id, right.id);
|
|
997
|
+
}
|
|
998
|
+
export function mergePolicies(policies) {
|
|
999
|
+
const sorted = [...policies].sort(policyComparator);
|
|
1000
|
+
const applicable = [];
|
|
1001
|
+
const suppressed = [];
|
|
1002
|
+
let conflict = false;
|
|
1003
|
+
const byTarget = new Map();
|
|
1004
|
+
for (const policy of sorted) {
|
|
1005
|
+
const target = policy.effect.target;
|
|
1006
|
+
if (!byTarget.has(target))
|
|
1007
|
+
byTarget.set(target, []);
|
|
1008
|
+
byTarget.get(target).push(policy);
|
|
1009
|
+
}
|
|
1010
|
+
for (const group of byTarget.values()) {
|
|
1011
|
+
const hard = group.filter((policy) => policy.strength === 'hard');
|
|
1012
|
+
const soft = group.filter((policy) => policy.strength === 'soft');
|
|
1013
|
+
let winningHard = hard;
|
|
1014
|
+
const decisions = new Set(hard.map((policy) => policy.effect.decision));
|
|
1015
|
+
if (decisions.has('require') && decisions.has('forbid')) {
|
|
1016
|
+
const winner = hard[0];
|
|
1017
|
+
const tied = hard.filter((policy) => policy.priority === winner.priority && SCOPE_SPECIFICITY[policy.scope.type] === SCOPE_SPECIFICITY[winner.scope.type]);
|
|
1018
|
+
if (new Set(tied.map((policy) => policy.effect.decision)).size > 1) {
|
|
1019
|
+
conflict = true;
|
|
1020
|
+
winningHard = tied;
|
|
1021
|
+
for (const policy of hard.filter((candidate) => !tied.includes(candidate))) {
|
|
1022
|
+
suppressed.push({
|
|
1023
|
+
policy_id: policy.id,
|
|
1024
|
+
suppressed_by_policy_id: winner.id,
|
|
1025
|
+
reason: policy.priority < winner.priority ? 'lower_priority' : 'lower_specificity'
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
winningHard = [winner];
|
|
1031
|
+
for (const policy of hard.slice(1)) {
|
|
1032
|
+
suppressed.push({
|
|
1033
|
+
policy_id: policy.id,
|
|
1034
|
+
suppressed_by_policy_id: winner.id,
|
|
1035
|
+
reason: policy.priority < winner.priority ? 'lower_priority' : 'lower_specificity'
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
applicable.push(...winningHard);
|
|
1041
|
+
if (hard.length > 0) {
|
|
1042
|
+
for (const policy of soft)
|
|
1043
|
+
suppressed.push({ policy_id: policy.id, suppressed_by_policy_id: hard[0].id, reason: 'hard_over_soft' });
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
applicable.push(...soft);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return { applicable: applicable.sort(policyComparator), suppressed, conflict };
|
|
1050
|
+
}
|
|
1051
|
+
export function selectedDags(classification, manifest) {
|
|
1052
|
+
const selected = [];
|
|
1053
|
+
for (const domain of classification.domains) {
|
|
1054
|
+
if (classification.intent === 'answer'
|
|
1055
|
+
&& ['none', 'read'].includes(classification.action_kind)
|
|
1056
|
+
&& classification.risk === 'low'
|
|
1057
|
+
&& ['engineering', 'operations'].includes(domain)) {
|
|
1058
|
+
selected.push(manifest.selectors.domain_dags.general);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
const implementationDag = domain === 'engineering' && classification.intent === 'implement'
|
|
1062
|
+
? manifest.selectors.engineering_implementation_dag : null;
|
|
1063
|
+
selected.push(implementationDag ?? manifest.selectors.domain_dags[domain]);
|
|
1064
|
+
}
|
|
1065
|
+
for (const signal of classification.signals)
|
|
1066
|
+
selected.push(manifest.selectors.signal_dags[signal]);
|
|
1067
|
+
if (['high', 'critical'].includes(classification.risk) || ['write', 'external'].includes(classification.action_kind) || classification.signals.includes('authority_boundary')) {
|
|
1068
|
+
selected.push(manifest.selectors.authority_dag);
|
|
1069
|
+
}
|
|
1070
|
+
return [...new Set(selected)];
|
|
1071
|
+
}
|
|
1072
|
+
export function buildGraph(dagIds, manifest, { clarification = false } = {}) {
|
|
1073
|
+
if (clarification) {
|
|
1074
|
+
return {
|
|
1075
|
+
active_nodes: ['entry', 'reconcile', 'clarification', 'receipt'],
|
|
1076
|
+
active_edges: [['entry', 'reconcile'], ['reconcile', 'clarification'], ['clarification', 'receipt']]
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const dagMap = new Map(manifest.dags.map((dag) => [dag.id, dag]));
|
|
1080
|
+
const activeNodes = ['entry', 'reconcile'];
|
|
1081
|
+
const activeEdges = [['entry', 'reconcile']];
|
|
1082
|
+
const addNode = (node) => { if (!activeNodes.includes(node))
|
|
1083
|
+
activeNodes.push(node); };
|
|
1084
|
+
const addEdge = (from, to) => {
|
|
1085
|
+
if (!activeEdges.some((edge) => edge[0] === from && edge[1] === to))
|
|
1086
|
+
activeEdges.push([from, to]);
|
|
1087
|
+
};
|
|
1088
|
+
for (const dagId of dagIds) {
|
|
1089
|
+
const path = dagMap.get(dagId).path;
|
|
1090
|
+
path.forEach(addNode);
|
|
1091
|
+
addEdge('reconcile', path[0]);
|
|
1092
|
+
for (let index = 1; index < path.length; index += 1)
|
|
1093
|
+
addEdge(path[index - 1], path[index]);
|
|
1094
|
+
addEdge(path.at(-1), 'merge');
|
|
1095
|
+
}
|
|
1096
|
+
addNode('merge');
|
|
1097
|
+
addNode('receipt');
|
|
1098
|
+
addEdge('merge', 'receipt');
|
|
1099
|
+
const activeNodeSet = new Set(activeNodes);
|
|
1100
|
+
for (const [from, to] of manifest.composition_edges) {
|
|
1101
|
+
if (activeNodeSet.has(from) && activeNodeSet.has(to))
|
|
1102
|
+
addEdge(from, to);
|
|
1103
|
+
}
|
|
1104
|
+
const orderedNodes = topologicallySortNodes(activeNodes, activeEdges);
|
|
1105
|
+
return { active_nodes: orderedNodes, active_edges: activeEdges };
|
|
1106
|
+
}
|
|
1107
|
+
export function materializeActiveNodeDefinitions(activeNodes, manifest, { evidenceRequired = true } = {}) {
|
|
1108
|
+
const nodeMap = new Map(manifest.nodes.map((node) => [node.id, node]));
|
|
1109
|
+
return activeNodes.map((nodeId) => {
|
|
1110
|
+
const node = nodeMap.get(nodeId);
|
|
1111
|
+
if (!node)
|
|
1112
|
+
throw new TypeError(`active judgment graph references missing node ${nodeId}`);
|
|
1113
|
+
return {
|
|
1114
|
+
id: node.id,
|
|
1115
|
+
kind: node.kind,
|
|
1116
|
+
instruction: node.instruction,
|
|
1117
|
+
required_capability_template: node.required_capability_template,
|
|
1118
|
+
...(evidenceRequired && ['problem-frame', 'observe', 'falsify'].every((id) => activeNodes.includes(id))
|
|
1119
|
+
&& node.execution_contract ? { execution_contract: node.execution_contract } : {})
|
|
1120
|
+
};
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
export function topologicallySortNodes(nodes, edges) {
|
|
1124
|
+
const indegree = new Map(nodes.map((node) => [node, 0]));
|
|
1125
|
+
const outgoing = new Map(nodes.map((node) => [node, []]));
|
|
1126
|
+
const originalOrder = new Map(nodes.map((node, index) => [node, index]));
|
|
1127
|
+
for (const [from, to] of edges) {
|
|
1128
|
+
if (!indegree.has(from) || !indegree.has(to))
|
|
1129
|
+
throw new TypeError('active judgment graph references a missing node');
|
|
1130
|
+
indegree.set(to, indegree.get(to) + 1);
|
|
1131
|
+
outgoing.get(from).push(to);
|
|
1132
|
+
}
|
|
1133
|
+
const queue = nodes.filter((node) => indegree.get(node) === 0);
|
|
1134
|
+
const ordered = [];
|
|
1135
|
+
while (queue.length > 0) {
|
|
1136
|
+
const node = queue.shift();
|
|
1137
|
+
ordered.push(node);
|
|
1138
|
+
for (const target of outgoing.get(node)) {
|
|
1139
|
+
indegree.set(target, indegree.get(target) - 1);
|
|
1140
|
+
if (indegree.get(target) === 0) {
|
|
1141
|
+
queue.push(target);
|
|
1142
|
+
queue.sort((left, right) => originalOrder.get(left) - originalOrder.get(right));
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (ordered.length !== nodes.length)
|
|
1147
|
+
throw new TypeError('active judgment graph contains a cycle');
|
|
1148
|
+
return ordered;
|
|
1149
|
+
}
|
|
1150
|
+
// ADR-014 F1: every resolved turn selects the philosophy, objectives, and
|
|
1151
|
+
// world models that bear on the response and records how each was used.
|
|
1152
|
+
// Emitted only to clients that declared support, because older MCP receipt
|
|
1153
|
+
// validators reject unknown capabilities and would break every turn.
|
|
1154
|
+
export const JUDGMENT_FRAME_CAPABILITY = 'judgment.frame';
|
|
1155
|
+
const JUDGMENT_FRAME_INTENTS = new Set(['design', 'review', 'operate']);
|
|
1156
|
+
const JUDGMENT_FRAME_DOMAINS = new Set(['personal_judgment', 'organization']);
|
|
1157
|
+
export function isJudgmentBearing(classification) {
|
|
1158
|
+
if (!classification)
|
|
1159
|
+
return false;
|
|
1160
|
+
return JUDGMENT_FRAME_INTENTS.has(classification.intent)
|
|
1161
|
+
|| classification.domains.some((domain) => JUDGMENT_FRAME_DOMAINS.has(domain));
|
|
1162
|
+
}
|
|
1163
|
+
export function frameCapabilities(input, classification, clientCapabilities, status) {
|
|
1164
|
+
if (status !== 'resolved' || !clientCapabilities.has(JUDGMENT_FRAME_CAPABILITY) || !classification)
|
|
1165
|
+
return [];
|
|
1166
|
+
return [{
|
|
1167
|
+
capability: JUDGMENT_FRAME_CAPABILITY,
|
|
1168
|
+
status: 'required',
|
|
1169
|
+
input: {
|
|
1170
|
+
kinds: ['philosophy', 'objective', 'model'],
|
|
1171
|
+
project_code: input.project_code
|
|
1172
|
+
},
|
|
1173
|
+
receipt_required: true
|
|
1174
|
+
}];
|
|
1175
|
+
}
|
|
1176
|
+
export function knowledgeCapabilities(input, classification) {
|
|
1177
|
+
if (!classification?.domains.includes('knowledge'))
|
|
1178
|
+
return [];
|
|
1179
|
+
const contentType = classification.domains.includes('personal_judgment')
|
|
1180
|
+
? 'personal_knowledge'
|
|
1181
|
+
: classification.domains.includes('operations')
|
|
1182
|
+
? 'operational_state'
|
|
1183
|
+
: 'unknown';
|
|
1184
|
+
return [{
|
|
1185
|
+
capability: 'knowledge.resolve',
|
|
1186
|
+
status: 'required',
|
|
1187
|
+
input: {
|
|
1188
|
+
intent: 'lookup',
|
|
1189
|
+
audience: classification.domains.includes('personal_judgment') ? 'personal' : 'team',
|
|
1190
|
+
content_type: contentType,
|
|
1191
|
+
project_code: input.project_code
|
|
1192
|
+
},
|
|
1193
|
+
receipt_required: true
|
|
1194
|
+
}];
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Resolve the deterministic runtime plan from normalized, already-authorized
|
|
1198
|
+
* inputs. `applicablePolicies` must have been filtered by the Org/Host adapter;
|
|
1199
|
+
* this core deliberately has no access or tenant lookup and never broadens a
|
|
1200
|
+
* deny decision.
|
|
1201
|
+
*/
|
|
1202
|
+
export function resolveJudgmentRuntimeCore({ input, manifest, applicablePolicies = [], ownerPersonId = null, clientCapabilities = [] }) {
|
|
1203
|
+
const reconciliation = reconcileModelInterpretation(input, manifest);
|
|
1204
|
+
const needsClassification = reconciliation.status === 'needs_classification';
|
|
1205
|
+
const dagIds = needsClassification
|
|
1206
|
+
? [manifest.selectors.clarification_dag]
|
|
1207
|
+
: selectedDags(reconciliation.classification, manifest);
|
|
1208
|
+
const graph = buildGraph(dagIds, manifest, { clarification: needsClassification });
|
|
1209
|
+
const policies = needsClassification
|
|
1210
|
+
? { applicable: [], suppressed: [], conflict: false }
|
|
1211
|
+
: mergePolicies(applicablePolicies.map((policy) => materializePolicy(policy, ownerPersonId)));
|
|
1212
|
+
const status = needsClassification
|
|
1213
|
+
? 'needs_classification'
|
|
1214
|
+
: policies.conflict
|
|
1215
|
+
? 'needs_policy_resolution'
|
|
1216
|
+
: 'resolved';
|
|
1217
|
+
const autonomy = autonomyResolution(status, reconciliation.classification, manifest, policies.applicable);
|
|
1218
|
+
const activeNodeDefinitions = materializeActiveNodeDefinitions(graph.active_nodes, manifest, {
|
|
1219
|
+
evidenceRequired: !(reconciliation.classification?.intent === 'answer'
|
|
1220
|
+
&& ['none', 'read'].includes(reconciliation.classification?.action_kind)
|
|
1221
|
+
&& reconciliation.classification?.risk === 'low')
|
|
1222
|
+
});
|
|
1223
|
+
return {
|
|
1224
|
+
status,
|
|
1225
|
+
...autonomy,
|
|
1226
|
+
reconciliation,
|
|
1227
|
+
selected_dag_ids: dagIds,
|
|
1228
|
+
applicable_policies: policies.applicable,
|
|
1229
|
+
suppressed_policies: policies.suppressed,
|
|
1230
|
+
policy_conflict: policies.conflict,
|
|
1231
|
+
required_capabilities: status === 'needs_classification' ? [] : [
|
|
1232
|
+
...knowledgeCapabilities(input, reconciliation.classification),
|
|
1233
|
+
...frameCapabilities(input, reconciliation.classification, new Set(clientCapabilities), status)
|
|
1234
|
+
],
|
|
1235
|
+
...graph,
|
|
1236
|
+
active_node_definitions: activeNodeDefinitions,
|
|
1237
|
+
unresolved: status === 'needs_classification'
|
|
1238
|
+
? reconciliation.reasons
|
|
1239
|
+
: status === 'needs_policy_resolution'
|
|
1240
|
+
? ['policy_conflict']
|
|
1241
|
+
: [],
|
|
1242
|
+
rationale: status === 'needs_classification'
|
|
1243
|
+
? ['Model semantic interpretation is required before the server can issue a TurnContract.']
|
|
1244
|
+
: status === 'needs_policy_resolution'
|
|
1245
|
+
? ['Equally authoritative hard policies conflict; resolve policy before proceeding.']
|
|
1246
|
+
: ['The server-owned manifest selected only the judgment branches required by this turn.']
|
|
1247
|
+
};
|
|
1248
|
+
}
|