mirai-graph 1.5.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.
@@ -29,6 +29,9 @@ jobs:
29
29
  - name: Verify portable project continuity
30
30
  run: node packages/cli/validate-project-continuity.js
31
31
 
32
+ - name: Verify local target and provider contracts
33
+ run: npm run test:project-technology
34
+
32
35
  release-check:
33
36
  runs-on: ubuntu-latest
34
37
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to Mirai Graph will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.6.0] - Unpublished candidate
8
+
9
+ Compatible additive release for the 1.x line; intended npm channel `legacy-1`.
10
+ The 2.x `latest` channel is not changed. Publication and independent acceptance
11
+ are pending; no installed consumer is automatically migrated.
12
+
13
+ - A valid new Git branch without its first commit can inventory only explicitly
14
+ declared sources. Corrupt or unavailable Git stays blocked; no revision is
15
+ invented and provider export still requires a real commit. The existing
16
+ three-platform CI matrix now includes local-target/provider regressions.
17
+
18
+ - Explicitly selected local accepted targets in ordinary folders and
19
+ Git repositories, using the existing execution-contract validator and graph
20
+ reader. Independent caller-verified approval anchors prevent self-acceptance.
21
+ - Local selection/disconnection reuse continuity lease, CAS, backup and rollback;
22
+ diagnostics are read-only. Semantic acceptance and source-content freshness
23
+ are separate; significant work requires current verification evidence.
24
+ - `plan` now reports the same target binding as `status`, `context` and `verify`.
25
+ External provider identity and ancestry checks are unchanged.
26
+
7
27
  ## [1.5.0] - 2026-09-03
8
28
 
9
29
  Compatible 1.x maintenance line, published under `legacy-1`; the npm `latest`
package/CITATION.cff CHANGED
@@ -17,5 +17,5 @@ keywords:
17
17
  - complex systems
18
18
  - context engineering
19
19
  - systems engineering
20
- version: "1.5.0"
20
+ version: "1.6.0"
21
21
  date-released: "2026-09-03"
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  Make your project understandable to humans and AI.
4
4
 
5
+ Local-target 1.6.0 candidate (not released): ordinary folders can select an accepted
6
+ target without Git or a self-connected provider. The calling application must
7
+ authenticate the owner's approval; adjacent checksums never grant authority.
8
+ See [Project Technology](standard/project-technology.md#local-accepted-targets-candidate-extension)
9
+ for the trust boundary, preview, verification and transactional selection.
10
+
5
11
  Mirai Graph turns scattered project knowledge into a structured graph of
6
12
  features, requirements, dependencies, risks, decisions, evidence and process
7
13
  gates.
@@ -10,7 +16,8 @@ Use it when a software project has useful knowledge spread across README files,
10
16
  docs, issues, chats, code comments and AI prompts, and you need a small
11
17
  machine-readable map that developers and AI assistants can validate and reuse.
12
18
 
13
- Status: stable public `1.3.0` release.
19
+ Status: compatible 1.x line; `1.6.0` is an unpublished candidate based on `1.5.0`.
20
+ The separate 2.x line is not included or migrated here.
14
21
 
15
22
  ## What Problem Does It Solve?
16
23
 
@@ -42,20 +49,18 @@ Start with a normal repository and use Mirai Graph to:
42
49
 
43
50
  ## Current Installation Status
44
51
 
45
- The repository provides the stable `mirai-graph@1.3.0` package. To verify a
46
- repository checkout directly, use:
52
+ The compatible published baseline is `mirai-graph@1.5.0` on `legacy-1`.
53
+ Do not use an unversioned npm install to select this generation: `latest` is
54
+ independent. To verify this candidate, use its exact reviewed checkout:
47
55
 
48
56
  ```bash
49
- git clone https://github.com/zabarov/mirai-graph.git
50
- cd mirai-graph
51
- npm install
52
57
  npm run release:check
53
58
  ```
54
59
 
55
- External projects use:
60
+ External 1.x projects can explicitly pin the published baseline:
56
61
 
57
62
  ```bash
58
- npm install -D mirai-graph
63
+ npm install --save-dev --save-exact mirai-graph@1.5.0
59
64
  ```
60
65
 
61
66
  ## Connect A Project In 10 Minutes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirai-graph",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "description": "An evolutionary graph operating model for managing the growth of complex systems.",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "pretest": "npm run test:project-technology && npm run test:context-traversal && npm run test:project-continuity && npm run test:artifact-releases && npm run test:technology-courses && npm run validate:project-technology-schemas",
17
- "test:project-technology": "node packages/cli/validate-project-technology.js && node packages/cli/validate-provider-archive.js",
17
+ "test:project-technology": "node packages/cli/validate-project-technology.js && node packages/cli/validate-provider-archive.js && node packages/cli/validate-local-target.js",
18
18
  "test:context-traversal": "node packages/cli/validate-context-traversal.js",
19
19
  "test:project-continuity": "node packages/cli/validate-project-continuity.js",
20
20
  "test:artifact-releases": "node packages/cli/validate-artifact-releases.js",
@@ -131,7 +131,14 @@ function validateManifest(manifest, repo = null) {
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
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
+ if (technology.target_source !== undefined) {
135
+ const selected = technology.target_source;
136
+ if (!selected || typeof selected !== "object" || Array.isArray(selected) || selected.kind !== "local" ||
137
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{1,255}$/.test(selected.target_id || "") ||
138
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{1,255}$/.test(selected.acceptance_ref || "") ||
139
+ Object.keys(selected).some(key => !["kind", "target_id", "acceptance_ref"].includes(key))) errors.push("extensions.mirai.project_technology.target_source is invalid");
140
+ }
141
+ for (const field of Object.keys(technology)) if (!(field in expected) && !["enabled", "continuity_policy", "target_source"].includes(field)) errors.push(`extensions.mirai.project_technology has unknown field ${field}`);
135
142
  }
136
143
  }
137
144
  if (manifest.graph !== null) {
@@ -14,6 +14,7 @@ function usage() {
14
14
  console.error(" --semantic-digest <sha256:...> --provider-revision <sha>");
15
15
  console.error(" --refresh-binding --significant-work --max-objects <count>");
16
16
  console.error("Archive connect: --provider-archive-trust <verified-release-anchor.json>");
17
+ console.error("Local target: sync --target-source <selector.json> --local-acceptance-trust <verified-owner-anchor.json> --expected-graph-digest <sha256:...> [--apply]");
17
18
  console.error("Continuity: sync --boundary task_start|stage_complete|task_complete [--evidence <json>] [--expected-graph-digest <sha256:...>]");
18
19
  console.error(" verify --significant-work [--receipt-digest <sha256:...>]");
19
20
  console.error("Context: --phase discover --task <text>");
@@ -48,7 +49,11 @@ function parse(argv) {
48
49
  const values = new Set(["--task", "--source", "--target-id", "--semantic-digest", "--provider-revision", "--max-objects", "--phase", "--input", "--select", "--selection", "--packet", "--evidence", "--selector", "--reason", "--confidence", "--context-budget", "--boundary", "--expected-graph-digest", "--receipt-digest", "--state-root", "--matter-id", "--direction", "--artifact-root", "--release-date", "--release-id", "--parent-release", "--base-release", "--target-release", "--client-note", "--technology", "--scenario", "--audience", "--course-pack", "--projection"]);
49
50
  const flags = new Set(["--apply", "--refresh-binding", "--significant-work", "--create-export"]);
50
51
  values.add("--provider-archive-trust");
52
+ values.add("--local-acceptance-trust");
53
+ values.add("--target-source");
51
54
  const names = {
55
+ "--local-acceptance-trust": "localAcceptanceTrustFile",
56
+ "--target-source": "targetSourceFile",
52
57
  "--provider-archive-trust": "providerArchiveTrustFile",
53
58
  "--task": "task", "--source": "source", "--target-id": "targetId",
54
59
  "--semantic-digest": "semanticDigest", "--provider-revision": "providerRevision",
@@ -92,6 +97,12 @@ function readJsonFile(file) {
92
97
  }
93
98
 
94
99
  function hydrateContextOptions(request) {
100
+ if (request.options.localAcceptanceTrustFile === "-" && request.options.providerArchiveTrustFile === "-") throw new Error("only one trust input may consume stdin");
101
+ if (request.options.localAcceptanceTrustFile) {
102
+ request.options.localAcceptance = request.options.localAcceptanceTrustFile === "-"
103
+ ? JSON.parse(fs.readFileSync(0, "utf8")) : readJsonFile(request.options.localAcceptanceTrustFile);
104
+ }
105
+ if (request.options.targetSourceFile) request.options.targetSource = readJsonFile(request.options.targetSourceFile);
95
106
  if (request.options.providerArchiveTrustFile) {
96
107
  if (request.operation !== "connect") throw new Error("archive trust is only supported by connect");
97
108
  request.options.providerArchive = request.options.providerArchiveTrustFile === "-"
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const assert = require("assert");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+ const t = require("../project-technology");
9
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "mirai-local-target-"));
10
+ const repo = path.join(root, "ordinary folder");
11
+ const stateRoot = path.join(root, "host-a");
12
+ const selection = {kind:"local", target_id:"target.example.delivery", acceptance_ref:"decision.example.accepted"};
13
+ const write = (ref,value) => {const file=path.join(repo,ref);fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,t.canonicalBytes(value));};
14
+ const read = ref => JSON.parse(fs.readFileSync(path.join(repo,ref)));
15
+ const objRef = id => `graph/specs/objects/${id}.json`;
16
+ const object = id => ({id,kind:"constraint",title:id,summary:"Bounded example",profile:"implementation_control",lifecycle:"accepted",readiness:"accepted",evidence:[]});
17
+ const snapshot = dir => {const out={}; function walk(at){for(const name of fs.readdirSync(at).sort()){const f=path.join(at,name);if(fs.statSync(f).isDirectory())walk(f);else out[path.relative(dir,f)]=t.sha256(fs.readFileSync(f));}} walk(dir);return out;};
18
+ const checks=[];
19
+ function check(id,fn){fn();checks.push({id,passed:true});}
20
+ let trust;
21
+ const options = () => ({stateRoot,localAcceptance:trust});
22
+ const binding = () => t.status(repo,options()).target_binding;
23
+ function mutate(ref,change,test){const before=fs.readFileSync(path.join(repo,ref)); const v=read(ref);change(v);write(ref,v);try{test();}finally{fs.writeFileSync(path.join(repo,ref),before);}}
24
+ try {
25
+ write("graph.json",{$schema:"https://mirai-graph.dev/schemas/graph-manifest.schema.json",format:"mirai-graph",schema_version:"2.0.0",id:"project.example",aliases:[],title:"Ordinary folder",scope:"repository",kind:"project_graph",owner:"owner.example",profiles:["project_management","implementation_control"],imports:[],graph:{root:"graph",source_of_truth:["graph/specs"],objects:["graph/specs/index.json"],relations:["graph/specs/relations.json"],schemas:[],generated:["graph/generated"],raw_sources:["data"]},extensions:{"mirai.project_technology":{contract_version:"1.0.0",enabled:true,context_policy:"task_scoped",source_boundary:"hybrid_sot"}}});
26
+ const contract={decision_refs:[selection.acceptance_ref],goal_binding:{goal_id:"goal.example",done_when_ids:["done.example"]},requirement_bindings:[{requirement_id:"requirement.example",acceptance_ids:["acceptance.example"],done_when_ids:["done.example"]}],constraint_ids:["constraint.example"],non_goal_ids:["non_goal.example"],deferred_boundary_ids:["deferred.example"],allowed_change_scope:[{repository_id:"project.example",owner_id:"owner.example",package_id:"package.example",file_patterns:["data/tools/**"],actions:["modify","test"]}],architecture_contract:{contract_ref:"contract.example",acceptance_ref:selection.acceptance_ref,architecture_owner_id:"human.example",lifecycle:"accepted",owner_ids:["owner.example"],component_ids:["component.example"],package_ids:["package.example"],capability_ids:["capability.example"],ownership_boundaries:[{subject_ref:"component.example",data_owner:"owner.example",access_owner:"owner.example",lifecycle_owner:"owner.example",interface_owner:"owner.example",runtime_owner:"owner.example"}],required_relations:[],allowed_relations:[],forbidden_relations:[],required_dependencies:[],forbidden_dependencies:[]}};
27
+ const ids=[selection.target_id,selection.acceptance_ref,"goal.example","done.example","requirement.example","acceptance.example","constraint.example","non_goal.example","deferred.example","contract.example"];
28
+ for(const id of ids)write(objRef(id),object(id));
29
+ write("graph/specs/index.json",{object_files:ids.map(id=>`objects/${id}.json`)});
30
+ write("graph/specs/relations.json",[]);
31
+ fs.mkdirSync(path.join(repo,"data/tools"),{recursive:true});fs.writeFileSync(path.join(repo,"data/tools/method.txt"),"A bounded public method.\n");
32
+ const target={...object(selection.target_id),kind:"contract",tz_role:"contract",provider_execution_contract:contract,source_refs:["data/tools/method.txt"]};
33
+ write(objRef(selection.target_id),target);
34
+ const candidate=t.plan(repo,{stateRoot,targetSource:selection}).target_binding;
35
+ assert(candidate.semantic_digest);
36
+ target.semantic_digest=candidate.semantic_digest;target.content_revision=candidate.content_revision;write(objRef(selection.target_id),target);
37
+ const decision={...object(selection.acceptance_ref),kind:"decision",subtype:"architecture_baseline_acceptance",graph_id:"project.example",target_id:selection.target_id,owner_id:"human.example",semantic_digest:candidate.semantic_digest,execution_contract_digest:candidate.execution_contract_digest,approval_ref:"approval.example.owner"};
38
+ write(objRef(selection.acceptance_ref),decision);
39
+ // Synthetic authority fixture: independent trusted input is held outside
40
+ // the project and frozen before tamper tests. Production must verify issuer.
41
+ trust={graphId:"project.example",targetId:selection.target_id,ownerId:"human.example",decisionSha256:t.sha256(t.canonicalBytes(decision))};
42
+ check("selection_preview_zero_write",()=>{const before=snapshot(root);const r=t.execute("sync",repo,{...options(),targetSource:selection});assert.equal(r.status,"preview");assert.equal(r.target_binding.status,"ready");assert.deepEqual(snapshot(root),before);});
43
+ check("selection_requires_cas",()=>assert(t.execute("sync",repo,{...options(),apply:true,targetSource:selection}).blockers.includes("local_target_compare_and_swap_conflict")));
44
+ check("ordinary_folder_selects_without_git",()=>{const r=t.execute("sync",repo,{...options(),apply:true,targetSource:selection,expectedGraphDigest:t.continuity.graphDigest(repo)});assert.equal(r.status,"success",JSON.stringify(r));assert.equal(r.changed,true);assert(!fs.existsSync(path.join(repo,".git")));});
45
+ check("ready_exact_local_identity",()=>{assert.equal(binding().status,"ready",JSON.stringify(binding()));assert.equal(binding().source_kind,"local");assert.equal(binding().provider_revision,undefined);});
46
+ check("status_plan_context_verify_agree_and_read_only",()=>{const before=snapshot(root);for(const op of ["status","plan","context","verify"]){const r=t.execute(op,repo,{...options(),significantWork:true,task:"Bounded example"});assert.equal(r.target_binding.status,"ready",JSON.stringify(r));}assert.deepEqual(snapshot(root),before);});
47
+ check("asserted_accepted_is_not_trust",()=>assert(t.status(repo,{stateRoot}).target_binding.blockers.includes("local_target_acceptance_unverified")));
48
+ check("two_hosts_same_target_identity",()=>assert.deepEqual(t.status(repo,{...options(),stateRoot:path.join(root,"host-b")}).target_binding,binding()));
49
+ check("repeat_selection_byte_identical",()=>{const before=snapshot(root);const r=t.execute("sync",repo,{...options(),apply:true,targetSource:selection,expectedGraphDigest:t.continuity.graphDigest(repo)});assert.equal(r.changed,false);assert.deepEqual(snapshot(root),before);});
50
+ for(const lifecycle of ["draft","reviewed","stale","deprecated","blocked"]){check(`reject_${lifecycle}`,()=>mutate(objRef(selection.target_id),v=>v.lifecycle=lifecycle,()=>assert.equal(binding().status,"blocked")));}
51
+ check("unapproved_semantic_change",()=>mutate(objRef("requirement.example"),v=>v.summary="Different meaning",()=>assert(binding().blockers.includes("local_target_semantic_digest_mismatch"))));
52
+ check("rehashed_decision_is_not_trusted",()=>mutate(objRef(selection.acceptance_ref),v=>v.approval_ref="approval.other",()=>assert(binding().blockers.includes("local_target_acceptance_unverified"))));
53
+ check("foreign_owner",()=>mutate(objRef(selection.acceptance_ref),v=>v.owner_id="human.other",()=>assert(binding().blockers.includes("local_target_acceptance_identity_mismatch"))));
54
+ for(const key of ["requirement_bindings","allowed_change_scope","non_goal_ids","constraint_ids","deferred_boundary_ids"]){check(`empty_${key}`,()=>mutate(objRef(selection.target_id),v=>v.provider_execution_contract[key]=[],()=>assert.equal(binding().status,"blocked")));}
55
+ check("scope_owner_unapproved",()=>mutate(objRef(selection.target_id),v=>v.provider_execution_contract.allowed_change_scope[0].owner_id="owner.other",()=>assert(binding().blockers.includes("provider_contract_scope_owner_unapproved"))));
56
+ check("duplicate_target",()=>mutate("graph/specs/index.json",v=>{write("graph/specs/duplicate.json",target);v.object_files.push("duplicate.json");},()=>assert(binding().blockers.includes("duplicate_object_id"))));
57
+ fs.unlinkSync(path.join(repo,"graph/specs/duplicate.json"));
58
+ check("required_cycle",()=>mutate("graph/specs/relations.json",v=>v.push({id:"relation.cycle",type:"requires",source:selection.target_id,target:selection.target_id,lifecycle:"accepted"}),()=>assert(binding().blockers.includes("local_target_required_cycle"))));
59
+ check("private_path_rejected",()=>mutate(objRef(selection.target_id),v=>v.source_refs=["source/private/method.txt"],()=>assert(binding().blockers.includes("local_target_source_unsafe_or_missing"))));
60
+ check("parent_traversal_rejected",()=>mutate(objRef(selection.target_id),v=>v.source_refs=["../outside.txt"],()=>assert(binding().blockers.includes("local_target_source_unsafe_or_missing"))));
61
+ check("malformed_source_refs_fail_closed",()=>mutate(objRef(selection.target_id),v=>v.source_refs={ref:"data/tools/method.txt"},()=>assert(binding().blockers.includes("local_target_source_refs_invalid"))));
62
+ check("tool_drift_not_semantic_acceptance",()=>{const file=path.join(repo,"data/tools/method.txt");const before=fs.readFileSync(file);fs.writeFileSync(file,"Changed tool\n");try{const b=binding();assert(b.blockers.includes("local_target_content_stale"));assert(!b.blockers.includes("local_target_semantic_digest_mismatch"));}finally{fs.writeFileSync(file,before);}});
63
+ check("external_binding_never_falls_back",()=>{const file=path.join(t.continuity.stateRoot(repo,read("graph.json"),options()),"target-provider-binding.json");fs.writeFileSync(file,"{}");try{assert(binding().blockers.includes("local_external_target_conflict"));}finally{fs.unlinkSync(file);}});
64
+ check("disabled_blocks_significant_verify",()=>mutate("graph.json",v=>v.extensions["mirai.project_technology"].enabled=false,()=>assert(t.verify(repo,{...options(),significantWork:true}).blockers.includes("project_technology_disabled"))));
65
+ check("shared_lease_conflict_preserves_bytes",()=>{const file=path.join(repo,"graph/.project-technology-continuity.lock");fs.writeFileSync(file,"occupied");const before=snapshot(root);try{assert(t.execute("sync",repo,{...options(),apply:true,targetSource:selection,expectedGraphDigest:t.continuity.graphDigest(repo)}).blockers.includes("continuity_lease_conflict"));assert.deepEqual(snapshot(root),before);}finally{fs.unlinkSync(file);}});
66
+ check("offline_stale_cas_blocks",()=>assert(t.execute("sync",repo,{...options(),apply:true,targetSource:selection,expectedGraphDigest:"sha256:"+"0".repeat(64)}).blockers.includes("local_target_compare_and_swap_conflict")));
67
+ check("selection_failure_rolls_back_project",()=>{
68
+ const manifest=read("graph.json");const unselected=structuredClone(manifest);delete unselected.extensions["mirai.project_technology"].target_source;write("graph.json",unselected);
69
+ const before=snapshot(repo);const rename=fs.renameSync;let injected=false;
70
+ fs.renameSync=(from,to)=>{const result=rename(from,to);if(!injected&&String(to)===path.join(repo,"graph.json")){injected=true;throw new Error("fixture failure after manifest rename");}return result;};
71
+ try{const r=t.execute("sync",repo,{...options(),apply:true,targetSource:selection,expectedGraphDigest:t.continuity.graphDigest(repo)});assert(r.blockers.includes("local_target_rollback_applied"),JSON.stringify(r));assert.deepEqual(snapshot(repo),before);}finally{fs.renameSync=rename;write("graph.json",manifest);}
72
+ });
73
+ check("cli_local_trust_and_plan",()=>{
74
+ const file=path.join(root,"owner-trust.json");fs.writeFileSync(file,JSON.stringify(trust));
75
+ const r=spawnSync(process.execPath,[path.join(__dirname,"project-technology.js"),"plan",repo,"--local-acceptance-trust",file,"--state-root",stateRoot],{encoding:"utf8"});
76
+ assert.equal(r.status,0,r.stderr);assert.equal(JSON.parse(r.stdout).target_binding.status,"ready");
77
+ });
78
+ check("missing_verification_blocks_execution_not_diagnostics",()=>{
79
+ assert(t.verify(repo,{...options(),significantWork:true}).blockers.includes("local_target_verification_missing"));
80
+ assert(t.context(repo,{...options(),significantWork:true,task:"Bounded example"}).blockers.includes("local_target_verification_missing"));
81
+ assert.equal(t.plan(repo,options()).target_binding.status,"ready");
82
+ });
83
+ const evidence={task_digest:t.sha256("verified local fixture",true),outcome:"Verified bounded local method",requirement_refs:["requirement.example"],evidence_refs:["data/tools/method.txt"],checks:[{id:"check.example",verdict:"pass",evidence_ref:"data/tools/method.txt"}],changed_surfaces:["data/tools/method.txt"]};
84
+ check("verified_boundary_unlocks_significant_verify",()=>{
85
+ const r=t.execute("sync",repo,{...options(),apply:true,boundary:"stage_complete",continuityEvidence:evidence,expectedGraphDigest:t.continuity.graphDigest(repo)});
86
+ assert.equal(r.status,"success",JSON.stringify(r));
87
+ const v=t.verify(repo,{...options(),significantWork:true});assert.equal(v.status,"success",JSON.stringify(v));
88
+ });
89
+ check("repointing_content_pin_does_not_refresh_verification",()=>{
90
+ const file=path.join(repo,"data/tools/method.txt");const before=fs.readFileSync(file);fs.writeFileSync(file,"A newly checked method needs fresh evidence.\n");
91
+ try{mutate(objRef(selection.target_id),v=>v.content_revision=binding().content_revision,()=>{
92
+ assert.equal(binding().status,"ready");
93
+ assert(t.verify(repo,{...options(),significantWork:true}).blockers.includes("local_target_verification_stale"));
94
+ });}finally{fs.writeFileSync(file,before);}
95
+ });
96
+ check("two_hosts_same_context_discovery_digest",()=>{
97
+ const a=t.context(repo,{...options(),task:"Bounded example"});const b=t.context(repo,{...options(),stateRoot:path.join(root,"host-b"),task:"Bounded example"});
98
+ assert.deepEqual(a.traversal_receipt,b.traversal_receipt);
99
+ assert(t.verify(repo,{...options(),stateRoot:path.join(root,"host-b"),significantWork:true}).blockers.includes("local_target_verification_missing"));
100
+ });
101
+ // Optional synthetic fixture for consumer integration tests; never real data.
102
+ if(process.env.MIRAI_LOCAL_TARGET_FIXTURE_EXPORT){
103
+ const output=path.resolve(process.env.MIRAI_LOCAL_TARGET_FIXTURE_EXPORT);
104
+ fs.mkdirSync(output,{recursive:true});
105
+ for(const name of ["project","state","owner-trust.json"])assert(!fs.existsSync(path.join(output,name)),"fixture destination must be empty");
106
+ fs.cpSync(repo,path.join(output,"project"),{recursive:true});fs.cpSync(stateRoot,path.join(output,"state"),{recursive:true});
107
+ fs.writeFileSync(path.join(output,"owner-trust.json"),t.canonicalBytes(trust));
108
+ }
109
+ check("local_disconnect_transaction_and_repeat",()=>{
110
+ const r=t.execute("disconnect",repo,{...options(),apply:true,expectedGraphDigest:t.continuity.graphDigest(repo)});
111
+ assert.equal(r.status,"success",JSON.stringify(r));assert.equal(r.changed,true);assert.equal(binding().status,"not_configured");assert(r.rollback_ref);
112
+ const before=snapshot(root);assert.equal(t.execute("disconnect",repo,{...options(),apply:true}).changed,false);assert.deepEqual(snapshot(root),before);
113
+ assert.equal(t.execute("sync",repo,{...options(),apply:true,targetSource:selection,expectedGraphDigest:t.continuity.graphDigest(repo)}).status,"success");
114
+ });
115
+ const unborn=path.join(root,"new Git project");fs.cpSync(repo,unborn,{recursive:true});
116
+ assert.equal(spawnSync("git",["init","-q",unborn]).status,0);
117
+ check("unborn_inventory_declared_sources_and_zero_write",()=>{
118
+ const before=snapshot(unborn);const inv=t.inventory(unborn);
119
+ assert.deepEqual(inv.blockers||[],[]);assert.equal(inv.revision,null);
120
+ assert(inv.files.some(f=>f.path==="data/tools/method.txt"));assert.deepEqual(snapshot(unborn),before);
121
+ });
122
+ check("unborn_sync_and_repeat",()=>{
123
+ assert.equal(t.execute("sync",unborn,{...options(),apply:true}).status,"success");
124
+ const before=snapshot(unborn);assert.equal(t.execute("sync",unborn,{...options(),apply:true}).changed,false);assert.deepEqual(snapshot(unborn),before);
125
+ });
126
+ const headFile=path.join(unborn,".git/HEAD");const headBytes=fs.readFileSync(headFile);
127
+ check("invalid_detached_head_not_unborn",()=>{
128
+ fs.writeFileSync(headFile,"f".repeat(40)+"\n");
129
+ try{assert(t.inventory(unborn).blockers.includes("inventory_git_unavailable"));}finally{fs.writeFileSync(headFile,headBytes);}
130
+ });
131
+ check("corrupt_symbolic_branch_not_unborn",()=>{
132
+ const ref=headBytes.toString().trim().replace(/^ref: /,"");const file=path.join(unborn,".git",ref);fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,"not-a-commit\n");
133
+ try{assert(t.inventory(unborn).blockers.includes("inventory_git_unavailable"));}finally{fs.unlinkSync(file);}
134
+ });
135
+ check("corrupt_index_not_unborn",()=>{
136
+ const file=path.join(unborn,".git/index");fs.writeFileSync(file,"broken index");
137
+ try{assert(t.inventory(unborn).blockers.includes("inventory_git_unavailable"));}finally{fs.unlinkSync(file);}
138
+ });
139
+ check("unavailable_git_not_unborn",()=>{
140
+ const prior=process.env.PATH;process.env.PATH=path.join(root,"no-tools");
141
+ try{assert(t.inventory(unborn).blockers.includes("inventory_git_unavailable"));}finally{process.env.PATH=prior;}
142
+ });
143
+ check("unborn_provider_does_not_invent_revision",()=>{
144
+ const r=t.execute("provide",unborn,{...options(),apply:true,targetId:selection.target_id,semanticDigest:target.semantic_digest,providerRevision:"a".repeat(40)});
145
+ assert.notEqual(r.status,"success");
146
+ });
147
+ check("git_equivalent_identity",()=>{const before=binding();for(const args of [["init","-q"],["config","user.name","Fixture"],["config","user.email","fixture@example.invalid"],["add","."],["commit","-qm","accepted fixture"]])assert.equal(spawnSync("git",args,{cwd:repo}).status,0);assert.deepEqual(binding(),before);});
148
+ console.log(JSON.stringify({status:"success",checks_passed:checks.length,checks},null,2));
149
+ } finally {fs.rmSync(root,{recursive:true,force:true});}
@@ -406,6 +406,8 @@ module.exports = {
406
406
  digest,
407
407
  graphDigest,
408
408
  latestReceipt,
409
+ lock,
410
+ releaseLock,
409
411
  normalizeEvidence,
410
412
  stateRoot,
411
413
  status,
@@ -456,9 +456,9 @@ function inventory(repo) {
456
456
  const entries = [];
457
457
  const blockers = [];
458
458
  let files = trackedFiles(repo);
459
- const revision = git(repo, "rev-parse", "HEAD") || null;
459
+ const revision = git(repo, "rev-parse", "--verify", "HEAD^{commit}") || null;
460
460
  if (!revision) {
461
- if (fs.existsSync(path.join(repo, ".git"))) blockers.push("inventory_git_unavailable");
461
+ if (fs.existsSync(path.join(repo, ".git")) && !unbornGitBranch(repo)) blockers.push("inventory_git_unavailable");
462
462
  else {
463
463
  // Ordinary folders and immutable distributions have no Git index. Use
464
464
  // only explicitly declared graph/raw sources, never scan arbitrary data.
@@ -506,7 +506,20 @@ function inventory(repo) {
506
506
  return payload;
507
507
  }
508
508
 
509
- 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 = {}) {
510
523
  if (options.phase === "expand") return traversal.expandContext(repoArg, options.traversalReceipt, options.selectedIds, options);
511
524
  if (options.phase === "compile") return traversal.compileContext(repoArg, options.traversalReceipt, options.selection, options);
512
525
  if (options.phase === "verify") return traversal.verifyContext(repoArg, options.contextPack, options.usageEvidence, options);
@@ -529,10 +542,147 @@ function context(repoArg, options = {}) {
529
542
  };
530
543
  }
531
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
+
532
672
  function targetBindingStatus(repo, options = {}) {
533
673
  const manifest = readManifest(repo).manifest;
534
674
  const root = runtimeRoot(repo, manifest, options);
535
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
+ }
536
686
  if (!fs.existsSync(bindingPath)) return { status: "not_configured", enabled: false, blockers: [], next_action: "connect an exact target provider binding" };
537
687
  let binding;
538
688
  try { binding = readJson(bindingPath); } catch (_) { return { status: "blocked", enabled: true, blockers: ["target_binding_invalid"] }; }
@@ -586,7 +736,7 @@ function status(repoArg, options = {}) {
586
736
  technology_freshness: freshness,
587
737
  technology_digest: current.inventory_digest,
588
738
  target_binding: binding,
589
- continuity: continuity.status(repo, manifestState.manifest, {}),
739
+ continuity: continuity.status(repo, manifestState.manifest, options),
590
740
  blockers: [...new Set(blockers)].sort(),
591
741
  next_action: ext.legacy ? "run enable --apply to migrate the legacy contract" : blockers.length ? "run plan, then the required transactional operation" : "none",
592
742
  });
@@ -599,10 +749,11 @@ function explain(repoArg) {
599
749
  });
600
750
  }
601
751
 
602
- function plan(repoArg) {
603
- const state = status(repoArg);
752
+ function plan(repoArg, options = {}) {
753
+ const state = status(repoArg, options);
604
754
  return result("plan", "read_only", "success", {
605
755
  current_status: state.status,
756
+ target_binding: state.target_binding,
606
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"],
607
758
  blockers: state.blockers.filter((item) => !["project_technology_not_configured", "project_technology_migration_required", "project_technology_inventory_missing", "project_technology_inventory_stale"].includes(item)),
608
759
  next_action: "rerun the selected operation with --apply",
@@ -611,6 +762,8 @@ function plan(repoArg) {
611
762
 
612
763
  function preview(operation, repoArg, options = {}) {
613
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" });
614
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` });
615
768
  }
616
769
 
@@ -624,7 +777,7 @@ function enable(repoArg, options = {}) {
624
777
  const extensions = { ...(manifest.extensions || {}) };
625
778
  const legacy = extensions[LEGACY_EXTENSION_KEY];
626
779
  delete extensions[LEGACY_EXTENSION_KEY];
627
- extensions[EXTENSION_KEY] = extensionContract();
780
+ extensions[EXTENSION_KEY] = { ...extensionContract(), ...(extensions[EXTENSION_KEY]?.target_source ? { target_source: extensions[EXTENSION_KEY].target_source } : {}) };
628
781
  manifest.extensions = extensions;
629
782
  const root = runtimeRoot(repo, manifest, options);
630
783
  const runtimeMigration = migrateProjectLocalRuntime(repo, manifest, root);
@@ -655,12 +808,15 @@ function enable(repoArg, options = {}) {
655
808
 
656
809
  function sync(repoArg, options = {}) {
657
810
  const repo = normalizeRepo(repoArg);
811
+ if (options.targetSource) return syncLocalSelection(repo, options);
658
812
  const manifestState = readManifest(repo);
659
813
  const ext = extensionState(manifestState.manifest);
660
814
  const blockers = [...manifestState.blockers, ...ext.blockers, ...(inventory(repo).blockers || [])];
661
815
  const hostRoot = runtimeRoot(repo, manifestState.manifest, options);
662
816
  if (legacyRuntimeState(repo).present && !fs.existsSync(path.join(hostRoot, INVENTORY_FILE))) blockers.push("project_technology_host_state_migration_required");
663
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);
664
820
  if (blockers.length) return result("sync", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "enable or repair Project Technology" });
665
821
  let continuityResult = null;
666
822
  if (options.boundary) {
@@ -688,6 +844,56 @@ function sync(repoArg, options = {}) {
688
844
  });
689
845
  }
690
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
+
691
897
  function provide(repoArg, options = {}) {
692
898
  const repo = normalizeRepo(repoArg);
693
899
  const identity = bindingValues(options);
@@ -748,6 +954,7 @@ function connect(repoArg, options = {}) {
748
954
  if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
749
955
  for (const key of Object.keys(identity.values)) if (read.export[key] !== identity.values[key]) blockers.push(`${key}_mismatch`);
750
956
  const archiveRequested = Object.hasOwn(options, "providerArchive");
957
+ if (manifestState.manifest?.extensions?.[EXTENSION_KEY]?.target_source) blockers.push("local_external_target_conflict");
751
958
  const archive = archiveRequested ? archiveProviderIdentity(source, read.export, options.providerArchive) : null;
752
959
  const providerRoot = archiveRequested ? null : providerRootFor(source);
753
960
  if (archive) blockers.push(...archive.blockers);
@@ -809,6 +1016,7 @@ function connect(repoArg, options = {}) {
809
1016
 
810
1017
  function disconnect(repoArg, options = {}) {
811
1018
  const repo = normalizeRepo(repoArg);
1019
+ if (readManifest(repo).manifest?.extensions?.[EXTENSION_KEY]?.target_source) return syncLocalSelection(repo, options, true);
812
1020
  const root = runtimeRoot(repo, readManifest(repo).manifest, options);
813
1021
  const bindingPath = path.join(root, BINDING_FILE);
814
1022
  if (!fs.existsSync(bindingPath)) return result("disconnect", "transactional", "success");
@@ -883,6 +1091,7 @@ function verify(repoArg, options = {}) {
883
1091
  const continuityResult = continuity.verify(repo, manifest, options);
884
1092
  if (continuity.continuityPolicy(manifest) === "task_boundary" || options.receiptDigest || options.significantWork) blockers.push(...continuityResult.blockers);
885
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));
886
1095
  return result("verify", "read_only", blockers.length ? "blocked" : "success", {
887
1096
  repository_id: state.repository_id,
888
1097
  enabled: state.enabled,
@@ -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,8 @@ 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.
18
20
  - [v1.5.0](1.5.0.md) - compatible 1.x authenticated archive providers and
19
21
  declared-source inventory without Git (`legacy-1`, not npm `latest`).
20
22
  - [v1.2.0](1.2.0.md) - portable task-boundary project continuity in Project
@@ -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
  }
@@ -1,5 +1,74 @@
1
1
  # Project Technology
2
2
 
3
+ ## Local accepted targets (candidate extension)
4
+
5
+ Local targets use the same execution-contract normalizer as external providers.
6
+ They do not need Git or a self-connected provider. The optional manifest
7
+ `extensions.mirai.project_technology.target_source` contains exactly
8
+ `{kind:"local", target_id, acceptance_ref}`. It selects existing `graph/specs`
9
+ objects and never approves them. No new profile or manifest version is required.
10
+
11
+ The selected target has the existing `provider_execution_contract` shape. Every
12
+ required object, goal/Done When, requirement/acceptance, constraint, non-goal,
13
+ deferred boundary and architecture reference must resolve to accepted canonical
14
+ specs. Required relations close transitively; cycles and conflicts block work.
15
+ The acceptance is a `decision` object with subtype
16
+ `architecture_baseline_acceptance`, accepted lifecycle/readiness, `graph_id`,
17
+ `target_id`, `owner_id`, `semantic_digest`, `execution_contract_digest` and a
18
+ bounded `approval_ref`. It must match the declared architecture owner.
19
+
20
+ The caller supplies `localAcceptance` / `--local-acceptance-trust <file>` (or `-`
21
+ for stdin): `{graphId,targetId,ownerId,decisionSha256}`. This is **trusted caller
22
+ input** obtained from the existing human-approval authority channel. The engine
23
+ checks consistency, not the human's identity. Calculating the hash from an
24
+ untrusted folder is not authentication. The anchor is never obtained from graph
25
+ contents or auto-created/persisted by sync. A new host without authenticated
26
+ approval gets `local_target_acceptance_unverified`; read-only diagnosis remains
27
+ available. A host-local cache is not portable acceptance authority.
28
+
29
+ `plan` returns the same `target_binding` as `status`; it exposes calculated
30
+ digests for review even when acceptance is blocked. The local `semantic_digest`
31
+ hashes canonical JSON of graph ID, target ID, selected objects and relations.
32
+ Object keys are sorted; objects and relations are sorted by ID. Timestamps,
33
+ evidence, approval refs and computed digest fields are excluded. The acceptance
34
+ object is excluded to avoid self-reference. Source refs retain their paths, not
35
+ content hashes. Normalized meaning, required closure and scope stay bound.
36
+ `execution_contract_digest` hashes the shared normalized contract. Source/evidence
37
+ bytes are separately hashed into `content_revision`; the target pins that value.
38
+ Tool drift blocks freshness without re-accepting architecture. JSON formatting
39
+ does not change semantics; changed source bytes (including line endings) change
40
+ content identity intentionally. Paths, clocks and Git HEAD are not local IDs.
41
+ `provider_revision` remains exclusive to external Git/archive bindings.
42
+
43
+ Selection is `sync --target-source <selector.json> --local-acceptance-trust <anchor.json>
44
+ --expected-graph-digest <digest> --apply`; omit `--apply` for zero-write preview.
45
+ Use `continuity.graphDigest(repo)` for the CAS value. Selection uses the existing
46
+ continuity lease, backup, atomic rename and readback. Repetition is byte-identical.
47
+ Select before a separate task-boundary sync; these are not a combined transaction.
48
+ Significant `context` and `verify` additionally require a current receipt from the
49
+ existing continuity mechanism, even when the optional policy was omitted.
50
+ Updating a content pin is not a verification receipt. Record actual checks through
51
+ `sync --boundary stage_complete --evidence <checks.json> --apply` after validating
52
+ the sources. A second host must verify those sources and record its own fresh
53
+ receipt; it does not have to re-approve unchanged architecture.
54
+ `disconnect --expected-graph-digest <digest> --apply` removes only the local
55
+ selector through the same lease/CAS/backup transaction. It does not remove the
56
+ target or its Decision. Without `--apply` it remains a preview.
57
+ Git hygiene remains strict: a changed tracked manifest must be committed through
58
+ the caller's authorized Git workflow before execution becomes ready.
59
+ Before the first Git commit, a structurally valid unborn branch may inventory
60
+ declared graph/raw sources using the same content-bound path as an ordinary
61
+ folder. Missing Git, corrupt refs/index and invalid detached HEAD stay blocked.
62
+ This never supplies `provider_revision` or permits a Git provider export.
63
+
64
+ Local and external bindings conflict rather than override each other. Missing
65
+ external providers never cause fallback to local data. Local raw sources must be
66
+ safe relative files, not `source/`, generated outputs, symlinks or external URLs;
67
+ external technologies continue to use verified providers. Raw file contents are
68
+ not included in the target packet. Existing folder safety and action authorization
69
+ still apply: ready is not permission to write. Shared-server leases do not lock
70
+ offline sync replicas; divergent expected state requires reconciliation.
71
+
3
72
  Status: 1.4 stable standard; activation contract remains 1.0.0
4
73
 
5
74
  Project Technology is the shared executable mechanism of Mirai Graph. It is