@geml/geml 1.5.0 → 1.6.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/README.md CHANGED
@@ -21,7 +21,8 @@ print("hi")
21
21
  ===
22
22
  ```
23
23
 
24
- - **Addressable** — every block has an `#id`; `geml get` / `geml set '#id'`
24
+ - **Addressable** — every block can be named: an `#id`, or a content address for
25
+ the ones nobody named; `geml get` / `geml set '<selector>'`
25
26
  read or patch one section without re-emitting the whole file (on this repo's
26
27
  own spec, ~**66× less context** than shipping the whole document).
27
28
  - **Verifiable** — references are checked at build time (a dangling `#id` is an
@@ -95,20 +96,50 @@ Every command reads a file path, or `-` for stdin. Exit codes: `0` ok ·
95
96
  ```sh
96
97
  geml doc.geml # document-model JSON (default --to json)
97
98
  geml doc.geml --to md|html|geml # convert; geml notes.md -> GEML
98
- geml get doc.geml ['#id'] # list addressable ids, or print one block (heading id = its section)
99
- geml set doc.geml '#id' [--head|--body] [--in F[#src]] # replace a block's content (id kept)
99
+ geml get doc.geml ['<selector>'] # list addressable blocks, or print what the selector matches
100
+ geml set doc.geml '<selector>' [--head|--body] [--in F[#src]] # replace ONE block's content
100
101
  geml add doc.geml (--append|--before #id|--after #id) [--in F[#src]] # insert a fragment
101
102
  geml delete doc.geml '#id' ['#id2' …] # remove one or more blocks
102
103
  geml rename doc.geml '#old' '#new' # rename an id + every reference to it
103
104
  geml revert doc.geml '#id' [--rev -1] # undo a block: splice / resurrect / remove
104
105
  geml check doc.geml [--root <dir>] # validate only: diagnostics + exit code (--json for the array)
105
- geml history <commit|verify|show|restore|log> doc.geml [...] # .gemlhistory version sidecar
106
+ geml history <save|get|restore|verify> doc.geml [...] # .gemlhistory version sidecar (get = list revisions, or print one)
106
107
  geml codemap <build|verify|render|serve|refresh|find|mcp> # your codebase's call graph as GEML docs
107
108
  geml --help | --version # --version --json prints {"parser","spec"}
108
109
  ```
109
110
 
110
111
  The agent loop: `geml get` a block → `set`/`add`/`delete`/`rename` it →
111
- `geml check` → `geml history commit` — small, precise, verifiable edits.
112
+ `geml check` → `geml history save` — small, precise, verifiable edits.
113
+
114
+ ### Selectors
115
+
116
+ `get` and `set` take the same selector, which is a **filter over blocks**:
117
+
118
+ | Selector | Matches |
119
+ |---|---|
120
+ | *(omitted)* | nothing — `get` **lists** every addressable block, one per line, by its shortest unique address |
121
+ | `#id` | that block. A heading id addresses its **whole section** |
122
+ | `'## Heading'` | a heading line copied out of the document, resolved to its id |
123
+ | `'=== note'` | **every** `note` block — 0..N of them |
124
+ | `'=== note@a3f9c1d2'` | one block by CONTENT, for blocks that carry no `#id` |
125
+ | `'@a3f9c1d2'` | the same, with the type check dropped |
126
+
127
+ `get` answers with N contents when N match (document order, count on stderr);
128
+ `set` writes ONE block, so a selector matching several is refused (exit 2) with
129
+ the unique address of each candidate. `--head` is the head line, `--body` the
130
+ body; both round-trip — `geml get f X --body | geml set f X --body` leaves the
131
+ file byte-identical.
132
+
133
+ A `@<hex>` **content address** is the first 8 hex of the SHA-256 of the block's
134
+ own text (line endings normalized to LF, no trailing newline), with `~1`, `~2`…
135
+ distinguishing byte-identical blocks. Read them out of `geml get doc.geml` —
136
+ they are printed for every block that has no `#id`. Being content-derived, an
137
+ address **goes stale when the block changes** and then fails with exit 1 rather
138
+ than silently addressing a different block: it doubles as a precondition. That
139
+ also means `set` through one prints the new address on stderr. The exact hash
140
+ input is pinned in
141
+ [the selector design doc](../docs/design/specs/2026-08-04-geml-get-set-selector-design-change.md)
142
+ §3.4 so a second implementation computes the same values.
112
143
 
113
144
  Conversion is one entry — `geml <file> [--to json|html|md|geml]`; the input
114
145
  format is inferred (`--from` overrides > extension > GEML), the target is `--to`
@@ -143,7 +174,7 @@ that did not exist then. So each forward edit has an inverse:
143
174
  | `rename #old #new` | `rename #new #old` (self-inverse) |
144
175
 
145
176
  `revert` reads the `.gemlhistory` sidecar, so `set`/`delete`/`add` undo needs a
146
- prior `geml history commit`; `rename` is its own inverse and needs no history.
177
+ prior `geml history save`; `rename` is its own inverse and needs no history.
147
178
 
148
179
  A **heading's** `#id` addresses its whole **section** — the heading line through
149
180
  the line before the next heading of the same-or-higher level — so the prose
package/codemap/build.mjs CHANGED
@@ -501,7 +501,7 @@ console.error(
501
501
  );
502
502
 
503
503
  // --history: snapshot every changed document into its .gemlhistory sidecar —
504
- // the graph's own architectural history (geml history log / revert per node).
504
+ // the graph's own architectural history (geml history get / revert per node).
505
505
  // Targets = documents rewritten this build, plus any document that has no
506
506
  // sidecar yet (first run, or --history adopted later).
507
507
  if (args.includes("--history")) {
@@ -510,7 +510,7 @@ if (args.includes("--history")) {
510
510
  console.error("--history needs the built parser (cd geml-parser && npm install && npm run build)");
511
511
  process.exit(1);
512
512
  }
513
- const { commit, isCurrent } = await import(`file://${histMod.replace(/\\/g, "/")}`);
513
+ const { save, isCurrent } = await import(`file://${histMod.replace(/\\/g, "/")}`);
514
514
  const message = flag("-m", flag("--message", "graph build"));
515
515
  const targets = new Set(stats.writtenDocs);
516
516
  for (const d of stats.allDocs) {
@@ -518,7 +518,7 @@ if (args.includes("--history")) {
518
518
  const gemlPath = join(outDir, d);
519
519
  const sidecar = gemlPath.replace(/\.geml$/, ".gemlhistory");
520
520
  // No sidecar yet, or the sidecar tip drifted from the file (a previous
521
- // commit attempt was refused): both need a snapshot even though this build
521
+ // save was refused): both need a snapshot even though this build
522
522
  // did not rewrite the document.
523
523
  if (!existsSync(sidecar) || !isCurrent(sidecar, gemlPath)) targets.add(d);
524
524
  }
@@ -527,10 +527,10 @@ if (args.includes("--history")) {
527
527
  for (const d of [...targets].sort()) {
528
528
  const gemlPath = join(outDir, d);
529
529
  try {
530
- commit({ gemlPath, historyPath: gemlPath.replace(/\.geml$/, ".gemlhistory"), summary: message });
530
+ save({ gemlPath, historyPath: gemlPath.replace(/\.geml$/, ".gemlhistory"), summary: message });
531
531
  committed++;
532
532
  } catch (e) {
533
- // One document's history refusing a commit (e.g. the round-trip gate)
533
+ // One document's history refusing a save (e.g. the round-trip gate)
534
534
  // must not abort the build or the other documents' snapshots. The
535
535
  // failing document's previous revision stays intact.
536
536
  histFailed.push(d);
@@ -1,4 +1,4 @@
1
- export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "embed-missing-src" | "ignored-embed-body" | "transclusion-cycle" | "embed-target-not-geml" | "media-target-is-document" | "inline-transclusion-not-inline" | "unsafe-embed-scheme" | "source-attr-conflict" | "unresolvable-table-source" | "table-source-not-a-table" | "unknown-metadata-reference" | "table-src-and-body" | "unknown-table-format" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "bad-span" | "span-outside-table" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value";
1
+ export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "unknown-attribute" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "embed-missing-src" | "ignored-embed-body" | "transclusion-cycle" | "embed-target-not-geml" | "media-target-is-document" | "inline-transclusion-not-inline" | "unsafe-embed-scheme" | "unresolvable-table-source" | "table-source-not-a-table" | "unknown-metadata-reference" | "table-src-and-body" | "unknown-table-format" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "compute-non-numeric-cell" | "compute-not-a-number" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value";
2
2
  export interface Diagnostic {
3
3
  severity: "error" | "warning";
4
4
  code: DiagnosticCode;
@@ -15,6 +15,7 @@
15
15
  export const SEVERITY = {
16
16
  "unterminated-block": "error",
17
17
  "unknown-block-type": "warning",
18
+ "unknown-attribute": "warning",
18
19
  "block-nesting-too-deep": "error",
19
20
  "list-nesting-too-deep": "error",
20
21
  "inline-nesting-too-deep": "error",
@@ -31,7 +32,6 @@ export const SEVERITY = {
31
32
  "media-target-is-document": "error",
32
33
  "inline-transclusion-not-inline": "error",
33
34
  "unsafe-embed-scheme": "error",
34
- "source-attr-conflict": "error",
35
35
  "unresolvable-table-source": "error",
36
36
  "table-source-not-a-table": "error",
37
37
  "unknown-metadata-reference": "error",
@@ -40,12 +40,12 @@ export const SEVERITY = {
40
40
  "bad-compute-formula": "error",
41
41
  "unlexable-compute-formula": "error",
42
42
  "compute-error": "error",
43
+ "compute-non-numeric-cell": "warning",
44
+ "compute-not-a-number": "warning",
43
45
  "bad-summary-entry": "error",
44
46
  "summary-unknown-column": "error",
45
47
  "unlexable-summary-expression": "error",
46
48
  "summary-error": "error",
47
- "bad-span": "error",
48
- "span-outside-table": "warning",
49
49
  "unknown-diagram-format": "warning",
50
50
  "ignored-diagram-body": "warning",
51
51
  "code-graph-missing-src": "warning",