@intentius/chant-lexicon-gitlab 0.0.6 → 0.0.9

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 (52) hide show
  1. package/dist/integrity.json +10 -6
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +186 -8
  4. package/dist/rules/wgl012.ts +86 -0
  5. package/dist/rules/wgl013.ts +62 -0
  6. package/dist/rules/wgl014.ts +51 -0
  7. package/dist/rules/wgl015.ts +85 -0
  8. package/dist/rules/yaml-helpers.ts +65 -3
  9. package/dist/skills/chant-gitlab.md +502 -0
  10. package/dist/types/index.d.ts +55 -16
  11. package/package.json +2 -2
  12. package/src/codegen/__snapshots__/snapshot.test.ts.snap +58 -0
  13. package/src/codegen/docs.ts +88 -11
  14. package/src/codegen/generate-lexicon.ts +6 -1
  15. package/src/codegen/generate.ts +45 -50
  16. package/src/codegen/naming.ts +3 -0
  17. package/src/codegen/package.ts +2 -0
  18. package/src/codegen/parse.test.ts +154 -4
  19. package/src/codegen/parse.ts +161 -49
  20. package/src/codegen/snapshot.test.ts +7 -5
  21. package/src/composites/composites.test.ts +452 -0
  22. package/src/composites/docker-build.ts +81 -0
  23. package/src/composites/index.ts +8 -0
  24. package/src/composites/node-pipeline.ts +104 -0
  25. package/src/composites/python-pipeline.ts +75 -0
  26. package/src/composites/review-app.ts +63 -0
  27. package/src/generated/index.d.ts +55 -16
  28. package/src/generated/index.ts +3 -0
  29. package/src/generated/lexicon-gitlab.json +186 -8
  30. package/src/import/generator.ts +3 -2
  31. package/src/import/parser.test.ts +3 -3
  32. package/src/import/parser.ts +12 -26
  33. package/src/index.ts +4 -0
  34. package/src/lint/post-synth/wgl012.test.ts +131 -0
  35. package/src/lint/post-synth/wgl012.ts +86 -0
  36. package/src/lint/post-synth/wgl013.test.ts +164 -0
  37. package/src/lint/post-synth/wgl013.ts +62 -0
  38. package/src/lint/post-synth/wgl014.test.ts +97 -0
  39. package/src/lint/post-synth/wgl014.ts +51 -0
  40. package/src/lint/post-synth/wgl015.test.ts +139 -0
  41. package/src/lint/post-synth/wgl015.ts +85 -0
  42. package/src/lint/post-synth/yaml-helpers.ts +65 -3
  43. package/src/lsp/completions.ts +2 -0
  44. package/src/lsp/hover.ts +2 -0
  45. package/src/plugin.test.ts +44 -19
  46. package/src/plugin.ts +671 -76
  47. package/src/serializer.test.ts +146 -6
  48. package/src/serializer.ts +64 -14
  49. package/src/validate.ts +1 -0
  50. package/src/variables.ts +4 -0
  51. package/dist/skills/gitlab-ci.md +0 -37
  52. package/src/codegen/rollback.ts +0 -26
@@ -46,6 +46,7 @@ export interface ParsedResource {
46
46
  description?: string;
47
47
  properties: ParsedProperty[];
48
48
  attributes: Array<{ name: string; tsType: string }>;
49
+ deprecatedProperties: string[];
49
50
  }
50
51
 
51
52
  export interface GitLabParseResult {
@@ -142,17 +143,9 @@ const PROPERTY_ENTITIES: Array<{
142
143
  { typeName: "GitLab::CI::Environment", source: "job_template:environment", description: "Deployment environment" },
143
144
  { typeName: "GitLab::CI::Trigger", source: "job_template:trigger", description: "Trigger downstream pipeline" },
144
145
  { typeName: "GitLab::CI::AutoCancel", source: "#/definitions/workflowAutoCancel", description: "Auto-cancel configuration" },
145
- ];
146
-
147
- /**
148
- * Enum types to extract.
149
- */
150
- const ENUM_ENTITIES: Array<{
151
- name: string;
152
- source: string;
153
- }> = [
154
- { name: "When", source: "#/definitions/when" },
155
- { name: "RetryError", source: "#/definitions/retry_errors" },
146
+ { typeName: "GitLab::CI::WorkflowRule", source: "root:workflow:rules:item", description: "Workflow rule with restricted when values" },
147
+ { typeName: "GitLab::CI::Need", source: "job_template:needs:item", description: "Job dependency specification" },
148
+ { typeName: "GitLab::CI::Inherit", source: "job_template:inherit", description: "Control default/variable inheritance" },
156
149
  ];
157
150
 
158
151
  // ── Parser ─────────────────────────────────────────────────────────
@@ -194,22 +187,35 @@ function extractResourceEntity(
194
187
  if (!def) return null;
195
188
 
196
189
  // Find the object variant if it's a oneOf/anyOf
197
- const objectDef = findObjectVariant(def);
190
+ const objectDef = findObjectVariant(def, schema);
198
191
  if (!objectDef?.properties) return null;
199
192
 
200
193
  const requiredSet = new Set<string>(objectDef.required ?? []);
201
194
  const properties = parseProperties(objectDef.properties, requiredSet, schema);
202
195
  const shortName = gitlabShortName(entity.typeName);
203
196
 
197
+ // Apply property type overrides for known entity references
198
+ const overrides = RESOURCE_PROPERTY_OVERRIDES[entity.typeName];
199
+ if (overrides) {
200
+ for (const prop of properties) {
201
+ if (overrides[prop.name]) {
202
+ prop.tsType = overrides[prop.name];
203
+ }
204
+ }
205
+ }
206
+
204
207
  // Extract nested property types from definition properties
205
208
  const { propertyTypes, enums } = extractNestedTypes(objectDef, shortName, schema);
206
209
 
210
+ const deprecatedProperties = mineDeprecatedProperties(properties);
211
+
207
212
  return {
208
213
  resource: {
209
214
  typeName: entity.typeName,
210
215
  description: entity.description ?? objectDef.description,
211
216
  properties,
212
217
  attributes: [], // CI entities have no read-only attributes
218
+ deprecatedProperties,
213
219
  },
214
220
  propertyTypes,
215
221
  enums,
@@ -227,7 +233,7 @@ function extractPropertyEntity(
227
233
  const def = resolveSource(schema, entity.source);
228
234
  if (!def) return null;
229
235
 
230
- const objectDef = findObjectVariant(def);
236
+ const objectDef = findObjectVariant(def, schema);
231
237
  if (!objectDef?.properties) {
232
238
  // Some entities like Parallel might be simple types
233
239
  // Create a minimal entry with no properties
@@ -237,6 +243,7 @@ function extractPropertyEntity(
237
243
  description: entity.description ?? def.description,
238
244
  properties: [],
239
245
  attributes: [],
246
+ deprecatedProperties: [],
240
247
  },
241
248
  propertyTypes: [],
242
249
  enums: [],
@@ -248,6 +255,7 @@ function extractPropertyEntity(
248
255
  const shortName = gitlabShortName(entity.typeName);
249
256
 
250
257
  const { propertyTypes, enums } = extractNestedTypes(objectDef, shortName, schema);
258
+ const deprecatedProperties = mineDeprecatedProperties(properties);
251
259
 
252
260
  return {
253
261
  resource: {
@@ -255,6 +263,7 @@ function extractPropertyEntity(
255
263
  description: entity.description ?? objectDef.description,
256
264
  properties,
257
265
  attributes: [],
266
+ deprecatedProperties,
258
267
  },
259
268
  propertyTypes,
260
269
  enums,
@@ -273,32 +282,51 @@ function resolveSource(schema: CISchema, source: string): CISchemaDefinition | n
273
282
  const defName = source.slice("#/definitions/".length).replace(":item", "");
274
283
  const arrayDef = schema.definitions?.[defName];
275
284
  if (!arrayDef?.items) return null;
276
- return findObjectVariant(arrayDef.items);
285
+ return findObjectVariant(arrayDef.items, schema);
277
286
  }
278
287
  const defName = source.slice("#/definitions/".length);
279
288
  return schema.definitions?.[defName] ?? null;
280
289
  }
281
290
 
291
+ // Multi-segment path under root: root:prop:subprop:...:item
282
292
  if (source.startsWith("root:")) {
283
- const propName = source.slice("root:".length);
284
- const prop = schema.properties?.[propName];
285
- if (!prop) return null;
286
- if (prop.$ref) {
287
- return resolveRef(prop.$ref, schema);
293
+ const segments = source.slice("root:".length).split(":");
294
+ let current: CISchemaDefinition | null = { properties: schema.properties } as CISchemaDefinition;
295
+
296
+ for (const seg of segments) {
297
+ if (!current) return null;
298
+ if (seg === "item") {
299
+ if (!current.items) return null;
300
+ return findObjectVariant(current.items, schema);
301
+ }
302
+ if (!current.properties) return null;
303
+ const prop = current.properties[seg];
304
+ if (!prop) return null;
305
+ current = prop.$ref ? resolveRef(prop.$ref, schema) : prop;
288
306
  }
289
- return prop;
307
+
308
+ return current;
290
309
  }
291
310
 
311
+ // Multi-segment path under job_template: job_template:prop:...:item
292
312
  if (source.startsWith("job_template:")) {
293
- const propName = source.slice("job_template:".length);
294
- const jobDef = schema.definitions?.job_template;
295
- if (!jobDef?.properties) return null;
296
- const prop = jobDef.properties[propName];
297
- if (!prop) return null;
298
- if (prop.$ref) {
299
- return resolveRef(prop.$ref, schema);
313
+ const segments = source.slice("job_template:".length).split(":");
314
+ let current: CISchemaDefinition | null = schema.definitions?.job_template ?? null;
315
+ if (!current) return null;
316
+
317
+ for (const seg of segments) {
318
+ if (!current) return null;
319
+ if (seg === "item") {
320
+ if (!current.items) return null;
321
+ return findObjectVariant(current.items, schema);
322
+ }
323
+ if (!current.properties) return null;
324
+ const prop = current.properties[seg];
325
+ if (!prop) return null;
326
+ current = prop.$ref ? resolveRef(prop.$ref, schema) : prop;
300
327
  }
301
- return prop;
328
+
329
+ return current;
302
330
  }
303
331
 
304
332
  return null;
@@ -317,35 +345,82 @@ function resolveRef(ref: string, schema: CISchema): CISchemaDefinition | null {
317
345
  /**
318
346
  * Find the object variant from a oneOf/anyOf union, or return the
319
347
  * definition itself if it already has properties.
348
+ *
349
+ * When multiple object variants exist, merges their properties:
350
+ * - Picks the variant with the most properties as the base
351
+ * - Adds unique properties from other variants
352
+ * - Union-merges overlapping property types (e.g. exit_codes: integer | integer[])
353
+ * - Required = intersection of all variants' required sets
320
354
  */
321
- function findObjectVariant(def: CISchemaDefinition): CISchemaDefinition | null {
355
+ function findObjectVariant(def: CISchemaDefinition, schema?: CISchema): CISchemaDefinition | null {
322
356
  if (def.properties) return def;
323
357
 
324
358
  const variants = def.oneOf ?? def.anyOf;
325
- if (variants) {
326
- // Prefer the variant with the most properties
327
- let best: CISchemaDefinition | null = null;
328
- let bestCount = 0;
329
- for (const v of variants) {
330
- // Resolve $ref in variant
331
- let resolved: CISchemaDefinition = v;
332
- if (v.$ref) {
333
- // We don't have schema here, just check properties
334
- continue;
335
- }
336
- if (resolved.properties) {
337
- const count = Object.keys(resolved.properties).length;
338
- if (count > bestCount) {
339
- best = resolved;
340
- bestCount = count;
359
+ if (!variants) return null;
360
+
361
+ // Collect all object variants, resolving $refs
362
+ const objectVariants: CISchemaDefinition[] = [];
363
+ for (const v of variants) {
364
+ let resolved: CISchemaDefinition = v;
365
+ if (v.$ref && schema) {
366
+ const r = resolveRef(v.$ref, schema);
367
+ if (r) resolved = r;
368
+ else continue;
369
+ } else if (v.$ref) {
370
+ continue;
371
+ }
372
+ if (resolved.properties) {
373
+ objectVariants.push(resolved);
374
+ }
375
+ }
376
+
377
+ if (objectVariants.length === 0) return null;
378
+ if (objectVariants.length === 1) return objectVariants[0];
379
+
380
+ // Find the variant with the most properties as the base
381
+ let best = objectVariants[0];
382
+ let bestCount = Object.keys(best.properties!).length;
383
+ for (let i = 1; i < objectVariants.length; i++) {
384
+ const count = Object.keys(objectVariants[i].properties!).length;
385
+ if (count > bestCount) {
386
+ best = objectVariants[i];
387
+ bestCount = count;
388
+ }
389
+ }
390
+
391
+ // Merge properties from other object variants into the base
392
+ const mergedProperties: Record<string, CISchemaProperty> = { ...best.properties };
393
+ const allRequiredSets = objectVariants.map((v) => new Set(v.required ?? []));
394
+
395
+ for (const variant of objectVariants) {
396
+ if (variant === best) continue;
397
+ for (const [propName, propDef] of Object.entries(variant.properties!)) {
398
+ if (propName in mergedProperties) {
399
+ // Property exists in base — if types differ, create a merged oneOf
400
+ const existing = mergedProperties[propName];
401
+ if (JSON.stringify(existing) !== JSON.stringify(propDef)) {
402
+ mergedProperties[propName] = {
403
+ oneOf: [existing, propDef],
404
+ description: existing.description ?? propDef.description,
405
+ };
341
406
  }
407
+ } else {
408
+ // New property from another variant
409
+ mergedProperties[propName] = propDef;
342
410
  }
343
411
  }
344
- return best;
345
412
  }
346
413
 
347
- // If it's a $ref, it would have been resolved before calling this
348
- return null;
414
+ // Required = intersection across all object variants
415
+ const requiredIntersection = [...allRequiredSets[0]].filter((r) =>
416
+ allRequiredSets.every((s) => s.has(r)),
417
+ );
418
+
419
+ return {
420
+ ...best,
421
+ properties: mergedProperties,
422
+ required: requiredIntersection.length > 0 ? requiredIntersection : undefined,
423
+ };
349
424
  }
350
425
 
351
426
  /**
@@ -442,6 +517,10 @@ function resolvePropertyType(prop: CISchemaProperty, schema: CISchema): string {
442
517
  case "array":
443
518
  if (prop.items) {
444
519
  const itemType = resolvePropertyType(prop.items, schema);
520
+ // Wrap union types in parens so [] applies to the whole union
521
+ if (itemType.includes(" | ")) {
522
+ return `(${itemType})[]`;
523
+ }
445
524
  return `${itemType}[]`;
446
525
  }
447
526
  return "any[]";
@@ -452,6 +531,24 @@ function resolvePropertyType(prop: CISchemaProperty, schema: CISchema): string {
452
531
  }
453
532
  }
454
533
 
534
+ /**
535
+ * Property type overrides for resource entities.
536
+ * Maps entity typeName → property name → desired TS type.
537
+ * Applied after parseProperties to replace generic types with entity references.
538
+ */
539
+ const RESOURCE_PROPERTY_OVERRIDES: Record<string, Record<string, string>> = {
540
+ "GitLab::CI::Job": {
541
+ environment: "Environment | string",
542
+ trigger: "Trigger | string",
543
+ release: "Release",
544
+ needs: "Need[]",
545
+ inherit: "Inherit",
546
+ },
547
+ "GitLab::CI::Workflow": {
548
+ rules: "WorkflowRule[]",
549
+ },
550
+ };
551
+
455
552
  /**
456
553
  * Map well-known definition names to their TypeScript entity types.
457
554
  */
@@ -476,7 +573,7 @@ function definitionToTsType(defName: string): string | null {
476
573
  rulesVariables: "Record<string, any>",
477
574
  when: '"on_success" | "on_failure" | "always" | "never" | "manual" | "delayed"',
478
575
  workflowAutoCancel: "AutoCancel",
479
- id_tokens: "Record<string, any>",
576
+ id_tokens: "Record<string, { aud: string | string[] }>",
480
577
  secrets: "Record<string, any>",
481
578
  timeout: "string",
482
579
  start_in: "string",
@@ -512,6 +609,21 @@ function extractNestedTypes(
512
609
  return { propertyTypes: [], enums: [] };
513
610
  }
514
611
 
612
+ const DEPRECATION_RE = /\bDeprecated\b|\bdeprecated\b|\blegacy\b|no longer (available|recommended|used|supported)|is not recommended/i;
613
+
614
+ /**
615
+ * Mine property descriptions for deprecation signals.
616
+ */
617
+ function mineDeprecatedProperties(properties: ParsedProperty[]): string[] {
618
+ const deprecated: string[] = [];
619
+ for (const prop of properties) {
620
+ if (prop.description && DEPRECATION_RE.test(prop.description)) {
621
+ deprecated.push(prop.name);
622
+ }
623
+ }
624
+ return deprecated;
625
+ }
626
+
515
627
  /**
516
628
  * Extract short name: "GitLab::CI::Job" → "Job"
517
629
  */
@@ -10,7 +10,7 @@ describe("generated lexicon-gitlab.json", () => {
10
10
  const registry = JSON.parse(content);
11
11
 
12
12
  test("is valid JSON with expected entries", () => {
13
- expect(Object.keys(registry)).toHaveLength(16);
13
+ expect(Object.keys(registry)).toHaveLength(19);
14
14
  });
15
15
 
16
16
  test("contains all resource entities", () => {
@@ -29,8 +29,9 @@ describe("generated lexicon-gitlab.json", () => {
29
29
  test("contains all property entities", () => {
30
30
  const propertyNames = [
31
31
  "AllowFailure", "Artifacts", "AutoCancel", "Cache",
32
- "Environment", "Image", "Include", "Parallel",
33
- "Release", "Retry", "Rule", "Service", "Trigger",
32
+ "Environment", "Image", "Include", "Inherit", "Parallel",
33
+ "Need", "Release", "Retry", "Rule", "Service", "Trigger",
34
+ "WorkflowRule",
34
35
  ];
35
36
  for (const name of propertyNames) {
36
37
  expect(registry[name]).toBeDefined();
@@ -54,8 +55,9 @@ describe("generated index.d.ts", () => {
54
55
  const expectedClasses = [
55
56
  "Job", "Default", "Workflow",
56
57
  "AllowFailure", "Artifacts", "AutoCancel", "Cache",
57
- "Environment", "Image", "Include", "Parallel",
58
- "Release", "Retry", "Rule", "Service", "Trigger",
58
+ "Environment", "Image", "Include", "Inherit", "Parallel",
59
+ "Need", "Release", "Retry", "Rule", "Service", "Trigger",
60
+ "WorkflowRule",
59
61
  ];
60
62
  for (const cls of expectedClasses) {
61
63
  expect(content).toContain(`export declare class ${cls}`);