@clossys/butler 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/audit-shape.check.d.ts +32 -0
- package/dist/audit-shape.check.d.ts.map +1 -0
- package/dist/audit-shape.check.js +7 -0
- package/dist/audit-shape.check.js.map +1 -0
- package/dist/cli.d.ts +54 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +426 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract.d.ts +256 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +377 -0
- package/dist/contract.js.map +1 -0
- package/dist/inbound/index.d.ts +120 -0
- package/dist/inbound/index.d.ts.map +1 -0
- package/dist/inbound/index.js +125 -0
- package/dist/inbound/index.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +374 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +304 -0
- package/dist/schema.js.map +1 -0
- package/dist/validation.d.ts +74 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +140 -0
- package/dist/validation.js.map +1 -0
- package/dist/web/index.d.ts +5 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +25 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/internal/peer-version.d.ts +53 -0
- package/dist/web/internal/peer-version.d.ts.map +1 -0
- package/dist/web/internal/peer-version.js +136 -0
- package/dist/web/internal/peer-version.js.map +1 -0
- package/dist/web/useStandingWants.d.ts +75 -0
- package/dist/web/useStandingWants.d.ts.map +1 -0
- package/dist/web/useStandingWants.js +66 -0
- package/dist/web/useStandingWants.js.map +1 -0
- package/package.json +93 -0
- package/src/audit-shape.check.ts +37 -0
- package/src/cli.ts +445 -0
- package/src/contract.ts +534 -0
- package/src/inbound/index.ts +190 -0
- package/src/index.ts +113 -0
- package/src/schema.ts +622 -0
- package/src/validation.ts +172 -0
- package/src/web/index.ts +27 -0
- package/src/web/internal/peer-version.ts +159 -0
- package/src/web/useStandingWants.ts +139 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime contract: evaluate one stored want, decide one change to it,
|
|
3
|
+
* and the three gates that read a whole set of them.
|
|
4
|
+
*
|
|
5
|
+
* THE LOOP THIS CLOSES
|
|
6
|
+
* ---------------------
|
|
7
|
+
* A stated want is the setpoint. Acting on it is the act. Reading the
|
|
8
|
+
* interpretation back to the person, and asking whether the standing
|
|
9
|
+
* answer is still current, is the observation. Unconfirmed intents and
|
|
10
|
+
* expired instructions are the comparison. Re-asking is the correction. A
|
|
11
|
+
* preference written to a row and never re-checked has no observation and
|
|
12
|
+
* no comparison — it is an open loop, and closing it is the entire reason
|
|
13
|
+
* this package exists.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is pure. No I/O, no clock read, no ambient state:
|
|
16
|
+
* `evaluateStandingInstruction` and `decideStandingChange` take `now` as a
|
|
17
|
+
* parameter rather than calling `Date.now()` themselves, so the same
|
|
18
|
+
* inputs always produce the same output and a test can put the clock
|
|
19
|
+
* anywhere it needs it. Storage and audit are host-supplied ports
|
|
20
|
+
* (`StandingInstructionStore`, `StandingAuditLedger` in `schema.ts`); this
|
|
21
|
+
* file never touches either.
|
|
22
|
+
*
|
|
23
|
+
* THE THREE GATES
|
|
24
|
+
* ----------------
|
|
25
|
+
* Each returns the same shape of three-state result: `ok: true` when it
|
|
26
|
+
* genuinely checked something and found nothing; `ok: false` with a
|
|
27
|
+
* violation reason and findings when it checked and found something; and
|
|
28
|
+
* `ok: false` with an INDETERMINATE reason when there was nothing to check
|
|
29
|
+
* at all. `cli.ts` maps those onto `0` / `1` / `2` and never collapses the
|
|
30
|
+
* third into either of the first two. A gate that reports "clean" after
|
|
31
|
+
* checking nothing is worse than no gate.
|
|
32
|
+
*/
|
|
33
|
+
import type { ConfidenceFloor, ConfirmationRecord, InstructionUsage, IntentRecord, PolicyVersion, PreferencePath, StandingAction, StandingAuditEvent, StandingEvaluation, StandingEvaluationPolicy, StandingInstruction, StandingTopic } from "./schema.js";
|
|
34
|
+
/**
|
|
35
|
+
* Compares one stored instruction against the policy in force AND the
|
|
36
|
+
* clock. This is the check a weaker tool skips: asking whether a row
|
|
37
|
+
* exists, or reading a boolean off it, cannot see age, so a row three
|
|
38
|
+
* policy versions old and a year past its own declared window both read as
|
|
39
|
+
* a pass. Presence is not currency.
|
|
40
|
+
*
|
|
41
|
+
* - `undefined` input, and a record whose own `state.kind` is `"absent"`,
|
|
42
|
+
* both return `{ status: "absent", reason: "no-record" }`. There is no
|
|
43
|
+
* third "never asked" representation, and this function deliberately does
|
|
44
|
+
* not runtime-validate its typed input or fall back to `"absent"` for a
|
|
45
|
+
* malformed one — a silent fallback would make a broken record
|
|
46
|
+
* indistinguishable from a subject who was genuinely never asked. Untyped
|
|
47
|
+
* input is validated at the boundary, by `schema.ts`'s validators.
|
|
48
|
+
* - An INFERRED instruction with no `confirmedAt` returns
|
|
49
|
+
* `{ status: "absent", reason: "unconfirmed-inference" }`. An inferred
|
|
50
|
+
* want is not a want we have until the person says it is; treating one as
|
|
51
|
+
* binding is the same defect as reading absence as permission, one step
|
|
52
|
+
* further upstream.
|
|
53
|
+
* - A `"granted"` record whose `policyVersion` differs from
|
|
54
|
+
* `currentPolicyVersion` is ALWAYS stale, unconditionally. A policy
|
|
55
|
+
* change invalidates prior permission by definition.
|
|
56
|
+
* - A `"denied"` record whose `policyVersion` differs is stale only when
|
|
57
|
+
* `policy.invalidateDenialOnPolicyBump` is `true`. Whether a policy bump
|
|
58
|
+
* also invalidates a prior refusal is a jurisdiction judgment; `policy`
|
|
59
|
+
* has no default, so nobody gets either answer by accident.
|
|
60
|
+
* - Independently of the policy version, a record whose `decidedAt` is more
|
|
61
|
+
* than `currency.days` before `at` is stale with reason
|
|
62
|
+
* `"window-elapsed"` — for grants AND denials alike. The window is the
|
|
63
|
+
* consumer's own declared statement of how long their own answer keeps
|
|
64
|
+
* speaking, and a refusal that has expired is as much a reason to ask
|
|
65
|
+
* again as a permission that has.
|
|
66
|
+
*
|
|
67
|
+
* When both a policy bump and an elapsed window apply, `"policy-superseded"`
|
|
68
|
+
* is reported: it is the more specific fact, it names something that
|
|
69
|
+
* happened on this side of the boundary rather than merely time passing,
|
|
70
|
+
* and either way the answer is the same — ask again.
|
|
71
|
+
*/
|
|
72
|
+
export declare function evaluateStandingInstruction(instruction: StandingInstruction | undefined, currentPolicyVersion: PolicyVersion, policy: StandingEvaluationPolicy, at: string): StandingEvaluation;
|
|
73
|
+
/**
|
|
74
|
+
* The pure decision core for one change to one standing instruction:
|
|
75
|
+
* produces the new record and its audit event. The caller's
|
|
76
|
+
* `StandingInstructionStore` and `StandingAuditLedger` perform the writes.
|
|
77
|
+
*
|
|
78
|
+
* `actorId` and `subjectId` are separate parameters, in that order, and
|
|
79
|
+
* are never derived from one another — an actor recording a subject's own
|
|
80
|
+
* decision and an actor deciding on a subject's behalf must be
|
|
81
|
+
* distinguishable in the audit trail, and a single conflated id makes them
|
|
82
|
+
* identical forever.
|
|
83
|
+
*
|
|
84
|
+
* Withdrawing produces `state: { kind: "absent" }`: the same value a
|
|
85
|
+
* subject who was never asked has. That is deliberate and it is the point
|
|
86
|
+
* of the three-state model — a withdrawal returns the person to "we do not
|
|
87
|
+
* have a want here", which is not permission, rather than to a `false`
|
|
88
|
+
* that some later boolean read could round back up.
|
|
89
|
+
*/
|
|
90
|
+
export declare function decideStandingChange(actorId: string, subjectId: string, instructionId: string, action: StandingAction, now: string): {
|
|
91
|
+
instruction: StandingInstruction;
|
|
92
|
+
auditEvent: StandingAuditEvent;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* A pure audit-event builder for a subject reopening their preference
|
|
96
|
+
* surface, independent of whether they change anything once it is open.
|
|
97
|
+
* Reopening at all is audit-worthy on its own: it is the observation half
|
|
98
|
+
* of the loop, and a surface nobody ever reopens is a surface whose
|
|
99
|
+
* instructions are never re-checked.
|
|
100
|
+
*/
|
|
101
|
+
export declare function recordReopened(actorId: string, subjectId: string, topic: StandingTopic, policyVersion: PolicyVersion, now: string): StandingAuditEvent;
|
|
102
|
+
/**
|
|
103
|
+
* A pure audit-event builder for the moment `evaluateStandingInstruction`
|
|
104
|
+
* reports a stored answer stale. `reason` picks the event type, so an
|
|
105
|
+
* auditor can tell a policy bump from an expired window without re-deriving
|
|
106
|
+
* it. `previousPolicyVersion` is the version the invalidated record
|
|
107
|
+
* actually answered; `currentPolicyVersion` is the one in force now.
|
|
108
|
+
* Calling this is optional — the evaluation itself performs no I/O and
|
|
109
|
+
* emits nothing — but a host that wants staleness to appear in its trail
|
|
110
|
+
* rather than only ever being silently recomputed on read calls it once it
|
|
111
|
+
* observes a `"stale"` evaluation.
|
|
112
|
+
*/
|
|
113
|
+
export declare function recordStaleness(actorId: string, subjectId: string, topic: StandingTopic, reason: "policy-superseded" | "window-elapsed", previousPolicyVersion: PolicyVersion, currentPolicyVersion: PolicyVersion, now: string): StandingAuditEvent;
|
|
114
|
+
export type ConfirmationFindingKind =
|
|
115
|
+
/** Something was done on this reading and the subject never answered a read-back at all. */
|
|
116
|
+
"acted-without-confirmation"
|
|
117
|
+
/** Something was done on this reading after the subject said it was misread. */
|
|
118
|
+
| "acted-against-misread"
|
|
119
|
+
/** Something was done on this reading after the subject said they could not tell. `"unclear"` is not a quiet yes. */
|
|
120
|
+
| "acted-against-unclear"
|
|
121
|
+
/** A reading below the declared floor was acted on with no hand-off and no confirmation. */
|
|
122
|
+
| "below-floor-acted-silently"
|
|
123
|
+
/** A read-back answers an intent that is not in the set being checked. */
|
|
124
|
+
| "confirmation-without-intent";
|
|
125
|
+
export interface ConfirmationFinding {
|
|
126
|
+
kind: ConfirmationFindingKind;
|
|
127
|
+
intentId: string;
|
|
128
|
+
/** The actor that dispositioned the intent, or the subject that answered a dangling read-back. Never both, never merged. */
|
|
129
|
+
actorId?: string;
|
|
130
|
+
message: string;
|
|
131
|
+
}
|
|
132
|
+
export type ConfirmationFailureReason = "unconfirmed-intents" | "no-intents-provided";
|
|
133
|
+
export interface ConfirmationCompletenessResult {
|
|
134
|
+
ok: boolean;
|
|
135
|
+
reason?: ConfirmationFailureReason;
|
|
136
|
+
intentsChecked: number;
|
|
137
|
+
confirmationsChecked: number;
|
|
138
|
+
/** The floor actually applied, echoed back so a report names the number it judged against rather than implying a universal one. */
|
|
139
|
+
floorApplied: number;
|
|
140
|
+
findings: ConfirmationFinding[];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* GATE 1 — every acted-on intent has the subject's own confirmation, or an
|
|
144
|
+
* explicit below-floor hand-off.
|
|
145
|
+
*
|
|
146
|
+
* Pure, no I/O. `floor` is supplied by the caller and has no default
|
|
147
|
+
* anywhere in this package: the number below which a reading is too weak
|
|
148
|
+
* to act on is one of the consumer's own values, and a floor this package
|
|
149
|
+
* invented would be this package authoring it.
|
|
150
|
+
*
|
|
151
|
+
* The below-floor rule is the AI-native half. A reading whose confidence
|
|
152
|
+
* is strictly below the floor may still be acted on — but only after the
|
|
153
|
+
* subject confirms it. What it may never be is acted on SILENTLY: a
|
|
154
|
+
* below-floor reading has to become a confirmation request or an explicit
|
|
155
|
+
* hand-off, and `"handed-off"` is a first-class disposition precisely so
|
|
156
|
+
* declining to act is representable as a decision rather than as an
|
|
157
|
+
* absence of one.
|
|
158
|
+
*
|
|
159
|
+
* `ok: false` with `"no-intents-provided"` is not a violation — it is this
|
|
160
|
+
* gate saying it never formed an opinion, and `cli.ts` maps it to `2`.
|
|
161
|
+
*/
|
|
162
|
+
export declare function checkConfirmationCompleteness(intents: readonly IntentRecord[], confirmations: readonly ConfirmationRecord[], floor: ConfidenceFloor): ConfirmationCompletenessResult;
|
|
163
|
+
export type CurrencyFindingKind =
|
|
164
|
+
/** An instruction was relied on after its own declared window ran out. */
|
|
165
|
+
"used-past-window"
|
|
166
|
+
/** An instruction was relied on after the policy version it answered was superseded. */
|
|
167
|
+
| "used-after-policy-superseded"
|
|
168
|
+
/** An instruction was relied on while there was no answer on record — including an inference the subject never confirmed. Absence is not permission. */
|
|
169
|
+
| "used-while-absent"
|
|
170
|
+
/** A usage names an instruction that is not in the set being checked. */
|
|
171
|
+
| "usage-without-instruction";
|
|
172
|
+
export interface CurrencyFinding {
|
|
173
|
+
kind: CurrencyFindingKind;
|
|
174
|
+
instructionId: string;
|
|
175
|
+
/** Whoever relied on the instruction. Separate from the instruction's own `subjectId`, always. */
|
|
176
|
+
actorId: string;
|
|
177
|
+
usedAt: string;
|
|
178
|
+
message: string;
|
|
179
|
+
}
|
|
180
|
+
export type CurrencyFailureReason = "stale-instructions-used" | "no-instructions-provided" | "no-usages-provided";
|
|
181
|
+
export interface CurrencyResult {
|
|
182
|
+
ok: boolean;
|
|
183
|
+
reason?: CurrencyFailureReason;
|
|
184
|
+
instructionsChecked: number;
|
|
185
|
+
usagesChecked: number;
|
|
186
|
+
findings: CurrencyFinding[];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* GATE 2 — no standing instruction is used past its declared window.
|
|
190
|
+
*
|
|
191
|
+
* Pure, no I/O. Reads USAGES, not instructions alone, and that is the
|
|
192
|
+
* whole design: a set of instructions with nobody relying on them proves
|
|
193
|
+
* nothing, while a set of usages is a record of the loop actually being
|
|
194
|
+
* closed or not. Each usage carries the policy version in force at the
|
|
195
|
+
* moment it happened, so this gate replays a real decision rather than
|
|
196
|
+
* re-deriving one against today's policy.
|
|
197
|
+
*
|
|
198
|
+
* `policy.invalidateDenialOnPolicyBump` has no default — see
|
|
199
|
+
* `evaluateStandingInstruction`. A caller that cannot supply it does not
|
|
200
|
+
* get a guess.
|
|
201
|
+
*
|
|
202
|
+
* `used-while-absent` is here because it is the same defect the three-state
|
|
203
|
+
* model exists to prevent, observed one layer up: relying on nothing at all
|
|
204
|
+
* — a subject never asked, or an inference they never confirmed — is not a
|
|
205
|
+
* currency problem in the narrow sense, but it is exactly what a boolean
|
|
206
|
+
* read of a missing row silently permits, and a gate that only measured
|
|
207
|
+
* age would pass it.
|
|
208
|
+
*
|
|
209
|
+
* `ok: false` with either "nothing provided" reason is indeterminate, and
|
|
210
|
+
* `cli.ts` maps it to `2`.
|
|
211
|
+
*/
|
|
212
|
+
export declare function checkCurrency(instructions: readonly StandingInstruction[], usages: readonly InstructionUsage[], policy: StandingEvaluationPolicy): CurrencyResult;
|
|
213
|
+
export type WithdrawalParityFindingKind =
|
|
214
|
+
/** The surface declares a way in and no way out. */
|
|
215
|
+
"withdrawal-unavailable"
|
|
216
|
+
/** Withdrawing takes more discrete actions than granting did. */
|
|
217
|
+
| "withdrawal-costs-more-steps"
|
|
218
|
+
/** Withdrawing requires contacting a human where granting did not. */
|
|
219
|
+
| "withdrawal-requires-contact"
|
|
220
|
+
/** Withdrawing requires an account where granting did not. */
|
|
221
|
+
| "withdrawal-requires-account";
|
|
222
|
+
export interface WithdrawalParityFinding {
|
|
223
|
+
kind: WithdrawalParityFindingKind;
|
|
224
|
+
surfaceId: string;
|
|
225
|
+
topic: StandingTopic;
|
|
226
|
+
message: string;
|
|
227
|
+
}
|
|
228
|
+
export type WithdrawalParityFailureReason = "withdrawal-harder-than-granting" | "no-paths-provided";
|
|
229
|
+
export interface WithdrawalParityResult {
|
|
230
|
+
ok: boolean;
|
|
231
|
+
reason?: WithdrawalParityFailureReason;
|
|
232
|
+
pathsChecked: number;
|
|
233
|
+
findings: WithdrawalParityFinding[];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* GATE 3 — withdrawing is no harder than granting.
|
|
237
|
+
*
|
|
238
|
+
* Pure, no I/O. Compares the two routes a consumer measured itself, using
|
|
239
|
+
* three coarse countable facts rather than a score: a parity claim that
|
|
240
|
+
* produces a number nobody can trace back to a step is not evidence.
|
|
241
|
+
*
|
|
242
|
+
* A missing `withdraw` path is the loudest finding here, not a skipped
|
|
243
|
+
* comparison. A surface that offers a way in and no way out is the extreme
|
|
244
|
+
* of the same defect, and a gate that quietly had nothing to compare would
|
|
245
|
+
* report it as a pass.
|
|
246
|
+
*
|
|
247
|
+
* Reopening is not a degraded path, and this gate is only the measurement
|
|
248
|
+
* half of that. The API half is structural and lives in `./web`, where
|
|
249
|
+
* `withdraw` shares `grant`'s and `deny`'s exact call shape, so there is no
|
|
250
|
+
* separate, harder-to-reach function for revoking than for giving.
|
|
251
|
+
*
|
|
252
|
+
* `ok: false` with `"no-paths-provided"` is indeterminate, and `cli.ts`
|
|
253
|
+
* maps it to `2`.
|
|
254
|
+
*/
|
|
255
|
+
export declare function checkWithdrawalParity(paths: readonly PreferencePath[]): WithdrawalParityResult;
|
|
256
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime contract: evaluate one stored want, decide one change to it,
|
|
3
|
+
* and the three gates that read a whole set of them.
|
|
4
|
+
*
|
|
5
|
+
* THE LOOP THIS CLOSES
|
|
6
|
+
* ---------------------
|
|
7
|
+
* A stated want is the setpoint. Acting on it is the act. Reading the
|
|
8
|
+
* interpretation back to the person, and asking whether the standing
|
|
9
|
+
* answer is still current, is the observation. Unconfirmed intents and
|
|
10
|
+
* expired instructions are the comparison. Re-asking is the correction. A
|
|
11
|
+
* preference written to a row and never re-checked has no observation and
|
|
12
|
+
* no comparison — it is an open loop, and closing it is the entire reason
|
|
13
|
+
* this package exists.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is pure. No I/O, no clock read, no ambient state:
|
|
16
|
+
* `evaluateStandingInstruction` and `decideStandingChange` take `now` as a
|
|
17
|
+
* parameter rather than calling `Date.now()` themselves, so the same
|
|
18
|
+
* inputs always produce the same output and a test can put the clock
|
|
19
|
+
* anywhere it needs it. Storage and audit are host-supplied ports
|
|
20
|
+
* (`StandingInstructionStore`, `StandingAuditLedger` in `schema.ts`); this
|
|
21
|
+
* file never touches either.
|
|
22
|
+
*
|
|
23
|
+
* THE THREE GATES
|
|
24
|
+
* ----------------
|
|
25
|
+
* Each returns the same shape of three-state result: `ok: true` when it
|
|
26
|
+
* genuinely checked something and found nothing; `ok: false` with a
|
|
27
|
+
* violation reason and findings when it checked and found something; and
|
|
28
|
+
* `ok: false` with an INDETERMINATE reason when there was nothing to check
|
|
29
|
+
* at all. `cli.ts` maps those onto `0` / `1` / `2` and never collapses the
|
|
30
|
+
* third into either of the first two. A gate that reports "clean" after
|
|
31
|
+
* checking nothing is worse than no gate.
|
|
32
|
+
*/
|
|
33
|
+
const MILLISECONDS_PER_DAY = 86_400_000;
|
|
34
|
+
function samePolicyVersion(a, b) {
|
|
35
|
+
return a.policyId === b.policyId && a.version === b.version;
|
|
36
|
+
}
|
|
37
|
+
function windowElapsed(decidedAt, currency, at) {
|
|
38
|
+
const decided = Date.parse(decidedAt);
|
|
39
|
+
const asked = Date.parse(at);
|
|
40
|
+
if (Number.isNaN(decided) || Number.isNaN(asked))
|
|
41
|
+
return false;
|
|
42
|
+
return asked - decided > currency.days * MILLISECONDS_PER_DAY;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compares one stored instruction against the policy in force AND the
|
|
46
|
+
* clock. This is the check a weaker tool skips: asking whether a row
|
|
47
|
+
* exists, or reading a boolean off it, cannot see age, so a row three
|
|
48
|
+
* policy versions old and a year past its own declared window both read as
|
|
49
|
+
* a pass. Presence is not currency.
|
|
50
|
+
*
|
|
51
|
+
* - `undefined` input, and a record whose own `state.kind` is `"absent"`,
|
|
52
|
+
* both return `{ status: "absent", reason: "no-record" }`. There is no
|
|
53
|
+
* third "never asked" representation, and this function deliberately does
|
|
54
|
+
* not runtime-validate its typed input or fall back to `"absent"` for a
|
|
55
|
+
* malformed one — a silent fallback would make a broken record
|
|
56
|
+
* indistinguishable from a subject who was genuinely never asked. Untyped
|
|
57
|
+
* input is validated at the boundary, by `schema.ts`'s validators.
|
|
58
|
+
* - An INFERRED instruction with no `confirmedAt` returns
|
|
59
|
+
* `{ status: "absent", reason: "unconfirmed-inference" }`. An inferred
|
|
60
|
+
* want is not a want we have until the person says it is; treating one as
|
|
61
|
+
* binding is the same defect as reading absence as permission, one step
|
|
62
|
+
* further upstream.
|
|
63
|
+
* - A `"granted"` record whose `policyVersion` differs from
|
|
64
|
+
* `currentPolicyVersion` is ALWAYS stale, unconditionally. A policy
|
|
65
|
+
* change invalidates prior permission by definition.
|
|
66
|
+
* - A `"denied"` record whose `policyVersion` differs is stale only when
|
|
67
|
+
* `policy.invalidateDenialOnPolicyBump` is `true`. Whether a policy bump
|
|
68
|
+
* also invalidates a prior refusal is a jurisdiction judgment; `policy`
|
|
69
|
+
* has no default, so nobody gets either answer by accident.
|
|
70
|
+
* - Independently of the policy version, a record whose `decidedAt` is more
|
|
71
|
+
* than `currency.days` before `at` is stale with reason
|
|
72
|
+
* `"window-elapsed"` — for grants AND denials alike. The window is the
|
|
73
|
+
* consumer's own declared statement of how long their own answer keeps
|
|
74
|
+
* speaking, and a refusal that has expired is as much a reason to ask
|
|
75
|
+
* again as a permission that has.
|
|
76
|
+
*
|
|
77
|
+
* When both a policy bump and an elapsed window apply, `"policy-superseded"`
|
|
78
|
+
* is reported: it is the more specific fact, it names something that
|
|
79
|
+
* happened on this side of the boundary rather than merely time passing,
|
|
80
|
+
* and either way the answer is the same — ask again.
|
|
81
|
+
*/
|
|
82
|
+
export function evaluateStandingInstruction(instruction, currentPolicyVersion, policy, at) {
|
|
83
|
+
if (instruction === undefined || instruction.state.kind === "absent") {
|
|
84
|
+
return { status: "absent", reason: "no-record" };
|
|
85
|
+
}
|
|
86
|
+
if (instruction.provenance === "inferred" && instruction.confirmedAt === undefined) {
|
|
87
|
+
return { status: "absent", reason: "unconfirmed-inference" };
|
|
88
|
+
}
|
|
89
|
+
const { state } = instruction;
|
|
90
|
+
const current = samePolicyVersion(state.policyVersion, currentPolicyVersion);
|
|
91
|
+
const expired = windowElapsed(state.decidedAt, instruction.currency, at);
|
|
92
|
+
if (state.kind === "granted") {
|
|
93
|
+
if (!current)
|
|
94
|
+
return { status: "stale", reason: "policy-superseded", previousPolicyVersion: state.policyVersion, decidedAt: state.decidedAt };
|
|
95
|
+
if (expired)
|
|
96
|
+
return { status: "stale", reason: "window-elapsed", previousPolicyVersion: state.policyVersion, decidedAt: state.decidedAt };
|
|
97
|
+
return { status: "granted", policyVersion: state.policyVersion };
|
|
98
|
+
}
|
|
99
|
+
// state.kind === "denied"
|
|
100
|
+
if (!current && policy.invalidateDenialOnPolicyBump) {
|
|
101
|
+
return { status: "stale", reason: "policy-superseded", previousPolicyVersion: state.policyVersion, decidedAt: state.decidedAt };
|
|
102
|
+
}
|
|
103
|
+
if (expired)
|
|
104
|
+
return { status: "stale", reason: "window-elapsed", previousPolicyVersion: state.policyVersion, decidedAt: state.decidedAt };
|
|
105
|
+
return { status: "denied", policyVersion: state.policyVersion };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The pure decision core for one change to one standing instruction:
|
|
109
|
+
* produces the new record and its audit event. The caller's
|
|
110
|
+
* `StandingInstructionStore` and `StandingAuditLedger` perform the writes.
|
|
111
|
+
*
|
|
112
|
+
* `actorId` and `subjectId` are separate parameters, in that order, and
|
|
113
|
+
* are never derived from one another — an actor recording a subject's own
|
|
114
|
+
* decision and an actor deciding on a subject's behalf must be
|
|
115
|
+
* distinguishable in the audit trail, and a single conflated id makes them
|
|
116
|
+
* identical forever.
|
|
117
|
+
*
|
|
118
|
+
* Withdrawing produces `state: { kind: "absent" }`: the same value a
|
|
119
|
+
* subject who was never asked has. That is deliberate and it is the point
|
|
120
|
+
* of the three-state model — a withdrawal returns the person to "we do not
|
|
121
|
+
* have a want here", which is not permission, rather than to a `false`
|
|
122
|
+
* that some later boolean read could round back up.
|
|
123
|
+
*/
|
|
124
|
+
export function decideStandingChange(actorId, subjectId, instructionId, action, now) {
|
|
125
|
+
const base = { instructionId, subjectId, topic: action.topic, provenance: "stated", currency: action.currency };
|
|
126
|
+
const instruction = action.kind === "grant"
|
|
127
|
+
? { ...base, state: { kind: "granted", policyVersion: action.policyVersion, decidedAt: now } }
|
|
128
|
+
: action.kind === "deny"
|
|
129
|
+
? { ...base, state: { kind: "denied", policyVersion: action.policyVersion, decidedAt: now } }
|
|
130
|
+
: { ...base, state: { kind: "absent" } };
|
|
131
|
+
const auditEvent = {
|
|
132
|
+
subjectId,
|
|
133
|
+
actorId,
|
|
134
|
+
topic: action.topic,
|
|
135
|
+
type: action.kind === "grant" ? "granted" : action.kind === "deny" ? "denied" : "withdrawn",
|
|
136
|
+
policyVersion: action.policyVersion,
|
|
137
|
+
occurredAt: now,
|
|
138
|
+
};
|
|
139
|
+
return { instruction, auditEvent };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A pure audit-event builder for a subject reopening their preference
|
|
143
|
+
* surface, independent of whether they change anything once it is open.
|
|
144
|
+
* Reopening at all is audit-worthy on its own: it is the observation half
|
|
145
|
+
* of the loop, and a surface nobody ever reopens is a surface whose
|
|
146
|
+
* instructions are never re-checked.
|
|
147
|
+
*/
|
|
148
|
+
export function recordReopened(actorId, subjectId, topic, policyVersion, now) {
|
|
149
|
+
return { subjectId, actorId, topic, type: "reopened", policyVersion, occurredAt: now };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A pure audit-event builder for the moment `evaluateStandingInstruction`
|
|
153
|
+
* reports a stored answer stale. `reason` picks the event type, so an
|
|
154
|
+
* auditor can tell a policy bump from an expired window without re-deriving
|
|
155
|
+
* it. `previousPolicyVersion` is the version the invalidated record
|
|
156
|
+
* actually answered; `currentPolicyVersion` is the one in force now.
|
|
157
|
+
* Calling this is optional — the evaluation itself performs no I/O and
|
|
158
|
+
* emits nothing — but a host that wants staleness to appear in its trail
|
|
159
|
+
* rather than only ever being silently recomputed on read calls it once it
|
|
160
|
+
* observes a `"stale"` evaluation.
|
|
161
|
+
*/
|
|
162
|
+
export function recordStaleness(actorId, subjectId, topic, reason, previousPolicyVersion, currentPolicyVersion, now) {
|
|
163
|
+
return {
|
|
164
|
+
subjectId,
|
|
165
|
+
actorId,
|
|
166
|
+
topic,
|
|
167
|
+
type: reason,
|
|
168
|
+
policyVersion: currentPolicyVersion,
|
|
169
|
+
previousPolicyVersion,
|
|
170
|
+
occurredAt: now,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* GATE 1 — every acted-on intent has the subject's own confirmation, or an
|
|
175
|
+
* explicit below-floor hand-off.
|
|
176
|
+
*
|
|
177
|
+
* Pure, no I/O. `floor` is supplied by the caller and has no default
|
|
178
|
+
* anywhere in this package: the number below which a reading is too weak
|
|
179
|
+
* to act on is one of the consumer's own values, and a floor this package
|
|
180
|
+
* invented would be this package authoring it.
|
|
181
|
+
*
|
|
182
|
+
* The below-floor rule is the AI-native half. A reading whose confidence
|
|
183
|
+
* is strictly below the floor may still be acted on — but only after the
|
|
184
|
+
* subject confirms it. What it may never be is acted on SILENTLY: a
|
|
185
|
+
* below-floor reading has to become a confirmation request or an explicit
|
|
186
|
+
* hand-off, and `"handed-off"` is a first-class disposition precisely so
|
|
187
|
+
* declining to act is representable as a decision rather than as an
|
|
188
|
+
* absence of one.
|
|
189
|
+
*
|
|
190
|
+
* `ok: false` with `"no-intents-provided"` is not a violation — it is this
|
|
191
|
+
* gate saying it never formed an opinion, and `cli.ts` maps it to `2`.
|
|
192
|
+
*/
|
|
193
|
+
export function checkConfirmationCompleteness(intents, confirmations, floor) {
|
|
194
|
+
const base = { intentsChecked: intents.length, confirmationsChecked: confirmations.length, floorApplied: floor.minimumConfidence };
|
|
195
|
+
if (intents.length === 0) {
|
|
196
|
+
return { ok: false, reason: "no-intents-provided", ...base, findings: [] };
|
|
197
|
+
}
|
|
198
|
+
const byIntentId = new Map();
|
|
199
|
+
for (const confirmation of confirmations)
|
|
200
|
+
byIntentId.set(confirmation.intentId, confirmation);
|
|
201
|
+
const knownIntentIds = new Set(intents.map((intent) => intent.intentId));
|
|
202
|
+
const findings = [];
|
|
203
|
+
for (const intent of intents) {
|
|
204
|
+
const confirmation = byIntentId.get(intent.intentId);
|
|
205
|
+
const belowFloor = intent.confidence < floor.minimumConfidence;
|
|
206
|
+
if (intent.disposition !== "acted") {
|
|
207
|
+
// "handed-off" and "awaiting-confirmation" are both explicit,
|
|
208
|
+
// recorded decisions not to act yet. Neither needs a confirmation to
|
|
209
|
+
// be complete, because neither has done anything.
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (confirmation === undefined) {
|
|
213
|
+
findings.push({
|
|
214
|
+
kind: belowFloor ? "below-floor-acted-silently" : "acted-without-confirmation",
|
|
215
|
+
intentId: intent.intentId,
|
|
216
|
+
actorId: intent.actorId,
|
|
217
|
+
message: belowFloor
|
|
218
|
+
? `acted on a reading of confidence ${intent.confidence} below the declared floor ${floor.minimumConfidence}, with no confirmation and no hand-off`
|
|
219
|
+
: "acted with no confirmation record from the subject",
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (confirmation.verdict === "misread") {
|
|
224
|
+
findings.push({
|
|
225
|
+
kind: "acted-against-misread",
|
|
226
|
+
intentId: intent.intentId,
|
|
227
|
+
actorId: intent.actorId,
|
|
228
|
+
message: "acted after the subject answered the read-back with \"misread\"",
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (confirmation.verdict === "unclear") {
|
|
233
|
+
findings.push({
|
|
234
|
+
kind: "acted-against-unclear",
|
|
235
|
+
intentId: intent.intentId,
|
|
236
|
+
actorId: intent.actorId,
|
|
237
|
+
message: "acted after the subject answered the read-back with \"unclear\"",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const confirmation of confirmations) {
|
|
242
|
+
if (knownIntentIds.has(confirmation.intentId))
|
|
243
|
+
continue;
|
|
244
|
+
findings.push({
|
|
245
|
+
kind: "confirmation-without-intent",
|
|
246
|
+
intentId: confirmation.intentId,
|
|
247
|
+
message: "a read-back answers an intent that is not in the set being checked",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (findings.length > 0)
|
|
251
|
+
return { ok: false, reason: "unconfirmed-intents", ...base, findings };
|
|
252
|
+
return { ok: true, ...base, findings: [] };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* GATE 2 — no standing instruction is used past its declared window.
|
|
256
|
+
*
|
|
257
|
+
* Pure, no I/O. Reads USAGES, not instructions alone, and that is the
|
|
258
|
+
* whole design: a set of instructions with nobody relying on them proves
|
|
259
|
+
* nothing, while a set of usages is a record of the loop actually being
|
|
260
|
+
* closed or not. Each usage carries the policy version in force at the
|
|
261
|
+
* moment it happened, so this gate replays a real decision rather than
|
|
262
|
+
* re-deriving one against today's policy.
|
|
263
|
+
*
|
|
264
|
+
* `policy.invalidateDenialOnPolicyBump` has no default — see
|
|
265
|
+
* `evaluateStandingInstruction`. A caller that cannot supply it does not
|
|
266
|
+
* get a guess.
|
|
267
|
+
*
|
|
268
|
+
* `used-while-absent` is here because it is the same defect the three-state
|
|
269
|
+
* model exists to prevent, observed one layer up: relying on nothing at all
|
|
270
|
+
* — a subject never asked, or an inference they never confirmed — is not a
|
|
271
|
+
* currency problem in the narrow sense, but it is exactly what a boolean
|
|
272
|
+
* read of a missing row silently permits, and a gate that only measured
|
|
273
|
+
* age would pass it.
|
|
274
|
+
*
|
|
275
|
+
* `ok: false` with either "nothing provided" reason is indeterminate, and
|
|
276
|
+
* `cli.ts` maps it to `2`.
|
|
277
|
+
*/
|
|
278
|
+
export function checkCurrency(instructions, usages, policy) {
|
|
279
|
+
const base = { instructionsChecked: instructions.length, usagesChecked: usages.length };
|
|
280
|
+
if (instructions.length === 0)
|
|
281
|
+
return { ok: false, reason: "no-instructions-provided", ...base, findings: [] };
|
|
282
|
+
if (usages.length === 0)
|
|
283
|
+
return { ok: false, reason: "no-usages-provided", ...base, findings: [] };
|
|
284
|
+
const byId = new Map();
|
|
285
|
+
for (const instruction of instructions)
|
|
286
|
+
byId.set(instruction.instructionId, instruction);
|
|
287
|
+
const findings = [];
|
|
288
|
+
for (const usage of usages) {
|
|
289
|
+
const instruction = byId.get(usage.instructionId);
|
|
290
|
+
if (instruction === undefined) {
|
|
291
|
+
findings.push({
|
|
292
|
+
kind: "usage-without-instruction",
|
|
293
|
+
instructionId: usage.instructionId,
|
|
294
|
+
actorId: usage.actorId,
|
|
295
|
+
usedAt: usage.usedAt,
|
|
296
|
+
message: "relied on an instruction that is not in the set being checked",
|
|
297
|
+
});
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const evaluation = evaluateStandingInstruction(instruction, usage.currentPolicyVersion, policy, usage.usedAt);
|
|
301
|
+
if (evaluation.status === "absent") {
|
|
302
|
+
findings.push({
|
|
303
|
+
kind: "used-while-absent",
|
|
304
|
+
instructionId: usage.instructionId,
|
|
305
|
+
actorId: usage.actorId,
|
|
306
|
+
usedAt: usage.usedAt,
|
|
307
|
+
message: evaluation.reason === "unconfirmed-inference"
|
|
308
|
+
? "relied on an inferred instruction the subject never confirmed"
|
|
309
|
+
: "relied on an instruction with no answer on record",
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (evaluation.status === "stale") {
|
|
314
|
+
findings.push({
|
|
315
|
+
kind: evaluation.reason === "window-elapsed" ? "used-past-window" : "used-after-policy-superseded",
|
|
316
|
+
instructionId: usage.instructionId,
|
|
317
|
+
actorId: usage.actorId,
|
|
318
|
+
usedAt: usage.usedAt,
|
|
319
|
+
message: evaluation.reason === "window-elapsed"
|
|
320
|
+
? `relied on an answer decided at ${evaluation.decidedAt}, past its declared window of ${instruction.currency.days} day(s)`
|
|
321
|
+
: `relied on an answer to policy ${evaluation.previousPolicyVersion.policyId}@${evaluation.previousPolicyVersion.version}, superseded by ${usage.currentPolicyVersion.policyId}@${usage.currentPolicyVersion.version}`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (findings.length > 0)
|
|
326
|
+
return { ok: false, reason: "stale-instructions-used", ...base, findings };
|
|
327
|
+
return { ok: true, ...base, findings: [] };
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* GATE 3 — withdrawing is no harder than granting.
|
|
331
|
+
*
|
|
332
|
+
* Pure, no I/O. Compares the two routes a consumer measured itself, using
|
|
333
|
+
* three coarse countable facts rather than a score: a parity claim that
|
|
334
|
+
* produces a number nobody can trace back to a step is not evidence.
|
|
335
|
+
*
|
|
336
|
+
* A missing `withdraw` path is the loudest finding here, not a skipped
|
|
337
|
+
* comparison. A surface that offers a way in and no way out is the extreme
|
|
338
|
+
* of the same defect, and a gate that quietly had nothing to compare would
|
|
339
|
+
* report it as a pass.
|
|
340
|
+
*
|
|
341
|
+
* Reopening is not a degraded path, and this gate is only the measurement
|
|
342
|
+
* half of that. The API half is structural and lives in `./web`, where
|
|
343
|
+
* `withdraw` shares `grant`'s and `deny`'s exact call shape, so there is no
|
|
344
|
+
* separate, harder-to-reach function for revoking than for giving.
|
|
345
|
+
*
|
|
346
|
+
* `ok: false` with `"no-paths-provided"` is indeterminate, and `cli.ts`
|
|
347
|
+
* maps it to `2`.
|
|
348
|
+
*/
|
|
349
|
+
export function checkWithdrawalParity(paths) {
|
|
350
|
+
if (paths.length === 0)
|
|
351
|
+
return { ok: false, reason: "no-paths-provided", pathsChecked: 0, findings: [] };
|
|
352
|
+
const findings = [];
|
|
353
|
+
for (const path of paths) {
|
|
354
|
+
const where = { surfaceId: path.surfaceId, topic: path.topic };
|
|
355
|
+
if (path.withdraw === undefined) {
|
|
356
|
+
findings.push({ ...where, kind: "withdrawal-unavailable", message: "declares a grant path and no withdraw path at all" });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (path.withdraw.steps > path.grant.steps) {
|
|
360
|
+
findings.push({
|
|
361
|
+
...where,
|
|
362
|
+
kind: "withdrawal-costs-more-steps",
|
|
363
|
+
message: `withdrawing takes ${path.withdraw.steps} step(s) against granting's ${path.grant.steps}`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (path.withdraw.requiresContact && !path.grant.requiresContact) {
|
|
367
|
+
findings.push({ ...where, kind: "withdrawal-requires-contact", message: "withdrawing requires contacting a human where granting did not" });
|
|
368
|
+
}
|
|
369
|
+
if (path.withdraw.requiresAccount && !path.grant.requiresAccount) {
|
|
370
|
+
findings.push({ ...where, kind: "withdrawal-requires-account", message: "withdrawing requires an account where granting did not" });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (findings.length > 0)
|
|
374
|
+
return { ok: false, reason: "withdrawal-harder-than-granting", pathsChecked: paths.length, findings };
|
|
375
|
+
return { ok: true, pathsChecked: paths.length, findings: [] };
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
|