@pmelab/gtd 59.0.1 → 60.1.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.
Files changed (108) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +48 -32
  3. package/claude/hooks/doors.ts +8 -4
  4. package/dist/chunks/flows-DENt0uWV.mjs +707 -0
  5. package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
  6. package/dist/chunks/testkit-BhTey1Do.mjs +30517 -0
  7. package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +11285 -19864
  10. package/dist/gtd.core.mjs +593 -0
  11. package/dist/steering.mjs +2 -2
  12. package/dist/testkit/gherkin.mjs +246 -37
  13. package/dist/testkit.mjs +3 -2
  14. package/dist/web/main.js +234 -232
  15. package/dist/wire.mjs +2 -2
  16. package/dist/workflow-kit.mjs +3 -3
  17. package/package.json +7 -7
  18. package/skills/authoring/SKILL.md +66 -60
  19. package/src/cli/Cli.test.ts +185 -184
  20. package/src/cli/Cli.ts +219 -224
  21. package/src/cli/History.ts +9 -4
  22. package/src/cli/Judges.ts +1 -1
  23. package/src/cli/Output.test.ts +35 -0
  24. package/src/cli/Output.ts +34 -3
  25. package/src/cli/Program.test.ts +78 -84
  26. package/src/cli/Program.ts +192 -128
  27. package/src/cli/ToolCommands.ts +2 -2
  28. package/src/edge/Edge.test.ts +72 -62
  29. package/src/edge/Edge.ts +6 -6
  30. package/src/flows/factory.test.ts +14 -0
  31. package/src/flows/factory.ts +22 -5
  32. package/src/flows/helpers.test.ts +35 -0
  33. package/src/flows/helpers.ts +19 -10
  34. package/src/flows/index.test.ts +66 -0
  35. package/src/flows/index.ts +109 -5
  36. package/src/flows/internal.ts +7 -0
  37. package/src/flows/runtime.test.ts +192 -1
  38. package/src/flows/runtime.ts +189 -55
  39. package/src/gherkin/git-control.feature +6 -6
  40. package/src/gherkin/index.test.ts +127 -0
  41. package/src/gherkin/index.ts +370 -51
  42. package/src/gherkin/messages.test.ts +38 -4
  43. package/src/gherkin/messages.ts +21 -4
  44. package/src/gherkin/more-steps.feature +170 -0
  45. package/src/gherkin/scenario-end.feature +101 -0
  46. package/src/gherkin/testkit.feature +83 -7
  47. package/src/install/Install.test.ts +7 -7
  48. package/src/install/Install.ts +8 -8
  49. package/src/install/Version.ts +9 -3
  50. package/src/install/index.ts +1 -1
  51. package/src/platform/CommandRunner.ts +13 -11
  52. package/src/platform/Commentary.ts +2 -2
  53. package/src/platform/Git.test.ts +4 -5
  54. package/src/platform/Git.ts +42 -38
  55. package/src/platform/Host.ts +1 -1
  56. package/src/platform/NodeServices.ts +11 -0
  57. package/src/platform/Workspace.test.ts +4 -5
  58. package/src/platform/Workspace.ts +3 -3
  59. package/src/platform/WorktreeState.test.ts +2 -3
  60. package/src/platform/WorktreeState.ts +1 -1
  61. package/src/platform/index.ts +2 -1
  62. package/src/replay/Replay.test.ts +13 -2
  63. package/src/replay/Replay.ts +19 -30
  64. package/src/replay/judgeDiffShape.test.ts +1 -1
  65. package/src/steering/Footnotes.test.ts +1 -1
  66. package/src/steering/SteeringFormats.test.ts +1 -1
  67. package/src/steering/freeform.test.ts +1 -1
  68. package/src/steering/index.test.ts +27 -1
  69. package/src/steering/index.ts +6 -104
  70. package/src/steering/internal.ts +11 -0
  71. package/src/steering/qa.test.ts +7 -2
  72. package/src/steering/reads.test.ts +42 -0
  73. package/src/steering/reads.ts +43 -0
  74. package/src/steering/registry.ts +93 -0
  75. package/src/steering/review.test.ts +2 -2
  76. package/src/step/planStart.test.ts +18 -16
  77. package/src/step/planStart.ts +12 -12
  78. package/src/testkit/TestRepo.test.ts +245 -7
  79. package/src/testkit/TestRepo.ts +271 -40
  80. package/src/testkit/imports.test.ts +58 -0
  81. package/src/testkit/imports.ts +102 -0
  82. package/src/testkit/index.test.ts +28 -0
  83. package/src/testkit/index.ts +14 -2
  84. package/src/twins/EmittedScriptRecognizer.ts +2 -6
  85. package/src/twins/EmittedScripts.test.ts +5 -3
  86. package/src/twins/GitTiers.ts +6 -7
  87. package/src/twins/InMemRepo.ts +1 -0
  88. package/src/twins/Layers.test.ts +1 -1
  89. package/src/twins/Layers.ts +10 -3
  90. package/src/twins/cliIo.ts +9 -4
  91. package/src/wire/Script.ts +11 -0
  92. package/src/wire/WireSchema.test.ts +3 -0
  93. package/src/wire/golden/1/script.json +4 -0
  94. package/src/wire/index.ts +1 -0
  95. package/src/workflow/SteeringFormats.test.ts +1 -1
  96. package/src/workflow/compile.ts +2 -2
  97. package/src/workflow/discovery.test.ts +27 -10
  98. package/src/workflow/discovery.ts +5 -6
  99. package/src/workflow/index.ts +1 -0
  100. package/src/workflow/load.test.ts +316 -119
  101. package/src/workflow/load.ts +478 -305
  102. package/src/workflow/pin.test.ts +159 -0
  103. package/src/workflow/pin.ts +72 -0
  104. package/src/workflow/vars.ts +3 -3
  105. package/src/workflow-kit/index.test.ts +25 -0
  106. package/src/workflow-kit/index.ts +37 -4
  107. package/src/workflow-kit/review.ts +2 -2
  108. package/dist/chunks/testkit-07Z_ENch.mjs +0 -32175
@@ -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
- //#region src/steering/index.ts
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 };