batho 0.1.4__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 (184) hide show
  1. batho/__init__.py +55 -0
  2. batho/bridge/__init__.py +42 -0
  3. batho/bridge/artifact_loader.py +214 -0
  4. batho/bridge/constants.py +79 -0
  5. batho/bridge/http_api.py +383 -0
  6. batho/bridge/mcp_server.py +159 -0
  7. batho/bridge/models.py +102 -0
  8. batho/bridge/registry_client.py +221 -0
  9. batho/bsg/__init__.py +31 -0
  10. batho/bsg/plugins/foundation/bsg_detection_cicd.yaml +87 -0
  11. batho/bsg/plugins/foundation/bsg_detection_cloud_providers.yaml +100 -0
  12. batho/bsg/plugins/foundation/bsg_detection_cpp.yaml +110 -0
  13. batho/bsg/plugins/foundation/bsg_detection_csharp.yaml +130 -0
  14. batho/bsg/plugins/foundation/bsg_detection_dart.yaml +89 -0
  15. batho/bsg/plugins/foundation/bsg_detection_elixir.yaml +92 -0
  16. batho/bsg/plugins/foundation/bsg_detection_foundation.yaml +268 -0
  17. batho/bsg/plugins/foundation/bsg_detection_kotlin.yaml +79 -0
  18. batho/bsg/plugins/foundation/bsg_detection_php.yaml +135 -0
  19. batho/bsg/plugins/foundation/bsg_detection_ruby.yaml +100 -0
  20. batho/bsg/plugins/foundation/bsg_detection_scala.yaml +93 -0
  21. batho/bsg/plugins/foundation/bsg_detection_swift.yaml +119 -0
  22. batho/bsg/plugins/foundation/bsg_detection_test_frameworks.yaml +130 -0
  23. batho/bsg/plugins/foundation/bsg_file_categorization.yaml +320 -0
  24. batho/bsg/plugins/foundation/bsg_framework_angular.yaml +40 -0
  25. batho/bsg/plugins/foundation/bsg_framework_django.yaml +44 -0
  26. batho/bsg/plugins/foundation/bsg_framework_flask.yaml +44 -0
  27. batho/bsg/plugins/foundation/bsg_framework_nodejs.yaml +378 -0
  28. batho/bsg/plugins/foundation/bsg_framework_other.yaml +278 -0
  29. batho/bsg/plugins/foundation/bsg_framework_python.yaml +249 -0
  30. batho/bsg/plugins/foundation/bsg_framework_react.yaml +92 -0
  31. batho/bsg/plugins/foundation/bsg_framework_vue.yaml +58 -0
  32. batho/bsg/plugins/foundation/bsg_graph_foundation.yaml +109 -0
  33. batho/bsg/plugins/foundation/bsg_token_optimization.yaml +184 -0
  34. batho/bsg/plugins/interceptors/bsg_api_contract_guardian.yaml +27 -0
  35. batho/bsg/plugins/interceptors/bsg_auth_boundary_shield.yaml +29 -0
  36. batho/bsg/plugins/interceptors/bsg_dependency_blast_radius.yaml +28 -0
  37. batho/bsg/plugins/interceptors/bsg_hardcoded_secret_catcher.yaml +30 -0
  38. batho/bsg/plugins/interceptors/bsg_iac_drift_sentinel.yaml +29 -0
  39. batho/bsg/plugins/interceptors/bsg_nplus1_query_catcher.yaml +29 -0
  40. batho/bsg/plugins/interceptors/bsg_resource_leak_preventer.yaml +27 -0
  41. batho/bsg/plugins/interceptors/bsg_schema_migration_enforcer.yaml +23 -0
  42. batho/bsg/plugins/interceptors/bsg_silent_failure_catcher.yaml +23 -0
  43. batho/bsg/plugins_cli.py +263 -0
  44. batho/bsg/rules.py +3268 -0
  45. batho/bsg/schemas/bsg-plugin-schema-v1.json +429 -0
  46. batho/bsg/testing.py +519 -0
  47. batho/cli/bridge.py +295 -0
  48. batho/cli/dashboard.py +280 -0
  49. batho/cloud_sync/__init__.py +11 -0
  50. batho/cloud_sync/client.py +358 -0
  51. batho/cloud_sync/config.py +55 -0
  52. batho/cloud_sync/uploader.py +195 -0
  53. batho/config.py +1021 -0
  54. batho/context/__init__.py +22 -0
  55. batho/context/bsg.py +8 -0
  56. batho/context/bsg_map.py +1835 -0
  57. batho/context/cache.py +473 -0
  58. batho/context/codegraph.py +1179 -0
  59. batho/context/extractor.py +1004 -0
  60. batho/context/graph_cache.py +99 -0
  61. batho/context/incremental.py +198 -0
  62. batho/context/languages/__init__.py +148 -0
  63. batho/context/languages/_common.py +271 -0
  64. batho/context/languages/bash.py +72 -0
  65. batho/context/languages/c.py +61 -0
  66. batho/context/languages/cpp.py +87 -0
  67. batho/context/languages/csharp.py +86 -0
  68. batho/context/languages/css.py +242 -0
  69. batho/context/languages/dart.py +84 -0
  70. batho/context/languages/detector.py +578 -0
  71. batho/context/languages/erlang.py +76 -0
  72. batho/context/languages/factory.py +500 -0
  73. batho/context/languages/go.py +67 -0
  74. batho/context/languages/hack.py +83 -0
  75. batho/context/languages/haskell.py +87 -0
  76. batho/context/languages/hcl.py +357 -0
  77. batho/context/languages/html.py +225 -0
  78. batho/context/languages/java.py +69 -0
  79. batho/context/languages/javascript.py +78 -0
  80. batho/context/languages/json.py +243 -0
  81. batho/context/languages/julia.py +83 -0
  82. batho/context/languages/kotlin.py +76 -0
  83. batho/context/languages/lua.py +70 -0
  84. batho/context/languages/markdown.py +357 -0
  85. batho/context/languages/objectivec.py +107 -0
  86. batho/context/languages/ocaml.py +87 -0
  87. batho/context/languages/perl.py +75 -0
  88. batho/context/languages/php.py +74 -0
  89. batho/context/languages/python.py +106 -0
  90. batho/context/languages/r.py +63 -0
  91. batho/context/languages/registry.py +635 -0
  92. batho/context/languages/ruby.py +67 -0
  93. batho/context/languages/rust.py +75 -0
  94. batho/context/languages/scala.py +99 -0
  95. batho/context/languages/swift.py +94 -0
  96. batho/context/languages/toml.py +262 -0
  97. batho/context/languages/typescript.py +88 -0
  98. batho/context/languages/verilog.py +100 -0
  99. batho/context/languages/yaml.py +285 -0
  100. batho/context/languages/zig.py +88 -0
  101. batho/context/mmap_storage.py +63 -0
  102. batho/context/pipeline.py +381 -0
  103. batho/context/query.py +313 -0
  104. batho/context/schema.py +227 -0
  105. batho/context/storage.py +1727 -0
  106. batho/context/symbol_index.py +124 -0
  107. batho/dashboard/DESIGN.md +8 -0
  108. batho/dashboard/README.md +51 -0
  109. batho/dashboard/assets/css/animations.css +35 -0
  110. batho/dashboard/assets/css/base.css +139 -0
  111. batho/dashboard/assets/css/components.css +326 -0
  112. batho/dashboard/assets/css/graph.css +267 -0
  113. batho/dashboard/assets/css/tokens.css +119 -0
  114. batho/dashboard/assets/fonts/inter-500.woff2 +11 -0
  115. batho/dashboard/assets/fonts/inter-500.woff2.LICENSE +2 -0
  116. batho/dashboard/assets/fonts/inter-600.woff2 +11 -0
  117. batho/dashboard/assets/fonts/space-grotesk-500.woff2 +11 -0
  118. batho/dashboard/assets/fonts/space-grotesk-500.woff2.LICENSE +2 -0
  119. batho/dashboard/assets/fonts/space-grotesk-700.woff2 +11 -0
  120. batho/dashboard/assets/js/ctn-loader.js +424 -0
  121. batho/dashboard/assets/js/cy-import.js +34 -0
  122. batho/dashboard/assets/js/cy-stylesheet.js +156 -0
  123. batho/dashboard/assets/js/format.js +60 -0
  124. batho/dashboard/assets/js/glob.js +94 -0
  125. batho/dashboard/assets/js/graph-stream.js +158 -0
  126. batho/dashboard/assets/js/main.js +175 -0
  127. batho/dashboard/assets/js/router.js +101 -0
  128. batho/dashboard/assets/js/store.js +53 -0
  129. batho/dashboard/index.html +88 -0
  130. batho/dashboard/pages/files.js +275 -0
  131. batho/dashboard/pages/hypergraph.js +564 -0
  132. batho/dashboard/pages/metrics.js +6 -0
  133. batho/dashboard/pages/overview.js +752 -0
  134. batho/dashboard/pages/relationships.js +192 -0
  135. batho/dashboard/pages/rules.js +6 -0
  136. batho/dashboard/pages/search.js +6 -0
  137. batho/dashboard/pages/snapshots.js +234 -0
  138. batho/dashboard/shared/components/audit-export.js +127 -0
  139. batho/dashboard/shared/components/chip-filter.js +67 -0
  140. batho/dashboard/shared/components/code-pane.js +47 -0
  141. batho/dashboard/shared/components/data-table.js +206 -0
  142. batho/dashboard/shared/components/drawer.js +134 -0
  143. batho/dashboard/shared/components/entity-chip.js +75 -0
  144. batho/dashboard/shared/components/header-bar.js +230 -0
  145. batho/dashboard/shared/components/kpi-row.js +28 -0
  146. batho/dashboard/shared/components/latency-bar.js +69 -0
  147. batho/dashboard/shared/components/side-rail.js +73 -0
  148. batho/dashboard/shared/components/staleness-gauge.js +69 -0
  149. batho/dashboard/shared/components/stat-tile.js +41 -0
  150. batho/dashboard/shared/components/tab-bar.js +70 -0
  151. batho/dashboard/vendor/cytoscape/LICENSE +19 -0
  152. batho/dashboard/vendor/cytoscape/VERSION.txt +1 -0
  153. batho/dashboard/vendor/cytoscape/cytoscape.min.js +32 -0
  154. batho/dashboard/vendor/marked/LICENSE +1 -0
  155. batho/dashboard/vendor/marked/VERSION.txt +1 -0
  156. batho/dashboard/vendor/marked/marked.min.js +6 -0
  157. batho/hooks/__init__.py +43 -0
  158. batho/hooks/bootstrap.py +18 -0
  159. batho/hooks/constants.py +97 -0
  160. batho/hooks/installer.py +159 -0
  161. batho/hooks/loader.py +63 -0
  162. batho/hooks/models.py +78 -0
  163. batho/hooks/planner.py +128 -0
  164. batho/hooks/runner.py +147 -0
  165. batho/synthesizer.py +249 -0
  166. batho/time_machine.py +1667 -0
  167. batho/utils/__init__.py +54 -0
  168. batho/utils/cli_output.py +165 -0
  169. batho/utils/dependencies.py +618 -0
  170. batho/utils/encoding.py +106 -0
  171. batho/utils/file_io.py +194 -0
  172. batho/utils/file_lock.py +294 -0
  173. batho/utils/hash.py +214 -0
  174. batho/utils/ignore.py +379 -0
  175. batho/utils/logging.py +160 -0
  176. batho/utils/memory_monitor.py +304 -0
  177. batho/utils/patch_errors.py +279 -0
  178. batho/utils/path_sanitizer.py +237 -0
  179. batho-0.1.4.dist-info/METADATA +1378 -0
  180. batho-0.1.4.dist-info/RECORD +184 -0
  181. batho-0.1.4.dist-info/WHEEL +4 -0
  182. batho-0.1.4.dist-info/entry_points.txt +2 -0
  183. batho-0.1.4.dist-info/licenses/LICENSE +201 -0
  184. batho_cli.py +3922 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Glob pattern matching for file path filtering.
3
+ *
4
+ * Supports:
5
+ * `*` — matches any sequence of characters except /
6
+ * `**` — matches any sequence including /
7
+ * `?` — matches exactly one character except /
8
+ * `{a,b}` — matches a or b (comma-separated alternatives)
9
+ *
10
+ * All matching is case-insensitive on the path segment level.
11
+ */
12
+
13
+ /**
14
+ * Convert a glob pattern to a RegExp.
15
+ */
16
+ function globToRegex(pattern) {
17
+ let i = 0;
18
+ const len = pattern.length;
19
+ let result = '^';
20
+
21
+ while (i < len) {
22
+ const ch = pattern[i];
23
+
24
+ if (ch === '*') {
25
+ // Peek ahead for **
26
+ if (i + 1 < len && pattern[i + 1] === '*') {
27
+ // ** matches any path including separators
28
+ result += '.*';
29
+ i += 2;
30
+ // Skip trailing /
31
+ if (i < len && pattern[i] === '/') i++;
32
+ } else {
33
+ // * matches any characters except /
34
+ result += '[^/]*';
35
+ i++;
36
+ }
37
+ } else if (ch === '?') {
38
+ result += '[^/]';
39
+ i++;
40
+ } else if (ch === '{') {
41
+ // Find closing brace
42
+ const closeIdx = pattern.indexOf('}', i);
43
+ if (closeIdx === -1) {
44
+ result += '\\{';
45
+ i++;
46
+ } else {
47
+ const inner = pattern.slice(i + 1, closeIdx);
48
+ const alternatives = inner.split(',').map(escapeRegex).join('|');
49
+ result += `(?:${alternatives})`;
50
+ i = closeIdx + 1;
51
+ }
52
+ } else if (ch === '[') {
53
+ // Pass through character classes
54
+ const closeIdx = pattern.indexOf(']', i);
55
+ if (closeIdx === -1) {
56
+ result += '\\[';
57
+ i++;
58
+ } else {
59
+ result += pattern.slice(i, closeIdx + 1);
60
+ i = closeIdx + 1;
61
+ }
62
+ } else {
63
+ result += escapeRegex(ch);
64
+ i++;
65
+ }
66
+ }
67
+
68
+ result += '$';
69
+ return new RegExp(result, 'i');
70
+ }
71
+
72
+ function escapeRegex(ch) {
73
+ if ('.+^$|()[]{}\\'.includes(ch)) return '\\' + ch;
74
+ return ch;
75
+ }
76
+
77
+ /**
78
+ * Test whether path matches pattern.
79
+ */
80
+ export function matchGlob(pattern, path) {
81
+ if (!pattern || pattern === '*' || pattern === '**') return true;
82
+ const re = globToRegex(pattern);
83
+ return re.test(path);
84
+ }
85
+
86
+ /**
87
+ * Filter an array of items by a glob pattern applied to a path field.
88
+ */
89
+ export function filterByGlob(items, pattern, pathField) {
90
+ if (!pattern || pattern === '*' || pattern === '**') return items;
91
+ const accessor = typeof pathField === 'function' ? pathField : (item) => item[pathField];
92
+ const re = globToRegex(pattern);
93
+ return items.filter((item) => re.test(accessor(item)));
94
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Streaming JSON parser for large graph files.
3
+ * Uses a bracket-depth state machine to yield batches of entities/relationships
4
+ * from a streamed JSON response without holding the full parse in memory.
5
+ */
6
+
7
+ const BATCH_SIZE = 500;
8
+
9
+ /**
10
+ * Stream-parse graph JSON and invoke progress callbacks with batches.
11
+ * Falls back to standard JSON.parse on any error.
12
+ *
13
+ * @param {string} url - The bridge URL to fetch
14
+ * @param {function} onProgress - Called with {entities, relationships, percent}
15
+ * @returns {Promise<{entities: Array, relationships: Array}>}
16
+ */
17
+ export async function streamParseGraph(url, onProgress) {
18
+ let response;
19
+ try {
20
+ response = await fetch(url);
21
+ } catch (_) {
22
+ throw new Error('Network failure fetching graph data');
23
+ }
24
+
25
+ if (!response.ok) throw new Error(`HTTP ${response.status} fetching graph data`);
26
+
27
+ const contentType = response.headers.get('content-type') || '';
28
+ const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
29
+
30
+ if (!response.body || !response.body.getReader || contentLength < 50000) {
31
+ return fallbackParse(response, onProgress);
32
+ }
33
+
34
+ const reader = response.body.getReader();
35
+ const decoder = new TextDecoder();
36
+ let buffer = '';
37
+ let loaded = 0;
38
+
39
+ const state = {
40
+ entities: [],
41
+ relationships: [],
42
+ currentArray: null,
43
+ depth: 0,
44
+ arrayDepth: -1,
45
+ inString: false,
46
+ escape: false,
47
+ braceDepth: 0,
48
+ bracketDepth: 0,
49
+ objectStart: -1,
50
+ };
51
+
52
+ while (true) {
53
+ const { done, value } = await reader.read();
54
+ if (done) break;
55
+
56
+ loaded += value.length;
57
+ buffer += decoder.decode(value, { stream: true });
58
+
59
+ try {
60
+ processChunk(buffer, state, onProgress, contentLength, loaded);
61
+ buffer = '';
62
+ } catch (_) {
63
+ return fallbackParse(response, onProgress);
64
+ }
65
+ }
66
+
67
+ if (state.entities.length === 0 && state.relationships.length === 0) {
68
+ return fallbackParse(response, onProgress);
69
+ }
70
+
71
+ if (onProgress) {
72
+ onProgress({
73
+ entities: state.entities,
74
+ relationships: state.relationships,
75
+ percent: 100,
76
+ });
77
+ }
78
+
79
+ return { entities: state.entities, relationships: state.relationships };
80
+ }
81
+
82
+ function processChunk(chunk, state, onProgress, total, loaded) {
83
+ for (let i = 0; i < chunk.length; i++) {
84
+ const ch = chunk[i];
85
+
86
+ if (state.escape) {
87
+ state.escape = false;
88
+ continue;
89
+ }
90
+
91
+ if (ch === '\\' && state.inString) {
92
+ state.escape = true;
93
+ continue;
94
+ }
95
+
96
+ if (ch === '"') {
97
+ state.inString = !state.inString;
98
+ continue;
99
+ }
100
+
101
+ if (state.inString) continue;
102
+
103
+ if (ch === '{') state.braceDepth++;
104
+ if (ch === '}') state.braceDepth--;
105
+ if (ch === '[') state.bracketDepth++;
106
+ if (ch === ']') state.bracketDepth--;
107
+
108
+ if (ch === '[' && !state.currentArray) {
109
+ const lookback = chunk.substring(Math.max(0, i - 20), i + 1);
110
+ if (lookback.match(/"entities"\s*:\s*\[/)) {
111
+ state.currentArray = 'entities';
112
+ state.arrayDepth = state.bracketDepth;
113
+ state.objectStart = i + 1;
114
+ } else if (lookback.match(/"relationships"\s*:\s*\[/)) {
115
+ state.currentArray = 'relationships';
116
+ state.arrayDepth = state.bracketDepth;
117
+ state.objectStart = i + 1;
118
+ }
119
+ }
120
+
121
+ if (state.currentArray && ch === '}' && state.braceDepth < state.arrayDepth) {
122
+ try {
123
+ const objStr = chunk.substring(state.objectStart, i + 1);
124
+ const obj = JSON.parse(objStr);
125
+ state[state.currentArray].push(obj);
126
+ } catch (_) { /* skip malformed object */ }
127
+ state.objectStart = i + 1;
128
+
129
+ if (state[state.currentArray].length % BATCH_SIZE === 0 && onProgress) {
130
+ onProgress({
131
+ entities: state.entities,
132
+ relationships: state.relationships,
133
+ percent: total > 0 ? Math.round((loaded / total) * 100) : 50,
134
+ });
135
+ }
136
+ }
137
+
138
+ if (ch === ']' && state.bracketDepth < state.arrayDepth) {
139
+ state.currentArray = null;
140
+ state.arrayDepth = -1;
141
+ }
142
+ }
143
+ }
144
+
145
+ async function fallbackParse(response, onProgress) {
146
+ const text = await response.text();
147
+ try {
148
+ const data = JSON.parse(text);
149
+ const entities = data.entities || [];
150
+ const relationships = data.relationships || [];
151
+ if (onProgress) {
152
+ onProgress({ entities, relationships, percent: 100 });
153
+ }
154
+ return { entities, relationships };
155
+ } catch (e) {
156
+ throw new Error(`Failed to parse graph JSON: ${e.message}`);
157
+ }
158
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Main bootstrap - mounts header, rail, registers routes.
3
+ */
4
+
5
+ import { router } from './router.js';
6
+ import { createHeaderBar } from '../../shared/components/header-bar.js';
7
+ import { createSideRail } from '../../shared/components/side-rail.js';
8
+ import { loadIndex } from './ctn-loader.js';
9
+ import { renderOverview } from '../../pages/overview.js';
10
+ import { renderHypergraph } from '../../pages/hypergraph.js';
11
+ import { renderFiles } from '../../pages/files.js';
12
+ import { renderRelationships } from '../../pages/relationships.js';
13
+ import { renderRules } from '../../pages/rules.js';
14
+ import { renderSnapshots } from '../../pages/snapshots.js';
15
+ import { renderMetrics } from '../../pages/metrics.js';
16
+ import { renderSearch } from '../../pages/search.js';
17
+
18
+ async function init() {
19
+ const app = document.getElementById('app');
20
+ if (!app) {
21
+ console.error('[batho] #app not found');
22
+ return;
23
+ }
24
+
25
+ // Clear pre-mount skeleton.
26
+ app.innerHTML = '';
27
+ app.className = 'app';
28
+
29
+ // Try to load the index via the bridge API. If the server is unreachable
30
+ // we still mount the shell so navigation works and pages can render their own error states.
31
+ let indexData = { currentIndexId: '—', indexes: {} };
32
+ let indexLoadError = null;
33
+ try {
34
+ indexData = await loadIndex();
35
+ } catch (err) {
36
+ indexLoadError = err;
37
+ console.warn('[batho] Could not load index:', err);
38
+ }
39
+
40
+ const savedIndexId = localStorage.getItem('batho.activeIndexId');
41
+ const activeIndexId =
42
+ savedIndexId && indexData.indexes[savedIndexId]
43
+ ? savedIndexId
44
+ : indexData.currentIndexId;
45
+ const activeEntry = indexData.indexes[activeIndexId] || {};
46
+ const repoRoot = activeEntry.root || '…';
47
+
48
+ const header = createHeaderBar({
49
+ repoRoot,
50
+ indexId: activeIndexId,
51
+ indexes: indexData.indexes,
52
+ });
53
+ header.classList.add('app__header');
54
+ app.appendChild(header);
55
+
56
+ const sideRail = createSideRail(router);
57
+ sideRail.classList.add('app__rail');
58
+ app.appendChild(sideRail);
59
+
60
+ const main = document.createElement('main');
61
+ main.id = 'page-mount';
62
+ main.className = 'app__main';
63
+ app.appendChild(main);
64
+
65
+ const aside = document.createElement('aside');
66
+ aside.id = 'notice-slot';
67
+ aside.setAttribute('aria-live', 'polite');
68
+ aside.setAttribute('aria-atomic', 'true');
69
+ aside.className = 'notice-slot';
70
+ app.appendChild(aside);
71
+
72
+ router.register('#/overview', renderOverview);
73
+ router.register('#/hypergraph', renderHypergraph);
74
+ router.register('#/files', renderFiles);
75
+ router.register('#/relationships', renderRelationships);
76
+ router.register('#/rules', renderRules);
77
+ router.register('#/snapshots', renderSnapshots);
78
+ router.register('#/metrics', renderMetrics);
79
+ router.register('#/search', renderSearch);
80
+ router.register('*', async () => {
81
+ const container = document.createElement('div');
82
+ container.className = 'panel panel--stub not-found';
83
+ container.innerHTML = `
84
+ <div class="not-found__code">404</div>
85
+ <div class="not-found__divider"></div>
86
+ <div class="not-found__message">Route is not part of the cockpit.</div>
87
+ <button class="btn" data-navigate="#/overview">return to overview</button>
88
+ `;
89
+ container
90
+ .querySelector('[data-navigate]')
91
+ .addEventListener('click', () => router.navigate('#/overview'));
92
+ return container;
93
+ });
94
+
95
+ router.on('change', ({ path }) => localStorage.setItem('batho.lastRoute', path));
96
+
97
+ // React to index switches from the header dropdown by re-running the
98
+ // current route handler. This keeps page state consistent everywhere.
99
+ window.addEventListener('batho:index-changed', () => router.handle());
100
+
101
+ if (indexLoadError) {
102
+ showNotice(
103
+ `Index unavailable: ${indexLoadError.message}. Some pages may render limited data.`,
104
+ 'warn'
105
+ );
106
+ }
107
+
108
+ if (isDevMode()) runTokenDriftCheck();
109
+
110
+ router.start();
111
+ }
112
+
113
+ function isDevMode() {
114
+ return new URLSearchParams(window.location.search).get('dev') === '1';
115
+ }
116
+
117
+ function showNotice(message, tone = 'info') {
118
+ const slot = document.getElementById('notice-slot');
119
+ if (!slot) return;
120
+ const el = document.createElement('div');
121
+ el.className = `notice notice--${tone}`;
122
+ el.textContent = message;
123
+ slot.appendChild(el);
124
+ // Auto-dismiss after 8s so the notice stack stays clean.
125
+ setTimeout(() => el.remove(), 8000);
126
+ }
127
+
128
+ async function runTokenDriftCheck() {
129
+ try {
130
+ const response = await fetch('/dashboard/DESIGN.md');
131
+ if (!response.ok) return;
132
+ const text = await response.text();
133
+ const match = text.match(/^---\n([\s\S]*?)\n---/);
134
+ if (!match) return;
135
+ const yaml = match[1];
136
+ const colorsMatch = yaml.match(/colors:\s*\n((?:\s{2}.+\n?)+)/);
137
+ if (!colorsMatch) return;
138
+ const designColors = {};
139
+ colorsMatch[1].split('\n').forEach((line) => {
140
+ const [key, value] = line.trim().split(/:\s*/);
141
+ if (key && value) designColors[key] = value;
142
+ });
143
+ const rootStyles = getComputedStyle(document.documentElement);
144
+ const checkColors = ['surface', 'primary', 'tertiary', 'error'];
145
+ const drift = [];
146
+ checkColors.forEach((color) => {
147
+ const cssValue = rootStyles.getPropertyValue(`--${color}`).trim();
148
+ const designValue = designColors[color];
149
+ if (
150
+ designValue &&
151
+ cssValue.toLowerCase() !== designValue.toLowerCase()
152
+ ) {
153
+ drift.push({ color, css: cssValue, design: designValue });
154
+ }
155
+ });
156
+ if (drift.length > 0) {
157
+ const notice = document.createElement('div');
158
+ notice.className = 'notice notice--warn';
159
+ notice.innerHTML = `<strong>Token Drift Detected:</strong>${drift
160
+ .map((d) => `<br>--${d.color}: ${d.css} (DESIGN: ${d.design})`)
161
+ .join('')}`;
162
+ const slot = document.getElementById('notice-slot');
163
+ if (slot) slot.appendChild(notice);
164
+ }
165
+ } catch (e) {
166
+ console.warn('[batho] Token drift check failed:', e);
167
+ }
168
+ }
169
+
170
+ if (document.readyState === 'loading') {
171
+ document.addEventListener('DOMContentLoaded', init);
172
+ } else {
173
+ // Module scripts defer by default, but be defensive.
174
+ init();
175
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Hash-based router with register/navigate/on('change') API.
3
+ */
4
+
5
+ const router = {
6
+ routes: new Map(),
7
+ listeners: new Map(),
8
+ defaultRoute: '#/overview',
9
+
10
+ register(path, handler) { this.routes.set(path, handler); },
11
+
12
+ navigate(path, params = {}) {
13
+ const queryString = Object.keys(params).length > 0
14
+ ? '?' + new URLSearchParams(params).toString() : '';
15
+ const hash = path.startsWith('#') ? path : `#${path}`;
16
+ location.hash = hash + queryString;
17
+ },
18
+
19
+ on(event, listener) {
20
+ if (!this.listeners.has(event)) this.listeners.set(event, []);
21
+ this.listeners.get(event).push(listener);
22
+ },
23
+
24
+ emit(event, data) {
25
+ const listeners = this.listeners.get(event) || [];
26
+ listeners.forEach(listener => listener(data));
27
+ },
28
+
29
+ parseHash(hash) {
30
+ if (!hash || hash === '#') return { path: this.defaultRoute, params: new URLSearchParams() };
31
+ const cleanHash = hash.startsWith('#') ? hash.slice(1) : hash;
32
+ const [pathPart, queryPart] = cleanHash.split('?');
33
+ const path = pathPart.startsWith('/') ? pathPart : '/' + pathPart;
34
+ const params = new URLSearchParams(queryPart || '');
35
+ return { path: '#' + path, params };
36
+ },
37
+
38
+ async handle() {
39
+ const { path, params } = this.parseHash(location.hash || this.defaultRoute);
40
+ this.emit('change', { path, params });
41
+ const handler = this.routes.get(path);
42
+
43
+ if (!handler) {
44
+ const wildcard = this.routes.get('*');
45
+ if (wildcard) {
46
+ try { this.mount(await wildcard({ path, params })); }
47
+ catch (err) { this.emit('error', err); this.mount(this.renderError(err)); }
48
+ } else { this.mount(this.render404(path)); }
49
+ return;
50
+ }
51
+
52
+ try { this.mount(await handler(params)); }
53
+ catch (err) { this.emit('error', err); this.mount(this.renderError(err)); }
54
+ },
55
+
56
+ mount(element) {
57
+ const mountPoint = document.getElementById('page-mount');
58
+ if (mountPoint) { mountPoint.innerHTML = ''; mountPoint.appendChild(element); }
59
+ },
60
+
61
+ render404(path) {
62
+ const container = document.createElement('div');
63
+ container.className = 'panel panel--stub not-found';
64
+ container.innerHTML = `
65
+ <div class="not-found__code">404</div>
66
+ <div class="not-found__divider"></div>
67
+ <div class="not-found__message">Route ${path} is not part of the cockpit.</div>
68
+ <button class="btn" data-navigate="#/overview">return to overview</button>
69
+ `;
70
+ container.querySelector('[data-navigate]').addEventListener('click', () => this.navigate('#/overview'));
71
+ return container;
72
+ },
73
+
74
+ renderError(err) {
75
+ const container = document.createElement('div');
76
+ container.className = 'panel error-panel';
77
+ container.innerHTML = `
78
+ <div class="error-panel__icon">⚠</div>
79
+ <div class="error-panel__title">Structural Fault</div>
80
+ <div class="error-panel__message">${this.escapeHtml(err.message || 'An unknown error occurred')}</div>
81
+ <div class="error-panel__actions">
82
+ <button class="btn" data-action="retry">retry</button>
83
+ <button class="btn" data-action="snapshots">open snapshots</button>
84
+ </div>
85
+ `;
86
+ container.querySelector('[data-action="retry"]').addEventListener('click', () => this.handle());
87
+ container.querySelector('[data-action="snapshots"]').addEventListener('click', () => this.navigate('#/snapshots'));
88
+ return container;
89
+ },
90
+
91
+ escapeHtml(text) { const d = document.createElement('div'); d.textContent = text; return d.innerHTML; },
92
+
93
+ start() {
94
+ const savedRoute = localStorage.getItem('batho.lastRoute');
95
+ if (savedRoute && !location.hash) location.hash = savedRoute;
96
+ window.addEventListener('hashchange', () => this.handle());
97
+ this.handle();
98
+ }
99
+ };
100
+
101
+ export { router };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * LRU store with namespace isolation. LRU cap = 3 by index_id.
3
+ */
4
+
5
+ const LRU_CAP = 3;
6
+
7
+ const store = {
8
+ data: new Map(),
9
+ insertionOrder: [],
10
+
11
+ put(indexId, key, value) {
12
+ if (!this.data.has(indexId)) {
13
+ if (this.data.size >= LRU_CAP) {
14
+ const oldestIndexId = this.insertionOrder.shift();
15
+ this.data.delete(oldestIndexId);
16
+ }
17
+ this.insertionOrder.push(indexId);
18
+ this.data.set(indexId, new Map());
19
+ } else {
20
+ const idx = this.insertionOrder.indexOf(indexId);
21
+ if (idx > -1) { this.insertionOrder.splice(idx, 1); this.insertionOrder.push(indexId); }
22
+ }
23
+ this.data.get(indexId).set(key, value);
24
+ },
25
+
26
+ get(indexId, key) { const ns = this.data.get(indexId); return ns ? ns.get(key) : undefined; },
27
+ has(indexId, key) { const ns = this.data.get(indexId); return ns ? ns.has(key) : false; },
28
+ delete(indexId, key) { const ns = this.data.get(indexId); if (ns) ns.delete(key); },
29
+
30
+ clear(indexId) {
31
+ if (indexId) {
32
+ const ns = this.data.get(indexId);
33
+ if (ns) {
34
+ const idx = this.insertionOrder.indexOf(indexId);
35
+ if (idx > -1) this.insertionOrder.splice(idx, 1);
36
+ ns.clear();
37
+ this.data.delete(indexId);
38
+ }
39
+ } else { this.data.clear(); this.insertionOrder = []; }
40
+ },
41
+
42
+ keys(indexId) { const ns = this.data.get(indexId); return ns ? Array.from(ns.keys()) : []; },
43
+
44
+ size(indexId) {
45
+ if (indexId) { const ns = this.data.get(indexId); return ns ? ns.size : 0; }
46
+ let total = 0; for (const ns of this.data.values()) total += ns.size;
47
+ return total;
48
+ },
49
+
50
+ namespaces() { return Array.from(this.data.keys()); }
51
+ };
52
+
53
+ export { store };
@@ -0,0 +1,88 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <meta name="description" content="Batho — brutalist code intelligence cockpit" />
8
+ <title>Batho Dashboard</title>
9
+ <link
10
+ rel="icon"
11
+ type="image/svg+xml"
12
+ href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect x='2' y='2' width='12' height='12' fill='%2300d9ff'/></svg>"
13
+ />
14
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
16
+ <link
17
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=Space+Grotesk:wght@500;700&family=JetBrains+Mono:wght@400;500&display=swap"
18
+ rel="stylesheet"
19
+ />
20
+ <link rel="stylesheet" href="/dashboard/assets/css/tokens.css" />
21
+ <link rel="stylesheet" href="/dashboard/assets/css/base.css" />
22
+ <link rel="stylesheet" href="/dashboard/assets/css/components.css" />
23
+ <link rel="stylesheet" href="/dashboard/assets/css/animations.css" />
24
+ <link rel="stylesheet" href="/dashboard/assets/css/graph.css" />
25
+ <style>
26
+ .notice-slot {
27
+ position: fixed;
28
+ bottom: 16px;
29
+ right: 16px;
30
+ z-index: 1000;
31
+ max-width: 400px;
32
+ pointer-events: none;
33
+ }
34
+ .notice {
35
+ padding: var(--space-pad);
36
+ margin-top: var(--space-gutter);
37
+ border: var(--hairline);
38
+ background: var(--surface-container);
39
+ font-size: var(--type-terminal-size);
40
+ pointer-events: auto;
41
+ }
42
+ .notice--warn {
43
+ border-color: var(--tertiary);
44
+ color: var(--tertiary);
45
+ }
46
+
47
+ /* Pre-mount skeleton so the page never flashes white. */
48
+ .app-bootstrap {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ height: 100vh;
53
+ width: 100vw;
54
+ color: var(--on-surface-variant, #aaa);
55
+ background: var(--surface, #0a0a0a);
56
+ font-family: var(--font-mono, monospace);
57
+ font-size: 12px;
58
+ letter-spacing: 0.04em;
59
+ }
60
+ .app-bootstrap__cursor {
61
+ display: inline-block;
62
+ width: 8px;
63
+ height: 12px;
64
+ background: var(--accent-cyan, #00d9ff);
65
+ margin-right: 8px;
66
+ animation: app-bootstrap-blink 1s steps(2) infinite;
67
+ }
68
+ @keyframes app-bootstrap-blink {
69
+ 0%, 50% { opacity: 1; }
70
+ 50.01%, 100% { opacity: 0; }
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div id="app">
76
+ <div class="app-bootstrap" role="status" aria-live="polite">
77
+ <span class="app-bootstrap__cursor" aria-hidden="true"></span>
78
+ <span>booting cockpit&hellip;</span>
79
+ </div>
80
+ </div>
81
+ <noscript>
82
+ <div style="padding:16px;font-family:monospace;background:#0a0a0a;color:#fff;">
83
+ Batho Dashboard requires JavaScript to render the cockpit.
84
+ </div>
85
+ </noscript>
86
+ <script type="module" src="/dashboard/assets/js/main.js"></script>
87
+ </body>
88
+ </html>