@pmelab/gtd 63.0.0 → 64.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -11
- package/dist/chunks/{flows-DENt0uWV.mjs → flows-B7ZRCe-B.mjs} +1 -1
- package/dist/chunks/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -1
- package/dist/chunks/{steering-DF_BMadH.mjs → steering-Cem5i6Xl.mjs} +422 -2
- package/dist/chunks/{testkit-DwypA7cH.mjs → testkit-DfWZQCWQ.mjs} +1073 -328
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +4372 -1310
- package/dist/gtd.core.mjs +126 -118
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +2 -2
- package/dist/testkit.mjs +2 -2
- package/dist/workflow-kit.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +5 -8
- package/src/cli/Cli.ts +22 -28
- package/src/cli/Output.ts +2 -1
- package/src/cli/Program.test.ts +55 -53
- package/src/cli/Program.ts +34 -34
- package/src/cli/ToolCommands.ts +3 -4
- package/src/edge/Edge.test.ts +38 -12
- package/src/edge/Edge.ts +53 -19
- package/src/emit/SteeringMode.test.ts +4 -2
- package/src/emit/SteeringMode.ts +31 -10
- package/src/flows/index.ts +1 -0
- package/src/flows/runtime.test.ts +2 -1
- package/src/flows/runtime.ts +8 -2
- package/src/gherkin/messages.test.ts +1 -1
- package/src/gherkin/module-config.feature +1 -1
- package/src/gherkin/testkit.feature +2 -2
- package/src/install/Version.ts +14 -13
- package/src/platform/Host.ts +75 -22
- package/src/platform/NodePinFile.ts +44 -0
- package/src/platform/NodeRuntime.ts +19 -0
- package/src/platform/NodeSync.ts +64 -0
- package/src/platform/PinFile.ts +0 -50
- package/src/platform/Sha.ts +121 -0
- package/src/platform/SyncIo.ts +35 -0
- package/src/platform/Workspace.test.ts +8 -3
- package/src/platform/Workspace.ts +82 -123
- package/src/platform/WorktreeState.test.ts +8 -4
- package/src/platform/WorktreeState.ts +21 -19
- package/src/platform/index.ts +18 -5
- package/src/replay/Format.test.ts +16 -13
- package/src/replay/Format.ts +4 -2
- package/src/replay/Replay.test.ts +25 -5
- package/src/replay/Replay.ts +10 -6
- package/src/replay/Trailers.test.ts +10 -10
- package/src/replay/Trailers.ts +23 -1
- package/src/replay/index.ts +1 -0
- package/src/steering/Declared.test.ts +218 -0
- package/src/steering/Declared.ts +607 -0
- package/src/steering/FORMATS.md +3 -0
- package/src/steering/index.ts +12 -0
- package/src/steering/internal.ts +1 -0
- package/src/step/planStart.test.ts +2 -2
- package/src/step/planStep.test.ts +4 -4
- package/src/testkit/TestRepo.test.ts +1 -1
- package/src/testkit/TestRepo.ts +4 -18
- package/src/twins/EmittedScriptRecognizer.ts +5 -3
- package/src/twins/EmittedScripts.test.ts +19 -0
- package/src/twins/Host.test.ts +151 -0
- package/src/twins/Layers.test.ts +2 -2
- package/src/twins/Layers.ts +44 -18
- package/src/twins/Path.test.ts +52 -0
- package/src/twins/SyncIo.test.ts +153 -0
- package/src/twins/SyncIo.ts +159 -0
- package/src/twins/cliIo.ts +8 -1
- package/src/twins/index.ts +5 -0
- package/src/workflow/SteeringFormats.ts +4 -0
- package/src/workflow/Workflow.ts +2 -0
- package/src/workflow/discovery.test.ts +6 -5
- package/src/workflow/discovery.ts +43 -32
- package/src/workflow/fingerprint.test.ts +23 -3
- package/src/workflow/fingerprint.ts +9 -11
- package/src/workflow/index.ts +5 -1
- package/src/workflow/load.test.ts +16 -6
- package/src/workflow/load.ts +107 -69
- package/src/workflow/pin.test.ts +3 -2
- package/src/workflow/pin.ts +17 -11
package/README.md
CHANGED
|
@@ -610,14 +610,14 @@ approvals as `approvals` (`[]` when none), and `gtd history show` prints each as
|
|
|
610
610
|
gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
|
|
611
611
|
[SemVer promise](https://github.com/pmelab/gtd/blob/main/COMPATIBILITY.md).
|
|
612
612
|
|
|
613
|
-
| Import | What it is
|
|
614
|
-
| ----------------------------- |
|
|
615
|
-
| `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), `scope`, `workflow()`, workflow factories, `defineConfig` and `FLOWS_PROTOCOL`.
|
|
616
|
-
| `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document.
|
|
617
|
-
| `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`, `findings`), the review-note readers, and `sections`, `threads` and `openQuestions` as plain functions.
|
|
618
|
-
| `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. See [Workflow kit](https://github.com/pmelab/gtd/blob/main/docs/workflow-kit.md).
|
|
619
|
-
| `@pmelab/gtd/testkit` | Runs any workflow against the real engine in an in-memory repository, under any test runner, plus a flow-context harness for rendering prompts. See [Testing a workflow](https://github.com/pmelab/gtd/blob/main/docs/testing.md).
|
|
620
|
-
| `@pmelab/gtd/testkit/gherkin` | The testkit as quickpickle steps for vitest. Needs `vitest` and `quickpickle`, optional peer dependencies.
|
|
613
|
+
| Import | What it is |
|
|
614
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
615
|
+
| `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), `scope`, `workflow()`, workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
|
|
616
|
+
| `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
|
|
617
|
+
| `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`, `findings`), `listFormat`/`sectionFormat` to [declare a format](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#declared-formats) a step's `mode` validates against, the review-note readers, and `sections`, `threads` and `openQuestions` as plain functions. |
|
|
618
|
+
| `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. See [Workflow kit](https://github.com/pmelab/gtd/blob/main/docs/workflow-kit.md). |
|
|
619
|
+
| `@pmelab/gtd/testkit` | Runs any workflow against the real engine in an in-memory repository, under any test runner, plus a flow-context harness for rendering prompts. See [Testing a workflow](https://github.com/pmelab/gtd/blob/main/docs/testing.md). |
|
|
620
|
+
| `@pmelab/gtd/testkit/gherkin` | The testkit as quickpickle steps for vitest. Needs `vitest` and `quickpickle`, optional peer dependencies. |
|
|
621
621
|
|
|
622
622
|
Types come from the TypeScript sources the package ships. Configs never need an
|
|
623
623
|
install for any of these but the testkit's: gtd resolves a `gtd.config.ts`'s
|
|
@@ -642,7 +642,8 @@ flow has ended, an unpin beat removing it — the commit that closes the process
|
|
|
642
642
|
Both are ordinary `script` beats a driver runs unattended; see
|
|
643
643
|
[Pin and unpin beats](https://github.com/pmelab/gtd/blob/main/docs/driver.md#pin-and-unpin-beats).
|
|
644
644
|
Histories with pin commits are history format 2 or later, which an older gtd
|
|
645
|
-
refuses.
|
|
645
|
+
refuses. The beats rest at `gtd:pin` and `gtd:unpin`; a workflow step may not
|
|
646
|
+
take a name starting with `gtd:`.
|
|
646
647
|
|
|
647
648
|
While a process is pinned, the `gtd` on your `PATH` hands every command over to
|
|
648
649
|
the pin, so editing the workflow or upgrading gtd mid-process changes nothing;
|
|
@@ -654,8 +655,8 @@ and run `gtd repin`: its script commits a fresh pin from the live engine and
|
|
|
654
655
|
config, and the process continues on it. Where the edited workflow no longer
|
|
655
656
|
replays the process, the repin continues from the divergence: the commits it
|
|
656
657
|
cannot replay stay as files, and the process rests at the step the edited
|
|
657
|
-
workflow reached (history format 3). A pin in a format your gtd does
|
|
658
|
-
refused: upgrade gtd. See
|
|
658
|
+
workflow reached (history format 3 or later). A pin in a format your gtd does
|
|
659
|
+
not know is refused: upgrade gtd. See
|
|
659
660
|
[Pinned processes](https://github.com/pmelab/gtd/blob/main/docs/cli.md#pinned-processes).
|
|
660
661
|
|
|
661
662
|
## License
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
//#region src/steering/SteeringFormat.ts
|
|
3
3
|
/** `file:line:col: message` — the shape editors and grep already jump on. Stored 0-based, printed 1-based. A finding with a `line` but no `range`, or none at all, degrades to the shorter forms. */
|
|
4
4
|
const formatFinding = (file, finding) => {
|
|
@@ -16600,6 +16600,426 @@ const freeFormFormat = {
|
|
|
16600
16600
|
clearTicks: (content) => content
|
|
16601
16601
|
};
|
|
16602
16602
|
//#endregion
|
|
16603
|
+
//#region src/steering/Declared.ts
|
|
16604
|
+
const FENCE_RE = /^( *)(`{3,}|~{3,})/;
|
|
16605
|
+
const HEADING_RE = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
|
|
16606
|
+
const ITEM_RE = /^( *)([-*+]|\d{1,9}[.)])(?:[ \t]+(.*))?$/;
|
|
16607
|
+
const CHECKBOX_RE = /^\[([ xX])\](?:[ \t]+|$)/;
|
|
16608
|
+
/** Marks fenced lines; an unclosed fence is a problem rather than swallowing the rest of the document. */
|
|
16609
|
+
const walk = (content) => {
|
|
16610
|
+
const raw = content.split(/\r?\n/);
|
|
16611
|
+
const lines = [];
|
|
16612
|
+
const problems = [];
|
|
16613
|
+
let open;
|
|
16614
|
+
for (const [index, text] of raw.entries()) {
|
|
16615
|
+
const fence = FENCE_RE.exec(text);
|
|
16616
|
+
if (open === void 0) {
|
|
16617
|
+
if (fence !== null) {
|
|
16618
|
+
open = {
|
|
16619
|
+
char: fence[2][0],
|
|
16620
|
+
length: fence[2].length,
|
|
16621
|
+
line: index,
|
|
16622
|
+
indent: fence[1].length
|
|
16623
|
+
};
|
|
16624
|
+
lines.push({
|
|
16625
|
+
index,
|
|
16626
|
+
text,
|
|
16627
|
+
fenced: true,
|
|
16628
|
+
fenceIndent: open.indent,
|
|
16629
|
+
opens: true
|
|
16630
|
+
});
|
|
16631
|
+
} else lines.push({
|
|
16632
|
+
index,
|
|
16633
|
+
text,
|
|
16634
|
+
fenced: false
|
|
16635
|
+
});
|
|
16636
|
+
continue;
|
|
16637
|
+
}
|
|
16638
|
+
lines.push({
|
|
16639
|
+
index,
|
|
16640
|
+
text,
|
|
16641
|
+
fenced: true,
|
|
16642
|
+
fenceIndent: open.indent
|
|
16643
|
+
});
|
|
16644
|
+
if (fence !== null && fence[2][0] === open.char && fence[2].length >= open.length && text.slice(fence[0].length).trim() === "") open = void 0;
|
|
16645
|
+
}
|
|
16646
|
+
if (open !== void 0) problems.push({
|
|
16647
|
+
line: open.line,
|
|
16648
|
+
message: "code fence is never closed"
|
|
16649
|
+
});
|
|
16650
|
+
return {
|
|
16651
|
+
lines,
|
|
16652
|
+
problems
|
|
16653
|
+
};
|
|
16654
|
+
};
|
|
16655
|
+
const headingOf = (line) => {
|
|
16656
|
+
if (line.fenced) return void 0;
|
|
16657
|
+
const match = HEADING_RE.exec(line.text);
|
|
16658
|
+
return match === null ? void 0 : {
|
|
16659
|
+
line: line.index,
|
|
16660
|
+
level: match[1].length,
|
|
16661
|
+
title: (match[2] ?? "").trim()
|
|
16662
|
+
};
|
|
16663
|
+
};
|
|
16664
|
+
const indentOf = (text) => /^ */.exec(text)[0].length;
|
|
16665
|
+
const markerKind = (marker) => marker === "-" || marker === "*" || marker === "+" ? marker : "1.";
|
|
16666
|
+
const markerList = (markers) => markers.map((m) => `\`${m}\``).join(" or ");
|
|
16667
|
+
const titleMatches = (spec, title) => typeof spec.title === "string" ? spec.title.trim().toLowerCase() === title.toLowerCase() : spec.title.test(title);
|
|
16668
|
+
const titleShown = (spec) => typeof spec.title === "string" ? spec.title : spec.example;
|
|
16669
|
+
/** The lines the list lives on, with the section-heading problems. */
|
|
16670
|
+
const regionOf = (spec, lines) => {
|
|
16671
|
+
const section = spec.section;
|
|
16672
|
+
if (section === void 0) return {
|
|
16673
|
+
region: {
|
|
16674
|
+
start: 0,
|
|
16675
|
+
end: lines.length
|
|
16676
|
+
},
|
|
16677
|
+
problems: []
|
|
16678
|
+
};
|
|
16679
|
+
const problems = [];
|
|
16680
|
+
const headings = lines.flatMap((line) => headingOf(line) ?? []);
|
|
16681
|
+
const matching = headings.filter((h) => titleMatches(section, h.title));
|
|
16682
|
+
const wanted = "#".repeat(section.level);
|
|
16683
|
+
for (const h of matching.filter((h) => h.level !== section.level)) problems.push({
|
|
16684
|
+
line: h.line,
|
|
16685
|
+
message: `"${h.title}" is a level-${h.level} heading — write it as \`${wanted} ${h.title}\``
|
|
16686
|
+
});
|
|
16687
|
+
const atLevel = matching.filter((h) => h.level === section.level);
|
|
16688
|
+
for (const h of atLevel.slice(1)) problems.push({
|
|
16689
|
+
line: h.line,
|
|
16690
|
+
message: `"${h.title}" repeats the section on line ${atLevel[0].line + 1} — keep one`
|
|
16691
|
+
});
|
|
16692
|
+
const first = atLevel[0];
|
|
16693
|
+
if (first === void 0) {
|
|
16694
|
+
if (section.required !== false && matching.length === 0) problems.push({ message: `missing the \`${wanted} ${titleShown(section)}\` section` });
|
|
16695
|
+
return { problems };
|
|
16696
|
+
}
|
|
16697
|
+
const next = headings.find((h) => h.line > first.line && h.level <= section.level);
|
|
16698
|
+
return {
|
|
16699
|
+
region: {
|
|
16700
|
+
start: first.line + 1,
|
|
16701
|
+
end: next?.line ?? lines.length
|
|
16702
|
+
},
|
|
16703
|
+
problems
|
|
16704
|
+
};
|
|
16705
|
+
};
|
|
16706
|
+
/** One pass over the region: items, their continuations, and every lint the format implies. */
|
|
16707
|
+
const readItems = (spec, lines, region) => {
|
|
16708
|
+
const markers = spec.markers ?? ["-"];
|
|
16709
|
+
const wrap = spec.wrap ?? "join";
|
|
16710
|
+
const problems = [];
|
|
16711
|
+
const drafts = [];
|
|
16712
|
+
let firstKind;
|
|
16713
|
+
let current;
|
|
16714
|
+
let blank = false;
|
|
16715
|
+
let prose = [];
|
|
16716
|
+
const continueWith = (line, text) => {
|
|
16717
|
+
if (wrap === "forbid") {
|
|
16718
|
+
problems.push({
|
|
16719
|
+
line: line.index,
|
|
16720
|
+
message: "one line per item — this line continues the one above"
|
|
16721
|
+
});
|
|
16722
|
+
return;
|
|
16723
|
+
}
|
|
16724
|
+
current.parts.push(text.trim());
|
|
16725
|
+
};
|
|
16726
|
+
for (const line of lines.slice(region.start, region.end)) {
|
|
16727
|
+
const text = line.text;
|
|
16728
|
+
if (line.fenced) {
|
|
16729
|
+
if (current !== void 0 && (line.fenceIndent ?? 0) >= 2) {
|
|
16730
|
+
blank = false;
|
|
16731
|
+
continue;
|
|
16732
|
+
}
|
|
16733
|
+
if (line.opens === true && current !== void 0) {
|
|
16734
|
+
prose.push(line.index);
|
|
16735
|
+
current = void 0;
|
|
16736
|
+
} else if (line.opens !== true && ITEM_RE.test(text) && indentOf(text) < 2) problems.push({
|
|
16737
|
+
line: line.index,
|
|
16738
|
+
message: "a list item inside a code fence is not read as an item"
|
|
16739
|
+
});
|
|
16740
|
+
blank = false;
|
|
16741
|
+
continue;
|
|
16742
|
+
}
|
|
16743
|
+
if (text.trim() === "") {
|
|
16744
|
+
blank = true;
|
|
16745
|
+
continue;
|
|
16746
|
+
}
|
|
16747
|
+
const item = ITEM_RE.exec(text);
|
|
16748
|
+
const indent = indentOf(text);
|
|
16749
|
+
if (item !== null && indent < 2) {
|
|
16750
|
+
for (const p of prose) problems.push({
|
|
16751
|
+
line: p,
|
|
16752
|
+
message: "text between items — make it part of an item, or move it out of the list"
|
|
16753
|
+
});
|
|
16754
|
+
prose = [];
|
|
16755
|
+
const kind = markerKind(item[2]);
|
|
16756
|
+
if (!markers.includes(kind)) problems.push({
|
|
16757
|
+
line: line.index,
|
|
16758
|
+
message: `\`${item[2]}\` is not an allowed marker — use ${markerList(markers)}`
|
|
16759
|
+
});
|
|
16760
|
+
else if (firstKind !== void 0 && kind !== firstKind) problems.push({
|
|
16761
|
+
line: line.index,
|
|
16762
|
+
message: `\`${item[2]}\` mixes markers in one list — the list started with \`${firstKind}\``
|
|
16763
|
+
});
|
|
16764
|
+
firstKind ??= kind;
|
|
16765
|
+
current = {
|
|
16766
|
+
line: line.index,
|
|
16767
|
+
parts: [(item[3] ?? "").trim()]
|
|
16768
|
+
};
|
|
16769
|
+
drafts.push(current);
|
|
16770
|
+
blank = false;
|
|
16771
|
+
continue;
|
|
16772
|
+
}
|
|
16773
|
+
if (current === void 0) {
|
|
16774
|
+
blank = false;
|
|
16775
|
+
continue;
|
|
16776
|
+
}
|
|
16777
|
+
if (item !== null) problems.push({
|
|
16778
|
+
line: line.index,
|
|
16779
|
+
message: "nested list — this format is flat, one level of items"
|
|
16780
|
+
});
|
|
16781
|
+
else if (indent >= 2) continueWith(line, text);
|
|
16782
|
+
else if (!blank) {
|
|
16783
|
+
problems.push({
|
|
16784
|
+
line: line.index,
|
|
16785
|
+
message: "continuation line is not indented — indent it under its item, or separate it with a blank line"
|
|
16786
|
+
});
|
|
16787
|
+
continueWith(line, text);
|
|
16788
|
+
} else {
|
|
16789
|
+
prose.push(line.index);
|
|
16790
|
+
current = void 0;
|
|
16791
|
+
}
|
|
16792
|
+
blank = false;
|
|
16793
|
+
}
|
|
16794
|
+
return {
|
|
16795
|
+
drafts,
|
|
16796
|
+
problems
|
|
16797
|
+
};
|
|
16798
|
+
};
|
|
16799
|
+
/** Strips a leading `[ ]`/`[x]`, reporting it where the format forbids or requires one. */
|
|
16800
|
+
const readCheckbox = (spec, draft, problems) => {
|
|
16801
|
+
const checkbox = spec.checkbox ?? "forbid";
|
|
16802
|
+
const text = draft.parts.join(" ").trim();
|
|
16803
|
+
const box = CHECKBOX_RE.exec(text);
|
|
16804
|
+
if (box === null) {
|
|
16805
|
+
if (checkbox === "require") problems.push({
|
|
16806
|
+
line: draft.line,
|
|
16807
|
+
message: "every item starts with a checkbox — write `- [ ] …`"
|
|
16808
|
+
});
|
|
16809
|
+
return checkbox === "forbid" ? { text } : {
|
|
16810
|
+
text,
|
|
16811
|
+
checked: false
|
|
16812
|
+
};
|
|
16813
|
+
}
|
|
16814
|
+
if (checkbox === "forbid") problems.push({
|
|
16815
|
+
line: draft.line,
|
|
16816
|
+
message: "no checkboxes in this list — drop the `[ ]`"
|
|
16817
|
+
});
|
|
16818
|
+
const rest = text.slice(box[0].length).trim();
|
|
16819
|
+
return checkbox === "forbid" ? { text: rest } : {
|
|
16820
|
+
text: rest,
|
|
16821
|
+
checked: box[1] !== " "
|
|
16822
|
+
};
|
|
16823
|
+
};
|
|
16824
|
+
const finishItem = (spec, draft, problems) => {
|
|
16825
|
+
const { text, ...checked } = readCheckbox(spec, draft, problems);
|
|
16826
|
+
const item = spec.item;
|
|
16827
|
+
if (text === "") {
|
|
16828
|
+
problems.push({
|
|
16829
|
+
line: draft.line,
|
|
16830
|
+
message: "empty item"
|
|
16831
|
+
});
|
|
16832
|
+
return;
|
|
16833
|
+
}
|
|
16834
|
+
if (item === void 0) return {
|
|
16835
|
+
line: draft.line,
|
|
16836
|
+
text,
|
|
16837
|
+
...checked
|
|
16838
|
+
};
|
|
16839
|
+
const at = text.indexOf(item.separator);
|
|
16840
|
+
const id = at < 0 ? "" : text.slice(0, at).trim();
|
|
16841
|
+
if (at < 0 || !item.id.test(id)) {
|
|
16842
|
+
problems.push({
|
|
16843
|
+
line: draft.line,
|
|
16844
|
+
message: `item has no ID — start it \`${item.example}${item.separator} …\``
|
|
16845
|
+
});
|
|
16846
|
+
return;
|
|
16847
|
+
}
|
|
16848
|
+
const body = text.slice(at + item.separator.length).trim();
|
|
16849
|
+
if (body === "") problems.push({
|
|
16850
|
+
line: draft.line,
|
|
16851
|
+
message: `empty item — ${id} has no text`
|
|
16852
|
+
});
|
|
16853
|
+
return {
|
|
16854
|
+
line: draft.line,
|
|
16855
|
+
text: body,
|
|
16856
|
+
id,
|
|
16857
|
+
...checked
|
|
16858
|
+
};
|
|
16859
|
+
};
|
|
16860
|
+
const parseList = (spec, content) => {
|
|
16861
|
+
const walked = walk(content);
|
|
16862
|
+
const { region, problems: sectionProblems } = regionOf(spec, walked.lines);
|
|
16863
|
+
const problems = [...walked.problems, ...sectionProblems];
|
|
16864
|
+
if (region === void 0) return {
|
|
16865
|
+
items: [],
|
|
16866
|
+
problems
|
|
16867
|
+
};
|
|
16868
|
+
const read = readItems(spec, walked.lines, region);
|
|
16869
|
+
problems.push(...read.problems);
|
|
16870
|
+
const items = read.drafts.flatMap((draft) => finishItem(spec, draft, problems) ?? []);
|
|
16871
|
+
if (spec.ids?.unique === true) {
|
|
16872
|
+
const seen = /* @__PURE__ */ new Map();
|
|
16873
|
+
for (const item of items) {
|
|
16874
|
+
if (item.id === void 0) continue;
|
|
16875
|
+
const first = seen.get(item.id);
|
|
16876
|
+
if (first === void 0) seen.set(item.id, item.line);
|
|
16877
|
+
else problems.push({
|
|
16878
|
+
line: item.line,
|
|
16879
|
+
message: `duplicate ID ${item.id} — first on line ${first + 1}`
|
|
16880
|
+
});
|
|
16881
|
+
}
|
|
16882
|
+
}
|
|
16883
|
+
problems.sort((a, b) => (a.line ?? -1) - (b.line ?? -1));
|
|
16884
|
+
return {
|
|
16885
|
+
items,
|
|
16886
|
+
problems
|
|
16887
|
+
};
|
|
16888
|
+
};
|
|
16889
|
+
const sectionSentence = (spec) => {
|
|
16890
|
+
if (spec.section === void 0) return "The whole document is the list.";
|
|
16891
|
+
return `Write the list under one \`${`${"#".repeat(spec.section.level)} ${titleShown(spec.section)}`}\` heading; it ends at the next heading of the same or a higher level.`;
|
|
16892
|
+
};
|
|
16893
|
+
const CHECKBOX_SENTENCE = {
|
|
16894
|
+
forbid: "Do not use checkboxes.",
|
|
16895
|
+
strip: "",
|
|
16896
|
+
require: "Start every item with a checkbox: `[ ]`, or `[x]` when done."
|
|
16897
|
+
};
|
|
16898
|
+
const idSentence = (spec) => {
|
|
16899
|
+
if (spec.item === void 0) return "";
|
|
16900
|
+
const unique = spec.ids?.unique === true ? ", unique within the list" : "";
|
|
16901
|
+
return `Start every item with its ID (like \`${spec.item.example}\`${unique}), then \`${spec.item.separator}\`, then the text.`;
|
|
16902
|
+
};
|
|
16903
|
+
const listInstructions = (spec) => [
|
|
16904
|
+
sectionSentence(spec),
|
|
16905
|
+
`Write each item as a ${markerList(spec.markers ?? ["-"])} list item, all with the same marker.`,
|
|
16906
|
+
CHECKBOX_SENTENCE[spec.checkbox ?? "forbid"],
|
|
16907
|
+
idSentence(spec),
|
|
16908
|
+
(spec.wrap ?? "join") === "join" ? "Continue a long item on lines indented two spaces under it." : "Keep every item on one line.",
|
|
16909
|
+
"Keep the list flat: no nested items, and no text between items. Items inside a code fence are not read."
|
|
16910
|
+
].filter((sentence) => sentence !== "").join(" ");
|
|
16911
|
+
const listSample = (spec) => {
|
|
16912
|
+
const item = `${(spec.markers ?? ["-"])[0] === "1." ? "1." : (spec.markers ?? ["-"])[0]} ${spec.checkbox === "require" ? "[ ] " : ""}${spec.item !== void 0 ? `${spec.item.example}${spec.item.separator} ` : ""}Describe the item.\n`;
|
|
16913
|
+
return spec.section === void 0 ? item : `${"#".repeat(spec.section.level)} ${titleShown(spec.section)}\n\n${item}`;
|
|
16914
|
+
};
|
|
16915
|
+
const bodyOf = (lines, start, end) => lines.slice(start, end).map((l) => l.text).join("\n").trim();
|
|
16916
|
+
/** The problem with heading `i` as a section, if any; `null` when it is not a section heading at all. */
|
|
16917
|
+
const sectionHeadingProblem = (spec, headings, i, firstLine) => {
|
|
16918
|
+
const h = headings[i];
|
|
16919
|
+
if (h.level < spec.level && headings.slice(0, i).some((p) => p.level === spec.level)) return `"${h.title}" is a level-${h.level} heading — sections are \`${"#".repeat(spec.level)}\``;
|
|
16920
|
+
if (h.level !== spec.level) return null;
|
|
16921
|
+
if (h.title === "") return "section heading has no title";
|
|
16922
|
+
const first = firstLine.get(h.title.toLowerCase());
|
|
16923
|
+
return spec.title === "unique" && first !== void 0 ? `"${h.title}" repeats the section on line ${first + 1} — titles are unique` : void 0;
|
|
16924
|
+
};
|
|
16925
|
+
const parseSections = (spec, content) => {
|
|
16926
|
+
const walked = walk(content);
|
|
16927
|
+
const problems = [...walked.problems];
|
|
16928
|
+
const headings = walked.lines.flatMap((line) => headingOf(line) ?? []);
|
|
16929
|
+
const sections = [];
|
|
16930
|
+
const firstLine = /* @__PURE__ */ new Map();
|
|
16931
|
+
for (const [i, h] of headings.entries()) {
|
|
16932
|
+
const problem = sectionHeadingProblem(spec, headings, i, firstLine);
|
|
16933
|
+
if (problem === null) continue;
|
|
16934
|
+
if (problem !== void 0) problems.push({
|
|
16935
|
+
line: h.line,
|
|
16936
|
+
message: problem
|
|
16937
|
+
});
|
|
16938
|
+
if (h.level !== spec.level || h.title === "") continue;
|
|
16939
|
+
if (!firstLine.has(h.title.toLowerCase())) firstLine.set(h.title.toLowerCase(), h.line);
|
|
16940
|
+
const end = headings.slice(i + 1).find((n) => n.level <= spec.level)?.line ?? walked.lines.length;
|
|
16941
|
+
sections.push({
|
|
16942
|
+
line: h.line,
|
|
16943
|
+
title: h.title,
|
|
16944
|
+
body: bodyOf(walked.lines, h.line + 1, end)
|
|
16945
|
+
});
|
|
16946
|
+
}
|
|
16947
|
+
problems.sort((a, b) => (a.line ?? -1) - (b.line ?? -1));
|
|
16948
|
+
return {
|
|
16949
|
+
sections,
|
|
16950
|
+
problems
|
|
16951
|
+
};
|
|
16952
|
+
};
|
|
16953
|
+
const sectionInstructions = (spec) => {
|
|
16954
|
+
const hashes = "#".repeat(spec.level);
|
|
16955
|
+
return [
|
|
16956
|
+
`Write each section under a \`${hashes} Title\` heading.`,
|
|
16957
|
+
spec.title === "unique" ? "No two sections share a title." : "",
|
|
16958
|
+
spec.level > 1 ? `Use deeper headings inside a section only; no heading above \`${hashes}\` after the first section.` : "",
|
|
16959
|
+
"Close every code fence."
|
|
16960
|
+
].filter((s) => s !== "").join(" ");
|
|
16961
|
+
};
|
|
16962
|
+
const NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
16963
|
+
const pattern = (re) => new RegExp(re.source, re.flags.replace(/[gy]/g, ""));
|
|
16964
|
+
const encode = (value) => value instanceof RegExp ? {
|
|
16965
|
+
source: value.source,
|
|
16966
|
+
flags: value.flags
|
|
16967
|
+
} : Array.isArray(value) ? value.map(encode) : typeof value === "object" && value !== null ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, encode(v)])) : value;
|
|
16968
|
+
const isPattern = (value) => typeof value === "object" && value !== null && typeof value.source === "string" && typeof value.flags === "string";
|
|
16969
|
+
const checkName = (name) => {
|
|
16970
|
+
if (typeof name !== "string" || !NAME_RE.test(name)) throw new Error(`format name must be lowercase letters, digits and "-" — got ${JSON.stringify(name)}`);
|
|
16971
|
+
return name;
|
|
16972
|
+
};
|
|
16973
|
+
const withThreads = (name, spec, kind, parse, instructions, sample) => ({
|
|
16974
|
+
...freeFormFormat,
|
|
16975
|
+
openThreads: openThreadsOf,
|
|
16976
|
+
name,
|
|
16977
|
+
spec: JSON.stringify({
|
|
16978
|
+
kind,
|
|
16979
|
+
...encode(spec)
|
|
16980
|
+
}),
|
|
16981
|
+
instructions,
|
|
16982
|
+
sample,
|
|
16983
|
+
parse,
|
|
16984
|
+
validate: (content) => [...parse(content).problems, ...threadFindings(content)]
|
|
16985
|
+
});
|
|
16986
|
+
/** Declares a list format: `parse`, `validate`, `instructions` and `sample` all derive from `spec`. Usable as a step's `mode`. */
|
|
16987
|
+
const listFormat = (spec) => {
|
|
16988
|
+
const name = checkName(spec.name);
|
|
16989
|
+
const normalized = {
|
|
16990
|
+
...spec,
|
|
16991
|
+
...spec.section !== void 0 && typeof spec.section.title !== "string" ? { section: {
|
|
16992
|
+
...spec.section,
|
|
16993
|
+
title: pattern(spec.section.title)
|
|
16994
|
+
} } : {},
|
|
16995
|
+
...spec.item !== void 0 ? { item: {
|
|
16996
|
+
...spec.item,
|
|
16997
|
+
id: pattern(spec.item.id)
|
|
16998
|
+
} } : {}
|
|
16999
|
+
};
|
|
17000
|
+
if ((normalized.markers ?? ["-"]).length === 0) throw new Error(`format "${name}": markers is empty`);
|
|
17001
|
+
if (normalized.item !== void 0 && !normalized.item.id.test(normalized.item.example)) throw new Error(`format "${name}": item.example "${normalized.item.example}" does not match item.id`);
|
|
17002
|
+
const section = normalized.section;
|
|
17003
|
+
if (section !== void 0 && typeof section.title !== "string" && (section.example === void 0 || !section.title.test(section.example))) throw new Error(`format "${name}": a RegExp section.title needs a section.example it matches`);
|
|
17004
|
+
const format = withThreads(name, normalized, "list", (content) => parseList(normalized, content), listInstructions(normalized), listSample(normalized));
|
|
17005
|
+
if (format.parse(format.sample).problems.length > 0) throw new Error(`format "${name}": its own sample does not validate`);
|
|
17006
|
+
return format;
|
|
17007
|
+
};
|
|
17008
|
+
/** Declares a document of headed sections. Usable as a step's `mode`. */
|
|
17009
|
+
const sectionFormat = (spec) => {
|
|
17010
|
+
const name = checkName(spec.name);
|
|
17011
|
+
const hashes = "#".repeat(spec.level);
|
|
17012
|
+
return withThreads(name, spec, "section", (content) => parseSections(spec, content), sectionInstructions(spec), `${hashes} First section\n\nWhat this section says.\n`);
|
|
17013
|
+
};
|
|
17014
|
+
const decodePatterns = (value, key) => (key === "title" || key === "id") && isPattern(value) ? new RegExp(value.source, value.flags) : Array.isArray(value) ? value.map((v) => decodePatterns(v)) : typeof value === "object" && value !== null ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, decodePatterns(v, k)])) : value;
|
|
17015
|
+
/** The format a declared format's `spec` describes — what `gtd check --spec` validates with. Throws on a spec no declaration produces. */
|
|
17016
|
+
const formatFromSpec = (spec) => {
|
|
17017
|
+
const { kind, ...rest } = decodePatterns(JSON.parse(spec));
|
|
17018
|
+
if (kind === "list") return listFormat(rest);
|
|
17019
|
+
if (kind === "section") return sectionFormat(rest);
|
|
17020
|
+
throw new Error(`not a declared format spec: ${JSON.stringify(kind)}`);
|
|
17021
|
+
};
|
|
17022
|
+
//#endregion
|
|
16603
17023
|
//#region src/steering/registry.ts
|
|
16604
17024
|
/** 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
17025
|
const REGISTRY = /* @__PURE__ */ new Map([
|
|
@@ -16827,4 +17247,4 @@ const stripCodeThreads = (path, text) => {
|
|
|
16827
17247
|
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
16828
17248
|
};
|
|
16829
17249
|
//#endregion
|
|
16830
|
-
export {
|
|
17250
|
+
export { findingEntries as A, reviewRisks as C, isAnswered as D, FREE_TEXT_PLACEHOLDER as E, headingSectionBodies as F, formatFinding as I, parseFootnotes as M, parseThreads as N, FIX_BOX as O, parseThreadsWithFindings as P, reviewNotes as S, unansweredNotes as T, ANSWER_LEAD as _, sections as a, reviewChunks as b, openThreads as c, formatFromSpec as d, listFormat as f, ANSWER_FIX as g, applySteeringEdits as h, sectionBodies as i, openThreadFindings as j, NOISE_HEADING as k, steeringFormatFor as l, freeFormFormat as m, stripCodeThreads as n, threads as o, sectionFormat as p, openQuestions as r, BUILT_IN_MODE_NAMES as s, parseCodeThreads as t, unansweredQuestions as u, ANSWER_WONT_FIX as v, reviewSections as w, reviewDecisions as x, REVIEW_SECTIONS as y };
|