mulch-cli 0.4.3 → 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.
- package/README.md +24 -4
- package/package.json +11 -16
- package/src/api.ts +310 -0
- package/src/cli.ts +54 -0
- package/src/commands/add.ts +61 -0
- package/src/commands/compact.ts +924 -0
- package/src/commands/delete.ts +103 -0
- package/src/commands/diff.ts +209 -0
- package/src/commands/doctor.ts +586 -0
- package/src/commands/edit.ts +253 -0
- package/src/commands/init.ts +33 -0
- package/src/commands/learn.ts +170 -0
- package/src/commands/onboard.ts +362 -0
- package/src/commands/prime.ts +327 -0
- package/src/commands/prune.ts +128 -0
- package/src/commands/query.ts +177 -0
- package/src/commands/ready.ts +194 -0
- package/src/commands/record.ts +959 -0
- package/src/commands/search.ts +234 -0
- package/src/commands/setup.ts +823 -0
- package/src/commands/status.ts +83 -0
- package/src/commands/sync.ts +224 -0
- package/src/commands/update.ts +112 -0
- package/src/commands/validate.ts +107 -0
- package/src/index.ts +50 -0
- package/src/schemas/config.ts +31 -0
- package/src/schemas/index.ts +18 -0
- package/src/schemas/record-schema.ts +177 -0
- package/src/schemas/record.ts +83 -0
- package/src/utils/bm25.ts +243 -0
- package/src/utils/budget.ts +157 -0
- package/src/utils/config.ts +117 -0
- package/src/utils/expertise.ts +379 -0
- package/src/utils/format.ts +767 -0
- package/src/utils/git.ts +89 -0
- package/src/utils/index.ts +54 -0
- package/src/utils/json-output.ts +13 -0
- package/src/utils/lock.ts +82 -0
- package/src/utils/markers.ts +51 -0
- package/src/utils/scoring.ts +101 -0
- package/src/utils/version.ts +46 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -50
- package/dist/cli.js.map +0 -1
- package/dist/commands/add.d.ts +0 -3
- package/dist/commands/add.d.ts.map +0 -1
- package/dist/commands/add.js +0 -47
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/compact.d.ts +0 -5
- package/dist/commands/compact.d.ts.map +0 -1
- package/dist/commands/compact.js +0 -709
- package/dist/commands/compact.js.map +0 -1
- package/dist/commands/delete.d.ts +0 -3
- package/dist/commands/delete.d.ts.map +0 -1
- package/dist/commands/delete.js +0 -82
- package/dist/commands/delete.js.map +0 -1
- package/dist/commands/diff.d.ts +0 -11
- package/dist/commands/diff.d.ts.map +0 -1
- package/dist/commands/diff.js +0 -170
- package/dist/commands/diff.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -3
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -391
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/edit.d.ts +0 -3
- package/dist/commands/edit.d.ts.map +0 -1
- package/dist/commands/edit.js +0 -210
- package/dist/commands/edit.js.map +0 -1
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -30
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/learn.d.ts +0 -12
- package/dist/commands/learn.d.ts.map +0 -1
- package/dist/commands/learn.js +0 -130
- package/dist/commands/learn.js.map +0 -1
- package/dist/commands/onboard.d.ts +0 -10
- package/dist/commands/onboard.d.ts.map +0 -1
- package/dist/commands/onboard.js +0 -286
- package/dist/commands/onboard.js.map +0 -1
- package/dist/commands/prime.d.ts +0 -3
- package/dist/commands/prime.d.ts.map +0 -1
- package/dist/commands/prime.js +0 -242
- package/dist/commands/prime.js.map +0 -1
- package/dist/commands/prune.d.ts +0 -8
- package/dist/commands/prune.d.ts.map +0 -1
- package/dist/commands/prune.js +0 -90
- package/dist/commands/prune.js.map +0 -1
- package/dist/commands/query.d.ts +0 -3
- package/dist/commands/query.d.ts.map +0 -1
- package/dist/commands/query.js +0 -118
- package/dist/commands/query.js.map +0 -1
- package/dist/commands/ready.d.ts +0 -3
- package/dist/commands/ready.d.ts.map +0 -1
- package/dist/commands/ready.js +0 -160
- package/dist/commands/ready.js.map +0 -1
- package/dist/commands/record.d.ts +0 -13
- package/dist/commands/record.d.ts.map +0 -1
- package/dist/commands/record.js +0 -688
- package/dist/commands/record.js.map +0 -1
- package/dist/commands/search.d.ts +0 -3
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/search.js +0 -163
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -548
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/status.d.ts +0 -3
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js +0 -61
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/sync.d.ts +0 -3
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js +0 -176
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -72
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/validate.d.ts +0 -3
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js +0 -86
- package/dist/commands/validate.js.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +0 -1
- package/dist/schemas/config.d.ts +0 -17
- package/dist/schemas/config.d.ts.map +0 -1
- package/dist/schemas/config.js +0 -16
- package/dist/schemas/config.js.map +0 -1
- package/dist/schemas/index.d.ts +0 -5
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/index.js +0 -3
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/record-schema.d.ts +0 -379
- package/dist/schemas/record-schema.d.ts.map +0 -1
- package/dist/schemas/record-schema.js +0 -148
- package/dist/schemas/record-schema.js.map +0 -1
- package/dist/schemas/record.d.ts +0 -60
- package/dist/schemas/record.d.ts.map +0 -1
- package/dist/schemas/record.js +0 -2
- package/dist/schemas/record.js.map +0 -1
- package/dist/utils/bm25.d.ts +0 -39
- package/dist/utils/bm25.d.ts.map +0 -1
- package/dist/utils/bm25.js +0 -171
- package/dist/utils/bm25.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -35
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js +0 -114
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/config.d.ts +0 -12
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -89
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/expertise.d.ts +0 -57
- package/dist/utils/expertise.d.ts.map +0 -1
- package/dist/utils/expertise.js +0 -264
- package/dist/utils/expertise.js.map +0 -1
- package/dist/utils/format.d.ts +0 -31
- package/dist/utils/format.d.ts.map +0 -1
- package/dist/utils/format.js +0 -556
- package/dist/utils/format.js.map +0 -1
- package/dist/utils/git.d.ts +0 -6
- package/dist/utils/git.d.ts.map +0 -1
- package/dist/utils/git.js +0 -81
- package/dist/utils/git.js.map +0 -1
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -8
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/json-output.d.ts +0 -8
- package/dist/utils/json-output.d.ts.map +0 -1
- package/dist/utils/json-output.js +0 -7
- package/dist/utils/json-output.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -6
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js +0 -70
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/markers.d.ts +0 -22
- package/dist/utils/markers.d.ts.map +0 -1
- package/dist/utils/markers.js +0 -42
- package/dist/utils/markers.js.map +0 -1
- package/dist/utils/scoring.d.ts +0 -73
- package/dist/utils/scoring.d.ts.map +0 -1
- package/dist/utils/scoring.js +0 -80
- package/dist/utils/scoring.js.map +0 -1
- package/dist/utils/version.d.ts +0 -15
- package/dist/utils/version.d.ts.map +0 -1
- package/dist/utils/version.js +0 -48
- package/dist/utils/version.js.map +0 -1
package/dist/commands/record.js
DELETED
|
@@ -1,688 +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"]))
|
|
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 outcome;
|
|
390
|
-
if (options.outcomeStatus) {
|
|
391
|
-
outcome = { status: options.outcomeStatus };
|
|
392
|
-
if (options.outcomeDuration !== undefined) {
|
|
393
|
-
outcome.duration = parseFloat(options.outcomeDuration);
|
|
394
|
-
}
|
|
395
|
-
if (options.outcomeTestResults) {
|
|
396
|
-
outcome.test_results = options.outcomeTestResults;
|
|
397
|
-
}
|
|
398
|
-
if (options.outcomeAgent) {
|
|
399
|
-
outcome.agent = options.outcomeAgent;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
let record;
|
|
403
|
-
switch (recordType) {
|
|
404
|
-
case "convention": {
|
|
405
|
-
const conventionContent = content ?? options.description;
|
|
406
|
-
if (!conventionContent) {
|
|
407
|
-
if (jsonMode) {
|
|
408
|
-
outputJsonError("record", "Convention records require content (positional argument or --description).");
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
console.error(chalk.red("Error: convention records require content (positional argument or --description)."));
|
|
412
|
-
}
|
|
413
|
-
process.exitCode = 1;
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
record = {
|
|
417
|
-
type: "convention",
|
|
418
|
-
content: conventionContent,
|
|
419
|
-
classification,
|
|
420
|
-
recorded_at: recordedAt,
|
|
421
|
-
...(evidence && { evidence }),
|
|
422
|
-
...(tags && tags.length > 0 && { tags }),
|
|
423
|
-
...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
424
|
-
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
425
|
-
...(outcome && { outcome }),
|
|
426
|
-
};
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
case "pattern": {
|
|
430
|
-
const patternName = options.name;
|
|
431
|
-
const patternDesc = options.description ?? content;
|
|
432
|
-
if (!patternName || !patternDesc) {
|
|
433
|
-
if (jsonMode) {
|
|
434
|
-
outputJsonError("record", "Pattern records require --name and --description (or positional content).");
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
console.error(chalk.red("Error: pattern records require --name and --description (or positional content)."));
|
|
438
|
-
}
|
|
439
|
-
process.exitCode = 1;
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
record = {
|
|
443
|
-
type: "pattern",
|
|
444
|
-
name: patternName,
|
|
445
|
-
description: patternDesc,
|
|
446
|
-
classification,
|
|
447
|
-
recorded_at: recordedAt,
|
|
448
|
-
...(evidence && { evidence }),
|
|
449
|
-
...(typeof options.files === "string" && {
|
|
450
|
-
files: options.files.split(","),
|
|
451
|
-
}),
|
|
452
|
-
...(tags && tags.length > 0 && { tags }),
|
|
453
|
-
...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
454
|
-
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
455
|
-
...(outcome && { outcome }),
|
|
456
|
-
};
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
case "failure": {
|
|
460
|
-
const failureDesc = options.description;
|
|
461
|
-
const failureResolution = options.resolution;
|
|
462
|
-
if (!failureDesc || !failureResolution) {
|
|
463
|
-
if (jsonMode) {
|
|
464
|
-
outputJsonError("record", "Failure records require --description and --resolution.");
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
console.error(chalk.red("Error: failure records require --description and --resolution."));
|
|
468
|
-
}
|
|
469
|
-
process.exitCode = 1;
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
record = {
|
|
473
|
-
type: "failure",
|
|
474
|
-
description: failureDesc,
|
|
475
|
-
resolution: failureResolution,
|
|
476
|
-
classification,
|
|
477
|
-
recorded_at: recordedAt,
|
|
478
|
-
...(evidence && { evidence }),
|
|
479
|
-
...(tags && tags.length > 0 && { tags }),
|
|
480
|
-
...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
481
|
-
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
482
|
-
...(outcome && { outcome }),
|
|
483
|
-
};
|
|
484
|
-
break;
|
|
485
|
-
}
|
|
486
|
-
case "decision": {
|
|
487
|
-
const decisionTitle = options.title;
|
|
488
|
-
const decisionRationale = options.rationale;
|
|
489
|
-
if (!decisionTitle || !decisionRationale) {
|
|
490
|
-
if (jsonMode) {
|
|
491
|
-
outputJsonError("record", "Decision records require --title and --rationale.");
|
|
492
|
-
}
|
|
493
|
-
else {
|
|
494
|
-
console.error(chalk.red("Error: decision records require --title and --rationale."));
|
|
495
|
-
}
|
|
496
|
-
process.exitCode = 1;
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
record = {
|
|
500
|
-
type: "decision",
|
|
501
|
-
title: decisionTitle,
|
|
502
|
-
rationale: decisionRationale,
|
|
503
|
-
classification,
|
|
504
|
-
recorded_at: recordedAt,
|
|
505
|
-
...(evidence && { evidence }),
|
|
506
|
-
...(tags && tags.length > 0 && { tags }),
|
|
507
|
-
...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
508
|
-
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
509
|
-
...(outcome && { outcome }),
|
|
510
|
-
};
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
case "reference": {
|
|
514
|
-
const refName = options.name;
|
|
515
|
-
const refDesc = options.description ?? content;
|
|
516
|
-
if (!refName || !refDesc) {
|
|
517
|
-
if (jsonMode) {
|
|
518
|
-
outputJsonError("record", "Reference records require --name and --description (or positional content).");
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
console.error(chalk.red("Error: reference records require --name and --description (or positional content)."));
|
|
522
|
-
}
|
|
523
|
-
process.exitCode = 1;
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
record = {
|
|
527
|
-
type: "reference",
|
|
528
|
-
name: refName,
|
|
529
|
-
description: refDesc,
|
|
530
|
-
classification,
|
|
531
|
-
recorded_at: recordedAt,
|
|
532
|
-
...(evidence && { evidence }),
|
|
533
|
-
...(typeof options.files === "string" && {
|
|
534
|
-
files: options.files.split(","),
|
|
535
|
-
}),
|
|
536
|
-
...(tags && tags.length > 0 && { tags }),
|
|
537
|
-
...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
538
|
-
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
539
|
-
...(outcome && { outcome }),
|
|
540
|
-
};
|
|
541
|
-
break;
|
|
542
|
-
}
|
|
543
|
-
case "guide": {
|
|
544
|
-
const guideName = options.name;
|
|
545
|
-
const guideDesc = options.description ?? content;
|
|
546
|
-
if (!guideName || !guideDesc) {
|
|
547
|
-
if (jsonMode) {
|
|
548
|
-
outputJsonError("record", "Guide records require --name and --description (or positional content).");
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
console.error(chalk.red("Error: guide records require --name and --description (or positional content)."));
|
|
552
|
-
}
|
|
553
|
-
process.exitCode = 1;
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
record = {
|
|
557
|
-
type: "guide",
|
|
558
|
-
name: guideName,
|
|
559
|
-
description: guideDesc,
|
|
560
|
-
classification,
|
|
561
|
-
recorded_at: recordedAt,
|
|
562
|
-
...(evidence && { evidence }),
|
|
563
|
-
...(tags && tags.length > 0 && { tags }),
|
|
564
|
-
...(relatesTo && relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
565
|
-
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
566
|
-
...(outcome && { outcome }),
|
|
567
|
-
};
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// Validate against JSON schema
|
|
572
|
-
const ajv = new Ajv();
|
|
573
|
-
const validate = ajv.compile(recordSchema);
|
|
574
|
-
if (!validate(record)) {
|
|
575
|
-
const errors = (validate.errors ?? []).map((err) => `${err.instancePath} ${err.message}`);
|
|
576
|
-
if (jsonMode) {
|
|
577
|
-
outputJsonError("record", `Schema validation failed: ${errors.join("; ")}`);
|
|
578
|
-
}
|
|
579
|
-
else {
|
|
580
|
-
console.error(chalk.red("Error: record failed schema validation:"));
|
|
581
|
-
for (const err of validate.errors ?? []) {
|
|
582
|
-
console.error(chalk.red(` ${err.instancePath} ${err.message}`));
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
process.exitCode = 1;
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
const filePath = getExpertisePath(domain);
|
|
589
|
-
const dryRun = options.dryRun === true;
|
|
590
|
-
if (dryRun) {
|
|
591
|
-
// Dry-run: check for duplicates without writing
|
|
592
|
-
const existing = await readExpertiseFile(filePath);
|
|
593
|
-
const dup = findDuplicate(existing, record);
|
|
594
|
-
let action = "created";
|
|
595
|
-
if (dup && !options.force) {
|
|
596
|
-
const isNamed = record.type === "pattern" || record.type === "decision" ||
|
|
597
|
-
record.type === "reference" || record.type === "guide";
|
|
598
|
-
action = isNamed ? "updated" : "skipped";
|
|
599
|
-
}
|
|
600
|
-
if (jsonMode) {
|
|
601
|
-
outputJson({
|
|
602
|
-
success: true,
|
|
603
|
-
command: "record",
|
|
604
|
-
action: "dry-run",
|
|
605
|
-
wouldDo: action,
|
|
606
|
-
domain,
|
|
607
|
-
type: recordType,
|
|
608
|
-
record,
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
if (action === "created") {
|
|
613
|
-
console.log(chalk.green(`✓ Dry-run: Would create ${recordType} in ${domain}`));
|
|
614
|
-
}
|
|
615
|
-
else if (action === "updated") {
|
|
616
|
-
console.log(chalk.green(`✓ Dry-run: Would update existing ${recordType} in ${domain}`));
|
|
617
|
-
}
|
|
618
|
-
else {
|
|
619
|
-
console.log(chalk.yellow(`Dry-run: Duplicate ${recordType} already exists in ${domain}. Would skip.`));
|
|
620
|
-
}
|
|
621
|
-
console.log(chalk.dim(" Run without --dry-run to apply changes."));
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
// Normal mode: write with file locking
|
|
626
|
-
await withFileLock(filePath, async () => {
|
|
627
|
-
const existing = await readExpertiseFile(filePath);
|
|
628
|
-
const dup = findDuplicate(existing, record);
|
|
629
|
-
if (dup && !options.force) {
|
|
630
|
-
const isNamed = record.type === "pattern" || record.type === "decision" ||
|
|
631
|
-
record.type === "reference" || record.type === "guide";
|
|
632
|
-
if (isNamed) {
|
|
633
|
-
// Upsert: replace in place
|
|
634
|
-
existing[dup.index] = record;
|
|
635
|
-
await writeExpertiseFile(filePath, existing);
|
|
636
|
-
if (jsonMode) {
|
|
637
|
-
outputJson({
|
|
638
|
-
success: true,
|
|
639
|
-
command: "record",
|
|
640
|
-
action: "updated",
|
|
641
|
-
domain,
|
|
642
|
-
type: recordType,
|
|
643
|
-
index: dup.index + 1,
|
|
644
|
-
record,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
console.log(chalk.green(`\u2714 Updated existing ${recordType} in ${domain} (record #${dup.index + 1})`));
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
// Exact match: skip
|
|
653
|
-
if (jsonMode) {
|
|
654
|
-
outputJson({
|
|
655
|
-
success: true,
|
|
656
|
-
command: "record",
|
|
657
|
-
action: "skipped",
|
|
658
|
-
domain,
|
|
659
|
-
type: recordType,
|
|
660
|
-
index: dup.index + 1,
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
console.log(chalk.yellow(`Duplicate ${recordType} already exists in ${domain} (record #${dup.index + 1}). Use --force to add anyway.`));
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
await appendRecord(filePath, record);
|
|
670
|
-
if (jsonMode) {
|
|
671
|
-
outputJson({
|
|
672
|
-
success: true,
|
|
673
|
-
command: "record",
|
|
674
|
-
action: "created",
|
|
675
|
-
domain,
|
|
676
|
-
type: recordType,
|
|
677
|
-
record,
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
console.log(chalk.green(`\u2714 Recorded ${recordType} in ${domain}`));
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
//# sourceMappingURL=record.js.map
|