skill-family-engineering-kit 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/CHANGELOG.zh-CN.md +21 -0
- package/README.md +11 -12
- package/README.zh-CN.md +11 -12
- package/candidate/adoption-cli.mjs +67 -0
- package/candidate/adoption-mechanisms.mjs +69 -0
- package/candidate/index.mjs +3 -0
- package/candidate/profile-bundle.mjs +464 -15
- package/candidate/projection-bundle-cli.mjs +18 -0
- package/docs/agents/capability-catalog.en.json +1746 -1562
- package/docs/agents/capability-catalog.json +1029 -930
- package/docs/agents/capability-catalog.zh-CN.json +1746 -1562
- package/docs/architecture/index.html +10 -3
- package/docs/en/architecture/index.html +12 -3
- package/docs/public/status/index.html +3 -3
- package/docs/reference/api/harness/index.html +396 -1
- package/docs/search/search_index.json +1 -1
- package/package.json +3 -3
- package/release-notes/0.4.0.yaml +23 -0
- package/src/adopt-plan.mjs +26 -1
- package/src/index.mjs +5 -2
- package/src/migration.mjs +121 -1
- package/src/projection.mjs +1152 -266
package/package.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"url": "https://github.com/ifoohoo/skill-family-engineering-kit/issues"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"skill-family-contracts": "0.
|
|
12
|
-
"skill-family-harness-node": "0.
|
|
11
|
+
"skill-family-contracts": "0.4.0",
|
|
12
|
+
"skill-family-harness-node": "0.4.0"
|
|
13
13
|
},
|
|
14
14
|
"description": "Build-time scaffold, adoption planning, projection, and checks.",
|
|
15
15
|
"engines": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"url": "https://github.com/ifoohoo/skill-family-engineering-kit.git"
|
|
47
47
|
},
|
|
48
48
|
"type": "module",
|
|
49
|
-
"version": "0.
|
|
49
|
+
"version": "0.4.0",
|
|
50
50
|
"scripts": {
|
|
51
51
|
"check": "node --test",
|
|
52
52
|
"test": "node --test"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: 0.4.0
|
|
2
|
+
date: 2026-08-16
|
|
3
|
+
locales:
|
|
4
|
+
en:
|
|
5
|
+
summary: This release adds the candidate adoption CLI and adoption mechanisms to the Engineering Kit while preserving the stable four-command surface and the 0.3.0 offline bundle.
|
|
6
|
+
changes:
|
|
7
|
+
added:
|
|
8
|
+
- Adds the adoption-cli candidate, a stdin/stdout CLI that assesses adoption bindings, legacy exit lists, and legacy references through the migration manifest, and verifies managed-bundle identity and harness surface inventory.
|
|
9
|
+
- Adds the adoption-mechanisms candidate module as the shared implementation behind the adoption CLI.
|
|
10
|
+
changed:
|
|
11
|
+
- Keeps the stable scaffold, adopt-plan, projection, and check commands unchanged.
|
|
12
|
+
- Carries forward the 0.3.0 Quickstart Profile v2 offline bundle (standalone validators selected by schema $id, full provenance recording) and the candidate plugin skill naming checker.
|
|
13
|
+
upgradeNotes: Version 0.4.0 is released on npm and the public mirror. The adoption CLI is a candidate entry point; invoke it through its explicit candidate subpath and pin the package to exactly 0.4.0.
|
|
14
|
+
zh-CN:
|
|
15
|
+
summary: 本版为 Engineering Kit 新增候选 adoption CLI 与 adoption mechanisms,同时保持稳定四命令边界与 0.3.0 的离线 Bundle 不变。
|
|
16
|
+
changes:
|
|
17
|
+
added:
|
|
18
|
+
- 新增 adoption-cli 候选:基于 stdin/stdout 的 CLI,通过 migration manifest 评估 adoption 绑定、遗留 exit list 与遗留引用,并校验 managed-bundle 身份与 harness 表面清单。
|
|
19
|
+
- 新增 adoption-mechanisms 候选模块,作为 adoption CLI 的共享实现。
|
|
20
|
+
changed:
|
|
21
|
+
- 稳定四命令(scaffold、adopt-plan、projection、check)保持不变。
|
|
22
|
+
- 延续 0.3.0 的 Quickstart Profile v2 离线 Bundle(按 schema $id 选择的 standalone validator、完整 provenance 记录)与候选插件技能命名检查器。
|
|
23
|
+
upgradeNotes: 0.4.0 已发布到 npm 与 public 镜像仓。adoption CLI 是候选入口,须经显式 candidate 子路径调用,并把包精确锁定为 0.4.0。
|
package/src/adopt-plan.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { probeGitFacts, probeGitState } from "./gitprobe.mjs";
|
|
|
6
6
|
import {
|
|
7
7
|
assessAdoptionBinding,
|
|
8
8
|
assessLegacyExitList,
|
|
9
|
+
assessLegacyReferences,
|
|
9
10
|
assessVerificationEvidence,
|
|
10
11
|
evaluateMigrationCompletion,
|
|
11
12
|
findNestedRepositories,
|
|
@@ -89,6 +90,7 @@ export function buildHandoffDraft({
|
|
|
89
90
|
conflicts,
|
|
90
91
|
risks,
|
|
91
92
|
legacyExitList,
|
|
93
|
+
legacyReferenceExitList,
|
|
92
94
|
verificationFacts,
|
|
93
95
|
migrationState,
|
|
94
96
|
entries,
|
|
@@ -137,8 +139,17 @@ export function buildHandoffDraft({
|
|
|
137
139
|
});
|
|
138
140
|
|
|
139
141
|
// 5. The validator/builder/docs-pipeline/git-preflight slated for exit.
|
|
142
|
+
// Includes both whole-file exits (legacyInfra) and reference-level
|
|
143
|
+
// exits (legacyReferences) for retained files.
|
|
140
144
|
push(5, HANDOFF_FIELDS[4], "derived", {
|
|
141
145
|
exits: legacyExitList.map((item) => ({ path: item.path, replacedBy: item.replacedBy, status: item.status })),
|
|
146
|
+
referenceExits: (legacyReferenceExitList ?? []).map((ref) => ({
|
|
147
|
+
path: ref.path,
|
|
148
|
+
text: ref.text,
|
|
149
|
+
replacedBy: ref.replacedBy,
|
|
150
|
+
status: ref.status,
|
|
151
|
+
occurrenceCount: ref.occurrenceCount,
|
|
152
|
+
})),
|
|
142
153
|
});
|
|
143
154
|
|
|
144
155
|
// 6. Business logic that must be kept: never derivable from repo facts.
|
|
@@ -420,6 +431,17 @@ export async function planAdoption({
|
|
|
420
431
|
});
|
|
421
432
|
}
|
|
422
433
|
}
|
|
434
|
+
const legacyReferenceExitList = await assessLegacyReferences(rootAbs, migrationManifest?.legacyReferences ?? []);
|
|
435
|
+
for (const ref of legacyReferenceExitList) {
|
|
436
|
+
if (ref.status === "invalid") {
|
|
437
|
+
conflicts.push({
|
|
438
|
+
path: MIGRATION_MANIFEST_PATH,
|
|
439
|
+
kind: "legacy-path-containment",
|
|
440
|
+
code: "SFC2004",
|
|
441
|
+
detail: `legacy reference path rejected by harness containment, fail-closed: ${ref.path ?? "<malformed>"} (${ref.invalidKind ?? "unknown"}); user paths are only ever read inside the target root`,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
423
445
|
const exceptionFindings = [];
|
|
424
446
|
const declaredExceptions = Array.isArray(migrationManifest?.exceptions) ? migrationManifest.exceptions : [];
|
|
425
447
|
for (const [index, exception] of declaredExceptions.entries()) {
|
|
@@ -481,6 +503,7 @@ export async function planAdoption({
|
|
|
481
503
|
const completion = evaluateMigrationCompletion({
|
|
482
504
|
manifestDeclared,
|
|
483
505
|
legacyExitList,
|
|
506
|
+
legacyReferenceExitList,
|
|
484
507
|
exceptionFindings,
|
|
485
508
|
conflicts,
|
|
486
509
|
binding,
|
|
@@ -512,6 +535,7 @@ export async function planAdoption({
|
|
|
512
535
|
conflicts,
|
|
513
536
|
risks,
|
|
514
537
|
legacyExitList,
|
|
538
|
+
legacyReferenceExitList,
|
|
515
539
|
verificationFacts,
|
|
516
540
|
migrationState: completion.state,
|
|
517
541
|
entries,
|
|
@@ -555,6 +579,7 @@ export async function planAdoption({
|
|
|
555
579
|
manifestStatus: manifestState.status,
|
|
556
580
|
manifestPath: MIGRATION_MANIFEST_PATH,
|
|
557
581
|
legacyExitList,
|
|
582
|
+
legacyReferenceExitList,
|
|
558
583
|
exceptions: { declared: declaredExceptions.length, findings: exceptionFindings },
|
|
559
584
|
binding,
|
|
560
585
|
adoptionProof,
|
|
@@ -589,6 +614,6 @@ export async function planAdoption({
|
|
|
589
614
|
},
|
|
590
615
|
],
|
|
591
616
|
policy:
|
|
592
|
-
"strictly read-only: this plan changes nothing in the target (no files, no temp files, no git); every listed byte is computed from describeSkeletonFiles, the same source scaffold consumes; the kit never renames repositories or directories and never touches remotes — infrastructure adoption and repository renaming are separate decisions; migration completion additionally requires every legacy implementation to have exited (dual-track wiring alone is not completion)",
|
|
617
|
+
"strictly read-only: this plan changes nothing in the target (no files, no temp files, no git); every listed byte is computed from describeSkeletonFiles, the same source scaffold consumes; the kit never renames repositories or directories and never touches remotes — infrastructure adoption and repository renaming are separate decisions; migration completion additionally requires every legacy implementation to have exited and every declared legacy reference to be absent from its retained file (dual-track wiring alone is not completion)",
|
|
593
618
|
};
|
|
594
619
|
}
|
package/src/index.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
assessAdoptionBinding,
|
|
24
24
|
assessLegacyExitList,
|
|
25
|
+
assessLegacyReferences,
|
|
25
26
|
assessVerificationEvidence,
|
|
26
27
|
evaluateMigrationCompletion,
|
|
27
28
|
EXCEPTION_REQUIRED_FIELDS,
|
|
@@ -50,7 +51,7 @@ import {
|
|
|
50
51
|
listLicensingProfiles,
|
|
51
52
|
loadLicensingProfile,
|
|
52
53
|
} from "./licensing.mjs";
|
|
53
|
-
import { runProjection, loadProjectionManifest } from "./projection.mjs";
|
|
54
|
+
import { compileProjectionPlan, runProjection, loadProjectionManifest } from "./projection.mjs";
|
|
54
55
|
import { checkReportAction, renderReportAction } from "./report.mjs";
|
|
55
56
|
import { scaffoldTarget } from "./scaffold.mjs";
|
|
56
57
|
import {
|
|
@@ -119,7 +120,7 @@ export const COMMAND_SIDE_EFFECTS = Object.freeze({
|
|
|
119
120
|
projection: Object.freeze({
|
|
120
121
|
summary: "投影受管生成物。",
|
|
121
122
|
sideEffect:
|
|
122
|
-
"writes only manifest-authorized managed artifacts; the report sub-action (projection report) writes only the explicitly named --out/--binding paths contained in --root (Markdown goes to stdout when no --out is given); unauthorized, handwritten, escaping, or conflicting paths are refused
|
|
123
|
+
"writes or deletes only explicitly owned, manifest-authorized managed artifacts; the report sub-action (projection report) writes only the explicitly named --out/--binding paths contained in --root (Markdown goes to stdout when no --out is given); unauthorized, handwritten, escaping, or conflicting paths are refused before mutation",
|
|
123
124
|
exitCodes: "0 成功;2 拒绝/用法/机制错误",
|
|
124
125
|
}),
|
|
125
126
|
check: Object.freeze({
|
|
@@ -191,6 +192,7 @@ export {
|
|
|
191
192
|
planAdoption,
|
|
192
193
|
buildHandoffDraft,
|
|
193
194
|
HANDOFF_FIELDS,
|
|
195
|
+
compileProjectionPlan,
|
|
194
196
|
runProjection,
|
|
195
197
|
loadProjectionManifest,
|
|
196
198
|
renderReportAction,
|
|
@@ -259,5 +261,6 @@ export {
|
|
|
259
261
|
assessAdoptionBinding,
|
|
260
262
|
assessVerificationEvidence,
|
|
261
263
|
assessLegacyExitList,
|
|
264
|
+
assessLegacyReferences,
|
|
262
265
|
evaluateMigrationCompletion,
|
|
263
266
|
};
|
package/src/migration.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { lstat } from "node:fs/promises";
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { TextDecoder } from "node:util";
|
|
2
3
|
import { findSchemaByObject } from "skill-family-contracts";
|
|
3
4
|
import { HarnessError, readFileContained, resolveContained, validateContractDocument } from "skill-family-harness-node";
|
|
4
5
|
import { normalizeRelPath, readOptionalJson } from "./workspace.mjs";
|
|
5
6
|
|
|
7
|
+
const UTF8_STRICT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Migration closure facts for adopt-plan (FND-070, formalized by the
|
|
8
11
|
* migration manifest contract).
|
|
@@ -189,6 +192,112 @@ export async function assessLegacyExitList(root, legacyItems) {
|
|
|
189
192
|
return list;
|
|
190
193
|
}
|
|
191
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Assesses the legacy reference exit list: for each declared reference,
|
|
197
|
+
* checks whether the exact literal `text` still appears in the retained file.
|
|
198
|
+
*
|
|
199
|
+
* Returns one entry per declared reference with:
|
|
200
|
+
* status: "absent" — the literal text is not found (0 occurrences); file
|
|
201
|
+
* missing also counts as absent (the text is gone);
|
|
202
|
+
* status: "present" — the literal text appears one or more times;
|
|
203
|
+
* status: "invalid" — path rejected by containment, file contains
|
|
204
|
+
* invalid UTF-8 bytes, unreadable, or is a directory.
|
|
205
|
+
*
|
|
206
|
+
* UTF-8 decoding is strict: any invalid byte sequence fails closed with
|
|
207
|
+
* `invalidKind: "invalid-utf8"` instead of silently replacing bad bytes
|
|
208
|
+
* with U+FFFD. This prevents false "absent" verdicts when the file
|
|
209
|
+
* contains corrupted text that would otherwise mask the literal search.
|
|
210
|
+
*
|
|
211
|
+
* The `occurrenceCount` field is reported for "present" and "absent" entries
|
|
212
|
+
* without leaking file contents. Paths are resolved through harness
|
|
213
|
+
* containment (fail-closed on escape).
|
|
214
|
+
*/
|
|
215
|
+
export async function assessLegacyReferences(root, legacyRefs) {
|
|
216
|
+
const list = [];
|
|
217
|
+
if (!Array.isArray(legacyRefs)) return list;
|
|
218
|
+
for (const ref of legacyRefs) {
|
|
219
|
+
if (!ref || typeof ref.path !== "string" || ref.path.trim() === "") {
|
|
220
|
+
list.push({ path: null, text: null, replacedBy: null, status: "invalid", invalidKind: "malformed-entry", occurrenceCount: 0 });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const rel = normalizeRelPath(ref.path);
|
|
224
|
+
const text = typeof ref.text === "string" ? ref.text : null;
|
|
225
|
+
if (text === null || text === "") {
|
|
226
|
+
list.push({ path: rel, text: null, replacedBy: null, status: "invalid", invalidKind: "empty-text", occurrenceCount: 0 });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const replacedBy = typeof ref.replacedBy === "string" && ref.replacedBy.trim() !== "" ? ref.replacedBy : null;
|
|
230
|
+
if (replacedBy === null) {
|
|
231
|
+
list.push({ path: rel, text, replacedBy: null, status: "invalid", invalidKind: "empty-replaced-by", occurrenceCount: 0 });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
let resolved;
|
|
235
|
+
try {
|
|
236
|
+
resolved = await resolveContained(root, rel);
|
|
237
|
+
} catch (cause) {
|
|
238
|
+
list.push({
|
|
239
|
+
path: rel,
|
|
240
|
+
text,
|
|
241
|
+
replacedBy,
|
|
242
|
+
status: "invalid",
|
|
243
|
+
invalidKind: cause instanceof HarnessError && cause.details?.kind ? cause.details.kind : "containment-rejected",
|
|
244
|
+
occurrenceCount: 0,
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
let bytes;
|
|
249
|
+
try {
|
|
250
|
+
bytes = await readFile(resolved);
|
|
251
|
+
} catch (cause) {
|
|
252
|
+
if (cause && cause.code === "ENOENT") {
|
|
253
|
+
// File missing: the old text is absent by definition.
|
|
254
|
+
list.push({ path: rel, text, replacedBy, status: "absent", occurrenceCount: 0 });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Directory, unreadable, or other failure: fail-closed as invalid.
|
|
258
|
+
list.push({
|
|
259
|
+
path: rel,
|
|
260
|
+
text,
|
|
261
|
+
replacedBy,
|
|
262
|
+
status: "invalid",
|
|
263
|
+
invalidKind: cause && cause.code === "EISDIR" ? "is-directory" : "unreadable",
|
|
264
|
+
occurrenceCount: 0,
|
|
265
|
+
});
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// Strict UTF-8 decode: reject any invalid byte sequences instead of
|
|
269
|
+
// replacing them with U+FFFD. This prevents silent data corruption
|
|
270
|
+
// when assessing whether legacy text has truly exited a file.
|
|
271
|
+
let content;
|
|
272
|
+
try {
|
|
273
|
+
content = UTF8_STRICT_DECODER.decode(bytes);
|
|
274
|
+
} catch {
|
|
275
|
+
list.push({
|
|
276
|
+
path: rel,
|
|
277
|
+
text,
|
|
278
|
+
replacedBy,
|
|
279
|
+
status: "invalid",
|
|
280
|
+
invalidKind: "invalid-utf8",
|
|
281
|
+
occurrenceCount: 0,
|
|
282
|
+
});
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// Strict UTF-8 decode succeeded. Exact literal match: count
|
|
286
|
+
// non-overlapping occurrences.
|
|
287
|
+
let count = 0;
|
|
288
|
+
let fromIndex = 0;
|
|
289
|
+
while (true) {
|
|
290
|
+
const idx = content.indexOf(text, fromIndex);
|
|
291
|
+
if (idx === -1) break;
|
|
292
|
+
count++;
|
|
293
|
+
fromIndex = idx + text.length;
|
|
294
|
+
}
|
|
295
|
+
const status = count === 0 ? "absent" : "present";
|
|
296
|
+
list.push({ path: rel, text, replacedBy, status, occurrenceCount: count });
|
|
297
|
+
}
|
|
298
|
+
return list;
|
|
299
|
+
}
|
|
300
|
+
|
|
192
301
|
/**
|
|
193
302
|
* Migration state machine. States advance monotonically as facts
|
|
194
303
|
* are proven; completion is the final state only. No tool ever advances a
|
|
@@ -317,6 +426,9 @@ export async function assessVerificationEvidence(rootAbs, verification, plannedP
|
|
|
317
426
|
* - every legacy implementation removed or provably absent — an invalid
|
|
318
427
|
* (containment-rejected or malformed) entry fails closed like a present
|
|
319
428
|
* one; dual-track wiring alone is not completion;
|
|
429
|
+
* - every declared legacy reference absent from its retained file — an
|
|
430
|
+
* invalid (containment-rejected, unreadable, malformed) entry fails
|
|
431
|
+
* closed like a present one;
|
|
320
432
|
* - no incomplete/invalid/expired temporary exceptions;
|
|
321
433
|
* - no unresolved adoption conflicts;
|
|
322
434
|
* - all four verification evidence documents proven with matching identity.
|
|
@@ -329,6 +441,7 @@ export async function assessVerificationEvidence(rootAbs, verification, plannedP
|
|
|
329
441
|
export function evaluateMigrationCompletion({
|
|
330
442
|
manifestDeclared,
|
|
331
443
|
legacyExitList,
|
|
444
|
+
legacyReferenceExitList,
|
|
332
445
|
exceptionFindings,
|
|
333
446
|
conflicts,
|
|
334
447
|
binding,
|
|
@@ -385,6 +498,13 @@ export function evaluateMigrationCompletion({
|
|
|
385
498
|
adoptionBlockers.push(`legacy entry cannot be verified (fail-closed): ${item.path ?? "<malformed>"} (${item.invalidKind ?? "unknown"})`);
|
|
386
499
|
}
|
|
387
500
|
}
|
|
501
|
+
for (const ref of legacyReferenceExitList ?? []) {
|
|
502
|
+
if (ref.status === "present") {
|
|
503
|
+
adoptionBlockers.push(`legacy reference still present in ${ref.path}: ${ref.occurrenceCount} occurrence(s)`);
|
|
504
|
+
} else if (ref.status === "invalid") {
|
|
505
|
+
adoptionBlockers.push(`legacy reference cannot be verified (fail-closed): ${ref.path ?? "<malformed>"} (${ref.invalidKind ?? "unknown"})`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
388
508
|
for (const finding of exceptionFindings ?? []) {
|
|
389
509
|
if (finding.kind === "exception-incomplete") {
|
|
390
510
|
adoptionBlockers.push(`exception #${finding.index} missing required fields: ${finding.missing.join(", ")}`);
|