@nseng-ai/harness-artifacts 0.1.2
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/package.json +32 -0
- package/src/api.ts +190 -0
- package/src/artifact-catalog.ts +65 -0
- package/src/diagnostic-sort.ts +14 -0
- package/src/filesystem.ts +181 -0
- package/src/first-party-catalog.ts +44 -0
- package/src/first-party-skill-provisioning.ts +162 -0
- package/src/fs-state.ts +22 -0
- package/src/harness-paths.ts +217 -0
- package/src/index.ts +1 -0
- package/src/module-artifact-declaration.ts +273 -0
- package/src/module-artifact-discovery.ts +416 -0
- package/src/ns/command.ts +32 -0
- package/src/ns/commands/install.ts +31 -0
- package/src/ns/commands/list.ts +23 -0
- package/src/ns/commands/path.ts +26 -0
- package/src/ns/commands/update.ts +28 -0
- package/src/ns/preinstalled-catalog.ts +26 -0
- package/src/ns/repo-local-ns-extension.ts +21 -0
- package/src/ns/skills-install.ts +141 -0
- package/src/ns/skills-list.ts +43 -0
- package/src/ns/skills-path.ts +53 -0
- package/src/ns/skills-shared.ts +65 -0
- package/src/ns/update.ts +99 -0
- package/src/ns-toml.ts +195 -0
- package/src/provision-apply.ts +350 -0
- package/src/provision-plan.ts +389 -0
- package/src/reconcile.ts +583 -0
- package/src/skill-frontmatter.ts +151 -0
- package/src/skill-mirror-conventions.ts +128 -0
- package/src/skills-lockfile.ts +107 -0
- package/src/sort.ts +3 -0
package/src/reconcile.ts
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { optionalEntry } from "@nseng-ai/foundation/primitives";
|
|
4
|
+
import { resultErr, resultOk, type Result } from "@nseng-ai/foundation/result";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { type SkillHarnessArtifactEntry } from "./artifact-catalog.ts";
|
|
8
|
+
import {
|
|
9
|
+
listFirstPartySkillArtifacts,
|
|
10
|
+
NS_FIRST_PARTY_HARNESS_ARTIFACT_CATALOG,
|
|
11
|
+
} from "./first-party-catalog.ts";
|
|
12
|
+
import {
|
|
13
|
+
FIRST_PARTY_SKILL_CATALOG_SOURCE_UNAVAILABLE_MESSAGE,
|
|
14
|
+
FIRST_PARTY_SKILL_CATALOG_SOURCE_VERSION,
|
|
15
|
+
firstPartySkillProvisionPathContext,
|
|
16
|
+
resolveFirstPartyCatalogSourceRoot,
|
|
17
|
+
} from "./first-party-skill-provisioning.ts";
|
|
18
|
+
import {
|
|
19
|
+
ALL_HARNESS_IDS,
|
|
20
|
+
HARNESS_SCOPES,
|
|
21
|
+
resolveHarnessSkillRoot,
|
|
22
|
+
type HarnessId,
|
|
23
|
+
type HarnessPathErrorInfo,
|
|
24
|
+
type HarnessScope,
|
|
25
|
+
} from "./harness-paths.ts";
|
|
26
|
+
import {
|
|
27
|
+
discoverExtensionModuleHarnessArtifacts,
|
|
28
|
+
moduleArtifactDiscoveryDiagnosticSchema,
|
|
29
|
+
type HarnessArtifactModuleDiscoveryGateway,
|
|
30
|
+
} from "./module-artifact-discovery.ts";
|
|
31
|
+
import { parseNsTomlHarnesses, type NsTomlErrorInfo } from "./ns-toml.ts";
|
|
32
|
+
import {
|
|
33
|
+
applyPreparedProvision,
|
|
34
|
+
INSTALL_MANIFEST_FILE_NAME,
|
|
35
|
+
nodeHarnessArtifactFileSystemGateway,
|
|
36
|
+
prepareProvision,
|
|
37
|
+
readInstallManifestAtRoot,
|
|
38
|
+
type HarnessArtifactFileSystemErrorInfo,
|
|
39
|
+
type HarnessArtifactFileSystemGateway,
|
|
40
|
+
type HarnessArtifactProvisionErrorInfo,
|
|
41
|
+
type HarnessArtifactProvisionPreview,
|
|
42
|
+
} from "./provision-apply.ts";
|
|
43
|
+
import {
|
|
44
|
+
provisionIdentityKey,
|
|
45
|
+
type InstallManifestData,
|
|
46
|
+
type InstallManifestEntryData,
|
|
47
|
+
} from "./provision-plan.ts";
|
|
48
|
+
import { sortStrings } from "./sort.ts";
|
|
49
|
+
|
|
50
|
+
export interface DesiredHarnessArtifact {
|
|
51
|
+
artifact: SkillHarnessArtifactEntry;
|
|
52
|
+
sourceRoot: string;
|
|
53
|
+
sourceVersion: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface HarnessManifestSnapshot {
|
|
57
|
+
harness: HarnessId;
|
|
58
|
+
targetRoot: string;
|
|
59
|
+
manifestPath: string;
|
|
60
|
+
manifest: InstallManifestData;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ReconcilePair {
|
|
64
|
+
key: string;
|
|
65
|
+
desired: DesiredHarnessArtifact;
|
|
66
|
+
harness: HarnessId;
|
|
67
|
+
scope: HarnessScope;
|
|
68
|
+
origin: "declared" | "manifest";
|
|
69
|
+
hasManifestEntry: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const harnessSchema = z.enum(ALL_HARNESS_IDS);
|
|
73
|
+
const scopeSchema = z.enum(HARNESS_SCOPES);
|
|
74
|
+
|
|
75
|
+
export const orphanedManifestEntrySchema = z.object({
|
|
76
|
+
artifactId: z.string(),
|
|
77
|
+
harness: harnessSchema,
|
|
78
|
+
scope: scopeSchema,
|
|
79
|
+
targetRoot: z.string(),
|
|
80
|
+
packageName: z.string(),
|
|
81
|
+
sourceType: z.enum(["first-party", "npm-module"]),
|
|
82
|
+
});
|
|
83
|
+
export type OrphanedManifestEntry = z.output<typeof orphanedManifestEntrySchema>;
|
|
84
|
+
|
|
85
|
+
export const skippedArtifactCollisionSchema = z.object({
|
|
86
|
+
kind: z.enum(["id", "target-name"]),
|
|
87
|
+
value: z.string(),
|
|
88
|
+
packages: z.array(z.string()),
|
|
89
|
+
});
|
|
90
|
+
export type SkippedArtifactCollision = z.output<typeof skippedArtifactCollisionSchema>;
|
|
91
|
+
|
|
92
|
+
export function planHarnessArtifactReconcile(input: {
|
|
93
|
+
desired: readonly DesiredHarnessArtifact[];
|
|
94
|
+
harnessSelection: readonly HarnessId[] | undefined;
|
|
95
|
+
manifests: readonly HarnessManifestSnapshot[];
|
|
96
|
+
}): {
|
|
97
|
+
pairs: readonly ReconcilePair[];
|
|
98
|
+
orphans: readonly OrphanedManifestEntry[];
|
|
99
|
+
skippedDesired: readonly DesiredHarnessArtifact[];
|
|
100
|
+
skippedCollisions: readonly SkippedArtifactCollision[];
|
|
101
|
+
} {
|
|
102
|
+
const collisionPlan = planDesiredCollisionSkips(input.desired);
|
|
103
|
+
|
|
104
|
+
const skippedDesiredIdentities = new Set(
|
|
105
|
+
collisionPlan.skippedDesired.map((desired) => desiredManifestIdentityKey(desired)),
|
|
106
|
+
);
|
|
107
|
+
const desiredByManifestIdentity = new Map<string, DesiredHarnessArtifact>();
|
|
108
|
+
for (const desired of input.desired) {
|
|
109
|
+
desiredByManifestIdentity.set(desiredManifestIdentityKey(desired), desired);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pairsByKey = new Map<string, ReconcilePair>();
|
|
113
|
+
if (input.harnessSelection !== undefined) {
|
|
114
|
+
for (const desired of collisionPlan.provisionableDesired) {
|
|
115
|
+
for (const harness of input.harnessSelection) {
|
|
116
|
+
const key = reconcilePairKey({
|
|
117
|
+
harness,
|
|
118
|
+
scope: "project",
|
|
119
|
+
artifactId: desired.artifact.id,
|
|
120
|
+
});
|
|
121
|
+
pairsByKey.set(key, {
|
|
122
|
+
key,
|
|
123
|
+
desired,
|
|
124
|
+
harness,
|
|
125
|
+
scope: "project",
|
|
126
|
+
origin: "declared",
|
|
127
|
+
hasManifestEntry: manifestHasEntry(input.manifests, key),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const orphans: OrphanedManifestEntry[] = [];
|
|
134
|
+
for (const snapshot of input.manifests) {
|
|
135
|
+
for (const entry of Object.values(snapshot.manifest.artifacts)) {
|
|
136
|
+
const desired = desiredByManifestIdentity.get(manifestEntryDesiredIdentityKey(entry));
|
|
137
|
+
if (desired === undefined) {
|
|
138
|
+
orphans.push({
|
|
139
|
+
artifactId: entry.artifactId,
|
|
140
|
+
harness: entry.harness,
|
|
141
|
+
scope: entry.scope,
|
|
142
|
+
targetRoot: entry.targetRoot,
|
|
143
|
+
packageName: entry.source.packageName,
|
|
144
|
+
sourceType: entry.source.type,
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (skippedDesiredIdentities.has(manifestEntryDesiredIdentityKey(entry))) continue;
|
|
149
|
+
const key = reconcilePairKey({
|
|
150
|
+
harness: entry.harness,
|
|
151
|
+
scope: entry.scope,
|
|
152
|
+
artifactId: entry.artifactId,
|
|
153
|
+
});
|
|
154
|
+
if (pairsByKey.has(key)) continue;
|
|
155
|
+
pairsByKey.set(key, {
|
|
156
|
+
key,
|
|
157
|
+
desired,
|
|
158
|
+
harness: entry.harness,
|
|
159
|
+
scope: entry.scope,
|
|
160
|
+
origin: "manifest",
|
|
161
|
+
hasManifestEntry: true,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
pairs: [...pairsByKey.values()].sort((left, right) => left.key.localeCompare(right.key)),
|
|
168
|
+
orphans: orphans.sort((left, right) =>
|
|
169
|
+
`${left.harness}\0${left.artifactId}`.localeCompare(`${right.harness}\0${right.artifactId}`),
|
|
170
|
+
),
|
|
171
|
+
skippedDesired: collisionPlan.skippedDesired,
|
|
172
|
+
skippedCollisions: collisionPlan.skippedCollisions,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface RunHarnessArtifactReconcileRequest {
|
|
177
|
+
projectRoot: string;
|
|
178
|
+
homeDir?: string;
|
|
179
|
+
env: Record<string, string | undefined>;
|
|
180
|
+
isDryRun: boolean;
|
|
181
|
+
shouldForce: boolean;
|
|
182
|
+
fs?: HarnessArtifactFileSystemGateway;
|
|
183
|
+
discoveryGateway?: HarnessArtifactModuleDiscoveryGateway;
|
|
184
|
+
firstPartySourceRoot?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const harnessSelectionStateSchema = z.discriminatedUnion("type", [
|
|
188
|
+
z.object({ type: z.literal("ns-toml"), harnesses: z.array(harnessSchema) }),
|
|
189
|
+
z.object({ type: z.literal("missing") }),
|
|
190
|
+
]);
|
|
191
|
+
export type HarnessSelectionState = z.output<typeof harnessSelectionStateSchema>;
|
|
192
|
+
|
|
193
|
+
export const reconcileArtifactOutcomeSchema = z.object({
|
|
194
|
+
action: z.enum(["installed", "refreshed", "unchanged", "conflicted", "skipped"]),
|
|
195
|
+
artifactId: z.string(),
|
|
196
|
+
skillName: z.string(),
|
|
197
|
+
harness: harnessSchema,
|
|
198
|
+
scope: scopeSchema,
|
|
199
|
+
origin: z.enum(["declared", "manifest"]),
|
|
200
|
+
sourceType: z.enum(["first-party", "npm-module"]),
|
|
201
|
+
packageName: z.string(),
|
|
202
|
+
targetArtifactPath: z.string(),
|
|
203
|
+
manifestPath: z.string(),
|
|
204
|
+
writtenFiles: z.array(z.string()),
|
|
205
|
+
conflictingFiles: z.array(z.string()),
|
|
206
|
+
});
|
|
207
|
+
export type ReconcileArtifactOutcome = z.output<typeof reconcileArtifactOutcomeSchema>;
|
|
208
|
+
|
|
209
|
+
export const reconcileReportSchema = z.object({
|
|
210
|
+
mode: z.enum(["dry-run", "applied"]),
|
|
211
|
+
harnessSelection: harnessSelectionStateSchema,
|
|
212
|
+
artifacts: z.array(reconcileArtifactOutcomeSchema),
|
|
213
|
+
orphans: z.array(orphanedManifestEntrySchema),
|
|
214
|
+
diagnostics: z.array(moduleArtifactDiscoveryDiagnosticSchema),
|
|
215
|
+
skippedCollisions: z.array(skippedArtifactCollisionSchema),
|
|
216
|
+
isForceRequired: z.boolean(),
|
|
217
|
+
});
|
|
218
|
+
export type ReconcileReport = z.output<typeof reconcileReportSchema>;
|
|
219
|
+
|
|
220
|
+
export type ReconcileErrorInfo =
|
|
221
|
+
| HarnessArtifactProvisionErrorInfo
|
|
222
|
+
| HarnessArtifactFileSystemErrorInfo
|
|
223
|
+
| HarnessPathErrorInfo
|
|
224
|
+
| { code: "invalid_ns_toml"; message: string; details: { path: string; error: NsTomlErrorInfo } }
|
|
225
|
+
| {
|
|
226
|
+
code: "first_party_source_root_unavailable";
|
|
227
|
+
message: string;
|
|
228
|
+
details: { catalogId: string };
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export async function runHarnessArtifactReconcile(
|
|
232
|
+
request: RunHarnessArtifactReconcileRequest,
|
|
233
|
+
): Promise<Result<ReconcileReport, ReconcileErrorInfo>> {
|
|
234
|
+
const fs = request.fs ?? nodeHarnessArtifactFileSystemGateway;
|
|
235
|
+
const discoveryGateway = request.discoveryGateway ?? nodeHarnessArtifactFileSystemGateway;
|
|
236
|
+
const moduleDiscovery = await discoverExtensionModuleHarnessArtifacts({
|
|
237
|
+
projectRoot: request.projectRoot,
|
|
238
|
+
...optionalEntry("homeDir", request.homeDir),
|
|
239
|
+
env: request.env,
|
|
240
|
+
gateway: discoveryGateway,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const desired = desiredHarnessArtifacts({
|
|
244
|
+
moduleCatalogs: moduleDiscovery.catalogs,
|
|
245
|
+
firstPartySourceRoot: request.firstPartySourceRoot,
|
|
246
|
+
});
|
|
247
|
+
if (!desired.ok) return desired;
|
|
248
|
+
|
|
249
|
+
const nsTomlPath = join(request.projectRoot, "ns.toml");
|
|
250
|
+
const nsToml = await fs.readOptionalTextFile(nsTomlPath);
|
|
251
|
+
if (!nsToml.ok) return nsToml;
|
|
252
|
+
const selection = parseHarnessSelection(nsToml.value, nsTomlPath);
|
|
253
|
+
if (!selection.ok) return selection;
|
|
254
|
+
|
|
255
|
+
const context = firstPartySkillProvisionPathContext({
|
|
256
|
+
projectRoot: request.projectRoot,
|
|
257
|
+
...optionalEntry("homeDir", request.homeDir),
|
|
258
|
+
env: request.env,
|
|
259
|
+
});
|
|
260
|
+
const manifests = await readProjectManifestSnapshots({ context, fs });
|
|
261
|
+
if (!manifests.ok) return manifests;
|
|
262
|
+
|
|
263
|
+
const plan = planHarnessArtifactReconcile({
|
|
264
|
+
desired: desired.value,
|
|
265
|
+
harnessSelection: selection.value.harnessSelection,
|
|
266
|
+
manifests: manifests.value,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const artifacts: ReconcileArtifactOutcome[] = [];
|
|
270
|
+
for (const desired of plan.skippedDesired) {
|
|
271
|
+
artifacts.push(
|
|
272
|
+
...skippedCollisionOutcomes({
|
|
273
|
+
desired,
|
|
274
|
+
context,
|
|
275
|
+
harnesses: selection.value.harnessSelection,
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
for (const pair of plan.pairs) {
|
|
280
|
+
const prepared = await prepareProvision({
|
|
281
|
+
artifact: pair.desired.artifact,
|
|
282
|
+
harness: pair.harness,
|
|
283
|
+
scope: pair.scope,
|
|
284
|
+
context,
|
|
285
|
+
sourceRoot: pair.desired.sourceRoot,
|
|
286
|
+
sourceVersion: pair.desired.sourceVersion,
|
|
287
|
+
fs,
|
|
288
|
+
});
|
|
289
|
+
if (!prepared.ok) return prepared;
|
|
290
|
+
if (request.isDryRun) {
|
|
291
|
+
artifacts.push(
|
|
292
|
+
reconcileOutcomeFromProvision({
|
|
293
|
+
pair,
|
|
294
|
+
provision: prepared.value,
|
|
295
|
+
...(prepared.value.decisions.isForceRequired ? { action: "conflicted" as const } : {}),
|
|
296
|
+
writtenFiles: [],
|
|
297
|
+
conflictingFiles: prepared.value.decisions.files
|
|
298
|
+
.filter((decision) => decision.type === "locally-edited-conflict")
|
|
299
|
+
.map((decision) => decision.file.targetPath),
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const applied = await applyPreparedProvision(prepared.value, {
|
|
306
|
+
shouldForce: request.shouldForce,
|
|
307
|
+
});
|
|
308
|
+
if (!applied.ok) return applied;
|
|
309
|
+
if (applied.value.outcome === "conflicted") {
|
|
310
|
+
artifacts.push(
|
|
311
|
+
reconcileConflictedOutcome({
|
|
312
|
+
pair,
|
|
313
|
+
provision: applied.value,
|
|
314
|
+
conflictingFiles: applied.value.conflictingFiles,
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
artifacts.push(
|
|
320
|
+
reconcileOutcomeFromProvision({
|
|
321
|
+
pair,
|
|
322
|
+
provision: applied.value,
|
|
323
|
+
writtenFiles: applied.value.writtenFiles,
|
|
324
|
+
conflictingFiles: [],
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return resultOk({
|
|
330
|
+
mode: request.isDryRun ? "dry-run" : "applied",
|
|
331
|
+
harnessSelection: selection.value.state,
|
|
332
|
+
artifacts,
|
|
333
|
+
orphans: [...plan.orphans],
|
|
334
|
+
diagnostics: [...moduleDiscovery.diagnostics],
|
|
335
|
+
skippedCollisions: [...plan.skippedCollisions],
|
|
336
|
+
isForceRequired: artifacts.some((artifact) => artifact.action === "conflicted"),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function planDesiredCollisionSkips(desired: readonly DesiredHarnessArtifact[]): {
|
|
341
|
+
provisionableDesired: readonly DesiredHarnessArtifact[];
|
|
342
|
+
skippedDesired: readonly DesiredHarnessArtifact[];
|
|
343
|
+
skippedCollisions: readonly SkippedArtifactCollision[];
|
|
344
|
+
} {
|
|
345
|
+
const idCollisions = collisionsForKey(desired, (item) => item.artifact.id, "id");
|
|
346
|
+
const targetNameCollisions = collisionsForKey(
|
|
347
|
+
desired,
|
|
348
|
+
(item) => item.artifact.skillName,
|
|
349
|
+
"target-name",
|
|
350
|
+
);
|
|
351
|
+
const skipped = new Set([...idCollisions.skipped, ...targetNameCollisions.skipped]);
|
|
352
|
+
const collisions = [...idCollisions.collisions, ...targetNameCollisions.collisions].sort(
|
|
353
|
+
(left, right) => `${left.kind}\0${left.value}`.localeCompare(`${right.kind}\0${right.value}`),
|
|
354
|
+
);
|
|
355
|
+
return {
|
|
356
|
+
provisionableDesired: desired.filter((item) => !skipped.has(item)),
|
|
357
|
+
skippedDesired: desired.filter((item) => skipped.has(item)),
|
|
358
|
+
skippedCollisions: collisions,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function collisionsForKey(
|
|
363
|
+
desired: readonly DesiredHarnessArtifact[],
|
|
364
|
+
keyForItem: (item: DesiredHarnessArtifact) => string,
|
|
365
|
+
kind: SkippedArtifactCollision["kind"],
|
|
366
|
+
): {
|
|
367
|
+
collisions: readonly SkippedArtifactCollision[];
|
|
368
|
+
skipped: readonly DesiredHarnessArtifact[];
|
|
369
|
+
} {
|
|
370
|
+
const itemsByKey = new Map<string, DesiredHarnessArtifact[]>();
|
|
371
|
+
for (const item of desired) {
|
|
372
|
+
const key = keyForItem(item);
|
|
373
|
+
const items = itemsByKey.get(key) ?? [];
|
|
374
|
+
items.push(item);
|
|
375
|
+
itemsByKey.set(key, items);
|
|
376
|
+
}
|
|
377
|
+
const collisions: SkippedArtifactCollision[] = [];
|
|
378
|
+
const skipped: DesiredHarnessArtifact[] = [];
|
|
379
|
+
for (const [value, items] of itemsByKey) {
|
|
380
|
+
if (items.length < 2) continue;
|
|
381
|
+
skipped.push(...items);
|
|
382
|
+
collisions.push({
|
|
383
|
+
kind,
|
|
384
|
+
value,
|
|
385
|
+
packages: sortStrings([...new Set(items.map((item) => item.artifact.source.packageName))]),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return { collisions, skipped };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function desiredManifestIdentityKey(desired: DesiredHarnessArtifact): string {
|
|
392
|
+
return [
|
|
393
|
+
desired.artifact.id,
|
|
394
|
+
desired.artifact.source.type,
|
|
395
|
+
desired.artifact.source.packageName,
|
|
396
|
+
].join("\0");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function manifestEntryDesiredIdentityKey(entry: InstallManifestEntryData): string {
|
|
400
|
+
return [entry.artifactId, entry.source.type, entry.source.packageName].join("\0");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function reconcilePairKey(input: {
|
|
404
|
+
harness: HarnessId;
|
|
405
|
+
scope: HarnessScope;
|
|
406
|
+
artifactId: string;
|
|
407
|
+
}): string {
|
|
408
|
+
return provisionIdentityKey({ ...input, kind: "skill" });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function manifestHasEntry(manifests: readonly HarnessManifestSnapshot[], key: string): boolean {
|
|
412
|
+
return manifests.some((snapshot) => snapshot.manifest.artifacts[key] !== undefined);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function desiredHarnessArtifacts(input: {
|
|
416
|
+
moduleCatalogs: readonly {
|
|
417
|
+
moduleRoot: string;
|
|
418
|
+
version: string;
|
|
419
|
+
artifacts: readonly SkillHarnessArtifactEntry[];
|
|
420
|
+
}[];
|
|
421
|
+
firstPartySourceRoot: string | undefined;
|
|
422
|
+
}): Result<readonly DesiredHarnessArtifact[], ReconcileErrorInfo> {
|
|
423
|
+
const firstPartySourceRoot = input.firstPartySourceRoot ?? resolveFirstPartyCatalogSourceRoot();
|
|
424
|
+
if (firstPartySourceRoot === undefined) {
|
|
425
|
+
return resultErr({
|
|
426
|
+
code: "first_party_source_root_unavailable",
|
|
427
|
+
message: FIRST_PARTY_SKILL_CATALOG_SOURCE_UNAVAILABLE_MESSAGE,
|
|
428
|
+
details: { catalogId: NS_FIRST_PARTY_HARNESS_ARTIFACT_CATALOG.catalogId },
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return resultOk([
|
|
432
|
+
...listFirstPartySkillArtifacts().map((artifact) => ({
|
|
433
|
+
artifact,
|
|
434
|
+
sourceRoot: firstPartySourceRoot,
|
|
435
|
+
sourceVersion: FIRST_PARTY_SKILL_CATALOG_SOURCE_VERSION,
|
|
436
|
+
})),
|
|
437
|
+
...input.moduleCatalogs.flatMap((catalog) =>
|
|
438
|
+
catalog.artifacts.map((artifact) => ({
|
|
439
|
+
artifact,
|
|
440
|
+
sourceRoot: catalog.moduleRoot,
|
|
441
|
+
sourceVersion: catalog.version,
|
|
442
|
+
})),
|
|
443
|
+
),
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function parseHarnessSelection(
|
|
448
|
+
state: { type: "missing" } | { type: "file"; text: string },
|
|
449
|
+
nsTomlPath: string,
|
|
450
|
+
): Result<
|
|
451
|
+
{ state: HarnessSelectionState; harnessSelection: readonly HarnessId[] | undefined },
|
|
452
|
+
ReconcileErrorInfo
|
|
453
|
+
> {
|
|
454
|
+
if (state.type === "missing") {
|
|
455
|
+
return resultOk({ state: { type: "missing" }, harnessSelection: undefined });
|
|
456
|
+
}
|
|
457
|
+
const parsed = parseNsTomlHarnesses(state.text, nsTomlPath);
|
|
458
|
+
if (parsed.type === "error") {
|
|
459
|
+
return resultErr({
|
|
460
|
+
code: "invalid_ns_toml",
|
|
461
|
+
message: parsed.error.message,
|
|
462
|
+
details: { path: nsTomlPath, error: parsed.error },
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (parsed.type === "missing") {
|
|
466
|
+
return resultOk({ state: { type: "missing" }, harnessSelection: undefined });
|
|
467
|
+
}
|
|
468
|
+
return resultOk({
|
|
469
|
+
state: { type: "ns-toml", harnesses: [...parsed.harnesses] },
|
|
470
|
+
harnessSelection: parsed.harnesses,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function readProjectManifestSnapshots(input: {
|
|
475
|
+
context: ReturnType<typeof firstPartySkillProvisionPathContext>;
|
|
476
|
+
fs: HarnessArtifactFileSystemGateway;
|
|
477
|
+
}): Promise<Result<readonly HarnessManifestSnapshot[], ReconcileErrorInfo>> {
|
|
478
|
+
const snapshots: HarnessManifestSnapshot[] = [];
|
|
479
|
+
for (const harness of ALL_HARNESS_IDS) {
|
|
480
|
+
const root = resolveHarnessSkillRoot({ harness, scope: "project", context: input.context });
|
|
481
|
+
if (!root.ok) return root;
|
|
482
|
+
const manifest = await readInstallManifestAtRoot({
|
|
483
|
+
targetRoot: root.value.rootPath,
|
|
484
|
+
fs: input.fs,
|
|
485
|
+
});
|
|
486
|
+
if (!manifest.ok) return manifest;
|
|
487
|
+
if (Object.keys(manifest.value.artifacts).length === 0) continue;
|
|
488
|
+
snapshots.push({
|
|
489
|
+
harness,
|
|
490
|
+
targetRoot: root.value.rootPath,
|
|
491
|
+
manifestPath: join(root.value.rootPath, INSTALL_MANIFEST_FILE_NAME),
|
|
492
|
+
manifest: manifest.value,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
return resultOk(snapshots);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function classifyReconcileAction(input: {
|
|
499
|
+
decisionsAreUnchanged: boolean;
|
|
500
|
+
hasManifestEntry: boolean;
|
|
501
|
+
}): ReconcileArtifactOutcome["action"] {
|
|
502
|
+
if (input.decisionsAreUnchanged && input.hasManifestEntry) return "unchanged";
|
|
503
|
+
if (input.hasManifestEntry) return "refreshed";
|
|
504
|
+
return "installed";
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function skippedCollisionOutcomes(input: {
|
|
508
|
+
desired: DesiredHarnessArtifact;
|
|
509
|
+
context: ReturnType<typeof firstPartySkillProvisionPathContext>;
|
|
510
|
+
harnesses: readonly HarnessId[] | undefined;
|
|
511
|
+
}): readonly ReconcileArtifactOutcome[] {
|
|
512
|
+
const harnesses = input.harnesses ?? [];
|
|
513
|
+
return harnesses.map((harness) => {
|
|
514
|
+
const root = resolveHarnessSkillRoot({ harness, scope: "project", context: input.context });
|
|
515
|
+
if (!root.ok) throw new Error(root.error.message);
|
|
516
|
+
const targetArtifactPath = join(root.value.rootPath, input.desired.artifact.skillName);
|
|
517
|
+
return {
|
|
518
|
+
action: "skipped",
|
|
519
|
+
artifactId: input.desired.artifact.id,
|
|
520
|
+
skillName: input.desired.artifact.skillName,
|
|
521
|
+
harness,
|
|
522
|
+
scope: "project",
|
|
523
|
+
origin: "declared",
|
|
524
|
+
sourceType: input.desired.artifact.source.type,
|
|
525
|
+
packageName: input.desired.artifact.source.packageName,
|
|
526
|
+
targetArtifactPath,
|
|
527
|
+
manifestPath: join(root.value.rootPath, INSTALL_MANIFEST_FILE_NAME),
|
|
528
|
+
writtenFiles: [],
|
|
529
|
+
conflictingFiles: [],
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function reconcileConflictedOutcome(input: {
|
|
535
|
+
pair: ReconcilePair;
|
|
536
|
+
provision: HarnessArtifactProvisionPreview;
|
|
537
|
+
conflictingFiles: readonly string[];
|
|
538
|
+
}): ReconcileArtifactOutcome {
|
|
539
|
+
return {
|
|
540
|
+
action: "conflicted",
|
|
541
|
+
artifactId: input.pair.desired.artifact.id,
|
|
542
|
+
skillName: input.pair.desired.artifact.skillName,
|
|
543
|
+
harness: input.pair.harness,
|
|
544
|
+
scope: input.pair.scope,
|
|
545
|
+
origin: input.pair.origin,
|
|
546
|
+
sourceType: input.pair.desired.artifact.source.type,
|
|
547
|
+
packageName: input.pair.desired.artifact.source.packageName,
|
|
548
|
+
targetArtifactPath: input.provision.plan.targetArtifactPath,
|
|
549
|
+
manifestPath: input.provision.manifestPath,
|
|
550
|
+
writtenFiles: [],
|
|
551
|
+
conflictingFiles: [...input.conflictingFiles],
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function reconcileOutcomeFromProvision(input: {
|
|
556
|
+
pair: ReconcilePair;
|
|
557
|
+
provision: HarnessArtifactProvisionPreview;
|
|
558
|
+
action?: ReconcileArtifactOutcome["action"];
|
|
559
|
+
writtenFiles: readonly string[];
|
|
560
|
+
conflictingFiles: readonly string[];
|
|
561
|
+
}): ReconcileArtifactOutcome {
|
|
562
|
+
return {
|
|
563
|
+
action:
|
|
564
|
+
input.action ??
|
|
565
|
+
classifyReconcileAction({
|
|
566
|
+
decisionsAreUnchanged: input.provision.decisions.files.every(
|
|
567
|
+
(decision) => decision.type === "unchanged",
|
|
568
|
+
),
|
|
569
|
+
hasManifestEntry: input.pair.hasManifestEntry,
|
|
570
|
+
}),
|
|
571
|
+
artifactId: input.pair.desired.artifact.id,
|
|
572
|
+
skillName: input.pair.desired.artifact.skillName,
|
|
573
|
+
harness: input.pair.harness,
|
|
574
|
+
scope: input.pair.scope,
|
|
575
|
+
origin: input.pair.origin,
|
|
576
|
+
sourceType: input.pair.desired.artifact.source.type,
|
|
577
|
+
packageName: input.pair.desired.artifact.source.packageName,
|
|
578
|
+
targetArtifactPath: input.provision.plan.targetArtifactPath,
|
|
579
|
+
manifestPath: input.provision.manifestPath,
|
|
580
|
+
writtenFiles: [...input.writtenFiles],
|
|
581
|
+
conflictingFiles: [...input.conflictingFiles],
|
|
582
|
+
};
|
|
583
|
+
}
|