@open-agent-toolkit/cli 0.2.72 → 0.2.74
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/assets/agents/oat-reviewer.md +4 -3
- package/assets/agents/recon-worker.md +42 -8
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +3 -56
- package/assets/docs/cli-utilities/tool-packs.md +4 -4
- package/assets/docs/contributing/code.md +2 -2
- package/assets/docs/contributing/index.md +0 -1
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/troubleshooting.md +17 -40
- package/assets/docs/workflows/projects/artifacts.md +16 -34
- package/assets/docs/workflows/projects/lifecycle.md +20 -19
- package/assets/docs/workflows/skills/explainer-kit.md +180 -440
- package/assets/docs/workflows/skills/index.md +1 -2
- package/assets/docs/workflows/skills/recon.md +114 -23
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +96 -171
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
- package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
- package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
- package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
- package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
- package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
- package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
- package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
- package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
- package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
- package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
- package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
- package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
- package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
- package/assets/skills/oat-project-complete/SKILL.md +168 -349
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-plan/SKILL.md +5 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
- package/assets/skills/oat-project-summary/SKILL.md +35 -25
- package/assets/skills/oat-wave-execute/SKILL.md +11 -36
- package/assets/skills/oat-wave-program/SKILL.md +13 -37
- package/assets/skills/recon/SKILL.md +103 -40
- package/assets/skills/recon/references/packet-contract.md +150 -67
- package/assets/skills/recon/references/profiles.md +38 -16
- package/assets/skills/recon/references/worker-contract.md +39 -8
- package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
- package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
- package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
- package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
- package/assets/skills/recon/scripts/render-packet.mjs +69 -1
- package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
- package/assets/skills/subagent-orchestration/SKILL.md +18 -9
- package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +9 -113
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +32 -184
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
- package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
- package/dist/config/oat-config.d.ts +0 -11
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +0 -34
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -8
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +2 -10
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +42 -3
- package/package.json +2 -2
- package/assets/docs/contributing/explainer-kit-verification.md +0 -133
- package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
- package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
- package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
- package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
- package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
- package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
- package/assets/skills/explainer-kit/references/contracts.md +0 -324
- package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
- package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
- package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
- package/assets/skills/explainer-kit/references/visual-review.md +0 -71
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
- package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
- package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
- package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
- package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
- package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
- package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
- package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
|
@@ -1,514 +0,0 @@
|
|
|
1
|
-
const HEADER_PATTERN = /^graph\s+(TD|LR)$/;
|
|
2
|
-
const ID_PATTERN = '[A-Za-z][A-Za-z0-9_-]*';
|
|
3
|
-
const OPERAND_PATTERN = `${ID_PATTERN}(?:\\[(?:[^\\]"]|"[^"]*")*\\]|\\((?:[^\\)"]|"[^"]*")*\\)|\\{(?:[^\\}"]|"[^"]*")*\\})?`;
|
|
4
|
-
const EDGE_PATTERN = new RegExp(
|
|
5
|
-
`^(${OPERAND_PATTERN})\\s*(-->|---)(?:\\|([^|]*)\\|)?\\s*(${OPERAND_PATTERN})$`,
|
|
6
|
-
);
|
|
7
|
-
const DECLARATION_PATTERN = new RegExp(`^${OPERAND_PATTERN}$`);
|
|
8
|
-
const NODE_PATTERN = new RegExp(
|
|
9
|
-
`^(${ID_PATTERN})(?:\\[((?:[^\\]"]|"[^"]*")*)\\]|\\(((?:[^\\)"]|"[^"]*")*)\\)|\\{((?:[^\\}"]|"[^"]*")*)\\})?$`,
|
|
10
|
-
);
|
|
11
|
-
const UNSUPPORTED_PATTERN =
|
|
12
|
-
/^(?:subgraph|end\b|classDef\b|class\b|style\b|linkStyle\b|click\b|sequenceDiagram\b|stateDiagram(?:-v2)?\b|flowchart\b)/i;
|
|
13
|
-
|
|
14
|
-
export function parseDiagram(source) {
|
|
15
|
-
if (typeof source !== 'string') {
|
|
16
|
-
throw new TypeError('Diagram source must be a string.');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const lines = source
|
|
20
|
-
.replaceAll('\r\n', '\n')
|
|
21
|
-
.split('\n')
|
|
22
|
-
.map((line) => line.trim())
|
|
23
|
-
.filter((line) => line && !line.startsWith('%%'));
|
|
24
|
-
const header = lines.shift() ?? '';
|
|
25
|
-
const headerMatch = header.match(HEADER_PATTERN);
|
|
26
|
-
if (!headerMatch) {
|
|
27
|
-
return degraded(
|
|
28
|
-
/^(?:sequenceDiagram|stateDiagram|flowchart)/i.test(header)
|
|
29
|
-
? `Unsupported diagram type: ${header || 'missing header'}.`
|
|
30
|
-
: 'Diagram must begin with “graph TD” or “graph LR”.',
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const nodes = new Map();
|
|
35
|
-
const edges = [];
|
|
36
|
-
for (const line of lines) {
|
|
37
|
-
if (UNSUPPORTED_PATTERN.test(line)) {
|
|
38
|
-
return degraded(`Unsupported diagram construct: ${line}.`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const edge = line.match(EDGE_PATTERN);
|
|
42
|
-
if (edge) {
|
|
43
|
-
const from = parseNode(edge[1]);
|
|
44
|
-
const to = parseNode(edge[4]);
|
|
45
|
-
if (!from || !to) return degraded(`Unsupported diagram syntax: ${line}.`);
|
|
46
|
-
registerNode(nodes, from);
|
|
47
|
-
registerNode(nodes, to);
|
|
48
|
-
edges.push({
|
|
49
|
-
from: from.id,
|
|
50
|
-
to: to.id,
|
|
51
|
-
kind: edge[2] === '-->' ? 'arrow' : 'line',
|
|
52
|
-
label: unquote(edge[3]?.trim() ?? ''),
|
|
53
|
-
});
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (DECLARATION_PATTERN.test(line)) {
|
|
58
|
-
const node = parseNode(line);
|
|
59
|
-
if (!node) return degraded(`Unsupported diagram syntax: ${line}.`);
|
|
60
|
-
registerNode(nodes, node);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return degraded(`Unsupported diagram syntax: ${line}.`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (nodes.size === 0) {
|
|
68
|
-
return degraded('Diagram requires at least one node declaration or edge.');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const topology = analyzeTopology([...nodes.values()], edges);
|
|
72
|
-
return {
|
|
73
|
-
valid: true,
|
|
74
|
-
direction: headerMatch[1],
|
|
75
|
-
nodes: [...nodes.values()],
|
|
76
|
-
edges,
|
|
77
|
-
inlineSupported: topology.kind === 'linear',
|
|
78
|
-
topology,
|
|
79
|
-
warnings: [],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function renderDiagram(source, { theme } = {}) {
|
|
84
|
-
const parsed = parseDiagram(source);
|
|
85
|
-
if (!parsed.valid) {
|
|
86
|
-
return {
|
|
87
|
-
html: `<div class="diagram-fallback" role="note"><p class="diagram-warning">${escapeHtml(parsed.warnings[0].message)}</p><pre><code>${escapeHtml(source)}</code></pre></div>`,
|
|
88
|
-
warnings: parsed.warnings,
|
|
89
|
-
degraded: true,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
if (!parsed.inlineSupported) {
|
|
93
|
-
const features = parsed.topology.features.join(', ');
|
|
94
|
-
return {
|
|
95
|
-
html: `<div class="diagram-fallback" role="note"><p class="diagram-warning">${escapeHtml(`Non-linear diagram topology (${features}) requires artistic composition.`)}</p><pre><code>${escapeHtml(source)}</code></pre></div>`,
|
|
96
|
-
warnings: [
|
|
97
|
-
{
|
|
98
|
-
code: 'non-linear-diagram-reroute',
|
|
99
|
-
message: `Non-linear diagram topology (${features}) requires artistic composition.`,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
degraded: true,
|
|
103
|
-
reroute: {
|
|
104
|
-
target: 'artistic',
|
|
105
|
-
source,
|
|
106
|
-
graph: {
|
|
107
|
-
direction: parsed.direction,
|
|
108
|
-
nodes: structuredClone(parsed.nodes),
|
|
109
|
-
edges: structuredClone(parsed.edges),
|
|
110
|
-
},
|
|
111
|
-
topology: structuredClone(parsed.topology),
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const horizontal = parsed.direction === 'LR';
|
|
117
|
-
const nodeWidth = 180;
|
|
118
|
-
const nodeHeight = 72;
|
|
119
|
-
const gap = 90;
|
|
120
|
-
const margin = 50;
|
|
121
|
-
const nodeById = new Map(parsed.nodes.map((node) => [node.id, node]));
|
|
122
|
-
const orderedNodes = parsed.topology.order.map((id) => nodeById.get(id));
|
|
123
|
-
const positions = new Map(
|
|
124
|
-
orderedNodes.map((node, index) => [
|
|
125
|
-
node.id,
|
|
126
|
-
horizontal
|
|
127
|
-
? { x: margin + index * (nodeWidth + gap), y: margin }
|
|
128
|
-
: { x: margin, y: margin + index * (nodeHeight + gap) },
|
|
129
|
-
]),
|
|
130
|
-
);
|
|
131
|
-
const width = horizontal
|
|
132
|
-
? margin * 2 +
|
|
133
|
-
orderedNodes.length * nodeWidth +
|
|
134
|
-
(orderedNodes.length - 1) * gap
|
|
135
|
-
: margin * 2 + nodeWidth;
|
|
136
|
-
const height = horizontal
|
|
137
|
-
? margin * 2 + nodeHeight
|
|
138
|
-
: margin * 2 +
|
|
139
|
-
orderedNodes.length * nodeHeight +
|
|
140
|
-
(orderedNodes.length - 1) * gap;
|
|
141
|
-
const markerId = `diagram-arrow-${stableHash(source)}`;
|
|
142
|
-
const mode = theme?.modes?.[theme.defaultMode];
|
|
143
|
-
const panel = mode?.surface?.panel ?? '#ffffff';
|
|
144
|
-
const ink = mode?.ink?.primary ?? '#172033';
|
|
145
|
-
const accent = mode?.accent?.primary ?? '#365270';
|
|
146
|
-
const muted = mode?.ink?.muted ?? '#526071';
|
|
147
|
-
const edges = parsed.edges
|
|
148
|
-
.map((edge) =>
|
|
149
|
-
renderEdge(edge, positions, {
|
|
150
|
-
horizontal,
|
|
151
|
-
nodeWidth,
|
|
152
|
-
nodeHeight,
|
|
153
|
-
markerId,
|
|
154
|
-
}),
|
|
155
|
-
)
|
|
156
|
-
.join('');
|
|
157
|
-
const nodes = orderedNodes
|
|
158
|
-
.map((node) =>
|
|
159
|
-
renderNode(node, positions.get(node.id), { nodeWidth, nodeHeight }),
|
|
160
|
-
)
|
|
161
|
-
.join('');
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
html: `<svg class="narrative-diagram" data-direction="${parsed.direction}" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-label="Diagram"><style>
|
|
165
|
-
.diagram-node-shape { fill: var(--panel, ${panel}); stroke: var(--accent, ${accent}); stroke-width: 2; }
|
|
166
|
-
.diagram-node-label { fill: var(--ink, ${ink}); font: 14px var(--sans, system-ui, sans-serif); text-anchor: middle; dominant-baseline: middle; }
|
|
167
|
-
.diagram-edge { fill: none; stroke: var(--muted, ${muted}); stroke-width: 2; }
|
|
168
|
-
.diagram-edge-label { fill: var(--muted, ${muted}); font: 12px var(--mono, ui-monospace, monospace); text-anchor: middle; }
|
|
169
|
-
</style><defs><marker id="${markerId}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="var(--muted, ${muted})"></path></marker></defs>${edges}${nodes}</svg>`,
|
|
170
|
-
warnings: [],
|
|
171
|
-
degraded: false,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function graphSemanticsForArtisticAuthor(diagrams) {
|
|
176
|
-
if (
|
|
177
|
-
!Array.isArray(diagrams) ||
|
|
178
|
-
diagrams.some(({ valid }) => valid !== true)
|
|
179
|
-
) {
|
|
180
|
-
throw topologyError(
|
|
181
|
-
'Planner-owned artistic diagrams must use the supported graph grammar.',
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
return diagrams
|
|
185
|
-
.filter(({ inlineSupported }) => inlineSupported === false)
|
|
186
|
-
.map(({ direction, nodes, edges, topology }) =>
|
|
187
|
-
deepFreeze({
|
|
188
|
-
direction,
|
|
189
|
-
nodes: structuredClone(nodes),
|
|
190
|
-
edges: structuredClone(edges),
|
|
191
|
-
topology: structuredClone(topology),
|
|
192
|
-
}),
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function assertAuthoredGraphSemantics(html, graphSemantics) {
|
|
197
|
-
if (
|
|
198
|
-
typeof html !== 'string' ||
|
|
199
|
-
!Array.isArray(graphSemantics) ||
|
|
200
|
-
graphSemantics.length !== 1
|
|
201
|
-
) {
|
|
202
|
-
throw topologyError(
|
|
203
|
-
'Artistic graph validation requires one unambiguous planned graph.',
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
const [planned] = graphSemantics;
|
|
207
|
-
const tags = html.match(/<[^>]+>/g) ?? [];
|
|
208
|
-
const directionTags = tags.filter((tag) => tag.includes('data-direction'));
|
|
209
|
-
const observedDirections = directionTags.flatMap((tag) =>
|
|
210
|
-
attributeValues(tag, 'data-direction'),
|
|
211
|
-
);
|
|
212
|
-
if (
|
|
213
|
-
directionTags.length !== 1 ||
|
|
214
|
-
observedDirections.length !== 1 ||
|
|
215
|
-
observedDirections[0] !== planned.direction
|
|
216
|
-
) {
|
|
217
|
-
throw topologyError(
|
|
218
|
-
'Authored graph direction does not exactly match the planned graph.',
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const nodeTags = tags.filter((tag) => tag.includes('data-node'));
|
|
223
|
-
const observedNodes = nodeTags.map((tag) => {
|
|
224
|
-
const id = canonicalAttribute(tag, 'data-node', {
|
|
225
|
-
pattern: new RegExp(`^${ID_PATTERN}$`),
|
|
226
|
-
});
|
|
227
|
-
const label = canonicalAttribute(tag, 'data-node-label');
|
|
228
|
-
const shape = canonicalAttribute(tag, 'data-node-shape', {
|
|
229
|
-
allowed: ['rectangle', 'rounded', 'diamond'],
|
|
230
|
-
});
|
|
231
|
-
const explicit = canonicalAttribute(tag, 'data-node-explicit', {
|
|
232
|
-
allowed: ['true', 'false'],
|
|
233
|
-
});
|
|
234
|
-
return canonicalTuple([id, label, shape, explicit]);
|
|
235
|
-
});
|
|
236
|
-
const edgeTags = tags.filter((tag) =>
|
|
237
|
-
['data-from', 'data-to', 'data-edge-kind', 'data-edge-label'].some((name) =>
|
|
238
|
-
tag.includes(name),
|
|
239
|
-
),
|
|
240
|
-
);
|
|
241
|
-
const observedEdges = edgeTags.map((tag) => {
|
|
242
|
-
const from = canonicalAttribute(tag, 'data-from', {
|
|
243
|
-
pattern: new RegExp(`^${ID_PATTERN}$`),
|
|
244
|
-
});
|
|
245
|
-
const to = canonicalAttribute(tag, 'data-to', {
|
|
246
|
-
pattern: new RegExp(`^${ID_PATTERN}$`),
|
|
247
|
-
});
|
|
248
|
-
const kind = canonicalAttribute(tag, 'data-edge-kind', {
|
|
249
|
-
allowed: ['arrow', 'line'],
|
|
250
|
-
});
|
|
251
|
-
const label = canonicalAttribute(tag, 'data-edge-label');
|
|
252
|
-
return canonicalTuple([from, to, kind, label]);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
const expectedNodes = planned.nodes.map(({ id, label, shape, explicit }) =>
|
|
256
|
-
canonicalTuple([id, escapeAttribute(label), shape, String(explicit)]),
|
|
257
|
-
);
|
|
258
|
-
const expectedEdges = planned.edges.map(({ from, to, kind, label }) =>
|
|
259
|
-
canonicalTuple([from, to, kind, escapeAttribute(label)]),
|
|
260
|
-
);
|
|
261
|
-
if (
|
|
262
|
-
!sameMultiset(observedNodes, expectedNodes) ||
|
|
263
|
-
!sameMultiset(observedEdges, expectedEdges)
|
|
264
|
-
) {
|
|
265
|
-
throw topologyError(
|
|
266
|
-
'Authored graph node or edge multiset does not exactly match the planned graph.',
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function analyzeTopology(nodes, edges) {
|
|
272
|
-
const nodeIds = nodes.map(({ id }) => id);
|
|
273
|
-
const incoming = new Map(nodeIds.map((id) => [id, []]));
|
|
274
|
-
const outgoing = new Map(nodeIds.map((id) => [id, []]));
|
|
275
|
-
for (const edge of edges) {
|
|
276
|
-
outgoing.get(edge.from).push(edge.to);
|
|
277
|
-
incoming.get(edge.to).push(edge.from);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const branchNodes = nodeIds.filter((id) => outgoing.get(id).length > 1);
|
|
281
|
-
const fanInNodes = nodeIds.filter((id) => incoming.get(id).length > 1);
|
|
282
|
-
const cycle = hasDirectedCycle(nodeIds, outgoing);
|
|
283
|
-
const connected = isWeaklyConnected(nodeIds, incoming, outgoing);
|
|
284
|
-
const features = [
|
|
285
|
-
...(branchNodes.length > 0 ? ['branch'] : []),
|
|
286
|
-
...(fanInNodes.length > 0 ? ['fan-in'] : []),
|
|
287
|
-
...(cycle ? ['cycle'] : []),
|
|
288
|
-
...(!connected ? ['disconnected'] : []),
|
|
289
|
-
];
|
|
290
|
-
const order =
|
|
291
|
-
features.length === 0 ? linearOrder(nodeIds, incoming, outgoing) : [];
|
|
292
|
-
if (features.length === 0 && order.length !== nodeIds.length) {
|
|
293
|
-
features.push('non-linear');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
kind: features.length === 0 ? 'linear' : 'non-linear',
|
|
298
|
-
features,
|
|
299
|
-
branchNodes,
|
|
300
|
-
fanInNodes,
|
|
301
|
-
cycle,
|
|
302
|
-
order: features.length === 0 ? order : [],
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function hasDirectedCycle(nodeIds, outgoing) {
|
|
307
|
-
const visiting = new Set();
|
|
308
|
-
const visited = new Set();
|
|
309
|
-
const visit = (id) => {
|
|
310
|
-
if (visiting.has(id)) return true;
|
|
311
|
-
if (visited.has(id)) return false;
|
|
312
|
-
visiting.add(id);
|
|
313
|
-
for (const next of outgoing.get(id)) {
|
|
314
|
-
if (visit(next)) return true;
|
|
315
|
-
}
|
|
316
|
-
visiting.delete(id);
|
|
317
|
-
visited.add(id);
|
|
318
|
-
return false;
|
|
319
|
-
};
|
|
320
|
-
return nodeIds.some((id) => visit(id));
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function isWeaklyConnected(nodeIds, incoming, outgoing) {
|
|
324
|
-
if (nodeIds.length <= 1) return true;
|
|
325
|
-
const seen = new Set();
|
|
326
|
-
const pending = [nodeIds[0]];
|
|
327
|
-
while (pending.length > 0) {
|
|
328
|
-
const id = pending.pop();
|
|
329
|
-
if (seen.has(id)) continue;
|
|
330
|
-
seen.add(id);
|
|
331
|
-
pending.push(...incoming.get(id), ...outgoing.get(id));
|
|
332
|
-
}
|
|
333
|
-
return seen.size === nodeIds.length;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function linearOrder(nodeIds, incoming, outgoing) {
|
|
337
|
-
if (nodeIds.length === 1 && incoming.get(nodeIds[0]).length === 0) {
|
|
338
|
-
return [...nodeIds];
|
|
339
|
-
}
|
|
340
|
-
const starts = nodeIds.filter(
|
|
341
|
-
(id) => incoming.get(id).length === 0 && outgoing.get(id).length === 1,
|
|
342
|
-
);
|
|
343
|
-
const ends = nodeIds.filter(
|
|
344
|
-
(id) => incoming.get(id).length === 1 && outgoing.get(id).length === 0,
|
|
345
|
-
);
|
|
346
|
-
const middleIsLinear = nodeIds
|
|
347
|
-
.filter((id) => !starts.includes(id) && !ends.includes(id))
|
|
348
|
-
.every(
|
|
349
|
-
(id) => incoming.get(id).length === 1 && outgoing.get(id).length === 1,
|
|
350
|
-
);
|
|
351
|
-
if (
|
|
352
|
-
starts.length !== 1 ||
|
|
353
|
-
ends.length !== 1 ||
|
|
354
|
-
!middleIsLinear ||
|
|
355
|
-
edgesFor(outgoing) !== nodeIds.length - 1
|
|
356
|
-
) {
|
|
357
|
-
return [];
|
|
358
|
-
}
|
|
359
|
-
const order = [];
|
|
360
|
-
let current = starts[0];
|
|
361
|
-
while (current !== undefined && !order.includes(current)) {
|
|
362
|
-
order.push(current);
|
|
363
|
-
current = outgoing.get(current)[0];
|
|
364
|
-
}
|
|
365
|
-
return order;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function edgesFor(outgoing) {
|
|
369
|
-
return [...outgoing.values()].reduce(
|
|
370
|
-
(total, targets) => total + targets.length,
|
|
371
|
-
0,
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function parseNode(value) {
|
|
376
|
-
const match = value.match(NODE_PATTERN);
|
|
377
|
-
if (!match) return null;
|
|
378
|
-
const [, id, rectangle, rounded, diamond] = match;
|
|
379
|
-
const explicitLabel = rectangle ?? rounded ?? diamond;
|
|
380
|
-
return {
|
|
381
|
-
id,
|
|
382
|
-
label: unquote(explicitLabel ?? id),
|
|
383
|
-
shape:
|
|
384
|
-
diamond !== undefined
|
|
385
|
-
? 'diamond'
|
|
386
|
-
: rounded !== undefined
|
|
387
|
-
? 'rounded'
|
|
388
|
-
: 'rectangle',
|
|
389
|
-
explicit: explicitLabel !== undefined,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function registerNode(nodes, candidate) {
|
|
394
|
-
const current = nodes.get(candidate.id);
|
|
395
|
-
if (!current || candidate.explicit) nodes.set(candidate.id, candidate);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function renderNode(node, position, { nodeWidth, nodeHeight }) {
|
|
399
|
-
const centerX = position.x + nodeWidth / 2;
|
|
400
|
-
const centerY = position.y + nodeHeight / 2;
|
|
401
|
-
let shape;
|
|
402
|
-
if (node.shape === 'diamond') {
|
|
403
|
-
shape = `<polygon class="diagram-node-shape" points="${centerX},${position.y} ${position.x + nodeWidth},${centerY} ${centerX},${position.y + nodeHeight} ${position.x},${centerY}"></polygon>`;
|
|
404
|
-
} else {
|
|
405
|
-
shape = `<rect class="diagram-node-shape" x="${position.x}" y="${position.y}" width="${nodeWidth}" height="${nodeHeight}"${node.shape === 'rounded' ? ' rx="24"' : ''}></rect>`;
|
|
406
|
-
}
|
|
407
|
-
return `<g class="diagram-node" data-node="${escapeAttribute(node.id)}" data-node-label="${escapeAttribute(node.label)}" data-node-shape="${node.shape}" data-node-explicit="${String(node.explicit)}">${shape}<text class="diagram-node-label" x="${centerX}" y="${centerY}">${escapeHtml(node.label)}</text></g>`;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function renderEdge(
|
|
411
|
-
edge,
|
|
412
|
-
positions,
|
|
413
|
-
{ horizontal, nodeWidth, nodeHeight, markerId },
|
|
414
|
-
) {
|
|
415
|
-
const from = positions.get(edge.from);
|
|
416
|
-
const to = positions.get(edge.to);
|
|
417
|
-
const start = horizontal
|
|
418
|
-
? { x: from.x + nodeWidth, y: from.y + nodeHeight / 2 }
|
|
419
|
-
: { x: from.x + nodeWidth / 2, y: from.y + nodeHeight };
|
|
420
|
-
const end = horizontal
|
|
421
|
-
? { x: to.x, y: to.y + nodeHeight / 2 }
|
|
422
|
-
: { x: to.x + nodeWidth / 2, y: to.y };
|
|
423
|
-
const label = edge.label
|
|
424
|
-
? `<text class="diagram-edge-label" x="${(start.x + end.x) / 2}" y="${(start.y + end.y) / 2 - 8}">${escapeHtml(edge.label)}</text>`
|
|
425
|
-
: '';
|
|
426
|
-
return `<g class="diagram-connection" data-from="${escapeAttribute(edge.from)}" data-to="${escapeAttribute(edge.to)}" data-edge-kind="${edge.kind}" data-edge-label="${escapeAttribute(edge.label)}"><path class="diagram-edge" d="M ${start.x} ${start.y} L ${end.x} ${end.y}"${edge.kind === 'arrow' ? ` marker-end="url(#${markerId})"` : ''}></path>${label}</g>`;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function degraded(message) {
|
|
430
|
-
return {
|
|
431
|
-
valid: false,
|
|
432
|
-
direction: null,
|
|
433
|
-
nodes: [],
|
|
434
|
-
edges: [],
|
|
435
|
-
warnings: [{ code: 'unsupported-diagram', message }],
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function unquote(value) {
|
|
440
|
-
return value.startsWith('"') && value.endsWith('"')
|
|
441
|
-
? value.slice(1, -1)
|
|
442
|
-
: value;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function stableHash(value) {
|
|
446
|
-
let hash = 2166136261;
|
|
447
|
-
for (const character of value) {
|
|
448
|
-
hash ^= character.codePointAt(0);
|
|
449
|
-
hash = Math.imul(hash, 16777619);
|
|
450
|
-
}
|
|
451
|
-
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function attributeValues(tag, name) {
|
|
455
|
-
return [
|
|
456
|
-
...tag.matchAll(
|
|
457
|
-
new RegExp(`(?:^|\\s)${name}\\s*=\\s*(["'])([^"'<>]*)\\1`, 'g'),
|
|
458
|
-
),
|
|
459
|
-
].map((match) => match[2]);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function canonicalAttribute(tag, name, { pattern, allowed } = {}) {
|
|
463
|
-
const values = attributeValues(tag, name);
|
|
464
|
-
if (
|
|
465
|
-
values.length !== 1 ||
|
|
466
|
-
(pattern && !pattern.test(values[0])) ||
|
|
467
|
-
(allowed && !allowed.includes(values[0]))
|
|
468
|
-
) {
|
|
469
|
-
throw topologyError(
|
|
470
|
-
`Authored graph contains a malformed or ambiguous ${name} observation.`,
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
return values[0];
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function canonicalTuple(values) {
|
|
477
|
-
return JSON.stringify(values);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function sameMultiset(actual, expected) {
|
|
481
|
-
return (
|
|
482
|
-
actual.length === expected.length &&
|
|
483
|
-
[...actual]
|
|
484
|
-
.sort()
|
|
485
|
-
.every((value, index) => value === [...expected].sort()[index])
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function topologyError(message) {
|
|
490
|
-
const error = new Error(message);
|
|
491
|
-
error.code = 'E_DIAGRAM_TOPOLOGY';
|
|
492
|
-
return error;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function deepFreeze(value) {
|
|
496
|
-
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
497
|
-
Object.freeze(value);
|
|
498
|
-
for (const child of Object.values(value)) deepFreeze(child);
|
|
499
|
-
}
|
|
500
|
-
return value;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function escapeHtml(value) {
|
|
504
|
-
return String(value)
|
|
505
|
-
.replaceAll('&', '&')
|
|
506
|
-
.replaceAll('<', '<')
|
|
507
|
-
.replaceAll('>', '>')
|
|
508
|
-
.replaceAll('"', '"')
|
|
509
|
-
.replaceAll("'", ''');
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function escapeAttribute(value) {
|
|
513
|
-
return escapeHtml(value);
|
|
514
|
-
}
|