mirai-graph 1.1.1 → 1.2.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 +28 -0
- package/CITATION.cff +2 -2
- package/README.md +15 -3
- package/package.json +3 -2
- package/packages/cli/graph-manifest.js +2 -1
- package/packages/cli/project-technology.js +10 -1
- package/packages/cli/validate-project-continuity.js +173 -0
- package/packages/cli/validate-project-technology.js +26 -2
- package/packages/project-technology/context-traversal.js +22 -6
- package/packages/project-technology/continuity.js +414 -0
- package/packages/project-technology/index.js +140 -36
- package/profiles/implementation-control/profile.json +3 -1
- package/profiles/project-management/profile.json +8 -1
- package/releases/1.2.0.md +19 -0
- package/releases/README.md +2 -0
- package/schemas/project-technology-extension.schema.json +2 -1
- package/standard/context-pack-generation.md +8 -2
- package/standard/object-model.md +6 -0
- package/standard/project-technology.md +35 -4
- package/standard/test-suite.md +4 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -26,6 +26,9 @@ jobs:
|
|
|
26
26
|
- name: Verify deterministic context traversal
|
|
27
27
|
run: node packages/cli/validate-context-traversal.js
|
|
28
28
|
|
|
29
|
+
- name: Verify portable project continuity
|
|
30
|
+
run: node packages/cli/validate-project-continuity.js
|
|
31
|
+
|
|
29
32
|
release-check:
|
|
30
33
|
runs-on: ubuntu-latest
|
|
31
34
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ All notable changes to Mirai Graph will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.2.0] - 2026-08-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Portable project continuity in the existing Project Technology `sync`,
|
|
12
|
+
`context` and `verify` operations.
|
|
13
|
+
- Task-boundary evidence compaction into accepted facts, evidence, reusable
|
|
14
|
+
regression cases and bounded proposals in `graph/specs`.
|
|
15
|
+
- Host-local receipts keyed by graph identity, with lease, compare-and-swap,
|
|
16
|
+
backup, atomic write, readback and rollback support for shared non-Git folders.
|
|
17
|
+
- Automatic discovery of relevant previous cases through the existing context
|
|
18
|
+
traversal protocol.
|
|
19
|
+
|
|
20
|
+
### Safety Boundary
|
|
21
|
+
|
|
22
|
+
- Chat transcripts, secrets, private source, user paths and unverified claims
|
|
23
|
+
are not portable project memory.
|
|
24
|
+
- New goals, architecture changes and ownership changes remain proposals.
|
|
25
|
+
- Generated human context is a projection only and never authorizes writes.
|
|
26
|
+
- The public `graph.json` schema and Project Technology activation contract
|
|
27
|
+
remain `2.0.0` and `1.0.0` respectively.
|
|
28
|
+
|
|
29
|
+
### Verification
|
|
30
|
+
|
|
31
|
+
- New fixtures cover Git and non-Git projects, two-installation portability,
|
|
32
|
+
idempotence, stale receipts, concurrent-write protection, secret rejection,
|
|
33
|
+
proposal boundaries and two-case method promotion.
|
|
34
|
+
|
|
7
35
|
## [1.1.1] - 2026-08-26
|
|
8
36
|
|
|
9
37
|
### Fixed
|
package/CITATION.cff
CHANGED
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Use it when a software project has useful knowledge spread across README files,
|
|
|
10
10
|
docs, issues, chats, code comments and AI prompts, and you need a small
|
|
11
11
|
machine-readable map that developers and AI assistants can validate and reuse.
|
|
12
12
|
|
|
13
|
-
Status: stable public `1.
|
|
13
|
+
Status: stable public `1.2.0` release.
|
|
14
14
|
|
|
15
15
|
## What Problem Does It Solve?
|
|
16
16
|
|
|
@@ -42,7 +42,7 @@ Start with a normal repository and use Mirai Graph to:
|
|
|
42
42
|
|
|
43
43
|
## Current Installation Status
|
|
44
44
|
|
|
45
|
-
The repository
|
|
45
|
+
The repository provides the stable `mirai-graph@1.2.0` package. To verify a
|
|
46
46
|
repository checkout directly, use:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
@@ -96,7 +96,8 @@ Enable it in the existing root manifest:
|
|
|
96
96
|
"contract_version": "1.0.0",
|
|
97
97
|
"enabled": true,
|
|
98
98
|
"context_policy": "task_scoped",
|
|
99
|
-
"source_boundary": "hybrid_sot"
|
|
99
|
+
"source_boundary": "hybrid_sot",
|
|
100
|
+
"continuity_policy": "task_boundary"
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
}
|
|
@@ -130,6 +131,17 @@ The programmatic API exposes the same operations as `discoverContext`,
|
|
|
130
131
|
`expandContext`, `compileContext` and `verifyContext`. Mirai Graph does not call
|
|
131
132
|
or depend on a particular AI model.
|
|
132
133
|
|
|
134
|
+
Verified project experience can be saved without storing a chat transcript:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
mirai-graph technology sync . --boundary task_complete --evidence continuity-evidence.json
|
|
138
|
+
mirai-graph technology sync . --boundary task_complete --evidence continuity-evidence.json --apply
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Accepted facts and reusable cases live in `graph/specs`; mutable receipts and
|
|
142
|
+
rollback data stay host-local. The same contract works in Git repositories and
|
|
143
|
+
ordinary shared folders.
|
|
144
|
+
|
|
133
145
|
Changing operations return a preview unless `--apply` is explicit. See
|
|
134
146
|
[Project Technology](standard/project-technology.md) for provider/consumer
|
|
135
147
|
binding, local state and safety boundaries.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirai-graph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "An evolutionary graph operating model for managing the growth of complex systems.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
"./project-technology": "./packages/project-technology/index.js"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"pretest": "npm run test:project-technology && npm run test:context-traversal && npm run validate:project-technology-schemas",
|
|
16
|
+
"pretest": "npm run test:project-technology && npm run test:context-traversal && npm run test:project-continuity && npm run validate:project-technology-schemas",
|
|
17
17
|
"test:project-technology": "node packages/cli/validate-project-technology.js",
|
|
18
18
|
"test:context-traversal": "node packages/cli/validate-context-traversal.js",
|
|
19
|
+
"test:project-continuity": "node packages/cli/validate-project-continuity.js",
|
|
19
20
|
"validate:project-technology-schemas": "node -e \"const fs=require('fs'); for (const file of ['project-technology-extension.schema.json','project-technology-target-export.schema.json','context-traversal-receipt.schema.json','context-pack.schema.json']) JSON.parse(fs.readFileSync('schemas/'+file))\"",
|
|
20
21
|
"validate:graph-manifest": "node packages/cli/validate-graph-manifest-migration.js",
|
|
21
22
|
"validate:minimal": "node packages/cli/validate-mirai-graph.js examples/minimal-graph",
|
|
@@ -130,7 +130,8 @@ function validateManifest(manifest, repo = null) {
|
|
|
130
130
|
};
|
|
131
131
|
for (const field of Object.keys(expected)) if (technology[field] !== expected[field]) errors.push(`extensions.mirai.project_technology.${field} is invalid`);
|
|
132
132
|
if (typeof technology.enabled !== "boolean") errors.push("extensions.mirai.project_technology.enabled is invalid");
|
|
133
|
-
|
|
133
|
+
if (technology.continuity_policy !== undefined && technology.continuity_policy !== "task_boundary") errors.push("extensions.mirai.project_technology.continuity_policy is invalid");
|
|
134
|
+
for (const field of Object.keys(technology)) if (!(field in expected) && !["enabled", "continuity_policy"].includes(field)) errors.push(`extensions.mirai.project_technology has unknown field ${field}`);
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
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("Continuity: sync --boundary task_start|stage_complete|task_complete [--evidence <json>] [--expected-graph-digest <sha256:...>]");
|
|
17
|
+
console.error(" verify --significant-work [--receipt-digest <sha256:...>]");
|
|
16
18
|
console.error("Context: --phase discover --task <text>");
|
|
17
19
|
console.error(" --phase expand --input <receipt.json> --select <id> [--select <id>]");
|
|
18
20
|
console.error(" --phase compile --input <receipt.json> --selection <selection.json>");
|
|
@@ -25,7 +27,7 @@ function parse(argv) {
|
|
|
25
27
|
let repository = ".";
|
|
26
28
|
if (argv[0] && !argv[0].startsWith("--")) repository = argv.shift();
|
|
27
29
|
const options = {};
|
|
28
|
-
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"]);
|
|
30
|
+
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"]);
|
|
29
31
|
const flags = new Set(["--apply", "--refresh-binding", "--significant-work"]);
|
|
30
32
|
const names = {
|
|
31
33
|
"--task": "task", "--source": "source", "--target-id": "targetId",
|
|
@@ -36,6 +38,8 @@ function parse(argv) {
|
|
|
36
38
|
"--selection": "selectionFile", "--packet": "packetFile", "--evidence": "evidenceFile",
|
|
37
39
|
"--selector": "selector", "--reason": "reason", "--confidence": "confidence",
|
|
38
40
|
"--context-budget": "contextBudget",
|
|
41
|
+
"--boundary": "boundary", "--expected-graph-digest": "expectedGraphDigest",
|
|
42
|
+
"--receipt-digest": "receiptDigest", "--state-root": "stateRoot",
|
|
39
43
|
};
|
|
40
44
|
while (argv.length) {
|
|
41
45
|
const token = argv.shift();
|
|
@@ -57,6 +61,11 @@ function readJsonFile(file) {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
function hydrateContextOptions(request) {
|
|
64
|
+
if (request.operation === "sync" && request.options.boundary) {
|
|
65
|
+
if (request.options.boundary !== "task_start" && !request.options.evidenceFile) throw new Error(`${request.options.boundary} requires --evidence`);
|
|
66
|
+
if (request.options.evidenceFile) request.options.continuityEvidence = readJsonFile(request.options.evidenceFile);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
60
69
|
if (request.operation !== "context" || !request.options.phase) return;
|
|
61
70
|
const phase = request.options.phase;
|
|
62
71
|
if (!new Set(["discover", "expand", "compile", "verify"]).has(phase)) throw new Error(`unsupported context phase ${phase}`);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
const technology = require("../project-technology");
|
|
11
|
+
|
|
12
|
+
function writeJson(file, value) {
|
|
13
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
14
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function git(repo, ...args) {
|
|
18
|
+
const run = spawnSync("git", args, { cwd: repo, encoding: "utf8" });
|
|
19
|
+
if (run.status !== 0) throw new Error(run.stderr || `git ${args.join(" ")} failed`);
|
|
20
|
+
return run.stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function manifest(id) {
|
|
24
|
+
return {
|
|
25
|
+
$schema: "https://mirai-graph.dev/schemas/graph-manifest.schema.json",
|
|
26
|
+
format: "mirai-graph", schema_version: "2.0.0", id, aliases: [], title: id,
|
|
27
|
+
scope: "repository", kind: "repository_graph", owner: "fixture-owner",
|
|
28
|
+
profiles: ["project_management", "implementation_control"],
|
|
29
|
+
graph: {
|
|
30
|
+
root: "graph", source_of_truth: ["graph/specs"], objects: ["graph/specs/objects.json"],
|
|
31
|
+
relations: ["graph/specs/relations.json"], schemas: [], generated: ["graph/generated"], raw_sources: ["README.md"],
|
|
32
|
+
},
|
|
33
|
+
imports: [],
|
|
34
|
+
extensions: { "mirai.project_technology": technology.extensionContract() },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createRepo(root, name, withGit = true) {
|
|
39
|
+
const repo = path.join(root, name); fs.mkdirSync(repo, { recursive: true });
|
|
40
|
+
writeJson(path.join(repo, "graph.json"), manifest(name));
|
|
41
|
+
writeJson(path.join(repo, "graph/specs/objects.json"), [{
|
|
42
|
+
id: "goal.fixture.delivery", kind: "goal", title: "Deliver fixture", summary: "Complete a safe fixture task",
|
|
43
|
+
readiness: "accepted", profile: "project_management", source_refs: ["README.md"],
|
|
44
|
+
}]);
|
|
45
|
+
writeJson(path.join(repo, "graph/specs/relations.json"), []);
|
|
46
|
+
fs.writeFileSync(path.join(repo, "README.md"), "# Fixture\nPortable safe context.\n");
|
|
47
|
+
if (withGit) {
|
|
48
|
+
git(repo, "init", "-q"); git(repo, "config", "user.name", "Mirai Fixture"); git(repo, "config", "user.email", "fixture@example.invalid");
|
|
49
|
+
git(repo, "add", "."); git(repo, "commit", "-qm", "initial graph");
|
|
50
|
+
}
|
|
51
|
+
return repo;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function evidence(task, options = {}) {
|
|
55
|
+
return {
|
|
56
|
+
task_digest: technology.sha256(task, true),
|
|
57
|
+
outcome: options.outcome || `Verified result for ${task}`,
|
|
58
|
+
requirement_refs: options.requirement_refs || ["requirement.fixture.portability"],
|
|
59
|
+
evidence_refs: ["repo://fixture/checks/continuity"],
|
|
60
|
+
checks: [{ id: "check.fixture.continuity", verdict: "pass", evidence_ref: "repo://fixture/checks/continuity" }],
|
|
61
|
+
changed_surfaces: ["src/fixture.js"],
|
|
62
|
+
case_signature: options.case_signature || "case.fixture.portable-work",
|
|
63
|
+
method_candidate: options.method_candidate || null,
|
|
64
|
+
decisions: options.decisions || [],
|
|
65
|
+
source_revision: options.source_revision || null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const checks = [];
|
|
70
|
+
function check(id, condition, details = null) {
|
|
71
|
+
checks.push({ id, passed: Boolean(condition), details });
|
|
72
|
+
assert.ok(condition, `${id}: ${JSON.stringify(details)}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mirai-continuity-"));
|
|
76
|
+
const stateRoot = path.join(root, "host-state");
|
|
77
|
+
try {
|
|
78
|
+
const repo = createRepo(root, "git-project");
|
|
79
|
+
const before = git(repo, "status", "--porcelain=v1");
|
|
80
|
+
const taskStart = technology.execute("sync", repo, { apply: true, boundary: "task_start", stateRoot });
|
|
81
|
+
check("task_start_is_read_only", taskStart.status === "success" && taskStart.changed === false && git(repo, "status", "--porcelain=v1") === before, taskStart);
|
|
82
|
+
const preview = technology.execute("sync", repo, { apply: false, boundary: "task_complete", continuityEvidence: evidence("preview"), stateRoot });
|
|
83
|
+
check("preview_is_zero_write", preview.status === "preview" && git(repo, "status", "--porcelain=v1") === before, preview);
|
|
84
|
+
const missingReceiptVerify = technology.verify(repo, { stateRoot });
|
|
85
|
+
check("configured_continuity_without_receipt_blocks_verify", missingReceiptVerify.status === "blocked" && missingReceiptVerify.blockers.includes("continuity_missing"), missingReceiptVerify);
|
|
86
|
+
|
|
87
|
+
const first = technology.execute("sync", repo, { apply: true, boundary: "stage_complete", continuityEvidence: evidence("first", { method_candidate: "Use the verified bounded fixture path." }), stateRoot });
|
|
88
|
+
check("verified_result_is_saved", first.status === "success" && first.changed && first.continuity.saved_refs.length === 2, first);
|
|
89
|
+
check("portable_authority_is_graph_specs", fs.existsSync(path.join(repo, "graph/specs/project-continuity.json")) && first.continuity.authority === "graph/specs");
|
|
90
|
+
check("host_state_is_outside_project", !fs.existsSync(path.join(repo, ".mirai-graph/project-technology/receipts")) && fs.existsSync(stateRoot));
|
|
91
|
+
check("human_projection_is_generated", fs.readFileSync(path.join(repo, "graph/docs/project-context.md"), "utf8").includes("not a source of truth"));
|
|
92
|
+
const objectsAfterFirst = JSON.parse(fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"), "utf8")).objects;
|
|
93
|
+
check("first_method_is_proposal", objectsAfterFirst.some((item) => item.kind === "lesson" && item.readiness === "proposal"));
|
|
94
|
+
check("architecture_decision_is_not_accepted", technology.execute("sync", repo, {
|
|
95
|
+
apply: true, boundary: "task_complete", stateRoot,
|
|
96
|
+
continuityEvidence: evidence("decision", { decisions: [{ summary: "Replace the project architecture", changes_architecture: true }] }),
|
|
97
|
+
}).status === "success");
|
|
98
|
+
const decisionObjects = JSON.parse(fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"), "utf8")).objects;
|
|
99
|
+
check("architecture_change_remains_proposal", decisionObjects.some((item) => item.kind === "decision" && item.summary === "Replace the project architecture" && item.readiness === "proposal"));
|
|
100
|
+
|
|
101
|
+
const second = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: evidence("second", { method_candidate: "Use the verified bounded fixture path." }), stateRoot });
|
|
102
|
+
check("second_independent_case_is_saved", second.status === "success", second);
|
|
103
|
+
const objectsAfterSecond = JSON.parse(fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"), "utf8")).objects;
|
|
104
|
+
check("method_promotes_after_two_independent_cases", objectsAfterSecond.some((item) => item.kind === "lesson" && item.readiness === "accepted" && item.supporting_case_ids.length >= 2));
|
|
105
|
+
|
|
106
|
+
const beforeRepeat = fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"));
|
|
107
|
+
const repeated = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: evidence("second", { method_candidate: "Use the verified bounded fixture path." }), stateRoot });
|
|
108
|
+
check("repeat_is_byte_identical", repeated.status === "success" && repeated.changed === false && fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json")).equals(beforeRepeat), repeated);
|
|
109
|
+
check("fresh_continuity_verifies_without_target_for_light_work", technology.verify(repo, { stateRoot }).status === "success");
|
|
110
|
+
check("fresh_receipt_allows_significant_work", technology.continuity.verify(repo, JSON.parse(fs.readFileSync(path.join(repo, "graph.json"), "utf8")), { stateRoot }).status === "success");
|
|
111
|
+
|
|
112
|
+
const staleDigest = technology.continuity.graphDigest(repo);
|
|
113
|
+
fs.appendFileSync(path.join(repo, "graph/specs/objects.json"), " ");
|
|
114
|
+
check("stale_continuity_blocks_significant_work", technology.continuity.verify(repo, JSON.parse(fs.readFileSync(path.join(repo, "graph.json"), "utf8")), { stateRoot }).blockers.includes("continuity_stale"));
|
|
115
|
+
const baseObjects = JSON.parse(fs.readFileSync(path.join(repo, "graph/specs/objects.json"), "utf8"));
|
|
116
|
+
baseObjects[0].summary = "Changed concurrently";
|
|
117
|
+
fs.writeFileSync(path.join(repo, "graph/specs/objects.json"), JSON.stringify(baseObjects, null, 2) + "\n");
|
|
118
|
+
const cas = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: evidence("cas"), expectedGraphDigest: staleDigest, stateRoot });
|
|
119
|
+
check("compare_and_swap_conflict_is_safe", cas.status === "blocked" && cas.blockers.includes("continuity_compare_and_swap_conflict"), cas);
|
|
120
|
+
baseObjects[0].summary = "Complete a safe fixture task";
|
|
121
|
+
fs.writeFileSync(path.join(repo, "graph/specs/objects.json"), JSON.stringify(baseObjects, null, 2) + "\n");
|
|
122
|
+
|
|
123
|
+
const secret = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: evidence("secret", { outcome: "token=not-a-real-secret-value-123456" }), stateRoot });
|
|
124
|
+
check("secret_is_rejected", secret.status === "blocked" && secret.blockers.includes("continuity_sensitive_or_host_data_forbidden"), secret);
|
|
125
|
+
const privatePath = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: { ...evidence("private"), changed_surfaces: ["source/memory/private.md"] }, stateRoot });
|
|
126
|
+
check("private_projection_is_rejected", privatePath.status === "blocked" && privatePath.blockers.includes("continuity_sensitive_or_host_data_forbidden"), privatePath);
|
|
127
|
+
const failedCheck = technology.execute("sync", repo, { apply: true, boundary: "task_complete", continuityEvidence: { ...evidence("fail"), checks: [{ id: "check.fixture", verdict: "fail", evidence_ref: "repo://fixture/fail" }] }, stateRoot });
|
|
128
|
+
check("unverified_result_is_rejected", failedCheck.status === "blocked" && failedCheck.blockers.includes("continuity_result_not_verified"), failedCheck);
|
|
129
|
+
|
|
130
|
+
git(repo, "add", "graph.json", "graph/specs", "graph/docs"); git(repo, "commit", "-qm", "portable continuity records");
|
|
131
|
+
const discovered = technology.context(repo, { phase: "discover", task: "use the verified portable work case" });
|
|
132
|
+
check("new_task_discovers_past_case", discovered.status === "success" && discovered.traversal_receipt.candidates.some((item) => item.id.startsWith("regression_case.continuity.")), discovered.blockers);
|
|
133
|
+
const beforeReadOnly = git(repo, "status", "--porcelain=v1");
|
|
134
|
+
technology.status(repo); technology.context(repo, { phase: "discover", task: "read only project context" });
|
|
135
|
+
check("read_only_operations_write_nothing", git(repo, "status", "--porcelain=v1") === beforeReadOnly);
|
|
136
|
+
|
|
137
|
+
const folder = createRepo(root, "shared-folder", false);
|
|
138
|
+
const folderFirst = technology.execute("sync", folder, { apply: true, boundary: "task_complete", continuityEvidence: evidence("folder"), stateRoot });
|
|
139
|
+
check("non_git_folder_supports_continuity", folderFirst.status === "success" && folderFirst.changed, folderFirst);
|
|
140
|
+
const folderRepeat = technology.execute("sync", folder, { apply: true, boundary: "task_complete", continuityEvidence: evidence("folder"), stateRoot });
|
|
141
|
+
check("non_git_repeat_is_idempotent", folderRepeat.status === "success" && folderRepeat.changed === false, folderRepeat);
|
|
142
|
+
const sharedLease = path.join(folder, "graph/.project-technology-continuity.lock");
|
|
143
|
+
fs.writeFileSync(sharedLease, "occupied\n");
|
|
144
|
+
const leaseConflict = technology.execute("sync", folder, { apply: true, boundary: "task_complete", continuityEvidence: evidence("parallel"), stateRoot: path.join(root, "third-installation-state") });
|
|
145
|
+
check("shared_folder_lease_blocks_parallel_writer", leaseConflict.status === "blocked" && leaseConflict.blockers.includes("continuity_lease_conflict"), leaseConflict);
|
|
146
|
+
fs.unlinkSync(sharedLease);
|
|
147
|
+
const folderDiscovery = technology.context(folder, { phase: "discover", task: "portable fixture case" });
|
|
148
|
+
check("non_git_context_is_available", folderDiscovery.status === "success", folderDiscovery.blockers);
|
|
149
|
+
check("same_content_has_stable_digest", technology.continuity.graphDigest(folder) === technology.continuity.graphDigest(folder));
|
|
150
|
+
|
|
151
|
+
const otherStateRoot = path.join(root, "other-installation-state");
|
|
152
|
+
const firstInstallContext = technology.context(repo, { phase: "discover", task: "use verified portable work" });
|
|
153
|
+
const secondInstallContext = technology.context(repo, { phase: "discover", task: "use verified portable work", stateRoot: otherStateRoot });
|
|
154
|
+
check("two_installations_get_same_context", firstInstallContext.traversal_receipt.traversal_digest === secondInstallContext.traversal_receipt.traversal_digest);
|
|
155
|
+
check("portable_specs_contain_no_host_paths", !/(?:\/Users\/|\/home\/|[A-Za-z]:\\Users\\)/.test(fs.readFileSync(path.join(repo, "graph/specs/project-continuity.json"), "utf8")));
|
|
156
|
+
check("continuity_policy_keeps_contract_version", JSON.parse(fs.readFileSync(path.join(repo, "graph.json"), "utf8")).extensions["mirai.project_technology"].contract_version === "1.0.0");
|
|
157
|
+
|
|
158
|
+
const bootstrapRepo = path.join(root, "existing-code-new-graph");
|
|
159
|
+
fs.mkdirSync(bootstrapRepo, { recursive: true });
|
|
160
|
+
fs.writeFileSync(path.join(bootstrapRepo, "README.md"), "# Existing code\n");
|
|
161
|
+
git(bootstrapRepo, "init", "-q"); git(bootstrapRepo, "config", "user.name", "Mirai Fixture"); git(bootstrapRepo, "config", "user.email", "fixture@example.invalid");
|
|
162
|
+
git(bootstrapRepo, "add", "README.md"); git(bootstrapRepo, "commit", "-qm", "existing code");
|
|
163
|
+
writeJson(path.join(bootstrapRepo, "graph.json"), manifest("bootstrap-project"));
|
|
164
|
+
writeJson(path.join(bootstrapRepo, "graph/specs/objects.json"), [{ id: "goal.bootstrap", kind: "goal", title: "Bootstrap", summary: "Use content-bound bootstrap context", readiness: "accepted", profile: "project_management" }]);
|
|
165
|
+
writeJson(path.join(bootstrapRepo, "graph/specs/relations.json"), []);
|
|
166
|
+
const bootstrapContext = technology.context(bootstrapRepo, { phase: "discover", task: "bootstrap context" });
|
|
167
|
+
check("new_git_graph_is_content_bound_before_first_commit", bootstrapContext.status === "success" && String(bootstrapContext.traversal_receipt.graph.revision).startsWith("content:"), bootstrapContext.blockers);
|
|
168
|
+
|
|
169
|
+
process.stdout.write(`${JSON.stringify({ status: "success", checks_passed: checks.length, checks_failed: 0, checks }, null, 2)}\n`);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
process.stdout.write(`${JSON.stringify({ status: "fail", checks_passed: checks.filter((item) => item.passed).length, checks_failed: 1, checks, error: error.message }, null, 2)}\n`);
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
}
|
|
@@ -72,6 +72,18 @@ function executionContract() {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function continuityEvidence(repositoryId) {
|
|
76
|
+
return {
|
|
77
|
+
task_digest: technology.sha256(`task:${repositoryId}`, true),
|
|
78
|
+
outcome: `Exact target binding verified for ${repositoryId}`,
|
|
79
|
+
requirement_refs: ["requirement.fixture.runtime"],
|
|
80
|
+
evidence_refs: ["repo://fixture/evidence/project-technology"],
|
|
81
|
+
checks: [{ id: "check.fixture.binding", verdict: "pass", evidence_ref: "repo://fixture/evidence/project-technology" }],
|
|
82
|
+
changed_surfaces: ["graph/specs/project-continuity.json"],
|
|
83
|
+
case_signature: "case.fixture.target-binding",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
function initRepo(root, name, scope = "repository", profiles) {
|
|
76
88
|
const repo = path.join(root, name); fs.mkdirSync(repo, { recursive: true });
|
|
77
89
|
git(repo, "init", "-q"); git(repo, "config", "user.name", "Mirai Fixture");
|
|
@@ -104,6 +116,7 @@ function check(id, condition, details = null) {
|
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mirai-project-technology-test-"));
|
|
119
|
+
process.env.MIRAI_GRAPH_STATE_ROOT = path.join(root, "host-state");
|
|
107
120
|
try {
|
|
108
121
|
const provider = initRepo(root, "fixture-provider", "repository");
|
|
109
122
|
const consumer = initRepo(root, "fixture-consumer", "repository");
|
|
@@ -112,6 +125,11 @@ try {
|
|
|
112
125
|
const federation = initRepo(root, "fixture-federation", "federation", ["project_management", "implementation_control"]);
|
|
113
126
|
const providerRevision = addTarget(provider);
|
|
114
127
|
|
|
128
|
+
const legacyRuntimeRepo = initRepo(root, "legacy-runtime-repository");
|
|
129
|
+
writeJson(path.join(legacyRuntimeRepo, ".mirai-graph/project-technology/inventory.json"), { schema_version: "1.0.0", repository_id: "legacy-runtime-repository", inventory_digest: `sha256:${"0".repeat(64)}`, files: [] });
|
|
130
|
+
const legacyRuntimeEnabled = technology.execute("enable", legacyRuntimeRepo, { apply: true });
|
|
131
|
+
check("project_local_runtime_moves_to_host_state", legacyRuntimeEnabled.status === "success" && legacyRuntimeEnabled.migration_ref.startsWith("host-local://") && fs.existsSync(path.join(legacyRuntimeRepo, ".mirai-graph/project-technology/inventory.json")), legacyRuntimeEnabled);
|
|
132
|
+
|
|
115
133
|
const before = fs.readFileSync(path.join(consumer, "graph.json"));
|
|
116
134
|
const preview = technology.execute("enable", consumer, { apply: false });
|
|
117
135
|
check("preview_is_zero_write", preview.status === "preview" && fs.readFileSync(path.join(consumer, "graph.json")).equals(before));
|
|
@@ -147,12 +165,18 @@ try {
|
|
|
147
165
|
apply: true, source: exportPath, targetId: TARGET_ID,
|
|
148
166
|
semanticDigest: SEMANTIC_DIGEST, providerRevision,
|
|
149
167
|
});
|
|
150
|
-
|
|
168
|
+
const consumerContinuity = technology.execute("sync", consumer, {
|
|
169
|
+
apply: true, boundary: "task_complete", continuityEvidence: continuityEvidence("fixture-consumer"),
|
|
170
|
+
});
|
|
171
|
+
check("consumer_binding_ready", connected.status === "success" && consumerContinuity.status === "success" && technology.verify(consumer, { significantWork: true }).status === "success", { connect: connected.blockers, continuity: consumerContinuity.blockers });
|
|
151
172
|
const connectedTwo = technology.execute("connect", consumerTwo, {
|
|
152
173
|
apply: true, source: exportPath, targetId: TARGET_ID,
|
|
153
174
|
semanticDigest: SEMANTIC_DIGEST, providerRevision,
|
|
154
175
|
});
|
|
155
|
-
|
|
176
|
+
const consumerTwoContinuity = technology.execute("sync", consumerTwo, {
|
|
177
|
+
apply: true, boundary: "task_complete", continuityEvidence: continuityEvidence("fixture-consumer-two"),
|
|
178
|
+
});
|
|
179
|
+
check("second_consumer_binding_ready", connectedTwo.status === "success" && consumerTwoContinuity.status === "success" && technology.verify(consumerTwo, { significantWork: true }).status === "success", { connect: connectedTwo.blockers, continuity: consumerTwoContinuity.blockers });
|
|
156
180
|
const repeated = technology.execute("connect", consumer, {
|
|
157
181
|
apply: true, source: exportPath, targetId: TARGET_ID,
|
|
158
182
|
semanticDigest: SEMANTIC_DIGEST, providerRevision,
|
|
@@ -63,18 +63,27 @@ function trackedState(repo) {
|
|
|
63
63
|
const trackedRun = spawnSync("git", ["ls-files", "-z"], { cwd: repo, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
64
64
|
const dirtyRun = spawnSync("git", ["diff", "--name-only", "-z", "HEAD", "--"], { cwd: repo, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
65
65
|
return {
|
|
66
|
+
hasGit: Boolean(git(repo, "rev-parse", "--is-inside-work-tree")),
|
|
66
67
|
tracked: new Set(trackedRun.status === 0 ? trackedRun.stdout.split("\0").filter(Boolean) : []),
|
|
67
68
|
dirty: new Set(dirtyRun.status === 0 ? dirtyRun.stdout.split("\0").filter(Boolean) : []),
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
function graphBlob(graph, relative) {
|
|
73
|
+
if (!graph.hasGit) {
|
|
74
|
+
const absolute = path.join(graph.repo, relative);
|
|
75
|
+
return fs.existsSync(absolute) && !fs.lstatSync(absolute).isSymbolicLink() && fs.statSync(absolute).isFile() ? fs.readFileSync(absolute) : null;
|
|
76
|
+
}
|
|
72
77
|
if (!graph.tracked?.has(relative) || graph.dirty?.has(relative)) return null;
|
|
73
78
|
if (!graph.blobCache.has(relative)) graph.blobCache.set(relative, gitBlob(graph.repo, relative));
|
|
74
79
|
return graph.blobCache.get(relative);
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
function revisionBound(repo, relative) {
|
|
83
|
+
if (!git(repo, "rev-parse", "--is-inside-work-tree")) {
|
|
84
|
+
const absolute = path.join(repo, relative);
|
|
85
|
+
return fs.existsSync(absolute) && !fs.lstatSync(absolute).isSymbolicLink() && fs.statSync(absolute).isFile();
|
|
86
|
+
}
|
|
78
87
|
if (!gitBlob(repo, relative)) return false;
|
|
79
88
|
return spawnSync("git", ["diff", "--quiet", "HEAD", "--", relative], { cwd: repo }).status === 0;
|
|
80
89
|
}
|
|
@@ -197,19 +206,25 @@ function readGraph(repoArg) {
|
|
|
197
206
|
if (!objects.has(relation.source) || !objects.has(relation.target)) { output.blockers.push("relation_endpoint_missing"); continue; }
|
|
198
207
|
relations.push({ ...relation, _record: record });
|
|
199
208
|
}
|
|
200
|
-
const revision = git(repo, "rev-parse", "HEAD") || null;
|
|
201
209
|
const state = trackedState(repo);
|
|
202
|
-
|
|
210
|
+
const graphSources = unique([
|
|
203
211
|
"graph.json",
|
|
204
212
|
...[...objects.values()].map((record) => record.relative),
|
|
205
213
|
...relations.map((relation) => relation._record.relative),
|
|
206
|
-
])
|
|
207
|
-
const
|
|
214
|
+
]);
|
|
215
|
+
const newContentBoundGraph = state.hasGit
|
|
216
|
+
&& graphSources.length > 0
|
|
217
|
+
&& graphSources.every((relative) => !state.tracked.has(relative))
|
|
218
|
+
&& graphSources.every((relative) => !state.dirty.has(relative));
|
|
219
|
+
for (const relative of graphSources) if (state.hasGit && !newContentBoundGraph && (!state.tracked.has(relative) || state.dirty.has(relative))) output.blockers.push("graph_source_not_revision_bound");
|
|
220
|
+
const gitRevision = !newContentBoundGraph ? git(repo, "rev-parse", "HEAD") || null : null;
|
|
221
|
+
const graphPayloadBase = {
|
|
208
222
|
manifest: { id: manifest.id, scope: manifest.scope, profiles: manifest.profiles, graph: manifest.graph },
|
|
209
223
|
objects: [...objects.values()].map((record) => record.value).sort((a, b) => a.id.localeCompare(b.id)),
|
|
210
224
|
relations: relations.map(({ _record, ...relation }) => relation).sort((a, b) => a.id.localeCompare(b.id)),
|
|
211
|
-
revision,
|
|
212
225
|
};
|
|
226
|
+
const revision = gitRevision || `content:${digest(graphPayloadBase).slice(7)}`;
|
|
227
|
+
const graphPayload = { ...graphPayloadBase, revision };
|
|
213
228
|
return {
|
|
214
229
|
repo,
|
|
215
230
|
manifest,
|
|
@@ -219,6 +234,7 @@ function readGraph(repoArg) {
|
|
|
219
234
|
relations,
|
|
220
235
|
tracked: state.tracked,
|
|
221
236
|
dirty: state.dirty,
|
|
237
|
+
hasGit: state.hasGit,
|
|
222
238
|
blobCache: new Map(),
|
|
223
239
|
blockers: unique(output.blockers),
|
|
224
240
|
};
|
|
@@ -638,7 +654,7 @@ function verifyContext(repository, contextPack, usageEvidence, options = {}) {
|
|
|
638
654
|
const absolute = path.join(graph.repo, source.ref);
|
|
639
655
|
if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink() || !fs.statSync(absolute).isFile()) blockers.push("context_pack_source_unavailable");
|
|
640
656
|
else {
|
|
641
|
-
const blob =
|
|
657
|
+
const blob = graphBlob(graph, source.ref);
|
|
642
658
|
if (!blob || !revisionBound(graph.repo, source.ref) || digest(blob) !== source.sha256 || source.revision !== graph.revision) blockers.push("context_pack_source_revision_or_digest_mismatch");
|
|
643
659
|
}
|
|
644
660
|
}
|