@sha3/code 1.0.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/AGENTS.md +75 -0
- package/README.md +554 -0
- package/ai/adapters/codex.md +7 -0
- package/ai/adapters/copilot.md +7 -0
- package/ai/adapters/cursor.md +7 -0
- package/ai/adapters/windsurf.md +8 -0
- package/ai/constitution.md +12 -0
- package/bin/code-standards.mjs +47 -0
- package/biome.json +37 -0
- package/index.mjs +11 -0
- package/lib/cli/parse-args.mjs +416 -0
- package/lib/cli/post-run-guidance.mjs +43 -0
- package/lib/cli/run-init.mjs +123 -0
- package/lib/cli/run-profile.mjs +46 -0
- package/lib/cli/run-refactor.mjs +152 -0
- package/lib/cli/run-verify.mjs +67 -0
- package/lib/constants.mjs +167 -0
- package/lib/contract/load-rule-catalog.mjs +12 -0
- package/lib/contract/render-agents.mjs +79 -0
- package/lib/contract/render-contract-json.mjs +7 -0
- package/lib/contract/resolve-contract.mjs +52 -0
- package/lib/paths.mjs +50 -0
- package/lib/profile.mjs +108 -0
- package/lib/project/ai-instructions.mjs +28 -0
- package/lib/project/biome-ignore.mjs +14 -0
- package/lib/project/managed-files.mjs +105 -0
- package/lib/project/package-metadata.mjs +132 -0
- package/lib/project/prompt-files.mjs +111 -0
- package/lib/project/template-resolution.mjs +70 -0
- package/lib/refactor/materialize-refactor-context.mjs +106 -0
- package/lib/refactor/preservation-questions.mjs +33 -0
- package/lib/refactor/public-contract-extractor.mjs +22 -0
- package/lib/refactor/render-analysis-summary.mjs +50 -0
- package/lib/refactor/source-analysis.mjs +74 -0
- package/lib/utils/fs.mjs +220 -0
- package/lib/utils/prompts.mjs +63 -0
- package/lib/utils/text.mjs +43 -0
- package/lib/verify/change-audit-verifier.mjs +140 -0
- package/lib/verify/change-context.mjs +36 -0
- package/lib/verify/error-handling-verifier.mjs +164 -0
- package/lib/verify/explain-rule.mjs +54 -0
- package/lib/verify/issue-helpers.mjs +132 -0
- package/lib/verify/project-layout-verifier.mjs +259 -0
- package/lib/verify/project-verifier.mjs +267 -0
- package/lib/verify/readme-public-api.mjs +237 -0
- package/lib/verify/readme-verifier.mjs +216 -0
- package/lib/verify/render-json-report.mjs +3 -0
- package/lib/verify/render-text-report.mjs +34 -0
- package/lib/verify/source-analysis.mjs +126 -0
- package/lib/verify/source-rule-verifier.mjs +453 -0
- package/lib/verify/testing-verifier.mjs +113 -0
- package/lib/verify/tooling-verifier.mjs +82 -0
- package/lib/verify/typescript-style-verifier.mjs +407 -0
- package/package.json +55 -0
- package/profiles/default.profile.json +40 -0
- package/profiles/schema.json +96 -0
- package/prompts/init-contract.md +25 -0
- package/prompts/init-phase-2-implement.md +25 -0
- package/prompts/init-phase-3-verify.md +23 -0
- package/prompts/init.prompt.md +24 -0
- package/prompts/refactor-contract.md +26 -0
- package/prompts/refactor-phase-2-rebuild.md +25 -0
- package/prompts/refactor-phase-3-verify.md +24 -0
- package/prompts/refactor.prompt.md +26 -0
- package/resources/ai/AGENTS.md +18 -0
- package/resources/ai/adapters/codex.md +5 -0
- package/resources/ai/adapters/copilot.md +5 -0
- package/resources/ai/adapters/cursor.md +5 -0
- package/resources/ai/adapters/windsurf.md +5 -0
- package/resources/ai/contract.schema.json +68 -0
- package/resources/ai/rule-catalog.json +878 -0
- package/resources/ai/rule-catalog.schema.json +66 -0
- package/resources/ai/templates/adapters/codex.template.md +7 -0
- package/resources/ai/templates/adapters/copilot.template.md +7 -0
- package/resources/ai/templates/adapters/cursor.template.md +7 -0
- package/resources/ai/templates/adapters/windsurf.template.md +7 -0
- package/resources/ai/templates/agents.project.template.md +141 -0
- package/resources/ai/templates/examples/demo/src/billing/billing.service.ts +73 -0
- package/resources/ai/templates/examples/demo/src/config.ts +3 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.errors.ts +51 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.service.ts +96 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.types.ts +9 -0
- package/resources/ai/templates/examples/rules/async-bad.ts +52 -0
- package/resources/ai/templates/examples/rules/async-good.ts +56 -0
- package/resources/ai/templates/examples/rules/class-first-bad.ts +36 -0
- package/resources/ai/templates/examples/rules/class-first-good.ts +74 -0
- package/resources/ai/templates/examples/rules/constructor-bad.ts +68 -0
- package/resources/ai/templates/examples/rules/constructor-good.ts +71 -0
- package/resources/ai/templates/examples/rules/control-flow-bad.ts +31 -0
- package/resources/ai/templates/examples/rules/control-flow-good.ts +54 -0
- package/resources/ai/templates/examples/rules/errors-bad.ts +42 -0
- package/resources/ai/templates/examples/rules/errors-good.ts +23 -0
- package/resources/ai/templates/examples/rules/functions-bad.ts +48 -0
- package/resources/ai/templates/examples/rules/functions-good.ts +58 -0
- package/resources/ai/templates/examples/rules/returns-bad.ts +38 -0
- package/resources/ai/templates/examples/rules/returns-good.ts +44 -0
- package/resources/ai/templates/examples/rules/testing-bad.ts +34 -0
- package/resources/ai/templates/examples/rules/testing-good.ts +54 -0
- package/resources/ai/templates/rules/architecture.md +41 -0
- package/resources/ai/templates/rules/async.md +13 -0
- package/resources/ai/templates/rules/class-first.md +45 -0
- package/resources/ai/templates/rules/control-flow.md +13 -0
- package/resources/ai/templates/rules/errors.md +18 -0
- package/resources/ai/templates/rules/functions.md +29 -0
- package/resources/ai/templates/rules/naming.md +13 -0
- package/resources/ai/templates/rules/readme.md +36 -0
- package/resources/ai/templates/rules/returns.md +13 -0
- package/resources/ai/templates/rules/testing.md +18 -0
- package/resources/ai/templates/rules.project.template.md +66 -0
- package/resources/ai/templates/skills/change-synchronization/SKILL.md +42 -0
- package/resources/ai/templates/skills/feature-shaping/SKILL.md +45 -0
- package/resources/ai/templates/skills/http-api-conventions/SKILL.md +171 -0
- package/resources/ai/templates/skills/init-workflow/SKILL.md +52 -0
- package/resources/ai/templates/skills/readme-authoring/SKILL.md +51 -0
- package/resources/ai/templates/skills/refactor-workflow/SKILL.md +50 -0
- package/resources/ai/templates/skills/simplicity-audit/SKILL.md +41 -0
- package/resources/ai/templates/skills/test-scope-selection/SKILL.md +50 -0
- package/resources/ai/templates/skills.index.template.md +25 -0
- package/standards/architecture.md +72 -0
- package/standards/changelog-policy.md +12 -0
- package/standards/manifest.json +36 -0
- package/standards/readme.md +56 -0
- package/standards/schema.json +124 -0
- package/standards/style.md +106 -0
- package/standards/testing.md +20 -0
- package/standards/tooling.md +38 -0
- package/templates/node-lib/.biomeignore +10 -0
- package/templates/node-lib/.vscode/extensions.json +1 -0
- package/templates/node-lib/.vscode/settings.json +9 -0
- package/templates/node-lib/README.md +172 -0
- package/templates/node-lib/biome.json +37 -0
- package/templates/node-lib/gitignore +6 -0
- package/templates/node-lib/package.json +32 -0
- package/templates/node-lib/scripts/release-publish.mjs +106 -0
- package/templates/node-lib/scripts/run-tests.mjs +65 -0
- package/templates/node-lib/src/config.ts +3 -0
- package/templates/node-lib/src/index.ts +2 -0
- package/templates/node-lib/src/logger.ts +7 -0
- package/templates/node-lib/src/package-info/package-info.service.ts +47 -0
- package/templates/node-lib/test/package-info.test.ts +10 -0
- package/templates/node-lib/tsconfig.build.json +1 -0
- package/templates/node-lib/tsconfig.json +5 -0
- package/templates/node-service/.biomeignore +10 -0
- package/templates/node-service/.vscode/extensions.json +1 -0
- package/templates/node-service/.vscode/settings.json +9 -0
- package/templates/node-service/README.md +244 -0
- package/templates/node-service/biome.json +37 -0
- package/templates/node-service/ecosystem.config.cjs +3 -0
- package/templates/node-service/gitignore +6 -0
- package/templates/node-service/package.json +42 -0
- package/templates/node-service/scripts/release-publish.mjs +106 -0
- package/templates/node-service/scripts/run-tests.mjs +65 -0
- package/templates/node-service/src/app/service-runtime.service.ts +57 -0
- package/templates/node-service/src/app-info/app-info.service.ts +47 -0
- package/templates/node-service/src/config.ts +11 -0
- package/templates/node-service/src/http/http-server.service.ts +66 -0
- package/templates/node-service/src/index.ts +2 -0
- package/templates/node-service/src/logger.ts +7 -0
- package/templates/node-service/src/main.ts +5 -0
- package/templates/node-service/test/service-runtime.test.ts +13 -0
- package/templates/node-service/tsconfig.build.json +1 -0
- package/templates/node-service/tsconfig.json +5 -0
- package/tsconfig/base.json +16 -0
- package/tsconfig/node-lib.json +5 -0
- package/tsconfig/node-service.json +1 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://sha3.dev/code/rule-catalog.schema.json",
|
|
4
|
+
"title": "AI Rule Catalog",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["version", "rules"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": { "type": "string", "const": "v2" },
|
|
10
|
+
"rules": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"minItems": 8,
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": [
|
|
17
|
+
"id",
|
|
18
|
+
"title",
|
|
19
|
+
"summary",
|
|
20
|
+
"severity",
|
|
21
|
+
"kind",
|
|
22
|
+
"appliesTo",
|
|
23
|
+
"deterministic",
|
|
24
|
+
"verificationMode",
|
|
25
|
+
"verificationSource",
|
|
26
|
+
"implementedBy",
|
|
27
|
+
"requiresContext",
|
|
28
|
+
"confidence",
|
|
29
|
+
"enforcedBy",
|
|
30
|
+
"examples",
|
|
31
|
+
"profileOverrides"
|
|
32
|
+
],
|
|
33
|
+
"properties": {
|
|
34
|
+
"id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
|
35
|
+
"title": { "type": "string", "minLength": 3 },
|
|
36
|
+
"summary": { "type": "string", "minLength": 10 },
|
|
37
|
+
"severity": { "type": "string", "enum": ["error", "warning", "audit"] },
|
|
38
|
+
"kind": { "type": "string", "enum": ["style", "architecture", "testing", "documentation", "workflow"] },
|
|
39
|
+
"appliesTo": { "type": "array", "minItems": 1, "items": { "type": "string" } },
|
|
40
|
+
"deterministic": { "type": "boolean" },
|
|
41
|
+
"verificationMode": { "type": "string", "enum": ["deterministic", "heuristic", "audit"] },
|
|
42
|
+
"verificationSource": { "type": "string", "enum": ["ast", "text", "filesystem", "package-json", "readme", "git-diff", "combined"] },
|
|
43
|
+
"implementedBy": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
|
|
44
|
+
"requiresContext": { "type": "string", "enum": ["none", "changed-files", "full-project"] },
|
|
45
|
+
"confidence": { "type": "string", "enum": ["high", "medium"] },
|
|
46
|
+
"enforcedBy": { "type": "array", "minItems": 1, "items": { "type": "string" } },
|
|
47
|
+
"examples": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"required": ["good", "bad"],
|
|
51
|
+
"properties": { "good": { "type": "array", "items": { "type": "string" } }, "bad": { "type": "array", "items": { "type": "string" } } }
|
|
52
|
+
},
|
|
53
|
+
"profileOverrides": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"required": ["key"],
|
|
59
|
+
"properties": { "key": { "type": "string" }, "equals": {}, "oneOf": { "type": "array", "items": {} } }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Codex Adapter
|
|
2
|
+
|
|
3
|
+
- Read `AGENTS.md` and `ai/contract.json` before implementation.
|
|
4
|
+
- Fix `error` rules, review `warning` rules with judgment, and report `audit` items.
|
|
5
|
+
- Keep `@sha3/code` managed files read-only unless the user explicitly requests a standards update.
|
|
6
|
+
- Rewrite `README.md` as real package documentation and document public exports and public methods after implementation.
|
|
7
|
+
- Run `npm run check` before finalizing and fix any failing rule or type error.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# GitHub Copilot Adapter
|
|
2
|
+
|
|
3
|
+
- Read `AGENTS.md` and `ai/contract.json` first.
|
|
4
|
+
- Fix `error` rules first and treat `warning` rules as review signals, not blind rewrite orders.
|
|
5
|
+
- Do not modify managed files unless the user explicitly requests a contract/tooling update.
|
|
6
|
+
- Rewrite `README.md` as real package documentation and document public exports and public methods after implementation.
|
|
7
|
+
- Run `npm run check` and resolve all failures before finalizing.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Cursor Adapter
|
|
2
|
+
|
|
3
|
+
- Read `AGENTS.md` and `ai/contract.json` before editing.
|
|
4
|
+
- Apply `error` rules first, then review `warning` rules without overcorrecting them into worse code.
|
|
5
|
+
- Keep managed files read-only unless the user explicitly asks for a standards update.
|
|
6
|
+
- Rewrite `README.md` as real package documentation and document public exports and public methods after implementation.
|
|
7
|
+
- Run `npm run check` and fix all reported issues before finishing.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Windsurf Adapter
|
|
2
|
+
|
|
3
|
+
- Treat `AGENTS.md` and `ai/contract.json` as the mandatory local contract.
|
|
4
|
+
- Managed files remain read-only unless the user explicitly requests a standards update.
|
|
5
|
+
- Rewrite `README.md` as real package documentation and document public exports and public methods after implementation.
|
|
6
|
+
- Prefer deterministic fixes that satisfy `npm run check` over ad hoc style guesses.
|
|
7
|
+
- Resolve all failing checks before finalizing.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# AI Coding Contract
|
|
2
|
+
|
|
3
|
+
Project: {{projectName}}
|
|
4
|
+
Contract format: `{{contractVersion}}`
|
|
5
|
+
Generated by `@sha3/code`: `{{generatedByVersion}}`
|
|
6
|
+
|
|
7
|
+
`ai/contract.json` is the machine-readable source of truth for active rules. This file is the always-on human-readable entrypoint. `SKILLS.md` lists specialized workflows that should be loaded only when the task requires them.
|
|
8
|
+
|
|
9
|
+
## Active Profile
|
|
10
|
+
|
|
11
|
+
{{profileSummary}}
|
|
12
|
+
|
|
13
|
+
## Rule Hierarchy
|
|
14
|
+
|
|
15
|
+
1. `ai/contract.json`
|
|
16
|
+
2. `AGENTS.md`
|
|
17
|
+
3. `SKILLS.md`
|
|
18
|
+
4. `skills/*`
|
|
19
|
+
5. `ai/*.md`
|
|
20
|
+
6. Local project configs (`package.json`, `biome.json`, `tsconfig.json`)
|
|
21
|
+
7. Existing source code and tests
|
|
22
|
+
|
|
23
|
+
If rules conflict, the higher item wins. Existing code has no grandfathered exceptions.
|
|
24
|
+
|
|
25
|
+
## Deterministic Rules
|
|
26
|
+
|
|
27
|
+
{{deterministicRules}}
|
|
28
|
+
|
|
29
|
+
## Heuristic Rules
|
|
30
|
+
|
|
31
|
+
{{heuristicRules}}
|
|
32
|
+
|
|
33
|
+
## Audit Rules
|
|
34
|
+
|
|
35
|
+
{{auditRules}}
|
|
36
|
+
|
|
37
|
+
## Deterministic Checks Run By Tooling
|
|
38
|
+
|
|
39
|
+
- `npm run standards:check` validates project structure, README sections, managed files, metadata sync, and AI contract presence.
|
|
40
|
+
- `npm run lint` uses Biome for baseline linting and formatting rules.
|
|
41
|
+
- `npm run standards:check` also enforces AST-level project rules such as single return outside `src/http/`, async/await-only, section ordering, identifier quality, and naming rules.
|
|
42
|
+
- `npm run check` is the final blocking gate.
|
|
43
|
+
- Let Biome decide the final line wrapping. Do not force single-line layouts that the formatter keeps multiline.
|
|
44
|
+
- `verify` MUST NOT enforce a visual layout that Biome can rewrite.
|
|
45
|
+
- Fix every `error`.
|
|
46
|
+
- Review every `warning` with judgment; do not rewrite code blindly if the result is less simple or less readable.
|
|
47
|
+
- Report `audit` items explicitly, but they do not fail the default verification flow.
|
|
48
|
+
|
|
49
|
+
## Section Ordering
|
|
50
|
+
|
|
51
|
+
For class-first files, the `comment_section_blocks` list in `ai/contract.json` is the required order, not a suggested order.
|
|
52
|
+
|
|
53
|
+
Required order:
|
|
54
|
+
|
|
55
|
+
1. `imports:externals`
|
|
56
|
+
2. `imports:internals`
|
|
57
|
+
3. `consts`
|
|
58
|
+
4. `types`
|
|
59
|
+
5. `class`
|
|
60
|
+
6. `private:attributes`
|
|
61
|
+
7. `protected:attributes`
|
|
62
|
+
8. `public:properties`
|
|
63
|
+
9. `constructor`
|
|
64
|
+
10. `static:properties`
|
|
65
|
+
11. `factory`
|
|
66
|
+
12. `private:methods`
|
|
67
|
+
13. `protected:methods`
|
|
68
|
+
14. `public:methods`
|
|
69
|
+
15. `static:methods`
|
|
70
|
+
|
|
71
|
+
Minimal example:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
/**
|
|
75
|
+
* @section types
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
type UserServiceOptions = { isEnabled: boolean };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @section class
|
|
82
|
+
*/
|
|
83
|
+
export class UserService {
|
|
84
|
+
/**
|
|
85
|
+
* @section constructor
|
|
86
|
+
*/
|
|
87
|
+
public constructor(private readonly options: UserServiceOptions) {}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @section public:methods
|
|
91
|
+
*/
|
|
92
|
+
public readStatus(): boolean {
|
|
93
|
+
return this.options.isEnabled;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`@section class` must sit directly above `export class`. Do not place JSDoc, block comments, or line comments between them.
|
|
99
|
+
|
|
100
|
+
## README Rule
|
|
101
|
+
|
|
102
|
+
- Rewrite `README.md` as package-quality integration documentation once real behavior exists.
|
|
103
|
+
- Document every public export from `src/index.ts`.
|
|
104
|
+
- If a public export is a class, document each public method with purpose, return value, and behavior notes.
|
|
105
|
+
- Use a structure inspired by high-quality package READMEs such as `ky`: short value proposition, practical examples first, exhaustive API reference after.
|
|
106
|
+
- Do not leave scaffold-placeholder API descriptions once implementation is real.
|
|
107
|
+
|
|
108
|
+
## Simplicity Rule
|
|
109
|
+
|
|
110
|
+
Simplicity is mandatory, not stylistic preference.
|
|
111
|
+
|
|
112
|
+
- Use the simplest correct design for the current requirement.
|
|
113
|
+
- Do not add speculative abstractions, wrappers, helper layers, extension points, or extra files without immediate justification.
|
|
114
|
+
- If a solution can be made smaller and more direct without losing correctness, that simpler version MUST be preferred.
|
|
115
|
+
- Simplicity does not justify removing valid boundaries.
|
|
116
|
+
- Keep distinct current responsibilities separated when they serve a real purpose.
|
|
117
|
+
- Keep code compact, but do not fight Biome over final wrapping decisions.
|
|
118
|
+
- Templates are the canonical simplicity baseline. If a rule seems to require a more complex template, the rule is suspect.
|
|
119
|
+
- Do not keep helper functions at module scope inside class-oriented feature files; move that logic into the class as private or static methods.
|
|
120
|
+
- If a class starts accumulating unrelated responsibilities or growing into a very large file, you MUST split it into smaller cohesive units with explicit roles.
|
|
121
|
+
|
|
122
|
+
## Managed Files
|
|
123
|
+
|
|
124
|
+
{{managedFiles}}
|
|
125
|
+
|
|
126
|
+
Do not edit managed files during normal feature work unless the user explicitly requests a standards update. Use `npx @sha3/code refactor --yes` when the package contract changes.
|
|
127
|
+
Biome ignores managed files by default so contract docs and prompts do not create lint or format noise during normal implementation work.
|
|
128
|
+
|
|
129
|
+
## Bootstrap Prompt
|
|
130
|
+
|
|
131
|
+
Before writing code:
|
|
132
|
+
|
|
133
|
+
1. Read `AGENTS.md`, `SKILLS.md`, `ai/contract.json`, and `ai/<assistant>.md`.
|
|
134
|
+
2. For init-based work, open `skills/init-workflow/SKILL.md`, `skills/feature-shaping/SKILL.md`, `skills/simplicity-audit/SKILL.md`, and `skills/change-synchronization/SKILL.md`.
|
|
135
|
+
3. For refactor work, open `skills/refactor-workflow/SKILL.md`, `skills/feature-shaping/SKILL.md`, `skills/simplicity-audit/SKILL.md`, and `skills/change-synchronization/SKILL.md`.
|
|
136
|
+
4. Open `skills/test-scope-selection/SKILL.md` for meaningful behavior changes, `skills/readme-authoring/SKILL.md` whenever `README.md` changes, and `skills/http-api-conventions/SKILL.md` whenever HTTP endpoints exist or change.
|
|
137
|
+
5. Summarize the `error` rules you will follow and the `warning` rules you will review carefully.
|
|
138
|
+
6. Run `npm run standards:check` before and after implementation when feasible, then fix every `error`, review every `warning`, and report every `audit` item it reports.
|
|
139
|
+
7. Implement the task without editing managed files unless the user explicitly asked for a standards update.
|
|
140
|
+
8. Run `npm run check` and fix all issues before finishing.
|
|
141
|
+
9. Prefer guard clauses and early returns inside `src/http/` transport adapters when they make request validation or response handling clearer; keep stricter single-return style in domain code.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @section imports:internals
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import config from "../config.ts";
|
|
6
|
+
import type { InvoiceService } from "../invoice/invoice.service.ts";
|
|
7
|
+
import type { InvoiceSummary } from "../invoice/invoice.types.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @section types
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type BillingSnapshot = { customerId: string; invoiceCount: number; totalAmount: number; formattedTotal: string; statusServiceUrl: string };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @section class
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export class BillingService {
|
|
20
|
+
/**
|
|
21
|
+
* @section private:attributes
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
private readonly invoiceService: InvoiceService;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @section constructor
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
public constructor(invoiceService: InvoiceService) {
|
|
31
|
+
this.invoiceService = invoiceService;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @section factory
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
public static create(invoiceService: InvoiceService): BillingService {
|
|
39
|
+
const service = new BillingService(invoiceService);
|
|
40
|
+
return service;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @section private:methods
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
private formatCurrency(amount: number): string {
|
|
48
|
+
const formattedAmount = `${config.BILLING_CURRENCY_SYMBOL}${amount.toFixed(2)}`;
|
|
49
|
+
return formattedAmount;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @section public:methods
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
public async snapshot(customerId: string): Promise<BillingSnapshot> {
|
|
57
|
+
const summary: InvoiceSummary = await this.invoiceService.summarizeForCustomer(customerId);
|
|
58
|
+
const snapshot: BillingSnapshot = {
|
|
59
|
+
customerId,
|
|
60
|
+
invoiceCount: summary.count,
|
|
61
|
+
totalAmount: summary.totalAmount,
|
|
62
|
+
formattedTotal: this.formatCurrency(summary.totalAmount),
|
|
63
|
+
statusServiceUrl: config.STATUS_SERVICE_URL,
|
|
64
|
+
};
|
|
65
|
+
return snapshot;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @section static:methods
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
// empty
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @section types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// empty
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @section class
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class InvalidInvoiceCommandError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* @section private:attributes
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
private readonly reason: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @section constructor
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
public constructor(reason: string) {
|
|
23
|
+
super(`Invalid invoice command: ${reason}`);
|
|
24
|
+
this.name = "InvalidInvoiceCommandError";
|
|
25
|
+
this.reason = reason;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @section factory
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
public static forReason(reason: string): InvalidInvoiceCommandError {
|
|
33
|
+
const error = new InvalidInvoiceCommandError(reason);
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @section public:methods
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
public getReason(): string {
|
|
42
|
+
const value = this.reason;
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @section static:methods
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// empty
|
|
51
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @section imports:externals
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @section imports:internals
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import config from "../config.ts";
|
|
12
|
+
import { InvalidInvoiceCommandError } from "./invoice.errors.ts";
|
|
13
|
+
import type { CreateInvoiceCommand, Invoice, InvoiceSummary } from "./invoice.types.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @section types
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// empty
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @section class
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export class InvoiceService {
|
|
26
|
+
/**
|
|
27
|
+
* @section private:attributes
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
private readonly invoicesById: Map<string, Invoice>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @section constructor
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
public constructor() {
|
|
37
|
+
this.invoicesById = new Map<string, Invoice>();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @section factory
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
public static create(): InvoiceService {
|
|
45
|
+
const service = new InvoiceService();
|
|
46
|
+
return service;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @section private:methods
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
private validate(command: CreateInvoiceCommand): void {
|
|
54
|
+
if (!command.customerId.trim()) {
|
|
55
|
+
throw InvalidInvoiceCommandError.forReason("customerId is required");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (command.amount <= config.MINIMUM_INVOICE_AMOUNT) {
|
|
59
|
+
throw InvalidInvoiceCommandError.forReason("amount must be greater than zero");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private toInvoice(command: CreateInvoiceCommand): Invoice {
|
|
64
|
+
const invoice: Invoice = { id: randomUUID(), customerId: command.customerId, amount: command.amount, issuedAt: new Date() };
|
|
65
|
+
return invoice;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @section public:methods
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
public async create(command: CreateInvoiceCommand): Promise<Invoice> {
|
|
73
|
+
this.validate(command);
|
|
74
|
+
const createdInvoice: Invoice = this.toInvoice(command);
|
|
75
|
+
this.invoicesById.set(createdInvoice.id, createdInvoice);
|
|
76
|
+
return createdInvoice;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async summarizeForCustomer(customerId: string): Promise<InvoiceSummary> {
|
|
80
|
+
const allInvoices: Invoice[] = Array.from(this.invoicesById.values());
|
|
81
|
+
const invoices: Invoice[] = allInvoices.filter((invoice) => {
|
|
82
|
+
return invoice.customerId === customerId;
|
|
83
|
+
});
|
|
84
|
+
const totalAmount = invoices.reduce((sum, invoice) => {
|
|
85
|
+
return sum + invoice.amount;
|
|
86
|
+
}, 0);
|
|
87
|
+
const summary: InvoiceSummary = { count: invoices.length, totalAmount };
|
|
88
|
+
return summary;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @section static:methods
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
// empty
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type InvoiceId = string;
|
|
2
|
+
|
|
3
|
+
export type CustomerId = string;
|
|
4
|
+
|
|
5
|
+
export type Invoice = { id: InvoiceId; customerId: CustomerId; amount: number; issuedAt: Date };
|
|
6
|
+
|
|
7
|
+
export type CreateInvoiceCommand = { customerId: CustomerId; amount: number };
|
|
8
|
+
|
|
9
|
+
export type InvoiceSummary = { count: number; totalAmount: number };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @section types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
type SyncInvoicesCommand = { accountId: string };
|
|
6
|
+
type Invoice = { id: string; accountId: string };
|
|
7
|
+
type SyncResult = { saved: number };
|
|
8
|
+
type InvoiceSource = { fetch(accountId: string): Promise<Invoice[]> };
|
|
9
|
+
type InvoiceWriter = { persist(invoices: Invoice[]): Promise<SyncResult> };
|
|
10
|
+
|
|
11
|
+
export class InvoiceSyncService {
|
|
12
|
+
/**
|
|
13
|
+
* @section private:attributes
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
private readonly source: InvoiceSource;
|
|
17
|
+
private readonly writer: InvoiceWriter;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @section constructor
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
public constructor(source: InvoiceSource, writer: InvoiceWriter) {
|
|
24
|
+
this.source = source;
|
|
25
|
+
this.writer = writer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @section factory
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
public static create(source: InvoiceSource, writer: InvoiceWriter): InvoiceSyncService {
|
|
33
|
+
const service = new InvoiceSyncService(source, writer);
|
|
34
|
+
return service;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @section public:methods
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
public execute(command: SyncInvoicesCommand): Promise<SyncResult> {
|
|
42
|
+
return this.source.fetch(command.accountId).then((invoices: Invoice[]) => {
|
|
43
|
+
return this.writer.persist(invoices);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @section static:methods
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// empty
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @section types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
type SyncInvoicesCommand = { accountId: string };
|
|
6
|
+
type Invoice = { id: string; accountId: string };
|
|
7
|
+
type SyncResult = { saved: number };
|
|
8
|
+
type InvoiceSource = { fetch(accountId: string): Promise<Invoice[]> };
|
|
9
|
+
type InvoiceWriter = { persist(invoices: Invoice[]): Promise<SyncResult> };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @section class
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class InvoiceSyncService {
|
|
16
|
+
/**
|
|
17
|
+
* @section private:attributes
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
private readonly source: InvoiceSource;
|
|
21
|
+
private readonly writer: InvoiceWriter;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @section constructor
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
public constructor(source: InvoiceSource, writer: InvoiceWriter) {
|
|
28
|
+
this.source = source;
|
|
29
|
+
this.writer = writer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @section factory
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
public static create(source: InvoiceSource, writer: InvoiceWriter): InvoiceSyncService {
|
|
37
|
+
const service = new InvoiceSyncService(source, writer);
|
|
38
|
+
return service;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @section public:methods
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
public async execute(command: SyncInvoicesCommand): Promise<SyncResult> {
|
|
46
|
+
const invoices: Invoice[] = await this.source.fetch(command.accountId);
|
|
47
|
+
const result: SyncResult = await this.writer.persist(invoices);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @section static:methods
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
// empty
|
|
56
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @section types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
type CreateInvoiceCommand = { customerId: string; amount: number };
|
|
6
|
+
type Invoice = { id: string; customerId: string; amount: number };
|
|
7
|
+
|
|
8
|
+
export class InvoiceService {
|
|
9
|
+
/**
|
|
10
|
+
* @section factory
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
public static create(): InvoiceService {
|
|
14
|
+
const service = new InvoiceService();
|
|
15
|
+
return service;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @section public:methods
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
public async create(command: CreateInvoiceCommand): Promise<Invoice> {
|
|
23
|
+
if (!command.customerId) {
|
|
24
|
+
return Promise.reject(new Error("invalid command"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const invoice: Invoice = { id: "1", customerId: command.customerId, amount: command.amount };
|
|
28
|
+
return invoice;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @section static:methods
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// empty
|
|
36
|
+
}
|