@mindot/will 0.5.0 → 0.7.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 (136) hide show
  1. package/dist/channels/discord.d.ts +1 -1
  2. package/dist/channels/discord.js.map +1 -1
  3. package/dist/channels/whatsapp.d.ts +1 -1
  4. package/dist/channels/whatsapp.js.map +1 -1
  5. package/dist/cli.js +998 -59
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +998 -59
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp/effectors.d.ts +1 -1
  11. package/dist/mcp/effectors.js.map +1 -1
  12. package/dist/{will-BDq-TMQr.d.ts → will-DAW0l-lY.d.ts} +90 -10
  13. package/package.json +1 -1
  14. package/src/channels/discord.ts +11 -11
  15. package/src/channels/roster.ts +1 -1
  16. package/src/channels/types.ts +1 -1
  17. package/src/channels/whatsapp.ts +25 -25
  18. package/src/cli.ts +20 -20
  19. package/src/cognition/acp.ts +28 -0
  20. package/src/cognition/agency/consequence.ts +174 -0
  21. package/src/cognition/agency/engines/action.selector.ts +333 -48
  22. package/src/cognition/agency/engines/affordance.synthesizer.ts +35 -16
  23. package/src/cognition/agency/engines/deliberation.engine.ts +28 -14
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +90 -14
  25. package/src/cognition/agency/engines/reafference.engine.ts +152 -23
  26. package/src/cognition/agency/execution.primitives.ts +13 -13
  27. package/src/cognition/agency/proactive.communicator.ts +1 -1
  28. package/src/cognition/agency/reconcile.learning.ts +10 -1
  29. package/src/cognition/agency/revocation.ts +79 -0
  30. package/src/cognition/agency/schemas/repertoire.ts +109 -9
  31. package/src/cognition/agency/selection.scoring.ts +32 -10
  32. package/src/cognition/agency/types.ts +9 -0
  33. package/src/cognition/bus.ts +2 -2
  34. package/src/cognition/completion.inbox.ts +1 -1
  35. package/src/cognition/config.mirror.entities.ts +5 -5
  36. package/src/cognition/event.log.ts +2 -2
  37. package/src/cognition/event.schemas.ts +67 -67
  38. package/src/cognition/faculties/aesthetic.evaluator.ts +6 -6
  39. package/src/cognition/faculties/affective.blender.ts +36 -4
  40. package/src/cognition/faculties/attachment.evaluator.ts +6 -6
  41. package/src/cognition/faculties/attention.allocator.ts +63 -8
  42. package/src/cognition/faculties/autobiographical.narrator.ts +3 -3
  43. package/src/cognition/faculties/bias.detector.ts +7 -7
  44. package/src/cognition/faculties/circadian.oscillator.ts +3 -3
  45. package/src/cognition/faculties/confidence.calibrator.ts +7 -7
  46. package/src/cognition/faculties/dream.simulator.ts +5 -5
  47. package/src/cognition/faculties/empathy.simulator.ts +7 -7
  48. package/src/cognition/faculties/energy.regulator.ts +4 -4
  49. package/src/cognition/faculties/episodic.consolidator.ts +6 -6
  50. package/src/cognition/faculties/executive.engine/commands.ts +6 -6
  51. package/src/cognition/faculties/executive.engine/context.ts +42 -42
  52. package/src/cognition/faculties/executive.engine/deferred.effects.ts +2 -2
  53. package/src/cognition/faculties/executive.engine/engine.ts +25 -25
  54. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +12 -12
  55. package/src/cognition/faculties/executive.engine/facet.ts +18 -18
  56. package/src/cognition/faculties/executive.engine/gating.ts +2 -2
  57. package/src/cognition/faculties/executive.engine/messages.ts +3 -3
  58. package/src/cognition/faculties/executive.engine/parser.ts +11 -11
  59. package/src/cognition/faculties/executive.engine/prompt.factory.ts +55 -55
  60. package/src/cognition/faculties/exteroception.ts +99 -5
  61. package/src/cognition/faculties/forgetting.curve.ts +3 -3
  62. package/src/cognition/faculties/frustration.evaluator.ts +6 -6
  63. package/src/cognition/faculties/goal.manager.ts +15 -15
  64. package/src/cognition/faculties/inhibition.controller.ts +3 -3
  65. package/src/cognition/faculties/interoception.ts +3 -3
  66. package/src/cognition/faculties/introspection.engine.ts +3 -3
  67. package/src/cognition/faculties/known.entity.tracker.ts +15 -15
  68. package/src/cognition/faculties/loss.evaluator.ts +2 -2
  69. package/src/cognition/faculties/moral.evaluator.ts +4 -4
  70. package/src/cognition/faculties/novelty.detector.ts +4 -4
  71. package/src/cognition/faculties/persona.consolidator.ts +12 -12
  72. package/src/cognition/faculties/planning.engine/engine.ts +47 -47
  73. package/src/cognition/faculties/planning.engine/plan.frontier.ts +5 -5
  74. package/src/cognition/faculties/planning.engine/plan.store.ts +1 -1
  75. package/src/cognition/faculties/planning.engine/plan.supervision.ts +20 -20
  76. package/src/cognition/faculties/reputation.tracker.ts +6 -6
  77. package/src/cognition/faculties/reward.evaluator.ts +4 -4
  78. package/src/cognition/faculties/self.model.updater.ts +8 -8
  79. package/src/cognition/faculties/semantic.engine/integrator.ts +15 -15
  80. package/src/cognition/faculties/sleep.pressure.regulator.ts +4 -4
  81. package/src/cognition/faculties/social.perception.ts +3 -3
  82. package/src/cognition/faculties/spaced.repetition.ts +2 -2
  83. package/src/cognition/faculties/stress.regulator.ts +27 -4
  84. package/src/cognition/faculties/task.switcher.ts +11 -6
  85. package/src/cognition/faculties/theory.of.mind.ts +6 -6
  86. package/src/cognition/faculties/threat.evaluator.ts +3 -3
  87. package/src/cognition/faculties/working.memory.ts +8 -8
  88. package/src/cognition/generative.model.ts +2 -2
  89. package/src/cognition/memory/vector.adapter.ts +5 -5
  90. package/src/cognition/memory/vector.content.ts +4 -4
  91. package/src/cognition/memory/vector.embedder.ts +9 -9
  92. package/src/cognition/persona.prior.ts +1 -1
  93. package/src/cognition/schema.registry.ts +2 -2
  94. package/src/cognition/senses/audition.engine/engine.ts +2 -2
  95. package/src/cognition/senses/base.sense.engine.ts +1 -1
  96. package/src/cognition/utilities/token.tracker.ts +5 -5
  97. package/src/core/abstracts.ts +1 -1
  98. package/src/core/completion.recorder.ts +2 -2
  99. package/src/core/serialization.ts +3 -3
  100. package/src/core/snapshot.manager.ts +1 -1
  101. package/src/core/state.manager.ts +1 -1
  102. package/src/core/utils.ts +1 -1
  103. package/src/host/boot.ts +30 -30
  104. package/src/host/utterances.ts +1 -1
  105. package/src/llm/index.ts +9 -9
  106. package/src/llm/summarizer.ts +4 -4
  107. package/src/llm/wire.contracts.ts +1 -1
  108. package/src/mcp/effectors.ts +10 -10
  109. package/src/mcp/server.ts +11 -11
  110. package/src/pma/eval.ts +1 -1
  111. package/src/pma/index.ts +21 -21
  112. package/src/runners/outreach.runner.ts +3 -3
  113. package/src/runners/social.runner.ts +1 -1
  114. package/src/runners/thin-shim.runner.ts +1 -1
  115. package/src/sdk/will.ts +5 -5
  116. package/src/serve/server.ts +12 -12
  117. package/src/stem/assembly.audit.ts +3 -3
  118. package/src/stem/guards/identity.coherence.ts +10 -10
  119. package/src/stem/guards/identity.guard.ts +22 -22
  120. package/src/stem/index.ts +26 -11
  121. package/src/stem/mind.ts +12 -12
  122. package/src/stem/policy/arbiter.ts +136 -0
  123. package/src/stem/policy/rule.table.ts +172 -0
  124. package/src/stem/policy/verdict.recorder.ts +0 -0
  125. package/src/stem/tracts/biography.writer.ts +3 -3
  126. package/src/stem/tracts/effector.controller.ts +383 -4
  127. package/src/stem/tracts/health.reporter.ts +1 -1
  128. package/src/stem/tracts/outbox.controller.ts +4 -0
  129. package/src/stem/tracts/outbox.writer.ts +4 -4
  130. package/src/stem/tracts/pma.controller.ts +1 -1
  131. package/src/stem/tracts/replay.controller.ts +10 -10
  132. package/src/stem/tracts/sensory.controller.ts +2 -2
  133. package/src/stem/tracts/session.logger.ts +4 -4
  134. package/src/stem/tracts/transport/socketio.transport.ts +7 -7
  135. package/src/stem/tracts/transport/stream.transport.ts +2 -2
  136. package/src/stem/tracts/transport.controller.ts +15 -15
package/src/stem/mind.ts CHANGED
@@ -409,10 +409,10 @@ export function _resolveVectorMemory(
409
409
  const mockMode = process.env.WILL_VECTOR_MEMORY === 'mock'
410
410
  const rawModel = embeddingModel
411
411
  ?? process.env.WILL_EMBEDDING_MODEL
412
- ?? ( process.env.WILL_EMBEDDING_API_KEY ? 'text-embedding-3-small' : 'none' )
412
+ ?? ( process.env.WILL_EMBEDDING_API_KEY ? 'text-embedding-3-small' : 'none')
413
413
 
414
414
  // Explicitly disabled — the documented "none" sentinel or recall turned off.
415
- if( !mockMode && ( rawModel === 'none' || process.env.WILL_SEMANTIC_RECALL === 'false' ) )
415
+ if( !mockMode && ( rawModel === 'none' || process.env.WILL_SEMANTIC_RECALL === 'false') )
416
416
  return { embedder: null, vectorMemory: null }
417
417
 
418
418
  // testMode promises a deterministic, zero-key, offline mind — but a dev .env
@@ -581,9 +581,9 @@ export function assembleMind( willId: string, config: WillConfig ): MindAssembly
581
581
  profileContext: profile?.context,
582
582
  })
583
583
  if( !idGuard.ok )
584
- throw new Error( `Invalid Will identity for "${willId}": ${ idGuard.errors.join( '; ' ) }` )
584
+ throw new Error(`Invalid Will identity for "${willId}": ${ idGuard.errors.join('; ') }`)
585
585
  for( const w of idGuard.warnings )
586
- logger.warn( `[identity-guard] ${willId}: ${w}` )
586
+ logger.warn(`[identity-guard] ${willId}: ${w}`)
587
587
  config = { ...config, identity: idGuard.sanitized.identity }
588
588
 
589
589
  // ── Construct ────────────────────────────────────────────
@@ -602,8 +602,8 @@ export function assembleMind( willId: string, config: WillConfig ): MindAssembly
602
602
  // sessionLogger/grants); tests/unit/assembly.order.test.ts pins that set per
603
603
  // tier, so a NEW unwired attachment fails loudly in CI, not here.
604
604
  for( const rec of auditAssemblyWiring( simulation.orchestrator.engines ) )
605
- if( rec.status === 'unwired' )
606
- logger.debug( `[assembly] ${willId}: ${rec.engine}.${rec.method} unwired at assembly (anatomy=${anatomy})` )
605
+ if( rec.status === 'unwired')
606
+ logger.debug(`[assembly] ${willId}: ${rec.engine}.${rec.method} unwired at assembly (anatomy=${anatomy})`)
607
607
 
608
608
  // ── Seed readable simulation state ───────────────────────
609
609
  // Identity, optional initial goals, and the engine-config mirror.
@@ -772,7 +772,7 @@ function _constructCognition(
772
772
  // ── Planning Engine ──────────────────────────────────────
773
773
  const planningEngine = new PlanningEngine()
774
774
  planningEngine.attachGoalManager( goalManager )
775
- if( anatomy !== 'reflex' ) planningEngine.attachExecutiveEngine( executiveEngine )
775
+ if( anatomy !== 'reflex') planningEngine.attachExecutiveEngine( executiveEngine )
776
776
  executiveEngine.attachPlanningEngine( planningEngine )
777
777
 
778
778
  // ── Executive ────────────────────────────────────────────
@@ -791,7 +791,7 @@ function _constructCognition(
791
791
  autobiographicalNarrator.attachEpisodicConsolidator( episodicConsolidator )
792
792
  autobiographicalNarrator.attachSemanticIntegrator( semanticIntegrator )
793
793
  // Satellites harvest the executive's own output — attach wherever it runs.
794
- if( anatomy !== 'reflex' ){
794
+ if( anatomy !== 'reflex'){
795
795
  autobiographicalNarrator.attachExecutiveEngine( executiveEngine )
796
796
  introspectionEngine.attachExecutiveEngine( executiveEngine )
797
797
  }
@@ -807,7 +807,7 @@ function _constructCognition(
807
807
  empathySimulator.attachTheoryOfMind( theoryOfMind )
808
808
 
809
809
  // ── Context compaction components (mind anatomy only) ──
810
- if( anatomy !== 'reflex' ){
810
+ if( anatomy !== 'reflex'){
811
811
  const summarizer = new ExecutiveSummarizer({
812
812
  summaryInterval: parseInt( process.env.WILL_SUMMARY_INTERVAL ?? '10'),
813
813
  bufferSize: parseInt( process.env.WILL_SUMMARY_BUFFER_SIZE ?? '12'),
@@ -845,7 +845,7 @@ function _constructCognition(
845
845
  const olfactionEngine = new OlfactionEngine()
846
846
  const gustationEngine = new GustationEngine()
847
847
 
848
- if( anatomy !== 'reflex' )
848
+ if( anatomy !== 'reflex')
849
849
  auditionEngine.attachExecutiveEngine( executiveEngine )
850
850
 
851
851
  // §5.4 — cold-spawn digest hydration: on the first turn for an entity, seed an
@@ -906,7 +906,7 @@ function _constructCognition(
906
906
  // winner (graceful System-1 degradation).
907
907
  const deliberationEngine = new DeliberationEngine()
908
908
  deliberationEngine.setWillName( config.name )
909
- if( anatomy !== 'reflex' )
909
+ if( anatomy !== 'reflex')
910
910
  deliberationEngine.attachExecutive( executiveEngine )
911
911
 
912
912
  // ── Build Cognition ──────────────────────────────────────
@@ -1108,7 +1108,7 @@ function _seedIdentity(
1108
1108
  // to infer its identity from context. config.name is the canonical source of truth.
1109
1109
  const nameAlreadyInPrompt = personaText.toLowerCase().includes( config.name.toLowerCase() )
1110
1110
  const namePrefix = nameAlreadyInPrompt ? '' : `I am ${config.name}.`
1111
- const fullPersonaText = [ namePrefix, personaText ].filter( Boolean ).join( ' ' )
1111
+ const fullPersonaText = [ namePrefix, personaText ].filter( Boolean ).join(' ')
1112
1112
 
1113
1113
  const prompt = [
1114
1114
  WILL_CORE_PREAMBLE,
@@ -0,0 +1,136 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/policy/arbiter.ts — the policy seam (PEP side)
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // POLICY_REAFFERENCE P0. A Policy Decision Point is consulted before a
6
+ // host-owned effector invocation is handed to the world. This file is the
7
+ // *interface* only — the enforcement point lives in effectorController
8
+ // (`bufferInvocation`), which is the single tract every external effect
9
+ // already passes through.
10
+ //
11
+ // Deliberately provider-agnostic: no transport, no vendor types, no Go. The
12
+ // null arbiter below is the default and is a strict no-op, so a Will with no
13
+ // policy configured runs byte-identically to one built before this file
14
+ // existed. `RuleTableArbiter` (rule.table.ts) is the local reference adapter;
15
+ // an external PDP (e.g. helm-ai-kernel) is a later phase and must implement
16
+ // nothing more than this interface.
17
+ //
18
+ // WHY THE STEM AND NOT COGNITION: the mind must never meet a permission
19
+ // dialog. It dispatches an intent and the world either yields or resists —
20
+ // a refusal arrives as reafference, in the same currency as any other
21
+ // outcome. Keeping the arbiter below the SDK and outside the cognition layer
22
+ // is what preserves that (and is the honest model besides: a body that
23
+ // cannot do the thing).
24
+ //
25
+ // SYNC OR ASYNC: `evaluate` may return a Verdict or a Promise of one. An
26
+ // external PDP will be async, and that is fine by construction — the intent
27
+ // is already held 'awaiting' for AWAIT_TIMEOUT (15 ticks) by the executor, so
28
+ // arbiter latency is absorbed by machinery that already exists. Both P0
29
+ // adapters are synchronous, which is why P0 changes no behaviour.
30
+ // ─────────────────────────────────────────────────────────────
31
+
32
+ /** What the boundary decided about a proposed effect. */
33
+ export type PolicyDecision = 'allow' | 'deny' | 'escalate'
34
+
35
+ /**
36
+ * Is this denial a property of the ACTION CLASS (never permitted under the
37
+ * active policy) or of THIS INSTANCE (wrong target, exceeded bound)?
38
+ *
39
+ * This is the distinction that makes a refusal learnable rather than a wall to
40
+ * re-probe forever, and it is the field we are proposing upstream to HELM. A
41
+ * 'class' denial should suppress the affordance; an 'instance' denial should
42
+ * only narrow the parameter envelope the Will reaches for.
43
+ */
44
+ export type DenialFinality = 'class' | 'instance'
45
+
46
+ /**
47
+ * The nearest allowed envelope — what WOULD have been permitted. Structured so
48
+ * a learner can consume it; `field` names the constraint that bit.
49
+ *
50
+ * Every denial branch of a policy evaluator already computes this and usually
51
+ * discards it. We keep it.
52
+ */
53
+ export interface PolicyCounterfactual {
54
+ /** The constrained field, e.g. 'ttl_days', 'target', 'amount'. */
55
+ field: string
56
+ /** What the Will asked for. */
57
+ requested?: unknown
58
+ /** The bound or permitted set, e.g. 30, or [ 'a', 'b' ]. */
59
+ allowed?: unknown
60
+ }
61
+
62
+ /** A boundary decision about one proposed invocation. */
63
+ export interface Verdict {
64
+ decision: PolicyDecision
65
+ /** Stable machine-readable code, e.g. 'TARGET_NOT_ALLOWED'. Never prose. */
66
+ reasonCode?: string
67
+ /** Meaningful on 'deny' only. Absent ⇒ treat as 'instance' (the safe default:
68
+ * it narrows rather than suppresses, so a mis-tagged denial cannot silently
69
+ * delete an ability from the Will's reach). */
70
+ finality?: DenialFinality
71
+ counterfactual?: PolicyCounterfactual
72
+ /** Free-text for logs and host UX. NEVER parsed by cognition. */
73
+ detail?: string
74
+ }
75
+
76
+ /**
77
+ * The proposed effect, as the stem sees it. Mirrors the `agency.invocation`
78
+ * bus payload the MotorSchemaExecutor emits — no cognitive internals cross
79
+ * this boundary, only the act itself.
80
+ */
81
+ export interface PolicyInvocation {
82
+ willId: string
83
+ /** The awaiting `agency.intent` id — the correlation handle, end to end. */
84
+ intentId: string
85
+ /** The motor schema id, i.e. the ability being enacted. */
86
+ schema: string
87
+ parameters: Record<string, unknown>
88
+ targetEntityId?: string
89
+ /** The ability's declared meaning, as given by the host at wiring time. */
90
+ description?: string
91
+ tick: number
92
+ }
93
+
94
+ /**
95
+ * A Policy Decision Point. Implementations must be PURE with respect to the
96
+ * Will: an arbiter may read its own policy and the invocation, and nothing
97
+ * else. It must not reach into simulation state.
98
+ *
99
+ * DETERMINISM CONTRACT: an arbiter is an external oracle, exactly like the LLM.
100
+ * Its verdicts are recorded on the tape and replayed back — replay never
101
+ * re-consults an arbiter (see P1). Implementations therefore need not be
102
+ * deterministic themselves, but MUST be free of side effects on the Will.
103
+ */
104
+ export interface PolicyArbiter {
105
+ /** Stable identifier, recorded alongside the verdict for audit. */
106
+ readonly name: string
107
+ evaluate( invocation: PolicyInvocation ): Verdict | Promise<Verdict>
108
+ }
109
+
110
+ /** The single allow verdict, frozen and shared — the null arbiter allocates nothing. */
111
+ const ALLOW: Readonly<Verdict> = Object.freeze({ decision: 'allow' as const })
112
+
113
+ /**
114
+ * The default. Allows everything, allocates nothing, logs nothing.
115
+ *
116
+ * A Will running this must be byte-identical to one built before the policy
117
+ * seam existed — that property is asserted by test, and it is what lets this
118
+ * ship dark.
119
+ */
120
+ export const NULL_ARBITER: PolicyArbiter = {
121
+ name: 'null',
122
+ evaluate(): Verdict { return ALLOW },
123
+ }
124
+
125
+ /** True when the arbiter is the no-op default (used to skip the seam entirely). */
126
+ export function isNullArbiter( arbiter: PolicyArbiter | null | undefined ): boolean {
127
+ return !arbiter || arbiter === NULL_ARBITER
128
+ }
129
+
130
+ /**
131
+ * Normalize a denial's finality. Absent ⇒ 'instance', the conservative reading:
132
+ * an unlabelled refusal narrows the envelope but never removes the ability.
133
+ */
134
+ export function finalityOf( verdict: Verdict ): DenialFinality {
135
+ return verdict.finality === 'class' ? 'class' : 'instance'
136
+ }
@@ -0,0 +1,172 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/policy/rule.table.ts — the local reference PDP
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // POLICY_REAFFERENCE P0. A declarative, in-process arbiter: enough policy to
6
+ // prove the seam is sufficient WITHOUT an external dependency, and the
7
+ // fallback if an external-PDP track stalls.
8
+ //
9
+ // It is also a working demonstration of the two receipt fields we are
10
+ // proposing upstream (`finality`, `counterfactual`): when a bound is violated
11
+ // this arbiter *returns what would have been allowed* instead of discarding
12
+ // it. That is the whole argument, implemented in ~40 lines — a denial that
13
+ // says "never" and one that says "not with these parameters" are different
14
+ // facts, and a learner needs to tell them apart.
15
+ //
16
+ // Evaluation is first-match-wins over an ordered rule array, so a verdict is a
17
+ // pure function of ( rules, invocation ) — no clock, no IO, no iteration-order
18
+ // surprises. Deterministic by construction, which keeps replay honest.
19
+ // ─────────────────────────────────────────────────────────────
20
+
21
+ import type {
22
+ PolicyArbiter, PolicyInvocation, Verdict,
23
+ PolicyDecision, DenialFinality, PolicyCounterfactual,
24
+ } from '#stem/policy/arbiter'
25
+
26
+ /** A bound on one parameter. Checked in declaration order: max, min, equals, oneOf. */
27
+ export interface ParamConstraint {
28
+ max?: number
29
+ min?: number
30
+ equals?: unknown
31
+ oneOf?: readonly unknown[]
32
+ }
33
+
34
+ /**
35
+ * One rule. `schema`/`target` scope it (omitted ⇒ matches any); the FIRST rule
36
+ * whose scope matches decides, so order is policy.
37
+ *
38
+ * `require` is meaningful with `decision: 'allow'` only: the scope matched, and
39
+ * these constraints must hold for the allow to stand. A violation flips the
40
+ * verdict to deny — carrying the counterfactual — with finality 'instance',
41
+ * because the ability itself was permitted and only these arguments were not.
42
+ *
43
+ * A rule with `decision: 'deny'` and no `require` is a flat class-level ban;
44
+ * it reports finality 'class' unless told otherwise.
45
+ */
46
+ export interface PolicyRule {
47
+ schema?: string
48
+ target?: string
49
+ decision: PolicyDecision
50
+ require?: Record<string, ParamConstraint>
51
+ reasonCode?: string
52
+ finality?: DenialFinality
53
+ }
54
+
55
+ export interface RuleTableOptions {
56
+ rules: readonly PolicyRule[]
57
+ /**
58
+ * The verdict when NO rule matches. Required — deliberately not defaulted.
59
+ * A policy component that silently defaults open is a trap; make the posture
60
+ * an explicit decision at the call site. 'deny' is fail-closed and is the
61
+ * right choice once a rule set is complete.
62
+ */
63
+ fallthrough: PolicyDecision
64
+ /** Recorded with every verdict for audit. Defaults to 'rule-table'. */
65
+ name?: string
66
+ }
67
+
68
+ export class RuleTableArbiter implements PolicyArbiter {
69
+ readonly name: string
70
+ private readonly _rules: readonly PolicyRule[]
71
+ private readonly _fallthrough: PolicyDecision
72
+
73
+ constructor( opts: RuleTableOptions ){
74
+ this.name = opts.name ?? 'rule-table'
75
+ this._rules = opts.rules
76
+ this._fallthrough = opts.fallthrough
77
+ }
78
+
79
+ evaluate( invocation: PolicyInvocation ): Verdict {
80
+ for( const rule of this._rules ){
81
+ if( !scopeMatches( rule, invocation ) ) continue
82
+
83
+ if( rule.decision !== 'allow')
84
+ return {
85
+ decision: rule.decision,
86
+ ...( rule.reasonCode ? { reasonCode: rule.reasonCode } : {} ),
87
+ ...( rule.decision === 'deny'
88
+ ? { finality: rule.finality ?? 'class' }
89
+ : {} ),
90
+ }
91
+
92
+ const violation = firstViolation( rule.require, invocation.parameters )
93
+ if( violation )
94
+ return {
95
+ decision: 'deny',
96
+ reasonCode: rule.reasonCode ?? violation.reasonCode,
97
+ finality: rule.finality ?? 'instance',
98
+ counterfactual: violation.counterfactual,
99
+ }
100
+
101
+ return { decision: 'allow' }
102
+ }
103
+
104
+ return {
105
+ decision: this._fallthrough,
106
+ ...( this._fallthrough !== 'allow' ? { reasonCode: 'NO_MATCHING_RULE', finality: 'class' as const } : {} ),
107
+ }
108
+ }
109
+ }
110
+
111
+ // ── matching ─────────────────────────────────────────────────────────────────
112
+
113
+ function scopeMatches( rule: PolicyRule, inv: PolicyInvocation ): boolean {
114
+ if( rule.schema !== undefined && rule.schema !== '*' && rule.schema !== inv.schema ) return false
115
+ if( rule.target !== undefined && rule.target !== '*' && rule.target !== inv.targetEntityId ) return false
116
+ return true
117
+ }
118
+
119
+ interface Violation {
120
+ reasonCode: string
121
+ counterfactual: PolicyCounterfactual
122
+ }
123
+
124
+ /**
125
+ * The first constraint that fails, in declared order — so the counterfactual a
126
+ * caller receives is stable, not whichever check happened to run first.
127
+ *
128
+ * An ABSENT parameter is a violation: a bound cannot be honoured by a value
129
+ * that was never supplied, and habitual enaction (which carries no args) must
130
+ * not slip past a constraint the deliberate path would have to satisfy.
131
+ */
132
+ function firstViolation(
133
+ require: Record<string, ParamConstraint> | undefined,
134
+ parameters: Record<string, unknown>,
135
+ ): Violation | null {
136
+ if( !require ) return null
137
+
138
+ for( const [ field, constraint ] of Object.entries( require ) ){
139
+ const present = Object.prototype.hasOwnProperty.call( parameters, field )
140
+ if( !present )
141
+ return {
142
+ reasonCode: 'PARAM_MISSING',
143
+ counterfactual: { field, allowed: describe( constraint ) },
144
+ }
145
+
146
+ const value = parameters[ field ]
147
+
148
+ if( constraint.max !== undefined && !( typeof value === 'number' && value <= constraint.max ) )
149
+ return { reasonCode: 'PARAM_ABOVE_MAX', counterfactual: { field, requested: value, allowed: constraint.max } }
150
+
151
+ if( constraint.min !== undefined && !( typeof value === 'number' && value >= constraint.min ) )
152
+ return { reasonCode: 'PARAM_BELOW_MIN', counterfactual: { field, requested: value, allowed: constraint.min } }
153
+
154
+ if( 'equals' in constraint && value !== constraint.equals )
155
+ return { reasonCode: 'PARAM_NOT_EQUAL', counterfactual: { field, requested: value, allowed: constraint.equals } }
156
+
157
+ if( constraint.oneOf !== undefined && !constraint.oneOf.includes( value ) )
158
+ return { reasonCode: 'PARAM_NOT_IN_SET', counterfactual: { field, requested: value, allowed: [ ...constraint.oneOf ] } }
159
+ }
160
+
161
+ return null
162
+ }
163
+
164
+ /** A compact description of a constraint, for the missing-parameter case. */
165
+ function describe( c: ParamConstraint ): unknown {
166
+ if( c.oneOf !== undefined ) return [ ...c.oneOf ]
167
+ if( 'equals' in c ) return c.equals
168
+ if( c.max !== undefined && c.min !== undefined ) return { min: c.min, max: c.max }
169
+ if( c.max !== undefined ) return { max: c.max }
170
+ if( c.min !== undefined ) return { min: c.min }
171
+ return null
172
+ }
@@ -38,15 +38,15 @@ export class BiographyWriter {
38
38
  if( !fileLoggingEnabled() ) return
39
39
 
40
40
  const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
41
- const dir = join( dataDir, 'wills', willId, 'profiles' )
41
+ const dir = join( dataDir, 'wills', willId, 'profiles')
42
42
  try {
43
43
  if( !this._ensuredDirs.has( dir ) ){
44
44
  mkdirSync( dir, { recursive: true } )
45
45
  this._ensuredDirs.add( dir )
46
46
  }
47
- appendFileSync( join( dir, file ), JSON.stringify( record ) + '\n', 'utf8' )
47
+ appendFileSync( join( dir, file ), JSON.stringify( record ) + '\n', 'utf8')
48
48
  } catch( err ){
49
- logger.error( `[biography] ${file} write failed:`, err )
49
+ logger.error(`[biography] ${file} write failed:`, err )
50
50
  }
51
51
  }
52
52
  /**