mirai-graph 1.4.0 → 1.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to Mirai Graph will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.5.0] - 2026-09-03
8
+
9
+ Compatible 1.x maintenance line, published under `legacy-1`; the npm `latest`
10
+ channel and the separate 2.x development line are unchanged.
11
+
12
+ - Project Technology can connect and refresh a bounded provider export without
13
+ Git using explicit authenticated-release trust supplied by the consumer.
14
+ Exact export bytes, graph identity, revision and forward ancestry are checked;
15
+ target/architecture/permission gates are unchanged. No automatic trust discovery.
16
+ - Read-only source export verification proves the archive anchor against the
17
+ accepted target and Git revision before a distributor seals it in release
18
+ metadata. CLI trust can be supplied on stdin; no temporary trust file is needed.
19
+ - No-Git inventory covers explicitly declared graph and raw-source paths, detects
20
+ stale content and blocks unsafe or missing sources before sync writes. Existing
21
+ Git inventories remain revision-bound; this does not invent source history.
22
+
7
23
  ## [1.4.0] - 2026-08-29
8
24
 
9
25
  ### Added
package/CITATION.cff CHANGED
@@ -17,5 +17,5 @@ keywords:
17
17
  - complex systems
18
18
  - context engineering
19
19
  - systems engineering
20
- version: "1.2.0"
21
- date-released: "2026-08-27"
20
+ version: "1.5.0"
21
+ date-released: "2026-09-03"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirai-graph",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "private": false,
5
5
  "description": "An evolutionary graph operating model for managing the growth of complex systems.",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "pretest": "npm run test:project-technology && npm run test:context-traversal && npm run test:project-continuity && npm run test:artifact-releases && npm run test:technology-courses && npm run validate:project-technology-schemas",
17
- "test:project-technology": "node packages/cli/validate-project-technology.js",
17
+ "test:project-technology": "node packages/cli/validate-project-technology.js && node packages/cli/validate-provider-archive.js",
18
18
  "test:context-traversal": "node packages/cli/validate-context-traversal.js",
19
19
  "test:project-continuity": "node packages/cli/validate-project-continuity.js",
20
20
  "test:artifact-releases": "node packages/cli/validate-artifact-releases.js",
@@ -13,6 +13,7 @@ function usage() {
13
13
  console.error("Options: --apply --task <text> --source <file> --target-id <id>");
14
14
  console.error(" --semantic-digest <sha256:...> --provider-revision <sha>");
15
15
  console.error(" --refresh-binding --significant-work --max-objects <count>");
16
+ console.error("Archive connect: --provider-archive-trust <verified-release-anchor.json>");
16
17
  console.error("Continuity: sync --boundary task_start|stage_complete|task_complete [--evidence <json>] [--expected-graph-digest <sha256:...>]");
17
18
  console.error(" verify --significant-work [--receipt-digest <sha256:...>]");
18
19
  console.error("Context: --phase discover --task <text>");
@@ -46,7 +47,9 @@ function parse(argv) {
46
47
  const options = {};
47
48
  const values = new Set(["--task", "--source", "--target-id", "--semantic-digest", "--provider-revision", "--max-objects", "--phase", "--input", "--select", "--selection", "--packet", "--evidence", "--selector", "--reason", "--confidence", "--context-budget", "--boundary", "--expected-graph-digest", "--receipt-digest", "--state-root", "--matter-id", "--direction", "--artifact-root", "--release-date", "--release-id", "--parent-release", "--base-release", "--target-release", "--client-note", "--technology", "--scenario", "--audience", "--course-pack", "--projection"]);
48
49
  const flags = new Set(["--apply", "--refresh-binding", "--significant-work", "--create-export"]);
50
+ values.add("--provider-archive-trust");
49
51
  const names = {
52
+ "--provider-archive-trust": "providerArchiveTrustFile",
50
53
  "--task": "task", "--source": "source", "--target-id": "targetId",
51
54
  "--semantic-digest": "semanticDigest", "--provider-revision": "providerRevision",
52
55
  "--max-objects": "maxObjects", "--apply": "apply",
@@ -89,6 +92,11 @@ function readJsonFile(file) {
89
92
  }
90
93
 
91
94
  function hydrateContextOptions(request) {
95
+ if (request.options.providerArchiveTrustFile) {
96
+ if (request.operation !== "connect") throw new Error("archive trust is only supported by connect");
97
+ request.options.providerArchive = request.options.providerArchiveTrustFile === "-"
98
+ ? JSON.parse(fs.readFileSync(0, "utf8")) : readJsonFile(request.options.providerArchiveTrustFile);
99
+ }
92
100
  if (request.operation === "sync" && request.options.boundary) {
93
101
  if (request.options.boundary !== "task_start" && !request.options.evidenceFile) throw new Error(`${request.options.boundary} requires --evidence`);
94
102
  if (request.options.evidenceFile) request.options.continuityEvidence = readJsonFile(request.options.evidenceFile);
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const assert = require("assert");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const vm = require("vm");
8
+ const { createRequire } = require("module");
9
+ const { spawnSync } = require("child_process");
10
+ const technology = require("../project-technology");
11
+ const fixtureFile = path.join(__dirname, "validate-project-technology.js");
12
+ const fixtureText = fs.readFileSync(fixtureFile, "utf8");
13
+ const sandbox = { require: createRequire(fixtureFile), module: { exports: {} } };
14
+ vm.runInNewContext(fixtureText.slice(0, fixtureText.indexOf("const checks = [];")) +
15
+ "\nmodule.exports={initRepo,addTarget,git,TARGET_ID,SEMANTIC_DIGEST};", sandbox);
16
+ const fixture = sandbox.module.exports;
17
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "mirai-archive-test-")));
18
+ const originalPath = process.env.PATH;
19
+ const checks = [];
20
+ function check(id, condition, details) { checks.push({ id, passed: Boolean(condition) }); assert.ok(condition, `${id}: ${JSON.stringify(details)}`); }
21
+ function digestTree(dir) {
22
+ const files = [];
23
+ function walk(current) {
24
+ if (!fs.existsSync(current)) return;
25
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
26
+ const file = path.join(current, entry.name);
27
+ if (entry.isDirectory()) walk(file);
28
+ else files.push([path.relative(dir, file), technology.sha256(fs.readFileSync(file))]);
29
+ }
30
+ }
31
+ walk(dir); return technology.sha256(JSON.stringify(files.sort()));
32
+ }
33
+ try {
34
+ const sourceRoot = path.join(root, "source"); fs.mkdirSync(sourceRoot);
35
+ const provider = fixture.initRepo(sourceRoot, "fixture-provider");
36
+ const consumer = fixture.initRepo(sourceRoot, "fixture-consumer");
37
+ let revision = fixture.addTarget(provider);
38
+ const stateRoot = path.join(root, "state");
39
+ const base = { targetId: fixture.TARGET_ID, semanticDigest: fixture.SEMANTIC_DIGEST, stateRoot, apply: true };
40
+ for (const repo of [provider, consumer]) assert.equal(technology.execute("enable", repo, base).status, "success");
41
+ fixture.git(provider, "add", "graph.json"); fixture.git(provider, "commit", "-qm", "accepted activation");
42
+ revision = fixture.git(provider, "rev-parse", "HEAD");
43
+ const releases = [];
44
+ for (let n = 0; n < 2; n++) {
45
+ if (n) { fixture.git(provider, "commit", "--allow-empty", "-qm", "compatible revision"); revision = fixture.git(provider, "rev-parse", "HEAD"); }
46
+ const provided = technology.execute("provide", provider, { ...base, providerRevision: revision });
47
+ assert.equal(provided.status, "success");
48
+ const beforeSourceProof = digestTree(root);
49
+ const sourceProof = technology.execute("verify", provider, { source: path.join(provider, provided.export_ref) });
50
+ check(`source_export_proven_${n}`, sourceProof.status === "success" && sourceProof.provider_archive.providerRevision === revision, sourceProof);
51
+ check(`source_proof_zero_write_${n}`, digestTree(root) === beforeSourceProof);
52
+ check(`source_proof_not_authority_${n}`, technology.execute("verify", provider, { source: path.join(provider, provided.export_ref), significantWork: true }).status === "blocked");
53
+ const providerManifestPath = path.join(provider, "graph.json");
54
+ const providerManifestBytes = fs.readFileSync(providerManifestPath);
55
+ fs.appendFileSync(providerManifestPath, " ");
56
+ check(`uncommitted_manifest_blocks_source_proof_${n}`, technology.execute("verify", provider, { source: path.join(provider, provided.export_ref) }).blockers.includes("provider_manifest_not_revision_bound"));
57
+ fs.writeFileSync(providerManifestPath, providerManifestBytes);
58
+ const bytes = fs.readFileSync(path.join(provider, provided.export_ref));
59
+ const source = path.join(root, `export-${n}.json`); fs.writeFileSync(source, bytes);
60
+ check(`external_export_blocks_source_proof_${n}`, technology.execute("verify", provider, { source }).blockers.includes("provider_export_outside_repository"));
61
+ // Build anchor on the verified SOURCE side, before Git is removed.
62
+ const ancestors = n ? [releases[0].providerRevision] : [];
63
+ for (const ancestor of ancestors) fixture.git(provider, "merge-base", "--is-ancestor", ancestor, revision);
64
+ releases.push({ ...base, source, providerRevision: revision, providerArchive: {
65
+ exportSha256: technology.sha256(bytes), graphId: "fixture-provider", providerRevision: revision, ancestorRevisions: ancestors,
66
+ } });
67
+ }
68
+ const packedConsumer = path.join(root, "consumer");
69
+ fs.cpSync(consumer, packedConsumer, { recursive: true, filter: file => path.basename(file) !== ".git" });
70
+ const emptyPath = path.join(root, "no-tools"); fs.mkdirSync(emptyPath); process.env.PATH = emptyPath;
71
+ check("git_is_absent", spawnSync("git", ["--version"]).error?.code === "ENOENT");
72
+ const invoke = options => technology.execute("connect", packedConsumer, options);
73
+ const noTrust = { ...releases[0] }; delete noTrust.providerArchive;
74
+ check("untrusted_archive_blocked", invoke(noTrust).blockers.includes("provider_revision_order_unverifiable"));
75
+ const before = digestTree(root);
76
+ check("preview", invoke({ ...releases[0], apply: false }).status === "preview");
77
+ check("preview_zero_write", digestTree(root) === before);
78
+ const connected = invoke(releases[0]);
79
+ check("archive_connect", connected.status === "success", connected);
80
+ check("complete_contract_imported", connected.target_binding.status === "ready", connected);
81
+ const after = digestTree(root);
82
+ check("repeat_changed_false", invoke(releases[0]).changed === false);
83
+ check("repeat_byte_identical", digestTree(root) === after);
84
+ const status = technology.execute("status", packedConsumer, base);
85
+ check("read_only_ready", status.target_binding.status === "ready", status);
86
+ technology.execute("verify", packedConsumer, base);
87
+ check("diagnostics_zero_write", digestTree(root) === after);
88
+ function denied(id, options, blocker) {
89
+ const prior = digestTree(root); const result = invoke(options);
90
+ check(id, result.status === "fail" && (!blocker || result.blockers.includes(blocker)), result);
91
+ check(`${id}_zero_write`, prior === digestTree(root));
92
+ }
93
+ denied("null_trust", { ...releases[0], providerArchive: null }, "provider_archive_trust_invalid");
94
+ denied("bad_digest", { ...releases[0], providerArchive: { ...releases[0].providerArchive, exportSha256: "0".repeat(64) } }, "provider_archive_export_digest_mismatch");
95
+ denied("bad_graph", { ...releases[0], providerArchive: { ...releases[0].providerArchive, graphId: "other.provider" } }, "provider_archive_graph_mismatch");
96
+ denied("bad_revision", { ...releases[0], providerArchive: { ...releases[0].providerArchive, providerRevision: "f".repeat(40) } }, "provider_archive_revision_mismatch");
97
+ denied("extra_trust_fields", { ...releases[0], providerArchive: { ...releases[0].providerArchive, approved: true } }, "provider_archive_trust_invalid");
98
+ denied("explicit_refresh_required", releases[1], "target_provider_refresh_required");
99
+ denied("missing_ancestry", { ...releases[1], refreshBinding: true, providerArchive: { ...releases[1].providerArchive, ancestorRevisions: [] } }, "provider_revision_not_forward");
100
+ denied("target_mismatch", { ...releases[1], targetId: "other.target", refreshBinding: true }, "target_provider_target_mismatch");
101
+ denied("semantic_mismatch", { ...releases[1], semanticDigest: `sha256:${"3".repeat(64)}`, refreshBinding: true }, "target_provider_semantic_mismatch");
102
+ const refreshed = invoke({ ...releases[1], refreshBinding: true });
103
+ check("forward_refresh", refreshed.status === "success" && refreshed.changed, refreshed);
104
+ const refreshedTree = digestTree(root);
105
+ check("refresh_idempotent", invoke({ ...releases[1], refreshBinding: true }).changed === false);
106
+ check("refresh_byte_identical", digestTree(root) === refreshedTree);
107
+ denied("downgrade", { ...releases[0], refreshBinding: true }, "provider_revision_not_forward");
108
+ const original = fs.readFileSync(releases[1].source);
109
+ fs.appendFileSync(releases[1].source, " ");
110
+ denied("tampered_bytes", { ...releases[1], refreshBinding: true }, "provider_archive_export_digest_mismatch");
111
+ fs.writeFileSync(releases[1].source, original);
112
+ function alteredExport(id, alter, expected) {
113
+ const payload = JSON.parse(original); alter(payload);
114
+ fs.writeFileSync(releases[1].source, technology.canonicalBytes(payload));
115
+ const options = { ...releases[1], refreshBinding: true, providerArchive: { ...releases[1].providerArchive,
116
+ exportSha256: technology.sha256(fs.readFileSync(releases[1].source)), graphId: payload.provider_graph_id } };
117
+ denied(id, options, expected); fs.writeFileSync(releases[1].source, original);
118
+ }
119
+ alteredExport("provider_conflict", p => { p.provider_graph_id = "another.provider"; }, "target_provider_graph_conflict");
120
+ alteredExport("incomplete_contract", p => { p.requirement_bindings = []; });
121
+ alteredExport("contract_tamper", p => { p.goal_binding.done_when_ids = ["done.other"]; });
122
+ alteredExport("valid_but_different_contract", p => {
123
+ p.allowed_change_scope[0].file_patterns = ["packages/other/**"];
124
+ p.execution_contract_digest = technology.sha256(technology.canonicalBytes(technology.normalizeExecutionContract(p).contract));
125
+ }, "provider_execution_contract_refresh_mismatch");
126
+ alteredExport("reviewed_not_accepted", p => { p.architecture_contract.lifecycle = "reviewed"; });
127
+ alteredExport("private_extra_field", p => { p.private_source = "not allowed"; }, "provider_export_not_bounded");
128
+ const graphFile = path.join(packedConsumer, "graph.json"); const graphBefore = fs.readFileSync(graphFile);
129
+ const graph = JSON.parse(graphBefore); graph.extensions["mirai.project_technology"].enabled = false;
130
+ fs.writeFileSync(graphFile, JSON.stringify(graph));
131
+ denied("disabled", releases[1], "project_technology_not_enabled");
132
+ check("disabled_diagnostics_available", technology.execute("status", packedConsumer, base).operation_mode === "read_only");
133
+ fs.writeFileSync(graphFile, graphBefore);
134
+ const trustFile = path.join(root, "trust.json"); fs.writeFileSync(trustFile, JSON.stringify(releases[1].providerArchive));
135
+ const cli = spawnSync(process.execPath, [path.join(__dirname, "project-technology.js"), "connect", packedConsumer,
136
+ "--source", releases[1].source, "--target-id", base.targetId, "--semantic-digest", base.semanticDigest,
137
+ "--provider-revision", releases[1].providerRevision, "--state-root", stateRoot,
138
+ "--provider-archive-trust", trustFile, "--apply"], { encoding: "utf8" });
139
+ check("cli_no_git_idempotent", cli.status === 0 && JSON.parse(cli.stdout).changed === false, cli.stdout);
140
+ // A new identity field is optional for historical Git exports, but required
141
+ // for authenticated archive imports. Existing stored contracts remain valid.
142
+ const legacy = JSON.parse(original); delete legacy.provider_graph_id;
143
+ fs.writeFileSync(releases[1].source, technology.canonicalBytes(legacy));
144
+ check("historical_export_readable", technology.readExport(releases[1].source).blockers.length === 0);
145
+ denied("legacy_archive_needs_graph_identity", { ...releases[1], providerArchive: { ...releases[1].providerArchive,
146
+ exportSha256: technology.sha256(fs.readFileSync(releases[1].source)) } }, "provider_archive_graph_mismatch");
147
+ fs.writeFileSync(releases[1].source, original);
148
+ const folderInventory = technology.inventory(packedConsumer);
149
+ check("folder_inventory_nonempty", folderInventory.files.some(file => file.path === "graph.json") && folderInventory.files.some(file => file.path === "README.md"), folderInventory);
150
+ check("folder_inventory_sync", technology.execute("sync", packedConsumer, base).status === "success");
151
+ check("folder_inventory_sync_noop", technology.execute("sync", packedConsumer, base).changed === false);
152
+ const readme = path.join(packedConsumer, "README.md"); const readmeBefore = fs.readFileSync(readme);
153
+ fs.appendFileSync(readme, "\nChanged declared raw source\n");
154
+ const staleTree = digestTree(root);
155
+ check("folder_changed_source_is_stale", technology.execute("status", packedConsumer, base).blockers.includes("project_technology_inventory_stale"));
156
+ check("folder_stale_status_zero_write", digestTree(root) === staleTree);
157
+ fs.writeFileSync(readme, readmeBefore);
158
+ const graphForMissing = JSON.parse(fs.readFileSync(graphFile)); graphForMissing.graph.raw_sources.push("missing-required-source.md");
159
+ fs.writeFileSync(graphFile, JSON.stringify(graphForMissing));
160
+ const missingBefore = digestTree(root);
161
+ check("missing_declared_source_blocks_sync", technology.execute("sync", packedConsumer, base).blockers.includes("inventory_declared_source_missing"));
162
+ check("missing_source_sync_zero_write", digestTree(root) === missingBefore);
163
+ fs.writeFileSync(graphFile, graphBefore);
164
+ console.log(JSON.stringify({ status: "success", check_count: checks.length, checks }, null, 2));
165
+ } finally { process.env.PATH = originalPath; fs.rmSync(root, { recursive: true, force: true }); }
@@ -36,7 +36,7 @@ const TARGET_EXPORT_KEYS = new Set([
36
36
  "schema_version", "target_id", "semantic_digest", "provider_revision",
37
37
  "decision_refs", "goal_binding", "requirement_bindings", "constraint_ids",
38
38
  "non_goal_ids", "deferred_boundary_ids", "allowed_change_scope",
39
- "architecture_contract", "execution_contract_digest",
39
+ "architecture_contract", "execution_contract_digest", "provider_graph_id",
40
40
  ]);
41
41
  const SECRET_PARTS = [".env", "credential", "secret", "token", "password", "private-key", "id_rsa", ".pem", ".p12"];
42
42
  const EXCLUDED_PARTS = new Set([".git", ".mirai-graph", ".simai", "node_modules", "vendor", "dist", "build", "coverage", "generated"]);
@@ -425,6 +425,7 @@ function readExport(filePath) {
425
425
  try { payload = JSON.parse(bytes.toString("utf8").replace(/^\uFEFF/, "")); } catch (_) { return { export: {}, blockers: ["provider_export_invalid"] }; }
426
426
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { export: {}, blockers: ["provider_export_invalid"] };
427
427
  if (Object.keys(payload).some((key) => !TARGET_EXPORT_KEYS.has(key))) blockers.push("provider_export_not_bounded");
428
+ if (Object.hasOwn(payload, "provider_graph_id") && (typeof payload.provider_graph_id !== "string" || !REF_RE.test(payload.provider_graph_id))) blockers.push("provider_graph_id_invalid");
428
429
  const identity = bindingValues(payload); blockers.push(...identity.blockers);
429
430
  const executionFields = [
430
431
  "decision_refs", "goal_binding", "requirement_bindings", "constraint_ids",
@@ -435,7 +436,7 @@ function readExport(filePath) {
435
436
  const digest = sha256(canonicalBytes(normalized.contract));
436
437
  if (!payload.execution_contract_digest) blockers.push("provider_execution_contract_digest_missing");
437
438
  else if (payload.execution_contract_digest !== digest) blockers.push("provider_execution_contract_digest_mismatch");
438
- return { export: { schema_version: "1.0.0", ...identity.values, ...normalized.contract, execution_contract_digest: digest }, blockers: [...new Set(blockers)].sort(), raw_sha256: sha256(bytes) };
439
+ return { export: { schema_version: "1.0.0", ...identity.values, ...normalized.contract, execution_contract_digest: digest, ...(payload.provider_graph_id ? { provider_graph_id: payload.provider_graph_id } : {}) }, blockers: [...new Set(blockers)].sort(), raw_sha256: sha256(bytes) };
439
440
  }
440
441
 
441
442
  function trackedFiles(repo) {
@@ -453,7 +454,42 @@ function safeTrackedFile(relative) {
453
454
 
454
455
  function inventory(repo) {
455
456
  const entries = [];
456
- for (const relative of trackedFiles(repo).filter(safeTrackedFile).sort()) {
457
+ const blockers = [];
458
+ let files = trackedFiles(repo);
459
+ const revision = git(repo, "rev-parse", "HEAD") || null;
460
+ if (!revision) {
461
+ if (fs.existsSync(path.join(repo, ".git"))) blockers.push("inventory_git_unavailable");
462
+ else {
463
+ // Ordinary folders and immutable distributions have no Git index. Use
464
+ // only explicitly declared graph/raw sources, never scan arbitrary data.
465
+ const graph = readManifest(repo).manifest?.graph || {};
466
+ const refs = ["graph.json", ...(graph.source_of_truth || []), ...(graph.objects || []),
467
+ ...(graph.relations || []), ...(graph.schemas || []), ...(graph.raw_sources || [])];
468
+ const found = new Set();
469
+ const visited = new Set();
470
+ function collect(relative) {
471
+ if (typeof relative !== "string" || !relative || path.isAbsolute(relative) || relative.includes("\\") || relative.includes(":") || relative.split("/").includes("..") || /[?*\[\]]/.test(relative)) {
472
+ blockers.push("inventory_declared_source_unsafe_or_unsupported"); return;
473
+ }
474
+ if (!safeTrackedFile(relative)) return;
475
+ if (visited.has(relative)) return;
476
+ visited.add(relative);
477
+ if (visited.size > 10000) { blockers.push("inventory_declared_source_budget_exceeded"); return; }
478
+ const absolute = path.join(repo, relative);
479
+ if (!fs.existsSync(absolute)) { blockers.push("inventory_declared_source_missing"); return; }
480
+ if (fs.lstatSync(absolute).isSymbolicLink() || !fs.realpathSync(absolute).startsWith(`${fs.realpathSync(repo)}${path.sep}`)) {
481
+ blockers.push("inventory_declared_source_unsafe_or_unsupported"); return;
482
+ }
483
+ if (fs.statSync(absolute).isDirectory()) {
484
+ for (const name of fs.readdirSync(absolute).sort()) collect(`${relative.replace(/\/$/, "")}/${name}`);
485
+ } else if (fs.statSync(absolute).isFile()) found.add(relative);
486
+ else blockers.push("inventory_declared_source_unsafe_or_unsupported");
487
+ }
488
+ refs.forEach(collect);
489
+ files = [...found];
490
+ }
491
+ }
492
+ for (const relative of files.filter(safeTrackedFile).sort()) {
457
493
  const absolute = path.join(repo, relative);
458
494
  if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink() || !fs.statSync(absolute).isFile()) continue;
459
495
  const bytes = fs.readFileSync(absolute);
@@ -462,10 +498,11 @@ function inventory(repo) {
462
498
  const payload = {
463
499
  schema_version: "1.0.0",
464
500
  repository_id: readManifest(repo).manifest?.id || path.basename(repo),
465
- revision: git(repo, "rev-parse", "HEAD") || null,
501
+ revision,
466
502
  files: entries,
467
503
  };
468
504
  payload.inventory_digest = sha256(canonicalBytes(payload), true);
505
+ if (blockers.length) payload.blockers = [...new Set(blockers)].sort();
469
506
  return payload;
470
507
  }
471
508
 
@@ -521,6 +558,7 @@ function targetBindingStatus(repo, options = {}) {
521
558
  provider_export_sha256: binding.provider_export_sha256 || null,
522
559
  execution_contract: Object.fromEntries(["decision_refs", "goal_binding", "requirement_bindings", "constraint_ids", "non_goal_ids", "deferred_boundary_ids", "allowed_change_scope", "architecture_contract"].map((key) => [key, exported.export[key]])),
523
560
  execution_contract_digest: exported.export.execution_contract_digest || null,
561
+ provider_graph_id: exported.export.provider_graph_id || null,
524
562
  blockers: [...new Set(blockers)].sort(),
525
563
  next_action: blockers.length ? "refresh or repair the exact provider binding" : "none",
526
564
  };
@@ -535,7 +573,7 @@ function status(repoArg, options = {}) {
535
573
  try { if (fs.existsSync(path.join(root, INVENTORY_FILE))) stored = readJson(path.join(root, INVENTORY_FILE)); } catch (_) { /* stale */ }
536
574
  const current = inventory(repo);
537
575
  const freshness = stored && stored.inventory_digest === current.inventory_digest ? "current" : stored ? "stale" : "missing";
538
- const blockers = [...manifestState.blockers, ...ext.blockers];
576
+ const blockers = [...manifestState.blockers, ...ext.blockers, ...(current.blockers || [])];
539
577
  if (!stored && legacyRuntimeState(repo).present) blockers.push("project_technology_host_state_migration_required");
540
578
  if (ext.contract && ext.contract.enabled === false) blockers.push("project_technology_disabled");
541
579
  if (freshness !== "current") blockers.push(`project_technology_inventory_${freshness}`);
@@ -580,6 +618,8 @@ function enable(repoArg, options = {}) {
580
618
  const repo = normalizeRepo(repoArg);
581
619
  const manifestState = readManifest(repo);
582
620
  if (!manifestState.manifest || manifestState.blockers.length) return result("enable", "transactional", "fail", { blockers: manifestState.blockers, next_action: "repair graph.json" });
621
+ const inventoryPreflight = inventory(repo);
622
+ if (inventoryPreflight.blockers?.length) return result("enable", "transactional", "blocked", { blockers: inventoryPreflight.blockers, next_action: "repair declared graph sources" });
583
623
  const manifest = structuredClone(manifestState.manifest);
584
624
  const extensions = { ...(manifest.extensions || {}) };
585
625
  const legacy = extensions[LEGACY_EXTENSION_KEY];
@@ -617,7 +657,7 @@ function sync(repoArg, options = {}) {
617
657
  const repo = normalizeRepo(repoArg);
618
658
  const manifestState = readManifest(repo);
619
659
  const ext = extensionState(manifestState.manifest);
620
- const blockers = [...manifestState.blockers, ...ext.blockers];
660
+ const blockers = [...manifestState.blockers, ...ext.blockers, ...(inventory(repo).blockers || [])];
621
661
  const hostRoot = runtimeRoot(repo, manifestState.manifest, options);
622
662
  if (legacyRuntimeState(repo).present && !fs.existsSync(path.join(hostRoot, INVENTORY_FILE))) blockers.push("project_technology_host_state_migration_required");
623
663
  if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
@@ -661,7 +701,7 @@ function provide(repoArg, options = {}) {
661
701
  blockers.push(...target.blockers);
662
702
  if (blockers.length) return result("provide", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "repair the accepted target contract" });
663
703
  const executionContractDigest = sha256(canonicalBytes(target.contract));
664
- const payload = { schema_version: "1.0.0", ...identity.values, ...target.contract, execution_contract_digest: executionContractDigest };
704
+ const payload = { schema_version: "1.0.0", ...identity.values, ...target.contract, execution_contract_digest: executionContractDigest, provider_graph_id: manifestState.manifest.id };
665
705
  const changed = atomicWrite(path.join(repo, EXPORT_FILE), canonicalBytes(payload));
666
706
  return result("provide", "transactional", "success", { changed, export_ref: EXPORT_FILE, target_binding: payload });
667
707
  }
@@ -671,6 +711,32 @@ function providerRootFor(exportPath) {
671
711
  return completed.status === 0 ? completed.stdout.trim() : null;
672
712
  }
673
713
 
714
+ // This is explicit consumer trust, NOT an assertion read from the provider.
715
+ // The caller must obtain it from authenticated, checksum-bound release metadata.
716
+ // Keeping it out of the export prevents an archive from authenticating itself.
717
+ function archiveProviderIdentity(source, exported, anchor) {
718
+ const fail = code => ({ blockers: [code], ancestors: [] });
719
+ if (!anchor || typeof anchor !== "object" || Array.isArray(anchor)) return fail("provider_archive_trust_invalid");
720
+ const keys = ["exportSha256", "graphId", "providerRevision", "ancestorRevisions"];
721
+ if (Object.keys(anchor).some(key => !keys.includes(key)) || keys.some(key => !Object.hasOwn(anchor, key))) return fail("provider_archive_trust_invalid");
722
+ if (!/^[0-9a-f]{64}$/.test(anchor.exportSha256 || "") ||
723
+ typeof anchor.graphId !== "string" || !REF_RE.test(anchor.graphId) ||
724
+ !REVISION_RE.test(anchor.providerRevision || "") ||
725
+ !Array.isArray(anchor.ancestorRevisions) || anchor.ancestorRevisions.length > 4096 ||
726
+ anchor.ancestorRevisions.some(rev => typeof rev !== "string" || !REVISION_RE.test(rev) || rev === anchor.providerRevision) ||
727
+ new Set(anchor.ancestorRevisions).size !== anchor.ancestorRevisions.length) return fail("provider_archive_trust_invalid");
728
+ try {
729
+ // Parent aliases such as macOS /var are allowed; the exact bytes are pinned.
730
+ if (fs.lstatSync(source).isSymbolicLink()) return fail("provider_archive_source_unsafe");
731
+ if (!fs.statSync(source).isFile() || fs.statSync(source).size > 1024 * 1024) return fail("provider_archive_source_unsafe");
732
+ const bytes = fs.readFileSync(source);
733
+ if (sha256(bytes) !== anchor.exportSha256 || canonicalBytes(JSON.parse(bytes)) !== canonicalBytes(exported)) return fail("provider_archive_export_digest_mismatch");
734
+ } catch (_) { return fail("provider_archive_source_unsafe"); }
735
+ if (exported.provider_graph_id !== anchor.graphId) return fail("provider_archive_graph_mismatch");
736
+ if (exported.provider_revision !== anchor.providerRevision) return fail("provider_archive_revision_mismatch");
737
+ return { blockers: [], ancestors: anchor.ancestorRevisions };
738
+ }
739
+
674
740
  function connect(repoArg, options = {}) {
675
741
  const repo = normalizeRepo(repoArg);
676
742
  const source = path.resolve(String(options.source || ""));
@@ -681,14 +747,20 @@ function connect(repoArg, options = {}) {
681
747
  const blockers = [...identity.blockers, ...read.blockers, ...manifestState.blockers, ...ext.blockers];
682
748
  if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
683
749
  for (const key of Object.keys(identity.values)) if (read.export[key] !== identity.values[key]) blockers.push(`${key}_mismatch`);
684
- const providerRoot = providerRootFor(source);
685
- if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
750
+ const archiveRequested = Object.hasOwn(options, "providerArchive");
751
+ const archive = archiveRequested ? archiveProviderIdentity(source, read.export, options.providerArchive) : null;
752
+ const providerRoot = archiveRequested ? null : providerRootFor(source);
753
+ if (archive) blockers.push(...archive.blockers);
754
+ else if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
686
755
  else if (git(providerRoot, "rev-parse", "HEAD").toLowerCase() !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
687
756
  const root = runtimeRoot(repo, manifestState.manifest, options);
688
757
  const currentPath = path.join(root, BINDING_FILE);
689
758
  let current = null;
690
759
  try { if (fs.existsSync(currentPath)) current = readJson(currentPath); } catch (_) { blockers.push("current_target_binding_invalid"); }
691
760
  if (current) {
761
+ const currentExport = targetBindingStatus(repo, options);
762
+ const existingGraphId = currentExport.provider_graph_id;
763
+ if (existingGraphId && existingGraphId !== read.export.provider_graph_id) blockers.push("target_provider_graph_conflict");
692
764
  if (!options.refreshBinding) {
693
765
  const same = Object.keys(identity.values).every((key) => current[key] === identity.values[key]);
694
766
  if (!same) blockers.push("target_provider_conflict", "target_provider_refresh_required");
@@ -703,7 +775,9 @@ function connect(repoArg, options = {}) {
703
775
  const currentState = targetBindingStatus(repo, options);
704
776
  if (currentState.status !== "ready") blockers.push(...currentState.blockers);
705
777
  if (currentState.execution_contract_digest !== read.export.execution_contract_digest) blockers.push("provider_execution_contract_refresh_mismatch");
706
- if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
778
+ if (archive) {
779
+ if (current.provider_revision !== identity.values.provider_revision && !archive.ancestors.includes(current.provider_revision)) blockers.push("provider_revision_not_forward");
780
+ } else if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
707
781
  else {
708
782
  if (current.provider_revision !== identity.values.provider_revision) {
709
783
  const ancestry = spawnSync("git", ["-C", providerRoot, "merge-base", "--is-ancestor", current.provider_revision, identity.values.provider_revision]);
@@ -768,7 +842,40 @@ function repair(repoArg, options = {}) {
768
842
  return sync(repo, options);
769
843
  }
770
844
 
845
+ function verifyProviderExport(repoArg, options = {}) {
846
+ const repo = normalizeRepo(repoArg);
847
+ const source = path.resolve(options.source || path.join(repo, EXPORT_FILE));
848
+ const exported = readExport(source);
849
+ const manifestState = readManifest(repo);
850
+ const ext = extensionState(manifestState.manifest);
851
+ const identity = bindingValues(exported.export);
852
+ const blockers = [...exported.blockers, ...manifestState.blockers, ...ext.blockers, ...identity.blockers];
853
+ if (options.significantWork) blockers.push("provider_export_verification_is_not_execution_authority");
854
+ if (!trackedFiles(repo).includes("graph.json") || spawnSync("git", ["diff", "--quiet", "HEAD", "--", "graph.json"], { cwd: repo }).status !== 0) blockers.push("provider_manifest_not_revision_bound");
855
+ if (!source.startsWith(`${repo}${path.sep}`)) blockers.push("provider_export_outside_repository");
856
+ if (ext.contract?.enabled !== true || ext.legacy) blockers.push("project_technology_not_enabled");
857
+ const head = git(repo, "rev-parse", "HEAD");
858
+ if (head !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
859
+ if (exported.export.provider_graph_id !== manifestState.manifest?.id) blockers.push("provider_archive_graph_mismatch");
860
+ const target = targetContract(repo, identity.values.target_id, identity.values.semantic_digest, manifestState.manifest);
861
+ blockers.push(...target.blockers);
862
+ if (sha256(canonicalBytes(target.contract)) !== exported.export.execution_contract_digest) blockers.push("provider_execution_contract_source_mismatch");
863
+ // Bound supported ancestry; deeper histories need a narrower supported
864
+ // release window rather than unbounded metadata in every consumer.
865
+ const history = git(repo, "rev-list", "--max-count=4098", "HEAD").split("\n").filter(Boolean);
866
+ if (history[0] !== head || history.length > 4097) blockers.push("provider_archive_ancestry_unverifiable_or_too_large");
867
+ return result("verify", "read_only", blockers.length ? "blocked" : "success", {
868
+ blockers: [...new Set(blockers)].sort(),
869
+ provider_archive: blockers.length ? null : {
870
+ exportSha256: exported.raw_sha256, graphId: manifestState.manifest.id,
871
+ providerRevision: head, ancestorRevisions: history.slice(1),
872
+ },
873
+ next_action: blockers.length ? "repair the canonical provider export before packaging" : "seal this anchor in authenticated release metadata",
874
+ });
875
+ }
876
+
771
877
  function verify(repoArg, options = {}) {
878
+ if (options.source) return verifyProviderExport(repoArg, options);
772
879
  const state = status(repoArg, options);
773
880
  const blockers = [...state.blockers];
774
881
  const repo = normalizeRepo(repoArg);
@@ -835,6 +942,7 @@ module.exports = {
835
942
  sync,
836
943
  targetBindingStatus,
837
944
  verify,
945
+ verifyProviderExport,
838
946
  verifyArtifactRelease: artifacts.verifyArtifactRelease,
839
947
  verifyTechnologyCourse: technologyCourse.verifyTechnologyCourse,
840
948
  verifyContext: traversal.verifyContext,
@@ -0,0 +1,37 @@
1
+ # Mirai Graph 1.5.0
2
+
3
+ Compatible additive release for existing 1.x consumers. Published to npm under
4
+ `legacy-1`; it does not replace the 2.x `latest` channel or migrate consumers.
5
+
6
+ ## Changes
7
+
8
+ - Existing Project Technology connect/refresh can import a bounded provider
9
+ export without Git when an authenticated distributor supplies its exact
10
+ export digest, graph identity, revision and forward ancestry proof.
11
+ - Read-only source verification binds that proof to the accepted target and
12
+ real Git revision before packaging. Caller-supplied trust is not inferred
13
+ from an export's own claims and never grants permission to perform work.
14
+ - Folder inventory follows only explicitly declared graph and raw-source paths,
15
+ detects stale content and rejects unsafe or missing sources.
16
+ - CLI archive trust can be passed through stdin without a temporary trust file.
17
+
18
+ ## Compatibility and safety
19
+
20
+ Existing Git provider operations, complete target and execution contracts,
21
+ transactional rollback and fail-closed behavior remain in force. No new profile,
22
+ registry, daemon or schema family is introduced. The graph manifest remains
23
+ 2.0.0 and the Project Technology activation contract remains 1.0.0.
24
+
25
+ Archive metadata must come from an independently authenticated release; this
26
+ package does not establish a distributor's trust or copy private source content.
27
+
28
+ ## Verification
29
+
30
+ `npm run release:check` runs the current suite, including 39 existing Project
31
+ Technology checks and 71 archive/source-proof checks. The archive cases cover
32
+ no-Git import, exact replay, forward refresh, downgrade, tamper, graph conflict,
33
+ missing trust, source inventory and read-only behavior.
34
+
35
+ Local validation uses macOS ARM64 with Node 22.23.2. Federation's native
36
+ Windows/macOS/Linux complete installation matrix is a separate acceptance gate;
37
+ this release does not claim that matrix or a Federation release has passed.
@@ -15,6 +15,8 @@ Release notes must separate:
15
15
 
16
16
  ## Release Notes
17
17
 
18
+ - [v1.5.0](1.5.0.md) - compatible 1.x authenticated archive providers and
19
+ declared-source inventory without Git (`legacy-1`, not npm `latest`).
18
20
  - [v1.2.0](1.2.0.md) - portable task-boundary project continuity in Project
19
21
  Technology.
20
22
  - [v1.1.0](1.1.0.md) - universal sequential context traversal and usage
@@ -15,6 +15,7 @@
15
15
  "target_id": { "type": "string", "minLength": 2 },
16
16
  "semantic_digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
17
17
  "provider_revision": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
18
+ "provider_graph_id": { "type": "string", "minLength": 2, "maxLength": 256, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/@?=+*-]+$" },
18
19
  "decision_refs": { "$ref": "#/$defs/nonEmptyRefs" },
19
20
  "goal_binding": {
20
21
  "type": "object", "additionalProperties": false,
@@ -7,6 +7,59 @@ not a profile, a second graph or a source of domain methodology.
7
7
 
8
8
  ## One Mechanism, Different Graphs
9
9
 
10
+ ### Archived providers (unreleased compatibility extension)
11
+
12
+ The normal source provider path continues to verify Git HEAD and ancestry.
13
+ For an immutable distribution, `connect` also accepts `providerArchive` in the
14
+ JavaScript options, or `--provider-archive-trust <file>` in the CLI (use `-`
15
+ to read that bounded JSON from stdin without creating a temporary file):
16
+
17
+ ```json
18
+ {
19
+ "exportSha256": "<SHA-256 of the exact bounded export bytes>",
20
+ "graphId": "example.provider",
21
+ "providerRevision": "<exact 40-character source revision>",
22
+ "ancestorRevisions": ["<previous supported revision, proven during release build>"]
23
+ }
24
+ ```
25
+
26
+ This is a **consumer trust input**, not provider self-authentication. The caller
27
+ must derive it from an authenticated release lock/manifest after checking its
28
+ integrity and issuer. Never calculate a hash from an untrusted download and
29
+ call that authentication. Mirai does not discover, download or trust adjacent
30
+ metadata automatically. Packaging must verify the export against the accepted
31
+ source target and the declared source revision before publishing its digest.
32
+ Ancestor entries must be proven by the source Git history during that build;
33
+ they are not inferred from version numbers, dates or commit hash ordering.
34
+
35
+ The existing `technology verify <repository> --source <bounded-export>` and
36
+ `verifyProviderExport(repository, options)` provide a read-only source-side
37
+ proof for packaging. They require a revision-bound enabled manifest, exact HEAD,
38
+ accepted target and matching execution contract, and return the anchor with at
39
+ most 4096 proven ancestors. A packaging caller must then authenticate the release
40
+ metadata containing this anchor. This proof is not permission to execute a
41
+ significant task; requesting significant work through it is blocked.
42
+
43
+ Exports produced by `provide` now include `provider_graph_id`. Old Git-backed
44
+ exports remain readable; archive import requires the graph identity. `connect`
45
+ uses the same full accepted execution contract validation and atomic import.
46
+ `--refresh-binding` still requires the same target and semantic/architecture
47
+ contract; a changed revision must explicitly descend from the current binding
48
+ in the authenticated release metadata. A wrong or incomplete anchor fails,
49
+ even when a working Git checkout happens to be available. The caller cannot
50
+ use archive trust to authorize work, change owners, expand scope or bypass
51
+ acceptance. `provide` continues to require the canonical Git source; archive
52
+ consumers import its immutable output, never regenerate acceptance from copies.
53
+
54
+ Status/verify use the hash-bound local import and remain read-only. Without a
55
+ Git repository, inventory reads only the manifest and explicitly declared graph
56
+ and raw-source paths, with a bounded traversal. Missing, unsafe or unsupported
57
+ declared paths block sync before writes. Symlinks and secret/excluded paths are
58
+ not imported. Ordinary folders use content digests and a null inventory revision,
59
+ never a fabricated Git revision. If Git metadata exists but Git is unavailable,
60
+ inventory fails closed instead of silently treating a checkout as an archive.
61
+ Raw-source access and release authenticity remain separate responsibilities.
62
+
10
63
  The root `graph.json` declares the repository scope and profiles. The same
11
64
  Project Technology operations therefore work for:
12
65