tldr-experts 0.3.0 → 0.4.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 +1957 -0
- package/README.md +99 -12
- package/dist/hooks/answer-capture.js +21 -12
- package/dist/hooks/budget-gate.js +95 -11
- package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
- package/dist/hooks/chunk-4cp363kv.js +1766 -0
- package/dist/hooks/{chunk-t8tdv11p.js → chunk-7y2dq0pj.js} +1 -1
- package/dist/hooks/chunk-9zsqxr6y.js +213 -0
- package/dist/hooks/{chunk-a8p2rc94.js → chunk-b8kxzna2.js} +1 -1
- package/dist/hooks/{chunk-j234zf0t.js → chunk-c6t5nx0r.js} +37 -166
- package/dist/hooks/{chunk-g395gk7e.js → chunk-m3mewgnw.js} +36 -17
- package/dist/hooks/chunk-phmdk72a.js +164 -0
- package/dist/hooks/chunk-rpcxsqh3.js +199 -0
- package/dist/hooks/{chunk-azctppjh.js → chunk-rz541e2b.js} +7 -1
- package/dist/hooks/{chunk-y0jdr3et.js → chunk-s1c5h7yx.js} +71 -29
- package/dist/hooks/{chunk-p274ckxv.js → chunk-sq44k6g2.js} +155 -12
- package/dist/hooks/{chunk-x98qs959.js → chunk-t1ywrfr4.js} +35 -42
- package/dist/hooks/{chunk-1zwcxd3f.js → chunk-tzzwddct.js} +1 -1
- package/dist/hooks/claim-sources.js +31 -23
- package/dist/hooks/dod-gate.js +12 -37
- package/dist/hooks/no-reask.js +9 -9
- package/dist/hooks/session-start.js +176 -422
- package/dist/hooks/statusline.js +14 -11
- package/dist/tldrx.js +22798 -12309
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/tldrx/SKILL.md +16 -3
- package/stages/build/stage.md +5 -0
- package/stages/build/stage.yml +2 -1
- package/stages/plan/stage.md +11 -0
- package/templates/budget.yml +5 -0
- package/templates/epic.md +5 -3
- package/dist/hooks/chunk-ae6bkfs5.js +0 -0
- package/dist/hooks/chunk-kw4tffzf.js +0 -139
- package/dist/hooks/chunk-sdjnnmzz.js +0 -497
- package/dist/hooks/chunk-t56k6146.js +0 -14
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasSrcMarker,
|
|
3
|
+
parseSrcToken,
|
|
4
|
+
resolveSrc
|
|
5
|
+
} from "./chunk-m3mewgnw.js";
|
|
6
|
+
|
|
7
|
+
// src/core/text/handoff.ts
|
|
8
|
+
var HANDOFF_SECTIONS = ["Findings", "Decisions", "Unknowns", "Evidence ledger"];
|
|
9
|
+
var H2_RE = /^##\s+(.+?)\s*$/;
|
|
10
|
+
var BULLET_RE = /^(?: {0,3}-|\d{1,9}[.)])\s+(\S.*)$/;
|
|
11
|
+
var CONTINUATION_RE = /^\s+\S/;
|
|
12
|
+
function parseHandoff(text) {
|
|
13
|
+
const lines = text.split(`
|
|
14
|
+
`);
|
|
15
|
+
const sections = [];
|
|
16
|
+
const headings = [];
|
|
17
|
+
let current = null;
|
|
18
|
+
let pending = null;
|
|
19
|
+
const flushBullet = () => {
|
|
20
|
+
if (pending === null)
|
|
21
|
+
return;
|
|
22
|
+
if (current !== null) {
|
|
23
|
+
const joined = pending.parts.join(" ");
|
|
24
|
+
current.bullets.push({ line: pending.line, text: joined, token: parseSrcToken(joined) });
|
|
25
|
+
}
|
|
26
|
+
pending = null;
|
|
27
|
+
};
|
|
28
|
+
const flush = () => {
|
|
29
|
+
flushBullet();
|
|
30
|
+
if (current !== null)
|
|
31
|
+
sections.push(current);
|
|
32
|
+
current = null;
|
|
33
|
+
};
|
|
34
|
+
for (let i = 0;i < lines.length; i++) {
|
|
35
|
+
const line = lines[i] ?? "";
|
|
36
|
+
const heading = H2_RE.exec(line);
|
|
37
|
+
if (heading !== null && heading[1] !== undefined) {
|
|
38
|
+
flush();
|
|
39
|
+
headings.push(heading[1]);
|
|
40
|
+
current = { name: heading[1], headingLine: i + 1, bullets: [] };
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (line.startsWith("# ")) {
|
|
44
|
+
flush();
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (current === null)
|
|
48
|
+
continue;
|
|
49
|
+
const bullet = BULLET_RE.exec(line);
|
|
50
|
+
if (bullet !== null && bullet[1] !== undefined) {
|
|
51
|
+
flushBullet();
|
|
52
|
+
pending = { line: i + 1, parts: [bullet[1]] };
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (pending !== null && CONTINUATION_RE.test(line)) {
|
|
56
|
+
pending.parts.push(line.trim());
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
flushBullet();
|
|
60
|
+
}
|
|
61
|
+
flush();
|
|
62
|
+
return { sections, headings };
|
|
63
|
+
}
|
|
64
|
+
function parseItems(text) {
|
|
65
|
+
const items = [];
|
|
66
|
+
let section = "";
|
|
67
|
+
let pending = null;
|
|
68
|
+
const flush = () => {
|
|
69
|
+
if (pending === null)
|
|
70
|
+
return;
|
|
71
|
+
const joined = pending.parts.join(" ");
|
|
72
|
+
items.push({ line: pending.line, text: joined, section: pending.section, token: parseSrcToken(joined) });
|
|
73
|
+
pending = null;
|
|
74
|
+
};
|
|
75
|
+
const lines = text.split(`
|
|
76
|
+
`);
|
|
77
|
+
for (let i = 0;i < lines.length; i++) {
|
|
78
|
+
const line = lines[i] ?? "";
|
|
79
|
+
const heading = H2_RE.exec(line);
|
|
80
|
+
if (heading !== null && heading[1] !== undefined) {
|
|
81
|
+
flush();
|
|
82
|
+
section = heading[1];
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const bullet = BULLET_RE.exec(line);
|
|
86
|
+
if (bullet !== null && bullet[1] !== undefined) {
|
|
87
|
+
flush();
|
|
88
|
+
pending = { line: i + 1, parts: [bullet[1]], section };
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (pending !== null && CONTINUATION_RE.test(line)) {
|
|
92
|
+
pending.parts.push(line.trim());
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
flush();
|
|
96
|
+
}
|
|
97
|
+
flush();
|
|
98
|
+
return items;
|
|
99
|
+
}
|
|
100
|
+
function isHandoff(text) {
|
|
101
|
+
return missingSections(parseHandoff(text)).length === 0;
|
|
102
|
+
}
|
|
103
|
+
function missingSections(handoff, required = HANDOFF_SECTIONS) {
|
|
104
|
+
const present = handoff.headings;
|
|
105
|
+
const missing = [];
|
|
106
|
+
let cursor = 0;
|
|
107
|
+
for (const name of required) {
|
|
108
|
+
const at = present.indexOf(name, cursor);
|
|
109
|
+
if (at === -1)
|
|
110
|
+
missing.push(name);
|
|
111
|
+
else
|
|
112
|
+
cursor = at + 1;
|
|
113
|
+
}
|
|
114
|
+
return missing;
|
|
115
|
+
}
|
|
116
|
+
var MAX_BULLETS = 200;
|
|
117
|
+
function validateSections(handoff, required, ctx, lineOffset = 0) {
|
|
118
|
+
const emptySections = [];
|
|
119
|
+
const unsourced = [];
|
|
120
|
+
const malformed = [];
|
|
121
|
+
const unresolved = [];
|
|
122
|
+
const unverified = [];
|
|
123
|
+
let bulletCount = 0;
|
|
124
|
+
const wanted = new Set(required);
|
|
125
|
+
for (const section of handoff.sections) {
|
|
126
|
+
if (!wanted.has(section.name))
|
|
127
|
+
continue;
|
|
128
|
+
if (section.bullets.length === 0) {
|
|
129
|
+
emptySections.push({ name: section.name, line: section.headingLine + lineOffset });
|
|
130
|
+
}
|
|
131
|
+
for (const bullet of section.bullets) {
|
|
132
|
+
bulletCount++;
|
|
133
|
+
const line = bullet.line + lineOffset;
|
|
134
|
+
if (bullet.token === null) {
|
|
135
|
+
if (hasSrcMarker(bullet.text)) {
|
|
136
|
+
malformed.push({
|
|
137
|
+
line,
|
|
138
|
+
message: "malformed citation — the `[src: …]` token must be the last thing on the line " + "(closing quotes, brackets and a final `.` are allowed after it, words are not)"
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
unsourced.push(line);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
for (const error of bullet.token.errors) {
|
|
146
|
+
unresolved.push({ line, message: `[src: ${error.raw}] — ${error.message}` });
|
|
147
|
+
}
|
|
148
|
+
const claim = bullet.text.replace(bullet.token.raw, " ").trim();
|
|
149
|
+
for (const ref of bullet.token.refs) {
|
|
150
|
+
const resolution = resolveSrc(ref, ctx, section.name, claim);
|
|
151
|
+
const issue = { line, message: `[src: ${ref.raw}] — ${resolution.message ?? "unresolvable"}` };
|
|
152
|
+
if (resolution.outcome === "refused")
|
|
153
|
+
unresolved.push(issue);
|
|
154
|
+
else if (resolution.outcome === "unverified")
|
|
155
|
+
unverified.push(issue);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { emptySections, unsourced, malformed, unresolved, unverified, bulletCount };
|
|
160
|
+
}
|
|
161
|
+
function validateHandoff(text, ctx) {
|
|
162
|
+
const handoff = parseHandoff(text);
|
|
163
|
+
const missing = missingSections(handoff);
|
|
164
|
+
const report = validateSections(handoff, HANDOFF_SECTIONS, ctx);
|
|
165
|
+
const unresolved = [...report.unresolved];
|
|
166
|
+
if (report.bulletCount > MAX_BULLETS) {
|
|
167
|
+
unresolved.push({ line: 0, message: `${report.bulletCount} bullets exceeds the ${MAX_BULLETS} cap` });
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: missing.length === 0 && report.emptySections.length === 0 && report.unsourced.length === 0 && report.malformed.length === 0 && unresolved.length === 0,
|
|
171
|
+
missingSections: missing,
|
|
172
|
+
emptySections: report.emptySections,
|
|
173
|
+
unsourced: report.unsourced,
|
|
174
|
+
malformed: report.malformed,
|
|
175
|
+
unresolved,
|
|
176
|
+
unverified: report.unverified,
|
|
177
|
+
bulletCount: report.bulletCount
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function validateCitations(text, ctx) {
|
|
181
|
+
const malformed = [];
|
|
182
|
+
const unresolved = [];
|
|
183
|
+
const unverified = [];
|
|
184
|
+
let cited = 0;
|
|
185
|
+
for (const item of parseItems(text)) {
|
|
186
|
+
if (item.token === null) {
|
|
187
|
+
if (!hasSrcMarker(item.text))
|
|
188
|
+
continue;
|
|
189
|
+
cited++;
|
|
190
|
+
malformed.push({
|
|
191
|
+
line: item.line,
|
|
192
|
+
message: "malformed citation — the `[src: …]` token must be the last thing on the line " + "(closing quotes, brackets and a final `.` are allowed after it, words are not)"
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
cited++;
|
|
197
|
+
for (const error of item.token.errors) {
|
|
198
|
+
unresolved.push({ line: item.line, message: `[src: ${error.raw}] — ${error.message}` });
|
|
199
|
+
}
|
|
200
|
+
const claim = item.text.replace(item.token.raw, " ").trim();
|
|
201
|
+
for (const ref of item.token.refs) {
|
|
202
|
+
const resolution = resolveSrc(ref, ctx, item.section, claim);
|
|
203
|
+
const issue = { line: item.line, message: `[src: ${ref.raw}] — ${resolution.message ?? "unresolvable"}` };
|
|
204
|
+
if (resolution.outcome === "refused")
|
|
205
|
+
unresolved.push(issue);
|
|
206
|
+
else if (resolution.outcome === "unverified")
|
|
207
|
+
unverified.push(issue);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { malformed, unresolved, unverified, cited };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { parseHandoff, isHandoff, validateHandoff, validateCitations };
|
|
@@ -1,170 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseSrcToken
|
|
3
|
-
} from "./chunk-g395gk7e.js";
|
|
4
1
|
import {
|
|
5
2
|
PROJECT_FRAMEWORK_DIR,
|
|
6
3
|
PROJECT_WORK_DIR
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
|
|
9
|
-
// src/core/text/questions.ts
|
|
10
|
-
var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
|
|
11
|
-
var HEADING_RE = /^##\s+(Q\d{1,6})\s+·\s+(.+?)\s*$/;
|
|
12
|
-
var METADATA_RE = /^<!--\s*(.*?)\s*-->$/;
|
|
13
|
-
var WHY_RE = /^Why asked:\s*(.*)$/;
|
|
14
|
-
var OPTION_RE = /^-\s+([A-E])\)\s*(.*)$/;
|
|
15
|
-
var ANSWER_RE = /^\[Answer\]:[ \t]*(\S.*)$/;
|
|
16
|
-
var ANSWER_SLOT_RE = /^\[Answer\]:/;
|
|
17
|
-
var FOOTER_KEYS = ["answered_by", "answered_at", "fact"];
|
|
18
|
-
function parseQuestions(text) {
|
|
19
|
-
const trailingNewline = text.endsWith(`
|
|
20
|
-
`);
|
|
21
|
-
const body = trailingNewline ? text.slice(0, -1) : text;
|
|
22
|
-
const lines = body === "" ? [] : body.split(`
|
|
23
|
-
`);
|
|
24
|
-
const preamble = [];
|
|
25
|
-
const blocks = [];
|
|
26
|
-
let pending = null;
|
|
27
|
-
const close = () => {
|
|
28
|
-
if (pending === null)
|
|
29
|
-
return;
|
|
30
|
-
blocks.push(buildBlock(pending.start + 1, pending.lines));
|
|
31
|
-
pending = null;
|
|
32
|
-
};
|
|
33
|
-
for (let i = 0;i < lines.length; i++) {
|
|
34
|
-
const line = lines[i] ?? "";
|
|
35
|
-
if (HEADING_RE.test(line)) {
|
|
36
|
-
close();
|
|
37
|
-
pending = { start: i, lines: [line] };
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (pending === null)
|
|
41
|
-
preamble.push(line);
|
|
42
|
-
else
|
|
43
|
-
pending.lines.push(line);
|
|
44
|
-
}
|
|
45
|
-
close();
|
|
46
|
-
return { preamble, blocks, trailingNewline };
|
|
47
|
-
}
|
|
48
|
-
function buildBlock(startLine, lines) {
|
|
49
|
-
const heading = HEADING_RE.exec(lines[0] ?? "");
|
|
50
|
-
const id = heading?.[1] ?? "";
|
|
51
|
-
const title = heading?.[2] ?? "";
|
|
52
|
-
let metadata = null;
|
|
53
|
-
let metadataIndex = -1;
|
|
54
|
-
let whyAsked = null;
|
|
55
|
-
let whySrc = null;
|
|
56
|
-
const options = [];
|
|
57
|
-
let answer = "";
|
|
58
|
-
let answerIndex = -1;
|
|
59
|
-
let footer = null;
|
|
60
|
-
for (let i = 1;i < lines.length; i++) {
|
|
61
|
-
const line = lines[i] ?? "";
|
|
62
|
-
const comment = METADATA_RE.exec(line);
|
|
63
|
-
if (comment !== null && comment[1] !== undefined) {
|
|
64
|
-
const pairs = parsePipeComment(comment[1]);
|
|
65
|
-
if (metadata === null && pairs.some(([k]) => k === "id")) {
|
|
66
|
-
metadata = toMetadata(pairs);
|
|
67
|
-
metadataIndex = i;
|
|
68
|
-
} else if (footer === null && pairs.some(([k]) => k === "answered_by")) {
|
|
69
|
-
footer = toFooter(pairs);
|
|
70
|
-
}
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const why = WHY_RE.exec(line);
|
|
74
|
-
if (why !== null && whyAsked === null) {
|
|
75
|
-
whyAsked = why[1] ?? "";
|
|
76
|
-
whySrc = parseSrcToken(line);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const option = OPTION_RE.exec(line);
|
|
80
|
-
if (option !== null && option[1] !== undefined && option[2] !== undefined) {
|
|
81
|
-
options.push({ letter: option[1], text: option[2] });
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (ANSWER_SLOT_RE.test(line) && answerIndex === -1) {
|
|
85
|
-
answerIndex = i;
|
|
86
|
-
const captured = ANSWER_RE.exec(line);
|
|
87
|
-
answer = (captured?.[1] ?? "").trim();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
id,
|
|
92
|
-
title,
|
|
93
|
-
metadata,
|
|
94
|
-
metadataIndex,
|
|
95
|
-
whyAsked,
|
|
96
|
-
whySrc,
|
|
97
|
-
options,
|
|
98
|
-
answer,
|
|
99
|
-
answerIndex,
|
|
100
|
-
footer,
|
|
101
|
-
startLine,
|
|
102
|
-
lines: [...lines]
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function parsePipeComment(inner) {
|
|
106
|
-
const pairs = [];
|
|
107
|
-
for (const part of inner.split("|")) {
|
|
108
|
-
const colon = part.indexOf(":");
|
|
109
|
-
if (colon === -1)
|
|
110
|
-
continue;
|
|
111
|
-
pairs.push([part.slice(0, colon).trim(), part.slice(colon + 1).trim()]);
|
|
112
|
-
}
|
|
113
|
-
return pairs;
|
|
114
|
-
}
|
|
115
|
-
function toMetadata(pairs) {
|
|
116
|
-
const map = new Map(pairs);
|
|
117
|
-
const extra = pairs.filter(([k]) => !REQUIRED_METADATA_KEYS.includes(k));
|
|
118
|
-
return {
|
|
119
|
-
id: map.get("id") ?? "",
|
|
120
|
-
status: map.get("status") ?? "",
|
|
121
|
-
area: map.get("area") ?? "",
|
|
122
|
-
asked_by: map.get("asked_by") ?? "",
|
|
123
|
-
asked_at: map.get("asked_at") ?? "",
|
|
124
|
-
extra
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function toFooter(pairs) {
|
|
128
|
-
const map = new Map(pairs);
|
|
129
|
-
return {
|
|
130
|
-
answered_by: map.get("answered_by") ?? "",
|
|
131
|
-
answered_at: map.get("answered_at") ?? "",
|
|
132
|
-
fact: map.get("fact") ?? ""
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function serializeQuestions(doc) {
|
|
136
|
-
const out = [...doc.preamble];
|
|
137
|
-
for (const block of doc.blocks)
|
|
138
|
-
out.push(...block.lines);
|
|
139
|
-
const text = out.join(`
|
|
140
|
-
`);
|
|
141
|
-
return doc.trailingNewline ? `${text}
|
|
142
|
-
` : text;
|
|
143
|
-
}
|
|
144
|
-
function detectAnswered(blocks) {
|
|
145
|
-
return blocks.filter((b) => b.metadata?.status === "open" && b.answer !== "");
|
|
146
|
-
}
|
|
147
|
-
function openBlocks(blocks) {
|
|
148
|
-
return blocks.filter((b) => b.metadata?.status === "open");
|
|
149
|
-
}
|
|
150
|
-
function recordAnswer(block, footer) {
|
|
151
|
-
const lines = [...block.lines];
|
|
152
|
-
if (block.metadataIndex >= 0) {
|
|
153
|
-
const line = lines[block.metadataIndex] ?? "";
|
|
154
|
-
lines[block.metadataIndex] = line.replace(/(\|\s*status:\s*)open(\s*\|)/, "$1answered$2");
|
|
155
|
-
}
|
|
156
|
-
const footerLine = `<!-- ${FOOTER_KEYS.map((k) => `${k}: ${footer[k]}`).join(" | ")} -->`;
|
|
157
|
-
const insertAt = block.answerIndex >= 0 ? block.answerIndex + 1 : lines.length;
|
|
158
|
-
lines.splice(insertAt, 0, footerLine);
|
|
159
|
-
const metadata = block.metadata === null ? null : { ...block.metadata, status: "answered" };
|
|
160
|
-
return { ...block, lines, metadata, footer };
|
|
161
|
-
}
|
|
162
|
-
function replaceBlock(doc, block) {
|
|
163
|
-
return {
|
|
164
|
-
...doc,
|
|
165
|
-
blocks: doc.blocks.map((b) => b.id === block.id ? block : b)
|
|
166
|
-
};
|
|
167
|
-
}
|
|
4
|
+
} from "./chunk-sq44k6g2.js";
|
|
168
5
|
|
|
169
6
|
// src/core/lock/workspaceLock.ts
|
|
170
7
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, rmSync as rmSync2, writeSync, closeSync } from "node:fs";
|
|
@@ -290,6 +127,8 @@ function withWorkspaceLock(root, fn, timeoutMs = WORKSPACE_LOCK_TIMEOUT_MS) {
|
|
|
290
127
|
// src/core/facts/emitFactsYaml.ts
|
|
291
128
|
var PLAIN_SAFE = /^[A-Za-z][A-Za-z0-9_./-]*$/;
|
|
292
129
|
var YAML_KEYWORDS = new Set(["true", "false", "null", "yes", "no", "on", "off", "~"]);
|
|
130
|
+
var LONE_HIGH_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g;
|
|
131
|
+
var LONE_LOW_SURROGATE = /(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
293
132
|
function yamlScalar(value) {
|
|
294
133
|
if (value === null)
|
|
295
134
|
return "null";
|
|
@@ -297,7 +136,7 @@ function yamlScalar(value) {
|
|
|
297
136
|
return String(value);
|
|
298
137
|
if (PLAIN_SAFE.test(value) && !YAML_KEYWORDS.has(value.toLowerCase()))
|
|
299
138
|
return value;
|
|
300
|
-
return
|
|
139
|
+
return JSON.stringify(value.replace(LONE_HIGH_SURROGATE, "�").replace(LONE_LOW_SURROGATE, "�"));
|
|
301
140
|
}
|
|
302
141
|
function inlineList(values) {
|
|
303
142
|
return `[${values.map((v) => yamlScalar(v)).join(", ")}]`;
|
|
@@ -315,6 +154,8 @@ function emitFact(fact, indent = " ") {
|
|
|
315
154
|
`${inner}supersedes: ${yamlScalar(fact.supersedes)}`,
|
|
316
155
|
`${inner}superseded_by: ${yamlScalar(fact.superseded_by)}`
|
|
317
156
|
];
|
|
157
|
+
if (fact.truncated === true)
|
|
158
|
+
lines.push(`${inner}truncated: true`);
|
|
318
159
|
if (fact.retired === null) {
|
|
319
160
|
lines.push(`${inner}retired: null`);
|
|
320
161
|
} else {
|
|
@@ -336,4 +177,34 @@ function emitFactsYaml(file, header) {
|
|
|
336
177
|
`;
|
|
337
178
|
}
|
|
338
179
|
|
|
339
|
-
|
|
180
|
+
// src/core/fs/writeAtomic.ts
|
|
181
|
+
import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
182
|
+
import { dirname as dirname3 } from "node:path";
|
|
183
|
+
var BACKUP_SUFFIX = ".bak";
|
|
184
|
+
function backupPathFor(path) {
|
|
185
|
+
return `${path}${BACKUP_SUFFIX}`;
|
|
186
|
+
}
|
|
187
|
+
function writeAtomic(path, content) {
|
|
188
|
+
const temp = `${path}.tmp-${String(process.pid)}`;
|
|
189
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync2(temp, content, "utf8");
|
|
192
|
+
keepPreviousVersion(path);
|
|
193
|
+
renameSync(temp, path);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
try {
|
|
196
|
+
if (existsSync3(temp))
|
|
197
|
+
rmSync3(temp, { force: true });
|
|
198
|
+
} catch {}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function keepPreviousVersion(path) {
|
|
203
|
+
if (!existsSync3(path))
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
copyFileSync(path, backupPathFor(path));
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { readLock, isAlive, workspaceRootOfRunDir, workspaceRootOfFactsPath, withWorkspaceLock, yamlScalar, emitFactsYaml, backupPathFor, writeAtomic };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROJECT_FRAMEWORK_DIR,
|
|
3
3
|
parseYaml
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-sq44k6g2.js";
|
|
5
5
|
|
|
6
6
|
// src/core/text/srcToken.ts
|
|
7
7
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
@@ -164,16 +164,20 @@ function resolveSrc(ref, ctx, section, claim = "") {
|
|
|
164
164
|
return refused(`unknown repo \`${ref.repo}\` (not in workspace.yml)`);
|
|
165
165
|
}
|
|
166
166
|
const bases = fileBases(ref, ctx);
|
|
167
|
+
let short = null;
|
|
167
168
|
for (const base of bases) {
|
|
168
169
|
if (!existsSync(base.abs) || !statSync(base.abs).isFile())
|
|
169
170
|
continue;
|
|
170
171
|
const lines = countLines(base.abs);
|
|
171
172
|
const highest = ref.endLine ?? ref.startLine;
|
|
172
173
|
if (highest > lines) {
|
|
173
|
-
|
|
174
|
+
short ??= refused(`${ref.path} has ${lines} line(s); cited line ${highest}`, base.abs);
|
|
175
|
+
continue;
|
|
174
176
|
}
|
|
175
177
|
return { ok: true, outcome: "ok", resolved: base.abs };
|
|
176
178
|
}
|
|
179
|
+
if (short !== null)
|
|
180
|
+
return short;
|
|
177
181
|
const tried = bases.map((b) => b.label).join(", ");
|
|
178
182
|
return refused(`no such file: ${ref.path} — tried ${tried}`, bases[0]?.abs);
|
|
179
183
|
}
|
|
@@ -270,26 +274,41 @@ function fileBases(ref, ctx) {
|
|
|
270
274
|
return [{ label: "the absolute path as written", abs: normalize(ref.path) }];
|
|
271
275
|
}
|
|
272
276
|
if (ref.repo !== null) {
|
|
273
|
-
const
|
|
274
|
-
return [
|
|
277
|
+
const rel2 = ctx.repos.get(ref.repo) ?? "";
|
|
278
|
+
return [
|
|
279
|
+
...epicBases(ctx, ref.repo, ref.path),
|
|
280
|
+
{ label: repoLabel(ref.repo, rel2), abs: normalize(join(ctx.root, rel2, ref.path)) }
|
|
281
|
+
];
|
|
275
282
|
}
|
|
276
|
-
const bases = [
|
|
277
|
-
|
|
278
|
-
|
|
283
|
+
const bases = [];
|
|
284
|
+
const slash = ref.path.indexOf("/");
|
|
285
|
+
const named = slash > 0 ? ref.path.slice(0, slash) : "";
|
|
286
|
+
const rel = named === "" ? undefined : ctx.repos.get(named);
|
|
287
|
+
if (rel !== undefined)
|
|
288
|
+
bases.push(...epicBases(ctx, named, ref.path.slice(slash + 1)));
|
|
289
|
+
bases.push({ label: "workspace root", abs: normalize(join(ctx.root, ref.path)) });
|
|
279
290
|
const runDir = ctx.runDir ?? null;
|
|
280
291
|
if (runDir !== null && runDir !== "") {
|
|
281
292
|
bases.push({ label: `run dir ${displayPath(ctx.root, runDir)}`, abs: normalize(join(runDir, ref.path)) });
|
|
282
293
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
if (rel !== undefined) {
|
|
295
|
+
bases.push({
|
|
296
|
+
label: repoLabel(named, rel),
|
|
297
|
+
abs: normalize(join(ctx.root, rel, ref.path.slice(slash + 1)))
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return bases;
|
|
301
|
+
}
|
|
302
|
+
function epicBases(ctx, repo, path) {
|
|
303
|
+
const trees = ctx.epicWorktrees ?? [];
|
|
304
|
+
const bases = [];
|
|
305
|
+
for (const tree of trees) {
|
|
306
|
+
if (tree.repo !== repo)
|
|
307
|
+
continue;
|
|
308
|
+
bases.push({
|
|
309
|
+
label: `epic worktree ${displayPath(ctx.root, tree.dir)}`,
|
|
310
|
+
abs: normalize(join(tree.dir, path))
|
|
311
|
+
});
|
|
293
312
|
}
|
|
294
313
|
return bases;
|
|
295
314
|
}
|