@dennisrongo/dsh-todo 0.4.1 → 0.5.1
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 +46 -0
- package/lib/bin.js +2 -0
- package/lib/cli.js +2 -0
- package/lib/client.js +152 -10
- package/lib/index.js +412 -8
- package/lib/scan.js +276 -0
- package/lib/suggest.js +93 -0
- package/lib/typert.host.js +58 -1
- package/package.json +2 -2
package/lib/scan.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// src/scan.ts
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join, relative, sep } from "node:path";
|
|
4
|
+
var DIGEST_BYTE_CAP = 24e3;
|
|
5
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
6
|
+
".git",
|
|
7
|
+
".hg",
|
|
8
|
+
".svn",
|
|
9
|
+
"node_modules",
|
|
10
|
+
"bower_components",
|
|
11
|
+
"jspm_packages",
|
|
12
|
+
"lib",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
"out",
|
|
16
|
+
"coverage",
|
|
17
|
+
".next",
|
|
18
|
+
".nuxt",
|
|
19
|
+
".svelte-kit",
|
|
20
|
+
".output",
|
|
21
|
+
".parcel-cache",
|
|
22
|
+
".turbo",
|
|
23
|
+
".cache",
|
|
24
|
+
".venv",
|
|
25
|
+
"venv",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
".tox",
|
|
28
|
+
".mypy_cache",
|
|
29
|
+
".pytest_cache",
|
|
30
|
+
"target",
|
|
31
|
+
"vendor",
|
|
32
|
+
"vendored",
|
|
33
|
+
"third_party",
|
|
34
|
+
"thirdparty",
|
|
35
|
+
"generated",
|
|
36
|
+
"__generated__",
|
|
37
|
+
"Pods",
|
|
38
|
+
"Carthage",
|
|
39
|
+
"DerivedData"
|
|
40
|
+
]);
|
|
41
|
+
var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
42
|
+
".ts",
|
|
43
|
+
".tsx",
|
|
44
|
+
".js",
|
|
45
|
+
".jsx",
|
|
46
|
+
".mjs",
|
|
47
|
+
".cjs",
|
|
48
|
+
".py",
|
|
49
|
+
".go",
|
|
50
|
+
".rs",
|
|
51
|
+
".java",
|
|
52
|
+
".rb",
|
|
53
|
+
".php",
|
|
54
|
+
".cs",
|
|
55
|
+
".swift",
|
|
56
|
+
".kt",
|
|
57
|
+
".scala",
|
|
58
|
+
".sh"
|
|
59
|
+
]);
|
|
60
|
+
var MAX_FILES_WALKED = 4e3;
|
|
61
|
+
var MAX_TREE_ENTRIES = 300;
|
|
62
|
+
var MAX_COMMENTS = 80;
|
|
63
|
+
var MAX_UNTESTED = 40;
|
|
64
|
+
var MAX_COMMENT_LINE = 160;
|
|
65
|
+
var README_BYTES = 4e3;
|
|
66
|
+
var MANIFEST_BYTES = 2e3;
|
|
67
|
+
var MAX_DEPTH = 8;
|
|
68
|
+
var SCAN_CEILING_FACTOR = 10;
|
|
69
|
+
var MAX_FILES_READ = 400;
|
|
70
|
+
var MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
71
|
+
function posix(path) {
|
|
72
|
+
return path.split(sep).join("/");
|
|
73
|
+
}
|
|
74
|
+
function walk(root) {
|
|
75
|
+
const files = [];
|
|
76
|
+
let truncated = false;
|
|
77
|
+
const visit = (dir, depth) => {
|
|
78
|
+
if (depth > MAX_DEPTH || files.length >= MAX_FILES_WALKED) {
|
|
79
|
+
truncated = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (files.length >= MAX_FILES_WALKED) {
|
|
90
|
+
truncated = true;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
95
|
+
visit(join(dir, entry.name), depth + 1);
|
|
96
|
+
} else if (entry.isFile()) {
|
|
97
|
+
files.push(posix(relative(root, join(dir, entry.name))));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
if (!statSync(root).isDirectory()) return { files: [], truncated: false };
|
|
103
|
+
} catch {
|
|
104
|
+
return { files: [], truncated: false };
|
|
105
|
+
}
|
|
106
|
+
visit(root, 0);
|
|
107
|
+
return { files, truncated };
|
|
108
|
+
}
|
|
109
|
+
function readText(path, limit = Number.MAX_SAFE_INTEGER) {
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
if (statSync(path).size > MAX_READ_BYTES) return "";
|
|
113
|
+
raw = readFileSync(path, "utf8");
|
|
114
|
+
} catch {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
if (raw.includes("\0")) return "";
|
|
118
|
+
return raw.length > limit ? raw.slice(0, limit) : raw;
|
|
119
|
+
}
|
|
120
|
+
function skippedForSize(path) {
|
|
121
|
+
try {
|
|
122
|
+
return statSync(path).size > MAX_READ_BYTES;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
var COMMENT_RE = /(?:^|\s)(?:\/\/|#|\/\*|\*)\s*(TODO|FIXME|HACK)\b[:\s]?(.*)$/;
|
|
128
|
+
function collectComments(root, files) {
|
|
129
|
+
const ceiling = MAX_COMMENTS * SCAN_CEILING_FACTOR;
|
|
130
|
+
const kept = [];
|
|
131
|
+
let total = 0;
|
|
132
|
+
let read = 0;
|
|
133
|
+
let skipped = 0;
|
|
134
|
+
let bounded = false;
|
|
135
|
+
for (const rel of files) {
|
|
136
|
+
const dot = rel.lastIndexOf(".");
|
|
137
|
+
if (dot < 0 || !SOURCE_EXT.has(rel.slice(dot))) continue;
|
|
138
|
+
if (total >= ceiling || read >= MAX_FILES_READ) {
|
|
139
|
+
bounded = true;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
const full = join(root, rel);
|
|
143
|
+
if (skippedForSize(full)) {
|
|
144
|
+
skipped += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
read += 1;
|
|
148
|
+
const text = readText(full);
|
|
149
|
+
if (text === "") continue;
|
|
150
|
+
const lines = text.split(/\r?\n/);
|
|
151
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
152
|
+
const match = COMMENT_RE.exec(lines[i]);
|
|
153
|
+
if (match === null) continue;
|
|
154
|
+
total += 1;
|
|
155
|
+
if (kept.length >= MAX_COMMENTS) continue;
|
|
156
|
+
const body = match[2].trim().slice(0, MAX_COMMENT_LINE);
|
|
157
|
+
kept.push(`${rel}:${i + 1} ${match[1]} ${body}`.trimEnd());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { kept, total, bounded, skippedForSize: skipped };
|
|
161
|
+
}
|
|
162
|
+
function hasTest(base, testNames) {
|
|
163
|
+
return testNames.has(`${base}.test`) || testNames.has(`${base}.spec`) || testNames.has(`test_${base}`) || testNames.has(`${base}_test`) || testNames.has(base);
|
|
164
|
+
}
|
|
165
|
+
function collectUntested(files) {
|
|
166
|
+
const testNames = /* @__PURE__ */ new Set();
|
|
167
|
+
for (const rel of files) {
|
|
168
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
169
|
+
const stem = name.replace(/\.[^.]+$/, "");
|
|
170
|
+
if (/(^|[./_-])(test|spec)([./_-]|$)/i.test(rel)) {
|
|
171
|
+
testNames.add(stem);
|
|
172
|
+
testNames.add(stem.replace(/\.(test|spec)$/i, ""));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const ceiling = MAX_UNTESTED * SCAN_CEILING_FACTOR;
|
|
176
|
+
const kept = [];
|
|
177
|
+
let total = 0;
|
|
178
|
+
let bounded = false;
|
|
179
|
+
for (const rel of files) {
|
|
180
|
+
const dot = rel.lastIndexOf(".");
|
|
181
|
+
if (dot < 0 || !SOURCE_EXT.has(rel.slice(dot))) continue;
|
|
182
|
+
if (/(^|[./_-])(test|spec)([./_-]|$)/i.test(rel)) continue;
|
|
183
|
+
const stem = rel.slice(rel.lastIndexOf("/") + 1).replace(/\.[^.]+$/, "");
|
|
184
|
+
if (/^(index|main|types|constants)$/i.test(stem)) continue;
|
|
185
|
+
if (hasTest(stem, testNames)) continue;
|
|
186
|
+
if (total >= ceiling) {
|
|
187
|
+
bounded = true;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
total += 1;
|
|
191
|
+
if (kept.length < MAX_UNTESTED) kept.push(rel);
|
|
192
|
+
}
|
|
193
|
+
return { kept, total, bounded, skippedForSize: 0 };
|
|
194
|
+
}
|
|
195
|
+
function sectionHeader(title, total, kept, options = {}) {
|
|
196
|
+
const bound = options.bounded === true ? "+" : "";
|
|
197
|
+
const skipped = options.skippedForSize ?? 0;
|
|
198
|
+
const note = skipped > 0 ? ` (${skipped} file(s) too large to read)` : "";
|
|
199
|
+
const counts = kept < total || options.bounded === true ? `(${total}${bound} found, showing ${kept})` : `(${total})`;
|
|
200
|
+
return `### ${title} ${counts}${note}`;
|
|
201
|
+
}
|
|
202
|
+
function fileHeader(name, text, limit) {
|
|
203
|
+
if (text.length < limit) return `### ${name}`;
|
|
204
|
+
return `### ${name} (clipped to first ${Math.round(limit / 1e3)} KB)`;
|
|
205
|
+
}
|
|
206
|
+
function assemble(sections, walkTruncated) {
|
|
207
|
+
const parts = walkTruncated ? sections.concat(
|
|
208
|
+
"[walk truncated \u2014 this workspace is deeper or larger than one scan walks; files below the depth or count limit were never examined]"
|
|
209
|
+
) : sections;
|
|
210
|
+
const joined = parts.join("\n\n");
|
|
211
|
+
if (joined.length <= DIGEST_BYTE_CAP) {
|
|
212
|
+
return { digest: joined, truncated: walkTruncated };
|
|
213
|
+
}
|
|
214
|
+
const marker = "\n\n[digest truncated \u2014 the workspace is larger than one scan can carry]";
|
|
215
|
+
return { digest: joined.slice(0, DIGEST_BYTE_CAP - marker.length) + marker, truncated: true };
|
|
216
|
+
}
|
|
217
|
+
function buildDigest(root) {
|
|
218
|
+
const { files, truncated } = walk(root);
|
|
219
|
+
const sections = [];
|
|
220
|
+
let sectionTruncated = false;
|
|
221
|
+
const tree = files.slice(0, MAX_TREE_ENTRIES);
|
|
222
|
+
if (tree.length > 0) {
|
|
223
|
+
if (tree.length < files.length) sectionTruncated = true;
|
|
224
|
+
sections.push(`${sectionHeader("Files", files.length, tree.length)}
|
|
225
|
+
${tree.join("\n")}`);
|
|
226
|
+
}
|
|
227
|
+
const readmeName = files.find((f) => /^readme(\.md|\.txt)?$/i.test(f));
|
|
228
|
+
if (readmeName !== void 0) {
|
|
229
|
+
const raw = readText(join(root, readmeName), README_BYTES);
|
|
230
|
+
const text = raw.trim();
|
|
231
|
+
if (text !== "") {
|
|
232
|
+
if (raw.length >= README_BYTES) sectionTruncated = true;
|
|
233
|
+
sections.push(`${fileHeader(readmeName, raw, README_BYTES)}
|
|
234
|
+
${text}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const manifest = files.find((f) => f === "package.json");
|
|
238
|
+
if (manifest !== void 0) {
|
|
239
|
+
const raw = readText(join(root, manifest), MANIFEST_BYTES);
|
|
240
|
+
const text = raw.trim();
|
|
241
|
+
if (text !== "") {
|
|
242
|
+
if (raw.length >= MANIFEST_BYTES) sectionTruncated = true;
|
|
243
|
+
sections.push(`${fileHeader("package.json", raw, MANIFEST_BYTES)}
|
|
244
|
+
${text}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const comments = collectComments(root, files);
|
|
248
|
+
if (comments.kept.length > 0 || comments.skippedForSize > 0) {
|
|
249
|
+
if (comments.kept.length < comments.total || comments.bounded || comments.skippedForSize > 0) sectionTruncated = true;
|
|
250
|
+
sections.push(
|
|
251
|
+
sectionHeader(
|
|
252
|
+
"Unresolved comments (TODO/FIXME/HACK)",
|
|
253
|
+
comments.total,
|
|
254
|
+
comments.kept.length,
|
|
255
|
+
{ bounded: comments.bounded, skippedForSize: comments.skippedForSize }
|
|
256
|
+
) + (comments.kept.length > 0 ? "\n" + comments.kept.join("\n") : "")
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const untested = collectUntested(files);
|
|
260
|
+
if (untested.kept.length > 0) {
|
|
261
|
+
if (untested.kept.length < untested.total || untested.bounded) sectionTruncated = true;
|
|
262
|
+
sections.push(
|
|
263
|
+
sectionHeader(
|
|
264
|
+
"Untested modules (name-based hint, not a coverage run)",
|
|
265
|
+
untested.total,
|
|
266
|
+
untested.kept.length,
|
|
267
|
+
{ bounded: untested.bounded }
|
|
268
|
+
) + "\n" + untested.kept.join("\n")
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return assemble(sections, truncated || sectionTruncated);
|
|
272
|
+
}
|
|
273
|
+
export {
|
|
274
|
+
DIGEST_BYTE_CAP,
|
|
275
|
+
buildDigest
|
|
276
|
+
};
|
package/lib/suggest.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var PRIORITIES = ["p0", "p1", "p2", "p3"];
|
|
3
|
+
var DEFAULT_PRIORITY = "p2";
|
|
4
|
+
function toPriority(value) {
|
|
5
|
+
return typeof value === "string" && PRIORITIES.includes(value) ? value : DEFAULT_PRIORITY;
|
|
6
|
+
}
|
|
7
|
+
var MAX_TEXT = 500;
|
|
8
|
+
var MAX_DESC = 5e3;
|
|
9
|
+
var MAX_LABEL = 60;
|
|
10
|
+
var SUGGESTIONS_DIR = ".dsh";
|
|
11
|
+
var SUGGESTIONS_FILE = `${SUGGESTIONS_DIR}/suggestions.json`;
|
|
12
|
+
function makeRunId(now = Date.now(), rand = Math.random) {
|
|
13
|
+
return `${now.toString(36)}${Math.floor(rand() * 1e6).toString(36)}`;
|
|
14
|
+
}
|
|
15
|
+
function suggestionsFileFor(runId) {
|
|
16
|
+
return `${SUGGESTIONS_DIR}/suggestions-${runId}.json`;
|
|
17
|
+
}
|
|
18
|
+
var MAX_SUGGESTIONS = 12;
|
|
19
|
+
|
|
20
|
+
// src/suggest.ts
|
|
21
|
+
function composeScanPrompt(digest, excludeTitles, runId) {
|
|
22
|
+
const parts = [
|
|
23
|
+
"# Propose work for this codebase",
|
|
24
|
+
"You are reviewing a workspace to propose concrete next tasks. Base every suggestion on the evidence below \u2014 do not speculate about code you cannot see.",
|
|
25
|
+
"Look for: unresolved TODO/FIXME/HACK comments, features the docs promise but the code does not implement, and modules with no tests.",
|
|
26
|
+
"## Evidence",
|
|
27
|
+
digest
|
|
28
|
+
];
|
|
29
|
+
const exclusions = excludeTitles.map((t) => t.trim()).filter((t) => t.length > 0);
|
|
30
|
+
if (exclusions.length > 0) {
|
|
31
|
+
parts.push(
|
|
32
|
+
"## Already planned \u2014 do NOT suggest these or close variants of them",
|
|
33
|
+
exclusions.map((t) => `- ${t}`).join("\n")
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
parts.push(
|
|
37
|
+
"## Output",
|
|
38
|
+
`Write ONLY a JSON array to \`${suggestionsFileFor(runId)}\` (create the directory if needed).`,
|
|
39
|
+
'Each element: {"title": string, "rationale": string, "priority": "p0"|"p1"|"p2"|"p3", "evidence": string}',
|
|
40
|
+
"`evidence` is a `file:line` pointer where one exists; omit it otherwise.",
|
|
41
|
+
`Produce at most ${MAX_SUGGESTIONS} suggestions. Write the file and stop \u2014 do not implement anything.`
|
|
42
|
+
);
|
|
43
|
+
return parts.join("\n\n");
|
|
44
|
+
}
|
|
45
|
+
function unfence(raw) {
|
|
46
|
+
const open = /```[ \t]*[A-Za-z0-9_-]*[ \t]*\r?\n?/.exec(raw);
|
|
47
|
+
if (open === null) return raw;
|
|
48
|
+
const lead = raw.slice(0, open.index);
|
|
49
|
+
if (lead.includes("[") || lead.includes("{")) return raw;
|
|
50
|
+
const body = raw.slice(open.index + open[0].length);
|
|
51
|
+
const close = body.lastIndexOf("```");
|
|
52
|
+
return close === -1 ? raw : body.slice(0, close);
|
|
53
|
+
}
|
|
54
|
+
function parseSuggestions(raw) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(unfence(raw));
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
return { ok: false, error: `the scan wrote invalid JSON: ${cause instanceof Error ? cause.message : String(cause)}` };
|
|
60
|
+
}
|
|
61
|
+
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.suggestions) ? parsed.suggestions : void 0;
|
|
62
|
+
if (list === void 0) {
|
|
63
|
+
return { ok: false, error: "the scan did not write a list of suggestions" };
|
|
64
|
+
}
|
|
65
|
+
const suggestions = [];
|
|
66
|
+
const seen = /* @__PURE__ */ new Set();
|
|
67
|
+
for (const entry of list) {
|
|
68
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
69
|
+
const row = entry;
|
|
70
|
+
const title = typeof row.title === "string" ? row.title.trim() : "";
|
|
71
|
+
if (title.length === 0) continue;
|
|
72
|
+
const stored = title.slice(0, MAX_TEXT);
|
|
73
|
+
const key = stored.toLowerCase();
|
|
74
|
+
if (seen.has(key)) continue;
|
|
75
|
+
seen.add(key);
|
|
76
|
+
const evidence = typeof row.evidence === "string" ? row.evidence.trim().slice(0, MAX_LABEL) : "";
|
|
77
|
+
suggestions.push({
|
|
78
|
+
title: stored,
|
|
79
|
+
rationale: typeof row.rationale === "string" ? row.rationale.trim().slice(0, MAX_DESC) : "",
|
|
80
|
+
priority: toPriority(row.priority),
|
|
81
|
+
// Absent optional fields are ABSENT KEYS, never '', matching TodoItem.
|
|
82
|
+
...evidence.length > 0 ? { evidence } : {}
|
|
83
|
+
});
|
|
84
|
+
if (suggestions.length >= MAX_SUGGESTIONS) break;
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, suggestions };
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
composeScanPrompt,
|
|
90
|
+
makeRunId,
|
|
91
|
+
parseSuggestions,
|
|
92
|
+
suggestionsFileFor
|
|
93
|
+
};
|
package/lib/typert.host.js
CHANGED
|
@@ -37,6 +37,26 @@ var replaceResultSchema = z.union([
|
|
|
37
37
|
list: todoListSchema
|
|
38
38
|
})
|
|
39
39
|
]);
|
|
40
|
+
var scanRequestSchema = z.object({ workspaceId: z.string() });
|
|
41
|
+
var readSuggestionsRequestSchema = z.object({
|
|
42
|
+
workspaceId: z.string(),
|
|
43
|
+
runId: z.string()
|
|
44
|
+
});
|
|
45
|
+
var scanDigestResultSchema = z.object({
|
|
46
|
+
digest: z.string(),
|
|
47
|
+
truncated: z.boolean()
|
|
48
|
+
});
|
|
49
|
+
var suggestionSchema = z.object({
|
|
50
|
+
title: z.string(),
|
|
51
|
+
rationale: z.string(),
|
|
52
|
+
priority: z.enum(["p0", "p1", "p2", "p3"]),
|
|
53
|
+
evidence: z.string().optional()
|
|
54
|
+
});
|
|
55
|
+
var readSuggestionsResultSchema = z.object({
|
|
56
|
+
status: z.enum(["pending", "ready", "error"]),
|
|
57
|
+
suggestions: z.array(suggestionSchema).optional(),
|
|
58
|
+
error: z.string().optional()
|
|
59
|
+
});
|
|
40
60
|
var PACKAGE = "@dennisrongo/dsh-todo";
|
|
41
61
|
function descriptor(method, request, result) {
|
|
42
62
|
return {
|
|
@@ -72,7 +92,9 @@ var TODO_REMOTE = {
|
|
|
72
92
|
package: PACKAGE,
|
|
73
93
|
descriptors: [
|
|
74
94
|
descriptor("list", listRequestSchema, listResultSchema),
|
|
75
|
-
descriptor("replace", replaceRequestSchema, replaceResultSchema)
|
|
95
|
+
descriptor("replace", replaceRequestSchema, replaceResultSchema),
|
|
96
|
+
descriptor("scanDigest", scanRequestSchema, scanDigestResultSchema),
|
|
97
|
+
descriptor("readSuggestions", readSuggestionsRequestSchema, readSuggestionsResultSchema)
|
|
76
98
|
]
|
|
77
99
|
};
|
|
78
100
|
|
|
@@ -103,6 +125,18 @@ var TYPERT = {
|
|
|
103
125
|
name: "replace",
|
|
104
126
|
signature: "@Remote replace(request: TodoReplaceRequest): Promise<TodoReplaceResult>",
|
|
105
127
|
summary: "Replace one workspace's list, guarded by the observed revision."
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
kind: "method",
|
|
131
|
+
name: "scanDigest",
|
|
132
|
+
signature: "@Remote scanDigest(request: SuggestScanRequest): Promise<ScanDigestResult>",
|
|
133
|
+
summary: "Build the bounded workspace evidence a scan session reasons over."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
kind: "method",
|
|
137
|
+
name: "readSuggestions",
|
|
138
|
+
signature: "@Remote readSuggestions(request: ReadSuggestionsRequest): Promise<ReadSuggestionsResult>",
|
|
139
|
+
summary: "Read and consume whatever a scan session has written so far."
|
|
106
140
|
}
|
|
107
141
|
],
|
|
108
142
|
types: [
|
|
@@ -129,6 +163,29 @@ var TYPERT = {
|
|
|
129
163
|
{
|
|
130
164
|
name: "TodoReplaceResult",
|
|
131
165
|
declaration: "export type TodoReplaceResult = { ok: true; list: TodoList } | { ok: false; code: 'revision-conflict'; list: TodoList };"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "SuggestScanRequest",
|
|
169
|
+
declaration: "export interface SuggestScanRequest {\n workspaceId: string;\n}"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
// `runId` is REQUIRED. A per-run result path is what stops a scan
|
|
173
|
+
// that timed out — archived, but never actually cancelled — writing
|
|
174
|
+
// its answer where the NEXT run reads it as fresh.
|
|
175
|
+
name: "ReadSuggestionsRequest",
|
|
176
|
+
declaration: "export interface ReadSuggestionsRequest {\n workspaceId: string;\n runId: string;\n}"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "ScanDigestResult",
|
|
180
|
+
declaration: "export interface ScanDigestResult {\n digest: string;\n truncated: boolean;\n}"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "Suggestion",
|
|
184
|
+
declaration: "export interface Suggestion {\n title: string;\n rationale: string;\n priority: TodoPriority;\n evidence?: string;\n}"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "ReadSuggestionsResult",
|
|
188
|
+
declaration: "export interface ReadSuggestionsResult {\n status: 'pending' | 'ready' | 'error';\n suggestions?: Suggestion[];\n error?: string;\n}"
|
|
132
189
|
}
|
|
133
190
|
]
|
|
134
191
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dennisrongo/dsh-todo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Todo list for DeepSeek Harness (dsh) — a per-workspace task list persisted on disk by the host",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "node build/build.mjs",
|
|
36
36
|
"typecheck": "tsc --noEmit",
|
|
37
|
-
"test": "node build/build.mjs && node test/smoke.mjs && node test/context-probe.mjs && node test/launch-effect.mjs && node test/launch-lifecycle.mjs && node test/cli.test.mjs && node test/cli-integration.mjs",
|
|
37
|
+
"test": "node build/build.mjs && node test/smoke.mjs && node test/context-probe.mjs && node test/launch-effect.mjs && node test/launch-lifecycle.mjs && node test/suggest-lifecycle.mjs && node test/suggest.test.mjs && node test/scan.test.mjs && node test/cli.test.mjs && node test/cli-integration.mjs",
|
|
38
38
|
"test:icons": "node test/icon-probe.mjs",
|
|
39
39
|
"test:modal": "node test/modal-probe.mjs",
|
|
40
40
|
"test:cli": "node test/cli.test.mjs",
|