@gmb/bitmark-parser 7.2.0 → 7.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 (47) hide show
  1. package/README.md +155 -23
  2. package/config/bitmark-json.tsp +330 -0
  3. package/config/bitmark.json +83394 -0
  4. package/dist/browser/bitmark-parser.min.js +6 -4
  5. package/dist/browser/bitmark-parser.min.js.map +1 -1
  6. package/dist/browser/cjs/index.cjs +140 -72
  7. package/dist/browser/cjs/index.cjs.map +1 -1
  8. package/dist/browser/cjs/index.d.cts +6286 -6806
  9. package/dist/browser/esm/index.d.ts +6286 -6806
  10. package/dist/browser/esm/index.js +139 -72
  11. package/dist/browser/esm/index.js.map +1 -1
  12. package/dist/browser/esm/worker-entry.js +86 -57
  13. package/dist/browser/esm/worker-entry.js.map +1 -1
  14. package/dist/browser/wasm/bitmark_browser_full_wasm_bg.wasm +0 -0
  15. package/dist/browser/wasm/bitmark_json_wasm_bg.wasm +0 -0
  16. package/dist/browser/wasm/bitmark_wasm_bg.wasm +0 -0
  17. package/dist/index.cjs +76 -29
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +6286 -6806
  20. package/dist/index.d.ts +6286 -6806
  21. package/dist/index.js +75 -29
  22. package/dist/index.js.map +1 -1
  23. package/dist/legacy.cjs +4 -3
  24. package/dist/legacy.cjs.map +1 -1
  25. package/dist/legacy.d.cts +2 -1
  26. package/dist/legacy.d.ts +2 -1
  27. package/dist/legacy.js +4 -3
  28. package/dist/legacy.js.map +1 -1
  29. package/dist/worker-entry.cjs +23 -15
  30. package/dist/worker-entry.cjs.map +1 -1
  31. package/package.json +11 -7
  32. package/schema/bitmark.schema.json +10427 -14051
  33. package/wasm/bitmark_wasm.d.ts +12 -3
  34. package/wasm/bitmark_wasm.js +34 -15
  35. package/wasm/bitmark_wasm_bg.wasm +0 -0
  36. package/wasm/bitmark_wasm_bg.wasm.d.ts +2 -2
  37. package/wasm/package.json +1 -1
  38. package/wasm-bitmark-json/bitmark_json_wasm.d.ts +12 -3
  39. package/wasm-bitmark-json/bitmark_json_wasm.js +34 -15
  40. package/wasm-bitmark-json/bitmark_json_wasm_bg.wasm +0 -0
  41. package/wasm-bitmark-json/bitmark_json_wasm_bg.wasm.d.ts +2 -2
  42. package/wasm-bitmark-json/package.json +1 -1
  43. package/wasm-browser-full/bitmark_browser_full_wasm.d.ts +12 -3
  44. package/wasm-browser-full/bitmark_browser_full_wasm.js +34 -15
  45. package/wasm-browser-full/bitmark_browser_full_wasm_bg.wasm +0 -0
  46. package/wasm-browser-full/bitmark_browser_full_wasm_bg.wasm.d.ts +2 -2
  47. package/wasm-browser-full/package.json +1 -1
package/README.md CHANGED
@@ -69,9 +69,9 @@ runtime — the API surface never changes:
69
69
 
70
70
  | Feature | Contents | Size (approx.) |
71
71
  | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------- |
72
- | `full` (Node default) | everything: rich `info` metadata + built-in translations + the semantic `diff` | ~1025 KB (~395 KB gzip) |
73
- | `browser-full` (browser default) | the same conversions and `diff`, without either | ~818 KB (~333 KB gzip) |
74
- | `bitmark-json` | bitmark ↔ JSON only: `convert`/`canonicalize`/`transform` (formats `auto`/`bitmark`/`json`, plus the `text`, `semantic-tokens` and `diagnostics` outputs), the editor services, `info` (JSON only), breakscape, text fragments — no `diff` | ~599 KB (~239 KB gzip) |
72
+ | `full` (Node default) | everything: rich `info` metadata + built-in translations + the semantic `diff` | ~1044 KB (~410 KB gzip) |
73
+ | `browser-full` (browser default) | the same conversions and `diff`, without either | ~855 KB (~353 KB gzip) |
74
+ | `bitmark-json` | bitmark ↔ JSON only: `convert`/`canonicalize`/`transform` (formats `auto`/`bitmark`/`json`, plus the `text`, `semantic-tokens` and `diagnostics` outputs), the editor services, `info` (JSON only), breakscape, text fragments — no `diff` | ~629 KB (~256 KB gzip) |
75
75
 
76
76
  **The rule.** _Every build includes every feature, except the two
77
77
  browser-targeted variants, which each carry an explicit exclusion list._ That
@@ -82,7 +82,7 @@ below:
82
82
  | -------------------------------------------------- | ----------------------------------------------------------------------------------- |
83
83
  | native CLI, daemon, docgen, schemagen, wasm `full` | **all** |
84
84
  | wasm `browser-full` | all **except** `info-meta` (bit + tag prose) and `translations` (the baked table) |
85
- | wasm `bitmark-json` | all **except** the above and `markup`, `mapping-report`, `diff`, `lex`, `info-text` |
85
+ | wasm `bitmark-json` | all **except** the above and `markup`, `mapping-report`, `diff`, `lex` (with `ast`), `info-text` |
86
86
 
87
87
  So, not in `bitmark-json`: the markup formats (`html`, `xml`,
88
88
  `xml-niso-iec`, …) as input or output (they throw an `unsupported` error), the
@@ -192,9 +192,10 @@ Convert between bitmark, JSON, and the mapped markup formats (html/xml/…).
192
192
 
193
193
  A JSON input may be an array or a single value, each entry a `{ bit: {…} }`
194
194
  envelope (the forward output) or a bare bit object. An entry that is not a
195
- bit produces no bitmark: it is skipped and reported as an `unknown-bit-type`
196
- issue naming its JSON path (`$[2] is not a bit: no "type" — skipped`) on the
197
- warnings channel of the CLI (`--warnings`) and the daemon; the bits beside it
195
+ bit produces no bitmark: it is skipped and reported as a `bit-invalid` issue
196
+ (or `bit-type-invalid` when it has a `type` the registry does not know) naming
197
+ its JSON path (`$[2] is not a bit: expected a bit object with "type" —
198
+ ignored`) on the warnings channel of the CLI (`--warnings`) and the daemon; the bits beside it
198
199
  still convert, and a document in which nothing is a bit converts to empty
199
200
  output. This `convert` returns a bare string and so cannot show the skip.
200
201
  Only malformed JSON *text* throws (`invalid-json`).
@@ -202,7 +203,7 @@ Only malformed JSON *text* throws (`invalid-json`).
202
203
  Options:
203
204
 
204
205
  - `inputFormat` — `"auto"` (default), `"bitmark"`, `"json"`, or a mapping id (`"html"`, `"xml"`, …)
205
- - `outputFormat` — `"auto"` (default: the opposite direction), `"text"` (lossy plain-text extraction; output-only), `"semantic-tokens"` / `"diagnostics"` / `"lex"` (output-only, bitmark input only — see their sections), or as above
206
+ - `outputFormat` — `"auto"` (default: the opposite direction), `"text"` (lossy plain-text extraction; output-only), `"semantic-tokens"` / `"diagnostics"` / `"lex"` / `"ast"` (output-only, bitmark input only — see their sections), or as above
206
207
  - `mode` — `"optimized"` (default) or `"full"`
207
208
  - `warnings` — include validation warnings (default: `false`)
208
209
  - `plainText` — output text as plain text (default: `false`)
@@ -255,7 +256,7 @@ Options:
255
256
  For bitmark input, output is a JSON array of entries with:
256
257
 
257
258
  - `bit` — serialized bit payload
258
- - `parser` — parser metadata, `errors` / `warnings`, and `infos` (informational notices for recoveries that are not necessarily wrong: text kept as text because it only looks like an unclosed tag, `unclosed-tag`, or a formatting mark with no partner, `unclosed-formatting` — in bitmark malformed markup is text, not an error). Each issue carries a stable machine-readable `code` (`"unknown-property"`, `"missing-required-tag"`, …) beside its human-readable `message`, plus `text` and `location`. **Branch on `code`** — a shipped code never changes meaning, whereas the message text is not contractual and may be reworded in any release
259
+ - `parser` — parser metadata, `errors` / `warnings`, and `infos` (informational notices for recoveries that are not necessarily wrong: text kept as text because it only looks like an unclosed tag, `tag-unclosed`, or a formatting mark with no partner, `formatting-unclosed` — in bitmark malformed markup is text, not an error). Each issue carries a stable machine-readable `code` (`"tag-invalid"`, `"tag-missing"`, …) and a `scope` (`"bit"`, `"card"`, `"bit-chain"` or `"card-chain"` — where the issue sits) beside its human-readable `message`, plus `text` and `location`. **Branch on `code` and `scope`** — a shipped code never changes meaning, whereas the message text is not contractual and may be reworded in any release. `bitmark info error-codes` lists every code with its severity and message template (see `info` below)
259
260
  - `bitmark` — source bitmark text (a convenience echo; every bit is fully described by `bit` alone)
260
261
 
261
262
  A region the parser could not read — an unknown bit type, or non-blank text
@@ -266,6 +267,17 @@ commented-out bit and any `:format` / `&resource` suffix; absent when there
266
267
  was no header) and `body` (the raw text after it, as a plain string), so
267
268
  generating bitmark from it reproduces the original region.
268
269
 
270
+ The other direction reports too. Going **to** bitmark (from JSON or from
271
+ markup), an inline mark that has no bitmark form is dropped — the forward
272
+ parser would not read it back as that mark, so writing it would corrupt the
273
+ surrounding text. The text it decorated is always kept, and the loss is
274
+ reported: `mark-dropped` when the whole mark goes, `mark-reduced` when it
275
+ survives in a lesser form (its value cleared, or one of its segments dropped).
276
+ Both arrive on the conversion's warnings, like every other issue. In practice
277
+ they are rare — a `href` holding a line terminator, a mark kind the grammar has
278
+ no tag for — so treat either as a signal that the JSON holds something bitmark
279
+ cannot express, not as routine noise.
280
+
269
281
  #### canonicalize(input: string, options?: CanonicalizeOptions): string
270
282
 
271
283
  Re-emit input in its own format in canonical form — the single same-format surface. `mode`: `"optimized"` (default, omit natural defaults) or `"full"` (all keys).
@@ -277,7 +289,17 @@ Apply a patch document to a document, bit by bit, and re-emit it.
277
289
 
278
290
  - `patch` — a `PatchDocument`, or the shorthand: a bare array of entries
279
291
  applied to every bit (build entries with `patchEntry`). Either form may be
280
- a JSON string
292
+ a JSON string. An entry's `path` is in one of two languages, told apart by
293
+ its first character (one language per call; a mixed call is refused):
294
+ - a **JSON path** addresses the bit JSON (`title`,
295
+ `quizzes[0].choices[1].choice`, `table.data[1][1]`, `id[-1]`) and the
296
+ value is a JSON value;
297
+ - a **bitmark path** addresses the bit as an author writes it and the value
298
+ is bitmark source (see *Bitmark paths* below). Bitmark paths need
299
+ bitmark input
300
+ - a skipped entry — a bad path, an index out of range, a value the engine
301
+ refuses — **throws `PatchError`** (`diagnostics: { path, kind, message }[]`)
302
+ rather than returning a document with the edit silently missing
281
303
  - `preHook` / `postHook` — per-bit hooks; the pre-hook may return patches, an
282
304
  output-format override or `drop`. Hooks see the original bits, in order,
283
305
  before the document reshapes anything; a hook's patches apply after the
@@ -286,13 +308,14 @@ Apply a patch document to a document, bit by bit, and re-emit it.
286
308
  `spacesAroundValues` — as for `convert`. `outputFormat` (default `"json"`)
287
309
  takes every per-bit format: `"json"`, `"bitmark"`, `"text"` or a mapping id
288
310
  such as `"html"` (per-bit text outputs are joined with newlines). The
289
- whole-document outputs `"lex"`, `"semantic-tokens"` and `"diagnostics"` are
290
- refused with an `unsupported` error in every driver — use `convert`. An
291
- explicit `inputFormat` is honoured as given, patch or hooks or not; it is
292
- never re-detected from the content
311
+ whole-document outputs `"lex"`, `"ast"`, `"semantic-tokens"` and
312
+ `"diagnostics"` are refused with an `unsupported` error in every driver —
313
+ use `convert`. An explicit `inputFormat` is honoured as given, patch or
314
+ hooks or not; it is never re-detected from the content
293
315
 
294
316
  A patch document addresses bits by their position (or id) in the input
295
- **before** any entry is applied, so it reads like a diff:
317
+ **before** any entry is applied, so it reads like a diff. An inserted `bit`
318
+ is a JSON bit, or a string holding one bit of bitmark source:
296
319
 
297
320
  ```json
298
321
  {
@@ -319,6 +342,53 @@ or `-1` for the document start. A bit may be the target of at most one
319
342
  and moving it are all rejected, naming the second of the conflicting entries.
320
343
  `diff` produces such a document from two versions of a file (below).
321
344
 
345
+ #### Bitmark paths
346
+
347
+ A bitmark path spells the thing it names the way an author writes it, so the
348
+ same path works for every bit type and no JSON key name is needed:
349
+
350
+ | Path | Addresses |
351
+ | --- | --- |
352
+ | `[@id]`, `[!]`, `[#]`, `[&image]` | the bit's first such tag; `[@id][-1]` the last, `[-][1]` the second `[-…]` |
353
+ | `[&image][@width]` | `[@width]` in the run after `[&image]` — a chain is spelled like the chain |
354
+ | `$body` | the bit's own text (tags, cards and footer untouched) |
355
+ | `$raw` | everything after the bit header |
356
+ | `$card[1]` | the whole second card; `$card[1][0]` its first side, `$card[1][0][1]` a variant |
357
+ | `$card[1][!]`, `$card[1][-][1]` | tags inside a card, by occurrence |
358
+ | `$card[1]$body`, `$card[1]$raw` | a card's own text / whole content |
359
+ | `$footer` | the `==== footer ====` section |
360
+
361
+ Values are bitmark source, breakscaped as an author would write it (a bare
362
+ `]` or a trailing `^` in a tag value is refused — use `breakscapeText` with
363
+ `location: "tag"` for user data, and add `subLocation: "attrValue"` when the
364
+ value is an inline-attr chain value such as a `link:` href); `null` is a
365
+ valueless tag (`[@x]`). The
366
+ four ops are the JSON engine's: `set` replaces a tag's value, a section's
367
+ content or a scope's text; `create` sets only when absent; `delete` removes
368
+ a tag (**a run head takes its whole run** — `[&image:u][@width:1]` minus
369
+ `[&image]` leaves nothing behind), a section, a body; `append` adds another
370
+ occurrence after the last one on its own line (`$card[1][-]` twice gives
371
+ white, green, blue, red), a sibling section after `$card[c]`, or a new last
372
+ card for a bare `$card`.
373
+
374
+ Every edit is a splice of the bit's own source: the parser's tree is the
375
+ address map, the rest of the bit keeps its formatting, and an edit that
376
+ would change the structure outside the addressed scope — a `==== text ====`
377
+ or `==== footer ====` that would swallow the cards after it, a bit header in
378
+ a value — is refused with the bit untouched. `convert(…, { outputFormat:
379
+ "ast" })` shows the tree a path resolves against.
380
+
381
+ ```js
382
+ transform(doc, {
383
+ outputFormat: "bitmark",
384
+ patch: [
385
+ { path: "$card[1][-]", op: "append", value: "green" },
386
+ { path: "$card[0][!]", op: "set", value: "Milk (cold)" },
387
+ { path: "$body", op: "set", value: "Think **twice**." },
388
+ ],
389
+ });
390
+ ```
391
+
322
392
  #### diff(a: string, b: string, options?: DiffOptions): string
323
393
 
324
394
  Semantic diff of two documents. Both sides are re-canonicalized and
@@ -458,10 +528,10 @@ const { positionEncoding, diagnostics: list } = diagnostics(source);
458
528
  // list[i] = {
459
529
  // range: { start: { line, character }, end: { line, character } }, // 0-based, UTF-16 units
460
530
  // severity: 2, // DiagnosticSeverity.Warning
461
- // code: "unknown-property", // the contract — branch on this
531
+ // code: "tag-invalid", // the contract — branch on this
462
532
  // source: "bitmark",
463
- // message: "[@nope] is an unknown property …", // prose, may change
464
- // data: { bit: 0 }, // the bit's index in the JSON array
533
+ // message: "[@nope] is not valid for [.article]", // prose, may change
534
+ // data: { bit: 0, scope: "bit" }, // the bit's index in the JSON array; the issue's scope
465
535
  // }
466
536
  ```
467
537
 
@@ -552,7 +622,7 @@ offers the bit types). The CLI has the same three services under one
552
622
  command: `bitmark editor diagnostics|complete|resolve|hover`. The contract is
553
623
  `.zen/specs/API-EDT-editor-services.tsp`.
554
624
 
555
- #### The `lex` output format
625
+ #### The `lex` and `ast` output formats
556
626
 
557
627
  `convert(input, { inputFormat: "bitmark", outputFormat: "lex" })` returns the
558
628
  lexer's token stream as a JSON array: one `{ kind, span, text }` per token
@@ -563,19 +633,59 @@ so a highlighter should use `semantic-tokens` instead. Bitmark input only; a
563
633
  JSON or markup document has no source text to lex. The former `lex()` export
564
634
  and `bitmark lex` command were removed in 7.0 (see the migration guide).
565
635
 
636
+ `convert(input, { inputFormat: "bitmark", outputFormat: "ast" })` returns the
637
+ parser's tree as JSON — bits, their blocks (tag runs, body text, dividers)
638
+ and inlines, every node with its byte `span`. Debug tooling like `lex`: the
639
+ shape follows the parser and is not a contract. It shows what a bitmark
640
+ patch path addresses (a run is one `TagChain`; a card is what sits between
641
+ divider blocks). Not to be confused with the legacy entry point's "ast",
642
+ which is bit JSON. Bitmark input only; in the same builds as `lex`.
643
+
566
644
  #### breakscapeText(input: string, options?: BreakscapeOptions): string
567
645
 
568
646
  Breakscape text (escape bitmark special characters).
569
647
 
570
648
  - `format` — `"bitmark++"` (default) or `"plainText"`
571
- - `location` — `"body"` (default) or `"tag"`
649
+ - `location` — `"body"` (default), `"cardBody"` or `"tag"`
650
+ - `subLocation` — `"text"` (default) or `"attrValue"`
651
+
652
+ `subLocation` REFINES `location`, it does not replace it. `"attrValue"` means
653
+ the string is the value half of an inline-attr chain segment
654
+ (`==t==|link:VALUE|`): it adds the chain delimiter `|` to the escaped set and
655
+ drops the running-text rules (inline doubles, block markers), while `location`
656
+ keeps deciding the tag rules — a `[`+trigger is escaped in `body` and inert in
657
+ `tag`, a `]` is inert in `body` and escaped in `tag`.
658
+
659
+ `"cardBody"` is a card side or variant body. It has the `body` rules plus the
660
+ card dividers `--` and `++`, which are markup only inside a card set and only
661
+ at the start of a line, where they break to `-^-` and `+^+`.
662
+
663
+ Reach for `subLocation: "attrValue"` when building a chain value for the patch
664
+ API by hand; without it, an href containing `[@` cannot be written correctly.
665
+
666
+ ```ts
667
+ breakscapeText("http://x/a[@b]", { location: "body", subLocation: "attrValue" });
668
+ // → "http://x/a[^@b]"
669
+ ```
670
+
671
+ Carets themselves follow one rule: **a literal `^` is written `^^` except with
672
+ `format: "plainText"` and `location: "body"`, where it is written `^`.** Three
673
+ cases land on the `^^` side although they read as though they would not — a tag
674
+ value in a plain-format bit (the `:text` suffix switches the body only), an
675
+ inline `code` mark (ordinary bitmark text carrying a mark), and an attr value
676
+ or bare URL. A `|code` *block* body is raw, but it has no public `format`: the
677
+ parser and generator apply that rule themselves at the block boundary.
572
678
 
573
679
  #### unbreakscapeText(input: string, options?: BreakscapeOptions): string
574
680
 
575
681
  Unbreakscape text (unescape bitmark special characters).
576
682
 
577
683
  - `format` — `"bitmark++"` (default) or `"plainText"`
578
- - `location` — `"body"` (default) or `"tag"`
684
+ - `location` — `"body"` (default), `"cardBody"` or `"tag"`
685
+ - `subLocation` — accepted and **ignored**
686
+
687
+ For `bitmark++` the rule is "remove one caret from each run" whatever the
688
+ context, so a single inverse undoes every breakscape mode.
579
689
 
580
690
  #### info(options?: InfoOptions): string
581
691
 
@@ -588,7 +698,10 @@ defaults), JSON keys, per-context overrides, and the card set structure. `"depre
588
698
  version and (separately) any migration target.
589
699
 
590
700
  - `infoType` — `"list"` (default), `"bit"`, `"all"`, `"deprecated"`,
591
- `"bit-groups"`, `"resource-groups"`, or `"languages"`
701
+ `"bit-groups"`, `"resource-groups"`, `"languages"`, or `"error-codes"`
702
+ (every issue code the parser can emit, with its severity and its message
703
+ templates per scope — `ErrorCodesInfo`; read from the validator's own
704
+ message table, so it is what the envelope emits, in every variant)
592
705
  - `format` — `"text"` (default) or `"json"`
593
706
  - `bit` — filter to a specific bit type (when `infoType` is `"bit"`)
594
707
  - `pretty` — prettify JSON output (default: `false`)
@@ -791,6 +904,21 @@ with each release (the site root hosts the bitmark language docs), or build
791
904
  it locally with `npm run docs` (→ `docs/api/`) and preview it with
792
905
  `npm run docs:serve` (http://localhost:8080, `--port` to change).
793
906
 
907
+ ### Resolved config document
908
+
909
+ The package ships the resolved bitmark configuration its parser was compiled
910
+ against — every bit type, tag group and card set with descriptions, formats,
911
+ defaults and mapping keys — as `bitmark.json`, with its TypeSpec contract
912
+ beside it:
913
+
914
+ ```ts
915
+ import config from "@gmb/bitmark-parser/bitmark.json" with { type: "json" };
916
+ // contract: node_modules/@gmb/bitmark-parser/config/bitmark-json.tsp
917
+ ```
918
+
919
+ It is generated by the build (`bitmark-confgen`) from the repo's jsonc config
920
+ tree, never hand-edited, and its `label` is a content hash of that tree.
921
+
794
922
  ### JSON Schema
795
923
 
796
924
  The package also ships a JSON Schema (Draft 2020-12) describing the parser's
@@ -827,7 +955,7 @@ A tag whose configured format is `number` accepts what JavaScript's
827
955
 
828
956
  `NaN`, `Infinity`, an exponent beyond ±4000, and a radix literal past 64 bits
829
957
  are **rejected**: the tag is dropped from the output and a
830
- `property-format-mismatch` warning names it, exactly like any other value that
958
+ `tag-value-invalid` warning names it, exactly like any other value that
831
959
  does not fit its format.
832
960
 
833
961
  Accepted values print in JavaScript's `JSON.stringify` form — `0.5`, `1.5`,
@@ -953,6 +1081,10 @@ bitmark convert input.bitmark --input-format bitmark --output-format lex --prett
953
1081
  # Breakscape / unbreakscape text
954
1082
  bitmark breakscape input.txt
955
1083
  bitmark breakscape input.txt --format plainText --location tag
1084
+ # a card side / variant body, where the card dividers are live
1085
+ bitmark breakscape input.txt --location cardBody
1086
+ # a value for an inline-attr chain segment (==t==|link:VALUE|)
1087
+ bitmark breakscape input.txt --location body --sub-location attrValue
956
1088
  bitmark unbreakscape input.txt
957
1089
 
958
1090
  # Query bit type information
@@ -0,0 +1,330 @@
1
+ /**
2
+ * bitmark.json — the resolved bitmark configuration document.
3
+ *
4
+ * `bitmark-confgen` resolves the hand-edited jsonc config tree
5
+ * (`resources/bitmark-configurator/config`) plus `manifest.jsonc` into this
6
+ * one document at build time (PLAN-144, PLAN-207). The parser's lookup tables,
7
+ * the JSON Schema, the TypeScript types and the docs site are all generated
8
+ * from it, and the npm package ships the exact document its parser was
9
+ * compiled against as `@gmb/bitmark-parser/bitmark.json`, with this file
10
+ * beside it as `@gmb/bitmark-parser/bitmark-json.tsp`.
11
+ *
12
+ * Prose spec with the derivation algorithms: `.zen/specs/BITMARK-JSON-FORMAT.md`.
13
+ * Shape source of truth: `crates/app/bitmark_confgen/src/model.rs` — any change
14
+ * there updates this file in the same PR.
15
+ *
16
+ * Conventions:
17
+ * - Identity is text. Root entities are keyed by name in sorted maps; every
18
+ * single-parent construct nests inside its owner. There are no numeric ids.
19
+ * - Optional members are omitted when absent, never `null`. Two exceptions:
20
+ * flag booleans (`bodyRequired`, `alwaysEmit`, …) are present only when
21
+ * `true`, and `TagDef.default` is `null` to mean "omit".
22
+ * - Arrays are in semantic (declaration) order; maps are sorted by key.
23
+ * - Derived members (`allTags`, `usedBy`, `parentOverrides`, `resources`) are
24
+ * computed by confgen, never authored.
25
+ */
26
+ namespace BitmarkConfig;
27
+
28
+ /** A mapping-id → key-pattern record. Ids are registered in `mappingTypes`;
29
+ * a pattern is the verbatim authored JSON value (string, object or array of
30
+ * predicate records — see `crates/lib/jsonkey_parser/doc/`). */
31
+ model MappingKeys is Record<unknown>;
32
+
33
+ /** The document. */
34
+ model BitmarkJson {
35
+ /** App semver, from `manifest.jsonc` (kept in lockstep with the packages). */
36
+ version: string;
37
+
38
+ /** Provenance: `fnv1a64:<16 hex>` over every input file keyed by its
39
+ * config-relative path, plus the manifest. Independent of where the tree
40
+ * lives on disk; no wall clock. */
41
+ label: string;
42
+
43
+ /** Manifest order, verbatim. */
44
+ locales: Locale[];
45
+
46
+ /** Manifest order, verbatim. */
47
+ mappingTypes: MappingType[];
48
+
49
+ /** Bits by technical name (sorted). Abstract base definitions are absent. */
50
+ bits: Record<Bit>;
51
+
52
+ /** Tag groups by name (sorted). */
53
+ groups: Record<Group>;
54
+
55
+ /** Card sets by key (sorted). */
56
+ cardSets: Record<CardSet>;
57
+
58
+ /** Resource catalog by resource type (sorted); derived from the
59
+ * `resource-<type>` groups minus the manifest's `resourceUtilityGroups`. */
60
+ resources: Record<Resource>;
61
+
62
+ /** Bit-group registry by key (sorted). Metadata only: membership lives on
63
+ * each bit's `bitGroups`. */
64
+ bitGroups: Record<BitGroup>;
65
+
66
+ /** Resource-group registry by key (sorted). Here the member list is
67
+ * authoritative. */
68
+ resourceGroups: Record<ResourceGroup>;
69
+ }
70
+
71
+ model Locale {
72
+ /** BCP-47 language code, e.g. "en". */
73
+ code: string;
74
+ isBase: boolean;
75
+ /** English label, e.g. "English". */
76
+ label: string;
77
+ }
78
+
79
+ /** A registered mapping id and its representation type. */
80
+ model MappingType {
81
+ /** Mapping id used as a key in every `mappingKeys` record, e.g. "json". */
82
+ id: string;
83
+
84
+ /** Representation type: "json" | "html" | "xml" (open set); informational. */
85
+ type: string;
86
+
87
+ /** Parent mapping id: key lookups under this id fall back to the base
88
+ * (publisher overrides). */
89
+ `extends`?: string;
90
+ }
91
+
92
+ model Bit {
93
+ description: string;
94
+
95
+ /** English display name; other languages come from `translations.json`.
96
+ * Absent when unauthored. */
97
+ title?: string;
98
+
99
+ /** Bit-group memberships, sorted — the single home of that fact. Omitted
100
+ * when the bit is deliberately ungrouped. */
101
+ bitGroups?: string[];
102
+
103
+ /** Author-facing usage notes, one bullet per entry; docs-only. */
104
+ usageNotes?: string[];
105
+
106
+ /** "bitmark" | "text" | "latex" | "json" | "xml". */
107
+ bodyFormat: string;
108
+
109
+ /** Flag booleans: present only when `true`. */
110
+ bodyRequired?: true;
111
+ bodyForbidden?: true;
112
+ footerRequired?: true;
113
+ footerForbidden?: true;
114
+
115
+ /** Version string at which the bit was deprecated; docs-only. */
116
+ deprecated?: string;
117
+
118
+ /** Migration target: engines re-emit input using this (deprecated) bit
119
+ * type under the named bit. */
120
+ migrateTo?: string;
121
+
122
+ /** "all" | "none". */
123
+ resourceAttachmentAllowed: string;
124
+
125
+ /** Required resource type (a key of `resources`); omitted when none. */
126
+ resourceRequired?: string;
127
+
128
+ /** Key of `cardSets`; omitted when the bit has no card set. */
129
+ cardSet?: string;
130
+
131
+ /** Bit-level named element keys for markup import; omitted when empty. */
132
+ mappingKeys?: MappingKeys;
133
+
134
+ /** Ordered declaration list: inline tag definitions and group references. */
135
+ tags: TagRef[];
136
+
137
+ /** Computed transitive closure of tags, as scoped keys, in declaration-DFS
138
+ * order with name dedup (first position kept, later declaration wins).
139
+ * A scoped key is `<kind>:<ownerPath>.<tag>` — `bit:article.#`,
140
+ * `group:article.#`, `variant:flashcard.default.question.1.@example` —
141
+ * and a chain child appends to its parent's key. */
142
+ allTags: string[];
143
+ }
144
+
145
+ /** One entry of a `tags` / `chain` array: an inline definition or a group
146
+ * reference, told apart by which of `tag` / `group` is present. */
147
+ union TagRef {
148
+ TagDef,
149
+ GroupRef,
150
+ }
151
+
152
+ model TagDef {
153
+ /** Config key including its sigil (`@example`, `#`, `&image`, `%`, …) —
154
+ * the identity within the owner scope. */
155
+ tag: string;
156
+
157
+ description: string;
158
+
159
+ /** "string" | "bitmark+" | "boolean" | "number" | "numberList4" | "enum". */
160
+ format: string;
161
+
162
+ /** Enum vocabulary: present (non-empty, unique) exactly when `format` is
163
+ * "enum". */
164
+ values?: string[];
165
+
166
+ min: int64;
167
+
168
+ /** -1 = unlimited. */
169
+ max: int64;
170
+
171
+ /** Always present. `null` means OMIT: absence is the canonical state and a
172
+ * present value is never stripped nor an absent one materialised.
173
+ * Otherwise the format's natural value ("false" / "0" / "") or an
174
+ * authored non-natural value, which requires `alwaysEmit`. */
175
+ default: string | null;
176
+
177
+ /** Flag boolean: pin the key even in optimised mode, materialising the
178
+ * default when absent. */
179
+ alwaysEmit?: true;
180
+
181
+ /** Version string at which the tag was deprecated; docs-only. */
182
+ deprecated?: string;
183
+
184
+ /** Omitted when none. A `json` member may be the string "@ignore": the
185
+ * tag takes no value. */
186
+ mappingKeys?: MappingKeys;
187
+
188
+ /** Ordered chain children; omitted when empty. */
189
+ chain?: TagRef[];
190
+
191
+ /** Computed: overrides this tag receives from each parent context, sorted
192
+ * by (parentType, parent). Omitted when empty. */
193
+ parentOverrides?: ParentOverride[];
194
+ }
195
+
196
+ /** A reference to a group, with optional link overrides. */
197
+ model GroupRef {
198
+ /** A key of `groups`. */
199
+ group: string;
200
+ mappingKeys?: MappingKeys;
201
+ min?: int64;
202
+ max?: int64;
203
+ }
204
+
205
+ model ParentOverride {
206
+ /** "bit" | "group" | "tag" | "variant" (never "cardSet"). */
207
+ parentType: string;
208
+
209
+ /** Name (bit / group), variant path, or — for `tag` — the parent tag's
210
+ * scoped key (`group:person.@partner`). */
211
+ parent: string;
212
+
213
+ /** At least one of the three is present. */
214
+ mappingKeys?: MappingKeys;
215
+ min?: int64;
216
+ max?: int64;
217
+ }
218
+
219
+ model Group {
220
+ description: string;
221
+ tags: TagRef[];
222
+ allTags: string[];
223
+
224
+ /** Computed reverse references; omitted when nothing references the group. */
225
+ usedBy?: UsedBy;
226
+ }
227
+
228
+ /** Reverse references; each member sorted lexicographically and omitted when
229
+ * empty. */
230
+ model UsedBy {
231
+ bits?: string[];
232
+ cardSets?: string[];
233
+ /** `<cardSetKey>.<cardName>.<sideName>.<variantIndex>` (1-based). */
234
+ variants?: string[];
235
+ groups?: string[];
236
+ /** Scoped keys (`group:person.@partner`) of the tags whose chains
237
+ * reference the group. */
238
+ tags?: string[];
239
+ }
240
+
241
+ model CardSet {
242
+ mappingKeys?: MappingKeys;
243
+
244
+ /** Card-set-level references; omitted when empty. */
245
+ tags?: TagRef[];
246
+ allTags?: string[];
247
+
248
+ /** Bits using this card set, sorted; omitted when none. */
249
+ usedBy?: UsedBy;
250
+
251
+ /** Ordered. */
252
+ cards: Card[];
253
+ }
254
+
255
+ model Card {
256
+ name: string;
257
+ isDefault: boolean;
258
+
259
+ /** 0 = no lower bound. */
260
+ min: int64;
261
+
262
+ /** 0 = unbounded (cards use 0, not -1). */
263
+ max: int64;
264
+
265
+ mappingKeys?: MappingKeys;
266
+
267
+ /** Ordered. */
268
+ sides: Side[];
269
+ }
270
+
271
+ model Side {
272
+ name: string;
273
+
274
+ /** -1 = repeat forever; omitted when none. */
275
+ repeatCount?: int64;
276
+
277
+ mappingKeys?: MappingKeys;
278
+
279
+ /** Ordered; a variant's identity is its 1-based index. */
280
+ variants: Variant[];
281
+ }
282
+
283
+ model Variant {
284
+ bodyFormat: string;
285
+ bodyRequired?: true;
286
+ bodyForbidden?: true;
287
+
288
+ /** -1 = infinite; omitted when none. */
289
+ repeatCount?: int64;
290
+
291
+ mappingKeys?: MappingKeys;
292
+ tags: TagRef[];
293
+ allTags: string[];
294
+ }
295
+
296
+ model Resource {
297
+ /** Name of the defining `resource-<type>` group. */
298
+ group: string;
299
+
300
+ /** Component resource types of a composed resource; omitted when empty. */
301
+ composedOf?: string[];
302
+ }
303
+
304
+ /** A search/filter category over bit types. Deliberately no member list:
305
+ * membership is authored per bit (`Bit.bitGroups`). */
306
+ model BitGroup {
307
+ /** English display name; other languages come from `translations.json`. */
308
+ title: string;
309
+ description: string;
310
+
311
+ /** Alternative keys accepted by lookups; omitted when empty. */
312
+ aliases?: string[];
313
+
314
+ /** Informational parent group; never implies membership. */
315
+ subgroupOf?: string;
316
+
317
+ /** Flag boolean: the group is allowed to have no members. */
318
+ allowEmpty?: true;
319
+ }
320
+
321
+ /** A search/filter category over resource types. The member list is the
322
+ * single home of that fact. */
323
+ model ResourceGroup {
324
+ title: string;
325
+ description: string;
326
+ aliases?: string[];
327
+
328
+ /** Member resource types, sorted; each is a key of `resources`. */
329
+ resourceTypes: string[];
330
+ }