@intentius/chant 0.0.8 → 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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/bench.test.ts +1 -1
  3. package/src/cli/commands/doctor.ts +8 -3
  4. package/src/cli/commands/init.test.ts +44 -4
  5. package/src/cli/commands/init.ts +55 -23
  6. package/src/cli/commands/lint.ts +27 -13
  7. package/src/cli/handlers/init.ts +1 -0
  8. package/src/cli/lsp/server.ts +1 -1
  9. package/src/cli/main.ts +4 -0
  10. package/src/cli/mcp/server.test.ts +28 -2
  11. package/src/cli/mcp/tools/scaffold.ts +21 -3
  12. package/src/cli/registry.ts +1 -0
  13. package/src/cli/reporters/stylish.test.ts +212 -1
  14. package/src/cli/reporters/stylish.ts +133 -36
  15. package/src/codegen/docs-rules.test.ts +112 -0
  16. package/src/codegen/docs-rules.ts +129 -0
  17. package/src/codegen/docs.ts +3 -1
  18. package/src/codegen/generate-typescript.test.ts +64 -0
  19. package/src/codegen/generate-typescript.ts +13 -3
  20. package/src/codegen/package.ts +1 -1
  21. package/src/composite.test.ts +83 -16
  22. package/src/composite.ts +7 -5
  23. package/src/detectLexicon.test.ts +2 -2
  24. package/src/discovery/collect.test.ts +2 -2
  25. package/src/discovery/collect.ts +1 -1
  26. package/src/index.ts +1 -0
  27. package/src/lexicon-schema.ts +8 -0
  28. package/src/lexicon.ts +13 -1
  29. package/src/lint/declarative.ts +6 -0
  30. package/src/lint/engine.test.ts +287 -11
  31. package/src/lint/engine.ts +101 -23
  32. package/src/lint/rule-registry.test.ts +112 -0
  33. package/src/lint/rule-registry.ts +118 -0
  34. package/src/lint/rule.ts +8 -0
  35. package/src/lint/rules/cor017-composite-name-match.ts +2 -1
  36. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
  37. package/src/lint/rules/declarable-naming-convention.ts +1 -0
  38. package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
  39. package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
  40. package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
  41. package/src/lint/rules/evl004-spread-non-const.ts +1 -0
  42. package/src/lint/rules/evl005-resource-block-body.ts +1 -0
  43. package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
  44. package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
  45. package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
  46. package/src/lint/rules/export-required.ts +1 -0
  47. package/src/lint/rules/file-declarable-limit.ts +1 -0
  48. package/src/lint/rules/flat-declarations.test.ts +8 -7
  49. package/src/lint/rules/flat-declarations.ts +2 -3
  50. package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
  51. package/src/lint/rules/no-redundant-type-import.ts +1 -0
  52. package/src/lint/rules/no-redundant-value-cast.ts +1 -0
  53. package/src/lint/rules/no-string-ref.ts +1 -0
  54. package/src/lint/rules/no-unused-declarable-import.ts +1 -0
  55. package/src/lint/rules/no-unused-declarable.test.ts +8 -0
  56. package/src/lint/rules/no-unused-declarable.ts +4 -0
  57. package/src/lint/rules/single-concern-file.ts +1 -0
  58. package/src/lsp/lexicon-providers.ts +7 -0
  59. package/src/lsp/types.ts +1 -0
  60. package/src/resource-attributes.test.ts +79 -0
  61. package/src/resource-attributes.ts +42 -0
  62. package/src/runtime.ts +4 -3
@@ -88,6 +88,70 @@ describe("writeConstructor", () => {
88
88
  });
89
89
  });
90
90
 
91
+ describe("writeConstructor with resourceAttributesType", () => {
92
+ test("empty props with attributes type emits both params", () => {
93
+ const lines: string[] = [];
94
+ writeConstructor(lines, [], undefined, "CFResourceAttributes");
95
+ expect(lines.join("\n")).toContain(
96
+ "constructor(props: Record<string, unknown>, attributes?: CFResourceAttributes);",
97
+ );
98
+ });
99
+
100
+ test("props with attributes type emits second param after closing brace", () => {
101
+ const lines: string[] = [];
102
+ writeConstructor(
103
+ lines,
104
+ [{ name: "BucketName", type: "string", required: true }],
105
+ undefined,
106
+ "CFResourceAttributes",
107
+ );
108
+ const output = lines.join("\n");
109
+ expect(output).toContain("constructor(props: {");
110
+ expect(output).toContain("BucketName: string;");
111
+ expect(output).toContain("}, attributes?: CFResourceAttributes);");
112
+ });
113
+
114
+ test("without attributes type, closing brace has no second param", () => {
115
+ const lines: string[] = [];
116
+ writeConstructor(
117
+ lines,
118
+ [{ name: "Name", type: "string", required: true }],
119
+ undefined,
120
+ );
121
+ const output = lines.join("\n");
122
+ expect(output).toContain(" });");
123
+ expect(output).not.toContain("attributes?");
124
+ });
125
+ });
126
+
127
+ describe("writeResourceClass with resourceAttributesType", () => {
128
+ test("resource class constructor includes attributes param", () => {
129
+ const lines: string[] = [];
130
+ writeResourceClass(
131
+ lines,
132
+ "Bucket",
133
+ [{ name: "BucketName", type: "string", required: false }],
134
+ [{ name: "Arn", type: "string" }],
135
+ undefined,
136
+ "CFResourceAttributes",
137
+ );
138
+ const output = lines.join("\n");
139
+ expect(output).toContain("}, attributes?: CFResourceAttributes);");
140
+ expect(output).toContain("readonly Arn: string;");
141
+ });
142
+
143
+ test("property class does not get attributes param", () => {
144
+ const lines: string[] = [];
145
+ writePropertyClass(
146
+ lines,
147
+ "BucketConfig",
148
+ [{ name: "Enabled", type: "boolean", required: false }],
149
+ );
150
+ const output = lines.join("\n");
151
+ expect(output).not.toContain("attributes?");
152
+ });
153
+ });
154
+
91
155
  describe("writeEnumType", () => {
92
156
  test("writes single-line for short enum", () => {
93
157
  const lines: string[] = [];
@@ -42,10 +42,11 @@ export function writeResourceClass(
42
42
  properties: DtsProperty[],
43
43
  attributes: DtsAttribute[],
44
44
  remap?: Map<string, string>,
45
+ resourceAttributesType?: string,
45
46
  ): void {
46
47
  lines.push("");
47
48
  lines.push(`export declare class ${tsName} {`);
48
- writeConstructor(lines, properties, remap);
49
+ writeConstructor(lines, properties, remap, resourceAttributesType);
49
50
 
50
51
  // Attributes as readonly properties (sorted)
51
52
  const attrs = [...attributes].sort((a, b) => a.name.localeCompare(b.name));
@@ -79,9 +80,14 @@ export function writeConstructor(
79
80
  lines: string[],
80
81
  props: DtsProperty[],
81
82
  remap: Map<string, string> | undefined,
83
+ resourceAttributesType?: string,
82
84
  ): void {
83
85
  if (props.length === 0) {
84
- lines.push(" constructor(props: Record<string, unknown>);");
86
+ if (resourceAttributesType) {
87
+ lines.push(` constructor(props: Record<string, unknown>, attributes?: ${resourceAttributesType});`);
88
+ } else {
89
+ lines.push(" constructor(props: Record<string, unknown>);");
90
+ }
85
91
  return;
86
92
  }
87
93
 
@@ -100,7 +106,11 @@ export function writeConstructor(
100
106
  }
101
107
  lines.push(` ${p.name}${optional}: ${tsType};`);
102
108
  }
103
- lines.push(" });");
109
+ if (resourceAttributesType) {
110
+ lines.push(` }, attributes?: ${resourceAttributesType});`);
111
+ } else {
112
+ lines.push(" });");
113
+ }
104
114
  }
105
115
 
106
116
  /**
@@ -113,7 +113,7 @@ export async function packagePipeline(
113
113
  /**
114
114
  * Collect lint rule source files from a lexicon package.
115
115
  * Auto-discovers .ts files in the specified directories,
116
- * skipping test files, barrel files (index.ts), and non-.ts files.
116
+ * skipping test files, re-export files (index.ts), and non-.ts files.
117
117
  */
118
118
  export function collectRules(
119
119
  srcDir: string,
@@ -151,8 +151,8 @@ describe("expandComposite", () => {
151
151
 
152
152
  const expanded = expandComposite("storage", MyComp({}));
153
153
  expect(expanded.size).toBe(2);
154
- expect(expanded.get("storage_bucket")?.entityType).toBe("Bucket");
155
- expect(expanded.get("storage_role")?.entityType).toBe("Role");
154
+ expect(expanded.get("storageBucket")?.entityType).toBe("Bucket");
155
+ expect(expanded.get("storageRole")?.entityType).toBe("Role");
156
156
  });
157
157
 
158
158
  test("handles nested composites", () => {
@@ -167,8 +167,8 @@ describe("expandComposite", () => {
167
167
 
168
168
  const expanded = expandComposite("app", Outer({}));
169
169
  expect(expanded.size).toBe(2);
170
- expect(expanded.get("app_bucket")?.entityType).toBe("Bucket");
171
- expect(expanded.get("app_nested_table")?.entityType).toBe("Table");
170
+ expect(expanded.get("appBucket")?.entityType).toBe("Bucket");
171
+ expect(expanded.get("appNestedTable")?.entityType).toBe("Table");
172
172
  });
173
173
 
174
174
  test("preserves Declarable identity (same object reference)", () => {
@@ -176,7 +176,7 @@ describe("expandComposite", () => {
176
176
  const MyComp = Composite<{}>(() => ({ bucket }));
177
177
 
178
178
  const expanded = expandComposite("s", MyComp({}));
179
- expect(expanded.get("s_bucket")).toBe(bucket);
179
+ expect(expanded.get("sBucket")).toBe(bucket);
180
180
  });
181
181
 
182
182
  test("handles empty composite", () => {
@@ -230,6 +230,22 @@ describe("resource() helper", () => {
230
230
  const instance = resource(MockResource, {});
231
231
  expect(instance.arn).toBeInstanceOf(AttrRef);
232
232
  });
233
+
234
+ test("forwards attributes as second constructor argument", () => {
235
+ // MockResource doesn't store attributes, so use createResource which does
236
+ const { createResource } = require("./runtime");
237
+ const TestRes = createResource("Test::Resource", "test", { arn: "Arn" });
238
+ const attrs = { DependsOn: ["Other"], Condition: "IsProd" };
239
+ const instance = resource(TestRes as any, { name: "test" }, attrs);
240
+ expect((instance as any).attributes).toEqual(attrs);
241
+ });
242
+
243
+ test("without attributes, resource() creates instance with empty attributes", () => {
244
+ const { createResource } = require("./runtime");
245
+ const TestRes = createResource("Test::Resource", "test", { arn: "Arn" });
246
+ const instance = resource(TestRes as any, { name: "test" });
247
+ expect((instance as any).attributes).toEqual({});
248
+ });
233
249
  });
234
250
 
235
251
  function mockDeclarableWithProps(type: string, props: Record<string, unknown>): Declarable {
@@ -313,6 +329,57 @@ describe("withDefaults", () => {
313
329
  expect(CompositeRegistry.size).toBe(1);
314
330
  });
315
331
 
332
+ test("function-based defaults receive caller props", () => {
333
+ let receivedByFn: Partial<{ name: string; timeout: number }> | undefined;
334
+ const Base = Composite<{ name: string; timeout: number }>((props) => ({
335
+ item: mockDeclarable(props.name),
336
+ }));
337
+
338
+ const Wrapped = withDefaults(Base, (props) => {
339
+ receivedByFn = props;
340
+ return { timeout: 30 } as { timeout: number };
341
+ });
342
+ Wrapped({ name: "test" });
343
+ expect(receivedByFn).toEqual({ name: "test" });
344
+ });
345
+
346
+ test("user props override computed defaults", () => {
347
+ let received: { timeout: number } | undefined;
348
+ const Base = Composite<{ timeout: number }>((props) => {
349
+ received = props;
350
+ return { item: mockDeclarable() };
351
+ });
352
+
353
+ const Wrapped = withDefaults(Base, () => ({ timeout: 30 }) as { timeout: number });
354
+ Wrapped({ timeout: 60 });
355
+ expect(received!.timeout).toBe(60);
356
+ });
357
+
358
+ test("stacking: withDefaults(withDefaults(base, static), fn) works", () => {
359
+ let received: { a: number; b: number; c: number } | undefined;
360
+ const Base = Composite<{ a: number; b: number; c: number }>((props) => {
361
+ received = props;
362
+ return { item: mockDeclarable() };
363
+ });
364
+
365
+ const Step1 = withDefaults(Base, { a: 1 });
366
+ const Step2 = withDefaults(Step1, (props) => ({ b: (props.c ?? 0) + 10 }) as { b: number });
367
+ Step2({ c: 3 });
368
+ expect(received).toEqual({ a: 1, b: 13, c: 3 });
369
+ });
370
+
371
+ test("undefined computed values don't overwrite user props", () => {
372
+ let received: { name: string; timeout: number } | undefined;
373
+ const Base = Composite<{ name: string; timeout: number }>((props) => {
374
+ received = props;
375
+ return { item: mockDeclarable() };
376
+ });
377
+
378
+ const Wrapped = withDefaults(Base, () => ({ timeout: undefined }) as unknown as { timeout: number });
379
+ Wrapped({ name: "test", timeout: 42 });
380
+ expect(received!.timeout).toBe(42);
381
+ });
382
+
316
383
  test("expandComposite works identically on defaulted composites", () => {
317
384
  const Base = Composite<{ name: string; timeout: number }>((props) => ({
318
385
  fn: mockDeclarable(`Fn-${props.name}`),
@@ -323,8 +390,8 @@ describe("withDefaults", () => {
323
390
  const expanded = expandComposite("api", Wrapped({ name: "test" }));
324
391
 
325
392
  expect(expanded.size).toBe(2);
326
- expect(expanded.get("api_fn")?.entityType).toBe("Fn-test");
327
- expect(expanded.get("api_role")?.entityType).toBe("Role-test");
393
+ expect(expanded.get("apiFn")?.entityType).toBe("Fn-test");
394
+ expect(expanded.get("apiRole")?.entityType).toBe("Role-test");
328
395
  });
329
396
  });
330
397
 
@@ -342,8 +409,8 @@ describe("propagate", () => {
342
409
  const instance = propagate(MyComp({}), { env: "prod" });
343
410
  const expanded = expandComposite("s", instance);
344
411
 
345
- const bucketProps = (expanded.get("s_bucket") as any).props;
346
- const roleProps = (expanded.get("s_role") as any).props;
412
+ const bucketProps = (expanded.get("sBucket") as any).props;
413
+ const roleProps = (expanded.get("sRole") as any).props;
347
414
  expect(bucketProps.env).toBe("prod");
348
415
  expect(roleProps.env).toBe("prod");
349
416
  });
@@ -359,7 +426,7 @@ describe("propagate", () => {
359
426
  tags: [{ key: "env", value: "prod" }],
360
427
  });
361
428
  const expanded = expandComposite("s", instance);
362
- const tags = (expanded.get("s_bucket") as any).props.tags;
429
+ const tags = (expanded.get("sBucket") as any).props.tags;
363
430
 
364
431
  expect(tags).toEqual([
365
432
  { key: "env", value: "prod" },
@@ -374,7 +441,7 @@ describe("propagate", () => {
374
441
 
375
442
  const instance = propagate(MyComp({}), { region: "eu-west-1" });
376
443
  const expanded = expandComposite("s", instance);
377
- expect((expanded.get("s_bucket") as any).props.region).toBe("us-west-2");
444
+ expect((expanded.get("sBucket") as any).props.region).toBe("us-west-2");
378
445
  });
379
446
 
380
447
  test("undefined values in shared props are stripped", () => {
@@ -384,7 +451,7 @@ describe("propagate", () => {
384
451
 
385
452
  const instance = propagate(MyComp({}), { name: undefined, extra: "yes" });
386
453
  const expanded = expandComposite("s", instance);
387
- const props = (expanded.get("s_bucket") as any).props;
454
+ const props = (expanded.get("sBucket") as any).props;
388
455
  expect(props.name).toBe("data");
389
456
  expect(props.extra).toBe("yes");
390
457
  });
@@ -402,8 +469,8 @@ describe("propagate", () => {
402
469
  const instance = propagate(Outer({}), { env: "prod" });
403
470
  const expanded = expandComposite("app", instance);
404
471
 
405
- expect((expanded.get("app_bucket") as any).props.env).toBe("prod");
406
- expect((expanded.get("app_nested_table") as any).props.env).toBe("prod");
472
+ expect((expanded.get("appBucket") as any).props.env).toBe("prod");
473
+ expect((expanded.get("appNestedTable") as any).props.env).toBe("prod");
407
474
  });
408
475
 
409
476
  test("expanded declarables are same object references", () => {
@@ -412,7 +479,7 @@ describe("propagate", () => {
412
479
 
413
480
  const instance = propagate(MyComp({}), { env: "prod" });
414
481
  const expanded = expandComposite("s", instance);
415
- expect(expanded.get("s_bucket")).toBe(bucket);
482
+ expect(expanded.get("sBucket")).toBe(bucket);
416
483
  });
417
484
 
418
485
  test("composites without propagate work unchanged", () => {
@@ -421,6 +488,6 @@ describe("propagate", () => {
421
488
  }));
422
489
 
423
490
  const expanded = expandComposite("s", MyComp({}));
424
- expect((expanded.get("s_bucket") as any).props.name).toBe("data");
491
+ expect((expanded.get("sBucket") as any).props.name).toBe("data");
425
492
  });
426
493
  });
package/src/composite.ts CHANGED
@@ -123,7 +123,7 @@ export function expandComposite(
123
123
  const shared = (instance as any)[SHARED_PROPS] as Record<string, unknown> | undefined;
124
124
 
125
125
  for (const [memberName, member] of Object.entries(instance.members)) {
126
- const fullName = `${prefix}_${memberName}`;
126
+ const fullName = `${prefix}${memberName[0].toUpperCase()}${memberName.slice(1)}`;
127
127
 
128
128
  if (isCompositeInstance(member)) {
129
129
  const nested = expandComposite(fullName, member);
@@ -182,10 +182,11 @@ type Simplify<T> = { [K in keyof T]: T[K] };
182
182
  */
183
183
  export function withDefaults<P, M extends CompositeMembers, D extends Partial<P>>(
184
184
  definition: CompositeDefinition<P, M>,
185
- defaults: D,
185
+ defaults: D | ((props: Partial<P>) => D),
186
186
  ): CompositeDefinition<Simplify<PartialByDefault<P, D>>, M> {
187
187
  const wrapped = ((props: Simplify<PartialByDefault<P, D>>) => {
188
- return definition({ ...defaults, ...props } as P);
188
+ const resolved = typeof defaults === "function" ? defaults(props as Partial<P>) : defaults;
189
+ return definition({ ...resolved, ...props } as P);
189
190
  }) as CompositeDefinition<Simplify<PartialByDefault<P, D>>, M>;
190
191
 
191
192
  Object.defineProperty(wrapped, "compositeName", {
@@ -236,8 +237,9 @@ export function propagate<M extends CompositeMembers>(
236
237
  * Exists so lint tooling can validate composite member construction (EVL005).
237
238
  */
238
239
  export function resource<T extends Declarable, P>(
239
- Type: new (props: P) => T,
240
+ Type: new (props: P, attributes?: Record<string, unknown>) => T,
240
241
  props: P,
242
+ attributes?: Record<string, unknown>,
241
243
  ): T {
242
- return new Type(props);
244
+ return new Type(props, attributes);
243
245
  }
@@ -213,7 +213,7 @@ describe("detectLexicons", () => {
213
213
  });
214
214
 
215
215
  test("detects lexicon from export statement", async () => {
216
- const file = join(testDir, "barrel.ts");
216
+ const file = join(testDir, "reexport.ts");
217
217
  await writeFile(
218
218
  file,
219
219
  'export * from "@intentius/chant-lexicon-testdom";\nimport * as core from "@intentius/chant";'
@@ -224,7 +224,7 @@ describe("detectLexicons", () => {
224
224
  });
225
225
 
226
226
  test("detects lexicon from export with curly braces", async () => {
227
- const file = join(testDir, "barrel.ts");
227
+ const file = join(testDir, "reexport.ts");
228
228
  await writeFile(
229
229
  file,
230
230
  'export { Bucket, Interpolate } from "@intentius/chant-lexicon-testdom";'
@@ -262,8 +262,8 @@ describe("collectEntities with composites", () => {
262
262
  { file: "test.ts", exports: { myComp: instance } },
263
263
  ]);
264
264
 
265
- expect(entities.has("myComp_a")).toBe(true);
266
- expect(entities.has("myComp_b")).toBe(true);
265
+ expect(entities.has("myCompA")).toBe(true);
266
+ expect(entities.has("myCompB")).toBe(true);
267
267
  expect(entities.has("myComp")).toBe(false);
268
268
  });
269
269
  });
@@ -23,7 +23,7 @@ export function collectEntities(
23
23
  for (const [name, value] of Object.entries(exports)) {
24
24
  if (isDeclarable(value)) {
25
25
  if (entities.has(name)) {
26
- // Same object re-exported from multiple files (e.g. barrel re-exports) is fine
26
+ // Same object re-exported from multiple files (e.g. re-exports from multiple files) is fine
27
27
  if (entities.get(name) !== value) {
28
28
  throw new DiscoveryError(
29
29
  file,
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export * from "./codegen/validate";
52
52
  export * from "./codegen/docs";
53
53
  export * from "./runtime";
54
54
  export * from "./runtime-adapter";
55
+ export * from "./resource-attributes";
55
56
  export * from "./stack-output";
56
57
  export * from "./child-project";
57
58
  export * from "./lsp/types";
@@ -72,6 +72,14 @@ export const LexiconEntrySchema = z.object({
72
72
  createOnly: z.array(z.string()).optional(),
73
73
  writeOnly: z.array(z.string()).optional(),
74
74
  primaryIdentifier: z.array(z.string()).optional(),
75
+ deprecatedProperties: z.array(z.string()).optional(),
76
+ conditionalCreateOnly: z.array(z.string()).optional(),
77
+ replacementStrategy: z.enum(["delete_then_create", "create_then_delete"]).optional(),
78
+ tagging: z.object({
79
+ taggable: z.boolean(),
80
+ tagOnCreate: z.boolean(),
81
+ tagUpdatable: z.boolean(),
82
+ }).optional(),
75
83
  runtimeDeprecations: z.record(z.string(), z.string()).optional(),
76
84
  });
77
85
 
package/src/lexicon.ts CHANGED
@@ -97,6 +97,18 @@ export interface IntrinsicDef {
97
97
  readonly isTag?: boolean;
98
98
  }
99
99
 
100
+ /**
101
+ * Structured init template output from a lexicon plugin.
102
+ */
103
+ export interface InitTemplateSet {
104
+ /** Source files written to src/ */
105
+ src: Record<string, string>;
106
+ /** Application scaffold files written to project root */
107
+ root?: Record<string, string>;
108
+ /** Scripts merged into generated package.json */
109
+ scripts?: Record<string, string>;
110
+ }
111
+
100
112
  /**
101
113
  * Plugin interface for lexicon packages.
102
114
  *
@@ -156,7 +168,7 @@ export interface LexiconPlugin {
156
168
  skills?(): SkillDefinition[];
157
169
 
158
170
  /** Return source file templates for `chant init` project scaffolding */
159
- initTemplates?(): Record<string, string>;
171
+ initTemplates?(template?: string): InitTemplateSet;
160
172
 
161
173
  /** Optional initialization hook */
162
174
  init?(): void | Promise<void>;
@@ -39,6 +39,10 @@ export interface RuleSpec {
39
39
  severity: Severity;
40
40
  /** Category for grouping */
41
41
  category: Category;
42
+ /** Human-readable description of what this rule checks */
43
+ description?: string;
44
+ /** Link to rule documentation */
45
+ helpUri?: string;
42
46
  /** Selector name (or compound) to find target nodes */
43
47
  selector: string;
44
48
  /** Optional match condition to further filter nodes */
@@ -61,6 +65,8 @@ export function rule(spec: RuleSpec): LintRule {
61
65
  id: spec.id,
62
66
  severity: spec.severity,
63
67
  category: spec.category,
68
+ description: spec.description,
69
+ helpUri: spec.helpUri,
64
70
  check(context: LintContext): LintDiagnostic[] {
65
71
  const diagnostics: LintDiagnostic[] = [];
66
72
  const sf = context.sourceFile;