@cmnwlth/curate 0.1.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/LICENSE +202 -0
- package/dist/index.js +738 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +204 -0
- package/dist/lib.js +396 -0
- package/dist/lib.js.map +1 -0
- package/package.json +40 -0
package/dist/lib.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// src/capture.ts
|
|
2
|
+
import { isFeatureEnabled as isFeatureEnabled2 } from "@cmnwlth/core";
|
|
3
|
+
|
|
4
|
+
// src/curate.ts
|
|
5
|
+
import {
|
|
6
|
+
hasSecrets,
|
|
7
|
+
isFeatureEnabled,
|
|
8
|
+
listNotes as listNotes2,
|
|
9
|
+
loadBrainConfig,
|
|
10
|
+
scanOptions
|
|
11
|
+
} from "@cmnwlth/core";
|
|
12
|
+
|
|
13
|
+
// src/staging.ts
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { listNotes, writeNote } from "@cmnwlth/core";
|
|
16
|
+
var STAGING_DIR = "staging";
|
|
17
|
+
function stagingRoot(brainDir) {
|
|
18
|
+
return path.join(brainDir, STAGING_DIR);
|
|
19
|
+
}
|
|
20
|
+
function stageNote(brainDir, input) {
|
|
21
|
+
return writeNote(stagingRoot(brainDir), input);
|
|
22
|
+
}
|
|
23
|
+
function listStaged(brainDir) {
|
|
24
|
+
return listNotes(stagingRoot(brainDir));
|
|
25
|
+
}
|
|
26
|
+
function stagedAbsPath(brainDir, note) {
|
|
27
|
+
return path.join(stagingRoot(brainDir), note.path);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/curate.ts
|
|
31
|
+
var MIN_BODY_LENGTH = 15;
|
|
32
|
+
var DUPLICATE_THRESHOLD = 0.8;
|
|
33
|
+
function tokenSet(text) {
|
|
34
|
+
const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").trim();
|
|
35
|
+
const tokens = normalized.split(/\s+/).filter((t) => t.length > 0);
|
|
36
|
+
return new Set(tokens);
|
|
37
|
+
}
|
|
38
|
+
function jaccard(a, b) {
|
|
39
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
40
|
+
let intersection = 0;
|
|
41
|
+
for (const token of a) {
|
|
42
|
+
if (b.has(token)) intersection += 1;
|
|
43
|
+
}
|
|
44
|
+
const union = a.size + b.size - intersection;
|
|
45
|
+
return union === 0 ? 0 : intersection / union;
|
|
46
|
+
}
|
|
47
|
+
function textSimilarity(a, b) {
|
|
48
|
+
return jaccard(tokenSet(a), tokenSet(b));
|
|
49
|
+
}
|
|
50
|
+
function candidateText(input) {
|
|
51
|
+
return `${input.title} ${input.body}`;
|
|
52
|
+
}
|
|
53
|
+
function candidateSecretScanText(input) {
|
|
54
|
+
const parts = [input.title, input.body, ...input.tags ?? []];
|
|
55
|
+
if (input.author) parts.push(input.author);
|
|
56
|
+
if (input.source) parts.push(input.source);
|
|
57
|
+
if (input.fields && Object.keys(input.fields).length > 0)
|
|
58
|
+
parts.push(JSON.stringify(input.fields));
|
|
59
|
+
return parts.join("\n");
|
|
60
|
+
}
|
|
61
|
+
function noteText(note) {
|
|
62
|
+
return `${note.frontmatter.title} ${note.body}`;
|
|
63
|
+
}
|
|
64
|
+
var defaultCurator = {
|
|
65
|
+
assess(candidate, existing) {
|
|
66
|
+
if (candidate.title.trim().length === 0 || candidate.body.trim().length < MIN_BODY_LENGTH) {
|
|
67
|
+
return { accept: false, reason: "too-thin" };
|
|
68
|
+
}
|
|
69
|
+
const candidateTokens = tokenSet(candidateText(candidate));
|
|
70
|
+
let bestScore = 0;
|
|
71
|
+
let bestId;
|
|
72
|
+
for (const note of existing) {
|
|
73
|
+
const score = jaccard(candidateTokens, tokenSet(noteText(note)));
|
|
74
|
+
if (score > bestScore) {
|
|
75
|
+
bestScore = score;
|
|
76
|
+
bestId = note.frontmatter.id;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (bestScore >= DUPLICATE_THRESHOLD && bestId !== void 0) {
|
|
80
|
+
return { accept: false, reason: "duplicate", duplicateOf: bestId };
|
|
81
|
+
}
|
|
82
|
+
return { accept: true, reason: "accepted" };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
async function curate(brainDir, candidates, curator = defaultCurator) {
|
|
86
|
+
const canon = await listNotes2(brainDir);
|
|
87
|
+
const staged = await listStaged(brainDir);
|
|
88
|
+
const existing = [...canon, ...staged];
|
|
89
|
+
const result = { staged: [], rejected: [] };
|
|
90
|
+
const autoAdr = await isFeatureEnabled(brainDir, "autoAdr");
|
|
91
|
+
const secretOpts = scanOptions(await loadBrainConfig(brainDir));
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
if (hasSecrets(candidateSecretScanText(candidate), secretOpts)) {
|
|
94
|
+
result.rejected.push({ candidate, reason: "contains-secret" });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (candidate.kind === "decision" && !autoAdr) {
|
|
98
|
+
result.rejected.push({ candidate, reason: "auto-adr-disabled" });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const assessment = curator.assess(candidate, existing);
|
|
102
|
+
if (!assessment.accept) {
|
|
103
|
+
result.rejected.push({
|
|
104
|
+
candidate,
|
|
105
|
+
reason: assessment.reason,
|
|
106
|
+
...assessment.duplicateOf !== void 0 ? { duplicateOf: assessment.duplicateOf } : {}
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const note = await stageNote(brainDir, candidate);
|
|
112
|
+
result.staged.push(note);
|
|
113
|
+
existing.push(note);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
result.rejected.push({
|
|
116
|
+
candidate,
|
|
117
|
+
reason: `invalid: ${err instanceof Error ? err.message : String(err)}`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/review.ts
|
|
125
|
+
import { promises as fs } from "fs";
|
|
126
|
+
import path2 from "path";
|
|
127
|
+
import { pathForNote, resolveWithinBrain } from "@cmnwlth/core";
|
|
128
|
+
function listPending(brainDir) {
|
|
129
|
+
return listStaged(brainDir);
|
|
130
|
+
}
|
|
131
|
+
async function findStaged(brainDir, id) {
|
|
132
|
+
const pending = await listStaged(brainDir);
|
|
133
|
+
return pending.find((n) => n.frontmatter.id === id);
|
|
134
|
+
}
|
|
135
|
+
async function approve(brainDir, id) {
|
|
136
|
+
const note = await findStaged(brainDir, id);
|
|
137
|
+
if (!note) {
|
|
138
|
+
throw new Error(`No staged note with id "${id}" to approve`);
|
|
139
|
+
}
|
|
140
|
+
const canonRel = pathForNote(note.frontmatter.kind, id, note.frontmatter.source);
|
|
141
|
+
const canonAbs = resolveWithinBrain(brainDir, canonRel);
|
|
142
|
+
const stagedAbs = stagedAbsPath(brainDir, note);
|
|
143
|
+
await fs.mkdir(path2.dirname(canonAbs), { recursive: true });
|
|
144
|
+
const content = await fs.readFile(stagedAbs, "utf8");
|
|
145
|
+
const tmp = `${canonAbs}.${Date.now()}.tmp`;
|
|
146
|
+
await fs.writeFile(tmp, content, "utf8");
|
|
147
|
+
await fs.rename(tmp, canonAbs);
|
|
148
|
+
await fs.rm(stagedAbs);
|
|
149
|
+
return canonRel;
|
|
150
|
+
}
|
|
151
|
+
async function reject(brainDir, id) {
|
|
152
|
+
const note = await findStaged(brainDir, id);
|
|
153
|
+
if (!note) {
|
|
154
|
+
throw new Error(`No staged note with id "${id}" to reject`);
|
|
155
|
+
}
|
|
156
|
+
await fs.rm(stagedAbsPath(brainDir, note));
|
|
157
|
+
}
|
|
158
|
+
async function approveAll(brainDir) {
|
|
159
|
+
const pending = await listStaged(brainDir);
|
|
160
|
+
const paths = [];
|
|
161
|
+
for (const note of pending) {
|
|
162
|
+
paths.push(await approve(brainDir, note.frontmatter.id));
|
|
163
|
+
}
|
|
164
|
+
return paths;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/capture.ts
|
|
168
|
+
async function captureCandidates(brainDir, candidates, curator) {
|
|
169
|
+
const result = await curate(brainDir, candidates, curator);
|
|
170
|
+
const promoted = [];
|
|
171
|
+
if (result.staged.length > 0 && await isFeatureEnabled2(brainDir, "autoPromote")) {
|
|
172
|
+
for (const note of result.staged) {
|
|
173
|
+
promoted.push(await approve(brainDir, note.frontmatter.id));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { ...result, promoted };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/consolidate.ts
|
|
180
|
+
import { acquireSyncLock, listNotes as listNotes3, supersedeNote } from "@cmnwlth/core";
|
|
181
|
+
var DEFAULT_CONSOLIDATE_THRESHOLD = 0.9;
|
|
182
|
+
function noteText2(n) {
|
|
183
|
+
return `${n.frontmatter.title} ${n.body}`;
|
|
184
|
+
}
|
|
185
|
+
function pickSurvivor(cluster) {
|
|
186
|
+
return [...cluster].sort((a, b) => {
|
|
187
|
+
const av = a.frontmatter.kind === "memory" ? a.frontmatter.verified ?? "" : "";
|
|
188
|
+
const bv = b.frontmatter.kind === "memory" ? b.frontmatter.verified ?? "" : "";
|
|
189
|
+
if (av !== bv) return av < bv ? 1 : -1;
|
|
190
|
+
if (a.frontmatter.created !== b.frontmatter.created) {
|
|
191
|
+
return a.frontmatter.created < b.frontmatter.created ? 1 : -1;
|
|
192
|
+
}
|
|
193
|
+
return a.frontmatter.id < b.frontmatter.id ? -1 : 1;
|
|
194
|
+
})[0];
|
|
195
|
+
}
|
|
196
|
+
function clusterBySimilarity(notes, threshold) {
|
|
197
|
+
const parent = notes.map((_, i) => i);
|
|
198
|
+
const find = (i) => {
|
|
199
|
+
while (parent[i] !== i) {
|
|
200
|
+
parent[i] = parent[parent[i]];
|
|
201
|
+
i = parent[i];
|
|
202
|
+
}
|
|
203
|
+
return i;
|
|
204
|
+
};
|
|
205
|
+
const union = (a, b) => {
|
|
206
|
+
parent[find(a)] = find(b);
|
|
207
|
+
};
|
|
208
|
+
for (let i = 0; i < notes.length; i++) {
|
|
209
|
+
for (let j = i + 1; j < notes.length; j++) {
|
|
210
|
+
if (textSimilarity(noteText2(notes[i]), noteText2(notes[j])) >= threshold) union(i, j);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
214
|
+
notes.forEach((n, i) => {
|
|
215
|
+
const r = find(i);
|
|
216
|
+
(byRoot.get(r) ?? byRoot.set(r, []).get(r)).push(n);
|
|
217
|
+
});
|
|
218
|
+
return [...byRoot.values()].filter((c) => c.length > 1);
|
|
219
|
+
}
|
|
220
|
+
async function consolidateCanon(brainDir, opts = {}) {
|
|
221
|
+
const threshold = opts.threshold ?? DEFAULT_CONSOLIDATE_THRESHOLD;
|
|
222
|
+
const release = await acquireSyncLock(brainDir);
|
|
223
|
+
if (!release)
|
|
224
|
+
return { clusters: 0, superseded: [], skipped: "another writer holds the sync lock" };
|
|
225
|
+
try {
|
|
226
|
+
const notes = await listNotes3(brainDir);
|
|
227
|
+
const active = notes.filter(
|
|
228
|
+
(n) => (n.frontmatter.kind === "memory" || n.frontmatter.kind === "decision") && n.frontmatter.status !== "superseded"
|
|
229
|
+
);
|
|
230
|
+
const superseded = [];
|
|
231
|
+
let clusters = 0;
|
|
232
|
+
for (const kind of ["memory", "decision"]) {
|
|
233
|
+
const ofKind = active.filter((n) => n.frontmatter.kind === kind);
|
|
234
|
+
for (const cluster of clusterBySimilarity(ofKind, threshold)) {
|
|
235
|
+
clusters += 1;
|
|
236
|
+
const survivor = pickSurvivor(cluster);
|
|
237
|
+
for (const dup of cluster) {
|
|
238
|
+
if (dup.frontmatter.id === survivor.frontmatter.id) continue;
|
|
239
|
+
if (!opts.dryRun) await supersedeNote(brainDir, dup.path, survivor.frontmatter.id);
|
|
240
|
+
superseded.push({
|
|
241
|
+
id: dup.frontmatter.id,
|
|
242
|
+
path: dup.path,
|
|
243
|
+
survivor: survivor.frontmatter.id
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
superseded.sort((a, b) => a.id < b.id ? -1 : 1);
|
|
249
|
+
return { clusters, superseded };
|
|
250
|
+
} finally {
|
|
251
|
+
await release();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/relevance.ts
|
|
256
|
+
import { listNotes as listNotes4, readNote, search } from "@cmnwlth/core";
|
|
257
|
+
var DEFAULT_LIMIT = 10;
|
|
258
|
+
function byId(a, b) {
|
|
259
|
+
return a.frontmatter.id < b.frontmatter.id ? -1 : a.frontmatter.id > b.frontmatter.id ? 1 : 0;
|
|
260
|
+
}
|
|
261
|
+
function byCreatedDesc(a, b) {
|
|
262
|
+
if (a.frontmatter.created !== b.frontmatter.created) {
|
|
263
|
+
return a.frontmatter.created < b.frontmatter.created ? 1 : -1;
|
|
264
|
+
}
|
|
265
|
+
return byId(a, b);
|
|
266
|
+
}
|
|
267
|
+
async function selectRelevant(brainDir, q = {}) {
|
|
268
|
+
const limit = q.limit ?? DEFAULT_LIMIT;
|
|
269
|
+
if (q.query !== void 0 && q.query.trim().length > 0) {
|
|
270
|
+
const hits = await search(brainDir, q.query, { limit });
|
|
271
|
+
const notes = [];
|
|
272
|
+
for (const hit of hits) {
|
|
273
|
+
notes.push(await readNote(brainDir, hit.path));
|
|
274
|
+
}
|
|
275
|
+
return notes;
|
|
276
|
+
}
|
|
277
|
+
const workStates = await listNotes4(brainDir, "work-state");
|
|
278
|
+
const active = workStates.filter((n) => n.frontmatter.kind === "work-state" && n.frontmatter.status !== "done").sort(byId);
|
|
279
|
+
const decisions = (await listNotes4(brainDir, "decision")).sort(byCreatedDesc);
|
|
280
|
+
return [...active, ...decisions].slice(0, limit);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/context.ts
|
|
284
|
+
import "@cmnwlth/core";
|
|
285
|
+
var SNIPPET_MAX = 120;
|
|
286
|
+
function firstLine(body) {
|
|
287
|
+
for (const line of body.split("\n")) {
|
|
288
|
+
const trimmed = line.trim();
|
|
289
|
+
if (trimmed.length > 0) {
|
|
290
|
+
return trimmed.length > SNIPPET_MAX ? trimmed.slice(0, SNIPPET_MAX) : trimmed;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
function formatContext(notes) {
|
|
296
|
+
if (notes.length === 0) return "";
|
|
297
|
+
const lines = [
|
|
298
|
+
`## Team brain \u2014 ${notes.length} relevant note(s)`,
|
|
299
|
+
'_Cite any note you use inline, e.g. "\u{1F4D6} from the team brain: TITLE"._',
|
|
300
|
+
""
|
|
301
|
+
];
|
|
302
|
+
for (const note of notes) {
|
|
303
|
+
const { title, kind } = note.frontmatter;
|
|
304
|
+
const snippet = firstLine(note.body);
|
|
305
|
+
lines.push(snippet ? `- **${title}** (${kind}) \u2014 ${snippet}` : `- **${title}** (${kind})`);
|
|
306
|
+
}
|
|
307
|
+
return lines.join("\n");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/scope.ts
|
|
311
|
+
import { promises as fs2 } from "fs";
|
|
312
|
+
import os from "os";
|
|
313
|
+
import path3 from "path";
|
|
314
|
+
function defaultConfigPath() {
|
|
315
|
+
return process.env.COMMONWEALTH_CONFIG ?? path3.join(os.homedir(), ".commonwealth", "config.json");
|
|
316
|
+
}
|
|
317
|
+
async function loadUserConfig(configPath = defaultConfigPath()) {
|
|
318
|
+
let raw;
|
|
319
|
+
try {
|
|
320
|
+
raw = await fs2.readFile(configPath, "utf8");
|
|
321
|
+
} catch {
|
|
322
|
+
return { allow: [], deny: [] };
|
|
323
|
+
}
|
|
324
|
+
let parsed;
|
|
325
|
+
try {
|
|
326
|
+
parsed = JSON.parse(raw);
|
|
327
|
+
} catch {
|
|
328
|
+
return { allow: [], deny: [] };
|
|
329
|
+
}
|
|
330
|
+
const obj = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
331
|
+
return {
|
|
332
|
+
allow: Array.isArray(obj.allow) ? obj.allow : [],
|
|
333
|
+
deny: Array.isArray(obj.deny) ? obj.deny : []
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function saveUserConfig(config, configPath = defaultConfigPath()) {
|
|
337
|
+
await fs2.mkdir(path3.dirname(configPath), { recursive: true });
|
|
338
|
+
await fs2.writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
339
|
+
`, "utf8");
|
|
340
|
+
}
|
|
341
|
+
function expand(entry) {
|
|
342
|
+
const home = os.homedir();
|
|
343
|
+
if (entry === "~") return path3.resolve(home);
|
|
344
|
+
if (entry.startsWith("~/")) return path3.resolve(home, entry.slice(2));
|
|
345
|
+
return path3.resolve(entry);
|
|
346
|
+
}
|
|
347
|
+
function isUnder(child, parent) {
|
|
348
|
+
if (parent === path3.sep) return true;
|
|
349
|
+
return child === parent || child.startsWith(parent + path3.sep);
|
|
350
|
+
}
|
|
351
|
+
function underAny(p, list) {
|
|
352
|
+
return list.some((entry) => isUnder(p, expand(entry)));
|
|
353
|
+
}
|
|
354
|
+
function isInScope(cwd, config) {
|
|
355
|
+
const target = expand(cwd);
|
|
356
|
+
const allowed = config.allow.length === 0 || underAny(target, config.allow);
|
|
357
|
+
return allowed && !underAny(target, config.deny);
|
|
358
|
+
}
|
|
359
|
+
async function addAllow(pathArg, configPath = defaultConfigPath()) {
|
|
360
|
+
const config = await loadUserConfig(configPath);
|
|
361
|
+
const resolved = expand(pathArg);
|
|
362
|
+
if (!config.allow.includes(resolved)) {
|
|
363
|
+
config.allow.push(resolved);
|
|
364
|
+
await saveUserConfig(config, configPath);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function addDeny(pathArg, configPath = defaultConfigPath()) {
|
|
368
|
+
const config = await loadUserConfig(configPath);
|
|
369
|
+
const resolved = expand(pathArg);
|
|
370
|
+
if (!config.deny.includes(resolved)) {
|
|
371
|
+
config.deny.push(resolved);
|
|
372
|
+
await saveUserConfig(config, configPath);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
export {
|
|
376
|
+
DEFAULT_CONSOLIDATE_THRESHOLD,
|
|
377
|
+
addAllow,
|
|
378
|
+
addDeny,
|
|
379
|
+
approve,
|
|
380
|
+
approveAll,
|
|
381
|
+
captureCandidates,
|
|
382
|
+
consolidateCanon,
|
|
383
|
+
curate,
|
|
384
|
+
defaultCurator,
|
|
385
|
+
formatContext,
|
|
386
|
+
isInScope,
|
|
387
|
+
listPending,
|
|
388
|
+
listStaged,
|
|
389
|
+
loadUserConfig,
|
|
390
|
+
reject,
|
|
391
|
+
selectRelevant,
|
|
392
|
+
stageNote,
|
|
393
|
+
stagedAbsPath,
|
|
394
|
+
stagingRoot
|
|
395
|
+
};
|
|
396
|
+
//# sourceMappingURL=lib.js.map
|
package/dist/lib.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/capture.ts","../src/curate.ts","../src/staging.ts","../src/review.ts","../src/consolidate.ts","../src/relevance.ts","../src/context.ts","../src/scope.ts"],"sourcesContent":["import { isFeatureEnabled, type NewNoteInput } from \"@cmnwlth/core\";\nimport { curate, type CurateResult, type Curator } from \"./curate.js\";\nimport { approve } from \"./review.js\";\n\n/** Result of {@link captureCandidates}: the staging outcome plus any notes auto-promoted. */\nexport interface CaptureResult extends CurateResult {\n /** Canonical repo-relative paths of notes promoted straight to canon (autoPromote). */\n promoted: string[];\n}\n\n/**\n * Capture agent-proposed notes (ADR-0007 #9). A SessionEnd/Stop hook calls this with the\n * candidates it extracted from a session; extracting them is the hook's job, not this\n * package's — here we gate, stage, and (by default) promote them.\n *\n * The candidates are first curated into `staging/` (dedup + validation gating). Then, unless\n * the brain has turned the `autoPromote` feature off, each freshly-staged note is approved\n * straight into canon (ADR-0014) — the gating still runs, only the *manual* review step is\n * skipped. With `autoPromote: false` the notes stay in the review queue for\n * `/commonwealth:promote`, and `promoted` is empty.\n */\nexport async function captureCandidates(\n brainDir: string,\n candidates: NewNoteInput[],\n curator?: Curator,\n): Promise<CaptureResult> {\n const result = await curate(brainDir, candidates, curator);\n const promoted: string[] = [];\n if (result.staged.length > 0 && (await isFeatureEnabled(brainDir, \"autoPromote\"))) {\n for (const note of result.staged) {\n promoted.push(await approve(brainDir, note.frontmatter.id));\n }\n }\n return { ...result, promoted };\n}\n","import {\n hasSecrets,\n isFeatureEnabled,\n listNotes,\n loadBrainConfig,\n scanOptions,\n type NewNoteInput,\n type Note,\n} from \"@cmnwlth/core\";\nimport { listStaged, stageNote } from \"./staging.js\";\n\n/** Minimum trimmed body length for a candidate to clear the relevance gate. */\nconst MIN_BODY_LENGTH = 15;\n\n/** Token-set Jaccard threshold at/above which a candidate is treated as a duplicate. */\nconst DUPLICATE_THRESHOLD = 0.8;\n\n/** Outcome of assessing a single candidate against the existing note set. */\nexport interface Assessment {\n accept: boolean;\n reason: string;\n /** Id of the existing note a rejected candidate duplicates, when reason is \"duplicate\". */\n duplicateOf?: string;\n}\n\n/**\n * Pluggable curation seam (ADR-0007). A curator decides whether a proposed note is worth\n * staging, given the notes that already exist (canon + already-staged). The default is\n * deterministic; an LLM/embedding-backed curator can be swapped in without reworking the\n * staging/review mechanics.\n */\nexport interface Curator {\n assess(candidate: NewNoteInput, existing: Note[]): Assessment;\n}\n\n/** Lowercase, strip punctuation, and split into a set of word tokens. */\nfunction tokenSet(text: string): Set<string> {\n const normalized = text\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .trim();\n const tokens = normalized.split(/\\s+/).filter((t) => t.length > 0);\n return new Set(tokens);\n}\n\n/** Jaccard similarity of two token sets (|A∩B| / |A∪B|); 0 when both are empty. */\nfunction jaccard(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const token of a) {\n if (b.has(token)) intersection += 1;\n }\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\n/** Token-set Jaccard similarity of two texts (0–1). Shared with the consolidation pass (#29). */\nexport function textSimilarity(a: string, b: string): number {\n return jaccard(tokenSet(a), tokenSet(b));\n}\n\n/** Combined title + body text used for similarity comparison. */\nfunction candidateText(input: NewNoteInput): string {\n return `${input.title} ${input.body}`;\n}\n\n/**\n * All candidate text a secret could hide in — title, body, tags, and every kind-specific\n * field value — flattened for the secret gate so its coverage matches the pre-commit scrub,\n * which scans the whole serialized note (#99). `fields` values may be strings/arrays/objects,\n * so JSON-encode them to reach nested secrets.\n */\nfunction candidateSecretScanText(input: NewNoteInput): string {\n const parts = [input.title, input.body, ...(input.tags ?? [])];\n if (input.author) parts.push(input.author);\n if (input.source) parts.push(input.source);\n if (input.fields && Object.keys(input.fields).length > 0)\n parts.push(JSON.stringify(input.fields));\n return parts.join(\"\\n\");\n}\n\n/** Combined title + body text of an existing note. */\nfunction noteText(note: Note): string {\n return `${note.frontmatter.title} ${note.body}`;\n}\n\n/**\n * Deterministic default curator (ADR-0007): a relevance gate plus token-similarity\n * dedupe. No external calls; fully offline. Semantic dedupe/verification are deferred to\n * an embedding-backed curator behind this same seam.\n */\nexport const defaultCurator: Curator = {\n assess(candidate: NewNoteInput, existing: Note[]): Assessment {\n // (a) Relevance gate: reject empty titles and trivially short bodies.\n if (candidate.title.trim().length === 0 || candidate.body.trim().length < MIN_BODY_LENGTH) {\n return { accept: false, reason: \"too-thin\" };\n }\n\n // (b) Dedupe: token-set Jaccard vs each existing note; reject the nearest near-dupe.\n const candidateTokens = tokenSet(candidateText(candidate));\n let bestScore = 0;\n let bestId: string | undefined;\n for (const note of existing) {\n const score = jaccard(candidateTokens, tokenSet(noteText(note)));\n if (score > bestScore) {\n bestScore = score;\n bestId = note.frontmatter.id;\n }\n }\n if (bestScore >= DUPLICATE_THRESHOLD && bestId !== undefined) {\n return { accept: false, reason: \"duplicate\", duplicateOf: bestId };\n }\n\n return { accept: true, reason: \"accepted\" };\n },\n};\n\n/** A candidate that the curator declined to stage, with the reason it was dropped. */\nexport interface RejectedCandidate {\n candidate: NewNoteInput;\n reason: string;\n duplicateOf?: string;\n}\n\n/** Result of a curation run: what got staged and what was rejected (and why). */\nexport interface CurateResult {\n staged: Note[];\n rejected: RejectedCandidate[];\n}\n\n/**\n * Run candidates through the curator and stage the accepted ones (ADR-0007). The existing\n * set is seeded from canon (`listNotes`) plus already-staged notes, and each accepted\n * candidate is folded back into it as we go — so within-batch near-duplicates are also\n * caught (only the first of a set is staged).\n */\nexport async function curate(\n brainDir: string,\n candidates: NewNoteInput[],\n curator: Curator = defaultCurator,\n): Promise<CurateResult> {\n const canon = await listNotes(brainDir);\n const staged = await listStaged(brainDir);\n const existing: Note[] = [...canon, ...staged];\n\n const result: CurateResult = { staged: [], rejected: [] };\n\n // auto-ADR gate (ADR-0009 #33): decisions only flow through when the team has opted in.\n const autoAdr = await isFeatureEnabled(brainDir, \"autoAdr\");\n // Secret-scanner tuning from the brain config (#46): entropy detection + allowlist, off by\n // default. Loaded once here and applied to every candidate's secret gate.\n const secretOpts = scanOptions(await loadBrainConfig(brainDir));\n\n for (const candidate of candidates) {\n // Secret gate (#16): never stage a candidate carrying a credential. Reject before\n // assess/dedupe so a secret-bearing note is neither staged nor folded into `existing`.\n // Scan the WHOLE candidate — title, body, tags AND kind-specific fields — not just\n // title+body: the pre-commit scrub scans the entire serialized note, so a secret in a tag\n // or field would otherwise pass this gate, promote to canon, then be silently withheld by\n // the scrub on every sync forever (#99).\n if (hasSecrets(candidateSecretScanText(candidate), secretOpts)) {\n result.rejected.push({ candidate, reason: \"contains-secret\" });\n continue;\n }\n\n // Gate decisions before the normal assess/dedupe path; a dropped decision is not staged\n // and does not count against dedupe (it never enters `existing`).\n if (candidate.kind === \"decision\" && !autoAdr) {\n result.rejected.push({ candidate, reason: \"auto-adr-disabled\" });\n continue;\n }\n\n const assessment = curator.assess(candidate, existing);\n if (!assessment.accept) {\n result.rejected.push({\n candidate,\n reason: assessment.reason,\n ...(assessment.duplicateOf !== undefined ? { duplicateOf: assessment.duplicateOf } : {}),\n });\n continue;\n }\n // Stage individually-guarded: a single malformed candidate (e.g. an invalid kind or a\n // field the schema rejects) is dropped on its own rather than aborting the whole batch and\n // discarding every other valid note in the session (#88).\n try {\n const note = await stageNote(brainDir, candidate);\n result.staged.push(note);\n // Fold into the existing set so later batch entries dedupe against it too.\n existing.push(note);\n } catch (err) {\n result.rejected.push({\n candidate,\n reason: `invalid: ${err instanceof Error ? err.message : String(err)}`,\n });\n }\n }\n\n return result;\n}\n","import path from \"node:path\";\nimport { listNotes, writeNote, type NewNoteInput, type Note } from \"@cmnwlth/core\";\n\n/** Subtree of a brain that holds proposed (not-yet-approved) notes. */\nconst STAGING_DIR = \"staging\";\n\n/**\n * Absolute path to the staging subtree of a brain. Staged notes are ordinary notes\n * rooted here instead of at the brain root, so core note IO works unchanged (ADR-0007).\n */\nexport function stagingRoot(brainDir: string): string {\n return path.join(brainDir, STAGING_DIR);\n}\n\n/**\n * Write a candidate note into the brain's `staging/` subtree. Returns the staged\n * {@link Note}; its `path` is relative to the staging root (e.g. `memory/<id>.md`),\n * so it is deliberately NOT visible to `listNotes(brainDir)` (canon).\n */\nexport function stageNote(brainDir: string, input: NewNoteInput): Promise<Note> {\n return writeNote(stagingRoot(brainDir), input);\n}\n\n/** List every note currently staged for review under `staging/`. */\nexport function listStaged(brainDir: string): Promise<Note[]> {\n return listNotes(stagingRoot(brainDir));\n}\n\n/** Absolute filesystem path of a staged note (whose `path` is staging-root-relative). */\nexport function stagedAbsPath(brainDir: string, note: Note): string {\n return path.join(stagingRoot(brainDir), note.path);\n}\n","import { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathForNote, resolveWithinBrain, type Note } from \"@cmnwlth/core\";\nimport { listStaged, stagedAbsPath } from \"./staging.js\";\n\n/** All notes currently awaiting review in the staging queue. */\nexport function listPending(brainDir: string): Promise<Note[]> {\n return listStaged(brainDir);\n}\n\n/** Find a staged note by its frontmatter id, or undefined if none matches. */\nasync function findStaged(brainDir: string, id: string): Promise<Note | undefined> {\n const pending = await listStaged(brainDir);\n return pending.find((n) => n.frontmatter.id === id);\n}\n\n/**\n * Approve a staged note (ADR-0007): move its file from `staging/<dir>/<id>.md` into the\n * canonical kind folder `<dir>/<id>.md`, preserving id and content, then remove the\n * staged copy. Returns the canonical repo-relative path. Throws if the id is not pending.\n */\nexport async function approve(brainDir: string, id: string): Promise<string> {\n const note = await findStaged(brainDir, id);\n if (!note) {\n throw new Error(`No staged note with id \"${id}\" to approve`);\n }\n // Promote into the same project subtree the note carries (ADR-0015), mirroring writeNote's\n // layout: `<project>/<kind>/<id>.md`, or `<kind>/<id>.md` when unattributed.\n const canonRel = pathForNote(note.frontmatter.kind, id, note.frontmatter.source);\n // Containment guard (#77): the schema already forbids a `..`/slash id, but assert the resolved\n // write path stays inside the brain so a crafted id/source can never escape on promote.\n const canonAbs = resolveWithinBrain(brainDir, canonRel);\n const stagedAbs = stagedAbsPath(brainDir, note);\n\n await fs.mkdir(path.dirname(canonAbs), { recursive: true });\n const content = await fs.readFile(stagedAbs, \"utf8\");\n const tmp = `${canonAbs}.${Date.now()}.tmp`;\n await fs.writeFile(tmp, content, \"utf8\");\n await fs.rename(tmp, canonAbs);\n await fs.rm(stagedAbs);\n return canonRel;\n}\n\n/** Reject a staged note (ADR-0007): discard its file. Throws if the id is not pending. */\nexport async function reject(brainDir: string, id: string): Promise<void> {\n const note = await findStaged(brainDir, id);\n if (!note) {\n throw new Error(`No staged note with id \"${id}\" to reject`);\n }\n await fs.rm(stagedAbsPath(brainDir, note));\n}\n\n/** Approve every pending staged note; returns the canonical paths, in id order. */\nexport async function approveAll(brainDir: string): Promise<string[]> {\n const pending = await listStaged(brainDir);\n const paths: string[] = [];\n for (const note of pending) {\n paths.push(await approve(brainDir, note.frontmatter.id));\n }\n return paths;\n}\n","import { acquireSyncLock, listNotes, supersedeNote, type Note } from \"@cmnwlth/core\";\nimport { textSimilarity } from \"./curate.js\";\n\n/**\n * Cross-user canon consolidation (ADR-0008 / #29). Write-time dedup only sees canon + local\n * staging, so two machines can independently land near-duplicate canon notes; once they merge,\n * this pass reconciles them. It is:\n *\n * - **supersede-not-delete**: a duplicate is marked `status: superseded` + `superseded_by: <survivor>`\n * (additive, union-merges) — never deleted, so history and the reconciliation stay visible;\n * - **single-writer**: gated by the same cross-process sync lock the daemon uses, so two\n * consolidations (or a consolidation and a sync) can't fight;\n * - **conservative + deterministic**: only very-near duplicates of the SAME kind, and only the\n * supersede-able kinds (memory, decision — the only ones with `status`/`superseded_by`).\n *\n * Similarity is the deterministic token-set Jaccard today; the pluggable embedder/curator seam\n * (ADR-0005) can replace it later without changing this control flow.\n */\n\n/** Default similarity at/above which two same-kind canon notes are treated as duplicates. */\nexport const DEFAULT_CONSOLIDATE_THRESHOLD = 0.9;\n\n/** One superseded note and the survivor it now points to. */\nexport interface Supersession {\n /** Id of the note that was superseded. */\n id: string;\n /** Repo-relative path of the superseded note (its file is kept). */\n path: string;\n /** Id of the surviving note it now defers to. */\n survivor: string;\n}\n\n/** Outcome of a consolidation pass. */\nexport interface ConsolidationResult {\n /** Duplicate clusters found (each collapses to one survivor). */\n clusters: number;\n /** Every supersession applied, in id order. */\n superseded: Supersession[];\n /** Set when the pass did nothing because another writer holds the lock (single-writer). */\n skipped?: string;\n}\n\n/** Options for {@link consolidateCanon}. */\nexport interface ConsolidateOptions {\n /** Similarity threshold (default {@link DEFAULT_CONSOLIDATE_THRESHOLD}). */\n threshold?: number;\n /**\n * Report the plan without writing (no supersessions applied). The lock is still taken so the\n * preview reflects a quiescent tree.\n */\n dryRun?: boolean;\n}\n\n/** Title+body text used for similarity. */\nfunction noteText(n: Note): string {\n return `${n.frontmatter.title} ${n.body}`;\n}\n\n/**\n * Pick the surviving note of a duplicate cluster deterministically: prefer a `verified` note\n * (most recently verified), then the most recently `created`, then the lexicographically\n * smallest id. Keeping the most-checked/newest note is the safest default; the tiebreak keeps\n * the choice stable across machines.\n */\nfunction pickSurvivor(cluster: Note[]): Note {\n return [...cluster].sort((a, b) => {\n const av = a.frontmatter.kind === \"memory\" ? (a.frontmatter.verified ?? \"\") : \"\";\n const bv = b.frontmatter.kind === \"memory\" ? (b.frontmatter.verified ?? \"\") : \"\";\n if (av !== bv) return av < bv ? 1 : -1; // later verified date first\n if (a.frontmatter.created !== b.frontmatter.created) {\n return a.frontmatter.created < b.frontmatter.created ? 1 : -1; // newer first\n }\n return a.frontmatter.id < b.frontmatter.id ? -1 : 1; // stable tiebreak\n })[0]!;\n}\n\n/** Group `notes` into clusters where each note is transitively ≥ `threshold` similar to another. */\nfunction clusterBySimilarity(notes: Note[], threshold: number): Note[][] {\n const parent = notes.map((_, i) => i);\n const find = (i: number): number => {\n while (parent[i] !== i) {\n parent[i] = parent[parent[i]!]!;\n i = parent[i]!;\n }\n return i;\n };\n const union = (a: number, b: number): void => {\n parent[find(a)] = find(b);\n };\n for (let i = 0; i < notes.length; i++) {\n for (let j = i + 1; j < notes.length; j++) {\n if (textSimilarity(noteText(notes[i]!), noteText(notes[j]!)) >= threshold) union(i, j);\n }\n }\n const byRoot = new Map<number, Note[]>();\n notes.forEach((n, i) => {\n const r = find(i);\n (byRoot.get(r) ?? byRoot.set(r, []).get(r)!).push(n);\n });\n return [...byRoot.values()].filter((c) => c.length > 1);\n}\n\n/**\n * Run one consolidation pass over `brainDir`'s canon (see the module docstring). Returns what it\n * superseded (or `skipped` when another writer holds the lock). Never deletes; never runs\n * concurrently with a sync.\n */\nexport async function consolidateCanon(\n brainDir: string,\n opts: ConsolidateOptions = {},\n): Promise<ConsolidationResult> {\n const threshold = opts.threshold ?? DEFAULT_CONSOLIDATE_THRESHOLD;\n\n const release = await acquireSyncLock(brainDir);\n if (!release)\n return { clusters: 0, superseded: [], skipped: \"another writer holds the sync lock\" };\n try {\n const notes = await listNotes(brainDir);\n // Only supersede-able kinds, and only notes not already superseded.\n const active = notes.filter(\n (n) =>\n (n.frontmatter.kind === \"memory\" || n.frontmatter.kind === \"decision\") &&\n n.frontmatter.status !== \"superseded\",\n );\n\n const superseded: Supersession[] = [];\n let clusters = 0;\n // Dedup within a kind only — a memory and a decision are never merged.\n for (const kind of [\"memory\", \"decision\"] as const) {\n const ofKind = active.filter((n) => n.frontmatter.kind === kind);\n for (const cluster of clusterBySimilarity(ofKind, threshold)) {\n clusters += 1;\n const survivor = pickSurvivor(cluster);\n for (const dup of cluster) {\n if (dup.frontmatter.id === survivor.frontmatter.id) continue;\n if (!opts.dryRun) await supersedeNote(brainDir, dup.path, survivor.frontmatter.id);\n superseded.push({\n id: dup.frontmatter.id,\n path: dup.path,\n survivor: survivor.frontmatter.id,\n });\n }\n }\n }\n superseded.sort((a, b) => (a.id < b.id ? -1 : 1));\n return { clusters, superseded };\n } finally {\n await release();\n }\n}\n","import { listNotes, readNote, search, type Note } from \"@cmnwlth/core\";\n\n/** How to select relevant context for injection (ADR-0007 #12). */\nexport interface RelevanceQuery {\n /** Free-text query; when present, drives a lexical search. */\n query?: string;\n /** Max notes to return (default 10). */\n limit?: number;\n}\n\nconst DEFAULT_LIMIT = 10;\n\n/** Stable, deterministic sort by id so injected context is reproducible across runs. */\nfunction byId(a: Note, b: Note): number {\n return a.frontmatter.id < b.frontmatter.id ? -1 : a.frontmatter.id > b.frontmatter.id ? 1 : 0;\n}\n\n/** Sort by created date desc, tie-broken by id for determinism. */\nfunction byCreatedDesc(a: Note, b: Note): number {\n if (a.frontmatter.created !== b.frontmatter.created) {\n return a.frontmatter.created < b.frontmatter.created ? 1 : -1;\n }\n return byId(a, b);\n}\n\n/**\n * Select the notes most relevant to inject into a session (ADR-0007 #12). With a query,\n * this is a lexical search (each hit re-read into a full {@link Note}). Without one, it\n * returns the current working context: active (not-`done`) work-state plus recent\n * decisions, deterministically ordered and capped at `limit`.\n */\nexport async function selectRelevant(brainDir: string, q: RelevanceQuery = {}): Promise<Note[]> {\n const limit = q.limit ?? DEFAULT_LIMIT;\n\n if (q.query !== undefined && q.query.trim().length > 0) {\n const hits = await search(brainDir, q.query, { limit });\n const notes: Note[] = [];\n for (const hit of hits) {\n notes.push(await readNote(brainDir, hit.path));\n }\n return notes;\n }\n\n const workStates = await listNotes(brainDir, \"work-state\");\n const active = workStates\n .filter((n) => n.frontmatter.kind === \"work-state\" && n.frontmatter.status !== \"done\")\n .sort(byId);\n\n const decisions = (await listNotes(brainDir, \"decision\")).sort(byCreatedDesc);\n\n return [...active, ...decisions].slice(0, limit);\n}\n","import { type Note } from \"@cmnwlth/core\";\n\n/** Max length of a body snippet before it is truncated. */\nconst SNIPPET_MAX = 120;\n\n/** First non-empty line of a note body, used as a compact snippet. */\nfunction firstLine(body: string): string {\n for (const line of body.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed.length > 0) {\n return trimmed.length > SNIPPET_MAX ? trimmed.slice(0, SNIPPET_MAX) : trimmed;\n }\n }\n return \"\";\n}\n\n/**\n * Render selected notes as compact markdown suitable for injection into a Claude Code\n * session (e.g. by a SessionStart hook). The FIRST LINE is a heading that encodes the note\n * count so a value receipt can parse it (`## Team brain — N relevant note(s)`), followed by\n * a citation hint and one bullet per note: `- **<title>** (<kind>) — <snippet>`. Returns \"\"\n * for an empty selection so a hook can inject nothing. Pure function.\n *\n * @param notes The selected notes to render.\n * @returns Markdown context, or \"\" when `notes` is empty.\n */\nexport function formatContext(notes: Note[]): string {\n if (notes.length === 0) return \"\";\n const lines = [\n `## Team brain — ${notes.length} relevant note(s)`,\n '_Cite any note you use inline, e.g. \"📖 from the team brain: TITLE\"._',\n \"\",\n ];\n for (const note of notes) {\n const { title, kind } = note.frontmatter;\n const snippet = firstLine(note.body);\n lines.push(snippet ? `- **${title}** (${kind}) — ${snippet}` : `- **${title}** (${kind})`);\n }\n return lines.join(\"\\n\");\n}\n","import { promises as fs } from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\n/**\n * A single user's scope configuration (the privacy gate). Personal project folders are\n * kept out of the brain by only capturing / injecting context for sessions whose cwd is\n * in scope. `deny` always wins over `allow`.\n */\nexport interface UserConfig {\n /** Absolute (tilde-allowed) roots that are in scope. Empty => everything is in scope. */\n allow: string[];\n /** Absolute (tilde-allowed) roots that are always out of scope. Wins over `allow`. */\n deny: string[];\n}\n\n/**\n * Resolve the user config path: `$COMMONWEALTH_CONFIG` if set, else `~/.commonwealth/config.json`.\n * The `COMMONWEALTH_CONFIG` override is essential so tests never touch the real `~/.commonwealth`.\n */\nexport function defaultConfigPath(): string {\n return process.env.COMMONWEALTH_CONFIG ?? path.join(os.homedir(), \".commonwealth\", \"config.json\");\n}\n\n/**\n * Load and parse the user config. If the file is missing or partial, returns an empty\n * config (`{ allow: [], deny: [] }`), filling any missing arrays. Never throws on a\n * missing file.\n */\nexport async function loadUserConfig(configPath = defaultConfigPath()): Promise<UserConfig> {\n let raw: string;\n try {\n raw = await fs.readFile(configPath, \"utf8\");\n } catch {\n return { allow: [], deny: [] };\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { allow: [], deny: [] };\n }\n const obj = (typeof parsed === \"object\" && parsed !== null ? parsed : {}) as Partial<UserConfig>;\n return {\n allow: Array.isArray(obj.allow) ? obj.allow : [],\n deny: Array.isArray(obj.deny) ? obj.deny : [],\n };\n}\n\n/**\n * Persist the user config as pretty JSON with a trailing newline, creating the parent\n * directory (`mkdir -p`) if needed.\n */\nexport async function saveUserConfig(\n config: UserConfig,\n configPath = defaultConfigPath(),\n): Promise<void> {\n await fs.mkdir(path.dirname(configPath), { recursive: true });\n await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\\n`, \"utf8\");\n}\n\n/** Expand a leading `~` to the user's home directory, then resolve to an absolute path. */\nfunction expand(entry: string): string {\n const home = os.homedir();\n if (entry === \"~\") return path.resolve(home);\n if (entry.startsWith(\"~/\")) return path.resolve(home, entry.slice(2));\n return path.resolve(entry);\n}\n\n/**\n * True when `child` is the same path as `parent` or nested beneath it. Operates on\n * normalized absolute paths (no tilde expansion — callers pass expanded paths).\n */\nfunction isUnder(child: string, parent: string): boolean {\n if (parent === path.sep) return true; // filesystem root contains everything\n return child === parent || child.startsWith(parent + path.sep);\n}\n\n/** True when `p` is under any entry in `list` (entries are tilde-expanded first). */\nfunction underAny(p: string, list: string[]): boolean {\n return list.some((entry) => isUnder(p, expand(entry)));\n}\n\n/**\n * Decide whether `cwd` is in scope for a user's config. Rule (deny wins):\n * `inScope = (allow empty || cwd under some allow) && cwd not under any deny`.\n * The default (empty allow, empty deny) puts everything in scope.\n */\nexport function isInScope(cwd: string, config: UserConfig): boolean {\n const target = expand(cwd);\n const allowed = config.allow.length === 0 || underAny(target, config.allow);\n return allowed && !underAny(target, config.deny);\n}\n\n/**\n * Add a path to the `allow` list (tilde-expanded to an absolute path) if not already\n * present, then persist. Used by the `scope allow` CLI command.\n */\nexport async function addAllow(pathArg: string, configPath = defaultConfigPath()): Promise<void> {\n const config = await loadUserConfig(configPath);\n const resolved = expand(pathArg);\n if (!config.allow.includes(resolved)) {\n config.allow.push(resolved);\n await saveUserConfig(config, configPath);\n }\n}\n\n/**\n * Add a path to the `deny` list (tilde-expanded to an absolute path) if not already\n * present, then persist. Used by the `scope deny` CLI command.\n */\nexport async function addDeny(pathArg: string, configPath = defaultConfigPath()): Promise<void> {\n const config = await loadUserConfig(configPath);\n const resolved = expand(pathArg);\n if (!config.deny.includes(resolved)) {\n config.deny.push(resolved);\n await saveUserConfig(config, configPath);\n }\n}\n"],"mappings":"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","names":["isFeatureEnabled","listNotes","listNotes","path","path","isFeatureEnabled","listNotes","noteText","listNotes","listNotes","fs","path"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cmnwlth/curate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Capture, curation engine (dedupe/relevance), in-repo review queue, and relevance selection",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"commonwealth-curate": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/lib.js",
|
|
11
|
+
"types": "./dist/lib.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/lib.d.ts",
|
|
15
|
+
"default": "./dist/lib.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@cmnwlth/core": "0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/kristoffeys/commonwealth.git",
|
|
32
|
+
"directory": "packages/curate"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/kristoffeys/commonwealth#readme",
|
|
35
|
+
"bugs": "https://github.com/kristoffeys/commonwealth/issues",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
}
|
|
40
|
+
}
|