create-t2k 0.1.0 → 0.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/README.md +12 -0
- package/package.json +1 -1
- package/src/scaffold.mjs +1 -0
- package/template/README.md.template +27 -3
- package/template/compose.yml +19 -0
- package/template/package.json.template +5 -2
- package/template/src/lifecycle.mjs +313 -0
package/README.md
CHANGED
|
@@ -14,6 +14,18 @@ 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
|
+
|
|
17
29
|
Use `--no-install` to generate files without running `npm install`:
|
|
18
30
|
|
|
19
31
|
```bash
|
package/package.json
CHANGED
package/src/scaffold.mjs
CHANGED
|
@@ -170,6 +170,7 @@ 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");
|
|
173
174
|
|
|
174
175
|
return { targetPath, projectName };
|
|
175
176
|
}
|
|
@@ -19,6 +19,29 @@ 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 database and its volume, run `npm run db:down`.
|
|
44
|
+
|
|
22
45
|
## Change the example
|
|
23
46
|
|
|
24
47
|
- Change current facts in `decision-context.json`.
|
|
@@ -26,6 +49,7 @@ requires a `dispatch_owner` to review the evidence and authorize any action.
|
|
|
26
49
|
- Add observed outcomes to `episodes/holdout.json` without reusing training data.
|
|
27
50
|
- Change concepts and decision contracts in `ontology-pack.json`.
|
|
28
51
|
|
|
29
|
-
The quickstart uses local files and `@t2kai/core`; it does not
|
|
30
|
-
hosted service.
|
|
31
|
-
|
|
52
|
+
The quickstart uses local files, local PostgreSQL, and `@t2kai/core`; it does not
|
|
53
|
+
send data to a hosted service. Set `T2K_DATABASE_URL` to use another Postgres
|
|
54
|
+
database. Use a dedicated database because the runtime owns the
|
|
55
|
+
`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,12 @@
|
|
|
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 -v",
|
|
14
|
+
"lifecycle": "node src/lifecycle.mjs"
|
|
12
15
|
},
|
|
13
16
|
"dependencies": {
|
|
14
|
-
"@t2kai/core": "^0.
|
|
17
|
+
"@t2kai/core": "^0.2.0"
|
|
15
18
|
}
|
|
16
19
|
}
|
|
@@ -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
|
+
}
|