lowkey-doc 0.0.0-stage → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "lowkey-doc",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Render a folder of Markdown into one self-contained documentation dashboard, with flowcharts and ER diagrams that stay readable when they get dense.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/lorebalbo/lowkey-doc.git"
8
+ },
9
+ "homepage": "https://github.com/lorebalbo/lowkey-doc#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/lorebalbo/lowkey-doc/issues"
12
+ },
13
+ "type": "module",
14
+ "bin": {
15
+ "lowkey-doc": "bin/lowkey-doc.mjs"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "src",
20
+ "skills",
21
+ "commands",
22
+ "licenses",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=22"
28
+ },
29
+ "scripts": {
30
+ "start": "node bin/lowkey-doc.mjs",
31
+ "build": "node bin/lowkey-doc.mjs --no-open",
32
+ "check": "node test/smoke.mjs"
33
+ },
34
+ "keywords": [
35
+ "documentation",
36
+ "markdown",
37
+ "mermaid",
38
+ "elk",
39
+ "flowchart",
40
+ "er-diagram",
41
+ "static-site",
42
+ "offline"
43
+ ],
44
+ "license": "MIT",
45
+ "dependencies": {}
46
+ }
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: audit-docs
3
+ description: Audit project documentation and return a structured audit report. Use when asked to review, audit, or sanity-check docs — "audit the docs", "review DOCS/3", "is this document clear enough". Read-only; it never edits the documents.
4
+ ---
5
+
6
+ # Auditing documentation
7
+
8
+ You are a documentation auditor. Audit the document(s) you were pointed at and return a
9
+ structured report. **Never edit the documents** — this produces a report only.
10
+
11
+ ## What to audit
12
+
13
+ Whatever the invocation named. If nothing was named, ask which document(s) to audit, or take the
14
+ obvious target: the folder `docs.config.json` sets as `"docs"`, else `docs/`, else `README.md`.
15
+ If it is a path, read it with your file tools first. If it is pasted text, audit that text.
16
+
17
+ ## Context
18
+
19
+ This documentation describes **what** the project should do, not necessarily **how** it will be
20
+ implemented. Some sections may incidentally mention implementation details — acceptable as long as
21
+ the intent of the document stays outcome-focused.
22
+
23
+ ## Audit criteria
24
+
25
+ ### 1. Clarity
26
+ - Is each section unambiguous? Could two people read it and reach different conclusions about what
27
+ is expected?
28
+ - Flag vague language ("somehow", "might", "as needed", "etc.") that leaves outcomes undefined.
29
+
30
+ ### 2. Completeness
31
+ - Gaps: missing sections, undefined terms, referenced concepts that are never explained.
32
+ - Are edge cases and failure states addressed?
33
+
34
+ ### 3. Consistency
35
+ - Does the document contradict itself?
36
+ - Are terms used consistently, or does one concept appear under several names?
37
+
38
+ ### 4. Scope integrity
39
+ - The document should describe *what*, not *how*.
40
+ - Flag sections that dive into implementation specifics in a way that would constrain the work
41
+ unnecessarily — unless the constraint is intentional and justified.
42
+
43
+ ### 5. Actionability
44
+ - Can a reader tell exactly what success looks like?
45
+ - Are acceptance criteria or expected outcomes clearly stated or implied?
46
+
47
+ ### 6. Structure
48
+ - Is it logically organised? Does the section order make sense?
49
+ - Anything that should be merged, split, or reordered?
50
+
51
+ ### 7. Cross-links and diagrams
52
+ - Does every "see below" / "see the other doc" resolve to an actual link?
53
+ - Does each flow that needs a diagram have one, and does the diagram agree with the prose beside it?
54
+ - Are edge labels short enough to read — a label of a dozen words or more is prose that escaped
55
+ into the diagram.
56
+
57
+ ### 8. Duplication across documents
58
+ - Does this document restate something another document owns? Each topic should have exactly one
59
+ owning file; a restatement is a future contradiction.
60
+
61
+ ## Output
62
+
63
+ **Summary** — 2–4 sentences on overall quality.
64
+
65
+ **Issues** — for each: location (section name or quote), category (Clarity / Completeness /
66
+ Consistency / Scope integrity / Actionability / Structure / Links / Duplication), severity
67
+ (Critical / Major / Minor), the problem, and a suggested fix.
68
+
69
+ **Strengths** — what the documentation does well.
70
+
71
+ **Recommended next steps** — prioritised.
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: write-doc
3
+ description: Write a new high-level product document for this project's documentation folder. Use when asked to document a feature, flow, or area for a non-technical reader — "write a doc about X", "document the checkout flow", "add a docs page for onboarding".
4
+ ---
5
+
6
+ # Writing a document
7
+
8
+ These are **high-level product documents**: plain-language guides to what a person
9
+ experiences, with diagrams anyone can follow. They are not architecture or implementation docs.
10
+
11
+ Read an existing document in the same folder before writing — match its voice and shape. The
12
+ folder is whatever `docs.config.json` sets as `"docs"` (`docs/` if there is no config).
13
+
14
+ ## Rules
15
+
16
+ 1. **Describe the experience, not the code.** What the user sees, taps, and gets back. No file
17
+ names, function names, table names, or libraries in the body. If a technical decision must be
18
+ recorded, put it in an appendix at the very end, clearly marked as direction, not detail.
19
+ 2. **Plain language.** Short sentences. Define any term the reader might not know; add a
20
+ **Quick glossary** at the end if the doc uses more than a couple.
21
+ 3. **Diagrams.** Include a Mermaid diagram for every flow, state machine, or set of relationships
22
+ a diagram would make clearer — not just one per doc. See [Diagrams](#diagrams) below.
23
+ 4. **Tables** for "what exists where" / availability comparisons.
24
+ 5. **Link every mention.** Any reference to another section, another document, or an open point is
25
+ a link — never a bare "see below" or "see the other doc". Same-doc:
26
+ `[the pipeline](#the-shape-of-the-flow)`. Other doc: `[CATALOG](./2.%20CATALOG.md)`. Open point:
27
+ `[open point](#op-1)`.
28
+ 6. **Open points, only if there are any.** A `## Points to evaluate (open considerations)` section
29
+ at the very bottom, before any appendix. Numbered list, each item anchored with
30
+ `<a id="op-N"></a>` and starting with a bold one-line title, then what's undecided and the
31
+ arguments each way. State the baseline behaviour until it's settled. These are questions, not
32
+ decisions — anything settled belongs in the body.
33
+
34
+ ## Diagrams
35
+
36
+ Prefer `flowchart TD`; use whatever Mermaid type fits (sequence, state, ER). Label nodes with what
37
+ the *user* does or sees, quote every label, and use `<br/>` for line breaks. A reader with no
38
+ technical background must be able to follow it.
39
+
40
+ The dashboard re-lays flowcharts and ER diagrams out with ELK, which routes edges at right angles
41
+ and reserves space for every edge label, so a dense chart stays readable without hand-tuning. Two
42
+ things still help it:
43
+
44
+ - **Name the transition, not the mechanism.** `-->|"tap Save"|` reads; `-->|"POST /parcels"|` does not.
45
+ - **Keep an edge label short.** One short phrase, or two lines split with `<br/>`. A label longer
46
+ than a dozen words is prose that escaped into the diagram — move it to the paragraph above.
47
+
48
+ Do not hand-place nodes, add `linkStyle`, or otherwise fight the layout: the dashboard lays the
49
+ chart out itself, and those directives only affect the mermaid fallback.
50
+
51
+ For an `erDiagram`, the entity is drawn as a table with the keys pinned to its right edge, so:
52
+
53
+ - **Give the attributes** in a `{ ... }` block whenever the diagram is about a schema. An entity
54
+ with no block is drawn as a plain named box — right for a diagram about how things relate, wrong
55
+ for one about what is stored.
56
+ - **Mark the keys** as mermaid spells them — `PK`, `FK`, `UK`, comma-separated for more than one
57
+ (`uuid parcel_id PK,FK`) — and quote a comment: `string note "free text"`.
58
+ - **Name the role in the reader's words.** `VOYAGE ||--o{ STOWAGE : carries` reads;
59
+ `: fk_stowage_voyage` does not.
60
+
61
+ ## Shape
62
+
63
+ ```markdown
64
+ # TITLE IN CAPS
65
+
66
+ > A plain-language guide to <what this covers>. It describes *what the user experiences*
67
+ > at each step, not how the code works. Each flow includes a diagram you can follow
68
+ > without any technical background.
69
+
70
+ ---
71
+
72
+ ## <Orienting section — the mental model, the two or three ideas to keep separate>
73
+
74
+ ## <What exists where — a comparison table, if relevant>
75
+
76
+ ## 1. <First flow>
77
+ Prose, then a ```mermaid block.
78
+
79
+ ## 2. <Next flow>
80
+ ...
81
+
82
+ ## Quick glossary (if needed)
83
+
84
+ ## Points to evaluate (open considerations) (if any)
85
+
86
+ ## Appendix — implementation direction (only if a decision must be recorded)
87
+ ```
88
+
89
+ ## Filing
90
+
91
+ - Name: `<n>. TITLE-IN-CAPS.md` in the documents folder, `<n>` being the next free number.
92
+ The number drives the order in the sidebar; a document without one sorts after the numbered ones.
93
+ - Sub-folders become their own group in the sidebar, named after the folder — put audits, reviews,
94
+ and other second-class material in one rather than in the main run.
95
+ - Sections separated by `---`. Wrap prose at ~100 columns.
96
+ - After writing, check whether the document settled anything about the product; if so update the
97
+ project's context document (usually `0. CONTEXT.md` or `0. ABOUT.md`).
package/src/build.mjs ADDED
@@ -0,0 +1,131 @@
1
+ // build.mjs - read the documents, write the page.
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { execFile } from 'node:child_process';
6
+ import { listDocs, assignIds, buildDoc } from './collect.mjs';
7
+ import { vendorMermaid, vendorElk, vendorFont, writeNotices } from './vendor.mjs';
8
+ import { renderPage } from './page.mjs';
9
+ import { DEFAULTS, CLI, VERSION } from './config.mjs';
10
+
11
+ export async function build(cfg, { open = true, refresh = false, quiet = false } = {}) {
12
+ const { docs } = collectDocs(cfg);
13
+
14
+ const assets = await vendorAssets(cfg, refresh);
15
+ const html = pageFor(cfg, docs, assets);
16
+
17
+ const outFile = path.join(cfg.outDir, 'index.html');
18
+ fs.writeFileSync(outFile, html);
19
+
20
+ const report = {
21
+ outFile,
22
+ docs,
23
+ bytes: fs.statSync(outFile).size,
24
+ notes: { mermaid: assets.mermaid.note, elk: assets.elk.note, font: assets.font.note },
25
+ };
26
+
27
+ if (!quiet) print(cfg, report);
28
+ if (open) openInBrowser(outFile);
29
+ return report;
30
+ }
31
+
32
+ // The documents half of a build, kept apart from the page so the watcher can
33
+ // redo it after a save without redoing everything. Hand the previous memo back
34
+ // in and only files whose bytes changed are rendered again - unless the set of
35
+ // files itself changed, which can move where any link points, so then every
36
+ // document is rendered again.
37
+ export function collectDocs(cfg, memo = null) {
38
+ // A folder named on the command line was checked before we got here, so a
39
+ // missing one is either the configured folder or the default docs/.
40
+ const shown = (path.relative(process.cwd(), cfg.docsDir) || '.') + path.sep;
41
+ if (!fs.existsSync(cfg.docsDir)) {
42
+ throw new Error(cfg.configFile
43
+ ? path.basename(cfg.configFile) + ' points at ' + shown + ', which does not exist.' +
44
+ '\nCreate it, or run: ' + CLI + ' init'
45
+ : 'No ' + shown + ' folder here.' +
46
+ '\nSet one up with: ' + CLI + ' init' +
47
+ '\nOr render another folder: ' + CLI + ' <folder>');
48
+ }
49
+
50
+ const entries = listDocs(cfg);
51
+ if (!entries.length) {
52
+ throw new Error('Nothing to render in ' + shown + ': add a .md file to it.' +
53
+ '\nIt reads files ending in ' + [...cfg.extensions, ...Object.keys(cfg.verbatim)].join(', '));
54
+ }
55
+
56
+ // ids first: cross-document links need to resolve to documents not yet built
57
+ const byFile = assignIds(entries);
58
+ const layout = JSON.stringify(entries.map((e) => [e.rel, e.group]));
59
+ const reuse = memo && memo.layout === layout;
60
+ const files = new Map();
61
+ const changed = [];
62
+
63
+ const docs = entries.map((e) => {
64
+ const raw = fs.readFileSync(e.abs, 'utf8');
65
+ const was = reuse && memo.files.get(e.rel);
66
+ if (was && was.raw === raw) { files.set(e.rel, was); return was.doc; }
67
+ let doc;
68
+ try {
69
+ doc = buildDoc(e, byFile, cfg, raw);
70
+ } catch (err) {
71
+ throw new Error(e.rel + ': ' + (err && err.message || err));
72
+ }
73
+ files.set(e.rel, { raw, doc });
74
+ changed.push(doc.id);
75
+ return doc;
76
+ });
77
+
78
+ return { docs, changed, memo: { layout, files } };
79
+ }
80
+
81
+ // ELK travels even when the default engine is mermaid: the reader can switch
82
+ // any single diagram over from the page, and that has to work offline too.
83
+ export async function vendorAssets(cfg, refresh = false) {
84
+ fs.mkdirSync(cfg.outDir, { recursive: true });
85
+ // The default output folder keeps itself out of git, so no project has to
86
+ // remember to. A custom `out` may be meant to be committed (GitHub Pages).
87
+ const ignore = path.join(cfg.outDir, '.gitignore');
88
+ if (cfg.outDir === path.resolve(cfg.root, DEFAULTS.out) && !fs.existsSync(ignore)) {
89
+ fs.writeFileSync(ignore, '# written by lowkey-doc: everything here is build output\n*\n');
90
+ }
91
+ const assets = {
92
+ mermaid: await vendorMermaid(cfg.outDir, refresh),
93
+ elk: await vendorElk(cfg.outDir, refresh),
94
+ font: await vendorFont(cfg.outDir, refresh),
95
+ };
96
+ writeNotices(cfg.outDir);
97
+ return assets;
98
+ }
99
+
100
+ export function pageFor(cfg, docs, assets) {
101
+ return renderPage({
102
+ cfg, docs,
103
+ fontCss: assets.font.css,
104
+ mermaidTag: assets.mermaid.tag,
105
+ elkTag: assets.elk.tag,
106
+ });
107
+ }
108
+
109
+ function print(cfg, r) {
110
+ const pad = (s, n) => String(s).padEnd(n).slice(0, n);
111
+ console.log(cfg.title + ' (lowkey-doc ' + VERSION + ')');
112
+ for (const d of r.docs) {
113
+ console.log(' ' + String(d.badge).padStart(3) + ' ' + pad(d.title, 38) +
114
+ String(d.sections.length).padStart(3) + ' sections' +
115
+ (d.diagrams ? ' ' + d.diagrams + (d.diagrams > 1 ? ' diagrams' : ' diagram') : ''));
116
+ }
117
+ console.log(' mermaid: ' + r.notes.mermaid);
118
+ console.log(' elk: ' + r.notes.elk);
119
+ console.log(' font: ' + r.notes.font);
120
+ console.log(' -> ' + r.outFile + ' (' + (r.bytes / 1024).toFixed(0) + ' KB)');
121
+ }
122
+
123
+ // `start` is a cmd built-in rather than a program, so on Windows it has to go
124
+ // through cmd; the empty first argument stops a quoted path being taken for
125
+ // the window title.
126
+ export function openInBrowser(target) {
127
+ const [cmd, args] = process.platform === 'darwin' ? ['open', [target]]
128
+ : process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', target]]
129
+ : ['xdg-open', [target]];
130
+ execFile(cmd, args, (err) => { if (err) console.error(' (open failed: ' + err.message + ')'); });
131
+ }
@@ -0,0 +1,102 @@
1
+ // api.mjs - the API call blocks, inlined into the page.
2
+ //
3
+ // A call is a box whose header - verb and url - is always shown. A click on
4
+ // the header opens the body, which says what each part of the url is.
5
+ //
6
+ // Folded, the box still answers questions: holding Ctrl - Option on a Mac,
7
+ // the same key the ER diagrams use - with the pointer on a described part of
8
+ // the url shows that part's description, without opening anything. Open, the
9
+ // descriptions are already on screen, so the key does nothing.
10
+
11
+ export const API_JS = String.raw`
12
+ (function(){
13
+ var IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || '');
14
+ var TIP_KEY = IS_MAC ? 'Alt' : 'Control';
15
+ function keyHeld(e){ return IS_MAC ? e.altKey : e.ctrlKey; }
16
+
17
+ function setOpen(box, open){
18
+ box.classList.toggle('is-open', open);
19
+ var head = box.querySelector('.api-head');
20
+ if (head && head.hasAttribute('aria-expanded')) head.setAttribute('aria-expanded', open ? 'true' : 'false');
21
+ hideTip();
22
+ }
23
+
24
+ document.addEventListener('click', function(e){
25
+ var head = e.target.closest && e.target.closest('.api-head[role=button]');
26
+ if (!head) return;
27
+ // a selection being made for a comment is not a click on the header
28
+ var sel = window.getSelection && window.getSelection();
29
+ if (sel && !sel.isCollapsed && head.contains(sel.anchorNode)) return;
30
+ var box = head.closest('.api');
31
+ setOpen(box, !box.classList.contains('is-open'));
32
+ });
33
+ document.addEventListener('keydown', function(e){
34
+ if (e.key !== 'Enter' && e.key !== ' ') return;
35
+ var head = e.target.closest && e.target.closest('.api-head[role=button]');
36
+ if (!head) return;
37
+ e.preventDefault();
38
+ var box = head.closest('.api');
39
+ setOpen(box, !box.classList.contains('is-open'));
40
+ });
41
+
42
+ /* ------------------------------------------------- a part's row */
43
+
44
+ function rowOf(part){
45
+ var box = part.closest('.api');
46
+ return box && box.querySelector('.api-row[data-part="' + part.getAttribute('data-part') + '"]');
47
+ }
48
+
49
+ /* ------------------------------------------ what a part is, on request */
50
+
51
+ var tip = null, tipFor = null, lastPointer = null;
52
+
53
+ function target(el){
54
+ var part = el && el.closest && el.closest('.api-part');
55
+ return part && !part.closest('.api').classList.contains('is-open') ? part : null;
56
+ }
57
+
58
+ function showTip(part){
59
+ if (tipFor === part) return;
60
+ hideTip();
61
+ var row = rowOf(part);
62
+ if (!row) return;
63
+ tipFor = part;
64
+ part.classList.add('is-asked');
65
+ tip = document.createElement('div');
66
+ tip.className = 'dg-tip api-tip';
67
+ tip.innerHTML = '<div class="api-tip-key">' + row.querySelector('dt').innerHTML + '</div>' +
68
+ row.querySelector('dd').innerHTML;
69
+ document.body.appendChild(tip);
70
+ var box = part.getBoundingClientRect();
71
+ var vw = document.documentElement.clientWidth, w = tip.offsetWidth, h = tip.offsetHeight;
72
+ var top = box.top - h - 8;
73
+ if (top < 8) top = box.bottom + 8;
74
+ tip.style.left = Math.round(Math.min(Math.max(8, box.left - 12), vw - w - 8)) + 'px';
75
+ tip.style.top = Math.round(top) + 'px';
76
+ }
77
+ function hideTip(){
78
+ if (tipFor) tipFor.classList.remove('is-asked');
79
+ tipFor = null;
80
+ if (tip){ tip.remove(); tip = null; }
81
+ }
82
+
83
+ document.addEventListener('mousemove', function(e){
84
+ lastPointer = { x: e.clientX, y: e.clientY };
85
+ var t = keyHeld(e) ? target(e.target) : null;
86
+ if (t) showTip(t); else if (tip) hideTip();
87
+ }, { passive: true });
88
+
89
+ document.addEventListener('keydown', function(e){
90
+ if (e.key === TIP_KEY) document.documentElement.classList.add('api-asking');
91
+ if (e.key !== TIP_KEY || e.repeat || !lastPointer) return;
92
+ var t = target(document.elementFromPoint(lastPointer.x, lastPointer.y));
93
+ if (t) showTip(t);
94
+ });
95
+ function letGo(){ document.documentElement.classList.remove('api-asking'); hideTip(); }
96
+ document.addEventListener('keyup', function(e){ if (e.key === TIP_KEY) letGo(); });
97
+ addEventListener('blur', letGo);
98
+ addEventListener('scroll', hideTip, { passive: true });
99
+ document.addEventListener('wheel', hideTip, { passive: true });
100
+ document.addEventListener('mousedown', hideTip, true);
101
+ })();
102
+ `;