create-t2k 0.1.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 T2K contributors
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # `create-t2k`
2
+
3
+ Create a runnable local T2K project with a synthetic ontology, accepted facts,
4
+ a Decision Context, two executable policies, and disjoint replay evidence.
5
+
6
+ ```bash
7
+ npx create-t2k my-decision-loop
8
+ cd my-decision-loop
9
+ npm start
10
+ ```
11
+
12
+ The generated run validates and compiles the ontology pack, executes the
13
+ baseline and challenger policies against the current facts, computes a held-out
14
+ replay comparison, and emits a recommendation that still requires explicit
15
+ human authorization.
16
+
17
+ Use `--no-install` to generate files without running `npm install`:
18
+
19
+ ```bash
20
+ npx create-t2k my-decision-loop --no-install
21
+ ```
22
+
23
+ The command refuses to write into a non-empty directory. Node.js 20.10 or newer
24
+ is required.
25
+
26
+ Apache-2.0. Contributions require DCO sign-off in the public repository.
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+
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 { parseArguments, scaffoldProject } from "../src/scaffold.mjs";
9
+
10
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ const packageManifest = JSON.parse(
12
+ await fs.readFile(path.join(packageRoot, "package.json"), "utf8")
13
+ );
14
+
15
+ const help = `Create a local T2K governed-decision project.
16
+
17
+ Usage:
18
+ create-t2k [directory] [options]
19
+
20
+ Options:
21
+ --no-install Generate the project without installing dependencies
22
+ --yes Accept non-interactive defaults
23
+ -h, --help Show this help
24
+ -v, --version Show the package version
25
+
26
+ The default directory is my-t2k-project. Existing non-empty directories are
27
+ never overwritten.`;
28
+
29
+ try {
30
+ const options = parseArguments(process.argv.slice(2));
31
+ if (options.help) {
32
+ process.stdout.write(`${help}\n`);
33
+ } else if (options.version) {
34
+ process.stdout.write(`${packageManifest.version}\n`);
35
+ } else {
36
+ await scaffoldProject({
37
+ targetDirectory: options.targetDirectory,
38
+ install: options.install,
39
+ cwd: process.cwd(),
40
+ stdout: process.stdout,
41
+ });
42
+ }
43
+ } catch (error) {
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ process.stderr.write(`create-t2k: ${message}\n`);
46
+ process.exitCode = 1;
47
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "create-t2k",
3
+ "version": "0.1.0",
4
+ "description": "Create a local T2K governed-decision project in minutes.",
5
+ "license": "Apache-2.0",
6
+ "keywords": [
7
+ "ontology",
8
+ "decision-intelligence",
9
+ "ai-agents",
10
+ "scaffolding"
11
+ ],
12
+ "homepage": "https://t2k.ai/developers/",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/sigaihealth/t2k-core.git",
16
+ "directory": "packages/create-t2k"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/sigaihealth/t2k-core/issues"
20
+ },
21
+ "type": "module",
22
+ "bin": {
23
+ "create-t2k": "./bin/create-t2k.mjs"
24
+ },
25
+ "engines": {
26
+ "node": ">=20.10.0"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "src",
31
+ "template",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "test": "node --test test/*.test.mjs",
37
+ "pack:check": "npm pack --dry-run",
38
+ "smoke:package": "node scripts/smoke-package.mjs",
39
+ "release:verify": "node scripts/verify-release-tag.mjs"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "provenance": true
44
+ }
45
+ }
@@ -0,0 +1,175 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const templateRoot = path.join(packageRoot, "template");
9
+
10
+ export function parseArguments(argumentsList) {
11
+ const options = {
12
+ targetDirectory: "my-t2k-project",
13
+ install: true,
14
+ help: false,
15
+ version: false,
16
+ };
17
+ const positionals = [];
18
+ let parseOptions = true;
19
+
20
+ for (const argument of argumentsList) {
21
+ if (parseOptions && argument === "--") {
22
+ parseOptions = false;
23
+ } else if (parseOptions && ["-h", "--help"].includes(argument)) {
24
+ options.help = true;
25
+ } else if (parseOptions && ["-v", "--version"].includes(argument)) {
26
+ options.version = true;
27
+ } else if (parseOptions && argument === "--no-install") {
28
+ options.install = false;
29
+ } else if (parseOptions && argument === "--yes") {
30
+ // The scaffolder has no interactive choices; this keeps npx usage familiar.
31
+ } else if (parseOptions && argument.startsWith("-")) {
32
+ throw new Error(`Unknown option: ${argument}`);
33
+ } else {
34
+ positionals.push(argument);
35
+ }
36
+ }
37
+
38
+ if (positionals.length > 1) {
39
+ throw new Error("Provide at most one project directory.");
40
+ }
41
+ if (positionals[0]) {
42
+ options.targetDirectory = positionals[0];
43
+ }
44
+ return options;
45
+ }
46
+
47
+ function packageNameFor(targetPath) {
48
+ const name = path.basename(targetPath).toLowerCase();
49
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
50
+ throw new Error(
51
+ "The project directory name must use lowercase letters, numbers, dots, dashes, or underscores."
52
+ );
53
+ }
54
+ return name;
55
+ }
56
+
57
+ async function ensureEmptyDirectory(targetPath) {
58
+ try {
59
+ const stat = await fs.lstat(targetPath);
60
+ if (!stat.isDirectory()) {
61
+ throw new Error(`Target exists and is not a directory: ${targetPath}`);
62
+ }
63
+ const entries = await fs.readdir(targetPath);
64
+ if (entries.length > 0) {
65
+ throw new Error(`Target directory is not empty: ${targetPath}`);
66
+ }
67
+ } catch (error) {
68
+ if (error && typeof error === "object" && error.code === "ENOENT") {
69
+ await fs.mkdir(targetPath, { recursive: true });
70
+ return;
71
+ }
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ async function copyTemplate(sourceDirectory, targetDirectory, replacements) {
77
+ const entries = await fs.readdir(sourceDirectory, { withFileTypes: true });
78
+ entries.sort((left, right) => left.name.localeCompare(right.name));
79
+
80
+ for (const entry of entries) {
81
+ if (entry.isSymbolicLink()) {
82
+ throw new Error(`Template symbolic links are not supported: ${entry.name}`);
83
+ }
84
+ const outputName = entry.name.endsWith(".template")
85
+ ? entry.name.slice(0, -".template".length)
86
+ : entry.name;
87
+ const sourcePath = path.join(sourceDirectory, entry.name);
88
+ const targetPath = path.join(targetDirectory, outputName);
89
+ if (entry.isDirectory()) {
90
+ await fs.mkdir(targetPath, { recursive: true });
91
+ await copyTemplate(sourcePath, targetPath, replacements);
92
+ continue;
93
+ }
94
+ if (!entry.isFile()) {
95
+ throw new Error(`Unsupported template entry: ${entry.name}`);
96
+ }
97
+ let contents = await fs.readFile(sourcePath, "utf8");
98
+ for (const [token, value] of Object.entries(replacements)) {
99
+ contents = contents.replaceAll(token, value);
100
+ }
101
+ await fs.writeFile(targetPath, contents, "utf8");
102
+ }
103
+ }
104
+
105
+ function run(command, argumentsList, options) {
106
+ return new Promise((resolve, reject) => {
107
+ const child = spawn(command, argumentsList, {
108
+ cwd: options.cwd,
109
+ env: process.env,
110
+ shell: false,
111
+ stdio: options.stdio,
112
+ });
113
+ child.once("error", reject);
114
+ child.once("exit", (code, signal) => {
115
+ if (code === 0) {
116
+ resolve();
117
+ } else {
118
+ reject(
119
+ new Error(
120
+ `${command} ${argumentsList.join(" ")} failed${
121
+ signal ? ` with signal ${signal}` : ` with exit code ${code}`
122
+ }.`
123
+ )
124
+ );
125
+ }
126
+ });
127
+ });
128
+ }
129
+
130
+ function shellDisplay(value) {
131
+ return /^[a-zA-Z0-9_./-]+$/.test(value)
132
+ ? value
133
+ : `'${value.replaceAll("'", `'\\''`)}'`;
134
+ }
135
+
136
+ export async function scaffoldProject({
137
+ targetDirectory,
138
+ install = true,
139
+ cwd = process.cwd(),
140
+ stdout = process.stdout,
141
+ }) {
142
+ if (typeof targetDirectory !== "string" || !targetDirectory.trim()) {
143
+ throw new Error("Project directory is required.");
144
+ }
145
+ const targetPath = path.resolve(cwd, targetDirectory);
146
+ const projectName = packageNameFor(targetPath);
147
+ await ensureEmptyDirectory(targetPath);
148
+ await copyTemplate(templateRoot, targetPath, {
149
+ "{{PROJECT_NAME}}": projectName,
150
+ });
151
+
152
+ if (install) {
153
+ stdout.write("Installing dependencies...\n");
154
+ await run(process.platform === "win32" ? "npm.cmd" : "npm", ["install"], {
155
+ cwd: targetPath,
156
+ stdio: "inherit",
157
+ });
158
+ }
159
+
160
+ const relativeTarget = path.relative(cwd, targetPath) || ".";
161
+ const commandTarget = path.isAbsolute(targetDirectory)
162
+ ? targetPath
163
+ : relativeTarget;
164
+ stdout.write(`\nCreated ${projectName} in ${targetPath}\n\n`);
165
+ if (commandTarget !== ".") {
166
+ stdout.write(` cd ${shellDisplay(commandTarget)}\n`);
167
+ }
168
+ if (!install) {
169
+ stdout.write(" npm install\n");
170
+ }
171
+ stdout.write(" npm start\n\n");
172
+ stdout.write("The first run computes a recommendation; a human must still authorize it.\n");
173
+
174
+ return { targetPath, projectName };
175
+ }
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ *.log
3
+ .DS_Store
@@ -0,0 +1,31 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ This project is a local T2K governed-decision quickstart. Its data is fully
4
+ synthetic.
5
+
6
+ ```bash
7
+ npm install
8
+ npm start
9
+ ```
10
+
11
+ The run performs five explicit steps:
12
+
13
+ 1. validate `ontology-pack.json` against the published T2K schema;
14
+ 2. compile the pack and resolve its decision template;
15
+ 3. bind accepted facts from `decision-context.json`;
16
+ 4. execute baseline and challenger policies;
17
+ 5. compute held-out replay evidence and emit a recommendation for human review.
18
+
19
+ The output is not an autonomous authorization. The generated Decision Context
20
+ requires a `dispatch_owner` to review the evidence and authorize any action.
21
+
22
+ ## Change the example
23
+
24
+ - Change current facts in `decision-context.json`.
25
+ - Change executable rules in `policies/*.json`.
26
+ - Add observed outcomes to `episodes/holdout.json` without reusing training data.
27
+ - Change concepts and decision contracts in `ontology-pack.json`.
28
+
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.
@@ -0,0 +1,31 @@
1
+ {
2
+ "contextKey": "harborlight-dispatch-2026-07-18",
3
+ "decisionType": "operations.dispatch_overflow",
4
+ "question": "How should Harborlight respond to the current dispatch queue pressure?",
5
+ "objective": "Protect on-time service while using the least disruptive reversible response.",
6
+ "facts": [
7
+ {
8
+ "claimKey": "harborlight-current-queue-pressure",
9
+ "subjectRef": "harborlight-service-business",
10
+ "predicateRef": "demo.harborlight-field-service:service_business.queue_pressure_ratio",
11
+ "objectValue": 0.78,
12
+ "status": "accepted",
13
+ "sourceRefs": [
14
+ "manual:dispatch-board-2026-07-18"
15
+ ]
16
+ }
17
+ ],
18
+ "alternatives": [
19
+ "hold",
20
+ "authorize_overtime",
21
+ "rebalance_route"
22
+ ],
23
+ "policies": [
24
+ "no_unreviewed_dispatch_change",
25
+ "connector_write_requires_rollback"
26
+ ],
27
+ "authority": {
28
+ "requiredRole": "dispatch_owner",
29
+ "humanReviewRequired": true
30
+ }
31
+ }
@@ -0,0 +1,322 @@
1
+ [
2
+ {
3
+ "episodeId": "holdout-overtime-01",
4
+ "state": {
5
+ "facts": [
6
+ {
7
+ "objectValue": 0.61
8
+ }
9
+ ]
10
+ },
11
+ "loggedAction": "authorize_overtime",
12
+ "scalarReward": -0.5,
13
+ "learningMode": "supervised_feedback",
14
+ "behaviorProbability": null,
15
+ "guardrailViolation": false,
16
+ "policyVersionId": "harborlight-dispatch@1.0.0"
17
+ },
18
+ {
19
+ "episodeId": "holdout-overtime-02",
20
+ "state": {
21
+ "facts": [
22
+ {
23
+ "objectValue": 0.62
24
+ }
25
+ ]
26
+ },
27
+ "loggedAction": "authorize_overtime",
28
+ "scalarReward": -0.5,
29
+ "learningMode": "supervised_feedback",
30
+ "behaviorProbability": null,
31
+ "guardrailViolation": false,
32
+ "policyVersionId": "harborlight-dispatch@1.0.0"
33
+ },
34
+ {
35
+ "episodeId": "holdout-overtime-03",
36
+ "state": {
37
+ "facts": [
38
+ {
39
+ "objectValue": 0.63
40
+ }
41
+ ]
42
+ },
43
+ "loggedAction": "authorize_overtime",
44
+ "scalarReward": -0.5,
45
+ "learningMode": "supervised_feedback",
46
+ "behaviorProbability": null,
47
+ "guardrailViolation": false,
48
+ "policyVersionId": "harborlight-dispatch@1.0.0"
49
+ },
50
+ {
51
+ "episodeId": "holdout-overtime-04",
52
+ "state": {
53
+ "facts": [
54
+ {
55
+ "objectValue": 0.64
56
+ }
57
+ ]
58
+ },
59
+ "loggedAction": "authorize_overtime",
60
+ "scalarReward": -0.5,
61
+ "learningMode": "supervised_feedback",
62
+ "behaviorProbability": null,
63
+ "guardrailViolation": false,
64
+ "policyVersionId": "harborlight-dispatch@1.0.0"
65
+ },
66
+ {
67
+ "episodeId": "holdout-overtime-05",
68
+ "state": {
69
+ "facts": [
70
+ {
71
+ "objectValue": 0.65
72
+ }
73
+ ]
74
+ },
75
+ "loggedAction": "authorize_overtime",
76
+ "scalarReward": -0.5,
77
+ "learningMode": "supervised_feedback",
78
+ "behaviorProbability": null,
79
+ "guardrailViolation": false,
80
+ "policyVersionId": "harborlight-dispatch@1.0.0"
81
+ },
82
+ {
83
+ "episodeId": "holdout-overtime-06",
84
+ "state": {
85
+ "facts": [
86
+ {
87
+ "objectValue": 0.66
88
+ }
89
+ ]
90
+ },
91
+ "loggedAction": "authorize_overtime",
92
+ "scalarReward": -0.5,
93
+ "learningMode": "supervised_feedback",
94
+ "behaviorProbability": null,
95
+ "guardrailViolation": false,
96
+ "policyVersionId": "harborlight-dispatch@1.0.0"
97
+ },
98
+ {
99
+ "episodeId": "holdout-overtime-07",
100
+ "state": {
101
+ "facts": [
102
+ {
103
+ "objectValue": 0.67
104
+ }
105
+ ]
106
+ },
107
+ "loggedAction": "authorize_overtime",
108
+ "scalarReward": -0.5,
109
+ "learningMode": "supervised_feedback",
110
+ "behaviorProbability": null,
111
+ "guardrailViolation": false,
112
+ "policyVersionId": "harborlight-dispatch@1.0.0"
113
+ },
114
+ {
115
+ "episodeId": "holdout-overtime-08",
116
+ "state": {
117
+ "facts": [
118
+ {
119
+ "objectValue": 0.68
120
+ }
121
+ ]
122
+ },
123
+ "loggedAction": "authorize_overtime",
124
+ "scalarReward": -0.5,
125
+ "learningMode": "supervised_feedback",
126
+ "behaviorProbability": null,
127
+ "guardrailViolation": false,
128
+ "policyVersionId": "harborlight-dispatch@1.0.0"
129
+ },
130
+ {
131
+ "episodeId": "holdout-overtime-09",
132
+ "state": {
133
+ "facts": [
134
+ {
135
+ "objectValue": 0.69
136
+ }
137
+ ]
138
+ },
139
+ "loggedAction": "authorize_overtime",
140
+ "scalarReward": -0.5,
141
+ "learningMode": "supervised_feedback",
142
+ "behaviorProbability": null,
143
+ "guardrailViolation": false,
144
+ "policyVersionId": "harborlight-dispatch@1.0.0"
145
+ },
146
+ {
147
+ "episodeId": "holdout-overtime-10",
148
+ "state": {
149
+ "facts": [
150
+ {
151
+ "objectValue": 0.7
152
+ }
153
+ ]
154
+ },
155
+ "loggedAction": "authorize_overtime",
156
+ "scalarReward": -0.5,
157
+ "learningMode": "supervised_feedback",
158
+ "behaviorProbability": null,
159
+ "guardrailViolation": false,
160
+ "policyVersionId": "harborlight-dispatch@1.0.0"
161
+ },
162
+ {
163
+ "episodeId": "holdout-rebalance-01",
164
+ "state": {
165
+ "facts": [
166
+ {
167
+ "objectValue": 0.72
168
+ }
169
+ ]
170
+ },
171
+ "loggedAction": "rebalance_route",
172
+ "scalarReward": 1,
173
+ "learningMode": "supervised_feedback",
174
+ "behaviorProbability": null,
175
+ "guardrailViolation": false,
176
+ "policyVersionId": "harborlight-dispatch@1.1.0"
177
+ },
178
+ {
179
+ "episodeId": "holdout-rebalance-02",
180
+ "state": {
181
+ "facts": [
182
+ {
183
+ "objectValue": 0.73
184
+ }
185
+ ]
186
+ },
187
+ "loggedAction": "rebalance_route",
188
+ "scalarReward": 1,
189
+ "learningMode": "supervised_feedback",
190
+ "behaviorProbability": null,
191
+ "guardrailViolation": false,
192
+ "policyVersionId": "harborlight-dispatch@1.1.0"
193
+ },
194
+ {
195
+ "episodeId": "holdout-rebalance-03",
196
+ "state": {
197
+ "facts": [
198
+ {
199
+ "objectValue": 0.74
200
+ }
201
+ ]
202
+ },
203
+ "loggedAction": "rebalance_route",
204
+ "scalarReward": 1,
205
+ "learningMode": "supervised_feedback",
206
+ "behaviorProbability": null,
207
+ "guardrailViolation": false,
208
+ "policyVersionId": "harborlight-dispatch@1.1.0"
209
+ },
210
+ {
211
+ "episodeId": "holdout-rebalance-04",
212
+ "state": {
213
+ "facts": [
214
+ {
215
+ "objectValue": 0.75
216
+ }
217
+ ]
218
+ },
219
+ "loggedAction": "rebalance_route",
220
+ "scalarReward": 1,
221
+ "learningMode": "supervised_feedback",
222
+ "behaviorProbability": null,
223
+ "guardrailViolation": false,
224
+ "policyVersionId": "harborlight-dispatch@1.1.0"
225
+ },
226
+ {
227
+ "episodeId": "holdout-rebalance-05",
228
+ "state": {
229
+ "facts": [
230
+ {
231
+ "objectValue": 0.76
232
+ }
233
+ ]
234
+ },
235
+ "loggedAction": "rebalance_route",
236
+ "scalarReward": 1,
237
+ "learningMode": "supervised_feedback",
238
+ "behaviorProbability": null,
239
+ "guardrailViolation": false,
240
+ "policyVersionId": "harborlight-dispatch@1.1.0"
241
+ },
242
+ {
243
+ "episodeId": "holdout-rebalance-06",
244
+ "state": {
245
+ "facts": [
246
+ {
247
+ "objectValue": 0.77
248
+ }
249
+ ]
250
+ },
251
+ "loggedAction": "rebalance_route",
252
+ "scalarReward": 1,
253
+ "learningMode": "supervised_feedback",
254
+ "behaviorProbability": null,
255
+ "guardrailViolation": false,
256
+ "policyVersionId": "harborlight-dispatch@1.1.0"
257
+ },
258
+ {
259
+ "episodeId": "holdout-hold-01",
260
+ "state": {
261
+ "facts": [
262
+ {
263
+ "objectValue": 0.21
264
+ }
265
+ ]
266
+ },
267
+ "loggedAction": "hold",
268
+ "scalarReward": 0,
269
+ "learningMode": "supervised_feedback",
270
+ "behaviorProbability": null,
271
+ "guardrailViolation": false,
272
+ "policyVersionId": "harborlight-dispatch@1.0.0"
273
+ },
274
+ {
275
+ "episodeId": "holdout-hold-02",
276
+ "state": {
277
+ "facts": [
278
+ {
279
+ "objectValue": 0.24
280
+ }
281
+ ]
282
+ },
283
+ "loggedAction": "hold",
284
+ "scalarReward": 0,
285
+ "learningMode": "supervised_feedback",
286
+ "behaviorProbability": null,
287
+ "guardrailViolation": false,
288
+ "policyVersionId": "harborlight-dispatch@1.0.0"
289
+ },
290
+ {
291
+ "episodeId": "holdout-hold-03",
292
+ "state": {
293
+ "facts": [
294
+ {
295
+ "objectValue": 0.27
296
+ }
297
+ ]
298
+ },
299
+ "loggedAction": "hold",
300
+ "scalarReward": 0,
301
+ "learningMode": "supervised_feedback",
302
+ "behaviorProbability": null,
303
+ "guardrailViolation": false,
304
+ "policyVersionId": "harborlight-dispatch@1.0.0"
305
+ },
306
+ {
307
+ "episodeId": "holdout-hold-04",
308
+ "state": {
309
+ "facts": [
310
+ {
311
+ "objectValue": 0.3
312
+ }
313
+ ]
314
+ },
315
+ "loggedAction": "hold",
316
+ "scalarReward": 0,
317
+ "learningMode": "supervised_feedback",
318
+ "behaviorProbability": null,
319
+ "guardrailViolation": false,
320
+ "policyVersionId": "harborlight-dispatch@1.0.0"
321
+ }
322
+ ]
@@ -0,0 +1,129 @@
1
+ {
2
+ "$schema": "https://t2k.ai/schemas/t2k-ontology-pack.v1.schema.json",
3
+ "manifestType": "t2k.ontology-pack",
4
+ "manifestVersion": "1.0",
5
+ "ontologyVersion": "1.0.0",
6
+ "ontologyId": "demo.harborlight-field-service",
7
+ "label": "Harborlight Field Service",
8
+ "description": "A fully synthetic ontology pack for a governed dispatch decision loop.",
9
+ "packKind": "vertical",
10
+ "status": "review",
11
+ "scope": {
12
+ "domain": "field_service",
13
+ "description": "Synthetic service dispatch operations for the T2K quickstart."
14
+ },
15
+ "objectTypes": [
16
+ {
17
+ "id": "service_business",
18
+ "label": "Service business",
19
+ "family": "Operating organization",
20
+ "nodeKind": "operating-entity",
21
+ "purpose": "A fictional field-service operator that dispatches work crews.",
22
+ "identity": [
23
+ "business_id"
24
+ ],
25
+ "properties": [
26
+ {
27
+ "id": "queue_pressure_ratio",
28
+ "valueType": "number",
29
+ "required": false,
30
+ "description": "Open urgent jobs divided by available dispatch capacity.",
31
+ "authorityDomain": "operations",
32
+ "temporal": true
33
+ },
34
+ {
35
+ "id": "on_time_completion_rate",
36
+ "valueType": "number",
37
+ "required": false,
38
+ "description": "Share of scheduled work completed inside the promised window.",
39
+ "authorityDomain": "operations",
40
+ "temporal": true
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "decisionTemplates": [
46
+ {
47
+ "id": "route_overflow",
48
+ "question": "How should Harborlight respond to the current dispatch queue pressure?",
49
+ "decisionType": "operations.dispatch_overflow",
50
+ "requiredContext": [],
51
+ "requiredFacts": [
52
+ "demo.harborlight-field-service:service_business.queue_pressure_ratio"
53
+ ],
54
+ "objective": "Protect on-time service while using the least disruptive reversible response.",
55
+ "successMeasure": "The on-time completion rate improves relative to the pre-decision baseline.",
56
+ "alternatives": [
57
+ "hold",
58
+ "authorize_overtime",
59
+ "rebalance_route"
60
+ ],
61
+ "criteria": [
62
+ "on_time_completion",
63
+ "reversibility",
64
+ "crew_load"
65
+ ],
66
+ "comparisonMethod": "reference_rule_set",
67
+ "policies": [
68
+ "no_unreviewed_dispatch_change"
69
+ ],
70
+ "authority": "dispatch_owner",
71
+ "riskLevel": "L1",
72
+ "allowedActionProposals": [
73
+ "dispatch_plan.apply"
74
+ ],
75
+ "rollbackExpectation": "Restore the prior dispatch plan if connector reconciliation fails.",
76
+ "outcomeMeasures": [
77
+ "demo.harborlight-field-service:service_business.on_time_completion_rate"
78
+ ],
79
+ "reviewHorizon": "Review after the seven-day service window.",
80
+ "learningContract": {
81
+ "mode": "supervised_feedback",
82
+ "stateSchema": {
83
+ "required": [
84
+ "facts.0.objectValue"
85
+ ]
86
+ },
87
+ "actionSchema": {
88
+ "allowed": [
89
+ "hold",
90
+ "authorize_overtime",
91
+ "rebalance_route"
92
+ ]
93
+ },
94
+ "rewardSpec": [
95
+ {
96
+ "measureRef": "demo.harborlight-field-service:service_business.on_time_completion_rate",
97
+ "label": "On-time completion rate",
98
+ "direction": "maximize",
99
+ "weight": 1,
100
+ "required": true,
101
+ "guardrail": false,
102
+ "unit": "ratio",
103
+ "observationWindow": "7d",
104
+ "aggregation": "latest",
105
+ "baselineMethod": "explicit",
106
+ "attributionMethod": "human_review"
107
+ }
108
+ ],
109
+ "observationSchedule": [
110
+ "7d"
111
+ ],
112
+ "terminalConditions": [
113
+ "Seven-day service window complete"
114
+ ],
115
+ "explorationPolicy": {
116
+ "mode": "none"
117
+ },
118
+ "safetyConstraints": [
119
+ "A human dispatch owner authorizes every action",
120
+ "Every connector write has a rollback contract and receipt"
121
+ ],
122
+ "promotionCriteria": {
123
+ "minimumPassingReplays": 1,
124
+ "minimumHeldOutEpisodes": 20
125
+ }
126
+ }
127
+ }
128
+ ]
129
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20.10.0"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/run.mjs",
11
+ "check": "node src/run.mjs"
12
+ },
13
+ "dependencies": {
14
+ "@t2kai/core": "^0.1.0"
15
+ }
16
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "policyKey": "harborlight-dispatch",
3
+ "version": "1.0.0",
4
+ "specification": {
5
+ "referencePolicy": {
6
+ "rules": [
7
+ {
8
+ "all": [
9
+ {
10
+ "path": "facts.0.objectValue",
11
+ "operator": "gte",
12
+ "value": 0.6
13
+ }
14
+ ],
15
+ "action": "authorize_overtime"
16
+ }
17
+ ],
18
+ "defaultAction": "hold",
19
+ "evaluation": {
20
+ "minimumEpisodes": 20,
21
+ "minimumImprovement": 0.05,
22
+ "confidenceZ": 1.96,
23
+ "minimumCoverage": 0.2
24
+ }
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "policyKey": "harborlight-dispatch",
3
+ "version": "1.1.0",
4
+ "status": "proposed",
5
+ "specification": {
6
+ "referencePolicy": {
7
+ "rules": [
8
+ {
9
+ "all": [
10
+ {
11
+ "path": "facts.0.objectValue",
12
+ "operator": "gte",
13
+ "value": 0.6
14
+ }
15
+ ],
16
+ "action": "rebalance_route"
17
+ }
18
+ ],
19
+ "defaultAction": "hold",
20
+ "evaluation": {
21
+ "minimumEpisodes": 20,
22
+ "minimumImprovement": 0.05,
23
+ "confidenceZ": 1.96,
24
+ "minimumCoverage": 0.2
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,109 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import {
7
+ aggregatePolicyRewards,
8
+ evaluateReferencePolicy,
9
+ evaluateReferenceReplay,
10
+ validateOntologyPackManifest,
11
+ } from "@t2kai/core";
12
+ import { compileOntologyPackSet } from "@t2kai/core/compiler";
13
+
14
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const readJson = async (relativePath) =>
16
+ JSON.parse(await fs.readFile(path.join(projectRoot, relativePath), "utf8"));
17
+
18
+ const [manifest, context, baseline, candidate, episodes] = await Promise.all([
19
+ readJson("ontology-pack.json"),
20
+ readJson("decision-context.json"),
21
+ readJson("policies/baseline.json"),
22
+ readJson("policies/candidate.json"),
23
+ readJson("episodes/holdout.json"),
24
+ ]);
25
+
26
+ const validation = validateOntologyPackManifest(manifest);
27
+ assert.equal(
28
+ validation.valid,
29
+ true,
30
+ `Ontology validation failed: ${JSON.stringify(validation.errors)}`
31
+ );
32
+ const compilation = compileOntologyPackSet({
33
+ manifests: [manifest],
34
+ roots: [{ ontologyId: manifest.ontologyId, version: manifest.ontologyVersion }],
35
+ });
36
+ assert.equal(compilation.status, "valid", JSON.stringify(compilation.diagnostics));
37
+
38
+ const decisionTemplate = compilation.definitions.find(
39
+ (definition) =>
40
+ definition.definitionKind === "decision_template" &&
41
+ definition.body.decisionType === context.decisionType
42
+ );
43
+ assert.ok(decisionTemplate, `No compiled template for ${context.decisionType}`);
44
+ const acceptedFactRefs = new Set(
45
+ context.facts
46
+ .filter((fact) => fact.status === "accepted")
47
+ .map((fact) => fact.predicateRef)
48
+ );
49
+ for (const requiredFact of decisionTemplate.body.requiredFacts ?? []) {
50
+ assert.ok(acceptedFactRefs.has(requiredFact), `Missing accepted fact: ${requiredFact}`);
51
+ }
52
+ assert.equal(
53
+ context.authority.humanReviewRequired,
54
+ true,
55
+ "The quickstart must not silently authorize its recommendation."
56
+ );
57
+
58
+ const state = { facts: context.facts };
59
+ const baselineAction = evaluateReferencePolicy(baseline.specification, state);
60
+ const candidateAction = evaluateReferencePolicy(candidate.specification, state);
61
+ const replay = evaluateReferenceReplay({
62
+ candidateSpecification: candidate.specification,
63
+ baselineSpecification: baseline.specification,
64
+ episodes: episodes.map(({ policyVersionId: _policyVersionId, ...episode }) => episode),
65
+ });
66
+ assert.equal(replay.status, "passed", "The challenger must pass computed replay.");
67
+ assert.equal(replay.candidate.guardrailViolations, 0);
68
+
69
+ const rewardAggregates = aggregatePolicyRewards(
70
+ episodes.map((episode) => ({
71
+ policyVersionId: episode.policyVersionId,
72
+ scalarReward: episode.scalarReward,
73
+ guardrailViolation: episode.guardrailViolation,
74
+ }))
75
+ );
76
+ const result = {
77
+ ontology: `${manifest.ontologyId}@${manifest.ontologyVersion}`,
78
+ resolutionHash: compilation.resolutionHash,
79
+ decisionContext: {
80
+ contextKey: context.contextKey,
81
+ decisionType: context.decisionType,
82
+ acceptedFacts: acceptedFactRefs.size,
83
+ },
84
+ reasoning: {
85
+ baseline: { version: baseline.version, action: baselineAction },
86
+ candidate: { version: candidate.version, action: candidateAction },
87
+ },
88
+ replay: {
89
+ status: replay.status,
90
+ sampleSize: replay.sampleSize,
91
+ estimatedImprovement: replay.estimatedImprovement,
92
+ improvementConfidenceLower: replay.improvementConfidenceLower,
93
+ candidateCoverage: replay.candidate.coverage,
94
+ baselineCoverage: replay.baseline.coverage,
95
+ guardrailViolations: replay.candidate.guardrailViolations,
96
+ },
97
+ rewardAggregates,
98
+ recommendation: {
99
+ action: candidateAction,
100
+ policyVersion: candidate.version,
101
+ status: "eligible_for_human_review",
102
+ },
103
+ authorization: {
104
+ requiredRole: context.authority.requiredRole,
105
+ status: "not_authorized",
106
+ },
107
+ };
108
+
109
+ console.log(JSON.stringify(result, null, 2));