coalesce-transform-mcp 0.1.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 (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -0
  3. package/dist/cache-dir.d.ts +26 -0
  4. package/dist/cache-dir.js +106 -0
  5. package/dist/client.d.ts +25 -0
  6. package/dist/client.js +212 -0
  7. package/dist/coalesce/api/environments.d.ts +20 -0
  8. package/dist/coalesce/api/environments.js +15 -0
  9. package/dist/coalesce/api/git-accounts.d.ts +21 -0
  10. package/dist/coalesce/api/git-accounts.js +21 -0
  11. package/dist/coalesce/api/jobs.d.ts +25 -0
  12. package/dist/coalesce/api/jobs.js +21 -0
  13. package/dist/coalesce/api/nodes.d.ts +29 -0
  14. package/dist/coalesce/api/nodes.js +33 -0
  15. package/dist/coalesce/api/projects.d.ts +22 -0
  16. package/dist/coalesce/api/projects.js +25 -0
  17. package/dist/coalesce/api/runs.d.ts +19 -0
  18. package/dist/coalesce/api/runs.js +34 -0
  19. package/dist/coalesce/api/subgraphs.d.ts +20 -0
  20. package/dist/coalesce/api/subgraphs.js +17 -0
  21. package/dist/coalesce/api/users.d.ts +30 -0
  22. package/dist/coalesce/api/users.js +31 -0
  23. package/dist/coalesce/types.d.ts +298 -0
  24. package/dist/coalesce/types.js +746 -0
  25. package/dist/generated/.gitkeep +0 -0
  26. package/dist/generated/node-type-corpus.json +42656 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +10 -0
  29. package/dist/mcp/cache.d.ts +3 -0
  30. package/dist/mcp/cache.js +137 -0
  31. package/dist/mcp/environments.d.ts +3 -0
  32. package/dist/mcp/environments.js +61 -0
  33. package/dist/mcp/git-accounts.d.ts +3 -0
  34. package/dist/mcp/git-accounts.js +70 -0
  35. package/dist/mcp/jobs.d.ts +3 -0
  36. package/dist/mcp/jobs.js +77 -0
  37. package/dist/mcp/node-type-corpus.d.ts +3 -0
  38. package/dist/mcp/node-type-corpus.js +173 -0
  39. package/dist/mcp/nodes.d.ts +3 -0
  40. package/dist/mcp/nodes.js +341 -0
  41. package/dist/mcp/pipelines.d.ts +3 -0
  42. package/dist/mcp/pipelines.js +342 -0
  43. package/dist/mcp/projects.d.ts +3 -0
  44. package/dist/mcp/projects.js +70 -0
  45. package/dist/mcp/repo-node-types.d.ts +135 -0
  46. package/dist/mcp/repo-node-types.js +387 -0
  47. package/dist/mcp/runs.d.ts +3 -0
  48. package/dist/mcp/runs.js +92 -0
  49. package/dist/mcp/subgraphs.d.ts +3 -0
  50. package/dist/mcp/subgraphs.js +60 -0
  51. package/dist/mcp/users.d.ts +3 -0
  52. package/dist/mcp/users.js +107 -0
  53. package/dist/prompts/index.d.ts +2 -0
  54. package/dist/prompts/index.js +58 -0
  55. package/dist/resources/context/aggregation-patterns.md +145 -0
  56. package/dist/resources/context/data-engineering-principles.md +183 -0
  57. package/dist/resources/context/hydrated-metadata.md +92 -0
  58. package/dist/resources/context/id-discovery.md +64 -0
  59. package/dist/resources/context/intelligent-node-configuration.md +162 -0
  60. package/dist/resources/context/node-creation-decision-tree.md +156 -0
  61. package/dist/resources/context/node-operations.md +316 -0
  62. package/dist/resources/context/node-payloads.md +114 -0
  63. package/dist/resources/context/node-type-corpus.md +166 -0
  64. package/dist/resources/context/node-type-selection-guide.md +96 -0
  65. package/dist/resources/context/overview.md +135 -0
  66. package/dist/resources/context/pipeline-workflows.md +355 -0
  67. package/dist/resources/context/run-operations.md +55 -0
  68. package/dist/resources/context/sql-bigquery.md +41 -0
  69. package/dist/resources/context/sql-databricks.md +40 -0
  70. package/dist/resources/context/sql-platform-selection.md +70 -0
  71. package/dist/resources/context/sql-snowflake.md +43 -0
  72. package/dist/resources/context/storage-mappings.md +49 -0
  73. package/dist/resources/context/tool-usage.md +98 -0
  74. package/dist/resources/index.d.ts +5 -0
  75. package/dist/resources/index.js +254 -0
  76. package/dist/schemas/node-payloads.d.ts +5019 -0
  77. package/dist/schemas/node-payloads.js +147 -0
  78. package/dist/server.d.ts +7 -0
  79. package/dist/server.js +63 -0
  80. package/dist/services/cache/snapshots.d.ts +108 -0
  81. package/dist/services/cache/snapshots.js +275 -0
  82. package/dist/services/config/context-analyzer.d.ts +14 -0
  83. package/dist/services/config/context-analyzer.js +76 -0
  84. package/dist/services/config/field-classifier.d.ts +23 -0
  85. package/dist/services/config/field-classifier.js +47 -0
  86. package/dist/services/config/intelligent.d.ts +55 -0
  87. package/dist/services/config/intelligent.js +306 -0
  88. package/dist/services/config/rules.d.ts +6 -0
  89. package/dist/services/config/rules.js +44 -0
  90. package/dist/services/config/schema-resolver.d.ts +18 -0
  91. package/dist/services/config/schema-resolver.js +80 -0
  92. package/dist/services/corpus/loader.d.ts +56 -0
  93. package/dist/services/corpus/loader.js +25 -0
  94. package/dist/services/corpus/search.d.ts +49 -0
  95. package/dist/services/corpus/search.js +69 -0
  96. package/dist/services/corpus/templates.d.ts +4 -0
  97. package/dist/services/corpus/templates.js +11 -0
  98. package/dist/services/pipelines/execution.d.ts +20 -0
  99. package/dist/services/pipelines/execution.js +290 -0
  100. package/dist/services/pipelines/node-type-intent.d.ts +96 -0
  101. package/dist/services/pipelines/node-type-intent.js +356 -0
  102. package/dist/services/pipelines/node-type-selection.d.ts +66 -0
  103. package/dist/services/pipelines/node-type-selection.js +758 -0
  104. package/dist/services/pipelines/planning.d.ts +543 -0
  105. package/dist/services/pipelines/planning.js +1839 -0
  106. package/dist/services/policies/sql-override.d.ts +7 -0
  107. package/dist/services/policies/sql-override.js +109 -0
  108. package/dist/services/repo/operations.d.ts +6 -0
  109. package/dist/services/repo/operations.js +10 -0
  110. package/dist/services/repo/parser.d.ts +70 -0
  111. package/dist/services/repo/parser.js +365 -0
  112. package/dist/services/repo/path.d.ts +2 -0
  113. package/dist/services/repo/path.js +58 -0
  114. package/dist/services/templates/nodes.d.ts +50 -0
  115. package/dist/services/templates/nodes.js +336 -0
  116. package/dist/services/workspace/analysis.d.ts +56 -0
  117. package/dist/services/workspace/analysis.js +151 -0
  118. package/dist/services/workspace/mutations.d.ts +150 -0
  119. package/dist/services/workspace/mutations.js +1718 -0
  120. package/dist/utils.d.ts +5 -0
  121. package/dist/utils.js +7 -0
  122. package/dist/workflows/get-environment-overview.d.ts +9 -0
  123. package/dist/workflows/get-environment-overview.js +23 -0
  124. package/dist/workflows/get-run-details.d.ts +10 -0
  125. package/dist/workflows/get-run-details.js +28 -0
  126. package/dist/workflows/progress.d.ts +20 -0
  127. package/dist/workflows/progress.js +54 -0
  128. package/dist/workflows/retry-and-wait.d.ts +13 -0
  129. package/dist/workflows/retry-and-wait.js +139 -0
  130. package/dist/workflows/run-and-wait.d.ts +13 -0
  131. package/dist/workflows/run-and-wait.js +141 -0
  132. package/dist/workflows/run-status.d.ts +10 -0
  133. package/dist/workflows/run-status.js +27 -0
  134. package/package.json +34 -0
@@ -0,0 +1,7 @@
1
+ export type SqlOverridePolicySanitizationResult = {
2
+ nodeDefinition: Record<string, unknown>;
3
+ warnings: string[];
4
+ };
5
+ export declare function filterSqlOverrideControls(values: string[]): string[];
6
+ export declare function sanitizeNodeDefinitionSqlOverridePolicy(nodeDefinition: Record<string, unknown>): SqlOverridePolicySanitizationResult;
7
+ export declare function assertNoSqlOverridePayload(value: unknown, context: string): void;
@@ -0,0 +1,109 @@
1
+ import { isPlainObject } from "../../utils.js";
2
+ const SQL_OVERRIDE_CONTROL_TYPE = "overrideSQLToggle";
3
+ function cloneValue(value) {
4
+ return JSON.parse(JSON.stringify(value));
5
+ }
6
+ export function filterSqlOverrideControls(values) {
7
+ return values.filter((value) => value !== SQL_OVERRIDE_CONTROL_TYPE);
8
+ }
9
+ export function sanitizeNodeDefinitionSqlOverridePolicy(nodeDefinition) {
10
+ const cloned = cloneValue(nodeDefinition);
11
+ let removedControlCount = 0;
12
+ let rewrittenExpressionCount = 0;
13
+ if (Array.isArray(cloned.config)) {
14
+ cloned.config = cloned.config.flatMap((group) => {
15
+ if (!isPlainObject(group)) {
16
+ return [];
17
+ }
18
+ const items = Array.isArray(group.items)
19
+ ? group.items.filter((item) => {
20
+ if (!isPlainObject(item)) {
21
+ return false;
22
+ }
23
+ if (item.type === SQL_OVERRIDE_CONTROL_TYPE) {
24
+ removedControlCount += 1;
25
+ return false;
26
+ }
27
+ return true;
28
+ })
29
+ : [];
30
+ if (items.length === 0) {
31
+ return [];
32
+ }
33
+ return [{ ...group, items }];
34
+ });
35
+ }
36
+ function sanitizeValue(value) {
37
+ if (Array.isArray(value)) {
38
+ return value.map((entry) => sanitizeValue(entry));
39
+ }
40
+ if (!isPlainObject(value)) {
41
+ return value;
42
+ }
43
+ const sanitized = {};
44
+ for (const [key, entryValue] of Object.entries(value)) {
45
+ if ((key === "enableIf" || key === "disableIf") &&
46
+ typeof entryValue === "string") {
47
+ const rewritten = entryValue.replace(/node\.override\.[A-Za-z0-9_.]+/gu, "false");
48
+ if (rewritten !== entryValue) {
49
+ rewrittenExpressionCount += 1;
50
+ }
51
+ sanitized[key] = rewritten;
52
+ continue;
53
+ }
54
+ sanitized[key] = sanitizeValue(entryValue);
55
+ }
56
+ return sanitized;
57
+ }
58
+ const sanitizedDefinition = sanitizeValue(cloned);
59
+ const warnings = [];
60
+ if (removedControlCount > 0) {
61
+ warnings.push(`Removed ${removedControlCount} SQL override control(s) from the returned node definition because SQL override is disallowed for this project.`);
62
+ }
63
+ if (rewrittenExpressionCount > 0) {
64
+ warnings.push(`Rewrote ${rewrittenExpressionCount} conditional expression(s) that referenced node.override.* so returned definitions behave as if SQL override is disabled.`);
65
+ }
66
+ return {
67
+ nodeDefinition: sanitizedDefinition,
68
+ warnings,
69
+ };
70
+ }
71
+ function formatPathSegment(segment) {
72
+ return /^\[\d+\]$/u.test(segment) ? segment : `.${segment}`;
73
+ }
74
+ function formatPath(segments) {
75
+ if (segments.length === 0) {
76
+ return "<root>";
77
+ }
78
+ return segments.reduce((path, segment, index) => {
79
+ if (index === 0 && !/^\[\d+\]$/u.test(segment)) {
80
+ return segment;
81
+ }
82
+ return `${path}${formatPathSegment(segment)}`;
83
+ }, "");
84
+ }
85
+ function collectSqlOverridePaths(value, pathSegments = []) {
86
+ if (Array.isArray(value)) {
87
+ return value.flatMap((entry, index) => collectSqlOverridePaths(entry, [...pathSegments, `[${index}]`]));
88
+ }
89
+ if (!isPlainObject(value)) {
90
+ return [];
91
+ }
92
+ const matches = [];
93
+ for (const [key, entryValue] of Object.entries(value)) {
94
+ const nextPath = [...pathSegments, key];
95
+ if (key === "overrideSQL" || key === "override") {
96
+ matches.push(formatPath(nextPath));
97
+ continue;
98
+ }
99
+ matches.push(...collectSqlOverridePaths(entryValue, nextPath));
100
+ }
101
+ return matches;
102
+ }
103
+ export function assertNoSqlOverridePayload(value, context) {
104
+ const offendingPaths = Array.from(new Set(collectSqlOverridePaths(value)));
105
+ if (offendingPaths.length === 0) {
106
+ return;
107
+ }
108
+ throw new Error(`${context} cannot set SQL override fields. Remove ${offendingPaths.join(", ")}. SQL override is intentionally disallowed in this project.`);
109
+ }
@@ -0,0 +1,6 @@
1
+ export interface RepoNodeTypeDefinition {
2
+ nodeDefinition: Record<string, unknown> | null;
3
+ resolvedNodeType: string;
4
+ warnings: string[];
5
+ }
6
+ export declare function getRepoNodeTypeDefinition(repoPath: string, nodeType: string): Promise<RepoNodeTypeDefinition>;
@@ -0,0 +1,10 @@
1
+ import { parseRepo, resolveRepoNodeType, } from "./parser.js";
2
+ export async function getRepoNodeTypeDefinition(repoPath, nodeType) {
3
+ const parsedRepo = parseRepo(repoPath);
4
+ const resolution = resolveRepoNodeType(parsedRepo, nodeType);
5
+ return {
6
+ nodeDefinition: resolution.nodeTypeRecord.nodeDefinition,
7
+ resolvedNodeType: resolution.resolvedNodeType,
8
+ warnings: resolution.nodeTypeRecord.warnings,
9
+ };
10
+ }
@@ -0,0 +1,70 @@
1
+ export type RepoOuterDefinition = {
2
+ fileVersion: unknown;
3
+ id: string | null;
4
+ isDisabled: boolean | null;
5
+ name: string | null;
6
+ type: string | null;
7
+ inputMode: string | null;
8
+ };
9
+ export type RepoNodeTypeRecord = {
10
+ dirName: string;
11
+ dirPath: string;
12
+ definitionPath: string;
13
+ createPath: string | null;
14
+ runPath: string | null;
15
+ outerDefinition: RepoOuterDefinition;
16
+ nodeMetadataSpec: string | null;
17
+ nodeDefinition: Record<string, unknown> | null;
18
+ parseError: string | null;
19
+ warnings: string[];
20
+ };
21
+ export type RepoPackageRecord = {
22
+ alias: string;
23
+ aliasSource: "name" | "filename";
24
+ packageFilePath: string;
25
+ packageID: string | null;
26
+ releaseID: string | null;
27
+ packageVariables: string | null;
28
+ enabledNodeTypeIDs: string[];
29
+ resolvedDefinitionIDs: string[];
30
+ missingDefinitionIDs: string[];
31
+ ambiguousDefinitionIDs: string[];
32
+ usageByNodeTypeID: Record<string, number>;
33
+ usageCount: number;
34
+ warnings: string[];
35
+ };
36
+ export type ParsedRepoSummary = {
37
+ repoPath: string;
38
+ resolvedRepoPath: string;
39
+ packageCount: number;
40
+ uniquePackageAliasCount: number;
41
+ nodeTypeDefinitionCount: number;
42
+ uniqueNodeTypeIDCount: number;
43
+ nodeCount: number;
44
+ warnings: string[];
45
+ };
46
+ export type ParsedRepo = {
47
+ summary: ParsedRepoSummary;
48
+ packages: RepoPackageRecord[];
49
+ nodeTypes: RepoNodeTypeRecord[];
50
+ usageCounts: Record<string, number>;
51
+ packagesByAlias: Map<string, RepoPackageRecord[]>;
52
+ nodeTypesByID: Map<string, RepoNodeTypeRecord[]>;
53
+ };
54
+ export type RepoNodeTypeResolution = {
55
+ resolutionKind: "direct";
56
+ requestedNodeType: string;
57
+ resolvedNodeType: string;
58
+ usageCount: number;
59
+ nodeTypeRecord: RepoNodeTypeRecord;
60
+ } | {
61
+ resolutionKind: "package";
62
+ requestedNodeType: string;
63
+ resolvedNodeType: string;
64
+ packageAlias: string;
65
+ packageRecord: RepoPackageRecord;
66
+ usageCount: number;
67
+ nodeTypeRecord: RepoNodeTypeRecord;
68
+ };
69
+ export declare function parseRepo(repoPath: string): ParsedRepo;
70
+ export declare function resolveRepoNodeType(parsedRepo: ParsedRepo, requestedNodeType: string): RepoNodeTypeResolution;
@@ -0,0 +1,365 @@
1
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync, } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import YAML from "yaml";
4
+ import { isPlainObject } from "../../utils.js";
5
+ function getScalarString(value) {
6
+ if (typeof value === "string") {
7
+ return value;
8
+ }
9
+ if (typeof value === "number" ||
10
+ typeof value === "boolean" ||
11
+ typeof value === "bigint") {
12
+ return String(value);
13
+ }
14
+ return null;
15
+ }
16
+ function compareStrings(left, right) {
17
+ return left.localeCompare(right, undefined, {
18
+ numeric: true,
19
+ sensitivity: "case",
20
+ });
21
+ }
22
+ function readYamlFile(filePath) {
23
+ return YAML.parse(readFileSync(filePath, "utf8"));
24
+ }
25
+ function listYamlFiles(dirPath) {
26
+ if (!existsSync(dirPath)) {
27
+ return [];
28
+ }
29
+ return readdirSync(dirPath, { withFileTypes: true })
30
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".yml"))
31
+ .map((entry) => join(dirPath, entry.name))
32
+ .sort(compareStrings);
33
+ }
34
+ function normalizeRepoPath(repoPath) {
35
+ const absolutePath = resolve(repoPath);
36
+ if (!existsSync(absolutePath)) {
37
+ throw new Error("Repo path does not exist. Check the provided path or COALESCE_REPO_PATH environment variable.");
38
+ }
39
+ const stats = statSync(absolutePath);
40
+ if (!stats.isDirectory()) {
41
+ throw new Error("Repo path is not a directory. Expected a Coalesce repo directory containing a nodeTypes/ subdirectory.");
42
+ }
43
+ const resolvedRepoPath = realpathSync(absolutePath);
44
+ const nodeTypesDir = join(resolvedRepoPath, "nodeTypes");
45
+ if (!existsSync(nodeTypesDir) || !statSync(nodeTypesDir).isDirectory()) {
46
+ throw new Error("Invalid repo path: missing nodeTypes/ subdirectory. " +
47
+ "Expected a Coalesce repo directory containing nodeTypes/.");
48
+ }
49
+ return resolvedRepoPath;
50
+ }
51
+ function buildOuterDefinition(parsed) {
52
+ return {
53
+ fileVersion: Object.prototype.hasOwnProperty.call(parsed, "fileVersion")
54
+ ? parsed.fileVersion
55
+ : null,
56
+ id: getScalarString(parsed.id),
57
+ isDisabled: typeof parsed.isDisabled === "boolean" ? parsed.isDisabled : null,
58
+ name: getScalarString(parsed.name),
59
+ type: getScalarString(parsed.type),
60
+ inputMode: getScalarString(parsed.inputMode),
61
+ };
62
+ }
63
+ function loadRepoNodeTypes(resolvedRepoPath, warnings) {
64
+ const nodeTypesDir = join(resolvedRepoPath, "nodeTypes");
65
+ return readdirSync(nodeTypesDir, { withFileTypes: true })
66
+ .filter((entry) => entry.isDirectory())
67
+ .map((entry) => entry.name)
68
+ .sort(compareStrings)
69
+ .flatMap((dirName) => {
70
+ const dirPath = join(nodeTypesDir, dirName);
71
+ const definitionPath = join(dirPath, "definition.yml");
72
+ const createPath = join(dirPath, "create.sql.j2");
73
+ const runPath = join(dirPath, "run.sql.j2");
74
+ if (!existsSync(definitionPath)) {
75
+ warnings.push(`Skipping node type directory without definition.yml: ${dirPath}`);
76
+ return [];
77
+ }
78
+ try {
79
+ const parsed = readYamlFile(definitionPath);
80
+ if (!isPlainObject(parsed)) {
81
+ warnings.push(`Skipping node type definition that did not parse to an object: ${definitionPath}`);
82
+ return [];
83
+ }
84
+ const metadata = isPlainObject(parsed.metadata) ? parsed.metadata : undefined;
85
+ const nodeMetadataSpec = metadata && typeof metadata.nodeMetadataSpec === "string"
86
+ ? metadata.nodeMetadataSpec
87
+ : null;
88
+ let nodeDefinition = null;
89
+ let parseError = null;
90
+ const recordWarnings = [];
91
+ if (!nodeMetadataSpec) {
92
+ recordWarnings.push(`Missing metadata.nodeMetadataSpec in ${definitionPath}`);
93
+ }
94
+ else {
95
+ try {
96
+ const parsedNodeDefinition = YAML.parse(nodeMetadataSpec);
97
+ if (!isPlainObject(parsedNodeDefinition)) {
98
+ throw new Error("Parsed nodeMetadataSpec was not an object");
99
+ }
100
+ nodeDefinition = parsedNodeDefinition;
101
+ }
102
+ catch (error) {
103
+ parseError = error instanceof Error ? error.message : String(error);
104
+ recordWarnings.push(`Unable to parse metadata.nodeMetadataSpec in ${definitionPath}: ${parseError}`);
105
+ }
106
+ }
107
+ if (!existsSync(createPath)) {
108
+ recordWarnings.push(`Missing create.sql.j2 for ${definitionPath}`);
109
+ }
110
+ if (!existsSync(runPath)) {
111
+ recordWarnings.push(`Missing run.sql.j2 for ${definitionPath}`);
112
+ }
113
+ return [
114
+ {
115
+ dirName,
116
+ dirPath,
117
+ definitionPath,
118
+ createPath: existsSync(createPath) ? createPath : null,
119
+ runPath: existsSync(runPath) ? runPath : null,
120
+ outerDefinition: buildOuterDefinition(parsed),
121
+ nodeMetadataSpec,
122
+ nodeDefinition,
123
+ parseError,
124
+ warnings: recordWarnings,
125
+ },
126
+ ];
127
+ }
128
+ catch (error) {
129
+ warnings.push(`Skipping unreadable node type definition ${definitionPath}: ${error instanceof Error ? error.message : String(error)}`);
130
+ return [];
131
+ }
132
+ });
133
+ }
134
+ function buildNodeTypesByID(nodeTypes) {
135
+ const nodeTypesByID = new Map();
136
+ for (const nodeType of nodeTypes) {
137
+ const id = nodeType.outerDefinition.id;
138
+ if (!id) {
139
+ continue;
140
+ }
141
+ const existing = nodeTypesByID.get(id) ?? [];
142
+ existing.push(nodeType);
143
+ nodeTypesByID.set(id, existing);
144
+ }
145
+ return nodeTypesByID;
146
+ }
147
+ function loadUsageCounts(resolvedRepoPath, warnings) {
148
+ const nodesDir = join(resolvedRepoPath, "nodes");
149
+ if (!existsSync(nodesDir) || !statSync(nodesDir).isDirectory()) {
150
+ warnings.push(`Repo ${resolvedRepoPath} is missing nodes/; usage counts are unavailable.`);
151
+ return { nodeCount: 0, usageCounts: {} };
152
+ }
153
+ const usageCounts = {};
154
+ let nodeCount = 0;
155
+ for (const filePath of listYamlFiles(nodesDir)) {
156
+ try {
157
+ const parsed = readYamlFile(filePath);
158
+ if (!isPlainObject(parsed)) {
159
+ warnings.push(`Skipping node file that did not parse to an object: ${filePath}`);
160
+ continue;
161
+ }
162
+ nodeCount += 1;
163
+ const operation = isPlainObject(parsed.operation) ? parsed.operation : undefined;
164
+ const sqlType = getScalarString(operation?.sqlType);
165
+ if (!sqlType) {
166
+ continue;
167
+ }
168
+ usageCounts[sqlType] = (usageCounts[sqlType] ?? 0) + 1;
169
+ }
170
+ catch (error) {
171
+ warnings.push(`Skipping unreadable node file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
172
+ }
173
+ }
174
+ return { nodeCount, usageCounts };
175
+ }
176
+ function loadPackages(resolvedRepoPath, nodeTypesByID, usageCounts, warnings) {
177
+ const packagesDir = join(resolvedRepoPath, "packages");
178
+ if (!existsSync(packagesDir) || !statSync(packagesDir).isDirectory()) {
179
+ warnings.push(`Repo ${resolvedRepoPath} is missing packages/; package-backed discovery is unavailable.`);
180
+ return [];
181
+ }
182
+ return listYamlFiles(packagesDir).flatMap((packageFilePath) => {
183
+ try {
184
+ const parsed = readYamlFile(packageFilePath);
185
+ if (!isPlainObject(parsed)) {
186
+ warnings.push(`Skipping package file that did not parse to an object: ${packageFilePath}`);
187
+ return [];
188
+ }
189
+ const parsedAlias = getScalarString(parsed.name);
190
+ const aliasSource = parsedAlias ? "name" : "filename";
191
+ const alias = parsedAlias ?? basename(packageFilePath, ".yml");
192
+ const config = isPlainObject(parsed.config) ? parsed.config : undefined;
193
+ const entities = isPlainObject(config?.entities) ? config.entities : undefined;
194
+ const nodeTypes = isPlainObject(entities?.nodeTypes) ? entities.nodeTypes : {};
195
+ const enabledNodeTypeIDs = Object.entries(nodeTypes)
196
+ .filter(([, value]) => !(isPlainObject(value) && value.isDisabled === true))
197
+ .map(([id]) => id)
198
+ .sort(compareStrings);
199
+ const resolvedDefinitionIDs = [];
200
+ const missingDefinitionIDs = [];
201
+ const ambiguousDefinitionIDs = [];
202
+ const usageByNodeTypeID = {};
203
+ for (const id of enabledNodeTypeIDs) {
204
+ const matches = nodeTypesByID.get(id) ?? [];
205
+ if (matches.length === 1) {
206
+ resolvedDefinitionIDs.push(id);
207
+ }
208
+ else if (matches.length === 0) {
209
+ missingDefinitionIDs.push(id);
210
+ }
211
+ else {
212
+ ambiguousDefinitionIDs.push(id);
213
+ }
214
+ usageByNodeTypeID[id] = usageCounts[`${alias}:::${id}`] ?? 0;
215
+ }
216
+ const recordWarnings = [];
217
+ if (!parsedAlias) {
218
+ recordWarnings.push(`Package ${packageFilePath} is missing name; falling back to filename alias ${alias}.`);
219
+ }
220
+ if (missingDefinitionIDs.length > 0) {
221
+ recordWarnings.push(`Package alias ${alias} enables node type IDs without committed definitions: ${missingDefinitionIDs.join(", ")}.`);
222
+ }
223
+ if (ambiguousDefinitionIDs.length > 0) {
224
+ recordWarnings.push(`Package alias ${alias} enables node type IDs with ambiguous committed definitions: ${ambiguousDefinitionIDs.join(", ")}.`);
225
+ }
226
+ return [
227
+ {
228
+ alias,
229
+ aliasSource,
230
+ packageFilePath,
231
+ packageID: getScalarString(parsed.packageID),
232
+ releaseID: getScalarString(parsed.releaseID),
233
+ packageVariables: getScalarString(config?.packageVariables),
234
+ enabledNodeTypeIDs,
235
+ resolvedDefinitionIDs,
236
+ missingDefinitionIDs,
237
+ ambiguousDefinitionIDs,
238
+ usageByNodeTypeID,
239
+ usageCount: Object.values(usageByNodeTypeID).reduce((sum, value) => sum + value, 0),
240
+ warnings: recordWarnings,
241
+ },
242
+ ];
243
+ }
244
+ catch (error) {
245
+ warnings.push(`Skipping unreadable package file ${packageFilePath}: ${error instanceof Error ? error.message : String(error)}`);
246
+ return [];
247
+ }
248
+ });
249
+ }
250
+ function buildPackagesByAlias(packages) {
251
+ const packagesByAlias = new Map();
252
+ for (const record of packages) {
253
+ const existing = packagesByAlias.get(record.alias) ?? [];
254
+ existing.push(record);
255
+ packagesByAlias.set(record.alias, existing);
256
+ }
257
+ return packagesByAlias;
258
+ }
259
+ function collectAmbiguityWarnings(packagesByAlias, nodeTypesByID) {
260
+ const warnings = [];
261
+ for (const [alias, matches] of packagesByAlias.entries()) {
262
+ if (matches.length < 2) {
263
+ continue;
264
+ }
265
+ warnings.push(`Multiple package manifests share alias ${alias}: ${matches
266
+ .map((match) => match.packageFilePath)
267
+ .join(", ")}.`);
268
+ }
269
+ for (const [id, matches] of nodeTypesByID.entries()) {
270
+ if (matches.length < 2) {
271
+ continue;
272
+ }
273
+ warnings.push(`Multiple committed nodeTypes share id ${id}: ${matches
274
+ .map((match) => match.definitionPath)
275
+ .join(", ")}.`);
276
+ }
277
+ return warnings.sort(compareStrings);
278
+ }
279
+ export function parseRepo(repoPath) {
280
+ const resolvedRepoPath = normalizeRepoPath(repoPath);
281
+ const warnings = [];
282
+ const nodeTypes = loadRepoNodeTypes(resolvedRepoPath, warnings);
283
+ const nodeTypesByID = buildNodeTypesByID(nodeTypes);
284
+ const { nodeCount, usageCounts } = loadUsageCounts(resolvedRepoPath, warnings);
285
+ const packages = loadPackages(resolvedRepoPath, nodeTypesByID, usageCounts, warnings);
286
+ const packagesByAlias = buildPackagesByAlias(packages);
287
+ warnings.push(...collectAmbiguityWarnings(packagesByAlias, nodeTypesByID));
288
+ return {
289
+ summary: {
290
+ repoPath,
291
+ resolvedRepoPath,
292
+ packageCount: packages.length,
293
+ uniquePackageAliasCount: packagesByAlias.size,
294
+ nodeTypeDefinitionCount: nodeTypes.length,
295
+ uniqueNodeTypeIDCount: nodeTypesByID.size,
296
+ nodeCount,
297
+ warnings: Array.from(new Set(warnings)),
298
+ },
299
+ packages,
300
+ nodeTypes,
301
+ usageCounts,
302
+ packagesByAlias,
303
+ nodeTypesByID,
304
+ };
305
+ }
306
+ function buildRepoResolutionError(parsedRepo, requestedNodeType, detail) {
307
+ return new Error(`Repo-backed definition could not be resolved for ${requestedNodeType} in ${parsedRepo.summary.resolvedRepoPath}. ${detail} Use the corpus tools (search-node-type-variants, get-node-type-variant, or generate-set-workspace-node-template-from-variant) as the next step.`);
308
+ }
309
+ export function resolveRepoNodeType(parsedRepo, requestedNodeType) {
310
+ const delimiterIndex = requestedNodeType.indexOf(":::");
311
+ if (delimiterIndex !== -1) {
312
+ const packageAlias = requestedNodeType.slice(0, delimiterIndex);
313
+ const definitionID = requestedNodeType.slice(delimiterIndex + 3);
314
+ if (!packageAlias || !definitionID) {
315
+ throw buildRepoResolutionError(parsedRepo, requestedNodeType, "Package-backed identifiers must use the exact alias:::id format.");
316
+ }
317
+ const packageMatches = parsedRepo.packagesByAlias.get(packageAlias) ?? [];
318
+ if (packageMatches.length === 0) {
319
+ throw buildRepoResolutionError(parsedRepo, requestedNodeType, `No committed package alias ${packageAlias} was found under packages/.`);
320
+ }
321
+ if (packageMatches.length > 1) {
322
+ throw buildRepoResolutionError(parsedRepo, requestedNodeType, `Package alias ${packageAlias} is ambiguous across ${packageMatches
323
+ .map((match) => match.packageFilePath)
324
+ .join(", ")}.`);
325
+ }
326
+ const packageRecord = packageMatches[0];
327
+ if (!packageRecord.enabledNodeTypeIDs.includes(definitionID)) {
328
+ throw buildRepoResolutionError(parsedRepo, requestedNodeType, `Package alias ${packageAlias} does not enable node type ID ${definitionID}.`);
329
+ }
330
+ const nodeTypeMatches = parsedRepo.nodeTypesByID.get(definitionID) ?? [];
331
+ if (nodeTypeMatches.length === 0) {
332
+ throw buildRepoResolutionError(parsedRepo, requestedNodeType, `Package alias ${packageAlias} enables ID ${definitionID}, but no committed nodeTypes definition with outer id ${definitionID} was found.`);
333
+ }
334
+ if (nodeTypeMatches.length > 1) {
335
+ throw buildRepoResolutionError(parsedRepo, requestedNodeType, `Package alias ${packageAlias} maps to multiple committed definitions for outer id ${definitionID}: ${nodeTypeMatches
336
+ .map((match) => match.definitionPath)
337
+ .join(", ")}.`);
338
+ }
339
+ return {
340
+ resolutionKind: "package",
341
+ requestedNodeType,
342
+ resolvedNodeType: `${packageAlias}:::${definitionID}`,
343
+ packageAlias,
344
+ packageRecord,
345
+ usageCount: parsedRepo.usageCounts[`${packageAlias}:::${definitionID}`] ?? 0,
346
+ nodeTypeRecord: nodeTypeMatches[0],
347
+ };
348
+ }
349
+ const nodeTypeMatches = parsedRepo.nodeTypesByID.get(requestedNodeType) ?? [];
350
+ if (nodeTypeMatches.length === 0) {
351
+ throw buildRepoResolutionError(parsedRepo, requestedNodeType, `No committed nodeTypes definition with outer id ${requestedNodeType} was found.`);
352
+ }
353
+ if (nodeTypeMatches.length > 1) {
354
+ throw buildRepoResolutionError(parsedRepo, requestedNodeType, `Multiple committed nodeTypes definitions share outer id ${requestedNodeType}: ${nodeTypeMatches
355
+ .map((match) => match.definitionPath)
356
+ .join(", ")}.`);
357
+ }
358
+ return {
359
+ resolutionKind: "direct",
360
+ requestedNodeType,
361
+ resolvedNodeType: requestedNodeType,
362
+ usageCount: parsedRepo.usageCounts[requestedNodeType] ?? 0,
363
+ nodeTypeRecord: nodeTypeMatches[0],
364
+ };
365
+ }
@@ -0,0 +1,2 @@
1
+ export declare function resolveOptionalRepoPathInput(repoPath?: string): string | undefined;
2
+ export declare function resolveRepoPathInput(repoPath?: string): string;
@@ -0,0 +1,58 @@
1
+ import { existsSync, realpathSync, statSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ /**
4
+ * Validates that a path is a legitimate Coalesce repo directory.
5
+ *
6
+ * Security boundary: repoPath originates from tool input (LLM-controlled) or
7
+ * the COALESCE_REPO_PATH env var. We resolve symlinks, verify it is a real
8
+ * directory, and require the Coalesce-specific nodeTypes/ subdirectory before
9
+ * allowing any filesystem reads downstream.
10
+ *
11
+ * Error messages intentionally omit the resolved path to avoid leaking
12
+ * filesystem layout through MCP tool responses.
13
+ */
14
+ function validateRepoPath(rawPath) {
15
+ const absolutePath = resolve(rawPath);
16
+ if (!existsSync(absolutePath)) {
17
+ throw new Error("repoPath does not exist. Check the provided path or COALESCE_REPO_PATH environment variable.");
18
+ }
19
+ const stats = statSync(absolutePath);
20
+ if (!stats.isDirectory()) {
21
+ throw new Error("repoPath is not a directory. Expected a Coalesce repo directory containing a nodeTypes/ subdirectory.");
22
+ }
23
+ // Resolve symlinks to get the canonical path
24
+ const resolvedPath = realpathSync(absolutePath);
25
+ // Structural validation: a Coalesce repo must have a nodeTypes/ directory
26
+ const nodeTypesDir = join(resolvedPath, "nodeTypes");
27
+ if (!existsSync(nodeTypesDir) || !statSync(nodeTypesDir).isDirectory()) {
28
+ throw new Error("repoPath is not a valid Coalesce repo: missing nodeTypes/ subdirectory. " +
29
+ "Expected a directory containing nodeTypes/, typically a cloned Coalesce project repo.");
30
+ }
31
+ return resolvedPath;
32
+ }
33
+ function getConfiguredRepoPathInput(repoPath) {
34
+ const explicitRepoPath = typeof repoPath === "string" && repoPath.trim().length > 0
35
+ ? repoPath
36
+ : undefined;
37
+ if (explicitRepoPath) {
38
+ return explicitRepoPath;
39
+ }
40
+ const envRepoPath = process.env.COALESCE_REPO_PATH;
41
+ if (typeof envRepoPath === "string" && envRepoPath.trim().length > 0) {
42
+ return envRepoPath;
43
+ }
44
+ return undefined;
45
+ }
46
+ export function resolveOptionalRepoPathInput(repoPath) {
47
+ // Optional callers handle repo parse failures themselves and should degrade
48
+ // gracefully to corpus- or warning-based behavior when the configured path
49
+ // is stale or invalid.
50
+ return getConfiguredRepoPathInput(repoPath);
51
+ }
52
+ export function resolveRepoPathInput(repoPath) {
53
+ const configuredRepoPath = getConfiguredRepoPathInput(repoPath);
54
+ if (configuredRepoPath) {
55
+ return validateRepoPath(configuredRepoPath);
56
+ }
57
+ throw new Error("repoPath is required for repo-backed tools. Provide repoPath explicitly or set COALESCE_REPO_PATH.");
58
+ }
@@ -0,0 +1,50 @@
1
+ export type NodeDefinitionTemplateOptions = {
2
+ nodeName?: string;
3
+ nodeType?: string;
4
+ locationName?: string;
5
+ database?: string;
6
+ schema?: string;
7
+ };
8
+ export type NodeDefinitionFieldMapping = {
9
+ groupIndex: number;
10
+ itemIndex: number;
11
+ groupName: string | null;
12
+ itemType: string | null;
13
+ displayName: string | null;
14
+ attributeName: string | null;
15
+ targetPath: string | null;
16
+ defaultValue: unknown;
17
+ enableIf: string | null;
18
+ note: string;
19
+ };
20
+ export type GeneratedNodeDefinitionTemplate = {
21
+ definitionSummary: {
22
+ capitalized: string | null;
23
+ short: string | null;
24
+ plural: string | null;
25
+ tagColor: string | null;
26
+ configGroupCount: number;
27
+ configItemCount: number;
28
+ };
29
+ fieldMappings: NodeDefinitionFieldMapping[];
30
+ inferredTopLevelFields: Record<string, unknown>;
31
+ inferredConfig: Record<string, unknown>;
32
+ setWorkspaceNodeBodyTemplate: Record<string, unknown>;
33
+ usageGuidance: string[];
34
+ warnings: string[];
35
+ };
36
+ export type TemplateComparisonResult = {
37
+ checkedFieldCount: number;
38
+ matchedFieldCount: number;
39
+ mismatchedFieldCount: number;
40
+ missingFieldCount: number;
41
+ fields: Array<{
42
+ targetPath: string;
43
+ inferredDefault: unknown;
44
+ actualValue: unknown;
45
+ status: "matched" | "mismatched" | "missing";
46
+ }>;
47
+ };
48
+ export declare function buildSetWorkspaceNodeTemplateFromDefinition(nodeDefinition: Record<string, unknown>, options?: NodeDefinitionTemplateOptions): GeneratedNodeDefinitionTemplate;
49
+ export declare function compareGeneratedTemplateToWorkspaceNode(generated: GeneratedNodeDefinitionTemplate, workspaceNode: Record<string, unknown>): TemplateComparisonResult;
50
+ export declare function renderYaml(value: unknown): string;