mulch-cli 0.5.0 → 0.6.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 (196) hide show
  1. package/README.md +12 -1
  2. package/package.json +11 -16
  3. package/src/api.ts +310 -0
  4. package/src/cli.ts +54 -0
  5. package/src/commands/add.ts +61 -0
  6. package/src/commands/compact.ts +924 -0
  7. package/src/commands/delete.ts +103 -0
  8. package/src/commands/diff.ts +209 -0
  9. package/src/commands/doctor.ts +586 -0
  10. package/src/commands/edit.ts +253 -0
  11. package/src/commands/init.ts +33 -0
  12. package/src/commands/learn.ts +170 -0
  13. package/src/commands/onboard.ts +362 -0
  14. package/src/commands/prime.ts +327 -0
  15. package/src/commands/prune.ts +128 -0
  16. package/src/commands/query.ts +177 -0
  17. package/src/commands/ready.ts +194 -0
  18. package/src/commands/record.ts +959 -0
  19. package/src/commands/search.ts +234 -0
  20. package/src/commands/setup.ts +823 -0
  21. package/src/commands/status.ts +83 -0
  22. package/src/commands/sync.ts +224 -0
  23. package/src/commands/update.ts +112 -0
  24. package/src/commands/validate.ts +107 -0
  25. package/src/index.ts +50 -0
  26. package/src/schemas/config.ts +31 -0
  27. package/src/schemas/index.ts +18 -0
  28. package/src/schemas/record-schema.ts +177 -0
  29. package/src/schemas/record.ts +83 -0
  30. package/src/utils/bm25.ts +243 -0
  31. package/src/utils/budget.ts +157 -0
  32. package/src/utils/config.ts +117 -0
  33. package/src/utils/expertise.ts +379 -0
  34. package/src/utils/format.ts +767 -0
  35. package/src/utils/git.ts +89 -0
  36. package/src/utils/index.ts +54 -0
  37. package/src/utils/json-output.ts +13 -0
  38. package/src/utils/lock.ts +82 -0
  39. package/src/utils/markers.ts +51 -0
  40. package/{dist/utils/scoring.d.ts → src/utils/scoring.ts} +53 -9
  41. package/src/utils/version.ts +46 -0
  42. package/dist/api.d.ts +0 -65
  43. package/dist/api.d.ts.map +0 -1
  44. package/dist/api.js +0 -196
  45. package/dist/api.js.map +0 -1
  46. package/dist/cli.d.ts +0 -3
  47. package/dist/cli.d.ts.map +0 -1
  48. package/dist/cli.js +0 -50
  49. package/dist/cli.js.map +0 -1
  50. package/dist/commands/add.d.ts +0 -3
  51. package/dist/commands/add.d.ts.map +0 -1
  52. package/dist/commands/add.js +0 -47
  53. package/dist/commands/add.js.map +0 -1
  54. package/dist/commands/compact.d.ts +0 -5
  55. package/dist/commands/compact.d.ts.map +0 -1
  56. package/dist/commands/compact.js +0 -709
  57. package/dist/commands/compact.js.map +0 -1
  58. package/dist/commands/delete.d.ts +0 -3
  59. package/dist/commands/delete.d.ts.map +0 -1
  60. package/dist/commands/delete.js +0 -82
  61. package/dist/commands/delete.js.map +0 -1
  62. package/dist/commands/diff.d.ts +0 -11
  63. package/dist/commands/diff.d.ts.map +0 -1
  64. package/dist/commands/diff.js +0 -170
  65. package/dist/commands/diff.js.map +0 -1
  66. package/dist/commands/doctor.d.ts +0 -3
  67. package/dist/commands/doctor.d.ts.map +0 -1
  68. package/dist/commands/doctor.js +0 -391
  69. package/dist/commands/doctor.js.map +0 -1
  70. package/dist/commands/edit.d.ts +0 -3
  71. package/dist/commands/edit.d.ts.map +0 -1
  72. package/dist/commands/edit.js +0 -198
  73. package/dist/commands/edit.js.map +0 -1
  74. package/dist/commands/init.d.ts +0 -3
  75. package/dist/commands/init.d.ts.map +0 -1
  76. package/dist/commands/init.js +0 -30
  77. package/dist/commands/init.js.map +0 -1
  78. package/dist/commands/learn.d.ts +0 -12
  79. package/dist/commands/learn.d.ts.map +0 -1
  80. package/dist/commands/learn.js +0 -130
  81. package/dist/commands/learn.js.map +0 -1
  82. package/dist/commands/onboard.d.ts +0 -10
  83. package/dist/commands/onboard.d.ts.map +0 -1
  84. package/dist/commands/onboard.js +0 -286
  85. package/dist/commands/onboard.js.map +0 -1
  86. package/dist/commands/prime.d.ts +0 -3
  87. package/dist/commands/prime.d.ts.map +0 -1
  88. package/dist/commands/prime.js +0 -242
  89. package/dist/commands/prime.js.map +0 -1
  90. package/dist/commands/prune.d.ts +0 -8
  91. package/dist/commands/prune.d.ts.map +0 -1
  92. package/dist/commands/prune.js +0 -90
  93. package/dist/commands/prune.js.map +0 -1
  94. package/dist/commands/query.d.ts +0 -3
  95. package/dist/commands/query.d.ts.map +0 -1
  96. package/dist/commands/query.js +0 -133
  97. package/dist/commands/query.js.map +0 -1
  98. package/dist/commands/ready.d.ts +0 -3
  99. package/dist/commands/ready.d.ts.map +0 -1
  100. package/dist/commands/ready.js +0 -160
  101. package/dist/commands/ready.js.map +0 -1
  102. package/dist/commands/record.d.ts +0 -13
  103. package/dist/commands/record.d.ts.map +0 -1
  104. package/dist/commands/record.js +0 -689
  105. package/dist/commands/record.js.map +0 -1
  106. package/dist/commands/search.d.ts +0 -3
  107. package/dist/commands/search.d.ts.map +0 -1
  108. package/dist/commands/search.js +0 -163
  109. package/dist/commands/search.js.map +0 -1
  110. package/dist/commands/setup.d.ts +0 -29
  111. package/dist/commands/setup.d.ts.map +0 -1
  112. package/dist/commands/setup.js +0 -548
  113. package/dist/commands/setup.js.map +0 -1
  114. package/dist/commands/status.d.ts +0 -3
  115. package/dist/commands/status.d.ts.map +0 -1
  116. package/dist/commands/status.js +0 -61
  117. package/dist/commands/status.js.map +0 -1
  118. package/dist/commands/sync.d.ts +0 -3
  119. package/dist/commands/sync.d.ts.map +0 -1
  120. package/dist/commands/sync.js +0 -176
  121. package/dist/commands/sync.js.map +0 -1
  122. package/dist/commands/update.d.ts +0 -3
  123. package/dist/commands/update.d.ts.map +0 -1
  124. package/dist/commands/update.js +0 -72
  125. package/dist/commands/update.js.map +0 -1
  126. package/dist/commands/validate.d.ts +0 -3
  127. package/dist/commands/validate.d.ts.map +0 -1
  128. package/dist/commands/validate.js +0 -86
  129. package/dist/commands/validate.js.map +0 -1
  130. package/dist/index.d.ts +0 -9
  131. package/dist/index.d.ts.map +0 -1
  132. package/dist/index.js +0 -10
  133. package/dist/index.js.map +0 -1
  134. package/dist/schemas/config.d.ts +0 -17
  135. package/dist/schemas/config.d.ts.map +0 -1
  136. package/dist/schemas/config.js +0 -16
  137. package/dist/schemas/config.js.map +0 -1
  138. package/dist/schemas/index.d.ts +0 -5
  139. package/dist/schemas/index.d.ts.map +0 -1
  140. package/dist/schemas/index.js +0 -3
  141. package/dist/schemas/index.js.map +0 -1
  142. package/dist/schemas/record-schema.d.ts +0 -403
  143. package/dist/schemas/record-schema.d.ts.map +0 -1
  144. package/dist/schemas/record-schema.js +0 -150
  145. package/dist/schemas/record-schema.js.map +0 -1
  146. package/dist/schemas/record.d.ts +0 -62
  147. package/dist/schemas/record.d.ts.map +0 -1
  148. package/dist/schemas/record.js +0 -2
  149. package/dist/schemas/record.js.map +0 -1
  150. package/dist/utils/bm25.d.ts +0 -39
  151. package/dist/utils/bm25.d.ts.map +0 -1
  152. package/dist/utils/bm25.js +0 -171
  153. package/dist/utils/bm25.js.map +0 -1
  154. package/dist/utils/budget.d.ts +0 -35
  155. package/dist/utils/budget.d.ts.map +0 -1
  156. package/dist/utils/budget.js +0 -114
  157. package/dist/utils/budget.js.map +0 -1
  158. package/dist/utils/config.d.ts +0 -12
  159. package/dist/utils/config.d.ts.map +0 -1
  160. package/dist/utils/config.js +0 -89
  161. package/dist/utils/config.js.map +0 -1
  162. package/dist/utils/expertise.d.ts +0 -57
  163. package/dist/utils/expertise.d.ts.map +0 -1
  164. package/dist/utils/expertise.js +0 -276
  165. package/dist/utils/expertise.js.map +0 -1
  166. package/dist/utils/format.d.ts +0 -31
  167. package/dist/utils/format.d.ts.map +0 -1
  168. package/dist/utils/format.js +0 -562
  169. package/dist/utils/format.js.map +0 -1
  170. package/dist/utils/git.d.ts +0 -6
  171. package/dist/utils/git.d.ts.map +0 -1
  172. package/dist/utils/git.js +0 -81
  173. package/dist/utils/git.js.map +0 -1
  174. package/dist/utils/index.d.ts +0 -8
  175. package/dist/utils/index.d.ts.map +0 -1
  176. package/dist/utils/index.js +0 -8
  177. package/dist/utils/index.js.map +0 -1
  178. package/dist/utils/json-output.d.ts +0 -8
  179. package/dist/utils/json-output.d.ts.map +0 -1
  180. package/dist/utils/json-output.js +0 -7
  181. package/dist/utils/json-output.js.map +0 -1
  182. package/dist/utils/lock.d.ts +0 -6
  183. package/dist/utils/lock.d.ts.map +0 -1
  184. package/dist/utils/lock.js +0 -70
  185. package/dist/utils/lock.js.map +0 -1
  186. package/dist/utils/markers.d.ts +0 -22
  187. package/dist/utils/markers.d.ts.map +0 -1
  188. package/dist/utils/markers.js +0 -42
  189. package/dist/utils/markers.js.map +0 -1
  190. package/dist/utils/scoring.d.ts.map +0 -1
  191. package/dist/utils/scoring.js +0 -80
  192. package/dist/utils/scoring.js.map +0 -1
  193. package/dist/utils/version.d.ts +0 -15
  194. package/dist/utils/version.d.ts.map +0 -1
  195. package/dist/utils/version.js +0 -48
  196. package/dist/utils/version.js.map +0 -1
@@ -1,689 +0,0 @@
1
- import { Option } from "commander";
2
- import _Ajv from "ajv";
3
- const Ajv = _Ajv.default ?? _Ajv;
4
- import chalk from "chalk";
5
- import { readConfig, getExpertisePath } from "../utils/config.js";
6
- import { appendRecord, readExpertiseFile, writeExpertiseFile, findDuplicate, } from "../utils/expertise.js";
7
- import { withFileLock } from "../utils/lock.js";
8
- import { recordSchema } from "../schemas/record-schema.js";
9
- import { outputJson, outputJsonError } from "../utils/json-output.js";
10
- import { readFileSync, existsSync } from "node:fs";
11
- /**
12
- * Process records from stdin (JSON single object or array)
13
- * Validates, dedups, and appends with file locking
14
- */
15
- export async function processStdinRecords(domain, jsonMode, force, dryRun, stdinData, cwd) {
16
- const config = await readConfig(cwd);
17
- if (!config.domains.includes(domain)) {
18
- throw new Error(`Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`);
19
- }
20
- // Read stdin (or use provided data for testing)
21
- const inputData = stdinData ?? readFileSync(0, "utf-8");
22
- let inputRecords;
23
- try {
24
- const parsed = JSON.parse(inputData);
25
- inputRecords = Array.isArray(parsed) ? parsed : [parsed];
26
- }
27
- catch (err) {
28
- throw new Error(`Failed to parse JSON from stdin: ${err instanceof Error ? err.message : String(err)}`);
29
- }
30
- // Validate each record against schema
31
- const ajv = new Ajv();
32
- const validate = ajv.compile(recordSchema);
33
- const errors = [];
34
- const validRecords = [];
35
- for (let i = 0; i < inputRecords.length; i++) {
36
- const record = inputRecords[i];
37
- // Ensure recorded_at and classification are set
38
- if (typeof record === "object" && record !== null) {
39
- if (!("recorded_at" in record)) {
40
- record.recorded_at = new Date().toISOString();
41
- }
42
- if (!("classification" in record)) {
43
- record.classification = "tactical";
44
- }
45
- }
46
- if (!validate(record)) {
47
- const validationErrors = (validate.errors ?? [])
48
- .map((err) => `${err.instancePath} ${err.message}`)
49
- .join("; ");
50
- errors.push(`Record ${i}: ${validationErrors}`);
51
- continue;
52
- }
53
- validRecords.push(record);
54
- }
55
- if (validRecords.length === 0) {
56
- return { created: 0, updated: 0, skipped: 0, errors };
57
- }
58
- // Process valid records with file locking (skip write in dry-run mode)
59
- const filePath = getExpertisePath(domain, cwd);
60
- let created = 0;
61
- let updated = 0;
62
- let skipped = 0;
63
- if (dryRun) {
64
- // Dry-run: check for duplicates without writing
65
- const existing = await readExpertiseFile(filePath);
66
- const currentRecords = [...existing];
67
- for (const record of validRecords) {
68
- const dup = findDuplicate(currentRecords, record);
69
- if (dup && !force) {
70
- const isNamed = record.type === "pattern" ||
71
- record.type === "decision" ||
72
- record.type === "reference" ||
73
- record.type === "guide";
74
- if (isNamed) {
75
- updated++;
76
- }
77
- else {
78
- skipped++;
79
- }
80
- }
81
- else {
82
- created++;
83
- }
84
- }
85
- }
86
- else {
87
- // Normal mode: write with file locking
88
- await withFileLock(filePath, async () => {
89
- const existing = await readExpertiseFile(filePath);
90
- let currentRecords = [...existing];
91
- for (const record of validRecords) {
92
- const dup = findDuplicate(currentRecords, record);
93
- if (dup && !force) {
94
- const isNamed = record.type === "pattern" ||
95
- record.type === "decision" ||
96
- record.type === "reference" ||
97
- record.type === "guide";
98
- if (isNamed) {
99
- // Upsert: replace in place
100
- currentRecords[dup.index] = record;
101
- updated++;
102
- }
103
- else {
104
- // Exact match: skip
105
- skipped++;
106
- }
107
- }
108
- else {
109
- // New record: append
110
- currentRecords.push(record);
111
- created++;
112
- }
113
- }
114
- // Write all changes at once
115
- if (created > 0 || updated > 0) {
116
- await writeExpertiseFile(filePath, currentRecords);
117
- }
118
- });
119
- }
120
- return { created, updated, skipped, errors };
121
- }
122
- export function registerRecordCommand(program) {
123
- program
124
- .command("record")
125
- .argument("<domain>", "expertise domain")
126
- .argument("[content]", "record content")
127
- .description("Record an expertise record")
128
- .addOption(new Option("--type <type>", "record type")
129
- .choices(["convention", "pattern", "failure", "decision", "reference", "guide"]))
130
- .addOption(new Option("--classification <classification>", "classification level")
131
- .choices(["foundational", "tactical", "observational"])
132
- .default("tactical"))
133
- .option("--name <name>", "name of the convention or pattern")
134
- .option("--description <description>", "description of the record")
135
- .option("--resolution <resolution>", "resolution for failure records")
136
- .option("--title <title>", "title for decision records")
137
- .option("--rationale <rationale>", "rationale for decision records")
138
- .option("--files <files>", "related files (comma-separated)")
139
- .option("--tags <tags>", "comma-separated tags")
140
- .option("--evidence-commit <commit>", "evidence: commit hash")
141
- .option("--evidence-issue <issue>", "evidence: issue reference")
142
- .option("--evidence-file <file>", "evidence: file path")
143
- .option("--evidence-bead <bead>", "evidence: bead ID")
144
- .option("--relates-to <ids>", "comma-separated record IDs this relates to")
145
- .option("--supersedes <ids>", "comma-separated record IDs this supersedes")
146
- .addOption(new Option("--outcome-status <status>", "outcome status").choices(["success", "failure", "partial"]))
147
- .option("--outcome-duration <ms>", "outcome duration in milliseconds")
148
- .option("--outcome-test-results <text>", "outcome test results summary")
149
- .option("--outcome-agent <agent>", "outcome agent name")
150
- .option("--force", "force recording even if duplicate exists")
151
- .option("--stdin", "read JSON record(s) from stdin (single object or array)")
152
- .option("--batch <file>", "read JSON record(s) from file (single object or array)")
153
- .option("--dry-run", "preview what would be recorded without writing")
154
- .addHelpText("after", `
155
- Required fields per record type:
156
- convention [content] or --description
157
- pattern --name, --description (or [content])
158
- failure --description, --resolution
159
- decision --title, --rationale
160
- reference --name, --description (or [content])
161
- guide --name, --description (or [content])
162
-
163
- Batch recording examples:
164
- mulch record cli --batch records.json
165
- mulch record cli --batch records.json --dry-run
166
- echo '[{"type":"convention","content":"test"}]' > batch.json && mulch record cli --batch batch.json
167
- `)
168
- .action(async (domain, content, options) => {
169
- const jsonMode = program.opts().json === true;
170
- // Handle --batch mode
171
- if (options.batch) {
172
- const batchFile = options.batch;
173
- const dryRun = options.dryRun === true;
174
- if (!existsSync(batchFile)) {
175
- if (jsonMode) {
176
- outputJsonError("record", `Batch file not found: ${batchFile}`);
177
- }
178
- else {
179
- console.error(chalk.red(`Error: batch file not found: ${batchFile}`));
180
- }
181
- process.exitCode = 1;
182
- return;
183
- }
184
- try {
185
- const fileContent = readFileSync(batchFile, "utf-8");
186
- const result = await processStdinRecords(domain, jsonMode, options.force === true, dryRun, fileContent);
187
- if (result.errors.length > 0) {
188
- if (jsonMode) {
189
- outputJsonError("record", `Validation errors: ${result.errors.join("; ")}`);
190
- }
191
- else {
192
- console.error(chalk.red("Validation errors:"));
193
- for (const error of result.errors) {
194
- console.error(chalk.red(` ${error}`));
195
- }
196
- }
197
- }
198
- if (jsonMode) {
199
- outputJson({
200
- success: result.errors.length === 0 || result.created + result.updated > 0,
201
- command: "record",
202
- action: dryRun ? "dry-run" : "batch",
203
- domain,
204
- created: result.created,
205
- updated: result.updated,
206
- skipped: result.skipped,
207
- errors: result.errors,
208
- });
209
- }
210
- else {
211
- if (dryRun) {
212
- const total = result.created + result.updated;
213
- if (total > 0 || result.skipped > 0) {
214
- console.log(chalk.green(`✓ Dry-run complete. Would process ${total} record(s) in ${domain}:`));
215
- if (result.created > 0) {
216
- console.log(chalk.dim(` Create: ${result.created}`));
217
- }
218
- if (result.updated > 0) {
219
- console.log(chalk.dim(` Update: ${result.updated}`));
220
- }
221
- if (result.skipped > 0) {
222
- console.log(chalk.dim(` Skip: ${result.skipped}`));
223
- }
224
- console.log(chalk.dim(" Run without --dry-run to apply changes."));
225
- }
226
- else {
227
- console.log(chalk.yellow("No records would be processed."));
228
- }
229
- }
230
- else {
231
- if (result.created > 0) {
232
- console.log(chalk.green(`✔ Created ${result.created} record(s) in ${domain}`));
233
- }
234
- if (result.updated > 0) {
235
- console.log(chalk.green(`✔ Updated ${result.updated} record(s) in ${domain}`));
236
- }
237
- if (result.skipped > 0) {
238
- console.log(chalk.yellow(`Skipped ${result.skipped} duplicate(s) in ${domain}`));
239
- }
240
- }
241
- }
242
- if (result.errors.length > 0 && result.created + result.updated === 0) {
243
- process.exitCode = 1;
244
- }
245
- }
246
- catch (err) {
247
- if (jsonMode) {
248
- outputJsonError("record", err instanceof Error ? err.message : String(err));
249
- }
250
- else {
251
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
252
- }
253
- process.exitCode = 1;
254
- }
255
- return;
256
- }
257
- // Handle --stdin mode
258
- if (options.stdin === true) {
259
- const dryRun = options.dryRun === true;
260
- try {
261
- const result = await processStdinRecords(domain, jsonMode, options.force === true, dryRun);
262
- if (result.errors.length > 0) {
263
- if (jsonMode) {
264
- outputJsonError("record", `Validation errors: ${result.errors.join("; ")}`);
265
- }
266
- else {
267
- console.error(chalk.red("Validation errors:"));
268
- for (const error of result.errors) {
269
- console.error(chalk.red(` ${error}`));
270
- }
271
- }
272
- }
273
- if (jsonMode) {
274
- outputJson({
275
- success: result.errors.length === 0 || result.created + result.updated > 0,
276
- command: "record",
277
- action: dryRun ? "dry-run" : "stdin",
278
- domain,
279
- created: result.created,
280
- updated: result.updated,
281
- skipped: result.skipped,
282
- errors: result.errors,
283
- });
284
- }
285
- else {
286
- if (dryRun) {
287
- const total = result.created + result.updated;
288
- if (total > 0 || result.skipped > 0) {
289
- console.log(chalk.green(`✓ Dry-run complete. Would process ${total} record(s) in ${domain}:`));
290
- if (result.created > 0) {
291
- console.log(chalk.dim(` Create: ${result.created}`));
292
- }
293
- if (result.updated > 0) {
294
- console.log(chalk.dim(` Update: ${result.updated}`));
295
- }
296
- if (result.skipped > 0) {
297
- console.log(chalk.dim(` Skip: ${result.skipped}`));
298
- }
299
- console.log(chalk.dim(" Run without --dry-run to apply changes."));
300
- }
301
- else {
302
- console.log(chalk.yellow("No records would be processed."));
303
- }
304
- }
305
- else {
306
- if (result.created > 0) {
307
- console.log(chalk.green(`✔ Created ${result.created} record(s) in ${domain}`));
308
- }
309
- if (result.updated > 0) {
310
- console.log(chalk.green(`✔ Updated ${result.updated} record(s) in ${domain}`));
311
- }
312
- if (result.skipped > 0) {
313
- console.log(chalk.yellow(`Skipped ${result.skipped} duplicate(s) in ${domain}`));
314
- }
315
- }
316
- }
317
- if (result.errors.length > 0 && result.created + result.updated === 0) {
318
- process.exitCode = 1;
319
- }
320
- }
321
- catch (err) {
322
- if (jsonMode) {
323
- outputJsonError("record", err instanceof Error ? err.message : String(err));
324
- }
325
- else {
326
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
327
- }
328
- process.exitCode = 1;
329
- }
330
- return;
331
- }
332
- const config = await readConfig();
333
- if (!config.domains.includes(domain)) {
334
- if (jsonMode) {
335
- outputJsonError("record", `Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`);
336
- }
337
- else {
338
- console.error(chalk.red(`Error: domain "${domain}" not found in config.`));
339
- console.error(chalk.red(`Available domains: ${config.domains.join(", ") || "(none)"}`));
340
- }
341
- process.exitCode = 1;
342
- return;
343
- }
344
- // Validate --type is provided for non-stdin mode
345
- if (!options.type) {
346
- if (jsonMode) {
347
- outputJsonError("record", "--type is required (convention, pattern, failure, decision, reference, guide)");
348
- }
349
- else {
350
- console.error(chalk.red("Error: --type is required (convention, pattern, failure, decision, reference, guide)"));
351
- }
352
- process.exitCode = 1;
353
- return;
354
- }
355
- const recordType = options.type;
356
- const classification = options.classification ?? "tactical";
357
- const recordedAt = new Date().toISOString();
358
- // Build evidence if any evidence option is provided
359
- let evidence;
360
- if (options.evidenceCommit || options.evidenceIssue || options.evidenceFile || options.evidenceBead) {
361
- evidence = {};
362
- if (options.evidenceCommit)
363
- evidence.commit = options.evidenceCommit;
364
- if (options.evidenceIssue)
365
- evidence.issue = options.evidenceIssue;
366
- if (options.evidenceFile)
367
- evidence.file = options.evidenceFile;
368
- if (options.evidenceBead)
369
- evidence.bead = options.evidenceBead;
370
- }
371
- const tags = typeof options.tags === "string"
372
- ? options.tags
373
- .split(",")
374
- .map((t) => t.trim())
375
- .filter(Boolean)
376
- : undefined;
377
- const relatesTo = typeof options.relatesTo === "string"
378
- ? options.relatesTo
379
- .split(",")
380
- .map((id) => id.trim())
381
- .filter(Boolean)
382
- : undefined;
383
- const supersedes = typeof options.supersedes === "string"
384
- ? options.supersedes
385
- .split(",")
386
- .map((id) => id.trim())
387
- .filter(Boolean)
388
- : undefined;
389
- let outcomes;
390
- if (options.outcomeStatus) {
391
- const o = { status: options.outcomeStatus };
392
- if (options.outcomeDuration !== undefined) {
393
- o.duration = parseFloat(options.outcomeDuration);
394
- }
395
- if (options.outcomeTestResults) {
396
- o.test_results = options.outcomeTestResults;
397
- }
398
- if (options.outcomeAgent) {
399
- o.agent = options.outcomeAgent;
400
- }
401
- outcomes = [o];
402
- }
403
- let record;
404
- switch (recordType) {
405
- case "convention": {
406
- const conventionContent = content ?? options.description;
407
- if (!conventionContent) {
408
- if (jsonMode) {
409
- outputJsonError("record", "Convention records require content (positional argument or --description).");
410
- }
411
- else {
412
- console.error(chalk.red("Error: convention records require content (positional argument or --description)."));
413
- }
414
- process.exitCode = 1;
415
- return;
416
- }
417
- record = {
418
- type: "convention",
419
- content: conventionContent,
420
- classification,
421
- recorded_at: recordedAt,
422
- ...(evidence && { evidence }),
423
- ...(tags && tags.length > 0 && { tags }),
424
- ...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
425
- ...(supersedes && supersedes.length > 0 && { supersedes }),
426
- ...(outcomes && { outcomes }),
427
- };
428
- break;
429
- }
430
- case "pattern": {
431
- const patternName = options.name;
432
- const patternDesc = options.description ?? content;
433
- if (!patternName || !patternDesc) {
434
- if (jsonMode) {
435
- outputJsonError("record", "Pattern records require --name and --description (or positional content).");
436
- }
437
- else {
438
- console.error(chalk.red("Error: pattern records require --name and --description (or positional content)."));
439
- }
440
- process.exitCode = 1;
441
- return;
442
- }
443
- record = {
444
- type: "pattern",
445
- name: patternName,
446
- description: patternDesc,
447
- classification,
448
- recorded_at: recordedAt,
449
- ...(evidence && { evidence }),
450
- ...(typeof options.files === "string" && {
451
- files: options.files.split(","),
452
- }),
453
- ...(tags && tags.length > 0 && { tags }),
454
- ...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
455
- ...(supersedes && supersedes.length > 0 && { supersedes }),
456
- ...(outcomes && { outcomes }),
457
- };
458
- break;
459
- }
460
- case "failure": {
461
- const failureDesc = options.description;
462
- const failureResolution = options.resolution;
463
- if (!failureDesc || !failureResolution) {
464
- if (jsonMode) {
465
- outputJsonError("record", "Failure records require --description and --resolution.");
466
- }
467
- else {
468
- console.error(chalk.red("Error: failure records require --description and --resolution."));
469
- }
470
- process.exitCode = 1;
471
- return;
472
- }
473
- record = {
474
- type: "failure",
475
- description: failureDesc,
476
- resolution: failureResolution,
477
- classification,
478
- recorded_at: recordedAt,
479
- ...(evidence && { evidence }),
480
- ...(tags && tags.length > 0 && { tags }),
481
- ...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
482
- ...(supersedes && supersedes.length > 0 && { supersedes }),
483
- ...(outcomes && { outcomes }),
484
- };
485
- break;
486
- }
487
- case "decision": {
488
- const decisionTitle = options.title;
489
- const decisionRationale = options.rationale;
490
- if (!decisionTitle || !decisionRationale) {
491
- if (jsonMode) {
492
- outputJsonError("record", "Decision records require --title and --rationale.");
493
- }
494
- else {
495
- console.error(chalk.red("Error: decision records require --title and --rationale."));
496
- }
497
- process.exitCode = 1;
498
- return;
499
- }
500
- record = {
501
- type: "decision",
502
- title: decisionTitle,
503
- rationale: decisionRationale,
504
- classification,
505
- recorded_at: recordedAt,
506
- ...(evidence && { evidence }),
507
- ...(tags && tags.length > 0 && { tags }),
508
- ...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
509
- ...(supersedes && supersedes.length > 0 && { supersedes }),
510
- ...(outcomes && { outcomes }),
511
- };
512
- break;
513
- }
514
- case "reference": {
515
- const refName = options.name;
516
- const refDesc = options.description ?? content;
517
- if (!refName || !refDesc) {
518
- if (jsonMode) {
519
- outputJsonError("record", "Reference records require --name and --description (or positional content).");
520
- }
521
- else {
522
- console.error(chalk.red("Error: reference records require --name and --description (or positional content)."));
523
- }
524
- process.exitCode = 1;
525
- return;
526
- }
527
- record = {
528
- type: "reference",
529
- name: refName,
530
- description: refDesc,
531
- classification,
532
- recorded_at: recordedAt,
533
- ...(evidence && { evidence }),
534
- ...(typeof options.files === "string" && {
535
- files: options.files.split(","),
536
- }),
537
- ...(tags && tags.length > 0 && { tags }),
538
- ...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
539
- ...(supersedes && supersedes.length > 0 && { supersedes }),
540
- ...(outcomes && { outcomes }),
541
- };
542
- break;
543
- }
544
- case "guide": {
545
- const guideName = options.name;
546
- const guideDesc = options.description ?? content;
547
- if (!guideName || !guideDesc) {
548
- if (jsonMode) {
549
- outputJsonError("record", "Guide records require --name and --description (or positional content).");
550
- }
551
- else {
552
- console.error(chalk.red("Error: guide records require --name and --description (or positional content)."));
553
- }
554
- process.exitCode = 1;
555
- return;
556
- }
557
- record = {
558
- type: "guide",
559
- name: guideName,
560
- description: guideDesc,
561
- classification,
562
- recorded_at: recordedAt,
563
- ...(evidence && { evidence }),
564
- ...(tags && tags.length > 0 && { tags }),
565
- ...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
566
- ...(supersedes && supersedes.length > 0 && { supersedes }),
567
- ...(outcomes && { outcomes }),
568
- };
569
- break;
570
- }
571
- }
572
- // Validate against JSON schema
573
- const ajv = new Ajv();
574
- const validate = ajv.compile(recordSchema);
575
- if (!validate(record)) {
576
- const errors = (validate.errors ?? []).map((err) => `${err.instancePath} ${err.message}`);
577
- if (jsonMode) {
578
- outputJsonError("record", `Schema validation failed: ${errors.join("; ")}`);
579
- }
580
- else {
581
- console.error(chalk.red("Error: record failed schema validation:"));
582
- for (const err of validate.errors ?? []) {
583
- console.error(chalk.red(` ${err.instancePath} ${err.message}`));
584
- }
585
- }
586
- process.exitCode = 1;
587
- return;
588
- }
589
- const filePath = getExpertisePath(domain);
590
- const dryRun = options.dryRun === true;
591
- if (dryRun) {
592
- // Dry-run: check for duplicates without writing
593
- const existing = await readExpertiseFile(filePath);
594
- const dup = findDuplicate(existing, record);
595
- let action = "created";
596
- if (dup && !options.force) {
597
- const isNamed = record.type === "pattern" || record.type === "decision" ||
598
- record.type === "reference" || record.type === "guide";
599
- action = isNamed ? "updated" : "skipped";
600
- }
601
- if (jsonMode) {
602
- outputJson({
603
- success: true,
604
- command: "record",
605
- action: "dry-run",
606
- wouldDo: action,
607
- domain,
608
- type: recordType,
609
- record,
610
- });
611
- }
612
- else {
613
- if (action === "created") {
614
- console.log(chalk.green(`✓ Dry-run: Would create ${recordType} in ${domain}`));
615
- }
616
- else if (action === "updated") {
617
- console.log(chalk.green(`✓ Dry-run: Would update existing ${recordType} in ${domain}`));
618
- }
619
- else {
620
- console.log(chalk.yellow(`Dry-run: Duplicate ${recordType} already exists in ${domain}. Would skip.`));
621
- }
622
- console.log(chalk.dim(" Run without --dry-run to apply changes."));
623
- }
624
- }
625
- else {
626
- // Normal mode: write with file locking
627
- await withFileLock(filePath, async () => {
628
- const existing = await readExpertiseFile(filePath);
629
- const dup = findDuplicate(existing, record);
630
- if (dup && !options.force) {
631
- const isNamed = record.type === "pattern" || record.type === "decision" ||
632
- record.type === "reference" || record.type === "guide";
633
- if (isNamed) {
634
- // Upsert: replace in place
635
- existing[dup.index] = record;
636
- await writeExpertiseFile(filePath, existing);
637
- if (jsonMode) {
638
- outputJson({
639
- success: true,
640
- command: "record",
641
- action: "updated",
642
- domain,
643
- type: recordType,
644
- index: dup.index + 1,
645
- record,
646
- });
647
- }
648
- else {
649
- console.log(chalk.green(`\u2714 Updated existing ${recordType} in ${domain} (record #${dup.index + 1})`));
650
- }
651
- }
652
- else {
653
- // Exact match: skip
654
- if (jsonMode) {
655
- outputJson({
656
- success: true,
657
- command: "record",
658
- action: "skipped",
659
- domain,
660
- type: recordType,
661
- index: dup.index + 1,
662
- });
663
- }
664
- else {
665
- console.log(chalk.yellow(`Duplicate ${recordType} already exists in ${domain} (record #${dup.index + 1}). Use --force to add anyway.`));
666
- }
667
- }
668
- }
669
- else {
670
- await appendRecord(filePath, record);
671
- if (jsonMode) {
672
- outputJson({
673
- success: true,
674
- command: "record",
675
- action: "created",
676
- domain,
677
- type: recordType,
678
- record,
679
- });
680
- }
681
- else {
682
- console.log(chalk.green(`\u2714 Recorded ${recordType} in ${domain}`));
683
- }
684
- }
685
- });
686
- }
687
- });
688
- }
689
- //# sourceMappingURL=record.js.map