@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,267 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { AI_ADAPTER_FILES, TEMPLATE_NAMES, TEMPLATE_REQUIRED_FILES } from "../constants.mjs";
5
+ import { loadContractSchema } from "../contract/load-rule-catalog.mjs";
6
+ import { resolvePackageRoot } from "../paths.mjs";
7
+ import { readProjectPackageJson } from "../project/package-metadata.mjs";
8
+ import { listRelativeFiles } from "../project/template-resolution.mjs";
9
+ import { pathExists, readJsonFile, validateAgainstSchema } from "../utils/fs.mjs";
10
+ import { verifyChangeAudits } from "./change-audit-verifier.mjs";
11
+ import { readChangedFiles } from "./change-context.mjs";
12
+ import { verifyErrorHandling } from "./error-handling-verifier.mjs";
13
+ import {
14
+ buildVerificationResult,
15
+ createVerificationIssue,
16
+ getVerifyRuleMap,
17
+ isFileSelected,
18
+ normalizeRequestedFiles,
19
+ shouldRunRule,
20
+ } from "./issue-helpers.mjs";
21
+ import { verifyProjectLayout } from "./project-layout-verifier.mjs";
22
+ import { verifyReadme } from "./readme-verifier.mjs";
23
+ import { loadProjectAnalysisContext } from "./source-analysis.mjs";
24
+ import { verifySourceRules } from "./source-rule-verifier.mjs";
25
+ import { verifyTestingRules } from "./testing-verifier.mjs";
26
+ import { verifyTooling } from "./tooling-verifier.mjs";
27
+ import { verifyTypeScriptStyle } from "./typescript-style-verifier.mjs";
28
+
29
+ const DISALLOWED_SOURCE_EXTENSIONS = [".js", ".mjs", ".cjs"];
30
+
31
+ function isForbiddenSourceFile(filePath) {
32
+ return (filePath.startsWith("src/") || filePath.startsWith("test/")) && DISALLOWED_SOURCE_EXTENSIONS.some((extension) => filePath.endsWith(extension));
33
+ }
34
+
35
+ function addCheckedRuleId(checkedRuleIds, onlyRuleIds, ruleId) {
36
+ if (shouldRunRule(onlyRuleIds, ruleId)) {
37
+ checkedRuleIds.push(ruleId);
38
+ }
39
+ }
40
+
41
+ export async function verifyProject(targetPath, options = {}) {
42
+ const packageRoot = resolvePackageRoot();
43
+ const issues = [];
44
+ const checkedRuleIds = [];
45
+ const checkedFiles = [];
46
+ const contractPath = path.join(targetPath, "ai", "contract.json");
47
+ const agentsPath = path.join(targetPath, "AGENTS.md");
48
+ const selectedFiles = normalizeRequestedFiles(targetPath, options.files);
49
+ const shouldCheckReadme = isFileSelected(selectedFiles, "README.md");
50
+ const shouldCheckSourceSubset = selectedFiles.length === 0 || selectedFiles.some((filePath) => filePath.startsWith("src/") || filePath.startsWith("test/"));
51
+
52
+ addCheckedRuleId(checkedRuleIds, options.onlyRuleIds, "contract-presence");
53
+
54
+ if (!(await pathExists(agentsPath)) && shouldRunRule(options.onlyRuleIds, "contract-presence")) {
55
+ checkedFiles.push("AGENTS.md");
56
+ issues.push(
57
+ createVerificationIssue({
58
+ ruleId: "contract-presence",
59
+ category: "contract",
60
+ relativePath: "AGENTS.md",
61
+ message: "missing AGENTS.md",
62
+ }),
63
+ );
64
+ }
65
+
66
+ if (!(await pathExists(contractPath)) && shouldRunRule(options.onlyRuleIds, "contract-presence")) {
67
+ checkedFiles.push("ai/contract.json");
68
+ issues.push(
69
+ createVerificationIssue({
70
+ ruleId: "contract-presence",
71
+ category: "contract",
72
+ relativePath: "ai/contract.json",
73
+ message: "missing ai/contract.json",
74
+ }),
75
+ );
76
+ }
77
+
78
+ if (issues.length > 0) {
79
+ return buildVerificationResult(issues, checkedRuleIds, checkedFiles, { strict: options.strict });
80
+ }
81
+
82
+ const [contract, contractSchema, { packageJson }] = await Promise.all([
83
+ readJsonFile(contractPath),
84
+ loadContractSchema(packageRoot),
85
+ readProjectPackageJson(targetPath),
86
+ ]);
87
+
88
+ validateAgainstSchema(contract, contractSchema, contractPath);
89
+
90
+ const files = await listRelativeFiles(targetPath);
91
+ const metadata = packageJson.codeStandards ?? {};
92
+ const analysisContext = shouldCheckSourceSubset ? await loadProjectAnalysisContext(targetPath, { files: selectedFiles }) : null;
93
+ const changedFilesContext = readChangedFiles(targetPath, {
94
+ changedAgainst: options.changedAgainst,
95
+ staged: options.staged,
96
+ allFiles: options.allFiles,
97
+ });
98
+
99
+ if (!TEMPLATE_NAMES.includes(contract.project.template) && shouldRunRule(options.onlyRuleIds, "contract-presence")) {
100
+ checkedFiles.push("ai/contract.json");
101
+ issues.push(
102
+ createVerificationIssue({
103
+ ruleId: "contract-presence",
104
+ category: "contract",
105
+ relativePath: "ai/contract.json",
106
+ message: `unsupported template in contract: ${contract.project.template}`,
107
+ }),
108
+ );
109
+ }
110
+
111
+ addCheckedRuleId(checkedRuleIds, options.onlyRuleIds, "metadata-sync");
112
+ if (shouldRunRule(options.onlyRuleIds, "metadata-sync")) {
113
+ checkedFiles.push("package.json", "ai/contract.json");
114
+
115
+ if (metadata.contractVersion !== contract.formatVersion) {
116
+ issues.push(
117
+ createVerificationIssue({
118
+ ruleId: "metadata-sync",
119
+ category: "metadata-sync",
120
+ relativePath: "package.json",
121
+ message: "package.json.codeStandards.contractVersion must match ai/contract.json formatVersion",
122
+ }),
123
+ );
124
+ }
125
+
126
+ if (metadata.template !== contract.project.template) {
127
+ issues.push(
128
+ createVerificationIssue({
129
+ ruleId: "metadata-sync",
130
+ category: "metadata-sync",
131
+ relativePath: "package.json",
132
+ message: "package.json.codeStandards.template must match ai/contract.json project.template",
133
+ }),
134
+ );
135
+ }
136
+
137
+ if (metadata.withAiAdapters !== contract.project.withAiAdapters) {
138
+ issues.push(
139
+ createVerificationIssue({
140
+ ruleId: "metadata-sync",
141
+ category: "metadata-sync",
142
+ relativePath: "package.json",
143
+ message: "package.json.codeStandards.withAiAdapters must match ai/contract.json project.withAiAdapters",
144
+ }),
145
+ );
146
+ }
147
+ }
148
+
149
+ addCheckedRuleId(checkedRuleIds, options.onlyRuleIds, "managed-files");
150
+ if (shouldRunRule(options.onlyRuleIds, "managed-files")) {
151
+ for (const managedFile of contract.managedFiles) {
152
+ checkedFiles.push(managedFile);
153
+ if (!(await pathExists(path.join(targetPath, managedFile)))) {
154
+ issues.push(
155
+ createVerificationIssue({
156
+ ruleId: "managed-files",
157
+ category: "managed-files",
158
+ relativePath: managedFile,
159
+ message: `missing managed file: ${managedFile}`,
160
+ }),
161
+ );
162
+ }
163
+ }
164
+ }
165
+
166
+ addCheckedRuleId(checkedRuleIds, options.onlyRuleIds, "typescript-only");
167
+ if (shouldRunRule(options.onlyRuleIds, "typescript-only") && shouldCheckSourceSubset) {
168
+ const checkedSourceFiles = files.filter(
169
+ (filePath) => (filePath.startsWith("src/") || filePath.startsWith("test/")) && isFileSelected(selectedFiles, filePath),
170
+ );
171
+ checkedFiles.push(...checkedSourceFiles);
172
+
173
+ for (const filePath of checkedSourceFiles.filter(isForbiddenSourceFile)) {
174
+ issues.push(
175
+ createVerificationIssue({
176
+ ruleId: "typescript-only",
177
+ category: "typescript-only",
178
+ relativePath: filePath,
179
+ message: `forbidden JS file in src/test: ${filePath}`,
180
+ }),
181
+ );
182
+ }
183
+ }
184
+
185
+ addCheckedRuleId(checkedRuleIds, options.onlyRuleIds, "template-layout");
186
+ if (shouldRunRule(options.onlyRuleIds, "template-layout")) {
187
+ const requiredTemplateFiles = (TEMPLATE_REQUIRED_FILES[contract.project.template] ?? []).filter((filePath) => isFileSelected(selectedFiles, filePath));
188
+ checkedFiles.push(...requiredTemplateFiles);
189
+
190
+ for (const requiredFile of requiredTemplateFiles) {
191
+ if (!files.includes(requiredFile)) {
192
+ issues.push(
193
+ createVerificationIssue({
194
+ ruleId: "template-layout",
195
+ category: "template-layout",
196
+ relativePath: requiredFile,
197
+ message: `missing required template file: ${requiredFile}`,
198
+ }),
199
+ );
200
+ }
201
+ }
202
+ }
203
+
204
+ for (const ruleId of ["readme-sections", "readme-public-api", "readme-public-methods", "readme-http-api", "readme-usage-examples"]) {
205
+ addCheckedRuleId(checkedRuleIds, options.onlyRuleIds, ruleId);
206
+ }
207
+ if (shouldCheckReadme) {
208
+ checkedFiles.push("README.md");
209
+ const readmeIssues = await verifyReadme(targetPath, {
210
+ template: contract.project.template,
211
+ packageName: packageJson.name,
212
+ onlyRuleIds: options.onlyRuleIds,
213
+ contract,
214
+ });
215
+ issues.push(...readmeIssues.filter((issue) => shouldRunRule(options.onlyRuleIds, issue.ruleId)));
216
+ }
217
+
218
+ addCheckedRuleId(checkedRuleIds, options.onlyRuleIds, "adapter-presence");
219
+ if (contract.project.withAiAdapters && shouldRunRule(options.onlyRuleIds, "adapter-presence")) {
220
+ const adapterDir = path.join(targetPath, "ai");
221
+ const adapterEntries = await readdir(adapterDir);
222
+ for (const adapterName of AI_ADAPTER_FILES) {
223
+ const relativePath = path.join("ai", adapterName).split(path.sep).join("/");
224
+ checkedFiles.push(relativePath);
225
+ if (!adapterEntries.includes(adapterName)) {
226
+ issues.push(
227
+ createVerificationIssue({
228
+ ruleId: "adapter-presence",
229
+ category: "adapter-presence",
230
+ relativePath,
231
+ message: `missing ai/${adapterName}`,
232
+ }),
233
+ );
234
+ }
235
+ }
236
+ }
237
+
238
+ if (analysisContext) {
239
+ const [sourceResult, layoutIssues, tsStyleIssues, errorHandlingIssues, testingIssues, changeAuditIssues] = await Promise.all([
240
+ verifySourceRules(targetPath, contract, {
241
+ onlyRuleIds: options.onlyRuleIds,
242
+ files: selectedFiles,
243
+ analysisContext,
244
+ }),
245
+ verifyProjectLayout(targetPath, contract, analysisContext, { onlyRuleIds: options.onlyRuleIds }),
246
+ verifyTypeScriptStyle(targetPath, contract, analysisContext, { onlyRuleIds: options.onlyRuleIds }),
247
+ verifyErrorHandling(targetPath, contract, analysisContext, { onlyRuleIds: options.onlyRuleIds }),
248
+ verifyTestingRules(targetPath, contract, analysisContext, changedFilesContext, { onlyRuleIds: options.onlyRuleIds }),
249
+ verifyChangeAudits(targetPath, contract, analysisContext, changedFilesContext, { onlyRuleIds: options.onlyRuleIds }),
250
+ ]);
251
+ issues.push(...sourceResult, ...layoutIssues, ...tsStyleIssues, ...errorHandlingIssues, ...testingIssues, ...changeAuditIssues);
252
+ }
253
+
254
+ const toolingIssues = await verifyTooling(targetPath, contract, packageJson, { onlyRuleIds: options.onlyRuleIds });
255
+ issues.push(...toolingIssues);
256
+
257
+ for (const ruleId of getVerifyRuleMap(contract).keys()) {
258
+ addCheckedRuleId(checkedRuleIds, options.onlyRuleIds, ruleId);
259
+ }
260
+
261
+ const checkedSourceFiles = files.filter(
262
+ (filePath) => (filePath.startsWith("src/") || filePath.startsWith("test/")) && isFileSelected(selectedFiles, filePath),
263
+ );
264
+ checkedFiles.push(...checkedSourceFiles);
265
+
266
+ return buildVerificationResult(issues, checkedRuleIds, checkedFiles, { strict: options.strict });
267
+ }
@@ -0,0 +1,237 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import ts from "typescript";
5
+
6
+ import { pathExists } from "../utils/fs.mjs";
7
+
8
+ function hasExportModifier(node) {
9
+ return Array.isArray(node.modifiers) && node.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
10
+ }
11
+
12
+ function getDeclarationName(node) {
13
+ return node.name && ts.isIdentifier(node.name) ? node.name.text : null;
14
+ }
15
+
16
+ function hasModifier(node, modifierKind) {
17
+ return Array.isArray(node.modifiers) && node.modifiers.some((modifier) => modifier.kind === modifierKind);
18
+ }
19
+
20
+ function isPublicMethod(member) {
21
+ return !hasModifier(member, ts.SyntaxKind.PrivateKeyword) && !hasModifier(member, ts.SyntaxKind.ProtectedKeyword);
22
+ }
23
+
24
+ function getPublicMethods(node) {
25
+ const methods = [];
26
+
27
+ for (const member of node.members) {
28
+ if (!ts.isMethodDeclaration(member) || !isPublicMethod(member)) {
29
+ continue;
30
+ }
31
+
32
+ const methodName = member.name && ts.isIdentifier(member.name) ? member.name.text : null;
33
+
34
+ if (!methodName) {
35
+ continue;
36
+ }
37
+
38
+ methods.push(methodName);
39
+ }
40
+
41
+ return methods.sort((left, right) => left.localeCompare(right));
42
+ }
43
+
44
+ function createDescriptor(kind, publicMethods = []) {
45
+ return { kind, publicMethods };
46
+ }
47
+
48
+ function collectLocalDeclarations(sourceFile) {
49
+ const declarations = new Map();
50
+
51
+ for (const statement of sourceFile.statements) {
52
+ if (ts.isClassDeclaration(statement)) {
53
+ const className = getDeclarationName(statement);
54
+ if (className) {
55
+ declarations.set(className, createDescriptor("class", getPublicMethods(statement)));
56
+ }
57
+ continue;
58
+ }
59
+
60
+ if (ts.isFunctionDeclaration(statement)) {
61
+ const functionName = getDeclarationName(statement);
62
+ if (functionName) {
63
+ declarations.set(functionName, createDescriptor("function"));
64
+ }
65
+ continue;
66
+ }
67
+
68
+ if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement)) {
69
+ const typeName = getDeclarationName(statement);
70
+ if (typeName) {
71
+ declarations.set(typeName, createDescriptor("type"));
72
+ }
73
+ continue;
74
+ }
75
+
76
+ if (ts.isEnumDeclaration(statement)) {
77
+ const enumName = getDeclarationName(statement);
78
+ if (enumName) {
79
+ declarations.set(enumName, createDescriptor("value"));
80
+ }
81
+ continue;
82
+ }
83
+
84
+ if (ts.isVariableStatement(statement)) {
85
+ for (const declaration of statement.declarationList.declarations) {
86
+ if (ts.isIdentifier(declaration.name)) {
87
+ declarations.set(declaration.name.text, createDescriptor("value"));
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ return declarations;
94
+ }
95
+
96
+ function resolveModuleSpecifier(fromFilePath, specifier) {
97
+ if (!specifier.startsWith(".")) {
98
+ return null;
99
+ }
100
+
101
+ const basePath = path.resolve(path.dirname(fromFilePath), specifier);
102
+ const candidates = [basePath, `${basePath}.ts`, path.join(basePath, "index.ts")];
103
+
104
+ return candidates;
105
+ }
106
+
107
+ async function readSourceFile(filePath) {
108
+ if (!(await pathExists(filePath))) {
109
+ return null;
110
+ }
111
+
112
+ const raw = await readFile(filePath, "utf8");
113
+ return ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
114
+ }
115
+
116
+ async function extractModuleExports(filePath, cache) {
117
+ if (cache.has(filePath)) {
118
+ return cache.get(filePath);
119
+ }
120
+
121
+ const sourceFile = await readSourceFile(filePath);
122
+
123
+ if (!sourceFile) {
124
+ const empty = new Map();
125
+ cache.set(filePath, empty);
126
+ return empty;
127
+ }
128
+
129
+ const declarations = collectLocalDeclarations(sourceFile);
130
+ const exportedDeclarations = new Map();
131
+ cache.set(filePath, exportedDeclarations);
132
+
133
+ for (const statement of sourceFile.statements) {
134
+ if (
135
+ hasExportModifier(statement) &&
136
+ (ts.isClassDeclaration(statement) ||
137
+ ts.isFunctionDeclaration(statement) ||
138
+ ts.isTypeAliasDeclaration(statement) ||
139
+ ts.isInterfaceDeclaration(statement) ||
140
+ ts.isEnumDeclaration(statement))
141
+ ) {
142
+ const name = getDeclarationName(statement);
143
+
144
+ if (name && declarations.has(name)) {
145
+ exportedDeclarations.set(name, declarations.get(name));
146
+ }
147
+
148
+ continue;
149
+ }
150
+
151
+ if (hasExportModifier(statement) && ts.isVariableStatement(statement)) {
152
+ for (const declaration of statement.declarationList.declarations) {
153
+ if (ts.isIdentifier(declaration.name) && declarations.has(declaration.name.text)) {
154
+ exportedDeclarations.set(declaration.name.text, declarations.get(declaration.name.text));
155
+ }
156
+ }
157
+
158
+ continue;
159
+ }
160
+
161
+ if (!ts.isExportDeclaration(statement)) {
162
+ continue;
163
+ }
164
+
165
+ const moduleSpecifier = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : null;
166
+
167
+ if (!moduleSpecifier) {
168
+ if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
169
+ for (const exportElement of statement.exportClause.elements) {
170
+ const sourceName = exportElement.propertyName?.text ?? exportElement.name.text;
171
+ const exportName = exportElement.name.text;
172
+ const descriptor = declarations.get(sourceName);
173
+
174
+ if (descriptor) {
175
+ exportedDeclarations.set(exportName, descriptor);
176
+ }
177
+ }
178
+ }
179
+
180
+ continue;
181
+ }
182
+
183
+ const candidates = resolveModuleSpecifier(filePath, moduleSpecifier);
184
+
185
+ if (!candidates) {
186
+ continue;
187
+ }
188
+
189
+ let targetExports = null;
190
+
191
+ for (const candidatePath of candidates) {
192
+ if (await pathExists(candidatePath)) {
193
+ targetExports = await extractModuleExports(candidatePath, cache);
194
+ break;
195
+ }
196
+ }
197
+
198
+ if (!targetExports) {
199
+ continue;
200
+ }
201
+
202
+ if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
203
+ for (const exportElement of statement.exportClause.elements) {
204
+ const sourceName = exportElement.propertyName?.text ?? exportElement.name.text;
205
+ const exportName = exportElement.name.text;
206
+ const descriptor = targetExports.get(sourceName);
207
+
208
+ if (descriptor) {
209
+ exportedDeclarations.set(exportName, descriptor);
210
+ }
211
+ }
212
+
213
+ continue;
214
+ }
215
+
216
+ for (const [exportName, descriptor] of targetExports.entries()) {
217
+ exportedDeclarations.set(exportName, descriptor);
218
+ }
219
+ }
220
+
221
+ return exportedDeclarations;
222
+ }
223
+
224
+ export async function extractPublicReadmeApi(targetPath) {
225
+ const indexPath = path.join(targetPath, "src", "index.ts");
226
+ const exportedDeclarations = await extractModuleExports(indexPath, new Map());
227
+
228
+ return {
229
+ exports: [...exportedDeclarations.entries()]
230
+ .map(([exportName, descriptor]) => ({
231
+ exportName,
232
+ kind: descriptor.kind,
233
+ publicMethods: descriptor.publicMethods,
234
+ }))
235
+ .sort((left, right) => left.exportName.localeCompare(right.exportName)),
236
+ };
237
+ }