@google-cloud/kcmd 0.0.0-stage → 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 (150) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +266 -2
  3. package/build/ts/kcmd/cli.js +19578 -0
  4. package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
  5. package/build/ts/kcmd/gcp/alloydb.js +70 -0
  6. package/build/ts/kcmd/gcp/api.d.ts +29 -0
  7. package/build/ts/kcmd/gcp/api.js +162 -0
  8. package/build/ts/kcmd/gcp/biglake.d.ts +16 -0
  9. package/build/ts/kcmd/gcp/biglake.js +50 -0
  10. package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
  11. package/build/ts/kcmd/gcp/bigquery.js +134 -0
  12. package/build/ts/kcmd/gcp/context.d.ts +10 -0
  13. package/build/ts/kcmd/gcp/context.js +74 -0
  14. package/build/ts/kcmd/gcp/crm.d.ts +13 -0
  15. package/build/ts/kcmd/gcp/crm.js +91 -0
  16. package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
  17. package/build/ts/kcmd/gcp/dataplex.js +406 -0
  18. package/build/ts/kcmd/gcp/index.d.ts +3 -0
  19. package/build/ts/kcmd/gcp/index.js +21 -0
  20. package/build/ts/kcmd/gcp/spanner.d.ts +63 -0
  21. package/build/ts/kcmd/gcp/spanner.js +123 -0
  22. package/build/ts/kcmd/index.d.ts +7 -0
  23. package/build/ts/kcmd/index.js +46 -0
  24. package/build/ts/kcmd/layout.d.ts +16 -0
  25. package/build/ts/kcmd/layout.js +31 -0
  26. package/build/ts/kcmd/layouts/documents.d.ts +24 -0
  27. package/build/ts/kcmd/layouts/documents.js +264 -0
  28. package/build/ts/kcmd/layouts/okf.d.ts +31 -0
  29. package/build/ts/kcmd/layouts/okf.js +344 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +542 -0
  32. package/build/ts/kcmd/layouts/standard.d.ts +13 -0
  33. package/build/ts/kcmd/layouts/standard.js +108 -0
  34. package/build/ts/kcmd/manifest.d.ts +34 -0
  35. package/build/ts/kcmd/manifest.js +262 -0
  36. package/build/ts/kcmd/metadata.d.ts +25 -0
  37. package/build/ts/kcmd/metadata.js +8 -0
  38. package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
  39. package/build/ts/kcmd/okf/catalog_residue.js +124 -0
  40. package/build/ts/kcmd/okf/concept.d.ts +14 -0
  41. package/build/ts/kcmd/okf/concept.js +104 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +115 -0
  46. package/build/ts/kcmd/okf/links.d.ts +23 -0
  47. package/build/ts/kcmd/okf/links.js +468 -0
  48. package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
  49. package/build/ts/kcmd/okf/okf_metadata.js +82 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +525 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +97 -0
  54. package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
  55. package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
  56. package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
  57. package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
  58. package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
  59. package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
  60. package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
  61. package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
  62. package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
  63. package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
  64. package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
  65. package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
  66. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +129 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -0
  68. package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
  69. package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
  70. package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
  71. package/build/ts/kcmd/semantic/deployment_target.js +379 -0
  72. package/build/ts/kcmd/semantic/graph_ddl.d.ts +34 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1275 -0
  76. package/build/ts/kcmd/semantic/ir.d.ts +589 -0
  77. package/build/ts/kcmd/semantic/ir.js +247 -0
  78. package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
  79. package/build/ts/kcmd/semantic/kc_actions.js +531 -0
  80. package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
  81. package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
  82. package/build/ts/kcmd/semantic/kc_converter.d.ts +31 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +824 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +828 -0
  86. package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
  87. package/build/ts/kcmd/semantic/kc_entries.js +284 -0
  88. package/build/ts/kcmd/semantic/kc_ids.d.ts +103 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +191 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +262 -0
  93. package/build/ts/kcmd/semantic/loader.js +3440 -0
  94. package/build/ts/kcmd/semantic/names.d.ts +10 -0
  95. package/build/ts/kcmd/semantic/names.js +85 -0
  96. package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
  97. package/build/ts/kcmd/semantic/osi_converter.js +911 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
  99. package/build/ts/kcmd/semantic/parameters.js +152 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +346 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -0
  106. package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
  107. package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
  108. package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
  109. package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
  110. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +94 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +318 -0
  112. package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
  113. package/build/ts/kcmd/semantic/runtime/store.js +344 -0
  114. package/build/ts/kcmd/semantic/skills.d.ts +110 -0
  115. package/build/ts/kcmd/semantic/skills.js +697 -0
  116. package/build/ts/kcmd/semantic/source_dialect.d.ts +10 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +261 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +282 -0
  120. package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
  121. package/build/ts/kcmd/semantic/spanner.js +60 -0
  122. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +17 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -0
  124. package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
  125. package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
  126. package/build/ts/kcmd/semantic/sql_parser.d.ts +44 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +378 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
  129. package/build/ts/kcmd/semantic/transpile.js +563 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +35 -0
  131. package/build/ts/kcmd/semantic/validate.js +1899 -0
  132. package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
  133. package/build/ts/kcmd/semantic/yaml_options.js +58 -0
  134. package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
  135. package/build/ts/kcmd/semantic/yaml_update.js +782 -0
  136. package/build/ts/kcmd/snapshot.d.ts +33 -0
  137. package/build/ts/kcmd/snapshot.js +379 -0
  138. package/build/ts/kcmd/source.d.ts +31 -0
  139. package/build/ts/kcmd/source.js +119 -0
  140. package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
  141. package/build/ts/kcmd/sources/bq-dataset.js +131 -0
  142. package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
  143. package/build/ts/kcmd/sources/entrygroup.js +83 -0
  144. package/build/ts/kcmd/sources/kb.d.ts +21 -0
  145. package/build/ts/kcmd/sources/kb.js +82 -0
  146. package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
  147. package/build/ts/kcmd/sources/semantic-model.js +48 -0
  148. package/build/ts/kcmd/sync.d.ts +35 -0
  149. package/build/ts/kcmd/sync.js +697 -0
  150. package/package.json +62 -4
@@ -0,0 +1,531 @@
1
+ "use strict";
2
+ // How a model's ACTIONS are encoded in Knowledge Catalog.
3
+ //
4
+ // An action is published exactly the way a metric is: one entry per action,
5
+ // parented to the model anchor, carrying one aspect that holds the executor and
6
+ // the typed parameters. What differs is the type. Every other construct has a
7
+ // built-in system type, and an action does not, so it uses the custom
8
+ // `semantic-action` pair DECLARED IN kc_custom_types.ts and created there by
9
+ // `kcmd init --semantic-model`. That file is the list of what is custom; this
10
+ // one is only the encoding that fills the aspect.
11
+ //
12
+ // The custom pair is what makes an action explicit in the catalog. A search can
13
+ // tell an action apart from anything else by its entry type, and the aspect's
14
+ // fields are typed and queryable rather than prose a reader has to interpret.
15
+ //
16
+ // WHEN A BUILT-IN ACTION TYPE SHIPS, follow the instructions at the top of
17
+ // kc_custom_types.ts. Nothing in this file changes: the readers below match a
18
+ // type by its id suffix, so they do not care which project it lives in.
19
+ //
20
+ // The call sites are `knowledge_catalog.ts` (emit the entries),
21
+ // `kc_converter.ts` (read them back), and `pull_kc.ts` (hydrate the aspect).
22
+ //
23
+ // `instructions` rides the action's own aspect rather than the built-in
24
+ // `guidelines` aspect that an entity or metric uses. A pull derives which
25
+ // aspect types to hydrate from the project the ENTRY type lives in, so an
26
+ // action entry, whose type is custom and therefore in the destination project,
27
+ // would ask for a `guidelines` aspect type that only exists under
28
+ // `dataplex-types`. Keeping the field on the action's own aspect keeps the
29
+ // whole encoding inside the one type kc_custom_types.ts provisions.
30
+ //
31
+ // The helpers at the bottom duplicate a few lines from the modules above on
32
+ // purpose. This module imports only the IR, the entry shape and the type
33
+ // registry, so it can be swapped or deleted as a unit.
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.actionEntryTypeName = actionEntryTypeName;
36
+ exports.actionAspectTypeName = actionAspectTypeName;
37
+ exports.actionAspectKey = actionAspectKey;
38
+ exports.actionEntryId = actionEntryId;
39
+ exports.actionOwnedPrefix = actionOwnedPrefix;
40
+ exports.actionEntries = actionEntries;
41
+ exports.isActionEntry = isActionEntry;
42
+ exports.actionAspectTypes = actionAspectTypes;
43
+ exports.readAction = readAction;
44
+ const ir_1 = require("./ir");
45
+ const kc_custom_types_1 = require("./kc_custom_types");
46
+ const resolve_inheritance_1 = require("./resolve_inheritance");
47
+ // Full resource name of the action entry type for a destination.
48
+ function actionEntryTypeName(dest) {
49
+ return (0, kc_custom_types_1.customEntryTypeName)(kc_custom_types_1.ACTION_TYPE_ID, dest);
50
+ }
51
+ // Full resource name of the action aspect type for a destination.
52
+ function actionAspectTypeName(dest) {
53
+ return (0, kc_custom_types_1.customAspectTypeName)(kc_custom_types_1.ACTION_TYPE_ID, dest);
54
+ }
55
+ // Aspect-map key: the `project.location.type` reference form the client keys an
56
+ // entry's aspects by.
57
+ function actionAspectKey(dest) {
58
+ return (0, kc_custom_types_1.customAspectKey)(kc_custom_types_1.ACTION_TYPE_ID, dest);
59
+ }
60
+ // The entry id of one action: `<model>.actions.<name>`, alongside
61
+ // `<model>.entities.<name>` and `<model>.metrics.<name>`.
62
+ function actionEntryId(modelId, actionName) {
63
+ return `${modelId}.actions.${slug(actionName)}`;
64
+ }
65
+ // The entry-id prefix a model's actions occupy, so delete reconciliation
66
+ // removes the entry of an action dropped from the model.
67
+ function actionOwnedPrefix(modelId) {
68
+ return `${modelId}.actions.`;
69
+ }
70
+ // Why a pull will not recover the field a parameter projects from, or '' when
71
+ // it will. Three ways to lose it, and naming which one is the difference
72
+ // between an author deleting the projection and an author binding a column:
73
+ //
74
+ // - the concept is not published at all (an abstract entity under the
75
+ // first-generation types);
76
+ // - the concept is published but the FIELD is not -- `pruneUnavailable`
77
+ // drops an unbound field while keeping the entity whose key still binds,
78
+ // and a projected parameter no longer prunes its action, so the entry goes
79
+ // out naming a field the `schema` aspect does not carry;
80
+ // - the concept is a relationship. Pull rebuilds relationships from
81
+ // `schema-join` entry links alone, and a link records its two endpoints
82
+ // and nothing else, so an association's own fields come back from no
83
+ // entry. The parameter still publishes intact either way -- the loader
84
+ // resolved its type and wording before any of this -- so what is lost is
85
+ // only the projection, and only on the way back.
86
+ function unrecoverable(concept, field, relationships, published, ctx) {
87
+ if (!ctx.publishedEntities.has(concept) && relationships.has(concept)) {
88
+ return `'${concept}' is a relationship, whose fields no entry records`;
89
+ }
90
+ if (!ctx.publishedEntities.has(concept)) {
91
+ return `this push does not publish '${concept}' (abstract or unavailable)`;
92
+ }
93
+ if (published && field !== undefined &&
94
+ !published.get(concept)?.fields.has(field)) {
95
+ return `this push publishes '${concept}' without its '${field}' field ` +
96
+ `(unbound under this profile)`;
97
+ }
98
+ return '';
99
+ }
100
+ /**
101
+ * One entry per action, to append to the model's entries.
102
+ *
103
+ * Empty when the model declares no actions, so a model without actions is
104
+ * unchanged from before actions existed. Warns once when it is non-empty:
105
+ * actions reach Knowledge Catalog and nowhere else, which is worth saying out
106
+ * loud on a push that also deploys a graph.
107
+ */
108
+ function actionEntries(model, modelId, ctx, warnings) {
109
+ const actions = model.actions ?? [];
110
+ if (!actions.length)
111
+ return [];
112
+ // A relationship is a perfectly good `affects` concept but is never in
113
+ // publishedEntities, so the check below reads the model's own edges for it.
114
+ // Pruning drops a whole relationship from the model, so a dropped one is
115
+ // absent from this set for the same reason a dropped entity is absent from
116
+ // publishedEntities.
117
+ const relationshipNames = new Set((model.relationships ?? []).map(r => r.name));
118
+ // The fields of each concept with inheritance resolved, which a later pull
119
+ // rebuilds from the catalog. Asked of the loader's own resolver so the
120
+ // answer accounts for `extends`; a model that will not resolve returns null
121
+ // and the field half of the check is skipped, since an emitter is not the
122
+ // place to discover that a model does not load.
123
+ let published;
124
+ try {
125
+ published = (0, resolve_inheritance_1.declaredConceptFields)(model);
126
+ }
127
+ catch {
128
+ published = null;
129
+ }
130
+ const entries = [];
131
+ for (const action of actions) {
132
+ const id = actionEntryId(modelId, action.name);
133
+ if (!ctx.claim(id, `action '${action.name}'`))
134
+ continue;
135
+ // A derived parameter -- one projected from a field -- leaves the catalog
136
+ // naming a concept it has no entry for when that concept is not part of
137
+ // this push. The parameter itself still publishes intact, type and wording
138
+ // and all, because the loader already resolved them; what a later pull
139
+ // cannot do is find the field the projection came from, so re-authoring
140
+ // that model would have to state the type by hand.
141
+ for (const p of action.parameters ?? []) {
142
+ if (!p.concept)
143
+ continue;
144
+ const why = unrecoverable(p.concept, p.field, relationshipNames, published, ctx);
145
+ if (!why)
146
+ continue;
147
+ warnings.push(`model '${model.name}': action '${action.name}' parameter ` +
148
+ `'${p.name}' is projected from '${p.concept}.${p.field}', ` +
149
+ `${why}, so a pull will recover the parameter but not the field ` +
150
+ `it came from.`);
151
+ }
152
+ // The same hazard for the concepts the action declares it changes. The
153
+ // entry is still published -- the action does change that concept, and
154
+ // dropping the claim would understate the blast radius -- but the catalog
155
+ // then names a concept it has no entry for, and pulling that model and
156
+ // pushing it unpruned fails hard on the same name. Say it where the
157
+ // divergence is created rather than two commands later.
158
+ for (const affected of action.affects ?? []) {
159
+ if (ctx.publishedEntities.has(affected.concept))
160
+ continue;
161
+ if (relationshipNames.has(affected.concept))
162
+ continue;
163
+ warnings.push(`model '${model.name}': action '${action.name}' affects ` +
164
+ `'${affected.concept}', which this push does not publish ` +
165
+ `(abstract or unavailable), so the catalog records a blast radius ` +
166
+ `naming a concept it has no entry for.`);
167
+ }
168
+ entries.push({
169
+ name: ctx.entry(id),
170
+ entryType: actionEntryTypeName(ctx),
171
+ parentEntry: ctx.anchor,
172
+ entrySource: compact({
173
+ displayName: action.name,
174
+ description: action.description,
175
+ }),
176
+ aspects: {
177
+ [actionAspectKey(ctx)]: {
178
+ aspectType: actionAspectTypeName(ctx),
179
+ data: actionAspectData(action),
180
+ },
181
+ },
182
+ });
183
+ }
184
+ // Counted from what was emitted rather than from the model, so a name
185
+ // collision that skipped an action does not inflate the number.
186
+ if (entries.length)
187
+ warnings.push(`model '${model.name}': ${entries.length} action(s) published as ` +
188
+ `${kc_custom_types_1.ACTION_TYPE_ID} entries (Knowledge Catalog is the only system an ` +
189
+ `action reaches).`);
190
+ return entries;
191
+ }
192
+ // The aspect payload for one action: the executor flattened to the fields of
193
+ // its kind, the typed parameters, the constraints that gate it, what it
194
+ // changes, and any AI instructions. Guards are the constraint NAMES, matching
195
+ // the sibling `semantic-constraint` entries by display name, so a reader
196
+ // holding one action entry can find the rules it is checked against; an
197
+ // an affected concept names an entity or relationship entry the same way.
198
+ function actionAspectData(action) {
199
+ return compact({
200
+ ...(action.executor ? executorData(action.executor) : {}),
201
+ parameters: action.parameters.map(p => compact({
202
+ name: p.name,
203
+ type: p.type,
204
+ concept: p.concept,
205
+ field: p.field,
206
+ description: p.description,
207
+ required: p.required,
208
+ default: p.default !== undefined ? JSON.stringify(p.default) :
209
+ undefined,
210
+ })),
211
+ guards: action.guards?.length ? action.guards : undefined,
212
+ affects: action.affects?.length ? action.affects.map(affectedConceptData) :
213
+ undefined,
214
+ instructions: action.aiContext?.instructions || undefined,
215
+ });
216
+ }
217
+ // One affected concept as its aspect record: exactly the three fields the IR
218
+ // carries, so
219
+ // the round-trip is a rename away from an identity.
220
+ function affectedConceptData(affected) {
221
+ return compact({
222
+ concept: affected.concept,
223
+ operation: affected.operation,
224
+ fields: affected.fields?.length ? [...affected.fields] : undefined,
225
+ });
226
+ }
227
+ // The executor's kind plus the two coordinates that kind uses. Only the fields
228
+ // of the live kind are set, so the aspect never shows blanks for the others.
229
+ function executorData(ex) {
230
+ switch (ex.kind) {
231
+ case 'mcp':
232
+ return {
233
+ executorKind: 'mcp',
234
+ mcpServer: ex.mcp.server,
235
+ mcpTool: ex.mcp.tool,
236
+ };
237
+ case 'rest':
238
+ return {
239
+ executorKind: 'rest',
240
+ restEndpoint: ex.rest.endpoint,
241
+ restMethod: ex.rest.method,
242
+ };
243
+ case 'grpc':
244
+ return {
245
+ executorKind: 'grpc',
246
+ grpcService: ex.grpc.service,
247
+ grpcMethod: ex.grpc.method,
248
+ };
249
+ case 'sql':
250
+ // The statements are published verbatim. A consumer that only routes on
251
+ // the kind can ignore them; one that wants to know what the action
252
+ // actually writes -- a review engine deciding whether the declared
253
+ // `affects` matches the write -- has the text without a second lookup.
254
+ return {
255
+ executorKind: 'sql',
256
+ sqlStatements: [...ex.sql.statements],
257
+ };
258
+ }
259
+ }
260
+ // ---------------------------------------------------------------------------
261
+ // Read side: an action entry -> the IR.
262
+ // ---------------------------------------------------------------------------
263
+ // True when an entry is one of a model's actions, matched by the entry type's
264
+ // id suffix so the project the type lives in need not be known -- which is what
265
+ // lets a pull keep working when the custom type is replaced by a built-in one.
266
+ function isActionEntry(entry) {
267
+ return entry.entryType?.endsWith(`/entryTypes/${kc_custom_types_1.ACTION_TYPE_ID}`) ?? false;
268
+ }
269
+ // The aspect type resource names to hydrate for an action entry. Named through
270
+ // the entry type's own project so the pull follows the type wherever it lives.
271
+ function actionAspectTypes(entryTypeBase) {
272
+ return [`${entryTypeBase}/aspectTypes/${kc_custom_types_1.ACTION_TYPE_ID}`];
273
+ }
274
+ /**
275
+ * Recovers one action from its entry, the inverse of actionEntries.
276
+ *
277
+ * A parameter recovers exactly what was stored, projection and all. Nothing is
278
+ * re-derived here: a derived parameter's type was resolved once, at load, and
279
+ * the aspect carries the answer, so a pull that recovered the concept and one
280
+ * that did not produce the same parameter rather than two different ones.
281
+ *
282
+ * An entry with NO executor recovers as an action with none: that is a legal
283
+ * published state, not damage. An entry whose executor names a kind but lacks
284
+ * the coordinates that kind needs is malformed, and returns undefined with a
285
+ * warning, so one bad entry degrades itself rather than the pull.
286
+ */
287
+ function readAction(entry, warnings) {
288
+ const name = entry.entrySource?.displayName || idOf(entry.name);
289
+ const data = actionAspectDataOf(entry);
290
+ // No kind at all is an action no binding performed, which round-trips as it
291
+ // was published. A kind whose coordinates are missing is damage.
292
+ const declaresExecutor = typeof data.executorKind === 'string' && data.executorKind.trim() !== '';
293
+ const executor = readExecutor(data);
294
+ if (declaresExecutor && !executor) {
295
+ warnings.push(`action '${name}': the ${kc_custom_types_1.ACTION_TYPE_ID} aspect has no usable ` +
296
+ `executor; the action is skipped`);
297
+ return undefined;
298
+ }
299
+ const parameters = asArray(data.parameters)
300
+ .map((p) => readParameter(p, name, warnings))
301
+ .filter((p) => p !== undefined);
302
+ const action = { name, parameters };
303
+ if (executor)
304
+ action.executor = executor;
305
+ // Guard names round-trip verbatim. A name whose constraint is not part of
306
+ // this pull is kept rather than dropped, because dropping it here would
307
+ // silently rewrite the author's model. It is not kept in silence:
308
+ // kc_converter warns once it has both lists in hand, and push-side validate
309
+ // rejects it.
310
+ //
311
+ // A REPEATED name is the exception, because the loader rejects one outright:
312
+ // keeping it would hand back a document that cannot be reloaded. A duplicate
313
+ // guard checks the same constraint twice, so dropping it loses no meaning.
314
+ const guards = [];
315
+ for (const g of asArray(data.guards)) {
316
+ if (typeof g !== 'string' || g === '')
317
+ continue;
318
+ if (guards.includes(g)) {
319
+ warnings.push(`action '${name}': the ${kc_custom_types_1.ACTION_TYPE_ID} aspect repeats guard ` +
320
+ `'${g}'; the duplicate is dropped so the document still loads`);
321
+ continue;
322
+ }
323
+ guards.push(g);
324
+ }
325
+ if (guards.length)
326
+ action.guards = guards;
327
+ // Affected concepts, deduplicated on the pair the loader rejects a repeat
328
+ // of, for the
329
+ // same reason a repeated guard is dropped here: keeping it would hand back a
330
+ // document that cannot be reloaded.
331
+ const affects = [];
332
+ const seen = new Set();
333
+ for (const raw of asArray(data.affects)) {
334
+ const affected = readAffectedConcept(raw, name, warnings);
335
+ if (!affected)
336
+ continue;
337
+ const key = `${affected.concept}/${affected.operation ?? ''}`;
338
+ if (seen.has(key)) {
339
+ warnings.push(`action '${name}': the ${kc_custom_types_1.ACTION_TYPE_ID} aspect repeats the entry ` +
340
+ `on '${affected.concept}'; the duplicate is dropped so the ` +
341
+ `document still loads`);
342
+ continue;
343
+ }
344
+ seen.add(key);
345
+ affects.push(affected);
346
+ }
347
+ if (affects.length)
348
+ action.affects = affects;
349
+ const description = entry.entrySource?.description;
350
+ if (description !== undefined && description !== '')
351
+ action.description = description;
352
+ if (typeof data.instructions === 'string' && data.instructions !== '')
353
+ action.aiContext = { instructions: data.instructions };
354
+ return action;
355
+ }
356
+ // One action parameter from its aspect record. A record missing a name is
357
+ // dropped. `concept` and `field` come back together or not at all: half a
358
+ // projection is not something a re-push could state, so a record carrying only
359
+ // one of them recovers as the plain scalar it already is, and says so.
360
+ function readParameter(p, actionName, warnings) {
361
+ const name = typeof p?.name === 'string' ? p.name : '';
362
+ const type = typeof p?.type === 'string' ? p.type : '';
363
+ if (!name)
364
+ return undefined;
365
+ // An absent type comes back ABSENT, not as an empty string. `type` is
366
+ // required on the aspect, so a record without one is damage -- but the rest
367
+ // of the codebase reads `type === undefined` as "this parameter has no
368
+ // type", and a `''` would slip past every one of those checks while being
369
+ // no more of a datatype than `undefined` is.
370
+ const param = type ? { name, type } : { name };
371
+ const concept = typeof p?.concept === 'string' ? p.concept : '';
372
+ const field = typeof p?.field === 'string' ? p.field : '';
373
+ if (concept && field) {
374
+ param.concept = concept;
375
+ param.field = field;
376
+ }
377
+ else if (concept || field) {
378
+ warnings.push(`action '${actionName}': parameter '${name}' stores only ` +
379
+ `'${concept ? 'concept' : 'field'}' of a field projection; ` +
380
+ `recovered as a plain ${type || 'untyped'} parameter.`);
381
+ }
382
+ if (typeof p?.description === 'string' && p.description !== '') {
383
+ param.description = p.description;
384
+ }
385
+ if (typeof p?.required === 'boolean') {
386
+ param.required = p.required;
387
+ }
388
+ if (p?.default !== undefined && p.default !== '') {
389
+ param.default = parseDefaultFromAspect(p.default);
390
+ }
391
+ if (!type) {
392
+ warnings.push(`action '${actionName}': parameter '${name}' stores no type, which ` +
393
+ `the aspect requires; pulled without one`);
394
+ }
395
+ else if (!ir_1.DATA_TYPES.includes(type)) {
396
+ // Every parameter is a scalar now, so a type that is not one is damage
397
+ // rather than a reference this pull failed to resolve. Kept as stored --
398
+ // dropping it would hide what the catalog actually holds -- and reported,
399
+ // because a re-push will refuse it.
400
+ warnings.push(`action '${actionName}': parameter '${name}' type '${type}' is not a ` +
401
+ `scalar datatype; pulled as stored`);
402
+ }
403
+ return param;
404
+ }
405
+ function parseDefaultFromAspect(raw) {
406
+ if (typeof raw !== 'string')
407
+ return raw;
408
+ try {
409
+ const parsed = JSON.parse(raw);
410
+ if (typeof parsed === 'number' && String(parsed) !== raw) {
411
+ return raw;
412
+ }
413
+ return parsed;
414
+ }
415
+ catch {
416
+ return raw;
417
+ }
418
+ }
419
+ // One affected concept from its aspect record, the inverse of
420
+ // affectedConceptData. A record with no concept is dropped: it names nothing,
421
+ // so there is nothing to recover.
422
+ //
423
+ // Unlike readParameter, this resolves nothing against the pulled model. It
424
+ // could not do so honestly if it tried: a pull recovers relationships from the
425
+ // schema-join entry links, so a many-to-many edge is never among them, and
426
+ // every entry naming one would look unresolvable on a model that is perfectly
427
+ // well-formed. Nothing here needs the answer, so nothing asks.
428
+ function readAffectedConcept(raw, actionName, warnings) {
429
+ const concept = typeof raw?.concept === 'string' ? raw.concept : '';
430
+ if (!concept)
431
+ return undefined;
432
+ const affected = { concept };
433
+ if (ir_1.CONCEPT_OPERATIONS.includes(raw.operation)) {
434
+ affected.operation = raw.operation;
435
+ }
436
+ else if (raw.operation !== undefined && raw.operation !== '') {
437
+ // An aspect edited by hand can carry anything. Dropping the operation
438
+ // keeps the entry (the blast radius is still true) and says why.
439
+ warnings.push(`action '${actionName}': affects '${concept}' with an unknown ` +
440
+ `operation '${raw.operation}'; recovered with the operation dropped`);
441
+ }
442
+ const fields = [];
443
+ for (const f of asArray(raw.fields)) {
444
+ if (typeof f !== 'string' || f === '' || fields.includes(f))
445
+ continue;
446
+ fields.push(f);
447
+ }
448
+ if (fields.length)
449
+ affected.fields = fields;
450
+ return affected;
451
+ }
452
+ // The IR executor from the aspect's flat fields, the inverse of executorData.
453
+ // Returns undefined when the kind is unknown or either of its coordinates is
454
+ // missing.
455
+ function readExecutor(data) {
456
+ // A coordinate must be a present, NON-BLANK string. An aspect edited by hand
457
+ // can carry an empty one; treat that as malformed so the reader rejects it
458
+ // exactly as push-side validate would, rather than recovering an action the
459
+ // next push cannot deploy.
460
+ const str = (v) => typeof v === 'string' && v.trim() !== '';
461
+ switch (data.executorKind) {
462
+ case 'mcp':
463
+ if (str(data.mcpServer) && str(data.mcpTool))
464
+ return { kind: 'mcp', mcp: { server: data.mcpServer, tool: data.mcpTool } };
465
+ return undefined;
466
+ case 'rest':
467
+ if (str(data.restEndpoint) && str(data.restMethod))
468
+ return {
469
+ kind: 'rest',
470
+ rest: { endpoint: data.restEndpoint, method: data.restMethod }
471
+ };
472
+ return undefined;
473
+ case 'grpc':
474
+ if (str(data.grpcService) && str(data.grpcMethod))
475
+ return {
476
+ kind: 'grpc',
477
+ grpc: { service: data.grpcService, method: data.grpcMethod }
478
+ };
479
+ return undefined;
480
+ case 'sql': {
481
+ // Unlike the other three, the coordinate here is a list. A blank entry is
482
+ // dropped rather than kept: it is not a statement, and validate would
483
+ // reject the pulled model for carrying it. An executor left with no
484
+ // statement at all is malformed, like a missing coordinate elsewhere.
485
+ const statements = Array.isArray(data.sqlStatements) ?
486
+ data.sqlStatements.filter(str).map((s) => s.trim()) :
487
+ [];
488
+ if (statements.length)
489
+ return { kind: 'sql', sql: { statements } };
490
+ return undefined;
491
+ }
492
+ default:
493
+ return undefined;
494
+ }
495
+ }
496
+ // ---------------------------------------------------------------------------
497
+ // Local helpers (see the file header on why they are not shared).
498
+ // ---------------------------------------------------------------------------
499
+ // The action aspect's `data` from an entry, matched by the aspect key's
500
+ // `.semantic-action` suffix or the aspectType's `/aspectTypes/semantic-action`
501
+ // suffix, so it is found whichever project the type was provisioned in.
502
+ function actionAspectDataOf(entry) {
503
+ for (const [key, aspect] of Object.entries(entry.aspects ?? {})) {
504
+ if (key.endsWith(`.${kc_custom_types_1.ACTION_TYPE_ID}`) ||
505
+ aspect.aspectType?.endsWith(`/aspectTypes/${kc_custom_types_1.ACTION_TYPE_ID}`)) {
506
+ return aspect.data ?? {};
507
+ }
508
+ }
509
+ return {};
510
+ }
511
+ // Entry ids allow letters, numbers, underscores, hyphens, and periods.
512
+ function slug(s) {
513
+ return s.replace(/[^A-Za-z0-9_.-]/g, '_');
514
+ }
515
+ // The id segment of a full entry resource name.
516
+ function idOf(name) {
517
+ return name.split('/').pop() ?? name;
518
+ }
519
+ // Drops undefined-valued keys so the emitted aspect (and its golden) only shows
520
+ // fields the model actually set.
521
+ function compact(obj) {
522
+ const out = {};
523
+ for (const [k, v] of Object.entries(obj)) {
524
+ if (v !== undefined)
525
+ out[k] = v;
526
+ }
527
+ return out;
528
+ }
529
+ function asArray(value) {
530
+ return Array.isArray(value) ? value : [];
531
+ }
@@ -0,0 +1,44 @@
1
+ import { Entry } from '../gcp/dataplex';
2
+ import { Constraint, SemanticModel } from './ir';
3
+ export declare function constraintEntryTypeName(dest: {
4
+ project: string;
5
+ }): string;
6
+ export declare function constraintAspectTypeName(dest: {
7
+ project: string;
8
+ }): string;
9
+ export declare function constraintAspectKey(dest: {
10
+ project: string;
11
+ }): string;
12
+ export interface ConstraintEmitContext {
13
+ project: string;
14
+ entry(entryId: string): string;
15
+ anchor: string;
16
+ claim(entryId: string, label: string): boolean;
17
+ }
18
+ export declare function constraintEntryId(modelId: string, constraintName: string): string;
19
+ export declare function constraintOwnedPrefix(modelId: string): string;
20
+ /**
21
+ * One entry per constraint, to append to the model's entries.
22
+ *
23
+ * Empty when the model declares no constraints, so a model without them is
24
+ * unchanged from before constraints existed. Warns once when it is non-empty:
25
+ * constraints reach Knowledge Catalog and nowhere else, which is worth saying
26
+ * out loud on a push that also deploys a graph.
27
+ */
28
+ export declare function constraintEntries(model: SemanticModel, modelId: string, ctx: ConstraintEmitContext, warnings: string[]): Entry[];
29
+ export declare function isConstraintEntry(entry: Entry): boolean;
30
+ export declare function constraintAspectTypes(entryTypeBase: string): string[];
31
+ /**
32
+ * Recovers one constraint from its entry, the inverse of constraintEntries.
33
+ *
34
+ * Returns undefined, with a warning, for an entry that states no judgment: an
35
+ * invariant that states no rule would be pulled into a model that then fails
36
+ * its own push-side validation, so one bad entry degrades itself rather than
37
+ * the pull. An entry left over from a push that wrote the removed `expression`
38
+ * body lands there too, and the warning says so by name -- that catalog holds a
39
+ * rule this model can no longer state, and the author has to restate it.
40
+ *
41
+ * `evaluation` is not read. There is one body, so the word it carried says
42
+ * nothing the entry does not.
43
+ */
44
+ export declare function readConstraint(entry: Entry, warnings: string[]): Constraint | undefined;