@dsdenes/devboard 0.1.560 → 0.1.562
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/AGENTS.md +59 -0
- package/README.md +2497 -26
- package/bin/devboard.mjs +352 -118
- package/client/App.svelte +523 -0
- package/client/FeedbackOverlay.svelte +1125 -0
- package/client/FlowMode.svelte +1015 -0
- package/client/app.css +3922 -0
- package/client/env.d.ts +23 -0
- package/client/lib/commands.ts +836 -0
- package/client/lib/components/ActivityBar.svelte +295 -0
- package/client/lib/components/AdrTable.svelte +241 -0
- package/client/lib/components/CommandPalette.svelte +198 -0
- package/client/lib/components/ConceptKind.svelte +66 -0
- package/client/lib/components/ConfirmDialog.svelte +62 -0
- package/client/lib/components/DataTable.svelte +243 -0
- package/client/lib/components/DetailSplit.svelte +68 -0
- package/client/lib/components/DiscoverOverview.svelte +898 -0
- package/client/lib/components/DiscoverWorkspace.svelte +437 -0
- package/client/lib/components/DraftsPage.svelte +186 -0
- package/client/lib/components/GoalWorkspace.svelte +1042 -0
- package/client/lib/components/IssuesAgentFocus.svelte +117 -0
- package/client/lib/components/IssuesAgentLog.svelte +50 -0
- package/client/lib/components/IssuesQuestion.svelte +95 -0
- package/client/lib/components/IssuesWorkspace.svelte +577 -0
- package/client/lib/components/MermaidDiagram.svelte +89 -0
- package/client/lib/components/MonacoEditor.svelte +190 -0
- package/client/lib/components/NotificationRegion.svelte +195 -0
- package/client/lib/components/PaneHandle.svelte +25 -0
- package/client/lib/components/ScoutIssues.svelte +215 -0
- package/client/lib/components/ScoutWorkspace.svelte +180 -0
- package/client/lib/components/Sidebar.svelte +622 -0
- package/client/lib/components/StatusBar.svelte +118 -0
- package/client/lib/components/Terminal.svelte +193 -0
- package/client/lib/components/WatchdogWorkspace.svelte +537 -0
- package/client/lib/components/ui/button.svelte +56 -0
- package/client/lib/components/ui/icon-button.svelte +56 -0
- package/client/lib/components/ui/input.svelte +19 -0
- package/client/lib/components/ui/page-header.svelte +120 -0
- package/client/lib/components/ui/stage-track.svelte +141 -0
- package/client/lib/components/watchdog-indicator.svelte +189 -0
- package/client/lib/connection.svelte.ts +210 -0
- package/client/lib/dependency-diagram.test.ts +90 -0
- package/client/lib/dependency-diagram.ts +75 -0
- package/client/lib/discover-format.test.ts +265 -0
- package/client/lib/discover-format.ts +394 -0
- package/client/lib/discover.svelte.ts +412 -0
- package/client/lib/draft-format.test.ts +48 -0
- package/client/lib/draft-format.ts +13 -0
- package/client/lib/flow-save.test.ts +148 -0
- package/client/lib/flow.svelte.ts +445 -0
- package/client/lib/goal-reconnect.test.ts +282 -0
- package/client/lib/goal.svelte.ts +652 -0
- package/client/lib/issues.svelte.ts +439 -0
- package/client/lib/layout.svelte.ts +234 -0
- package/client/lib/notification-issue.test.ts +53 -0
- package/client/lib/notification-issue.ts +27 -0
- package/client/lib/notifications.svelte.ts +62 -0
- package/client/lib/poll-status.test.ts +141 -0
- package/client/lib/poll-status.ts +88 -0
- package/client/lib/record-status.test.ts +19 -0
- package/client/lib/record-status.ts +25 -0
- package/client/lib/scout.svelte.ts +164 -0
- package/client/lib/skill-document.test.ts +77 -0
- package/client/lib/skill-document.ts +57 -0
- package/client/lib/status-tone.test.ts +40 -0
- package/client/lib/status-tone.ts +33 -0
- package/client/lib/types.ts +578 -0
- package/client/lib/utils.ts +6 -0
- package/client/lib/viewport.svelte.ts +20 -0
- package/client/lib/watchdog.svelte.ts +69 -0
- package/client/main.ts +7 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
- package/dist/assets/arc-BZgCSX2H.js +1 -0
- package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
- package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
- package/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
- package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
- package/dist/assets/channel-CgM4lcUW.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
- package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
- package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
- package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
- package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
- package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
- package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
- package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
- package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
- package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
- package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
- package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
- package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
- package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
- package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
- package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
- package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
- package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
- package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/assets/dagre-D7JM2Yhv.js +1 -0
- package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
- package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
- package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
- package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
- package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
- package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
- package/dist/assets/dist-Dce-2YbA.js +1 -0
- package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
- package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
- package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
- package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
- package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
- package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
- package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
- package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
- package/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/assets/index-ByQ-XX38.css +1 -0
- package/dist/assets/index-CIf5TpfY.js +214 -0
- package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
- package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
- package/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
- package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
- package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
- package/dist/assets/katex-DJhrJSbw.js +257 -0
- package/dist/assets/line-N9XiCREw.js +1 -0
- package/dist/assets/linear-C9hnqmJv.js +1 -0
- package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
- package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
- package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
- package/dist/assets/monaco-DjpsE8to.css +1 -0
- package/dist/assets/monaco-DqzJaNIe.js +908 -0
- package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
- package/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
- package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
- package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
- package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
- package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
- package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
- package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
- package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
- package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
- package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
- package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
- package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/assets/src-CGp3bfio.js +1 -0
- package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
- package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
- package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
- package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
- package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
- package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
- package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
- package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
- package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
- package/dist/assets/xterm-BrP-ENHg.css +1 -0
- package/dist/assets/xterm-C8zLTNGk.js +16 -0
- package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +6 -0
- package/dist/index.html +34 -0
- package/extensions/implement-goal-issue.ts +368 -0
- package/extensions/implement-next-epic.ts +835 -0
- package/index.html +28 -0
- package/install.sh +111 -0
- package/issues/control.ts +91 -0
- package/issues/guards.ts +12 -0
- package/issues/ledger.ts +122 -0
- package/issues/legacy.ts +68 -0
- package/issues/lock.ts +305 -0
- package/issues/paths.ts +76 -0
- package/issues/preflight.ts +189 -0
- package/issues/repository.ts +203 -0
- package/issues/scout/actions.ts +255 -0
- package/issues/scout/control.ts +80 -0
- package/issues/scout/github.ts +358 -0
- package/issues/scout/paths.ts +129 -0
- package/issues/scout/preflight.ts +277 -0
- package/issues/scout/recommendation.ts +400 -0
- package/issues/scout/scan.ts +143 -0
- package/issues/scout/state.ts +448 -0
- package/issues/scout/worker.ts +1282 -0
- package/issues/state.ts +584 -0
- package/issues/tracker.ts +271 -0
- package/issues/worker.ts +1065 -0
- package/lib/dependencies.ts +61 -0
- package/lib/forge-auth.ts +953 -0
- package/lib/forge.ts +141 -0
- package/lib/gitea.ts +709 -0
- package/lib/github-release.ts +318 -0
- package/lib/inventory.test.ts +79 -0
- package/lib/inventory.ts +53 -0
- package/lib/release.test.ts +269 -0
- package/lib/release.ts +801 -0
- package/omp/commands/adr-resolve.md +8 -0
- package/omp/commands/discover.md +6 -0
- package/omp/commands/epic-next.md +8 -0
- package/omp/commands/flow-change.md +8 -0
- package/omp/commands/flow-goal.md +8 -0
- package/omp/commands/flow-implement.md +8 -0
- package/omp/commands/flow-widget.md +8 -0
- package/omp/commands/goal-draft.md +8 -0
- package/omp/commands/goal-outcome.md +8 -0
- package/omp/commands/implement-issue.md +8 -0
- package/omp/commands/prepare-skills.md +6 -0
- package/omp/commands/scout-recommendations.md +8 -0
- package/omp/commands/setup-devboard.md +24 -0
- package/omp/config.yml +12 -0
- package/omp/extensions/implement-goal-issue.ts +2 -0
- package/omp/extensions/implement-next-epic.ts +2 -0
- package/omp/mcp.json +9 -0
- package/package.json +86 -7
- package/prompts/adr-resolve.md +130 -0
- package/prompts/discover.md +541 -0
- package/prompts/epic-next.md +533 -0
- package/prompts/flow-change.md +76 -0
- package/prompts/flow-goal.md +126 -0
- package/prompts/flow-implement.md +103 -0
- package/prompts/flow-widget.md +54 -0
- package/prompts/goal-draft.md +147 -0
- package/prompts/goal-outcome.md +87 -0
- package/prompts/implement-issue.md +66 -0
- package/prompts/prepare-skills.md +142 -0
- package/prompts/scout-recommendations.md +143 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +6 -0
- package/server/acp.ts +540 -0
- package/server/activity.test.ts +96 -0
- package/server/activity.ts +48 -0
- package/server/app.test.ts +882 -0
- package/server/app.ts +6179 -0
- package/server/ask.test.ts +543 -0
- package/server/ask.ts +589 -0
- package/server/dependencies.test.ts +118 -0
- package/server/discover-remote.test.ts +1602 -0
- package/server/discover-sheet.test.ts +176 -0
- package/server/discover.test.ts +1510 -0
- package/server/discover.ts +3198 -0
- package/server/feedback.test.ts +458 -0
- package/server/feedback.ts +369 -0
- package/server/fixtures/adr-source.ts +71 -0
- package/server/fixtures/hang-download.ts +14 -0
- package/server/fixtures/stub-agent.ts +1870 -0
- package/server/fixtures/stub-forge.ts +141 -0
- package/server/fixtures/stub-gh.ts +57 -0
- package/server/flow-pr.test.ts +976 -0
- package/server/flow.test.ts +1632 -0
- package/server/flow.ts +2655 -0
- package/server/forge-pr.ts +387 -0
- package/server/goal-creation.test.ts +957 -0
- package/server/goal-draft.test.ts +348 -0
- package/server/goal-drafts.ts +449 -0
- package/server/goal-proposal.test.ts +479 -0
- package/server/goal-proposal.ts +232 -0
- package/server/goal-revision.test.ts +613 -0
- package/server/goal-slug.test.ts +56 -0
- package/server/goal-slug.ts +96 -0
- package/server/goal.ts +20 -0
- package/server/index.ts +147 -0
- package/server/issue-close.test.ts +100 -0
- package/server/issue-close.ts +178 -0
- package/server/issues.test.ts +167 -0
- package/server/issues.ts +479 -0
- package/server/journal.test.ts +350 -0
- package/server/journal.ts +329 -0
- package/server/lifecycle-pause.test.ts +1795 -0
- package/server/lifecycle.test.ts +1879 -0
- package/server/parallel-lifecycle.test.ts +1029 -0
- package/server/persistence-restart.test.ts +723 -0
- package/server/persistence.test.ts +1935 -0
- package/server/records.test.ts +180 -0
- package/server/records.ts +99 -0
- package/server/reset-implementation.test.ts +997 -0
- package/server/run.test.ts +611 -0
- package/server/scout.test.ts +877 -0
- package/server/scout.ts +459 -0
- package/server/skills-preparation.test.ts +1073 -0
- package/server/state.ts +249 -0
- package/server/tracker.ts +369 -0
- package/server/updates.test.ts +444 -0
- package/server/updates.ts +198 -0
- package/server/version.test.ts +140 -0
- package/server/version.ts +151 -0
- package/server/watchdog.ts +389 -0
- package/svelte.config.js +5 -0
- package/templates/host-AGENTS.md +235 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +21 -0
- package/watchdog/cleanup-main.ts +98 -0
- package/watchdog/cleanup.ts +301 -0
- package/watchdog/collect.ts +311 -0
- package/watchdog/coordinator.ts +397 -0
- package/watchdog/delivery.ts +107 -0
- package/watchdog/discovery.ts +209 -0
- package/watchdog/findings.ts +211 -0
- package/watchdog/github.ts +189 -0
- package/watchdog/history.ts +138 -0
- package/watchdog/install.ts +103 -0
- package/watchdog/ledger.ts +188 -0
- package/watchdog/lock.ts +63 -0
- package/watchdog/main.ts +106 -0
- package/watchdog/status.ts +71 -0
- package/watchdog/systemd.ts +96 -0
- package/watchdog/uninstall.ts +30 -0
- package/watchdog/units/devboard-watchdog.service +9 -0
- package/watchdog/units/devboard-watchdog.timer +10 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { fixtureRecord } from "./fixtures/adr-source";
|
|
2
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { readRecords } from "./records";
|
|
6
|
+
|
|
7
|
+
const temporaryDirectories: string[] = [];
|
|
8
|
+
const TEMP_ROOT = join(import.meta.dir, "..", ".tmp");
|
|
9
|
+
|
|
10
|
+
async function temporaryDirectory() {
|
|
11
|
+
await mkdir(TEMP_ROOT, { recursive: true });
|
|
12
|
+
const directory = await mkdtemp(join(TEMP_ROOT, "records-"));
|
|
13
|
+
temporaryDirectories.push(directory);
|
|
14
|
+
return directory;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
await Promise.all(
|
|
19
|
+
temporaryDirectories.map((directory) => rm(directory, { recursive: true, force: true })),
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("readRecords", () => {
|
|
24
|
+
test("reads the title and status from the frontmatter", async () => {
|
|
25
|
+
const directory = await temporaryDirectory();
|
|
26
|
+
await writeFile(join(directory, "0001-example.md"), fixtureRecord("0001-example.md", "Example record"));
|
|
27
|
+
|
|
28
|
+
const [record] = (await readRecords(directory)).records;
|
|
29
|
+
|
|
30
|
+
expect(record).toEqual({
|
|
31
|
+
file: "0001-example.md",
|
|
32
|
+
title: "Example record",
|
|
33
|
+
status: "Accepted",
|
|
34
|
+
pending: false,
|
|
35
|
+
summary: "",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("summarizes the first paragraph of the Decision section", async () => {
|
|
40
|
+
const directory = await temporaryDirectory();
|
|
41
|
+
await writeFile(
|
|
42
|
+
join(directory, "0007-decision.md"),
|
|
43
|
+
fixtureRecord(
|
|
44
|
+
"0007-decision.md",
|
|
45
|
+
"A decision",
|
|
46
|
+
"accepted",
|
|
47
|
+
[
|
|
48
|
+
"## Context",
|
|
49
|
+
"",
|
|
50
|
+
"This must not be the summary.",
|
|
51
|
+
"",
|
|
52
|
+
"## Decision",
|
|
53
|
+
"",
|
|
54
|
+
"Use the `boring` runtime for **every** service.",
|
|
55
|
+
"The second sentence stays in the summary.",
|
|
56
|
+
"",
|
|
57
|
+
"## Consequences",
|
|
58
|
+
"",
|
|
59
|
+
"Ignored.",
|
|
60
|
+
].join("\n"),
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const [record] = (await readRecords(directory)).records;
|
|
65
|
+
|
|
66
|
+
expect(record?.summary).toBe(
|
|
67
|
+
"Use the boring runtime for every service. The second sentence stays in the summary.",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("shortens a long decision paragraph at a word boundary", async () => {
|
|
72
|
+
const directory = await temporaryDirectory();
|
|
73
|
+
const paragraph = `${"word ".repeat(80)}end.`;
|
|
74
|
+
await writeFile(
|
|
75
|
+
join(directory, "0008-long.md"),
|
|
76
|
+
fixtureRecord("0008-long.md", "Long", "proposed", `## Decision\n\n${paragraph}\n`),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const [record] = (await readRecords(directory)).records;
|
|
80
|
+
|
|
81
|
+
expect(record?.summary.length).toBeLessThanOrEqual(281);
|
|
82
|
+
expect(record?.summary.endsWith("…")).toBe(true);
|
|
83
|
+
expect(record?.summary.startsWith("word word")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("marks draft and proposed records as pending", async () => {
|
|
87
|
+
const directory = await temporaryDirectory();
|
|
88
|
+
await writeFile(join(directory, "0001-draft.md"), fixtureRecord("0001-draft.md", "Draft", "draft"));
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(directory, "0002-proposed.md"),
|
|
91
|
+
fixtureRecord("0002-proposed.md", "Proposed", "proposed"),
|
|
92
|
+
);
|
|
93
|
+
await writeFile(join(directory, "0003-accepted.md"), fixtureRecord("0003-accepted.md", "Accepted"));
|
|
94
|
+
await writeFile(
|
|
95
|
+
join(directory, "0004-rejected.md"),
|
|
96
|
+
fixtureRecord("0004-rejected.md", "Rejected", "rejected"),
|
|
97
|
+
);
|
|
98
|
+
await writeFile(
|
|
99
|
+
join(directory, "0005-deprecated.md"),
|
|
100
|
+
fixtureRecord("0005-deprecated.md", "Deprecated", "deprecated"),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const list = await readRecords(directory);
|
|
104
|
+
|
|
105
|
+
expect(list.records.map((record) => [record.file, record.status, record.pending])).toEqual([
|
|
106
|
+
["0001-draft.md", "Draft", true],
|
|
107
|
+
["0002-proposed.md", "Proposed", true],
|
|
108
|
+
["0003-accepted.md", "Accepted", false],
|
|
109
|
+
["0004-rejected.md", "Rejected", false],
|
|
110
|
+
["0005-deprecated.md", "Deprecated", false],
|
|
111
|
+
]);
|
|
112
|
+
expect(list.next).toBe("0001-draft.md");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("leaves a file without valid frontmatter out of the list, pending set and next", async () => {
|
|
116
|
+
const directory = await temporaryDirectory();
|
|
117
|
+
await writeFile(join(directory, "0001-invalid.md"), "# Invalid\n\nNo frontmatter here.\n");
|
|
118
|
+
await writeFile(join(directory, "0002-valid.md"), fixtureRecord("0002-valid.md", "Valid", "proposed"));
|
|
119
|
+
|
|
120
|
+
const list = await readRecords(directory);
|
|
121
|
+
|
|
122
|
+
expect(list.records.map((record) => record.file)).toEqual(["0002-valid.md"]);
|
|
123
|
+
expect(list.next).toBe("0002-valid.md");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("orders records by file name and marks the lowest pending record as next", async () => {
|
|
127
|
+
const directory = await temporaryDirectory();
|
|
128
|
+
await writeFile(
|
|
129
|
+
join(directory, "0002-second.md"),
|
|
130
|
+
fixtureRecord("0002-second.md", "Second", "proposed"),
|
|
131
|
+
);
|
|
132
|
+
await writeFile(join(directory, "0001-first.md"), fixtureRecord("0001-first.md", "First"));
|
|
133
|
+
await writeFile(
|
|
134
|
+
join(directory, "0003-third.md"),
|
|
135
|
+
fixtureRecord("0003-third.md", "Third", "proposed"),
|
|
136
|
+
);
|
|
137
|
+
await writeFile(join(directory, "notes.txt"), "not a record");
|
|
138
|
+
|
|
139
|
+
const list = await readRecords(directory);
|
|
140
|
+
|
|
141
|
+
expect(list.records.map((record) => record.file)).toEqual([
|
|
142
|
+
"0001-first.md",
|
|
143
|
+
"0002-second.md",
|
|
144
|
+
"0003-third.md",
|
|
145
|
+
]);
|
|
146
|
+
expect(list.next).toBe("0002-second.md");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("returns null next when every record is resolved", async () => {
|
|
150
|
+
const directory = await temporaryDirectory();
|
|
151
|
+
await writeFile(join(directory, "0001-first.md"), fixtureRecord("0001-first.md", "First"));
|
|
152
|
+
|
|
153
|
+
const list = await readRecords(directory);
|
|
154
|
+
|
|
155
|
+
expect(list.records).toHaveLength(1);
|
|
156
|
+
expect(list.next).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("reflects records added and removed between reads", async () => {
|
|
160
|
+
const directory = await temporaryDirectory();
|
|
161
|
+
await writeFile(join(directory, "0001-first.md"), fixtureRecord("0001-first.md", "First"));
|
|
162
|
+
|
|
163
|
+
expect((await readRecords(directory)).next).toBeNull();
|
|
164
|
+
|
|
165
|
+
await writeFile(
|
|
166
|
+
join(directory, "0002-added.md"),
|
|
167
|
+
fixtureRecord("0002-added.md", "Added", "proposed"),
|
|
168
|
+
);
|
|
169
|
+
expect((await readRecords(directory)).next).toBe("0002-added.md");
|
|
170
|
+
|
|
171
|
+
await rm(join(directory, "0002-added.md"));
|
|
172
|
+
expect((await readRecords(directory)).next).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("treats an absent record directory as an empty list", async () => {
|
|
176
|
+
const directory = join(await temporaryDirectory(), "removed");
|
|
177
|
+
|
|
178
|
+
expect(await readRecords(directory)).toEqual({ records: [], next: null });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { lintCorpus, type Adr } from "@adrkit/core";
|
|
3
|
+
|
|
4
|
+
export interface AdrRecord {
|
|
5
|
+
file: string;
|
|
6
|
+
title: string;
|
|
7
|
+
status: string;
|
|
8
|
+
pending: boolean;
|
|
9
|
+
/** The first paragraph of the Decision section, shortened for the record dropdown. */
|
|
10
|
+
summary: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AdrList {
|
|
14
|
+
records: AdrRecord[];
|
|
15
|
+
next: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const HEADING_PATTERN = /^#{1,6}\s+(.+?)\s*$/;
|
|
19
|
+
const DECISION_HEADING = /^decision$/i;
|
|
20
|
+
const SUMMARY_LIMIT = 280;
|
|
21
|
+
|
|
22
|
+
/** Collapses the first decision paragraph to the short form the dropdown shows. */
|
|
23
|
+
function summarizeDecision(paragraph: string[]): string {
|
|
24
|
+
if (paragraph.length === 0) return "";
|
|
25
|
+
const text = paragraph
|
|
26
|
+
.join(" ")
|
|
27
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
|
|
28
|
+
.replace(/[`*_]/g, "")
|
|
29
|
+
.replace(/^[-+]\s+/, "")
|
|
30
|
+
.replace(/\s+/g, " ")
|
|
31
|
+
.trim();
|
|
32
|
+
if (text.length <= SUMMARY_LIMIT) return text;
|
|
33
|
+
const cut = text.slice(0, SUMMARY_LIMIT);
|
|
34
|
+
const boundary = cut.lastIndexOf(" ");
|
|
35
|
+
return `${(boundary > 0 ? cut.slice(0, boundary) : cut).replace(/[.,;:!?-]+$/, "")}…`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reads the first paragraph under the record's Decision heading. */
|
|
39
|
+
function readDecision(lines: string[]): string {
|
|
40
|
+
let inDecision = false;
|
|
41
|
+
let started = false;
|
|
42
|
+
const paragraph: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const heading = HEADING_PATTERN.exec(line)?.[1];
|
|
46
|
+
if (heading !== undefined) {
|
|
47
|
+
if (DECISION_HEADING.test(heading)) {
|
|
48
|
+
inDecision = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (inDecision) break;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!inDecision) continue;
|
|
55
|
+
|
|
56
|
+
if (line.trim().length === 0) {
|
|
57
|
+
if (started) break;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
started = true;
|
|
61
|
+
paragraph.push(line.trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return summarizeDecision(paragraph);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Projects one adrkit record onto the interface's record contract. */
|
|
68
|
+
export function recordFromAdr(adr: Adr): AdrRecord {
|
|
69
|
+
const { status } = adr.frontmatter;
|
|
70
|
+
return {
|
|
71
|
+
file: basename(adr.path),
|
|
72
|
+
title: adr.frontmatter.title,
|
|
73
|
+
// The interface renders a capitalized status; adrkit stores the lowercase enum.
|
|
74
|
+
status: status.charAt(0).toUpperCase() + status.slice(1),
|
|
75
|
+
pending: status === "draft" || status === "proposed",
|
|
76
|
+
summary: readDecision(adr.body.split(/\r?\n/)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Reads the corpus through adrkit and lists only the records it accepts. */
|
|
81
|
+
export async function readRecords(directory: string): Promise<AdrList> {
|
|
82
|
+
let records: Adr[];
|
|
83
|
+
try {
|
|
84
|
+
({ records } = await lintCorpus({ dir: directory }));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// An implementation reset removes the record directory. An absent
|
|
87
|
+
// directory is an empty record set, not a read failure.
|
|
88
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
89
|
+
return { records: [], next: null };
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const list = records.map(recordFromAdr).sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
95
|
+
return {
|
|
96
|
+
records: list,
|
|
97
|
+
next: list.find((record) => record.pending)?.file ?? null,
|
|
98
|
+
};
|
|
99
|
+
}
|