skill-family-engineering-kit 0.4.0 → 0.6.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 +42 -0
- package/CHANGELOG.zh-CN.md +42 -0
- package/README.md +12 -11
- package/README.zh-CN.md +12 -11
- package/package.json +3 -3
- package/release-notes/0.5.0.yaml +21 -0
- package/release-notes/0.6.0.yaml +25 -0
- package/src/adopt-plan.mjs +81 -159
- package/src/check.mjs +443 -19
- package/src/cli.mjs +80 -1
- package/src/core-check.mjs +8 -4
- package/src/entry-check.mjs +384 -0
- package/src/errors.mjs +21 -0
- package/src/index.mjs +35 -6
- package/src/projection.mjs +221 -12
- package/src/relock.mjs +397 -0
- package/src/skeleton.mjs +162 -4
- package/CODE_OF_CONDUCT.md +0 -131
- package/CONTRIBUTING.md +0 -25
- package/SECURITY.md +0 -33
- package/docs/.nojekyll +0 -0
- package/docs/404.html +0 -1666
- package/docs/agents/architecture-routing/index.html +0 -1944
- package/docs/agents/capability-catalog.en.json +0 -1748
- package/docs/agents/capability-catalog.json +0 -1034
- package/docs/agents/capability-catalog.schema.json +0 -179
- package/docs/agents/capability-catalog.zh-CN.json +0 -1748
- package/docs/agents/index.html +0 -1914
- package/docs/architecture/index.html +0 -2194
- package/docs/assets/images/favicon.png +0 -0
- package/docs/assets/javascripts/bundle.d7400e89.min.js +0 -16
- package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- package/docs/assets/javascripts/lunr/tinyseg.js +0 -206
- package/docs/assets/javascripts/lunr/wordcut.js +0 -6708
- package/docs/assets/javascripts/workers/search.2c215733.min.js +0 -42
- package/docs/assets/stylesheets/main.ec1eaa64.min.css +0 -1
- package/docs/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- package/docs/en/agents/architecture-routing/index.html +0 -1944
- package/docs/en/agents/index.html +0 -1925
- package/docs/en/architecture/index.html +0 -2196
- package/docs/en/examples-and-fixtures/index.html +0 -1888
- package/docs/en/help/index.html +0 -2035
- package/docs/en/index.html +0 -1895
- package/docs/en/licensing/index.html +0 -1917
- package/docs/en/migration/index.html +0 -2335
- package/docs/en/quickstart/index.html +0 -1919
- package/docs/en/recipes/adapter-text-closure/index.html +0 -2000
- package/docs/en/recipes/adopt-existing-repository/index.html +0 -1987
- package/docs/en/recipes/deterministic-human-report/index.html +0 -1998
- package/docs/en/recipes/domain-schema-validation/index.html +0 -2009
- package/docs/en/recipes/durable-local-state/index.html +0 -2009
- package/docs/en/recipes/host-profile-integration/index.html +0 -2001
- package/docs/en/recipes/index.html +0 -1877
- package/docs/en/recipes/safe-filesystem-and-atomic-write/index.html +0 -1994
- package/docs/en/reference/api/index.html +0 -1941
- package/docs/en/reference/compatibility/index.html +0 -2036
- package/docs/en/reference/failure-and-side-effect-matrix/index.html +0 -2216
- package/docs/examples-and-fixtures/index.html +0 -1871
- package/docs/git-lifecycle/index.html +0 -2029
- package/docs/help/index.html +0 -2035
- package/docs/index.html +0 -1893
- package/docs/integration/audit/baseline/audit-codes.json +0 -62
- package/docs/integration/audit/failure-evidence/index.html +0 -1925
- package/docs/integration/audit/independence/index.html +0 -1894
- package/docs/integration/audit/index.html +0 -1880
- package/docs/integration/audit/mutation-taxonomy/index.html +0 -2075
- package/docs/integration/audit/schemas/audit-evidence.schema.json +0 -182
- package/docs/integration/audit/version-compatibility/index.html +0 -1901
- package/docs/licensing/index.html +0 -1917
- package/docs/migration/index.html +0 -2335
- package/docs/public/status/index.html +0 -1884
- package/docs/quickstart/index.html +0 -1919
- package/docs/recipes/adapter-text-closure/index.html +0 -2000
- package/docs/recipes/adopt-existing-repository/index.html +0 -1987
- package/docs/recipes/deterministic-human-report/index.html +0 -1998
- package/docs/recipes/domain-schema-validation/index.html +0 -2009
- package/docs/recipes/durable-local-state/index.html +0 -2009
- package/docs/recipes/host-profile-integration/index.html +0 -2001
- package/docs/recipes/index.html +0 -1877
- package/docs/recipes/safe-filesystem-and-atomic-write/index.html +0 -1994
- package/docs/reference/api/contracts/index.html +0 -2558
- package/docs/reference/api/engineering-kit/index.html +0 -2577
- package/docs/reference/api/harness/index.html +0 -3029
- package/docs/reference/api/index.html +0 -1881
- package/docs/reference/compatibility/index.html +0 -2036
- package/docs/reference/failure-and-side-effect-matrix/index.html +0 -2216
- package/docs/search/search_index.json +0 -1
- package/docs/setup/index.html +0 -1995
- package/docs/sitemap.xml +0 -207
package/src/check.mjs
CHANGED
|
@@ -5,13 +5,16 @@ import {
|
|
|
5
5
|
readFileContained,
|
|
6
6
|
validateContractDocument,
|
|
7
7
|
} from "skill-family-harness-node";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
8
9
|
import { invalidParamsError, KIT_ERROR_KINDS } from "./errors.mjs";
|
|
9
10
|
import { probeGitState } from "./gitprobe.mjs";
|
|
10
11
|
import {
|
|
11
12
|
KIT_TOOL_NAME,
|
|
12
13
|
KIT_VERSION,
|
|
13
14
|
MANAGED_LOCK_PATH,
|
|
15
|
+
PLATFORM_SUBSET_DECLARATION_PATH,
|
|
14
16
|
PROJECT_MANIFEST_PATH,
|
|
17
|
+
PUBLIC_BOUNDARY_DECLARATION_PATH,
|
|
15
18
|
} from "./skeleton.mjs";
|
|
16
19
|
import {
|
|
17
20
|
listTargetEntries,
|
|
@@ -31,14 +34,44 @@ import {
|
|
|
31
34
|
/**
|
|
32
35
|
* check — diagnosis only, never auto-fix.
|
|
33
36
|
*
|
|
34
|
-
*
|
|
37
|
+
* Nine diagnosis classes. The first seven are exactly as the FND-030 and
|
|
38
|
+
* FND-045 hand-offs bound them; the audit remediation C2 delivery extends
|
|
39
|
+
* the closed set to nine (boundary, platform) and extends the version class
|
|
40
|
+
* with the version single-source consistency facts:
|
|
35
41
|
* contracts — discovered contract documents against registered schemas;
|
|
36
|
-
* drift — managed-file-lock entries vs actual bytes on disk
|
|
42
|
+
* drift — managed-file-lock entries vs actual bytes on disk (the
|
|
43
|
+
* managed-artifact drift check; C2 "生成物漂移" is carried by
|
|
44
|
+
* this existing class, aligned, not duplicated; SG-37: on a
|
|
45
|
+
* fresh checkout the build outputs simply do not exist yet, so
|
|
46
|
+
* when EVERY well-formed lock entry is absent on disk the
|
|
47
|
+
* target is diagnosed as never-built — one dedicated
|
|
48
|
+
* NEVER_BUILT finding carrying build-first guidance, kept
|
|
49
|
+
* distinct from both content drift and partial loss; the
|
|
50
|
+
* fail-closed semantics are unchanged: it remains a finding);
|
|
37
51
|
* closure — harness resource closure over declared inputs/outputs;
|
|
38
|
-
* version — declared contracts version vs the frozen contracts version
|
|
52
|
+
* version — declared contracts version vs the frozen contracts version,
|
|
53
|
+
* plus version single-source consistency (SG-13/14, VRG-001/002:
|
|
54
|
+
* package.json is the single version authority; no double-headed
|
|
55
|
+
* VERSION authority; every workspace package.json carries the
|
|
56
|
+
* same single-source version);
|
|
39
57
|
* docs — documentation facts (README presence, identity consistency);
|
|
40
58
|
* git — read-only Git pre-state (never a git write);
|
|
41
|
-
* identity — licensing and identity drift checks (FND-045)
|
|
59
|
+
* identity — licensing and identity drift checks (FND-045);
|
|
60
|
+
* boundary — public release boundary declaration (SG-17, VRG-005): when a
|
|
61
|
+
* public-boundary-declaration document is present it is schema-
|
|
62
|
+
* verified and mechanically reconciled against the actual file
|
|
63
|
+
* set (required paths present, forbidden paths absent, no
|
|
64
|
+
* undeclared file under a public root); absence is data, not a
|
|
65
|
+
* finding — whether a project owes a boundary declaration is a
|
|
66
|
+
* semantic/release decision;
|
|
67
|
+
* platform — platform subset restriction declaration (SFA-PLAT-002 / B3):
|
|
68
|
+
* when a platform-subset-declaration document is present it is
|
|
69
|
+
* verified against the frozen four-field template with the
|
|
70
|
+
* controlled platform vocabulary of the observation-scope
|
|
71
|
+
* contract, and its declared version is reconciled against the
|
|
72
|
+
* single-source package.json version; absence is data, not a
|
|
73
|
+
* finding (declaration completeness is carried by semantic
|
|
74
|
+
* review until a project declares).
|
|
42
75
|
*
|
|
43
76
|
* Every class is independently complete: it loads its own inputs and never
|
|
44
77
|
* consumes another class's cache, so a full run and a `--only <class>` run
|
|
@@ -55,7 +88,17 @@ import {
|
|
|
55
88
|
* signal (0 clean, 1 findings, 2 rejected/usage/mechanism error).
|
|
56
89
|
*/
|
|
57
90
|
|
|
58
|
-
const CHECK_CLASSES = Object.freeze([
|
|
91
|
+
const CHECK_CLASSES = Object.freeze([
|
|
92
|
+
"contracts",
|
|
93
|
+
"drift",
|
|
94
|
+
"closure",
|
|
95
|
+
"version",
|
|
96
|
+
"docs",
|
|
97
|
+
"git",
|
|
98
|
+
"identity",
|
|
99
|
+
"boundary",
|
|
100
|
+
"platform",
|
|
101
|
+
]);
|
|
59
102
|
|
|
60
103
|
export { CHECK_CLASSES };
|
|
61
104
|
|
|
@@ -199,39 +242,71 @@ async function checkDrift(rootAbs, findings) {
|
|
|
199
242
|
return null;
|
|
200
243
|
}
|
|
201
244
|
const entries = Array.isArray(lock?.entries) ? lock.entries : [];
|
|
245
|
+
// SG-37: classify every well-formed lock entry before emitting findings, so
|
|
246
|
+
// a target whose managed build outputs were NEVER built is diagnosed with
|
|
247
|
+
// the dedicated never-built finding (plus build-first guidance) instead of
|
|
248
|
+
// N per-file missing findings that are indistinguishable from content
|
|
249
|
+
// drift or partial loss. Fail-closed semantics are unchanged: every branch
|
|
250
|
+
// below remains a finding (exit code 1), never a silent pass.
|
|
251
|
+
const missingPaths = [];
|
|
252
|
+
const driftFindings = [];
|
|
253
|
+
let wellFormed = 0;
|
|
254
|
+
let escaped = 0;
|
|
202
255
|
for (const entry of entries) {
|
|
203
256
|
const rel = typeof entry?.path === "string" ? normalizeRelPath(entry.path) : null;
|
|
204
257
|
const declaredHash = entry?.hash?.value;
|
|
205
258
|
if (!rel || typeof declaredHash !== "string" || !/^[0-9a-f]{64}$/.test(declaredHash)) {
|
|
206
259
|
continue; // malformed entries are reported by the contracts class
|
|
207
260
|
}
|
|
261
|
+
wellFormed += 1;
|
|
208
262
|
const text = await readManagedCandidate(rootAbs, rel, findings);
|
|
209
263
|
if (text === null) {
|
|
210
|
-
|
|
264
|
+
missingPaths.push(rel);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (typeof text !== "string") {
|
|
268
|
+
escaped += 1; // escaping path already reported by readManagedCandidate
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const actual = digestBytes(Buffer.from(text, "utf8"));
|
|
272
|
+
if (actual !== declaredHash) {
|
|
273
|
+
driftFindings.push(
|
|
211
274
|
finding(
|
|
212
275
|
"drift",
|
|
213
|
-
KIT_ERROR_KINDS.
|
|
276
|
+
KIT_ERROR_KINDS.MANAGED_FILE_DRIFT,
|
|
214
277
|
"SFC2004",
|
|
215
|
-
`managed file
|
|
278
|
+
`managed file drifted from its locked hash: ${rel} (lock=${declaredHash.slice(0, 12)}… actual=${actual.slice(0, 12)}…)`,
|
|
216
279
|
{ path: rel },
|
|
217
280
|
),
|
|
218
281
|
);
|
|
219
|
-
continue;
|
|
220
282
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
283
|
+
}
|
|
284
|
+
if (wellFormed > 0 && escaped === 0 && missingPaths.length === wellFormed) {
|
|
285
|
+
// Every managed output declared in the lock is absent on disk: the build
|
|
286
|
+
// was never run. This is the fresh-checkout state, not content drift.
|
|
287
|
+
findings.push(
|
|
288
|
+
finding(
|
|
289
|
+
"drift",
|
|
290
|
+
KIT_ERROR_KINDS.NEVER_BUILT,
|
|
291
|
+
"SFC2004",
|
|
292
|
+
`managed build outputs have never been built: all ${wellFormed} managed file(s) declared in the lock are absent on disk; this is a never-built target, not content drift — run the project's build first, then re-run verify/check`,
|
|
293
|
+
{ neverBuilt: true, absentPaths: [...missingPaths].sort() },
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
} else {
|
|
297
|
+
for (const rel of missingPaths) {
|
|
224
298
|
findings.push(
|
|
225
299
|
finding(
|
|
226
300
|
"drift",
|
|
227
|
-
KIT_ERROR_KINDS.
|
|
301
|
+
KIT_ERROR_KINDS.MANAGED_FILE_MISSING,
|
|
228
302
|
"SFC2004",
|
|
229
|
-
`managed file
|
|
303
|
+
`managed file declared in the lock does not exist: ${rel}`,
|
|
230
304
|
{ path: rel },
|
|
231
305
|
),
|
|
232
306
|
);
|
|
233
307
|
}
|
|
234
308
|
}
|
|
309
|
+
findings.push(...driftFindings);
|
|
235
310
|
return lock;
|
|
236
311
|
}
|
|
237
312
|
|
|
@@ -291,7 +366,70 @@ async function checkClosure(rootAbs, findings) {
|
|
|
291
366
|
}
|
|
292
367
|
}
|
|
293
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Version single-source consistency (SG-13/14; audit VRG-001/VRG-002).
|
|
371
|
+
*
|
|
372
|
+
* The version truth source of a release unit is the `version` field of its
|
|
373
|
+
* package.json. Two mechanical facts follow:
|
|
374
|
+
* - VRG-001: a VERSION file alongside package.json is a double-headed
|
|
375
|
+
* version authority; the authority must converge on package.json;
|
|
376
|
+
* - VRG-002: the version value is declared once; every other occurrence is
|
|
377
|
+
* mechanically synchronized — here: every nested workspace package.json
|
|
378
|
+
* must carry exactly the root package.json version.
|
|
379
|
+
*
|
|
380
|
+
* When no root package.json version exists there is no single source to
|
|
381
|
+
* compare against and the sub-check reports nothing (never guesses).
|
|
382
|
+
* Returns { rootVersion, versionFilePresent, drifted }.
|
|
383
|
+
*/
|
|
384
|
+
async function checkVersionSingleSource(rootAbs, findings) {
|
|
385
|
+
const facts = { rootVersion: null, versionFilePresent: false, drifted: [] };
|
|
386
|
+
const packageDocument = await loadDocument(rootAbs, "package.json");
|
|
387
|
+
if (packageDocument.state !== "ok" || typeof packageDocument.value?.version !== "string") {
|
|
388
|
+
return facts;
|
|
389
|
+
}
|
|
390
|
+
facts.rootVersion = packageDocument.value.version;
|
|
391
|
+
|
|
392
|
+
const versionFile = await readOptionalFile(rootAbs, "VERSION");
|
|
393
|
+
if (versionFile !== null) {
|
|
394
|
+
facts.versionFilePresent = true;
|
|
395
|
+
findings.push(
|
|
396
|
+
finding(
|
|
397
|
+
"version",
|
|
398
|
+
KIT_ERROR_KINDS.VERSION_AUTHORITY_DUAL,
|
|
399
|
+
"SFC2004",
|
|
400
|
+
"VERSION file exists alongside package.json; version authority must converge on the package.json version field as the single source (VRG-001)",
|
|
401
|
+
{ path: "VERSION" },
|
|
402
|
+
),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const entries = await listTargetEntries(rootAbs);
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
if (entry.kind !== "file" || entry.path === "package.json" || !entry.path.endsWith("package.json")) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const nested = await loadDocument(rootAbs, entry.path);
|
|
412
|
+
if (nested.state !== "ok" || typeof nested.value?.version !== "string") continue;
|
|
413
|
+
if (nested.value.version !== facts.rootVersion) {
|
|
414
|
+
facts.drifted.push({ path: entry.path, version: nested.value.version });
|
|
415
|
+
findings.push(
|
|
416
|
+
finding(
|
|
417
|
+
"version",
|
|
418
|
+
KIT_ERROR_KINDS.VERSION_SINGLE_SOURCE_DRIFT,
|
|
419
|
+
"SFC2004",
|
|
420
|
+
`${entry.path} declares version ${nested.value.version}; the single version source is the root package.json version ${facts.rootVersion} (VRG-002)`,
|
|
421
|
+
{ path: entry.path, declared: nested.value.version, expected: facts.rootVersion },
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return facts;
|
|
427
|
+
}
|
|
428
|
+
|
|
294
429
|
async function checkVersion(rootAbs, findings) {
|
|
430
|
+
// The single-source consistency facts do not depend on the project manifest;
|
|
431
|
+
// they are computed on every run (class independence).
|
|
432
|
+
const singleSource = await checkVersionSingleSource(rootAbs, findings);
|
|
295
433
|
// Loads its own manifest without schema validation: schema conformance is
|
|
296
434
|
// the contracts class's finding, the version class only needs the fact.
|
|
297
435
|
const document = await loadDocument(rootAbs, PROJECT_MANIFEST_PATH, {
|
|
@@ -307,7 +445,7 @@ async function checkVersion(rootAbs, findings) {
|
|
|
307
445
|
{ path: PROJECT_MANIFEST_PATH, documentState: document.state },
|
|
308
446
|
),
|
|
309
447
|
);
|
|
310
|
-
return { contractsVersion: null };
|
|
448
|
+
return { contractsVersion: null, singleSource };
|
|
311
449
|
}
|
|
312
450
|
if (document.state === "parse-failed") {
|
|
313
451
|
findings.push(
|
|
@@ -319,7 +457,7 @@ async function checkVersion(rootAbs, findings) {
|
|
|
319
457
|
{ path: PROJECT_MANIFEST_PATH, documentState: document.state },
|
|
320
458
|
),
|
|
321
459
|
);
|
|
322
|
-
return { contractsVersion: null };
|
|
460
|
+
return { contractsVersion: null, singleSource };
|
|
323
461
|
}
|
|
324
462
|
if (document.state === "incomplete") {
|
|
325
463
|
findings.push(
|
|
@@ -331,7 +469,7 @@ async function checkVersion(rootAbs, findings) {
|
|
|
331
469
|
{ path: PROJECT_MANIFEST_PATH, documentState: document.state },
|
|
332
470
|
),
|
|
333
471
|
);
|
|
334
|
-
return { contractsVersion: null };
|
|
472
|
+
return { contractsVersion: null, singleSource };
|
|
335
473
|
}
|
|
336
474
|
const declared = document.value.contracts.version;
|
|
337
475
|
if (declared !== CONTRACTS_VERSION) {
|
|
@@ -345,7 +483,7 @@ async function checkVersion(rootAbs, findings) {
|
|
|
345
483
|
),
|
|
346
484
|
);
|
|
347
485
|
}
|
|
348
|
-
return { contractsVersion: declared };
|
|
486
|
+
return { contractsVersion: declared, singleSource };
|
|
349
487
|
}
|
|
350
488
|
|
|
351
489
|
async function checkDocs(rootAbs, findings) {
|
|
@@ -489,6 +627,284 @@ async function checkIdentity(rootAbs, findings, profilesRoot) {
|
|
|
489
627
|
return identityRecord;
|
|
490
628
|
}
|
|
491
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Public boundary verification (SG-17; audit VRG-005, carrier of the C5
|
|
632
|
+
* public-boundary-declaration contract).
|
|
633
|
+
*
|
|
634
|
+
* When the target holds a public-boundary-declaration document it is
|
|
635
|
+
* verified in two steps: (1) the document must pass the registered
|
|
636
|
+
* public-boundary-declaration schema; (2) the declaration is mechanically
|
|
637
|
+
* reconciled against the actual regular file set — every required path must
|
|
638
|
+
* exist, every forbidden path must be absent, every declared public file
|
|
639
|
+
* must exist and live under a declared public root, and every regular file
|
|
640
|
+
* under a public root must be declared (an undeclared file is a leak).
|
|
641
|
+
*
|
|
642
|
+
* Absence of the declaration is data, never a finding: whether a project
|
|
643
|
+
* owes a boundary declaration depends on its release surface and is decided
|
|
644
|
+
* by semantic review / the release process (VRG-005). Returns
|
|
645
|
+
* { declared, declarationId, declaredVersion, documentState }.
|
|
646
|
+
*/
|
|
647
|
+
async function checkBoundary(rootAbs, findings) {
|
|
648
|
+
const data = { declared: false, declarationId: null, declaredVersion: null, documentState: "missing" };
|
|
649
|
+
const document = await loadDocument(rootAbs, PUBLIC_BOUNDARY_DECLARATION_PATH, {
|
|
650
|
+
schemaObject: "public-boundary-declaration",
|
|
651
|
+
});
|
|
652
|
+
data.documentState = document.state;
|
|
653
|
+
if (document.state === "missing") return data;
|
|
654
|
+
data.declared = true;
|
|
655
|
+
if (document.state === "parse-failed") {
|
|
656
|
+
findings.push(
|
|
657
|
+
finding(
|
|
658
|
+
"boundary",
|
|
659
|
+
KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
|
|
660
|
+
"SFC2004",
|
|
661
|
+
`${PUBLIC_BOUNDARY_DECLARATION_PATH} is not valid JSON`,
|
|
662
|
+
{ path: PUBLIC_BOUNDARY_DECLARATION_PATH, documentState: document.state },
|
|
663
|
+
),
|
|
664
|
+
);
|
|
665
|
+
return data;
|
|
666
|
+
}
|
|
667
|
+
if (document.state === "schema-invalid") {
|
|
668
|
+
findings.push(
|
|
669
|
+
finding(
|
|
670
|
+
"boundary",
|
|
671
|
+
"schema-validation-failed",
|
|
672
|
+
"SFC1001",
|
|
673
|
+
`${PUBLIC_BOUNDARY_DECLARATION_PATH} fails the registered public-boundary-declaration schema: ${document.outcome.errors
|
|
674
|
+
.slice(0, 3)
|
|
675
|
+
.map((entry) => `${entry.instancePath || "/"} ${entry.message}`)
|
|
676
|
+
.join("; ")}`,
|
|
677
|
+
{
|
|
678
|
+
path: PUBLIC_BOUNDARY_DECLARATION_PATH,
|
|
679
|
+
schemaId: findSchemaByObject("public-boundary-declaration").$id,
|
|
680
|
+
documentState: document.state,
|
|
681
|
+
},
|
|
682
|
+
),
|
|
683
|
+
);
|
|
684
|
+
return data;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const declaration = document.value;
|
|
688
|
+
data.declarationId = declaration.declarationId;
|
|
689
|
+
data.declaredVersion = declaration.declaredVersion;
|
|
690
|
+
const violation = (message, extra) =>
|
|
691
|
+
findings.push(
|
|
692
|
+
finding("boundary", KIT_ERROR_KINDS.PUBLIC_BOUNDARY_VIOLATION, "SFC2004", message, {
|
|
693
|
+
path: PUBLIC_BOUNDARY_DECLARATION_PATH,
|
|
694
|
+
...(extra ?? {}),
|
|
695
|
+
}),
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const entries = await listTargetEntries(rootAbs);
|
|
699
|
+
const regularFiles = new Set(entries.filter((entry) => entry.kind === "file").map((entry) => entry.path));
|
|
700
|
+
const publicFiles = new Set(declaration.publicFiles);
|
|
701
|
+
const underPublicRoot = (rel) =>
|
|
702
|
+
declaration.publicRoots.some((root) => rel.startsWith(`${root}/`));
|
|
703
|
+
|
|
704
|
+
for (const required of declaration.requiredPaths) {
|
|
705
|
+
if (!regularFiles.has(required)) {
|
|
706
|
+
violation(`required public path is missing from the release set: ${required}`, {
|
|
707
|
+
subject: required,
|
|
708
|
+
rule: "requiredPaths",
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
for (const forbidden of declaration.forbiddenPaths) {
|
|
713
|
+
if (regularFiles.has(forbidden)) {
|
|
714
|
+
violation(`forbidden path is present in the release set: ${forbidden}`, {
|
|
715
|
+
subject: forbidden,
|
|
716
|
+
rule: "forbiddenPaths",
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
for (const declared of declaration.publicFiles) {
|
|
721
|
+
if (!regularFiles.has(declared)) {
|
|
722
|
+
violation(`declared public file does not exist on disk: ${declared}`, {
|
|
723
|
+
subject: declared,
|
|
724
|
+
rule: "publicFiles",
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (!underPublicRoot(declared)) {
|
|
728
|
+
violation(`declared public file is outside every declared public root: ${declared}`, {
|
|
729
|
+
subject: declared,
|
|
730
|
+
rule: "publicRoots",
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const actual of regularFiles) {
|
|
735
|
+
if (underPublicRoot(actual) && !publicFiles.has(actual)) {
|
|
736
|
+
violation(`file under a public root is not declared in publicFiles (undeclared leak): ${actual}`, {
|
|
737
|
+
subject: actual,
|
|
738
|
+
rule: "publicFiles",
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return data;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Kind of the kit-owned platform subset restriction declaration template. */
|
|
746
|
+
export const PLATFORM_SUBSET_DECLARATION_KIND = "skill-family.platform-subset-declaration";
|
|
747
|
+
|
|
748
|
+
const SEMVER_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Loads the controlled platform vocabulary frozen by the observation-scope
|
|
752
|
+
* contract (standardPlatforms enum). Derived mechanically from the
|
|
753
|
+
* registered schema document of skill-family-contracts — the kit never
|
|
754
|
+
* restates the vocabulary as a second owned copy. Throws when the contract
|
|
755
|
+
* cannot be located (a mechanism failure of the platform class).
|
|
756
|
+
*/
|
|
757
|
+
function loadStandardPlatformVocabulary() {
|
|
758
|
+
const registration = findSchemaByObject("observation-scope");
|
|
759
|
+
const contractsIndexUrl = import.meta.resolve("skill-family-contracts");
|
|
760
|
+
const packageRootUrl = new URL("..", contractsIndexUrl);
|
|
761
|
+
const schemaUrl = new URL(registration.file, packageRootUrl);
|
|
762
|
+
const schema = JSON.parse(readFileSync(schemaUrl, "utf8"));
|
|
763
|
+
const vocabulary = schema?.properties?.standardPlatforms?.items?.enum;
|
|
764
|
+
if (!Array.isArray(vocabulary) || vocabulary.length === 0) {
|
|
765
|
+
throw new Error("observation-scope schema does not freeze a standardPlatforms vocabulary");
|
|
766
|
+
}
|
|
767
|
+
return Object.freeze([...vocabulary]);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Platform subset restriction declaration verification (SFA-PLAT-002 / B3;
|
|
772
|
+
* audit OC-3, decision D-9). Platform coverage is optional; the obligation
|
|
773
|
+
* is an honest, machine-checkable declaration of the released platform
|
|
774
|
+
* subset. The frozen four-field template:
|
|
775
|
+
* project / released_platform_subset / missing_platforms / declared_version.
|
|
776
|
+
*
|
|
777
|
+
* When the target holds a platform-subset-declaration document it is
|
|
778
|
+
* verified mechanically: the template shape, platform ids restricted to the
|
|
779
|
+
* controlled observation-scope vocabulary, no duplicate or overlapping
|
|
780
|
+
* entries, and the declared version reconciled against the single-source
|
|
781
|
+
* root package.json version. Absence is data, never a finding: declaration
|
|
782
|
+
* completeness is carried by semantic review until a project declares.
|
|
783
|
+
* Returns { declared, project, released, missing, declaredVersion, documentState }.
|
|
784
|
+
*/
|
|
785
|
+
async function checkPlatform(rootAbs, findings) {
|
|
786
|
+
const data = {
|
|
787
|
+
declared: false,
|
|
788
|
+
project: null,
|
|
789
|
+
released: [],
|
|
790
|
+
missing: [],
|
|
791
|
+
declaredVersion: null,
|
|
792
|
+
documentState: "missing",
|
|
793
|
+
};
|
|
794
|
+
const loaded = await readOptionalJson(rootAbs, PLATFORM_SUBSET_DECLARATION_PATH);
|
|
795
|
+
if (loaded.reason === "missing") return data;
|
|
796
|
+
data.declared = true;
|
|
797
|
+
const invalid = (message, extra) =>
|
|
798
|
+
findings.push(
|
|
799
|
+
finding("platform", KIT_ERROR_KINDS.PLATFORM_SUBSET_INVALID, "SFC2004", message, {
|
|
800
|
+
path: PLATFORM_SUBSET_DECLARATION_PATH,
|
|
801
|
+
...(extra ?? {}),
|
|
802
|
+
}),
|
|
803
|
+
);
|
|
804
|
+
if (!loaded.ok) {
|
|
805
|
+
data.documentState = "parse-failed";
|
|
806
|
+
findings.push(
|
|
807
|
+
finding(
|
|
808
|
+
"platform",
|
|
809
|
+
KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
|
|
810
|
+
"SFC2004",
|
|
811
|
+
`${PLATFORM_SUBSET_DECLARATION_PATH} is not valid JSON`,
|
|
812
|
+
{ path: PLATFORM_SUBSET_DECLARATION_PATH, documentState: data.documentState },
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
return data;
|
|
816
|
+
}
|
|
817
|
+
data.documentState = "ok";
|
|
818
|
+
const declaration = loaded.value;
|
|
819
|
+
if (declaration?.schemaVersion !== 1) {
|
|
820
|
+
invalid(`schemaVersion must be exactly 1 (got ${JSON.stringify(declaration?.schemaVersion ?? null)})`, {
|
|
821
|
+
field: "schemaVersion",
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
if (declaration?.kind !== PLATFORM_SUBSET_DECLARATION_KIND) {
|
|
825
|
+
invalid(`kind must be "${PLATFORM_SUBSET_DECLARATION_KIND}" (got ${JSON.stringify(declaration?.kind ?? null)})`, {
|
|
826
|
+
field: "kind",
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
if (typeof declaration?.project !== "string" || declaration.project.length === 0) {
|
|
830
|
+
invalid("project must be a non-empty string (template field 1/4)", { field: "project" });
|
|
831
|
+
} else {
|
|
832
|
+
data.project = declaration.project;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const vocabulary = loadStandardPlatformVocabulary();
|
|
836
|
+
const verifyPlatformList = (fieldName) => {
|
|
837
|
+
const value = declaration?.[fieldName];
|
|
838
|
+
if (!Array.isArray(value)) {
|
|
839
|
+
invalid(`${fieldName} must be an array of platform ids (got ${JSON.stringify(value ?? null)})`, {
|
|
840
|
+
field: fieldName,
|
|
841
|
+
});
|
|
842
|
+
return [];
|
|
843
|
+
}
|
|
844
|
+
const seen = new Set();
|
|
845
|
+
for (const platform of value) {
|
|
846
|
+
if (typeof platform !== "string" || platform.length === 0) {
|
|
847
|
+
invalid(`${fieldName} entries must be non-empty strings`, { field: fieldName });
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (seen.has(platform)) {
|
|
851
|
+
invalid(`${fieldName} contains a duplicate platform id: ${platform}`, { field: fieldName, subject: platform });
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
seen.add(platform);
|
|
855
|
+
if (!vocabulary.includes(platform)) {
|
|
856
|
+
invalid(
|
|
857
|
+
`${fieldName} entry is outside the controlled platform vocabulary of the observation-scope contract: ${platform}`,
|
|
858
|
+
{ field: fieldName, subject: platform },
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return [...seen];
|
|
863
|
+
};
|
|
864
|
+
data.released = verifyPlatformList("released_platform_subset");
|
|
865
|
+
data.missing = verifyPlatformList("missing_platforms");
|
|
866
|
+
|
|
867
|
+
const overlap = data.released.filter((platform) => data.missing.includes(platform));
|
|
868
|
+
if (overlap.length > 0) {
|
|
869
|
+
invalid(
|
|
870
|
+
`released_platform_subset and missing_platforms must be disjoint; overlap: ${overlap.join(", ")}`,
|
|
871
|
+
{ field: "released_platform_subset/missing_platforms", subject: overlap },
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (typeof declaration?.declared_version !== "string" || !SEMVER_PATTERN.test(declaration.declared_version)) {
|
|
876
|
+
invalid(
|
|
877
|
+
`declared_version must be a semantic version string X.Y.Z (got ${JSON.stringify(declaration?.declared_version ?? null)})`,
|
|
878
|
+
{ field: "declared_version" },
|
|
879
|
+
);
|
|
880
|
+
} else {
|
|
881
|
+
data.declaredVersion = declaration.declared_version;
|
|
882
|
+
// The declared version is reconciled against the single version source
|
|
883
|
+
// (root package.json); without one there is nothing to compare.
|
|
884
|
+
const packageDocument = await loadDocument(rootAbs, "package.json");
|
|
885
|
+
if (
|
|
886
|
+
packageDocument.state === "ok" &&
|
|
887
|
+
typeof packageDocument.value?.version === "string" &&
|
|
888
|
+
packageDocument.value.version !== declaration.declared_version
|
|
889
|
+
) {
|
|
890
|
+
findings.push(
|
|
891
|
+
finding(
|
|
892
|
+
"platform",
|
|
893
|
+
KIT_ERROR_KINDS.PLATFORM_SUBSET_VERSION_DRIFT,
|
|
894
|
+
"SFC2004",
|
|
895
|
+
`platform subset declaration declares version ${declaration.declared_version}; the single version source (package.json) is ${packageDocument.value.version}`,
|
|
896
|
+
{
|
|
897
|
+
path: PLATFORM_SUBSET_DECLARATION_PATH,
|
|
898
|
+
declared: declaration.declared_version,
|
|
899
|
+
expected: packageDocument.value.version,
|
|
900
|
+
},
|
|
901
|
+
),
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return data;
|
|
906
|
+
}
|
|
907
|
+
|
|
492
908
|
/**
|
|
493
909
|
* Runs all check classes over one target.
|
|
494
910
|
* Options: { root, allowGitSpawn, only, profilesRoot }.
|
|
@@ -518,13 +934,17 @@ export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot
|
|
|
518
934
|
docs: () => checkDocs(rootAbs, findings),
|
|
519
935
|
git: () => checkGit(rootAbs, findings, allowGitSpawn),
|
|
520
936
|
identity: () => checkIdentity(rootAbs, findings, profilesRoot),
|
|
937
|
+
boundary: () => checkBoundary(rootAbs, findings),
|
|
938
|
+
platform: () => checkPlatform(rootAbs, findings),
|
|
521
939
|
};
|
|
522
940
|
|
|
523
941
|
const classData = {
|
|
524
942
|
closure: { digest: null, note: "not-selected" },
|
|
525
943
|
git: null,
|
|
526
|
-
version: { contractsVersion: null },
|
|
944
|
+
version: { contractsVersion: null, singleSource: null },
|
|
527
945
|
identity: null,
|
|
946
|
+
boundary: { declared: false, declarationId: null, declaredVersion: null, documentState: "missing" },
|
|
947
|
+
platform: { declared: false, project: null, released: [], missing: [], declaredVersion: null, documentState: "missing" },
|
|
528
948
|
};
|
|
529
949
|
|
|
530
950
|
for (const name of CHECK_CLASSES) {
|
|
@@ -535,6 +955,8 @@ export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot
|
|
|
535
955
|
if (name === "closure") classData.closure = result;
|
|
536
956
|
if (name === "git") classData.git = result;
|
|
537
957
|
if (name === "version") classData.version = result;
|
|
958
|
+
if (name === "boundary") classData.boundary = result;
|
|
959
|
+
if (name === "platform") classData.platform = result;
|
|
538
960
|
if (name === "identity") {
|
|
539
961
|
classData.identity = result
|
|
540
962
|
? { record: result, licensing: result.licensing, authors: result.authors }
|
|
@@ -597,6 +1019,8 @@ export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot
|
|
|
597
1019
|
version: classData.version,
|
|
598
1020
|
managedDeclarations,
|
|
599
1021
|
identity: classData.identity,
|
|
1022
|
+
boundary: classData.boundary,
|
|
1023
|
+
platform: classData.platform,
|
|
600
1024
|
},
|
|
601
1025
|
policy:
|
|
602
1026
|
"check is diagnosis only: it never writes, never fixes, and never calls git write commands; findings must be resolved by a human or an authorized generator",
|