@diagrammo/dgmo 0.32.1 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorrules +7 -2
- package/.github/copilot-instructions.md +7 -2
- package/.windsurfrules +7 -2
- package/SKILL.md +7 -2
- package/dist/advanced.cjs +1434 -443
- package/dist/advanced.d.cts +41 -1
- package/dist/advanced.d.ts +41 -1
- package/dist/advanced.js +1433 -443
- package/dist/auto.cjs +1242 -318
- package/dist/auto.js +91 -91
- package/dist/auto.mjs +1242 -318
- package/dist/cli.cjs +138 -136
- package/dist/index.cjs +1308 -317
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +1307 -317
- package/docs/ai-integration.md +7 -2
- package/docs/language-reference.md +39 -8
- package/package.json +2 -2
- package/src/advanced.ts +6 -1
- package/src/boxes-and-lines/label-placement.ts +339 -0
- package/src/boxes-and-lines/layout-grouped.ts +909 -0
- package/src/boxes-and-lines/layout-search.ts +34 -3
- package/src/boxes-and-lines/layout.ts +32 -7
- package/src/boxes-and-lines/parser.ts +3 -2
- package/src/boxes-and-lines/renderer.ts +67 -139
- package/src/c4/layout.ts +2 -1
- package/src/c4/parser.ts +3 -2
- package/src/c4/renderer.ts +3 -2
- package/src/cli.ts +68 -0
- package/src/er/parser.ts +2 -1
- package/src/er/renderer.ts +2 -1
- package/src/gantt/parser.ts +3 -5
- package/src/gantt/renderer.ts +7 -6
- package/src/index.ts +6 -1
- package/src/infra/parser.ts +2 -1
- package/src/infra/renderer.ts +4 -3
- package/src/journey-map/parser.ts +27 -8
- package/src/journey-map/renderer.ts +26 -9
- package/src/kanban/parser.ts +4 -3
- package/src/kanban/renderer.ts +6 -5
- package/src/map/completion.ts +116 -0
- package/src/map/layout.ts +4 -3
- package/src/map/parser.ts +3 -2
- package/src/mindmap/parser.ts +4 -3
- package/src/mindmap/renderer.ts +2 -1
- package/src/org/layout.ts +3 -2
- package/src/org/parser.ts +3 -2
- package/src/org/renderer.ts +5 -4
- package/src/pert/parser.ts +3 -2
- package/src/sequence/renderer.ts +10 -12
- package/src/sequence/tag-resolution.ts +2 -1
- package/src/sitemap/layout.ts +3 -2
- package/src/sitemap/parser.ts +36 -2
- package/src/sitemap/renderer.ts +12 -6
- package/src/timeline/renderer.ts +4 -3
- package/src/utils/legend-d3.ts +4 -3
- package/src/utils/legend-svg.ts +3 -2
- package/src/utils/tag-groups.ts +28 -7
- package/src/visualizations/parse.ts +2 -1
package/src/gantt/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Gantt Chart Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Scale from 'd3-scale';
|
|
6
7
|
import * as d3Selection from 'd3-selection';
|
|
7
8
|
import { FONT_FAMILY } from '../fonts';
|
|
@@ -660,7 +661,7 @@ export function renderGantt(
|
|
|
660
661
|
(tg) => tg.name.toLowerCase() === currentSwimlaneGroup.toLowerCase()
|
|
661
662
|
);
|
|
662
663
|
if (tagGroup) {
|
|
663
|
-
const tagKey = tagGroup.name
|
|
664
|
+
const tagKey = tagAttrKey(tagGroup.name);
|
|
664
665
|
for (const rt of resolved.tasks) {
|
|
665
666
|
let value = rt.effectiveMetadata[tagKey];
|
|
666
667
|
if (!value && tagGroup.defaultValue) value = tagGroup.defaultValue;
|
|
@@ -2091,7 +2092,7 @@ function renderTagLegend(
|
|
|
2091
2092
|
const usedValues = new Map<string, Set<string>>();
|
|
2092
2093
|
if (resolvedTasks) {
|
|
2093
2094
|
for (const group of visibleGroups) {
|
|
2094
|
-
const key = group.name
|
|
2095
|
+
const key = tagAttrKey(group.name);
|
|
2095
2096
|
const used = new Set<string>();
|
|
2096
2097
|
for (const rt of resolvedTasks) {
|
|
2097
2098
|
const val = rt.effectiveMetadata[key];
|
|
@@ -2104,7 +2105,7 @@ function renderTagLegend(
|
|
|
2104
2105
|
// Filter entries to only those used in the current view
|
|
2105
2106
|
const filteredEntries = new Map<string, readonly TagEntry[]>();
|
|
2106
2107
|
for (const group of visibleGroups) {
|
|
2107
|
-
const key = group.name
|
|
2108
|
+
const key = tagAttrKey(group.name);
|
|
2108
2109
|
const used = usedValues.get(key);
|
|
2109
2110
|
if (used && used.size > 0) {
|
|
2110
2111
|
filteredEntries.set(
|
|
@@ -2133,7 +2134,7 @@ function renderTagLegend(
|
|
|
2133
2134
|
let groupW = pillW;
|
|
2134
2135
|
if (isActive) {
|
|
2135
2136
|
const entries =
|
|
2136
|
-
filteredEntries.get(group.name
|
|
2137
|
+
filteredEntries.get(tagAttrKey(group.name)) ?? group.entries;
|
|
2137
2138
|
let entriesW = 0;
|
|
2138
2139
|
for (const entry of entries) {
|
|
2139
2140
|
entriesW +=
|
|
@@ -2215,7 +2216,7 @@ function renderTagLegend(
|
|
|
2215
2216
|
}),
|
|
2216
2217
|
...(onControlsToggle !== undefined && { onControlsToggle }),
|
|
2217
2218
|
onEntryHover: (groupName, entryValue) => {
|
|
2218
|
-
const tagKey = groupName
|
|
2219
|
+
const tagKey = tagAttrKey(groupName);
|
|
2219
2220
|
if (entryValue) {
|
|
2220
2221
|
const ev = entryValue.toLowerCase();
|
|
2221
2222
|
chartG
|
|
@@ -3388,7 +3389,7 @@ export function buildTagLaneRowList(
|
|
|
3388
3389
|
);
|
|
3389
3390
|
if (!tagGroup) return null;
|
|
3390
3391
|
|
|
3391
|
-
const tagKey = tagGroup.name
|
|
3392
|
+
const tagKey = tagAttrKey(tagGroup.name);
|
|
3392
3393
|
const rows: Row[] = [];
|
|
3393
3394
|
|
|
3394
3395
|
// Bucket tasks by tag value
|
package/src/index.ts
CHANGED
|
@@ -226,11 +226,16 @@ export { normalizeSvgForEmbed, getEmbedSvgViewBox } from './utils/svg-embed';
|
|
|
226
226
|
// ============================================================
|
|
227
227
|
// Pure + dependency-injected: the caller supplies the `Gazetteer` asset. Safe
|
|
228
228
|
// to export from the main index — no d3-geo/topojson runtime imports.
|
|
229
|
-
export {
|
|
229
|
+
export {
|
|
230
|
+
completeMapPlaces,
|
|
231
|
+
completeMapRegions,
|
|
232
|
+
searchMapLocations,
|
|
233
|
+
} from './map/completion';
|
|
230
234
|
export type {
|
|
231
235
|
MapPlaceCompletion,
|
|
232
236
|
MapRegionCompletion,
|
|
233
237
|
MapCompletionOptions,
|
|
238
|
+
MapLocationMatch,
|
|
234
239
|
} from './map/completion';
|
|
235
240
|
export type {
|
|
236
241
|
Gazetteer,
|
package/src/infra/parser.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Handles: chart metadata, component blocks with indented properties
|
|
7
7
|
// and connections, [Group] containers, tag groups, pipe metadata.
|
|
8
8
|
|
|
9
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
9
10
|
import {
|
|
10
11
|
descriptionBareRemovedMessage,
|
|
11
12
|
formatDgmoError,
|
|
@@ -1054,7 +1055,7 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
1054
1055
|
// Inject default tag values into nodes that don't have one
|
|
1055
1056
|
for (const tg of result.tagGroups) {
|
|
1056
1057
|
if (!tg.defaultValue) continue;
|
|
1057
|
-
const key = (tg.alias ?? tg.name)
|
|
1058
|
+
const key = tagAttrKey(tg.alias ?? tg.name);
|
|
1058
1059
|
for (const node of result.nodes) {
|
|
1059
1060
|
if (node.isEdge) continue;
|
|
1060
1061
|
if (!(key in node.tags)) {
|
package/src/infra/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Infra Chart SVG Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Selection from 'd3-selection';
|
|
6
7
|
import * as d3Shape from 'd3-shape';
|
|
7
8
|
import { FONT_FAMILY } from '../fonts';
|
|
@@ -1388,7 +1389,7 @@ function resolveActiveTagStroke(
|
|
|
1388
1389
|
(t) => t.name.toLowerCase() === activeGroup.toLowerCase()
|
|
1389
1390
|
);
|
|
1390
1391
|
if (!tg) return null;
|
|
1391
|
-
const tagKey = (tg.alias ?? tg.name)
|
|
1392
|
+
const tagKey = tagAttrKey(tg.alias ?? tg.name);
|
|
1392
1393
|
const tagVal = node.tags[tagKey];
|
|
1393
1394
|
if (!tagVal) return null;
|
|
1394
1395
|
const tv = tg.values.find(
|
|
@@ -2073,7 +2074,7 @@ export function computeInfraLegendGroups(
|
|
|
2073
2074
|
groups.push({
|
|
2074
2075
|
name: tg.name,
|
|
2075
2076
|
type: 'tag',
|
|
2076
|
-
tagKey: (tg.alias ?? tg.name)
|
|
2077
|
+
tagKey: tagAttrKey(tg.alias ?? tg.name),
|
|
2077
2078
|
entries,
|
|
2078
2079
|
width: LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth,
|
|
2079
2080
|
minifiedWidth: pillWidth,
|
|
@@ -2146,7 +2147,7 @@ function renderLegend(
|
|
|
2146
2147
|
// Add infra-specific classes and data attributes for app interactivity
|
|
2147
2148
|
legendG.selectAll('[data-legend-group]').classed('infra-legend-group', true);
|
|
2148
2149
|
for (const group of legendGroups) {
|
|
2149
|
-
const groupKey = group.name
|
|
2150
|
+
const groupKey = tagAttrKey(group.name);
|
|
2150
2151
|
for (const entry of group.entries) {
|
|
2151
2152
|
const entryEl = legendG.select(
|
|
2152
2153
|
`[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
JOURNEY_MAP_REGISTRY,
|
|
15
15
|
withTagAliases,
|
|
16
16
|
} from '../utils/reserved-key-registry';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
splitNameAndMeta,
|
|
19
|
+
warnUnknownMetaKeys,
|
|
20
|
+
peelTrailingColorName,
|
|
21
|
+
} from '../utils/parsing';
|
|
18
22
|
import {
|
|
19
23
|
matchTagBlockHeading,
|
|
20
24
|
emitTagLegacyDiagnostic,
|
|
@@ -22,6 +26,7 @@ import {
|
|
|
22
26
|
validateTagGroupNames,
|
|
23
27
|
finalizeAutoTagColors,
|
|
24
28
|
AUTO_TAG_COLOR_SENTINEL,
|
|
29
|
+
tagAttrKey,
|
|
25
30
|
} from '../utils/tag-groups';
|
|
26
31
|
import {
|
|
27
32
|
measureIndent,
|
|
@@ -180,8 +185,10 @@ export function parseJourneyMap(
|
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
} else {
|
|
183
|
-
// Same-line form (pipes removed in 0.18.0)
|
|
184
|
-
// `color: <token>`
|
|
188
|
+
// Same-line form (pipes removed in 0.18.0). Two ways to set color:
|
|
189
|
+
// 1. explicit `persona <name> color: <token>` (long form)
|
|
190
|
+
// 2. universal §1.5 trailing-token color (`persona <name> green`);
|
|
191
|
+
// capitalize the word (`Green`) to keep it as literal name text.
|
|
185
192
|
const colorMatch = afterKeyword.match(/^(.+?)\s+color:\s*(\S+)$/i);
|
|
186
193
|
if (colorMatch) {
|
|
187
194
|
personaName = colorMatch[1]!.trim();
|
|
@@ -193,7 +200,19 @@ export function parseJourneyMap(
|
|
|
193
200
|
palette
|
|
194
201
|
) ?? undefined;
|
|
195
202
|
} else {
|
|
196
|
-
|
|
203
|
+
const { label, colorName } = peelTrailingColorName(afterKeyword);
|
|
204
|
+
if (colorName) {
|
|
205
|
+
personaName = label;
|
|
206
|
+
personaColor =
|
|
207
|
+
resolveColorWithDiagnostic(
|
|
208
|
+
colorName,
|
|
209
|
+
lineNumber,
|
|
210
|
+
result.diagnostics,
|
|
211
|
+
palette
|
|
212
|
+
) ?? undefined;
|
|
213
|
+
} else {
|
|
214
|
+
personaName = afterKeyword;
|
|
215
|
+
}
|
|
197
216
|
}
|
|
198
217
|
}
|
|
199
218
|
|
|
@@ -246,12 +265,12 @@ export function parseJourneyMap(
|
|
|
246
265
|
if (tagBlockMatch.alias) {
|
|
247
266
|
aliasMap.set(
|
|
248
267
|
tagBlockMatch.alias.toLowerCase(),
|
|
249
|
-
tagBlockMatch.name
|
|
268
|
+
tagAttrKey(tagBlockMatch.name)
|
|
250
269
|
);
|
|
251
270
|
}
|
|
252
271
|
aliasMap.set(
|
|
253
|
-
tagBlockMatch.name
|
|
254
|
-
tagBlockMatch.name
|
|
272
|
+
tagAttrKey(tagBlockMatch.name),
|
|
273
|
+
tagAttrKey(tagBlockMatch.name)
|
|
255
274
|
);
|
|
256
275
|
result.tagGroups.push(currentTagGroup);
|
|
257
276
|
continue;
|
|
@@ -445,7 +464,7 @@ export function parseJourneyMap(
|
|
|
445
464
|
// Build tag value sets
|
|
446
465
|
for (const group of result.tagGroups) {
|
|
447
466
|
const values = new Set(group.entries.map((e) => e.value.toLowerCase()));
|
|
448
|
-
tagValueSets.set(group.name
|
|
467
|
+
tagValueSets.set(tagAttrKey(group.name), values);
|
|
449
468
|
}
|
|
450
469
|
|
|
451
470
|
// Validate tag values on all steps
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
1
2
|
import * as d3 from 'd3-selection';
|
|
2
3
|
import * as d3Shape from 'd3-shape';
|
|
3
4
|
import type { PaletteColors } from '../palettes';
|
|
@@ -18,6 +19,7 @@ import type {
|
|
|
18
19
|
JourneyMapStep,
|
|
19
20
|
} from './types';
|
|
20
21
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
22
|
+
import { wrapNoteBody, NOTE_BULLET_INDENT } from '../utils/note-box';
|
|
21
23
|
import { renderIntegratedLegend } from '../utils/legend-integration';
|
|
22
24
|
import { resolveActiveTagGroup } from '../utils/tag-groups';
|
|
23
25
|
import { ScaleContext } from '../utils/scaling';
|
|
@@ -194,9 +196,10 @@ export function renderJourneyMap(
|
|
|
194
196
|
const textAreaWidth = panelWidth - silhouetteZone - CARD_PADDING_X;
|
|
195
197
|
const descLineH = 14;
|
|
196
198
|
|
|
197
|
-
// Wrap description text into lines
|
|
199
|
+
// Wrap description text into lines — bullet-aware (`- `/`* ` → hanging
|
|
200
|
+
// "•") + inline markdown, matching how note boxes render rich bodies.
|
|
198
201
|
const descLines = parsed.persona.description
|
|
199
|
-
?
|
|
202
|
+
? wrapNoteBody(parsed.persona.description, textAreaWidth, FONT_SIZE_META)
|
|
200
203
|
: [];
|
|
201
204
|
const descRowH =
|
|
202
205
|
descLines.length > 0 ? descLines.length * descLineH + 8 : 0;
|
|
@@ -276,16 +279,30 @@ export function renderJourneyMap(
|
|
|
276
279
|
.attr('fill', onPersonaText)
|
|
277
280
|
.text(parsed.persona.name);
|
|
278
281
|
|
|
279
|
-
// Description — wrapped lines below divider, with inline markdown
|
|
282
|
+
// Description — wrapped lines below divider, with inline markdown.
|
|
283
|
+
// Bullet first-lines get a "•" glyph at the left edge with the body
|
|
284
|
+
// hanging-indented; continuation lines align under the body.
|
|
280
285
|
for (let li = 0; li < descLines.length; li++) {
|
|
286
|
+
// In-bounds by loop guard.
|
|
287
|
+
const line = descLines[li]!;
|
|
288
|
+
const indent = line.kind === 'plain' ? 0 : NOTE_BULLET_INDENT;
|
|
289
|
+
const lineY = panelY + titleRowH + descLineH * (li + 1);
|
|
290
|
+
if (line.kind === 'bullet-first') {
|
|
291
|
+
personaG
|
|
292
|
+
.append('text')
|
|
293
|
+
.attr('x', textX)
|
|
294
|
+
.attr('y', lineY)
|
|
295
|
+
.attr('font-size', FONT_SIZE_META)
|
|
296
|
+
.attr('fill', onPersonaText)
|
|
297
|
+
.text('•');
|
|
298
|
+
}
|
|
281
299
|
const lineEl = personaG
|
|
282
300
|
.append('text')
|
|
283
|
-
.attr('x', textX)
|
|
284
|
-
.attr('y',
|
|
301
|
+
.attr('x', textX + indent)
|
|
302
|
+
.attr('y', lineY)
|
|
285
303
|
.attr('font-size', FONT_SIZE_META)
|
|
286
304
|
.attr('fill', onPersonaText);
|
|
287
|
-
|
|
288
|
-
renderInlineText(lineEl, descLines[li]!, palette, FONT_SIZE_META);
|
|
305
|
+
renderInlineText(lineEl, line.text, palette, FONT_SIZE_META);
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
if (onNavigateToLine) {
|
|
@@ -346,7 +363,7 @@ export function renderJourneyMap(
|
|
|
346
363
|
const isScore = groupName === 'Score';
|
|
347
364
|
const attrName = isScore
|
|
348
365
|
? 'data-score'
|
|
349
|
-
: `data-tag-${groupName
|
|
366
|
+
: `data-tag-${tagAttrKey(groupName)}`;
|
|
350
367
|
|
|
351
368
|
const matches = (el: Element) =>
|
|
352
369
|
el.getAttribute(attrName) === entryValue;
|
|
@@ -1094,7 +1111,7 @@ function resolveStepColor(
|
|
|
1094
1111
|
);
|
|
1095
1112
|
if (!group) return undefined;
|
|
1096
1113
|
|
|
1097
|
-
const tagValue = step.tags[group.name
|
|
1114
|
+
const tagValue = step.tags[tagAttrKey(group.name)];
|
|
1098
1115
|
if (!tagValue) return undefined;
|
|
1099
1116
|
|
|
1100
1117
|
const entry = group.entries.find(
|
package/src/kanban/parser.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
validateTagGroupNames,
|
|
20
20
|
finalizeAutoTagColors,
|
|
21
21
|
AUTO_TAG_COLOR_SENTINEL,
|
|
22
|
+
tagAttrKey,
|
|
22
23
|
} from '../utils/tag-groups';
|
|
23
24
|
import {
|
|
24
25
|
measureIndent,
|
|
@@ -168,14 +169,14 @@ export function parseKanban(
|
|
|
168
169
|
if (tagBlockMatch.alias) {
|
|
169
170
|
metaAliasMap.set(
|
|
170
171
|
normalizeName(tagBlockMatch.alias),
|
|
171
|
-
tagBlockMatch.name
|
|
172
|
+
tagAttrKey(tagBlockMatch.name)
|
|
172
173
|
);
|
|
173
174
|
}
|
|
174
175
|
// §1.4 dispatch: register the canonical tag name so `priority: X`
|
|
175
176
|
// also dispatches as metadata (not just the optional `p: X` alias).
|
|
176
177
|
metaAliasMap.set(
|
|
177
178
|
normalizeName(tagBlockMatch.name),
|
|
178
|
-
tagBlockMatch.name
|
|
179
|
+
tagAttrKey(tagBlockMatch.name)
|
|
179
180
|
);
|
|
180
181
|
result.tagGroups.push(currentTagGroup);
|
|
181
182
|
continue;
|
|
@@ -419,7 +420,7 @@ export function parseKanban(
|
|
|
419
420
|
// Build tag value sets for validation
|
|
420
421
|
for (const group of result.tagGroups) {
|
|
421
422
|
const values = new Set(group.entries.map((e) => e.value.toLowerCase()));
|
|
422
|
-
tagValueSets.set(group.name
|
|
423
|
+
tagValueSets.set(tagAttrKey(group.name), values);
|
|
423
424
|
}
|
|
424
425
|
|
|
425
426
|
// Validate WIP limits
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Kanban Board SVG Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Selection from 'd3-selection';
|
|
6
7
|
import { FONT_FAMILY } from '../fonts';
|
|
7
8
|
import type { PaletteColors } from '../palettes';
|
|
@@ -82,7 +83,7 @@ function resolveCardTagMeta(
|
|
|
82
83
|
const meta: { label: string; value: string; color?: string }[] = [];
|
|
83
84
|
for (const group of tagGroups) {
|
|
84
85
|
if (hiddenMetaGroups?.includes(group.name.toLowerCase())) continue;
|
|
85
|
-
const tagValue = card.tags[group.name
|
|
86
|
+
const tagValue = card.tags[tagAttrKey(group.name)];
|
|
86
87
|
const value = tagValue ?? group.defaultValue;
|
|
87
88
|
if (!value) continue;
|
|
88
89
|
const entry = group.entries.find(
|
|
@@ -107,7 +108,7 @@ function resolveCardTagColor(
|
|
|
107
108
|
(g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
|
|
108
109
|
);
|
|
109
110
|
if (!group) return card.color;
|
|
110
|
-
const tagValue = card.tags[group.name
|
|
111
|
+
const tagValue = card.tags[tagAttrKey(group.name)];
|
|
111
112
|
const value = tagValue ?? group.defaultValue;
|
|
112
113
|
if (!value) return undefined;
|
|
113
114
|
const entry = group.entries.find(
|
|
@@ -639,7 +640,7 @@ export function renderKanban(
|
|
|
639
640
|
.attr('data-line-number', card.lineNumber);
|
|
640
641
|
|
|
641
642
|
if (activeTagGroup) {
|
|
642
|
-
const tagKey = activeTagGroup
|
|
643
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
643
644
|
const tagValue = card.tags[tagKey];
|
|
644
645
|
const group = parsed.tagGroups.find(
|
|
645
646
|
(tg) => tg.name.toLowerCase() === tagKey
|
|
@@ -806,7 +807,7 @@ function bucketCardsBySwimlane(
|
|
|
806
807
|
columns: KanbanColumn[],
|
|
807
808
|
swimlaneGroup: KanbanTagGroup
|
|
808
809
|
): SwimlaneBucket[] {
|
|
809
|
-
const tagKey = swimlaneGroup.name
|
|
810
|
+
const tagKey = tagAttrKey(swimlaneGroup.name);
|
|
810
811
|
const buckets = new Map<string, SwimlaneBucket>();
|
|
811
812
|
|
|
812
813
|
// Seed lanes from declaration order
|
|
@@ -1369,7 +1370,7 @@ function renderSwimlaneCard(
|
|
|
1369
1370
|
.attr('data-line-number', card.lineNumber);
|
|
1370
1371
|
|
|
1371
1372
|
if (activeTagGroup) {
|
|
1372
|
-
const tagKey = activeTagGroup
|
|
1373
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
1373
1374
|
const group = tagGroups.find((tg) => tg.name.toLowerCase() === tagKey);
|
|
1374
1375
|
const value = card.tags[tagKey] ?? group?.defaultValue;
|
|
1375
1376
|
if (value) {
|
package/src/map/completion.ts
CHANGED
|
@@ -151,6 +151,122 @@ export function completeMapPlaces(
|
|
|
151
151
|
return [...cityItems, ...airportItems].slice(0, limit);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/** A POI search hit — a city or airport the resolver can resolve, with the
|
|
155
|
+
* exact `token` to paste into DGMO (a `poi`/route endpoint). Powers the CLI
|
|
156
|
+
* `dgmo map search` command and the MCP `lookup_map_location` tool, so authors
|
|
157
|
+
* (and AI agents) can DISCOVER valid place tokens instead of guessing — e.g.
|
|
158
|
+
* learn that "New York" must be `New York City` and that `JFK` is bundled. */
|
|
159
|
+
export interface MapLocationMatch {
|
|
160
|
+
/** Canonical display name (cities) or full airport name. */
|
|
161
|
+
readonly name: string;
|
|
162
|
+
/** Exact text to use in DGMO. Cities: the name, ISO-qualified when ambiguous
|
|
163
|
+
* (`Portland US-OR`). Airports: the upper-case IATA code (`JFK`). */
|
|
164
|
+
readonly token: string;
|
|
165
|
+
readonly kind: 'city' | 'airport';
|
|
166
|
+
readonly iso: string;
|
|
167
|
+
readonly sub?: string;
|
|
168
|
+
/** City population (0 for airports). */
|
|
169
|
+
readonly pop: number;
|
|
170
|
+
/** Human detail, e.g. `US-OR · 652,503` or `Airport · John F Kennedy Intl`. */
|
|
171
|
+
readonly detail: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Substring-search cities + airports for a discovery surface (CLI / MCP), so an
|
|
176
|
+
* author can find the exact token the resolver expects. Unlike
|
|
177
|
+
* {@link completeMapPlaces} (prefix-only, editor type-ahead), this matches
|
|
178
|
+
* anywhere in a city name OR an airport code/name, so "york" finds
|
|
179
|
+
* `New York City` and "kennedy" finds `JFK`.
|
|
180
|
+
*
|
|
181
|
+
* Ranking: exact-name (or exact-IATA) → prefix → substring; within a tier,
|
|
182
|
+
* cities by population desc (deterministic index tie-break), cities before
|
|
183
|
+
* airports. Pure + deterministic. Empty query → `[]`.
|
|
184
|
+
*/
|
|
185
|
+
export function searchMapLocations(
|
|
186
|
+
query: string,
|
|
187
|
+
gazetteer: Gazetteer,
|
|
188
|
+
opts?: { readonly limit?: number; readonly airports?: AirportData }
|
|
189
|
+
): MapLocationMatch[] {
|
|
190
|
+
const q = fold(query);
|
|
191
|
+
if (!q) return [];
|
|
192
|
+
const limit = opts?.limit ?? 20;
|
|
193
|
+
|
|
194
|
+
// rank: 0 exact, 1 prefix, 2 substring; -1 = no match.
|
|
195
|
+
const rankOf = (hay: string): number =>
|
|
196
|
+
hay === q ? 0 : hay.startsWith(q) ? 1 : hay.includes(q) ? 2 : -1;
|
|
197
|
+
|
|
198
|
+
// Cities — match on folded display name (via byName keys) + aliases.
|
|
199
|
+
const cityRank = new Map<number, number>(); // city index → best rank
|
|
200
|
+
const consider = (idx: number, rank: number): void => {
|
|
201
|
+
if (rank < 0) return;
|
|
202
|
+
const prev = cityRank.get(idx);
|
|
203
|
+
if (prev === undefined || rank < prev) cityRank.set(idx, rank);
|
|
204
|
+
};
|
|
205
|
+
for (const [key, list] of Object.entries(gazetteer.byName)) {
|
|
206
|
+
const r = rankOf(key);
|
|
207
|
+
if (r >= 0) for (const i of list) consider(i, r);
|
|
208
|
+
}
|
|
209
|
+
for (const [key, idx] of Object.entries(gazetteer.alt)) {
|
|
210
|
+
consider(idx, rankOf(key));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cityItems: Array<MapLocationMatch & { _rank: number }> = [...cityRank]
|
|
214
|
+
.filter(([i]) => gazetteer.cities[i] !== undefined)
|
|
215
|
+
.map(([i, rank]) => {
|
|
216
|
+
const c = gazetteer.cities[i]!;
|
|
217
|
+
const [, , iso, pop, name, sub] = c;
|
|
218
|
+
const ambiguous = (gazetteer.byName[fold(name)]?.length ?? 0) > 1;
|
|
219
|
+
const qualifier = sub ?? iso;
|
|
220
|
+
return {
|
|
221
|
+
name,
|
|
222
|
+
token: ambiguous ? `${name} ${qualifier}` : name,
|
|
223
|
+
kind: 'city' as const,
|
|
224
|
+
iso,
|
|
225
|
+
...(sub !== undefined && { sub }),
|
|
226
|
+
pop,
|
|
227
|
+
detail: `${qualifier} · ${groupThousands(pop)}`,
|
|
228
|
+
_rank: rank,
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Airports — match on folded IATA code OR folded airport name.
|
|
233
|
+
const airports = opts?.airports;
|
|
234
|
+
const airportItems: Array<MapLocationMatch & { _rank: number }> = [];
|
|
235
|
+
if (airports?.airportIata) {
|
|
236
|
+
for (const [code, idx] of Object.entries(airports.airportIata)) {
|
|
237
|
+
const a = airports.airports[idx];
|
|
238
|
+
if (!a) continue;
|
|
239
|
+
const codeRank = rankOf(code);
|
|
240
|
+
const nameRank = rankOf(fold(a[4]));
|
|
241
|
+
const rank = Math.min(
|
|
242
|
+
codeRank < 0 ? 99 : codeRank,
|
|
243
|
+
nameRank < 0 ? 99 : nameRank
|
|
244
|
+
);
|
|
245
|
+
if (rank > 2) continue;
|
|
246
|
+
airportItems.push({
|
|
247
|
+
name: a[4],
|
|
248
|
+
token: code.toUpperCase(),
|
|
249
|
+
kind: 'airport',
|
|
250
|
+
iso: a[2],
|
|
251
|
+
pop: 0,
|
|
252
|
+
detail: `Airport · ${a[4]}`,
|
|
253
|
+
_rank: rank,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [...cityItems, ...airportItems]
|
|
259
|
+
.sort(
|
|
260
|
+
(x, y) =>
|
|
261
|
+
x._rank - y._rank || // best match tier first
|
|
262
|
+
(x.kind === y.kind ? 0 : x.kind === 'city' ? -1 : 1) || // city before airport
|
|
263
|
+
y.pop - x.pop || // populous city first
|
|
264
|
+
(x.token < y.token ? -1 : x.token > y.token ? 1 : 0)
|
|
265
|
+
)
|
|
266
|
+
.slice(0, limit)
|
|
267
|
+
.map(({ _rank, ...rest }) => rest);
|
|
268
|
+
}
|
|
269
|
+
|
|
154
270
|
export interface MapRegionCompletion {
|
|
155
271
|
/** Display name = insert text (the resolver disambiguates cross-layer
|
|
156
272
|
* collisions like Georgia by map scope, §24B.8). */
|
package/src/map/layout.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// POI radii + edge widths, and places labels with per-cluster collision
|
|
6
6
|
// escalation. The SVG emission is renderer.ts (it only draws what we compute).
|
|
7
7
|
// See spec section 24B.3-.7/.11 + the tech-spec Adversarial Review Resolutions AR1-AR9.
|
|
8
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
8
9
|
import {
|
|
9
10
|
geoPath,
|
|
10
11
|
geoNaturalEarth1,
|
|
@@ -1582,7 +1583,7 @@ export function layoutMap(
|
|
|
1582
1583
|
(g) => g.name.toLowerCase() === groupName.toLowerCase()
|
|
1583
1584
|
);
|
|
1584
1585
|
if (!group) return null;
|
|
1585
|
-
const val = tags[group.name
|
|
1586
|
+
const val = tags[tagAttrKey(group.name)] ?? group.defaultValue;
|
|
1586
1587
|
if (!val) return null;
|
|
1587
1588
|
const entry = group.entries.find(
|
|
1588
1589
|
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
@@ -2656,7 +2657,7 @@ export function layoutMap(
|
|
|
2656
2657
|
if (directHex)
|
|
2657
2658
|
return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
|
|
2658
2659
|
for (const group of resolved.tagGroups) {
|
|
2659
|
-
const val = p.tags[group.name
|
|
2660
|
+
const val = p.tags[tagAttrKey(group.name)];
|
|
2660
2661
|
if (!val) continue;
|
|
2661
2662
|
const entry = group.entries.find(
|
|
2662
2663
|
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
@@ -2678,7 +2679,7 @@ export function layoutMap(
|
|
|
2678
2679
|
// or null → caller falls back to the neutral connector mix.
|
|
2679
2680
|
const lineColor = (tags: Readonly<Record<string, string>>): string | null => {
|
|
2680
2681
|
for (const group of resolved.tagGroups) {
|
|
2681
|
-
const val = tags[group.name
|
|
2682
|
+
const val = tags[tagAttrKey(group.name)];
|
|
2682
2683
|
if (!val) continue;
|
|
2683
2684
|
const entry = group.entries.find(
|
|
2684
2685
|
(e) => e.value.toLowerCase() === val.toLowerCase()
|
package/src/map/parser.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
validateTagGroupNames,
|
|
26
26
|
finalizeAutoTagColors,
|
|
27
27
|
AUTO_TAG_COLOR_SENTINEL,
|
|
28
|
+
tagAttrKey,
|
|
28
29
|
} from '../utils/tag-groups';
|
|
29
30
|
import { parseInArrowLabel } from '../utils/arrows';
|
|
30
31
|
import type {
|
|
@@ -157,7 +158,7 @@ export function parseMap(content: string, palette?: PaletteColors): ParsedMap {
|
|
|
157
158
|
// Declared tag-group names (lowercased) — the keys `partitionMeta` treats as
|
|
158
159
|
// tag values (resolved from aliases). Recomputed as groups are declared.
|
|
159
160
|
const tagGroupNames = (): ReadonlySet<string> =>
|
|
160
|
-
new Set(tagGroups.map((g) => g.name
|
|
161
|
+
new Set(tagGroups.map((g) => tagAttrKey(g.name)));
|
|
161
162
|
|
|
162
163
|
const closeBlocks = (): void => {
|
|
163
164
|
open.tag = null;
|
|
@@ -378,7 +379,7 @@ export function parseMap(content: string, palette?: PaletteColors): ParsedMap {
|
|
|
378
379
|
entries: [],
|
|
379
380
|
lineNumber: line,
|
|
380
381
|
};
|
|
381
|
-
if (m.alias) aliasMap.set(m.alias.toLowerCase(), m.name
|
|
382
|
+
if (m.alias) aliasMap.set(m.alias.toLowerCase(), tagAttrKey(m.name));
|
|
382
383
|
tagGroups.push(group);
|
|
383
384
|
open.tag = group;
|
|
384
385
|
// Inline form: `tag Market as m HQ indigo, Region teal` (R4).
|
package/src/mindmap/parser.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
finalizeAutoTagColors,
|
|
20
20
|
cascadeTagMetadata,
|
|
21
21
|
AUTO_TAG_COLOR_SENTINEL,
|
|
22
|
+
tagAttrKey,
|
|
22
23
|
} from '../utils/tag-groups';
|
|
23
24
|
import {
|
|
24
25
|
measureIndent,
|
|
@@ -167,12 +168,12 @@ export function parseMindmap(
|
|
|
167
168
|
if (tagBlockMatch.alias) {
|
|
168
169
|
aliasMap.set(
|
|
169
170
|
tagBlockMatch.alias.toLowerCase(),
|
|
170
|
-
tagBlockMatch.name
|
|
171
|
+
tagAttrKey(tagBlockMatch.name)
|
|
171
172
|
);
|
|
172
173
|
}
|
|
173
174
|
aliasMap.set(
|
|
174
|
-
tagBlockMatch.name
|
|
175
|
-
tagBlockMatch.name
|
|
175
|
+
tagAttrKey(tagBlockMatch.name),
|
|
176
|
+
tagAttrKey(tagBlockMatch.name)
|
|
176
177
|
);
|
|
177
178
|
result.tagGroups.push(currentTagGroup);
|
|
178
179
|
continue;
|
package/src/mindmap/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Mindmap SVG Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Selection from 'd3-selection';
|
|
6
7
|
import { FONT_FAMILY } from '../fonts';
|
|
7
8
|
import {
|
|
@@ -389,7 +390,7 @@ export function renderMindmap(
|
|
|
389
390
|
|
|
390
391
|
// Expose active tag group value for legend-entry hover dimming
|
|
391
392
|
if (activeTagGroup) {
|
|
392
|
-
const tagKey = activeTagGroup
|
|
393
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
393
394
|
const metaValue = node.metadata[tagKey];
|
|
394
395
|
if (metaValue) {
|
|
395
396
|
nodeG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
|
package/src/org/layout.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Org Chart Tree Layout Engine (d3-hierarchy)
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import { hierarchy, tree } from 'd3-hierarchy';
|
|
6
7
|
import type { ParsedOrg, OrgNode } from './parser';
|
|
7
8
|
import type { Writable } from '../utils/brand';
|
|
@@ -303,7 +304,7 @@ function computeLegendGroups(
|
|
|
303
304
|
if (group.entries.length === 0) continue;
|
|
304
305
|
|
|
305
306
|
// Filter entries to only values actually used by nodes (if provided)
|
|
306
|
-
const usedValues = usedValuesByGroup?.get(group.name
|
|
307
|
+
const usedValues = usedValuesByGroup?.get(tagAttrKey(group.name));
|
|
307
308
|
const visibleEntries = usedValues
|
|
308
309
|
? group.entries.filter((e) => usedValues.has(e.value.toLowerCase()))
|
|
309
310
|
: group.entries;
|
|
@@ -1185,7 +1186,7 @@ export function layoutOrg(
|
|
|
1185
1186
|
// Collect which tag group values are actually used by nodes
|
|
1186
1187
|
const usedValuesByGroup = new Map<string, Set<string>>();
|
|
1187
1188
|
for (const group of parsed.tagGroups) {
|
|
1188
|
-
const key = group.name
|
|
1189
|
+
const key = tagAttrKey(group.name);
|
|
1189
1190
|
const used = new Set<string>();
|
|
1190
1191
|
const walk = (node: OrgNode) => {
|
|
1191
1192
|
if (!node.isContainer && node.metadata[key]) {
|
package/src/org/parser.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
stripDefaultModifier,
|
|
21
21
|
finalizeAutoTagColors,
|
|
22
22
|
AUTO_TAG_COLOR_SENTINEL,
|
|
23
|
+
tagAttrKey,
|
|
23
24
|
} from '../utils/tag-groups';
|
|
24
25
|
import {
|
|
25
26
|
measureIndent,
|
|
@@ -219,14 +220,14 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
|
|
|
219
220
|
if (tagBlockMatch.alias) {
|
|
220
221
|
metaAliasMap.set(
|
|
221
222
|
normalizeName(tagBlockMatch.alias),
|
|
222
|
-
tagBlockMatch.name
|
|
223
|
+
tagAttrKey(tagBlockMatch.name)
|
|
223
224
|
);
|
|
224
225
|
}
|
|
225
226
|
// §1.4 dispatch: register canonical name so `<name>:` triggers
|
|
226
227
|
// the metadata cut even without an explicit `as <alias>`.
|
|
227
228
|
metaAliasMap.set(
|
|
228
229
|
normalizeName(tagBlockMatch.name),
|
|
229
|
-
tagBlockMatch.name
|
|
230
|
+
tagAttrKey(tagBlockMatch.name)
|
|
230
231
|
);
|
|
231
232
|
result.tagGroups.push(currentTagGroup);
|
|
232
233
|
continue;
|