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.
- package/README.md +12 -1
- 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/{dist/utils/scoring.d.ts → src/utils/scoring.ts} +53 -9
- package/src/utils/version.ts +46 -0
- package/dist/api.d.ts +0 -65
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -196
- package/dist/api.js.map +0 -1
- 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 -198
- 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 -133
- 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 -689
- 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 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -10
- 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 -403
- package/dist/schemas/record-schema.d.ts.map +0 -1
- package/dist/schemas/record-schema.js +0 -150
- package/dist/schemas/record-schema.js.map +0 -1
- package/dist/schemas/record.d.ts +0 -62
- 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 -276
- 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 -562
- 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.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
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import Ajv from "ajv";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { type Command, Option } from "commander";
|
|
5
|
+
import { recordSchema } from "../schemas/record-schema.ts";
|
|
6
|
+
import type {
|
|
7
|
+
Classification,
|
|
8
|
+
Evidence,
|
|
9
|
+
ExpertiseRecord,
|
|
10
|
+
Outcome,
|
|
11
|
+
RecordType,
|
|
12
|
+
} from "../schemas/record.ts";
|
|
13
|
+
import { getExpertisePath, readConfig } from "../utils/config.ts";
|
|
14
|
+
import {
|
|
15
|
+
appendRecord,
|
|
16
|
+
findDuplicate,
|
|
17
|
+
readExpertiseFile,
|
|
18
|
+
writeExpertiseFile,
|
|
19
|
+
} from "../utils/expertise.ts";
|
|
20
|
+
import { outputJson, outputJsonError } from "../utils/json-output.ts";
|
|
21
|
+
import { withFileLock } from "../utils/lock.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Process records from stdin (JSON single object or array)
|
|
25
|
+
* Validates, dedups, and appends with file locking
|
|
26
|
+
*/
|
|
27
|
+
export async function processStdinRecords(
|
|
28
|
+
domain: string,
|
|
29
|
+
jsonMode: boolean,
|
|
30
|
+
force: boolean,
|
|
31
|
+
dryRun: boolean,
|
|
32
|
+
stdinData?: string,
|
|
33
|
+
cwd?: string,
|
|
34
|
+
): Promise<{
|
|
35
|
+
created: number;
|
|
36
|
+
updated: number;
|
|
37
|
+
skipped: number;
|
|
38
|
+
errors: string[];
|
|
39
|
+
}> {
|
|
40
|
+
const config = await readConfig(cwd);
|
|
41
|
+
|
|
42
|
+
if (!config.domains.includes(domain)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read stdin (or use provided data for testing)
|
|
49
|
+
const inputData = stdinData ?? readFileSync(0, "utf-8");
|
|
50
|
+
let inputRecords: unknown[];
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(inputData);
|
|
54
|
+
inputRecords = Array.isArray(parsed) ? parsed : [parsed];
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Failed to parse JSON from stdin: ${err instanceof Error ? err.message : String(err)}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate each record against schema
|
|
62
|
+
const ajv = new Ajv();
|
|
63
|
+
const validate = ajv.compile(recordSchema);
|
|
64
|
+
|
|
65
|
+
const errors: string[] = [];
|
|
66
|
+
const validRecords: ExpertiseRecord[] = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < inputRecords.length; i++) {
|
|
69
|
+
const record = inputRecords[i];
|
|
70
|
+
|
|
71
|
+
// Ensure recorded_at and classification are set
|
|
72
|
+
if (typeof record === "object" && record !== null) {
|
|
73
|
+
if (!("recorded_at" in record)) {
|
|
74
|
+
(record as Record<string, unknown>).recorded_at =
|
|
75
|
+
new Date().toISOString();
|
|
76
|
+
}
|
|
77
|
+
if (!("classification" in record)) {
|
|
78
|
+
(record as Record<string, unknown>).classification = "tactical";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!validate(record)) {
|
|
83
|
+
const validationErrors = (validate.errors ?? [])
|
|
84
|
+
.map((err) => `${err.instancePath} ${err.message}`)
|
|
85
|
+
.join("; ");
|
|
86
|
+
errors.push(`Record ${i}: ${validationErrors}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
validRecords.push(record as ExpertiseRecord);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (validRecords.length === 0) {
|
|
94
|
+
return { created: 0, updated: 0, skipped: 0, errors };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Process valid records with file locking (skip write in dry-run mode)
|
|
98
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
99
|
+
let created = 0;
|
|
100
|
+
let updated = 0;
|
|
101
|
+
let skipped = 0;
|
|
102
|
+
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
// Dry-run: check for duplicates without writing
|
|
105
|
+
const existing = await readExpertiseFile(filePath);
|
|
106
|
+
const currentRecords = [...existing];
|
|
107
|
+
|
|
108
|
+
for (const record of validRecords) {
|
|
109
|
+
const dup = findDuplicate(currentRecords, record);
|
|
110
|
+
|
|
111
|
+
if (dup && !force) {
|
|
112
|
+
const isNamed =
|
|
113
|
+
record.type === "pattern" ||
|
|
114
|
+
record.type === "decision" ||
|
|
115
|
+
record.type === "reference" ||
|
|
116
|
+
record.type === "guide";
|
|
117
|
+
|
|
118
|
+
if (isNamed) {
|
|
119
|
+
updated++;
|
|
120
|
+
} else {
|
|
121
|
+
skipped++;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
created++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Normal mode: write with file locking
|
|
129
|
+
await withFileLock(filePath, async () => {
|
|
130
|
+
const existing = await readExpertiseFile(filePath);
|
|
131
|
+
const currentRecords = [...existing];
|
|
132
|
+
|
|
133
|
+
for (const record of validRecords) {
|
|
134
|
+
const dup = findDuplicate(currentRecords, record);
|
|
135
|
+
|
|
136
|
+
if (dup && !force) {
|
|
137
|
+
const isNamed =
|
|
138
|
+
record.type === "pattern" ||
|
|
139
|
+
record.type === "decision" ||
|
|
140
|
+
record.type === "reference" ||
|
|
141
|
+
record.type === "guide";
|
|
142
|
+
|
|
143
|
+
if (isNamed) {
|
|
144
|
+
// Upsert: replace in place
|
|
145
|
+
currentRecords[dup.index] = record;
|
|
146
|
+
updated++;
|
|
147
|
+
} else {
|
|
148
|
+
// Exact match: skip
|
|
149
|
+
skipped++;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// New record: append
|
|
153
|
+
currentRecords.push(record);
|
|
154
|
+
created++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Write all changes at once
|
|
159
|
+
if (created > 0 || updated > 0) {
|
|
160
|
+
await writeExpertiseFile(filePath, currentRecords);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { created, updated, skipped, errors };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function registerRecordCommand(program: Command): void {
|
|
169
|
+
program
|
|
170
|
+
.command("record")
|
|
171
|
+
.argument("<domain>", "expertise domain")
|
|
172
|
+
.argument("[content]", "record content")
|
|
173
|
+
.description("Record an expertise record")
|
|
174
|
+
.addOption(
|
|
175
|
+
new Option("--type <type>", "record type").choices([
|
|
176
|
+
"convention",
|
|
177
|
+
"pattern",
|
|
178
|
+
"failure",
|
|
179
|
+
"decision",
|
|
180
|
+
"reference",
|
|
181
|
+
"guide",
|
|
182
|
+
]),
|
|
183
|
+
)
|
|
184
|
+
.addOption(
|
|
185
|
+
new Option("--classification <classification>", "classification level")
|
|
186
|
+
.choices(["foundational", "tactical", "observational"])
|
|
187
|
+
.default("tactical"),
|
|
188
|
+
)
|
|
189
|
+
.option("--name <name>", "name of the convention or pattern")
|
|
190
|
+
.option("--description <description>", "description of the record")
|
|
191
|
+
.option("--resolution <resolution>", "resolution for failure records")
|
|
192
|
+
.option("--title <title>", "title for decision records")
|
|
193
|
+
.option("--rationale <rationale>", "rationale for decision records")
|
|
194
|
+
.option("--files <files>", "related files (comma-separated)")
|
|
195
|
+
.option("--tags <tags>", "comma-separated tags")
|
|
196
|
+
.option("--evidence-commit <commit>", "evidence: commit hash")
|
|
197
|
+
.option("--evidence-issue <issue>", "evidence: issue reference")
|
|
198
|
+
.option("--evidence-file <file>", "evidence: file path")
|
|
199
|
+
.option("--evidence-bead <bead>", "evidence: bead ID")
|
|
200
|
+
.option("--relates-to <ids>", "comma-separated record IDs this relates to")
|
|
201
|
+
.option("--supersedes <ids>", "comma-separated record IDs this supersedes")
|
|
202
|
+
.addOption(
|
|
203
|
+
new Option("--outcome-status <status>", "outcome status").choices([
|
|
204
|
+
"success",
|
|
205
|
+
"failure",
|
|
206
|
+
"partial",
|
|
207
|
+
]),
|
|
208
|
+
)
|
|
209
|
+
.option("--outcome-duration <ms>", "outcome duration in milliseconds")
|
|
210
|
+
.option("--outcome-test-results <text>", "outcome test results summary")
|
|
211
|
+
.option("--outcome-agent <agent>", "outcome agent name")
|
|
212
|
+
.option("--force", "force recording even if duplicate exists")
|
|
213
|
+
.option(
|
|
214
|
+
"--stdin",
|
|
215
|
+
"read JSON record(s) from stdin (single object or array)",
|
|
216
|
+
)
|
|
217
|
+
.option(
|
|
218
|
+
"--batch <file>",
|
|
219
|
+
"read JSON record(s) from file (single object or array)",
|
|
220
|
+
)
|
|
221
|
+
.option("--dry-run", "preview what would be recorded without writing")
|
|
222
|
+
.addHelpText(
|
|
223
|
+
"after",
|
|
224
|
+
`
|
|
225
|
+
Required fields per record type:
|
|
226
|
+
convention [content] or --description
|
|
227
|
+
pattern --name, --description (or [content])
|
|
228
|
+
failure --description, --resolution
|
|
229
|
+
decision --title, --rationale
|
|
230
|
+
reference --name, --description (or [content])
|
|
231
|
+
guide --name, --description (or [content])
|
|
232
|
+
|
|
233
|
+
Batch recording examples:
|
|
234
|
+
mulch record cli --batch records.json
|
|
235
|
+
mulch record cli --batch records.json --dry-run
|
|
236
|
+
echo '[{"type":"convention","content":"test"}]' > batch.json && mulch record cli --batch batch.json
|
|
237
|
+
`,
|
|
238
|
+
)
|
|
239
|
+
.action(
|
|
240
|
+
async (
|
|
241
|
+
domain: string,
|
|
242
|
+
content: string | undefined,
|
|
243
|
+
options: Record<string, unknown>,
|
|
244
|
+
) => {
|
|
245
|
+
const jsonMode = program.opts().json === true;
|
|
246
|
+
|
|
247
|
+
// Handle --batch mode
|
|
248
|
+
if (options.batch) {
|
|
249
|
+
const batchFile = options.batch as string;
|
|
250
|
+
const dryRun = options.dryRun === true;
|
|
251
|
+
|
|
252
|
+
if (!existsSync(batchFile)) {
|
|
253
|
+
if (jsonMode) {
|
|
254
|
+
outputJsonError("record", `Batch file not found: ${batchFile}`);
|
|
255
|
+
} else {
|
|
256
|
+
console.error(
|
|
257
|
+
chalk.red(`Error: batch file not found: ${batchFile}`),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const fileContent = readFileSync(batchFile, "utf-8");
|
|
266
|
+
const result = await processStdinRecords(
|
|
267
|
+
domain,
|
|
268
|
+
jsonMode,
|
|
269
|
+
options.force === true,
|
|
270
|
+
dryRun,
|
|
271
|
+
fileContent,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (result.errors.length > 0) {
|
|
275
|
+
if (jsonMode) {
|
|
276
|
+
outputJsonError(
|
|
277
|
+
"record",
|
|
278
|
+
`Validation errors: ${result.errors.join("; ")}`,
|
|
279
|
+
);
|
|
280
|
+
} else {
|
|
281
|
+
console.error(chalk.red("Validation errors:"));
|
|
282
|
+
for (const error of result.errors) {
|
|
283
|
+
console.error(chalk.red(` ${error}`));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (jsonMode) {
|
|
289
|
+
outputJson({
|
|
290
|
+
success:
|
|
291
|
+
result.errors.length === 0 ||
|
|
292
|
+
result.created + result.updated > 0,
|
|
293
|
+
command: "record",
|
|
294
|
+
action: dryRun ? "dry-run" : "batch",
|
|
295
|
+
domain,
|
|
296
|
+
created: result.created,
|
|
297
|
+
updated: result.updated,
|
|
298
|
+
skipped: result.skipped,
|
|
299
|
+
errors: result.errors,
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
if (dryRun) {
|
|
303
|
+
const total = result.created + result.updated;
|
|
304
|
+
if (total > 0 || result.skipped > 0) {
|
|
305
|
+
console.log(
|
|
306
|
+
chalk.green(
|
|
307
|
+
`✓ Dry-run complete. Would process ${total} record(s) in ${domain}:`,
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
if (result.created > 0) {
|
|
311
|
+
console.log(chalk.dim(` Create: ${result.created}`));
|
|
312
|
+
}
|
|
313
|
+
if (result.updated > 0) {
|
|
314
|
+
console.log(chalk.dim(` Update: ${result.updated}`));
|
|
315
|
+
}
|
|
316
|
+
if (result.skipped > 0) {
|
|
317
|
+
console.log(chalk.dim(` Skip: ${result.skipped}`));
|
|
318
|
+
}
|
|
319
|
+
console.log(
|
|
320
|
+
chalk.dim(" Run without --dry-run to apply changes."),
|
|
321
|
+
);
|
|
322
|
+
} else {
|
|
323
|
+
console.log(chalk.yellow("No records would be processed."));
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
if (result.created > 0) {
|
|
327
|
+
console.log(
|
|
328
|
+
chalk.green(
|
|
329
|
+
`✔ Created ${result.created} record(s) in ${domain}`,
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (result.updated > 0) {
|
|
334
|
+
console.log(
|
|
335
|
+
chalk.green(
|
|
336
|
+
`✔ Updated ${result.updated} record(s) in ${domain}`,
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (result.skipped > 0) {
|
|
341
|
+
console.log(
|
|
342
|
+
chalk.yellow(
|
|
343
|
+
`Skipped ${result.skipped} duplicate(s) in ${domain}`,
|
|
344
|
+
),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (
|
|
351
|
+
result.errors.length > 0 &&
|
|
352
|
+
result.created + result.updated === 0
|
|
353
|
+
) {
|
|
354
|
+
process.exitCode = 1;
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
if (jsonMode) {
|
|
358
|
+
outputJsonError(
|
|
359
|
+
"record",
|
|
360
|
+
err instanceof Error ? err.message : String(err),
|
|
361
|
+
);
|
|
362
|
+
} else {
|
|
363
|
+
console.error(
|
|
364
|
+
chalk.red(
|
|
365
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
process.exitCode = 1;
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Handle --stdin mode
|
|
375
|
+
if (options.stdin === true) {
|
|
376
|
+
const dryRun = options.dryRun === true;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const result = await processStdinRecords(
|
|
380
|
+
domain,
|
|
381
|
+
jsonMode,
|
|
382
|
+
options.force === true,
|
|
383
|
+
dryRun,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (result.errors.length > 0) {
|
|
387
|
+
if (jsonMode) {
|
|
388
|
+
outputJsonError(
|
|
389
|
+
"record",
|
|
390
|
+
`Validation errors: ${result.errors.join("; ")}`,
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
console.error(chalk.red("Validation errors:"));
|
|
394
|
+
for (const error of result.errors) {
|
|
395
|
+
console.error(chalk.red(` ${error}`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (jsonMode) {
|
|
401
|
+
outputJson({
|
|
402
|
+
success:
|
|
403
|
+
result.errors.length === 0 ||
|
|
404
|
+
result.created + result.updated > 0,
|
|
405
|
+
command: "record",
|
|
406
|
+
action: dryRun ? "dry-run" : "stdin",
|
|
407
|
+
domain,
|
|
408
|
+
created: result.created,
|
|
409
|
+
updated: result.updated,
|
|
410
|
+
skipped: result.skipped,
|
|
411
|
+
errors: result.errors,
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
if (dryRun) {
|
|
415
|
+
const total = result.created + result.updated;
|
|
416
|
+
if (total > 0 || result.skipped > 0) {
|
|
417
|
+
console.log(
|
|
418
|
+
chalk.green(
|
|
419
|
+
`✓ Dry-run complete. Would process ${total} record(s) in ${domain}:`,
|
|
420
|
+
),
|
|
421
|
+
);
|
|
422
|
+
if (result.created > 0) {
|
|
423
|
+
console.log(chalk.dim(` Create: ${result.created}`));
|
|
424
|
+
}
|
|
425
|
+
if (result.updated > 0) {
|
|
426
|
+
console.log(chalk.dim(` Update: ${result.updated}`));
|
|
427
|
+
}
|
|
428
|
+
if (result.skipped > 0) {
|
|
429
|
+
console.log(chalk.dim(` Skip: ${result.skipped}`));
|
|
430
|
+
}
|
|
431
|
+
console.log(
|
|
432
|
+
chalk.dim(" Run without --dry-run to apply changes."),
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(chalk.yellow("No records would be processed."));
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
if (result.created > 0) {
|
|
439
|
+
console.log(
|
|
440
|
+
chalk.green(
|
|
441
|
+
`✔ Created ${result.created} record(s) in ${domain}`,
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (result.updated > 0) {
|
|
446
|
+
console.log(
|
|
447
|
+
chalk.green(
|
|
448
|
+
`✔ Updated ${result.updated} record(s) in ${domain}`,
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (result.skipped > 0) {
|
|
453
|
+
console.log(
|
|
454
|
+
chalk.yellow(
|
|
455
|
+
`Skipped ${result.skipped} duplicate(s) in ${domain}`,
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (
|
|
463
|
+
result.errors.length > 0 &&
|
|
464
|
+
result.created + result.updated === 0
|
|
465
|
+
) {
|
|
466
|
+
process.exitCode = 1;
|
|
467
|
+
}
|
|
468
|
+
} catch (err) {
|
|
469
|
+
if (jsonMode) {
|
|
470
|
+
outputJsonError(
|
|
471
|
+
"record",
|
|
472
|
+
err instanceof Error ? err.message : String(err),
|
|
473
|
+
);
|
|
474
|
+
} else {
|
|
475
|
+
console.error(
|
|
476
|
+
chalk.red(
|
|
477
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
process.exitCode = 1;
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const config = await readConfig();
|
|
486
|
+
|
|
487
|
+
if (!config.domains.includes(domain)) {
|
|
488
|
+
if (jsonMode) {
|
|
489
|
+
outputJsonError(
|
|
490
|
+
"record",
|
|
491
|
+
`Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`,
|
|
492
|
+
);
|
|
493
|
+
} else {
|
|
494
|
+
console.error(
|
|
495
|
+
chalk.red(`Error: domain "${domain}" not found in config.`),
|
|
496
|
+
);
|
|
497
|
+
console.error(
|
|
498
|
+
chalk.red(
|
|
499
|
+
`Available domains: ${config.domains.join(", ") || "(none)"}`,
|
|
500
|
+
),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Validate --type is provided for non-stdin mode
|
|
508
|
+
if (!options.type) {
|
|
509
|
+
if (jsonMode) {
|
|
510
|
+
outputJsonError(
|
|
511
|
+
"record",
|
|
512
|
+
"--type is required (convention, pattern, failure, decision, reference, guide)",
|
|
513
|
+
);
|
|
514
|
+
} else {
|
|
515
|
+
console.error(
|
|
516
|
+
chalk.red(
|
|
517
|
+
"Error: --type is required (convention, pattern, failure, decision, reference, guide)",
|
|
518
|
+
),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
process.exitCode = 1;
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const recordType = options.type as RecordType;
|
|
526
|
+
const classification =
|
|
527
|
+
(options.classification as Classification) ?? "tactical";
|
|
528
|
+
const recordedAt = new Date().toISOString();
|
|
529
|
+
|
|
530
|
+
// Build evidence if any evidence option is provided
|
|
531
|
+
let evidence: Evidence | undefined;
|
|
532
|
+
if (
|
|
533
|
+
options.evidenceCommit ||
|
|
534
|
+
options.evidenceIssue ||
|
|
535
|
+
options.evidenceFile ||
|
|
536
|
+
options.evidenceBead
|
|
537
|
+
) {
|
|
538
|
+
evidence = {};
|
|
539
|
+
if (options.evidenceCommit)
|
|
540
|
+
evidence.commit = options.evidenceCommit as string;
|
|
541
|
+
if (options.evidenceIssue)
|
|
542
|
+
evidence.issue = options.evidenceIssue as string;
|
|
543
|
+
if (options.evidenceFile)
|
|
544
|
+
evidence.file = options.evidenceFile as string;
|
|
545
|
+
if (options.evidenceBead)
|
|
546
|
+
evidence.bead = options.evidenceBead as string;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const tags =
|
|
550
|
+
typeof options.tags === "string"
|
|
551
|
+
? options.tags
|
|
552
|
+
.split(",")
|
|
553
|
+
.map((t) => (t as string).trim())
|
|
554
|
+
.filter(Boolean)
|
|
555
|
+
: undefined;
|
|
556
|
+
|
|
557
|
+
const relatesTo =
|
|
558
|
+
typeof options.relatesTo === "string"
|
|
559
|
+
? options.relatesTo
|
|
560
|
+
.split(",")
|
|
561
|
+
.map((id: string) => id.trim())
|
|
562
|
+
.filter(Boolean)
|
|
563
|
+
: undefined;
|
|
564
|
+
|
|
565
|
+
const supersedes =
|
|
566
|
+
typeof options.supersedes === "string"
|
|
567
|
+
? options.supersedes
|
|
568
|
+
.split(",")
|
|
569
|
+
.map((id: string) => id.trim())
|
|
570
|
+
.filter(Boolean)
|
|
571
|
+
: undefined;
|
|
572
|
+
|
|
573
|
+
let outcomes: Outcome[] | undefined;
|
|
574
|
+
if (options.outcomeStatus) {
|
|
575
|
+
const o: Outcome = {
|
|
576
|
+
status: options.outcomeStatus as "success" | "failure" | "partial",
|
|
577
|
+
};
|
|
578
|
+
if (options.outcomeDuration !== undefined) {
|
|
579
|
+
o.duration = Number.parseFloat(options.outcomeDuration as string);
|
|
580
|
+
}
|
|
581
|
+
if (options.outcomeTestResults) {
|
|
582
|
+
o.test_results = options.outcomeTestResults as string;
|
|
583
|
+
}
|
|
584
|
+
if (options.outcomeAgent) {
|
|
585
|
+
o.agent = options.outcomeAgent as string;
|
|
586
|
+
}
|
|
587
|
+
outcomes = [o];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let record: ExpertiseRecord;
|
|
591
|
+
|
|
592
|
+
switch (recordType) {
|
|
593
|
+
case "convention": {
|
|
594
|
+
const conventionContent =
|
|
595
|
+
content ?? (options.description as string | undefined);
|
|
596
|
+
if (!conventionContent) {
|
|
597
|
+
if (jsonMode) {
|
|
598
|
+
outputJsonError(
|
|
599
|
+
"record",
|
|
600
|
+
"Convention records require content (positional argument or --description).",
|
|
601
|
+
);
|
|
602
|
+
} else {
|
|
603
|
+
console.error(
|
|
604
|
+
chalk.red(
|
|
605
|
+
"Error: convention records require content (positional argument or --description).",
|
|
606
|
+
),
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
process.exitCode = 1;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
record = {
|
|
613
|
+
type: "convention",
|
|
614
|
+
content: conventionContent,
|
|
615
|
+
classification,
|
|
616
|
+
recorded_at: recordedAt,
|
|
617
|
+
...(evidence && { evidence }),
|
|
618
|
+
...(tags && tags.length > 0 && { tags }),
|
|
619
|
+
...(relatesTo &&
|
|
620
|
+
relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
621
|
+
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
622
|
+
...(outcomes && { outcomes }),
|
|
623
|
+
};
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
case "pattern": {
|
|
628
|
+
const patternName = options.name as string | undefined;
|
|
629
|
+
const patternDesc =
|
|
630
|
+
(options.description as string | undefined) ?? content;
|
|
631
|
+
if (!patternName || !patternDesc) {
|
|
632
|
+
if (jsonMode) {
|
|
633
|
+
outputJsonError(
|
|
634
|
+
"record",
|
|
635
|
+
"Pattern records require --name and --description (or positional content).",
|
|
636
|
+
);
|
|
637
|
+
} else {
|
|
638
|
+
console.error(
|
|
639
|
+
chalk.red(
|
|
640
|
+
"Error: pattern records require --name and --description (or positional content).",
|
|
641
|
+
),
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
process.exitCode = 1;
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
record = {
|
|
648
|
+
type: "pattern",
|
|
649
|
+
name: patternName,
|
|
650
|
+
description: patternDesc,
|
|
651
|
+
classification,
|
|
652
|
+
recorded_at: recordedAt,
|
|
653
|
+
...(evidence && { evidence }),
|
|
654
|
+
...(typeof options.files === "string" && {
|
|
655
|
+
files: options.files.split(","),
|
|
656
|
+
}),
|
|
657
|
+
...(tags && tags.length > 0 && { tags }),
|
|
658
|
+
...(relatesTo &&
|
|
659
|
+
relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
660
|
+
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
661
|
+
...(outcomes && { outcomes }),
|
|
662
|
+
};
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
case "failure": {
|
|
667
|
+
const failureDesc = options.description as string | undefined;
|
|
668
|
+
const failureResolution = options.resolution as string | undefined;
|
|
669
|
+
if (!failureDesc || !failureResolution) {
|
|
670
|
+
if (jsonMode) {
|
|
671
|
+
outputJsonError(
|
|
672
|
+
"record",
|
|
673
|
+
"Failure records require --description and --resolution.",
|
|
674
|
+
);
|
|
675
|
+
} else {
|
|
676
|
+
console.error(
|
|
677
|
+
chalk.red(
|
|
678
|
+
"Error: failure records require --description and --resolution.",
|
|
679
|
+
),
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
process.exitCode = 1;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
record = {
|
|
686
|
+
type: "failure",
|
|
687
|
+
description: failureDesc,
|
|
688
|
+
resolution: failureResolution,
|
|
689
|
+
classification,
|
|
690
|
+
recorded_at: recordedAt,
|
|
691
|
+
...(evidence && { evidence }),
|
|
692
|
+
...(tags && tags.length > 0 && { tags }),
|
|
693
|
+
...(relatesTo &&
|
|
694
|
+
relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
695
|
+
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
696
|
+
...(outcomes && { outcomes }),
|
|
697
|
+
};
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
case "decision": {
|
|
702
|
+
const decisionTitle = options.title as string | undefined;
|
|
703
|
+
const decisionRationale = options.rationale as string | undefined;
|
|
704
|
+
if (!decisionTitle || !decisionRationale) {
|
|
705
|
+
if (jsonMode) {
|
|
706
|
+
outputJsonError(
|
|
707
|
+
"record",
|
|
708
|
+
"Decision records require --title and --rationale.",
|
|
709
|
+
);
|
|
710
|
+
} else {
|
|
711
|
+
console.error(
|
|
712
|
+
chalk.red(
|
|
713
|
+
"Error: decision records require --title and --rationale.",
|
|
714
|
+
),
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
process.exitCode = 1;
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
record = {
|
|
721
|
+
type: "decision",
|
|
722
|
+
title: decisionTitle,
|
|
723
|
+
rationale: decisionRationale,
|
|
724
|
+
classification,
|
|
725
|
+
recorded_at: recordedAt,
|
|
726
|
+
...(evidence && { evidence }),
|
|
727
|
+
...(tags && tags.length > 0 && { tags }),
|
|
728
|
+
...(relatesTo &&
|
|
729
|
+
relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
730
|
+
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
731
|
+
...(outcomes && { outcomes }),
|
|
732
|
+
};
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
case "reference": {
|
|
737
|
+
const refName = options.name as string | undefined;
|
|
738
|
+
const refDesc =
|
|
739
|
+
(options.description as string | undefined) ?? content;
|
|
740
|
+
if (!refName || !refDesc) {
|
|
741
|
+
if (jsonMode) {
|
|
742
|
+
outputJsonError(
|
|
743
|
+
"record",
|
|
744
|
+
"Reference records require --name and --description (or positional content).",
|
|
745
|
+
);
|
|
746
|
+
} else {
|
|
747
|
+
console.error(
|
|
748
|
+
chalk.red(
|
|
749
|
+
"Error: reference records require --name and --description (or positional content).",
|
|
750
|
+
),
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
process.exitCode = 1;
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
record = {
|
|
757
|
+
type: "reference",
|
|
758
|
+
name: refName,
|
|
759
|
+
description: refDesc,
|
|
760
|
+
classification,
|
|
761
|
+
recorded_at: recordedAt,
|
|
762
|
+
...(evidence && { evidence }),
|
|
763
|
+
...(typeof options.files === "string" && {
|
|
764
|
+
files: options.files.split(","),
|
|
765
|
+
}),
|
|
766
|
+
...(tags && tags.length > 0 && { tags }),
|
|
767
|
+
...(relatesTo &&
|
|
768
|
+
relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
769
|
+
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
770
|
+
...(outcomes && { outcomes }),
|
|
771
|
+
};
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
case "guide": {
|
|
776
|
+
const guideName = options.name as string | undefined;
|
|
777
|
+
const guideDesc =
|
|
778
|
+
(options.description as string | undefined) ?? content;
|
|
779
|
+
if (!guideName || !guideDesc) {
|
|
780
|
+
if (jsonMode) {
|
|
781
|
+
outputJsonError(
|
|
782
|
+
"record",
|
|
783
|
+
"Guide records require --name and --description (or positional content).",
|
|
784
|
+
);
|
|
785
|
+
} else {
|
|
786
|
+
console.error(
|
|
787
|
+
chalk.red(
|
|
788
|
+
"Error: guide records require --name and --description (or positional content).",
|
|
789
|
+
),
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
process.exitCode = 1;
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
record = {
|
|
796
|
+
type: "guide",
|
|
797
|
+
name: guideName,
|
|
798
|
+
description: guideDesc,
|
|
799
|
+
classification,
|
|
800
|
+
recorded_at: recordedAt,
|
|
801
|
+
...(evidence && { evidence }),
|
|
802
|
+
...(tags && tags.length > 0 && { tags }),
|
|
803
|
+
...(relatesTo &&
|
|
804
|
+
relatesTo.length > 0 && { relates_to: relatesTo }),
|
|
805
|
+
...(supersedes && supersedes.length > 0 && { supersedes }),
|
|
806
|
+
...(outcomes && { outcomes }),
|
|
807
|
+
};
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Validate against JSON schema
|
|
813
|
+
const ajv = new Ajv();
|
|
814
|
+
const validate = ajv.compile(recordSchema);
|
|
815
|
+
if (!validate(record)) {
|
|
816
|
+
const errors = (validate.errors ?? []).map(
|
|
817
|
+
(err) => `${err.instancePath} ${err.message}`,
|
|
818
|
+
);
|
|
819
|
+
if (jsonMode) {
|
|
820
|
+
outputJsonError(
|
|
821
|
+
"record",
|
|
822
|
+
`Schema validation failed: ${errors.join("; ")}`,
|
|
823
|
+
);
|
|
824
|
+
} else {
|
|
825
|
+
console.error(chalk.red("Error: record failed schema validation:"));
|
|
826
|
+
for (const err of validate.errors ?? []) {
|
|
827
|
+
console.error(chalk.red(` ${err.instancePath} ${err.message}`));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
process.exitCode = 1;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const filePath = getExpertisePath(domain);
|
|
835
|
+
const dryRun = options.dryRun === true;
|
|
836
|
+
|
|
837
|
+
if (dryRun) {
|
|
838
|
+
// Dry-run: check for duplicates without writing
|
|
839
|
+
const existing = await readExpertiseFile(filePath);
|
|
840
|
+
const dup = findDuplicate(existing, record);
|
|
841
|
+
|
|
842
|
+
let action = "created";
|
|
843
|
+
if (dup && !options.force) {
|
|
844
|
+
const isNamed =
|
|
845
|
+
record.type === "pattern" ||
|
|
846
|
+
record.type === "decision" ||
|
|
847
|
+
record.type === "reference" ||
|
|
848
|
+
record.type === "guide";
|
|
849
|
+
|
|
850
|
+
action = isNamed ? "updated" : "skipped";
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (jsonMode) {
|
|
854
|
+
outputJson({
|
|
855
|
+
success: true,
|
|
856
|
+
command: "record",
|
|
857
|
+
action: "dry-run",
|
|
858
|
+
wouldDo: action,
|
|
859
|
+
domain,
|
|
860
|
+
type: recordType,
|
|
861
|
+
record,
|
|
862
|
+
});
|
|
863
|
+
} else {
|
|
864
|
+
if (action === "created") {
|
|
865
|
+
console.log(
|
|
866
|
+
chalk.green(
|
|
867
|
+
`✓ Dry-run: Would create ${recordType} in ${domain}`,
|
|
868
|
+
),
|
|
869
|
+
);
|
|
870
|
+
} else if (action === "updated") {
|
|
871
|
+
console.log(
|
|
872
|
+
chalk.green(
|
|
873
|
+
`✓ Dry-run: Would update existing ${recordType} in ${domain}`,
|
|
874
|
+
),
|
|
875
|
+
);
|
|
876
|
+
} else {
|
|
877
|
+
console.log(
|
|
878
|
+
chalk.yellow(
|
|
879
|
+
`Dry-run: Duplicate ${recordType} already exists in ${domain}. Would skip.`,
|
|
880
|
+
),
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
console.log(chalk.dim(" Run without --dry-run to apply changes."));
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
// Normal mode: write with file locking
|
|
887
|
+
await withFileLock(filePath, async () => {
|
|
888
|
+
const existing = await readExpertiseFile(filePath);
|
|
889
|
+
const dup = findDuplicate(existing, record);
|
|
890
|
+
|
|
891
|
+
if (dup && !options.force) {
|
|
892
|
+
const isNamed =
|
|
893
|
+
record.type === "pattern" ||
|
|
894
|
+
record.type === "decision" ||
|
|
895
|
+
record.type === "reference" ||
|
|
896
|
+
record.type === "guide";
|
|
897
|
+
|
|
898
|
+
if (isNamed) {
|
|
899
|
+
// Upsert: replace in place
|
|
900
|
+
existing[dup.index] = record;
|
|
901
|
+
await writeExpertiseFile(filePath, existing);
|
|
902
|
+
if (jsonMode) {
|
|
903
|
+
outputJson({
|
|
904
|
+
success: true,
|
|
905
|
+
command: "record",
|
|
906
|
+
action: "updated",
|
|
907
|
+
domain,
|
|
908
|
+
type: recordType,
|
|
909
|
+
index: dup.index + 1,
|
|
910
|
+
record,
|
|
911
|
+
});
|
|
912
|
+
} else {
|
|
913
|
+
console.log(
|
|
914
|
+
chalk.green(
|
|
915
|
+
`\u2714 Updated existing ${recordType} in ${domain} (record #${dup.index + 1})`,
|
|
916
|
+
),
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
} else {
|
|
920
|
+
// Exact match: skip
|
|
921
|
+
if (jsonMode) {
|
|
922
|
+
outputJson({
|
|
923
|
+
success: true,
|
|
924
|
+
command: "record",
|
|
925
|
+
action: "skipped",
|
|
926
|
+
domain,
|
|
927
|
+
type: recordType,
|
|
928
|
+
index: dup.index + 1,
|
|
929
|
+
});
|
|
930
|
+
} else {
|
|
931
|
+
console.log(
|
|
932
|
+
chalk.yellow(
|
|
933
|
+
`Duplicate ${recordType} already exists in ${domain} (record #${dup.index + 1}). Use --force to add anyway.`,
|
|
934
|
+
),
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
await appendRecord(filePath, record);
|
|
940
|
+
if (jsonMode) {
|
|
941
|
+
outputJson({
|
|
942
|
+
success: true,
|
|
943
|
+
command: "record",
|
|
944
|
+
action: "created",
|
|
945
|
+
domain,
|
|
946
|
+
type: recordType,
|
|
947
|
+
record,
|
|
948
|
+
});
|
|
949
|
+
} else {
|
|
950
|
+
console.log(
|
|
951
|
+
chalk.green(`\u2714 Recorded ${recordType} in ${domain}`),
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
);
|
|
959
|
+
}
|