@worca/app 1.0.0 → 1.2.0-rc.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 +30 -9
- package/agents/clarify.meta.json +4 -4
- package/agents/decomposer.meta.json +5 -5
- package/agents/implementer.meta.json +15 -5
- package/agents/manualTestsChecklist.meta.json +5 -4
- package/agents/manualWebUiTesting.meta.json +9 -4
- package/agents/planReviewer.meta.json +12 -4
- package/agents/planner.meta.json +12 -5
- package/agents/refiner.meta.json +15 -4
- package/agents/reviewer.meta.json +14 -4
- package/agents/worca-cc-clarify.md +7 -0
- package/agents/worca-cc-code-reviewer.md +11 -6
- package/agents/worca-cc-decomposer.md +7 -0
- package/agents/worca-cc-implementer.md +9 -0
- package/agents/worca-cc-manual-tests-checklist.md +8 -5
- package/agents/worca-cc-manual-web-ui-testing.md +10 -6
- package/agents/worca-cc-plan-refiner.md +11 -6
- package/agents/worca-cc-plan-reviewer.md +10 -7
- package/agents/worca-cc-planner.md +9 -0
- package/agents/worca-cc-workspace-reviewer.md +11 -4
- package/agents/worca-cc-workspace-scanner.md +8 -4
- package/agents/workspaceReviewer.meta.json +15 -4
- package/agents/workspaceScanner.meta.json +5 -4
- package/package.json +8 -2
- package/skills/worca/SKILL.md +5 -5
- package/src/cli/render.mjs +148 -0
- package/src/cli/worca-cc.mjs +386 -56
- package/src/core/agent-gen.mjs +69 -31
- package/src/core/agent-registry.mjs +124 -144
- package/src/core/agent-store.mjs +164 -4
- package/src/core/artifacts.mjs +199 -23
- package/src/core/ask/attachment-kind.mjs +95 -0
- package/src/core/ask/catalog.mjs +111 -0
- package/src/core/ask/comment-deps.mjs +55 -0
- package/src/core/ask/events.mjs +545 -0
- package/src/core/ask/follow.mjs +113 -0
- package/src/core/ask/git-allowlist.mjs +226 -0
- package/src/core/ask/limits.mjs +57 -0
- package/src/core/ask/mcp-stdio.mjs +135 -0
- package/src/core/ask/models.mjs +125 -0
- package/src/core/ask/prompt.mjs +286 -0
- package/src/core/ask/proposal.mjs +170 -0
- package/src/core/ask/redact.mjs +30 -0
- package/src/core/ask/spawn.mjs +156 -0
- package/src/core/ask/store.mjs +438 -0
- package/src/core/ask/tool-deps.mjs +87 -0
- package/src/core/ask/tools.mjs +879 -0
- package/src/core/ask/turn.mjs +462 -0
- package/src/core/ask/worktree-deps.mjs +27 -0
- package/src/core/ask/worktrees.mjs +285 -0
- package/src/core/chat/command-router.mjs +28 -7
- package/src/core/chat/notifier.mjs +6 -1
- package/src/core/chat/renderers.mjs +15 -8
- package/src/core/claude-runner.mjs +541 -62
- package/src/core/config.mjs +310 -44
- package/src/core/cost-budget.mjs +29 -2
- package/src/core/db.mjs +773 -53
- package/src/core/diff-anchor.mjs +213 -0
- package/src/core/diff-comments.mjs +273 -0
- package/src/core/engine-select.mjs +32 -0
- package/src/core/failure-policy.mjs +201 -0
- package/src/core/git-info.mjs +49 -10
- package/src/core/graph/builtin-workflows.mjs +51 -0
- package/src/core/graph/executor.mjs +894 -0
- package/src/core/graph/registry-ports.mjs +12 -0
- package/src/core/graph/scheduler.mjs +1072 -0
- package/src/core/graph/seed-templates.mjs +318 -0
- package/src/core/host-guard.mjs +271 -0
- package/src/core/model-env.mjs +180 -8
- package/src/core/model-test.mjs +79 -0
- package/src/core/orchestrator.mjs +994 -4097
- package/src/core/overview-agent.mjs +15 -3
- package/src/core/phases.mjs +208 -537
- package/src/core/pipeline-delete.mjs +13 -2
- package/src/core/plugin-api.mjs +8 -3
- package/src/core/plugin-config.mjs +178 -28
- package/src/core/plugin-inventory.mjs +6 -2
- package/src/core/plugin-manifest.mjs +199 -11
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-repo.mjs +16 -4
- package/src/core/plugin-shim-child.mjs +9 -3
- package/src/core/plugin-shim.mjs +80 -17
- package/src/core/plugin-store.mjs +236 -29
- package/src/core/plugin-workflows.mjs +90 -41
- package/src/core/preflight.mjs +135 -3
- package/src/core/projects.mjs +7 -5
- package/src/core/protocol.mjs +8 -35
- package/src/core/recoverable-error.mjs +1 -1
- package/src/core/run-harness.mjs +3934 -0
- package/src/core/run-manifest.mjs +5 -1
- package/src/core/settings.mjs +184 -13
- package/src/core/skills.mjs +10 -3
- package/src/core/source-bindings.mjs +175 -0
- package/src/core/sources.mjs +87 -25
- package/src/core/stats.mjs +25 -6
- package/src/core/title.mjs +51 -4
- package/src/core/workflows.mjs +358 -259
- package/src/core/workspace-scan.mjs +4 -0
- package/src/core/worktree.mjs +98 -7
- package/src/shared/graph/agent-meta.mjs +278 -0
- package/src/shared/graph/constants.mjs +105 -0
- package/src/shared/graph/geometry.mjs +157 -0
- package/src/shared/graph/layout.mjs +134 -0
- package/src/shared/graph/loops.mjs +130 -0
- package/src/shared/graph/manifest.mjs +257 -0
- package/src/shared/graph/ports.mjs +153 -0
- package/src/shared/graph/route.mjs +397 -0
- package/src/shared/graph/template.mjs +165 -0
- package/src/shared/graph/thumbnail.mjs +67 -0
- package/src/shared/graph/validate.mjs +491 -0
- package/src/shared/graph/verdict.mjs +41 -0
- package/ui/public/app.js +4240 -1682
- package/ui/public/ask-markdown.mjs +145 -0
- package/ui/public/ask-model.mjs +317 -0
- package/ui/public/ask-panel.mjs +2129 -0
- package/ui/public/chat-settings-view.mjs +6 -2
- package/ui/public/diff-view.mjs +66 -11
- package/ui/public/file-tree.mjs +305 -0
- package/ui/public/graph/composer.mjs +889 -0
- package/ui/public/graph/inspector.mjs +183 -0
- package/ui/public/graph/model.mjs +37 -0
- package/ui/public/graph/palette.mjs +144 -0
- package/ui/public/graph/run-decor.mjs +410 -0
- package/ui/public/graph/run-hosts.mjs +201 -0
- package/ui/public/graph/save-dialog.mjs +56 -0
- package/ui/public/graph/view.mjs +858 -0
- package/ui/public/guardrails-view.mjs +4 -2
- package/ui/public/hljs-loader.mjs +180 -0
- package/ui/public/index.html +311 -265
- package/ui/public/log-filter.mjs +22 -4
- package/ui/public/log-line.mjs +45 -19
- package/ui/public/models-view.mjs +171 -9
- package/ui/public/plugins-view.mjs +106 -4
- package/ui/public/source-pane.mjs +190 -8
- package/ui/public/stats-view.mjs +81 -1
- package/ui/public/style.css +1487 -229
- package/ui/public/syntax-highlight.mjs +270 -0
- package/ui/public/thinking-orb.mjs +110 -0
- package/ui/server.mjs +1894 -104
- package/src/core/channels.mjs +0 -302
- package/src/core/runners.mjs +0 -167
- package/src/core/workflow-validator.mjs +0 -185
- package/ui/public/composer-core.mjs +0 -211
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/core/ask/attachment-kind.mjs
|
|
2
|
+
// Attachment typing for the Ask Worca chat (issue #398): which extensions are
|
|
3
|
+
// accepted, what kind/mime each maps to, and content sniffing for the binary
|
|
4
|
+
// ones. Pure and synchronous — the single source of truth for the type table;
|
|
5
|
+
// limits.mjs re-exports the two extension lists and ui/server.mjs validates
|
|
6
|
+
// uploads with classifyExtension + sniffMime.
|
|
7
|
+
//
|
|
8
|
+
// Kinds: 'text' (UTF-8, inlineable into the turn prompt, redactable),
|
|
9
|
+
// 'image' (fed to the model via its Read tool on the stored file) and
|
|
10
|
+
// 'binary' (today only PDF — same Read-tool path, never inlined).
|
|
11
|
+
//
|
|
12
|
+
// The extension names the CLAIMED type; for binary kinds the claim is verified
|
|
13
|
+
// against the leading bytes (magic number) so a mislabeled body is refused at
|
|
14
|
+
// upload rather than stored wrong. SVG is deliberately absent: it is scriptable
|
|
15
|
+
// markup, and the download route serves attachment bodies with their real mime.
|
|
16
|
+
|
|
17
|
+
/** Extension -> {kind, mime} for the text kinds (the pre-#398 allowlist). */
|
|
18
|
+
const TEXT_TYPES = Object.freeze({
|
|
19
|
+
'.md': 'text/markdown',
|
|
20
|
+
'.markdown': 'text/markdown',
|
|
21
|
+
'.txt': 'text/plain',
|
|
22
|
+
'.json': 'application/json',
|
|
23
|
+
'.csv': 'text/csv',
|
|
24
|
+
'.log': 'text/plain',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Extension -> mime for the binary kinds. Every mime here MUST be sniffable. */
|
|
28
|
+
const BINARY_TYPES = Object.freeze({
|
|
29
|
+
'.png': 'image/png',
|
|
30
|
+
'.jpg': 'image/jpeg',
|
|
31
|
+
'.jpeg': 'image/jpeg',
|
|
32
|
+
'.gif': 'image/gif',
|
|
33
|
+
'.webp': 'image/webp',
|
|
34
|
+
'.pdf': 'application/pdf',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const TEXT_EXTENSIONS = Object.freeze(Object.keys(TEXT_TYPES));
|
|
38
|
+
export const BINARY_EXTENSIONS = Object.freeze(Object.keys(BINARY_TYPES));
|
|
39
|
+
|
|
40
|
+
const kindForMime = (mime) => (mime.startsWith('image/') ? 'image' : 'binary');
|
|
41
|
+
|
|
42
|
+
/** ISO 32000-1 §7.5.2 (implementation note 13): the `%PDF-` header may be
|
|
43
|
+
* preceded by up to 1024 bytes of junk (a UTF-8 BOM, print-driver or mail-
|
|
44
|
+
* gateway preamble). Acrobat and pdf.js accept such files, so the sniff does too. */
|
|
45
|
+
const PDF_HEADER_WINDOW = 1024;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Classify a lower-cased extension (with the leading dot) into {kind, mime},
|
|
49
|
+
* or null when it is not on either allowlist.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyExtension(ext) {
|
|
52
|
+
if (typeof ext !== 'string') return null;
|
|
53
|
+
const e = ext.toLowerCase();
|
|
54
|
+
if (Object.prototype.hasOwnProperty.call(TEXT_TYPES, e)) return { kind: 'text', mime: TEXT_TYPES[e] };
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(BINARY_TYPES, e)) return { kind: kindForMime(BINARY_TYPES[e]), mime: BINARY_TYPES[e] };
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sniff the real mime of a binary body from its magic number, or null when the
|
|
61
|
+
* bytes match none of the accepted binary types. Text kinds are validated by
|
|
62
|
+
* UTF-8 decoding instead (ui/server.mjs), never sniffed here.
|
|
63
|
+
*/
|
|
64
|
+
export function sniffMime(buf) {
|
|
65
|
+
if (!Buffer.isBuffer(buf) || buf.length < 3) return null;
|
|
66
|
+
if (buf.length >= 8
|
|
67
|
+
&& buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47
|
|
68
|
+
&& buf[4] === 0x0d && buf[5] === 0x0a && buf[6] === 0x1a && buf[7] === 0x0a) return 'image/png';
|
|
69
|
+
// SOI (FF D8) followed by the first marker's FF and its marker byte (>= 0xC0:
|
|
70
|
+
// APPn/DQT/SOFn/…) — a bare 3-byte FF D8 FF stub is not a JPEG anything can open.
|
|
71
|
+
if (buf.length >= 4 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff && buf[3] >= 0xc0) return 'image/jpeg';
|
|
72
|
+
if (buf.length >= 6) {
|
|
73
|
+
const head6 = buf.toString('latin1', 0, 6);
|
|
74
|
+
if (head6 === 'GIF87a' || head6 === 'GIF89a') return 'image/gif';
|
|
75
|
+
}
|
|
76
|
+
if (buf.length >= 12
|
|
77
|
+
&& buf.toString('latin1', 0, 4) === 'RIFF'
|
|
78
|
+
&& buf.toString('latin1', 8, 12) === 'WEBP') return 'image/webp';
|
|
79
|
+
if (buf.length >= 5) {
|
|
80
|
+
const at = buf.toString('latin1', 0, Math.min(buf.length, PDF_HEADER_WINDOW + 5)).indexOf('%PDF-');
|
|
81
|
+
if (at !== -1 && at <= PDF_HEADER_WINDOW) return 'application/pdf';
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The on-disk extension for a stored body: derived from the SNIFFED mime (or
|
|
87
|
+
* '.txt' for text kinds), never from the user-supplied name — the path stays a
|
|
88
|
+
* function of row data the store minted (store.mjs traversal guard). */
|
|
89
|
+
export function extensionForAttachment(kind, mime) {
|
|
90
|
+
if (kind === 'text' || kind == null) return '.txt';
|
|
91
|
+
for (const [ext, m] of Object.entries(BINARY_TYPES)) {
|
|
92
|
+
if (m === mime) return ext; // first match: '.jpg' wins over '.jpeg' for image/jpeg
|
|
93
|
+
}
|
|
94
|
+
return '.bin';
|
|
95
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/core/ask/catalog.mjs
|
|
2
|
+
// The static catalog the assistant sees (ask-worca-design.md §6.1, D9): projects,
|
|
3
|
+
// workspaces and workflows with their ordered step groups. ONE builder feeds both
|
|
4
|
+
// the system prompt (prompt.mjs renders a subset) and the list_projects /
|
|
5
|
+
// list_workflows tools (tools.mjs returns the objects) — never two readers.
|
|
6
|
+
// Readers are injected so unit tests run without a DB.
|
|
7
|
+
import { listProjects as realListProjects } from '../projects.mjs';
|
|
8
|
+
import { listWorkspaces as realListWorkspaces } from '../workspaces.mjs';
|
|
9
|
+
import { listWorkflows as realListWorkflows, GRAPH_DEFAULT_WORKFLOW } from '../workflows.mjs';
|
|
10
|
+
import { classifyLoops } from '../../shared/graph/loops.mjs';
|
|
11
|
+
import { rankNodes } from '../../shared/graph/layout.mjs';
|
|
12
|
+
import { registryPortsFn } from '../graph/registry-ports.mjs';
|
|
13
|
+
|
|
14
|
+
/** A v2 graph -> the v1-shaped step groups the assistant already understands:
|
|
15
|
+
* one group per rank (loop wires excluded), agent nodes only. */
|
|
16
|
+
function graphSteps(tpl, portsFn) {
|
|
17
|
+
const ranks = rankNodes(tpl, classifyLoops(tpl, portsFn));
|
|
18
|
+
const byRank = new Map();
|
|
19
|
+
for (const node of tpl.nodes) {
|
|
20
|
+
// buildCatalog maps EVERY template, so one malformed node used to take down
|
|
21
|
+
// the whole Ask system prompt and list_workflows, not just its own row.
|
|
22
|
+
if (!node || node.kind !== 'agent') continue;
|
|
23
|
+
const r = ranks[node.id] ?? 0;
|
|
24
|
+
if (!byRank.has(r)) byRank.set(r, []);
|
|
25
|
+
byRank.get(r).push(node);
|
|
26
|
+
}
|
|
27
|
+
return [...byRank.keys()].sort((a, b) => a - b).map((r) => byRank.get(r));
|
|
28
|
+
}
|
|
29
|
+
import { loadAgentRegistry as realLoadAgentRegistry } from '../agent-registry.mjs';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pure: a stored workflow template → the catalog shape. `tpl.steps` is already
|
|
33
|
+
* Array<Array<{id,key}>> (outer = ordered step groups, inner = parallel nodes,
|
|
34
|
+
* workflows.mjs:93-111 / :208-220), so this is a straight map against the agent
|
|
35
|
+
* registry for display names; unknown keys fall back to the key itself.
|
|
36
|
+
*/
|
|
37
|
+
export function shapeWorkflow(tpl, registry = {}) {
|
|
38
|
+
if (tpl && tpl.version === 2 && Array.isArray(tpl.nodes)) {
|
|
39
|
+
const portsFn = registryPortsFn(registry);
|
|
40
|
+
const groups = graphSteps(tpl, portsFn);
|
|
41
|
+
const { loopWireIds } = classifyLoops(tpl, portsFn);
|
|
42
|
+
return {
|
|
43
|
+
id: tpl.id,
|
|
44
|
+
name: tpl.name,
|
|
45
|
+
domain: typeof tpl.domain === 'string' && tpl.domain ? tpl.domain : 'general',
|
|
46
|
+
origin: tpl.origin ?? null,
|
|
47
|
+
steps: groups.map((group) => group.map((node) => {
|
|
48
|
+
const meta = registry && registry[node.key] ? registry[node.key] : null;
|
|
49
|
+
return {
|
|
50
|
+
nodeId: node.id,
|
|
51
|
+
key: node.key,
|
|
52
|
+
displayName: meta && meta.displayName ? meta.displayName : node.key,
|
|
53
|
+
description: meta && typeof meta.description === 'string' ? meta.description : '',
|
|
54
|
+
};
|
|
55
|
+
})),
|
|
56
|
+
feedbacks: (tpl.wires || []).filter((w) => w && loopWireIds.has(w.id))
|
|
57
|
+
.map((w) => ({ id: w.id, from: w.from.node, to: w.to.node })),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const steps = Array.isArray(tpl.steps) ? tpl.steps : [];
|
|
61
|
+
const feedbacks = Array.isArray(tpl.feedbacks) ? tpl.feedbacks : [];
|
|
62
|
+
return {
|
|
63
|
+
id: tpl.id,
|
|
64
|
+
name: tpl.name,
|
|
65
|
+
domain: typeof tpl.domain === 'string' && tpl.domain ? tpl.domain : 'general',
|
|
66
|
+
origin: tpl.origin ?? null,
|
|
67
|
+
steps: steps.map((group) => (Array.isArray(group) ? group : []).map((node) => {
|
|
68
|
+
const meta = registry && node && registry[node.key] ? registry[node.key] : null;
|
|
69
|
+
return {
|
|
70
|
+
nodeId: node.id,
|
|
71
|
+
key: node.key,
|
|
72
|
+
displayName: meta && typeof meta.displayName === 'string' && meta.displayName ? meta.displayName : node.key,
|
|
73
|
+
description: meta && typeof meta.description === 'string' ? meta.description : '',
|
|
74
|
+
};
|
|
75
|
+
})),
|
|
76
|
+
feedbacks: feedbacks.map((f) => ({ id: f.id, from: f.from, to: f.to })),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {{listProjects?:Function, listWorkspaces?:Function, listWorkflows?:Function, defaultWorkflow?:object, loadAgentRegistry?:Function}} [deps]
|
|
82
|
+
*/
|
|
83
|
+
export function createCatalog({
|
|
84
|
+
listProjects = realListProjects,
|
|
85
|
+
listWorkspaces = realListWorkspaces,
|
|
86
|
+
listWorkflows = realListWorkflows,
|
|
87
|
+
defaultWorkflow = GRAPH_DEFAULT_WORKFLOW,
|
|
88
|
+
loadAgentRegistry = realLoadAgentRegistry,
|
|
89
|
+
} = {}) {
|
|
90
|
+
async function buildCatalog() {
|
|
91
|
+
const [projects, workspaces, workflows] = await Promise.all([listProjects(), listWorkspaces(), listWorkflows()]);
|
|
92
|
+
let registry = {};
|
|
93
|
+
try { registry = loadAgentRegistry() || {}; } catch { registry = {}; }
|
|
94
|
+
// Same order as GET /api/workflows: the graph default, then saved rows.
|
|
95
|
+
// shapeWorkflow already derives `steps` (condensation-topo ranks) +
|
|
96
|
+
// `feedbacks` (loop wires) for a v2 template, so the LLM-facing shape is unchanged.
|
|
97
|
+
const templates = [
|
|
98
|
+
defaultWorkflow,
|
|
99
|
+
...workflows.filter((t) => t && t.id !== defaultWorkflow.id),
|
|
100
|
+
];
|
|
101
|
+
return {
|
|
102
|
+
projects: projects.map((p) => ({ key: p.key, name: p.name, path: p.path })),
|
|
103
|
+
workspaces: workspaces.map((w) => ({ id: w.id, name: w.name, projectKeys: [...(w.projectKeys || [])] })),
|
|
104
|
+
workflows: templates.map((t) => shapeWorkflow(t, registry)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { buildCatalog };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Bound to the real readers — what the server and the MCP child use. */
|
|
111
|
+
export const buildCatalog = createCatalog().buildCatalog;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/core/ask/comment-deps.mjs
|
|
2
|
+
// The WRITE-CAPABLE dep bundle of the diff-comment tools. Deliberately separate
|
|
3
|
+
// from tool-deps.mjs, whose source is scanned as read-only
|
|
4
|
+
// (test/ask-tools.test.mjs): every row that is created, resolved or deleted is
|
|
5
|
+
// reachable ONLY through here, and test/ask-diff-comment-tools.test.mjs pins this
|
|
6
|
+
// module's import surface. Same shape as worktree-deps.mjs — one namespaced
|
|
7
|
+
// sub-object.
|
|
8
|
+
//
|
|
9
|
+
// Everything writes through src/core/diff-comments.mjs — the one mutation module —
|
|
10
|
+
// so the MCP path and the REST path share anchor validation, the body cap and the
|
|
11
|
+
// change notification. Reads add the surrounding hunk lines here (tools.mjs has no
|
|
12
|
+
// imports at all); tools.mjs owns the protected-path filter and the redaction, so
|
|
13
|
+
// the fail-closed read rules live next to get_run_diff's.
|
|
14
|
+
import {
|
|
15
|
+
addDiffComment, listDiffComments, getDiffComment, setDiffCommentResolved, deleteDiffComment,
|
|
16
|
+
} from '../diff-comments.mjs';
|
|
17
|
+
import { hunkContext } from '../diff-anchor.mjs';
|
|
18
|
+
|
|
19
|
+
/** Rows either side of an anchor served to the model. */
|
|
20
|
+
export const COMMENT_CONTEXT_RADIUS = 3;
|
|
21
|
+
|
|
22
|
+
// COST NOTE, deliberate: hunkContext() re-runs splitPatchSections + patchIndex +
|
|
23
|
+
// parseFileSection over the WHOLE patch once per comment, so listing N comments
|
|
24
|
+
// on a run costs N full parses. That is the price of using the ONE parser (D3)
|
|
25
|
+
// instead of a second, faster, divergent one. It is bounded in practice — this
|
|
26
|
+
// runs in the MCP child, once per tool call, on runs with a handful of comments
|
|
27
|
+
// — and the REST list path does not do it at all. If a real run ever makes this
|
|
28
|
+
// hurt, memoize the parsed index per (patchText) inside this closure; do NOT
|
|
29
|
+
// hand-roll a cheaper scan.
|
|
30
|
+
|
|
31
|
+
/** No thread scoping: comments belong to RUNS, not to chat threads. */
|
|
32
|
+
export function defaultCommentDeps() {
|
|
33
|
+
return {
|
|
34
|
+
comments: {
|
|
35
|
+
/** Comments of a run, each with `context` when the patch is readable.
|
|
36
|
+
* `keep` is the CALLER's protected-path filter (tools.mjs owns the guard
|
|
37
|
+
* rules); applied BEFORE the context parse so a row that will be dropped
|
|
38
|
+
* never costs a whole-patch parse, and so the order is fail-closed-first
|
|
39
|
+
* like everywhere else. */
|
|
40
|
+
list: (storeKey, pipelineId, { status = 'all', path = null, patchText = null, keep = null } = {}) => {
|
|
41
|
+
const rows = listDiffComments(storeKey, pipelineId, { status, path });
|
|
42
|
+
const kept = typeof keep === 'function' ? rows.filter(keep) : rows;
|
|
43
|
+
return kept.map((c) => (patchText == null ? c : {
|
|
44
|
+
...c,
|
|
45
|
+
context: hunkContext(patchText, { project: c.projectKey, path: c.path, side: c.side, line: c.line },
|
|
46
|
+
COMMENT_CONTEXT_RADIUS),
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
add: (input) => addDiffComment({ ...input, author: 'ask' }),
|
|
50
|
+
get: (id) => getDiffComment(id),
|
|
51
|
+
setResolved: (id, resolved) => setDiffCommentResolved(id, resolved),
|
|
52
|
+
remove: (id) => deleteDiffComment(id),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|