create-t2k 0.1.0 → 0.3.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 +36 -2
- package/bin/create-t2k.mjs +2 -1
- package/package.json +1 -1
- package/src/scaffold.mjs +2 -0
- package/template/README.md.template +49 -3
- package/template/compose.yml +19 -0
- package/template/package.json.template +6 -2
- package/template/src/lifecycle.mjs +313 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Create a runnable local T2K project with a synthetic ontology, accepted facts,
|
|
|
4
4
|
a Decision Context, two executable policies, and disjoint replay evidence.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
npx create-t2k my-decision-loop
|
|
7
|
+
npx create-t2k@latest my-decision-loop
|
|
8
8
|
cd my-decision-loop
|
|
9
9
|
npm start
|
|
10
10
|
```
|
|
@@ -14,10 +14,44 @@ baseline and challenger policies against the current facts, computes a held-out
|
|
|
14
14
|
replay comparison, and emits a recommendation that still requires explicit
|
|
15
15
|
human authorization.
|
|
16
16
|
|
|
17
|
+
The generated project also includes a PostgreSQL 16 Compose service and a
|
|
18
|
+
persisted golden path:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run db:up
|
|
22
|
+
npm run lifecycle
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That command records authorization, execution receipts, observations, computed
|
|
26
|
+
rewards, held-out evaluation, independent promotion, and exact rollback in the
|
|
27
|
+
open reference runtime.
|
|
28
|
+
|
|
29
|
+
Stop the local containers without deleting lifecycle data with `npm run
|
|
30
|
+
db:down`. Use the explicitly destructive `npm run db:reset` only when you
|
|
31
|
+
intend to delete the disposable local database volume.
|
|
32
|
+
|
|
33
|
+
To expose ontology validation, compilation, policy execution, replay, and
|
|
34
|
+
reward evaluation to an MCP host, add:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"t2k": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "@t2kai/mcp@latest"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This starts read-only and does not send project data to a hosted service. See
|
|
48
|
+
the [`@t2kai/mcp` guide](https://github.com/sigaihealth/t2k-core/blob/main/packages/mcp/README.md)
|
|
49
|
+
before enabling database access or agent writes.
|
|
50
|
+
|
|
17
51
|
Use `--no-install` to generate files without running `npm install`:
|
|
18
52
|
|
|
19
53
|
```bash
|
|
20
|
-
npx create-t2k my-decision-loop --no-install
|
|
54
|
+
npx create-t2k@latest my-decision-loop --no-install
|
|
21
55
|
```
|
|
22
56
|
|
|
23
57
|
The command refuses to write into a non-empty directory. Node.js 20.10 or newer
|
package/bin/create-t2k.mjs
CHANGED
|
@@ -24,7 +24,8 @@ Options:
|
|
|
24
24
|
-v, --version Show the package version
|
|
25
25
|
|
|
26
26
|
The default directory is my-t2k-project. Existing non-empty directories are
|
|
27
|
-
never overwritten
|
|
27
|
+
never overwritten. Generated projects include decision and optional
|
|
28
|
+
persisted-lifecycle examples.`;
|
|
28
29
|
|
|
29
30
|
try {
|
|
30
31
|
const options = parseArguments(process.argv.slice(2));
|
package/package.json
CHANGED
package/src/scaffold.mjs
CHANGED
|
@@ -170,6 +170,8 @@ export async function scaffoldProject({
|
|
|
170
170
|
}
|
|
171
171
|
stdout.write(" npm start\n\n");
|
|
172
172
|
stdout.write("The first run computes a recommendation; a human must still authorize it.\n");
|
|
173
|
+
stdout.write("Run `npm run db:up && npm run lifecycle` for the persisted closed loop.\n");
|
|
174
|
+
stdout.write("Use `npm run db:down` to stop it or `npm run db:reset` to delete its volume.\n");
|
|
173
175
|
|
|
174
176
|
return { targetPath, projectName };
|
|
175
177
|
}
|
|
@@ -19,6 +19,51 @@ The run performs five explicit steps:
|
|
|
19
19
|
The output is not an autonomous authorization. The generated Decision Context
|
|
20
20
|
requires a `dispatch_owner` to review the evidence and authorize any action.
|
|
21
21
|
|
|
22
|
+
## Run the complete persisted loop
|
|
23
|
+
|
|
24
|
+
Start the included PostgreSQL 16 service, then execute the lifecycle:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run db:up
|
|
28
|
+
npm run lifecycle
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This path persists 24 synthetic episodes and proves the full sequence:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
context -> recommendation -> human authorization -> execution receipt
|
|
35
|
+
-> observation -> computed reward -> held-out replay
|
|
36
|
+
-> human promotion -> exact rollback
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The actors who propose, evaluate, promote, and roll back are deliberately
|
|
40
|
+
separate synthetic identities. The event ledger is hash-chained and append-only.
|
|
41
|
+
In production, authenticate those actor IDs and enforce organization roles in
|
|
42
|
+
the calling service; the local runtime is not an identity provider. To remove
|
|
43
|
+
the local containers while preserving their volume, run `npm run db:down`. To
|
|
44
|
+
explicitly delete the disposable local volume and all lifecycle data in it, run
|
|
45
|
+
`npm run db:reset`.
|
|
46
|
+
|
|
47
|
+
## Connect an MCP host
|
|
48
|
+
|
|
49
|
+
Expose validation, compilation, policy execution, replay, and reward evaluation
|
|
50
|
+
to an MCP host with the safe database-free mode:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"t2k": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "@t2kai/mcp@latest"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The MCP server does not expose human authorization or promotion operations.
|
|
64
|
+
Read <https://t2k.ai/developers/> before enabling its optional Postgres or agent
|
|
65
|
+
mutation modes.
|
|
66
|
+
|
|
22
67
|
## Change the example
|
|
23
68
|
|
|
24
69
|
- Change current facts in `decision-context.json`.
|
|
@@ -26,6 +71,7 @@ requires a `dispatch_owner` to review the evidence and authorize any action.
|
|
|
26
71
|
- Add observed outcomes to `episodes/holdout.json` without reusing training data.
|
|
27
72
|
- Change concepts and decision contracts in `ontology-pack.json`.
|
|
28
73
|
|
|
29
|
-
The quickstart uses local files and `@t2kai/core`; it does not
|
|
30
|
-
hosted service.
|
|
31
|
-
|
|
74
|
+
The quickstart uses local files, local PostgreSQL, and `@t2kai/core`; it does not
|
|
75
|
+
send data to a hosted service. Set `T2K_DATABASE_URL` to use another Postgres
|
|
76
|
+
database. Use a dedicated database because the runtime owns the
|
|
77
|
+
`t2k_reference` schema.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:16-alpine
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_DB: t2k_reference
|
|
6
|
+
POSTGRES_PASSWORD: t2k
|
|
7
|
+
POSTGRES_USER: t2k
|
|
8
|
+
healthcheck:
|
|
9
|
+
test: ["CMD-SHELL", "pg_isready -U t2k -d t2k_reference"]
|
|
10
|
+
interval: 2s
|
|
11
|
+
timeout: 5s
|
|
12
|
+
retries: 15
|
|
13
|
+
ports:
|
|
14
|
+
- "55432:5432"
|
|
15
|
+
volumes:
|
|
16
|
+
- t2k-reference-data:/var/lib/postgresql/data
|
|
17
|
+
|
|
18
|
+
volumes:
|
|
19
|
+
t2k-reference-data:
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/run.mjs",
|
|
11
|
-
"check": "node src/run.mjs"
|
|
11
|
+
"check": "node src/run.mjs",
|
|
12
|
+
"db:up": "docker compose up -d --wait",
|
|
13
|
+
"db:down": "docker compose down",
|
|
14
|
+
"db:reset": "docker compose down -v",
|
|
15
|
+
"lifecycle": "node src/lifecycle.mjs"
|
|
12
16
|
},
|
|
13
17
|
"dependencies": {
|
|
14
|
-
"@t2kai/core": "^0.
|
|
18
|
+
"@t2kai/core": "^0.3.0"
|
|
15
19
|
}
|
|
16
20
|
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { validateOntologyPackManifest } from "@t2kai/core";
|
|
9
|
+
import { compileOntologyPackSet } from "@t2kai/core/compiler";
|
|
10
|
+
import { PostgresReferenceLifecycle } from "@t2kai/core/postgres";
|
|
11
|
+
|
|
12
|
+
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const readJson = async (relativePath) =>
|
|
14
|
+
JSON.parse(await fs.readFile(path.join(projectRoot, relativePath), "utf8"));
|
|
15
|
+
const [manifest, baseline, candidatePolicy, holdout] = await Promise.all([
|
|
16
|
+
readJson("ontology-pack.json"),
|
|
17
|
+
readJson("policies/baseline.json"),
|
|
18
|
+
readJson("policies/candidate.json"),
|
|
19
|
+
readJson("episodes/holdout.json"),
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const validation = validateOntologyPackManifest(manifest);
|
|
23
|
+
assert.equal(validation.valid, true, JSON.stringify(validation.errors));
|
|
24
|
+
const compilation = compileOntologyPackSet({
|
|
25
|
+
manifests: [manifest],
|
|
26
|
+
roots: [{ ontologyId: manifest.ontologyId, version: manifest.ontologyVersion }],
|
|
27
|
+
});
|
|
28
|
+
assert.equal(compilation.status, "valid", JSON.stringify(compilation.diagnostics));
|
|
29
|
+
|
|
30
|
+
const template = manifest.decisionTemplates[0];
|
|
31
|
+
const learningContract = template.learningContract;
|
|
32
|
+
const runId = randomUUID().replaceAll("-", "").slice(0, 10);
|
|
33
|
+
const policyKey = `${baseline.policyKey}-${runId}`;
|
|
34
|
+
const decisionType = `${template.decisionType}.${runId}`;
|
|
35
|
+
const connectionString =
|
|
36
|
+
process.env.T2K_DATABASE_URL ??
|
|
37
|
+
process.env.DATABASE_URL ??
|
|
38
|
+
process.env.T2K_TEST_DATABASE_URL ??
|
|
39
|
+
"postgresql://t2k:t2k@127.0.0.1:55432/t2k_reference";
|
|
40
|
+
|
|
41
|
+
const actors = {
|
|
42
|
+
proposer: { actorType: "agent", actorId: "agent:policy-builder" },
|
|
43
|
+
reviewer: { actorType: "human", actorId: "human:dispatch-owner" },
|
|
44
|
+
evaluator: { actorType: "human", actorId: "human:policy-evaluator" },
|
|
45
|
+
promoter: { actorType: "human", actorId: "human:policy-promoter" },
|
|
46
|
+
rollback: { actorType: "human", actorId: "human:rollback-reviewer" },
|
|
47
|
+
reward: { actorType: "system", actorId: "system:reward-engine" },
|
|
48
|
+
};
|
|
49
|
+
const lifecycle = new PostgresReferenceLifecycle({ connectionString });
|
|
50
|
+
const trainingEpisodeIds = [];
|
|
51
|
+
const holdoutEpisodeIds = [];
|
|
52
|
+
|
|
53
|
+
const sha256 = (value) =>
|
|
54
|
+
createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
55
|
+
|
|
56
|
+
async function createAcceptedVersion(version, specification, parentVersionId) {
|
|
57
|
+
const proposed = await lifecycle.createPolicyVersion(
|
|
58
|
+
policyKey,
|
|
59
|
+
{
|
|
60
|
+
policyVersion: version,
|
|
61
|
+
learningMode: learningContract.mode,
|
|
62
|
+
specification,
|
|
63
|
+
rewardSpec: learningContract.rewardSpec,
|
|
64
|
+
parentVersionId,
|
|
65
|
+
rationale: `Synthetic reference policy ${version}.`,
|
|
66
|
+
},
|
|
67
|
+
actors.proposer
|
|
68
|
+
);
|
|
69
|
+
return lifecycle.acceptPolicyVersion(
|
|
70
|
+
policyKey,
|
|
71
|
+
proposed.policyVersion,
|
|
72
|
+
"An independent human accepted the deterministic policy.",
|
|
73
|
+
actors.reviewer
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function persistFixture(fixture, index) {
|
|
78
|
+
const key = `${runId}:${fixture.cohort}:${fixture.fixtureId}`;
|
|
79
|
+
const pressure = fixture.state.facts[0].objectValue;
|
|
80
|
+
const context = await lifecycle.createDecisionContext(
|
|
81
|
+
{
|
|
82
|
+
contextKey: `context:${key}`,
|
|
83
|
+
question: template.question,
|
|
84
|
+
decisionType,
|
|
85
|
+
stateSnapshot: fixture.state,
|
|
86
|
+
objective: { maximize: template.successMeasure },
|
|
87
|
+
constraints: template.policies,
|
|
88
|
+
requiredAuthority: { role: template.authority },
|
|
89
|
+
learningContract,
|
|
90
|
+
},
|
|
91
|
+
actors.proposer
|
|
92
|
+
);
|
|
93
|
+
const recommendation = await lifecycle.recommend(
|
|
94
|
+
context.contextKey,
|
|
95
|
+
{ recommendationKey: `recommendation:${key}` },
|
|
96
|
+
actors.proposer
|
|
97
|
+
);
|
|
98
|
+
assert.equal(recommendation.proposedAction, fixture.loggedAction);
|
|
99
|
+
const authorization = await lifecycle.authorizeRecommendation(
|
|
100
|
+
recommendation.id,
|
|
101
|
+
{ rationale: "The dispatch owner authorizes this reversible synthetic action." },
|
|
102
|
+
actors.reviewer
|
|
103
|
+
);
|
|
104
|
+
const episode = await lifecycle.openEpisode(
|
|
105
|
+
{
|
|
106
|
+
episodeKey: `episode:${key}`,
|
|
107
|
+
contextKey: context.contextKey,
|
|
108
|
+
authorizedDecisionId: authorization.id,
|
|
109
|
+
externalEffect: true,
|
|
110
|
+
},
|
|
111
|
+
actors.proposer
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const request = { action: fixture.loggedAction, pressure };
|
|
115
|
+
const response = { applied: true, synthetic: true };
|
|
116
|
+
await lifecycle.recordExecutionReceipt(
|
|
117
|
+
episode.id,
|
|
118
|
+
{
|
|
119
|
+
receiptKey: `receipt:${key}`,
|
|
120
|
+
idempotencyKey: `idempotency:${key}`,
|
|
121
|
+
connectorRef: "synthetic.harborlight.dispatch",
|
|
122
|
+
externalTransactionId: `dispatch-${key}`,
|
|
123
|
+
outcome: "succeeded",
|
|
124
|
+
requestHash: sha256(request),
|
|
125
|
+
responseHash: sha256(response),
|
|
126
|
+
response,
|
|
127
|
+
rollbackContract: { operation: "restore_prior_dispatch_plan" },
|
|
128
|
+
reconciliationStatus: "reconciled",
|
|
129
|
+
},
|
|
130
|
+
actors.proposer
|
|
131
|
+
);
|
|
132
|
+
await lifecycle.recordObservation(
|
|
133
|
+
episode.id,
|
|
134
|
+
{
|
|
135
|
+
measureRef: learningContract.rewardSpec[0].measureRef,
|
|
136
|
+
observedValue: 0.4 + fixture.scalarReward * 0.4,
|
|
137
|
+
baselineValue: 0.4,
|
|
138
|
+
unit: "ratio",
|
|
139
|
+
observationWindow: "7d",
|
|
140
|
+
sourceRefs: [`fixture://harborlight/${key}`],
|
|
141
|
+
provenance: { cohort: fixture.cohort, synthetic: true },
|
|
142
|
+
attributionConfidence: 1,
|
|
143
|
+
observedAt: new Date(Date.UTC(2026, 0, index + 1)).toISOString(),
|
|
144
|
+
},
|
|
145
|
+
actors.proposer
|
|
146
|
+
);
|
|
147
|
+
const reward = await lifecycle.assessReward(
|
|
148
|
+
episode.id,
|
|
149
|
+
{
|
|
150
|
+
assessmentKey: `assessment:${key}`,
|
|
151
|
+
attribution: { method: "deterministic_synthetic_fixture" },
|
|
152
|
+
},
|
|
153
|
+
actors.reward
|
|
154
|
+
);
|
|
155
|
+
assert.equal(reward.lifecycleStatus, "complete");
|
|
156
|
+
await lifecycle.closeEpisode(
|
|
157
|
+
episode.id,
|
|
158
|
+
"The receipt and outcome observation are complete.",
|
|
159
|
+
actors.reviewer
|
|
160
|
+
);
|
|
161
|
+
(fixture.cohort === "training" ? trainingEpisodeIds : holdoutEpisodeIds).push(
|
|
162
|
+
episode.id
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const behaviorTraining = [
|
|
167
|
+
{
|
|
168
|
+
fixtureId: "training-rebalance-01",
|
|
169
|
+
cohort: "training",
|
|
170
|
+
state: { facts: [{ objectValue: 0.82 }] },
|
|
171
|
+
loggedAction: "rebalance_route",
|
|
172
|
+
scalarReward: 0.8,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
fixtureId: "training-rebalance-02",
|
|
176
|
+
cohort: "training",
|
|
177
|
+
state: { facts: [{ objectValue: 0.74 }] },
|
|
178
|
+
loggedAction: "rebalance_route",
|
|
179
|
+
scalarReward: 0.7,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
const baselineTraining = [
|
|
183
|
+
{
|
|
184
|
+
fixtureId: "training-hold-01",
|
|
185
|
+
cohort: "training",
|
|
186
|
+
state: { facts: [{ objectValue: 0.31 }] },
|
|
187
|
+
loggedAction: "hold",
|
|
188
|
+
scalarReward: 0,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
fixtureId: "training-hold-02",
|
|
192
|
+
cohort: "training",
|
|
193
|
+
state: { facts: [{ objectValue: 0.22 }] },
|
|
194
|
+
loggedAction: "hold",
|
|
195
|
+
scalarReward: 0.05,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
const normalizedHoldout = holdout.map((episode) => ({
|
|
199
|
+
...episode,
|
|
200
|
+
fixtureId: episode.episodeId,
|
|
201
|
+
cohort: "holdout",
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const schemaVersion = await lifecycle.migrate();
|
|
206
|
+
await lifecycle.createPolicy(
|
|
207
|
+
{
|
|
208
|
+
policyKey,
|
|
209
|
+
label: "Harborlight dispatch policy",
|
|
210
|
+
decisionType,
|
|
211
|
+
description: "A synthetic persisted decision-learning loop.",
|
|
212
|
+
},
|
|
213
|
+
actors.proposer
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const behavior = await createAcceptedVersion(
|
|
217
|
+
"0.9.0",
|
|
218
|
+
candidatePolicy.specification
|
|
219
|
+
);
|
|
220
|
+
await lifecycle.deployPolicyVersion(policyKey, "0.9.0", actors.reviewer);
|
|
221
|
+
const behaviorFixtures = [
|
|
222
|
+
...behaviorTraining,
|
|
223
|
+
...normalizedHoldout.filter((episode) => episode.loggedAction === "rebalance_route"),
|
|
224
|
+
];
|
|
225
|
+
for (const [index, fixture] of behaviorFixtures.entries()) {
|
|
226
|
+
await persistFixture(fixture, index);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const baselineVersion = await createAcceptedVersion(
|
|
230
|
+
baseline.version,
|
|
231
|
+
baseline.specification,
|
|
232
|
+
behavior.id
|
|
233
|
+
);
|
|
234
|
+
await lifecycle.deployPolicyVersion(policyKey, baseline.version, actors.reviewer);
|
|
235
|
+
const baselineFixtures = [
|
|
236
|
+
...baselineTraining,
|
|
237
|
+
...normalizedHoldout.filter((episode) => episode.loggedAction !== "rebalance_route"),
|
|
238
|
+
];
|
|
239
|
+
for (const [index, fixture] of baselineFixtures.entries()) {
|
|
240
|
+
await persistFixture(fixture, behaviorFixtures.length + index);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
assert.equal(trainingEpisodeIds.length, 4);
|
|
244
|
+
assert.equal(holdoutEpisodeIds.length, 20);
|
|
245
|
+
const candidate = await lifecycle.createCandidate(
|
|
246
|
+
{
|
|
247
|
+
candidateKey: `candidate:${runId}:${candidatePolicy.version}`,
|
|
248
|
+
policyKey,
|
|
249
|
+
sourcePolicyVersionId: baselineVersion.id,
|
|
250
|
+
proposedPolicyVersion: candidatePolicy.version,
|
|
251
|
+
proposedSpecification: candidatePolicy.specification,
|
|
252
|
+
trainingEpisodeIds,
|
|
253
|
+
rationale: "Training evidence supports route rebalancing under pressure.",
|
|
254
|
+
},
|
|
255
|
+
actors.proposer
|
|
256
|
+
);
|
|
257
|
+
const replay = await lifecycle.evaluateCandidate(
|
|
258
|
+
candidate.id,
|
|
259
|
+
{
|
|
260
|
+
evaluationKey: `replay:${runId}:${candidatePolicy.version}`,
|
|
261
|
+
holdoutEpisodeIds,
|
|
262
|
+
evidenceRefs: [`fixture://harborlight/${runId}/holdout`],
|
|
263
|
+
},
|
|
264
|
+
actors.evaluator
|
|
265
|
+
);
|
|
266
|
+
assert.equal(replay.lifecycleStatus, "passed");
|
|
267
|
+
|
|
268
|
+
const promoted = await lifecycle.promoteCandidate(
|
|
269
|
+
candidate.id,
|
|
270
|
+
{
|
|
271
|
+
reviewRationale: "Computed held-out replay passed the promotion threshold.",
|
|
272
|
+
deploy: true,
|
|
273
|
+
},
|
|
274
|
+
actors.promoter
|
|
275
|
+
);
|
|
276
|
+
const rollback = await lifecycle.rollbackPromotion(
|
|
277
|
+
promoted.promotion.id,
|
|
278
|
+
"Restore the exact parent after proving rollback.",
|
|
279
|
+
actors.rollback
|
|
280
|
+
);
|
|
281
|
+
const restored = await lifecycle.getActivePolicy(decisionType);
|
|
282
|
+
assert.equal(restored?.version.id, baselineVersion.id);
|
|
283
|
+
const eventChain = await lifecycle.verifyEventChain();
|
|
284
|
+
assert.equal(eventChain.valid, true);
|
|
285
|
+
|
|
286
|
+
console.log(
|
|
287
|
+
JSON.stringify(
|
|
288
|
+
{
|
|
289
|
+
status: "passed",
|
|
290
|
+
ontology: `${manifest.ontologyId}@${manifest.ontologyVersion}`,
|
|
291
|
+
resolutionHash: compilation.resolutionHash,
|
|
292
|
+
runId,
|
|
293
|
+
schemaVersion,
|
|
294
|
+
persisted: { episodes: 24, executionReceipts: 24 },
|
|
295
|
+
replay: replay.metrics,
|
|
296
|
+
promotion: {
|
|
297
|
+
version: promoted.policyVersion.policyVersion,
|
|
298
|
+
status: promoted.promotion.lifecycleStatus,
|
|
299
|
+
},
|
|
300
|
+
rollback: {
|
|
301
|
+
status: rollback.lifecycleStatus,
|
|
302
|
+
restoredVersion: restored.version.policyVersion,
|
|
303
|
+
exactParentRestored: true,
|
|
304
|
+
},
|
|
305
|
+
eventChain,
|
|
306
|
+
},
|
|
307
|
+
null,
|
|
308
|
+
2
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
} finally {
|
|
312
|
+
await lifecycle.close();
|
|
313
|
+
}
|