@ferax564/noma-cli 0.11.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/bin/noma.mjs +8 -0
  4. package/dist/ast.d.ts +111 -0
  5. package/dist/ast.js +23 -0
  6. package/dist/ast.js.map +1 -0
  7. package/dist/book.d.ts +56 -0
  8. package/dist/book.js +120 -0
  9. package/dist/book.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +573 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/diff.d.ts +29 -0
  14. package/dist/diff.js +77 -0
  15. package/dist/diff.js.map +1 -0
  16. package/dist/fmt.d.ts +1 -0
  17. package/dist/fmt.js +105 -0
  18. package/dist/fmt.js.map +1 -0
  19. package/dist/ids.d.ts +15 -0
  20. package/dist/ids.js +27 -0
  21. package/dist/ids.js.map +1 -0
  22. package/dist/index.d.ts +20 -0
  23. package/dist/index.js +12 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/inline.d.ts +14 -0
  26. package/dist/inline.js +83 -0
  27. package/dist/inline.js.map +1 -0
  28. package/dist/loader.d.ts +12 -0
  29. package/dist/loader.js +59 -0
  30. package/dist/loader.js.map +1 -0
  31. package/dist/parser.d.ts +7 -0
  32. package/dist/parser.js +434 -0
  33. package/dist/parser.js.map +1 -0
  34. package/dist/patch.d.ts +61 -0
  35. package/dist/patch.js +530 -0
  36. package/dist/patch.js.map +1 -0
  37. package/dist/renderer-html.d.ts +44 -0
  38. package/dist/renderer-html.js +929 -0
  39. package/dist/renderer-html.js.map +1 -0
  40. package/dist/renderer-json.d.ts +5 -0
  41. package/dist/renderer-json.js +4 -0
  42. package/dist/renderer-json.js.map +1 -0
  43. package/dist/renderer-llm.d.ts +29 -0
  44. package/dist/renderer-llm.js +275 -0
  45. package/dist/renderer-llm.js.map +1 -0
  46. package/dist/renderer-noma.d.ts +10 -0
  47. package/dist/renderer-noma.js +179 -0
  48. package/dist/renderer-noma.js.map +1 -0
  49. package/dist/renderer-site.d.ts +11 -0
  50. package/dist/renderer-site.js +175 -0
  51. package/dist/renderer-site.js.map +1 -0
  52. package/dist/validator.d.ts +24 -0
  53. package/dist/validator.js +699 -0
  54. package/dist/validator.js.map +1 -0
  55. package/dist/verify.d.ts +10 -0
  56. package/dist/verify.js +141 -0
  57. package/dist/verify.js.map +1 -0
  58. package/package.json +83 -0
  59. package/schemas/ast.schema.json +187 -0
  60. package/schemas/capability.schema.json +70 -0
  61. package/schemas/patch-op.schema.json +92 -0
  62. package/schemas/patch-transaction.schema.json +28 -0
  63. package/schemas/transcript.schema.json +95 -0
  64. package/src/ast.ts +152 -0
  65. package/src/book.ts +162 -0
  66. package/src/cli.ts +595 -0
  67. package/src/diff.ts +108 -0
  68. package/src/fmt.ts +126 -0
  69. package/src/ids.ts +42 -0
  70. package/src/index.ts +20 -0
  71. package/src/inline.ts +92 -0
  72. package/src/loader.ts +55 -0
  73. package/src/parser.ts +501 -0
  74. package/src/patch.ts +646 -0
  75. package/src/renderer-html.ts +1047 -0
  76. package/src/renderer-json.ts +9 -0
  77. package/src/renderer-llm.ts +320 -0
  78. package/src/renderer-noma.ts +220 -0
  79. package/src/renderer-site.ts +245 -0
  80. package/src/validator.ts +733 -0
  81. package/src/verify.ts +157 -0
  82. package/themes/dark.css +382 -0
  83. package/themes/default.css +537 -0
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ferax564.github.io/noma/schemas/patch-transaction.schema.json",
4
+ "title": "Noma Patch Transaction",
5
+ "description": "A transaction-shaped patch payload accepted by `noma patch --ops`.",
6
+ "oneOf": [
7
+ { "$ref": "patch-op.schema.json" },
8
+ {
9
+ "type": "array",
10
+ "minItems": 1,
11
+ "items": { "$ref": "patch-op.schema.json" }
12
+ },
13
+ {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": ["ops"],
17
+ "properties": {
18
+ "ops": {
19
+ "type": "array",
20
+ "minItems": 1,
21
+ "items": { "$ref": "patch-op.schema.json" }
22
+ },
23
+ "prevalidate": { "type": "boolean", "default": false },
24
+ "postvalidate": { "type": "boolean", "default": false }
25
+ }
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,95 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ferax564.github.io/noma/schemas/transcript.schema.json",
4
+ "title": "Noma Patch Transcript Record",
5
+ "description": "One JSONL entry emitted by the MCP patch writer.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "protocol_version",
10
+ "tool_version",
11
+ "op_id",
12
+ "ts",
13
+ "actor",
14
+ "doc_uri",
15
+ "pre_sha256",
16
+ "post_sha256",
17
+ "pre_sha",
18
+ "post_sha",
19
+ "op",
20
+ "patch_result",
21
+ "pre_validation",
22
+ "post_validation"
23
+ ],
24
+ "properties": {
25
+ "protocol_version": { "const": "1.0" },
26
+ "tool_version": { "type": "string" },
27
+ "op_id": { "type": "string", "format": "uuid" },
28
+ "ts": { "type": "string", "format": "date-time" },
29
+ "actor": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": ["kind", "name"],
33
+ "properties": {
34
+ "kind": { "enum": ["human", "agent", "tool"] },
35
+ "name": { "type": "string" },
36
+ "model": { "type": "string" },
37
+ "version": { "type": "string" }
38
+ }
39
+ },
40
+ "doc_uri": { "type": "string" },
41
+ "pre_sha256": { "$ref": "#/$defs/sha256" },
42
+ "post_sha256": { "$ref": "#/$defs/sha256" },
43
+ "pre_sha": { "type": "string", "pattern": "^[a-f0-9]{8}$" },
44
+ "post_sha": { "type": "string", "pattern": "^[a-f0-9]{8}$" },
45
+ "op": { "$ref": "patch-op.schema.json" },
46
+ "patch_result": { "enum": ["applied", "rejected", "noop"] },
47
+ "pre_validation": { "$ref": "#/$defs/validationSummary" },
48
+ "post_validation": { "$ref": "#/$defs/validationSummary" },
49
+ "reason": { "type": "string" },
50
+ "parent_op_id": { "type": "string", "format": "uuid" },
51
+ "base_sha256": { "$ref": "#/$defs/sha256" },
52
+ "diagnostics": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "required": ["phase", "severity", "code", "message"],
57
+ "properties": {
58
+ "phase": { "enum": ["pre", "post"] },
59
+ "severity": { "enum": ["error", "warning", "info"] },
60
+ "code": { "type": "string" },
61
+ "message": { "type": "string" },
62
+ "nodeId": { "type": "string" },
63
+ "pos": {
64
+ "type": "object",
65
+ "required": ["line", "column"],
66
+ "properties": {
67
+ "line": { "type": "integer", "minimum": 1 },
68
+ "column": { "type": "integer", "minimum": 1 }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ },
74
+ "elapsed_ms": { "type": "number", "minimum": 0 },
75
+ "prev_entry_sha256": { "$ref": "#/$defs/sha256" },
76
+ "signature": {
77
+ "oneOf": [
78
+ { "type": "null" },
79
+ {
80
+ "type": "object",
81
+ "required": ["algorithm", "key_id", "value"],
82
+ "properties": {
83
+ "algorithm": { "type": "string" },
84
+ "key_id": { "type": "string" },
85
+ "value": { "type": "string" }
86
+ }
87
+ }
88
+ ]
89
+ }
90
+ },
91
+ "$defs": {
92
+ "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
93
+ "validationSummary": { "enum": ["ok", "warn", "error"] }
94
+ }
95
+ }
package/src/ast.ts ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Noma AST — single source of truth.
3
+ *
4
+ * Renderers and the validator must import types from this file. Adding a new
5
+ * node variant requires extending the `Node` union and updating every
6
+ * renderer's switch (the compiler will tell you where).
7
+ */
8
+
9
+ export type AttrValue = string | number | boolean;
10
+ export type Attrs = Record<string, AttrValue>;
11
+
12
+ export interface Position {
13
+ line: number;
14
+ column: number;
15
+ }
16
+
17
+ interface NodeBase {
18
+ /** Stable, user-facing identifier when set. Auto-generated for headings. */
19
+ id?: string;
20
+ /**
21
+ * Additional IDs that resolve to this node. Populated by:
22
+ * • frontmatter `aliases:` list (attaches to chapter root section)
23
+ * • chapter filename slug in book mode
24
+ * • path-scoped heading ID in book mode (registers the bare slug as alias)
25
+ */
26
+ aliases?: string[];
27
+ /** Source position of the node's first character. */
28
+ pos?: Position;
29
+ /**
30
+ * 1-based last source line covered by this node (inclusive). Populated by
31
+ * the parser; used by `patchSource` to splice ranges without re-rendering
32
+ * the rest of the document.
33
+ */
34
+ endLine?: number;
35
+ }
36
+
37
+ export interface DocumentNode extends NodeBase {
38
+ type: "document";
39
+ meta: Record<string, unknown>;
40
+ children: Node[];
41
+ }
42
+
43
+ export interface FrontmatterNode extends NodeBase {
44
+ type: "frontmatter";
45
+ /** Parsed YAML object (string keys → arbitrary values). */
46
+ data: Record<string, unknown>;
47
+ /** Raw frontmatter source text (between the --- fences, exclusive). */
48
+ raw: string;
49
+ }
50
+
51
+ export interface SectionNode extends NodeBase {
52
+ type: "section";
53
+ level: number;
54
+ title: string;
55
+ children: Node[];
56
+ }
57
+
58
+ export interface ParagraphNode extends NodeBase {
59
+ type: "paragraph";
60
+ content: string;
61
+ }
62
+
63
+ export interface CodeNode extends NodeBase {
64
+ type: "code";
65
+ lang?: string;
66
+ content: string;
67
+ }
68
+
69
+ export interface ListNode extends NodeBase {
70
+ type: "list";
71
+ ordered: boolean;
72
+ items: ListItemNode[];
73
+ }
74
+
75
+ export interface ListItemNode extends NodeBase {
76
+ type: "list_item";
77
+ content: string;
78
+ }
79
+
80
+ export interface QuoteNode extends NodeBase {
81
+ type: "quote";
82
+ content: string;
83
+ }
84
+
85
+ export interface ThematicBreakNode extends NodeBase {
86
+ type: "thematic_break";
87
+ }
88
+
89
+ export type TableAlign = "left" | "center" | "right" | null;
90
+
91
+ export interface TableNode extends NodeBase {
92
+ type: "table";
93
+ /** Header cells (one row). Inline markdown is preserved as plain strings. */
94
+ header: string[];
95
+ /** Per-column alignment from the separator row (`:---`, `:---:`, `---:`). */
96
+ align: TableAlign[];
97
+ /** Body rows, each with one entry per column. */
98
+ rows: string[][];
99
+ }
100
+
101
+ /**
102
+ * Generic block directive — covers every typed semantic block (claim, evidence,
103
+ * grid, card, plot, dataset, agent_task, ...). Renderers dispatch on `name`.
104
+ */
105
+ export interface DirectiveNode extends NodeBase {
106
+ type: "directive";
107
+ name: string;
108
+ attrs: Attrs;
109
+ /** Inline body text (when the block has no nested children). */
110
+ body?: string;
111
+ /** Nested directive children (grids, cards, etc.). */
112
+ children: Node[];
113
+ }
114
+
115
+ export type Node =
116
+ | DocumentNode
117
+ | SectionNode
118
+ | FrontmatterNode
119
+ | ParagraphNode
120
+ | CodeNode
121
+ | ListNode
122
+ | ListItemNode
123
+ | QuoteNode
124
+ | ThematicBreakNode
125
+ | TableNode
126
+ | DirectiveNode;
127
+
128
+ export type BlockNode = Exclude<Node, ListItemNode | FrontmatterNode>;
129
+
130
+ export interface Diagnostic {
131
+ severity: "error" | "warning" | "info";
132
+ code: string;
133
+ message: string;
134
+ pos?: Position;
135
+ nodeId?: string;
136
+ }
137
+
138
+ export const isDirective = (n: Node): n is DirectiveNode => n.type === "directive";
139
+ export const isSection = (n: Node): n is SectionNode => n.type === "section";
140
+
141
+ export function* walk(node: Node): Generator<Node> {
142
+ yield node;
143
+ if (
144
+ node.type === "document" ||
145
+ node.type === "section" ||
146
+ node.type === "directive"
147
+ ) {
148
+ for (const child of node.children) yield* walk(child);
149
+ } else if (node.type === "list") {
150
+ for (const item of node.items) yield* walk(item);
151
+ }
152
+ }
package/src/book.ts ADDED
@@ -0,0 +1,162 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import yaml from "js-yaml";
4
+ import type { DocumentNode, Node, SectionNode } from "./ast.js";
5
+ import { walk } from "./ast.js";
6
+ import { parse } from "./parser.js";
7
+ import { inlineDatasetSources } from "./loader.js";
8
+
9
+ /**
10
+ * Shape of `book.yml` manifests. Note: YAML keys are snake_case (`trusted_publishing`),
11
+ * read directly from the manifest map. The typed shape below covers fields the
12
+ * loader/renderer consume by name; unrecognised keys pass through untouched.
13
+ *
14
+ * Recognised security-posture keys (read at the render path, not surfaced as
15
+ * typed fields):
16
+ * - `trusted_publishing: true` — implies `--no-unsafe` for every render driven
17
+ * by this manifest. Disables `::html`, `::svg`, `::script` escape hatches.
18
+ * The manifest is the final word; no CLI flag re-enables them once the
19
+ * manifest forbids them.
20
+ */
21
+ export interface BookManifest {
22
+ title?: string;
23
+ author?: string;
24
+ chapters: string[];
25
+ outputs?: {
26
+ html?: { theme?: string; math?: "katex" | "none" };
27
+ llm?: Record<string, unknown>;
28
+ pdf?: Record<string, unknown>;
29
+ };
30
+ }
31
+
32
+ export interface LoadedChapter {
33
+ /** Path-safe chapter slug derived from filename (or root H1 id). */
34
+ slug: string;
35
+ /** Source path of the chapter file (absolute). */
36
+ source: string;
37
+ /** Parsed chapter document with scoped IDs applied. */
38
+ doc: DocumentNode;
39
+ }
40
+
41
+ /**
42
+ * Load a YAML book manifest and assemble a single DocumentNode by
43
+ * concatenating each chapter's parsed AST. Chapter file paths are
44
+ * resolved relative to the manifest's directory.
45
+ */
46
+ export function loadBook(manifestPath: string): DocumentNode {
47
+ const absManifest = resolve(manifestPath);
48
+ const raw = readFileSync(absManifest, "utf8");
49
+ const parsed = yaml.load(raw);
50
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
51
+ throw new Error(`book manifest must be a YAML object: ${manifestPath}`);
52
+ }
53
+ const manifest = parsed as Record<string, unknown>;
54
+ const chapters = Array.isArray(manifest.chapters)
55
+ ? (manifest.chapters as unknown[]).filter((c): c is string => typeof c === "string")
56
+ : [];
57
+ if (chapters.length === 0) {
58
+ throw new Error(`book manifest has no chapters: ${manifestPath}`);
59
+ }
60
+ const baseDir = dirname(absManifest);
61
+
62
+ const meta: Record<string, unknown> = {};
63
+ if (typeof manifest.title === "string") meta.title = manifest.title;
64
+ if (typeof manifest.author === "string") meta.author = manifest.author;
65
+ meta.book = {
66
+ chapters: chapters.length,
67
+ manifest: manifestPath,
68
+ };
69
+
70
+ const children: Node[] = [];
71
+ const loaded = loadChapters(chapters, baseDir);
72
+ for (const ch of loaded) {
73
+ for (const child of ch.doc.children) children.push(child);
74
+ }
75
+
76
+ return { type: "document", meta, children };
77
+ }
78
+
79
+ /**
80
+ * Load every chapter listed in a book manifest as a separate DocumentNode,
81
+ * applying chapter-scoped heading IDs in book mode. Used by the multi-page
82
+ * site renderer; loadBook() concatenates the same set into one doc.
83
+ */
84
+ export function loadBookChapters(manifestPath: string): {
85
+ manifest: Record<string, unknown>;
86
+ chapters: LoadedChapter[];
87
+ baseDir: string;
88
+ } {
89
+ const absManifest = resolve(manifestPath);
90
+ const raw = readFileSync(absManifest, "utf8");
91
+ const parsed = yaml.load(raw);
92
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
93
+ throw new Error(`book manifest must be a YAML object: ${manifestPath}`);
94
+ }
95
+ const manifest = parsed as Record<string, unknown>;
96
+ const chapterPaths = Array.isArray(manifest.chapters)
97
+ ? (manifest.chapters as unknown[]).filter((c): c is string => typeof c === "string")
98
+ : [];
99
+ if (chapterPaths.length === 0) {
100
+ throw new Error(`book manifest has no chapters: ${manifestPath}`);
101
+ }
102
+ const baseDir = dirname(absManifest);
103
+ return { manifest, chapters: loadChapters(chapterPaths, baseDir), baseDir };
104
+ }
105
+
106
+ function loadChapters(chapterPaths: string[], baseDir: string): LoadedChapter[] {
107
+ const out: LoadedChapter[] = [];
108
+ for (const chapter of chapterPaths) {
109
+ const chapterPath = isAbsolute(chapter) ? chapter : resolve(baseDir, chapter);
110
+ const source = readFileSync(chapterPath, "utf8");
111
+ const doc = parse(source, { filename: chapterPath });
112
+ inlineDatasetSources(doc, dirname(chapterPath));
113
+ const slug = chapterSlug(chapterPath, doc);
114
+ scopeHeadingIds(doc, slug);
115
+ out.push({ slug, source: chapterPath, doc });
116
+ }
117
+ return out;
118
+ }
119
+
120
+ function chapterSlug(chapterPath: string, doc: DocumentNode): string {
121
+ const root = doc.children.find(
122
+ (n): n is SectionNode => n.type === "section" && n.level === 1,
123
+ );
124
+ if (root && root.id) return root.id;
125
+ const base = chapterPath.replace(/\\/g, "/").split("/").pop() ?? chapterPath;
126
+ const stem = base.replace(/\.noma$/i, "").replace(/^\d+[-_]/, "");
127
+ return stem.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "");
128
+ }
129
+
130
+ /**
131
+ * In book mode, every heading slug is path-prefixed by its chapter root.
132
+ * `# Risk Premia 3` + `## Risks` → `risk-premia-3` and `risk-premia-3/risks`.
133
+ * Original (un-prefixed) ID is kept as an alias on the same node so any
134
+ * legacy `[[risks]]` writes still resolve to the first occurrence.
135
+ */
136
+ function scopeHeadingIds(doc: DocumentNode, chapterSlug: string): void {
137
+ for (const node of walk(doc)) {
138
+ if (node.type !== "section") continue;
139
+ if (node.level === 1) continue;
140
+ if (!node.id) continue;
141
+ if (node.id.startsWith(`${chapterSlug}/`)) continue;
142
+ const original = node.id;
143
+ node.id = `${chapterSlug}/${original}`;
144
+ const aliases = new Set<string>(node.aliases ?? []);
145
+ aliases.add(original);
146
+ node.aliases = [...aliases];
147
+ }
148
+ }
149
+
150
+ export function isBookManifestPath(path: string): boolean {
151
+ return /\.ya?ml$/i.test(path);
152
+ }
153
+
154
+ /**
155
+ * Pull the implicit chapter list off a fully-loaded document for TOC
156
+ * purposes. Each top-level h1 section is treated as a chapter heading.
157
+ */
158
+ export function listChapters(doc: DocumentNode): SectionNode[] {
159
+ return doc.children.filter(
160
+ (n): n is SectionNode => n.type === "section" && n.level === 1,
161
+ );
162
+ }