@dewtech/dare-cli 3.11.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/__tests__/cli-only-invariants.test.d.ts +2 -0
- package/dist/__tests__/cli-only-invariants.test.d.ts.map +1 -0
- package/dist/__tests__/cli-only-invariants.test.js +100 -0
- package/dist/__tests__/cli-only-invariants.test.js.map +1 -0
- package/dist/__tests__/cli-only-regression.test.d.ts +2 -0
- package/dist/__tests__/cli-only-regression.test.d.ts.map +1 -0
- package/dist/__tests__/cli-only-regression.test.js +29 -0
- package/dist/__tests__/cli-only-regression.test.js.map +1 -0
- package/dist/__tests__/ensure-skills.test.js +5 -0
- package/dist/__tests__/ensure-skills.test.js.map +1 -1
- package/dist/__tests__/ide-command-parity.test.js +1 -0
- package/dist/__tests__/ide-command-parity.test.js.map +1 -1
- package/dist/__tests__/project-generator.test.js +17 -0
- package/dist/__tests__/project-generator.test.js.map +1 -1
- package/dist/__tests__/reverse-facts.test.js +1 -0
- package/dist/__tests__/reverse-facts.test.js.map +1 -1
- package/dist/__tests__/terminal-parity-regression.test.d.ts +2 -0
- package/dist/__tests__/terminal-parity-regression.test.d.ts.map +1 -0
- package/dist/__tests__/terminal-parity-regression.test.js +116 -0
- package/dist/__tests__/terminal-parity-regression.test.js.map +1 -0
- package/dist/__tests__/terminal-parity.test.d.ts +2 -0
- package/dist/__tests__/terminal-parity.test.d.ts.map +1 -0
- package/dist/__tests__/terminal-parity.test.js +81 -0
- package/dist/__tests__/terminal-parity.test.js.map +1 -0
- package/dist/agent/__tests__/antigravity-driver.test.d.ts +2 -0
- package/dist/agent/__tests__/antigravity-driver.test.d.ts.map +1 -0
- package/dist/agent/__tests__/antigravity-driver.test.js +52 -0
- package/dist/agent/__tests__/antigravity-driver.test.js.map +1 -0
- package/dist/agent/__tests__/codex-driver.test.d.ts +2 -0
- package/dist/agent/__tests__/codex-driver.test.d.ts.map +1 -0
- package/dist/agent/__tests__/codex-driver.test.js +68 -0
- package/dist/agent/__tests__/codex-driver.test.js.map +1 -0
- package/dist/agent/__tests__/cursor-driver.test.d.ts +2 -0
- package/dist/agent/__tests__/cursor-driver.test.d.ts.map +1 -0
- package/dist/agent/__tests__/cursor-driver.test.js +52 -0
- package/dist/agent/__tests__/cursor-driver.test.js.map +1 -0
- package/dist/agent/driver.d.ts +1 -1
- package/dist/agent/driver.d.ts.map +1 -1
- package/dist/agent/drivers/antigravity.d.ts +8 -0
- package/dist/agent/drivers/antigravity.d.ts.map +1 -0
- package/dist/agent/drivers/antigravity.js +99 -0
- package/dist/agent/drivers/antigravity.js.map +1 -0
- package/dist/agent/drivers/codex.d.ts +12 -0
- package/dist/agent/drivers/codex.d.ts.map +1 -0
- package/dist/agent/drivers/codex.js +137 -0
- package/dist/agent/drivers/codex.js.map +1 -0
- package/dist/agent/drivers/cursor.d.ts +8 -0
- package/dist/agent/drivers/cursor.d.ts.map +1 -0
- package/dist/agent/drivers/cursor.js +99 -0
- package/dist/agent/drivers/cursor.js.map +1 -0
- package/dist/ai/__tests__/ai-core.test.d.ts +2 -0
- package/dist/ai/__tests__/ai-core.test.d.ts.map +1 -0
- package/dist/ai/__tests__/ai-core.test.js +41 -0
- package/dist/ai/__tests__/ai-core.test.js.map +1 -0
- package/dist/ai/__tests__/parity.test.d.ts +2 -0
- package/dist/ai/__tests__/parity.test.d.ts.map +1 -0
- package/dist/ai/__tests__/parity.test.js +36 -0
- package/dist/ai/__tests__/parity.test.js.map +1 -0
- package/dist/ai/__tests__/pipeline.test.d.ts +2 -0
- package/dist/ai/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/ai/__tests__/pipeline.test.js +147 -0
- package/dist/ai/__tests__/pipeline.test.js.map +1 -0
- package/dist/ai/__tests__/refine-bridge.test.d.ts +2 -0
- package/dist/ai/__tests__/refine-bridge.test.d.ts.map +1 -0
- package/dist/ai/__tests__/refine-bridge.test.js +17 -0
- package/dist/ai/__tests__/refine-bridge.test.js.map +1 -0
- package/dist/ai/__tests__/resolve.test.d.ts +2 -0
- package/dist/ai/__tests__/resolve.test.d.ts.map +1 -0
- package/dist/ai/__tests__/resolve.test.js +42 -0
- package/dist/ai/__tests__/resolve.test.js.map +1 -0
- package/dist/ai/capabilities.d.ts +3 -0
- package/dist/ai/capabilities.d.ts.map +1 -0
- package/dist/ai/capabilities.js +11 -0
- package/dist/ai/capabilities.js.map +1 -0
- package/dist/ai/command-options.d.ts +10 -0
- package/dist/ai/command-options.d.ts.map +1 -0
- package/dist/ai/command-options.js +15 -0
- package/dist/ai/command-options.js.map +1 -0
- package/dist/ai/config.d.ts +27 -0
- package/dist/ai/config.d.ts.map +1 -0
- package/dist/ai/config.js +89 -0
- package/dist/ai/config.js.map +1 -0
- package/dist/ai/parity.d.ts +13 -0
- package/dist/ai/parity.d.ts.map +1 -0
- package/dist/ai/parity.js +87 -0
- package/dist/ai/parity.js.map +1 -0
- package/dist/ai/parse-json-output.d.ts +5 -0
- package/dist/ai/parse-json-output.d.ts.map +1 -0
- package/dist/ai/parse-json-output.js +25 -0
- package/dist/ai/parse-json-output.js.map +1 -0
- package/dist/ai/pipeline.d.ts +20 -0
- package/dist/ai/pipeline.d.ts.map +1 -0
- package/dist/ai/pipeline.js +303 -0
- package/dist/ai/pipeline.js.map +1 -0
- package/dist/ai/prompts.d.ts +6 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +49 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/providers.d.ts +63 -0
- package/dist/ai/providers.d.ts.map +1 -0
- package/dist/ai/providers.js +297 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/ai/refine-bridge.d.ts +5 -0
- package/dist/ai/refine-bridge.d.ts.map +1 -0
- package/dist/ai/refine-bridge.js +14 -0
- package/dist/ai/refine-bridge.js.map +1 -0
- package/dist/ai/registry.d.ts +12 -0
- package/dist/ai/registry.d.ts.map +1 -0
- package/dist/ai/registry.js +43 -0
- package/dist/ai/registry.js.map +1 -0
- package/dist/ai/resolve.d.ts +28 -0
- package/dist/ai/resolve.d.ts.map +1 -0
- package/dist/ai/resolve.js +83 -0
- package/dist/ai/resolve.js.map +1 -0
- package/dist/ai/schemas.d.ts +175 -0
- package/dist/ai/schemas.d.ts.map +1 -0
- package/dist/ai/schemas.js +199 -0
- package/dist/ai/schemas.js.map +1 -0
- package/dist/ai/types.d.ts +52 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +8 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/bin/dare.js +2 -0
- package/dist/bin/dare.js.map +1 -1
- package/dist/commands/__tests__/ai-command.test.d.ts +2 -0
- package/dist/commands/__tests__/ai-command.test.d.ts.map +1 -0
- package/dist/commands/__tests__/ai-command.test.js +68 -0
- package/dist/commands/__tests__/ai-command.test.js.map +1 -0
- package/dist/commands/__tests__/execute-agent.test.js +82 -0
- package/dist/commands/__tests__/execute-agent.test.js.map +1 -1
- package/dist/commands/ai.d.ts +3 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +141 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/blueprint.d.ts.map +1 -1
- package/dist/commands/blueprint.js +17 -3
- package/dist/commands/blueprint.js.map +1 -1
- package/dist/commands/design.d.ts.map +1 -1
- package/dist/commands/design.js +21 -2
- package/dist/commands/design.js.map +1 -1
- package/dist/commands/discover.d.ts.map +1 -1
- package/dist/commands/discover.js +9 -1
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/dna.d.ts.map +1 -1
- package/dist/commands/dna.js +23 -3
- package/dist/commands/dna.js.map +1 -1
- package/dist/commands/execute.d.ts +11 -0
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +111 -4
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +14 -2
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/patterns.d.ts.map +1 -1
- package/dist/commands/patterns.js +14 -2
- package/dist/commands/patterns.js.map +1 -1
- package/dist/commands/refine.d.ts.map +1 -1
- package/dist/commands/refine.js +23 -2
- package/dist/commands/refine.js.map +1 -1
- package/dist/commands/reverse.d.ts.map +1 -1
- package/dist/commands/reverse.js +28 -3
- package/dist/commands/reverse.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +25 -3
- package/dist/commands/review.js.map +1 -1
- package/dist/core/types/project.d.ts +1 -1
- package/dist/core/types/project.d.ts.map +1 -1
- package/dist/dag-runner/run_dag.d.ts +1 -1
- package/dist/dag-runner/run_dag.d.ts.map +1 -1
- package/dist/exec/safe-spawn.d.ts.map +1 -1
- package/dist/exec/safe-spawn.js +6 -1
- package/dist/exec/safe-spawn.js.map +1 -1
- package/dist/skills/bundled.d.ts +5 -0
- package/dist/skills/bundled.d.ts.map +1 -0
- package/dist/skills/bundled.js +34 -0
- package/dist/skills/bundled.js.map +1 -0
- package/dist/skills/commands/add.d.ts +1 -3
- package/dist/skills/commands/add.d.ts.map +1 -1
- package/dist/skills/commands/add.js +20 -3
- package/dist/skills/commands/add.js.map +1 -1
- package/dist/skills/tests/bundled.spec.d.ts +2 -0
- package/dist/skills/tests/bundled.spec.d.ts.map +1 -0
- package/dist/skills/tests/bundled.spec.js +24 -0
- package/dist/skills/tests/bundled.spec.js.map +1 -0
- package/dist/types/UpdateManifest.types.d.ts +1 -1
- package/dist/types/UpdateManifest.types.d.ts.map +1 -1
- package/dist/utils/dag-converter.js +1 -1
- package/dist/utils/dag-converter.js.map +1 -1
- package/dist/utils/project-detector.d.ts +1 -0
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +8 -0
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/project-generator.d.ts +1 -1
- package/dist/utils/project-generator.d.ts.map +1 -1
- package/dist/utils/project-generator.js +23 -2
- package/dist/utils/project-generator.js.map +1 -1
- package/dist/utils/templates.d.ts +2 -0
- package/dist/utils/templates.d.ts.map +1 -1
- package/dist/utils/templates.js +74 -0
- package/dist/utils/templates.js.map +1 -1
- package/dist/verification/__tests__/safe-spawn.test.js +12 -0
- package/dist/verification/__tests__/safe-spawn.test.js.map +1 -1
- package/package.json +2 -1
- package/skills/dare-ax/generator.ts +325 -0
- package/skills/dare-ax/index.ts +19 -0
- package/skills/dare-ax/metrics.ts +352 -0
- package/skills/dare-ax/package-lock.json +1855 -0
- package/skills/dare-ax/package.json +50 -0
- package/skills/dare-ax/secret-detector.ts +123 -0
- package/skills/dare-ax/skill.yml +19 -0
- package/skills/dare-ax/templates/llms.txt.jinja2 +80 -0
- package/skills/dare-ax/tests/generator.spec.ts +193 -0
- package/skills/dare-ax/tests/metrics.spec.ts +394 -0
- package/skills/dare-ax/tests/validator.spec.ts +298 -0
- package/skills/dare-ax/tsconfig.json +18 -0
- package/skills/dare-ax/types.ts +79 -0
- package/skills/dare-ax/validator.ts +238 -0
- package/skills/dare-frontend-design/generator.ts +616 -0
- package/skills/dare-frontend-design/index.ts +25 -0
- package/skills/dare-frontend-design/linter.ts +227 -0
- package/skills/dare-frontend-design/metrics.ts +82 -0
- package/skills/dare-frontend-design/package-lock.json +1855 -0
- package/skills/dare-frontend-design/package.json +43 -0
- package/skills/dare-frontend-design/skill.yml +20 -0
- package/skills/dare-frontend-design/tests/frontend_design.spec.ts +435 -0
- package/skills/dare-frontend-design/tsconfig.json +18 -0
- package/skills/dare-frontend-design/types.ts +62 -0
- package/skills/dare-layered-design/generator.ts +740 -0
- package/skills/dare-layered-design/index.ts +17 -0
- package/skills/dare-layered-design/linter.ts +462 -0
- package/skills/dare-layered-design/metrics.ts +409 -0
- package/skills/dare-layered-design/package-lock.json +1855 -0
- package/skills/dare-layered-design/package.json +50 -0
- package/skills/dare-layered-design/skill.yml +35 -0
- package/skills/dare-layered-design/tests/generator.spec.ts +156 -0
- package/skills/dare-layered-design/tests/linter.spec.ts +255 -0
- package/skills/dare-layered-design/tests/metrics.spec.ts +286 -0
- package/skills/dare-layered-design/tsconfig.json +18 -0
- package/skills/dare-layered-design/types.ts +48 -0
- package/skills/dare-llm-integration/cache/llm_cache.ts +122 -0
- package/skills/dare-llm-integration/index.ts +49 -0
- package/skills/dare-llm-integration/metrics.ts +107 -0
- package/skills/dare-llm-integration/package-lock.json +1855 -0
- package/skills/dare-llm-integration/package.json +49 -0
- package/skills/dare-llm-integration/prompts/prompt_loader.ts +258 -0
- package/skills/dare-llm-integration/providers/anthropic_provider.ts +159 -0
- package/skills/dare-llm-integration/providers/dummy_provider.ts +113 -0
- package/skills/dare-llm-integration/providers/llm_provider.ts +6 -0
- package/skills/dare-llm-integration/providers/openai_provider.ts +215 -0
- package/skills/dare-llm-integration/rate_limit/token_bucket.ts +86 -0
- package/skills/dare-llm-integration/skill.yml +23 -0
- package/skills/dare-llm-integration/tests/fixtures/greet_v1.jinja2 +1 -0
- package/skills/dare-llm-integration/tests/fixtures/summarize_v1.jinja2 +1 -0
- package/skills/dare-llm-integration/tests/fixtures/summarize_v2.jinja2 +3 -0
- package/skills/dare-llm-integration/tests/llm_integration.spec.ts +657 -0
- package/skills/dare-llm-integration/tsconfig.json +23 -0
- package/skills/dare-llm-integration/types.ts +91 -0
- package/skills/dare-llm-integration/validators/output_validator.ts +200 -0
- package/skills/dare-quality-telemetry/collect.ts +134 -0
- package/skills/dare-quality-telemetry/collectors/dare_ax_collector.ts +301 -0
- package/skills/dare-quality-telemetry/collectors/dare_layered_design_collector.ts +406 -0
- package/skills/dare-quality-telemetry/collectors/index.ts +24 -0
- package/skills/dare-quality-telemetry/github_actions_template.ts +25 -0
- package/skills/dare-quality-telemetry/index.ts +18 -0
- package/skills/dare-quality-telemetry/metrics.ts +137 -0
- package/skills/dare-quality-telemetry/package-lock.json +1855 -0
- package/skills/dare-quality-telemetry/package.json +48 -0
- package/skills/dare-quality-telemetry/regression.ts +60 -0
- package/skills/dare-quality-telemetry/reporter.ts +132 -0
- package/skills/dare-quality-telemetry/skill.yml +18 -0
- package/skills/dare-quality-telemetry/tests/quality_telemetry.spec.ts +885 -0
- package/skills/dare-quality-telemetry/tsconfig.json +19 -0
- package/skills/dare-quality-telemetry/types.ts +41 -0
- package/skills/dare-realtime/event_registry.ts +101 -0
- package/skills/dare-realtime/index.ts +30 -0
- package/skills/dare-realtime/metrics.ts +84 -0
- package/skills/dare-realtime/package-lock.json +1855 -0
- package/skills/dare-realtime/package.json +43 -0
- package/skills/dare-realtime/reconnect_strategy.ts +85 -0
- package/skills/dare-realtime/schema_validator.ts +80 -0
- package/skills/dare-realtime/skill.yml +21 -0
- package/skills/dare-realtime/subscription_manager.ts +106 -0
- package/skills/dare-realtime/tests/realtime.spec.ts +482 -0
- package/skills/dare-realtime/tsconfig.json +18 -0
- package/skills/dare-realtime/types.ts +51 -0
- package/templates/ide/antigravity/.agents/skills/dare-ai/SKILL.md +17 -0
- package/templates/ide/antigravity/.agents/skills/dare-blueprint/SKILL.md +2 -0
- package/templates/ide/antigravity/.agents/skills/dare-design/SKILL.md +2 -0
- package/templates/ide/antigravity/.agents/skills/dare-dna/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-migrate/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-patterns/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-refine/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-reverse/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-review/SKILL.md +3 -0
- package/templates/ide/claude/.claude/commands/dare-ai.md +17 -0
- package/templates/ide/claude/.claude/commands/dare-blueprint.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-design.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-dna.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-migrate.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-patterns.md +3 -0
- package/templates/ide/claude/.claude/commands/dare-refine.md +3 -0
- package/templates/ide/claude/.claude/commands/dare-reverse.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-review.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-ai.md +17 -0
- package/templates/ide/cursor/.cursor/commands/dare-blueprint.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-design.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-dna.md +2 -0
- package/templates/ide/cursor/.cursor/commands/dare-migrate.md +2 -0
- package/templates/ide/cursor/.cursor/commands/dare-patterns.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-refine.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-reverse.md +2 -0
- package/templates/ide/cursor/.cursor/commands/dare-review.md +3 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dare-layered-design — LayeredDesignGenerator
|
|
3
|
+
* Scaffolds the 5-layer directory structure for a DARE project.
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { Language, ScaffoldOptions, ScaffoldResult } from './types.js';
|
|
10
|
+
|
|
11
|
+
/** Per-language configuration for directory and file naming */
|
|
12
|
+
interface LayerConfig {
|
|
13
|
+
dirName: string;
|
|
14
|
+
description: string;
|
|
15
|
+
exampleFile?: (entityName: string, ext: string) => { name: string; content: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LAYERS: LayerConfig[] = [
|
|
19
|
+
{
|
|
20
|
+
dirName: 'handlers',
|
|
21
|
+
description: 'HTTP/gRPC entry points — receives requests, validates input, calls services',
|
|
22
|
+
exampleFile: (entity, ext) => ({
|
|
23
|
+
name: `${toSnakeCase(entity)}_handler${ext}`,
|
|
24
|
+
content: generateHandlerExample(entity, ext),
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
dirName: 'services',
|
|
29
|
+
description: 'Business logic — one operation per service class, no HTTP or DB concerns',
|
|
30
|
+
exampleFile: (entity, ext) => ({
|
|
31
|
+
name: `create_${toSnakeCase(entity)}_service${ext}`,
|
|
32
|
+
content: generateServiceExample(entity, ext),
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
dirName: 'repositories',
|
|
37
|
+
description: 'Data access — abstractions over DB/cache/external APIs',
|
|
38
|
+
exampleFile: (entity, ext) => ({
|
|
39
|
+
name: `${toSnakeCase(entity)}_repository${ext}`,
|
|
40
|
+
content: generateRepositoryExample(entity, ext),
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
dirName: 'models',
|
|
45
|
+
description: 'Domain objects — entities and value objects, no HTTP or DB concerns',
|
|
46
|
+
exampleFile: (entity, ext) => ({
|
|
47
|
+
name: `${toSnakeCase(entity)}${ext}`,
|
|
48
|
+
content: generateModelExample(entity, ext),
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
dirName: 'presenters',
|
|
53
|
+
description: 'Serializers — converts Models to JSON/XML/DTO for responses',
|
|
54
|
+
exampleFile: (entity, ext) => ({
|
|
55
|
+
name: `${toSnakeCase(entity)}_presenter${ext}`,
|
|
56
|
+
content: generatePresenterExample(entity, ext),
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export class LayeredDesignGenerator {
|
|
62
|
+
/**
|
|
63
|
+
* Scaffolds the layered directory structure inside the given project path.
|
|
64
|
+
*
|
|
65
|
+
* Creates:
|
|
66
|
+
* - {srcDir}/handlers/
|
|
67
|
+
* - {srcDir}/services/
|
|
68
|
+
* - {srcDir}/repositories/
|
|
69
|
+
* - {srcDir}/models/
|
|
70
|
+
* - {srcDir}/presenters/
|
|
71
|
+
*
|
|
72
|
+
* Each directory gets a README.md describing its contract, plus either
|
|
73
|
+
* a .gitkeep (default) or example files (if withExamples: true).
|
|
74
|
+
*
|
|
75
|
+
* @param projectPath - Absolute path to the project root.
|
|
76
|
+
* @param options - Scaffold options.
|
|
77
|
+
* @returns ScaffoldResult with lists of created dirs and files.
|
|
78
|
+
*/
|
|
79
|
+
scaffold(projectPath: string, options: ScaffoldOptions = {}): ScaffoldResult {
|
|
80
|
+
const {
|
|
81
|
+
srcDir = 'src',
|
|
82
|
+
language = 'typescript',
|
|
83
|
+
withExamples = false,
|
|
84
|
+
exampleEntity = 'example',
|
|
85
|
+
} = options;
|
|
86
|
+
|
|
87
|
+
const ext = languageToExtension(language);
|
|
88
|
+
const rootDir = path.join(projectPath, srcDir);
|
|
89
|
+
|
|
90
|
+
const createdDirs: string[] = [];
|
|
91
|
+
const createdFiles: string[] = [];
|
|
92
|
+
|
|
93
|
+
// Ensure src dir exists
|
|
94
|
+
if (!fs.existsSync(rootDir)) {
|
|
95
|
+
fs.mkdirSync(rootDir, { recursive: true });
|
|
96
|
+
createdDirs.push(rootDir);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const layer of LAYERS) {
|
|
100
|
+
const layerDir = path.join(rootDir, layer.dirName);
|
|
101
|
+
|
|
102
|
+
// Create directory
|
|
103
|
+
if (!fs.existsSync(layerDir)) {
|
|
104
|
+
fs.mkdirSync(layerDir, { recursive: true });
|
|
105
|
+
createdDirs.push(layerDir);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create README.md with layer contract
|
|
109
|
+
const readmePath = path.join(layerDir, 'README.md');
|
|
110
|
+
if (!fs.existsSync(readmePath)) {
|
|
111
|
+
fs.writeFileSync(readmePath, generateLayerReadme(layer.dirName, layer.description, language), 'utf-8');
|
|
112
|
+
createdFiles.push(readmePath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (withExamples && layer.exampleFile) {
|
|
116
|
+
// Create example file
|
|
117
|
+
const { name, content } = layer.exampleFile(exampleEntity, ext);
|
|
118
|
+
const examplePath = path.join(layerDir, name);
|
|
119
|
+
if (!fs.existsSync(examplePath)) {
|
|
120
|
+
fs.writeFileSync(examplePath, content, 'utf-8');
|
|
121
|
+
createdFiles.push(examplePath);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Create .gitkeep so the directory is tracked by git
|
|
125
|
+
const gitkeepPath = path.join(layerDir, '.gitkeep');
|
|
126
|
+
if (!fs.existsSync(gitkeepPath) && !hasNonReadmeFiles(layerDir)) {
|
|
127
|
+
fs.writeFileSync(gitkeepPath, '', 'utf-8');
|
|
128
|
+
createdFiles.push(gitkeepPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Create a top-level ARCHITECTURE.md in srcDir
|
|
134
|
+
const archPath = path.join(rootDir, 'ARCHITECTURE.md');
|
|
135
|
+
if (!fs.existsSync(archPath)) {
|
|
136
|
+
fs.writeFileSync(archPath, generateArchitectureDoc(srcDir, language), 'utf-8');
|
|
137
|
+
createdFiles.push(archPath);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { createdDirs, createdFiles };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Layer README generator ──────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
function generateLayerReadme(dirName: string, description: string, _language: Language): string {
|
|
147
|
+
const rules = LAYER_RULES[dirName] ?? [];
|
|
148
|
+
const rulesText = rules.map((r) => `- ${r}`).join('\n');
|
|
149
|
+
|
|
150
|
+
return `# ${capitalize(dirName)}
|
|
151
|
+
|
|
152
|
+
${description}
|
|
153
|
+
|
|
154
|
+
## Rules
|
|
155
|
+
|
|
156
|
+
${rulesText}
|
|
157
|
+
|
|
158
|
+
## Dependency Rule
|
|
159
|
+
|
|
160
|
+
\`\`\`
|
|
161
|
+
Handler → Service → Repository → Model
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
This layer is: **${dirName.toUpperCase()}**
|
|
165
|
+
|
|
166
|
+
${LAYER_DEPENDENCY_TEXT[dirName] ?? ''}
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
*Generated by dare-layered-design v1.0.0 — DARE Method*
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const LAYER_RULES: Record<string, string[]> = {
|
|
174
|
+
handlers: [
|
|
175
|
+
'May call Services only — never call Repositories or Models directly',
|
|
176
|
+
'Validate HTTP input (types, required fields, format) — do NOT validate business rules',
|
|
177
|
+
'Return HTTP responses (status codes, headers) — the only layer that knows about HTTP',
|
|
178
|
+
'Authenticate and authorize via middleware — do NOT inline auth logic',
|
|
179
|
+
'Receive services via dependency injection — do NOT instantiate with new Service()',
|
|
180
|
+
],
|
|
181
|
+
services: [
|
|
182
|
+
'May call Repositories only — never call Handlers or HTTP concerns',
|
|
183
|
+
'One class/module = one business operation (CreateUser, not UserService with 10 methods)',
|
|
184
|
+
'Receive repositories via dependency injection — do NOT instantiate with new Repository()',
|
|
185
|
+
'Return domain Models — do NOT return HTTP-specific DTOs',
|
|
186
|
+
'No knowledge of HTTP status codes, headers, or request/response objects',
|
|
187
|
+
],
|
|
188
|
+
repositories: [
|
|
189
|
+
'May call Models only — no awareness of Handlers or Services above',
|
|
190
|
+
'Abstract over storage: define an interface/trait, provide multiple implementations',
|
|
191
|
+
'Never throw HTTP-specific exceptions (no 404 exceptions — return null or custom errors)',
|
|
192
|
+
'Parameterized queries only — never raw string concatenation in SQL',
|
|
193
|
+
'InMemory implementation required for unit tests (no real DB in unit tests)',
|
|
194
|
+
],
|
|
195
|
+
models: [
|
|
196
|
+
'Pure domain objects — no HTTP imports, no database imports, no framework imports',
|
|
197
|
+
'Business rules that belong to the entity only (e.g., User#full_name)',
|
|
198
|
+
'No side effects (no email sending, no DB writes) — leave those to Services',
|
|
199
|
+
'Immutable preferred — use value objects for IDs, money, dates',
|
|
200
|
+
'Serializers/Presenters are separate — Model does not know JSON format',
|
|
201
|
+
],
|
|
202
|
+
presenters: [
|
|
203
|
+
'Converts Models to serializable format (JSON, XML, CSV, gRPC message)',
|
|
204
|
+
'No business logic — only formatting and field mapping',
|
|
205
|
+
'May be called from Handlers only — not from Services or Repositories',
|
|
206
|
+
'One presenter per output format (UserJSONPresenter, UserCSVPresenter)',
|
|
207
|
+
'Handle date formatting, currency formatting, field renaming',
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const LAYER_DEPENDENCY_TEXT: Record<string, string> = {
|
|
212
|
+
handlers: 'Can call: **Services** only\nCannot call: Repositories, Models directly',
|
|
213
|
+
services: 'Can call: **Repositories**\nCannot call: Handlers, HTTP concerns',
|
|
214
|
+
repositories: 'Can call: **Models**\nCannot call: Services, Handlers',
|
|
215
|
+
models: 'Can call: nothing external\nCannot call: any other layer',
|
|
216
|
+
presenters: 'Can call: **Models** (read-only)\nCannot call: Services, Repositories',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// ── Architecture doc ─────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
function generateArchitectureDoc(srcDir: string, _language: Language): string {
|
|
222
|
+
return `# Architecture — Layered Design
|
|
223
|
+
|
|
224
|
+
This project follows the **DARE Layered Design** pattern.
|
|
225
|
+
|
|
226
|
+
## Dependency Rule
|
|
227
|
+
|
|
228
|
+
\`\`\`
|
|
229
|
+
Handler → Service → Repository → Model
|
|
230
|
+
↑ ↑
|
|
231
|
+
└── Presenter ──────────┘
|
|
232
|
+
\`\`\`
|
|
233
|
+
|
|
234
|
+
Dependencies flow **downward only**. Upper layers call lower layers; lower layers never call up.
|
|
235
|
+
|
|
236
|
+
## Layer Responsibilities
|
|
237
|
+
|
|
238
|
+
| Layer | Directory | Responsibility |
|
|
239
|
+
|-------|-----------|----------------|
|
|
240
|
+
| Handlers | \`${srcDir}/handlers/\` | HTTP entry points; input validation; auth |
|
|
241
|
+
| Services | \`${srcDir}/services/\` | Business logic; one operation per class |
|
|
242
|
+
| Repositories | \`${srcDir}/repositories/\` | Data access; abstract over DB/cache |
|
|
243
|
+
| Models | \`${srcDir}/models/\` | Domain objects; no HTTP or DB concerns |
|
|
244
|
+
| Presenters | \`${srcDir}/presenters/\` | Serializers; Model → JSON/XML/DTO |
|
|
245
|
+
|
|
246
|
+
## Rules
|
|
247
|
+
|
|
248
|
+
1. **Handlers** never call Repositories directly — always through Services
|
|
249
|
+
2. **Services** never know about HTTP (status codes, request objects)
|
|
250
|
+
3. **Repositories** never throw HTTP-specific exceptions
|
|
251
|
+
4. **Models** are pure domain objects — no framework imports
|
|
252
|
+
5. **Dependency Injection** is used at every layer boundary
|
|
253
|
+
|
|
254
|
+
## CI Validation
|
|
255
|
+
|
|
256
|
+
Run \`dare metrics collect\` to check M-02 (0% Handler→Repository violations).
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
*Generated by dare-layered-design v1.0.0 — DARE Method*
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Example file generators ──────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
function generateHandlerExample(entity: string, ext: string): string {
|
|
266
|
+
const Name = capitalize(toCamelCase(entity));
|
|
267
|
+
const snake = toSnakeCase(entity);
|
|
268
|
+
|
|
269
|
+
if (ext === '.ts') {
|
|
270
|
+
return `/**
|
|
271
|
+
* ${Name}Handler — HTTP handler for ${entity} resource
|
|
272
|
+
* Responsibility: receive requests, validate input, call service, return response
|
|
273
|
+
*
|
|
274
|
+
* RULE: Never call ${Name}Repository directly — always through ${Name}Service
|
|
275
|
+
*/
|
|
276
|
+
import { Request, Response } from 'express';
|
|
277
|
+
|
|
278
|
+
// Services injected — never instantiated here
|
|
279
|
+
interface ${Name}HandlerDeps {
|
|
280
|
+
create${Name}Service: Create${Name}Service;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface Create${Name}Service {
|
|
284
|
+
execute(input: Create${Name}Input): Promise<${Name}>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
interface Create${Name}Input {
|
|
288
|
+
name: string;
|
|
289
|
+
email: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface ${Name} {
|
|
293
|
+
id: string;
|
|
294
|
+
name: string;
|
|
295
|
+
email: string;
|
|
296
|
+
createdAt: Date;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export class ${Name}Handler {
|
|
300
|
+
constructor(private readonly deps: ${Name}HandlerDeps) {}
|
|
301
|
+
|
|
302
|
+
async create(req: Request, res: Response): Promise<void> {
|
|
303
|
+
// Input validation (HTTP layer responsibility)
|
|
304
|
+
const { name, email } = req.body as Record<string, string>;
|
|
305
|
+
if (!name || !email) {
|
|
306
|
+
res.status(422).json({
|
|
307
|
+
type: '/errors/validation',
|
|
308
|
+
title: 'Validation error',
|
|
309
|
+
status: 422,
|
|
310
|
+
detail: 'name and email are required',
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Delegate to service (business logic)
|
|
316
|
+
const result = await this.deps.create${Name}Service.execute({ name, email });
|
|
317
|
+
|
|
318
|
+
// Return response (HTTP layer responsibility)
|
|
319
|
+
res.status(201).json(result);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (ext === '.rb') {
|
|
326
|
+
return `# ${Name}Controller — HTTP controller for ${snake} resource
|
|
327
|
+
# Responsibility: receive requests, validate input, call service, return response
|
|
328
|
+
#
|
|
329
|
+
# RULE: Never call ${Name}Repository directly — always through ${Name}Service
|
|
330
|
+
class ${Name}Controller < ApplicationController
|
|
331
|
+
# Services injected via constructor — never instantiated here
|
|
332
|
+
def initialize(create_${snake}_service:)
|
|
333
|
+
@create_${snake}_service = create_${snake}_service
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def create
|
|
337
|
+
result = @create_${snake}_service.call(
|
|
338
|
+
name: params.require(:name),
|
|
339
|
+
email: params.require(:email)
|
|
340
|
+
)
|
|
341
|
+
render json: result, status: :created
|
|
342
|
+
rescue ActionController::ParameterMissing => e
|
|
343
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return `# ${Name} handler — see README.md for rules\n# Language: ${ext}\n`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function generateServiceExample(entity: string, ext: string): string {
|
|
353
|
+
const Name = capitalize(toCamelCase(entity));
|
|
354
|
+
const snake = toSnakeCase(entity);
|
|
355
|
+
|
|
356
|
+
if (ext === '.ts') {
|
|
357
|
+
return `/**
|
|
358
|
+
* Create${Name}Service — creates a new ${entity}
|
|
359
|
+
* Responsibility: business logic for ${entity} creation
|
|
360
|
+
*
|
|
361
|
+
* RULES:
|
|
362
|
+
* - No HTTP imports (no Request, Response, status codes)
|
|
363
|
+
* - Receive ${Name}Repository via dependency injection
|
|
364
|
+
* - Return ${Name} domain model — not HTTP DTO
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
export interface ${Name}Repository {
|
|
368
|
+
findByEmail(email: string): Promise<${Name} | null>;
|
|
369
|
+
save(${snake}: ${Name}): Promise<${Name}>;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface ${Name} {
|
|
373
|
+
id: string;
|
|
374
|
+
name: string;
|
|
375
|
+
email: string;
|
|
376
|
+
createdAt: Date;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface Create${Name}Input {
|
|
380
|
+
name: string;
|
|
381
|
+
email: string;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export class Create${Name}Service {
|
|
385
|
+
constructor(private readonly ${snake}Repository: ${Name}Repository) {}
|
|
386
|
+
|
|
387
|
+
async execute(input: Create${Name}Input): Promise<${Name}> {
|
|
388
|
+
// Business rule validation (not HTTP validation)
|
|
389
|
+
if (!input.email.includes('@')) {
|
|
390
|
+
throw new Error('Invalid email format');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Check uniqueness (business rule)
|
|
394
|
+
const existing = await this.${snake}Repository.findByEmail(input.email);
|
|
395
|
+
if (existing) {
|
|
396
|
+
throw new Error('${Name} with this email already exists');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Create and save domain object
|
|
400
|
+
const ${snake}: ${Name} = {
|
|
401
|
+
id: crypto.randomUUID(),
|
|
402
|
+
name: input.name,
|
|
403
|
+
email: input.email,
|
|
404
|
+
createdAt: new Date(),
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return this.${snake}Repository.save(${snake});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (ext === '.rb') {
|
|
414
|
+
return `# Create${Name}Service — creates a new ${snake}
|
|
415
|
+
# Responsibility: business logic for ${snake} creation
|
|
416
|
+
#
|
|
417
|
+
# RULES:
|
|
418
|
+
# - No HTTP concerns (no status codes, request objects)
|
|
419
|
+
# - Receive ${Name}Repository via constructor injection
|
|
420
|
+
class Create${Name}Service
|
|
421
|
+
def initialize(${snake}_repository:)
|
|
422
|
+
@${snake}_repository = ${snake}_repository
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def call(name:, email:)
|
|
426
|
+
raise ArgumentError, "Invalid email" unless email.include?("@")
|
|
427
|
+
raise "${Name}AlreadyExists" if @${snake}_repository.find_by_email(email)
|
|
428
|
+
|
|
429
|
+
@${snake}_repository.save(
|
|
430
|
+
id: SecureRandom.uuid,
|
|
431
|
+
name: name,
|
|
432
|
+
email: email,
|
|
433
|
+
created_at: Time.now
|
|
434
|
+
)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return `# Create${Name} service — see README.md for rules\n# Language: ${ext}\n`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function generateRepositoryExample(entity: string, ext: string): string {
|
|
444
|
+
const Name = capitalize(toCamelCase(entity));
|
|
445
|
+
const snake = toSnakeCase(entity);
|
|
446
|
+
|
|
447
|
+
if (ext === '.ts') {
|
|
448
|
+
return `/**
|
|
449
|
+
* ${Name}Repository — interface and implementations for ${entity} data access
|
|
450
|
+
* Responsibility: abstract over storage; multiple implementations
|
|
451
|
+
*
|
|
452
|
+
* RULES:
|
|
453
|
+
* - Never throw HTTP-specific exceptions (no 404 — return null)
|
|
454
|
+
* - Parameterized queries only (no raw string SQL concatenation)
|
|
455
|
+
* - InMemory implementation for unit tests
|
|
456
|
+
*/
|
|
457
|
+
|
|
458
|
+
export interface ${Name} {
|
|
459
|
+
id: string;
|
|
460
|
+
name: string;
|
|
461
|
+
email: string;
|
|
462
|
+
createdAt: Date;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Repository interface (contract)
|
|
466
|
+
export interface ${Name}Repository {
|
|
467
|
+
findById(id: string): Promise<${Name} | null>;
|
|
468
|
+
findByEmail(email: string): Promise<${Name} | null>;
|
|
469
|
+
save(${snake}: ${Name}): Promise<${Name}>;
|
|
470
|
+
delete(id: string): Promise<void>;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// In-memory implementation (for unit tests)
|
|
474
|
+
export class InMemory${Name}Repository implements ${Name}Repository {
|
|
475
|
+
private readonly store = new Map<string, ${Name}>();
|
|
476
|
+
|
|
477
|
+
async findById(id: string): Promise<${Name} | null> {
|
|
478
|
+
return this.store.get(id) ?? null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async findByEmail(email: string): Promise<${Name} | null> {
|
|
482
|
+
for (const ${snake} of this.store.values()) {
|
|
483
|
+
if (${snake}.email === email) return ${snake};
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async save(${snake}: ${Name}): Promise<${Name}> {
|
|
489
|
+
this.store.set(${snake}.id, ${snake});
|
|
490
|
+
return ${snake};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async delete(id: string): Promise<void> {
|
|
494
|
+
this.store.delete(id);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Database implementation stub
|
|
499
|
+
export class Postgres${Name}Repository implements ${Name}Repository {
|
|
500
|
+
// TODO: inject db client via constructor
|
|
501
|
+
async findById(_id: string): Promise<${Name} | null> {
|
|
502
|
+
throw new Error('Not implemented — add your DB client');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async findByEmail(_email: string): Promise<${Name} | null> {
|
|
506
|
+
throw new Error('Not implemented — add your DB client');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async save(_${snake}: ${Name}): Promise<${Name}> {
|
|
510
|
+
throw new Error('Not implemented — add your DB client');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async delete(_id: string): Promise<void> {
|
|
514
|
+
throw new Error('Not implemented — add your DB client');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (ext === '.rb') {
|
|
521
|
+
return `# ${Name}Repository — interface and implementations for ${snake} data access
|
|
522
|
+
# RULES:
|
|
523
|
+
# - Never raise HTTP-specific errors (no Not Found with status code)
|
|
524
|
+
# - Return nil for missing records, not exceptions
|
|
525
|
+
module ${Name}Repository
|
|
526
|
+
def find_by_id(id)
|
|
527
|
+
raise NotImplementedError
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def find_by_email(email)
|
|
531
|
+
raise NotImplementedError
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def save(${snake})
|
|
535
|
+
raise NotImplementedError
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# In-memory implementation (for unit tests)
|
|
540
|
+
class InMemory${Name}Repository
|
|
541
|
+
include ${Name}Repository
|
|
542
|
+
|
|
543
|
+
def initialize
|
|
544
|
+
@store = {}
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def find_by_id(id)
|
|
548
|
+
@store[id]
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def find_by_email(email)
|
|
552
|
+
@store.values.find { |u| u[:email] == email }
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def save(${snake})
|
|
556
|
+
@store[${snake}[:id]] = ${snake}
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return `# ${Name}Repository — see README.md for rules\n# Language: ${ext}\n`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function generateModelExample(entity: string, ext: string): string {
|
|
566
|
+
const Name = capitalize(toCamelCase(entity));
|
|
567
|
+
|
|
568
|
+
if (ext === '.ts') {
|
|
569
|
+
return `/**
|
|
570
|
+
* ${Name} — domain model
|
|
571
|
+
* Responsibility: represent the ${entity} entity with its business rules
|
|
572
|
+
*
|
|
573
|
+
* RULES:
|
|
574
|
+
* - No HTTP imports (no Request, Response)
|
|
575
|
+
* - No database imports (no ORM, no query builders)
|
|
576
|
+
* - No framework imports
|
|
577
|
+
* - Pure TypeScript data structure + business methods
|
|
578
|
+
*/
|
|
579
|
+
|
|
580
|
+
export interface ${Name} {
|
|
581
|
+
readonly id: string;
|
|
582
|
+
readonly name: string;
|
|
583
|
+
readonly email: string;
|
|
584
|
+
readonly createdAt: Date;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Factory function for creating new instances
|
|
588
|
+
export function create${Name}(params: Omit<${Name}, 'createdAt'>): ${Name} {
|
|
589
|
+
if (!params.email.includes('@')) {
|
|
590
|
+
throw new Error('${Name}: invalid email format');
|
|
591
|
+
}
|
|
592
|
+
if (!params.name.trim()) {
|
|
593
|
+
throw new Error('${Name}: name cannot be empty');
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
...params,
|
|
597
|
+
createdAt: new Date(),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Business rule: computed property
|
|
602
|
+
export function ${toCamelCase(entity)}DisplayName(${toCamelCase(entity)}: ${Name}): string {
|
|
603
|
+
return \`\${${toCamelCase(entity)}.name} <\${${toCamelCase(entity)}.email}>\`;
|
|
604
|
+
}
|
|
605
|
+
`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (ext === '.rb') {
|
|
609
|
+
return `# ${Name} — domain model
|
|
610
|
+
# RULES:
|
|
611
|
+
# - No ActiveRecord concerns — pure Ruby
|
|
612
|
+
# - No HTTP imports
|
|
613
|
+
# - Business rules only (not validation framework)
|
|
614
|
+
${Name} = Struct.new(:id, :name, :email, :created_at, keyword_init: true) do
|
|
615
|
+
def display_name
|
|
616
|
+
"\#{name} <\#{email}>"
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def valid_email?
|
|
620
|
+
email.include?("@")
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return `# ${Name} model — see README.md for rules\n# Language: ${ext}\n`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function generatePresenterExample(entity: string, ext: string): string {
|
|
630
|
+
const Name = capitalize(toCamelCase(entity));
|
|
631
|
+
const snake = toSnakeCase(entity);
|
|
632
|
+
|
|
633
|
+
if (ext === '.ts') {
|
|
634
|
+
return `/**
|
|
635
|
+
* ${Name}Presenter — serializes ${Name} domain model to JSON
|
|
636
|
+
* Responsibility: convert ${entity} to response format (no business logic)
|
|
637
|
+
*
|
|
638
|
+
* RULES:
|
|
639
|
+
* - No business logic — only formatting and field mapping
|
|
640
|
+
* - Called from Handlers only (not Services, not Repositories)
|
|
641
|
+
*/
|
|
642
|
+
|
|
643
|
+
export interface ${Name} {
|
|
644
|
+
id: string;
|
|
645
|
+
name: string;
|
|
646
|
+
email: string;
|
|
647
|
+
createdAt: Date;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export interface ${Name}JSON {
|
|
651
|
+
id: string;
|
|
652
|
+
name: string;
|
|
653
|
+
email: string;
|
|
654
|
+
created_at: string; // ISO 8601
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export class ${Name}Presenter {
|
|
658
|
+
static toJSON(${snake}: ${Name}): ${Name}JSON {
|
|
659
|
+
return {
|
|
660
|
+
id: ${snake}.id,
|
|
661
|
+
name: ${snake}.name,
|
|
662
|
+
email: ${snake}.email,
|
|
663
|
+
created_at: ${snake}.createdAt.toISOString(),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
static toJSONList(${snake}s: ${Name}[]): ${Name}JSON[] {
|
|
668
|
+
return ${snake}s.map((u) => this.toJSON(u));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (ext === '.rb') {
|
|
675
|
+
return `# ${Name}Presenter — serializes ${snake} domain model to JSON
|
|
676
|
+
# RULES:
|
|
677
|
+
# - No business logic — only formatting
|
|
678
|
+
# - Called from controllers/handlers only
|
|
679
|
+
class ${Name}Presenter
|
|
680
|
+
def initialize(${snake})
|
|
681
|
+
@${snake} = ${snake}
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def as_json(*)
|
|
685
|
+
{
|
|
686
|
+
id: @${snake}.id,
|
|
687
|
+
name: @${snake}.name,
|
|
688
|
+
email: @${snake}.email,
|
|
689
|
+
created_at: @${snake}.created_at.iso8601
|
|
690
|
+
}
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return `# ${Name}Presenter — see README.md for rules\n# Language: ${ext}\n`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ── String utilities ─────────────────────────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
function capitalize(str: string): string {
|
|
702
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function toCamelCase(str: string): string {
|
|
706
|
+
return str
|
|
707
|
+
.replace(/[-_\s]+(.)/g, (_, c: string) => c.toUpperCase())
|
|
708
|
+
.replace(/^(.)/, (c: string) => c.toLowerCase());
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function toSnakeCase(str: string): string {
|
|
712
|
+
return str
|
|
713
|
+
.replace(/([A-Z])/g, '_$1')
|
|
714
|
+
.replace(/[-\s]+/g, '_')
|
|
715
|
+
.toLowerCase()
|
|
716
|
+
.replace(/^_/, '');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function languageToExtension(language: Language): string {
|
|
720
|
+
const map: Record<Language, string> = {
|
|
721
|
+
typescript: '.ts',
|
|
722
|
+
javascript: '.js',
|
|
723
|
+
ruby: '.rb',
|
|
724
|
+
rust: '.rs',
|
|
725
|
+
python: '.py',
|
|
726
|
+
go: '.go',
|
|
727
|
+
php: '.php',
|
|
728
|
+
unknown: '.ts',
|
|
729
|
+
};
|
|
730
|
+
return map[language] ?? '.ts';
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function hasNonReadmeFiles(dir: string): boolean {
|
|
734
|
+
try {
|
|
735
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
736
|
+
return entries.some((e) => e.isFile() && e.name !== 'README.md');
|
|
737
|
+
} catch {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
}
|