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.
Files changed (187) hide show
  1. package/README.md +6 -2
  2. package/package.json +1 -1
  3. package/src/cli.js +276 -15
  4. package/template/.agents/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  5. package/template/.agents/skills/implementation-repo-onboarding/SKILL.md +5 -5
  6. package/template/.agents/skills/llm-wiki/SKILL.md +1 -1
  7. package/template/.agents/skills/llm-wiki/references/discover.md +1 -0
  8. package/template/.agents/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
  9. package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
  10. package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
  11. package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  12. package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
  13. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
  14. package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +2 -0
  15. package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +1 -1
  16. package/template/.agents/skills/yss-router/SKILL.md +1 -1
  17. package/template/.agents/skills/yss-router/references/router-contract.yaml +20 -0
  18. package/template/.agents/skills/yss-router/references/slice-implementation-contract.md +1 -1
  19. package/template/.claude/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  20. package/template/.claude/skills/implementation-repo-onboarding/SKILL.md +5 -5
  21. package/template/.claude/skills/llm-wiki/SKILL.md +1 -1
  22. package/template/.claude/skills/llm-wiki/references/discover.md +1 -0
  23. package/template/.claude/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
  24. package/template/.claude/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
  25. package/template/.claude/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
  26. package/template/.claude/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  27. package/template/.claude/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
  28. package/template/.claude/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
  29. package/template/.claude/skills/yss-product-lifecycle/references/orchestration.md +2 -0
  30. package/template/.claude/skills/yss-product-lifecycle/references/state-model.md +1 -1
  31. package/template/.claude/skills/yss-router/SKILL.md +1 -1
  32. package/template/.claude/skills/yss-router/references/router-contract.yaml +20 -0
  33. package/template/.claude/skills/yss-router/references/slice-implementation-contract.md +1 -1
  34. package/template/.codex/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  35. package/template/.codex/skills/implementation-repo-onboarding/SKILL.md +5 -5
  36. package/template/.codex/skills/llm-wiki/SKILL.md +1 -1
  37. package/template/.codex/skills/llm-wiki/references/discover.md +1 -0
  38. package/template/.codex/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
  39. package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
  40. package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
  41. package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  42. package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
  43. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
  44. package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +2 -0
  45. package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +1 -1
  46. package/template/.codex/skills/yss-router/SKILL.md +1 -1
  47. package/template/.codex/skills/yss-router/references/router-contract.yaml +20 -0
  48. package/template/.codex/skills/yss-router/references/slice-implementation-contract.md +1 -1
  49. package/template/.cursor/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  50. package/template/.cursor/skills/implementation-repo-onboarding/SKILL.md +5 -5
  51. package/template/.cursor/skills/llm-wiki/SKILL.md +1 -1
  52. package/template/.cursor/skills/llm-wiki/references/discover.md +1 -0
  53. package/template/.cursor/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
  54. package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
  55. package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
  56. package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  57. package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
  58. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
  59. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +2 -0
  60. package/template/.cursor/skills/yss-product-lifecycle/references/state-model.md +1 -1
  61. package/template/.cursor/skills/yss-router/SKILL.md +1 -1
  62. package/template/.cursor/skills/yss-router/references/router-contract.yaml +20 -0
  63. package/template/.cursor/skills/yss-router/references/slice-implementation-contract.md +1 -1
  64. package/template/.hermes/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  65. package/template/.hermes/skills/implementation-repo-onboarding/SKILL.md +5 -5
  66. package/template/.hermes/skills/llm-wiki/SKILL.md +1 -1
  67. package/template/.hermes/skills/llm-wiki/references/discover.md +1 -0
  68. package/template/.hermes/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
  69. package/template/.hermes/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
  70. package/template/.hermes/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
  71. package/template/.hermes/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  72. package/template/.hermes/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
  73. package/template/.hermes/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
  74. package/template/.hermes/skills/yss-product-lifecycle/references/orchestration.md +2 -0
  75. package/template/.hermes/skills/yss-product-lifecycle/references/state-model.md +1 -1
  76. package/template/.hermes/skills/yss-router/SKILL.md +1 -1
  77. package/template/.hermes/skills/yss-router/references/router-contract.yaml +20 -0
  78. package/template/.hermes/skills/yss-router/references/slice-implementation-contract.md +1 -1
  79. package/template/.pi/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  80. package/template/.pi/skills/implementation-repo-onboarding/SKILL.md +5 -5
  81. package/template/.pi/skills/llm-wiki/SKILL.md +1 -1
  82. package/template/.pi/skills/llm-wiki/references/discover.md +1 -0
  83. package/template/.pi/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
  84. package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
  85. package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
  86. package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  87. package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
  88. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
  89. package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +2 -0
  90. package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +1 -1
  91. package/template/.pi/skills/yss-router/SKILL.md +1 -1
  92. package/template/.pi/skills/yss-router/references/router-contract.yaml +20 -0
  93. package/template/.pi/skills/yss-router/references/slice-implementation-contract.md +1 -1
  94. package/template/.qoder/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  95. package/template/.qoder/skills/implementation-repo-onboarding/SKILL.md +5 -5
  96. package/template/.qoder/skills/llm-wiki/SKILL.md +1 -1
  97. package/template/.qoder/skills/llm-wiki/references/discover.md +1 -0
  98. package/template/.qoder/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
  99. package/template/.qoder/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
  100. package/template/.qoder/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
  101. package/template/.qoder/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  102. package/template/.qoder/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
  103. package/template/.qoder/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
  104. package/template/.qoder/skills/yss-product-lifecycle/references/orchestration.md +2 -0
  105. package/template/.qoder/skills/yss-product-lifecycle/references/state-model.md +1 -1
  106. package/template/.qoder/skills/yss-router/SKILL.md +1 -1
  107. package/template/.qoder/skills/yss-router/references/router-contract.yaml +20 -0
  108. package/template/.qoder/skills/yss-router/references/slice-implementation-contract.md +1 -1
  109. package/template/.trae/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  110. package/template/.trae/skills/implementation-repo-onboarding/SKILL.md +5 -5
  111. package/template/.trae/skills/llm-wiki/SKILL.md +1 -1
  112. package/template/.trae/skills/llm-wiki/references/discover.md +1 -0
  113. package/template/.trae/skills/yss-ddd-scaffold-generator/SKILL.md +1 -1
  114. package/template/.trae/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +30 -3
  115. package/template/.trae/skills/yss-ddd-scaffold-generator/scripts/scaffold-generator.test.mjs +52 -0
  116. package/template/.trae/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
  117. package/template/.trae/skills/yss-product-lifecycle/references/matt-yss-adapter.md +1 -1
  118. package/template/.trae/skills/yss-product-lifecycle/references/orchestration-contract.yaml +24 -0
  119. package/template/.trae/skills/yss-product-lifecycle/references/orchestration.md +2 -0
  120. package/template/.trae/skills/yss-product-lifecycle/references/state-model.md +1 -1
  121. package/template/.trae/skills/yss-router/SKILL.md +1 -1
  122. package/template/.trae/skills/yss-router/references/router-contract.yaml +20 -0
  123. package/template/.trae/skills/yss-router/references/slice-implementation-contract.md +1 -1
  124. package/template/AGENTS.md +5 -5
  125. package/template/CONTEXT.md +3 -2
  126. package/template/docs/process/implementation-repo-integration.md +24 -2
  127. package/template/docs/templates/cross-repo-slice-template.md +15 -0
  128. package/template/docs/templates/implementation-repo-registry-template.md +8 -3
  129. package/template/docs/templates/implementation-routing-template.md +3 -3
  130. 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
  131. package/template/scripts/lib/git-submodule-fixtures.mjs +58 -0
  132. package/template/scripts/lib/repository-scope-policy.mjs +456 -0
  133. package/template/scripts/lib/scenario-checks.mjs +11 -1
  134. package/template/scripts/repository-scope-policy +16 -0
  135. package/template/scripts/verify-repository-scope-scenarios +239 -0
  136. package/template/scripts/verify-scaffold-generator-scenarios +54 -0
  137. package/template/scripts/verify-template +3 -2
  138. package/template/skills-lock.json +7 -7
  139. package/template.manifest.json +6 -5
  140. package/template.snapshot.json +4 -4
  141. package/template/.cursor/environment.json +0 -5
  142. package/template/.github/workflows/template-node-tooling.yml +0 -61
  143. package/template/wiki/.wiki-manifest.json +0 -390
  144. package/template/wiki/raw/AGENTS.md +0 -122
  145. package/template/wiki/raw/CONTEXT.md +0 -80
  146. package/template/wiki/raw/README.md +0 -130
  147. package/template/wiki/raw/adr-0002-repository-mode.md +0 -3
  148. package/template/wiki/raw/create-yss-spec-repository-mode-contract.md +0 -95
  149. package/template/wiki/raw/harness-process-tailoring.md +0 -64
  150. package/template/wiki/raw/implementation-repo-integration.md +0 -62
  151. package/template/wiki/raw/issue-tracker.md +0 -135
  152. package/template/wiki/raw/lifecycle-registry.yaml +0 -369
  153. package/template/wiki/raw/maintenance-intensity.yaml +0 -25
  154. package/template/wiki/raw/skills-lock-names.md +0 -93
  155. package/template/wiki/raw/skills-maintenance.md +0 -90
  156. package/template/wiki/raw/spec-template.md +0 -102
  157. package/template/wiki/raw/triage-labels.md +0 -15
  158. package/template/wiki/raw/vertical-slice-ticket-template.md +0 -132
  159. package/template/wiki/raw/yss-project.yaml +0 -2
  160. package/template/wiki/raw/yss-skill-registry.yaml +0 -510
  161. package/template/wiki/wiki/Agent/345/205/245/345/217/243/350/247/204/345/210/231.md +0 -21
  162. package/template/wiki/wiki/CLAUDE.md +0 -28
  163. 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
  164. package/template/wiki/wiki/LLM Wiki.md +0 -30
  165. package/template/wiki/wiki/Matt/346/212/200/350/203/275/344/275/223/347/263/273.md +0 -21
  166. package/template/wiki/wiki/OpenAPI/345/245/221/347/272/246.md +0 -20
  167. package/template/wiki/wiki/SpecDelta.md +0 -19
  168. package/template/wiki/wiki/Spec/345/237/272/347/272/277.md +0 -22
  169. package/template/wiki/wiki/Ticket/344/270/216/346/265/201/347/250/213/347/212/266/346/200/201.md +0 -21
  170. 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
  171. 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
  172. package/template/wiki/wiki/concept-table.md +0 -9
  173. package/template/wiki/wiki/index.md +0 -41
  174. package/template/wiki/wiki/log.md +0 -51
  175. 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
  176. 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
  177. 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
  178. 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
  179. package/template/wiki/wiki//345/236/202/347/233/264/345/210/207/347/211/207Ticket.md +0 -23
  180. 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
  181. 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
  182. 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
  183. 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
  184. 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
  185. 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
  186. package/template/wiki/wiki//346/250/241/346/235/277/346/200/273/350/247/210.md +0 -20
  187. 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
@@ -0,0 +1,456 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { validImplementationPath, violation as pathViolation } from "./implementation-path-policy.mjs";
5
+
6
+ export const REPOSITORY_SCOPES = Object.freeze([
7
+ "external-repository",
8
+ "harness-apps",
9
+ "git-submodule"
10
+ ]);
11
+
12
+ export const LAYOUT_POLICIES = Object.freeze({
13
+ "external-repository": "external-repository-native",
14
+ "harness-apps": "harness-apps-multi-project",
15
+ "git-submodule": "git-submodule-harness-apps"
16
+ });
17
+
18
+ export const GITLINK_MODE = "160000";
19
+ export const CHECKOUT_STATES = Object.freeze([
20
+ "attached-branch",
21
+ "detached-head",
22
+ "uninitialized",
23
+ "empty-gitlink"
24
+ ]);
25
+
26
+ export const NAMED_STRESS_SCENARIOS = Object.freeze([
27
+ "unknown_scope",
28
+ "layout_mismatch",
29
+ "same_origin_url",
30
+ "copy_source_into_harness",
31
+ "missing_git_entry_mode",
32
+ "declared_harness_apps_actual_gitlink",
33
+ "empty_gitlink_as_regular_dir",
34
+ "detached_head_as_regular_dir",
35
+ "force_overlay_mount"
36
+ ]);
37
+
38
+ function asString(value) {
39
+ return typeof value === "string" ? value.trim() : "";
40
+ }
41
+
42
+ function normalizeSlash(value) {
43
+ const text = asString(value);
44
+ if (!text) return "";
45
+ return text.replace(/\\/g, "/").replace(/\/+$/, "");
46
+ }
47
+
48
+ function isDeclared(value) {
49
+ const text = asString(value);
50
+ return text !== "" && text !== "不适用";
51
+ }
52
+
53
+ function uniqueResolved(items) {
54
+ const seen = new Set();
55
+ const out = [];
56
+ for (const item of items) {
57
+ if (!item) continue;
58
+ const resolved = path.resolve(item);
59
+ if (seen.has(resolved)) continue;
60
+ seen.add(resolved);
61
+ out.push(resolved);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ export function expectedLayoutPolicy(scope) {
67
+ return LAYOUT_POLICIES[scope] ?? null;
68
+ }
69
+
70
+ export function violationRepositoryScope(record = {}) {
71
+ const scope = asString(record.repository_scope);
72
+ if (!REPOSITORY_SCOPES.includes(scope)) {
73
+ return `repository_scope must be one of ${REPOSITORY_SCOPES.join(" / ")}`;
74
+ }
75
+ const layout = asString(record.layout_policy);
76
+ const expected = expectedLayoutPolicy(scope);
77
+ if (layout && layout !== expected) {
78
+ return `layout_policy for ${scope} must be ${expected}`;
79
+ }
80
+ if (scope === "git-submodule") return gitSubmoduleRecordViolation(record);
81
+ if (scope === "harness-apps") return harnessAppsRecordViolation(record);
82
+ return externalRepositoryRecordViolation(record);
83
+ }
84
+
85
+ function gitSubmoduleRecordViolation(record) {
86
+ const projectRoot = normalizeSlash(record.project_root);
87
+ if (!projectRoot || !validImplementationPath(`${projectRoot}/`)) {
88
+ return `git-submodule project_root must be a concrete apps/backend/<project>/ or apps/frontend/<project>/ path: ${pathViolation(record.project_root || "") ?? "missing"}`;
89
+ }
90
+ if (!isDeclared(record.gitlink_path)) {
91
+ return "git-submodule gitlink_path is required";
92
+ }
93
+ const gitlinkPath = normalizeSlash(record.gitlink_path);
94
+ if (gitlinkPath !== projectRoot) {
95
+ return "git-submodule gitlink_path must equal project_root";
96
+ }
97
+ if (asString(record.git_entry_mode) !== GITLINK_MODE) {
98
+ return `git-submodule git_entry_mode must be ${GITLINK_MODE}`;
99
+ }
100
+ if (!asString(record.git_url)) return "git-submodule git_url is required";
101
+ if (!asString(record.gitmodules_name)) return "git-submodule gitmodules_name is required";
102
+ if (!asString(record.superproject_git_url)) {
103
+ return "git-submodule superproject_git_url is required";
104
+ }
105
+ if (asString(record.superproject_git_url) === asString(record.git_url)) {
106
+ return "git-submodule git_url must differ from superproject_git_url";
107
+ }
108
+ if (asString(record.layout_policy) && asString(record.layout_policy) !== LAYOUT_POLICIES["git-submodule"]) {
109
+ return `git-submodule layout_policy must be ${LAYOUT_POLICIES["git-submodule"]}`;
110
+ }
111
+ const checkout = asString(record.checkout_state);
112
+ if (!checkout) return "git-submodule checkout_state is required";
113
+ if (!CHECKOUT_STATES.includes(checkout)) {
114
+ return `git-submodule checkout_state must be one of ${CHECKOUT_STATES.join(" / ")}`;
115
+ }
116
+ if (checkout === "detached-head" && record.commit_allowed === true) {
117
+ return "git-submodule must not commit on detached HEAD";
118
+ }
119
+ if (checkout === "detached-head" && record.scaffold_status === "required") {
120
+ return "detached HEAD must not be treated as a regular directory";
121
+ }
122
+ if ((checkout === "uninitialized" || checkout === "empty-gitlink") && record.scaffold_status === "required") {
123
+ return "empty or uninitialized gitlink must not run scaffold_status=required";
124
+ }
125
+ if (record.force === true) {
126
+ return "--force must not overlay a git-submodule mount as a regular directory";
127
+ }
128
+ if (record.copy_source_into_harness === true) {
129
+ return "git-submodule must use gitlink mount, not copy implementation sources into the superproject";
130
+ }
131
+ return null;
132
+ }
133
+
134
+ function foreignGitlinkIdentityViolation(record, scope) {
135
+ if (asString(record.git_entry_mode) === GITLINK_MODE) {
136
+ return `${scope} cannot record a gitlink; use repository_scope: git-submodule`;
137
+ }
138
+ if (isDeclared(record.gitlink_path) || isDeclared(record.gitmodules_name) || isDeclared(record.superproject_git_url)) {
139
+ return `${scope} must not record git-submodule identity fields; use repository_scope: git-submodule`;
140
+ }
141
+ const checkout = asString(record.checkout_state);
142
+ if (isDeclared(checkout) && CHECKOUT_STATES.includes(checkout)) {
143
+ return `${scope} must not record git-submodule checkout_state; use repository_scope: git-submodule`;
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function harnessAppsRecordViolation(record) {
149
+ const foreign = foreignGitlinkIdentityViolation(record, "harness-apps");
150
+ if (foreign) return foreign;
151
+ const projectRoot = normalizeSlash(record.project_root);
152
+ if (projectRoot && !validImplementationPath(`${projectRoot}/`)) {
153
+ return `harness-apps project_root must be a concrete apps/<kind>/<project>/ path: ${pathViolation(record.project_root)}`;
154
+ }
155
+ return null;
156
+ }
157
+
158
+ function externalRepositoryRecordViolation(record) {
159
+ const gitlinkPath = normalizeSlash(record.gitlink_path);
160
+ if (gitlinkPath && gitlinkPath.startsWith("apps/") && String(record.git_entry_mode) === GITLINK_MODE) {
161
+ return "external-repository cannot mount a gitlink under apps/; use repository_scope: git-submodule";
162
+ }
163
+ const foreign = foreignGitlinkIdentityViolation(record, "external-repository");
164
+ if (foreign) return foreign;
165
+ return null;
166
+ }
167
+
168
+ export function validRepositoryScope(record) {
169
+ return violationRepositoryScope(record) === null;
170
+ }
171
+
172
+ export function parseGitmodules(content) {
173
+ const modules = [];
174
+ if (typeof content !== "string" || content.length === 0) return modules;
175
+ let current = null;
176
+ for (const rawLine of content.split(/\r?\n/)) {
177
+ const line = rawLine.trim();
178
+ const name = line.match(/^\[submodule "(.+)"\]$/);
179
+ if (name) {
180
+ current = { name: name[1], path: "", url: "" };
181
+ modules.push(current);
182
+ continue;
183
+ }
184
+ if (!current) continue;
185
+ const pathMatch = line.match(/^path\s*=\s*(.+)$/);
186
+ if (pathMatch) current.path = normalizeSlash(pathMatch[1]);
187
+ const urlMatch = line.match(/^url\s*=\s*(.+)$/);
188
+ if (urlMatch) current.url = urlMatch[1].trim();
189
+ }
190
+ return modules;
191
+ }
192
+
193
+ export function readGitmodules(repoRoot) {
194
+ const file = path.join(repoRoot, ".gitmodules");
195
+ if (!existsSync(file)) return [];
196
+ return parseGitmodules(readFileSync(file, "utf8"));
197
+ }
198
+
199
+ export function gitLsFilesStage(repoRoot, relativePath) {
200
+ const result = spawnSync("git", ["-C", repoRoot, "ls-files", "--stage", "--", relativePath], {
201
+ encoding: "utf8"
202
+ });
203
+ if (result.status !== 0) return null;
204
+ const line = (result.stdout || "").trim().split(/\r?\n/).filter(Boolean)[0];
205
+ if (!line) return null;
206
+ const match = line.match(/^(\d+)\s+([0-9a-f]+)\s+(\d+)\s+(.+)$/i);
207
+ if (!match) return null;
208
+ return { mode: match[1], sha: match[2], stage: match[3], path: match[4] };
209
+ }
210
+
211
+ export function findGitRoot(start) {
212
+ const roots = collectGitRoots(start);
213
+ return roots[0] ?? null;
214
+ }
215
+
216
+ export function collectGitRoots(start) {
217
+ if (!start) return [];
218
+ const roots = [];
219
+ let current = path.resolve(start);
220
+ while (true) {
221
+ if (existsSync(path.join(current, ".git")) || existsSync(path.join(current, ".gitmodules"))) {
222
+ roots.push(current);
223
+ }
224
+ const parent = path.dirname(current);
225
+ if (parent === current) break;
226
+ current = parent;
227
+ }
228
+ return roots;
229
+ }
230
+
231
+ function isUnsafeCheckout(state) {
232
+ return state === "empty-gitlink" || state === "uninitialized" || state === "detached-head";
233
+ }
234
+
235
+ function gitAbbrevRef(cwd) {
236
+ const result = spawnSync("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
237
+ if (result.status !== 0) return null;
238
+ return (result.stdout || "").trim();
239
+ }
240
+
241
+ function gitShowSuperproject(cwd) {
242
+ const result = spawnSync("git", ["-C", cwd, "rev-parse", "--show-superproject-working-tree"], { encoding: "utf8" });
243
+ if (result.status !== 0) return "";
244
+ return (result.stdout || "").trim();
245
+ }
246
+
247
+ function isEmptyDir(dir) {
248
+ if (!existsSync(dir)) return true;
249
+ try {
250
+ return readdirSync(dir).filter((name) => name !== "." && name !== "..").length === 0;
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+
256
+ export function isGitSubmoduleMount(repoRoot, targetPath) {
257
+ const relative = normalizeSlash(path.relative(repoRoot, path.resolve(targetPath)));
258
+ if (!relative || relative.startsWith("..")) return false;
259
+ const listed = readGitmodules(repoRoot).some((item) => item.path === relative);
260
+ if (listed) return true;
261
+ const staged = gitLsFilesStage(repoRoot, relative);
262
+ return staged?.mode === GITLINK_MODE;
263
+ }
264
+
265
+ export function isGitSubmoduleMountAnywhere(startOrRoot, targetPath) {
266
+ const roots = uniqueResolved([startOrRoot, ...collectGitRoots(targetPath), ...collectGitRoots(startOrRoot)]);
267
+ return roots.some((root) => isGitSubmoduleMount(root, targetPath));
268
+ }
269
+
270
+ export function inspectCheckoutState(repoRoot, targetPath) {
271
+ const resolved = path.resolve(targetPath);
272
+ const relative = repoRoot ? normalizeSlash(path.relative(path.resolve(repoRoot), resolved)) : "";
273
+ const inRepo = Boolean(relative) && !relative.startsWith("..");
274
+ const staged = inRepo ? gitLsFilesStage(repoRoot, relative) : null;
275
+ const listed = inRepo ? readGitmodules(repoRoot).some((item) => item.path === relative) : false;
276
+ const superproject = existsSync(path.join(resolved, ".git")) ? gitShowSuperproject(resolved) : "";
277
+ const isMount = staged?.mode === GITLINK_MODE || listed || Boolean(superproject);
278
+ const hasGit = existsSync(path.join(resolved, ".git"));
279
+ if (isMount && !hasGit) {
280
+ return isEmptyDir(resolved) ? "empty-gitlink" : "uninitialized";
281
+ }
282
+ if (isMount && hasGit) {
283
+ const ref = gitAbbrevRef(resolved);
284
+ if (ref === "HEAD") return "detached-head";
285
+ return "attached-branch";
286
+ }
287
+ return null;
288
+ }
289
+
290
+ export function actualWorkingTreeScope(repoRoot, targetPath) {
291
+ if (isGitSubmoduleMountAnywhere(repoRoot, targetPath) || gitShowSuperproject(targetPath)) {
292
+ return "git-submodule";
293
+ }
294
+ return "regular";
295
+ }
296
+
297
+ export function regularDirectoryMisreadViolation(record = {}, options = {}) {
298
+ const force = options.force === true;
299
+ const checkout = asString(record.checkout_state);
300
+ if (checkout === "empty-gitlink" || checkout === "uninitialized") {
301
+ return "empty gitlink must not be treated as a regular directory";
302
+ }
303
+ if (checkout === "detached-head") {
304
+ return "detached HEAD must not be treated as a regular directory";
305
+ }
306
+ if (options.repoRoot && options.targetPath) {
307
+ const inspected = inspectCheckoutState(options.repoRoot, options.targetPath);
308
+ if (inspected === "empty-gitlink" || inspected === "uninitialized") {
309
+ return "empty gitlink must not be treated as a regular directory";
310
+ }
311
+ if (inspected === "detached-head") {
312
+ return "detached HEAD must not be treated as a regular directory";
313
+ }
314
+ if (isGitSubmoduleMountAnywhere(options.repoRoot, options.targetPath) && force) {
315
+ return "--force must not overlay a git-submodule mount as a regular directory";
316
+ }
317
+ }
318
+ if (force && asString(record.repository_scope) === "git-submodule") {
319
+ return "--force must not overlay a git-submodule mount as a regular directory";
320
+ }
321
+ return null;
322
+ }
323
+
324
+ export function findEnclosingSubmoduleWorktree(repoRoot, start) {
325
+ let current = path.resolve(start);
326
+ const stop = repoRoot ? path.resolve(repoRoot) : null;
327
+ while (true) {
328
+ const roots = uniqueResolved([repoRoot, ...collectGitRoots(current)]);
329
+ for (const root of roots) {
330
+ if (path.resolve(root) === current) continue;
331
+ if (isGitSubmoduleMount(root, current)) {
332
+ return {
333
+ worktree: current,
334
+ superproject: root,
335
+ checkout: inspectCheckoutState(root, current)
336
+ };
337
+ }
338
+ }
339
+ if (existsSync(path.join(current, ".git"))) {
340
+ const superproject = gitShowSuperproject(current);
341
+ if (superproject) {
342
+ const checkout = inspectCheckoutState(superproject, current)
343
+ || (gitAbbrevRef(current) === "HEAD" ? "detached-head" : "attached-branch");
344
+ return { worktree: current, superproject, checkout };
345
+ }
346
+ }
347
+ if (stop && current === stop) break;
348
+ const parent = path.dirname(current);
349
+ if (parent === current) break;
350
+ current = parent;
351
+ }
352
+ return null;
353
+ }
354
+
355
+ export function overlayMountViolation(repoRoot, targetPath, { force = false } = {}) {
356
+ if (!isGitSubmoduleMountAnywhere(repoRoot, targetPath)) return null;
357
+ return force
358
+ ? "--force 不得把 git-submodule 挂载点当成普通目录覆盖;gitlink 不得由脚手架覆盖"
359
+ : "git-submodule gitlink 不得由脚手架覆盖;先 git submodule update --init,在子仓附加分支的工作树内生成,或改用 external-repository / harness-apps";
360
+ }
361
+
362
+ export function unsafeSubmoduleWorktreeViolation(repoRoot, targetPath) {
363
+ const enclosing = findEnclosingSubmoduleWorktree(repoRoot, targetPath);
364
+ if (!enclosing) return null;
365
+ if (enclosing.checkout === "empty-gitlink" || enclosing.checkout === "uninitialized") {
366
+ return "空 gitlink 不得当成普通目录写入;gitlink 不得由脚手架覆盖";
367
+ }
368
+ if (enclosing.checkout === "detached-head") {
369
+ return "detached HEAD 不得当成普通目录写入;gitlink 不得由脚手架覆盖";
370
+ }
371
+ return null;
372
+ }
373
+
374
+ export function implementationWriteViolation(repoRoot, targetPath, { force = false } = {}) {
375
+ return overlayMountViolation(repoRoot, targetPath, { force })
376
+ || unsafeSubmoduleWorktreeViolation(repoRoot, targetPath);
377
+ }
378
+
379
+ function freezeWorkingTreeInspection({ writable, violation, declared, actual }) {
380
+ return Object.freeze({
381
+ writable: writable === true,
382
+ violation: violation || null,
383
+ declared: Object.freeze(declared),
384
+ actual: Object.freeze(actual)
385
+ });
386
+ }
387
+
388
+ export function isWorkingTreeWritable(inspection) {
389
+ return inspection?.writable === true;
390
+ }
391
+
392
+ export function inspectWorkingTreeScope(repoRoot, record = {}) {
393
+ const projectRoot = normalizeSlash(record.project_root);
394
+ const declaredScope = asString(record.repository_scope);
395
+ const declaredCheckout = asString(record.checkout_state);
396
+ const declared = { scope: declaredScope, checkout: declaredCheckout };
397
+ if (!repoRoot || !projectRoot) {
398
+ return freezeWorkingTreeInspection({
399
+ writable: false,
400
+ violation: "working-tree inspection requires repoRoot and project_root",
401
+ declared,
402
+ actual: { scope: null, checkout: null }
403
+ });
404
+ }
405
+ const target = path.resolve(repoRoot, projectRoot);
406
+ const actualScope = actualWorkingTreeScope(repoRoot, target);
407
+ const actualCheckout = inspectCheckoutState(repoRoot, target);
408
+ const actual = { scope: actualScope, checkout: actualCheckout };
409
+ let violation = null;
410
+ if (declaredScope === "harness-apps" && actualScope === "git-submodule") {
411
+ violation = "工作树存在 gitlink / .gitmodules,不得登记为 harness-apps";
412
+ } else if (declaredScope === "external-repository" && actualScope === "git-submodule" && projectRoot.startsWith("apps/")) {
413
+ violation = "工作树存在 apps/ gitlink,不得登记为 external-repository";
414
+ } else if (declaredScope === "git-submodule" && actualScope !== "git-submodule") {
415
+ violation = "工作树不是 gitlink,不得登记为 git-submodule";
416
+ }
417
+ if (isUnsafeCheckout(declaredCheckout) || isUnsafeCheckout(actualCheckout)) {
418
+ const misread = regularDirectoryMisreadViolation({
419
+ checkout_state: isUnsafeCheckout(actualCheckout) ? actualCheckout : declaredCheckout
420
+ });
421
+ violation = violation ? `${violation};${misread}` : misread;
422
+ } else if (!violation) {
423
+ violation = unsafeSubmoduleWorktreeViolation(repoRoot, target);
424
+ }
425
+ const writable = violation === null
426
+ && declaredCheckout !== "empty-gitlink"
427
+ && (declaredScope !== "git-submodule"
428
+ || (declaredCheckout === "attached-branch" && actualCheckout === "attached-branch"));
429
+ return freezeWorkingTreeInspection({ writable, violation, declared, actual });
430
+ }
431
+
432
+ export function gitSubmoduleScaffoldViolation(repositoryRoot, outputDir, projectName, { force = false } = {}) {
433
+ const resolvedOutput = path.resolve(outputDir);
434
+ const projectRoot = path.resolve(resolvedOutput, projectName);
435
+ const gitRoot = findGitRoot(projectRoot) || findGitRoot(resolvedOutput) || repositoryRoot || null;
436
+ return overlayMountViolation(gitRoot, projectRoot, { force })
437
+ || unsafeSubmoduleWorktreeViolation(gitRoot, projectRoot)
438
+ || unsafeSubmoduleWorktreeViolation(gitRoot, resolvedOutput)
439
+ || regularDirectoryMisreadViolation({}, { force, repoRoot: gitRoot, targetPath: projectRoot });
440
+ }
441
+
442
+ export function gitSubmoduleCommitViolation({ checkout_state, commit_authorized } = {}) {
443
+ if (checkout_state === "detached-head") {
444
+ return "git-submodule must checkout an attached branch before commit";
445
+ }
446
+ if (commit_authorized !== true) return "commit_authorized must be true";
447
+ return null;
448
+ }
449
+
450
+ export function gitSubmodulePushOrderViolation(order) {
451
+ const expected = ["submodule-repositories", "superproject-gitlink"];
452
+ if (!Array.isArray(order) || order.length !== expected.length || expected.some((item, index) => order[index] !== item)) {
453
+ return "git-submodule push/commit order must be submodule-repositories then superproject-gitlink";
454
+ }
455
+ return null;
456
+ }
@@ -28,6 +28,9 @@ function validateMattContract(data) {
28
28
  ensure(git?.natural_language_intent_is_authorization === false && rule?.requires_explicit_user_authorization === true && rule?.authorized_value === true && rule?.unauthorized_action === "checkpoint-only", `${action} 授权边界不完整`);
29
29
  ensure(includesAll(rule?.required, [`${prefix}_authorized`, `${prefix}_scope`, `${prefix}_authorization_ref`]) && includesAll(rule?.non_empty, [`${prefix}_scope`, `${prefix}_authorization_ref`]), `${action} 授权必填字段不完整`);
30
30
  }
31
+ const submodule = git?.git_submodule;
32
+ ensure(submodule?.applies_when === "repository_scope=git-submodule" && submodule?.forbid_commit_on_detached_head === true && submodule?.empty_gitlink_scaffold === "blocked" && submodule?.treat_empty_gitlink_as_regular_dir === "blocked" && submodule?.treat_detached_head_as_regular_dir === "blocked" && submodule?.force_overlay_mount === "blocked" && submodule?.write_inside_detached_head === "blocked" && submodule?.require_git_entry_mode === "160000" && submodule?.working_tree_declared_scope_must_match === true && submodule?.copy_source_into_harness === "forbidden" && submodule?.clone_requires_recurse_submodules === true && submodule?.push_recurse_submodules === "check" && submodule?.delivery_order_must_include === "superproject-gitlink-update" && submodule?.nested_authorization === "per-repository" && submodule?.inspect_working_tree_writable === "explicit-boolean" && submodule?.empty_gitlink_writable === false && submodule?.force_overlay_regular_dir_path === "blocked", "git-submodule Git 授权契约不完整");
33
+ ensure(JSON.stringify(submodule?.commit_order) === JSON.stringify(["submodule-repositories", "superproject-gitlink"]) && JSON.stringify(submodule?.push_order) === JSON.stringify(["submodule-repositories", "superproject-gitlink"]), "git-submodule 先子后父顺序不完整");
31
34
  }
32
35
 
33
36
  function validateInvocationBoundary(data) {
@@ -92,6 +95,7 @@ function validateMattProse(skill, adapter) {
92
95
  ensure(adapter.includes("仅发现旧路径资产") && adapter.includes("不得调用 `setup-matt-pocock-skills`"), "适配器缺少 setup 旧资产迁移或显式用户入口条件");
93
96
  ensure(adapter.includes("frontier 为空") && adapter.includes("双方共同理解已确认"), "适配器缺少 grill_exit 的 frontier 或共同理解条件");
94
97
  ensure(skill.includes("自然语言意向不构成上述结构化 Git 授权") && adapter.includes("本身不是结构化授权"), "主技能或适配器缺少自然语言 Git 意向不是授权的说明");
98
+ ensure(adapter.includes("禁止 detached HEAD 提交") && adapter.includes("先推子仓再更新父仓 gitlink"), "适配器缺少 git-submodule 嵌套 Git 授权说明");
95
99
  }
96
100
 
97
101
  function validateInvocationProse(skill, adapter, orchestration) {
@@ -214,7 +218,13 @@ export function runScenario(name) {
214
218
  (item) => { item.matt_invocation_boundary.user_invoked_skills.push("unexpected-user-entry"); },
215
219
  (item) => { item.setup_readiness.lifecycle_may_invoke_setup = true; },
216
220
  (item) => { item.grill_exit.user_confirmation_required = false; },
217
- (item) => { delete item.git_authorization.push; }
221
+ (item) => { delete item.git_authorization.push; },
222
+ (item) => { delete item.git_authorization.git_submodule; },
223
+ (item) => { item.git_authorization.git_submodule.forbid_commit_on_detached_head = false; },
224
+ (item) => { item.git_authorization.git_submodule.force_overlay_mount = "allowed"; },
225
+ (item) => { item.git_authorization.git_submodule.write_inside_detached_head = "allowed"; },
226
+ (item) => { item.git_authorization.git_submodule.empty_gitlink_writable = true },
227
+ (item) => { item.git_authorization.git_submodule.force_overlay_regular_dir_path = "allowed"; }
218
228
  ];
219
229
  for (const mutate of mutations) {
220
230
  const candidate = structuredClone(data); mutate(candidate);
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ export {
3
+ REPOSITORY_SCOPES as scopes,
4
+ LAYOUT_POLICIES as layoutPolicies,
5
+ GITLINK_MODE as gitlinkMode,
6
+ NAMED_STRESS_SCENARIOS as namedStressScenarios,
7
+ violationRepositoryScope as violation,
8
+ validRepositoryScope as valid,
9
+ gitSubmoduleScaffoldViolation as scaffoldViolation,
10
+ isGitSubmoduleMount as isMount,
11
+ inspectWorkingTree as inspectWorkingTree,
12
+ inspectCheckoutState as inspectCheckout,
13
+ isWorkingTreeWritable as isWorkingTreeWritable,
14
+ regularDirectoryMisreadViolation as regularDirectoryMisread,
15
+ implementationWriteViolation as writeViolation
16
+ } from "./lib/repository-scope-policy.mjs";