@pmelab/gtd 59.0.0 → 60.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 +42 -38
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/{testkit-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
- package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +1101 -1025
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +371 -82
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +68 -61
- package/src/cli/Cli.test.ts +311 -229
- package/src/cli/Cli.ts +486 -585
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +31 -1
- package/src/cli/Program.test.ts +131 -82
- package/src/cli/Program.ts +197 -111
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +25 -6
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +188 -54
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +481 -98
- package/src/gherkin/messages.test.ts +124 -0
- package/src/gherkin/messages.ts +101 -0
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +9 -9
- package/src/testkit/TestRepo.test.ts +289 -6
- package/src/testkit/TestRepo.ts +317 -41
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +7 -14
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Land.ts +1 -1
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +175 -152
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
|
@@ -13146,7 +13146,6 @@ function spaceThenNonSpace(effects, ok, nok) {
|
|
|
13146
13146
|
* timing is not.
|
|
13147
13147
|
*/
|
|
13148
13148
|
let parseCount = 0;
|
|
13149
|
-
const getParseCount = () => parseCount;
|
|
13150
13149
|
/** One-entry memo: the last content string parsed and its tree, so re-validating the same document never re-parses. */
|
|
13151
13150
|
let memoContent;
|
|
13152
13151
|
let memoTree;
|
|
@@ -14281,6 +14280,250 @@ const blockRunInline = (content, nodes, options) => {
|
|
|
14281
14280
|
return joinInlineRuns(visibleNodes(content, nodes, options).map((node) => nodeInline(content, node, options)));
|
|
14282
14281
|
};
|
|
14283
14282
|
//#endregion
|
|
14283
|
+
//#region src/steering/findings.ts
|
|
14284
|
+
/** The `# ` heading the findings a judge called noise sit under. */
|
|
14285
|
+
const NOISE_HEADING = "Dropped as noise";
|
|
14286
|
+
/** The label of the one box under each finding. */
|
|
14287
|
+
const FIX_BOX = "fix";
|
|
14288
|
+
const OPEN_GROUP = "Open";
|
|
14289
|
+
const MARKER = /\[\^[^\]\s]+\]/g;
|
|
14290
|
+
const isBox = (item, content) => {
|
|
14291
|
+
if (typeof item.checked !== "boolean") return false;
|
|
14292
|
+
const first = item.children[0];
|
|
14293
|
+
if (first?.type !== "paragraph" || first.position === void 0) return false;
|
|
14294
|
+
const text = content.slice(first.position.start.offset, first.position.end.offset);
|
|
14295
|
+
return new RegExp(`^fix(?![\\w-])`).test(text);
|
|
14296
|
+
};
|
|
14297
|
+
const slice = (content, node) => content.slice(node.position.start.offset, node.position.end.offset);
|
|
14298
|
+
const lineOf = (node, end = false) => toLspPosition(end ? node.position.end : node.position.start).line;
|
|
14299
|
+
/** The box item's own text past its label: a formatter may fold a guidance line into it. */
|
|
14300
|
+
const inlineGuidance = (content, item) => {
|
|
14301
|
+
const first = item.children[0];
|
|
14302
|
+
return content.slice(first.position.start.offset, item.position.end.offset).slice(3).replace(/^[\s:—–-]+/, "").trim();
|
|
14303
|
+
};
|
|
14304
|
+
const guidanceOf = (parts) => parts.map((p) => p.replace(MARKER, "").trim()).filter((p) => p !== "").join("\n\n");
|
|
14305
|
+
/** `nodes` split at `box`: the body before it, the guidance after it. */
|
|
14306
|
+
const splitAtBox = (content, nodes, box) => {
|
|
14307
|
+
const at = nodes.findIndex((n) => n.type === "list" && n.children.includes(box));
|
|
14308
|
+
const list = nodes[at];
|
|
14309
|
+
const boxAt = list.children.indexOf(box);
|
|
14310
|
+
const bodyNodes = nodes.slice(0, boxAt > 0 ? at + 1 : at);
|
|
14311
|
+
const trailing = list.children.slice(boxAt + 1);
|
|
14312
|
+
const after = nodes.slice(at + 1);
|
|
14313
|
+
return {
|
|
14314
|
+
bodyNodes,
|
|
14315
|
+
body: bodyNodes.map((n) => n === list ? content.slice(n.position.start.offset, box.position.start.offset).trimEnd() : slice(content, n)).join("\n\n"),
|
|
14316
|
+
notBody: list.children.slice(boxAt),
|
|
14317
|
+
guidance: guidanceOf([inlineGuidance(content, box), ...[...trailing, ...after].map((n) => slice(content, n))]),
|
|
14318
|
+
last: after.at(-1) ?? trailing.at(-1) ?? box
|
|
14319
|
+
};
|
|
14320
|
+
};
|
|
14321
|
+
const entryOf = (content, title, noise, headingLine, nodes, endLine) => {
|
|
14322
|
+
const notes = nodes.filter((n) => n.type !== "footnoteDefinition");
|
|
14323
|
+
const boxes = notes.flatMap((n) => n.type === "list" ? n.children.filter((i) => isBox(i, content)) : []);
|
|
14324
|
+
const box = boxes[0];
|
|
14325
|
+
const split = box === void 0 ? {
|
|
14326
|
+
bodyNodes: notes,
|
|
14327
|
+
body: notes.map((n) => slice(content, n)).join("\n\n"),
|
|
14328
|
+
notBody: [],
|
|
14329
|
+
guidance: ""
|
|
14330
|
+
} : splitAtBox(content, notes, box);
|
|
14331
|
+
return {
|
|
14332
|
+
title,
|
|
14333
|
+
body: split.body,
|
|
14334
|
+
noise,
|
|
14335
|
+
fix: box?.checked === true,
|
|
14336
|
+
guidance: split.guidance,
|
|
14337
|
+
boxes: boxes.length,
|
|
14338
|
+
headingLine,
|
|
14339
|
+
bodyNodes: split.bodyNodes,
|
|
14340
|
+
...box === void 0 ? {} : { box },
|
|
14341
|
+
notBody: new Set(split.notBody),
|
|
14342
|
+
lastLine: split.last === void 0 ? headingLine : lineOf(split.last, true),
|
|
14343
|
+
endLine
|
|
14344
|
+
};
|
|
14345
|
+
};
|
|
14346
|
+
const parse = (content) => {
|
|
14347
|
+
const tree = parseMarkdown(content);
|
|
14348
|
+
const entries = [];
|
|
14349
|
+
let noise = false;
|
|
14350
|
+
let open;
|
|
14351
|
+
const close = (endLine) => {
|
|
14352
|
+
if (open !== void 0) entries.push(entryOf(content, open.title, noise, open.line, open.nodes, endLine));
|
|
14353
|
+
open = void 0;
|
|
14354
|
+
};
|
|
14355
|
+
for (const node of tree.children) {
|
|
14356
|
+
if (node.type === "heading" && node.depth <= 2) {
|
|
14357
|
+
close(lineOf(node) - 1);
|
|
14358
|
+
const text = headingText$1(content, node, (t) => t.replace(MARKER, ""));
|
|
14359
|
+
if (node.depth === 1) noise = text === NOISE_HEADING;
|
|
14360
|
+
else open = {
|
|
14361
|
+
title: text,
|
|
14362
|
+
line: lineOf(node),
|
|
14363
|
+
nodes: []
|
|
14364
|
+
};
|
|
14365
|
+
continue;
|
|
14366
|
+
}
|
|
14367
|
+
open?.nodes.push(node);
|
|
14368
|
+
}
|
|
14369
|
+
close(content.split(/\r?\n/).length - 1);
|
|
14370
|
+
return entries;
|
|
14371
|
+
};
|
|
14372
|
+
/** Every `## ` finding in a finding-gate file, in file order. */
|
|
14373
|
+
const findingEntries = (content) => parse(content).map(({ title, body, noise, fix, guidance, boxes, headingLine }) => ({
|
|
14374
|
+
title,
|
|
14375
|
+
body,
|
|
14376
|
+
noise,
|
|
14377
|
+
fix,
|
|
14378
|
+
guidance,
|
|
14379
|
+
boxes,
|
|
14380
|
+
headingLine
|
|
14381
|
+
}));
|
|
14382
|
+
const entryFindings = (content) => parse(content).flatMap((e) => {
|
|
14383
|
+
if (e.boxes === 0) return [{
|
|
14384
|
+
message: `"${e.title}" has no \`- [ ] fix\` box — restore it, ticked to fix the finding or unticked to drop it`,
|
|
14385
|
+
line: e.headingLine
|
|
14386
|
+
}];
|
|
14387
|
+
if (e.boxes > 1) return [{
|
|
14388
|
+
message: `"${e.title}" has ${e.boxes} \`fix\` boxes — keep exactly one`,
|
|
14389
|
+
line: e.headingLine
|
|
14390
|
+
}];
|
|
14391
|
+
if (!e.fix && e.guidance !== "") return [{
|
|
14392
|
+
message: `"${e.title}" has guidance under an unticked box — tick it to fix the finding, or delete the text to drop it`,
|
|
14393
|
+
line: lineOf(e.box)
|
|
14394
|
+
}];
|
|
14395
|
+
return [];
|
|
14396
|
+
});
|
|
14397
|
+
const findingsView = (content) => {
|
|
14398
|
+
const noteAt = noteLookup(content);
|
|
14399
|
+
return { nodes: parse(content).map((e, index) => {
|
|
14400
|
+
const note = noteAt(e.headingLine);
|
|
14401
|
+
return {
|
|
14402
|
+
title: e.title,
|
|
14403
|
+
anchor: {
|
|
14404
|
+
kind: "chunk",
|
|
14405
|
+
index
|
|
14406
|
+
},
|
|
14407
|
+
group: e.noise ? NOISE_HEADING : OPEN_GROUP,
|
|
14408
|
+
...e.boxes === 1 ? { checked: e.fix } : {},
|
|
14409
|
+
...e.guidance === "" ? {} : { note: e.guidance },
|
|
14410
|
+
...note?.thread !== void 0 ? { thread: note.thread } : {},
|
|
14411
|
+
body: blockNodesOfRun(content, e.bodyNodes, { skipListItem: (i) => e.notBody.has(i) })
|
|
14412
|
+
};
|
|
14413
|
+
}) };
|
|
14414
|
+
};
|
|
14415
|
+
const findingsOutline = (content) => {
|
|
14416
|
+
const lines = content.split(/\r?\n/);
|
|
14417
|
+
return [...parse(content).map((e) => ({
|
|
14418
|
+
name: e.title,
|
|
14419
|
+
detail: e.noise ? "noise" : e.fix ? "fix" : "drop",
|
|
14420
|
+
range: spanRange(lines, e.headingLine, Math.max(e.headingLine, e.endLine)),
|
|
14421
|
+
selectionRange: lineRange(lines, e.headingLine)
|
|
14422
|
+
})), ...threadOutlineNodes(content).map((t) => t.node)];
|
|
14423
|
+
};
|
|
14424
|
+
const findingsAnnotate = (content, anchor, text) => {
|
|
14425
|
+
const entries = parse(content);
|
|
14426
|
+
const lines = content.split(/\r?\n/);
|
|
14427
|
+
const resolved = (() => {
|
|
14428
|
+
if (anchor.kind === "chunk") {
|
|
14429
|
+
const e = entries[anchor.index];
|
|
14430
|
+
return e && {
|
|
14431
|
+
line: e.headingLine,
|
|
14432
|
+
blockEndLine: e.headingLine,
|
|
14433
|
+
key: `chunk:${anchor.index}`
|
|
14434
|
+
};
|
|
14435
|
+
}
|
|
14436
|
+
if (anchor.kind !== "paragraph") return void 0;
|
|
14437
|
+
const block = entries.flatMap((e) => e.bodyNodes).find((n) => anchor.line >= lineOf(n) && anchor.line <= lineOf(n, true));
|
|
14438
|
+
return block && {
|
|
14439
|
+
line: anchor.line,
|
|
14440
|
+
blockEndLine: lineOf(block, true),
|
|
14441
|
+
key: `paragraph:${anchor.line}`
|
|
14442
|
+
};
|
|
14443
|
+
})();
|
|
14444
|
+
if (resolved === void 0) return {
|
|
14445
|
+
ok: false,
|
|
14446
|
+
reason: "anchor-not-found"
|
|
14447
|
+
};
|
|
14448
|
+
const result = footnoteAttachEdits(content, {
|
|
14449
|
+
...resolved,
|
|
14450
|
+
endCharacter: (lines[resolved.line] ?? "").length
|
|
14451
|
+
}, text);
|
|
14452
|
+
return result.ok ? {
|
|
14453
|
+
ok: true,
|
|
14454
|
+
edits: result.edits
|
|
14455
|
+
} : result;
|
|
14456
|
+
};
|
|
14457
|
+
/** Ticks or unticks a finding's box; `text` replaces its guidance, and unticking drops it. */
|
|
14458
|
+
const findingsApply = (content, anchor, opts) => {
|
|
14459
|
+
if (anchor.kind !== "chunk") return {
|
|
14460
|
+
ok: false,
|
|
14461
|
+
reason: "anchor-not-found"
|
|
14462
|
+
};
|
|
14463
|
+
const e = parse(content)[anchor.index];
|
|
14464
|
+
if (e?.box === void 0 || e.boxes !== 1) return {
|
|
14465
|
+
ok: false,
|
|
14466
|
+
reason: "anchor-not-found"
|
|
14467
|
+
};
|
|
14468
|
+
const fix = opts.checked ?? e.fix;
|
|
14469
|
+
const guidance = fix ? (opts.text ?? e.guidance).trim() : "";
|
|
14470
|
+
if (fix === e.fix && guidance === e.guidance) return {
|
|
14471
|
+
ok: true,
|
|
14472
|
+
edits: []
|
|
14473
|
+
};
|
|
14474
|
+
const lines = content.split(/\r?\n/);
|
|
14475
|
+
const eol = eolOf(content);
|
|
14476
|
+
const start = lineOf(e.box);
|
|
14477
|
+
const box = `${lines[start].match(/^\s*[-*+]\s+/)[0]}[${fix ? "x" : " "}] fix`;
|
|
14478
|
+
const text = guidance === "" ? box : `${box}${eol}${eol}${guidance.split(/\r?\n/).join(eol)}`;
|
|
14479
|
+
return {
|
|
14480
|
+
ok: true,
|
|
14481
|
+
edits: [{
|
|
14482
|
+
range: {
|
|
14483
|
+
start: {
|
|
14484
|
+
line: start,
|
|
14485
|
+
character: 0
|
|
14486
|
+
},
|
|
14487
|
+
end: lineRange(lines, e.lastLine).end
|
|
14488
|
+
},
|
|
14489
|
+
newText: text
|
|
14490
|
+
}]
|
|
14491
|
+
};
|
|
14492
|
+
};
|
|
14493
|
+
/** The finding gate's file: one `## ` finding each, with one `fix` box and optional guidance below it. */
|
|
14494
|
+
const findingsDescriptor = {
|
|
14495
|
+
sample: `# Quality
|
|
14496
|
+
|
|
14497
|
+
## A finding a lens wrote[^nafind1]
|
|
14498
|
+
|
|
14499
|
+
[^nafind1]:
|
|
14500
|
+
Attached via the phone UI, this note shows a comment on a finding with a
|
|
14501
|
+
\`multi word code span\` that runs past eighty characters in total length.
|
|
14502
|
+
|
|
14503
|
+
What the lens found, and where.
|
|
14504
|
+
|
|
14505
|
+
- [x] fix
|
|
14506
|
+
|
|
14507
|
+
Fix it the simple way.
|
|
14508
|
+
|
|
14509
|
+
# Dropped as noise
|
|
14510
|
+
|
|
14511
|
+
## A finding the judge called noise
|
|
14512
|
+
|
|
14513
|
+
Why the judge dropped it.
|
|
14514
|
+
|
|
14515
|
+
- [ ] fix
|
|
14516
|
+
`,
|
|
14517
|
+
validate: (content) => [...entryFindings(content), ...threadFindings(content)],
|
|
14518
|
+
outline: findingsOutline,
|
|
14519
|
+
actions: (content, range) => threadReplyActions(content, range.start),
|
|
14520
|
+
view: findingsView,
|
|
14521
|
+
annotate: findingsAnnotate,
|
|
14522
|
+
apply: findingsApply,
|
|
14523
|
+
clearTicks: (content) => content,
|
|
14524
|
+
openThreads: openThreadsOf
|
|
14525
|
+
};
|
|
14526
|
+
//#endregion
|
|
14284
14527
|
//#region src/steering/qa.ts
|
|
14285
14528
|
/**
|
|
14286
14529
|
* A marker as plain text. Local to this module because `headingText`/
|
|
@@ -16209,250 +16452,6 @@ const reviewSections = (content) => splitChunks(parseMarkdown(content), content)
|
|
|
16209
16452
|
line: s.headingLine
|
|
16210
16453
|
}));
|
|
16211
16454
|
//#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);
|
|
16225
|
-
};
|
|
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
|
-
};
|
|
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) => {
|
|
16276
|
-
const tree = parseMarkdown(content);
|
|
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;
|
|
16283
|
-
};
|
|
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
|
-
}]
|
|
16420
|
-
};
|
|
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
|
|
16454
|
-
};
|
|
16455
|
-
//#endregion
|
|
16456
16455
|
//#region src/steering/Edits.ts
|
|
16457
16456
|
/** Splices `edits` into `content`, sorted last-to-first so an earlier range's offset is never invalidated by a later edit. Positions are 0-based line/character over `\n`-split lines. */
|
|
16458
16457
|
const applySteeringEdits = (content, edits) => {
|
|
@@ -16601,6 +16600,53 @@ const freeFormFormat = {
|
|
|
16601
16600
|
clearTicks: (content) => content
|
|
16602
16601
|
};
|
|
16603
16602
|
//#endregion
|
|
16603
|
+
//#region src/steering/registry.ts
|
|
16604
|
+
/** 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. */
|
|
16605
|
+
const REGISTRY = /* @__PURE__ */ new Map([
|
|
16606
|
+
["qa", qaDescriptor],
|
|
16607
|
+
["review", reviewDescriptor],
|
|
16608
|
+
["findings", findingsDescriptor]
|
|
16609
|
+
]);
|
|
16610
|
+
/** Every built-in format name, in registry order. */
|
|
16611
|
+
const BUILT_IN_MODE_NAMES = [...REGISTRY.keys()];
|
|
16612
|
+
/** 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. */
|
|
16613
|
+
const steeringFormatFor = (mode) => REGISTRY.get(mode);
|
|
16614
|
+
/**
|
|
16615
|
+
* `qa`-only: every OPEN question in `content` not yet answered, as
|
|
16616
|
+
* `{ question, headingLine }` pairs — enough for a refusal message ("which
|
|
16617
|
+
* question", "where"). Semantically distinct from `validate`'s findings:
|
|
16618
|
+
* a document can be perfectly well-formed (zero findings) while still having
|
|
16619
|
+
* open questions nobody has answered, which is the whole point of the
|
|
16620
|
+
* answer-completeness gate this feeds. Format-parameterised like everything
|
|
16621
|
+
* else here: `review` (or any future format with no such concept) yields
|
|
16622
|
+
* `[]` rather than this being a `qa`-specific function some caller has to
|
|
16623
|
+
* know not to call on the wrong format.
|
|
16624
|
+
*/
|
|
16625
|
+
const unansweredQuestions = (format, content) => format.unansweredQuestions?.(content) ?? [];
|
|
16626
|
+
/** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
|
|
16627
|
+
const openThreads = (format, content) => format.openThreads?.(content) ?? [];
|
|
16628
|
+
//#endregion
|
|
16629
|
+
//#region src/steering/reads.ts
|
|
16630
|
+
/** The top-level `## ` heading texts of markdown `text`. */
|
|
16631
|
+
const sections = (text) => headingSections(text);
|
|
16632
|
+
/** The top-level `## ` sections of markdown `text`, each with its own markdown, its heading line included. */
|
|
16633
|
+
const sectionBodies = (text) => headingSectionBodies(text);
|
|
16634
|
+
/** The `qa`-format questions in `text` still unanswered, each with its heading's 1-based line. */
|
|
16635
|
+
const openQuestions = (text) => unansweredQuestions(qaDescriptor, text).map((q) => ({
|
|
16636
|
+
question: q.question,
|
|
16637
|
+
line: q.headingLine + 1
|
|
16638
|
+
}));
|
|
16639
|
+
/** Every `H:`/`A:` footnote thread in `text`: its 1-based definition line, who it waits on (`"human"` means open) and its syntax faults. */
|
|
16640
|
+
const threads = (text) => {
|
|
16641
|
+
const { threads, findings } = parseThreadsWithFindings(text);
|
|
16642
|
+
return threads.map((t) => ({
|
|
16643
|
+
name: t.name,
|
|
16644
|
+
line: t.line + 1,
|
|
16645
|
+
waitingOn: t.waitingOn,
|
|
16646
|
+
faults: findings.filter((f) => f.message.startsWith(`Footnote thread "[^${t.name}]": `)).map((f) => f.message)
|
|
16647
|
+
}));
|
|
16648
|
+
};
|
|
16649
|
+
//#endregion
|
|
16604
16650
|
//#region src/steering/CodeThreads.ts
|
|
16605
16651
|
const TOKENS = [
|
|
16606
16652
|
"//",
|
|
@@ -16781,47 +16827,4 @@ const stripCodeThreads = (path, text) => {
|
|
|
16781
16827
|
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
16782
16828
|
};
|
|
16783
16829
|
//#endregion
|
|
16784
|
-
|
|
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
|
-
]);
|
|
16791
|
-
/** Every built-in format name, in registry order. */
|
|
16792
|
-
const BUILT_IN_MODE_NAMES = [...REGISTRY.keys()];
|
|
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. */
|
|
16794
|
-
const steeringFormatFor = (mode) => REGISTRY.get(mode);
|
|
16795
|
-
/**
|
|
16796
|
-
* The ui boundary's own resolution: `mode`'s registered format, or
|
|
16797
|
-
* `freeFormFormat` when `mode` is absent or names nothing in `REGISTRY` —
|
|
16798
|
-
* never `undefined`. `resolveMode` (`SteeringMode.ts`) stays on
|
|
16799
|
-
* `steeringFormatFor` alone, so a workflow naming a mode nothing defines
|
|
16800
|
-
* still fails to compile; this fallback is deliberately reachable only from
|
|
16801
|
-
* `src/ui/View.ts` and `src/ui/Write.ts`, where a mode-less/unknown-mode
|
|
16802
|
-
* document must still render and accept a paragraph note, never refuse.
|
|
16803
|
-
*/
|
|
16804
|
-
const steeringFormatOrFreeForm = (mode) => (mode !== void 0 ? REGISTRY.get(mode) : void 0) ?? freeFormFormat;
|
|
16805
|
-
/**
|
|
16806
|
-
* `qa`-only: every OPEN question in `content` not yet answered, as
|
|
16807
|
-
* `{ question, headingLine }` pairs — enough for a refusal message ("which
|
|
16808
|
-
* question", "where"). Semantically distinct from `validate`'s findings:
|
|
16809
|
-
* a document can be perfectly well-formed (zero findings) while still having
|
|
16810
|
-
* open questions nobody has answered, which is the whole point of the
|
|
16811
|
-
* answer-completeness gate this feeds. Format-parameterised like everything
|
|
16812
|
-
* else here: `review` (or any future format with no such concept) yields
|
|
16813
|
-
* `[]` rather than this being a `qa`-specific function some caller has to
|
|
16814
|
-
* know not to call on the wrong format.
|
|
16815
|
-
*/
|
|
16816
|
-
const unansweredQuestions = (format, content) => format.unansweredQuestions?.(content) ?? [];
|
|
16817
|
-
/** Builds `format`'s `SteeringEditorView` of `content` — see `SteeringEditorView`'s own doc for why this is one value, not several. Distinct from `SteeringView` (`SteeringFormat.ts`), which is the format's own node tree the phone client renders. */
|
|
16818
|
-
const viewOf = (format, content) => ({
|
|
16819
|
-
outline: format.outline(content),
|
|
16820
|
-
documentLinks: format.documentLinks ? format.documentLinks(content) : [],
|
|
16821
|
-
pointerAt: (position) => format.pointerAt?.(content, position),
|
|
16822
|
-
actionsAt: (range) => format.actions(content, range)
|
|
16823
|
-
});
|
|
16824
|
-
/** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
|
|
16825
|
-
const openThreads = (format, content) => format.openThreads?.(content) ?? [];
|
|
16826
|
-
//#endregion
|
|
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 };
|
|
16830
|
+
export { parseThreads as A, FREE_TEXT_PLACEHOLDER as C, findingEntries as D, NOISE_HEADING as E, headingSectionBodies as M, formatFinding as N, openThreadFindings as O, unansweredNotes as S, FIX_BOX as T, reviewChunks as _, sections as a, reviewRisks as b, openThreads as c, freeFormFormat as d, applySteeringEdits as f, REVIEW_SECTIONS as g, ANSWER_WONT_FIX as h, sectionBodies as i, parseThreadsWithFindings as j, parseFootnotes as k, steeringFormatFor as l, ANSWER_LEAD as m, stripCodeThreads as n, threads as o, ANSWER_FIX as p, openQuestions as r, BUILT_IN_MODE_NAMES as s, parseCodeThreads as t, unansweredQuestions as u, reviewDecisions as v, isAnswered as w, reviewSections as x, reviewNotes as y };
|