@google-cloud/kcmd 0.0.0-stage → 0.2.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 +264 -2
  3. package/build/ts/kcmd/cli.js +20252 -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 +18 -0
  9. package/build/ts/kcmd/gcp/biglake.js +59 -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 +73 -0
  21. package/build/ts/kcmd/gcp/spanner.js +126 -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 +386 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +621 -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 +110 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +113 -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 +99 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +469 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +92 -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 +104 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -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 +44 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1698 -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 +37 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +839 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +751 -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 +102 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +188 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +258 -0
  93. package/build/ts/kcmd/semantic/loader.js +3576 -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 +910 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
  99. package/build/ts/kcmd/semantic/parameters.js +171 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +272 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -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 +88 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +301 -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 +17 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +311 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +341 -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 +23 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -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 +47 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +400 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
  129. package/build/ts/kcmd/semantic/transpile.js +581 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +70 -0
  131. package/build/ts/kcmd/semantic/validate.js +2266 -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 +37 -0
  137. package/build/ts/kcmd/snapshot.js +386 -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 +734 -0
  150. package/package.json +62 -4
@@ -0,0 +1,621 @@
1
+ "use strict";
2
+ // Implements the SemanticModel layout.
3
+ //
4
+ // The whole model definition is a single Apache Ossie document per model,
5
+ // located at `catalog/EntryGroups/<entryGroupId>/<model>.yaml`, with its
6
+ // profile files beside it. Each Knowledge Catalog entry of the model may have
7
+ // an overview sidecar at its entry id under the same directory:
8
+ // `<model>.overview.md` for the model and, for example,
9
+ // `<model>/entities/<entity>.overview.md` for an entity.
10
+ //
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.ProfileFileError = exports.SemanticModelLayout = void 0;
46
+ exports.profileFilesBeside = profileFilesBeside;
47
+ exports.profileFileNamesBeside = profileFileNamesBeside;
48
+ exports.vanillaSiblingError = vanillaSiblingError;
49
+ exports.legacyProfileFiles = legacyProfileFiles;
50
+ exports.legacyProfileDirError = legacyProfileDirError;
51
+ exports.fileNameOf = fileNameOf;
52
+ exports.parseOverviewSidecar = parseOverviewSidecar;
53
+ exports.formatOverviewSidecar = formatOverviewSidecar;
54
+ exports.checkProfileFileName = checkProfileFileName;
55
+ exports.profileNameError = profileNameError;
56
+ exports.profileParseError = profileParseError;
57
+ const glob = __importStar(require("glob"));
58
+ const fs = __importStar(require("node:fs"));
59
+ const path = __importStar(require("node:path"));
60
+ const yaml = __importStar(require("yaml"));
61
+ const loader_1 = require("../semantic/loader");
62
+ const names_1 = require("../semantic/names");
63
+ const yaml_options_1 = require("../semantic/yaml_options");
64
+ const documents_1 = require("./documents");
65
+ // Sidecar suffixes that are NOT model documents.
66
+ const SIDECAR_SUFFIXES = ['.aspects.yaml', '.overview.yaml'];
67
+ // A binding-profile file sits beside its model as `<model>.profile.<name>.yaml`.
68
+ // `<model>.profile.yaml`, with no name, is not a model either.
69
+ const PROFILE_FILE = /^(.+)\.profile\.([^.]+)\.yaml$/;
70
+ // Any file shaped like a profile file, including a nameless one and one whose
71
+ // profile name has a dot, which is never a model either.
72
+ const ANY_PROFILE_FILE = /\.profile\.(.*\.)?yaml$/;
73
+ // The model and profile a `<model>.profile.<name>.yaml` file name names, or
74
+ // undefined when `fileName` is not one.
75
+ function profileFileName(fileName) {
76
+ const m = fileName.match(PROFILE_FILE);
77
+ return m ? { model: m[1], profile: m[2] } : undefined;
78
+ }
79
+ // Each `<model>.profile.<name>.yaml` in `dir`, by profile name.
80
+ function profileFilesBeside(dir, model) {
81
+ if (!fs.existsSync(dir))
82
+ return [];
83
+ const out = [];
84
+ for (const f of fs.readdirSync(dir)) {
85
+ const named = profileFileName(f);
86
+ if (named?.model === model) {
87
+ out.push({ name: named.profile, path: path.join(dir, f) });
88
+ }
89
+ }
90
+ return out.sort((a, b) => a.name.localeCompare(b.name));
91
+ }
92
+ // Each `<model>.profile.*.yaml` in `dir`, whatever its profile name: the
93
+ // nameless file first, then those whose profile name is empty or has a dot, by
94
+ // file name, then the rest by profile name.
95
+ function profileFileNamesBeside(dir, model) {
96
+ if (!fs.existsSync(dir))
97
+ return [];
98
+ const named = profileFilesBeside(dir, model).map(p => path.basename(p.path));
99
+ const nameless = `${model}.profile.yaml`;
100
+ const others = fs.readdirSync(dir).filter(f => f.startsWith(`${model}.profile.`) && f.endsWith('.yaml') &&
101
+ f !== nameless && !named.includes(f));
102
+ return [
103
+ ...(fs.existsSync(path.join(dir, nameless)) ? [nameless] : []),
104
+ ...others.sort(),
105
+ ...named,
106
+ ];
107
+ }
108
+ // The suffix of an entry's overview sidecar.
109
+ const OVERVIEW_SIDECAR = '.overview.md';
110
+ // The profile name reserved for the inline bindings in the model file.
111
+ const DEFAULT_PROFILE_NAME = 'default';
112
+ // The flavor that keeps its profiles inline, in a GOOGLE block, and so has no
113
+ // sibling profile files.
114
+ const VANILLA_VERSION = '0.2.0.dev0';
115
+ // Push's error for the profile file `file` beside the vanilla model file
116
+ // `modelFile`, which keeps its profiles in its GOOGLE block.
117
+ function vanillaSiblingError(file, modelFile) {
118
+ return `Profile file '${file}' sits beside '${modelFile}', which is a ` +
119
+ `${VANILLA_VERSION} document; sibling profile files are Google-flavor ` +
120
+ `only, and a ${VANILLA_VERSION} model keeps its profiles in its GOOGLE ` +
121
+ `block.`;
122
+ }
123
+ // The profile files in the older `<model>.profiles/` directory in `dir`, each
124
+ // named by its file basename.
125
+ function legacyProfileFiles(dir, model) {
126
+ const legacy = path.join(dir, `${model}.profiles`);
127
+ if (!fs.statSync(legacy, { throwIfNoEntry: false })?.isDirectory())
128
+ return [];
129
+ const out = [];
130
+ for (const entry of fs.readdirSync(legacy)) {
131
+ if (!entry.endsWith('.yaml'))
132
+ continue;
133
+ if (SIDECAR_SUFFIXES.some(s => entry.endsWith(s)))
134
+ continue;
135
+ out.push({ name: path.basename(entry, '.yaml'), path: path.join(legacy, entry) });
136
+ }
137
+ return out.sort((a, b) => a.name.localeCompare(b.name));
138
+ }
139
+ // Push's error for the older `<model>.profiles/` directory.
140
+ function legacyProfileDirError(model) {
141
+ return `Legacy profile directory '${model}.profiles/' is no longer ` +
142
+ `supported; rename profile files to '${model}.profile.<name>.yaml' ` +
143
+ `beside '${model}.yaml'.`;
144
+ }
145
+ class SemanticModelLayout {
146
+ _catalogPath;
147
+ _entryGroup;
148
+ // Maps a model handle (the document's file basename) to its absolute path.
149
+ _index = new Map();
150
+ constructor(catalogPath, entryGroup) {
151
+ this._catalogPath = catalogPath;
152
+ this._entryGroup = entryGroup;
153
+ }
154
+ async init() {
155
+ this._index.clear();
156
+ if (!fs.existsSync(this._catalogPath)) {
157
+ return;
158
+ }
159
+ // A model document is a top-level `<model>.yaml` under the configured
160
+ // EntryGroup dir, excluding the `.aspects.yaml` / `.overview.yaml`
161
+ // sidecars. Scoping to the manifest's entryGroup keeps unrelated group
162
+ // directories out of the deploy set and avoids the basename collision that
163
+ // a cross-group `EntryGroups/*/*.yaml` glob would produce (same file name
164
+ // in two groups). Fall back to all groups only when no scope was provided.
165
+ const pattern = this._entryGroup ?
166
+ `EntryGroups/${this._entryGroup}/*.yaml` :
167
+ 'EntryGroups/*/*.yaml';
168
+ const matches = await glob.glob(pattern, {
169
+ cwd: this._catalogPath,
170
+ absolute: true,
171
+ nodir: true,
172
+ });
173
+ // glob returns files in directory order, which differs between
174
+ // filesystems. Sorted, push and `kcmd profiles` visit models in the same
175
+ // order everywhere.
176
+ for (const localPath of matches.sort()) {
177
+ if (SIDECAR_SUFFIXES.some(s => localPath.endsWith(s))) {
178
+ continue;
179
+ }
180
+ const base = path.basename(localPath);
181
+ if (ANY_PROFILE_FILE.test(base))
182
+ continue;
183
+ const name = path.basename(localPath, '.yaml');
184
+ this._index.set(name, localPath);
185
+ }
186
+ }
187
+ // Consistent with listEntries(): this push-only layout exposes no per-entry
188
+ // Knowledge Catalog files, so no entry "exists" at the KC layer. Answering
189
+ // from _index (which holds model-document handles) would report an entry that
190
+ // listEntries() won't return and that loadEntry()/saveEntry() reject. The
191
+ // model documents are surfaced via modelDocuments(), not as entries.
192
+ entryExists(_name) {
193
+ return false;
194
+ }
195
+ // This is a push-only layout: it exposes no per-entry Knowledge Catalog
196
+ // files, so it lists no entries. Returning model handles here would make
197
+ // callers that pair listEntries() with loadEntry() -- e.g. the MCP server --
198
+ // list a model and then throw on read. modelDocuments() is the sole accessor
199
+ // for the authored model documents.
200
+ listEntries() {
201
+ return [];
202
+ }
203
+ // Reads the raw Ossie document text for each discovered model. This is the
204
+ // seam the push path consumes; the Ossie text is parsed to the semantic IR by
205
+ // the loader, not mapped to a Knowledge Catalog entry here.
206
+ modelDocuments() {
207
+ const docs = [];
208
+ for (const [name, localPath] of this._index) {
209
+ docs.push({ name, text: fs.readFileSync(localPath, 'utf8') });
210
+ }
211
+ return docs;
212
+ }
213
+ // Every local profile file of a model, for pull to find the ones the
214
+ // catalog did not return: each sibling file whose name is a valid profile
215
+ // name, with that name, then each file in the older `<model>.profiles/`
216
+ // directory, which push no longer reads. It applies no flavor check. A
217
+ // sibling file with an invalid profile name is left out; push rejects it,
218
+ // naming it.
219
+ localProfileFiles(model) {
220
+ return [
221
+ ...this._siblingProfilePaths(model)
222
+ .filter(p => !(0, names_1.nameProblem)('profile', p.name))
223
+ .map(p => ({ path: p.path, profileName: p.name })),
224
+ ...this._legacyProfilePaths(model).map(p => ({ path: p.path })),
225
+ ];
226
+ }
227
+ // Every profile file of a model on disk, whatever its profile name: each
228
+ // `<model>.profile.*.yaml` in the entry group, as profileFileNamesBeside
229
+ // lists them, then each file in the older `<model>.profiles/` directory.
230
+ profileFilePaths(model) {
231
+ if (!this._entryGroup)
232
+ return [];
233
+ const dir = this._groupDir();
234
+ return [
235
+ ...profileFileNamesBeside(dir, model).map(f => path.join(dir, f)),
236
+ ...this._legacyProfilePaths(model).map(p => p.path),
237
+ ];
238
+ }
239
+ // The overview sidecar of each entry, by entry id. Throws naming a sidecar
240
+ // parseOverviewSidecar rejects.
241
+ overviewSidecars() {
242
+ return new Map(this.overviewSidecarPaths().map(({ entryId, path: p }) => [
243
+ entryId,
244
+ parseOverviewSidecar(fs.readFileSync(p, 'utf8'), path.relative(this._groupDir(), p)),
245
+ ]));
246
+ }
247
+ // Each overview sidecar on disk at a model, entity, metric or relationship
248
+ // path, with the entry id its path names, in path order.
249
+ overviewSidecarPaths() {
250
+ if (!this._entryGroup)
251
+ return [];
252
+ const dir = this._groupDir();
253
+ const files = glob.globSync([
254
+ `*${OVERVIEW_SIDECAR}`,
255
+ `*/{entities,metrics,relationships}/*${OVERVIEW_SIDECAR}`,
256
+ ], { cwd: dir, nodir: true, posix: true });
257
+ return files.sort().map(f => ({
258
+ entryId: f.slice(0, -OVERVIEW_SIDECAR.length),
259
+ path: path.join(dir, f),
260
+ }));
261
+ }
262
+ // The text and path of each profile file beside a model, by profile name.
263
+ // Sibling files are checked first, and the first problem found with any of
264
+ // them throws, naming the file:
265
+ // - a `<model>.profile.yaml` names no profile;
266
+ // - the file's `name:` must equal its filename suffix, because `pull`
267
+ // writes a profile back to the file its name implies;
268
+ // - `default` names the model's inline bindings, so no profile may use it;
269
+ // - a vanilla `0.2.0.dev0` model keeps its profiles in its GOOGLE block, so
270
+ // it must have no sibling profile files;
271
+ // - the files are named after the model, so the model file must declare
272
+ // that name, which `pull` uses to name both.
273
+ profileDocuments(model) {
274
+ const nameless = this._entryGroup ?
275
+ path.join(this._groupDir(), `${model}.profile.yaml`) :
276
+ undefined;
277
+ if (nameless && fs.existsSync(nameless)) {
278
+ throw new Error(profileNameError(path.basename(nameless), ''));
279
+ }
280
+ const prefix = `${model}.profile.`;
281
+ const badName = this._entryGroup && fs.existsSync(this._groupDir()) ?
282
+ fs.readdirSync(this._groupDir()).find(f => {
283
+ if (!f.startsWith(prefix) || !f.endsWith('.yaml'))
284
+ return false;
285
+ const n = f.slice(prefix.length, -'.yaml'.length);
286
+ return f !== `${model}.profile.yaml` &&
287
+ (0, names_1.nameProblem)('profile', n) !== undefined;
288
+ }) :
289
+ undefined;
290
+ if (badName) {
291
+ throw new Error(profileNameError(badName, badName.slice(prefix.length, -'.yaml'.length)));
292
+ }
293
+ const siblings = this._siblingProfilePaths(model);
294
+ if (!siblings.length)
295
+ return [];
296
+ const modelFile = `${model}.yaml`;
297
+ const header = this._modelHeader(model);
298
+ if (header.name !== undefined && header.name !== model) {
299
+ // A name the loader rejects cannot name the files either.
300
+ const problem = (0, names_1.nameProblem)('model', header.name);
301
+ if (problem)
302
+ throw new Error(`Model file '${modelFile}': ${problem}`);
303
+ throw new Error(`Profile file '${path.basename(siblings[0].path)}' is named after ` +
304
+ `'${modelFile}', which declares model '${header.name}'; a profile ` +
305
+ `file is named after its model, so name the model file and its ` +
306
+ `profile files after '${header.name}'.`);
307
+ }
308
+ const version = header.version;
309
+ const docs = [];
310
+ const seen = new Map();
311
+ for (const { name, path: p } of siblings) {
312
+ const file = path.basename(p);
313
+ if (!fs.statSync(p).isFile()) {
314
+ throw new Error(`Profile file '${file}' is not a file.`);
315
+ }
316
+ if (version === VANILLA_VERSION) {
317
+ throw new Error(vanillaSiblingError(file, modelFile));
318
+ }
319
+ const text = fs.readFileSync(p, 'utf8');
320
+ checkProfileFileName(modelFile, file, name, text, seen);
321
+ docs.push({ name, text, path: p });
322
+ }
323
+ return docs;
324
+ }
325
+ // The text of one profile file beside a model, or undefined when the model
326
+ // has no profile of that name. The rules `profileDocuments` enforces apply.
327
+ profileDocument(model, profileName) {
328
+ return this.profileDocuments(model).find(d => d.name === profileName)
329
+ ?.text;
330
+ }
331
+ // The path a profile file for this model and profile name maps to:
332
+ // `<catalog>/EntryGroups/<entryGroup>/<model>.profile.<name>.yaml`. Throws
333
+ // when `profileName` breaks the profile naming rule or `model` holds a path
334
+ // separator, so a name read from elsewhere cannot point outside the entry
335
+ // group.
336
+ profilePath(model, profileName) {
337
+ if (/[\\/]/.test(model)) {
338
+ throw new Error(`Model name '${model}' is not valid in a file name.`);
339
+ }
340
+ if ((0, names_1.nameProblem)('profile', profileName)) {
341
+ throw new Error(`Profile name '${profileName}' is not valid; a profile name is a ` +
342
+ `letter, then letters, digits, underscores or hyphens.`);
343
+ }
344
+ return path.join(this._groupDir(), `${model}.profile.${profileName}.yaml`);
345
+ }
346
+ // Writes a profile file beside its model, creating the EntryGroup directory
347
+ // if needed, for `pull` to write a catalog profile through.
348
+ writeProfileDocument(model, profileName, text) {
349
+ const localPath = this.profilePath(model, profileName);
350
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
351
+ fs.writeFileSync(localPath, text);
352
+ }
353
+ // Deletes a profile file, for `pull --force-remove` to drop a local profile
354
+ // the catalog no longer has.
355
+ removeProfileDocument(model, profileName) {
356
+ const localPath = this.profilePath(model, profileName);
357
+ if (fs.existsSync(localPath))
358
+ fs.rmSync(localPath);
359
+ }
360
+ // Profile files with no model beside them, for push, `kcmd profiles` and
361
+ // `kcmd skills-generate` to reject: any
362
+ // `<prefix>.profile.<name>.yaml`, or nameless `<prefix>.profile.yaml`, where
363
+ // `<prefix>.yaml` does not exist.
364
+ orphanProfilePaths() {
365
+ const dir = this._entryGroup ? this._groupDir() : undefined;
366
+ if (!dir || !fs.existsSync(dir))
367
+ return [];
368
+ return fs.readdirSync(dir)
369
+ .filter(f => {
370
+ if (f.startsWith('.') || !ANY_PROFILE_FILE.test(f))
371
+ return false;
372
+ const prefix = f.slice(0, f.indexOf('.profile.'));
373
+ return !fs.existsSync(path.join(dir, `${prefix}.yaml`));
374
+ })
375
+ .sort()
376
+ .map(f => path.join(dir, f));
377
+ }
378
+ // The older `<model>.profiles/` directories in the EntryGroup dir that still
379
+ // hold a profile file, which push, `kcmd profiles` and `kcmd skills-generate`
380
+ // reject.
381
+ legacyProfileDirs() {
382
+ const dir = this._entryGroup ? this._groupDir() : undefined;
383
+ if (!dir || !fs.existsSync(dir))
384
+ return [];
385
+ return fs.readdirSync(dir)
386
+ .filter(f => f.endsWith('.profiles') &&
387
+ fs.statSync(path.join(dir, f), { throwIfNoEntry: false })
388
+ ?.isDirectory() &&
389
+ this._legacyProfilePaths(f.slice(0, -'.profiles'.length))
390
+ .length > 0)
391
+ .sort()
392
+ .map(f => path.join(dir, f));
393
+ }
394
+ _groupDir() {
395
+ if (!this._entryGroup) {
396
+ throw new Error('SemanticModel layout has no entry group; cannot resolve a profile path.');
397
+ }
398
+ return path.join(this._catalogPath, 'EntryGroups', this._entryGroup);
399
+ }
400
+ _siblingProfilePaths(model) {
401
+ return this._entryGroup ? profileFilesBeside(this._groupDir(), model) : [];
402
+ }
403
+ // `<model>.profiles/*.yaml`, each named by its file basename, for pull to
404
+ // find the stale ones (see localProfileFiles). A model name
405
+ // that pull read from the catalog may hold a path separator, and with it a
406
+ // `..` segment, so such a name has no directory here.
407
+ _legacyProfilePaths(model) {
408
+ if (!this._entryGroup || /[\\/]/.test(model))
409
+ return [];
410
+ return legacyProfileFiles(this._groupDir(), model);
411
+ }
412
+ // The `version` and model `name` a model document declares, each undefined
413
+ // when the document has none. In a document with a YAML error, the version
414
+ // is read as the lookup of its GOOGLE block profiles reads it, and is
415
+ // undefined when the error may change it. The name is undefined then, since
416
+ // the error can change the name the text seems to declare. The loader
417
+ // reports the error.
418
+ _modelHeader(model) {
419
+ const localPath = this._index.get(model);
420
+ if (!localPath)
421
+ return {};
422
+ try {
423
+ const text = fs.readFileSync(localPath, 'utf8');
424
+ let doc;
425
+ try {
426
+ doc = yaml.parse(text, yaml_options_1.YAML_OPTIONS);
427
+ }
428
+ catch {
429
+ // A YAML error; the name stays undefined.
430
+ }
431
+ const name = Array.isArray(doc?.semantic_model) ?
432
+ doc.semantic_model[0]?.name :
433
+ undefined;
434
+ return {
435
+ version: (0, loader_1.modelDocumentVersion)(text),
436
+ name: typeof name === 'string' ? name : undefined,
437
+ };
438
+ }
439
+ catch {
440
+ return {};
441
+ }
442
+ }
443
+ // True when a model document with this handle already exists on disk. `pull`
444
+ // uses it to report which files it would overwrite vs. create.
445
+ hasModel(name) {
446
+ return fs.existsSync(this.modelPath(name));
447
+ }
448
+ // The absolute path a model document with this handle maps to:
449
+ // `<catalog>/EntryGroups/<entryGroup>/<name>.yaml`. Path separators in the
450
+ // model name are replaced so a name still yields a single flat file. Requires
451
+ // the layout to be scoped to an entry group (the semantic-model source always
452
+ // is).
453
+ modelPath(name) {
454
+ if (!this._entryGroup) {
455
+ throw new Error('SemanticModel layout has no entry group; cannot resolve a model path.');
456
+ }
457
+ const file = `${name.replace(/[/\\]/g, '_')}.yaml`;
458
+ return path.join(this._catalogPath, 'EntryGroups', this._entryGroup, file);
459
+ }
460
+ // Writes a model's serialized document to its path, creating the EntryGroup
461
+ // directory if needed, and indexes it so a later modelDocuments() sees it.
462
+ // This is the sink `pull` writes reconstructed models to.
463
+ writeModelDocument(name, text) {
464
+ const localPath = this.modelPath(name);
465
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
466
+ fs.writeFileSync(localPath, text);
467
+ this._index.set(name, localPath);
468
+ }
469
+ // Deletes a model document from disk and the index. `pull --force-remove`
470
+ // uses it to drop a local model the catalog no longer names before writing
471
+ // the catalog's, so the entry group is never left holding two models.
472
+ removeModelDocument(name) {
473
+ const localPath = this.modelPath(name);
474
+ if (fs.existsSync(localPath)) {
475
+ fs.rmSync(localPath);
476
+ }
477
+ this._index.delete(name);
478
+ }
479
+ // The Knowledge Catalog entry-level members are not applicable to this
480
+ // push-only layout; the model is authored as a single Ossie document, not as
481
+ // per-entry Knowledge Catalog files. These are wired when KC-resource emit
482
+ // and semantic-model `pull` land.
483
+ async loadEntry(_name) {
484
+ throw new Error('The SemanticModel layout does not expose per-entry Knowledge Catalog files yet.');
485
+ }
486
+ async saveEntry(_name, _entry) {
487
+ throw new Error('The SemanticModel layout does not support writing per-entry files yet.');
488
+ }
489
+ async deleteEntry(_name) {
490
+ throw new Error('The SemanticModel layout does not support deleting per-entry files yet.');
491
+ }
492
+ }
493
+ exports.SemanticModelLayout = SemanticModelLayout;
494
+ // The name push gives a model file or profile file of the model `model`: its
495
+ // path relative to the entry group directory, with `/` between segments.
496
+ function fileNameOf(layout, model, localPath) {
497
+ return path.relative(path.dirname(layout.modelPath(model)), localPath)
498
+ .split(path.sep)
499
+ .join('/');
500
+ }
501
+ // Reads the overview sidecar `text`. Throws naming `file` when its frontmatter
502
+ // does not parse, holds a key other than contentType and links, gives a
503
+ // contentType other than MARKDOWN or HTML, or a link with no url.
504
+ function parseOverviewSidecar(text, file) {
505
+ const fail = (problem) => new Error(`Overview sidecar '${file}': ${problem}`);
506
+ let split;
507
+ try {
508
+ split = (0, documents_1.splitFrontmatter)(text);
509
+ }
510
+ catch (err) {
511
+ throw fail(`its frontmatter does not parse: ${err?.message ?? err}`);
512
+ }
513
+ const metadata = split.metadata ?? {};
514
+ const extra = typeof metadata === 'object' && !Array.isArray(metadata) ?
515
+ Object.keys(metadata).find(k => k !== 'contentType' && k !== 'links') :
516
+ '';
517
+ if (extra !== undefined) {
518
+ throw fail(`its frontmatter holds only contentType and links` +
519
+ (extra ? `, not '${extra}'.` : '.'));
520
+ }
521
+ const { contentType, links } = metadata;
522
+ if (contentType !== undefined && contentType !== 'MARKDOWN' &&
523
+ contentType !== 'HTML') {
524
+ throw fail(`contentType is MARKDOWN or HTML, not '${contentType}'.`);
525
+ }
526
+ const isLink = (l) => typeof l?.url === 'string' &&
527
+ ['string', 'undefined'].includes(typeof l.title) &&
528
+ Object.keys(l).every(k => k === 'url' || k === 'title');
529
+ if (links !== undefined && !(Array.isArray(links) && links.every(isLink))) {
530
+ throw fail('links is a list of entries, each a url and an optional title.');
531
+ }
532
+ return { content: split.body.trim() || undefined, contentType, links };
533
+ }
534
+ // The text of the overview sidecar that holds `overview`.
535
+ function formatOverviewSidecar({ content = '', ...rest }) {
536
+ // The body ends with a newline, which push trims.
537
+ const body = content && !content.endsWith('\n') ? `${content}\n` : content;
538
+ // Content whose first line is `---` would otherwise read back as frontmatter.
539
+ return Object.keys(rest).length || body.split(/\r?\n/, 1)[0] === '---' ?
540
+ (0, documents_1.joinFrontmatter)(rest, body) :
541
+ body;
542
+ }
543
+ // The error checkProfileFileName throws for a profile file that breaks one of
544
+ // its rules.
545
+ class ProfileFileError extends Error {
546
+ }
547
+ exports.ProfileFileError = ProfileFileError;
548
+ // Throws when the profile file `file`, holding `text` for profile `name` beside
549
+ // the model file `modelFile`, names the same profile as a file in `seen`, which
550
+ // maps each profile name read before, in lower case, to its file. It also
551
+ // throws when the name breaks the naming rule, the file does not parse, uses
552
+ // the legacy `semantic_model:` wrapper, takes the name reserved for the inline
553
+ // bindings, or declares no name or another name. Adds the file to `seen`.
554
+ function checkProfileFileName(modelFile, file, name, text, seen) {
555
+ const badName = profileNameError(file, name);
556
+ if (badName)
557
+ throw new ProfileFileError(badName);
558
+ // Profile names are unique ignoring case.
559
+ const twin = seen.get(name.toLowerCase());
560
+ if (twin) {
561
+ throw new ProfileFileError(`Profile files '${twin}' and '${file}' name the same profile; ` +
562
+ `profile names are unique ignoring case.`);
563
+ }
564
+ seen.set(name.toLowerCase(), file);
565
+ const declared = parseProfileName(text, file, name);
566
+ const reserved = (n) => n?.toLowerCase() === DEFAULT_PROFILE_NAME;
567
+ if (reserved(name)) {
568
+ throw new ProfileFileError(`Profile name '${DEFAULT_PROFILE_NAME}' is reserved for the ` +
569
+ `inline bindings in '${modelFile}'; remove '${file}'.`);
570
+ }
571
+ if (reserved(declared)) {
572
+ throw new ProfileFileError(`Profile name '${DEFAULT_PROFILE_NAME}' is reserved for the ` +
573
+ `inline bindings in '${modelFile}'; change the name in '${file}' to ` +
574
+ `'${name}'.`);
575
+ }
576
+ if (declared === undefined) {
577
+ throw new ProfileFileError(`Profile file '${file}' declares no name; add 'name: ${name}'.`);
578
+ }
579
+ if (declared !== name) {
580
+ throw new ProfileFileError(`Profile file '${file}' declares name '${declared}', which does not ` +
581
+ `match filename suffix '${name}'.`);
582
+ }
583
+ }
584
+ // Push's error for the profile file `file` whose profile name `name` breaks
585
+ // the naming rule, or undefined. A `<model>.profile.yaml` names no profile.
586
+ function profileNameError(file, name) {
587
+ const nameless = file.match(/^(.*)\.profile\.yaml$/);
588
+ if (nameless && name === '') {
589
+ return `Profile file '${file}' names no profile; rename it ` +
590
+ `'${nameless[1]}.profile.<name>.yaml'.`;
591
+ }
592
+ if ((0, names_1.nameProblem)('profile', name) === undefined)
593
+ return undefined;
594
+ return `Profile file '${file}' has profile name '${name}'; a profile name ` +
595
+ `is a letter followed by letters, digits, underscores and hyphens.`;
596
+ }
597
+ // Push's error for the profile file `file`, which does not parse, with the
598
+ // parser's `message`.
599
+ function profileParseError(file, message) {
600
+ return `Profile file '${file}' does not parse: ${message}`;
601
+ }
602
+ // The `name` a profile file declares at its top level, or undefined when it
603
+ // declares none. A file that does not parse or that uses the legacy
604
+ // `semantic_model:` wrapper throws, naming the file.
605
+ function parseProfileName(text, file, name) {
606
+ let doc;
607
+ try {
608
+ doc = yaml.parse(text, yaml_options_1.YAML_OPTIONS);
609
+ }
610
+ catch (err) {
611
+ throw new ProfileFileError(profileParseError(file, err?.message ?? err));
612
+ }
613
+ if (doc?.semantic_model !== undefined) {
614
+ throw new ProfileFileError(`Profile file '${file}' uses the legacy 'semantic_model:' wrapper, ` +
615
+ `which is no longer supported; write its entities, relationships ` +
616
+ `and actions at the top level under 'name: ${name}', and move any ` +
617
+ `'deployment_target' to an entry under 'deployments:' in the model ` +
618
+ `file with 'profile: ${name}'.`);
619
+ }
620
+ return typeof doc?.name === 'string' ? doc.name : undefined;
621
+ }
@@ -0,0 +1,13 @@
1
+ import { CatalogLayout } from '../layout';
2
+ import * as md from '../metadata';
3
+ export declare class StandardLayout implements CatalogLayout {
4
+ private readonly _catalogPath;
5
+ private readonly _index;
6
+ constructor(catalogPath: string);
7
+ init(): Promise<void>;
8
+ entryExists(name: string): boolean;
9
+ listEntries(): string[];
10
+ loadEntry(name: string): Promise<md.Entry>;
11
+ saveEntry(name: string, entry: md.Entry): Promise<void>;
12
+ deleteEntry(name: string): Promise<void>;
13
+ }