@affordance/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/engine/compute.d.ts +147 -0
- package/dist/engine/compute.js +152 -0
- package/dist/engine/compute.js.map +1 -0
- package/dist/engine/engine.d.ts +162 -0
- package/dist/engine/engine.js +115 -0
- package/dist/engine/engine.js.map +1 -0
- package/dist/engine/errors.d.ts +6 -0
- package/dist/engine/errors.js +11 -0
- package/dist/engine/errors.js.map +1 -0
- package/dist/engine/index.d.ts +16 -0
- package/dist/engine/index.js +15 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/execution/delta.d.ts +42 -0
- package/dist/execution/delta.js +93 -0
- package/dist/execution/delta.js.map +1 -0
- package/dist/execution/errors.d.ts +94 -0
- package/dist/execution/errors.js +132 -0
- package/dist/execution/errors.js.map +1 -0
- package/dist/execution/execute.d.ts +179 -0
- package/dist/execution/execute.js +356 -0
- package/dist/execution/execute.js.map +1 -0
- package/dist/execution/index.d.ts +25 -0
- package/dist/execution/index.js +20 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/journal.d.ts +190 -0
- package/dist/execution/journal.js +192 -0
- package/dist/execution/journal.js.map +1 -0
- package/dist/execution/port.d.ts +81 -0
- package/dist/execution/port.js +101 -0
- package/dist/execution/port.js.map +1 -0
- package/dist/execution/replay.d.ts +58 -0
- package/dist/execution/replay.js +59 -0
- package/dist/execution/replay.js.map +1 -0
- package/dist/execution/timers.d.ts +21 -0
- package/dist/execution/timers.js +26 -0
- package/dist/execution/timers.js.map +1 -0
- package/dist/execution/transaction.d.ts +24 -0
- package/dist/execution/transaction.js +49 -0
- package/dist/execution/transaction.js.map +1 -0
- package/dist/guards/condition.d.ts +86 -0
- package/dist/guards/condition.js +11 -0
- package/dist/guards/condition.js.map +1 -0
- package/dist/guards/evaluate.d.ts +143 -0
- package/dist/guards/evaluate.js +167 -0
- package/dist/guards/evaluate.js.map +1 -0
- package/dist/guards/guard.d.ts +136 -0
- package/dist/guards/guard.js +118 -0
- package/dist/guards/guard.js.map +1 -0
- package/dist/guards/index.d.ts +20 -0
- package/dist/guards/index.js +17 -0
- package/dist/guards/index.js.map +1 -0
- package/dist/guards/time.d.ts +32 -0
- package/dist/guards/time.js +45 -0
- package/dist/guards/time.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion/correlation.d.ts +54 -0
- package/dist/ingestion/correlation.js +78 -0
- package/dist/ingestion/correlation.js.map +1 -0
- package/dist/ingestion/index.d.ts +16 -0
- package/dist/ingestion/index.js +15 -0
- package/dist/ingestion/index.js.map +1 -0
- package/dist/ingestion/ingest.d.ts +196 -0
- package/dist/ingestion/ingest.js +275 -0
- package/dist/ingestion/ingest.js.map +1 -0
- package/dist/migration/index.d.ts +10 -0
- package/dist/migration/index.js +10 -0
- package/dist/migration/index.js.map +1 -0
- package/dist/migration/migrate.d.ts +119 -0
- package/dist/migration/migrate.js +195 -0
- package/dist/migration/migrate.js.map +1 -0
- package/dist/model/casetype.d.ts +53 -0
- package/dist/model/casetype.js +51 -0
- package/dist/model/casetype.js.map +1 -0
- package/dist/model/errors.d.ts +28 -0
- package/dist/model/errors.js +39 -0
- package/dist/model/errors.js.map +1 -0
- package/dist/model/handler.d.ts +120 -0
- package/dist/model/handler.js +16 -0
- package/dist/model/handler.js.map +1 -0
- package/dist/model/index.d.ts +24 -0
- package/dist/model/index.js +19 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/retry.d.ts +40 -0
- package/dist/model/retry.js +51 -0
- package/dist/model/retry.js.map +1 -0
- package/dist/model/scope.d.ts +67 -0
- package/dist/model/scope.js +19 -0
- package/dist/model/scope.js.map +1 -0
- package/dist/model/step.d.ts +232 -0
- package/dist/model/step.js +210 -0
- package/dist/model/step.js.map +1 -0
- package/dist/model/target.d.ts +175 -0
- package/dist/model/target.js +236 -0
- package/dist/model/target.js.map +1 -0
- package/dist/store/bootstrap.d.ts +57 -0
- package/dist/store/bootstrap.js +268 -0
- package/dist/store/bootstrap.js.map +1 -0
- package/dist/store/errors.d.ts +18 -0
- package/dist/store/errors.js +26 -0
- package/dist/store/errors.js.map +1 -0
- package/dist/store/ids.d.ts +15 -0
- package/dist/store/ids.js +12 -0
- package/dist/store/ids.js.map +1 -0
- package/dist/store/index.d.ts +12 -0
- package/dist/store/index.js +9 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/queryable.d.ts +60 -0
- package/dist/store/queryable.js +7 -0
- package/dist/store/queryable.js.map +1 -0
- package/dist/store/resolve.d.ts +66 -0
- package/dist/store/resolve.js +71 -0
- package/dist/store/resolve.js.map +1 -0
- package/dist/store/sql.d.ts +26 -0
- package/dist/store/sql.js +21 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/store/store.d.ts +66 -0
- package/dist/store/store.js +108 -0
- package/dist/store/store.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit reconstruction.
|
|
3
|
+
*
|
|
4
|
+
* "Why was this affordance available last Tuesday?" is answered from the
|
|
5
|
+
* journaled record of what the system actually believed at the time, never by
|
|
6
|
+
* re-deriving the past through present-day code. A `claimed` entry
|
|
7
|
+
* carries everything that answer needs: the guard evaluation, the instant it
|
|
8
|
+
* was made as of, the Actor, and the Case State it was evaluated against.
|
|
9
|
+
*
|
|
10
|
+
* `replayGuard` re-runs **today's** definition against **that** recorded
|
|
11
|
+
* moment and reports both results side by side. A mismatch is not a bug in
|
|
12
|
+
* the journal — it is the interesting signal: cases float to the latest
|
|
13
|
+
* definitions, so it means the step's guard changed since the
|
|
14
|
+
* Execution ran, and it says exactly how.
|
|
15
|
+
*
|
|
16
|
+
* Replay is a sweep over the Journal, so it is the lenient filter over
|
|
17
|
+
* {@link addressTarget}: a journaled moment today's definitions can no
|
|
18
|
+
* longer address — the step was removed, the element its scope key named no
|
|
19
|
+
* longer selects, the selector throws over the historical document — is
|
|
20
|
+
* itself definition drift, reported as `unaddressable` rather than thrown.
|
|
21
|
+
* One drifted entry must never take down an audit of the rest.
|
|
22
|
+
*/
|
|
23
|
+
import type { GuardEvaluation } from '../guards/index.js';
|
|
24
|
+
import type { AnyCaseType } from '../model/index.js';
|
|
25
|
+
import type { ClaimedJournalEntry } from './journal.js';
|
|
26
|
+
/** What today's definitions make of a journaled moment. */
|
|
27
|
+
export interface GuardReplay {
|
|
28
|
+
readonly executionId: string;
|
|
29
|
+
readonly step: string;
|
|
30
|
+
readonly scopeKey: string | null;
|
|
31
|
+
/** The instant the recorded evaluation was made as of. */
|
|
32
|
+
readonly asOf: string;
|
|
33
|
+
/** The guard results as journaled at the enforcement moment. */
|
|
34
|
+
readonly recorded: GuardEvaluation;
|
|
35
|
+
/**
|
|
36
|
+
* The same guard, re-evaluated now against the journaled state, actor and
|
|
37
|
+
* instant — or `null` when today's definitions cannot address the
|
|
38
|
+
* journaled (step × scope key) at all.
|
|
39
|
+
*/
|
|
40
|
+
readonly reproduced: GuardEvaluation | null;
|
|
41
|
+
/** True when the two agree exactly — the definitions have not drifted for this step. */
|
|
42
|
+
readonly matches: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Why today's definitions could not address the journaled moment, when
|
|
45
|
+
* they could not — the strongest form of drift. `null` when `reproduced`
|
|
46
|
+
* is present.
|
|
47
|
+
*/
|
|
48
|
+
readonly unaddressable: {
|
|
49
|
+
readonly reason: string;
|
|
50
|
+
} | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Re-evaluate a `claimed` entry's guard against the state, actor and instant
|
|
54
|
+
* the entry recorded. Only `claimed` entries carry an evaluation to
|
|
55
|
+
* reproduce, and the parameter type says so — narrow a read entry with
|
|
56
|
+
* `isClaimedEntry` first.
|
|
57
|
+
*/
|
|
58
|
+
export declare const replayGuard: (definition: AnyCaseType, entry: ClaimedJournalEntry) => GuardReplay;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit reconstruction.
|
|
3
|
+
*
|
|
4
|
+
* "Why was this affordance available last Tuesday?" is answered from the
|
|
5
|
+
* journaled record of what the system actually believed at the time, never by
|
|
6
|
+
* re-deriving the past through present-day code. A `claimed` entry
|
|
7
|
+
* carries everything that answer needs: the guard evaluation, the instant it
|
|
8
|
+
* was made as of, the Actor, and the Case State it was evaluated against.
|
|
9
|
+
*
|
|
10
|
+
* `replayGuard` re-runs **today's** definition against **that** recorded
|
|
11
|
+
* moment and reports both results side by side. A mismatch is not a bug in
|
|
12
|
+
* the journal — it is the interesting signal: cases float to the latest
|
|
13
|
+
* definitions, so it means the step's guard changed since the
|
|
14
|
+
* Execution ran, and it says exactly how.
|
|
15
|
+
*
|
|
16
|
+
* Replay is a sweep over the Journal, so it is the lenient filter over
|
|
17
|
+
* {@link addressTarget}: a journaled moment today's definitions can no
|
|
18
|
+
* longer address — the step was removed, the element its scope key named no
|
|
19
|
+
* longer selects, the selector throws over the historical document — is
|
|
20
|
+
* itself definition drift, reported as `unaddressable` rather than thrown.
|
|
21
|
+
* One drifted entry must never take down an audit of the rest.
|
|
22
|
+
*/
|
|
23
|
+
import { addressTarget, evaluateTarget } from '../model/index.js';
|
|
24
|
+
import { jsonEqual } from './delta.js';
|
|
25
|
+
/**
|
|
26
|
+
* Re-evaluate a `claimed` entry's guard against the state, actor and instant
|
|
27
|
+
* the entry recorded. Only `claimed` entries carry an evaluation to
|
|
28
|
+
* reproduce, and the parameter type says so — narrow a read entry with
|
|
29
|
+
* `isClaimedEntry` first.
|
|
30
|
+
*/
|
|
31
|
+
export const replayGuard = (definition, entry) => {
|
|
32
|
+
const identity = {
|
|
33
|
+
executionId: entry.executionId,
|
|
34
|
+
step: entry.step,
|
|
35
|
+
scopeKey: entry.scopeKey,
|
|
36
|
+
asOf: entry.asOf,
|
|
37
|
+
recorded: entry.guard,
|
|
38
|
+
};
|
|
39
|
+
const address = addressTarget(definition, entry.state, entry.step, entry.scopeKey ?? undefined);
|
|
40
|
+
if (address.failure !== null) {
|
|
41
|
+
return {
|
|
42
|
+
...identity,
|
|
43
|
+
reproduced: null,
|
|
44
|
+
matches: false,
|
|
45
|
+
unaddressable: { reason: address.failure.error.message },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const reproduced = evaluateTarget(address.target, {
|
|
49
|
+
actor: entry.actor,
|
|
50
|
+
asOf: entry.asOf,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
...identity,
|
|
54
|
+
reproduced,
|
|
55
|
+
matches: jsonEqual(entry.guard, reproduced),
|
|
56
|
+
unaddressable: null,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/execution/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AA4BtC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,UAAuB,EACvB,KAA0B,EACb,EAAE;IACf,MAAM,QAAQ,GAAG;QACf,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ,EAAE,KAAK,CAAC,KAAK;KACtB,CAAA;IACD,MAAM,OAAO,GAAG,aAAa,CAC3B,UAAU,EACV,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,QAAQ,IAAI,SAAS,CAC5B,CAAA;IACD,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC7B,OAAO;YACL,GAAG,QAAQ;YACX,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE,KAAK;YACd,aAAa,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE;SACzD,CAAA;IACH,CAAC;IACD,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE;QAChD,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,IAAI,EAAE,KAAK,CAAC,IAAI;KACjB,CAAC,CAAA;IACF,OAAO;QACL,GAAG,QAAQ;QACX,UAAU;QACV,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,UAAU,CAAC;QAC3C,aAAa,EAAE,IAAI;KACpB,CAAA;AACH,CAAC,CAAA","sourcesContent":["/**\n * Audit reconstruction.\n *\n * \"Why was this affordance available last Tuesday?\" is answered from the\n * journaled record of what the system actually believed at the time, never by\n * re-deriving the past through present-day code. A `claimed` entry\n * carries everything that answer needs: the guard evaluation, the instant it\n * was made as of, the Actor, and the Case State it was evaluated against.\n *\n * `replayGuard` re-runs **today's** definition against **that** recorded\n * moment and reports both results side by side. A mismatch is not a bug in\n * the journal — it is the interesting signal: cases float to the latest\n * definitions, so it means the step's guard changed since the\n * Execution ran, and it says exactly how.\n *\n * Replay is a sweep over the Journal, so it is the lenient filter over\n * {@link addressTarget}: a journaled moment today's definitions can no\n * longer address — the step was removed, the element its scope key named no\n * longer selects, the selector throws over the historical document — is\n * itself definition drift, reported as `unaddressable` rather than thrown.\n * One drifted entry must never take down an audit of the rest.\n */\n\nimport type { GuardEvaluation } from '../guards/index.js'\nimport type { AnyCaseType } from '../model/index.js'\nimport { addressTarget, evaluateTarget } from '../model/index.js'\nimport { jsonEqual } from './delta.js'\nimport type { ClaimedJournalEntry } from './journal.js'\n\n/** What today's definitions make of a journaled moment. */\nexport interface GuardReplay {\n readonly executionId: string\n readonly step: string\n readonly scopeKey: string | null\n /** The instant the recorded evaluation was made as of. */\n readonly asOf: string\n /** The guard results as journaled at the enforcement moment. */\n readonly recorded: GuardEvaluation\n /**\n * The same guard, re-evaluated now against the journaled state, actor and\n * instant — or `null` when today's definitions cannot address the\n * journaled (step × scope key) at all.\n */\n readonly reproduced: GuardEvaluation | null\n /** True when the two agree exactly — the definitions have not drifted for this step. */\n readonly matches: boolean\n /**\n * Why today's definitions could not address the journaled moment, when\n * they could not — the strongest form of drift. `null` when `reproduced`\n * is present.\n */\n readonly unaddressable: { readonly reason: string } | null\n}\n\n/**\n * Re-evaluate a `claimed` entry's guard against the state, actor and instant\n * the entry recorded. Only `claimed` entries carry an evaluation to\n * reproduce, and the parameter type says so — narrow a read entry with\n * `isClaimedEntry` first.\n */\nexport const replayGuard = (\n definition: AnyCaseType,\n entry: ClaimedJournalEntry,\n): GuardReplay => {\n const identity = {\n executionId: entry.executionId,\n step: entry.step,\n scopeKey: entry.scopeKey,\n asOf: entry.asOf,\n recorded: entry.guard,\n }\n const address = addressTarget(\n definition,\n entry.state,\n entry.step,\n entry.scopeKey ?? undefined,\n )\n if (address.failure !== null) {\n return {\n ...identity,\n reproduced: null,\n matches: false,\n unaddressable: { reason: address.failure.error.message },\n }\n }\n const reproduced = evaluateTarget(address.target, {\n actor: entry.actor,\n asOf: entry.asOf,\n })\n return {\n ...identity,\n reproduced,\n matches: jsonEqual(entry.guard, reproduced),\n unaddressable: null,\n }\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process-timer half of the lifecycle's clock.
|
|
3
|
+
*
|
|
4
|
+
* `now` answers "what time is it"; these answer "wake me later" — the retry
|
|
5
|
+
* delay and the heartbeat. They are a seam for the same reason `now` is:
|
|
6
|
+
* the lease story (renewal past the TTL, backoff between attempts) is only
|
|
7
|
+
* testable when a test can drive elapsed time instead of sleeping through
|
|
8
|
+
* it. The engine always binds {@link realTimers}; the in-memory lifecycle
|
|
9
|
+
* tests bind a virtual clock that advances `now` and fires due timers
|
|
10
|
+
* together.
|
|
11
|
+
*/
|
|
12
|
+
export interface Timers {
|
|
13
|
+
/** Resolve after `ms` — the delay between retry attempts. */
|
|
14
|
+
readonly sleep: (ms: number) => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Run `fn` every `ms` until the returned stop function is called — the
|
|
17
|
+
* heartbeat.
|
|
18
|
+
*/
|
|
19
|
+
readonly every: (ms: number, fn: () => void) => () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare const realTimers: Timers;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process-timer half of the lifecycle's clock.
|
|
3
|
+
*
|
|
4
|
+
* `now` answers "what time is it"; these answer "wake me later" — the retry
|
|
5
|
+
* delay and the heartbeat. They are a seam for the same reason `now` is:
|
|
6
|
+
* the lease story (renewal past the TTL, backoff between attempts) is only
|
|
7
|
+
* testable when a test can drive elapsed time instead of sleeping through
|
|
8
|
+
* it. The engine always binds {@link realTimers}; the in-memory lifecycle
|
|
9
|
+
* tests bind a virtual clock that advances `now` and fires due timers
|
|
10
|
+
* together.
|
|
11
|
+
*/
|
|
12
|
+
export const realTimers = {
|
|
13
|
+
// Deliberately not unref'd: a retry delay is work in progress, and a
|
|
14
|
+
// process that exits during one abandons a claimed Execution.
|
|
15
|
+
sleep: (ms) => ms <= 0
|
|
16
|
+
? Promise.resolve()
|
|
17
|
+
: new Promise((resolve) => {
|
|
18
|
+
setTimeout(resolve, ms);
|
|
19
|
+
}),
|
|
20
|
+
every: (ms, fn) => {
|
|
21
|
+
const timer = setInterval(fn, ms);
|
|
22
|
+
timer.unref?.();
|
|
23
|
+
return () => clearInterval(timer);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=timers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timers.js","sourceRoot":"","sources":["../../src/execution/timers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH,MAAM,CAAC,MAAM,UAAU,GAAW;IAChC,qEAAqE;IACrE,8DAA8D;IAC9D,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CACZ,EAAE,IAAI,CAAC;QACL,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE;QACnB,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YACtB,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;QACzB,CAAC,CAAC;IACR,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;QAChB,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;QACjC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;QACf,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IACnC,CAAC;CACF,CAAA","sourcesContent":["/**\n * The process-timer half of the lifecycle's clock.\n *\n * `now` answers \"what time is it\"; these answer \"wake me later\" — the retry\n * delay and the heartbeat. They are a seam for the same reason `now` is:\n * the lease story (renewal past the TTL, backoff between attempts) is only\n * testable when a test can drive elapsed time instead of sleeping through\n * it. The engine always binds {@link realTimers}; the in-memory lifecycle\n * tests bind a virtual clock that advances `now` and fires due timers\n * together.\n */\n\nexport interface Timers {\n /** Resolve after `ms` — the delay between retry attempts. */\n readonly sleep: (ms: number) => Promise<void>\n /**\n * Run `fn` every `ms` until the returned stop function is called — the\n * heartbeat.\n */\n readonly every: (ms: number, fn: () => void) => () => void\n}\n\nexport const realTimers: Timers = {\n // Deliberately not unref'd: a retry delay is work in progress, and a\n // process that exits during one abandons a claimed Execution.\n sleep: (ms) =>\n ms <= 0\n ? Promise.resolve()\n : new Promise((resolve) => {\n setTimeout(resolve, ms)\n }),\n every: (ms, fn) => {\n const timer = setInterval(fn, ms)\n timer.unref?.()\n return () => clearInterval(timer)\n },\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transaction seam.
|
|
3
|
+
*
|
|
4
|
+
* The claim and the commit are each one short transaction; the handler runs
|
|
5
|
+
* between them, outside both — so no transaction is ever held open
|
|
6
|
+
* across a handler's external calls. `withTransaction` is how the execution
|
|
7
|
+
* lifecycle gets one, and it is also what a handler's `ctx.onCommit` writes
|
|
8
|
+
* ride along inside.
|
|
9
|
+
*/
|
|
10
|
+
import type { DatabaseAccess, Transaction } from '../store/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` inside a transaction and hand it the handle to use — a checked-out
|
|
13
|
+
* client when the caller brought a pool, the client itself when they brought
|
|
14
|
+
* one (issuing `begin` on a *pool* would put each statement on a different
|
|
15
|
+
* connection, so the distinction is not cosmetic). Which case applies is the
|
|
16
|
+
* caller's declaration — {@link DatabaseAccess} — never sniffed from the
|
|
17
|
+
* object.
|
|
18
|
+
*
|
|
19
|
+
* Commits on return, rolls back on throw, and always releases what it
|
|
20
|
+
* checked out. A rollback that itself fails is swallowed: the original error
|
|
21
|
+
* is what the caller needs, and a connection too broken to roll back is
|
|
22
|
+
* discarded by the pool anyway.
|
|
23
|
+
*/
|
|
24
|
+
export declare const withTransaction: <T>(db: DatabaseAccess, fn: (tx: Transaction) => Promise<T>) => Promise<T>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transaction seam.
|
|
3
|
+
*
|
|
4
|
+
* The claim and the commit are each one short transaction; the handler runs
|
|
5
|
+
* between them, outside both — so no transaction is ever held open
|
|
6
|
+
* across a handler's external calls. `withTransaction` is how the execution
|
|
7
|
+
* lifecycle gets one, and it is also what a handler's `ctx.onCommit` writes
|
|
8
|
+
* ride along inside.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Run `fn` inside a transaction and hand it the handle to use — a checked-out
|
|
12
|
+
* client when the caller brought a pool, the client itself when they brought
|
|
13
|
+
* one (issuing `begin` on a *pool* would put each statement on a different
|
|
14
|
+
* connection, so the distinction is not cosmetic). Which case applies is the
|
|
15
|
+
* caller's declaration — {@link DatabaseAccess} — never sniffed from the
|
|
16
|
+
* object.
|
|
17
|
+
*
|
|
18
|
+
* Commits on return, rolls back on throw, and always releases what it
|
|
19
|
+
* checked out. A rollback that itself fails is swallowed: the original error
|
|
20
|
+
* is what the caller needs, and a connection too broken to roll back is
|
|
21
|
+
* discarded by the pool anyway.
|
|
22
|
+
*/
|
|
23
|
+
export const withTransaction = async (db, fn) => {
|
|
24
|
+
if (!('pool' in db))
|
|
25
|
+
return runTransaction(db.client, fn);
|
|
26
|
+
const client = await db.pool.connect();
|
|
27
|
+
try {
|
|
28
|
+
return await runTransaction(client, fn);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
client.release();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const runTransaction = async (handle, fn) => {
|
|
35
|
+
// The one place the brand is applied: past the `begin` below, this handle
|
|
36
|
+
// really is inside a transaction.
|
|
37
|
+
const tx = handle;
|
|
38
|
+
await tx.query('begin');
|
|
39
|
+
try {
|
|
40
|
+
const result = await fn(tx);
|
|
41
|
+
await tx.query('commit');
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
await tx.query('rollback').catch(() => undefined);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../../src/execution/transaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,EAAkB,EAClB,EAAmC,EACvB,EAAE;IACd,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;QAAE,OAAO,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACzD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAA;IACtC,IAAI,CAAC;QACH,OAAO,MAAM,cAAc,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACzC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,MAAM,cAAc,GAAG,KAAK,EAC1B,MAAiB,EACjB,EAAmC,EACvB,EAAE;IACd,0EAA0E;IAC1E,kCAAkC;IAClC,MAAM,EAAE,GAAG,MAAqB,CAAA;IAChC,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACvB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAA;QAC3B,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QACxB,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QACjD,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC,CAAA","sourcesContent":["/**\n * The transaction seam.\n *\n * The claim and the commit are each one short transaction; the handler runs\n * between them, outside both — so no transaction is ever held open\n * across a handler's external calls. `withTransaction` is how the execution\n * lifecycle gets one, and it is also what a handler's `ctx.onCommit` writes\n * ride along inside.\n */\n\nimport type { DatabaseAccess, Queryable, Transaction } from '../store/index.js'\n\n/**\n * Run `fn` inside a transaction and hand it the handle to use — a checked-out\n * client when the caller brought a pool, the client itself when they brought\n * one (issuing `begin` on a *pool* would put each statement on a different\n * connection, so the distinction is not cosmetic). Which case applies is the\n * caller's declaration — {@link DatabaseAccess} — never sniffed from the\n * object.\n *\n * Commits on return, rolls back on throw, and always releases what it\n * checked out. A rollback that itself fails is swallowed: the original error\n * is what the caller needs, and a connection too broken to roll back is\n * discarded by the pool anyway.\n */\nexport const withTransaction = async <T>(\n db: DatabaseAccess,\n fn: (tx: Transaction) => Promise<T>,\n): Promise<T> => {\n if (!('pool' in db)) return runTransaction(db.client, fn)\n const client = await db.pool.connect()\n try {\n return await runTransaction(client, fn)\n } finally {\n client.release()\n }\n}\n\nconst runTransaction = async <T>(\n handle: Queryable,\n fn: (tx: Transaction) => Promise<T>,\n): Promise<T> => {\n // The one place the brand is applied: past the `begin` below, this handle\n // really is inside a transaction.\n const tx = handle as Transaction\n await tx.query('begin')\n try {\n const result = await fn(tx)\n await tx.query('commit')\n return result\n } catch (error) {\n await tx.query('rollback').catch(() => undefined)\n throw error\n }\n}\n"]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named conditions — the unit of explainability.
|
|
3
|
+
*
|
|
4
|
+
* A guard is a set of independently-evaluated named conditions, never an
|
|
5
|
+
* opaque boolean closure. Every consumer of the framework's answer to "what
|
|
6
|
+
* can happen now" — affordance listings, `explain`, the journal's
|
|
7
|
+
* guard-results field — leans on conditions being
|
|
8
|
+
* structured, synchronous, and side-effect-free.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The escape-hatch result form: a condition may return `{ ok, reason? }`
|
|
12
|
+
* instead of a bare boolean when it wants to say *why* it failed (or passed)
|
|
13
|
+
* in domain language. The `reason` surfaces verbatim in the evaluation
|
|
14
|
+
* record, and from there in `explain` and the journal.
|
|
15
|
+
*/
|
|
16
|
+
export interface ConditionVerdict {
|
|
17
|
+
readonly ok: boolean;
|
|
18
|
+
readonly reason?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a condition may return: a bare boolean, or a {@link ConditionVerdict}
|
|
22
|
+
* carrying a reason. Nothing else — in particular no `Promise`. A `Promise`
|
|
23
|
+
* is not assignable to this type, which is what makes async conditions
|
|
24
|
+
* inexpressible rather than merely discouraged.
|
|
25
|
+
*/
|
|
26
|
+
export type ConditionOutcome = boolean | ConditionVerdict;
|
|
27
|
+
/**
|
|
28
|
+
* The context a condition receives alongside case state.
|
|
29
|
+
*
|
|
30
|
+
* Carries the acting actor — opaque to the framework; the app owns identity
|
|
31
|
+
* and roles. Deliberately carries no
|
|
32
|
+
* clock: a condition can never read "now", so that evaluation
|
|
33
|
+
* "as of T" stays well-defined and deterministic.
|
|
34
|
+
*
|
|
35
|
+
* Scoped steps: when a scoped step's guard is evaluated for
|
|
36
|
+
* one element of its scoped collection, the bound element rides here as
|
|
37
|
+
* `ctx.scope` — e.g. one buyer of `s.buyers` — so a condition can read
|
|
38
|
+
* both the whole case state (its first argument) and the element the
|
|
39
|
+
* affordance is about. For unscoped evaluation `scope` is absent. This is the
|
|
40
|
+
* additive extension the original context reserved; conditions written
|
|
41
|
+
* before it exist keep compiling and behaving identically.
|
|
42
|
+
*/
|
|
43
|
+
export interface ConditionContext<TActor = unknown, TScope = unknown> {
|
|
44
|
+
/** The acting actor — app-defined shape; the framework never owns identity. */
|
|
45
|
+
readonly actor: TActor;
|
|
46
|
+
/**
|
|
47
|
+
* The bound scope element, when evaluating a scoped step's guard for one
|
|
48
|
+
* element of its scoped collection. Absent for unscoped steps.
|
|
49
|
+
* The model layer's scoped-condition types narrow this to the element type
|
|
50
|
+
* and make it non-optional for scoped-step authors.
|
|
51
|
+
*/
|
|
52
|
+
readonly scope?: TScope;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One named, pure, synchronous predicate within a guard — the unit of
|
|
56
|
+
* explainability.
|
|
57
|
+
*
|
|
58
|
+
* A condition must be:
|
|
59
|
+
*
|
|
60
|
+
* - **Synchronous.** Enforced by type: the return type admits no `Promise`,
|
|
61
|
+
* so an `async` condition is a type error.
|
|
62
|
+
* - **Side-effect-free.** Conditions are evaluated freely and repeatedly —
|
|
63
|
+
* for affordance listings, `explain`, transactional re-evaluation at
|
|
64
|
+
* execution time, and historical reconstruction. They must not mutate
|
|
65
|
+
* state, log, or touch the outside world.
|
|
66
|
+
* - **Total over historical state.** Cases float to the latest
|
|
67
|
+
* definitions, so a condition WILL be evaluated against state written
|
|
68
|
+
* before the condition existed. Handle absence deliberately with the
|
|
69
|
+
* `?? fallback` discipline — `s.split?.confirmed ?? false` — and
|
|
70
|
+
* never assume a field exists just because current handlers write it.
|
|
71
|
+
* This cannot be enforced by types; it is the baseline discipline every
|
|
72
|
+
* condition author owes the cases already in flight.
|
|
73
|
+
* - **Clock-free.** Never read `Date.now()` or `new Date()` — a condition's
|
|
74
|
+
* answer must be a function of (state, actor) alone.
|
|
75
|
+
*
|
|
76
|
+
* A condition should never throw. If one does (typically a totality bug
|
|
77
|
+
* against sparse historical state), evaluation does not propagate the throw:
|
|
78
|
+
* the condition is reported as failed with the thrown message as its reason,
|
|
79
|
+
* so one buggy condition degrades one answer, not the whole case.
|
|
80
|
+
*
|
|
81
|
+
* @typeParam TState - the case state document the condition reads. Conditions
|
|
82
|
+
* of a scoped step also receive the whole case state here; the
|
|
83
|
+
* bound scope element arrives via `ctx.scope`.
|
|
84
|
+
* @typeParam TActor - the app-defined actor shape; opaque to the framework.
|
|
85
|
+
*/
|
|
86
|
+
export type Condition<TState, TActor = unknown> = (state: TState, ctx: ConditionContext<TActor>) => ConditionOutcome;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named conditions — the unit of explainability.
|
|
3
|
+
*
|
|
4
|
+
* A guard is a set of independently-evaluated named conditions, never an
|
|
5
|
+
* opaque boolean closure. Every consumer of the framework's answer to "what
|
|
6
|
+
* can happen now" — affordance listings, `explain`, the journal's
|
|
7
|
+
* guard-results field — leans on conditions being
|
|
8
|
+
* structured, synchronous, and side-effect-free.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=condition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"condition.js","sourceRoot":"","sources":["../../src/guards/condition.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG","sourcesContent":["/**\n * Named conditions — the unit of explainability.\n *\n * A guard is a set of independently-evaluated named conditions, never an\n * opaque boolean closure. Every consumer of the framework's answer to \"what\n * can happen now\" — affordance listings, `explain`, the journal's\n * guard-results field — leans on conditions being\n * structured, synchronous, and side-effect-free.\n */\n\n/**\n * The escape-hatch result form: a condition may return `{ ok, reason? }`\n * instead of a bare boolean when it wants to say *why* it failed (or passed)\n * in domain language. The `reason` surfaces verbatim in the evaluation\n * record, and from there in `explain` and the journal.\n */\nexport interface ConditionVerdict {\n readonly ok: boolean\n readonly reason?: string\n}\n\n/**\n * What a condition may return: a bare boolean, or a {@link ConditionVerdict}\n * carrying a reason. Nothing else — in particular no `Promise`. A `Promise`\n * is not assignable to this type, which is what makes async conditions\n * inexpressible rather than merely discouraged.\n */\nexport type ConditionOutcome = boolean | ConditionVerdict\n\n/**\n * The context a condition receives alongside case state.\n *\n * Carries the acting actor — opaque to the framework; the app owns identity\n * and roles. Deliberately carries no\n * clock: a condition can never read \"now\", so that evaluation\n * \"as of T\" stays well-defined and deterministic.\n *\n * Scoped steps: when a scoped step's guard is evaluated for\n * one element of its scoped collection, the bound element rides here as\n * `ctx.scope` — e.g. one buyer of `s.buyers` — so a condition can read\n * both the whole case state (its first argument) and the element the\n * affordance is about. For unscoped evaluation `scope` is absent. This is the\n * additive extension the original context reserved; conditions written\n * before it exist keep compiling and behaving identically.\n */\nexport interface ConditionContext<TActor = unknown, TScope = unknown> {\n /** The acting actor — app-defined shape; the framework never owns identity. */\n readonly actor: TActor\n /**\n * The bound scope element, when evaluating a scoped step's guard for one\n * element of its scoped collection. Absent for unscoped steps.\n * The model layer's scoped-condition types narrow this to the element type\n * and make it non-optional for scoped-step authors.\n */\n readonly scope?: TScope\n}\n\n/**\n * One named, pure, synchronous predicate within a guard — the unit of\n * explainability.\n *\n * A condition must be:\n *\n * - **Synchronous.** Enforced by type: the return type admits no `Promise`,\n * so an `async` condition is a type error.\n * - **Side-effect-free.** Conditions are evaluated freely and repeatedly —\n * for affordance listings, `explain`, transactional re-evaluation at\n * execution time, and historical reconstruction. They must not mutate\n * state, log, or touch the outside world.\n * - **Total over historical state.** Cases float to the latest\n * definitions, so a condition WILL be evaluated against state written\n * before the condition existed. Handle absence deliberately with the\n * `?? fallback` discipline — `s.split?.confirmed ?? false` — and\n * never assume a field exists just because current handlers write it.\n * This cannot be enforced by types; it is the baseline discipline every\n * condition author owes the cases already in flight.\n * - **Clock-free.** Never read `Date.now()` or `new Date()` — a condition's\n * answer must be a function of (state, actor) alone.\n *\n * A condition should never throw. If one does (typically a totality bug\n * against sparse historical state), evaluation does not propagate the throw:\n * the condition is reported as failed with the thrown message as its reason,\n * so one buggy condition degrades one answer, not the whole case.\n *\n * @typeParam TState - the case state document the condition reads. Conditions\n * of a scoped step also receive the whole case state here; the\n * bound scope element arrives via `ctx.scope`.\n * @typeParam TActor - the app-defined actor shape; opaque to the framework.\n */\nexport type Condition<TState, TActor = unknown> = (\n state: TState,\n ctx: ConditionContext<TActor>,\n) => ConditionOutcome\n"]}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard evaluation — pure, deterministic, explainable.
|
|
3
|
+
*
|
|
4
|
+
* `evaluateGuard(guard, ctx)` turns a guard and an evaluation context
|
|
5
|
+
* (case state, actor, asOf) into a full per-condition evaluation record:
|
|
6
|
+
* the raw material for affordance computation, `explain`, and
|
|
7
|
+
* the journal's guard-results field.
|
|
8
|
+
*/
|
|
9
|
+
import type { Guard, GuardSection } from './guard.js';
|
|
10
|
+
import type { Instant } from './time.js';
|
|
11
|
+
/**
|
|
12
|
+
* Everything guard evaluation is a function of: case state, the acting
|
|
13
|
+
* actor, and the instant to evaluate as of. Nothing else — no ambient
|
|
14
|
+
* clock, no I/O — so the same context always produces the same record.
|
|
15
|
+
*
|
|
16
|
+
* `asOf` is explicit and mandatory: it is the instant the evaluation is
|
|
17
|
+
* made as of, stated on the record so the journal's claim-time evidence is
|
|
18
|
+
* self-contained.
|
|
19
|
+
*
|
|
20
|
+
* Scoped steps: when evaluating a scoped step's guard for
|
|
21
|
+
* one element of its scoped collection, bind the element as `scope`. It is
|
|
22
|
+
* threaded through to conditions via {@link ConditionContext}. When
|
|
23
|
+
* `scope` is absent, evaluation is exactly what it always was.
|
|
24
|
+
*/
|
|
25
|
+
export interface GuardEvaluationContext<TState, TActor = unknown> {
|
|
26
|
+
readonly state: TState;
|
|
27
|
+
readonly actor: TActor;
|
|
28
|
+
readonly asOf: Instant;
|
|
29
|
+
/**
|
|
30
|
+
* The bound scope element, when evaluating a scoped step's guard for one
|
|
31
|
+
* element. Threaded to conditions as `ctx.scope`. Omit for unscoped
|
|
32
|
+
* evaluation. `undefined` means "not scoped" — a scope binding is always a
|
|
33
|
+
* present element of a state collection.
|
|
34
|
+
*/
|
|
35
|
+
readonly scope?: unknown;
|
|
36
|
+
}
|
|
37
|
+
/** Fields common to every per-condition result in an evaluation record. */
|
|
38
|
+
export interface ConditionResultBase {
|
|
39
|
+
/** The condition's name in its section map (or its arm name, within an anyOf group). */
|
|
40
|
+
readonly name: string;
|
|
41
|
+
/** Which guard section the condition came from — `requires` or `permits`. */
|
|
42
|
+
readonly section: GuardSection;
|
|
43
|
+
readonly passed: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Present when the condition supplied one via the `{ ok, reason? }`
|
|
46
|
+
* escape hatch, or when evaluation had to absorb a defect (a condition
|
|
47
|
+
* that threw, or returned an unrecognized result).
|
|
48
|
+
*/
|
|
49
|
+
readonly reason?: string;
|
|
50
|
+
}
|
|
51
|
+
/** Result of one plain (predicate) condition. */
|
|
52
|
+
export interface SingleConditionResult extends ConditionResultBase {
|
|
53
|
+
readonly kind: 'condition';
|
|
54
|
+
}
|
|
55
|
+
/** Result of one arm of an `anyOf` group; `name` is the arm name. */
|
|
56
|
+
export type AnyOfArmResult = SingleConditionResult;
|
|
57
|
+
/**
|
|
58
|
+
* Result of an `anyOf` group: passed when at least one arm passed. Every
|
|
59
|
+
* arm is always evaluated and reported, so a failing group names each
|
|
60
|
+
* failing arm and a passing group shows which arm carried it.
|
|
61
|
+
*/
|
|
62
|
+
export interface AnyOfConditionResult extends ConditionResultBase {
|
|
63
|
+
readonly kind: 'anyOf';
|
|
64
|
+
readonly arms: readonly AnyOfArmResult[];
|
|
65
|
+
}
|
|
66
|
+
/** Result of one named entry in a guard section. */
|
|
67
|
+
export type ConditionResult = SingleConditionResult | AnyOfConditionResult;
|
|
68
|
+
/**
|
|
69
|
+
* The full record of one guard evaluation — a plain, JSON-serializable
|
|
70
|
+
* object, deterministic in (guard, state, actor, asOf). This is what
|
|
71
|
+
* affordance computation filters on, what `explain` renders, and what the
|
|
72
|
+
* journal stores as an execution's guard results.
|
|
73
|
+
*
|
|
74
|
+
* The `possible` / `permitted` pair is the requires/permits split made mechanical:
|
|
75
|
+
* `possible` is false when a `requires` condition is unmet (the step is not
|
|
76
|
+
* possible on this case, for anyone); `permitted` is false when a `permits`
|
|
77
|
+
* condition is unmet (not permitted for this actor). `available` is their
|
|
78
|
+
* conjunction. An omitted section is vacuously satisfied.
|
|
79
|
+
*/
|
|
80
|
+
export interface GuardEvaluation {
|
|
81
|
+
/** The instant evaluated as of, normalized to ISO-8601 UTC. */
|
|
82
|
+
readonly asOf: string;
|
|
83
|
+
/** Every `requires` condition holds — the step is possible on this case. */
|
|
84
|
+
readonly possible: boolean;
|
|
85
|
+
/** Every `permits` condition holds — the actor is permitted. */
|
|
86
|
+
readonly permitted: boolean;
|
|
87
|
+
/** `possible && permitted` — the step is an affordance for this actor. */
|
|
88
|
+
readonly available: boolean;
|
|
89
|
+
/** Per-condition results, in guard declaration order (`requires` first). */
|
|
90
|
+
readonly conditions: readonly ConditionResult[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Marker for evaluating a guard with no actor at hand — `explain`'s
|
|
94
|
+
* requires-only probe. A `permits` condition is the only kind of entry
|
|
95
|
+
* that reads the actor, so it alone is skipped: reported failed with
|
|
96
|
+
* {@link NOT_EVALUATED_REASON} rather than run against nothing and absorbed
|
|
97
|
+
* as a thrown `TypeError`.
|
|
98
|
+
*/
|
|
99
|
+
export declare const NO_ACTOR: unique symbol;
|
|
100
|
+
/** The stated reason a `permits` condition reports under {@link NO_ACTOR}. */
|
|
101
|
+
export declare const NOT_EVALUATED_REASON = "not evaluated: no actor supplied";
|
|
102
|
+
/**
|
|
103
|
+
* Evaluate a guard against (case state, actor, asOf) and return the full
|
|
104
|
+
* evaluation record.
|
|
105
|
+
*
|
|
106
|
+
* Guarantees the rest of the engine leans on:
|
|
107
|
+
*
|
|
108
|
+
* - **Pure and deterministic.** No I/O, no ambient clock; the same
|
|
109
|
+
* (guard, state, actor, asOf) always yields a deeply-equal record.
|
|
110
|
+
* - **Complete.** Every condition — including every arm of every `anyOf`
|
|
111
|
+
* group — is evaluated and reported; there is no short-circuiting, so the
|
|
112
|
+
* record always answers "why / why not" in full.
|
|
113
|
+
* - **Total.** A condition that throws, or returns a malformed
|
|
114
|
+
* outcome, becomes a failed result with a diagnostic `reason`; one
|
|
115
|
+
* defective condition can never take down affordance computation for a
|
|
116
|
+
* case. (The only throw is a `TypeError` when `ctx.asOf` itself is not a
|
|
117
|
+
* determinable instant — that is a caller bug, not case data.)
|
|
118
|
+
* - **Serializable.** The record is a plain object with only JSON-safe
|
|
119
|
+
* values, ready for the journal's guard-results field verbatim.
|
|
120
|
+
*/
|
|
121
|
+
export declare const evaluateGuard: <TState, TActor = unknown>(guard: Guard<TState, TActor>, ctx: GuardEvaluationContext<TState, TActor>) => GuardEvaluation;
|
|
122
|
+
/**
|
|
123
|
+
* The unmet conditions of an evaluation — the derived view every consumer
|
|
124
|
+
* of a refusal renders. A view, not a convention: the error message, the
|
|
125
|
+
* blocked entry and the wire payload all call this instead of each spelling
|
|
126
|
+
* `conditions.filter(!passed)` for themselves.
|
|
127
|
+
*/
|
|
128
|
+
export declare const unmetConditions: (evaluation: GuardEvaluation) => readonly ConditionResult[];
|
|
129
|
+
/** One unmet condition, addressed — `requires.financing.preApproved` — with its reason when it gave one. */
|
|
130
|
+
export interface AddressedUnmet {
|
|
131
|
+
/** See {@link conditionAddress}: `section.name`, or `section.name.arm` inside an `anyOf`. */
|
|
132
|
+
readonly address: string;
|
|
133
|
+
readonly reason?: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The unmet conditions of an evaluation, flattened to addresses. An `anyOf`
|
|
137
|
+
* group that failed names each failing arm — the same addresses `explain`
|
|
138
|
+
* uses — so a refusal never says less than the
|
|
139
|
+
* evaluation knows.
|
|
140
|
+
*/
|
|
141
|
+
export declare const unmetAddresses: (evaluation: GuardEvaluation) => readonly AddressedUnmet[];
|
|
142
|
+
/** Render the unmet conditions as one message clause: `requires.a — why; permits.b`. */
|
|
143
|
+
export declare const describeUnmet: (evaluation: GuardEvaluation) => string;
|