changeledger 0.3.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 (44) hide show
  1. package/AGENTS.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +160 -0
  4. package/bin/changeledger.mjs +375 -0
  5. package/package.json +72 -0
  6. package/src/atomic-write.mjs +134 -0
  7. package/src/change.mjs +102 -0
  8. package/src/check.mjs +536 -0
  9. package/src/commands/agent.mjs +256 -0
  10. package/src/commands/check.mjs +48 -0
  11. package/src/commands/graduate.mjs +105 -0
  12. package/src/commands/init.mjs +43 -0
  13. package/src/commands/new.mjs +164 -0
  14. package/src/commands/register.mjs +28 -0
  15. package/src/commands/release.mjs +138 -0
  16. package/src/commands/view.mjs +52 -0
  17. package/src/config.mjs +76 -0
  18. package/src/contract.mjs +100 -0
  19. package/src/git.mjs +73 -0
  20. package/src/lifecycle.mjs +57 -0
  21. package/src/metrics.mjs +143 -0
  22. package/src/paths.mjs +12 -0
  23. package/src/registry.mjs +55 -0
  24. package/src/release.mjs +71 -0
  25. package/src/repo.mjs +122 -0
  26. package/src/slug.mjs +12 -0
  27. package/src/spec.mjs +12 -0
  28. package/src/viewer/domain.mjs +133 -0
  29. package/src/viewer/public/api.js +25 -0
  30. package/src/viewer/public/app-state.js +87 -0
  31. package/src/viewer/public/app.js +717 -0
  32. package/src/viewer/public/index.html +64 -0
  33. package/src/viewer/public/security.js +65 -0
  34. package/src/viewer/public/state.js +31 -0
  35. package/src/viewer/public/styles.css +1062 -0
  36. package/src/viewer/public/templates.js +9 -0
  37. package/src/viewer/public/view-parts.js +162 -0
  38. package/src/viewer/public/view-renderers.js +191 -0
  39. package/src/viewer/server/router.mjs +200 -0
  40. package/src/viewer/server/security.mjs +27 -0
  41. package/src/writer.mjs +151 -0
  42. package/src/yaml.mjs +75 -0
  43. package/templates/AGENTS.md +540 -0
  44. package/templates/config.yml +55 -0
@@ -0,0 +1,64 @@
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>ChangeLedger</title>
7
+ <link rel="stylesheet" href="/styles.css" />
8
+ <script>
9
+ window.__CHANGELEDGER_TOKEN__ = '__CHANGELEDGER_TOKEN_VALUE__';
10
+ </script>
11
+ <script src="/vendor/marked.min.js"></script>
12
+ <script src="/vendor/purify.min.js"></script>
13
+ <script src="/vendor/mermaid.min.js"></script>
14
+ <script type="importmap">
15
+ {
16
+ "imports": {
17
+ "lit-html": "/vendor/lit-html/lit-html.js",
18
+ "lit-html/": "/vendor/lit-html/"
19
+ }
20
+ }
21
+ </script>
22
+ </head>
23
+ <body>
24
+ <header class="topbar">
25
+ <div class="brand">ChangeLedger <span id="lang" class="badge"></span></div>
26
+ <select id="project" class="filter" title="Project"></select>
27
+ <input id="search" class="search" type="search" placeholder="Search anything…" />
28
+ <button type="button" id="toggle-global" class="chip" title="Search across all projects">Global</button>
29
+ <select id="type-filter" class="filter"></select>
30
+ <select id="owner-filter" class="filter"></select>
31
+ <div id="status-filter"></div>
32
+ <div class="spacer"></div>
33
+ <div class="tabs">
34
+ <button type="button" id="view-board" class="tab active">Board</button>
35
+ <button type="button" id="view-table" class="tab">Table</button>
36
+ <button type="button" id="view-graph" class="tab">Graph</button>
37
+ <button type="button" id="view-specs" class="tab">Specs</button>
38
+ <button type="button" id="view-metrics" class="tab">Metrics</button>
39
+ </div>
40
+ </header>
41
+
42
+ <main id="board" class="board"></main>
43
+ <section id="table" class="table-view hidden"></section>
44
+ <section id="graph" class="graph hidden"></section>
45
+ <section id="specs" class="specs-view hidden"></section>
46
+ <section id="global" class="global-view hidden"></section>
47
+ <section id="metrics" class="metrics-view hidden"></section>
48
+
49
+ <div id="overlay" class="overlay hidden">
50
+ <div id="detail" class="detail"></div>
51
+ </div>
52
+
53
+ <div id="diagram-overlay" class="diagram-overlay hidden" role="dialog" aria-modal="true" aria-label="Expanded diagram">
54
+ <div class="diagram-shell">
55
+ <button type="button" id="close-diagram" class="icon-button diagram-close" aria-label="Close diagram" title="Close diagram">
56
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.75 3.75 12.25 12.25M12.25 3.75 3.75 12.25"></path></svg>
57
+ </button>
58
+ <div id="diagram-canvas" class="diagram-canvas"></div>
59
+ </div>
60
+ </div>
61
+
62
+ <script type="module" src="/app.js"></script>
63
+ </body>
64
+ </html>
@@ -0,0 +1,65 @@
1
+ export function initMermaid() {
2
+ if (typeof mermaid === 'undefined') return;
3
+ // securityLevel 'strict' is explicit: change/spec bodies are untrusted input,
4
+ // so diagram text must not run scripts or click handlers.
5
+ mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'strict' });
6
+ }
7
+
8
+ // Renders untrusted Markdown to sanitized HTML. Marked does not strip active
9
+ // HTML (event handlers, javascript: URLs, <script>), so every body that reaches
10
+ // innerHTML passes through DOMPurify first. Repo documents are untrusted even
11
+ // locally — opening the viewer must not let a document run code in its origin.
12
+ export function safeHtml(markdown) {
13
+ if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') {
14
+ return '<p class="empty">Markdown rendering is unavailable because a required viewer dependency failed to load.</p>';
15
+ }
16
+ const html = marked.parse(markdown || '');
17
+ return DOMPurify.sanitize(html, { FORBID_TAGS: ['style'] });
18
+ }
19
+
20
+ // Replace ```mermaid code blocks (rendered by marked as <pre><code>) with live
21
+ // diagrams. Uses textContent so escaped chars (-->, etc.) are decoded first.
22
+ export function renderMermaid(root) {
23
+ if (typeof mermaid === 'undefined') return;
24
+ const blocks = root.querySelectorAll('pre > code.language-mermaid');
25
+ blocks.forEach((code) => {
26
+ const div = document.createElement('div');
27
+ div.className = 'mermaid';
28
+ div.textContent = code.textContent;
29
+ code.parentElement.replaceWith(div);
30
+ });
31
+ const nodes = root.querySelectorAll('.mermaid');
32
+ if (nodes.length) return mermaid.run({ nodes });
33
+ }
34
+
35
+ export function makeMermaidExpandable(root, onOpen) {
36
+ root.querySelectorAll('.mermaid').forEach((node) => {
37
+ node.classList.add('mermaid-expandable');
38
+ node.tabIndex = 0;
39
+ node.setAttribute('role', 'button');
40
+ node.setAttribute('aria-label', 'Expand diagram');
41
+ const open = () => node.querySelector('svg') && onOpen(node);
42
+ node.onclick = open;
43
+ node.onkeydown = (event) => {
44
+ if (event.key === 'Enter' || event.key === ' ') {
45
+ event.preventDefault();
46
+ open();
47
+ }
48
+ };
49
+ });
50
+ }
51
+
52
+ // Escapes a value for HTML text and double-quoted attribute contexts. Every
53
+ // untrusted document/config field (id, type, status, stage heading, timestamps,
54
+ // dependency ids…) passes through this before reaching innerHTML — sanitizing
55
+ // the Markdown body is not enough on its own.
56
+ export const esc = (s) =>
57
+ String(s ?? '').replace(
58
+ /['&<>"]/g,
59
+ (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c],
60
+ );
61
+
62
+ // A value interpolated into a CSS custom-property name (`var(--TYPE)`) must be a
63
+ // bare identifier; anything else is dropped to a neutral, defined fallback so a
64
+ // crafted `type` cannot break out of the declaration or inject extra rules.
65
+ export const cssIdent = (s) => (/^[A-Za-z][\w-]*$/.test(String(s ?? '')) ? String(s) : 'muted');
@@ -0,0 +1,31 @@
1
+ // Tombstone visibility: archived and discarded changes are hidden by default and
2
+ // each revealed by its own toggle. Shared by every view (board/table via
3
+ // isVisible, and the graph) so no view can diverge on the rule.
4
+ export const passesTombstones = (c, f) =>
5
+ (f.showArchived || !c.archived) && (f.showDiscarded || c.status !== 'discarded');
6
+
7
+ // Statuses that get a board column. `discarded` stays off-board by default and
8
+ // becomes the final lane only while its explicit visibility toggle is active.
9
+ export const boardStatuses = (statuses, showDiscarded = false) =>
10
+ statuses.filter((s) => s !== 'discarded' || showDiscarded);
11
+
12
+ // Full-text haystack: id, title, type, stage headings/bodies and task text.
13
+ function haystack(c) {
14
+ const stages = c.stages.map((s) => `${s.heading} ${s.body}`).join(' ');
15
+ const tasks = c.tasks
16
+ .map((t) => `${t.text} ${(t.criteria || []).join(' ')} ${t.reason || ''}`)
17
+ .join(' ');
18
+ return `${c.id} ${c.title} ${c.type} ${c.status} ${c.owner || ''} ${stages} ${tasks}`.toLowerCase();
19
+ }
20
+
21
+ // Whether a change is shown under the current filters. Exported as a pure
22
+ // predicate so the rule is testable.
23
+ export function isVisible(c, f) {
24
+ if (!passesTombstones(c, f)) return false;
25
+ if (f.type !== 'all' && c.type !== f.type) return false;
26
+ if (f.owner !== 'all' && c.owner !== f.owner) return false;
27
+ if (f.statuses.size && !f.statuses.has(c.status)) return false;
28
+ const q = f.text.toLowerCase();
29
+ if (!q) return true;
30
+ return haystack(c).includes(q);
31
+ }