@milehimikey/em 1.7.2 → 1.8.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 (112) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +92 -65
  2. package/.claude/skills/event-modeling/reference/conform.md +45 -20
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +120 -55
  4. package/.claude/skills/event-modeling/reference/extract.md +5 -4
  5. package/.claude/skills/event-modeling/reference/implement.md +34 -22
  6. package/.claude/skills/event-modeling/templates/model-readme.md +8 -5
  7. package/.claude/skills/event-modeling/templates/slice.md +19 -5
  8. package/README.md +6 -4
  9. package/dist/catalog/docJoin.d.ts +28 -3
  10. package/dist/catalog/docJoin.js +82 -13
  11. package/dist/catalog/docJoin.js.map +1 -1
  12. package/dist/catalog/docModelConsistencyValidate.d.ts +8 -0
  13. package/dist/catalog/docModelConsistencyValidate.js +335 -0
  14. package/dist/catalog/docModelConsistencyValidate.js.map +1 -0
  15. package/dist/catalog/noteBindingValidate.d.ts +8 -0
  16. package/dist/catalog/noteBindingValidate.js +133 -0
  17. package/dist/catalog/noteBindingValidate.js.map +1 -0
  18. package/dist/catalog/pages.js +3 -3
  19. package/dist/catalog/pages.js.map +1 -1
  20. package/dist/catalog/sliceDoc.d.ts +14 -0
  21. package/dist/catalog/sliceDoc.js +22 -0
  22. package/dist/catalog/sliceDoc.js.map +1 -1
  23. package/dist/catalog/sliceReadyValidate.d.ts +18 -0
  24. package/dist/catalog/sliceReadyValidate.js +25 -1
  25. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  26. package/dist/cli/agentsMd.d.ts +15 -0
  27. package/dist/cli/agentsMd.js +72 -0
  28. package/dist/cli/agentsMd.js.map +1 -0
  29. package/dist/cli/conformScope.d.ts +86 -0
  30. package/dist/cli/conformScope.js +156 -0
  31. package/dist/cli/conformScope.js.map +1 -0
  32. package/dist/cli/contract.d.ts +7 -0
  33. package/dist/cli/contract.js +27 -0
  34. package/dist/cli/contract.js.map +1 -0
  35. package/dist/cli/coverage.d.ts +69 -0
  36. package/dist/cli/coverage.js +157 -0
  37. package/dist/cli/coverage.js.map +1 -0
  38. package/dist/cli/markImplemented.d.ts +34 -0
  39. package/dist/cli/markImplemented.js +180 -0
  40. package/dist/cli/markImplemented.js.map +1 -0
  41. package/dist/cli/migrateReactionShape.d.ts +62 -0
  42. package/dist/cli/migrateReactionShape.js +283 -0
  43. package/dist/cli/migrateReactionShape.js.map +1 -0
  44. package/dist/cli/sliceIndex.d.ts +49 -0
  45. package/dist/cli/sliceIndex.js +115 -0
  46. package/dist/cli/sliceIndex.js.map +1 -0
  47. package/dist/cli/sliceNew.d.ts +18 -0
  48. package/dist/cli/sliceNew.js +53 -0
  49. package/dist/cli/sliceNew.js.map +1 -0
  50. package/dist/cli/stateFile.d.ts +74 -0
  51. package/dist/cli/stateFile.js +191 -0
  52. package/dist/cli/stateFile.js.map +1 -0
  53. package/dist/cli.js +535 -43
  54. package/dist/cli.js.map +1 -1
  55. package/dist/emit/coverageJson.d.ts +8 -0
  56. package/dist/emit/coverageJson.js +30 -0
  57. package/dist/emit/coverageJson.js.map +1 -0
  58. package/dist/emit/dot.js +41 -10
  59. package/dist/emit/dot.js.map +1 -1
  60. package/dist/emit/json.d.ts +72 -4
  61. package/dist/emit/json.js +73 -4
  62. package/dist/emit/json.js.map +1 -1
  63. package/dist/emit/validateJson.d.ts +52 -0
  64. package/dist/emit/validateJson.js +120 -0
  65. package/dist/emit/validateJson.js.map +1 -0
  66. package/dist/mcp/main.d.ts +2 -0
  67. package/dist/mcp/main.js +20 -0
  68. package/dist/mcp/main.js.map +1 -0
  69. package/dist/mcp/server.d.ts +11 -0
  70. package/dist/mcp/server.js +260 -0
  71. package/dist/mcp/server.js.map +1 -0
  72. package/dist/model/model.d.ts +30 -1
  73. package/dist/model/model.js +26 -0
  74. package/dist/model/model.js.map +1 -1
  75. package/dist/model/rules.d.ts +114 -0
  76. package/dist/model/rules.js +116 -4
  77. package/dist/model/rules.js.map +1 -1
  78. package/dist/model/validate.js +83 -7
  79. package/dist/model/validate.js.map +1 -1
  80. package/dist/parser/ast.d.ts +41 -0
  81. package/dist/parser/lexer.d.ts +16 -0
  82. package/dist/parser/lexer.js +44 -2
  83. package/dist/parser/lexer.js.map +1 -1
  84. package/dist/parser/parser.js +340 -20
  85. package/dist/parser/parser.js.map +1 -1
  86. package/dist/render/drawNotes.js +8 -1
  87. package/dist/render/drawNotes.js.map +1 -1
  88. package/dist/render/render.js +85 -17
  89. package/dist/render/render.js.map +1 -1
  90. package/dist/render/serve.d.ts +4 -0
  91. package/dist/render/serve.js +23 -239
  92. package/dist/render/serve.js.map +1 -1
  93. package/dist/render/sliceStatus.d.ts +1 -1
  94. package/dist/render/sliceStatus.js +35 -6
  95. package/dist/render/sliceStatus.js.map +1 -1
  96. package/dist/render/viewerHtml.d.ts +1 -0
  97. package/dist/render/viewerHtml.js +5 -0
  98. package/dist/render/viewerHtml.js.map +1 -0
  99. package/dist/render/watch.d.ts +10 -0
  100. package/dist/render/watch.js +29 -0
  101. package/dist/render/watch.js.map +1 -1
  102. package/dist/templates.d.ts +19 -0
  103. package/dist/templates.js +169 -0
  104. package/dist/templates.js.map +1 -1
  105. package/dist/util/markers.d.ts +13 -0
  106. package/dist/util/markers.js +30 -0
  107. package/dist/util/markers.js.map +1 -0
  108. package/dist/util/walkDir.d.ts +10 -1
  109. package/dist/util/walkDir.js +11 -7
  110. package/dist/util/walkDir.js.map +1 -1
  111. package/package.json +12 -3
  112. package/.claude/skills/event-modeling/templates/live.html +0 -117
@@ -6,6 +6,38 @@ export interface Field {
6
6
  name: string;
7
7
  /** Optional type annotation after a `:`. */
8
8
  type?: string;
9
+ /** Trailing `tag` clause on the field line (event fields only): marks this field as an
10
+ * identity tag — the tag key defaults to the field's own name. Merged with any
11
+ * element-level `tag` clauses (`ElementNode.tags`) at export time via `model/model.ts`'s
12
+ * `collectTags`, rather than normalized into that array here — keeps the field's own
13
+ * `tag` flag visible right next to the field it marks. */
14
+ tag?: boolean;
15
+ /** Trailing `renamed from "Old1", "Old2"` clause on the field line (event/command fields
16
+ * only, MIL-68): the prior name(s) this field was known as, most-recent-first, for
17
+ * payload-conversion codegen at handling time. `undefined` when absent — a declared
18
+ * type's fields can never carry this clause (it can't parse there), so they always export
19
+ * it as `null` (see `emit/json.ts`'s `fieldExport`). Purely export/codegen metadata —
20
+ * `em diff` still reports a rename as remove+add (see `ElementNode.renamedFrom`). */
21
+ renamedFrom?: string[];
22
+ }
23
+ /** Kind of an element-level `tag` clause (`ElementNode.tags`) — the inline field `tag` (identity)
24
+ * isn't one of these; it lives on `Field.tag` instead and is merged in only at export/validate
25
+ * time (see `model/model.ts`'s `collectTags`). */
26
+ export type TagClauseKind = "composite" | "external";
27
+ /** An element-level `tag` clause — `tag <key> from a, b` (composite) or
28
+ * `tag <key> external "text"` (external). Events only (MIL-66). Declared either as a
29
+ * trailing clause on the element (same family as `note`/`issue`, via `extractClauses`) or as
30
+ * one or more standalone `tag ...` lines immediately following the event inside a slice body.
31
+ * Multiple accumulate, in declaration order. */
32
+ export interface TagClause {
33
+ /** The declared tag key (bare identifier, unquoted). */
34
+ key: string;
35
+ kind: TagClauseKind;
36
+ /** composite only: the ≥2 field names forming this tag key, in declaration order. */
37
+ fields?: string[];
38
+ /** external only: free-text documentation of how this tag would be computed — never parsed. */
39
+ description?: string;
40
+ line: number;
9
41
  }
10
42
  export interface ElementNode {
11
43
  kind: ElementKind;
@@ -32,6 +64,15 @@ export interface ElementNode {
32
64
  * surface (an event as a contract, a view as a public read API's response shape), as
33
65
  * opposed to an internal-only fact or read model. */
34
66
  public?: boolean;
67
+ /** Element-level `tag` clauses (composite/external) — events only. `undefined`/absent when
68
+ * none are declared; inline field identity tags live on `Field.tag` instead, not here. */
69
+ tags?: TagClause[];
70
+ /** `renamed from "Old1", "Old2"` on the element's own name — event or command only (MIL-68):
71
+ * the prior name(s) this element was known as, most-recent-first. Codegen/export metadata
72
+ * for payload conversion — `em diff` deliberately does NOT infer a rename from this (stays
73
+ * remove+add, no-inference philosophy); this is purely additive metadata a consumer opts
74
+ * into reading. `undefined` when absent. */
75
+ renamedFrom?: string[];
35
76
  line: number;
36
77
  }
37
78
  export interface SliceNode {
@@ -17,5 +17,21 @@ export declare function stripComment(line: string): string;
17
17
  * `\"`/`\\` escapes inside. Text that merely starts with `"` but whose string
18
18
  * never actually closes at the end of `s` is left untouched, unquoted. */
19
19
  export declare function unquote(s: string): string;
20
+ /** Split a comma-separated list, honouring quotes — a comma inside a quoted span is literal, not
21
+ * a separator — but unlike `splitQuotedList` below, returns each segment's RAW text untrimmed
22
+ * and unquoted, with the same "one entry per separator, including a trailing empty one" contract
23
+ * as native `String.split(sep)` (for input with no quotes at all, output is character-for-
24
+ * character identical to `s.split(sep)`). Used for field specs: `parseInlineFields` needs one
25
+ * comma-separated span per field, but a field-level clause may itself contain a quoted list with
26
+ * embedded commas (MIL-68's `renamed from "Old1", "Old2"`), which a naive `.split(",")` would
27
+ * break apart mid-string.
28
+ *
29
+ * A `"` only opens a span when `isQuoteOpener` says it's grammar-anchored (start of `s`, after
30
+ * a `QUOTE_OPENER_KEYWORDS` keyword, or after a comma) — same MIL-122 discipline as
31
+ * `scanUnquoted` above. Two independently-stray inch marks in a field list
32
+ * (`{ size24: 24", size32: 32" }`) must NOT pair with each other and swallow the comma between
33
+ * them; only a genuinely anchored `"` (e.g. right after `renamed from`, or after a from-list
34
+ * comma) opens a span at all. */
35
+ export declare function splitTopLevel(s: string, sep: string): string[];
20
36
  /** Split a comma-separated list, honouring quotes, returning trimmed unquoted items. */
21
37
  export declare function splitQuotedList(s: string): string[];
@@ -30,8 +30,13 @@ export function matchQuote(s, openIdx) {
30
30
  }
31
31
  /** Keywords after which a `"` is grammar-legitimate as the start of a quoted
32
32
  * string: a declaration's name (`model "Name"`, `slice "Name"`, `type "Name"`)
33
- * or a clause's value (`note`/`issue`/`divergence`/`from`/`source "..."`). */
34
- const QUOTE_OPENER_KEYWORDS = /(?:^|\s)(?:model|slice|type|note|issue|divergence|from|source)$/i;
33
+ * or a clause's value (`note`/`issue`/`divergence`/`from`/`source`/`external "..."`).
34
+ * `external` (MIL-66) is here so a `#`/`{`/`}` inside a `tag X external "..."` description is
35
+ * inert string content, never mistaken for a comment or block boundary (same MIL-122 discipline
36
+ * every other quoted clause value gets). `renamed from "..."` (MIL-68) needs no entry of its
37
+ * own: it incidentally ends in the same `from` this list already carries, so a `#`/`{`/`}`
38
+ * inside a rename item's quotes is already inert here — relied on deliberately, not by luck. */
39
+ const QUOTE_OPENER_KEYWORDS = /(?:^|\s)(?:model|slice|type|note|issue|divergence|from|source|external)$/i;
35
40
  /** True if the `"` at `s[i]` is grammar-legitimate as the START of a quoted
36
41
  * string — the very first character of `s`, immediately after one of
37
42
  * `QUOTE_OPENER_KEYWORDS` (`keyword "value"`), or immediately after a
@@ -125,6 +130,43 @@ export function unquote(s) {
125
130
  }
126
131
  return t;
127
132
  }
133
+ /** Split a comma-separated list, honouring quotes — a comma inside a quoted span is literal, not
134
+ * a separator — but unlike `splitQuotedList` below, returns each segment's RAW text untrimmed
135
+ * and unquoted, with the same "one entry per separator, including a trailing empty one" contract
136
+ * as native `String.split(sep)` (for input with no quotes at all, output is character-for-
137
+ * character identical to `s.split(sep)`). Used for field specs: `parseInlineFields` needs one
138
+ * comma-separated span per field, but a field-level clause may itself contain a quoted list with
139
+ * embedded commas (MIL-68's `renamed from "Old1", "Old2"`), which a naive `.split(",")` would
140
+ * break apart mid-string.
141
+ *
142
+ * A `"` only opens a span when `isQuoteOpener` says it's grammar-anchored (start of `s`, after
143
+ * a `QUOTE_OPENER_KEYWORDS` keyword, or after a comma) — same MIL-122 discipline as
144
+ * `scanUnquoted` above. Two independently-stray inch marks in a field list
145
+ * (`{ size24: 24", size32: 32" }`) must NOT pair with each other and swallow the comma between
146
+ * them; only a genuinely anchored `"` (e.g. right after `renamed from`, or after a from-list
147
+ * comma) opens a span at all. */
148
+ export function splitTopLevel(s, sep) {
149
+ const items = [];
150
+ let cur = "";
151
+ for (let i = 0; i < s.length; i++) {
152
+ const c = s[i];
153
+ if (c === '"' && isQuoteOpener(s, i)) {
154
+ const close = matchQuote(s, i);
155
+ const end = close >= 0 ? close : s.length - 1;
156
+ cur += s.slice(i, end + 1);
157
+ i = end;
158
+ }
159
+ else if (c === sep) {
160
+ items.push(cur);
161
+ cur = "";
162
+ }
163
+ else {
164
+ cur += c;
165
+ }
166
+ }
167
+ items.push(cur);
168
+ return items;
169
+ }
128
170
  /** Split a comma-separated list, honouring quotes, returning trimmed unquoted items. */
129
171
  export function splitQuotedList(s) {
130
172
  const items = [];
@@ -1 +1 @@
1
- {"version":3,"file":"lexer.js","sourceRoot":"","sources":["../../src/parser/lexer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lexer.js","sourceRoot":"","sources":["../../src/parser/lexer.ts"],"names":[],"mappings":"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"}
@@ -16,7 +16,7 @@
16
16
  // arrow <From Element> -> <To Element>
17
17
  // type "Name" { field: Type, ... }
18
18
  import { AUTOMATION_KINDS, } from "./ast.js";
19
- import { decodeQuoted, hasUnterminatedQuote, indexOfUnquoted, lastIndexOfUnquoted, matchQuote, splitQuotedList, stripComment, unquote, } from "./lexer.js";
19
+ import { decodeQuoted, hasUnterminatedQuote, indexOfUnquoted, lastIndexOfUnquoted, matchQuote, splitQuotedList, splitTopLevel, stripComment, unquote, } from "./lexer.js";
20
20
  export class ParseError extends Error {
21
21
  line;
22
22
  constructor(message, line) {
@@ -89,21 +89,54 @@ export function parse(source) {
89
89
  }
90
90
  throw new ParseError("unexpected '}'", lineNo);
91
91
  }
92
- // Inside an open field block: each line is a field declaration.
92
+ // Inside an open field block: each line is a field declaration — except a line that is
93
+ // itself an element-level `tag <key> from …`/`tag <key> external "…` clause, which reads
94
+ // as a standalone `tag` line's canonical position (after the event's closing `}`, MIL-66)
95
+ // and would otherwise be silently swallowed as junk fields (e.g. a bare identifier list
96
+ // parsed as one-field-per-name) with no diagnostic at all.
93
97
  if (currentElement) {
94
- for (const f of parseInlineFields(line))
98
+ if (isTagClauseLineShape(line)) {
99
+ throw new ParseError(currentElement.kind === "event"
100
+ ? "a `tag` clause belongs after the event's closing '}', not inside its field block " +
101
+ "— move this line below the '}'"
102
+ : "`tag` is only valid on event — identity/composite/external tags describe an " +
103
+ `event's DCB tag, not a ${currentElement.kind} field`, lineNo);
104
+ }
105
+ for (const f of parseInlineFields(line, lineNo, currentElement.kind))
95
106
  (currentElement.fields ??= []).push(f);
96
107
  continue;
97
108
  }
98
- // Inside an open `type` block: each line is a field declaration too.
109
+ // Inside an open `type` block: each line is a field declaration too, with the same guard —
110
+ // `tag` is events-only, so a `type` block never has a legal position for it at all.
99
111
  if (currentTypeDecl) {
100
- currentTypeDecl.fields.push(...parseInlineFields(line));
112
+ if (isTagClauseLineShape(line)) {
113
+ throw new ParseError("`tag` is only valid on event — identity/composite/external tags describe an event's " +
114
+ "DCB tag, not a type field", lineNo);
115
+ }
116
+ currentTypeDecl.fields.push(...parseInlineFields(line, lineNo, "type-decl"));
101
117
  continue;
102
118
  }
103
119
  const [keyword, ...rest] = splitFirstWord(line);
104
120
  const remainder = rest.join(" ").trim();
105
- // Inside a slice: only element declarations are allowed.
121
+ // Inside a slice: only element declarations are allowed, plus a standalone `tag ...` line
122
+ // (MIL-66's canonical form), which attaches to the most recently declared element rather
123
+ // than opening one of its own. Checked before the element-keyword gate below since `tag`
124
+ // is not itself an ELEMENT_KEYWORD.
106
125
  if (currentSlice) {
126
+ if (keyword === "tag") {
127
+ const target = currentSlice.elements[currentSlice.elements.length - 1];
128
+ if (!target) {
129
+ throw new ParseError("a standalone `tag` line must follow an event declared earlier in this slice", lineNo);
130
+ }
131
+ if (target.kind !== "event") {
132
+ throw new ParseError(`a standalone \`tag\` line must follow an event — "${target.name}" is a ${target.kind}`, lineNo);
133
+ }
134
+ const leftover = extractTagClauses(target, `tag ${remainder}`, lineNo);
135
+ if (leftover) {
136
+ throw new ParseError(`unrecognized trailing text in 'tag' clause: '${leftover}'`, lineNo);
137
+ }
138
+ continue;
139
+ }
107
140
  if (!ELEMENT_KEYWORDS.has(keyword)) {
108
141
  throw new ParseError(`'${keyword}' is not valid inside a slice (expected ui/command/view/event/automation/processor/saga/translation or '}')`, lineNo);
109
142
  }
@@ -118,7 +151,7 @@ export function parse(source) {
118
151
  const after = remainder.slice(braceAt + 1);
119
152
  const closeAt = indexOfUnquoted(after, "}");
120
153
  const inner = closeAt >= 0 ? after.slice(0, closeAt) : after;
121
- el.fields.push(...parseInlineFields(inner));
154
+ el.fields.push(...parseInlineFields(inner, lineNo, el.kind));
122
155
  if (closeAt < 0) {
123
156
  currentElement = el; // block stays open across lines
124
157
  }
@@ -188,7 +221,7 @@ export function parse(source) {
188
221
  const after = remainder.slice(braceAt + 1);
189
222
  const closeAt = indexOfUnquoted(after, "}");
190
223
  const inner = closeAt >= 0 ? after.slice(0, closeAt) : after;
191
- decl.fields.push(...parseInlineFields(inner));
224
+ decl.fields.push(...parseInlineFields(inner, lineNo, "type-decl"));
192
225
  if (closeAt < 0) {
193
226
  currentTypeDecl = decl; // block stays open across lines
194
227
  }
@@ -226,6 +259,39 @@ function parseElement(kind, raw, line) {
226
259
  throw new ParseError(`${kind} requires a name`, line);
227
260
  return node;
228
261
  }
262
+ /**
263
+ * Pulls a `keyword "A", "B", ...` clause out of `rest`: `keywordPattern` (a regex source
264
+ * fragment, e.g. `renamed\\s+from`) must be immediately followed by an opening quote, then a
265
+ * quoted item, optionally followed by `,` + another quoted item, repeated for as many items as
266
+ * are actually there. Unlike `extractQuotedClause`, this is deliberately NOT greedy to
267
+ * end-of-line: it stops consuming as soon as the text after a closing quote isn't `, "` — so a
268
+ * trailing `@Tag`/`public`/`{ … }` on the same line is left in `rest` for its own clause to
269
+ * find (MIL-68's `renamed from "Old" @Payment { … }` needs the list to end cleanly before
270
+ * `@Payment`). `keywordLabel` is the human-readable name used in the unterminated-string error.
271
+ * Returns `null` if `keywordPattern` isn't immediately followed by a quote (clause absent).
272
+ */
273
+ function extractQuotedListClause(rest, keywordPattern, keywordLabel, line) {
274
+ const kwMatch = rest.match(new RegExp(`(?:^|\\s)${keywordPattern}\\s+(?=")`));
275
+ if (!kwMatch || kwMatch.index === undefined)
276
+ return null;
277
+ let idx = kwMatch.index + kwMatch[0].length;
278
+ const values = [];
279
+ for (;;) {
280
+ const close = matchQuote(rest, idx);
281
+ if (close < 0)
282
+ throw new ParseError(`unterminated string literal in '${keywordLabel}' clause`, line);
283
+ values.push(decodeQuoted(rest.slice(idx + 1, close)));
284
+ idx = close + 1;
285
+ const commaMatch = rest.slice(idx).match(/^\s*,\s*(?=")/);
286
+ if (!commaMatch)
287
+ break;
288
+ idx += commaMatch[0].length;
289
+ }
290
+ return {
291
+ values,
292
+ rest: (rest.slice(0, kwMatch.index) + rest.slice(idx)).trim(),
293
+ };
294
+ }
229
295
  /**
230
296
  * Pulls a `keyword "value"` clause out of `rest`, case-insensitively, honouring
231
297
  * `\"`/`\\` escapes inside the string (decoded in the returned `value`) and a
@@ -288,6 +354,28 @@ function extractClauses(node, raw, line) {
288
354
  node.divergence = divergenceClause.value;
289
355
  rest = divergenceClause.rest;
290
356
  }
357
+ // Element-level `tag` clause(s) (event only, MIL-66): `tag <key> from a, b` (composite) or
358
+ // `tag <key> external "text"` (external). Extracted before `from` below — though the two
359
+ // never actually collide (this `from` is an unquoted bare-identifier list, the view/reaction
360
+ // `from` below requires a quote immediately after — see `extractTagClauses`) — so both belong
361
+ // in this same note/issue/divergence "extract before `from`" cluster. May match more than
362
+ // once: `tag` clauses accumulate, unlike every other clause here.
363
+ rest = extractTagClauses(node, rest, line);
364
+ // `renamed from "Old1", "Old2"` clause (event or command only, MIL-68): the prior name(s)
365
+ // this element was known as, for payload-conversion codegen. Extracted here, before the
366
+ // plain `from` clause below, because `renamed from` itself ends in the word `from` — the
367
+ // general clause's `from\s+(?=")` regex would otherwise match the `from "..."` inside this
368
+ // one's own text. Non-greedy (see `extractQuotedListClause`), so a trailing `@Context`/
369
+ // `public`/`{ … }` on the same header line is untouched. `em diff` deliberately does not
370
+ // read this — it stays remove+add, no-inference (see docs/dsl.md).
371
+ const renamedFromClause = extractQuotedListClause(rest, "renamed\\s+from", "renamed from", line);
372
+ if (renamedFromClause) {
373
+ if (node.kind !== "event" && node.kind !== "command")
374
+ throw new ParseError("`renamed from` is only valid on event or command — only elements with a wire/API " +
375
+ "identity can be renamed", line);
376
+ node.renamedFrom = renamedFromClause.values;
377
+ rest = renamedFromClause.rest;
378
+ }
291
379
  // `from "A", "B"` clause (views and reactions). The keyword is case-sensitive
292
380
  // and its operand must open with a quote, so a capitalized `From` — or a bare
293
381
  // lowercase `from` — inside an element name (`event Widget Removed From
@@ -355,27 +443,259 @@ function extractClauses(node, raw, line) {
355
443
  }
356
444
  return rest;
357
445
  }
358
- /** Parse one field spec: `name` or `name: Type`. Returns null for blanks. */
359
- function parseFieldSpec(raw) {
446
+ /** Bare-identifier grammar shared by every element-level `tag` clause: the composite/external
447
+ * tag key itself, and each field name in a composite tag's field list. */
448
+ const TAG_IDENT = "[A-Za-z_][A-Za-z0-9_]*";
449
+ /** Matches an element-level composite tag clause, `tag <key> from a, b[, c …]`, anywhere in a
450
+ * clause-text string — group 1 is the key, group 2 the raw (still comma-joined) field list.
451
+ * The key is constrained to `TAG_IDENT`, not `\S+`: a quoted or punctuated key (`tag "x" from
452
+ * a, b`, `tag pro-duct from a, b`) must never silently match with the junk folded into the
453
+ * exported key — see `TAG_CLAUSE_LEFTOVER_RE` below. Shared between `extractTagClauses` (which
454
+ * matches anywhere via the `(?:^|\s)` alternation) and `isTagClauseLineShape` (which requires
455
+ * the match to start at index 0) so both recognize the exact same clause shape. */
456
+ const TAG_COMPOSITE_RE = new RegExp(`(?:^|\\s)tag\\s+(${TAG_IDENT})\\s+from\\s+(${TAG_IDENT}(?:\\s*,\\s*${TAG_IDENT})*)`);
457
+ /** Matches an element-level external tag clause, `tag <key> external "…`, anywhere in a
458
+ * clause-text string, up to (not including) the opening quote — same key constraint and
459
+ * sharing rationale as `TAG_COMPOSITE_RE`. */
460
+ const TAG_EXTERNAL_RE = new RegExp(`(?:^|\\s)tag\\s+(${TAG_IDENT})\\s+external\\s+(?=")`);
461
+ /** Catches a malformed tag key — quoted, punctuated, or otherwise not a bare identifier — that
462
+ * `TAG_COMPOSITE_RE`/`TAG_EXTERNAL_RE` deliberately never match, so it would otherwise sit
463
+ * unmentioned in a clause's leftover text (silently folding into the element's free-text name,
464
+ * or surfacing only as a confusing downstream "unrecognized trailing text"). Checked once,
465
+ * after `extractTagClauses`'s extraction loop has taken everything a well-formed clause could
466
+ * give it: anything still shaped like `tag <junk> from …`/`tag <junk> external …` at that point
467
+ * can only be a malformed key. */
468
+ const TAG_CLAUSE_LEFTOVER_RE = /(?:^|\s)tag\s+\S+\s+(?:from\b|external\b)/;
469
+ /** True when `line` — already trimmed, the full text of one field-block line — OPENS with an
470
+ * element-level `tag <key> from …`/`tag <key> external "…` clause shape. Used to reject a
471
+ * standalone element-level tag line written INSIDE an open element/type field block (MIL-66's
472
+ * canonical standalone form only follows the event's closing `}`, at the slice level, or
473
+ * trails an inline block on the same line) instead of silently swallowing it as junk fields.
474
+ * Anchored to the START of `line` — unlike `extractTagClauses`'s `(?:^|\s)` match-anywhere,
475
+ * which is right for scanning trailing clause text — so a field merely NAMED `tag` (bare
476
+ * `tag`, `tag: UUID`) or a field like `tagline: string` never trips this: neither has the
477
+ * clause shape starting at position 0. */
478
+ function isTagClauseLineShape(line) {
479
+ const c = line.match(TAG_COMPOSITE_RE);
480
+ if (c && c.index === 0)
481
+ return true;
482
+ const e = line.match(TAG_EXTERNAL_RE);
483
+ return !!e && e.index === 0;
484
+ }
485
+ /**
486
+ * Pulls every element-level `tag <key> from a, b` (composite) or `tag <key> external "text"`
487
+ * (external) clause out of `raw`, in a loop — `tag` clauses accumulate, unlike every other
488
+ * clause `extractClauses` handles. Applies each to `node.tags` and returns whatever text is
489
+ * left. Events only (event or nothing — same posture as the `public` kind check above); a
490
+ * non-event `node` throws as soon as a `tag` clause is actually found, so an element that
491
+ * merely has "tag" nowhere in its trailing text pays no penalty.
492
+ *
493
+ * Shared by `extractClauses` (the trailing-clause position, on the header line, after an
494
+ * inline `{ … }` block, or on a multi-line block's closing `}` line) and the standalone
495
+ * `tag ...` line handler in `parse()` (which prepends the literal `"tag "` back on before
496
+ * calling this, so both entry points share one matcher/error-message implementation).
497
+ *
498
+ * The composite field list is a bounded bare-identifier grammar (`a`, `a, b`, `a, b, c`, …),
499
+ * not "everything to end of line" the way the quoted `from`/`note`/etc. clauses are — so a
500
+ * `tag` clause never has to be the line's last token; a trailing `public`/`@Context`/etc.
501
+ * simply isn't valid identifier-list syntax and is left for those clauses to find afterward.
502
+ */
503
+ function extractTagClauses(node, raw, line) {
504
+ let rest = raw;
505
+ for (;;) {
506
+ const cMatch = rest.match(TAG_COMPOSITE_RE);
507
+ const eMatch = rest.match(TAG_EXTERNAL_RE);
508
+ let which = null;
509
+ if (cMatch && eMatch)
510
+ which = (cMatch.index ?? 0) <= (eMatch.index ?? 0) ? "composite" : "external";
511
+ else if (cMatch)
512
+ which = "composite";
513
+ else if (eMatch)
514
+ which = "external";
515
+ if (!which)
516
+ break;
517
+ if (node.kind !== "event") {
518
+ throw new ParseError("`tag` is only valid on event — identity/composite/external tags describe an event's " +
519
+ "DCB tag, not a command/view/ui", line);
520
+ }
521
+ if (which === "composite") {
522
+ const m = cMatch;
523
+ const key = m[1];
524
+ const fields = m[2].split(",").map((s) => s.trim());
525
+ if (fields.length < 2)
526
+ throw new ParseError(`tag "${key}": a composite tag needs at least 2 fields (got ${fields.length})`, line);
527
+ (node.tags ??= []).push({ key, kind: "composite", fields, line });
528
+ rest = (rest.slice(0, m.index) + rest.slice((m.index ?? 0) + m[0].length)).trim();
529
+ }
530
+ else {
531
+ const m = eMatch;
532
+ const key = m[1];
533
+ const openIdx = (m.index ?? 0) + m[0].length;
534
+ const closeIdx = matchQuote(rest, openIdx);
535
+ if (closeIdx < 0)
536
+ throw new ParseError(`unterminated string literal in tag "${key}" external clause`, line);
537
+ const description = decodeQuoted(rest.slice(openIdx + 1, closeIdx));
538
+ (node.tags ??= []).push({ key, kind: "external", description, line });
539
+ rest = (rest.slice(0, m.index) + rest.slice(closeIdx + 1)).trim();
540
+ }
541
+ }
542
+ if (TAG_CLAUSE_LEFTOVER_RE.test(rest)) {
543
+ throw new ParseError("a `tag` clause's key must be a bare identifier (letters, digits, underscore) — " +
544
+ "quoted or punctuated keys aren't supported", line);
545
+ }
546
+ return rest;
547
+ }
548
+ /**
549
+ * Pulls recognized trailing clauses off one field spec's raw text (a single comma-separated
550
+ * entry from a `{ … }` block's inner text, BEFORE the `name`/`name: Type` split — a clause
551
+ * trails the type, or the bare name for a typeless field). Returns the leftover text (still to
552
+ * be split on `:`) plus whatever clauses were found.
553
+ *
554
+ * The reusable field-level counterpart to `extractClauses` (element-level) — kept a separate
555
+ * step from the name/type split below so a second field clause slots in as a new case here
556
+ * instead of a restructure. `context` is the enclosing block's element kind (or `"type-decl"`);
557
+ * a clause legal only on certain kinds throws a ParseError naming the actual context, mirroring
558
+ * `extractClauses`'s per-clause kind checks.
559
+ *
560
+ * Anchored as a genuinely TRAILING word requiring non-blank content before it (`/^(.*\S)\s+tag$/`)
561
+ * so a field literally NAMED `tag` (bare `tag`, or `tag: UUID`) is a field declaration, never a
562
+ * clause — the text "tag" alone can't satisfy "something, then whitespace, then tag" against
563
+ * itself. Same discipline `extractClauses` uses for `public`/`again` against a same-named
564
+ * element.
565
+ *
566
+ * Runs as a fixpoint loop over both clauses so `tag` and `renamed from` may trail a field in
567
+ * EITHER order (`paymentId: UUID renamed from "id" tag` and `paymentId: UUID tag renamed from
568
+ * "id"` both work, MIL-68): each pass tries whichever clause hasn't been found yet, and stops
569
+ * once a pass finds nothing new. At most two passes ever do real work (one clause can only
570
+ * unblock the other once), so this never loops meaningfully longer than a fixed two-clause
571
+ * chain would.
572
+ */
573
+ function extractFieldClauses(raw, line, context) {
574
+ const clauses = {};
575
+ let rest = raw;
576
+ for (;;) {
577
+ let progressed = false;
578
+ if (clauses.tag === undefined) {
579
+ const tagMatch = rest.match(/^(.*\S)\s+tag$/);
580
+ if (tagMatch) {
581
+ if (context !== "event") {
582
+ throw new ParseError("`tag` is only valid on an event field — identity/composite/external tags describe " +
583
+ `an event's DCB tag, not a ${context === "type-decl" ? "type" : context} field`, line);
584
+ }
585
+ clauses.tag = true;
586
+ rest = tagMatch[1];
587
+ progressed = true;
588
+ }
589
+ }
590
+ if (clauses.renamedFrom === undefined) {
591
+ const renamedFromClause = extractQuotedListClause(rest, "renamed\\s+from", "renamed from", line);
592
+ if (renamedFromClause) {
593
+ if (context !== "event" && context !== "command") {
594
+ throw new ParseError("`renamed from` is only valid on an event or command field — only elements with a " +
595
+ `wire/API identity can be renamed, not a ${context === "type-decl" ? "type" : context} field`, line);
596
+ }
597
+ clauses.renamedFrom = renamedFromClause.values;
598
+ rest = renamedFromClause.rest;
599
+ progressed = true;
600
+ }
601
+ }
602
+ if (!progressed)
603
+ break;
604
+ }
605
+ return { rest, clauses };
606
+ }
607
+ /** Parse one field spec: `name` or `name: Type`, plus any trailing field-level clauses
608
+ * (`extractFieldClauses`). Returns null for blanks. */
609
+ function parseFieldSpec(raw, line, context) {
360
610
  const s = raw.trim();
361
611
  if (!s)
362
612
  return null;
363
- const colon = s.indexOf(":");
613
+ const { rest, clauses } = extractFieldClauses(s, line, context);
614
+ const spec = rest.trim();
615
+ if (!spec)
616
+ return null;
617
+ const colon = spec.indexOf(":");
618
+ let field;
364
619
  if (colon >= 0) {
365
- const name = unquote(s.slice(0, colon).trim());
366
- const type = unquote(s.slice(colon + 1).trim());
367
- return name ? { name, ...(type ? { type } : {}) } : null;
620
+ const name = unquote(spec.slice(0, colon).trim());
621
+ const type = unquote(spec.slice(colon + 1).trim());
622
+ field = name ? { name, ...(type ? { type } : {}) } : null;
623
+ }
624
+ else {
625
+ const name = unquote(spec);
626
+ field = name ? { name } : null;
627
+ }
628
+ if (field && clauses.tag)
629
+ field.tag = true;
630
+ if (field && clauses.renamedFrom)
631
+ field.renamedFrom = clauses.renamedFrom;
632
+ return field;
633
+ }
634
+ /** Matches a fragment ending in an in-progress `renamed from` list: `renamed from` followed by
635
+ * one or more quoted items, comma-separated, the LAST one flush against the fragment's end —
636
+ * exactly the shape `splitTopLevel` leaves on the fragment that owns the list once it's split
637
+ * the top-level commas BETWEEN list items apart (see `mergeRenamedFromContinuations`). */
638
+ const RENAMED_FROM_TAIL = /(?:^|\s)renamed\s+from\s+"(?:[^"\\]|\\.)*"(?:\s*,\s*"(?:[^"\\]|\\.)*")*$/;
639
+ /** Matches a fragment that, trimmed, STARTS with a quoted string — capturing that leading
640
+ * quoted span so the caller can inspect what (if anything) follows it (see
641
+ * `mergeRenamedFromContinuations`). */
642
+ const LEADING_QUOTED_STRING = /^"(?:[^"\\]|\\.)*"/;
643
+ /**
644
+ * Re-merges `splitTopLevel`'s fragments across a field-level `renamed from "Old1", "Old2"`
645
+ * list's own internal commas (MIL-68, hazard 4). `splitTopLevel` only treats a comma as
646
+ * literal WHILE INSIDE a single quoted span — the commas BETWEEN two list items are
647
+ * themselves top-level separators, so `{ a: X renamed from "A", "B", b: Y }` splits into
648
+ * THREE raw fragments (`a: X renamed from "A"`, `"B"`, `b: Y`), not two fields. Worse, a
649
+ * continuation fragment need not be JUST the quoted string — `{ paymentId: UUID renamed from
650
+ * "id", "pid" tag }` splits into `paymentId: UUID renamed from "id"` and `"pid" tag`, and that
651
+ * second fragment must fold in WHOLE (quote and trailing ` tag` both) or the ` tag` survives as
652
+ * a fabricated field of its own — a phantom identity tag flowing into the export with no
653
+ * diagnostic.
654
+ *
655
+ * A fragment whose trimmed text STARTS with a quoted string is folded back into the PREVIOUS
656
+ * fragment in full (`prev + ", " + frag`) whenever both (a) that previous fragment's own
657
+ * trailing text is itself an in-progress `renamed from` list, and (b) the text right after the
658
+ * fragment's leading quote is not a `:` (allowing leading whitespace) — otherwise it's left
659
+ * alone. Condition (b) preserves the documented escape hatch: a quoted field name immediately
660
+ * followed by `: Type` (`{ a: X renamed from "A", "B": Type }`) is an ordinary field with a
661
+ * quoted name, not a list continuation, unchanged from pre-MIL-68 behavior. Anything else
662
+ * trailing the quote (a bare quoted field, or trailing clause text like ` tag`) is swept into
663
+ * the list fragment and left for `extractFieldClauses`'s fixpoint loop to sort out — this makes
664
+ * the ambiguous case always resolve to "continuation of the rename list", never "next field
665
+ * (or fields)"; see docs/dsl.md for the two escape hatches (multi-line field blocks, or giving
666
+ * the quoted-name field a type).
667
+ */
668
+ function mergeRenamedFromContinuations(fragments) {
669
+ const merged = [];
670
+ for (const raw of fragments) {
671
+ const trimmed = raw.trim();
672
+ const prev = merged.length > 0 ? merged[merged.length - 1] : undefined;
673
+ const leadingQuote = trimmed.match(LEADING_QUOTED_STRING);
674
+ const afterQuote = leadingQuote ? trimmed.slice(leadingQuote[0].length).trimStart() : undefined;
675
+ if (prev !== undefined &&
676
+ leadingQuote !== null &&
677
+ !afterQuote.startsWith(":") &&
678
+ RENAMED_FROM_TAIL.test(prev)) {
679
+ merged[merged.length - 1] = `${prev}, ${trimmed}`;
680
+ }
681
+ else {
682
+ merged.push(trimmed);
683
+ }
368
684
  }
369
- const name = unquote(s);
370
- return name ? { name } : null;
685
+ return merged;
371
686
  }
372
687
  /** Parse comma-separated field specs from the inner text of a `{ … }` block (or a single
373
688
  * line inside an already-open one). Shared by element field blocks and `type` blocks —
374
- * every field-bearing `{ … }` block in the grammar splits and parses fields the same way. */
375
- function parseInlineFields(inner) {
689
+ * every field-bearing `{ … }` block in the grammar splits and parses fields the same way.
690
+ * The split is quote-aware (`splitTopLevel`, identical to native `.split(",")` when `inner`
691
+ * has no quotes at all) so a field clause carrying a quoted, comma-bearing list (MIL-68's
692
+ * `renamed from "Old1", "Old2"`) isn't broken apart mid-string; `mergeRenamedFromContinuations`
693
+ * then re-joins the fragments the split still leaves apart BETWEEN list items. */
694
+ function parseInlineFields(inner, line, context) {
376
695
  const fields = [];
377
- for (const spec of inner.split(",")) {
378
- const f = parseFieldSpec(spec);
696
+ const fragments = mergeRenamedFromContinuations(splitTopLevel(inner, ","));
697
+ for (const spec of fragments) {
698
+ const f = parseFieldSpec(spec, line, context);
379
699
  if (f)
380
700
  fields.push(f);
381
701
  }