@namewta/speculo 0.7.4 → 0.7.5

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 (157) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/template/canonical/README.md +208 -63
  4. package/template/canonical/canonical-person-steelman-deliberation.md +518 -0
  5. package/template/skills/engineering-standards-builder/README.md +43 -0
  6. package/template/skills/engineering-standards-builder/SKILL.md +233 -0
  7. package/template/skills/engineering-standards-builder/examples/README.md +15 -0
  8. package/template/skills/engineering-standards-builder/examples/fallback/kotlin-gradle/build.gradle.kts +5 -0
  9. package/template/skills/engineering-standards-builder/examples/fallback/kotlin-gradle/expected.json +12 -0
  10. package/template/skills/engineering-standards-builder/examples/fallback/kotlin-gradle/src/main/kotlin/example/App.kt +2 -0
  11. package/template/skills/engineering-standards-builder/examples/go/service/.golangci.yml +3 -0
  12. package/template/skills/engineering-standards-builder/examples/go/service/cmd/api/main.go +3 -0
  13. package/template/skills/engineering-standards-builder/examples/go/service/expected.json +23 -0
  14. package/template/skills/engineering-standards-builder/examples/go/service/go.mod +3 -0
  15. package/template/skills/engineering-standards-builder/examples/go/service/internal/service/service.go +2 -0
  16. package/template/skills/engineering-standards-builder/examples/go/service/internal/service/service_test.go +3 -0
  17. package/template/skills/engineering-standards-builder/examples/java/spring-boot-maven/expected.json +22 -0
  18. package/template/skills/engineering-standards-builder/examples/java/spring-boot-maven/pom.xml +12 -0
  19. package/template/skills/engineering-standards-builder/examples/java/spring-boot-maven/src/main/java/dev/speculo/orders/OrdersApplication.java +7 -0
  20. package/template/skills/engineering-standards-builder/examples/java/spring-boot-maven/src/test/java/dev/speculo/orders/OrdersApplicationTest.java +3 -0
  21. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/.github/workflows/ci.yml +11 -0
  22. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/apps/web/package.json +11 -0
  23. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/apps/web/src/App.vue +2 -0
  24. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/apps/web/tsconfig.json +8 -0
  25. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/expected.json +22 -0
  26. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/package.json +11 -0
  27. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/pnpm-workspace.yaml +2 -0
  28. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/services/orders/pom.xml +12 -0
  29. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/services/orders/src/main/java/dev/speculo/orders/OrdersApplication.java +7 -0
  30. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/services/worker/go.mod +3 -0
  31. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/services/worker/main.go +2 -0
  32. package/template/skills/engineering-standards-builder/examples/rust/workspace/Cargo.toml +7 -0
  33. package/template/skills/engineering-standards-builder/examples/rust/workspace/crates/cli/Cargo.toml +8 -0
  34. package/template/skills/engineering-standards-builder/examples/rust/workspace/crates/cli/src/main.rs +1 -0
  35. package/template/skills/engineering-standards-builder/examples/rust/workspace/crates/core/Cargo.toml +5 -0
  36. package/template/skills/engineering-standards-builder/examples/rust/workspace/crates/core/src/lib.rs +3 -0
  37. package/template/skills/engineering-standards-builder/examples/rust/workspace/expected.json +14 -0
  38. package/template/skills/engineering-standards-builder/examples/rust/workspace/rust-toolchain.toml +3 -0
  39. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/expected.json +21 -0
  40. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/package.json +20 -0
  41. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/src/App.test.tsx +2 -0
  42. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/src/App.tsx +1 -0
  43. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/src/main.tsx +3 -0
  44. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/tsconfig.json +10 -0
  45. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/expected.json +22 -0
  46. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/package.json +21 -0
  47. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/src/App.test.ts +2 -0
  48. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/src/App.vue +7 -0
  49. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/src/main.ts +4 -0
  50. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/tsconfig.json +11 -0
  51. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/vite.config.ts +2 -0
  52. package/template/skills/engineering-standards-builder/manifest.txt +112 -0
  53. package/template/skills/engineering-standards-builder/references/go/00-detection-and-scope.md +37 -0
  54. package/template/skills/engineering-standards-builder/references/go/01-language-and-idioms.md +34 -0
  55. package/template/skills/engineering-standards-builder/references/go/02-modules-packages-and-layout.md +34 -0
  56. package/template/skills/engineering-standards-builder/references/go/03-errors-context-and-concurrency.md +38 -0
  57. package/template/skills/engineering-standards-builder/references/go/04-testing-tooling-quality-gates.md +46 -0
  58. package/template/skills/engineering-standards-builder/references/go/README.md +9 -0
  59. package/template/skills/engineering-standards-builder/references/java/00-detection-and-scope.md +40 -0
  60. package/template/skills/engineering-standards-builder/references/java/01-language-packages-and-api.md +35 -0
  61. package/template/skills/engineering-standards-builder/references/java/02-build-modules-and-dependencies.md +39 -0
  62. package/template/skills/engineering-standards-builder/references/java/03-errors-nullability-resources-concurrency.md +36 -0
  63. package/template/skills/engineering-standards-builder/references/java/04-testing-tooling-quality-gates.md +36 -0
  64. package/template/skills/engineering-standards-builder/references/java/README.md +10 -0
  65. package/template/skills/engineering-standards-builder/references/java/frameworks/spring-boot.md +74 -0
  66. package/template/skills/engineering-standards-builder/references/rules/00-governance-and-precedence.md +52 -0
  67. package/template/skills/engineering-standards-builder/references/rules/01-project-discovery.md +79 -0
  68. package/template/skills/engineering-standards-builder/references/rules/02-evidence-topology-and-scope.md +87 -0
  69. package/template/skills/engineering-standards-builder/references/rules/03-interview-and-decisions.md +77 -0
  70. package/template/skills/engineering-standards-builder/references/rules/04-architecture-and-dependency-boundaries.md +58 -0
  71. package/template/skills/engineering-standards-builder/references/rules/05-files-directories-and-naming.md +53 -0
  72. package/template/skills/engineering-standards-builder/references/rules/06-apis-errors-resources-and-concurrency.md +53 -0
  73. package/template/skills/engineering-standards-builder/references/rules/07-documentation-and-comments.md +46 -0
  74. package/template/skills/engineering-standards-builder/references/rules/08-testing-strategy.md +45 -0
  75. package/template/skills/engineering-standards-builder/references/rules/09-security-configuration-and-data.md +44 -0
  76. package/template/skills/engineering-standards-builder/references/rules/10-performance-observability-and-i18n.md +36 -0
  77. package/template/skills/engineering-standards-builder/references/rules/11-tooling-quality-gates-and-ci.md +69 -0
  78. package/template/skills/engineering-standards-builder/references/rules/12-git-review-and-delivery.md +46 -0
  79. package/template/skills/engineering-standards-builder/references/rules/13-adoption-exceptions-and-ratchets.md +60 -0
  80. package/template/skills/engineering-standards-builder/references/rules/14-generation-contract.md +105 -0
  81. package/template/skills/engineering-standards-builder/references/rules/15-validation-contract.md +65 -0
  82. package/template/skills/engineering-standards-builder/references/rules/16-language-adapter-contract.md +55 -0
  83. package/template/skills/engineering-standards-builder/references/rules/README.md +23 -0
  84. package/template/skills/engineering-standards-builder/references/rust/00-detection-and-scope.md +38 -0
  85. package/template/skills/engineering-standards-builder/references/rust/01-language-api-and-safety.md +35 -0
  86. package/template/skills/engineering-standards-builder/references/rust/02-crates-workspaces-and-layout.md +36 -0
  87. package/template/skills/engineering-standards-builder/references/rust/03-errors-ownership-and-concurrency.md +37 -0
  88. package/template/skills/engineering-standards-builder/references/rust/04-testing-tooling-quality-gates.md +44 -0
  89. package/template/skills/engineering-standards-builder/references/rust/README.md +9 -0
  90. package/template/skills/engineering-standards-builder/references/typescript/00-detection-and-scope.md +58 -0
  91. package/template/skills/engineering-standards-builder/references/typescript/01-language-and-type-system.md +49 -0
  92. package/template/skills/engineering-standards-builder/references/typescript/02-modules-packages-and-runtime-boundaries.md +49 -0
  93. package/template/skills/engineering-standards-builder/references/typescript/03-functions-async-errors-resources.md +42 -0
  94. package/template/skills/engineering-standards-builder/references/typescript/04-testing-tooling-quality-gates.md +51 -0
  95. package/template/skills/engineering-standards-builder/references/typescript/README.md +27 -0
  96. package/template/skills/engineering-standards-builder/references/typescript/app-types/cli.md +11 -0
  97. package/template/skills/engineering-standards-builder/references/typescript/app-types/library.md +11 -0
  98. package/template/skills/engineering-standards-builder/references/typescript/frameworks/react.md +71 -0
  99. package/template/skills/engineering-standards-builder/references/typescript/frameworks/vue.md +131 -0
  100. package/template/skills/engineering-standards-builder/references/typescript/runtimes/browser.md +15 -0
  101. package/template/skills/engineering-standards-builder/references/typescript/runtimes/electron.md +25 -0
  102. package/template/skills/engineering-standards-builder/references/typescript/runtimes/node.md +13 -0
  103. package/template/skills/engineering-standards-builder/scripts/discover-project.mjs +957 -0
  104. package/template/skills/engineering-standards-builder/scripts/self-test.mjs +203 -0
  105. package/template/skills/engineering-standards-builder/scripts/sync-manifest.mjs +105 -0
  106. package/template/skills/engineering-standards-builder/scripts/validate-builder.mjs +255 -0
  107. package/template/skills/engineering-standards-builder/scripts/validate-generated-skill.mjs +286 -0
  108. package/template/skills/engineering-standards-builder/templates/README.md +18 -0
  109. package/template/skills/engineering-standards-builder/templates/compatibility/agents-typescript--standards.md +6 -0
  110. package/template/skills/engineering-standards-builder/templates/compatibility/agents-typescript-standards.md +6 -0
  111. package/template/skills/engineering-standards-builder/templates/compatibility/claude-engineering-standards.md +6 -0
  112. package/template/skills/engineering-standards-builder/templates/compatibility/claude-typescript-standards.md +6 -0
  113. package/template/skills/engineering-standards-builder/templates/project-skill/SKILL.md.template +26 -0
  114. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/00-project-profile.md.template +26 -0
  115. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/01-module-map.md.template +27 -0
  116. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/02-decisions-and-exceptions.md.template +31 -0
  117. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/review-checklist.md.template +14 -0
  118. package/template/skills/source-code-zip-skill/SKILL.md +343 -0
  119. package/template/skills/source-code-zip-skill/scripts/zip_source_code.py +638 -0
  120. package/template/workflows/person/INDEX.md +1 -0
  121. package/template/workflows/person/S-steelman-deliberation/S-steelman-deliberation.md +218 -0
  122. package/template/workflows/person/S-steelman-deliberation/_templates/decision-template.md +51 -0
  123. package/template/workflows/person/S-steelman-deliberation/_templates/steelman-dossier-template.md +96 -0
  124. package/template/workflows/person/S-steelman-deliberation/deliberate.md +127 -0
  125. package/template/workflows/person/S-steelman-deliberation/evidence-gate.md +37 -0
  126. package/template/workflows/person/S-steelman-deliberation/judge.md +69 -0
  127. package/template/workflows/person/S-steelman-deliberation/tools/validate-steelman-change.mjs +697 -0
  128. package/template/skills/typescript-standards-builder/README.md +0 -53
  129. package/template/skills/typescript-standards-builder/SKILL.md +0 -245
  130. package/template/skills/typescript-standards-builder/examples/sample-generated-tree.md +0 -30
  131. package/template/skills/typescript-standards-builder/examples/sample-interview-decisions.md +0 -26
  132. package/template/skills/typescript-standards-builder/manifest.txt +0 -29
  133. package/template/skills/typescript-standards-builder/references/00-governance-and-fixed-defaults.md +0 -77
  134. package/template/skills/typescript-standards-builder/references/01-project-discovery.md +0 -100
  135. package/template/skills/typescript-standards-builder/references/02-interview-workflow.md +0 -129
  136. package/template/skills/typescript-standards-builder/references/03-project-architecture-and-directory-layout.md +0 -84
  137. package/template/skills/typescript-standards-builder/references/04-file-directory-and-symbol-naming.md +0 -92
  138. package/template/skills/typescript-standards-builder/references/05-modules-imports-exports-and-dependencies.md +0 -63
  139. package/template/skills/typescript-standards-builder/references/06-typescript-type-system.md +0 -64
  140. package/template/skills/typescript-standards-builder/references/07-functions-async-errors-and-resources.md +0 -42
  141. package/template/skills/typescript-standards-builder/references/08-comments-jsdoc-and-documentation.md +0 -51
  142. package/template/skills/typescript-standards-builder/references/09-testing-strategy.md +0 -58
  143. package/template/skills/typescript-standards-builder/references/10-react-and-frontend.md +0 -39
  144. package/template/skills/typescript-standards-builder/references/11-node-cli-and-cross-platform.md +0 -31
  145. package/template/skills/typescript-standards-builder/references/12-formatting-lint-and-complexity.md +0 -58
  146. package/template/skills/typescript-standards-builder/references/13-configuration-dependencies-and-ci.md +0 -71
  147. package/template/skills/typescript-standards-builder/references/14-security-performance-and-i18n.md +0 -32
  148. package/template/skills/typescript-standards-builder/references/15-git-review-and-delivery.md +0 -28
  149. package/template/skills/typescript-standards-builder/references/16-adoption-exceptions-and-migration.md +0 -61
  150. package/template/skills/typescript-standards-builder/references/17-generation-contract.md +0 -104
  151. package/template/skills/typescript-standards-builder/references/README.md +0 -37
  152. package/template/skills/typescript-standards-builder/templates/agents-compat-skill/SKILL.md +0 -1
  153. package/template/skills/typescript-standards-builder/templates/claude-skill/SKILL.md +0 -1
  154. package/template/skills/typescript-standards-builder/templates/project-skill/SKILL.md.template +0 -34
  155. package/template/skills/typescript-standards-builder/templates/project-skill/references/00-project-profile.md.template +0 -17
  156. package/template/skills/typescript-standards-builder/templates/project-skill/references/10-review-checklist.md +0 -23
  157. package/template/skills/typescript-standards-builder/templates/project-skill/references/11-decisions-and-exceptions.md.template +0 -19
@@ -0,0 +1,697 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+
8
+ const WORKFLOW = 'person';
9
+ const WORK_ID = 'person/steelman-deliberation';
10
+ const ALLOWED_PHASES = new Set(['awaiting-answer', 'completed']);
11
+ const ALLOWED_DECISIONS = new Set(['recommend', 'reject', 'conditional', 'defer-for-evidence']);
12
+ const DOSSIER_HEADINGS = [
13
+ '原始问题',
14
+ '真正问题重述',
15
+ '决策目标与成功标准',
16
+ '已知事实、推断、价值与未知',
17
+ '双向钢人',
18
+ '共同基础',
19
+ '真正分歧',
20
+ '关键变量',
21
+ '唯一关键问题',
22
+ '证据缺口与适用边界',
23
+ ];
24
+ const DECISION_HEADINGS = [
25
+ '唯一关键问题',
26
+ '用户回答',
27
+ '明确判断',
28
+ '决定性理由',
29
+ '为什么另一边没有赢',
30
+ '下一步行动',
31
+ '反转条件',
32
+ '置信度与剩余不确定性',
33
+ ];
34
+
35
+ class ValidationError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = 'ValidationError';
39
+ }
40
+ }
41
+
42
+ function fail(message) {
43
+ throw new ValidationError(message);
44
+ }
45
+
46
+ function parseArgs(argv) {
47
+ const options = {
48
+ selfCheck: false,
49
+ phase: null,
50
+ changeDir: null,
51
+ statusFile: null,
52
+ dossierFile: null,
53
+ decisionFile: null,
54
+ };
55
+
56
+ for (let index = 0; index < argv.length; index += 1) {
57
+ const arg = argv[index];
58
+ if (arg === '--self-check') {
59
+ options.selfCheck = true;
60
+ continue;
61
+ }
62
+
63
+ const valueOptions = new Map([
64
+ ['--phase', 'phase'],
65
+ ['--change-dir', 'changeDir'],
66
+ ['--status-file', 'statusFile'],
67
+ ['--dossier-file', 'dossierFile'],
68
+ ['--decision-file', 'decisionFile'],
69
+ ]);
70
+ const field = valueOptions.get(arg);
71
+ if (!field) {
72
+ fail(`unknown argument: ${arg}`);
73
+ }
74
+ const value = argv[index + 1];
75
+ if (!value || value.startsWith('--')) {
76
+ fail(`${arg} requires a value`);
77
+ }
78
+ options[field] = value;
79
+ index += 1;
80
+ }
81
+
82
+ if (options.selfCheck) {
83
+ if (argv.length !== 1) {
84
+ fail('--self-check cannot be combined with other arguments');
85
+ }
86
+ return options;
87
+ }
88
+
89
+ if (!ALLOWED_PHASES.has(options.phase)) {
90
+ fail('--phase must be awaiting-answer or completed');
91
+ }
92
+ if (!options.changeDir) {
93
+ fail('--change-dir is required');
94
+ }
95
+ return options;
96
+ }
97
+
98
+ function normalizeNewlines(value) {
99
+ return value.replace(/\r\n?/g, '\n');
100
+ }
101
+
102
+ function normalizeInline(value) {
103
+ return value
104
+ .replace(/<!--[^]*?-->/g, ' ')
105
+ .replace(/\s+/g, ' ')
106
+ .trim();
107
+ }
108
+
109
+ function stripYamlValue(value) {
110
+ const trimmed = value.trim();
111
+ if (
112
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
113
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
114
+ ) {
115
+ return trimmed.slice(1, -1);
116
+ }
117
+ if (trimmed === 'true') return true;
118
+ if (trimmed === 'false') return false;
119
+ if (trimmed === 'null') return null;
120
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
121
+ return trimmed;
122
+ }
123
+
124
+ function parseFrontmatter(text, filePath) {
125
+ const normalized = normalizeNewlines(text);
126
+ const lines = normalized.split('\n');
127
+ if (lines[0] !== '---') {
128
+ fail(`${filePath}: missing opening frontmatter delimiter`);
129
+ }
130
+ const closing = lines.indexOf('---', 1);
131
+ if (closing === -1) {
132
+ fail(`${filePath}: missing closing frontmatter delimiter`);
133
+ }
134
+
135
+ const data = {};
136
+ for (const rawLine of lines.slice(1, closing)) {
137
+ const line = rawLine.trim();
138
+ if (!line || line.startsWith('#')) continue;
139
+ const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
140
+ if (!match) {
141
+ fail(`${filePath}: unsupported frontmatter line: ${rawLine}`);
142
+ }
143
+ data[match[1]] = stripYamlValue(match[2]);
144
+ }
145
+
146
+ return { data, body: lines.slice(closing + 1).join('\n') };
147
+ }
148
+
149
+ function exactHeadingSections(body, level, heading, filePath) {
150
+ const prefix = `${'#'.repeat(level)} ${heading}`;
151
+ const lines = normalizeNewlines(body).split('\n');
152
+ const indexes = [];
153
+ for (let index = 0; index < lines.length; index += 1) {
154
+ if (lines[index].trim() === prefix) indexes.push(index);
155
+ }
156
+ if (indexes.length !== 1) {
157
+ fail(`${filePath}: requires exactly one "${prefix}" heading; found ${indexes.length}`);
158
+ }
159
+ const start = indexes[0] + 1;
160
+ let end = lines.length;
161
+ const nextHeading = new RegExp(`^#{1,${level}}\\s+`);
162
+ for (let index = start; index < lines.length; index += 1) {
163
+ if (nextHeading.test(lines[index])) {
164
+ end = index;
165
+ break;
166
+ }
167
+ }
168
+ return lines.slice(start, end).join('\n').trim();
169
+ }
170
+
171
+ function requireSections(body, headings, filePath) {
172
+ const sections = new Map();
173
+ for (const heading of headings) {
174
+ const content = exactHeadingSections(body, 2, heading, filePath);
175
+ if (!normalizeInline(content)) {
176
+ fail(`${filePath}: section "${heading}" is empty`);
177
+ }
178
+ sections.set(heading, content);
179
+ }
180
+ return sections;
181
+ }
182
+
183
+ function assertNoTodos(text, filePath) {
184
+ if (/\[TODO(?::[^\]]*)?\]/i.test(text)) {
185
+ fail(`${filePath}: contains unresolved [TODO] placeholder`);
186
+ }
187
+ }
188
+
189
+ function requireScalar(frontmatter, key, expected, filePath) {
190
+ if (frontmatter[key] !== expected) {
191
+ fail(`${filePath}: frontmatter ${key} must be ${JSON.stringify(expected)}; got ${JSON.stringify(frontmatter[key])}`);
192
+ }
193
+ }
194
+
195
+ function requireIso(value, label) {
196
+ if (typeof value !== 'string' || !value || Number.isNaN(Date.parse(value))) {
197
+ fail(`${label} must be a valid ISO-8601 timestamp`);
198
+ }
199
+ }
200
+
201
+ function validateKeyQuestion(rawQuestion, filePath) {
202
+ const withoutComments = rawQuestion.replace(/<!--[^]*?-->/g, '').trim();
203
+ const paragraphs = withoutComments.split(/\n\s*\n/).map(normalizeInline).filter(Boolean);
204
+ if (paragraphs.length !== 1) {
205
+ fail(`${filePath}: "唯一关键问题" must contain one paragraph and one question`);
206
+ }
207
+ const question = paragraphs[0].replace(/^[-*+]\s+/, '').trim();
208
+ const questionMarks = (question.match(/[??]/g) ?? []).length;
209
+ if (questionMarks !== 1) {
210
+ fail(`${filePath}: "唯一关键问题" must contain exactly one question mark; found ${questionMarks}`);
211
+ }
212
+ if (/^\d+[.)、]\s/.test(question) || /(?:^|\s)[A-Ca-c][.)、]\s/.test(question)) {
213
+ fail(`${filePath}: "唯一关键问题" must not be a numbered or multi-part list`);
214
+ }
215
+ return normalizeInline(question);
216
+ }
217
+
218
+ function validateSteelmanShape(body, filePath) {
219
+ const lines = normalizeNewlines(body).split('\n').map((line) => line.trim());
220
+ const steelmen = lines.filter((line) => /^###\s+钢人\s+[A-ZA-Z]/u.test(line));
221
+ if (steelmen.length >= 2) return;
222
+
223
+ const optionIndexes = [];
224
+ for (let index = 0; index < lines.length; index += 1) {
225
+ if (/^###\s+选项\s+/u.test(lines[index])) optionIndexes.push(index);
226
+ }
227
+ if (optionIndexes.length < 2) {
228
+ fail(`${filePath}: "双向钢人" requires at least two steelman blocks, or at least two option blocks`);
229
+ }
230
+ optionIndexes.push(lines.length);
231
+ for (let index = 0; index < optionIndexes.length - 1; index += 1) {
232
+ const block = lines.slice(optionIndexes[index] + 1, optionIndexes[index + 1]);
233
+ const required = ['#### 最强支持论证', '#### 最强反对论证', '#### 成立条件', '#### 失败条件'];
234
+ const missing = required.filter((heading) => !block.includes(heading));
235
+ if (missing.length > 0) {
236
+ fail(`${filePath}: each option block is missing ${missing.join(', ')}`);
237
+ }
238
+ }
239
+ }
240
+
241
+ function validateDossier(text, filePath, expectedChange) {
242
+ assertNoTodos(text, filePath);
243
+ const { data, body } = parseFrontmatter(text, filePath);
244
+ requireScalar(data, 'schema_version', 1, filePath);
245
+ requireScalar(data, 'artifact', 'steelman-dossier', filePath);
246
+ requireScalar(data, 'workflow', WORKFLOW, filePath);
247
+ requireScalar(data, 'work_id', WORK_ID, filePath);
248
+ requireScalar(data, 'change', expectedChange, filePath);
249
+ requireScalar(data, 'status', 'awaiting-answer', filePath);
250
+ requireIso(data.generated_at, `${filePath}: generated_at`);
251
+
252
+ const sections = requireSections(body, DOSSIER_HEADINGS, filePath);
253
+ validateSteelmanShape(sections.get('双向钢人'), filePath);
254
+ const question = validateKeyQuestion(sections.get('唯一关键问题'), filePath);
255
+
256
+ if (/^##\s+明确判断\s*$/m.test(body) || /^##\s+下一步行动\s*$/m.test(body)) {
257
+ fail(`${filePath}: answer-before artifact must not contain a final judgment or next action section`);
258
+ }
259
+ return { question, sections, frontmatter: data };
260
+ }
261
+
262
+ function validateStatus(status, statusPath, expectedChange, phase, expectedQuestion) {
263
+ if (!status || typeof status !== 'object' || Array.isArray(status)) {
264
+ fail(`${statusPath}: status must be a JSON object`);
265
+ }
266
+ const expected = {
267
+ schema_version: 1,
268
+ artifact: 'person-work-status',
269
+ workflow: WORKFLOW,
270
+ change: expectedChange,
271
+ work_id: WORK_ID,
272
+ };
273
+ for (const [key, value] of Object.entries(expected)) {
274
+ if (status[key] !== value) {
275
+ fail(`${statusPath}: ${key} must be ${JSON.stringify(value)}; got ${JSON.stringify(status[key])}`);
276
+ }
277
+ }
278
+ if (status.key_question !== expectedQuestion) {
279
+ fail(`${statusPath}: key_question must exactly match the dossier question after whitespace normalization`);
280
+ }
281
+ if (!Array.isArray(status.blockers)) {
282
+ fail(`${statusPath}: blockers must be an array`);
283
+ }
284
+ if (!['asked', 'already-answered'].includes(status.question_disposition)) {
285
+ fail(`${statusPath}: question_disposition must be asked or already-answered`);
286
+ }
287
+ if (status.question_disposition === 'asked' && status.key_question_asked !== true) {
288
+ fail(`${statusPath}: question_disposition=asked requires key_question_asked=true`);
289
+ }
290
+ if (status.question_disposition === 'already-answered' && status.key_question_asked !== false) {
291
+ fail(`${statusPath}: question_disposition=already-answered requires key_question_asked=false`);
292
+ }
293
+ requireIso(status.created_at, `${statusPath}: created_at`);
294
+ requireIso(status.updated_at, `${statusPath}: updated_at`);
295
+
296
+ if (phase === 'awaiting-answer') {
297
+ if (status.status !== 'active' || status.phase !== 'awaiting-answer') {
298
+ fail(`${statusPath}: awaiting-answer requires status=active and phase=awaiting-answer`);
299
+ }
300
+ if (status.question_disposition !== 'asked') {
301
+ fail(`${statusPath}: awaiting-answer requires question_disposition=asked`);
302
+ }
303
+ if (status.answer_received !== false) {
304
+ fail(`${statusPath}: awaiting-answer requires answer_received=false`);
305
+ }
306
+ if (status.user_answer !== null && status.user_answer !== '') {
307
+ fail(`${statusPath}: awaiting-answer requires an empty user_answer`);
308
+ }
309
+ if (status.completed_at !== null) {
310
+ fail(`${statusPath}: awaiting-answer requires completed_at=null`);
311
+ }
312
+ return;
313
+ }
314
+
315
+ if (status.status !== 'completed' || status.phase !== 'completed') {
316
+ fail(`${statusPath}: completed validation requires status=completed and phase=completed`);
317
+ }
318
+ if (status.answer_received !== true) {
319
+ fail(`${statusPath}: completed validation requires answer_received=true`);
320
+ }
321
+ if (typeof status.user_answer !== 'string' || !normalizeInline(status.user_answer)) {
322
+ fail(`${statusPath}: completed validation requires a non-empty user_answer`);
323
+ }
324
+ requireIso(status.completed_at, `${statusPath}: completed_at`);
325
+ if (status.blockers.length !== 0) {
326
+ fail(`${statusPath}: completed validation requires blockers=[]`);
327
+ }
328
+ }
329
+
330
+ function isEvasiveOnly(value) {
331
+ const compact = normalizeInline(value)
332
+ .replace(/[,。;、,.!!??::"“”'‘’()()\-—]/g, '')
333
+ .replace(/\s+/g, '');
334
+ const evasive = [
335
+ '两边都有道理',
336
+ '具体情况具体分析',
337
+ '你可以综合考虑',
338
+ '最终还是看你自己',
339
+ '这取决于你',
340
+ ];
341
+ return evasive.some((phrase) => compact === phrase.replace(/\s+/g, ''));
342
+ }
343
+
344
+ function validateActionTable(section, filePath) {
345
+ const rows = normalizeNewlines(section)
346
+ .split('\n')
347
+ .map((line) => line.trim())
348
+ .filter((line) => line.startsWith('|') && line.endsWith('|'));
349
+ const dataRows = rows.filter((row) => {
350
+ if (/^\|\s*:?-{3,}/.test(row)) return false;
351
+ const normalized = row.replace(/\s+/g, '');
352
+ if (normalized.includes('|动作|责任人|时机/截止|完成证据|')) return false;
353
+ return true;
354
+ });
355
+ if (dataRows.length < 1) {
356
+ fail(`${filePath}: "下一步行动" requires at least one concrete table row`);
357
+ }
358
+ }
359
+
360
+ function validateDecision(text, filePath, expectedChange, dossierQuestion, statusAnswer) {
361
+ assertNoTodos(text, filePath);
362
+ const { data, body } = parseFrontmatter(text, filePath);
363
+ requireScalar(data, 'schema_version', 1, filePath);
364
+ requireScalar(data, 'artifact', 'steelman-decision', filePath);
365
+ requireScalar(data, 'workflow', WORKFLOW, filePath);
366
+ requireScalar(data, 'work_id', WORK_ID, filePath);
367
+ requireScalar(data, 'change', expectedChange, filePath);
368
+ requireScalar(data, 'status', 'completed', filePath);
369
+ if (!ALLOWED_DECISIONS.has(data.decision_type)) {
370
+ fail(`${filePath}: decision_type must be one of ${[...ALLOWED_DECISIONS].join(', ')}`);
371
+ }
372
+ requireIso(data.generated_at, `${filePath}: generated_at`);
373
+
374
+ const sections = requireSections(body, DECISION_HEADINGS, filePath);
375
+ const decisionQuestion = validateKeyQuestion(sections.get('唯一关键问题'), filePath);
376
+ if (decisionQuestion !== dossierQuestion) {
377
+ fail(`${filePath}: decision question must exactly match the frozen dossier question`);
378
+ }
379
+
380
+ const userAnswer = normalizeInline(sections.get('用户回答'));
381
+ if (userAnswer !== normalizeInline(statusAnswer)) {
382
+ fail(`${filePath}: user answer must exactly match .status.json user_answer after whitespace normalization`);
383
+ }
384
+
385
+ const judgment = normalizeInline(sections.get('明确判断'));
386
+ if (isEvasiveOnly(judgment)) {
387
+ fail(`${filePath}: "明确判断" contains only an evasive non-decision`);
388
+ }
389
+ validateActionTable(sections.get('下一步行动'), filePath);
390
+
391
+ const confidence = normalizeInline(sections.get('置信度与剩余不确定性'));
392
+ if (!/[高中低]/u.test(confidence)) {
393
+ fail(`${filePath}: confidence section must state 高, 中, or 低`);
394
+ }
395
+
396
+ return { decisionType: data.decision_type, sections, frontmatter: data };
397
+ }
398
+
399
+ async function readJson(filePath) {
400
+ let raw;
401
+ try {
402
+ raw = await readFile(filePath, 'utf8');
403
+ } catch (error) {
404
+ fail(`${filePath}: cannot read (${error.message})`);
405
+ }
406
+ try {
407
+ return JSON.parse(raw);
408
+ } catch (error) {
409
+ fail(`${filePath}: invalid JSON (${error.message})`);
410
+ }
411
+ }
412
+
413
+ async function readText(filePath) {
414
+ try {
415
+ return await readFile(filePath, 'utf8');
416
+ } catch (error) {
417
+ fail(`${filePath}: cannot read (${error.message})`);
418
+ }
419
+ }
420
+
421
+ async function validateChange(options) {
422
+ const changeDir = path.resolve(options.changeDir);
423
+ const expectedChange = path.basename(changeDir);
424
+ const statusPath = path.resolve(options.statusFile ?? path.join(changeDir, '.status.json'));
425
+ const dossierPath = path.resolve(options.dossierFile ?? path.join(changeDir, 'steelman-dossier.md'));
426
+ const decisionPath = path.resolve(options.decisionFile ?? path.join(changeDir, 'decision.md'));
427
+
428
+ const [status, dossierText] = await Promise.all([
429
+ readJson(statusPath),
430
+ readText(dossierPath),
431
+ ]);
432
+ const dossier = validateDossier(dossierText, dossierPath, expectedChange);
433
+ validateStatus(status, statusPath, expectedChange, options.phase, dossier.question);
434
+
435
+ if (options.phase === 'awaiting-answer') {
436
+ if (!options.decisionFile) {
437
+ try {
438
+ await readFile(decisionPath, 'utf8');
439
+ fail(`${decisionPath}: decision.md must not exist while awaiting an answer`);
440
+ } catch (error) {
441
+ if (error instanceof ValidationError) throw error;
442
+ if (error.code !== 'ENOENT') {
443
+ fail(`${decisionPath}: cannot verify absence (${error.message})`);
444
+ }
445
+ }
446
+ }
447
+ return {
448
+ phase: options.phase,
449
+ change: expectedChange,
450
+ question: dossier.question,
451
+ decisionType: null,
452
+ };
453
+ }
454
+
455
+ const decisionText = await readText(decisionPath);
456
+ const decision = validateDecision(
457
+ decisionText,
458
+ decisionPath,
459
+ expectedChange,
460
+ dossier.question,
461
+ status.user_answer,
462
+ );
463
+ return {
464
+ phase: options.phase,
465
+ change: expectedChange,
466
+ question: dossier.question,
467
+ decisionType: decision.decisionType,
468
+ };
469
+ }
470
+
471
+ function fixtureDossier(change, question = '假设十年以后多数成员都不了解创业起点,你最希望他们共同纪念的究竟是什么?') {
472
+ return `---\nschema_version: 1\nartifact: steelman-dossier\nworkflow: person\nwork_id: person/steelman-deliberation\nchange: "${change}"\nstatus: awaiting-answer\ngenerated_at: "2026-08-18T00:00:00.000Z"\n---\n\n# 双向钢人论证\n\n## 原始问题\n\n公司应选择哪个日期作为司庆日。\n\n## 真正问题重述\n\n真正决策是定义组织未来共同纪念的起点。\n\n## 决策目标与成功标准\n\n选择能被未来成员理解并长期复述的组织叙事。\n\n## 已知事实、推断、价值与未知\n\n事实与价值已分开记录,未来成员的认同仍未知。\n\n## 双向钢人\n\n### 钢人 A:支持理念起点\n\n理念起点最能解释组织为何存在,并能连接长期文化。\n\n### 钢人 B:支持公司成立日\n\n法律实体成立日最清晰、可验证,也最容易形成制度。\n\n## 共同基础\n\n双方都希望日期可长期解释并形成共同认同。\n\n## 真正分歧\n\n分歧是纪念思想起点还是组织实体起点。\n\n## 关键变量\n\n| 变量 | 当前假设 | 改变结论的阈值 | 当前不确定性 | 可外部发现 | 用户特异性 |\n|---|---|---|---|---|---|\n| 共同纪念对象 | 尚未确定 | 未来成员更应认同的起点 | 高 | 否 | 高 |\n\n## 唯一关键问题\n\n${question}\n\n## 证据缺口与适用边界\n\n无决定性事实缺口。\n`;
473
+ }
474
+
475
+ function fixtureMultiOptionDossier(change, question) {
476
+ const options = `### 选项 A:理念公开表达日
477
+
478
+ #### 最强支持论证
479
+
480
+ 它最能解释组织为何存在。
481
+
482
+ #### 最强反对论证
483
+
484
+ 早期日期可能无法被后来员工识别。
485
+
486
+ #### 成立条件
487
+
488
+ 组织长期使命仍承接该理念。
489
+
490
+ #### 失败条件
491
+
492
+ 组织使命已经实质改变。
493
+
494
+ ### 选项 B:IP 公司成立日
495
+
496
+ #### 最强支持论证
497
+
498
+ 它是组织化经营开始的可验证节点。
499
+
500
+ #### 最强反对论证
501
+
502
+ 它可能只代表法律容器,而非共同使命。
503
+
504
+ #### 成立条件
505
+
506
+ 成员更重视法律与制度连续性。
507
+
508
+ #### 失败条件
509
+
510
+ 多数成员不把该实体视为当前组织起点。
511
+
512
+ ### 选项 C:主要团队成立日
513
+
514
+ #### 最强支持论证
515
+
516
+ 它最接近多数员工共同工作的现实起点。
517
+
518
+ #### 最强反对论证
519
+
520
+ 它会抹去更早形成的理念与积累。
521
+
522
+ #### 成立条件
523
+
524
+ 共同劳动经历是组织身份的主要来源。
525
+
526
+ #### 失败条件
527
+
528
+ 未来团队结构变化使这一节点失去代表性。
529
+ `;
530
+ return fixtureDossier(change, question).replace(
531
+ /### 钢人 A:支持理念起点[\s\S]*?(?=\n## 共同基础)/,
532
+ options,
533
+ );
534
+ }
535
+
536
+ function fixtureStatus(change, question, completed = false) {
537
+ return {
538
+ schema_version: 1,
539
+ artifact: 'person-work-status',
540
+ workflow: 'person',
541
+ change,
542
+ work_id: WORK_ID,
543
+ status: completed ? 'completed' : 'active',
544
+ phase: completed ? 'completed' : 'awaiting-answer',
545
+ key_question: question,
546
+ key_question_asked: true,
547
+ question_disposition: 'asked',
548
+ answer_received: completed,
549
+ user_answer: completed ? '希望大家纪念人与人真诚链接这一最初理念。' : null,
550
+ created_at: '2026-08-18T00:00:00.000Z',
551
+ updated_at: '2026-08-18T01:00:00.000Z',
552
+ completed_at: completed ? '2026-08-18T01:00:00.000Z' : null,
553
+ blockers: [],
554
+ };
555
+ }
556
+
557
+ function fixtureDecision(change, question) {
558
+ return `---\nschema_version: 1\nartifact: steelman-decision\nworkflow: person\nwork_id: person/steelman-deliberation\nchange: "${change}"\nstatus: completed\ndecision_type: recommend\ngenerated_at: "2026-08-18T01:00:00.000Z"\n---\n\n# 决策结论\n\n## 唯一关键问题\n\n${question}\n\n## 用户回答\n\n希望大家纪念人与人真诚链接这一最初理念。\n\n## 明确判断\n\n选择最初公开表达该理念的日期作为司庆日。\n\n## 决定性理由\n\n用户明确把长期共同纪念对象定义为理念,而不是法律实体。\n\n## 为什么另一边没有赢\n\n公司成立日更清晰,但不能像理念起点一样解释组织长期存在的理由;若未来改以制度连续性为首要目标,结论会反转。\n\n## 下一步行动\n\n| 动作 | 责任人 | 时机/截止 | 完成证据 |\n|---|---|---|---|\n| 将司庆日期与理念叙事写入公司制度 | 创始人与行政负责人 | 本月内 | 制度文件获批准并发布 |\n\n## 反转条件\n\n若组织正式使命不再承接该理念,改用法律实体成立日。\n\n## 置信度与剩余不确定性\n\n- **置信度**:高\n- **剩余不确定性**:未来成员对叙事的实际接受度仍需周年活动验证。\n`;
559
+ }
560
+
561
+ async function expectFailure(label, fn, expectedFragment) {
562
+ try {
563
+ await fn();
564
+ } catch (error) {
565
+ if (!(error instanceof ValidationError)) throw error;
566
+ if (!error.message.includes(expectedFragment)) {
567
+ fail(`self-check ${label}: expected error containing ${JSON.stringify(expectedFragment)}, got ${JSON.stringify(error.message)}`);
568
+ }
569
+ return;
570
+ }
571
+ fail(`self-check ${label}: expected validation failure`);
572
+ }
573
+
574
+ async function runSelfCheck() {
575
+ const root = await mkdtemp(path.join(os.tmpdir(), 'speculo-steelman-validator-'));
576
+ try {
577
+ const change = '2026-08-18-steelman-company-anniversary';
578
+ const changeDir = path.join(root, change);
579
+ await mkdir(changeDir, { recursive: true });
580
+ const question = '假设十年以后多数成员都不了解创业起点,你最希望他们共同纪念的究竟是什么?';
581
+ const stageDir = path.join(changeDir, '.steelman-stage');
582
+ await mkdir(stageDir, { recursive: true });
583
+ await writeFile(path.join(stageDir, 'steelman-dossier.md'), fixtureDossier(change, question));
584
+ await writeFile(path.join(stageDir, '.status.json'), `${JSON.stringify(fixtureStatus(change, question), null, 2)}\n`);
585
+ await validateChange({
586
+ phase: 'awaiting-answer',
587
+ changeDir,
588
+ statusFile: path.join(stageDir, '.status.json'),
589
+ dossierFile: path.join(stageDir, 'steelman-dossier.md'),
590
+ decisionFile: null,
591
+ });
592
+ console.log('PASS staged awaiting-answer fixture');
593
+
594
+ await writeFile(path.join(changeDir, 'steelman-dossier.md'), fixtureDossier(change, question));
595
+ await writeFile(path.join(changeDir, '.status.json'), `${JSON.stringify(fixtureStatus(change, question), null, 2)}\n`);
596
+
597
+ const awaiting = await validateChange({
598
+ phase: 'awaiting-answer',
599
+ changeDir,
600
+ statusFile: null,
601
+ dossierFile: null,
602
+ decisionFile: null,
603
+ });
604
+ if (awaiting.question !== question) fail('self-check awaiting: question mismatch');
605
+ console.log('PASS awaiting-answer fixture');
606
+
607
+ await writeFile(path.join(stageDir, 'decision.md'), fixtureDecision(change, question));
608
+ await writeFile(path.join(stageDir, '.status.json'), `${JSON.stringify(fixtureStatus(change, question, true), null, 2)}\n`);
609
+ await validateChange({
610
+ phase: 'completed',
611
+ changeDir,
612
+ statusFile: path.join(stageDir, '.status.json'),
613
+ dossierFile: null,
614
+ decisionFile: path.join(stageDir, 'decision.md'),
615
+ });
616
+ console.log('PASS staged completed fixture');
617
+
618
+ await writeFile(path.join(changeDir, 'decision.md'), fixtureDecision(change, question));
619
+ await writeFile(path.join(changeDir, '.status.json'), `${JSON.stringify(fixtureStatus(change, question, true), null, 2)}\n`);
620
+ const completed = await validateChange({
621
+ phase: 'completed',
622
+ changeDir,
623
+ statusFile: null,
624
+ dossierFile: null,
625
+ decisionFile: null,
626
+ });
627
+ if (completed.decisionType !== 'recommend') fail('self-check completed: decision type mismatch');
628
+ console.log('PASS completed fixture');
629
+
630
+ const alreadyAnswered = fixtureStatus(change, question, true);
631
+ alreadyAnswered.key_question_asked = false;
632
+ alreadyAnswered.question_disposition = 'already-answered';
633
+ await writeFile(path.join(changeDir, '.status.json'), `${JSON.stringify(alreadyAnswered, null, 2)}\n`);
634
+ await validateChange({
635
+ phase: 'completed',
636
+ changeDir,
637
+ statusFile: null,
638
+ dossierFile: null,
639
+ decisionFile: null,
640
+ });
641
+ console.log('PASS completed fixture with already-answered question');
642
+
643
+ validateDossier(fixtureMultiOptionDossier(change, question), 'valid-multi-option-dossier.md', change);
644
+ console.log('PASS multi-option steelman fixture');
645
+
646
+ await expectFailure(
647
+ 'incomplete multi-option block',
648
+ async () => validateDossier(
649
+ fixtureMultiOptionDossier(change, question).replace('#### 失败条件', '#### 缺失的失败条件'),
650
+ 'invalid-multi-option-dossier.md',
651
+ change,
652
+ ),
653
+ 'each option block is missing',
654
+ );
655
+ console.log('PASS rejects incomplete multi-option steelman');
656
+
657
+ await expectFailure(
658
+ 'multiple questions',
659
+ async () => validateDossier(fixtureDossier(change, '你更重视理念吗?还是更重视法律实体吗?'), 'invalid-dossier.md', change),
660
+ 'exactly one question mark',
661
+ );
662
+ console.log('PASS rejects multiple questions');
663
+
664
+ await expectFailure(
665
+ 'evasive judgment',
666
+ async () => validateDecision(
667
+ fixtureDecision(change, question).replace('选择最初公开表达该理念的日期作为司庆日。', '两边都有道理。'),
668
+ 'invalid-decision.md',
669
+ change,
670
+ question,
671
+ '希望大家纪念人与人真诚链接这一最初理念。',
672
+ ),
673
+ 'evasive non-decision',
674
+ );
675
+ console.log('PASS rejects evasive judgment');
676
+ } finally {
677
+ await rm(root, { recursive: true, force: true });
678
+ }
679
+ }
680
+
681
+ async function main() {
682
+ const options = parseArgs(process.argv.slice(2));
683
+ if (options.selfCheck) {
684
+ await runSelfCheck();
685
+ console.log('Steelman validator self-check passed.');
686
+ return;
687
+ }
688
+ const result = await validateChange(options);
689
+ const suffix = result.decisionType ? ` decision_type=${result.decisionType}` : '';
690
+ console.log(`PASS ${result.phase} change=${result.change}${suffix}`);
691
+ }
692
+
693
+ main().catch((error) => {
694
+ const prefix = error instanceof ValidationError ? 'VALIDATION ERROR' : 'ERROR';
695
+ console.error(`${prefix}: ${error.message}`);
696
+ process.exitCode = 1;
697
+ });