skill-family-engineering-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +131 -0
- package/CONTRIBUTING.md +69 -0
- package/LICENSE +201 -0
- package/README.md +74 -0
- package/SECURITY.md +34 -0
- package/data/licensing/registry.json +211 -0
- package/data/licensing/schema.json +207 -0
- package/docs/.nojekyll +0 -0
- package/docs/404.html +613 -0
- package/docs/architecture/index.html +904 -0
- package/docs/assets/images/favicon.png +0 -0
- package/docs/assets/javascripts/bundle.d7400e89.min.js +16 -0
- package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- package/docs/assets/javascripts/lunr/tinyseg.js +206 -0
- package/docs/assets/javascripts/lunr/wordcut.js +6708 -0
- package/docs/assets/javascripts/workers/search.2c215733.min.js +42 -0
- package/docs/assets/stylesheets/main.ec1eaa64.min.css +1 -0
- package/docs/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
- package/docs/git-lifecycle/index.html +976 -0
- package/docs/help/index.html +907 -0
- package/docs/index.html +837 -0
- package/docs/integration/audit/baseline/audit-codes.json +62 -0
- package/docs/integration/audit/failure-evidence/index.html +872 -0
- package/docs/integration/audit/independence/index.html +841 -0
- package/docs/integration/audit/index.html +826 -0
- package/docs/integration/audit/mutation-taxonomy/index.html +1022 -0
- package/docs/integration/audit/schemas/audit-evidence.schema.json +182 -0
- package/docs/integration/audit/version-compatibility/index.html +845 -0
- package/docs/migration/index.html +1213 -0
- package/docs/quickstart/index.html +842 -0
- package/docs/search/search_index.json +1 -0
- package/docs/setup/index.html +941 -0
- package/docs/sitemap.xml +51 -0
- package/package.json +42 -0
- package/src/adopt-plan.mjs +594 -0
- package/src/check.mjs +415 -0
- package/src/cli.mjs +230 -0
- package/src/errors.mjs +144 -0
- package/src/gitprobe.mjs +230 -0
- package/src/identity-check.mjs +563 -0
- package/src/index.mjs +233 -0
- package/src/license-texts/Apache-2.0.txt +201 -0
- package/src/license-texts/MIT.txt +21 -0
- package/src/licensing.mjs +299 -0
- package/src/migration.mjs +427 -0
- package/src/projection.mjs +397 -0
- package/src/scaffold.mjs +178 -0
- package/src/skeleton.mjs +886 -0
- package/src/workspace.mjs +236 -0
package/src/skeleton.mjs
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
import { CONTRACTS_VERSION } from "skill-family-contracts";
|
|
2
|
+
import { digestBytes } from "skill-family-harness-node";
|
|
3
|
+
import { invalidParamsError, kitError, KIT_ERROR_KINDS } from "./errors.mjs";
|
|
4
|
+
import {
|
|
5
|
+
generateIdentityRecord,
|
|
6
|
+
generateLicenseContent,
|
|
7
|
+
generateNoticeContent,
|
|
8
|
+
loadLicensingProfile,
|
|
9
|
+
} from "./licensing.mjs";
|
|
10
|
+
import { MIGRATION_MANIFEST_PATH } from "./migration.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The planned skeleton of one Skill Family project.
|
|
14
|
+
*
|
|
15
|
+
* describeSkeletonFiles is the single source of truth for both scaffold
|
|
16
|
+
* (which writes the files) and adopt-plan (which previews exactly the same
|
|
17
|
+
* bytes). The two commands cannot diverge because they consume the same
|
|
18
|
+
* pure function; the plan is the action and the action is the plan.
|
|
19
|
+
*
|
|
20
|
+
* The function is deterministic: equal inputs produce equal bytes, so
|
|
21
|
+
* adoption plans stay comparable across runs (no timestamps are embedded).
|
|
22
|
+
*
|
|
23
|
+
* The skeleton is a COMPLETE, immediately developable Skill Family
|
|
24
|
+
* workspace (§4.2), not just a few manifests: workspace tooling with exact
|
|
25
|
+
* pinned versions, docs and MkDocs config, docs CI with a hard-disabled
|
|
26
|
+
* Pages gate, file classification with an explicit lockfile tracking
|
|
27
|
+
* decision, minimal Git workflow rules (rules only — the kit never executes
|
|
28
|
+
* Git writes), contract documents, and check entrypoints. The public
|
|
29
|
+
* skeleton and Profile differences are layered: everything Profile-specific
|
|
30
|
+
* flows from the licensing profile data, never from this module.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export const KIT_TOOL_NAME = "skill-family-engineering-kit";
|
|
34
|
+
export const KIT_VERSION = "0.0.0";
|
|
35
|
+
|
|
36
|
+
/** Kit-owned document paths inside a target workspace. */
|
|
37
|
+
export const PROJECT_MANIFEST_PATH = "skill-family.project-manifest.json";
|
|
38
|
+
export const MANAGED_LOCK_PATH = "skill-family.managed-file-lock.json";
|
|
39
|
+
export const PROJECTION_MANIFEST_PATH = "skill-family.projection.json";
|
|
40
|
+
export const IDENTITY_RECORD_PATH = "skill-family.identity-record.json";
|
|
41
|
+
export const RELEASE_INTEGRATION_PATH = "skill-family.release-integration.json";
|
|
42
|
+
export const FILE_REGISTRY_PATH = ".foundation/file-registry.json";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Exact tool versions of the generated workspace. These are engineering
|
|
46
|
+
* facts of the skeleton itself (not per-profile policy): Node major locked
|
|
47
|
+
* to 22, pnpm and projen pinned to exact versions.
|
|
48
|
+
*/
|
|
49
|
+
export const SKELETON_NODE_MAJOR = "22";
|
|
50
|
+
export const SKELETON_PNPM_VERSION = "10.30.0";
|
|
51
|
+
export const SKELETON_PROJEN_VERSION = "0.101.23";
|
|
52
|
+
|
|
53
|
+
const PROJEN_MARKER = '~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".';
|
|
54
|
+
|
|
55
|
+
const KEBAB_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
56
|
+
|
|
57
|
+
/** Validates the kebab-case identifiers used by the skeleton. */
|
|
58
|
+
export function assertKebabId(value, label) {
|
|
59
|
+
if (typeof value !== "string" || !KEBAB_PATTERN.test(value)) {
|
|
60
|
+
throw invalidParamsError(
|
|
61
|
+
`${label} must be a kebab-case identifier (^[a-z][a-z0-9]*(-[a-z0-9]+)*$)`,
|
|
62
|
+
{ value: String(value) },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertProjectName(value) {
|
|
69
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
70
|
+
throw invalidParamsError("project name must be a non-empty string", {
|
|
71
|
+
value: String(value),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalizes scaffold/adopt-plan inputs. Defaults: projectId falls back to
|
|
79
|
+
* the target directory basename when it is a valid kebab id, projectName
|
|
80
|
+
* falls back to projectId, profileId defaults to "generic". A null
|
|
81
|
+
* licensingProfile is resolved at load time to the first variant declared
|
|
82
|
+
* in the licensing registry (the default is a data fact, never a
|
|
83
|
+
* hardcoded profile name).
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeSkeletonInputs({ projectId, projectName, profileId, licensingProfile, licensingVariant, rootBasename }) {
|
|
86
|
+
let resolvedId = projectId;
|
|
87
|
+
if (resolvedId === undefined || resolvedId === null) {
|
|
88
|
+
resolvedId = rootBasename;
|
|
89
|
+
}
|
|
90
|
+
assertKebabId(resolvedId, "project id");
|
|
91
|
+
const name = projectName === undefined || projectName === null ? resolvedId : projectName;
|
|
92
|
+
assertProjectName(name);
|
|
93
|
+
const profile = profileId === undefined || profileId === null ? "generic" : profileId;
|
|
94
|
+
assertKebabId(profile, "profile id");
|
|
95
|
+
const licensing = licensingProfile === undefined || licensingProfile === null ? null : licensingProfile;
|
|
96
|
+
if (licensing !== null) {
|
|
97
|
+
assertKebabId(licensing, "licensing profile");
|
|
98
|
+
}
|
|
99
|
+
if (licensingVariant !== undefined && licensingVariant !== null) {
|
|
100
|
+
assertKebabId(licensingVariant, "licensing variant");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
projectId: resolvedId,
|
|
104
|
+
projectName: name,
|
|
105
|
+
profileId: profile,
|
|
106
|
+
licensingProfile: licensing,
|
|
107
|
+
licensingVariant: licensingVariant ?? null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function jsonString(value) {
|
|
112
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ------------------------------------------------------------------ */
|
|
116
|
+
/* Root workspace tooling (projen-managed in the generated project) */
|
|
117
|
+
/* ------------------------------------------------------------------ */
|
|
118
|
+
|
|
119
|
+
function workspaceName(projectId) {
|
|
120
|
+
return `${projectId}-workspace`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderRootPackageJson({ projectId, projectName, licensingProfile }) {
|
|
124
|
+
return jsonString({
|
|
125
|
+
name: workspaceName(projectId),
|
|
126
|
+
version: "0.0.0",
|
|
127
|
+
private: true,
|
|
128
|
+
description: `Skill Family workspace for project ${projectName}.`,
|
|
129
|
+
author: licensingProfile.identity.displayValue,
|
|
130
|
+
license: licensingProfile.licensing.spdx,
|
|
131
|
+
engines: { node: `>=${SKELETON_NODE_MAJOR} <${Number(SKELETON_NODE_MAJOR) + 1}` },
|
|
132
|
+
packageManager: `pnpm@${SKELETON_PNPM_VERSION}`,
|
|
133
|
+
scripts: {
|
|
134
|
+
// Direct execution of the projenrc: the projen CLI needs .projen/tasks.json
|
|
135
|
+
// to discover the default task, but a fresh skeleton has no .projen/ yet.
|
|
136
|
+
// `node .projenrc.js` works for the first synth and every later one.
|
|
137
|
+
synth: "node .projenrc.js",
|
|
138
|
+
check:
|
|
139
|
+
"node scripts/check-core.mjs && node scripts/check-docs.mjs && node scripts/security-negative.mjs && pnpm --recursive --if-present run check",
|
|
140
|
+
test: "pnpm --recursive --if-present run test",
|
|
141
|
+
"docs-build": "mkdocs build --strict",
|
|
142
|
+
},
|
|
143
|
+
devDependencies: {
|
|
144
|
+
projen: SKELETON_PROJEN_VERSION,
|
|
145
|
+
},
|
|
146
|
+
"//": PROJEN_MARKER,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderLeafPackageJson({ projectId, licensingProfile }) {
|
|
151
|
+
return jsonString({
|
|
152
|
+
name: projectId,
|
|
153
|
+
version: "0.0.0",
|
|
154
|
+
private: true,
|
|
155
|
+
description: `Skill Family project ${projectId}.`,
|
|
156
|
+
author: licensingProfile.identity.displayValue,
|
|
157
|
+
license: licensingProfile.licensing.spdx,
|
|
158
|
+
type: "module",
|
|
159
|
+
engines: { node: `>=${SKELETON_NODE_MAJOR} <${Number(SKELETON_NODE_MAJOR) + 1}` },
|
|
160
|
+
exports: "./src/index.mjs",
|
|
161
|
+
scripts: {
|
|
162
|
+
check: "node --test",
|
|
163
|
+
test: "node --test",
|
|
164
|
+
},
|
|
165
|
+
"//": PROJEN_MARKER,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The four renderers below must reproduce, byte for byte, what the seeded
|
|
170
|
+
// .projenrc.js synthesizes with projen 0.101.23 (verified empirically):
|
|
171
|
+
// - IgnoreFile: `# marker` + patterns in insertion order, joined with "\n",
|
|
172
|
+
// trailing "\n"; the Project constructor seeds node_modules/ and
|
|
173
|
+
// !/.gitattributes, Tasks seeds the four !/.projen/*.json includes, every
|
|
174
|
+
// committed file seeds its own !/<path> include at construction time.
|
|
175
|
+
// - GitAttributesFile: `# marker`, blank line, sorted attributes, no trailing newline.
|
|
176
|
+
// - TextFile (.npmrc/.node-version): lines joined with "\n", no marker, no
|
|
177
|
+
// trailing newline.
|
|
178
|
+
|
|
179
|
+
function renderPnpmWorkspaceYaml() {
|
|
180
|
+
return [`# ${PROJEN_MARKER}`, "", "packages:", " - packages/*", ""].join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderNpmrc() {
|
|
184
|
+
return ["save-exact=true", "shared-workspace-lockfile=true", "strict-peer-dependencies=true"].join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderNodeVersion() {
|
|
188
|
+
return SKELETON_NODE_MAJOR;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderGitIgnore({ projectId }) {
|
|
192
|
+
// pnpm-lock.yaml is deliberately NOT ignored: a public workspace tracks
|
|
193
|
+
// its lockfile for reproducible installs.
|
|
194
|
+
// site/.qoder/.artifacts are root-only (`/site/`, `/.qoder/`,
|
|
195
|
+
// `/.artifacts/`), identical to the foundation root project: a nested
|
|
196
|
+
// packages/site/** (or any non-root occurrence) must stay tracked.
|
|
197
|
+
return [
|
|
198
|
+
`# ${PROJEN_MARKER}`,
|
|
199
|
+
"!/.gitattributes",
|
|
200
|
+
"!/.projen/tasks.json",
|
|
201
|
+
"!/.projen/deps.json",
|
|
202
|
+
"!/.projen/tree.json",
|
|
203
|
+
"!/.projen/files.json",
|
|
204
|
+
"node_modules/",
|
|
205
|
+
"/site/",
|
|
206
|
+
".cache/",
|
|
207
|
+
"/.qoder/",
|
|
208
|
+
"*.log",
|
|
209
|
+
".DS_Store",
|
|
210
|
+
".env",
|
|
211
|
+
".env.*",
|
|
212
|
+
"!.env.example",
|
|
213
|
+
"/.artifacts/",
|
|
214
|
+
"!/package.json",
|
|
215
|
+
"!/pnpm-workspace.yaml",
|
|
216
|
+
"!/.npmrc",
|
|
217
|
+
"!/.node-version",
|
|
218
|
+
`!/packages/${projectId}/package.json`,
|
|
219
|
+
"",
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderGitAttributes() {
|
|
224
|
+
return [`# ${PROJEN_MARKER}`, "", "* text=auto eol=lf"].join("\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ------------------------------------------------------------------ */
|
|
228
|
+
/* Kit-managed documents and entrypoints */
|
|
229
|
+
/* ------------------------------------------------------------------ */
|
|
230
|
+
|
|
231
|
+
function renderProjectManifest({ projectId, projectName, profileId, managedFiles }) {
|
|
232
|
+
return jsonString({
|
|
233
|
+
schemaVersion: 1,
|
|
234
|
+
kind: "skill-family.project-manifest",
|
|
235
|
+
project: {
|
|
236
|
+
id: workspaceName(projectId),
|
|
237
|
+
name: projectName,
|
|
238
|
+
description: `Skill Family project ${projectName}.`,
|
|
239
|
+
},
|
|
240
|
+
contracts: {
|
|
241
|
+
version: CONTRACTS_VERSION,
|
|
242
|
+
profile: profileId,
|
|
243
|
+
},
|
|
244
|
+
managedFiles,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function renderManagedLock(entries) {
|
|
249
|
+
return jsonString({
|
|
250
|
+
schemaVersion: 1,
|
|
251
|
+
kind: "skill-family.managed-file-lock",
|
|
252
|
+
lockVersion: 1,
|
|
253
|
+
entries,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderMigrationManifest() {
|
|
258
|
+
// Fresh skeleton: nothing has been migrated yet; the state machine starts
|
|
259
|
+
// at "planned" with an empty legacy list (migration manifest contract shape).
|
|
260
|
+
return jsonString({
|
|
261
|
+
schemaVersion: 1,
|
|
262
|
+
kind: "skill-family.migration-manifest",
|
|
263
|
+
legacyInfra: [],
|
|
264
|
+
exceptions: [],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderReleaseIntegration({ projectId }) {
|
|
269
|
+
// Integration boundary placeholder only: the foundation never replicates
|
|
270
|
+
// a release state machine (§4.2).
|
|
271
|
+
return jsonString({
|
|
272
|
+
schemaVersion: 1,
|
|
273
|
+
kind: "skill-family.release-integration",
|
|
274
|
+
project: projectId,
|
|
275
|
+
status: "placeholder",
|
|
276
|
+
boundary:
|
|
277
|
+
"release-skill integrates here when a project decides to publish; no release state machine is replicated in the skeleton",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function renderFileRegistry({ projectId, projenManaged, kitManaged, handwritten }) {
|
|
282
|
+
return jsonString({
|
|
283
|
+
schemaVersion: 1,
|
|
284
|
+
kind: "skill-family.file-registry",
|
|
285
|
+
workspace: workspaceName(projectId),
|
|
286
|
+
generatedBy: { tool: KIT_TOOL_NAME, version: KIT_VERSION },
|
|
287
|
+
classes: {
|
|
288
|
+
projenManaged: {
|
|
289
|
+
files: projenManaged,
|
|
290
|
+
note: "由 projen 生成:只改 .projenrc.js 后运行 pnpm synth。",
|
|
291
|
+
},
|
|
292
|
+
kitManaged: {
|
|
293
|
+
files: kitManaged,
|
|
294
|
+
note: "由 engineering kit 维护的契约与门禁入口;经 scaffold/重新锁定更新。",
|
|
295
|
+
},
|
|
296
|
+
handwritten: {
|
|
297
|
+
entries: handwritten,
|
|
298
|
+
note: "由人维护的源事实;任何工具永不覆盖。",
|
|
299
|
+
},
|
|
300
|
+
artifacts: {
|
|
301
|
+
patterns: ["node_modules/", "/site/", ".cache/", "/.qoder/", "*.log", ".DS_Store", ".env", ".env.*", "/.artifacts/"],
|
|
302
|
+
note: "可再生构建物/缓存/运行证据;经 .gitignore 忽略,绝不跟踪。",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
lockfileDecision: {
|
|
306
|
+
path: "pnpm-lock.yaml",
|
|
307
|
+
tracked: true,
|
|
308
|
+
rationale: "公开 workspace 默认跟踪锁文件以保证可复现安装。",
|
|
309
|
+
},
|
|
310
|
+
rules: {
|
|
311
|
+
closedWorld: "每个拟跟踪文件恰好属于 projenManaged/kitManaged/handwritten 之一;每个忽略项属于 artifacts。",
|
|
312
|
+
gitPolicy: "工具只生成 Git 规则文本,绝不执行任何 Git 写操作。",
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderDocsWorkflow() {
|
|
318
|
+
return [
|
|
319
|
+
"name: docs",
|
|
320
|
+
"on:",
|
|
321
|
+
" push:",
|
|
322
|
+
" branches: [main]",
|
|
323
|
+
" pull_request:",
|
|
324
|
+
"jobs:",
|
|
325
|
+
" docs-build:",
|
|
326
|
+
" runs-on: ubuntu-latest",
|
|
327
|
+
" steps:",
|
|
328
|
+
" - uses: actions/checkout@v4",
|
|
329
|
+
" - uses: actions/setup-python@v5",
|
|
330
|
+
" with:",
|
|
331
|
+
" python-version: '3.12'",
|
|
332
|
+
" - name: Install MkDocs",
|
|
333
|
+
" run: pip install mkdocs-material==9.6.14",
|
|
334
|
+
" - name: Build docs",
|
|
335
|
+
" run: mkdocs build --strict",
|
|
336
|
+
"",
|
|
337
|
+
].join("\n");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function renderDocsDeployWorkflow() {
|
|
341
|
+
// Pages deployment stays HARD-DISABLED (`if: false`). The confirmation
|
|
342
|
+
// value travels through an environment variable, never through shell
|
|
343
|
+
// interpolation (FC-16 pattern).
|
|
344
|
+
return [
|
|
345
|
+
"name: docs-deploy",
|
|
346
|
+
"on:",
|
|
347
|
+
" workflow_dispatch:",
|
|
348
|
+
" inputs:",
|
|
349
|
+
" confirm:",
|
|
350
|
+
" description: 'Type the confirmation value to enable Pages deployment'",
|
|
351
|
+
" required: true",
|
|
352
|
+
"jobs:",
|
|
353
|
+
" deploy-pages:",
|
|
354
|
+
" if: false",
|
|
355
|
+
" runs-on: ubuntu-latest",
|
|
356
|
+
" env:",
|
|
357
|
+
" PAGES_CONFIRM: ${{ inputs.confirm }}",
|
|
358
|
+
" steps:",
|
|
359
|
+
" - name: Refuse until explicitly enabled",
|
|
360
|
+
" run: |",
|
|
361
|
+
' if [ "$PAGES_CONFIRM" != "enable-pages" ]; then',
|
|
362
|
+
" echo 'Pages deployment is disabled; refusing.'",
|
|
363
|
+
" exit 1",
|
|
364
|
+
" fi",
|
|
365
|
+
" echo 'Pages deployment is still gated by if:false and must be reviewed before enabling.'",
|
|
366
|
+
"",
|
|
367
|
+
].join("\n");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function renderCheckCoreScript() {
|
|
371
|
+
return [
|
|
372
|
+
"#!/usr/bin/env node",
|
|
373
|
+
"// 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\"));",
|
|
379
|
+
"const failures = [];",
|
|
380
|
+
"for (const entry of lock.entries) {",
|
|
381
|
+
" if (!existsSync(entry.path)) {",
|
|
382
|
+
" failures.push(`managed file missing: ${entry.path}`);",
|
|
383
|
+
" continue;",
|
|
384
|
+
" }",
|
|
385
|
+
" const actual = createHash(\"sha256\").update(readFileSync(entry.path)).digest(\"hex\");",
|
|
386
|
+
" if (actual !== entry.hash.value) failures.push(`managed file drifted: ${entry.path}`);",
|
|
387
|
+
"}",
|
|
388
|
+
"const registry = JSON.parse(readFileSync(\".foundation/file-registry.json\", \"utf8\"));",
|
|
389
|
+
"const seen = new Map();",
|
|
390
|
+
"for (const [className, files] of [",
|
|
391
|
+
' ["projenManaged", registry.classes.projenManaged.files],',
|
|
392
|
+
' ["kitManaged", registry.classes.kitManaged.files],',
|
|
393
|
+
' ["handwritten", registry.classes.handwritten.entries],',
|
|
394
|
+
"]) {",
|
|
395
|
+
" for (const file of files) {",
|
|
396
|
+
' if (seen.has(file)) failures.push(`file classified twice: ${file} (${seen.get(file)} and ${className})`);',
|
|
397
|
+
" seen.set(file, className);",
|
|
398
|
+
" }",
|
|
399
|
+
"}",
|
|
400
|
+
"if (failures.length > 0) {",
|
|
401
|
+
' for (const failure of failures) console.error(`[check-core] ${failure}`);',
|
|
402
|
+
" process.exit(2);",
|
|
403
|
+
"}",
|
|
404
|
+
'console.log(`[check-core] OK (${lock.entries.length} locked files, closed-world registry)`);',
|
|
405
|
+
"",
|
|
406
|
+
].join("\n");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function renderCheckDocsScript() {
|
|
410
|
+
return [
|
|
411
|
+
"#!/usr/bin/env node",
|
|
412
|
+
"// docs check: README exists and every mkdocs nav page exists on disk.",
|
|
413
|
+
"// Dependency-free by design.",
|
|
414
|
+
'import { readFileSync, existsSync } from "node:fs";',
|
|
415
|
+
"",
|
|
416
|
+
"const failures = [];",
|
|
417
|
+
'if (!existsSync("README.md") || readFileSync("README.md", "utf8").trim() === "") {',
|
|
418
|
+
' failures.push("README.md is missing or empty");',
|
|
419
|
+
"}",
|
|
420
|
+
'const mkdocs = readFileSync("mkdocs.yml", "utf8");',
|
|
421
|
+
'for (const match of mkdocs.matchAll(/\\bdocs\\/[A-Za-z0-9._\\/-]+\\.md\\b/g)) {',
|
|
422
|
+
" if (!existsSync(match[0])) failures.push(`nav page missing: ${match[0]}`);",
|
|
423
|
+
"}",
|
|
424
|
+
"if (failures.length > 0) {",
|
|
425
|
+
' for (const failure of failures) console.error(`[check-docs] ${failure}`);',
|
|
426
|
+
" process.exit(2);",
|
|
427
|
+
"}",
|
|
428
|
+
'console.log("[check-docs] OK");',
|
|
429
|
+
"",
|
|
430
|
+
].join("\n");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function renderSecurityNegativeScript() {
|
|
434
|
+
return [
|
|
435
|
+
"#!/usr/bin/env node",
|
|
436
|
+
"// 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
|
+
"}",
|
|
446
|
+
"",
|
|
447
|
+
"const negatives = [",
|
|
448
|
+
' "../outside.txt",',
|
|
449
|
+
' "/etc/passwd",',
|
|
450
|
+
' "C:\\\\Windows\\\\system32",',
|
|
451
|
+
' "\\\\\\\\server\\\\share",',
|
|
452
|
+
' "a/../../b",',
|
|
453
|
+
' "file:///etc/passwd",',
|
|
454
|
+
' "",',
|
|
455
|
+
"];",
|
|
456
|
+
"const failures = negatives.filter((candidate) => classify(candidate) !== \"reject\");",
|
|
457
|
+
"if (failures.length > 0) {",
|
|
458
|
+
" console.error(`[security-negative] hostile path accepted: ${failures.join(\", \")}`);",
|
|
459
|
+
" process.exit(2);",
|
|
460
|
+
"}",
|
|
461
|
+
'console.log(`[security-negative] OK (${negatives.length} hostile paths rejected)`);',
|
|
462
|
+
"",
|
|
463
|
+
].join("\n");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* ------------------------------------------------------------------ */
|
|
467
|
+
/* Handwritten seeds */
|
|
468
|
+
/* ------------------------------------------------------------------ */
|
|
469
|
+
|
|
470
|
+
function renderReadme({ projectId, projectName, licensingProfile }) {
|
|
471
|
+
return [
|
|
472
|
+
`# ${projectName}`,
|
|
473
|
+
"",
|
|
474
|
+
`Skill Family project \`${projectId}\` (workspace \`${workspaceName(projectId)}\`).`,
|
|
475
|
+
"",
|
|
476
|
+
`Licensed under ${licensingProfile.licensing.spdx} by ${licensingProfile.identity.displayValue}.`,
|
|
477
|
+
"",
|
|
478
|
+
"- 开发:`pnpm install && pnpm synth`",
|
|
479
|
+
"- 验证:`pnpm check`",
|
|
480
|
+
"- 文档:`pnpm run docs-build`",
|
|
481
|
+
"- Git lifecycle guide: [docs/git-lifecycle.md](docs/git-lifecycle.md)",
|
|
482
|
+
"",
|
|
483
|
+
"This README is handwritten source material: the engineering kit never overwrites it.",
|
|
484
|
+
"",
|
|
485
|
+
].join("\n");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function renderAgents({ projectId }) {
|
|
489
|
+
// MCR-020: this file carries ONLY the Git safety baseline and a link to
|
|
490
|
+
// the single complete guide (docs/git-lifecycle.md). No automatic Git
|
|
491
|
+
// actions, no duplicated process.
|
|
492
|
+
return [
|
|
493
|
+
"# Project Agent Rules",
|
|
494
|
+
"",
|
|
495
|
+
`Engineering rules for ${workspaceName(projectId)}. Maintain this file by hand;`,
|
|
496
|
+
"the engineering kit treats it as handwritten source material.",
|
|
497
|
+
"",
|
|
498
|
+
"## Managed files",
|
|
499
|
+
"",
|
|
500
|
+
"- projen-managed files: only edit `.projenrc.js`, then run `pnpm synth`.",
|
|
501
|
+
"- kit-managed files are declared in `skill-family.managed-file-lock.json`.",
|
|
502
|
+
"- File classification lives in `.foundation/file-registry.json` (closed world).",
|
|
503
|
+
"- Run `pnpm check` before handing work over.",
|
|
504
|
+
"",
|
|
505
|
+
"## Git safety baseline (the full process lives in docs/git-lifecycle.md)",
|
|
506
|
+
"",
|
|
507
|
+
"Without an explicit human authorization for the current task, no tool or",
|
|
508
|
+
"automation in this workspace may stage, commit, push, tag, publish, create",
|
|
509
|
+
"or delete remotes, open pull requests, or delete remote branches. A green",
|
|
510
|
+
"`pnpm check` only makes a change eligible to commit — eligible never means",
|
|
511
|
+
"authorized.",
|
|
512
|
+
"",
|
|
513
|
+
"The single complete Git lifecycle guide of this workspace is",
|
|
514
|
+
"`docs/git-lifecycle.md` (branch model, safe commit points, eligibility,",
|
|
515
|
+
"failure recovery, branch exit). Link to that guide; never duplicate the",
|
|
516
|
+
"full process here or in README/help/quickstart.",
|
|
517
|
+
"",
|
|
518
|
+
].join("\n");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function renderProjenrc({ projectId, projectName, licensingProfile }) {
|
|
522
|
+
// Handwritten seed: the generated project's single projen entry. It must
|
|
523
|
+
// reproduce every projen-managed byte of the skeleton, so `pnpm synth`
|
|
524
|
+
// inside the generated workspace is idempotent.
|
|
525
|
+
// The seed must never contain the PROJEN_MARKER literal ("~~ Generated by
|
|
526
|
+
// projen"): projen's legacy cleanup (first synth without .projen/files.json)
|
|
527
|
+
// deletes any file containing that literal, which would remove .projenrc.js
|
|
528
|
+
// itself. The `marker: true` option below injects the identical marker key
|
|
529
|
+
// at synth time instead.
|
|
530
|
+
const rootPkg = JSON.parse(renderRootPackageJson({ projectId, projectName, licensingProfile }));
|
|
531
|
+
const leafPkg = JSON.parse(renderLeafPackageJson({ projectId, licensingProfile }));
|
|
532
|
+
delete rootPkg["//"];
|
|
533
|
+
delete leafPkg["//"];
|
|
534
|
+
return [
|
|
535
|
+
`const { JsonFile, Project, TextFile, YamlFile } = require("projen");`,
|
|
536
|
+
"",
|
|
537
|
+
`const WORKSPACE_NAME = ${JSON.stringify(workspaceName(projectId))};`,
|
|
538
|
+
"",
|
|
539
|
+
"class WorkspaceProject extends Project {",
|
|
540
|
+
" constructor() {",
|
|
541
|
+
" super({ name: WORKSPACE_NAME, commitGenerated: true, projectTree: true });",
|
|
542
|
+
' this.defaultTask?.execArgs(["node", ".projenrc.js"]);',
|
|
543
|
+
' this.addGitIgnore("node_modules/");',
|
|
544
|
+
' this.addGitIgnore("/site/");',
|
|
545
|
+
' this.addGitIgnore(".cache/");',
|
|
546
|
+
' this.addGitIgnore("/.qoder/");',
|
|
547
|
+
' this.addGitIgnore("*.log");',
|
|
548
|
+
' this.addGitIgnore(".DS_Store");',
|
|
549
|
+
' this.addGitIgnore(".env");',
|
|
550
|
+
' this.addGitIgnore(".env.*");',
|
|
551
|
+
' this.addGitIgnore("!.env.example");',
|
|
552
|
+
' this.addGitIgnore("/.artifacts/");',
|
|
553
|
+
` new JsonFile(this, "package.json", { obj: ${JSON.stringify(rootPkg)}, marker: true, readonly: true });`,
|
|
554
|
+
` new YamlFile(this, "pnpm-workspace.yaml", { obj: { packages: ["packages/*"] }, marker: true, readonly: true });`,
|
|
555
|
+
` new TextFile(this, ".npmrc", { lines: ["save-exact=true", "shared-workspace-lockfile=true", "strict-peer-dependencies=true"], marker: true, readonly: true });`,
|
|
556
|
+
` new TextFile(this, ".node-version", { lines: [${JSON.stringify(SKELETON_NODE_MAJOR)}], marker: true, readonly: true });`,
|
|
557
|
+
` new JsonFile(this, "packages/${projectId}/package.json", { obj: ${JSON.stringify(leafPkg)}, marker: true, readonly: true });`,
|
|
558
|
+
" }",
|
|
559
|
+
"}",
|
|
560
|
+
"",
|
|
561
|
+
"new WorkspaceProject().synth();",
|
|
562
|
+
"",
|
|
563
|
+
].join("\n");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function renderMkdocsYml({ projectName }) {
|
|
567
|
+
return [
|
|
568
|
+
`site_name: ${projectName}`,
|
|
569
|
+
"theme:",
|
|
570
|
+
" name: material",
|
|
571
|
+
"nav:",
|
|
572
|
+
" - Home: index.md",
|
|
573
|
+
" - Help: help.md",
|
|
574
|
+
" - Setup: setup.md",
|
|
575
|
+
" - Quickstart: quickstart.md",
|
|
576
|
+
" - Git lifecycle: git-lifecycle.md",
|
|
577
|
+
" - Architecture: architecture.md",
|
|
578
|
+
"",
|
|
579
|
+
].join("\n");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function renderDocPage(title, projectName, body) {
|
|
583
|
+
return [`# ${title}`, "", ...body.map((line) => line.replaceAll("{{PROJECT}}", projectName)), ""].join("\n");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function renderGitLifecycle() {
|
|
587
|
+
// MCR-020: the generated docs/git-lifecycle.md is the SINGLE complete Git
|
|
588
|
+
// lifecycle guide of the scaffolded workspace. AGENTS.md keeps only the
|
|
589
|
+
// safety baseline and a link here; README/help/quickstart link, never
|
|
590
|
+
// duplicate. Eligible (checks green) never means authorized.
|
|
591
|
+
// RCR-010: the four Git write-authorization categories are independent —
|
|
592
|
+
// each needs its own explicit per-task authorization and none of them can
|
|
593
|
+
// be derived from another (commit !=> push/tag/release; push !=>
|
|
594
|
+
// tag/release/remote-branch deletion).
|
|
595
|
+
return [
|
|
596
|
+
"# Git lifecycle",
|
|
597
|
+
"",
|
|
598
|
+
"The single complete Git lifecycle guide of this workspace. AGENTS.md",
|
|
599
|
+
"keeps only the safety baseline and a link here; README/help/quickstart",
|
|
600
|
+
"link to this guide and never duplicate the process.",
|
|
601
|
+
"",
|
|
602
|
+
"## Authorization boundary",
|
|
603
|
+
"",
|
|
604
|
+
"Tools in this workspace never execute Git writes. Without an explicit",
|
|
605
|
+
"human authorization for the current task, nothing may stage, commit,",
|
|
606
|
+
"push, tag, publish, create or delete remotes, open pull requests, or",
|
|
607
|
+
"delete remote branches.",
|
|
608
|
+
"",
|
|
609
|
+
"## Authorizations are independent and never imply each other",
|
|
610
|
+
"",
|
|
611
|
+
"Git write authorizations fall into four separate categories. Each",
|
|
612
|
+
"category requires its own explicit authorization for the current task;",
|
|
613
|
+
"the categories are independent and one never implies another:",
|
|
614
|
+
"",
|
|
615
|
+
"1. stage;",
|
|
616
|
+
"2. commit;",
|
|
617
|
+
"3. push, opening a pull request, deleting a remote branch;",
|
|
618
|
+
"4. tag, release/publish.",
|
|
619
|
+
"",
|
|
620
|
+
"One authorization never derives another: one commit authorization does",
|
|
621
|
+
"not automatically authorize push, tag, or release; one push",
|
|
622
|
+
"authorization does not automatically authorize tag, release, or",
|
|
623
|
+
"deleting a remote branch.",
|
|
624
|
+
"",
|
|
625
|
+
"## Eligible vs authorized",
|
|
626
|
+
"",
|
|
627
|
+
"A change reaches eligible (ready to be committed) when `pnpm check`",
|
|
628
|
+
"exits 0 on the current tree. Eligible never means authorized:",
|
|
629
|
+
"committing an eligible change still requires the explicit per-task",
|
|
630
|
+
"authorization above.",
|
|
631
|
+
"",
|
|
632
|
+
"## Branch model",
|
|
633
|
+
"",
|
|
634
|
+
"- One long-lived `main` branch plus short-lived task branches",
|
|
635
|
+
" (`task/<id>-<slug>`).",
|
|
636
|
+
"- Never push `main` directly and never revert `main` by force-push.",
|
|
637
|
+
"",
|
|
638
|
+
"## Safe commit point",
|
|
639
|
+
"",
|
|
640
|
+
"Commit only an eligible tree — never commit a broken state to `main`.",
|
|
641
|
+
"",
|
|
642
|
+
"## Failure recovery",
|
|
643
|
+
"",
|
|
644
|
+
"On a red build, fix forward on the task branch; if the branch is",
|
|
645
|
+
"unsalvageable, delete it and restart from `main` (each as an explicitly",
|
|
646
|
+
"authorized action).",
|
|
647
|
+
"",
|
|
648
|
+
"## Branch exit",
|
|
649
|
+
"",
|
|
650
|
+
"After a reviewed merge, the task branch may be deleted locally and",
|
|
651
|
+
"remotely — only as explicitly authorized actions, never automated.",
|
|
652
|
+
"",
|
|
653
|
+
].join("\n");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function renderIndexModule({ projectId }) {
|
|
657
|
+
return [
|
|
658
|
+
`// Entry module of ${projectId}. Handwritten source material: the`,
|
|
659
|
+
"// engineering kit never overwrites files under src/.",
|
|
660
|
+
"",
|
|
661
|
+
"export const PROJECT_ID = " + JSON.stringify(projectId) + ";",
|
|
662
|
+
"",
|
|
663
|
+
].join("\n");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function renderLeafTest({ projectId }) {
|
|
667
|
+
return [
|
|
668
|
+
`import test from "node:test";`,
|
|
669
|
+
'import assert from "node:assert/strict";',
|
|
670
|
+
'import { PROJECT_ID } from "../src/index.mjs";',
|
|
671
|
+
"",
|
|
672
|
+
'test("project id is stable", () => {',
|
|
673
|
+
` assert.equal(PROJECT_ID, ${JSON.stringify(projectId)});`,
|
|
674
|
+
"});",
|
|
675
|
+
"",
|
|
676
|
+
].join("\n");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* ------------------------------------------------------------------ */
|
|
680
|
+
/* Skeleton assembly */
|
|
681
|
+
/* ------------------------------------------------------------------ */
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Computes the skeleton files for one project.
|
|
685
|
+
* Returns { files, managedFiles, lockEntries, licensing } where files is the
|
|
686
|
+
* ordered array of { path, fileClass: "managed"|"handwritten", content } and
|
|
687
|
+
* every managed content hash is already bound in the lock entries.
|
|
688
|
+
*/
|
|
689
|
+
export async function describeSkeletonFiles(inputs) {
|
|
690
|
+
// Same-source guarantee: every caller — scaffold, adopt-plan,
|
|
691
|
+
// conformance verifiers — goes through the same intake normalization, so
|
|
692
|
+
// identical options always yield identical bytes.
|
|
693
|
+
const normalized = normalizeSkeletonInputs({
|
|
694
|
+
...inputs,
|
|
695
|
+
rootBasename: inputs.rootBasename ?? inputs.projectId,
|
|
696
|
+
});
|
|
697
|
+
const {
|
|
698
|
+
projectId,
|
|
699
|
+
projectName,
|
|
700
|
+
profileId,
|
|
701
|
+
licensingProfile,
|
|
702
|
+
licensingVariant,
|
|
703
|
+
} = normalized;
|
|
704
|
+
const { licensingProfileData, profilesRoot, identityProjections } = inputs;
|
|
705
|
+
|
|
706
|
+
// Load licensing profile if not provided
|
|
707
|
+
let profileData = licensingProfileData;
|
|
708
|
+
if (!profileData && profilesRoot) {
|
|
709
|
+
profileData = await loadLicensingProfile({
|
|
710
|
+
profilesRoot,
|
|
711
|
+
profileId: licensingProfile,
|
|
712
|
+
variant: licensingVariant ?? undefined,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!profileData) {
|
|
717
|
+
throw kitError(
|
|
718
|
+
KIT_ERROR_KINDS.INVALID_MANIFEST,
|
|
719
|
+
"licensing profile data is required (provide licensingProfileData or profilesRoot)",
|
|
720
|
+
{ licensingProfile },
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const leafDir = `packages/${projectId}`;
|
|
725
|
+
const generator = { tool: KIT_TOOL_NAME, version: KIT_VERSION };
|
|
726
|
+
|
|
727
|
+
const licenseContent = generateLicenseContent(profileData);
|
|
728
|
+
const noticeContent = generateNoticeContent(profileData, { projectName });
|
|
729
|
+
const identityRecord = generateIdentityRecord(profileData, {
|
|
730
|
+
projectId,
|
|
731
|
+
projectName,
|
|
732
|
+
projections: identityProjections,
|
|
733
|
+
});
|
|
734
|
+
const identityRecordContent = jsonString(identityRecord);
|
|
735
|
+
|
|
736
|
+
const handwritten = [
|
|
737
|
+
"README.md",
|
|
738
|
+
"AGENTS.md",
|
|
739
|
+
"LICENSE",
|
|
740
|
+
".projenrc.js",
|
|
741
|
+
"mkdocs.yml",
|
|
742
|
+
"docs/**",
|
|
743
|
+
`${leafDir}/src/**`,
|
|
744
|
+
`${leafDir}/test/**`,
|
|
745
|
+
MIGRATION_MANIFEST_PATH,
|
|
746
|
+
];
|
|
747
|
+
if (noticeContent) handwritten.push("NOTICE");
|
|
748
|
+
|
|
749
|
+
const projenManaged = [
|
|
750
|
+
"package.json",
|
|
751
|
+
"pnpm-workspace.yaml",
|
|
752
|
+
".npmrc",
|
|
753
|
+
".node-version",
|
|
754
|
+
".gitignore",
|
|
755
|
+
`${leafDir}/package.json`,
|
|
756
|
+
];
|
|
757
|
+
const kitManaged = [
|
|
758
|
+
".gitattributes",
|
|
759
|
+
".github/workflows/docs.yml",
|
|
760
|
+
".github/workflows/docs-deploy.yml",
|
|
761
|
+
"scripts/check-core.mjs",
|
|
762
|
+
"scripts/check-docs.mjs",
|
|
763
|
+
"scripts/security-negative.mjs",
|
|
764
|
+
FILE_REGISTRY_PATH,
|
|
765
|
+
PROJECT_MANIFEST_PATH,
|
|
766
|
+
IDENTITY_RECORD_PATH,
|
|
767
|
+
RELEASE_INTEGRATION_PATH,
|
|
768
|
+
MANAGED_LOCK_PATH,
|
|
769
|
+
];
|
|
770
|
+
|
|
771
|
+
const managedFiles = [...projenManaged, ...kitManaged];
|
|
772
|
+
const files = [
|
|
773
|
+
{ path: "README.md", fileClass: "handwritten", content: renderReadme({ projectId, projectName, licensingProfile: profileData }) },
|
|
774
|
+
{ path: "AGENTS.md", fileClass: "handwritten", content: renderAgents({ projectId }) },
|
|
775
|
+
{ path: "LICENSE", fileClass: "handwritten", content: licenseContent },
|
|
776
|
+
{ path: ".gitignore", fileClass: "managed", content: renderGitIgnore({ projectId }) },
|
|
777
|
+
{ path: ".gitattributes", fileClass: "managed", content: renderGitAttributes() },
|
|
778
|
+
{ path: ".npmrc", fileClass: "managed", content: renderNpmrc() },
|
|
779
|
+
{ path: ".node-version", fileClass: "managed", content: renderNodeVersion() },
|
|
780
|
+
{ path: "package.json", fileClass: "managed", content: renderRootPackageJson({ projectId, projectName, licensingProfile: profileData }) },
|
|
781
|
+
{ path: "pnpm-workspace.yaml", fileClass: "managed", content: renderPnpmWorkspaceYaml() },
|
|
782
|
+
{ path: ".projenrc.js", fileClass: "handwritten", content: renderProjenrc({ projectId, projectName, licensingProfile: profileData }) },
|
|
783
|
+
{ path: "mkdocs.yml", fileClass: "handwritten", content: renderMkdocsYml({ projectName }) },
|
|
784
|
+
{
|
|
785
|
+
path: "docs/index.md",
|
|
786
|
+
fileClass: "handwritten",
|
|
787
|
+
content: renderDocPage("Home", projectName, ["{{PROJECT}} 的工程主页。骨架由 engineering kit 生成,本页为手写源事实。"]),
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
path: "docs/help.md",
|
|
791
|
+
fileClass: "handwritten",
|
|
792
|
+
content: renderDocPage("Help", projectName, [
|
|
793
|
+
"遇到问题时先运行 `pnpm check`;门禁输出会给出稳定错误码与类别。",
|
|
794
|
+
"完整 Git lifecycle 指南:[git-lifecycle.md](git-lifecycle.md)。",
|
|
795
|
+
]),
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
path: "docs/setup.md",
|
|
799
|
+
fileClass: "handwritten",
|
|
800
|
+
content: renderDocPage(
|
|
801
|
+
"Setup",
|
|
802
|
+
projectName,
|
|
803
|
+
[
|
|
804
|
+
`需要 Node ${SKELETON_NODE_MAJOR}(major 锁定)与 pnpm ${SKELETON_PNPM_VERSION}(packageManager 字段精确锁定)。`,
|
|
805
|
+
"执行 `pnpm install`,然后 `pnpm synth` 生成受管文件。",
|
|
806
|
+
],
|
|
807
|
+
),
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
path: "docs/quickstart.md",
|
|
811
|
+
fileClass: "handwritten",
|
|
812
|
+
content: renderDocPage(
|
|
813
|
+
"Quickstart",
|
|
814
|
+
projectName,
|
|
815
|
+
[
|
|
816
|
+
"`pnpm install && pnpm synth && pnpm check` 全绿即完成最小验证。",
|
|
817
|
+
"任何 Git 写操作前先阅读完整指南:[git-lifecycle.md](git-lifecycle.md)。",
|
|
818
|
+
],
|
|
819
|
+
),
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
path: "docs/architecture.md",
|
|
823
|
+
fileClass: "handwritten",
|
|
824
|
+
content: renderDocPage(
|
|
825
|
+
"Architecture",
|
|
826
|
+
projectName,
|
|
827
|
+
[
|
|
828
|
+
"本 workspace 分为手写源事实、projen 受管工具文件与 kit 受管契约文件三层。",
|
|
829
|
+
"文件分类的唯一登记是 `.foundation/file-registry.json`(闭世界)。",
|
|
830
|
+
],
|
|
831
|
+
),
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
path: "docs/git-lifecycle.md",
|
|
835
|
+
fileClass: "handwritten",
|
|
836
|
+
content: renderGitLifecycle(),
|
|
837
|
+
},
|
|
838
|
+
{ path: ".github/workflows/docs.yml", fileClass: "managed", content: renderDocsWorkflow() },
|
|
839
|
+
{ path: ".github/workflows/docs-deploy.yml", fileClass: "managed", content: renderDocsDeployWorkflow() },
|
|
840
|
+
{ path: "scripts/check-core.mjs", fileClass: "managed", content: renderCheckCoreScript() },
|
|
841
|
+
{ path: "scripts/check-docs.mjs", fileClass: "managed", content: renderCheckDocsScript() },
|
|
842
|
+
{ path: "scripts/security-negative.mjs", fileClass: "managed", content: renderSecurityNegativeScript() },
|
|
843
|
+
{ path: `${leafDir}/package.json`, fileClass: "managed", content: renderLeafPackageJson({ projectId, licensingProfile: profileData }) },
|
|
844
|
+
{ path: `${leafDir}/src/index.mjs`, fileClass: "handwritten", content: renderIndexModule({ projectId }) },
|
|
845
|
+
{ path: `${leafDir}/test/index.test.mjs`, fileClass: "handwritten", content: renderLeafTest({ projectId }) },
|
|
846
|
+
{
|
|
847
|
+
path: FILE_REGISTRY_PATH,
|
|
848
|
+
fileClass: "managed",
|
|
849
|
+
content: renderFileRegistry({ projectId, projenManaged, kitManaged, handwritten }),
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
path: PROJECT_MANIFEST_PATH,
|
|
853
|
+
fileClass: "managed",
|
|
854
|
+
content: renderProjectManifest({ projectId, projectName, profileId, managedFiles }),
|
|
855
|
+
},
|
|
856
|
+
{ path: IDENTITY_RECORD_PATH, fileClass: "managed", content: identityRecordContent },
|
|
857
|
+
// The migration manifest is seeded by the skeleton but becomes
|
|
858
|
+
// target-held data: the lock never binds it and adoption never
|
|
859
|
+
// overwrites it — its validity is judged by the migration contract.
|
|
860
|
+
{ path: MIGRATION_MANIFEST_PATH, fileClass: "handwritten", content: renderMigrationManifest() },
|
|
861
|
+
{ path: RELEASE_INTEGRATION_PATH, fileClass: "managed", content: renderReleaseIntegration({ projectId }) },
|
|
862
|
+
];
|
|
863
|
+
if (noticeContent) {
|
|
864
|
+
files.push({ path: "NOTICE", fileClass: "handwritten", content: noticeContent });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// The lock binds every managed file except itself (a document cannot
|
|
868
|
+
// contain its own content hash); the manifest still declares the lock
|
|
869
|
+
// path as managed.
|
|
870
|
+
const lockEntries = files
|
|
871
|
+
.filter((file) => file.fileClass === "managed" && file.path !== MANAGED_LOCK_PATH)
|
|
872
|
+
.map((file) => ({
|
|
873
|
+
path: file.path,
|
|
874
|
+
hash: { algorithm: "sha256", value: digestBytes(Buffer.from(file.content, "utf8")) },
|
|
875
|
+
generator,
|
|
876
|
+
policy: "managed",
|
|
877
|
+
}));
|
|
878
|
+
files.push({ path: MANAGED_LOCK_PATH, fileClass: "managed", content: renderManagedLock(lockEntries) });
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
files,
|
|
882
|
+
managedFiles,
|
|
883
|
+
lockEntries,
|
|
884
|
+
licensing: { profile: profileData.profile.id, variant: profileData.profile.variant },
|
|
885
|
+
};
|
|
886
|
+
}
|