skill-family-engineering-kit 0.1.2 → 0.2.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.
@@ -504,6 +504,33 @@
504
504
 
505
505
 
506
506
 
507
+ <li class="md-nav__item">
508
+ <a href="../public/status/" class="md-nav__link">
509
+
510
+
511
+
512
+ <span class="md-ellipsis">
513
+
514
+
515
+ 公开状态
516
+
517
+
518
+
519
+ </span>
520
+
521
+
522
+
523
+ </a>
524
+ </li>
525
+
526
+
527
+
528
+
529
+
530
+
531
+
532
+
533
+
507
534
  <li class="md-nav__item">
508
535
  <a href="../migration/" class="md-nav__link">
509
536
 
@@ -813,9 +840,9 @@ pnpm check
813
840
  </code></pre>
814
841
  <ul>
815
842
  <li><code>pnpm synth</code> 再生成 projen 受管文件。受管文件只能通过修改 <code>.projenrc.js</code> 间接变更;手写源码、文档和 fixture 不会被 synth 覆盖。</li>
816
- <li><code>pnpm check</code> 是当前统一根门禁:10 个稳定门禁 ID 按固定顺序串联执行,任一步骤非 0 即整体失败;本仓没有任何 CI/CD workflow,全部门禁都在私有工作区内运行。下方区块由根 <code>package.json</code> 的 <code>scripts.check</code> 机械投影(<code>node scripts/docs/fact-check.mjs</code> 逐字核对,禁止手写清单再次滞后):</li>
843
+ <li><code>pnpm check</code> 是当前统一根门禁:11 个稳定门禁 ID 按固定顺序串联执行,任一步骤非 0 即整体失败;本仓没有任何 CI/CD workflow,全部门禁都在私有工作区内运行。下方区块由根 <code>package.json</code> 的 <code>scripts.check</code> 机械投影(<code>node scripts/docs/fact-check.mjs</code> 逐字核对,禁止手写清单再次滞后):</li>
817
844
  </ul>
818
- <!-- BEGIN SKILL-FAMILY-GATE-STAGES v2 (10 stable gate IDs; derived from package.json scripts.check; verified by fact-check) -->
845
+ <!-- BEGIN SKILL-FAMILY-GATE-STAGES v2 (11 stable gate IDs; derived from package.json scripts.check; verified by fact-check) -->
819
846
  <ol>
820
847
  <li>pnpm run check:structure</li>
821
848
  <li>pnpm run check:packages</li>
@@ -827,6 +854,7 @@ pnpm check
827
854
  <li>pnpm run check:release-artifacts</li>
828
855
  <li>pnpm run check:projen</li>
829
856
  <li>pnpm run check:integration</li>
857
+ <li>pnpm run check:artifacts</li>
830
858
  </ol>
831
859
  <!-- END SKILL-FAMILY-GATE-STAGES -->
832
860
 
package/docs/sitemap.xml CHANGED
@@ -48,4 +48,8 @@
48
48
  <loc>https://ifoohoo.github.io/skill-family-engineering-kit/migration/</loc>
49
49
  <lastmod>1970-01-01</lastmod>
50
50
  </url>
51
+ <url>
52
+ <loc>https://ifoohoo.github.io/skill-family-engineering-kit/public/status/</loc>
53
+ <lastmod>1970-01-01</lastmod>
54
+ </url>
51
55
  </urlset>
package/package.json CHANGED
@@ -8,8 +8,8 @@
8
8
  "url": "https://github.com/ifoohoo/skill-family-engineering-kit/issues"
9
9
  },
10
10
  "dependencies": {
11
- "skill-family-contracts": "0.1.2",
12
- "skill-family-harness-node": "0.1.2"
11
+ "skill-family-contracts": "0.2.0",
12
+ "skill-family-harness-node": "0.2.0"
13
13
  },
14
14
  "description": "Build-time scaffold, adoption planning, projection, and checks.",
15
15
  "engines": {
@@ -35,9 +35,9 @@
35
35
  "url": "https://github.com/ifoohoo/skill-family-engineering-kit.git"
36
36
  },
37
37
  "type": "module",
38
- "version": "0.1.2",
38
+ "version": "0.2.0",
39
39
  "scripts": {
40
- "check": "node --input-type=module -e \"import('./src/index.mjs').then(() => console.log('smoke ok')).catch(e => { console.error(e); process.exit(1) })\"",
41
- "test": "node --input-type=module -e \"import('./src/index.mjs').then(() => console.log('smoke ok')).catch(e => { console.error(e); process.exit(1) })\""
40
+ "check": "node --test",
41
+ "test": "node --test"
42
42
  }
43
43
  }
package/src/check.mjs CHANGED
@@ -40,6 +40,15 @@ import {
40
40
  * git — read-only Git pre-state (never a git write);
41
41
  * identity — licensing and identity drift checks (FND-045).
42
42
  *
43
+ * Every class is independently complete: it loads its own inputs and never
44
+ * consumes another class's cache, so a full run and a `--only <class>` run
45
+ * produce byte-identical findings for that class (the equivalence matrix in
46
+ * test/check-equivalence.test.mjs is the mechanical witness). Document
47
+ * loading distinguishes the four stable states missing / parse-failed /
48
+ * schema-invalid / incomplete, and class results distinguish selected /
49
+ * completed / findings — a class that never ran is never reported as having
50
+ * run.
51
+ *
43
52
  * This module contains no write call of any kind. Mutation-looking flags
44
53
  * (--fix, --apply, ...) are refused at intake by the CLI. Findings carry
45
54
  * stable kinds and registered SFC codes; the exit code is the stable
@@ -50,48 +59,86 @@ const CHECK_CLASSES = Object.freeze(["contracts", "drift", "closure", "version",
50
59
 
51
60
  export { CHECK_CLASSES };
52
61
 
62
+ /**
63
+ * Stable document loading states every check class must distinguish:
64
+ * missing — the file does not exist;
65
+ * parse-failed — the bytes are not valid JSON;
66
+ * schema-invalid — the parsed document fails its registered schema;
67
+ * incomplete — parsed (and schema-checked where requested) but a fact
68
+ * the consuming class needs is absent;
69
+ * ok — every requested state passed.
70
+ */
71
+ export const DOCUMENT_STATES = Object.freeze([
72
+ "missing",
73
+ "parse-failed",
74
+ "schema-invalid",
75
+ "incomplete",
76
+ "ok",
77
+ ]);
78
+
53
79
  function finding(checkClass, kind, code, message, extra) {
54
80
  return { class: checkClass, kind, code, message, ...(extra ?? {}) };
55
81
  }
56
82
 
57
- async function checkContracts(rootAbs, docs, findings) {
83
+ /**
84
+ * Loads one JSON document and classifies it into the stable document states.
85
+ * Options:
86
+ * schemaObject — when given, an ok state additionally requires the parsed
87
+ * document to pass the registered schema of that object;
88
+ * completeWhen — when given, an ok state additionally requires the predicate
89
+ * to hold on the parsed value (absence => "incomplete").
90
+ */
91
+ async function loadDocument(rootAbs, relPath, { schemaObject, completeWhen } = {}) {
92
+ const loaded = await readOptionalJson(rootAbs, relPath);
93
+ if (loaded.reason === "missing") return { state: "missing", value: null };
94
+ if (!loaded.ok) return { state: "parse-failed", value: null };
95
+ if (schemaObject !== undefined) {
96
+ const registration = findSchemaByObject(schemaObject);
97
+ const outcome = validateContractDocument(loaded.value, { schemaId: registration.$id });
98
+ if (!outcome.valid) return { state: "schema-invalid", value: loaded.value, outcome };
99
+ }
100
+ if (completeWhen !== undefined && !completeWhen(loaded.value)) {
101
+ return { state: "incomplete", value: loaded.value };
102
+ }
103
+ return { state: "ok", value: loaded.value };
104
+ }
105
+
106
+ async function checkContracts(rootAbs, findings) {
58
107
  const targets = [
59
108
  ["project-manifest", PROJECT_MANIFEST_PATH],
60
109
  ["managed-file-lock", MANAGED_LOCK_PATH],
61
110
  ];
62
111
  let present = 0;
63
112
  for (const [objectName, relPath] of targets) {
64
- const loaded = await readOptionalJson(rootAbs, relPath);
65
- if (loaded.reason === "missing") continue;
113
+ const document = await loadDocument(rootAbs, relPath, { schemaObject: objectName });
114
+ if (document.state === "missing") continue;
66
115
  present += 1;
67
- docs[relPath] = loaded.ok ? loaded.value : null;
68
- if (!loaded.ok) {
116
+ if (document.state === "parse-failed") {
69
117
  findings.push(
70
118
  finding(
71
119
  "contracts",
72
120
  KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
73
121
  "SFC1001",
74
122
  `${relPath} is not valid JSON`,
75
- { path: relPath },
123
+ { path: relPath, documentState: document.state },
76
124
  ),
77
125
  );
78
126
  continue;
79
127
  }
80
- const registration = findSchemaByObject(objectName);
81
- const outcome = validateContractDocument(loaded.value, { schemaId: registration.$id });
82
- if (!outcome.valid) {
128
+ if (document.state === "schema-invalid") {
83
129
  findings.push(
84
130
  finding(
85
131
  "contracts",
86
132
  "schema-validation-failed",
87
- outcome.errorCode,
88
- `${relPath} fails the registered ${objectName} schema: ${outcome.errors
133
+ "SFC1001",
134
+ `${relPath} fails the registered ${objectName} schema: ${document.outcome.errors
89
135
  .slice(0, 3)
90
136
  .map((entry) => `${entry.instancePath || "/"} ${entry.message}`)
91
137
  .join("; ")}`,
92
- { path: relPath, schemaId: registration.$id },
138
+ { path: relPath, schemaId: findSchemaByObject(objectName).$id, documentState: document.state },
93
139
  ),
94
140
  );
141
+ continue;
95
142
  }
96
143
  }
97
144
  if (present === 0) {
@@ -188,7 +235,10 @@ async function checkDrift(rootAbs, findings) {
188
235
  return lock;
189
236
  }
190
237
 
191
- async function checkClosure(rootAbs, lock, findings) {
238
+ async function checkClosure(rootAbs, findings) {
239
+ // The closure class loads its own lock: it never depends on the drift
240
+ // class having run first.
241
+ const lock = await loadManagedFileLock(rootAbs);
192
242
  if (!lock) {
193
243
  findings.push(
194
244
  finding(
@@ -241,26 +291,49 @@ async function checkClosure(rootAbs, lock, findings) {
241
291
  }
242
292
  }
243
293
 
244
- async function checkVersion(rootAbs, docs, findings) {
245
- let manifest = docs[PROJECT_MANIFEST_PATH];
246
- if (!manifest) {
247
- // Load independently when the contracts check didn't run first
248
- const loaded = await readOptionalJson(rootAbs, PROJECT_MANIFEST_PATH);
249
- if (loaded.ok) manifest = loaded.value;
250
- }
251
- const declared = manifest?.contracts?.version;
252
- if (typeof declared !== "string") {
294
+ async function checkVersion(rootAbs, findings) {
295
+ // Loads its own manifest without schema validation: schema conformance is
296
+ // the contracts class's finding, the version class only needs the fact.
297
+ const document = await loadDocument(rootAbs, PROJECT_MANIFEST_PATH, {
298
+ completeWhen: (value) => typeof value?.contracts?.version === "string",
299
+ });
300
+ if (document.state === "missing") {
253
301
  findings.push(
254
302
  finding(
255
303
  "version",
256
304
  KIT_ERROR_KINDS.CONTRACTS_MISSING,
257
305
  "SFC2004",
258
- "project manifest is absent or lacks contracts.version; version check cannot run",
259
- { path: PROJECT_MANIFEST_PATH },
306
+ "project manifest is absent; version check cannot run without it",
307
+ { path: PROJECT_MANIFEST_PATH, documentState: document.state },
308
+ ),
309
+ );
310
+ return { contractsVersion: null };
311
+ }
312
+ if (document.state === "parse-failed") {
313
+ findings.push(
314
+ finding(
315
+ "version",
316
+ KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
317
+ "SFC2004",
318
+ "project manifest is not valid JSON; version check cannot run without it",
319
+ { path: PROJECT_MANIFEST_PATH, documentState: document.state },
320
+ ),
321
+ );
322
+ return { contractsVersion: null };
323
+ }
324
+ if (document.state === "incomplete") {
325
+ findings.push(
326
+ finding(
327
+ "version",
328
+ KIT_ERROR_KINDS.DOCUMENT_INCOMPLETE,
329
+ "SFC2004",
330
+ "project manifest lacks contracts.version; version check cannot run without it",
331
+ { path: PROJECT_MANIFEST_PATH, documentState: document.state },
260
332
  ),
261
333
  );
262
334
  return { contractsVersion: null };
263
335
  }
336
+ const declared = document.value.contracts.version;
264
337
  if (declared !== CONTRACTS_VERSION) {
265
338
  findings.push(
266
339
  finding(
@@ -275,7 +348,9 @@ async function checkVersion(rootAbs, docs, findings) {
275
348
  return { contractsVersion: declared };
276
349
  }
277
350
 
278
- async function checkDocs(rootAbs, docs, findings) {
351
+ async function checkDocs(rootAbs, findings) {
352
+ // The docs class reads every documentation fact itself: README, package.json
353
+ // and the project manifest. It never consumes another class's state.
279
354
  const readme = await readOptionalFile(rootAbs, "README.md");
280
355
  if (readme === null || readme.trim().length === 0) {
281
356
  findings.push(
@@ -284,26 +359,57 @@ async function checkDocs(rootAbs, docs, findings) {
284
359
  KIT_ERROR_KINDS.README_MISSING,
285
360
  "SFC2004",
286
361
  "README.md is missing or empty (documentation fact)",
287
- { path: "README.md" },
362
+ { path: "README.md", documentState: "missing" },
288
363
  ),
289
364
  );
290
365
  }
291
- const packageJson = await readOptionalJson(rootAbs, "package.json");
292
- const manifest = docs[PROJECT_MANIFEST_PATH];
293
- if (packageJson.ok && manifest && typeof manifest?.project === "object") {
294
- const packageName = packageJson.value?.name;
295
- if (typeof packageName === "string") {
296
- if (manifest.project.id !== packageName) {
297
- findings.push(
298
- finding(
299
- "docs",
300
- KIT_ERROR_KINDS.IDENTITY_MISMATCH,
301
- "SFC2004",
302
- `project manifest id "${manifest.project.id}" does not match package.json name "${packageName}"`,
303
- { manifestId: manifest.project.id, packageName },
304
- ),
305
- );
306
- }
366
+ const packageDocument = await loadDocument(rootAbs, "package.json");
367
+ if (packageDocument.state === "parse-failed") {
368
+ findings.push(
369
+ finding(
370
+ "docs",
371
+ KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
372
+ "SFC2004",
373
+ "package.json is not valid JSON; documentation identity facts cannot be compared",
374
+ { path: "package.json", documentState: packageDocument.state },
375
+ ),
376
+ );
377
+ }
378
+ const manifestDocument = await loadDocument(rootAbs, PROJECT_MANIFEST_PATH, {
379
+ completeWhen: (value) => typeof value?.project?.id === "string",
380
+ });
381
+ if (manifestDocument.state === "parse-failed") {
382
+ findings.push(
383
+ finding(
384
+ "docs",
385
+ KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
386
+ "SFC2004",
387
+ "project manifest is not valid JSON; documentation identity facts cannot be compared",
388
+ { path: PROJECT_MANIFEST_PATH, documentState: manifestDocument.state },
389
+ ),
390
+ );
391
+ } else if (manifestDocument.state === "incomplete") {
392
+ findings.push(
393
+ finding(
394
+ "docs",
395
+ KIT_ERROR_KINDS.DOCUMENT_INCOMPLETE,
396
+ "SFC2004",
397
+ "project manifest lacks a comparable project.id; documentation identity facts cannot be compared",
398
+ { path: PROJECT_MANIFEST_PATH, documentState: manifestDocument.state },
399
+ ),
400
+ );
401
+ } else if (manifestDocument.state === "ok" && packageDocument.state === "ok") {
402
+ const packageName = packageDocument.value?.name;
403
+ if (typeof packageName === "string" && manifestDocument.value.project.id !== packageName) {
404
+ findings.push(
405
+ finding(
406
+ "docs",
407
+ KIT_ERROR_KINDS.IDENTITY_MISMATCH,
408
+ "SFC2004",
409
+ `project manifest id "${manifestDocument.value.project.id}" does not match package.json name "${packageName}"`,
410
+ { manifestId: manifestDocument.value.project.id, packageName },
411
+ ),
412
+ );
307
413
  }
308
414
  }
309
415
  }
@@ -333,7 +439,7 @@ async function checkGit(rootAbs, findings, allowGitSpawn) {
333
439
  return git;
334
440
  }
335
441
 
336
- async function checkIdentity(rootAbs, docs, findings, profilesRoot) {
442
+ async function checkIdentity(rootAbs, findings, profilesRoot) {
337
443
  // Load identity record
338
444
  const identityRecord = await loadIdentityRecord(rootAbs);
339
445
 
@@ -350,9 +456,6 @@ async function checkIdentity(rootAbs, docs, findings, profilesRoot) {
350
456
  return null;
351
457
  }
352
458
 
353
- // Store for later use
354
- docs["skill-family.identity-record.json"] = identityRecord;
355
-
356
459
  // Validate against profile
357
460
  if (profilesRoot) {
358
461
  const profileValidation = await validateIdentityAgainstProfile(identityRecord, profilesRoot);
@@ -388,9 +491,14 @@ async function checkIdentity(rootAbs, docs, findings, profilesRoot) {
388
491
 
389
492
  /**
390
493
  * Runs all check classes over one target.
391
- * Options: { root, allowGitSpawn, profilesRoot }.
494
+ * Options: { root, allowGitSpawn, only, profilesRoot }.
392
495
  * Returns the report document. Never writes anywhere; throws KitError only
393
496
  * for unusable inputs (an unreadable target).
497
+ *
498
+ * Class results distinguish three facts: selected (included by --only),
499
+ * completed (actually ran to its end), and findings (count). A class that
500
+ * throws mid-flight stays completed=false and contributes a mechanism
501
+ * finding; the report's mechanism flag maps to exit code 2.
394
502
  */
395
503
  export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot } = {}) {
396
504
  if (only !== undefined && !CHECK_CLASSES.includes(only)) {
@@ -398,31 +506,60 @@ export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot
398
506
  }
399
507
  const rootAbs = await resolveTargetRoot(root ?? ".");
400
508
  const findings = [];
401
- const docs = {};
509
+ const selected = new Set(only === undefined ? CHECK_CLASSES : [only]);
510
+ const completed = new Set();
511
+ let mechanismFailure = false;
402
512
 
403
- const classes = only === undefined ? CHECK_CLASSES : CHECK_CLASSES.filter((name) => name === only);
404
- let closureInfo = { digest: null, note: "skipped" };
405
- let git = null;
406
- let versionInfo = { contractsVersion: null };
407
- let identityRecord = null;
408
-
409
- if (classes.includes("contracts")) await checkContracts(rootAbs, docs, findings);
513
+ const runners = {
514
+ contracts: () => checkContracts(rootAbs, findings),
515
+ drift: () => checkDrift(rootAbs, findings),
516
+ closure: () => checkClosure(rootAbs, findings),
517
+ version: () => checkVersion(rootAbs, findings),
518
+ docs: () => checkDocs(rootAbs, findings),
519
+ git: () => checkGit(rootAbs, findings, allowGitSpawn),
520
+ identity: () => checkIdentity(rootAbs, findings, profilesRoot),
521
+ };
410
522
 
411
- let driftLock = null;
412
- if (classes.includes("drift")) {
413
- driftLock = await checkDrift(rootAbs, findings);
414
- }
523
+ const classData = {
524
+ closure: { digest: null, note: "not-selected" },
525
+ git: null,
526
+ version: { contractsVersion: null },
527
+ identity: null,
528
+ };
415
529
 
416
- if (classes.includes("closure")) {
417
- const lock = driftLock ?? (classes.includes("drift") ? null : await loadManagedFileLock(rootAbs));
418
- closureInfo = await checkClosure(rootAbs, lock, findings);
530
+ for (const name of CHECK_CLASSES) {
531
+ if (!selected.has(name)) continue;
532
+ try {
533
+ const result = await runners[name]();
534
+ completed.add(name);
535
+ if (name === "closure") classData.closure = result;
536
+ if (name === "git") classData.git = result;
537
+ if (name === "version") classData.version = result;
538
+ if (name === "identity") {
539
+ classData.identity = result
540
+ ? { record: result, licensing: result.licensing, authors: result.authors }
541
+ : null;
542
+ }
543
+ } catch (cause) {
544
+ // A selected class that could not finish is a mechanism finding, never a
545
+ // silent skip: completed stays false and the report says so.
546
+ mechanismFailure = true;
547
+ const causeKind =
548
+ cause && cause.details && typeof cause.details.kind === "string"
549
+ ? cause.details.kind
550
+ : "unknown";
551
+ findings.push(
552
+ finding(
553
+ name,
554
+ KIT_ERROR_KINDS.CHECK_CLASS_FAILED,
555
+ "SFC2004",
556
+ `check class '${name}' could not complete: ${cause && cause.message ? cause.message : String(cause)}`,
557
+ { causeKind },
558
+ ),
559
+ );
560
+ }
419
561
  }
420
562
 
421
- if (classes.includes("version")) versionInfo = await checkVersion(rootAbs, docs, findings);
422
- if (classes.includes("docs")) await checkDocs(rootAbs, docs, findings);
423
- if (classes.includes("git")) git = await checkGit(rootAbs, findings, allowGitSpawn);
424
- if (classes.includes("identity")) identityRecord = await checkIdentity(rootAbs, docs, findings, profilesRoot);
425
-
426
563
  const byClass = {};
427
564
  for (const item of findings) {
428
565
  byClass[item.class] = (byClass[item.class] ?? 0) + 1;
@@ -446,22 +583,20 @@ export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot
446
583
  generatedBy: { tool: KIT_TOOL_NAME, version: KIT_VERSION },
447
584
  target: { root: ".", entryCount: entries.length },
448
585
  ok: findings.length === 0,
586
+ mechanism: mechanismFailure,
449
587
  classes: CHECK_CLASSES.map((name) => ({
450
588
  name,
451
- ran: classes.includes(name),
589
+ selected: selected.has(name),
590
+ completed: completed.has(name),
452
591
  findings: byClass[name] ?? 0,
453
592
  })),
454
593
  findings,
455
594
  data: {
456
- closure: closureInfo,
457
- git,
458
- version: versionInfo,
595
+ closure: classData.closure,
596
+ git: classData.git,
597
+ version: classData.version,
459
598
  managedDeclarations,
460
- identity: identityRecord ? {
461
- record: identityRecord,
462
- licensing: identityRecord.licensing,
463
- authors: identityRecord.authors,
464
- } : null,
599
+ identity: classData.identity,
465
600
  },
466
601
  policy:
467
602
  "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",