@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,394 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Concept,
|
|
3
|
+
ConceptSignificance,
|
|
4
|
+
ConceptUsage,
|
|
5
|
+
DiscoverTab,
|
|
6
|
+
Glossary,
|
|
7
|
+
GlossarySignificance,
|
|
8
|
+
GlossaryUsage,
|
|
9
|
+
Leaf,
|
|
10
|
+
Labelled,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
/** Tabs that show the published sheet or glossary rather than a skill. */
|
|
14
|
+
export const SECTION_TABS: DiscoverTab[] = [
|
|
15
|
+
{ id: "overview", label: "Overview", skill: null },
|
|
16
|
+
{ id: "glossary", label: "Glossary", skill: null },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/** The five generated skills; the sidebar groups them under Skills. */
|
|
20
|
+
export const SKILL_TABS: DiscoverTab[] = [
|
|
21
|
+
{
|
|
22
|
+
id: "testing-strategy",
|
|
23
|
+
label: "Testing Strategy",
|
|
24
|
+
skill: "discover-testing-strategy",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "naming-conventions",
|
|
28
|
+
label: "Naming Conventions",
|
|
29
|
+
skill: "discover-naming-conventions",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "coding-conventions",
|
|
33
|
+
label: "Coding Conventions",
|
|
34
|
+
skill: "discover-coding-conventions",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "architecture-boundaries",
|
|
38
|
+
label: "Architecture & Boundaries",
|
|
39
|
+
skill: "discover-architecture-boundaries",
|
|
40
|
+
},
|
|
41
|
+
{ id: "glossary-skill", label: "Glossary", skill: "discover-glossary" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const TABS: DiscoverTab[] = [...SECTION_TABS, ...SKILL_TABS];
|
|
45
|
+
|
|
46
|
+
/** Leaves already shown elsewhere or too detailed for the operator's scan. */
|
|
47
|
+
export const HIDDEN_LEAVES: Record<string, string[]> = {
|
|
48
|
+
deployment: ["pipelineSources"],
|
|
49
|
+
runtime: ["versions"],
|
|
50
|
+
ci: ["qualityGates"],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const COMMAND_LABELS: Record<string, string> = {
|
|
54
|
+
install: "Install",
|
|
55
|
+
build: "Build",
|
|
56
|
+
test: "Test",
|
|
57
|
+
lint: "Lint",
|
|
58
|
+
typecheck: "Typecheck",
|
|
59
|
+
dev: "Dev / run",
|
|
60
|
+
tryOut: "Try it out",
|
|
61
|
+
release: "Release",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Pyramid tiers from the base (widest) to the apex (narrowest). */
|
|
65
|
+
export const PYRAMID_LEVELS = ["unit", "integration", "contract", "end-to-end", "other"];
|
|
66
|
+
|
|
67
|
+
export function labelText(labels: Labelled[] | undefined): string {
|
|
68
|
+
if (!Array.isArray(labels)) return "";
|
|
69
|
+
const match = labels.find((label) => label.language === "en") ?? labels[0];
|
|
70
|
+
return match?.text ?? "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function labelTexts(labels: Labelled[] | undefined): string[] {
|
|
74
|
+
if (!Array.isArray(labels)) return [];
|
|
75
|
+
return labels.map((label) => label.text).filter((text) => typeof text === "string");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isLeaf(value: unknown): value is Leaf {
|
|
79
|
+
return (
|
|
80
|
+
typeof value === "object" &&
|
|
81
|
+
value !== null &&
|
|
82
|
+
"confidence" in value &&
|
|
83
|
+
"evidence" in value &&
|
|
84
|
+
Array.isArray((value as { evidence: unknown }).evidence)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function humanize(key: string): string {
|
|
89
|
+
const spaced = key
|
|
90
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
91
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
92
|
+
.toLowerCase();
|
|
93
|
+
return spaced.length === 0 ? key : spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const MINUTE = 60_000;
|
|
97
|
+
const HOUR = 60 * MINUTE;
|
|
98
|
+
const DAY = 24 * HOUR;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Human elapsed time for an ISO timestamp, such as `3 minutes ago`. A future
|
|
102
|
+
* timestamp (clock skew between the server and the browser) reads `just now`.
|
|
103
|
+
*/
|
|
104
|
+
export function relativeTime(value: string, now: number = Date.now()): string {
|
|
105
|
+
const timestamp = Date.parse(value);
|
|
106
|
+
if (Number.isNaN(timestamp)) return "";
|
|
107
|
+
const elapsed = now - timestamp;
|
|
108
|
+
if (elapsed < MINUTE) return "just now";
|
|
109
|
+
if (elapsed < 2 * MINUTE) return "a minute ago";
|
|
110
|
+
if (elapsed < HOUR) return `${Math.floor(elapsed / MINUTE)} minutes ago`;
|
|
111
|
+
if (elapsed < 2 * HOUR) return "an hour ago";
|
|
112
|
+
if (elapsed < DAY) return `${Math.floor(elapsed / HOUR)} hours ago`;
|
|
113
|
+
if (elapsed < 2 * DAY) return "yesterday";
|
|
114
|
+
if (elapsed < 30 * DAY) return `${Math.floor(elapsed / DAY)} days ago`;
|
|
115
|
+
if (elapsed < 60 * DAY) return "a month ago";
|
|
116
|
+
if (elapsed < 365 * DAY) return `${Math.floor(elapsed / (30 * DAY))} months ago`;
|
|
117
|
+
return `${Math.floor(elapsed / (365 * DAY))} years ago`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function scalarText(value: unknown): string {
|
|
121
|
+
if (typeof value === "string") return value;
|
|
122
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function objectEntries(value: Record<string, unknown>): Array<[string, string]> {
|
|
127
|
+
return Object.entries(value)
|
|
128
|
+
.filter(([, entry]) => entry !== null && entry !== undefined && entry !== "")
|
|
129
|
+
.map(([key, entry]) => [
|
|
130
|
+
key,
|
|
131
|
+
Array.isArray(entry)
|
|
132
|
+
? entry.map((item) => scalarText(item)).join(", ")
|
|
133
|
+
: typeof entry === "object" && entry !== null
|
|
134
|
+
? ""
|
|
135
|
+
: scalarText(entry),
|
|
136
|
+
] as [string, string])
|
|
137
|
+
.filter(([, text]) => text.length > 0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Renders a scalar or scalar list as one display line per item. */
|
|
141
|
+
export function leafLines(value: unknown): string[] {
|
|
142
|
+
if (value === null || value === undefined) return ["Not recorded."];
|
|
143
|
+
if (typeof value === "string") return value.length === 0 ? ["Not recorded."] : [value];
|
|
144
|
+
if (typeof value === "number" || typeof value === "boolean") return [String(value)];
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
if (value.length === 0) return ["None detected."];
|
|
147
|
+
return value
|
|
148
|
+
.map((item) => (typeof item === "object" && item !== null ? "" : scalarText(item)))
|
|
149
|
+
.filter((line) => line.length > 0);
|
|
150
|
+
}
|
|
151
|
+
if (typeof value === "object") {
|
|
152
|
+
const lines = objectEntries(value as Record<string, unknown>).map(
|
|
153
|
+
([key, text]) => `${humanize(key)}: ${text}`,
|
|
154
|
+
);
|
|
155
|
+
return lines.length === 0 ? ["Not recorded."] : lines;
|
|
156
|
+
}
|
|
157
|
+
return [String(value)];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function recordItems(leaf: Leaf | null | undefined): Array<Record<string, unknown>> {
|
|
161
|
+
if (leaf === null || leaf === undefined || !Array.isArray(leaf.value)) return [];
|
|
162
|
+
return leaf.value.filter(
|
|
163
|
+
(item): item is Record<string, unknown> =>
|
|
164
|
+
typeof item === "object" && item !== null && !Array.isArray(item),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function sectionEntries(section: unknown, hidden: string[]): Array<[string, Leaf]> {
|
|
169
|
+
if (typeof section !== "object" || section === null) return [];
|
|
170
|
+
return Object.entries(section).filter(
|
|
171
|
+
(entry): entry is [string, Leaf] => isLeaf(entry[1]) && !hidden.includes(entry[0]),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function sectionLeaf(section: unknown, key: string): Leaf | undefined {
|
|
176
|
+
if (typeof section !== "object" || section === null) return undefined;
|
|
177
|
+
const leaf = (section as Record<string, unknown>)[key];
|
|
178
|
+
return isLeaf(leaf) ? leaf : undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The text one unknown leaf carries. */
|
|
182
|
+
export function unknownText(leaf: Leaf): string {
|
|
183
|
+
return typeof leaf.reason === "string" && leaf.reason.length > 0
|
|
184
|
+
? `unknown — ${leaf.reason}`
|
|
185
|
+
: "unknown";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface RecordColumn {
|
|
189
|
+
key: string;
|
|
190
|
+
label: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** One table column per field any record carries, in first-seen order. */
|
|
194
|
+
export function recordColumns(records: Array<Record<string, unknown>>): RecordColumn[] {
|
|
195
|
+
const columns = new Map<string, string>();
|
|
196
|
+
for (const record of records) {
|
|
197
|
+
for (const [key, value] of Object.entries(record)) {
|
|
198
|
+
if (value === null || value === undefined || value === "") continue;
|
|
199
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
200
|
+
if (!columns.has(key)) columns.set(key, humanize(key));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return [...columns].map(([key, label]) => ({ key, label }));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function recordColumnValues(record: Record<string, unknown>, key: string): string[] {
|
|
207
|
+
const value = record[key];
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
return value.map((item) => scalarText(item)).filter((text) => text.length > 0);
|
|
210
|
+
}
|
|
211
|
+
const text = scalarText(value);
|
|
212
|
+
return text.length > 0 ? [text] : [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface PyramidTier {
|
|
216
|
+
level: string;
|
|
217
|
+
tests: string;
|
|
218
|
+
/** Test cases that exercise the level's primary success path. */
|
|
219
|
+
happyPath: string;
|
|
220
|
+
/** The recorded command that runs this level. */
|
|
221
|
+
command: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Pyramid tiers ordered from the base to the apex. */
|
|
225
|
+
export function pyramidTiers(leaf: Leaf | null | undefined): PyramidTier[] {
|
|
226
|
+
return recordItems(leaf)
|
|
227
|
+
.map((record) => ({
|
|
228
|
+
level: scalarText(record.level) || "other",
|
|
229
|
+
tests: scalarText(record.tests) || "unknown",
|
|
230
|
+
happyPath: scalarText(record.happyPath) || "unknown",
|
|
231
|
+
command: scalarText(record.command),
|
|
232
|
+
}))
|
|
233
|
+
.sort(
|
|
234
|
+
(left, right) =>
|
|
235
|
+
(PYRAMID_LEVELS.indexOf(left.level) + 1 || PYRAMID_LEVELS.length) -
|
|
236
|
+
(PYRAMID_LEVELS.indexOf(right.level) + 1 || PYRAMID_LEVELS.length),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Tiers that actually carry tests; a recorded level with zero tests is not implemented. */
|
|
241
|
+
export function implementedTiers(tiers: PyramidTier[]): PyramidTier[] {
|
|
242
|
+
return tiers.filter((tier) => (Number.parseInt(tier.tests, 10) || 0) > 0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Happy-path and total test counts, but only when every tier reports both: a
|
|
247
|
+
* partial sum would read as coverage the crawl never observed.
|
|
248
|
+
*/
|
|
249
|
+
export function pyramidHappyPathCount(tiers: PyramidTier[]): { happy: number; tests: number } | null {
|
|
250
|
+
let happy = 0;
|
|
251
|
+
let tests = 0;
|
|
252
|
+
for (const tier of tiers) {
|
|
253
|
+
const tierHappy = Number.parseInt(tier.happyPath, 10);
|
|
254
|
+
const tierTests = Number.parseInt(tier.tests, 10);
|
|
255
|
+
if (!Number.isFinite(tierHappy) || !Number.isFinite(tierTests)) return null;
|
|
256
|
+
happy += tierHappy;
|
|
257
|
+
tests += tierTests;
|
|
258
|
+
}
|
|
259
|
+
return tests > 0 ? { happy, tests } : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface CommandAlias {
|
|
263
|
+
command: string;
|
|
264
|
+
alias: string;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The installed invocation for a recorded command, when the manifest declares one. */
|
|
268
|
+
export function commandAlias(
|
|
269
|
+
aliases: readonly CommandAlias[] | undefined,
|
|
270
|
+
command: string,
|
|
271
|
+
): string {
|
|
272
|
+
if (aliases === undefined) return command;
|
|
273
|
+
const target = command.replace(/\s+/g, " ").trim();
|
|
274
|
+
return aliases.find((entry) => entry.command === target)?.alias ?? command;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** A bounded context's display name; never its raw identifier when a title exists. */
|
|
278
|
+
export function contextTitle(glossary: Glossary): string {
|
|
279
|
+
return labelText(glossary.titles) || humanize(glossary.boundedContext);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Sum of the numeric test counts a pyramid declares, or null when none is known. */
|
|
283
|
+
export function pyramidTestCount(tiers: PyramidTier[]): number | null {
|
|
284
|
+
let total = 0;
|
|
285
|
+
let known = false;
|
|
286
|
+
for (const tier of tiers) {
|
|
287
|
+
const count = Number.parseInt(tier.tests, 10);
|
|
288
|
+
if (!Number.isFinite(count)) continue;
|
|
289
|
+
known = true;
|
|
290
|
+
total += count;
|
|
291
|
+
}
|
|
292
|
+
return known ? total : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Usage for one concept; zeroes when the server has not measured it. */
|
|
296
|
+
export function conceptUsage(
|
|
297
|
+
usage: GlossaryUsage | undefined,
|
|
298
|
+
context: string,
|
|
299
|
+
id: string,
|
|
300
|
+
): ConceptUsage {
|
|
301
|
+
return usage?.[context]?.[id] ?? { count: 0, files: 0, examples: [] };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The score at or above which the page calls a concept significant. */
|
|
305
|
+
export const SIGNIFICANCE_FLOOR = 50;
|
|
306
|
+
|
|
307
|
+
/** The crawl's judgment for one concept, or undefined when it is unscored. */
|
|
308
|
+
export function conceptSignificance(
|
|
309
|
+
significance: GlossarySignificance | undefined,
|
|
310
|
+
context: string,
|
|
311
|
+
id: string,
|
|
312
|
+
): ConceptSignificance | undefined {
|
|
313
|
+
return significance?.[context]?.[id];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** True when at least one listed concept carries a crawl-authored score. */
|
|
317
|
+
export function glossaryHasSignificance(
|
|
318
|
+
glossaries: Glossary[],
|
|
319
|
+
significance: GlossarySignificance | undefined,
|
|
320
|
+
): boolean {
|
|
321
|
+
return glossaries.some((context) =>
|
|
322
|
+
(Array.isArray(context.concepts) ? context.concepts : []).some(
|
|
323
|
+
(concept) =>
|
|
324
|
+
concept.status !== "invalidated" &&
|
|
325
|
+
conceptSignificance(significance, context.boundedContext, concept.id) !== undefined,
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface GlossaryFilter {
|
|
331
|
+
search: string;
|
|
332
|
+
/** Hide a scored concept below the floor; ignored while nothing is scored. */
|
|
333
|
+
significantOnly: boolean;
|
|
334
|
+
floor: number;
|
|
335
|
+
significance?: GlossarySignificance;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function matchesSearch(concept: Concept, needle: string): boolean {
|
|
339
|
+
if (needle.length === 0) return true;
|
|
340
|
+
const haystack = [
|
|
341
|
+
concept.id,
|
|
342
|
+
labelText(concept.prefLabels),
|
|
343
|
+
...labelTexts(concept.altLabels),
|
|
344
|
+
...labelTexts(concept.definitions),
|
|
345
|
+
]
|
|
346
|
+
.join(" ")
|
|
347
|
+
.toLowerCase();
|
|
348
|
+
return haystack.includes(needle);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Terms matching the search and the significance floor, ordered by the crawl's
|
|
353
|
+
* score with unscored terms last. Usage never decides inclusion or order.
|
|
354
|
+
*/
|
|
355
|
+
export function filterContexts(glossaries: Glossary[], filter: GlossaryFilter): Glossary[] {
|
|
356
|
+
const needle = filter.search.trim().toLowerCase();
|
|
357
|
+
const floorActive =
|
|
358
|
+
filter.significantOnly && glossaryHasSignificance(glossaries, filter.significance);
|
|
359
|
+
return glossaries
|
|
360
|
+
.map((context) => ({
|
|
361
|
+
...context,
|
|
362
|
+
concepts: (Array.isArray(context.concepts) ? context.concepts : [])
|
|
363
|
+
.filter((concept) => concept.status !== "invalidated")
|
|
364
|
+
.filter((concept) => matchesSearch(concept, needle))
|
|
365
|
+
.filter((concept) => {
|
|
366
|
+
if (!floorActive) return true;
|
|
367
|
+
const entry = conceptSignificance(filter.significance, context.boundedContext, concept.id);
|
|
368
|
+
return entry === undefined || entry.score >= filter.floor;
|
|
369
|
+
})
|
|
370
|
+
.sort((left, right) => {
|
|
371
|
+
const leftScore = conceptSignificance(
|
|
372
|
+
filter.significance,
|
|
373
|
+
context.boundedContext,
|
|
374
|
+
left.id,
|
|
375
|
+
)?.score;
|
|
376
|
+
const rightScore = conceptSignificance(
|
|
377
|
+
filter.significance,
|
|
378
|
+
context.boundedContext,
|
|
379
|
+
right.id,
|
|
380
|
+
)?.score;
|
|
381
|
+
if (leftScore !== rightScore) {
|
|
382
|
+
if (leftScore === undefined) return 1;
|
|
383
|
+
if (rightScore === undefined) return -1;
|
|
384
|
+
return rightScore - leftScore;
|
|
385
|
+
}
|
|
386
|
+
return conceptTitle(left).localeCompare(conceptTitle(right));
|
|
387
|
+
}),
|
|
388
|
+
}))
|
|
389
|
+
.filter((context) => context.concepts.length > 0);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function conceptTitle(concept: Concept): string {
|
|
393
|
+
return labelText(concept.prefLabels) || concept.id;
|
|
394
|
+
}
|