mirai-graph 1.3.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 +22 -0
- package/README.md +17 -0
- package/examples/executable-technology-course/README.md +16 -0
- package/examples/executable-technology-course/technology.json +99 -0
- package/package.json +40 -39
- package/packages/cli/context-pack.js +0 -0
- package/packages/cli/mirai-graph.js +1 -1
- package/packages/cli/mirai_graph.js +0 -0
- package/packages/cli/project-technology.js +13 -1
- package/packages/cli/readiness-score.js +0 -0
- package/packages/cli/seed-preview.js +0 -0
- package/packages/cli/validate-mirai-graph.js +0 -0
- package/packages/cli/validate-profile-results.js +0 -0
- package/packages/cli/validate-technology-course.js +81 -0
- package/packages/project-technology/index.js +6 -0
- package/packages/project-technology/technology-course.js +282 -0
- package/schemas/executable-technology.schema.json +70 -0
- package/schemas/technology-course-pack.schema.json +57 -0
- package/standard/project-technology.md +30 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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
|
+
|
|
7
29
|
## [1.3.0] - 2026-08-28
|
|
8
30
|
|
|
9
31
|
### Added
|
package/README.md
CHANGED
|
@@ -156,6 +156,23 @@ The protected files remain outside graph data. The graph keeps only safe
|
|
|
156
156
|
identity, lineage, state, references and checksums. Mirai Graph performs
|
|
157
157
|
technical integrity checks; a domain owner decides what a version means.
|
|
158
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
|
+
|
|
159
176
|
Changing operations return a preview unless `--apply` is explicit. See
|
|
160
177
|
[Project Technology](standard/project-technology.md) for provider/consumer
|
|
161
178
|
binding, local state and safety boundaries.
|
|
@@ -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",
|
|
@@ -12,47 +12,14 @@
|
|
|
12
12
|
"exports": {
|
|
13
13
|
"./project-technology": "./packages/project-technology/index.js"
|
|
14
14
|
},
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/zabarov/mirai-graph.git"
|
|
18
|
-
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"graph",
|
|
21
|
-
"knowledge-graph",
|
|
22
|
-
"governance",
|
|
23
|
-
"complex-systems",
|
|
24
|
-
"context-engineering",
|
|
25
|
-
"ai-assisted-work"
|
|
26
|
-
],
|
|
27
|
-
"author": "Rim Zabarov",
|
|
28
|
-
"files": [
|
|
29
|
-
"README.md",
|
|
30
|
-
"CHANGELOG.md",
|
|
31
|
-
"ROADMAP.md",
|
|
32
|
-
"LICENSE",
|
|
33
|
-
"LICENSE-DOCS",
|
|
34
|
-
"CITATION.cff",
|
|
35
|
-
"docs",
|
|
36
|
-
"standard",
|
|
37
|
-
"schemas",
|
|
38
|
-
"benchmarks",
|
|
39
|
-
"examples",
|
|
40
|
-
"packages",
|
|
41
|
-
"profiles",
|
|
42
|
-
"pilots",
|
|
43
|
-
"playground",
|
|
44
|
-
"publications",
|
|
45
|
-
"releases",
|
|
46
|
-
"templates",
|
|
47
|
-
".github"
|
|
48
|
-
],
|
|
49
15
|
"scripts": {
|
|
50
|
-
"pretest": "npm run test:project-technology && npm run test:context-traversal && npm run test:project-continuity && npm run test:artifact-releases && 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",
|
|
51
17
|
"test:project-technology": "node packages/cli/validate-project-technology.js",
|
|
52
18
|
"test:context-traversal": "node packages/cli/validate-context-traversal.js",
|
|
53
19
|
"test:project-continuity": "node packages/cli/validate-project-continuity.js",
|
|
54
20
|
"test:artifact-releases": "node packages/cli/validate-artifact-releases.js",
|
|
55
|
-
"
|
|
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))\"",
|
|
56
23
|
"validate:graph-manifest": "node packages/cli/validate-graph-manifest-migration.js",
|
|
57
24
|
"validate:minimal": "node packages/cli/validate-mirai-graph.js examples/minimal-graph",
|
|
58
25
|
"validate:benchmark": "node packages/cli/validate-mirai-graph.js benchmarks/synthetic-context-reduction-v0",
|
|
@@ -123,5 +90,39 @@
|
|
|
123
90
|
"validate:playground-report": "npm run --silent playground:report >/tmp/mirai-graph-playground-report.md && diff -u playground/demo-report.md /tmp/mirai-graph-playground-report.md && rg 'transition explanation|Instrumentation|Multi-source feedback|Baseline Comparison|baseline comparison' playground/demo-report.md",
|
|
124
91
|
"test": "npm run validate:graph-manifest && npm run validate:minimal && npm run validate:benchmark && npm run validate:seed && npm run validate:profiles && npm run validate:ai-employee && npm run validate:skill-runtime && npm run validate:managed-project && npm run validate:implementation-control && npm run validate:implementation-control-cycles && npm run validate:launch-record && npm run validate:process-transition && npm run validate:process-transition-report && npm run validate:process-control && npm run validate:technology-quality-feedback && npm run validate:instrumentation-layer && npm run validate:instrumentation-report && npm run validate:dynamic-episodes && npm run validate:dynamic-episode-report && npm run validate:semantic-intent-resolution && npm run validate:goal-vector-quality-control && npm run validate:dna-alignment && npm run validate:work-state && npm run validate:risk-control && npm run validate:source-boundary && npm run validate:organization-governance && npm run validate:societal-governance && npm run validate:character-layer && npm run validate:character-layer-integration && npm run validate:model-portability-evidence && npm run validate:character-layer-readiness && npm run validate:templates && npm run validate:adopter-workflow && npm run validate:self-service-onboarding && npm run validate:init-fixtures && npm run validate:bootstrap-fixtures && npm run validate:release-state && npm run validate:mirai-graph-compat && npm run validate:cli-unified && npm run validate:pilots && npm run validate:context-packs && npm run validate:profile-results && npm run validate:routing-control && npm run seed:preview && npm run context:generate && npm run playground:demo && npm run playground:report && npm run readiness:benchmark && npm run benchmark:synthetic && npm run validate:baseline-comparison && npm run validate:markdown-report && npm run validate:playground-demo && npm run validate:playground-report && npm run test:negative && npm run test:character-layer-semantic-negative && npm run test:societal-governance-negative && npm run test:process-transition-negative && npm run test:technology-quality-feedback-negative && npm run test:instrumentation-layer-negative && npm run test:dynamic-episodes-negative && npm run test:semantic-intent-resolution-negative && npm run test:goal-vector-quality-control-negative",
|
|
125
92
|
"release:check": "npm test && git diff --check"
|
|
126
|
-
}
|
|
127
|
-
|
|
93
|
+
},
|
|
94
|
+
"repository": {
|
|
95
|
+
"type": "git",
|
|
96
|
+
"url": "git+https://github.com/zabarov/mirai-graph.git"
|
|
97
|
+
},
|
|
98
|
+
"keywords": [
|
|
99
|
+
"graph",
|
|
100
|
+
"knowledge-graph",
|
|
101
|
+
"governance",
|
|
102
|
+
"complex-systems",
|
|
103
|
+
"context-engineering",
|
|
104
|
+
"ai-assisted-work"
|
|
105
|
+
],
|
|
106
|
+
"author": "Rim Zabarov",
|
|
107
|
+
"files": [
|
|
108
|
+
"README.md",
|
|
109
|
+
"CHANGELOG.md",
|
|
110
|
+
"ROADMAP.md",
|
|
111
|
+
"LICENSE",
|
|
112
|
+
"LICENSE-DOCS",
|
|
113
|
+
"CITATION.cff",
|
|
114
|
+
"docs",
|
|
115
|
+
"standard",
|
|
116
|
+
"schemas",
|
|
117
|
+
"benchmarks",
|
|
118
|
+
"examples",
|
|
119
|
+
"packages",
|
|
120
|
+
"profiles",
|
|
121
|
+
"pilots",
|
|
122
|
+
"playground",
|
|
123
|
+
"publications",
|
|
124
|
+
"releases",
|
|
125
|
+
"templates",
|
|
126
|
+
".github"
|
|
127
|
+
]
|
|
128
|
+
}
|
|
File without changes
|
|
@@ -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|artifact> [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.");
|
|
File without changes
|
|
@@ -23,20 +23,28 @@ function usage() {
|
|
|
23
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
24
|
console.error(" artifact compare <repo> --matter-id <id> --base-release <id> --target-release <id>");
|
|
25
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>");
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function parse(argv) {
|
|
29
32
|
if (!argv.length) return null;
|
|
30
33
|
const operation = argv.shift();
|
|
31
34
|
let artifactAction = null;
|
|
35
|
+
let courseAction = null;
|
|
32
36
|
if (operation === "artifact") {
|
|
33
37
|
artifactAction = argv.shift() || null;
|
|
34
38
|
if (!new Set(["inspect", "release", "compare", "verify"]).has(artifactAction)) throw new Error("artifact requires inspect, release, compare or verify");
|
|
35
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
|
+
}
|
|
36
44
|
let repository = ".";
|
|
37
45
|
if (argv[0] && !argv[0].startsWith("--")) repository = argv.shift();
|
|
38
46
|
const options = {};
|
|
39
|
-
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"]);
|
|
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"]);
|
|
40
48
|
const flags = new Set(["--apply", "--refresh-binding", "--significant-work", "--create-export"]);
|
|
41
49
|
const names = {
|
|
42
50
|
"--task": "task", "--source": "source", "--target-id": "targetId",
|
|
@@ -54,6 +62,8 @@ function parse(argv) {
|
|
|
54
62
|
"--parent-release": "parentReleaseIds", "--base-release": "baseReleaseId",
|
|
55
63
|
"--target-release": "targetReleaseId", "--client-note": "clientNote",
|
|
56
64
|
"--create-export": "createExport",
|
|
65
|
+
"--technology": "technologyFile", "--scenario": "scenarioIds",
|
|
66
|
+
"--audience": "audience", "--course-pack": "coursePackFile", "--projection": "projectionFile",
|
|
57
67
|
};
|
|
58
68
|
while (argv.length) {
|
|
59
69
|
const token = argv.shift();
|
|
@@ -63,10 +73,12 @@ function parse(argv) {
|
|
|
63
73
|
const value = argv.shift();
|
|
64
74
|
if (token === "--select") options.selectedIds = [...(options.selectedIds || []), value];
|
|
65
75
|
else if (token === "--parent-release") options.parentReleaseIds = [...(options.parentReleaseIds || []), value];
|
|
76
|
+
else if (token === "--scenario") options.scenarioIds = [...(options.scenarioIds || []), value];
|
|
66
77
|
else options[names[token]] = value;
|
|
67
78
|
} else throw new Error(`unsupported argument ${token}`);
|
|
68
79
|
}
|
|
69
80
|
if (artifactAction) options.artifactAction = artifactAction;
|
|
81
|
+
if (courseAction) options.courseAction = courseAction;
|
|
70
82
|
return { operation, repository: path.resolve(repository), options };
|
|
71
83
|
}
|
|
72
84
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
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 {
|
|
10
|
+
canonicalBytes,
|
|
11
|
+
compileTechnologyCourse,
|
|
12
|
+
reconcileTechnologyCourse,
|
|
13
|
+
verifyTechnologyCourse,
|
|
14
|
+
} = require("../project-technology/technology-course");
|
|
15
|
+
|
|
16
|
+
const root = path.resolve(__dirname, "../..");
|
|
17
|
+
const fixture = JSON.parse(fs.readFileSync(path.join(root, "examples/executable-technology-course/technology.json"), "utf8"));
|
|
18
|
+
|
|
19
|
+
function compile(options = {}) {
|
|
20
|
+
return compileTechnologyCourse(root, { technology: fixture, audience: "staff", ...options });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const full = compile();
|
|
24
|
+
assert.equal(full.status, "success");
|
|
25
|
+
assert.deepEqual(full.course_pack.scenario_ids, ["scenario.full_release", "scenario.recovery_preparation"]);
|
|
26
|
+
assert.deepEqual(full.course_pack.sections.map((item) => item.technology_node_id), [
|
|
27
|
+
"operation.inventory", "operation.backup", "operation.validate", "operation.promote",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const partial = compile({ scenarioIds: ["scenario.recovery_preparation"] });
|
|
31
|
+
assert.equal(partial.status, "success");
|
|
32
|
+
assert.deepEqual(partial.course_pack.sections.map((item) => item.technology_node_id), ["operation.inventory", "operation.backup"]);
|
|
33
|
+
assert.equal(partial.course_pack.course_pack_digest, compile({ scenarioIds: ["scenario.recovery_preparation"] }).course_pack.course_pack_digest);
|
|
34
|
+
assert.equal(verifyTechnologyCourse(root, { coursePack: partial.course_pack }).status, "success");
|
|
35
|
+
assert.equal(verifyTechnologyCourse(root, { coursePack: partial }).status, "success");
|
|
36
|
+
|
|
37
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "mirai-course-"));
|
|
38
|
+
const technologyFile = path.join(temp, "technology.json");
|
|
39
|
+
fs.writeFileSync(technologyFile, canonicalBytes(fixture));
|
|
40
|
+
const fromFile = compileTechnologyCourse(root, { technologyFile, scenarioIds: ["scenario.recovery_preparation"], audience: "staff" });
|
|
41
|
+
assert.equal(fromFile.course_pack.course_pack_digest, partial.course_pack.course_pack_digest);
|
|
42
|
+
|
|
43
|
+
const editorial = JSON.parse(JSON.stringify(partial.course_pack));
|
|
44
|
+
editorial.sections[0].title = "Understand the current state";
|
|
45
|
+
const editorialResult = reconcileTechnologyCourse(root, { coursePack: partial.course_pack, projection: editorial });
|
|
46
|
+
assert.equal(editorialResult.status, "success");
|
|
47
|
+
assert.equal(editorialResult.editorial_changes.length, 1);
|
|
48
|
+
assert.equal(editorialResult.canonical_write_allowed, false);
|
|
49
|
+
|
|
50
|
+
const semantic = JSON.parse(JSON.stringify(partial.course_pack));
|
|
51
|
+
semantic.sections[1].check_refs = [];
|
|
52
|
+
const semanticResult = reconcileTechnologyCourse(root, { coursePack: partial.course_pack, projection: semantic });
|
|
53
|
+
assert.equal(semanticResult.status, "needs_decision");
|
|
54
|
+
assert.equal(semanticResult.semantic_proposals.length, 1);
|
|
55
|
+
assert.equal(semanticResult.canonical_write_allowed, false);
|
|
56
|
+
|
|
57
|
+
const stale = JSON.parse(JSON.stringify(partial.course_pack));
|
|
58
|
+
stale.course_pack_digest = `sha256:${"0".repeat(64)}`;
|
|
59
|
+
assert.equal(reconcileTechnologyCourse(root, { coursePack: partial.course_pack, projection: stale }).status, "blocked");
|
|
60
|
+
|
|
61
|
+
for (const mutate of [
|
|
62
|
+
(item) => { item.operations[1].prerequisites = ["operation.unknown"]; },
|
|
63
|
+
(item) => { item.operations[0].prerequisites = ["operation.backup"]; },
|
|
64
|
+
(item) => { item.operations.push(JSON.parse(JSON.stringify(item.operations[0]))); },
|
|
65
|
+
(item) => { item.scenarios[0].operation_ids = ["operation.unknown"]; },
|
|
66
|
+
]) {
|
|
67
|
+
const invalid = JSON.parse(JSON.stringify(fixture)); mutate(invalid);
|
|
68
|
+
assert.equal(compileTechnologyCourse(root, { technology: invalid }).status, "blocked");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const tampered = JSON.parse(JSON.stringify(partial.course_pack));
|
|
72
|
+
tampered.sections[0].summary = "Tampered";
|
|
73
|
+
assert.equal(verifyTechnologyCourse(root, { coursePack: tampered }).status, "blocked");
|
|
74
|
+
|
|
75
|
+
const secret = JSON.parse(JSON.stringify(partial.course_pack));
|
|
76
|
+
secret.sections[0].token = "forbidden";
|
|
77
|
+
assert.equal(verifyTechnologyCourse(root, { coursePack: secret }).status, "blocked");
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(fs.readdirSync(temp).sort(), ["technology.json"]);
|
|
80
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
81
|
+
console.log("technology course validation: PASS");
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
const traversal = require("./context-traversal");
|
|
17
17
|
const continuity = require("./continuity");
|
|
18
18
|
const artifacts = require("./artifact-release");
|
|
19
|
+
const technologyCourse = require("./technology-course");
|
|
19
20
|
|
|
20
21
|
const CONTRACT_VERSION = "1.0.0";
|
|
21
22
|
const EXTENSION_KEY = "mirai.project_technology";
|
|
@@ -790,6 +791,7 @@ function verify(repoArg, options = {}) {
|
|
|
790
791
|
|
|
791
792
|
function execute(operation, repoArg, options = {}) {
|
|
792
793
|
if (operation === "artifact") return artifacts.executeArtifact(repoArg, options);
|
|
794
|
+
if (operation === "course") return technologyCourse.executeCourse(repoArg, options);
|
|
793
795
|
const readOnly = { explain, status, plan, verify, context };
|
|
794
796
|
if (readOnly[operation]) return readOnly[operation](repoArg, options);
|
|
795
797
|
const transactional = { enable, sync, connect, disconnect, provide, disable, repair };
|
|
@@ -807,6 +809,7 @@ module.exports = {
|
|
|
807
809
|
compareArtifactReleases: artifacts.compareArtifactReleases,
|
|
808
810
|
connect,
|
|
809
811
|
compileContext: traversal.compileContext,
|
|
812
|
+
compileTechnologyCourse: technologyCourse.compileTechnologyCourse,
|
|
810
813
|
context,
|
|
811
814
|
createArtifactRelease: artifacts.createArtifactRelease,
|
|
812
815
|
discoverContext: traversal.discoverContext,
|
|
@@ -820,10 +823,12 @@ module.exports = {
|
|
|
820
823
|
inventory,
|
|
821
824
|
inspectArtifactBundle: artifacts.inspectArtifactBundle,
|
|
822
825
|
normalizeExecutionContract,
|
|
826
|
+
normalizeTechnology: technologyCourse.normalizeTechnology,
|
|
823
827
|
continuity,
|
|
824
828
|
plan,
|
|
825
829
|
provide,
|
|
826
830
|
readExport,
|
|
831
|
+
reconcileTechnologyCourse: technologyCourse.reconcileTechnologyCourse,
|
|
827
832
|
repair,
|
|
828
833
|
sha256,
|
|
829
834
|
status,
|
|
@@ -831,5 +836,6 @@ module.exports = {
|
|
|
831
836
|
targetBindingStatus,
|
|
832
837
|
verify,
|
|
833
838
|
verifyArtifactRelease: artifacts.verifyArtifactRelease,
|
|
839
|
+
verifyTechnologyCourse: technologyCourse.verifyTechnologyCourse,
|
|
834
840
|
verifyContext: traversal.verifyContext,
|
|
835
841
|
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/@?=+*-]{1,255}$/;
|
|
8
|
+
const ACTIVE_LIFECYCLES = new Set(["reviewed", "accepted", "active"]);
|
|
9
|
+
const SECRET_MARKERS = ["password", "secret", "token", "cookie", "private_key", "totp", ".env"];
|
|
10
|
+
|
|
11
|
+
function sortValue(value) {
|
|
12
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
13
|
+
if (!value || typeof value !== "object") return value;
|
|
14
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortValue(value[key])]));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function canonicalBytes(value) {
|
|
18
|
+
return `${JSON.stringify(sortValue(value), null, 2)}\n`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sha256(value, prefix = true) {
|
|
22
|
+
const digest = crypto.createHash("sha256").update(Buffer.from(String(value))).digest("hex");
|
|
23
|
+
return prefix ? `sha256:${digest}` : digest;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function result(action, status, extra = {}) {
|
|
27
|
+
return {
|
|
28
|
+
schema_version: "1.0.0",
|
|
29
|
+
operation_id: `mirai.project_technology.course.${action}`,
|
|
30
|
+
operation_mode: "read_only",
|
|
31
|
+
status,
|
|
32
|
+
changed: false,
|
|
33
|
+
blockers: [],
|
|
34
|
+
warnings: [],
|
|
35
|
+
next_action: "none",
|
|
36
|
+
...extra,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readJson(file) {
|
|
41
|
+
const absolute = path.resolve(file);
|
|
42
|
+
if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink()) throw new Error("unsafe_or_missing_json_input");
|
|
43
|
+
return JSON.parse(fs.readFileSync(absolute, "utf8").replace(/^\uFEFF/, ""));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function unwrapCoursePack(value) {
|
|
47
|
+
return value && typeof value === "object" && !Array.isArray(value) && value.course_pack
|
|
48
|
+
? value.course_pack
|
|
49
|
+
: value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function uniqueStrings(value, field, blockers, required = true) {
|
|
53
|
+
if (!Array.isArray(value) || (required && value.length === 0)) {
|
|
54
|
+
blockers.push(`technology_${field}_empty`);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const output = [];
|
|
58
|
+
for (const item of value) {
|
|
59
|
+
if (typeof item !== "string" || !SAFE_ID.test(item)) blockers.push(`technology_${field}_unsafe`);
|
|
60
|
+
else output.push(item);
|
|
61
|
+
}
|
|
62
|
+
return [...new Set(output)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeTechnology(input) {
|
|
66
|
+
const blockers = [];
|
|
67
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return { technology: {}, blockers: ["technology_contract_missing"] };
|
|
68
|
+
const technology = {
|
|
69
|
+
schema_version: String(input.schema_version || "1.0.0"),
|
|
70
|
+
id: String(input.id || ""),
|
|
71
|
+
title: String(input.title || ""),
|
|
72
|
+
owner: String(input.owner || ""),
|
|
73
|
+
outcome: String(input.outcome || ""),
|
|
74
|
+
lifecycle: String(input.lifecycle || ""),
|
|
75
|
+
version: String(input.version || ""),
|
|
76
|
+
source_refs: uniqueStrings(input.source_refs || [], "source_refs", blockers),
|
|
77
|
+
projection_refs: uniqueStrings(input.projection_refs || [], "projection_refs", blockers, false),
|
|
78
|
+
};
|
|
79
|
+
for (const field of ["id", "owner"]) if (!SAFE_ID.test(technology[field])) blockers.push(`technology_${field}_missing_or_unsafe`);
|
|
80
|
+
for (const field of ["title", "outcome", "version"]) if (!technology[field].trim()) blockers.push(`technology_${field}_missing`);
|
|
81
|
+
if (!ACTIVE_LIFECYCLES.has(technology.lifecycle)) blockers.push("technology_lifecycle_not_executable");
|
|
82
|
+
|
|
83
|
+
const operationIds = new Set();
|
|
84
|
+
technology.operations = [];
|
|
85
|
+
if (!Array.isArray(input.operations) || input.operations.length === 0) blockers.push("technology_operations_empty");
|
|
86
|
+
else for (const raw of input.operations) {
|
|
87
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) { blockers.push("technology_operation_invalid"); continue; }
|
|
88
|
+
const operation = {
|
|
89
|
+
id: String(raw.id || ""), title: String(raw.title || ""), summary: String(raw.summary || ""),
|
|
90
|
+
owner: String(raw.owner || ""), capability_ref: String(raw.capability_ref || ""),
|
|
91
|
+
prerequisites: uniqueStrings(raw.prerequisites || [], "operation_prerequisites", blockers, false),
|
|
92
|
+
input_refs: uniqueStrings(raw.input_refs || [], "operation_input_refs", blockers, false),
|
|
93
|
+
output_refs: uniqueStrings(raw.output_refs || [], "operation_output_refs", blockers),
|
|
94
|
+
check_refs: uniqueStrings(raw.check_refs || [], "operation_check_refs", blockers),
|
|
95
|
+
stop_condition_refs: uniqueStrings(raw.stop_condition_refs || [], "operation_stop_condition_refs", blockers, false),
|
|
96
|
+
rollback_refs: uniqueStrings(raw.rollback_refs || [], "operation_rollback_refs", blockers, false),
|
|
97
|
+
source_refs: uniqueStrings(raw.source_refs || [], "operation_source_refs", blockers),
|
|
98
|
+
instructional_refs: uniqueStrings(raw.instructional_refs || [], "operation_instructional_refs", blockers, false),
|
|
99
|
+
applicability: Array.isArray(raw.applicability) ? [...new Set(raw.applicability.map(String))].sort() : [],
|
|
100
|
+
negative_boundaries: Array.isArray(raw.negative_boundaries) ? [...new Set(raw.negative_boundaries.map(String))].sort() : [],
|
|
101
|
+
};
|
|
102
|
+
for (const field of ["id", "owner", "capability_ref"]) if (!SAFE_ID.test(operation[field])) blockers.push(`technology_operation_${field}_missing_or_unsafe`);
|
|
103
|
+
if (!operation.title || !operation.summary) blockers.push("technology_operation_explanation_missing");
|
|
104
|
+
if (operationIds.has(operation.id)) blockers.push("technology_operation_duplicate");
|
|
105
|
+
operationIds.add(operation.id); technology.operations.push(operation);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
technology.scenarios = [];
|
|
109
|
+
if (!Array.isArray(input.scenarios) || input.scenarios.length === 0) blockers.push("technology_scenarios_empty");
|
|
110
|
+
else {
|
|
111
|
+
const scenarioIds = new Set();
|
|
112
|
+
for (const raw of input.scenarios) {
|
|
113
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) { blockers.push("technology_scenario_invalid"); continue; }
|
|
114
|
+
const scenario = {
|
|
115
|
+
id: String(raw.id || ""), title: String(raw.title || ""), outcome: String(raw.outcome || ""),
|
|
116
|
+
operation_ids: uniqueStrings(raw.operation_ids || [], "scenario_operation_ids", blockers),
|
|
117
|
+
required_inputs: uniqueStrings(raw.required_inputs || [], "scenario_required_inputs", blockers, false),
|
|
118
|
+
audience: Array.isArray(raw.audience) ? [...new Set(raw.audience.map(String))].sort() : [],
|
|
119
|
+
};
|
|
120
|
+
if (!SAFE_ID.test(scenario.id)) blockers.push("technology_scenario_id_missing_or_unsafe");
|
|
121
|
+
if (!scenario.title || !scenario.outcome) blockers.push("technology_scenario_explanation_missing");
|
|
122
|
+
if (scenarioIds.has(scenario.id)) blockers.push("technology_scenario_duplicate");
|
|
123
|
+
scenarioIds.add(scenario.id);
|
|
124
|
+
for (const id of scenario.operation_ids) if (!operationIds.has(id)) blockers.push("technology_scenario_operation_unknown");
|
|
125
|
+
technology.scenarios.push(scenario);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const operation of technology.operations) for (const dependency of operation.prerequisites) if (!operationIds.has(dependency)) blockers.push("technology_operation_prerequisite_unknown");
|
|
129
|
+
|
|
130
|
+
const visiting = new Set(); const visited = new Set(); const byId = new Map(technology.operations.map((item) => [item.id, item]));
|
|
131
|
+
function visit(id) {
|
|
132
|
+
if (visiting.has(id)) return true;
|
|
133
|
+
if (visited.has(id)) return false;
|
|
134
|
+
visiting.add(id);
|
|
135
|
+
for (const dependency of (byId.get(id) || {}).prerequisites || []) if (visit(dependency)) return true;
|
|
136
|
+
visiting.delete(id); visited.add(id); return false;
|
|
137
|
+
}
|
|
138
|
+
if ([...byId.keys()].some(visit)) blockers.push("technology_required_dependency_cycle");
|
|
139
|
+
return { technology, blockers: [...new Set(blockers)].sort() };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function technologyDigest(technology) {
|
|
143
|
+
return sha256(canonicalBytes(technology));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveClosure(operations, selectedIds, blockers) {
|
|
147
|
+
const byId = new Map(operations.map((item) => [item.id, item]));
|
|
148
|
+
const output = []; const seen = new Set();
|
|
149
|
+
function add(id) {
|
|
150
|
+
if (seen.has(id)) return;
|
|
151
|
+
const operation = byId.get(id);
|
|
152
|
+
if (!operation) { blockers.push("course_required_operation_missing"); return; }
|
|
153
|
+
for (const dependency of operation.prerequisites) add(dependency);
|
|
154
|
+
seen.add(id); output.push(operation);
|
|
155
|
+
}
|
|
156
|
+
for (const id of selectedIds) add(id);
|
|
157
|
+
return output;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function compileTechnologyCourse(repository, options = {}) {
|
|
161
|
+
let raw;
|
|
162
|
+
try { raw = options.technology || readJson(options.technologyFile); }
|
|
163
|
+
catch (error) { return result("compile", "blocked", { blockers: [String(error.message || error)], next_action: "provide a valid executable technology contract" }); }
|
|
164
|
+
const normalized = normalizeTechnology(raw);
|
|
165
|
+
const blockers = [...normalized.blockers];
|
|
166
|
+
if (blockers.length) return result("compile", "blocked", { blockers, next_action: "repair the executable technology contract" });
|
|
167
|
+
const scenarioIds = options.scenarioIds && options.scenarioIds.length ? options.scenarioIds : normalized.technology.scenarios.map((item) => item.id);
|
|
168
|
+
const scenarios = normalized.technology.scenarios.filter((item) => scenarioIds.includes(item.id));
|
|
169
|
+
if (scenarios.length !== new Set(scenarioIds).size) blockers.push("course_scenario_unknown");
|
|
170
|
+
const selectedOperationIds = scenarios.flatMap((item) => item.operation_ids);
|
|
171
|
+
const operations = resolveClosure(normalized.technology.operations, selectedOperationIds, blockers);
|
|
172
|
+
const audience = String(options.audience || "learner");
|
|
173
|
+
if (!SAFE_ID.test(audience)) blockers.push("course_audience_unsafe");
|
|
174
|
+
if (blockers.length) return result("compile", "blocked", { blockers: [...new Set(blockers)].sort(), next_action: "repair the technology or course selection" });
|
|
175
|
+
const technology_digest = technologyDigest(normalized.technology);
|
|
176
|
+
const packBase = {
|
|
177
|
+
schema_version: "1.0.0",
|
|
178
|
+
technology_id: normalized.technology.id,
|
|
179
|
+
technology_title: normalized.technology.title,
|
|
180
|
+
technology_outcome: normalized.technology.outcome,
|
|
181
|
+
technology_version: normalized.technology.version,
|
|
182
|
+
technology_digest,
|
|
183
|
+
audience,
|
|
184
|
+
scenario_ids: scenarios.map((item) => item.id),
|
|
185
|
+
source_refs: normalized.technology.source_refs,
|
|
186
|
+
source_revisions: options.sourceRevisions || {},
|
|
187
|
+
scenarios,
|
|
188
|
+
sections: operations.map((item, index) => ({
|
|
189
|
+
order: index + 1,
|
|
190
|
+
technology_node_id: item.id,
|
|
191
|
+
title: item.title,
|
|
192
|
+
summary: item.summary,
|
|
193
|
+
owner: item.owner,
|
|
194
|
+
capability_ref: item.capability_ref,
|
|
195
|
+
prerequisite_ids: item.prerequisites,
|
|
196
|
+
input_refs: item.input_refs,
|
|
197
|
+
output_refs: item.output_refs,
|
|
198
|
+
check_refs: item.check_refs,
|
|
199
|
+
stop_condition_refs: item.stop_condition_refs,
|
|
200
|
+
rollback_refs: item.rollback_refs,
|
|
201
|
+
source_refs: item.source_refs,
|
|
202
|
+
instructional_refs: item.instructional_refs,
|
|
203
|
+
})),
|
|
204
|
+
exercises: options.exercises || [],
|
|
205
|
+
checks: [...new Set(operations.flatMap((item) => item.check_refs))].sort(),
|
|
206
|
+
omissions: [],
|
|
207
|
+
limitations: [],
|
|
208
|
+
};
|
|
209
|
+
const context = { ...packBase, course_pack_digest: sha256(canonicalBytes(packBase)) };
|
|
210
|
+
return result("compile", "success", { repository: path.resolve(repository || "."), course_pack: context });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function verifyTechnologyCourse(repository, options = {}) {
|
|
214
|
+
let pack;
|
|
215
|
+
try { pack = unwrapCoursePack(options.coursePack || readJson(options.coursePackFile)); }
|
|
216
|
+
catch (error) { return result("verify", "blocked", { blockers: [String(error.message || error)] }); }
|
|
217
|
+
const blockers = [];
|
|
218
|
+
const digest = pack.course_pack_digest;
|
|
219
|
+
const base = { ...pack }; delete base.course_pack_digest;
|
|
220
|
+
if (digest !== sha256(canonicalBytes(base))) blockers.push("course_pack_digest_mismatch");
|
|
221
|
+
if (!Array.isArray(pack.sections) || pack.sections.length === 0) blockers.push("course_sections_empty");
|
|
222
|
+
const ids = new Set();
|
|
223
|
+
for (const section of pack.sections || []) {
|
|
224
|
+
if (!SAFE_ID.test(String(section.technology_node_id || ""))) blockers.push("course_section_identity_missing");
|
|
225
|
+
if (ids.has(section.technology_node_id)) blockers.push("course_section_duplicate");
|
|
226
|
+
ids.add(section.technology_node_id);
|
|
227
|
+
if (!section.title || !section.summary || !section.owner || !section.capability_ref) blockers.push("course_section_incomplete");
|
|
228
|
+
}
|
|
229
|
+
const serialized = JSON.stringify(pack).toLowerCase();
|
|
230
|
+
for (const marker of SECRET_MARKERS) if (serialized.includes(`\"${marker}\":`)) blockers.push("course_pack_secret_field_forbidden");
|
|
231
|
+
return result("verify", blockers.length ? "blocked" : "success", { blockers: [...new Set(blockers)].sort(), course_pack_digest: digest, next_action: blockers.length ? "recompile the course from current accepted technology" : "none" });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function reconcileTechnologyCourse(repository, options = {}) {
|
|
235
|
+
let pack; let projection;
|
|
236
|
+
try {
|
|
237
|
+
pack = unwrapCoursePack(options.coursePack || readJson(options.coursePackFile));
|
|
238
|
+
projection = options.projection || readJson(options.projectionFile);
|
|
239
|
+
} catch (error) { return result("reconcile", "blocked", { blockers: [String(error.message || error)] }); }
|
|
240
|
+
const verified = verifyTechnologyCourse(repository, { coursePack: pack });
|
|
241
|
+
if (verified.status !== "success") return result("reconcile", "blocked", { blockers: verified.blockers, next_action: verified.next_action });
|
|
242
|
+
if (projection.course_pack_digest !== pack.course_pack_digest) return result("reconcile", "blocked", { blockers: ["course_projection_source_stale"], next_action: "re-export the projection or explicitly reconcile against its source pack" });
|
|
243
|
+
const base = new Map(pack.sections.map((item) => [item.technology_node_id, item]));
|
|
244
|
+
const seen = new Set(); const changes = [];
|
|
245
|
+
for (const current of projection.sections || []) {
|
|
246
|
+
const id = current.technology_node_id;
|
|
247
|
+
if (!base.has(id)) { changes.push({ type: "semantic_proposal", technology_node_id: id || null, reason: "course_section_added" }); continue; }
|
|
248
|
+
seen.add(id); const previous = base.get(id);
|
|
249
|
+
const semanticFields = ["owner", "capability_ref", "prerequisite_ids", "input_refs", "output_refs", "check_refs", "stop_condition_refs", "rollback_refs", "source_refs"];
|
|
250
|
+
const semantic = semanticFields.some((field) => canonicalBytes(previous[field] || null) !== canonicalBytes(current[field] || null));
|
|
251
|
+
const editorial = previous.title !== current.title || previous.summary !== current.summary || canonicalBytes(previous.instructional_refs || []) !== canonicalBytes(current.instructional_refs || []);
|
|
252
|
+
if (semantic) changes.push({ type: "semantic_proposal", technology_node_id: id, reason: "executable_contract_changed" });
|
|
253
|
+
else if (editorial) changes.push({ type: "editorial", technology_node_id: id, reason: "instructional_projection_changed" });
|
|
254
|
+
}
|
|
255
|
+
for (const id of base.keys()) if (!seen.has(id)) changes.push({ type: "semantic_proposal", technology_node_id: id, reason: "required_course_section_removed" });
|
|
256
|
+
const semantic = changes.filter((item) => item.type === "semantic_proposal");
|
|
257
|
+
return result("reconcile", semantic.length ? "needs_decision" : "success", {
|
|
258
|
+
changes,
|
|
259
|
+
semantic_proposals: semantic,
|
|
260
|
+
editorial_changes: changes.filter((item) => item.type === "editorial"),
|
|
261
|
+
canonical_write_allowed: false,
|
|
262
|
+
next_action: semantic.length ? "send semantic proposals to the technology owners" : changes.length ? "apply editorial changes through the documentation owner" : "none",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function executeCourse(repository, options = {}) {
|
|
267
|
+
const action = options.courseAction;
|
|
268
|
+
if (action === "compile") return compileTechnologyCourse(repository, options);
|
|
269
|
+
if (action === "verify") return verifyTechnologyCourse(repository, options);
|
|
270
|
+
if (action === "reconcile") return reconcileTechnologyCourse(repository, options);
|
|
271
|
+
return result(String(action || "unknown"), "fail", { blockers: ["unsupported_technology_course_action"] });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
canonicalBytes,
|
|
276
|
+
compileTechnologyCourse,
|
|
277
|
+
executeCourse,
|
|
278
|
+
normalizeTechnology,
|
|
279
|
+
reconcileTechnologyCourse,
|
|
280
|
+
technologyDigest,
|
|
281
|
+
verifyTechnologyCourse,
|
|
282
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://mirai-graph.org/schemas/executable-technology.schema.json",
|
|
4
|
+
"title": "Mirai Graph Executable Technology",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "id", "title", "owner", "outcome", "lifecycle", "version", "source_refs", "operations", "scenarios"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": "1.0.0" },
|
|
10
|
+
"id": { "$ref": "#/$defs/id" },
|
|
11
|
+
"title": { "$ref": "#/$defs/text" },
|
|
12
|
+
"owner": { "$ref": "#/$defs/id" },
|
|
13
|
+
"outcome": { "$ref": "#/$defs/text" },
|
|
14
|
+
"lifecycle": { "enum": ["reviewed", "accepted", "active"] },
|
|
15
|
+
"version": { "$ref": "#/$defs/text" },
|
|
16
|
+
"source_refs": { "$ref": "#/$defs/nonEmptyIds" },
|
|
17
|
+
"projection_refs": { "$ref": "#/$defs/ids" },
|
|
18
|
+
"operations": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"minItems": 1,
|
|
21
|
+
"items": { "$ref": "#/$defs/operation" }
|
|
22
|
+
},
|
|
23
|
+
"scenarios": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"minItems": 1,
|
|
26
|
+
"items": { "$ref": "#/$defs/scenario" }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"$defs": {
|
|
30
|
+
"id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/@?=+*\\-]{1,255}$" },
|
|
31
|
+
"text": { "type": "string", "minLength": 1 },
|
|
32
|
+
"ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/id" } },
|
|
33
|
+
"nonEmptyIds": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/id" } },
|
|
34
|
+
"operation": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["id", "title", "summary", "owner", "capability_ref", "output_refs", "check_refs", "source_refs"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"id": { "$ref": "#/$defs/id" },
|
|
40
|
+
"title": { "$ref": "#/$defs/text" },
|
|
41
|
+
"summary": { "$ref": "#/$defs/text" },
|
|
42
|
+
"owner": { "$ref": "#/$defs/id" },
|
|
43
|
+
"capability_ref": { "$ref": "#/$defs/id" },
|
|
44
|
+
"prerequisites": { "$ref": "#/$defs/ids" },
|
|
45
|
+
"input_refs": { "$ref": "#/$defs/ids" },
|
|
46
|
+
"output_refs": { "$ref": "#/$defs/nonEmptyIds" },
|
|
47
|
+
"check_refs": { "$ref": "#/$defs/nonEmptyIds" },
|
|
48
|
+
"stop_condition_refs": { "$ref": "#/$defs/ids" },
|
|
49
|
+
"rollback_refs": { "$ref": "#/$defs/ids" },
|
|
50
|
+
"source_refs": { "$ref": "#/$defs/nonEmptyIds" },
|
|
51
|
+
"instructional_refs": { "$ref": "#/$defs/ids" },
|
|
52
|
+
"applicability": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/text" } },
|
|
53
|
+
"negative_boundaries": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/text" } }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"scenario": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": ["id", "title", "outcome", "operation_ids"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"id": { "$ref": "#/$defs/id" },
|
|
62
|
+
"title": { "$ref": "#/$defs/text" },
|
|
63
|
+
"outcome": { "$ref": "#/$defs/text" },
|
|
64
|
+
"operation_ids": { "$ref": "#/$defs/nonEmptyIds" },
|
|
65
|
+
"required_inputs": { "$ref": "#/$defs/ids" },
|
|
66
|
+
"audience": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/text" } }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://mirai-graph.org/schemas/technology-course-pack.schema.json",
|
|
4
|
+
"title": "Mirai Graph Technology Course Pack",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "technology_id", "technology_title", "technology_outcome", "technology_version", "technology_digest", "audience", "scenario_ids", "source_refs", "source_revisions", "scenarios", "sections", "exercises", "checks", "omissions", "limitations", "course_pack_digest"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": "1.0.0" },
|
|
10
|
+
"technology_id": { "$ref": "#/$defs/id" },
|
|
11
|
+
"technology_title": { "type": "string", "minLength": 1 },
|
|
12
|
+
"technology_outcome": { "type": "string", "minLength": 1 },
|
|
13
|
+
"technology_version": { "type": "string", "minLength": 1 },
|
|
14
|
+
"technology_digest": { "$ref": "#/$defs/digest" },
|
|
15
|
+
"audience": { "$ref": "#/$defs/id" },
|
|
16
|
+
"scenario_ids": { "$ref": "#/$defs/nonEmptyIds" },
|
|
17
|
+
"source_refs": { "$ref": "#/$defs/nonEmptyIds" },
|
|
18
|
+
"source_revisions": { "type": "object", "additionalProperties": { "type": "string", "minLength": 1 } },
|
|
19
|
+
"scenarios": { "type": "array", "minItems": 1, "items": { "type": "object" } },
|
|
20
|
+
"sections": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"minItems": 1,
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["order", "technology_node_id", "title", "summary", "owner", "capability_ref", "prerequisite_ids", "input_refs", "output_refs", "check_refs", "stop_condition_refs", "rollback_refs", "source_refs", "instructional_refs"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"order": { "type": "integer", "minimum": 1 },
|
|
29
|
+
"technology_node_id": { "$ref": "#/$defs/id" },
|
|
30
|
+
"title": { "type": "string", "minLength": 1 },
|
|
31
|
+
"summary": { "type": "string", "minLength": 1 },
|
|
32
|
+
"owner": { "$ref": "#/$defs/id" },
|
|
33
|
+
"capability_ref": { "$ref": "#/$defs/id" },
|
|
34
|
+
"prerequisite_ids": { "$ref": "#/$defs/ids" },
|
|
35
|
+
"input_refs": { "$ref": "#/$defs/ids" },
|
|
36
|
+
"output_refs": { "$ref": "#/$defs/nonEmptyIds" },
|
|
37
|
+
"check_refs": { "$ref": "#/$defs/nonEmptyIds" },
|
|
38
|
+
"stop_condition_refs": { "$ref": "#/$defs/ids" },
|
|
39
|
+
"rollback_refs": { "$ref": "#/$defs/ids" },
|
|
40
|
+
"source_refs": { "$ref": "#/$defs/nonEmptyIds" },
|
|
41
|
+
"instructional_refs": { "$ref": "#/$defs/ids" }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"exercises": { "type": "array" },
|
|
46
|
+
"checks": { "$ref": "#/$defs/nonEmptyIds" },
|
|
47
|
+
"omissions": { "type": "array" },
|
|
48
|
+
"limitations": { "type": "array" },
|
|
49
|
+
"course_pack_digest": { "$ref": "#/$defs/digest" }
|
|
50
|
+
},
|
|
51
|
+
"$defs": {
|
|
52
|
+
"id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/@?=+*\\-]{1,255}$" },
|
|
53
|
+
"digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
|
|
54
|
+
"ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/id" } },
|
|
55
|
+
"nonEmptyIds": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/id" } }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Project Technology
|
|
2
2
|
|
|
3
|
-
Status: 1.
|
|
3
|
+
Status: 1.4 stable standard; activation contract remains 1.0.0
|
|
4
4
|
|
|
5
5
|
Project Technology is the shared executable mechanism of Mirai Graph. It is
|
|
6
6
|
not a profile, a second graph or a source of domain methodology.
|
|
@@ -19,6 +19,35 @@ Each graph keeps its own objects and relations. Project Technology only
|
|
|
19
19
|
standardizes safe inventory, task context, accepted-target binding, freshness
|
|
20
20
|
and verification.
|
|
21
21
|
|
|
22
|
+
## Executable Technologies And Course Projections
|
|
23
|
+
|
|
24
|
+
An executable technology is an accepted graph of reusable operations and
|
|
25
|
+
user-facing scenarios. It is suitable for a long method that has both an
|
|
26
|
+
end-to-end path and independently useful parts. Each operation binds its owner,
|
|
27
|
+
capability, prerequisites, inputs, outputs, checks, stop conditions, rollback
|
|
28
|
+
and exact raw source references.
|
|
29
|
+
|
|
30
|
+
A scenario names an outcome and selects the operations needed to reach it.
|
|
31
|
+
Project Technology calculates prerequisite closure, so a course or executor
|
|
32
|
+
cannot silently omit a required safety step.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
mirai-graph technology course compile . --technology graph/specs/technology.json
|
|
36
|
+
mirai-graph technology course compile . --technology graph/specs/technology.json --scenario scenario.recovery
|
|
37
|
+
mirai-graph technology course verify . --course-pack course-pack.json
|
|
38
|
+
mirai-graph technology course reconcile . --course-pack course-pack.json --projection edited-course.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The JavaScript API exposes `compileTechnologyCourse`,
|
|
42
|
+
`verifyTechnologyCourse` and `reconcileTechnologyCourse`.
|
|
43
|
+
|
|
44
|
+
A Course Pack is hash-bound to the normalized technology, chosen scenarios,
|
|
45
|
+
sources and revisions. It may be rendered into a document, learning system or
|
|
46
|
+
documentation site. It remains a projection: editorial changes may be routed
|
|
47
|
+
to the documentation owner, while changed prerequisites, owners, checks, stop
|
|
48
|
+
conditions, rollback or scope become semantic proposals. Reconciliation never
|
|
49
|
+
writes them into the accepted technology automatically.
|
|
50
|
+
|
|
22
51
|
## Immutable Artifact Releases
|
|
23
52
|
|
|
24
53
|
Project Technology can preserve versioned file bundles without turning their
|