create-t2k 0.3.0 → 0.4.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 +33 -0
- package/bin/create-t2k.mjs +5 -2
- package/package.json +3 -2
- package/src/scaffold.mjs +68 -7
- package/template/package.json.template +1 -1
- package/template-integration-hub/.gitignore.template +3 -0
- package/template-integration-hub/README.md.template +45 -0
- package/template-integration-hub/authority-policy.json +10 -0
- package/template-integration-hub/ontology-pack.json +143 -0
- package/template-integration-hub/package.json.template +16 -0
- package/template-integration-hub/source-records/registry-alpha.json +24 -0
- package/template-integration-hub/source-records/registry-beta.json +24 -0
- package/template-integration-hub/src/run.mjs +231 -0
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
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
|
+
The existing decision-loop project remains the default profile.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
npx create-t2k@latest my-decision-loop
|
|
@@ -57,4 +58,36 @@ npx create-t2k@latest my-decision-loop --no-install
|
|
|
57
58
|
The command refuses to write into a non-empty directory. Node.js 20.10 or newer
|
|
58
59
|
is required.
|
|
59
60
|
|
|
61
|
+
## Integration-hub profile
|
|
62
|
+
|
|
63
|
+
Generate a focused, fully synthetic source-integration project explicitly:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx create-t2k@latest my-integration-hub --profile integration-hub
|
|
67
|
+
cd my-integration-hub
|
|
68
|
+
npm start
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This profile maps two independent registry records into one canonical party
|
|
72
|
+
identity, preserves conflicting name evidence, and applies a versioned authority
|
|
73
|
+
order to a conflicting registry state. It runs reconciliation again with the
|
|
74
|
+
opposite input order and proves the proposal hash is identical.
|
|
75
|
+
|
|
76
|
+
Every regular `source-records/*.json` file is discovered in canonical filename
|
|
77
|
+
order, so the generated README experiments work without editing the runner:
|
|
78
|
+
reverse the versioned authority priorities or add a third synthetic source with
|
|
79
|
+
the same canonical key and run `npm start` again.
|
|
80
|
+
|
|
81
|
+
The output is a deterministic evidence packet for human review. It preserves
|
|
82
|
+
the complete source receipts and alternatives inside the hashed packet, does
|
|
83
|
+
not mutate any source record, and does not promote the authority-selected
|
|
84
|
+
candidate to accepted truth. Both included synthetic sources deliberately have
|
|
85
|
+
an `unknown` authentication state. The unkeyed hashes demonstrate deterministic
|
|
86
|
+
self-consistency only; they are not signatures, authentication, or proof that a
|
|
87
|
+
source assertion is true.
|
|
88
|
+
|
|
89
|
+
Supported profiles are `decision-loop` (the default) and `integration-hub`.
|
|
90
|
+
An unknown or repeated `--profile` option fails before the target directory is
|
|
91
|
+
created.
|
|
92
|
+
|
|
60
93
|
Apache-2.0. Contributions require DCO sign-off in the public repository.
|
package/bin/create-t2k.mjs
CHANGED
|
@@ -18,14 +18,16 @@ Usage:
|
|
|
18
18
|
create-t2k [directory] [options]
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
|
+
--profile <name> Generate decision-loop (default) or integration-hub
|
|
21
22
|
--no-install Generate the project without installing dependencies
|
|
22
23
|
--yes Accept non-interactive defaults
|
|
23
24
|
-h, --help Show this help
|
|
24
25
|
-v, --version Show the package version
|
|
25
26
|
|
|
26
27
|
The default directory is my-t2k-project. Existing non-empty directories are
|
|
27
|
-
never overwritten.
|
|
28
|
-
persisted-lifecycle examples
|
|
28
|
+
never overwritten. The decision-loop profile includes decision and optional
|
|
29
|
+
persisted-lifecycle examples. The integration-hub profile maps independent
|
|
30
|
+
synthetic sources into a non-mutating reconciliation proposal for human review.`;
|
|
29
31
|
|
|
30
32
|
try {
|
|
31
33
|
const options = parseArguments(process.argv.slice(2));
|
|
@@ -37,6 +39,7 @@ try {
|
|
|
37
39
|
await scaffoldProject({
|
|
38
40
|
targetDirectory: options.targetDirectory,
|
|
39
41
|
install: options.install,
|
|
42
|
+
profile: options.profile,
|
|
40
43
|
cwd: process.cwd(),
|
|
41
44
|
stdout: process.stdout,
|
|
42
45
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-t2k",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Create a local T2K governed-decision project in minutes.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"type": "module",
|
|
22
22
|
"bin": {
|
|
23
|
-
"create-t2k": "
|
|
23
|
+
"create-t2k": "bin/create-t2k.mjs"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=20.10.0"
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"bin",
|
|
30
30
|
"src",
|
|
31
31
|
"template",
|
|
32
|
+
"template-integration-hub",
|
|
32
33
|
"README.md",
|
|
33
34
|
"LICENSE"
|
|
34
35
|
],
|
package/src/scaffold.mjs
CHANGED
|
@@ -5,7 +5,27 @@ import process from "node:process";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
-
const
|
|
8
|
+
const profiles = {
|
|
9
|
+
"decision-loop": {
|
|
10
|
+
templateRoot: path.join(packageRoot, "template"),
|
|
11
|
+
},
|
|
12
|
+
"integration-hub": {
|
|
13
|
+
templateRoot: path.join(packageRoot, "template-integration-hub"),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const CREATE_T2K_PROFILES = Object.freeze(Object.keys(profiles));
|
|
18
|
+
|
|
19
|
+
function requireProfile(value) {
|
|
20
|
+
if (typeof value !== "string" || !Object.hasOwn(profiles, value)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Unsupported profile: ${String(value)}. Choose one of: ${CREATE_T2K_PROFILES.join(
|
|
23
|
+
", "
|
|
24
|
+
)}.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
9
29
|
|
|
10
30
|
export function parseArguments(argumentsList) {
|
|
11
31
|
const options = {
|
|
@@ -13,11 +33,14 @@ export function parseArguments(argumentsList) {
|
|
|
13
33
|
install: true,
|
|
14
34
|
help: false,
|
|
15
35
|
version: false,
|
|
36
|
+
profile: "decision-loop",
|
|
16
37
|
};
|
|
17
38
|
const positionals = [];
|
|
18
39
|
let parseOptions = true;
|
|
40
|
+
let profileProvided = false;
|
|
19
41
|
|
|
20
|
-
for (
|
|
42
|
+
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
43
|
+
const argument = argumentsList[index];
|
|
21
44
|
if (parseOptions && argument === "--") {
|
|
22
45
|
parseOptions = false;
|
|
23
46
|
} else if (parseOptions && ["-h", "--help"].includes(argument)) {
|
|
@@ -28,6 +51,27 @@ export function parseArguments(argumentsList) {
|
|
|
28
51
|
options.install = false;
|
|
29
52
|
} else if (parseOptions && argument === "--yes") {
|
|
30
53
|
// The scaffolder has no interactive choices; this keeps npx usage familiar.
|
|
54
|
+
} else if (parseOptions && argument === "--profile") {
|
|
55
|
+
if (profileProvided) {
|
|
56
|
+
throw new Error("Provide --profile at most once.");
|
|
57
|
+
}
|
|
58
|
+
const value = argumentsList[index + 1];
|
|
59
|
+
if (!value || value.startsWith("-")) {
|
|
60
|
+
throw new Error("--profile requires a profile name.");
|
|
61
|
+
}
|
|
62
|
+
options.profile = requireProfile(value);
|
|
63
|
+
profileProvided = true;
|
|
64
|
+
index += 1;
|
|
65
|
+
} else if (parseOptions && argument.startsWith("--profile=")) {
|
|
66
|
+
if (profileProvided) {
|
|
67
|
+
throw new Error("Provide --profile at most once.");
|
|
68
|
+
}
|
|
69
|
+
const value = argument.slice("--profile=".length);
|
|
70
|
+
if (!value) {
|
|
71
|
+
throw new Error("--profile requires a profile name.");
|
|
72
|
+
}
|
|
73
|
+
options.profile = requireProfile(value);
|
|
74
|
+
profileProvided = true;
|
|
31
75
|
} else if (parseOptions && argument.startsWith("-")) {
|
|
32
76
|
throw new Error(`Unknown option: ${argument}`);
|
|
33
77
|
} else {
|
|
@@ -136,16 +180,18 @@ function shellDisplay(value) {
|
|
|
136
180
|
export async function scaffoldProject({
|
|
137
181
|
targetDirectory,
|
|
138
182
|
install = true,
|
|
183
|
+
profile = "decision-loop",
|
|
139
184
|
cwd = process.cwd(),
|
|
140
185
|
stdout = process.stdout,
|
|
141
186
|
}) {
|
|
142
187
|
if (typeof targetDirectory !== "string" || !targetDirectory.trim()) {
|
|
143
188
|
throw new Error("Project directory is required.");
|
|
144
189
|
}
|
|
190
|
+
const selectedProfile = requireProfile(profile);
|
|
145
191
|
const targetPath = path.resolve(cwd, targetDirectory);
|
|
146
192
|
const projectName = packageNameFor(targetPath);
|
|
147
193
|
await ensureEmptyDirectory(targetPath);
|
|
148
|
-
await copyTemplate(templateRoot, targetPath, {
|
|
194
|
+
await copyTemplate(profiles[selectedProfile].templateRoot, targetPath, {
|
|
149
195
|
"{{PROJECT_NAME}}": projectName,
|
|
150
196
|
});
|
|
151
197
|
|
|
@@ -169,9 +215,24 @@ export async function scaffoldProject({
|
|
|
169
215
|
stdout.write(" npm install\n");
|
|
170
216
|
}
|
|
171
217
|
stdout.write(" npm start\n\n");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
218
|
+
if (selectedProfile === "integration-hub") {
|
|
219
|
+
stdout.write(
|
|
220
|
+
"The run maps two synthetic sources into a deterministic evidence proposal for human review.\n"
|
|
221
|
+
);
|
|
222
|
+
stdout.write(
|
|
223
|
+
"It does not authenticate either source, mutate source records, or promote a selected value to accepted truth.\n"
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
stdout.write(
|
|
227
|
+
"The first run computes a recommendation; a human must still authorize it.\n"
|
|
228
|
+
);
|
|
229
|
+
stdout.write(
|
|
230
|
+
"Run `npm run db:up && npm run lifecycle` for the persisted closed loop.\n"
|
|
231
|
+
);
|
|
232
|
+
stdout.write(
|
|
233
|
+
"Use `npm run db:down` to stop it or `npm run db:reset` to delete its volume.\n"
|
|
234
|
+
);
|
|
235
|
+
}
|
|
175
236
|
|
|
176
|
-
return { targetPath, projectName };
|
|
237
|
+
return { targetPath, projectName, profile: selectedProfile };
|
|
177
238
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
This is a fully synthetic T2K integration-hub quickstart. It demonstrates how
|
|
4
|
+
two independently governed source records can be mapped into one canonical
|
|
5
|
+
identity without silently merging evidence or declaring either source true.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm start
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The run performs these explicit steps:
|
|
13
|
+
|
|
14
|
+
1. validate and compile `ontology-pack.json`;
|
|
15
|
+
2. canonically discover and map every regular `source-records/*.json` file with
|
|
16
|
+
one versioned mapping;
|
|
17
|
+
3. preserve all conflicting display-name candidates;
|
|
18
|
+
4. apply `authority-policy.json` to select a registry-state candidate within
|
|
19
|
+
the proposal;
|
|
20
|
+
5. repeat reconciliation in the opposite input order; and
|
|
21
|
+
6. emit a deterministic evidence packet for human review.
|
|
22
|
+
|
|
23
|
+
The selected registry-state value is only a candidate within a non-mutating
|
|
24
|
+
proposal. No source record is changed and no value becomes accepted truth.
|
|
25
|
+
Both source envelopes deliberately use `authenticationState: "unknown"`.
|
|
26
|
+
Production callers must authenticate or attest sources and policies outside
|
|
27
|
+
this example.
|
|
28
|
+
|
|
29
|
+
Each complete source-mapping receipt is included in the evidence packet before
|
|
30
|
+
the packet hash is computed. The receipt, policy, proposal, and packet hashes
|
|
31
|
+
are unkeyed deterministic self-consistency checks. They are not signatures,
|
|
32
|
+
authentication tokens, or proof that an assertion is true.
|
|
33
|
+
|
|
34
|
+
## Explore the hub
|
|
35
|
+
|
|
36
|
+
- Change a source value in `source-records/` and inspect the preserved evidence.
|
|
37
|
+
- Reverse the priorities in `authority-policy.json`, rerun, and inspect the
|
|
38
|
+
changed proposed authority selection.
|
|
39
|
+
- Add a third synthetic `.json` source with the same `party_key`; it is
|
|
40
|
+
discovered automatically on the next run.
|
|
41
|
+
- Replace `humanCheckpoint: "always"` only after defining a real review and
|
|
42
|
+
authorization boundary in the calling system.
|
|
43
|
+
|
|
44
|
+
Keep real people, credentials, customer vocabulary, and private source
|
|
45
|
+
locations out of this project.
|
|
@@ -0,0 +1,143 @@
|
|
|
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.integration-hub",
|
|
7
|
+
"label": "Synthetic Integration Hub",
|
|
8
|
+
"description": "Maps independently governed synthetic party records while preserving source evidence and human review.",
|
|
9
|
+
"packKind": "project",
|
|
10
|
+
"status": "accepted",
|
|
11
|
+
"scope": {
|
|
12
|
+
"domain": "synthetic_integration",
|
|
13
|
+
"description": "A non-production demonstration of canonical mapping and reconciliation.",
|
|
14
|
+
"jurisdictions": ["DEMO"],
|
|
15
|
+
"industries": [],
|
|
16
|
+
"businessStages": [],
|
|
17
|
+
"organizationSizes": [],
|
|
18
|
+
"exclusions": ["production data", "identity proofing", "autonomous truth promotion"]
|
|
19
|
+
},
|
|
20
|
+
"objectTypes": [
|
|
21
|
+
{
|
|
22
|
+
"id": "party",
|
|
23
|
+
"label": "Party",
|
|
24
|
+
"family": "Common party",
|
|
25
|
+
"nodeKind": "operating-entity",
|
|
26
|
+
"identity": ["party_id"],
|
|
27
|
+
"purpose": "A canonical party proposal whose values retain source-level evidence.",
|
|
28
|
+
"properties": [
|
|
29
|
+
{
|
|
30
|
+
"id": "party_id",
|
|
31
|
+
"valueType": "string",
|
|
32
|
+
"required": true,
|
|
33
|
+
"description": "Synthetic canonical identity key.",
|
|
34
|
+
"authorityDomain": "identity",
|
|
35
|
+
"temporal": false
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "display_name",
|
|
39
|
+
"valueType": "string",
|
|
40
|
+
"required": false,
|
|
41
|
+
"description": "A source-provided display-name candidate.",
|
|
42
|
+
"authorityDomain": "identity",
|
|
43
|
+
"temporal": true
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "registry_state",
|
|
47
|
+
"valueType": "string",
|
|
48
|
+
"required": false,
|
|
49
|
+
"description": "A source-provided registry-state candidate.",
|
|
50
|
+
"authorityDomain": "registry_status",
|
|
51
|
+
"temporal": true
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"sourceMappings": [
|
|
57
|
+
{
|
|
58
|
+
"id": "synthetic_party_record_v1",
|
|
59
|
+
"mappingVersion": "1.0.0",
|
|
60
|
+
"sourceType": "independent_registry_record",
|
|
61
|
+
"sourceLocator": "synthetic://party-registry",
|
|
62
|
+
"sourceSchemaVersion": "party-record-v1",
|
|
63
|
+
"object": "party",
|
|
64
|
+
"fieldMappings": [
|
|
65
|
+
{
|
|
66
|
+
"sourcePath": "$.party_key",
|
|
67
|
+
"targetProperty": "party_id",
|
|
68
|
+
"required": true,
|
|
69
|
+
"normalizations": ["trim", "uppercase"],
|
|
70
|
+
"valueMap": {},
|
|
71
|
+
"authorityDomain": "identity",
|
|
72
|
+
"conflictPolicy": "require_review"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"sourcePath": "$.display_name",
|
|
76
|
+
"targetProperty": "display_name",
|
|
77
|
+
"required": true,
|
|
78
|
+
"normalizations": ["trim", "collapse_whitespace"],
|
|
79
|
+
"valueMap": {},
|
|
80
|
+
"authorityDomain": "identity",
|
|
81
|
+
"conflictPolicy": "preserve_all"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"sourcePath": "$.registry_state",
|
|
85
|
+
"targetProperty": "registry_state",
|
|
86
|
+
"required": true,
|
|
87
|
+
"normalizations": ["trim", "lowercase"],
|
|
88
|
+
"valueMap": {},
|
|
89
|
+
"authorityDomain": "registry_status",
|
|
90
|
+
"conflictPolicy": "prefer_authority"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"targetIdentity": ["party_id"],
|
|
94
|
+
"idempotencyPath": "$.record_id",
|
|
95
|
+
"eventTimePath": "$.event_time",
|
|
96
|
+
"observedTimePath": "$.observed_time",
|
|
97
|
+
"authority": "source_specific",
|
|
98
|
+
"riskTier": "high",
|
|
99
|
+
"reviewStatus": "accepted",
|
|
100
|
+
"driftPolicy": "quarantine",
|
|
101
|
+
"lateArrivalPolicy": "quarantine",
|
|
102
|
+
"humanCheckpoint": "always",
|
|
103
|
+
"replayable": true
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"authorityModel": [
|
|
107
|
+
{
|
|
108
|
+
"domain": "identity",
|
|
109
|
+
"authority": "human_reviewed_source_evidence",
|
|
110
|
+
"status": "accepted",
|
|
111
|
+
"scope": "Identity candidates remain proposals until a human-owned process accepts them."
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"domain": "registry_status",
|
|
115
|
+
"authority": "versioned_priority_policy",
|
|
116
|
+
"status": "accepted",
|
|
117
|
+
"scope": "Priority selects a candidate within the proposal and does not establish truth."
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"eventTypes": [
|
|
121
|
+
{
|
|
122
|
+
"id": "source_record_observed",
|
|
123
|
+
"source": "integration_hub",
|
|
124
|
+
"createsOrUpdates": "party",
|
|
125
|
+
"humanCheckpoint": "required"
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"reasoningFunctions": [
|
|
129
|
+
{
|
|
130
|
+
"id": "propose_canonical_reconciliation",
|
|
131
|
+
"input": "receipt-bound source evidence and versioned authority policy",
|
|
132
|
+
"output": "deterministic non-mutating proposal",
|
|
133
|
+
"humanCheckpoint": "required"
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"extensions": {
|
|
137
|
+
"dataPolicy": {
|
|
138
|
+
"syntheticOnly": true,
|
|
139
|
+
"authenticationProvided": false,
|
|
140
|
+
"truthPromotionAllowed": false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -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.4.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceSystem": "synthetic-registry-alpha",
|
|
3
|
+
"sourceLocator": "synthetic://registry-alpha/party/ALPHA-0042",
|
|
4
|
+
"sourceRecordKey": "ALPHA-0042",
|
|
5
|
+
"sourceSchemaVersion": "party-record-v1",
|
|
6
|
+
"eventTime": "2026-08-29T15:00:00.000Z",
|
|
7
|
+
"observedTime": "2026-08-29T15:00:05.000Z",
|
|
8
|
+
"authenticationState": "unknown",
|
|
9
|
+
"authorityRef": "synthetic_registry_alpha",
|
|
10
|
+
"dataClassification": "synthetic_demo",
|
|
11
|
+
"purposeTags": ["integration_hub_demo"],
|
|
12
|
+
"retentionPolicy": {
|
|
13
|
+
"policyId": "synthetic-demo-30d",
|
|
14
|
+
"expiresAt": "2026-09-28"
|
|
15
|
+
},
|
|
16
|
+
"payload": {
|
|
17
|
+
"record_id": "ALPHA-0042",
|
|
18
|
+
"party_key": " shared-0042 ",
|
|
19
|
+
"display_name": " Sample Services LLC ",
|
|
20
|
+
"registry_state": " ACTIVE ",
|
|
21
|
+
"event_time": "2026-08-29T15:00:00.000Z",
|
|
22
|
+
"observed_time": "2026-08-29T15:00:05.000Z"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceSystem": "synthetic-registry-beta",
|
|
3
|
+
"sourceLocator": "synthetic://registry-beta/party/BETA-7001",
|
|
4
|
+
"sourceRecordKey": "BETA-7001",
|
|
5
|
+
"sourceSchemaVersion": "party-record-v1",
|
|
6
|
+
"eventTime": "2026-08-29T15:01:00.000Z",
|
|
7
|
+
"observedTime": "2026-08-29T15:01:08.000Z",
|
|
8
|
+
"authenticationState": "unknown",
|
|
9
|
+
"authorityRef": "synthetic_registry_beta",
|
|
10
|
+
"dataClassification": "synthetic_demo",
|
|
11
|
+
"purposeTags": ["integration_hub_demo"],
|
|
12
|
+
"retentionPolicy": {
|
|
13
|
+
"policyId": "synthetic-demo-30d",
|
|
14
|
+
"expiresAt": "2026-09-28"
|
|
15
|
+
},
|
|
16
|
+
"payload": {
|
|
17
|
+
"record_id": "BETA-7001",
|
|
18
|
+
"party_key": "SHARED-0042",
|
|
19
|
+
"display_name": "Sample Service Company",
|
|
20
|
+
"registry_state": "pending",
|
|
21
|
+
"event_time": "2026-08-29T15:01:00.000Z",
|
|
22
|
+
"observed_time": "2026-08-29T15:01:08.000Z"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
executeSourceMapping,
|
|
8
|
+
reconcileCanonicalRecords,
|
|
9
|
+
validateOntologyPackManifest,
|
|
10
|
+
} from "@t2kai/core";
|
|
11
|
+
import {
|
|
12
|
+
compileOntologyPackSet,
|
|
13
|
+
compareCanonicalStrings,
|
|
14
|
+
semanticHash,
|
|
15
|
+
} from "@t2kai/core/compiler";
|
|
16
|
+
|
|
17
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
|
|
19
|
+
async function readJson(relativePath) {
|
|
20
|
+
return JSON.parse(await fs.readFile(path.join(root, relativePath), "utf8"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readSourceRecords() {
|
|
24
|
+
const sourceDirectory = path.join(root, "source-records");
|
|
25
|
+
const sourceFiles = (await fs.readdir(sourceDirectory, { withFileTypes: true }))
|
|
26
|
+
.filter((entry) => entry.isFile() && path.extname(entry.name) === ".json")
|
|
27
|
+
.map((entry) => entry.name)
|
|
28
|
+
.sort(compareCanonicalStrings);
|
|
29
|
+
|
|
30
|
+
assert.ok(
|
|
31
|
+
sourceFiles.length >= 2,
|
|
32
|
+
"The integration-hub profile requires at least two source-records/*.json files."
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return Promise.all(
|
|
36
|
+
sourceFiles.map((sourceFile) =>
|
|
37
|
+
readJson(path.posix.join("source-records", sourceFile))
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const manifest = await readJson("ontology-pack.json");
|
|
43
|
+
const authorityPolicy = await readJson("authority-policy.json");
|
|
44
|
+
const sourceRecords = await readSourceRecords();
|
|
45
|
+
const sourceSnapshot = structuredClone(sourceRecords);
|
|
46
|
+
|
|
47
|
+
const validation = validateOntologyPackManifest(manifest);
|
|
48
|
+
assert.equal(
|
|
49
|
+
validation.valid,
|
|
50
|
+
true,
|
|
51
|
+
`Integration-hub manifest failed validation: ${JSON.stringify(validation.errors)}`
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const compilation = compileOntologyPackSet({
|
|
55
|
+
manifests: [manifest],
|
|
56
|
+
roots: [
|
|
57
|
+
{
|
|
58
|
+
ontologyId: manifest.ontologyId,
|
|
59
|
+
version: manifest.ontologyVersion,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
assert.equal(compilation.status, "valid", JSON.stringify(compilation.diagnostics));
|
|
64
|
+
|
|
65
|
+
const mapping = manifest.sourceMappings.find(
|
|
66
|
+
(candidate) => candidate.id === "synthetic_party_record_v1"
|
|
67
|
+
);
|
|
68
|
+
assert.ok(mapping, "The integration-hub source mapping is missing.");
|
|
69
|
+
|
|
70
|
+
const mappedResults = sourceRecords.map((sourceRecord) => {
|
|
71
|
+
const { payload, ...envelopeMetadata } = sourceRecord;
|
|
72
|
+
return executeSourceMapping({
|
|
73
|
+
mapping,
|
|
74
|
+
envelope: {
|
|
75
|
+
...envelopeMetadata,
|
|
76
|
+
payload: structuredClone(payload),
|
|
77
|
+
contentHash: semanticHash(payload),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
assert.ok(
|
|
83
|
+
mappedResults.every((result) => result.receipt.status === "mapped"),
|
|
84
|
+
"Every synthetic source record must map successfully."
|
|
85
|
+
);
|
|
86
|
+
assert.ok(
|
|
87
|
+
mappedResults.every((result) => result.receipt.humanReviewRequired),
|
|
88
|
+
"Every source receipt must retain the explicit human checkpoint."
|
|
89
|
+
);
|
|
90
|
+
assert.ok(
|
|
91
|
+
mappedResults.every(
|
|
92
|
+
(result) => result.receipt.authenticationState === "unknown"
|
|
93
|
+
),
|
|
94
|
+
"The synthetic profile must not claim source authentication."
|
|
95
|
+
);
|
|
96
|
+
assert.equal(
|
|
97
|
+
new Set(mappedResults.map((result) => result.receipt.receiptHash)).size,
|
|
98
|
+
mappedResults.length,
|
|
99
|
+
"Each source record must produce independent receipt-bound evidence."
|
|
100
|
+
);
|
|
101
|
+
assert.equal(
|
|
102
|
+
new Set(
|
|
103
|
+
mappedResults.map((result) => semanticHash(result.canonicalRecord.identity))
|
|
104
|
+
).size,
|
|
105
|
+
1,
|
|
106
|
+
"Every discovered source record must map to one shared canonical identity."
|
|
107
|
+
);
|
|
108
|
+
assert.ok(
|
|
109
|
+
Object.keys(mappedResults[0].canonicalRecord.identity).length > 0,
|
|
110
|
+
"The shared canonical identity must not be empty."
|
|
111
|
+
);
|
|
112
|
+
assert.deepEqual(sourceRecords, sourceSnapshot, "Source mapping mutated its input.");
|
|
113
|
+
|
|
114
|
+
const mappedSnapshot = structuredClone(mappedResults);
|
|
115
|
+
const reconciliation = reconcileCanonicalRecords({
|
|
116
|
+
results: mappedResults,
|
|
117
|
+
authorityPolicy,
|
|
118
|
+
});
|
|
119
|
+
const reverseOrderReconciliation = reconcileCanonicalRecords({
|
|
120
|
+
results: [...mappedResults].reverse(),
|
|
121
|
+
authorityPolicy,
|
|
122
|
+
});
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
mappedResults,
|
|
125
|
+
mappedSnapshot,
|
|
126
|
+
"Canonical reconciliation mutated mapped source evidence."
|
|
127
|
+
);
|
|
128
|
+
assert.equal(reconciliation.status, "needs_review");
|
|
129
|
+
assert.equal(reconciliation.humanReviewRequired, true);
|
|
130
|
+
assert.equal(reconciliation.nonMutating, true);
|
|
131
|
+
assert.equal(reconciliation.alternativesPreserved, true);
|
|
132
|
+
assert.equal(
|
|
133
|
+
reconciliation.proposalHash,
|
|
134
|
+
reverseOrderReconciliation.proposalHash,
|
|
135
|
+
"Reconciliation must be deterministic across source input order."
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const preserveAllField = reconciliation.fields.find(
|
|
139
|
+
(field) =>
|
|
140
|
+
field.conflictPolicy === "preserve_all" &&
|
|
141
|
+
field.resolution === "preserve_all"
|
|
142
|
+
);
|
|
143
|
+
const preferAuthorityField = reconciliation.fields.find(
|
|
144
|
+
(field) =>
|
|
145
|
+
field.conflictPolicy === "prefer_authority" &&
|
|
146
|
+
field.resolution === "preferred_authority"
|
|
147
|
+
);
|
|
148
|
+
assert.ok(
|
|
149
|
+
preserveAllField,
|
|
150
|
+
"At least one conflicting field must exercise preserve_all reconciliation."
|
|
151
|
+
);
|
|
152
|
+
assert.equal(preserveAllField.selectedValue, null);
|
|
153
|
+
assert.ok(preserveAllField.candidates.length >= 2);
|
|
154
|
+
assert.ok(
|
|
155
|
+
preferAuthorityField,
|
|
156
|
+
"At least one conflicting field must exercise prefer_authority reconciliation."
|
|
157
|
+
);
|
|
158
|
+
assert.notEqual(preferAuthorityField.selectedValue, null);
|
|
159
|
+
assert.ok(preferAuthorityField.candidates.length >= 2);
|
|
160
|
+
|
|
161
|
+
const packetWithoutHash = {
|
|
162
|
+
profile: "integration-hub",
|
|
163
|
+
ontology: `${manifest.ontologyId}@${manifest.ontologyVersion}`,
|
|
164
|
+
resolutionHash: compilation.resolutionHash,
|
|
165
|
+
sourceEvidence: mappedResults
|
|
166
|
+
.map((result) => ({
|
|
167
|
+
summary: {
|
|
168
|
+
sourceSystem: result.receipt.sourceSystem,
|
|
169
|
+
sourceRecordKey: result.receipt.sourceRecordKey,
|
|
170
|
+
authorityRef: result.receipt.authorityRef,
|
|
171
|
+
authenticationState: result.receipt.authenticationState,
|
|
172
|
+
receiptHash: result.receipt.receiptHash,
|
|
173
|
+
humanReviewRequired: result.receipt.humanReviewRequired,
|
|
174
|
+
},
|
|
175
|
+
receipt: result.receipt,
|
|
176
|
+
}))
|
|
177
|
+
.sort((left, right) =>
|
|
178
|
+
compareCanonicalStrings(
|
|
179
|
+
left.receipt.receiptHash,
|
|
180
|
+
right.receipt.receiptHash
|
|
181
|
+
)
|
|
182
|
+
),
|
|
183
|
+
reconciliation: {
|
|
184
|
+
proposalHash: reconciliation.proposalHash,
|
|
185
|
+
status: reconciliation.status,
|
|
186
|
+
canonicalIdentity: reconciliation.identity,
|
|
187
|
+
authorityPolicy: {
|
|
188
|
+
policyId: reconciliation.policyId,
|
|
189
|
+
policyVersion: reconciliation.policyVersion,
|
|
190
|
+
policyHash: reconciliation.policyHash,
|
|
191
|
+
},
|
|
192
|
+
deterministicAcrossInputOrder:
|
|
193
|
+
reconciliation.proposalHash === reverseOrderReconciliation.proposalHash,
|
|
194
|
+
humanReviewRequired: reconciliation.humanReviewRequired,
|
|
195
|
+
nonMutating: reconciliation.nonMutating,
|
|
196
|
+
alternativesPreserved: reconciliation.alternativesPreserved,
|
|
197
|
+
preserveAll: {
|
|
198
|
+
propertyRef: preserveAllField.propertyRef,
|
|
199
|
+
resolution: preserveAllField.resolution,
|
|
200
|
+
selectedValue: preserveAllField.selectedValue,
|
|
201
|
+
candidates: preserveAllField.candidates,
|
|
202
|
+
},
|
|
203
|
+
preferAuthority: {
|
|
204
|
+
propertyRef: preferAuthorityField.propertyRef,
|
|
205
|
+
resolution: preferAuthorityField.resolution,
|
|
206
|
+
selectedWithinProposal: preferAuthorityField.selectedValue,
|
|
207
|
+
candidates: preferAuthorityField.candidates,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
humanReview: {
|
|
211
|
+
status: "pending_human_review",
|
|
212
|
+
proposalOnly: true,
|
|
213
|
+
issues: reconciliation.issues
|
|
214
|
+
.filter((issue) => issue.severity === "review")
|
|
215
|
+
.map(({ code, message, receiptHash }) => ({ code, message, receiptHash })),
|
|
216
|
+
},
|
|
217
|
+
boundaries: {
|
|
218
|
+
syntheticDataOnly: true,
|
|
219
|
+
sourceRecordsMutated: false,
|
|
220
|
+
sourceAuthenticationEstablished: false,
|
|
221
|
+
acceptedTruthCreated: false,
|
|
222
|
+
authoritySelectionIsProposalOnly: true,
|
|
223
|
+
externalAuthenticationAndDispositionRequired: true,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
const output = {
|
|
227
|
+
...packetWithoutHash,
|
|
228
|
+
evidencePacketHash: semanticHash(packetWithoutHash),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
console.log(JSON.stringify(output, null, 2));
|