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.
- package/.github/workflows/ci.yml +3 -0
- package/CHANGELOG.md +20 -0
- package/CITATION.cff +1 -1
- package/README.md +13 -8
- package/package.json +2 -2
- package/packages/cli/graph-manifest.js +8 -1
- package/packages/cli/project-technology.js +11 -0
- package/packages/cli/validate-local-target.js +149 -0
- package/packages/project-technology/continuity.js +2 -0
- package/packages/project-technology/index.js +216 -7
- package/releases/1.6.0.md +43 -0
- package/releases/README.md +2 -0
- package/schemas/project-technology-extension.schema.json +10 -1
- package/standard/project-technology.md +69 -0
package/.github/workflows/ci.yml
CHANGED
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
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:
|
|
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
|
|
46
|
-
|
|
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
|
|
60
|
+
External 1.x projects can explicitly pin the published baseline:
|
|
56
61
|
|
|
57
62
|
```bash
|
|
58
|
-
npm install -
|
|
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.
|
|
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
|
-
|
|
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});}
|
|
@@ -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
|
-
|
|
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.
|
package/releases/README.md
CHANGED
|
@@ -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
|