skill-family-engineering-kit 0.1.0 → 0.1.1

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/CONTRIBUTING.md CHANGED
@@ -7,8 +7,18 @@ Git 写操作的授权边界见仓库内文档站的生命周期指南(`docs/g
7
7
  拓扑说明:本工作区仓永久 private,是唯一开发真源;三个 public 仓
8
8
  (`skill-family-contracts`、`skill-family-harness-node`、
9
9
  `skill-family-engineering-kit`)是版本化发布镜像,不承载完整开发历史与
10
- 测试,也不接受直接 PR 或 issue。本指南描述的是私有工作区内经授权的开发
11
- 流程;消费坐标与发布状态唯一见 `docs/status/current.md`。
10
+ 测试。本指南描述的是私有工作区内经授权的开发流程;消费坐标与发布状态
11
+ 唯一见 `docs/status/current.md`。
12
+
13
+ ## Bug 报告与 Issue
14
+
15
+ 普通 bug 报告欢迎通过各 public 发布镜像仓的 GitHub Issues 提交(例如
16
+ `https://github.com/ifoohoo/skill-family-engineering-kit/issues`)。提交
17
+ bug 报告时请尽量提供:受影响包与版本、最小复现步骤、预期与实际行为。
18
+
19
+ **未经事先讨论的 Pull Request 不予接受。** 如果你希望贡献代码,请先
20
+ 通过 Issue 讨论变更的动机与范围,获得维护者确认后再提交 PR。这是为了
21
+ 避免重复劳动、确保变更与本项目的架构裁决和发布合同一致。
12
22
 
13
23
  ## 本地安装
14
24
 
package/LICENSE CHANGED
@@ -186,7 +186,8 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright [yyyy] [name of copyright owner]
189
+ Copyright (c) 2026 skill-family-foundation-workspace contributors
190
+ Copyright (c) 2026 广州市风荷科技有限公司
190
191
 
191
192
  Licensed under the Apache License, Version 2.0 (the "License");
192
193
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -15,11 +15,18 @@
15
15
  ## 使用
16
16
 
17
17
  ```sh
18
- node packages/skill-family-engineering-kit/src/cli.mjs --help
19
- node packages/skill-family-engineering-kit/src/cli.mjs scaffold --root <empty-dir> --project-id my-project
20
- node packages/skill-family-engineering-kit/src/cli.mjs adopt-plan --root <repo>
21
- node packages/skill-family-engineering-kit/src/cli.mjs projection --root <repo>
22
- node packages/skill-family-engineering-kit/src/cli.mjs check --root <repo>
18
+ npx skill-family-kit --help
19
+ npx skill-family-kit scaffold --root <empty-dir> --project-id my-project
20
+ npx skill-family-kit adopt-plan --root <repo>
21
+ npx skill-family-kit projection --root <repo>
22
+ npx skill-family-kit check --root <repo>
23
+ ```
24
+
25
+ 或全局安装后直接使用:
26
+
27
+ ```sh
28
+ npm install -g skill-family-engineering-kit
29
+ skill-family-kit --help
23
30
  ```
24
31
 
25
32
  ## 边界机制
package/SECURITY.md CHANGED
@@ -12,11 +12,15 @@
12
12
  ## 报告漏洞
13
13
 
14
14
  请勿通过公开 issue 直接披露未修复的安全漏洞。private workspace 仓永久
15
- private,不是公开报告渠道;请通过任一 public 发布镜像仓(例如
16
- `https://github.com/ifoohoo/skill-family-engineering-kit`)在 GitHub 上使用
17
- **Private vulnerability reporting**(仓库 Security 页签的
18
- "Report a vulnerability")私下报告;如该通道不可用,再通过仓库维护者
19
- 提供的私下联系方式报告。
15
+ private,不是公开报告渠道;请通过以下渠道私下报告:
16
+
17
+ 1. **首选**:通过任一 public 发布镜像仓(例如
18
+ `https://github.com/ifoohoo/skill-family-engineering-kit`)在 GitHub 上使用
19
+ **Private vulnerability reporting**(仓库 Security 页签的
20
+ "Report a vulnerability");
21
+ 2. **备选**:如上述通道不可用,请通过邮件联系仓库维护者
22
+ (`ifoohoo` GitHub 组织的 `security` 团队,或仓库 `README.md` 中列出的
23
+ 维护者邮箱)。
20
24
 
21
25
  报告时请尽量提供:
22
26
 
@@ -0,0 +1,129 @@
1
+ THIRD-PARTY SOFTWARE NOTICES AND ATTRIBUTION
2
+ =============================================
3
+
4
+ The public documentation site for skill-family-engineering-kit bundles the
5
+ following third-party assets in docs/assets/ (delivered as docs/ in the
6
+ published Pages site). This file provides the required attribution for
7
+ those assets.
8
+
9
+ The project's own source code is licensed under Apache-2.0; see LICENSE.
10
+ The notices below cover only the frontend assets distributed with the
11
+ public documentation site.
12
+
13
+
14
+ 1. MkDocs Material (CSS, JavaScript, favicon)
15
+ ----------------------------------------------
16
+
17
+ Version: 9.7.7
18
+ License: MIT
19
+ Copyright: Copyright (c) 2016-2025 Martin Donath (squidfunk)
20
+ Files:
21
+ - docs/assets/stylesheets/main.*.min.css
22
+ - docs/assets/stylesheets/palette.*.min.css
23
+ - docs/assets/javascripts/bundle.*.min.js
24
+ - docs/assets/javascripts/workers/search.*.min.js
25
+ - docs/assets/images/favicon.png
26
+
27
+ MIT License
28
+
29
+ Copyright (c) 2016-2025 Martin Donath
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining a copy
32
+ of this software and associated documentation files (the "Software"), to
33
+ deal in the Software without restriction, including without limitation the
34
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
35
+ sell copies of the Software, and to permit persons to whom the Software is
36
+ furnished to do so, subject to the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be included in
39
+ all copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
44
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
46
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
47
+ IN THE SOFTWARE.
48
+
49
+
50
+ 2. lunr (search library)
51
+ -------------------------
52
+
53
+ Version: 2.3.9
54
+ License: MIT
55
+ Copyright: Copyright (C) 2013 by Oliver Nightingale
56
+ Files:
57
+ - docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js
58
+ - docs/assets/javascripts/lunr/min/lunr.multi.min.js
59
+
60
+ MIT License
61
+
62
+ Copyright (C) 2013 by Oliver Nightingale
63
+
64
+ Permission is hereby granted, free of charge, to any person obtaining a copy
65
+ of this software and associated documentation files (the "Software"), to deal
66
+ in the Software without restriction, including without limitation the rights
67
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
68
+ copies of the Software, and to permit persons to whom the Software is
69
+ furnished to do so, subject to the following conditions:
70
+
71
+ The above copyright notice and this permission notice shall be included in
72
+ all copies or substantial portions of the Software.
73
+
74
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
75
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
76
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
77
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
78
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
79
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
80
+ IN THE SOFTWARE.
81
+
82
+
83
+ 3. lunr-languages (language stemmers and word cutters)
84
+ -------------------------------------------------------
85
+
86
+ License: MPL-1.1
87
+ Copyright: Mihai Valentin (and individual language stemmer authors)
88
+ Files:
89
+ - docs/assets/javascripts/lunr/min/lunr.ar.min.js
90
+ - docs/assets/javascripts/lunr/min/lunr.da.min.js
91
+ - docs/assets/javascripts/lunr/min/lunr.de.min.js
92
+ - docs/assets/javascripts/lunr/min/lunr.du.min.js
93
+ - docs/assets/javascripts/lunr/min/lunr.el.min.js
94
+ - docs/assets/javascripts/lunr/min/lunr.es.min.js
95
+ - docs/assets/javascripts/lunr/min/lunr.fi.min.js
96
+ - docs/assets/javascripts/lunr/min/lunr.fr.min.js
97
+ - docs/assets/javascripts/lunr/min/lunr.he.min.js
98
+ - docs/assets/javascripts/lunr/min/lunr.hi.min.js
99
+ - docs/assets/javascripts/lunr/min/lunr.hu.min.js
100
+ - docs/assets/javascripts/lunr/min/lunr.hy.min.js
101
+ - docs/assets/javascripts/lunr/min/lunr.it.min.js
102
+ - docs/assets/javascripts/lunr/min/lunr.ja.min.js
103
+ - docs/assets/javascripts/lunr/min/lunr.jp.min.js
104
+ - docs/assets/javascripts/lunr/min/lunr.kn.min.js
105
+ - docs/assets/javascripts/lunr/min/lunr.ko.min.js
106
+ - docs/assets/javascripts/lunr/min/lunr.nl.min.js
107
+ - docs/assets/javascripts/lunr/min/lunr.no.min.js
108
+ - docs/assets/javascripts/lunr/min/lunr.pt.min.js
109
+ - docs/assets/javascripts/lunr/min/lunr.ro.min.js
110
+ - docs/assets/javascripts/lunr/min/lunr.ru.min.js
111
+ - docs/assets/javascripts/lunr/min/lunr.sa.min.js
112
+ - docs/assets/javascripts/lunr/min/lunr.sv.min.js
113
+ - docs/assets/javascripts/lunr/min/lunr.ta.min.js
114
+ - docs/assets/javascripts/lunr/min/lunr.te.min.js
115
+ - docs/assets/javascripts/lunr/min/lunr.th.min.js
116
+ - docs/assets/javascripts/lunr/min/lunr.tr.min.js
117
+ - docs/assets/javascripts/lunr/min/lunr.vi.min.js
118
+ - docs/assets/javascripts/lunr/min/lunr.zh.min.js
119
+ - docs/assets/javascripts/lunr/tinyseg.js
120
+ - docs/assets/javascripts/lunr/wordcut.js
121
+
122
+ These files are licensed under the Mozilla Public License Version 1.1
123
+ (MPL-1.1). The full text of MPL-1.1 is available from the Mozilla project
124
+ at mozilla.org (path: /en-US/MPL/1.1/).
125
+
126
+ MPL-1.1 requires that recipients of a larger work containing MPL-covered
127
+ files be informed of the location of the MPL source. The source for these
128
+ files is the lunr-languages project on GitHub (user: MihaiValentin,
129
+ repo: lunr-languages).
package/package.json CHANGED
@@ -1,42 +1,43 @@
1
1
  {
2
- "name": "skill-family-engineering-kit",
3
- "version": "0.1.0",
4
- "private": false,
5
- "description": "Build-time scaffold, adoption planning, projection, and checks.",
2
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\".",
6
3
  "author": "广州市风荷科技有限公司",
7
- "license": "Apache-2.0",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/ifoohoo/skill-family-engineering-kit.git"
4
+ "bin": {
5
+ "skill-family-kit": "./src/cli.mjs"
11
6
  },
12
- "homepage": "https://github.com/ifoohoo/skill-family-engineering-kit",
13
7
  "bugs": {
14
8
  "url": "https://github.com/ifoohoo/skill-family-engineering-kit/issues"
15
9
  },
16
- "type": "module",
10
+ "dependencies": {
11
+ "skill-family-contracts": "0.1.1",
12
+ "skill-family-harness-node": "0.1.1"
13
+ },
14
+ "description": "Build-time scaffold, adoption planning, projection, and checks.",
17
15
  "engines": {
18
16
  "node": ">=22.22.2 <23"
19
17
  },
20
18
  "exports": "./src/index.mjs",
21
- "dependencies": {
22
- "skill-family-contracts": "0.1.0",
23
- "skill-family-harness-node": "0.1.0"
24
- },
25
- "bin": {
26
- "skill-family-kit": "./src/cli.mjs"
27
- },
28
19
  "files": [
29
20
  "src",
30
21
  "data",
31
22
  "LICENSE",
23
+ "THIRD_PARTY_NOTICES",
32
24
  "SECURITY.md",
33
25
  "CONTRIBUTING.md",
34
26
  "CODE_OF_CONDUCT.md",
35
27
  "docs"
36
28
  ],
37
- "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\".",
29
+ "homepage": "https://github.com/ifoohoo/skill-family-engineering-kit",
30
+ "license": "Apache-2.0",
31
+ "name": "skill-family-engineering-kit",
32
+ "private": false,
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/ifoohoo/skill-family-engineering-kit.git"
36
+ },
37
+ "type": "module",
38
+ "version": "0.1.1",
38
39
  "scripts": {
39
- "check": "node --test",
40
- "test": "node --test"
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) })\""
41
42
  }
42
43
  }
package/src/check.mjs CHANGED
@@ -127,10 +127,19 @@ async function readManagedCandidate(rootAbs, rel, findings) {
127
127
  }
128
128
  }
129
129
 
130
- async function checkDrift(rootAbs, findings) {
130
+ /**
131
+ * Loads and parses the managed-file-lock JSON independently of any check class.
132
+ * Returns the parsed lock object, or null when the lock is absent or unreadable.
133
+ */
134
+ async function loadManagedFileLock(rootAbs) {
131
135
  const loaded = await readOptionalJson(rootAbs, MANAGED_LOCK_PATH);
132
- if (!loaded.ok) return null; // no lock => nothing to drift against
133
- const lock = loaded.value;
136
+ if (!loaded.ok) return null;
137
+ return loaded.value;
138
+ }
139
+
140
+ async function checkDrift(rootAbs, findings) {
141
+ const lock = await loadManagedFileLock(rootAbs);
142
+ if (!lock) return null; // no lock => nothing to drift against
134
143
  const entries = Array.isArray(lock?.entries) ? lock.entries : [];
135
144
  for (const entry of entries) {
136
145
  const rel = typeof entry?.path === "string" ? normalizeRelPath(entry.path) : null;
@@ -210,8 +219,13 @@ async function checkClosure(rootAbs, lock, findings) {
210
219
  }
211
220
  }
212
221
 
213
- async function checkVersion(docs, findings) {
214
- const manifest = docs[PROJECT_MANIFEST_PATH];
222
+ async function checkVersion(rootAbs, docs, findings) {
223
+ let manifest = docs[PROJECT_MANIFEST_PATH];
224
+ if (!manifest) {
225
+ // Load independently when the contracts check didn't run first
226
+ const loaded = await readOptionalJson(rootAbs, PROJECT_MANIFEST_PATH);
227
+ if (loaded.ok) manifest = loaded.value;
228
+ }
215
229
  const declared = manifest?.contracts?.version;
216
230
  if (typeof declared !== "string") return { contractsVersion: null };
217
231
  if (declared !== CONTRACTS_VERSION) {
@@ -360,11 +374,18 @@ export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot
360
374
  let identityRecord = null;
361
375
 
362
376
  if (classes.includes("contracts")) await checkContracts(rootAbs, docs, findings);
377
+
378
+ let driftLock = null;
363
379
  if (classes.includes("drift")) {
364
- const lock = await checkDrift(rootAbs, findings);
365
- if (classes.includes("closure")) closureInfo = await checkClosure(rootAbs, lock, findings);
380
+ driftLock = await checkDrift(rootAbs, findings);
366
381
  }
367
- if (classes.includes("version")) versionInfo = await checkVersion(docs, findings);
382
+
383
+ if (classes.includes("closure")) {
384
+ const lock = driftLock ?? (classes.includes("drift") ? null : await loadManagedFileLock(rootAbs));
385
+ closureInfo = await checkClosure(rootAbs, lock, findings);
386
+ }
387
+
388
+ if (classes.includes("version")) versionInfo = await checkVersion(rootAbs, docs, findings);
368
389
  if (classes.includes("docs")) await checkDocs(rootAbs, docs, findings);
369
390
  if (classes.includes("git")) git = await checkGit(rootAbs, findings, allowGitSpawn);
370
391
  if (classes.includes("identity")) identityRecord = await checkIdentity(rootAbs, docs, findings, profilesRoot);
package/src/cli.mjs CHANGED
@@ -24,7 +24,7 @@ import {
24
24
 
25
25
  const HELP_TEXT = `skill-family-engineering-kit —— 构建期工程工具包(恰好四个顶层命令)
26
26
 
27
- 用法: node src/cli.mjs <command> [options]
27
+ 用法: skill-family-kit <command> [options]
28
28
 
29
29
  命令(四且仅四):
30
30
  scaffold 在空目录生成 Skill Family 项目骨架。
package/src/skeleton.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
1
4
  import { CONTRACTS_VERSION } from "skill-family-contracts";
2
5
  import { digestBytes } from "skill-family-harness-node";
3
6
  import { invalidParamsError, kitError, KIT_ERROR_KINDS } from "./errors.mjs";
@@ -31,7 +34,10 @@ import { MIGRATION_MANIFEST_PATH } from "./migration.mjs";
31
34
  */
32
35
 
33
36
  export const KIT_TOOL_NAME = "skill-family-engineering-kit";
34
- export const KIT_VERSION = "0.0.0";
37
+
38
+ const __dirname = dirname(fileURLToPath(import.meta.url));
39
+ const _kitPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
40
+ export const KIT_VERSION = _kitPkg.version;
35
41
 
36
42
  /** Kit-owned document paths inside a target workspace. */
37
43
  export const PROJECT_MANIFEST_PATH = "skill-family.project-manifest.json";
@@ -143,6 +149,10 @@ function renderRootPackageJson({ projectId, projectName, licensingProfile }) {
143
149
  devDependencies: {
144
150
  projen: SKELETON_PROJEN_VERSION,
145
151
  },
152
+ dependencies: {
153
+ "skill-family-contracts": "^0.1.0",
154
+ "skill-family-harness-node": "^0.1.0",
155
+ },
146
156
  "//": PROJEN_MARKER,
147
157
  });
148
158
  }
@@ -371,21 +381,100 @@ function renderCheckCoreScript() {
371
381
  return [
372
382
  "#!/usr/bin/env node",
373
383
  "// core check: verifies the managed lock binds the real bytes and the",
374
- "// file registry stays a closed world. Dependency-free by design.",
375
- 'import { createHash } from "node:crypto";',
376
- 'import { readFileSync, existsSync } from "node:fs";',
377
- "",
378
- "const lock = JSON.parse(readFileSync(\"skill-family.managed-file-lock.json\", \"utf8\"));",
384
+ "// file registry stays a closed world. Uses Foundation packages for",
385
+ "// path containment, digest computation, and closure verification.",
386
+ 'import { lstat } from "node:fs/promises";',
387
+ 'import { readFileSync } from "node:fs";',
388
+ 'import path from "node:path";',
389
+ 'import { findSchemaByObject } from "skill-family-contracts";',
390
+ 'import {',
391
+ ' classifyPathInput,',
392
+ ' computeResourceClosure,',
393
+ ' digestBytes,',
394
+ ' readFileContained,',
395
+ ' validateContractDocument,',
396
+ '} from "skill-family-harness-node";',
397
+ "",
398
+ "const root = process.cwd();",
379
399
  "const failures = [];",
400
+ "",
401
+ "// Load and validate the managed file lock",
402
+ 'let lockText;',
403
+ 'try {',
404
+ ' lockText = await readFileContained(root, "skill-family.managed-file-lock.json", { encoding: "utf8" });',
405
+ '} catch (cause) {',
406
+ ' console.error(`[check-core] cannot read managed-file-lock: ${cause.message}`);',
407
+ ' process.exit(2);',
408
+ '}',
409
+ "const lock = JSON.parse(lockText);",
410
+ "",
411
+ "// Validate lock schema",
412
+ "const lockSchema = findSchemaByObject(\"managed-file-lock\");",
413
+ "const lockValidation = validateContractDocument(lock, { schemaId: lockSchema.$id });",
414
+ "if (!lockValidation.valid) {",
415
+ ' console.error(`[check-core] managed-file-lock schema invalid: ${JSON.stringify(lockValidation.errors)}`);',
416
+ " process.exit(2);",
417
+ "}",
418
+ "",
419
+ "// Verify each lock entry: lstat for type check, readFileContained for safe read, digestBytes for hash",
380
420
  "for (const entry of lock.entries) {",
381
- " if (!existsSync(entry.path)) {",
382
- " failures.push(`managed file missing: ${entry.path}`);",
421
+ " const relPath = entry.path;",
422
+ "",
423
+ " // Classify the path input first",
424
+ " const classification = classifyPathInput(relPath);",
425
+ " if (!classification.ok) {",
426
+ " failures.push(`lock entry has invalid path: ${relPath} (kind: ${classification.kind})`);",
427
+ " continue;",
428
+ " }",
429
+ "",
430
+ " // lstat to detect symlinks and special files",
431
+ " let stat;",
432
+ " try {",
433
+ " stat = await lstat(path.resolve(root, relPath));",
434
+ " } catch {",
435
+ " failures.push(`managed file missing: ${relPath}`);",
436
+ " continue;",
437
+ " }",
438
+ "",
439
+ " // Reject symlinks",
440
+ " if (stat.isSymbolicLink()) {",
441
+ " failures.push(`managed file is a symlink: ${relPath}`);",
383
442
  " continue;",
384
443
  " }",
385
- " const actual = createHash(\"sha256\").update(readFileSync(entry.path)).digest(\"hex\");",
386
- " if (actual !== entry.hash.value) failures.push(`managed file drifted: ${entry.path}`);",
444
+ "",
445
+ " // Reject special files (FIFO, socket, device, etc.)",
446
+ " if (!stat.isFile()) {",
447
+ " failures.push(`managed file is not a regular file: ${relPath}`);",
448
+ " continue;",
449
+ " }",
450
+ "",
451
+ " // Read through Foundation's contained read",
452
+ " let content;",
453
+ " try {",
454
+ " content = await readFileContained(root, relPath);",
455
+ " } catch (cause) {",
456
+ " failures.push(`cannot read managed file: ${relPath} (${cause.message})`);",
457
+ " continue;",
458
+ " }",
459
+ "",
460
+ " // Compare digest",
461
+ " const actual = digestBytes(content);",
462
+ " if (actual !== entry.hash.value) {",
463
+ " failures.push(`managed file drifted: ${relPath}`);",
464
+ " }",
387
465
  "}",
388
- "const registry = JSON.parse(readFileSync(\".foundation/file-registry.json\", \"utf8\"));",
466
+ "",
467
+ "// Load and validate the file registry",
468
+ 'let registryText;',
469
+ 'try {',
470
+ ' registryText = await readFileContained(root, ".foundation/file-registry.json", { encoding: "utf8" });',
471
+ '} catch (cause) {',
472
+ ' console.error(`[check-core] cannot read file-registry: ${cause.message}`);',
473
+ ' process.exit(2);',
474
+ '}',
475
+ "const registry = JSON.parse(registryText);",
476
+ "",
477
+ "// Check for duplicate classifications",
389
478
  "const seen = new Map();",
390
479
  "for (const [className, files] of [",
391
480
  ' ["projenManaged", registry.classes.projenManaged.files],',
@@ -393,10 +482,33 @@ function renderCheckCoreScript() {
393
482
  ' ["handwritten", registry.classes.handwritten.entries],',
394
483
  "]) {",
395
484
  " for (const file of files) {",
396
- ' if (seen.has(file)) failures.push(`file classified twice: ${file} (${seen.get(file)} and ${className})`);',
485
+ " if (seen.has(file)) {",
486
+ " failures.push(`file classified twice: ${file} (${seen.get(file)} and ${className})`);",
487
+ " }",
397
488
  " seen.set(file, className);",
398
489
  " }",
399
490
  "}",
491
+ "",
492
+ "// Use Foundation's closure computation to verify closed-world",
493
+ "// Skip if there are already failures (to avoid hanging on special files like FIFOs)",
494
+ "if (failures.length === 0) {",
495
+ " const resources = [",
496
+ ' { path: "skill-family.managed-file-lock.json", role: "input" },',
497
+ " ...lock.entries.map((entry) => ({ path: entry.path, role: \"output\" })),",
498
+ " ];",
499
+ " try {",
500
+ " const closure = await computeResourceClosure({ root, resources });",
501
+ " const missingOutputs = closure.resources.filter(",
502
+ " (r) => r.role === \"output\" && !r.exists,",
503
+ " );",
504
+ " for (const missing of missingOutputs) {",
505
+ " failures.push(`closure output missing: ${missing.path}`);",
506
+ " }",
507
+ " } catch (cause) {",
508
+ " failures.push(`closure computation failed: ${cause.message}`);",
509
+ " }",
510
+ "}",
511
+ "",
400
512
  "if (failures.length > 0) {",
401
513
  ' for (const failure of failures) console.error(`[check-core] ${failure}`);',
402
514
  " process.exit(2);",
@@ -434,31 +546,51 @@ function renderSecurityNegativeScript() {
434
546
  return [
435
547
  "#!/usr/bin/env node",
436
548
  "// security negative cases: every hostile path MUST be rejected by the",
437
- "// containment classifier below; if any passes, this script exits 2.",
438
- "",
439
- "function classify(relPath) {",
440
- ' if (typeof relPath !== "string" || relPath.length === 0) return "reject";',
441
- ' if (relPath.includes("\\\\") || relPath.includes("..")) return "reject";',
442
- ' if (relPath.startsWith("/") || /^[A-Za-z]:/.test(relPath)) return "reject";',
443
- ' if (relPath.startsWith("file:") || relPath.startsWith("http")) return "reject";',
444
- ' return "accept";',
445
- "}",
549
+ "// Foundation path containment mechanisms; if any passes, this script exits 2.",
550
+ "// Tests both lexical classification (classifyPathInput) and resolution-time",
551
+ "// containment (resolveContained).",
552
+ 'import { classifyPathInput, resolveContained } from "skill-family-harness-node";',
553
+ "",
554
+ "// Paths that should be rejected at the lexical classification level",
555
+ "const lexicalNegatives = [",
556
+ ' "/etc/passwd", // absolute path',
557
+ ' "C:\\\\Windows\\\\system32", // Windows drive path',
558
+ ' "\\\\\\\\server\\\\share", // UNC path',
559
+ ' "", // empty path',
560
+ "];",
446
561
  "",
447
- "const negatives = [",
448
- ' "../outside.txt",',
449
- ' "/etc/passwd",',
450
- ' "C:\\\\Windows\\\\system32",',
451
- ' "\\\\\\\\server\\\\share",',
452
- ' "a/../../b",',
453
- ' "file:///etc/passwd",',
454
- ' "",',
562
+ "// Paths that should be rejected at the resolution level (path traversal)",
563
+ "const resolutionNegatives = [",
564
+ ' "../outside.txt", // parent traversal',
565
+ ' "a/../../b", // nested traversal',
455
566
  "];",
456
- "const failures = negatives.filter((candidate) => classify(candidate) !== \"reject\");",
567
+ "",
568
+ "const failures = [];",
569
+ "",
570
+ "// Test lexical classification",
571
+ "for (const candidate of lexicalNegatives) {",
572
+ " const result = classifyPathInput(candidate);",
573
+ " if (result.ok) {",
574
+ " failures.push(`lexical: ${candidate}`);",
575
+ " }",
576
+ "}",
577
+ "",
578
+ "// Test resolution-time containment",
579
+ 'const fakeRoot = "/nonexistent-root-for-testing";',
580
+ "for (const candidate of resolutionNegatives) {",
581
+ " try {",
582
+ " await resolveContained(fakeRoot, candidate);",
583
+ " failures.push(`resolution: ${candidate}`);",
584
+ " } catch {",
585
+ " // Expected: resolution should fail",
586
+ " }",
587
+ "}",
588
+ "",
457
589
  "if (failures.length > 0) {",
458
590
  " console.error(`[security-negative] hostile path accepted: ${failures.join(\", \")}`);",
459
591
  " process.exit(2);",
460
592
  "}",
461
- 'console.log(`[security-negative] OK (${negatives.length} hostile paths rejected)`);',
593
+ 'console.log(`[security-negative] OK (${lexicalNegatives.length + resolutionNegatives.length} hostile paths rejected)`);',
462
594
  "",
463
595
  ].join("\n");
464
596
  }