mirai-graph 1.4.0 → 1.6.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.
@@ -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", "--verify", "HEAD^{commit}") || null;
460
+ if (!revision) {
461
+ if (fs.existsSync(path.join(repo, ".git")) && !unbornGitBranch(repo)) 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,14 +498,28 @@ 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
 
472
- function context(repoArg, options = {}) {
509
+ // A valid new repository has no HEAD commit yet. It may inventory declared
510
+ // sources, but must never impersonate a revision-bound provider. Missing tools,
511
+ // corrupt refs/index and detached invalid HEAD are not an unborn branch.
512
+ function unbornGitBranch(repo) {
513
+ const options = { cwd: repo, encoding: "utf8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } };
514
+ const branch = spawnSync("git", ["symbolic-ref", "--quiet", "HEAD"], options);
515
+ const ref = (branch.stdout || "").trim();
516
+ if (branch.status !== 0 || !ref.startsWith("refs/heads/")) return false;
517
+ const existing = spawnSync("git", ["show-ref", "--verify", "--quiet", ref], options);
518
+ if (existing.status !== 1 || existing.stderr) return false;
519
+ return spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=no"], options).status === 0;
520
+ }
521
+
522
+ function contextTraversal(repoArg, options = {}) {
473
523
  if (options.phase === "expand") return traversal.expandContext(repoArg, options.traversalReceipt, options.selectedIds, options);
474
524
  if (options.phase === "compile") return traversal.compileContext(repoArg, options.traversalReceipt, options.selection, options);
475
525
  if (options.phase === "verify") return traversal.verifyContext(repoArg, options.contextPack, options.usageEvidence, options);
@@ -492,10 +542,147 @@ function context(repoArg, options = {}) {
492
542
  };
493
543
  }
494
544
 
545
+ function context(repoArg, options = {}) {
546
+ const output = contextTraversal(repoArg, options);
547
+ const binding = targetBindingStatus(normalizeRepo(repoArg), options);
548
+ const blockers = [...(output.blockers || [])];
549
+ if (options.significantWork && binding.status !== "ready") blockers.push(...binding.blockers, "accepted_target_binding_required_for_significant_work");
550
+ if (options.significantWork && binding.source_kind === "local") blockers.push(...localVerificationBlockers(normalizeRepo(repoArg), options));
551
+ return { ...output, target_binding: binding, blockers: [...new Set(blockers)].sort(),
552
+ status: blockers.length ? "blocked" : output.status };
553
+ }
554
+
555
+ function localVerificationBlockers(repo, options) {
556
+ const current = continuity.status(repo, readManifest(repo).manifest, options);
557
+ if (!current.terminal_receipt) return ["local_target_verification_missing"];
558
+ return current.terminal_receipt.current_graph_digest === current.graph_digest ? [] : ["local_target_verification_stale"];
559
+ }
560
+
561
+ // Resolve a local target from the same canonical object reader used by context.
562
+ // Approval origin is a caller responsibility, just like archive-provider trust.
563
+ // Never infer that trust from the decision file or persist it as approval.
564
+ function localTargetStatus(repo, manifest, selection, options = {}) {
565
+ const blockers = [];
566
+ const blocked = () => ({ status: "blocked", enabled: true, source_kind: "local", blockers: [...new Set(blockers)].sort(), next_action: "review the local target and verify its owner acceptance" });
567
+ if (!selection || selection.kind !== "local" || Object.keys(selection).some(k => !["kind", "target_id", "acceptance_ref"].includes(k)) ||
568
+ !REF_RE.test(selection.target_id || "") || !REF_RE.test(selection.acceptance_ref || "")) {
569
+ blockers.push("local_target_selection_invalid"); return blocked();
570
+ }
571
+ const graph = traversal.readGraph(repo);
572
+ blockers.push(...graph.blockers);
573
+ if (!(graph.objects instanceof Map)) { blockers.push("local_target_graph_invalid"); return blocked(); }
574
+ const target = graph.objects.get(selection.target_id)?.value;
575
+ const decision = graph.objects.get(selection.acceptance_ref)?.value;
576
+ const decisionRecord = graph.objects.get(selection.acceptance_ref);
577
+ if (decisionRecord && (!decisionRecord.relative.startsWith("graph/specs/") || !localSafeFile(repo, decisionRecord.relative))) blockers.push("local_target_acceptance_source_unsafe");
578
+ if (!target || !["contract", "system"].includes(target.tz_role)) { blockers.push("local_target_missing_or_invalid"); return blocked(); }
579
+ const normalized = normalizeExecutionContract(target.provider_execution_contract);
580
+ blockers.push(...normalized.blockers);
581
+ const contract = normalized.contract;
582
+ const accepted = obj => obj && ACCEPTED_LIFECYCLES.has(obj.lifecycle) &&
583
+ (!obj.readiness || ACCEPTED_LIFECYCLES.has(obj.readiness));
584
+ if (!accepted(target)) blockers.push("local_target_not_accepted");
585
+ const refs = new Set([selection.target_id, contract.goal_binding?.goal_id,
586
+ ...(contract.goal_binding?.done_when_ids || []), ...(contract.constraint_ids || []),
587
+ ...(contract.non_goal_ids || []), ...(contract.deferred_boundary_ids || []),
588
+ ...(contract.decision_refs || []), contract.architecture_contract?.contract_ref]);
589
+ for (const item of contract.requirement_bindings || []) {
590
+ refs.add(item.requirement_id);
591
+ for (const id of [...item.acceptance_ids, ...item.done_when_ids]) refs.add(id);
592
+ }
593
+ // The approval object cannot participate in the digest it signs.
594
+ refs.delete(selection.acceptance_ref); refs.delete(undefined);
595
+ const active = new Set(); const visited = new Set();
596
+ function visit(id) {
597
+ if (active.has(id)) { blockers.push("local_target_required_cycle"); return; }
598
+ if (visited.has(id)) return;
599
+ visited.add(id); active.add(id);
600
+ for (const relation of graph.relations.filter(r => r.source === id && traversal.REQUIRED_RELATIONS.has(r.type))) {
601
+ if (!ACCEPTED_LIFECYCLES.has(relation.lifecycle || relation.readiness)) blockers.push("local_target_relation_not_accepted");
602
+ if (relation.target === selection.acceptance_ref) continue;
603
+ refs.add(relation.target); visit(relation.target);
604
+ }
605
+ active.delete(id);
606
+ }
607
+ for (const id of [...refs]) visit(id);
608
+ const semanticObjects = []; const sources = [];
609
+ const excluded = new Set(["created_at", "updated_at", "semantic_digest", "execution_contract_digest", "content_revision", "evidence", "approval_ref"]);
610
+ for (const id of [...refs].sort()) {
611
+ const record = graph.objects.get(id);
612
+ if (!record) { blockers.push("local_target_required_object_missing"); continue; }
613
+ if (!accepted(record.value)) blockers.push("local_target_required_object_not_accepted");
614
+ if (!record.relative.startsWith("graph/specs/") || !localSafeFile(repo, record.relative)) { blockers.push("local_target_source_unsafe"); continue; }
615
+ const value = Object.fromEntries(Object.entries(record.value).filter(([key]) => !excluded.has(key)));
616
+ // Digests of referenced tools/evidence belong to content freshness, not meaning.
617
+ const sourceRefs = record.value.source_refs || [];
618
+ const evidenceRefs = record.value.evidence || [];
619
+ if (!Array.isArray(sourceRefs) || !Array.isArray(evidenceRefs) ||
620
+ sourceRefs.some(ref => typeof (typeof ref === "string" ? ref : ref?.ref) !== "string")) {
621
+ blockers.push("local_target_source_refs_invalid"); continue;
622
+ }
623
+ value.source_refs = sourceRefs.map(ref => typeof ref === "string" ? ref : ref.ref).sort();
624
+ semanticObjects.push(value);
625
+ for (const raw of [...sourceRefs, ...evidenceRefs]) {
626
+ const ref = typeof raw === "string" ? raw : raw?.ref;
627
+ if (!localSafeFile(repo, ref)) { blockers.push("local_target_source_unsafe_or_missing"); continue; }
628
+ const bytes = fs.readFileSync(path.join(repo, ref));
629
+ const actual = sha256(bytes, true);
630
+ if (typeof raw === "object" && (raw.sha256 || raw.digest) && (raw.sha256 || raw.digest) !== actual) blockers.push("local_target_source_digest_mismatch");
631
+ sources.push({ ref, sha256: actual });
632
+ }
633
+ }
634
+ const relations = graph.relations.filter(r => refs.has(r.source) && refs.has(r.target)).map(({ _record, ...r }) =>
635
+ Object.fromEntries(Object.entries(r).filter(([k]) => !excluded.has(k))));
636
+ relations.sort((a,b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
637
+ if (relations.some(r => r.type === "conflicts_with")) blockers.push("local_target_conflict");
638
+ if (contract.goal_binding?.done_when_ids.some(id => !contract.requirement_bindings.some(r => r.done_when_ids.includes(id)))) blockers.push("local_target_done_when_uncovered");
639
+ const semanticDigest = sha256(canonicalBytes({ graph_id: manifest.id, target_id: selection.target_id, objects: semanticObjects, relations }), true);
640
+ const executionDigest = sha256(canonicalBytes(contract));
641
+ const contentRevision = sha256(canonicalBytes([...new Map(sources.map(s => [s.ref, s])).values()].sort((a,b) => a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)), true);
642
+ if (target.semantic_digest !== semanticDigest) blockers.push("local_target_semantic_digest_mismatch");
643
+ if (target.content_revision !== contentRevision) blockers.push("local_target_content_stale");
644
+ if (!decision || decision.kind !== "decision" || decision.subtype !== "architecture_baseline_acceptance" || !accepted(decision)) blockers.push("local_target_acceptance_missing_or_invalid");
645
+ const architecture = contract.architecture_contract || {};
646
+ if (!contract.decision_refs?.includes(selection.acceptance_ref) || architecture.acceptance_ref !== selection.acceptance_ref) blockers.push("local_target_acceptance_ref_mismatch");
647
+ if (decision && (decision.graph_id !== manifest.id || decision.target_id !== selection.target_id ||
648
+ decision.owner_id !== architecture.architecture_owner_id || decision.semantic_digest !== semanticDigest ||
649
+ decision.execution_contract_digest !== executionDigest || !REF_RE.test(decision.approval_ref || ""))) blockers.push("local_target_acceptance_identity_mismatch");
650
+ const trust = options.localAcceptance;
651
+ if (!trust || Object.keys(trust).some(k => !["graphId", "targetId", "ownerId", "decisionSha256"].includes(k)) ||
652
+ trust.graphId !== manifest.id || trust.targetId !== selection.target_id || trust.ownerId !== architecture.architecture_owner_id ||
653
+ !decision || trust.decisionSha256 !== sha256(canonicalBytes(decision))) blockers.push("local_target_acceptance_unverified");
654
+ if (!contract.allowed_change_scope?.some(s => s.repository_id === manifest.id)) blockers.push("local_target_repository_scope_unapproved");
655
+ return { ...blocked(), status: blockers.length ? "blocked" : "ready", repository_id: manifest.id,
656
+ target_id: selection.target_id, acceptance_ref: selection.acceptance_ref,
657
+ semantic_digest: semanticDigest, content_revision: contentRevision,
658
+ execution_contract_digest: executionDigest, execution_contract: contract,
659
+ next_action: blockers.length ? "review the local target and verify its owner acceptance" : "none" };
660
+ }
661
+
662
+ function localSafeFile(repo, ref) {
663
+ if (typeof ref !== "string" || !ref || /[:\\\\]/.test(ref) || path.posix.isAbsolute(ref) || ref.split("/").some(p => ["..", ".", "source", "generated"].includes(p)) ||
664
+ SECRET_PARTS.some(p => ref.toLowerCase().includes(p))) return false;
665
+ let current = repo;
666
+ try {
667
+ for (const part of ref.split("/")) { current = path.join(current, part); if (fs.lstatSync(current).isSymbolicLink()) return false; }
668
+ return fs.statSync(current).isFile();
669
+ } catch (_) { return false; }
670
+ }
671
+
495
672
  function targetBindingStatus(repo, options = {}) {
496
673
  const manifest = readManifest(repo).manifest;
497
674
  const root = runtimeRoot(repo, manifest, options);
498
675
  const bindingPath = path.join(root, BINDING_FILE);
676
+ const selection = options.targetSource || manifest?.extensions?.[EXTENSION_KEY]?.target_source;
677
+ if (selection) {
678
+ if (fs.existsSync(bindingPath)) return { status: "blocked", enabled: true, source_kind: "conflict", blockers: ["local_external_target_conflict"], next_action: "explicitly reconcile local and external targets" };
679
+ const local = localTargetStatus(repo, manifest, selection, options);
680
+ if (manifest?.extensions?.[EXTENSION_KEY]?.enabled !== true) {
681
+ local.status = "blocked";
682
+ local.blockers = [...new Set([...local.blockers, "project_technology_disabled"])].sort();
683
+ }
684
+ return local;
685
+ }
499
686
  if (!fs.existsSync(bindingPath)) return { status: "not_configured", enabled: false, blockers: [], next_action: "connect an exact target provider binding" };
500
687
  let binding;
501
688
  try { binding = readJson(bindingPath); } catch (_) { return { status: "blocked", enabled: true, blockers: ["target_binding_invalid"] }; }
@@ -521,6 +708,7 @@ function targetBindingStatus(repo, options = {}) {
521
708
  provider_export_sha256: binding.provider_export_sha256 || null,
522
709
  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
710
  execution_contract_digest: exported.export.execution_contract_digest || null,
711
+ provider_graph_id: exported.export.provider_graph_id || null,
524
712
  blockers: [...new Set(blockers)].sort(),
525
713
  next_action: blockers.length ? "refresh or repair the exact provider binding" : "none",
526
714
  };
@@ -535,7 +723,7 @@ function status(repoArg, options = {}) {
535
723
  try { if (fs.existsSync(path.join(root, INVENTORY_FILE))) stored = readJson(path.join(root, INVENTORY_FILE)); } catch (_) { /* stale */ }
536
724
  const current = inventory(repo);
537
725
  const freshness = stored && stored.inventory_digest === current.inventory_digest ? "current" : stored ? "stale" : "missing";
538
- const blockers = [...manifestState.blockers, ...ext.blockers];
726
+ const blockers = [...manifestState.blockers, ...ext.blockers, ...(current.blockers || [])];
539
727
  if (!stored && legacyRuntimeState(repo).present) blockers.push("project_technology_host_state_migration_required");
540
728
  if (ext.contract && ext.contract.enabled === false) blockers.push("project_technology_disabled");
541
729
  if (freshness !== "current") blockers.push(`project_technology_inventory_${freshness}`);
@@ -548,7 +736,7 @@ function status(repoArg, options = {}) {
548
736
  technology_freshness: freshness,
549
737
  technology_digest: current.inventory_digest,
550
738
  target_binding: binding,
551
- continuity: continuity.status(repo, manifestState.manifest, {}),
739
+ continuity: continuity.status(repo, manifestState.manifest, options),
552
740
  blockers: [...new Set(blockers)].sort(),
553
741
  next_action: ext.legacy ? "run enable --apply to migrate the legacy contract" : blockers.length ? "run plan, then the required transactional operation" : "none",
554
742
  });
@@ -561,10 +749,11 @@ function explain(repoArg) {
561
749
  });
562
750
  }
563
751
 
564
- function plan(repoArg) {
565
- const state = status(repoArg);
752
+ function plan(repoArg, options = {}) {
753
+ const state = status(repoArg, options);
566
754
  return result("plan", "read_only", "success", {
567
755
  current_status: state.status,
756
+ target_binding: state.target_binding,
568
757
  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"],
569
758
  blockers: state.blockers.filter((item) => !["project_technology_not_configured", "project_technology_migration_required", "project_technology_inventory_missing", "project_technology_inventory_stale"].includes(item)),
570
759
  next_action: "rerun the selected operation with --apply",
@@ -573,6 +762,8 @@ function plan(repoArg) {
573
762
 
574
763
  function preview(operation, repoArg, options = {}) {
575
764
  const state = status(repoArg, options);
765
+ if (options.targetSource) return result(operation, "preview", "preview", { apply_required: true,
766
+ target_binding: state.target_binding, blockers: state.target_binding.blockers, next_action: "sync --apply with expected graph digest and independently verified acceptance" });
576
767
  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` });
577
768
  }
578
769
 
@@ -580,11 +771,13 @@ function enable(repoArg, options = {}) {
580
771
  const repo = normalizeRepo(repoArg);
581
772
  const manifestState = readManifest(repo);
582
773
  if (!manifestState.manifest || manifestState.blockers.length) return result("enable", "transactional", "fail", { blockers: manifestState.blockers, next_action: "repair graph.json" });
774
+ const inventoryPreflight = inventory(repo);
775
+ if (inventoryPreflight.blockers?.length) return result("enable", "transactional", "blocked", { blockers: inventoryPreflight.blockers, next_action: "repair declared graph sources" });
583
776
  const manifest = structuredClone(manifestState.manifest);
584
777
  const extensions = { ...(manifest.extensions || {}) };
585
778
  const legacy = extensions[LEGACY_EXTENSION_KEY];
586
779
  delete extensions[LEGACY_EXTENSION_KEY];
587
- extensions[EXTENSION_KEY] = extensionContract();
780
+ extensions[EXTENSION_KEY] = { ...extensionContract(), ...(extensions[EXTENSION_KEY]?.target_source ? { target_source: extensions[EXTENSION_KEY].target_source } : {}) };
588
781
  manifest.extensions = extensions;
589
782
  const root = runtimeRoot(repo, manifest, options);
590
783
  const runtimeMigration = migrateProjectLocalRuntime(repo, manifest, root);
@@ -615,12 +808,15 @@ function enable(repoArg, options = {}) {
615
808
 
616
809
  function sync(repoArg, options = {}) {
617
810
  const repo = normalizeRepo(repoArg);
811
+ if (options.targetSource) return syncLocalSelection(repo, options);
618
812
  const manifestState = readManifest(repo);
619
813
  const ext = extensionState(manifestState.manifest);
620
- const blockers = [...manifestState.blockers, ...ext.blockers];
814
+ const blockers = [...manifestState.blockers, ...ext.blockers, ...(inventory(repo).blockers || [])];
621
815
  const hostRoot = runtimeRoot(repo, manifestState.manifest, options);
622
816
  if (legacyRuntimeState(repo).present && !fs.existsSync(path.join(hostRoot, INVENTORY_FILE))) blockers.push("project_technology_host_state_migration_required");
623
817
  if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
818
+ const binding = targetBindingStatus(repo, options);
819
+ if (binding.source_kind && binding.status !== "ready") blockers.push(...binding.blockers);
624
820
  if (blockers.length) return result("sync", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "enable or repair Project Technology" });
625
821
  let continuityResult = null;
626
822
  if (options.boundary) {
@@ -648,6 +844,56 @@ function sync(repoArg, options = {}) {
648
844
  });
649
845
  }
650
846
 
847
+ function syncLocalSelection(repo, options, remove = false) {
848
+ const operation = remove ? "disconnect" : "sync";
849
+ const manifestState = readManifest(repo);
850
+ const manifest = manifestState.manifest;
851
+ const ext = extensionState(manifest);
852
+ const binding = targetBindingStatus(repo, options);
853
+ const blockers = [...manifestState.blockers, ...ext.blockers, ...(remove ? [] : binding.blockers)];
854
+ if (!remove && ext.contract?.enabled !== true) blockers.push("project_technology_not_enabled");
855
+ if (options.boundary) blockers.push("local_target_selection_requires_separate_boundary");
856
+ const beforeDigest = continuity.graphDigest(repo);
857
+ if (!options.expectedGraphDigest || beforeDigest !== options.expectedGraphDigest) blockers.push("local_target_compare_and_swap_conflict");
858
+ if (blockers.length) return result(operation, "transactional", "blocked", { target_binding: binding, blockers: [...new Set(blockers)].sort() });
859
+ const root = runtimeRoot(repo, manifest, options);
860
+ const lease = continuity.lock(repo, root);
861
+ if (!lease.acquired) return result(operation, "transactional", "blocked", { blockers: ["continuity_lease_conflict"] });
862
+ const file = path.join(repo, "graph.json"); const inventoryPath = path.join(root, INVENTORY_FILE);
863
+ const before = fs.readFileSync(file);
864
+ const inventoryBefore = fs.existsSync(inventoryPath) ? fs.readFileSync(inventoryPath) : null;
865
+ try {
866
+ if (continuity.graphDigest(repo) !== beforeDigest) return result(operation, "transactional", "blocked", { blockers: ["local_target_compare_and_swap_conflict"] });
867
+ const next = structuredClone(manifest);
868
+ if (remove) delete next.extensions[EXTENSION_KEY].target_source;
869
+ else next.extensions[EXTENSION_KEY].target_source = options.targetSource;
870
+ if (canonicalBytes(next) === canonicalBytes(manifest)) return result(operation, "transactional", "success", { target_binding: binding });
871
+ const backup = path.join(root, "rollback", beforeDigest.slice(7), "graph.json");
872
+ atomicWrite(backup, before);
873
+ atomicWrite(file, canonicalBytes(next));
874
+ const inv = inventory(repo);
875
+ if (inv.blockers?.length) throw new Error("local_target_inventory_failed");
876
+ atomicWrite(inventoryPath, canonicalBytes(inv));
877
+ // No source write besides the selector is permitted in this transaction.
878
+ // Ignore expected Git dirtiness from the selector itself during readback.
879
+ if (!fs.readFileSync(file).equals(Buffer.from(canonicalBytes(next)))) throw new Error("local_target_readback_failed");
880
+ const readback = targetBindingStatus(repo, remove ? { ...options, targetSource: undefined } : options);
881
+ if (!remove && (readback.semantic_digest !== binding.semantic_digest || readback.content_revision !== binding.content_revision ||
882
+ readback.blockers.some(code => code !== "graph_source_not_revision_bound"))) throw new Error("local_target_source_changed_during_selection");
883
+ return result(operation, "transactional", "success", { changed: true, target_binding: readback,
884
+ rollback_ref: `host-local://rollback/${beforeDigest.slice(7)}/graph.json`, technology_digest: inv.inventory_digest });
885
+ } catch (_) {
886
+ let restored = false;
887
+ try {
888
+ atomicWrite(file, before);
889
+ if (inventoryBefore) atomicWrite(inventoryPath, inventoryBefore);
890
+ else if (fs.existsSync(inventoryPath)) fs.unlinkSync(inventoryPath);
891
+ restored = fs.readFileSync(file).equals(before);
892
+ } catch (_) { /* recovery required remains explicit */ }
893
+ return result(operation, "transactional", "fail", { blockers: [restored ? "local_target_rollback_applied" : "local_target_recovery_required"] });
894
+ } finally { continuity.releaseLock(lease); }
895
+ }
896
+
651
897
  function provide(repoArg, options = {}) {
652
898
  const repo = normalizeRepo(repoArg);
653
899
  const identity = bindingValues(options);
@@ -661,7 +907,7 @@ function provide(repoArg, options = {}) {
661
907
  blockers.push(...target.blockers);
662
908
  if (blockers.length) return result("provide", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "repair the accepted target contract" });
663
909
  const executionContractDigest = sha256(canonicalBytes(target.contract));
664
- const payload = { schema_version: "1.0.0", ...identity.values, ...target.contract, execution_contract_digest: executionContractDigest };
910
+ const payload = { schema_version: "1.0.0", ...identity.values, ...target.contract, execution_contract_digest: executionContractDigest, provider_graph_id: manifestState.manifest.id };
665
911
  const changed = atomicWrite(path.join(repo, EXPORT_FILE), canonicalBytes(payload));
666
912
  return result("provide", "transactional", "success", { changed, export_ref: EXPORT_FILE, target_binding: payload });
667
913
  }
@@ -671,6 +917,32 @@ function providerRootFor(exportPath) {
671
917
  return completed.status === 0 ? completed.stdout.trim() : null;
672
918
  }
673
919
 
920
+ // This is explicit consumer trust, NOT an assertion read from the provider.
921
+ // The caller must obtain it from authenticated, checksum-bound release metadata.
922
+ // Keeping it out of the export prevents an archive from authenticating itself.
923
+ function archiveProviderIdentity(source, exported, anchor) {
924
+ const fail = code => ({ blockers: [code], ancestors: [] });
925
+ if (!anchor || typeof anchor !== "object" || Array.isArray(anchor)) return fail("provider_archive_trust_invalid");
926
+ const keys = ["exportSha256", "graphId", "providerRevision", "ancestorRevisions"];
927
+ if (Object.keys(anchor).some(key => !keys.includes(key)) || keys.some(key => !Object.hasOwn(anchor, key))) return fail("provider_archive_trust_invalid");
928
+ if (!/^[0-9a-f]{64}$/.test(anchor.exportSha256 || "") ||
929
+ typeof anchor.graphId !== "string" || !REF_RE.test(anchor.graphId) ||
930
+ !REVISION_RE.test(anchor.providerRevision || "") ||
931
+ !Array.isArray(anchor.ancestorRevisions) || anchor.ancestorRevisions.length > 4096 ||
932
+ anchor.ancestorRevisions.some(rev => typeof rev !== "string" || !REVISION_RE.test(rev) || rev === anchor.providerRevision) ||
933
+ new Set(anchor.ancestorRevisions).size !== anchor.ancestorRevisions.length) return fail("provider_archive_trust_invalid");
934
+ try {
935
+ // Parent aliases such as macOS /var are allowed; the exact bytes are pinned.
936
+ if (fs.lstatSync(source).isSymbolicLink()) return fail("provider_archive_source_unsafe");
937
+ if (!fs.statSync(source).isFile() || fs.statSync(source).size > 1024 * 1024) return fail("provider_archive_source_unsafe");
938
+ const bytes = fs.readFileSync(source);
939
+ if (sha256(bytes) !== anchor.exportSha256 || canonicalBytes(JSON.parse(bytes)) !== canonicalBytes(exported)) return fail("provider_archive_export_digest_mismatch");
940
+ } catch (_) { return fail("provider_archive_source_unsafe"); }
941
+ if (exported.provider_graph_id !== anchor.graphId) return fail("provider_archive_graph_mismatch");
942
+ if (exported.provider_revision !== anchor.providerRevision) return fail("provider_archive_revision_mismatch");
943
+ return { blockers: [], ancestors: anchor.ancestorRevisions };
944
+ }
945
+
674
946
  function connect(repoArg, options = {}) {
675
947
  const repo = normalizeRepo(repoArg);
676
948
  const source = path.resolve(String(options.source || ""));
@@ -681,14 +953,21 @@ function connect(repoArg, options = {}) {
681
953
  const blockers = [...identity.blockers, ...read.blockers, ...manifestState.blockers, ...ext.blockers];
682
954
  if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
683
955
  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");
956
+ const archiveRequested = Object.hasOwn(options, "providerArchive");
957
+ if (manifestState.manifest?.extensions?.[EXTENSION_KEY]?.target_source) blockers.push("local_external_target_conflict");
958
+ const archive = archiveRequested ? archiveProviderIdentity(source, read.export, options.providerArchive) : null;
959
+ const providerRoot = archiveRequested ? null : providerRootFor(source);
960
+ if (archive) blockers.push(...archive.blockers);
961
+ else if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
686
962
  else if (git(providerRoot, "rev-parse", "HEAD").toLowerCase() !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
687
963
  const root = runtimeRoot(repo, manifestState.manifest, options);
688
964
  const currentPath = path.join(root, BINDING_FILE);
689
965
  let current = null;
690
966
  try { if (fs.existsSync(currentPath)) current = readJson(currentPath); } catch (_) { blockers.push("current_target_binding_invalid"); }
691
967
  if (current) {
968
+ const currentExport = targetBindingStatus(repo, options);
969
+ const existingGraphId = currentExport.provider_graph_id;
970
+ if (existingGraphId && existingGraphId !== read.export.provider_graph_id) blockers.push("target_provider_graph_conflict");
692
971
  if (!options.refreshBinding) {
693
972
  const same = Object.keys(identity.values).every((key) => current[key] === identity.values[key]);
694
973
  if (!same) blockers.push("target_provider_conflict", "target_provider_refresh_required");
@@ -703,7 +982,9 @@ function connect(repoArg, options = {}) {
703
982
  const currentState = targetBindingStatus(repo, options);
704
983
  if (currentState.status !== "ready") blockers.push(...currentState.blockers);
705
984
  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");
985
+ if (archive) {
986
+ if (current.provider_revision !== identity.values.provider_revision && !archive.ancestors.includes(current.provider_revision)) blockers.push("provider_revision_not_forward");
987
+ } else if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
707
988
  else {
708
989
  if (current.provider_revision !== identity.values.provider_revision) {
709
990
  const ancestry = spawnSync("git", ["-C", providerRoot, "merge-base", "--is-ancestor", current.provider_revision, identity.values.provider_revision]);
@@ -735,6 +1016,7 @@ function connect(repoArg, options = {}) {
735
1016
 
736
1017
  function disconnect(repoArg, options = {}) {
737
1018
  const repo = normalizeRepo(repoArg);
1019
+ if (readManifest(repo).manifest?.extensions?.[EXTENSION_KEY]?.target_source) return syncLocalSelection(repo, options, true);
738
1020
  const root = runtimeRoot(repo, readManifest(repo).manifest, options);
739
1021
  const bindingPath = path.join(root, BINDING_FILE);
740
1022
  if (!fs.existsSync(bindingPath)) return result("disconnect", "transactional", "success");
@@ -768,7 +1050,40 @@ function repair(repoArg, options = {}) {
768
1050
  return sync(repo, options);
769
1051
  }
770
1052
 
1053
+ function verifyProviderExport(repoArg, options = {}) {
1054
+ const repo = normalizeRepo(repoArg);
1055
+ const source = path.resolve(options.source || path.join(repo, EXPORT_FILE));
1056
+ const exported = readExport(source);
1057
+ const manifestState = readManifest(repo);
1058
+ const ext = extensionState(manifestState.manifest);
1059
+ const identity = bindingValues(exported.export);
1060
+ const blockers = [...exported.blockers, ...manifestState.blockers, ...ext.blockers, ...identity.blockers];
1061
+ if (options.significantWork) blockers.push("provider_export_verification_is_not_execution_authority");
1062
+ if (!trackedFiles(repo).includes("graph.json") || spawnSync("git", ["diff", "--quiet", "HEAD", "--", "graph.json"], { cwd: repo }).status !== 0) blockers.push("provider_manifest_not_revision_bound");
1063
+ if (!source.startsWith(`${repo}${path.sep}`)) blockers.push("provider_export_outside_repository");
1064
+ if (ext.contract?.enabled !== true || ext.legacy) blockers.push("project_technology_not_enabled");
1065
+ const head = git(repo, "rev-parse", "HEAD");
1066
+ if (head !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
1067
+ if (exported.export.provider_graph_id !== manifestState.manifest?.id) blockers.push("provider_archive_graph_mismatch");
1068
+ const target = targetContract(repo, identity.values.target_id, identity.values.semantic_digest, manifestState.manifest);
1069
+ blockers.push(...target.blockers);
1070
+ if (sha256(canonicalBytes(target.contract)) !== exported.export.execution_contract_digest) blockers.push("provider_execution_contract_source_mismatch");
1071
+ // Bound supported ancestry; deeper histories need a narrower supported
1072
+ // release window rather than unbounded metadata in every consumer.
1073
+ const history = git(repo, "rev-list", "--max-count=4098", "HEAD").split("\n").filter(Boolean);
1074
+ if (history[0] !== head || history.length > 4097) blockers.push("provider_archive_ancestry_unverifiable_or_too_large");
1075
+ return result("verify", "read_only", blockers.length ? "blocked" : "success", {
1076
+ blockers: [...new Set(blockers)].sort(),
1077
+ provider_archive: blockers.length ? null : {
1078
+ exportSha256: exported.raw_sha256, graphId: manifestState.manifest.id,
1079
+ providerRevision: head, ancestorRevisions: history.slice(1),
1080
+ },
1081
+ next_action: blockers.length ? "repair the canonical provider export before packaging" : "seal this anchor in authenticated release metadata",
1082
+ });
1083
+ }
1084
+
771
1085
  function verify(repoArg, options = {}) {
1086
+ if (options.source) return verifyProviderExport(repoArg, options);
772
1087
  const state = status(repoArg, options);
773
1088
  const blockers = [...state.blockers];
774
1089
  const repo = normalizeRepo(repoArg);
@@ -776,6 +1091,7 @@ function verify(repoArg, options = {}) {
776
1091
  const continuityResult = continuity.verify(repo, manifest, options);
777
1092
  if (continuity.continuityPolicy(manifest) === "task_boundary" || options.receiptDigest || options.significantWork) blockers.push(...continuityResult.blockers);
778
1093
  if (options.significantWork && state.target_binding.status !== "ready") blockers.push("accepted_target_binding_required_for_significant_work");
1094
+ if (options.significantWork && state.target_binding.source_kind === "local") blockers.push(...localVerificationBlockers(repo, options));
779
1095
  return result("verify", "read_only", blockers.length ? "blocked" : "success", {
780
1096
  repository_id: state.repository_id,
781
1097
  enabled: state.enabled,
@@ -835,6 +1151,7 @@ module.exports = {
835
1151
  sync,
836
1152
  targetBindingStatus,
837
1153
  verify,
1154
+ verifyProviderExport,
838
1155
  verifyArtifactRelease: artifacts.verifyArtifactRelease,
839
1156
  verifyTechnologyCourse: technologyCourse.verifyTechnologyCourse,
840
1157
  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.
@@ -0,0 +1,43 @@
1
+ # Mirai Graph 1.6.0 candidate
2
+
3
+ Status: local release preparation, not published or independently accepted.
4
+ This is an additive compatible 1.x release. Intended npm channel: `legacy-1`;
5
+ it must not move the separate 2.x `latest` channel.
6
+
7
+ ## Result
8
+
9
+ An ordinary project folder can explicitly select its own accepted target without
10
+ Git or a self-connected external provider. The caller authenticates the human
11
+ approval and supplies an independent trust anchor; the graph cannot approve
12
+ itself. Existing external provider bindings remain supported and fail closed.
13
+
14
+ The same target identity is exposed in status, plan, context and verify. Selection
15
+ and disconnect reuse the existing transaction, lease, compare-and-swap, backup
16
+ and rollback. Semantic acceptance is distinct from source-content freshness.
17
+ Significant work still requires current task-boundary verification evidence.
18
+
19
+ A valid new Git branch can inventory declared sources before its first commit.
20
+ Corrupt refs/indexes and unavailable Git remain blocked. No commit revision is
21
+ invented and provider export still requires a real committed revision.
22
+
23
+ ## Compatibility
24
+
25
+ - Manifest schema remains `2.0.0`; activation contract remains `1.0.0`.
26
+ - No new profile, engine, approval issuer or source-of-truth store.
27
+ - Local target selection is explicit; missing approval never falls back to ready.
28
+ - An offline synchronization conflict requires reconciliation. A filesystem
29
+ lease is not a distributed lock between disconnected copies.
30
+ - A new host may inspect the project, but cannot execute solely because a copied
31
+ folder declares itself accepted or contains a matching adjacent checksum.
32
+
33
+ ## Verification and limits
34
+
35
+ The implementation commits passed the complete test suite and the existing
36
+ Windows/macOS/Ubuntu Project Technology, context and continuity CI matrix.
37
+ Release packaging is checked separately on the versioned artifact. These tests
38
+ do not prove the complete installer of any downstream application, a real shared
39
+ network filesystem or an employee's legacy installation.
40
+
41
+ Independent acceptance, immutable package publication and downstream exact-lock
42
+ installation are separate requirements. No tag, GitHub Release or npm package
43
+ is represented as published by this candidate document.
@@ -15,6 +15,10 @@ Release notes must separate:
15
15
 
16
16
  ## Release Notes
17
17
 
18
+ - [v1.6.0 candidate](1.6.0.md) - local accepted targets and valid unborn Git
19
+ inventory; unpublished, intended for `legacy-1` only.
20
+ - [v1.5.0](1.5.0.md) - compatible 1.x authenticated archive providers and
21
+ declared-source inventory without Git (`legacy-1`, not npm `latest`).
18
22
  - [v1.2.0](1.2.0.md) - portable task-boundary project continuity in Project
19
23
  Technology.
20
24
  - [v1.1.0](1.1.0.md) - universal sequential context traversal and usage
@@ -10,6 +10,15 @@
10
10
  "enabled": { "type": "boolean" },
11
11
  "context_policy": { "const": "task_scoped" },
12
12
  "source_boundary": { "const": "hybrid_sot" },
13
- "continuity_policy": { "enum": ["task_boundary"] }
13
+ "continuity_policy": { "enum": ["task_boundary"] },
14
+ "target_source": {
15
+ "type": "object", "additionalProperties": false,
16
+ "required": ["kind", "target_id", "acceptance_ref"],
17
+ "properties": {
18
+ "kind": { "const": "local" },
19
+ "target_id": { "type": "string", "minLength": 2 },
20
+ "acceptance_ref": { "type": "string", "minLength": 2 }
21
+ }
22
+ }
14
23
  }
15
24
  }