@pmelab/gtd 55.0.0 → 57.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +72 -80
- package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-X2RkNHin.mjs} +110 -318
- package/dist/chunks/{review-BJGKwlYv.mjs → review-v7sh5GF4.mjs} +30 -125
- package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
- package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-S04KEewi.mjs} +144 -56
- package/dist/gtd.bundle.mjs +259 -22
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +6 -6
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +551 -66
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +63 -175
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +603 -727
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -3
- package/src/cli/Cli.ts +5 -5
- package/src/edge/Edge.test.ts +1 -1
- package/src/steering/FORMATS.md +5 -1
- package/src/steering/SteeringFormats.test.ts +2 -2
- package/src/steering/contract.test.ts +1 -0
- package/src/steering/findings.test.ts +47 -0
- package/src/steering/findings.ts +350 -0
- package/src/steering/fixtures/findings/gate.md +34 -0
- package/src/steering/fixtures/findings/gate.read.json +87 -0
- package/src/steering/index.test.ts +2 -2
- package/src/steering/index.ts +4 -3
- package/src/steering/review.test.ts +1 -75
- package/src/steering/review.ts +1 -74
- package/src/workflow/compile.test.ts +1 -0
- package/src/workflows/feature/criteria-coverage.feature +1 -7
- package/src/workflows/feature/criteria.test.ts +1 -41
- package/src/workflows/feature/criteria.ts +2 -14
- package/src/workflows/feature/fast-path.feature +0 -1
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +16 -25
- package/src/workflows/feature/flag-mode.feature +1 -1
- package/src/workflows/feature/lens-models.feature +0 -1
- package/src/workflows/feature/lens-review.feature +323 -172
- package/src/workflows/feature/naive-test.feature +258 -0
- package/src/workflows/feature/one-step-land.feature +0 -1
- package/src/workflows/feature/refactor-absorbs.feature +1 -8
- package/src/workflows/feature/refactor-first.feature +2 -11
- package/src/workflows/feature/refactor-rebuild.feature +4 -25
- package/src/workflows/feature/refactor-review.feature +33 -118
- package/src/workflows/feature/refactor.test.ts +0 -2
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +7 -24
- package/src/workflows/feature/reviewer-walkthrough.feature +16 -104
- package/src/workflows/feature/seam-rejection.feature +0 -1
- package/src/workflows/feature/seam.feature +0 -1
- package/src/workflows/feature/seam.test.ts +1 -2
- package/src/workflows/feature/seam.ts +56 -10
- package/src/workflows/feature/standards.feature +75 -35
- package/src/workflows/fix/fix-lens-origin.feature +20 -7
- package/src/workflows/fix/fix-quality.feature +102 -23
- package/src/workflows/review/review-door.feature +62 -59
- package/src/workflows/shared/learn.test.ts +0 -14
- package/src/workflows/shared/learn.ts +1 -6
- package/src/workflows/shared/lenses.ts +149 -43
- package/src/workflows/shared/options.ts +9 -1
- package/src/workflows/shared/prose.ts +0 -6
- package/src/workflows/shared/review.test.ts +27 -26
- package/src/workflows/shared/review.ts +29 -196
- package/src/workflows/shared/skills.test.ts +0 -4
- package/src/workflows/shared/skills.ts +0 -5
- package/src/workflows/shared/steps.ts +1 -1
- package/src/workflows/shared/tail.ts +18 -9
- package/src/workflows/shared/text.test.ts +125 -329
- package/src/workflows/shared/text.ts +173 -471
- package/src/workflows/shared/vars.ts +2 -3
- package/src/workflows/feature/hands-on-skip.feature +0 -316
- package/src/workflows/feature/prototype-rounds.feature +0 -156
- package/src/workflows/feature/prototype.feature +0 -428
- package/src/workflows/feature/prototype.test.ts +0 -43
- package/src/workflows/feature/prototype.ts +0 -198
- package/src/workflows/feature/review-answers.feature +0 -324
- package/src/workflows/feature/severity-budget.feature +0 -116
- package/src/workflows/feature/severity-sorted-review.feature +0 -198
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { answered, changesSince, head, judge, numeric, read, refuse, requireRevert, restoreScript, run,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-
|
|
1
|
+
import { answered, changesSince, head, judge, numeric, read, refuse, requireRevert, restoreScript, run, vars, writeScript } from "../flows.mjs";
|
|
2
|
+
import { S as reviewSections, _ as REVIEW_SECTIONS, v as reviewChunks, x as reviewRisks } from "./steering-CCNvngSH.mjs";
|
|
3
|
+
import { review } from "../workflow-kit.mjs";
|
|
4
|
+
import { At as oneLine, C as collecting, D as fixRisks, Dt as conflictFor, Et as conflictBody, Mt as renderStandards, Nt as standardKey, Ot as conflictTitle, P as buildReviewTriageMessage, T as fixNits, Tt as STANDARDS, b as answerReviewQuestions, d as guarded, et as nitFixBlock, f as healthy, j as reviewing, jt as parseStandards, k as reviewMissing, kt as isConflictTitle, lt as reviewEditNotesCapture, pt as reviewNotesCapture, ut as reviewEditsCapture, w as fixCheck, x as awaitReview, xt as withSummary, y as REVIEW } from "./reserved-X2RkNHin.mjs";
|
|
5
|
+
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-S04KEewi.mjs";
|
|
6
6
|
//#region src/workflows/shared/learn.ts
|
|
7
7
|
const SUMMARY_MAX = 200;
|
|
8
|
-
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*/;
|
|
9
8
|
const summary = (body) => {
|
|
10
|
-
const text = oneLine(body)
|
|
9
|
+
const text = oneLine(body);
|
|
11
10
|
return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
|
|
12
11
|
};
|
|
13
12
|
/** The standard a won't-fix or an own answer would make; a fix states none. */
|
|
@@ -276,139 +275,41 @@ const triage = (severities) => async (notes) => {
|
|
|
276
275
|
return [n.id, "edit"];
|
|
277
276
|
}));
|
|
278
277
|
};
|
|
279
|
-
/** Review order: security, one-way doors, the judge unsure, then should, then could. */
|
|
280
|
-
const chunkKey = (s) => s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2;
|
|
281
|
-
/** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
|
|
282
|
-
const chunkEvidence = (text, files) => [text, ...files.map(({ path, line, rangeEnd }) => {
|
|
283
|
-
const where = `${path}${line === void 0 ? "" : `#${line}${rangeEnd === void 0 ? "" : `-${rangeEnd}`}`}`;
|
|
284
|
-
const lines = read(path.replace(/^\.\//, ""))?.split("\n");
|
|
285
|
-
if (lines === void 0) return `${where}: no longer exists`;
|
|
286
|
-
return `${where}:\n${(line === void 0 ? lines : lines.slice(line - 1, rangeEnd ?? line)).join("\n")}`;
|
|
287
|
-
})].join("\n\n");
|
|
288
|
-
const bytes = (text) => new TextEncoder().encode(text).length;
|
|
289
|
-
/** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
|
|
290
|
-
const batched = (items, budget) => {
|
|
291
|
-
const batches = [];
|
|
292
|
-
let batch = [];
|
|
293
|
-
let widest = 0;
|
|
294
|
-
for (const item of items) {
|
|
295
|
-
const size = bytes(item.evidence);
|
|
296
|
-
if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
|
|
297
|
-
batches.push(batch);
|
|
298
|
-
batch = [];
|
|
299
|
-
widest = 0;
|
|
300
|
-
}
|
|
301
|
-
batch.push(item);
|
|
302
|
-
widest = Math.max(widest, size);
|
|
303
|
-
}
|
|
304
|
-
return batch.length === 0 ? batches : [...batches, batch];
|
|
305
|
-
};
|
|
306
|
-
/**
|
|
307
|
-
* Rates every chunk the reviewer wrote and sorts the \`findings\` among them
|
|
308
|
-
* first in \`full\`, each led by its tier and reason; a walkthrough chunk
|
|
309
|
-
* keeps its place and carries no tier. It is still rated, so a review with no
|
|
310
|
-
* findings still reaches \`review.ranking\` and older histories replay.
|
|
311
|
-
*/
|
|
312
|
-
const ranked = async (review, full, findings) => {
|
|
313
|
-
const texts = chunkTexts(full);
|
|
314
|
-
const chunks = reviewChunks(review).map((c, i) => ({
|
|
315
|
-
id: `C${i + 1}`,
|
|
316
|
-
title: c.title,
|
|
317
|
-
evidence: chunkEvidence(texts.get(c.title) ?? "", c.files)
|
|
318
|
-
}));
|
|
319
|
-
if (chunks.length === 0) return full;
|
|
320
|
-
const minP = numeric(vars.reviewSeverityMinP, Infinity);
|
|
321
|
-
const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768));
|
|
322
|
-
const severities = /* @__PURE__ */ new Map();
|
|
323
|
-
for (const [i, batch] of batches.entries()) {
|
|
324
|
-
const judgment = await judge("review.ranking", {
|
|
325
|
-
questions: batch.flatMap((c) => severityQuestions(c.id, `the change review chunk "${c.id}" ("${c.title}") walks through`, `the change in review chunk "${c.id}"`)),
|
|
326
|
-
evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
|
|
327
|
-
message: reviewRankingMessage(),
|
|
328
|
-
label: batches.length === 1 ? "Ranking the review" : `Ranking the review (${i + 1}/${batches.length})`
|
|
329
|
-
});
|
|
330
|
-
for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP));
|
|
331
|
-
}
|
|
332
|
-
return rankedReview(full, chunks.map((c, i) => ({
|
|
333
|
-
c,
|
|
334
|
-
i,
|
|
335
|
-
s: severities.get(c.id)
|
|
336
|
-
})).filter(({ c }) => findings.has(c.title)).sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i).map(({ c, s }) => ({
|
|
337
|
-
title: c.title,
|
|
338
|
-
label: s.reason
|
|
339
|
-
})));
|
|
340
|
-
};
|
|
341
278
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
nitFix,
|
|
345
|
-
answered,
|
|
346
|
-
standards: {
|
|
347
|
-
added: standardsAddedSince(base),
|
|
348
|
-
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
/**
|
|
352
|
-
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
353
|
-
* turn. A re-review
|
|
354
|
-
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
355
|
-
* a fresh one leaves out the notes the human already answered.
|
|
356
|
-
*/
|
|
357
|
-
const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
|
|
358
|
-
...composedOf(base, report, nitFix, answered),
|
|
359
|
-
weakened: removedAssertions(changesSince(base))
|
|
360
|
-
})) : showScript(carry, REVIEW, REVIEW), {
|
|
361
|
-
label: "Composing the review",
|
|
362
|
-
file: REVIEW,
|
|
363
|
-
base
|
|
364
|
-
});
|
|
365
|
-
/**
|
|
366
|
-
* Refuses a review with a chunk pointing at no code, or a lap note without a
|
|
367
|
-
* chunk of its own. A range with no code change has nothing to point at, so
|
|
368
|
-
* there only a note's chunk needs a pointer.
|
|
369
|
-
*/
|
|
370
|
-
const requirePointers = (review, notes, codeChanged) => {
|
|
279
|
+
/** Refuses a review with a chunk pointing at no code; a range with no code change has nothing to point at. */
|
|
280
|
+
const requirePointers = (review, codeChanged) => {
|
|
371
281
|
const chunks = reviewChunks(review);
|
|
372
|
-
const titles = new Set(chunks.map((c) => c.title));
|
|
373
|
-
const noted = new Set(notes.map((n) => n.title));
|
|
374
282
|
const problems = [
|
|
375
|
-
...chunks.filter((c) => c.pointers === 0 &&
|
|
283
|
+
...chunks.filter((c) => c.pointers === 0 && codeChanged).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
|
|
376
284
|
...chunks.filter((c) => isConflictTitle(c.title)).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — gtd writes conflicts; leave it out`),
|
|
377
|
-
...notes.filter((n) => !titles.has(n.title)).map((n) => ` - ${n.title} — no chunk of its own`),
|
|
378
285
|
...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`),
|
|
379
|
-
...reviewSections(review).map((s) => ` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} —
|
|
380
|
-
...reviewRisks(review).filter((r) => [...noted].some((title) => r.anchor.startsWith(`${title} ./`))).map((r) => ` - ${r.anchor} — no \`Risk:\` in a quality finding's chunk`)
|
|
286
|
+
...reviewSections(review).map((s) => ` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — write \`## \` chunks only`)
|
|
381
287
|
];
|
|
382
288
|
if (problems.length === 0) return;
|
|
383
|
-
refuse(`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code
|
|
289
|
+
refuse(`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code:\n${problems.join("\n")}`);
|
|
384
290
|
};
|
|
385
291
|
/**
|
|
386
292
|
* A reviewer walks the human through everything since `base`, every chunk
|
|
387
|
-
* pointing at its code
|
|
388
|
-
* then adds each note's text and answer boxes, and the summary chunks.
|
|
293
|
+
* pointing at its code; gtd then appends the summary chunks.
|
|
389
294
|
*/
|
|
390
|
-
const walkthrough = async (base,
|
|
391
|
-
|
|
392
|
-
await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
|
|
295
|
+
const walkthrough = async (base, summary, lap, carry, nitFix, afterRiskFix) => {
|
|
296
|
+
await reviewing(base, carry, lap, nitFixBlock(nitFix));
|
|
393
297
|
const written = read(REVIEW);
|
|
394
298
|
if (written === void 0) return;
|
|
395
|
-
|
|
396
|
-
requirePointers(written, notes, changesSince(base).some((c) => isCode(c.path)));
|
|
299
|
+
requirePointers(written, changesSince(base).some((c) => isCode(c.path)));
|
|
397
300
|
if (!afterRiskFix && reviewRisks(written).length > 0) return;
|
|
398
|
-
const full =
|
|
301
|
+
const full = withSummary(written, summary);
|
|
399
302
|
if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
|
|
400
|
-
label: "
|
|
303
|
+
label: "Adding the quality lap's summary",
|
|
401
304
|
file: REVIEW,
|
|
402
305
|
base
|
|
403
306
|
});
|
|
404
307
|
};
|
|
405
308
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
* caller to build, questions are answered in place, nits fixed in one batch,
|
|
411
|
-
* praise dropped.
|
|
309
|
+
* A reviewer's walkthrough of everything since `base` reaches a human, who
|
|
310
|
+
* may comment: each note is judged — edits end the review as `feedback` for
|
|
311
|
+
* the caller to build, questions are answered in place, nits fixed in one
|
|
312
|
+
* batch, praise dropped.
|
|
412
313
|
*/
|
|
413
314
|
const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
414
315
|
const { guard, guardRests, fixAccess, rules } = options;
|
|
@@ -419,7 +320,6 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
419
320
|
};
|
|
420
321
|
const severities = /* @__PURE__ */ new Map();
|
|
421
322
|
let nitFix;
|
|
422
|
-
const answered = /* @__PURE__ */ new Set();
|
|
423
323
|
let riskFixed = false;
|
|
424
324
|
return review(base, {
|
|
425
325
|
file: REVIEW,
|
|
@@ -429,8 +329,14 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
429
329
|
const fixedAt = nitFix;
|
|
430
330
|
const afterRiskFix = riskFixed;
|
|
431
331
|
riskFixed = false;
|
|
432
|
-
const
|
|
433
|
-
|
|
332
|
+
const summary = {
|
|
333
|
+
report: options.report ?? NO_FINDINGS,
|
|
334
|
+
standards: {
|
|
335
|
+
added: standardsAddedSince(b),
|
|
336
|
+
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
return guarded(() => walkthrough(b, summary, options.lap ?? "first", carry, fixedAt, afterRiskFix), guard)();
|
|
434
340
|
},
|
|
435
341
|
fixRisks: guarded((notes) => {
|
|
436
342
|
riskFixed = true;
|
|
@@ -449,7 +355,6 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
449
355
|
reviewMissing,
|
|
450
356
|
collecting: feedback.collecting,
|
|
451
357
|
triage: triage(severities),
|
|
452
|
-
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
453
358
|
settles: settlesStandard,
|
|
454
359
|
learn: learnStandards,
|
|
455
360
|
pending: standardsPending,
|
|
@@ -519,4 +424,4 @@ const reUnwind = async (feedback) => {
|
|
|
519
424
|
requireRevert(edited, base);
|
|
520
425
|
};
|
|
521
426
|
//#endregion
|
|
522
|
-
export {
|
|
427
|
+
export { isCode as n, reUnwind as r, buildTail as t };
|
|
@@ -9415,7 +9415,7 @@ function serializeChunks(chunks, expandTabs) {
|
|
|
9415
9415
|
* @returns {ParseContext}
|
|
9416
9416
|
* Parser.
|
|
9417
9417
|
*/
|
|
9418
|
-
function parse(options) {
|
|
9418
|
+
function parse$1(options) {
|
|
9419
9419
|
/** @type {ParseContext} */
|
|
9420
9420
|
const parser = {
|
|
9421
9421
|
constructs: combineExtensions([constructs_exports, ...(options || {}).extensions || []]),
|
|
@@ -9730,7 +9730,7 @@ function fromMarkdown(value, encoding, options) {
|
|
|
9730
9730
|
options = encoding;
|
|
9731
9731
|
encoding = void 0;
|
|
9732
9732
|
}
|
|
9733
|
-
return compiler(options)(postprocess(parse(options).document().write(preprocess()(value, encoding, true))));
|
|
9733
|
+
return compiler(options)(postprocess(parse$1(options).document().write(preprocess()(value, encoding, true))));
|
|
9734
9734
|
}
|
|
9735
9735
|
/**
|
|
9736
9736
|
* Note this compiler only understand complete buffering, not streaming.
|
|
@@ -13347,7 +13347,7 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
|
|
|
13347
13347
|
//#endregion
|
|
13348
13348
|
//#region src/steering/Threads.ts
|
|
13349
13349
|
const PREFIX_RE$1 = /^(H|A):\s*/;
|
|
13350
|
-
const lineOf = (node, which) => toLspPosition(node.position[which]).line;
|
|
13350
|
+
const lineOf$1 = (node, which) => toLspPosition(node.position[which]).line;
|
|
13351
13351
|
/** 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. */
|
|
13352
13352
|
const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
13353
13353
|
const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText$1(content, item));
|
|
@@ -13361,9 +13361,9 @@ const isThreadBody = (content, children) => {
|
|
|
13361
13361
|
};
|
|
13362
13362
|
const foldName$1 = (name) => name.toLowerCase();
|
|
13363
13363
|
/** One list item's entry, reporting any syntax problem; `previous` is the author before it. */
|
|
13364
|
-
const entryOf = (content, item, previous, report) => {
|
|
13365
|
-
const start = lineOf(item, "start");
|
|
13366
|
-
const end = lineOf(item, "end");
|
|
13364
|
+
const entryOf$1 = (content, item, previous, report) => {
|
|
13365
|
+
const start = lineOf$1(item, "start");
|
|
13366
|
+
const end = lineOf$1(item, "end");
|
|
13367
13367
|
const match = prefixOf(content, item);
|
|
13368
13368
|
if (!match) {
|
|
13369
13369
|
report(`an entry must start with "H:" or "A:"`, start, end);
|
|
@@ -13379,7 +13379,7 @@ const entryOf = (content, item, previous, report) => {
|
|
|
13379
13379
|
author,
|
|
13380
13380
|
text,
|
|
13381
13381
|
line: start,
|
|
13382
|
-
endLine: last ? lineOf(last, "end") : end,
|
|
13382
|
+
endLine: last ? lineOf$1(last, "end") : end,
|
|
13383
13383
|
column: toLspPosition(item.position.start).character
|
|
13384
13384
|
};
|
|
13385
13385
|
};
|
|
@@ -13388,16 +13388,16 @@ const threadOf = (content, def, list, markers, report) => {
|
|
|
13388
13388
|
const entries = [];
|
|
13389
13389
|
let previous;
|
|
13390
13390
|
for (const item of list.children) {
|
|
13391
|
-
const entry = entryOf(content, item, previous, report);
|
|
13391
|
+
const entry = entryOf$1(content, item, previous, report);
|
|
13392
13392
|
if (!entry) continue;
|
|
13393
13393
|
previous = entry.author;
|
|
13394
13394
|
entries.push(entry);
|
|
13395
13395
|
}
|
|
13396
|
-
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"));
|
|
13396
|
+
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"));
|
|
13397
13397
|
return {
|
|
13398
13398
|
name,
|
|
13399
|
-
line: lineOf(def, "start"),
|
|
13400
|
-
endLine: lineOf(def, "end"),
|
|
13399
|
+
line: lineOf$1(def, "start"),
|
|
13400
|
+
endLine: lineOf$1(def, "end"),
|
|
13401
13401
|
markers: markers.filter((m) => foldName$1(m.name) === foldName$1(name)),
|
|
13402
13402
|
entries,
|
|
13403
13403
|
waitingOn: entries[entries.length - 1]?.author === "agent" ? "human" : "agent"
|
|
@@ -16208,62 +16208,249 @@ const reviewSections = (content) => splitChunks(parseMarkdown(content), content)
|
|
|
16208
16208
|
section: s.section,
|
|
16209
16209
|
line: s.headingLine
|
|
16210
16210
|
}));
|
|
16211
|
-
|
|
16212
|
-
|
|
16213
|
-
|
|
16214
|
-
|
|
16215
|
-
|
|
16216
|
-
|
|
16217
|
-
|
|
16218
|
-
|
|
16219
|
-
|
|
16220
|
-
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
16211
|
+
//#endregion
|
|
16212
|
+
//#region src/steering/findings.ts
|
|
16213
|
+
/** The `# ` heading the findings a judge called noise sit under. */
|
|
16214
|
+
const NOISE_HEADING = "Dropped as noise";
|
|
16215
|
+
/** The label of the one box under each finding. */
|
|
16216
|
+
const FIX_BOX = "fix";
|
|
16217
|
+
const OPEN_GROUP = "Open";
|
|
16218
|
+
const MARKER = /\[\^[^\]\s]+\]/g;
|
|
16219
|
+
const isBox = (item, content) => {
|
|
16220
|
+
if (typeof item.checked !== "boolean") return false;
|
|
16221
|
+
const first = item.children[0];
|
|
16222
|
+
if (first?.type !== "paragraph" || first.position === void 0) return false;
|
|
16223
|
+
const text = content.slice(first.position.start.offset, first.position.end.offset);
|
|
16224
|
+
return new RegExp(`^fix(?![\\w-])`).test(text);
|
|
16224
16225
|
};
|
|
16225
|
-
|
|
16226
|
-
const
|
|
16227
|
-
|
|
16228
|
-
|
|
16229
|
-
|
|
16230
|
-
|
|
16231
|
-
return rest.length === 0 ? heading : `${heading}\n\n${rest.join("\n")}`;
|
|
16226
|
+
const slice = (content, node) => content.slice(node.position.start.offset, node.position.end.offset);
|
|
16227
|
+
const lineOf = (node, end = false) => toLspPosition(end ? node.position.end : node.position.start).line;
|
|
16228
|
+
/** The box item's own text past its label: a formatter may fold a guidance line into it. */
|
|
16229
|
+
const inlineGuidance = (content, item) => {
|
|
16230
|
+
const first = item.children[0];
|
|
16231
|
+
return content.slice(first.position.start.offset, item.position.end.offset).slice(3).replace(/^[\s:—–-]+/, "").trim();
|
|
16232
16232
|
};
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
const
|
|
16233
|
+
const guidanceOf = (parts) => parts.map((p) => p.replace(MARKER, "").trim()).filter((p) => p !== "").join("\n\n");
|
|
16234
|
+
/** `nodes` split at `box`: the body before it, the guidance after it. */
|
|
16235
|
+
const splitAtBox = (content, nodes, box) => {
|
|
16236
|
+
const at = nodes.findIndex((n) => n.type === "list" && n.children.includes(box));
|
|
16237
|
+
const list = nodes[at];
|
|
16238
|
+
const boxAt = list.children.indexOf(box);
|
|
16239
|
+
const bodyNodes = nodes.slice(0, boxAt > 0 ? at + 1 : at);
|
|
16240
|
+
const trailing = list.children.slice(boxAt + 1);
|
|
16241
|
+
const after = nodes.slice(at + 1);
|
|
16242
|
+
return {
|
|
16243
|
+
bodyNodes,
|
|
16244
|
+
body: bodyNodes.map((n) => n === list ? content.slice(n.position.start.offset, box.position.start.offset).trimEnd() : slice(content, n)).join("\n\n"),
|
|
16245
|
+
notBody: list.children.slice(boxAt),
|
|
16246
|
+
guidance: guidanceOf([inlineGuidance(content, box), ...[...trailing, ...after].map((n) => slice(content, n))]),
|
|
16247
|
+
last: after.at(-1) ?? trailing.at(-1) ?? box
|
|
16248
|
+
};
|
|
16249
|
+
};
|
|
16250
|
+
const entryOf = (content, title, noise, headingLine, nodes, endLine) => {
|
|
16251
|
+
const notes = nodes.filter((n) => n.type !== "footnoteDefinition");
|
|
16252
|
+
const boxes = notes.flatMap((n) => n.type === "list" ? n.children.filter((i) => isBox(i, content)) : []);
|
|
16253
|
+
const box = boxes[0];
|
|
16254
|
+
const split = box === void 0 ? {
|
|
16255
|
+
bodyNodes: notes,
|
|
16256
|
+
body: notes.map((n) => slice(content, n)).join("\n\n"),
|
|
16257
|
+
notBody: [],
|
|
16258
|
+
guidance: ""
|
|
16259
|
+
} : splitAtBox(content, notes, box);
|
|
16260
|
+
return {
|
|
16261
|
+
title,
|
|
16262
|
+
body: split.body,
|
|
16263
|
+
noise,
|
|
16264
|
+
fix: box?.checked === true,
|
|
16265
|
+
guidance: split.guidance,
|
|
16266
|
+
boxes: boxes.length,
|
|
16267
|
+
headingLine,
|
|
16268
|
+
bodyNodes: split.bodyNodes,
|
|
16269
|
+
...box === void 0 ? {} : { box },
|
|
16270
|
+
notBody: new Set(split.notBody),
|
|
16271
|
+
lastLine: split.last === void 0 ? headingLine : lineOf(split.last, true),
|
|
16272
|
+
endLine
|
|
16273
|
+
};
|
|
16274
|
+
};
|
|
16275
|
+
const parse = (content) => {
|
|
16241
16276
|
const tree = parseMarkdown(content);
|
|
16242
|
-
const
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
const
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
const chunk = chunks[i];
|
|
16249
|
-
const block = lines.slice(chunk.headingLine, chunks[i + 1]?.headingLine ?? lines.length);
|
|
16250
|
-
return chunkText(demote ? demoted(chunk, block) : block, demote ? 1 : headingSize(chunk));
|
|
16277
|
+
const entries = [];
|
|
16278
|
+
let noise = false;
|
|
16279
|
+
let open;
|
|
16280
|
+
const close = (endLine) => {
|
|
16281
|
+
if (open !== void 0) entries.push(entryOf(content, open.title, noise, open.line, open.nodes, endLine));
|
|
16282
|
+
open = void 0;
|
|
16251
16283
|
};
|
|
16252
|
-
const
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
|
|
16284
|
+
for (const node of tree.children) {
|
|
16285
|
+
if (node.type === "heading" && node.depth <= 2) {
|
|
16286
|
+
close(lineOf(node) - 1);
|
|
16287
|
+
const text = headingText$1(content, node, (t) => t.replace(MARKER, ""));
|
|
16288
|
+
if (node.depth === 1) noise = text === NOISE_HEADING;
|
|
16289
|
+
else open = {
|
|
16290
|
+
title: text,
|
|
16291
|
+
line: lineOf(node),
|
|
16292
|
+
nodes: []
|
|
16293
|
+
};
|
|
16294
|
+
continue;
|
|
16295
|
+
}
|
|
16296
|
+
open?.nodes.push(node);
|
|
16297
|
+
}
|
|
16298
|
+
close(content.split(/\r?\n/).length - 1);
|
|
16299
|
+
return entries;
|
|
16300
|
+
};
|
|
16301
|
+
/** Every `## ` finding in a finding-gate file, in file order. */
|
|
16302
|
+
const findingEntries = (content) => parse(content).map(({ title, body, noise, fix, guidance, boxes, headingLine }) => ({
|
|
16303
|
+
title,
|
|
16304
|
+
body,
|
|
16305
|
+
noise,
|
|
16306
|
+
fix,
|
|
16307
|
+
guidance,
|
|
16308
|
+
boxes,
|
|
16309
|
+
headingLine
|
|
16310
|
+
}));
|
|
16311
|
+
const entryFindings = (content) => parse(content).flatMap((e) => {
|
|
16312
|
+
if (e.boxes === 0) return [{
|
|
16313
|
+
message: `"${e.title}" has no \`- [ ] fix\` box — restore it, ticked to fix the finding or unticked to drop it`,
|
|
16314
|
+
line: e.headingLine
|
|
16315
|
+
}];
|
|
16316
|
+
if (e.boxes > 1) return [{
|
|
16317
|
+
message: `"${e.title}" has ${e.boxes} \`fix\` boxes — keep exactly one`,
|
|
16318
|
+
line: e.headingLine
|
|
16319
|
+
}];
|
|
16320
|
+
if (!e.fix && e.guidance !== "") return [{
|
|
16321
|
+
message: `"${e.title}" has guidance under an unticked box — tick it to fix the finding, or delete the text to drop it`,
|
|
16322
|
+
line: lineOf(e.box)
|
|
16323
|
+
}];
|
|
16324
|
+
return [];
|
|
16325
|
+
});
|
|
16326
|
+
const findingsView = (content) => {
|
|
16327
|
+
const noteAt = noteLookup(content);
|
|
16328
|
+
return { nodes: parse(content).map((e, index) => {
|
|
16329
|
+
const note = noteAt(e.headingLine);
|
|
16330
|
+
return {
|
|
16331
|
+
title: e.title,
|
|
16332
|
+
anchor: {
|
|
16333
|
+
kind: "chunk",
|
|
16334
|
+
index
|
|
16335
|
+
},
|
|
16336
|
+
group: e.noise ? NOISE_HEADING : OPEN_GROUP,
|
|
16337
|
+
...e.boxes === 1 ? { checked: e.fix } : {},
|
|
16338
|
+
...e.guidance === "" ? {} : { note: e.guidance },
|
|
16339
|
+
...note?.thread !== void 0 ? { thread: note.thread } : {},
|
|
16340
|
+
body: blockNodesOfRun(content, e.bodyNodes, { skipListItem: (i) => e.notBody.has(i) })
|
|
16341
|
+
};
|
|
16342
|
+
}) };
|
|
16343
|
+
};
|
|
16344
|
+
const findingsOutline = (content) => {
|
|
16345
|
+
const lines = content.split(/\r?\n/);
|
|
16346
|
+
return [...parse(content).map((e) => ({
|
|
16347
|
+
name: e.title,
|
|
16348
|
+
detail: e.noise ? "noise" : e.fix ? "fix" : "drop",
|
|
16349
|
+
range: spanRange(lines, e.headingLine, Math.max(e.headingLine, e.endLine)),
|
|
16350
|
+
selectionRange: lineRange(lines, e.headingLine)
|
|
16351
|
+
})), ...threadOutlineNodes(content).map((t) => t.node)];
|
|
16352
|
+
};
|
|
16353
|
+
const findingsAnnotate = (content, anchor, text) => {
|
|
16354
|
+
const entries = parse(content);
|
|
16355
|
+
const lines = content.split(/\r?\n/);
|
|
16356
|
+
const resolved = (() => {
|
|
16357
|
+
if (anchor.kind === "chunk") {
|
|
16358
|
+
const e = entries[anchor.index];
|
|
16359
|
+
return e && {
|
|
16360
|
+
line: e.headingLine,
|
|
16361
|
+
blockEndLine: e.headingLine,
|
|
16362
|
+
key: `chunk:${anchor.index}`
|
|
16363
|
+
};
|
|
16364
|
+
}
|
|
16365
|
+
if (anchor.kind !== "paragraph") return void 0;
|
|
16366
|
+
const block = entries.flatMap((e) => e.bodyNodes).find((n) => anchor.line >= lineOf(n) && anchor.line <= lineOf(n, true));
|
|
16367
|
+
return block && {
|
|
16368
|
+
line: anchor.line,
|
|
16369
|
+
blockEndLine: lineOf(block, true),
|
|
16370
|
+
key: `paragraph:${anchor.line}`
|
|
16371
|
+
};
|
|
16372
|
+
})();
|
|
16373
|
+
if (resolved === void 0) return {
|
|
16374
|
+
ok: false,
|
|
16375
|
+
reason: "anchor-not-found"
|
|
16376
|
+
};
|
|
16377
|
+
const result = footnoteAttachEdits(content, {
|
|
16378
|
+
...resolved,
|
|
16379
|
+
endCharacter: (lines[resolved.line] ?? "").length
|
|
16380
|
+
}, text);
|
|
16381
|
+
return result.ok ? {
|
|
16382
|
+
ok: true,
|
|
16383
|
+
edits: result.edits
|
|
16384
|
+
} : result;
|
|
16385
|
+
};
|
|
16386
|
+
/** Ticks or unticks a finding's box; `text` replaces its guidance, and unticking drops it. */
|
|
16387
|
+
const findingsApply = (content, anchor, opts) => {
|
|
16388
|
+
if (anchor.kind !== "chunk") return {
|
|
16389
|
+
ok: false,
|
|
16390
|
+
reason: "anchor-not-found"
|
|
16391
|
+
};
|
|
16392
|
+
const e = parse(content)[anchor.index];
|
|
16393
|
+
if (e?.box === void 0 || e.boxes !== 1) return {
|
|
16394
|
+
ok: false,
|
|
16395
|
+
reason: "anchor-not-found"
|
|
16396
|
+
};
|
|
16397
|
+
const fix = opts.checked ?? e.fix;
|
|
16398
|
+
const guidance = fix ? (opts.text ?? e.guidance).trim() : "";
|
|
16399
|
+
if (fix === e.fix && guidance === e.guidance) return {
|
|
16400
|
+
ok: true,
|
|
16401
|
+
edits: []
|
|
16402
|
+
};
|
|
16403
|
+
const lines = content.split(/\r?\n/);
|
|
16404
|
+
const eol = eolOf(content);
|
|
16405
|
+
const start = lineOf(e.box);
|
|
16406
|
+
const box = `${lines[start].match(/^\s*[-*+]\s+/)[0]}[${fix ? "x" : " "}] fix`;
|
|
16407
|
+
const text = guidance === "" ? box : `${box}${eol}${eol}${guidance.split(/\r?\n/).join(eol)}`;
|
|
16408
|
+
return {
|
|
16409
|
+
ok: true,
|
|
16410
|
+
edits: [{
|
|
16411
|
+
range: {
|
|
16412
|
+
start: {
|
|
16413
|
+
line: start,
|
|
16414
|
+
character: 0
|
|
16415
|
+
},
|
|
16416
|
+
end: lineRange(lines, e.lastLine).end
|
|
16417
|
+
},
|
|
16418
|
+
newText: text
|
|
16419
|
+
}]
|
|
16259
16420
|
};
|
|
16260
|
-
|
|
16261
|
-
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16421
|
+
};
|
|
16422
|
+
/** The finding gate's file: one `## ` finding each, with one `fix` box and optional guidance below it. */
|
|
16423
|
+
const findingsDescriptor = {
|
|
16424
|
+
sample: `# Quality
|
|
16425
|
+
|
|
16426
|
+
## A finding a lens wrote[^nafind1]
|
|
16427
|
+
|
|
16428
|
+
[^nafind1]:
|
|
16429
|
+
Attached via the phone UI, this note shows a comment on a finding with a
|
|
16430
|
+
\`multi word code span\` that runs past eighty characters in total length.
|
|
16431
|
+
|
|
16432
|
+
What the lens found, and where.
|
|
16433
|
+
|
|
16434
|
+
- [x] fix
|
|
16435
|
+
|
|
16436
|
+
Fix it the simple way.
|
|
16437
|
+
|
|
16438
|
+
# Dropped as noise
|
|
16439
|
+
|
|
16440
|
+
## A finding the judge called noise
|
|
16441
|
+
|
|
16442
|
+
Why the judge dropped it.
|
|
16443
|
+
|
|
16444
|
+
- [ ] fix
|
|
16445
|
+
`,
|
|
16446
|
+
validate: (content) => [...entryFindings(content), ...threadFindings(content)],
|
|
16447
|
+
outline: findingsOutline,
|
|
16448
|
+
actions: (content, range) => threadReplyActions(content, range.start),
|
|
16449
|
+
view: findingsView,
|
|
16450
|
+
annotate: findingsAnnotate,
|
|
16451
|
+
apply: findingsApply,
|
|
16452
|
+
clearTicks: (content) => content,
|
|
16453
|
+
openThreads: openThreadsOf
|
|
16267
16454
|
};
|
|
16268
16455
|
//#endregion
|
|
16269
16456
|
//#region src/steering/Edits.ts
|
|
@@ -16595,8 +16782,12 @@ const stripCodeThreads = (path, text) => {
|
|
|
16595
16782
|
};
|
|
16596
16783
|
//#endregion
|
|
16597
16784
|
//#region src/steering/index.ts
|
|
16598
|
-
/** The built-in registry: mode name → its format, in registry order. The single place
|
|
16599
|
-
const REGISTRY = /* @__PURE__ */ new Map([
|
|
16785
|
+
/** 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. */
|
|
16786
|
+
const REGISTRY = /* @__PURE__ */ new Map([
|
|
16787
|
+
["qa", qaDescriptor],
|
|
16788
|
+
["review", reviewDescriptor],
|
|
16789
|
+
["findings", findingsDescriptor]
|
|
16790
|
+
]);
|
|
16600
16791
|
/** Every built-in format name, in registry order. */
|
|
16601
16792
|
const BUILT_IN_MODE_NAMES = [...REGISTRY.keys()];
|
|
16602
16793
|
/** 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. */
|
|
@@ -16633,4 +16824,4 @@ const viewOf = (format, content) => ({
|
|
|
16633
16824
|
/** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
|
|
16634
16825
|
const openThreads = (format, content) => format.openThreads?.(content) ?? [];
|
|
16635
16826
|
//#endregion
|
|
16636
|
-
export {
|
|
16827
|
+
export { parseThreads as A, unansweredNotes as C, THREAD_REPLY_ACTION_TITLE as D, FOOTNOTE_ACTION_TITLE as E, formatFinding as F, getParseCount as M, headingSectionBodies as N, openThreadFindings as O, headingSections as P, reviewSections as S, isAnswered as T, REVIEW_SECTIONS as _, unansweredQuestions as a, reviewNotes as b, stripCodeThreads as c, FIX_BOX as d, NOISE_HEADING as f, ANSWER_WONT_FIX as g, ANSWER_LEAD as h, steeringFormatOrFreeForm as i, parseThreadsWithFindings as j, parseFootnotes as k, freeFormFormat as l, ANSWER_FIX as m, openThreads as n, viewOf as o, findingEntries as p, steeringFormatFor as r, parseCodeThreads as s, BUILT_IN_MODE_NAMES as t, applySteeringEdits as u, reviewChunks as v, FREE_TEXT_PLACEHOLDER as w, reviewRisks as x, reviewDecisions as y };
|