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,153 @@
1
+ import * as fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ export const hash = (b) => createHash("sha256").update(b).digest("hex");
5
+ export const json = (x) => JSON.stringify(x, null, 2) + "\n";
6
+ export function ensure(ok, message, code = "INVALID") {
7
+ if (!ok) throw Object.assign(new Error(message), { code });
8
+ }
9
+ export function stat(p) {
10
+ try {
11
+ return fs.lstatSync(p);
12
+ } catch (e) {
13
+ if (e.code === "ENOENT") return null;
14
+ throw e;
15
+ }
16
+ }
17
+ export function relative(ref) {
18
+ ensure(
19
+ typeof ref === "string" &&
20
+ ref.length > 0 &&
21
+ !ref.includes("\\") &&
22
+ !/[\x00-\x1f\x7f:]/.test(ref) &&
23
+ !path.posix.isAbsolute(ref) &&
24
+ ref.split("/").every((x) => x && x !== "." && x !== ".."),
25
+ `非法路径: ${ref}`,
26
+ "PATH",
27
+ );
28
+ return ref;
29
+ }
30
+ export function safe(root, ref) {
31
+ relative(ref);
32
+ let cursor = root;
33
+ const parts = ref.split("/");
34
+ for (let i = 0; i < parts.length; i++) {
35
+ cursor = path.join(cursor, parts[i]);
36
+ const s = stat(cursor);
37
+ ensure(!s?.isSymbolicLink(), `路径不允许符号链接: ${ref}`, "PATH");
38
+ if (i < parts.length - 1)
39
+ ensure(!s || s.isDirectory(), `父路径不是目录: ${ref}`, "PATH");
40
+ }
41
+ return cursor;
42
+ }
43
+ export function targetPath(p) {
44
+ const resolved = path.resolve(p);
45
+ let cursor = path.parse(resolved).root;
46
+ for (const part of resolved.slice(cursor.length).split("/").filter(Boolean)) {
47
+ cursor = path.join(cursor, part);
48
+ const s = stat(cursor);
49
+ ensure(
50
+ !s || (!s.isSymbolicLink() && s.isDirectory()),
51
+ `目标路径不是普通目录: ${cursor}`,
52
+ "PATH",
53
+ );
54
+ }
55
+ return resolved;
56
+ }
57
+ const protectedRoots = new Set([
58
+ ".git",
59
+ ".gitmodules",
60
+ "node_modules",
61
+ "src",
62
+ "app",
63
+ "apps",
64
+ "backend",
65
+ "frontend",
66
+ "target",
67
+ "dist",
68
+ "build",
69
+ "deploy",
70
+ "Dockerfile",
71
+ "docker-compose.yml",
72
+ "package.json",
73
+ "package-lock.json",
74
+ "pnpm-lock.yaml",
75
+ "yarn.lock",
76
+ "bun.lock",
77
+ "bun.lockb",
78
+ "pom.xml",
79
+ "mvnw",
80
+ "mvnw.cmd",
81
+ ".mvn",
82
+ "gradlew",
83
+ "gradlew.bat",
84
+ "build.gradle",
85
+ "settings.gradle",
86
+ ".github",
87
+ ".yss-harness-state",
88
+ ]);
89
+ export function governance(ref) {
90
+ relative(ref);
91
+ ensure(
92
+ !protectedRoots.has(ref.split("/")[0]) && !ref.split("/").includes(".git"),
93
+ `受保护业务/Git/状态路径: ${ref}`,
94
+ "PROTECTED",
95
+ );
96
+ ensure(
97
+ /^(?:\.(?:agents|claude|codex|cursor|pi|qoder|trae)\/|\.template-spec\/|docs\/|scripts\/)/.test(
98
+ ref,
99
+ ) ||
100
+ [
101
+ ".cursorrules",
102
+ ".gitignore",
103
+ ".nvmrc",
104
+ "AGENTS.md",
105
+ "CLAUDE.md",
106
+ "CONTEXT.md",
107
+ "README.md",
108
+ "DESIGN.md",
109
+ "skills-lock.json",
110
+ "yss-project.yaml",
111
+ "yss-public-skills.json",
112
+ ].includes(ref),
113
+ `不在治理边界: ${ref}`,
114
+ "PROTECTED",
115
+ );
116
+ ensure(
117
+ ref !== "scripts/instantiate-harness",
118
+ `旧创建入口不得分发`,
119
+ "PROTECTED",
120
+ );
121
+ }
122
+ export function descriptor(root, ref) {
123
+ const file = safe(root, ref),
124
+ s = stat(file);
125
+ if (!s) return null;
126
+ ensure(
127
+ s.isFile() && s.nlink === 1,
128
+ `目标必须是非硬链接普通文件: ${ref}`,
129
+ "PATH",
130
+ );
131
+ return {
132
+ type: "file",
133
+ digest: hash(fs.readFileSync(file)),
134
+ mode: s.mode & 0o777,
135
+ };
136
+ }
137
+ export function same(a, b) {
138
+ return (
139
+ (a === null && b === null) ||
140
+ Boolean(
141
+ a && b && a.type === b.type && a.digest === b.digest && a.mode === b.mode,
142
+ )
143
+ );
144
+ }
145
+ export function write(root, ref, bytes, mode = 0o644) {
146
+ const file = safe(root, ref);
147
+ fs.mkdirSync(path.dirname(file), { recursive: true });
148
+ fs.writeFileSync(file, bytes);
149
+ fs.chmodSync(file, mode);
150
+ }
151
+ export function readJson(root, ref) {
152
+ return JSON.parse(fs.readFileSync(safe(root, ref), "utf8"));
153
+ }
@@ -0,0 +1,64 @@
1
+ import * as fs from "node:fs";
2
+ import { descriptor, ensure, hash, safe, same } from "./io.mjs";
3
+
4
+ const oldDirs = [
5
+ "docs/agents", "docs/process", "docs/templates", "docs/user-guide",
6
+ "docs/plan/templates", "docs/api/templates", "docs/architecture/templates",
7
+ "docs/design/templates", "docs/design/tokens", "docs/design/schemas",
8
+ "docs/design/diagrams", "docs/discovery/templates",
9
+ ];
10
+ const oldFiles = [
11
+ "docs/adr/README.md", "docs/architecture/README.md",
12
+ "docs/plan/README.md", "docs/plan/entry-review.md",
13
+ "docs/discovery/README.md", "docs/discovery/IDEATION.md",
14
+ "docs/design/README.md", "docs/design/design.md",
15
+ "docs/design/design-system-sync.yaml", "docs/design/preview.html",
16
+ "docs/design/preview-dark.html", "docs/design/preview.css",
17
+ "docs/design/preview.js", "docs/engineering/backend-platforms.json",
18
+ "docs/engineering/backend-platforms.md",
19
+ ];
20
+ const trackerFrom = "docs/agents/issue-tracker.md";
21
+ const trackerTo = ".template-spec/agents/issue-tracker.md";
22
+
23
+ function collect(target) {
24
+ const found = new Set();
25
+ function visit(ref) {
26
+ const file = safe(target, ref);
27
+ let entry;
28
+ try { entry = fs.lstatSync(file); }
29
+ catch (error) { if (error.code === "ENOENT") return; throw error; }
30
+ ensure(!entry.isSymbolicLink(), `旧治理路径为符号链接: ${ref}`, "PATH");
31
+ if (entry.isFile()) { found.add(ref); return; }
32
+ ensure(entry.isDirectory(), `旧治理路径不是普通文件或目录: ${ref}`, "PATH");
33
+ for (const name of fs.readdirSync(file)) {
34
+ if (name !== ".DS_Store") visit(`${ref}/${name}`);
35
+ }
36
+ }
37
+ for (const ref of [...oldDirs, ...oldFiles]) visit(ref);
38
+ return [...found].sort();
39
+ }
40
+
41
+ export function prepareLayoutMigration(target, meta, files, opts) {
42
+ const legacyFiles = collect(target);
43
+ if (!legacyFiles.length) return { legacyFiles, replacements: new Map(), deletions: [] };
44
+ ensure(opts.command !== "diff" && opts.migrateLayout, `旧治理目录需显式 --migrate-layout: ${legacyFiles.join(", ")}`, "LAYOUT");
45
+ const replacements = new Map();
46
+ const deletions = [];
47
+ for (const from of legacyFiles) {
48
+ const to = `.template-spec/${from.slice("docs/".length)}`;
49
+ const before = descriptor(target, from);
50
+ const destination = descriptor(target, to);
51
+ if (from === trackerFrom) {
52
+ ensure(files.has(trackerTo), `当前模板缺少 tracker 目标: ${trackerTo}`, "LAYOUT");
53
+ const bytes = fs.readFileSync(safe(target, from));
54
+ ensure(!destination || destination.digest === hash(bytes), `项目 tracker 新旧配置不一致: ${from}`, "LAYOUT");
55
+ if (!destination) replacements.set(to, bytes);
56
+ } else {
57
+ const baseline = meta?.managedFiles?.[from]?.baseline;
58
+ ensure(baseline && same(before, baseline), `旧治理文件缺少可信基线或已修改: ${from}`, "LAYOUT");
59
+ ensure(files.has(to) || !destination, `目标路径已有文件且当前模板未分发: ${to}`, "LAYOUT");
60
+ }
61
+ deletions.push({ path: from, before, after: null });
62
+ }
63
+ return { legacyFiles, replacements, deletions };
64
+ }
@@ -0,0 +1,19 @@
1
+ import { ensure, governance } from './io.mjs';
2
+ import { userOwned } from './family.mjs';
3
+ export function normalizeLegacy(meta, family) {
4
+ ensure(family.side === 'design' && meta.metadataSchemaVersion === 1, '不支持旧实例', 'LEGACY');
5
+ for (const key of ['profileId', 'templateName', 'templateSource']) ensure(meta[key] === family[key], `旧 metadata 身份矛盾: ${key}`, 'IDENTITY');
6
+ ensure(/^[a-f0-9]{40}$/.test(meta.templateCommit) && /^[a-f0-9]{64}$/.test(meta.snapshotHash) && /^[a-f0-9]{64}$/.test(meta.managedFilesManifestVersion), '旧 metadata 来源摘要不完整', 'BASELINE');
7
+ ensure(['committed', 'working-tree'].includes(meta.templateSourceState), '旧 metadata 来源状态非法', 'BASELINE');
8
+ ensure(meta.variables && ['projectName','businessDomain','teamSize','issueTracker'].every(k => typeof meta.variables[k] === 'string' && meta.variables[k].trim()) && ['local-markdown','github','gitlab'].includes(meta.variables.issueTracker), '旧实例变量不完整', 'BASELINE');
9
+ ensure(meta.managedFiles && typeof meta.managedFiles === 'object' && !Array.isArray(meta.managedFiles), '旧基线缺失', 'BASELINE');
10
+ const managedFiles = {}, legacyRetainedFiles = [];
11
+ for (const [ref, record] of Object.entries(meta.managedFiles)) {
12
+ if (ref !== "scripts/instantiate-harness") governance(ref);
13
+ ensure(record && ['copy','render'].includes(record.type) && /^[a-f0-9]{64}$/.test(record.contentHash), `旧基线非法: ${ref}`, 'BASELINE');
14
+ if (ref === "scripts/instantiate-harness") { legacyRetainedFiles.push(ref); continue; }
15
+ if (!userOwned(ref, family)) managedFiles[ref] = { baseline: {type:'file',digest:record.contentHash,mode:0o644}, lastApplied:null, legacyUnproven:true };
16
+ }
17
+ for (const ref of ['yss-project.yaml','.template-spec/process/harness-profile.yaml','AGENTS.md','CONTEXT.md']) ensure(managedFiles[ref], `旧基线缺少合同: ${ref}`, 'BASELINE');
18
+ return {...meta, legacy:true, legacyRetainedFiles, variables:{...meta.variables,includeExampleDocs:true}, managedFiles};
19
+ }
@@ -0,0 +1,237 @@
1
+ // Public migration driver. Adapters only prepare candidates; this module owns writes.
2
+ import * as fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {randomUUID} from 'node:crypto';
6
+ import {spawnSync} from 'node:child_process';
7
+ import {hash,json,ensure,stat,safe,descriptor,same,write,targetPath,governance} from './io.mjs';
8
+ import {guardNestedRepository,gitlinks,METADATA} from './identity.mjs';
9
+ import {applyTransaction,inspectState,recoveryPreview,recover as recoverTransaction} from './transaction.mjs';
10
+
11
+ const INDEX='.yss-harness-state/upgrade.json';
12
+ export function assertUpgradeVersion(from,to) {
13
+ const parts=v=>{
14
+ if(typeof v!=='string')return null;
15
+ const match=v.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
16
+ if(!match)return null;
17
+ const pre=match[4]?.split('.') || [];
18
+ if(pre.some(x=>/^\d+$/.test(x) && x.length>1 && x[0]==='0'))return null;
19
+ return {numbers:match.slice(1,4).map(BigInt),pre};
20
+ };
21
+ const a=parts(from),b=parts(to);ensure(a && b,'缺少可比较的 CLI 版本基线','VERSION');
22
+ let order=0;
23
+ for(let i=0;i<3 && !order;i++)order=b.numbers[i]>a.numbers[i]?1:b.numbers[i]<a.numbers[i]?-1:0;
24
+ if(!order && (!a.pre.length || !b.pre.length))order=b.pre.length?-1:a.pre.length?1:0;
25
+ else if(!order)for(let i=0;i<Math.max(a.pre.length,b.pre.length) && !order;i++){
26
+ const x=a.pre[i],y=b.pre[i];
27
+ if(x===undefined)order=1;else if(y===undefined)order=-1;
28
+ else if(x!==y){const xn=/^\d+$/.test(x),yn=/^\d+$/.test(y);order=xn&&yn?(BigInt(y)>BigInt(x)?1:-1):xn!==yn?(xn?1:-1):(y>x?1:-1);}
29
+ }
30
+ ensure(order>=0,'migrate 不支持 CLI 降级','VERSION');
31
+ }
32
+ const OMIT=new Set(['.git','node_modules','.codegraph','.graphify','__pycache__','.DS_Store']);
33
+ export function inventory(root, prefix='', out={}) {
34
+ for(const name of fs.readdirSync(safeDirectory(root,prefix)).sort()) {
35
+ if(OMIT.has(name) || !prefix && name==='.yss-harness-state') continue;
36
+ const ref=prefix?`${prefix}/${name}`:name, p=path.join(root,ref), s=fs.lstatSync(p);
37
+ if(s.isSymbolicLink()) out[ref]={type:'symlink',link:fs.readlinkSync(p)};
38
+ else if(s.isDirectory()) {out[ref]={type:'directory',mode:s.mode&0o777};inventory(root,ref,out);}
39
+ else {ensure(s.isFile() && s.nlink===1,`不支持特殊文件/硬链接: ${ref}`,'PATH');out[ref]=descriptor(root,ref);}
40
+ }
41
+ return out;
42
+ }
43
+ function safeDirectory(root,ref) {return ref?safe(root,ref):root;}
44
+ export function copyCandidate(root,destination,tree=inventory(root)) {
45
+ for(const [ref,d] of Object.entries(tree)) {
46
+ const p=path.join(destination,ref);fs.mkdirSync(path.dirname(p),{recursive:true});
47
+ if(d.type==='directory') fs.mkdirSync(p,{recursive:true,mode:d.mode});
48
+ else if(d.type==='symlink') fs.symlinkSync(d.link,p);
49
+ else {fs.copyFileSync(safe(root,ref),p);fs.chmodSync(p,d.mode);}
50
+ }
51
+ }
52
+ const digest=value=>hash(JSON.stringify(value));
53
+ function gitStamp(root) {
54
+ const r=spawnSync('git',['--no-optional-locks','-c','core.fsmonitor=false','-C',root,'ls-files','--stage','-z'],{encoding:'buffer',maxBuffer:32*1024*1024});
55
+ const head=spawnSync('git',['--no-optional-locks','-C',root,'rev-parse','HEAD'],{encoding:'utf8'});
56
+ return {index:r.status===0?hash(r.stdout):null,head:head.status===0?head.stdout.trim():null,links:gitlinks(root)};
57
+ }
58
+ function outside(root,p) {
59
+ ensure(typeof p==='string' && path.isAbsolute(p),'计划与归档路径必须是绝对路径','PATH');
60
+ const result=path.resolve(p);targetPath(path.dirname(result));
61
+ ensure(result!==root && !result.startsWith(root+path.sep) && !root.startsWith(result+path.sep),'计划/归档必须位于项目外','PATH');
62
+ ensure(!stat(result)?.isSymbolicLink(),'外部路径不能是符号链接','PATH');return result;
63
+ }
64
+ function durable(root,ref,bytes,mode=0o600) {
65
+ const p=safe(root,ref);fs.mkdirSync(path.dirname(p),{recursive:true});
66
+ const temp=p+'.writing-'+randomUUID();
67
+ const fd=fs.openSync(temp,'wx',mode);
68
+ try{fs.writeFileSync(fd,bytes);fs.fsyncSync(fd);}finally{fs.closeSync(fd);}
69
+ fs.renameSync(temp,p);
70
+ const dir=fs.openSync(path.dirname(p),'r');try{fs.fsyncSync(dir);}finally{fs.closeSync(dir);}
71
+ }
72
+ function readFile(p){ensure(stat(p)?.isFile() && !stat(p).isSymbolicLink() && stat(p).nlink===1,`不是普通文件: ${p}`,'PATH');return fs.readFileSync(p);}
73
+ function guard(root,adapter,ref) {
74
+ if(ref!==adapter.family.metadataFile) governance(ref);
75
+ ensure(ref!==INDEX,'不能迁移事务状态','PROTECTED');
76
+ guardNestedRepository(root,ref);
77
+ ensure(!gitlinks(root).some(p=>ref===p || ref.startsWith(p+'/')),`受保护 gitlink: ${ref}`,'PROTECTED');
78
+ descriptor(root,ref);
79
+ }
80
+ function encode(operations) {return operations.map(op=>({path:op.path,before:op.before,after:op.after,...(op.after?{contentBase64:op.bytes.toString('base64')}:{})}));}
81
+ function validateOperations(root,adapter,ops) {
82
+ ensure(Array.isArray(ops),'操作列表非法','PLAN');const paths=new Set();
83
+ for(const op of ops){guard(root,adapter,op.path);ensure(!paths.has(op.path),'重复迁移路径','PLAN');paths.add(op.path);
84
+ for(const d of [op.before,op.after]) ensure(d===null || d?.type==='file' && /^[a-f0-9]{64}$/.test(d.digest) && Number.isInteger(d.mode) && d.mode>=0 && d.mode<=0o777,'文件描述非法','PLAN');
85
+ if(op.after) ensure(typeof op.contentBase64==='string' && hash(Buffer.from(op.contentBase64,'base64'))===op.after.digest,'候选内容摘要错误','PLAN');
86
+ }
87
+ }
88
+ function assertExecutor(adapter,plan) {ensure(plan.schemaVersion===1 && plan.family===adapter.family.packageName && plan.executor===adapter.fingerprint,'CLI 家族/执行器与计划不一致','EXECUTOR');}
89
+ function checkPlan(adapter,plan) {
90
+ const {planDigest,...body}=plan;ensure(planDigest===digest(body),'计划摘要错误','PLAN');assertExecutor(adapter,plan);
91
+ targetPath(plan.target);ensure(fs.realpathSync(plan.target)===plan.target,'项目实际路径已变化','PATH');
92
+ ensure(/^[a-f0-9-]{36}$/.test(plan.id) && !Number.isNaN(Date.parse(plan.createdAt)),'计划身份非法','PLAN');
93
+ outside(plan.target,plan.archive);validateOperations(plan.target,adapter,plan.operations);
94
+ }
95
+ function index(root,adapter) {
96
+ if(!stat(safe(root,INDEX)))return {schemaVersion:1,family:adapter.family.packageName,runs:[]};
97
+ const value=JSON.parse(readFile(safe(root,INDEX)));
98
+ ensure(value.schemaVersion===1 && value.family===adapter.family.packageName && Array.isArray(value.runs),'迁移索引身份不一致','STATE');return value;
99
+ }
100
+ function state(root,adapter) {
101
+ if(!stat(safe(root,'.yss-harness-state')))return {pending:[]};
102
+ return inspectState(root,adapter.family);
103
+ }
104
+ function lock(root,fn) {
105
+ const file=safe(root,'.yss-harness-migrate.lock');
106
+ if(stat(file)) {
107
+ const owner=JSON.parse(readFile(file));ensure(owner.host===os.hostname() && Number.isInteger(owner.pid) && owner.pid>0,'无法判断迁移锁持有者','LOCKED');
108
+ let alive=true;try{process.kill(owner.pid,0);}catch(e){if(e.code==='ESRCH')alive=false;else throw e;}
109
+ ensure(!alive,'另一个迁移正在运行','LOCKED');fs.unlinkSync(file);
110
+ }
111
+ const fd=fs.openSync(file,'wx',0o600);fs.writeFileSync(fd,json({pid:process.pid,host:os.hostname()}));fs.fsyncSync(fd);fs.closeSync(fd);
112
+ try{return fn();}finally{fs.unlinkSync(file);}
113
+ }
114
+ function observed(root) {const tree=inventory(root);delete tree['.yss-harness-migrate.lock'];return tree;}
115
+ function currentMatches(root,plan) {ensure(digest(observed(root))===digest(plan.inputs) && digest(gitStamp(root))===digest(plan.git),'计划后项目或 Git 输入已变化,请重新规划','STALE_PLAN');}
116
+ function record(root,idx,run,phase) {run.phase=phase;durable(root,INDEX,json(idx));durable(run.archive,'receipt.json',json(run));}
117
+ function loadArchived(root,adapter,run) {
118
+ outside(root,run.archive);targetPath(run.archive);
119
+ const plan=JSON.parse(readFile(safe(run.archive,'plan.json')));checkPlan(adapter,plan);
120
+ ensure(plan.id===run.id && plan.planDigest===run.planDigest && plan.target===root && plan.archive===run.archive,'归档与回执不一致','STATE');
121
+ const journalRef=safe(root,`.yss-harness-state/${adapter.family.side}/transactions/${plan.id}/journal.json`);
122
+ if(stat(journalRef)){const journal=JSON.parse(readFile(journalRef));const shape=ops=>ops.map(({path,before,after})=>({path,before,after}));ensure(journal.id===plan.id && journal.profileId===adapter.family.profileId && digest(shape(journal.operations))===digest(shape(plan.operations)),'归档操作与原事务不一致','ARCHIVE');}
123
+ else ensure(run.phase==='applying','原事务日志缺失','STATE');
124
+ return plan;
125
+ }
126
+ function afterMatches(root,operations) {for(const op of operations)ensure(same(descriptor(root,op.path),op.after),`升级后已有修改: ${op.path}`,'CONCURRENT');}
127
+ function unchangedInputs(root,plan) {
128
+ const changed=new Set(plan.operations.map(o=>o.path));
129
+ const select=tree=>Object.fromEntries(Object.entries(tree).filter(([ref,d])=>!changed.has(ref) && !(d.type==='directory' && [...changed].some(p=>p.startsWith(ref+'/')))));
130
+ ensure(digest(select(observed(root)))===digest(select(plan.inputs)) && digest(gitStamp(root))===digest(plan.git),'迁移期间未写入资产或 Git 状态变化','CONCURRENT');
131
+ }
132
+ function archivedBefore(plan) {
133
+ return plan.operations.map((op,i)=>{
134
+ let bytes;
135
+ if(op.before){bytes=readFile(safe(plan.archive,`before/${i}`));ensure(hash(bytes)===op.before.digest,'备份摘要损坏','ARCHIVE');}
136
+ return {path:op.path,before:op.after,after:op.before,bytes};
137
+ });
138
+ }
139
+ function closeInterrupted(root,adapter,idx) {
140
+ const pending=state(root,adapter);
141
+ const validate=()=>{targetPath(root);ensure(!METADATA.some(ref=>ref!==adapter.family.metadataFile && stat(safe(root,ref))),'异族身份禁止恢复','IDENTITY');};
142
+ validate();
143
+ if(pending.pending.length)recoverTransaction(root,adapter.family,pending,validate);
144
+ for(const run of idx.runs.filter(r=>['applying','rolling-back'].includes(r.phase))) {
145
+ const plan=loadArchived(root,adapter,run);
146
+ const journalPath=safe(root,`.yss-harness-state/${adapter.family.side}/transactions/${run.phase==='rolling-back'?run.rollbackId:run.id}/journal.json`);
147
+ const journal=stat(journalPath)?JSON.parse(readFile(journalPath)):null;
148
+ const succeeded=journal?.phase==='committed';
149
+ if(succeeded)afterMatches(root,run.phase==='rolling-back'?plan.operations.map(o=>({...o,after:o.before})):plan.operations);
150
+ record(root,idx,run,run.phase==='rolling-back'?(succeeded?'rolled-back':'applied'):(succeeded?'applied':'recovered'));
151
+ }
152
+ }
153
+ export function migration(adapter,command,opts={}) {
154
+ let plan;
155
+ if(command==='apply'){ensure(opts.plan,'apply 需要 --plan','ARGS');plan=JSON.parse(readFile(path.resolve(opts.plan)));checkPlan(adapter,plan);}
156
+ const root=targetPath(opts.targetDir || plan?.target || process.cwd());
157
+ ensure(stat(root)?.isDirectory(),'项目目录不存在','PATH');
158
+ ensure(!plan || plan.target===root,'计划与目标目录不一致','IDENTITY');
159
+ if(command==='plan') {
160
+ adapter.check(root,opts);
161
+ ensure(!state(root,adapter).pending.length && !index(root,adapter).runs.some(r=>['applying','rolling-back'].includes(r.phase)),'请先 recover 未完成迁移','INTERRUPTED');
162
+ const output=outside(root,path.resolve(opts.output || ''));
163
+ ensure(opts.output && !stat(output),'--output 必须是项目外全新文件','PATH');
164
+ const inputs=observed(root), git=gitStamp(root), id=randomUUID(),createdAt=new Date().toISOString();
165
+ const archive=outside(root,path.resolve(opts.archiveDir || path.join(os.homedir(),'.yss-harness/archives',hash(root+'\0'+adapter.family.packageName),id)));
166
+ ensure(!stat(archive),'归档目录必须全新','ARCHIVE');
167
+ const resolutions=opts.resolutions?JSON.parse(readFile(path.resolve(opts.resolutions))):{};
168
+ ensure(resolutions && typeof resolutions==='object' && !Array.isArray(resolutions),'冲突决议必须为路径映射','PLAN');
169
+ const settings={prune:Boolean(opts.prune),migrateLayout:Boolean(opts.migrateLayout),resolutions};
170
+ let prepared;try{prepared=adapter.prepare(root,settings,id,createdAt);}catch(e){if(e.code==='CONFLICT' || e.result?.conflicts?.length)e.result={...e.result,status:'blocked'};throw e;}
171
+ const operations=encode(prepared.operations);validateOperations(root,adapter,operations);
172
+ const body={schemaVersion:1,id,createdAt,target:root,family:adapter.family.packageName,executor:adapter.fingerprint,template:adapter.template,archive,settings,inputs,git,operations,changes:prepared.changes || [],rules:['managed-sync.v1',...(settings.migrateLayout?['layout-migration.v1']:[]),...(settings.prune?['retirement.v1']:[])]};
173
+ plan={...body,planDigest:digest(body)};currentMatches(root,plan);
174
+ durable(path.dirname(output),path.basename(output),json(plan));
175
+ return {schemaVersion:1,command:'migrate plan',status:'planned',target:root,plan:output,planDigest:plan.planDigest,changes:plan.changes,operations:operations.length,archive};
176
+ }
177
+ const idx=index(root,adapter);
178
+ if(command==='status'){adapter.check(root,{migrateLayout:true});return {schemaVersion:1,command:'migrate status',status:'ok',target:root,transactions:idx.runs,pending:state(root,adapter).pending.map(x=>x.journal.id)};}
179
+ if(command==='recover') {
180
+ // Preview uses existing journal validation, including drift and archive checks, without creating state.
181
+ const pending=state(root,adapter);recoveryPreview(root,adapter.family,pending);for(const run of idx.runs.filter(r=>['applying','rolling-back'].includes(r.phase))){const p=loadArchived(root,adapter,run);archivedBefore(p);}
182
+ if(!opts.apply)return {schemaVersion:1,status:'preview',target:root,transactions:idx.runs.filter(r=>['applying','rolling-back'].includes(r.phase)),pending:pending.pending.map(x=>x.journal.id)};
183
+ return lock(root,()=>{closeInterrupted(root,adapter,idx);return {schemaVersion:1,status:'recovered',target:root,transactions:idx.runs};});
184
+ }
185
+ ensure(['apply','rollback'].includes(command),'未知 migrate 子命令','ARGS');
186
+ if(command==='rollback' && !opts.apply) {
187
+ ensure(!state(root,adapter).pending.length,'请先 recover 未完成事务','INTERRUPTED');
188
+ const run=idx.runs.at(-1);ensure(run?.phase==='applied','没有可回退的最近成功迁移','STATE');
189
+ const p=loadArchived(root,adapter,run);afterMatches(root,p.operations);const inverse=archivedBefore(p);
190
+ return {schemaVersion:1,status:'preview',target:root,id:run.id,paths:inverse.map(o=>o.path)};
191
+ }
192
+ return lock(root,()=>{
193
+ ensure(!state(root,adapter).pending.length && !idx.runs.some(r=>['applying','rolling-back'].includes(r.phase)),'请先 recover 未完成迁移','INTERRUPTED');
194
+ if(command==='rollback') {
195
+ const run=idx.runs.at(-1);ensure(run?.phase==='applied','没有可回退的最近成功迁移','STATE');
196
+ plan=loadArchived(root,adapter,run);afterMatches(root,plan.operations);const inverse=archivedBefore(plan);
197
+ if(!opts.apply)return {schemaVersion:1,status:'preview',target:root,id:run.id,paths:inverse.map(o=>o.path)};
198
+ run.rollbackId=randomUUID();record(root,idx,run,'rolling-back');
199
+ try{applyTransaction(root,adapter.family,inverse,op=>{if(op)guard(root,adapter,op.path);},run.rollbackId,false,()=>afterMatches(root,inverse));record(root,idx,run,'rolled-back');}
200
+ catch(e){closeInterrupted(root,adapter,idx);throw e;}
201
+ return {schemaVersion:1,status:'rolled-back',target:root,id:run.id,archive:run.archive};
202
+ }
203
+ const existing=idx.runs.find(r=>r.id===plan.id);
204
+ if(existing){ensure(existing.phase==='applied' && existing.planDigest===plan.planDigest,'计划已执行或恢复,请重新规划','STATE');afterMatches(root,plan.operations);return {schemaVersion:1,status:'applied',target:root,reused:true,...existing};}
205
+ adapter.check(root,plan.settings);currentMatches(root,plan);
206
+ // Regenerate from the fixed executable and explicit resolutions: edited plans cannot add arbitrary writes.
207
+ const rebuilt=encode(adapter.prepare(root,plan.settings,plan.id,plan.createdAt).operations);
208
+ ensure(digest(rebuilt)===digest(plan.operations),'计划内容与固定执行器重新生成的结果不一致','PLAN');currentMatches(root,plan);
209
+ if(!plan.operations.length)return {schemaVersion:1,status:'unchanged',target:root};
210
+ ensure(!stat(plan.archive),'归档目录已存在','ARCHIVE');fs.mkdirSync(plan.archive,{recursive:true,mode:0o700});
211
+ durable(plan.archive,'plan.json',json(plan));
212
+ for(const [i,op]of plan.operations.entries())if(op.before){const bytes=readFile(safe(root,op.path));ensure(hash(bytes)===op.before.digest,'备份期间输入变化','CONCURRENT');durable(plan.archive,`before/${i}`,bytes,op.before.mode);}
213
+ if(plan.settings.migrateLayout || plan.settings.prune || Object.keys(plan.settings.resolutions).length)copyCandidate(root,path.join(plan.archive,'project-copy'),plan.inputs);
214
+ archivedBefore(plan);currentMatches(root,plan);
215
+ // applyTransaction owns its durable WAL. Create its state before writing our receipt index.
216
+ if(!stat(safe(root,'.yss-harness-state'))) {
217
+ fs.mkdirSync(safe(root,`.yss-harness-state/${adapter.family.side}/transactions`),{recursive:true});
218
+ durable(root,'.yss-harness-state/owner.json',json({schemaVersion:1,profileId:adapter.family.profileId}));
219
+ }
220
+ const run={schemaVersion:1,id:plan.id,planDigest:plan.planDigest,archive:plan.archive,family:plan.family,createdAt:plan.createdAt,template:plan.template,executor:plan.executor,phase:'applying'};idx.runs.push(run);record(root,idx,run,'applying');
221
+ try {
222
+ const ops=plan.operations.map(op=>({...op,bytes:op.after?Buffer.from(op.contentBase64,'base64'):undefined}));
223
+ applyTransaction(root,adapter.family,ops,op=>{if(op)guard(root,adapter,op.path);else unchangedInputs(root,plan);},plan.id,false,()=>{const checks=adapter.verify(root);afterMatches(root,plan.operations);unchangedInputs(root,plan);run.verification={status:'passed',checks:checks || [],completedAt:new Date().toISOString()};});
224
+ record(root,idx,run,'applied');
225
+ }catch(e){closeInterrupted(root,adapter,idx);throw e;}
226
+ return {schemaVersion:1,status:'applied',target:root,...run};
227
+ });
228
+ }
229
+ export function parseMigration(argv) {
230
+ const args=[...argv],command=args.shift() || 'status',opts={};
231
+ ensure(['plan','apply','status','recover','rollback'].includes(command),'migrate plan|apply|status|recover|rollback','ARGS');
232
+ const values={'--target-dir':'targetDir','--output':'output','--plan':'plan','--archive-dir':'archiveDir','--resolutions':'resolutions'};
233
+ const flags={'--apply':'apply','--prune':'prune','--migrate-layout':'migrateLayout','--json':'json'};
234
+ const allowed={plan:['targetDir','output','archiveDir','resolutions','prune','migrateLayout','json'],apply:['targetDir','plan','json'],status:['targetDir','json'],recover:['targetDir','apply','json'],rollback:['targetDir','apply','json']};
235
+ while(args.length){const flag=args.shift(),key=values[flag] || flags[flag];ensure(key && allowed[command].includes(key),`不适用的参数: ${flag}`,'ARGS');ensure(!(key in opts),`重复参数: ${flag}`,'ARGS');if(values[flag]){ensure(args.length && !args[0].startsWith('--'),`参数缺少值: ${flag}`,'ARGS');opts[key]=args.shift();}else opts[key]=true;}
236
+ return {command,opts};
237
+ }
@@ -0,0 +1,19 @@
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {loadBundle} from './bundle.mjs';
4
+ import {identity} from './identity.mjs';
5
+ import {execute} from './engine.mjs';
6
+ import {verifyInstance} from './verification.mjs';
7
+ import {hash,ensure} from './io.mjs';
8
+ import {migration,parseMigration,assertUpgradeVersion} from './migrate.mjs';
9
+ export function dedicatedAdapter(packageRoot) {
10
+ const bundle=loadBundle(packageRoot);
11
+ const fingerprint=hash(JSON.stringify({pkg:bundle.pkg,snapshot:bundle.snapshot,core:bundle.core,driver:hash(fs.readFileSync(new URL('./migrate.mjs',import.meta.url)))}));
12
+ return {
13
+ family:bundle.family,fingerprint,template:{version:bundle.pkg.version,commit:bundle.snapshot.templateCommit,snapshotHash:bundle.snapshot.snapshotHash},
14
+ check(root,opts){ensure(root!==packageRoot && !root.startsWith(packageRoot+path.sep) && !packageRoot.startsWith(root+path.sep),'目标不得覆盖 CLI 安装目录','PATH');const meta=identity(root,bundle,'sync',opts.migrateLayout);ensure(meta,'migrate 仅接受已有同家族实例','IDENTITY');assertUpgradeVersion(meta.cliVersion,bundle.pkg.version);},
15
+ prepare(root,opts,id,time){return execute(bundle,root,{command:'sync',apply:true,prune:opts.prune,migrateLayout:opts.migrateLayout,migrationResolutions:opts.resolutions,migrationId:id,migrationTime:time,captureMigration:(operations,result)=>({operations,changes:result.changes})});},
16
+ verify(root){return verifyInstance(bundle,root);},
17
+ };
18
+ }
19
+ export function runMigration(packageRoot,args){const {command,opts}=parseMigration(args);return migration(dedicatedAdapter(packageRoot),command,opts);}
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@yss-internal/dedicated-harness-cli-core",
3
+ "version": "0.2.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22 <27"
8
+ },
9
+ "packageManager": "pnpm@10.15.0",
10
+ "scripts": {
11
+ "test": "node --test tests/*.test.mjs"
12
+ }
13
+ }
@@ -0,0 +1,117 @@
1
+ // Maintainer entry: create only an empty thin CLI working directory.
2
+ import * as fs from "node:fs";
3
+ import path from "node:path";
4
+ import { ensure, write, json, stat } from "./io.mjs";
5
+ const [side, output] = process.argv.slice(2);
6
+ ensure(
7
+ ["backend", "frontend"].includes(side) && output,
8
+ "usage: node scaffold.mjs backend|frontend <empty-directory>",
9
+ );
10
+ const root = path.resolve(output),
11
+ name = `create-yss-harness-${side}`;
12
+ ensure(
13
+ !stat(root) || fs.readdirSync(root).length === 0,
14
+ "薄包目标必须不存在或为空",
15
+ );
16
+ write(
17
+ root,
18
+ "package.json",
19
+ json({
20
+ name,
21
+ version: "0.1.0",
22
+ description: `创建和同步 YSS ${side} 专职 Harness 治理项目`,
23
+ type: "module",
24
+ license: "UNLICENSED",
25
+ engines: { node: ">=22 <27" },
26
+ packageManager: "pnpm@10.15.0",
27
+ repository: {
28
+ type: "git",
29
+ url: `git+https://github.com/iloveZzz/${name}.git`,
30
+ },
31
+ bin: { [name]: `bin/${name}.js` },
32
+ files: [
33
+ "bin",
34
+ "config",
35
+ "vendor/cli-core",
36
+ "cli-core.lock.json",
37
+ "template",
38
+ "template.manifest.json",
39
+ "template.snapshot.json",
40
+ ],
41
+ scripts: {
42
+ "sync-core": "node scripts/sync-core.mjs",
43
+ "sync-template": "node scripts/sync-template.mjs",
44
+ "verify-bundle": "node scripts/verify-bundle.mjs",
45
+ test: "node --test tests/*.test.mjs",
46
+ prepack: "node scripts/verify-bundle.mjs",
47
+ },
48
+ }),
49
+ );
50
+ write(
51
+ root,
52
+ `bin/${name}.js`,
53
+ `#!/usr/bin/env node\nimport {fileURLToPath} from 'node:url';\nimport {main} from '../vendor/cli-core/cli.mjs';\nawait main(fileURLToPath(new URL('..',import.meta.url)));\n`,
54
+ 0o755,
55
+ );
56
+ for (const kind of ["core", "template"])
57
+ write(
58
+ root,
59
+ `scripts/sync-${kind}.mjs`,
60
+ `import {fileURLToPath} from 'node:url';\nimport {sync${kind[0].toUpperCase() + kind.slice(1)}} from '../vendor/cli-core/build.mjs';\nconst args=process.argv.slice(2),check=args.includes('--check'),values=args.filter(x=>x!=='--check');\nif(values.length!==2)throw new Error('usage: pnpm sync-${kind} <source-repo> <commit> [--check]');\nconsole.log(JSON.stringify(sync${kind[0].toUpperCase() + kind.slice(1)}(values[0],values[1],fileURLToPath(new URL('..',import.meta.url)),check),null,2));\n`,
61
+ );
62
+ write(
63
+ root,
64
+ "scripts/verify-bundle.mjs",
65
+ `import {fileURLToPath} from 'node:url';\nimport {verifyCore} from '../vendor/cli-core/build.mjs';\nimport {loadBundle} from '../vendor/cli-core/bundle.mjs';\nconst root=fileURLToPath(new URL('..',import.meta.url));\nconst core=verifyCore(root),bundle=loadBundle(root);\nconsole.log(JSON.stringify({package:bundle.pkg.name,core:core.digest,template:bundle.snapshot.snapshotHash,files:bundle.files.size}));\n`,
66
+ );
67
+ write(
68
+ root,
69
+ "tests/package.test.mjs",
70
+ `import {test} from 'node:test';
71
+ import assert from 'node:assert/strict';
72
+ import * as fs from 'node:fs';
73
+ import os from 'node:os';
74
+ import path from 'node:path';
75
+ import {fileURLToPath} from 'node:url';
76
+ import {spawnSync} from 'node:child_process';
77
+ const root=fileURLToPath(new URL('..',import.meta.url));
78
+ test('固定包契约、真实模板计划零写入和旧实例拒绝',t=>{
79
+ const scratch=fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(),'${name}-')));t.after(()=>fs.rmSync(scratch,{recursive:true,force:true}));
80
+ const target=path.join(scratch,'project');
81
+ const run=(...args)=>spawnSync(process.execPath,[path.join(root,'bin/${name}.js'),...args,'--json'],{encoding:'utf8',maxBuffer:32*1024*1024});
82
+ const verify=spawnSync(process.execPath,[path.join(root,'scripts/verify-bundle.mjs')],{encoding:'utf8'});assert.equal(verify.status,0,verify.stderr);
83
+ let r=run('init','--target-dir',target,'--project-name','验收实例','--dry-run');assert.equal(r.status,0,r.stderr);
84
+ const plan=JSON.parse(r.stdout);assert.equal(plan.status,'preview');assert.equal(fs.existsSync(target),false);
85
+ assert.ok(plan.changes.some(x=>x.path==='scripts/repository-mode'));assert.ok(plan.changes.some(x=>x.path==='.template-spec/process/harness-profile.yaml'));assert.ok(!plan.changes.some(x=>x.path==='scripts/instantiate-harness'));
86
+ fs.mkdirSync(target);const metadata=path.join(target,'.yss-harness-${side}.json');fs.writeFileSync(metadata,'{"schema_version":1}');
87
+ r=run('attach','--target-dir',target,'--apply','--force');assert.equal(r.status,1);assert.equal(JSON.parse(r.stdout).code,'LEGACY');assert.deepEqual(fs.readdirSync(target),['.yss-harness-${side}.json']);
88
+ });
89
+ `,
90
+ );
91
+ write(
92
+ root,
93
+ ".github/workflows/ci.yml",
94
+ `name: CLI\non: [push, pull_request, workflow_dispatch]\njobs:\n verify:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n node: [22, 24, 26]\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: \u0024{{ matrix.node }}\n - uses: pnpm/action-setup@v4\n with:\n version: 10.15.0\n - run: pnpm test\n - run: pnpm verify-bundle\n - run: npm pack\n - run: |\n mkdir -p /tmp/cli-consumer\n npm install --prefix /tmp/cli-consumer --ignore-scripts ./\u0024(npm pkg get name | tr -d '\"')-\u0024(npm pkg get version | tr -d '\"').tgz\n /tmp/cli-consumer/node_modules/.bin/${name} init --target-dir /tmp/harness-instance --project-name ci --json\n /tmp/cli-consumer/node_modules/.bin/${name} sync --target-dir /tmp/harness-instance --json\n`,
95
+ );
96
+ write(root, ".gitignore", "node_modules/\n*.tgz\n.DS_Store\n");
97
+ write(
98
+ root,
99
+ "yss-project.yaml",
100
+ "schema_version: 1\nrepository_mode: template-source\n",
101
+ );
102
+ write(
103
+ root,
104
+ "AGENTS.md",
105
+ "# CLI 维护入口\n\n本仓是模板工具链薄包,不生成产品 Spec / Ticket。公共实现权威位于综合模板 `.template-source/cli-core/`;`vendor/cli-core/` 和 `template/` 只通过同步脚本生成。修改身份时先更新来源 profile。日常 implementation-ready 可显式使用 `WORKTREE` 并在锁中记录来源状态;candidate/release 必须从完整提交重建。验证使用 `pnpm test`、`pnpm verify-bundle`,测试结束后才执行 `npm pack`。npm 发布和旧入口退役独立执行发布流程。\n",
106
+ );
107
+ write(
108
+ root,
109
+ "README.md",
110
+ `# ${name}\n\n创建、接入和同步 \`harness.${side}-delivery\` 的治理资产。CLI 包内包含固定模板与公共核心,init / attach / sync 离线运行。不会生成业务运行时代码。当前安装版本和 npm 发布状态分别以 \`${name} --version\` 与 registry 为准;本地候选可用 \`npm pack\` 后的实际 tgz 验收。\n\n\`\`\`sh\nnpx ${name}@latest init --target-dir ./my-project --project-name 我的项目\nnpx ${name}@latest attach --target-dir ./existing-project\nnpx ${name}@latest attach --target-dir ./existing-project --apply\nnpx ${name}@latest sync --target-dir ./my-project --plan --prune\nnpx ${name}@latest sync --target-dir ./my-project --apply --prune\nnpx ${name}@latest recover --target-dir ./my-project\n${name} update --dry-run\n\`\`\`\n\ninit 只接受不存在或空目录;\`--dry-run\` 不写入。attach / sync 默认预览,写入要求 \`--apply\`。治理冲突整次暂停,确认备份覆盖后可用 \`--apply --force\`。业务源码、构建文件、Git、子模块与越界路径不能强制接管。\n\n仓内旧脚本生成的实例不受支持,不转换旧 metadata 或重建基线;其他 Harness 家族同样拒绝。已有 metadata v2 实例使用同家族 sync。旧 \`docs/\` 治理文件迁移需显式 \`--migrate-layout\`,先用 \`--plan\` 审阅,再用 \`--apply\`;项目 tracker 保留原文,旧受管文件缺可信基线或已修改时阻断。普通 sync 保留退出分发文件;显式 \`--prune\` 只删除内容和 mode 仍等于可信旧 baseline 的文件,用户修改过的旧副本继续保留并报告。\n\n参数:\`--project-name\`、\`--business-domain\`、\`--team-size\`、\`--issue-tracker local-markdown|github|gitlab\`、\`--git-init\`(仅 init)、\`--include-example-docs\` / \`--no-example-docs\`、\`--json\`。程序升级命令 \`update\` / \`upgrade\` 只更新 CLI;npx、源码或未知安装方式输出安装指引。\n\n每次应用保存 \`.yss-harness-state/${side}/transactions/<id>/journal.json\` 和原文件备份。失败自动恢复;进程中断后先用 \`recover\` 诊断,带 \`--apply\` 才执行恢复。恢复遇到后续修改会停下并保留恢复清单。成功事务保留备份,但不提供历史 rollback。状态目录应由项目自行排除 Git;CLI 不改已有 Git 配置。\n\nJSON 协议版本 1:成功返回 \`preview\`、\`applied\`、\`recovered\`;错误退出码 1,含 \`code\` / \`message\`,冲突含完整 \`changes\` / \`conflicts\`。存在普通可应用更新的预览退出码为 0。\n\n维护:日常 implementation-ready 可运行 \`pnpm sync-core <source-repo> WORKTREE\` 和 \`pnpm sync-template <source-repo> WORKTREE\`,来源状态会写入锁和快照;candidate/release 必须改用完整 40 位提交。加 \`--check\` 只核验。\`pnpm test\` 完成后依次 \`pnpm verify-bundle\`、\`npm pack\` 和干净目录安装验收。来源锁和 blob 编码随包携带;不要手改生成文件。\n`,
111
+ );
112
+ write(
113
+ root,
114
+ "LICENSE",
115
+ "UNLICENSED\n\n本包遵循现有 YSS CLI 的分发许可标识。随包第三方资产保留各自的许可证与来源声明。\n",
116
+ );
117
+ console.log(root);