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.
- package/.github/workflows/ci.yml +3 -0
- package/CHANGELOG.md +36 -0
- package/CITATION.cff +2 -2
- package/README.md +13 -8
- package/package.json +2 -2
- package/packages/cli/graph-manifest.js +8 -1
- package/packages/cli/project-technology.js +19 -0
- package/packages/cli/validate-local-target.js +149 -0
- package/packages/cli/validate-provider-archive.js +165 -0
- package/packages/project-technology/continuity.js +2 -0
- package/packages/project-technology/index.js +332 -15
- package/releases/1.5.0.md +37 -0
- package/releases/1.6.0.md +43 -0
- package/releases/README.md +4 -0
- package/schemas/project-technology-extension.schema.json +10 -1
- package/schemas/project-technology-target-export.schema.json +1 -0
- package/standard/project-technology.md +122 -0
package/.github/workflows/ci.yml
CHANGED
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
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",
|
|
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) {
|
|
@@ -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 }); }
|