@medicine-wheel/developer-repository-mapping 0.0.0-stage → 0.21.0

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 (74) hide show
  1. package/README.md +68 -2
  2. package/dist/check.d.ts +28 -0
  3. package/dist/check.d.ts.map +1 -0
  4. package/dist/check.js +111 -0
  5. package/dist/check.js.map +1 -0
  6. package/dist/cli.d.ts +26 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +408 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/ids.d.ts +28 -0
  11. package/dist/ids.d.ts.map +1 -0
  12. package/dist/ids.js +68 -0
  13. package/dist/ids.js.map +1 -0
  14. package/dist/index.d.ts +40 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +89 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/jsonl.d.ts +19 -0
  19. package/dist/jsonl.d.ts.map +1 -0
  20. package/dist/jsonl.js +63 -0
  21. package/dist/jsonl.js.map +1 -0
  22. package/dist/lifecycle.d.ts +53 -0
  23. package/dist/lifecycle.d.ts.map +1 -0
  24. package/dist/lifecycle.js +78 -0
  25. package/dist/lifecycle.js.map +1 -0
  26. package/dist/merge.d.ts +30 -0
  27. package/dist/merge.d.ts.map +1 -0
  28. package/dist/merge.js +94 -0
  29. package/dist/merge.js.map +1 -0
  30. package/dist/node/fs.d.ts +28 -0
  31. package/dist/node/fs.d.ts.map +1 -0
  32. package/dist/node/fs.js +162 -0
  33. package/dist/node/fs.js.map +1 -0
  34. package/dist/node/git.d.ts +15 -0
  35. package/dist/node/git.d.ts.map +1 -0
  36. package/dist/node/git.js +73 -0
  37. package/dist/node/git.js.map +1 -0
  38. package/dist/node/hub.d.ts +45 -0
  39. package/dist/node/hub.d.ts.map +1 -0
  40. package/dist/node/hub.js +113 -0
  41. package/dist/node/hub.js.map +1 -0
  42. package/dist/node/index.d.ts +16 -0
  43. package/dist/node/index.d.ts.map +1 -0
  44. package/dist/node/index.js +30 -0
  45. package/dist/node/index.js.map +1 -0
  46. package/dist/node/page.d.ts +9 -0
  47. package/dist/node/page.d.ts.map +1 -0
  48. package/dist/node/page.js +219 -0
  49. package/dist/node/page.js.map +1 -0
  50. package/dist/node/server.d.ts +33 -0
  51. package/dist/node/server.d.ts.map +1 -0
  52. package/dist/node/server.js +111 -0
  53. package/dist/node/server.js.map +1 -0
  54. package/dist/read.d.ts +13 -0
  55. package/dist/read.d.ts.map +1 -0
  56. package/dist/read.js +38 -0
  57. package/dist/read.js.map +1 -0
  58. package/dist/schemas.d.ts +409 -0
  59. package/dist/schemas.d.ts.map +1 -0
  60. package/dist/schemas.js +160 -0
  61. package/dist/schemas.js.map +1 -0
  62. package/dist/summary.d.ts +31 -0
  63. package/dist/summary.d.ts.map +1 -0
  64. package/dist/summary.js +64 -0
  65. package/dist/summary.js.map +1 -0
  66. package/dist/types.d.ts +184 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +36 -0
  69. package/dist/types.js.map +1 -0
  70. package/dist/wheel.d.ts +14 -0
  71. package/dist/wheel.d.ts.map +1 -0
  72. package/dist/wheel.js +74 -0
  73. package/dist/wheel.js.map +1 -0
  74. package/package.json +63 -4
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The one page `mw-repomap serve` answers `/` with. It reads `/api/mapping`
3
+ * and renders it: repositories with their lifecycle, git activity, drift and
4
+ * revision state, a repository's features and relations, the capabilities and
5
+ * who provides them, and every issue the reading found. No build step and no
6
+ * external script, so it runs wherever the CLI runs.
7
+ */
8
+ export declare const MAPPING_PAGE = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Repository mapping</title>\n<style>\n:root {\n --bg: #fbfaf7; --panel: #ffffff; --ink: #1d1c1a; --muted: #6b675f; --line: #e3dfd6;\n --accent: #2f5d8a; --ok: #2e7d4f; --warn: #a3650b; --bad: #a83232; --chip: #f1ede4;\n}\n@media (prefers-color-scheme: dark) {\n :root { --bg: #161513; --panel: #1f1e1b; --ink: #ebe7df; --muted: #a29d93; --line: #34322d;\n --accent: #8db7e0; --ok: #7cc79a; --warn: #e3b25c; --bad: #e98a8a; --chip: #2a2925; }\n}\n* { box-sizing: border-box; }\nbody { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.45 system-ui, -apple-system, \"Segoe UI\", sans-serif; }\nheader, main { max-width: 1200px; margin: 0 auto; padding: 16px; }\nh1 { font-size: 20px; margin: 0 0 4px; }\n.sub { color: var(--muted); font-size: 13px; }\n.counts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }\n.count { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; font-size: 13px; }\n.count b { font-size: 16px; margin-right: 4px; }\nnav { display: flex; gap: 4px; margin: 8px 0 12px; border-bottom: 1px solid var(--line); }\nnav button { background: none; border: 0; border-bottom: 2px solid transparent; color: var(--muted); padding: 8px 10px; font: inherit; cursor: pointer; }\nnav button[aria-selected=\"true\"] { color: var(--ink); border-bottom-color: var(--accent); }\n.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }\n.filters input, .filters select { background: var(--panel); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; font: inherit; font-size: 14px; }\n.filters input { flex: 1 1 220px; }\n.layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }\n@media (min-width: 960px) { .layout.open { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); } }\n.scroll { overflow-x: auto; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }\ntable { border-collapse: collapse; width: 100%; font-size: 14px; }\nth, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }\nth { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; }\ntbody tr { cursor: pointer; }\ntbody tr:hover, tbody tr.sel { background: var(--chip); }\n.chip { display: inline-block; background: var(--chip); border-radius: 999px; padding: 1px 8px; font-size: 12px; margin: 0 4px 2px 0; white-space: nowrap; }\n.converged { color: var(--ok); } .drifted { color: var(--bad); } .unobserved, .undeclared { color: var(--warn); }\n.pending { color: var(--warn); } .affirmed { color: var(--ok); }\n.clip { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; max-width: 420px; }\naside { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; align-self: start; }\naside h2 { font-size: 17px; margin: 0 0 4px; word-break: break-word; }\naside h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); margin: 16px 0 6px; }\naside p { margin: 4px 0; }\n.item { border-top: 1px solid var(--line); padding: 8px 0; }\n.item:first-of-type { border-top: 0; }\na { color: var(--accent); }\n.close { float: right; background: none; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); cursor: pointer; padding: 2px 8px; }\n.issue { padding: 8px 10px; border-bottom: 1px solid var(--line); font-size: 14px; }\n.issue .error { color: var(--bad); font-weight: 600; } .issue .warning { color: var(--warn); font-weight: 600; }\n.empty { color: var(--muted); padding: 16px; }\ncode { font-size: 13px; }\n</style>\n</head>\n<body>\n<header>\n <h1 id=\"title\">Repository mapping</h1>\n <div class=\"sub\" id=\"sub\">Reading\u2026</div>\n <div class=\"counts\" id=\"counts\"></div>\n</header>\n<main>\n <nav role=\"tablist\" id=\"tabs\"></nav>\n <div class=\"filters\" id=\"filters\"></div>\n <div class=\"layout\" id=\"layout\"><div id=\"list\"></div><aside id=\"detail\" hidden></aside></div>\n</main>\n<script>\nconst S = { v: null, tab: 'repositories', q: '', lifecycle: '', drift: '', revision: '', field: '', owner: '', sel: null };\nconst esc = (s) => String(s == null ? '' : s).replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]));\nconst short = (id) => String(id).replace(/^[a-z-]+:/, '');\nconst gh = (id) => { const m = /^repository:([^/]+)\\/(.+)$/.exec(id); return m ? 'https://github.com/' + m[1] + '/' + m[2] : null; };\nconst chip = (t, cls) => '<span class=\"chip ' + esc(cls || '') + '\">' + esc(t) + '</span>';\nconst $ = (id) => document.getElementById(id);\n// A malformed link (#%) opens the list instead of breaking the page.\nconst fromHash = () => { try { return decodeURIComponent(location.hash.slice(1)); } catch (e) { return ''; } };\n\nfetch('/api/mapping').then((r) => r.json()).then((v) => {\n if (v.error) { $('sub').textContent = v.error; return; }\n S.v = v;\n S.readings = Object.fromEntries(v.summary.readings.map((r) => [r.nodeId, r]));\n const hash = fromHash();\n if (hash.startsWith('repository:')) S.sel = hash;\n header(); render();\n}).catch((e) => { $('sub').textContent = 'Could not read /api/mapping: ' + e.message; });\n\nwindow.addEventListener('hashchange', () => { if (!S.v) return; const h = fromHash(); S.sel = h || null; S.tab = 'repositories'; render(); });\n\nfunction header() {\n const v = S.v, s = v.summary;\n $('title').textContent = v.manifest.hub + ' \u2014 repository mapping';\n document.title = v.manifest.hub + ' mapping';\n $('sub').innerHTML = esc(v.manifest.kind === 'meta' ? 'Meta hub, reading ' + v.hubs.length + ' hubs' : 'Owner hub') +\n (v.manifest.description ? ' \u00B7 ' + esc(v.manifest.description) : '');\n const c = [['repositories', s.repositories], ['features', s.features], ['capabilities', s.capabilities], ['relations', s.relations],\n ['drifted', s.byDrift.drifted], ['unobserved', s.byDrift.unobserved], ['pending revision', s.byRevision.pending],\n ['errors', v.issues.filter((i) => i.severity === 'error').length]];\n $('counts').innerHTML = c.map(([k, n]) => '<span class=\"count\"><b>' + n + '</b>' + esc(k) + '</span>').join('');\n}\n\nfunction tabs() {\n const t = [['repositories', 'Repositories'], ['capabilities', 'Capabilities'], ['hubs', 'Hubs'], ['issues', 'Issues (' + S.v.issues.length + ')']];\n $('tabs').innerHTML = t.map(([k, label]) => '<button role=\"tab\" aria-selected=\"' + (S.tab === k) + '\" data-tab=\"' + k + '\">' + esc(label) + '</button>').join('');\n $('tabs').querySelectorAll('button').forEach((b) => b.onclick = () => { S.tab = b.dataset.tab; render(); });\n}\n\nfunction filters() {\n if (S.tab !== 'repositories') { $('filters').innerHTML = ''; return; }\n const v = S.v;\n const opt = (name, values, label) => '<select data-f=\"' + name + '\"><option value=\"\">' + name + ': all</option>' +\n values.map((x) => '<option value=\"' + esc(x) + '\"' + (S[name] === x ? ' selected' : '') + '>' + esc(label ? label(x) : x) + '</option>').join('') + '</select>';\n const owners = [...new Set(v.repositories.map((r) => r.nodeId.split(':')[1].split('/')[0]))].sort();\n $('filters').innerHTML = '<input type=\"search\" placeholder=\"Search purpose, features, ids\" value=\"' + esc(S.q) + '\" data-f=\"q\">' +\n opt('lifecycle', ['unclassified', 'prototype', 'implementation', 'maintained', 'archived']) +\n opt('drift', ['converged', 'drifted', 'unobserved', 'undeclared']) +\n opt('revision', ['pending', 'revised', 'affirmed']) +\n opt('field', Object.keys(v.summary.fields).sort(), short) +\n (owners.length > 1 ? opt('owner', owners) : '');\n $('filters').querySelectorAll('[data-f]').forEach((el) => el.oninput = () => { S[el.dataset.f] = el.value; list(); });\n}\n\nfunction matches(r) {\n const rd = S.readings[r.nodeId] || {};\n if (S.lifecycle && r.lifecycle !== S.lifecycle) return false;\n if (S.drift && rd.drift !== S.drift) return false;\n if (S.revision && r.revision !== S.revision) return false;\n if (S.field && !r.fields.includes(S.field)) return false;\n if (S.owner && r.nodeId.split(':')[1].split('/')[0] !== S.owner) return false;\n if (!S.q) return true;\n const q = S.q.toLowerCase();\n const feats = S.v.features.filter((f) => f.repository === r.nodeId).map((f) => f.name + ' ' + (f.statement || '')).join(' ');\n return (r.nodeId + ' ' + (r.purpose || '') + ' ' + (r.horizon || '') + ' ' + feats).toLowerCase().includes(q);\n}\n\nfunction list() {\n const v = S.v;\n if (S.tab === 'repositories') {\n const rows = v.repositories.filter(matches);\n if (!rows.length) { $('list').innerHTML = '<div class=\"scroll\"><div class=\"empty\">No repository matches.</div></div>'; return; }\n $('list').innerHTML = '<div class=\"scroll\"><table><thead><tr><th>Repository</th><th>Lifecycle</th><th>Git</th><th>Purpose</th><th>Revision</th></tr></thead><tbody>' +\n rows.map((r) => {\n const rd = S.readings[r.nodeId] || {};\n const git = rd.activity ? esc(rd.activity) + (rd.daysSinceLastCommit != null ? ' \u00B7 ' + rd.daysSinceLastCommit + 'd' : '') : '\u2014';\n return '<tr data-id=\"' + esc(r.nodeId) + '\"' + (S.sel === r.nodeId ? ' class=\"sel\"' : '') + '><td><b>' + esc(short(r.nodeId)) + '</b><br>' +\n r.fields.map((f) => chip(short(f))).join('') + '</td><td>' + esc(r.lifecycle) + '<br><span class=\"' + esc(rd.drift) + '\">' + esc(rd.drift || '') +\n '</span></td><td>' + git + '</td><td><span class=\"chip\">' + esc(r.status) + '</span><div class=\"clip\">' + esc(r.purpose || '') +\n '</div></td><td class=\"' + esc(r.revision) + '\">' + esc(r.revision) + '</td></tr>';\n }).join('') + '</tbody></table></div>';\n $('list').querySelectorAll('tr[data-id]').forEach((tr) => tr.onclick = () => { location.hash = encodeURIComponent(tr.dataset.id); });\n } else if (S.tab === 'capabilities') {\n const providers = {};\n v.capabilities.forEach((c) => c.providedBy.forEach((p) => (providers[c.nodeId] = providers[c.nodeId] || new Set()).add(p)));\n const repoOf = (feature) => { const m = /^feature:([^#]+)#/.exec(feature); return m ? 'repository:' + m[1] : null; };\n v.features.forEach((f) => f.realizes && (providers[f.realizes] = providers[f.realizes] || new Set()).add(f.repository));\n v.relations.filter((r) => r.type === 'realizes' && repoOf(r.from)).forEach((r) => (providers[r.to] = providers[r.to] || new Set()).add(repoOf(r.from)));\n v.relations.filter((r) => r.type === 'provides').forEach((r) => (providers[r.to] = providers[r.to] || new Set()).add(r.from));\n const ids = [...new Set([...v.capabilities.map((c) => c.nodeId), ...Object.keys(providers)])].sort();\n $('list').innerHTML = '<div class=\"scroll\">' + (ids.length ? '<table><thead><tr><th>Capability</th><th>Statement</th><th>Provided by</th></tr></thead><tbody>' +\n ids.map((id) => {\n const c = v.capabilities.find((x) => x.nodeId === id);\n const ps = [...(providers[id] || [])].sort();\n return '<tr><td><b>' + esc(c ? c.name : short(id)) + '</b><br><code>' + esc(id) + '</code>' + (ps.length > 1 ? '<br>' + chip('shared') : '') +\n '</td><td>' + (c ? chip(c.status) + esc(c.statement || '') : '<span class=\"unobserved\">not defined in any hub read</span>') + '</td><td>' +\n ps.map((p) => p.startsWith('repository:') ? '<a href=\"#' + encodeURIComponent(p) + '\">' + esc(short(p)) + '</a>' : esc(p)).join('<br>') + '</td></tr>';\n }).join('') + '</tbody></table>' : '<div class=\"empty\">No capability yet.</div>') + '</div>';\n } else if (S.tab === 'hubs') {\n $('list').innerHTML = '<div class=\"scroll\"><table><thead><tr><th>Hub</th><th>Source</th><th>Repositories</th><th>Issues</th></tr></thead><tbody>' +\n v.hubs.map((h) => '<tr><td><b>' + esc(h.hub) + '</b></td><td><code>' + esc(h.source) + '</code></td><td>' + (h.read ? h.repositories : '<span class=\"drifted\">not read</span>') +\n '</td><td>' + h.issues.length + '</td></tr>').join('') + '</tbody></table></div>';\n } else {\n $('list').innerHTML = '<div class=\"scroll\">' + (v.issues.length ? v.issues.map((i) => '<div class=\"issue\"><span class=\"' + esc(i.severity) + '\">' + esc(i.severity) +\n '</span> ' + esc((i.file || '') + (i.line ? ':' + i.line : '')) + ' ' + (i.nodeId ? '<code>' + esc(i.nodeId) + '</code> ' : '') + esc(i.message) + '</div>').join('')\n : '<div class=\"empty\">No issue.</div>') + '</div>';\n }\n}\n\nfunction detail() {\n const v = S.v, el = $('detail'), layout = $('layout');\n const r = S.tab === 'repositories' && S.sel ? v.repositories.find((x) => x.nodeId === S.sel) : null;\n if (!r) { el.hidden = true; layout.classList.remove('open'); return; }\n el.hidden = false; layout.classList.add('open');\n const rd = S.readings[r.nodeId] || {};\n const feats = v.features.filter((f) => f.repository === r.nodeId);\n const rels = v.relations.filter((x) => x.from === r.nodeId || x.to === r.nodeId);\n const link = (id) => id.startsWith('repository:') ? '<a href=\"#' + encodeURIComponent(id) + '\">' + esc(short(id)) + '</a>' : '<code>' + esc(id) + '</code>';\n const attribution = (x) => '<div class=\"sub\">' + esc(x.statedBy) + ' \u00B7 ' + esc(x.statedAt) + ' \u00B7 <span class=\"' + esc(x.revision) + '\">' + esc(x.revision) + '</span>' +\n (x.revisedBy ? ' by ' + esc(x.revisedBy) : '') + '</div>';\n const url = gh(r.nodeId);\n el.innerHTML = '<button class=\"close\" onclick=\"location.hash=\\'\\'\">Close</button><h2>' + esc(short(r.nodeId)) + '</h2>' +\n '<p>' + chip(r.lifecycle) + chip(r.origin) + chip(r.status) + (url ? ' <a href=\"' + esc(url) + '\" target=\"_blank\" rel=\"noopener\">GitHub \u2197</a>' : '') + '</p>' +\n (r.purpose ? '<h3>Purpose</h3><p>' + esc(r.purpose) + '</p>' : '') +\n (r.horizon ? '<h3>Horizon</h3><p>' + esc(r.horizon) + '</p>' : '') +\n '<h3>Git</h3><p class=\"' + esc(rd.drift) + '\">' + esc(rd.reason || '') + '</p>' +\n (r.observed ? '<p class=\"sub\">' + r.observed.commits + ' commits, ' + r.observed.commitsLast90Days + ' in the 90 days before the reading, ' + r.observed.tags + ' tags \u00B7 read by ' + esc(r.observed.observedBy) + ' ' + esc(r.observed.observedAt) + '</p>' : '') +\n (r.fields.length ? '<h3>Fields</h3><p>' + r.fields.map((f) => chip(short(f))).join('') + '</p>' : '') +\n (r.prototypes.length ? '<h3>Prototypes</h3><p>' + r.prototypes.map((p) => '<code>' + esc(p) + '</code>').join(' ') + '</p>' : '') +\n '<h3>Features (' + feats.length + ')</h3>' + (feats.length ? feats.map((f) => '<div class=\"item\"><b>' + esc(f.name) + '</b> ' + chip(f.status) +\n (f.realizes ? chip('realizes ' + short(f.realizes)) : '') + '<p>' + esc(f.statement || '') + '</p>' + attribution(f) + '</div>').join('') : '<p class=\"sub\">None named yet.</p>') +\n '<h3>Relations (' + rels.length + ')</h3>' + (rels.length ? rels.map((x) => '<div class=\"item\">' + link(x.from) + ' <b>' + esc(x.type) + '</b> ' + link(x.to) +\n (x.description ? '<p>' + esc(x.description) + '</p>' : '') + attribution(x) + '</div>').join('') : '<p class=\"sub\">None named yet.</p>') +\n '<h3>Evidence read</h3><p>' + r.grounding.map((g) => '<code>' + esc(g) + '</code>').join('<br>') + '</p>' + attribution(r) +\n (v.sources[r.nodeId] ? '<p class=\"sub\">From ' + esc(v.sources[r.nodeId]) + '</p>' : '');\n}\n\nfunction render() { tabs(); filters(); list(); detail(); }\n</script>\n</body>\n</html>\n";
9
+ //# sourceMappingURL=page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/node/page.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,YAAY,o4eA+MxB,CAAC"}
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ /**
3
+ * The one page `mw-repomap serve` answers `/` with. It reads `/api/mapping`
4
+ * and renders it: repositories with their lifecycle, git activity, drift and
5
+ * revision state, a repository's features and relations, the capabilities and
6
+ * who provides them, and every issue the reading found. No build step and no
7
+ * external script, so it runs wherever the CLI runs.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.MAPPING_PAGE = void 0;
11
+ exports.MAPPING_PAGE = `<!doctype html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1">
16
+ <title>Repository mapping</title>
17
+ <style>
18
+ :root {
19
+ --bg: #fbfaf7; --panel: #ffffff; --ink: #1d1c1a; --muted: #6b675f; --line: #e3dfd6;
20
+ --accent: #2f5d8a; --ok: #2e7d4f; --warn: #a3650b; --bad: #a83232; --chip: #f1ede4;
21
+ }
22
+ @media (prefers-color-scheme: dark) {
23
+ :root { --bg: #161513; --panel: #1f1e1b; --ink: #ebe7df; --muted: #a29d93; --line: #34322d;
24
+ --accent: #8db7e0; --ok: #7cc79a; --warn: #e3b25c; --bad: #e98a8a; --chip: #2a2925; }
25
+ }
26
+ * { box-sizing: border-box; }
27
+ body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
28
+ header, main { max-width: 1200px; margin: 0 auto; padding: 16px; }
29
+ h1 { font-size: 20px; margin: 0 0 4px; }
30
+ .sub { color: var(--muted); font-size: 13px; }
31
+ .counts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
32
+ .count { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; font-size: 13px; }
33
+ .count b { font-size: 16px; margin-right: 4px; }
34
+ nav { display: flex; gap: 4px; margin: 8px 0 12px; border-bottom: 1px solid var(--line); }
35
+ nav button { background: none; border: 0; border-bottom: 2px solid transparent; color: var(--muted); padding: 8px 10px; font: inherit; cursor: pointer; }
36
+ nav button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
37
+ .filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
38
+ .filters input, .filters select { background: var(--panel); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; font: inherit; font-size: 14px; }
39
+ .filters input { flex: 1 1 220px; }
40
+ .layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
41
+ @media (min-width: 960px) { .layout.open { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); } }
42
+ .scroll { overflow-x: auto; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
43
+ table { border-collapse: collapse; width: 100%; font-size: 14px; }
44
+ th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
45
+ th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; }
46
+ tbody tr { cursor: pointer; }
47
+ tbody tr:hover, tbody tr.sel { background: var(--chip); }
48
+ .chip { display: inline-block; background: var(--chip); border-radius: 999px; padding: 1px 8px; font-size: 12px; margin: 0 4px 2px 0; white-space: nowrap; }
49
+ .converged { color: var(--ok); } .drifted { color: var(--bad); } .unobserved, .undeclared { color: var(--warn); }
50
+ .pending { color: var(--warn); } .affirmed { color: var(--ok); }
51
+ .clip { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; max-width: 420px; }
52
+ aside { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; align-self: start; }
53
+ aside h2 { font-size: 17px; margin: 0 0 4px; word-break: break-word; }
54
+ aside h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); margin: 16px 0 6px; }
55
+ aside p { margin: 4px 0; }
56
+ .item { border-top: 1px solid var(--line); padding: 8px 0; }
57
+ .item:first-of-type { border-top: 0; }
58
+ a { color: var(--accent); }
59
+ .close { float: right; background: none; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); cursor: pointer; padding: 2px 8px; }
60
+ .issue { padding: 8px 10px; border-bottom: 1px solid var(--line); font-size: 14px; }
61
+ .issue .error { color: var(--bad); font-weight: 600; } .issue .warning { color: var(--warn); font-weight: 600; }
62
+ .empty { color: var(--muted); padding: 16px; }
63
+ code { font-size: 13px; }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <header>
68
+ <h1 id="title">Repository mapping</h1>
69
+ <div class="sub" id="sub">Reading…</div>
70
+ <div class="counts" id="counts"></div>
71
+ </header>
72
+ <main>
73
+ <nav role="tablist" id="tabs"></nav>
74
+ <div class="filters" id="filters"></div>
75
+ <div class="layout" id="layout"><div id="list"></div><aside id="detail" hidden></aside></div>
76
+ </main>
77
+ <script>
78
+ const S = { v: null, tab: 'repositories', q: '', lifecycle: '', drift: '', revision: '', field: '', owner: '', sel: null };
79
+ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
80
+ const short = (id) => String(id).replace(/^[a-z-]+:/, '');
81
+ const gh = (id) => { const m = /^repository:([^/]+)\\/(.+)$/.exec(id); return m ? 'https://github.com/' + m[1] + '/' + m[2] : null; };
82
+ const chip = (t, cls) => '<span class="chip ' + esc(cls || '') + '">' + esc(t) + '</span>';
83
+ const $ = (id) => document.getElementById(id);
84
+ // A malformed link (#%) opens the list instead of breaking the page.
85
+ const fromHash = () => { try { return decodeURIComponent(location.hash.slice(1)); } catch (e) { return ''; } };
86
+
87
+ fetch('/api/mapping').then((r) => r.json()).then((v) => {
88
+ if (v.error) { $('sub').textContent = v.error; return; }
89
+ S.v = v;
90
+ S.readings = Object.fromEntries(v.summary.readings.map((r) => [r.nodeId, r]));
91
+ const hash = fromHash();
92
+ if (hash.startsWith('repository:')) S.sel = hash;
93
+ header(); render();
94
+ }).catch((e) => { $('sub').textContent = 'Could not read /api/mapping: ' + e.message; });
95
+
96
+ window.addEventListener('hashchange', () => { if (!S.v) return; const h = fromHash(); S.sel = h || null; S.tab = 'repositories'; render(); });
97
+
98
+ function header() {
99
+ const v = S.v, s = v.summary;
100
+ $('title').textContent = v.manifest.hub + ' — repository mapping';
101
+ document.title = v.manifest.hub + ' mapping';
102
+ $('sub').innerHTML = esc(v.manifest.kind === 'meta' ? 'Meta hub, reading ' + v.hubs.length + ' hubs' : 'Owner hub') +
103
+ (v.manifest.description ? ' · ' + esc(v.manifest.description) : '');
104
+ const c = [['repositories', s.repositories], ['features', s.features], ['capabilities', s.capabilities], ['relations', s.relations],
105
+ ['drifted', s.byDrift.drifted], ['unobserved', s.byDrift.unobserved], ['pending revision', s.byRevision.pending],
106
+ ['errors', v.issues.filter((i) => i.severity === 'error').length]];
107
+ $('counts').innerHTML = c.map(([k, n]) => '<span class="count"><b>' + n + '</b>' + esc(k) + '</span>').join('');
108
+ }
109
+
110
+ function tabs() {
111
+ const t = [['repositories', 'Repositories'], ['capabilities', 'Capabilities'], ['hubs', 'Hubs'], ['issues', 'Issues (' + S.v.issues.length + ')']];
112
+ $('tabs').innerHTML = t.map(([k, label]) => '<button role="tab" aria-selected="' + (S.tab === k) + '" data-tab="' + k + '">' + esc(label) + '</button>').join('');
113
+ $('tabs').querySelectorAll('button').forEach((b) => b.onclick = () => { S.tab = b.dataset.tab; render(); });
114
+ }
115
+
116
+ function filters() {
117
+ if (S.tab !== 'repositories') { $('filters').innerHTML = ''; return; }
118
+ const v = S.v;
119
+ const opt = (name, values, label) => '<select data-f="' + name + '"><option value="">' + name + ': all</option>' +
120
+ values.map((x) => '<option value="' + esc(x) + '"' + (S[name] === x ? ' selected' : '') + '>' + esc(label ? label(x) : x) + '</option>').join('') + '</select>';
121
+ const owners = [...new Set(v.repositories.map((r) => r.nodeId.split(':')[1].split('/')[0]))].sort();
122
+ $('filters').innerHTML = '<input type="search" placeholder="Search purpose, features, ids" value="' + esc(S.q) + '" data-f="q">' +
123
+ opt('lifecycle', ['unclassified', 'prototype', 'implementation', 'maintained', 'archived']) +
124
+ opt('drift', ['converged', 'drifted', 'unobserved', 'undeclared']) +
125
+ opt('revision', ['pending', 'revised', 'affirmed']) +
126
+ opt('field', Object.keys(v.summary.fields).sort(), short) +
127
+ (owners.length > 1 ? opt('owner', owners) : '');
128
+ $('filters').querySelectorAll('[data-f]').forEach((el) => el.oninput = () => { S[el.dataset.f] = el.value; list(); });
129
+ }
130
+
131
+ function matches(r) {
132
+ const rd = S.readings[r.nodeId] || {};
133
+ if (S.lifecycle && r.lifecycle !== S.lifecycle) return false;
134
+ if (S.drift && rd.drift !== S.drift) return false;
135
+ if (S.revision && r.revision !== S.revision) return false;
136
+ if (S.field && !r.fields.includes(S.field)) return false;
137
+ if (S.owner && r.nodeId.split(':')[1].split('/')[0] !== S.owner) return false;
138
+ if (!S.q) return true;
139
+ const q = S.q.toLowerCase();
140
+ const feats = S.v.features.filter((f) => f.repository === r.nodeId).map((f) => f.name + ' ' + (f.statement || '')).join(' ');
141
+ return (r.nodeId + ' ' + (r.purpose || '') + ' ' + (r.horizon || '') + ' ' + feats).toLowerCase().includes(q);
142
+ }
143
+
144
+ function list() {
145
+ const v = S.v;
146
+ if (S.tab === 'repositories') {
147
+ const rows = v.repositories.filter(matches);
148
+ if (!rows.length) { $('list').innerHTML = '<div class="scroll"><div class="empty">No repository matches.</div></div>'; return; }
149
+ $('list').innerHTML = '<div class="scroll"><table><thead><tr><th>Repository</th><th>Lifecycle</th><th>Git</th><th>Purpose</th><th>Revision</th></tr></thead><tbody>' +
150
+ rows.map((r) => {
151
+ const rd = S.readings[r.nodeId] || {};
152
+ const git = rd.activity ? esc(rd.activity) + (rd.daysSinceLastCommit != null ? ' · ' + rd.daysSinceLastCommit + 'd' : '') : '—';
153
+ return '<tr data-id="' + esc(r.nodeId) + '"' + (S.sel === r.nodeId ? ' class="sel"' : '') + '><td><b>' + esc(short(r.nodeId)) + '</b><br>' +
154
+ r.fields.map((f) => chip(short(f))).join('') + '</td><td>' + esc(r.lifecycle) + '<br><span class="' + esc(rd.drift) + '">' + esc(rd.drift || '') +
155
+ '</span></td><td>' + git + '</td><td><span class="chip">' + esc(r.status) + '</span><div class="clip">' + esc(r.purpose || '') +
156
+ '</div></td><td class="' + esc(r.revision) + '">' + esc(r.revision) + '</td></tr>';
157
+ }).join('') + '</tbody></table></div>';
158
+ $('list').querySelectorAll('tr[data-id]').forEach((tr) => tr.onclick = () => { location.hash = encodeURIComponent(tr.dataset.id); });
159
+ } else if (S.tab === 'capabilities') {
160
+ const providers = {};
161
+ v.capabilities.forEach((c) => c.providedBy.forEach((p) => (providers[c.nodeId] = providers[c.nodeId] || new Set()).add(p)));
162
+ const repoOf = (feature) => { const m = /^feature:([^#]+)#/.exec(feature); return m ? 'repository:' + m[1] : null; };
163
+ v.features.forEach((f) => f.realizes && (providers[f.realizes] = providers[f.realizes] || new Set()).add(f.repository));
164
+ v.relations.filter((r) => r.type === 'realizes' && repoOf(r.from)).forEach((r) => (providers[r.to] = providers[r.to] || new Set()).add(repoOf(r.from)));
165
+ v.relations.filter((r) => r.type === 'provides').forEach((r) => (providers[r.to] = providers[r.to] || new Set()).add(r.from));
166
+ const ids = [...new Set([...v.capabilities.map((c) => c.nodeId), ...Object.keys(providers)])].sort();
167
+ $('list').innerHTML = '<div class="scroll">' + (ids.length ? '<table><thead><tr><th>Capability</th><th>Statement</th><th>Provided by</th></tr></thead><tbody>' +
168
+ ids.map((id) => {
169
+ const c = v.capabilities.find((x) => x.nodeId === id);
170
+ const ps = [...(providers[id] || [])].sort();
171
+ return '<tr><td><b>' + esc(c ? c.name : short(id)) + '</b><br><code>' + esc(id) + '</code>' + (ps.length > 1 ? '<br>' + chip('shared') : '') +
172
+ '</td><td>' + (c ? chip(c.status) + esc(c.statement || '') : '<span class="unobserved">not defined in any hub read</span>') + '</td><td>' +
173
+ ps.map((p) => p.startsWith('repository:') ? '<a href="#' + encodeURIComponent(p) + '">' + esc(short(p)) + '</a>' : esc(p)).join('<br>') + '</td></tr>';
174
+ }).join('') + '</tbody></table>' : '<div class="empty">No capability yet.</div>') + '</div>';
175
+ } else if (S.tab === 'hubs') {
176
+ $('list').innerHTML = '<div class="scroll"><table><thead><tr><th>Hub</th><th>Source</th><th>Repositories</th><th>Issues</th></tr></thead><tbody>' +
177
+ v.hubs.map((h) => '<tr><td><b>' + esc(h.hub) + '</b></td><td><code>' + esc(h.source) + '</code></td><td>' + (h.read ? h.repositories : '<span class="drifted">not read</span>') +
178
+ '</td><td>' + h.issues.length + '</td></tr>').join('') + '</tbody></table></div>';
179
+ } else {
180
+ $('list').innerHTML = '<div class="scroll">' + (v.issues.length ? v.issues.map((i) => '<div class="issue"><span class="' + esc(i.severity) + '">' + esc(i.severity) +
181
+ '</span> ' + esc((i.file || '') + (i.line ? ':' + i.line : '')) + ' ' + (i.nodeId ? '<code>' + esc(i.nodeId) + '</code> ' : '') + esc(i.message) + '</div>').join('')
182
+ : '<div class="empty">No issue.</div>') + '</div>';
183
+ }
184
+ }
185
+
186
+ function detail() {
187
+ const v = S.v, el = $('detail'), layout = $('layout');
188
+ const r = S.tab === 'repositories' && S.sel ? v.repositories.find((x) => x.nodeId === S.sel) : null;
189
+ if (!r) { el.hidden = true; layout.classList.remove('open'); return; }
190
+ el.hidden = false; layout.classList.add('open');
191
+ const rd = S.readings[r.nodeId] || {};
192
+ const feats = v.features.filter((f) => f.repository === r.nodeId);
193
+ const rels = v.relations.filter((x) => x.from === r.nodeId || x.to === r.nodeId);
194
+ const link = (id) => id.startsWith('repository:') ? '<a href="#' + encodeURIComponent(id) + '">' + esc(short(id)) + '</a>' : '<code>' + esc(id) + '</code>';
195
+ const attribution = (x) => '<div class="sub">' + esc(x.statedBy) + ' · ' + esc(x.statedAt) + ' · <span class="' + esc(x.revision) + '">' + esc(x.revision) + '</span>' +
196
+ (x.revisedBy ? ' by ' + esc(x.revisedBy) : '') + '</div>';
197
+ const url = gh(r.nodeId);
198
+ el.innerHTML = '<button class="close" onclick="location.hash=\\'\\'">Close</button><h2>' + esc(short(r.nodeId)) + '</h2>' +
199
+ '<p>' + chip(r.lifecycle) + chip(r.origin) + chip(r.status) + (url ? ' <a href="' + esc(url) + '" target="_blank" rel="noopener">GitHub ↗</a>' : '') + '</p>' +
200
+ (r.purpose ? '<h3>Purpose</h3><p>' + esc(r.purpose) + '</p>' : '') +
201
+ (r.horizon ? '<h3>Horizon</h3><p>' + esc(r.horizon) + '</p>' : '') +
202
+ '<h3>Git</h3><p class="' + esc(rd.drift) + '">' + esc(rd.reason || '') + '</p>' +
203
+ (r.observed ? '<p class="sub">' + r.observed.commits + ' commits, ' + r.observed.commitsLast90Days + ' in the 90 days before the reading, ' + r.observed.tags + ' tags · read by ' + esc(r.observed.observedBy) + ' ' + esc(r.observed.observedAt) + '</p>' : '') +
204
+ (r.fields.length ? '<h3>Fields</h3><p>' + r.fields.map((f) => chip(short(f))).join('') + '</p>' : '') +
205
+ (r.prototypes.length ? '<h3>Prototypes</h3><p>' + r.prototypes.map((p) => '<code>' + esc(p) + '</code>').join(' ') + '</p>' : '') +
206
+ '<h3>Features (' + feats.length + ')</h3>' + (feats.length ? feats.map((f) => '<div class="item"><b>' + esc(f.name) + '</b> ' + chip(f.status) +
207
+ (f.realizes ? chip('realizes ' + short(f.realizes)) : '') + '<p>' + esc(f.statement || '') + '</p>' + attribution(f) + '</div>').join('') : '<p class="sub">None named yet.</p>') +
208
+ '<h3>Relations (' + rels.length + ')</h3>' + (rels.length ? rels.map((x) => '<div class="item">' + link(x.from) + ' <b>' + esc(x.type) + '</b> ' + link(x.to) +
209
+ (x.description ? '<p>' + esc(x.description) + '</p>' : '') + attribution(x) + '</div>').join('') : '<p class="sub">None named yet.</p>') +
210
+ '<h3>Evidence read</h3><p>' + r.grounding.map((g) => '<code>' + esc(g) + '</code>').join('<br>') + '</p>' + attribution(r) +
211
+ (v.sources[r.nodeId] ? '<p class="sub">From ' + esc(v.sources[r.nodeId]) + '</p>' : '');
212
+ }
213
+
214
+ function render() { tabs(); filters(); list(); detail(); }
215
+ </script>
216
+ </body>
217
+ </html>
218
+ `;
219
+ //# sourceMappingURL=page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/node/page.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEU,QAAA,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+M3B,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The small web server of `mw-repomap serve`. Read-only: it answers GET, and
3
+ * reads the hub again on every request, so a writer can keep editing the
4
+ * JSONL while a person reads it.
5
+ *
6
+ * GET / the page
7
+ * GET /api/mapping the merged reading, its summary and every issue
8
+ * GET /api/health { ok: true }
9
+ */
10
+ import * as http from 'node:http';
11
+ import { type MappingSummary } from '../summary';
12
+ import type { MergedMapping } from '../merge';
13
+ import type { MappingIssue } from '../types';
14
+ import type { HubRead, HubStatus } from './hub';
15
+ export interface MappingView extends Omit<MergedMapping, 'issues' | 'hubs'> {
16
+ hubs: HubStatus[];
17
+ summary: MappingSummary;
18
+ issues: MappingIssue[];
19
+ }
20
+ /** What `/api/mapping` answers, or `undefined` when the folder holds no readable manifest. */
21
+ export declare function toView(read: HubRead): MappingView | undefined;
22
+ export interface MappingServerOptions {
23
+ /**
24
+ * Host header names (without port) the server answers. When set, any other
25
+ * Host is refused with 421, so a web page that rebinds a name to 127.0.0.1
26
+ * cannot read the mapping through the browser. Absent: every Host is answered.
27
+ */
28
+ allowedHosts?: readonly string[];
29
+ }
30
+ /** The Host names a loopback server answers. */
31
+ export declare const LOOPBACK_HOSTS: readonly ["localhost", "127.0.0.1", "[::1]"];
32
+ export declare function createMappingServer(load: () => Promise<HubRead>, options?: MappingServerOptions): http.Server;
33
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/node/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAa,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGhD,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,MAAM,CAAC;IACzE,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,8FAA8F;AAC9F,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAI7D;AAOD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,gDAAgD;AAChD,eAAO,MAAM,cAAc,8CAA+C,CAAC;AAO3E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,GAAE,oBAAyB,GAAG,IAAI,CAAC,MAAM,CAsCjH"}
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * The small web server of `mw-repomap serve`. Read-only: it answers GET, and
4
+ * reads the hub again on every request, so a writer can keep editing the
5
+ * JSONL while a person reads it.
6
+ *
7
+ * GET / the page
8
+ * GET /api/mapping the merged reading, its summary and every issue
9
+ * GET /api/health { ok: true }
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.LOOPBACK_HOSTS = void 0;
46
+ exports.toView = toView;
47
+ exports.createMappingServer = createMappingServer;
48
+ const http = __importStar(require("node:http"));
49
+ const summary_1 = require("../summary");
50
+ const page_1 = require("./page");
51
+ /** What `/api/mapping` answers, or `undefined` when the folder holds no readable manifest. */
52
+ function toView(read) {
53
+ if (!read.merged)
54
+ return undefined;
55
+ const { issues: _mergeIssues, hubs: _hubNames, ...merged } = read.merged;
56
+ return { ...merged, hubs: read.hubs, summary: (0, summary_1.summarize)(read.merged), issues: read.issues };
57
+ }
58
+ function send(res, status, body, type) {
59
+ res.writeHead(status, { 'content-type': type, 'cache-control': 'no-store' });
60
+ res.end(body);
61
+ }
62
+ /** The Host names a loopback server answers. */
63
+ exports.LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]'];
64
+ function hostName(header) {
65
+ if (!header)
66
+ return '';
67
+ return (header.startsWith('[') ? header.slice(0, header.indexOf(']') + 1) : header.split(':')[0]).toLowerCase();
68
+ }
69
+ function createMappingServer(load, options = {}) {
70
+ const allowed = options.allowedHosts?.map((h) => h.toLowerCase());
71
+ return http.createServer((req, res) => {
72
+ if (allowed && !allowed.includes(hostName(req.headers.host))) {
73
+ send(res, 421, JSON.stringify({ error: `not served to Host ${req.headers.host ?? '(none)'}` }), 'application/json');
74
+ return;
75
+ }
76
+ let url;
77
+ try {
78
+ url = new URL(req.url ?? '/', 'http://localhost');
79
+ }
80
+ catch {
81
+ send(res, 400, JSON.stringify({ error: 'not a request path' }), 'application/json');
82
+ return;
83
+ }
84
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
85
+ send(res, 405, JSON.stringify({ error: 'read-only: GET only' }), 'application/json');
86
+ return;
87
+ }
88
+ if (url.pathname === '/' || url.pathname === '/index.html') {
89
+ send(res, 200, page_1.MAPPING_PAGE, 'text/html; charset=utf-8');
90
+ return;
91
+ }
92
+ if (url.pathname === '/api/health') {
93
+ send(res, 200, JSON.stringify({ ok: true }), 'application/json');
94
+ return;
95
+ }
96
+ if (url.pathname === '/api/mapping') {
97
+ load()
98
+ .then((read) => {
99
+ const view = toView(read);
100
+ if (view)
101
+ send(res, 200, JSON.stringify(view), 'application/json');
102
+ else
103
+ send(res, 404, JSON.stringify({ error: 'no mapping here', issues: read.issues }), 'application/json');
104
+ })
105
+ .catch((error) => send(res, 500, JSON.stringify({ error: error.message }), 'application/json'));
106
+ return;
107
+ }
108
+ send(res, 404, JSON.stringify({ error: `no route ${url.pathname}` }), 'application/json');
109
+ });
110
+ }
111
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/node/server.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBH,wBAIC;AAwBD,kDAsCC;AAhFD,gDAAkC;AAClC,wCAA4D;AAI5D,iCAAsC;AAQtC,8FAA8F;AAC9F,SAAgB,MAAM,CAAC,IAAa;IAClC,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACnC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;IACzE,OAAO,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAA,mBAAS,EAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AAC9F,CAAC;AAED,SAAS,IAAI,CAAC,GAAwB,EAAE,MAAc,EAAE,IAAY,EAAE,IAAY;IAChF,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC,CAAC;IAC7E,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAWD,gDAAgD;AACnC,QAAA,cAAc,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAU,CAAC;AAE3E,SAAS,QAAQ,CAAC,MAA0B;IAC1C,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IACvB,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;AAClH,CAAC;AAED,SAAgB,mBAAmB,CAAC,IAA4B,EAAE,UAAgC,EAAE;IAClG,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAClE,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACpC,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC7D,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,sBAAsB,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,QAAQ,EAAE,EAAE,CAAC,EAAE,kBAAkB,CAAC,CAAC;YACpH,OAAO;QACT,CAAC;QACD,IAAI,GAAQ,CAAC;QACb,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC,EAAE,kBAAkB,CAAC,CAAC;YACpF,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAClD,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,EAAE,kBAAkB,CAAC,CAAC;YACrF,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YAC3D,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,mBAAY,EAAE,0BAA0B,CAAC,CAAC;YACzD,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YACnC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,kBAAkB,CAAC,CAAC;YACjE,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,KAAK,cAAc,EAAE,CAAC;YACpC,IAAI,EAAE;iBACH,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;gBACb,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;gBAC1B,IAAI,IAAI;oBAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;;oBAC9D,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,kBAAkB,CAAC,CAAC;YAC7G,CAAC,CAAC;iBACD,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAG,KAAe,CAAC,OAAO,EAAE,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC;YAC7G,OAAO;QACT,CAAC;QACD,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,YAAY,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC5F,CAAC,CAAC,CAAC;AACL,CAAC"}
package/dist/read.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Read a mapping folder through any reader: the file system, GitHub, a test.
3
+ * The reader returns a file's text, or `undefined` when the file is absent.
4
+ * Only `manifest.json` is required; an absent JSONL file is an empty one.
5
+ */
6
+ import { type MappingIssue, type RepositoryMapping } from './types';
7
+ export type MappingReader = (file: string) => Promise<string | undefined>;
8
+ export interface MappingRead {
9
+ mapping?: RepositoryMapping;
10
+ issues: MappingIssue[];
11
+ }
12
+ export declare function readMapping(read: MappingReader, label?: string): Promise<MappingRead>;
13
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAYH,OAAO,EAAiB,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEnF,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAE1E,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAyB3F"}
package/dist/read.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ /**
3
+ * Read a mapping folder through any reader: the file system, GitHub, a test.
4
+ * The reader returns a file's text, or `undefined` when the file is absent.
5
+ * Only `manifest.json` is required; an absent JSONL file is an empty one.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.readMapping = readMapping;
9
+ const capabilities_1 = require("@medicine-wheel/capabilities");
10
+ const jsonl_1 = require("./jsonl");
11
+ const schemas_1 = require("./schemas");
12
+ const types_1 = require("./types");
13
+ async function readMapping(read, label) {
14
+ const where = (file) => (label ? `${label}/${file}` : file);
15
+ const manifestText = await read(types_1.MAPPING_FILES.manifest);
16
+ if (manifestText === undefined) {
17
+ return { issues: [{ severity: 'error', file: where(types_1.MAPPING_FILES.manifest), message: 'no manifest.json: not a mapping folder (mw-repomap init makes one)' }] };
18
+ }
19
+ const manifest = (0, jsonl_1.parseJsonDocument)(manifestText, schemas_1.MappingManifestSchema, where(types_1.MAPPING_FILES.manifest));
20
+ if (!manifest.value)
21
+ return { issues: manifest.issues };
22
+ const issues = [];
23
+ const rows = async (file, schema) => {
24
+ const parsed = (0, jsonl_1.parseJsonl)(await read(file) ?? '', schema, where(file));
25
+ issues.push(...parsed.issues);
26
+ return parsed.rows;
27
+ };
28
+ const mapping = {
29
+ manifest: manifest.value,
30
+ repositories: await rows(types_1.MAPPING_FILES.repositories, schemas_1.RepositoryFacetSchema),
31
+ features: await rows(types_1.MAPPING_FILES.features, schemas_1.FeatureFacetSchema),
32
+ capabilities: await rows(types_1.MAPPING_FILES.capabilities, capabilities_1.CapabilityFacetSchema),
33
+ relations: await rows(types_1.MAPPING_FILES.relations, schemas_1.RelationRowSchema),
34
+ owners: await rows(types_1.MAPPING_FILES.owners, schemas_1.OwnerRegistrationSchema),
35
+ };
36
+ return { mapping, issues };
37
+ }
38
+ //# sourceMappingURL=read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAqBH,kCAyBC;AA3CD,+DAAqE;AACrE,mCAAwD;AACxD,uCAMmB;AACnB,mCAAmF;AAS5E,KAAK,UAAU,WAAW,CAAC,IAAmB,EAAE,KAAc;IACnE,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACpE,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,qBAAa,CAAC,QAAQ,CAAC,CAAC;IACxD,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,qBAAa,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,oEAAoE,EAAE,CAAC,EAAE,CAAC;IACjK,CAAC;IACD,MAAM,QAAQ,GAAG,IAAA,yBAAiB,EAAC,YAAY,EAAE,+BAAqB,EAAE,KAAK,CAAC,qBAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvG,IAAI,CAAC,QAAQ,CAAC,KAAK;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IAExD,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,KAAK,EAAK,IAAY,EAAE,MAA2C,EAAgB,EAAE;QAChG,MAAM,MAAM,GAAG,IAAA,kBAAU,EAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACvE,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9B,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC,CAAC;IAEF,MAAM,OAAO,GAAsB;QACjC,QAAQ,EAAE,QAAQ,CAAC,KAAK;QACxB,YAAY,EAAE,MAAM,IAAI,CAAC,qBAAa,CAAC,YAAY,EAAE,+BAAqB,CAAC;QAC3E,QAAQ,EAAE,MAAM,IAAI,CAAC,qBAAa,CAAC,QAAQ,EAAE,4BAAkB,CAAC;QAChE,YAAY,EAAE,MAAM,IAAI,CAAC,qBAAa,CAAC,YAAY,EAAE,oCAAqB,CAAC;QAC3E,SAAS,EAAE,MAAM,IAAI,CAAC,qBAAa,CAAC,SAAS,EAAE,2BAAiB,CAAC;QACjE,MAAM,EAAE,MAAM,IAAI,CAAC,qBAAa,CAAC,MAAM,EAAE,iCAAuB,CAAC;KAClE,CAAC;IACF,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC"}