tldr-experts 0.3.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/CHANGELOG.md +913 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/hooks/answer-capture.js +174 -0
- package/dist/hooks/budget-gate.js +173 -0
- package/dist/hooks/chunk-0bt6yb2q.js +88 -0
- package/dist/hooks/chunk-1zwcxd3f.js +66 -0
- package/dist/hooks/chunk-9gb21660.js +66 -0
- package/dist/hooks/chunk-a8p2rc94.js +20 -0
- package/dist/hooks/chunk-ae6bkfs5.js +0 -0
- package/dist/hooks/chunk-azctppjh.js +198 -0
- package/dist/hooks/chunk-g395gk7e.js +503 -0
- package/dist/hooks/chunk-j234zf0t.js +339 -0
- package/dist/hooks/chunk-kw4tffzf.js +139 -0
- package/dist/hooks/chunk-p274ckxv.js +7435 -0
- package/dist/hooks/chunk-sdjnnmzz.js +497 -0
- package/dist/hooks/chunk-t56k6146.js +14 -0
- package/dist/hooks/chunk-t8tdv11p.js +35 -0
- package/dist/hooks/chunk-x98qs959.js +302 -0
- package/dist/hooks/chunk-y0jdr3et.js +627 -0
- package/dist/hooks/claim-sources.js +72 -0
- package/dist/hooks/dod-gate.js +198 -0
- package/dist/hooks/no-reask.js +67 -0
- package/dist/hooks/session-start.js +1348 -0
- package/dist/hooks/statusline.js +116 -0
- package/dist/tldrx.js +34219 -0
- package/env.yml +79 -0
- package/package.json +60 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/README.md +113 -0
- package/plugin/agents/README.md +22 -0
- package/plugin/hooks/hooks.json +107 -0
- package/plugin/skills/tldrx/SKILL.md +170 -0
- package/stages/build/stage.md +70 -0
- package/stages/build/stage.yml +54 -0
- package/stages/how/stage.md +89 -0
- package/stages/how/stage.yml +53 -0
- package/stages/plan/stage.md +89 -0
- package/stages/plan/stage.yml +48 -0
- package/stages/watch/stage.md +85 -0
- package/stages/watch/stage.yml +61 -0
- package/stages/what/stage.md +89 -0
- package/stages/what/stage.yml +61 -0
- package/templates/budget.yml +17 -0
- package/templates/competencies.yml +14 -0
- package/templates/env.yml +19 -0
- package/templates/epic.md +38 -0
- package/templates/expert.md +51 -0
- package/templates/experts/architect.md +77 -0
- package/templates/experts/delivery.md +76 -0
- package/templates/experts/developer.md +78 -0
- package/templates/experts/operations.md +74 -0
- package/templates/experts/product.md +78 -0
- package/templates/facts.yml +18 -0
- package/templates/handoff.md +63 -0
- package/templates/process.yml +33 -0
- package/templates/questions.md +63 -0
- package/templates/run.yml +24 -0
- package/templates/story.md +55 -0
- package/templates/watcher.md +68 -0
- package/templates/waves.yml +17 -0
- package/templates/workspace.yml +30 -0
- package/workflows/bugfix.yml +26 -0
- package/workflows/docs.yml +23 -0
- package/workflows/feature.yml +22 -0
- package/workflows/hotfix.yml +24 -0
- package/workflows/integration.yml +24 -0
- package/workflows/migration.yml +24 -0
- package/workflows/performance.yml +23 -0
- package/workflows/prototype.yml +24 -0
- package/workflows/refactor.yml +24 -0
- package/workflows/retro.yml +20 -0
- package/workflows/security-patch.yml +23 -0
- package/workflows/spike.yml +23 -0
- package/workflows/upgrade.yml +23 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
emitFactsYaml,
|
|
3
|
+
withWorkspaceLock,
|
|
4
|
+
workspaceRootOfFactsPath
|
|
5
|
+
} from "./chunk-j234zf0t.js";
|
|
6
|
+
import {
|
|
7
|
+
noteDeprecations
|
|
8
|
+
} from "./chunk-t56k6146.js";
|
|
9
|
+
import {
|
|
10
|
+
asDocument,
|
|
11
|
+
isRecord,
|
|
12
|
+
requireArray,
|
|
13
|
+
requireEnum,
|
|
14
|
+
requireKeys,
|
|
15
|
+
requireString,
|
|
16
|
+
requireVersion,
|
|
17
|
+
result
|
|
18
|
+
} from "./chunk-0bt6yb2q.js";
|
|
19
|
+
import {
|
|
20
|
+
parseYaml
|
|
21
|
+
} from "./chunk-p274ckxv.js";
|
|
22
|
+
|
|
23
|
+
// src/core/facts/FactsStore.ts
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { dirname } from "node:path";
|
|
26
|
+
|
|
27
|
+
// src/core/facts/Fact.ts
|
|
28
|
+
var FACT_KINDS = ["answer", "observed", "derived"];
|
|
29
|
+
var FACT_CONFIDENCES = ["measured", "inferred", "stated"];
|
|
30
|
+
var MAX_FACTS = 5000;
|
|
31
|
+
var MAX_FACT_CHARS = 300;
|
|
32
|
+
function isRetired(fact) {
|
|
33
|
+
return fact.retired !== null && fact.retired.at !== null;
|
|
34
|
+
}
|
|
35
|
+
function factNumber(id) {
|
|
36
|
+
return Number.parseInt(id.slice(1), 10);
|
|
37
|
+
}
|
|
38
|
+
function formatFactId(n) {
|
|
39
|
+
return `F${String(n).padStart(3, "0")}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/core/facts/validateFactsFile.ts
|
|
43
|
+
var ID_RE = /^F\d{3,6}$/;
|
|
44
|
+
var Q_RE = /^Q\d{1,6}$/;
|
|
45
|
+
function validateFactsFile(input) {
|
|
46
|
+
const issues = [];
|
|
47
|
+
const deprecations = [];
|
|
48
|
+
const doc = asDocument(input, issues);
|
|
49
|
+
if (!doc)
|
|
50
|
+
return result(issues);
|
|
51
|
+
requireVersion(doc, issues, deprecations);
|
|
52
|
+
requireKeys(doc, ["facts"], "", issues);
|
|
53
|
+
if (!requireArray(doc.facts, "facts", issues))
|
|
54
|
+
return result(issues, deprecations);
|
|
55
|
+
const rows = doc.facts;
|
|
56
|
+
if (rows.length > MAX_FACTS) {
|
|
57
|
+
issues.push({ path: "facts", message: `${rows.length} facts exceeds the ${MAX_FACTS} cap` });
|
|
58
|
+
}
|
|
59
|
+
const byId = new Map;
|
|
60
|
+
let previous = 0;
|
|
61
|
+
rows.forEach((row, i) => {
|
|
62
|
+
const path = `facts[${i}]`;
|
|
63
|
+
if (!isRecord(row)) {
|
|
64
|
+
issues.push({ path, message: "expected a mapping" });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
requireKeys(row, ["id", "fact", "area", "repos", "kind", "confidence", "source", "supersedes", "superseded_by", "retired"], path, issues);
|
|
68
|
+
const id = typeof row.id === "string" ? row.id : "";
|
|
69
|
+
if (!ID_RE.test(id))
|
|
70
|
+
issues.push({ path: `${path}.id`, message: "id must match ^F\\d{3,6}$" });
|
|
71
|
+
else if (byId.has(id))
|
|
72
|
+
issues.push({ path: `${path}.id`, message: `duplicate fact id ${id}` });
|
|
73
|
+
else {
|
|
74
|
+
const n = factNumber(id);
|
|
75
|
+
if (n <= previous)
|
|
76
|
+
issues.push({ path: `${path}.id`, message: `fact ids must ascend (${id} follows F${previous})` });
|
|
77
|
+
previous = n;
|
|
78
|
+
byId.set(id, row);
|
|
79
|
+
}
|
|
80
|
+
requireString(row.fact, `${path}.fact`, issues);
|
|
81
|
+
if (typeof row.fact === "string" && row.fact.length > MAX_FACT_CHARS) {
|
|
82
|
+
issues.push({ path: `${path}.fact`, message: `fact is ${row.fact.length} chars (max ${MAX_FACT_CHARS})` });
|
|
83
|
+
}
|
|
84
|
+
requireString(row.area, `${path}.area`, issues);
|
|
85
|
+
requireArray(row.repos, `${path}.repos`, issues);
|
|
86
|
+
requireEnum(row.kind, FACT_KINDS, `${path}.kind`, issues);
|
|
87
|
+
requireEnum(row.confidence, FACT_CONFIDENCES, `${path}.confidence`, issues);
|
|
88
|
+
if (isRecord(row.source)) {
|
|
89
|
+
requireKeys(row.source, ["who", "when", "run", "q"], `${path}.source`, issues);
|
|
90
|
+
const q = row.source.q;
|
|
91
|
+
if (typeof q === "string" && !Q_RE.test(q)) {
|
|
92
|
+
issues.push({ path: `${path}.source.q`, message: "expected ^Q\\d+$ or null" });
|
|
93
|
+
}
|
|
94
|
+
} else if (row.source !== undefined) {
|
|
95
|
+
issues.push({ path: `${path}.source`, message: "expected a mapping" });
|
|
96
|
+
}
|
|
97
|
+
const superseded = typeof row.superseded_by === "string";
|
|
98
|
+
const retired = isRecord(row.retired) && row.retired.at !== null && row.retired.at !== undefined;
|
|
99
|
+
if (superseded && retired) {
|
|
100
|
+
issues.push({ path, message: "a fact may be superseded or retired, not both" });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
for (const [id, row] of byId) {
|
|
104
|
+
const forward = row.superseded_by;
|
|
105
|
+
if (typeof forward === "string") {
|
|
106
|
+
const target = byId.get(forward);
|
|
107
|
+
if (target === undefined) {
|
|
108
|
+
issues.push({ path: `facts.${id}.superseded_by`, message: `${forward} is not in this file` });
|
|
109
|
+
} else if (target.supersedes !== id) {
|
|
110
|
+
issues.push({ path: `facts.${id}.superseded_by`, message: `${forward}.supersedes must be ${id} (links are reciprocal)` });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const back = row.supersedes;
|
|
114
|
+
if (typeof back === "string") {
|
|
115
|
+
const target = byId.get(back);
|
|
116
|
+
if (target === undefined) {
|
|
117
|
+
issues.push({ path: `facts.${id}.supersedes`, message: `${back} is not in this file` });
|
|
118
|
+
} else if (target.superseded_by !== id) {
|
|
119
|
+
issues.push({ path: `facts.${id}.supersedes`, message: `${back}.superseded_by must be ${id} (links are reciprocal)` });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result(issues, deprecations);
|
|
124
|
+
}
|
|
125
|
+
function asFactsFile(input) {
|
|
126
|
+
const doc = input;
|
|
127
|
+
return { version: doc.version, facts: doc.facts };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/core/facts/findDuplicate.ts
|
|
131
|
+
var DEFAULT_JACCARD_THRESHOLD = 0.6;
|
|
132
|
+
var MIN_TOKEN_LENGTH = 4;
|
|
133
|
+
var WORD_RE = /[a-z0-9]+/g;
|
|
134
|
+
function tokenize(text) {
|
|
135
|
+
const tokens = new Set;
|
|
136
|
+
for (const word of text.toLowerCase().matchAll(WORD_RE)) {
|
|
137
|
+
const token = word[0];
|
|
138
|
+
if (token.length >= MIN_TOKEN_LENGTH)
|
|
139
|
+
tokens.add(token);
|
|
140
|
+
}
|
|
141
|
+
return tokens;
|
|
142
|
+
}
|
|
143
|
+
function jaccard(a, b) {
|
|
144
|
+
if (a.size === 0 || b.size === 0)
|
|
145
|
+
return 0;
|
|
146
|
+
let intersection = 0;
|
|
147
|
+
for (const token of a)
|
|
148
|
+
if (b.has(token))
|
|
149
|
+
intersection++;
|
|
150
|
+
const union = a.size + b.size - intersection;
|
|
151
|
+
return union === 0 ? 0 : intersection / union;
|
|
152
|
+
}
|
|
153
|
+
function findDuplicate(question, area, facts, threshold = DEFAULT_JACCARD_THRESHOLD) {
|
|
154
|
+
const asked = tokenize(question);
|
|
155
|
+
if (asked.size === 0)
|
|
156
|
+
return null;
|
|
157
|
+
let best = null;
|
|
158
|
+
for (const fact of facts) {
|
|
159
|
+
if (fact.area !== area)
|
|
160
|
+
continue;
|
|
161
|
+
if (isRetired(fact))
|
|
162
|
+
continue;
|
|
163
|
+
const score = jaccard(asked, tokenize(fact.fact));
|
|
164
|
+
if (score < threshold)
|
|
165
|
+
continue;
|
|
166
|
+
if (best === null || score > best.score || score === best.score && fact.id > best.fact.id) {
|
|
167
|
+
best = { fact, score };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return best;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/core/facts/FactsStore.ts
|
|
174
|
+
var NEW_FILE_HEADER = `# .tldrx/memory/facts.yml — durable, provenanced answers (spec §2.5).
|
|
175
|
+
` + "# Append-mostly: supersede or retire, never edit. A fact without a source is not a fact.";
|
|
176
|
+
|
|
177
|
+
class FactsStore {
|
|
178
|
+
path;
|
|
179
|
+
version;
|
|
180
|
+
rows;
|
|
181
|
+
constructor(path, version, rows) {
|
|
182
|
+
this.path = path;
|
|
183
|
+
this.version = version;
|
|
184
|
+
this.rows = [...rows];
|
|
185
|
+
}
|
|
186
|
+
static load(path) {
|
|
187
|
+
const text = readFileSync(path, "utf8");
|
|
188
|
+
const doc = parseYaml(text);
|
|
189
|
+
const validation = validateFactsFile(doc);
|
|
190
|
+
noteDeprecations(path, validation);
|
|
191
|
+
if (!validation.ok) {
|
|
192
|
+
const first = validation.issues[0];
|
|
193
|
+
throw new Error(`invalid facts.yml (${path}): ${first?.path ?? ""} ${first?.message ?? "schema error"}`);
|
|
194
|
+
}
|
|
195
|
+
const file = asFactsFile(doc);
|
|
196
|
+
return new FactsStore(path, file.version, file.facts);
|
|
197
|
+
}
|
|
198
|
+
static loadOrEmpty(path) {
|
|
199
|
+
if (!existsSync(path))
|
|
200
|
+
return new FactsStore(path, 1, []);
|
|
201
|
+
return FactsStore.load(path);
|
|
202
|
+
}
|
|
203
|
+
get facts() {
|
|
204
|
+
return this.rows;
|
|
205
|
+
}
|
|
206
|
+
get active() {
|
|
207
|
+
return this.rows.filter((f) => !isRetired(f));
|
|
208
|
+
}
|
|
209
|
+
get(id) {
|
|
210
|
+
return this.rows.find((f) => f.id === id);
|
|
211
|
+
}
|
|
212
|
+
nextId() {
|
|
213
|
+
const highest = this.rows.reduce((max, f) => Math.max(max, factNumber(f.id)), 0);
|
|
214
|
+
return formatFactId(highest + 1);
|
|
215
|
+
}
|
|
216
|
+
append(input) {
|
|
217
|
+
const fact = {
|
|
218
|
+
id: this.nextId(),
|
|
219
|
+
fact: input.fact.length > MAX_FACT_CHARS ? `${input.fact.slice(0, MAX_FACT_CHARS - 1)}…` : input.fact,
|
|
220
|
+
area: input.area,
|
|
221
|
+
repos: [...input.repos],
|
|
222
|
+
kind: input.kind,
|
|
223
|
+
confidence: input.confidence,
|
|
224
|
+
source: { ...input.source },
|
|
225
|
+
supersedes: input.supersedes ?? null,
|
|
226
|
+
superseded_by: null,
|
|
227
|
+
retired: input.retired ?? null
|
|
228
|
+
};
|
|
229
|
+
this.rows.push(fact);
|
|
230
|
+
return fact;
|
|
231
|
+
}
|
|
232
|
+
supersede(oldId, input) {
|
|
233
|
+
const index = this.rows.findIndex((f) => f.id === oldId);
|
|
234
|
+
if (index === -1)
|
|
235
|
+
throw new Error(`cannot supersede ${oldId}: no such fact`);
|
|
236
|
+
const old = this.rows[index];
|
|
237
|
+
if (old.superseded_by !== null) {
|
|
238
|
+
throw new Error(`${oldId} is already superseded by ${old.superseded_by}`);
|
|
239
|
+
}
|
|
240
|
+
if (isRetired(old))
|
|
241
|
+
throw new Error(`${oldId} is retired; a retired fact is not superseded`);
|
|
242
|
+
const replacement = this.append({ ...input, supersedes: oldId });
|
|
243
|
+
this.rows[index] = { ...old, superseded_by: replacement.id };
|
|
244
|
+
return replacement;
|
|
245
|
+
}
|
|
246
|
+
retire(id, retirement) {
|
|
247
|
+
const index = this.rows.findIndex((f) => f.id === id);
|
|
248
|
+
if (index === -1)
|
|
249
|
+
throw new Error(`cannot retire ${id}: no such fact`);
|
|
250
|
+
const fact = this.rows[index];
|
|
251
|
+
if (fact.superseded_by !== null) {
|
|
252
|
+
throw new Error(`${id} is superseded by ${fact.superseded_by}; a superseded fact is not retired`);
|
|
253
|
+
}
|
|
254
|
+
const retired = { ...fact, retired: retirement };
|
|
255
|
+
this.rows[index] = retired;
|
|
256
|
+
return retired;
|
|
257
|
+
}
|
|
258
|
+
findDuplicate(question, area, threshold) {
|
|
259
|
+
return findDuplicate(question, area, this.active, threshold);
|
|
260
|
+
}
|
|
261
|
+
toFile() {
|
|
262
|
+
return { version: this.version, facts: this.rows };
|
|
263
|
+
}
|
|
264
|
+
toYaml() {
|
|
265
|
+
return emitFactsYaml(this.toFile(), existsSync(this.path) ? undefined : NEW_FILE_HEADER);
|
|
266
|
+
}
|
|
267
|
+
save(path = this.path) {
|
|
268
|
+
const text = this.toYaml();
|
|
269
|
+
const validation = validateFactsFile(parseYaml(text));
|
|
270
|
+
if (!validation.ok) {
|
|
271
|
+
const first = validation.issues[0];
|
|
272
|
+
throw new Error(`refusing to write an invalid facts.yml: ${first?.path ?? ""} ${first?.message ?? ""}`);
|
|
273
|
+
}
|
|
274
|
+
withWorkspaceLock(workspaceRootOfFactsPath(path), () => {
|
|
275
|
+
writeFactsAtomic(path, text);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
static update(path, fn) {
|
|
279
|
+
return withWorkspaceLock(workspaceRootOfFactsPath(path), () => {
|
|
280
|
+
const store = FactsStore.loadOrEmpty(path);
|
|
281
|
+
const value = fn(store);
|
|
282
|
+
store.save(path);
|
|
283
|
+
return value;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function writeFactsAtomic(path, text) {
|
|
288
|
+
const temp = `${path}.tmp-${String(process.pid)}`;
|
|
289
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
290
|
+
try {
|
|
291
|
+
writeFileSync(temp, text, "utf8");
|
|
292
|
+
renameSync(temp, path);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
try {
|
|
295
|
+
if (existsSync(temp))
|
|
296
|
+
rmSync(temp, { force: true });
|
|
297
|
+
} catch {}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { MAX_FACT_CHARS, FactsStore };
|