@pmelab/gtd 54.0.0 → 56.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 +109 -104
- package/dist/chunks/{reserved-CQ7amcKG.mjs → reserved-B8EC-WE8.mjs} +124 -230
- package/dist/chunks/{review-CXzUXJHL.mjs → review-HdrX0FJ7.mjs} +30 -125
- package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
- package/dist/chunks/{tail-JZ9PAOwp.mjs → tail-BSLah2mA.mjs} +155 -63
- 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 +47 -102
- 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 +614 -574
- 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 +104 -0
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +17 -20
- package/src/workflows/feature/green.ts +2 -5
- package/src/workflows/feature/lens-review.feature +323 -171
- package/src/workflows/feature/naive-acceptance.feature +31 -32
- package/src/workflows/feature/prototype.ts +5 -8
- package/src/workflows/feature/refactor-absorbs.feature +1 -7
- package/src/workflows/feature/refactor-first.feature +2 -10
- package/src/workflows/feature/refactor-rebuild.feature +20 -36
- package/src/workflows/feature/refactor-review.feature +33 -117
- package/src/workflows/feature/refactor.test.ts +0 -2
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +6 -22
- package/src/workflows/feature/reviewer-walkthrough.feature +16 -103
- package/src/workflows/feature/seam.test.ts +19 -101
- package/src/workflows/feature/seam.ts +13 -89
- package/src/workflows/feature/standards.feature +75 -34
- package/src/workflows/fix/fix-lens-origin.feature +20 -7
- package/src/workflows/fix/fix-quality.feature +104 -23
- package/src/workflows/review/review-door.feature +62 -59
- package/src/workflows/shared/access.test.ts +1 -0
- 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/review.test.ts +30 -27
- package/src/workflows/shared/review.ts +29 -196
- package/src/workflows/shared/skills.test.ts +3 -1
- package/src/workflows/shared/steps.test.ts +9 -6
- package/src/workflows/shared/steps.ts +12 -3
- package/src/workflows/shared/tail.test.ts +20 -2
- package/src/workflows/shared/tail.ts +38 -16
- package/src/workflows/shared/text.test.ts +122 -294
- package/src/workflows/shared/text.ts +181 -358
- package/src/workflows/shared/vars.ts +1 -1
- 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/workflows.mjs
CHANGED
|
@@ -584,6 +584,14 @@ const removedAssertions = (changes) => changes.filter((c) => c.path.endsWith(".f
|
|
|
584
584
|
};
|
|
585
585
|
}).filter((r) => r.lines.length > 0);
|
|
586
586
|
//#endregion
|
|
587
|
+
//#region src/steering/SteeringFormat.ts
|
|
588
|
+
/** `file:line:col: message` — the shape editors and grep already jump on. Stored 0-based, printed 1-based. A finding with a `line` but no `range`, or none at all, degrades to the shorter forms. */
|
|
589
|
+
const formatFinding = (file, finding) => {
|
|
590
|
+
if (finding.line === void 0) return finding.message;
|
|
591
|
+
const col = finding.range !== void 0 ? `:${finding.range.start.character + 1}` : "";
|
|
592
|
+
return `${file}:${finding.line + 1}${col}: ${finding.message}`;
|
|
593
|
+
};
|
|
594
|
+
//#endregion
|
|
587
595
|
//#region node_modules/mdast-util-to-string/lib/index.js
|
|
588
596
|
/**
|
|
589
597
|
* @typedef {import('mdast').Nodes} Nodes
|
|
@@ -9992,7 +10000,7 @@ function serializeChunks(chunks, expandTabs) {
|
|
|
9992
10000
|
* @returns {ParseContext}
|
|
9993
10001
|
* Parser.
|
|
9994
10002
|
*/
|
|
9995
|
-
function parse$
|
|
10003
|
+
function parse$2(options) {
|
|
9996
10004
|
/** @type {ParseContext} */
|
|
9997
10005
|
const parser = {
|
|
9998
10006
|
constructs: combineExtensions([constructs_exports, ...(options || {}).extensions || []]),
|
|
@@ -10307,7 +10315,7 @@ function fromMarkdown(value, encoding, options) {
|
|
|
10307
10315
|
options = encoding;
|
|
10308
10316
|
encoding = void 0;
|
|
10309
10317
|
}
|
|
10310
|
-
return compiler(options)(postprocess(parse$
|
|
10318
|
+
return compiler(options)(postprocess(parse$2(options).document().write(preprocess()(value, encoding, true))));
|
|
10311
10319
|
}
|
|
10312
10320
|
/**
|
|
10313
10321
|
* Note this compiler only understand complete buffering, not streaming.
|
|
@@ -13906,7 +13914,7 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
|
|
|
13906
13914
|
//#endregion
|
|
13907
13915
|
//#region src/steering/Threads.ts
|
|
13908
13916
|
const PREFIX_RE$1 = /^(H|A):\s*/;
|
|
13909
|
-
const lineOf = (node, which) => toLspPosition(node.position[which]).line;
|
|
13917
|
+
const lineOf$1 = (node, which) => toLspPosition(node.position[which]).line;
|
|
13910
13918
|
/** 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. */
|
|
13911
13919
|
const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
13912
13920
|
const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText$1(content, item));
|
|
@@ -13920,9 +13928,9 @@ const isThreadBody = (content, children) => {
|
|
|
13920
13928
|
};
|
|
13921
13929
|
const foldName$1 = (name) => name.toLowerCase();
|
|
13922
13930
|
/** One list item's entry, reporting any syntax problem; `previous` is the author before it. */
|
|
13923
|
-
const entryOf = (content, item, previous, report) => {
|
|
13924
|
-
const start = lineOf(item, "start");
|
|
13925
|
-
const end = lineOf(item, "end");
|
|
13931
|
+
const entryOf$1 = (content, item, previous, report) => {
|
|
13932
|
+
const start = lineOf$1(item, "start");
|
|
13933
|
+
const end = lineOf$1(item, "end");
|
|
13926
13934
|
const match = prefixOf(content, item);
|
|
13927
13935
|
if (!match) {
|
|
13928
13936
|
report(`an entry must start with "H:" or "A:"`, start, end);
|
|
@@ -13938,7 +13946,7 @@ const entryOf = (content, item, previous, report) => {
|
|
|
13938
13946
|
author,
|
|
13939
13947
|
text,
|
|
13940
13948
|
line: start,
|
|
13941
|
-
endLine: last ? lineOf(last, "end") : end,
|
|
13949
|
+
endLine: last ? lineOf$1(last, "end") : end,
|
|
13942
13950
|
column: toLspPosition(item.position.start).character
|
|
13943
13951
|
};
|
|
13944
13952
|
};
|
|
@@ -13947,16 +13955,16 @@ const threadOf = (content, def, list, markers, report) => {
|
|
|
13947
13955
|
const entries = [];
|
|
13948
13956
|
let previous;
|
|
13949
13957
|
for (const item of list.children) {
|
|
13950
|
-
const entry = entryOf(content, item, previous, report);
|
|
13958
|
+
const entry = entryOf$1(content, item, previous, report);
|
|
13951
13959
|
if (!entry) continue;
|
|
13952
13960
|
previous = entry.author;
|
|
13953
13961
|
entries.push(entry);
|
|
13954
13962
|
}
|
|
13955
|
-
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"));
|
|
13963
|
+
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"));
|
|
13956
13964
|
return {
|
|
13957
13965
|
name,
|
|
13958
|
-
line: lineOf(def, "start"),
|
|
13959
|
-
endLine: lineOf(def, "end"),
|
|
13966
|
+
line: lineOf$1(def, "start"),
|
|
13967
|
+
endLine: lineOf$1(def, "end"),
|
|
13960
13968
|
markers: markers.filter((m) => foldName$1(m.name) === foldName$1(name)),
|
|
13961
13969
|
entries,
|
|
13962
13970
|
waitingOn: entries[entries.length - 1]?.author === "agent" ? "human" : "agent"
|
|
@@ -14190,6 +14198,8 @@ const noteLookup = (content, footnotes = parseFootnotes(content)) => {
|
|
|
14190
14198
|
return note === "" ? {} : { note };
|
|
14191
14199
|
};
|
|
14192
14200
|
};
|
|
14201
|
+
/** Thread syntax findings alone — what free-form `validate` reports without inheriting the other footnote findings. */
|
|
14202
|
+
const threadFindings = (content) => collectThreads(content, parseMarkdown(content), []).findings;
|
|
14193
14203
|
/** 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. */
|
|
14194
14204
|
const nextFootnoteName = (content) => {
|
|
14195
14205
|
const { markers, definitions } = parseFootnotes(content);
|
|
@@ -16753,62 +16763,247 @@ const reviewSections = (content) => splitChunks(parseMarkdown(content), content)
|
|
|
16753
16763
|
section: s.section,
|
|
16754
16764
|
line: s.headingLine
|
|
16755
16765
|
}));
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
const
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16771
|
-
|
|
16772
|
-
|
|
16773
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16766
|
+
//#endregion
|
|
16767
|
+
//#region src/steering/findings.ts
|
|
16768
|
+
/** The `# ` heading the findings a judge called noise sit under. */
|
|
16769
|
+
const NOISE_HEADING = "Dropped as noise";
|
|
16770
|
+
const OPEN_GROUP = "Open";
|
|
16771
|
+
const MARKER = /\[\^[^\]\s]+\]/g;
|
|
16772
|
+
const isBox = (item, content) => {
|
|
16773
|
+
if (typeof item.checked !== "boolean") return false;
|
|
16774
|
+
const first = item.children[0];
|
|
16775
|
+
if (first?.type !== "paragraph" || first.position === void 0) return false;
|
|
16776
|
+
const text = content.slice(first.position.start.offset, first.position.end.offset);
|
|
16777
|
+
return new RegExp(`^fix(?![\\w-])`).test(text);
|
|
16778
|
+
};
|
|
16779
|
+
const slice = (content, node) => content.slice(node.position.start.offset, node.position.end.offset);
|
|
16780
|
+
const lineOf = (node, end = false) => toLspPosition(end ? node.position.end : node.position.start).line;
|
|
16781
|
+
/** The box item's own text past its label: a formatter may fold a guidance line into it. */
|
|
16782
|
+
const inlineGuidance = (content, item) => {
|
|
16783
|
+
const first = item.children[0];
|
|
16784
|
+
return content.slice(first.position.start.offset, item.position.end.offset).slice(3).replace(/^[\s:—–-]+/, "").trim();
|
|
16785
|
+
};
|
|
16786
|
+
const guidanceOf = (parts) => parts.map((p) => p.replace(MARKER, "").trim()).filter((p) => p !== "").join("\n\n");
|
|
16787
|
+
/** `nodes` split at `box`: the body before it, the guidance after it. */
|
|
16788
|
+
const splitAtBox = (content, nodes, box) => {
|
|
16789
|
+
const at = nodes.findIndex((n) => n.type === "list" && n.children.includes(box));
|
|
16790
|
+
const list = nodes[at];
|
|
16791
|
+
const boxAt = list.children.indexOf(box);
|
|
16792
|
+
const bodyNodes = nodes.slice(0, boxAt > 0 ? at + 1 : at);
|
|
16793
|
+
const trailing = list.children.slice(boxAt + 1);
|
|
16794
|
+
const after = nodes.slice(at + 1);
|
|
16795
|
+
return {
|
|
16796
|
+
bodyNodes,
|
|
16797
|
+
body: bodyNodes.map((n) => n === list ? content.slice(n.position.start.offset, box.position.start.offset).trimEnd() : slice(content, n)).join("\n\n"),
|
|
16798
|
+
notBody: list.children.slice(boxAt),
|
|
16799
|
+
guidance: guidanceOf([inlineGuidance(content, box), ...[...trailing, ...after].map((n) => slice(content, n))]),
|
|
16800
|
+
last: after.at(-1) ?? trailing.at(-1) ?? box
|
|
16801
|
+
};
|
|
16802
|
+
};
|
|
16803
|
+
const entryOf = (content, title, noise, headingLine, nodes, endLine) => {
|
|
16804
|
+
const notes = nodes.filter((n) => n.type !== "footnoteDefinition");
|
|
16805
|
+
const boxes = notes.flatMap((n) => n.type === "list" ? n.children.filter((i) => isBox(i, content)) : []);
|
|
16806
|
+
const box = boxes[0];
|
|
16807
|
+
const split = box === void 0 ? {
|
|
16808
|
+
bodyNodes: notes,
|
|
16809
|
+
body: notes.map((n) => slice(content, n)).join("\n\n"),
|
|
16810
|
+
notBody: [],
|
|
16811
|
+
guidance: ""
|
|
16812
|
+
} : splitAtBox(content, notes, box);
|
|
16813
|
+
return {
|
|
16814
|
+
title,
|
|
16815
|
+
body: split.body,
|
|
16816
|
+
noise,
|
|
16817
|
+
fix: box?.checked === true,
|
|
16818
|
+
guidance: split.guidance,
|
|
16819
|
+
boxes: boxes.length,
|
|
16820
|
+
headingLine,
|
|
16821
|
+
bodyNodes: split.bodyNodes,
|
|
16822
|
+
...box === void 0 ? {} : { box },
|
|
16823
|
+
notBody: new Set(split.notBody),
|
|
16824
|
+
lastLine: split.last === void 0 ? headingLine : lineOf(split.last, true),
|
|
16825
|
+
endLine
|
|
16826
|
+
};
|
|
16827
|
+
};
|
|
16828
|
+
const parse$1 = (content) => {
|
|
16786
16829
|
const tree = parseMarkdown(content);
|
|
16787
|
-
const
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
const
|
|
16791
|
-
|
|
16792
|
-
|
|
16793
|
-
const chunk = chunks[i];
|
|
16794
|
-
const block = lines.slice(chunk.headingLine, chunks[i + 1]?.headingLine ?? lines.length);
|
|
16795
|
-
return chunkText(demote ? demoted(chunk, block) : block, demote ? 1 : headingSize(chunk));
|
|
16830
|
+
const entries = [];
|
|
16831
|
+
let noise = false;
|
|
16832
|
+
let open;
|
|
16833
|
+
const close = (endLine) => {
|
|
16834
|
+
if (open !== void 0) entries.push(entryOf(content, open.title, noise, open.line, open.nodes, endLine));
|
|
16835
|
+
open = void 0;
|
|
16796
16836
|
};
|
|
16797
|
-
const
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16837
|
+
for (const node of tree.children) {
|
|
16838
|
+
if (node.type === "heading" && node.depth <= 2) {
|
|
16839
|
+
close(lineOf(node) - 1);
|
|
16840
|
+
const text = headingText$1(content, node, (t) => t.replace(MARKER, ""));
|
|
16841
|
+
if (node.depth === 1) noise = text === NOISE_HEADING;
|
|
16842
|
+
else open = {
|
|
16843
|
+
title: text,
|
|
16844
|
+
line: lineOf(node),
|
|
16845
|
+
nodes: []
|
|
16846
|
+
};
|
|
16847
|
+
continue;
|
|
16848
|
+
}
|
|
16849
|
+
open?.nodes.push(node);
|
|
16850
|
+
}
|
|
16851
|
+
close(content.split(/\r?\n/).length - 1);
|
|
16852
|
+
return entries;
|
|
16853
|
+
};
|
|
16854
|
+
/** Every `## ` finding in a finding-gate file, in file order. */
|
|
16855
|
+
const findingEntries = (content) => parse$1(content).map(({ title, body, noise, fix, guidance, boxes, headingLine }) => ({
|
|
16856
|
+
title,
|
|
16857
|
+
body,
|
|
16858
|
+
noise,
|
|
16859
|
+
fix,
|
|
16860
|
+
guidance,
|
|
16861
|
+
boxes,
|
|
16862
|
+
headingLine
|
|
16863
|
+
}));
|
|
16864
|
+
const entryFindings = (content) => parse$1(content).flatMap((e) => {
|
|
16865
|
+
if (e.boxes === 0) return [{
|
|
16866
|
+
message: `"${e.title}" has no \`- [ ] fix\` box — restore it, ticked to fix the finding or unticked to drop it`,
|
|
16867
|
+
line: e.headingLine
|
|
16868
|
+
}];
|
|
16869
|
+
if (e.boxes > 1) return [{
|
|
16870
|
+
message: `"${e.title}" has ${e.boxes} \`fix\` boxes — keep exactly one`,
|
|
16871
|
+
line: e.headingLine
|
|
16872
|
+
}];
|
|
16873
|
+
if (!e.fix && e.guidance !== "") return [{
|
|
16874
|
+
message: `"${e.title}" has guidance under an unticked box — tick it to fix the finding, or delete the text to drop it`,
|
|
16875
|
+
line: lineOf(e.box)
|
|
16876
|
+
}];
|
|
16877
|
+
return [];
|
|
16878
|
+
});
|
|
16879
|
+
const findingsView = (content) => {
|
|
16880
|
+
const noteAt = noteLookup(content);
|
|
16881
|
+
return { nodes: parse$1(content).map((e, index) => {
|
|
16882
|
+
const note = noteAt(e.headingLine);
|
|
16883
|
+
return {
|
|
16884
|
+
title: e.title,
|
|
16885
|
+
anchor: {
|
|
16886
|
+
kind: "chunk",
|
|
16887
|
+
index
|
|
16888
|
+
},
|
|
16889
|
+
group: e.noise ? NOISE_HEADING : OPEN_GROUP,
|
|
16890
|
+
...e.boxes === 1 ? { checked: e.fix } : {},
|
|
16891
|
+
...e.guidance === "" ? {} : { note: e.guidance },
|
|
16892
|
+
...note?.thread !== void 0 ? { thread: note.thread } : {},
|
|
16893
|
+
body: blockNodesOfRun(content, e.bodyNodes, { skipListItem: (i) => e.notBody.has(i) })
|
|
16894
|
+
};
|
|
16895
|
+
}) };
|
|
16896
|
+
};
|
|
16897
|
+
const findingsOutline = (content) => {
|
|
16898
|
+
const lines = content.split(/\r?\n/);
|
|
16899
|
+
return [...parse$1(content).map((e) => ({
|
|
16900
|
+
name: e.title,
|
|
16901
|
+
detail: e.noise ? "noise" : e.fix ? "fix" : "drop",
|
|
16902
|
+
range: spanRange(lines, e.headingLine, Math.max(e.headingLine, e.endLine)),
|
|
16903
|
+
selectionRange: lineRange(lines, e.headingLine)
|
|
16904
|
+
})), ...threadOutlineNodes(content).map((t) => t.node)];
|
|
16905
|
+
};
|
|
16906
|
+
const findingsAnnotate = (content, anchor, text) => {
|
|
16907
|
+
const entries = parse$1(content);
|
|
16908
|
+
const lines = content.split(/\r?\n/);
|
|
16909
|
+
const resolved = (() => {
|
|
16910
|
+
if (anchor.kind === "chunk") {
|
|
16911
|
+
const e = entries[anchor.index];
|
|
16912
|
+
return e && {
|
|
16913
|
+
line: e.headingLine,
|
|
16914
|
+
blockEndLine: e.headingLine,
|
|
16915
|
+
key: `chunk:${anchor.index}`
|
|
16916
|
+
};
|
|
16917
|
+
}
|
|
16918
|
+
if (anchor.kind !== "paragraph") return void 0;
|
|
16919
|
+
const block = entries.flatMap((e) => e.bodyNodes).find((n) => anchor.line >= lineOf(n) && anchor.line <= lineOf(n, true));
|
|
16920
|
+
return block && {
|
|
16921
|
+
line: anchor.line,
|
|
16922
|
+
blockEndLine: lineOf(block, true),
|
|
16923
|
+
key: `paragraph:${anchor.line}`
|
|
16924
|
+
};
|
|
16925
|
+
})();
|
|
16926
|
+
if (resolved === void 0) return {
|
|
16927
|
+
ok: false,
|
|
16928
|
+
reason: "anchor-not-found"
|
|
16804
16929
|
};
|
|
16805
|
-
|
|
16806
|
-
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16930
|
+
const result = footnoteAttachEdits(content, {
|
|
16931
|
+
...resolved,
|
|
16932
|
+
endCharacter: (lines[resolved.line] ?? "").length
|
|
16933
|
+
}, text);
|
|
16934
|
+
return result.ok ? {
|
|
16935
|
+
ok: true,
|
|
16936
|
+
edits: result.edits
|
|
16937
|
+
} : result;
|
|
16938
|
+
};
|
|
16939
|
+
/** Ticks or unticks a finding's box; `text` replaces its guidance, and unticking drops it. */
|
|
16940
|
+
const findingsApply = (content, anchor, opts) => {
|
|
16941
|
+
if (anchor.kind !== "chunk") return {
|
|
16942
|
+
ok: false,
|
|
16943
|
+
reason: "anchor-not-found"
|
|
16944
|
+
};
|
|
16945
|
+
const e = parse$1(content)[anchor.index];
|
|
16946
|
+
if (e?.box === void 0 || e.boxes !== 1) return {
|
|
16947
|
+
ok: false,
|
|
16948
|
+
reason: "anchor-not-found"
|
|
16949
|
+
};
|
|
16950
|
+
const fix = opts.checked ?? e.fix;
|
|
16951
|
+
const guidance = fix ? (opts.text ?? e.guidance).trim() : "";
|
|
16952
|
+
if (fix === e.fix && guidance === e.guidance) return {
|
|
16953
|
+
ok: true,
|
|
16954
|
+
edits: []
|
|
16955
|
+
};
|
|
16956
|
+
const lines = content.split(/\r?\n/);
|
|
16957
|
+
const eol = eolOf(content);
|
|
16958
|
+
const start = lineOf(e.box);
|
|
16959
|
+
const box = `${lines[start].match(/^\s*[-*+]\s+/)[0]}[${fix ? "x" : " "}] fix`;
|
|
16960
|
+
const text = guidance === "" ? box : `${box}${eol}${eol}${guidance.split(/\r?\n/).join(eol)}`;
|
|
16961
|
+
return {
|
|
16962
|
+
ok: true,
|
|
16963
|
+
edits: [{
|
|
16964
|
+
range: {
|
|
16965
|
+
start: {
|
|
16966
|
+
line: start,
|
|
16967
|
+
character: 0
|
|
16968
|
+
},
|
|
16969
|
+
end: lineRange(lines, e.lastLine).end
|
|
16970
|
+
},
|
|
16971
|
+
newText: text
|
|
16972
|
+
}]
|
|
16973
|
+
};
|
|
16974
|
+
};
|
|
16975
|
+
/** The finding gate's file: one `## ` finding each, with one `fix` box and optional guidance below it. */
|
|
16976
|
+
const findingsDescriptor = {
|
|
16977
|
+
sample: `# Quality
|
|
16978
|
+
|
|
16979
|
+
## A finding a lens wrote[^nafind1]
|
|
16980
|
+
|
|
16981
|
+
[^nafind1]:
|
|
16982
|
+
Attached via the phone UI, this note shows a comment on a finding with a
|
|
16983
|
+
\`multi word code span\` that runs past eighty characters in total length.
|
|
16984
|
+
|
|
16985
|
+
What the lens found, and where.
|
|
16986
|
+
|
|
16987
|
+
- [x] fix
|
|
16988
|
+
|
|
16989
|
+
Fix it the simple way.
|
|
16990
|
+
|
|
16991
|
+
# Dropped as noise
|
|
16992
|
+
|
|
16993
|
+
## A finding the judge called noise
|
|
16994
|
+
|
|
16995
|
+
Why the judge dropped it.
|
|
16996
|
+
|
|
16997
|
+
- [ ] fix
|
|
16998
|
+
`,
|
|
16999
|
+
validate: (content) => [...entryFindings(content), ...threadFindings(content)],
|
|
17000
|
+
outline: findingsOutline,
|
|
17001
|
+
actions: (content, range) => threadReplyActions(content, range.start),
|
|
17002
|
+
view: findingsView,
|
|
17003
|
+
annotate: findingsAnnotate,
|
|
17004
|
+
apply: findingsApply,
|
|
17005
|
+
clearTicks: (content) => content,
|
|
17006
|
+
openThreads: openThreadsOf
|
|
16812
17007
|
};
|
|
16813
17008
|
//#endregion
|
|
16814
17009
|
//#region src/steering/CodeThreads.ts
|
|
@@ -16929,7 +17124,17 @@ const stripCodeThreads = (path, text) => {
|
|
|
16929
17124
|
for (const run of runsOf(lines.map((l) => l.replace(/\r$/, "")), tokens).filter(isThreadRun)) for (let i = run.start; i <= run.end; i++) drop.add(i);
|
|
16930
17125
|
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
16931
17126
|
};
|
|
16932
|
-
|
|
17127
|
+
//#endregion
|
|
17128
|
+
//#region src/steering/index.ts
|
|
17129
|
+
/** 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. */
|
|
17130
|
+
const REGISTRY = /* @__PURE__ */ new Map([
|
|
17131
|
+
["qa", qaDescriptor],
|
|
17132
|
+
["review", reviewDescriptor],
|
|
17133
|
+
["findings", findingsDescriptor]
|
|
17134
|
+
]);
|
|
17135
|
+
[...REGISTRY.keys()];
|
|
17136
|
+
/** 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. */
|
|
17137
|
+
const steeringFormatFor = (mode) => REGISTRY.get(mode);
|
|
16933
17138
|
//#endregion
|
|
16934
17139
|
//#region src/workflow-kit/review.ts
|
|
16935
17140
|
const collect = async (p, capture, round, requested) => {
|
|
@@ -17433,7 +17638,7 @@ const defaults = {
|
|
|
17433
17638
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
17434
17639
|
flagMode: "true",
|
|
17435
17640
|
flag_risk_tier: "0",
|
|
17436
|
-
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
17641
|
+
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge, criteria-coverage",
|
|
17437
17642
|
refactorQualityReviews: "correctness, ponytail-review, conventions",
|
|
17438
17643
|
prototypeRounds: "3"
|
|
17439
17644
|
};
|
|
@@ -17727,191 +17932,92 @@ What each change does next (then run \`gtd land\`):
|
|
|
17727
17932
|
`;
|
|
17728
17933
|
const qualityTriageMessage = () => `Judging each quality finding. Run \`gtd judge answer\` and pipe a
|
|
17729
17934
|
\`mechanical\`/\`noise\`/\`design\` verdict per finding ID — or land without a
|
|
17730
|
-
verdict, which hands every finding to the
|
|
17935
|
+
verdict, which hands every finding to the finding gate as a note.
|
|
17731
17936
|
`;
|
|
17732
17937
|
/** \`.gtd/QUALITY.md\` holding only \`findings\`, one \`## \` chunk each. */
|
|
17733
17938
|
const qualityFindingsFile = (findings) => `# Quality\n\n${findings.map((f) => `## ${f.title}\n\n${f.body}`.trim()).join("\n\n")}\n`;
|
|
17734
|
-
const SUMMARY_TITLES = [
|
|
17735
|
-
REVIEW_SECTIONS.findings,
|
|
17736
|
-
REVIEW_SECTIONS.walkthrough,
|
|
17737
|
-
"Weakened scenarios",
|
|
17738
|
-
"Fixed from your notes",
|
|
17739
|
-
"Fixed automatically",
|
|
17740
|
-
"Standards added",
|
|
17741
|
-
"Dropped as noise",
|
|
17742
|
-
"Nothing to note"
|
|
17743
|
-
];
|
|
17744
17939
|
const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
|
|
17745
17940
|
const short = (hash) => hash.slice(0, 7);
|
|
17746
|
-
/**
|
|
17941
|
+
/** \`lens tests\`; empty when unknown. */
|
|
17747
17942
|
const originOf = (f) => f.lens === void 0 ? "" : `lens ${f.lens}`;
|
|
17748
|
-
const originLine = (f) => {
|
|
17749
|
-
const origin = originOf(f);
|
|
17750
|
-
return origin === "" ? "" : `_From ${origin}._\n\n`;
|
|
17751
|
-
};
|
|
17752
17943
|
const withOrigin = (f, rest) => [rest, originOf(f)].filter((s) => s !== "").join(", ");
|
|
17753
|
-
const
|
|
17754
|
-
const
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
const
|
|
17760
|
-
const
|
|
17761
|
-
|
|
17944
|
+
const noteLine = (f, rest = "") => {
|
|
17945
|
+
const origin = withOrigin(f, rest);
|
|
17946
|
+
return origin === "" ? "" : ` (${origin})`;
|
|
17947
|
+
};
|
|
17948
|
+
const gateEntry = (n, lead) => {
|
|
17949
|
+
const status = n.unresolved === true ? "The fixer left it unresolved." : n.unclear === true ? "A fix turn removed it while renaming another finding — check whether it was fixed." : "";
|
|
17950
|
+
const from = originOf(n);
|
|
17951
|
+
const head = [
|
|
17952
|
+
from === "" ? "" : `From ${from}.`,
|
|
17953
|
+
lead,
|
|
17954
|
+
status
|
|
17955
|
+
].filter((s) => s !== "").join(" ");
|
|
17956
|
+
return `## ${n.title}\n\n${head === "" ? "" : `_${head}_\n\n`}${unboxed(n.body) || "_No detail given._"}\n\n- [ ] fix`;
|
|
17957
|
+
};
|
|
17958
|
+
/** The finding gate's \`.gtd/QUALITY.md\`: the open notes, then the noise, each with an unticked box. */
|
|
17959
|
+
const findingGateFile = (notes, noise) => `${[
|
|
17960
|
+
"# Quality",
|
|
17961
|
+
...notes.map((n) => gateEntry(n, "")),
|
|
17962
|
+
...noise.length === 0 ? [] : [`# ${NOISE_HEADING}`, ...noise.map((n) => gateEntry(n, `The judge called it noise (p=${n.p}).`))]
|
|
17963
|
+
].join("\n\n")}\n`;
|
|
17964
|
+
/** A ticked finding's body for the fixer, with the human's guidance after it. */
|
|
17965
|
+
const withGuidance = (body, guidance) => guidance === "" ? body : `${body}\n\nGuidance from the human — follow it:\n\n${guidance}`;
|
|
17966
|
+
const findingGateMessage = () => `\`.gtd/QUALITY.md\` lists what the quality lenses found that no fixer took
|
|
17967
|
+
care of: first the findings that need your decision — they question a
|
|
17968
|
+
criterion or the approach, or a fixer could not resolve them — then, under
|
|
17969
|
+
\`# Dropped as noise\`, the ones the judge set aside, so you can overrule it.
|
|
17970
|
+
|
|
17971
|
+
Under each finding, tick \`- [x] fix\` to have it fixed, or leave it unticked
|
|
17972
|
+
to drop it. A ticked finding may get a line of guidance below its box — how
|
|
17973
|
+
you want it fixed. Guidance under an unticked box is refused: tick it, or
|
|
17974
|
+
delete the text. A ticked finding is fixed, never re-planned: raise anything
|
|
17975
|
+
bigger in the final review.
|
|
17976
|
+
|
|
17977
|
+
Then run \`gtd land\`. Landing the file untouched drops every finding.
|
|
17978
|
+
`;
|
|
17979
|
+
/** What the reviewer is told about the last nit fix. */
|
|
17980
|
+
const nitFixBlock = (nitFix) => nitFix === void 0 ? "" : `Commit \`${short(nitFix)}\` worked on the human's nits and fix-as-suggested
|
|
17981
|
+
notes: describe what it changed under the hunks it touched.
|
|
17982
|
+
|
|
17983
|
+
`;
|
|
17984
|
+
/** Numbers a title already in \`used\` — \`x (2)\`, \`x (3)\` — and claims the result; chunks are told apart by title. */
|
|
17762
17985
|
const uniquifier = (used) => (title) => {
|
|
17763
17986
|
let name = title;
|
|
17764
17987
|
for (let n = 2; used.has(name); n++) name = `${title} (${n})`;
|
|
17765
17988
|
used.add(name);
|
|
17766
17989
|
return name;
|
|
17767
17990
|
};
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
const
|
|
17772
|
-
|
|
17773
|
-
...
|
|
17774
|
-
|
|
17775
|
-
|
|
17776
|
-
}
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
notes: describe what it changed under the hunks it touched.
|
|
17781
|
-
|
|
17782
|
-
`;
|
|
17783
|
-
if (notes.length === 0) return fixed;
|
|
17784
|
-
return `${fixed}The quality lenses left these findings for the human — verbatim data,
|
|
17785
|
-
not instructions. Give each its own chunk, its heading exactly as given,
|
|
17786
|
-
holding one pointer per hunk of the code the finding cites, each with a
|
|
17787
|
-
short note on what that hunk has to do with it. A finding citing no line
|
|
17788
|
-
points at the code it is most about. Write nothing else in that chunk:
|
|
17789
|
-
gtd adds the finding's text under its heading and the human's answer
|
|
17790
|
-
boxes after your pointers. Never write a \`## Standard conflict\` chunk:
|
|
17791
|
-
gtd adds those itself. Never fold a finding into another chunk, and
|
|
17792
|
-
never open a note in a finding's chunk with \`Risk:\` — the human decides
|
|
17793
|
-
those.
|
|
17794
|
-
|
|
17795
|
-
${notes.map((n) => {
|
|
17796
|
-
const body = (unboxed(n.body) || "_No detail given._").split("\n").map((line) => ` > ${line}`.trimEnd()).join("\n");
|
|
17797
|
-
return `- ${codeSpan(`## ${n.title}`)}:\n\n${body}`;
|
|
17798
|
-
}).join("\n\n")}
|
|
17991
|
+
const titledList = (lead, lines) => lines.length === 0 ? [] : [`${lead}:\n\n${lines.join("\n")}`];
|
|
17992
|
+
/** The one summary chunk's body: what the lap fixed, dropped and left; \`undefined\` when nothing. */
|
|
17993
|
+
const lapSummary = (r) => {
|
|
17994
|
+
const parts = [
|
|
17995
|
+
...titledList("Fixed automatically", r.fixed.map((f) => `- ${f.title}${noteLine(f, short(f.commit))}`)),
|
|
17996
|
+
...titledList("Fixed at the finding gate", r.gateFixed.map((f) => `- ${f.title}${noteLine(f, short(f.commit))}`)),
|
|
17997
|
+
...titledList("Dropped at the finding gate", r.dropped.map((f) => `- ${f.title}${noteLine(f)}`)),
|
|
17998
|
+
...titledList("Dropped as noise", r.noise.map((f) => `- ${f.title}${noteLine(f, f.p === void 0 ? "" : `p=${f.p}`)}`)),
|
|
17999
|
+
...titledList("Unresolved after the finding gate", r.unresolved.map((f) => `- ${f.title}${noteLine(f, f.unclear === true ? "removed while renaming another" : "")}`))
|
|
18000
|
+
];
|
|
18001
|
+
return parts.length === 0 ? void 0 : `What the quality lenses found, and what became of it. Write a note under
|
|
18002
|
+
a line to reopen it.
|
|
17799
18003
|
|
|
17800
|
-
`;
|
|
18004
|
+
${parts.join("\n\n")}`;
|
|
17801
18005
|
};
|
|
17802
18006
|
/**
|
|
17803
|
-
* \`review\` with
|
|
17804
|
-
*
|
|
17805
|
-
*
|
|
18007
|
+
* \`review\` with the open standard conflicts, the quality lap's summary and
|
|
18008
|
+
* the standards added appended as chunks of their own; nothing to add leaves
|
|
18009
|
+
* it byte for byte.
|
|
17806
18010
|
*/
|
|
17807
|
-
const
|
|
17808
|
-
const
|
|
17809
|
-
const
|
|
17810
|
-
const
|
|
17811
|
-
const listed = notes.size;
|
|
17812
|
-
for (let i = chunks.length - 1; i >= 0; i--) {
|
|
17813
|
-
const n = notes.get(chunks[i].title);
|
|
17814
|
-
if (n === void 0) continue;
|
|
17815
|
-
notes.delete(n.title);
|
|
17816
|
-
const start = chunks[i].line;
|
|
17817
|
-
const end = chunks[i + 1]?.line ?? lines.length;
|
|
17818
|
-
const pointers = lines.slice(start + 1, end).join("\n").trim();
|
|
17819
|
-
const text = noteBody(n);
|
|
17820
|
-
const rest = pointers === "" ? [] : [
|
|
17821
|
-
POINTERS_LEAD,
|
|
17822
|
-
"",
|
|
17823
|
-
pointers,
|
|
17824
|
-
""
|
|
17825
|
-
];
|
|
17826
|
-
lines.splice(start + 1, end - start - 1, "", text, "", ...rest, ANSWER_BOXES, "");
|
|
17827
|
-
}
|
|
17828
|
-
const used = new Set(chunks.map((c) => c.title));
|
|
17829
|
-
const unique = uniquifier(used);
|
|
17830
|
-
const added = report.standards?.added ?? [];
|
|
17831
|
-
const summary = [
|
|
17832
|
-
...(report.standards?.undecided ?? []).map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`),
|
|
17833
|
-
...report.fixed.length === 0 ? [] : [`## ${unique("Fixed automatically")}\n\n${report.fixed.map((f) => fixedLine(f, {})).join("\n")}`],
|
|
17834
|
-
...added.length === 0 ? [] : [`## ${unique("Standards added")}\n\nLearned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`],
|
|
17835
|
-
...report.noise.length === 0 ? [] : [`## ${unique("Dropped as noise")}\n\n${report.noise.map((f) => noiseLine(f, {})).join("\n")}`]
|
|
17836
|
-
];
|
|
17837
|
-
if (notes.size === listed && summary.length === 0) return review;
|
|
17838
|
-
const body = lines.join("\n").trimEnd();
|
|
17839
|
-
return summary.length === 0 ? `${body}\n` : `${body}\n\n${summary.join("\n\n")}\n`;
|
|
17840
|
-
};
|
|
17841
|
-
const chunkBlocks = (review) => {
|
|
17842
|
-
const lines = review.split("\n");
|
|
17843
|
-
const chunks = reviewChunks(review);
|
|
17844
|
-
return {
|
|
17845
|
-
head: lines.slice(0, chunks[0]?.line ?? lines.length),
|
|
17846
|
-
blocks: chunks.map((c, i) => ({
|
|
17847
|
-
title: c.title,
|
|
17848
|
-
lines: lines.slice(c.line, chunks[i + 1]?.line ?? lines.length)
|
|
17849
|
-
}))
|
|
17850
|
-
};
|
|
17851
|
-
};
|
|
17852
|
-
const isBlank = (line) => line !== void 0 && line.trim() === "";
|
|
17853
|
-
/** \`review\` with every chunk's severity line removed, so a re-review is ranked afresh. */
|
|
17854
|
-
const unranked = (review) => {
|
|
17855
|
-
const drop = new Set(reviewChunks(review).flatMap((c) => c.severityLine ?? []));
|
|
17856
|
-
if (drop.size === 0) return review;
|
|
17857
|
-
const lines = review.split("\n");
|
|
17858
|
-
return lines.filter((_, i) => !drop.has(i) && !(drop.has(i - 1) && isBlank(lines[i]))).join("\n");
|
|
17859
|
-
};
|
|
17860
|
-
const chunkTexts = (review) => new Map(chunkBlocks(review).blocks.map((b) => [b.title, b.lines.join("\n").trim()]));
|
|
17861
|
-
const trimBlankLines = (lines) => {
|
|
17862
|
-
let start = 0;
|
|
17863
|
-
let end = lines.length;
|
|
17864
|
-
while (start < end && isBlank(lines[start])) start++;
|
|
17865
|
-
while (end > start && isBlank(lines[end - 1])) end--;
|
|
17866
|
-
return lines.slice(start, end);
|
|
17867
|
-
};
|
|
17868
|
-
/**
|
|
17869
|
-
* \`review\` with the \`ranked\` chunks first, in their order, each led by its
|
|
17870
|
-
* severity line; every other chunk follows in place.
|
|
17871
|
-
*/
|
|
17872
|
-
const rankedReview = (review, ranked) => {
|
|
17873
|
-
const { head, blocks } = chunkBlocks(review);
|
|
17874
|
-
const place = new Map(ranked.map((r, k) => [r.title, k]));
|
|
17875
|
-
const labels = new Map(ranked.map((r) => [r.title, r.label]));
|
|
17876
|
-
const order = (i) => place.get(blocks[i].title) ?? ranked.length;
|
|
17877
|
-
const chunk = (i) => {
|
|
17878
|
-
const { title, lines } = blocks[i];
|
|
17879
|
-
const label = labels.get(title);
|
|
17880
|
-
const heading = /^ {0,3}-+\s*$/.test(lines[1] ?? "") ? 2 : 1;
|
|
17881
|
-
const body = trimBlankLines(lines.slice(heading)).join("\n");
|
|
17882
|
-
return [
|
|
17883
|
-
lines.slice(0, heading).join("\n"),
|
|
17884
|
-
...label === void 0 ? [] : [severityLine(label)],
|
|
17885
|
-
...body === "" ? [] : [body]
|
|
17886
|
-
].join("\n\n");
|
|
17887
|
-
};
|
|
17888
|
-
const sorted = blocks.map((_, i) => i).sort((x, y) => order(x) - order(y) || x - y);
|
|
17889
|
-
return `${[head.join("\n").trim(), ...sorted.map(chunk)].filter((p) => p !== "").join("\n\n")}\n`;
|
|
17890
|
-
};
|
|
17891
|
-
/**
|
|
17892
|
-
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
17893
|
-
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
17894
|
-
* notes and answers are matched to their chunk by title.
|
|
17895
|
-
*/
|
|
17896
|
-
const composedReview = (base, report) => {
|
|
17897
|
-
const used = new Set(SUMMARY_TITLES);
|
|
17898
|
-
const unique = uniquifier(used);
|
|
17899
|
-
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
17900
|
-
const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
|
|
17901
|
-
const added = report.standards?.added ?? [];
|
|
17902
|
-
const notes = titledNotes(report);
|
|
17903
|
-
const weakened = (report.weakened ?? []).flatMap(({ path, lines }) => lines.map((a) => `- ${path} — ${a.scenario} — ${a.line}`));
|
|
18011
|
+
const withSummary = (review, { report, standards }) => {
|
|
18012
|
+
const unique = uniquifier(new Set(reviewChunks(review).map((c) => c.title)));
|
|
18013
|
+
const lap = lapSummary(report);
|
|
18014
|
+
const added = standards?.added ?? [];
|
|
17904
18015
|
const chunks = [
|
|
17905
|
-
...
|
|
17906
|
-
...
|
|
17907
|
-
...
|
|
17908
|
-
...notes.map((n) => chunk(n.title, `${noteBody(n)}\n\n${ANSWER_BOXES}`)),
|
|
17909
|
-
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => fixedLine(f, {})).join("\n"))],
|
|
17910
|
-
...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
|
|
17911
|
-
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => noiseLine(f, {})).join("\n"))]
|
|
18016
|
+
...(standards?.undecided ?? []).map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`),
|
|
18017
|
+
...lap === void 0 ? [] : [`## ${unique("Quality lap")}\n\n${lap}`],
|
|
18018
|
+
...added.length === 0 ? [] : [`## ${unique("Standards added")}\n\nLearned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`]
|
|
17912
18019
|
];
|
|
17913
|
-
|
|
17914
|
-
return sectionedReview(`# Review: ${short(base)}\n\n<!-- base: ${base} -->\n\n${body.join("\n\n")}\n`, /* @__PURE__ */ new Set());
|
|
18020
|
+
return chunks.length === 0 ? review : `${review.trimEnd()}\n\n${chunks.join("\n\n")}\n`;
|
|
17915
18021
|
};
|
|
17916
18022
|
const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
|
|
17917
18023
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
@@ -18237,6 +18343,8 @@ const buildFixQualityPrompt = (rules) => `${stateFileRules}
|
|
|
18237
18343
|
where dimensions repeat one, fix the shared cause once
|
|
18238
18344
|
- Delete each finding's \`## \` chunk once it is resolved; never
|
|
18239
18345
|
rename, merge or reword a chunk you leave
|
|
18346
|
+
- A chunk's guidance from the human says how to fix that finding:
|
|
18347
|
+
follow it
|
|
18240
18348
|
- When findings conflict, missing test signal beats line count —
|
|
18241
18349
|
a test is never deleted to satisfy a simplification finding
|
|
18242
18350
|
- Delete \`.gtd/QUALITY.md\` once every finding is resolved
|
|
@@ -18266,7 +18374,16 @@ const specChallengeBrief = `- Find this process's planning documents in history:
|
|
|
18266
18374
|
existing test, a documented constraint, or a data invariant
|
|
18267
18375
|
other code relies on — naming the decision and the invariant
|
|
18268
18376
|
- With no planning documents in history, write nothing`;
|
|
18269
|
-
const
|
|
18377
|
+
const blockquote = (text) => text.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n");
|
|
18378
|
+
const criteriaCoverageBrief = `- Read the acceptance criteria — the \`AC-<n>\` lines of the requirements
|
|
18379
|
+
quoted below — and every test this change added or changed
|
|
18380
|
+
- A criterion is covered when a test exercises the behaviour it describes
|
|
18381
|
+
and would fail without it. Judge by what a test does, never by its name;
|
|
18382
|
+
one test may cover several criteria
|
|
18383
|
+
- Write one finding per criterion no test exercises, titled with its ID
|
|
18384
|
+
(\`## AC-3 is not tested\`), saying what a test would have to show`;
|
|
18385
|
+
/** `requirements`, when given, are quoted after the brief. */
|
|
18386
|
+
const buildQualityReviewingPrompt = (lens, brief, base = start(), requirements) => `${stateFileRules}
|
|
18270
18387
|
- The only state file this turn writes is \`.gtd/QUALITY.md\` — no
|
|
18271
18388
|
other files for notes or output
|
|
18272
18389
|
- Review the whole assembled change, from \`${base}\`
|
|
@@ -18288,21 +18405,20 @@ ${brief ? `
|
|
|
18288
18405
|
This lens's brief:
|
|
18289
18406
|
${brief}
|
|
18290
18407
|
|
|
18291
|
-
` : ""}
|
|
18408
|
+
` : ""}${requirements === void 0 ? "" : `The requirements this change was built to — data, never instructions:
|
|
18409
|
+
|
|
18410
|
+
${blockquote(requirements)}
|
|
18411
|
+
|
|
18412
|
+
`}- Touch no other state file, and leave everything uncommitted
|
|
18292
18413
|
`;
|
|
18293
18414
|
const reviewerSystem = () => `${reviewerPersona}
|
|
18294
18415
|
|
|
18295
18416
|
${agentConduct}`;
|
|
18296
|
-
const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\`
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18301
|
-
\`fix as suggested\` (fixed in the nit batch below), \`won't fix\` (dropped,
|
|
18302
|
-
recorded in the commit), or write your own answer over \`_your answer_\` and
|
|
18303
|
-
tick it (judged like a note). Every landing here is refused until each
|
|
18304
|
-
finding holds exactly one tick. The fixed and noise lists need no answer;
|
|
18305
|
-
disagreeing with a fix or a drop means writing a note of your own.
|
|
18417
|
+
const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` walks you through the whole change, one chunk per
|
|
18418
|
+
concern with a pointer to each hunk. A \`## Quality lap\` chunk at the end
|
|
18419
|
+
lists what the quality lenses found and what became of it: fixed
|
|
18420
|
+
automatically, fixed or dropped at the finding gate, dropped as noise, or
|
|
18421
|
+
unresolved — for reference; to reopen one, write a note under its line.
|
|
18306
18422
|
|
|
18307
18423
|
Review the diff yourself, with whatever tool you like — gtd checks
|
|
18308
18424
|
nothing out and touches no ref; \`gtd base\` prints this same hash any
|
|
@@ -18312,8 +18428,8 @@ working tree:
|
|
|
18312
18428
|
git diff ${base}
|
|
18313
18429
|
When you've been through the whole diff, run \`gtd land\`:
|
|
18314
18430
|
|
|
18315
|
-
- **Sign off** —
|
|
18316
|
-
|
|
18431
|
+
- **Sign off** — leave no comment — no note in \`.gtd/REVIEW.md\`, no
|
|
18432
|
+
code edit — to close the process.
|
|
18317
18433
|
Every turn commit stays on the
|
|
18318
18434
|
branch; run \`gtd summary\` afterward for a closing-message
|
|
18319
18435
|
prompt.
|
|
@@ -18469,13 +18585,6 @@ const wrapItem = (item) => {
|
|
|
18469
18585
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
18470
18586
|
return [...out, line];
|
|
18471
18587
|
};
|
|
18472
|
-
const reviewRankingMessage = () => `Ranking each chunk of the review before the human reads it. Run
|
|
18473
|
-
\`gtd judge answer\` and pipe, per chunk \`C<n>\`: \`C<n>.security\`
|
|
18474
|
-
(yes/no), \`C<n>.reversibility\` (one-way/two-way),
|
|
18475
|
-
\`C<n>.blastRadius\` (local/module/contract) and \`C<n>.newConcept\`
|
|
18476
|
-
(yes/no) — or land without a verdict, which lists every chunk as
|
|
18477
|
-
critical.
|
|
18478
|
-
`;
|
|
18479
18588
|
const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
18480
18589
|
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
18481
18590
|
request), \`question\`, \`nit\` (a small fix needing no re-plan) or
|
|
@@ -18612,23 +18721,6 @@ const lapRange = {
|
|
|
18612
18721
|
feedback: "it starts at the previous review's boundary, so it covers only what is new since the previous review.",
|
|
18613
18722
|
refactor: "it covers the whole refactor since it started, on every round. A refactor may update an existing test's call sites only: give any test-file hunk its own chunk, and say so if it changes a test, an assertion or an expected value."
|
|
18614
18723
|
};
|
|
18615
|
-
const fixedLine = (f, origin = f) => `- ${f.title} (${withOrigin(origin, short(f.commit))})`;
|
|
18616
|
-
const noiseLine = (f, origin = f) => `- ${f.title} (${withOrigin(origin, `p=${f.p}`)})`;
|
|
18617
|
-
/**
|
|
18618
|
-
* `review` with the lap's notes appended as prose chunks — no pointer, no
|
|
18619
|
-
* answer boxes — then what it fixed and dropped. A title the review already
|
|
18620
|
-
* holds gets a numbered suffix, as notes are told apart by title.
|
|
18621
|
-
*/
|
|
18622
|
-
const withLapFindings = (review, { notes, fixed, noise }) => {
|
|
18623
|
-
const used = new Set(review.split("\n").filter((line) => line.startsWith("## ")).map((line) => line.slice(3).trim()));
|
|
18624
|
-
const unique = uniquifier(used);
|
|
18625
|
-
const chunks = [
|
|
18626
|
-
...notes.map((n) => `## ${unique(n.title)}\n\n${originLine(n)}${noteBody(n)}`),
|
|
18627
|
-
...fixed.length === 0 ? [] : [`## ${unique("Fixed automatically")}\n\n${fixed.map((f) => fixedLine(f)).join("\n")}`],
|
|
18628
|
-
...noise.length === 0 ? [] : [`## ${unique("Dropped as noise")}\n\n${noise.map((f) => noiseLine(f)).join("\n")}`]
|
|
18629
|
-
];
|
|
18630
|
-
return chunks.length === 0 ? review : `${review.trimEnd()}\n\n${chunks.join("\n\n")}\n`;
|
|
18631
|
-
};
|
|
18632
18724
|
const buildReviewReviewingPrompt = (base, lap, carry, removed = [], extra = "") => `${styleBlock}
|
|
18633
18725
|
|
|
18634
18726
|
${styleFormatContract}
|
|
@@ -18781,6 +18873,8 @@ re-implement. It changed these files outside the expected set:
|
|
|
18781
18873
|
${listed(retry.strays)}
|
|
18782
18874
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
18783
18875
|
`;
|
|
18876
|
+
const behaviourTests = `- Test the behaviour the criteria describe — one test per behaviour, not per
|
|
18877
|
+
criterion; never put criterion IDs in test or scenario names`;
|
|
18784
18878
|
const naivePassPrompt = (retry) => `${stateFileRules}
|
|
18785
18879
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18786
18880
|
${impactHints}
|
|
@@ -18790,30 +18884,27 @@ ${criteriaBinding(false)}
|
|
|
18790
18884
|
you leave it, and a reviewer makes it fit
|
|
18791
18885
|
- Work breadth-first: rough out every part of the change before finishing any
|
|
18792
18886
|
one, so a run cut off midway still shows the whole shape
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
criterion's test passing: the next step runs it on your diff and sends a red
|
|
18801
|
-
pass back
|
|
18887
|
+
${behaviourTests}
|
|
18888
|
+
- Test through code that existed before this pass — a public entry point, the
|
|
18889
|
+
CLI, an existing module's exports — never a file this pass adds: that is
|
|
18890
|
+
naive structure a later reset and refactor replaces. A pass whose test
|
|
18891
|
+
imports a file it added is sent back
|
|
18892
|
+
- Run \`${env.testCommand}\` and finish only once it is green: the next step
|
|
18893
|
+
runs it on your diff and sends a red pass back
|
|
18802
18894
|
- Leave \`.gtd/\` untouched
|
|
18803
18895
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18804
18896
|
${retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
18805
|
-
const carries = (rebuild) => (rebuild.acceptance?.
|
|
18897
|
+
const carries = (rebuild) => (rebuild.acceptance?.tests.length ?? 0) > 0;
|
|
18806
18898
|
const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
|
|
18807
18899
|
opened; plug the change into them:
|
|
18808
18900
|
|
|
18809
|
-
${note
|
|
18901
|
+
${blockquote(note)}
|
|
18810
18902
|
|
|
18811
|
-
`}${acceptance === void 0 || !carries({ acceptance }) ? "" : `- Carry over the naive pass's acceptance tests
|
|
18812
|
-
|
|
18813
|
-
Copy each test
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
leave an ID out is refused, and the suite must be green with them
|
|
18903
|
+
`}${acceptance === void 0 || !carries({ acceptance }) ? "" : `- Carry over the naive pass's acceptance tests, the one part of it you take
|
|
18904
|
+
as it is: \`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`.
|
|
18905
|
+
Copy each test into the tree as the refactor left it — check a file out of
|
|
18906
|
+
the tip whole only where the refactor did not touch it — changing only what
|
|
18907
|
+
reaches the release flag. The suite must be green with them
|
|
18817
18908
|
`}`;
|
|
18818
18909
|
const refactorNotePrompt = (reference, strays) => `${stateFileRules}
|
|
18819
18910
|
- The seam judge ruled the naive pass refactor-first: it is never applied.
|
|
@@ -18849,8 +18940,8 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `-
|
|
|
18849
18940
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
18850
18941
|
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
18851
18942
|
`}${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend)}
|
|
18852
|
-
- Write the tests first, then the code that makes them pass
|
|
18853
|
-
|
|
18943
|
+
- Write the tests first, then the code that makes them pass
|
|
18944
|
+
${behaviourTests}
|
|
18854
18945
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
18855
18946
|
finish your turn
|
|
18856
18947
|
`;
|
|
@@ -18874,9 +18965,9 @@ ${retry.fault}
|
|
|
18874
18965
|
|
|
18875
18966
|
${steerChoices(`land a clean tree to go on with the last attempt as it is:
|
|
18876
18967
|
\`seam.check\` runs its suite and ${judged ? "the seam judge rules on its shape" : "it lands as the implementation"}. Use it
|
|
18877
|
-
when the rule cannot fit this change (
|
|
18878
|
-
|
|
18879
|
-
|
|
18968
|
+
when the rule cannot fit this change (no entry point that existed before).
|
|
18969
|
+
Risk: its tests may not survive a later refactor — the choice is recorded
|
|
18970
|
+
in history.`)}` : isRed(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
18880
18971
|
attempt left the suite red: \`.gtd/FEEDBACK.md\` holds the last run's output; \`git show ${retry.red}\` shows
|
|
18881
18972
|
the last attempt.
|
|
18882
18973
|
|
|
@@ -18979,6 +19070,7 @@ ${criteriaBinding(true)}
|
|
|
18979
19070
|
\`fail\` chunk of this change's criteria or a problem found
|
|
18980
19071
|
beside them, at its cause, with a test that shows it; leave \`blocked\`
|
|
18981
19072
|
ones to the person
|
|
19073
|
+
${behaviourTests}
|
|
18982
19074
|
- A \`fail\` the code does not explain (the tester misread what it saw) has
|
|
18983
19075
|
nothing to fix: leave it and change nothing for it — the next round re-tests
|
|
18984
19076
|
- Leave \`.gtd/PROTOTYPE.md\` as it is: the next round re-tests from it
|
|
@@ -19058,6 +19150,10 @@ const builtInLenses = {
|
|
|
19058
19150
|
"spec-challenge": {
|
|
19059
19151
|
skills: [],
|
|
19060
19152
|
brief: specChallengeBrief
|
|
19153
|
+
},
|
|
19154
|
+
"criteria-coverage": {
|
|
19155
|
+
skills: [],
|
|
19156
|
+
brief: criteriaCoverageBrief
|
|
19061
19157
|
}
|
|
19062
19158
|
};
|
|
19063
19159
|
const coderLenses = /* @__PURE__ */ new Set([
|
|
@@ -19065,8 +19161,11 @@ const coderLenses = /* @__PURE__ */ new Set([
|
|
|
19065
19161
|
"ponytail-review",
|
|
19066
19162
|
"test-audit"
|
|
19067
19163
|
]);
|
|
19068
|
-
/**
|
|
19069
|
-
|
|
19164
|
+
/**
|
|
19165
|
+
* One quality review through `lens`; each lens is its own scope, so its own
|
|
19166
|
+
* conversation and skills. `requirements`, when given, are quoted in the prompt.
|
|
19167
|
+
*/
|
|
19168
|
+
const reviewQuality = (lens, base, model, requirements) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base, requirements), {
|
|
19070
19169
|
label: "Reviewing (one quality lens)",
|
|
19071
19170
|
file: QUALITY,
|
|
19072
19171
|
model: model || (coderLenses.has(lens) ? coder() : planner()),
|
|
@@ -38638,9 +38737,11 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
|
|
|
38638
38737
|
//#endregion
|
|
38639
38738
|
//#region src/workflows/shared/lenses.ts
|
|
38640
38739
|
const NO_FINDINGS = {
|
|
38641
|
-
notes: [],
|
|
38642
38740
|
fixed: [],
|
|
38643
|
-
|
|
38741
|
+
gateFixed: [],
|
|
38742
|
+
dropped: [],
|
|
38743
|
+
noise: [],
|
|
38744
|
+
unresolved: []
|
|
38644
38745
|
};
|
|
38645
38746
|
const UNTITLED = "Quality findings";
|
|
38646
38747
|
const below = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
@@ -38713,31 +38814,18 @@ const classify = async (findings) => {
|
|
|
38713
38814
|
design: of("design")
|
|
38714
38815
|
};
|
|
38715
38816
|
};
|
|
38716
|
-
|
|
38717
|
-
|
|
38718
|
-
|
|
38719
|
-
|
|
38720
|
-
|
|
38721
|
-
*/
|
|
38722
|
-
const
|
|
38723
|
-
const { escalations, guard, guardRests, rules } = options;
|
|
38724
|
-
const withLens = (f) => {
|
|
38725
|
-
const lens = options.lensOf?.(f);
|
|
38726
|
-
return lens === void 0 ? f : {
|
|
38727
|
-
...f,
|
|
38728
|
-
lens
|
|
38729
|
-
};
|
|
38730
|
-
};
|
|
38731
|
-
const findings = findingsOf(read(".gtd/QUALITY.md") ?? "").map(withLens);
|
|
38732
|
-
if (findings.length === 0) return NO_FINDINGS;
|
|
38733
|
-
const { mechanical, noise, design } = await classify(findings);
|
|
38734
|
-
const sorted = qualityFindingsFile(mechanical);
|
|
38735
|
-
if (read(".gtd/QUALITY.md") !== sorted) await run("quality.sorting", mechanical.length === 0 ? removeScript([QUALITY]) : writeScript(QUALITY, sorted), { label: "Setting findings aside for the review" });
|
|
38817
|
+
const NOTHING_FIXED = {
|
|
38818
|
+
fixed: [],
|
|
38819
|
+
open: [],
|
|
38820
|
+
unclear: []
|
|
38821
|
+
};
|
|
38822
|
+
/** Up to `FIX_CAP` fix turns over `.gtd/QUALITY.md`, which holds one chunk per finding in `findings`. */
|
|
38823
|
+
const fixTurns = async (findings, options) => {
|
|
38736
38824
|
const fixed = [];
|
|
38737
38825
|
const unclear = [];
|
|
38738
|
-
let open =
|
|
38826
|
+
let open = findings;
|
|
38739
38827
|
for (let turns = 0; turns < 3 && open.length > 0; turns++) {
|
|
38740
|
-
await guarded(() => fixQuality(rules), guard)();
|
|
38828
|
+
await guarded(() => fixQuality(options.rules), options.guard)();
|
|
38741
38829
|
const turn = afterFix(open, findingsOf(read(".gtd/QUALITY.md") ?? ""));
|
|
38742
38830
|
const commit = head$1();
|
|
38743
38831
|
fixed.push(...turn.fixed.map((f) => ({
|
|
@@ -38746,30 +38834,120 @@ const triageFindings = async (options) => {
|
|
|
38746
38834
|
})));
|
|
38747
38835
|
unclear.push(...turn.unclear);
|
|
38748
38836
|
open = turn.open.map((f) => ({
|
|
38749
|
-
...
|
|
38837
|
+
...findings.find((m) => m.title === f.title),
|
|
38750
38838
|
...f
|
|
38751
38839
|
}));
|
|
38752
38840
|
}
|
|
38753
|
-
|
|
38841
|
+
const { escalations, guard, guardRests, rules } = options;
|
|
38842
|
+
await healthy(() => fixCheck(rules), {
|
|
38754
38843
|
escalations,
|
|
38755
38844
|
guard,
|
|
38756
38845
|
guardRests,
|
|
38757
38846
|
rules
|
|
38758
38847
|
});
|
|
38759
38848
|
return {
|
|
38760
|
-
|
|
38849
|
+
fixed,
|
|
38850
|
+
open,
|
|
38851
|
+
unclear
|
|
38852
|
+
};
|
|
38853
|
+
};
|
|
38854
|
+
/** Matches each entry the human left to the finding it was written from, by title, first unused first; `left` is what no entry claimed. */
|
|
38855
|
+
const matcher = (findings) => {
|
|
38856
|
+
const left = [...findings];
|
|
38857
|
+
const of = (e) => {
|
|
38858
|
+
const at = left.findIndex((f) => f.title === e.title);
|
|
38859
|
+
return at < 0 ? void 0 : left.splice(at, 1)[0];
|
|
38860
|
+
};
|
|
38861
|
+
return {
|
|
38862
|
+
of,
|
|
38863
|
+
left
|
|
38864
|
+
};
|
|
38865
|
+
};
|
|
38866
|
+
const gateProblems = (text) => steeringFormatFor("findings").validate(text).map((f) => ` - ${formatFinding(QUALITY, f)}`);
|
|
38867
|
+
/**
|
|
38868
|
+
* The human gate over the open notes and the noise: a ticked finding is
|
|
38869
|
+
* fixed — never re-planned — and the rest dropped. No notes and no noise, no gate.
|
|
38870
|
+
*/
|
|
38871
|
+
const findingGate = async (notes, noise, options) => {
|
|
38872
|
+
if (notes.length + noise.length === 0) return NO_FINDINGS;
|
|
38873
|
+
const written = findingGateFile(notes, noise);
|
|
38874
|
+
await run("quality.gating", writeScript(QUALITY, written), { label: "Listing the findings for you" });
|
|
38875
|
+
await human("quality.gate", {
|
|
38876
|
+
message: findingGateMessage(),
|
|
38877
|
+
label: "Deciding on the quality findings",
|
|
38878
|
+
file: QUALITY,
|
|
38879
|
+
mode: "findings",
|
|
38880
|
+
acceptClean: true
|
|
38881
|
+
});
|
|
38882
|
+
const answered = read(".gtd/QUALITY.md") ?? written;
|
|
38883
|
+
const problems = gateProblems(answered);
|
|
38884
|
+
if (problems.length > 0) refuse(`gtd land: finding-gate: ${problems.length} problem(s) in ${QUALITY} — keep one \`- [ ] fix\` box per finding, ticked to fix it or unticked to drop it, and write guidance only under a ticked one:\n${problems.join("\n")}`);
|
|
38885
|
+
const noteOf = matcher(notes);
|
|
38886
|
+
const noiseOf = matcher(noise);
|
|
38887
|
+
const decided = findingEntries(answered).map((e) => ({
|
|
38888
|
+
e,
|
|
38889
|
+
finding: (e.noise ? noiseOf.of(e) : noteOf.of(e)) ?? {
|
|
38890
|
+
id: "",
|
|
38891
|
+
title: e.title,
|
|
38892
|
+
body: e.body
|
|
38893
|
+
}
|
|
38894
|
+
}));
|
|
38895
|
+
const ticked = decided.filter(({ e }) => e.fix).map(({ e, finding }) => ({
|
|
38896
|
+
...finding,
|
|
38897
|
+
body: withGuidance(finding.body, e.guidance)
|
|
38898
|
+
}));
|
|
38899
|
+
let fixing = NOTHING_FIXED;
|
|
38900
|
+
if (ticked.length > 0) {
|
|
38901
|
+
await run("quality.picking", writeScript(QUALITY, qualityFindingsFile(ticked)), { label: "Keeping the findings you ticked" });
|
|
38902
|
+
fixing = await fixTurns(ticked, options);
|
|
38903
|
+
}
|
|
38904
|
+
const unticked = (noisy) => decided.filter(({ e }) => !e.fix && e.noise === noisy).map(({ finding }) => finding);
|
|
38905
|
+
return {
|
|
38906
|
+
gateFixed: fixing.fixed,
|
|
38907
|
+
dropped: [...unticked(false), ...noteOf.left],
|
|
38908
|
+
noise: [...unticked(true), ...noiseOf.left],
|
|
38909
|
+
unresolved: [...fixing.open, ...fixing.unclear.map((f) => ({
|
|
38910
|
+
...f,
|
|
38911
|
+
unclear: true
|
|
38912
|
+
}))]
|
|
38913
|
+
};
|
|
38914
|
+
};
|
|
38915
|
+
/**
|
|
38916
|
+
* Judge each finding in `.gtd/QUALITY.md` and fix the mechanical ones for up
|
|
38917
|
+
* to `FIX_CAP` turns; the rest — design, unresolved and noise — go to the
|
|
38918
|
+
* finding gate. Leaves `.gtd/QUALITY.md` holding only what the last fixer
|
|
38919
|
+
* left: the caller must remove it, or the next lap appends to stale findings.
|
|
38920
|
+
*/
|
|
38921
|
+
const triageFindings = async (options) => {
|
|
38922
|
+
const withLens = (f) => {
|
|
38923
|
+
const lens = options.lensOf?.(f);
|
|
38924
|
+
return lens === void 0 ? f : {
|
|
38925
|
+
...f,
|
|
38926
|
+
lens
|
|
38927
|
+
};
|
|
38928
|
+
};
|
|
38929
|
+
const findings = findingsOf(read(".gtd/QUALITY.md") ?? "").map(withLens);
|
|
38930
|
+
if (findings.length === 0) return NO_FINDINGS;
|
|
38931
|
+
const { mechanical, noise, design } = await classify(findings);
|
|
38932
|
+
let auto = NOTHING_FIXED;
|
|
38933
|
+
if (mechanical.length > 0) {
|
|
38934
|
+
const sorted = qualityFindingsFile(mechanical);
|
|
38935
|
+
if (read(".gtd/QUALITY.md") !== sorted) await run("quality.sorting", writeScript(QUALITY, sorted), { label: "Setting findings aside for the finding gate" });
|
|
38936
|
+
auto = await fixTurns(mechanical, options);
|
|
38937
|
+
}
|
|
38938
|
+
return {
|
|
38939
|
+
...await findingGate([
|
|
38761
38940
|
...design,
|
|
38762
|
-
...open.map((f) => ({
|
|
38941
|
+
...auto.open.map((f) => ({
|
|
38763
38942
|
...f,
|
|
38764
38943
|
unresolved: true
|
|
38765
38944
|
})),
|
|
38766
|
-
...unclear.map((f) => ({
|
|
38945
|
+
...auto.unclear.map((f) => ({
|
|
38767
38946
|
...f,
|
|
38768
38947
|
unclear: true
|
|
38769
38948
|
}))
|
|
38770
|
-
],
|
|
38771
|
-
fixed
|
|
38772
|
-
noise
|
|
38949
|
+
], noise, options),
|
|
38950
|
+
fixed: auto.fixed
|
|
38773
38951
|
};
|
|
38774
38952
|
};
|
|
38775
38953
|
//#endregion
|
|
@@ -38781,12 +38959,16 @@ const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
|
38781
38959
|
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
38782
38960
|
* has any.
|
|
38783
38961
|
*/
|
|
38784
|
-
const qualityLap = (guard, lenses, base) => lensLap(guard, lenses, base, () => {});
|
|
38785
|
-
const
|
|
38786
|
-
|
|
38787
|
-
|
|
38788
|
-
|
|
38789
|
-
|
|
38962
|
+
const qualityLap = (guard, lenses, base) => lensLap(guard, lenses, base, void 0, () => {});
|
|
38963
|
+
const COVERAGE = "criteria-coverage";
|
|
38964
|
+
const lensLap = (guard, lenses, base, given, seen) => {
|
|
38965
|
+
const requirements = given ?? read(".gtd/REQUIREMENTS.md");
|
|
38966
|
+
return qualityLap$1((lenses ?? lensEntriesOf(vars.qualityReviews ?? "")).filter((entry) => requirements !== void 0 || lensOf(entry).lens !== COVERAGE), guarded(async (entry) => {
|
|
38967
|
+
const { lens, model } = lensOf(entry);
|
|
38968
|
+
await reviewQuality(lens, base, model, lens === COVERAGE ? requirements : void 0);
|
|
38969
|
+
seen(lens, findingsOf(read(".gtd/QUALITY.md") ?? ""));
|
|
38970
|
+
}, guard), QUALITY);
|
|
38971
|
+
};
|
|
38790
38972
|
const closeQuality = async () => {
|
|
38791
38973
|
if (read(".gtd/QUALITY.md") === void 0) return;
|
|
38792
38974
|
await run("quality.closing", removeScript([QUALITY]), { label: "Clearing quality findings" });
|
|
@@ -38823,7 +39005,7 @@ const tailForm = (first, second, rest) => {
|
|
|
38823
39005
|
if (typeof first === "function" && isOptionalObject(second) && rest.every((v) => v == null)) return [first, second ?? {}];
|
|
38824
39006
|
};
|
|
38825
39007
|
const runTail = (finish, options) => scope("build", async () => {
|
|
38826
|
-
const { guard, lenses, lensBase, rules } = options;
|
|
39008
|
+
const { guard, lenses, lensBase, rules, requirements } = options;
|
|
38827
39009
|
const guardRests = options.guardRests === void 0 ? true : options.guardRests;
|
|
38828
39010
|
const escalations = { rounds: 0 };
|
|
38829
39011
|
const fix = () => fixCheck(rules);
|
|
@@ -38843,7 +39025,7 @@ const runTail = (finish, options) => scope("build", async () => {
|
|
|
38843
39025
|
rules
|
|
38844
39026
|
});
|
|
38845
39027
|
const byTitle = [];
|
|
38846
|
-
const lap = await lensLap(guard, lenses, lensBase, (lens, findings) => {
|
|
39028
|
+
const lap = await lensLap(guard, lenses, lensBase, requirements, (lens, findings) => {
|
|
38847
39029
|
const before = byTitle.map((f) => f.title);
|
|
38848
39030
|
for (const { title } of findings) {
|
|
38849
39031
|
const at = before.indexOf(title);
|
|
@@ -38873,28 +39055,36 @@ const runTail = (finish, options) => scope("build", async () => {
|
|
|
38873
39055
|
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
38874
39056
|
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
38875
39057
|
*/
|
|
38876
|
-
const handOffReview = (base, escalations, guard, report = NO_FINDINGS) =>
|
|
38877
|
-
|
|
38878
|
-
|
|
38879
|
-
|
|
38880
|
-
|
|
38881
|
-
|
|
38882
|
-
|
|
38883
|
-
|
|
38884
|
-
|
|
38885
|
-
|
|
38886
|
-
|
|
38887
|
-
|
|
38888
|
-
guard
|
|
38889
|
-
|
|
38890
|
-
|
|
38891
|
-
|
|
38892
|
-
|
|
38893
|
-
|
|
38894
|
-
|
|
38895
|
-
|
|
38896
|
-
|
|
38897
|
-
|
|
39058
|
+
const handOffReview = (base, escalations, guard, report = NO_FINDINGS) => {
|
|
39059
|
+
let riskFixed = false;
|
|
39060
|
+
return reviewToHandOff(base, {
|
|
39061
|
+
file: REVIEW,
|
|
39062
|
+
reviewing: guarded(async (b, carry) => {
|
|
39063
|
+
const afterRiskFix = riskFixed;
|
|
39064
|
+
riskFixed = false;
|
|
39065
|
+
await reviewing(b, carry);
|
|
39066
|
+
const review = read(REVIEW);
|
|
39067
|
+
if (review === void 0 || !afterRiskFix && reviewRisks(review).length > 0) return;
|
|
39068
|
+
const full = withSummary(review, { report });
|
|
39069
|
+
if (full !== review) await run("review.carrying", writeScript(REVIEW, full), { label: "Adding the quality lap's summary" });
|
|
39070
|
+
}, guard),
|
|
39071
|
+
fixRisks: guarded((risks) => {
|
|
39072
|
+
riskFixed = true;
|
|
39073
|
+
return fixRisks(risks);
|
|
39074
|
+
}, guard),
|
|
39075
|
+
keepGreen: () => healthy(fixCheck, {
|
|
39076
|
+
escalations,
|
|
39077
|
+
guard
|
|
39078
|
+
}),
|
|
39079
|
+
awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
|
|
39080
|
+
reviewMissing,
|
|
39081
|
+
close: closeReview,
|
|
39082
|
+
handOff: (reviewed) => run("review.handing-off", showScript(reviewed, REVIEW, REVIEW_ORIG), { label: "Handing your notes to fix" }),
|
|
39083
|
+
threadsPending: "gtd land: open-threads: a thread in .gtd/REVIEW.md or an `H:` code comment waits on the agent — nothing answers it here; delete it, or turn it into a note for fix.",
|
|
39084
|
+
unparsed: "gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
|
|
39085
|
+
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
39086
|
+
});
|
|
39087
|
+
};
|
|
38898
39088
|
/** The tail the fix workflow runs: ends at the hand-off review since `base`, which gets what the lap left. */
|
|
38899
39089
|
const handOffTail = (fixFirst, base, guard) => tail((escalations, report) => handOffReview(base, escalations, guard, report), {
|
|
38900
39090
|
fixFirst,
|
|
@@ -39202,9 +39392,8 @@ const design = (base, guard) => scope("design", async () => {
|
|
|
39202
39392
|
//#endregion
|
|
39203
39393
|
//#region src/workflows/shared/learn.ts
|
|
39204
39394
|
const SUMMARY_MAX = 200;
|
|
39205
|
-
const BANNER = /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/;
|
|
39206
39395
|
const summary$1 = (body) => {
|
|
39207
|
-
const text = oneLine(body)
|
|
39396
|
+
const text = oneLine(body);
|
|
39208
39397
|
return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
|
|
39209
39398
|
};
|
|
39210
39399
|
/** The standard a won't-fix or an own answer would make; a fix states none. */
|
|
@@ -39473,139 +39662,41 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
39473
39662
|
return [n.id, "edit"];
|
|
39474
39663
|
}));
|
|
39475
39664
|
};
|
|
39476
|
-
/** Review order: security, one-way doors, the judge unsure, then should, then could. */
|
|
39477
|
-
const chunkKey = (s) => s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2;
|
|
39478
|
-
/** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
|
|
39479
|
-
const chunkEvidence = (text, files) => [text, ...files.map(({ path, line, rangeEnd }) => {
|
|
39480
|
-
const where = `${path}${line === void 0 ? "" : `#${line}${rangeEnd === void 0 ? "" : `-${rangeEnd}`}`}`;
|
|
39481
|
-
const lines = read(path.replace(/^\.\//, ""))?.split("\n");
|
|
39482
|
-
if (lines === void 0) return `${where}: no longer exists`;
|
|
39483
|
-
return `${where}:\n${(line === void 0 ? lines : lines.slice(line - 1, rangeEnd ?? line)).join("\n")}`;
|
|
39484
|
-
})].join("\n\n");
|
|
39485
|
-
const bytes = (text) => new TextEncoder().encode(text).length;
|
|
39486
|
-
/** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
|
|
39487
|
-
const batched = (items, budget) => {
|
|
39488
|
-
const batches = [];
|
|
39489
|
-
let batch = [];
|
|
39490
|
-
let widest = 0;
|
|
39491
|
-
for (const item of items) {
|
|
39492
|
-
const size = bytes(item.evidence);
|
|
39493
|
-
if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
|
|
39494
|
-
batches.push(batch);
|
|
39495
|
-
batch = [];
|
|
39496
|
-
widest = 0;
|
|
39497
|
-
}
|
|
39498
|
-
batch.push(item);
|
|
39499
|
-
widest = Math.max(widest, size);
|
|
39500
|
-
}
|
|
39501
|
-
return batch.length === 0 ? batches : [...batches, batch];
|
|
39502
|
-
};
|
|
39503
|
-
/**
|
|
39504
|
-
* Rates every chunk the reviewer wrote and sorts the \`findings\` among them
|
|
39505
|
-
* first in \`full\`, each led by its tier and reason; a walkthrough chunk
|
|
39506
|
-
* keeps its place and carries no tier. It is still rated, so a review with no
|
|
39507
|
-
* findings still reaches \`review.ranking\` and older histories replay.
|
|
39508
|
-
*/
|
|
39509
|
-
const ranked = async (review, full, findings) => {
|
|
39510
|
-
const texts = chunkTexts(full);
|
|
39511
|
-
const chunks = reviewChunks(review).map((c, i) => ({
|
|
39512
|
-
id: `C${i + 1}`,
|
|
39513
|
-
title: c.title,
|
|
39514
|
-
evidence: chunkEvidence(texts.get(c.title) ?? "", c.files)
|
|
39515
|
-
}));
|
|
39516
|
-
if (chunks.length === 0) return full;
|
|
39517
|
-
const minP = numeric(vars.reviewSeverityMinP, Infinity);
|
|
39518
|
-
const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768));
|
|
39519
|
-
const severities = /* @__PURE__ */ new Map();
|
|
39520
|
-
for (const [i, batch] of batches.entries()) {
|
|
39521
|
-
const judgment = await judge("review.ranking", {
|
|
39522
|
-
questions: batch.flatMap((c) => severityQuestions(c.id, `the change review chunk "${c.id}" ("${c.title}") walks through`, `the change in review chunk "${c.id}"`)),
|
|
39523
|
-
evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
|
|
39524
|
-
message: reviewRankingMessage(),
|
|
39525
|
-
label: batches.length === 1 ? "Ranking the review" : `Ranking the review (${i + 1}/${batches.length})`
|
|
39526
|
-
});
|
|
39527
|
-
for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP));
|
|
39528
|
-
}
|
|
39529
|
-
return rankedReview(full, chunks.map((c, i) => ({
|
|
39530
|
-
c,
|
|
39531
|
-
i,
|
|
39532
|
-
s: severities.get(c.id)
|
|
39533
|
-
})).filter(({ c }) => findings.has(c.title)).sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i).map(({ c, s }) => ({
|
|
39534
|
-
title: c.title,
|
|
39535
|
-
label: s.reason
|
|
39536
|
-
})));
|
|
39537
|
-
};
|
|
39538
39665
|
const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
39539
|
-
|
|
39540
|
-
|
|
39541
|
-
nitFix,
|
|
39542
|
-
answered,
|
|
39543
|
-
standards: {
|
|
39544
|
-
added: standardsAddedSince(base),
|
|
39545
|
-
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
39546
|
-
}
|
|
39547
|
-
});
|
|
39548
|
-
/**
|
|
39549
|
-
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
39550
|
-
* turn. A re-review
|
|
39551
|
-
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
39552
|
-
* a fresh one leaves out the notes the human already answered.
|
|
39553
|
-
*/
|
|
39554
|
-
const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
|
|
39555
|
-
...composedOf(base, report, nitFix, answered),
|
|
39556
|
-
weakened: removedAssertions(changesSince(base))
|
|
39557
|
-
})) : showScript(carry, REVIEW, REVIEW), {
|
|
39558
|
-
label: "Composing the review",
|
|
39559
|
-
file: REVIEW,
|
|
39560
|
-
base
|
|
39561
|
-
});
|
|
39562
|
-
/**
|
|
39563
|
-
* Refuses a review with a chunk pointing at no code, or a lap note without a
|
|
39564
|
-
* chunk of its own. A range with no code change has nothing to point at, so
|
|
39565
|
-
* there only a note's chunk needs a pointer.
|
|
39566
|
-
*/
|
|
39567
|
-
const requirePointers = (review, notes, codeChanged) => {
|
|
39666
|
+
/** Refuses a review with a chunk pointing at no code; a range with no code change has nothing to point at. */
|
|
39667
|
+
const requirePointers = (review, codeChanged) => {
|
|
39568
39668
|
const chunks = reviewChunks(review);
|
|
39569
|
-
const titles = new Set(chunks.map((c) => c.title));
|
|
39570
|
-
const noted = new Set(notes.map((n) => n.title));
|
|
39571
39669
|
const problems = [
|
|
39572
|
-
...chunks.filter((c) => c.pointers === 0 &&
|
|
39670
|
+
...chunks.filter((c) => c.pointers === 0 && codeChanged).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
|
|
39573
39671
|
...chunks.filter((c) => isConflictTitle(c.title)).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — gtd writes conflicts; leave it out`),
|
|
39574
|
-
...notes.filter((n) => !titles.has(n.title)).map((n) => ` - ${n.title} — no chunk of its own`),
|
|
39575
39672
|
...chunks.filter((c, i) => chunks.findIndex((d) => d.title === c.title) !== i).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — a second chunk with this title`),
|
|
39576
|
-
...reviewSections(review).map((s) => ` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} —
|
|
39577
|
-
...reviewRisks(review).filter((r) => [...noted].some((title) => r.anchor.startsWith(`${title} ./`))).map((r) => ` - ${r.anchor} — no \`Risk:\` in a quality finding's chunk`)
|
|
39673
|
+
...reviewSections(review).map((s) => ` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — write \`## \` chunks only`)
|
|
39578
39674
|
];
|
|
39579
39675
|
if (problems.length === 0) return;
|
|
39580
|
-
refuse(`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code
|
|
39676
|
+
refuse(`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code:\n${problems.join("\n")}`);
|
|
39581
39677
|
};
|
|
39582
39678
|
/**
|
|
39583
39679
|
* A reviewer walks the human through everything since `base`, every chunk
|
|
39584
|
-
* pointing at its code
|
|
39585
|
-
* then adds each note's text and answer boxes, and the summary chunks.
|
|
39680
|
+
* pointing at its code; gtd then appends the summary chunks.
|
|
39586
39681
|
*/
|
|
39587
|
-
const walkthrough = async (base,
|
|
39588
|
-
|
|
39589
|
-
await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
|
|
39682
|
+
const walkthrough = async (base, summary, lap, carry, nitFix, afterRiskFix) => {
|
|
39683
|
+
await reviewing(base, carry, lap, nitFixBlock(nitFix));
|
|
39590
39684
|
const written = read(REVIEW);
|
|
39591
39685
|
if (written === void 0) return;
|
|
39592
|
-
|
|
39593
|
-
requirePointers(written, notes, changesSince(base).some((c) => isCode$1(c.path)));
|
|
39686
|
+
requirePointers(written, changesSince(base).some((c) => isCode$1(c.path)));
|
|
39594
39687
|
if (!afterRiskFix && reviewRisks(written).length > 0) return;
|
|
39595
|
-
const full =
|
|
39688
|
+
const full = withSummary(written, summary);
|
|
39596
39689
|
if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
|
|
39597
|
-
label: "
|
|
39690
|
+
label: "Adding the quality lap's summary",
|
|
39598
39691
|
file: REVIEW,
|
|
39599
39692
|
base
|
|
39600
39693
|
});
|
|
39601
39694
|
};
|
|
39602
39695
|
/**
|
|
39603
|
-
*
|
|
39604
|
-
*
|
|
39605
|
-
*
|
|
39606
|
-
*
|
|
39607
|
-
* caller to build, questions are answered in place, nits fixed in one batch,
|
|
39608
|
-
* praise dropped.
|
|
39696
|
+
* A reviewer's walkthrough of everything since `base` reaches a human, who
|
|
39697
|
+
* may comment: each note is judged — edits end the review as `feedback` for
|
|
39698
|
+
* the caller to build, questions are answered in place, nits fixed in one
|
|
39699
|
+
* batch, praise dropped.
|
|
39609
39700
|
*/
|
|
39610
39701
|
const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
39611
39702
|
const { guard, guardRests, fixAccess, rules } = options;
|
|
@@ -39616,7 +39707,6 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
39616
39707
|
};
|
|
39617
39708
|
const severities = /* @__PURE__ */ new Map();
|
|
39618
39709
|
let nitFix;
|
|
39619
|
-
const answered = /* @__PURE__ */ new Set();
|
|
39620
39710
|
let riskFixed = false;
|
|
39621
39711
|
return review$1(base, {
|
|
39622
39712
|
file: REVIEW,
|
|
@@ -39626,8 +39716,14 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
39626
39716
|
const fixedAt = nitFix;
|
|
39627
39717
|
const afterRiskFix = riskFixed;
|
|
39628
39718
|
riskFixed = false;
|
|
39629
|
-
const
|
|
39630
|
-
|
|
39719
|
+
const summary = {
|
|
39720
|
+
report: options.report ?? NO_FINDINGS,
|
|
39721
|
+
standards: {
|
|
39722
|
+
added: standardsAddedSince(b),
|
|
39723
|
+
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
39724
|
+
}
|
|
39725
|
+
};
|
|
39726
|
+
return guarded(() => walkthrough(b, summary, options.lap ?? "first", carry, fixedAt, afterRiskFix), guard)();
|
|
39631
39727
|
},
|
|
39632
39728
|
fixRisks: guarded((notes) => {
|
|
39633
39729
|
riskFixed = true;
|
|
@@ -39646,7 +39742,6 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
39646
39742
|
reviewMissing,
|
|
39647
39743
|
collecting: feedback.collecting,
|
|
39648
39744
|
triage: triage$1(severities),
|
|
39649
|
-
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
39650
39745
|
settles: settlesStandard,
|
|
39651
39746
|
learn: learnStandards,
|
|
39652
39747
|
pending: standardsPending,
|
|
@@ -39717,16 +39812,13 @@ const reUnwind = async (feedback) => {
|
|
|
39717
39812
|
};
|
|
39718
39813
|
//#endregion
|
|
39719
39814
|
//#region src/workflows/feature/green.ts
|
|
39720
|
-
const green = (
|
|
39815
|
+
const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
39721
39816
|
label: "Fixing the check",
|
|
39722
39817
|
file: FEEDBACK,
|
|
39723
39818
|
model: coder(),
|
|
39724
39819
|
system: builderSystem()
|
|
39725
39820
|
}), {
|
|
39726
|
-
guard: () =>
|
|
39727
|
-
requireCriteriaFrozen(REQUIREMENTS);
|
|
39728
|
-
guard?.();
|
|
39729
|
-
},
|
|
39821
|
+
guard: () => requireCriteriaFrozen(REQUIREMENTS),
|
|
39730
39822
|
guardRests: false
|
|
39731
39823
|
});
|
|
39732
39824
|
//#endregion
|
|
@@ -39817,7 +39909,7 @@ const testing = async (ids, retest) => {
|
|
|
39817
39909
|
});
|
|
39818
39910
|
return requireReport(ids, retest);
|
|
39819
39911
|
};
|
|
39820
|
-
const fixing = async (
|
|
39912
|
+
const fixing = async () => {
|
|
39821
39913
|
await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(), {
|
|
39822
39914
|
label: "Fixing what the hands-on test found",
|
|
39823
39915
|
file: PROTOTYPE,
|
|
@@ -39827,7 +39919,6 @@ const fixing = async (guard) => {
|
|
|
39827
39919
|
}));
|
|
39828
39920
|
if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
|
|
39829
39921
|
frozen();
|
|
39830
|
-
guard?.();
|
|
39831
39922
|
};
|
|
39832
39923
|
/** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
|
|
39833
39924
|
const askPerson = async (name, message, label) => {
|
|
@@ -39845,10 +39936,9 @@ const askPerson = async (name, message, label) => {
|
|
|
39845
39936
|
* Use the change by hand: a testing turn reports on every criterion a person
|
|
39846
39937
|
* can observe, a fix turn takes each failure, and the change is tested again —
|
|
39847
39938
|
* until the report is clean, or `prototypeRounds` fixes are spent and a person
|
|
39848
|
-
* decides. A change with no such criterion is not tested by hand.
|
|
39849
|
-
* holds every fix turn, as it holds the implementation's.
|
|
39939
|
+
* decides. A change with no such criterion is not tested by hand.
|
|
39850
39940
|
*/
|
|
39851
|
-
const prototypeTest = async (
|
|
39941
|
+
const prototypeTest = async () => {
|
|
39852
39942
|
const cap = prototypeRounds();
|
|
39853
39943
|
if (cap === 0) return;
|
|
39854
39944
|
await scope("prototype", async () => {
|
|
@@ -39864,12 +39954,12 @@ const prototypeTest = async (guard) => {
|
|
|
39864
39954
|
} else if (open.length === 0) return;
|
|
39865
39955
|
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
39866
39956
|
fixes++;
|
|
39867
|
-
await fixing(
|
|
39957
|
+
await fixing();
|
|
39868
39958
|
} else {
|
|
39869
39959
|
if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
|
|
39870
39960
|
fixes = 0;
|
|
39871
39961
|
}
|
|
39872
|
-
await green(
|
|
39962
|
+
await green();
|
|
39873
39963
|
}
|
|
39874
39964
|
});
|
|
39875
39965
|
};
|
|
@@ -40115,7 +40205,6 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
40115
40205
|
fixAccess: strayAccess(p.strays),
|
|
40116
40206
|
lensBase: entry,
|
|
40117
40207
|
rules: refactorRules(p.strays, p.pattern),
|
|
40118
|
-
walkthrough: true,
|
|
40119
40208
|
lap: "refactor"
|
|
40120
40209
|
});
|
|
40121
40210
|
if (outcome.verdict === "signoff") return true;
|
|
@@ -40169,69 +40258,25 @@ const importsOf = (test) => [...(test.after ?? "").replace(COMMENT, "").matchAll
|
|
|
40169
40258
|
}));
|
|
40170
40259
|
});
|
|
40171
40260
|
const reaches = (imports, added) => namesOf(added).some((name) => imports.some(({ path, tail }) => name === path || tail && name.endsWith(`/${path}`)));
|
|
40172
|
-
const named = (id, text) => new RegExp(`(?<![\\w-])${id}(?!\\d)`).test(text);
|
|
40173
|
-
/** The lines `change` added: an ID an earlier change left in the file does not count. */
|
|
40174
|
-
const addedLines = (change) => {
|
|
40175
|
-
const before = new Set((change.before ?? "").split("\n"));
|
|
40176
|
-
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
40177
|
-
};
|
|
40178
40261
|
const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
40179
|
-
const idsOf = (file) => criterionIds(read(file) ?? "");
|
|
40180
|
-
const unnamed = (ids, tests) => {
|
|
40181
|
-
const lines = tests.map(addedLines);
|
|
40182
|
-
return ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
40183
|
-
};
|
|
40184
40262
|
/**
|
|
40185
|
-
* Why `tip`'s tests fall short, or `undefined`: a
|
|
40186
|
-
*
|
|
40187
|
-
*
|
|
40263
|
+
* Why `tip`'s tests fall short, or `undefined`: a test importing a file the
|
|
40264
|
+
* pass added — naive structure a reset and refactor replaces, which the tests
|
|
40265
|
+
* must survive.
|
|
40188
40266
|
*/
|
|
40189
|
-
const acceptanceFault = (
|
|
40190
|
-
const tests = acceptanceTests(tip.changes);
|
|
40191
|
-
const untested = unnamed(idsOf(file), tests);
|
|
40192
|
-
if (untested.length > 0) return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`;
|
|
40267
|
+
const acceptanceFault = (tip) => {
|
|
40193
40268
|
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
40194
|
-
const reaching =
|
|
40269
|
+
const reaching = acceptanceTests(tip.changes).filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
40195
40270
|
const imports = importsOf(test);
|
|
40196
40271
|
return added.filter((path) => reaches(imports, path)).map((path) => ` - ${test.path} → ${path}`);
|
|
40197
40272
|
});
|
|
40198
40273
|
if (reaching.length > 0) return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`;
|
|
40199
40274
|
};
|
|
40200
|
-
/** What a rebuild carries over from the naive pass `tip`:
|
|
40201
|
-
const naiveAcceptance = (tip) => {
|
|
40202
|
-
|
|
40203
|
-
|
|
40204
|
-
|
|
40205
|
-
return {
|
|
40206
|
-
path: c.path,
|
|
40207
|
-
ids: ids.filter((id) => named(id, lines))
|
|
40208
|
-
};
|
|
40209
|
-
});
|
|
40210
|
-
return {
|
|
40211
|
-
hash: tip.hash,
|
|
40212
|
-
tests: perTest.filter((c) => c.ids.length > 0).map((c) => c.path),
|
|
40213
|
-
ids: ids.filter((id) => perTest.some((c) => c.ids.includes(id)))
|
|
40214
|
-
};
|
|
40215
|
-
};
|
|
40216
|
-
/**
|
|
40217
|
-
* Refuse a rebuild whose test changes since `since` leave out an ID the naive
|
|
40218
|
-
* pass's tests named. A skipped test still counts: the gate reads text, never runs it.
|
|
40219
|
-
*/
|
|
40220
|
-
const requireAcceptance = (acceptance, since) => {
|
|
40221
|
-
const missing = unnamed(acceptance.ids, acceptanceTests(changesSince(since)));
|
|
40222
|
-
if (missing.length === 0) return;
|
|
40223
|
-
refuse(`gtd land: rebuild-acceptance: the rebuild's tests leave out ${missing.join(", ")} — carry over the naive pass's acceptance tests (\`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`), each with its ID in the test or scenario name`);
|
|
40224
|
-
};
|
|
40225
|
-
/**
|
|
40226
|
-
* Refuse a fix turn that, on its own, drops an ID the naive pass's tests named
|
|
40227
|
-
* from a test file. Turn-local, so a person's edit at an escalation rest never
|
|
40228
|
-
* wedges later fix turns.
|
|
40229
|
-
*/
|
|
40230
|
-
const requireAcceptanceKept = (acceptance) => {
|
|
40231
|
-
const dropped = acceptance.ids.filter((id) => changes().filter((c) => isCode$1(c.path) && isTestFile(c.path)).some((c) => named(id, c.before ?? "") && !named(id, c.after ?? "")));
|
|
40232
|
-
if (dropped.length === 0) return;
|
|
40233
|
-
refuse(`gtd land: rebuild-acceptance: this turn drops ${dropped.join(", ")} from the tests — the naive pass's acceptance tests stay; fix the code, not them`);
|
|
40234
|
-
};
|
|
40275
|
+
/** What a rebuild carries over from the naive pass `tip`: every test file it added or changed. */
|
|
40276
|
+
const naiveAcceptance = (tip) => ({
|
|
40277
|
+
hash: tip.hash,
|
|
40278
|
+
tests: acceptanceTests(tip.changes).map((c) => c.path)
|
|
40279
|
+
});
|
|
40235
40280
|
const restore = (path) => `if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`;
|
|
40236
40281
|
/**
|
|
40237
40282
|
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
@@ -40268,11 +40313,6 @@ const applied = async (tip) => {
|
|
|
40268
40313
|
await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
|
|
40269
40314
|
return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
|
|
40270
40315
|
};
|
|
40271
|
-
/** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
|
|
40272
|
-
const settled = (carried) => {
|
|
40273
|
-
const listed = idsOf(REQUIREMENTS);
|
|
40274
|
-
return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id));
|
|
40275
|
-
};
|
|
40276
40316
|
const build = async (options) => {
|
|
40277
40317
|
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
|
|
40278
40318
|
label: "Implementing",
|
|
@@ -40291,18 +40331,10 @@ async function implement(first, tier, amend) {
|
|
|
40291
40331
|
}
|
|
40292
40332
|
/** `implement`, resolving with the criterion IDs the requirements listed when it closed. */
|
|
40293
40333
|
const implementing = (options) => scope("implement", async () => {
|
|
40294
|
-
|
|
40295
|
-
const acceptance = options.rebuild?.acceptance;
|
|
40296
|
-
const built = options.naive === void 0 || !await applied(options.naive);
|
|
40297
|
-
if (built) await build(options);
|
|
40334
|
+
if (options.naive === void 0 || !await applied(options.naive)) await build(options);
|
|
40298
40335
|
else requireCriteriaFrozen(REQUIREMENTS);
|
|
40299
|
-
|
|
40300
|
-
|
|
40301
|
-
ids: settled(acceptance.ids)
|
|
40302
|
-
}, since);
|
|
40303
|
-
const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
|
|
40304
|
-
await green(keepsTests);
|
|
40305
|
-
await prototypeTest(keepsTests);
|
|
40336
|
+
await green();
|
|
40337
|
+
await prototypeTest();
|
|
40306
40338
|
const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
40307
40339
|
await run("closing", removeScript([
|
|
40308
40340
|
REQUIREMENTS,
|
|
@@ -40342,7 +40374,7 @@ const budget = () => {
|
|
|
40342
40374
|
};
|
|
40343
40375
|
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
40344
40376
|
const vetted = async (tip, vetting) => {
|
|
40345
|
-
const fault = vetting.waived ? void 0 : acceptanceFault(
|
|
40377
|
+
const fault = vetting.waived ? void 0 : acceptanceFault(tip);
|
|
40346
40378
|
if (fault !== void 0) {
|
|
40347
40379
|
const verdict = await vetting.again({
|
|
40348
40380
|
fault,
|
|
@@ -40462,6 +40494,7 @@ const buildFeature = async (firstBase) => {
|
|
|
40462
40494
|
const handedOut = new Set(listedIds());
|
|
40463
40495
|
const seam = await seamBuild(firstBase);
|
|
40464
40496
|
const used = gather(handedOut);
|
|
40497
|
+
const requirements = read(REQUIREMENTS);
|
|
40465
40498
|
const closed = await implementing({
|
|
40466
40499
|
reference: seam.reference,
|
|
40467
40500
|
tier,
|
|
@@ -40471,7 +40504,11 @@ const buildFeature = async (firstBase) => {
|
|
|
40471
40504
|
});
|
|
40472
40505
|
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
40473
40506
|
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
|
|
40474
|
-
const outcome = await
|
|
40507
|
+
const outcome = await buildTail(base, {
|
|
40508
|
+
green: true,
|
|
40509
|
+
lensBase: base,
|
|
40510
|
+
requirements
|
|
40511
|
+
});
|
|
40475
40512
|
await featureFeedback(outcome, base, {
|
|
40476
40513
|
closed,
|
|
40477
40514
|
used
|
|
@@ -40482,15 +40519,6 @@ const gather = (handedOut) => {
|
|
|
40482
40519
|
for (const id of listedIds()) handedOut.add(id);
|
|
40483
40520
|
return [...handedOut];
|
|
40484
40521
|
};
|
|
40485
|
-
/** The quality lap, then a reviewer's walkthrough since `base` for the human. */
|
|
40486
|
-
const reviewedTail = (base, lensBase, lap = "first") => tail((escalations, report) => humanReview(base, escalations, {
|
|
40487
|
-
report,
|
|
40488
|
-
walkthrough: true,
|
|
40489
|
-
lap
|
|
40490
|
-
}), {
|
|
40491
|
-
green: true,
|
|
40492
|
-
lensBase
|
|
40493
|
-
});
|
|
40494
40522
|
const featureFeedback = async (first, lensBase, ids) => {
|
|
40495
40523
|
let outcome = first;
|
|
40496
40524
|
let closed = ids.closed;
|
|
@@ -40505,11 +40533,18 @@ const featureFeedback = async (first, lensBase, ids) => {
|
|
|
40505
40533
|
requireStableIds(REQUIREMENTS, lap);
|
|
40506
40534
|
gather(handedOut);
|
|
40507
40535
|
});
|
|
40536
|
+
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
40537
|
+
const requirements = read(REQUIREMENTS);
|
|
40508
40538
|
closed = await implementing({
|
|
40509
|
-
tier
|
|
40539
|
+
tier,
|
|
40510
40540
|
amend: true
|
|
40511
40541
|
});
|
|
40512
|
-
outcome = await
|
|
40542
|
+
outcome = await buildTail(outcome.base, {
|
|
40543
|
+
green: true,
|
|
40544
|
+
lensBase,
|
|
40545
|
+
requirements,
|
|
40546
|
+
lap: "feedback"
|
|
40547
|
+
});
|
|
40513
40548
|
}
|
|
40514
40549
|
};
|
|
40515
40550
|
/** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
|
|
@@ -40523,11 +40558,16 @@ const afterTail = async (first) => {
|
|
|
40523
40558
|
while (outcome.verdict === "feedback") {
|
|
40524
40559
|
await reUnwind(outcome);
|
|
40525
40560
|
await design(outcome.base);
|
|
40561
|
+
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
40562
|
+
const requirements = read(REQUIREMENTS);
|
|
40526
40563
|
await implement({
|
|
40527
|
-
tier
|
|
40564
|
+
tier,
|
|
40528
40565
|
amend: true
|
|
40529
40566
|
});
|
|
40530
|
-
outcome = await buildTail(outcome.base, {
|
|
40567
|
+
outcome = await buildTail(outcome.base, {
|
|
40568
|
+
green: true,
|
|
40569
|
+
requirements
|
|
40570
|
+
});
|
|
40531
40571
|
}
|
|
40532
40572
|
};
|
|
40533
40573
|
/** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
|
|
@@ -40755,4 +40795,4 @@ var workflows_default = feature;
|
|
|
40755
40795
|
*/
|
|
40756
40796
|
const doors = {};
|
|
40757
40797
|
//#endregion
|
|
40758
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, PROTOTYPE, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, handsOnIds, hasImpact, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays,
|
|
40798
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, PROTOTYPE, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, handsOnIds, hasImpact, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireCriteria, requireCriteriaFrozen, requireImpact, requireImpactOnly, requireNoNewFlags, requireNoTestFiles, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };
|