@google-cloud/kcmd 0.2.0 → 0.2.1

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.
@@ -83,15 +83,13 @@ exports.modelDocument = modelDocument;
83
83
  exports.compact = compact;
84
84
  exports.nonEmpty = nonEmpty;
85
85
  const yaml = __importStar(require("yaml"));
86
- const deployment_target_1 = require("./deployment_target");
87
86
  const ir_1 = require("./ir");
88
87
  const loader_1 = require("./loader");
89
88
  const names_1 = require("./names");
90
89
  const resolve_inheritance_1 = require("./resolve_inheritance");
91
90
  const resolve_profiles_1 = require("./resolve_profiles");
92
91
  // The vendor tag for Google-specific extension blocks on the IR (kept in sync
93
- // with loader.GOOGLE_VENDOR). In the Google flavor, a model-level deployment
94
- // target list rides in one as a folded shim alongside `model.deployments`.
92
+ // with loader.GOOGLE_VENDOR).
95
93
  const GOOGLE_VENDOR = 'GOOGLE';
96
94
  // The dialect label for the IR's target/canonical `expression` when `dialects`
97
95
  // is absent and `stringForm` is not set. BIGQUERY is the loader's default
@@ -340,17 +338,16 @@ function modelDoc(model, warnings, logical, version) {
340
338
  `at least one and will not load until an entity is present.`);
341
339
  }
342
340
  const tableKey = isGoogleVersion ? 'entities' : 'datasets';
343
- // Under GOOGLE_VERSION, deployments come from `model.deployments`, or from a
344
- // GOOGLE `deploymentTargets` block when that is empty. Under OSSIE_VERSION,
345
- // named deployments and profiles go in a GOOGLE block on the model.
341
+ // Under GOOGLE_VERSION, deployments come from `model.deployments`. Under
342
+ // OSSIE_VERSION, named deployments and profiles go in a GOOGLE block on the
343
+ // model.
346
344
  const namedDeployments = (model.deployments ?? []).map(d => compact({
347
345
  name: d.name,
348
346
  target: d.target,
349
347
  profile: d.profile
350
348
  }));
351
349
  const deployments = placement.model.deployments === 'native' ?
352
- nonEmpty(namedDeployments.length ? namedDeployments :
353
- deploymentsFromGoogleExtensions(model)) :
350
+ nonEmpty(namedDeployments) :
354
351
  undefined;
355
352
  const blockProfiles = placement.model.profiles === 'block' ?
356
353
  nonEmpty(profileDocuments(model, warnings).map(p => p.doc)) :
@@ -385,8 +382,7 @@ function modelDoc(model, warnings, logical, version) {
385
382
  constraints: placement.model.constraints === 'native' ?
386
383
  nonEmpty((model.constraints ?? []).map(c => constraintDoc(c))) :
387
384
  undefined,
388
- custom_extensions: customExtensionsDoc(model.customExtensions, `model '${model.name}'`, isGoogleVersion,
389
- /* isModel= */ true, warnings, /* isField= */ false, blockPayload),
385
+ custom_extensions: customExtensionsDoc(model.customExtensions, `model '${model.name}'`, isGoogleVersion, warnings, /* isField= */ false, blockPayload),
390
386
  });
391
387
  }
392
388
  // Warns about each IR construct OSSIE_VERSION has no representation for,
@@ -449,8 +445,7 @@ function datasetDoc(entity, warnings, logical, version) {
449
445
  ai_context: aiContextDoc(entity.aiContext, isGoogleVersion),
450
446
  fields: nonEmpty((entity.fields ?? [])
451
447
  .map(f => fieldDoc(f, warnings, logical || (isGoogleVersion && !!entity.abstract), isGoogleVersion))),
452
- custom_extensions: customExtensionsDoc(entity.customExtensions, `entity '${entity.name}'`, isGoogleVersion,
453
- /* isModel= */ false, warnings, /* isField= */ false, blockPayload),
448
+ custom_extensions: customExtensionsDoc(entity.customExtensions, `entity '${entity.name}'`, isGoogleVersion, warnings, /* isField= */ false, blockPayload),
454
449
  });
455
450
  }
456
451
  // Filters out the IR's `'Opaque'` placeholder so untyped fields and metrics
@@ -482,8 +477,7 @@ function fieldDoc(field, warnings, logical, isGoogleVersion) {
482
477
  dimension: dimensionDoc(field),
483
478
  description: field.description,
484
479
  ai_context: aiContextDoc(field.aiContext, isGoogleVersion),
485
- custom_extensions: customExtensionsDoc(field.customExtensions, `field '${field.name}'`, isGoogleVersion,
486
- /* isModel= */ false, warnings, /* isField= */ true),
480
+ custom_extensions: customExtensionsDoc(field.customExtensions, `field '${field.name}'`, isGoogleVersion, warnings, /* isField= */ true),
487
481
  });
488
482
  }
489
483
  function metricDoc(metric, warnings, version) {
@@ -508,8 +502,7 @@ function metricDoc(metric, warnings, version) {
508
502
  datatype: datatypeDoc(metric.type),
509
503
  description: metric.description,
510
504
  ai_context: aiContextDoc(metric.aiContext, isGoogleVersion),
511
- custom_extensions: customExtensionsDoc(metric.customExtensions, `metric '${metric.name}'`, isGoogleVersion,
512
- /* isModel= */ false, warnings, /* isField= */ false, blockPayload),
505
+ custom_extensions: customExtensionsDoc(metric.customExtensions, `metric '${metric.name}'`, isGoogleVersion, warnings, /* isField= */ false, blockPayload),
513
506
  });
514
507
  }
515
508
  // The `Concept.field` pairs this document will still resolve on reload. A
@@ -697,8 +690,7 @@ function relationshipDoc(rel, warnings, version) {
697
690
  description: placement.relationship.description === 'native' ? description :
698
691
  undefined,
699
692
  ai_context: aiContextDoc(rel.aiContext, isGoogleVersion),
700
- custom_extensions: customExtensionsDoc(rel.customExtensions, `relationship '${rel.name}'`, isGoogleVersion,
701
- /* isModel= */ false, warnings, /* isField= */ false, blockPayload),
693
+ custom_extensions: customExtensionsDoc(rel.customExtensions, `relationship '${rel.name}'`, isGoogleVersion, warnings, /* isField= */ false, blockPayload),
702
694
  });
703
695
  }
704
696
  // Renders a field or metric `expression` for the target version. The IR comes
@@ -796,24 +788,14 @@ function aiContextDoc(ai, isGoogleVersion) {
796
788
  }
797
789
  return isGoogleVersion ? { ...base, custom } : { ...custom, ...base };
798
790
  }
799
- // The deployments a vanilla model lists only as targets in its GOOGLE blocks,
800
- // named as a pull names them. A block whose data is not valid JSON gives none,
801
- // and customExtensionsDoc warns that it was dropped. The other blocks still
802
- // give theirs.
803
- function deploymentsFromGoogleExtensions(model) {
804
- const targets = (model.customExtensions ??
805
- []).flatMap(ext => blockTargets(ext, model.name));
806
- return (0, deployment_target_1.unnamedDeployments)(targets);
807
- }
808
791
  // Serializes `custom_extensions` for one IR element. Under OSSIE_VERSION,
809
792
  // third-party extensions are emitted, followed by a synthesized GOOGLE block
810
- // when `blockPayload` carries any keys. Any other raw GOOGLE block on the IR
793
+ // when `blockPayload` carries any keys. Any raw GOOGLE block on the IR
811
794
  // is dropped with a warning so it cannot collide with the synthesized block or
812
795
  // write keys the loader rejects. Under GOOGLE_VERSION,
813
796
  // third-party extensions are written out unchanged without warning, while
814
- // `GOOGLE` blocks are skipped (silently for the model's `deploymentTargets`
815
- // block, and with a warning for any other `GOOGLE` block).
816
- function customExtensionsDoc(exts, where, isGoogleVersion, isModel, warnings, isField = false, blockPayload) {
797
+ // `GOOGLE` blocks are skipped with a warning.
798
+ function customExtensionsDoc(exts, where, isGoogleVersion, warnings, isField = false, blockPayload) {
817
799
  const payload = blockPayload ? { ...blockPayload } : {};
818
800
  if (!exts?.length && Object.keys(payload).length === 0)
819
801
  return undefined;
@@ -825,12 +807,6 @@ function customExtensionsDoc(exts, where, isGoogleVersion, isModel, warnings, is
825
807
  kept.push({ vendor_name: e.vendorName, data: e.data });
826
808
  continue;
827
809
  }
828
- // The loader's copy of the model's `deployments`, which the GOOGLE
829
- // block below writes in their own form.
830
- if (isModel && payload.deployments !== undefined &&
831
- isDeploymentTargetsBlock(e)) {
832
- continue;
833
- }
834
810
  droppedGoogle += 1;
835
811
  }
836
812
  if (droppedGoogle > 0) {
@@ -859,8 +835,6 @@ function customExtensionsDoc(exts, where, isGoogleVersion, isModel, warnings, is
859
835
  kept.push({ vendor_name: ext.vendorName, data: ext.data });
860
836
  continue;
861
837
  }
862
- if (isModel && blockTargets(ext, '').length > 0)
863
- continue;
864
838
  droppedGoogle += 1;
865
839
  }
866
840
  if (droppedGoogle > 0) {
@@ -869,30 +843,6 @@ function customExtensionsDoc(exts, where, isGoogleVersion, isModel, warnings, is
869
843
  }
870
844
  return nonEmpty(kept);
871
845
  }
872
- /**
873
- * Returns true if `e.data` is a JSON object with a `'deploymentTargets'`
874
- * property.
875
- *
876
- * The loader folds a Google-flavor model's `deployments` into such a block.
877
- */
878
- function isDeploymentTargetsBlock(e) {
879
- try {
880
- const d = JSON.parse(e.data);
881
- return !!d && typeof d === 'object' &&
882
- Object.hasOwn(d, 'deploymentTargets');
883
- }
884
- catch {
885
- return false;
886
- }
887
- }
888
- function blockTargets(ext, modelName) {
889
- try {
890
- return (0, deployment_target_1.googleBlockTargets)([ext], modelName);
891
- }
892
- catch {
893
- return [];
894
- }
895
- }
896
846
  // Drops undefined-valued keys so the emitted YAML only shows fields the model
897
847
  // actually set (matching the emitters' `compact` convention).
898
848
  function compact(obj) {
@@ -23,17 +23,15 @@ export interface SemanticRuntime {
23
23
  * profile's several targets may leave the store ambiguous, the model may
24
24
  * bind a table outside its Spanner or AlloyDB store, or finding how to
25
25
  * connect may fail, such as the lookup of an AlloyDB cluster's primary
26
- * instance. Also set when a GOOGLE block's data is not valid JSON, so its
27
- * targets cannot be read. `storeError` says why.
26
+ * instance. `storeError` says why.
28
27
  */
29
28
  storeFailed?: boolean;
30
29
  /**
31
30
  * Set with `storeFailed` when the model's deployments give no store
32
31
  * whatever the model is used for. One such case is a target in
33
- * no accepted form in any deployment. Another is a GOOGLE block that is not
34
- * valid JSON. A third is several targets for the profile with no single
35
- * store. A table bound outside the store and a failed AlloyDB lookup are
36
- * not such cases.
32
+ * no accepted form in any deployment. Another is several targets for the
33
+ * profile with no single store. A table bound outside the store and a failed
34
+ * AlloyDB lookup are not such cases.
37
35
  */
38
36
  deploymentsRejected?: boolean;
39
37
  /** Which binding profile produced this. Provenance, for messages. */
@@ -50,13 +50,13 @@ export type ChosenStore = SpannerStore | AlloyDbDatabase | BigQueryStore;
50
50
  * `named` is set when the model's deployments name a target and still no
51
51
  * store results, so a caller that runs against the store fails. `rejected` is
52
52
  * set with it when the deployments give no store whatever the model is used
53
- * for: a target in no accepted form in any deployment, a GOOGLE block whose
54
- * data is not valid JSON, or several targets for the profile. `named` alone
55
- * means the model binds a table outside its Spanner or AlloyDB store.
53
+ * for: a target in no accepted form in any deployment, or several targets for
54
+ * the profile. `named` alone means the model binds a table outside its Spanner
55
+ * or AlloyDB store.
56
56
  *
57
57
  * Neither is set when the profile names no store, unless a target is in no
58
- * accepted form, or a GOOGLE block is not valid JSON. Neither is set either
59
- * when a BigQuery model binds a table outside the graph's dataset.
58
+ * accepted form. Neither is set either when a BigQuery model binds a table
59
+ * outside the graph's dataset.
60
60
  */
61
61
  export interface StoreError {
62
62
  error: string;
@@ -67,21 +67,18 @@ export interface StoreError {
67
67
  * The store a model runs against under binding profile `profile`, which
68
68
  * defaults to `default`, the model file's own bindings. It is the one the
69
69
  * deployments for that profile name: those that name it in `profile`, or that
70
- * name none or `default` for `default`. A model with no `deployments` list
71
- * names no profile for any of its targets, so they are all for `default`.
72
- * With several targets, the store is the single operational one, Spanner or
73
- * AlloyDB.
70
+ * name none or `default` for `default`. With several targets, the store is the
71
+ * single operational one, Spanner or AlloyDB.
74
72
  *
75
- * Fails six ways:
73
+ * Fails five ways:
76
74
  *
77
- * 1. A GOOGLE block's data is not valid JSON, so its targets cannot be read.
78
- * 2. A target names no supported destination. Push checks the target of every
75
+ * 1. A target names no supported destination. Push checks the target of every
79
76
  * deployment, so this fails the store under every profile.
80
- * 3. No deployment uses the profile.
81
- * 4. There are several targets and no single operational one, so which store
77
+ * 2. No deployment uses the profile.
78
+ * 3. There are several targets and no single operational one, so which store
82
79
  * is meant is ambiguous.
83
- * 5. There is no target, so there is nothing to run against.
84
- * 6. The model binds a table outside the store. For a Spanner or AlloyDB
80
+ * 4. There is no target, so there is nothing to run against.
81
+ * 5. The model binds a table outside the store. For a Spanner or AlloyDB
85
82
  * store, a write would land in the wrong place, and the error is named. A
86
83
  * BigQuery graph may read tables in any dataset, so
87
84
  * for a BigQuery store the error is not named.
@@ -89,9 +86,7 @@ export interface StoreError {
89
86
  export declare function resolveStore(model: SemanticModel, profile?: string): ChosenStore | StoreError;
90
87
  /**
91
88
  * The targets binding profile `profile` uses, as resolveStore reads them: its
92
- * deployments' targets, or every target in a GOOGLE block for `default` when
93
- * the model has no `deployments` list. Empty when no deployment is for the
94
- * profile. Throws when a GOOGLE block's data is not valid JSON.
89
+ * deployments' targets. Empty when no deployment is for the profile.
95
90
  */
96
91
  export declare function profileTargets(model: SemanticModel, profile: string): string[];
97
92
  /**
@@ -101,21 +101,18 @@ function strayBindings(model, store) {
101
101
  * The store a model runs against under binding profile `profile`, which
102
102
  * defaults to `default`, the model file's own bindings. It is the one the
103
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.
104
+ * name none or `default` for `default`. With several targets, the store is the
105
+ * single operational one, Spanner or AlloyDB.
108
106
  *
109
- * Fails six ways:
107
+ * Fails five ways:
110
108
  *
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
109
+ * 1. A target names no supported destination. Push checks the target of every
113
110
  * 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
111
+ * 2. No deployment uses the profile.
112
+ * 3. There are several targets and no single operational one, so which store
116
113
  * 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
114
+ * 4. There is no target, so there is nothing to run against.
115
+ * 5. The model binds a table outside the store. For a Spanner or AlloyDB
119
116
  * store, a write would land in the wrong place, and the error is named. A
120
117
  * BigQuery graph may read tables in any dataset, so
121
118
  * for a BigQuery store the error is not named.
@@ -123,15 +120,7 @@ function strayBindings(model, store) {
123
120
  function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
124
121
  // Push checks the target of every deployment the model declares, so the
125
122
  // 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
- }
123
+ const all = (0, deployment_target_1.googleDeploymentTargets)(model);
135
124
  // The loader reads a vanilla model's `deployments` out of its GOOGLE block,
136
125
  // so a message names the block's `deployments`, as push does.
137
126
  const vanilla = model.version === loader_1.OSSIE_VERSION;
@@ -141,17 +130,19 @@ function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
141
130
  // A target in no accepted form is reported, alone or beside one that gives
142
131
  // a store, whichever profile its deployment is for.
143
132
  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 };
133
+ const unparsed = model.deployments.find(d => d.target === all.malformed[0]);
134
+ return {
135
+ error: (0, deployment_target_1.deploymentTargetError)(model, unparsed),
136
+ named: true,
137
+ rejected: true,
138
+ };
148
139
  }
149
140
  const used = deploymentsFor(model, profile);
150
141
  if ('error' in used)
151
142
  return used;
152
143
  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.
144
+ // The profile's targets are among those read above, so none of them is in no
145
+ // accepted form.
155
146
  const targets = (0, deployment_target_1.googleDeploymentTargets)(scoped);
156
147
  const { spanner, alloyDb, bigQuery } = targets;
157
148
  const declared = spanner.length + alloyDb.length + bigQuery.length;
@@ -202,9 +193,7 @@ function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
202
193
  return store;
203
194
  }
204
195
  // 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.
196
+ // A model with no or empty `deployments` list names no deployment.
208
197
  function deploymentsFor(model, profile) {
209
198
  const nothingToRun = `so there is no database to run it against`;
210
199
  const namedProfile = `binding profile '${profile}' is a named profile, ` +
@@ -235,21 +224,16 @@ function deploymentsFor(model, profile) {
235
224
  }
236
225
  /**
237
226
  * 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.
227
+ * deployments' targets. Empty when no deployment is for the profile.
241
228
  */
242
229
  function profileTargets(model, profile) {
243
230
  const used = deploymentsFor(model, profile);
244
231
  return 'error' in used ? [] : (0, deployment_target_1.googleDeploymentTargets)(used.model).uris;
245
232
  }
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
233
+ // The deployments whose targets are `uris`, for a message. `profile` is the
248
234
  // profile they are for.
249
235
  function deploymentNames(model, uris, profile) {
250
- const deployments = model.deployments;
251
- if (!deployments)
252
- return '';
236
+ const deployments = model.deployments ?? [];
253
237
  const names = uris.map(uri => deployments.find(d => d.target === uri)?.name ?? uri);
254
238
  return ` for profile '${profile}' (${names.join(', ')})`;
255
239
  }
@@ -7,8 +7,7 @@
7
7
  // empty array means valid); push prints them and aborts, and `kcmd import`
8
8
  // reports them. Kept separate from the loader -- which validates a document
9
9
  // against the schema -- because these are deployment requirements, not schema
10
- // rules, and they read the GOOGLE deployment-target extension the BigQuery leg
11
- // owns.
10
+ // rules.
12
11
  Object.defineProperty(exports, "__esModule", { value: true });
13
12
  exports.validatePushRequirements = validatePushRequirements;
14
13
  exports.validateDeployments = validateDeployments;
@@ -60,18 +59,7 @@ function validatePushRequirements(models, opts = {}) {
60
59
  const errors = [];
61
60
  for (const { document, model } of models) {
62
61
  errors.push(...nameAndDescriptionErrors(model, document, !!opts.v2Aspects));
63
- let deployInfo;
64
- try {
65
- // One pass over the model's GOOGLE extension(s): both checks below read
66
- // the same parse rather than re-parsing the JSON per reader.
67
- deployInfo = (0, deploy_bigquery_1.googleDeploymentTargets)(model);
68
- }
69
- catch (err) {
70
- // Malformed GOOGLE extension JSON: surface it as a validation error here
71
- // rather than letting it throw out of a later leg as an uncaught stack.
72
- errors.push(`${err.message || err} (${document})`);
73
- continue;
74
- }
62
+ const deployInfo = (0, deploy_bigquery_1.googleDeploymentTargets)(model);
75
63
  // A model that targets a graph (BigQuery OR Spanner) must have every
76
64
  // relationship's join columns bound. The loader accepts a column-less
77
65
  // relationship so a purely logical model (an OWL import) loads and pushes
@@ -2089,16 +2077,8 @@ async function validateBigQueryDataSources(models, bq, defaultProject, graph) {
2089
2077
  // bills to: the project of the model's first BigQuery Graph deployment target
2090
2078
  // (where the CREATE PROPERTY GRAPH runs), falling back to the scope's default
2091
2079
  // project when the model declares no parseable BigQuery Graph target.
2092
- // googleDeploymentTargets is safe here: validatePushRequirements ran first and
2093
- // already rejected a malformed GOOGLE extension.
2094
2080
  function billingProject(model, defaultProject) {
2095
- try {
2096
- return (0, deploy_bigquery_1.googleDeploymentTargets)(model).bigQuery[0]?.project ??
2097
- defaultProject;
2098
- }
2099
- catch {
2100
- return defaultProject;
2101
- }
2081
+ return (0, deploy_bigquery_1.googleDeploymentTargets)(model).bigQuery[0]?.project ?? defaultProject;
2102
2082
  }
2103
2083
  // A source that can be probed as a BigQuery table: the canonical `dataSource`,
2104
2084
  // trimmed, or null when it is empty or a non-BigQuery resource URI. Unlike a
@@ -2181,13 +2161,7 @@ async function validateSpannerActionStatements(models, spanner) {
2181
2161
  // The database an action writes to is the one this profile deploys its
2182
2162
  // graph to: on Spanner the published graph and the operational rows live in
2183
2163
  // the same database. A model with no Spanner target contributes nothing.
2184
- let target;
2185
- try {
2186
- target = (0, deployment_target_1.spannerGraphTargets)(loaded.model).targets[0];
2187
- }
2188
- catch {
2189
- continue;
2190
- }
2164
+ const target = (0, deployment_target_1.spannerGraphTargets)(loaded.model).targets[0];
2191
2165
  if (!target)
2192
2166
  continue;
2193
2167
  const key = `${target.project}/${target.instance}/${target.database}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/kcmd",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Google Knowledge Catalog Metadata as Code Library, CLI and MCP Server",
5
5
  "repository": {
6
6
  "type": "git",