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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-t2k",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Create a local T2K governed-decision project in minutes.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
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 send data to a
30
- hosted service. Persistence, receipts, promotion, and rollback require the
31
- portable lifecycle runtime or hosted Studio and are not simulated here.
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.1.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
+ }