@juzser/blacksmith 0.1.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 (172) hide show
  1. package/.claude/agents/auditor.md +146 -0
  2. package/.claude/agents/coder.md +164 -0
  3. package/.claude/agents/grader.md +110 -0
  4. package/.claude/agents/merger.md +107 -0
  5. package/.claude/agents/planner.md +92 -0
  6. package/.claude/agents/researcher.md +105 -0
  7. package/.claude/agents/reviewer.md +171 -0
  8. package/.claude/agents/scribe.md +72 -0
  9. package/.claude/agents/security-reviewer.md +130 -0
  10. package/.claude/agents/spec-reviewer.md +215 -0
  11. package/.claude/agents/tester.md +121 -0
  12. package/.claude/agents/uiux.md +126 -0
  13. package/.claude/agents/verifier.md +103 -0
  14. package/.claude/agents/wave-runner.md +74 -0
  15. package/LICENSE +21 -0
  16. package/README.md +389 -0
  17. package/factory/orchestrator/dist/actors.js +46 -0
  18. package/factory/orchestrator/dist/agents-registry.js +308 -0
  19. package/factory/orchestrator/dist/args.js +84 -0
  20. package/factory/orchestrator/dist/artifacts.js +111 -0
  21. package/factory/orchestrator/dist/attribution.js +160 -0
  22. package/factory/orchestrator/dist/audit.js +812 -0
  23. package/factory/orchestrator/dist/autonomy.js +151 -0
  24. package/factory/orchestrator/dist/budgetAlarm.js +366 -0
  25. package/factory/orchestrator/dist/budgets.js +196 -0
  26. package/factory/orchestrator/dist/claims.js +633 -0
  27. package/factory/orchestrator/dist/cli.js +3185 -0
  28. package/factory/orchestrator/dist/commit.js +152 -0
  29. package/factory/orchestrator/dist/coverage.js +188 -0
  30. package/factory/orchestrator/dist/crossFinding.js +479 -0
  31. package/factory/orchestrator/dist/crosscheck.js +396 -0
  32. package/factory/orchestrator/dist/daemon.js +707 -0
  33. package/factory/orchestrator/dist/db/projector.js +1317 -0
  34. package/factory/orchestrator/dist/db/queries.js +1714 -0
  35. package/factory/orchestrator/dist/db/schema.js +305 -0
  36. package/factory/orchestrator/dist/delegation.js +400 -0
  37. package/factory/orchestrator/dist/diffstat.js +164 -0
  38. package/factory/orchestrator/dist/dispatchAudit.js +275 -0
  39. package/factory/orchestrator/dist/dotenv.js +72 -0
  40. package/factory/orchestrator/dist/effort.js +202 -0
  41. package/factory/orchestrator/dist/epic.js +1018 -0
  42. package/factory/orchestrator/dist/epicWidth.js +238 -0
  43. package/factory/orchestrator/dist/errorIssues.js +249 -0
  44. package/factory/orchestrator/dist/errors.js +11 -0
  45. package/factory/orchestrator/dist/escalation.js +500 -0
  46. package/factory/orchestrator/dist/events.js +1015 -0
  47. package/factory/orchestrator/dist/findingAge.js +91 -0
  48. package/factory/orchestrator/dist/findingContext.js +103 -0
  49. package/factory/orchestrator/dist/findings.js +1156 -0
  50. package/factory/orchestrator/dist/gate.js +911 -0
  51. package/factory/orchestrator/dist/gh.js +146 -0
  52. package/factory/orchestrator/dist/git.js +87 -0
  53. package/factory/orchestrator/dist/goalCheck.js +380 -0
  54. package/factory/orchestrator/dist/graph.js +61 -0
  55. package/factory/orchestrator/dist/hookDecision.js +106 -0
  56. package/factory/orchestrator/dist/immutability.js +171 -0
  57. package/factory/orchestrator/dist/impact.js +280 -0
  58. package/factory/orchestrator/dist/integration.js +141 -0
  59. package/factory/orchestrator/dist/judgePreflight.js +155 -0
  60. package/factory/orchestrator/dist/judges.js +310 -0
  61. package/factory/orchestrator/dist/lessonAudit.js +399 -0
  62. package/factory/orchestrator/dist/lessons.js +1329 -0
  63. package/factory/orchestrator/dist/mcp.js +555 -0
  64. package/factory/orchestrator/dist/paths.js +82 -0
  65. package/factory/orchestrator/dist/plan.js +502 -0
  66. package/factory/orchestrator/dist/planQuorum.js +421 -0
  67. package/factory/orchestrator/dist/policy.js +1308 -0
  68. package/factory/orchestrator/dist/policyHook.js +28 -0
  69. package/factory/orchestrator/dist/preconditions.js +47 -0
  70. package/factory/orchestrator/dist/projects.js +134 -0
  71. package/factory/orchestrator/dist/prompts.js +57 -0
  72. package/factory/orchestrator/dist/provenance.js +201 -0
  73. package/factory/orchestrator/dist/providers/api-transport.js +117 -0
  74. package/factory/orchestrator/dist/providers/cli-transport.js +208 -0
  75. package/factory/orchestrator/dist/providers/index.js +54 -0
  76. package/factory/orchestrator/dist/providers/schema-validate.js +152 -0
  77. package/factory/orchestrator/dist/providers/types.js +49 -0
  78. package/factory/orchestrator/dist/queue.js +263 -0
  79. package/factory/orchestrator/dist/quorum.js +347 -0
  80. package/factory/orchestrator/dist/quorumEscalations.js +132 -0
  81. package/factory/orchestrator/dist/results.js +65 -0
  82. package/factory/orchestrator/dist/roadmap.js +437 -0
  83. package/factory/orchestrator/dist/runtime.js +86 -0
  84. package/factory/orchestrator/dist/sameMistakeKpi.js +243 -0
  85. package/factory/orchestrator/dist/sandbox.js +165 -0
  86. package/factory/orchestrator/dist/scaffold.js +373 -0
  87. package/factory/orchestrator/dist/scheduler.js +408 -0
  88. package/factory/orchestrator/dist/schemas.js +195 -0
  89. package/factory/orchestrator/dist/security.js +222 -0
  90. package/factory/orchestrator/dist/severity.js +293 -0
  91. package/factory/orchestrator/dist/spec.js +376 -0
  92. package/factory/orchestrator/dist/specChange.js +497 -0
  93. package/factory/orchestrator/dist/stack.js +324 -0
  94. package/factory/orchestrator/dist/symbols.js +966 -0
  95. package/factory/orchestrator/dist/taskEvents.js +399 -0
  96. package/factory/orchestrator/dist/taskId.js +126 -0
  97. package/factory/orchestrator/dist/taxonomy.js +113 -0
  98. package/factory/orchestrator/dist/testSelect.js +129 -0
  99. package/factory/orchestrator/dist/testerAudit.js +264 -0
  100. package/factory/orchestrator/dist/testgate.js +171 -0
  101. package/factory/orchestrator/dist/usage.js +874 -0
  102. package/factory/orchestrator/dist/waivers.js +228 -0
  103. package/factory/orchestrator/dist/waveBudget.js +223 -0
  104. package/factory/orchestrator/dist/waveConcurrency.js +250 -0
  105. package/factory/orchestrator/dist/waveNext.js +210 -0
  106. package/factory/orchestrator/dist/waveSchedule.js +146 -0
  107. package/factory/orchestrator/dist/worktree.js +207 -0
  108. package/factory/orchestrator/drizzle/0000_broken_rockslide.sql +183 -0
  109. package/factory/orchestrator/drizzle/0001_luxuriant_zaladane.sql +1 -0
  110. package/factory/orchestrator/drizzle/0002_yellow_wild_pack.sql +8 -0
  111. package/factory/orchestrator/drizzle/0003_damp_riptide.sql +12 -0
  112. package/factory/orchestrator/drizzle/0004_broad_lady_vermin.sql +1 -0
  113. package/factory/orchestrator/drizzle/0005_romantic_jocasta.sql +2 -0
  114. package/factory/orchestrator/drizzle/0006_bent_madelyne_pryor.sql +16 -0
  115. package/factory/orchestrator/drizzle/0007_overjoyed_goliath.sql +1 -0
  116. package/factory/orchestrator/drizzle/0008_smart_iron_patriot.sql +2 -0
  117. package/factory/orchestrator/drizzle/0009_agents_epic_id.sql +2 -0
  118. package/factory/orchestrator/drizzle/0010_milestone_kind.sql +1 -0
  119. package/factory/orchestrator/drizzle/meta/_journal.json +83 -0
  120. package/factory/policies/budgets.yml +262 -0
  121. package/factory/policies/crosscheck.yml +308 -0
  122. package/factory/policies/delegation.yml +44 -0
  123. package/factory/policies/effort.yml +142 -0
  124. package/factory/policies/guardrails.yml +306 -0
  125. package/factory/policies/lessons.md +239 -0
  126. package/factory/policies/scheduler.yml +90 -0
  127. package/factory/policies/sensitive-paths.yml +148 -0
  128. package/factory/policies/severity.yml +83 -0
  129. package/factory/policies/stack.yml +60 -0
  130. package/factory/policies/taxonomy.yml +340 -0
  131. package/factory/policies/worktree.yml +95 -0
  132. package/factory/scaffold/base/.github/workflows/ci.yml +34 -0
  133. package/factory/scaffold/base/.gitignore.tmpl +11 -0
  134. package/factory/scaffold/base/AGENTS.md +33 -0
  135. package/factory/scaffold/base/README.md +25 -0
  136. package/factory/scaffold/base/biome.json.tmpl +30 -0
  137. package/factory/scaffold/base/package.json +25 -0
  138. package/factory/scaffold/base/pnpm-workspace.yaml.tmpl +16 -0
  139. package/factory/scaffold/base/src/index.ts +4 -0
  140. package/factory/scaffold/base/test/index.test.ts +8 -0
  141. package/factory/scaffold/base/tsconfig.json +20 -0
  142. package/factory/scaffold/base/tsconfig.test.json +9 -0
  143. package/factory/scaffold/base/vitest.config.ts +29 -0
  144. package/factory/scaffold/mcp/mcp.manifest.json +22 -0
  145. package/factory/scaffold/mcp/package.fragment.json +9 -0
  146. package/factory/scaffold/mcp/src/mcp/README.md +63 -0
  147. package/factory/scaffold/mcp/src/mcp/guard.ts +110 -0
  148. package/factory/scaffold/mcp/src/mcp/redact.ts +69 -0
  149. package/factory/scaffold/mcp/src/mcp/server.ts +55 -0
  150. package/factory/scaffold/mcp/src/mcp/tools/health.ts +60 -0
  151. package/factory/scaffold/mcp/test/mcp/guard.test.ts +108 -0
  152. package/factory/scaffold/mcp/test/mcp/redact.test.ts +61 -0
  153. package/factory/scaffold/mcp/test/mcp/surface.test.ts +100 -0
  154. package/factory/scaffold/ui/index.html +12 -0
  155. package/factory/scaffold/ui/package.fragment.json +13 -0
  156. package/factory/scaffold/ui/src/App.vue +10 -0
  157. package/factory/scaffold/ui/src/main.ts +5 -0
  158. package/factory/scaffold/ui/src/shims.d.ts +11 -0
  159. package/factory/scaffold/ui/vite.config.ts +20 -0
  160. package/factory/scaffold/ui-tailwind/package.fragment.json +6 -0
  161. package/factory/scaffold/ui-tailwind/vite.config.ts +26 -0
  162. package/factory/specs/schema/event.schema.json +82 -0
  163. package/factory/specs/schema/finding-evidence.schema.json +45 -0
  164. package/factory/specs/schema/finding.schema.json +115 -0
  165. package/factory/specs/schema/grader-verdict.schema.json +50 -0
  166. package/factory/specs/schema/judge-verdict.schema.json +19 -0
  167. package/factory/specs/schema/lesson.schema.json +85 -0
  168. package/factory/specs/schema/mcp-manifest.schema.json +230 -0
  169. package/factory/specs/schema/result.schema.json +81 -0
  170. package/factory/specs/schema/spec-change-request.schema.json +46 -0
  171. package/factory/specs/schema/task-spec.schema.json +134 -0
  172. package/package.json +98 -0
@@ -0,0 +1,324 @@
1
+ // The operator's stack answers (architecture §14 "Unified stack standard"),
2
+ // read from factory/policies/stack.yml.
3
+ //
4
+ // This used to be prose. `docs/standards/stack.md` carried one operator's
5
+ // 2026-08-03 interview answers — Vue, Cloudflare Workers, Drizzle, a private
6
+ // design system — phrased as a mandate ("Every project Blacksmith scaffolds
7
+ // uses this stack"), which is a fine sentence in a private repo and a false
8
+ // one in a public clone: the design system it required is not obtainable, and
9
+ // nobody else answered those questions. So the answers moved here, where they
10
+ // are data an operator sets at install time and the scaffolder can act on.
11
+ //
12
+ // Two rules keep that honest, and both are enforced below rather than
13
+ // described:
14
+ //
15
+ // 1. An answer the shipped templates cannot implement makes `smith new`
16
+ // REFUSE. The failure mode this exists to prevent is the quiet one —
17
+ // answering `frontend: react` and receiving a Vue project, which reads as
18
+ // the factory ignoring the interview it just ran.
19
+ // 2. `smith stack check` reports, per answer, whether the templates honour
20
+ // it or merely record it for the agents to read. Most answers are
21
+ // recorded: Blacksmith scaffolds a TypeScript library and optionally a
22
+ // Vue front end, and says so, instead of implying it can scaffold every
23
+ // row of the file.
24
+ import { existsSync, readFileSync } from 'node:fs';
25
+ import path from 'node:path';
26
+ import { parse as parseYaml } from 'yaml';
27
+ import { SmithError } from './errors.js';
28
+ import { REPO_ROOT, STACK_POLICY_PATH } from './paths.js';
29
+ export class StackError extends SmithError {
30
+ }
31
+ /**
32
+ * The closed vocabularies, and the defaults.
33
+ *
34
+ * Every list carries the value that assumes least — `none`, or the one thing
35
+ * the templates actually build — as its default, and most carry an `other`
36
+ * escape so an unlisted stack is representable rather than unspeakable. The
37
+ * lists are closed anyway, because these are answers code branches on: a
38
+ * `frontend: veu` that parses as free text scaffolds nothing and explains
39
+ * nothing, while a closed vocabulary can name the typo back at the operator.
40
+ */
41
+ const VOCABULARY = {
42
+ language: ['typescript', 'javascript', 'python', 'go', 'rust', 'other'],
43
+ frontend: ['none', 'vue', 'react', 'svelte', 'solid', 'vite-vanilla', 'other'],
44
+ styling: ['plain-css', 'tailwind', 'css-modules', 'vanilla-extract', 'other'],
45
+ backend: ['none', 'node', 'deno', 'bun', 'cloudflare-workers', 'other'],
46
+ database: ['none', 'sqlite', 'postgres', 'mysql', 'd1', 'other'],
47
+ orm: ['none', 'drizzle', 'prisma', 'kysely', 'other'],
48
+ package_manager: ['pnpm', 'npm', 'yarn', 'bun'],
49
+ repo_shape: ['single', 'monorepo'],
50
+ lint: ['biome', 'eslint-prettier', 'none'],
51
+ test_unit: ['vitest', 'jest', 'node-test', 'none'],
52
+ test_e2e: ['playwright', 'cypress', 'none'],
53
+ ci: ['github-actions', 'gitlab-ci', 'circleci', 'none'],
54
+ };
55
+ const DEFAULTS = {
56
+ language: 'typescript',
57
+ frontend: 'none',
58
+ design_system: 'none',
59
+ design_system_source: '',
60
+ styling: 'plain-css',
61
+ backend: 'none',
62
+ database: 'none',
63
+ orm: 'none',
64
+ package_manager: 'pnpm',
65
+ repo_shape: 'single',
66
+ lint: 'biome',
67
+ test_unit: 'vitest',
68
+ test_e2e: 'playwright',
69
+ ci: 'github-actions',
70
+ hosting: 'unspecified',
71
+ };
72
+ /**
73
+ * A closed-vocabulary answer, checked rather than trusted (D-203).
74
+ *
75
+ * The YAML 1.2 core schema is the reason this cannot lean on TypeScript's
76
+ * declared types: `frontend: no` parses as the string `'no'`, `hosting: on`
77
+ * as `'on'`, and an unquoted version number as a number. A field read as
78
+ * anything but a listed string is a field whose branch below picks a
79
+ * direction the operator did not choose, so it stops here with the list in
80
+ * the message — a stack file is hand-edited, and a refusal that does not name
81
+ * the alternatives just sends the operator back to the docs.
82
+ */
83
+ function choice(field, value) {
84
+ const allowed = VOCABULARY[field];
85
+ if (allowed === undefined)
86
+ throw new StackError('stack.unknown-field', `No vocabulary for ${field}.`);
87
+ if (typeof value !== 'string' || !allowed.includes(value)) {
88
+ throw new StackError('stack.invalid-policy', `stack.yml ${field} must be one of ${allowed.join(' | ')}; got ${JSON.stringify(value)}.`, { field, value, allowed });
89
+ }
90
+ return value;
91
+ }
92
+ /** A free-text answer: a name, a path, a place. Still has to be a string. */
93
+ function text(field, value) {
94
+ if (typeof value !== 'string') {
95
+ throw new StackError('stack.invalid-policy', `stack.yml ${field} must be a string; got ${JSON.stringify(value)}. Quote it if YAML is reading it as something else.`, { field, value });
96
+ }
97
+ return value.trim();
98
+ }
99
+ export function parseStackAnswers(yamlText) {
100
+ const doc = (parseYaml(yamlText) ?? {});
101
+ const pick = (field) => doc[field] ?? DEFAULTS[field];
102
+ const answers = {
103
+ language: choice('language', pick('language')),
104
+ frontend: choice('frontend', pick('frontend')),
105
+ designSystem: text('design_system', pick('design_system')) || 'none',
106
+ designSystemSource: text('design_system_source', pick('design_system_source')),
107
+ styling: choice('styling', pick('styling')),
108
+ backend: choice('backend', pick('backend')),
109
+ database: choice('database', pick('database')),
110
+ orm: choice('orm', pick('orm')),
111
+ packageManager: choice('package_manager', pick('package_manager')),
112
+ repoShape: choice('repo_shape', pick('repo_shape')),
113
+ lint: choice('lint', pick('lint')),
114
+ testUnit: choice('test_unit', pick('test_unit')),
115
+ testE2e: choice('test_e2e', pick('test_e2e')),
116
+ ci: choice('ci', pick('ci')),
117
+ hosting: text('hosting', pick('hosting')) || 'unspecified',
118
+ };
119
+ // A source with nothing to vendor into is the one cross-field mistake worth
120
+ // catching at parse time: `design_system: none` plus a path reads as a kit
121
+ // that will be copied, and it never is.
122
+ if (answers.designSystem === 'none' && answers.designSystemSource !== '') {
123
+ throw new StackError('stack.invalid-policy', 'stack.yml sets design_system_source while design_system is none — name the design system, or clear the source.', { source: answers.designSystemSource });
124
+ }
125
+ return answers;
126
+ }
127
+ export function loadStackAnswers(filePath = STACK_POLICY_PATH) {
128
+ return parseStackAnswers(readFileSync(filePath, 'utf8'));
129
+ }
130
+ /** Values `factory/scaffold/` can build. Everything else is a refusal, not a substitution. */
131
+ export const SCAFFOLDABLE = {
132
+ language: ['typescript'],
133
+ frontend: ['none', 'vue'],
134
+ styling: ['plain-css', 'tailwind'],
135
+ };
136
+ /**
137
+ * What the template tree hardcodes for answers no code branches on.
138
+ *
139
+ * These are not unsupported — they are supported at exactly one value. Saying
140
+ * which one turns "we ignore this answer" into "we ship pnpm; yours says npm;
141
+ * the agents will read npm and the scaffold will not", which is a thing an
142
+ * operator can act on.
143
+ */
144
+ const HARDCODED = {
145
+ package_manager: {
146
+ value: 'pnpm',
147
+ where: 'the scaffold toolchain run, ci.yml and pnpm-workspace.yaml',
148
+ },
149
+ repo_shape: { value: 'single', where: "pnpm-workspace.yaml's single-package list" },
150
+ lint: { value: 'biome', where: 'biome.json and the `lint` script' },
151
+ test_unit: { value: 'vitest', where: 'vitest.config.ts and the `test` scripts' },
152
+ ci: { value: 'github-actions', where: '.github/workflows/ci.yml' },
153
+ };
154
+ /** Answers the templates scaffold nothing for, and the agent that reads each one instead. */
155
+ const READ_BY = {
156
+ backend: 'the planner and coder, when a task needs a server',
157
+ database: 'the planner and coder, when a task needs storage',
158
+ orm: 'the coder, when a task touches the database',
159
+ test_e2e: 'the tester, which sets the harness up on the first e2e task',
160
+ hosting: 'the planner, when an epic reaches a deploy runbook',
161
+ };
162
+ function reportFor(field, value, source) {
163
+ if (field === 'language') {
164
+ return SCAFFOLDABLE.language.includes(value)
165
+ ? { field, value, support: 'honoured', note: 'The base template scaffolds TypeScript.' }
166
+ : {
167
+ field,
168
+ value,
169
+ support: 'refused',
170
+ note: `\`smith new\` refuses: factory/scaffold/base is TypeScript, and scaffolding it under a ${value} answer would hand you a project you did not ask for.`,
171
+ };
172
+ }
173
+ if (field === 'frontend') {
174
+ if (value === 'none') {
175
+ return {
176
+ field,
177
+ value,
178
+ support: 'honoured',
179
+ note: '`smith new --ui` refuses until this names a framework.',
180
+ };
181
+ }
182
+ return value === 'vue'
183
+ ? { field, value, support: 'honoured', note: 'factory/scaffold/ui scaffolds Vue 3 + Vite.' }
184
+ : {
185
+ field,
186
+ value,
187
+ support: 'refused',
188
+ note: `\`smith new --ui\` refuses: the UI template is Vue, and there is no ${value} template to layer instead.`,
189
+ };
190
+ }
191
+ if (field === 'styling') {
192
+ return SCAFFOLDABLE.styling.includes(value)
193
+ ? {
194
+ field,
195
+ value,
196
+ support: 'honoured',
197
+ note: value === 'tailwind'
198
+ ? 'factory/scaffold/ui-tailwind layers the Vite plugin and the dependency.'
199
+ : 'The UI template ships no utility layer.',
200
+ }
201
+ : {
202
+ field,
203
+ value,
204
+ support: 'refused',
205
+ note: `\`smith new --ui\` refuses: the UI template wires plain CSS or Tailwind, not ${value}.`,
206
+ };
207
+ }
208
+ if (field === 'design_system') {
209
+ if (value === 'none') {
210
+ return {
211
+ field,
212
+ value,
213
+ support: 'honoured',
214
+ note: "The uiux agent specs against the project's own components, and nothing is vendored.",
215
+ };
216
+ }
217
+ if (source === null || source === '') {
218
+ return {
219
+ field,
220
+ value,
221
+ support: 'recorded',
222
+ note: `The uiux agent is told to ground its specs in ${value}; with design_system_source empty, no kit is copied into scaffolded projects.`,
223
+ };
224
+ }
225
+ const resolved = path.isAbsolute(source) ? source : path.join(REPO_ROOT, source);
226
+ return existsSync(resolved)
227
+ ? {
228
+ field,
229
+ value,
230
+ support: 'honoured',
231
+ note: `Vendored from ${source} into <project>/design/.`,
232
+ }
233
+ : {
234
+ field,
235
+ value,
236
+ support: 'refused',
237
+ note: `design_system_source ${source} does not exist, so \`smith new --ui\` would vendor nothing while claiming ${value}.`,
238
+ };
239
+ }
240
+ const hardcoded = HARDCODED[field];
241
+ if (hardcoded !== undefined) {
242
+ return value === hardcoded.value
243
+ ? {
244
+ field,
245
+ value,
246
+ support: 'honoured',
247
+ note: `Matches what the templates ship in ${hardcoded.where}.`,
248
+ }
249
+ : {
250
+ field,
251
+ value,
252
+ support: 'recorded',
253
+ note: `The templates hardcode ${hardcoded.value} in ${hardcoded.where}. The agents read ${value}; \`smith new\` still scaffolds ${hardcoded.value}.`,
254
+ };
255
+ }
256
+ return {
257
+ field,
258
+ value,
259
+ support: 'recorded',
260
+ note: `Nothing in factory/scaffold reads this. Read by ${READ_BY[field] ?? 'the agents'}.`,
261
+ };
262
+ }
263
+ /**
264
+ * The per-answer support report behind `smith stack check`.
265
+ *
266
+ * `ok` is false only for `refused` — a `recorded` mismatch is information, not
267
+ * a fault, and failing on it would make the check red for every operator whose
268
+ * stack is merely wider than the template tree. A `refused` answer, by
269
+ * contrast, is a scaffold that will not run, and the install is the right time
270
+ * to learn that rather than the first `smith new`.
271
+ */
272
+ export function checkStack(answers) {
273
+ const fields = [
274
+ ['language', answers.language],
275
+ ['frontend', answers.frontend],
276
+ ['design_system', answers.designSystem],
277
+ ['design_system_source', answers.designSystemSource],
278
+ ['styling', answers.styling],
279
+ ['backend', answers.backend],
280
+ ['database', answers.database],
281
+ ['orm', answers.orm],
282
+ ['package_manager', answers.packageManager],
283
+ ['repo_shape', answers.repoShape],
284
+ ['lint', answers.lint],
285
+ ['test_unit', answers.testUnit],
286
+ ['test_e2e', answers.testE2e],
287
+ ['ci', answers.ci],
288
+ ['hosting', answers.hosting],
289
+ ];
290
+ const report = fields
291
+ // design_system_source has no verdict of its own: it is half of the
292
+ // design_system answer, and reporting it twice would let one row say
293
+ // `honoured` while the other said `refused` about the same fact.
294
+ .filter(([field]) => field !== 'design_system_source')
295
+ .map(([field, value]) => reportFor(field, value, answers.designSystemSource));
296
+ return { ok: report.every((entry) => entry.support !== 'refused'), answers: report };
297
+ }
298
+ /**
299
+ * The gate `smith new` calls before it writes a single file.
300
+ *
301
+ * Throws on the answers this scaffold cannot serve, and says which file to
302
+ * edit. `ui` is separate because a library project has no opinion about
303
+ * frontend or styling, and refusing a plain `smith new` over a `frontend:
304
+ * react` answer would block work the templates can do perfectly well.
305
+ */
306
+ export function requireScaffoldable(answers, opts) {
307
+ const refuse = (field, value, allowed, what) => {
308
+ throw new StackError('stack.unsupported-answer', `${what} — factory/policies/stack.yml answers ${field}: ${value}, and factory/scaffold/ ships ${allowed.join(' or ')}. Change the answer, or scaffold this project by hand.`, { field, value, allowed });
309
+ };
310
+ if (!SCAFFOLDABLE.language.includes(answers.language)) {
311
+ refuse('language', answers.language, SCAFFOLDABLE.language, 'Cannot scaffold this project');
312
+ }
313
+ if (!opts.ui)
314
+ return;
315
+ if (answers.frontend === 'none') {
316
+ throw new StackError('stack.unsupported-answer', '--ui needs a frontend: factory/policies/stack.yml answers frontend: none. Set it to vue, or drop --ui.', { field: 'frontend', value: answers.frontend });
317
+ }
318
+ if (!SCAFFOLDABLE.frontend.includes(answers.frontend)) {
319
+ refuse('frontend', answers.frontend, ['vue'], 'Cannot scaffold this UI');
320
+ }
321
+ if (!SCAFFOLDABLE.styling.includes(answers.styling)) {
322
+ refuse('styling', answers.styling, SCAFFOLDABLE.styling, 'Cannot scaffold this UI');
323
+ }
324
+ }