@adrkit/evaluator 0.1.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.
Files changed (81) hide show
  1. package/README.md +23 -0
  2. package/dist/LICENSE +201 -0
  3. package/dist/NOTICE +11 -0
  4. package/dist/assertions/evaluate.d.ts +36 -0
  5. package/dist/assertions/jsonpath.d.ts +20 -0
  6. package/dist/assertions/limits.d.ts +21 -0
  7. package/dist/assertions/registry.d.ts +20 -0
  8. package/dist/assertions/rego.d.ts +27 -0
  9. package/dist/catalog.d.ts +39 -0
  10. package/dist/compare.d.ts +10 -0
  11. package/dist/crypto/sha256.d.ts +10 -0
  12. package/dist/identity/directory.d.ts +23 -0
  13. package/dist/index.d.ts +24 -0
  14. package/dist/index.js +2064 -0
  15. package/dist/keys.d.ts +27 -0
  16. package/dist/pass0.d.ts +19 -0
  17. package/dist/patch/project.d.ts +20 -0
  18. package/dist/report/aggregate.d.ts +21 -0
  19. package/dist/report/assemble.d.ts +16 -0
  20. package/dist/report/order.d.ts +19 -0
  21. package/dist/report/serialize.d.ts +42 -0
  22. package/dist/routing/accepted-assertion.d.ts +12 -0
  23. package/dist/routing/route.d.ts +18 -0
  24. package/dist/routing/target.d.ts +20 -0
  25. package/dist/rules/affects-overlap.d.ts +13 -0
  26. package/dist/rules/affects-resolvable.d.ts +14 -0
  27. package/dist/rules/assertions-compile.d.ts +12 -0
  28. package/dist/rules/assertions-pass.d.ts +13 -0
  29. package/dist/rules/context.d.ts +21 -0
  30. package/dist/rules/decider-resolvable.d.ts +10 -0
  31. package/dist/rules/expiry-sane.d.ts +11 -0
  32. package/dist/rules/id-unique.d.ts +11 -0
  33. package/dist/rules/kernel.d.ts +16 -0
  34. package/dist/rules/no-orphan-refs.d.ts +12 -0
  35. package/dist/rules/schema-valid.d.ts +11 -0
  36. package/dist/rules/scope-hierarchy.d.ts +14 -0
  37. package/dist/rules/supersession-consistent.d.ts +12 -0
  38. package/dist/targets/canonical.d.ts +37 -0
  39. package/dist/targets/package.d.ts +11 -0
  40. package/dist/targets/path.d.ts +10 -0
  41. package/dist/targets/registry.d.ts +11 -0
  42. package/dist/types.d.ts +360 -0
  43. package/package.json +54 -0
  44. package/src/assertions/evaluate.ts +214 -0
  45. package/src/assertions/jsonpath.ts +95 -0
  46. package/src/assertions/limits.ts +57 -0
  47. package/src/assertions/registry.ts +38 -0
  48. package/src/assertions/rego.ts +272 -0
  49. package/src/catalog.ts +263 -0
  50. package/src/compare.ts +13 -0
  51. package/src/crypto/sha256.ts +101 -0
  52. package/src/identity/directory.ts +69 -0
  53. package/src/index.ts +81 -0
  54. package/src/keys.ts +55 -0
  55. package/src/pass0.ts +163 -0
  56. package/src/patch/project.ts +51 -0
  57. package/src/report/aggregate.ts +59 -0
  58. package/src/report/assemble.ts +43 -0
  59. package/src/report/order.ts +53 -0
  60. package/src/report/serialize.ts +152 -0
  61. package/src/routing/accepted-assertion.ts +39 -0
  62. package/src/routing/route.ts +105 -0
  63. package/src/routing/target.ts +104 -0
  64. package/src/rules/affects-overlap.ts +55 -0
  65. package/src/rules/affects-resolvable.ts +83 -0
  66. package/src/rules/assertions-compile.ts +18 -0
  67. package/src/rules/assertions-pass.ts +21 -0
  68. package/src/rules/context.ts +31 -0
  69. package/src/rules/decider-resolvable.ts +55 -0
  70. package/src/rules/expiry-sane.ts +30 -0
  71. package/src/rules/id-unique.ts +57 -0
  72. package/src/rules/kernel.ts +33 -0
  73. package/src/rules/no-orphan-refs.ts +102 -0
  74. package/src/rules/schema-valid.ts +49 -0
  75. package/src/rules/scope-hierarchy.ts +108 -0
  76. package/src/rules/supersession-consistent.ts +138 -0
  77. package/src/targets/canonical.ts +114 -0
  78. package/src/targets/package.ts +41 -0
  79. package/src/targets/path.ts +32 -0
  80. package/src/targets/registry.ts +23 -0
  81. package/src/types.ts +445 -0
package/src/catalog.ts ADDED
@@ -0,0 +1,263 @@
1
+ /**
2
+ * @adrkit/evaluator — fixed Pass 0 catalog.
3
+ *
4
+ * The eleven rule ids (fixed rubric order + fixed severity), the exhaustive,
5
+ * namespaced reason-code catalog, the per-rule primary-reason precedence, and the
6
+ * eight ordered routing triggers. These are the frozen contract vocabulary
7
+ * (data-model §7/§8/§11; research §R12). Adding or renaming a code is a versioned
8
+ * contract change, never a silent edit.
9
+ */
10
+
11
+ import type { Severity } from '@adrkit/core';
12
+
13
+ /** The eleven rubric rule ids, in fixed order (FR-002, C11). */
14
+ export const RULE_IDS = [
15
+ 'schema-valid',
16
+ 'id-unique',
17
+ 'supersession-consistent',
18
+ 'no-orphan-refs',
19
+ 'affects-resolvable',
20
+ 'affects-overlap',
21
+ 'scope-hierarchy',
22
+ 'assertions-compile',
23
+ 'assertions-pass',
24
+ 'decider-resolvable',
25
+ 'expiry-sane',
26
+ ] as const;
27
+
28
+ export type RuleId = (typeof RULE_IDS)[number];
29
+
30
+ /** Fixed severity per rule. Present on a result only when `status === 'fail'`. */
31
+ export const RULE_SEVERITY: Readonly<Record<RuleId, Severity>> = {
32
+ 'schema-valid': 'error',
33
+ 'id-unique': 'error',
34
+ 'supersession-consistent': 'error',
35
+ 'no-orphan-refs': 'error',
36
+ 'affects-resolvable': 'warn',
37
+ 'affects-overlap': 'warn',
38
+ 'scope-hierarchy': 'error',
39
+ 'assertions-compile': 'error',
40
+ 'assertions-pass': 'warn',
41
+ 'decider-resolvable': 'warn',
42
+ 'expiry-sane': 'info',
43
+ };
44
+
45
+ /** The eight Pass-0-provable escalation triggers, in fixed order (C4, R10). */
46
+ export const ROUTING_TRIGGERS = [
47
+ 'one-way-door',
48
+ 'cost-threshold',
49
+ 'security-surface',
50
+ 'data-residency',
51
+ 'regulatory',
52
+ 'contradicts-accepted-adr',
53
+ 'agent-authored-production',
54
+ 'human-requested',
55
+ ] as const;
56
+
57
+ export type Pass0EscalationReason = (typeof ROUTING_TRIGGERS)[number];
58
+
59
+ /**
60
+ * Exhaustive, stable, namespaced reason codes (data-model §11). The order within a
61
+ * rule's group is that rule's primary-reason precedence: the aggregate `reason` is
62
+ * the first code in this order among the sub-findings that share the winning status.
63
+ */
64
+ export const REASON_CODES = [
65
+ // schema-valid
66
+ 'schema-valid.ok',
67
+ 'schema-valid.file-read',
68
+ 'schema-valid.parse-error',
69
+ 'schema-valid.contract-error',
70
+ // id-unique
71
+ 'id-unique.ok',
72
+ 'id-unique.collision',
73
+ // supersession-consistent
74
+ 'supersession-consistent.ok',
75
+ 'supersession-consistent.dangling-superseded-by',
76
+ 'supersession-consistent.non-reciprocal',
77
+ 'supersession-consistent.cycle',
78
+ // no-orphan-refs
79
+ 'no-orphan-refs.ok',
80
+ 'no-orphan-refs.dangling-supersedes',
81
+ 'no-orphan-refs.dangling-relates-to',
82
+ 'no-orphan-refs.federated-log-absent',
83
+ // affects-resolvable
84
+ 'affects-resolvable.ok',
85
+ 'affects-resolvable.zero-targets',
86
+ 'affects-resolvable.backing-absent',
87
+ 'affects-resolvable.resolver-absent',
88
+ // affects-overlap (special primary precedence, §3.1)
89
+ 'affects-overlap.accepted-intersection',
90
+ 'affects-overlap.no-accepted-corpus',
91
+ 'affects-overlap.backing-absent',
92
+ 'affects-overlap.none',
93
+ // scope-hierarchy
94
+ 'scope-hierarchy.ok',
95
+ 'scope-hierarchy.contradicts-org-assertion',
96
+ 'scope-hierarchy.evidence-absent',
97
+ 'scope-hierarchy.engine-absent',
98
+ 'scope-hierarchy.source-absent',
99
+ 'scope-hierarchy.base-input-absent',
100
+ 'scope-hierarchy.proposed-input-absent',
101
+ 'scope-hierarchy.not-applicable-scope',
102
+ // assertions-compile
103
+ 'assertions-compile.ok',
104
+ 'assertions-compile.none',
105
+ 'assertions-compile.no-source',
106
+ 'assertions-compile.ambiguous-source',
107
+ 'assertions-compile.parse-error',
108
+ 'assertions-compile.engine-absent',
109
+ 'assertions-compile.source-absent',
110
+ // assertions-pass
111
+ 'assertions-pass.ok',
112
+ 'assertions-pass.none',
113
+ 'assertions-pass.evaluates-false',
114
+ 'assertions-pass.evaluation-error',
115
+ 'assertions-pass.engine-absent',
116
+ 'assertions-pass.input-absent',
117
+ // decider-resolvable
118
+ 'decider-resolvable.ok',
119
+ 'decider-resolvable.none-declared',
120
+ 'decider-resolvable.zero-match',
121
+ 'decider-resolvable.ambiguous-match',
122
+ 'decider-resolvable.directory-absent',
123
+ // expiry-sane
124
+ 'expiry-sane.ok',
125
+ 'expiry-sane.past-or-equal',
126
+ // shared not-evaluated
127
+ 'not-evaluated.schema-invalid',
128
+ 'not-evaluated.prereq-failed',
129
+ // routing evidence (escalate)
130
+ 'route.escalate.one-way-door',
131
+ 'route.escalate.cost-threshold',
132
+ 'route.escalate.security-surface',
133
+ 'route.escalate.data-residency',
134
+ 'route.escalate.regulatory',
135
+ 'route.escalate.contradicts-accepted-adr',
136
+ 'route.escalate.agent-authored-production',
137
+ 'route.escalate.human-requested',
138
+ // routing evidence (not-proven)
139
+ 'route.evidence.one-way-door.not-proven',
140
+ 'route.evidence.cost-threshold.not-proven',
141
+ 'route.evidence.security-surface.not-proven',
142
+ 'route.evidence.data-residency.not-proven',
143
+ 'route.evidence.regulatory.not-proven',
144
+ 'route.evidence.contradicts-accepted-adr.not-proven',
145
+ 'route.evidence.agent-authored-production.not-proven',
146
+ 'route.evidence.human-requested.not-proven',
147
+ // route targets
148
+ 'route.target.not-required',
149
+ 'route.target.deciders',
150
+ 'route.target.codeowners',
151
+ 'route.target.catalog-owner',
152
+ 'route.target.unresolved',
153
+ ] as const;
154
+
155
+ export type ReasonCode = (typeof REASON_CODES)[number];
156
+
157
+ const REASON_CODE_SET: ReadonlySet<string> = new Set(REASON_CODES);
158
+
159
+ export function isReasonCode(value: string): value is ReasonCode {
160
+ return REASON_CODE_SET.has(value);
161
+ }
162
+
163
+ /**
164
+ * Per-rule primary-reason precedence, derived from the catalog groupings above.
165
+ * The aggregate reason is the first entry present among the winning-status
166
+ * sub-findings; a clean rule with no sub-findings uses its first (`.ok`) entry,
167
+ * except `affects-overlap`, whose fixed precedence is documented in §3.1.
168
+ */
169
+ export const RULE_REASON_PRECEDENCE: Readonly<Record<RuleId, readonly ReasonCode[]>> = {
170
+ 'schema-valid': [
171
+ 'schema-valid.ok',
172
+ 'schema-valid.file-read',
173
+ 'schema-valid.parse-error',
174
+ 'schema-valid.contract-error',
175
+ ],
176
+ 'id-unique': ['id-unique.ok', 'id-unique.collision'],
177
+ 'supersession-consistent': [
178
+ 'supersession-consistent.ok',
179
+ 'supersession-consistent.dangling-superseded-by',
180
+ 'supersession-consistent.non-reciprocal',
181
+ 'supersession-consistent.cycle',
182
+ ],
183
+ 'no-orphan-refs': [
184
+ 'no-orphan-refs.ok',
185
+ 'no-orphan-refs.dangling-supersedes',
186
+ 'no-orphan-refs.dangling-relates-to',
187
+ 'no-orphan-refs.federated-log-absent',
188
+ ],
189
+ 'affects-resolvable': [
190
+ 'affects-resolvable.ok',
191
+ 'affects-resolvable.zero-targets',
192
+ 'affects-resolvable.backing-absent',
193
+ 'affects-resolvable.resolver-absent',
194
+ ],
195
+ 'affects-overlap': [
196
+ 'affects-overlap.accepted-intersection',
197
+ 'affects-overlap.no-accepted-corpus',
198
+ 'affects-overlap.backing-absent',
199
+ 'affects-overlap.none',
200
+ ],
201
+ 'scope-hierarchy': [
202
+ 'scope-hierarchy.ok',
203
+ 'scope-hierarchy.contradicts-org-assertion',
204
+ 'scope-hierarchy.evidence-absent',
205
+ 'scope-hierarchy.engine-absent',
206
+ 'scope-hierarchy.source-absent',
207
+ 'scope-hierarchy.base-input-absent',
208
+ 'scope-hierarchy.proposed-input-absent',
209
+ 'scope-hierarchy.not-applicable-scope',
210
+ ],
211
+ 'assertions-compile': [
212
+ 'assertions-compile.ok',
213
+ 'assertions-compile.none',
214
+ 'assertions-compile.no-source',
215
+ 'assertions-compile.ambiguous-source',
216
+ 'assertions-compile.parse-error',
217
+ 'assertions-compile.engine-absent',
218
+ 'assertions-compile.source-absent',
219
+ ],
220
+ 'assertions-pass': [
221
+ 'assertions-pass.ok',
222
+ 'assertions-pass.none',
223
+ 'assertions-pass.evaluates-false',
224
+ 'assertions-pass.evaluation-error',
225
+ 'assertions-pass.engine-absent',
226
+ 'assertions-pass.input-absent',
227
+ ],
228
+ 'decider-resolvable': [
229
+ 'decider-resolvable.ok',
230
+ 'decider-resolvable.none-declared',
231
+ 'decider-resolvable.zero-match',
232
+ 'decider-resolvable.ambiguous-match',
233
+ 'decider-resolvable.directory-absent',
234
+ ],
235
+ 'expiry-sane': ['expiry-sane.ok', 'expiry-sane.past-or-equal'],
236
+ };
237
+
238
+ /** Trigger → its proven escalate reason code. */
239
+ export const ROUTE_ESCALATE_CODE: Readonly<Record<Pass0EscalationReason, ReasonCode>> = {
240
+ 'one-way-door': 'route.escalate.one-way-door',
241
+ 'cost-threshold': 'route.escalate.cost-threshold',
242
+ 'security-surface': 'route.escalate.security-surface',
243
+ 'data-residency': 'route.escalate.data-residency',
244
+ regulatory: 'route.escalate.regulatory',
245
+ 'contradicts-accepted-adr': 'route.escalate.contradicts-accepted-adr',
246
+ 'agent-authored-production': 'route.escalate.agent-authored-production',
247
+ 'human-requested': 'route.escalate.human-requested',
248
+ };
249
+
250
+ /** Trigger → its not-proven evidence reason code. */
251
+ export const ROUTE_EVIDENCE_NOT_PROVEN_CODE: Readonly<Record<Pass0EscalationReason, ReasonCode>> = {
252
+ 'one-way-door': 'route.evidence.one-way-door.not-proven',
253
+ 'cost-threshold': 'route.evidence.cost-threshold.not-proven',
254
+ 'security-surface': 'route.evidence.security-surface.not-proven',
255
+ 'data-residency': 'route.evidence.data-residency.not-proven',
256
+ regulatory: 'route.evidence.regulatory.not-proven',
257
+ 'contradicts-accepted-adr': 'route.evidence.contradicts-accepted-adr.not-proven',
258
+ 'agent-authored-production': 'route.evidence.agent-authored-production.not-proven',
259
+ 'human-requested': 'route.evidence.human-requested.not-proven',
260
+ };
261
+
262
+ /** The rubric version recorded on `Pass0Report` (part of the deterministic payload). */
263
+ export const RUBRIC_VERSION = '0.1.0' as const;
package/src/compare.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @adrkit/evaluator — deterministic string comparison.
3
+ *
4
+ * `String.prototype.localeCompare` (no locale) is ECMA-402 implementation-/ICU-/locale-
5
+ * dependent and non-injective (`"\u00e9".localeCompare("e\u0301") === 0`), so it cannot
6
+ * back a byte-for-byte "across machines" guarantee (FR-005) or the envelope hash
7
+ * recomputation. Every ordering that feeds canonical bytes or a hash MUST use this pure
8
+ * code-unit comparator instead.
9
+ */
10
+
11
+ export function byCodeUnit(a: string, b: string): number {
12
+ return a < b ? -1 : a > b ? 1 : 0;
13
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @adrkit/evaluator — pure SHA-256.
3
+ *
4
+ * A dependency-free, deterministic SHA-256 over byte arrays / UTF-8 strings, used to
5
+ * verify the Rego-Wasm policy envelope hashes without importing any Node builtin or
6
+ * touching I/O (keeps the evaluator purity gate green). Not a general crypto surface.
7
+ */
8
+
9
+ const K = new Uint32Array([
10
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
11
+ 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
12
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
13
+ 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
14
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
15
+ 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
16
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
17
+ 0xc67178f2,
18
+ ]);
19
+
20
+ function rotr(x: number, n: number): number {
21
+ return (x >>> n) | (x << (32 - n));
22
+ }
23
+
24
+ export function sha256Bytes(message: Uint8Array): Uint8Array {
25
+ const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
26
+
27
+ const length = message.length;
28
+ const bitLength = length * 8;
29
+ const paddedLength = ((length + 8) >> 6) + 1;
30
+ const padded = new Uint8Array(paddedLength * 64);
31
+ padded.set(message);
32
+ padded[length] = 0x80;
33
+ const dv = new DataView(padded.buffer);
34
+ dv.setUint32(padded.length - 4, bitLength >>> 0, false);
35
+ dv.setUint32(padded.length - 8, Math.floor(bitLength / 0x100000000), false);
36
+
37
+ const w = new Uint32Array(64);
38
+ for (let offset = 0; offset < padded.length; offset += 64) {
39
+ for (let i = 0; i < 16; i += 1) w[i] = dv.getUint32(offset + i * 4, false);
40
+ for (let i = 16; i < 64; i += 1) {
41
+ const a = w[i - 15] as number;
42
+ const b = w[i - 2] as number;
43
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
44
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
45
+ w[i] = ((w[i - 16] as number) + s0 + (w[i - 7] as number) + s1) >>> 0;
46
+ }
47
+ let a = h[0]!;
48
+ let b = h[1]!;
49
+ let c = h[2]!;
50
+ let d = h[3]!;
51
+ let e = h[4]!;
52
+ let f = h[5]!;
53
+ let g = h[6]!;
54
+ let hh = h[7]!;
55
+ for (let i = 0; i < 64; i += 1) {
56
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
57
+ const ch = (e & f) ^ (~e & g);
58
+ const t1 = (hh + S1 + ch + (K[i] as number) + (w[i] as number)) >>> 0;
59
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
60
+ const maj = (a & b) ^ (a & c) ^ (b & c);
61
+ const t2 = (S0 + maj) >>> 0;
62
+ hh = g;
63
+ g = f;
64
+ f = e;
65
+ e = (d + t1) >>> 0;
66
+ d = c;
67
+ c = b;
68
+ b = a;
69
+ a = (t1 + t2) >>> 0;
70
+ }
71
+ h[0] = (h[0]! + a) >>> 0;
72
+ h[1] = (h[1]! + b) >>> 0;
73
+ h[2] = (h[2]! + c) >>> 0;
74
+ h[3] = (h[3]! + d) >>> 0;
75
+ h[4] = (h[4]! + e) >>> 0;
76
+ h[5] = (h[5]! + f) >>> 0;
77
+ h[6] = (h[6]! + g) >>> 0;
78
+ h[7] = (h[7]! + hh) >>> 0;
79
+ }
80
+
81
+ const out = new Uint8Array(32);
82
+ const outView = new DataView(out.buffer);
83
+ for (let i = 0; i < 8; i += 1) outView.setUint32(i * 4, h[i]!, false);
84
+ return out;
85
+ }
86
+
87
+ function toHex(bytes: Uint8Array): string {
88
+ let hex = '';
89
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, '0');
90
+ return hex;
91
+ }
92
+
93
+ export function sha256Hex(message: Uint8Array): string {
94
+ return toHex(sha256Bytes(message));
95
+ }
96
+
97
+ const encoder = new TextEncoder();
98
+
99
+ export function sha256HexUtf8(text: string): string {
100
+ return sha256Hex(encoder.encode(text));
101
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @adrkit/evaluator — identity directory resolution.
3
+ *
4
+ * Pure resolution over an immutable identity-directory snapshot (R9). Resolves a
5
+ * principal ref (a direct human or a team) to a single active human, or reports
6
+ * zero/ambiguous. Used by `decider-resolvable` and named-human routing. No I/O.
7
+ */
8
+
9
+ import type { IdentityDirectorySnapshot, Principal, PrincipalRef } from '../types.ts';
10
+
11
+ export type IdentityResolution =
12
+ | { readonly status: 'resolved'; readonly human: PrincipalRef }
13
+ | { readonly status: 'zero' }
14
+ | { readonly status: 'ambiguous' };
15
+
16
+ /** Indexed, pure view over an identity snapshot. */
17
+ export interface IdentityIndex {
18
+ resolveToActiveHuman(ref: PrincipalRef): IdentityResolution;
19
+ isActiveHuman(ref: PrincipalRef): boolean;
20
+ isTeam(ref: PrincipalRef): boolean;
21
+ }
22
+
23
+ export function buildIdentityIndex(directory: IdentityDirectorySnapshot): IdentityIndex {
24
+ const principalsById = new Map<PrincipalRef, Principal>();
25
+ for (const principal of directory.principals) {
26
+ principalsById.set(principal.id, principal);
27
+ }
28
+ const teamMembersById = new Map<PrincipalRef, readonly PrincipalRef[]>();
29
+ for (const team of directory.teams) {
30
+ teamMembersById.set(team.id, team.members);
31
+ }
32
+
33
+ function isActiveHuman(ref: PrincipalRef): boolean {
34
+ const principal = principalsById.get(ref);
35
+ return principal !== undefined && principal.kind === 'human' && principal.active;
36
+ }
37
+
38
+ function isTeam(ref: PrincipalRef): boolean {
39
+ if (teamMembersById.has(ref)) return true;
40
+ const principal = principalsById.get(ref);
41
+ return principal !== undefined && principal.kind === 'team';
42
+ }
43
+
44
+ function activeHumansForTeam(ref: PrincipalRef): PrincipalRef[] {
45
+ const members = teamMembersById.get(ref) ?? [];
46
+ const seen = new Set<PrincipalRef>();
47
+ const humans: PrincipalRef[] = [];
48
+ for (const member of members) {
49
+ if (seen.has(member)) continue;
50
+ if (isActiveHuman(member)) {
51
+ seen.add(member);
52
+ humans.push(member);
53
+ }
54
+ }
55
+ return humans;
56
+ }
57
+
58
+ function resolveToActiveHuman(ref: PrincipalRef): IdentityResolution {
59
+ if (isTeam(ref)) {
60
+ const humans = activeHumansForTeam(ref);
61
+ if (humans.length === 1) return { status: 'resolved', human: humans[0] as PrincipalRef };
62
+ return humans.length === 0 ? { status: 'zero' } : { status: 'ambiguous' };
63
+ }
64
+ if (isActiveHuman(ref)) return { status: 'resolved', human: ref };
65
+ return { status: 'zero' };
66
+ }
67
+
68
+ return { resolveToActiveHuman, isActiveHuman, isTeam };
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @adrkit/evaluator — public surface.
3
+ *
4
+ * Exports the pure `evaluatePass0` entry point, the immutable contract types, the
5
+ * fixed catalogs, the deterministic port interfaces, and the pure in-process port
6
+ * factories the CLI composes at its impure boundary. It exports NO CLI, CI, adapter,
7
+ * or executable snapshot-bundle loader (T011) — those impurities live in `@adrkit/cli`.
8
+ */
9
+
10
+ export { evaluatePass0 } from './pass0.ts';
11
+
12
+ // Fixed catalogs (data-model §7/§8/§11).
13
+ export {
14
+ RULE_IDS,
15
+ RULE_SEVERITY,
16
+ ROUTING_TRIGGERS,
17
+ REASON_CODES,
18
+ RULE_REASON_PRECEDENCE,
19
+ ROUTE_ESCALATE_CODE,
20
+ ROUTE_EVIDENCE_NOT_PROVEN_CODE,
21
+ RUBRIC_VERSION,
22
+ isReasonCode,
23
+ type ReasonCode,
24
+ type RuleId,
25
+ type Pass0EscalationReason,
26
+ } from './catalog.ts';
27
+
28
+ // Canonical key helpers.
29
+ export {
30
+ makeAssertionKey,
31
+ assertionKeyForAssertion,
32
+ isCanonicalAssertionKey,
33
+ parseAssertionKey,
34
+ canonicalTargetKey,
35
+ } from './keys.ts';
36
+
37
+ // Immutable contract types + deterministic port interfaces.
38
+ export type * from './types.ts';
39
+
40
+ // Pure composition helpers (deterministic; no I/O). The CLI builds registries from
41
+ // these — never from snapshot JSON.
42
+ export {
43
+ createTargetResolutionRegistry,
44
+ emptyTargetResolutionRegistry,
45
+ } from './targets/registry.ts';
46
+ export { createPathTargetResolver } from './targets/path.ts';
47
+ export { createPackageTargetResolver } from './targets/package.ts';
48
+ export {
49
+ makeTargetId,
50
+ normalizePathId,
51
+ resolveRecordTargets,
52
+ anyMatcherInert,
53
+ type RecordTargetResolution,
54
+ type MatcherResolution,
55
+ } from './targets/canonical.ts';
56
+ export {
57
+ createAssertionEngineRegistry,
58
+ emptyAssertionEngineRegistry,
59
+ type AssertionEnginePorts,
60
+ } from './assertions/registry.ts';
61
+ export { createJsonPathEngine } from './assertions/jsonpath.ts';
62
+ export { validateRegoWasmPolicyEnvelopeV1, type EnvelopeValidation } from './assertions/rego.ts';
63
+ export { computeAssertionOutcomes, type AssertionOutcomes } from './assertions/evaluate.ts';
64
+ export {
65
+ buildIdentityIndex,
66
+ type IdentityIndex,
67
+ type IdentityResolution,
68
+ } from './identity/directory.ts';
69
+
70
+ // Canonical serialization + ordering (deterministic bytes; R11).
71
+ export {
72
+ canonicalize,
73
+ canonicalStringify,
74
+ canonicalBytes,
75
+ serializeReport,
76
+ serializePatch,
77
+ serializeArtifacts,
78
+ type CanonicalArtifacts,
79
+ } from './report/serialize.ts';
80
+ export { compareRuleFindings, sortRuleFindings } from './report/order.ts';
81
+ export { aggregate, type SubResult } from './report/aggregate.ts';
package/src/keys.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @adrkit/evaluator — canonical key helpers.
3
+ *
4
+ * The compact assertion key and the canonical target key are the two identity
5
+ * strings that keep the report byte-stable and collision-safe (data-model §4/§5).
6
+ * Both are pure string functions.
7
+ */
8
+
9
+ import type { Adr, Assertion } from '@adrkit/core';
10
+ import type { AssertionKey, CanonicalTargetId, CanonicalTargetKey } from './types.ts';
11
+
12
+ /** Compact standard `JSON.stringify([log ?? "", path, id])` — no added whitespace. */
13
+ export function makeAssertionKey(
14
+ log: string | undefined,
15
+ path: string,
16
+ id: string,
17
+ ): AssertionKey {
18
+ return JSON.stringify([log ?? '', path, id]);
19
+ }
20
+
21
+ /** The canonical assertion key for one assertion declared on one record. */
22
+ export function assertionKeyForAssertion(record: Adr, assertion: Assertion): AssertionKey {
23
+ return makeAssertionKey(record.log, record.path, assertion.id);
24
+ }
25
+
26
+ /**
27
+ * A key is canonical iff parsing it yields exactly three strings AND the original
28
+ * key is byte-equal to the compact re-serialization. Whitespace-padded or otherwise
29
+ * noncanonical spellings are rejected, never normalized.
30
+ */
31
+ export function isCanonicalAssertionKey(key: string): boolean {
32
+ let parsed: unknown;
33
+ try {
34
+ parsed = JSON.parse(key);
35
+ } catch {
36
+ return false;
37
+ }
38
+ if (!Array.isArray(parsed) || parsed.length !== 3) return false;
39
+ if (!parsed.every((part): part is string => typeof part === 'string')) return false;
40
+ return JSON.stringify(parsed) === key;
41
+ }
42
+
43
+ /** Parse a canonical assertion key back into its three string components. */
44
+ export function parseAssertionKey(
45
+ key: string,
46
+ ): { readonly log: string; readonly path: string; readonly id: string } | undefined {
47
+ if (!isCanonicalAssertionKey(key)) return undefined;
48
+ const [log, path, id] = JSON.parse(key) as [string, string, string];
49
+ return { log, path, id };
50
+ }
51
+
52
+ /** Stable `${kind}:${id}` serialization used for equality/intersection/ordering. */
53
+ export function canonicalTargetKey(id: CanonicalTargetId): CanonicalTargetKey {
54
+ return `${id.kind}:${id.id}`;
55
+ }