@pmelab/gtd 55.0.0 → 57.0.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/.claude-plugin/plugin.json +1 -1
- package/README.md +72 -80
- package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-X2RkNHin.mjs} +110 -318
- package/dist/chunks/{review-BJGKwlYv.mjs → review-v7sh5GF4.mjs} +30 -125
- package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
- package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-S04KEewi.mjs} +144 -56
- package/dist/gtd.bundle.mjs +259 -22
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +6 -6
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +551 -66
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +63 -175
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +603 -727
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -3
- package/src/cli/Cli.ts +5 -5
- package/src/edge/Edge.test.ts +1 -1
- package/src/steering/FORMATS.md +5 -1
- package/src/steering/SteeringFormats.test.ts +2 -2
- package/src/steering/contract.test.ts +1 -0
- package/src/steering/findings.test.ts +47 -0
- package/src/steering/findings.ts +350 -0
- package/src/steering/fixtures/findings/gate.md +34 -0
- package/src/steering/fixtures/findings/gate.read.json +87 -0
- package/src/steering/index.test.ts +2 -2
- package/src/steering/index.ts +4 -3
- package/src/steering/review.test.ts +1 -75
- package/src/steering/review.ts +1 -74
- package/src/workflow/compile.test.ts +1 -0
- package/src/workflows/feature/criteria-coverage.feature +1 -7
- package/src/workflows/feature/criteria.test.ts +1 -41
- package/src/workflows/feature/criteria.ts +2 -14
- package/src/workflows/feature/fast-path.feature +0 -1
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +16 -25
- package/src/workflows/feature/flag-mode.feature +1 -1
- package/src/workflows/feature/lens-models.feature +0 -1
- package/src/workflows/feature/lens-review.feature +323 -172
- package/src/workflows/feature/naive-test.feature +258 -0
- package/src/workflows/feature/one-step-land.feature +0 -1
- package/src/workflows/feature/refactor-absorbs.feature +1 -8
- package/src/workflows/feature/refactor-first.feature +2 -11
- package/src/workflows/feature/refactor-rebuild.feature +4 -25
- package/src/workflows/feature/refactor-review.feature +33 -118
- package/src/workflows/feature/refactor.test.ts +0 -2
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +7 -24
- package/src/workflows/feature/reviewer-walkthrough.feature +16 -104
- package/src/workflows/feature/seam-rejection.feature +0 -1
- package/src/workflows/feature/seam.feature +0 -1
- package/src/workflows/feature/seam.test.ts +1 -2
- package/src/workflows/feature/seam.ts +56 -10
- package/src/workflows/feature/standards.feature +75 -35
- package/src/workflows/fix/fix-lens-origin.feature +20 -7
- package/src/workflows/fix/fix-quality.feature +102 -23
- package/src/workflows/review/review-door.feature +62 -59
- package/src/workflows/shared/learn.test.ts +0 -14
- package/src/workflows/shared/learn.ts +1 -6
- package/src/workflows/shared/lenses.ts +149 -43
- package/src/workflows/shared/options.ts +9 -1
- package/src/workflows/shared/prose.ts +0 -6
- package/src/workflows/shared/review.test.ts +27 -26
- package/src/workflows/shared/review.ts +29 -196
- package/src/workflows/shared/skills.test.ts +0 -4
- package/src/workflows/shared/skills.ts +0 -5
- package/src/workflows/shared/steps.ts +1 -1
- package/src/workflows/shared/tail.ts +18 -9
- package/src/workflows/shared/text.test.ts +125 -329
- package/src/workflows/shared/text.ts +173 -471
- package/src/workflows/shared/vars.ts +2 -3
- package/src/workflows/feature/hands-on-skip.feature +0 -316
- package/src/workflows/feature/prototype-rounds.feature +0 -156
- package/src/workflows/feature/prototype.feature +0 -428
- package/src/workflows/feature/prototype.test.ts +0 -43
- package/src/workflows/feature/prototype.ts +0 -198
- package/src/workflows/feature/review-answers.feature +0 -324
- package/src/workflows/feature/severity-budget.feature +0 -116
- package/src/workflows/feature/severity-sorted-review.feature +0 -198
package/dist/web/main.js
CHANGED
|
@@ -25928,7 +25928,7 @@ function serializeChunks(chunks, expandTabs) {
|
|
|
25928
25928
|
* @returns {ParseContext}
|
|
25929
25929
|
* Parser.
|
|
25930
25930
|
*/
|
|
25931
|
-
function parse(options) {
|
|
25931
|
+
function parse$1(options) {
|
|
25932
25932
|
/** @type {ParseContext} */
|
|
25933
25933
|
const parser = {
|
|
25934
25934
|
constructs: combineExtensions([constructs_exports, ...(options || {}).extensions || []]),
|
|
@@ -26243,7 +26243,7 @@ function fromMarkdown(value, encoding, options) {
|
|
|
26243
26243
|
options = encoding;
|
|
26244
26244
|
encoding = void 0;
|
|
26245
26245
|
}
|
|
26246
|
-
return compiler(options)(postprocess(parse(options).document().write(preprocess()(value, encoding, true))));
|
|
26246
|
+
return compiler(options)(postprocess(parse$1(options).document().write(preprocess()(value, encoding, true))));
|
|
26247
26247
|
}
|
|
26248
26248
|
/**
|
|
26249
26249
|
* Note this compiler only understand complete buffering, not streaming.
|
|
@@ -29842,7 +29842,7 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
|
|
|
29842
29842
|
//#endregion
|
|
29843
29843
|
//#region src/steering/Threads.ts
|
|
29844
29844
|
const PREFIX_RE = /^(H|A):\s*/;
|
|
29845
|
-
const lineOf = (node, which) => toLspPosition(node.position[which]).line;
|
|
29845
|
+
const lineOf$1 = (node, which) => toLspPosition(node.position[which]).line;
|
|
29846
29846
|
/** The item's own blocks joined — never the item node's span, which can run past its last line into the next bullet's marker. */
|
|
29847
29847
|
const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
29848
29848
|
const prefixOf = (content, item) => PREFIX_RE.exec(itemText$1(content, item));
|
|
@@ -29856,9 +29856,9 @@ const isThreadBody = (content, children) => {
|
|
|
29856
29856
|
};
|
|
29857
29857
|
const foldName$1 = (name) => name.toLowerCase();
|
|
29858
29858
|
/** One list item's entry, reporting any syntax problem; `previous` is the author before it. */
|
|
29859
|
-
const entryOf = (content, item, previous, report) => {
|
|
29860
|
-
const start = lineOf(item, "start");
|
|
29861
|
-
const end = lineOf(item, "end");
|
|
29859
|
+
const entryOf$1 = (content, item, previous, report) => {
|
|
29860
|
+
const start = lineOf$1(item, "start");
|
|
29861
|
+
const end = lineOf$1(item, "end");
|
|
29862
29862
|
const match = prefixOf(content, item);
|
|
29863
29863
|
if (!match) {
|
|
29864
29864
|
report(`an entry must start with "H:" or "A:"`, start, end);
|
|
@@ -29874,7 +29874,7 @@ const entryOf = (content, item, previous, report) => {
|
|
|
29874
29874
|
author,
|
|
29875
29875
|
text,
|
|
29876
29876
|
line: start,
|
|
29877
|
-
endLine: last ? lineOf(last, "end") : end,
|
|
29877
|
+
endLine: last ? lineOf$1(last, "end") : end,
|
|
29878
29878
|
column: toLspPosition(item.position.start).character
|
|
29879
29879
|
};
|
|
29880
29880
|
};
|
|
@@ -29883,16 +29883,16 @@ const threadOf = (content, def, list, markers, report) => {
|
|
|
29883
29883
|
const entries = [];
|
|
29884
29884
|
let previous;
|
|
29885
29885
|
for (const item of list.children) {
|
|
29886
|
-
const entry = entryOf(content, item, previous, report);
|
|
29886
|
+
const entry = entryOf$1(content, item, previous, report);
|
|
29887
29887
|
if (!entry) continue;
|
|
29888
29888
|
previous = entry.author;
|
|
29889
29889
|
entries.push(entry);
|
|
29890
29890
|
}
|
|
29891
|
-
for (const extra of def.children.slice(1)) report(`only one list of "H:"/"A:" entries may make up the body`, lineOf(extra, "start"), lineOf(extra, "end"));
|
|
29891
|
+
for (const extra of def.children.slice(1)) report(`only one list of "H:"/"A:" entries may make up the body`, lineOf$1(extra, "start"), lineOf$1(extra, "end"));
|
|
29892
29892
|
return {
|
|
29893
29893
|
name,
|
|
29894
|
-
line: lineOf(def, "start"),
|
|
29895
|
-
endLine: lineOf(def, "end"),
|
|
29894
|
+
line: lineOf$1(def, "start"),
|
|
29895
|
+
endLine: lineOf$1(def, "end"),
|
|
29896
29896
|
markers: markers.filter((m) => foldName$1(m.name) === foldName$1(name)),
|
|
29897
29897
|
entries,
|
|
29898
29898
|
waitingOn: entries[entries.length - 1]?.author === "agent" ? "human" : "agent"
|
|
@@ -30126,6 +30126,8 @@ const noteLookup = (content, footnotes = parseFootnotes(content)) => {
|
|
|
30126
30126
|
return note === "" ? {} : { note };
|
|
30127
30127
|
};
|
|
30128
30128
|
};
|
|
30129
|
+
/** Thread syntax findings alone — what free-form `validate` reports without inheriting the other footnote findings. */
|
|
30130
|
+
const threadFindings = (content) => collectThreads(content, parseMarkdown(content), []).findings;
|
|
30129
30131
|
/** The first integer unused by any `fnN` marker or definition already in the document — deterministic (no clock, no randomness), so "add a footnote" is testable and idempotent under re-run: applying it twice yields `fn1` then `fn2`, never a collision. Counts orphan markers too (via `parseFootnotes`), so it never reuses a name that's already written but undefined. */
|
|
30130
30132
|
const nextFootnoteName = (content) => {
|
|
30131
30133
|
const { markers, definitions } = parseFootnotes(content);
|
|
@@ -32116,14 +32118,14 @@ const CHOICE_NAMES = {
|
|
|
32116
32118
|
/** A finding's decision as an outline node of its own; none for a chunk with no answer boxes. */
|
|
32117
32119
|
const decisionNodes = (lines, chunk, end) => {
|
|
32118
32120
|
if (chunk.answerLine === void 0) return [];
|
|
32119
|
-
const d = decisionOf(chunk);
|
|
32121
|
+
const d = decisionOf$1(chunk);
|
|
32120
32122
|
return [{
|
|
32121
32123
|
name: `Decision: ${d === void 0 ? "open" : CHOICE_NAMES[d.choice] || d.text}`,
|
|
32122
32124
|
range: spanRange(lines, chunk.answerLine, Math.max(chunk.answerLine, end)),
|
|
32123
32125
|
selectionRange: lineRange(lines, chunk.answerLine)
|
|
32124
32126
|
}];
|
|
32125
32127
|
};
|
|
32126
|
-
const isOpenChunk = (chunk, markers) => chunk.files.some((file) => !file.checked) || markers > 0 || chunk.answers !== void 0 && decisionOf(chunk) === void 0;
|
|
32128
|
+
const isOpenChunk = (chunk, markers) => chunk.files.some((file) => !file.checked) || markers > 0 || chunk.answers !== void 0 && decisionOf$1(chunk) === void 0;
|
|
32127
32129
|
/**
|
|
32128
32130
|
* Document outline for `.gtd/REVIEW.md`: the chunks still left to review — an
|
|
32129
32131
|
* unchecked hunk, a footnote (so a human's comment never vanishes with the
|
|
@@ -32333,7 +32335,7 @@ const reviewView = (content) => {
|
|
|
32333
32335
|
},
|
|
32334
32336
|
...chunk.section === void 0 ? {} : { group: REVIEW_SECTIONS[chunk.section] },
|
|
32335
32337
|
...chunk.answers === void 0 ? {} : {
|
|
32336
|
-
answered: decisionOf(chunk) !== void 0,
|
|
32338
|
+
answered: decisionOf$1(chunk) !== void 0,
|
|
32337
32339
|
decision: chunk.answers.map((option, index) => ({
|
|
32338
32340
|
title: option.text,
|
|
32339
32341
|
checked: option.checked,
|
|
@@ -32549,7 +32551,7 @@ const CHOICES = [
|
|
|
32549
32551
|
"wontfix",
|
|
32550
32552
|
"answer"
|
|
32551
32553
|
];
|
|
32552
|
-
const decisionOf = (chunk) => {
|
|
32554
|
+
const decisionOf$1 = (chunk) => {
|
|
32553
32555
|
if (chunk.answers?.length !== 3) return void 0;
|
|
32554
32556
|
const ticked = chunk.answers.flatMap((o, i) => o.checked ? [i] : []);
|
|
32555
32557
|
if (ticked.length !== 1) return void 0;
|
|
@@ -32564,9 +32566,243 @@ const decisionOf = (chunk) => {
|
|
|
32564
32566
|
};
|
|
32565
32567
|
};
|
|
32566
32568
|
//#endregion
|
|
32569
|
+
//#region src/steering/findings.ts
|
|
32570
|
+
/** The `# ` heading the findings a judge called noise sit under. */
|
|
32571
|
+
const NOISE_HEADING = "Dropped as noise";
|
|
32572
|
+
const OPEN_GROUP = "Open";
|
|
32573
|
+
const MARKER = /\[\^[^\]\s]+\]/g;
|
|
32574
|
+
const isBox = (item, content) => {
|
|
32575
|
+
if (typeof item.checked !== "boolean") return false;
|
|
32576
|
+
const first = item.children[0];
|
|
32577
|
+
if (first?.type !== "paragraph" || first.position === void 0) return false;
|
|
32578
|
+
const text = content.slice(first.position.start.offset, first.position.end.offset);
|
|
32579
|
+
return new RegExp(`^fix(?![\\w-])`).test(text);
|
|
32580
|
+
};
|
|
32581
|
+
const slice = (content, node) => content.slice(node.position.start.offset, node.position.end.offset);
|
|
32582
|
+
const lineOf = (node, end = false) => toLspPosition(end ? node.position.end : node.position.start).line;
|
|
32583
|
+
/** The box item's own text past its label: a formatter may fold a guidance line into it. */
|
|
32584
|
+
const inlineGuidance = (content, item) => {
|
|
32585
|
+
const first = item.children[0];
|
|
32586
|
+
return content.slice(first.position.start.offset, item.position.end.offset).slice(3).replace(/^[\s:—–-]+/, "").trim();
|
|
32587
|
+
};
|
|
32588
|
+
const guidanceOf = (parts) => parts.map((p) => p.replace(MARKER, "").trim()).filter((p) => p !== "").join("\n\n");
|
|
32589
|
+
/** `nodes` split at `box`: the body before it, the guidance after it. */
|
|
32590
|
+
const splitAtBox = (content, nodes, box) => {
|
|
32591
|
+
const at = nodes.findIndex((n) => n.type === "list" && n.children.includes(box));
|
|
32592
|
+
const list = nodes[at];
|
|
32593
|
+
const boxAt = list.children.indexOf(box);
|
|
32594
|
+
const bodyNodes = nodes.slice(0, boxAt > 0 ? at + 1 : at);
|
|
32595
|
+
const trailing = list.children.slice(boxAt + 1);
|
|
32596
|
+
const after = nodes.slice(at + 1);
|
|
32597
|
+
return {
|
|
32598
|
+
bodyNodes,
|
|
32599
|
+
body: bodyNodes.map((n) => n === list ? content.slice(n.position.start.offset, box.position.start.offset).trimEnd() : slice(content, n)).join("\n\n"),
|
|
32600
|
+
notBody: list.children.slice(boxAt),
|
|
32601
|
+
guidance: guidanceOf([inlineGuidance(content, box), ...[...trailing, ...after].map((n) => slice(content, n))]),
|
|
32602
|
+
last: after.at(-1) ?? trailing.at(-1) ?? box
|
|
32603
|
+
};
|
|
32604
|
+
};
|
|
32605
|
+
const entryOf = (content, title, noise, headingLine, nodes, endLine) => {
|
|
32606
|
+
const notes = nodes.filter((n) => n.type !== "footnoteDefinition");
|
|
32607
|
+
const boxes = notes.flatMap((n) => n.type === "list" ? n.children.filter((i) => isBox(i, content)) : []);
|
|
32608
|
+
const box = boxes[0];
|
|
32609
|
+
const split = box === void 0 ? {
|
|
32610
|
+
bodyNodes: notes,
|
|
32611
|
+
body: notes.map((n) => slice(content, n)).join("\n\n"),
|
|
32612
|
+
notBody: [],
|
|
32613
|
+
guidance: ""
|
|
32614
|
+
} : splitAtBox(content, notes, box);
|
|
32615
|
+
return {
|
|
32616
|
+
title,
|
|
32617
|
+
body: split.body,
|
|
32618
|
+
noise,
|
|
32619
|
+
fix: box?.checked === true,
|
|
32620
|
+
guidance: split.guidance,
|
|
32621
|
+
boxes: boxes.length,
|
|
32622
|
+
headingLine,
|
|
32623
|
+
bodyNodes: split.bodyNodes,
|
|
32624
|
+
...box === void 0 ? {} : { box },
|
|
32625
|
+
notBody: new Set(split.notBody),
|
|
32626
|
+
lastLine: split.last === void 0 ? headingLine : lineOf(split.last, true),
|
|
32627
|
+
endLine
|
|
32628
|
+
};
|
|
32629
|
+
};
|
|
32630
|
+
const parse = (content) => {
|
|
32631
|
+
const tree = parseMarkdown(content);
|
|
32632
|
+
const entries = [];
|
|
32633
|
+
let noise = false;
|
|
32634
|
+
let open;
|
|
32635
|
+
const close = (endLine) => {
|
|
32636
|
+
if (open !== void 0) entries.push(entryOf(content, open.title, noise, open.line, open.nodes, endLine));
|
|
32637
|
+
open = void 0;
|
|
32638
|
+
};
|
|
32639
|
+
for (const node of tree.children) {
|
|
32640
|
+
if (node.type === "heading" && node.depth <= 2) {
|
|
32641
|
+
close(lineOf(node) - 1);
|
|
32642
|
+
const text = headingText$1(content, node, (t) => t.replace(MARKER, ""));
|
|
32643
|
+
if (node.depth === 1) noise = text === NOISE_HEADING;
|
|
32644
|
+
else open = {
|
|
32645
|
+
title: text,
|
|
32646
|
+
line: lineOf(node),
|
|
32647
|
+
nodes: []
|
|
32648
|
+
};
|
|
32649
|
+
continue;
|
|
32650
|
+
}
|
|
32651
|
+
open?.nodes.push(node);
|
|
32652
|
+
}
|
|
32653
|
+
close(content.split(/\r?\n/).length - 1);
|
|
32654
|
+
return entries;
|
|
32655
|
+
};
|
|
32656
|
+
const entryFindings = (content) => parse(content).flatMap((e) => {
|
|
32657
|
+
if (e.boxes === 0) return [{
|
|
32658
|
+
message: `"${e.title}" has no \`- [ ] fix\` box — restore it, ticked to fix the finding or unticked to drop it`,
|
|
32659
|
+
line: e.headingLine
|
|
32660
|
+
}];
|
|
32661
|
+
if (e.boxes > 1) return [{
|
|
32662
|
+
message: `"${e.title}" has ${e.boxes} \`fix\` boxes — keep exactly one`,
|
|
32663
|
+
line: e.headingLine
|
|
32664
|
+
}];
|
|
32665
|
+
if (!e.fix && e.guidance !== "") return [{
|
|
32666
|
+
message: `"${e.title}" has guidance under an unticked box — tick it to fix the finding, or delete the text to drop it`,
|
|
32667
|
+
line: lineOf(e.box)
|
|
32668
|
+
}];
|
|
32669
|
+
return [];
|
|
32670
|
+
});
|
|
32671
|
+
const findingsView = (content) => {
|
|
32672
|
+
const noteAt = noteLookup(content);
|
|
32673
|
+
return { nodes: parse(content).map((e, index) => {
|
|
32674
|
+
const note = noteAt(e.headingLine);
|
|
32675
|
+
return {
|
|
32676
|
+
title: e.title,
|
|
32677
|
+
anchor: {
|
|
32678
|
+
kind: "chunk",
|
|
32679
|
+
index
|
|
32680
|
+
},
|
|
32681
|
+
group: e.noise ? NOISE_HEADING : OPEN_GROUP,
|
|
32682
|
+
...e.boxes === 1 ? { checked: e.fix } : {},
|
|
32683
|
+
...e.guidance === "" ? {} : { note: e.guidance },
|
|
32684
|
+
...note?.thread !== void 0 ? { thread: note.thread } : {},
|
|
32685
|
+
body: blockNodesOfRun(content, e.bodyNodes, { skipListItem: (i) => e.notBody.has(i) })
|
|
32686
|
+
};
|
|
32687
|
+
}) };
|
|
32688
|
+
};
|
|
32689
|
+
const findingsOutline = (content) => {
|
|
32690
|
+
const lines = content.split(/\r?\n/);
|
|
32691
|
+
return [...parse(content).map((e) => ({
|
|
32692
|
+
name: e.title,
|
|
32693
|
+
detail: e.noise ? "noise" : e.fix ? "fix" : "drop",
|
|
32694
|
+
range: spanRange(lines, e.headingLine, Math.max(e.headingLine, e.endLine)),
|
|
32695
|
+
selectionRange: lineRange(lines, e.headingLine)
|
|
32696
|
+
})), ...threadOutlineNodes(content).map((t) => t.node)];
|
|
32697
|
+
};
|
|
32698
|
+
const findingsAnnotate = (content, anchor, text) => {
|
|
32699
|
+
const entries = parse(content);
|
|
32700
|
+
const lines = content.split(/\r?\n/);
|
|
32701
|
+
const resolved = (() => {
|
|
32702
|
+
if (anchor.kind === "chunk") {
|
|
32703
|
+
const e = entries[anchor.index];
|
|
32704
|
+
return e && {
|
|
32705
|
+
line: e.headingLine,
|
|
32706
|
+
blockEndLine: e.headingLine,
|
|
32707
|
+
key: `chunk:${anchor.index}`
|
|
32708
|
+
};
|
|
32709
|
+
}
|
|
32710
|
+
if (anchor.kind !== "paragraph") return void 0;
|
|
32711
|
+
const block = entries.flatMap((e) => e.bodyNodes).find((n) => anchor.line >= lineOf(n) && anchor.line <= lineOf(n, true));
|
|
32712
|
+
return block && {
|
|
32713
|
+
line: anchor.line,
|
|
32714
|
+
blockEndLine: lineOf(block, true),
|
|
32715
|
+
key: `paragraph:${anchor.line}`
|
|
32716
|
+
};
|
|
32717
|
+
})();
|
|
32718
|
+
if (resolved === void 0) return {
|
|
32719
|
+
ok: false,
|
|
32720
|
+
reason: "anchor-not-found"
|
|
32721
|
+
};
|
|
32722
|
+
const result = footnoteAttachEdits(content, {
|
|
32723
|
+
...resolved,
|
|
32724
|
+
endCharacter: (lines[resolved.line] ?? "").length
|
|
32725
|
+
}, text);
|
|
32726
|
+
return result.ok ? {
|
|
32727
|
+
ok: true,
|
|
32728
|
+
edits: result.edits
|
|
32729
|
+
} : result;
|
|
32730
|
+
};
|
|
32731
|
+
/** Ticks or unticks a finding's box; `text` replaces its guidance, and unticking drops it. */
|
|
32732
|
+
const findingsApply = (content, anchor, opts) => {
|
|
32733
|
+
if (anchor.kind !== "chunk") return {
|
|
32734
|
+
ok: false,
|
|
32735
|
+
reason: "anchor-not-found"
|
|
32736
|
+
};
|
|
32737
|
+
const e = parse(content)[anchor.index];
|
|
32738
|
+
if (e?.box === void 0 || e.boxes !== 1) return {
|
|
32739
|
+
ok: false,
|
|
32740
|
+
reason: "anchor-not-found"
|
|
32741
|
+
};
|
|
32742
|
+
const fix = opts.checked ?? e.fix;
|
|
32743
|
+
const guidance = fix ? (opts.text ?? e.guidance).trim() : "";
|
|
32744
|
+
if (fix === e.fix && guidance === e.guidance) return {
|
|
32745
|
+
ok: true,
|
|
32746
|
+
edits: []
|
|
32747
|
+
};
|
|
32748
|
+
const lines = content.split(/\r?\n/);
|
|
32749
|
+
const eol = eolOf(content);
|
|
32750
|
+
const start = lineOf(e.box);
|
|
32751
|
+
const box = `${lines[start].match(/^\s*[-*+]\s+/)[0]}[${fix ? "x" : " "}] fix`;
|
|
32752
|
+
const text = guidance === "" ? box : `${box}${eol}${eol}${guidance.split(/\r?\n/).join(eol)}`;
|
|
32753
|
+
return {
|
|
32754
|
+
ok: true,
|
|
32755
|
+
edits: [{
|
|
32756
|
+
range: {
|
|
32757
|
+
start: {
|
|
32758
|
+
line: start,
|
|
32759
|
+
character: 0
|
|
32760
|
+
},
|
|
32761
|
+
end: lineRange(lines, e.lastLine).end
|
|
32762
|
+
},
|
|
32763
|
+
newText: text
|
|
32764
|
+
}]
|
|
32765
|
+
};
|
|
32766
|
+
};
|
|
32767
|
+
//#endregion
|
|
32567
32768
|
//#region src/steering/index.ts
|
|
32568
|
-
/** The built-in registry: mode name → its format, in registry order. The single place
|
|
32569
|
-
const REGISTRY = /* @__PURE__ */ new Map([
|
|
32769
|
+
/** The built-in registry: mode name → its format, in registry order. The single place the built-in formats are wired into names — everything else in this module dispatches over a resolved `SteeringFormat`, never a mode-name string. */
|
|
32770
|
+
const REGISTRY = /* @__PURE__ */ new Map([
|
|
32771
|
+
["qa", qaDescriptor],
|
|
32772
|
+
["review", reviewDescriptor],
|
|
32773
|
+
["findings", {
|
|
32774
|
+
sample: `# Quality
|
|
32775
|
+
|
|
32776
|
+
## A finding a lens wrote[^nafind1]
|
|
32777
|
+
|
|
32778
|
+
[^nafind1]:
|
|
32779
|
+
Attached via the phone UI, this note shows a comment on a finding with a
|
|
32780
|
+
\`multi word code span\` that runs past eighty characters in total length.
|
|
32781
|
+
|
|
32782
|
+
What the lens found, and where.
|
|
32783
|
+
|
|
32784
|
+
- [x] fix
|
|
32785
|
+
|
|
32786
|
+
Fix it the simple way.
|
|
32787
|
+
|
|
32788
|
+
# Dropped as noise
|
|
32789
|
+
|
|
32790
|
+
## A finding the judge called noise
|
|
32791
|
+
|
|
32792
|
+
Why the judge dropped it.
|
|
32793
|
+
|
|
32794
|
+
- [ ] fix
|
|
32795
|
+
`,
|
|
32796
|
+
validate: (content) => [...entryFindings(content), ...threadFindings(content)],
|
|
32797
|
+
outline: findingsOutline,
|
|
32798
|
+
actions: (content, range) => threadReplyActions(content, range.start),
|
|
32799
|
+
view: findingsView,
|
|
32800
|
+
annotate: findingsAnnotate,
|
|
32801
|
+
apply: findingsApply,
|
|
32802
|
+
clearTicks: (content) => content,
|
|
32803
|
+
openThreads: openThreadsOf
|
|
32804
|
+
}]
|
|
32805
|
+
]);
|
|
32570
32806
|
[...REGISTRY.keys()];
|
|
32571
32807
|
/** The built-in `SteeringFormat` registered under `mode`'s name, or `undefined` when it isn't a built-in mode at all. The one place a bare mode-name string is ever looked up — every consumer takes the RESOLVED value this returns, never a name, so "mode is required, never inferred from a file's basename" is a type, not a rule to remember. */
|
|
32572
32808
|
const steeringFormatFor = (mode) => REGISTRY.get(mode);
|
|
@@ -32830,53 +33066,6 @@ const NoteSheet = ({ anchor, note, thread, onSave, onDismiss, onDone, busy, titl
|
|
|
32830
33066
|
});
|
|
32831
33067
|
};
|
|
32832
33068
|
//#endregion
|
|
32833
|
-
//#region src/web/notes.ts
|
|
32834
|
-
/** The thread with the human's saved reply applied the way the server applies it: appended to an open thread, replacing the last `H` entry of one waiting on the agent. Idempotent against a refetch that already carries the reply. */
|
|
32835
|
-
const withReply = (thread, text) => {
|
|
32836
|
-
const entries = thread.waitingOn === "human" ? [...thread.entries, {
|
|
32837
|
-
author: "me",
|
|
32838
|
-
text
|
|
32839
|
-
}] : [...thread.entries.slice(0, -1), {
|
|
32840
|
-
author: "me",
|
|
32841
|
-
text
|
|
32842
|
-
}];
|
|
32843
|
-
return {
|
|
32844
|
-
...thread,
|
|
32845
|
-
entries,
|
|
32846
|
-
waitingOn: "agent"
|
|
32847
|
-
};
|
|
32848
|
-
};
|
|
32849
|
-
const sameAnchor = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
32850
|
-
/** Every node, its `body` blocks and its `children`, depth-first. */
|
|
32851
|
-
const flatten = (nodes) => nodes.flatMap((node) => [
|
|
32852
|
-
node,
|
|
32853
|
-
...flatten(node.body ?? []),
|
|
32854
|
-
...flatten(node.children ?? [])
|
|
32855
|
-
]);
|
|
32856
|
-
const nodeAt = (nodes, anchor) => flatten(nodes).find((n) => sameAnchor(n.anchor, anchor));
|
|
32857
|
-
/**
|
|
32858
|
-
* The note text a sheet should open with. A `paragraph` anchor can name
|
|
32859
|
-
* either a top-level prose block OR a block riding in one of `body` (a
|
|
32860
|
-
* question's own body), so the lookup walks both — searching `nodes` alone
|
|
32861
|
-
* reopens a body block's note with an empty field, silently offering to
|
|
32862
|
-
* replace text the reader can still see behind the sheet.
|
|
32863
|
-
*
|
|
32864
|
-
* A thread opens EMPTY for a reply when the agent spoke last, and prefilled
|
|
32865
|
-
* with the last `H` entry when it waits on the agent (saving replaces it).
|
|
32866
|
-
*/
|
|
32867
|
-
const existingNoteFor = (nodes, anchor, overrides) => {
|
|
32868
|
-
const node = nodeAt(nodes, anchor);
|
|
32869
|
-
if (anchor.kind === "paragraph") {
|
|
32870
|
-
const saved = overrides[anchor.line];
|
|
32871
|
-
if (saved !== void 0) return saved;
|
|
32872
|
-
}
|
|
32873
|
-
if (node?.thread) {
|
|
32874
|
-
if (node.thread.waitingOn === "human") return "";
|
|
32875
|
-
return node.thread.entries.findLast((e) => e.author === "me")?.text ?? "";
|
|
32876
|
-
}
|
|
32877
|
-
return anchor.kind === "paragraph" ? node?.note : void 0;
|
|
32878
|
-
};
|
|
32879
|
-
//#endregion
|
|
32880
33069
|
//#region node_modules/zustand/esm/vanilla.mjs
|
|
32881
33070
|
const createStoreImpl = (createState) => {
|
|
32882
33071
|
let state;
|
|
@@ -33354,6 +33543,53 @@ const SaveIndicator = ({ refusal, saveStatus, expanded, failedCount, otherFailed
|
|
|
33354
33543
|
});
|
|
33355
33544
|
};
|
|
33356
33545
|
//#endregion
|
|
33546
|
+
//#region src/web/notes.ts
|
|
33547
|
+
/** The thread with the human's saved reply applied the way the server applies it: appended to an open thread, replacing the last `H` entry of one waiting on the agent. Idempotent against a refetch that already carries the reply. */
|
|
33548
|
+
const withReply = (thread, text) => {
|
|
33549
|
+
const entries = thread.waitingOn === "human" ? [...thread.entries, {
|
|
33550
|
+
author: "me",
|
|
33551
|
+
text
|
|
33552
|
+
}] : [...thread.entries.slice(0, -1), {
|
|
33553
|
+
author: "me",
|
|
33554
|
+
text
|
|
33555
|
+
}];
|
|
33556
|
+
return {
|
|
33557
|
+
...thread,
|
|
33558
|
+
entries,
|
|
33559
|
+
waitingOn: "agent"
|
|
33560
|
+
};
|
|
33561
|
+
};
|
|
33562
|
+
const sameAnchor = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
33563
|
+
/** Every node, its `body` blocks and its `children`, depth-first. */
|
|
33564
|
+
const flatten = (nodes) => nodes.flatMap((node) => [
|
|
33565
|
+
node,
|
|
33566
|
+
...flatten(node.body ?? []),
|
|
33567
|
+
...flatten(node.children ?? [])
|
|
33568
|
+
]);
|
|
33569
|
+
const nodeAt = (nodes, anchor) => flatten(nodes).find((n) => sameAnchor(n.anchor, anchor));
|
|
33570
|
+
/**
|
|
33571
|
+
* The note text a sheet should open with. A `paragraph` anchor can name
|
|
33572
|
+
* either a top-level prose block OR a block riding in one of `body` (a
|
|
33573
|
+
* question's own body), so the lookup walks both — searching `nodes` alone
|
|
33574
|
+
* reopens a body block's note with an empty field, silently offering to
|
|
33575
|
+
* replace text the reader can still see behind the sheet.
|
|
33576
|
+
*
|
|
33577
|
+
* A thread opens EMPTY for a reply when the agent spoke last, and prefilled
|
|
33578
|
+
* with the last `H` entry when it waits on the agent (saving replaces it).
|
|
33579
|
+
*/
|
|
33580
|
+
const existingNoteFor = (nodes, anchor, overrides) => {
|
|
33581
|
+
const node = nodeAt(nodes, anchor);
|
|
33582
|
+
if (anchor.kind === "paragraph") {
|
|
33583
|
+
const saved = overrides[anchor.line];
|
|
33584
|
+
if (saved !== void 0) return saved;
|
|
33585
|
+
}
|
|
33586
|
+
if (node?.thread) {
|
|
33587
|
+
if (node.thread.waitingOn === "human") return "";
|
|
33588
|
+
return node.thread.entries.findLast((e) => e.author === "me")?.text ?? "";
|
|
33589
|
+
}
|
|
33590
|
+
return anchor.kind === "paragraph" ? node?.note : void 0;
|
|
33591
|
+
};
|
|
33592
|
+
//#endregion
|
|
33357
33593
|
//#region src/web/screens/InlineRun.tsx
|
|
33358
33594
|
/**
|
|
33359
33595
|
* One projected `image`'s own rendering — its own component (not a case
|
|
@@ -33594,6 +33830,255 @@ const ProseBlocks = ({ nodes, noteOverrides, onOpenNote, readOnly }) => /* @__PU
|
|
|
33594
33830
|
}, node.anchor.kind === "paragraph" ? node.anchor.line : index))]
|
|
33595
33831
|
});
|
|
33596
33832
|
//#endregion
|
|
33833
|
+
//#region src/web/screens/Findings.tsx
|
|
33834
|
+
const MODE = "findings";
|
|
33835
|
+
const NOISE_GROUP = "Dropped as noise";
|
|
33836
|
+
const decisionOf = (node) => ({
|
|
33837
|
+
fix: node.checked === true,
|
|
33838
|
+
guidance: node.note ?? ""
|
|
33839
|
+
});
|
|
33840
|
+
const FindingRow = ({ node, index, decision, onToggle, onEditGuidance }) => {
|
|
33841
|
+
const id = `finding-fix-${index}`;
|
|
33842
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
|
|
33843
|
+
"data-testid": `finding-${index}`,
|
|
33844
|
+
className: `border-b border-divider px-3 py-3 ${decision.fix ? "border-l-4 border-l-accent bg-surface" : ""}`,
|
|
33845
|
+
children: [
|
|
33846
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
|
|
33847
|
+
htmlFor: id,
|
|
33848
|
+
className: "flex min-h-11 items-start gap-3",
|
|
33849
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
33850
|
+
id,
|
|
33851
|
+
type: "checkbox",
|
|
33852
|
+
"data-testid": `finding-checkbox-${index}`,
|
|
33853
|
+
className: "mt-1 size-5 shrink-0",
|
|
33854
|
+
checked: decision.fix,
|
|
33855
|
+
onChange: (e) => onToggle(e.target.checked)
|
|
33856
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
33857
|
+
className: "min-w-0 flex-1",
|
|
33858
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
33859
|
+
className: "block font-medium",
|
|
33860
|
+
children: node.title
|
|
33861
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
33862
|
+
className: "block text-small text-muted",
|
|
33863
|
+
children: decision.fix ? "Fix" : "Drop"
|
|
33864
|
+
})]
|
|
33865
|
+
})]
|
|
33866
|
+
}),
|
|
33867
|
+
node.body !== void 0 && node.body.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
33868
|
+
className: "pl-8",
|
|
33869
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProseBlocks, {
|
|
33870
|
+
nodes: node.body,
|
|
33871
|
+
noteOverrides: {},
|
|
33872
|
+
onOpenNote: () => {},
|
|
33873
|
+
readOnly: true
|
|
33874
|
+
})
|
|
33875
|
+
}),
|
|
33876
|
+
decision.fix && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
33877
|
+
className: "pl-8",
|
|
33878
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
33879
|
+
variant: "ghost",
|
|
33880
|
+
"data-testid": `finding-guidance-${index}`,
|
|
33881
|
+
onClick: onEditGuidance,
|
|
33882
|
+
"aria-label": decision.guidance === "" ? void 0 : `Edit guidance: ${decision.guidance}`,
|
|
33883
|
+
className: "w-full px-0 text-left text-small font-normal text-muted",
|
|
33884
|
+
children: decision.guidance !== "" ? decision.guidance : "Add guidance for the fixer (optional)"
|
|
33885
|
+
})
|
|
33886
|
+
})
|
|
33887
|
+
]
|
|
33888
|
+
});
|
|
33889
|
+
};
|
|
33890
|
+
const Group = ({ title, entries, render }) => entries.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
|
|
33891
|
+
"aria-label": title,
|
|
33892
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
33893
|
+
className: "m-0 px-3 pt-4 pb-1 text-small font-semibold text-muted uppercase",
|
|
33894
|
+
children: title
|
|
33895
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
|
|
33896
|
+
className: "m-0 list-none p-0",
|
|
33897
|
+
children: entries.map(({ node, index }) => render(node, index))
|
|
33898
|
+
})]
|
|
33899
|
+
});
|
|
33900
|
+
/** The finding gate: tick a finding to fix it, with optional guidance; unticked ones are dropped. */
|
|
33901
|
+
const FindingsView = ({ view, filePath, content, isLoading, readError, onDecide, onDone, busy }) => {
|
|
33902
|
+
const overlay = useWriteStore((s) => s.overlay);
|
|
33903
|
+
const save = useWriteStore((s) => s.save);
|
|
33904
|
+
const [editing, setEditing] = (0, import_react.useState)(void 0);
|
|
33905
|
+
if (view === void 0) {
|
|
33906
|
+
const refusal = readError === void 0 ? void 0 : readRefusalFrom(readError);
|
|
33907
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Notice, {
|
|
33908
|
+
tone: isLoading ? "info" : "error",
|
|
33909
|
+
children: isLoading ? "Loading the findings…" : refusal !== void 0 ? messageForReadRefusal(refusal) : "Could not load the findings."
|
|
33910
|
+
});
|
|
33911
|
+
}
|
|
33912
|
+
const decisionAt = (node) => overlay[cellKey(filePath, node.anchor, "decision")]?.value ?? decisionOf(node);
|
|
33913
|
+
const decide = (node, next) => {
|
|
33914
|
+
save({
|
|
33915
|
+
cell: cellKey(filePath, node.anchor, "decision"),
|
|
33916
|
+
optimistic: next,
|
|
33917
|
+
write: onDecide?.(node.anchor, next.fix ? {
|
|
33918
|
+
checked: true,
|
|
33919
|
+
text: next.guidance
|
|
33920
|
+
} : { checked: false })
|
|
33921
|
+
}).catch(() => {});
|
|
33922
|
+
};
|
|
33923
|
+
const entries = view.nodes.map((node, index) => ({
|
|
33924
|
+
node,
|
|
33925
|
+
index
|
|
33926
|
+
}));
|
|
33927
|
+
const problems = content === void 0 ? [] : steeringFormatFor(MODE).validate(content);
|
|
33928
|
+
const editingNode = editing === void 0 ? void 0 : view.nodes[editing];
|
|
33929
|
+
const fixing = view.nodes.filter((n) => decisionAt(n).fix).length;
|
|
33930
|
+
const row = (node, index) => {
|
|
33931
|
+
const decision = decisionAt(node);
|
|
33932
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FindingRow, {
|
|
33933
|
+
node,
|
|
33934
|
+
index,
|
|
33935
|
+
decision,
|
|
33936
|
+
onToggle: (fix) => decide(node, {
|
|
33937
|
+
fix,
|
|
33938
|
+
guidance: fix ? decision.guidance : ""
|
|
33939
|
+
}),
|
|
33940
|
+
onEditGuidance: () => setEditing(index)
|
|
33941
|
+
}, index);
|
|
33942
|
+
};
|
|
33943
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
33944
|
+
"data-testid": "findings-screen",
|
|
33945
|
+
className: "flex h-full min-h-0 flex-1 flex-col",
|
|
33946
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
33947
|
+
className: "min-h-0 flex-1 overflow-auto",
|
|
33948
|
+
children: [
|
|
33949
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
33950
|
+
className: "m-0 px-3 pt-3 text-small text-muted",
|
|
33951
|
+
children: "Tick a finding to have it fixed; an unticked one is dropped. Anything bigger belongs in the final review."
|
|
33952
|
+
}),
|
|
33953
|
+
view.nodes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Notice, {
|
|
33954
|
+
"data-testid": "findings-empty",
|
|
33955
|
+
children: "No findings to decide on."
|
|
33956
|
+
}),
|
|
33957
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Group, {
|
|
33958
|
+
title: "Open",
|
|
33959
|
+
entries: entries.filter(({ node }) => node.group !== NOISE_GROUP),
|
|
33960
|
+
render: row
|
|
33961
|
+
}),
|
|
33962
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Group, {
|
|
33963
|
+
title: NOISE_GROUP,
|
|
33964
|
+
entries: entries.filter(({ node }) => node.group === NOISE_GROUP),
|
|
33965
|
+
render: row
|
|
33966
|
+
}),
|
|
33967
|
+
problems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Notice, {
|
|
33968
|
+
tone: "error",
|
|
33969
|
+
"data-testid": "findings-problems",
|
|
33970
|
+
role: "status",
|
|
33971
|
+
children: problems.map((p) => p.message).join(" ")
|
|
33972
|
+
})
|
|
33973
|
+
]
|
|
33974
|
+
}), onDone !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
33975
|
+
className: "flex shrink-0 items-center justify-between gap-3 border-t border-border p-3",
|
|
33976
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
33977
|
+
"data-testid": "findings-summary",
|
|
33978
|
+
className: "text-small text-muted",
|
|
33979
|
+
children: `${fixing} to fix, ${view.nodes.length - fixing} to drop`
|
|
33980
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
33981
|
+
variant: "primary",
|
|
33982
|
+
"data-testid": "findings-done",
|
|
33983
|
+
onClick: () => {
|
|
33984
|
+
save({
|
|
33985
|
+
cell: cellKey(filePath, void 0, "done"),
|
|
33986
|
+
optimistic: true,
|
|
33987
|
+
mutate: onDone
|
|
33988
|
+
}).catch(() => {});
|
|
33989
|
+
},
|
|
33990
|
+
disabled: busy === true || problems.length > 0,
|
|
33991
|
+
children: "Done"
|
|
33992
|
+
})]
|
|
33993
|
+
})]
|
|
33994
|
+
}), editingNode !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoteSheet, {
|
|
33995
|
+
anchor: editingNode.anchor,
|
|
33996
|
+
title: "Guidance for the fixer",
|
|
33997
|
+
label: "Guidance",
|
|
33998
|
+
note: decisionAt(editingNode).guidance,
|
|
33999
|
+
onSave: (_anchor, text) => {
|
|
34000
|
+
setEditing(void 0);
|
|
34001
|
+
decide(editingNode, {
|
|
34002
|
+
fix: true,
|
|
34003
|
+
guidance: text.trim()
|
|
34004
|
+
});
|
|
34005
|
+
},
|
|
34006
|
+
onDismiss: () => setEditing(void 0)
|
|
34007
|
+
})] });
|
|
34008
|
+
};
|
|
34009
|
+
const HandedBackPanel$3 = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Notice, {
|
|
34010
|
+
"data-testid": "handed-back-panel",
|
|
34011
|
+
role: "status",
|
|
34012
|
+
"aria-live": "polite",
|
|
34013
|
+
children: "Handed back — this turn is done."
|
|
34014
|
+
});
|
|
34015
|
+
const FindingsInner = ({ filePath, data, isLoading, error }) => {
|
|
34016
|
+
const indicatorProps = useSaveIndicatorProps();
|
|
34017
|
+
const pending = useWriteStore((s) => s.pendingCount);
|
|
34018
|
+
const [formatNotice, setFormatNotice] = (0, import_react.useState)(void 0);
|
|
34019
|
+
const utils = trpc.useUtils();
|
|
34020
|
+
const setValue = trpc.setValue.useMutation({ onSettled: () => utils.readSteeringFile.invalidate({
|
|
34021
|
+
filePath,
|
|
34022
|
+
mode: MODE
|
|
34023
|
+
}) });
|
|
34024
|
+
const done = trpc.done.useMutation();
|
|
34025
|
+
const onDecide = (anchor, opts) => (cas) => setValue.mutateAsync({
|
|
34026
|
+
filePath,
|
|
34027
|
+
...cas,
|
|
34028
|
+
mode: MODE,
|
|
34029
|
+
anchor,
|
|
34030
|
+
...opts
|
|
34031
|
+
}).then((result) => {
|
|
34032
|
+
setFormatNotice(result.formatNotice);
|
|
34033
|
+
return result;
|
|
34034
|
+
});
|
|
34035
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
34036
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SaveIndicator, { ...indicatorProps }),
|
|
34037
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FormatNoticeBanner, {
|
|
34038
|
+
notice: formatNotice,
|
|
34039
|
+
onDismiss: () => setFormatNotice(void 0)
|
|
34040
|
+
}),
|
|
34041
|
+
done.isSuccess ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HandedBackPanel$3, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FindingsView, {
|
|
34042
|
+
view: data?.view,
|
|
34043
|
+
content: data?.content,
|
|
34044
|
+
filePath,
|
|
34045
|
+
isLoading,
|
|
34046
|
+
readError: error,
|
|
34047
|
+
onDecide,
|
|
34048
|
+
onDone: () => done.mutateAsync({}),
|
|
34049
|
+
busy: pending > 0
|
|
34050
|
+
})
|
|
34051
|
+
] });
|
|
34052
|
+
};
|
|
34053
|
+
/** The finding gate's screen: reads `.gtd/QUALITY.md` as the `findings` format and writes each decision back. */
|
|
34054
|
+
const Findings = ({ filePath }) => {
|
|
34055
|
+
const utils = trpc.useUtils();
|
|
34056
|
+
const query = trpc.readSteeringFile.useQuery({
|
|
34057
|
+
filePath,
|
|
34058
|
+
mode: MODE
|
|
34059
|
+
});
|
|
34060
|
+
const refetchTokens = async () => {
|
|
34061
|
+
const fresh = await utils.readSteeringFile.fetch({
|
|
34062
|
+
filePath,
|
|
34063
|
+
mode: MODE
|
|
34064
|
+
});
|
|
34065
|
+
return {
|
|
34066
|
+
expectedHeadSha: fresh.headSha,
|
|
34067
|
+
expectedContentHash: fresh.contentHash
|
|
34068
|
+
};
|
|
34069
|
+
};
|
|
34070
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WriteStoreProvider, {
|
|
34071
|
+
tokens: query.data,
|
|
34072
|
+
refetchTokens,
|
|
34073
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FindingsInner, {
|
|
34074
|
+
filePath,
|
|
34075
|
+
data: query.data,
|
|
34076
|
+
isLoading: query.isLoading,
|
|
34077
|
+
error: query.error
|
|
34078
|
+
})
|
|
34079
|
+
});
|
|
34080
|
+
};
|
|
34081
|
+
//#endregion
|
|
33597
34082
|
//#region src/web/screens/FreeForm.tsx
|
|
33598
34083
|
/**
|
|
33599
34084
|
* The empty-document save's own anchor line: `freeform.ts#freeFormApply`'s
|
|
@@ -35432,7 +35917,7 @@ const openable = (step) => step?.status === "ok" && step.file !== void 0;
|
|
|
35432
35917
|
* and no route back to one. `trpc.step`'s own `mode` picks the screen —
|
|
35433
35918
|
* `"review"` always means `Review` (that screen's own `readSteeringFile` call
|
|
35434
35919
|
* is hardcoded to `mode: "review"`, matching `.gtd/REVIEW.md`'s one format);
|
|
35435
|
-
* `"qa"` means `Plan`; anything else (absent, or an unregistered mode name)
|
|
35920
|
+
* `"qa"` means `Plan`; `"findings"` means `Findings`; anything else (absent, or an unregistered mode name)
|
|
35436
35921
|
* falls through to `FreeForm`, which reads the file as plain markdown and
|
|
35437
35922
|
* offers only a note per block, plus a textfield when it's empty. Neither
|
|
35438
35923
|
* screen offers a way back to a list — `done` is the only
|
|
@@ -35479,7 +35964,7 @@ const App = () => {
|
|
|
35479
35964
|
children: step.mode === "review" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Review, { filePath: step.file }) : step.mode === "qa" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plan, {
|
|
35480
35965
|
filePath: step.file,
|
|
35481
35966
|
mode: "qa"
|
|
35482
|
-
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FreeForm, {
|
|
35967
|
+
}) : step.mode === "findings" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Findings, { filePath: step.file }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FreeForm, {
|
|
35483
35968
|
filePath: step.file,
|
|
35484
35969
|
mode: step.mode
|
|
35485
35970
|
})
|