@geonosis/policy 1.0.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/index.js ADDED
@@ -0,0 +1,779 @@
1
+ import {
2
+ toPolicyRule,
3
+ toPolicyRules
4
+ } from "./chunk-JICJ4XEX.js";
5
+
6
+ // src/lattice.ts
7
+ var specificityOf = (scopeKeys, scope) => {
8
+ if (!scope) return 0;
9
+ let score = 0;
10
+ for (const [index, key] of scopeKeys.entries()) {
11
+ if (scope[key] !== void 0) score += 2 ** index;
12
+ }
13
+ return score;
14
+ };
15
+ var appliesTo = (rule, query, scopeKeys) => {
16
+ if (!rule.scope) return true;
17
+ for (const [key, want] of Object.entries(rule.scope)) {
18
+ if (want === void 0) continue;
19
+ if (!scopeKeys.includes(key)) return false;
20
+ if (query[key] !== want) return false;
21
+ }
22
+ return true;
23
+ };
24
+ var unsupportedScopesOf = (rules, scopeKeys) => {
25
+ const out = [];
26
+ for (const rule of rules) {
27
+ const scope = rule.scope;
28
+ if (!scope) continue;
29
+ const keys = Object.keys(scope).filter(
30
+ (key) => scope[key] !== void 0 && !scopeKeys.includes(key)
31
+ );
32
+ if (keys.length > 0) out.push({ id: rule.id, keys });
33
+ }
34
+ return out;
35
+ };
36
+ var unusableBecause = (rule, data) => {
37
+ if (!data.tiers.some((tier) => tier.name === rule.tier)) return `unknown tier "${rule.tier}"`;
38
+ const kind = data.kinds.find((spec) => spec.kind === rule.constraint.kind);
39
+ if (!kind) return `unknown kind "${rule.constraint.kind}"`;
40
+ if (kind.schema && !kind.schema.safeParse(rule.constraint.value).success) {
41
+ return `value ${JSON.stringify(rule.constraint.value)} is not a ${kind.kind}`;
42
+ }
43
+ return void 0;
44
+ };
45
+ var unusableRulesOf = (rules, data) => rules.flatMap((rule) => {
46
+ const reason2 = unusableBecause(rule, data);
47
+ return reason2 ? [{ id: rule.id, reason: reason2 }] : [];
48
+ });
49
+ var usableRulesOf = (rules, data) => rules.filter((rule) => unusableBecause(rule, data) === void 0);
50
+ var unsetKindsOf = (rules, kinds) => {
51
+ const set = new Set(rules.map((rule) => rule.constraint.kind));
52
+ return kinds.map((kind) => kind.kind).filter((kind) => !set.has(kind));
53
+ };
54
+ var sameValue = (kind, a, b) => kind?.sameValue ? kind.sameValue(a, b) : JSON.stringify(a) === JSON.stringify(b);
55
+ var explainDecision = (winner, overrode, conflicts) => {
56
+ const parts = [`${winner.id} (${winner.tier}) \u2014 ${winner.reason}`];
57
+ if (overrode) parts.push(`overrides ${overrode.id} (${overrode.tier}, overridable)`);
58
+ if (conflicts.length > 0) {
59
+ parts.push(
60
+ `CONFLICT with equal authority: ${conflicts.map((rule) => rule.id).join(", ")} \u2014 resolve in policy, not in code`
61
+ );
62
+ }
63
+ return parts.join("; ");
64
+ };
65
+ var resolveConstraint = (data, kind, query = {}, rules = data.rules) => {
66
+ const rank = (tier) => data.tiers.findIndex((spec) => spec.name === tier);
67
+ const specificity = (scope) => specificityOf(data.scopeKeys, scope);
68
+ const pool = usableRulesOf(rules, data).filter(
69
+ (rule) => rule.constraint.kind === kind && appliesTo(rule, query, data.scopeKeys)
70
+ );
71
+ if (pool.length === 0) return void 0;
72
+ const sorted = pool.map((rule, index) => ({ index, rule })).toSorted((a, b) => {
73
+ const byTier = rank(a.rule.tier) - rank(b.rule.tier);
74
+ if (byTier !== 0) return byTier;
75
+ const bySpecificity = specificity(b.rule.scope) - specificity(a.rule.scope);
76
+ return bySpecificity === 0 ? a.index - b.index : bySpecificity;
77
+ }).map((entry) => entry.rule);
78
+ const strongest = sorted[0];
79
+ if (!strongest) return void 0;
80
+ const tierOf = (rule) => data.tiers.find((spec) => spec.name === rule.tier);
81
+ const mayBeOverridden = (rule) => tierOf(rule)?.overridable === true && (rule.overridable ?? data.overridableByDefault);
82
+ const overrideTier = data.tiers.find((tier) => tier.overrides)?.name;
83
+ const override = overrideTier ? sorted.find((rule) => rule.tier === overrideTier) : void 0;
84
+ const overrode = override && override !== strongest && mayBeOverridden(strongest) ? strongest : void 0;
85
+ const winner = overrode ? override : strongest;
86
+ const suppressed = sorted.filter((rule) => rule !== winner);
87
+ const kindSpec = data.kinds.find((spec) => spec.kind === kind);
88
+ const conflicts = suppressed.filter(
89
+ (rule) => rank(rule.tier) === rank(winner.tier) && specificity(rule.scope) === specificity(winner.scope) && !sameValue(kindSpec, rule.constraint.value, winner.constraint.value)
90
+ );
91
+ return {
92
+ conflicts,
93
+ explain: explainDecision(winner, overrode, conflicts),
94
+ kind,
95
+ ...overrode ? { overrode } : {},
96
+ suppressed,
97
+ value: winner.constraint.value,
98
+ winner
99
+ };
100
+ };
101
+
102
+ // src/define.ts
103
+ var asTiers = (values) => values.map((value) => typeof value === "string" ? { name: value } : value);
104
+ var asKinds = (values) => values.map((value) => typeof value === "string" ? { kind: value } : value);
105
+ var refuseDuplicates = (values, what) => {
106
+ const seen = /* @__PURE__ */ new Set();
107
+ for (const value of values) {
108
+ if (seen.has(value)) {
109
+ throw new TypeError(
110
+ `definePolicy: ${what} "${value}" is declared twice. Which one a rule means would then depend on which was found first.`
111
+ );
112
+ }
113
+ seen.add(value);
114
+ }
115
+ };
116
+ var definePolicy = (input) => {
117
+ const tiers = asTiers(input.tiers);
118
+ const kinds = asKinds(input.kinds);
119
+ const scopeKeys = [...input.scopeKeys ?? []];
120
+ const rules = [...input.rules ?? []];
121
+ if (tiers.length === 0) {
122
+ throw new TypeError(
123
+ "definePolicy: `tiers` is empty. A lattice with no authorities could never decide anything, and every rule written against it would name a tier it does not have."
124
+ );
125
+ }
126
+ refuseDuplicates(
127
+ tiers.map((tier) => tier.name),
128
+ "tier"
129
+ );
130
+ refuseDuplicates(
131
+ kinds.map((kind) => kind.kind),
132
+ "kind"
133
+ );
134
+ refuseDuplicates(scopeKeys, "scope key");
135
+ const overriding = tiers.filter((tier) => tier.overrides);
136
+ if (overriding.length > 1) {
137
+ throw new TypeError(
138
+ `definePolicy: ${overriding.map((tier) => `"${tier.name}"`).join(
139
+ " and "
140
+ )} both declare \`overrides\`. The audited escape hatch is one tier or none \u2014 two of them race for the same rule.`
141
+ );
142
+ }
143
+ const lastAuthority = tiers.findLastIndex((tier) => !tier.advisory);
144
+ const firstAdvisory = tiers.findIndex((tier) => tier.advisory);
145
+ if (firstAdvisory !== -1 && firstAdvisory < lastAuthority) {
146
+ throw new TypeError(
147
+ `definePolicy: advisory tier "${tiers[firstAdvisory]?.name}" is declared stronger than "${tiers[lastAuthority]?.name}". An advisory tier fills silence; one that outranks an authority is not advisory, it is an authority with a reassuring name.`
148
+ );
149
+ }
150
+ const data = {
151
+ kinds,
152
+ overridableByDefault: input.overridableByDefault ?? true,
153
+ rules,
154
+ scopeKeys,
155
+ tiers
156
+ };
157
+ const journalledOnce = /* @__PURE__ */ new Set();
158
+ return {
159
+ ...data,
160
+ journalledOnce,
161
+ kindOf: (kind) => kinds.find((spec) => spec.kind === kind),
162
+ rank: (tier) => {
163
+ const index = tiers.findIndex((spec) => spec.name === tier);
164
+ return index === -1 ? void 0 : index;
165
+ },
166
+ resetJournal: () => journalledOnce.clear(),
167
+ resolve: (kind, query, given) => resolveConstraint(data, kind, query, given),
168
+ specificity: (scope) => specificityOf(scopeKeys, scope),
169
+ tierOf: (tier) => tiers.find((spec) => spec.name === tier),
170
+ unsetKinds: (given = rules) => unsetKindsOf(given, kinds),
171
+ unsupportedScopes: (given = rules) => unsupportedScopesOf(given, scopeKeys),
172
+ unusableRules: (given = rules) => unusableRulesOf(given, data),
173
+ usableRules: (given = rules) => usableRulesOf(given, data)
174
+ };
175
+ };
176
+
177
+ // src/types.ts
178
+ var POLICY_FALLBACK = "policy.fallback";
179
+ var POLICY_INVALID_VALUE = "policy.invalid_value";
180
+
181
+ // src/read.ts
182
+ var attempt = async (what) => {
183
+ try {
184
+ return await what();
185
+ } catch {
186
+ return void 0;
187
+ }
188
+ };
189
+ var accepts = (policy, kind, value) => {
190
+ const schema = policy.kindOf(kind)?.schema;
191
+ return schema ? schema.safeParse(value).success : true;
192
+ };
193
+ var record = async (policy, journal, entry) => {
194
+ const key = `${entry.event}::${entry.kind}::${entry.op ?? ""}`;
195
+ if (policy.journalledOnce.has(key)) return;
196
+ policy.journalledOnce.add(key);
197
+ await attempt(() => journal?.write(entry));
198
+ };
199
+ var readPolicy = async (policy, kind, query = {}, options) => {
200
+ const rules = options.rule ? await attempt(() => options.rule?.rules(kind, query)) ?? [] : policy.rules;
201
+ const decision = policy.resolve(kind, query, rules);
202
+ if (decision) {
203
+ const spec = policy.kindOf(kind);
204
+ if (spec?.isReference?.(decision.value)) {
205
+ const referenced = await attempt(
206
+ () => spec.dereference?.(decision.value, { config: options.config, query })
207
+ );
208
+ if (referenced !== void 0) {
209
+ return { decision, source: "rule", usedFallback: false, value: referenced };
210
+ }
211
+ } else {
212
+ return { decision, source: "rule", usedFallback: false, value: decision.value };
213
+ }
214
+ }
215
+ const configured = await attempt(() => options.config?.get(kind, query));
216
+ if (configured !== void 0 && accepts(policy, kind, configured)) {
217
+ return { source: "config", usedFallback: false, value: configured };
218
+ }
219
+ if (!options.silent) {
220
+ await record(policy, options.journal, {
221
+ event: POLICY_FALLBACK,
222
+ fellBackTo: options.fallback,
223
+ kind,
224
+ ...options.op ? { op: options.op } : {},
225
+ ...options.subject ? { subject: options.subject } : {}
226
+ });
227
+ }
228
+ return { source: "fallback", usedFallback: true, value: options.fallback };
229
+ };
230
+ var policyOrDefault = async (policy, input, apply) => {
231
+ try {
232
+ return apply(input.value);
233
+ } catch (error) {
234
+ await record(policy, input.journal, {
235
+ event: POLICY_INVALID_VALUE,
236
+ fellBackTo: input.fallback,
237
+ kind: input.kind,
238
+ op: input.op,
239
+ rejected: {
240
+ reason: error instanceof Error ? error.message : String(error),
241
+ value: input.value
242
+ },
243
+ ...input.subject ? { subject: input.subject } : {}
244
+ });
245
+ return apply(input.fallback);
246
+ }
247
+ };
248
+
249
+ // src/conformance.ts
250
+ var probePolicy = () => definePolicy({
251
+ kinds: ["quoteExpiryDays", "containerCutoff", "aiCostCapUsd", "unread"],
252
+ scopeKeys: ["companyId"],
253
+ tiers: ["contract", { name: "tenant", overridable: true }]
254
+ });
255
+ var reason = "the reason a person blocked by this rule would read";
256
+ var ROWS = [
257
+ {
258
+ constraint_kind: "quoteExpiryDays",
259
+ constraint_value: 30,
260
+ reason,
261
+ rule_key: "expiry-30",
262
+ tier: "tenant"
263
+ },
264
+ {
265
+ constraint_kind: "quoteExpiryDays",
266
+ constraint_value: 60,
267
+ reason,
268
+ rule_key: "expiry-60-acme",
269
+ scope: { companyId: "comp_acme" },
270
+ tier: "tenant"
271
+ },
272
+ {
273
+ constraint_kind: "containerCutoff",
274
+ constraint_value: { dayOfWeek: 5, hourUtc: 18 },
275
+ reason,
276
+ rule_key: "cutoff-friday",
277
+ tier: "tenant"
278
+ },
279
+ {
280
+ constraint_kind: "aiCostCapUsd",
281
+ constraint_value: 0.5,
282
+ overridable: false,
283
+ reason,
284
+ rule_key: "cost-cap",
285
+ tier: "tenant"
286
+ },
287
+ {
288
+ constraint_kind: "quoteExpiryDays",
289
+ constraint_value: 1,
290
+ reason,
291
+ rule_key: "expiry-retired",
292
+ status: "retired",
293
+ tier: "contract"
294
+ }
295
+ ];
296
+ var check = async (name, assertion) => {
297
+ try {
298
+ await assertion();
299
+ return { name, passed: true };
300
+ } catch (error) {
301
+ return { detail: error instanceof Error ? error.message : String(error), name, passed: false };
302
+ }
303
+ };
304
+ var must = (condition, detail) => {
305
+ if (!condition) throw new Error(detail);
306
+ };
307
+ var same = (actual, expected, what) => must(
308
+ JSON.stringify(actual) === JSON.stringify(expected),
309
+ `${what}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
310
+ );
311
+ var runPolicyAdapterConformance = async (harness) => {
312
+ const over = (config) => harness.create({ config, rows: ROWS });
313
+ const checks = [
314
+ await check("reads a stored rule back as one the lattice can use", async () => {
315
+ const rules = await over().adapter.rule.rules("quoteExpiryDays", {});
316
+ const found = rules.find((rule) => rule.id === "expiry-30");
317
+ must(found !== void 0, "no rule came back under the key it was stored with");
318
+ same(found?.constraint, { kind: "quoteExpiryDays", value: 30 }, "the constraint");
319
+ same(found?.tier, "tenant", "the authority");
320
+ same(found?.reason, reason, "the reason");
321
+ }),
322
+ await check("leaves a retired row out, because it is not a current opinion", async () => {
323
+ const rules = await over().adapter.rule.rules("quoteExpiryDays", {});
324
+ must(
325
+ !rules.some((rule) => rule.id === "expiry-retired"),
326
+ "a retired rule came back and would have decided"
327
+ );
328
+ }),
329
+ await check("keeps the scope, so a rule reaches only the subject it was aimed at", async () => {
330
+ const { adapter } = over();
331
+ const policy = probePolicy();
332
+ const rules = await adapter.rule.rules("quoteExpiryDays", {});
333
+ same(
334
+ policy.resolve("quoteExpiryDays", { companyId: "comp_acme" }, rules)?.value,
335
+ 60,
336
+ "the scoped rule"
337
+ );
338
+ same(
339
+ policy.resolve("quoteExpiryDays", { companyId: "comp_other" }, rules)?.value,
340
+ 30,
341
+ "the general rule"
342
+ );
343
+ }),
344
+ await check("carries a structured value through the store unchanged", async () => {
345
+ const rules = await over().adapter.rule.rules("containerCutoff", {});
346
+ same(
347
+ rules.find((rule) => rule.id === "cutoff-friday")?.constraint.value,
348
+ { dayOfWeek: 5, hourUtc: 18 },
349
+ "the schedule"
350
+ );
351
+ }),
352
+ await check("keeps a row that refuses to be overridden", async () => {
353
+ const rules = await over().adapter.rule.rules("aiCostCapUsd", {});
354
+ same(rules.find((rule) => rule.id === "cost-cap")?.overridable, false, "the override flag");
355
+ }),
356
+ await check("answers nothing for a kind the store holds no value for", async () => {
357
+ const value = await over().adapter.config?.get("unread", {});
358
+ must(value === void 0, `an unset kind answered with ${JSON.stringify(value)}`);
359
+ }),
360
+ await check("reads an admin-set value when no rule speaks", async () => {
361
+ const { adapter } = over({ unread: 7 });
362
+ const reading = await readPolicy(
363
+ probePolicy(),
364
+ "unread",
365
+ {},
366
+ {
367
+ config: adapter.config,
368
+ fallback: 1,
369
+ rule: adapter.rule
370
+ }
371
+ );
372
+ same(reading.value, 7, "the admin-set value");
373
+ same(reading.source, "config", "which layer decided");
374
+ }),
375
+ await check("records a fallback, naming the kind that had nothing to say", async () => {
376
+ const { adapter, written } = over();
377
+ await readPolicy(
378
+ probePolicy(),
379
+ "unread",
380
+ {},
381
+ {
382
+ config: adapter.config,
383
+ fallback: 1,
384
+ journal: adapter.journal,
385
+ op: "a-named-operation",
386
+ rule: adapter.rule
387
+ }
388
+ );
389
+ const entries = written();
390
+ same(entries.length, 1, "how many records one fallback produced");
391
+ must(
392
+ JSON.stringify(entries).includes("unread"),
393
+ "the record does not name the kind, so nobody can count it"
394
+ );
395
+ must(
396
+ JSON.stringify(entries).includes("a-named-operation"),
397
+ "the record does not name the operation, so repeats cannot be told apart"
398
+ );
399
+ }),
400
+ await check("does not let a store that is down throw out of a config read", async () => {
401
+ await harness.createBroken().config?.get("unread", {});
402
+ })
403
+ ];
404
+ return { adapter: harness.name, checks, passed: checks.every((entry) => entry.passed) };
405
+ };
406
+
407
+ // src/learning.ts
408
+ var emptyPlaybook = () => ({ entries: [], version: 0 });
409
+ var DEFAULT_MIN_OBSERVATIONS = 3;
410
+ var LearningError = class extends Error {
411
+ constructor(code, message) {
412
+ super(`${code}: ${message}`);
413
+ this.code = code;
414
+ this.name = "LearningError";
415
+ }
416
+ code;
417
+ };
418
+ var DEFAULT_REFUSAL_PREFIX = "POLICY_REFUSED";
419
+ var DEFAULT_REVERSAL_VERBS = /\b(override|revert|undo|cancel|reassign|reschedule)\b/i;
420
+ var observeReversals = (entries, verbs) => {
421
+ const out = [];
422
+ for (const [index, decided] of entries.entries()) {
423
+ const next = entries[index + 1];
424
+ if (!next) continue;
425
+ if (decided.actor.kind === "user" || next.actor.kind !== "user") continue;
426
+ if (!decided.subject || decided.subject !== next.subject) continue;
427
+ if (!verbs.test(next.name)) continue;
428
+ const op = decided.op ?? decided.name;
429
+ out.push({
430
+ id: `reversed:${op}`,
431
+ journalRefs: [decided.id, next.id],
432
+ kind: "pitfall",
433
+ statement: `A person reversed the automated "${op}" on ${decided.subject} (via ${next.name}) \u2014 check the decision before repeating it`
434
+ });
435
+ }
436
+ return out;
437
+ };
438
+ var observeRuleFriction = (entry, prefix) => {
439
+ if (!entry.error) return [];
440
+ const [, ruleId, detail] = new RegExp(`${prefix}: ([\\w:.-]+)(?: \u2014 (.*))?`).exec(entry.error) ?? [];
441
+ if (!ruleId) return [];
442
+ return [
443
+ {
444
+ id: `friction:${ruleId}`,
445
+ journalRefs: [entry.id],
446
+ kind: "rule-friction",
447
+ statement: `Rule "${ruleId}" blocked an action again${detail ? ` \u2014 ${detail.slice(0, 140)}` : ""} \u2014 amend the rule or stop attempting it`
448
+ }
449
+ ];
450
+ };
451
+ var observeUnsetPolicy = (entry) => {
452
+ const fell = entry.fellBackTo;
453
+ if (!fell) return [];
454
+ return [
455
+ {
456
+ id: `unset:${fell.kind}`,
457
+ journalRefs: [entry.id],
458
+ kind: "unset-policy",
459
+ statement: `No rule sets "${fell.kind}", so the hardcoded default ${JSON.stringify(
460
+ fell.value
461
+ )} decided instead \u2014 write the rule or delete the knob`
462
+ }
463
+ ];
464
+ };
465
+ var observeRecurringOps = (entries, minimum) => {
466
+ const byOp = /* @__PURE__ */ new Map();
467
+ for (const entry of entries) {
468
+ if (entry.error || !entry.op) continue;
469
+ const refs = byOp.get(entry.op);
470
+ if (refs) refs.push(entry.id);
471
+ else byOp.set(entry.op, [entry.id]);
472
+ }
473
+ return [...byOp].filter(([, refs]) => refs.length >= minimum).map(([op, refs]) => ({
474
+ id: `macro:${op}`,
475
+ journalRefs: refs,
476
+ kind: "macro",
477
+ statement: `"${op}" ran ${refs.length} times \u2014 save it as a named strategy`
478
+ }));
479
+ };
480
+ var observeJournal = (entries, options = {}) => [
481
+ ...observeReversals(entries, options.reversalVerbs ?? DEFAULT_REVERSAL_VERBS),
482
+ ...entries.flatMap((entry) => [
483
+ ...observeRuleFriction(entry, options.refusalPrefix ?? DEFAULT_REFUSAL_PREFIX),
484
+ ...observeUnsetPolicy(entry)
485
+ ]),
486
+ ...observeRecurringOps(entries, options.minRepeatsForMacro ?? 2)
487
+ ];
488
+ var reflect = (observations, now = /* @__PURE__ */ new Date()) => {
489
+ const byId = /* @__PURE__ */ new Map();
490
+ const at = now.toISOString();
491
+ for (const observation of observations) {
492
+ const existing = byId.get(observation.id);
493
+ if (existing) {
494
+ existing.evidence.observations += 1;
495
+ existing.evidence.journalRefs = [
496
+ .../* @__PURE__ */ new Set([...existing.evidence.journalRefs, ...observation.journalRefs])
497
+ ];
498
+ continue;
499
+ }
500
+ byId.set(observation.id, {
501
+ evidence: { journalRefs: [...observation.journalRefs], observations: 1, updatedAt: at },
502
+ id: observation.id,
503
+ kind: observation.kind,
504
+ ...observation.scope ? { scope: observation.scope } : {},
505
+ statement: observation.statement,
506
+ status: "candidate",
507
+ ...observation.suggests ? { suggests: observation.suggests } : {}
508
+ });
509
+ }
510
+ return [...byId.values()];
511
+ };
512
+ var curate = (playbook, candidates) => {
513
+ const entries = playbook.entries.map((entry) => ({ ...entry, evidence: { ...entry.evidence } }));
514
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
515
+ for (const candidate of candidates) {
516
+ const existing = byId.get(candidate.id);
517
+ if (!existing) {
518
+ entries.push(candidate);
519
+ byId.set(candidate.id, candidate);
520
+ continue;
521
+ }
522
+ if (existing.status === "demoted") continue;
523
+ existing.evidence.observations += candidate.evidence.observations;
524
+ existing.evidence.journalRefs = [
525
+ .../* @__PURE__ */ new Set([...existing.evidence.journalRefs, ...candidate.evidence.journalRefs])
526
+ ];
527
+ existing.evidence.updatedAt = candidate.evidence.updatedAt;
528
+ if (existing.status === "candidate") existing.statement = candidate.statement;
529
+ }
530
+ return { entries, version: playbook.version + 1 };
531
+ };
532
+ var promote = (playbook, id, options = {}) => {
533
+ const minimum = options.minObservations ?? DEFAULT_MIN_OBSERVATIONS;
534
+ const entry = playbook.entries.find((candidate) => candidate.id === id);
535
+ if (!entry) throw new LearningError("UNKNOWN_LEARNING", `no learning "${id}"`);
536
+ if (entry.evidence.observations < minimum) {
537
+ throw new LearningError(
538
+ "INSUFFICIENT_EVIDENCE",
539
+ `"${id}" has ${entry.evidence.observations} observation(s), needs ${minimum}`
540
+ );
541
+ }
542
+ return {
543
+ entries: playbook.entries.map(
544
+ (candidate) => candidate.id === id ? {
545
+ ...candidate,
546
+ evidence: {
547
+ ...candidate.evidence,
548
+ ...options.metric ? { metric: options.metric } : {}
549
+ },
550
+ status: "active"
551
+ } : candidate
552
+ ),
553
+ version: playbook.version + 1
554
+ };
555
+ };
556
+ var demote = (playbook, id, reason2) => {
557
+ if (!playbook.entries.some((entry) => entry.id === id)) {
558
+ throw new LearningError("UNKNOWN_LEARNING", `no learning "${id}"`);
559
+ }
560
+ return {
561
+ entries: playbook.entries.map(
562
+ (entry) => entry.id === id ? { ...entry, demotedReason: reason2, status: "demoted" } : entry
563
+ ),
564
+ version: playbook.version + 1
565
+ };
566
+ };
567
+ var playbookRules = (policy, playbook) => {
568
+ const advisory = policy.tiers.find((tier) => tier.advisory);
569
+ if (!advisory) {
570
+ throw new TypeError(
571
+ "playbookRules: this policy declares no advisory tier. A learning has to enter the lattice somewhere it can only fill silence; without one, promoting it would give a suggestion the authority of an authored rule."
572
+ );
573
+ }
574
+ return playbook.entries.flatMap((entry) => {
575
+ const suggests = entry.suggests;
576
+ if (entry.status !== "active" || !suggests) return [];
577
+ return [
578
+ {
579
+ constraint: suggests,
580
+ id: `${advisory.name}:${entry.id}`,
581
+ reason: `${entry.statement} (${entry.evidence.observations} observations)`,
582
+ ...entry.scope ? { scope: entry.scope } : {},
583
+ tier: advisory.name
584
+ }
585
+ ];
586
+ });
587
+ };
588
+ var playbookGuidance = (playbook) => {
589
+ const active = playbook.entries.filter((entry) => entry.status === "active");
590
+ if (active.length === 0) return "";
591
+ const lines = active.map(
592
+ (entry) => `- [${entry.kind}] ${entry.statement} (${entry.evidence.observations} observations)`
593
+ );
594
+ return `Learned playbook (v${playbook.version}, advisory):
595
+ ${lines.join("\n")}`;
596
+ };
597
+
598
+ // src/journal-events.ts
599
+ var firstString = (data, keys) => {
600
+ for (const key of keys) {
601
+ const value = data[key];
602
+ if (typeof value === "string" && value.length > 0) return value;
603
+ }
604
+ return null;
605
+ };
606
+ var journalRowForEvent = (eventName, data, options) => {
607
+ const payload = Array.isArray(data) ? data[0] : data;
608
+ if (!payload || typeof payload !== "object") return null;
609
+ const subject = firstString(payload, options.subjectKeys);
610
+ if (!subject) return null;
611
+ const actorId = firstString(payload, options.actorKeys ?? []);
612
+ return {
613
+ actor_id: actorId,
614
+ actor_kind: actorId ? "user" : "system",
615
+ event_name: eventName,
616
+ // The STEM, not the full name: two outcomes of one decision are the same operation being run
617
+ // again, and the recurring-ops signal counts operations.
618
+ op: eventName.split(".")[0] ?? eventName,
619
+ subject
620
+ };
621
+ };
622
+
623
+ // src/markdown.ts
624
+ var scopeOf = (rule) => {
625
+ const entries = Object.entries(rule.scope ?? {}).filter(([, value]) => value !== void 0);
626
+ return entries.length === 0 ? "all" : entries.map(([key, value]) => `${key}=${String(value)}`).join(", ");
627
+ };
628
+ var generatePolicyMarkdown = (policy, options = {}) => {
629
+ const rules = options.rules ?? policy.rules;
630
+ const lines = [
631
+ `# ${options.title ?? "Policy"}`,
632
+ "",
633
+ `> GENERATED by ${options.generator ?? "the policy kernel"} \u2014 do not hand-edit.`,
634
+ `> ${options.source ?? "The policy data"} beside it is what the engine reads.`,
635
+ "> Committed on purpose: this is the domain knowledge the business has accumulated, and it",
636
+ "> should arrive with a checkout.",
637
+ "",
638
+ `## Rules (${rules.length}) \u2014 enforced`,
639
+ "",
640
+ // DERIVED, and printed whether or not any rule uses it: the ordering is a property of the
641
+ // lattice, not of today's rules. Written as prose this sentence lies the moment a tier is added
642
+ // or reordered — in a document whose whole purpose is that a fresh checkout starts knowing the
643
+ // truth.
644
+ `Precedence: ${policy.tiers.map((tier) => tier.name).join(" > ")}.`,
645
+ "Within a tier the more specific scope wins; two equal authorities that disagree are reported,",
646
+ "never resolved silently.",
647
+ ""
648
+ ];
649
+ if (rules.length === 0) {
650
+ lines.push("_None installed. Every reader falls back to its own constant, and says so._", "");
651
+ } else {
652
+ lines.push(
653
+ "| Rule | Tier | Scope | Constraint | Why |",
654
+ "| --- | --- | --- | --- | --- |",
655
+ ...rules.map(
656
+ (rule) => `| \`${rule.id}\` | ${rule.tier} | ${scopeOf(rule)} | \`${rule.constraint.kind}\` = \`${JSON.stringify(rule.constraint.value)}\` | ${rule.reason} |`
657
+ ),
658
+ ""
659
+ );
660
+ }
661
+ const unset = policy.unsetKinds(rules);
662
+ if (unset.length > 0) {
663
+ lines.push(
664
+ `## Constraints nothing sets (${unset.length})`,
665
+ "",
666
+ "A reader falling back to its own default forever looks identical to one that is working.",
667
+ "",
668
+ ...unset.map((kind) => `- \`${kind}\``),
669
+ ""
670
+ );
671
+ }
672
+ const unusable = [
673
+ ...policy.unusableRules(rules).map((rule) => `- \`${rule.id}\` \u2014 ${rule.reason}`),
674
+ ...policy.unsupportedScopes(rules).map(
675
+ (rule) => `- \`${rule.id}\` \u2014 scoped on ${rule.keys.join(", ")}, which this build has no key for`
676
+ )
677
+ ];
678
+ if (unusable.length > 0) {
679
+ lines.push(
680
+ `## Rules this build cannot honour (${unusable.length})`,
681
+ "",
682
+ "Named rather than dropped: a rule the code cannot read means the policy is newer than the",
683
+ "build, or older \u2014 a deployment-order problem, and the one case always worth saying aloud.",
684
+ "",
685
+ ...unusable,
686
+ ""
687
+ );
688
+ }
689
+ const playbook = options.playbook;
690
+ if (playbook) {
691
+ const active = playbook.entries.filter((entry) => entry.status === "active");
692
+ const candidates = playbook.entries.filter((entry) => entry.status === "candidate");
693
+ lines.push(
694
+ `## Learned playbook (v${playbook.version})`,
695
+ "",
696
+ `${active.length} active, ${candidates.length} candidate. An active entry that carries a`,
697
+ "constraint enters the lattice at the advisory tier, where it can only fill silence.",
698
+ "",
699
+ ...active.map((entry) => `- **${entry.id}** \u2014 ${entry.statement}`),
700
+ ""
701
+ );
702
+ }
703
+ return `${lines.join("\n")}
704
+ `;
705
+ };
706
+ var describePolicy = (policy, options = {}) => {
707
+ const rules = options.rules ?? policy.rules;
708
+ const maximum = options.maxRules ?? 8;
709
+ const active = (options.playbook?.entries ?? []).filter((entry) => entry.status === "active");
710
+ if (rules.length === 0) {
711
+ return "No policy is installed yet \u2014 nothing is enforced, and every reader falls back to the constant compiled into it.";
712
+ }
713
+ const lines = [
714
+ `ACTIVE POLICY (${rules.length} rule${rules.length === 1 ? "" : "s"}, enforced \u2014 a refusal names the rule that blocked it):`,
715
+ ...rules.slice(0, maximum).map((rule) => {
716
+ const scope = rule.scope && Object.keys(rule.scope).length > 0 ? ` [${scopeOf(rule)}]` : "";
717
+ return ` ${rule.tier}:${rule.id}${scope} \u2014 ${rule.constraint.kind}=${JSON.stringify(
718
+ rule.constraint.value
719
+ )} (${rule.reason})`;
720
+ })
721
+ ];
722
+ if (rules.length > maximum) {
723
+ lines.push(` + ${rules.length - maximum} more \u2014 read the whole set before deciding.`);
724
+ }
725
+ lines.push(
726
+ " A more specific scope beats a general one inside a tier; stronger tiers win outright."
727
+ );
728
+ if (active.length > 0) {
729
+ lines.push("", "LEARNED (advisory \u2014 fills silence, never overrides a rule):");
730
+ for (const entry of active.slice(0, 4)) lines.push(` ${entry.statement}`);
731
+ }
732
+ return lines.join("\n");
733
+ };
734
+
735
+ // src/seed.ts
736
+ var sameScope = (left, right) => {
737
+ const a = left ?? {};
738
+ const b = right ?? {};
739
+ return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].every((key) => a[key] === b[key]);
740
+ };
741
+ var rulesToSeed = (seed, existing) => {
742
+ const active = existing.filter((rule) => (rule.status ?? "active") === "active");
743
+ return seed.filter(
744
+ (rule) => !active.some(
745
+ (row) => row.id === rule.id || row.constraint.kind === rule.constraint.kind && sameScope(row.scope, rule.scope)
746
+ )
747
+ );
748
+ };
749
+ export {
750
+ DEFAULT_MIN_OBSERVATIONS,
751
+ LearningError,
752
+ POLICY_FALLBACK,
753
+ POLICY_INVALID_VALUE,
754
+ appliesTo,
755
+ curate,
756
+ definePolicy,
757
+ demote,
758
+ describePolicy,
759
+ emptyPlaybook,
760
+ generatePolicyMarkdown,
761
+ journalRowForEvent,
762
+ observeJournal,
763
+ playbookGuidance,
764
+ playbookRules,
765
+ policyOrDefault,
766
+ promote,
767
+ readPolicy,
768
+ reflect,
769
+ resolveConstraint,
770
+ rulesToSeed,
771
+ runPolicyAdapterConformance,
772
+ specificityOf,
773
+ toPolicyRule,
774
+ toPolicyRules,
775
+ unsetKindsOf,
776
+ unsupportedScopesOf,
777
+ unusableRulesOf,
778
+ usableRulesOf
779
+ };