@lmzhen/dsh-evolution-merge 0.31.0 → 0.32.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/README.md +6 -0
- package/lib/index.js +3 -2
- package/lib/types/adapters/library.js +4 -2
- package/lib/types/index.d.ts +3 -1
- package/package.json +4 -4
- package/lib/types/kernel/adapter-v4.d.ts +0 -12
- package/lib/types/kernel/adapter-v4.js +0 -54
- package/lib/types/kernel/failure.d.ts +0 -8
- package/lib/types/kernel/failure.js +0 -33
- package/lib/types/kernel/index.d.ts +0 -9
- package/lib/types/kernel/index.js +0 -9
- package/lib/types/kernel/invariants.d.ts +0 -4
- package/lib/types/kernel/invariants.js +0 -47
- package/lib/types/kernel/ledger.d.ts +0 -46
- package/lib/types/kernel/ledger.js +0 -62
- package/lib/types/kernel/machine.d.ts +0 -13
- package/lib/types/kernel/machine.js +0 -108
- package/lib/types/kernel/model.d.ts +0 -154
- package/lib/types/kernel/model.js +0 -5
- package/lib/types/kernel/projection.d.ts +0 -28
- package/lib/types/kernel/projection.js +0 -44
- package/lib/types/kernel/recovery.d.ts +0 -31
- package/lib/types/kernel/recovery.js +0 -19
- package/lib/types/kernel/unit.d.ts +0 -43
- package/lib/types/kernel/unit.js +0 -63
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@ Skill auto-merge host surface: candidate screening through the vendored pipeline
|
|
|
4
4
|
two-round child authoring, per-pair staging, pending approval, commit receipts and
|
|
5
5
|
unit undo.
|
|
6
6
|
|
|
7
|
+
> **Screening pipeline freeze:** the merge-engine screening pipeline ①–⑤ is specially
|
|
8
|
+
> tuned. Its substantive flow (stage order, inputs/outputs, models, weights, thresholds,
|
|
9
|
+
> filters, scoring, decisions, and any selection policy affecting candidate set/order)
|
|
10
|
+
> must not change. Only packaging, loading, adapters, error boundaries, diagnostics and
|
|
11
|
+
> tests may change, with parity evidence proving identical screening output.
|
|
12
|
+
|
|
7
13
|
## Surface
|
|
8
14
|
|
|
9
15
|
- `/evolution merge <subcommand>` — canonical command.
|
package/lib/index.js
CHANGED
|
@@ -9046,13 +9046,14 @@ function skillLibraryPort(skills) {
|
|
|
9046
9046
|
return {
|
|
9047
9047
|
readSkill: (name) => skills.read(name),
|
|
9048
9048
|
isArchived: async (name) => {
|
|
9049
|
-
if (typeof archivedNames !== "function")
|
|
9049
|
+
if (typeof archivedNames !== "function") throw new Error("SkillLibrary.archivedNames is unavailable; refusing to treat an unknown archive state as unarchived");
|
|
9050
9050
|
const names = await archivedNames.call(skills);
|
|
9051
9051
|
return names === null ? true : names.includes(name);
|
|
9052
9052
|
},
|
|
9053
9053
|
isNameOccupied: async (name) => {
|
|
9054
9054
|
const probe = skills.skillDirExists;
|
|
9055
|
-
|
|
9055
|
+
if (typeof probe !== "function") throw new Error("SkillLibrary.skillDirExists is unavailable; refusing to treat an unknown name as free");
|
|
9056
|
+
return await probe.call(skills, name);
|
|
9056
9057
|
},
|
|
9057
9058
|
async readSupportFiles(name) {
|
|
9058
9059
|
if (typeof skills.listSupportFiles !== "function") return [];
|
|
@@ -4,13 +4,15 @@ export function skillLibraryPort(skills) {
|
|
|
4
4
|
readSkill: name => skills.read(name),
|
|
5
5
|
isArchived: async (name) => {
|
|
6
6
|
if (typeof archivedNames !== 'function')
|
|
7
|
-
|
|
7
|
+
throw new Error('SkillLibrary.archivedNames is unavailable; refusing to treat an unknown archive state as unarchived');
|
|
8
8
|
const names = await archivedNames.call(skills);
|
|
9
9
|
return names === null ? true : names.includes(name);
|
|
10
10
|
},
|
|
11
11
|
isNameOccupied: async (name) => {
|
|
12
12
|
const probe = skills.skillDirExists;
|
|
13
|
-
|
|
13
|
+
if (typeof probe !== 'function')
|
|
14
|
+
throw new Error('SkillLibrary.skillDirExists is unavailable; refusing to treat an unknown name as free');
|
|
15
|
+
return await probe.call(skills, name);
|
|
14
16
|
},
|
|
15
17
|
async readSupportFiles(name) {
|
|
16
18
|
if (typeof skills.listSupportFiles !== 'function')
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Context as CordisContext } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { Context as HostContext } from '@deepseek-ai/cordis/src/context.ts';
|
|
4
|
+
type Context = Omit<CordisContext, 'logger'> & HostContext;
|
|
3
5
|
/** E3 knobs are volatile (user layer); the E2 pair timeout is deployment-only. */
|
|
4
6
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
5
7
|
mergeApproval: z<boolean, boolean, "volatile-defined">;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-merge",
|
|
3
3
|
"description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.32.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@deepseek-ai/schemastery": "^3.18.5",
|
|
35
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
36
|
-
"@lmzhen/dsh-evolution-commands": "^0.
|
|
37
|
-
"@lmzhen/dsh-evolution-merge-engine": "0.
|
|
35
|
+
"@lmzhen/dsh-evolution-core": "^0.32.0",
|
|
36
|
+
"@lmzhen/dsh-evolution-commands": "^0.32.0",
|
|
37
|
+
"@lmzhen/dsh-evolution-merge-engine": "0.32.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "^4.0.5"
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { V4Row } from '../domain/row-v4.ts';
|
|
2
|
-
import type { PairSnapshot } from './model.ts';
|
|
3
|
-
export interface V4AdapterResult {
|
|
4
|
-
readonly snapshot: PairSnapshot;
|
|
5
|
-
readonly warnings: readonly string[];
|
|
6
|
-
}
|
|
7
|
-
export declare function fromV4Row(row: V4Row): V4AdapterResult;
|
|
8
|
-
export declare function fromV4Rows(rows: readonly V4Row[]): {
|
|
9
|
-
readonly snapshots: readonly PairSnapshot[];
|
|
10
|
-
readonly warnings: readonly string[];
|
|
11
|
-
};
|
|
12
|
-
//# sourceMappingURL=adapter-v4.d.ts.map
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
function decisionOf(row) {
|
|
2
|
-
return { target: row.target, sources: row.decision.sources, ...(row.decision.rationale === undefined ? {} : { rationale: row.decision.rationale }) };
|
|
3
|
-
}
|
|
4
|
-
function failureOfRow(failure) {
|
|
5
|
-
const known = new Set(['source-gone', 'source-changed', 'target-taken', 'validation-failed', 'commit-failed', 'schema-invalid', 'timeout', 'capability-missing', 'runtime']);
|
|
6
|
-
const code = known.has(failure.code) ? failure.code : 'runtime';
|
|
7
|
-
return { code, stage: failure.stage === 'screening' ? 'screen' : failure.stage === 'decide' ? 'decide' : failure.stage === 'author' ? 'author' : failure.stage === 'validate' ? 'validate' : 'commit', retryable: failure.retryable, reportOnly: true, userMessageKey: 'failure.' + code, detail: failure.message };
|
|
8
|
-
}
|
|
9
|
-
function closedReasonOf(row) {
|
|
10
|
-
if (row.reason.includes('merge-over-limit') || row.reason.includes('over-limit'))
|
|
11
|
-
return 'over-limit';
|
|
12
|
-
return row.origin === 'human' ? 'reject' : 'keep';
|
|
13
|
-
}
|
|
14
|
-
export function fromV4Row(row) {
|
|
15
|
-
const warnings = [];
|
|
16
|
-
const base = { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, version: row.version, createdAt: row.createdAt, updatedAt: row.updatedAt, origin: row.origin, attempts: row.attempts, ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
17
|
-
if (row.failure !== undefined) {
|
|
18
|
-
warnings.push('row carries failure; kernel state=failed (current row state=' + row.state + ')');
|
|
19
|
-
return { snapshot: { ...base, state: 'failed', failure: failureOfRow(row.failure) }, warnings };
|
|
20
|
-
}
|
|
21
|
-
if (row.state === 'verify') {
|
|
22
|
-
return { snapshot: { ...base, state: row.verifySubState === 'screened' ? 'deciding' : 'candidate' }, warnings };
|
|
23
|
-
}
|
|
24
|
-
if (row.state === 'pending') {
|
|
25
|
-
if (row.expired !== undefined) {
|
|
26
|
-
warnings.push('pending expired without a timestamp; kernel state=expired');
|
|
27
|
-
return { snapshot: { ...base, state: 'expired', target: row.target, decision: decisionOf(row), approval: 'on' }, warnings };
|
|
28
|
-
}
|
|
29
|
-
return { snapshot: { ...base, state: 'pending', target: row.target, decision: decisionOf(row), approval: 'on' }, warnings };
|
|
30
|
-
}
|
|
31
|
-
if (row.state === 'approved')
|
|
32
|
-
return { snapshot: { ...base, state: 'approved', target: row.target, decision: decisionOf(row), approval: 'off' }, warnings };
|
|
33
|
-
if (row.state === 'fuse') {
|
|
34
|
-
warnings.push('current fuse row has no stagingRef; kernel state=authoring');
|
|
35
|
-
return { snapshot: { ...base, state: 'authoring', target: row.target, decision: decisionOf(row), approval: 'off' }, warnings };
|
|
36
|
-
}
|
|
37
|
-
if (row.expiredAt !== undefined) {
|
|
38
|
-
warnings.push('skipped-expired row maps to closed+expiresAt, not a failure');
|
|
39
|
-
return { snapshot: { ...base, state: 'closed', closedReason: closedReasonOf(row), expiresAt: row.expiredAt }, warnings };
|
|
40
|
-
}
|
|
41
|
-
return { snapshot: { ...base, state: 'closed', closedReason: closedReasonOf(row) }, warnings };
|
|
42
|
-
}
|
|
43
|
-
export function fromV4Rows(rows) {
|
|
44
|
-
const snapshots = [];
|
|
45
|
-
const warnings = [];
|
|
46
|
-
for (const row of rows) {
|
|
47
|
-
const result = fromV4Row(row);
|
|
48
|
-
snapshots.push(result.snapshot);
|
|
49
|
-
for (const warning of result.warnings)
|
|
50
|
-
warnings.push(row.id + ': ' + warning);
|
|
51
|
-
}
|
|
52
|
-
return { snapshots, warnings };
|
|
53
|
-
}
|
|
54
|
-
//# sourceMappingURL=adapter-v4.js.map
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/** Pure failure taxonomy: the ONE place a code becomes a route. */
|
|
2
|
-
import type { ClosedReason, Failure, FailureCode } from './model.ts';
|
|
3
|
-
export declare function failureOf(code: FailureCode, detail?: string): Failure;
|
|
4
|
-
/** Only policy closures are closed; failures never map here. */
|
|
5
|
-
export declare function closedReasonFor(code: string): ClosedReason | undefined;
|
|
6
|
-
export declare function isFailureRoute(reportOnly: boolean): boolean;
|
|
7
|
-
export declare const FAILURE_CODES: readonly FailureCode[];
|
|
8
|
-
//# sourceMappingURL=failure.d.ts.map
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
const TABLE = {
|
|
2
|
-
'bad-pair': { stage: 'command', retryable: true, reportOnly: false, userMessageKey: 'failure.bad-pair' },
|
|
3
|
-
'bad-target': { stage: 'command', retryable: true, reportOnly: false, userMessageKey: 'failure.bad-target' },
|
|
4
|
-
'bad-flag': { stage: 'command', retryable: true, reportOnly: false, userMessageKey: 'failure.bad-flag' },
|
|
5
|
-
'source-gone': { stage: 'decide', retryable: true, reportOnly: true, userMessageKey: 'failure.source-gone' },
|
|
6
|
-
'source-changed': { stage: 'commit', retryable: true, reportOnly: true, userMessageKey: 'failure.source-changed' },
|
|
7
|
-
'target-taken': { stage: 'commit', retryable: false, reportOnly: true, userMessageKey: 'failure.target-taken' },
|
|
8
|
-
'validation-failed': { stage: 'validate', retryable: true, reportOnly: true, userMessageKey: 'failure.validation' },
|
|
9
|
-
'commit-failed': { stage: 'commit', retryable: false, reportOnly: true, userMessageKey: 'failure.commit' },
|
|
10
|
-
'schema-invalid': { stage: 'schema', retryable: false, reportOnly: true, userMessageKey: 'failure.schema' },
|
|
11
|
-
timeout: { stage: 'run', retryable: true, reportOnly: true, userMessageKey: 'failure.timeout' },
|
|
12
|
-
'capability-missing': { stage: 'surface', retryable: false, reportOnly: false, userMessageKey: 'failure.capability' },
|
|
13
|
-
runtime: { stage: 'run', retryable: true, reportOnly: true, userMessageKey: 'failure.runtime' },
|
|
14
|
-
};
|
|
15
|
-
export function failureOf(code, detail) {
|
|
16
|
-
const spec = TABLE[code];
|
|
17
|
-
return { code, stage: spec.stage, retryable: spec.retryable, reportOnly: spec.reportOnly, userMessageKey: spec.userMessageKey, ...(detail === undefined ? {} : { detail }) };
|
|
18
|
-
}
|
|
19
|
-
/** Only policy closures are closed; failures never map here. */
|
|
20
|
-
export function closedReasonFor(code) {
|
|
21
|
-
if (code === 'merge-over-limit')
|
|
22
|
-
return 'over-limit';
|
|
23
|
-
if (code === 'keep')
|
|
24
|
-
return 'keep';
|
|
25
|
-
if (code === 'reject')
|
|
26
|
-
return 'reject';
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
export function isFailureRoute(reportOnly) {
|
|
30
|
-
return reportOnly;
|
|
31
|
-
}
|
|
32
|
-
export const FAILURE_CODES = Object.keys(TABLE);
|
|
33
|
-
//# sourceMappingURL=failure.js.map
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export * from './model.ts';
|
|
2
|
-
export * from './failure.ts';
|
|
3
|
-
export * from './machine.ts';
|
|
4
|
-
export * from './unit.ts';
|
|
5
|
-
export * from './ledger.ts';
|
|
6
|
-
export * from './projection.ts';
|
|
7
|
-
export * from './recovery.ts';
|
|
8
|
-
export * from './invariants.ts';
|
|
9
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export * from "./model.js";
|
|
2
|
-
export * from "./failure.js";
|
|
3
|
-
export * from "./machine.js";
|
|
4
|
-
export * from "./unit.js";
|
|
5
|
-
export * from "./ledger.js";
|
|
6
|
-
export * from "./projection.js";
|
|
7
|
-
export * from "./recovery.js";
|
|
8
|
-
export * from "./invariants.js";
|
|
9
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/** Pure invariant checks over one pair snapshot. */
|
|
2
|
-
import { CLOSED_REASONS } from "./model.js";
|
|
3
|
-
export function checkPair(snapshot) {
|
|
4
|
-
const problems = [];
|
|
5
|
-
if (snapshot.id.length === 0)
|
|
6
|
-
problems.push('id is empty');
|
|
7
|
-
if (snapshot.pairKey.length === 0)
|
|
8
|
-
problems.push('pairKey is empty');
|
|
9
|
-
if (snapshot.a.length === 0 || snapshot.b.length === 0)
|
|
10
|
-
problems.push('a/b is empty');
|
|
11
|
-
if (!Number.isInteger(snapshot.version) || snapshot.version < 0)
|
|
12
|
-
problems.push('version must be a non-negative integer');
|
|
13
|
-
if (snapshot.attempts < 0)
|
|
14
|
-
problems.push('attempts must be non-negative');
|
|
15
|
-
if (snapshot.state === 'closed') {
|
|
16
|
-
if (snapshot.closedReason === undefined || !CLOSED_REASONS.includes(snapshot.closedReason))
|
|
17
|
-
problems.push('closed requires closedReason in ' + CLOSED_REASONS.join('/'));
|
|
18
|
-
if (snapshot.failure !== undefined)
|
|
19
|
-
problems.push('closed must not carry a failure');
|
|
20
|
-
}
|
|
21
|
-
if (snapshot.state === 'failed') {
|
|
22
|
-
if (snapshot.failure === undefined)
|
|
23
|
-
problems.push('failed requires a failure');
|
|
24
|
-
if (snapshot.closedReason !== undefined)
|
|
25
|
-
problems.push('failed must not carry closedReason');
|
|
26
|
-
}
|
|
27
|
-
if (snapshot.state === 'committed') {
|
|
28
|
-
if (snapshot.receipt === undefined)
|
|
29
|
-
problems.push('committed requires a receipt');
|
|
30
|
-
if (snapshot.failure !== undefined)
|
|
31
|
-
problems.push('committed must not carry a failure');
|
|
32
|
-
if (snapshot.unitId === undefined)
|
|
33
|
-
problems.push('committed requires unitId');
|
|
34
|
-
}
|
|
35
|
-
if (snapshot.state === 'committing' && snapshot.unitId === undefined)
|
|
36
|
-
problems.push('committing requires unitId');
|
|
37
|
-
if (snapshot.state === 'staged' && (snapshot.stageRef === undefined || snapshot.stageRef.length === 0))
|
|
38
|
-
problems.push('staged requires stageRef');
|
|
39
|
-
if ((snapshot.state === 'pending' || snapshot.state === 'approved') && snapshot.decision === undefined)
|
|
40
|
-
problems.push(snapshot.state + ' requires a decision');
|
|
41
|
-
if ((snapshot.state === 'pending' || snapshot.state === 'approved') && snapshot.target === undefined)
|
|
42
|
-
problems.push(snapshot.state + ' requires a target');
|
|
43
|
-
if (snapshot.state === 'committed' && snapshot.receipt !== undefined && snapshot.receipt.unitId !== snapshot.unitId)
|
|
44
|
-
problems.push('receipt.unitId must equal unitId');
|
|
45
|
-
return problems;
|
|
46
|
-
}
|
|
47
|
-
//# sourceMappingURL=invariants.js.map
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/** Pure append-only ledger and deterministic fold. No IO. */
|
|
2
|
-
import type { PairEvent, PairSnapshot } from './model.ts';
|
|
3
|
-
import { type UnitEvent, type UnitSnapshot } from './unit.ts';
|
|
4
|
-
export type LedgerEntry = {
|
|
5
|
-
readonly seq: number;
|
|
6
|
-
readonly at: string;
|
|
7
|
-
readonly type: 'pair-created';
|
|
8
|
-
readonly snapshot: PairSnapshot;
|
|
9
|
-
} | {
|
|
10
|
-
readonly seq: number;
|
|
11
|
-
readonly at: string;
|
|
12
|
-
readonly type: 'pair-transition';
|
|
13
|
-
readonly pairId: string;
|
|
14
|
-
readonly expectedVersion: number;
|
|
15
|
-
readonly event: PairEvent;
|
|
16
|
-
} | {
|
|
17
|
-
readonly seq: number;
|
|
18
|
-
readonly at: string;
|
|
19
|
-
readonly type: 'unit-created';
|
|
20
|
-
readonly snapshot: UnitSnapshot;
|
|
21
|
-
} | {
|
|
22
|
-
readonly seq: number;
|
|
23
|
-
readonly at: string;
|
|
24
|
-
readonly type: 'unit-transition';
|
|
25
|
-
readonly unitId: string;
|
|
26
|
-
readonly expectedVersion: number;
|
|
27
|
-
readonly event: UnitEvent;
|
|
28
|
-
};
|
|
29
|
-
export interface LedgerState {
|
|
30
|
-
readonly seq: number;
|
|
31
|
-
readonly pairs: ReadonlyMap<string, PairSnapshot>;
|
|
32
|
-
readonly units: ReadonlyMap<string, UnitSnapshot>;
|
|
33
|
-
}
|
|
34
|
-
export type FoldResult = {
|
|
35
|
-
readonly ok: true;
|
|
36
|
-
readonly state: LedgerState;
|
|
37
|
-
} | {
|
|
38
|
-
readonly ok: false;
|
|
39
|
-
readonly seq: number;
|
|
40
|
-
readonly message: string;
|
|
41
|
-
};
|
|
42
|
-
export declare function emptyLedger(): LedgerState;
|
|
43
|
-
export declare function fold(entries: readonly LedgerEntry[]): FoldResult;
|
|
44
|
-
/** Deterministic projection bytes for replay-equality tests. */
|
|
45
|
-
export declare function ledgerProjectionJson(state: LedgerState): string;
|
|
46
|
-
//# sourceMappingURL=ledger.d.ts.map
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { transition as transitionPair } from "./machine.js";
|
|
2
|
-
import { checkPair } from "./invariants.js";
|
|
3
|
-
import { checkUnit, transitionUnit } from "./unit.js";
|
|
4
|
-
export function emptyLedger() {
|
|
5
|
-
return { seq: 0, pairs: new Map(), units: new Map() };
|
|
6
|
-
}
|
|
7
|
-
export function fold(entries) {
|
|
8
|
-
let state = emptyLedger();
|
|
9
|
-
for (const entry of entries) {
|
|
10
|
-
if (!Number.isInteger(entry.seq) || entry.seq !== state.seq + 1)
|
|
11
|
-
return { ok: false, seq: entry.seq, message: 'expected seq ' + String(state.seq + 1) + ', got ' + String(entry.seq) };
|
|
12
|
-
if (entry.type === 'pair-created') {
|
|
13
|
-
if (state.pairs.has(entry.snapshot.id))
|
|
14
|
-
return { ok: false, seq: entry.seq, message: 'duplicate pair ' + entry.snapshot.id };
|
|
15
|
-
const problems = checkPair(entry.snapshot);
|
|
16
|
-
if (problems.length > 0)
|
|
17
|
-
return { ok: false, seq: entry.seq, message: 'pair invalid: ' + problems.join('; ') };
|
|
18
|
-
state = { ...state, seq: entry.seq, pairs: new Map(state.pairs).set(entry.snapshot.id, entry.snapshot) };
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (entry.type === 'pair-transition') {
|
|
22
|
-
const current = state.pairs.get(entry.pairId);
|
|
23
|
-
if (current === undefined)
|
|
24
|
-
return { ok: false, seq: entry.seq, message: 'pair not found: ' + entry.pairId };
|
|
25
|
-
const moved = transitionPair(current, entry.event, entry.expectedVersion);
|
|
26
|
-
if (!moved.ok)
|
|
27
|
-
return { ok: false, seq: entry.seq, message: moved.message };
|
|
28
|
-
const problems = checkPair(moved.snapshot);
|
|
29
|
-
if (problems.length > 0)
|
|
30
|
-
return { ok: false, seq: entry.seq, message: 'pair invalid after transition: ' + problems.join('; ') };
|
|
31
|
-
state = { ...state, seq: entry.seq, pairs: new Map(state.pairs).set(entry.pairId, moved.snapshot) };
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (entry.type === 'unit-created') {
|
|
35
|
-
if (state.units.has(entry.snapshot.unitId))
|
|
36
|
-
return { ok: false, seq: entry.seq, message: 'duplicate unit ' + entry.snapshot.unitId };
|
|
37
|
-
const problems = checkUnit(entry.snapshot);
|
|
38
|
-
if (problems.length > 0)
|
|
39
|
-
return { ok: false, seq: entry.seq, message: 'unit invalid: ' + problems.join('; ') };
|
|
40
|
-
state = { ...state, seq: entry.seq, units: new Map(state.units).set(entry.snapshot.unitId, entry.snapshot) };
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
const current = state.units.get(entry.unitId);
|
|
44
|
-
if (current === undefined)
|
|
45
|
-
return { ok: false, seq: entry.seq, message: 'unit not found: ' + entry.unitId };
|
|
46
|
-
const moved = transitionUnit(current, entry.event, entry.expectedVersion);
|
|
47
|
-
if (!moved.ok)
|
|
48
|
-
return { ok: false, seq: entry.seq, message: moved.message };
|
|
49
|
-
const problems = checkUnit(moved.snapshot);
|
|
50
|
-
if (problems.length > 0)
|
|
51
|
-
return { ok: false, seq: entry.seq, message: 'unit invalid after transition: ' + problems.join('; ') };
|
|
52
|
-
state = { ...state, seq: entry.seq, units: new Map(state.units).set(entry.unitId, moved.snapshot) };
|
|
53
|
-
}
|
|
54
|
-
return { ok: true, state };
|
|
55
|
-
}
|
|
56
|
-
/** Deterministic projection bytes for replay-equality tests. */
|
|
57
|
-
export function ledgerProjectionJson(state) {
|
|
58
|
-
const pairs = [...state.pairs.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
59
|
-
const units = [...state.units.values()].sort((left, right) => left.unitId.localeCompare(right.unitId));
|
|
60
|
-
return JSON.stringify({ seq: state.seq, pairs, units });
|
|
61
|
-
}
|
|
62
|
-
//# sourceMappingURL=ledger.js.map
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** Pure pair state machine. Every legal transition is explicit; no IO. */
|
|
2
|
-
import type { PairEvent, PairSnapshot } from './model.ts';
|
|
3
|
-
export type TransitionErrorCode = 'illegal-transition' | 'version-conflict' | 'invalid-event';
|
|
4
|
-
export type TransitionResult = {
|
|
5
|
-
readonly ok: true;
|
|
6
|
-
readonly snapshot: PairSnapshot;
|
|
7
|
-
} | {
|
|
8
|
-
readonly ok: false;
|
|
9
|
-
readonly code: TransitionErrorCode;
|
|
10
|
-
readonly message: string;
|
|
11
|
-
};
|
|
12
|
-
export declare function transition(snapshot: PairSnapshot, event: PairEvent, expectedVersion: number): TransitionResult;
|
|
13
|
-
//# sourceMappingURL=machine.d.ts.map
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
function fail(code, message) {
|
|
2
|
-
return { ok: false, code, message };
|
|
3
|
-
}
|
|
4
|
-
function clean(snapshot) {
|
|
5
|
-
const { failure, closedReason, ...rest } = snapshot;
|
|
6
|
-
void failure;
|
|
7
|
-
void closedReason;
|
|
8
|
-
return rest;
|
|
9
|
-
}
|
|
10
|
-
function next(snapshot, state, at, patch = {}) {
|
|
11
|
-
return { ...clean(snapshot), state, version: snapshot.version + 1, updatedAt: at, ...patch };
|
|
12
|
-
}
|
|
13
|
-
function requireText(value, field) {
|
|
14
|
-
return value === undefined || value.length === 0 ? fail('invalid-event', field + ' is required') : null;
|
|
15
|
-
}
|
|
16
|
-
export function transition(snapshot, event, expectedVersion) {
|
|
17
|
-
if (snapshot.version !== expectedVersion)
|
|
18
|
-
return fail('version-conflict', 'expected ' + String(expectedVersion) + ', got ' + String(snapshot.version));
|
|
19
|
-
const at = event.at;
|
|
20
|
-
switch (event.type) {
|
|
21
|
-
case 'screen':
|
|
22
|
-
return snapshot.state === 'candidate' ? { ok: true, snapshot: next(snapshot, 'deciding', at) } : fail('illegal-transition', 'screen from ' + snapshot.state);
|
|
23
|
-
case 'decide': {
|
|
24
|
-
if (snapshot.state !== 'deciding')
|
|
25
|
-
return fail('illegal-transition', 'decide from ' + snapshot.state);
|
|
26
|
-
if (event.decision === 'keep')
|
|
27
|
-
return { ok: true, snapshot: next(snapshot, 'closed', at, { closedReason: 'keep', origin: 'agent' }) };
|
|
28
|
-
const missing = requireText(event.target, 'target');
|
|
29
|
-
if (missing !== null)
|
|
30
|
-
return missing;
|
|
31
|
-
const decision = { target: event.target, sources: [snapshot.a, snapshot.b] };
|
|
32
|
-
const patch = { decision, target: event.target, approval: event.approval, ...(event.runId === undefined ? {} : { runId: event.runId }) };
|
|
33
|
-
return { ok: true, snapshot: next(snapshot, event.approval === 'on' ? 'pending' : 'approved', at, patch) };
|
|
34
|
-
}
|
|
35
|
-
case 'approve':
|
|
36
|
-
return snapshot.state === 'pending' ? { ok: true, snapshot: next(snapshot, 'approved', at) } : fail('illegal-transition', 'approve from ' + snapshot.state);
|
|
37
|
-
case 'reject':
|
|
38
|
-
return snapshot.state === 'pending' ? { ok: true, snapshot: next(snapshot, 'closed', at, { closedReason: 'reject', origin: 'human' }) } : fail('illegal-transition', 'reject from ' + snapshot.state);
|
|
39
|
-
case 'author-start':
|
|
40
|
-
return snapshot.state === 'approved' ? { ok: true, snapshot: next(snapshot, 'authoring', at) } : fail('illegal-transition', 'author-start from ' + snapshot.state);
|
|
41
|
-
case 'staged': {
|
|
42
|
-
if (snapshot.state !== 'authoring')
|
|
43
|
-
return fail('illegal-transition', 'staged from ' + snapshot.state);
|
|
44
|
-
const missing = requireText(event.stageRef, 'stageRef');
|
|
45
|
-
return missing ?? { ok: true, snapshot: next(snapshot, 'staged', at, { stageRef: event.stageRef }) };
|
|
46
|
-
}
|
|
47
|
-
case 'commit-start': {
|
|
48
|
-
if (snapshot.state !== 'staged')
|
|
49
|
-
return fail('illegal-transition', 'commit-start from ' + snapshot.state);
|
|
50
|
-
const missing = requireText(event.unitId, 'unitId');
|
|
51
|
-
return missing ?? { ok: true, snapshot: next(snapshot, 'committing', at, { unitId: event.unitId }) };
|
|
52
|
-
}
|
|
53
|
-
case 'commit-succeeded':
|
|
54
|
-
if (snapshot.state !== 'committing')
|
|
55
|
-
return fail('illegal-transition', 'commit-succeeded from ' + snapshot.state);
|
|
56
|
-
if (event.receipt.unitId.length === 0)
|
|
57
|
-
return fail('invalid-event', 'receipt.unitId is required');
|
|
58
|
-
return { ok: true, snapshot: next(snapshot, 'committed', at, { unitId: event.receipt.unitId, receipt: event.receipt }) };
|
|
59
|
-
case 'commit-failed':
|
|
60
|
-
return snapshot.state === 'committing' ? { ok: true, snapshot: next(snapshot, 'failed', at, { failure: event.failure }) } : fail('illegal-transition', 'commit-failed from ' + snapshot.state);
|
|
61
|
-
case 'commit-unknown':
|
|
62
|
-
return snapshot.state === 'committing' ? { ok: true, snapshot: next(snapshot, 'unknown', at) } : fail('illegal-transition', 'commit-unknown from ' + snapshot.state);
|
|
63
|
-
case 'recover': {
|
|
64
|
-
if (snapshot.state !== 'unknown')
|
|
65
|
-
return fail('illegal-transition', 'recover from ' + snapshot.state);
|
|
66
|
-
if (event.outcome === 'committed') {
|
|
67
|
-
if (event.receipt === undefined || event.receipt.unitId.length === 0)
|
|
68
|
-
return fail('invalid-event', 'receipt is required for recovered commit');
|
|
69
|
-
return { ok: true, snapshot: next(snapshot, 'committed', at, { unitId: event.receipt.unitId, receipt: event.receipt }) };
|
|
70
|
-
}
|
|
71
|
-
if (event.outcome === 'failed') {
|
|
72
|
-
if (event.failure === undefined)
|
|
73
|
-
return fail('invalid-event', 'failure is required for recovered failure');
|
|
74
|
-
return { ok: true, snapshot: next(snapshot, 'failed', at, { failure: event.failure }) };
|
|
75
|
-
}
|
|
76
|
-
return { ok: true, snapshot: next(snapshot, 'cancelled', at) };
|
|
77
|
-
}
|
|
78
|
-
case 'close':
|
|
79
|
-
if (event.reason !== 'over-limit')
|
|
80
|
-
return fail('invalid-event', 'close only accepts over-limit; keep/reject belong to decide/reject');
|
|
81
|
-
return snapshot.state === 'authoring' || snapshot.state === 'staged'
|
|
82
|
-
? { ok: true, snapshot: next(snapshot, 'closed', at, { closedReason: event.reason }) }
|
|
83
|
-
: fail('illegal-transition', 'close from ' + snapshot.state);
|
|
84
|
-
case 'expire':
|
|
85
|
-
return snapshot.state === 'pending' || snapshot.state === 'staged'
|
|
86
|
-
? { ok: true, snapshot: next(snapshot, 'expired', at) }
|
|
87
|
-
: fail('illegal-transition', 'expire from ' + snapshot.state);
|
|
88
|
-
case 'cancel':
|
|
89
|
-
return ['candidate', 'deciding', 'pending', 'approved', 'authoring', 'staged', 'unknown'].includes(snapshot.state)
|
|
90
|
-
? { ok: true, snapshot: next(snapshot, 'cancelled', at) }
|
|
91
|
-
: fail('illegal-transition', 'cancel from ' + snapshot.state);
|
|
92
|
-
case 'fail':
|
|
93
|
-
return ['candidate', 'deciding', 'pending', 'approved', 'authoring', 'staged'].includes(snapshot.state)
|
|
94
|
-
? { ok: true, snapshot: next(snapshot, 'failed', at, { failure: event.failure }) }
|
|
95
|
-
: fail('illegal-transition', 'fail from ' + snapshot.state);
|
|
96
|
-
case 'retry':
|
|
97
|
-
return snapshot.state === 'failed'
|
|
98
|
-
? { ok: true, snapshot: next(snapshot, 'candidate', at, { attempts: snapshot.attempts + 1 }) }
|
|
99
|
-
: fail('illegal-transition', 'retry from ' + snapshot.state);
|
|
100
|
-
case 'undo':
|
|
101
|
-
return snapshot.state === 'committed' ? { ok: true, snapshot: next(snapshot, 'undone', at) } : fail('illegal-transition', 'undo from ' + snapshot.state);
|
|
102
|
-
case 'supersede':
|
|
103
|
-
return snapshot.state === 'undone' ? fail('illegal-transition', 'supersede from undone') : { ok: true, snapshot: next(snapshot, 'superseded', at) };
|
|
104
|
-
default:
|
|
105
|
-
return fail('invalid-event', 'unknown event');
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
//# sourceMappingURL=machine.js.map
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/** Pure merge-kernel model. No IO, no host, no clock. */
|
|
2
|
-
export type PairState = 'candidate' | 'deciding' | 'pending' | 'approved' | 'authoring' | 'staged' | 'committing' | 'committed' | 'closed' | 'expired' | 'failed' | 'cancelled' | 'undone' | 'superseded' | 'unknown';
|
|
3
|
-
export type ClosedReason = 'keep' | 'reject' | 'over-limit';
|
|
4
|
-
export type ApprovalState = 'on' | 'off';
|
|
5
|
-
export type FailureStage = 'command' | 'screen' | 'decide' | 'author' | 'validate' | 'commit' | 'run' | 'surface' | 'schema';
|
|
6
|
-
export type FailureCode = 'bad-pair' | 'bad-target' | 'bad-flag' | 'source-gone' | 'source-changed' | 'target-taken' | 'validation-failed' | 'commit-failed' | 'schema-invalid' | 'timeout' | 'capability-missing' | 'runtime';
|
|
7
|
-
export interface Failure {
|
|
8
|
-
readonly code: FailureCode;
|
|
9
|
-
readonly stage: FailureStage;
|
|
10
|
-
readonly retryable: boolean;
|
|
11
|
-
readonly reportOnly: boolean;
|
|
12
|
-
readonly userMessageKey: string;
|
|
13
|
-
readonly detail?: string;
|
|
14
|
-
}
|
|
15
|
-
export interface MergeDecision {
|
|
16
|
-
readonly target: string;
|
|
17
|
-
readonly sources: readonly string[];
|
|
18
|
-
readonly rationale?: string;
|
|
19
|
-
}
|
|
20
|
-
export interface CommitReceipt {
|
|
21
|
-
readonly unitId: string;
|
|
22
|
-
readonly evidenceSha256: string;
|
|
23
|
-
readonly targetRevision?: string;
|
|
24
|
-
readonly sourceRevisions?: Readonly<Record<string, string>>;
|
|
25
|
-
}
|
|
26
|
-
export type RunState = 'queued' | 'screening' | 'deciding' | 'awaiting-approval' | 'authoring' | 'committing' | 'settled' | 'failed' | 'cancelled' | 'recovering';
|
|
27
|
-
export interface SkillSnapshot {
|
|
28
|
-
readonly name: string;
|
|
29
|
-
readonly revision: string;
|
|
30
|
-
readonly protection?: 'managed' | 'bundled' | 'hub' | 'pinned' | 'protected' | 'unknown';
|
|
31
|
-
}
|
|
32
|
-
export interface Candidate {
|
|
33
|
-
readonly pairKey: string;
|
|
34
|
-
readonly a: string;
|
|
35
|
-
readonly b: string;
|
|
36
|
-
readonly evidenceSeq: readonly number[];
|
|
37
|
-
readonly score?: number;
|
|
38
|
-
}
|
|
39
|
-
export interface RunSnapshot {
|
|
40
|
-
readonly id: string;
|
|
41
|
-
readonly state: RunState;
|
|
42
|
-
readonly version: number;
|
|
43
|
-
readonly pairs: readonly string[];
|
|
44
|
-
readonly startedAt: string;
|
|
45
|
-
readonly updatedAt: string;
|
|
46
|
-
}
|
|
47
|
-
export interface UnitEffect {
|
|
48
|
-
readonly kind: 'skill' | 'usage' | 'staging' | 'report';
|
|
49
|
-
readonly ref: string;
|
|
50
|
-
}
|
|
51
|
-
export interface UnitPlan {
|
|
52
|
-
readonly unitId: string;
|
|
53
|
-
readonly pairKey: string;
|
|
54
|
-
readonly target: string;
|
|
55
|
-
readonly sources: readonly string[];
|
|
56
|
-
readonly effects: readonly UnitEffect[];
|
|
57
|
-
}
|
|
58
|
-
export interface PairSnapshot {
|
|
59
|
-
readonly id: string;
|
|
60
|
-
readonly pairKey: string;
|
|
61
|
-
readonly a: string;
|
|
62
|
-
readonly b: string;
|
|
63
|
-
readonly state: PairState;
|
|
64
|
-
readonly version: number;
|
|
65
|
-
readonly createdAt: string;
|
|
66
|
-
readonly updatedAt: string;
|
|
67
|
-
readonly origin?: 'agent' | 'human' | 'system';
|
|
68
|
-
readonly runId?: string;
|
|
69
|
-
readonly target?: string;
|
|
70
|
-
readonly decision?: MergeDecision;
|
|
71
|
-
readonly approval?: ApprovalState;
|
|
72
|
-
readonly failure?: Failure;
|
|
73
|
-
readonly closedReason?: ClosedReason;
|
|
74
|
-
readonly stageRef?: string;
|
|
75
|
-
readonly unitId?: string;
|
|
76
|
-
readonly expiresAt?: number;
|
|
77
|
-
readonly receipt?: CommitReceipt;
|
|
78
|
-
readonly attempts: number;
|
|
79
|
-
}
|
|
80
|
-
export type PairEvent = {
|
|
81
|
-
readonly type: 'screen';
|
|
82
|
-
readonly at: string;
|
|
83
|
-
} | {
|
|
84
|
-
readonly type: 'decide';
|
|
85
|
-
readonly at: string;
|
|
86
|
-
readonly decision: 'merge' | 'keep';
|
|
87
|
-
readonly approval: ApprovalState;
|
|
88
|
-
readonly target?: string;
|
|
89
|
-
readonly runId?: string;
|
|
90
|
-
} | {
|
|
91
|
-
readonly type: 'approve';
|
|
92
|
-
readonly at: string;
|
|
93
|
-
} | {
|
|
94
|
-
readonly type: 'reject';
|
|
95
|
-
readonly at: string;
|
|
96
|
-
} | {
|
|
97
|
-
readonly type: 'author-start';
|
|
98
|
-
readonly at: string;
|
|
99
|
-
} | {
|
|
100
|
-
readonly type: 'staged';
|
|
101
|
-
readonly at: string;
|
|
102
|
-
readonly stageRef: string;
|
|
103
|
-
} | {
|
|
104
|
-
readonly type: 'commit-start';
|
|
105
|
-
readonly at: string;
|
|
106
|
-
readonly unitId: string;
|
|
107
|
-
} | {
|
|
108
|
-
readonly type: 'commit-succeeded';
|
|
109
|
-
readonly at: string;
|
|
110
|
-
readonly receipt: CommitReceipt;
|
|
111
|
-
} | {
|
|
112
|
-
readonly type: 'commit-failed';
|
|
113
|
-
readonly at: string;
|
|
114
|
-
readonly failure: Failure;
|
|
115
|
-
} | {
|
|
116
|
-
readonly type: 'commit-unknown';
|
|
117
|
-
readonly at: string;
|
|
118
|
-
readonly reason: string;
|
|
119
|
-
} | {
|
|
120
|
-
readonly type: 'recover';
|
|
121
|
-
readonly at: string;
|
|
122
|
-
readonly outcome: 'committed' | 'failed' | 'cancelled';
|
|
123
|
-
readonly receipt?: CommitReceipt;
|
|
124
|
-
readonly failure?: Failure;
|
|
125
|
-
} | {
|
|
126
|
-
readonly type: 'close';
|
|
127
|
-
readonly at: string;
|
|
128
|
-
readonly reason: ClosedReason;
|
|
129
|
-
} | {
|
|
130
|
-
readonly type: 'expire';
|
|
131
|
-
readonly at: string;
|
|
132
|
-
readonly reason: 'pending-ttl' | 'staging-ttl';
|
|
133
|
-
} | {
|
|
134
|
-
readonly type: 'cancel';
|
|
135
|
-
readonly at: string;
|
|
136
|
-
} | {
|
|
137
|
-
readonly type: 'fail';
|
|
138
|
-
readonly at: string;
|
|
139
|
-
readonly failure: Failure;
|
|
140
|
-
} | {
|
|
141
|
-
readonly type: 'retry';
|
|
142
|
-
readonly at: string;
|
|
143
|
-
} | {
|
|
144
|
-
readonly type: 'undo';
|
|
145
|
-
readonly at: string;
|
|
146
|
-
} | {
|
|
147
|
-
readonly type: 'supersede';
|
|
148
|
-
readonly at: string;
|
|
149
|
-
};
|
|
150
|
-
export declare const TERMINAL_STATES: readonly PairState[];
|
|
151
|
-
export declare const ACTIVE_STATES: readonly PairState[];
|
|
152
|
-
export declare const ALL_STATES: readonly PairState[];
|
|
153
|
-
export declare const CLOSED_REASONS: readonly ClosedReason[];
|
|
154
|
-
//# sourceMappingURL=model.d.ts.map
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export const TERMINAL_STATES = ['closed', 'expired', 'cancelled', 'committed', 'undone', 'superseded'];
|
|
2
|
-
export const ACTIVE_STATES = ['candidate', 'deciding', 'pending', 'approved', 'authoring', 'staged', 'committing', 'unknown'];
|
|
3
|
-
export const ALL_STATES = [...ACTIVE_STATES, ...TERMINAL_STATES];
|
|
4
|
-
export const CLOSED_REASONS = ['keep', 'reject', 'over-limit'];
|
|
5
|
-
//# sourceMappingURL=model.js.map
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/** Pure projection over the ledger. One expired predicate, one queue mapping. */
|
|
2
|
-
import type { PairSnapshot } from './model.ts';
|
|
3
|
-
import type { LedgerState } from './ledger.ts';
|
|
4
|
-
import type { UnitSnapshot } from './unit.ts';
|
|
5
|
-
export type ProjectionQueue = 'verify' | 'pending' | 'fuse' | 'skipped';
|
|
6
|
-
export interface RowProjection {
|
|
7
|
-
readonly id: string;
|
|
8
|
-
readonly pairKey: string;
|
|
9
|
-
readonly state: PairSnapshot['state'];
|
|
10
|
-
readonly queue: ProjectionQueue | null;
|
|
11
|
-
readonly expired: boolean;
|
|
12
|
-
readonly version: number;
|
|
13
|
-
}
|
|
14
|
-
export interface ProjectionCounts {
|
|
15
|
-
readonly active: Readonly<Record<ProjectionQueue, number>>;
|
|
16
|
-
readonly expired: number;
|
|
17
|
-
}
|
|
18
|
-
export interface MergeProjection {
|
|
19
|
-
readonly seq: number;
|
|
20
|
-
readonly rows: readonly RowProjection[];
|
|
21
|
-
readonly units: readonly UnitSnapshot[];
|
|
22
|
-
readonly counts: ProjectionCounts;
|
|
23
|
-
}
|
|
24
|
-
export declare function queueOf(pair: PairSnapshot): ProjectionQueue | null;
|
|
25
|
-
export declare function isExpired(pair: PairSnapshot, now: number): boolean;
|
|
26
|
-
export declare function project(state: LedgerState, now: number): MergeProjection;
|
|
27
|
-
export declare function projectionJson(projection: MergeProjection): string;
|
|
28
|
-
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export function queueOf(pair) {
|
|
2
|
-
switch (pair.state) {
|
|
3
|
-
case 'candidate':
|
|
4
|
-
case 'deciding': return 'verify';
|
|
5
|
-
case 'pending': return 'pending';
|
|
6
|
-
case 'approved':
|
|
7
|
-
case 'authoring':
|
|
8
|
-
case 'staged':
|
|
9
|
-
case 'committing':
|
|
10
|
-
case 'unknown':
|
|
11
|
-
case 'failed': return 'fuse';
|
|
12
|
-
case 'closed': return 'skipped';
|
|
13
|
-
case 'expired': return 'skipped';
|
|
14
|
-
default: return null;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export function isExpired(pair, now) {
|
|
18
|
-
if (pair.state === 'expired')
|
|
19
|
-
return true;
|
|
20
|
-
if (pair.expiresAt === undefined)
|
|
21
|
-
return false;
|
|
22
|
-
if (pair.state !== 'pending' && pair.state !== 'staged')
|
|
23
|
-
return false;
|
|
24
|
-
return pair.expiresAt <= now;
|
|
25
|
-
}
|
|
26
|
-
export function project(state, now) {
|
|
27
|
-
const counts = { active: { verify: 0, pending: 0, fuse: 0, skipped: 0 }, expired: 0 };
|
|
28
|
-
const rows = [];
|
|
29
|
-
for (const pair of [...state.pairs.values()].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
30
|
-
const expired = isExpired(pair, now);
|
|
31
|
-
const queue = queueOf(pair);
|
|
32
|
-
rows.push({ id: pair.id, pairKey: pair.pairKey, state: pair.state, queue, expired, version: pair.version });
|
|
33
|
-
if (expired)
|
|
34
|
-
counts.expired += 1;
|
|
35
|
-
else if (queue !== null)
|
|
36
|
-
counts.active[queue] += 1;
|
|
37
|
-
}
|
|
38
|
-
const units = [...state.units.values()].sort((left, right) => left.unitId.localeCompare(right.unitId));
|
|
39
|
-
return { seq: state.seq, rows, units, counts };
|
|
40
|
-
}
|
|
41
|
-
export function projectionJson(projection) {
|
|
42
|
-
return JSON.stringify(projection);
|
|
43
|
-
}
|
|
44
|
-
//# sourceMappingURL=projection.js.map
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/** Pure recovery planner and timeout decision. No IO. */
|
|
2
|
-
import type { CommitReceipt } from './model.ts';
|
|
3
|
-
export type RecoveryDecision = {
|
|
4
|
-
readonly kind: 'retry';
|
|
5
|
-
readonly reason: 'no-intent';
|
|
6
|
-
} | {
|
|
7
|
-
readonly kind: 'committed';
|
|
8
|
-
readonly receipt: CommitReceipt;
|
|
9
|
-
} | {
|
|
10
|
-
readonly kind: 'roll-forward';
|
|
11
|
-
readonly reason: 'effects-complete' | 'after-hash-matches';
|
|
12
|
-
} | {
|
|
13
|
-
readonly kind: 'abort';
|
|
14
|
-
readonly reason: 'no-effects' | 'before-hash-matches';
|
|
15
|
-
} | {
|
|
16
|
-
readonly kind: 'blocked';
|
|
17
|
-
readonly reason: 'partial-effects';
|
|
18
|
-
};
|
|
19
|
-
export interface RecoveryObservation {
|
|
20
|
-
readonly hasIntent: boolean;
|
|
21
|
-
readonly hasReceipt: boolean;
|
|
22
|
-
readonly receipt?: CommitReceipt;
|
|
23
|
-
readonly beforeMatches: boolean;
|
|
24
|
-
readonly afterMatches: boolean;
|
|
25
|
-
readonly effects: 'none' | 'partial' | 'complete';
|
|
26
|
-
}
|
|
27
|
-
export declare function planRecovery(observation: RecoveryObservation): RecoveryDecision;
|
|
28
|
-
export type TimeoutDecision = 'failed' | 'unknown';
|
|
29
|
-
/** Before a durable intent, timeout is a normal failed run; after it, only recovery may decide. */
|
|
30
|
-
export declare function timeoutDecision(unitPrepared: boolean): TimeoutDecision;
|
|
31
|
-
//# sourceMappingURL=recovery.d.ts.map
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export function planRecovery(observation) {
|
|
2
|
-
if (observation.hasReceipt) {
|
|
3
|
-
if (observation.receipt === undefined)
|
|
4
|
-
return { kind: 'blocked', reason: 'partial-effects' };
|
|
5
|
-
return { kind: 'committed', receipt: observation.receipt };
|
|
6
|
-
}
|
|
7
|
-
if (!observation.hasIntent)
|
|
8
|
-
return { kind: 'retry', reason: 'no-intent' };
|
|
9
|
-
if (observation.effects === 'complete' || observation.afterMatches)
|
|
10
|
-
return { kind: 'roll-forward', reason: observation.effects === 'complete' ? 'effects-complete' : 'after-hash-matches' };
|
|
11
|
-
if (observation.effects === 'none' || observation.beforeMatches)
|
|
12
|
-
return { kind: 'abort', reason: observation.effects === 'none' ? 'no-effects' : 'before-hash-matches' };
|
|
13
|
-
return { kind: 'blocked', reason: 'partial-effects' };
|
|
14
|
-
}
|
|
15
|
-
/** Before a durable intent, timeout is a normal failed run; after it, only recovery may decide. */
|
|
16
|
-
export function timeoutDecision(unitPrepared) {
|
|
17
|
-
return unitPrepared ? 'unknown' : 'failed';
|
|
18
|
-
}
|
|
19
|
-
//# sourceMappingURL=recovery.js.map
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/** Pure unit-of-work protocol: intent -> applied -> committed, with recovery states. */
|
|
2
|
-
import type { CommitReceipt } from './model.ts';
|
|
3
|
-
export type UnitState = 'planned' | 'prepared' | 'applied' | 'committed' | 'aborted' | 'blocked';
|
|
4
|
-
export interface UnitSnapshot {
|
|
5
|
-
readonly unitId: string;
|
|
6
|
-
readonly state: UnitState;
|
|
7
|
-
readonly version: number;
|
|
8
|
-
readonly planHash: string;
|
|
9
|
-
readonly receipt?: CommitReceipt;
|
|
10
|
-
readonly reason?: string;
|
|
11
|
-
}
|
|
12
|
-
export type UnitEvent = {
|
|
13
|
-
readonly type: 'prepare';
|
|
14
|
-
readonly planHash: string;
|
|
15
|
-
} | {
|
|
16
|
-
readonly type: 'applied';
|
|
17
|
-
} | {
|
|
18
|
-
readonly type: 'commit';
|
|
19
|
-
readonly receipt: CommitReceipt;
|
|
20
|
-
} | {
|
|
21
|
-
readonly type: 'abort';
|
|
22
|
-
readonly reason: string;
|
|
23
|
-
} | {
|
|
24
|
-
readonly type: 'block';
|
|
25
|
-
readonly reason: string;
|
|
26
|
-
} | {
|
|
27
|
-
readonly type: 'recover-commit';
|
|
28
|
-
readonly receipt: CommitReceipt;
|
|
29
|
-
} | {
|
|
30
|
-
readonly type: 'recover-abort';
|
|
31
|
-
readonly reason: string;
|
|
32
|
-
};
|
|
33
|
-
export type UnitTransition = {
|
|
34
|
-
readonly ok: true;
|
|
35
|
-
readonly snapshot: UnitSnapshot;
|
|
36
|
-
} | {
|
|
37
|
-
readonly ok: false;
|
|
38
|
-
readonly code: 'illegal-transition' | 'version-conflict' | 'invalid-event';
|
|
39
|
-
readonly message: string;
|
|
40
|
-
};
|
|
41
|
-
export declare function transitionUnit(snapshot: UnitSnapshot, event: UnitEvent, expectedVersion: number): UnitTransition;
|
|
42
|
-
export declare function checkUnit(snapshot: UnitSnapshot): string[];
|
|
43
|
-
//# sourceMappingURL=unit.d.ts.map
|
package/lib/types/kernel/unit.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
function fail(code, message) {
|
|
2
|
-
return { ok: false, code, message };
|
|
3
|
-
}
|
|
4
|
-
function next(snapshot, state, patch = {}) {
|
|
5
|
-
return { ...snapshot, state, version: snapshot.version + 1, ...patch };
|
|
6
|
-
}
|
|
7
|
-
export function transitionUnit(snapshot, event, expectedVersion) {
|
|
8
|
-
if (snapshot.version !== expectedVersion)
|
|
9
|
-
return fail('version-conflict', 'expected ' + String(expectedVersion) + ', got ' + String(snapshot.version));
|
|
10
|
-
switch (event.type) {
|
|
11
|
-
case 'prepare':
|
|
12
|
-
if (snapshot.state !== 'planned')
|
|
13
|
-
return fail('illegal-transition', 'prepare from ' + snapshot.state);
|
|
14
|
-
if (event.planHash.length === 0)
|
|
15
|
-
return fail('invalid-event', 'planHash is required');
|
|
16
|
-
return { ok: true, snapshot: next(snapshot, 'prepared', { planHash: event.planHash }) };
|
|
17
|
-
case 'applied':
|
|
18
|
-
return snapshot.state === 'prepared' ? { ok: true, snapshot: next(snapshot, 'applied') } : fail('illegal-transition', 'applied from ' + snapshot.state);
|
|
19
|
-
case 'commit':
|
|
20
|
-
if (snapshot.state !== 'applied')
|
|
21
|
-
return fail('illegal-transition', 'commit from ' + snapshot.state);
|
|
22
|
-
if (event.receipt.unitId.length === 0)
|
|
23
|
-
return fail('invalid-event', 'receipt.unitId is required');
|
|
24
|
-
return { ok: true, snapshot: next(snapshot, 'committed', { receipt: event.receipt }) };
|
|
25
|
-
case 'abort':
|
|
26
|
-
return snapshot.state === 'planned' || snapshot.state === 'prepared' || snapshot.state === 'applied'
|
|
27
|
-
? { ok: true, snapshot: next(snapshot, 'aborted', { reason: event.reason }) }
|
|
28
|
-
: fail('illegal-transition', 'abort from ' + snapshot.state);
|
|
29
|
-
case 'block':
|
|
30
|
-
return snapshot.state === 'prepared' || snapshot.state === 'applied'
|
|
31
|
-
? { ok: true, snapshot: next(snapshot, 'blocked', { reason: event.reason }) }
|
|
32
|
-
: fail('illegal-transition', 'block from ' + snapshot.state);
|
|
33
|
-
case 'recover-commit':
|
|
34
|
-
if (snapshot.state !== 'blocked')
|
|
35
|
-
return fail('illegal-transition', 'recover-commit from ' + snapshot.state);
|
|
36
|
-
return { ok: true, snapshot: next(snapshot, 'committed', { receipt: event.receipt }) };
|
|
37
|
-
case 'recover-abort':
|
|
38
|
-
return snapshot.state === 'blocked'
|
|
39
|
-
? { ok: true, snapshot: next(snapshot, 'aborted', { reason: event.reason }) }
|
|
40
|
-
: fail('illegal-transition', 'recover-abort from ' + snapshot.state);
|
|
41
|
-
default:
|
|
42
|
-
return fail('invalid-event', 'unknown unit event');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
export function checkUnit(snapshot) {
|
|
46
|
-
const problems = [];
|
|
47
|
-
if (snapshot.unitId.length === 0)
|
|
48
|
-
problems.push('unitId is empty');
|
|
49
|
-
if (snapshot.planHash.length === 0)
|
|
50
|
-
problems.push('planHash is empty');
|
|
51
|
-
if (!Number.isInteger(snapshot.version) || snapshot.version < 0)
|
|
52
|
-
problems.push('version must be a non-negative integer');
|
|
53
|
-
if (snapshot.state === 'committed' && snapshot.receipt === undefined)
|
|
54
|
-
problems.push('committed requires a receipt');
|
|
55
|
-
if (snapshot.state === 'blocked' && snapshot.reason === undefined)
|
|
56
|
-
problems.push('blocked requires a reason');
|
|
57
|
-
if (snapshot.state === 'aborted' && snapshot.reason === undefined)
|
|
58
|
-
problems.push('aborted requires a reason');
|
|
59
|
-
if (snapshot.receipt !== undefined && snapshot.receipt.unitId !== snapshot.unitId)
|
|
60
|
-
problems.push('receipt.unitId must equal unitId');
|
|
61
|
-
return problems;
|
|
62
|
-
}
|
|
63
|
-
//# sourceMappingURL=unit.js.map
|