@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,344 @@
1
+ "use strict";
2
+ // The store a model's data lives in.
3
+ //
4
+ // A deployment that uses a binding profile names a target; this module turns
5
+ // that declaration into structured store coordinates. A profile may deploy to
6
+ // Spanner, to AlloyDB or to BigQuery. `kind` is the discriminant, and what
7
+ // differs by it is how the store is ADDRESSED -- the coordinates it takes and
8
+ // the dialect it speaks -- not whether an action may write to it.
9
+ //
10
+ // All three execute SQL DML, so all three can be the store a model's actions
11
+ // run against. BigQuery is a warehouse rather than a transactional database
12
+ // and a write there costs what a warehouse write costs, but that is a question
13
+ // about the model an author is building, not a rule this module enforces.
14
+ //
15
+ // Identity is `name`, the resource the store addresses. Two profiles naming
16
+ // the same database describe ONE store; the profile is how a caller found it,
17
+ // not what it is.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.resolveStore = resolveStore;
20
+ exports.profileTargets = profileTargets;
21
+ exports.withPrimaryInstance = withPrimaryInstance;
22
+ exports.storeLine = storeLine;
23
+ const deployment_target_1 = require("../deployment_target");
24
+ const loader_1 = require("../loader");
25
+ const resolve_profiles_1 = require("../resolve_profiles");
26
+ // A BigQuery table an entity is bound to. The loader rewrites the resource-name
27
+ // URI an author may write into `project.dataset.table`, which is the form the
28
+ // generator emits and the only one that reaches here. The table name takes any
29
+ // character the loader allows in one, which is anything but a dot or a slash,
30
+ // so a name with `$` or a letter outside ASCII is still a table here.
31
+ const BIGQUERY_TABLE_SOURCE = /^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)\.[^./]+$/;
32
+ // The database a bound table sits in, or undefined when the source names no
33
+ // table of any backend this module knows. A Spanner or AlloyDB source, in
34
+ // either spelling, takes the loader's databaseOf form, `spanner/<p>/<i>/<db>`
35
+ // or `alloydb/<p>/<l>/<c>/<db>`.
36
+ function storeOf(source) {
37
+ const database = (0, loader_1.databaseOf)(source);
38
+ if (database?.startsWith('spanner/') || database?.startsWith('alloydb/')) {
39
+ return database;
40
+ }
41
+ const bigQuery = source.match(BIGQUERY_TABLE_SOURCE);
42
+ if (bigQuery)
43
+ return `bigquery/${bigQuery[1]}/${bigQuery[2]}`;
44
+ return undefined;
45
+ }
46
+ // Every table the model binds, not just its entities: a many-to-many
47
+ // relationship is backed by a junction table of its own, and an action that
48
+ // creates the edge writes to exactly that one.
49
+ function boundTables(model) {
50
+ const bindings = [];
51
+ for (const entity of model.entities ?? []) {
52
+ bindings.push({
53
+ name: entity.name,
54
+ source: entity.dataSource ?? '',
55
+ shown: (0, loader_1.writtenSource)(entity),
56
+ });
57
+ }
58
+ for (const relationship of model.relationships ?? []) {
59
+ const source = relationship.association?.dataSource;
60
+ if (source) {
61
+ bindings.push({ name: relationship.name, source, shown: (0, loader_1.asResourceName)(source) });
62
+ }
63
+ }
64
+ return bindings;
65
+ }
66
+ // The database a store addresses, in the form storeOf gives. An AlloyDB
67
+ // store matches a source on project, location, cluster and database.
68
+ function storeDatabase(store) {
69
+ switch (store.kind) {
70
+ case 'spanner':
71
+ return `spanner/${store.project}/${store.instance}/${store.database}`;
72
+ case 'alloydb':
73
+ return `alloydb/${store.project}/${store.location}/${store.cluster}/${store.database}`;
74
+ case 'bigquery':
75
+ return `bigquery/${store.project}/${store.dataset}`;
76
+ }
77
+ }
78
+ // Bindings that address something other than the store the deployment target
79
+ // names. An action's statements address a table by its NAME, with the
80
+ // project/instance/database qualifier dropped, so an entity bound elsewhere
81
+ // would produce a statement against whatever table of that name the target
82
+ // store happens to hold.
83
+ function strayBindings(model, store) {
84
+ const database = storeDatabase(store);
85
+ const strays = [];
86
+ for (const binding of boundTables(model)) {
87
+ const source = binding.source.trim();
88
+ if (!source)
89
+ continue;
90
+ const bound = storeOf(source);
91
+ if (!bound) {
92
+ strays.push(`'${binding.name}' to ${binding.shown}, which is not a table in this store`);
93
+ continue;
94
+ }
95
+ if (bound !== database)
96
+ strays.push(`'${binding.name}' to ${binding.shown}`);
97
+ }
98
+ return strays;
99
+ }
100
+ /**
101
+ * The store a model runs against under binding profile `profile`, which
102
+ * defaults to `default`, the model file's own bindings. It is the one the
103
+ * deployments for that profile name: those that name it in `profile`, or that
104
+ * name none or `default` for `default`. A model with no `deployments` list
105
+ * names no profile for any of its targets, so they are all for `default`.
106
+ * With several targets, the store is the single operational one, Spanner or
107
+ * AlloyDB.
108
+ *
109
+ * Fails six ways:
110
+ *
111
+ * 1. A GOOGLE block's data is not valid JSON, so its targets cannot be read.
112
+ * 2. A target names no supported destination. Push checks the target of every
113
+ * deployment, so this fails the store under every profile.
114
+ * 3. No deployment uses the profile.
115
+ * 4. There are several targets and no single operational one, so which store
116
+ * is meant is ambiguous.
117
+ * 5. There is no target, so there is nothing to run against.
118
+ * 6. The model binds a table outside the store. For a Spanner or AlloyDB
119
+ * store, a write would land in the wrong place, and the error is named. A
120
+ * BigQuery graph may read tables in any dataset, so
121
+ * for a BigQuery store the error is not named.
122
+ */
123
+ function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
124
+ // Push checks the target of every deployment the model declares, so the
125
+ // targets of every deployment are read before the profile's are picked.
126
+ let all;
127
+ try {
128
+ all = (0, deployment_target_1.googleDeploymentTargets)(model);
129
+ }
130
+ catch (err) {
131
+ // It throws when a GOOGLE block's data is not valid JSON. Push reports
132
+ // that as an error, so the store fails.
133
+ return { error: `${err?.message || err}`, named: true, rejected: true };
134
+ }
135
+ // The loader reads a vanilla model's `deployments` out of its GOOGLE block,
136
+ // so a message names the block's `deployments`, as push does.
137
+ const vanilla = model.version === loader_1.OSSIE_VERSION;
138
+ const where = vanilla && model.deployments ?
139
+ `its GOOGLE block's 'deployments'` :
140
+ `'deployments'`;
141
+ // A target in no accepted form is reported, alone or beside one that gives
142
+ // a store, whichever profile its deployment is for.
143
+ if (all.malformed.length) {
144
+ const unparsed = model.deployments?.find(d => d.target === all.malformed[0]);
145
+ const error = unparsed ? (0, deployment_target_1.deploymentTargetError)(model, unparsed) :
146
+ (0, deployment_target_1.googleBlockTargetError)(model, all.malformed[0]);
147
+ return { error, named: true, rejected: true };
148
+ }
149
+ const used = deploymentsFor(model, profile);
150
+ if ('error' in used)
151
+ return used;
152
+ const scoped = used.model;
153
+ // The profile's targets are among those read above. So this does not
154
+ // throw, and none of them is in no accepted form.
155
+ const targets = (0, deployment_target_1.googleDeploymentTargets)(scoped);
156
+ const { spanner, alloyDb, bigQuery } = targets;
157
+ const declared = spanner.length + alloyDb.length + bigQuery.length;
158
+ const operational = [...spanner, ...alloyDb];
159
+ const contenders = operational.length ? operational : bigQuery;
160
+ if (contenders.length > 1) {
161
+ const counted = [
162
+ ...(spanner.length ? [`${spanner.length} Spanner`] : []),
163
+ ...(alloyDb.length ? [`${alloyDb.length} AlloyDB`] : []),
164
+ ...(operational.length ? [] : [`${bigQuery.length} BigQuery`]),
165
+ ];
166
+ const what = counted.length > 1 ?
167
+ `deployment targets on two operational backends (${counted.join(' and ')})` :
168
+ `${counted[0]} deployment targets`;
169
+ const names = deploymentNames(scoped, contenders.map(t => t.uri), profile);
170
+ return {
171
+ error: `Model '${model.name}' declares ${what} in ${where}${names}, so which store it runs against is ambiguous; the ` +
172
+ `runtime uses exactly one deployment for now.`,
173
+ named: true,
174
+ rejected: true,
175
+ };
176
+ }
177
+ if (!declared) {
178
+ const declare = vanilla ?
179
+ ` In a GOOGLE custom_extensions block on the model, declare a ` +
180
+ `deployment in 'deployments' whose target names a database.` :
181
+ ` Declare a deployment in 'deployments' whose target names a ` +
182
+ `database.`;
183
+ return {
184
+ error: `Model '${model.name}' declares no deployment target in ${where}, so there is no store to run against.${declare}`,
185
+ };
186
+ }
187
+ const store = storeFor(spanner[0], alloyDb[0], bigQuery[0]);
188
+ const strays = strayBindings(model, store);
189
+ if (strays.length) {
190
+ return {
191
+ error: `Model '${model.name}' binds ${strays.join(', ')}, but its ` +
192
+ `deployment target is ${store.name}. An action's statements ` +
193
+ `address a table by name alone, so the write would land in the ` +
194
+ `target store's table of that name rather than in the bound one. ` +
195
+ `Bind both to the same store.`,
196
+ // A BigQuery graph may read tables in any dataset, BigLake included,
197
+ // so the model then has no store, and a caller
198
+ // does not fail.
199
+ ...(store.kind === 'bigquery' ? {} : { named: true }),
200
+ };
201
+ }
202
+ return store;
203
+ }
204
+ // The model with only the deployments `profile` uses, or why there are none.
205
+ // A model with no `deployments` list keeps every target in its GOOGLE block
206
+ // for `default`. An empty list names no deployment. Either way, any other
207
+ // profile has none.
208
+ function deploymentsFor(model, profile) {
209
+ const nothingToRun = `so there is no database to run it against`;
210
+ const namedProfile = `binding profile '${profile}' is a named profile, ` +
211
+ `which names no deployment target`;
212
+ if (!model.deployments?.length) {
213
+ return profile === resolve_profiles_1.DEFAULT_PROFILE ?
214
+ { model } :
215
+ { error: `${namedProfile}, ${nothingToRun}` };
216
+ }
217
+ const used = model.deployments.filter(d => (d.profile ?? resolve_profiles_1.DEFAULT_PROFILE) === profile);
218
+ if (!used.length) {
219
+ // A vanilla model lists its deployments in its GOOGLE block.
220
+ const listed = model.version === loader_1.OSSIE_VERSION ?
221
+ `its GOOGLE block's 'deployments'` :
222
+ `'deployments'`;
223
+ let error;
224
+ if (profile === resolve_profiles_1.DEFAULT_PROFILE) {
225
+ error = `Model '${model.name}' declares no deployment without a ` +
226
+ `'profile' in ${listed}, so there is no store to run against ` +
227
+ `under binding profile '${resolve_profiles_1.DEFAULT_PROFILE}'.`;
228
+ }
229
+ else {
230
+ error = `${namedProfile}, and no deployment in ${listed} uses it, ${nothingToRun}`;
231
+ }
232
+ return { error };
233
+ }
234
+ return { model: { ...model, deployments: used } };
235
+ }
236
+ /**
237
+ * The targets binding profile `profile` uses, as resolveStore reads them: its
238
+ * deployments' targets, or every target in a GOOGLE block for `default` when
239
+ * the model has no `deployments` list. Empty when no deployment is for the
240
+ * profile. Throws when a GOOGLE block's data is not valid JSON.
241
+ */
242
+ function profileTargets(model, profile) {
243
+ const used = deploymentsFor(model, profile);
244
+ return 'error' in used ? [] : (0, deployment_target_1.googleDeploymentTargets)(used.model).uris;
245
+ }
246
+ // The deployments whose targets are `uris`, for a message, or nothing when
247
+ // the targets come from a GOOGLE block, which names none. `profile` is the
248
+ // profile they are for.
249
+ function deploymentNames(model, uris, profile) {
250
+ const deployments = model.deployments;
251
+ if (!deployments)
252
+ return '';
253
+ const names = uris.map(uri => deployments.find(d => d.target === uri)?.name ?? uri);
254
+ return ` for profile '${profile}' (${names.join(', ')})`;
255
+ }
256
+ function storeFor(spanner, alloyDb, bigQuery) {
257
+ if (spanner) {
258
+ return {
259
+ kind: 'spanner',
260
+ name: `projects/${spanner.project}/instances/${spanner.instance}/databases/${spanner.database}`,
261
+ project: spanner.project,
262
+ instance: spanner.instance,
263
+ database: spanner.database,
264
+ };
265
+ }
266
+ if (alloyDb) {
267
+ return {
268
+ kind: 'alloydb',
269
+ name: `projects/${alloyDb.project}/locations/${alloyDb.location}/clusters/${alloyDb.cluster}/databases/${alloyDb.database}`,
270
+ project: alloyDb.project,
271
+ location: alloyDb.location,
272
+ cluster: alloyDb.cluster,
273
+ database: alloyDb.database,
274
+ };
275
+ }
276
+ return {
277
+ kind: 'bigquery',
278
+ name: `projects/${bigQuery.project}/datasets/${bigQuery.dataset}`,
279
+ project: bigQuery.project,
280
+ dataset: bigQuery.dataset,
281
+ };
282
+ }
283
+ /**
284
+ * `store` with the instance a connection goes through. An AlloyDB target
285
+ * names a database in a cluster, so this asks the AlloyDB API for the
286
+ * cluster's primary instance. Fails, naming the cluster, when it has none,
287
+ * when the primary is not READY, or when the listing fails. Any other store
288
+ * comes back unchanged.
289
+ */
290
+ async function withPrimaryInstance(store, client) {
291
+ if (store.kind !== 'alloydb')
292
+ return store;
293
+ const cluster = `projects/${store.project}/locations/${store.location}/clusters/${store.cluster}`;
294
+ let res;
295
+ try {
296
+ res = await client.listInstances(store.project, store.location, store.cluster);
297
+ }
298
+ catch (err) {
299
+ return {
300
+ error: `Could not list the instances of AlloyDB cluster '${cluster}': ${err?.message ?? err}`,
301
+ };
302
+ }
303
+ if (res.status !== 200) {
304
+ return {
305
+ error: `Could not list the instances of AlloyDB cluster '${cluster}': ${apiError(res)}`,
306
+ };
307
+ }
308
+ const primary = res.result?.find(i => i.instanceType === 'PRIMARY');
309
+ const instance = primary?.name?.split('/').pop();
310
+ if (!primary || !instance) {
311
+ return {
312
+ error: `AlloyDB cluster '${cluster}' has no primary instance, so there ` +
313
+ `is no instance to connect to database '${store.database}' through.`,
314
+ };
315
+ }
316
+ if (primary.state !== 'READY') {
317
+ return {
318
+ error: `AlloyDB cluster '${cluster}' has primary instance '${instance}' in state ${primary.state ?? 'STATE_UNSPECIFIED'}; it ` +
319
+ `must be READY to connect to database '${store.database}' through it.`,
320
+ };
321
+ }
322
+ return { ...store, instance };
323
+ }
324
+ // The message of a failed API call, after the HTTP status.
325
+ function apiError(res) {
326
+ const message = res.message?.trim();
327
+ return message ? `${res.status} ${message}` : `${res.status}`;
328
+ }
329
+ /**
330
+ * How a store is written down for a reader: the resource it addresses, with
331
+ * the backend named ahead of it for everything but Spanner, which is the one
332
+ * an unprefixed line has always meant.
333
+ */
334
+ function storeLine(store) {
335
+ switch (store.kind) {
336
+ case 'spanner':
337
+ return `${store.project}/${store.instance}/${store.database}`;
338
+ case 'alloydb':
339
+ return `alloydb:${store.project}/${store.location}/${store.cluster}/` +
340
+ `${store.instance}/${store.database}`;
341
+ case 'bigquery':
342
+ return `bigquery:${store.project}/${store.dataset}`;
343
+ }
344
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Generating an Agent Skill from a semantic model.
3
+ *
4
+ * An Agent Skill (agentskills.io) is a directory holding a `SKILL.md` -- YAML
5
+ * frontmatter naming the skill, then Markdown telling an agent how to do
6
+ * something. Clients load the frontmatter at startup and the body only when
7
+ * the skill looks relevant, so the body has a budget: under 500 lines, and
8
+ * under about 5,000 tokens. Anything longer goes in files the body points at,
9
+ * which the agent reads only if it needs them.
10
+ *
11
+ * That budget decides the shape of what this emits. A model's actions carry
12
+ * their parameters, the rules that gate them and the blast radius of each
13
+ * call, and inlining all of it puts a page of detail about every action in
14
+ * front of an agent that wanted one of them. So `SKILL.md` is a ROUTER: what
15
+ * the model is, a one-line table of the actions, and the parts that are true
16
+ * of every call. Each action's detail is a file under `references/`, named in
17
+ * the table, read on demand. A model with thirty actions costs the same at
18
+ * startup as a model with one.
19
+ *
20
+ * Nothing here decides what an action means. `modelTools` already turns a
21
+ * bound model into the action descriptors a skill needs -- the tool name, the
22
+ * description with the gating rules folded in, the typed parameters, and
23
+ * whether this binding can run it -- so this module renders what `modelTools`
24
+ * returns and derives nothing of its own beyond layout.
25
+ *
26
+ * The skill is written against the LOGICAL model, which is what makes it worth
27
+ * generating once. An action's name, arguments, rules and blast radius are the
28
+ * same wherever it is deployed, because an executor is a physical binding that
29
+ * a profile supplies. So every `references/` page is a fact about the model
30
+ * and nothing else, and everything that reads the binding lives in `SKILL.md`:
31
+ * the store, the executor kinds, which actions this deployment cannot run and
32
+ * why, and the command line to try one with, all gathered under "Running an
33
+ * action", plus the snippet under "Finding a record" that reads the store
34
+ * directly, which needs the store's kind and its coordinates. Point this at a
35
+ * different profile and those move and the reference pages do not.
36
+ *
37
+ * Keeping that true takes some discipline: whether an action is RUNNABLE is a
38
+ * binding fact wearing a logical name, and putting `tool.unavailable` on the
39
+ * action's own page -- which reads naturally, and which this module did at
40
+ * first -- quietly makes every page profile-specific.
41
+ */
42
+ import { SemanticRuntime } from './runtime/runtime';
43
+ /** One file of the generated package, at a path relative to the skill root. */
44
+ export interface SkillFile {
45
+ /** Relative to the skill directory, POSIX-separated. `SKILL.md` is first. */
46
+ path: string;
47
+ text: string;
48
+ }
49
+ /** A generated skill: the directory name it must be written under, and why. */
50
+ export interface SkillPackage {
51
+ /**
52
+ * The skill's name, which is also the directory name it must be written
53
+ * under. The two are required to match, so they come from one place.
54
+ */
55
+ name: string;
56
+ files: SkillFile[];
57
+ /**
58
+ * What a reader should know about the skill that was generated -- a body
59
+ * over its budget, a model that declares nothing to act on. Not errors:
60
+ * every one of them still produces a skill, and the caller decides whether
61
+ * it is the skill it wanted.
62
+ */
63
+ warnings: string[];
64
+ }
65
+ export interface GenerateSkillOptions {
66
+ /** The model to describe, under the binding profile it was read with. */
67
+ runtime: SemanticRuntime;
68
+ /**
69
+ * The skill's name, defaulting to the model's own in the form the spec
70
+ * requires. Validated either way: a name the spec rejects is an error here
71
+ * rather than a skill some clients silently skip.
72
+ */
73
+ name?: string;
74
+ }
75
+ /**
76
+ * The skill for one model, as files a caller writes out.
77
+ *
78
+ * Returns an error for a name the Agent Skills spec would reject. It also
79
+ * returns the store error when the model's deployments are rejected, and
80
+ * when the profile names a store that cannot be resolved for a model with
81
+ * actions, whose skill names the store. A model with no actions is not an
82
+ * error -- it is a skill that says the model declares nothing to act on, which
83
+ * is the honest thing to hand an agent -- but it is warned about, because it
84
+ * is rarely what the caller meant.
85
+ */
86
+ export declare function generateSkill(opts: GenerateSkillOptions): SkillPackage | {
87
+ error: string;
88
+ };
89
+ /**
90
+ * A model name in the form the spec requires: 1-64 characters of lowercase
91
+ * `a-z0-9-`, with no leading, trailing or doubled hyphen.
92
+ *
93
+ * `Commerce` and `commerce_demo` are both ordinary model names and neither is
94
+ * a legal skill name, so this is a conversion rather than a check. What it
95
+ * cannot fix -- a name with no alphanumeric character in it at all -- falls
96
+ * through to the validator, which says so.
97
+ */
98
+ export declare function skillNameFor(modelName: string): string;
99
+ /**
100
+ * Why this name is not a legal skill name, or undefined if it is.
101
+ *
102
+ * Worth being strict about here rather than leaving to the client. A skill
103
+ * whose `name` does not match its directory is loaded by a lenient client and
104
+ * SKIPPED by a strict one -- the Agent Plugins spec requires a client to skip
105
+ * a non-conformant skill -- so the failure shows up as a skill that quietly
106
+ * does nothing on somebody else's machine. This generator names the directory
107
+ * from the same string it writes into the frontmatter, so the pair cannot
108
+ * disagree; the rest is the character rules.
109
+ */
110
+ export declare function whyNameIsInvalid(name: string): string | undefined;