mirai-graph 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,8 @@ const {
15
15
  } = require("../cli/graph-manifest");
16
16
  const traversal = require("./context-traversal");
17
17
  const continuity = require("./continuity");
18
+ const artifacts = require("./artifact-release");
19
+ const technologyCourse = require("./technology-course");
18
20
 
19
21
  const CONTRACT_VERSION = "1.0.0";
20
22
  const EXTENSION_KEY = "mirai.project_technology";
@@ -788,6 +790,8 @@ function verify(repoArg, options = {}) {
788
790
  }
789
791
 
790
792
  function execute(operation, repoArg, options = {}) {
793
+ if (operation === "artifact") return artifacts.executeArtifact(repoArg, options);
794
+ if (operation === "course") return technologyCourse.executeCourse(repoArg, options);
791
795
  const readOnly = { explain, status, plan, verify, context };
792
796
  if (readOnly[operation]) return readOnly[operation](repoArg, options);
793
797
  const transactional = { enable, sync, connect, disconnect, provide, disable, repair };
@@ -802,9 +806,12 @@ module.exports = {
802
806
  LOCAL_DIR,
803
807
  bindingValues,
804
808
  canonicalBytes,
809
+ compareArtifactReleases: artifacts.compareArtifactReleases,
805
810
  connect,
806
811
  compileContext: traversal.compileContext,
812
+ compileTechnologyCourse: technologyCourse.compileTechnologyCourse,
807
813
  context,
814
+ createArtifactRelease: artifacts.createArtifactRelease,
808
815
  discoverContext: traversal.discoverContext,
809
816
  disable,
810
817
  disconnect,
@@ -814,16 +821,21 @@ module.exports = {
814
821
  explain,
815
822
  extensionContract,
816
823
  inventory,
824
+ inspectArtifactBundle: artifacts.inspectArtifactBundle,
817
825
  normalizeExecutionContract,
826
+ normalizeTechnology: technologyCourse.normalizeTechnology,
818
827
  continuity,
819
828
  plan,
820
829
  provide,
821
830
  readExport,
831
+ reconcileTechnologyCourse: technologyCourse.reconcileTechnologyCourse,
822
832
  repair,
823
833
  sha256,
824
834
  status,
825
835
  sync,
826
836
  targetBindingStatus,
827
837
  verify,
838
+ verifyArtifactRelease: artifacts.verifyArtifactRelease,
839
+ verifyTechnologyCourse: technologyCourse.verifyTechnologyCourse,
828
840
  verifyContext: traversal.verifyContext,
829
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,16 @@
1
+ # Mirai Graph 1.3.0
2
+
3
+ Mirai Graph 1.3.0 adds generic immutable releases for file bundles to the
4
+ existing Project Technology engine.
5
+
6
+ Projects can inspect a bundle, preview or create a release, compare two
7
+ releases and verify stored files against their manifest. The graph stores only
8
+ safe lineage, state, references and checksums; protected artifact content
9
+ remains outside graph data.
10
+
11
+ The release supports direct files, directories, ZIP, TAR and TAR.GZ, with
12
+ fail-closed archive checks, transactional activation, rollback, concurrency
13
+ protection and idempotent repeats.
14
+
15
+ The public `graph.json` schema remains `2.0.0`. The Project Technology
16
+ activation contract remains `1.0.0`.
@@ -20,6 +20,7 @@ Release notes must separate:
20
20
  - [v1.1.0](1.1.0.md) - universal sequential context traversal and usage
21
21
  verification in Project Technology.
22
22
  - [v1.0.0](1.0.0.md) - stable Project Technology, CLI and public contract.
23
+ - [v1.3.0](1.3.0.md) - immutable artifact releases in Project Technology.
23
24
  - [v1.0.0-rc.6](1.0.0-rc.6.md) - anti-drift / quality-control release
24
25
  consolidation across Semantic Intent, Dynamic Episode, Goal Vector and
25
26
  Technology Quality Feedback.
@@ -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.2 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,75 @@ 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
+
51
+ ## Immutable Artifact Releases
52
+
53
+ Project Technology can preserve versioned file bundles without turning their
54
+ contents into graph data:
55
+
56
+ ```text
57
+ inspect -> release preview -> transactional release -> compare -> verify
58
+ ```
59
+
60
+ The generic CLI is:
61
+
62
+ ```bash
63
+ mirai-graph technology artifact inspect . --input incoming.zip
64
+ mirai-graph technology artifact release . --input incoming.zip --matter-id agreement-main --direction inbound
65
+ mirai-graph technology artifact release . --input incoming.zip --matter-id agreement-main --direction inbound --apply
66
+ mirai-graph technology artifact compare . --matter-id agreement-main --base-release 20260828-01 --target-release 20260828-02
67
+ mirai-graph technology artifact verify . --matter-id agreement-main --release-id 20260828-02
68
+ ```
69
+
70
+ The programmatic API exposes `inspectArtifactBundle`,
71
+ `createArtifactRelease`, `compareArtifactReleases` and
72
+ `verifyArtifactRelease`.
73
+
74
+ Each immutable release keeps the original input, a normalized package, a
75
+ hash-bound manifest and a technical comparison. The portable registry at
76
+ `graph/specs/artifact-releases.json` stores only opaque matter/release ids,
77
+ relative or provider refs, lineage, state and digests. Raw files, document
78
+ contents, party names and private discussion remain in the protected artifact
79
+ store.
80
+
81
+ Direct files, directories, ZIP, TAR and TAR.GZ are supported. Unsafe paths,
82
+ links, encrypted archives, executable or macro-enabled files, nested archives,
83
+ normalized path collisions and configured archive limits fail closed. RAR and
84
+ 7z need an explicitly supplied safe provider.
85
+
86
+ Creation uses a lease, compare-and-swap registry digest, temporary assembly,
87
+ atomic activation and readback. Repeating the same release returns
88
+ `changed=false`. Domain consumers own the meaning of release states; Mirai
89
+ Graph owns only generic identity, lineage, integrity and technical comparison.
90
+
22
91
  ## Portable Project Continuity
23
92
 
24
93
  Project Technology preserves verified project experience at task boundaries:
@@ -137,6 +206,10 @@ source still answers "how should the domain work be done?".
137
206
  rejected from portable continuity.
138
207
  - Significant work with an enabled continuity policy fails closed when its
139
208
  terminal receipt is missing or stale.
209
+ - Artifact inspection, comparison and verification are read-only. Release
210
+ creation is preview-only without `--apply`.
211
+ - An artifact manifest or generated export never proves legal, business or
212
+ domain acceptance.
140
213
  - A context pack is not ready while a required branch, source, access boundary
141
214
  or validator is missing, stale, blocked, deprecated, conflicting or
142
215
  digest-mismatched.