@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,369 @@
|
|
|
1
|
+
import { giteaCreateIssue, type FetchLike } from "../lib/gitea";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Operator issue reporting for the local interface.
|
|
5
|
+
*
|
|
6
|
+
* The client sends one action plus the DOM context of the element it is about.
|
|
7
|
+
* A report also carries the operator's free text and, when the panel answered
|
|
8
|
+
* questions, the Ask conversation; the removal and redesign shortcuts carry
|
|
9
|
+
* only the context and use a fixed instruction. This module
|
|
10
|
+
* validates that payload, renders it as a Gitea issue for an implementation
|
|
11
|
+
* agent, and files the issue through the configured Gitea API.
|
|
12
|
+
*
|
|
13
|
+
* Reports describe the harness interface itself, so the issue always lands in
|
|
14
|
+
* the harness repository, never in the host project that embeds the harness.
|
|
15
|
+
* Without `DEVBOARD_GITEA_URL` reporting is disabled rather than redirected to
|
|
16
|
+
* another forge.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The repository that receives every reported UI issue. */
|
|
20
|
+
export const FEEDBACK_REPOSITORY = "dsdenes/devboard";
|
|
21
|
+
|
|
22
|
+
const MAX_REPORT = 8000;
|
|
23
|
+
const MAX_CONTEXT_FIELD = 2000;
|
|
24
|
+
const MAX_DOM_EXCERPT = 8000;
|
|
25
|
+
const MAX_VISIBLE_TEXT = 8000;
|
|
26
|
+
const MAX_VIEWPORT = 500;
|
|
27
|
+
const MAX_TITLE = 200;
|
|
28
|
+
const TITLE_PREFIX = "[ui] ";
|
|
29
|
+
const BODY_VISIBLE_TEXT = 1500;
|
|
30
|
+
/** Bounds shared with the client's Ask transcript capture. */
|
|
31
|
+
const MAX_CONVERSATION_TURNS = 40;
|
|
32
|
+
const MAX_CONVERSATION_TURN = 4000;
|
|
33
|
+
const MAX_CONVERSATION_TEXT = 12_000;
|
|
34
|
+
|
|
35
|
+
/** What the operator asked for when they picked an element. */
|
|
36
|
+
export type FeedbackAction = "report" | "remove" | "redesign";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The instruction a removal issue carries. It is fixed, so the implementation
|
|
40
|
+
* agent never has to interpret operator text for the one-click shortcuts.
|
|
41
|
+
*/
|
|
42
|
+
export const REMOVAL_INSTRUCTION = "Remove the selected UI element from the interface.";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The instruction a redesign issue carries. It names the normative design
|
|
46
|
+
* sources so the agent checks the element against the project's own system.
|
|
47
|
+
*/
|
|
48
|
+
export const REDESIGN_INSTRUCTION =
|
|
49
|
+
"Check the selected UI element and redesign it because it does not look good. " +
|
|
50
|
+
"Use the global OMP UI/UX standard (the ui-ux skill) and this repository's " +
|
|
51
|
+
"DESIGN.md and design/tokens.json as the normative design system.";
|
|
52
|
+
|
|
53
|
+
const ACTION_ERROR = "action must be one of report, remove, redesign.";
|
|
54
|
+
|
|
55
|
+
/** One visible turn of the element-scoped Ask conversation. */
|
|
56
|
+
export interface FeedbackTurn {
|
|
57
|
+
role: "developer" | "agent";
|
|
58
|
+
text: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FeedbackSubmission {
|
|
62
|
+
action: FeedbackAction;
|
|
63
|
+
report: string;
|
|
64
|
+
/** The Ask conversation the operator had about this element, if any. */
|
|
65
|
+
conversation: FeedbackTurn[];
|
|
66
|
+
route: string;
|
|
67
|
+
url: string;
|
|
68
|
+
selector: string;
|
|
69
|
+
domExcerpt: string;
|
|
70
|
+
visibleText: string;
|
|
71
|
+
viewport: string;
|
|
72
|
+
capturedAt: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FeedbackIssue {
|
|
76
|
+
url: string;
|
|
77
|
+
title: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A payload the endpoint refuses with a client error. */
|
|
81
|
+
export class FeedbackRequestError extends Error {
|
|
82
|
+
readonly status: number;
|
|
83
|
+
|
|
84
|
+
constructor(status: number, message: string) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "FeedbackRequestError";
|
|
87
|
+
this.status = status;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A delivery failure: the Gitea API refused the request or timed out. */
|
|
92
|
+
export class FeedbackDeliveryError extends Error {
|
|
93
|
+
readonly status: number;
|
|
94
|
+
|
|
95
|
+
constructor(status: number, message: string) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.name = "FeedbackDeliveryError";
|
|
98
|
+
this.status = status;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface FeedbackIssueOptions {
|
|
103
|
+
/** Gitea HTTP origin; reporting is disabled when this is absent. */
|
|
104
|
+
baseUrl: string;
|
|
105
|
+
/** `owner/name` repository that receives the issue. */
|
|
106
|
+
repo: string;
|
|
107
|
+
token?: string;
|
|
108
|
+
/** Wall-clock bound for the API request. */
|
|
109
|
+
timeoutMs: number;
|
|
110
|
+
/** Test seam; production uses the global fetch. */
|
|
111
|
+
fetchImpl?: FetchLike;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function field(
|
|
115
|
+
value: unknown,
|
|
116
|
+
name: string,
|
|
117
|
+
limit: number,
|
|
118
|
+
required: false,
|
|
119
|
+
): string;
|
|
120
|
+
function field(
|
|
121
|
+
value: unknown,
|
|
122
|
+
name: string,
|
|
123
|
+
limit: number,
|
|
124
|
+
required: true,
|
|
125
|
+
): string;
|
|
126
|
+
function field(
|
|
127
|
+
value: unknown,
|
|
128
|
+
name: string,
|
|
129
|
+
limit: number,
|
|
130
|
+
required: boolean,
|
|
131
|
+
): string {
|
|
132
|
+
if (value === undefined || value === null) {
|
|
133
|
+
if (required) throw new FeedbackRequestError(400, `${name} is required.`);
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
if (typeof value !== "string") {
|
|
137
|
+
throw new FeedbackRequestError(400, `${name} must be a string.`);
|
|
138
|
+
}
|
|
139
|
+
if (value.length > limit) {
|
|
140
|
+
throw new FeedbackRequestError(400, `${name} is longer than ${limit} characters.`);
|
|
141
|
+
}
|
|
142
|
+
if (required && value.trim().length === 0) {
|
|
143
|
+
throw new FeedbackRequestError(400, `${name} must not be empty.`);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function feedbackAction(value: unknown): FeedbackAction {
|
|
149
|
+
// An absent action keeps the original free-text report contract.
|
|
150
|
+
if (value === undefined || value === null) return "report";
|
|
151
|
+
if (value === "report" || value === "remove" || value === "redesign") return value;
|
|
152
|
+
throw new FeedbackRequestError(400, ACTION_ERROR);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The element context every browser request carries. */
|
|
156
|
+
export interface CapturedFields {
|
|
157
|
+
route: string;
|
|
158
|
+
url: string;
|
|
159
|
+
selector: string;
|
|
160
|
+
domExcerpt: string;
|
|
161
|
+
visibleText: string;
|
|
162
|
+
viewport: string;
|
|
163
|
+
capturedAt: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Validates the captured element context under the shared per-field limits. */
|
|
167
|
+
export function parseCapturedFields(source: Record<string, unknown>): CapturedFields {
|
|
168
|
+
return {
|
|
169
|
+
route: field(source.route, "route", MAX_CONTEXT_FIELD, false),
|
|
170
|
+
url: field(source.url, "url", MAX_CONTEXT_FIELD, false),
|
|
171
|
+
selector: field(source.selector, "selector", MAX_CONTEXT_FIELD, false),
|
|
172
|
+
domExcerpt: field(source.dom_excerpt, "dom_excerpt", MAX_DOM_EXCERPT, false),
|
|
173
|
+
visibleText: field(source.visible_text, "visible_text", MAX_VISIBLE_TEXT, false),
|
|
174
|
+
viewport: field(source.viewport, "viewport", MAX_VIEWPORT, false),
|
|
175
|
+
capturedAt: field(source.captured_at, "captured_at", MAX_CONTEXT_FIELD, false),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Validates the required free text under the shared report limit. */
|
|
180
|
+
export function parseReportText(value: unknown): string {
|
|
181
|
+
return field(value, "report", MAX_REPORT, true);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Validates the Ask conversation. A submission without one carries no turns,
|
|
186
|
+
* so an older client keeps filing reports unchanged.
|
|
187
|
+
*/
|
|
188
|
+
export function parseConversation(value: unknown): FeedbackTurn[] {
|
|
189
|
+
if (value === undefined || value === null) return [];
|
|
190
|
+
if (!Array.isArray(value)) {
|
|
191
|
+
throw new FeedbackRequestError(400, "conversation must be an array.");
|
|
192
|
+
}
|
|
193
|
+
if (value.length > MAX_CONVERSATION_TURNS) {
|
|
194
|
+
throw new FeedbackRequestError(
|
|
195
|
+
400,
|
|
196
|
+
`conversation has more than ${MAX_CONVERSATION_TURNS} turns.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const turns: FeedbackTurn[] = [];
|
|
200
|
+
let total = 0;
|
|
201
|
+
for (const [index, entry] of value.entries()) {
|
|
202
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
203
|
+
throw new FeedbackRequestError(400, `conversation[${index}] must be an object.`);
|
|
204
|
+
}
|
|
205
|
+
const source = entry as Record<string, unknown>;
|
|
206
|
+
const role = source.role;
|
|
207
|
+
if (role !== "developer" && role !== "agent") {
|
|
208
|
+
throw new FeedbackRequestError(
|
|
209
|
+
400,
|
|
210
|
+
`conversation[${index}].role must be developer or agent.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const text = field(source.text, `conversation[${index}].text`, MAX_CONVERSATION_TURN, true);
|
|
214
|
+
total += text.length;
|
|
215
|
+
turns.push({ role, text });
|
|
216
|
+
}
|
|
217
|
+
if (total > MAX_CONVERSATION_TEXT) {
|
|
218
|
+
throw new FeedbackRequestError(
|
|
219
|
+
400,
|
|
220
|
+
`conversation is longer than ${MAX_CONVERSATION_TEXT} characters.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return turns;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Validates one JSON body. Unknown fields are ignored for forwards compatibility. */
|
|
227
|
+
export function parseFeedbackSubmission(value: unknown): FeedbackSubmission {
|
|
228
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
229
|
+
throw new FeedbackRequestError(400, "The report must be a JSON object.");
|
|
230
|
+
}
|
|
231
|
+
const source = value as Record<string, unknown>;
|
|
232
|
+
const action = feedbackAction(source.action);
|
|
233
|
+
return {
|
|
234
|
+
action,
|
|
235
|
+
// The one-click shortcuts use a fixed instruction, so any text the client
|
|
236
|
+
// still sends is ignored rather than validated or filed.
|
|
237
|
+
report: action === "report" ? parseReportText(source.report) : "",
|
|
238
|
+
conversation: parseConversation(source.conversation),
|
|
239
|
+
...parseCapturedFields(source),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function collapse(value: string, limit: number): string {
|
|
244
|
+
const single = value.replace(/\s+/g, " ").trim();
|
|
245
|
+
if (single.length <= limit) return single;
|
|
246
|
+
return `${single.slice(0, limit - 1).trimEnd()}…`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function truncate(value: string, limit: number): string {
|
|
250
|
+
if (value.length <= limit) return value;
|
|
251
|
+
return `${value.slice(0, limit - 1).trimEnd()}…`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function firstLine(value: string): string {
|
|
255
|
+
const line =
|
|
256
|
+
value
|
|
257
|
+
.split("\n")
|
|
258
|
+
.map((entry) => entry.trim())
|
|
259
|
+
.find((entry) => entry.length > 0) ?? "no diagnostic";
|
|
260
|
+
return truncate(line, 300);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** One-line issue title for the submission's action. */
|
|
264
|
+
export function feedbackTitle(submission: FeedbackSubmission): string {
|
|
265
|
+
if (submission.action !== "report") {
|
|
266
|
+
const label = submission.action === "remove" ? "Remove element" : "Redesign element";
|
|
267
|
+
const selector = submission.selector.replace(/\s+/g, " ").trim();
|
|
268
|
+
if (selector.length === 0) return `${TITLE_PREFIX}${label}`;
|
|
269
|
+
return truncate(`${TITLE_PREFIX}${label}: ${selector}`, MAX_TITLE);
|
|
270
|
+
}
|
|
271
|
+
const line =
|
|
272
|
+
submission.report
|
|
273
|
+
.split("\n")
|
|
274
|
+
.map((value) => value.trim())
|
|
275
|
+
.find((value) => value.length > 0) ?? "operator report";
|
|
276
|
+
return truncate(`${TITLE_PREFIX}${line.replace(/\s+/g, " ")}`, MAX_TITLE);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The Ask conversation as Markdown: one labelled block per visible turn. It is
|
|
281
|
+
* empty when the operator filed without asking the agent anything.
|
|
282
|
+
*/
|
|
283
|
+
export function conversationSection(conversation: FeedbackTurn[]): string[] {
|
|
284
|
+
const lines: string[] = [];
|
|
285
|
+
for (const turn of conversation) {
|
|
286
|
+
lines.push("", `**${turn.role === "developer" ? "You" : "Agent"}:**`, "", turn.text.trim());
|
|
287
|
+
}
|
|
288
|
+
if (lines.length === 0) return [];
|
|
289
|
+
return ["## Ask conversation", ...lines];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The ticket body: the request first, then the conversation and captured context. */
|
|
293
|
+
export function feedbackBody(submission: FeedbackSubmission): string {
|
|
294
|
+
const request =
|
|
295
|
+
submission.action === "remove"
|
|
296
|
+
? ["## Removal request", "", REMOVAL_INSTRUCTION]
|
|
297
|
+
: submission.action === "redesign"
|
|
298
|
+
? ["## Redesign request", "", REDESIGN_INSTRUCTION]
|
|
299
|
+
: ["## Operator report", "", submission.report.trim()];
|
|
300
|
+
const lines = [...request, ""];
|
|
301
|
+
|
|
302
|
+
const context: string[] = [];
|
|
303
|
+
for (const [label, value] of [
|
|
304
|
+
["Route", submission.route],
|
|
305
|
+
["URL", submission.url],
|
|
306
|
+
["Selected element", submission.selector],
|
|
307
|
+
["Viewport", submission.viewport],
|
|
308
|
+
["Captured at", submission.capturedAt],
|
|
309
|
+
] as const) {
|
|
310
|
+
// A backtick inside an inline code span would close it early.
|
|
311
|
+
if (value.trim().length > 0) {
|
|
312
|
+
context.push(`- ${label}: \`${value.trim().replace(/`/g, "'")}\``);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const visible = collapse(submission.visibleText, BODY_VISIBLE_TEXT);
|
|
317
|
+
if (visible.length > 0) {
|
|
318
|
+
context.push("", "### Visible text", "", visible);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const dom = submission.domExcerpt.trim();
|
|
322
|
+
if (dom.length > 0) {
|
|
323
|
+
const fence = dom.includes("```") ? "````" : "```";
|
|
324
|
+
context.push("", "### DOM excerpt", "", `${fence}html`, dom, fence);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const conversation = conversationSection(submission.conversation);
|
|
328
|
+
if (conversation.length > 0) lines.push(...conversation, "");
|
|
329
|
+
|
|
330
|
+
lines.push("## Captured UI context", "");
|
|
331
|
+
lines.push(...(context.length === 0 ? ["No page context was captured."] : context));
|
|
332
|
+
return lines.join("\n");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Files one report and returns the created issue. */
|
|
336
|
+
export async function fileFeedbackIssue(
|
|
337
|
+
submission: FeedbackSubmission,
|
|
338
|
+
options: FeedbackIssueOptions,
|
|
339
|
+
): Promise<FeedbackIssue> {
|
|
340
|
+
const title = feedbackTitle(submission);
|
|
341
|
+
const body = feedbackBody(submission);
|
|
342
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
343
|
+
const withTimeout: FetchLike = (input, init) =>
|
|
344
|
+
fetchImpl(input, {
|
|
345
|
+
...init,
|
|
346
|
+
signal: AbortSignal.timeout(options.timeoutMs),
|
|
347
|
+
});
|
|
348
|
+
try {
|
|
349
|
+
const issue = await giteaCreateIssue(
|
|
350
|
+
{
|
|
351
|
+
baseUrl: options.baseUrl,
|
|
352
|
+
project: options.repo,
|
|
353
|
+
...(options.token === undefined ? {} : { token: options.token }),
|
|
354
|
+
},
|
|
355
|
+
{ title, body },
|
|
356
|
+
withTimeout,
|
|
357
|
+
);
|
|
358
|
+
return { url: issue.url, title };
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
361
|
+
throw new FeedbackDeliveryError(
|
|
362
|
+
504,
|
|
363
|
+
`Issue creation timed out after ${options.timeoutMs} ms.`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
367
|
+
throw new FeedbackDeliveryError(502, `Issue creation failed: ${firstLine(detail)}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
|
|
3
|
+
/** The status vocabulary a fixture record may carry. */
|
|
4
|
+
export type AdrStatus = "draft" | "proposed" | "accepted" | "rejected" | "superseded" | "deprecated";
|
|
5
|
+
|
|
6
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds a fixture decision record with the YAML frontmatter the corpus format requires.
|
|
10
|
+
* The four-digit id is taken from the file name, so the record passes corpus validation
|
|
11
|
+
* exactly where the fixture writes it.
|
|
12
|
+
*/
|
|
13
|
+
export function fixtureRecord(
|
|
14
|
+
file: string,
|
|
15
|
+
title: string,
|
|
16
|
+
status: AdrStatus = "accepted",
|
|
17
|
+
body = "",
|
|
18
|
+
extra: Record<string, string> = {},
|
|
19
|
+
): string {
|
|
20
|
+
const id = /^(\d{4,})-/.exec(basename(file))?.[1];
|
|
21
|
+
if (id === undefined) throw new Error(`Fixture record name needs a numeric id: ${file}`);
|
|
22
|
+
return [
|
|
23
|
+
"---",
|
|
24
|
+
"schemaVersion: 0.2.0",
|
|
25
|
+
`id: "${id}"`,
|
|
26
|
+
`title: ${title}`,
|
|
27
|
+
`status: ${status}`,
|
|
28
|
+
...Object.entries(extra).map(([key, value]) => `${key}: ${value}`),
|
|
29
|
+
"date: 2026-10-19",
|
|
30
|
+
'deciders: ["@dsdenes"]',
|
|
31
|
+
"affects: []",
|
|
32
|
+
"---",
|
|
33
|
+
`# ${title}`,
|
|
34
|
+
"",
|
|
35
|
+
body,
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A record that is still waiting for a decision. */
|
|
40
|
+
export function pendingRecord(file: string, title: string, context = "", decision = ""): string {
|
|
41
|
+
return fixtureRecord(
|
|
42
|
+
file,
|
|
43
|
+
title,
|
|
44
|
+
"proposed",
|
|
45
|
+
`## Context\n\n${context}\n\n## Decision\n\n${decision}\n`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The status field of a record's leading YAML frontmatter block, if it has one. */
|
|
50
|
+
export function frontmatterStatus(source: string): AdrStatus | undefined {
|
|
51
|
+
const block = FRONTMATTER.exec(source)?.[1];
|
|
52
|
+
if (block === undefined) return undefined;
|
|
53
|
+
return /^status:\s*([a-z]+)\s*$/m.exec(block)?.[1] as AdrStatus | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether a status is waiting for a decision. */
|
|
57
|
+
export function isPendingStatus(status: AdrStatus | undefined): boolean {
|
|
58
|
+
return status === "draft" || status === "proposed";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Rewrites the status field inside the frontmatter, leaving the rest of the file untouched. */
|
|
62
|
+
export function setFrontmatterStatus(source: string, status: AdrStatus): string {
|
|
63
|
+
const match = FRONTMATTER.exec(source);
|
|
64
|
+
if (match === null) throw new Error("Record has no YAML frontmatter to update.");
|
|
65
|
+
const block = match[1] ?? "";
|
|
66
|
+
if (!/^status:\s*[a-z]+\s*$/m.test(block)) throw new Error("Record frontmatter has no status field.");
|
|
67
|
+
const updated = block.replace(/^status:\s*[a-z]+\s*$/m, `status: ${status}`);
|
|
68
|
+
const start = match[0].indexOf(block);
|
|
69
|
+
const head = match[0].slice(0, start) + updated + match[0].slice(start + block.length);
|
|
70
|
+
return head + source.slice(match[0].length);
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gh` substitute whose asset download never settles. It records its pid in
|
|
3
|
+
* `STUB_GH_PID_FILE` so a test can prove the downloader's deadline killed the
|
|
4
|
+
* child instead of leaving it running.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
const pidFile = process.env.STUB_GH_PID_FILE;
|
|
10
|
+
if (pidFile !== undefined) writeFileSync(pidFile, `${process.pid}\n`);
|
|
11
|
+
|
|
12
|
+
await Bun.sleep(120_000);
|
|
13
|
+
|
|
14
|
+
export {};
|