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.
@@ -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,42 @@ 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
+
27
+ ## [1.5.0] - 2026-09-03
28
+
29
+ Compatible 1.x maintenance line, published under `legacy-1`; the npm `latest`
30
+ channel and the separate 2.x development line are unchanged.
31
+
32
+ - Project Technology can connect and refresh a bounded provider export without
33
+ Git using explicit authenticated-release trust supplied by the consumer.
34
+ Exact export bytes, graph identity, revision and forward ancestry are checked;
35
+ target/architecture/permission gates are unchanged. No automatic trust discovery.
36
+ - Read-only source export verification proves the archive anchor against the
37
+ accepted target and Git revision before a distributor seals it in release
38
+ metadata. CLI trust can be supplied on stdin; no temporary trust file is needed.
39
+ - No-Git inventory covers explicitly declared graph and raw-source paths, detects
40
+ stale content and blocks unsafe or missing sources before sync writes. Existing
41
+ Git inventories remain revision-bound; this does not invent source history.
42
+
7
43
  ## [1.4.0] - 2026-08-29
8
44
 
9
45
  ### Added
package/CITATION.cff CHANGED
@@ -17,5 +17,5 @@ keywords:
17
17
  - complex systems
18
18
  - context engineering
19
19
  - systems engineering
20
- version: "1.2.0"
21
- date-released: "2026-08-27"
20
+ version: "1.6.0"
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.4.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",
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) {
@@ -13,6 +13,8 @@ function usage() {
13
13
  console.error("Options: --apply --task <text> --source <file> --target-id <id>");
14
14
  console.error(" --semantic-digest <sha256:...> --provider-revision <sha>");
15
15
  console.error(" --refresh-binding --significant-work --max-objects <count>");
16
+ console.error("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]");
16
18
  console.error("Continuity: sync --boundary task_start|stage_complete|task_complete [--evidence <json>] [--expected-graph-digest <sha256:...>]");
17
19
  console.error(" verify --significant-work [--receipt-digest <sha256:...>]");
18
20
  console.error("Context: --phase discover --task <text>");
@@ -46,7 +48,13 @@ function parse(argv) {
46
48
  const options = {};
47
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"]);
48
50
  const flags = new Set(["--apply", "--refresh-binding", "--significant-work", "--create-export"]);
51
+ values.add("--provider-archive-trust");
52
+ values.add("--local-acceptance-trust");
53
+ values.add("--target-source");
49
54
  const names = {
55
+ "--local-acceptance-trust": "localAcceptanceTrustFile",
56
+ "--target-source": "targetSourceFile",
57
+ "--provider-archive-trust": "providerArchiveTrustFile",
50
58
  "--task": "task", "--source": "source", "--target-id": "targetId",
51
59
  "--semantic-digest": "semanticDigest", "--provider-revision": "providerRevision",
52
60
  "--max-objects": "maxObjects", "--apply": "apply",
@@ -89,6 +97,17 @@ function readJsonFile(file) {
89
97
  }
90
98
 
91
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);
106
+ if (request.options.providerArchiveTrustFile) {
107
+ if (request.operation !== "connect") throw new Error("archive trust is only supported by connect");
108
+ request.options.providerArchive = request.options.providerArchiveTrustFile === "-"
109
+ ? JSON.parse(fs.readFileSync(0, "utf8")) : readJsonFile(request.options.providerArchiveTrustFile);
110
+ }
92
111
  if (request.operation === "sync" && request.options.boundary) {
93
112
  if (request.options.boundary !== "task_start" && !request.options.evidenceFile) throw new Error(`${request.options.boundary} requires --evidence`);
94
113
  if (request.options.evidenceFile) request.options.continuityEvidence = readJsonFile(request.options.evidenceFile);
@@ -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});}
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const assert = require("assert");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const vm = require("vm");
8
+ const { createRequire } = require("module");
9
+ const { spawnSync } = require("child_process");
10
+ const technology = require("../project-technology");
11
+ const fixtureFile = path.join(__dirname, "validate-project-technology.js");
12
+ const fixtureText = fs.readFileSync(fixtureFile, "utf8");
13
+ const sandbox = { require: createRequire(fixtureFile), module: { exports: {} } };
14
+ vm.runInNewContext(fixtureText.slice(0, fixtureText.indexOf("const checks = [];")) +
15
+ "\nmodule.exports={initRepo,addTarget,git,TARGET_ID,SEMANTIC_DIGEST};", sandbox);
16
+ const fixture = sandbox.module.exports;
17
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "mirai-archive-test-")));
18
+ const originalPath = process.env.PATH;
19
+ const checks = [];
20
+ function check(id, condition, details) { checks.push({ id, passed: Boolean(condition) }); assert.ok(condition, `${id}: ${JSON.stringify(details)}`); }
21
+ function digestTree(dir) {
22
+ const files = [];
23
+ function walk(current) {
24
+ if (!fs.existsSync(current)) return;
25
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
26
+ const file = path.join(current, entry.name);
27
+ if (entry.isDirectory()) walk(file);
28
+ else files.push([path.relative(dir, file), technology.sha256(fs.readFileSync(file))]);
29
+ }
30
+ }
31
+ walk(dir); return technology.sha256(JSON.stringify(files.sort()));
32
+ }
33
+ try {
34
+ const sourceRoot = path.join(root, "source"); fs.mkdirSync(sourceRoot);
35
+ const provider = fixture.initRepo(sourceRoot, "fixture-provider");
36
+ const consumer = fixture.initRepo(sourceRoot, "fixture-consumer");
37
+ let revision = fixture.addTarget(provider);
38
+ const stateRoot = path.join(root, "state");
39
+ const base = { targetId: fixture.TARGET_ID, semanticDigest: fixture.SEMANTIC_DIGEST, stateRoot, apply: true };
40
+ for (const repo of [provider, consumer]) assert.equal(technology.execute("enable", repo, base).status, "success");
41
+ fixture.git(provider, "add", "graph.json"); fixture.git(provider, "commit", "-qm", "accepted activation");
42
+ revision = fixture.git(provider, "rev-parse", "HEAD");
43
+ const releases = [];
44
+ for (let n = 0; n < 2; n++) {
45
+ if (n) { fixture.git(provider, "commit", "--allow-empty", "-qm", "compatible revision"); revision = fixture.git(provider, "rev-parse", "HEAD"); }
46
+ const provided = technology.execute("provide", provider, { ...base, providerRevision: revision });
47
+ assert.equal(provided.status, "success");
48
+ const beforeSourceProof = digestTree(root);
49
+ const sourceProof = technology.execute("verify", provider, { source: path.join(provider, provided.export_ref) });
50
+ check(`source_export_proven_${n}`, sourceProof.status === "success" && sourceProof.provider_archive.providerRevision === revision, sourceProof);
51
+ check(`source_proof_zero_write_${n}`, digestTree(root) === beforeSourceProof);
52
+ check(`source_proof_not_authority_${n}`, technology.execute("verify", provider, { source: path.join(provider, provided.export_ref), significantWork: true }).status === "blocked");
53
+ const providerManifestPath = path.join(provider, "graph.json");
54
+ const providerManifestBytes = fs.readFileSync(providerManifestPath);
55
+ fs.appendFileSync(providerManifestPath, " ");
56
+ check(`uncommitted_manifest_blocks_source_proof_${n}`, technology.execute("verify", provider, { source: path.join(provider, provided.export_ref) }).blockers.includes("provider_manifest_not_revision_bound"));
57
+ fs.writeFileSync(providerManifestPath, providerManifestBytes);
58
+ const bytes = fs.readFileSync(path.join(provider, provided.export_ref));
59
+ const source = path.join(root, `export-${n}.json`); fs.writeFileSync(source, bytes);
60
+ check(`external_export_blocks_source_proof_${n}`, technology.execute("verify", provider, { source }).blockers.includes("provider_export_outside_repository"));
61
+ // Build anchor on the verified SOURCE side, before Git is removed.
62
+ const ancestors = n ? [releases[0].providerRevision] : [];
63
+ for (const ancestor of ancestors) fixture.git(provider, "merge-base", "--is-ancestor", ancestor, revision);
64
+ releases.push({ ...base, source, providerRevision: revision, providerArchive: {
65
+ exportSha256: technology.sha256(bytes), graphId: "fixture-provider", providerRevision: revision, ancestorRevisions: ancestors,
66
+ } });
67
+ }
68
+ const packedConsumer = path.join(root, "consumer");
69
+ fs.cpSync(consumer, packedConsumer, { recursive: true, filter: file => path.basename(file) !== ".git" });
70
+ const emptyPath = path.join(root, "no-tools"); fs.mkdirSync(emptyPath); process.env.PATH = emptyPath;
71
+ check("git_is_absent", spawnSync("git", ["--version"]).error?.code === "ENOENT");
72
+ const invoke = options => technology.execute("connect", packedConsumer, options);
73
+ const noTrust = { ...releases[0] }; delete noTrust.providerArchive;
74
+ check("untrusted_archive_blocked", invoke(noTrust).blockers.includes("provider_revision_order_unverifiable"));
75
+ const before = digestTree(root);
76
+ check("preview", invoke({ ...releases[0], apply: false }).status === "preview");
77
+ check("preview_zero_write", digestTree(root) === before);
78
+ const connected = invoke(releases[0]);
79
+ check("archive_connect", connected.status === "success", connected);
80
+ check("complete_contract_imported", connected.target_binding.status === "ready", connected);
81
+ const after = digestTree(root);
82
+ check("repeat_changed_false", invoke(releases[0]).changed === false);
83
+ check("repeat_byte_identical", digestTree(root) === after);
84
+ const status = technology.execute("status", packedConsumer, base);
85
+ check("read_only_ready", status.target_binding.status === "ready", status);
86
+ technology.execute("verify", packedConsumer, base);
87
+ check("diagnostics_zero_write", digestTree(root) === after);
88
+ function denied(id, options, blocker) {
89
+ const prior = digestTree(root); const result = invoke(options);
90
+ check(id, result.status === "fail" && (!blocker || result.blockers.includes(blocker)), result);
91
+ check(`${id}_zero_write`, prior === digestTree(root));
92
+ }
93
+ denied("null_trust", { ...releases[0], providerArchive: null }, "provider_archive_trust_invalid");
94
+ denied("bad_digest", { ...releases[0], providerArchive: { ...releases[0].providerArchive, exportSha256: "0".repeat(64) } }, "provider_archive_export_digest_mismatch");
95
+ denied("bad_graph", { ...releases[0], providerArchive: { ...releases[0].providerArchive, graphId: "other.provider" } }, "provider_archive_graph_mismatch");
96
+ denied("bad_revision", { ...releases[0], providerArchive: { ...releases[0].providerArchive, providerRevision: "f".repeat(40) } }, "provider_archive_revision_mismatch");
97
+ denied("extra_trust_fields", { ...releases[0], providerArchive: { ...releases[0].providerArchive, approved: true } }, "provider_archive_trust_invalid");
98
+ denied("explicit_refresh_required", releases[1], "target_provider_refresh_required");
99
+ denied("missing_ancestry", { ...releases[1], refreshBinding: true, providerArchive: { ...releases[1].providerArchive, ancestorRevisions: [] } }, "provider_revision_not_forward");
100
+ denied("target_mismatch", { ...releases[1], targetId: "other.target", refreshBinding: true }, "target_provider_target_mismatch");
101
+ denied("semantic_mismatch", { ...releases[1], semanticDigest: `sha256:${"3".repeat(64)}`, refreshBinding: true }, "target_provider_semantic_mismatch");
102
+ const refreshed = invoke({ ...releases[1], refreshBinding: true });
103
+ check("forward_refresh", refreshed.status === "success" && refreshed.changed, refreshed);
104
+ const refreshedTree = digestTree(root);
105
+ check("refresh_idempotent", invoke({ ...releases[1], refreshBinding: true }).changed === false);
106
+ check("refresh_byte_identical", digestTree(root) === refreshedTree);
107
+ denied("downgrade", { ...releases[0], refreshBinding: true }, "provider_revision_not_forward");
108
+ const original = fs.readFileSync(releases[1].source);
109
+ fs.appendFileSync(releases[1].source, " ");
110
+ denied("tampered_bytes", { ...releases[1], refreshBinding: true }, "provider_archive_export_digest_mismatch");
111
+ fs.writeFileSync(releases[1].source, original);
112
+ function alteredExport(id, alter, expected) {
113
+ const payload = JSON.parse(original); alter(payload);
114
+ fs.writeFileSync(releases[1].source, technology.canonicalBytes(payload));
115
+ const options = { ...releases[1], refreshBinding: true, providerArchive: { ...releases[1].providerArchive,
116
+ exportSha256: technology.sha256(fs.readFileSync(releases[1].source)), graphId: payload.provider_graph_id } };
117
+ denied(id, options, expected); fs.writeFileSync(releases[1].source, original);
118
+ }
119
+ alteredExport("provider_conflict", p => { p.provider_graph_id = "another.provider"; }, "target_provider_graph_conflict");
120
+ alteredExport("incomplete_contract", p => { p.requirement_bindings = []; });
121
+ alteredExport("contract_tamper", p => { p.goal_binding.done_when_ids = ["done.other"]; });
122
+ alteredExport("valid_but_different_contract", p => {
123
+ p.allowed_change_scope[0].file_patterns = ["packages/other/**"];
124
+ p.execution_contract_digest = technology.sha256(technology.canonicalBytes(technology.normalizeExecutionContract(p).contract));
125
+ }, "provider_execution_contract_refresh_mismatch");
126
+ alteredExport("reviewed_not_accepted", p => { p.architecture_contract.lifecycle = "reviewed"; });
127
+ alteredExport("private_extra_field", p => { p.private_source = "not allowed"; }, "provider_export_not_bounded");
128
+ const graphFile = path.join(packedConsumer, "graph.json"); const graphBefore = fs.readFileSync(graphFile);
129
+ const graph = JSON.parse(graphBefore); graph.extensions["mirai.project_technology"].enabled = false;
130
+ fs.writeFileSync(graphFile, JSON.stringify(graph));
131
+ denied("disabled", releases[1], "project_technology_not_enabled");
132
+ check("disabled_diagnostics_available", technology.execute("status", packedConsumer, base).operation_mode === "read_only");
133
+ fs.writeFileSync(graphFile, graphBefore);
134
+ const trustFile = path.join(root, "trust.json"); fs.writeFileSync(trustFile, JSON.stringify(releases[1].providerArchive));
135
+ const cli = spawnSync(process.execPath, [path.join(__dirname, "project-technology.js"), "connect", packedConsumer,
136
+ "--source", releases[1].source, "--target-id", base.targetId, "--semantic-digest", base.semanticDigest,
137
+ "--provider-revision", releases[1].providerRevision, "--state-root", stateRoot,
138
+ "--provider-archive-trust", trustFile, "--apply"], { encoding: "utf8" });
139
+ check("cli_no_git_idempotent", cli.status === 0 && JSON.parse(cli.stdout).changed === false, cli.stdout);
140
+ // A new identity field is optional for historical Git exports, but required
141
+ // for authenticated archive imports. Existing stored contracts remain valid.
142
+ const legacy = JSON.parse(original); delete legacy.provider_graph_id;
143
+ fs.writeFileSync(releases[1].source, technology.canonicalBytes(legacy));
144
+ check("historical_export_readable", technology.readExport(releases[1].source).blockers.length === 0);
145
+ denied("legacy_archive_needs_graph_identity", { ...releases[1], providerArchive: { ...releases[1].providerArchive,
146
+ exportSha256: technology.sha256(fs.readFileSync(releases[1].source)) } }, "provider_archive_graph_mismatch");
147
+ fs.writeFileSync(releases[1].source, original);
148
+ const folderInventory = technology.inventory(packedConsumer);
149
+ check("folder_inventory_nonempty", folderInventory.files.some(file => file.path === "graph.json") && folderInventory.files.some(file => file.path === "README.md"), folderInventory);
150
+ check("folder_inventory_sync", technology.execute("sync", packedConsumer, base).status === "success");
151
+ check("folder_inventory_sync_noop", technology.execute("sync", packedConsumer, base).changed === false);
152
+ const readme = path.join(packedConsumer, "README.md"); const readmeBefore = fs.readFileSync(readme);
153
+ fs.appendFileSync(readme, "\nChanged declared raw source\n");
154
+ const staleTree = digestTree(root);
155
+ check("folder_changed_source_is_stale", technology.execute("status", packedConsumer, base).blockers.includes("project_technology_inventory_stale"));
156
+ check("folder_stale_status_zero_write", digestTree(root) === staleTree);
157
+ fs.writeFileSync(readme, readmeBefore);
158
+ const graphForMissing = JSON.parse(fs.readFileSync(graphFile)); graphForMissing.graph.raw_sources.push("missing-required-source.md");
159
+ fs.writeFileSync(graphFile, JSON.stringify(graphForMissing));
160
+ const missingBefore = digestTree(root);
161
+ check("missing_declared_source_blocks_sync", technology.execute("sync", packedConsumer, base).blockers.includes("inventory_declared_source_missing"));
162
+ check("missing_source_sync_zero_write", digestTree(root) === missingBefore);
163
+ fs.writeFileSync(graphFile, graphBefore);
164
+ console.log(JSON.stringify({ status: "success", check_count: checks.length, checks }, null, 2));
165
+ } finally { process.env.PATH = originalPath; fs.rmSync(root, { recursive: true, force: true }); }
@@ -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,