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