@aduek/ne-sy 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ir.js ADDED
@@ -0,0 +1,859 @@
1
+ import { createHash } from "node:crypto";
2
+ import { DECISION_RANK, errorCategoryForCode, } from "./types.js";
3
+ import { BOUNDED_SIMULATION_CAPABILITIES, validateCapabilityList, validateRequiredCapabilities, } from "./capabilities.js";
4
+ import { compactRecord, compareStrings, getDotted, isRecord, stableValue } from "./utils.js";
5
+ export const POLICY_IR_HASH_ALGORITHM = "sha256";
6
+ export const ANALYSIS_SEMANTIC_TRACE_VERSION = "analysis-semantics.v0";
7
+ const POLICY_OPERATORS = new Set(["equals", "not_equals", "greater_than", "less_than", "in"]);
8
+ export function normalizePolicyPack(policyInput) {
9
+ if (policyInput === undefined || policyInput === null) {
10
+ return {
11
+ kind: "PolicyPack",
12
+ id: null,
13
+ version: null,
14
+ defaultDecision: "review",
15
+ defaultReason: "No policies were configured for verification.",
16
+ policies: [],
17
+ };
18
+ }
19
+ if (!isRecord(policyInput)) {
20
+ return {
21
+ kind: "PolicyPack",
22
+ id: null,
23
+ version: null,
24
+ defaultDecision: "allow",
25
+ defaultReason: "No policy matched; action allowed by default.",
26
+ policies: [normalizePolicy(policyInput, 0)],
27
+ };
28
+ }
29
+ const policySource = "policies" in policyInput ? policyInput.policies : policyInput;
30
+ const policyItems = Array.isArray(policySource)
31
+ ? policySource
32
+ : isRecord(policySource)
33
+ ? [policySource]
34
+ : [];
35
+ const defaultDecision = String(policyInput.defaultDecision ?? "allow");
36
+ return {
37
+ kind: "PolicyPack",
38
+ id: policyInput.id === undefined ? null : String(policyInput.id),
39
+ version: policyInput.version === undefined ? null : String(policyInput.version),
40
+ defaultDecision,
41
+ defaultReason: String(policyInput.defaultReason ??
42
+ `No policy matched; default decision \`${defaultDecision}\` applied.`),
43
+ policies: policyItems.map((policy, index) => normalizePolicy(policy, index)),
44
+ };
45
+ }
46
+ export function analyzePolicyIr(policyIr) {
47
+ const findings = [];
48
+ const ids = new Map();
49
+ const signatures = new Map();
50
+ const fieldTypes = new Map();
51
+ for (const policy of policyIr.policies) {
52
+ ids.set(policy.id, (ids.get(policy.id) ?? 0) + 1);
53
+ if (!(policy.effect in DECISION_RANK)) {
54
+ findings.push({
55
+ severity: "error",
56
+ code: "unsupported_effect",
57
+ message: `Policy has unsupported effect: ${String(policy.effect)}`,
58
+ policyIds: [policy.id],
59
+ field: "effect",
60
+ });
61
+ }
62
+ findings.push(...conditionFindings(policy.condition, policy.id));
63
+ collectFieldTypes(policy.condition, fieldTypes);
64
+ findings.push(...contradictionFindings(policy.condition, policy.id));
65
+ const signature = conditionSignature(policy.condition);
66
+ signatures.set(signature, [...(signatures.get(signature) ?? []), policy]);
67
+ }
68
+ for (const [policyId, count] of ids.entries()) {
69
+ if (count > 1) {
70
+ findings.push({
71
+ severity: "error",
72
+ code: "duplicate_policy_id",
73
+ message: `Policy ID appears ${count} times.`,
74
+ policyIds: [policyId],
75
+ field: "id",
76
+ });
77
+ }
78
+ }
79
+ for (const policiesWithSignature of signatures.values()) {
80
+ if (policiesWithSignature.length < 2)
81
+ continue;
82
+ const effects = new Set(policiesWithSignature.map((policy) => policy.effect));
83
+ const policyIds = policiesWithSignature.map((policy) => policy.id);
84
+ if (effects.size > 1) {
85
+ findings.push({
86
+ severity: "warning",
87
+ code: "conflicting_identical_conditions",
88
+ message: "Policies share identical conditions but have different effects.",
89
+ policyIds,
90
+ field: "conditions",
91
+ });
92
+ }
93
+ const highestRank = Math.max(...[...effects].map(decisionRank));
94
+ const lowerPolicies = policiesWithSignature
95
+ .filter((policy) => decisionRank(policy.effect) < highestRank)
96
+ .map((policy) => policy.id);
97
+ if (lowerPolicies.length > 0) {
98
+ findings.push({
99
+ severity: "info",
100
+ code: "shadowed_policy",
101
+ message: "A lower-precedence policy shares conditions with a higher-precedence policy.",
102
+ policyIds: lowerPolicies,
103
+ field: "conditions",
104
+ });
105
+ }
106
+ }
107
+ for (const [field, types] of [...fieldTypes.entries()].sort(([left], [right]) => compareStrings(left, right))) {
108
+ const concreteTypes = [...types].filter((type) => type !== "unknown");
109
+ if (new Set(concreteTypes).size > 1) {
110
+ findings.push({
111
+ severity: "warning",
112
+ code: "inconsistent_field_type",
113
+ message: `Field \`${field}\` is compared against inconsistent value types.`,
114
+ policyIds: [],
115
+ field,
116
+ });
117
+ }
118
+ }
119
+ return findings.map(categorizeFinding);
120
+ }
121
+ export function canonicalPolicyIrJson(policyIr) {
122
+ return JSON.stringify(stableValue(policyIr));
123
+ }
124
+ export function hashPolicyIr(policyIr) {
125
+ return createHash(POLICY_IR_HASH_ALGORITHM)
126
+ .update(canonicalPolicyIrJson(policyIr), "utf8")
127
+ .digest("hex");
128
+ }
129
+ export function conditionSignature(conditionIr) {
130
+ return JSON.stringify(signatureValue(conditionIr));
131
+ }
132
+ export function checkPolicyProperty(policyIr, propertySpec) {
133
+ if (propertySpec.type !== "no_allowing_policy_for_condition") {
134
+ return {
135
+ result: "unknown",
136
+ reason: `Unsupported non-solver property type: ${String(propertySpec.type)}`,
137
+ policyIds: [],
138
+ };
139
+ }
140
+ const targetSignature = conditionSignature(normalizeCondition(propertySpec.condition));
141
+ const matchedPolicyIds = policyIr.policies
142
+ .filter((policy) => ["allow", "log"].includes(policy.effect))
143
+ .filter((policy) => conditionSignature(policy.condition) === targetSignature)
144
+ .map((policy) => policy.id);
145
+ if (matchedPolicyIds.length > 0) {
146
+ return {
147
+ result: "fail",
148
+ reason: "One or more allow/log policies have the forbidden condition signature.",
149
+ policyIds: matchedPolicyIds,
150
+ };
151
+ }
152
+ return {
153
+ result: "unknown",
154
+ reason: "No exact allow/log condition match was found; implication checks require a solver.",
155
+ policyIds: [],
156
+ };
157
+ }
158
+ export function simulatePolicyIr(policyIr, simulationSpec) {
159
+ const propertySpec = simulationSpec.property;
160
+ if (!isRecord(propertySpec)) {
161
+ return simulationUnknown("Simulation property must be an object.", { errorCode: "simulation_invalid_property" });
162
+ }
163
+ const capabilityValidation = validateSimulationCapabilities(simulationSpec);
164
+ if (capabilityValidation !== undefined)
165
+ return capabilityValidation;
166
+ const propertyType = String(propertySpec.type ?? "");
167
+ if (![
168
+ "no_allow_or_log_for_condition",
169
+ "must_review_for_condition",
170
+ "must_deny_for_condition",
171
+ "decision_must_be_one_of",
172
+ "policy_must_match_for_condition",
173
+ ].includes(propertyType)) {
174
+ return simulationUnknown(`Unsupported bounded simulation property type: ${String(propertySpec.type)}`, {
175
+ errorCode: "simulation_unsupported_property",
176
+ });
177
+ }
178
+ const propertyValidation = validateSimulationProperty(propertySpec);
179
+ if (propertyValidation !== undefined)
180
+ return propertyValidation;
181
+ if (!isRecord(simulationSpec.domains)) {
182
+ return simulationUnknown("Simulation domains must be an object.", { errorCode: "simulation_invalid_domain" });
183
+ }
184
+ const domains = {};
185
+ const fieldRegistry = simulationSpec.fieldRegistry;
186
+ if (fieldRegistry !== undefined && !isRecord(fieldRegistry)) {
187
+ return simulationUnknown("Simulation fieldRegistry must be an object.", { errorCode: "simulation_invalid_field_registry" });
188
+ }
189
+ if (fieldRegistry !== undefined) {
190
+ const registryCapabilities = validateCapabilityList(fieldRegistry.capabilities);
191
+ if (!registryCapabilities.ok) {
192
+ return simulationUnknown(registryCapabilities.reason ?? "Invalid capability metadata.", {
193
+ errorCode: registryCapabilities.errorCode,
194
+ });
195
+ }
196
+ }
197
+ for (const [field, values] of Object.entries(simulationSpec.domains)) {
198
+ if (!["agent", "action", "context"].includes(field.split(".")[0])) {
199
+ return simulationUnknown(`Simulation domain path must start with agent, action, or context: ${field}`, {
200
+ errorCode: "simulation_invalid_domain",
201
+ });
202
+ }
203
+ if (!Array.isArray(values) || values.length === 0) {
204
+ return simulationUnknown(`Simulation domain \`${field}\` must be a non-empty array.`, {
205
+ errorCode: "simulation_invalid_domain",
206
+ });
207
+ }
208
+ const registryValidation = validateDomainAgainstRegistry(field, values, fieldRegistry);
209
+ if (registryValidation !== undefined)
210
+ return registryValidation;
211
+ domains[field] = values.map(cloneValue);
212
+ }
213
+ const maxSamples = Number(simulationSpec.maxSamples ?? 500);
214
+ if (!Number.isFinite(maxSamples)) {
215
+ return simulationUnknown("Simulation maxSamples must be a number.", { errorCode: "simulation_invalid_max_samples" });
216
+ }
217
+ const sampleCount = Object.values(domains).reduce((count, values) => count * values.length, 1);
218
+ if (sampleCount > maxSamples) {
219
+ return simulationUnknown(`Simulation would generate ${sampleCount} samples, above maxSamples=${maxSamples}.`, {
220
+ errorCode: "simulation_sample_limit",
221
+ });
222
+ }
223
+ const propertyCondition = normalizeCondition(propertySpec.condition);
224
+ const decisionCounts = emptyDecisionCounts();
225
+ let checkedSamples = 0;
226
+ let matchingSamples = 0;
227
+ const policyIds = [];
228
+ const counterexamples = [];
229
+ const counterexampleLimit = Number(simulationSpec.counterexampleLimit ?? 5);
230
+ let index = 0;
231
+ for (const sample of simulationSamples(domains, simulationSpec.baseAgent ?? {}, simulationSpec.baseAction ?? {}, simulationSpec.baseContext ?? {})) {
232
+ const sampleIndex = index;
233
+ index += 1;
234
+ checkedSamples += 1;
235
+ const scope = { agent: sample.agent, action: sample.action, context: sample.context };
236
+ const propertyMatch = conditionMatches(propertyCondition, scope);
237
+ if (propertyMatch === undefined) {
238
+ return simulationUnknown("Simulation property condition could not be evaluated.", {
239
+ errorCode: "simulation_property_evaluation_unknown",
240
+ checkedSamples,
241
+ matchingSamples,
242
+ decisionCounts,
243
+ policyIds,
244
+ counterexamples,
245
+ });
246
+ }
247
+ const trace = tracePolicyIrSemantics(policyIr, scope);
248
+ if (isDecisionCountKey(trace.decision))
249
+ decisionCounts[trace.decision] += 1;
250
+ else
251
+ decisionCounts.unknown += 1;
252
+ for (const policyId of trace.matchedPolicyIds)
253
+ appendUnique(policyIds, policyId);
254
+ if (!propertyMatch)
255
+ continue;
256
+ matchingSamples += 1;
257
+ if (trace.result === "unknown" || trace.decision === "unknown") {
258
+ return simulationUnknown("A matching sample could not be evaluated.", {
259
+ errorCode: "simulation_evaluation_unknown",
260
+ checkedSamples,
261
+ matchingSamples,
262
+ decisionCounts,
263
+ policyIds,
264
+ counterexamples,
265
+ });
266
+ }
267
+ const decision = simulationDecisionFromTrace(trace);
268
+ const violation = propertyViolation(propertySpec, decision);
269
+ if (violation !== undefined) {
270
+ if (counterexamples.length < counterexampleLimit) {
271
+ counterexamples.push({
272
+ sampleIndex,
273
+ action: sample.action,
274
+ context: sample.context,
275
+ decision,
276
+ reason: violation,
277
+ });
278
+ }
279
+ }
280
+ }
281
+ if (matchingSamples === 0) {
282
+ return simulationUnknown("No generated samples matched the property condition.", {
283
+ errorCode: "simulation_no_matching_samples",
284
+ checkedSamples,
285
+ matchingSamples,
286
+ decisionCounts,
287
+ policyIds,
288
+ counterexamples,
289
+ });
290
+ }
291
+ if (counterexamples.length > 0) {
292
+ return {
293
+ result: "fail",
294
+ reason: propertyFailureReason(propertySpec),
295
+ policyIds,
296
+ checkedSamples,
297
+ matchingSamples,
298
+ decisionCounts,
299
+ counterexamples,
300
+ };
301
+ }
302
+ return {
303
+ result: "pass",
304
+ reason: propertyPassReason(propertySpec),
305
+ policyIds,
306
+ checkedSamples,
307
+ matchingSamples,
308
+ decisionCounts,
309
+ counterexamples: [],
310
+ };
311
+ }
312
+ function normalizePolicy(policy, index) {
313
+ const policyRecord = isRecord(policy) ? policy : {};
314
+ const rawMetadata = policyRecord.metadata;
315
+ return compactRecord({
316
+ kind: "Policy",
317
+ index,
318
+ id: String(policyRecord.id ?? `policy-${index}`),
319
+ name: policyRecord.name === undefined ? undefined : String(policyRecord.name),
320
+ description: policyRecord.description === undefined ? undefined : String(policyRecord.description),
321
+ effect: String(policyRecord.effect ?? ""),
322
+ reason: String(policyRecord.reason ?? "Policy matched."),
323
+ condition: normalizeCondition(policyRecord.conditions),
324
+ metadata: isRecord(rawMetadata) ? rawMetadata : {},
325
+ });
326
+ }
327
+ function normalizeCondition(condition) {
328
+ if (condition === undefined || condition === null) {
329
+ return { kind: "all", children: [] };
330
+ }
331
+ if (!isRecord(condition)) {
332
+ return { kind: "invalid", error: "Policy conditions must be an object." };
333
+ }
334
+ if ("all" in condition || "any" in condition) {
335
+ const key = "all" in condition ? "all" : "any";
336
+ const group = condition[key];
337
+ if (!Array.isArray(group)) {
338
+ return {
339
+ kind: "invalid",
340
+ error: `Condition group \`${key}\` must be a list.`,
341
+ group: key,
342
+ };
343
+ }
344
+ return {
345
+ kind: key,
346
+ children: group.map(normalizeCondition).sort((left, right) => compareStrings(conditionSignature(left), conditionSignature(right))),
347
+ };
348
+ }
349
+ const value = condition.value;
350
+ return {
351
+ kind: "condition",
352
+ field: condition.field === undefined ? "" : String(condition.field),
353
+ operator: String(condition.operator ?? "equals"),
354
+ value,
355
+ valueType: valueType(value),
356
+ };
357
+ }
358
+ function valueType(value) {
359
+ if (typeof value === "boolean")
360
+ return "boolean";
361
+ if (typeof value === "number")
362
+ return "number";
363
+ if (typeof value === "string")
364
+ return "string";
365
+ if (Array.isArray(value)) {
366
+ const itemTypes = new Set(value.map(valueType));
367
+ if (itemTypes.size === 1 && ["string", "number", "boolean"].includes([...itemTypes][0])) {
368
+ return "enum";
369
+ }
370
+ return "object";
371
+ }
372
+ if (isRecord(value))
373
+ return "object";
374
+ return "unknown";
375
+ }
376
+ function conditionFindings(condition, policyId) {
377
+ const findings = [];
378
+ if (condition.kind === "invalid") {
379
+ findings.push({
380
+ severity: "error",
381
+ code: "malformed_condition_group",
382
+ message: condition.error,
383
+ policyIds: [policyId],
384
+ field: condition.group ? `conditions.${condition.group}` : "conditions",
385
+ });
386
+ }
387
+ if (condition.kind === "all" || condition.kind === "any") {
388
+ for (const child of condition.children)
389
+ findings.push(...conditionFindings(child, policyId));
390
+ }
391
+ if (condition.kind === "condition") {
392
+ if (!condition.field) {
393
+ findings.push({
394
+ severity: "error",
395
+ code: "missing_condition_field",
396
+ message: "Condition is missing a field.",
397
+ policyIds: [policyId],
398
+ field: "conditions.field",
399
+ });
400
+ }
401
+ if (!POLICY_OPERATORS.has(condition.operator)) {
402
+ findings.push({
403
+ severity: "error",
404
+ code: "unsupported_operator",
405
+ message: `Condition uses unsupported operator: ${condition.operator}`,
406
+ policyIds: [policyId],
407
+ field: "conditions.operator",
408
+ });
409
+ }
410
+ }
411
+ return findings;
412
+ }
413
+ function collectFieldTypes(condition, fieldTypes) {
414
+ if (condition.kind === "all" || condition.kind === "any") {
415
+ for (const child of condition.children)
416
+ collectFieldTypes(child, fieldTypes);
417
+ }
418
+ if (condition.kind === "condition" && condition.field) {
419
+ fieldTypes.set(condition.field, fieldTypes.get(condition.field) ?? new Set());
420
+ fieldTypes.get(condition.field)?.add(condition.valueType);
421
+ }
422
+ }
423
+ function contradictionFindings(condition, policyId) {
424
+ if (condition.kind !== "all")
425
+ return [];
426
+ const equalsByField = new Map();
427
+ const greaterThan = new Map();
428
+ const lessThan = new Map();
429
+ const findings = [];
430
+ for (const child of condition.children) {
431
+ if (child.kind !== "condition")
432
+ continue;
433
+ if (child.operator === "equals") {
434
+ equalsByField.set(child.field, equalsByField.get(child.field) ?? new Set());
435
+ equalsByField.get(child.field)?.add(JSON.stringify(child.value));
436
+ }
437
+ if (child.operator === "greater_than" && typeof child.value === "number") {
438
+ greaterThan.set(child.field, Math.max(greaterThan.get(child.field) ?? child.value, child.value));
439
+ }
440
+ if (child.operator === "less_than" && typeof child.value === "number") {
441
+ lessThan.set(child.field, Math.min(lessThan.get(child.field) ?? child.value, child.value));
442
+ }
443
+ }
444
+ for (const [field, values] of equalsByField.entries()) {
445
+ if (values.size > 1)
446
+ findings.push(contradiction(policyId, field));
447
+ }
448
+ for (const [field, lower] of greaterThan.entries()) {
449
+ const upper = lessThan.get(field);
450
+ if (upper !== undefined && lower >= upper)
451
+ findings.push(contradiction(policyId, field));
452
+ }
453
+ return findings;
454
+ }
455
+ function contradiction(policyId, field) {
456
+ return {
457
+ severity: "warning",
458
+ code: "contradictory_conditions",
459
+ message: `Policy contains contradictory conditions for \`${field}\`.`,
460
+ policyIds: [policyId],
461
+ field: `conditions.${field}`,
462
+ };
463
+ }
464
+ function simulationUnknown(reason, partial = {}) {
465
+ return {
466
+ result: "unknown",
467
+ reason,
468
+ errorCategory: "simulation_unknown",
469
+ errorCode: partial.errorCode ?? "simulation_unknown",
470
+ policyIds: partial.policyIds ?? [],
471
+ checkedSamples: partial.checkedSamples ?? 0,
472
+ matchingSamples: partial.matchingSamples ?? 0,
473
+ decisionCounts: partial.decisionCounts ?? emptyDecisionCounts(),
474
+ counterexamples: partial.counterexamples ?? [],
475
+ };
476
+ }
477
+ function validateSimulationCapabilities(simulationSpec) {
478
+ const validation = validateRequiredCapabilities(simulationSpec.capabilities, BOUNDED_SIMULATION_CAPABILITIES);
479
+ if (!validation.ok) {
480
+ return simulationUnknown(validation.reason ?? "Invalid capability metadata.", {
481
+ errorCode: validation.errorCode,
482
+ });
483
+ }
484
+ return undefined;
485
+ }
486
+ function validateSimulationProperty(propertySpec) {
487
+ const propertyType = String(propertySpec.type ?? "");
488
+ if (propertyType === "decision_must_be_one_of") {
489
+ const decisions = propertySpec.decisions;
490
+ if (!Array.isArray(decisions) || decisions.length === 0) {
491
+ return simulationUnknown("Property `decision_must_be_one_of` requires a non-empty decisions array.", {
492
+ errorCode: "simulation_invalid_property",
493
+ });
494
+ }
495
+ const unsupported = decisions.filter((decision) => !(String(decision) in DECISION_RANK));
496
+ if (unsupported.length > 0) {
497
+ return simulationUnknown(`Property \`decision_must_be_one_of\` contains unsupported decisions: ${JSON.stringify(unsupported)}`, { errorCode: "simulation_invalid_property" });
498
+ }
499
+ }
500
+ if (propertyType === "policy_must_match_for_condition" && propertyPolicyIds(propertySpec).length === 0) {
501
+ return simulationUnknown("Property `policy_must_match_for_condition` requires policyId or policyIds.", {
502
+ errorCode: "simulation_invalid_property",
503
+ });
504
+ }
505
+ return undefined;
506
+ }
507
+ function propertyViolation(propertySpec, decision) {
508
+ const propertyType = String(propertySpec.type ?? "");
509
+ if (propertyType === "no_allow_or_log_for_condition") {
510
+ return decision.decision === "allow" || decision.decision === "log"
511
+ ? "Decision allowed or logged a matching sample."
512
+ : undefined;
513
+ }
514
+ if (propertyType === "must_review_for_condition") {
515
+ return decision.decision !== "review" ? "Decision was not review for a matching sample." : undefined;
516
+ }
517
+ if (propertyType === "must_deny_for_condition") {
518
+ return decision.decision !== "deny" ? "Decision was not deny for a matching sample." : undefined;
519
+ }
520
+ if (propertyType === "decision_must_be_one_of") {
521
+ const decisions = new Set(propertySpec.decisions.map(String));
522
+ return decisions.has(decision.decision) ? undefined : `Decision \`${decision.decision}\` was outside the allowed set.`;
523
+ }
524
+ if (propertyType === "policy_must_match_for_condition") {
525
+ const expected = new Set(propertyPolicyIds(propertySpec));
526
+ return decision.matchedPolicyIds.some((policyId) => expected.has(policyId))
527
+ ? undefined
528
+ : "No expected policy matched a matching sample.";
529
+ }
530
+ return "Unsupported property type.";
531
+ }
532
+ function propertyPolicyIds(propertySpec) {
533
+ if (propertySpec.policyId !== undefined)
534
+ return [String(propertySpec.policyId)];
535
+ return Array.isArray(propertySpec.policyIds) ? propertySpec.policyIds.map(String) : [];
536
+ }
537
+ function propertyFailureReason(propertySpec) {
538
+ const propertyType = String(propertySpec.type ?? "");
539
+ if (propertyType === "no_allow_or_log_for_condition") {
540
+ return "One or more bounded samples matched the forbidden condition and resulted in allow/log.";
541
+ }
542
+ if (propertyType === "must_review_for_condition") {
543
+ return "One or more bounded samples matched the condition and did not result in review.";
544
+ }
545
+ if (propertyType === "must_deny_for_condition") {
546
+ return "One or more bounded samples matched the condition and did not result in deny.";
547
+ }
548
+ if (propertyType === "decision_must_be_one_of") {
549
+ return "One or more bounded samples matched the condition and selected a decision outside the allowed set.";
550
+ }
551
+ if (propertyType === "policy_must_match_for_condition") {
552
+ return "One or more bounded samples matched the condition without matching the expected policy.";
553
+ }
554
+ return "One or more bounded samples violated the property.";
555
+ }
556
+ function propertyPassReason(propertySpec) {
557
+ const propertyType = String(propertySpec.type ?? "");
558
+ if (propertyType === "no_allow_or_log_for_condition") {
559
+ return "All matching bounded samples avoided allow/log decisions.";
560
+ }
561
+ if (propertyType === "must_review_for_condition") {
562
+ return "All matching bounded samples resulted in review.";
563
+ }
564
+ if (propertyType === "must_deny_for_condition") {
565
+ return "All matching bounded samples resulted in deny.";
566
+ }
567
+ if (propertyType === "decision_must_be_one_of") {
568
+ return "All matching bounded samples selected an allowed decision.";
569
+ }
570
+ if (propertyType === "policy_must_match_for_condition") {
571
+ return "All matching bounded samples matched the expected policy.";
572
+ }
573
+ return "All matching bounded samples satisfied the property.";
574
+ }
575
+ function categorizeFinding(finding) {
576
+ return {
577
+ ...finding,
578
+ errorCategory: finding.errorCategory ?? errorCategoryForCode(finding.code),
579
+ };
580
+ }
581
+ function emptyDecisionCounts() {
582
+ return { allow: 0, log: 0, review: 0, deny: 0, unknown: 0 };
583
+ }
584
+ function validateDomainAgainstRegistry(field, values, fieldRegistry) {
585
+ if (fieldRegistry === undefined)
586
+ return undefined;
587
+ const fields = fieldRegistry.fields;
588
+ if (!isRecord(fields) || !(field in fields))
589
+ return undefined;
590
+ const entry = fields[field];
591
+ if (!isRecord(entry)) {
592
+ return simulationUnknown(`Field registry entry for \`${field}\` must be an object.`, {
593
+ errorCode: "simulation_invalid_field_registry",
594
+ });
595
+ }
596
+ const fieldType = String(entry.type ?? "");
597
+ if (!["string", "enum", "number", "boolean"].includes(fieldType)) {
598
+ return simulationUnknown(`Field registry entry for \`${field}\` has unsupported type: ${fieldType}`, {
599
+ errorCode: "simulation_unsupported_field_registry_type",
600
+ });
601
+ }
602
+ for (const value of values) {
603
+ if ((fieldType === "string" || fieldType === "enum") && typeof value !== "string") {
604
+ return simulationUnknown(`Simulation domain \`${field}\` contains non-string value for ${fieldType} field.`, {
605
+ errorCode: "simulation_invalid_domain",
606
+ });
607
+ }
608
+ if (fieldType === "boolean" && typeof value !== "boolean") {
609
+ return simulationUnknown(`Simulation domain \`${field}\` contains non-boolean value.`, {
610
+ errorCode: "simulation_invalid_domain",
611
+ });
612
+ }
613
+ if (fieldType === "number" && (typeof value !== "number" || !Number.isFinite(value))) {
614
+ return simulationUnknown(`Simulation domain \`${field}\` contains nonnumeric value.`, {
615
+ errorCode: "simulation_invalid_domain",
616
+ });
617
+ }
618
+ if (fieldType === "enum" && Array.isArray(entry.values) && !entry.values.includes(value)) {
619
+ return simulationUnknown(`Simulation domain \`${field}\` contains value outside registry enum values.`, {
620
+ errorCode: "simulation_invalid_domain",
621
+ });
622
+ }
623
+ if (fieldType === "number" && typeof value === "number") {
624
+ if (typeof entry.min === "number" && value < entry.min) {
625
+ return simulationUnknown(`Simulation domain \`${field}\` contains value below registry min.`, {
626
+ errorCode: "simulation_invalid_domain",
627
+ });
628
+ }
629
+ if (typeof entry.max === "number" && value > entry.max) {
630
+ return simulationUnknown(`Simulation domain \`${field}\` contains value above registry max.`, {
631
+ errorCode: "simulation_invalid_domain",
632
+ });
633
+ }
634
+ }
635
+ }
636
+ return undefined;
637
+ }
638
+ function isDecisionCountKey(value) {
639
+ return value === "allow" || value === "log" || value === "review" || value === "deny";
640
+ }
641
+ function* simulationSamples(domains, baseAgent, baseAction, baseContext) {
642
+ const keys = Object.keys(domains).sort(compareStrings);
643
+ if (keys.length === 0) {
644
+ yield {
645
+ agent: cloneValue(baseAgent),
646
+ action: cloneValue(baseAction),
647
+ context: cloneValue(baseContext),
648
+ };
649
+ return;
650
+ }
651
+ yield* simulationSampleAt(domains, keys, 0, {}, baseAgent, baseAction, baseContext);
652
+ }
653
+ function* simulationSampleAt(domains, keys, keyIndex, assignments, baseAgent, baseAction, baseContext) {
654
+ if (keyIndex >= keys.length) {
655
+ const agent = cloneValue(baseAgent);
656
+ const action = cloneValue(baseAction);
657
+ const context = cloneValue(baseContext);
658
+ const scope = { agent, action, context };
659
+ for (const [path, value] of Object.entries(assignments))
660
+ setDotted(scope, path, value);
661
+ yield { agent, action, context };
662
+ return;
663
+ }
664
+ const key = keys[keyIndex];
665
+ for (const value of domains[key]) {
666
+ assignments[key] = value;
667
+ yield* simulationSampleAt(domains, keys, keyIndex + 1, assignments, baseAgent, baseAction, baseContext);
668
+ }
669
+ delete assignments[key];
670
+ }
671
+ function setDotted(scope, path, value) {
672
+ const parts = path.split(".");
673
+ let current = scope;
674
+ for (const part of parts.slice(0, -1)) {
675
+ const next = current[part];
676
+ if (!isRecord(next))
677
+ current[part] = {};
678
+ current = current[part];
679
+ }
680
+ current[parts[parts.length - 1]] = cloneValue(value);
681
+ }
682
+ export function tracePolicyIrSemantics(policyIr, scope) {
683
+ if (!Array.isArray(policyIr.policies)) {
684
+ return unknownSemanticTrace("Policy IR policies must be a list.", "simulation_malformed_ir");
685
+ }
686
+ const matched = [];
687
+ for (const policy of policyIr.policies) {
688
+ const conditionMatch = conditionMatches(policy.condition, scope);
689
+ if (conditionMatch === undefined) {
690
+ return unknownSemanticTrace(`Policy \`${policy.id}\` condition could not be evaluated.`, "simulation_evaluation_unknown");
691
+ }
692
+ if (conditionMatch)
693
+ matched.push(policy);
694
+ }
695
+ if (matched.length === 0) {
696
+ const defaultDecision = policyIr.defaultDecision;
697
+ if (!(defaultDecision in DECISION_RANK)) {
698
+ return unknownSemanticTrace(`Unsupported default decision: ${defaultDecision}`, "simulation_unsupported_default_decision");
699
+ }
700
+ return evaluatedSemanticTrace({
701
+ decision: defaultDecision,
702
+ reason: policyIr.defaultReason,
703
+ matchedPolicyIds: [],
704
+ selectedPolicyId: null,
705
+ selectedMetadata: {},
706
+ enforcementSource: "policy_pack_default",
707
+ });
708
+ }
709
+ const selected = matched.reduce((current, next) => decisionRank(next.effect) > decisionRank(current.effect) ? next : current);
710
+ const matchedPolicyIds = matched.map((policy) => policy.id);
711
+ if (!(selected.effect in DECISION_RANK)) {
712
+ return unknownSemanticTrace(`Unsupported policy effect: ${selected.effect}`, "simulation_unsupported_effect", matchedPolicyIds);
713
+ }
714
+ return evaluatedSemanticTrace({
715
+ decision: selected.effect,
716
+ reason: selected.reason,
717
+ matchedPolicyIds,
718
+ selectedPolicyId: selected.id,
719
+ selectedMetadata: selected.metadata,
720
+ enforcementSource: "matched_policy",
721
+ });
722
+ }
723
+ function evaluatePolicyIr(policyIr, scope) {
724
+ return simulationDecisionFromTrace(tracePolicyIrSemantics(policyIr, scope));
725
+ }
726
+ function evaluatedSemanticTrace(input) {
727
+ const metadata = { enforcementSource: input.enforcementSource };
728
+ if (input.enforcementSource === "matched_policy") {
729
+ metadata.matchedPolicyCount = input.matchedPolicyIds.length;
730
+ metadata.selectedPolicyId = input.selectedPolicyId;
731
+ Object.assign(metadata, stableValue(input.selectedMetadata));
732
+ }
733
+ return {
734
+ semanticTraceVersion: ANALYSIS_SEMANTIC_TRACE_VERSION,
735
+ result: "evaluated",
736
+ decision: input.decision,
737
+ reason: input.reason,
738
+ matchedPolicyIds: input.matchedPolicyIds,
739
+ selectedPolicyId: input.selectedPolicyId,
740
+ selectedMetadata: stableValue(input.selectedMetadata),
741
+ enforcementSource: input.enforcementSource,
742
+ metadata,
743
+ };
744
+ }
745
+ function unknownSemanticTrace(reason, errorCode, matchedPolicyIds = []) {
746
+ const metadata = simulationErrorMetadata(errorCode);
747
+ return {
748
+ semanticTraceVersion: ANALYSIS_SEMANTIC_TRACE_VERSION,
749
+ result: "unknown",
750
+ decision: "unknown",
751
+ reason,
752
+ matchedPolicyIds,
753
+ selectedPolicyId: null,
754
+ selectedMetadata: {},
755
+ enforcementSource: String(metadata.enforcementSource),
756
+ unknownCategory: "simulation_unknown",
757
+ unknownCode: errorCode,
758
+ metadata,
759
+ };
760
+ }
761
+ function simulationDecisionFromTrace(trace) {
762
+ return {
763
+ decision: trace.decision,
764
+ reason: trace.reason,
765
+ matchedPolicyIds: trace.matchedPolicyIds,
766
+ metadata: stableValue(trace.metadata),
767
+ };
768
+ }
769
+ function simulationErrorMetadata(errorCode) {
770
+ return {
771
+ enforcementSource: "simulation_error",
772
+ errorCategory: "simulation_unknown",
773
+ errorCode,
774
+ };
775
+ }
776
+ function conditionMatches(condition, scope) {
777
+ if (condition.kind === "all") {
778
+ let unknown = false;
779
+ for (const child of condition.children) {
780
+ const childMatch = conditionMatches(child, scope);
781
+ if (childMatch === undefined) {
782
+ unknown = true;
783
+ continue;
784
+ }
785
+ if (!childMatch)
786
+ return false;
787
+ }
788
+ return unknown ? undefined : true;
789
+ }
790
+ if (condition.kind === "any") {
791
+ let unknown = false;
792
+ for (const child of condition.children) {
793
+ const childMatch = conditionMatches(child, scope);
794
+ if (childMatch === undefined) {
795
+ unknown = true;
796
+ continue;
797
+ }
798
+ if (childMatch)
799
+ return true;
800
+ }
801
+ return unknown ? undefined : false;
802
+ }
803
+ if (condition.kind === "condition")
804
+ return leafConditionMatches(condition, scope);
805
+ return undefined;
806
+ }
807
+ function leafConditionMatches(condition, scope) {
808
+ const actual = getDotted(scope, condition.field);
809
+ if (condition.operator === "equals")
810
+ return actual === condition.value;
811
+ if (condition.operator === "not_equals")
812
+ return actual !== condition.value;
813
+ if (condition.operator === "greater_than") {
814
+ const actualNumber = numericValue(actual);
815
+ const expectedNumber = numericValue(condition.value);
816
+ return actualNumber === undefined || expectedNumber === undefined ? false : actualNumber > expectedNumber;
817
+ }
818
+ if (condition.operator === "less_than") {
819
+ const actualNumber = numericValue(actual);
820
+ const expectedNumber = numericValue(condition.value);
821
+ return actualNumber === undefined || expectedNumber === undefined ? false : actualNumber < expectedNumber;
822
+ }
823
+ if (condition.operator === "in") {
824
+ return Array.isArray(condition.value) ? condition.value.includes(actual) : false;
825
+ }
826
+ return undefined;
827
+ }
828
+ function numericValue(value) {
829
+ if (typeof value === "number" && Number.isFinite(value))
830
+ return value;
831
+ if (typeof value === "string" && value.trim() !== "") {
832
+ const parsed = Number(value);
833
+ return Number.isFinite(parsed) ? parsed : undefined;
834
+ }
835
+ return undefined;
836
+ }
837
+ function appendUnique(values, value) {
838
+ if (!values.includes(value))
839
+ values.push(value);
840
+ }
841
+ function cloneValue(value) {
842
+ if (value === undefined)
843
+ return value;
844
+ return JSON.parse(JSON.stringify(value));
845
+ }
846
+ function decisionRank(effect) {
847
+ return effect in DECISION_RANK ? DECISION_RANK[effect] : -1;
848
+ }
849
+ function signatureValue(value) {
850
+ if (Array.isArray(value))
851
+ return value.map(signatureValue);
852
+ if (isRecord(value)) {
853
+ return Object.fromEntries(Object.entries(value)
854
+ .filter(([key]) => key !== "index")
855
+ .sort(([left], [right]) => compareStrings(left, right))
856
+ .map(([key, entry]) => [key, signatureValue(entry)]));
857
+ }
858
+ return value;
859
+ }