create-yss-spec 2.1.15 → 2.2.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/README.md +6 -2
- package/package.json +1 -1
- package/src/cli.js +276 -15
- package/template/.agents/skills/cross-repo-implementation-routing/SKILL.md +2 -2
- package/template/.agents/skills/implementation-repo-onboarding/SKILL.md +5 -5
- package/template/.agents/skills/llm-wiki/SKILL.md +1 -1
- package/template/.agents/skills/llm-wiki/references/discover.md +1 -0
- package/template/.agents/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
- package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
- package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
- package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +2 -0
- package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.agents/skills/yss-router/SKILL.md +1 -1
- package/template/.agents/skills/yss-router/references/router-contract.yaml +20 -0
- package/template/.agents/skills/yss-router/references/slice-implementation-contract.md +1 -1
- package/template/.claude/skills/cross-repo-implementation-routing/SKILL.md +2 -2
- package/template/.claude/skills/implementation-repo-onboarding/SKILL.md +5 -5
- package/template/.claude/skills/llm-wiki/SKILL.md +1 -1
- package/template/.claude/skills/llm-wiki/references/discover.md +1 -0
- package/template/.claude/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
- package/template/.claude/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
- package/template/.claude/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
- package/template/.claude/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.claude/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
- package/template/.claude/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
- package/template/.claude/skills/yss-product-lifecycle/references/orchestration.md +2 -0
- package/template/.claude/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.claude/skills/yss-router/SKILL.md +1 -1
- package/template/.claude/skills/yss-router/references/router-contract.yaml +20 -0
- package/template/.claude/skills/yss-router/references/slice-implementation-contract.md +1 -1
- package/template/.codex/skills/cross-repo-implementation-routing/SKILL.md +2 -2
- package/template/.codex/skills/implementation-repo-onboarding/SKILL.md +5 -5
- package/template/.codex/skills/llm-wiki/SKILL.md +1 -1
- package/template/.codex/skills/llm-wiki/references/discover.md +1 -0
- package/template/.codex/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
- package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
- package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
- package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +2 -0
- package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.codex/skills/yss-router/SKILL.md +1 -1
- package/template/.codex/skills/yss-router/references/router-contract.yaml +20 -0
- package/template/.codex/skills/yss-router/references/slice-implementation-contract.md +1 -1
- package/template/.cursor/skills/cross-repo-implementation-routing/SKILL.md +2 -2
- package/template/.cursor/skills/implementation-repo-onboarding/SKILL.md +5 -5
- package/template/.cursor/skills/llm-wiki/SKILL.md +1 -1
- package/template/.cursor/skills/llm-wiki/references/discover.md +1 -0
- package/template/.cursor/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
- package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
- package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
- package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +2 -0
- package/template/.cursor/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.cursor/skills/yss-router/SKILL.md +1 -1
- package/template/.cursor/skills/yss-router/references/router-contract.yaml +20 -0
- package/template/.cursor/skills/yss-router/references/slice-implementation-contract.md +1 -1
- package/template/.hermes/skills/cross-repo-implementation-routing/SKILL.md +2 -2
- package/template/.hermes/skills/implementation-repo-onboarding/SKILL.md +5 -5
- package/template/.hermes/skills/llm-wiki/SKILL.md +1 -1
- package/template/.hermes/skills/llm-wiki/references/discover.md +1 -0
- package/template/.hermes/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
- package/template/.hermes/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
- package/template/.hermes/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
- package/template/.hermes/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.hermes/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
- package/template/.hermes/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
- package/template/.hermes/skills/yss-product-lifecycle/references/orchestration.md +2 -0
- package/template/.hermes/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.hermes/skills/yss-router/SKILL.md +1 -1
- package/template/.hermes/skills/yss-router/references/router-contract.yaml +20 -0
- package/template/.hermes/skills/yss-router/references/slice-implementation-contract.md +1 -1
- package/template/.pi/skills/cross-repo-implementation-routing/SKILL.md +2 -2
- package/template/.pi/skills/implementation-repo-onboarding/SKILL.md +5 -5
- package/template/.pi/skills/llm-wiki/SKILL.md +1 -1
- package/template/.pi/skills/llm-wiki/references/discover.md +1 -0
- package/template/.pi/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
- package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
- package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
- package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +2 -0
- package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.pi/skills/yss-router/SKILL.md +1 -1
- package/template/.pi/skills/yss-router/references/router-contract.yaml +20 -0
- package/template/.pi/skills/yss-router/references/slice-implementation-contract.md +1 -1
- package/template/.qoder/skills/cross-repo-implementation-routing/SKILL.md +2 -2
- package/template/.qoder/skills/implementation-repo-onboarding/SKILL.md +5 -5
- package/template/.qoder/skills/llm-wiki/SKILL.md +1 -1
- package/template/.qoder/skills/llm-wiki/references/discover.md +1 -0
- package/template/.qoder/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
- package/template/.qoder/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
- package/template/.qoder/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
- package/template/.qoder/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.qoder/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
- package/template/.qoder/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
- package/template/.qoder/skills/yss-product-lifecycle/references/orchestration.md +2 -0
- package/template/.qoder/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.qoder/skills/yss-router/SKILL.md +1 -1
- package/template/.qoder/skills/yss-router/references/router-contract.yaml +20 -0
- package/template/.qoder/skills/yss-router/references/slice-implementation-contract.md +1 -1
- package/template/.trae/skills/cross-repo-implementation-routing/SKILL.md +2 -2
- package/template/.trae/skills/implementation-repo-onboarding/SKILL.md +5 -5
- package/template/.trae/skills/llm-wiki/SKILL.md +1 -1
- package/template/.trae/skills/llm-wiki/references/discover.md +1 -0
- package/template/.trae/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
- package/template/.trae/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
- package/template/.trae/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
- package/template/.trae/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.trae/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
- package/template/.trae/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
- package/template/.trae/skills/yss-product-lifecycle/references/orchestration.md +2 -0
- package/template/.trae/skills/yss-product-lifecycle/references/state-model.md +1 -1
- package/template/.trae/skills/yss-router/SKILL.md +1 -1
- package/template/.trae/skills/yss-router/references/router-contract.yaml +20 -0
- package/template/.trae/skills/yss-router/references/slice-implementation-contract.md +1 -1
- package/template/AGENTS.md +5 -5
- package/template/CONTEXT.md +3 -2
- package/template/docs/process/implementation-repo-integration.md +24 -2
- package/template/docs/templates/cross-repo-slice-template.md +15 -0
- package/template/docs/templates/implementation-repo-registry-template.md +8 -3
- package/template/docs/templates/implementation-routing-template.md +3 -3
- package/template/docs/user-guide//344/272/247/345/223/201/347/224/237/345/221/275/345/221/250/346/234/237/345/267/245/344/275/234/346/265/201.md +2 -2
- package/template/scripts/lib/git-submodule-fixtures.mjs +58 -0
- package/template/scripts/lib/repository-scope-policy.mjs +456 -0
- package/template/scripts/lib/scenario-checks.mjs +11 -1
- package/template/scripts/repository-scope-policy +16 -0
- package/template/scripts/verify-repository-scope-scenarios +239 -0
- package/template/scripts/verify-scaffold-generator-scenarios +54 -0
- package/template/scripts/verify-template +3 -2
- package/template/skills-lock.json +7 -7
- package/template.manifest.json +6 -5
- package/template.snapshot.json +4 -4
- package/template/.cursor/environment.json +0 -5
- package/template/.github/workflows/template-node-tooling.yml +0 -61
- package/template/wiki/.wiki-manifest.json +0 -390
- package/template/wiki/raw/AGENTS.md +0 -122
- package/template/wiki/raw/CONTEXT.md +0 -80
- package/template/wiki/raw/README.md +0 -130
- package/template/wiki/raw/adr-0002-repository-mode.md +0 -3
- package/template/wiki/raw/create-yss-spec-repository-mode-contract.md +0 -95
- package/template/wiki/raw/harness-process-tailoring.md +0 -64
- package/template/wiki/raw/implementation-repo-integration.md +0 -62
- package/template/wiki/raw/issue-tracker.md +0 -135
- package/template/wiki/raw/lifecycle-registry.yaml +0 -369
- package/template/wiki/raw/maintenance-intensity.yaml +0 -25
- package/template/wiki/raw/skills-lock-names.md +0 -93
- package/template/wiki/raw/skills-maintenance.md +0 -90
- package/template/wiki/raw/spec-template.md +0 -102
- package/template/wiki/raw/triage-labels.md +0 -15
- package/template/wiki/raw/vertical-slice-ticket-template.md +0 -132
- package/template/wiki/raw/yss-project.yaml +0 -2
- package/template/wiki/raw/yss-skill-registry.yaml +0 -510
- package/template/wiki/wiki/Agent/345/205/245/345/217/243/350/247/204/345/210/231.md +0 -21
- package/template/wiki/wiki/CLAUDE.md +0 -28
- package/template/wiki/wiki/Fresh/351/252/214/350/257/201/344/270/216/347/213/254/347/253/213/345/256/241/346/237/245.md +0 -19
- package/template/wiki/wiki/LLM Wiki.md +0 -30
- package/template/wiki/wiki/Matt/346/212/200/350/203/275/344/275/223/347/263/273.md +0 -21
- package/template/wiki/wiki/OpenAPI/345/245/221/347/272/246.md +0 -20
- package/template/wiki/wiki/SpecDelta.md +0 -19
- package/template/wiki/wiki/Spec/345/237/272/347/272/277.md +0 -22
- package/template/wiki/wiki/Ticket/344/270/216/346/265/201/347/250/213/347/212/266/346/200/201.md +0 -21
- package/template/wiki/wiki/YSS/345/267/245/347/250/213/346/212/200/350/203/275/344/275/223/347/263/273.md +0 -23
- package/template/wiki/wiki/YSS/350/267/257/347/224/261/344/270/216/345/220/210/345/220/214/347/274/226/350/257/221.md +0 -20
- package/template/wiki/wiki/concept-table.md +0 -9
- package/template/wiki/wiki/index.md +0 -41
- package/template/wiki/wiki/log.md +0 -51
- package/template/wiki/wiki//344/272/247/345/223/201/347/240/224/345/217/221/347/224/237/345/221/275/345/221/250/346/234/237.md +0 -20
- package/template/wiki/wiki//344/272/247/345/223/201/350/256/276/350/256/241/345/275/261/345/223/215/344/270/216/345/216/237/345/236/213.md +0 -19
- package/template/wiki/wiki//344/273/223/345/272/223/350/272/253/344/273/275/344/270/216/350/267/257/347/224/261.md +0 -20
- package/template/wiki/wiki//345/210/207/347/211/207/345/256/236/347/216/260/345/220/210/345/220/214.md +0 -21
- package/template/wiki/wiki//345/236/202/347/233/264/345/210/207/347/211/207Ticket.md +0 -23
- package/template/wiki/wiki//345/244/215/347/233/230/344/270/216/346/235/203/345/250/201/350/265/204/344/272/247/344/277/256/350/256/242.md +0 -20
- package/template/wiki/wiki//345/256/236/347/216/260/344/273/223/345/272/223/344/270/216/350/267/250/344/273/223/345/272/223/345/245/221/347/272/246.md +0 -22
- package/template/wiki/wiki//345/275/261/345/223/215/351/235/242/345/210/206/350/257/212/344/270/216/346/265/201/347/250/213/350/243/201/345/211/252.md +0 -20
- package/template/wiki/wiki//346/212/200/350/203/275/346/212/225/345/275/261/344/270/216/351/224/201/345/256/232.md +0 -23
- package/template/wiki/wiki//346/235/241/344/273/266/345/274/272/345/210/266/351/227/250/347/246/201.md +0 -18
- package/template/wiki/wiki//346/250/241/346/235/277/345/217/221/345/270/203/351/227/250/347/246/201/344/270/216/351/252/214/350/257/201.md +0 -22
- package/template/wiki/wiki//346/250/241/346/235/277/346/200/273/350/247/210.md +0 -20
- package/template/wiki/wiki//346/250/241/346/235/277/347/273/264/346/212/244/346/265/201/347/250/213.md +0 -19
package/README.md
CHANGED
|
@@ -44,8 +44,10 @@ npx create-yss-spec@latest --version
|
|
|
44
44
|
- 接管 / 升级时迁移 Spec / Ticket 路径并删除 `to-prd`、`to-issues` 旧 skill
|
|
45
45
|
- 旧、新资产内容冲突或清单 schema / mode 非法时 fail closed
|
|
46
46
|
- 保留模板的共享 skill 投影;生成实例可在尚未 `git init` 时运行模板校验
|
|
47
|
+
- init / sync 不把模板源治理笔记、wiki、审查证据、源仓 CI / Cloud 环境和公开发布清单写入项目实例;attach 会带上 `yss-public-skills.json` 供 `verify-template` 使用
|
|
48
|
+
- 空 gitlink / detached HEAD / git-submodule 挂载点 fail closed,`--force` 也不能覆盖
|
|
47
49
|
|
|
48
|
-
本次模板适配按 `2.
|
|
50
|
+
本次模板适配按 `2.2.0` 发布,绑定 `yss-spec-project-template@04a6151`。`templateCommit` 会写入实例 metadata;“最新模板”指用户执行的 `npx create-yss-spec@latest` 所携带的最新已发布快照,CLI 运行时不会拉取模板仓库。
|
|
49
51
|
|
|
50
52
|
## 接管已有项目
|
|
51
53
|
|
|
@@ -72,6 +74,7 @@ npx create-yss-spec@latest attach \
|
|
|
72
74
|
- 根规则文件等受管冲突默认阻断;`--force` 才覆盖,并把被覆盖文件备份到目标目录外的临时目录。
|
|
73
75
|
- 合法 `template-source` 身份会转换为 `project-instance`;非法身份、迁移冲突和无法判断归属的扁平 Ticket 始终阻断。
|
|
74
76
|
- Git worktree 有脏改动时只提醒,不自动 stash 或提交。
|
|
77
|
+
- `.gitmodules`、gitlink 和 `apps/` 挂载工作树是用户资产;空 gitlink / detached HEAD 即使 `--force` 也阻断。
|
|
75
78
|
|
|
76
79
|
## 同步已有模板实例仓库
|
|
77
80
|
|
|
@@ -96,7 +99,8 @@ npx create-yss-spec@latest sync --dry-run
|
|
|
96
99
|
- 对模板已删除文件只报告,不自动删除
|
|
97
100
|
- 对已知的 Spec / Ticket 旧路径执行一次性迁移
|
|
98
101
|
- 迁移目标已存在且内容不一致时停止,不静默覆盖
|
|
99
|
-
-
|
|
102
|
+
- `.gitmodules`、gitlink(mode `160000`)和 `apps/` 下已挂载实现仓是用户资产,不创建、不覆盖、不删除
|
|
103
|
+
- init / sync 保持瘦实例:不复制 `scripts/`、`wiki/`、`.github/`、`.nvmrc`、`.gitignore`、`.template-source/`;attach 结束后会执行 `scripts/sync-skills --check`、`scripts/update-skill-lock --check` 和 `scripts/verify-template`,门禁失败会回滚文件和 metadata
|
|
100
104
|
|
|
101
105
|
```bash
|
|
102
106
|
npx create-yss-spec@latest sync --target-dir . --dry-run
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -39,11 +39,21 @@ const AGENT_SKILL_ROOTS = [
|
|
|
39
39
|
".agents/skills",
|
|
40
40
|
".claude/skills",
|
|
41
41
|
".codex/skills",
|
|
42
|
+
".cursor/skills",
|
|
42
43
|
".hermes/skills",
|
|
43
44
|
".pi/skills",
|
|
44
45
|
".qoder/skills",
|
|
45
46
|
".trae/skills",
|
|
46
47
|
];
|
|
48
|
+
const GITLINK_MODE = "160000";
|
|
49
|
+
const UNMANAGED_USER_PATHS = new Set([".gitmodules"]);
|
|
50
|
+
const INSTANCE_FORBIDDEN_PATHS = [
|
|
51
|
+
".template-source",
|
|
52
|
+
".github",
|
|
53
|
+
".cursor/environment.json",
|
|
54
|
+
"wiki",
|
|
55
|
+
"docs/reviews",
|
|
56
|
+
];
|
|
47
57
|
const LEGACY_SKILL_MAPPINGS = [
|
|
48
58
|
["to-prd", "to-spec"],
|
|
49
59
|
["to-issues", "to-tickets"],
|
|
@@ -301,6 +311,235 @@ function isInsideTemplateRoot(targetDir) {
|
|
|
301
311
|
);
|
|
302
312
|
}
|
|
303
313
|
|
|
314
|
+
function posixRelative(from, to) {
|
|
315
|
+
return path.relative(from, to).split(path.sep).join("/");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function collectGitRoots(start) {
|
|
319
|
+
const roots = [];
|
|
320
|
+
if (!start) {
|
|
321
|
+
return roots;
|
|
322
|
+
}
|
|
323
|
+
let current = path.resolve(start);
|
|
324
|
+
while (true) {
|
|
325
|
+
if (
|
|
326
|
+
fs.existsSync(path.join(current, ".git")) ||
|
|
327
|
+
fs.existsSync(path.join(current, ".gitmodules"))
|
|
328
|
+
) {
|
|
329
|
+
roots.push(current);
|
|
330
|
+
}
|
|
331
|
+
const parent = path.dirname(current);
|
|
332
|
+
if (parent === current) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
current = parent;
|
|
336
|
+
}
|
|
337
|
+
return roots;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseGitmodules(content) {
|
|
341
|
+
const modules = [];
|
|
342
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
343
|
+
return modules;
|
|
344
|
+
}
|
|
345
|
+
let current = null;
|
|
346
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
347
|
+
const line = rawLine.trim();
|
|
348
|
+
const name = line.match(/^\[submodule "(.+)"\]$/);
|
|
349
|
+
if (name) {
|
|
350
|
+
current = { name: name[1], path: "", url: "" };
|
|
351
|
+
modules.push(current);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (!current) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const pathMatch = line.match(/^path\s*=\s*(.+)$/);
|
|
358
|
+
if (pathMatch) {
|
|
359
|
+
current.path = pathMatch[1].trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
360
|
+
}
|
|
361
|
+
const urlMatch = line.match(/^url\s*=\s*(.+)$/);
|
|
362
|
+
if (urlMatch) {
|
|
363
|
+
current.url = urlMatch[1].trim();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return modules;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function readGitmodules(repoRoot) {
|
|
370
|
+
const file = path.join(repoRoot, ".gitmodules");
|
|
371
|
+
if (!fs.existsSync(file) || pathKind(file) !== "file") {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
return parseGitmodules(fs.readFileSync(file, "utf8"));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function gitLsFilesStage(repoRoot, relativePath) {
|
|
378
|
+
const result = spawnSync(
|
|
379
|
+
"git",
|
|
380
|
+
["-C", repoRoot, "ls-files", "--stage", "--", relativePath],
|
|
381
|
+
{ encoding: "utf8" },
|
|
382
|
+
);
|
|
383
|
+
if (result.status !== 0) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const line = (result.stdout || "").trim().split(/\r?\n/).filter(Boolean)[0];
|
|
387
|
+
if (!line) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const match = line.match(/^(\d+)\s+([0-9a-f]+)\s+(\d+)\s+(.+)$/i);
|
|
391
|
+
if (!match) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return { mode: match[1], sha: match[2], stage: match[3], path: match[4] };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function gitShowSuperproject(cwd) {
|
|
398
|
+
const result = spawnSync(
|
|
399
|
+
"git",
|
|
400
|
+
["-C", cwd, "rev-parse", "--show-superproject-working-tree"],
|
|
401
|
+
{ encoding: "utf8" },
|
|
402
|
+
);
|
|
403
|
+
if (result.status !== 0) {
|
|
404
|
+
return "";
|
|
405
|
+
}
|
|
406
|
+
return (result.stdout || "").trim();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function gitAbbrevRef(cwd) {
|
|
410
|
+
const result = spawnSync(
|
|
411
|
+
"git",
|
|
412
|
+
["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"],
|
|
413
|
+
{ encoding: "utf8" },
|
|
414
|
+
);
|
|
415
|
+
if (result.status !== 0) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
return (result.stdout || "").trim();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function isEmptyDir(dir) {
|
|
422
|
+
if (!fs.existsSync(dir)) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
return fs.readdirSync(dir).filter((name) => name !== "." && name !== "..")
|
|
427
|
+
.length === 0;
|
|
428
|
+
} catch {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function isGitSubmoduleMount(repoRoot, targetPathValue) {
|
|
434
|
+
const relative = posixRelative(repoRoot, path.resolve(targetPathValue));
|
|
435
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
if (readGitmodules(repoRoot).some((item) => item.path === relative)) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
const staged = gitLsFilesStage(repoRoot, relative);
|
|
442
|
+
return staged?.mode === GITLINK_MODE;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function inspectCheckoutState(repoRoot, targetPathValue) {
|
|
446
|
+
const resolved = path.resolve(targetPathValue);
|
|
447
|
+
const relative = posixRelative(path.resolve(repoRoot), resolved);
|
|
448
|
+
const inRepo = Boolean(relative) && !relative.startsWith("..");
|
|
449
|
+
const staged = inRepo ? gitLsFilesStage(repoRoot, relative) : null;
|
|
450
|
+
const listed = inRepo
|
|
451
|
+
? readGitmodules(repoRoot).some((item) => item.path === relative)
|
|
452
|
+
: false;
|
|
453
|
+
const superproject = fs.existsSync(path.join(resolved, ".git"))
|
|
454
|
+
? gitShowSuperproject(resolved)
|
|
455
|
+
: "";
|
|
456
|
+
const isMount = staged?.mode === GITLINK_MODE || listed || Boolean(superproject);
|
|
457
|
+
const hasGit = fs.existsSync(path.join(resolved, ".git"));
|
|
458
|
+
if (isMount && !hasGit) {
|
|
459
|
+
return isEmptyDir(resolved) ? "empty-gitlink" : "uninitialized";
|
|
460
|
+
}
|
|
461
|
+
if (isMount && hasGit) {
|
|
462
|
+
const ref = gitAbbrevRef(resolved);
|
|
463
|
+
if (ref === "HEAD") {
|
|
464
|
+
return "detached-head";
|
|
465
|
+
}
|
|
466
|
+
return "attached-branch";
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function gitlinkWriteViolation(targetDir, { force = false } = {}) {
|
|
472
|
+
const resolved = path.resolve(targetDir);
|
|
473
|
+
const roots = collectGitRoots(resolved);
|
|
474
|
+
for (const root of roots) {
|
|
475
|
+
if (path.resolve(root) === PACKAGE_ROOT) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
let current = resolved;
|
|
479
|
+
while (true) {
|
|
480
|
+
if (isGitSubmoduleMount(root, current)) {
|
|
481
|
+
const checkout = inspectCheckoutState(root, current);
|
|
482
|
+
if (checkout === "empty-gitlink" || checkout === "uninitialized") {
|
|
483
|
+
return "空 gitlink 不得当成普通目录写入;gitlink 不得由 CLI 覆盖";
|
|
484
|
+
}
|
|
485
|
+
if (checkout === "detached-head") {
|
|
486
|
+
return "detached HEAD 不得当成普通目录写入;gitlink 不得由 CLI 覆盖";
|
|
487
|
+
}
|
|
488
|
+
if (path.resolve(current) === resolved) {
|
|
489
|
+
return force
|
|
490
|
+
? "--force 不得把 git-submodule 挂载点当成普通目录覆盖"
|
|
491
|
+
: "git-submodule gitlink 不得由 CLI 覆盖;先 git submodule update --init,在子仓附加分支的工作树内操作,或改用独立目录";
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (path.resolve(current) === path.resolve(root)) {
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
const parent = path.dirname(current);
|
|
498
|
+
if (parent === current) {
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
current = parent;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function assertTargetWorkingTreeWritable(targetDir, { force = false } = {}) {
|
|
508
|
+
const violation = gitlinkWriteViolation(targetDir, { force });
|
|
509
|
+
if (violation) {
|
|
510
|
+
throw new Error(violation);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function unmanagedPathReason(targetDir, relativePath) {
|
|
515
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
516
|
+
if (UNMANAGED_USER_PATHS.has(normalized) || normalized.split("/")[0] === ".gitmodules") {
|
|
517
|
+
return ".gitmodules 是用户资产,不属于受管模板文件";
|
|
518
|
+
}
|
|
519
|
+
const absolutePath = path.resolve(targetDir, normalized);
|
|
520
|
+
const roots = collectGitRoots(targetDir);
|
|
521
|
+
for (const root of roots) {
|
|
522
|
+
if (path.resolve(root) === PACKAGE_ROOT) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
let current = absolutePath;
|
|
526
|
+
while (true) {
|
|
527
|
+
if (isGitSubmoduleMount(root, current)) {
|
|
528
|
+
return "gitlink / apps 挂载工作树是用户资产,不属于受管模板文件";
|
|
529
|
+
}
|
|
530
|
+
if (path.resolve(current) === path.resolve(root)) {
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
const parent = path.dirname(current);
|
|
534
|
+
if (parent === current) {
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
current = parent;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
304
543
|
function normalizeRelativePath(relativePath) {
|
|
305
544
|
return relativePath.split(path.sep).join("/");
|
|
306
545
|
}
|
|
@@ -861,6 +1100,14 @@ function classifyAttachPlan(targetDir, variables) {
|
|
|
861
1100
|
};
|
|
862
1101
|
|
|
863
1102
|
for (const operation of desiredOperations) {
|
|
1103
|
+
const unmanagedReason = unmanagedPathReason(targetDir, operation.relativePath);
|
|
1104
|
+
if (unmanagedReason) {
|
|
1105
|
+
plan.unsafe.push({
|
|
1106
|
+
...operation,
|
|
1107
|
+
reason: unmanagedReason,
|
|
1108
|
+
});
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
864
1111
|
const kind = pathKind(operation.targetPath);
|
|
865
1112
|
if (kind === "missing") {
|
|
866
1113
|
plan.missing.push(operation);
|
|
@@ -904,6 +1151,14 @@ function classifySyncPlan(targetDir, metadata, identity = readTargetIdentity(tar
|
|
|
904
1151
|
const unsafe = [];
|
|
905
1152
|
|
|
906
1153
|
for (const operation of desiredOperations) {
|
|
1154
|
+
const unmanagedReason = unmanagedPathReason(targetDir, operation.relativePath);
|
|
1155
|
+
if (unmanagedReason) {
|
|
1156
|
+
unsafe.push({
|
|
1157
|
+
...operation,
|
|
1158
|
+
reason: unmanagedReason,
|
|
1159
|
+
});
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
907
1162
|
const existingRecord = managedFiles[operation.relativePath];
|
|
908
1163
|
const existingKind = pathKind(operation.targetPath);
|
|
909
1164
|
|
|
@@ -1028,6 +1283,8 @@ function inspectTargetDir(targetDir, force) {
|
|
|
1028
1283
|
throw new Error("目标目录不能位于模板源仓库内部");
|
|
1029
1284
|
}
|
|
1030
1285
|
|
|
1286
|
+
assertTargetWorkingTreeWritable(targetDir, { force });
|
|
1287
|
+
|
|
1031
1288
|
if (!fs.existsSync(targetDir)) {
|
|
1032
1289
|
return { exists: false, clearEntries: false };
|
|
1033
1290
|
}
|
|
@@ -1044,13 +1301,14 @@ function inspectTargetDir(targetDir, force) {
|
|
|
1044
1301
|
return { exists: true, clearEntries: entries.length > 0 && force };
|
|
1045
1302
|
}
|
|
1046
1303
|
|
|
1047
|
-
function inspectExistingTargetDir(targetDir) {
|
|
1304
|
+
function inspectExistingTargetDir(targetDir, { force = false } = {}) {
|
|
1048
1305
|
if (isInsideTemplateRoot(targetDir)) {
|
|
1049
1306
|
throw new Error("目标目录不能位于模板源仓库内部");
|
|
1050
1307
|
}
|
|
1051
1308
|
if (!fs.existsSync(targetDir) || pathKind(targetDir) !== "directory") {
|
|
1052
1309
|
throw new Error("attach 目标目录必须是已经存在的项目目录");
|
|
1053
1310
|
}
|
|
1311
|
+
assertTargetWorkingTreeWritable(targetDir, { force });
|
|
1054
1312
|
}
|
|
1055
1313
|
|
|
1056
1314
|
function prepareTargetDir(targetDir, targetState) {
|
|
@@ -1130,8 +1388,8 @@ function runTemplateVerification(targetDir, scriptPath) {
|
|
|
1130
1388
|
}
|
|
1131
1389
|
|
|
1132
1390
|
function verifyGeneratedTemplate(targetDir, mode = "managed") {
|
|
1133
|
-
if (mode === "init") {
|
|
1134
|
-
verifyGeneratedInstance(targetDir);
|
|
1391
|
+
if (mode === "init" || mode === "sync") {
|
|
1392
|
+
verifyGeneratedInstance(targetDir, { checkForbiddenPaths: mode === "init" });
|
|
1135
1393
|
return;
|
|
1136
1394
|
}
|
|
1137
1395
|
|
|
@@ -1144,15 +1402,18 @@ function verifyGeneratedTemplate(targetDir, mode = "managed") {
|
|
|
1144
1402
|
}
|
|
1145
1403
|
}
|
|
1146
1404
|
|
|
1147
|
-
function verifyGeneratedInstance(targetDir) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1405
|
+
function verifyGeneratedInstance(targetDir, { checkForbiddenPaths = true } = {}) {
|
|
1406
|
+
if (checkForbiddenPaths) {
|
|
1407
|
+
const forbiddenPaths = [
|
|
1408
|
+
...INSTANCE_FORBIDDEN_PATHS,
|
|
1409
|
+
...[...INIT_EXCLUDED_ROOT_ENTRIES],
|
|
1410
|
+
...[...INIT_EXCLUDED_ROOT_FILES],
|
|
1411
|
+
...[...INIT_EXCLUDED_RELATIVE_PATHS],
|
|
1412
|
+
];
|
|
1413
|
+
for (const relativePath of [...new Set(forbiddenPaths)]) {
|
|
1414
|
+
if (pathKind(targetPath(targetDir, relativePath)) !== "missing") {
|
|
1415
|
+
throw new Error(`初始化结果包含禁止分发的模板源资产:${relativePath}`);
|
|
1416
|
+
}
|
|
1156
1417
|
}
|
|
1157
1418
|
}
|
|
1158
1419
|
|
|
@@ -1614,7 +1875,7 @@ function runAttach(argv = []) {
|
|
|
1614
1875
|
|
|
1615
1876
|
readTemplateSnapshot();
|
|
1616
1877
|
const targetDir = normalizeTargetDir(options.targetDir);
|
|
1617
|
-
inspectExistingTargetDir(targetDir);
|
|
1878
|
+
inspectExistingTargetDir(targetDir, { force: Boolean(options.force) });
|
|
1618
1879
|
if (pathKind(targetPath(targetDir, TEMPLATE_METADATA_FILENAME)) !== "missing") {
|
|
1619
1880
|
throw new Error("当前项目已有模板元数据,请使用 sync,不要重复 attach");
|
|
1620
1881
|
}
|
|
@@ -1710,7 +1971,7 @@ function runSync(argv = []) {
|
|
|
1710
1971
|
}
|
|
1711
1972
|
readTemplateSnapshot();
|
|
1712
1973
|
const targetDir = normalizeTargetDir(options.targetDir || ".");
|
|
1713
|
-
inspectExistingTargetDir(targetDir);
|
|
1974
|
+
inspectExistingTargetDir(targetDir, { force: Boolean(options.force) });
|
|
1714
1975
|
const { metadata } = loadTemplateMetadata(targetDir);
|
|
1715
1976
|
const identity = readTargetIdentity(targetDir);
|
|
1716
1977
|
const syncPlan = classifySyncPlan(targetDir, metadata, identity);
|
|
@@ -1754,7 +2015,7 @@ function runSync(argv = []) {
|
|
|
1754
2015
|
applyManagedOperation(operation, transaction);
|
|
1755
2016
|
}
|
|
1756
2017
|
applyMigrationPlan(migrationPlan, transaction);
|
|
1757
|
-
verifyGeneratedTemplate(targetDir, "
|
|
2018
|
+
verifyGeneratedTemplate(targetDir, "sync");
|
|
1758
2019
|
writeTemplateMetadata(
|
|
1759
2020
|
targetDir,
|
|
1760
2021
|
buildNextSyncMetadata(metadata, syncPlan, targetDir),
|
|
@@ -19,8 +19,8 @@ description: Use when a Harness change or vertical slice may require work across
|
|
|
19
19
|
1. 读取 `docs/process/implementation-repo-integration.md` 和当前 change 资产。
|
|
20
20
|
2. 判断影响面:Harness-only、backend-only、frontend-only、backend+frontend、contract-only、release-only。
|
|
21
21
|
3. 确认每个受影响实现仓库已有登记记录;缺失则阻断并要求 onboarding。
|
|
22
|
-
4. 核对项目根路径:Harness
|
|
23
|
-
5. 输出最小任务分配:Harness 文档 / OpenAPI / 后端 MR / 前端 MR / 验证 /
|
|
22
|
+
4. 核对项目根路径:Harness 内 `harness-apps` 与 `git-submodule` 项目必须是 `apps/backend/<project>/` 或 `apps/frontend/<project>/`;`apps/backend/`、`apps/frontend/` 只能作为容器,`app/backend/`、`app/frontend/` 及其子路径必须阻断。`git-submodule` 另核 gitlink、`.gitmodules` 和独立 `git_url`。外部 `external-repository` 记录各自的真实项目根。
|
|
23
|
+
5. 输出最小任务分配:Harness 文档 / OpenAPI / 后端 MR / 前端 MR / 验证 / 发布。`git-submodule` 必须追加 `superproject-gitlink-update`。
|
|
24
24
|
6. 按 `docs/templates/cross-repo-slice-template.md` 生成切片记录草案。
|
|
25
25
|
7. 给出 fresh verification 命令和阶段 checkpoint 回写字段。
|
|
26
26
|
|
|
@@ -18,11 +18,11 @@ description: Use when connecting an existing frontend, backend, fullstack, or ot
|
|
|
18
18
|
|
|
19
19
|
1. 确认当前仓库是 Harness 仓库,读取 `docs/process/implementation-repo-integration.md`。
|
|
20
20
|
2. 如果输入是本地路径,只读检查 Git remote、当前分支、目录结构、构建文件和测试脚本。
|
|
21
|
-
3. 如果输入是远端 URL,优先使用只读 Git
|
|
21
|
+
3. 如果输入是远端 URL,优先使用只读 Git 查询。需要完整工作树时:`external-repository` 只能 clone 到临时目录;`git-submodule` 只能通过 `git submodule add` / `git submodule update --init` 在已批准的 `apps/backend/<project>/` 或 `apps/frontend/<project>/` gitlink 挂载点检出,禁止普通 clone 或复制源码进 Harness。
|
|
22
22
|
4. 识别技术栈、包管理器、CI、测试命令、构建命令、OpenAPI 接入和设计 token 接入。
|
|
23
|
-
5.
|
|
24
|
-
6. 按 `docs/templates/implementation-repo-registry-template.md`
|
|
25
|
-
7. 列出 `known_gaps`、人审点、fresh verification 命令和需要回写到 Harness change / Issue / checkpoint
|
|
23
|
+
5. 判定实现位置与 `repository_scope`:`external-repository` 记录真实项目根;`harness-apps` 与 `git-submodule` 只能使用 `apps/backend/<project>/` 或 `apps/frontend/<project>/`。`apps/backend/`、`apps/frontend/` 仅为容器,`app/backend/`、`app/frontend/` 及其子路径必须标记为阻断。gitlink(mode `160000`)必须登记为 `git-submodule` 和 `layout_policy: git-submodule-harness-apps`,不得写成 `harness-apps`。必须对照工作树:`git ls-files --stage`、`.gitmodules` 与声明 scope 不一致时阻断;缺少 `git_entry_mode: 160000` 不得把挂载点当普通目录。
|
|
24
|
+
6. 按 `docs/templates/implementation-repo-registry-template.md` 输出实现仓库登记内容。`git-submodule` 必填 `gitmodules_name`、`gitlink_path`、`git_entry_mode`、`superproject_git_url` 和 `checkout_state`。`harness-apps` / `external-repository` 这些字段填 `不适用`。
|
|
25
|
+
7. 列出 `known_gaps`、人审点、fresh verification 命令和需要回写到 Harness change / Issue / checkpoint 的信息。空 gitlink、detached HEAD、`--force` 覆盖挂载点或缺少递归检出凭据时标记阻断。写入前必须看 `inspectWorkingTreeScope` / `implementationWriteViolation`:只接受对象结果且 `.writable === true` 才可写;字符串、`null` 或 `.writable !== true` 一律不可写。即使已正确登记为 `git-submodule`,空 gitlink 或 detached HEAD 也必须 `.writable === false`,不得当普通目录写文件或脚手架。
|
|
26
26
|
|
|
27
27
|
## Baseline Checks
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ description: Use when connecting an existing frontend, backend, fullstack, or ot
|
|
|
36
36
|
## Boundaries
|
|
37
37
|
|
|
38
38
|
- 不直接提交、推送、创建 MR / PR 或修改实现仓库。
|
|
39
|
-
- 不把实现仓库源码复制进 Harness
|
|
39
|
+
- 不把实现仓库源码复制进 Harness 仓库;`git-submodule` 只能形成 gitlink,不能 copy / subtree 冒充。
|
|
40
40
|
- 不把缺失命令编造为已存在;找不到时标记 `unknown` 或 `需人工确认`。
|
|
41
41
|
- 不得把 `app/backend/`、`app/frontend/` 或 `apps/backend/`、`apps/frontend/` 容器根登记为可生成项目根。
|
|
42
42
|
- 触碰认证、授权、SQL、迁移、加密、公共基础库 API 时标记 `TODO-HUMAN-REVIEW`。
|
|
@@ -51,7 +51,7 @@ node <skill-root>/scripts/extract.mjs skill-names --in <live-lock.json> --out <w
|
|
|
51
51
|
|
|
52
52
|
## Steps
|
|
53
53
|
|
|
54
|
-
1. Detect:
|
|
54
|
+
1. Detect: `<wiki-root>/wiki/index.md` + `CLAUDE.md` (or `AGENTS.md`) means a wiki exists. Default wiki-root is repo-root `wiki/`. If `yss-project.yaml` is `repository_mode: template-source`, wiki-root is `.template-source/wiki`.
|
|
55
55
|
2. If the user is asking a repository question, load [query.md](references/query.md) and stop. Do not lint, ingest, or append `log.md`.
|
|
56
56
|
3. Load the mode algorithm: [compile.md](references/compile.md) for init/refresh/rebuild, [ingest.md](references/ingest.md) for ingest. For init/rebuild corpus choice, load [discover.md](discover.md). For writing, load [writing.md](references/writing.md). For checks, load [lint.md](references/lint.md).
|
|
57
57
|
4. **Fact order:** called code > table comments / config defaults > stale raw copies. After writing, re-read live sources for a sample of claims (`N = min(5, changed pages)`).
|
|
@@ -27,6 +27,7 @@ Unless the user replaces this list with the host's single source-of-truth table:
|
|
|
27
27
|
## Default exclude
|
|
28
28
|
|
|
29
29
|
- `docs/reviews/`
|
|
30
|
+
- `.template-source/evidence/`
|
|
30
31
|
- `docs/.scratch/`
|
|
31
32
|
- Agent projection directories (copies or symlinks of shared skills)
|
|
32
33
|
- `node_modules`
|
|
@@ -64,7 +64,7 @@ node scripts/generate_scaffold.mjs \
|
|
|
64
64
|
- 永不生成 `User CRUD` 示例;`--with-example` 已禁用,业务代码必须按批准的 Slice Implementation Contract 逐切片实现。
|
|
65
65
|
- 若仓库已经存在相同工程,先核对覆盖范围。
|
|
66
66
|
- 输出目录必须显式指定;非空目录默认拒绝写入,确认覆盖时才使用 `--force`。
|
|
67
|
-
- Harness 内多项目布局必须使用 `apps/backend/<project>/`;`apps/backend/` 只能是生成器的父容器,`app/backend/`、`app/frontend/`
|
|
67
|
+
- Harness 内多项目布局必须使用 `apps/backend/<project>/`;`apps/backend/` 只能是生成器的父容器,`app/backend/`、`app/frontend/` 及其子路径一律拒绝。`git-submodule` gitlink、空挂载点、detached HEAD 工作树不得覆盖生成,即使传入 `--force` 也不得当成普通目录,且不得走「请显式传入 `--force`」普通目录覆盖 / rename 路径。`--output-dir` 指向 detached HEAD 子仓时不得 mkdir、staging 或生成工程。先 `git submodule update --init` 并在子仓附加分支工作树内生成。生成器必须先调用 `gitSubmoduleScaffoldViolation`,再在 exists / `--force` / rename 之前调用 `refuseGitlinkAsRegularDirectory`。
|
|
68
68
|
- 不要在 skill 里硬编码用户业务字段或真实连接信息。
|
|
69
69
|
- 生成后要检查依赖关系是否仍符合分层约束。
|
|
70
70
|
- 生成后必须继续使用 `yss-backend-scaffold-parent` 校验工程基线,再按垂直切片加载 `yss-domain`、`yss-application`、`yss-repository`、`yss-web-controller` 等行为 skill;各 skill 的 `references/*-layer-guide.md` 承载分层开发规范。
|
|
@@ -5,6 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
findGitRoot,
|
|
10
|
+
gitSubmoduleScaffoldViolation,
|
|
11
|
+
overlayMountViolation
|
|
12
|
+
} from "../../../../scripts/lib/repository-scope-policy.mjs";
|
|
13
|
+
|
|
8
14
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
15
|
const SKILL_ROOT = path.resolve(SCRIPT_DIR, "..");
|
|
10
16
|
const REPOSITORY_ROOT = path.resolve(SCRIPT_DIR, "../../../..");
|
|
@@ -87,11 +93,28 @@ class ScaffoldGenerator {
|
|
|
87
93
|
this.scaffoldContract = undefined;
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
gitRootForOutput() {
|
|
97
|
+
return findGitRoot(this.finalProjectRoot) || findGitRoot(this.outputDir) || REPOSITORY_ROOT;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
refuseGitlinkAsRegularDirectory(gitRoot, target) {
|
|
101
|
+
const overlay = overlayMountViolation(gitRoot, target, { force: true });
|
|
102
|
+
if (overlay) fail(overlay);
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
async generate() {
|
|
91
106
|
await this.validateHarnessOutputLayout();
|
|
92
|
-
|
|
107
|
+
const gitRoot = this.gitRootForOutput();
|
|
108
|
+
const gitlinkViolation = gitSubmoduleScaffoldViolation(gitRoot, this.outputDir, this.projectName, { force: this.options.force });
|
|
109
|
+
if (gitlinkViolation) fail(gitlinkViolation);
|
|
110
|
+
this.refuseGitlinkAsRegularDirectory(gitRoot, this.finalProjectRoot);
|
|
111
|
+
const targetExists = await exists(this.finalProjectRoot);
|
|
112
|
+
if (targetExists) {
|
|
113
|
+
this.refuseGitlinkAsRegularDirectory(gitRoot, this.finalProjectRoot);
|
|
114
|
+
if (!this.options.force) fail(`输出目录已存在: ${this.finalProjectRoot};如确认覆盖,请显式传入 --force`);
|
|
115
|
+
}
|
|
93
116
|
await this.validateContractMetadata();
|
|
94
|
-
if (
|
|
117
|
+
if (targetExists && await nonEmpty(this.finalProjectRoot) && this.options.force) this.validateForceMetadata();
|
|
95
118
|
await mkdir(this.outputDir, { recursive: true });
|
|
96
119
|
const stagingRoot = await mkdtemp(path.join(this.outputDir, `.${this.projectName}.staging-`));
|
|
97
120
|
this.projectRoot = path.join(stagingRoot, this.projectName);
|
|
@@ -101,7 +124,11 @@ class ScaffoldGenerator {
|
|
|
101
124
|
this.generateDatabaseScripts(); await this.generateDocumentation(); await this.writeGenerationManifest();
|
|
102
125
|
await this.copyWrapperFiles(); await this.validateGeneratedArtifacts();
|
|
103
126
|
let backupPath;
|
|
104
|
-
if (await exists(this.finalProjectRoot)) {
|
|
127
|
+
if (await exists(this.finalProjectRoot)) {
|
|
128
|
+
this.refuseGitlinkAsRegularDirectory(gitRoot, this.finalProjectRoot);
|
|
129
|
+
backupPath = path.join(this.outputDir, `.${this.projectName}.backup-${backupStamp()}`);
|
|
130
|
+
await rename(this.finalProjectRoot, backupPath);
|
|
131
|
+
}
|
|
105
132
|
await rename(this.projectRoot, this.finalProjectRoot); this.projectRoot = this.finalProjectRoot;
|
|
106
133
|
console.log(`\n✅ 项目生成完成!\n📂 项目位置: ${this.projectRoot}`);
|
|
107
134
|
if (backupPath) console.log(`♻️ 原项目备份: ${backupPath}`);
|
package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { chmod, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import test from "node:test";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { run } from "./run_scaffold_verification.mjs";
|
|
10
|
+
import { makeGitlinkFixture } from "../../../../scripts/lib/git-submodule-fixtures.mjs";
|
|
11
|
+
import { GITLINK_MODE, gitLsFilesStage } from "../../../../scripts/lib/repository-scope-policy.mjs";
|
|
9
12
|
|
|
10
13
|
const scripts = path.dirname(fileURLToPath(import.meta.url));
|
|
11
14
|
const generator = path.join(scripts, "generate_scaffold.mjs");
|
|
@@ -21,6 +24,55 @@ test("拒绝不一致合同和未经明确批准的非空目录覆盖", async (t
|
|
|
21
24
|
|
|
22
25
|
test("已批准的 --force 先备份非空目标,再提交 staging 工程", async (t) => { const data = await fixture(); t.after(() => rm(data.root, { recursive: true, force: true })); const oldProject = path.join(data.output, "demo-service"); await mkdir(oldProject); await writeFile(path.join(oldProject, "old.txt"), "recoverable"); const result = await command([...data.args, "--force", "--overwrite-scope", "replace-service", "--rollback-ref", "checkpoint-1"]); assert.equal(result.code, 0, result.stderr); const entries = await readdir(data.output); const backup = entries.find((entry) => entry.startsWith(".demo-service.backup-")); assert.ok(backup); assert.equal(await readFile(path.join(data.output, backup, "old.txt"), "utf8"), "recoverable"); assert.ok((await readdir(path.join(data.output, "demo-service"))).includes("pom.xml")); });
|
|
23
26
|
|
|
27
|
+
test("拒绝覆盖 .gitmodules 登记的 git-submodule 挂载点", async (t) => {
|
|
28
|
+
const data = await fixture();
|
|
29
|
+
t.after(() => rm(data.root, { recursive: true, force: true }));
|
|
30
|
+
const superproject = path.join(data.root, "harness");
|
|
31
|
+
const output = path.join(superproject, "apps/backend");
|
|
32
|
+
await mkdir(path.join(output, "demo-service"), { recursive: true });
|
|
33
|
+
await writeFile(path.join(superproject, ".gitmodules"), "[submodule \"demo-service\"]\n\tpath = apps/backend/demo-service\n\turl = https://example.invalid/demo-service.git\n");
|
|
34
|
+
const contractFile = path.join(data.root, "gitlink-contract.json");
|
|
35
|
+
await writeFile(contractFile, `${JSON.stringify(contract(output), null, 2)}\n`);
|
|
36
|
+
const result = await command(["--project-name", "demo-service", "--base-package", "com.yss.demo", "--output-dir", output, "--database", "mysql", "--contract-id", "scaffold-1", "--contract-version", "1", "--approval-ref", "approval-1", "--router-draft-ref", "router-1", "--persisted-ref", "persisted-1", "--contract-file", contractFile, "--force", "--overwrite-scope", "replace-service", "--rollback-ref", "checkpoint-1"]);
|
|
37
|
+
assert.equal(result.code, 1, result.stderr);
|
|
38
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /gitlink 不得由脚手架覆盖/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("拒绝在 detached HEAD 子仓工作树内当成普通目录生成", async (t) => {
|
|
42
|
+
const data = await fixture();
|
|
43
|
+
const detached = makeGitlinkFixture({ checkout: "detached-head" });
|
|
44
|
+
t.after(() => rm(data.root, { recursive: true, force: true }));
|
|
45
|
+
t.after(() => detached.cleanup());
|
|
46
|
+
const output = path.join(detached.superproject, detached.mount);
|
|
47
|
+
const contractFile = path.join(data.root, "detached-contract.json");
|
|
48
|
+
await writeFile(contractFile, `${JSON.stringify(contract(output), null, 2)}\n`);
|
|
49
|
+
const result = await command(["--project-name", "nested-service", "--base-package", "com.yss.demo", "--output-dir", output, "--database", "mysql", "--contract-id", "scaffold-1", "--contract-version", "1", "--approval-ref", "approval-1", "--router-draft-ref", "router-1", "--persisted-ref", "persisted-1", "--contract-file", contractFile]);
|
|
50
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
51
|
+
assert.equal(result.code, 1, text);
|
|
52
|
+
assert.doesNotMatch(text, /请显式传入 --force/);
|
|
53
|
+
assert.match(text, /detached HEAD 不得当成普通目录写入/);
|
|
54
|
+
assert.equal(existsSync(path.join(output, "nested-service", "pom.xml")), false);
|
|
55
|
+
assert.equal((await readdir(output)).some((name) => name.includes("staging") || name === "nested-service"), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("--force 覆盖真实 gitlink 不得走普通目录覆盖路径", async (t) => {
|
|
59
|
+
const data = await fixture();
|
|
60
|
+
const empty = makeGitlinkFixture({ checkout: "empty-gitlink" });
|
|
61
|
+
t.after(() => rm(data.root, { recursive: true, force: true }));
|
|
62
|
+
t.after(() => empty.cleanup());
|
|
63
|
+
const output = path.join(empty.superproject, "apps/backend");
|
|
64
|
+
const contractFile = path.join(data.root, "empty-gitlink-contract.json");
|
|
65
|
+
await writeFile(contractFile, `${JSON.stringify(contract(output), null, 2)}\n`);
|
|
66
|
+
const result = await command(["--project-name", "billing-service", "--base-package", "com.yss.demo", "--output-dir", output, "--database", "mysql", "--contract-id", "scaffold-1", "--contract-version", "1", "--approval-ref", "approval-1", "--router-draft-ref", "router-1", "--persisted-ref", "persisted-1", "--contract-file", contractFile, "--force", "--overwrite-scope", "replace-service", "--rollback-ref", "checkpoint-1"]);
|
|
67
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
68
|
+
assert.equal(result.code, 1, text);
|
|
69
|
+
assert.doesNotMatch(text, /请显式传入 --force/);
|
|
70
|
+
assert.match(text, /--force 不得把 git-submodule 挂载点当成普通目录覆盖|gitlink 不得/);
|
|
71
|
+
assert.equal(existsSync(path.join(output, "billing-service", "pom.xml")), false);
|
|
72
|
+
assert.equal((await readdir(output)).some((name) => name.includes("backup")), false);
|
|
73
|
+
assert.equal(gitLsFilesStage(empty.superproject, empty.mount)?.mode, GITLINK_MODE);
|
|
74
|
+
});
|
|
75
|
+
|
|
24
76
|
test("验证器记录三条 wrapper 命令的成功和失败证据", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "yss-scaffold-verifier-")); t.after(() => rm(root, { recursive: true, force: true })); const project = path.join(root, "project"); await mkdir(path.join(project, ".yss"), { recursive: true }); await writeFile(path.join(project, ".yss/scaffold-generation.json"), `${JSON.stringify({ schema_version: 1, contract_id: "id", contract_version: 1, slice_id: "slice", approval_ref: "approval", approver: "reviewer", lifecycle_approval_ref: "approval", router_draft_ref: "router", persisted_ref: "persisted", contract_file_ref: "contract", current_version: 1, allowed_write_paths: ["."], expected_evidence_files: ["manifest"], verification_commands: ["./mvnw validate", "./mvnw test", "./mvnw package"], generation_mode: "controlled-generation" })}\n`); const wrapper = path.join(project, "mvnw"); await writeFile(wrapper, "#!/bin/sh\n[ \"$1\" = test ] && exit 2\nprintf 'ran %s\\n' \"$1\"\n"); await chmod(wrapper, 0o755); const evidence = path.join(root, "evidence"); const report = await run(project, evidence); assert.equal(report.status, "failed"); assert.deepEqual(report.commands.map((item) => item.exit_code), [0, 2, 0]); assert.match(await readFile(path.join(evidence, "mvnw-test.stderr.log"), "utf8"), /^$/); });
|
|
25
77
|
|
|
26
78
|
test("验证器在全部 Maven 命令成功时通过并保留日志引用", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "yss-scaffold-verifier-success-")); t.after(() => rm(root, { recursive: true, force: true })); const project = path.join(root, "project"); await mkdir(path.join(project, ".yss"), { recursive: true }); await writeFile(path.join(project, ".yss/scaffold-generation.json"), `${JSON.stringify({ schema_version: 1, contract_id: "id", contract_version: 1, slice_id: "slice", approval_ref: "approval", approver: "reviewer", lifecycle_approval_ref: "approval", router_draft_ref: "router", persisted_ref: "persisted", contract_file_ref: "contract", current_version: 1, allowed_write_paths: ["."], expected_evidence_files: ["manifest"], verification_commands: ["./mvnw validate", "./mvnw test", "./mvnw package"], generation_mode: "controlled-generation" })}\n`); const wrapper = path.join(project, "mvnw"); await writeFile(wrapper, "#!/bin/sh\nprintf 'ran %s\\n' \"$1\"\n"); await chmod(wrapper, 0o755); const report = await run(project, path.join(root, "evidence")); assert.equal(report.status, "passed"); assert.ok(report.commands.every((item) => item.exit_code === 0 && item.stdout_ref.endsWith(".stdout.log"))); });
|
|
@@ -31,7 +31,7 @@ branch: template
|
|
|
31
31
|
## Workflow
|
|
32
32
|
|
|
33
33
|
1. 确认当前任务已经通过 Harness 入口分诊,且实现位置已记录在 proposal、design、build entry review、实施计划或实现路由记录中。
|
|
34
|
-
2. 确认目标是外部实现仓库;只有用户明确选择时才输出到 Harness 仓库的 `apps/frontend/<project>/`。`apps/frontend/` 只能作为项目容器,`app/frontend/`、`app/backend/`
|
|
34
|
+
2. 确认目标是外部实现仓库;只有用户明确选择时才输出到 Harness 仓库的 `apps/frontend/<project>/`。`apps/frontend/` 只能作为项目容器,`app/frontend/`、`app/backend/` 及其子路径禁止作为输出位置。`git-submodule` 只能在已初始化且附加分支的子仓工作树生成;空 gitlink、detached HEAD、`--force` 覆盖挂载点不得当成普通目录。
|
|
35
35
|
3. 只读检查模板分支是否可访问:`git ls-remote --heads <repo> template`。
|
|
36
36
|
4. 需要生成工程时,克隆或复制模板到用户确认的目标位置;不得默认写入 Harness 仓库。
|
|
37
37
|
5. 替换应用名、微应用名、路由、`micro-config.json`、环境变量和 README 中的模板占位。
|
|
@@ -78,7 +78,7 @@ Router 状态映射为:`draft → completed`、`blocked → blocked`、`ready-
|
|
|
78
78
|
|
|
79
79
|
YSS 调用 `code-review` 前必须形成 review input,至少包含 `review_mode`、`review_base_ref`、`implementation_candidate_ref`、`candidate_snapshot_ref`、`candidate_digest`、`spec_ref`、`ticket_ref`、`slice_contract_ref`、`build_architecture_checklist_ref` 和 `yss_execution_result_refs`,并满足 `orchestration-contract.yaml.review_input.manifest_required_by_mode`。`committed` 审查 merge-base 到不可变 `HEAD`;`worktree` 一次捕获 merge-base 到 working tree 的 committed、staged、unstaged 和 untracked 内容,使用 `yss-worktree-candidate-v1` 规定的 raw path、uint64 big-endian 长度、tracked/untracked record 和不支持条目阻断规则计算 SHA-256,让两个 Reviewer 消费同一不可变快照,并在返回和完成 checkpoint 复核摘要未变化。缺少输入、候选为空或摘要变化时返回 `blocked`,不能缩小审查范围或合并不同候选的结论。
|
|
80
80
|
|
|
81
|
-
Matt `implement` 的通用提交指令不构成 YSS Git 授权。只有用户明确给出 `commit_authorized` 为 `true`、非空 `commit_scope` 和 `commit_authorization_ref` 时才能 commit;只有明确给出 `push_authorized` 为 `true`、非空 `push_scope` 和 `push_authorization_ref` 时才能 push。缺少任一字段时保持工作区不变,只输出 checkpoint 判断;不得把 `orchestrate
|
|
81
|
+
Matt `implement` 的通用提交指令不构成 YSS Git 授权。只有用户明确给出 `commit_authorized` 为 `true`、非空 `commit_scope` 和 `commit_authorization_ref` 时才能 commit;只有明确给出 `push_authorized` 为 `true`、非空 `push_scope` 和 `push_authorization_ref` 时才能 push。缺少任一字段时保持工作区不变,只输出 checkpoint 判断;不得把 `orchestrate`、实现授权、当前分支、测试通过或负责人要求解释为隐含授权。`git-submodule` 还必须按仓授权、禁止 detached HEAD 提交,并先推子仓再更新父仓 gitlink。
|
|
82
82
|
|
|
83
83
|
“然后 commit”“做完提交”“可以帮我提交”等自然语言意向本身不是结构化授权。编排器必须取得上述三个 commit 字段;不能先把意向解释成授权,再在完成时补 scope 或引用。
|
|
84
84
|
|