@rosenfan/matrix 0.1.3 → 0.1.5
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/LICENSE +21 -21
- package/README.md +72 -63
- package/README.zh-CN.md +70 -61
- package/{.claude → assets}/skills/matrix/SKILL.md +81 -86
- package/{.claude → assets}/skills/matrix/agents/openai.yaml +4 -4
- package/{.claude → assets}/skills/matrix/scripts/matrix_claude.py +11 -11
- package/{.claude → assets}/skills/matrix/scripts/matrix_state.py +60 -60
- package/assets/skills/matrix/scripts/test_matrix_claude.py +69 -0
- package/assets/skills/matrix/scripts/test_matrix_state.py +69 -0
- package/{.claude → assets}/skills/matrix-archive/SKILL.md +27 -27
- package/{.claude/skills/matrix-hotfix → assets/skills/matrix-archive}/agents/openai.yaml +4 -4
- package/{.claude → assets}/skills/matrix-build/SKILL.md +41 -38
- package/{.claude/skills/matrix-archive → assets/skills/matrix-build}/agents/openai.yaml +4 -4
- package/{.claude → assets}/skills/matrix-claude/SKILL.md +93 -93
- package/{.claude → assets}/skills/matrix-claude/agents/openai.yaml +4 -4
- package/{.claude → assets}/skills/matrix-design/SKILL.md +69 -67
- package/{.claude → assets}/skills/matrix-design/agents/openai.yaml +4 -4
- package/{.claude → assets}/skills/matrix-hotfix/SKILL.md +7 -7
- package/{.claude/skills/matrix-build → assets/skills/matrix-hotfix}/agents/openai.yaml +4 -4
- package/{.claude → assets}/skills/matrix-open/SKILL.md +26 -24
- package/assets/skills/matrix-open/agents/openai.yaml +4 -0
- package/{.claude → assets}/skills/matrix-status/SKILL.md +14 -14
- package/{.claude → assets}/skills/matrix-status/agents/openai.yaml +4 -4
- package/{.claude → assets}/skills/matrix-tweak/SKILL.md +7 -7
- package/assets/skills/matrix-tweak/agents/openai.yaml +4 -0
- package/assets/skills/matrix-verify/SKILL.md +89 -0
- package/assets/skills/matrix-verify/agents/openai.yaml +4 -0
- package/assets/skills-zh-CN/matrix/SKILL.md +27 -27
- package/assets/skills-zh-CN/matrix-archive/SKILL.md +10 -10
- package/assets/skills-zh-CN/matrix-build/SKILL.md +13 -11
- package/assets/skills-zh-CN/matrix-claude/SKILL.md +10 -10
- package/assets/skills-zh-CN/matrix-design/SKILL.md +12 -10
- package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +1 -1
- package/assets/skills-zh-CN/matrix-open/SKILL.md +18 -16
- package/assets/skills-zh-CN/matrix-status/SKILL.md +3 -3
- package/assets/skills-zh-CN/matrix-tweak/SKILL.md +1 -1
- package/assets/skills-zh-CN/matrix-verify/SKILL.md +25 -9
- package/bin/matrix.js +15 -15
- package/package.json +4 -13
- package/src/catalog.js +110 -92
- package/src/cli.js +138 -80
- package/src/distribution.js +379 -253
- package/src/installer.js +4 -4
- package/src/matt-catalog.mjs +67 -0
- package/src/matt.js +91 -9
- package/src/messages.js +14 -10
- package/src/runtime-resolver.js +76 -0
- package/src/self-update.js +2 -2
- package/src/workflow-transaction.js +9 -5
- package/src/workflow.js +688 -429
- package/.claude/skills/matrix-open/agents/openai.yaml +0 -4
- package/.claude/skills/matrix-tweak/agents/openai.yaml +0 -4
- package/.claude/skills/matrix-verify/SKILL.md +0 -73
- package/.claude/skills/matrix-verify/agents/openai.yaml +0 -4
package/src/distribution.js
CHANGED
|
@@ -1,36 +1,41 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
|
|
7
|
-
installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
|
|
8
|
-
} from "./catalog.js";
|
|
9
|
-
import { createMattAdapter } from "./matt.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
|
|
7
|
+
installationRoot, manifestPath, mattCatalogSource, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
|
|
8
|
+
} from "./catalog.js";
|
|
9
|
+
import { createMattAdapter, mattReceiptPath } from "./matt.js";
|
|
10
|
+
import { MATT_CATALOG_DIGEST, MATT_COMPATIBILITY, MATT_CONTENT_HASHES } from "./matt-catalog.mjs";
|
|
11
|
+
import { text } from "./messages.js";
|
|
12
|
+
|
|
13
|
+
// Manifest v4 records content digests computed over EOL-normalized bytes and the
|
|
14
|
+
// assets/skills source layout; v3 manifests (raw-byte hashes, .claude/skills era)
|
|
15
|
+
// stay readable and are refreshed to v4 by the next update.
|
|
16
|
+
const VERSION = 4;
|
|
17
|
+
const MATT_OPERATIONS = new Set(["none", "missing", "readonly"]);
|
|
18
|
+
const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
19
|
+
const exists = (value) => fs.existsSync(value);
|
|
20
|
+
const pathExists = (value) => { try { fs.lstatSync(value); return true; } catch { return false; } };
|
|
21
|
+
const nowStamp = () => new Date().toISOString().replace(/[:.]/g, "-");
|
|
22
|
+
const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
|
|
23
|
+
const configPath = (intent) => path.join(intent.projectRoot, ".matrix", "config.yaml");
|
|
24
|
+
const writeJson = (file, value) => {
|
|
25
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
26
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
27
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
28
|
+
fs.renameSync(temporary, file);
|
|
29
|
+
};
|
|
30
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
|
|
31
|
+
const readConfig = (file) => {
|
|
32
|
+
try {
|
|
33
|
+
return Object.fromEntries(fs.readFileSync(file, "utf8").split(/\r?\n/).filter((line) => line.includes(":") && !line.startsWith(" ")).map((line) => {
|
|
34
|
+
const [key, ...value] = line.split(":");
|
|
35
|
+
return [key.trim(), value.join(":").trim()];
|
|
36
|
+
}));
|
|
37
|
+
} catch { return {}; }
|
|
38
|
+
};
|
|
34
39
|
const configContent = (existing, defaultOrchestration, scope, language) => {
|
|
35
40
|
const values = {
|
|
36
41
|
...existing,
|
|
@@ -44,33 +49,52 @@ const configContent = (existing, defaultOrchestration, scope, language) => {
|
|
|
44
49
|
const keys = [...preferred, ...Object.keys(values).filter((key) => !preferred.includes(key)).sort()];
|
|
45
50
|
return `${keys.map((key) => `${key}: ${values[key]}`).join("\n")}\n`;
|
|
46
51
|
};
|
|
47
|
-
const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
|
|
48
|
-
const removeStaging = (stagingRoot) => {
|
|
49
|
-
removeEntry(stagingRoot);
|
|
50
|
-
const parent = path.dirname(stagingRoot);
|
|
51
|
-
if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
|
|
52
|
-
};
|
|
53
|
-
const compareVersions = (left, right) => {
|
|
54
|
-
const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
|
|
55
|
-
const a = parse(left); const b = parse(right);
|
|
56
|
-
for (let index = 0; index < Math.max(a.length, b.length); index += 1) if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) > (b[index] ?? 0) ? 1 : -1;
|
|
57
|
-
return 0;
|
|
58
|
-
};
|
|
59
|
-
const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
|
|
60
|
-
const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
|
|
61
|
-
|
|
52
|
+
const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
|
|
53
|
+
const removeStaging = (stagingRoot) => {
|
|
54
|
+
removeEntry(stagingRoot);
|
|
55
|
+
const parent = path.dirname(stagingRoot);
|
|
56
|
+
if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
|
|
57
|
+
};
|
|
58
|
+
const compareVersions = (left, right) => {
|
|
59
|
+
const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
|
|
60
|
+
const a = parse(left); const b = parse(right);
|
|
61
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) > (b[index] ?? 0) ? 1 : -1;
|
|
62
|
+
return 0;
|
|
63
|
+
};
|
|
64
|
+
const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
|
|
65
|
+
const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
|
|
66
|
+
const mattObservationMatchesRelease = (observation, contentHashes) => MATT_SKILLS.every((skill) => observation.matt.skills?.[skill]?.hash === contentHashes[skill]);
|
|
67
|
+
const mattObservationMatchesReceipt = (observation) => MATT_SKILLS.every((skill) => observation.matt.classifications?.[skill]?.state === "matching");
|
|
68
|
+
const verifyMattCandidate = (candidate, actions, contentHashes) => {
|
|
69
|
+
for (const action of actions) for (const skill of action.matt.skills) {
|
|
70
|
+
const source = path.join(candidate.platforms[action.platform], skill);
|
|
71
|
+
if (!exists(path.join(source, "SKILL.md")) || hashDirectory(source) !== contentHashes[skill]) {
|
|
72
|
+
throw new Error(`Matt candidate content did not match the reviewed release: ${action.platform}/${skill}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const localizedState = (language, state) => text(language, ({
|
|
77
|
+
missing: "stateMissing", matching: "stateMatching", complete: "stateComplete", "user-modified": "stateUserModified",
|
|
78
|
+
partial: "statePartial", damaged: "statePartial", newer: "stateMatching", "outdated-unchanged": "stateOutdated",
|
|
79
|
+
"safe-update": "stateSafeUpdate", adoptable: "stateAdoptable", unreadable: "stateUnreadable"
|
|
80
|
+
})[state] ?? state);
|
|
81
|
+
const localizedAction = (language, action) => text(language, ({
|
|
82
|
+
install: "actionInstall", keep: "actionKeep", "backup-replace": "actionBackupReplace", repair: "actionRepair", blocked: "actionBlocked"
|
|
83
|
+
})[action] ?? action);
|
|
84
|
+
|
|
62
85
|
function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
|
|
63
86
|
|
|
64
87
|
export function installedIntent({ projectRoot = process.cwd(), home = os.homedir() } = {}) {
|
|
65
88
|
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
66
89
|
const config = readConfig(path.join(resolvedProjectRoot, ".matrix", "config.yaml"));
|
|
67
|
-
const
|
|
68
|
-
const
|
|
90
|
+
const projectManifest = readJson(manifestPath("project", resolvedProjectRoot, home));
|
|
91
|
+
const scope = projectManifest ? "project" : config.installation_scope === "global" ? "global" : "project";
|
|
92
|
+
const manifest = projectManifest ?? readJson(manifestPath(scope, resolvedProjectRoot, home));
|
|
69
93
|
const platforms = Object.keys(manifest?.platforms ?? {}).filter((id) => PLATFORM_IDS.includes(id));
|
|
70
94
|
if (!manifest || !platforms.length) return { ok: false, code: "MANIFEST_MISSING", scope, config };
|
|
71
95
|
const language = ["en", "zh-CN"].includes(manifest.language) ? manifest.language : (["en", "zh-CN"].includes(config.language) ? config.language : "en");
|
|
72
96
|
const defaultOrchestration = ["prim", "arch"].includes(manifest.orchestration?.default) ? manifest.orchestration.default : "prim";
|
|
73
|
-
const matt =
|
|
97
|
+
const matt = "readonly";
|
|
74
98
|
return {
|
|
75
99
|
ok: true,
|
|
76
100
|
intent: {
|
|
@@ -82,206 +106,308 @@ export function installedIntent({ projectRoot = process.cwd(), home = os.homedir
|
|
|
82
106
|
config
|
|
83
107
|
};
|
|
84
108
|
}
|
|
85
|
-
|
|
86
|
-
function detectPlatforms(projectRoot, scope, home) {
|
|
87
|
-
const bases = [scopeBase(scope, projectRoot, home)];
|
|
88
|
-
if (scope === "project") bases.push(home);
|
|
89
|
-
return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function normalizeIntent(raw) {
|
|
93
|
-
const scope = raw.scope ?? "project";
|
|
94
|
-
const platforms = [...new Set(raw.platforms ?? [])];
|
|
95
|
-
if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
|
|
96
|
-
if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
|
|
97
|
-
if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
|
|
98
|
-
if (raw.defaultOrchestration != null && !["prim", "arch"].includes(raw.defaultOrchestration)) return { error: "INVALID_INTENT", detail: "default-orchestration" };
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
109
|
+
|
|
110
|
+
function detectPlatforms(projectRoot, scope, home) {
|
|
111
|
+
const bases = [scopeBase(scope, projectRoot, home)];
|
|
112
|
+
if (scope === "project") bases.push(home);
|
|
113
|
+
return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeIntent(raw) {
|
|
117
|
+
const scope = raw.scope ?? "project";
|
|
118
|
+
const platforms = [...new Set(raw.platforms ?? [])];
|
|
119
|
+
if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
|
|
120
|
+
if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
|
|
121
|
+
if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
|
|
122
|
+
if (raw.defaultOrchestration != null && !["prim", "arch"].includes(raw.defaultOrchestration)) return { error: "INVALID_INTENT", detail: "default-orchestration" };
|
|
123
|
+
if (!MATT_OPERATIONS.has(raw.matt ?? "none")) return { error: "INVALID_INTENT", detail: "matt" };
|
|
124
|
+
if (raw.mattPolicy != null && !["preserve", "replace"].includes(raw.mattPolicy)) return { error: "INVALID_INTENT", detail: "matt-policy" };
|
|
125
|
+
if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
|
|
126
|
+
return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
|
|
127
|
+
language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
|
|
128
|
+
matt: raw.matt ?? "none", mattPolicy: raw.mattPolicy ?? "preserve", defaultOrchestration: raw.defaultOrchestration ?? null,
|
|
129
|
+
nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function classifyMattTarget({ present, readable = true, actual, expected, recorded, receiptTrusted = false }) {
|
|
133
|
+
if (!present) return "missing";
|
|
134
|
+
if (!readable || !actual) return "unreadable";
|
|
135
|
+
if (expected && actual === expected) return receiptTrusted && recorded === actual ? "matching" : "adoptable";
|
|
136
|
+
if (recorded && actual === recorded) return "safe-update";
|
|
137
|
+
return "user-modified";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function trustedMattReceipt(receipt) {
|
|
141
|
+
return Boolean(receipt
|
|
142
|
+
&& receipt.compatibility?.release === MATT_COMPATIBILITY.release
|
|
143
|
+
&& receipt.compatibility?.commit === MATT_COMPATIBILITY.commit
|
|
144
|
+
&& receipt.catalogDigest === MATT_CATALOG_DIGEST);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function observePlatform(intent, catalog, manifest, mattReceipt, platformId, contentHashes) {
|
|
148
|
+
const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
|
|
149
|
+
const root = platform.skillRoot(base); const records = manifest?.platforms?.[platformId]?.skills ?? {};
|
|
150
|
+
const skills = MATRIX_SKILLS.map((skill) => {
|
|
151
|
+
const target = path.join(root, skill);
|
|
152
|
+
if (!pathExists(target)) return { skill, target, state: "missing" };
|
|
153
|
+
if (isBrokenLink(target) || !exists(path.join(target, "SKILL.md"))) return { skill, target, state: "damaged" };
|
|
154
|
+
const actual = hashDirectory(target); const recorded = records[skill]?.hash; const expected = catalog.skills[skill];
|
|
155
|
+
if (!recorded) return { skill, target, state: "user-modified", actual };
|
|
156
|
+
if (actual === expected) return { skill, target, state: "matching", actual };
|
|
157
|
+
if (actual === recorded) return { skill, target, state: compareVersions(manifest?.catalogVersion, catalog.version) > 0 ? "newer" : "outdated-unchanged", actual };
|
|
158
|
+
return { skill, target, state: "user-modified", actual };
|
|
159
|
+
});
|
|
160
|
+
const states = new Set(skills.map((item) => item.state));
|
|
161
|
+
const matrix = states.size === 1 && states.has("matching") ? "matching" : states.has("user-modified") ? "user-modified"
|
|
162
|
+
: states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
|
|
163
|
+
: states.has("damaged") ? "damaged" : states.has("outdated-unchanged") ? "outdated-unchanged" : "partial";
|
|
164
|
+
const mattRoot = platform.skillRoot(intent.projectRoot);
|
|
165
|
+
const globalRoot = platform.skillRoot(intent.home);
|
|
166
|
+
const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(mattRoot, skill));
|
|
167
|
+
const globalMatt = MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill));
|
|
168
|
+
const missing = MATT_SKILLS.filter((skill) => !mattPresent.includes(skill));
|
|
169
|
+
const ignoredGlobal = globalMatt.filter((skill) => !mattPresent.includes(skill)).length;
|
|
170
|
+
const receiptRecords = mattReceipt?.platforms?.[platformId]?.skills ?? {};
|
|
171
|
+
const legacyRecords = manifest?.platforms?.[platformId]?.matt?.skills ?? {};
|
|
172
|
+
const receiptIsTrusted = trustedMattReceipt(mattReceipt);
|
|
173
|
+
const classifications = Object.fromEntries(MATT_SKILLS.map((skill) => {
|
|
174
|
+
const target = path.join(mattRoot, skill);
|
|
175
|
+
const present = pathExists(target);
|
|
176
|
+
let actual = null; let readable = true;
|
|
177
|
+
if (present) {
|
|
178
|
+
try { actual = hashDirectory(target); } catch { readable = false; }
|
|
179
|
+
}
|
|
180
|
+
const recorded = receiptRecords[skill]?.hash ?? legacyRecords[skill]?.hash ?? null;
|
|
181
|
+
const state = classifyMattTarget({ present, readable, actual, expected: contentHashes[skill], recorded, receiptTrusted: receiptIsTrusted });
|
|
182
|
+
return [skill, { state, actual, expected: contentHashes[skill], recorded }];
|
|
183
|
+
}));
|
|
184
|
+
const managedSkills = Object.fromEntries(Object.entries(classifications).filter(([, item]) => item.actual).map(([skill, item]) => [skill, { root: path.join(mattRoot, skill), hash: item.actual }]));
|
|
185
|
+
const classificationCounts = Object.values(classifications).reduce((counts, item) => ({ ...counts, [item.state]: (counts[item.state] ?? 0) + 1 }), {});
|
|
186
|
+
const unmanaged = UNMANAGED_MATT_SKILLS.filter((skill) => skillPresent(mattRoot, skill));
|
|
187
|
+
return { id: platformId, name: platform.name, root, skills, matrix, matt: { root: mattRoot, present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited: 0, ignoredGlobal,
|
|
188
|
+
state: mattPresent.length === MATT_SKILLS.length ? "complete" : mattPresent.length ? "partial" : "missing", skills: managedSkills, classifications, classificationCounts, unmanaged } };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function actionFor(observation, policy) {
|
|
192
|
+
if (observation.matrix === "matching" || observation.matrix === "newer") return "keep";
|
|
193
|
+
if (observation.matrix === "missing") return "install";
|
|
194
|
+
if (observation.matrix === "outdated-unchanged") return "backup-replace";
|
|
195
|
+
if (["damaged", "user-modified"].includes(observation.matrix)) return policy === "replace" ? "backup-replace" : "blocked";
|
|
196
|
+
return "repair";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function recoverPendingTransaction(intent, { allowMattWrites = true } = {}) {
|
|
200
|
+
const file = transactionPath(intent); const journal = readJson(file);
|
|
201
|
+
if (!journal) return { ok: true, recovered: false };
|
|
202
|
+
if (!allowMattWrites && ((journal.operations ?? []).some((operation) => operation.kind === "matt") || journal.mattReceipt?.written || journal.mattReceipt?.backup)) {
|
|
203
|
+
return { ok: false, code: "RECOVERY_REQUIRED", recovery: `A pending Matt init transaction must be recovered by matrix init --with-mattpocock before Matrix update can continue. Keep ${file}.` };
|
|
204
|
+
}
|
|
205
|
+
const restoreMetadata = (entry) => {
|
|
206
|
+
if (!entry) return;
|
|
207
|
+
if (entry.backup && pathExists(entry.backup)) {
|
|
208
|
+
removeEntry(entry.path);
|
|
209
|
+
fs.mkdirSync(path.dirname(entry.path), { recursive: true });
|
|
210
|
+
fs.renameSync(entry.backup, entry.path);
|
|
211
|
+
} else if (entry.written && !entry.existed) removeEntry(entry.path);
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
if (journal.state === "committed") {
|
|
215
|
+
removeStaging(journal.stagingRoot);
|
|
216
|
+
for (const operation of journal.operations ?? []) removeEntry(operation.backup);
|
|
217
|
+
removeEntry(journal.manifest?.backup); removeEntry(journal.config?.backup); removeEntry(journal.mattReceipt?.backup); removeEntry(file);
|
|
218
|
+
return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
|
|
219
|
+
}
|
|
220
|
+
for (const operation of [...(journal.operations ?? [])].reverse()) {
|
|
221
|
+
if (operation.status !== "pending") removeEntry(operation.target);
|
|
222
|
+
if (operation.backup && pathExists(operation.backup)) { fs.mkdirSync(path.dirname(operation.target), { recursive: true }); fs.renameSync(operation.backup, operation.target); }
|
|
223
|
+
}
|
|
224
|
+
restoreMetadata(journal.config);
|
|
225
|
+
restoreMetadata(journal.manifest);
|
|
226
|
+
restoreMetadata(journal.mattReceipt);
|
|
227
|
+
removeStaging(journal.stagingRoot); removeEntry(file);
|
|
228
|
+
return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
|
|
229
|
+
} catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function createDistribution({ failAt, mattAdapter = createMattAdapter(), mattCandidateVerifier = null, mattContentHashes = MATT_CONTENT_HASHES } = {}) {
|
|
233
|
+
const candidateVerifier = mattCandidateVerifier ?? ((candidate, actions) => verifyMattCandidate(candidate, actions, mattContentHashes));
|
|
234
|
+
const evaluate = (rawIntent) => {
|
|
235
|
+
const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
|
|
236
|
+
if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
|
|
237
|
+
if (!intent.platforms.length) { const detected = detectPlatforms(intent.projectRoot, intent.scope, intent.home); if (!detected.length) return { ok: false, code: "INPUT_REQUIRED", diagnostics: [{ code: "INPUT_REQUIRED", message: text(language, "noPlatform") }] }; intent.platforms = detected; }
|
|
238
|
+
let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
|
|
239
|
+
const manifest = readManifest(intent);
|
|
240
|
+
const mattReceipt = readJson(mattReceiptPath(intent.projectRoot));
|
|
241
|
+
intent.platforms = [...new Set([...Object.keys(manifest?.platforms ?? {}), ...intent.platforms])];
|
|
242
|
+
const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, mattReceipt, id, mattContentHashes));
|
|
243
|
+
const existingConfig = readConfig(configPath(intent));
|
|
244
|
+
const existingDefault = ["prim", "arch"].includes(existingConfig.default_orchestration) ? existingConfig.default_orchestration
|
|
245
|
+
: ["prim", "arch"].includes(manifest?.orchestration?.default) ? manifest.orchestration.default : null;
|
|
246
|
+
intent.defaultOrchestration = intent.defaultOrchestration ?? existingDefault ?? (intent.matt === "missing" && intent.nonInteractive ? "arch" : "prim");
|
|
247
|
+
const actions = observations.map((item) => ({ platform: item.id, matrix: actionFor(item, intent.matrixPolicy), matt: intent.matt === "missing" ? { kind: "reconcile", skills: [...MATT_SKILLS] } : { kind: "keep", skills: [] } }));
|
|
248
|
+
const blocked = actions.filter((item) => item.matrix === "blocked");
|
|
249
|
+
const blockedMatt = intent.matt === "missing" && intent.mattPolicy !== "replace"
|
|
250
|
+
? observations.filter((item) => (item.matt.classificationCounts["user-modified"] ?? 0) > 0 || (item.matt.classificationCounts.unreadable ?? 0) > 0)
|
|
251
|
+
: [];
|
|
252
|
+
const archMustRemainAvailable = manifest?.orchestration?.available?.includes("arch") || intent.defaultOrchestration === "arch";
|
|
253
|
+
const mattReady = trustedMattReceipt(mattReceipt) && observations.every(mattObservationMatchesReceipt);
|
|
254
|
+
const incompleteWithoutInstall = intent.matt !== "readonly" && intent.matt !== "missing" && archMustRemainAvailable && !mattReady;
|
|
255
|
+
const plan = { version: VERSION, intent, catalogDigest: catalog.digest, manifestPath: manifestPath(intent.scope, intent.projectRoot, intent.home), configPath: configPath(intent), actions, observations, preconditionDigest: digest(observations) };
|
|
256
|
+
plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
|
|
257
|
+
const code = blocked.length ? "USER_MODIFIED_BLOCKED" : blockedMatt.length ? "MATT_USER_MODIFIED" : incompleteWithoutInstall ? "ARCH_INSTALLATION_INCOMPLETE" : "OK";
|
|
258
|
+
return { ok: code === "OK", code, intent, observations, actions, plan, diagnostics: [
|
|
259
|
+
...blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: text(intent.language, "matrixModifiedDiagnostic").replace("{platform}", item.platform) })),
|
|
260
|
+
...blockedMatt.map((item) => ({ code: "MATT_USER_MODIFIED", platform: item.id, states: item.matt.classificationCounts, message: text(intent.language, "mattModifiedDiagnostic").replace("{platform}", item.name) })),
|
|
261
|
+
...(incompleteWithoutInstall ? [{ code: "ARCH_INSTALLATION_INCOMPLETE", message: text(intent.language, "archIncompleteDiagnostic") }] : [])
|
|
262
|
+
], summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${localizedAction(intent.language, item.matrix)}`).join(", ")}` };
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return { evaluate,
|
|
266
|
+
diagnose(rawIntent) {
|
|
267
|
+
const evaluation = evaluate(rawIntent); if (!evaluation.intent) return evaluation;
|
|
268
|
+
const diagnostics = []; const pending = readJson(transactionPath(evaluation.intent)); const manifest = readManifest(evaluation.intent);
|
|
269
|
+
if (pending) diagnostics.push({ code: "RECOVERY_REQUIRED", message: text(evaluation.intent.language, "transactionPending") });
|
|
270
|
+
if (!manifest) diagnostics.push({ code: "MANIFEST_MISSING", message: text(evaluation.intent.language, "manifestMissing") });
|
|
271
|
+
for (const observation of evaluation.observations ?? []) {
|
|
272
|
+
diagnostics.push({ code: `MATRIX_${observation.matrix.toUpperCase().replaceAll("-", "_")}`, platform: observation.id, message: text(evaluation.intent.language, "matrixStateDiagnostic").replace("{platform}", observation.name).replace("{state}", localizedState(evaluation.intent.language, observation.matrix)) });
|
|
273
|
+
const matrixRoot = path.join(observation.root, "matrix");
|
|
274
|
+
if (pathExists(matrixRoot) && (!exists(matrixRoot) || !exists(path.join(matrixRoot, "scripts", "matrix-runtime.mjs")) || !exists(path.join(matrixRoot, "scripts", "workflow-transaction.js")))) diagnostics.push({ code: "RUNTIME_MISSING", platform: observation.id, message: `${observation.name}: ${text(evaluation.intent.language, "runtimeMissing")}` });
|
|
275
|
+
for (const skill of observation.skills.filter((entry) => isBrokenLink(entry.target))) diagnostics.push({ code: "BROKEN_LINK", platform: observation.id, path: skill.target, message: `${observation.name}: ${text(evaluation.intent.language, "brokenLink")} ${skill.target}` });
|
|
276
|
+
if (observation.id === "codex" && PLATFORMS.codex.legacyRoots(scopeBase(evaluation.intent.scope, evaluation.intent.projectRoot, evaluation.intent.home)).some(exists)) diagnostics.push({ code: "CODEX_LEGACY_DETECTED", platform: observation.id, message: text(evaluation.intent.language, "codexLegacyDiagnostic") });
|
|
277
|
+
diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: text(evaluation.intent.language, "mattStateDiagnostic").replace("{platform}", observation.name).replace("{state}", localizedState(evaluation.intent.language, observation.matt.state)) });
|
|
278
|
+
if (observation.matt.ignoredGlobal) diagnostics.push({ code: "MATT_GLOBAL_IGNORED", platform: observation.id, count: observation.matt.ignoredGlobal, message: `${observation.name}: ${observation.matt.ignoredGlobal} ${text(evaluation.intent.language, "mattGlobalIgnored")}` });
|
|
279
|
+
if (observation.matt.unmanaged.length) diagnostics.push({ code: "ARCH_UNMANAGED_EXTRA", platform: observation.id, skills: observation.matt.unmanaged, message: text(evaluation.intent.language, "unmanagedExtraDiagnostic").replace("{platform}", observation.name).replace("{skills}", observation.matt.unmanaged.join(", ")) });
|
|
280
|
+
}
|
|
281
|
+
return { ...evaluation, diagnosis: diagnostics };
|
|
282
|
+
},
|
|
283
|
+
commit(plan) {
|
|
284
|
+
if (!plan?.seal || plan.seal !== digest({ intent: plan.intent, catalogDigest: plan.catalogDigest, actions: plan.actions, preconditionDigest: plan.preconditionDigest })) return { ok: false, code: "INVALID_INTENT" };
|
|
285
|
+
const recovery = recoverPendingTransaction(plan.intent, { allowMattWrites: plan.intent.matt !== "readonly" }); if (!recovery.ok) return recovery;
|
|
286
|
+
const refreshed = evaluate({ ...plan.intent, platforms: plan.intent.platforms });
|
|
287
|
+
if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
|
|
288
|
+
if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
|
|
289
|
+
if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
|
|
290
|
+
const previousManifest = readManifest(plan.intent);
|
|
291
|
+
const previousMattReceipt = readJson(mattReceiptPath(plan.intent.projectRoot));
|
|
292
|
+
const root = installationRoot(plan.intent.scope, plan.intent.projectRoot, plan.intent.home); const id = nowStamp(); const stagingRoot = path.join(root, "staging", id); const journalFile = transactionPath(plan.intent);
|
|
293
|
+
const mattActions = plan.actions.filter((item) => item.matt.kind === "reconcile");
|
|
294
|
+
const journal = {
|
|
295
|
+
id, state: "in-progress", stagingRoot,
|
|
296
|
+
manifest: { path: plan.manifestPath, existed: pathExists(plan.manifestPath), backup: null, written: false },
|
|
297
|
+
config: { path: plan.configPath, existed: pathExists(plan.configPath), backup: null, written: false },
|
|
298
|
+
mattReceipt: { path: mattReceiptPath(plan.intent.projectRoot), existed: pathExists(mattReceiptPath(plan.intent.projectRoot)), backup: null, written: false },
|
|
299
|
+
operations: []
|
|
300
|
+
};
|
|
301
|
+
for (const item of refreshed.observations) if (plan.actions.find((entry) => entry.platform === item.id).matrix !== "keep") for (const skill of MATRIX_SKILLS) journal.operations.push({ target: path.join(item.root, skill), backup: null, status: "pending" });
|
|
302
|
+
for (const action of mattActions) {
|
|
303
|
+
const observation = refreshed.observations.find((item) => item.id === action.platform);
|
|
304
|
+
for (const skill of action.matt.skills) journal.operations.push({ kind: "matt", platform: action.platform, skill, target: path.join(observation.matt.root, skill), backup: null, status: "pending" });
|
|
305
|
+
}
|
|
306
|
+
const persist = () => writeJson(journalFile, journal);
|
|
307
|
+
let mattCandidate = null;
|
|
308
|
+
try {
|
|
309
|
+
persist(); const catalog = releaseCatalog(plan.intent.language); const stagedPlatforms = new Map();
|
|
310
|
+
for (const item of refreshed.observations) {
|
|
311
|
+
if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
|
|
312
|
+
const platformStage = path.join(stagingRoot, item.id);
|
|
313
|
+
for (const skill of MATRIX_SKILLS) { const staged = path.join(platformStage, skill); fs.mkdirSync(path.dirname(staged), { recursive: true }); fs.cpSync(path.join(catalog.root, skill), staged, { recursive: true }); fs.mkdirSync(path.join(staged, "scripts"), { recursive: true }); fs.writeFileSync(path.join(staged, "scripts", "package.json"), '{"type":"module"}\n'); fs.copyFileSync(workflowRuntimeSource(), path.join(staged, "scripts", "matrix-runtime.mjs")); fs.copyFileSync(workflowTransactionSource(), path.join(staged, "scripts", "workflow-transaction.js")); fs.copyFileSync(mattCatalogSource(), path.join(staged, "scripts", "matt-catalog.mjs")); if (skill === "matrix") for (const filename of ["matrix_state.py", "matrix_claude.py"]) fs.copyFileSync(path.join(sourceSkillsRoot("en"), "matrix", "scripts", filename), path.join(staged, "scripts", filename)); if (!exists(path.join(staged, "SKILL.md"))) throw new Error(`Stage validation failed: ${item.id}/${skill}`); }
|
|
314
|
+
stagedPlatforms.set(item.id, platformStage);
|
|
315
|
+
}
|
|
316
|
+
if (mattActions.length) {
|
|
317
|
+
if (typeof mattAdapter.prepareCandidate !== "function") {
|
|
318
|
+
const restored = recoverPendingTransaction(plan.intent);
|
|
319
|
+
return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: "Matt adapter cannot provide an isolated candidate. Run matrix init --with-mattpocock with the supported Matrix CLI.", matt: [{ ok: false, code: "MATT_DIRECT_INSTALL_UNSUPPORTED" }], recoveryReceipt: restored.receipt ?? recovery.receipt };
|
|
320
|
+
}
|
|
321
|
+
mattCandidate = mattAdapter.prepareCandidate({ platforms: mattActions.map((item) => item.platform), skills: MATT_SKILLS });
|
|
322
|
+
if (!mattCandidate.ok) {
|
|
323
|
+
const restored = recoverPendingTransaction(plan.intent);
|
|
324
|
+
return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: mattCandidate.recovery, matt: [mattCandidate], recoveryReceipt: restored.receipt ?? recovery.receipt };
|
|
325
|
+
}
|
|
326
|
+
candidateVerifier(mattCandidate, mattActions);
|
|
327
|
+
}
|
|
328
|
+
failAt?.("after-stage"); let operationIndex = 0;
|
|
329
|
+
for (const item of refreshed.observations) {
|
|
330
|
+
if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
|
|
331
|
+
fs.mkdirSync(item.root, { recursive: true });
|
|
332
|
+
for (const skill of MATRIX_SKILLS) {
|
|
333
|
+
const operation = journal.operations[operationIndex++]; const target = operation.target; operation.status = "backing-up";
|
|
334
|
+
if (pathExists(target)) operation.backup = path.join(root, "backups", id, item.id, skill); persist();
|
|
335
|
+
if (operation.backup) { fs.mkdirSync(path.dirname(operation.backup), { recursive: true }); fs.renameSync(target, operation.backup); }
|
|
336
|
+
operation.status = "backed-up"; persist(); failAt?.("after-backup", { platform: item.id, skill });
|
|
337
|
+
const staged = path.join(stagedPlatforms.get(item.id), skill);
|
|
338
|
+
if (plan.intent.mode === "symlink") { const store = path.join(root, "skills", catalog.digest, skill); if (!pathExists(store)) { fs.mkdirSync(path.dirname(store), { recursive: true }); fs.renameSync(staged, store); } fs.symlinkSync(store, target, process.platform === "win32" ? "junction" : "dir"); } else fs.renameSync(staged, target);
|
|
339
|
+
operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const mattResults = [];
|
|
343
|
+
if (mattCandidate) {
|
|
344
|
+
for (const operation of journal.operations.filter((item) => item.kind === "matt")) {
|
|
345
|
+
const source = path.join(mattCandidate.platforms[operation.platform], operation.skill);
|
|
346
|
+
if (pathExists(operation.target)) {
|
|
347
|
+
const actual = hashDirectory(operation.target); const candidateHash = hashDirectory(source);
|
|
348
|
+
if (actual === candidateHash) continue;
|
|
349
|
+
const recorded = previousMattReceipt?.platforms?.[operation.platform]?.skills?.[operation.skill]?.hash
|
|
350
|
+
?? previousManifest?.platforms?.[operation.platform]?.matt?.skills?.[operation.skill]?.hash;
|
|
351
|
+
if ((!recorded || actual !== recorded) && plan.intent.mattPolicy !== "replace") {
|
|
352
|
+
const error = new Error(`Matt target is user-modified: ${operation.target}`);
|
|
353
|
+
error.code = "MATT_USER_MODIFIED";
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
operation.status = "backing-up";
|
|
357
|
+
operation.backup = path.join(root, "backups", id, "matt", operation.platform, operation.skill); persist();
|
|
358
|
+
fs.mkdirSync(path.dirname(operation.backup), { recursive: true });
|
|
359
|
+
fs.renameSync(operation.target, operation.backup);
|
|
360
|
+
operation.status = "backed-up"; persist();
|
|
361
|
+
}
|
|
362
|
+
fs.mkdirSync(path.dirname(operation.target), { recursive: true });
|
|
363
|
+
fs.cpSync(source, operation.target, { recursive: true });
|
|
364
|
+
operation.status = "committed"; persist(); failAt?.("after-matt-commit", { platform: operation.platform, skill: operation.skill });
|
|
365
|
+
}
|
|
366
|
+
for (const action of mattActions) mattResults.push({ platform: action.platform, ok: true, code: "OK", attempted: [action.platform] });
|
|
367
|
+
}
|
|
368
|
+
const postObservations = plan.intent.platforms.map((platform) => observePlatform(plan.intent, catalog, readManifest(plan.intent), previousMattReceipt, platform, mattContentHashes));
|
|
369
|
+
const incomplete = mattActions.length ? postObservations.filter((item) => !mattObservationMatchesRelease(item, mattContentHashes)) : [];
|
|
370
|
+
const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
|
|
371
|
+
if (mattResults.some((item) => !item.ok) || incomplete.length) {
|
|
372
|
+
if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate);
|
|
373
|
+
const restored = recoverPendingTransaction(plan.intent);
|
|
374
|
+
return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: "The platform was not added. Repair the Arch Skill installation and rerun matrix init --with-mattpocock.", matt: mattResults, recoveryReceipt: restored.receipt ?? recovery.receipt };
|
|
375
|
+
}
|
|
376
|
+
if (journal.manifest.existed) { journal.manifest.backup = path.join(root, "backups", id, "installation.json"); persist(); fs.mkdirSync(path.dirname(journal.manifest.backup), { recursive: true }); fs.renameSync(plan.manifestPath, journal.manifest.backup); }
|
|
377
|
+
if (journal.config.existed) { journal.config.backup = path.join(root, "backups", id, "config.yaml"); persist(); fs.mkdirSync(path.dirname(journal.config.backup), { recursive: true }); fs.renameSync(plan.configPath, journal.config.backup); }
|
|
378
|
+
if (mattActions.length && journal.mattReceipt.existed) { journal.mattReceipt.backup = path.join(root, "backups", id, "matt-installation.json"); persist(); fs.mkdirSync(path.dirname(journal.mattReceipt.backup), { recursive: true }); fs.renameSync(journal.mattReceipt.path, journal.mattReceipt.backup); }
|
|
379
|
+
const records = Object.fromEntries(postObservations.map((item) => {
|
|
380
|
+
const legacyMatt = previousManifest?.platforms?.[item.id]?.matt ?? null;
|
|
381
|
+
return [item.id, {
|
|
382
|
+
root: item.root,
|
|
383
|
+
skills: Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, { hash: hashDirectory(path.join(item.root, skill)) }])),
|
|
384
|
+
...(legacyMatt ? { matt: legacyMatt } : {})
|
|
385
|
+
}];
|
|
386
|
+
}));
|
|
387
|
+
const available = plan.intent.matt === "readonly" ? (previousManifest?.orchestration?.available ?? ["prim"])
|
|
388
|
+
: mattActions.length ? (postObservations.every((observation) => mattObservationMatchesRelease(observation, mattContentHashes)) ? ["prim", "arch"] : ["prim"])
|
|
389
|
+
: (previousManifest?.orchestration?.available ?? ["prim"]);
|
|
390
|
+
const previousConfig = readConfig(journal.config.backup ?? plan.configPath);
|
|
391
|
+
fs.mkdirSync(path.dirname(plan.configPath), { recursive: true });
|
|
276
392
|
fs.writeFileSync(plan.configPath, configContent(previousConfig, plan.intent.defaultOrchestration, plan.intent.scope, plan.intent.language)); journal.config.written = true; persist();
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
393
|
+
if (mattActions.length && available.includes("arch")) {
|
|
394
|
+
writeJson(journal.mattReceipt.path, {
|
|
395
|
+
version: 1,
|
|
396
|
+
compatibility: MATT_COMPATIBILITY,
|
|
397
|
+
catalogDigest: MATT_CATALOG_DIGEST,
|
|
398
|
+
platforms: Object.fromEntries(postObservations.map((item) => [item.id, { root: item.matt.root, skills: item.matt.skills }]))
|
|
399
|
+
});
|
|
400
|
+
journal.mattReceipt.written = true; persist();
|
|
401
|
+
}
|
|
402
|
+
writeJson(plan.manifestPath, {
|
|
403
|
+
version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, scope: plan.intent.scope,
|
|
404
|
+
catalogDigest: catalog.digest, orchestration: { default: plan.intent.defaultOrchestration, available }, platforms: records
|
|
405
|
+
});
|
|
406
|
+
journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
|
|
407
|
+
if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate);
|
|
408
|
+
removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journal.config.backup); removeEntry(journal.mattReceipt.backup); removeEntry(journalFile);
|
|
409
|
+
return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
|
|
410
|
+
} catch (error) { if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate); const restored = recoverPendingTransaction(plan.intent); return { ok: false, code: error.code === "MATT_USER_MODIFIED" ? error.code : "MATRIX_COMMIT_FAILED", error: error.message, recovery: restored.ok ? "Matrix targets and installation manifest were restored." : restored.recovery }; }
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|