create-yss-spec 3.5.4 → 3.5.6

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 (105) hide show
  1. package/README.md +10 -0
  2. package/cli-core.lock.json +130 -0
  3. package/package.json +7 -3
  4. package/resources/upgrade-skill/SKILL.md +26 -0
  5. package/resources/upgrade-skill/protocol.md +40 -0
  6. package/resources/upgrade-skill/references/cli-families.md +14 -0
  7. package/src/cli/help.js +1 -0
  8. package/src/cli/index.js +12 -0
  9. package/src/filesystem/transaction-runner.js +27 -0
  10. package/src/template/asset-runtime.js +9 -1
  11. package/src/template/instance-runtime.js +4 -2
  12. package/src/template/verification-runtime.js +9 -1
  13. package/template/.agents/skills/.strategic-design-skills-manifest.json +2 -2
  14. package/template/.agents/skills/yss-harness-upgrade/SKILL.md +26 -0
  15. package/template/.agents/skills/yss-harness-upgrade/references/cli-families.md +14 -0
  16. package/template/.agents/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
  17. package/template/.agents/skills/yss-product-lifecycle/SKILL.md +15 -19
  18. package/template/.agents/skills/yss-stage-decision/SKILL.md +3 -0
  19. package/template/.agents/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
  20. package/template/.agents/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
  21. package/template/.agents/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
  22. package/template/.codex/skills/yss-harness-upgrade/SKILL.md +26 -0
  23. package/template/.codex/skills/yss-harness-upgrade/references/cli-families.md +14 -0
  24. package/template/.codex/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
  25. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +15 -19
  26. package/template/.codex/skills/yss-stage-decision/SKILL.md +3 -0
  27. package/template/.codex/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
  28. package/template/.codex/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
  29. package/template/.codex/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
  30. package/template/.cursor/skills/yss-harness-upgrade/SKILL.md +26 -0
  31. package/template/.cursor/skills/yss-harness-upgrade/references/cli-families.md +14 -0
  32. package/template/.cursor/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
  33. package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +15 -19
  34. package/template/.cursor/skills/yss-stage-decision/SKILL.md +3 -0
  35. package/template/.cursor/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
  36. package/template/.cursor/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
  37. package/template/.cursor/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
  38. package/template/.pi/skills/yss-harness-upgrade/SKILL.md +26 -0
  39. package/template/.pi/skills/yss-harness-upgrade/references/cli-families.md +14 -0
  40. package/template/.pi/skills/yss-implementation-contract-compiler/references/boundaries.md +1 -0
  41. package/template/.pi/skills/yss-product-lifecycle/SKILL.md +15 -19
  42. package/template/.pi/skills/yss-stage-decision/SKILL.md +3 -0
  43. package/template/.pi/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +8 -4
  44. package/template/.pi/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +8 -4
  45. package/template/.pi/skills/yss-stage-decision/tests/run-scenarios.mjs +13 -1
  46. package/template/.template-spec/agents/yss-skill-registry.yaml +9 -1
  47. package/template/.template-spec/process/contract-reading.md +1 -1
  48. package/template/.template-spec/process/document-writing.md +12 -0
  49. package/template/.template-spec/process/harness-upgrade.md +40 -0
  50. package/template/.template-spec/process/lifecycle-artifact-map.md +2 -2
  51. package/template/.template-spec/process/lifecycle-registry.yaml +3 -1
  52. package/template/.template-spec/process/schemas/approval-record.schema.json +23 -0
  53. package/template/.template-spec/process/stage-tracking.md +2 -2
  54. package/template/.template-spec/process/structured-assets.md +35 -0
  55. package/template/.template-spec/process/templates/lifecycle-checkpoint-template.json +48 -0
  56. package/template/.template-spec/templates/approval-record-template.json +23 -0
  57. package/template/scripts/check-plan-stage-coverage +12 -0
  58. package/template/scripts/contract +6 -0
  59. package/template/scripts/lib/approval-record.mjs +4 -3
  60. package/template/scripts/lib/asset-command.mjs +35 -0
  61. package/template/scripts/lib/asset-migration.mjs +101 -0
  62. package/template/scripts/lib/asset-transactions.mjs +235 -0
  63. package/template/scripts/lib/checkpoint-boundary.mjs +3 -0
  64. package/template/scripts/lib/contract-views.mjs +7 -1
  65. package/template/scripts/lib/json-schema.mjs +43 -4
  66. package/template/scripts/lib/lifecycle-context-query.mjs +7 -0
  67. package/template/scripts/lib/lifecycle-presentation.mjs +77 -0
  68. package/template/scripts/lib/lifecycle-status.mjs +16 -1
  69. package/template/scripts/lib/plan-stage-coverage.mjs +40 -0
  70. package/template/scripts/lib/reading-view-bundle.mjs +1 -0
  71. package/template/scripts/lib/reading-view-markdown.mjs +8 -6
  72. package/template/scripts/lib/stage-tracking-migration.mjs +11 -3
  73. package/template/scripts/lib/structured-assets.mjs +81 -0
  74. package/template/scripts/lib/task-package.mjs +4 -1
  75. package/template/scripts/lifecycle-status +6 -3
  76. package/template/scripts/verify-lifecycle-checkpoint +8 -5
  77. package/template/skills-lock.json +21 -9
  78. package/template.manifest.json +2 -1
  79. package/template.snapshot.json +5 -5
  80. package/vendor/cli-core/build.mjs +302 -0
  81. package/vendor/cli-core/bundle.mjs +198 -0
  82. package/vendor/cli-core/cli.mjs +182 -0
  83. package/vendor/cli-core/command-runner.mjs +107 -0
  84. package/vendor/cli-core/engine.mjs +239 -0
  85. package/vendor/cli-core/family.mjs +28 -0
  86. package/vendor/cli-core/gitignore.mjs +15 -0
  87. package/vendor/cli-core/identity.mjs +274 -0
  88. package/vendor/cli-core/io.mjs +153 -0
  89. package/vendor/cli-core/layout-migration.mjs +64 -0
  90. package/vendor/cli-core/legacy.mjs +19 -0
  91. package/vendor/cli-core/migrate.mjs +237 -0
  92. package/vendor/cli-core/migration-adapter.mjs +19 -0
  93. package/vendor/cli-core/package.json +13 -0
  94. package/vendor/cli-core/scaffold.mjs +117 -0
  95. package/vendor/cli-core/spec-migration-adapter.mjs +83 -0
  96. package/vendor/cli-core/tests/build.test.mjs +40 -0
  97. package/vendor/cli-core/tests/cli.test.mjs +1065 -0
  98. package/vendor/cli-core/tests/migration-version.test.mjs +8 -0
  99. package/vendor/cli-core/tests/package-contract.mjs +89 -0
  100. package/vendor/cli-core/transaction.mjs +529 -0
  101. package/vendor/cli-core/update.mjs +79 -0
  102. package/vendor/cli-core/vendor/NOTICES.md +6 -0
  103. package/vendor/cli-core/vendor/yaml.LICENSE +13 -0
  104. package/vendor/cli-core/vendor/yaml.mjs +6311 -0
  105. package/vendor/cli-core/verification.mjs +131 -0
@@ -0,0 +1,302 @@
1
+ // Build-time only: source revisions are resolved before reading any distribution bytes.
2
+ import * as fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { execFileSync } from "node:child_process";
6
+ import {
7
+ ensure,
8
+ hash,
9
+ json,
10
+ relative,
11
+ governance,
12
+ safe,
13
+ write,
14
+ readJson,
15
+ } from "./io.mjs";
16
+ import { yaml, PROFILE, loadBundle } from "./bundle.mjs";
17
+ import { familyFor } from "./family.mjs";
18
+ const CORE_PATH = ".template-source/cli-core";
19
+ const UPGRADE_SKILL = ".agents/skills/yss-harness-upgrade";
20
+ const UPGRADE_PROTOCOL = ".template-spec/process/harness-upgrade.md";
21
+ function upgradeResources(root) {
22
+ if (!fs.existsSync(path.join(root, UPGRADE_SKILL))) return {};
23
+ const files = {};
24
+ for (const ref of Object.keys(inventory(path.join(root, UPGRADE_SKILL)))) {
25
+ let bytes = fs.readFileSync(path.join(root, UPGRADE_SKILL, ref));
26
+ if (ref === 'SKILL.md') bytes = Buffer.from(bytes.toString().replace('../../../.template-spec/process/harness-upgrade.md', 'protocol.md'));
27
+ files[ref] = bytes;
28
+ }
29
+ files['protocol.md'] = fs.readFileSync(path.join(root, UPGRADE_PROTOCOL));
30
+ return files;
31
+ }
32
+ function archive(source, ref, consume, paths = []) {
33
+ const revision = execFileSync(
34
+ "git",
35
+ ["-C", source, "rev-parse", `${ref === "WORKTREE" ? "HEAD" : ref}^{commit}`],
36
+ { encoding: "utf8" },
37
+ ).trim();
38
+ ensure(/^[a-f0-9]{40}$/.test(revision), "source revision 必须是完整提交");
39
+ if (ref === "WORKTREE") {
40
+ return consume(fs.realpathSync(source), revision, "working-tree");
41
+ }
42
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "harness-source-"));
43
+ try {
44
+ const tar = execFileSync("git", ["-C", source, "archive", revision, ...paths], {
45
+ maxBuffer: 256 * 1024 * 1024,
46
+ });
47
+ execFileSync("tar", ["-xf", "-", "-C", scratch], { input: tar });
48
+ return consume(fs.realpathSync(scratch), revision, "committed");
49
+ } finally {
50
+ fs.rmSync(scratch, { recursive: true, force: true });
51
+ }
52
+ }
53
+ function inventory(root) {
54
+ const files = {};
55
+ function walk(dir) {
56
+ for (const entry of fs
57
+ .readdirSync(path.join(root, dir), { withFileTypes: true })
58
+ .sort((a, b) => a.name.localeCompare(b.name, "en"))) {
59
+ const ref = dir ? `${dir}/${entry.name}` : entry.name;
60
+ if (entry.isDirectory()) walk(ref);
61
+ else {
62
+ ensure(entry.isFile(), `核心不允许链接: ${ref}`);
63
+ files[ref] = {
64
+ digest: hash(fs.readFileSync(path.join(root, ref))),
65
+ mode: fs.statSync(path.join(root, ref)).mode & 0o111 ? 0o755 : 0o644,
66
+ };
67
+ }
68
+ }
69
+ }
70
+ walk("");
71
+ return Object.fromEntries(
72
+ Object.entries(files).sort(([a], [b]) => (a < b ? -1 : 1)),
73
+ );
74
+ }
75
+ export function verifyCore(packageRoot) {
76
+ const lock = readJson(packageRoot, "cli-core.lock.json");
77
+ ensure(
78
+ lock.schemaVersion === 1 &&
79
+ lock.protocolVersion === 1 &&
80
+ ["committed", "working-tree"].includes(lock.sourceState || "committed") &&
81
+ /^[a-f0-9]{40}$/.test(lock.sourceRevision),
82
+ "核心锁格式错误",
83
+ );
84
+ const files = inventory(path.join(packageRoot, "vendor/cli-core"));
85
+ ensure(
86
+ JSON.stringify(files) === JSON.stringify(lock.files) &&
87
+ hash(JSON.stringify(files)) === lock.digest,
88
+ "vendor/cli-core 漂移",
89
+ );
90
+ ensure(
91
+ readJson(packageRoot, "vendor/cli-core/package.json").version ===
92
+ lock.coreVersion,
93
+ "核心版本漂移",
94
+ );
95
+ if (lock.upgradeResources) ensure(JSON.stringify(inventory(path.join(packageRoot,'resources/upgrade-skill'))) === JSON.stringify(lock.upgradeResources), '升级技能资源漂移');
96
+ return lock;
97
+ }
98
+ export function syncCore(source, ref, packageRoot, check = false) {
99
+ const paths = [CORE_PATH];
100
+ for (const candidate of [UPGRADE_SKILL, UPGRADE_PROTOCOL]) {
101
+ if (ref === 'WORKTREE') { if (fs.existsSync(path.join(source,candidate))) paths.push(candidate); }
102
+ else { try { execFileSync('git',['-C',source,'cat-file','-e',`${ref}:${candidate}`],{stdio:'ignore'}); paths.push(candidate); } catch {} }
103
+ }
104
+ return archive(source, ref, (root, revision, sourceState) => {
105
+ const sourceRoot = path.join(root, CORE_PATH),
106
+ files = inventory(sourceRoot),
107
+ coreVersion = readJson(sourceRoot, "package.json").version;
108
+ const resources = upgradeResources(root);
109
+ const resourceInventory = Object.fromEntries(Object.entries(resources).sort(([a],[b])=>a<b?-1:1).map(([ref,bytes])=>[ref,{digest:hash(bytes),mode:0o644}]));
110
+ const lock = {
111
+ schemaVersion: 1,
112
+ sourceRepository:
113
+ "https://github.com/iloveZzz/yss-spec-project-template.git",
114
+ sourceRevision: revision,
115
+ sourceState,
116
+ sourcePath: CORE_PATH,
117
+ coreVersion,
118
+ protocolVersion: 1,
119
+ files,
120
+ digest: hash(JSON.stringify(files)),
121
+ ...(Object.keys(resources).length ? {upgradeResources:resourceInventory} : {}),
122
+ };
123
+ if (check) {
124
+ ensure(
125
+ json(readJson(packageRoot, "cli-core.lock.json")) === json(lock),
126
+ "核心来源锁漂移",
127
+ );
128
+ verifyCore(packageRoot);
129
+ return lock;
130
+ }
131
+ fs.rmSync(path.join(packageRoot, "vendor/cli-core"), {
132
+ recursive: true,
133
+ force: true,
134
+ });
135
+ for (const [ref, entry] of Object.entries(files))
136
+ write(
137
+ packageRoot,
138
+ `vendor/cli-core/${ref}`,
139
+ fs.readFileSync(path.join(sourceRoot, ref)),
140
+ entry.mode,
141
+ );
142
+ write(packageRoot, "cli-core.lock.json", json(lock));
143
+ if (Object.keys(resources).length) {
144
+ fs.rmSync(path.join(packageRoot,'resources/upgrade-skill'),{recursive:true,force:true});
145
+ for (const [ref,bytes] of Object.entries(resources)) write(packageRoot,`resources/upgrade-skill/${ref}`,bytes);
146
+ }
147
+ return lock;
148
+ }, paths);
149
+ }
150
+ function selected(ref, m) {
151
+ const top = ref.split("/")[0],
152
+ prefix = (p, base) => p === base || p.startsWith(base + "/");
153
+ return (
154
+ ((m.allowRootEntries || []).includes(top) ||
155
+ (m.allowRootFiles || []).includes(ref) ||
156
+ (m.allowFiles || []).includes(ref)) &&
157
+ ![
158
+ ...(m.excludeRootEntries || []),
159
+ ...(m.initExcludeRootEntries || []),
160
+ ].includes(top) &&
161
+ ![
162
+ ...(m.excludeRootFiles || []),
163
+ ...(m.initExcludeRootFiles || []),
164
+ ].includes(ref) &&
165
+ ![...(m.excludePaths || []), ...(m.initExcludePaths || [])].some((x) =>
166
+ prefix(ref, x),
167
+ ) &&
168
+ ref !== "scripts/instantiate-harness"
169
+ );
170
+ }
171
+ export function syncTemplate(source, ref, packageRoot, check = false) {
172
+ return archive(source, ref, (root, revision, sourceState) => {
173
+ const profile = yaml(fs.readFileSync(path.join(root, PROFILE))),
174
+ identity = yaml(fs.readFileSync(path.join(root, "yss-project.yaml")));
175
+ ensure(
176
+ identity.schema_version === 1 &&
177
+ identity.repository_mode === "template-source",
178
+ "来源不是 template-source",
179
+ );
180
+ const side = profile.profile_id === 'harness.business-ddd-strategy-handoff' ? 'design' : profile.profile_id?.match(/^harness\.(backend|frontend)-delivery$/)?.[1];
181
+ const family = familyFor(side);
182
+ ensure(readJson(packageRoot,'package.json').name === family.packageName, '来源 profile 与薄包身份不一致');
183
+ let manifestBytes;
184
+ if (side === 'design') {
185
+ const raw = yaml(fs.readFileSync(safe(root,'.template-spec/process/instance-distribution-manifest.yaml')));
186
+ ensure(raw.profile_id === family.profileId && raw.cli_package === family.packageName && raw.template_source === family.templateSource, '战略分发清单身份矛盾');
187
+ const manifest = {profileId:raw.profile_id};
188
+ for (const key of ['allow_root_entries','allow_root_files','allow_files','exclude_root_entries','exclude_root_files','exclude_paths','render_paths','init_exclude_root_entries','init_exclude_root_files','init_exclude_paths','instance_forbidden_paths']) manifest[key.replace(/_([a-z])/g,(_,c)=>c.toUpperCase())]=raw[key] || [];
189
+ manifestBytes = Buffer.from(json(manifest));
190
+ } else {
191
+ ensure(profile.instantiation?.cli_package === family.packageName && profile.instantiation?.metadata_file === family.metadataFile && profile.instantiation?.template_source === family.templateSource, '来源实例化合同矛盾');
192
+ manifestBytes = fs.readFileSync(safe(root, profile.instantiation.distribution_manifest));
193
+ }
194
+ const manifest = JSON.parse(manifestBytes),
195
+ entries = {},
196
+ blobs = new Map(),
197
+ canonical = path.join(root, ".agents/skills"),
198
+ captured = new Map();
199
+ function capture(physical, logical, ancestors = []) {
200
+ relative(logical);
201
+ const s = fs.lstatSync(physical);
202
+ if (s.isSymbolicLink()) {
203
+ const link = fs.readlinkSync(physical),
204
+ resolved = fs.realpathSync(physical);
205
+ ensure(
206
+ !path.isAbsolute(link) &&
207
+ resolved.startsWith(canonical + path.sep) &&
208
+ /^\.(agents|claude|codex|cursor|pi|qoder|trae)\/skills\//.test(
209
+ logical,
210
+ ),
211
+ `不允许的投影链接: ${logical}`,
212
+ );
213
+ ensure(!ancestors.includes(resolved), "投影链接循环");
214
+ return capture(resolved, logical, [...ancestors, resolved]);
215
+ }
216
+ if (s.isDirectory()) {
217
+ for (const name of fs.readdirSync(physical).sort()) {
218
+ if (name === ".DS_Store") continue;
219
+ capture(path.join(physical, name), `${logical}/${name}`, ancestors);
220
+ }
221
+ return;
222
+ }
223
+ if (!selected(logical, manifest)) return;
224
+ governance(logical);
225
+ ensure(s.isFile(), `未知分发类型: ${logical}`);
226
+ let content = captured.get(physical);
227
+ if (!content) {
228
+ const bytes = fs.readFileSync(physical);
229
+ content = { bytes, digest: hash(bytes) };
230
+ captured.set(physical, content);
231
+ }
232
+ const { bytes, digest } = content;
233
+ entries[logical] = {
234
+ type: "file",
235
+ digest,
236
+ mode: s.mode & 0o111 ? 0o755 : 0o644,
237
+ blob: `blobs/${digest}`,
238
+ };
239
+ blobs.set(digest, bytes);
240
+ }
241
+ for (const name of fs.readdirSync(root).sort())
242
+ if (selected(name, manifest)) capture(path.join(root, name), name);
243
+ // Explicit allowFiles may lie below an otherwise excluded directory.
244
+ for (const ref of manifest.allowFiles || [])
245
+ if (fs.existsSync(safe(root, ref))) capture(safe(root, ref), ref);
246
+ const files = Object.fromEntries(
247
+ Object.entries(entries).sort(([a], [b]) => (a < b ? -1 : 1)),
248
+ );
249
+ let previous = null;
250
+ try {
251
+ previous = process.env.YSS_TEMPLATE_BASELINE_SNAPSHOT
252
+ ? JSON.parse(fs.readFileSync(process.env.YSS_TEMPLATE_BASELINE_SNAPSHOT, "utf8"))
253
+ : readJson(packageRoot, "template.snapshot.json");
254
+ } catch {}
255
+ const retiredFiles = Object.fromEntries(
256
+ Object.entries({...(previous?.retiredFiles || {}), ...(previous?.files || {})})
257
+ .filter(([ref]) => !Object.hasOwn(files, ref))
258
+ .map(([ref, item]) => [ref, {type:"file", digest:item.digest, mode:item.mode}])
259
+ .sort(([a], [b]) => (a < b ? -1 : 1)),
260
+ );
261
+ const transitionBaselines = Object.fromEntries(
262
+ Object.entries({...(previous?.files || {}), ...(previous?.transitionBaselines || {})})
263
+ .filter(([ref, item]) => Object.hasOwn(files, ref) && item.digest !== files[ref].digest)
264
+ .map(([ref, item]) => [ref, {type:"file", digest:item.digest, mode:item.mode}])
265
+ .sort(([a], [b]) => (a < b ? -1 : 1)),
266
+ );
267
+ const snapshot = {
268
+ schemaVersion: 1,
269
+ ...family,
270
+ templateCommit: revision,
271
+ sourceState,
272
+ manifestHash: hash(manifestBytes),
273
+ files,
274
+ retiredFiles,
275
+ retiredFilesHash: hash(JSON.stringify(retiredFiles)),
276
+ transitionBaselines,
277
+ transitionBaselinesHash: hash(JSON.stringify(transitionBaselines)),
278
+ snapshotHash: hash(JSON.stringify(files)),
279
+ projectionRepresentation: "materialized-files",
280
+ pathEncoding: "sha256-blobs-v1",
281
+ };
282
+ if (check) {
283
+ ensure(
284
+ json(readJson(packageRoot, "template.snapshot.json")) ===
285
+ json(snapshot),
286
+ "模板来源快照漂移",
287
+ );
288
+ loadBundle(packageRoot);
289
+ return snapshot;
290
+ }
291
+ fs.rmSync(path.join(packageRoot, "template"), {
292
+ recursive: true,
293
+ force: true,
294
+ });
295
+ for (const [digest, bytes] of blobs)
296
+ write(packageRoot, `template/blobs/${digest}`, bytes);
297
+ write(packageRoot, "template.manifest.json", manifestBytes);
298
+ write(packageRoot, "template.snapshot.json", json(snapshot));
299
+ write(packageRoot, "config/family.json", json(family));
300
+ return snapshot;
301
+ });
302
+ }
@@ -0,0 +1,198 @@
1
+ import * as fs from "node:fs";
2
+ import { parseDocument } from "./vendor/yaml.mjs";
3
+ import { ensure, hash, readJson, safe, governance } from "./io.mjs";
4
+ import { familyFor, CHECKS } from './family.mjs';
5
+ export const PROFILE = ".template-spec/process/harness-profile.yaml";
6
+ export function yaml(bytes) {
7
+ const doc = parseDocument(String(bytes), { uniqueKeys: true });
8
+ ensure(!doc.errors.length, "YAML 不合法");
9
+ const data = doc.toJS({ maxAliasCount: 0 });
10
+ ensure(
11
+ data && typeof data === "object" && !Array.isArray(data),
12
+ "YAML 必须是对象",
13
+ );
14
+ return data;
15
+ }
16
+ export function loadBundle(root) {
17
+ const pkg = readJson(root, "package.json"),
18
+ family = readJson(root, "config/family.json"),
19
+ snapshot = readJson(root, "template.snapshot.json"),
20
+ core = readJson(root, "cli-core.lock.json");
21
+ const expected = familyFor(family.side);
22
+ for (const [key, value] of Object.entries(expected))
23
+ ensure(
24
+ family[key] === value && snapshot[key] === value,
25
+ `包内身份不一致: ${key}`,
26
+ "IDENTITY",
27
+ );
28
+ ensure(
29
+ pkg.name === family.packageName && snapshot.schemaVersion === 1,
30
+ "包身份或快照 schema 不支持",
31
+ "IDENTITY",
32
+ );
33
+ ensure(
34
+ /^[0-9a-f]{40}$/.test(snapshot.templateCommit) &&
35
+ ["committed", "working-tree"].includes(snapshot.sourceState || "committed") &&
36
+ hash(JSON.stringify(snapshot.files)) === snapshot.snapshotHash,
37
+ "快照摘要或 revision 不一致",
38
+ "BUNDLE",
39
+ );
40
+ ensure(
41
+ hash(fs.readFileSync(safe(root, "template.manifest.json"))) ===
42
+ snapshot.manifestHash,
43
+ "分发清单摘要不一致",
44
+ "BUNDLE",
45
+ );
46
+ const retiredFiles = snapshot.retiredFiles || {};
47
+ ensure(
48
+ (!snapshot.retiredFilesHash && !Object.keys(retiredFiles).length) ||
49
+ hash(JSON.stringify(retiredFiles)) === snapshot.retiredFilesHash,
50
+ "退出分发基线摘要不一致",
51
+ "BUNDLE",
52
+ );
53
+ for (const [ref, item] of Object.entries(retiredFiles)) {
54
+ governance(ref);
55
+ ensure(
56
+ item.type === "file" &&
57
+ [0o644, 0o755].includes(item.mode) &&
58
+ /^[0-9a-f]{64}$/.test(item.digest),
59
+ `退出分发基线不合法: ${ref}`,
60
+ "BUNDLE",
61
+ );
62
+ }
63
+ const transitionBaselines = snapshot.transitionBaselines || {};
64
+ ensure(
65
+ (!snapshot.transitionBaselinesHash && !Object.keys(transitionBaselines).length) ||
66
+ hash(JSON.stringify(transitionBaselines)) === snapshot.transitionBaselinesHash,
67
+ "受管范围迁移基线摘要不一致",
68
+ "BUNDLE",
69
+ );
70
+ for (const [ref, item] of Object.entries(transitionBaselines)) {
71
+ governance(ref);
72
+ ensure(
73
+ snapshot.files[ref] && item.type === "file" &&
74
+ [0o644, 0o755].includes(item.mode) &&
75
+ /^[0-9a-f]{64}$/.test(item.digest),
76
+ `受管范围迁移基线不合法: ${ref}`,
77
+ "BUNDLE",
78
+ );
79
+ }
80
+ ensure(
81
+ core.schemaVersion === 1 &&
82
+ core.protocolVersion === 1 &&
83
+ ["committed", "working-tree"].includes(core.sourceState || "committed") &&
84
+ /^[0-9a-f]{40}$/.test(core.sourceRevision) &&
85
+ /^[0-9a-f]{64}$/.test(core.digest),
86
+ "核心锁不合法",
87
+ "BUNDLE",
88
+ );
89
+ if (core.coreVersion === '0.2.0') ensure(core.files && Object.keys(core.files).length > 0, '核心文件清单缺失', 'BUNDLE');
90
+ for (const [ref, item] of Object.entries(core.files || {})) {
91
+ const file=safe(root, `vendor/cli-core/${ref}`);
92
+ ensure(hash(fs.readFileSync(file))===item.digest && (fs.statSync(file).mode & 0o111 ? 0o755 : 0o644)===item.mode, `核心文件漂移: ${ref}`, 'BUNDLE');
93
+ }
94
+ if (Object.keys(core.files || {}).length) ensure(hash(JSON.stringify(core.files))===core.digest, '核心清单摘要不一致', 'BUNDLE');
95
+ const manifest = readJson(root, "template.manifest.json");
96
+ const files = new Map();
97
+ const blobs = new Map();
98
+ for (const [ref, item] of Object.entries(snapshot.files)) {
99
+ governance(ref);
100
+ ensure(!(manifest.instanceForbiddenPaths || []).some(p=>ref===p || ref.startsWith(p+"/")), `快照含家族禁止分发路径: ${ref}`, "BUNDLE");
101
+ ensure(
102
+ item.type === "file" &&
103
+ [0o644, 0o755].includes(item.mode) &&
104
+ item.blob === `blobs/${item.digest}` &&
105
+ /^[0-9a-f]{64}$/.test(item.digest),
106
+ `快照类型/编码非法: ${ref}`,
107
+ "BUNDLE",
108
+ );
109
+ let bytes = blobs.get(item.digest);
110
+ if (!bytes) {
111
+ bytes = fs.readFileSync(safe(root, `template/${item.blob}`));
112
+ ensure(hash(bytes) === item.digest, `快照内容摘要不一致: ${ref}`, "BUNDLE");
113
+ blobs.set(item.digest, bytes);
114
+ }
115
+ files.set(ref, {
116
+ bytes: Buffer.from(bytes),
117
+ baseline: { type: "file", digest: item.digest, mode: item.mode },
118
+ });
119
+ }
120
+ for (const ref of [
121
+ "README.md",
122
+ "AGENTS.md",
123
+ "CONTEXT.md",
124
+ "yss-project.yaml",
125
+ PROFILE,
126
+ ])
127
+ ensure(files.has(ref), `缺少必需合同: ${ref}`, "BUNDLE");
128
+ const profile = yaml(files.get(PROFILE).bytes),
129
+ identity = yaml(files.get("yss-project.yaml").bytes);
130
+ ensure(
131
+ identity.schema_version === 1 &&
132
+ identity.repository_mode === "template-source",
133
+ "包内不是模板源",
134
+ "IDENTITY",
135
+ );
136
+ ensure(
137
+ [1, 2].includes(profile.schema_version) &&
138
+ profile.profile_id === family.profileId &&
139
+ (family.side === "design" || (profile.instantiation?.cli_package === family.packageName &&
140
+ profile.instantiation?.metadata_file === family.metadataFile &&
141
+ profile.instantiation?.template_source === family.templateSource)),
142
+ "模板 profile 与包身份矛盾",
143
+ "IDENTITY",
144
+ );
145
+ if (files.has("skills-lock.json")) for (const [, ref] of CHECKS) ensure(files.has(ref), `缺少实例校验器: ${ref}`, "BUNDLE");
146
+ return {
147
+ root,
148
+ pkg,
149
+ family,
150
+ snapshot,
151
+ core,
152
+ manifest,
153
+ retiredFiles,
154
+ transitionBaselines,
155
+ files,
156
+ };
157
+ }
158
+ export function render(bundle, variables) {
159
+ const result = new Map();
160
+ for (const [ref, item] of bundle.files) {
161
+ if (
162
+ !variables.includeExampleDocs &&
163
+ bundle.manifest.exampleDocPaths?.some(
164
+ (x) => ref === x || ref.startsWith(x + "/"),
165
+ )
166
+ )
167
+ continue;
168
+ let bytes = item.bytes;
169
+ if (ref === "yss-project.yaml")
170
+ bytes = Buffer.from(
171
+ "schema_version: 1\nrepository_mode: project-instance\n",
172
+ );
173
+ else if (ref === "README.md")
174
+ bytes = Buffer.from(
175
+ `# ${variables.projectName}\n\n本仓是 \`${bundle.family.profileId}\` 的 \`project-instance\`。\n\n先读 [AGENTS.md](AGENTS.md)、[CONTEXT.md](CONTEXT.md) 与 [profile](.template-spec/process/harness-profile.yaml)。\n\n业务领域:${variables.businessDomain}\n团队规模:${variables.teamSize}\n`,
176
+ );
177
+ if (bundle.family.side === "design" && ref === "AGENTS.md")
178
+ bytes = Buffer.from(bytes.toString().replace("**项目名称:** [填写]", () => `**项目名称:** ${variables.projectName}`).replace("**业务领域:** [填写]", () => `**业务领域:** ${variables.businessDomain}`).replace("**团队规模:** [填写]", () => `**团队规模:** ${variables.teamSize}`));
179
+ if (ref === ".template-spec/agents/issue-tracker.md")
180
+ bytes = Buffer.from(
181
+ bytes
182
+ .toString()
183
+ .replace(
184
+ "platform: local-markdown",
185
+ `platform: ${variables.issueTracker}`,
186
+ )
187
+ .replace(
188
+ "| `platform` | `local-markdown` |",
189
+ `| \`platform\` | \`${variables.issueTracker}\` |`,
190
+ ),
191
+ );
192
+ result.set(ref, {
193
+ bytes,
194
+ baseline: { ...item.baseline, digest: hash(bytes) },
195
+ });
196
+ }
197
+ return result;
198
+ }
@@ -0,0 +1,182 @@
1
+ import { runMigration } from "./migration-adapter.mjs";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { doctor } from "./verification.mjs";
4
+ import { inspectState, recoveryPreview, recover } from "./transaction.mjs";
5
+ import { identity, recoveryIdentity } from "./identity.mjs";
6
+ import * as fs from "node:fs";
7
+ import path from "node:path";
8
+ import { loadBundle } from "./bundle.mjs";
9
+ import { update } from "./update.mjs";
10
+ import { execute } from "./engine.mjs";
11
+ import { ensure, targetPath, stat, write, json } from "./io.mjs";
12
+ export function parse(argv) {
13
+ const args = [...argv],
14
+ options = {
15
+ command: "init",
16
+ projectName: "",
17
+ businessDomain: "未指定",
18
+ teamSize: "未指定",
19
+ issueTracker: "local-markdown",
20
+ includeExampleDocs: true,
21
+ };
22
+ if (args[0] && !args[0].startsWith("-")) options.command = args.shift();
23
+ const values = {
24
+ "--target-dir": "targetDir",
25
+ "--project-name": "projectName",
26
+ "--business-domain": "businessDomain",
27
+ "--team-size": "teamSize",
28
+ "--issue-tracker": "issueTracker",
29
+ };
30
+ const flags = {
31
+ "--plan": "plan",
32
+ "--prune": "prune",
33
+ "--migrate-layout": "migrateLayout",
34
+ "--apply": "apply",
35
+ "--dry-run": "dryRun",
36
+ "--force": "force",
37
+ "--json": "json",
38
+ "--git-init": "gitInit",
39
+ "--help": "help",
40
+ "-h": "help",
41
+ "--version": "version",
42
+ "-v": "version",
43
+ "--include-example-docs": "includeExampleDocs",
44
+ };
45
+ while (args.length) {
46
+ const flag = args.shift();
47
+ if (values[flag]) {
48
+ ensure(args.length && !args[0].startsWith("-"), `${flag} 缺少值`);
49
+ options[values[flag]] = args.shift();
50
+ } else if (flag === "--no-example-docs") options.includeExampleDocs = false;
51
+ else if (flags[flag]) options[flags[flag]] = true;
52
+ else throw new Error(`未知参数: ${flag}`);
53
+ }
54
+ ensure(
55
+ ["init", "attach", "sync", "diff", "doctor", "recover", "update", "upgrade"].includes(options.command),
56
+ "未知命令",
57
+ );
58
+ ensure(
59
+ !(options.apply && (options.dryRun || options.plan)),
60
+ "--apply 与 --dry-run 不可同时使用",
61
+ );
62
+ ensure(
63
+ !options.gitInit || options.command === "init",
64
+ "--git-init 仅用于 init",
65
+ );
66
+ const accepted = {
67
+ init: ['projectName','businessDomain','teamSize','targetDir','issueTracker','gitInit','includeExampleDocs','dryRun'],
68
+ attach: ['projectName','businessDomain','teamSize','targetDir','issueTracker','includeExampleDocs','dryRun','apply','force','plan','migrateLayout'],
69
+ sync: ['targetDir','dryRun','apply','force','plan','prune','migrateLayout'],
70
+ diff: ['targetDir','dryRun'], doctor: ['targetDir'], recover: ['targetDir','dryRun','apply'],
71
+ update: ['dryRun','force'], upgrade: ['dryRun','force'],
72
+ };
73
+ if (!options.help && !options.version) for (const flag of argv.filter(x=>x.startsWith('-'))) {
74
+ const key = flag === '--no-example-docs' ? 'includeExampleDocs' : values[flag] || flags[flag];
75
+ ensure(['help','version','json'].includes(key) || accepted[options.command].includes(key), `${flag} 不适用于 ${options.command}`);
76
+ }
77
+ if (options.issueTracker === "local") options.issueTracker = "local-markdown";
78
+ ensure(
79
+ ["local-markdown", "github", "gitlab"].includes(options.issueTracker),
80
+ "issue-tracker 必须为 local-markdown、github 或 gitlab",
81
+ );
82
+ for (const k of ["projectName", "businessDomain", "teamSize"])
83
+ ensure(!/[\x00-\x1f\x7f]/.test(options[k]), `${k} 不允许控制字符`);
84
+ return options;
85
+ }
86
+ export async function main(packageRoot, argv = process.argv.slice(2)) {
87
+ packageRoot = path.resolve(packageRoot);
88
+ const wantsJson = argv.includes("--json");
89
+ try {
90
+ if (argv[0] === "migrate") {
91
+ const result = runMigration(packageRoot, argv.slice(1));
92
+ process.stdout.write(json(result));
93
+ return;
94
+ }
95
+ const opts = parse(argv);
96
+ const pkg = JSON.parse(
97
+ fs.readFileSync(path.join(packageRoot, "package.json")),
98
+ );
99
+ if (opts.help) {
100
+ const help = `${pkg.name} init|attach|sync|diff|doctor|recover|update|upgrade|migrate\nmigrate plan|apply|status|recover|rollback:固定计划、持久归档和最近升级回退。\n--target-dir <目录> --project-name <名称> --business-domain <领域> --team-size <规模>\n--issue-tracker <local-markdown|github|gitlab> --git-init --no-example-docs\nattach/sync 默认只预览,写入必须 --apply;冲突显式 --apply --force。\n--plan --prune(仅 sync) --migrate-layout(旧目录显式迁移) --dry-run --json --version\ndoctor/diff/recover 默认只读;recover --apply 恢复未完成事务。init 只接受空目录。\n`;
101
+ process.stdout.write(wantsJson ? json({schemaVersion:1,command:"help",status:"ok",help}) : help);
102
+ return;
103
+ }
104
+ if (opts.version) {
105
+ process.stdout.write(wantsJson ? json({schemaVersion:1,command:"version",status:"ok",packageName:pkg.name,cliVersion:pkg.version}) : pkg.version + "\n");
106
+ return;
107
+ }
108
+ if (["update", "upgrade"].includes(opts.command)) {
109
+ const result = update(packageRoot, pkg, opts);
110
+ process.stdout.write(
111
+ wantsJson
112
+ ? json(result)
113
+ : `${result.status}: ${result.currentVersion} → ${result.latestVersion}\n${result.advice}\n`,
114
+ );
115
+ return;
116
+ }
117
+ if (pkg.name === 'create-yss-harness-design' && opts.command === 'init') {
118
+ const required = ['projectName','businessDomain','targetDir'];
119
+ const missing = required.filter(k=>!opts[k] || opts[k]==='未指定');
120
+ if (missing.length && process.stdin.isTTY && !wantsJson) {
121
+ const rl=createInterface({input:process.stdin,output:process.stderr});
122
+ try { for(const key of missing) opts[key]=(await rl.question(({projectName:'项目名称',businessDomain:'业务领域',targetDir:'目标目录'})[key]+': ')).trim(); }
123
+ finally { rl.close(); }
124
+ }
125
+ ensure(required.every(k=>opts[k] && opts[k]!=='未指定'),'init 需要 --project-name、--business-domain 和 --target-dir');
126
+ ensure(!required.some(k=>/[\x00-\x1f\x7f]/.test(opts[k])),'初始化参数含控制字符');
127
+ }
128
+ const bundle = loadBundle(packageRoot),
129
+ target = targetPath(opts.targetDir || process.cwd());
130
+ ensure(
131
+ target !== packageRoot &&
132
+ !target.startsWith(packageRoot + path.sep) &&
133
+ !packageRoot.startsWith(target + path.sep),
134
+ "目标不得覆盖 CLI 安装目录",
135
+ );
136
+ let result;
137
+ if (opts.command === 'doctor') result = doctor(bundle,target);
138
+ else if (opts.command === 'recover') {
139
+ ensure(stat(target)?.isDirectory(),'恢复目标不存在','PATH');
140
+ const state=inspectState(target,bundle.family);
141
+ if (!state.pending.length) { identity(target,bundle,'recover');result={schemaVersion:1,command:'recover',status:'preview',target,transactions:[],message:'没有未完成事务'}; }
142
+ else {
143
+ const validate=()=>recoveryIdentity(target,bundle,state);
144
+ result=opts.apply ? recover(target,bundle.family,state,validate) : recoveryPreview(target,bundle.family,state,validate);
145
+ result.command='recover';
146
+ }
147
+ } else result = execute(bundle, target, opts);
148
+ if (result.status === 'error') process.exitCode=1;
149
+ process.stdout.write(wantsJson ? json(result) : formatPlan(result));
150
+ } catch (error) {
151
+ if (wantsJson)
152
+ process.stdout.write(
153
+ json({
154
+ schemaVersion: 1,
155
+ command: argv[0]?.startsWith("-") ? "init" : argv[0] || "init",
156
+ ...error.result,
157
+ status: "error",
158
+ code: error.code || "INVALID",
159
+ message: error.message,
160
+ }),
161
+ );
162
+ if (error.result && !wantsJson)
163
+ process.stdout.write(formatPlan(error.result));
164
+ process.stderr.write(error.message + "\n");
165
+ process.exitCode = 1;
166
+ }
167
+ }
168
+
169
+ function formatPlan(result) {
170
+ const lines = [
171
+ `${result.status}: ${result.target}`,
172
+ `CLI ${result.cliVersion || "-"} / core ${result.coreVersion || "-"} / template ${result.templateCommit || "-"}`,
173
+ ];
174
+ for (const change of result.changes || [])
175
+ lines.push(`${change.action.padEnd(10)} ${change.path}`);
176
+ for (const check of result.checks || []) lines.push(`[${check.status}] ${check.name}: ${check.message || ''}`);
177
+ for (const transaction of result.transactions || []) lines.push(`恢复事务 ${transaction.id || transaction}: ${transaction.backupPath || ''}`);
178
+ if (result.advice) lines.push(result.advice);
179
+ if (result.backupPath) lines.push(`备份和恢复清单:${result.backupPath}`);
180
+ if (result.message) lines.push(result.message);
181
+ return lines.join("\n") + "\n";
182
+ }