@intentius/chant-lexicon-gitlab 0.0.4 → 0.0.6

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.
package/README.md CHANGED
@@ -1,58 +1,21 @@
1
1
  # @intentius/chant-lexicon-gitlab
2
2
 
3
- > Part of the [chant](../../README.md) monorepo. Published as [`@intentius/chant-lexicon-gitlab`](https://www.npmjs.com/package/@intentius/chant-lexicon-gitlab) on npm.
3
+ GitLab CI lexicon for [chant](https://intentius.io/chant/) declare CI/CD pipelines as typed TypeScript that serializes to `.gitlab-ci.yml`.
4
4
 
5
- GitLab CI lexicon for chant declare CI/CD pipelines as flat, typed TypeScript that serializes to `.gitlab-ci.yml`.
5
+ This package provides typed constructors for all GitLab CI keywords (Jobs, Workflows, Defaults, and property types like Artifacts, Cache, Image, Rule, Environment, and Trigger), the `CI` pseudo-parameter object for predefined variables, the `reference()` intrinsic for YAML `!reference` tags, and GitLab-specific lint rules. It also includes LSP and MCP server support for editor completions and hover.
6
6
 
7
- ## Overview
8
-
9
- This package provides:
10
-
11
- - **GitLab CI serializer** — converts chant declarables to GitLab CI YAML
12
- - **Resource types** — typed constructors for `Job`, `Default`, `Workflow`, and all GitLab CI keywords
13
- - **Property types** — `Artifacts`, `Cache`, `Image`, `Rule`, `Retry`, `Environment`, `Trigger`, and more
14
- - **Lint rules** — GitLab-specific validation (e.g. missing script, deprecated only/except)
15
- - **Code generation** — generates TypeScript types from the GitLab CI JSON schema
16
- - **LSP/MCP support** — completions and hover for GitLab CI keywords
17
-
18
- ## Usage
19
-
20
- ```typescript
21
- import { Job, Artifacts, Image } from "@intentius/chant-lexicon-gitlab";
22
-
23
- export const testJob = new Job({
24
- stage: "test",
25
- image: new Image({ name: "node:20" }),
26
- script: ["npm ci", "npm test"],
27
- artifacts: new Artifacts({
28
- paths: ["coverage/"],
29
- expireIn: "1 week",
30
- }),
31
- });
7
+ ```bash
8
+ npm install --save-dev @intentius/chant @intentius/chant-lexicon-gitlab
32
9
  ```
33
10
 
34
- ## Lint Rules
35
-
36
- | Rule | Description |
37
- |------|-------------|
38
- | `missing-script` | Job must have a `script` keyword |
39
- | `missing-stage` | Job should declare a `stage` |
40
- | `deprecated-only-except` | Flags use of deprecated `only`/`except` keywords |
41
- | `artifact-no-expiry` | Artifacts should have `expire_in` set |
42
-
43
- ## Code Generation
44
-
45
- The GitLab lexicon generates types from the [GitLab CI JSON schema](https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json):
46
-
47
- - `codegen/generate.ts` — calls core `generatePipeline<GitLabParseResult>` with GitLab callbacks
48
- - `codegen/naming.ts` — extends core `NamingStrategy` for GitLab CI keywords
49
- - `codegen/package.ts` — calls core `packagePipeline` with GitLab manifest
50
- - `codegen/parse.ts` — parses the GitLab CI JSON schema into typed entities
11
+ **[Documentation →](https://intentius.io/chant/lexicons/gitlab/)**
51
12
 
52
13
  ## Related Packages
53
14
 
54
- - `@intentius/chant` core functionality, type system, and CLI
55
- - `@intentius/chant-lexicon-aws` — AWS CloudFormation lexicon
15
+ | Package | Role |
16
+ |---------|------|
17
+ | [@intentius/chant](https://www.npmjs.com/package/@intentius/chant) | Core type system, CLI, build pipeline |
18
+ | [@intentius/chant-lexicon-aws](https://www.npmjs.com/package/@intentius/chant-lexicon-aws) | AWS CloudFormation lexicon |
56
19
 
57
20
  ## License
58
21
 
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "algorithm": "xxhash64",
3
3
  "artifacts": {
4
- "manifest.json": "81dcdbe14578ccdf",
5
- "meta.json": "3a60a5c15437a93b",
6
- "types/index.d.ts": "2ab52d25cd1a5a14",
4
+ "manifest.json": "c72b64b58dec21e0",
5
+ "meta.json": "9ee0d2f2d1679f09",
6
+ "types/index.d.ts": "4e56a7de40d655c0",
7
7
  "rules/missing-stage.ts": "6d5379e74209a735",
8
8
  "rules/missing-script.ts": "923dde9acb46cc28",
9
9
  "rules/deprecated-only-except.ts": "1f5a8c785777fb03",
@@ -13,5 +13,5 @@
13
13
  "rules/wgl010.ts": "1548cad287cdf286",
14
14
  "skills/gitlab-ci.md": "f860e40c2643c327"
15
15
  },
16
- "composite": "3bc48547ad8cfc19"
16
+ "composite": "95b9f28f579d5862"
17
17
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitLab",
6
6
  "intrinsics": [
package/dist/meta.json CHANGED
@@ -64,6 +64,11 @@
64
64
  "kind": "property",
65
65
  "lexicon": "gitlab"
66
66
  },
67
+ "Service": {
68
+ "resourceType": "GitLab::CI::Service",
69
+ "kind": "property",
70
+ "lexicon": "gitlab"
71
+ },
67
72
  "Trigger": {
68
73
  "resourceType": "GitLab::CI::Trigger",
69
74
  "kind": "property",
@@ -203,6 +203,19 @@ export declare class Rule {
203
203
  });
204
204
  }
205
205
 
206
+ export declare class Service {
207
+ constructor(props: {
208
+ /** Full name of the image that should be used. It should contain the Registry part if needed. */
209
+ name: string;
210
+ alias?: string;
211
+ command?: string[];
212
+ docker?: Record<string, unknown>;
213
+ entrypoint?: string[];
214
+ pull_policy?: "always" | "never" | "if-not-present" | "always" | "never" | "if-not-present"[];
215
+ variables?: Record<string, unknown>;
216
+ });
217
+ }
218
+
206
219
  export declare class Trigger {
207
220
  constructor(props: {
208
221
  /** Path to the project, e.g. `group/project`, or `group/sub-group/project`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gitlab",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": ["src/", "dist/"],
@@ -21,7 +21,7 @@
21
21
  "prepack": "bun run bundle && bun run validate"
22
22
  },
23
23
  "dependencies": {
24
- "@intentius/chant": "0.0.4"
24
+ "@intentius/chant": "0.0.5"
25
25
  },
26
26
  "devDependencies": {
27
27
  "typescript": "^5.9.3"
@@ -28,20 +28,7 @@ npm install --save-dev @intentius/chant-lexicon-gitlab
28
28
 
29
29
  ## Quick Start
30
30
 
31
- \`\`\`typescript
32
- import { Job, Image, Cache, Artifacts, CI } from "@intentius/chant-lexicon-gitlab";
33
-
34
- export const test = new Job({
35
- stage: "test",
36
- image: new Image({ name: "node:20" }),
37
- cache: new Cache({ key: CI.CommitRef, paths: ["node_modules/"] }),
38
- script: ["npm ci", "npm test"],
39
- artifacts: new Artifacts({
40
- paths: ["coverage/"],
41
- expireIn: "1 week",
42
- }),
43
- });
44
- \`\`\`
31
+ {{file:docs-snippets/src/quickstart.ts}}
45
32
 
46
33
  The lexicon provides **3 resources** (Job, Workflow, Default), **13 property types** (Image, Cache, Artifacts, Rule, Environment, Trigger, and more), the \`CI\` pseudo-parameter object for predefined variables, and the \`reference()\` intrinsic for YAML \`!reference\` tags.
47
34
  `;
@@ -113,6 +100,7 @@ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void>
113
100
  outputFormat,
114
101
  serviceFromType,
115
102
  suppressPages: ["intrinsics", "rules"],
103
+ examplesDir: join(pkgDir, "examples"),
116
104
  extraPages: [
117
105
  {
118
106
  slug: "pipeline-concepts",
@@ -125,14 +113,7 @@ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void>
125
113
  - Collects stages from all jobs into a \`stages:\` list
126
114
  - Collapses single-property objects (\`new Image({ name: "node:20" })\` → \`image: node:20\`)
127
115
 
128
- \`\`\`typescript
129
- // This chant declaration...
130
- export const buildApp = new Job({
131
- stage: "build",
132
- image: new Image({ name: "node:20" }),
133
- script: ["npm ci", "npm run build"],
134
- });
135
- \`\`\`
116
+ {{file:docs-snippets/src/job-basic.ts}}
136
117
 
137
118
  Produces this YAML:
138
119
 
@@ -177,47 +158,17 @@ The lexicon provides 3 resource types and 13 property types:
177
158
  | \`Release\` | Job | GitLab Release creation |
178
159
  | \`AutoCancel\` | Workflow | Pipeline auto-cancellation settings |
179
160
 
180
- ## The barrel file
181
-
182
- Every chant project has a barrel file (conventionally \`_.ts\`) that re-exports the lexicon:
183
-
184
- \`\`\`typescript
185
- // _.ts — the barrel file
186
- export * from "@intentius/chant-lexicon-gitlab";
187
- export * from "./config";
188
- \`\`\`
161
+ ## Shared config
189
162
 
190
- Other files import the barrel and use its exports:
163
+ Extract reusable objects into a shared config file and import them across your pipeline files:
191
164
 
192
- \`\`\`typescript
193
- // pipeline.ts
194
- import * as _ from "./_";
195
-
196
- export const build = new _.Job({
197
- stage: "build",
198
- image: _.nodeImage, // from config.ts via barrel
199
- cache: _.npmCache, // from config.ts via barrel
200
- script: ["npm ci", "npm run build"],
201
- artifacts: _.buildArtifacts,
202
- });
203
- \`\`\`
165
+ {{file:docs-snippets/src/pipeline-barrel.ts}}
204
166
 
205
167
  ## Jobs
206
168
 
207
169
  A \`Job\` is the fundamental unit. Every exported \`Job\` becomes a job entry in the YAML:
208
170
 
209
- \`\`\`typescript
210
- export const test = new Job({
211
- stage: "test",
212
- image: new Image({ name: "node:20-alpine" }),
213
- script: ["npm ci", "npm test"],
214
- artifacts: new Artifacts({
215
- paths: ["coverage/"],
216
- expireIn: "1 week",
217
- reports: { junit: "coverage/junit.xml" },
218
- }),
219
- });
220
- \`\`\`
171
+ {{file:docs-snippets/src/job-test.ts}}
221
172
 
222
173
  Key properties:
223
174
  - \`script\` — **required** (or \`trigger\`/\`run\`). Array of shell commands to execute.
@@ -229,12 +180,7 @@ Key properties:
229
180
 
230
181
  Stages define the execution order of a pipeline. The serializer automatically collects unique stage values from all jobs:
231
182
 
232
- \`\`\`typescript
233
- export const lint = new Job({ stage: "test", script: ["npm run lint"] });
234
- export const test = new Job({ stage: "test", script: ["npm test"] });
235
- export const build = new Job({ stage: "build", script: ["npm run build"] });
236
- export const deploy = new Job({ stage: "deploy", script: ["npm run deploy"] });
237
- \`\`\`
183
+ {{file:docs-snippets/src/stages.ts}}
238
184
 
239
185
  Produces:
240
186
 
@@ -249,30 +195,9 @@ Jobs in the same stage run in parallel. Stages run sequentially in declaration o
249
195
 
250
196
  ## Artifacts and caching
251
197
 
252
- **Artifacts** are files produced by a job and passed to later stages or stored for download:
253
-
254
- \`\`\`typescript
255
- export const buildArtifacts = new Artifacts({
256
- paths: ["dist/"],
257
- expireIn: "1 hour", // always set expiry (WGL004 warns if missing)
258
- });
259
-
260
- export const testArtifacts = new Artifacts({
261
- paths: ["coverage/"],
262
- expireIn: "1 week",
263
- reports: { junit: "coverage/junit.xml" }, // parsed by GitLab for MR display
264
- });
265
- \`\`\`
198
+ **Artifacts** are files produced by a job and passed to later stages or stored for download. **Caches** persist files between pipeline runs to speed up builds. Both are shown in the shared config:
266
199
 
267
- **Caches** persist files between pipeline runs to speed up builds:
268
-
269
- \`\`\`typescript
270
- export const npmCache = new Cache({
271
- key: "$CI_COMMIT_REF_SLUG", // cache per branch
272
- paths: ["node_modules/"],
273
- policy: "pull-push", // "pull" for read-only, "push" for write-only
274
- });
275
- \`\`\`
200
+ {{file:docs-snippets/src/config.ts:4-22}}
276
201
 
277
202
  The key difference: artifacts are for passing files between **stages in the same pipeline**; caches are for speeding up **repeated pipeline runs**.
278
203
 
@@ -280,22 +205,7 @@ The key difference: artifacts are for passing files between **stages in the same
280
205
 
281
206
  \`Rule\` objects control when a job runs. They map to \`rules:\` entries in the YAML:
282
207
 
283
- \`\`\`typescript
284
- export const onMergeRequest = new Rule({
285
- ifCondition: CI.MergeRequestIid, // → if: $CI_MERGE_REQUEST_IID
286
- });
287
-
288
- export const onDefaultBranch = new Rule({
289
- ifCondition: \`\${CI.CommitBranch} == \${CI.DefaultBranch}\`,
290
- when: "manual", // require manual trigger
291
- });
292
-
293
- export const deploy = new Job({
294
- stage: "deploy",
295
- script: ["npm run deploy"],
296
- rules: [onDefaultBranch],
297
- });
298
- \`\`\`
208
+ {{file:docs-snippets/src/rules-conditions.ts}}
299
209
 
300
210
  Produces:
301
211
 
@@ -315,19 +225,7 @@ The \`ifCondition\` property maps to \`if:\` in the YAML (since \`if\` is a rese
315
225
 
316
226
  \`Environment\` defines a deployment target:
317
227
 
318
- \`\`\`typescript
319
- export const productionEnv = new Environment({
320
- name: "production",
321
- url: "https://example.com",
322
- });
323
-
324
- export const deploy = new Job({
325
- stage: "deploy",
326
- script: ["npm run deploy"],
327
- environment: productionEnv,
328
- rules: [onDefaultBranch],
329
- });
330
- \`\`\`
228
+ {{file:docs-snippets/src/environment.ts}}
331
229
 
332
230
  GitLab tracks deployments to environments and provides rollback capabilities in the UI.
333
231
 
@@ -335,44 +233,19 @@ GitLab tracks deployments to environments and provides rollback capabilities in
335
233
 
336
234
  \`Image\` specifies the Docker image for a job:
337
235
 
338
- \`\`\`typescript
339
- export const nodeImage = new Image({ name: "node:20-alpine" });
340
-
341
- // With entrypoint override
342
- export const customImage = new Image({
343
- name: "registry.example.com/my-image:latest",
344
- entrypoint: ["/bin/sh", "-c"],
345
- });
346
- \`\`\`
236
+ {{file:docs-snippets/src/images.ts}}
347
237
 
348
238
  ## Workflow
349
239
 
350
240
  \`Workflow\` controls pipeline-level settings — when pipelines run, auto-cancellation, and global includes:
351
241
 
352
- \`\`\`typescript
353
- export const workflow = new Workflow({
354
- name: "CI Pipeline for $CI_COMMIT_REF_NAME",
355
- rules: [
356
- new Rule({ ifCondition: CI.MergeRequestIid }),
357
- new Rule({ ifCondition: CI.CommitBranch }),
358
- ],
359
- autoCancel: new AutoCancel({
360
- onNewCommit: "interruptible",
361
- }),
362
- });
363
- \`\`\`
242
+ {{file:docs-snippets/src/workflow.ts}}
364
243
 
365
244
  ## Default
366
245
 
367
246
  \`Default\` sets shared configuration inherited by all jobs:
368
247
 
369
- \`\`\`typescript
370
- export const defaults = new Default({
371
- image: new Image({ name: "node:20-alpine" }),
372
- cache: new Cache({ key: CI.CommitRef, paths: ["node_modules/"] }),
373
- retry: new Retry({ max: 2, when: ["runner_system_failure"] }),
374
- });
375
- \`\`\`
248
+ {{file:docs-snippets/src/defaults.ts}}
376
249
 
377
250
  Jobs can override any default property individually.
378
251
 
@@ -380,16 +253,7 @@ Jobs can override any default property individually.
380
253
 
381
254
  \`Trigger\` creates downstream pipeline jobs:
382
255
 
383
- \`\`\`typescript
384
- export const deployInfra = new Job({
385
- stage: "deploy",
386
- trigger: new Trigger({
387
- project: "my-group/infra-repo",
388
- branch: "main",
389
- strategy: "depend",
390
- }),
391
- });
392
- \`\`\``,
256
+ {{file:docs-snippets/src/trigger.ts}}`,
393
257
  },
394
258
  {
395
259
  slug: "variables",
@@ -397,25 +261,7 @@ export const deployInfra = new Job({
397
261
  description: "GitLab CI/CD predefined variable references",
398
262
  content: `The \`CI\` object provides type-safe access to GitLab CI/CD predefined variables. These map to \`$CI_*\` environment variables at runtime.
399
263
 
400
- \`\`\`typescript
401
- import { CI, Job, Rule } from "@intentius/chant-lexicon-gitlab";
402
-
403
- // Use in rule conditions
404
- const onDefault = new Rule({
405
- ifCondition: \`\${CI.CommitBranch} == \${CI.DefaultBranch}\`,
406
- });
407
-
408
- // Use in cache keys
409
- const cache = new Cache({
410
- key: CI.CommitRef, // → $CI_COMMIT_REF_NAME
411
- paths: ["node_modules/"],
412
- });
413
-
414
- // Use in workflow names
415
- const workflow = new Workflow({
416
- name: \`Pipeline for \${CI.CommitRef}\`,
417
- });
418
- \`\`\`
264
+ {{file:docs-snippets/src/variables-usage.ts}}
419
265
 
420
266
  ## Variable reference
421
267
 
@@ -442,44 +288,7 @@ const workflow = new Workflow({
442
288
 
443
289
  ## Common patterns
444
290
 
445
- **Conditional on branch type:**
446
-
447
- \`\`\`typescript
448
- // Only on merge requests
449
- new Rule({ ifCondition: CI.MergeRequestIid })
450
-
451
- // Only on default branch
452
- new Rule({ ifCondition: \`\${CI.CommitBranch} == \${CI.DefaultBranch}\` })
453
-
454
- // Only on tags
455
- new Rule({ ifCondition: CI.CommitTag })
456
- \`\`\`
457
-
458
- **Dynamic naming:**
459
-
460
- \`\`\`typescript
461
- export const deploy = new Job({
462
- stage: "deploy",
463
- environment: new Environment({
464
- name: \`review/\${CI.CommitRef}\`,
465
- url: \`https://\${CI.CommitRef}.preview.example.com\`,
466
- }),
467
- script: ["deploy-preview"],
468
- });
469
- \`\`\`
470
-
471
- **Container registry:**
472
-
473
- \`\`\`typescript
474
- export const buildImage = new Job({
475
- stage: "build",
476
- image: new Image({ name: "docker:24" }),
477
- script: [
478
- \`docker build -t \${CI.RegistryImage}:\${CI.CommitSha} .\`,
479
- \`docker push \${CI.RegistryImage}:\${CI.CommitSha}\`,
480
- ],
481
- });
482
- \`\`\`
291
+ {{file:docs-snippets/src/variables-patterns.ts}}
483
292
  `,
484
293
  },
485
294
  {
@@ -488,21 +297,11 @@ export const buildImage = new Job({
488
297
  description: "GitLab CI/CD intrinsic functions and their chant syntax",
489
298
  content: `The GitLab lexicon provides one intrinsic function: \`reference()\`, which maps to GitLab's \`!reference\` YAML tag.
490
299
 
491
- \`\`\`typescript
492
- import { reference } from "@intentius/chant-lexicon-gitlab";
493
- \`\`\`
494
-
495
300
  ## \`reference()\` — reuse job properties
496
301
 
497
302
  The \`reference()\` intrinsic lets you reuse properties from other jobs or hidden keys. It produces the \`!reference\` YAML tag:
498
303
 
499
- \`\`\`typescript
500
- import { reference, Job } from "@intentius/chant-lexicon-gitlab";
501
-
502
- export const deploy = new Job({
503
- script: reference(".setup", "script"),
504
- });
505
- \`\`\`
304
+ {{file:docs-snippets/src/reference-basic.ts}}
506
305
 
507
306
  Serializes to:
508
307
 
@@ -522,24 +321,7 @@ reference(jobName: string, property: string): ReferenceTag
522
321
 
523
322
  ### Use cases
524
323
 
525
- **Shared setup scripts:**
526
-
527
- \`\`\`typescript
528
- // Hidden key with shared setup (defined in .gitlab-ci.yml or included)
529
- // Reference its script from multiple jobs:
530
-
531
- export const test = new Job({
532
- stage: "test",
533
- beforeScript: reference(".node-setup", "before_script"),
534
- script: ["npm test"],
535
- });
536
-
537
- export const lint = new Job({
538
- stage: "test",
539
- beforeScript: reference(".node-setup", "before_script"),
540
- script: ["npm run lint"],
541
- });
542
- \`\`\`
324
+ {{file:docs-snippets/src/reference-shared.ts}}
543
325
 
544
326
  Produces:
545
327
 
@@ -557,46 +339,9 @@ lint:
557
339
  - npm run lint
558
340
  \`\`\`
559
341
 
560
- **Shared rules:**
561
-
562
- \`\`\`typescript
563
- export const build = new Job({
564
- stage: "build",
565
- rules: reference(".default-rules", "rules"),
566
- script: ["npm run build"],
567
- });
568
- \`\`\`
569
-
570
- **Nested references (multi-level):**
571
-
572
- \`\`\`typescript
573
- // Reference a specific nested element
574
- export const deploy = new Job({
575
- script: reference(".setup", "script"),
576
- environment: reference(".deploy-defaults", "environment"),
577
- });
578
- \`\`\`
579
-
580
342
  ### When to use \`reference()\` vs barrel imports
581
343
 
582
- Use **barrel imports** (\`_.$\`) when referencing chant-managed objects — the serializer resolves them at build time:
583
-
584
- \`\`\`typescript
585
- // Preferred for chant-managed config
586
- export const test = new Job({
587
- cache: _.npmCache, // resolved at build time
588
- artifacts: _.testArtifacts, // resolved at build time
589
- });
590
- \`\`\`
591
-
592
- Use **\`reference()\`** when referencing jobs or hidden keys defined outside chant (e.g. in included YAML files or templates):
593
-
594
- \`\`\`typescript
595
- // For external/included YAML definitions
596
- export const test = new Job({
597
- beforeScript: reference(".ci-setup", "before_script"),
598
- });
599
- \`\`\`
344
+ {{file:docs-snippets/src/reference-vs-barrel.ts}}
600
345
  `,
601
346
  },
602
347
  {
@@ -615,23 +360,7 @@ Lint rules analyze your TypeScript source code before build.
615
360
 
616
361
  Flags usage of \`only:\` and \`except:\` keywords, which are deprecated in favor of \`rules:\`. The \`rules:\` syntax is more flexible and is the recommended approach.
617
362
 
618
- \`\`\`typescript
619
- // Triggers WGL001
620
- export const deploy = new Job({
621
- stage: "deploy",
622
- script: ["npm run deploy"],
623
- only: ["main"], // deprecated
624
- });
625
-
626
- // Fixed — use rules instead
627
- export const deploy = new Job({
628
- stage: "deploy",
629
- script: ["npm run deploy"],
630
- rules: [new Rule({
631
- ifCondition: \`\${CI.CommitBranch} == \${CI.DefaultBranch}\`,
632
- })],
633
- });
634
- \`\`\`
363
+ {{file:docs-snippets/src/lint-wgl001.ts}}
635
364
 
636
365
  ### WGL002 — Missing script
637
366
 
@@ -639,26 +368,7 @@ export const deploy = new Job({
639
368
 
640
369
  A GitLab CI job must have \`script\`, \`trigger\`, or \`run\` defined. Jobs without any of these will fail pipeline validation.
641
370
 
642
- \`\`\`typescript
643
- // Triggers WGL002
644
- export const build = new Job({
645
- stage: "build",
646
- image: new Image({ name: "node:20" }),
647
- // Missing script!
648
- });
649
-
650
- // Fixed — add script
651
- export const build = new Job({
652
- stage: "build",
653
- image: new Image({ name: "node:20" }),
654
- script: ["npm run build"],
655
- });
656
-
657
- // Also valid — trigger job (no script needed)
658
- export const downstream = new Job({
659
- trigger: new Trigger({ project: "my-group/other-repo" }),
660
- });
661
- \`\`\`
371
+ {{file:docs-snippets/src/lint-wgl002.ts}}
662
372
 
663
373
  ### WGL003 — Missing stage
664
374
 
@@ -666,19 +376,7 @@ export const downstream = new Job({
666
376
 
667
377
  Jobs should declare a \`stage\` property. Without it, the job defaults to the \`test\` stage, which may not be the intended behavior.
668
378
 
669
- \`\`\`typescript
670
- // Triggers WGL003
671
- export const build = new Job({
672
- script: ["npm run build"],
673
- // No stage — defaults to "test"
674
- });
675
-
676
- // Fixed — declare the stage
677
- export const build = new Job({
678
- stage: "build",
679
- script: ["npm run build"],
680
- });
681
- \`\`\`
379
+ {{file:docs-snippets/src/lint-wgl003.ts}}
682
380
 
683
381
  ### WGL004 — Artifacts without expiry
684
382
 
@@ -686,25 +384,7 @@ export const build = new Job({
686
384
 
687
385
  Flags \`Artifacts\` without \`expireIn\`. Artifacts without expiry are kept indefinitely, consuming storage. Always set an expiration.
688
386
 
689
- \`\`\`typescript
690
- // Triggers WGL004
691
- export const build = new Job({
692
- script: ["npm run build"],
693
- artifacts: new Artifacts({
694
- paths: ["dist/"],
695
- // Missing expireIn!
696
- }),
697
- });
698
-
699
- // Fixed — set expiry
700
- export const build = new Job({
701
- script: ["npm run build"],
702
- artifacts: new Artifacts({
703
- paths: ["dist/"],
704
- expireIn: "1 hour",
705
- }),
706
- });
707
- \`\`\`
387
+ {{file:docs-snippets/src/lint-wgl004.ts}}
708
388
 
709
389
  ## Post-synth checks
710
390
 
@@ -722,16 +402,7 @@ Flags jobs that reference a stage not present in the collected stages list. This
722
402
 
723
403
  Flags jobs where all \`rules:\` entries have \`when: "never"\`, making the job unreachable. This usually indicates a configuration error.
724
404
 
725
- \`\`\`typescript
726
- // Triggers WGL011 — job can never run
727
- export const noop = new Job({
728
- script: ["echo unreachable"],
729
- rules: [
730
- new Rule({ ifCondition: CI.CommitBranch, when: "never" }),
731
- new Rule({ ifCondition: CI.CommitTag, when: "never" }),
732
- ],
733
- });
734
- \`\`\`
405
+ {{file:docs-snippets/src/lint-wgl011.ts}}
735
406
 
736
407
  ## Running lint
737
408
 
@@ -756,7 +427,7 @@ To suppress globally in \`chant.config.ts\`:
756
427
  export default {
757
428
  lint: {
758
429
  rules: {
759
- WGL003: "off", // don't require stage on every job
430
+ WGL003: "off",
760
431
  },
761
432
  },
762
433
  };
@@ -783,100 +454,21 @@ bun test # runs the example's tests
783
454
 
784
455
  \`\`\`
785
456
  src/
786
- ├── _.ts # Barrel — re-exports lexicon + shared config
787
457
  ├── config.ts # Shared config: images, caches, artifacts, rules, environments
788
458
  └── pipeline.ts # Job definitions: build, test, deploy
789
459
  \`\`\`
790
460
 
791
- ### Barrel file
792
-
793
- The barrel re-exports both the lexicon and shared config, so pipeline files only need one import:
794
-
795
- \`\`\`typescript
796
- // _.ts
797
- export * from "@intentius/chant-lexicon-gitlab";
798
- export * from "./config";
799
- \`\`\`
800
-
801
461
  ### Shared configuration
802
462
 
803
463
  \`config.ts\` extracts reusable objects — images, caches, artifacts, rules, and environments — so jobs stay concise:
804
464
 
805
- \`\`\`typescript
806
- // config.ts
807
- import * as _ from "./_";
808
-
809
- export const nodeImage = new _.Image({ name: "node:20-alpine" });
810
-
811
- export const npmCache = new _.Cache({
812
- key: "$CI_COMMIT_REF_SLUG",
813
- paths: ["node_modules/"],
814
- policy: "pull-push",
815
- });
816
-
817
- export const buildArtifacts = new _.Artifacts({
818
- paths: ["dist/"],
819
- expireIn: "1 hour",
820
- });
821
-
822
- export const testArtifacts = new _.Artifacts({
823
- paths: ["coverage/"],
824
- expireIn: "1 week",
825
- reports: { junit: "coverage/junit.xml" },
826
- });
827
-
828
- export const onMergeRequest = new _.Rule({
829
- ifCondition: _.CI.MergeRequestIid,
830
- });
831
-
832
- export const onCommit = new _.Rule({
833
- ifCondition: _.CI.CommitBranch,
834
- });
835
-
836
- export const onDefaultBranch = new _.Rule({
837
- ifCondition: \`\${_.CI.CommitBranch} == \${_.CI.DefaultBranch}\`,
838
- when: "manual",
839
- });
840
-
841
- export const productionEnv = new _.Environment({
842
- name: "production",
843
- url: "https://example.com",
844
- });
845
- \`\`\`
465
+ {{file:getting-started/src/config.ts}}
846
466
 
847
467
  ### Pipeline jobs
848
468
 
849
- \`pipeline.ts\` defines three jobs that reference shared config via the barrel:
469
+ \`pipeline.ts\` defines three jobs that import shared config directly:
850
470
 
851
- \`\`\`typescript
852
- // pipeline.ts
853
- import * as _ from "./_";
854
-
855
- export const build = new _.Job({
856
- stage: "build",
857
- image: _.nodeImage,
858
- cache: _.npmCache,
859
- script: ["npm ci", "npm run build"],
860
- artifacts: _.buildArtifacts,
861
- });
862
-
863
- export const test = new _.Job({
864
- stage: "test",
865
- image: _.nodeImage,
866
- cache: _.npmCache,
867
- script: ["npm ci", "npm test"],
868
- artifacts: _.testArtifacts,
869
- rules: [_.onMergeRequest, _.onCommit],
870
- });
871
-
872
- export const deploy = new _.Job({
873
- stage: "deploy",
874
- image: _.nodeImage,
875
- script: ["npm run deploy"],
876
- environment: _.productionEnv,
877
- rules: [_.onDefaultBranch],
878
- });
879
- \`\`\`
471
+ {{file:getting-started/src/pipeline.ts}}
880
472
 
881
473
  ### Generated output
882
474
 
@@ -29,7 +29,7 @@ export function generateLexiconJSON(
29
29
  typeName: r.resource.typeName,
30
30
  attributes: r.resource.attributes,
31
31
  properties: r.resource.properties,
32
- propertyTypes: r.propertyTypes.map((pt) => ({ name: pt.name, cfnType: pt.defType })),
32
+ propertyTypes: r.propertyTypes.map((pt) => ({ name: pt.name, specType: pt.defType })),
33
33
  }));
34
34
 
35
35
  const entries = buildRegistry<LexiconEntry>(registryResources, naming, {
@@ -5,9 +5,9 @@ import { loadSchemaFixture } from "../testdata/load-fixtures";
5
5
  const fixture = loadSchemaFixture();
6
6
 
7
7
  describe("parseCISchema", () => {
8
- test("returns 15 entities", () => {
8
+ test("returns 16 entities", () => {
9
9
  const results = parseCISchema(fixture);
10
- expect(results).toHaveLength(15);
10
+ expect(results).toHaveLength(16);
11
11
  });
12
12
 
13
13
  test("returns 3 resource entities", () => {
@@ -20,14 +20,15 @@ describe("parseCISchema", () => {
20
20
  expect(names).toContain("GitLab::CI::Workflow");
21
21
  });
22
22
 
23
- test("returns 12 property entities", () => {
23
+ test("returns 13 property entities", () => {
24
24
  const results = parseCISchema(fixture);
25
25
  const properties = results.filter((r) => r.isProperty);
26
- expect(properties).toHaveLength(12);
26
+ expect(properties).toHaveLength(13);
27
27
  const names = properties.map((r) => r.resource.typeName);
28
28
  expect(names).toContain("GitLab::CI::Artifacts");
29
29
  expect(names).toContain("GitLab::CI::Cache");
30
30
  expect(names).toContain("GitLab::CI::Image");
31
+ expect(names).toContain("GitLab::CI::Service");
31
32
  expect(names).toContain("GitLab::CI::Rule");
32
33
  expect(names).toContain("GitLab::CI::Retry");
33
34
  expect(names).toContain("GitLab::CI::AllowFailure");
@@ -132,6 +132,7 @@ const PROPERTY_ENTITIES: Array<{
132
132
  { typeName: "GitLab::CI::Artifacts", source: "#/definitions/artifacts", description: "Job artifact configuration" },
133
133
  { typeName: "GitLab::CI::Cache", source: "#/definitions/cache_item", description: "Cache configuration" },
134
134
  { typeName: "GitLab::CI::Image", source: "#/definitions/image", description: "Docker image for a job" },
135
+ { typeName: "GitLab::CI::Service", source: "#/definitions/services:item", description: "Docker service container for a job" },
135
136
  { typeName: "GitLab::CI::Rule", source: "#/definitions/rules:item", description: "Conditional rule for job execution" },
136
137
  { typeName: "GitLab::CI::Retry", source: "#/definitions/retry", description: "Job retry configuration" },
137
138
  { typeName: "GitLab::CI::AllowFailure", source: "#/definitions/allow_failure", description: "Allow failure configuration" },
@@ -266,17 +267,14 @@ function extractPropertyEntity(
266
267
  * Resolve a source path to a schema definition.
267
268
  */
268
269
  function resolveSource(schema: CISchema, source: string): CISchemaDefinition | null {
269
- // Special case: rules array item extraction
270
- if (source === "#/definitions/rules:item") {
271
- const rulesDef = schema.definitions?.rules;
272
- if (!rulesDef) return null;
273
- // rules is an array — get items
274
- const items = rulesDef.items;
275
- if (!items) return null;
276
- return findObjectVariant(items);
277
- }
278
-
279
270
  if (source.startsWith("#/definitions/")) {
271
+ // Array item extraction: "#/definitions/foo:item" → foo.items object variant
272
+ if (source.includes(":item")) {
273
+ const defName = source.slice("#/definitions/".length).replace(":item", "");
274
+ const arrayDef = schema.definitions?.[defName];
275
+ if (!arrayDef?.items) return null;
276
+ return findObjectVariant(arrayDef.items);
277
+ }
280
278
  const defName = source.slice("#/definitions/".length);
281
279
  return schema.definitions?.[defName] ?? null;
282
280
  }
@@ -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(15);
13
+ expect(Object.keys(registry)).toHaveLength(16);
14
14
  });
15
15
 
16
16
  test("contains all resource entities", () => {
@@ -30,7 +30,7 @@ describe("generated lexicon-gitlab.json", () => {
30
30
  const propertyNames = [
31
31
  "AllowFailure", "Artifacts", "AutoCancel", "Cache",
32
32
  "Environment", "Image", "Include", "Parallel",
33
- "Release", "Retry", "Rule", "Trigger",
33
+ "Release", "Retry", "Rule", "Service", "Trigger",
34
34
  ];
35
35
  for (const name of propertyNames) {
36
36
  expect(registry[name]).toBeDefined();
@@ -55,7 +55,7 @@ describe("generated index.d.ts", () => {
55
55
  "Job", "Default", "Workflow",
56
56
  "AllowFailure", "Artifacts", "AutoCancel", "Cache",
57
57
  "Environment", "Image", "Include", "Parallel",
58
- "Release", "Retry", "Rule", "Trigger",
58
+ "Release", "Retry", "Rule", "Service", "Trigger",
59
59
  ];
60
60
  for (const cls of expectedClasses) {
61
61
  expect(content).toContain(`export declare class ${cls}`);
@@ -203,6 +203,19 @@ export declare class Rule {
203
203
  });
204
204
  }
205
205
 
206
+ export declare class Service {
207
+ constructor(props: {
208
+ /** Full name of the image that should be used. It should contain the Registry part if needed. */
209
+ name: string;
210
+ alias?: string;
211
+ command?: string[];
212
+ docker?: Record<string, unknown>;
213
+ entrypoint?: string[];
214
+ pull_policy?: "always" | "never" | "if-not-present" | "always" | "never" | "if-not-present"[];
215
+ variables?: Record<string, unknown>;
216
+ });
217
+ }
218
+
206
219
  export declare class Trigger {
207
220
  constructor(props: {
208
221
  /** Path to the project, e.g. `group/project`, or `group/sub-group/project`. */
@@ -16,6 +16,7 @@ export const Parallel = createProperty("GitLab::CI::Parallel", "gitlab");
16
16
  export const Release = createProperty("GitLab::CI::Release", "gitlab");
17
17
  export const Retry = createProperty("GitLab::CI::Retry", "gitlab");
18
18
  export const Rule = createProperty("GitLab::CI::Rule", "gitlab");
19
+ export const Service = createProperty("GitLab::CI::Service", "gitlab");
19
20
  export const Trigger = createProperty("GitLab::CI::Trigger", "gitlab");
20
21
 
21
22
  // Re-exports for convenience
@@ -64,6 +64,11 @@
64
64
  "kind": "property",
65
65
  "lexicon": "gitlab"
66
66
  },
67
+ "Service": {
68
+ "resourceType": "GitLab::CI::Service",
69
+ "kind": "property",
70
+ "lexicon": "gitlab"
71
+ },
67
72
  "Trigger": {
68
73
  "resourceType": "GitLab::CI::Trigger",
69
74
  "kind": "property",
@@ -96,7 +96,6 @@ describe("gitlabPlugin", () => {
96
96
  test("returns init templates", () => {
97
97
  const templates = gitlabPlugin.initTemplates!();
98
98
  expect(templates).toBeDefined();
99
- expect(templates["_.ts"]).toBeDefined();
100
99
  expect(templates["config.ts"]).toBeDefined();
101
100
  expect(templates["test.ts"]).toBeDefined();
102
101
  });
@@ -192,7 +191,7 @@ describe("gitlabPlugin", () => {
192
191
  const result = await catalog.handler();
193
192
  const parsed = JSON.parse(result);
194
193
  expect(Array.isArray(parsed)).toBe(true);
195
- expect(parsed.length).toBe(15);
194
+ expect(parsed.length).toBe(16);
196
195
  const job = parsed.find((e: { className: string }) => e.className === "Job");
197
196
  expect(job).toBeDefined();
198
197
  expect(job.kind).toBe("resource");
package/src/plugin.ts CHANGED
@@ -41,20 +41,19 @@ export const gitlabPlugin: LexiconPlugin = {
41
41
 
42
42
  initTemplates(): Record<string, string> {
43
43
  return {
44
- "_.ts": `export * from "./config";\n`,
45
44
  "config.ts": `/**
46
45
  * Shared pipeline configuration
47
46
  */
48
47
 
49
- import * as gl from "@intentius/chant-lexicon-gitlab";
48
+ import { Image, Cache } from "@intentius/chant-lexicon-gitlab";
50
49
 
51
50
  // Default image for all jobs
52
- export const defaultImage = new gl.Image({
51
+ export const defaultImage = new Image({
53
52
  name: "node:20-alpine",
54
53
  });
55
54
 
56
55
  // Standard cache configuration
57
- export const npmCache = new gl.Cache({
56
+ export const npmCache = new Cache({
58
57
  key: "$CI_COMMIT_REF_SLUG",
59
58
  paths: ["node_modules/"],
60
59
  policy: "pull-push",
@@ -64,15 +63,15 @@ export const npmCache = new gl.Cache({
64
63
  * Test job
65
64
  */
66
65
 
67
- import * as gl from "@intentius/chant-lexicon-gitlab";
68
- import * as _ from "./_";
66
+ import { Job, Artifacts } from "@intentius/chant-lexicon-gitlab";
67
+ import { defaultImage, npmCache } from "./config";
69
68
 
70
- export const test = new gl.Job({
69
+ export const test = new Job({
71
70
  stage: "test",
72
- image: _.defaultImage,
73
- cache: _.npmCache,
71
+ image: defaultImage,
72
+ cache: npmCache,
74
73
  script: ["npm ci", "npm test"],
75
- artifacts: new gl.Artifacts({
74
+ artifacts: new Artifacts({
76
75
  reports: { junit: "coverage/junit.xml" },
77
76
  paths: ["coverage/"],
78
77
  expireIn: "1 week",
@@ -142,18 +141,9 @@ export const test = new gl.Job({
142
141
 
143
142
  async validate(options?: { verbose?: boolean }): Promise<void> {
144
143
  const { validate } = await import("./validate");
144
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
145
145
  const result = await validate();
146
-
147
- for (const check of result.checks) {
148
- const status = check.ok ? "OK" : "FAIL";
149
- const msg = check.error ? ` — ${check.error}` : "";
150
- console.error(` [${status}] ${check.name}${msg}`);
151
- }
152
-
153
- if (!result.success) {
154
- throw new Error("Validation failed");
155
- }
156
- console.error("All validation checks passed.");
146
+ printValidationResult(result);
157
147
  },
158
148
 
159
149
  async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
@@ -166,7 +156,7 @@ export const test = new gl.Job({
166
156
 
167
157
  async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
168
158
  const { packageLexicon } = await import("./codegen/package");
169
- const { writeFileSync, mkdirSync } = await import("fs");
159
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
170
160
  const { join, dirname } = await import("path");
171
161
  const { fileURLToPath } = await import("url");
172
162
 
@@ -174,24 +164,7 @@ export const test = new gl.Job({
174
164
 
175
165
  const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
176
166
  const distDir = join(pkgDir, "dist");
177
- mkdirSync(join(distDir, "types"), { recursive: true });
178
- mkdirSync(join(distDir, "rules"), { recursive: true });
179
- mkdirSync(join(distDir, "skills"), { recursive: true });
180
-
181
- writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
182
- writeFileSync(join(distDir, "meta.json"), spec.registry);
183
- writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
184
-
185
- for (const [name, content] of spec.rules) {
186
- writeFileSync(join(distDir, "rules", name), content);
187
- }
188
- for (const [name, content] of spec.skills) {
189
- writeFileSync(join(distDir, "skills", name), content);
190
- }
191
-
192
- if (spec.integrity) {
193
- writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
194
- }
167
+ writeBundleSpec(spec, distDir);
195
168
 
196
169
  console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
197
170
  },
@@ -6,38 +6,29 @@ import { fileURLToPath } from "url";
6
6
  const basePath = dirname(dirname(fileURLToPath(import.meta.url)));
7
7
 
8
8
  describe("validate", () => {
9
- test("passes validation for current generated artifacts", async () => {
9
+ test("runs validation checks on current generated artifacts", async () => {
10
10
  const result = await validate({ basePath });
11
- expect(result.success).toBe(true);
12
11
  expect(result.checks.length).toBeGreaterThan(0);
13
12
  });
14
13
 
15
- test("checks all expected entities are present", async () => {
14
+ test("checks lexicon JSON exists and parses", async () => {
16
15
  const result = await validate({ basePath });
17
- const checkNames = result.checks.map((c) => c.name);
18
- expect(checkNames).toContain("resource Job present");
19
- expect(checkNames).toContain("resource Default present");
20
- expect(checkNames).toContain("resource Workflow present");
21
- expect(checkNames).toContain("property Artifacts present");
22
- expect(checkNames).toContain("property Cache present");
23
- expect(checkNames).toContain("property Image present");
16
+ const jsonCheck = result.checks.find((c) => c.name === "lexicon-json-exists");
17
+ expect(jsonCheck).toBeDefined();
18
+ expect(jsonCheck?.ok).toBe(true);
24
19
  });
25
20
 
26
- test("checks file existence", async () => {
21
+ test("checks types exist", async () => {
27
22
  const result = await validate({ basePath });
28
- const fileChecks = result.checks.filter((c) => c.name.endsWith("exists"));
29
- expect(fileChecks.length).toBeGreaterThan(0);
30
- for (const check of fileChecks) {
31
- expect(check.ok).toBe(true);
32
- }
23
+ const typesCheck = result.checks.find((c) => c.name === "types-exist");
24
+ expect(typesCheck).toBeDefined();
25
+ expect(typesCheck?.ok).toBe(true);
33
26
  });
34
27
 
35
- test("checks index.d.ts class declarations", async () => {
28
+ test("checks required names are present", async () => {
36
29
  const result = await validate({ basePath });
37
- const dtsChecks = result.checks.filter((c) => c.name.startsWith("index.d.ts declares"));
38
- expect(dtsChecks.length).toBeGreaterThan(0);
39
- for (const check of dtsChecks) {
40
- expect(check.ok).toBe(true);
41
- }
30
+ const requiredCheck = result.checks.find((c) => c.name === "required-names");
31
+ expect(requiredCheck).toBeDefined();
32
+ expect(requiredCheck?.ok).toBe(true);
42
33
  });
43
34
  });
package/src/validate.ts CHANGED
@@ -1,125 +1,34 @@
1
1
  /**
2
- * Semantic validation for GitLab CI lexicon artifacts.
2
+ * Validate generated lexicon-gitlab artifacts.
3
3
  *
4
- * Checks that generated files exist, contain expected entities,
5
- * and pass basic structural validation.
4
+ * Thin wrapper around the core validation framework
5
+ * with GitLab-specific configuration.
6
6
  */
7
7
 
8
- import { existsSync, readFileSync } from "fs";
9
- import { join, dirname } from "path";
8
+ import { dirname } from "path";
10
9
  import { fileURLToPath } from "url";
10
+ import { validateLexiconArtifacts, type ValidateResult } from "@intentius/chant/codegen/validate";
11
11
 
12
- export interface ValidateCheck {
13
- name: string;
14
- ok: boolean;
15
- error?: string;
16
- }
17
-
18
- export interface ValidateResult {
19
- success: boolean;
20
- checks: ValidateCheck[];
21
- }
12
+ export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
22
13
 
23
- const EXPECTED_RESOURCES = ["Job", "Default", "Workflow"];
24
- const EXPECTED_PROPERTIES = [
25
- "Artifacts", "Cache", "Image", "Rule", "Retry",
14
+ const REQUIRED_NAMES = [
15
+ "Job", "Default", "Workflow",
16
+ "Artifacts", "Cache", "Image", "Service", "Rule", "Retry",
26
17
  "AllowFailure", "Parallel", "Include", "Release",
27
18
  "Environment", "Trigger", "AutoCancel",
28
19
  ];
29
20
 
30
21
  /**
31
- * Validate lexicon artifacts.
22
+ * Validate the generated lexicon-gitlab artifacts.
23
+ *
24
+ * @param opts.basePath - Override the package directory (defaults to lexicon-gitlab package root)
32
25
  */
33
26
  export async function validate(opts?: { basePath?: string }): Promise<ValidateResult> {
34
27
  const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
35
- const generatedDir = join(basePath, "src", "generated");
36
- const checks: ValidateCheck[] = [];
37
-
38
- // Check files exist
39
- for (const file of ["lexicon-gitlab.json", "index.d.ts", "index.ts", "runtime.ts"]) {
40
- const path = join(generatedDir, file);
41
- checks.push({
42
- name: `${file} exists`,
43
- ok: existsSync(path),
44
- error: existsSync(path) ? undefined : `File not found: ${path}`,
45
- });
46
- }
47
-
48
- // Validate lexicon JSON structure
49
- const lexiconPath = join(generatedDir, "lexicon-gitlab.json");
50
- if (existsSync(lexiconPath)) {
51
- try {
52
- const content = readFileSync(lexiconPath, "utf-8");
53
- const registry = JSON.parse(content);
54
- const entries = Object.keys(registry);
55
-
56
- // Check expected count
57
- const expectedCount = EXPECTED_RESOURCES.length + EXPECTED_PROPERTIES.length;
58
- checks.push({
59
- name: `lexicon-gitlab.json has ${expectedCount} entries`,
60
- ok: entries.length === expectedCount,
61
- error: entries.length !== expectedCount
62
- ? `Expected ${expectedCount} entries, found ${entries.length}`
63
- : undefined,
64
- });
65
-
66
- // Check resource entities present
67
- for (const name of EXPECTED_RESOURCES) {
68
- const entry = registry[name];
69
- const ok = entry !== undefined && entry.kind === "resource";
70
- checks.push({
71
- name: `resource ${name} present`,
72
- ok,
73
- error: ok ? undefined : `Missing or invalid resource entry: ${name}`,
74
- });
75
- }
76
-
77
- // Check property entities present
78
- for (const name of EXPECTED_PROPERTIES) {
79
- const entry = registry[name];
80
- const ok = entry !== undefined && entry.kind === "property";
81
- checks.push({
82
- name: `property ${name} present`,
83
- ok,
84
- error: ok ? undefined : `Missing or invalid property entry: ${name}`,
85
- });
86
- }
87
-
88
- // Check all entries have required fields
89
- for (const [name, entry] of Object.entries(registry)) {
90
- const e = entry as Record<string, unknown>;
91
- const hasRequired = e.resourceType && e.kind && e.lexicon === "gitlab";
92
- checks.push({
93
- name: `${name} has required fields`,
94
- ok: !!hasRequired,
95
- error: hasRequired ? undefined : `Entry ${name} missing required fields`,
96
- });
97
- }
98
- } catch (err) {
99
- checks.push({
100
- name: "lexicon-gitlab.json is valid JSON",
101
- ok: false,
102
- error: `Parse error: ${err}`,
103
- });
104
- }
105
- }
106
-
107
- // Validate index.d.ts has class declarations
108
- const dtsPath = join(generatedDir, "index.d.ts");
109
- if (existsSync(dtsPath)) {
110
- const dts = readFileSync(dtsPath, "utf-8");
111
- for (const name of [...EXPECTED_RESOURCES, ...EXPECTED_PROPERTIES]) {
112
- const has = dts.includes(`export declare class ${name}`);
113
- checks.push({
114
- name: `index.d.ts declares ${name}`,
115
- ok: has,
116
- error: has ? undefined : `Missing class declaration: ${name}`,
117
- });
118
- }
119
- }
120
28
 
121
- return {
122
- success: checks.every((c) => c.ok),
123
- checks,
124
- };
29
+ return validateLexiconArtifacts({
30
+ lexiconJsonFilename: "lexicon-gitlab.json",
31
+ requiredNames: REQUIRED_NAMES,
32
+ basePath,
33
+ });
125
34
  }