@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.
- package/.claude/agents/auditor.md +146 -0
- package/.claude/agents/coder.md +164 -0
- package/.claude/agents/grader.md +110 -0
- package/.claude/agents/merger.md +107 -0
- package/.claude/agents/planner.md +92 -0
- package/.claude/agents/researcher.md +105 -0
- package/.claude/agents/reviewer.md +171 -0
- package/.claude/agents/scribe.md +72 -0
- package/.claude/agents/security-reviewer.md +130 -0
- package/.claude/agents/spec-reviewer.md +215 -0
- package/.claude/agents/tester.md +121 -0
- package/.claude/agents/uiux.md +126 -0
- package/.claude/agents/verifier.md +103 -0
- package/.claude/agents/wave-runner.md +74 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/factory/orchestrator/dist/actors.js +46 -0
- package/factory/orchestrator/dist/agents-registry.js +308 -0
- package/factory/orchestrator/dist/args.js +84 -0
- package/factory/orchestrator/dist/artifacts.js +111 -0
- package/factory/orchestrator/dist/attribution.js +160 -0
- package/factory/orchestrator/dist/audit.js +812 -0
- package/factory/orchestrator/dist/autonomy.js +151 -0
- package/factory/orchestrator/dist/budgetAlarm.js +366 -0
- package/factory/orchestrator/dist/budgets.js +196 -0
- package/factory/orchestrator/dist/claims.js +633 -0
- package/factory/orchestrator/dist/cli.js +3185 -0
- package/factory/orchestrator/dist/commit.js +152 -0
- package/factory/orchestrator/dist/coverage.js +188 -0
- package/factory/orchestrator/dist/crossFinding.js +479 -0
- package/factory/orchestrator/dist/crosscheck.js +396 -0
- package/factory/orchestrator/dist/daemon.js +707 -0
- package/factory/orchestrator/dist/db/projector.js +1317 -0
- package/factory/orchestrator/dist/db/queries.js +1714 -0
- package/factory/orchestrator/dist/db/schema.js +305 -0
- package/factory/orchestrator/dist/delegation.js +400 -0
- package/factory/orchestrator/dist/diffstat.js +164 -0
- package/factory/orchestrator/dist/dispatchAudit.js +275 -0
- package/factory/orchestrator/dist/dotenv.js +72 -0
- package/factory/orchestrator/dist/effort.js +202 -0
- package/factory/orchestrator/dist/epic.js +1018 -0
- package/factory/orchestrator/dist/epicWidth.js +238 -0
- package/factory/orchestrator/dist/errorIssues.js +249 -0
- package/factory/orchestrator/dist/errors.js +11 -0
- package/factory/orchestrator/dist/escalation.js +500 -0
- package/factory/orchestrator/dist/events.js +1015 -0
- package/factory/orchestrator/dist/findingAge.js +91 -0
- package/factory/orchestrator/dist/findingContext.js +103 -0
- package/factory/orchestrator/dist/findings.js +1156 -0
- package/factory/orchestrator/dist/gate.js +911 -0
- package/factory/orchestrator/dist/gh.js +146 -0
- package/factory/orchestrator/dist/git.js +87 -0
- package/factory/orchestrator/dist/goalCheck.js +380 -0
- package/factory/orchestrator/dist/graph.js +61 -0
- package/factory/orchestrator/dist/hookDecision.js +106 -0
- package/factory/orchestrator/dist/immutability.js +171 -0
- package/factory/orchestrator/dist/impact.js +280 -0
- package/factory/orchestrator/dist/integration.js +141 -0
- package/factory/orchestrator/dist/judgePreflight.js +155 -0
- package/factory/orchestrator/dist/judges.js +310 -0
- package/factory/orchestrator/dist/lessonAudit.js +399 -0
- package/factory/orchestrator/dist/lessons.js +1329 -0
- package/factory/orchestrator/dist/mcp.js +555 -0
- package/factory/orchestrator/dist/paths.js +82 -0
- package/factory/orchestrator/dist/plan.js +502 -0
- package/factory/orchestrator/dist/planQuorum.js +421 -0
- package/factory/orchestrator/dist/policy.js +1308 -0
- package/factory/orchestrator/dist/policyHook.js +28 -0
- package/factory/orchestrator/dist/preconditions.js +47 -0
- package/factory/orchestrator/dist/projects.js +134 -0
- package/factory/orchestrator/dist/prompts.js +57 -0
- package/factory/orchestrator/dist/provenance.js +201 -0
- package/factory/orchestrator/dist/providers/api-transport.js +117 -0
- package/factory/orchestrator/dist/providers/cli-transport.js +208 -0
- package/factory/orchestrator/dist/providers/index.js +54 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +152 -0
- package/factory/orchestrator/dist/providers/types.js +49 -0
- package/factory/orchestrator/dist/queue.js +263 -0
- package/factory/orchestrator/dist/quorum.js +347 -0
- package/factory/orchestrator/dist/quorumEscalations.js +132 -0
- package/factory/orchestrator/dist/results.js +65 -0
- package/factory/orchestrator/dist/roadmap.js +437 -0
- package/factory/orchestrator/dist/runtime.js +86 -0
- package/factory/orchestrator/dist/sameMistakeKpi.js +243 -0
- package/factory/orchestrator/dist/sandbox.js +165 -0
- package/factory/orchestrator/dist/scaffold.js +373 -0
- package/factory/orchestrator/dist/scheduler.js +408 -0
- package/factory/orchestrator/dist/schemas.js +195 -0
- package/factory/orchestrator/dist/security.js +222 -0
- package/factory/orchestrator/dist/severity.js +293 -0
- package/factory/orchestrator/dist/spec.js +376 -0
- package/factory/orchestrator/dist/specChange.js +497 -0
- package/factory/orchestrator/dist/stack.js +324 -0
- package/factory/orchestrator/dist/symbols.js +966 -0
- package/factory/orchestrator/dist/taskEvents.js +399 -0
- package/factory/orchestrator/dist/taskId.js +126 -0
- package/factory/orchestrator/dist/taxonomy.js +113 -0
- package/factory/orchestrator/dist/testSelect.js +129 -0
- package/factory/orchestrator/dist/testerAudit.js +264 -0
- package/factory/orchestrator/dist/testgate.js +171 -0
- package/factory/orchestrator/dist/usage.js +874 -0
- package/factory/orchestrator/dist/waivers.js +228 -0
- package/factory/orchestrator/dist/waveBudget.js +223 -0
- package/factory/orchestrator/dist/waveConcurrency.js +250 -0
- package/factory/orchestrator/dist/waveNext.js +210 -0
- package/factory/orchestrator/dist/waveSchedule.js +146 -0
- package/factory/orchestrator/dist/worktree.js +207 -0
- package/factory/orchestrator/drizzle/0000_broken_rockslide.sql +183 -0
- package/factory/orchestrator/drizzle/0001_luxuriant_zaladane.sql +1 -0
- package/factory/orchestrator/drizzle/0002_yellow_wild_pack.sql +8 -0
- package/factory/orchestrator/drizzle/0003_damp_riptide.sql +12 -0
- package/factory/orchestrator/drizzle/0004_broad_lady_vermin.sql +1 -0
- package/factory/orchestrator/drizzle/0005_romantic_jocasta.sql +2 -0
- package/factory/orchestrator/drizzle/0006_bent_madelyne_pryor.sql +16 -0
- package/factory/orchestrator/drizzle/0007_overjoyed_goliath.sql +1 -0
- package/factory/orchestrator/drizzle/0008_smart_iron_patriot.sql +2 -0
- package/factory/orchestrator/drizzle/0009_agents_epic_id.sql +2 -0
- package/factory/orchestrator/drizzle/0010_milestone_kind.sql +1 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +83 -0
- package/factory/policies/budgets.yml +262 -0
- package/factory/policies/crosscheck.yml +308 -0
- package/factory/policies/delegation.yml +44 -0
- package/factory/policies/effort.yml +142 -0
- package/factory/policies/guardrails.yml +306 -0
- package/factory/policies/lessons.md +239 -0
- package/factory/policies/scheduler.yml +90 -0
- package/factory/policies/sensitive-paths.yml +148 -0
- package/factory/policies/severity.yml +83 -0
- package/factory/policies/stack.yml +60 -0
- package/factory/policies/taxonomy.yml +340 -0
- package/factory/policies/worktree.yml +95 -0
- package/factory/scaffold/base/.github/workflows/ci.yml +34 -0
- package/factory/scaffold/base/.gitignore.tmpl +11 -0
- package/factory/scaffold/base/AGENTS.md +33 -0
- package/factory/scaffold/base/README.md +25 -0
- package/factory/scaffold/base/biome.json.tmpl +30 -0
- package/factory/scaffold/base/package.json +25 -0
- package/factory/scaffold/base/pnpm-workspace.yaml.tmpl +16 -0
- package/factory/scaffold/base/src/index.ts +4 -0
- package/factory/scaffold/base/test/index.test.ts +8 -0
- package/factory/scaffold/base/tsconfig.json +20 -0
- package/factory/scaffold/base/tsconfig.test.json +9 -0
- package/factory/scaffold/base/vitest.config.ts +29 -0
- package/factory/scaffold/mcp/mcp.manifest.json +22 -0
- package/factory/scaffold/mcp/package.fragment.json +9 -0
- package/factory/scaffold/mcp/src/mcp/README.md +63 -0
- package/factory/scaffold/mcp/src/mcp/guard.ts +110 -0
- package/factory/scaffold/mcp/src/mcp/redact.ts +69 -0
- package/factory/scaffold/mcp/src/mcp/server.ts +55 -0
- package/factory/scaffold/mcp/src/mcp/tools/health.ts +60 -0
- package/factory/scaffold/mcp/test/mcp/guard.test.ts +108 -0
- package/factory/scaffold/mcp/test/mcp/redact.test.ts +61 -0
- package/factory/scaffold/mcp/test/mcp/surface.test.ts +100 -0
- package/factory/scaffold/ui/index.html +12 -0
- package/factory/scaffold/ui/package.fragment.json +13 -0
- package/factory/scaffold/ui/src/App.vue +10 -0
- package/factory/scaffold/ui/src/main.ts +5 -0
- package/factory/scaffold/ui/src/shims.d.ts +11 -0
- package/factory/scaffold/ui/vite.config.ts +20 -0
- package/factory/scaffold/ui-tailwind/package.fragment.json +6 -0
- package/factory/scaffold/ui-tailwind/vite.config.ts +26 -0
- package/factory/specs/schema/event.schema.json +82 -0
- package/factory/specs/schema/finding-evidence.schema.json +45 -0
- package/factory/specs/schema/finding.schema.json +115 -0
- package/factory/specs/schema/grader-verdict.schema.json +50 -0
- package/factory/specs/schema/judge-verdict.schema.json +19 -0
- package/factory/specs/schema/lesson.schema.json +85 -0
- package/factory/specs/schema/mcp-manifest.schema.json +230 -0
- package/factory/specs/schema/result.schema.json +81 -0
- package/factory/specs/schema/spec-change-request.schema.json +46 -0
- package/factory/specs/schema/task-spec.schema.json +134 -0
- 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
|
+
}
|