skill-family-engineering-kit 0.5.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/CHANGELOG.zh-CN.md +22 -0
  3. package/README.md +12 -10
  4. package/README.zh-CN.md +12 -10
  5. package/package.json +3 -3
  6. package/release-notes/0.6.0.yaml +25 -0
  7. package/src/adopt-plan.mjs +81 -159
  8. package/src/check.mjs +443 -19
  9. package/src/cli.mjs +80 -1
  10. package/src/core-check.mjs +8 -4
  11. package/src/entry-check.mjs +384 -0
  12. package/src/errors.mjs +21 -0
  13. package/src/index.mjs +35 -6
  14. package/src/projection.mjs +221 -12
  15. package/src/relock.mjs +397 -0
  16. package/src/skeleton.mjs +162 -4
  17. package/CODE_OF_CONDUCT.md +0 -131
  18. package/CONTRIBUTING.md +0 -25
  19. package/SECURITY.md +0 -33
  20. package/docs/.nojekyll +0 -0
  21. package/docs/404.html +0 -1666
  22. package/docs/agents/architecture-routing/index.html +0 -1944
  23. package/docs/agents/capability-catalog.en.json +0 -1821
  24. package/docs/agents/capability-catalog.json +0 -1073
  25. package/docs/agents/capability-catalog.schema.json +0 -179
  26. package/docs/agents/capability-catalog.zh-CN.json +0 -1821
  27. package/docs/agents/index.html +0 -1914
  28. package/docs/architecture/index.html +0 -2194
  29. package/docs/assets/images/favicon.png +0 -0
  30. package/docs/assets/javascripts/bundle.d7400e89.min.js +0 -16
  31. package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
  32. package/docs/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
  33. package/docs/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
  34. package/docs/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
  35. package/docs/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
  36. package/docs/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
  37. package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
  38. package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
  39. package/docs/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
  40. package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
  41. package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
  42. package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
  43. package/docs/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
  44. package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
  45. package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
  46. package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
  47. package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
  48. package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
  49. package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
  50. package/docs/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
  51. package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
  52. package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
  53. package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
  54. package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
  55. package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
  56. package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
  57. package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
  58. package/docs/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
  59. package/docs/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
  60. package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
  61. package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
  62. package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
  63. package/docs/assets/javascripts/lunr/tinyseg.js +0 -206
  64. package/docs/assets/javascripts/lunr/wordcut.js +0 -6708
  65. package/docs/assets/javascripts/workers/search.2c215733.min.js +0 -42
  66. package/docs/assets/stylesheets/main.ec1eaa64.min.css +0 -1
  67. package/docs/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
  68. package/docs/en/agents/architecture-routing/index.html +0 -1944
  69. package/docs/en/agents/index.html +0 -1925
  70. package/docs/en/architecture/index.html +0 -2196
  71. package/docs/en/examples-and-fixtures/index.html +0 -1888
  72. package/docs/en/help/index.html +0 -2035
  73. package/docs/en/index.html +0 -1895
  74. package/docs/en/licensing/index.html +0 -1917
  75. package/docs/en/migration/index.html +0 -2335
  76. package/docs/en/quickstart/index.html +0 -1919
  77. package/docs/en/recipes/adapter-text-closure/index.html +0 -2000
  78. package/docs/en/recipes/adopt-existing-repository/index.html +0 -1987
  79. package/docs/en/recipes/deterministic-human-report/index.html +0 -1998
  80. package/docs/en/recipes/domain-schema-validation/index.html +0 -2009
  81. package/docs/en/recipes/durable-local-state/index.html +0 -2009
  82. package/docs/en/recipes/host-profile-integration/index.html +0 -2001
  83. package/docs/en/recipes/index.html +0 -1877
  84. package/docs/en/recipes/safe-filesystem-and-atomic-write/index.html +0 -1994
  85. package/docs/en/reference/api/index.html +0 -1941
  86. package/docs/en/reference/compatibility/index.html +0 -2036
  87. package/docs/en/reference/failure-and-side-effect-matrix/index.html +0 -2216
  88. package/docs/examples-and-fixtures/index.html +0 -1871
  89. package/docs/git-lifecycle/index.html +0 -2029
  90. package/docs/help/index.html +0 -2035
  91. package/docs/index.html +0 -1893
  92. package/docs/integration/audit/baseline/audit-codes.json +0 -62
  93. package/docs/integration/audit/failure-evidence/index.html +0 -1925
  94. package/docs/integration/audit/independence/index.html +0 -1894
  95. package/docs/integration/audit/index.html +0 -1880
  96. package/docs/integration/audit/mutation-taxonomy/index.html +0 -2075
  97. package/docs/integration/audit/schemas/audit-evidence.schema.json +0 -182
  98. package/docs/integration/audit/version-compatibility/index.html +0 -1902
  99. package/docs/licensing/index.html +0 -1917
  100. package/docs/migration/index.html +0 -2335
  101. package/docs/public/status/index.html +0 -1884
  102. package/docs/quickstart/index.html +0 -1919
  103. package/docs/recipes/adapter-text-closure/index.html +0 -2000
  104. package/docs/recipes/adopt-existing-repository/index.html +0 -1987
  105. package/docs/recipes/deterministic-human-report/index.html +0 -1998
  106. package/docs/recipes/domain-schema-validation/index.html +0 -2009
  107. package/docs/recipes/durable-local-state/index.html +0 -2009
  108. package/docs/recipes/host-profile-integration/index.html +0 -2001
  109. package/docs/recipes/index.html +0 -1877
  110. package/docs/recipes/safe-filesystem-and-atomic-write/index.html +0 -1994
  111. package/docs/reference/api/contracts/index.html +0 -2558
  112. package/docs/reference/api/engineering-kit/index.html +0 -2577
  113. package/docs/reference/api/harness/index.html +0 -3029
  114. package/docs/reference/api/index.html +0 -1881
  115. package/docs/reference/compatibility/index.html +0 -2036
  116. package/docs/reference/failure-and-side-effect-matrix/index.html +0 -2216
  117. package/docs/search/search_index.json +0 -1
  118. package/docs/setup/index.html +0 -1995
  119. package/docs/sitemap.xml +0 -207
package/src/cli.mjs CHANGED
@@ -4,6 +4,8 @@ import process from "node:process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { readFileContained, resolveContained } from "skill-family-harness-node";
6
6
  import { bundledProfilesRoot } from "./licensing.mjs";
7
+ import { checkEntriesAction } from "./entry-check.mjs";
8
+ import { relockAction } from "./relock.mjs";
7
9
  import { checkReportAction, renderReportAction } from "./report.mjs";
8
10
  import {
9
11
  COMMAND_SIDE_EFFECTS,
@@ -56,6 +58,13 @@ const HELP_TEXT = `skill-family-engineering-kit —— 构建期工程工具包
56
58
  报告子动作: check report 对一份已渲染报告做分级诊断:
57
59
  硬失败(SFC3001/3002/3003)计为发现(退出码 1);
58
60
  风格告警只报告、绝不阻塞机器正确的报告。
61
+ 入口契约子动作: check entries 运行共享入口契约门禁
62
+ (SFA-ENTRY-003/004/005/007 + SFA-CONTEXT-001/002),
63
+ 读取 skill-family.entry-contract.json 声明;声明缺失是数据、不是发现。
64
+ 受控重锁子动作: check relock 运行受控 relock 事务(SG-36):
65
+ 把新手写文件登记进 .foundation/file-registry.json,并以当前字节
66
+ 重算 skill-family.managed-file-lock.json(单次 fail-closed 事务;
67
+ 只写这两个收容状态文档,任何拒绝都零写入)。
59
68
 
60
69
  全局选项:
61
70
  --root <dir> 目标工作区根目录(默认当前目录)
@@ -119,7 +128,7 @@ function commandHelp(command) {
119
128
  check: [
120
129
  "选项:",
121
130
  " --root <dir> 目标工作区(默认当前目录)",
122
- " --only <class> 只运行一个诊断类: contracts|drift|closure|version|docs|git|identity",
131
+ " --only <class> 只运行一个诊断类: contracts|drift|closure|version|docs|git|identity|boundary|platform",
123
132
  " --profiles-root <dir> 许可证 Profile 根目录",
124
133
  " --no-git-spawn 禁用只读 git status 探测,仅用文件系统事实",
125
134
  "",
@@ -232,6 +241,17 @@ const COMMAND_SPECS = {
232
241
  "--binding": { key: "binding", value: true },
233
242
  },
234
243
  },
244
+ "check entries": {
245
+ flags: {
246
+ "--root": { key: "root", value: true },
247
+ },
248
+ },
249
+ "check relock": {
250
+ flags: {
251
+ "--root": { key: "root", value: true },
252
+ "--files": { key: "files", value: true },
253
+ },
254
+ },
235
255
  "scaffold host-build": {
236
256
  flags: {
237
257
  "--root": { key: "root", value: true },
@@ -342,6 +362,53 @@ async function runReportSubAction(command, rest) {
342
362
  }
343
363
  }
344
364
 
365
+ async function runEntriesSubAction(rest) {
366
+ const specName = "check entries";
367
+ try {
368
+ const options = parseOptions(rest, { command: specName, flags: COMMAND_SPECS[specName].flags });
369
+ if (options.help) {
370
+ process.stdout.write(`${commandHelp("check")}\n`);
371
+ return KIT_EXIT_CODES.ok;
372
+ }
373
+ const { status, output } = await checkEntriesAction({ root: options.root });
374
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
375
+ if (status === "ok") return KIT_EXIT_CODES.ok;
376
+ if (status === "findings") return KIT_EXIT_CODES.findings;
377
+ return KIT_EXIT_CODES.rejected;
378
+ } catch (error) {
379
+ printError(error);
380
+ return KIT_EXIT_CODES.rejected;
381
+ }
382
+ }
383
+
384
+ async function runRelockSubAction(rest) {
385
+ const specName = "check relock";
386
+ try {
387
+ const options = parseOptions(rest, { command: specName, flags: COMMAND_SPECS[specName].flags });
388
+ if (options.help) {
389
+ process.stdout.write(`${commandHelp("check")}\n`);
390
+ return KIT_EXIT_CODES.ok;
391
+ }
392
+ // --files is one comma-separated relative-path list; absent means the
393
+ // auto-discovery mode of the closed-world stage (mirrors runRelock).
394
+ const files =
395
+ options.files === undefined
396
+ ? null
397
+ : options.files
398
+ .split(",")
399
+ .map((rel) => rel.trim())
400
+ .filter((rel) => rel.length > 0);
401
+ const { status, output } = await relockAction({ root: options.root, files });
402
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
403
+ // Relock has no findings state: it either commits the whole transaction
404
+ // or throws (exit 2). Any status other than "ok" is a mechanism error.
405
+ return status === "ok" ? KIT_EXIT_CODES.ok : KIT_EXIT_CODES.rejected;
406
+ } catch (error) {
407
+ printError(error);
408
+ return KIT_EXIT_CODES.rejected;
409
+ }
410
+ }
411
+
345
412
  export async function cliMain(argv) {
346
413
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
347
414
  process.stdout.write(HELP_TEXT);
@@ -359,6 +426,18 @@ export async function cliMain(argv) {
359
426
  if ((command === "projection" || command === "check") && argv[1] === "report") {
360
427
  return runReportSubAction(command, argv.slice(2));
361
428
  }
429
+ // Positional entry-contract sub-action (SG-34): `check entries` runs the
430
+ // shared entry contract gate (SFA-ENTRY-003/004/005/007 + SFA-CONTEXT-001/002).
431
+ // It hangs under the existing check command; no fifth command is created.
432
+ if (command === "check" && argv[1] === "entries") {
433
+ return runEntriesSubAction(argv.slice(2));
434
+ }
435
+ // Positional controlled-relock sub-action (SG-36): `check relock` runs the
436
+ // fail-closed registration + lock-recompute transaction. It hangs under the
437
+ // existing check command, parallel to `check entries`; no fifth command.
438
+ if (command === "check" && argv[1] === "relock") {
439
+ return runRelockSubAction(argv.slice(2));
440
+ }
362
441
  if (command === "scaffold" && argv[1] === "host-build") {
363
442
  return runHostSubAction(command, "host-build", argv.slice(2));
364
443
  }
@@ -5,7 +5,9 @@ import { CHECK_CLASSES, runChecks } from "./check.mjs";
5
5
  import {
6
6
  IDENTITY_RECORD_PATH,
7
7
  MANAGED_LOCK_PATH,
8
+ PLATFORM_SUBSET_DECLARATION_PATH,
8
9
  PROJECT_MANIFEST_PATH,
10
+ PUBLIC_BOUNDARY_DECLARATION_PATH,
9
11
  } from "./skeleton.mjs";
10
12
  import {
11
13
  listTargetEntries,
@@ -23,7 +25,7 @@ import {
23
25
  * ONLY import this function: safe file-tree enumeration, lstat/symlink/FIFO
24
26
  * and special-entry rejection, file-registry classification (managed /
25
27
  * handwritten / artifact closed world), declaration path containment, the
26
- * seven check classes, and the stable finding/exit mapping all live here and
28
+ * nine check classes, and the stable finding/exit mapping all live here and
27
29
  * nowhere else. No generated string may re-derive a recursive walk, an lstat
28
30
  * classification, a glob match, or a local closed-world algorithm.
29
31
  *
@@ -35,7 +37,7 @@ import {
35
37
  * 3. tree enumeration — every symlink and special entry fails closed;
36
38
  * every regular file must belong to the registered closed world
37
39
  * (managed, handwritten, artifact, or a tracked tool lock);
38
- * 4. the seven check classes through runChecks — skipped whenever an
40
+ * 4. the nine check classes through runChecks — skipped whenever an
39
41
  * unsafe path (symlink/special at a managed or class-read path) would
40
42
  * be opened by them: FIFOs must never be read (they block), symlinks
41
43
  * must never be followed.
@@ -68,7 +70,7 @@ export const CORE_CHECK_SECURITY_KINDS = Object.freeze([
68
70
  const CORE_CLASS = "core";
69
71
 
70
72
  /**
71
- * Paths the seven check classes open by name. They take part in the unsafe
73
+ * Paths the nine check classes open by name. They take part in the unsafe
72
74
  * read-set even when they are not declared managed (README.md is handwritten
73
75
  * in the skeleton), so a symlink/FIFO there can never hang or escape a read.
74
76
  */
@@ -79,6 +81,8 @@ const CLASS_READ_PATHS = Object.freeze([
79
81
  MANAGED_LOCK_PATH,
80
82
  IDENTITY_RECORD_PATH,
81
83
  "identity-record.json",
84
+ PUBLIC_BOUNDARY_DECLARATION_PATH,
85
+ PLATFORM_SUBSET_DECLARATION_PATH,
82
86
  ]);
83
87
 
84
88
  /** Lexical containment of a managed declaration; never touches the fs. */
@@ -189,7 +193,7 @@ export async function runCoreCheck({ root, allowGitSpawn = true, profilesRoot }
189
193
  );
190
194
  }
191
195
 
192
- // ---- Stage 4: the seven check classes through the shared entry ---------
196
+ // ---- Stage 4: the nine check classes through the shared entry ---------
193
197
  let classReport = null;
194
198
  if (unsafeReadPaths.size === 0) {
195
199
  classReport = await runChecks({ root: rootAbs, allowGitSpawn, profilesRoot });
@@ -0,0 +1,384 @@
1
+ import {
2
+ readFileContained,
3
+ estimateTokens,
4
+ TOKEN_ESTIMATOR_ID,
5
+ TOKEN_ESTIMATOR_VERSION,
6
+ TOKEN_ESTIMATION_ALGORITHM,
7
+ } from "skill-family-harness-node";
8
+ import {
9
+ consumeTokenEstimateStrict,
10
+ TOKEN_ESTIMATE_CONSUMPTION,
11
+ } from "skill-family-contracts";
12
+ import { KIT_ERROR_KINDS } from "./errors.mjs";
13
+ import { KIT_TOOL_NAME, KIT_VERSION } from "./skeleton.mjs";
14
+ import { readOptionalJson, resolveTargetRoot } from "./workspace.mjs";
15
+
16
+ /**
17
+ * runEntryContractCheck — the shared entry contract gate (SG-34; audit
18
+ * friction F2, satisfaction carrier of PGM-001(c)).
19
+ *
20
+ * Before this sub-action existed, every project hand-wrote roughly 150 lines
21
+ * of equivalent entry-gate logic and owned its correctness alone. The five
22
+ * frozen checks implement exactly the semantics the audit rules define:
23
+ *
24
+ * 1. SFA-ENTRY-003 — every declared entry exists physically in its
25
+ * declared form (v1 freezes the single form skill_md: the physicalRef
26
+ * must be a readable regular file named SKILL.md);
27
+ * 2. SFA-ENTRY-004 — the outward logical name of every entry is the
28
+ * qualified name `<project>:<name>` and logical names are unique;
29
+ * 3. SFA-ENTRY-005 — entries declared as human entries never appear in
30
+ * the method-registration boundary (registeredMethods);
31
+ * 4. SFA-ENTRY-007 — entries flagged readOnlyRequired declare the
32
+ * read_only side-effect class (controlled vocabulary: read_only or
33
+ * transactional);
34
+ * 5. SFA-CONTEXT-001/002 — every entry's SKILL.md token budget is
35
+ * measured with the authoritative deterministic estimator and consumed
36
+ * through the frozen token estimate consumption contract (SG-33):
37
+ * tokens >= hardLimitTokens is a hard-line finding (SFA-CONTEXT-002),
38
+ * tokens >= warnLimitTokens a warn-line finding (SFA-CONTEXT-001).
39
+ * The estimate is never coerced: consumption failure is a mechanism
40
+ * error (fail closed), and degradation never relaxes the thresholds —
41
+ * inside the kit the authoritative estimator is a hard dependency, so
42
+ * no degraded path exists here.
43
+ *
44
+ * The input is one kit-owned declaration document
45
+ * (skill-family.entry-contract.json, kind
46
+ * skill-family.entry-contract-declaration). Absence of the declaration is
47
+ * data, never a finding — whether a project owes an entry contract depends
48
+ * on its entry surface. The gate is diagnosis only: it reads, estimates and
49
+ * reports; it never writes.
50
+ *
51
+ * Report shape: kind skill-family.entry-check-report; exit mapping
52
+ * 0 clean / 1 findings / 2 mechanism (thrown by the caller).
53
+ */
54
+
55
+ export const ENTRY_CONTRACT_DECLARATION_PATH = "skill-family.entry-contract.json";
56
+ export const ENTRY_CONTRACT_DECLARATION_KIND = "skill-family.entry-contract-declaration";
57
+ export const ENTRY_FORMS = Object.freeze(["skill_md"]);
58
+ export const ENTRY_SIDE_EFFECT_CLASSES = Object.freeze(["read_only", "transactional"]);
59
+
60
+ const SEMVER_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/;
61
+ const LOCAL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
62
+
63
+ function finding(kind, code, message, extra) {
64
+ return { class: "entries", kind, code, message, ...(extra ?? {}) };
65
+ }
66
+
67
+ function isPlainObject(value) {
68
+ return value !== null && typeof value === "object" && !Array.isArray(value);
69
+ }
70
+
71
+ /**
72
+ * Runs the five entry contract checks over one target.
73
+ * Options: { root }.
74
+ * Returns the entry-check report document. Never writes anywhere.
75
+ * Throws only for unusable inputs (an unreadable target root) and for
76
+ * fail-closed token consumption failures (mechanism errors, exit 2).
77
+ */
78
+ export async function runEntryContractCheck({ root } = {}) {
79
+ const rootAbs = await resolveTargetRoot(root ?? ".");
80
+ const findings = [];
81
+ const data = {
82
+ declared: false,
83
+ documentState: "missing",
84
+ project: null,
85
+ declaredVersion: null,
86
+ entries: [],
87
+ estimator: {
88
+ source: "foundation-token-estimator",
89
+ id: TOKEN_ESTIMATOR_ID,
90
+ version: TOKEN_ESTIMATOR_VERSION,
91
+ algorithm: TOKEN_ESTIMATION_ALGORITHM,
92
+ consumptionContract: TOKEN_ESTIMATE_CONSUMPTION.kind,
93
+ numericField: TOKEN_ESTIMATE_CONSUMPTION.numericField,
94
+ },
95
+ };
96
+
97
+ const loaded = await readOptionalJson(rootAbs, ENTRY_CONTRACT_DECLARATION_PATH);
98
+ if (loaded.reason === "missing") {
99
+ return entryReport(data, findings);
100
+ }
101
+ data.declared = true;
102
+ if (!loaded.ok) {
103
+ data.documentState = "parse-failed";
104
+ findings.push(
105
+ finding(
106
+ KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
107
+ "SFC2004",
108
+ `${ENTRY_CONTRACT_DECLARATION_PATH} is not valid JSON`,
109
+ { path: ENTRY_CONTRACT_DECLARATION_PATH, documentState: data.documentState },
110
+ ),
111
+ );
112
+ return entryReport(data, findings);
113
+ }
114
+
115
+ const declaration = loaded.value;
116
+ const invalid = (message, extra) =>
117
+ findings.push(
118
+ finding(KIT_ERROR_KINDS.ENTRY_CONTRACT_INVALID, "SFC2004", message, {
119
+ path: ENTRY_CONTRACT_DECLARATION_PATH,
120
+ ...(extra ?? {}),
121
+ }),
122
+ );
123
+
124
+ // ---- template shape -----------------------------------------------------
125
+ data.documentState = "ok";
126
+ if (!isPlainObject(declaration)) {
127
+ invalid("the declaration must be a JSON object");
128
+ return entryReport(data, findings);
129
+ }
130
+ if (declaration.schemaVersion !== 1) {
131
+ invalid(`schemaVersion must be exactly 1 (got ${JSON.stringify(declaration.schemaVersion ?? null)})`, {
132
+ field: "schemaVersion",
133
+ });
134
+ }
135
+ if (declaration.kind !== ENTRY_CONTRACT_DECLARATION_KIND) {
136
+ invalid(`kind must be "${ENTRY_CONTRACT_DECLARATION_KIND}" (got ${JSON.stringify(declaration.kind ?? null)})`, {
137
+ field: "kind",
138
+ });
139
+ }
140
+ if (typeof declaration.project !== "string" || !LOCAL_NAME_PATTERN.test(declaration.project)) {
141
+ invalid("project must be a kebab-case project id", { field: "project" });
142
+ } else {
143
+ data.project = declaration.project;
144
+ }
145
+ if (typeof declaration.declaredVersion !== "string" || !SEMVER_PATTERN.test(declaration.declaredVersion)) {
146
+ invalid("declaredVersion must be a semantic version string X.Y.Z", { field: "declaredVersion" });
147
+ } else {
148
+ data.declaredVersion = declaration.declaredVersion;
149
+ const packageDocument = await readOptionalJson(rootAbs, "package.json");
150
+ if (
151
+ packageDocument.ok &&
152
+ typeof packageDocument.value?.version === "string" &&
153
+ packageDocument.value.version !== declaration.declaredVersion
154
+ ) {
155
+ findings.push(
156
+ finding(
157
+ KIT_ERROR_KINDS.ENTRY_CONTRACT_VERSION_DRIFT,
158
+ "SFC2004",
159
+ `entry contract declaration declares version ${declaration.declaredVersion}; the single version source (package.json) is ${packageDocument.value.version}`,
160
+ {
161
+ path: ENTRY_CONTRACT_DECLARATION_PATH,
162
+ declared: declaration.declaredVersion,
163
+ expected: packageDocument.value.version,
164
+ },
165
+ ),
166
+ );
167
+ }
168
+ }
169
+
170
+ const entries = Array.isArray(declaration.entries) ? declaration.entries : null;
171
+ if (entries === null) {
172
+ invalid("entries must be an array of entry declarations", { field: "entries" });
173
+ return entryReport(data, findings);
174
+ }
175
+ const registeredMethods = Array.isArray(declaration.registeredMethods)
176
+ ? declaration.registeredMethods.filter((item) => typeof item === "string")
177
+ : [];
178
+ if (!Array.isArray(declaration.registeredMethods)) {
179
+ invalid("registeredMethods must be an array of registered method logical names", {
180
+ field: "registeredMethods",
181
+ });
182
+ }
183
+
184
+ const tokenBudget = isPlainObject(declaration.tokenBudget) ? declaration.tokenBudget : null;
185
+ const warnLimitTokens = tokenBudget?.warnLimitTokens;
186
+ const hardLimitTokens = tokenBudget?.hardLimitTokens;
187
+ const budgetUsable =
188
+ Number.isInteger(warnLimitTokens) &&
189
+ warnLimitTokens > 0 &&
190
+ Number.isInteger(hardLimitTokens) &&
191
+ hardLimitTokens > 0;
192
+ if (!budgetUsable) {
193
+ invalid("tokenBudget.warnLimitTokens and tokenBudget.hardLimitTokens must be positive integers", {
194
+ field: "tokenBudget",
195
+ });
196
+ } else if (warnLimitTokens >= hardLimitTokens) {
197
+ invalid("tokenBudget.warnLimitTokens must be strictly below hardLimitTokens", { field: "tokenBudget" });
198
+ }
199
+
200
+ // ---- per-entry checks ----------------------------------------------------
201
+ const seenLogicalNames = new Set();
202
+ for (const [index, entry] of entries.entries()) {
203
+ const at = `entries[${index}]`;
204
+ if (!isPlainObject(entry)) {
205
+ invalid(`${at} must be an object`, { field: at });
206
+ continue;
207
+ }
208
+ const name = typeof entry.name === "string" ? entry.name : null;
209
+ if (name === null || !LOCAL_NAME_PATTERN.test(name)) {
210
+ invalid(`${at}.name must be a kebab-case local entry name`, { field: `${at}.name` });
211
+ continue;
212
+ }
213
+ const expectedLogicalName = data.project ? `${data.project}:${name}` : null;
214
+
215
+ // Check 2 (SFA-ENTRY-004): qualified logical name consistency + uniqueness.
216
+ if (typeof entry.logicalName !== "string" || entry.logicalName.length === 0) {
217
+ invalid(`${at}.logicalName must be a non-empty string`, { field: `${at}.logicalName` });
218
+ } else {
219
+ if (expectedLogicalName !== null && entry.logicalName !== expectedLogicalName) {
220
+ findings.push(
221
+ finding(
222
+ KIT_ERROR_KINDS.ENTRY_NAME_INCONSISTENT,
223
+ "SFC2004",
224
+ `entry '${name}' logical name is ${entry.logicalName}; the qualified name must be ${expectedLogicalName} (SFA-ENTRY-004)`,
225
+ { entry: name, declared: entry.logicalName, expected: expectedLogicalName },
226
+ ),
227
+ );
228
+ }
229
+ if (seenLogicalNames.has(entry.logicalName)) {
230
+ findings.push(
231
+ finding(
232
+ KIT_ERROR_KINDS.ENTRY_NAME_INCONSISTENT,
233
+ "SFC2004",
234
+ `logical name ${entry.logicalName} is declared more than once (SFA-ENTRY-004)`,
235
+ { entry: name, declared: entry.logicalName },
236
+ ),
237
+ );
238
+ }
239
+ seenLogicalNames.add(entry.logicalName);
240
+ }
241
+
242
+ const form = entry.form;
243
+ if (!ENTRY_FORMS.includes(form)) {
244
+ invalid(`${at}.form must be one of: ${ENTRY_FORMS.join(", ")} (got ${JSON.stringify(form ?? null)})`, {
245
+ field: `${at}.form`,
246
+ });
247
+ continue;
248
+ }
249
+ const physicalRef = typeof entry.physicalRef === "string" ? entry.physicalRef : null;
250
+ if (physicalRef === null || physicalRef.length === 0 || physicalRef.startsWith("/") || physicalRef.split("/").includes("..")) {
251
+ invalid(`${at}.physicalRef must be a contained relative path`, { field: `${at}.physicalRef` });
252
+ continue;
253
+ }
254
+ if (!physicalRef.endsWith("/SKILL.md") && physicalRef !== "SKILL.md") {
255
+ invalid(`${at}.physicalRef must name a SKILL.md file for form skill_md`, { field: `${at}.physicalRef` });
256
+ continue;
257
+ }
258
+
259
+ // Check 1 (SFA-ENTRY-003): physical presence in the declared form.
260
+ let text = null;
261
+ try {
262
+ text = await readFileContained(rootAbs, physicalRef, { encoding: "utf8" });
263
+ } catch (cause) {
264
+ const kind = cause && cause.details ? cause.details.kind : null;
265
+ if (kind === "missing-resource") {
266
+ findings.push(
267
+ finding(
268
+ KIT_ERROR_KINDS.ENTRY_PHYSICAL_MISSING,
269
+ "SFC2004",
270
+ `entry '${name}' physical SKILL.md is absent: ${physicalRef} (SFA-ENTRY-003)`,
271
+ { entry: name, path: physicalRef },
272
+ ),
273
+ );
274
+ } else {
275
+ // Escaping or unsafe references are target-supplied data: report, never follow.
276
+ findings.push(
277
+ finding(
278
+ KIT_ERROR_KINDS.ENTRY_PHYSICAL_MISSING,
279
+ "SFC2004",
280
+ `entry '${name}' physicalRef is not safely readable inside the target: ${physicalRef} (kind: ${kind ?? "unknown"})`,
281
+ { entry: name, path: physicalRef, causeKind: kind ?? "unknown" },
282
+ ),
283
+ );
284
+ }
285
+ continue;
286
+ }
287
+
288
+ // Check 3 (SFA-ENTRY-005): human entries stay outside method registration.
289
+ if (entry.humanEntry === true && entry.logicalName && registeredMethods.includes(entry.logicalName)) {
290
+ findings.push(
291
+ finding(
292
+ KIT_ERROR_KINDS.ENTRY_HUMAN_REGISTERED,
293
+ "SFC2004",
294
+ `human entry '${name}' appears in the method-registration boundary: ${entry.logicalName} (SFA-ENTRY-005)`,
295
+ { entry: name, logicalName: entry.logicalName },
296
+ ),
297
+ );
298
+ }
299
+
300
+ // Check 4 (SFA-ENTRY-007): read-only obligation vs declared side-effect class.
301
+ if (!ENTRY_SIDE_EFFECT_CLASSES.includes(entry.sideEffectClass)) {
302
+ invalid(
303
+ `${at}.sideEffectClass must be one of: ${ENTRY_SIDE_EFFECT_CLASSES.join(", ")} (got ${JSON.stringify(entry.sideEffectClass ?? null)})`,
304
+ { field: `${at}.sideEffectClass` },
305
+ );
306
+ } else if (entry.readOnlyRequired === true && entry.sideEffectClass !== "read_only") {
307
+ findings.push(
308
+ finding(
309
+ KIT_ERROR_KINDS.ENTRY_SIDE_EFFECT_VIOLATION,
310
+ "SFC2004",
311
+ `entry '${name}' is obligated read-only but declares side-effect class ${entry.sideEffectClass} (SFA-ENTRY-007)`,
312
+ { entry: name, declared: entry.sideEffectClass, expected: "read_only" },
313
+ ),
314
+ );
315
+ }
316
+
317
+ // Check 5 (SFA-CONTEXT-001/002): token budget via the frozen consumption
318
+ // contract. consumeTokenEstimateStrict throws fail-closed on a
319
+ // non-consumable estimate; the throw propagates as a mechanism error.
320
+ let tokens = null;
321
+ if (budgetUsable && warnLimitTokens < hardLimitTokens) {
322
+ const record = estimateTokens(text);
323
+ tokens = consumeTokenEstimateStrict(record);
324
+ if (tokens >= hardLimitTokens) {
325
+ findings.push(
326
+ finding(
327
+ KIT_ERROR_KINDS.ENTRY_TOKEN_BUDGET_EXCEEDED,
328
+ "SFC2004",
329
+ `entry '${name}' SKILL.md reaches the hard token line (${tokens} >= ${hardLimitTokens}, SFA-CONTEXT-002)`,
330
+ { entry: name, path: physicalRef, tokens, limit: hardLimitTokens, rule: "SFA-CONTEXT-002" },
331
+ ),
332
+ );
333
+ } else if (tokens >= warnLimitTokens) {
334
+ findings.push(
335
+ finding(
336
+ KIT_ERROR_KINDS.ENTRY_TOKEN_BUDGET_EXCEEDED,
337
+ "SFC2004",
338
+ `entry '${name}' SKILL.md reaches the warn token line (${tokens} >= ${warnLimitTokens}, SFA-CONTEXT-001)`,
339
+ { entry: name, path: physicalRef, tokens, limit: warnLimitTokens, rule: "SFA-CONTEXT-001" },
340
+ ),
341
+ );
342
+ }
343
+ }
344
+
345
+ data.entries.push({
346
+ name,
347
+ logicalName: typeof entry.logicalName === "string" ? entry.logicalName : null,
348
+ physicalRef,
349
+ humanEntry: entry.humanEntry === true,
350
+ sideEffectClass: entry.sideEffectClass ?? null,
351
+ tokens,
352
+ });
353
+ }
354
+
355
+ return entryReport(data, findings);
356
+ }
357
+
358
+ function entryReport(data, findings) {
359
+ return {
360
+ kind: "skill-family.entry-check-report",
361
+ schemaVersion: 1,
362
+ generatedBy: { tool: KIT_TOOL_NAME, version: KIT_VERSION },
363
+ target: { root: "." },
364
+ declared: data.declared,
365
+ documentState: data.documentState,
366
+ ok: findings.length === 0,
367
+ exitCode: findings.length === 0 ? 0 : 1,
368
+ findings,
369
+ data,
370
+ policy:
371
+ "entry contract gate is diagnosis only: it reads declarations and SKILL.md bytes, estimates tokens through the authoritative estimator and the frozen consumption contract, and never writes",
372
+ };
373
+ }
374
+
375
+ /**
376
+ * CLI sub-action wrapper (positioned as `check entries`, parallel to
377
+ * `check report`). Returns { status, output } where status maps to the
378
+ * stable kit exit codes: ok -> 0, findings -> 1; mechanism failures throw
379
+ * and the CLI maps them to 2.
380
+ */
381
+ export async function checkEntriesAction({ root } = {}) {
382
+ const output = await runEntryContractCheck({ root });
383
+ return { status: output.ok ? "ok" : "findings", output };
384
+ }
package/src/errors.mjs CHANGED
@@ -52,6 +52,10 @@ export const KIT_ERROR_KINDS = Object.freeze({
52
52
  CHECK_CLASS_FAILED: "check-class-failed",
53
53
  MANAGED_FILE_MISSING: "managed-file-missing",
54
54
  MANAGED_FILE_DRIFT: "managed-file-drift",
55
+ // never-built distinction (SG-37): every well-formed managed entry declared
56
+ // in the lock is absent on disk — the build outputs were never produced.
57
+ // Distinguished from content drift; carries build-first guidance.
58
+ NEVER_BUILT: "never-built",
55
59
  SYMLINK_AT_MANAGED_PATH: "symlink-at-managed-path",
56
60
  NOT_A_REGULAR_FILE: "not-a-regular-file",
57
61
  CLOSURE_INPUT_MISSING: "closure-input-missing",
@@ -67,10 +71,27 @@ export const KIT_ERROR_KINDS = Object.freeze({
67
71
  SYMLINK_ENTRY: "symlink-entry",
68
72
  SPECIAL_ENTRY: "special-entry",
69
73
  UNCONTAINED_DECLARATION: "uncontained-declaration",
74
+ // check extensions (audit remediation C2: SG-13/14, SG-17, SFA-PLAT-002)
75
+ VERSION_AUTHORITY_DUAL: "version-authority-dual",
76
+ VERSION_SINGLE_SOURCE_DRIFT: "version-single-source-drift",
77
+ PUBLIC_BOUNDARY_VIOLATION: "public-boundary-violation",
78
+ PLATFORM_SUBSET_INVALID: "platform-subset-declaration-invalid",
79
+ PLATFORM_SUBSET_VERSION_DRIFT: "platform-subset-version-drift",
80
+ // entry contract gate sub-action (SG-34: SFA-ENTRY-003/004/005/007 + SFA-CONTEXT-001/002)
81
+ ENTRY_CONTRACT_INVALID: "entry-contract-declaration-invalid",
82
+ ENTRY_CONTRACT_VERSION_DRIFT: "entry-contract-version-drift",
83
+ ENTRY_PHYSICAL_MISSING: "entry-physical-missing",
84
+ ENTRY_NAME_INCONSISTENT: "entry-name-inconsistent",
85
+ ENTRY_HUMAN_REGISTERED: "entry-human-registered",
86
+ ENTRY_SIDE_EFFECT_VIOLATION: "entry-side-effect-violation",
87
+ ENTRY_TOKEN_BUDGET_EXCEEDED: "entry-token-budget-exceeded",
70
88
  // report sub-actions (projection report / check report, FND-ADR-005)
71
89
  REPORT_INPUT_MISSING: "report-input-missing",
72
90
  REPORT_PATH_CONFLICT: "report-path-conflict",
73
91
  REPORT_WRITE_FAILED: "report-write-failed",
92
+ // controlled relock channel (SG-36; audit friction F5): `check relock`
93
+ RELOCK_REGISTRATION_REFUSED: "relock-registration-refused",
94
+ RELOCK_LOCK_DRIFT: "relock-lock-drift",
74
95
  // read-only host integration slice
75
96
  HOST_CONTRACT_INVALID: "host-contract-invalid",
76
97
  HOST_PROBE_FAILED: "host-probe-failed",
package/src/index.mjs CHANGED
@@ -1,7 +1,21 @@
1
1
  import { ContractsError } from "skill-family-contracts";
2
- import { buildHandoffDraft, HANDOFF_FIELDS, planAdoption } from "./adopt-plan.mjs";
3
- import { CHECK_CLASSES, DOCUMENT_STATES, runChecks } from "./check.mjs";
2
+ import { buildProfileDraft, planAdoption } from "./adopt-plan.mjs";
3
+ import { CHECK_CLASSES, DOCUMENT_STATES, PLATFORM_SUBSET_DECLARATION_KIND, runChecks } from "./check.mjs";
4
4
  import { CORE_CHECK_SECURITY_KINDS, isContainedDeclaration, runCoreCheck } from "./core-check.mjs";
5
+ import {
6
+ checkEntriesAction,
7
+ ENTRY_CONTRACT_DECLARATION_KIND,
8
+ ENTRY_CONTRACT_DECLARATION_PATH,
9
+ ENTRY_FORMS,
10
+ ENTRY_SIDE_EFFECT_CLASSES,
11
+ runEntryContractCheck,
12
+ } from "./entry-check.mjs";
13
+ import {
14
+ RELOCK_LOCK_PATH_PATTERN,
15
+ RELOCK_REPORT_KIND,
16
+ relockAction,
17
+ runRelock,
18
+ } from "./relock.mjs";
5
19
  import {
6
20
  invalidParamsError,
7
21
  KitError,
@@ -51,7 +65,7 @@ import {
51
65
  listLicensingProfiles,
52
66
  loadLicensingProfile,
53
67
  } from "./licensing.mjs";
54
- import { compileProjectionPlan, runProjection, loadProjectionManifest } from "./projection.mjs";
68
+ import { compileProjectionPlan, PROJECTION_AUTHORITY_BINDING_KINDS, runProjection, loadProjectionManifest } from "./projection.mjs";
55
69
  import { checkReportAction, renderReportAction } from "./report.mjs";
56
70
  import { scaffoldTarget } from "./scaffold.mjs";
57
71
  import {
@@ -70,8 +84,10 @@ import {
70
84
  KIT_VERSION,
71
85
  MANAGED_LOCK_PATH,
72
86
  normalizeSkeletonInputs,
87
+ PLATFORM_SUBSET_DECLARATION_PATH,
73
88
  PROJECT_MANIFEST_PATH,
74
89
  PROJECTION_MANIFEST_PATH,
90
+ PUBLIC_BOUNDARY_DECLARATION_PATH,
75
91
  } from "./skeleton.mjs";
76
92
  import { matchAnyGlob } from "./workspace.mjs";
77
93
 
@@ -126,7 +142,7 @@ export const COMMAND_SIDE_EFFECTS = Object.freeze({
126
142
  check: Object.freeze({
127
143
  summary: "契约/漂移/闭包/版本/文档事实/Git 前置状态诊断。",
128
144
  sideEffect:
129
- "none — diagnosis only: never writes, never auto-fixes; git is probed read-only (one frozen status query at most); the report sub-action (check report) grades one rendered report against its bound machine result and writes nothing",
145
+ "none — diagnosis only: never writes, never auto-fixes; git is probed read-only (one frozen status query at most); the report sub-action (check report) grades one rendered report against its bound machine result and writes nothing; the entries sub-action (check entries) runs the shared entry contract gate (SFA-ENTRY-003/004/005/007 + SFA-CONTEXT-001/002) over skill-family.entry-contract.json and SKILL.md bytes and writes nothing; the relock sub-action (check relock) is the one controlled exception: one fail-closed transaction writing exactly the two contained state documents (.foundation/file-registry.json and skill-family.managed-file-lock.json), with drift validation before the first write and zero writes on any refusal",
130
146
  exitCodes: "0 无发现;1 有发现;2 拒绝/用法/机制错误",
131
147
  }),
132
148
  });
@@ -190,16 +206,27 @@ export {
190
206
  export {
191
207
  scaffoldTarget,
192
208
  planAdoption,
193
- buildHandoffDraft,
194
- HANDOFF_FIELDS,
209
+ buildProfileDraft,
195
210
  compileProjectionPlan,
211
+ PROJECTION_AUTHORITY_BINDING_KINDS,
196
212
  runProjection,
197
213
  loadProjectionManifest,
198
214
  renderReportAction,
199
215
  checkReportAction,
216
+ runEntryContractCheck,
217
+ checkEntriesAction,
218
+ ENTRY_CONTRACT_DECLARATION_PATH,
219
+ ENTRY_CONTRACT_DECLARATION_KIND,
220
+ ENTRY_FORMS,
221
+ ENTRY_SIDE_EFFECT_CLASSES,
222
+ runRelock,
223
+ relockAction,
224
+ RELOCK_LOCK_PATH_PATTERN,
225
+ RELOCK_REPORT_KIND,
200
226
  runChecks,
201
227
  CHECK_CLASSES,
202
228
  DOCUMENT_STATES,
229
+ PLATFORM_SUBSET_DECLARATION_KIND,
203
230
  runCoreCheck,
204
231
  CORE_CHECK_SECURITY_KINDS,
205
232
  isContainedDeclaration,
@@ -217,6 +244,8 @@ export {
217
244
  MANAGED_LOCK_PATH,
218
245
  PROJECTION_MANIFEST_PATH,
219
246
  IDENTITY_RECORD_PATH,
247
+ PUBLIC_BOUNDARY_DECLARATION_PATH,
248
+ PLATFORM_SUBSET_DECLARATION_PATH,
220
249
  matchAnyGlob,
221
250
  describeHost,
222
251
  loadHostRegistry,