@shahmarasy/prodo 0.1.4 → 0.1.5

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 (173) hide show
  1. package/README.md +201 -97
  2. package/bin/prodo.cjs +6 -6
  3. package/dist/agents/agent-registry.d.ts +13 -0
  4. package/dist/agents/agent-registry.js +79 -0
  5. package/dist/agents/anthropic/index.d.ts +9 -0
  6. package/dist/agents/anthropic/index.js +55 -0
  7. package/dist/agents/base.d.ts +25 -0
  8. package/dist/agents/base.js +71 -0
  9. package/dist/agents/google/index.d.ts +9 -0
  10. package/dist/agents/google/index.js +53 -0
  11. package/dist/agents/mock/index.d.ts +11 -0
  12. package/dist/agents/mock/index.js +26 -0
  13. package/dist/agents/openai/index.d.ts +9 -0
  14. package/dist/agents/openai/index.js +57 -0
  15. package/dist/agents/system-prompts.d.ts +3 -0
  16. package/dist/agents/system-prompts.js +32 -0
  17. package/dist/cli/agent-command-installer.d.ts +4 -0
  18. package/dist/cli/agent-command-installer.js +148 -0
  19. package/dist/cli/agent-ids.d.ts +15 -0
  20. package/dist/cli/agent-ids.js +49 -0
  21. package/dist/cli/doctor.d.ts +1 -0
  22. package/dist/cli/doctor.js +144 -0
  23. package/dist/cli/fix-tui.d.ts +4 -0
  24. package/dist/cli/fix-tui.js +79 -0
  25. package/dist/cli/index.d.ts +9 -0
  26. package/dist/cli/index.js +465 -0
  27. package/dist/cli/init-tui.d.ts +23 -0
  28. package/dist/cli/init-tui.js +176 -0
  29. package/dist/cli/init.d.ts +11 -0
  30. package/dist/cli/init.js +334 -0
  31. package/dist/cli/normalize-interactive.d.ts +8 -0
  32. package/dist/cli/normalize-interactive.js +167 -0
  33. package/dist/cli/preset-loader.d.ts +4 -0
  34. package/dist/cli/preset-loader.js +210 -0
  35. package/dist/core/artifact-registry.d.ts +11 -0
  36. package/dist/core/artifact-registry.js +49 -0
  37. package/dist/core/artifacts.d.ts +10 -0
  38. package/dist/core/artifacts.js +892 -0
  39. package/dist/core/clean.d.ts +10 -0
  40. package/dist/core/clean.js +74 -0
  41. package/dist/core/consistency.d.ts +8 -0
  42. package/dist/core/consistency.js +328 -0
  43. package/dist/core/constants.d.ts +7 -0
  44. package/dist/core/constants.js +64 -0
  45. package/dist/core/errors.d.ts +3 -0
  46. package/dist/core/errors.js +10 -0
  47. package/dist/core/fix.d.ts +31 -0
  48. package/dist/core/fix.js +188 -0
  49. package/dist/core/hook-executor.d.ts +1 -0
  50. package/dist/core/hook-executor.js +175 -0
  51. package/dist/core/markdown.d.ts +16 -0
  52. package/dist/core/markdown.js +81 -0
  53. package/dist/core/normalize.d.ts +8 -0
  54. package/dist/core/normalize.js +125 -0
  55. package/dist/core/normalized-brief.d.ts +48 -0
  56. package/dist/core/normalized-brief.js +182 -0
  57. package/dist/core/output-index.d.ts +13 -0
  58. package/dist/core/output-index.js +55 -0
  59. package/dist/core/paths.d.ts +17 -0
  60. package/dist/core/paths.js +80 -0
  61. package/dist/core/project-config.d.ts +14 -0
  62. package/dist/core/project-config.js +69 -0
  63. package/dist/core/registry.d.ts +13 -0
  64. package/dist/core/registry.js +115 -0
  65. package/dist/core/settings.d.ts +7 -0
  66. package/dist/core/settings.js +35 -0
  67. package/dist/core/template-engine.d.ts +3 -0
  68. package/dist/core/template-engine.js +43 -0
  69. package/dist/core/template-resolver.d.ts +15 -0
  70. package/dist/core/template-resolver.js +46 -0
  71. package/dist/core/templates.d.ts +33 -0
  72. package/dist/core/templates.js +440 -0
  73. package/dist/core/terminology.d.ts +21 -0
  74. package/dist/core/terminology.js +143 -0
  75. package/dist/core/tracing.d.ts +21 -0
  76. package/dist/core/tracing.js +74 -0
  77. package/dist/core/types.d.ts +35 -0
  78. package/dist/core/types.js +5 -0
  79. package/dist/core/utils.d.ts +7 -0
  80. package/dist/core/utils.js +66 -0
  81. package/dist/core/validate.d.ts +10 -0
  82. package/dist/core/validate.js +226 -0
  83. package/dist/core/validator.d.ts +5 -0
  84. package/dist/core/validator.js +76 -0
  85. package/dist/core/version.d.ts +1 -0
  86. package/dist/core/version.js +30 -0
  87. package/dist/core/workflow-commands.d.ts +7 -0
  88. package/dist/core/workflow-commands.js +29 -0
  89. package/dist/i18n/en.json +45 -0
  90. package/dist/i18n/index.d.ts +5 -0
  91. package/dist/i18n/index.js +63 -0
  92. package/dist/i18n/tr.json +45 -0
  93. package/dist/providers/index.d.ts +2 -1
  94. package/dist/providers/index.js +20 -6
  95. package/dist/providers/mock-provider.d.ts +1 -1
  96. package/dist/providers/mock-provider.js +7 -6
  97. package/dist/providers/openai-provider.d.ts +1 -1
  98. package/dist/providers/openai-provider.js +1 -1
  99. package/dist/skills/engine.d.ts +10 -0
  100. package/dist/skills/engine.js +75 -0
  101. package/dist/skills/fix-skill.d.ts +2 -0
  102. package/dist/skills/fix-skill.js +38 -0
  103. package/dist/skills/generate-artifact-skill.d.ts +2 -0
  104. package/dist/skills/generate-artifact-skill.js +32 -0
  105. package/dist/skills/generate-pipeline-skill.d.ts +2 -0
  106. package/dist/skills/generate-pipeline-skill.js +45 -0
  107. package/dist/skills/normalize-skill.d.ts +2 -0
  108. package/dist/skills/normalize-skill.js +29 -0
  109. package/dist/skills/types.d.ts +28 -0
  110. package/dist/skills/types.js +2 -0
  111. package/dist/skills/validate-skill.d.ts +2 -0
  112. package/dist/skills/validate-skill.js +29 -0
  113. package/package.json +74 -45
  114. package/src/agents/agent-registry.ts +93 -0
  115. package/src/agents/anthropic/index.ts +86 -0
  116. package/src/agents/anthropic/manifest.json +7 -0
  117. package/src/agents/base.ts +77 -0
  118. package/src/agents/google/index.ts +79 -0
  119. package/src/agents/google/manifest.json +7 -0
  120. package/src/agents/mock/index.ts +32 -0
  121. package/src/agents/mock/manifest.json +7 -0
  122. package/src/agents/openai/index.ts +83 -0
  123. package/src/agents/openai/manifest.json +7 -0
  124. package/src/agents/system-prompts.ts +35 -0
  125. package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
  126. package/src/{agents.ts → cli/agent-ids.ts} +58 -58
  127. package/src/{doctor.ts → cli/doctor.ts} +157 -137
  128. package/src/cli/fix-tui.ts +111 -0
  129. package/src/{cli.ts → cli/index.ts} +459 -410
  130. package/src/{init-tui.ts → cli/init-tui.ts} +208 -208
  131. package/src/{init.ts → cli/init.ts} +398 -398
  132. package/src/cli/normalize-interactive.ts +241 -0
  133. package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
  134. package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
  135. package/src/{artifacts.ts → core/artifacts.ts} +1081 -1072
  136. package/src/core/clean.ts +88 -0
  137. package/src/{consistency.ts → core/consistency.ts} +374 -303
  138. package/src/{constants.ts → core/constants.ts} +72 -72
  139. package/src/{errors.ts → core/errors.ts} +7 -7
  140. package/src/core/fix.ts +253 -0
  141. package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
  142. package/src/{markdown.ts → core/markdown.ts} +93 -73
  143. package/src/{normalize.ts → core/normalize.ts} +145 -137
  144. package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
  145. package/src/{output-index.ts → core/output-index.ts} +59 -59
  146. package/src/{paths.ts → core/paths.ts} +75 -71
  147. package/src/{project-config.ts → core/project-config.ts} +78 -78
  148. package/src/{registry.ts → core/registry.ts} +119 -119
  149. package/src/{settings.ts → core/settings.ts} +35 -35
  150. package/src/core/template-engine.ts +45 -0
  151. package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
  152. package/src/{templates.ts → core/templates.ts} +452 -452
  153. package/src/core/terminology.ts +177 -0
  154. package/src/core/tracing.ts +110 -0
  155. package/src/{types.ts → core/types.ts} +46 -46
  156. package/src/{utils.ts → core/utils.ts} +64 -64
  157. package/src/{validate.ts → core/validate.ts} +252 -246
  158. package/src/{validator.ts → core/validator.ts} +92 -92
  159. package/src/{version.ts → core/version.ts} +24 -24
  160. package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -32
  161. package/src/i18n/en.json +45 -0
  162. package/src/i18n/index.ts +58 -0
  163. package/src/i18n/tr.json +45 -0
  164. package/src/providers/index.ts +29 -12
  165. package/src/providers/mock-provider.ts +200 -199
  166. package/src/providers/openai-provider.ts +88 -88
  167. package/src/skills/engine.ts +94 -0
  168. package/src/skills/fix-skill.ts +38 -0
  169. package/src/skills/generate-artifact-skill.ts +32 -0
  170. package/src/skills/generate-pipeline-skill.ts +49 -0
  171. package/src/skills/normalize-skill.ts +29 -0
  172. package/src/skills/types.ts +36 -0
  173. package/src/skills/validate-skill.ts +29 -0
@@ -1,196 +1,196 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { spawn } from "node:child_process";
4
- import yaml from "js-yaml";
5
- import { UserError } from "./errors";
6
- import { fileExists } from "./utils";
7
-
8
- type HookItem = {
9
- command?: string;
10
- optional?: boolean;
11
- enabled?: boolean;
12
- description?: string;
13
- prompt?: string;
14
- extension?: string;
15
- condition?: string;
16
- timeout_ms?: number;
17
- retry?: number;
18
- retry_delay_ms?: number;
19
- };
20
-
21
- type HooksConfig = {
22
- hooks?: Record<string, HookItem[]>;
23
- };
24
-
25
- function hooksPath(cwd: string): string {
26
- return path.join(cwd, ".prodo", "hooks.yml");
27
- }
28
-
29
- async function runShellCommand(
30
- command: string,
31
- cwd: string,
32
- timeoutMs: number
33
- ): Promise<{ code: number; stdout: string; stderr: string; timedOut: boolean }> {
34
- const parsed = parseCommand(command);
35
- if (!parsed) {
36
- return { code: 1, stdout: "", stderr: "Invalid hook command syntax.", timedOut: false };
37
- }
38
- return new Promise((resolve) => {
39
- const child = spawn(parsed.bin, parsed.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
40
- let stdout = "";
41
- let stderr = "";
42
- let timedOut = false;
43
- const timer = setTimeout(() => {
44
- timedOut = true;
45
- child.kill();
46
- }, Math.max(1000, timeoutMs));
47
-
48
- child.stdout.on("data", (chunk) => {
49
- stdout += chunk.toString();
50
- });
51
- child.stderr.on("data", (chunk) => {
52
- stderr += chunk.toString();
53
- });
54
- child.on("close", (code) => {
55
- clearTimeout(timer);
56
- resolve({ code: code ?? 1, stdout, stderr, timedOut });
57
- });
58
- });
59
- }
60
-
61
- function parseCommand(command: string): { bin: string; args: string[] } | null {
62
- const src = command.trim();
63
- if (!src) return null;
64
- const out: string[] = [];
65
- let current = "";
66
- let quote: '"' | "'" | null = null;
67
- let escaping = false;
68
- for (let i = 0; i < src.length; i += 1) {
69
- const ch = src[i];
70
- if (escaping) {
71
- current += ch;
72
- escaping = false;
73
- continue;
74
- }
75
- if (ch === "\\") {
76
- escaping = true;
77
- continue;
78
- }
79
- if (quote) {
80
- if (ch === quote) {
81
- quote = null;
82
- } else {
83
- current += ch;
84
- }
85
- continue;
86
- }
87
- if (ch === "'" || ch === '"') {
88
- quote = ch;
89
- continue;
90
- }
91
- if (/\s/.test(ch)) {
92
- if (current.length > 0) {
93
- out.push(current);
94
- current = "";
95
- }
96
- continue;
97
- }
98
- current += ch;
99
- }
100
- if (escaping || quote) return null;
101
- if (current.length > 0) out.push(current);
102
- if (out.length === 0) return null;
103
- return { bin: out[0], args: out.slice(1) };
104
- }
105
-
106
- function toPositiveInt(value: unknown, fallback: number): number {
107
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
108
- const normalized = Math.floor(value);
109
- return normalized > 0 ? normalized : fallback;
110
- }
111
-
112
- function toNonNegativeInt(value: unknown, fallback: number): number {
113
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
114
- const normalized = Math.floor(value);
115
- return normalized >= 0 ? normalized : fallback;
116
- }
117
-
118
- async function sleep(ms: number): Promise<void> {
119
- await new Promise((resolve) => setTimeout(resolve, ms));
120
- }
121
-
122
- async function evaluateCondition(condition: string, cwd: string): Promise<boolean> {
123
- const trimmed = condition.trim();
124
- if (!trimmed) return true;
125
- const result = await runShellCommand(trimmed, cwd, 10_000);
126
- return !result.timedOut && result.code === 0;
127
- }
128
-
129
- async function readHooks(cwd: string): Promise<HooksConfig | null> {
130
- const file = hooksPath(cwd);
131
- if (!(await fileExists(file))) return null;
132
- try {
133
- const raw = await fs.readFile(file, "utf8");
134
- const parsed = yaml.load(raw) as HooksConfig;
135
- return parsed ?? null;
136
- } catch {
137
- return null;
138
- }
139
- }
140
-
141
- export async function runHookPhase(
142
- cwd: string,
143
- phaseKey: string,
144
- log: (message: string) => void
145
- ): Promise<void> {
146
- const config = await readHooks(cwd);
147
- const phaseHooks = config?.hooks?.[phaseKey];
148
- if (!Array.isArray(phaseHooks) || phaseHooks.length === 0) return;
149
-
150
- for (const hook of phaseHooks) {
151
- if (hook?.enabled === false) continue;
152
- const command = typeof hook?.command === "string" ? hook.command.trim() : "";
153
- if (!command) continue;
154
- if (typeof hook.condition === "string" && hook.condition.trim()) {
155
- const pass = await evaluateCondition(hook.condition, cwd);
156
- if (!pass) {
157
- log(`[Hook:skipped:${phaseKey}] condition=false for ${command}`);
158
- continue;
159
- }
160
- }
161
-
162
- const label = hook.extension || hook.description || command;
163
- if (hook.optional) {
164
- log(`[Hook:optional:${phaseKey}] ${label}`);
165
- if (hook.prompt) log(` Prompt: ${hook.prompt}`);
166
- log(` To run manually: ${command}`);
167
- continue;
168
- }
169
-
170
- const timeoutMs = toPositiveInt(hook.timeout_ms, 30_000);
171
- const retries = toNonNegativeInt(hook.retry, 0);
172
- const retryDelayMs = toNonNegativeInt(hook.retry_delay_ms, 500);
173
- const attempts = 1 + retries;
174
-
175
- let lastDetail = "";
176
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
177
- log(`[Hook:mandatory:${phaseKey}] Running (attempt ${attempt}/${attempts}): ${command}`);
178
- const result = await runShellCommand(command, cwd, timeoutMs);
179
- if (!result.timedOut && result.code === 0) {
180
- lastDetail = "";
181
- break;
182
- }
183
-
184
- const stderr = result.stderr.trim();
185
- const stdout = result.stdout.trim();
186
- lastDetail = result.timedOut ? `Timed out after ${timeoutMs}ms` : stderr || stdout || "unknown error";
187
- if (attempt < attempts) {
188
- await sleep(retryDelayMs);
189
- }
190
- }
191
-
192
- if (lastDetail) {
193
- throw new UserError(`Mandatory hook failed (${phaseKey}): ${command}\n${lastDetail}`);
194
- }
195
- }
196
- }
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import yaml from "js-yaml";
5
+ import { UserError } from "./errors";
6
+ import { fileExists } from "./utils";
7
+
8
+ type HookItem = {
9
+ command?: string;
10
+ optional?: boolean;
11
+ enabled?: boolean;
12
+ description?: string;
13
+ prompt?: string;
14
+ extension?: string;
15
+ condition?: string;
16
+ timeout_ms?: number;
17
+ retry?: number;
18
+ retry_delay_ms?: number;
19
+ };
20
+
21
+ type HooksConfig = {
22
+ hooks?: Record<string, HookItem[]>;
23
+ };
24
+
25
+ function hooksPath(cwd: string): string {
26
+ return path.join(cwd, ".prodo", "hooks.yml");
27
+ }
28
+
29
+ async function runShellCommand(
30
+ command: string,
31
+ cwd: string,
32
+ timeoutMs: number
33
+ ): Promise<{ code: number; stdout: string; stderr: string; timedOut: boolean }> {
34
+ const parsed = parseCommand(command);
35
+ if (!parsed) {
36
+ return { code: 1, stdout: "", stderr: "Invalid hook command syntax.", timedOut: false };
37
+ }
38
+ return new Promise((resolve) => {
39
+ const child = spawn(parsed.bin, parsed.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
40
+ let stdout = "";
41
+ let stderr = "";
42
+ let timedOut = false;
43
+ const timer = setTimeout(() => {
44
+ timedOut = true;
45
+ child.kill();
46
+ }, Math.max(1000, timeoutMs));
47
+
48
+ child.stdout.on("data", (chunk) => {
49
+ stdout += chunk.toString();
50
+ });
51
+ child.stderr.on("data", (chunk) => {
52
+ stderr += chunk.toString();
53
+ });
54
+ child.on("close", (code) => {
55
+ clearTimeout(timer);
56
+ resolve({ code: code ?? 1, stdout, stderr, timedOut });
57
+ });
58
+ });
59
+ }
60
+
61
+ function parseCommand(command: string): { bin: string; args: string[] } | null {
62
+ const src = command.trim();
63
+ if (!src) return null;
64
+ const out: string[] = [];
65
+ let current = "";
66
+ let quote: '"' | "'" | null = null;
67
+ let escaping = false;
68
+ for (let i = 0; i < src.length; i += 1) {
69
+ const ch = src[i];
70
+ if (escaping) {
71
+ current += ch;
72
+ escaping = false;
73
+ continue;
74
+ }
75
+ if (ch === "\\") {
76
+ escaping = true;
77
+ continue;
78
+ }
79
+ if (quote) {
80
+ if (ch === quote) {
81
+ quote = null;
82
+ } else {
83
+ current += ch;
84
+ }
85
+ continue;
86
+ }
87
+ if (ch === "'" || ch === '"') {
88
+ quote = ch;
89
+ continue;
90
+ }
91
+ if (/\s/.test(ch)) {
92
+ if (current.length > 0) {
93
+ out.push(current);
94
+ current = "";
95
+ }
96
+ continue;
97
+ }
98
+ current += ch;
99
+ }
100
+ if (escaping || quote) return null;
101
+ if (current.length > 0) out.push(current);
102
+ if (out.length === 0) return null;
103
+ return { bin: out[0], args: out.slice(1) };
104
+ }
105
+
106
+ function toPositiveInt(value: unknown, fallback: number): number {
107
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
108
+ const normalized = Math.floor(value);
109
+ return normalized > 0 ? normalized : fallback;
110
+ }
111
+
112
+ function toNonNegativeInt(value: unknown, fallback: number): number {
113
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
114
+ const normalized = Math.floor(value);
115
+ return normalized >= 0 ? normalized : fallback;
116
+ }
117
+
118
+ async function sleep(ms: number): Promise<void> {
119
+ await new Promise((resolve) => setTimeout(resolve, ms));
120
+ }
121
+
122
+ async function evaluateCondition(condition: string, cwd: string): Promise<boolean> {
123
+ const trimmed = condition.trim();
124
+ if (!trimmed) return true;
125
+ const result = await runShellCommand(trimmed, cwd, 10_000);
126
+ return !result.timedOut && result.code === 0;
127
+ }
128
+
129
+ async function readHooks(cwd: string): Promise<HooksConfig | null> {
130
+ const file = hooksPath(cwd);
131
+ if (!(await fileExists(file))) return null;
132
+ try {
133
+ const raw = await fs.readFile(file, "utf8");
134
+ const parsed = yaml.load(raw) as HooksConfig;
135
+ return parsed ?? null;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ export async function runHookPhase(
142
+ cwd: string,
143
+ phaseKey: string,
144
+ log: (message: string) => void
145
+ ): Promise<void> {
146
+ const config = await readHooks(cwd);
147
+ const phaseHooks = config?.hooks?.[phaseKey];
148
+ if (!Array.isArray(phaseHooks) || phaseHooks.length === 0) return;
149
+
150
+ for (const hook of phaseHooks) {
151
+ if (hook?.enabled === false) continue;
152
+ const command = typeof hook?.command === "string" ? hook.command.trim() : "";
153
+ if (!command) continue;
154
+ if (typeof hook.condition === "string" && hook.condition.trim()) {
155
+ const pass = await evaluateCondition(hook.condition, cwd);
156
+ if (!pass) {
157
+ log(`[Hook:skipped:${phaseKey}] condition=false for ${command}`);
158
+ continue;
159
+ }
160
+ }
161
+
162
+ const label = hook.extension || hook.description || command;
163
+ if (hook.optional) {
164
+ log(`[Hook:optional:${phaseKey}] ${label}`);
165
+ if (hook.prompt) log(` Prompt: ${hook.prompt}`);
166
+ log(` To run manually: ${command}`);
167
+ continue;
168
+ }
169
+
170
+ const timeoutMs = toPositiveInt(hook.timeout_ms, 30_000);
171
+ const retries = toNonNegativeInt(hook.retry, 0);
172
+ const retryDelayMs = toNonNegativeInt(hook.retry_delay_ms, 500);
173
+ const attempts = 1 + retries;
174
+
175
+ let lastDetail = "";
176
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
177
+ log(`[Hook:mandatory:${phaseKey}] Running (attempt ${attempt}/${attempts}): ${command}`);
178
+ const result = await runShellCommand(command, cwd, timeoutMs);
179
+ if (!result.timedOut && result.code === 0) {
180
+ lastDetail = "";
181
+ break;
182
+ }
183
+
184
+ const stderr = result.stderr.trim();
185
+ const stdout = result.stdout.trim();
186
+ lastDetail = result.timedOut ? `Timed out after ${timeoutMs}ms` : stderr || stdout || "unknown error";
187
+ if (attempt < attempts) {
188
+ await sleep(retryDelayMs);
189
+ }
190
+ }
191
+
192
+ if (lastDetail) {
193
+ throw new UserError(`Mandatory hook failed (${phaseKey}): ${command}\n${lastDetail}`);
194
+ }
195
+ }
196
+ }
@@ -1,73 +1,93 @@
1
- export type MarkdownSection = {
2
- heading: string;
3
- headingKey: string;
4
- level: number;
5
- textLines: string[];
6
- listItems: string[];
7
- };
8
-
9
- function normalizeText(input: string): string {
10
- return input.trim().replace(/\s+/g, " ");
11
- }
12
-
13
- export function normalizeHeadingKey(heading: string): string {
14
- return normalizeText(heading)
15
- .toLowerCase()
16
- .replace(/[^a-z0-9\s]/g, " ")
17
- .replace(/\s+/g, " ")
18
- .trim();
19
- }
20
-
21
- export function parseMarkdownSections(markdown: string): MarkdownSection[] {
22
- const sections: MarkdownSection[] = [];
23
- let current: MarkdownSection | null = null;
24
-
25
- for (const rawLine of markdown.split(/\r?\n/)) {
26
- const headingMatch = rawLine.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
27
- if (headingMatch) {
28
- const title = normalizeText(headingMatch[2]);
29
- current = {
30
- heading: title,
31
- headingKey: normalizeHeadingKey(title),
32
- level: headingMatch[1].length,
33
- textLines: [],
34
- listItems: []
35
- };
36
- sections.push(current);
37
- continue;
38
- }
39
-
40
- if (!current) continue;
41
- const listMatch = rawLine.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
42
- if (listMatch) {
43
- const item = normalizeText(listMatch[1]);
44
- if (item.length > 0 && !current.listItems.includes(item)) current.listItems.push(item);
45
- if (item.length > 0) current.textLines.push(item);
46
- continue;
47
- }
48
-
49
- const text = normalizeText(rawLine);
50
- if (text.length > 0) current.textLines.push(text);
51
- }
52
-
53
- return sections;
54
- }
55
-
56
- export function extractRequiredHeadings(content: string): string[] {
57
- const sections = parseMarkdownSections(content);
58
- return sections
59
- .filter((section) => section.level === 2)
60
- .map((section) => `## ${section.heading}`)
61
- .filter((heading) => heading.length > 3);
62
- }
63
-
64
- export function sectionTextMap(content: string): Map<string, string> {
65
- const sections = parseMarkdownSections(content);
66
- const mapped = new Map<string, string>();
67
- for (const section of sections) {
68
- const parts = [...section.listItems, ...section.textLines].filter((item) => item.length > 0);
69
- mapped.set(`## ${section.heading}`, parts.join("\n").trim());
70
- }
71
- return mapped;
72
- }
73
-
1
+ export type MarkdownSection = {
2
+ heading: string;
3
+ headingKey: string;
4
+ level: number;
5
+ textLines: string[];
6
+ listItems: string[];
7
+ };
8
+
9
+ function normalizeText(input: string): string {
10
+ return input.trim().replace(/\s+/g, " ");
11
+ }
12
+
13
+ export function normalizeHeadingKey(heading: string): string {
14
+ return normalizeText(heading)
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9\s]/g, " ")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+ }
20
+
21
+ export function parseMarkdownSections(markdown: string): MarkdownSection[] {
22
+ const sections: MarkdownSection[] = [];
23
+ let current: MarkdownSection | null = null;
24
+
25
+ for (const rawLine of markdown.split(/\r?\n/)) {
26
+ const headingMatch = rawLine.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
27
+ if (headingMatch) {
28
+ const title = normalizeText(headingMatch[2]);
29
+ current = {
30
+ heading: title,
31
+ headingKey: normalizeHeadingKey(title),
32
+ level: headingMatch[1].length,
33
+ textLines: [],
34
+ listItems: []
35
+ };
36
+ sections.push(current);
37
+ continue;
38
+ }
39
+
40
+ if (!current) continue;
41
+ const listMatch = rawLine.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
42
+ if (listMatch) {
43
+ const item = normalizeText(listMatch[1]);
44
+ if (item.length > 0 && !current.listItems.includes(item)) current.listItems.push(item);
45
+ if (item.length > 0) current.textLines.push(item);
46
+ continue;
47
+ }
48
+
49
+ const text = normalizeText(rawLine);
50
+ if (text.length > 0) current.textLines.push(text);
51
+ }
52
+
53
+ return sections;
54
+ }
55
+
56
+ export function extractRequiredHeadings(content: string): string[] {
57
+ const sections = parseMarkdownSections(content);
58
+ return sections
59
+ .filter((section) => section.level === 2)
60
+ .map((section) => `## ${section.heading}`)
61
+ .filter((heading) => heading.length > 3);
62
+ }
63
+
64
+ export type TaggedLine = {
65
+ contractId: string;
66
+ line: string;
67
+ };
68
+
69
+ export function taggedLinesByContract(body: string): TaggedLine[] {
70
+ const lines = body
71
+ .split(/\r?\n/)
72
+ .map((line) => line.trim())
73
+ .filter((line) => line.length > 0);
74
+ const tagged: TaggedLine[] = [];
75
+ for (const line of lines) {
76
+ const matches = line.match(/\[([GFC][0-9]+)\]/g) ?? [];
77
+ for (const match of matches) {
78
+ tagged.push({ contractId: match.slice(1, -1), line });
79
+ }
80
+ }
81
+ return tagged;
82
+ }
83
+
84
+ export function sectionTextMap(content: string): Map<string, string> {
85
+ const sections = parseMarkdownSections(content);
86
+ const mapped = new Map<string, string>();
87
+ for (const section of sections) {
88
+ const parts = [...section.listItems, ...section.textLines].filter((item) => item.length > 0);
89
+ mapped.set(`## ${section.heading}`, parts.join("\n").trim());
90
+ }
91
+ return mapped;
92
+ }
93
+