jsfe 1.6.0 → 1.7.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.
Files changed (103) hide show
  1. package/README.md +32 -4
  2. package/dist/forms/action-projection.d.ts +4 -0
  3. package/dist/forms/action-projection.d.ts.map +1 -0
  4. package/dist/forms/action-projection.js +21 -0
  5. package/dist/forms/action-recovery.d.ts +12 -0
  6. package/dist/forms/action-recovery.d.ts.map +1 -0
  7. package/dist/forms/action-recovery.js +125 -0
  8. package/dist/forms/action-tools.d.ts +29 -0
  9. package/dist/forms/action-tools.d.ts.map +1 -0
  10. package/dist/forms/action-tools.js +6 -0
  11. package/dist/forms/address-flows.d.ts +2 -0
  12. package/dist/forms/address-flows.d.ts.map +1 -0
  13. package/dist/forms/address-flows.js +39 -0
  14. package/dist/forms/choice-flow.d.ts +419 -0
  15. package/dist/forms/choice-flow.d.ts.map +1 -0
  16. package/dist/forms/choice-flow.js +707 -0
  17. package/dist/forms/contracts.d.ts +237 -0
  18. package/dist/forms/contracts.d.ts.map +1 -0
  19. package/dist/forms/contracts.js +278 -0
  20. package/dist/forms/flows.d.ts +611 -0
  21. package/dist/forms/flows.d.ts.map +1 -0
  22. package/dist/forms/flows.js +675 -0
  23. package/dist/forms/identity-flows.d.ts +17 -0
  24. package/dist/forms/identity-flows.d.ts.map +1 -0
  25. package/dist/forms/identity-flows.js +302 -0
  26. package/dist/forms/index.d.ts +7 -0
  27. package/dist/forms/index.d.ts.map +1 -0
  28. package/dist/forms/index.js +10 -0
  29. package/dist/forms/prompt-compiler.d.ts +3 -0
  30. package/dist/forms/prompt-compiler.d.ts.map +1 -0
  31. package/dist/forms/prompt-compiler.js +30 -0
  32. package/dist/forms/session-actions.d.ts +14 -0
  33. package/dist/forms/session-actions.d.ts.map +1 -0
  34. package/dist/forms/session-actions.js +40 -0
  35. package/dist/helpers/address.d.ts +27 -0
  36. package/dist/helpers/address.d.ts.map +1 -0
  37. package/dist/helpers/address.js +29 -0
  38. package/dist/helpers/answer.d.ts +5 -0
  39. package/dist/helpers/answer.d.ts.map +1 -1
  40. package/dist/helpers/answer.js +5 -3
  41. package/dist/helpers/choice-offer.d.ts +3 -0
  42. package/dist/helpers/choice-offer.d.ts.map +1 -0
  43. package/dist/helpers/choice-offer.js +6 -0
  44. package/dist/helpers/form-actions.d.ts +13 -0
  45. package/dist/helpers/form-actions.d.ts.map +1 -0
  46. package/dist/helpers/form-actions.js +144 -0
  47. package/dist/helpers/form-sources.d.ts +7 -0
  48. package/dist/helpers/form-sources.d.ts.map +1 -0
  49. package/dist/helpers/form-sources.js +47 -0
  50. package/dist/helpers/form-state.d.ts +94 -0
  51. package/dist/helpers/form-state.d.ts.map +1 -0
  52. package/dist/helpers/form-state.js +870 -0
  53. package/dist/helpers/index.d.ts +114 -1
  54. package/dist/helpers/index.d.ts.map +1 -1
  55. package/dist/helpers/index.js +9 -4
  56. package/dist/helpers/match-inputs.d.ts +14 -0
  57. package/dist/helpers/match-inputs.d.ts.map +1 -1
  58. package/dist/helpers/match-inputs.js +154 -41
  59. package/dist/helpers/money.d.ts +12 -0
  60. package/dist/helpers/money.d.ts.map +1 -0
  61. package/dist/helpers/money.js +54 -0
  62. package/dist/helpers/spoken-money.d.ts +20 -0
  63. package/dist/helpers/spoken-money.d.ts.map +1 -0
  64. package/dist/helpers/spoken-money.js +143 -0
  65. package/dist/host/action-journal.d.ts +98 -0
  66. package/dist/host/action-journal.d.ts.map +1 -0
  67. package/dist/host/action-journal.js +218 -0
  68. package/dist/host/action-session.d.ts +12 -0
  69. package/dist/host/action-session.d.ts.map +1 -0
  70. package/dist/host/action-session.js +29 -0
  71. package/dist/host/capabilities.d.ts +14 -0
  72. package/dist/host/capabilities.d.ts.map +1 -0
  73. package/dist/host/capabilities.js +60 -0
  74. package/dist/host/checkpoint-codec-error.d.ts +6 -0
  75. package/dist/host/checkpoint-codec-error.d.ts.map +1 -0
  76. package/dist/host/checkpoint-codec-error.js +4 -0
  77. package/dist/host/index.d.ts +6 -0
  78. package/dist/host/index.d.ts.map +1 -1
  79. package/dist/host/index.js +6 -0
  80. package/dist/host/intent-stages.d.ts +1 -0
  81. package/dist/host/intent-stages.d.ts.map +1 -1
  82. package/dist/host/intent-stages.js +3 -3
  83. package/dist/host/node.d.ts +17 -0
  84. package/dist/host/node.d.ts.map +1 -0
  85. package/dist/host/node.js +64 -0
  86. package/dist/host/recovery-identity.d.ts +16 -0
  87. package/dist/host/recovery-identity.d.ts.map +1 -0
  88. package/dist/host/recovery-identity.js +73 -0
  89. package/dist/host/recovery-policy.d.ts +5 -0
  90. package/dist/host/recovery-policy.d.ts.map +1 -0
  91. package/dist/host/recovery-policy.js +54 -0
  92. package/dist/host/runtime-manifest.d.ts +3 -0
  93. package/dist/host/runtime-manifest.d.ts.map +1 -0
  94. package/dist/host/runtime-manifest.js +38 -0
  95. package/dist/index.d.ts +61 -0
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +186 -45
  98. package/dist/runtime-identity.d.ts +3 -0
  99. package/dist/runtime-identity.d.ts.map +1 -0
  100. package/dist/runtime-identity.js +1 -0
  101. package/docs/development-release.md +25 -0
  102. package/docs/forms.md +132 -0
  103. package/package.json +15 -4
@@ -0,0 +1,20 @@
1
+ /** [{value,start,end}] covers the entire currency phrase, including a connected cents clause.
2
+ * Only readings containing actual cardinal words are returned; existing numeric parsing retains ownership of digits. */
3
+ export function scanSpokenMoney(answer: any): {
4
+ readings: {
5
+ value: any;
6
+ start: number;
7
+ end: number;
8
+ }[];
9
+ invalid: {
10
+ start: number;
11
+ end: number;
12
+ reason: string;
13
+ }[];
14
+ };
15
+ export function readSpokenMoney(answer: any): {
16
+ value: any;
17
+ start: number;
18
+ end: number;
19
+ }[];
20
+ //# sourceMappingURL=spoken-money.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spoken-money.d.ts","sourceRoot":"","sources":["../../src/helpers/spoken-money.js"],"names":[],"mappings":"AAqDA;wHACwH;AACxH;;;;;;;;;;;EA+BC;AAED;;;;IAAkF"}
@@ -0,0 +1,143 @@
1
+ /** Deterministic cardinal grammar for explicitly named monetary phrases. No conversation or intent policy. */
2
+ const EN_SMALL = 'zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen'.split(' ');
3
+ const EN_TENS = { twenty: 20, thirty: 30, forty: 40, fifty: 50, sixty: 60, seventy: 70, eighty: 80, ninety: 90 };
4
+ const ES_SMALL = { cero: 0, uno: 1, un: 1, una: 1, dos: 2, tres: 3, cuatro: 4, cinco: 5, seis: 6, siete: 7, ocho: 8, nueve: 9, diez: 10, once: 11, doce: 12, trece: 13, catorce: 14, quince: 15, dieciseis: 16, diecisiete: 17, dieciocho: 18, diecinueve: 19, veinte: 20, veintiuno: 21, veintiun: 21, veintiuna: 21, veintidos: 22, veintitres: 23, veinticuatro: 24, veinticinco: 25, veintiseis: 26, veintisiete: 27, veintiocho: 28, veintinueve: 29 };
5
+ const ES_TENS = { treinta: 30, cuarenta: 40, cincuenta: 50, sesenta: 60, setenta: 70, ochenta: 80, noventa: 90 };
6
+ const ES_HUNDREDS = { doscientos: 200, doscientas: 200, trescientos: 300, trescientas: 300, cuatrocientos: 400, cuatrocientas: 400, quinientos: 500, quinientas: 500, seiscientos: 600, seiscientas: 600, setecientos: 700, setecientas: 700, ochocientos: 800, ochocientas: 800, novecientos: 900, novecientas: 900 };
7
+ const NUMBER_WORDS = new Set([...EN_SMALL, ...Object.keys(EN_TENS), ...Object.keys(ES_SMALL), ...Object.keys(ES_TENS), ...Object.keys(ES_HUNDREDS), 'hundred', 'thousand', 'million', 'cien', 'ciento', 'mil', 'millon', 'millones', 'and', 'y', 'a', 'an']);
8
+ const MAJOR = new Set(['dollar', 'dollars', 'dolar', 'dolares']);
9
+ const MINOR = new Set(['cent', 'cents', 'centavo', 'centavos']);
10
+ const plain = value => value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
11
+ const valid = value => Number.isSafeInteger(value) && value >= 0;
12
+ function englishSmall(words) {
13
+ if (words.length === 1 && EN_SMALL.includes(words[0]))
14
+ return EN_SMALL.indexOf(words[0]);
15
+ const tens = EN_TENS[words[0]], unit = EN_SMALL.indexOf(words[1]);
16
+ return tens && (words.length === 1 || words.length === 2 && unit > 0 && unit < 10) ? tens + (words.length === 2 ? unit : 0) : null;
17
+ }
18
+ function englishHundreds(words) {
19
+ const hundred = words.indexOf('hundred');
20
+ if (hundred < 0)
21
+ return englishSmall(words);
22
+ if (hundred !== 1 || words.lastIndexOf('hundred') !== 1)
23
+ return null;
24
+ const lead = ['a', 'an'].includes(words[0]) ? 1 : EN_SMALL.indexOf(words[0]);
25
+ if (lead < 1 || lead > 9)
26
+ return null;
27
+ let tail = words.slice(2);
28
+ if (tail[0] === 'and') {
29
+ if (tail.length === 1)
30
+ return null;
31
+ tail = tail.slice(1);
32
+ }
33
+ const rest = tail.length ? englishSmall(tail) : 0;
34
+ return rest === null || tail.length && rest === 0 ? null : lead * 100 + rest;
35
+ }
36
+ function spanishSmall(words) {
37
+ if (words.length === 1 && Object.hasOwn(ES_SMALL, words[0]))
38
+ return ES_SMALL[words[0]];
39
+ const tens = ES_TENS[words[0]], unit = ES_SMALL[words[2]];
40
+ return tens && (words.length === 1 || words.length === 3 && words[1] === 'y' && unit > 0 && unit < 10) ? tens + (words.length === 3 ? unit : 0) : null;
41
+ }
42
+ function spanishHundreds(words) {
43
+ if (words[0] === 'cien')
44
+ return words.length === 1 ? 100 : null;
45
+ if (words[0] === 'ciento') {
46
+ const rest = spanishSmall(words.slice(1));
47
+ return rest !== null && rest > 0 ? 100 + rest : null;
48
+ }
49
+ const lead = ES_HUNDREDS[words[0]];
50
+ if (!lead)
51
+ return spanishSmall(words);
52
+ const rest = words.length > 1 ? spanishSmall(words.slice(1)) : 0;
53
+ return rest === null || words.length > 1 && rest === 0 ? null : lead + rest;
54
+ }
55
+ function scaled(words, lang, level = 0) {
56
+ const lower = lang === 'en' ? englishHundreds : spanishHundreds;
57
+ if (level === 2)
58
+ return lower(words);
59
+ const scales = lang === 'en' ? [['million'], ['thousand']] : [['millon', 'millones'], ['mil']], scale = level === 0 ? 1000000 : 1000;
60
+ const positions = words.map((word, index) => scales[level].includes(word) ? index : -1).filter(index => index >= 0);
61
+ if (!positions.length)
62
+ return scaled(words, lang, level + 1);
63
+ if (positions.length !== 1)
64
+ return null;
65
+ const at = positions[0], left = words.slice(0, at), right = words.slice(at + 1), lead = left.length ? lang === 'en' && left.length === 1 && ['a', 'an'].includes(left[0]) ? 1 : lower(left) : lang === 'es' && scale === 1000 ? 1 : null;
66
+ if (lead === null || lead < 1 || lead > 999)
67
+ return null;
68
+ if (lang === 'es' && scale === 1000000 && ((lead === 1) !== (words[at] === 'millon')))
69
+ return null;
70
+ if (lang === 'en' && right[0] === 'and') {
71
+ if (right.length === 1)
72
+ return null;
73
+ right.shift();
74
+ }
75
+ const rest = right.length ? scaled(right, lang, level + 1) : 0;
76
+ return rest === null || right.length && rest === 0 ? null : lead * scale + rest;
77
+ }
78
+ function cardinal(words) {
79
+ if (words.length === 1 && /^\d+$/.test(words[0])) {
80
+ const value = Number(words[0]);
81
+ return valid(value) ? value : null;
82
+ }
83
+ if (!words.length || words.length > 24 || words.some(word => /\d/.test(word)))
84
+ return null;
85
+ const en = scaled(words, 'en'), es = scaled(words, 'es');
86
+ return en !== null ? en : es;
87
+ }
88
+ /** [{value,start,end}] covers the entire currency phrase, including a connected cents clause.
89
+ * Only readings containing actual cardinal words are returned; existing numeric parsing retains ownership of digits. */
90
+ export function scanSpokenMoney(answer) {
91
+ const raw = String(answer ?? ''), tokens = [...raw.matchAll(/\p{L}+|\d+(?:[.,]\d+)?|[^\s]/gu)].map(match => ({ word: plain(match[0]), start: match.index, end: match.index + match[0].length }));
92
+ const units = [], invalid = [];
93
+ for (let i = 0; i < tokens.length; i++) {
94
+ const unit = tokens[i].word;
95
+ if (!MAJOR.has(unit) && !MINOR.has(unit))
96
+ continue;
97
+ let start = i;
98
+ while (start > 0 && (NUMBER_WORDS.has(tokens[start - 1].word) || /^\d+$/.test(tokens[start - 1].word) || tokens[start - 1].word === '-' && start > 1 && NUMBER_WORDS.has(tokens[start - 2].word)))
99
+ start--;
100
+ while (start < i && ['and', 'y'].includes(tokens[start].word))
101
+ start++;
102
+ const numberTokens = tokens.slice(start, i), words = numberTokens.filter(token => token.word !== '-').map(token => token.word);
103
+ // The established numeric reader owns digit-only connector phrases, including Spanish 'un' before cents.
104
+ const digitOnly = words.some(word => /^\d+$/.test(word)) && words.every(word => /^\d+$/.test(word) || ['and', 'y', 'un'].includes(word));
105
+ const numericCents = MINOR.has(unit) && words.length === 2 && words[0] === 'un' && /^\d+$/.test(words[1]);
106
+ if (digitOnly && words.length > 1 && !numericCents)
107
+ continue;
108
+ const value = cardinal(numericCents ? words.slice(1) : words);
109
+ const spoken = !digitOnly && words.some(word => /[a-z]/.test(word));
110
+ if (value === null) {
111
+ if (spoken)
112
+ invalid.push({ start: tokens[start].start, end: tokens[i].end, reason: 'invalid_cardinal' });
113
+ continue;
114
+ }
115
+ // A detached sign or a negative phrase is not a positive payment amount.
116
+ if (start > 0 && ['-', 'minus', 'negative', 'menos', 'negativo'].includes(tokens[start - 1].word)) {
117
+ if (spoken)
118
+ invalid.push({ start: tokens[start - 1].start, end: tokens[i].end, reason: 'negative_amount' });
119
+ continue;
120
+ }
121
+ units.push({ value, unit: MAJOR.has(unit) ? 'major' : 'minor', spoken, start: tokens[start].start, end: tokens[i].end });
122
+ }
123
+ const readings = [];
124
+ for (let i = 0; i < units.length; i++) {
125
+ const first = units[i], next = units[i + 1];
126
+ if (first.unit === 'major' && next?.unit === 'minor' && /^(?:and|y|con)$/i.test(raw.slice(first.end, next.start).trim())) {
127
+ if (next.value > 99) {
128
+ if (first.spoken || next.spoken)
129
+ invalid.push({ start: first.start, end: next.end, reason: 'invalid_fractional_cents' });
130
+ i++;
131
+ continue;
132
+ }
133
+ if (first.spoken || next.spoken)
134
+ readings.push({ value: Math.round(first.value * 100 + next.value) / 100, start: first.start, end: next.end });
135
+ i++;
136
+ continue;
137
+ }
138
+ if (first.spoken)
139
+ readings.push({ value: first.unit === 'minor' ? first.value / 100 : first.value, start: first.start, end: first.end });
140
+ }
141
+ return { readings, invalid };
142
+ }
143
+ export function readSpokenMoney(answer) { return scanSpokenMoney(answer).readings; }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Storage contract (all writes are compare-and-swap, false means the comparison lost):
3
+ * readAttempt(attemptId), readScope(scopeId) -> record|null
4
+ * readPending(sessionId) -> {version:1,sessionId,sequence,revision,attempts:{[attemptId]:sequence}}|null
5
+ * claim({attempt,expectedGuard,superseded?,index,expectedIndex}) -> boolean; atomic attempt + guard + session index
6
+ * complete({attempt,expected,index,expectedIndex}) -> boolean; atomic matching attempt + owned guard + session index
7
+ * An unresolved claim has no lease expiry. A timeout cannot establish whether an external effect ran.
8
+ */
9
+ export function createActionJournal({ storage, namespace, now }: {
10
+ storage: any;
11
+ namespace: any;
12
+ now?: (() => number) | undefined;
13
+ }): Readonly<{
14
+ claim: (input: any) => Promise<{
15
+ status: string;
16
+ outcome: any;
17
+ receipt: {
18
+ key: any;
19
+ attemptId: any;
20
+ scopeId: any;
21
+ signature: any;
22
+ sequence: any;
23
+ };
24
+ } | {
25
+ status: string;
26
+ outcome: {
27
+ status: string;
28
+ reason: any;
29
+ };
30
+ receipt: {
31
+ key: any;
32
+ attemptId: any;
33
+ scopeId: any;
34
+ signature: any;
35
+ sequence: any;
36
+ } | null;
37
+ } | {
38
+ status: string;
39
+ receipt: {
40
+ key: any;
41
+ attemptId: any;
42
+ scopeId: any;
43
+ signature: any;
44
+ sequence: any;
45
+ };
46
+ }>;
47
+ complete: (input: any) => Promise<{
48
+ status: any;
49
+ outcome: any;
50
+ receipt: {
51
+ key: any;
52
+ attemptId: any;
53
+ scopeId: any;
54
+ signature: any;
55
+ sequence: any;
56
+ };
57
+ }>;
58
+ checkpoint: ({ sessionId, keys, receipts }: {
59
+ sessionId: any;
60
+ keys?: never[] | undefined;
61
+ receipts: any;
62
+ }) => Promise<number>;
63
+ pending: ({ sessionId }: {
64
+ sessionId: any;
65
+ }) => Promise<{
66
+ records: any;
67
+ checkpoint: {
68
+ key: any;
69
+ sequence: any;
70
+ status: any;
71
+ session: any;
72
+ } | null;
73
+ }>;
74
+ }>;
75
+ /** Hermetic adapter. Share this object across recreated ports to model process loss; never use it as production persistence. */
76
+ export function createMemoryActionStorage(): Readonly<{
77
+ readAttempt(id: any): Promise<any>;
78
+ readScope(id: any): Promise<any>;
79
+ readPending(id: any): Promise<any>;
80
+ claim({ attempt, expectedGuard, superseded, index, expectedIndex }: {
81
+ attempt: any;
82
+ expectedGuard: any;
83
+ superseded?: null | undefined;
84
+ index: any;
85
+ expectedIndex: any;
86
+ }): Promise<boolean>;
87
+ complete({ attempt, expected, index, expectedIndex }: {
88
+ attempt: any;
89
+ expected: any;
90
+ index: any;
91
+ expectedIndex: any;
92
+ }): Promise<boolean>;
93
+ }>;
94
+ export class ActionJournalError extends Error {
95
+ constructor(code: any);
96
+ code: any;
97
+ }
98
+ //# sourceMappingURL=action-journal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-journal.d.ts","sourceRoot":"","sources":["../../src/host/action-journal.js"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0HC;AAED,gIAAgI;AAChI;;;;;;;;;;;;;;;;;GAmBC;AA3JD;IACG,uBAAkG;IAAhB,UAAc;CAClG"}
@@ -0,0 +1,218 @@
1
+ /** Durable action policy. The injected storage adapter owns atomic I/O, never conversational decisions. */
2
+ const STATES = new Set(['attempted', 'unknown', 'completed', 'superseded']);
3
+ const text = (value, name, max = 4096) => { if (typeof value !== 'string' || !value.trim() || value.length > max)
4
+ throw new ActionJournalError('invalid_' + name); return value; };
5
+ const clone = value => JSON.parse(JSON.stringify(value));
6
+ const hash = async (value) => Array.from(new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))), b => b.toString(16).padStart(2, '0')).join('');
7
+ export class ActionJournalError extends Error {
8
+ constructor(code) { super(`Action journal: ${code}`); this.name = 'ActionJournalError'; this.code = code; }
9
+ }
10
+ /**
11
+ * Storage contract (all writes are compare-and-swap, false means the comparison lost):
12
+ * readAttempt(attemptId), readScope(scopeId) -> record|null
13
+ * readPending(sessionId) -> {version:1,sessionId,sequence,revision,attempts:{[attemptId]:sequence}}|null
14
+ * claim({attempt,expectedGuard,superseded?,index,expectedIndex}) -> boolean; atomic attempt + guard + session index
15
+ * complete({attempt,expected,index,expectedIndex}) -> boolean; atomic matching attempt + owned guard + session index
16
+ * An unresolved claim has no lease expiry. A timeout cannot establish whether an external effect ran.
17
+ */
18
+ export function createActionJournal({ storage, namespace, now = Date.now }) {
19
+ text(namespace, 'namespace');
20
+ for (const method of ['readAttempt', 'readScope', 'readPending', 'claim', 'complete'])
21
+ if (typeof storage?.[method] !== 'function')
22
+ throw new ActionJournalError('missing_storage_' + method);
23
+ const sessionHash = sessionId => hash(JSON.stringify([namespace, text(sessionId, 'session')]));
24
+ const tick = () => { const stamp = now(); if (!Number.isFinite(stamp) || stamp < 0)
25
+ throw new ActionJournalError('invalid_clock'); return stamp; };
26
+ const attemptId = async (sessionId, key) => hash(JSON.stringify([await sessionHash(sessionId), text(key, 'key')]));
27
+ const scopeId = async (sessionId, scope) => hash(JSON.stringify([await sessionHash(sessionId), text(scope, 'scope')]));
28
+ const receipt = row => ({ key: row.key, attemptId: row.attemptId, scopeId: row.scopeId, signature: row.signature, sequence: row.sequence });
29
+ const check = (row, { scope, attempt, signature } = {}) => {
30
+ if (!row)
31
+ return null;
32
+ if (row.version !== 1 || !STATES.has(row.status) || typeof row.revision !== 'string' || typeof row.key !== 'string' || typeof row.scopeId !== 'string' || typeof row.attemptId !== 'string' || typeof row.signature !== 'string' || !Number.isFinite(row.createdAt) || !Number.isFinite(row.updatedAt) || (['completed', 'unknown'].includes(row.status) && (!row.result || typeof row.result.status !== 'string')))
33
+ throw new ActionJournalError('invalid_stored_record');
34
+ if (scope && row.scopeId !== scope || attempt && row.attemptId !== attempt)
35
+ throw new ActionJournalError('stored_identity_mismatch');
36
+ if (signature && row.signature !== signature)
37
+ throw new ActionJournalError('key_reused_for_different_request');
38
+ return row;
39
+ };
40
+ const checkIndex = (row, session) => {
41
+ if (!row)
42
+ return null;
43
+ if (row.version !== 1 || row.sessionId !== session || !Number.isSafeInteger(row.sequence) || row.sequence < 1 || typeof row.revision !== 'string' || !row.attempts || typeof row.attempts !== 'object' || Array.isArray(row.attempts))
44
+ throw new ActionJournalError('invalid_session_index');
45
+ return row;
46
+ };
47
+ const nextIndex = (previous, session) => ({ version: 1, sessionId: session, sequence: Number(previous?.sequence || 0) + 1, revision: globalThis.crypto.randomUUID(), attempts: { ...previous?.attempts } });
48
+ async function identity({ sessionId, record, action }) {
49
+ if (!record || !action || typeof action.action !== 'string' || record.action !== action.action || !action.effect)
50
+ throw new ActionJournalError('invalid_action');
51
+ return { session: await sessionHash(sessionId), attempt: await attemptId(sessionId, record.key), scope: await scopeId(sessionId, record.scope), signature: await hash(text(record.signature, 'signature', 1048576)) };
52
+ }
53
+ const replay = row => ({ status: 'replay', outcome: clone(row.result), receipt: receipt(row) });
54
+ const hold = (row, reason) => ({ status: 'hold', outcome: { status: 'unknown', reason }, receipt: row ? receipt(row) : null });
55
+ function retryAllowed(guard, record, action) {
56
+ const retry = record.retryAuthorization, proof = retry?.authorization;
57
+ return ['attempted', 'unknown'].includes(guard.status) && action.effect === 'send' && action.retry?.unknown === 'explicit' && retry?.previousKey === guard.key && proof && typeof proof.form === 'string' && !!proof.form && typeof proof.input === 'string' && !!proof.input && Object.hasOwn(proof, 'value') && Number.isInteger(proof.revision) && proof.revision >= 0 && Number.isFinite(proof.at) && proof.at >= guard.createdAt;
58
+ }
59
+ async function claim(input) {
60
+ const { sessionId, record, action } = input, id = await identity(input);
61
+ for (let race = 0; race < 3; race++) {
62
+ const [prior, guard, indexBefore] = await Promise.all([storage.readAttempt(id.attempt), storage.readScope(id.scope), storage.readPending(id.session)]);
63
+ check(prior, { scope: id.scope, attempt: id.attempt, signature: id.signature });
64
+ check(guard, { scope: id.scope });
65
+ checkIndex(indexBefore, id.session);
66
+ if (prior)
67
+ return prior.status === 'completed' ? replay(prior) : hold(prior, prior.status === 'superseded' ? 'action_superseded' : 'action_result_unresolved');
68
+ if (guard?.status === 'completed' && !guard.checkpointed)
69
+ return guard.signature === id.signature ? replay(guard) : hold(guard, 'completed_action_not_checkpointed');
70
+ let superseded = null, consentFingerprint = '';
71
+ if (guard && ['attempted', 'unknown'].includes(guard.status)) {
72
+ if (!retryAllowed(guard, record, action))
73
+ return hold(guard, 'action_result_unresolved');
74
+ const proof = record.retryAuthorization.authorization;
75
+ consentFingerprint = await hash(JSON.stringify([proof.form, proof.input, proof.sequence ?? null, proof.revision, proof.at, proof.value]));
76
+ if (consentFingerprint === guard.consentFingerprint)
77
+ return hold(guard, 'retry_consent_already_used');
78
+ const previous = check(await storage.readAttempt(guard.attemptId), { scope: id.scope, attempt: guard.attemptId });
79
+ if (!previous || previous.revision !== guard.revision || previous.status !== guard.status)
80
+ return hold(guard, 'retry_history_changed');
81
+ superseded = { expected: previous, attempt: { ...previous, status: 'superseded', retryAuthorization: clone(record.retryAuthorization), revision: globalThis.crypto.randomUUID(), updatedAt: tick() } };
82
+ delete superseded.attempt.checkpoint;
83
+ }
84
+ else if (guard && guard.status !== 'completed')
85
+ return hold(guard, 'invalid_scope_state');
86
+ const stamp = tick();
87
+ const index = nextIndex(indexBefore, id.session);
88
+ if (superseded)
89
+ delete index.attempts[superseded.expected.attemptId];
90
+ index.attempts[id.attempt] = index.sequence;
91
+ const attempt = { version: 1, sessionId: id.session, sequence: index.sequence, attemptId: id.attempt, scopeId: id.scope, key: record.key, action: action.action, effect: action.effect, signature: id.signature,
92
+ revision: globalThis.crypto.randomUUID(), status: 'attempted', checkpointed: false, createdAt: stamp, updatedAt: stamp,
93
+ ...(input.checkpoint !== undefined ? { checkpoint: clone(input.checkpoint) } : {}),
94
+ ...(superseded ? { retryAuthorization: clone(record.retryAuthorization), consentFingerprint } : {}) };
95
+ if (await storage.claim({ attempt, expectedGuard: guard || null, superseded, index, expectedIndex: indexBefore || null }))
96
+ return { status: 'execute', receipt: receipt(attempt) };
97
+ }
98
+ return hold(null, 'claim_contended');
99
+ }
100
+ async function complete(input) {
101
+ const { record, action, outcome } = input, id = await identity(input);
102
+ const candidate = outcome && typeof outcome === 'object' && Object.hasOwn(action.outcomes || {}, outcome.status) ? clone(outcome) : { status: 'unknown', reason: 'invalid_action_result' };
103
+ for (let race = 0; race < 3; race++) {
104
+ const [stored, indexBefore] = await Promise.all([storage.readAttempt(id.attempt), storage.readPending(id.session)]);
105
+ const prior = check(stored, { scope: id.scope, attempt: id.attempt, signature: id.signature });
106
+ checkIndex(indexBefore, id.session);
107
+ if (!prior)
108
+ throw new ActionJournalError('completion_without_claim');
109
+ if (prior.status === 'completed')
110
+ return { status: 'completed', outcome: clone(prior.result), receipt: receipt(prior) };
111
+ if (prior.status === 'superseded')
112
+ throw new ActionJournalError('completion_after_supersession');
113
+ const guard = check(await storage.readScope(id.scope), { scope: id.scope });
114
+ if (!guard || guard.key !== record.key || guard.revision !== prior.revision)
115
+ throw new ActionJournalError('completion_ownership_lost');
116
+ if (!indexBefore || !Object.hasOwn(indexBefore.attempts, id.attempt) && !prior.checkpointed)
117
+ throw new ActionJournalError('missing_pending_action');
118
+ const index = nextIndex(indexBefore, id.session);
119
+ index.attempts[id.attempt] = index.sequence;
120
+ const attempt = { ...prior, checkpointed: false, sequence: index.sequence, status: candidate.status === 'unknown' ? 'unknown' : 'completed', result: candidate, revision: globalThis.crypto.randomUUID(), updatedAt: tick(), ...(input.checkpoint !== undefined ? { checkpoint: clone(input.checkpoint) } : {}) };
121
+ if (await storage.complete({ attempt, expected: prior, index, expectedIndex: indexBefore }))
122
+ return { status: attempt.status, outcome: clone(candidate), receipt: receipt(attempt) };
123
+ }
124
+ throw new ActionJournalError('completion_contended');
125
+ }
126
+ async function checkpoint({ sessionId, keys = [], receipts }) {
127
+ let count = 0;
128
+ const versioned = receipts !== undefined;
129
+ if (versioned && (!Array.isArray(receipts) || receipts.some(item => !item || typeof item.key !== 'string' || !Number.isSafeInteger(item.sequence) || item.sequence < 1)))
130
+ throw new ActionJournalError('invalid_checkpoint_receipt');
131
+ const expected = new Map(versioned ? receipts.map(item => [item.key, item.sequence]) : keys.map(key => [key, null]));
132
+ const session = await sessionHash(sessionId);
133
+ for (const [key, sequence] of expected) {
134
+ const id = await attemptId(sessionId, key);
135
+ for (let race = 0; race < 3; race++) {
136
+ const [stored, indexBefore] = await Promise.all([storage.readAttempt(id), storage.readPending(session)]);
137
+ const prior = check(stored, { attempt: id });
138
+ checkIndex(indexBefore, session);
139
+ if (!prior || versioned && prior.sequence !== sequence)
140
+ break;
141
+ if (prior.status === 'superseded') {
142
+ count++;
143
+ break;
144
+ }
145
+ if (prior.checkpointed) {
146
+ count++;
147
+ break;
148
+ }
149
+ const guard = check(await storage.readScope(prior.scopeId), { scope: prior.scopeId });
150
+ if (!guard || guard.key !== prior.key || guard.revision !== prior.revision)
151
+ break;
152
+ if (!indexBefore || !Object.hasOwn(indexBefore.attempts, id))
153
+ throw new ActionJournalError('missing_pending_action');
154
+ const index = nextIndex(indexBefore, session);
155
+ delete index.attempts[id];
156
+ const attempt = { ...prior, checkpointed: true, revision: globalThis.crypto.randomUUID(), updatedAt: tick() };
157
+ delete attempt.checkpoint;
158
+ if (await storage.complete({ attempt, expected: prior, index, expectedIndex: indexBefore })) {
159
+ count++;
160
+ break;
161
+ }
162
+ if (race === 2)
163
+ throw new ActionJournalError('checkpoint_contended');
164
+ }
165
+ }
166
+ return count;
167
+ }
168
+ async function pending({ sessionId }) {
169
+ const session = await sessionHash(sessionId);
170
+ for (let race = 0; race < 3; race++) {
171
+ const index = checkIndex(await storage.readPending(session), session);
172
+ if (!index)
173
+ return { records: [], checkpoint: null };
174
+ const records = await Promise.all(Object.keys(index.attempts).map(async (id) => check(await storage.readAttempt(id), { attempt: id })));
175
+ const after = checkIndex(await storage.readPending(session), session);
176
+ if (after?.revision !== index.revision)
177
+ continue;
178
+ if (records.some(row => !row || row.sessionId !== session || row.sequence !== index.attempts[row.attemptId]))
179
+ throw new ActionJournalError('pending_index_mismatch');
180
+ records.sort((a, b) => a.sequence - b.sequence || a.updatedAt - b.updatedAt || a.key.localeCompare(b.key));
181
+ const latest = records.filter(row => row.checkpoint !== undefined).at(-1);
182
+ return { records: clone(records), checkpoint: latest ? { key: latest.key, sequence: latest.sequence, status: latest.status, session: clone(latest.checkpoint) } : null };
183
+ }
184
+ throw new ActionJournalError('pending_read_contended');
185
+ }
186
+ return Object.freeze({ claim, complete, checkpoint, pending });
187
+ }
188
+ /** Hermetic adapter. Share this object across recreated ports to model process loss; never use it as production persistence. */
189
+ export function createMemoryActionStorage() {
190
+ const attempts = new Map(), scopes = new Map(), indexes = new Map();
191
+ const read = (rows, id) => rows.has(id) ? clone(rows.get(id)) : null;
192
+ const matches = (actual, expected) => expected ? actual?.revision === expected.revision : !actual;
193
+ return Object.freeze({
194
+ async readAttempt(id) { return read(attempts, id); }, async readScope(id) { return read(scopes, id); }, async readPending(id) { return read(indexes, id); },
195
+ async claim({ attempt, expectedGuard, superseded = null, index, expectedIndex }) {
196
+ if (attempts.has(attempt.attemptId) || !matches(scopes.get(attempt.scopeId), expectedGuard) || !matches(indexes.get(index.sessionId), expectedIndex) || superseded && !matches(attempts.get(superseded.expected.attemptId), superseded.expected))
197
+ return false;
198
+ const guard = clone(attempt);
199
+ delete guard.checkpoint;
200
+ attempts.set(attempt.attemptId, clone(attempt));
201
+ scopes.set(attempt.scopeId, guard);
202
+ indexes.set(index.sessionId, clone(index));
203
+ if (superseded)
204
+ attempts.set(superseded.attempt.attemptId, clone(superseded.attempt));
205
+ return true;
206
+ },
207
+ async complete({ attempt, expected, index, expectedIndex }) {
208
+ if (!matches(attempts.get(attempt.attemptId), expected) || !matches(scopes.get(attempt.scopeId), expected) || !matches(indexes.get(index.sessionId), expectedIndex))
209
+ return false;
210
+ const guard = clone(attempt);
211
+ delete guard.checkpoint;
212
+ attempts.set(attempt.attemptId, clone(attempt));
213
+ scopes.set(attempt.scopeId, guard);
214
+ indexes.set(index.sessionId, clone(index));
215
+ return true;
216
+ },
217
+ });
218
+ }
@@ -0,0 +1,12 @@
1
+ /** Save-boundary helpers. A receipt acknowledges only the exact action version consumed by the saved session. */
2
+ export function actionSessionSnapshot(session: any): {
3
+ keys: any[];
4
+ receipts: {
5
+ key: any;
6
+ sequence: any;
7
+ }[];
8
+ session: any;
9
+ };
10
+ /** String keys resolve through the consumed-revision map; explicit receipt objects freeze the host's save boundary. */
11
+ export function checkpointActionSession(journal: any, session: any, keys?: any[]): Promise<any>;
12
+ //# sourceMappingURL=action-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-session.d.ts","sourceRoot":"","sources":["../../src/host/action-session.js"],"names":[],"mappings":"AAAA,iHAAiH;AACjH;;;;;;;EAKC;AACD,uHAAuH;AACvH,gGAeC"}
@@ -0,0 +1,29 @@
1
+ /** Save-boundary helpers. A receipt acknowledges only the exact action version consumed by the saved session. */
2
+ export function actionSessionSnapshot(session) {
3
+ const keys = [...new Set(session?.cargo?.actionJournalReceipts || [])];
4
+ const revisions = session?.cargo?.actionJournalReceiptRevisions || {};
5
+ const receipts = keys.filter(key => Number.isSafeInteger(revisions[key]) && revisions[key] > 0).map(key => ({ key, sequence: revisions[key] }));
6
+ return { keys, receipts, session };
7
+ }
8
+ /** String keys resolve through the consumed-revision map; explicit receipt objects freeze the host's save boundary. */
9
+ export async function checkpointActionSession(journal, session, keys = []) {
10
+ if (!keys.length)
11
+ return 0;
12
+ const revisions = session?.cargo?.actionJournalReceiptRevisions || {};
13
+ const selected = new Map(keys.map(item => typeof item === 'string' ? [item, revisions[item]] : [item.key, item.sequence]));
14
+ const receipts = [...selected].filter(([, sequence]) => Number.isSafeInteger(sequence) && sequence > 0).map(([key, sequence]) => ({ key, sequence }));
15
+ if (!receipts.length)
16
+ return 0;
17
+ if (!journal)
18
+ throw new Error('Cannot checkpoint action receipts without a durable journal');
19
+ const count = await journal.checkpoint({ sessionId: session.sessionId, receipts });
20
+ if (count === selected.size && receipts.length === selected.size) {
21
+ const acknowledged = new Map(receipts.map(item => [item.key, item.sequence]));
22
+ const current = session.cargo.actionJournalReceiptRevisions || {};
23
+ session.cargo.actionJournalReceipts = (session.cargo.actionJournalReceipts || []).filter(key => !acknowledged.has(key) || current[key] !== acknowledged.get(key));
24
+ for (const { key, sequence } of receipts)
25
+ if (current[key] === sequence)
26
+ delete current[key];
27
+ }
28
+ return count;
29
+ }
@@ -0,0 +1,14 @@
1
+ /** Copy/normalize declaration data so subsequent caller mutations cannot change admission. */
2
+ export function normalizeHostRequirements(requirements: any, flowId: any): readonly any[];
3
+ /** Durable is the native default; session-only must be selected explicitly by the host. */
4
+ export function actionPersistenceMode(mode?: string): "durable" | "session-only";
5
+ /** Boolean requirements remain strict. guardedActions additionally accepts the explicit session-only policy. */
6
+ export function assertFlowHostCapabilities(flows: any, capabilities?: {}): boolean;
7
+ /** Declarative flow admission. No engine imports, I/O, or implicit host capability activation. */
8
+ export class JSFEHostCapabilityError extends Error {
9
+ constructor(flowId: any, capability: any, reason: any);
10
+ code: string;
11
+ flowId: any;
12
+ capability: any;
13
+ }
14
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/host/capabilities.js"],"names":[],"mappings":"AAWA,8FAA8F;AAC9F,0FASC;AAED,2FAA2F;AAC3F,iFAKC;AAED,gHAAgH;AAChH,mFA0BC;AA1DD,kGAAkG;AAClG;IACG,uDAMC;IAHE,aAA2C;IAC3C,YAA0B;IAC1B,gBAAkC;CAEvC"}
@@ -0,0 +1,60 @@
1
+ /** Declarative flow admission. No engine imports, I/O, or implicit host capability activation. */
2
+ export class JSFEHostCapabilityError extends Error {
3
+ constructor(flowId, capability, reason) {
4
+ super(`Flow ${flowId || '<unknown>'} host capability ${capability || '<declaration>'}: ${reason}`);
5
+ this.name = 'JSFEHostCapabilityError';
6
+ this.code = 'JSFE_HOST_CAPABILITY_REQUIRED';
7
+ this.flowId = flowId || '';
8
+ this.capability = capability || '';
9
+ }
10
+ }
11
+ /** Copy/normalize declaration data so subsequent caller mutations cannot change admission. */
12
+ export function normalizeHostRequirements(requirements, flowId) {
13
+ if (requirements === undefined)
14
+ return Object.freeze([]);
15
+ if (!Array.isArray(requirements))
16
+ throw new JSFEHostCapabilityError(flowId, '', 'contract.requires must be an array of nonempty capability names');
17
+ for (const name of requirements) {
18
+ if (typeof name !== 'string' || !name || name.trim() !== name) {
19
+ throw new JSFEHostCapabilityError(flowId, '', 'contract.requires must contain nonempty, unpadded strings');
20
+ }
21
+ }
22
+ return Object.freeze([...new Set(requirements)]);
23
+ }
24
+ /** Durable is the native default; session-only must be selected explicitly by the host. */
25
+ export function actionPersistenceMode(mode = 'durable') {
26
+ if (mode !== 'durable' && mode !== 'session-only') {
27
+ throw new JSFEHostCapabilityError('', 'actionPersistence', "must be 'durable' or 'session-only'");
28
+ }
29
+ return mode;
30
+ }
31
+ /** Boolean requirements remain strict. guardedActions additionally accepts the explicit session-only policy. */
32
+ export function assertFlowHostCapabilities(flows, capabilities = {}) {
33
+ if (!Array.isArray(flows))
34
+ throw new JSFEHostCapabilityError('', '', 'flows must be an array');
35
+ const own = key => Object.prototype.hasOwnProperty.call(capabilities || {}, key);
36
+ const mode = actionPersistenceMode(own('actionPersistence') ? capabilities.actionPersistence : undefined);
37
+ if (mode === 'session-only' && capabilities?.durableActions === true) {
38
+ throw new JSFEHostCapabilityError('', 'actionPersistence', 'session-only conflicts with durableActions:true');
39
+ }
40
+ for (const flow of flows) {
41
+ const requirements = normalizeHostRequirements(flow?.variables?.contract?.value?.requires, flow?.id);
42
+ for (const capability of requirements) {
43
+ if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities)) {
44
+ throw new JSFEHostCapabilityError(flow?.id, capability, 'explicit host capabilities must be an object');
45
+ }
46
+ if (capability === 'guardedActions') {
47
+ if (own('durableActions') && capabilities.durableActions === true || own('actionPersistence') && mode === 'session-only')
48
+ continue;
49
+ throw new JSFEHostCapabilityError(flow?.id, capability, 'requires explicit boolean true for durableActions or explicit actionPersistence:session-only');
50
+ }
51
+ if (!Object.prototype.hasOwnProperty.call(capabilities, capability)) {
52
+ throw new JSFEHostCapabilityError(flow?.id, capability, 'requires explicit boolean true; capability is missing');
53
+ }
54
+ if (capabilities[capability] !== true) {
55
+ throw new JSFEHostCapabilityError(flow?.id, capability, `requires explicit boolean true; received ${typeof capabilities[capability]}`);
56
+ }
57
+ }
58
+ }
59
+ return true;
60
+ }
@@ -0,0 +1,6 @@
1
+ /** Stable codec errors are safe to inspect without loading a Node-only storage implementation. */
2
+ export class CheckpointCodecError extends Error {
3
+ constructor(code: any);
4
+ code: any;
5
+ }
6
+ //# sourceMappingURL=checkpoint-codec-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-codec-error.d.ts","sourceRoot":"","sources":["../../src/host/checkpoint-codec-error.js"],"names":[],"mappings":"AAAA,kGAAkG;AAClG;IACC,uBAAyG;IAAhB,UAAc;CACvG"}
@@ -0,0 +1,4 @@
1
+ /** Stable codec errors are safe to inspect without loading a Node-only storage implementation. */
2
+ export class CheckpointCodecError extends Error {
3
+ constructor(code) { super('JSFE checkpoint codec: ' + code); this.name = 'CheckpointCodecError'; this.code = code; }
4
+ }