@intentius/chant-lexicon-gitlab 0.46.0 → 0.49.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.
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "c3703cd4ebf4f650408c0bd187c1a242041dfdb0461b52d67db198d2cf486c67",
4
+ "manifest.json": "6e5bfe31912b7af3fa1bd5241c2a22ae87b5e95afbe871d0736fd8d96cd3cd69",
5
5
  "meta.json": "931fc3246a55645b1493080bbeb160e5d205e42349e8bdca96ca243adb5f0da3",
6
6
  "types/index.d.ts": "5cd2e99f135a929b72bdd822d00d780d39b1cd407ba0cfb3d511c7ac9d667b58",
7
7
  "rules/artifact-no-expiry.ts": "3f3cabf9792cbf8207e53a25f506715466b19ec25e9c3b4d0d77fed6b2eb4542",
8
8
  "rules/deprecated-only-except.ts": "2341b95c0aaf3013c375fb404969858e23451e3430f3d1a63b128fcb21b90156",
9
- "rules/missing-script.ts": "1dba062d77fa248fc02857fd08bc7789f90eefd3206e552ac63782a04b843888",
10
- "rules/missing-stage.ts": "ce810703cba6c8942282c4ebaeff23fe3312c4ecc362acce05baf85d0fd32ea3",
9
+ "rules/import-source.ts": "b5b2e79ef4c18a21711a380dce60e8bbab840223eda568b18039879d3a0a549f",
10
+ "rules/missing-script.ts": "c9efd57e2d5cc0a022c6e7858f7ffa98d2fbb055e6ea915d2d54cf2b2d130e47",
11
+ "rules/missing-stage.ts": "b0f4b7e0b70e496ddc9d239d78a26410da4c18fcf9c518813523bb2459ffa5f7",
11
12
  "rules/wgl010.ts": "e9a14d5f9e4c49b1524ba5ee3746be739603125ec3c2dc4eb4b7c0bfc32581b0",
12
13
  "rules/wgl011.ts": "2fbb164669204ac8c3662b9c1ae156b59589913e59dd596fb38f289a0f218aaf",
13
14
  "rules/wgl012.ts": "ec4bd3fe93d30824c74223bb4e5929e493a06561a7bd3265b68dc1ba5d734da4",
@@ -17,7 +18,7 @@
17
18
  "rules/wgl016.ts": "d8bf9a371b22f37a767f92e3361d5f8e064e6dd6ba879ff1a5fc90b4b09fd304",
18
19
  "rules/wgl017.ts": "2d212de62eee4f7b086b49c92975b09d988c1ff05c2fb20cffccdc500a14e668",
19
20
  "rules/wgl018.ts": "dba1e1357becb1bbea9b435d9ce88b12138a53d8fc934483bc0ec8f22e4b18f5",
20
- "rules/wgl019.ts": "d54038ed7b24ac8e8d59efba9645ef435fe090647138fff1946443dcd75ebfd1",
21
+ "rules/wgl019.ts": "01d703b2734385c3a9c6a132e1988f07d24636aa5e8fc22d8e06cc7b4aae5d97",
21
22
  "rules/wgl020.ts": "81a84b49308f36ab2b01342bd0e262b59b2a8395d233760ac52a679c2a4ae0e6",
22
23
  "rules/wgl021.ts": "59a75e0b0123779bcd4c7f66eb068c4302860df8333a4bc7b41c68e3983505da",
23
24
  "rules/wgl022.ts": "96b87f8f410f5ffa6358823f4c8cb08a59166a7575166ede24e8c41b50f3be65",
@@ -52,5 +53,5 @@
52
53
  "skills/chant-gitlab-migrate.md": "4853d04980560b379e5b0f7267dcb0a1e5d5879cb9e9931681ce76b2a5fe69d8",
53
54
  "skills/chant-gitlab-patterns.md": "6d9a44e9e8de4c3820be9d65381b0d2ede4cb3626aaaf0011e21838a9fbf556e"
54
55
  },
55
- "composite": "b01961b1bd64c168d5d5d28d9d6cd160745f706b69b7451fe5b9218dff4c1d66"
56
+ "composite": "9f91a75e02d3e027b840d19762da56a8155b6c95227917df289e27fe46d48f8b"
56
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wgl019.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wgl019.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAK9G,eAAO,MAAM,MAAM,EAAE,cA+BpB,CAAC"}
1
+ {"version":3,"file":"wgl019.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wgl019.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAK9G,eAAO,MAAM,MAAM,EAAE,cAoCpB,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared helper for gitlab lint rules that match a bare TypeScript
3
+ * identifier (`Job`, ...) and need to know whether it actually came from
4
+ * this lexicon before flagging it (chant #1544 — cross-lexicon rule bleed:
5
+ * `new Job(...)` matches ANY lexicon's `Job` class by name alone, so a
6
+ * multi-lexicon project got gitlab-only rules applied to github/forgejo
7
+ * jobs that were never missing anything).
8
+ */
9
+ import * as ts from "typescript";
10
+ /**
11
+ * The module specifier a top-level import bound `name` to, whether via a
12
+ * named import (`import { Job } from "..."`, matched on its local —
13
+ * possibly aliased — binding) or a namespace import (`import * as gl from
14
+ * "..."`, matched on the namespace's own name). Undefined when `name` isn't
15
+ * bound by any top-level import in this file (no import at all, a
16
+ * re-export chain, a dynamic `require`, …) — the caller should treat that
17
+ * as "can't tell", not "not gitlab", to stay conservative for the common
18
+ * single-lexicon-project case (and every pre-#1544 unit test fixture, which
19
+ * has no import statements at all).
20
+ */
21
+ export declare function importSourceFor(sourceFile: ts.SourceFile, name: string): string | undefined;
22
+ /**
23
+ * True when `expression` (a `new`-expression callee) resolves — via a
24
+ * traced import — to a lexicon OTHER than gitlab. False for an unresolved
25
+ * import (conservative: still checked) or one that does resolve to gitlab.
26
+ */
27
+ export declare function isJobFromAnotherLexicon(sourceFile: ts.SourceFile, expression: ts.LeftHandSideExpression): boolean;
28
+ //# sourceMappingURL=import-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-source.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/import-source.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAiB3F;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,EAAE,CAAC,sBAAsB,GAAG,OAAO,CAWjH"}
@@ -1 +1 @@
1
- {"version":3,"file":"missing-script.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/missing-script.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,4BAA4B,CAAC;AAKxF,eAAO,MAAM,iBAAiB,EAAE,QAoD/B,CAAC"}
1
+ {"version":3,"file":"missing-script.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/missing-script.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,4BAA4B,CAAC;AAMxF,eAAO,MAAM,iBAAiB,EAAE,QAgE/B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"missing-stage.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/missing-stage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,4BAA4B,CAAC;AAGxF,eAAO,MAAM,gBAAgB,EAAE,QAmD9B,CAAC"}
1
+ {"version":3,"file":"missing-stage.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/missing-stage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,4BAA4B,CAAC;AAIxF,eAAO,MAAM,gBAAgB,EAAE,QA0D9B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab",
3
- "version": "0.46.0",
3
+ "version": "0.49.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitLab",
6
6
  "intrinsics": [
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared helper for gitlab lint rules that match a bare TypeScript
3
+ * identifier (`Job`, ...) and need to know whether it actually came from
4
+ * this lexicon before flagging it (chant #1544 — cross-lexicon rule bleed:
5
+ * `new Job(...)` matches ANY lexicon's `Job` class by name alone, so a
6
+ * multi-lexicon project got gitlab-only rules applied to github/forgejo
7
+ * jobs that were never missing anything).
8
+ */
9
+
10
+ import * as ts from "typescript";
11
+
12
+ /**
13
+ * The module specifier a top-level import bound `name` to, whether via a
14
+ * named import (`import { Job } from "..."`, matched on its local —
15
+ * possibly aliased — binding) or a namespace import (`import * as gl from
16
+ * "..."`, matched on the namespace's own name). Undefined when `name` isn't
17
+ * bound by any top-level import in this file (no import at all, a
18
+ * re-export chain, a dynamic `require`, …) — the caller should treat that
19
+ * as "can't tell", not "not gitlab", to stay conservative for the common
20
+ * single-lexicon-project case (and every pre-#1544 unit test fixture, which
21
+ * has no import statements at all).
22
+ */
23
+ export function importSourceFor(sourceFile: ts.SourceFile, name: string): string | undefined {
24
+ for (const stmt of sourceFile.statements) {
25
+ if (!ts.isImportDeclaration(stmt)) continue;
26
+ const moduleSpecifier = stmt.moduleSpecifier;
27
+ if (!ts.isStringLiteral(moduleSpecifier)) continue;
28
+ const namedBindings = stmt.importClause?.namedBindings;
29
+ if (!namedBindings) continue;
30
+
31
+ if (ts.isNamespaceImport(namedBindings)) {
32
+ if (namedBindings.name.text === name) return moduleSpecifier.text;
33
+ } else if (ts.isNamedImports(namedBindings)) {
34
+ for (const element of namedBindings.elements) {
35
+ if (element.name.text === name) return moduleSpecifier.text;
36
+ }
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ /**
43
+ * True when `expression` (a `new`-expression callee) resolves — via a
44
+ * traced import — to a lexicon OTHER than gitlab. False for an unresolved
45
+ * import (conservative: still checked) or one that does resolve to gitlab.
46
+ */
47
+ export function isJobFromAnotherLexicon(sourceFile: ts.SourceFile, expression: ts.LeftHandSideExpression): boolean {
48
+ let bindingName: string | undefined;
49
+ if (ts.isIdentifier(expression)) {
50
+ bindingName = expression.text;
51
+ } else if (ts.isPropertyAccessExpression(expression)) {
52
+ bindingName = ts.isIdentifier(expression.expression) ? expression.expression.text : undefined;
53
+ }
54
+ if (!bindingName) return false;
55
+
56
+ const source = importSourceFor(sourceFile, bindingName);
57
+ return source !== undefined && !source.includes("chant-lexicon-gitlab");
58
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
9
  import * as ts from "typescript";
10
+ import { isJobFromAnotherLexicon } from "./import-source";
10
11
 
11
12
  const VALID_EXECUTION_PROPS = new Set(["script", "trigger", "run"]);
12
13
 
@@ -31,6 +32,18 @@ export const missingScriptRule: LintRule = {
31
32
  isJob = true;
32
33
  }
33
34
 
35
+ // chant #1544 — cross-lexicon rule bleed: `new Job(...)` matches
36
+ // ANY lexicon's `Job` class by bare name alone (github's, forgejo's,
37
+ // ...), so a multi-lexicon project got WGL002 (gitlab-only) applied
38
+ // to github/forgejo jobs that were never missing anything — they
39
+ // just don't use `script`/`trigger`/`run`. Skip when the import
40
+ // resolves to a lexicon other than gitlab; an unresolved import
41
+ // (no `import` statement — most unit-test fixtures, a re-export) is
42
+ // "can't tell", so it keeps the previous, conservative behavior.
43
+ if (isJob && isJobFromAnotherLexicon(sourceFile, expression)) {
44
+ isJob = false;
45
+ }
46
+
34
47
  if (isJob && node.arguments && node.arguments.length > 0) {
35
48
  const props = node.arguments[0];
36
49
  if (ts.isObjectLiteralExpression(props)) {
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
9
  import * as ts from "typescript";
10
+ import { isJobFromAnotherLexicon } from "./import-source";
10
11
 
11
12
  export const missingStageRule: LintRule = {
12
13
  id: "WGL003",
@@ -28,6 +29,13 @@ export const missingStageRule: LintRule = {
28
29
  isJob = true;
29
30
  }
30
31
 
32
+ // chant #1544 — same cross-lexicon rule bleed as WGL002 (see
33
+ // missing-script.ts): a github/forgejo `Job` must not be judged
34
+ // against gitlab's `stage` convention.
35
+ if (isJob && isJobFromAnotherLexicon(sourceFile, expression)) {
36
+ isJob = false;
37
+ }
38
+
31
39
  if (isJob && node.arguments && node.arguments.length > 0) {
32
40
  const props = node.arguments[0];
33
41
  if (ts.isObjectLiteralExpression(props)) {
@@ -28,7 +28,12 @@ export const wgl019: PostSynthCheck = {
28
28
  const stage = props.stage as string | undefined;
29
29
  if (!stage || !DEPLOY_STAGES.has(stage.toLowerCase())) continue;
30
30
 
31
- if (!props.retry) {
31
+ // chant #1544 — `!props.retry` treated an explicit `retry: 0` the same
32
+ // as no `retry` at all, warning on a job that stated its policy
33
+ // ("retrying a deploy without a human is deliberate") as if it hadn't.
34
+ // `retry: 0` is falsy but present — only an actually-absent key means
35
+ // "no retry strategy".
36
+ if (props.retry === undefined) {
32
37
  diagnostics.push({
33
38
  checkId: "WGL019",
34
39
  severity: "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gitlab",
3
- "version": "0.46.0",
3
+ "version": "0.49.0",
4
4
  "description": "GitLab CI lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -67,8 +67,8 @@
67
67
  "typescript": "^5.9.3"
68
68
  },
69
69
  "peerDependencies": {
70
- "@intentius/chant": "^0.46.0",
71
- "@intentius/chant-lexicon-github": "^0.46.0",
70
+ "@intentius/chant": "^0.49.0",
71
+ "@intentius/chant-lexicon-github": "^0.49.0",
72
72
  "typescript": "^5.9.3"
73
73
  },
74
74
  "peerDependenciesMeta": {
@@ -53,6 +53,14 @@ describe("WGL019: Missing Retry on Deploy Jobs", () => {
53
53
  expect(diags).toHaveLength(0);
54
54
  });
55
55
 
56
+ test("does not flag deploy job with explicit retry: 0 (#1544)", () => {
57
+ const entities = new Map<string, Declarable>([
58
+ ["deployApp", new MockJob({ script: ["deploy.sh"], stage: "deploy", retry: 0 })],
59
+ ]);
60
+ const diags = wgl019.check(makeCtx(entities));
61
+ expect(diags).toHaveLength(0);
62
+ });
63
+
56
64
  test("does not flag non-deploy job without retry", () => {
57
65
  const entities = new Map<string, Declarable>([
58
66
  ["testJob", new MockJob({ script: ["npm test"], stage: "test" })],
@@ -28,7 +28,12 @@ export const wgl019: PostSynthCheck = {
28
28
  const stage = props.stage as string | undefined;
29
29
  if (!stage || !DEPLOY_STAGES.has(stage.toLowerCase())) continue;
30
30
 
31
- if (!props.retry) {
31
+ // chant #1544 — `!props.retry` treated an explicit `retry: 0` the same
32
+ // as no `retry` at all, warning on a job that stated its policy
33
+ // ("retrying a deploy without a human is deliberate") as if it hadn't.
34
+ // `retry: 0` is falsy but present — only an actually-absent key means
35
+ // "no retry strategy".
36
+ if (props.retry === undefined) {
32
37
  diagnostics.push({
33
38
  checkId: "WGL019",
34
39
  severity: "info",
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared helper for gitlab lint rules that match a bare TypeScript
3
+ * identifier (`Job`, ...) and need to know whether it actually came from
4
+ * this lexicon before flagging it (chant #1544 — cross-lexicon rule bleed:
5
+ * `new Job(...)` matches ANY lexicon's `Job` class by name alone, so a
6
+ * multi-lexicon project got gitlab-only rules applied to github/forgejo
7
+ * jobs that were never missing anything).
8
+ */
9
+
10
+ import * as ts from "typescript";
11
+
12
+ /**
13
+ * The module specifier a top-level import bound `name` to, whether via a
14
+ * named import (`import { Job } from "..."`, matched on its local —
15
+ * possibly aliased — binding) or a namespace import (`import * as gl from
16
+ * "..."`, matched on the namespace's own name). Undefined when `name` isn't
17
+ * bound by any top-level import in this file (no import at all, a
18
+ * re-export chain, a dynamic `require`, …) — the caller should treat that
19
+ * as "can't tell", not "not gitlab", to stay conservative for the common
20
+ * single-lexicon-project case (and every pre-#1544 unit test fixture, which
21
+ * has no import statements at all).
22
+ */
23
+ export function importSourceFor(sourceFile: ts.SourceFile, name: string): string | undefined {
24
+ for (const stmt of sourceFile.statements) {
25
+ if (!ts.isImportDeclaration(stmt)) continue;
26
+ const moduleSpecifier = stmt.moduleSpecifier;
27
+ if (!ts.isStringLiteral(moduleSpecifier)) continue;
28
+ const namedBindings = stmt.importClause?.namedBindings;
29
+ if (!namedBindings) continue;
30
+
31
+ if (ts.isNamespaceImport(namedBindings)) {
32
+ if (namedBindings.name.text === name) return moduleSpecifier.text;
33
+ } else if (ts.isNamedImports(namedBindings)) {
34
+ for (const element of namedBindings.elements) {
35
+ if (element.name.text === name) return moduleSpecifier.text;
36
+ }
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ /**
43
+ * True when `expression` (a `new`-expression callee) resolves — via a
44
+ * traced import — to a lexicon OTHER than gitlab. False for an unresolved
45
+ * import (conservative: still checked) or one that does resolve to gitlab.
46
+ */
47
+ export function isJobFromAnotherLexicon(sourceFile: ts.SourceFile, expression: ts.LeftHandSideExpression): boolean {
48
+ let bindingName: string | undefined;
49
+ if (ts.isIdentifier(expression)) {
50
+ bindingName = expression.text;
51
+ } else if (ts.isPropertyAccessExpression(expression)) {
52
+ bindingName = ts.isIdentifier(expression.expression) ? expression.expression.text : undefined;
53
+ }
54
+ if (!bindingName) return false;
55
+
56
+ const source = importSourceFor(sourceFile, bindingName);
57
+ return source !== undefined && !source.includes("chant-lexicon-gitlab");
58
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
9
  import * as ts from "typescript";
10
+ import { isJobFromAnotherLexicon } from "./import-source";
10
11
 
11
12
  const VALID_EXECUTION_PROPS = new Set(["script", "trigger", "run"]);
12
13
 
@@ -31,6 +32,18 @@ export const missingScriptRule: LintRule = {
31
32
  isJob = true;
32
33
  }
33
34
 
35
+ // chant #1544 — cross-lexicon rule bleed: `new Job(...)` matches
36
+ // ANY lexicon's `Job` class by bare name alone (github's, forgejo's,
37
+ // ...), so a multi-lexicon project got WGL002 (gitlab-only) applied
38
+ // to github/forgejo jobs that were never missing anything — they
39
+ // just don't use `script`/`trigger`/`run`. Skip when the import
40
+ // resolves to a lexicon other than gitlab; an unresolved import
41
+ // (no `import` statement — most unit-test fixtures, a re-export) is
42
+ // "can't tell", so it keeps the previous, conservative behavior.
43
+ if (isJob && isJobFromAnotherLexicon(sourceFile, expression)) {
44
+ isJob = false;
45
+ }
46
+
34
47
  if (isJob && node.arguments && node.arguments.length > 0) {
35
48
  const props = node.arguments[0];
36
49
  if (ts.isObjectLiteralExpression(props)) {
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
9
  import * as ts from "typescript";
10
+ import { isJobFromAnotherLexicon } from "./import-source";
10
11
 
11
12
  export const missingStageRule: LintRule = {
12
13
  id: "WGL003",
@@ -28,6 +29,13 @@ export const missingStageRule: LintRule = {
28
29
  isJob = true;
29
30
  }
30
31
 
32
+ // chant #1544 — same cross-lexicon rule bleed as WGL002 (see
33
+ // missing-script.ts): a github/forgejo `Job` must not be judged
34
+ // against gitlab's `stage` convention.
35
+ if (isJob && isJobFromAnotherLexicon(sourceFile, expression)) {
36
+ isJob = false;
37
+ }
38
+
31
39
  if (isJob && node.arguments && node.arguments.length > 0) {
32
40
  const props = node.arguments[0];
33
41
  if (ts.isObjectLiteralExpression(props)) {
@@ -88,6 +88,49 @@ describe("WGL002: missing-script", () => {
88
88
  const diags = missingScriptRule.check(ctx);
89
89
  expect(diags).toHaveLength(0);
90
90
  });
91
+
92
+ // chant #1544 — cross-lexicon rule bleed: a github/forgejo `Job` imported
93
+ // from its own lexicon package must not be judged by gitlab's `script`/
94
+ // `trigger`/`run` requirement.
95
+ test("does not flag a Job imported from the github lexicon", () => {
96
+ const ctx = createContext(
97
+ `import { Job } from "@intentius/chant-lexicon-github";\nconst j = new Job({ "runs-on": "ubuntu-latest", steps: [] });`,
98
+ );
99
+ const diags = missingScriptRule.check(ctx);
100
+ expect(diags).toHaveLength(0);
101
+ });
102
+
103
+ test("does not flag a Job imported from the forgejo lexicon", () => {
104
+ const ctx = createContext(
105
+ `import { Job } from "@intentius/chant-lexicon-forgejo";\nconst j = new Job({ "runs-on": "ubuntu-latest", steps: [] });`,
106
+ );
107
+ const diags = missingScriptRule.check(ctx);
108
+ expect(diags).toHaveLength(0);
109
+ });
110
+
111
+ test("still flags a Job imported from the gitlab lexicon", () => {
112
+ const ctx = createContext(
113
+ `import { Job } from "@intentius/chant-lexicon-gitlab";\nconst j = new Job({ stage: "test" });`,
114
+ );
115
+ const diags = missingScriptRule.check(ctx);
116
+ expect(diags).toHaveLength(1);
117
+ });
118
+
119
+ test("still flags a namespace-imported gl.Job() from the gitlab lexicon", () => {
120
+ const ctx = createContext(
121
+ `import * as gl from "@intentius/chant-lexicon-gitlab";\nconst j = new gl.Job({ stage: "build" });`,
122
+ );
123
+ const diags = missingScriptRule.check(ctx);
124
+ expect(diags).toHaveLength(1);
125
+ });
126
+
127
+ test("does not flag a namespace-imported Job from another lexicon", () => {
128
+ const ctx = createContext(
129
+ `import * as gh from "@intentius/chant-lexicon-github";\nconst j = new gh.Job({ "runs-on": "ubuntu-latest", steps: [] });`,
130
+ );
131
+ const diags = missingScriptRule.check(ctx);
132
+ expect(diags).toHaveLength(0);
133
+ });
91
134
  });
92
135
 
93
136
  // ── WGL003: missing stage ───────────────────────────────────────────
@@ -107,6 +150,23 @@ describe("WGL003: missing-stage", () => {
107
150
  const diags = missingStageRule.check(ctx);
108
151
  expect(diags).toHaveLength(0);
109
152
  });
153
+
154
+ // chant #1544 — same cross-lexicon rule bleed as WGL002.
155
+ test("does not flag a Job imported from the github lexicon", () => {
156
+ const ctx = createContext(
157
+ `import { Job } from "@intentius/chant-lexicon-github";\nconst j = new Job({ "runs-on": "ubuntu-latest", steps: [] });`,
158
+ );
159
+ const diags = missingStageRule.check(ctx);
160
+ expect(diags).toHaveLength(0);
161
+ });
162
+
163
+ test("still flags a Job imported from the gitlab lexicon", () => {
164
+ const ctx = createContext(
165
+ `import { Job } from "@intentius/chant-lexicon-gitlab";\nconst j = new Job({ script: ["test"] });`,
166
+ );
167
+ const diags = missingStageRule.check(ctx);
168
+ expect(diags).toHaveLength(1);
169
+ });
110
170
  });
111
171
 
112
172
  // ── WGL004: artifact no expiry ──────────────────────────────────────