mirai-graph 1.1.0 → 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.
@@ -14,15 +14,16 @@ const {
14
14
  validateManifest,
15
15
  } = require("../cli/graph-manifest");
16
16
  const traversal = require("./context-traversal");
17
+ const continuity = require("./continuity");
17
18
 
18
19
  const CONTRACT_VERSION = "1.0.0";
19
20
  const EXTENSION_KEY = "mirai.project_technology";
20
21
  const LEGACY_EXTENSION_KEY = "simai.project_technology";
21
22
  const LOCAL_DIR = path.join(".mirai-graph", "project-technology");
22
23
  const LEGACY_LOCAL_DIR = path.join(".simai", "project-technology");
23
- const INVENTORY_FILE = path.join(LOCAL_DIR, "inventory.json");
24
- const BINDING_FILE = path.join(LOCAL_DIR, "target-provider-binding.json");
25
- const IMPORT_DIR = path.join(LOCAL_DIR, "provider-exports");
24
+ const INVENTORY_FILE = "inventory.json";
25
+ const BINDING_FILE = "target-provider-binding.json";
26
+ const IMPORT_DIR = "provider-exports";
26
27
  const EXPORT_FILE = path.join("graph", "generated", "project-technology", "target-provider-export.json");
27
28
  const TARGET_DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
28
29
  const REVISION_RE = /^[0-9a-f]{40}$/;
@@ -90,6 +91,58 @@ function normalizeRepo(repo) {
90
91
  return path.resolve(repo || ".");
91
92
  }
92
93
 
94
+ function runtimeRoot(repo, manifest = null, options = {}) {
95
+ return continuity.stateRoot(repo, manifest || readManifest(repo).manifest, options);
96
+ }
97
+
98
+ function legacyRuntimeState(repo) {
99
+ const root = path.join(repo, LOCAL_DIR);
100
+ return {
101
+ root,
102
+ present: fs.existsSync(root),
103
+ inventory: path.join(root, INVENTORY_FILE),
104
+ binding: path.join(root, BINDING_FILE),
105
+ };
106
+ }
107
+
108
+ function migrateProjectLocalRuntime(repo, manifest, root) {
109
+ const legacy = legacyRuntimeState(repo);
110
+ if (!legacy.present) return { changed: false, blockers: [], migration_ref: null };
111
+ const blockers = [];
112
+ let changed = false;
113
+ if (fs.existsSync(legacy.binding)) {
114
+ const binding = readJson(legacy.binding);
115
+ const identity = bindingValues(binding); blockers.push(...identity.blockers);
116
+ const ref = String(binding.provider_export_ref || "");
117
+ const exportPath = ref && !path.isAbsolute(ref) ? path.resolve(repo, ref) : null;
118
+ const exported = exportPath ? readExport(exportPath) : { export: {}, blockers: ["legacy_provider_export_missing"] };
119
+ blockers.push(...exported.blockers);
120
+ for (const key of Object.keys(identity.values)) if (exported.export[key] !== identity.values[key]) blockers.push(`legacy_${key}_mismatch`);
121
+ if (!blockers.length) {
122
+ const exportBytes = canonicalBytes(exported.export);
123
+ const exportSha = sha256(exportBytes);
124
+ changed = atomicWrite(path.join(root, IMPORT_DIR, `${exportSha}.json`), exportBytes) || changed;
125
+ changed = atomicWrite(path.join(root, BINDING_FILE), canonicalBytes({
126
+ schema_version: "1.0.0", ...identity.values,
127
+ provider_export_ref: `host-local://${IMPORT_DIR}/${exportSha}.json`,
128
+ provider_export_sha256: exportSha,
129
+ })) || changed;
130
+ }
131
+ }
132
+ if (blockers.length) return { changed: false, blockers: [...new Set(blockers)].sort(), migration_ref: null };
133
+ const receipt = {
134
+ schema_version: "1.0.0",
135
+ status: "migrated",
136
+ repository_id: manifest.id,
137
+ source: LOCAL_DIR,
138
+ source_preserved: true,
139
+ active_runtime: "host-local",
140
+ };
141
+ const receiptName = `project-local-runtime-${sha256(canonicalBytes(receipt)).slice(0, 16)}.json`;
142
+ changed = atomicWrite(path.join(root, "migrations", receiptName), canonicalBytes(receipt)) || changed;
143
+ return { changed, blockers: [], migration_ref: `host-local://migrations/${receiptName}` };
144
+ }
145
+
93
146
  function readManifest(repo) {
94
147
  const manifestPath = path.join(repo, "graph.json");
95
148
  if (!fs.existsSync(manifestPath)) return { manifest: null, blockers: ["graph_manifest_missing"] };
@@ -106,6 +159,7 @@ function extensionContract() {
106
159
  enabled: true,
107
160
  context_policy: "task_scoped",
108
161
  source_boundary: "hybrid_sot",
162
+ continuity_policy: "task_boundary",
109
163
  };
110
164
  }
111
165
 
@@ -432,19 +486,23 @@ function context(repoArg, options = {}) {
432
486
  source_refs: (nodes.get(candidate.id)?.source_refs || []).map((source) => source.ref),
433
487
  })),
434
488
  runtime_contract: { graph_first_scope: "routing/capability/policy orientation", raw_source_authoritative: true, canonical_write_allowed: false },
489
+ continuity: continuity.status(normalizeRepo(repoArg), readManifest(normalizeRepo(repoArg)).manifest, options),
435
490
  };
436
491
  }
437
492
 
438
- function targetBindingStatus(repo) {
439
- const bindingPath = path.join(repo, BINDING_FILE);
493
+ function targetBindingStatus(repo, options = {}) {
494
+ const manifest = readManifest(repo).manifest;
495
+ const root = runtimeRoot(repo, manifest, options);
496
+ const bindingPath = path.join(root, BINDING_FILE);
440
497
  if (!fs.existsSync(bindingPath)) return { status: "not_configured", enabled: false, blockers: [], next_action: "connect an exact target provider binding" };
441
498
  let binding;
442
499
  try { binding = readJson(bindingPath); } catch (_) { return { status: "blocked", enabled: true, blockers: ["target_binding_invalid"] }; }
443
500
  const identity = bindingValues(binding);
444
501
  const exportRef = String(binding.provider_export_ref || "");
445
- const exportPath = exportRef && !path.isAbsolute(exportRef) ? path.resolve(repo, exportRef) : null;
502
+ const hostPrefix = "host-local://";
503
+ const exportPath = exportRef.startsWith(hostPrefix) ? path.join(root, exportRef.slice(hostPrefix.length)) : null;
446
504
  const blockers = [...identity.blockers];
447
- if (!exportPath || !exportPath.startsWith(`${repo}${path.sep}`)) blockers.push("provider_export_outside_repository");
505
+ if (!exportPath || !exportPath.startsWith(`${root}${path.sep}`)) blockers.push("provider_export_outside_host_state");
448
506
  const exported = exportPath ? readExport(exportPath) : { export: {}, blockers: ["provider_export_missing"] };
449
507
  blockers.push(...exported.blockers);
450
508
  for (const key of Object.keys(identity.values)) if (exported.export[key] !== identity.values[key]) blockers.push(`${key}_mismatch`);
@@ -466,18 +524,20 @@ function targetBindingStatus(repo) {
466
524
  };
467
525
  }
468
526
 
469
- function status(repoArg) {
527
+ function status(repoArg, options = {}) {
470
528
  const repo = normalizeRepo(repoArg);
471
529
  const manifestState = readManifest(repo);
472
530
  const ext = extensionState(manifestState.manifest);
473
531
  let stored = null;
474
- try { if (fs.existsSync(path.join(repo, INVENTORY_FILE))) stored = readJson(path.join(repo, INVENTORY_FILE)); } catch (_) { /* stale */ }
532
+ const root = runtimeRoot(repo, manifestState.manifest, options);
533
+ try { if (fs.existsSync(path.join(root, INVENTORY_FILE))) stored = readJson(path.join(root, INVENTORY_FILE)); } catch (_) { /* stale */ }
475
534
  const current = inventory(repo);
476
535
  const freshness = stored && stored.inventory_digest === current.inventory_digest ? "current" : stored ? "stale" : "missing";
477
536
  const blockers = [...manifestState.blockers, ...ext.blockers];
537
+ if (!stored && legacyRuntimeState(repo).present) blockers.push("project_technology_host_state_migration_required");
478
538
  if (ext.contract && ext.contract.enabled === false) blockers.push("project_technology_disabled");
479
539
  if (freshness !== "current") blockers.push(`project_technology_inventory_${freshness}`);
480
- const binding = targetBindingStatus(repo);
540
+ const binding = targetBindingStatus(repo, options);
481
541
  if (binding.status === "blocked") blockers.push(...binding.blockers);
482
542
  return result("status", "read_only", blockers.length ? "blocked" : "success", {
483
543
  repository_id: manifestState.manifest?.id || path.basename(repo),
@@ -486,6 +546,7 @@ function status(repoArg) {
486
546
  technology_freshness: freshness,
487
547
  technology_digest: current.inventory_digest,
488
548
  target_binding: binding,
549
+ continuity: continuity.status(repo, manifestState.manifest, {}),
489
550
  blockers: [...new Set(blockers)].sort(),
490
551
  next_action: ext.legacy ? "run enable --apply to migrate the legacy contract" : blockers.length ? "run plan, then the required transactional operation" : "none",
491
552
  });
@@ -502,18 +563,18 @@ function plan(repoArg) {
502
563
  const state = status(repoArg);
503
564
  return result("plan", "read_only", "success", {
504
565
  current_status: state.status,
505
- planned_changes: ["validate_graph_manifest", "migrate_legacy_extension_if_present", "enable_public_contract", "build_safe_inventory", "verify_target_binding"],
566
+ planned_changes: ["validate_graph_manifest", "migrate_legacy_extension_if_present", "migrate_project_local_runtime_to_host_state", "enable_public_contract", "build_safe_inventory", "verify_target_binding"],
506
567
  blockers: state.blockers.filter((item) => !["project_technology_not_configured", "project_technology_migration_required", "project_technology_inventory_missing", "project_technology_inventory_stale"].includes(item)),
507
568
  next_action: "rerun the selected operation with --apply",
508
569
  });
509
570
  }
510
571
 
511
- function preview(operation, repoArg) {
512
- const state = status(repoArg);
572
+ function preview(operation, repoArg, options = {}) {
573
+ const state = status(repoArg, options);
513
574
  return result(operation, "preview", "preview", { apply_required: true, current_status: state.status, blockers: state.blockers.filter((item) => item.startsWith("graph_manifest_")), next_action: `${operation} --apply` });
514
575
  }
515
576
 
516
- function enable(repoArg) {
577
+ function enable(repoArg, options = {}) {
517
578
  const repo = normalizeRepo(repoArg);
518
579
  const manifestState = readManifest(repo);
519
580
  if (!manifestState.manifest || manifestState.blockers.length) return result("enable", "transactional", "fail", { blockers: manifestState.blockers, next_action: "repair graph.json" });
@@ -523,7 +584,13 @@ function enable(repoArg) {
523
584
  delete extensions[LEGACY_EXTENSION_KEY];
524
585
  extensions[EXTENSION_KEY] = extensionContract();
525
586
  manifest.extensions = extensions;
526
- const migrationRoot = path.join(repo, LOCAL_DIR, "migrations", sha256(canonicalBytes({ legacy: legacy || null, head: git(repo, "rev-parse", "HEAD") })).slice(0, 16));
587
+ const root = runtimeRoot(repo, manifest, options);
588
+ const runtimeMigration = migrateProjectLocalRuntime(repo, manifest, root);
589
+ if (runtimeMigration.blockers.length) return result("enable", "transactional", "fail", {
590
+ blockers: runtimeMigration.blockers,
591
+ next_action: "repair the project-local runtime before host-state migration",
592
+ });
593
+ const migrationRoot = path.join(root, "migrations", sha256(canonicalBytes({ legacy: legacy || null, head: git(repo, "rev-parse", "HEAD") })).slice(0, 16));
527
594
  fs.mkdirSync(migrationRoot, { recursive: true });
528
595
  atomicWrite(path.join(migrationRoot, "graph.json.before"), fs.readFileSync(path.join(repo, "graph.json")));
529
596
  if (legacy) atomicWrite(path.join(migrationRoot, "legacy-extension.json"), canonicalBytes(legacy));
@@ -531,25 +598,52 @@ function enable(repoArg) {
531
598
  if (fs.existsSync(legacyLocal)) atomicWrite(path.join(migrationRoot, "legacy-local-state.json"), canonicalBytes({ status: "preserved_for_rollback", path: LEGACY_LOCAL_DIR }));
532
599
  const manifestChanged = atomicWrite(path.join(repo, "graph.json"), canonicalBytes(manifest));
533
600
  const inv = inventory(repo);
534
- const inventoryChanged = atomicWrite(path.join(repo, INVENTORY_FILE), canonicalBytes(inv));
601
+ const inventoryChanged = atomicWrite(path.join(root, INVENTORY_FILE), canonicalBytes(inv));
535
602
  const verified = readManifest(repo);
536
603
  if (verified.blockers.length || extensionState(verified.manifest).blockers.length) {
537
604
  fs.copyFileSync(path.join(migrationRoot, "graph.json.before"), path.join(repo, "graph.json"));
538
605
  return result("enable", "transactional", "fail", { blockers: ["project_technology_enable_rolled_back", ...verified.blockers], next_action: "inspect the rollback receipt" });
539
606
  }
540
- return result("enable", "transactional", "success", { changed: manifestChanged || inventoryChanged, migration_ref: path.relative(repo, migrationRoot).split(path.sep).join("/"), technology_digest: inv.inventory_digest });
607
+ return result("enable", "transactional", "success", {
608
+ changed: manifestChanged || inventoryChanged || runtimeMigration.changed,
609
+ migration_ref: runtimeMigration.migration_ref || `host-local://migrations/${path.basename(migrationRoot)}`,
610
+ technology_digest: inv.inventory_digest,
611
+ });
541
612
  }
542
613
 
543
- function sync(repoArg) {
614
+ function sync(repoArg, options = {}) {
544
615
  const repo = normalizeRepo(repoArg);
545
616
  const manifestState = readManifest(repo);
546
617
  const ext = extensionState(manifestState.manifest);
547
618
  const blockers = [...manifestState.blockers, ...ext.blockers];
619
+ const hostRoot = runtimeRoot(repo, manifestState.manifest, options);
620
+ if (legacyRuntimeState(repo).present && !fs.existsSync(path.join(hostRoot, INVENTORY_FILE))) blockers.push("project_technology_host_state_migration_required");
548
621
  if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
549
622
  if (blockers.length) return result("sync", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "enable or repair Project Technology" });
623
+ let continuityResult = null;
624
+ if (options.boundary) {
625
+ continuityResult = continuity.sync(repo, manifestState.manifest, options.boundary, options.continuityEvidence, options);
626
+ if (continuityResult.status !== "success") return result("sync", "transactional", continuityResult.status, {
627
+ blockers: continuityResult.blockers,
628
+ continuity: continuityResult.continuity || null,
629
+ next_action: "repair the continuity evidence or reconcile the graph state",
630
+ });
631
+ if (options.boundary === "task_start") return result("sync", "read_only", "success", {
632
+ changed: false,
633
+ technology_digest: inventory(repo).inventory_digest,
634
+ target_binding: targetBindingStatus(repo, options),
635
+ continuity: continuityResult.continuity,
636
+ });
637
+ }
550
638
  const inv = inventory(repo);
551
- const changed = atomicWrite(path.join(repo, INVENTORY_FILE), canonicalBytes(inv));
552
- return result("sync", "transactional", "success", { changed, technology_digest: inv.inventory_digest, target_binding: targetBindingStatus(repo) });
639
+ const currentManifest = readManifest(repo).manifest;
640
+ const changed = atomicWrite(path.join(runtimeRoot(repo, currentManifest || manifestState.manifest, options), INVENTORY_FILE), canonicalBytes(inv));
641
+ return result("sync", "transactional", "success", {
642
+ changed: changed || Boolean(continuityResult?.changed),
643
+ technology_digest: inv.inventory_digest,
644
+ target_binding: targetBindingStatus(repo, options),
645
+ continuity: continuityResult?.continuity || continuity.status(repo, currentManifest, options),
646
+ });
553
647
  }
554
648
 
555
649
  function provide(repoArg, options = {}) {
@@ -588,7 +682,8 @@ function connect(repoArg, options = {}) {
588
682
  const providerRoot = providerRootFor(source);
589
683
  if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
590
684
  else if (git(providerRoot, "rev-parse", "HEAD").toLowerCase() !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
591
- const currentPath = path.join(repo, BINDING_FILE);
685
+ const root = runtimeRoot(repo, manifestState.manifest, options);
686
+ const currentPath = path.join(root, BINDING_FILE);
592
687
  let current = null;
593
688
  try { if (fs.existsSync(currentPath)) current = readJson(currentPath); } catch (_) { blockers.push("current_target_binding_invalid"); }
594
689
  if (current) {
@@ -596,14 +691,14 @@ function connect(repoArg, options = {}) {
596
691
  const same = Object.keys(identity.values).every((key) => current[key] === identity.values[key]);
597
692
  if (!same) blockers.push("target_provider_conflict", "target_provider_refresh_required");
598
693
  else {
599
- const currentState = targetBindingStatus(repo);
694
+ const currentState = targetBindingStatus(repo, options);
600
695
  if (currentState.status !== "ready") blockers.push(...currentState.blockers);
601
696
  if (currentState.execution_contract_digest !== read.export.execution_contract_digest) blockers.push("target_provider_conflict", "provider_execution_contract_mismatch");
602
697
  }
603
698
  } else {
604
699
  if (current.target_id !== identity.values.target_id) blockers.push("target_provider_target_mismatch");
605
700
  if (current.semantic_digest !== identity.values.semantic_digest) blockers.push("target_provider_semantic_mismatch");
606
- const currentState = targetBindingStatus(repo);
701
+ const currentState = targetBindingStatus(repo, options);
607
702
  if (currentState.status !== "ready") blockers.push(...currentState.blockers);
608
703
  if (currentState.execution_contract_digest !== read.export.execution_contract_digest) blockers.push("provider_execution_contract_refresh_mismatch");
609
704
  if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
@@ -618,8 +713,8 @@ function connect(repoArg, options = {}) {
618
713
  if (blockers.length) return result("connect", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "repair the provider export or request an explicit accepted target change" });
619
714
  const exportBytes = canonicalBytes(read.export);
620
715
  const exportSha = sha256(exportBytes);
621
- const imported = path.join(repo, IMPORT_DIR, `${exportSha}.json`);
622
- const binding = { schema_version: "1.0.0", ...identity.values, provider_export_ref: path.relative(repo, imported).split(path.sep).join("/"), provider_export_sha256: exportSha };
716
+ const imported = path.join(root, IMPORT_DIR, `${exportSha}.json`);
717
+ const binding = { schema_version: "1.0.0", ...identity.values, provider_export_ref: `host-local://${IMPORT_DIR}/${exportSha}.json`, provider_export_sha256: exportSha };
623
718
  const transaction = fs.mkdtempSync(path.join(os.tmpdir(), "mirai-project-technology-"));
624
719
  const bindingBefore = fs.existsSync(currentPath) ? fs.readFileSync(currentPath) : null;
625
720
  const importBefore = fs.existsSync(imported) ? fs.readFileSync(imported) : null;
@@ -627,7 +722,7 @@ function connect(repoArg, options = {}) {
627
722
  const exportChanged = atomicWrite(imported, exportBytes);
628
723
  const bindingChanged = atomicWrite(currentPath, canonicalBytes(binding));
629
724
  fs.rmSync(transaction, { recursive: true, force: true });
630
- return result("connect", "transactional", "success", { changed: exportChanged || bindingChanged, refresh_binding: Boolean(options.refreshBinding), target_binding: targetBindingStatus(repo) });
725
+ return result("connect", "transactional", "success", { changed: exportChanged || bindingChanged, refresh_binding: Boolean(options.refreshBinding), target_binding: targetBindingStatus(repo, options) });
631
726
  } catch (_) {
632
727
  if (bindingBefore) atomicWrite(currentPath, bindingBefore); else if (fs.existsSync(currentPath)) fs.unlinkSync(currentPath);
633
728
  if (importBefore) atomicWrite(imported, importBefore); else if (fs.existsSync(imported)) fs.unlinkSync(imported);
@@ -636,14 +731,15 @@ function connect(repoArg, options = {}) {
636
731
  }
637
732
  }
638
733
 
639
- function disconnect(repoArg) {
734
+ function disconnect(repoArg, options = {}) {
640
735
  const repo = normalizeRepo(repoArg);
641
- const bindingPath = path.join(repo, BINDING_FILE);
736
+ const root = runtimeRoot(repo, readManifest(repo).manifest, options);
737
+ const bindingPath = path.join(root, BINDING_FILE);
642
738
  if (!fs.existsSync(bindingPath)) return result("disconnect", "transactional", "success");
643
- const backup = path.join(repo, LOCAL_DIR, "rollback", `binding-${sha256(fs.readFileSync(bindingPath)).slice(0, 16)}.json`);
739
+ const backup = path.join(root, "rollback", `binding-${sha256(fs.readFileSync(bindingPath)).slice(0, 16)}.json`);
644
740
  atomicWrite(backup, fs.readFileSync(bindingPath));
645
741
  fs.unlinkSync(bindingPath);
646
- return result("disconnect", "transactional", "success", { changed: true, rollback_ref: path.relative(repo, backup).split(path.sep).join("/") });
742
+ return result("disconnect", "transactional", "success", { changed: true, rollback_ref: `host-local://rollback/${path.basename(backup)}` });
647
743
  }
648
744
 
649
745
  function disable(repoArg) {
@@ -658,19 +754,25 @@ function disable(repoArg) {
658
754
  return result("disable", "transactional", "success", { changed });
659
755
  }
660
756
 
661
- function repair(repoArg) {
757
+ function repair(repoArg, options = {}) {
662
758
  const repo = normalizeRepo(repoArg);
663
759
  const manifestState = readManifest(repo);
664
760
  if (!manifestState.manifest || manifestState.blockers.length) return result("repair", "transactional", "fail", { blockers: manifestState.blockers, next_action: "repair graph.json before Project Technology" });
665
761
  const ext = extensionState(manifestState.manifest);
666
- if (!ext.contract || ext.legacy) return enable(repo);
667
- if (ext.blockers.length) return enable(repo);
668
- return sync(repo);
762
+ if (!ext.contract || ext.legacy) return enable(repo, options);
763
+ if (ext.blockers.length) return enable(repo, options);
764
+ const root = runtimeRoot(repo, manifestState.manifest, options);
765
+ if (legacyRuntimeState(repo).present && !fs.existsSync(path.join(root, INVENTORY_FILE))) return enable(repo, options);
766
+ return sync(repo, options);
669
767
  }
670
768
 
671
769
  function verify(repoArg, options = {}) {
672
- const state = status(repoArg);
770
+ const state = status(repoArg, options);
673
771
  const blockers = [...state.blockers];
772
+ const repo = normalizeRepo(repoArg);
773
+ const manifest = readManifest(repo).manifest;
774
+ const continuityResult = continuity.verify(repo, manifest, options);
775
+ if (continuity.continuityPolicy(manifest) === "task_boundary" || options.receiptDigest || options.significantWork) blockers.push(...continuityResult.blockers);
674
776
  if (options.significantWork && state.target_binding.status !== "ready") blockers.push("accepted_target_binding_required_for_significant_work");
675
777
  return result("verify", "read_only", blockers.length ? "blocked" : "success", {
676
778
  repository_id: state.repository_id,
@@ -679,6 +781,7 @@ function verify(repoArg, options = {}) {
679
781
  technology_freshness: state.technology_freshness,
680
782
  technology_digest: state.technology_digest,
681
783
  target_binding: state.target_binding,
784
+ continuity: continuityResult.continuity,
682
785
  blockers: [...new Set(blockers)].sort(),
683
786
  next_action: blockers.length ? "repair the listed Project Technology blockers" : "none",
684
787
  });
@@ -689,7 +792,7 @@ function execute(operation, repoArg, options = {}) {
689
792
  if (readOnly[operation]) return readOnly[operation](repoArg, options);
690
793
  const transactional = { enable, sync, connect, disconnect, provide, disable, repair };
691
794
  if (!transactional[operation]) return result(operation, "read_only", "fail", { blockers: ["unsupported_project_technology_operation"] });
692
- if (!options.apply) return preview(operation, repoArg);
795
+ if (!options.apply) return preview(operation, repoArg, options);
693
796
  return transactional[operation](repoArg, options);
694
797
  }
695
798
 
@@ -712,6 +815,7 @@ module.exports = {
712
815
  extensionContract,
713
816
  inventory,
714
817
  normalizeExecutionContract,
818
+ continuity,
715
819
  plan,
716
820
  provide,
717
821
  readExport,
@@ -47,7 +47,9 @@
47
47
  "review",
48
48
  "release_gate",
49
49
  "kaizen_item",
50
- "evidence"
50
+ "evidence",
51
+ "regression_case",
52
+ "lesson"
51
53
  ],
52
54
  "allowed_relation_types": [
53
55
  "derives_projection",
@@ -12,7 +12,14 @@
12
12
  "milestone",
13
13
  "stakeholder",
14
14
  "evidence",
15
- "governance_gate"
15
+ "governance_gate",
16
+ "project_dna",
17
+ "stage",
18
+ "batch",
19
+ "requirement",
20
+ "method",
21
+ "lesson",
22
+ "regression_case"
16
23
  ],
17
24
  "allowed_relation_types": [
18
25
  "depends_on",
@@ -0,0 +1,25 @@
1
+ # Mirai Graph 1.1.1
2
+
3
+ Mirai Graph 1.1.1 is a compatibility correction for sequential context
4
+ traversal.
5
+
6
+ ## What changed
7
+
8
+ - Existing graph 2.0.0 object and relation aliases are normalized in memory.
9
+ - Skill-style `implements`, `has_capability`, `validates` and related accepted
10
+ relations retain their structural or mandatory meaning.
11
+ - Accepted historical readiness levels remain executable, while incomplete and
12
+ unsafe states still block compilation.
13
+
14
+ ## Why this patch is needed
15
+
16
+ The 1.1.0 traversal fixtures used the newest field names, while long-lived
17
+ graphs used accepted older aliases. The result was a false loss of nested
18
+ context and two blocked repositories. This patch fixes the reader instead of
19
+ forcing every consumer to rewrite its graph.
20
+
21
+ ## Evidence boundary
22
+
23
+ The release candidate is accepted only when the full test suite and real
24
+ multi-repository consumer harness are green. Publication remains a separate
25
+ external action.
@@ -0,0 +1,19 @@
1
+ # Mirai Graph 1.2.0
2
+
3
+ Mirai Graph 1.2.0 adds portable project continuity to the existing Project
4
+ Technology mechanism.
5
+
6
+ Verified outcomes, requirements, evidence and reusable cases can now be stored
7
+ in `graph/specs` and discovered automatically by a later task. The mechanism
8
+ does not store chat transcripts and does not introduce a second memory database.
9
+
10
+ Mutable receipts and rollback data remain on the current host. Git repositories
11
+ and ordinary shared folders use the same canonical graph contract; shared-folder
12
+ writes additionally use a lease and compare-and-swap digest.
13
+
14
+ Goals, architectural decisions and owner or source-of-truth changes remain
15
+ proposals until explicitly accepted. A generated context document remains a
16
+ readable projection rather than a source of truth or write authorization.
17
+
18
+ `graph.json` remains schema `2.0.0` and
19
+ `mirai.project_technology.contract_version` remains `1.0.0`.
@@ -15,6 +15,8 @@ Release notes must separate:
15
15
 
16
16
  ## Release Notes
17
17
 
18
+ - [v1.2.0](1.2.0.md) - portable task-boundary project continuity in Project
19
+ Technology.
18
20
  - [v1.1.0](1.1.0.md) - universal sequential context traversal and usage
19
21
  verification in Project Technology.
20
22
  - [v1.0.0](1.0.0.md) - stable Project Technology, CLI and public contract.
@@ -9,6 +9,7 @@
9
9
  "contract_version": { "const": "1.0.0" },
10
10
  "enabled": { "type": "boolean" },
11
11
  "context_policy": { "const": "task_scoped" },
12
- "source_boundary": { "const": "hybrid_sot" }
12
+ "source_boundary": { "const": "hybrid_sot" },
13
+ "continuity_policy": { "enum": ["task_boundary"] }
13
14
  }
14
15
  }
@@ -1,6 +1,6 @@
1
1
  # Mirai Graph Context-Pack Generation
2
2
 
3
- Status: 1.1 stable standard section
3
+ Status: 1.2 stable standard section
4
4
 
5
5
  ## Purpose
6
6
 
@@ -56,7 +56,7 @@ task -> select relevant objects -> select relevant relations
56
56
  -> generate context pack -> review/use for task
57
57
  ```
58
58
 
59
- Project Technology 1.1 implements this as a deterministic four-phase protocol:
59
+ Project Technology implements this as a deterministic four-phase protocol:
60
60
 
61
61
  ```text
62
62
  discover -> expand -> compile -> verify
@@ -168,6 +168,12 @@ Generated context packs may be reviewed and then used as evidence or source
168
168
  material for graph updates. They do not automatically mutate canonical graph
169
169
  state.
170
170
 
171
+ With `continuity_policy: task_boundary`, accepted continuity objects in
172
+ `graph/specs` participate in the same traversal as every other graph object.
173
+ Relevant previous cases can therefore be found without reading chat history or
174
+ requiring a user to say "continue". Host-local receipts never enter a context
175
+ pack and never become portable authority.
176
+
171
177
  ## Anti-Patterns
172
178
 
173
179
  - Treating a generated context pack as the graph.
@@ -112,11 +112,17 @@ Initial core kinds:
112
112
  - `resource`, `source`;
113
113
  - `check`, `gate`, `constraint`;
114
114
  - `access_requirement`.
115
+ - `project_dna`, `stage`, `batch`;
116
+ - `method`, `lesson`, `regression_case`.
115
117
 
116
118
  The additional domain-neutral kinds support Project Technology traversal. A
117
119
  consumer may display them using its own vocabulary without changing their
118
120
  canonical meaning.
119
121
 
122
+ Portable continuity uses these existing domain-neutral kinds. A verified task
123
+ result is evidence plus a regression case; an inferred reusable method remains
124
+ a lesson proposal until its promotion rule is satisfied.
125
+
120
126
  ## Object Boundary Rules
121
127
 
122
128
  An object should be created when the item needs at least one of:
@@ -1,6 +1,6 @@
1
1
  # Project Technology
2
2
 
3
- Status: 1.1 stable standard; activation contract remains 1.0.0
3
+ Status: 1.2 stable standard; activation contract remains 1.0.0
4
4
 
5
5
  Project Technology is the shared executable mechanism of Mirai Graph. It is
6
6
  not a profile, a second graph or a source of domain methodology.
@@ -19,6 +19,31 @@ Each graph keeps its own objects and relations. Project Technology only
19
19
  standardizes safe inventory, task context, accepted-target binding, freshness
20
20
  and verification.
21
21
 
22
+ ## Portable Project Continuity
23
+
24
+ Project Technology preserves verified project experience at task boundaries:
25
+
26
+ ```text
27
+ task context -> verified outcome -> semantic candidate -> graph/specs
28
+ -> next task context -> usage verification
29
+ ```
30
+
31
+ `graph/specs` is the portable authority. Chat history and local workflow files
32
+ are not required by another person or installation. Mutable receipts and
33
+ rollback data are stored outside the project in a host-local directory keyed by
34
+ `graph.id`.
35
+
36
+ The existing `sync` operation accepts `task_start`, `stage_complete` and
37
+ `task_complete`. Task start is read-only. A completed stage or task requires a
38
+ verified, requirement-bound input and records accepted evidence plus one
39
+ reusable regression case. New goals, architectural decisions and ownership
40
+ changes remain proposals.
41
+
42
+ Two independent verified cases with the same signature may promote a method
43
+ lesson when they do not change architecture. Repeated input is byte-identical
44
+ and returns `changed=false`. Shared folders without Git additionally use a
45
+ lease, compare-and-swap graph digest, backup, atomic write and readback.
46
+
22
47
  ## Sequential Context Traversal
23
48
 
24
49
  Project Technology exposes one model-independent navigation protocol:
@@ -59,13 +84,15 @@ The only manifest extension key is `mirai.project_technology`:
59
84
  "contract_version": "1.0.0",
60
85
  "enabled": true,
61
86
  "context_policy": "task_scoped",
62
- "source_boundary": "hybrid_sot"
87
+ "source_boundary": "hybrid_sot",
88
+ "continuity_policy": "task_boundary"
63
89
  }
64
90
  ```
65
91
 
66
92
  Canonical graph objects remain in `graph/specs`. Exact external revisions
67
- remain in `graph.lock.json`. Mutable local state is ignored under
68
- `.mirai-graph/project-technology/`.
93
+ remain in `graph.lock.json`. Mutable continuity state is host-local and keyed
94
+ by graph identity; project-local runtime directories are not continuity
95
+ authority.
69
96
 
70
97
  The only generated provider artifact is:
71
98
 
@@ -106,6 +133,10 @@ source still answers "how should the domain work be done?".
106
133
  diagnostic `verify` calls.
107
134
  - Context traversal is read-only in every phase and never grants write
108
135
  authority.
136
+ - Chat text, secrets, private source, user paths and unverified assumptions are
137
+ rejected from portable continuity.
138
+ - Significant work with an enabled continuity policy fails closed when its
139
+ terminal receipt is missing or stale.
109
140
  - A context pack is not ready while a required branch, source, access boundary
110
141
  or validator is missing, stale, blocked, deprecated, conflicting or
111
142
  digest-mismatched.
@@ -1,6 +1,6 @@
1
1
  # Mirai Graph Test Suite
2
2
 
3
- Status: 1.1 stable standard section
3
+ Status: 1.2 stable standard section
4
4
 
5
5
  ## Purpose
6
6
 
@@ -36,6 +36,9 @@ Current checks:
36
36
  research, organization, platform, skill-like and multi-repository scopes;
37
37
  - reject incomplete mandatory closure, stale nodes, conflicts, cycles,
38
38
  tampered digests, secret-like evidence and context-budget shortcuts.
39
+ - verify portable continuity for Git and non-Git projects, two independent
40
+ installations, idempotent repeats, stale receipts, CAS conflicts, secret
41
+ rejection, bounded proposals and two-case method promotion.
39
42
 
40
43
  ## Structural Validation
41
44