mhproto 0.8.0-preview.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.
- package/CONTRIBUTING.md +62 -0
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/bin/mhproto.mjs +248 -0
- package/doc/context-design.md +43 -0
- package/doc/context-measurements.json +28 -0
- package/doc/guide.md +138 -0
- package/doc/publishing.md +93 -0
- package/doc/release-notes.md +23 -0
- package/doc/release-review-2026-10-02.md +127 -0
- package/doc/release-review.md +68 -0
- package/doc/viewer-design.md +80 -0
- package/package.json +69 -0
- package/skills/mhproto-discover/SKILL.md +14 -0
- package/skills/mhproto-implement/SKILL.md +14 -0
- package/skills/mhproto-reconcile/SKILL.md +14 -0
- package/skills/mhproto-specify/SKILL.md +16 -0
- package/skills/mhproto-specify/references/format.md +20 -0
- package/skills/mhproto-verify/SKILL.md +14 -0
- package/src/config.mjs +202 -0
- package/src/context.mjs +228 -0
- package/src/core.mjs +471 -0
- package/src/node-reporter.mjs +15 -0
- package/src/paths.mjs +51 -0
- package/src/server.mjs +290 -0
- package/src/verify.mjs +149 -0
- package/src/visuals.mjs +189 -0
- package/viewer/app.js +1463 -0
- package/viewer/diff.js +245 -0
- package/viewer/index.html +51 -0
- package/viewer/style.css +1003 -0
- package/viewer/vendor/LICENSE +21 -0
- package/viewer/vendor/NOTICE.txt +3372 -0
- package/viewer/vendor/README.md +75 -0
- package/viewer/vendor/build-evidence.json +2775 -0
- package/viewer/vendor/bundled-audit.json +88 -0
- package/viewer/vendor/bundled-packages.json +84 -0
- package/viewer/vendor/embedded-notices.txt +33 -0
- package/viewer/vendor/license-inventory.json +1456 -0
- package/viewer/vendor/licenses/braintree__sanitize-url-7.1.2.txt +21 -0
- package/viewer/vendor/licenses/chevrotain-13.2.0.txt +202 -0
- package/viewer/vendor/licenses/chevrotain-allstar-0.5.0.txt +16 -0
- package/viewer/vendor/licenses/chevrotain__cst-dts-gen-13.2.0.txt +202 -0
- package/viewer/vendor/licenses/chevrotain__gast-13.2.0.txt +202 -0
- package/viewer/vendor/licenses/chevrotain__regexp-to-ast-13.2.0.txt +202 -0
- package/viewer/vendor/licenses/chevrotain__utils-13.2.0.txt +202 -0
- package/viewer/vendor/licenses/cose-base-1.0.3.txt +21 -0
- package/viewer/vendor/licenses/cose-base-2.2.0.txt +21 -0
- package/viewer/vendor/licenses/cytoscape-3.34.3.txt +19 -0
- package/viewer/vendor/licenses/cytoscape-cose-bilkent-4.1.0.txt +21 -0
- package/viewer/vendor/licenses/cytoscape-fcose-2.2.0.txt +19 -0
- package/viewer/vendor/licenses/d3-7.9.0.txt +13 -0
- package/viewer/vendor/licenses/d3-array-2.12.1.txt +27 -0
- package/viewer/vendor/licenses/d3-array-3.2.4.txt +13 -0
- package/viewer/vendor/licenses/d3-axis-3.0.0.txt +13 -0
- package/viewer/vendor/licenses/d3-brush-3.0.0.txt +13 -0
- package/viewer/vendor/licenses/d3-chord-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-color-3.1.0.txt +13 -0
- package/viewer/vendor/licenses/d3-contour-4.0.2.txt +13 -0
- package/viewer/vendor/licenses/d3-delaunay-6.0.4.txt +14 -0
- package/viewer/vendor/licenses/d3-dispatch-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-drag-3.0.0.txt +13 -0
- package/viewer/vendor/licenses/d3-dsv-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-ease-3.0.1.txt +28 -0
- package/viewer/vendor/licenses/d3-fetch-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-force-3.0.0.txt +13 -0
- package/viewer/vendor/licenses/d3-format-3.1.2.txt +13 -0
- package/viewer/vendor/licenses/d3-geo-3.1.1.txt +34 -0
- package/viewer/vendor/licenses/d3-hierarchy-3.1.2.txt +13 -0
- package/viewer/vendor/licenses/d3-interpolate-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-path-1.0.9.txt +27 -0
- package/viewer/vendor/licenses/d3-path-3.1.0.txt +13 -0
- package/viewer/vendor/licenses/d3-polygon-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-quadtree-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-random-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-sankey-0.12.3.txt +27 -0
- package/viewer/vendor/licenses/d3-scale-4.0.2.txt +13 -0
- package/viewer/vendor/licenses/d3-scale-chromatic-3.1.0.txt +28 -0
- package/viewer/vendor/licenses/d3-selection-3.0.0.txt +13 -0
- package/viewer/vendor/licenses/d3-shape-1.3.7.txt +27 -0
- package/viewer/vendor/licenses/d3-shape-3.2.0.txt +13 -0
- package/viewer/vendor/licenses/d3-time-3.1.0.txt +13 -0
- package/viewer/vendor/licenses/d3-time-format-4.1.0.txt +13 -0
- package/viewer/vendor/licenses/d3-timer-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-transition-3.0.1.txt +13 -0
- package/viewer/vendor/licenses/d3-zoom-3.0.0.txt +13 -0
- package/viewer/vendor/licenses/dagre-d3-es-7.0.14.txt +23 -0
- package/viewer/vendor/licenses/dayjs-1.11.23.txt +21 -0
- package/viewer/vendor/licenses/delaunator-5.1.0.txt +15 -0
- package/viewer/vendor/licenses/dompurify-3.4.16.txt +202 -0
- package/viewer/vendor/licenses/elk-source-notice.txt +11 -0
- package/viewer/vendor/licenses/elkjs-0.9.3.txt +264 -0
- package/viewer/vendor/licenses/embedded-and-node-notices.txt +45 -0
- package/viewer/vendor/licenses/es-toolkit-1.52.0-1.txt +39 -0
- package/viewer/vendor/licenses/es-toolkit-1.52.0.txt +21 -0
- package/viewer/vendor/licenses/fastdom-1.0.12.txt +221 -0
- package/viewer/vendor/licenses/iconify__utils-3.1.7.txt +21 -0
- package/viewer/vendor/licenses/internmap-1.0.1.txt +13 -0
- package/viewer/vendor/licenses/internmap-2.0.3.txt +13 -0
- package/viewer/vendor/licenses/js-yaml-4.3.2.txt +21 -0
- package/viewer/vendor/licenses/katex-0.16.47.txt +21 -0
- package/viewer/vendor/licenses/khroma-2.1.0.txt +21 -0
- package/viewer/vendor/licenses/langium-4.4.0.txt +16 -0
- package/viewer/vendor/licenses/layout-base-1.0.2.txt +21 -0
- package/viewer/vendor/licenses/layout-base-2.0.1.txt +21 -0
- package/viewer/vendor/licenses/lodash-es-4.18.1.txt +47 -0
- package/viewer/vendor/licenses/marked-16.4.2.txt +44 -0
- package/viewer/vendor/licenses/mermaid-12.1.0.txt +21 -0
- package/viewer/vendor/licenses/mermaid-js__parser-2.0.1.txt +21 -0
- package/viewer/vendor/licenses/path-browserify-1.0.1.txt +20 -0
- package/viewer/vendor/licenses/robust-predicates-3.0.3.txt +24 -0
- package/viewer/vendor/licenses/roughjs-4.6.6.txt +21 -0
- package/viewer/vendor/licenses/stylis-4.4.0.txt +21 -0
- package/viewer/vendor/licenses/ts-dedent-2.3.0.txt +21 -0
- package/viewer/vendor/licenses/upsetjs__venn.js-2.0.0.txt +22 -0
- package/viewer/vendor/licenses/uuid-14.0.2.txt +9 -0
- package/viewer/vendor/licenses/vscode-jsonrpc-9.0.3.txt +11 -0
- package/viewer/vendor/licenses/vscode-languageserver-protocol-3.18.4.txt +11 -0
- package/viewer/vendor/licenses/vscode-languageserver-textdocument-1.0.15.txt +11 -0
- package/viewer/vendor/licenses/vscode-languageserver-types-3.18.4.txt +11 -0
- package/viewer/vendor/licenses/vscode-uri-3.1.0.txt +9 -0
- package/viewer/vendor/manifest.json +21 -0
- package/viewer/vendor/mermaid.min.js +7729 -0
package/viewer/diff.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Pure contract comparison, shared by the CLI, server and browser.
|
|
2
|
+
const unordered = new Set([
|
|
3
|
+
'required',
|
|
4
|
+
'enum',
|
|
5
|
+
'type',
|
|
6
|
+
'rules',
|
|
7
|
+
'operations',
|
|
8
|
+
'examples',
|
|
9
|
+
'sources',
|
|
10
|
+
'testNames',
|
|
11
|
+
'x-mhproto-rules',
|
|
12
|
+
'x-clauses',
|
|
13
|
+
]);
|
|
14
|
+
export function canonical(value, key = '', parents = []) {
|
|
15
|
+
const path = [...parents, key];
|
|
16
|
+
const literal =
|
|
17
|
+
path.some(
|
|
18
|
+
(part, i) =>
|
|
19
|
+
['example', 'default', 'const', 'value'].includes(part) && path[i - 1] !== 'properties',
|
|
20
|
+
) || path.some((part, i) => part === 'body' && ['request', 'response'].includes(path[i - 1]));
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
const items = value.map((v) => canonical(v, '', path));
|
|
23
|
+
return !literal && unordered.has(key)
|
|
24
|
+
? items.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
|
|
25
|
+
: items;
|
|
26
|
+
}
|
|
27
|
+
if (value && typeof value === 'object')
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Object.keys(value)
|
|
30
|
+
.sort()
|
|
31
|
+
.filter((k) => value[k] !== undefined)
|
|
32
|
+
.map((k) => [k, canonical(value[k], k, path)]),
|
|
33
|
+
);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function snapshotProject(value) {
|
|
37
|
+
const wrapped = ['mhproto-snapshot', 'bive-snapshot'].includes(value?.format);
|
|
38
|
+
if (wrapped && value.version !== 1) throw new Error('Unsupported MHProto snapshot version.');
|
|
39
|
+
const project = wrapped ? value.project : value;
|
|
40
|
+
if (
|
|
41
|
+
!project ||
|
|
42
|
+
!Array.isArray(project.capabilities) ||
|
|
43
|
+
!project.capabilities.length ||
|
|
44
|
+
typeof project.name !== 'string'
|
|
45
|
+
)
|
|
46
|
+
throw new Error('Choose a MHProto snapshot JSON or exported preview HTML.');
|
|
47
|
+
const ids = new Set();
|
|
48
|
+
for (const cap of project.capabilities) {
|
|
49
|
+
if (
|
|
50
|
+
!cap ||
|
|
51
|
+
typeof cap.id !== 'string' ||
|
|
52
|
+
ids.has(cap.id) ||
|
|
53
|
+
!Array.isArray(cap.operations) ||
|
|
54
|
+
!Array.isArray(cap.rules) ||
|
|
55
|
+
!Array.isArray(cap.examples) ||
|
|
56
|
+
!Array.isArray(cap.checks) ||
|
|
57
|
+
!cap.openapi
|
|
58
|
+
)
|
|
59
|
+
throw new Error('The baseline does not contain a valid MHProto contract.');
|
|
60
|
+
ids.add(cap.id);
|
|
61
|
+
}
|
|
62
|
+
return project;
|
|
63
|
+
}
|
|
64
|
+
export function contractSnapshot(
|
|
65
|
+
value,
|
|
66
|
+
{ label = 'Iteration baseline', createdAt = new Date().toISOString() } = {},
|
|
67
|
+
) {
|
|
68
|
+
const project = snapshotProject(value);
|
|
69
|
+
// Detach nested schemas and visuals: later viewer edits must not move the baseline.
|
|
70
|
+
return JSON.parse(
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
format: 'mhproto-snapshot',
|
|
73
|
+
version: 1,
|
|
74
|
+
label,
|
|
75
|
+
createdAt,
|
|
76
|
+
project: {
|
|
77
|
+
name: project.name,
|
|
78
|
+
system: project.system ?? '',
|
|
79
|
+
visuals: project.visuals ?? [],
|
|
80
|
+
capabilities: project.capabilities.map(({ evidence, digest, ...cap }) => cap),
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const status = (a, b) => (a === undefined ? 'added' : b === undefined ? 'removed' : 'changed');
|
|
86
|
+
export function fieldChanges(before, after, path = []) {
|
|
87
|
+
if (
|
|
88
|
+
JSON.stringify(canonical(before, path.at(-1), path.slice(0, -1))) ===
|
|
89
|
+
JSON.stringify(canonical(after, path.at(-1), path.slice(0, -1)))
|
|
90
|
+
)
|
|
91
|
+
return [];
|
|
92
|
+
const object = (value) => value && typeof value === 'object' && !Array.isArray(value);
|
|
93
|
+
if (object(before) && object(after))
|
|
94
|
+
return [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
95
|
+
.sort()
|
|
96
|
+
.flatMap((key) => fieldChanges(before[key], after[key], [...path, key]));
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
path,
|
|
100
|
+
status: status(before, after),
|
|
101
|
+
beforePresent: before !== undefined,
|
|
102
|
+
afterPresent: after !== undefined,
|
|
103
|
+
before: before ?? null,
|
|
104
|
+
after: after ?? null,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
function behaviourText(text = '') {
|
|
109
|
+
let rule = false,
|
|
110
|
+
fence;
|
|
111
|
+
return text
|
|
112
|
+
.replaceAll('\r\n', '\n')
|
|
113
|
+
.split('\n')
|
|
114
|
+
.filter((line) => {
|
|
115
|
+
const marker = /^\s{0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
|
116
|
+
if (marker) {
|
|
117
|
+
if (!fence) fence = marker;
|
|
118
|
+
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;
|
|
119
|
+
rule = false;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (fence) return true;
|
|
123
|
+
if (/^\s*-\s+\*\*[A-Z][A-Z0-9-]*-\d+\*\*/.test(line)) {
|
|
124
|
+
rule = true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (rule && /^\s{2,}\S/.test(line)) return false;
|
|
128
|
+
rule = false;
|
|
129
|
+
return true;
|
|
130
|
+
})
|
|
131
|
+
.map((line) => line.trimEnd())
|
|
132
|
+
.join('\n')
|
|
133
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
134
|
+
.trim();
|
|
135
|
+
}
|
|
136
|
+
export function compareModels(beforeValue, afterValue) {
|
|
137
|
+
const before = snapshotProject(beforeValue),
|
|
138
|
+
after = snapshotProject(afterValue),
|
|
139
|
+
changes = [];
|
|
140
|
+
const add = (capability, kind, id, a, b) => {
|
|
141
|
+
const fields = fieldChanges(a, b);
|
|
142
|
+
if (fields.length)
|
|
143
|
+
changes.push({
|
|
144
|
+
capability,
|
|
145
|
+
kind,
|
|
146
|
+
id,
|
|
147
|
+
status: status(a, b),
|
|
148
|
+
before: a ?? null,
|
|
149
|
+
after: b ?? null,
|
|
150
|
+
fields,
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const compare = (capability, kind, oldItems, newItems, key) => {
|
|
154
|
+
const oldMap = new Map(oldItems.map((x) => [x[key], x])),
|
|
155
|
+
newMap = new Map(newItems.map((x) => [x[key], x]));
|
|
156
|
+
for (const id of new Set([...oldMap.keys(), ...newMap.keys()]))
|
|
157
|
+
add(capability, kind, id, oldMap.get(id), newMap.get(id));
|
|
158
|
+
};
|
|
159
|
+
add(
|
|
160
|
+
null,
|
|
161
|
+
'system',
|
|
162
|
+
'system',
|
|
163
|
+
{ name: before.name, system: before.system ?? '' },
|
|
164
|
+
{ name: after.name, system: after.system ?? '' },
|
|
165
|
+
);
|
|
166
|
+
const oldCaps = new Map(before.capabilities.map((c) => [c.id, c])),
|
|
167
|
+
newCaps = new Map(after.capabilities.map((c) => [c.id, c]));
|
|
168
|
+
const feature = (cap) => {
|
|
169
|
+
if (!cap) return undefined;
|
|
170
|
+
const { operations, ruleTitles, ...presentation } = cap.presentation ?? {};
|
|
171
|
+
const { paths, components, ...api } = cap.openapi,
|
|
172
|
+
{ schemas, ...shared } = components ?? {};
|
|
173
|
+
const pathItems = Object.fromEntries(
|
|
174
|
+
Object.entries(paths ?? {}).map(([url, item]) => [
|
|
175
|
+
url,
|
|
176
|
+
Object.fromEntries(
|
|
177
|
+
Object.entries(item).filter(
|
|
178
|
+
([key]) =>
|
|
179
|
+
![
|
|
180
|
+
'get',
|
|
181
|
+
'post',
|
|
182
|
+
'put',
|
|
183
|
+
'patch',
|
|
184
|
+
'delete',
|
|
185
|
+
'options',
|
|
186
|
+
'head',
|
|
187
|
+
'trace',
|
|
188
|
+
'parameters',
|
|
189
|
+
].includes(key),
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
]),
|
|
193
|
+
);
|
|
194
|
+
return {
|
|
195
|
+
title: cap.title ?? cap.id,
|
|
196
|
+
description: cap.description ?? '',
|
|
197
|
+
url: cap.url ?? '',
|
|
198
|
+
behaviour: behaviourText(cap.prose),
|
|
199
|
+
api: { ...api, components: shared, pathItems },
|
|
200
|
+
transitions: cap.transitions ?? [],
|
|
201
|
+
nonTransitions: cap.nonTransitions ?? [],
|
|
202
|
+
presentation,
|
|
203
|
+
files: cap.files ?? {},
|
|
204
|
+
sources: cap.sources ?? [],
|
|
205
|
+
gaps: cap.gaps ?? [],
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
const rules = (cap) =>
|
|
209
|
+
(cap?.rules ?? []).map((r) => ({ ...r, title: cap.presentation?.ruleTitles?.[r.id] ?? r.id }));
|
|
210
|
+
const operations = (cap) =>
|
|
211
|
+
(cap?.operations ?? []).map((op) => ({
|
|
212
|
+
...op,
|
|
213
|
+
presentation: cap.presentation?.operations?.[op.operationId] ?? {},
|
|
214
|
+
}));
|
|
215
|
+
for (const id of new Set([...oldCaps.keys(), ...newCaps.keys()])) {
|
|
216
|
+
const a = oldCaps.get(id),
|
|
217
|
+
b = newCaps.get(id);
|
|
218
|
+
add(id, 'feature', id, feature(a), feature(b));
|
|
219
|
+
compare(id, 'rule', rules(a), rules(b), 'id');
|
|
220
|
+
compare(id, 'operation', operations(a), operations(b), 'operationId');
|
|
221
|
+
compare(id, 'example', a?.examples ?? [], b?.examples ?? [], 'id');
|
|
222
|
+
compare(id, 'check', a?.checks ?? [], b?.checks ?? [], 'id');
|
|
223
|
+
compare(
|
|
224
|
+
id,
|
|
225
|
+
'schema',
|
|
226
|
+
Object.entries(a?.openapi.components?.schemas ?? {}).map(([name, schema]) => ({
|
|
227
|
+
name,
|
|
228
|
+
schema,
|
|
229
|
+
})),
|
|
230
|
+
Object.entries(b?.openapi.components?.schemas ?? {}).map(([name, schema]) => ({
|
|
231
|
+
name,
|
|
232
|
+
schema,
|
|
233
|
+
})),
|
|
234
|
+
'name',
|
|
235
|
+
);
|
|
236
|
+
compare(
|
|
237
|
+
id,
|
|
238
|
+
'visual',
|
|
239
|
+
(before.visuals ?? []).filter((v) => v.target?.capability === id),
|
|
240
|
+
(after.visuals ?? []).filter((v) => v.target?.capability === id),
|
|
241
|
+
'id',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return changes;
|
|
245
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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" />
|
|
6
|
+
<title>MHProto</title>
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="A shared contract for humans and agents: linked behaviour, API types, examples and verification."
|
|
10
|
+
/>
|
|
11
|
+
<link
|
|
12
|
+
rel="icon"
|
|
13
|
+
type="image/svg+xml"
|
|
14
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' fill='white'/%3E%3Ctext x='3' y='43' font-family='monospace' font-size='26' font-weight='700' fill='%23141414'%3E%5Bmh%5D%3C/text%3E%3C/svg%3E"
|
|
15
|
+
/>
|
|
16
|
+
<link rel="stylesheet" href="/style.css" />
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<aside class="sidebar">
|
|
20
|
+
<a class="brand" id="home" href="https://mhproto.dev/" aria-label="MHProto homepage"
|
|
21
|
+
><span class="brand-mark">[mh]</span><span>mhproto</span></a
|
|
22
|
+
>
|
|
23
|
+
<div class="nav-label">Project</div>
|
|
24
|
+
<div id="project-name" class="project-name">Loading…</div>
|
|
25
|
+
<div class="nav-label">Features</div>
|
|
26
|
+
<nav id="features" aria-label="Features"></nav>
|
|
27
|
+
<div class="sidebar-footer">
|
|
28
|
+
<a id="changes-link" href="#/changes">Changes</a><a id="sources-link" href="#">Sources</a
|
|
29
|
+
><button id="save-preview" class="text-button" hidden>Save preview</button
|
|
30
|
+
><span id="view-mode"></span>
|
|
31
|
+
</div>
|
|
32
|
+
</aside>
|
|
33
|
+
<main>
|
|
34
|
+
<div class="search-row">
|
|
35
|
+
<label class="sr-only" for="search">Search this project</label
|
|
36
|
+
><input
|
|
37
|
+
id="search"
|
|
38
|
+
type="search"
|
|
39
|
+
placeholder="Search this project…"
|
|
40
|
+
autocomplete="off"
|
|
41
|
+
/><kbd>/</kbd>
|
|
42
|
+
</div>
|
|
43
|
+
<div id="content" aria-live="polite">Loading…</div>
|
|
44
|
+
<footer class="page-footer">
|
|
45
|
+
<span id="freshness"></span><span id="read-mode">Read-only</span>
|
|
46
|
+
</footer>
|
|
47
|
+
</main>
|
|
48
|
+
<script src="/vendor/mermaid.min.js"></script>
|
|
49
|
+
<script type="module" src="/app.js"></script>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|