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
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { findSchemaByObject } from "skill-family-contracts";
|
|
3
|
+
import { HarnessError, readFileContained, resolveContained, validateContractDocument } from "skill-family-harness-node";
|
|
4
|
+
import { normalizeRelPath, readOptionalJson } from "./workspace.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Migration closure facts for adopt-plan (FND-070, formalized by the
|
|
8
|
+
* migration manifest contract).
|
|
9
|
+
*
|
|
10
|
+
* Everything here is strictly read-only and pure: adoption never mutates
|
|
11
|
+
* the target, and completion is a judgement computed from filesystem facts
|
|
12
|
+
* plus the target's own migration manifest. The kit never deletes legacy
|
|
13
|
+
* implementations itself — it only reports whether they have exited.
|
|
14
|
+
*
|
|
15
|
+
* The target declares its migration state in `skill-family.migration.json`,
|
|
16
|
+
* which is a formal Contract (migration-manifest, Contracts 1.1.1):
|
|
17
|
+
*
|
|
18
|
+
* {
|
|
19
|
+
* "schemaVersion": 1,
|
|
20
|
+
* "kind": "skill-family.migration-manifest",
|
|
21
|
+
* "legacyInfra": [ { "path": "scripts/old-validator.mjs", "replacedBy": "sf-kit check" } ],
|
|
22
|
+
* "exceptions": [ { "owner": "...", "reason": "...", "deadline": "2026-12-31", "migrationTarget": "..." } ]
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* The manifest loader distinguishes missing / parse-failed / schema-invalid /
|
|
26
|
+
* valid; only a valid manifest counts as declared. Temporary exceptions are
|
|
27
|
+
* only accepted when all four fields are present and non-blank; a missing
|
|
28
|
+
* field fails the plan (conflict), an expired deadline is never auto-renewed
|
|
29
|
+
* (risk + completion blocker). Every user-provided legacy path is assessed
|
|
30
|
+
* through harness containment: escapes fail closed and never reveal whether
|
|
31
|
+
* an outside path exists.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Where a target declares its legacy exit list and temporary exceptions. */
|
|
35
|
+
export const MIGRATION_MANIFEST_PATH = "skill-family.migration.json";
|
|
36
|
+
|
|
37
|
+
export const MIGRATION_MANIFEST_KIND = "skill-family.migration-manifest";
|
|
38
|
+
|
|
39
|
+
/** Registered schema $id of the migration-manifest contract. */
|
|
40
|
+
export const MIGRATION_MANIFEST_SCHEMA_ID = findSchemaByObject("migration-manifest").$id;
|
|
41
|
+
|
|
42
|
+
/** Loader outcome vocabulary: exactly one state per manifest file. */
|
|
43
|
+
export const MIGRATION_MANIFEST_STATES = Object.freeze([
|
|
44
|
+
"missing",
|
|
45
|
+
"parse-failed",
|
|
46
|
+
"schema-invalid",
|
|
47
|
+
"valid",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/** Every temporary exception must carry all four fields; one missing fails. */
|
|
51
|
+
export const EXCEPTION_REQUIRED_FIELDS = Object.freeze([
|
|
52
|
+
"owner",
|
|
53
|
+
"reason",
|
|
54
|
+
"deadline",
|
|
55
|
+
"migrationTarget",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Loads and validates the target's migration manifest against the formal
|
|
60
|
+
* migration-manifest contract (read-only). Returns:
|
|
61
|
+
* { status: "missing" } — no manifest file;
|
|
62
|
+
* { status: "parse-failed" } — not parseable JSON;
|
|
63
|
+
* { status: "schema-invalid", problems } — contract violation;
|
|
64
|
+
* { status: "valid", manifest } — contract-conforming.
|
|
65
|
+
* Only "valid" counts as a declared manifest for planning and completion.
|
|
66
|
+
*/
|
|
67
|
+
export async function loadMigrationManifestState(root) {
|
|
68
|
+
const result = await readOptionalJson(root, MIGRATION_MANIFEST_PATH);
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
return { status: result.reason === "missing" ? "missing" : "parse-failed" };
|
|
71
|
+
}
|
|
72
|
+
const outcome = validateContractDocument(result.value, { schemaId: MIGRATION_MANIFEST_SCHEMA_ID });
|
|
73
|
+
if (!outcome.valid) {
|
|
74
|
+
return {
|
|
75
|
+
status: "schema-invalid",
|
|
76
|
+
problems: outcome.errors.map((error) => error.message ?? "schema violation"),
|
|
77
|
+
errorCode: outcome.errorCode,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { status: "valid", manifest: result.value };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Loads the target's migration manifest (read-only, tolerant). Kept for
|
|
85
|
+
* callers that only need the parsed object: returns it when the manifest is
|
|
86
|
+
* contract-valid, otherwise null. Prefer loadMigrationManifestState, which
|
|
87
|
+
* distinguishes every failure state.
|
|
88
|
+
*/
|
|
89
|
+
export async function loadMigrationManifest(root) {
|
|
90
|
+
const state = await loadMigrationManifestState(root);
|
|
91
|
+
return state.status === "valid" ? state.manifest : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validates one temporary exception. Returns finding descriptors:
|
|
96
|
+
* - { kind: "exception-incomplete", missing } when any required field is absent/blank;
|
|
97
|
+
* - { kind: "exception-invalid-deadline" } when deadline is not parseable;
|
|
98
|
+
* - { kind: "exception-expired" } when the deadline lies before `nowMs`.
|
|
99
|
+
* Expired exceptions are never renewed by tooling; they simply keep blocking.
|
|
100
|
+
*/
|
|
101
|
+
export function validateException(exception, index, nowMs) {
|
|
102
|
+
const missing = EXCEPTION_REQUIRED_FIELDS.filter(
|
|
103
|
+
(field) => typeof exception?.[field] !== "string" || exception[field].trim() === "",
|
|
104
|
+
);
|
|
105
|
+
if (missing.length > 0) {
|
|
106
|
+
return [{ index, kind: "exception-incomplete", missing }];
|
|
107
|
+
}
|
|
108
|
+
const deadlineMs = Date.parse(exception.deadline);
|
|
109
|
+
if (Number.isNaN(deadlineMs)) {
|
|
110
|
+
return [{ index, kind: "exception-invalid-deadline", deadline: exception.deadline }];
|
|
111
|
+
}
|
|
112
|
+
if (deadlineMs < nowMs) {
|
|
113
|
+
return [{ index, kind: "exception-expired", deadline: exception.deadline }];
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Embedded repositories: only a nested `.git` entry is repository evidence
|
|
120
|
+
* (read-only Git evidence probe). A `.git` directory below the target root, or a `.git` *file*
|
|
121
|
+
* (the gitfile form used by submodules and linked worktrees), both count;
|
|
122
|
+
* `node_modules` and any other opaque directory is never repository
|
|
123
|
+
* evidence. The target's own root `.git` is not a nested repository. The
|
|
124
|
+
* plan reports the containing paths as risks; adoption never operates
|
|
125
|
+
* inside a nested repository.
|
|
126
|
+
*/
|
|
127
|
+
export function findNestedRepositories(entries) {
|
|
128
|
+
const repositories = [];
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const segments = entry.path.split("/");
|
|
131
|
+
if (segments[segments.length - 1] !== ".git") continue;
|
|
132
|
+
if (entry.path === ".git") continue;
|
|
133
|
+
const isGitDirectory = entry.kind === "directory-opaque";
|
|
134
|
+
const isGitFile = entry.kind === "file"; // gitfile: submodule/worktree link
|
|
135
|
+
if (isGitDirectory || isGitFile) repositories.push(segments.slice(0, -1).join("/"));
|
|
136
|
+
}
|
|
137
|
+
return repositories.sort();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Assesses the legacy exit list: one entry per declared legacy path with
|
|
142
|
+
* status "present" (still on disk), "absent" (already removed) or "invalid"
|
|
143
|
+
* (path rejected by harness containment or unreadable — fail-closed).
|
|
144
|
+
*
|
|
145
|
+
* Path containment: every user-provided path is resolved through
|
|
146
|
+
* the harness containment layer, which rejects absolute paths, `..`
|
|
147
|
+
* traversal, Windows drive/UNC/backslash forms and symlink escapes BEFORE
|
|
148
|
+
* any outside access; an invalid entry therefore never reveals whether a
|
|
149
|
+
* path outside the target root exists. Nothing is deleted by this function.
|
|
150
|
+
*/
|
|
151
|
+
export async function assessLegacyExitList(root, legacyItems) {
|
|
152
|
+
const list = [];
|
|
153
|
+
if (!Array.isArray(legacyItems)) return list;
|
|
154
|
+
for (const item of legacyItems) {
|
|
155
|
+
if (!item || typeof item.path !== "string" || item.path.trim() === "") {
|
|
156
|
+
list.push({ path: null, replacedBy: null, status: "invalid", invalidKind: "malformed-entry" });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const rel = normalizeRelPath(item.path);
|
|
160
|
+
const replacedBy = typeof item.replacedBy === "string" && item.replacedBy.trim() !== "" ? item.replacedBy : null;
|
|
161
|
+
if (replacedBy === null) {
|
|
162
|
+
list.push({ path: rel, replacedBy: null, status: "invalid", invalidKind: "empty-replaced-by" });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
let resolved;
|
|
166
|
+
try {
|
|
167
|
+
resolved = await resolveContained(root, rel);
|
|
168
|
+
} catch (cause) {
|
|
169
|
+
// Containment rejection (traversal, absolute/UNC/drive forms, symlink
|
|
170
|
+
// escape) is decided without probing outside the root: fail closed and
|
|
171
|
+
// report the stable harness kind, never an existence oracle.
|
|
172
|
+
list.push({
|
|
173
|
+
path: rel,
|
|
174
|
+
replacedBy,
|
|
175
|
+
status: "invalid",
|
|
176
|
+
invalidKind: cause instanceof HarnessError && cause.details?.kind ? cause.details.kind : "containment-rejected",
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let status = "absent";
|
|
181
|
+
try {
|
|
182
|
+
await lstat(resolved);
|
|
183
|
+
status = "present";
|
|
184
|
+
} catch {
|
|
185
|
+
status = "absent";
|
|
186
|
+
}
|
|
187
|
+
list.push({ path: rel, replacedBy, status });
|
|
188
|
+
}
|
|
189
|
+
return list;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Migration state machine. States advance monotonically as facts
|
|
194
|
+
* are proven; completion is the final state only. No tool ever advances a
|
|
195
|
+
* state by writing — states are judgements over read-only evidence.
|
|
196
|
+
*
|
|
197
|
+
* not-declared no contract-valid migration manifest exists;
|
|
198
|
+
* declared a contract-valid manifest exists, adoption unproven;
|
|
199
|
+
* adopted Foundation bytes proven on disk (digests match), no
|
|
200
|
+
* pending write actions, every legacy entry exited, no
|
|
201
|
+
* invalid/expired exceptions, no unresolved conflicts;
|
|
202
|
+
* verified adopted, and the Foundation check gate is green;
|
|
203
|
+
* complete verified, and all four verification evidence documents
|
|
204
|
+
* (unit, integration, consumer, independent audit) exist
|
|
205
|
+
* with matching project identity.
|
|
206
|
+
*/
|
|
207
|
+
export const MIGRATION_STATES = Object.freeze([
|
|
208
|
+
"not-declared",
|
|
209
|
+
"declared",
|
|
210
|
+
"adopted",
|
|
211
|
+
"verified",
|
|
212
|
+
"complete",
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
/** Every Foundation package a complete migration must bind exactly. */
|
|
216
|
+
export const REQUIRED_FOUNDATION_PACKAGES = Object.freeze([
|
|
217
|
+
"skill-family-contracts",
|
|
218
|
+
"skill-family-harness-node",
|
|
219
|
+
"skill-family-engineering-kit",
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
/** The four evidence kinds a complete migration must prove. */
|
|
223
|
+
export const VERIFICATION_EVIDENCE_KINDS = Object.freeze([
|
|
224
|
+
"unit",
|
|
225
|
+
"integration",
|
|
226
|
+
"consumer",
|
|
227
|
+
"independentAudit",
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Assesses the adoption binding declared by a contract-valid manifest
|
|
232
|
+
* (pure): the adopted profile id must equal the planned profile id, and
|
|
233
|
+
* every required Foundation package must be pinned to an exact version and
|
|
234
|
+
* sha256 digest. Returns { profileDeclared, profileMatches, covered,
|
|
235
|
+
* missingPackages }.
|
|
236
|
+
*/
|
|
237
|
+
export function assessAdoptionBinding(manifest, plannedProfileId) {
|
|
238
|
+
const declared = typeof manifest?.targetProfile === "string" && manifest.targetProfile.trim() !== "";
|
|
239
|
+
const profileMatches = declared && manifest.targetProfile === plannedProfileId;
|
|
240
|
+
const packages = Array.isArray(manifest?.foundationPackages) ? manifest.foundationPackages : [];
|
|
241
|
+
const boundNames = new Set(
|
|
242
|
+
packages
|
|
243
|
+
.filter(
|
|
244
|
+
(pkg) =>
|
|
245
|
+
pkg &&
|
|
246
|
+
typeof pkg.name === "string" &&
|
|
247
|
+
typeof pkg.version === "string" &&
|
|
248
|
+
/^[0-9]+\.[0-9]+\.[0-9]+$/.test(pkg.version) &&
|
|
249
|
+
typeof pkg.digest === "string" &&
|
|
250
|
+
/^sha256:[a-f0-9]{64}$/.test(pkg.digest),
|
|
251
|
+
)
|
|
252
|
+
.map((pkg) => pkg.name),
|
|
253
|
+
);
|
|
254
|
+
const missingPackages = REQUIRED_FOUNDATION_PACKAGES.filter((name) => !boundNames.has(name));
|
|
255
|
+
return { profileDeclared: declared, profileMatches, covered: [...boundNames].sort(), missingPackages };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Assesses the four verification evidence documents declared by the manifest
|
|
260
|
+
* (read-only, fail-closed). For each kind the outcome is one of:
|
|
261
|
+
* undeclared the manifest declares no path for this kind;
|
|
262
|
+
* invalid-path the path escapes containment (never an existence oracle);
|
|
263
|
+
* missing the declared file does not exist;
|
|
264
|
+
* unreadable not parseable JSON — evidence must be machine-checkable;
|
|
265
|
+
* identity-mismatch parses, but its projectId does not match the plan;
|
|
266
|
+
* proven exists and its projectId matches the plan's project id.
|
|
267
|
+
*/
|
|
268
|
+
export async function assessVerificationEvidence(rootAbs, verification, plannedProjectId) {
|
|
269
|
+
const declared = verification && typeof verification === "object" ? verification : {};
|
|
270
|
+
const facts = [];
|
|
271
|
+
for (const kind of VERIFICATION_EVIDENCE_KINDS) {
|
|
272
|
+
const rel = declared[kind];
|
|
273
|
+
if (typeof rel !== "string" || rel.trim() === "") {
|
|
274
|
+
facts.push({ kind, path: null, status: "undeclared" });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const pathValue = normalizeRelPath(rel);
|
|
278
|
+
let bytes;
|
|
279
|
+
try {
|
|
280
|
+
// Contained read: rejection or absence both fail closed without
|
|
281
|
+
// revealing anything about paths outside the target root.
|
|
282
|
+
bytes = await readFileContained(rootAbs, pathValue);
|
|
283
|
+
} catch {
|
|
284
|
+
facts.push({ kind, path: pathValue, status: "invalid-path" });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (bytes === null) {
|
|
288
|
+
facts.push({ kind, path: pathValue, status: "missing" });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
let document;
|
|
292
|
+
try {
|
|
293
|
+
document = JSON.parse(bytes.toString("utf8"));
|
|
294
|
+
} catch {
|
|
295
|
+
facts.push({ kind, path: pathValue, status: "unreadable" });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!document || document.projectId !== plannedProjectId) {
|
|
299
|
+
facts.push({ kind, path: pathValue, status: "identity-mismatch" });
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
facts.push({ kind, path: pathValue, status: "proven" });
|
|
303
|
+
}
|
|
304
|
+
return facts;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Migration completion gate (pure). Completion requires ALL of:
|
|
309
|
+
* - a contract-valid declared migration manifest;
|
|
310
|
+
* - the adoption binding proven: the declared targetProfile equals the
|
|
311
|
+
* planned profile id and every required Foundation package is pinned to
|
|
312
|
+
* an exact version plus sha256 digest;
|
|
313
|
+
* - the Project Manifest, managed lock, identity record and every managed
|
|
314
|
+
* skeleton file present on disk with matching digests (adoptionProof);
|
|
315
|
+
* - no pending create/replace/project action left in the writeSet;
|
|
316
|
+
* - the Foundation check gate green;
|
|
317
|
+
* - every legacy implementation removed or provably absent — an invalid
|
|
318
|
+
* (containment-rejected or malformed) entry fails closed like a present
|
|
319
|
+
* one; dual-track wiring alone is not completion;
|
|
320
|
+
* - no incomplete/invalid/expired temporary exceptions;
|
|
321
|
+
* - no unresolved adoption conflicts;
|
|
322
|
+
* - all four verification evidence documents proven with matching identity.
|
|
323
|
+
*
|
|
324
|
+
* Returns { complete, state, blockers } where state is one of
|
|
325
|
+
* MIGRATION_STATES and blockers are stable, human-readable strings.
|
|
326
|
+
* Inputs that are absent are judged exactly like failed proofs: this gate
|
|
327
|
+
* only ever advances on presented evidence.
|
|
328
|
+
*/
|
|
329
|
+
export function evaluateMigrationCompletion({
|
|
330
|
+
manifestDeclared,
|
|
331
|
+
legacyExitList,
|
|
332
|
+
exceptionFindings,
|
|
333
|
+
conflicts,
|
|
334
|
+
binding,
|
|
335
|
+
adoptionProof,
|
|
336
|
+
pendingWrites = 0,
|
|
337
|
+
checkGreen,
|
|
338
|
+
verificationFacts,
|
|
339
|
+
}) {
|
|
340
|
+
const adoptionBlockers = [];
|
|
341
|
+
const verificationBlockers = [];
|
|
342
|
+
const evidenceBlockers = [];
|
|
343
|
+
|
|
344
|
+
if (binding) {
|
|
345
|
+
if (!binding.profileMatches) {
|
|
346
|
+
adoptionBlockers.push(
|
|
347
|
+
binding.profileDeclared
|
|
348
|
+
? "declared targetProfile does not match the planned profile id"
|
|
349
|
+
: "no targetProfile binding declared in the migration manifest",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (binding.missingPackages?.length > 0) {
|
|
353
|
+
adoptionBlockers.push(
|
|
354
|
+
`Foundation packages not bound to exact version+digest: ${binding.missingPackages.join(", ")}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
adoptionBlockers.push("adoption binding not assessed");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (adoptionProof) {
|
|
362
|
+
if (adoptionProof.missing?.length > 0) {
|
|
363
|
+
adoptionBlockers.push(
|
|
364
|
+
`${adoptionProof.missing.length} managed skeleton file(s) absent on disk: ${adoptionProof.missing.join(", ")}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (adoptionProof.mismatched?.length > 0) {
|
|
368
|
+
adoptionBlockers.push(
|
|
369
|
+
`${adoptionProof.mismatched.length} managed file(s) drifted from the planned digests: ${adoptionProof.mismatched.join(", ")}`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
adoptionBlockers.push("adoption proof not assessed");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const pending = typeof pendingWrites === "number" ? pendingWrites : (pendingWrites ?? []).length;
|
|
377
|
+
if (pending > 0) {
|
|
378
|
+
adoptionBlockers.push(`${pending} pending write action(s) remain in the writeSet`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (const item of legacyExitList ?? []) {
|
|
382
|
+
if (item.status === "present") {
|
|
383
|
+
adoptionBlockers.push(`legacy implementation still present: ${item.path}`);
|
|
384
|
+
} else if (item.status === "invalid") {
|
|
385
|
+
adoptionBlockers.push(`legacy entry cannot be verified (fail-closed): ${item.path ?? "<malformed>"} (${item.invalidKind ?? "unknown"})`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const finding of exceptionFindings ?? []) {
|
|
389
|
+
if (finding.kind === "exception-incomplete") {
|
|
390
|
+
adoptionBlockers.push(`exception #${finding.index} missing required fields: ${finding.missing.join(", ")}`);
|
|
391
|
+
} else if (finding.kind === "exception-invalid-deadline") {
|
|
392
|
+
adoptionBlockers.push(`exception #${finding.index} has an unparseable deadline: ${finding.deadline}`);
|
|
393
|
+
} else if (finding.kind === "exception-expired") {
|
|
394
|
+
adoptionBlockers.push(`exception #${finding.index} expired at ${finding.deadline}; tooling never renews exceptions`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if ((conflicts ?? []).length > 0) {
|
|
398
|
+
adoptionBlockers.push(`${conflicts.length} unresolved adoption conflict(s) remain`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (checkGreen !== true) {
|
|
402
|
+
verificationBlockers.push("the Foundation check gate is not green");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const fact of verificationFacts ?? []) {
|
|
406
|
+
if (fact.status === "proven") continue;
|
|
407
|
+
evidenceBlockers.push(`verification evidence ${fact.kind} is ${fact.status}${fact.path ? ` (${fact.path})` : ""}`);
|
|
408
|
+
}
|
|
409
|
+
if (!Array.isArray(verificationFacts)) {
|
|
410
|
+
evidenceBlockers.push("verification evidence not assessed");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let state = "not-declared";
|
|
414
|
+
if (manifestDeclared) {
|
|
415
|
+
if (adoptionBlockers.length > 0) state = "declared";
|
|
416
|
+
else if (verificationBlockers.length > 0) state = "adopted";
|
|
417
|
+
else if (evidenceBlockers.length > 0) state = "verified";
|
|
418
|
+
else state = "complete";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const blockers = [];
|
|
422
|
+
if (!manifestDeclared) {
|
|
423
|
+
blockers.push(`no contract-valid migration manifest declared at ${MIGRATION_MANIFEST_PATH}`);
|
|
424
|
+
}
|
|
425
|
+
blockers.push(...adoptionBlockers, ...verificationBlockers, ...evidenceBlockers);
|
|
426
|
+
return { complete: state === "complete", state, blockers };
|
|
427
|
+
}
|