open-multi-agent-kit 1.0.0 → 1.2.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/CHANGELOG.md +6 -1
- package/README.md +2 -2
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +1 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/coordination/awareness.d.ts +32 -0
- package/dist/coordination/awareness.d.ts.map +1 -0
- package/dist/coordination/awareness.js +55 -0
- package/dist/coordination/awareness.js.map +1 -0
- package/dist/coordination/broker.d.ts +79 -0
- package/dist/coordination/broker.d.ts.map +1 -0
- package/dist/coordination/broker.js +195 -0
- package/dist/coordination/broker.js.map +1 -0
- package/dist/coordination/index.d.ts +16 -0
- package/dist/coordination/index.d.ts.map +1 -0
- package/dist/coordination/index.js +16 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/coordination/integration.d.ts +59 -0
- package/dist/coordination/integration.d.ts.map +1 -0
- package/dist/coordination/integration.js +126 -0
- package/dist/coordination/integration.js.map +1 -0
- package/dist/coordination/operation.d.ts +106 -0
- package/dist/coordination/operation.d.ts.map +1 -0
- package/dist/coordination/operation.js +179 -0
- package/dist/coordination/operation.js.map +1 -0
- package/dist/coordination/resource.d.ts +33 -0
- package/dist/coordination/resource.d.ts.map +1 -0
- package/dist/coordination/resource.js +89 -0
- package/dist/coordination/resource.js.map +1 -0
- package/dist/coordination/session.d.ts +74 -0
- package/dist/coordination/session.d.ts.map +1 -0
- package/dist/coordination/session.js +143 -0
- package/dist/coordination/session.js.map +1 -0
- package/dist/coordination/types.d.ts +68 -0
- package/dist/coordination/types.d.ts.map +1 -0
- package/dist/coordination/types.js +26 -0
- package/dist/coordination/types.js.map +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +28 -7
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/control-state.d.ts +41 -0
- package/dist/core/compaction/control-state.d.ts.map +1 -0
- package/dist/core/compaction/control-state.js +85 -0
- package/dist/core/compaction/control-state.js.map +1 -0
- package/dist/core/compaction/fallback.d.ts +60 -0
- package/dist/core/compaction/fallback.d.ts.map +1 -0
- package/dist/core/compaction/fallback.js +117 -0
- package/dist/core/compaction/fallback.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/context-budget-headroom-candidates.d.ts +16 -1
- package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
- package/dist/core/context-budget-headroom-candidates.js +31 -13
- package/dist/core/context-budget-headroom-candidates.js.map +1 -1
- package/dist/core/context-budget-headroom-types.d.ts +7 -1
- package/dist/core/context-budget-headroom-types.d.ts.map +1 -1
- package/dist/core/context-budget-headroom-types.js +0 -1
- package/dist/core/context-budget-headroom-types.js.map +1 -1
- package/dist/core/context-budget-headroom.d.ts +10 -0
- package/dist/core/context-budget-headroom.d.ts.map +1 -1
- package/dist/core/context-budget-headroom.js +11 -2
- package/dist/core/context-budget-headroom.js.map +1 -1
- package/dist/core/context-budget-v2-global-pass.d.ts +21 -0
- package/dist/core/context-budget-v2-global-pass.d.ts.map +1 -0
- package/dist/core/context-budget-v2-global-pass.js +203 -0
- package/dist/core/context-budget-v2-global-pass.js.map +1 -0
- package/dist/core/context-budget-v2-planned-items.d.ts +12 -0
- package/dist/core/context-budget-v2-planned-items.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planned-items.js +21 -2
- package/dist/core/context-budget-v2-planned-items.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +18 -19
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-scoring.d.ts +7 -1
- package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
- package/dist/core/context-budget-v2-scoring.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts +3 -0
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +6 -2
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +6 -1
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js +6 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/provider-default-models.d.ts +1 -0
- package/dist/core/provider-default-models.d.ts.map +1 -1
- package/dist/core/provider-default-models.js +1 -0
- package/dist/core/provider-default-models.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +1 -0
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/reasoning-router-resolver.d.ts +7 -0
- package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
- package/dist/core/reasoning-router-resolver.js +21 -3
- package/dist/core/reasoning-router-resolver.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts +8 -5
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +8 -5
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/session-compaction-service.d.ts +15 -0
- package/dist/core/session-compaction-service.d.ts.map +1 -1
- package/dist/core/session-compaction-service.js +17 -4
- package/dist/core/session-compaction-service.js.map +1 -1
- package/dist/core/todo-runtime-state.d.ts +12 -0
- package/dist/core/todo-runtime-state.d.ts.map +1 -1
- package/dist/core/todo-runtime-state.js +23 -0
- package/dist/core/todo-runtime-state.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/metacognition/calibration-selective.d.ts +78 -0
- package/dist/metacognition/calibration-selective.d.ts.map +1 -0
- package/dist/metacognition/calibration-selective.js +161 -0
- package/dist/metacognition/calibration-selective.js.map +1 -0
- package/dist/metacognition/calibration.d.ts +62 -0
- package/dist/metacognition/calibration.d.ts.map +1 -0
- package/dist/metacognition/calibration.js +122 -0
- package/dist/metacognition/calibration.js.map +1 -0
- package/dist/metacognition/checkpoint.d.ts +60 -0
- package/dist/metacognition/checkpoint.d.ts.map +1 -0
- package/dist/metacognition/checkpoint.js +57 -0
- package/dist/metacognition/checkpoint.js.map +1 -0
- package/dist/metacognition/context7.d.ts +48 -0
- package/dist/metacognition/context7.d.ts.map +1 -0
- package/dist/metacognition/context7.js +154 -0
- package/dist/metacognition/context7.js.map +1 -0
- package/dist/metacognition/decision.d.ts +44 -0
- package/dist/metacognition/decision.d.ts.map +1 -0
- package/dist/metacognition/decision.js +111 -0
- package/dist/metacognition/decision.js.map +1 -0
- package/dist/metacognition/evaluation.d.ts +73 -0
- package/dist/metacognition/evaluation.d.ts.map +1 -0
- package/dist/metacognition/evaluation.js +97 -0
- package/dist/metacognition/evaluation.js.map +1 -0
- package/dist/metacognition/index.d.ts +31 -0
- package/dist/metacognition/index.d.ts.map +1 -0
- package/dist/metacognition/index.js +31 -0
- package/dist/metacognition/index.js.map +1 -0
- package/dist/metacognition/knowledge-action.d.ts +43 -0
- package/dist/metacognition/knowledge-action.d.ts.map +1 -0
- package/dist/metacognition/knowledge-action.js +60 -0
- package/dist/metacognition/knowledge-action.js.map +1 -0
- package/dist/metacognition/knowledge.d.ts +84 -0
- package/dist/metacognition/knowledge.d.ts.map +1 -0
- package/dist/metacognition/knowledge.js +165 -0
- package/dist/metacognition/knowledge.js.map +1 -0
- package/dist/metacognition/obligations.d.ts +82 -0
- package/dist/metacognition/obligations.d.ts.map +1 -0
- package/dist/metacognition/obligations.js +139 -0
- package/dist/metacognition/obligations.js.map +1 -0
- package/dist/metacognition/observation-validity.d.ts +83 -0
- package/dist/metacognition/observation-validity.d.ts.map +1 -0
- package/dist/metacognition/observation-validity.js +118 -0
- package/dist/metacognition/observation-validity.js.map +1 -0
- package/dist/metacognition/observe.d.ts +45 -0
- package/dist/metacognition/observe.d.ts.map +1 -0
- package/dist/metacognition/observe.js +59 -0
- package/dist/metacognition/observe.js.map +1 -0
- package/dist/metacognition/policy.d.ts +53 -0
- package/dist/metacognition/policy.d.ts.map +1 -0
- package/dist/metacognition/policy.js +155 -0
- package/dist/metacognition/policy.js.map +1 -0
- package/dist/metacognition/predictions.d.ts +78 -0
- package/dist/metacognition/predictions.d.ts.map +1 -0
- package/dist/metacognition/predictions.js +181 -0
- package/dist/metacognition/predictions.js.map +1 -0
- package/dist/metacognition/retrieval.d.ts +53 -0
- package/dist/metacognition/retrieval.d.ts.map +1 -0
- package/dist/metacognition/retrieval.js +170 -0
- package/dist/metacognition/retrieval.js.map +1 -0
- package/dist/metacognition/risk.d.ts +48 -0
- package/dist/metacognition/risk.d.ts.map +1 -0
- package/dist/metacognition/risk.js +165 -0
- package/dist/metacognition/risk.js.map +1 -0
- package/dist/metacognition/route-economics.d.ts +95 -0
- package/dist/metacognition/route-economics.d.ts.map +1 -0
- package/dist/metacognition/route-economics.js +110 -0
- package/dist/metacognition/route-economics.js.map +1 -0
- package/dist/metacognition/runtime-bridge.d.ts +54 -0
- package/dist/metacognition/runtime-bridge.d.ts.map +1 -0
- package/dist/metacognition/runtime-bridge.js +140 -0
- package/dist/metacognition/runtime-bridge.js.map +1 -0
- package/dist/metacognition/skills.d.ts +42 -0
- package/dist/metacognition/skills.d.ts.map +1 -0
- package/dist/metacognition/skills.js +220 -0
- package/dist/metacognition/skills.js.map +1 -0
- package/dist/metacognition/state.d.ts +110 -0
- package/dist/metacognition/state.d.ts.map +1 -0
- package/dist/metacognition/state.js +52 -0
- package/dist/metacognition/state.js.map +1 -0
- package/dist/metacognition/validation.d.ts +19 -0
- package/dist/metacognition/validation.d.ts.map +1 -0
- package/dist/metacognition/validation.js +58 -0
- package/dist/metacognition/validation.js.map +1 -0
- package/dist/metacognition/verification.d.ts +93 -0
- package/dist/metacognition/verification.d.ts.map +1 -0
- package/dist/metacognition/verification.js +132 -0
- package/dist/metacognition/verification.js.map +1 -0
- package/dist/metacognition/verifier.d.ts +49 -0
- package/dist/metacognition/verifier.d.ts.map +1 -0
- package/dist/metacognition/verifier.js +88 -0
- package/dist/metacognition/verifier.js.map +1 -0
- package/dist/observation/identity.d.ts +14 -0
- package/dist/observation/identity.d.ts.map +1 -0
- package/dist/observation/identity.js +29 -0
- package/dist/observation/identity.js.map +1 -0
- package/dist/observation/index.d.ts +13 -0
- package/dist/observation/index.d.ts.map +1 -0
- package/dist/observation/index.js +13 -0
- package/dist/observation/index.js.map +1 -0
- package/dist/observation/observe-mode.d.ts +46 -0
- package/dist/observation/observe-mode.d.ts.map +1 -0
- package/dist/observation/observe-mode.js +83 -0
- package/dist/observation/observe-mode.js.map +1 -0
- package/dist/observation/store.d.ts +53 -0
- package/dist/observation/store.d.ts.map +1 -0
- package/dist/observation/store.js +126 -0
- package/dist/observation/store.js.map +1 -0
- package/dist/observation/types.d.ts +72 -0
- package/dist/observation/types.d.ts.map +1 -0
- package/dist/observation/types.js +9 -0
- package/dist/observation/types.js.map +1 -0
- package/dist/observation/view.d.ts +39 -0
- package/dist/observation/view.d.ts.map +1 -0
- package/dist/observation/view.js +173 -0
- package/dist/observation/view.js.map +1 -0
- package/docs/metacognition.md +51 -0
- package/docs/model-catalog-refresh.md +59 -0
- package/docs/providers.md +53 -0
- package/docs/runtime-algorithms.md +107 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +6 -6
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel-session coordination kernel.
|
|
3
|
+
*
|
|
4
|
+
* Pure, deterministic, bounded. Implements the P0 shared by the 2026-09-20
|
|
5
|
+
* coordination design and the Jev audit: bind admission to actual claims, and
|
|
6
|
+
* keep a possibly-live effect's claims held until termination is observed.
|
|
7
|
+
* No IPC, no process control, no persistence.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./awareness.ts";
|
|
10
|
+
export * from "./broker.ts";
|
|
11
|
+
export * from "./integration.ts";
|
|
12
|
+
export * from "./operation.ts";
|
|
13
|
+
export * from "./resource.ts";
|
|
14
|
+
export * from "./session.ts";
|
|
15
|
+
export * from "./types.ts";
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/coordination/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC","sourcesContent":["/**\n * Parallel-session coordination kernel.\n *\n * Pure, deterministic, bounded. Implements the P0 shared by the 2026-09-20\n * coordination design and the Jev audit: bind admission to actual claims, and\n * keep a possibly-live effect's claims held until termination is observed.\n * No IPC, no process control, no persistence.\n */\nexport * from \"./awareness.ts\";\nexport * from \"./broker.ts\";\nexport * from \"./integration.ts\";\nexport * from \"./operation.ts\";\nexport * from \"./resource.ts\";\nexport * from \"./session.ts\";\nexport * from \"./types.ts\";\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel-session coordination kernel.
|
|
3
|
+
*
|
|
4
|
+
* Pure, deterministic, bounded. Implements the P0 shared by the 2026-09-20
|
|
5
|
+
* coordination design and the Jev audit: bind admission to actual claims, and
|
|
6
|
+
* keep a possibly-live effect's claims held until termination is observed.
|
|
7
|
+
* No IPC, no process control, no persistence.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./awareness.js";
|
|
10
|
+
export * from "./broker.js";
|
|
11
|
+
export * from "./integration.js";
|
|
12
|
+
export * from "./operation.js";
|
|
13
|
+
export * from "./resource.js";
|
|
14
|
+
export * from "./session.js";
|
|
15
|
+
export * from "./types.js";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/coordination/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC","sourcesContent":["/**\n * Parallel-session coordination kernel.\n *\n * Pure, deterministic, bounded. Implements the P0 shared by the 2026-09-20\n * coordination design and the Jev audit: bind admission to actual claims, and\n * keep a possibly-live effect's claims held until termination is observed.\n * No IPC, no process control, no persistence.\n */\nexport * from \"./awareness.ts\";\nexport * from \"./broker.ts\";\nexport * from \"./integration.ts\";\nexport * from \"./operation.ts\";\nexport * from \"./resource.ts\";\nexport * from \"./session.ts\";\nexport * from \"./types.ts\";\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidate publication with read-version preconditions and bound receipts.
|
|
3
|
+
*
|
|
4
|
+
* A publication is admitted only when the snapshot that was verified is still
|
|
5
|
+
* the snapshot being published. Three separate facts are checked, because any
|
|
6
|
+
* one of them alone lets a stale candidate through:
|
|
7
|
+
* - every key the proposal read is still at the version it read,
|
|
8
|
+
* - the parent revision has not advanced,
|
|
9
|
+
* - the receipt is bound to this candidate, contract, check set and
|
|
10
|
+
* environment rather than to an earlier composition of them.
|
|
11
|
+
*
|
|
12
|
+
* Absence is a dependency: reading a key that did not exist records version 0,
|
|
13
|
+
* so a later creation invalidates the proposal instead of silently racing it.
|
|
14
|
+
*
|
|
15
|
+
* Bounded model: one accepted snapshot pointer, file effects abstracted as a
|
|
16
|
+
* single update. Real multi-file atomicity needs immutable candidates plus a
|
|
17
|
+
* Git CAS/outbox protocol; this class does not provide that.
|
|
18
|
+
*/
|
|
19
|
+
export type PublicationOutcome = "accepted" | "already-accepted" | "id-collision" | "stale" | "invalid-binding" | "unverified";
|
|
20
|
+
export type WriteValue = string | null;
|
|
21
|
+
export interface ChangeProposal {
|
|
22
|
+
readonly proposalId: string;
|
|
23
|
+
/** Every key touched, with the version observed at propose time. */
|
|
24
|
+
readonly readVersions: readonly (readonly [string, number])[];
|
|
25
|
+
/** `null` deletes the key. */
|
|
26
|
+
readonly writes: readonly (readonly [string, WriteValue])[];
|
|
27
|
+
}
|
|
28
|
+
export interface StagedChange {
|
|
29
|
+
readonly proposal: ChangeProposal;
|
|
30
|
+
readonly parentRevision: number;
|
|
31
|
+
readonly candidateDigest: string;
|
|
32
|
+
readonly contractDigest: string;
|
|
33
|
+
readonly checkDigest: string;
|
|
34
|
+
readonly environmentDigest: string;
|
|
35
|
+
}
|
|
36
|
+
export interface VerificationReceipt {
|
|
37
|
+
readonly candidateDigest: string;
|
|
38
|
+
readonly contractDigest: string;
|
|
39
|
+
readonly checkDigest: string;
|
|
40
|
+
readonly environmentDigest: string;
|
|
41
|
+
readonly verdict: "pass" | "fail" | "inconclusive";
|
|
42
|
+
readonly pendingEffects: number;
|
|
43
|
+
}
|
|
44
|
+
export declare class IntegrationPublisher {
|
|
45
|
+
private readonly values;
|
|
46
|
+
private readonly versions;
|
|
47
|
+
private readonly acceptedFingerprints;
|
|
48
|
+
private currentRevision;
|
|
49
|
+
constructor(initial: Readonly<Record<string, string>>);
|
|
50
|
+
get revision(): number;
|
|
51
|
+
snapshot(): Record<string, string>;
|
|
52
|
+
/** Record the versions of every key this change reads or writes. */
|
|
53
|
+
propose(proposalId: string, reads: readonly string[], writes: Readonly<Record<string, WriteValue>>): ChangeProposal;
|
|
54
|
+
private isCurrent;
|
|
55
|
+
private applied;
|
|
56
|
+
stage(proposal: ChangeProposal, contractDigest?: string, checkDigest?: string, environmentDigest?: string): StagedChange | null;
|
|
57
|
+
publish(staged: StagedChange, receipt: VerificationReceipt): PublicationOutcome;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=integration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../src/coordination/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,MAAM,MAAM,kBAAkB,GAC3B,UAAU,GACV,kBAAkB,GAClB,cAAc,GACd,OAAO,GACP,iBAAiB,GACjB,YAAY,CAAC;AAEhB,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC;AAEvC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;IAC9D,8BAA8B;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;CAC5D;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CAChC;AAWD,qBAAa,oBAAoB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA6B;IAClE,OAAO,CAAC,eAAe,CAAK;IAE5B,YAAY,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAKpD;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEjC;IAED,oEAAoE;IACpE,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,GAAG,cAAc,CAgBlH;IAED,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IASf,KAAK,CACJ,QAAQ,EAAE,cAAc,EACxB,cAAc,SAAgB,EAC9B,WAAW,SAAc,EACzB,iBAAiB,SAAW,GAC1B,YAAY,GAAG,IAAI,CAUrB;IAED,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAyB9E;CACD","sourcesContent":["/**\n * Candidate publication with read-version preconditions and bound receipts.\n *\n * A publication is admitted only when the snapshot that was verified is still\n * the snapshot being published. Three separate facts are checked, because any\n * one of them alone lets a stale candidate through:\n * - every key the proposal read is still at the version it read,\n * - the parent revision has not advanced,\n * - the receipt is bound to this candidate, contract, check set and\n * environment rather than to an earlier composition of them.\n *\n * Absence is a dependency: reading a key that did not exist records version 0,\n * so a later creation invalidates the proposal instead of silently racing it.\n *\n * Bounded model: one accepted snapshot pointer, file effects abstracted as a\n * single update. Real multi-file atomicity needs immutable candidates plus a\n * Git CAS/outbox protocol; this class does not provide that.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { canonical } from \"../metacognition/validation.ts\";\n\nexport type PublicationOutcome =\n\t| \"accepted\"\n\t| \"already-accepted\"\n\t| \"id-collision\"\n\t| \"stale\"\n\t| \"invalid-binding\"\n\t| \"unverified\";\n\nexport type WriteValue = string | null;\n\nexport interface ChangeProposal {\n\treadonly proposalId: string;\n\t/** Every key touched, with the version observed at propose time. */\n\treadonly readVersions: readonly (readonly [string, number])[];\n\t/** `null` deletes the key. */\n\treadonly writes: readonly (readonly [string, WriteValue])[];\n}\n\nexport interface StagedChange {\n\treadonly proposal: ChangeProposal;\n\treadonly parentRevision: number;\n\treadonly candidateDigest: string;\n\treadonly contractDigest: string;\n\treadonly checkDigest: string;\n\treadonly environmentDigest: string;\n}\n\nexport interface VerificationReceipt {\n\treadonly candidateDigest: string;\n\treadonly contractDigest: string;\n\treadonly checkDigest: string;\n\treadonly environmentDigest: string;\n\treadonly verdict: \"pass\" | \"fail\" | \"inconclusive\";\n\treadonly pendingEffects: number;\n}\n\nfunction digest(value: unknown): string {\n\treturn createHash(\"sha256\").update(canonical(value), \"utf8\").digest(\"hex\");\n}\n\nfunction byKey(a: readonly [string, unknown], b: readonly [string, unknown]): number {\n\tif (a[0] < b[0]) return -1;\n\treturn a[0] > b[0] ? 1 : 0;\n}\n\nexport class IntegrationPublisher {\n\tprivate readonly values = new Map<string, string>();\n\tprivate readonly versions = new Map<string, number>();\n\tprivate readonly acceptedFingerprints = new Map<string, string>();\n\tprivate currentRevision = 0;\n\n\tconstructor(initial: Readonly<Record<string, string>>) {\n\t\tfor (const [key, value] of Object.entries(initial)) {\n\t\t\tthis.values.set(key, value);\n\t\t\tthis.versions.set(key, 1);\n\t\t}\n\t}\n\n\tget revision(): number {\n\t\treturn this.currentRevision;\n\t}\n\n\tsnapshot(): Record<string, string> {\n\t\treturn Object.fromEntries(this.values);\n\t}\n\n\t/** Record the versions of every key this change reads or writes. */\n\tpropose(proposalId: string, reads: readonly string[], writes: Readonly<Record<string, WriteValue>>): ChangeProposal {\n\t\tif (typeof proposalId !== \"string\" || proposalId.length === 0) {\n\t\t\tthrow new TypeError(\"proposalId must be a non-empty string\");\n\t\t}\n\t\tconst touched = new Set<string>([...reads, ...Object.keys(writes)]);\n\t\treturn Object.freeze({\n\t\t\tproposalId,\n\t\t\treadVersions: Object.freeze(\n\t\t\t\t[...touched].map((key) => Object.freeze([key, this.versions.get(key) ?? 0] as const)).sort(byKey),\n\t\t\t),\n\t\t\twrites: Object.freeze(\n\t\t\t\tObject.entries(writes)\n\t\t\t\t\t.map(([key, value]) => Object.freeze([key, value] as const))\n\t\t\t\t\t.sort(byKey),\n\t\t\t),\n\t\t});\n\t}\n\n\tprivate isCurrent(proposal: ChangeProposal): boolean {\n\t\tconst declared = new Map(proposal.readVersions);\n\t\tif (proposal.writes.some(([key]) => !declared.has(key))) return false;\n\t\treturn proposal.readVersions.every(([key, version]) => (this.versions.get(key) ?? 0) === version);\n\t}\n\n\tprivate applied(proposal: ChangeProposal): Map<string, string> {\n\t\tconst next = new Map(this.values);\n\t\tfor (const [key, value] of proposal.writes) {\n\t\t\tif (value === null) next.delete(key);\n\t\t\telse next.set(key, value);\n\t\t}\n\t\treturn next;\n\t}\n\n\tstage(\n\t\tproposal: ChangeProposal,\n\t\tcontractDigest = \"contract-v1\",\n\t\tcheckDigest = \"checks-v1\",\n\t\tenvironmentDigest = \"env-v1\",\n\t): StagedChange | null {\n\t\tif (!this.isCurrent(proposal)) return null;\n\t\treturn Object.freeze({\n\t\t\tproposal,\n\t\t\tparentRevision: this.currentRevision,\n\t\t\tcandidateDigest: digest(Object.fromEntries(this.applied(proposal))),\n\t\t\tcontractDigest,\n\t\t\tcheckDigest,\n\t\t\tenvironmentDigest,\n\t\t});\n\t}\n\n\tpublish(staged: StagedChange, receipt: VerificationReceipt): PublicationOutcome {\n\t\tconst proposal = staged.proposal;\n\t\tconst fingerprint = digest({\n\t\t\tproposalId: proposal.proposalId,\n\t\t\treadVersions: proposal.readVersions,\n\t\t\twrites: proposal.writes,\n\t\t});\n\t\tconst seen = this.acceptedFingerprints.get(proposal.proposalId);\n\t\tif (seen !== undefined) return seen === fingerprint ? \"already-accepted\" : \"id-collision\";\n\t\tif (staged.parentRevision !== this.currentRevision || !this.isCurrent(proposal)) return \"stale\";\n\t\tif (!sameBinding(staged, receipt)) return \"invalid-binding\";\n\t\tif (receipt.verdict !== \"pass\" || receipt.pendingEffects !== 0) return \"unverified\";\n\n\t\t// Recompute the candidate: guards a staged payload swapped after verification.\n\t\tconst rebuilt = this.stage(proposal, staged.contractDigest, staged.checkDigest, staged.environmentDigest);\n\t\tif (rebuilt === null || rebuilt.candidateDigest !== staged.candidateDigest) return \"invalid-binding\";\n\n\t\tfor (const [key, value] of proposal.writes) {\n\t\t\tif (value === null) this.values.delete(key);\n\t\t\telse this.values.set(key, value);\n\t\t\tthis.versions.set(key, (this.versions.get(key) ?? 0) + 1);\n\t\t}\n\t\tthis.currentRevision += 1;\n\t\tthis.acceptedFingerprints.set(proposal.proposalId, fingerprint);\n\t\treturn \"accepted\";\n\t}\n}\n\nfunction sameBinding(staged: StagedChange, receipt: VerificationReceipt): boolean {\n\treturn (\n\t\tstaged.candidateDigest === receipt.candidateDigest &&\n\t\tstaged.contractDigest === receipt.contractDigest &&\n\t\tstaged.checkDigest === receipt.checkDigest &&\n\t\tstaged.environmentDigest === receipt.environmentDigest\n\t);\n}\n"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidate publication with read-version preconditions and bound receipts.
|
|
3
|
+
*
|
|
4
|
+
* A publication is admitted only when the snapshot that was verified is still
|
|
5
|
+
* the snapshot being published. Three separate facts are checked, because any
|
|
6
|
+
* one of them alone lets a stale candidate through:
|
|
7
|
+
* - every key the proposal read is still at the version it read,
|
|
8
|
+
* - the parent revision has not advanced,
|
|
9
|
+
* - the receipt is bound to this candidate, contract, check set and
|
|
10
|
+
* environment rather than to an earlier composition of them.
|
|
11
|
+
*
|
|
12
|
+
* Absence is a dependency: reading a key that did not exist records version 0,
|
|
13
|
+
* so a later creation invalidates the proposal instead of silently racing it.
|
|
14
|
+
*
|
|
15
|
+
* Bounded model: one accepted snapshot pointer, file effects abstracted as a
|
|
16
|
+
* single update. Real multi-file atomicity needs immutable candidates plus a
|
|
17
|
+
* Git CAS/outbox protocol; this class does not provide that.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { canonical } from "../metacognition/validation.js";
|
|
21
|
+
function digest(value) {
|
|
22
|
+
return createHash("sha256").update(canonical(value), "utf8").digest("hex");
|
|
23
|
+
}
|
|
24
|
+
function byKey(a, b) {
|
|
25
|
+
if (a[0] < b[0])
|
|
26
|
+
return -1;
|
|
27
|
+
return a[0] > b[0] ? 1 : 0;
|
|
28
|
+
}
|
|
29
|
+
export class IntegrationPublisher {
|
|
30
|
+
values = new Map();
|
|
31
|
+
versions = new Map();
|
|
32
|
+
acceptedFingerprints = new Map();
|
|
33
|
+
currentRevision = 0;
|
|
34
|
+
constructor(initial) {
|
|
35
|
+
for (const [key, value] of Object.entries(initial)) {
|
|
36
|
+
this.values.set(key, value);
|
|
37
|
+
this.versions.set(key, 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
get revision() {
|
|
41
|
+
return this.currentRevision;
|
|
42
|
+
}
|
|
43
|
+
snapshot() {
|
|
44
|
+
return Object.fromEntries(this.values);
|
|
45
|
+
}
|
|
46
|
+
/** Record the versions of every key this change reads or writes. */
|
|
47
|
+
propose(proposalId, reads, writes) {
|
|
48
|
+
if (typeof proposalId !== "string" || proposalId.length === 0) {
|
|
49
|
+
throw new TypeError("proposalId must be a non-empty string");
|
|
50
|
+
}
|
|
51
|
+
const touched = new Set([...reads, ...Object.keys(writes)]);
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
proposalId,
|
|
54
|
+
readVersions: Object.freeze([...touched].map((key) => Object.freeze([key, this.versions.get(key) ?? 0])).sort(byKey)),
|
|
55
|
+
writes: Object.freeze(Object.entries(writes)
|
|
56
|
+
.map(([key, value]) => Object.freeze([key, value]))
|
|
57
|
+
.sort(byKey)),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
isCurrent(proposal) {
|
|
61
|
+
const declared = new Map(proposal.readVersions);
|
|
62
|
+
if (proposal.writes.some(([key]) => !declared.has(key)))
|
|
63
|
+
return false;
|
|
64
|
+
return proposal.readVersions.every(([key, version]) => (this.versions.get(key) ?? 0) === version);
|
|
65
|
+
}
|
|
66
|
+
applied(proposal) {
|
|
67
|
+
const next = new Map(this.values);
|
|
68
|
+
for (const [key, value] of proposal.writes) {
|
|
69
|
+
if (value === null)
|
|
70
|
+
next.delete(key);
|
|
71
|
+
else
|
|
72
|
+
next.set(key, value);
|
|
73
|
+
}
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
stage(proposal, contractDigest = "contract-v1", checkDigest = "checks-v1", environmentDigest = "env-v1") {
|
|
77
|
+
if (!this.isCurrent(proposal))
|
|
78
|
+
return null;
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
proposal,
|
|
81
|
+
parentRevision: this.currentRevision,
|
|
82
|
+
candidateDigest: digest(Object.fromEntries(this.applied(proposal))),
|
|
83
|
+
contractDigest,
|
|
84
|
+
checkDigest,
|
|
85
|
+
environmentDigest,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
publish(staged, receipt) {
|
|
89
|
+
const proposal = staged.proposal;
|
|
90
|
+
const fingerprint = digest({
|
|
91
|
+
proposalId: proposal.proposalId,
|
|
92
|
+
readVersions: proposal.readVersions,
|
|
93
|
+
writes: proposal.writes,
|
|
94
|
+
});
|
|
95
|
+
const seen = this.acceptedFingerprints.get(proposal.proposalId);
|
|
96
|
+
if (seen !== undefined)
|
|
97
|
+
return seen === fingerprint ? "already-accepted" : "id-collision";
|
|
98
|
+
if (staged.parentRevision !== this.currentRevision || !this.isCurrent(proposal))
|
|
99
|
+
return "stale";
|
|
100
|
+
if (!sameBinding(staged, receipt))
|
|
101
|
+
return "invalid-binding";
|
|
102
|
+
if (receipt.verdict !== "pass" || receipt.pendingEffects !== 0)
|
|
103
|
+
return "unverified";
|
|
104
|
+
// Recompute the candidate: guards a staged payload swapped after verification.
|
|
105
|
+
const rebuilt = this.stage(proposal, staged.contractDigest, staged.checkDigest, staged.environmentDigest);
|
|
106
|
+
if (rebuilt === null || rebuilt.candidateDigest !== staged.candidateDigest)
|
|
107
|
+
return "invalid-binding";
|
|
108
|
+
for (const [key, value] of proposal.writes) {
|
|
109
|
+
if (value === null)
|
|
110
|
+
this.values.delete(key);
|
|
111
|
+
else
|
|
112
|
+
this.values.set(key, value);
|
|
113
|
+
this.versions.set(key, (this.versions.get(key) ?? 0) + 1);
|
|
114
|
+
}
|
|
115
|
+
this.currentRevision += 1;
|
|
116
|
+
this.acceptedFingerprints.set(proposal.proposalId, fingerprint);
|
|
117
|
+
return "accepted";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function sameBinding(staged, receipt) {
|
|
121
|
+
return (staged.candidateDigest === receipt.candidateDigest &&
|
|
122
|
+
staged.contractDigest === receipt.contractDigest &&
|
|
123
|
+
staged.checkDigest === receipt.checkDigest &&
|
|
124
|
+
staged.environmentDigest === receipt.environmentDigest);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=integration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../../src/coordination/integration.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Candidate publication with read-version preconditions and bound receipts.\n *\n * A publication is admitted only when the snapshot that was verified is still\n * the snapshot being published. Three separate facts are checked, because any\n * one of them alone lets a stale candidate through:\n * - every key the proposal read is still at the version it read,\n * - the parent revision has not advanced,\n * - the receipt is bound to this candidate, contract, check set and\n * environment rather than to an earlier composition of them.\n *\n * Absence is a dependency: reading a key that did not exist records version 0,\n * so a later creation invalidates the proposal instead of silently racing it.\n *\n * Bounded model: one accepted snapshot pointer, file effects abstracted as a\n * single update. Real multi-file atomicity needs immutable candidates plus a\n * Git CAS/outbox protocol; this class does not provide that.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { canonical } from \"../metacognition/validation.ts\";\n\nexport type PublicationOutcome =\n\t| \"accepted\"\n\t| \"already-accepted\"\n\t| \"id-collision\"\n\t| \"stale\"\n\t| \"invalid-binding\"\n\t| \"unverified\";\n\nexport type WriteValue = string | null;\n\nexport interface ChangeProposal {\n\treadonly proposalId: string;\n\t/** Every key touched, with the version observed at propose time. */\n\treadonly readVersions: readonly (readonly [string, number])[];\n\t/** `null` deletes the key. */\n\treadonly writes: readonly (readonly [string, WriteValue])[];\n}\n\nexport interface StagedChange {\n\treadonly proposal: ChangeProposal;\n\treadonly parentRevision: number;\n\treadonly candidateDigest: string;\n\treadonly contractDigest: string;\n\treadonly checkDigest: string;\n\treadonly environmentDigest: string;\n}\n\nexport interface VerificationReceipt {\n\treadonly candidateDigest: string;\n\treadonly contractDigest: string;\n\treadonly checkDigest: string;\n\treadonly environmentDigest: string;\n\treadonly verdict: \"pass\" | \"fail\" | \"inconclusive\";\n\treadonly pendingEffects: number;\n}\n\nfunction digest(value: unknown): string {\n\treturn createHash(\"sha256\").update(canonical(value), \"utf8\").digest(\"hex\");\n}\n\nfunction byKey(a: readonly [string, unknown], b: readonly [string, unknown]): number {\n\tif (a[0] < b[0]) return -1;\n\treturn a[0] > b[0] ? 1 : 0;\n}\n\nexport class IntegrationPublisher {\n\tprivate readonly values = new Map<string, string>();\n\tprivate readonly versions = new Map<string, number>();\n\tprivate readonly acceptedFingerprints = new Map<string, string>();\n\tprivate currentRevision = 0;\n\n\tconstructor(initial: Readonly<Record<string, string>>) {\n\t\tfor (const [key, value] of Object.entries(initial)) {\n\t\t\tthis.values.set(key, value);\n\t\t\tthis.versions.set(key, 1);\n\t\t}\n\t}\n\n\tget revision(): number {\n\t\treturn this.currentRevision;\n\t}\n\n\tsnapshot(): Record<string, string> {\n\t\treturn Object.fromEntries(this.values);\n\t}\n\n\t/** Record the versions of every key this change reads or writes. */\n\tpropose(proposalId: string, reads: readonly string[], writes: Readonly<Record<string, WriteValue>>): ChangeProposal {\n\t\tif (typeof proposalId !== \"string\" || proposalId.length === 0) {\n\t\t\tthrow new TypeError(\"proposalId must be a non-empty string\");\n\t\t}\n\t\tconst touched = new Set<string>([...reads, ...Object.keys(writes)]);\n\t\treturn Object.freeze({\n\t\t\tproposalId,\n\t\t\treadVersions: Object.freeze(\n\t\t\t\t[...touched].map((key) => Object.freeze([key, this.versions.get(key) ?? 0] as const)).sort(byKey),\n\t\t\t),\n\t\t\twrites: Object.freeze(\n\t\t\t\tObject.entries(writes)\n\t\t\t\t\t.map(([key, value]) => Object.freeze([key, value] as const))\n\t\t\t\t\t.sort(byKey),\n\t\t\t),\n\t\t});\n\t}\n\n\tprivate isCurrent(proposal: ChangeProposal): boolean {\n\t\tconst declared = new Map(proposal.readVersions);\n\t\tif (proposal.writes.some(([key]) => !declared.has(key))) return false;\n\t\treturn proposal.readVersions.every(([key, version]) => (this.versions.get(key) ?? 0) === version);\n\t}\n\n\tprivate applied(proposal: ChangeProposal): Map<string, string> {\n\t\tconst next = new Map(this.values);\n\t\tfor (const [key, value] of proposal.writes) {\n\t\t\tif (value === null) next.delete(key);\n\t\t\telse next.set(key, value);\n\t\t}\n\t\treturn next;\n\t}\n\n\tstage(\n\t\tproposal: ChangeProposal,\n\t\tcontractDigest = \"contract-v1\",\n\t\tcheckDigest = \"checks-v1\",\n\t\tenvironmentDigest = \"env-v1\",\n\t): StagedChange | null {\n\t\tif (!this.isCurrent(proposal)) return null;\n\t\treturn Object.freeze({\n\t\t\tproposal,\n\t\t\tparentRevision: this.currentRevision,\n\t\t\tcandidateDigest: digest(Object.fromEntries(this.applied(proposal))),\n\t\t\tcontractDigest,\n\t\t\tcheckDigest,\n\t\t\tenvironmentDigest,\n\t\t});\n\t}\n\n\tpublish(staged: StagedChange, receipt: VerificationReceipt): PublicationOutcome {\n\t\tconst proposal = staged.proposal;\n\t\tconst fingerprint = digest({\n\t\t\tproposalId: proposal.proposalId,\n\t\t\treadVersions: proposal.readVersions,\n\t\t\twrites: proposal.writes,\n\t\t});\n\t\tconst seen = this.acceptedFingerprints.get(proposal.proposalId);\n\t\tif (seen !== undefined) return seen === fingerprint ? \"already-accepted\" : \"id-collision\";\n\t\tif (staged.parentRevision !== this.currentRevision || !this.isCurrent(proposal)) return \"stale\";\n\t\tif (!sameBinding(staged, receipt)) return \"invalid-binding\";\n\t\tif (receipt.verdict !== \"pass\" || receipt.pendingEffects !== 0) return \"unverified\";\n\n\t\t// Recompute the candidate: guards a staged payload swapped after verification.\n\t\tconst rebuilt = this.stage(proposal, staged.contractDigest, staged.checkDigest, staged.environmentDigest);\n\t\tif (rebuilt === null || rebuilt.candidateDigest !== staged.candidateDigest) return \"invalid-binding\";\n\n\t\tfor (const [key, value] of proposal.writes) {\n\t\t\tif (value === null) this.values.delete(key);\n\t\t\telse this.values.set(key, value);\n\t\t\tthis.versions.set(key, (this.versions.get(key) ?? 0) + 1);\n\t\t}\n\t\tthis.currentRevision += 1;\n\t\tthis.acceptedFingerprints.set(proposal.proposalId, fingerprint);\n\t\treturn \"accepted\";\n\t}\n}\n\nfunction sameBinding(staged: StagedChange, receipt: VerificationReceipt): boolean {\n\treturn (\n\t\tstaged.candidateDigest === receipt.candidateDigest &&\n\t\tstaged.contractDigest === receipt.contractDigest &&\n\t\tstaged.checkDigest === receipt.checkDigest &&\n\t\tstaged.environmentDigest === receipt.environmentDigest\n\t);\n}\n"]}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorized operation lifecycle for GUI-driving adapters (Jev audit A6).
|
|
3
|
+
*
|
|
4
|
+
* Two reproduced defects shape this module:
|
|
5
|
+
* F01 — an adapter re-interpreted a free-text instruction at execution time,
|
|
6
|
+
* so an approved candidate and the executed action could differ. Here
|
|
7
|
+
* dispatch consumes a recorded observation binding, never a sentence.
|
|
8
|
+
* F02 — a cancellation signal was accepted and then ignored across the
|
|
9
|
+
* approval await, so a cancelled operation still dispatched. Here the
|
|
10
|
+
* permit is re-evaluated at the dispatch boundary, and cancellation
|
|
11
|
+
* after dispatch can never be reported as cancelled-before-dispatch.
|
|
12
|
+
*
|
|
13
|
+
* Pure state machine: no browser, no driver, no timers. "Dispatched" means the
|
|
14
|
+
* command left this process, which is not evidence about the remote effect —
|
|
15
|
+
* that is why `outcome-unknown` is a first-class state rather than a failure.
|
|
16
|
+
*/
|
|
17
|
+
import type { Sequence } from "./types.ts";
|
|
18
|
+
export type OperationState = "created" | "observed" | "proposed" | "authorized" | "dispatched" | "applied" | "verified" | "inspection-required" | "failed-confirmed" | "outcome-unknown" | "cancelled-before-dispatch";
|
|
19
|
+
/** Outcomes a driver may report once a command has left the process. */
|
|
20
|
+
export type SettledOutcome = "applied" | "failed-confirmed" | "outcome-unknown";
|
|
21
|
+
/**
|
|
22
|
+
* Host-held reference to one observed candidate.
|
|
23
|
+
*
|
|
24
|
+
* The adapter receives these identifiers, not an instruction string, so the
|
|
25
|
+
* action that was approved is the action that runs.
|
|
26
|
+
*/
|
|
27
|
+
export interface ObservationBinding {
|
|
28
|
+
readonly observationId: string;
|
|
29
|
+
readonly actionId: string;
|
|
30
|
+
readonly targetId: string;
|
|
31
|
+
/** Document/DOM generation the candidate was observed against. */
|
|
32
|
+
readonly documentGeneration: Sequence;
|
|
33
|
+
}
|
|
34
|
+
export type PermitDenial = "cancelled" | "policy-denied" | "approval-mismatch" | "stale-lease" | "observation-invalid" | "budget-exhausted";
|
|
35
|
+
export type PermitDecision = {
|
|
36
|
+
readonly allowed: true;
|
|
37
|
+
} | {
|
|
38
|
+
readonly allowed: false;
|
|
39
|
+
readonly reason: PermitDenial;
|
|
40
|
+
};
|
|
41
|
+
export interface PermitInput {
|
|
42
|
+
readonly policyAllowed: boolean;
|
|
43
|
+
/** Digest of the intent actually about to run. */
|
|
44
|
+
readonly intentDigest: string;
|
|
45
|
+
/** Digest the operator approved; a post-approval edit breaks this equality. */
|
|
46
|
+
readonly approvalDigest: string;
|
|
47
|
+
readonly policyVersion: string;
|
|
48
|
+
readonly approvalPolicyVersion: string;
|
|
49
|
+
readonly leaseGeneration: Sequence;
|
|
50
|
+
readonly currentLeaseGeneration: Sequence;
|
|
51
|
+
readonly observationValid: boolean;
|
|
52
|
+
readonly cancelled: boolean;
|
|
53
|
+
readonly budgetAvailable: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Permit(I) = PolicyAllowed ∧ ApprovalMatches ∧ LeaseCurrent ∧ ObservationValid
|
|
57
|
+
* ∧ ¬Cancelled ∧ BudgetAvailable.
|
|
58
|
+
*
|
|
59
|
+
* Cancellation is reported first: when a run is cancelled, saying "budget
|
|
60
|
+
* exhausted" would send the operator looking for the wrong problem.
|
|
61
|
+
*/
|
|
62
|
+
export declare function evaluatePermit(input: PermitInput): PermitDecision;
|
|
63
|
+
export declare class OperationLifecycle {
|
|
64
|
+
readonly operationId: string;
|
|
65
|
+
private current;
|
|
66
|
+
private readonly states;
|
|
67
|
+
private observations;
|
|
68
|
+
private selected;
|
|
69
|
+
private hasDispatched;
|
|
70
|
+
private cancelRequested;
|
|
71
|
+
constructor(operationId: string);
|
|
72
|
+
get state(): OperationState;
|
|
73
|
+
get history(): readonly OperationState[];
|
|
74
|
+
/** True once a command left this process; it says nothing about the effect. */
|
|
75
|
+
get dispatched(): boolean;
|
|
76
|
+
get cancellationRequested(): boolean;
|
|
77
|
+
private enter;
|
|
78
|
+
private assertOpen;
|
|
79
|
+
/** Record the host-held candidate set. Without this, nothing can dispatch. */
|
|
80
|
+
observe(candidates: readonly ObservationBinding[]): void;
|
|
81
|
+
/** Select one observed candidate. Free-text re-interpretation has no path here. */
|
|
82
|
+
propose(candidate: ObservationBinding): void;
|
|
83
|
+
authorize(permit: PermitInput): PermitDecision;
|
|
84
|
+
/**
|
|
85
|
+
* Re-evaluate the permit and hand the command to the driver.
|
|
86
|
+
*
|
|
87
|
+
* The second evaluation is the point of the audit finding: approval and
|
|
88
|
+
* dispatch are separated by awaits (queueing, the operator prompt, browser
|
|
89
|
+
* startup), and state observed before those awaits is not state now.
|
|
90
|
+
*/
|
|
91
|
+
dispatch(permit: PermitInput): PermitDecision;
|
|
92
|
+
private applyPreDispatchDenial;
|
|
93
|
+
/** Driver-reported outcome. Only valid once the command actually left. */
|
|
94
|
+
settle(outcome: SettledOutcome): void;
|
|
95
|
+
/** Explicit postcondition check. A driver's return value is not this. */
|
|
96
|
+
verify(postconditionMet: boolean): void;
|
|
97
|
+
/**
|
|
98
|
+
* Request cancellation.
|
|
99
|
+
*
|
|
100
|
+
* Before dispatch this is terminal. After dispatch it only records the
|
|
101
|
+
* request: the command is already outside this process, so the real outcome
|
|
102
|
+
* has to be settled separately and stays `outcome-unknown` until it is.
|
|
103
|
+
*/
|
|
104
|
+
cancel(): void;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=operation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation.d.ts","sourceRoot":"","sources":["../../src/coordination/operation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorized operation lifecycle for GUI-driving adapters (Jev audit A6).\n *\n * Two reproduced defects shape this module:\n * F01 — an adapter re-interpreted a free-text instruction at execution time,\n * so an approved candidate and the executed action could differ. Here\n * dispatch consumes a recorded observation binding, never a sentence.\n * F02 — a cancellation signal was accepted and then ignored across the\n * approval await, so a cancelled operation still dispatched. Here the\n * permit is re-evaluated at the dispatch boundary, and cancellation\n * after dispatch can never be reported as cancelled-before-dispatch.\n *\n * Pure state machine: no browser, no driver, no timers. \"Dispatched\" means the\n * command left this process, which is not evidence about the remote effect —\n * that is why `outcome-unknown` is a first-class state rather than a failure.\n */\n\nimport type { Sequence } from \"./types.ts\";\n\nexport type OperationState =\n\t| \"created\"\n\t| \"observed\"\n\t| \"proposed\"\n\t| \"authorized\"\n\t| \"dispatched\"\n\t| \"applied\"\n\t| \"verified\"\n\t| \"inspection-required\"\n\t| \"failed-confirmed\"\n\t| \"outcome-unknown\"\n\t| \"cancelled-before-dispatch\";\n\n/** Outcomes a driver may report once a command has left the process. */\nexport type SettledOutcome = \"applied\" | \"failed-confirmed\" | \"outcome-unknown\";\n\nconst TERMINAL_STATES: ReadonlySet<OperationState> = new Set<OperationState>([\n\t\"verified\",\n\t\"inspection-required\",\n\t\"failed-confirmed\",\n\t\"cancelled-before-dispatch\",\n]);\n\n/**\n * Host-held reference to one observed candidate.\n *\n * The adapter receives these identifiers, not an instruction string, so the\n * action that was approved is the action that runs.\n */\nexport interface ObservationBinding {\n\treadonly observationId: string;\n\treadonly actionId: string;\n\treadonly targetId: string;\n\t/** Document/DOM generation the candidate was observed against. */\n\treadonly documentGeneration: Sequence;\n}\n\nexport type PermitDenial =\n\t| \"cancelled\"\n\t| \"policy-denied\"\n\t| \"approval-mismatch\"\n\t| \"stale-lease\"\n\t| \"observation-invalid\"\n\t| \"budget-exhausted\";\n\nexport type PermitDecision = { readonly allowed: true } | { readonly allowed: false; readonly reason: PermitDenial };\n\nexport interface PermitInput {\n\treadonly policyAllowed: boolean;\n\t/** Digest of the intent actually about to run. */\n\treadonly intentDigest: string;\n\t/** Digest the operator approved; a post-approval edit breaks this equality. */\n\treadonly approvalDigest: string;\n\treadonly policyVersion: string;\n\treadonly approvalPolicyVersion: string;\n\treadonly leaseGeneration: Sequence;\n\treadonly currentLeaseGeneration: Sequence;\n\treadonly observationValid: boolean;\n\treadonly cancelled: boolean;\n\treadonly budgetAvailable: boolean;\n}\n\n/**\n * Permit(I) = PolicyAllowed ∧ ApprovalMatches ∧ LeaseCurrent ∧ ObservationValid\n * ∧ ¬Cancelled ∧ BudgetAvailable.\n *\n * Cancellation is reported first: when a run is cancelled, saying \"budget\n * exhausted\" would send the operator looking for the wrong problem.\n */\nexport function evaluatePermit(input: PermitInput): PermitDecision {\n\tif (input.cancelled) return { allowed: false, reason: \"cancelled\" };\n\tif (!input.policyAllowed) return { allowed: false, reason: \"policy-denied\" };\n\tif (input.approvalDigest !== input.intentDigest || input.approvalPolicyVersion !== input.policyVersion) {\n\t\treturn { allowed: false, reason: \"approval-mismatch\" };\n\t}\n\tif (input.leaseGeneration !== input.currentLeaseGeneration) return { allowed: false, reason: \"stale-lease\" };\n\tif (!input.observationValid) return { allowed: false, reason: \"observation-invalid\" };\n\tif (!input.budgetAvailable) return { allowed: false, reason: \"budget-exhausted\" };\n\treturn { allowed: true };\n}\n\nexport class OperationLifecycle {\n\treadonly operationId: string;\n\tprivate current: OperationState = \"created\";\n\tprivate readonly states: OperationState[] = [\"created\"];\n\tprivate observations: readonly ObservationBinding[] = [];\n\tprivate selected: ObservationBinding | undefined;\n\tprivate hasDispatched = false;\n\tprivate cancelRequested = false;\n\n\tconstructor(operationId: string) {\n\t\tif (typeof operationId !== \"string\" || operationId.length === 0) {\n\t\t\tthrow new TypeError(\"operationId must be a non-empty string\");\n\t\t}\n\t\tthis.operationId = operationId;\n\t}\n\n\tget state(): OperationState {\n\t\treturn this.current;\n\t}\n\n\tget history(): readonly OperationState[] {\n\t\treturn [...this.states];\n\t}\n\n\t/** True once a command left this process; it says nothing about the effect. */\n\tget dispatched(): boolean {\n\t\treturn this.hasDispatched;\n\t}\n\n\tget cancellationRequested(): boolean {\n\t\treturn this.cancelRequested;\n\t}\n\n\tprivate enter(next: OperationState): void {\n\t\tthis.current = next;\n\t\tthis.states.push(next);\n\t}\n\n\tprivate assertOpen(): void {\n\t\tif (TERMINAL_STATES.has(this.current)) {\n\t\t\tthrow new Error(`operation ${this.operationId} is terminal in state ${this.current}`);\n\t\t}\n\t}\n\n\t/** Record the host-held candidate set. Without this, nothing can dispatch. */\n\tobserve(candidates: readonly ObservationBinding[]): void {\n\t\tthis.assertOpen();\n\t\tif (candidates.length === 0) throw new TypeError(\"observation must yield at least one candidate\");\n\t\tthis.observations = [...candidates];\n\t\tthis.enter(\"observed\");\n\t}\n\n\t/** Select one observed candidate. Free-text re-interpretation has no path here. */\n\tpropose(candidate: ObservationBinding): void {\n\t\tthis.assertOpen();\n\t\tif (this.observations.length === 0) {\n\t\t\tthrow new Error(\"cannot propose before observe: the candidate set is unbound\");\n\t\t}\n\t\tconst match = this.observations.find(\n\t\t\t(o) =>\n\t\t\t\to.observationId === candidate.observationId &&\n\t\t\t\to.actionId === candidate.actionId &&\n\t\t\t\to.targetId === candidate.targetId &&\n\t\t\t\to.documentGeneration === candidate.documentGeneration,\n\t\t);\n\t\tif (match === undefined) throw new Error(\"proposed action is not among the recorded observations\");\n\t\tthis.selected = match;\n\t\tthis.enter(\"proposed\");\n\t}\n\n\tauthorize(permit: PermitInput): PermitDecision {\n\t\tthis.assertOpen();\n\t\tif (this.current !== \"proposed\") throw new Error(\"authorize requires a proposed operation\");\n\t\tconst decision = evaluatePermit(permit);\n\t\tif (!decision.allowed) {\n\t\t\tthis.applyPreDispatchDenial(decision.reason);\n\t\t\treturn decision;\n\t\t}\n\t\tthis.enter(\"authorized\");\n\t\treturn decision;\n\t}\n\n\t/**\n\t * Re-evaluate the permit and hand the command to the driver.\n\t *\n\t * The second evaluation is the point of the audit finding: approval and\n\t * dispatch are separated by awaits (queueing, the operator prompt, browser\n\t * startup), and state observed before those awaits is not state now.\n\t */\n\tdispatch(permit: PermitInput): PermitDecision {\n\t\tthis.assertOpen();\n\t\tif (this.current !== \"authorized\") throw new Error(\"dispatch requires an authorized operation\");\n\t\tif (this.selected === undefined) throw new Error(\"dispatch requires a bound observation\");\n\t\tconst decision = evaluatePermit(permit);\n\t\tif (!decision.allowed) {\n\t\t\tthis.applyPreDispatchDenial(decision.reason);\n\t\t\treturn decision;\n\t\t}\n\t\tthis.hasDispatched = true;\n\t\tthis.enter(\"dispatched\");\n\t\treturn decision;\n\t}\n\n\tprivate applyPreDispatchDenial(reason: PermitDenial): void {\n\t\tif (reason === \"cancelled\") {\n\t\t\tthis.cancelRequested = true;\n\t\t\tthis.enter(\"cancelled-before-dispatch\");\n\t\t}\n\t}\n\n\t/** Driver-reported outcome. Only valid once the command actually left. */\n\tsettle(outcome: SettledOutcome): void {\n\t\tif (!this.hasDispatched) throw new Error(\"settle requires a dispatched operation\");\n\t\tif (this.current === \"verified\" || this.current === \"inspection-required\") {\n\t\t\tthrow new Error(\"outcome already verified\");\n\t\t}\n\t\tthis.enter(outcome);\n\t}\n\n\t/** Explicit postcondition check. A driver's return value is not this. */\n\tverify(postconditionMet: boolean): void {\n\t\tif (this.current !== \"applied\") throw new Error(\"verify requires an applied outcome\");\n\t\tthis.enter(postconditionMet ? \"verified\" : \"inspection-required\");\n\t}\n\n\t/**\n\t * Request cancellation.\n\t *\n\t * Before dispatch this is terminal. After dispatch it only records the\n\t * request: the command is already outside this process, so the real outcome\n\t * has to be settled separately and stays `outcome-unknown` until it is.\n\t */\n\tcancel(): void {\n\t\tthis.cancelRequested = true;\n\t\tif (TERMINAL_STATES.has(this.current)) return;\n\t\tif (!this.hasDispatched) {\n\t\t\tthis.enter(\"cancelled-before-dispatch\");\n\t\t\treturn;\n\t\t}\n\t\tif (this.current === \"dispatched\") this.enter(\"outcome-unknown\");\n\t}\n}\n"]}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorized operation lifecycle for GUI-driving adapters (Jev audit A6).
|
|
3
|
+
*
|
|
4
|
+
* Two reproduced defects shape this module:
|
|
5
|
+
* F01 — an adapter re-interpreted a free-text instruction at execution time,
|
|
6
|
+
* so an approved candidate and the executed action could differ. Here
|
|
7
|
+
* dispatch consumes a recorded observation binding, never a sentence.
|
|
8
|
+
* F02 — a cancellation signal was accepted and then ignored across the
|
|
9
|
+
* approval await, so a cancelled operation still dispatched. Here the
|
|
10
|
+
* permit is re-evaluated at the dispatch boundary, and cancellation
|
|
11
|
+
* after dispatch can never be reported as cancelled-before-dispatch.
|
|
12
|
+
*
|
|
13
|
+
* Pure state machine: no browser, no driver, no timers. "Dispatched" means the
|
|
14
|
+
* command left this process, which is not evidence about the remote effect —
|
|
15
|
+
* that is why `outcome-unknown` is a first-class state rather than a failure.
|
|
16
|
+
*/
|
|
17
|
+
const TERMINAL_STATES = new Set([
|
|
18
|
+
"verified",
|
|
19
|
+
"inspection-required",
|
|
20
|
+
"failed-confirmed",
|
|
21
|
+
"cancelled-before-dispatch",
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Permit(I) = PolicyAllowed ∧ ApprovalMatches ∧ LeaseCurrent ∧ ObservationValid
|
|
25
|
+
* ∧ ¬Cancelled ∧ BudgetAvailable.
|
|
26
|
+
*
|
|
27
|
+
* Cancellation is reported first: when a run is cancelled, saying "budget
|
|
28
|
+
* exhausted" would send the operator looking for the wrong problem.
|
|
29
|
+
*/
|
|
30
|
+
export function evaluatePermit(input) {
|
|
31
|
+
if (input.cancelled)
|
|
32
|
+
return { allowed: false, reason: "cancelled" };
|
|
33
|
+
if (!input.policyAllowed)
|
|
34
|
+
return { allowed: false, reason: "policy-denied" };
|
|
35
|
+
if (input.approvalDigest !== input.intentDigest || input.approvalPolicyVersion !== input.policyVersion) {
|
|
36
|
+
return { allowed: false, reason: "approval-mismatch" };
|
|
37
|
+
}
|
|
38
|
+
if (input.leaseGeneration !== input.currentLeaseGeneration)
|
|
39
|
+
return { allowed: false, reason: "stale-lease" };
|
|
40
|
+
if (!input.observationValid)
|
|
41
|
+
return { allowed: false, reason: "observation-invalid" };
|
|
42
|
+
if (!input.budgetAvailable)
|
|
43
|
+
return { allowed: false, reason: "budget-exhausted" };
|
|
44
|
+
return { allowed: true };
|
|
45
|
+
}
|
|
46
|
+
export class OperationLifecycle {
|
|
47
|
+
operationId;
|
|
48
|
+
current = "created";
|
|
49
|
+
states = ["created"];
|
|
50
|
+
observations = [];
|
|
51
|
+
selected;
|
|
52
|
+
hasDispatched = false;
|
|
53
|
+
cancelRequested = false;
|
|
54
|
+
constructor(operationId) {
|
|
55
|
+
if (typeof operationId !== "string" || operationId.length === 0) {
|
|
56
|
+
throw new TypeError("operationId must be a non-empty string");
|
|
57
|
+
}
|
|
58
|
+
this.operationId = operationId;
|
|
59
|
+
}
|
|
60
|
+
get state() {
|
|
61
|
+
return this.current;
|
|
62
|
+
}
|
|
63
|
+
get history() {
|
|
64
|
+
return [...this.states];
|
|
65
|
+
}
|
|
66
|
+
/** True once a command left this process; it says nothing about the effect. */
|
|
67
|
+
get dispatched() {
|
|
68
|
+
return this.hasDispatched;
|
|
69
|
+
}
|
|
70
|
+
get cancellationRequested() {
|
|
71
|
+
return this.cancelRequested;
|
|
72
|
+
}
|
|
73
|
+
enter(next) {
|
|
74
|
+
this.current = next;
|
|
75
|
+
this.states.push(next);
|
|
76
|
+
}
|
|
77
|
+
assertOpen() {
|
|
78
|
+
if (TERMINAL_STATES.has(this.current)) {
|
|
79
|
+
throw new Error(`operation ${this.operationId} is terminal in state ${this.current}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Record the host-held candidate set. Without this, nothing can dispatch. */
|
|
83
|
+
observe(candidates) {
|
|
84
|
+
this.assertOpen();
|
|
85
|
+
if (candidates.length === 0)
|
|
86
|
+
throw new TypeError("observation must yield at least one candidate");
|
|
87
|
+
this.observations = [...candidates];
|
|
88
|
+
this.enter("observed");
|
|
89
|
+
}
|
|
90
|
+
/** Select one observed candidate. Free-text re-interpretation has no path here. */
|
|
91
|
+
propose(candidate) {
|
|
92
|
+
this.assertOpen();
|
|
93
|
+
if (this.observations.length === 0) {
|
|
94
|
+
throw new Error("cannot propose before observe: the candidate set is unbound");
|
|
95
|
+
}
|
|
96
|
+
const match = this.observations.find((o) => o.observationId === candidate.observationId &&
|
|
97
|
+
o.actionId === candidate.actionId &&
|
|
98
|
+
o.targetId === candidate.targetId &&
|
|
99
|
+
o.documentGeneration === candidate.documentGeneration);
|
|
100
|
+
if (match === undefined)
|
|
101
|
+
throw new Error("proposed action is not among the recorded observations");
|
|
102
|
+
this.selected = match;
|
|
103
|
+
this.enter("proposed");
|
|
104
|
+
}
|
|
105
|
+
authorize(permit) {
|
|
106
|
+
this.assertOpen();
|
|
107
|
+
if (this.current !== "proposed")
|
|
108
|
+
throw new Error("authorize requires a proposed operation");
|
|
109
|
+
const decision = evaluatePermit(permit);
|
|
110
|
+
if (!decision.allowed) {
|
|
111
|
+
this.applyPreDispatchDenial(decision.reason);
|
|
112
|
+
return decision;
|
|
113
|
+
}
|
|
114
|
+
this.enter("authorized");
|
|
115
|
+
return decision;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Re-evaluate the permit and hand the command to the driver.
|
|
119
|
+
*
|
|
120
|
+
* The second evaluation is the point of the audit finding: approval and
|
|
121
|
+
* dispatch are separated by awaits (queueing, the operator prompt, browser
|
|
122
|
+
* startup), and state observed before those awaits is not state now.
|
|
123
|
+
*/
|
|
124
|
+
dispatch(permit) {
|
|
125
|
+
this.assertOpen();
|
|
126
|
+
if (this.current !== "authorized")
|
|
127
|
+
throw new Error("dispatch requires an authorized operation");
|
|
128
|
+
if (this.selected === undefined)
|
|
129
|
+
throw new Error("dispatch requires a bound observation");
|
|
130
|
+
const decision = evaluatePermit(permit);
|
|
131
|
+
if (!decision.allowed) {
|
|
132
|
+
this.applyPreDispatchDenial(decision.reason);
|
|
133
|
+
return decision;
|
|
134
|
+
}
|
|
135
|
+
this.hasDispatched = true;
|
|
136
|
+
this.enter("dispatched");
|
|
137
|
+
return decision;
|
|
138
|
+
}
|
|
139
|
+
applyPreDispatchDenial(reason) {
|
|
140
|
+
if (reason === "cancelled") {
|
|
141
|
+
this.cancelRequested = true;
|
|
142
|
+
this.enter("cancelled-before-dispatch");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Driver-reported outcome. Only valid once the command actually left. */
|
|
146
|
+
settle(outcome) {
|
|
147
|
+
if (!this.hasDispatched)
|
|
148
|
+
throw new Error("settle requires a dispatched operation");
|
|
149
|
+
if (this.current === "verified" || this.current === "inspection-required") {
|
|
150
|
+
throw new Error("outcome already verified");
|
|
151
|
+
}
|
|
152
|
+
this.enter(outcome);
|
|
153
|
+
}
|
|
154
|
+
/** Explicit postcondition check. A driver's return value is not this. */
|
|
155
|
+
verify(postconditionMet) {
|
|
156
|
+
if (this.current !== "applied")
|
|
157
|
+
throw new Error("verify requires an applied outcome");
|
|
158
|
+
this.enter(postconditionMet ? "verified" : "inspection-required");
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Request cancellation.
|
|
162
|
+
*
|
|
163
|
+
* Before dispatch this is terminal. After dispatch it only records the
|
|
164
|
+
* request: the command is already outside this process, so the real outcome
|
|
165
|
+
* has to be settled separately and stays `outcome-unknown` until it is.
|
|
166
|
+
*/
|
|
167
|
+
cancel() {
|
|
168
|
+
this.cancelRequested = true;
|
|
169
|
+
if (TERMINAL_STATES.has(this.current))
|
|
170
|
+
return;
|
|
171
|
+
if (!this.hasDispatched) {
|
|
172
|
+
this.enter("cancelled-before-dispatch");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (this.current === "dispatched")
|
|
176
|
+
this.enter("outcome-unknown");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=operation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation.js","sourceRoot":"","sources":["../../src/coordination/operation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorized operation lifecycle for GUI-driving adapters (Jev audit A6).\n *\n * Two reproduced defects shape this module:\n * F01 — an adapter re-interpreted a free-text instruction at execution time,\n * so an approved candidate and the executed action could differ. Here\n * dispatch consumes a recorded observation binding, never a sentence.\n * F02 — a cancellation signal was accepted and then ignored across the\n * approval await, so a cancelled operation still dispatched. Here the\n * permit is re-evaluated at the dispatch boundary, and cancellation\n * after dispatch can never be reported as cancelled-before-dispatch.\n *\n * Pure state machine: no browser, no driver, no timers. \"Dispatched\" means the\n * command left this process, which is not evidence about the remote effect —\n * that is why `outcome-unknown` is a first-class state rather than a failure.\n */\n\nimport type { Sequence } from \"./types.ts\";\n\nexport type OperationState =\n\t| \"created\"\n\t| \"observed\"\n\t| \"proposed\"\n\t| \"authorized\"\n\t| \"dispatched\"\n\t| \"applied\"\n\t| \"verified\"\n\t| \"inspection-required\"\n\t| \"failed-confirmed\"\n\t| \"outcome-unknown\"\n\t| \"cancelled-before-dispatch\";\n\n/** Outcomes a driver may report once a command has left the process. */\nexport type SettledOutcome = \"applied\" | \"failed-confirmed\" | \"outcome-unknown\";\n\nconst TERMINAL_STATES: ReadonlySet<OperationState> = new Set<OperationState>([\n\t\"verified\",\n\t\"inspection-required\",\n\t\"failed-confirmed\",\n\t\"cancelled-before-dispatch\",\n]);\n\n/**\n * Host-held reference to one observed candidate.\n *\n * The adapter receives these identifiers, not an instruction string, so the\n * action that was approved is the action that runs.\n */\nexport interface ObservationBinding {\n\treadonly observationId: string;\n\treadonly actionId: string;\n\treadonly targetId: string;\n\t/** Document/DOM generation the candidate was observed against. */\n\treadonly documentGeneration: Sequence;\n}\n\nexport type PermitDenial =\n\t| \"cancelled\"\n\t| \"policy-denied\"\n\t| \"approval-mismatch\"\n\t| \"stale-lease\"\n\t| \"observation-invalid\"\n\t| \"budget-exhausted\";\n\nexport type PermitDecision = { readonly allowed: true } | { readonly allowed: false; readonly reason: PermitDenial };\n\nexport interface PermitInput {\n\treadonly policyAllowed: boolean;\n\t/** Digest of the intent actually about to run. */\n\treadonly intentDigest: string;\n\t/** Digest the operator approved; a post-approval edit breaks this equality. */\n\treadonly approvalDigest: string;\n\treadonly policyVersion: string;\n\treadonly approvalPolicyVersion: string;\n\treadonly leaseGeneration: Sequence;\n\treadonly currentLeaseGeneration: Sequence;\n\treadonly observationValid: boolean;\n\treadonly cancelled: boolean;\n\treadonly budgetAvailable: boolean;\n}\n\n/**\n * Permit(I) = PolicyAllowed ∧ ApprovalMatches ∧ LeaseCurrent ∧ ObservationValid\n * ∧ ¬Cancelled ∧ BudgetAvailable.\n *\n * Cancellation is reported first: when a run is cancelled, saying \"budget\n * exhausted\" would send the operator looking for the wrong problem.\n */\nexport function evaluatePermit(input: PermitInput): PermitDecision {\n\tif (input.cancelled) return { allowed: false, reason: \"cancelled\" };\n\tif (!input.policyAllowed) return { allowed: false, reason: \"policy-denied\" };\n\tif (input.approvalDigest !== input.intentDigest || input.approvalPolicyVersion !== input.policyVersion) {\n\t\treturn { allowed: false, reason: \"approval-mismatch\" };\n\t}\n\tif (input.leaseGeneration !== input.currentLeaseGeneration) return { allowed: false, reason: \"stale-lease\" };\n\tif (!input.observationValid) return { allowed: false, reason: \"observation-invalid\" };\n\tif (!input.budgetAvailable) return { allowed: false, reason: \"budget-exhausted\" };\n\treturn { allowed: true };\n}\n\nexport class OperationLifecycle {\n\treadonly operationId: string;\n\tprivate current: OperationState = \"created\";\n\tprivate readonly states: OperationState[] = [\"created\"];\n\tprivate observations: readonly ObservationBinding[] = [];\n\tprivate selected: ObservationBinding | undefined;\n\tprivate hasDispatched = false;\n\tprivate cancelRequested = false;\n\n\tconstructor(operationId: string) {\n\t\tif (typeof operationId !== \"string\" || operationId.length === 0) {\n\t\t\tthrow new TypeError(\"operationId must be a non-empty string\");\n\t\t}\n\t\tthis.operationId = operationId;\n\t}\n\n\tget state(): OperationState {\n\t\treturn this.current;\n\t}\n\n\tget history(): readonly OperationState[] {\n\t\treturn [...this.states];\n\t}\n\n\t/** True once a command left this process; it says nothing about the effect. */\n\tget dispatched(): boolean {\n\t\treturn this.hasDispatched;\n\t}\n\n\tget cancellationRequested(): boolean {\n\t\treturn this.cancelRequested;\n\t}\n\n\tprivate enter(next: OperationState): void {\n\t\tthis.current = next;\n\t\tthis.states.push(next);\n\t}\n\n\tprivate assertOpen(): void {\n\t\tif (TERMINAL_STATES.has(this.current)) {\n\t\t\tthrow new Error(`operation ${this.operationId} is terminal in state ${this.current}`);\n\t\t}\n\t}\n\n\t/** Record the host-held candidate set. Without this, nothing can dispatch. */\n\tobserve(candidates: readonly ObservationBinding[]): void {\n\t\tthis.assertOpen();\n\t\tif (candidates.length === 0) throw new TypeError(\"observation must yield at least one candidate\");\n\t\tthis.observations = [...candidates];\n\t\tthis.enter(\"observed\");\n\t}\n\n\t/** Select one observed candidate. Free-text re-interpretation has no path here. */\n\tpropose(candidate: ObservationBinding): void {\n\t\tthis.assertOpen();\n\t\tif (this.observations.length === 0) {\n\t\t\tthrow new Error(\"cannot propose before observe: the candidate set is unbound\");\n\t\t}\n\t\tconst match = this.observations.find(\n\t\t\t(o) =>\n\t\t\t\to.observationId === candidate.observationId &&\n\t\t\t\to.actionId === candidate.actionId &&\n\t\t\t\to.targetId === candidate.targetId &&\n\t\t\t\to.documentGeneration === candidate.documentGeneration,\n\t\t);\n\t\tif (match === undefined) throw new Error(\"proposed action is not among the recorded observations\");\n\t\tthis.selected = match;\n\t\tthis.enter(\"proposed\");\n\t}\n\n\tauthorize(permit: PermitInput): PermitDecision {\n\t\tthis.assertOpen();\n\t\tif (this.current !== \"proposed\") throw new Error(\"authorize requires a proposed operation\");\n\t\tconst decision = evaluatePermit(permit);\n\t\tif (!decision.allowed) {\n\t\t\tthis.applyPreDispatchDenial(decision.reason);\n\t\t\treturn decision;\n\t\t}\n\t\tthis.enter(\"authorized\");\n\t\treturn decision;\n\t}\n\n\t/**\n\t * Re-evaluate the permit and hand the command to the driver.\n\t *\n\t * The second evaluation is the point of the audit finding: approval and\n\t * dispatch are separated by awaits (queueing, the operator prompt, browser\n\t * startup), and state observed before those awaits is not state now.\n\t */\n\tdispatch(permit: PermitInput): PermitDecision {\n\t\tthis.assertOpen();\n\t\tif (this.current !== \"authorized\") throw new Error(\"dispatch requires an authorized operation\");\n\t\tif (this.selected === undefined) throw new Error(\"dispatch requires a bound observation\");\n\t\tconst decision = evaluatePermit(permit);\n\t\tif (!decision.allowed) {\n\t\t\tthis.applyPreDispatchDenial(decision.reason);\n\t\t\treturn decision;\n\t\t}\n\t\tthis.hasDispatched = true;\n\t\tthis.enter(\"dispatched\");\n\t\treturn decision;\n\t}\n\n\tprivate applyPreDispatchDenial(reason: PermitDenial): void {\n\t\tif (reason === \"cancelled\") {\n\t\t\tthis.cancelRequested = true;\n\t\t\tthis.enter(\"cancelled-before-dispatch\");\n\t\t}\n\t}\n\n\t/** Driver-reported outcome. Only valid once the command actually left. */\n\tsettle(outcome: SettledOutcome): void {\n\t\tif (!this.hasDispatched) throw new Error(\"settle requires a dispatched operation\");\n\t\tif (this.current === \"verified\" || this.current === \"inspection-required\") {\n\t\t\tthrow new Error(\"outcome already verified\");\n\t\t}\n\t\tthis.enter(outcome);\n\t}\n\n\t/** Explicit postcondition check. A driver's return value is not this. */\n\tverify(postconditionMet: boolean): void {\n\t\tif (this.current !== \"applied\") throw new Error(\"verify requires an applied outcome\");\n\t\tthis.enter(postconditionMet ? \"verified\" : \"inspection-required\");\n\t}\n\n\t/**\n\t * Request cancellation.\n\t *\n\t * Before dispatch this is terminal. After dispatch it only records the\n\t * request: the command is already outside this process, so the real outcome\n\t * has to be settled separately and stays `outcome-unknown` until it is.\n\t */\n\tcancel(): void {\n\t\tthis.cancelRequested = true;\n\t\tif (TERMINAL_STATES.has(this.current)) return;\n\t\tif (!this.hasDispatched) {\n\t\t\tthis.enter(\"cancelled-before-dispatch\");\n\t\t\treturn;\n\t\t}\n\t\tif (this.current === \"dispatched\") this.enter(\"outcome-unknown\");\n\t}\n}\n"]}
|