skill-family-engineering-kit 0.1.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/CODE_OF_CONDUCT.md +131 -0
- package/CONTRIBUTING.md +69 -0
- package/LICENSE +201 -0
- package/README.md +74 -0
- package/SECURITY.md +34 -0
- package/data/licensing/registry.json +211 -0
- package/data/licensing/schema.json +207 -0
- package/docs/.nojekyll +0 -0
- package/docs/404.html +613 -0
- package/docs/architecture/index.html +904 -0
- package/docs/assets/images/favicon.png +0 -0
- package/docs/assets/javascripts/bundle.d7400e89.min.js +16 -0
- package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- package/docs/assets/javascripts/lunr/tinyseg.js +206 -0
- package/docs/assets/javascripts/lunr/wordcut.js +6708 -0
- package/docs/assets/javascripts/workers/search.2c215733.min.js +42 -0
- package/docs/assets/stylesheets/main.ec1eaa64.min.css +1 -0
- package/docs/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
- package/docs/git-lifecycle/index.html +976 -0
- package/docs/help/index.html +907 -0
- package/docs/index.html +837 -0
- package/docs/integration/audit/baseline/audit-codes.json +62 -0
- package/docs/integration/audit/failure-evidence/index.html +872 -0
- package/docs/integration/audit/independence/index.html +841 -0
- package/docs/integration/audit/index.html +826 -0
- package/docs/integration/audit/mutation-taxonomy/index.html +1022 -0
- package/docs/integration/audit/schemas/audit-evidence.schema.json +182 -0
- package/docs/integration/audit/version-compatibility/index.html +845 -0
- package/docs/migration/index.html +1213 -0
- package/docs/quickstart/index.html +842 -0
- package/docs/search/search_index.json +1 -0
- package/docs/setup/index.html +941 -0
- package/docs/sitemap.xml +51 -0
- package/package.json +42 -0
- package/src/adopt-plan.mjs +594 -0
- package/src/check.mjs +415 -0
- package/src/cli.mjs +230 -0
- package/src/errors.mjs +144 -0
- package/src/gitprobe.mjs +230 -0
- package/src/identity-check.mjs +563 -0
- package/src/index.mjs +233 -0
- package/src/license-texts/Apache-2.0.txt +201 -0
- package/src/license-texts/MIT.txt +21 -0
- package/src/licensing.mjs +299 -0
- package/src/migration.mjs +427 -0
- package/src/projection.mjs +397 -0
- package/src/scaffold.mjs +178 -0
- package/src/skeleton.mjs +886 -0
- package/src/workspace.mjs +236 -0
package/docs/sitemap.xml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
3
|
+
<url>
|
|
4
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/</loc>
|
|
5
|
+
<lastmod>1970-01-01</lastmod>
|
|
6
|
+
</url>
|
|
7
|
+
<url>
|
|
8
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/architecture/</loc>
|
|
9
|
+
<lastmod>1970-01-01</lastmod>
|
|
10
|
+
</url>
|
|
11
|
+
<url>
|
|
12
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/git-lifecycle/</loc>
|
|
13
|
+
<lastmod>1970-01-01</lastmod>
|
|
14
|
+
</url>
|
|
15
|
+
<url>
|
|
16
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/help/</loc>
|
|
17
|
+
<lastmod>1970-01-01</lastmod>
|
|
18
|
+
</url>
|
|
19
|
+
<url>
|
|
20
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/quickstart/</loc>
|
|
21
|
+
<lastmod>1970-01-01</lastmod>
|
|
22
|
+
</url>
|
|
23
|
+
<url>
|
|
24
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/setup/</loc>
|
|
25
|
+
<lastmod>1970-01-01</lastmod>
|
|
26
|
+
</url>
|
|
27
|
+
<url>
|
|
28
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/integration/audit/</loc>
|
|
29
|
+
<lastmod>1970-01-01</lastmod>
|
|
30
|
+
</url>
|
|
31
|
+
<url>
|
|
32
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/integration/audit/failure-evidence/</loc>
|
|
33
|
+
<lastmod>1970-01-01</lastmod>
|
|
34
|
+
</url>
|
|
35
|
+
<url>
|
|
36
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/integration/audit/independence/</loc>
|
|
37
|
+
<lastmod>1970-01-01</lastmod>
|
|
38
|
+
</url>
|
|
39
|
+
<url>
|
|
40
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/integration/audit/mutation-taxonomy/</loc>
|
|
41
|
+
<lastmod>1970-01-01</lastmod>
|
|
42
|
+
</url>
|
|
43
|
+
<url>
|
|
44
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/integration/audit/version-compatibility/</loc>
|
|
45
|
+
<lastmod>1970-01-01</lastmod>
|
|
46
|
+
</url>
|
|
47
|
+
<url>
|
|
48
|
+
<loc>https://ifoohoo.github.io/skill-family-engineering-kit/migration/</loc>
|
|
49
|
+
<lastmod>1970-01-01</lastmod>
|
|
50
|
+
</url>
|
|
51
|
+
</urlset>
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skill-family-engineering-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Build-time scaffold, adoption planning, projection, and checks.",
|
|
6
|
+
"author": "广州市风荷科技有限公司",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/ifoohoo/skill-family-engineering-kit.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ifoohoo/skill-family-engineering-kit",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ifoohoo/skill-family-engineering-kit/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.22.2 <23"
|
|
19
|
+
},
|
|
20
|
+
"exports": "./src/index.mjs",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"skill-family-contracts": "0.1.0",
|
|
23
|
+
"skill-family-harness-node": "0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"skill-family-kit": "./src/cli.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"data",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"SECURITY.md",
|
|
33
|
+
"CONTRIBUTING.md",
|
|
34
|
+
"CODE_OF_CONDUCT.md",
|
|
35
|
+
"docs"
|
|
36
|
+
],
|
|
37
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\".",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"check": "node --test",
|
|
40
|
+
"test": "node --test"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { CONTRACTS_VERSION } from "skill-family-contracts";
|
|
3
|
+
import { digestBytes, readFileContained } from "skill-family-harness-node";
|
|
4
|
+
import { runChecks } from "./check.mjs";
|
|
5
|
+
import { probeGitFacts, probeGitState } from "./gitprobe.mjs";
|
|
6
|
+
import {
|
|
7
|
+
assessAdoptionBinding,
|
|
8
|
+
assessLegacyExitList,
|
|
9
|
+
assessVerificationEvidence,
|
|
10
|
+
evaluateMigrationCompletion,
|
|
11
|
+
findNestedRepositories,
|
|
12
|
+
loadMigrationManifestState,
|
|
13
|
+
MIGRATION_MANIFEST_PATH,
|
|
14
|
+
validateException,
|
|
15
|
+
VERIFICATION_EVIDENCE_KINDS,
|
|
16
|
+
} from "./migration.mjs";
|
|
17
|
+
import {
|
|
18
|
+
describeSkeletonFiles,
|
|
19
|
+
KIT_TOOL_NAME,
|
|
20
|
+
KIT_VERSION,
|
|
21
|
+
normalizeSkeletonInputs,
|
|
22
|
+
} from "./skeleton.mjs";
|
|
23
|
+
import {
|
|
24
|
+
listTargetEntries,
|
|
25
|
+
loadTargetFacts,
|
|
26
|
+
matchAnyGlob,
|
|
27
|
+
normalizeRelPath,
|
|
28
|
+
resolveTargetRoot,
|
|
29
|
+
} from "./workspace.mjs";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* adopt-plan — strictly read-only adoption planning.
|
|
33
|
+
*
|
|
34
|
+
* Computes the exact write set, conflicts, risks, and verification plan for
|
|
35
|
+
* adopting the Skill Family skeleton into an existing target. The function
|
|
36
|
+
* contains no write call of any kind: it reads the target, computes the
|
|
37
|
+
* plan in memory, and returns it. Callers print it; nothing is written,
|
|
38
|
+
* not even a temporary file (so "zero byte change on dirty repositories"
|
|
39
|
+
* is trivially verifiable with before/after hash walks).
|
|
40
|
+
*
|
|
41
|
+
* Planned content comes from describeSkeletonFiles — the same pure function
|
|
42
|
+
* scaffold consumes — so the plan and the later action are byte-identical.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
async function readExistingBytes(rootAbs, relPath) {
|
|
46
|
+
// Contained read: even though planned paths are a frozen set, every fs
|
|
47
|
+
// access goes through the harness containment layer.
|
|
48
|
+
try {
|
|
49
|
+
return await readFileContained(rootAbs, relPath);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The ten-field per-repository adoption hand-off draft.
|
|
57
|
+
*
|
|
58
|
+
* adopt-plan must carry enough structured data to draft the planner's
|
|
59
|
+
* ten-field hand-off. Fields that cannot be derived mechanically from
|
|
60
|
+
* read-only evidence are marked INCOMPLETE and block the draft — the kit
|
|
61
|
+
* never guesses. The draft binds the target file-set digest and the
|
|
62
|
+
* Foundation plan digest; rendering is a separate read-only planner task
|
|
63
|
+
* that only ever writes the planner-side draft, never the consumer repo.
|
|
64
|
+
*/
|
|
65
|
+
export const HANDOFF_FIELDS = Object.freeze([
|
|
66
|
+
"project-identity",
|
|
67
|
+
"repository-state",
|
|
68
|
+
"foundation-binding",
|
|
69
|
+
"write-set-policy",
|
|
70
|
+
"legacy-exits",
|
|
71
|
+
"business-logic-to-keep",
|
|
72
|
+
"plan-summary",
|
|
73
|
+
"verification-entries",
|
|
74
|
+
"legacy-removal-recovery",
|
|
75
|
+
"authorization-status",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
export function buildHandoffDraft({
|
|
79
|
+
projectId,
|
|
80
|
+
rootAbs,
|
|
81
|
+
agentsFiles,
|
|
82
|
+
git,
|
|
83
|
+
gitFacts,
|
|
84
|
+
unregisteredPaths,
|
|
85
|
+
nestedRepositories,
|
|
86
|
+
inputs,
|
|
87
|
+
migrationManifest,
|
|
88
|
+
writeSet,
|
|
89
|
+
conflicts,
|
|
90
|
+
risks,
|
|
91
|
+
legacyExitList,
|
|
92
|
+
verificationFacts,
|
|
93
|
+
migrationState,
|
|
94
|
+
entries,
|
|
95
|
+
}) {
|
|
96
|
+
const fields = [];
|
|
97
|
+
const push = (id, name, status, value) => fields.push({ id, name, status, value });
|
|
98
|
+
|
|
99
|
+
// 1. Project identity, absolute path, project-level AGENTS files.
|
|
100
|
+
push(1, HANDOFF_FIELDS[0], "derived", {
|
|
101
|
+
projectId,
|
|
102
|
+
root: rootAbs,
|
|
103
|
+
agentsFiles: agentsFiles ?? [],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 2. dirty / untracked / nested-repository state — facts or not-proven,
|
|
107
|
+
// never fabricated.
|
|
108
|
+
push(2, HANDOFF_FIELDS[1], "derived", {
|
|
109
|
+
repository: git.repository,
|
|
110
|
+
headCommit: git.headCommit,
|
|
111
|
+
cleanState: git.cleanState,
|
|
112
|
+
dirty: git.cleanState === false,
|
|
113
|
+
unregisteredContent: git.repository ? null : unregisteredPaths ?? [],
|
|
114
|
+
nestedRepositories: nestedRepositories ?? [],
|
|
115
|
+
trackedButIgnored: gitFacts?.status === "proven" ? gitFacts.trackedButIgnored : null,
|
|
116
|
+
gitFactsStatus: gitFacts?.status ?? "not-proven",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 3. Profile plus exact Foundation versions and digests: only the
|
|
120
|
+
// manifest-declared binding counts; an undeclared binding is INCOMPLETE.
|
|
121
|
+
const declaredPackages = Array.isArray(migrationManifest?.foundationPackages)
|
|
122
|
+
? migrationManifest.foundationPackages
|
|
123
|
+
: null;
|
|
124
|
+
push(3, HANDOFF_FIELDS[2], declaredPackages && declaredPackages.length > 0 ? "derived" : "INCOMPLETE", {
|
|
125
|
+
plannedProfile: inputs.profileId,
|
|
126
|
+
contractsVersion: CONTRACTS_VERSION,
|
|
127
|
+
foundationPackages: declaredPackages,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// 4. Allowed and forbidden write sets.
|
|
131
|
+
const creates = writeSet.filter((item) => item.action !== "unchanged").map((item) => item.path).sort();
|
|
132
|
+
push(4, HANDOFF_FIELDS[3], "derived", {
|
|
133
|
+
allowed: creates,
|
|
134
|
+
unchanged: writeSet.length - creates.length,
|
|
135
|
+
forbidden:
|
|
136
|
+
"nothing outside the listed write set is ever written; the kit never renames, deletes, rewrites handwritten files, or touches remotes; legacy removal is a human-performed exit step",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 5. The validator/builder/docs-pipeline/git-preflight slated for exit.
|
|
140
|
+
push(5, HANDOFF_FIELDS[4], "derived", {
|
|
141
|
+
exits: legacyExitList.map((item) => ({ path: item.path, replacedBy: item.replacedBy, status: item.status })),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 6. Business logic that must be kept: never derivable from repo facts.
|
|
145
|
+
push(6, HANDOFF_FIELDS[5], "INCOMPLETE", {
|
|
146
|
+
reason: "business-logic boundaries cannot be derived mechanically; a human owner must enumerate them before any hand-off starts",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 7. adopt-plan summary.
|
|
150
|
+
push(7, HANDOFF_FIELDS[6], "derived", {
|
|
151
|
+
writeSetCreates: creates.length,
|
|
152
|
+
writeSetUnchanged: writeSet.length - creates.length,
|
|
153
|
+
conflicts: conflicts.length,
|
|
154
|
+
risks: risks.length,
|
|
155
|
+
migrationState,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 8. Unit / integration / consumer / independent-audit entries: derived
|
|
159
|
+
// only when every evidence kind is proven. assessVerificationEvidence
|
|
160
|
+
// always returns one status-bearing fact per kind (undeclared/missing/
|
|
161
|
+
// invalid-path/unreadable/identity-mismatch/proven), so presence alone
|
|
162
|
+
// proves nothing — fail-closed, never guessed (FC-12).
|
|
163
|
+
const evidenceByKind = new Map((verificationFacts ?? []).map((fact) => [fact.kind, fact]));
|
|
164
|
+
const missingEvidence = VERIFICATION_EVIDENCE_KINDS.filter((kind) => evidenceByKind.get(kind)?.status !== "proven");
|
|
165
|
+
push(8, HANDOFF_FIELDS[7], missingEvidence.length === 0 ? "derived" : "INCOMPLETE", {
|
|
166
|
+
entries: (verificationFacts ?? []).map((fact) => ({ kind: fact.kind, path: fact.path ?? null, status: fact.status })),
|
|
167
|
+
missing: missingEvidence,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// 9. How removed legacy implementations can be recovered: never
|
|
171
|
+
// derivable mechanically (depends on the target's own history/backups).
|
|
172
|
+
push(9, HANDOFF_FIELDS[8], "INCOMPLETE", {
|
|
173
|
+
reason: "recovery paths for removed legacy implementations depend on the target's own history and backups; the kit never guesses them",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// 10. commit/push/tag/publish authorization: a fixed policy fact —
|
|
177
|
+
// adoption planning authorizes none of them.
|
|
178
|
+
push(10, HANDOFF_FIELDS[9], "derived", {
|
|
179
|
+
commit: false,
|
|
180
|
+
push: false,
|
|
181
|
+
tag: false,
|
|
182
|
+
publish: false,
|
|
183
|
+
note: "adoption authorizes no git write, tag, or release; each requires explicit per-task user authorization",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const incompleteFields = fields.filter((field) => field.status === "INCOMPLETE").map((field) => field.name);
|
|
187
|
+
|
|
188
|
+
// Binding digests: the target file-set summary and the Foundation plan.
|
|
189
|
+
const targetSetDigest = digestBytes(
|
|
190
|
+
Buffer.from(entries.map((entry) => `${entry.path}:${entry.kind}`).sort().join("\n"), "utf8"),
|
|
191
|
+
);
|
|
192
|
+
const foundationPlanDigest = digestBytes(
|
|
193
|
+
Buffer.from(writeSet.map((item) => `${item.path}:${item.action}:${item.sha256}`).sort().join("\n"), "utf8"),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
kind: "skill-family.handoff-draft",
|
|
198
|
+
schemaVersion: 1,
|
|
199
|
+
fields,
|
|
200
|
+
incompleteFields,
|
|
201
|
+
ready: incompleteFields.length === 0,
|
|
202
|
+
binding: { targetSetDigest, foundationPlanDigest },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Plans the adoption of the skeleton into root.
|
|
208
|
+
* Options: { root, projectId, projectName, profileId, licensingProfile, licensingProfileData, profilesRoot, allowGitSpawn, now }.
|
|
209
|
+
* Returns the plan document; throws KitError only for unusable inputs
|
|
210
|
+
* (an unreadable target). Never writes anywhere.
|
|
211
|
+
*/
|
|
212
|
+
export async function planAdoption({
|
|
213
|
+
root,
|
|
214
|
+
projectId,
|
|
215
|
+
projectName,
|
|
216
|
+
profileId,
|
|
217
|
+
licensingProfile,
|
|
218
|
+
licensingVariant,
|
|
219
|
+
licensingProfileData,
|
|
220
|
+
profilesRoot,
|
|
221
|
+
identityProjections,
|
|
222
|
+
allowGitSpawn = true,
|
|
223
|
+
now = Date.now(),
|
|
224
|
+
} = {}) {
|
|
225
|
+
const rootAbs = await resolveTargetRoot(root ?? ".");
|
|
226
|
+
const entries = await listTargetEntries(rootAbs);
|
|
227
|
+
const facts = await loadTargetFacts(rootAbs);
|
|
228
|
+
const git = await probeGitState(rootAbs, { allowSpawn: allowGitSpawn });
|
|
229
|
+
|
|
230
|
+
const inputs = normalizeSkeletonInputs({
|
|
231
|
+
projectId,
|
|
232
|
+
projectName,
|
|
233
|
+
profileId,
|
|
234
|
+
licensingProfile,
|
|
235
|
+
licensingVariant,
|
|
236
|
+
rootBasename: path.basename(rootAbs),
|
|
237
|
+
});
|
|
238
|
+
const skeleton = await describeSkeletonFiles({
|
|
239
|
+
...inputs,
|
|
240
|
+
licensingProfileData,
|
|
241
|
+
profilesRoot,
|
|
242
|
+
identityProjections,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const entryByPath = new Map(entries.map((entry) => [entry.path, entry]));
|
|
246
|
+
|
|
247
|
+
const writeSet = [];
|
|
248
|
+
const conflicts = [];
|
|
249
|
+
for (const file of skeleton.files) {
|
|
250
|
+
const rel = normalizeRelPath(file.path);
|
|
251
|
+
const plannedSha256 = digestBytes(Buffer.from(file.content, "utf8"));
|
|
252
|
+
const existing = entryByPath.get(rel);
|
|
253
|
+
|
|
254
|
+
if (!existing) {
|
|
255
|
+
writeSet.push({
|
|
256
|
+
path: rel,
|
|
257
|
+
fileClass: file.fileClass,
|
|
258
|
+
action: "create",
|
|
259
|
+
sha256: plannedSha256,
|
|
260
|
+
});
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (existing.kind === "symlink") {
|
|
265
|
+
conflicts.push({
|
|
266
|
+
path: rel,
|
|
267
|
+
kind: "symlink-on-planned-path",
|
|
268
|
+
code: "SFC2004",
|
|
269
|
+
detail:
|
|
270
|
+
"a symbolic link occupies the planned path; the kit never writes through links — resolve it manually before adoption",
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (existing.kind === "directory") {
|
|
275
|
+
conflicts.push({
|
|
276
|
+
path: rel,
|
|
277
|
+
kind: "type-conflict",
|
|
278
|
+
code: "SFC2004",
|
|
279
|
+
detail: "a directory occupies the planned file path",
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const bytes = await readExistingBytes(rootAbs, rel);
|
|
285
|
+
const existingSha256 = bytes === null ? null : digestBytes(bytes);
|
|
286
|
+
if (existingSha256 === plannedSha256) {
|
|
287
|
+
writeSet.push({
|
|
288
|
+
path: rel,
|
|
289
|
+
fileClass: file.fileClass,
|
|
290
|
+
action: "unchanged",
|
|
291
|
+
sha256: plannedSha256,
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// The migration manifest is target-held data once seeded: adoption
|
|
297
|
+
// never overwrites an existing manifest, and its contract validity is
|
|
298
|
+
// assessed exclusively by the migration section below (parse/schema,
|
|
299
|
+
// legacy exit list, temporary exceptions).
|
|
300
|
+
if (rel === MIGRATION_MANIFEST_PATH) continue;
|
|
301
|
+
|
|
302
|
+
// Existing, different content: adoption never overwrites. Handwritten
|
|
303
|
+
// material is a hard conflict; a drifted managed declaration needs an
|
|
304
|
+
// explicit human decision before any re-projection.
|
|
305
|
+
const handwritten =
|
|
306
|
+
matchAnyGlob(facts.handwrittenPatterns, rel) || !facts.managedSet.has(rel);
|
|
307
|
+
conflicts.push({
|
|
308
|
+
path: rel,
|
|
309
|
+
kind: handwritten ? "handwritten-conflict" : "managed-drift",
|
|
310
|
+
code: "SFC2004",
|
|
311
|
+
detail: handwritten
|
|
312
|
+
? "existing content is handwritten (or not declared managed); the kit will never overwrite it"
|
|
313
|
+
: "existing content differs from the planned managed bytes; reconcile before adoption",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const risks = [];
|
|
318
|
+
// Git-independent unregistered content (also feeds the ten-field hand-off
|
|
319
|
+
// draft field 2): neither planned nor declared managed.
|
|
320
|
+
const plannedPaths = new Set(
|
|
321
|
+
skeleton.files
|
|
322
|
+
.filter((file) => normalizeRelPath(file.path) !== MIGRATION_MANIFEST_PATH)
|
|
323
|
+
.map((file) => normalizeRelPath(file.path)),
|
|
324
|
+
);
|
|
325
|
+
const unregisteredPaths = entries
|
|
326
|
+
.filter(
|
|
327
|
+
(entry) =>
|
|
328
|
+
(entry.kind === "file" || entry.kind === "symlink") &&
|
|
329
|
+
!plannedPaths.has(entry.path) &&
|
|
330
|
+
!facts.managedSet.has(entry.path),
|
|
331
|
+
)
|
|
332
|
+
.map((entry) => entry.path)
|
|
333
|
+
.sort();
|
|
334
|
+
if (git.repository && git.headCommit === false) {
|
|
335
|
+
risks.push({
|
|
336
|
+
kind: "git-no-commits",
|
|
337
|
+
detail: "the target is a git repository without any commit; nothing is protected by history yet",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
if (git.repository && git.cleanState === false) {
|
|
341
|
+
risks.push({
|
|
342
|
+
kind: "git-dirty",
|
|
343
|
+
detail: "the target has uncommitted changes; settle them before adoption (the plan itself changes nothing)",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (!git.repository) {
|
|
347
|
+
// Git-independent dirty equivalence (FND-001 modelling): content that
|
|
348
|
+
// is neither planned nor declared managed counts as uncommitted state.
|
|
349
|
+
// The migration manifest is target-held data, not planned content: it
|
|
350
|
+
// is judged by the migration contract, so it stays in this equivalence.
|
|
351
|
+
if (unregisteredPaths.length > 0) {
|
|
352
|
+
risks.push({
|
|
353
|
+
kind: "unregistered-content",
|
|
354
|
+
detail: `${unregisteredPaths.length} entr${unregisteredPaths.length === 1 ? "y" : "ies"} are neither planned nor declared managed; adoption leaves them untouched`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (conflicts.some((conflict) => conflict.kind === "symlink-on-planned-path")) {
|
|
359
|
+
risks.push({
|
|
360
|
+
kind: "symlink-on-planned-path",
|
|
361
|
+
detail: "at least one planned path is a symbolic link; writes through links are refused",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Migration-closure facts (FND-070): nested repositories and
|
|
366
|
+
// tracked-but-ignored hazards are reported, never acted upon.
|
|
367
|
+
const nestedRepositories = findNestedRepositories(entries);
|
|
368
|
+
if (nestedRepositories.length > 0) {
|
|
369
|
+
risks.push({
|
|
370
|
+
kind: "nested-repository",
|
|
371
|
+
detail: `embedded repositories present: ${nestedRepositories.join(", ")}; adoption never operates inside a nested repository`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
// Read-only Git evidence probe: tracked/ignore facts come from real git semantics (read-only
|
|
375
|
+
// index + git's own ignore machinery), never from lexical guesses and
|
|
376
|
+
// never from managed declarations impersonating tracked state. When git
|
|
377
|
+
// is unavailable the facts stay not-proven — unknown, not fabricated.
|
|
378
|
+
const gitFactCandidates = [
|
|
379
|
+
...entries.filter((entry) => entry.kind === "file" || entry.kind === "symlink").map((entry) => entry.path),
|
|
380
|
+
...facts.managedSet,
|
|
381
|
+
];
|
|
382
|
+
const gitFacts = await probeGitFacts(rootAbs, gitFactCandidates, { allowSpawn: allowGitSpawn });
|
|
383
|
+
if (gitFacts.status === "proven" && gitFacts.trackedButIgnored.length > 0) {
|
|
384
|
+
risks.push({
|
|
385
|
+
kind: "tracked-but-ignored",
|
|
386
|
+
detail: `tracked in the git index yet matched by ignore rules: ${gitFacts.trackedButIgnored.join(", ")}; reconcile tracking and ignore state before adoption`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Legacy exit list and temporary exceptions come from the target's own
|
|
391
|
+
// migration manifest; the kit only assesses, it never deletes or renews.
|
|
392
|
+
// The manifest is a formal Contract — only a contract-valid
|
|
393
|
+
// manifest counts as declared; parse/schema failures are hard conflicts.
|
|
394
|
+
const manifestState = await loadMigrationManifestState(rootAbs);
|
|
395
|
+
const manifestDeclared = manifestState.status === "valid";
|
|
396
|
+
if (manifestState.status === "parse-failed") {
|
|
397
|
+
conflicts.push({
|
|
398
|
+
path: MIGRATION_MANIFEST_PATH,
|
|
399
|
+
kind: "migration-manifest-parse-failed",
|
|
400
|
+
code: "SFC2004",
|
|
401
|
+
detail: "the migration manifest is not parseable JSON; fix it before adoption can proceed",
|
|
402
|
+
});
|
|
403
|
+
} else if (manifestState.status === "schema-invalid") {
|
|
404
|
+
conflicts.push({
|
|
405
|
+
path: MIGRATION_MANIFEST_PATH,
|
|
406
|
+
kind: "migration-manifest-invalid",
|
|
407
|
+
code: "SFC1001",
|
|
408
|
+
detail: `the migration manifest violates the migration-manifest contract: ${manifestState.problems.join("; ")}`,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const migrationManifest = manifestDeclared ? manifestState.manifest : null;
|
|
412
|
+
const legacyExitList = await assessLegacyExitList(rootAbs, migrationManifest?.legacyInfra ?? []);
|
|
413
|
+
for (const item of legacyExitList) {
|
|
414
|
+
if (item.status === "invalid") {
|
|
415
|
+
conflicts.push({
|
|
416
|
+
path: MIGRATION_MANIFEST_PATH,
|
|
417
|
+
kind: "legacy-path-containment",
|
|
418
|
+
code: "SFC2004",
|
|
419
|
+
detail: `legacy path rejected by harness containment, fail-closed: ${item.path ?? "<malformed>"} (${item.invalidKind ?? "unknown"}); user paths are only ever read inside the target root`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const exceptionFindings = [];
|
|
424
|
+
const declaredExceptions = Array.isArray(migrationManifest?.exceptions) ? migrationManifest.exceptions : [];
|
|
425
|
+
for (const [index, exception] of declaredExceptions.entries()) {
|
|
426
|
+
for (const finding of validateException(exception, index, now)) {
|
|
427
|
+
exceptionFindings.push(finding);
|
|
428
|
+
if (finding.kind === "exception-incomplete") {
|
|
429
|
+
conflicts.push({
|
|
430
|
+
path: MIGRATION_MANIFEST_PATH,
|
|
431
|
+
kind: "exception-incomplete",
|
|
432
|
+
code: "SFC2004",
|
|
433
|
+
detail: `temporary exception #${index} is missing required fields: ${finding.missing.join(", ")} (owner, reason, deadline and migrationTarget are all mandatory)`,
|
|
434
|
+
});
|
|
435
|
+
} else if (finding.kind === "exception-invalid-deadline") {
|
|
436
|
+
conflicts.push({
|
|
437
|
+
path: MIGRATION_MANIFEST_PATH,
|
|
438
|
+
kind: "exception-invalid-deadline",
|
|
439
|
+
code: "SFC2004",
|
|
440
|
+
detail: `temporary exception #${index} carries an unparseable deadline: ${finding.deadline}`,
|
|
441
|
+
});
|
|
442
|
+
} else {
|
|
443
|
+
risks.push({
|
|
444
|
+
kind: "exception-expired",
|
|
445
|
+
detail: `temporary exception #${index} expired at ${finding.deadline}; expired exceptions are never auto-renewed`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Migration completion facts — all derived read-only from evidence on disk:
|
|
451
|
+
// the adoption binding declared by the manifest, the managed-byte proof,
|
|
452
|
+
// the remaining write actions, the Foundation check gate, and the four
|
|
453
|
+
// verification evidence documents.
|
|
454
|
+
const binding = assessAdoptionBinding(migrationManifest, inputs.profileId);
|
|
455
|
+
const adoptionProof = {
|
|
456
|
+
checked: skeleton.files.filter((file) => file.fileClass === "managed").length,
|
|
457
|
+
matched: writeSet.filter((item) => item.fileClass === "managed" && item.action === "unchanged").length,
|
|
458
|
+
missing: writeSet
|
|
459
|
+
.filter((item) => item.fileClass === "managed" && item.action !== "unchanged")
|
|
460
|
+
.map((item) => item.path)
|
|
461
|
+
.sort(),
|
|
462
|
+
mismatched: conflicts.filter((conflict) => conflict.kind === "managed-drift").map((conflict) => conflict.path).sort(),
|
|
463
|
+
};
|
|
464
|
+
const pendingWrites = writeSet.filter((item) => item.action !== "unchanged").length;
|
|
465
|
+
let checkGreen = false;
|
|
466
|
+
let checkNote = null;
|
|
467
|
+
try {
|
|
468
|
+
// check is diagnosis-only (read-only git probes included), so running it
|
|
469
|
+
// inside the read-only plan keeps the zero-byte-change promise.
|
|
470
|
+
const checkReport = await runChecks({ root: rootAbs, allowGitSpawn, profilesRoot });
|
|
471
|
+
checkGreen = checkReport.ok === true;
|
|
472
|
+
} catch (cause) {
|
|
473
|
+
checkNote = cause?.message ?? "check gate could not run";
|
|
474
|
+
}
|
|
475
|
+
const verificationFacts = await assessVerificationEvidence(
|
|
476
|
+
rootAbs,
|
|
477
|
+
migrationManifest?.verification,
|
|
478
|
+
inputs.projectId,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const completion = evaluateMigrationCompletion({
|
|
482
|
+
manifestDeclared,
|
|
483
|
+
legacyExitList,
|
|
484
|
+
exceptionFindings,
|
|
485
|
+
conflicts,
|
|
486
|
+
binding,
|
|
487
|
+
adoptionProof,
|
|
488
|
+
pendingWrites,
|
|
489
|
+
checkGreen,
|
|
490
|
+
verificationFacts,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const existingManagedDeclarations = [...facts.managedSet].sort();
|
|
494
|
+
|
|
495
|
+
// The ten-field hand-off draft. Fields that cannot be derived
|
|
496
|
+
// mechanically stay INCOMPLETE and block readiness — never guessed.
|
|
497
|
+
const agentsFiles = entries
|
|
498
|
+
.filter((entry) => entry.kind === "file" && (entry.path === "AGENTS.md" || entry.path === "CLAUDE.md"))
|
|
499
|
+
.map((entry) => entry.path)
|
|
500
|
+
.sort();
|
|
501
|
+
const handoffDraft = buildHandoffDraft({
|
|
502
|
+
projectId: inputs.projectId,
|
|
503
|
+
rootAbs,
|
|
504
|
+
agentsFiles,
|
|
505
|
+
git,
|
|
506
|
+
gitFacts,
|
|
507
|
+
unregisteredPaths,
|
|
508
|
+
nestedRepositories,
|
|
509
|
+
inputs,
|
|
510
|
+
migrationManifest,
|
|
511
|
+
writeSet,
|
|
512
|
+
conflicts,
|
|
513
|
+
risks,
|
|
514
|
+
legacyExitList,
|
|
515
|
+
verificationFacts,
|
|
516
|
+
migrationState: completion.state,
|
|
517
|
+
entries,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
kind: "skill-family.adoption-plan",
|
|
522
|
+
schemaVersion: 1,
|
|
523
|
+
generatedBy: { tool: KIT_TOOL_NAME, version: KIT_VERSION },
|
|
524
|
+
target: {
|
|
525
|
+
root: ".",
|
|
526
|
+
entryCount: entries.length,
|
|
527
|
+
hasOwnFileRegistry: facts.hasOwnRegistry,
|
|
528
|
+
existingManagedDeclarations,
|
|
529
|
+
},
|
|
530
|
+
project: {
|
|
531
|
+
...inputs,
|
|
532
|
+
licensingProfile: skeleton.licensing.profile,
|
|
533
|
+
licensingVariant: skeleton.licensing.variant,
|
|
534
|
+
contractsVersion: CONTRACTS_VERSION,
|
|
535
|
+
},
|
|
536
|
+
git: {
|
|
537
|
+
repository: git.repository,
|
|
538
|
+
headCommit: git.headCommit,
|
|
539
|
+
cleanState: git.cleanState,
|
|
540
|
+
probe: git.probe,
|
|
541
|
+
},
|
|
542
|
+
gitFacts:
|
|
543
|
+
gitFacts.status === "proven"
|
|
544
|
+
? {
|
|
545
|
+
status: "proven",
|
|
546
|
+
trackedButIgnored: gitFacts.trackedButIgnored,
|
|
547
|
+
ignoredNotTracked: gitFacts.ignoredNotTracked,
|
|
548
|
+
}
|
|
549
|
+
: { status: "not-proven", reason: gitFacts.reason },
|
|
550
|
+
writeSet,
|
|
551
|
+
conflicts,
|
|
552
|
+
risks,
|
|
553
|
+
migration: {
|
|
554
|
+
manifestDeclared,
|
|
555
|
+
manifestStatus: manifestState.status,
|
|
556
|
+
manifestPath: MIGRATION_MANIFEST_PATH,
|
|
557
|
+
legacyExitList,
|
|
558
|
+
exceptions: { declared: declaredExceptions.length, findings: exceptionFindings },
|
|
559
|
+
binding,
|
|
560
|
+
adoptionProof,
|
|
561
|
+
pendingWrites,
|
|
562
|
+
check: { green: checkGreen, note: checkNote },
|
|
563
|
+
verification: verificationFacts,
|
|
564
|
+
state: completion.state,
|
|
565
|
+
completion,
|
|
566
|
+
},
|
|
567
|
+
handoffDraft,
|
|
568
|
+
traceability: {
|
|
569
|
+
contractsVersion: CONTRACTS_VERSION,
|
|
570
|
+
skeletonSource: "describeSkeletonFiles",
|
|
571
|
+
licensingProfile: skeleton.licensing.profile,
|
|
572
|
+
licensingVariant: skeleton.licensing.variant,
|
|
573
|
+
},
|
|
574
|
+
verificationPlan: [
|
|
575
|
+
{
|
|
576
|
+
step: 1,
|
|
577
|
+
command: "sf-kit check --root <target>",
|
|
578
|
+
purpose: "diagnose contracts, drift, closure, versions, doc facts, and git pre-state",
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
step: 2,
|
|
582
|
+
command: "sf-kit projection --root <target>",
|
|
583
|
+
purpose: "materialize only manifest-authorized managed artifacts (never handwritten files)",
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
step: 3,
|
|
587
|
+
command: "sf-kit check --root <target>",
|
|
588
|
+
purpose: "re-diagnose after projection; the kit never auto-fixes any finding",
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
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)",
|
|
593
|
+
};
|
|
594
|
+
}
|