@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,33 @@
1
+ import { promptYesNo, withReadline } from "../utils/prompts.mjs";
2
+
3
+ const DEFAULTS = {
4
+ publicApi: true,
5
+ persistence: true,
6
+ transport: true,
7
+ configuration: true,
8
+ runtimeBoundaries: true,
9
+ };
10
+
11
+ export function getDefaultPreservationDecisions() {
12
+ return { ...DEFAULTS };
13
+ }
14
+
15
+ export async function resolvePreservationDecisions(rawOptions) {
16
+ if (rawOptions.yes || !process.stdin.isTTY) {
17
+ return getDefaultPreservationDecisions();
18
+ }
19
+
20
+ return withReadline(async (rl) => {
21
+ const publicApi = await promptYesNo(rl, "Preserve public package contracts (exports, public names, observable types/shapes)?", DEFAULTS.publicApi);
22
+ const persistence = await promptYesNo(rl, "Preserve persistence contracts (DB schema, migrations, table and column names)?", DEFAULTS.persistence);
23
+ const transport = await promptYesNo(rl, "Preserve transport contracts (HTTP routes, status codes, message payloads)?", DEFAULTS.transport);
24
+ const configuration = await promptYesNo(rl, "Preserve configuration contracts (env vars, config keys, config filenames)?", DEFAULTS.configuration);
25
+ const runtimeBoundaries = await promptYesNo(
26
+ rl,
27
+ "Preserve runtime and package boundaries (ESM/CJS mode, bin scripts, exports map, build artifacts)?",
28
+ DEFAULTS.runtimeBoundaries,
29
+ );
30
+
31
+ return { publicApi, persistence, transport, configuration, runtimeBoundaries };
32
+ });
33
+ }
@@ -0,0 +1,22 @@
1
+ export function extractPublicContract(projectPackageJson, template, profilePath, sourceAnalysis) {
2
+ return {
3
+ template,
4
+ profilePath,
5
+ package: {
6
+ name: projectPackageJson.name ?? null,
7
+ version: projectPackageJson.version ?? null,
8
+ type: projectPackageJson.type ?? null,
9
+ main: projectPackageJson.main ?? null,
10
+ types: projectPackageJson.types ?? null,
11
+ exports: projectPackageJson.exports ?? null,
12
+ bin: projectPackageJson.bin ?? null,
13
+ },
14
+ repository: projectPackageJson.repository ?? null,
15
+ publicEntrypoints: sourceAnalysis.entrypoints,
16
+ compatibilitySurface: {
17
+ hasDatabaseSurface: sourceAnalysis.hasDatabaseSurface,
18
+ hasTransportSurface: sourceAnalysis.hasTransportSurface,
19
+ envVars: sourceAnalysis.envVars,
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,50 @@
1
+ function renderList(items, fallback = "- none detected") {
2
+ if (!items || items.length === 0) {
3
+ return fallback;
4
+ }
5
+
6
+ return items.map((item) => `- ${item}`).join("\n");
7
+ }
8
+
9
+ export function renderAnalysisSummary(projectPackageJson, sourceAnalysis, preservation) {
10
+ const publicEntrypoints = sourceAnalysis.entrypoints.map((entrypoint) => `\`${entrypoint.kind}\`: \`${JSON.stringify(entrypoint.value)}\``);
11
+ const compatibilityNotes = [
12
+ `- preserve public API: ${preservation.publicApi ? "yes" : "no"}`,
13
+ `- preserve persistence contracts: ${preservation.persistence ? "yes" : "no"}`,
14
+ `- preserve transport contracts: ${preservation.transport ? "yes" : "no"}`,
15
+ `- preserve configuration surface: ${preservation.configuration ? "yes" : "no"}`,
16
+ `- preserve runtime/package boundaries: ${preservation.runtimeBoundaries ? "yes" : "no"}`,
17
+ ].join("\n");
18
+
19
+ return `# Refactor Analysis Summary
20
+
21
+ ## Package
22
+
23
+ - name: \`${projectPackageJson.name ?? "unknown"}\`
24
+ - version: \`${projectPackageJson.version ?? "unknown"}\`
25
+ - module type: \`${projectPackageJson.type ?? "unknown"}\`
26
+ - inferred architecture: ${sourceAnalysis.architecture}
27
+
28
+ ## Public Entrypoints
29
+
30
+ ${renderList(publicEntrypoints)}
31
+
32
+ ## Surfaces
33
+
34
+ - database-related files detected: ${sourceAnalysis.hasDatabaseSurface ? "yes" : "no"}
35
+ - transport-related files detected: ${sourceAnalysis.hasTransportSurface ? "yes" : "no"}
36
+
37
+ ## Environment Variables
38
+
39
+ ${renderList(sourceAnalysis.envVars.map((envVar) => `\`${envVar}\``))}
40
+
41
+ ## Source Layout
42
+
43
+ - source files: ${sourceAnalysis.sourceFiles.length}
44
+ - test files: ${sourceAnalysis.testFiles.length}
45
+
46
+ ## Preservation Decisions
47
+
48
+ ${compatibilityNotes}
49
+ `;
50
+ }
@@ -0,0 +1,74 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { listRelativeFiles } from "../project/template-resolution.mjs";
5
+
6
+ const DATABASE_PATH_PATTERN = /(^|\/)(db|database|prisma|drizzle|sequelize|typeorm|migrations?)(\/|$)/i;
7
+ const TRANSPORT_PATH_PATTERN = /(^|\/)(http|server|routes?|controllers?|handlers?|events?|messages?)(\/|$)/i;
8
+ const ENV_VAR_PATTERN = /\bprocess\.env\.([A-Z0-9_]+)/g;
9
+
10
+ function summarizeArchitecture(files) {
11
+ if (files.some((filePath) => filePath.startsWith("src/app/")) || files.some((filePath) => filePath.startsWith("src/http/"))) {
12
+ return "service-oriented feature folders";
13
+ }
14
+
15
+ if (files.some((filePath) => filePath.startsWith("src/") && filePath.split("/").length >= 3)) {
16
+ return "feature folders";
17
+ }
18
+
19
+ return "flat module layout";
20
+ }
21
+
22
+ function collectPublicEntrypoints(projectPackageJson) {
23
+ const entrypoints = [];
24
+
25
+ if (projectPackageJson.main) {
26
+ entrypoints.push({ kind: "main", value: projectPackageJson.main });
27
+ }
28
+
29
+ if (projectPackageJson.types) {
30
+ entrypoints.push({ kind: "types", value: projectPackageJson.types });
31
+ }
32
+
33
+ if (projectPackageJson.bin) {
34
+ entrypoints.push({ kind: "bin", value: projectPackageJson.bin });
35
+ }
36
+
37
+ if (projectPackageJson.exports) {
38
+ entrypoints.push({ kind: "exports", value: projectPackageJson.exports });
39
+ }
40
+
41
+ return entrypoints;
42
+ }
43
+
44
+ async function collectEnvVars(targetPath, files) {
45
+ const envVars = new Set();
46
+
47
+ for (const filePath of files.filter((candidate) => candidate.endsWith(".ts") || candidate === ".env.example")) {
48
+ const raw = await readFile(path.join(targetPath, filePath), "utf8").catch(() => "");
49
+ const matches = raw.matchAll(ENV_VAR_PATTERN);
50
+
51
+ for (const match of matches) {
52
+ envVars.add(match[1]);
53
+ }
54
+ }
55
+
56
+ return [...envVars].sort((left, right) => left.localeCompare(right));
57
+ }
58
+
59
+ export async function analyzeProjectSource(targetPath, projectPackageJson) {
60
+ const files = await listRelativeFiles(targetPath);
61
+ const envVars = await collectEnvVars(targetPath, files);
62
+
63
+ return {
64
+ files,
65
+ packageType: typeof projectPackageJson.type === "string" ? projectPackageJson.type : null,
66
+ entrypoints: collectPublicEntrypoints(projectPackageJson),
67
+ architecture: summarizeArchitecture(files),
68
+ hasDatabaseSurface: files.some((filePath) => DATABASE_PATH_PATTERN.test(filePath)),
69
+ hasTransportSurface: files.some((filePath) => TRANSPORT_PATH_PATTERN.test(filePath)),
70
+ envVars,
71
+ testFiles: files.filter((filePath) => filePath.startsWith("test/")),
72
+ sourceFiles: files.filter((filePath) => filePath.startsWith("src/")),
73
+ };
74
+ }
@@ -0,0 +1,220 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { access, copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ import Ajv2020 from "ajv/dist/2020.js";
7
+
8
+ import { PROFILE_KEY_ORDER } from "../constants.mjs";
9
+ import { mapTemplateFileName, replaceTokens } from "./text.mjs";
10
+
11
+ export async function readJsonFile(filePath) {
12
+ const raw = await readFile(filePath, "utf8");
13
+ return JSON.parse(raw);
14
+ }
15
+
16
+ export async function writeJsonFile(filePath, value) {
17
+ await mkdir(path.dirname(filePath), { recursive: true });
18
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
19
+ }
20
+
21
+ export async function writeTextFile(filePath, value) {
22
+ await mkdir(path.dirname(filePath), { recursive: true });
23
+ await writeFile(filePath, value, "utf8");
24
+ }
25
+
26
+ export async function pathExists(targetPath) {
27
+ try {
28
+ await access(targetPath, constants.F_OK);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ export async function ensureTargetReady(targetPath, force) {
36
+ const exists = await pathExists(targetPath);
37
+
38
+ if (!exists) {
39
+ await mkdir(targetPath, { recursive: true });
40
+ return;
41
+ }
42
+
43
+ const fileStat = await stat(targetPath);
44
+
45
+ if (!fileStat.isDirectory()) {
46
+ throw new Error(`Target exists and is not a directory: ${targetPath}`);
47
+ }
48
+
49
+ const entries = await readdir(targetPath);
50
+ const nonGitEntries = entries.filter((entry) => entry !== ".git");
51
+
52
+ if (nonGitEntries.length > 0 && !force) {
53
+ throw new Error(`Target directory is not empty: ${targetPath}. Use --force to continue and overwrite files.`);
54
+ }
55
+ }
56
+
57
+ export async function collectTemplateFiles(templateDir, baseDir = templateDir) {
58
+ const entries = await readdir(templateDir, { withFileTypes: true });
59
+ const files = [];
60
+
61
+ for (const entry of entries) {
62
+ const sourcePath = path.join(templateDir, entry.name);
63
+
64
+ if (entry.isDirectory()) {
65
+ files.push(...(await collectTemplateFiles(sourcePath, baseDir)));
66
+ continue;
67
+ }
68
+
69
+ if (!entry.isFile()) {
70
+ continue;
71
+ }
72
+
73
+ const sourceRelativePath = path.relative(baseDir, sourcePath);
74
+ const sourceDirectory = path.dirname(sourceRelativePath);
75
+ const sourceFileName = path.basename(sourceRelativePath);
76
+ const mappedFileName = mapTemplateFileName(sourceFileName);
77
+ const targetRelativePath = sourceDirectory === "." ? mappedFileName : path.join(sourceDirectory, mappedFileName);
78
+
79
+ files.push({ sourcePath, sourceRelativePath, targetRelativePath });
80
+ }
81
+
82
+ return files.sort((left, right) => left.targetRelativePath.localeCompare(right.targetRelativePath));
83
+ }
84
+
85
+ export async function copyTemplateDirectory(sourceDir, targetDir, tokens) {
86
+ await mkdir(targetDir, { recursive: true });
87
+ const entries = await readdir(sourceDir, { withFileTypes: true });
88
+
89
+ for (const entry of entries) {
90
+ const sourcePath = path.join(sourceDir, entry.name);
91
+
92
+ if (entry.isDirectory()) {
93
+ await copyTemplateDirectory(sourcePath, path.join(targetDir, entry.name), tokens);
94
+ continue;
95
+ }
96
+
97
+ if (entry.isSymbolicLink()) {
98
+ continue;
99
+ }
100
+
101
+ const raw = await readFile(sourcePath, "utf8");
102
+ const targetName = mapTemplateFileName(entry.name);
103
+ const targetPath = path.join(targetDir, targetName);
104
+ await writeFile(targetPath, replaceTokens(raw, tokens), "utf8");
105
+ }
106
+ }
107
+
108
+ export async function mirrorFile(sourcePath, targetPath, tokens) {
109
+ const raw = await readFile(sourcePath, "utf8");
110
+ await mkdir(path.dirname(targetPath), { recursive: true });
111
+ await writeFile(targetPath, replaceTokens(raw, tokens), "utf8");
112
+ }
113
+
114
+ export function asPlainObject(value) {
115
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
116
+ return {};
117
+ }
118
+
119
+ return value;
120
+ }
121
+
122
+ export function normalizeProfile(rawProfile) {
123
+ const normalized = {};
124
+
125
+ for (const key of PROFILE_KEY_ORDER) {
126
+ normalized[key] = rawProfile[key];
127
+ }
128
+
129
+ return normalized;
130
+ }
131
+
132
+ export function validateAgainstSchema(value, schema, sourceLabel) {
133
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
134
+ const validate = ajv.compile(schema);
135
+ const valid = validate(value);
136
+
137
+ if (valid) {
138
+ return;
139
+ }
140
+
141
+ const details = (validate.errors ?? []).map((issue) => `${issue.instancePath || "/"}: ${issue.message ?? "invalid"}`).join("; ");
142
+ throw new Error(`Invalid data at ${sourceLabel}: ${details}`);
143
+ }
144
+
145
+ export async function runCommand(command, args, cwd) {
146
+ return new Promise((resolve, reject) => {
147
+ const child = spawn(command, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
148
+
149
+ child.on("error", reject);
150
+ child.on("exit", (code) => {
151
+ if (code === 0) {
152
+ resolve();
153
+ return;
154
+ }
155
+
156
+ reject(new Error(`${command} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
157
+ });
158
+ });
159
+ }
160
+
161
+ export async function formatFilesWithBiome(packageRoot, targetDir, relativePaths) {
162
+ const filesToFormat = [...new Set(relativePaths)].filter((relativePath) => typeof relativePath === "string" && relativePath.length > 0);
163
+
164
+ if (filesToFormat.length === 0) {
165
+ return;
166
+ }
167
+
168
+ const biomeBinPath = path.join(packageRoot, "node_modules", "@biomejs", "biome", "bin", "biome");
169
+
170
+ if (!(await pathExists(biomeBinPath))) {
171
+ throw new Error(`Biome binary was not found at ${biomeBinPath}. Install @biomejs/biome before running init or refactor.`);
172
+ }
173
+
174
+ await runCommand(process.execPath, [biomeBinPath, "format", "--write", ...filesToFormat], targetDir);
175
+ }
176
+
177
+ export async function copyFileIfExists(sourcePath, targetPath) {
178
+ if (!(await pathExists(sourcePath))) {
179
+ return;
180
+ }
181
+
182
+ await mkdir(path.dirname(targetPath), { recursive: true });
183
+ await copyFile(sourcePath, targetPath);
184
+ }
185
+
186
+ export async function removePathIfExists(targetPath) {
187
+ if (!(await pathExists(targetPath))) {
188
+ return;
189
+ }
190
+
191
+ await rm(targetPath, { force: true, recursive: true });
192
+ }
193
+
194
+ export async function copyDirectoryWithFilter(sourceDir, targetDir, shouldSkipRelativePath, baseDir = sourceDir) {
195
+ await mkdir(targetDir, { recursive: true });
196
+ const entries = await readdir(sourceDir, { withFileTypes: true });
197
+
198
+ for (const entry of entries) {
199
+ const sourcePath = path.join(sourceDir, entry.name);
200
+ const relativePath = path.relative(baseDir, sourcePath);
201
+
202
+ if (shouldSkipRelativePath(relativePath)) {
203
+ continue;
204
+ }
205
+
206
+ const targetPath = path.join(targetDir, entry.name);
207
+
208
+ if (entry.isDirectory()) {
209
+ await copyDirectoryWithFilter(sourcePath, targetPath, shouldSkipRelativePath, baseDir);
210
+ continue;
211
+ }
212
+
213
+ if (!entry.isFile()) {
214
+ continue;
215
+ }
216
+
217
+ await mkdir(path.dirname(targetPath), { recursive: true });
218
+ await copyFile(sourcePath, targetPath);
219
+ }
220
+ }
@@ -0,0 +1,63 @@
1
+ import readline from "node:readline/promises";
2
+
3
+ export async function askChoice(rl, prompt, options, defaultValue) {
4
+ const defaultIndex = options.indexOf(defaultValue);
5
+
6
+ if (defaultIndex < 0) {
7
+ throw new Error(`Invalid default value for question ${prompt}`);
8
+ }
9
+
10
+ console.log(`\n${prompt}`);
11
+
12
+ for (let index = 0; index < options.length; index += 1) {
13
+ console.log(` ${index + 1}) ${options[index]}`);
14
+ }
15
+
16
+ const answer = await rl.question(`Select option [${defaultIndex + 1}]: `);
17
+ const normalized = answer.trim();
18
+
19
+ if (normalized.length === 0) {
20
+ return defaultValue;
21
+ }
22
+
23
+ const numeric = Number.parseInt(normalized, 10);
24
+
25
+ if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= options.length) {
26
+ return options[numeric - 1];
27
+ }
28
+
29
+ if (options.includes(normalized)) {
30
+ return normalized;
31
+ }
32
+
33
+ throw new Error(`Invalid option for ${prompt}: ${answer}`);
34
+ }
35
+
36
+ export async function promptYesNo(rl, prompt, defaultYes = true) {
37
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
38
+ const answer = (await rl.question(`${prompt} ${suffix}: `)).trim().toLowerCase();
39
+
40
+ if (answer.length === 0) {
41
+ return defaultYes;
42
+ }
43
+
44
+ if (answer === "y" || answer === "yes") {
45
+ return true;
46
+ }
47
+
48
+ if (answer === "n" || answer === "no") {
49
+ return false;
50
+ }
51
+
52
+ throw new Error(`Invalid yes/no answer: ${answer}`);
53
+ }
54
+
55
+ export async function withReadline(action) {
56
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
57
+
58
+ try {
59
+ return await action(rl);
60
+ } finally {
61
+ rl.close();
62
+ }
63
+ }
@@ -0,0 +1,43 @@
1
+ export function replaceTokens(template, tokens) {
2
+ let rendered = template;
3
+
4
+ for (const [key, value] of Object.entries(tokens)) {
5
+ rendered = rendered.replaceAll(`{{${key}}}`, value);
6
+ }
7
+
8
+ return rendered;
9
+ }
10
+
11
+ export function normalizePackageName(rawPackageName, projectName) {
12
+ if (!rawPackageName || rawPackageName.trim().length === 0) {
13
+ return `@sha3/${projectName}`;
14
+ }
15
+
16
+ return rawPackageName.trim();
17
+ }
18
+
19
+ export function defaultPackageNameForProject(projectName) {
20
+ return `@sha3/${projectName}`;
21
+ }
22
+
23
+ export function defaultRepositoryUrlForPackage(packageName) {
24
+ const repositoryName = packageName.replace(/^@/, "").replace("/", "/");
25
+ const normalizedRepositoryName = repositoryName.includes("/") ? repositoryName.split("/")[1] : repositoryName;
26
+ return `https://github.com/sha3dev/${normalizedRepositoryName}`;
27
+ }
28
+
29
+ export function normalizeRepositoryUrl(rawRepositoryUrl, packageName) {
30
+ if (!rawRepositoryUrl || rawRepositoryUrl.trim().length === 0) {
31
+ return defaultRepositoryUrlForPackage(packageName);
32
+ }
33
+
34
+ return rawRepositoryUrl.trim();
35
+ }
36
+
37
+ export function mapTemplateFileName(fileName) {
38
+ if (fileName === "gitignore") {
39
+ return ".gitignore";
40
+ }
41
+
42
+ return fileName;
43
+ }
@@ -0,0 +1,140 @@
1
+ import ts from "typescript";
2
+
3
+ import { pathExists } from "../utils/fs.mjs";
4
+ import { createContractIssue, getActiveVerifyRuleIds, shouldRunRule } from "./issue-helpers.mjs";
5
+
6
+ const MANAGED_FILE_PREFIXES = ["AGENTS.md", "SKILLS.md", "ai/", "prompts/", "skills/", "biome.json", "tsconfig.json", "tsconfig.build.json", ".vscode/"];
7
+
8
+ function createIssue(contract, ruleId, relativePath, message, options = {}) {
9
+ return createContractIssue(contract, {
10
+ ruleId,
11
+ category: "change-audit",
12
+ relativePath,
13
+ message,
14
+ verificationMode: options.verificationMode,
15
+ confidence: options.confidence,
16
+ evidence: options.evidence,
17
+ });
18
+ }
19
+
20
+ export async function verifyChangeAudits(targetPath, contract, analysisContext, changedFilesContext, options = {}) {
21
+ const activeRuleIds = getActiveVerifyRuleIds(contract);
22
+ const issues = [];
23
+ const isRefactorSession = await pathExists(`${targetPath}/.code-standards/refactor-source/latest`);
24
+
25
+ if (activeRuleIds.has("managed-files-read-only") && shouldRunRule(options.onlyRuleIds, "managed-files-read-only")) {
26
+ issues.push(...verifyManagedFilesReadOnly(contract, changedFilesContext, isRefactorSession));
27
+ }
28
+
29
+ if (activeRuleIds.has("simplicity-audit") && shouldRunRule(options.onlyRuleIds, "simplicity-audit")) {
30
+ issues.push(...verifySimplicityAudit(contract, analysisContext));
31
+ }
32
+
33
+ if (activeRuleIds.has("no-speculative-abstractions") && shouldRunRule(options.onlyRuleIds, "no-speculative-abstractions")) {
34
+ issues.push(...verifyNoSpeculativeAbstractions(contract, analysisContext));
35
+ }
36
+
37
+ return issues;
38
+ }
39
+
40
+ function verifyManagedFilesReadOnly(contract, changedFilesContext, isRefactorSession) {
41
+ if (isRefactorSession) {
42
+ return [];
43
+ }
44
+
45
+ if (!changedFilesContext.available) {
46
+ return [];
47
+ }
48
+
49
+ const changedManagedFiles = changedFilesContext.files.filter((file) => MANAGED_FILE_PREFIXES.some((prefix) => file === prefix || file.startsWith(prefix)));
50
+ return changedManagedFiles.map((relativePath) =>
51
+ createIssue(contract, "managed-files-read-only", relativePath, "managed files are read-only during normal feature work", {
52
+ verificationMode: "audit",
53
+ confidence: "medium",
54
+ }),
55
+ );
56
+ }
57
+
58
+ function verifySimplicityAudit(contract, analysisContext) {
59
+ const issues = [];
60
+ const featureLayerCounts = new Map();
61
+
62
+ for (const file of analysisContext.sourceFiles) {
63
+ if (!file.featureName || file.relativePath.endsWith(".types.ts")) {
64
+ continue;
65
+ }
66
+
67
+ const role = file.relativePath.split("/").at(-1)?.split(".").slice(-2, -1)[0] ?? "unknown";
68
+ const roles = featureLayerCounts.get(file.featureName) ?? new Set();
69
+ roles.add(role);
70
+ featureLayerCounts.set(file.featureName, roles);
71
+ }
72
+
73
+ for (const [featureName, roles] of featureLayerCounts) {
74
+ if (roles.size >= 4) {
75
+ issues.push(
76
+ createIssue(
77
+ contract,
78
+ "simplicity-audit",
79
+ `src/${featureName}`,
80
+ "feature appears to have accumulated many role layers; review whether the design can be simplified",
81
+ {
82
+ verificationMode: "audit",
83
+ confidence: "medium",
84
+ evidence: [...roles].sort((left, right) => left.localeCompare(right)).join(", "),
85
+ },
86
+ ),
87
+ );
88
+ }
89
+ }
90
+
91
+ return issues;
92
+ }
93
+
94
+ function verifyNoSpeculativeAbstractions(contract, analysisContext) {
95
+ const issues = [];
96
+
97
+ for (const file of analysisContext.sourceFiles.filter((candidate) => candidate.relativePath.startsWith("src/"))) {
98
+ const wrapperSignals = countWrapperSignals(file.ast);
99
+ if (wrapperSignals >= 6) {
100
+ issues.push(
101
+ createIssue(
102
+ contract,
103
+ "no-speculative-abstractions",
104
+ file.relativePath,
105
+ "file appears to contain multiple abstraction signals without proof of current need",
106
+ {
107
+ verificationMode: "heuristic",
108
+ confidence: "medium",
109
+ evidence: `${wrapperSignals} wrapper signal(s)`,
110
+ },
111
+ ),
112
+ );
113
+ }
114
+ }
115
+
116
+ return issues;
117
+ }
118
+
119
+ function countWrapperSignals(ast) {
120
+ let count = 0;
121
+
122
+ function visit(node) {
123
+ if (ts.isTypeAliasDeclaration(node) && /Options$/.test(node.name.text)) {
124
+ count += 1;
125
+ }
126
+
127
+ if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name) && /^(create|build|make)/.test(node.name.text)) {
128
+ count += 1;
129
+ }
130
+
131
+ if (ts.isClassDeclaration(node) && node.name && /Factory|Builder|Wrapper/.test(node.name.text)) {
132
+ count += 2;
133
+ }
134
+
135
+ ts.forEachChild(node, visit);
136
+ }
137
+
138
+ visit(ast);
139
+ return count;
140
+ }
@@ -0,0 +1,36 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ function runGit(args, cwd) {
4
+ return spawnSync("git", args, { cwd, encoding: "utf8" });
5
+ }
6
+
7
+ function parseChangedFiles(raw) {
8
+ return raw
9
+ .split("\n")
10
+ .map((line) => line.trim())
11
+ .filter((line) => line.length > 0);
12
+ }
13
+
14
+ export function readChangedFiles(targetPath, options = {}) {
15
+ const hasGit = runGit(["rev-parse", "--is-inside-work-tree"], targetPath);
16
+ if (hasGit.status !== 0) {
17
+ return { available: false, files: [], reason: "git repository not available" };
18
+ }
19
+
20
+ if (options.allFiles) {
21
+ return { available: true, files: [], reason: "all-files" };
22
+ }
23
+
24
+ const args = options.staged
25
+ ? ["diff", "--name-only", "--cached", options.changedAgainst ?? "HEAD"]
26
+ : options.changedAgainst
27
+ ? ["diff", "--name-only", options.changedAgainst]
28
+ : ["diff", "--name-only", "HEAD"];
29
+ const result = runGit(args, targetPath);
30
+
31
+ if (result.status !== 0) {
32
+ return { available: false, files: [], reason: result.stderr.trim() || "unable to compute git diff" };
33
+ }
34
+
35
+ return { available: true, files: parseChangedFiles(result.stdout), reason: options.staged ? "staged" : (options.changedAgainst ?? "HEAD") };
36
+ }