codetrail-cli 1.4.1__py3-none-any.whl

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.
Files changed (171) hide show
  1. codetrail/__init__.py +3 -0
  2. codetrail/__main__.py +5 -0
  3. codetrail/agents/__init__.py +4 -0
  4. codetrail/agents/convert.py +92 -0
  5. codetrail/agents/export.py +284 -0
  6. codetrail/agents/instructions.py +42 -0
  7. codetrail/agents/protocol.py +111 -0
  8. codetrail/agents/tools.py +185 -0
  9. codetrail/assistant/__init__.py +161 -0
  10. codetrail/assistant/claude_code.py +330 -0
  11. codetrail/assistant/codex.py +260 -0
  12. codetrail/assistant/environment.py +66 -0
  13. codetrail/assistant/estimate.py +225 -0
  14. codetrail/assistant/fake.py +74 -0
  15. codetrail/assistant/guard.py +80 -0
  16. codetrail/assistant/guard_hook.py +58 -0
  17. codetrail/assistant/local.py +245 -0
  18. codetrail/assistant/local_tools.py +177 -0
  19. codetrail/assistant/prompts.py +404 -0
  20. codetrail/assistant/routing.py +86 -0
  21. codetrail/assistant/runner.py +120 -0
  22. codetrail/assistant/status.py +186 -0
  23. codetrail/assistant/usage.py +93 -0
  24. codetrail/bridge.py +292 -0
  25. codetrail/cli.py +407 -0
  26. codetrail/config.py +485 -0
  27. codetrail/database/__init__.py +60 -0
  28. codetrail/database/migrations/0001_facts.sql +34 -0
  29. codetrail/database/migrations/0002_settings.sql +5 -0
  30. codetrail/database/migrations/0003_learning.sql +34 -0
  31. codetrail/database/migrations/0004_assistant_usage.sql +24 -0
  32. codetrail/database/migrations/0005_page_failures.sql +7 -0
  33. codetrail/database/migrations/0006_metric_values.sql +9 -0
  34. codetrail/errors.py +5 -0
  35. codetrail/extract/__init__.py +249 -0
  36. codetrail/extract/adr.py +98 -0
  37. codetrail/extract/github_actions.py +140 -0
  38. codetrail/extract/openapi.py +101 -0
  39. codetrail/extract/python.py +221 -0
  40. codetrail/extract/swift.py +204 -0
  41. codetrail/extract/terraform.py +166 -0
  42. codetrail/extract/typescript.py +464 -0
  43. codetrail/facts/__init__.py +112 -0
  44. codetrail/facts/store.py +209 -0
  45. codetrail/generate/__init__.py +1 -0
  46. codetrail/generate/failures.py +31 -0
  47. codetrail/generate/outline.py +159 -0
  48. codetrail/generate/revise.py +72 -0
  49. codetrail/generate/run.py +530 -0
  50. codetrail/generate/scope.py +135 -0
  51. codetrail/generate/validate.py +218 -0
  52. codetrail/guide/__init__.py +147 -0
  53. codetrail/learn/__init__.py +143 -0
  54. codetrail/learn/routes.py +139 -0
  55. codetrail/locales/ar/LC_MESSAGES/codetrail.mo +0 -0
  56. codetrail/locales/ar/LC_MESSAGES/codetrail.po +2236 -0
  57. codetrail/locales/codetrail.pot +1942 -0
  58. codetrail/lock.py +114 -0
  59. codetrail/metrics/__init__.py +6 -0
  60. codetrail/metrics/activity.py +96 -0
  61. codetrail/metrics/churn.py +156 -0
  62. codetrail/metrics/commits.py +45 -0
  63. codetrail/metrics/coverage.py +114 -0
  64. codetrail/metrics/decisions.py +77 -0
  65. codetrail/metrics/history.py +47 -0
  66. codetrail/metrics/inventory.py +63 -0
  67. codetrail/metrics/rationale.py +74 -0
  68. codetrail/metrics/releases.py +26 -0
  69. codetrail/metrics/report.py +111 -0
  70. codetrail/metrics/repository.py +128 -0
  71. codetrail/metrics/size.py +95 -0
  72. codetrail/py.typed +0 -0
  73. codetrail/remove.py +50 -0
  74. codetrail/repo/__init__.py +4 -0
  75. codetrail/repo/git.py +56 -0
  76. codetrail/repo/gitleaks.toml +6 -0
  77. codetrail/repo/history.py +262 -0
  78. codetrail/repo/mirror.py +137 -0
  79. codetrail/repo/refresh.py +41 -0
  80. codetrail/repo/rules.py +83 -0
  81. codetrail/repo/secrets.py +145 -0
  82. codetrail/repo/signal.py +65 -0
  83. codetrail/repo/source.py +166 -0
  84. codetrail/search.py +178 -0
  85. codetrail/server.py +45 -0
  86. codetrail/system.py +484 -0
  87. codetrail/update.py +276 -0
  88. codetrail/web/__init__.py +1 -0
  89. codetrail/web/app.py +897 -0
  90. codetrail/web/diagrams.py +414 -0
  91. codetrail/web/documentation.py +146 -0
  92. codetrail/web/i18n.py +44 -0
  93. codetrail/web/navigation.py +127 -0
  94. codetrail/web/render.py +126 -0
  95. codetrail/web/repository_view.py +230 -0
  96. codetrail/web/security.py +171 -0
  97. codetrail/web/static/brand/favicon.svg +1 -0
  98. codetrail/web/static/brand/mark.svg +1 -0
  99. codetrail/web/static/css/base.css +192 -0
  100. codetrail/web/static/css/components.css +2254 -0
  101. codetrail/web/static/css/content.css +310 -0
  102. codetrail/web/static/css/layout.css +410 -0
  103. codetrail/web/static/css/print.css +42 -0
  104. codetrail/web/static/css/tokens.css +228 -0
  105. codetrail/web/static/js/api.js +49 -0
  106. codetrail/web/static/js/ask.js +182 -0
  107. codetrail/web/static/js/d2-worker.js +34 -0
  108. codetrail/web/static/js/d2.js +28 -0
  109. codetrail/web/static/js/diagram.js +52 -0
  110. codetrail/web/static/js/documentation.js +24 -0
  111. codetrail/web/static/js/learning.js +47 -0
  112. codetrail/web/static/js/main.js +38 -0
  113. codetrail/web/static/js/outline.js +44 -0
  114. codetrail/web/static/js/palette.js +211 -0
  115. codetrail/web/static/js/repository.js +528 -0
  116. codetrail/web/static/js/resize.js +126 -0
  117. codetrail/web/static/js/shortcuts.js +87 -0
  118. codetrail/web/static/js/sidebar.js +194 -0
  119. codetrail/web/static/js/theme-init.js +27 -0
  120. codetrail/web/static/js/theme.js +42 -0
  121. codetrail/web/static/js/update.js +107 -0
  122. codetrail/web/static/vendor/d2/LICENSE +375 -0
  123. codetrail/web/static/vendor/d2/LICENSE-elk +263 -0
  124. codetrail/web/static/vendor/d2/LICENSE-wasm_exec +27 -0
  125. codetrail/web/static/vendor/d2/VERSION +12 -0
  126. codetrail/web/static/vendor/d2/d2.wasm +0 -0
  127. codetrail/web/static/vendor/d2/elk.js +105914 -0
  128. codetrail/web/static/vendor/d2/wasm_exec.js +656 -0
  129. codetrail/web/static/vendor/inter/InterVariable-Italic.woff2 +0 -0
  130. codetrail/web/static/vendor/inter/InterVariable.woff2 +0 -0
  131. codetrail/web/static/vendor/inter/LICENSE.txt +92 -0
  132. codetrail/web/static/vendor/inter/VERSION +6 -0
  133. codetrail/web/static/vendor/noto-sans-arabic/NotoSansArabic-wght.woff2 +0 -0
  134. codetrail/web/static/vendor/noto-sans-arabic/OFL.txt +93 -0
  135. codetrail/web/static/vendor/noto-sans-arabic/VERSION +12 -0
  136. codetrail/web/target_view.py +63 -0
  137. codetrail/web/templates/answers.html +25 -0
  138. codetrail/web/templates/area.html +46 -0
  139. codetrail/web/templates/base.html +37 -0
  140. codetrail/web/templates/decisions.html +30 -0
  141. codetrail/web/templates/diagram.html +10 -0
  142. codetrail/web/templates/digests.html +24 -0
  143. codetrail/web/templates/documentation.html +247 -0
  144. codetrail/web/templates/error.html +9 -0
  145. codetrail/web/templates/estimate.html +48 -0
  146. codetrail/web/templates/fact.html +29 -0
  147. codetrail/web/templates/home.html +196 -0
  148. codetrail/web/templates/layout/ask_panel.html +27 -0
  149. codetrail/web/templates/layout/header.html +32 -0
  150. codetrail/web/templates/layout/icons.html +27 -0
  151. codetrail/web/templates/layout/outline.html +4 -0
  152. codetrail/web/templates/layout/palette.html +27 -0
  153. codetrail/web/templates/layout/shortcuts.html +27 -0
  154. codetrail/web/templates/layout/sidebar.html +79 -0
  155. codetrail/web/templates/layout/update_dialog.html +18 -0
  156. codetrail/web/templates/macros.html +13 -0
  157. codetrail/web/templates/page.html +93 -0
  158. codetrail/web/templates/progress.html +46 -0
  159. codetrail/web/templates/repository.html +369 -0
  160. codetrail/web/templates/search.html +31 -0
  161. codetrail/web/templates/source.html +15 -0
  162. codetrail/web/templates/system.html +43 -0
  163. codetrail/web/templates/system_arrows.html +17 -0
  164. codetrail/web/templates/system_legend.html +9 -0
  165. codetrail/web/templates/update_log.html +30 -0
  166. codetrail/web/trend.py +22 -0
  167. codetrail_cli-1.4.1.dist-info/METADATA +372 -0
  168. codetrail_cli-1.4.1.dist-info/RECORD +171 -0
  169. codetrail_cli-1.4.1.dist-info/WHEEL +4 -0
  170. codetrail_cli-1.4.1.dist-info/entry_points.txt +3 -0
  171. codetrail_cli-1.4.1.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,42 @@
1
+ /* Printing a page: the content alone, in black on white. */
2
+
3
+ .topbar,
4
+ .sidebar,
5
+ .sidebar-edge,
6
+ .resize-handle,
7
+ .outline,
8
+ .ask-panel,
9
+ .update-panel,
10
+ .skip-link,
11
+ .pager,
12
+ .checks,
13
+ .page-status button,
14
+ .diagram-tools {
15
+ display: none !important;
16
+ }
17
+
18
+ .shell {
19
+ display: block;
20
+ }
21
+
22
+ .content {
23
+ padding: 0;
24
+ }
25
+
26
+ /* The page's card is the screen's frame: on paper there is only the page. */
27
+ .page-area {
28
+ margin: 0;
29
+ padding: 0;
30
+ border-radius: 0;
31
+ box-shadow: none;
32
+ }
33
+
34
+ body {
35
+ background: #ffffff;
36
+ color: #000000;
37
+ }
38
+
39
+ a {
40
+ color: inherit;
41
+ text-decoration: underline;
42
+ }
@@ -0,0 +1,228 @@
1
+ /* Codetrail's design tokens (design section 16.2): every color, size and space the page uses.
2
+ Light by default; dark follows the system unless the reader chose a theme ([data-theme] on <html>). */
3
+
4
+ @font-face {
5
+ font-family: "Inter";
6
+ font-style: normal;
7
+ font-weight: 100 900;
8
+ font-display: swap;
9
+ src: url("/static/vendor/inter/InterVariable.woff2") format("woff2");
10
+ }
11
+
12
+ @font-face {
13
+ font-family: "Inter";
14
+ font-style: italic;
15
+ font-weight: 100 900;
16
+ font-display: swap;
17
+ src: url("/static/vendor/inter/InterVariable-Italic.woff2") format("woff2");
18
+ }
19
+
20
+ /* Arabic letters, which Inter doesn't have (ADR 0014); loaded only by a page that shows some. */
21
+ @font-face {
22
+ font-family: "Noto Sans Arabic";
23
+ font-style: normal;
24
+ font-weight: 400 700;
25
+ font-display: swap;
26
+ src: url("/static/vendor/noto-sans-arabic/NotoSansArabic-wght.woff2") format("woff2");
27
+ unicode-range: U+0600-06FF, U+0750-077F, U+0870-08FF, U+200C-200F, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF,
28
+ U+FE70-FEFF, U+10E60-10E7F;
29
+ }
30
+
31
+ :root {
32
+ color-scheme: light;
33
+ --font-sans: "Inter", "Noto Sans Arabic", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
34
+ --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
35
+
36
+ --accent: #5b5bd6;
37
+ --accent-hover: #4b4bc4;
38
+ --accent-text: #4a48c0;
39
+ --accent-tint: #efeffd;
40
+ --on-accent: #ffffff;
41
+
42
+ --text: #1a1f36;
43
+ --text-muted: #5c6378;
44
+ --text-faint: #646b7f;
45
+
46
+ --page: #ffffff;
47
+ --sidebar: #f7f8fb;
48
+ --card: #f5f6fa;
49
+ --raised: #ffffff;
50
+ --hover: #eef0f6;
51
+
52
+ --line: #e3e6ee;
53
+ --line-strong: #cfd4e0;
54
+
55
+ --documented: #5b5bd6;
56
+ --documented-tint: #f3f3fe;
57
+ --inferred: #b26a00;
58
+ --inferred-tint: #fdf6ea;
59
+ --inferred-text: #8a5200;
60
+ --learned: #18774d;
61
+ --learned-tint: #e8f6ef;
62
+ --danger: #c0392b;
63
+ --mark: #fff1b8;
64
+
65
+ /* Charts (design section 20.3): four categorical colors checked for color-blind separation, gray for Other, and
66
+ one indigo ramp with the ink that reads on each step. Amber stays the inferred color only. */
67
+ --series-1: #5b5bd6;
68
+ --series-2: #e87ba4;
69
+ --series-3: #2a78d6;
70
+ --series-4: #008300;
71
+ --series-other: #8b92a8;
72
+ --heat-0: #ebecfa;
73
+ --heat-1: #d0d2f6;
74
+ --heat-2: #aeb0ee;
75
+ --heat-3: #7f81e1;
76
+ --heat-4: #4b4bc4;
77
+ --heat-ink-0: #1a1f36;
78
+ --heat-ink-1: #1a1f36;
79
+ --heat-ink-2: #1a1f36;
80
+ --heat-ink-3: #1a1f36;
81
+ --heat-ink-4: #ffffff;
82
+ --chart-band: rgb(91 91 214 / 0.07);
83
+
84
+ /* The home page's metric cards (design section 16.2): the system blue and the repository pink, from the chart
85
+ series, each with a darker shade for its text. Indigo is documentation's, green learned's, amber inferred's. */
86
+ --system: var(--series-3);
87
+ --system-text: #1f62b5;
88
+ --repository: var(--series-2);
89
+ --repository-text: #b23a6a;
90
+
91
+ --shadow-float: 0 12px 32px rgb(16 24 64 / 0.14), 0 2px 6px rgb(16 24 64 / 0.08);
92
+ --shadow-card: 0 1px 2px rgb(16 24 64 / 0.06), 0 0 0 1px var(--line); /* the page's card in the frame */
93
+ --shadow-pill: 0 1px 2px rgb(16 24 64 / 0.1), 0 0 0 1px var(--line); /* the sidebar's current row and tile */
94
+ --focus: 0 0 0 3px rgb(91 91 214 / 0.35);
95
+
96
+ --space-1: 4px;
97
+ --space-2: 8px;
98
+ --space-3: 12px;
99
+ --space-4: 16px;
100
+ --space-5: 20px;
101
+ --space-6: 24px;
102
+ --space-8: 32px;
103
+ --space-10: 40px;
104
+ --space-12: 48px;
105
+
106
+ --radius: 8px;
107
+ --radius-card: 12px;
108
+
109
+ --header-height: 60px;
110
+ --sidebar-width: 272px;
111
+ --outline-width: 224px;
112
+ --panel-width: 420px;
113
+ --article-width: 46rem;
114
+ --wide-width: 72rem;
115
+
116
+ --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
117
+ --duration: 180ms;
118
+ }
119
+
120
+ @media (prefers-color-scheme: dark) {
121
+ :root:not([data-theme="light"]) {
122
+ color-scheme: dark;
123
+ --accent: #9b9bf5;
124
+ --accent-hover: #b1b1f8;
125
+ --accent-text: #b4b4f9;
126
+ --accent-tint: #23264a;
127
+ --on-accent: #0f1220;
128
+ --text: #e6e8f0;
129
+ --text-muted: #a1a8bd;
130
+ --text-faint: #8b92a8;
131
+ --page: #0f1220;
132
+ --sidebar: #141829;
133
+ --card: #191e31;
134
+ --raised: #1b2035;
135
+ --hover: #22283f;
136
+ --line: #262c44;
137
+ --line-strong: #353c58;
138
+ --documented: #9b9bf5;
139
+ --documented-tint: #1c1f3c;
140
+ --inferred: #f0b255;
141
+ --inferred-tint: #2a2213;
142
+ --inferred-text: #f0b255;
143
+ --learned: #5fd39b;
144
+ --learned-tint: #12291f;
145
+ --danger: #ff8a7a;
146
+ --mark: #4a3f12;
147
+ --shadow-float: 0 16px 40px rgb(0 0 0 / 0.5), 0 2px 8px rgb(0 0 0 / 0.4);
148
+ --shadow-card: 0 1px 2px rgb(0 0 0 / 0.4), 0 0 0 1px var(--line);
149
+ --shadow-pill: 0 1px 2px rgb(0 0 0 / 0.5), 0 0 0 1px var(--line);
150
+ --focus: 0 0 0 3px rgb(155 155 245 / 0.45);
151
+
152
+ --series-1: #7c7cee;
153
+ --series-2: #d55181;
154
+ --series-3: #3987e5;
155
+ --series-4: #2b8a2b;
156
+ --series-other: #646b7f;
157
+ --heat-0: #222842;
158
+ --heat-1: #2f3567;
159
+ --heat-2: #43479a;
160
+ --heat-3: #7b7de0;
161
+ --heat-4: #9b9bf5;
162
+ --heat-ink-0: #e6e8f0;
163
+ --heat-ink-1: #e6e8f0;
164
+ --heat-ink-2: #e6e8f0;
165
+ --heat-ink-3: #0f1220;
166
+ --heat-ink-4: #0f1220;
167
+ --chart-band: rgb(155 155 245 / 0.08);
168
+ --system-text: #79b0f2;
169
+ --repository-text: #f08cb3;
170
+ }
171
+ }
172
+
173
+ :root[data-theme="dark"] {
174
+ color-scheme: dark;
175
+ --accent: #9b9bf5;
176
+ --accent-hover: #b1b1f8;
177
+ --accent-text: #b4b4f9;
178
+ --accent-tint: #23264a;
179
+ --on-accent: #0f1220;
180
+ --text: #e6e8f0;
181
+ --text-muted: #a1a8bd;
182
+ --text-faint: #8b92a8;
183
+ --page: #0f1220;
184
+ --sidebar: #141829;
185
+ --card: #191e31;
186
+ --raised: #1b2035;
187
+ --hover: #22283f;
188
+ --line: #262c44;
189
+ --line-strong: #353c58;
190
+ --documented: #9b9bf5;
191
+ --documented-tint: #1c1f3c;
192
+ --inferred: #f0b255;
193
+ --inferred-tint: #2a2213;
194
+ --inferred-text: #f0b255;
195
+ --learned: #5fd39b;
196
+ --learned-tint: #12291f;
197
+ --danger: #ff8a7a;
198
+ --mark: #4a3f12;
199
+ --shadow-float: 0 16px 40px rgb(0 0 0 / 0.5), 0 2px 8px rgb(0 0 0 / 0.4);
200
+ --shadow-card: 0 1px 2px rgb(0 0 0 / 0.4), 0 0 0 1px var(--line);
201
+ --shadow-pill: 0 1px 2px rgb(0 0 0 / 0.5), 0 0 0 1px var(--line);
202
+ --focus: 0 0 0 3px rgb(155 155 245 / 0.45);
203
+
204
+ --series-1: #7c7cee;
205
+ --series-2: #d55181;
206
+ --series-3: #3987e5;
207
+ --series-4: #2b8a2b;
208
+ --series-other: #646b7f;
209
+ --heat-0: #222842;
210
+ --heat-1: #2f3567;
211
+ --heat-2: #43479a;
212
+ --heat-3: #7b7de0;
213
+ --heat-4: #9b9bf5;
214
+ --heat-ink-0: #e6e8f0;
215
+ --heat-ink-1: #e6e8f0;
216
+ --heat-ink-2: #e6e8f0;
217
+ --heat-ink-3: #0f1220;
218
+ --heat-ink-4: #0f1220;
219
+ --chart-band: rgb(155 155 245 / 0.08);
220
+ --system-text: #79b0f2;
221
+ --repository-text: #f08cb3;
222
+ }
223
+
224
+ @media (prefers-reduced-motion: reduce) {
225
+ :root {
226
+ --duration: 0ms;
227
+ }
228
+ }
@@ -0,0 +1,49 @@
1
+ // Talking to Codetrail's server: every write and every JSON read carries the session's token (design 7.4 and 16.3).
2
+
3
+ const token = document.querySelector('meta[name="codetrail-token"]')?.content ?? "";
4
+
5
+ export async function post(url, data) {
6
+ return fetch(url, {
7
+ method: "POST",
8
+ headers: { "X-Codetrail-Token": token, "Content-Type": "application/json" },
9
+ body: JSON.stringify(data),
10
+ credentials: "same-origin",
11
+ });
12
+ }
13
+
14
+ export async function getJSON(url, options = {}) {
15
+ const response = await fetch(url, {
16
+ headers: { "X-Codetrail-Token": token, Accept: "application/json" },
17
+ credentials: "same-origin",
18
+ cache: "no-store",
19
+ signal: options.signal,
20
+ });
21
+ if (!response.ok) throw new Error(`${response.status}`);
22
+ return response.json();
23
+ }
24
+
25
+ // What a call actually used, beside its estimate on the button (design section 15.4).
26
+ export function usedText(labels, usage) {
27
+ if (!usage) return "";
28
+ const tokens = usage.tokens >= 1000 ? `${Math.round(usage.tokens / 1000)}k` : `${usage.tokens}`;
29
+ const cost = usage.cost_usd ? ` · $${usage.cost_usd.toFixed(2)}` : "";
30
+ return `${labels.dataset.labelUsed} ${tokens} ${labels.dataset.labelTokens}${cost}`;
31
+ }
32
+
33
+ // Session and local storage can be missing or refuse (private windows, blocked site data); the page works without.
34
+ export function remember(storage, key, value) {
35
+ try {
36
+ if (value === null) window[storage].removeItem(key);
37
+ else window[storage].setItem(key, value);
38
+ } catch {
39
+ // nothing to do: the choice just isn't remembered
40
+ }
41
+ }
42
+
43
+ export function recall(storage, key) {
44
+ try {
45
+ return window[storage].getItem(key);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
@@ -0,0 +1,182 @@
1
+ // The Ask panel (design section 16.4): this session's answers, a question about the page being read, and saving
2
+ // answers to the guide. A question is sent only by the Send button, which shows its estimate; nothing else spends.
3
+ import { getJSON, post, recall, remember, usedText } from "./api.js";
4
+ import { drawDiagramsIn } from "./diagram.js";
5
+ import { hideUpdatePanel } from "./update.js";
6
+
7
+ const panel = document.querySelector("[data-ask]");
8
+ const thread = panel?.querySelector("[data-ask-thread]");
9
+ const intro = panel?.querySelector("[data-ask-intro]");
10
+ const form = panel?.querySelector("[data-ask-form]");
11
+ const status = panel?.querySelector("[data-ask-status]");
12
+ let loaded = false;
13
+ let opener = null;
14
+
15
+ function item(question, answerLanguage) {
16
+ const article = document.createElement("article");
17
+ article.className = "ask-item";
18
+ const asked = document.createElement("p");
19
+ asked.className = "ask-question";
20
+ asked.textContent = question;
21
+ const answer = document.createElement("div");
22
+ answer.className = "ask-answer";
23
+ answer.lang = answerLanguage;
24
+ answer.dir = "auto";
25
+ const actions = document.createElement("div");
26
+ actions.className = "ask-actions";
27
+ article.append(asked, answer, actions);
28
+ intro.hidden = true;
29
+ thread.append(article);
30
+ return { article, answer, actions };
31
+ }
32
+
33
+ function addSaveButton(actions, answerId, question) {
34
+ const button = document.createElement("button");
35
+ button.type = "button";
36
+ button.className = "button small";
37
+ button.dataset.saveAnswer = "";
38
+ button.textContent = panel.dataset.labelSave;
39
+ button.addEventListener("click", async () => {
40
+ button.disabled = true;
41
+ const response = await post(`/bridge/answers/${encodeURIComponent(answerId)}/save`, {});
42
+ const result = await response.json().catch(() => ({}));
43
+ if (!response.ok) {
44
+ button.disabled = false;
45
+ const problem = document.createElement("span");
46
+ problem.textContent = result.error ?? panel.dataset.labelSaveFailed;
47
+ actions.append(problem);
48
+ return;
49
+ }
50
+ const link = document.createElement("a");
51
+ link.href = `/pages/${result.page_id}`;
52
+ link.textContent = panel.dataset.labelSaved;
53
+ button.replaceWith(link);
54
+ addToSidebar(result.page_id, question);
55
+ });
56
+ actions.prepend(button);
57
+ }
58
+
59
+ function addToSidebar(pageId, question) {
60
+ const list = document.querySelector("[data-nav-answers]");
61
+ if (!list) return;
62
+ list.querySelector("[data-nav-empty]")?.remove();
63
+ const entry = document.createElement("li");
64
+ const link = document.createElement("a");
65
+ link.className = "nav-link";
66
+ link.href = `/pages/${pageId}`;
67
+ const label = document.createElement("span");
68
+ label.className = "label";
69
+ label.textContent = question;
70
+ link.append(label);
71
+ entry.append(link);
72
+ list.prepend(entry);
73
+ }
74
+
75
+ async function loadSessionAnswers() {
76
+ if (loaded) return;
77
+ loaded = true;
78
+ try {
79
+ const { answers } = await getJSON("/bridge/answers");
80
+ for (const answer of answers) {
81
+ const { answer: body, actions } = item(answer.question, panel.dataset.language);
82
+ body.innerHTML = answer.html; // rendered and sanitized on the server, raw HTML disabled
83
+ drawDiagramsIn(body);
84
+ addSaveButton(actions, answer.id, answer.question);
85
+ }
86
+ thread.scrollTop = thread.scrollHeight;
87
+ } catch {
88
+ loaded = false; // try again next time the panel opens
89
+ }
90
+ }
91
+
92
+ export function isAskOpen() {
93
+ return Boolean(panel && !panel.hidden);
94
+ }
95
+
96
+ export function openAsk(question, focus = true) {
97
+ if (!panel) return;
98
+ if (focus) opener ??= document.activeElement;
99
+ hideUpdatePanel(); // Ask takes the side; the Update button shows a running update again
100
+ panel.hidden = false;
101
+ document.body.classList.add("ask-open");
102
+ remember("sessionStorage", "codetrail.ask", "open");
103
+ loadSessionAnswers();
104
+ const field = form.elements.question;
105
+ if (typeof question === "string" && question) field.value = question;
106
+ if (focus) field.focus();
107
+ }
108
+
109
+ export function closeAsk() {
110
+ if (!panel || panel.hidden) return;
111
+ panel.hidden = true;
112
+ document.body.classList.remove("ask-open");
113
+ remember("sessionStorage", "codetrail.ask", null);
114
+ if (opener instanceof HTMLElement) opener.focus();
115
+ opener = null;
116
+ }
117
+
118
+ async function send(event) {
119
+ event.preventDefault();
120
+ const question = form.elements.question.value.trim();
121
+ if (!question) return;
122
+ const button = form.querySelector("button[type=submit]");
123
+ button.disabled = true;
124
+ status.textContent = status.dataset.labelReading;
125
+ const { answer, actions } = item(question, panel.dataset.language);
126
+ thread.scrollTop = thread.scrollHeight;
127
+ const body = { question };
128
+ if (panel.dataset.pageId) body.page_id = panel.dataset.pageId;
129
+ try {
130
+ const response = await post("/bridge/questions", body);
131
+ if (!response.ok) {
132
+ const problem = await response.json().catch(() => ({}));
133
+ answer.textContent = problem.error ?? `The question was refused (${response.status}).`;
134
+ status.textContent = "";
135
+ return;
136
+ }
137
+ form.elements.question.value = "";
138
+ const reader = response.body.getReader();
139
+ const decoder = new TextDecoder();
140
+ let buffer = "";
141
+ for (;;) {
142
+ const { value, done } = await reader.read();
143
+ if (done) break;
144
+ buffer += decoder.decode(value, { stream: true });
145
+ let newline;
146
+ while ((newline = buffer.indexOf("\n")) >= 0) {
147
+ const line = buffer.slice(0, newline);
148
+ buffer = buffer.slice(newline + 1);
149
+ if (!line.trim()) continue;
150
+ const message = JSON.parse(line);
151
+ if (message.type === "text") {
152
+ answer.textContent += message.text; // plain text while it streams
153
+ status.textContent = "";
154
+ } else if (message.type === "done") {
155
+ answer.innerHTML = message.html; // rendered on the server with raw HTML disabled
156
+ drawDiagramsIn(answer); // diagrams drawn from facts, as on the guide's pages
157
+ addSaveButton(actions, message.answer_id, question);
158
+ const used = document.createElement("span");
159
+ used.textContent = usedText(status, message.usage);
160
+ actions.append(used);
161
+ status.textContent = "";
162
+ } else if (message.type === "error") {
163
+ answer.textContent = message.message; // nothing of a failed or withheld answer stays on the page
164
+ status.textContent = "";
165
+ }
166
+ thread.scrollTop = thread.scrollHeight;
167
+ }
168
+ }
169
+ } finally {
170
+ button.disabled = false;
171
+ }
172
+ }
173
+
174
+ export function setUpAsk() {
175
+ if (!panel) return;
176
+ form.addEventListener("submit", send);
177
+ for (const button of document.querySelectorAll("[data-ask-open]")) {
178
+ button.addEventListener("click", () => openAsk());
179
+ }
180
+ panel.querySelector("[data-ask-close]")?.addEventListener("click", closeAsk);
181
+ if (recall("sessionStorage", "codetrail.ask") === "open") openAsk(undefined, false); // reopened, not focused
182
+ }
@@ -0,0 +1,34 @@
1
+ // D2's WebAssembly build, in a worker of its own (ADR 0016). Its ELK layout evaluates JavaScript from strings, so
2
+ // this file is served with its own policy: eval is allowed here, but there is no DOM and no network but d2.wasm.
3
+ // It gets D2 text Codetrail wrote from facts and answers with an SVG, which the page shows only as an image.
4
+ // Diagrams are laid out by ELK (ADR 0018): D2 calls the elkLayoutSync that elk.js defines, which needs an ELK
5
+ // instance to exist first. D2.js also runs a setup.js, which isn't loaded here: it would replace setTimeout.
6
+ importScripts("/static/vendor/d2/wasm_exec.js", "/static/vendor/d2/elk.js");
7
+ new ELK();
8
+
9
+ const ready = (async () => {
10
+ const go = new Go();
11
+ const { instance } = await WebAssembly.instantiateStreaming(fetch("/static/vendor/d2/d2.wasm"), go.importObject);
12
+ go.run(instance); // D2's main keeps running and sets self.d2
13
+ return self.d2;
14
+ })();
15
+
16
+ function data(response) {
17
+ const parsed = JSON.parse(response);
18
+ if (parsed.error) throw new Error(parsed.error.message);
19
+ return parsed.data;
20
+ }
21
+
22
+ self.onmessage = async ({ data: { id, source, dark } }) => {
23
+ try {
24
+ const d2 = await ready;
25
+ const compiled = data(await d2.compile(JSON.stringify({ fs: { index: source }, options: { layout: "elk" } })));
26
+ // At its natural size (scale 1): the page's CSS shrinks a wide diagram to fit, and a small one isn't blown up.
27
+ const options = { ...compiled.renderOptions, themeID: dark ? 200 : 0, pad: 16, scale: 1, noXMLTag: true };
28
+ const encoded = data(await d2.render(JSON.stringify({ diagram: compiled.diagram, options })));
29
+ const svg = new TextDecoder().decode(Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0)));
30
+ self.postMessage({ id, svg });
31
+ } catch (error) {
32
+ self.postMessage({ id, error: String(error?.message ?? error) });
33
+ }
34
+ };
@@ -0,0 +1,28 @@
1
+ // Draws D2 text as an SVG in d2-worker.js, whose own policy allows the eval D2's layout needs; the page's never does
2
+ // (ADR 0016). One worker serves every diagram on the page.
3
+ let worker;
4
+ let next = 0;
5
+ const waiting = new Map();
6
+
7
+ function start() {
8
+ worker = new Worker("/static/js/d2-worker.js");
9
+ worker.onmessage = ({ data }) => {
10
+ const request = waiting.get(data.id);
11
+ waiting.delete(data.id);
12
+ if (data.error === undefined) request?.resolve(data.svg);
13
+ else request?.reject(new Error(data.error));
14
+ };
15
+ worker.onerror = () => {
16
+ for (const request of waiting.values()) request.reject(new Error("D2's worker failed"));
17
+ waiting.clear();
18
+ };
19
+ }
20
+
21
+ export function renderD2(source, dark) {
22
+ if (!worker) start();
23
+ return new Promise((resolve, reject) => {
24
+ const id = next++;
25
+ waiting.set(id, { resolve, reject });
26
+ worker.postMessage({ id, source, dark });
27
+ });
28
+ }
@@ -0,0 +1,52 @@
1
+ // Diagrams drawn from facts, written in D2 and drawn by D2's WebAssembly build in its own worker (ADR 0016), in the
2
+ // page's light or dark theme. Pages draw theirs on load; the Ask panel draws an answer's when it arrives.
3
+ import { renderD2 } from "./d2.js";
4
+ import { isDark } from "./theme.js";
5
+
6
+ const figures = [];
7
+
8
+ // D2's SVG is shown as an image, where it can't run script or follow links, whatever the worker sent.
9
+ async function draw(figure) {
10
+ const source = figure.dataset.source;
11
+ const target = figure.querySelector("[data-diagram-target]");
12
+ if (!source || !target) return;
13
+ try {
14
+ const image = document.createElement("img");
15
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(await renderD2(source, isDark()))}`;
16
+ image.alt = figure.dataset.alt ?? "";
17
+ target.replaceChildren(image);
18
+ target.hidden = false;
19
+ figure.querySelector("pre.diagram")?.setAttribute("hidden", "");
20
+ } catch {
21
+ figure.classList.add("diagram-failed");
22
+ }
23
+ }
24
+
25
+ function prepare(figure) {
26
+ const block = figure.querySelector("pre.diagram");
27
+ if (!block || figure.dataset.source !== undefined) return false;
28
+ figure.dataset.source = block.textContent;
29
+ const target = document.createElement("div");
30
+ target.className = "diagram-svg";
31
+ target.dataset.diagramTarget = "";
32
+ target.hidden = true;
33
+ block.after(target);
34
+ figure.querySelector("[data-diagram-enlarge]")?.addEventListener("click", (event) => {
35
+ const enlarged = figure.classList.toggle("enlarged");
36
+ event.currentTarget.setAttribute("aria-pressed", String(enlarged));
37
+ });
38
+ figures.push(figure);
39
+ return true;
40
+ }
41
+
42
+ export async function drawDiagramsIn(root) {
43
+ const found = [...root.querySelectorAll("figure.diagram-block")].filter(prepare);
44
+ for (const figure of found) await draw(figure);
45
+ }
46
+
47
+ export function setUpDiagrams() {
48
+ drawDiagramsIn(document);
49
+ document.addEventListener("codetrail:theme", async () => {
50
+ for (const figure of figures.filter((each) => each.isConnected)) await draw(figure);
51
+ });
52
+ }
@@ -0,0 +1,24 @@
1
+ // The Documentation page's filter chips (design section 21.2): each shows one kind of item, in its list and in the
2
+ // list's folded rest. The server writes every count and label; this sets only hidden, aria-pressed and the live
3
+ // region's words, copied from the chip. Without it, every item shows.
4
+ export function setUpDocumentation() {
5
+ for (const area of document.querySelectorAll("[data-filterable]")) {
6
+ const group = area.querySelector("[data-filter]");
7
+ if (!group) continue;
8
+ const status = area.querySelector("[data-filter-status]");
9
+ const buttons = [...group.querySelectorAll("[data-show]")];
10
+ group.hidden = false;
11
+ for (const button of buttons) {
12
+ button.addEventListener("click", () => {
13
+ const kind = button.dataset.show;
14
+ for (const other of buttons) other.setAttribute("aria-pressed", String(other === button));
15
+ for (const item of area.querySelectorAll("li[data-kind]")) item.hidden = kind !== "all" && item.dataset.kind !== kind;
16
+ for (const more of area.querySelectorAll("[data-more]")) {
17
+ more.hidden = more.dataset.more !== kind;
18
+ if (!more.hidden) more.closest("details").hidden = more.dataset.count === "0";
19
+ }
20
+ if (status) status.textContent = button.textContent;
21
+ });
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,47 @@
1
+ // Marking pages read and grading checks (design section 8). Grading feedback is shown as text, never as HTML.
2
+ import { post, usedText } from "./api.js";
3
+
4
+ export async function toggleRead() {
5
+ const read = document.querySelector("[data-mark-read]");
6
+ const unread = document.querySelector("[data-mark-unread]");
7
+ const target = read ?? unread;
8
+ if (!target) return;
9
+ const url = read ? "/learn/read" : "/learn/unread";
10
+ const response = await post(url, { page_id: (read ?? unread).dataset[read ? "markRead" : "markUnread"] });
11
+ if (response.ok) window.location.reload();
12
+ }
13
+
14
+ export function setUpLearning() {
15
+ for (const button of document.querySelectorAll("[data-mark-read], [data-mark-unread]")) {
16
+ button.addEventListener("click", toggleRead);
17
+ }
18
+ for (const form of document.querySelectorAll("form[data-check]")) {
19
+ const feedback = form.querySelector("[data-feedback]");
20
+ const labels = form.closest("[data-label-pass]").dataset; // the verdicts in the reader's language
21
+ form.addEventListener("submit", async (event) => {
22
+ event.preventDefault();
23
+ const button = form.querySelector("button");
24
+ button.disabled = true;
25
+ feedback.textContent = "…";
26
+ try {
27
+ const response = await post("/learn/checks", {
28
+ page_id: form.dataset.page,
29
+ check_id: form.dataset.check,
30
+ answer: form.elements.answer.value,
31
+ });
32
+ const result = await response.json().catch(() => ({}));
33
+ if (!response.ok) {
34
+ feedback.textContent = result.error ?? labels.labelUngraded;
35
+ return;
36
+ }
37
+ const verdict = { pass: labels.labelPass, partial: labels.labelPartial, fail: labels.labelFail }[result.verdict];
38
+ feedback.textContent = `${verdict}: ${result.feedback}`;
39
+ const usage = document.querySelector("[data-ask-status]");
40
+ if (usage && result.usage) feedback.textContent += ` (${usedText(usage, result.usage)})`;
41
+ if (result.state === "learned") window.setTimeout(() => window.location.reload(), 1500);
42
+ } finally {
43
+ button.disabled = false;
44
+ }
45
+ });
46
+ }
47
+ }