@diagrammo/dgmo 0.34.0 → 0.36.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 (55) hide show
  1. package/.claude/commands/dgmo.md +41 -43
  2. package/.github/copilot-instructions.md +1 -1
  3. package/README.md +7 -1
  4. package/SKILL.md +12 -7
  5. package/dist/advanced.cjs +460 -463
  6. package/dist/advanced.d.cts +2 -51
  7. package/dist/advanced.d.ts +2 -51
  8. package/dist/advanced.js +460 -464
  9. package/dist/auto.cjs +461 -140
  10. package/dist/auto.js +91 -91
  11. package/dist/auto.mjs +461 -140
  12. package/dist/cli.cjs +143 -326
  13. package/dist/index.cjs +460 -139
  14. package/dist/index.js +460 -139
  15. package/docs/ai-integration.md +47 -25
  16. package/docs/language-reference.md +2 -2
  17. package/package.json +2 -2
  18. package/src/advanced.ts +0 -12
  19. package/src/c4/parser.ts +15 -6
  20. package/src/chart.ts +1 -1
  21. package/src/class/parser.ts +6 -2
  22. package/src/cli-banner.ts +2 -3
  23. package/src/cli.ts +539 -1223
  24. package/src/cycle/parser.ts +4 -1
  25. package/src/dgmo-router.ts +16 -2
  26. package/src/diagnostics.ts +0 -0
  27. package/src/echarts.ts +5 -1
  28. package/src/er/parser.ts +5 -1
  29. package/src/gantt/parser.ts +13 -4
  30. package/src/graph/flowchart-parser.ts +5 -1
  31. package/src/graph/state-parser.ts +5 -1
  32. package/src/infra/parser.ts +10 -6
  33. package/src/journey-map/parser.ts +5 -2
  34. package/src/kanban/parser.ts +6 -2
  35. package/src/map/context-labels.ts +29 -19
  36. package/src/map/layout.ts +403 -108
  37. package/src/map/legend-band.ts +28 -0
  38. package/src/map/parser.ts +13 -4
  39. package/src/map/renderer.ts +74 -23
  40. package/src/map/resolver.ts +2 -2
  41. package/src/org/parser.ts +8 -2
  42. package/src/pyramid/parser.ts +4 -1
  43. package/src/raci/parser.ts +4 -1
  44. package/src/ring/parser.ts +4 -1
  45. package/src/sequence/parser.ts +6 -3
  46. package/src/sitemap/parser.ts +12 -3
  47. package/src/tech-radar/parser.ts +6 -3
  48. package/src/utils/parsing.ts +2 -2
  49. package/src/utils/tag-groups.ts +3 -3
  50. package/src/visualizations/parse.ts +9 -3
  51. package/src/migrate/data-comma.ts +0 -117
  52. package/src/migrate/embedded.ts +0 -238
  53. package/src/migrate/index.ts +0 -295
  54. package/src/migrate/line-classifier.ts +0 -128
  55. package/src/migrate/line-transformer.ts +0 -218
@@ -1,238 +0,0 @@
1
- // Markdown / mdx fenced-block handler for `dgmo migrate --embedded`.
2
- //
3
- // Walks ```dgmo (and ```diagrammo) fenced blocks inside `.md` / `.mdx`
4
- // files, migrates each block, and re-assembles the document. Atomic
5
- // per file: if any block fails to parse under the legacy parser, the
6
- // entire file is skipped — no writes, no partial migrations, no `.bak`.
7
- //
8
- // Why atomic? Half-new-half-legacy markdown files are unreachable —
9
- // a re-run on a partially-migrated file would produce inconsistent
10
- // behavior and a corrupted `.bak`. Per spec "Migration Tool Contract".
11
-
12
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
13
-
14
- import { parseDgmo } from '../dgmo-router';
15
- import { METADATA_DIAGNOSTIC_CODES } from '../diagnostics';
16
- import { migrateContent } from './index';
17
-
18
- // Diagnostic codes the migration tool itself is repairing — these
19
- // MUST NOT count as "block fails to parse" reasons, otherwise every
20
- // legacy block would be skipped. Derived from the canonical registry
21
- // so a code rename can't silently desync the migrator.
22
- const MIGRATION_TARGET_CODES = new Set<string>([
23
- METADATA_DIAGNOSTIC_CODES.PIPE_OPERATOR_REMOVED,
24
- METADATA_DIAGNOSTIC_CODES.GANTT_BARE_PERCENT_REMOVED,
25
- METADATA_DIAGNOSTIC_CODES.JOURNEY_BARE_SCORE_REMOVED,
26
- METADATA_DIAGNOSTIC_CODES.PYRAMID_BARE_DESCRIPTION_REMOVED,
27
- METADATA_DIAGNOSTIC_CODES.RING_BARE_DESCRIPTION_REMOVED,
28
- ]);
29
-
30
- const FENCE_RE = /^(\s*)(```+|~~~+)\s*(dgmo|diagrammo)\b([^\n]*)$/i;
31
-
32
- interface FencedBlock {
33
- /** Index in the line array where the opening fence sits. */
34
- readonly fenceStartLine: number;
35
- /** Index of the closing fence line. */
36
- readonly fenceEndLine: number;
37
- /** The fence sequence (e.g. ` ``` ` or ` ~~~ `), used to find the close. */
38
- readonly fenceSeq: string;
39
- /** 0-based indices [start, end) of the body lines (between fences, exclusive). */
40
- readonly bodyStart: number;
41
- readonly bodyEnd: number;
42
- /** Joined body source. */
43
- readonly body: string;
44
- }
45
-
46
- /**
47
- * Locate every fenced ```dgmo / ```diagrammo block in `source`.
48
- * Returns empty array when no blocks exist (caller treats as no-op).
49
- */
50
- function findFencedBlocks(source: string): FencedBlock[] {
51
- const lines = source.split('\n');
52
- const blocks: FencedBlock[] = [];
53
-
54
- let i = 0;
55
- while (i < lines.length) {
56
- const open = lines[i]!.match(FENCE_RE);
57
- if (!open) {
58
- i++;
59
- continue;
60
- }
61
- const fenceSeq = open[2]!;
62
- const indent = open[1] ?? '';
63
- // Find matching closer — a fence line whose sequence is `fenceSeq`
64
- // (or longer of the same char) at the same or lower indent.
65
- const closeRe = new RegExp(
66
- `^${escapeRegex(indent)}${escapeRegex(fenceSeq[0]!)}{${fenceSeq.length},}\\s*$`
67
- );
68
- let j = i + 1;
69
- while (j < lines.length) {
70
- if (closeRe.test(lines[j]!)) break;
71
- j++;
72
- }
73
- if (j >= lines.length) {
74
- // Unclosed fence — bail; don't treat as a block.
75
- break;
76
- }
77
- const body = lines.slice(i + 1, j).join('\n');
78
- blocks.push({
79
- fenceStartLine: i,
80
- fenceEndLine: j,
81
- fenceSeq,
82
- bodyStart: i + 1,
83
- bodyEnd: j,
84
- body,
85
- });
86
- i = j + 1;
87
- }
88
-
89
- return blocks;
90
- }
91
-
92
- function escapeRegex(s: string): string {
93
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
94
- }
95
-
96
- export interface EmbeddedMigrationResult {
97
- readonly path: string;
98
- readonly original: string;
99
- readonly migrated: string;
100
- readonly changed: boolean;
101
- /** True when the file was skipped due to a parse-error block. */
102
- readonly skipped: boolean;
103
- /** Reason for skip — `null` when not skipped. */
104
- readonly skipReason: string | null;
105
- /** Number of dgmo blocks found in the file. */
106
- readonly blockCount: number;
107
- /** Number of blocks that actually changed. */
108
- readonly changedBlocks: number;
109
- readonly backupWritten: boolean;
110
- readonly written: boolean;
111
- }
112
-
113
- export interface MigrateEmbeddedOptions {
114
- /** When false, write changes to disk. Default: true (dry-run). */
115
- readonly dryRun?: boolean;
116
- /** When true and dryRun is false, skip writing the `.bak` sidecar. */
117
- readonly noBackup?: boolean;
118
- }
119
-
120
- /**
121
- * Migrate every ```dgmo block in an `.md` / `.mdx` file. All-or-nothing
122
- * per file — a single parse-error block aborts the whole file.
123
- */
124
- export function migrateEmbedded(
125
- path: string,
126
- opts: MigrateEmbeddedOptions = {}
127
- ): EmbeddedMigrationResult {
128
- const dryRun = opts.dryRun ?? true;
129
- const original = readFileSync(path, 'utf-8');
130
- const blocks = findFencedBlocks(original);
131
-
132
- if (blocks.length === 0) {
133
- return {
134
- path,
135
- original,
136
- migrated: original,
137
- changed: false,
138
- skipped: false,
139
- skipReason: null,
140
- blockCount: 0,
141
- changedBlocks: 0,
142
- backupWritten: false,
143
- written: false,
144
- };
145
- }
146
-
147
- // Pre-flight: every block must parse cleanly under the legacy
148
- // parser modulo the migration-target diagnostics (those are what
149
- // we're rewriting). Any other severity:error or a null chart-type
150
- // detection aborts the whole file (atomicity contract).
151
- for (const block of blocks) {
152
- const parsed = parseDgmo(block.body);
153
- if (parsed.chartType === null) {
154
- return {
155
- path,
156
- original,
157
- migrated: original,
158
- changed: false,
159
- skipped: true,
160
- skipReason: `block at line ${block.fenceStartLine + 1} has no recognized chart type`,
161
- blockCount: blocks.length,
162
- changedBlocks: 0,
163
- backupWritten: false,
164
- written: false,
165
- };
166
- }
167
- const blockingErrors = parsed.diagnostics.filter(
168
- (d) =>
169
- d.severity === 'error' &&
170
- (d.code === undefined || !MIGRATION_TARGET_CODES.has(d.code))
171
- );
172
- if (blockingErrors.length > 0) {
173
- return {
174
- path,
175
- original,
176
- migrated: original,
177
- changed: false,
178
- skipped: true,
179
- skipReason: `block at line ${block.fenceStartLine + 1} failed to parse: ${blockingErrors[0]!.message}`,
180
- blockCount: blocks.length,
181
- changedBlocks: 0,
182
- backupWritten: false,
183
- written: false,
184
- };
185
- }
186
- }
187
-
188
- // Migrate each block and rebuild the document.
189
- const lines = original.split('\n');
190
- const out: string[] = [];
191
- let cursor = 0;
192
- let changedBlocks = 0;
193
- let anyChange = false;
194
-
195
- for (const block of blocks) {
196
- for (let k = cursor; k <= block.bodyStart - 1; k++) {
197
- out.push(lines[k]!);
198
- }
199
- const blockResult = migrateContent(block.body);
200
- out.push(blockResult.migrated);
201
- if (blockResult.changed) {
202
- anyChange = true;
203
- changedBlocks++;
204
- }
205
- cursor = block.bodyEnd;
206
- }
207
- for (let k = cursor; k < lines.length; k++) {
208
- out.push(lines[k]!);
209
- }
210
- const migrated = out.join('\n');
211
-
212
- let backupWritten = false;
213
- let written = false;
214
- if (anyChange && !dryRun) {
215
- if (!opts.noBackup) {
216
- const bakPath = `${path}.bak`;
217
- if (!existsSync(bakPath)) {
218
- writeFileSync(bakPath, original, 'utf-8');
219
- backupWritten = true;
220
- }
221
- }
222
- writeFileSync(path, migrated, 'utf-8');
223
- written = true;
224
- }
225
-
226
- return {
227
- path,
228
- original,
229
- migrated,
230
- changed: anyChange,
231
- skipped: false,
232
- skipReason: null,
233
- blockCount: blocks.length,
234
- changedBlocks,
235
- backupWritten,
236
- written,
237
- };
238
- }
@@ -1,295 +0,0 @@
1
- // Orchestration for the `dgmo migrate` tool.
2
- //
3
- // `migrateContent(source)` is the chart-type-aware whole-document
4
- // transform: detects chart type from the first non-empty line, walks
5
- // lines through the classifier + transformer, returns the new source
6
- // plus a per-line change report. Pure — no I/O.
7
- //
8
- // `migrateFile(path, opts)` is the on-disk wrapper: reads, calls
9
- // `migrateContent`, optionally writes back with `.bak` semantics.
10
- // `migrateEmbedded(path, opts)` handles markdown / mdx files with
11
- // fenced ```dgmo blocks (atomic per file — see `embedded.ts`).
12
-
13
- import {
14
- readFileSync,
15
- writeFileSync,
16
- existsSync,
17
- statSync,
18
- readdirSync,
19
- } from 'node:fs';
20
- import { join, extname } from 'node:path';
21
-
22
- import { parseDgmoChartType } from '../dgmo-router';
23
- import { isLegacyMetadataLine } from './line-classifier';
24
- import { transformLine } from './line-transformer';
25
- import { DATA_COMMA_CHART_TYPES, migrateDataRowCommaLine } from './data-comma';
26
-
27
- export interface ContentMigration {
28
- /** Updated source — equal to the input when `changed: false`. */
29
- readonly migrated: string;
30
- /** True iff at least one line changed. */
31
- readonly changed: boolean;
32
- /** Detected chart type (from line 1), or `null` if unrecognized. */
33
- readonly chartType: string | null;
34
- /** 1-based line numbers that were rewritten. */
35
- readonly changedLines: readonly number[];
36
- }
37
-
38
- /**
39
- * Whole-document migration. Pure transform: takes legacy source,
40
- * returns new-grammar source plus a change report. Idempotent —
41
- * running on already-migrated content produces zero changes.
42
- */
43
- export function migrateContent(source: string): ContentMigration {
44
- const chartType = parseDgmoChartType(source);
45
- const lines = source.split('\n');
46
- const out: string[] = [];
47
- const changedLines: number[] = [];
48
- let changed = false;
49
-
50
- const migrateDataCommas =
51
- chartType !== null && DATA_COMMA_CHART_TYPES.has(chartType);
52
-
53
- let firstContentSeen = false;
54
- for (let i = 0; i < lines.length; i++) {
55
- let line = lines[i]!;
56
- const trimmed = line.trim();
57
-
58
- // Data-row comma migration (1.0): rewrite value-separator + thousands
59
- // commas to space-separated, byte-preserving everything else. Skip the
60
- // chart-type declaration line and blank/comment lines.
61
- if (
62
- migrateDataCommas &&
63
- firstContentSeen &&
64
- trimmed &&
65
- !trimmed.startsWith('//')
66
- ) {
67
- const dc = migrateDataRowCommaLine(line);
68
- if (dc.changed) {
69
- line = dc.line;
70
- changed = true;
71
- changedLines.push(i + 1);
72
- }
73
- }
74
- if (trimmed && !trimmed.startsWith('//')) firstContentSeen = true;
75
-
76
- if (!isLegacyMetadataLine(line)) {
77
- out.push(line);
78
- continue;
79
- }
80
- const result = transformLine(line, chartType);
81
- if (result.changed) {
82
- changed = true;
83
- if (!changedLines.includes(i + 1)) changedLines.push(i + 1);
84
- }
85
- out.push(result.line);
86
- }
87
-
88
- return {
89
- migrated: out.join('\n'),
90
- changed,
91
- chartType,
92
- changedLines,
93
- };
94
- }
95
-
96
- // ============================================================
97
- // File-level operations
98
- // ============================================================
99
-
100
- export interface MigrateFileOptions {
101
- /** When false, write changes to disk. Default: true (dry-run). */
102
- readonly dryRun?: boolean;
103
- /** When true and dryRun is false, skip writing the `.bak` sidecar. */
104
- readonly noBackup?: boolean;
105
- }
106
-
107
- export interface FileMigrationResult {
108
- readonly path: string;
109
- /** Original content. */
110
- readonly original: string;
111
- /** Migrated content (equal to original when `changed: false`). */
112
- readonly migrated: string;
113
- /** True iff content differs. */
114
- readonly changed: boolean;
115
- /** Detected chart type. */
116
- readonly chartType: string | null;
117
- /** 1-based line numbers changed. */
118
- readonly changedLines: readonly number[];
119
- /** True iff a `.bak` file was written (only when applying). */
120
- readonly backupWritten: boolean;
121
- /** True iff the migrated content was written to `path`. */
122
- readonly written: boolean;
123
- }
124
-
125
- /**
126
- * Migrate a single `.dgmo` file. Defaults to dry-run — no writes
127
- * happen unless `dryRun: false` is passed explicitly.
128
- *
129
- * On apply: writes the migrated content to `path`. Also writes the
130
- * original to `<path>.bak` unless `noBackup` is set, and unless a
131
- * `.bak` already exists (preserves the original `.bak` across re-runs
132
- * — re-migrating an already-migrated file is a no-op anyway).
133
- */
134
- export function migrateFile(
135
- path: string,
136
- opts: MigrateFileOptions = {}
137
- ): FileMigrationResult {
138
- const dryRun = opts.dryRun ?? true;
139
- const original = readFileSync(path, 'utf-8');
140
- const { migrated, changed, chartType, changedLines } =
141
- migrateContent(original);
142
-
143
- let backupWritten = false;
144
- let written = false;
145
-
146
- if (changed && !dryRun) {
147
- if (!opts.noBackup) {
148
- const bakPath = `${path}.bak`;
149
- if (!existsSync(bakPath)) {
150
- writeFileSync(bakPath, original, 'utf-8');
151
- backupWritten = true;
152
- }
153
- }
154
- writeFileSync(path, migrated, 'utf-8');
155
- written = true;
156
- }
157
-
158
- return {
159
- path,
160
- original,
161
- migrated,
162
- changed,
163
- chartType,
164
- changedLines,
165
- backupWritten,
166
- written,
167
- };
168
- }
169
-
170
- // ============================================================
171
- // Directory walking
172
- // ============================================================
173
-
174
- const DGMO_EXTENSION = '.dgmo';
175
- const EMBEDDED_EXTENSIONS = new Set(['.md', '.mdx']);
176
-
177
- /**
178
- * Recursively collect `.dgmo` files under `root`, skipping common
179
- * noise (`node_modules`, `.git`, `dist`, `.bak` sidecars).
180
- */
181
- export function collectDgmoFiles(root: string): string[] {
182
- const stat = statSync(root);
183
- if (stat.isFile()) {
184
- return root.endsWith(DGMO_EXTENSION) ? [root] : [];
185
- }
186
- const out: string[] = [];
187
- walk(root, out, DGMO_EXTENSION);
188
- return out;
189
- }
190
-
191
- /**
192
- * Recursively collect `.md` / `.mdx` files under `root`. Used in
193
- * `--embedded` mode.
194
- */
195
- export function collectEmbeddedFiles(root: string): string[] {
196
- const stat = statSync(root);
197
- if (stat.isFile()) {
198
- return EMBEDDED_EXTENSIONS.has(extname(root)) ? [root] : [];
199
- }
200
- const out: string[] = [];
201
- walkEmbedded(root, out);
202
- return out;
203
- }
204
-
205
- function walk(dir: string, out: string[], ext: string): void {
206
- let entries;
207
- try {
208
- entries = readdirSync(dir, { withFileTypes: true });
209
- } catch {
210
- return;
211
- }
212
- for (const entry of entries) {
213
- const name = entry.name;
214
- if (
215
- name === 'node_modules' ||
216
- name === '.git' ||
217
- name === 'dist' ||
218
- name === '.next' ||
219
- name.endsWith('.bak')
220
- ) {
221
- continue;
222
- }
223
- const full = join(dir, name);
224
- if (entry.isDirectory()) {
225
- walk(full, out, ext);
226
- } else if (entry.isFile() && name.endsWith(ext)) {
227
- out.push(full);
228
- }
229
- }
230
- }
231
-
232
- function walkEmbedded(dir: string, out: string[]): void {
233
- let entries;
234
- try {
235
- entries = readdirSync(dir, { withFileTypes: true });
236
- } catch {
237
- return;
238
- }
239
- for (const entry of entries) {
240
- const name = entry.name;
241
- if (
242
- name === 'node_modules' ||
243
- name === '.git' ||
244
- name === 'dist' ||
245
- name === '.next' ||
246
- name.endsWith('.bak')
247
- ) {
248
- continue;
249
- }
250
- const full = join(dir, name);
251
- if (entry.isDirectory()) {
252
- walkEmbedded(full, out);
253
- } else if (entry.isFile() && EMBEDDED_EXTENSIONS.has(extname(name))) {
254
- out.push(full);
255
- }
256
- }
257
- }
258
-
259
- // ============================================================
260
- // Diff helper (used by --diff)
261
- // ============================================================
262
-
263
- /**
264
- * Minimal unified-style diff for printing changed lines.
265
- * Not a full unified-diff: only outputs the touched lines with
266
- * `- old` / `+ new` markers and a leading file header. Adequate
267
- * for the `--diff` flag on the migration CLI; no patch consumer.
268
- */
269
- export function formatLineDiff(
270
- path: string,
271
- original: string,
272
- migrated: string
273
- ): string {
274
- const origLines = original.split('\n');
275
- const newLines = migrated.split('\n');
276
- const lines: string[] = [`--- ${path}`, `+++ ${path}`];
277
- const max = Math.max(origLines.length, newLines.length);
278
- for (let i = 0; i < max; i++) {
279
- const a = origLines[i];
280
- const b = newLines[i];
281
- if (a === b) continue;
282
- if (a !== undefined) lines.push(`@@ line ${i + 1}\n- ${a}`);
283
- if (b !== undefined) lines.push(`+ ${b}`);
284
- }
285
- return lines.join('\n');
286
- }
287
-
288
- // Re-exports so consumers (cli.ts, dgmo-mcp) can import everything
289
- // they need from this entrypoint.
290
- export {
291
- isLegacyMetadataLine,
292
- findUnsafePipePositions,
293
- } from './line-classifier';
294
- export { transformLine } from './line-transformer';
295
- export type { TransformResult } from './line-transformer';
@@ -1,128 +0,0 @@
1
- // Single-line classifier for the `dgmo migrate` tool.
2
- //
3
- // `isLegacyMetadataLine(line)` returns true iff `line` contains a `|`
4
- // in a position that the unified §1.4 grammar treats as the legacy
5
- // metadata delimiter — i.e. NOT inside one of these surviving regions:
6
- //
7
- // * Wireframe option braces `{A | B}` (brace-depth tracked)
8
- // * Quoted strings `"..."` / `'...'` (quote-state tracked)
9
- // * In-arrow labels `A -text|more-> B` (§1.10, arrow regions)
10
- //
11
- // The classifier is intentionally pure and chart-type agnostic — every
12
- // chart type whose legacy syntax used `|` for metadata gets the same
13
- // treatment. Chart-type-specific bare-positional shapes (gantt `| 80%`,
14
- // journey-map `| 4 Delighted`, pyramid/ring `| description text`) all
15
- // start with `|` so the classifier flags them; the transformer is
16
- // where chart-type awareness lives.
17
-
18
- /**
19
- * Returns the byte offsets of every `|` on `line` that is OUTSIDE the
20
- * surviving non-metadata regions enumerated above. Empty array means
21
- * "no legacy metadata pipe on this line."
22
- */
23
- export function findUnsafePipePositions(line: string): number[] {
24
- const positions: number[] = [];
25
- let i = 0;
26
- let inDouble = false;
27
- let inSingle = false;
28
- let braceDepth = 0;
29
- let inArrow = false;
30
- let arrowOpenChar: '-' | '~' | null = null;
31
-
32
- while (i < line.length) {
33
- const ch = line[i]!;
34
- const ch2 = i + 1 < line.length ? line[i + 1]! : '';
35
-
36
- if (inDouble) {
37
- if (ch === '"') inDouble = false;
38
- i++;
39
- continue;
40
- }
41
- if (inSingle) {
42
- if (ch === "'") inSingle = false;
43
- i++;
44
- continue;
45
- }
46
- if (inArrow) {
47
- if (
48
- (arrowOpenChar === '-' && ch === '-' && ch2 === '>') ||
49
- (arrowOpenChar === '~' && ch === '~' && ch2 === '>')
50
- ) {
51
- inArrow = false;
52
- arrowOpenChar = null;
53
- i += 2;
54
- continue;
55
- }
56
- i++;
57
- continue;
58
- }
59
-
60
- if (ch === '"') {
61
- inDouble = true;
62
- i++;
63
- continue;
64
- }
65
- if (ch === "'") {
66
- inSingle = true;
67
- i++;
68
- continue;
69
- }
70
- if (ch === '{') {
71
- braceDepth++;
72
- i++;
73
- continue;
74
- }
75
- if (ch === '}') {
76
- if (braceDepth > 0) braceDepth--;
77
- i++;
78
- continue;
79
- }
80
-
81
- // Arrow start: an opening `-` or `~` that is preceded by whitespace
82
- // (token boundary) and followed by a non-arrow, non-space char,
83
- // with a matching `->` (or `~>`) closer further along the line and
84
- // no embedded `->`/`~>` substring inside the candidate label.
85
- // Conservative — false negatives leave the `|` flagged as metadata
86
- // (worst case: the migration tool tries to migrate the line and
87
- // the legacy parser rejects it, which is a surfaced skip, not a
88
- // silent corruption).
89
- if ((ch === '-' || ch === '~') && i > 0) {
90
- const prev = line[i - 1];
91
- const onBoundary = prev === ' ' || prev === '\t';
92
- const nextIsLabelChar =
93
- ch2 !== '' && ch2 !== ch && ch2 !== '>' && ch2 !== ' ' && ch2 !== '\t';
94
- if (onBoundary && nextIsLabelChar) {
95
- const closer = ch + '>';
96
- const closeIdx = line.indexOf(closer, i + 1);
97
- if (closeIdx > i) {
98
- const labelRegion = line.substring(i + 1, closeIdx);
99
- // Spec §1.10: `->` and `~>` substrings are forbidden inside
100
- // an arrow label. If they appear, this isn't a well-formed
101
- // arrow — bail and treat the `-`/`~` as ordinary text.
102
- if (!labelRegion.includes('->') && !labelRegion.includes('~>')) {
103
- inArrow = true;
104
- arrowOpenChar = ch as '-' | '~';
105
- i++;
106
- continue;
107
- }
108
- }
109
- }
110
- }
111
-
112
- if (ch === '|' && braceDepth === 0) {
113
- positions.push(i);
114
- }
115
-
116
- i++;
117
- }
118
-
119
- return positions;
120
- }
121
-
122
- /**
123
- * True iff `line` carries at least one legacy metadata `|` (outside
124
- * the surviving non-metadata regions). Pure, chart-type agnostic.
125
- */
126
- export function isLegacyMetadataLine(line: string): boolean {
127
- return findUnsafePipePositions(line).length > 0;
128
- }