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 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.0",
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
- "validate:project-technology-schemas": "node -e \"const fs=require('fs'); for (const file of ['project-technology-extension.schema.json','project-technology-target-export.schema.json','context-traversal-receipt.schema.json','context-pack.schema.json']) JSON.parse(fs.readFileSync('schemas/'+file))\"",
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 stable standard; activation contract remains 1.0.0
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