@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.
Files changed (165) hide show
  1. package/AGENTS.md +75 -0
  2. package/README.md +554 -0
  3. package/ai/adapters/codex.md +7 -0
  4. package/ai/adapters/copilot.md +7 -0
  5. package/ai/adapters/cursor.md +7 -0
  6. package/ai/adapters/windsurf.md +8 -0
  7. package/ai/constitution.md +12 -0
  8. package/bin/code-standards.mjs +47 -0
  9. package/biome.json +37 -0
  10. package/index.mjs +11 -0
  11. package/lib/cli/parse-args.mjs +416 -0
  12. package/lib/cli/post-run-guidance.mjs +43 -0
  13. package/lib/cli/run-init.mjs +123 -0
  14. package/lib/cli/run-profile.mjs +46 -0
  15. package/lib/cli/run-refactor.mjs +152 -0
  16. package/lib/cli/run-verify.mjs +67 -0
  17. package/lib/constants.mjs +167 -0
  18. package/lib/contract/load-rule-catalog.mjs +12 -0
  19. package/lib/contract/render-agents.mjs +79 -0
  20. package/lib/contract/render-contract-json.mjs +7 -0
  21. package/lib/contract/resolve-contract.mjs +52 -0
  22. package/lib/paths.mjs +50 -0
  23. package/lib/profile.mjs +108 -0
  24. package/lib/project/ai-instructions.mjs +28 -0
  25. package/lib/project/biome-ignore.mjs +14 -0
  26. package/lib/project/managed-files.mjs +105 -0
  27. package/lib/project/package-metadata.mjs +132 -0
  28. package/lib/project/prompt-files.mjs +111 -0
  29. package/lib/project/template-resolution.mjs +70 -0
  30. package/lib/refactor/materialize-refactor-context.mjs +106 -0
  31. package/lib/refactor/preservation-questions.mjs +33 -0
  32. package/lib/refactor/public-contract-extractor.mjs +22 -0
  33. package/lib/refactor/render-analysis-summary.mjs +50 -0
  34. package/lib/refactor/source-analysis.mjs +74 -0
  35. package/lib/utils/fs.mjs +220 -0
  36. package/lib/utils/prompts.mjs +63 -0
  37. package/lib/utils/text.mjs +43 -0
  38. package/lib/verify/change-audit-verifier.mjs +140 -0
  39. package/lib/verify/change-context.mjs +36 -0
  40. package/lib/verify/error-handling-verifier.mjs +164 -0
  41. package/lib/verify/explain-rule.mjs +54 -0
  42. package/lib/verify/issue-helpers.mjs +132 -0
  43. package/lib/verify/project-layout-verifier.mjs +259 -0
  44. package/lib/verify/project-verifier.mjs +267 -0
  45. package/lib/verify/readme-public-api.mjs +237 -0
  46. package/lib/verify/readme-verifier.mjs +216 -0
  47. package/lib/verify/render-json-report.mjs +3 -0
  48. package/lib/verify/render-text-report.mjs +34 -0
  49. package/lib/verify/source-analysis.mjs +126 -0
  50. package/lib/verify/source-rule-verifier.mjs +453 -0
  51. package/lib/verify/testing-verifier.mjs +113 -0
  52. package/lib/verify/tooling-verifier.mjs +82 -0
  53. package/lib/verify/typescript-style-verifier.mjs +407 -0
  54. package/package.json +55 -0
  55. package/profiles/default.profile.json +40 -0
  56. package/profiles/schema.json +96 -0
  57. package/prompts/init-contract.md +25 -0
  58. package/prompts/init-phase-2-implement.md +25 -0
  59. package/prompts/init-phase-3-verify.md +23 -0
  60. package/prompts/init.prompt.md +24 -0
  61. package/prompts/refactor-contract.md +26 -0
  62. package/prompts/refactor-phase-2-rebuild.md +25 -0
  63. package/prompts/refactor-phase-3-verify.md +24 -0
  64. package/prompts/refactor.prompt.md +26 -0
  65. package/resources/ai/AGENTS.md +18 -0
  66. package/resources/ai/adapters/codex.md +5 -0
  67. package/resources/ai/adapters/copilot.md +5 -0
  68. package/resources/ai/adapters/cursor.md +5 -0
  69. package/resources/ai/adapters/windsurf.md +5 -0
  70. package/resources/ai/contract.schema.json +68 -0
  71. package/resources/ai/rule-catalog.json +878 -0
  72. package/resources/ai/rule-catalog.schema.json +66 -0
  73. package/resources/ai/templates/adapters/codex.template.md +7 -0
  74. package/resources/ai/templates/adapters/copilot.template.md +7 -0
  75. package/resources/ai/templates/adapters/cursor.template.md +7 -0
  76. package/resources/ai/templates/adapters/windsurf.template.md +7 -0
  77. package/resources/ai/templates/agents.project.template.md +141 -0
  78. package/resources/ai/templates/examples/demo/src/billing/billing.service.ts +73 -0
  79. package/resources/ai/templates/examples/demo/src/config.ts +3 -0
  80. package/resources/ai/templates/examples/demo/src/invoice/invoice.errors.ts +51 -0
  81. package/resources/ai/templates/examples/demo/src/invoice/invoice.service.ts +96 -0
  82. package/resources/ai/templates/examples/demo/src/invoice/invoice.types.ts +9 -0
  83. package/resources/ai/templates/examples/rules/async-bad.ts +52 -0
  84. package/resources/ai/templates/examples/rules/async-good.ts +56 -0
  85. package/resources/ai/templates/examples/rules/class-first-bad.ts +36 -0
  86. package/resources/ai/templates/examples/rules/class-first-good.ts +74 -0
  87. package/resources/ai/templates/examples/rules/constructor-bad.ts +68 -0
  88. package/resources/ai/templates/examples/rules/constructor-good.ts +71 -0
  89. package/resources/ai/templates/examples/rules/control-flow-bad.ts +31 -0
  90. package/resources/ai/templates/examples/rules/control-flow-good.ts +54 -0
  91. package/resources/ai/templates/examples/rules/errors-bad.ts +42 -0
  92. package/resources/ai/templates/examples/rules/errors-good.ts +23 -0
  93. package/resources/ai/templates/examples/rules/functions-bad.ts +48 -0
  94. package/resources/ai/templates/examples/rules/functions-good.ts +58 -0
  95. package/resources/ai/templates/examples/rules/returns-bad.ts +38 -0
  96. package/resources/ai/templates/examples/rules/returns-good.ts +44 -0
  97. package/resources/ai/templates/examples/rules/testing-bad.ts +34 -0
  98. package/resources/ai/templates/examples/rules/testing-good.ts +54 -0
  99. package/resources/ai/templates/rules/architecture.md +41 -0
  100. package/resources/ai/templates/rules/async.md +13 -0
  101. package/resources/ai/templates/rules/class-first.md +45 -0
  102. package/resources/ai/templates/rules/control-flow.md +13 -0
  103. package/resources/ai/templates/rules/errors.md +18 -0
  104. package/resources/ai/templates/rules/functions.md +29 -0
  105. package/resources/ai/templates/rules/naming.md +13 -0
  106. package/resources/ai/templates/rules/readme.md +36 -0
  107. package/resources/ai/templates/rules/returns.md +13 -0
  108. package/resources/ai/templates/rules/testing.md +18 -0
  109. package/resources/ai/templates/rules.project.template.md +66 -0
  110. package/resources/ai/templates/skills/change-synchronization/SKILL.md +42 -0
  111. package/resources/ai/templates/skills/feature-shaping/SKILL.md +45 -0
  112. package/resources/ai/templates/skills/http-api-conventions/SKILL.md +171 -0
  113. package/resources/ai/templates/skills/init-workflow/SKILL.md +52 -0
  114. package/resources/ai/templates/skills/readme-authoring/SKILL.md +51 -0
  115. package/resources/ai/templates/skills/refactor-workflow/SKILL.md +50 -0
  116. package/resources/ai/templates/skills/simplicity-audit/SKILL.md +41 -0
  117. package/resources/ai/templates/skills/test-scope-selection/SKILL.md +50 -0
  118. package/resources/ai/templates/skills.index.template.md +25 -0
  119. package/standards/architecture.md +72 -0
  120. package/standards/changelog-policy.md +12 -0
  121. package/standards/manifest.json +36 -0
  122. package/standards/readme.md +56 -0
  123. package/standards/schema.json +124 -0
  124. package/standards/style.md +106 -0
  125. package/standards/testing.md +20 -0
  126. package/standards/tooling.md +38 -0
  127. package/templates/node-lib/.biomeignore +10 -0
  128. package/templates/node-lib/.vscode/extensions.json +1 -0
  129. package/templates/node-lib/.vscode/settings.json +9 -0
  130. package/templates/node-lib/README.md +172 -0
  131. package/templates/node-lib/biome.json +37 -0
  132. package/templates/node-lib/gitignore +6 -0
  133. package/templates/node-lib/package.json +32 -0
  134. package/templates/node-lib/scripts/release-publish.mjs +106 -0
  135. package/templates/node-lib/scripts/run-tests.mjs +65 -0
  136. package/templates/node-lib/src/config.ts +3 -0
  137. package/templates/node-lib/src/index.ts +2 -0
  138. package/templates/node-lib/src/logger.ts +7 -0
  139. package/templates/node-lib/src/package-info/package-info.service.ts +47 -0
  140. package/templates/node-lib/test/package-info.test.ts +10 -0
  141. package/templates/node-lib/tsconfig.build.json +1 -0
  142. package/templates/node-lib/tsconfig.json +5 -0
  143. package/templates/node-service/.biomeignore +10 -0
  144. package/templates/node-service/.vscode/extensions.json +1 -0
  145. package/templates/node-service/.vscode/settings.json +9 -0
  146. package/templates/node-service/README.md +244 -0
  147. package/templates/node-service/biome.json +37 -0
  148. package/templates/node-service/ecosystem.config.cjs +3 -0
  149. package/templates/node-service/gitignore +6 -0
  150. package/templates/node-service/package.json +42 -0
  151. package/templates/node-service/scripts/release-publish.mjs +106 -0
  152. package/templates/node-service/scripts/run-tests.mjs +65 -0
  153. package/templates/node-service/src/app/service-runtime.service.ts +57 -0
  154. package/templates/node-service/src/app-info/app-info.service.ts +47 -0
  155. package/templates/node-service/src/config.ts +11 -0
  156. package/templates/node-service/src/http/http-server.service.ts +66 -0
  157. package/templates/node-service/src/index.ts +2 -0
  158. package/templates/node-service/src/logger.ts +7 -0
  159. package/templates/node-service/src/main.ts +5 -0
  160. package/templates/node-service/test/service-runtime.test.ts +13 -0
  161. package/templates/node-service/tsconfig.build.json +1 -0
  162. package/templates/node-service/tsconfig.json +5 -0
  163. package/tsconfig/base.json +16 -0
  164. package/tsconfig/node-lib.json +5 -0
  165. 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,3 @@
1
+ const config = { MINIMUM_INVOICE_AMOUNT: 0, BILLING_CURRENCY_SYMBOL: "$", STATUS_SERVICE_URL: "https://status.example.com/health" } as const;
2
+
3
+ export default config;
@@ -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
+ }