@intentius/chant 0.0.5 → 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 (91) hide show
  1. package/bin/chant +20 -0
  2. package/package.json +18 -17
  3. package/src/bench.test.ts +1 -1
  4. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
  5. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
  6. package/src/cli/commands/build.ts +1 -2
  7. package/src/cli/commands/doctor.ts +8 -3
  8. package/src/cli/commands/import.ts +2 -2
  9. package/src/cli/commands/init-lexicon.test.ts +0 -3
  10. package/src/cli/commands/init-lexicon.ts +1 -79
  11. package/src/cli/commands/init.test.ts +44 -4
  12. package/src/cli/commands/init.ts +69 -26
  13. package/src/cli/commands/lint.ts +27 -13
  14. package/src/cli/commands/list.ts +2 -2
  15. package/src/cli/commands/update.ts +5 -3
  16. package/src/cli/conflict-check.test.ts +0 -1
  17. package/src/cli/handlers/dev.ts +1 -9
  18. package/src/cli/handlers/init.ts +1 -0
  19. package/src/cli/lsp/server.ts +1 -1
  20. package/src/cli/main.ts +17 -3
  21. package/src/cli/mcp/server.test.ts +233 -4
  22. package/src/cli/mcp/server.ts +6 -0
  23. package/src/cli/mcp/tools/explain.ts +134 -0
  24. package/src/cli/mcp/tools/scaffold.ts +125 -0
  25. package/src/cli/mcp/tools/search.ts +98 -0
  26. package/src/cli/registry.ts +1 -0
  27. package/src/cli/reporters/stylish.test.ts +212 -1
  28. package/src/cli/reporters/stylish.ts +133 -36
  29. package/src/codegen/docs-rules.test.ts +112 -0
  30. package/src/codegen/docs-rules.ts +129 -0
  31. package/src/codegen/docs.ts +3 -1
  32. package/src/codegen/generate-registry.test.ts +1 -1
  33. package/src/codegen/generate-registry.ts +2 -3
  34. package/src/codegen/generate-typescript.test.ts +70 -6
  35. package/src/codegen/generate-typescript.ts +15 -9
  36. package/src/codegen/generate.ts +1 -12
  37. package/src/codegen/package.ts +1 -1
  38. package/src/codegen/typecheck.ts +6 -11
  39. package/src/composite.test.ts +83 -16
  40. package/src/composite.ts +7 -5
  41. package/src/config.ts +4 -0
  42. package/src/detectLexicon.test.ts +2 -2
  43. package/src/discovery/collect.test.ts +2 -2
  44. package/src/discovery/collect.ts +1 -1
  45. package/src/index.ts +2 -1
  46. package/src/lexicon-integrity.ts +5 -4
  47. package/src/lexicon-schema.ts +8 -0
  48. package/src/lexicon.ts +15 -7
  49. package/src/lint/config.ts +8 -6
  50. package/src/lint/declarative.ts +6 -0
  51. package/src/lint/engine.test.ts +287 -11
  52. package/src/lint/engine.ts +101 -23
  53. package/src/lint/rule-registry.test.ts +112 -0
  54. package/src/lint/rule-registry.ts +118 -0
  55. package/src/lint/rule.ts +8 -0
  56. package/src/lint/rules/cor017-composite-name-match.ts +2 -1
  57. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
  58. package/src/lint/rules/declarable-naming-convention.ts +1 -0
  59. package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
  60. package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
  61. package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
  62. package/src/lint/rules/evl004-spread-non-const.ts +1 -0
  63. package/src/lint/rules/evl005-resource-block-body.ts +1 -0
  64. package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
  65. package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
  66. package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
  67. package/src/lint/rules/export-required.ts +1 -0
  68. package/src/lint/rules/file-declarable-limit.ts +1 -0
  69. package/src/lint/rules/flat-declarations.test.ts +8 -7
  70. package/src/lint/rules/flat-declarations.ts +2 -3
  71. package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
  72. package/src/lint/rules/no-redundant-type-import.ts +1 -0
  73. package/src/lint/rules/no-redundant-value-cast.ts +1 -0
  74. package/src/lint/rules/no-string-ref.ts +1 -0
  75. package/src/lint/rules/no-unused-declarable-import.ts +1 -0
  76. package/src/lint/rules/no-unused-declarable.test.ts +8 -0
  77. package/src/lint/rules/no-unused-declarable.ts +4 -0
  78. package/src/lint/rules/single-concern-file.ts +1 -0
  79. package/src/lsp/lexicon-providers.ts +7 -0
  80. package/src/lsp/types.ts +1 -0
  81. package/src/resource-attributes.test.ts +79 -0
  82. package/src/resource-attributes.ts +42 -0
  83. package/src/runtime-adapter.ts +158 -0
  84. package/src/runtime.ts +4 -3
  85. package/src/serializer-walker.test.ts +0 -9
  86. package/src/serializer-walker.ts +1 -3
  87. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  88. package/src/codegen/case.test.ts +0 -30
  89. package/src/codegen/case.ts +0 -11
  90. package/src/codegen/rollback.test.ts +0 -92
  91. package/src/codegen/rollback.ts +0 -115
@@ -0,0 +1,112 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { buildRuleRegistry } from "./rule-registry";
3
+ import type { LintRule, LintContext, LintDiagnostic } from "./rule";
4
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "./post-synth";
5
+
6
+ function mockRule(id: string, overrides: Partial<LintRule> = {}): LintRule {
7
+ return {
8
+ id,
9
+ severity: "warning",
10
+ category: "correctness",
11
+ description: `Description for ${id}`,
12
+ check: (): LintDiagnostic[] => [],
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ function mockCheck(id: string, description: string): PostSynthCheck {
18
+ return {
19
+ id,
20
+ description,
21
+ check: (): PostSynthDiagnostic[] => [],
22
+ };
23
+ }
24
+
25
+ describe("buildRuleRegistry", () => {
26
+ test("collects core rules", () => {
27
+ const entries = buildRuleRegistry([
28
+ mockRule("COR001"),
29
+ mockRule("COR008"),
30
+ ]);
31
+
32
+ expect(entries).toHaveLength(2);
33
+ expect(entries[0].id).toBe("COR001");
34
+ expect(entries[0].source).toBe("core");
35
+ expect(entries[0].phase).toBe("pre-synth");
36
+ expect(entries[0].description).toBe("Description for COR001");
37
+ });
38
+
39
+ test("collects plugin lint rules", () => {
40
+ const entries = buildRuleRegistry([], [
41
+ {
42
+ name: "aws",
43
+ rules: [mockRule("WAW001", { category: "security" })],
44
+ },
45
+ ]);
46
+
47
+ expect(entries).toHaveLength(1);
48
+ expect(entries[0].source).toBe("aws");
49
+ expect(entries[0].category).toBe("security");
50
+ });
51
+
52
+ test("collects plugin post-synth checks", () => {
53
+ const entries = buildRuleRegistry([], [
54
+ {
55
+ name: "aws",
56
+ postSynthChecks: [
57
+ mockCheck("WAW018", "S3 public access not blocked"),
58
+ ],
59
+ },
60
+ ]);
61
+
62
+ expect(entries).toHaveLength(1);
63
+ expect(entries[0].phase).toBe("post-synth");
64
+ expect(entries[0].hasAutoFix).toBe(false);
65
+ expect(entries[0].category).toBe("security");
66
+ });
67
+
68
+ test("sorts entries by ID", () => {
69
+ const entries = buildRuleRegistry([
70
+ mockRule("COR008"),
71
+ mockRule("COR001"),
72
+ ]);
73
+
74
+ expect(entries[0].id).toBe("COR001");
75
+ expect(entries[1].id).toBe("COR008");
76
+ });
77
+
78
+ test("detects auto-fix capability", () => {
79
+ const withFix = mockRule("COR001");
80
+ withFix.fix = () => [];
81
+ const withoutFix = mockRule("COR002");
82
+
83
+ const entries = buildRuleRegistry([withFix, withoutFix]);
84
+
85
+ expect(entries.find((e) => e.id === "COR001")?.hasAutoFix).toBe(true);
86
+ expect(entries.find((e) => e.id === "COR002")?.hasAutoFix).toBe(false);
87
+ });
88
+
89
+ test("falls back to rule ID when description is missing", () => {
90
+ const rule = mockRule("COR001");
91
+ delete (rule as Record<string, unknown>).description;
92
+
93
+ const entries = buildRuleRegistry([rule]);
94
+ expect(entries[0].description).toBe("COR001");
95
+ });
96
+
97
+ test("combines core and plugin rules", () => {
98
+ const entries = buildRuleRegistry(
99
+ [mockRule("COR001")],
100
+ [
101
+ {
102
+ name: "aws",
103
+ rules: [mockRule("WAW001")],
104
+ postSynthChecks: [mockCheck("WAW010", "Redundant DependsOn")],
105
+ },
106
+ ],
107
+ );
108
+
109
+ expect(entries).toHaveLength(3);
110
+ expect(entries.map((e) => e.id)).toEqual(["COR001", "WAW001", "WAW010"]);
111
+ });
112
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Rule registry — collects metadata from all rule sources into a flat, sortable array.
3
+ * Used for documentation generation and programmatic rule introspection.
4
+ */
5
+
6
+ import type { LintRule, Severity, Category } from "./rule";
7
+ import type { PostSynthCheck } from "./post-synth";
8
+
9
+ export interface RuleEntry {
10
+ /** Unique rule ID (e.g. "COR001", "WAW018") */
11
+ id: string;
12
+ /** Human-readable description of what this rule checks */
13
+ description: string;
14
+ /** Rule category */
15
+ category: Category;
16
+ /** Default severity level */
17
+ defaultSeverity: Severity;
18
+ /** Source of the rule ("core" or lexicon name) */
19
+ source: "core" | string;
20
+ /** Phase the rule runs in */
21
+ phase: "pre-synth" | "post-synth";
22
+ /** Whether this rule provides automatic fixes */
23
+ hasAutoFix: boolean;
24
+ /** Link to rule documentation */
25
+ helpUri?: string;
26
+ }
27
+
28
+ /**
29
+ * Build a rule registry from core rules and plugin-provided rules/checks.
30
+ *
31
+ * @param coreRules - Core LintRules (COR/EVL rules from packages/core)
32
+ * @param plugins - Array of plugin contributions with name, rules, and post-synth checks
33
+ * @returns Flat array of RuleEntry sorted by ID
34
+ */
35
+ export function buildRuleRegistry(
36
+ coreRules: LintRule[],
37
+ plugins: Array<{
38
+ name: string;
39
+ rules?: LintRule[];
40
+ postSynthChecks?: PostSynthCheck[];
41
+ }> = [],
42
+ ): RuleEntry[] {
43
+ const entries: RuleEntry[] = [];
44
+
45
+ // Core lint rules
46
+ for (const rule of coreRules) {
47
+ entries.push({
48
+ id: rule.id,
49
+ description: rule.description ?? rule.id,
50
+ category: rule.category,
51
+ defaultSeverity: rule.severity,
52
+ source: "core",
53
+ phase: "pre-synth",
54
+ hasAutoFix: typeof rule.fix === "function",
55
+ helpUri: rule.helpUri,
56
+ });
57
+ }
58
+
59
+ // Plugin-provided lint rules and post-synth checks
60
+ for (const plugin of plugins) {
61
+ if (plugin.rules) {
62
+ for (const rule of plugin.rules) {
63
+ entries.push({
64
+ id: rule.id,
65
+ description: rule.description ?? rule.id,
66
+ category: rule.category,
67
+ defaultSeverity: rule.severity,
68
+ source: plugin.name,
69
+ phase: "pre-synth",
70
+ hasAutoFix: typeof rule.fix === "function",
71
+ helpUri: rule.helpUri,
72
+ });
73
+ }
74
+ }
75
+
76
+ if (plugin.postSynthChecks) {
77
+ for (const check of plugin.postSynthChecks) {
78
+ entries.push({
79
+ id: check.id,
80
+ description: check.description,
81
+ category: inferPostSynthCategory(check.id),
82
+ defaultSeverity: inferPostSynthSeverity(check.id, check.description),
83
+ source: plugin.name,
84
+ phase: "post-synth",
85
+ hasAutoFix: false,
86
+ helpUri: `https://chant.dev/lint-rules/${check.id.toLowerCase()}`,
87
+ });
88
+ }
89
+ }
90
+ }
91
+
92
+ return entries.sort((a, b) => a.id.localeCompare(b.id));
93
+ }
94
+
95
+ /**
96
+ * Infer category for post-synth checks from rule ID prefix.
97
+ * WAW018+ are security rules, existing WAW/COR/EXT default to correctness.
98
+ */
99
+ function inferPostSynthCategory(id: string): Category {
100
+ const num = parseInt(id.replace(/\D/g, ""), 10);
101
+ if (id.startsWith("WAW") && num >= 18) return "security";
102
+ return "correctness";
103
+ }
104
+
105
+ /**
106
+ * Infer default severity for post-synth checks.
107
+ * Security checks with "error" level: WAW018, WAW019, WAW021.
108
+ */
109
+ function inferPostSynthSeverity(id: string, description: string): Severity {
110
+ // Check if the description hints at severity
111
+ if (description.toLowerCase().includes("error")) return "error";
112
+ // Known error-level security checks
113
+ const errorIds = new Set(["WAW018", "WAW019", "WAW021"]);
114
+ if (errorIds.has(id)) return "error";
115
+ // Info-level checks
116
+ if (id === "WAW027") return "info";
117
+ return "warning";
118
+ }
package/src/lint/rule.ts CHANGED
@@ -34,6 +34,10 @@ export interface LintDiagnostic {
34
34
  line: number;
35
35
  /** Column number (1-based) */
36
36
  column: number;
37
+ /** End line number (1-based), when the diagnostic spans a range */
38
+ endLine?: number;
39
+ /** End column number (1-based), when the diagnostic spans a range */
40
+ endColumn?: number;
37
41
  /** ID of the rule that produced this diagnostic */
38
42
  ruleId: string;
39
43
  /** Severity level */
@@ -73,6 +77,10 @@ export interface LintRule {
73
77
  severity: Severity;
74
78
  /** Category for grouping */
75
79
  category: Category;
80
+ /** Human-readable description of what this rule checks */
81
+ description?: string;
82
+ /** Link to rule documentation */
83
+ helpUri?: string;
76
84
  /** Check the code and return diagnostics */
77
85
  check(context: LintContext, options?: Record<string, unknown>): LintDiagnostic[];
78
86
  /** Optionally provide fixes for issues found */
@@ -7,7 +7,7 @@ import { isCompositeCallee } from "./composite-scope";
7
7
  *
8
8
  * The second argument to Composite() (the name string) must match the
9
9
  * const variable it's assigned to. This name is used in resource expansion
10
- * (e.g. healthApi_role, healthApi_func) and in error messages.
10
+ * (e.g. healthApiRole, healthApiFunc) and in error messages.
11
11
  *
12
12
  * Triggers on: const LambdaApi = Composite(fn, "MyFunction") — mismatch
13
13
  * Triggers on: const LambdaApi = Composite(fn) — missing name
@@ -100,6 +100,7 @@ export const cor017CompositeNameMatchRule: LintRule = {
100
100
  id: "COR017",
101
101
  severity: "error",
102
102
  category: "correctness",
103
+ description: "Composite name argument must match the variable it is assigned to",
103
104
  check(context: LintContext): LintDiagnostic[] {
104
105
  const diagnostics: LintDiagnostic[] = [];
105
106
  checkNode(context.sourceFile, context, diagnostics);
@@ -5,7 +5,7 @@ import { isCompositeCallee } from "./composite-scope";
5
5
  /**
6
6
  * COR018: Prefer lexicon property types in Composite props
7
7
  *
8
- * Composite prop interfaces should use lexicon property types (via the barrel)
8
+ * Composite prop interfaces should use lexicon property types (via the lexicon package)
9
9
  * instead of locally-declared interfaces or type aliases. Local types next to
10
10
  * a Composite definition often duplicate existing lexicon property types.
11
11
  *
@@ -13,7 +13,7 @@ import { isCompositeCallee } from "./composite-scope";
13
13
  * in the same file as the Composite (excluding the props interface itself)
14
14
  * OK: InstanceType<typeof _.Role_Policy>
15
15
  * OK: primitives (string, number, boolean)
16
- * OK: barrel-imported types
16
+ * OK: lexicon-imported types
17
17
  */
18
18
 
19
19
  /**
@@ -60,7 +60,7 @@ function findLocalTypeDeclarations(sourceFile: ts.SourceFile): Map<string, ts.No
60
60
 
61
61
  /**
62
62
  * Collect type references used in a props interface's field types.
63
- * Returns names of locally-referenced types (not primitives, not from barrel).
63
+ * Returns names of locally-referenced types (not primitives, not from the lexicon package).
64
64
  */
65
65
  function collectFieldTypeRefs(
66
66
  propsDecl: ts.Node,
@@ -161,6 +161,7 @@ export const cor018CompositePreferLexiconTypeRule: LintRule = {
161
161
  id: "COR018",
162
162
  severity: "info",
163
163
  category: "style",
164
+ description: "Prefer lexicon property types over local interfaces in Composite props",
164
165
  check(context: LintContext): LintDiagnostic[] {
165
166
  return checkFile(context);
166
167
  },
@@ -62,6 +62,7 @@ export const declarableNamingConventionRule: LintRule = {
62
62
  id: "COR005",
63
63
  severity: "warning",
64
64
  category: "style",
65
+ description: "Exported declarable instances must use camelCase naming",
65
66
  check(context: LintContext): LintDiagnostic[] {
66
67
  const diagnostics: LintDiagnostic[] = [];
67
68
  checkNode(context.sourceFile, context, diagnostics);
@@ -141,6 +141,7 @@ export const evl001NonLiteralExpressionRule: LintRule = {
141
141
  id: "EVL001",
142
142
  severity: "error",
143
143
  category: "correctness",
144
+ description: "Resource constructor property values must be statically evaluable — no function calls",
144
145
  check(context: LintContext): LintDiagnostic[] {
145
146
  const diagnostics: LintDiagnostic[] = [];
146
147
  checkNode(context.sourceFile, context, diagnostics);
@@ -53,6 +53,7 @@ export const evl002ControlFlowResourceRule: LintRule = {
53
53
  id: "EVL002",
54
54
  severity: "error",
55
55
  category: "correctness",
56
+ description: "Resource constructors must not appear inside control flow statements",
56
57
  check(context: LintContext): LintDiagnostic[] {
57
58
  const diagnostics: LintDiagnostic[] = [];
58
59
  checkNode(context.sourceFile, context, diagnostics);
@@ -33,6 +33,7 @@ export const evl003DynamicPropertyAccessRule: LintRule = {
33
33
  id: "EVL003",
34
34
  severity: "error",
35
35
  category: "correctness",
36
+ description: "Computed property access must use a string or numeric literal key",
36
37
  check(context: LintContext): LintDiagnostic[] {
37
38
  const diagnostics: LintDiagnostic[] = [];
38
39
  checkNode(context.sourceFile, context, diagnostics);
@@ -103,6 +103,7 @@ export const evl004SpreadNonConstRule: LintRule = {
103
103
  id: "EVL004",
104
104
  severity: "error",
105
105
  category: "correctness",
106
+ description: "Spread expressions must reference a const variable or literal, not a dynamic source",
106
107
  check(context: LintContext): LintDiagnostic[] {
107
108
  const diagnostics: LintDiagnostic[] = [];
108
109
  checkNode(context.sourceFile, context, diagnostics);
@@ -41,6 +41,7 @@ export const evl005ResourceBlockBodyRule: LintRule = {
41
41
  id: "EVL005",
42
42
  severity: "error",
43
43
  category: "correctness",
44
+ description: "The resource() callback must use expression body, not block body",
44
45
  check(context: LintContext): LintDiagnostic[] {
45
46
  const diagnostics: LintDiagnostic[] = [];
46
47
  checkNode(context.sourceFile, context, diagnostics);
@@ -131,6 +131,7 @@ export const evl007InvalidSiblingsRule: LintRule = {
131
131
  id: "EVL007",
132
132
  severity: "error",
133
133
  category: "correctness",
134
+ description: "Validates that siblings access in Composite factories references existing member keys",
134
135
  check(context: LintContext): LintDiagnostic[] {
135
136
  const diagnostics: LintDiagnostic[] = [];
136
137
  checkNode(context.sourceFile, context, diagnostics);
@@ -183,6 +183,7 @@ export const evl009CompositeNoConstantRule: LintRule = {
183
183
  id: "EVL009",
184
184
  severity: "warning",
185
185
  category: "style",
186
+ description: "Flags extractable constants inside Composite factories — move to a separate file",
186
187
  check(context: LintContext): LintDiagnostic[] {
187
188
  const diagnostics: LintDiagnostic[] = [];
188
189
  const importedNames = collectImportedNames(context.sourceFile);
@@ -61,6 +61,7 @@ export const evl010CompositeNoTransformRule: LintRule = {
61
61
  id: "EVL010",
62
62
  severity: "warning",
63
63
  category: "style",
64
+ description: "Flags data transformations (map, filter, reduce) inside Composite factories",
64
65
  check(context: LintContext): LintDiagnostic[] {
65
66
  const diagnostics: LintDiagnostic[] = [];
66
67
  checkNode(context.sourceFile, context, diagnostics);
@@ -149,6 +149,7 @@ export const exportRequiredRule: LintRule = {
149
149
  id: "COR008",
150
150
  severity: "warning",
151
151
  category: "correctness",
152
+ description: "Detects declarable instances that are not exported for chant discovery",
152
153
  check(context: LintContext): LintDiagnostic[] {
153
154
  const diagnostics: LintDiagnostic[] = [];
154
155
  const exportInfo = collectExportInfo(context.sourceFile);
@@ -73,6 +73,7 @@ export const fileDeclarableLimitRule: LintRule = {
73
73
  id: "COR009",
74
74
  severity: "warning",
75
75
  category: "style",
76
+ description: "Limits the number of Declarable instances per file to encourage splitting by concern",
76
77
  check(context: LintContext, options?: Record<string, unknown>): LintDiagnostic[] {
77
78
  const limit = (typeof options?.max === "number" ? options.max : null) ?? DECLARABLE_LIMIT;
78
79
  const instances: ts.NewExpression[] = [];
@@ -163,7 +163,7 @@ describe("COR001: flat-declarations", () => {
163
163
  expect(diagnostics[0].ruleId).toBe("COR001");
164
164
  });
165
165
 
166
- test("does not trigger inside Composite() factory callback", () => {
166
+ test("triggers inside Composite() factory callback", () => {
167
167
  const code = `
168
168
  const MyComposite = Composite((props) => {
169
169
  const role = new Role({
@@ -177,10 +177,11 @@ describe("COR001: flat-declarations", () => {
177
177
  `;
178
178
  const context = createContext(code);
179
179
  const diagnostics = flatDeclarationsRule.check(context);
180
- expect(diagnostics).toHaveLength(0);
180
+ expect(diagnostics).toHaveLength(1);
181
+ expect(diagnostics[0].ruleId).toBe("COR001");
181
182
  });
182
183
 
183
- test("does not trigger inside _.Composite() factory callback", () => {
184
+ test("triggers inside _.Composite() factory callback", () => {
184
185
  const code = `
185
186
  const MyComposite = _.Composite((props) => {
186
187
  const role = new _.Role({
@@ -191,10 +192,11 @@ describe("COR001: flat-declarations", () => {
191
192
  `;
192
193
  const context = createContext(code);
193
194
  const diagnostics = flatDeclarationsRule.check(context);
194
- expect(diagnostics).toHaveLength(0);
195
+ expect(diagnostics).toHaveLength(1);
196
+ expect(diagnostics[0].ruleId).toBe("COR001");
195
197
  });
196
198
 
197
- test("still triggers outside Composite() in same file", () => {
199
+ test("triggers both inside and outside Composite() in same file", () => {
198
200
  const code = `
199
201
  const MyComposite = Composite((props) => {
200
202
  const role = new Role({ config: { value: 1 } });
@@ -204,7 +206,6 @@ describe("COR001: flat-declarations", () => {
204
206
  `;
205
207
  const context = createContext(code);
206
208
  const diagnostics = flatDeclarationsRule.check(context);
207
- expect(diagnostics).toHaveLength(1);
208
- expect(diagnostics[0].message).toContain("Inline object");
209
+ expect(diagnostics).toHaveLength(2);
209
210
  });
210
211
  });
@@ -1,6 +1,5 @@
1
1
  import * as ts from "typescript";
2
2
  import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
- import { isInsideCompositeFactory } from "./composite-scope";
4
3
 
5
4
  /**
6
5
  * COR001: No inline objects in Declarable constructors
@@ -17,8 +16,7 @@ import { isInsideCompositeFactory } from "./composite-scope";
17
16
 
18
17
  function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
19
18
  // Check for NewExpression nodes (constructor calls)
20
- // Skip resource constructors inside Composite() factory callbacks
21
- if (ts.isNewExpression(node) && !isInsideCompositeFactory(node)) {
19
+ if (ts.isNewExpression(node)) {
22
20
  // Check if the first argument is an object literal
23
21
  if (node.arguments && node.arguments.length > 0) {
24
22
  const firstArg = node.arguments[0];
@@ -62,6 +60,7 @@ export const flatDeclarationsRule: LintRule = {
62
60
  id: "COR001",
63
61
  severity: "warning",
64
62
  category: "style",
63
+ description: "Detects inline object literals in Declarable constructors — extract to named exports",
65
64
  check(context: LintContext): LintDiagnostic[] {
66
65
  const diagnostics: LintDiagnostic[] = [];
67
66
  checkNode(context.sourceFile, context, diagnostics);
@@ -128,6 +128,7 @@ export const noCyclicDeclarableRefRule: LintRule = {
128
128
  id: "COR011",
129
129
  severity: "error",
130
130
  category: "correctness",
131
+ description: "Detects circular references between declarables in the same file",
131
132
  check(context: LintContext): LintDiagnostic[] {
132
133
  const diagnostics: LintDiagnostic[] = [];
133
134
  const declarables = collectExportedDeclarables(context.sourceFile);
@@ -13,6 +13,7 @@ export const noRedundantTypeImportRule: LintRule = {
13
13
  id: "COR012",
14
14
  severity: "warning",
15
15
  category: "style",
16
+ description: "Flags redundant type imports when a namespace import already provides access",
16
17
  check(context: LintContext): LintDiagnostic[] {
17
18
  const diagnostics: LintDiagnostic[] = [];
18
19
  const sf = context.sourceFile;
@@ -38,6 +38,7 @@ export const noRedundantValueCastRule: LintRule = {
38
38
  id: "COR015",
39
39
  severity: "warning",
40
40
  category: "style",
41
+ description: "Flags redundant 'as Value<...>' casts — Intrinsic types already satisfy Value<T>",
41
42
  check(context: LintContext): LintDiagnostic[] {
42
43
  const diagnostics: LintDiagnostic[] = [];
43
44
  checkNode(context.sourceFile, context, diagnostics);
@@ -15,6 +15,7 @@ export const noStringRefRule: LintRule = {
15
15
  id: "COR003",
16
16
  severity: "warning",
17
17
  category: "correctness",
18
+ description: "Flags string-based GetAtt() and Ref() calls — use typed property access instead",
18
19
  check(context: LintContext): LintDiagnostic[] {
19
20
  const diagnostics: LintDiagnostic[] = [];
20
21
  const sf = context.sourceFile;
@@ -77,6 +77,7 @@ export const noUnusedDeclarableImportRule: LintRule = {
77
77
  id: "COR010",
78
78
  severity: "warning",
79
79
  category: "style",
80
+ description: "Flags unused namespace imports from @intentius/chant packages",
80
81
  check(context: LintContext): LintDiagnostic[] {
81
82
  const diagnostics: LintDiagnostic[] = [];
82
83
  const imports = collectNamespaceImports(context.sourceFile);
@@ -124,6 +124,14 @@ describe("COR004: no-unused-declarable", () => {
124
124
  expect(diags).toHaveLength(0);
125
125
  });
126
126
 
127
+ test("OK for Parameter declarations (inherently cross-file)", () => {
128
+ const ctx = createContext(
129
+ `export const environment = new Parameter("String", { description: "env" });`,
130
+ );
131
+ const diags = noUnusedDeclarableRule.check(ctx);
132
+ expect(diags).toHaveLength(0);
133
+ });
134
+
127
135
  test("detects reference in array literal", () => {
128
136
  const ctx = createContext(
129
137
  `export const bucket = new Bucket({});\n` + `export const list = [bucket];`,
@@ -51,6 +51,9 @@ function collectExportedDeclarables(sourceFile: ts.SourceFile): DeclarableInfo[]
51
51
  const className = getNewExpressionClassName(decl.initializer);
52
52
  if (!className || !isCapitalized(className)) continue;
53
53
 
54
+ // Parameters are inherently cross-file (declared in params.ts, consumed via Ref() elsewhere)
55
+ if (className === "Parameter") continue;
56
+
54
57
  declarables.push({
55
58
  name: decl.name.text,
56
59
  node,
@@ -92,6 +95,7 @@ export const noUnusedDeclarableRule: LintRule = {
92
95
  id: "COR004",
93
96
  severity: "warning",
94
97
  category: "correctness",
98
+ description: "Detects exported declarables that are never referenced in the same file",
95
99
  check(context: LintContext): LintDiagnostic[] {
96
100
  const diagnostics: LintDiagnostic[] = [];
97
101
  const declarables = collectExportedDeclarables(context.sourceFile);
@@ -64,6 +64,7 @@ export const singleConcernFileRule: LintRule = {
64
64
  id: "COR013",
65
65
  severity: "info",
66
66
  category: "style",
67
+ description: "Flags files mixing resource Declarables with configuration Declarables",
67
68
  check(context: LintContext): LintDiagnostic[] {
68
69
  const newExpressions: NewExpressionInfo[] = [];
69
70
  collectNewExpressions(context.sourceFile, context.sourceFile, newExpressions);
@@ -19,6 +19,10 @@ export interface LexiconEntry {
19
19
  createOnly?: string[];
20
20
  writeOnly?: string[];
21
21
  primaryIdentifier?: string[];
22
+ deprecatedProperties?: string[];
23
+ conditionalCreateOnly?: string[];
24
+ replacementStrategy?: "delete_then_create" | "create_then_delete";
25
+ tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
22
26
  }
23
27
 
24
28
  // ── LexiconIndex ───────────────────────────────────────────────────
@@ -109,8 +113,10 @@ export function lexiconCompletions(
109
113
 
110
114
  if (constructorMatch) {
111
115
  const className = constructorMatch[1];
116
+ const entry = index.getEntry(className);
112
117
  const props = index.getPropertyNames(className);
113
118
  if (props.length > 0) {
119
+ const deprecatedSet = new Set(entry?.deprecatedProperties ?? []);
114
120
  const filtered = wordAtCursor
115
121
  ? props.filter((p) => p.toLowerCase().startsWith(wordAtCursor.toLowerCase()))
116
122
  : props;
@@ -119,6 +125,7 @@ export function lexiconCompletions(
119
125
  insertText: p,
120
126
  kind: "property" as const,
121
127
  detail: `Property of ${className}`,
128
+ ...(deprecatedSet.has(p) && { deprecated: true }),
122
129
  }));
123
130
  }
124
131
  }
package/src/lsp/types.ts CHANGED
@@ -31,6 +31,7 @@ export interface CompletionItem {
31
31
  kind?: CompletionItemKind;
32
32
  detail?: string;
33
33
  documentation?: string;
34
+ deprecated?: boolean;
34
35
  }
35
36
 
36
37
  export interface CompletionContext {