mirai-graph 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,9 @@ jobs:
26
26
  - name: Verify deterministic context traversal
27
27
  run: node packages/cli/validate-context-traversal.js
28
28
 
29
+ - name: Verify portable project continuity
30
+ run: node packages/cli/validate-project-continuity.js
31
+
29
32
  release-check:
30
33
  runs-on: ubuntu-latest
31
34
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to Mirai Graph will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.0] - 2026-08-27
8
+
9
+ ### Added
10
+
11
+ - Portable project continuity in the existing Project Technology `sync`,
12
+ `context` and `verify` operations.
13
+ - Task-boundary evidence compaction into accepted facts, evidence, reusable
14
+ regression cases and bounded proposals in `graph/specs`.
15
+ - Host-local receipts keyed by graph identity, with lease, compare-and-swap,
16
+ backup, atomic write, readback and rollback support for shared non-Git folders.
17
+ - Automatic discovery of relevant previous cases through the existing context
18
+ traversal protocol.
19
+
20
+ ### Safety Boundary
21
+
22
+ - Chat transcripts, secrets, private source, user paths and unverified claims
23
+ are not portable project memory.
24
+ - New goals, architecture changes and ownership changes remain proposals.
25
+ - Generated human context is a projection only and never authorizes writes.
26
+ - The public `graph.json` schema and Project Technology activation contract
27
+ remain `2.0.0` and `1.0.0` respectively.
28
+
29
+ ### Verification
30
+
31
+ - New fixtures cover Git and non-Git projects, two-installation portability,
32
+ idempotence, stale receipts, concurrent-write protection, secret rejection,
33
+ proposal boundaries and two-case method promotion.
34
+
7
35
  ## [1.1.1] - 2026-08-26
8
36
 
9
37
  ### Fixed
package/CITATION.cff CHANGED
@@ -17,5 +17,5 @@ keywords:
17
17
  - complex systems
18
18
  - context engineering
19
19
  - systems engineering
20
- version: "1.1.0"
21
- date-released: "2026-08-26"
20
+ version: "1.2.0"
21
+ date-released: "2026-08-27"
package/README.md CHANGED
@@ -10,7 +10,7 @@ Use it when a software project has useful knowledge spread across README files,
10
10
  docs, issues, chats, code comments and AI prompts, and you need a small
11
11
  machine-readable map that developers and AI assistants can validate and reuse.
12
12
 
13
- Status: stable public `1.1.0` release.
13
+ Status: stable public `1.2.0` release.
14
14
 
15
15
  ## What Problem Does It Solve?
16
16
 
@@ -42,7 +42,7 @@ Start with a normal repository and use Mirai Graph to:
42
42
 
43
43
  ## Current Installation Status
44
44
 
45
- The repository is ready as the stable `mirai-graph@1.1.0` package. To verify a
45
+ The repository provides the stable `mirai-graph@1.2.0` package. To verify a
46
46
  repository checkout directly, use:
47
47
 
48
48
  ```bash
@@ -96,7 +96,8 @@ Enable it in the existing root manifest:
96
96
  "contract_version": "1.0.0",
97
97
  "enabled": true,
98
98
  "context_policy": "task_scoped",
99
- "source_boundary": "hybrid_sot"
99
+ "source_boundary": "hybrid_sot",
100
+ "continuity_policy": "task_boundary"
100
101
  }
101
102
  }
102
103
  }
@@ -130,6 +131,17 @@ The programmatic API exposes the same operations as `discoverContext`,
130
131
  `expandContext`, `compileContext` and `verifyContext`. Mirai Graph does not call
131
132
  or depend on a particular AI model.
132
133
 
134
+ Verified project experience can be saved without storing a chat transcript:
135
+
136
+ ```bash
137
+ mirai-graph technology sync . --boundary task_complete --evidence continuity-evidence.json
138
+ mirai-graph technology sync . --boundary task_complete --evidence continuity-evidence.json --apply
139
+ ```
140
+
141
+ Accepted facts and reusable cases live in `graph/specs`; mutable receipts and
142
+ rollback data stay host-local. The same contract works in Git repositories and
143
+ ordinary shared folders.
144
+
133
145
  Changing operations return a preview unless `--apply` is explicit. See
134
146
  [Project Technology](standard/project-technology.md) for provider/consumer
135
147
  binding, local state and safety boundaries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirai-graph",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "description": "An evolutionary graph operating model for managing the growth of complex systems.",
6
6
  "license": "MIT",
@@ -13,9 +13,10 @@
13
13
  "./project-technology": "./packages/project-technology/index.js"
14
14
  },
15
15
  "scripts": {
16
- "pretest": "npm run test:project-technology && npm run test:context-traversal && npm run validate:project-technology-schemas",
16
+ "pretest": "npm run test:project-technology && npm run test:context-traversal && npm run test:project-continuity && npm run validate:project-technology-schemas",
17
17
  "test:project-technology": "node packages/cli/validate-project-technology.js",
18
18
  "test:context-traversal": "node packages/cli/validate-context-traversal.js",
19
+ "test:project-continuity": "node packages/cli/validate-project-continuity.js",
19
20
  "validate:project-technology-schemas": "node -e \"const fs=require('fs'); for (const file of ['project-technology-extension.schema.json','project-technology-target-export.schema.json','context-traversal-receipt.schema.json','context-pack.schema.json']) JSON.parse(fs.readFileSync('schemas/'+file))\"",
20
21
  "validate:graph-manifest": "node packages/cli/validate-graph-manifest-migration.js",
21
22
  "validate:minimal": "node packages/cli/validate-mirai-graph.js examples/minimal-graph",
@@ -130,7 +130,8 @@ function validateManifest(manifest, repo = null) {
130
130
  };
131
131
  for (const field of Object.keys(expected)) if (technology[field] !== expected[field]) errors.push(`extensions.mirai.project_technology.${field} is invalid`);
132
132
  if (typeof technology.enabled !== "boolean") errors.push("extensions.mirai.project_technology.enabled is invalid");
133
- for (const field of Object.keys(technology)) if (!(field in expected) && field !== "enabled") errors.push(`extensions.mirai.project_technology has unknown field ${field}`);
133
+ if (technology.continuity_policy !== undefined && technology.continuity_policy !== "task_boundary") errors.push("extensions.mirai.project_technology.continuity_policy is invalid");
134
+ for (const field of Object.keys(technology)) if (!(field in expected) && !["enabled", "continuity_policy"].includes(field)) errors.push(`extensions.mirai.project_technology has unknown field ${field}`);
134
135
  }
135
136
  }
136
137
  if (manifest.graph !== null) {
@@ -13,6 +13,8 @@ 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("Continuity: sync --boundary task_start|stage_complete|task_complete [--evidence <json>] [--expected-graph-digest <sha256:...>]");
17
+ console.error(" verify --significant-work [--receipt-digest <sha256:...>]");
16
18
  console.error("Context: --phase discover --task <text>");
17
19
  console.error(" --phase expand --input <receipt.json> --select <id> [--select <id>]");
18
20
  console.error(" --phase compile --input <receipt.json> --selection <selection.json>");
@@ -25,7 +27,7 @@ function parse(argv) {
25
27
  let repository = ".";
26
28
  if (argv[0] && !argv[0].startsWith("--")) repository = argv.shift();
27
29
  const options = {};
28
- 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"]);
30
+ 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"]);
29
31
  const flags = new Set(["--apply", "--refresh-binding", "--significant-work"]);
30
32
  const names = {
31
33
  "--task": "task", "--source": "source", "--target-id": "targetId",
@@ -36,6 +38,8 @@ function parse(argv) {
36
38
  "--selection": "selectionFile", "--packet": "packetFile", "--evidence": "evidenceFile",
37
39
  "--selector": "selector", "--reason": "reason", "--confidence": "confidence",
38
40
  "--context-budget": "contextBudget",
41
+ "--boundary": "boundary", "--expected-graph-digest": "expectedGraphDigest",
42
+ "--receipt-digest": "receiptDigest", "--state-root": "stateRoot",
39
43
  };
40
44
  while (argv.length) {
41
45
  const token = argv.shift();
@@ -57,6 +61,11 @@ function readJsonFile(file) {
57
61
  }
58
62
 
59
63
  function hydrateContextOptions(request) {
64
+ if (request.operation === "sync" && request.options.boundary) {
65
+ if (request.options.boundary !== "task_start" && !request.options.evidenceFile) throw new Error(`${request.options.boundary} requires --evidence`);
66
+ if (request.options.evidenceFile) request.options.continuityEvidence = readJsonFile(request.options.evidenceFile);
67
+ return;
68
+ }
60
69
  if (request.operation !== "context" || !request.options.phase) return;
61
70
  const phase = request.options.phase;
62
71
  if (!new Set(["discover", "expand", "compile", "verify"]).has(phase)) throw new Error(`unsupported context phase ${phase}`);
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const assert = require("assert");
6
+ const fs = require("fs");
7
+ const os = require("os");
8
+ const path = require("path");
9
+ const { spawnSync } = require("child_process");
10
+ const technology = require("../project-technology");
11
+
12
+ function writeJson(file, value) {
13
+ fs.mkdirSync(path.dirname(file), { recursive: true });
14
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
15
+ }
16
+
17
+ function git(repo, ...args) {
18
+ const run = spawnSync("git", args, { cwd: repo, encoding: "utf8" });
19
+ if (run.status !== 0) throw new Error(run.stderr || `git ${args.join(" ")} failed`);
20
+ return run.stdout.trim();
21
+ }
22
+
23
+ function manifest(id) {
24
+ return {
25
+ $schema: "https://mirai-graph.dev/schemas/graph-manifest.schema.json",
26
+ format: "mirai-graph", schema_version: "2.0.0", id, aliases: [], title: id,
27
+ scope: "repository", kind: "repository_graph", owner: "fixture-owner",
28
+ profiles: ["project_management", "implementation_control"],
29
+ graph: {
30
+ root: "graph", source_of_truth: ["graph/specs"], objects: ["graph/specs/objects.json"],
31
+ relations: ["graph/specs/relations.json"], schemas: [], generated: ["graph/generated"], raw_sources: ["README.md"],
32
+ },
33
+ imports: [],
34
+ extensions: { "mirai.project_technology": technology.extensionContract() },
35
+ };
36
+ }
37
+
38
+ function createRepo(root, name, withGit = true) {
39
+ const repo = path.join(root, name); fs.mkdirSync(repo, { recursive: true });
40
+ writeJson(path.join(repo, "graph.json"), manifest(name));
41
+ writeJson(path.join(repo, "graph/specs/objects.json"), [{
42
+ id: "goal.fixture.delivery", kind: "goal", title: "Deliver fixture", summary: "Complete a safe fixture task",
43
+ readiness: "accepted", profile: "project_management", source_refs: ["README.md"],
44
+ }]);
45
+ writeJson(path.join(repo, "graph/specs/relations.json"), []);
46
+ fs.writeFileSync(path.join(repo, "README.md"), "# Fixture\nPortable safe context.\n");
47
+ if (withGit) {
48
+ git(repo, "init", "-q"); git(repo, "config", "user.name", "Mirai Fixture"); git(repo, "config", "user.email", "fixture@example.invalid");
49
+ git(repo, "add", "."); git(repo, "commit", "-qm", "initial graph");
50
+ }
51
+ return repo;
52
+ }
53
+
54
+ function evidence(task, options = {}) {
55
+ return {
56
+ task_digest: technology.sha256(task, true),
57
+ outcome: options.outcome || `Verified result for ${task}`,
58
+ requirement_refs: options.requirement_refs || ["requirement.fixture.portability"],
59
+ evidence_refs: ["repo://fixture/checks/continuity"],
60
+ checks: [{ id: "check.fixture.continuity", verdict: "pass", evidence_ref: "repo://fixture/checks/continuity" }],
61
+ changed_surfaces: ["src/fixture.js"],
62
+ case_signature: options.case_signature || "case.fixture.portable-work",
63
+ method_candidate: options.method_candidate || null,
64
+ decisions: options.decisions || [],
65
+ source_revision: options.source_revision || null,
66
+ };
67
+ }
68
+
69
+ const checks = [];
70
+ function check(id, condition, details = null) {
71
+ checks.push({ id, passed: Boolean(condition), details });
72
+ assert.ok(condition, `${id}: ${JSON.stringify(details)}`);
73
+ }
74
+
75
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "mirai-continuity-"));
76
+ const stateRoot = path.join(root, "host-state");
77
+ try {
78
+ const repo = createRepo(root, "git-project");
79
+ const before = git(repo, "status", "--porcelain=v1");
80
+ const taskStart = technology.execute("sync", repo, { apply: true, boundary: "task_start", stateRoot });
81
+ check("task_start_is_read_only", taskStart.status === "success" && taskStart.changed === false && git(repo, "status", "--porcelain=v1") === before, taskStart);
82
+ const preview = technology.execute("sync", repo, { apply: false, boundary: "task_complete", continuityEvidence: evidence("preview"), stateRoot });
83
+ check("preview_is_zero_write", preview.status === "preview" && git(repo, "status", "--porcelain=v1") === before, preview);
84
+ const missingReceiptVerify = technology.verify(repo, { stateRoot });
85
+ check("configured_continuity_without_receipt_blocks_verify", missingReceiptVerify.status === "blocked" && missingReceiptVerify.blockers.includes("continuity_missing"), missingReceiptVerify);
86
+
87
+ const first = technology.execute("sync", repo, { apply: true, boundary: "stage_complete", continuityEvidence: evidence("first", { method_candidate: "Use the verified bounded fixture path." }), stateRoot });
88
+ check("verified_result_is_saved", first.status === "success" && first.changed && first.continuity.saved_refs.length === 2, first);
89
+ check("portable_authority_is_graph_specs", fs.existsSync(path.join(repo, "graph/specs/project-continuity.json")) && first.continuity.authority === "graph/specs");
90
+ check("host_state_is_outside_project", !fs.existsSync(path.join(repo, ".mirai-graph/project-technology/receipts")) && fs.existsSync(stateRoot));
91
+ check("human_projection_is_generated", fs.readFileSync(path.join(repo, "graph/docs/project-context.md"), "utf8").includes("not a source of truth"));
92
+ const objectsAfterFirst = JSON.parse(fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"), "utf8")).objects;
93
+ check("first_method_is_proposal", objectsAfterFirst.some((item) => item.kind === "lesson" && item.readiness === "proposal"));
94
+ check("architecture_decision_is_not_accepted", technology.execute("sync", repo, {
95
+ apply: true, boundary: "task_complete", stateRoot,
96
+ continuityEvidence: evidence("decision", { decisions: [{ summary: "Replace the project architecture", changes_architecture: true }] }),
97
+ }).status === "success");
98
+ const decisionObjects = JSON.parse(fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"), "utf8")).objects;
99
+ check("architecture_change_remains_proposal", decisionObjects.some((item) => item.kind === "decision" && item.summary === "Replace the project architecture" && item.readiness === "proposal"));
100
+
101
+ const second = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: evidence("second", { method_candidate: "Use the verified bounded fixture path." }), stateRoot });
102
+ check("second_independent_case_is_saved", second.status === "success", second);
103
+ const objectsAfterSecond = JSON.parse(fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"), "utf8")).objects;
104
+ check("method_promotes_after_two_independent_cases", objectsAfterSecond.some((item) => item.kind === "lesson" && item.readiness === "accepted" && item.supporting_case_ids.length >= 2));
105
+
106
+ const beforeRepeat = fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"));
107
+ const repeated = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: evidence("second", { method_candidate: "Use the verified bounded fixture path." }), stateRoot });
108
+ check("repeat_is_byte_identical", repeated.status === "success" && repeated.changed === false && fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json")).equals(beforeRepeat), repeated);
109
+ check("fresh_continuity_verifies_without_target_for_light_work", technology.verify(repo, { stateRoot }).status === "success");
110
+ check("fresh_receipt_allows_significant_work", technology.continuity.verify(repo, JSON.parse(fs.readFileSync(path.join(repo, "graph.json"), "utf8")), { stateRoot }).status === "success");
111
+
112
+ const staleDigest = technology.continuity.graphDigest(repo);
113
+ fs.appendFileSync(path.join(repo, "graph/specs/objects.json"), " ");
114
+ check("stale_continuity_blocks_significant_work", technology.continuity.verify(repo, JSON.parse(fs.readFileSync(path.join(repo, "graph.json"), "utf8")), { stateRoot }).blockers.includes("continuity_stale"));
115
+ const baseObjects = JSON.parse(fs.readFileSync(path.join(repo, "graph/specs/objects.json"), "utf8"));
116
+ baseObjects[0].summary = "Changed concurrently";
117
+ fs.writeFileSync(path.join(repo, "graph/specs/objects.json"), JSON.stringify(baseObjects, null, 2) + "\n");
118
+ const cas = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: evidence("cas"), expectedGraphDigest: staleDigest, stateRoot });
119
+ check("compare_and_swap_conflict_is_safe", cas.status === "blocked" && cas.blockers.includes("continuity_compare_and_swap_conflict"), cas);
120
+ baseObjects[0].summary = "Complete a safe fixture task";
121
+ fs.writeFileSync(path.join(repo, "graph/specs/objects.json"), JSON.stringify(baseObjects, null, 2) + "\n");
122
+
123
+ const secret = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: evidence("secret", { outcome: "token=not-a-real-secret-value-123456" }), stateRoot });
124
+ check("secret_is_rejected", secret.status === "blocked" && secret.blockers.includes("continuity_sensitive_or_host_data_forbidden"), secret);
125
+ const privatePath = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: { ...evidence("private"), changed_surfaces: ["source/memory/private.md"] }, stateRoot });
126
+ check("private_projection_is_rejected", privatePath.status === "blocked" && privatePath.blockers.includes("continuity_sensitive_or_host_data_forbidden"), privatePath);
127
+ const failedCheck = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: { ...evidence("fail"), checks: [{ id: "check.fixture", verdict: "fail", evidence_ref: "repo://fixture/fail" }] }, stateRoot });
128
+ check("unverified_result_is_rejected", failedCheck.status === "blocked" && failedCheck.blockers.includes("continuity_result_not_verified"), failedCheck);
129
+
130
+ git(repo, "add", "graph.json", "graph/specs", "graph/docs"); git(repo, "commit", "-qm", "portable continuity records");
131
+ const discovered = technology.context(repo, { phase: "discover", task: "use the verified portable work case" });
132
+ check("new_task_discovers_past_case", discovered.status === "success" && discovered.traversal_receipt.candidates.some((item) => item.id.startsWith("regression_case.continuity.")), discovered.blockers);
133
+ const beforeReadOnly = git(repo, "status", "--porcelain=v1");
134
+ technology.status(repo); technology.context(repo, { phase: "discover", task: "read only project context" });
135
+ check("read_only_operations_write_nothing", git(repo, "status", "--porcelain=v1") === beforeReadOnly);
136
+
137
+ const folder = createRepo(root, "shared-folder", false);
138
+ const folderFirst = technology.execute("sync", folder, { apply: true, boundary: "task_complete", continuityEvidence: evidence("folder"), stateRoot });
139
+ check("non_git_folder_supports_continuity", folderFirst.status === "success" && folderFirst.changed, folderFirst);
140
+ const folderRepeat = technology.execute("sync", folder, { apply: true, boundary: "task_complete", continuityEvidence: evidence("folder"), stateRoot });
141
+ check("non_git_repeat_is_idempotent", folderRepeat.status === "success" && folderRepeat.changed === false, folderRepeat);
142
+ const sharedLease = path.join(folder, "graph/.project-technology-continuity.lock");
143
+ fs.writeFileSync(sharedLease, "occupied\n");
144
+ const leaseConflict = technology.execute("sync", folder, { apply: true, boundary: "task_complete", continuityEvidence: evidence("parallel"), stateRoot: path.join(root, "third-installation-state") });
145
+ check("shared_folder_lease_blocks_parallel_writer", leaseConflict.status === "blocked" && leaseConflict.blockers.includes("continuity_lease_conflict"), leaseConflict);
146
+ fs.unlinkSync(sharedLease);
147
+ const folderDiscovery = technology.context(folder, { phase: "discover", task: "portable fixture case" });
148
+ check("non_git_context_is_available", folderDiscovery.status === "success", folderDiscovery.blockers);
149
+ check("same_content_has_stable_digest", technology.continuity.graphDigest(folder) === technology.continuity.graphDigest(folder));
150
+
151
+ const otherStateRoot = path.join(root, "other-installation-state");
152
+ const firstInstallContext = technology.context(repo, { phase: "discover", task: "use verified portable work" });
153
+ const secondInstallContext = technology.context(repo, { phase: "discover", task: "use verified portable work", stateRoot: otherStateRoot });
154
+ check("two_installations_get_same_context", firstInstallContext.traversal_receipt.traversal_digest === secondInstallContext.traversal_receipt.traversal_digest);
155
+ check("portable_specs_contain_no_host_paths", !/(?:\/Users\/|\/home\/|[A-Za-z]:\\Users\\)/.test(fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"), "utf8")));
156
+ check("continuity_policy_keeps_contract_version", JSON.parse(fs.readFileSync(path.join(repo, "graph.json"), "utf8")).extensions["mirai.project_technology"].contract_version === "1.0.0");
157
+
158
+ const bootstrapRepo = path.join(root, "existing-code-new-graph");
159
+ fs.mkdirSync(bootstrapRepo, { recursive: true });
160
+ fs.writeFileSync(path.join(bootstrapRepo, "README.md"), "# Existing code\n");
161
+ git(bootstrapRepo, "init", "-q"); git(bootstrapRepo, "config", "user.name", "Mirai Fixture"); git(bootstrapRepo, "config", "user.email", "fixture@example.invalid");
162
+ git(bootstrapRepo, "add", "README.md"); git(bootstrapRepo, "commit", "-qm", "existing code");
163
+ writeJson(path.join(bootstrapRepo, "graph.json"), manifest("bootstrap-project"));
164
+ writeJson(path.join(bootstrapRepo, "graph/specs/objects.json"), [{ id: "goal.bootstrap", kind: "goal", title: "Bootstrap", summary: "Use content-bound bootstrap context", readiness: "accepted", profile: "project_management" }]);
165
+ writeJson(path.join(bootstrapRepo, "graph/specs/relations.json"), []);
166
+ const bootstrapContext = technology.context(bootstrapRepo, { phase: "discover", task: "bootstrap context" });
167
+ check("new_git_graph_is_content_bound_before_first_commit", bootstrapContext.status === "success" && String(bootstrapContext.traversal_receipt.graph.revision).startsWith("content:"), bootstrapContext.blockers);
168
+
169
+ process.stdout.write(`${JSON.stringify({ status: "success", checks_passed: checks.length, checks_failed: 0, checks }, null, 2)}\n`);
170
+ } catch (error) {
171
+ process.stdout.write(`${JSON.stringify({ status: "fail", checks_passed: checks.filter((item) => item.passed).length, checks_failed: 1, checks, error: error.message }, null, 2)}\n`);
172
+ process.exitCode = 1;
173
+ }
@@ -72,6 +72,18 @@ function executionContract() {
72
72
  };
73
73
  }
74
74
 
75
+ function continuityEvidence(repositoryId) {
76
+ return {
77
+ task_digest: technology.sha256(`task:${repositoryId}`, true),
78
+ outcome: `Exact target binding verified for ${repositoryId}`,
79
+ requirement_refs: ["requirement.fixture.runtime"],
80
+ evidence_refs: ["repo://fixture/evidence/project-technology"],
81
+ checks: [{ id: "check.fixture.binding", verdict: "pass", evidence_ref: "repo://fixture/evidence/project-technology" }],
82
+ changed_surfaces: ["graph/specs/project-continuity.json"],
83
+ case_signature: "case.fixture.target-binding",
84
+ };
85
+ }
86
+
75
87
  function initRepo(root, name, scope = "repository", profiles) {
76
88
  const repo = path.join(root, name); fs.mkdirSync(repo, { recursive: true });
77
89
  git(repo, "init", "-q"); git(repo, "config", "user.name", "Mirai Fixture");
@@ -104,6 +116,7 @@ function check(id, condition, details = null) {
104
116
  }
105
117
 
106
118
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "mirai-project-technology-test-"));
119
+ process.env.MIRAI_GRAPH_STATE_ROOT = path.join(root, "host-state");
107
120
  try {
108
121
  const provider = initRepo(root, "fixture-provider", "repository");
109
122
  const consumer = initRepo(root, "fixture-consumer", "repository");
@@ -112,6 +125,11 @@ try {
112
125
  const federation = initRepo(root, "fixture-federation", "federation", ["project_management", "implementation_control"]);
113
126
  const providerRevision = addTarget(provider);
114
127
 
128
+ const legacyRuntimeRepo = initRepo(root, "legacy-runtime-repository");
129
+ writeJson(path.join(legacyRuntimeRepo, ".mirai-graph/project-technology/inventory.json"), { schema_version: "1.0.0", repository_id: "legacy-runtime-repository", inventory_digest: `sha256:${"0".repeat(64)}`, files: [] });
130
+ const legacyRuntimeEnabled = technology.execute("enable", legacyRuntimeRepo, { apply: true });
131
+ check("project_local_runtime_moves_to_host_state", legacyRuntimeEnabled.status === "success" && legacyRuntimeEnabled.migration_ref.startsWith("host-local://") && fs.existsSync(path.join(legacyRuntimeRepo, ".mirai-graph/project-technology/inventory.json")), legacyRuntimeEnabled);
132
+
115
133
  const before = fs.readFileSync(path.join(consumer, "graph.json"));
116
134
  const preview = technology.execute("enable", consumer, { apply: false });
117
135
  check("preview_is_zero_write", preview.status === "preview" && fs.readFileSync(path.join(consumer, "graph.json")).equals(before));
@@ -147,12 +165,18 @@ try {
147
165
  apply: true, source: exportPath, targetId: TARGET_ID,
148
166
  semanticDigest: SEMANTIC_DIGEST, providerRevision,
149
167
  });
150
- check("consumer_binding_ready", connected.status === "success" && technology.verify(consumer, { significantWork: true }).status === "success", connected.blockers);
168
+ const consumerContinuity = technology.execute("sync", consumer, {
169
+ apply: true, boundary: "task_complete", continuityEvidence: continuityEvidence("fixture-consumer"),
170
+ });
171
+ check("consumer_binding_ready", connected.status === "success" && consumerContinuity.status === "success" && technology.verify(consumer, { significantWork: true }).status === "success", { connect: connected.blockers, continuity: consumerContinuity.blockers });
151
172
  const connectedTwo = technology.execute("connect", consumerTwo, {
152
173
  apply: true, source: exportPath, targetId: TARGET_ID,
153
174
  semanticDigest: SEMANTIC_DIGEST, providerRevision,
154
175
  });
155
- check("second_consumer_binding_ready", connectedTwo.status === "success" && technology.verify(consumerTwo, { significantWork: true }).status === "success", connectedTwo.blockers);
176
+ const consumerTwoContinuity = technology.execute("sync", consumerTwo, {
177
+ apply: true, boundary: "task_complete", continuityEvidence: continuityEvidence("fixture-consumer-two"),
178
+ });
179
+ check("second_consumer_binding_ready", connectedTwo.status === "success" && consumerTwoContinuity.status === "success" && technology.verify(consumerTwo, { significantWork: true }).status === "success", { connect: connectedTwo.blockers, continuity: consumerTwoContinuity.blockers });
156
180
  const repeated = technology.execute("connect", consumer, {
157
181
  apply: true, source: exportPath, targetId: TARGET_ID,
158
182
  semanticDigest: SEMANTIC_DIGEST, providerRevision,
@@ -63,18 +63,27 @@ function trackedState(repo) {
63
63
  const trackedRun = spawnSync("git", ["ls-files", "-z"], { cwd: repo, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
64
64
  const dirtyRun = spawnSync("git", ["diff", "--name-only", "-z", "HEAD", "--"], { cwd: repo, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
65
65
  return {
66
+ hasGit: Boolean(git(repo, "rev-parse", "--is-inside-work-tree")),
66
67
  tracked: new Set(trackedRun.status === 0 ? trackedRun.stdout.split("\0").filter(Boolean) : []),
67
68
  dirty: new Set(dirtyRun.status === 0 ? dirtyRun.stdout.split("\0").filter(Boolean) : []),
68
69
  };
69
70
  }
70
71
 
71
72
  function graphBlob(graph, relative) {
73
+ if (!graph.hasGit) {
74
+ const absolute = path.join(graph.repo, relative);
75
+ return fs.existsSync(absolute) && !fs.lstatSync(absolute).isSymbolicLink() && fs.statSync(absolute).isFile() ? fs.readFileSync(absolute) : null;
76
+ }
72
77
  if (!graph.tracked?.has(relative) || graph.dirty?.has(relative)) return null;
73
78
  if (!graph.blobCache.has(relative)) graph.blobCache.set(relative, gitBlob(graph.repo, relative));
74
79
  return graph.blobCache.get(relative);
75
80
  }
76
81
 
77
82
  function revisionBound(repo, relative) {
83
+ if (!git(repo, "rev-parse", "--is-inside-work-tree")) {
84
+ const absolute = path.join(repo, relative);
85
+ return fs.existsSync(absolute) && !fs.lstatSync(absolute).isSymbolicLink() && fs.statSync(absolute).isFile();
86
+ }
78
87
  if (!gitBlob(repo, relative)) return false;
79
88
  return spawnSync("git", ["diff", "--quiet", "HEAD", "--", relative], { cwd: repo }).status === 0;
80
89
  }
@@ -197,19 +206,25 @@ function readGraph(repoArg) {
197
206
  if (!objects.has(relation.source) || !objects.has(relation.target)) { output.blockers.push("relation_endpoint_missing"); continue; }
198
207
  relations.push({ ...relation, _record: record });
199
208
  }
200
- const revision = git(repo, "rev-parse", "HEAD") || null;
201
209
  const state = trackedState(repo);
202
- for (const relative of unique([
210
+ const graphSources = unique([
203
211
  "graph.json",
204
212
  ...[...objects.values()].map((record) => record.relative),
205
213
  ...relations.map((relation) => relation._record.relative),
206
- ])) if (!state.tracked.has(relative) || state.dirty.has(relative)) output.blockers.push("graph_source_not_revision_bound");
207
- const graphPayload = {
214
+ ]);
215
+ const newContentBoundGraph = state.hasGit
216
+ && graphSources.length > 0
217
+ && graphSources.every((relative) => !state.tracked.has(relative))
218
+ && graphSources.every((relative) => !state.dirty.has(relative));
219
+ for (const relative of graphSources) if (state.hasGit && !newContentBoundGraph && (!state.tracked.has(relative) || state.dirty.has(relative))) output.blockers.push("graph_source_not_revision_bound");
220
+ const gitRevision = !newContentBoundGraph ? git(repo, "rev-parse", "HEAD") || null : null;
221
+ const graphPayloadBase = {
208
222
  manifest: { id: manifest.id, scope: manifest.scope, profiles: manifest.profiles, graph: manifest.graph },
209
223
  objects: [...objects.values()].map((record) => record.value).sort((a, b) => a.id.localeCompare(b.id)),
210
224
  relations: relations.map(({ _record, ...relation }) => relation).sort((a, b) => a.id.localeCompare(b.id)),
211
- revision,
212
225
  };
226
+ const revision = gitRevision || `content:${digest(graphPayloadBase).slice(7)}`;
227
+ const graphPayload = { ...graphPayloadBase, revision };
213
228
  return {
214
229
  repo,
215
230
  manifest,
@@ -219,6 +234,7 @@ function readGraph(repoArg) {
219
234
  relations,
220
235
  tracked: state.tracked,
221
236
  dirty: state.dirty,
237
+ hasGit: state.hasGit,
222
238
  blobCache: new Map(),
223
239
  blockers: unique(output.blockers),
224
240
  };
@@ -638,7 +654,7 @@ function verifyContext(repository, contextPack, usageEvidence, options = {}) {
638
654
  const absolute = path.join(graph.repo, source.ref);
639
655
  if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink() || !fs.statSync(absolute).isFile()) blockers.push("context_pack_source_unavailable");
640
656
  else {
641
- const blob = gitBlob(graph.repo, source.ref);
657
+ const blob = graphBlob(graph, source.ref);
642
658
  if (!blob || !revisionBound(graph.repo, source.ref) || digest(blob) !== source.sha256 || source.revision !== graph.revision) blockers.push("context_pack_source_revision_or_digest_mismatch");
643
659
  }
644
660
  }