@geml/dsh-plugin 1.0.3 → 1.0.4

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,6 @@
1
1
  {
2
2
  "name": "@geml/dsh-plugin",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Agent-Native document handling for DSH — addressable blocks let an agent read and edit one section instead of the whole file. Ships the geml MCP server and the authoring and code-graph skills.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/geml-spec/geml",
@@ -37,14 +37,17 @@ runs 5–11% of the file it maps, but on a changelog of many small sections it i
37
37
  rather than an error, so if a search you expect to hit comes back empty,
38
38
  use `list`.)
39
39
  3. `geml get <file> '<address>'` — that block and nothing else.
40
- 4. Edit with the ORDINARY file-editing tool, using the text from step 3 as the
41
- string to replace.
42
-
43
- Step 4 is a safety property, not a shortcut taken for speed. `geml set` and
44
- `geml replace` write through GEML's own semantics; a Markdown document nobody
45
- asked to convert is edited the ordinary way. If the block text does not match
46
- the file byte for byte, that edit fails loudly instead of writing something
47
- wrong.
40
+ 4. Change it. Two ways, and the choice is about **consent**, not capability:
41
+ - **Ordinary file-editing tool**, using the text from step 3 as the string to
42
+ replace. The default: a document nobody asked to address by block is edited
43
+ the way its author edits it.
44
+ - **`geml set <file> '#id' …`**, when block-addressed editing is what was
45
+ asked for — a knowledge base, a generated index, a log that only grows.
46
+ Measured on a real vault: the frontmatter and every block you did not
47
+ address come out **byte-for-byte unchanged**, and the body you write lands
48
+ verbatim — no escaping, no reflowing. Read
49
+ [`references/markdown-writes.md`](references/markdown-writes.md) first; two
50
+ of its rules are silent when broken.
48
51
 
49
52
  Never convert a document to GEML, never leave a `.gemlhistory` beside one, and
50
53
  do not pitch the format: use the tool, report the change you made, and let the
@@ -125,7 +128,7 @@ already; none is ever created for you. `--dry-run` shows what it would do.
125
128
  geml list file.geml # CALL THIS FIRST — every block, its address, kind, lines
126
129
  geml find "text" file|dir # search block CONTENT -> file<TAB>address (exit 1 = no hit)
127
130
  # a NAMED file is searched whatever its extension (.md too);
128
- # a directory walks *.geml only
131
+ # a directory walks *.geml and *.md
129
132
  geml get file.geml '#id' # read ONE block (a heading id = its whole section)
130
133
  geml set file.geml '#id' --in f # replace ONE block (re-parsed; never writes a broken doc)
131
134
  geml history save file.geml -m "…" # snapshot to .gemlhistory after each meaningful edit
@@ -156,7 +159,7 @@ geml get <skill-base>/references/authoring.geml '#tables'
156
159
  | section | covers |
157
160
  |---|---|
158
161
  | `#typed-block` | block anatomy, attribute object, examples of every registered type |
159
- | `#tables` | pipe/CSV bodies, `compute=`, `summary=`, printf display, `span=` merges |
162
+ | `#tables` | pipe/CSV bodies of FACTS, `delim=`, printf display — and the `view` that derives over one: `compute=`, `summary=`, `where=`, `order=`, `limit=`, `select=`, `by=`/`aggregate=` |
160
163
  | `#charts` | `geml-chart` diagrams bound to a table via `data=#id` |
161
164
  | `#data` | the `data` block — value tree, `json`/`jsonl` formats, blind append, chart binding |
162
165
  | `#inline` | inline markup, links/refs/footnotes, task lists, media embeds |
@@ -83,10 +83,13 @@ Two bodies, one model: the visual (pipe) form, or the data form
83
83
  | Basic | 1 | 30 |
84
84
  ===
85
85
 
86
- === table {#fy25 format=csv header=1 compute="FY [%.1f] = Q1 + Q2 + Q3 + Q4" summary="Segment = 'Total'; FY [%.1f] = sum(FY)"}
86
+ === table {#fy25 format=csv header=1}
87
87
  Segment, Q1, Q2, Q3, Q4
88
88
  Cloud, 8, 10, 12, 14
89
89
  ===
90
+
91
+ === view {#fy25-report src=#fy25 compute="FY [%.1f] = Q1 + Q2 + Q3 + Q4" summary="Segment = 'Total'; FY [%.1f] = sum(FY)"}
92
+ ===
90
93
  ====
91
94
 
92
95
  - `delim=";"` — one character replacing the data form's natural delimiter (`,`
@@ -94,6 +97,11 @@ Cloud, 8, 10, 12, 14
94
97
  single character is an error, a tab is `format=tsv` (attribute values have no
95
98
  escapes), and `delim` without a data `format` is ignored (warning). The data
96
99
  form splits and nothing more — it never strips outer `|` like the visual form.
100
+ A `table` holds FACTS and derives nothing. Everything below belongs to a
101
+ `view`, whose `src=` names a table, another view, or a `.csv`/`.tsv` file —
102
+ a `table` carrying these attributes is an `unknown-attribute` warning, and a
103
+ `table` whose `src=` names a BLOCK is an error pointing at `view`.
104
+
97
105
  - `compute="Name = expr; Name2 = expr2"` — per-row formulas over columns (by
98
106
  header name, or single letter `A`,`B`,…), operators `+ - * / ( )`.
99
107
  Reference an earlier computed column by name. Quote names with spaces:
@@ -102,7 +110,20 @@ Cloud, 8, 10, 12, 14
102
110
  foot row. A bare (non-aggregated) column ref in `summary` is an error.
103
111
  - A trailing `[printf]` on a name sets numeric display: `FY [%.1f]`,
104
112
  `P [%.1f%%]`.
105
- - Merge cells with `span="r2c1:2x1"`.
113
+ - `where="Status = 'open' and N > 3"` — keep rows. Comparisons take a number
114
+ or a single-quoted string; `not`/`and`/`or` and parentheses combine them. It
115
+ MAY name a per-row computed column; naming an aggregate-derived one is
116
+ circular and refused.
117
+ - `order="N desc, Id"` (`asc` default, stable), `limit=10`, and
118
+ `select="Id, N"` — columns only, in the order given; an `=` there points at
119
+ `compute=`.
120
+ - `by="Area"` groups; `aggregate="Open = count(Id)"` names the group's
121
+ columns. `by=` alone is the distinct set of those keys. To filter GROUPS,
122
+ consume the view from another one (SQL's `HAVING`).
123
+ - Order of evaluation is SQL's: per-row `compute` → `where` → aggregate
124
+ `compute` → `by`/`aggregate` → `order` → `limit` → `select` → `summary`.
125
+ So `sum(FY)` means the same in `compute=` and `summary=` — over the rows
126
+ shown — and a `summary=` must target a column `select=` kept.
106
127
 
107
128
  # Charts {#charts}
108
129
 
@@ -156,8 +177,12 @@ body the engine rejects is a build ERROR naming the line.
156
177
  file no longer has is an error (a drifted reference fails the build), and a
157
178
  body kept alongside it is a snapshot that warns when it goes stale. Routes
158
179
  resolve document-relative, or relative to `--root` when one is given.
159
- - `yaml`/`toml` are RESERVED names: no engine in the core — body kept raw
160
- plus a warning, never guessed. csv/tsv belong to `table`, not `data`
180
+ - `yaml` IS read, for a declared subset: block mappings and sequences,
181
+ quoted and block scalars, and YAML 1.2 **core-schema** scalars — so `yes`
182
+ is the string `"yes"`, not `true`. Anchors, aliases, tags, merge keys, flow
183
+ collections and a second document are outside it and are parse errors,
184
+ never guesses. `toml` stays a RESERVED name with no engine — body kept raw
185
+ plus a warning. csv/tsv belong to `table`, not `data`
161
186
  (their delimiter/header dialect parameters only mean something against a
162
187
  column model).
163
188
  - A chart's `data=#id` accepts a `data` block whose value is a RECORD ARRAY
@@ -222,7 +247,8 @@ geml list file.geml # CALL FIRST: every block, its address, ki
222
247
  geml find "text" file|dir # search block CONTENT -> file<TAB>address; exit 1 = no hit
223
248
  # a NAMED file is searched whatever its extension — `list`,
224
249
  # `get` and `find` all read Markdown, so this addresses a
225
- # plain README without converting it; a DIRECTORY walks *.geml
250
+ # plain README without converting it; a DIRECTORY walks
251
+ # *.geml and *.md — both formats the parser reads
226
252
  geml get file.geml # same listing as `list` (the no-selector default)
227
253
  geml get file.geml '#id' # print ONE block (raw span; --json = model node)
228
254
  geml get file.geml '=== note' # every block of a type; '@a3f9c1d2' = a block with no #id
@@ -281,8 +307,10 @@ snapshot as you go, rather than re-emitting the whole file:
281
307
 
282
308
  === code {#editing-loop lang=sh}
283
309
  geml get file.geml '#intro' # read just this block (a heading id = its whole section)
284
- geml set file.geml '#intro' --in - # replace just this span (stdin or --in FILE);
285
- # the splice is re-parsed and REJECTED if it breaks the doc
310
+ geml set file.geml '#intro' --body --in - # replace just this span (stdin or --in FILE);
311
+ # raw text needs --body; a bare --in takes a WHOLE
312
+ # block, fences and all. Either way the splice is
313
+ # re-parsed and REJECTED if it breaks the doc
286
314
  geml history save file.geml -m "…" # snapshot into the .gemlhistory sidecar — do this each step
287
315
  geml history get file.geml # revisions, newest first; first column IS the --rev selector
288
316
  geml revert file.geml '#intro' # roll ONE block back to the previous revision (= --rev -1)
@@ -0,0 +1,116 @@
1
+ # Writing to a Markdown file with `geml set`
2
+
3
+ `geml set` and `geml add` edit one block of a `.md` in place. Nothing is
4
+ converted: the file stays the Markdown it was, and a reader who does not have
5
+ `geml` sees no trace of it.
6
+
7
+ Everything below was measured against real documents, not read off the help
8
+ text. Two of the rules are **silent** when broken — no error, exit code 0 — and
9
+ they are the reason to read this before the first write rather than after.
10
+
11
+ ## What holds
12
+
13
+ **A write is surgical.** After `geml set <file> '#id' --body --in -`, the YAML
14
+ frontmatter and every block other than `#id` are byte-for-byte what they were.
15
+ The verb splices; it does not re-serialize the file.
16
+
17
+ **The body lands verbatim.** Whatever you write goes in as typed — a
18
+ `> [!tip]` callout, a `[[wikilink]]`, a `` ```dataview `` fence, a table. No
19
+ escaping, no reflowing, no normalization of your Markdown to anyone else's
20
+ taste.
21
+
22
+ **A broken result is refused.** The file is re-parsed before the write lands;
23
+ if the result would not parse, nothing is written.
24
+
25
+ **Heading addresses are stable.** `## Entities` is `#entities` on every run and
26
+ every platform, and it survives anything happening above it. That is the whole
27
+ reason to address a file this way instead of by line number.
28
+
29
+ One exception, and it is GitHub's too: **a repeated heading is numbered by
30
+ position.** The second `## Added` is `#added-1`, the third `#added-2` — the same
31
+ anchors GitHub gives them — so a Keep-a-Changelog file is writable section by
32
+ section. But a new `## Added` inserted above one shifts it: `#added-1` becomes
33
+ `#added-2`. Take a repeated heading's address from a fresh `geml list`, never
34
+ from memory.
35
+
36
+ **The file is read as Markdown, not as GEML.** Where the two disagree, a `.md`
37
+ gets Markdown's reading:
38
+
39
+ - `[[Note]]`, `[[Note#Heading]]`, `[[Note#Heading|alias]]`, `[[Note#^block]]`
40
+ and `![[Note#Heading]]` are Obsidian links. The note is found by name
41
+ anywhere under the resolution root, `.md` implied; one that does not exist
42
+ yet is a **warning**, never a refusal — a vault plans notes that way.
43
+ Within the page, `[[#Heading Text]]` may name a heading by its text.
44
+ - `[^label]` is a GFM footnote. With a `[^label]:` line it is a footnote;
45
+ without one it is plain text — so `[^0-9]` in a sentence about a regex is
46
+ nothing to worry about.
47
+ - `{{title}}` is text: a template engine's placeholder, not a reference.
48
+ - `~~~` fences and indented code blocks are code, like ``` ones. Nothing inside
49
+ is a link, a heading or a footnote.
50
+
51
+ The resolution root is the file's own directory unless you pass `--root`. A
52
+ note in a subfolder that links across its vault needs `--root <vault>`, and
53
+ `geml check` says so when it sees the vault's `.obsidian/` above you. It never
54
+ looks above the root on its own.
55
+
56
+ ## The three that bite
57
+
58
+ ### 1. Never `set` the frontmatter block — it destroys the frontmatter
59
+
60
+ Frontmatter is not a typed block in Markdown; it is an anonymous prose block,
61
+ and its **closing `---` is part of that block's body**. Replace the body and the
62
+ closer is gone, leaving an opener with nothing to close it — no properties at
63
+ all, for every tool that reads them.
64
+
65
+ Change frontmatter with an ordinary editor. This is silent: exit code 0.
66
+
67
+ ### 2. `--body` is for a heading. On a prose block it APPENDS
68
+
69
+ A heading's block is a heading line plus a body. A prose block is body all the
70
+ way down, so it has no separate body to set — `get '#x' --body` on one comes
71
+ back empty, and `set --body` writes into that emptiness, which lands **after**
72
+ the prose already there. Nothing is removed.
73
+
74
+ | target | `set '#id'` | `set '#id' --body` |
75
+ |---|---|---|
76
+ | a heading block | refused: *content is prose, not a block — use --body* | replaces the section body, keeps the heading line |
77
+ | a prose block | replaces it | **appends to it, silently** |
78
+
79
+ The rule is the opposite of what one habit would give you, and picking wrong
80
+ fails loudly one way and quietly the other. `geml list` prints the kind in its
81
+ second column — read it before choosing. This is the other silent one.
82
+
83
+ ### 3. `--in <file>` is not "read this text"
84
+
85
+ `--in F` means *take block `#id` from file F*. Raw text goes in on **stdin**:
86
+
87
+ ```sh
88
+ printf '…' | geml set page.md '#id' --body --in - # right
89
+ geml set page.md '#id' --body --in fragment.txt # looks for #id INSIDE fragment.txt
90
+ ```
91
+
92
+ Loud, at least: when F has no such block the refusal names the stdin form.
93
+
94
+ ## Smaller things worth knowing
95
+
96
+ - `find` is a **literal substring**, not a pattern. `Hot.Cache` does not match
97
+ "Hot Cache". Case-insensitive unless `--case`.
98
+ - `find --head` shows **one line per block**. A block with thirteen matches
99
+ reports one; `find` locates, `get` reads.
100
+ - `set --body` replaces a trailing `---` rule too, if the section ends with one.
101
+ - The blank line after a heading is not re-inserted. Cosmetic; renderers do not
102
+ care, a diff does.
103
+ - A directory walk skips hidden directories. A tree that hides sources in
104
+ `.raw/` must name that directory: `geml find 'x' notes .raw`.
105
+ - `@…` addresses are content hashes and change when the content does. Fine to
106
+ read from a fresh `list`; never store one, and never write to one.
107
+
108
+ Before a first write to a page, `geml check <file>` — exit 0 means every block is
109
+ writable; warnings (a note not yet written) never block one.
110
+
111
+ ## Do not convert the file
112
+
113
+ `geml <page>.md --to geml` and back is lossy for anything beyond plain
114
+ Markdown — frontmatter lists collapse, `[[…]]` comes back escaped, thematic
115
+ breaks are dropped. The point of addressing a Markdown file is that it **stays
116
+ Markdown**.