mirai-graph 1.2.0 → 1.4.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/CHANGELOG.md +52 -0
- package/README.md +33 -2
- package/docs/adoption/cli.md +13 -0
- package/examples/executable-technology-course/README.md +16 -0
- package/examples/executable-technology-course/technology.json +99 -0
- package/package.json +5 -3
- package/packages/cli/mirai-graph.js +1 -1
- package/packages/cli/project-technology.js +30 -2
- package/packages/cli/validate-artifact-releases.js +282 -0
- package/packages/cli/validate-technology-course.js +81 -0
- package/packages/project-technology/artifact-release.js +738 -0
- package/packages/project-technology/index.js +12 -0
- package/packages/project-technology/technology-course.js +282 -0
- package/releases/1.3.0.md +16 -0
- package/releases/README.md +1 -0
- package/schemas/executable-technology.schema.json +70 -0
- package/schemas/technology-course-pack.schema.json +57 -0
- package/standard/project-technology.md +74 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,58 @@ All notable changes to Mirai Graph will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.4.0] - 2026-08-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A domain-neutral executable-technology contract for large, ordered methods
|
|
12
|
+
whose scenarios reuse independently executable operations.
|
|
13
|
+
- Deterministic Course Pack compilation with required dependency closure,
|
|
14
|
+
exact source binding and partial courses for one scenario.
|
|
15
|
+
- Read-only `technology course compile`, `verify` and `reconcile` operations in
|
|
16
|
+
the public CLI and JavaScript API.
|
|
17
|
+
- Universal positive and negative fixtures for course projection.
|
|
18
|
+
|
|
19
|
+
### Safety Boundary
|
|
20
|
+
|
|
21
|
+
- A course is a projection, not another source of truth. Reconciliation never
|
|
22
|
+
writes to the executable technology and semantic changes require an owner
|
|
23
|
+
decision.
|
|
24
|
+
- Missing operations, dependency cycles, stale digests and secret fields fail
|
|
25
|
+
closed.
|
|
26
|
+
- The public `graph.json` schema remains `2.0.0`; the Project Technology
|
|
27
|
+
activation contract remains `1.0.0` and no new profile is introduced.
|
|
28
|
+
|
|
29
|
+
## [1.3.0] - 2026-08-28
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Generic immutable artifact releases in the existing Project Technology
|
|
34
|
+
engine: inspect, release, compare and verify.
|
|
35
|
+
- Public JavaScript API and matching CLI for direct files, directories, ZIP,
|
|
36
|
+
TAR and TAR.GZ bundles.
|
|
37
|
+
- Hash-bound manifests, release lineage, technical comparisons, deterministic
|
|
38
|
+
client-safe exports and portable metadata in `graph/specs`.
|
|
39
|
+
- Transactional release creation with lease, compare-and-swap, atomic
|
|
40
|
+
activation, readback, rollback and idempotent repeat behavior.
|
|
41
|
+
|
|
42
|
+
### Safety Boundary
|
|
43
|
+
|
|
44
|
+
- Unsafe paths, links, encrypted or nested archives, executable and
|
|
45
|
+
macro-enabled files, normalized duplicates and archive-limit violations fail
|
|
46
|
+
closed.
|
|
47
|
+
- Raw artifacts and private document content do not enter graph metadata.
|
|
48
|
+
- Artifact integrity is not domain approval; consumers retain semantic
|
|
49
|
+
ownership.
|
|
50
|
+
- The public graph schema remains `2.0.0` and the Project Technology
|
|
51
|
+
activation contract remains `1.0.0`.
|
|
52
|
+
|
|
53
|
+
### Verification
|
|
54
|
+
|
|
55
|
+
- New fixtures cover immutable releases, multiple matters, branching,
|
|
56
|
+
comparisons, tamper detection, concurrency, CAS conflicts, safe archives,
|
|
57
|
+
unsupported formats, read-only zero-write and shared folders.
|
|
58
|
+
|
|
7
59
|
## [1.2.0] - 2026-08-27
|
|
8
60
|
|
|
9
61
|
### Added
|
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.3.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 provides the stable `mirai-graph@1.
|
|
45
|
+
The repository provides the stable `mirai-graph@1.3.0` package. To verify a
|
|
46
46
|
repository checkout directly, use:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
@@ -142,6 +142,37 @@ Accepted facts and reusable cases live in `graph/specs`; mutable receipts and
|
|
|
142
142
|
rollback data stay host-local. The same contract works in Git repositories and
|
|
143
143
|
ordinary shared folders.
|
|
144
144
|
|
|
145
|
+
Project Technology can also preserve immutable releases of file bundles:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
mirai-graph technology artifact inspect . --input incoming.zip
|
|
149
|
+
mirai-graph technology artifact release . --input incoming.zip --matter-id agreement-main --direction inbound
|
|
150
|
+
mirai-graph technology artifact release . --input incoming.zip --matter-id agreement-main --direction inbound --apply
|
|
151
|
+
mirai-graph technology artifact compare . --matter-id agreement-main --base-release 20260828-01 --target-release 20260828-02
|
|
152
|
+
mirai-graph technology artifact verify . --matter-id agreement-main --release-id 20260828-02
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The protected files remain outside graph data. The graph keeps only safe
|
|
156
|
+
identity, lineage, state, references and checksums. Mirai Graph performs
|
|
157
|
+
technical integrity checks; a domain owner decides what a version means.
|
|
158
|
+
|
|
159
|
+
Large operational methods can be described once as executable technologies and
|
|
160
|
+
compiled into staff courses without maintaining a second semantic copy:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
mirai-graph technology course compile . --technology graph/specs/technology.json
|
|
164
|
+
mirai-graph technology course compile . --technology graph/specs/technology.json --scenario scenario.recovery
|
|
165
|
+
mirai-graph technology course verify . --course-pack course-pack.json
|
|
166
|
+
mirai-graph technology course reconcile . --course-pack course-pack.json --projection edited-course.json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
An executable technology contains reusable operations, their prerequisites,
|
|
170
|
+
inputs, outputs, stop conditions, rollback paths, checks and raw source refs.
|
|
171
|
+
Scenarios select the operations needed for a complete or partial outcome.
|
|
172
|
+
Compilation restores every required dependency. Reconciliation is read-only:
|
|
173
|
+
editorial differences go to the documentation owner, while changed execution
|
|
174
|
+
or safety rules become proposals to the technology owner.
|
|
175
|
+
|
|
145
176
|
Changing operations return a preview unless `--apply` is explicit. See
|
|
146
177
|
[Project Technology](standard/project-technology.md) for provider/consumer
|
|
147
178
|
binding, local state and safety boundaries.
|
package/docs/adoption/cli.md
CHANGED
|
@@ -91,6 +91,19 @@ npx mirai-graph technology context . --phase compile --input receipt.json --sele
|
|
|
91
91
|
npx mirai-graph technology context . --phase verify --packet context-pack.json --evidence usage-evidence.json
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
Versioned file bundles use the same Project Technology entrypoint:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx mirai-graph technology artifact inspect . --input incoming.zip
|
|
98
|
+
npx mirai-graph technology artifact release . --input incoming.zip --matter-id agreement-main --direction inbound
|
|
99
|
+
npx mirai-graph technology artifact release . --input incoming.zip --matter-id agreement-main --direction inbound --apply
|
|
100
|
+
npx mirai-graph technology artifact compare . --matter-id agreement-main --base-release 20260828-01 --target-release 20260828-02
|
|
101
|
+
npx mirai-graph technology artifact verify . --matter-id agreement-main --release-id 20260828-02
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Inspection, comparison and verification are read-only. Release creation is a
|
|
105
|
+
preview until `--apply` is present.
|
|
106
|
+
|
|
94
107
|
Omitting `--phase` keeps the original bounded context-discovery behavior.
|
|
95
108
|
Receipt, selection, context-pack and usage-evidence files are caller-owned
|
|
96
109
|
inputs and outputs; the command never stores them itself.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Executable Technology Course
|
|
2
|
+
|
|
3
|
+
This domain-neutral fixture demonstrates one accepted executable technology,
|
|
4
|
+
two independently usable scenarios and a deterministic instructional course
|
|
5
|
+
projection. The recovery scenario automatically includes its required
|
|
6
|
+
inventory operation.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
mirai-graph technology course compile . \
|
|
10
|
+
--technology examples/executable-technology-course/technology.json \
|
|
11
|
+
--scenario scenario.recovery_preparation
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Course packs are read-only projections. Their reconciliation result can return
|
|
15
|
+
editorial changes, but semantic changes remain owner proposals and never write
|
|
16
|
+
back into the executable technology.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0.0",
|
|
3
|
+
"id": "technology.service_release",
|
|
4
|
+
"title": "Safe service release",
|
|
5
|
+
"owner": "role.release_owner",
|
|
6
|
+
"outcome": "A verified service version is promoted with a tested recovery path.",
|
|
7
|
+
"lifecycle": "accepted",
|
|
8
|
+
"version": "2026.08",
|
|
9
|
+
"source_refs": ["repo://example/operations/release.md"],
|
|
10
|
+
"projection_refs": ["docs://staff-course/service-release"],
|
|
11
|
+
"operations": [
|
|
12
|
+
{
|
|
13
|
+
"id": "operation.inventory",
|
|
14
|
+
"title": "Inventory the current state",
|
|
15
|
+
"summary": "Record exact versions, dependencies and available environments before change.",
|
|
16
|
+
"owner": "role.release_owner",
|
|
17
|
+
"capability_ref": "capability.release.inventory",
|
|
18
|
+
"prerequisites": [],
|
|
19
|
+
"input_refs": ["input.release_request"],
|
|
20
|
+
"output_refs": ["evidence.current_state"],
|
|
21
|
+
"check_refs": ["check.inventory_complete"],
|
|
22
|
+
"stop_condition_refs": ["stop.unknown_current_state"],
|
|
23
|
+
"rollback_refs": [],
|
|
24
|
+
"source_refs": ["repo://example/operations/inventory.md"],
|
|
25
|
+
"instructional_refs": ["lesson.inventory"],
|
|
26
|
+
"applicability": ["release", "recovery"],
|
|
27
|
+
"negative_boundaries": ["Do not mutate the environment during inventory."]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "operation.backup",
|
|
31
|
+
"title": "Prepare recovery",
|
|
32
|
+
"summary": "Create and verify a bounded backup and rollback procedure.",
|
|
33
|
+
"owner": "role.operations",
|
|
34
|
+
"capability_ref": "capability.release.recovery",
|
|
35
|
+
"prerequisites": ["operation.inventory"],
|
|
36
|
+
"input_refs": ["evidence.current_state"],
|
|
37
|
+
"output_refs": ["evidence.backup", "procedure.rollback"],
|
|
38
|
+
"check_refs": ["check.restore_smoke"],
|
|
39
|
+
"stop_condition_refs": ["stop.restore_not_proven"],
|
|
40
|
+
"rollback_refs": ["procedure.rollback"],
|
|
41
|
+
"source_refs": ["repo://example/operations/recovery.md"],
|
|
42
|
+
"instructional_refs": ["lesson.recovery"],
|
|
43
|
+
"applicability": ["release", "recovery"],
|
|
44
|
+
"negative_boundaries": ["A backup without a restore check is not sufficient."]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "operation.validate",
|
|
48
|
+
"title": "Validate the candidate",
|
|
49
|
+
"summary": "Run the required functional, compatibility and safety checks.",
|
|
50
|
+
"owner": "role.tester",
|
|
51
|
+
"capability_ref": "capability.release.validation",
|
|
52
|
+
"prerequisites": ["operation.inventory"],
|
|
53
|
+
"input_refs": ["input.release_candidate"],
|
|
54
|
+
"output_refs": ["evidence.validation"],
|
|
55
|
+
"check_refs": ["check.acceptance"],
|
|
56
|
+
"stop_condition_refs": ["stop.blocking_regression"],
|
|
57
|
+
"rollback_refs": [],
|
|
58
|
+
"source_refs": ["repo://example/operations/validation.md"],
|
|
59
|
+
"instructional_refs": ["lesson.validation"],
|
|
60
|
+
"applicability": ["release"],
|
|
61
|
+
"negative_boundaries": ["A green syntax check is not an outcome verdict."]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "operation.promote",
|
|
65
|
+
"title": "Promote the accepted version",
|
|
66
|
+
"summary": "Apply the exact accepted candidate and verify the resulting identity.",
|
|
67
|
+
"owner": "role.release_owner",
|
|
68
|
+
"capability_ref": "capability.release.promotion",
|
|
69
|
+
"prerequisites": ["operation.backup", "operation.validate"],
|
|
70
|
+
"input_refs": ["evidence.backup", "evidence.validation"],
|
|
71
|
+
"output_refs": ["evidence.release_readback"],
|
|
72
|
+
"check_refs": ["check.release_identity", "check.runtime_smoke"],
|
|
73
|
+
"stop_condition_refs": ["stop.identity_mismatch"],
|
|
74
|
+
"rollback_refs": ["procedure.rollback"],
|
|
75
|
+
"source_refs": ["repo://example/operations/promotion.md"],
|
|
76
|
+
"instructional_refs": ["lesson.promotion"],
|
|
77
|
+
"applicability": ["release"],
|
|
78
|
+
"negative_boundaries": ["Do not promote a candidate that differs from the accepted identity."]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"scenarios": [
|
|
82
|
+
{
|
|
83
|
+
"id": "scenario.full_release",
|
|
84
|
+
"title": "Full release",
|
|
85
|
+
"outcome": "The accepted service version is released and verified.",
|
|
86
|
+
"operation_ids": ["operation.inventory", "operation.backup", "operation.validate", "operation.promote"],
|
|
87
|
+
"required_inputs": ["input.release_request", "input.release_candidate"],
|
|
88
|
+
"audience": ["release_manager", "operations_engineer"]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "scenario.recovery_preparation",
|
|
92
|
+
"title": "Recovery preparation only",
|
|
93
|
+
"outcome": "A verified rollback path exists without performing a release.",
|
|
94
|
+
"operation_ids": ["operation.backup"],
|
|
95
|
+
"required_inputs": ["input.release_request"],
|
|
96
|
+
"audience": ["operations_engineer"]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirai-graph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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,11 +13,13 @@
|
|
|
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 test:project-continuity && 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 test:artifact-releases && npm run test:technology-courses && 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
19
|
"test:project-continuity": "node packages/cli/validate-project-continuity.js",
|
|
20
|
-
"
|
|
20
|
+
"test:artifact-releases": "node packages/cli/validate-artifact-releases.js",
|
|
21
|
+
"test:technology-courses": "node packages/cli/validate-technology-course.js",
|
|
22
|
+
"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','executable-technology.schema.json','technology-course-pack.schema.json']) JSON.parse(fs.readFileSync('schemas/'+file))\"",
|
|
21
23
|
"validate:graph-manifest": "node packages/cli/validate-graph-manifest-migration.js",
|
|
22
24
|
"validate:minimal": "node packages/cli/validate-mirai-graph.js examples/minimal-graph",
|
|
23
25
|
"validate:benchmark": "node packages/cli/validate-mirai-graph.js benchmarks/synthetic-context-reduction-v0",
|
|
@@ -25,7 +25,7 @@ function usage() {
|
|
|
25
25
|
console.error(" mirai-graph report playground");
|
|
26
26
|
console.error(" mirai-graph release state [--markdown] [--require-git-tag] [--require-github-release] [--require-npm-published]");
|
|
27
27
|
console.error(" mirai-graph technology <explain|status|plan|verify|context> [repository] [options]");
|
|
28
|
-
console.error(" mirai-graph technology <enable|sync|connect|disconnect|provide|disable|repair> [repository] [options] [--apply]");
|
|
28
|
+
console.error(" mirai-graph technology <enable|sync|connect|disconnect|provide|disable|repair|artifact|course> [repository] [options] [--apply]");
|
|
29
29
|
console.error("");
|
|
30
30
|
console.error("Boundary:");
|
|
31
31
|
console.error(" Reports explain validation decisions. They do not authorize canonical updates.");
|
|
@@ -19,16 +19,33 @@ function usage() {
|
|
|
19
19
|
console.error(" --phase expand --input <receipt.json> --select <id> [--select <id>]");
|
|
20
20
|
console.error(" --phase compile --input <receipt.json> --selection <selection.json>");
|
|
21
21
|
console.error(" --phase verify --packet <context-pack.json> --evidence <usage-evidence.json>");
|
|
22
|
+
console.error("Artifacts: artifact inspect <repo> --input <path>");
|
|
23
|
+
console.error(" artifact release <repo> --input <path> --matter-id <id> --direction inbound|internal|outbound [--parent-release <id>] [--client-note <text>] [--create-export] [--apply]");
|
|
24
|
+
console.error(" artifact compare <repo> --matter-id <id> --base-release <id> --target-release <id>");
|
|
25
|
+
console.error(" artifact verify <repo> --matter-id <id> --release-id <id>");
|
|
26
|
+
console.error("Courses: course compile <repo> --technology <file> [--scenario <id>] [--audience <id>]");
|
|
27
|
+
console.error(" course verify <repo> --course-pack <file>");
|
|
28
|
+
console.error(" course reconcile <repo> --course-pack <file> --projection <file>");
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
function parse(argv) {
|
|
25
32
|
if (!argv.length) return null;
|
|
26
33
|
const operation = argv.shift();
|
|
34
|
+
let artifactAction = null;
|
|
35
|
+
let courseAction = null;
|
|
36
|
+
if (operation === "artifact") {
|
|
37
|
+
artifactAction = argv.shift() || null;
|
|
38
|
+
if (!new Set(["inspect", "release", "compare", "verify"]).has(artifactAction)) throw new Error("artifact requires inspect, release, compare or verify");
|
|
39
|
+
}
|
|
40
|
+
if (operation === "course") {
|
|
41
|
+
courseAction = argv.shift() || null;
|
|
42
|
+
if (!new Set(["compile", "verify", "reconcile"]).has(courseAction)) throw new Error("course requires compile, verify or reconcile");
|
|
43
|
+
}
|
|
27
44
|
let repository = ".";
|
|
28
45
|
if (argv[0] && !argv[0].startsWith("--")) repository = argv.shift();
|
|
29
46
|
const options = {};
|
|
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"]);
|
|
31
|
-
const flags = new Set(["--apply", "--refresh-binding", "--significant-work"]);
|
|
47
|
+
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
|
+
const flags = new Set(["--apply", "--refresh-binding", "--significant-work", "--create-export"]);
|
|
32
49
|
const names = {
|
|
33
50
|
"--task": "task", "--source": "source", "--target-id": "targetId",
|
|
34
51
|
"--semantic-digest": "semanticDigest", "--provider-revision": "providerRevision",
|
|
@@ -40,6 +57,13 @@ function parse(argv) {
|
|
|
40
57
|
"--context-budget": "contextBudget",
|
|
41
58
|
"--boundary": "boundary", "--expected-graph-digest": "expectedGraphDigest",
|
|
42
59
|
"--receipt-digest": "receiptDigest", "--state-root": "stateRoot",
|
|
60
|
+
"--matter-id": "matterId", "--direction": "direction", "--artifact-root": "artifactRoot",
|
|
61
|
+
"--release-date": "releaseDate", "--release-id": "releaseId",
|
|
62
|
+
"--parent-release": "parentReleaseIds", "--base-release": "baseReleaseId",
|
|
63
|
+
"--target-release": "targetReleaseId", "--client-note": "clientNote",
|
|
64
|
+
"--create-export": "createExport",
|
|
65
|
+
"--technology": "technologyFile", "--scenario": "scenarioIds",
|
|
66
|
+
"--audience": "audience", "--course-pack": "coursePackFile", "--projection": "projectionFile",
|
|
43
67
|
};
|
|
44
68
|
while (argv.length) {
|
|
45
69
|
const token = argv.shift();
|
|
@@ -48,9 +72,13 @@ function parse(argv) {
|
|
|
48
72
|
if (!argv.length) throw new Error(`missing value for ${token}`);
|
|
49
73
|
const value = argv.shift();
|
|
50
74
|
if (token === "--select") options.selectedIds = [...(options.selectedIds || []), value];
|
|
75
|
+
else if (token === "--parent-release") options.parentReleaseIds = [...(options.parentReleaseIds || []), value];
|
|
76
|
+
else if (token === "--scenario") options.scenarioIds = [...(options.scenarioIds || []), value];
|
|
51
77
|
else options[names[token]] = value;
|
|
52
78
|
} else throw new Error(`unsupported argument ${token}`);
|
|
53
79
|
}
|
|
80
|
+
if (artifactAction) options.artifactAction = artifactAction;
|
|
81
|
+
if (courseAction) options.courseAction = courseAction;
|
|
54
82
|
return { operation, repository: path.resolve(repository), options };
|
|
55
83
|
}
|
|
56
84
|
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const zlib = require("zlib");
|
|
10
|
+
const technology = require("../project-technology");
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
const failures = [];
|
|
14
|
+
function check(id, condition, detail = null) {
|
|
15
|
+
if (condition) passed += 1;
|
|
16
|
+
else failures.push({ id, detail });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeJson(file, value) {
|
|
20
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
21
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function repository(root, id) {
|
|
25
|
+
const repo = path.join(root, id);
|
|
26
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
27
|
+
writeJson(path.join(repo, "graph.json"), {
|
|
28
|
+
$schema: "https://mirai-graph.dev/schemas/graph-manifest.schema.json",
|
|
29
|
+
format: "mirai-graph",
|
|
30
|
+
schema_version: "2.0.0",
|
|
31
|
+
id,
|
|
32
|
+
aliases: [],
|
|
33
|
+
title: id,
|
|
34
|
+
scope: "repository",
|
|
35
|
+
kind: "project_graph",
|
|
36
|
+
owner: "fixture",
|
|
37
|
+
profiles: [],
|
|
38
|
+
graph: { root: "graph", source_of_truth: ["graph/specs"], objects: [], relations: [], schemas: [], generated: [], raw_sources: [] },
|
|
39
|
+
imports: [],
|
|
40
|
+
extensions: { "mirai.project_technology": { contract_version: "1.0.0", enabled: true, context_policy: "task_scoped", source_boundary: "hybrid_sot" } },
|
|
41
|
+
});
|
|
42
|
+
return repo;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function octal(value, width) {
|
|
46
|
+
return Buffer.from(value.toString(8).padStart(width - 1, "0") + "\0");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tar(entries) {
|
|
50
|
+
const output = [];
|
|
51
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
52
|
+
const bytes = Buffer.from(content);
|
|
53
|
+
const header = Buffer.alloc(512);
|
|
54
|
+
Buffer.from(name).copy(header, 0, 0, 100);
|
|
55
|
+
octal(0o600, 8).copy(header, 100);
|
|
56
|
+
octal(0, 8).copy(header, 108);
|
|
57
|
+
octal(0, 8).copy(header, 116);
|
|
58
|
+
octal(bytes.length, 12).copy(header, 124);
|
|
59
|
+
octal(0, 12).copy(header, 136);
|
|
60
|
+
Buffer.from(" ").copy(header, 148);
|
|
61
|
+
header[156] = "0".charCodeAt(0);
|
|
62
|
+
Buffer.from("ustar\0").copy(header, 257);
|
|
63
|
+
const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
|
64
|
+
octal(checksum, 8).copy(header, 148);
|
|
65
|
+
output.push(header, bytes, Buffer.alloc((512 - (bytes.length % 512)) % 512));
|
|
66
|
+
}
|
|
67
|
+
output.push(Buffer.alloc(1024));
|
|
68
|
+
return Buffer.concat(output);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function rawZip(entries) {
|
|
72
|
+
const localParts = [];
|
|
73
|
+
const centralParts = [];
|
|
74
|
+
let offset = 0;
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const name = Buffer.from(entry.name);
|
|
77
|
+
const content = Buffer.from(entry.content || "");
|
|
78
|
+
const compressed = zlib.deflateRawSync(content);
|
|
79
|
+
const flags = entry.flags || 0x0800;
|
|
80
|
+
const local = Buffer.alloc(30);
|
|
81
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
82
|
+
local.writeUInt16LE(20, 4);
|
|
83
|
+
local.writeUInt16LE(flags, 6);
|
|
84
|
+
local.writeUInt16LE(8, 8);
|
|
85
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
86
|
+
local.writeUInt32LE(entry.claimedSize || content.length, 22);
|
|
87
|
+
local.writeUInt16LE(name.length, 26);
|
|
88
|
+
localParts.push(local, name, compressed);
|
|
89
|
+
const central = Buffer.alloc(46);
|
|
90
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
91
|
+
central.writeUInt16LE(0x0314, 4);
|
|
92
|
+
central.writeUInt16LE(20, 6);
|
|
93
|
+
central.writeUInt16LE(flags, 8);
|
|
94
|
+
central.writeUInt16LE(8, 10);
|
|
95
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
96
|
+
central.writeUInt32LE(entry.claimedSize || content.length, 24);
|
|
97
|
+
central.writeUInt16LE(name.length, 28);
|
|
98
|
+
central.writeUInt32LE(entry.externalAttributes || 0, 38);
|
|
99
|
+
central.writeUInt32LE(offset, 42);
|
|
100
|
+
centralParts.push(central, name);
|
|
101
|
+
offset += local.length + name.length + compressed.length;
|
|
102
|
+
}
|
|
103
|
+
const centralBytes = Buffer.concat(centralParts);
|
|
104
|
+
const end = Buffer.alloc(22);
|
|
105
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
106
|
+
end.writeUInt16LE(entries.length, 8);
|
|
107
|
+
end.writeUInt16LE(entries.length, 10);
|
|
108
|
+
end.writeUInt32LE(centralBytes.length, 12);
|
|
109
|
+
end.writeUInt32LE(offset, 16);
|
|
110
|
+
return Buffer.concat([...localParts, centralBytes, end]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function repoSnapshot(repo) {
|
|
114
|
+
const files = [];
|
|
115
|
+
function walk(current) {
|
|
116
|
+
for (const name of fs.readdirSync(current).sort()) {
|
|
117
|
+
const absolute = path.join(current, name);
|
|
118
|
+
const stat = fs.lstatSync(absolute);
|
|
119
|
+
if (stat.isDirectory()) walk(absolute);
|
|
120
|
+
else if (stat.isFile()) files.push([path.relative(repo, absolute), crypto.createHash("sha256").update(fs.readFileSync(absolute)).digest("hex")]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
walk(repo);
|
|
124
|
+
return JSON.stringify(files);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mirai-artifact-release-"));
|
|
128
|
+
const repo = repository(root, "artifact-project");
|
|
129
|
+
const input = path.join(root, "incoming");
|
|
130
|
+
fs.mkdirSync(input);
|
|
131
|
+
fs.writeFileSync(path.join(input, "contract.txt"), "version one\n");
|
|
132
|
+
|
|
133
|
+
const inspectBefore = repoSnapshot(repo);
|
|
134
|
+
const inspected = technology.inspectArtifactBundle(repo, input);
|
|
135
|
+
check("inspect_success", inspected.status === "success" && inspected.bundle.file_count === 1, inspected);
|
|
136
|
+
check("inspect_zero_write", repoSnapshot(repo) === inspectBefore);
|
|
137
|
+
|
|
138
|
+
const preview = technology.createArtifactRelease(repo, input, {
|
|
139
|
+
matterId: "contract-main",
|
|
140
|
+
direction: "inbound",
|
|
141
|
+
artifactRoot: "documents/matters",
|
|
142
|
+
releaseDate: "20260828",
|
|
143
|
+
});
|
|
144
|
+
check("release_preview", preview.status === "preview" && preview.operation_mode === "preview" && preview.changed === false, preview);
|
|
145
|
+
check("preview_zero_write", repoSnapshot(repo) === inspectBefore);
|
|
146
|
+
|
|
147
|
+
const first = technology.createArtifactRelease(repo, input, {
|
|
148
|
+
matterId: "contract-main",
|
|
149
|
+
direction: "inbound",
|
|
150
|
+
artifactRoot: "documents/matters",
|
|
151
|
+
releaseDate: "20260828",
|
|
152
|
+
apply: true,
|
|
153
|
+
});
|
|
154
|
+
check("first_release_created", first.status === "success" && first.changed && first.release_id === "20260828-01", first);
|
|
155
|
+
check("first_release_verified", technology.verifyArtifactRelease(repo, first.release_id, { matterId: "contract-main" }).status === "success");
|
|
156
|
+
|
|
157
|
+
const repeated = technology.createArtifactRelease(repo, input, {
|
|
158
|
+
matterId: "contract-main",
|
|
159
|
+
direction: "inbound",
|
|
160
|
+
artifactRoot: "documents/matters",
|
|
161
|
+
releaseDate: "20260828",
|
|
162
|
+
apply: true,
|
|
163
|
+
});
|
|
164
|
+
check("same_release_idempotent", repeated.status === "success" && repeated.changed === false && repeated.release_id === first.release_id, repeated);
|
|
165
|
+
|
|
166
|
+
fs.writeFileSync(path.join(input, "contract.txt"), "version two\n");
|
|
167
|
+
fs.writeFileSync(path.join(input, "appendix.txt"), "appendix\n");
|
|
168
|
+
const second = technology.createArtifactRelease(repo, input, {
|
|
169
|
+
matterId: "contract-main",
|
|
170
|
+
direction: "outbound",
|
|
171
|
+
artifactRoot: "documents/matters",
|
|
172
|
+
releaseDate: "20260828",
|
|
173
|
+
parentReleaseIds: [first.release_id],
|
|
174
|
+
clientNote: "Обновлены условия и добавлено приложение.",
|
|
175
|
+
createExport: true,
|
|
176
|
+
apply: true,
|
|
177
|
+
});
|
|
178
|
+
check("second_release_numbered", second.status === "success" && second.release_id === "20260828-02", second);
|
|
179
|
+
check("client_export_created", Boolean(second.export_ref && fs.existsSync(path.join(repo, second.export_ref))), second);
|
|
180
|
+
const compared = technology.compareArtifactReleases(repo, first.release_id, second.release_id, { matterId: "contract-main" });
|
|
181
|
+
check("comparison_detects_changes", compared.status === "success" && compared.comparison.added.includes("appendix.txt") && compared.comparison.changed.includes("contract.txt"), compared);
|
|
182
|
+
|
|
183
|
+
const readOnlySnapshot = repoSnapshot(repo);
|
|
184
|
+
check("verify_success", technology.verifyArtifactRelease(repo, second.release_id, { matterId: "contract-main" }).status === "success");
|
|
185
|
+
check("compare_and_verify_zero_write", repoSnapshot(repo) === readOnlySnapshot);
|
|
186
|
+
const exportedInspect = technology.inspectArtifactBundle(repo, path.join(repo, second.export_ref));
|
|
187
|
+
check("generated_zip_is_readable", exportedInspect.status === "success" && exportedInspect.bundle.file_count === 3, exportedInspect);
|
|
188
|
+
|
|
189
|
+
const secondFile = path.join(repo, "documents/matters/contract-main/releases/20260828-02/package/contract.txt");
|
|
190
|
+
const secondBytes = fs.readFileSync(secondFile);
|
|
191
|
+
fs.writeFileSync(secondFile, "tampered\n");
|
|
192
|
+
check("tampered_file_blocked", technology.verifyArtifactRelease(repo, second.release_id, { matterId: "contract-main" }).blockers.includes("artifact_package_file_digest_mismatch"));
|
|
193
|
+
fs.writeFileSync(secondFile, secondBytes);
|
|
194
|
+
|
|
195
|
+
check("missing_parent_blocked", technology.createArtifactRelease(repo, input, {
|
|
196
|
+
matterId: "contract-main", direction: "internal", artifactRoot: "documents/matters",
|
|
197
|
+
releaseDate: "20260829", parentReleaseIds: ["20260101-01"], apply: true,
|
|
198
|
+
}).blockers.includes("artifact_parent_release_missing"));
|
|
199
|
+
check("stale_cas_blocked", technology.createArtifactRelease(repo, input, {
|
|
200
|
+
matterId: "contract-main", direction: "internal", artifactRoot: "documents/matters",
|
|
201
|
+
releaseDate: "20260829", expectedGraphDigest: "sha256:" + "0".repeat(64), apply: true,
|
|
202
|
+
}).blockers.includes("artifact_registry_compare_and_swap_conflict"));
|
|
203
|
+
|
|
204
|
+
const lease = path.join(repo, "documents/matters/contract-main/.artifact-release.lock");
|
|
205
|
+
fs.writeFileSync(lease, "busy");
|
|
206
|
+
check("concurrent_lease_blocked", technology.createArtifactRelease(repo, input, {
|
|
207
|
+
matterId: "contract-main", direction: "internal", artifactRoot: "documents/matters",
|
|
208
|
+
releaseDate: "20260829", apply: true,
|
|
209
|
+
}).blockers.includes("artifact_release_lease_conflict"));
|
|
210
|
+
fs.unlinkSync(lease);
|
|
211
|
+
|
|
212
|
+
const other = technology.createArtifactRelease(repo, input, {
|
|
213
|
+
matterId: "nda-secondary", direction: "inbound", artifactRoot: "documents/matters",
|
|
214
|
+
releaseDate: "20260828", apply: true,
|
|
215
|
+
});
|
|
216
|
+
check("multiple_matters_independent", other.status === "success" && other.release_id === "20260828-01", other);
|
|
217
|
+
|
|
218
|
+
const tarPath = path.join(root, "safe.tar");
|
|
219
|
+
fs.writeFileSync(tarPath, tar({ "folder/agreement.txt": "safe\n" }));
|
|
220
|
+
check("tar_supported", technology.inspectArtifactBundle(repo, tarPath).status === "success");
|
|
221
|
+
const tgzPath = path.join(root, "safe.tar.gz");
|
|
222
|
+
fs.writeFileSync(tgzPath, zlib.gzipSync(fs.readFileSync(tarPath)));
|
|
223
|
+
check("tar_gz_supported", technology.inspectArtifactBundle(repo, tgzPath).status === "success");
|
|
224
|
+
|
|
225
|
+
const unsafeTar = path.join(root, "unsafe.tar");
|
|
226
|
+
fs.writeFileSync(unsafeTar, tar({ "../escape.txt": "unsafe\n" }));
|
|
227
|
+
check("traversal_blocked", technology.inspectArtifactBundle(repo, unsafeTar).blockers.includes("artifact_path_unsafe"));
|
|
228
|
+
const unsafeZip = path.join(root, "unsafe.zip");
|
|
229
|
+
fs.writeFileSync(unsafeZip, rawZip([{ name: "../escape.txt", content: "unsafe" }]));
|
|
230
|
+
check("zip_traversal_blocked", technology.inspectArtifactBundle(repo, unsafeZip).blockers.includes("artifact_path_unsafe"));
|
|
231
|
+
const encryptedZip = path.join(root, "encrypted.zip");
|
|
232
|
+
fs.writeFileSync(encryptedZip, rawZip([{ name: "contract.txt", content: "secret", flags: 0x0801 }]));
|
|
233
|
+
check("encrypted_zip_blocked", technology.inspectArtifactBundle(repo, encryptedZip).blockers.includes("artifact_archive_encrypted"));
|
|
234
|
+
const duplicateZip = path.join(root, "duplicate.zip");
|
|
235
|
+
fs.writeFileSync(duplicateZip, rawZip([{ name: "Contract.txt", content: "a" }, { name: "contract.txt", content: "b" }]));
|
|
236
|
+
check("normalized_duplicate_blocked", technology.inspectArtifactBundle(repo, duplicateZip).blockers.includes("artifact_normalized_path_duplicate"));
|
|
237
|
+
const symlinkZip = path.join(root, "symlink.zip");
|
|
238
|
+
fs.writeFileSync(symlinkZip, rawZip([{ name: "contract-link", content: "target", externalAttributes: (0o120777 << 16) >>> 0 }]));
|
|
239
|
+
check("zip_symlink_blocked", technology.inspectArtifactBundle(repo, symlinkZip).blockers.includes("artifact_symlink_blocked"));
|
|
240
|
+
const bombZip = path.join(root, "bomb.zip");
|
|
241
|
+
fs.writeFileSync(bombZip, rawZip([{ name: "large.txt", content: "a", claimedSize: 50 * 1024 * 1024 }]));
|
|
242
|
+
check("compression_ratio_blocked", technology.inspectArtifactBundle(repo, bombZip).blockers.includes("artifact_compression_ratio_limit_exceeded"));
|
|
243
|
+
const executable = path.join(root, "payload.exe");
|
|
244
|
+
fs.writeFileSync(executable, "not executable");
|
|
245
|
+
check("executable_blocked", technology.inspectArtifactBundle(repo, executable).blockers.includes("artifact_executable_or_macro_blocked"));
|
|
246
|
+
const unsupported = path.join(root, "bundle.7z");
|
|
247
|
+
fs.writeFileSync(unsupported, "fixture");
|
|
248
|
+
check("unsupported_archive_blocked", technology.inspectArtifactBundle(repo, unsupported).blockers.includes("artifact_archive_provider_required"));
|
|
249
|
+
|
|
250
|
+
if (process.platform !== "win32") {
|
|
251
|
+
const symlink = path.join(root, "input-link");
|
|
252
|
+
fs.symlinkSync(input, symlink);
|
|
253
|
+
check("input_symlink_blocked", technology.inspectArtifactBundle(repo, symlink).blockers.includes("artifact_input_symlink_blocked"));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const registryText = fs.readFileSync(path.join(repo, "graph/specs/artifact-releases.json"), "utf8");
|
|
257
|
+
check("registry_has_no_host_path", !registryText.includes(root));
|
|
258
|
+
check("registry_has_no_document_content", !registryText.includes("version two") && !registryText.includes("Обновлены условия"));
|
|
259
|
+
|
|
260
|
+
const folder = repository(root, "shared-folder");
|
|
261
|
+
const folderInput = path.join(root, "folder-input.txt");
|
|
262
|
+
fs.writeFileSync(folderInput, "same bytes\n");
|
|
263
|
+
const folderRelease = technology.createArtifactRelease(folder, folderInput, {
|
|
264
|
+
matterId: "matter-one", direction: "inbound", releaseDate: "20260828", apply: true,
|
|
265
|
+
});
|
|
266
|
+
check("plain_folder_supported", folderRelease.status === "success", folderRelease);
|
|
267
|
+
const folderRepeat = technology.createArtifactRelease(folder, folderInput, {
|
|
268
|
+
matterId: "matter-one", direction: "inbound", releaseDate: "20260828", apply: true,
|
|
269
|
+
});
|
|
270
|
+
check("plain_folder_repeat_byte_identical", folderRepeat.changed === false);
|
|
271
|
+
|
|
272
|
+
const report = {
|
|
273
|
+
schema_version: "1.0.0",
|
|
274
|
+
operation_id: "mirai.project_technology.artifact_release_validation",
|
|
275
|
+
status: failures.length ? "fail" : "success",
|
|
276
|
+
passed,
|
|
277
|
+
failed: failures.length,
|
|
278
|
+
failures,
|
|
279
|
+
};
|
|
280
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
281
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
282
|
+
process.exitCode = failures.length ? 1 : 0;
|