@open-tomato/rafa 0.44.0 → 0.46.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/README.md +280 -42
- package/dist/PROMPT.md +18 -1
- package/dist/adapters/logger/settings.d.ts +132 -0
- package/dist/bugs/closest.d.ts +19 -0
- package/dist/bugs/fenced.d.ts +8 -0
- package/dist/bugs/places.d.ts +20 -0
- package/dist/bugs/record.d.ts +145 -0
- package/dist/bugs/title.d.ts +83 -0
- package/dist/bundled/agents/doc-updater.md +17 -3
- package/dist/bundled/agents/loop-implementer.md +14 -2
- package/dist/bundled/operators/skills/rafa-stretch-pit-stop/SKILL.md +10 -3
- package/dist/bundled/operators/skills/rafa-stretch-sweep/SKILL.md +5 -0
- package/dist/bundled/skills/dev-planner/SKILL.md +120 -24
- package/dist/bundled/skills/documentation/SKILL.md +2 -12
- package/dist/cli.js +72387 -59411
- package/dist/config/entry.d.ts +119 -0
- package/dist/config/index.d.ts +36 -0
- package/dist/config/index.js +51 -0
- package/dist/config/shape.d.ts +232 -0
- package/dist/config-registry.d.ts +127 -0
- package/dist/config-schema-board-project.d.ts +157 -0
- package/dist/config-schema-bugs.d.ts +71 -0
- package/dist/config-schema-errors.d.ts +54 -0
- package/dist/config-schema-features.d.ts +64 -0
- package/dist/config-schema-hub.d.ts +150 -0
- package/dist/config-schema-issues.d.ts +95 -0
- package/dist/config-schema-logger.d.ts +91 -0
- package/dist/config-schema-loop-continue.d.ts +122 -0
- package/dist/config-schema-loop-retries.d.ts +94 -0
- package/dist/config-schema-release.d.ts +307 -0
- package/dist/config-schema-tests.d.ts +170 -0
- package/dist/config-schema-triage.d.ts +142 -0
- package/dist/config-schema-wrap-up.d.ts +96 -0
- package/dist/core/command/command.d.ts +228 -0
- package/dist/core/command/confirm.d.ts +67 -0
- package/dist/core/command/index.d.ts +112 -0
- package/dist/core/command/lazy.d.ts +54 -0
- package/dist/core/command/registry-types.d.ts +96 -0
- package/dist/core/command/running.d.ts +96 -0
- package/dist/core/command/spends.d.ts +39 -0
- package/dist/core/command/types.d.ts +92 -0
- package/dist/core/command/version.d.ts +4 -0
- package/dist/core/config/branch-template.d.ts +20 -0
- package/dist/core/config/config-items.d.ts +86 -0
- package/dist/core/config/config-load.d.ts +47 -0
- package/dist/core/config/config-origins.d.ts +57 -0
- package/dist/core/config/config-readers.d.ts +167 -0
- package/dist/core/config/config-schema.d.ts +207 -0
- package/dist/core/config/config-sections.d.ts +493 -0
- package/dist/core/config/config.d.ts +320 -0
- package/dist/core/config/core-section.d.ts +39 -0
- package/dist/core/config/dangerous.d.ts +58 -0
- package/dist/core/config/default-routing.d.ts +16 -0
- package/dist/core/config/index.d.ts +141 -0
- package/dist/core/config/scope.d.ts +42 -0
- package/dist/core/config/section.d.ts +206 -0
- package/dist/core/config/sections.d.ts +45 -0
- package/dist/core/config/standard-schema.d.ts +27 -0
- package/dist/core/git/commit.d.ts +232 -0
- package/dist/core/git/gh-runner.d.ts +24 -0
- package/dist/core/git/git.d.ts +22 -0
- package/dist/core/git/index.d.ts +85 -0
- package/dist/core/loop/claude.d.ts +388 -0
- package/dist/core/loop/hop-record.d.ts +52 -0
- package/dist/core/loop/index.d.ts +179 -0
- package/dist/core/loop/session-env.d.ts +20 -0
- package/dist/core/loop/session-record-parse.d.ts +63 -0
- package/dist/core/loop/sessions.d.ts +408 -0
- package/dist/core/loop/suite-failure.d.ts +25 -0
- package/dist/core/output/active.d.ts +100 -0
- package/dist/core/output/stream.d.ts +20 -0
- package/dist/core/output/text.d.ts +55 -0
- package/dist/core/plan/block-reader.d.ts +46 -0
- package/dist/core/plan/blocks.d.ts +139 -0
- package/dist/core/plan/declaration.d.ts +366 -0
- package/dist/core/plan/plan-stamp.d.ts +75 -0
- package/dist/core/runtime/identity.d.ts +21 -0
- package/dist/core/runtime/index.d.ts +46 -0
- package/dist/effort/store/bring-forward.d.ts +131 -0
- package/dist/effort/store/check-rows.d.ts +31 -0
- package/dist/effort/store/commit-stats.d.ts +42 -0
- package/dist/effort/store/copy.d.ts +35 -0
- package/dist/effort/store/development-build.d.ts +105 -0
- package/dist/effort/store/effort-levels.d.ts +13 -0
- package/dist/effort/store/findings.d.ts +214 -0
- package/dist/effort/store/fix-schema.d.ts +184 -0
- package/dist/effort/store/index.js +10 -7
- package/dist/effort/store/location.d.ts +44 -0
- package/dist/effort/store/merge-commit-gaps.d.ts +68 -0
- package/dist/effort/store/merge-conflicts.d.ts +91 -0
- package/dist/effort/store/merge-rules.d.ts +82 -0
- package/dist/effort/store/merge-store.d.ts +211 -0
- package/dist/effort/store/merge-union.d.ts +92 -0
- package/dist/effort/store/migrate.d.ts +146 -0
- package/dist/effort/store/migrations.d.ts +92 -0
- package/dist/effort/store/ndjson-files.d.ts +114 -0
- package/dist/effort/store/origins.d.ts +116 -0
- package/dist/effort/store/plan-stubs.d.ts +79 -0
- package/dist/effort/store/rebuild-aside.d.ts +172 -0
- package/dist/effort/store/schema-plan.d.ts +102 -0
- package/dist/effort/store/session-row-types.d.ts +64 -0
- package/dist/effort/store/settings.d.ts +13 -0
- package/dist/effort/store/sqlite.d.ts +340 -0
- package/dist/effort/store/store-generation.d.ts +17 -0
- package/dist/effort/store/store-identity.d.ts +220 -0
- package/dist/effort/store/store-meta.d.ts +305 -0
- package/dist/effort/store/store-replaced.d.ts +72 -0
- package/dist/effort/store/types.d.ts +223 -0
- package/dist/errors/codes.d.ts +56 -0
- package/dist/errors/rafa-codes.d.ts +36 -0
- package/dist/index-2a7nb3an.js +125 -0
- package/dist/{index-57mmzkcn.js → index-39kbcw68.js} +516 -217
- package/dist/{index-nv34ptht.js → index-77ajfffn.js} +14 -9
- package/dist/index-cy98y87p.js +2597 -0
- package/dist/index-gqz4jn6j.js +13447 -0
- package/dist/index-gvek4r8m.js +710 -0
- package/dist/index-j4k2vdz3.js +1125 -0
- package/dist/index-jh1r4kcn.js +1067 -0
- package/dist/index-ykggr043.js +540 -0
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index.js +16091 -22798
- package/dist/learning/index.js +5 -6
- package/dist/plan/index.js +12 -11
- package/dist/plan-prompt.md +11 -2
- package/dist/ports/index.d.ts +732 -0
- package/dist/ports/learning.d.ts +101 -0
- package/dist/ports/log-level.d.ts +13 -0
- package/dist/ports/spec-review.d.ts +66 -0
- package/dist/pr/checks.d.ts +133 -0
- package/dist/pr/types.d.ts +422 -0
- package/dist/ready-4f8edezt.js +2016 -0
- package/dist/release-templates/release-bot-fragment.yml +153 -0
- package/dist/release-templates/release-publish.yml +110 -0
- package/dist/release-templates/release-settle.yml +122 -0
- package/dist/release-templates/release-tag.yml +102 -0
- package/dist/report/parse.d.ts +415 -0
- package/dist/schema/project-id.d.ts +79 -0
- package/dist/schema/tiers.d.ts +107 -0
- package/package.json +42 -4
- package/dist/index-6q30dtgm.js +0 -6342
- package/dist/index-cknayrpc.js +0 -2414
- package/dist/index-hf2k497d.js +0 -16
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
activeOutput,
|
|
4
|
-
isStampableStub
|
|
4
|
+
isStampableStub
|
|
5
|
+
} from "./index-2a7nb3an.js";
|
|
6
|
+
import {
|
|
5
7
|
messageOf,
|
|
6
|
-
parseTaskDeclaration
|
|
7
|
-
|
|
8
|
-
} from "./index-cknayrpc.js";
|
|
8
|
+
parseTaskDeclaration
|
|
9
|
+
} from "./index-ykggr043.js";
|
|
9
10
|
// src/preflight/prerequisites-md.ts
|
|
10
11
|
import { basename, dirname, join } from "path";
|
|
11
12
|
var SECTION_AUTO_RE = /\[auto\]/i;
|
|
12
13
|
var SECTION_START_RE = /\[start\]/i;
|
|
13
14
|
var SECTION_HUMAN_RE = /\[human\]|manual|human|sign.?off|team/i;
|
|
15
|
+
var TASKS_MARKER_RE = /\[tasks\]/i;
|
|
16
|
+
var WRAP_UP_MARKER_RE = /\[wrap-up\]/i;
|
|
14
17
|
var ITEM_RE = /^-\s+\[\s*\]\s+/;
|
|
15
|
-
var
|
|
16
|
-
var
|
|
17
|
-
var INLINE_START_RE = /^-\s+\[\s*\]\s+\[start\]\s+/i;
|
|
18
|
+
var SCOPE_MARKERS = String.raw`((?:\[(?:tasks|wrap-up)\]\s+)*)`;
|
|
19
|
+
var ITEM_OPENING_RE = new RegExp(String.raw`^-\s+\[\s*\]\s+${SCOPE_MARKERS}(?:\[(auto|human|start)\]\s+)?${SCOPE_MARKERS}`, "i");
|
|
18
20
|
var PROBE_RE = /:\s+`([^`]+)`\s*$/;
|
|
19
21
|
var HEADING_MARKS_RE = /^#+/;
|
|
20
22
|
var FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
@@ -29,18 +31,29 @@ function inferSectionTag(header) {
|
|
|
29
31
|
return "human";
|
|
30
32
|
return null;
|
|
31
33
|
}
|
|
34
|
+
function markedScope(text) {
|
|
35
|
+
const isForTasks = TASKS_MARKER_RE.test(text);
|
|
36
|
+
const isForWrapUp = WRAP_UP_MARKER_RE.test(text);
|
|
37
|
+
if (isForTasks && isForWrapUp)
|
|
38
|
+
return "both";
|
|
39
|
+
if (isForTasks)
|
|
40
|
+
return "tasks";
|
|
41
|
+
if (isForWrapUp)
|
|
42
|
+
return "wrap-up";
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
32
45
|
function enterHeading(scope, header) {
|
|
33
46
|
const level = HEADING_MARKS_RE.exec(header)?.[0].length ?? 1;
|
|
34
47
|
const above = Array.from({ length: level - 1 }, (_, index) => scope[index] ?? null);
|
|
35
|
-
return [...above, inferSectionTag(header)];
|
|
48
|
+
return [...above, { tag: inferSectionTag(header), scope: markedScope(header) }];
|
|
36
49
|
}
|
|
37
|
-
function
|
|
50
|
+
function sectionMark(scope, mark) {
|
|
38
51
|
for (let index = scope.length - 1;index >= 0; index -= 1) {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
52
|
+
const named = scope[index]?.[mark];
|
|
53
|
+
if (named)
|
|
54
|
+
return named;
|
|
42
55
|
}
|
|
43
|
-
return
|
|
56
|
+
return null;
|
|
44
57
|
}
|
|
45
58
|
function fenceOf(line) {
|
|
46
59
|
return FENCE_RE.exec(line)?.[1] ?? null;
|
|
@@ -61,17 +74,18 @@ function continuationOf(lines, start) {
|
|
|
61
74
|
}
|
|
62
75
|
return following;
|
|
63
76
|
}
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (INLINE_START_RE.test(line))
|
|
68
|
-
return "start";
|
|
69
|
-
if (INLINE_HUMAN_RE.test(line))
|
|
70
|
-
return "human";
|
|
71
|
-
return null;
|
|
77
|
+
function tagNamed(word) {
|
|
78
|
+
const tag = word?.toLowerCase();
|
|
79
|
+
return tag === "auto" || tag === "human" || tag === "start" ? tag : null;
|
|
72
80
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
81
|
+
function readOpening(line) {
|
|
82
|
+
const opening = ITEM_OPENING_RE.exec(line);
|
|
83
|
+
const [matched = "", before = "", tag, after = ""] = opening ?? [];
|
|
84
|
+
return {
|
|
85
|
+
tag: tagNamed(tag),
|
|
86
|
+
scope: markedScope(`${before}${after}`),
|
|
87
|
+
rest: line.slice(matched.length).trim()
|
|
88
|
+
};
|
|
75
89
|
}
|
|
76
90
|
function isProbedTag(tag) {
|
|
77
91
|
return tag === "auto" || tag === "start";
|
|
@@ -80,12 +94,14 @@ function extractProbe(description) {
|
|
|
80
94
|
const probe = PROBE_RE.exec(description)?.[1]?.trim();
|
|
81
95
|
return probe || null;
|
|
82
96
|
}
|
|
83
|
-
function readItem(lines, lineIndex,
|
|
97
|
+
function readItem(lines, lineIndex, headings) {
|
|
84
98
|
const raw = lines[lineIndex] ?? "";
|
|
85
|
-
const
|
|
86
|
-
const
|
|
99
|
+
const opening = readOpening(raw);
|
|
100
|
+
const tag = opening.tag ?? sectionMark(headings, "tag") ?? "human";
|
|
101
|
+
const scope = opening.scope ?? sectionMark(headings, "scope") ?? "both";
|
|
102
|
+
const description = [opening.rest, ...continuationOf(lines, lineIndex + 1)].join(" ");
|
|
87
103
|
const probe = isProbedTag(tag) ? extractProbe(description) : null;
|
|
88
|
-
return Object.freeze({ raw, description, tag, lineIndex, probe });
|
|
104
|
+
return Object.freeze({ raw, description, tag, scope, lineIndex, probe });
|
|
89
105
|
}
|
|
90
106
|
function parsePrerequisites(content) {
|
|
91
107
|
const lines = content.split(`
|
|
@@ -110,7 +126,7 @@ function parsePrerequisites(content) {
|
|
|
110
126
|
continue;
|
|
111
127
|
}
|
|
112
128
|
if (ITEM_RE.test(line))
|
|
113
|
-
items.push(readItem(lines, index,
|
|
129
|
+
items.push(readItem(lines, index, scope));
|
|
114
130
|
}
|
|
115
131
|
return Object.freeze(items);
|
|
116
132
|
}
|
|
@@ -118,7 +134,7 @@ function isProbed(item) {
|
|
|
118
134
|
return isProbedTag(item.tag) && item.probe !== null;
|
|
119
135
|
}
|
|
120
136
|
function requiredItem(item) {
|
|
121
|
-
return Object.freeze({ kind: "tool", name: item.description, probe: item.probe });
|
|
137
|
+
return Object.freeze({ kind: "tool", name: item.description, probe: item.probe, scope: item.scope });
|
|
122
138
|
}
|
|
123
139
|
function requiredItemsTagged(items, tag) {
|
|
124
140
|
const probed = items.filter(isProbed).filter((item) => item.tag === tag);
|
|
@@ -375,13 +391,341 @@ async function runPreflight(items, options) {
|
|
|
375
391
|
knownMissing: Object.freeze(knownMissing)
|
|
376
392
|
});
|
|
377
393
|
}
|
|
378
|
-
// src/
|
|
379
|
-
|
|
394
|
+
// packages/rafa/src/core/plan/blocks.ts
|
|
395
|
+
var RAFA_BLOCK_KINDS = [
|
|
396
|
+
"plan",
|
|
397
|
+
"context",
|
|
398
|
+
"stage-context",
|
|
399
|
+
"report"
|
|
400
|
+
];
|
|
401
|
+
var RAFA_PREFIX = "rafa:";
|
|
402
|
+
var FENCE_RUN = /^( {0,3})(`{3,}|~{3,})/;
|
|
403
|
+
var CLOSING_FENCE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
404
|
+
function isRafaBlockKind(kind) {
|
|
405
|
+
return RAFA_BLOCK_KINDS.includes(kind);
|
|
406
|
+
}
|
|
407
|
+
function linesOf(markdown) {
|
|
408
|
+
const lines = markdown.split(`
|
|
409
|
+
`);
|
|
410
|
+
if (lines[lines.length - 1] === "")
|
|
411
|
+
lines.pop();
|
|
412
|
+
return lines.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
413
|
+
}
|
|
414
|
+
function kindOf(info) {
|
|
415
|
+
const word = info.trim().split(/\s+/)[0] ?? "";
|
|
416
|
+
return word.startsWith(RAFA_PREFIX) ? word.slice(RAFA_PREFIX.length) : null;
|
|
417
|
+
}
|
|
418
|
+
function openingFence(line) {
|
|
419
|
+
const match = FENCE_RUN.exec(line);
|
|
420
|
+
const opener = match?.[0];
|
|
421
|
+
const indent = match?.[1];
|
|
422
|
+
const run = match?.[2];
|
|
423
|
+
if (opener === undefined || indent === undefined || run === undefined) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const marker = run.charAt(0);
|
|
427
|
+
const info = line.slice(opener.length);
|
|
428
|
+
if (marker === "`" && info.includes("`"))
|
|
429
|
+
return null;
|
|
430
|
+
return { marker, length: run.length, indent: indent.length, kind: kindOf(info) };
|
|
431
|
+
}
|
|
432
|
+
function closes(line, fence) {
|
|
433
|
+
const run = CLOSING_FENCE.exec(line)?.[1];
|
|
434
|
+
return run !== undefined && run.charAt(0) === fence.marker && run.length >= fence.length;
|
|
435
|
+
}
|
|
436
|
+
function closingIndex(lines, from, fence) {
|
|
437
|
+
for (let index = from;index < lines.length; index += 1) {
|
|
438
|
+
if (closes(lines[index] ?? "", fence))
|
|
439
|
+
return index;
|
|
440
|
+
}
|
|
441
|
+
return lines.length;
|
|
442
|
+
}
|
|
443
|
+
function dedent(line, indent) {
|
|
444
|
+
let cut = 0;
|
|
445
|
+
while (cut < indent && line.charAt(cut) === " ")
|
|
446
|
+
cut += 1;
|
|
447
|
+
return line.slice(cut);
|
|
448
|
+
}
|
|
449
|
+
function readRafaBlocks(markdown) {
|
|
450
|
+
const lines = linesOf(markdown);
|
|
451
|
+
const blocks = [];
|
|
452
|
+
let index = 0;
|
|
453
|
+
while (index < lines.length) {
|
|
454
|
+
const fence = openingFence(lines[index] ?? "");
|
|
455
|
+
if (fence === null) {
|
|
456
|
+
index += 1;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const closing = closingIndex(lines, index + 1, fence);
|
|
460
|
+
const closed = closing < lines.length;
|
|
461
|
+
if (fence.kind !== null) {
|
|
462
|
+
const body = lines.slice(index + 1, closing).map((line) => dedent(line, fence.indent)).join(`
|
|
463
|
+
`);
|
|
464
|
+
const last = closed ? closing + 1 : lines.length;
|
|
465
|
+
blocks.push({ kind: fence.kind, body, span: { first: index + 1, last }, closed });
|
|
466
|
+
}
|
|
467
|
+
index = closing + 1;
|
|
468
|
+
}
|
|
469
|
+
return blocks;
|
|
470
|
+
}
|
|
471
|
+
// packages/rafa/src/report/parse.ts
|
|
472
|
+
var REPORT_STATUSES = ["done", "blocked"];
|
|
473
|
+
var FINDING_KINDS = ["gotcha", "pattern", "location", "skill-suggestion"];
|
|
474
|
+
var FINDING_SIGNALS = ["loud", "silent"];
|
|
475
|
+
var FINDING_DOMAINS = [
|
|
476
|
+
"code-style",
|
|
477
|
+
"testing",
|
|
478
|
+
"git",
|
|
479
|
+
"debugging",
|
|
480
|
+
"workflow",
|
|
481
|
+
"security",
|
|
482
|
+
"general-best-practices"
|
|
483
|
+
];
|
|
484
|
+
var CHANGE_LEVELS = ["patch", "minor", "major", "none"];
|
|
485
|
+
var BUG_KINDS = ["bug", "own-fallout", "own-gap", "convention", "docs", "machine"];
|
|
486
|
+
var DEFAULT_BUG_KIND = "bug";
|
|
487
|
+
var REPORT_KIND = "report";
|
|
488
|
+
var REQUIRED = true;
|
|
489
|
+
var OPTIONAL = false;
|
|
490
|
+
var REPORT_KEYS = [
|
|
491
|
+
"status",
|
|
492
|
+
"feedback",
|
|
493
|
+
"findings",
|
|
494
|
+
"skills_used",
|
|
495
|
+
"blockers",
|
|
496
|
+
"out_of_scope_bugs",
|
|
497
|
+
"changes",
|
|
498
|
+
"stale_docs"
|
|
499
|
+
];
|
|
500
|
+
var FINDING_KEYS = [
|
|
501
|
+
"trigger",
|
|
502
|
+
"kind",
|
|
503
|
+
"what",
|
|
504
|
+
"cause",
|
|
505
|
+
"resolution",
|
|
506
|
+
"artifact",
|
|
507
|
+
"signal",
|
|
508
|
+
"domain"
|
|
509
|
+
];
|
|
510
|
+
var BLOCKER_KEYS = ["what", "artifact"];
|
|
511
|
+
var BUG_KEYS = ["what", "artifact", "security", "kind", "module", "action", "reproduce", "cause"];
|
|
512
|
+
var CHANGE_KEYS = ["level", "area", "summary"];
|
|
513
|
+
var STALE_DOC_KEYS = ["where", "what"];
|
|
514
|
+
function isMapping(value) {
|
|
515
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
516
|
+
}
|
|
517
|
+
function isOneOf(choices, value) {
|
|
518
|
+
return choices.includes(value);
|
|
519
|
+
}
|
|
520
|
+
function describeValue(value) {
|
|
521
|
+
if (typeof value === "string")
|
|
522
|
+
return JSON.stringify(value);
|
|
523
|
+
if (value === null)
|
|
524
|
+
return "nothing";
|
|
525
|
+
if (Array.isArray(value))
|
|
526
|
+
return "a list";
|
|
527
|
+
if (typeof value === "object")
|
|
528
|
+
return "a mapping";
|
|
529
|
+
return `the ${typeof value} ${String(value)}`;
|
|
530
|
+
}
|
|
531
|
+
function messageOf2(error) {
|
|
532
|
+
return error instanceof Error ? error.message : String(error);
|
|
533
|
+
}
|
|
534
|
+
function reportIssue(reason, field, text) {
|
|
535
|
+
return { reason, field, text };
|
|
536
|
+
}
|
|
537
|
+
function absent(reason, block, text) {
|
|
538
|
+
return { present: false, reason, block, text };
|
|
539
|
+
}
|
|
540
|
+
function scopeOf(mapping, keys, prefix, issues) {
|
|
541
|
+
const fields = new Map;
|
|
542
|
+
const extras = [];
|
|
543
|
+
for (const [key, value] of Object.entries(mapping)) {
|
|
544
|
+
if (keys.includes(key))
|
|
545
|
+
fields.set(key, value);
|
|
546
|
+
else
|
|
547
|
+
extras.push({ key, value });
|
|
548
|
+
}
|
|
549
|
+
return { fields, extras, prefix, issues };
|
|
550
|
+
}
|
|
551
|
+
function valueOf(scope, key, required) {
|
|
552
|
+
const value = scope.fields.get(key) ?? null;
|
|
553
|
+
if (value === null && required) {
|
|
554
|
+
const field = `${scope.prefix}${key}`;
|
|
555
|
+
scope.issues.push(reportIssue("missing-field", field, `${field} is missing`));
|
|
556
|
+
}
|
|
557
|
+
return value;
|
|
558
|
+
}
|
|
559
|
+
function unusable(scope, key, why) {
|
|
560
|
+
const field = `${scope.prefix}${key}`;
|
|
561
|
+
scope.issues.push(reportIssue("unusable-field", field, `${field} ${why}`));
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
function stringProblem(value) {
|
|
565
|
+
if (typeof value !== "string")
|
|
566
|
+
return `is ${describeValue(value)}, not a string; quote it`;
|
|
567
|
+
if (value.trim().length === 0)
|
|
568
|
+
return "is blank";
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
function stringField(scope, key, required) {
|
|
572
|
+
const value = valueOf(scope, key, required);
|
|
573
|
+
if (value === null)
|
|
574
|
+
return null;
|
|
575
|
+
const problem = stringProblem(value);
|
|
576
|
+
return problem === null ? value : unusable(scope, key, problem);
|
|
577
|
+
}
|
|
578
|
+
function choiceField(scope, key, choices, required) {
|
|
579
|
+
const value = valueOf(scope, key, required);
|
|
580
|
+
if (value === null || isOneOf(choices, value))
|
|
581
|
+
return value;
|
|
582
|
+
return unusable(scope, key, `is ${describeValue(value)}, not one of ${choices.join(", ")}`);
|
|
583
|
+
}
|
|
584
|
+
function booleanField(scope, key, required) {
|
|
585
|
+
const value = valueOf(scope, key, required);
|
|
586
|
+
if (value === null || typeof value === "boolean")
|
|
587
|
+
return value;
|
|
588
|
+
return unusable(scope, key, `is ${describeValue(value)}, not true or false; write it unquoted`);
|
|
589
|
+
}
|
|
590
|
+
function listField(scope, key, readEntry) {
|
|
591
|
+
const value = valueOf(scope, key, OPTIONAL);
|
|
592
|
+
if (value === null)
|
|
593
|
+
return [];
|
|
594
|
+
if (!Array.isArray(value)) {
|
|
595
|
+
unusable(scope, key, `is ${describeValue(value)}, not a list`);
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
const entries = [];
|
|
599
|
+
const items = value;
|
|
600
|
+
for (const [index, item] of items.entries()) {
|
|
601
|
+
const entry = readEntry(item, `${scope.prefix}${key}[${index}]`, scope.issues);
|
|
602
|
+
if (entry !== null)
|
|
603
|
+
entries.push(entry);
|
|
604
|
+
}
|
|
605
|
+
return entries;
|
|
606
|
+
}
|
|
607
|
+
function entryScope(item, field, keys, issues) {
|
|
608
|
+
if (isMapping(item))
|
|
609
|
+
return scopeOf(item, keys, `${field}.`, issues);
|
|
610
|
+
const text = `${field} is ${describeValue(item)}, not a mapping of ${keys.join(", ")}; dropped`;
|
|
611
|
+
issues.push(reportIssue("unusable-field", field, text));
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
function readSkill(item, field, issues) {
|
|
615
|
+
const problem = stringProblem(item);
|
|
616
|
+
if (problem === null)
|
|
617
|
+
return item;
|
|
618
|
+
issues.push(reportIssue("unusable-field", field, `${field} ${problem}; dropped`));
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
function readFinding(item, field, issues) {
|
|
622
|
+
const scope = entryScope(item, field, FINDING_KEYS, issues);
|
|
623
|
+
if (scope === null)
|
|
624
|
+
return null;
|
|
625
|
+
return {
|
|
626
|
+
trigger: stringField(scope, "trigger", REQUIRED),
|
|
627
|
+
kind: choiceField(scope, "kind", FINDING_KINDS, REQUIRED),
|
|
628
|
+
what: stringField(scope, "what", REQUIRED),
|
|
629
|
+
cause: stringField(scope, "cause", OPTIONAL),
|
|
630
|
+
resolution: stringField(scope, "resolution", OPTIONAL),
|
|
631
|
+
artifact: stringField(scope, "artifact", OPTIONAL),
|
|
632
|
+
signal: choiceField(scope, "signal", FINDING_SIGNALS, REQUIRED),
|
|
633
|
+
domain: choiceField(scope, "domain", FINDING_DOMAINS, OPTIONAL),
|
|
634
|
+
extras: scope.extras
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function readBlocker(item, field, issues) {
|
|
638
|
+
const scope = entryScope(item, field, BLOCKER_KEYS, issues);
|
|
639
|
+
if (scope === null)
|
|
640
|
+
return null;
|
|
641
|
+
return {
|
|
642
|
+
what: stringField(scope, "what", REQUIRED),
|
|
643
|
+
artifact: stringField(scope, "artifact", OPTIONAL),
|
|
644
|
+
extras: scope.extras
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function readBug(item, field, issues) {
|
|
648
|
+
const scope = entryScope(item, field, BUG_KEYS, issues);
|
|
649
|
+
if (scope === null)
|
|
650
|
+
return null;
|
|
651
|
+
return {
|
|
652
|
+
what: stringField(scope, "what", REQUIRED),
|
|
653
|
+
artifact: stringField(scope, "artifact", OPTIONAL),
|
|
654
|
+
security: booleanField(scope, "security", REQUIRED),
|
|
655
|
+
kind: choiceField(scope, "kind", BUG_KINDS, OPTIONAL) ?? DEFAULT_BUG_KIND,
|
|
656
|
+
module: stringField(scope, "module", OPTIONAL),
|
|
657
|
+
action: stringField(scope, "action", OPTIONAL),
|
|
658
|
+
reproduce: stringField(scope, "reproduce", OPTIONAL),
|
|
659
|
+
cause: stringField(scope, "cause", OPTIONAL),
|
|
660
|
+
extras: scope.extras
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function readChange(item, field, issues) {
|
|
664
|
+
const scope = entryScope(item, field, CHANGE_KEYS, issues);
|
|
665
|
+
if (scope === null)
|
|
666
|
+
return null;
|
|
667
|
+
return {
|
|
668
|
+
level: choiceField(scope, "level", CHANGE_LEVELS, REQUIRED),
|
|
669
|
+
area: stringField(scope, "area", OPTIONAL),
|
|
670
|
+
summary: stringField(scope, "summary", REQUIRED),
|
|
671
|
+
extras: scope.extras
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function readStaleDoc(item, field, issues) {
|
|
675
|
+
const scope = entryScope(item, field, STALE_DOC_KEYS, issues);
|
|
676
|
+
if (scope === null)
|
|
677
|
+
return null;
|
|
678
|
+
const where = stringField(scope, "where", OPTIONAL);
|
|
679
|
+
const what = stringField(scope, "what", REQUIRED);
|
|
680
|
+
if (what === null)
|
|
681
|
+
return null;
|
|
682
|
+
return { where, what, extras: scope.extras };
|
|
683
|
+
}
|
|
684
|
+
function readReport(document, issues) {
|
|
685
|
+
const scope = scopeOf(document, REPORT_KEYS, "", issues);
|
|
686
|
+
return {
|
|
687
|
+
status: choiceField(scope, "status", REPORT_STATUSES, REQUIRED),
|
|
688
|
+
feedback: stringField(scope, "feedback", OPTIONAL),
|
|
689
|
+
findings: listField(scope, "findings", readFinding),
|
|
690
|
+
skillsUsed: listField(scope, "skills_used", readSkill),
|
|
691
|
+
blockers: listField(scope, "blockers", readBlocker),
|
|
692
|
+
outOfScopeBugs: listField(scope, "out_of_scope_bugs", readBug),
|
|
693
|
+
changes: listField(scope, "changes", readChange),
|
|
694
|
+
staleDocs: listField(scope, "stale_docs", readStaleDoc),
|
|
695
|
+
extras: scope.extras
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function parseReport(output) {
|
|
699
|
+
const reports = readRafaBlocks(output).filter((block2) => block2.kind === REPORT_KIND);
|
|
700
|
+
const block = reports.at(-1);
|
|
701
|
+
if (block === undefined) {
|
|
702
|
+
return absent("no-block", null, "the session output holds no rafa:report block");
|
|
703
|
+
}
|
|
704
|
+
const line = block.span.first;
|
|
705
|
+
if (!block.closed) {
|
|
706
|
+
return absent("unclosed-block", block, `rafa:report block at line ${line} is never closed, so its body may have been cut short ` + "and is not read");
|
|
707
|
+
}
|
|
708
|
+
let document;
|
|
709
|
+
try {
|
|
710
|
+
document = Bun.YAML.parse(block.body);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
const text = `rafa:report block at line ${line} is not valid YAML (${messageOf2(error)})`;
|
|
713
|
+
return absent("malformed-block", block, text);
|
|
714
|
+
}
|
|
715
|
+
if (!isMapping(document)) {
|
|
716
|
+
const text = `rafa:report block at line ${line} holds ${describeValue(document)}, ` + "not a mapping of report fields";
|
|
717
|
+
return absent("malformed-block", block, text);
|
|
718
|
+
}
|
|
719
|
+
const issues = [];
|
|
720
|
+
const report = readReport(document, issues);
|
|
721
|
+
return { present: true, report, block, issues };
|
|
722
|
+
}
|
|
723
|
+
// packages/rafa/src/core/plan/blocker-comment.ts
|
|
380
724
|
var BLOCKER_OPEN = "<!-- blocked: ";
|
|
725
|
+
var REPAIRS_OPEN = "<!-- repairs: ";
|
|
381
726
|
var BLOCKER_CLOSE = " -->";
|
|
382
727
|
var BLOCKER_GAP = " ";
|
|
383
|
-
var
|
|
384
|
-
var OPEN_TASK_PREFIX = /^- \[(?: |BLOCKED)\] /;
|
|
728
|
+
var TRAILING_COMMENT = /\s<!-- (blocked|repairs): ((?:(?!<!--|-->).)*) -->$/;
|
|
385
729
|
var ESCAPE_SEQUENCE = /\\(u[0-9A-Fa-f]{4}|[\s\S])/g;
|
|
386
730
|
var TAB = 9;
|
|
387
731
|
var FIRST_PRINTABLE = 32;
|
|
@@ -418,163 +762,45 @@ function unescapeBlockerText(escaped) {
|
|
|
418
762
|
function blockerComment(blocker) {
|
|
419
763
|
return `${BLOCKER_OPEN}${escapeBlockerText(blocker)}${BLOCKER_CLOSE}`;
|
|
420
764
|
}
|
|
421
|
-
function
|
|
765
|
+
function repairsComment(repaired) {
|
|
766
|
+
return `${REPAIRS_OPEN}${escapeBlockerText(repaired)}${BLOCKER_CLOSE}`;
|
|
767
|
+
}
|
|
768
|
+
function findTrailingComment(text) {
|
|
422
769
|
const trimmed = text.trimEnd();
|
|
423
|
-
const match =
|
|
424
|
-
const
|
|
425
|
-
|
|
770
|
+
const match = TRAILING_COMMENT.exec(trimmed);
|
|
771
|
+
const kind = match?.[1];
|
|
772
|
+
const escaped = match?.[2];
|
|
773
|
+
if (match === null || escaped === undefined || kind !== "blocked" && kind !== "repairs")
|
|
426
774
|
return null;
|
|
427
775
|
const before = trimmed.slice(0, match.index).trimEnd();
|
|
428
|
-
return before.length === 0 ? null : { before, escaped };
|
|
429
|
-
}
|
|
430
|
-
function
|
|
431
|
-
const
|
|
432
|
-
if (
|
|
433
|
-
return { text: taskText,
|
|
434
|
-
const
|
|
776
|
+
return before.length === 0 ? null : { kind, before, escaped };
|
|
777
|
+
}
|
|
778
|
+
function splitTrailingComments(taskText) {
|
|
779
|
+
const last = findTrailingComment(taskText);
|
|
780
|
+
if (last === null)
|
|
781
|
+
return { text: taskText, blocked: null, repairs: null };
|
|
782
|
+
const earlier = findTrailingComment(last.before);
|
|
783
|
+
const found = earlier === null || earlier.kind === last.kind ? [last] : [last, earlier];
|
|
435
784
|
return {
|
|
436
|
-
text: found.before,
|
|
437
|
-
|
|
785
|
+
text: found.at(-1)?.before ?? last.before,
|
|
786
|
+
blocked: found.find((comment) => comment.kind === "blocked")?.escaped ?? null,
|
|
787
|
+
repairs: found.find((comment) => comment.kind === "repairs")?.escaped ?? null
|
|
438
788
|
};
|
|
439
789
|
}
|
|
440
|
-
function
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const prefix = OPEN_TASK_PREFIX.exec(body)?.[0];
|
|
446
|
-
if (prefix === undefined)
|
|
447
|
-
return line;
|
|
448
|
-
const found = findBlockerComment(body.slice(prefix.length));
|
|
449
|
-
return found === null ? line : `${prefix}${found.before}${cr}`;
|
|
450
|
-
}
|
|
451
|
-
function closedBlockLines(trackerContent) {
|
|
452
|
-
const inside = new Set;
|
|
453
|
-
for (const block of readRafaBlocks(trackerContent)) {
|
|
454
|
-
if (!block.closed)
|
|
455
|
-
continue;
|
|
456
|
-
for (let line = block.span.first;line <= block.span.last; line += 1) {
|
|
457
|
-
inside.add(line - 1);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return inside;
|
|
461
|
-
}
|
|
462
|
-
function taskInfoOf(capture, lineNum, status) {
|
|
463
|
-
const { text, blocker } = splitBlockerComment(capture.trim());
|
|
464
|
-
return blocker === null ? { task: text, lineNum, status } : { task: text, lineNum, status, blocker };
|
|
465
|
-
}
|
|
466
|
-
var TASK_LINE_STATUSES = [
|
|
467
|
-
[/^- \[BLOCKED\] (.+)/, "blocked"],
|
|
468
|
-
[/^- \[ \] (.+)/, "unchecked"],
|
|
469
|
-
[/^- \[x\] (.+)/, "done"]
|
|
470
|
-
];
|
|
471
|
-
var NO_LINES = new Set;
|
|
472
|
-
function findNextTask(trackerContent, options = {}) {
|
|
473
|
-
const skip = options.skipLines ?? NO_LINES;
|
|
474
|
-
const open = listOpenTasks(trackerContent).filter((task) => !skip.has(task.lineNum));
|
|
475
|
-
return open.find((task) => task.status === "blocked") ?? open.find((task) => task.status === "unchecked") ?? null;
|
|
476
|
-
}
|
|
477
|
-
function listTrackerTasks(trackerContent) {
|
|
478
|
-
const lines = trackerContent.split(`
|
|
479
|
-
`);
|
|
480
|
-
const inBlock = closedBlockLines(trackerContent);
|
|
481
|
-
const tasks = [];
|
|
482
|
-
for (let i = 0;i < lines.length; i++) {
|
|
483
|
-
if (inBlock.has(i))
|
|
484
|
-
continue;
|
|
485
|
-
for (const [pattern, status] of TASK_LINE_STATUSES) {
|
|
486
|
-
const capture = lines[i].match(pattern)?.[1];
|
|
487
|
-
if (!capture)
|
|
488
|
-
continue;
|
|
489
|
-
tasks.push(taskInfoOf(capture, i, status));
|
|
490
|
-
break;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
return tasks;
|
|
494
|
-
}
|
|
495
|
-
function isOpenTask(task) {
|
|
496
|
-
return task.status !== "done";
|
|
790
|
+
function commentText(escaped) {
|
|
791
|
+
if (escaped === null)
|
|
792
|
+
return null;
|
|
793
|
+
const text = unescapeBlockerText(escaped);
|
|
794
|
+
return text.trim().length === 0 ? null : text;
|
|
497
795
|
}
|
|
498
|
-
function
|
|
499
|
-
return
|
|
796
|
+
function keptRepairs(escaped) {
|
|
797
|
+
return escaped === null ? "" : `${BLOCKER_GAP}${REPAIRS_OPEN}${escaped}${BLOCKER_CLOSE}`;
|
|
500
798
|
}
|
|
501
|
-
function
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
const line = lines[lineNum];
|
|
505
|
-
if (!line)
|
|
506
|
-
return;
|
|
507
|
-
if (newStatus === "done") {
|
|
508
|
-
lines[lineNum] = withoutBlockerComment(line).replace(/^- \[ \]/, "- [x]").replace(/^- \[BLOCKED\]/, "- [x]");
|
|
509
|
-
} else {
|
|
510
|
-
lines[lineNum] = line.replace(/^- \[ \]/, "- [BLOCKED]");
|
|
511
|
-
}
|
|
512
|
-
fs.writeFileSync(trackerPath, lines.join(`
|
|
513
|
-
`), "utf8");
|
|
514
|
-
}
|
|
515
|
-
function writeTrackerBlocker(trackerPath, lineNum, blocker) {
|
|
516
|
-
const lines = fs.readFileSync(trackerPath, "utf8").split(`
|
|
517
|
-
`);
|
|
518
|
-
const line = lines[lineNum];
|
|
519
|
-
if (line === undefined)
|
|
520
|
-
return false;
|
|
521
|
-
const { body, cr } = splitCarriageReturn(line);
|
|
522
|
-
const prefix = OPEN_TASK_PREFIX.exec(body)?.[0];
|
|
523
|
-
if (prefix === undefined)
|
|
524
|
-
return false;
|
|
525
|
-
const rest = body.slice(prefix.length);
|
|
526
|
-
const text = (findBlockerComment(rest)?.before ?? rest).trimEnd();
|
|
527
|
-
if (text.trim().length === 0)
|
|
528
|
-
return false;
|
|
529
|
-
const comment = blocker.trim().length === 0 ? "" : `${BLOCKER_GAP}${blockerComment(blocker)}`;
|
|
530
|
-
lines[lineNum] = `- [BLOCKED] ${text}${comment}${cr}`;
|
|
531
|
-
fs.writeFileSync(trackerPath, lines.join(`
|
|
532
|
-
`), "utf8");
|
|
533
|
-
return true;
|
|
534
|
-
}
|
|
535
|
-
var ANY_TASK_PREFIX = /^- \[(?: |x|BLOCKED)\] /;
|
|
536
|
-
var LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
|
|
537
|
-
function assertInsertable(entry) {
|
|
538
|
-
if (entry.task.trim().length === 0 || LINE_TERMINATOR.test(entry.task)) {
|
|
539
|
-
throw new Error("insertTrackerTask: the task text must be one line and not blank");
|
|
540
|
-
}
|
|
541
|
-
if (entry.declaration.trim().length === 0 || /[{}]/.test(entry.declaration) || LINE_TERMINATOR.test(entry.declaration)) {
|
|
542
|
-
throw new Error("insertTrackerTask: the declaration must be one line of entries holding no brace");
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
function endOfChecklist(lines, inBlock) {
|
|
546
|
-
for (let i = lines.length - 1;i >= 0; i -= 1) {
|
|
547
|
-
if (!inBlock.has(i) && ANY_TASK_PREFIX.test(lines[i]))
|
|
548
|
-
return i + 1;
|
|
549
|
-
}
|
|
550
|
-
const last = lines.length - 1;
|
|
551
|
-
return lines[last] === "" ? last : lines.length;
|
|
552
|
-
}
|
|
553
|
-
function insertTrackerTask(trackerPath, entry, beforeLine) {
|
|
554
|
-
assertInsertable(entry);
|
|
555
|
-
const content = fs.readFileSync(trackerPath, "utf8");
|
|
556
|
-
const lines = content.split(`
|
|
557
|
-
`);
|
|
558
|
-
const inBlock = closedBlockLines(content);
|
|
559
|
-
if (beforeLine !== null) {
|
|
560
|
-
const target = lines[beforeLine];
|
|
561
|
-
if (target === undefined || inBlock.has(beforeLine) || !ANY_TASK_PREFIX.test(target)) {
|
|
562
|
-
throw new Error(`insertTrackerTask: line ${beforeLine} is no task line`);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
const at = beforeLine ?? endOfChecklist(lines, inBlock);
|
|
566
|
-
const { cr } = splitCarriageReturn(lines[0] ?? "");
|
|
567
|
-
const line = `- [BLOCKED] ${entry.task.trim()}${BLOCKER_GAP}{${entry.declaration.trim()}}` + `${BLOCKER_GAP}${blockerComment(entry.blocker)}${cr}`;
|
|
568
|
-
const written = [...lines.slice(0, at), line, ...lines.slice(at)];
|
|
569
|
-
fs.writeFileSync(trackerPath, written.join(`
|
|
570
|
-
`), "utf8");
|
|
571
|
-
return at;
|
|
572
|
-
}
|
|
573
|
-
function trackerPathFor(planPath) {
|
|
574
|
-
return planPath.replace(/PLAN(-[^/]*)?\.md$/, (_m, stub) => `PLAN_TRACKER${stub ?? ""}.md`);
|
|
799
|
+
function splitBlockerComment(taskText) {
|
|
800
|
+
const { text, blocked } = splitTrailingComments(taskText);
|
|
801
|
+
return { text, blocker: commentText(blocked) };
|
|
575
802
|
}
|
|
576
|
-
|
|
577
|
-
// src/plan/parse.ts
|
|
803
|
+
// packages/rafa/src/core/plan/parse.ts
|
|
578
804
|
var PLAN_BLOCK_KINDS = ["plan", "context", "stage-context"];
|
|
579
805
|
var PLAN_HEADER_FIELDS = ["stub", "issue", "spec", "release"];
|
|
580
806
|
var PLAN_RELEASE_LEVELS = ["patch", "minor", "major", "none"];
|
|
@@ -593,10 +819,10 @@ function isHeaderField(key) {
|
|
|
593
819
|
function isReleaseLevel(value) {
|
|
594
820
|
return PLAN_RELEASE_LEVELS.includes(value);
|
|
595
821
|
}
|
|
596
|
-
function
|
|
822
|
+
function isMapping2(value) {
|
|
597
823
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
598
824
|
}
|
|
599
|
-
function
|
|
825
|
+
function describeValue2(value) {
|
|
600
826
|
if (typeof value === "string")
|
|
601
827
|
return JSON.stringify(value);
|
|
602
828
|
if (value === null)
|
|
@@ -607,7 +833,7 @@ function describeValue(value) {
|
|
|
607
833
|
return "a mapping";
|
|
608
834
|
return String(value);
|
|
609
835
|
}
|
|
610
|
-
function
|
|
836
|
+
function messageOf3(error) {
|
|
611
837
|
return error instanceof Error ? error.message : String(error);
|
|
612
838
|
}
|
|
613
839
|
function planIssue(reason, line, text) {
|
|
@@ -739,10 +965,10 @@ function unusableText(field, value) {
|
|
|
739
965
|
return `${subject} is empty; a value opening with # after a space is a YAML comment unless quoted`;
|
|
740
966
|
}
|
|
741
967
|
if (field === "release") {
|
|
742
|
-
return `${subject} is ${
|
|
968
|
+
return `${subject} is ${describeValue2(value)}, not one of ${PLAN_RELEASE_LEVELS.join(", ")}`;
|
|
743
969
|
}
|
|
744
970
|
if (typeof value !== "string") {
|
|
745
|
-
return field === "issue" ? `${subject} is ${
|
|
971
|
+
return field === "issue" ? `${subject} is ${describeValue2(value)}, not a string or a whole number; quote it` : `${subject} is ${describeValue2(value)}, not a string; quote it`;
|
|
746
972
|
}
|
|
747
973
|
if (value.trim().length === 0)
|
|
748
974
|
return `${subject} is blank`;
|
|
@@ -786,13 +1012,13 @@ function readHeader(block) {
|
|
|
786
1012
|
try {
|
|
787
1013
|
document = Bun.YAML.parse(block.body);
|
|
788
1014
|
} catch (error) {
|
|
789
|
-
const text = `rafa:plan block is not valid YAML (${
|
|
1015
|
+
const text = `rafa:plan block is not valid YAML (${messageOf3(error)})`;
|
|
790
1016
|
return { header: emptyHeader(), issues: [planIssue("malformed-header", line, text)] };
|
|
791
1017
|
}
|
|
792
1018
|
if (document === null)
|
|
793
1019
|
return { header: emptyHeader(), issues: [] };
|
|
794
|
-
if (!
|
|
795
|
-
const text = `rafa:plan block holds ${
|
|
1020
|
+
if (!isMapping2(document)) {
|
|
1021
|
+
const text = `rafa:plan block holds ${describeValue2(document)}, not a mapping of fields`;
|
|
796
1022
|
return { header: emptyHeader(), issues: [planIssue("malformed-header", line, text)] };
|
|
797
1023
|
}
|
|
798
1024
|
return readFields(document, line);
|
|
@@ -818,7 +1044,42 @@ function parsePlan(markdown) {
|
|
|
818
1044
|
issues
|
|
819
1045
|
};
|
|
820
1046
|
}
|
|
821
|
-
|
|
1047
|
+
// packages/rafa/src/core/plan/plan-files.ts
|
|
1048
|
+
import { statSync } from "fs";
|
|
1049
|
+
import { basename as basename2, dirname as dirname2, join as join2, resolve } from "path";
|
|
1050
|
+
function plansDirAt(root, planDir) {
|
|
1051
|
+
return { label: planDir, path: resolve(root, planDir) };
|
|
1052
|
+
}
|
|
1053
|
+
function countTasks(tasks) {
|
|
1054
|
+
const done = tasks.filter((task) => task.status === "done").length;
|
|
1055
|
+
const blocked = tasks.filter((task) => task.status === "blocked").length;
|
|
1056
|
+
return { total: tasks.length, done, blocked, open: tasks.length - done - blocked };
|
|
1057
|
+
}
|
|
1058
|
+
function planFileName(stub, tracker) {
|
|
1059
|
+
return tracker ? `PLAN_TRACKER-${stub}.md` : `PLAN-${stub}.md`;
|
|
1060
|
+
}
|
|
1061
|
+
function stubOfPlanFile(name) {
|
|
1062
|
+
const stub = /^PLAN-(.+)\.md$/.exec(name)?.[1];
|
|
1063
|
+
return stub !== undefined && isStampableStub(stub) ? stub : null;
|
|
1064
|
+
}
|
|
1065
|
+
function isFile(path) {
|
|
1066
|
+
return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
1067
|
+
}
|
|
1068
|
+
var REJECTED_DIR = "rejected";
|
|
1069
|
+
function rejectedPath(path) {
|
|
1070
|
+
const parent = dirname2(path);
|
|
1071
|
+
return parent === "." ? join2(REJECTED_DIR, basename2(path)) : join2(parent, REJECTED_DIR, basename2(path));
|
|
1072
|
+
}
|
|
1073
|
+
function issueLine(file, issue) {
|
|
1074
|
+
return `${file}:${issue.line}: ${issue.reason}: ${issue.text}`;
|
|
1075
|
+
}
|
|
1076
|
+
function plural(count, noun) {
|
|
1077
|
+
return count === 1 ? `${count} ${noun}` : `${count} ${noun}s`;
|
|
1078
|
+
}
|
|
1079
|
+
// packages/rafa/src/core/plan/tracker-path.ts
|
|
1080
|
+
function trackerPathFor(planPath) {
|
|
1081
|
+
return planPath.replace(/PLAN(-[^/]*)?\.md$/, (_m, stub) => `PLAN_TRACKER${stub ?? ""}.md`);
|
|
1082
|
+
}
|
|
822
1083
|
// src/plan/inject.ts
|
|
823
1084
|
var PLAN_CONTEXT_HEADING = "## Plan context";
|
|
824
1085
|
var STAGE_HEADING_PREFIX = "## Stage: ";
|
|
@@ -830,19 +1091,49 @@ var CHECKBOXES = {
|
|
|
830
1091
|
blocked: "- [BLOCKED]",
|
|
831
1092
|
done: "- [x]"
|
|
832
1093
|
};
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
function
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1094
|
+
var HANDED_OVER = ", so the whole plan is handed over";
|
|
1095
|
+
function placeOf(position) {
|
|
1096
|
+
return position !== undefined && Number.isInteger(position) && position >= 0 ? position : null;
|
|
1097
|
+
}
|
|
1098
|
+
var DISPATCHED_TEXT = "the dispatched text";
|
|
1099
|
+
var REPAIRED_TEXT = "the text the dispatched repair names";
|
|
1100
|
+
function noTaskWithText(place) {
|
|
1101
|
+
const beyond = place === null ? "" : ` and no task number ${place + 1}`;
|
|
1102
|
+
const text = `the plan holds no task with ${DISPATCHED_TEXT}${beyond}${HANDED_OVER}; ` + "the plan may have changed since its tracker was copied, or the task line sits inside a rafa:* block";
|
|
1103
|
+
return { found: false, fallback: { reason: "no-task-with-text", text } };
|
|
1104
|
+
}
|
|
1105
|
+
function severalTasksWithText(count, place, sought) {
|
|
1106
|
+
const which = place === null ? "the dispatch gives no position among its tasks" : `task number ${place + 1} is none of them`;
|
|
1107
|
+
const text = `the plan holds ${count} tasks with ${sought} and ${which}${HANDED_OVER}`;
|
|
1108
|
+
return { found: false, fallback: { reason: "several-tasks-with-text", text } };
|
|
1109
|
+
}
|
|
1110
|
+
function noRepairedTask() {
|
|
1111
|
+
const text = `the plan holds no task with ${REPAIRED_TEXT}${HANDED_OVER}; ` + "the plan may have changed since the repair was written";
|
|
1112
|
+
return { found: false, fallback: { reason: "no-task-with-text", text } };
|
|
1113
|
+
}
|
|
1114
|
+
function locate(model, wanted, position) {
|
|
1115
|
+
const place = placeOf(position);
|
|
1116
|
+
const atPlace = place === null ? undefined : model.tasks[place];
|
|
1117
|
+
const holders = model.tasks.filter((candidate) => candidate.task === wanted.task);
|
|
1118
|
+
const [first] = holders;
|
|
1119
|
+
if (first === undefined) {
|
|
1120
|
+
return atPlace === undefined ? noTaskWithText(place) : { found: true, task: atPlace };
|
|
1121
|
+
}
|
|
1122
|
+
if (holders.length === 1)
|
|
1123
|
+
return { found: true, task: first };
|
|
1124
|
+
return atPlace !== undefined && holders.includes(atPlace) ? { found: true, task: atPlace } : severalTasksWithText(holders.length, place, DISPATCHED_TEXT);
|
|
1125
|
+
}
|
|
1126
|
+
function locateRepaired(model, named, position) {
|
|
1127
|
+
const sentence = named.trim();
|
|
1128
|
+
const holders = model.tasks.filter((candidate) => candidate.text.trim() === sentence);
|
|
1129
|
+
const [first] = holders;
|
|
1130
|
+
if (first === undefined)
|
|
1131
|
+
return noRepairedTask();
|
|
1132
|
+
if (holders.length === 1)
|
|
1133
|
+
return { found: true, task: first };
|
|
1134
|
+
const place = placeOf(position);
|
|
1135
|
+
const atPlace = place === null ? undefined : model.tasks[place];
|
|
1136
|
+
return atPlace !== undefined && holders.includes(atPlace) ? { found: true, task: atPlace } : severalTasksWithText(holders.length, place, REPAIRED_TEXT);
|
|
846
1137
|
}
|
|
847
1138
|
function contextBody(body) {
|
|
848
1139
|
if (body === null || body.trim().length === 0)
|
|
@@ -862,15 +1153,22 @@ function renderDocument(sections) {
|
|
|
862
1153
|
`;
|
|
863
1154
|
}
|
|
864
1155
|
function shownStatus(task, dispatched) {
|
|
865
|
-
if (task.lineNum < dispatched.lineNum)
|
|
1156
|
+
if (task.lineNum < dispatched.task.lineNum)
|
|
866
1157
|
return "done";
|
|
867
|
-
if (task.lineNum
|
|
868
|
-
return
|
|
869
|
-
return
|
|
1158
|
+
if (task.lineNum > dispatched.task.lineNum)
|
|
1159
|
+
return task.status;
|
|
1160
|
+
return dispatched.repair === null ? "unchecked" : "done";
|
|
870
1161
|
}
|
|
871
1162
|
function taskLine(status, task) {
|
|
872
1163
|
return `${CHECKBOXES[status]} ${task.text}`;
|
|
873
1164
|
}
|
|
1165
|
+
function openLine(dispatched) {
|
|
1166
|
+
return taskLine("unchecked", { text: dispatched.repair ?? dispatched.task.text });
|
|
1167
|
+
}
|
|
1168
|
+
function checklistLines(task, dispatched) {
|
|
1169
|
+
const line = taskLine(shownStatus(task, dispatched), task);
|
|
1170
|
+
return task === dispatched.task && dispatched.repair !== null ? [line, openLine(dispatched)] : [line];
|
|
1171
|
+
}
|
|
874
1172
|
function planContextSection(model) {
|
|
875
1173
|
const body = contextBody(model.context);
|
|
876
1174
|
return body === null ? null : section(PLAN_CONTEXT_HEADING, body);
|
|
@@ -894,17 +1192,17 @@ function stageListSection(model, dispatched) {
|
|
|
894
1192
|
`));
|
|
895
1193
|
}
|
|
896
1194
|
function renderStage(model, dispatched) {
|
|
897
|
-
const checklist = model.tasks.filter((task) => task.stage === dispatched.stage).
|
|
1195
|
+
const checklist = model.tasks.filter((task) => task.stage === dispatched.task.stage).flatMap((task) => checklistLines(task, dispatched));
|
|
898
1196
|
return renderDocument([
|
|
899
1197
|
planContextSection(model),
|
|
900
|
-
stageSection(model, dispatched, checklist),
|
|
901
|
-
stageListSection(model, dispatched)
|
|
1198
|
+
stageSection(model, dispatched.task, checklist),
|
|
1199
|
+
stageListSection(model, dispatched.task)
|
|
902
1200
|
]);
|
|
903
1201
|
}
|
|
904
1202
|
function renderTask(model, dispatched) {
|
|
905
1203
|
return renderDocument([
|
|
906
1204
|
planContextSection(model),
|
|
907
|
-
stageSection(model, dispatched, [
|
|
1205
|
+
stageSection(model, dispatched.task, [openLine(dispatched)])
|
|
908
1206
|
]);
|
|
909
1207
|
}
|
|
910
1208
|
var RENDERERS = {
|
|
@@ -912,14 +1210,15 @@ var RENDERERS = {
|
|
|
912
1210
|
task: renderTask
|
|
913
1211
|
};
|
|
914
1212
|
function renderInjection(request) {
|
|
915
|
-
const { mode, plan, task } = request;
|
|
916
|
-
if (mode === "full")
|
|
917
|
-
return { requested: mode, mode, text: plan, fallback: null };
|
|
1213
|
+
const { mode, plan, task, position, repairs } = request;
|
|
1214
|
+
if (mode === "full" || repairs === null)
|
|
1215
|
+
return { requested: mode, mode: "full", text: plan, fallback: null };
|
|
918
1216
|
const model = parsePlan(plan);
|
|
919
|
-
const location = locate(model, task);
|
|
1217
|
+
const location = repairs === undefined ? locate(model, task, position) : locateRepaired(model, repairs, position);
|
|
920
1218
|
if (!location.found) {
|
|
921
1219
|
return { requested: mode, mode: "full", text: plan, fallback: location.fallback };
|
|
922
1220
|
}
|
|
923
|
-
|
|
1221
|
+
const repair = repairs === undefined ? null : parseTaskDeclaration(task.task).text.trim();
|
|
1222
|
+
return { requested: mode, mode, text: RENDERERS[mode](model, { task: location.task, repair }), fallback: null };
|
|
924
1223
|
}
|
|
925
|
-
export {
|
|
1224
|
+
export { RAFA_BLOCK_KINDS, isRafaBlockKind, readRafaBlocks, REPORT_STATUSES, FINDING_KINDS, FINDING_SIGNALS, FINDING_DOMAINS, CHANGE_LEVELS, BUG_KINDS, parseReport, BLOCKER_GAP, escapeBlockerText, unescapeBlockerText, blockerComment, repairsComment, splitTrailingComments, commentText, keptRepairs, splitBlockerComment, PLAN_BLOCK_KINDS, PLAN_HEADER_FIELDS, PLAN_RELEASE_LEVELS, parsePlan, plansDirAt, countTasks, planFileName, stubOfPlanFile, isFile, rejectedPath, issueLine, plural, trackerPathFor, parsePrerequisites, planPrerequisites, malformedPrerequisiteLines, mergePlanPrerequisites, prerequisitesPathForPlan, loadPlanPrerequisites, PROBE_TIMEOUT_MS, runShellProbe, runPreflight, renderInjection };
|