@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.
- package/bin/chant +20 -0
- package/package.json +18 -17
- package/src/bench.test.ts +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
- package/src/cli/commands/build.ts +1 -2
- package/src/cli/commands/doctor.ts +8 -3
- package/src/cli/commands/import.ts +2 -2
- package/src/cli/commands/init-lexicon.test.ts +0 -3
- package/src/cli/commands/init-lexicon.ts +1 -79
- package/src/cli/commands/init.test.ts +44 -4
- package/src/cli/commands/init.ts +69 -26
- package/src/cli/commands/lint.ts +27 -13
- package/src/cli/commands/list.ts +2 -2
- package/src/cli/commands/update.ts +5 -3
- package/src/cli/conflict-check.test.ts +0 -1
- package/src/cli/handlers/dev.ts +1 -9
- package/src/cli/handlers/init.ts +1 -0
- package/src/cli/lsp/server.ts +1 -1
- package/src/cli/main.ts +17 -3
- package/src/cli/mcp/server.test.ts +233 -4
- package/src/cli/mcp/server.ts +6 -0
- package/src/cli/mcp/tools/explain.ts +134 -0
- package/src/cli/mcp/tools/scaffold.ts +125 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- package/src/cli/registry.ts +1 -0
- package/src/cli/reporters/stylish.test.ts +212 -1
- package/src/cli/reporters/stylish.ts +133 -36
- package/src/codegen/docs-rules.test.ts +112 -0
- package/src/codegen/docs-rules.ts +129 -0
- package/src/codegen/docs.ts +3 -1
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +2 -3
- package/src/codegen/generate-typescript.test.ts +70 -6
- package/src/codegen/generate-typescript.ts +15 -9
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/package.ts +1 -1
- package/src/codegen/typecheck.ts +6 -11
- package/src/composite.test.ts +83 -16
- package/src/composite.ts +7 -5
- package/src/config.ts +4 -0
- package/src/detectLexicon.test.ts +2 -2
- package/src/discovery/collect.test.ts +2 -2
- package/src/discovery/collect.ts +1 -1
- package/src/index.ts +2 -1
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon-schema.ts +8 -0
- package/src/lexicon.ts +15 -7
- package/src/lint/config.ts +8 -6
- package/src/lint/declarative.ts +6 -0
- package/src/lint/engine.test.ts +287 -11
- package/src/lint/engine.ts +101 -23
- package/src/lint/rule-registry.test.ts +112 -0
- package/src/lint/rule-registry.ts +118 -0
- package/src/lint/rule.ts +8 -0
- package/src/lint/rules/cor017-composite-name-match.ts +2 -1
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
- package/src/lint/rules/declarable-naming-convention.ts +1 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
- package/src/lint/rules/evl004-spread-non-const.ts +1 -0
- package/src/lint/rules/evl005-resource-block-body.ts +1 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
- package/src/lint/rules/export-required.ts +1 -0
- package/src/lint/rules/file-declarable-limit.ts +1 -0
- package/src/lint/rules/flat-declarations.test.ts +8 -7
- package/src/lint/rules/flat-declarations.ts +2 -3
- package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
- package/src/lint/rules/no-redundant-type-import.ts +1 -0
- package/src/lint/rules/no-redundant-value-cast.ts +1 -0
- package/src/lint/rules/no-string-ref.ts +1 -0
- package/src/lint/rules/no-unused-declarable-import.ts +1 -0
- package/src/lint/rules/no-unused-declarable.test.ts +8 -0
- package/src/lint/rules/no-unused-declarable.ts +4 -0
- package/src/lint/rules/single-concern-file.ts +1 -0
- package/src/lsp/lexicon-providers.ts +7 -0
- package/src/lsp/types.ts +1 -0
- package/src/resource-attributes.test.ts +79 -0
- package/src/resource-attributes.ts +42 -0
- package/src/runtime-adapter.ts +158 -0
- package/src/runtime.ts +4 -3
- package/src/serializer-walker.test.ts +0 -9
- package/src/serializer-walker.ts +1 -3
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
- package/src/codegen/case.test.ts +0 -30
- package/src/codegen/case.ts +0 -11
- package/src/codegen/rollback.test.ts +0 -92
- package/src/codegen/rollback.ts +0 -115
package/src/composite.test.ts
CHANGED
|
@@ -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("
|
|
155
|
-
expect(expanded.get("
|
|
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("
|
|
171
|
-
expect(expanded.get("
|
|
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("
|
|
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("
|
|
327
|
-
expect(expanded.get("
|
|
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("
|
|
346
|
-
const roleProps = (expanded.get("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
406
|
-
expect((expanded.get("
|
|
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("
|
|
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("
|
|
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}
|
|
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
|
-
|
|
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
|
}
|
package/src/config.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { LintConfig } from "./lint/config";
|
|
|
7
7
|
* Zod schema for ChantConfig validation.
|
|
8
8
|
*/
|
|
9
9
|
export const ChantConfigSchema = z.object({
|
|
10
|
+
runtime: z.enum(["bun", "node"]).optional(),
|
|
10
11
|
lexicons: z.array(z.string().min(1)).optional(),
|
|
11
12
|
lint: z.record(z.string(), z.unknown()).optional(),
|
|
12
13
|
}).passthrough();
|
|
@@ -17,6 +18,9 @@ export const ChantConfigSchema = z.object({
|
|
|
17
18
|
* Loaded from `chant.config.ts` (preferred) or `chant.config.json`.
|
|
18
19
|
*/
|
|
19
20
|
export interface ChantConfig {
|
|
21
|
+
/** JS runtime to use for spawned commands: "bun" (default) or "node" */
|
|
22
|
+
runtime?: "bun" | "node";
|
|
23
|
+
|
|
20
24
|
/** Lexicon package names to load (e.g. ["aws"]) */
|
|
21
25
|
lexicons?: string[];
|
|
22
26
|
|
|
@@ -213,7 +213,7 @@ describe("detectLexicons", () => {
|
|
|
213
213
|
});
|
|
214
214
|
|
|
215
215
|
test("detects lexicon from export statement", async () => {
|
|
216
|
-
const file = join(testDir, "
|
|
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, "
|
|
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("
|
|
266
|
-
expect(entities.has("
|
|
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
|
});
|
package/src/discovery/collect.ts
CHANGED
|
@@ -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.
|
|
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
|
@@ -47,11 +47,12 @@ export * from "./codegen/fetch";
|
|
|
47
47
|
export * from "./codegen/generate";
|
|
48
48
|
export * from "./codegen/package";
|
|
49
49
|
export * from "./codegen/typecheck";
|
|
50
|
-
export * from "./codegen/rollback";
|
|
51
50
|
export * from "./codegen/coverage";
|
|
52
51
|
export * from "./codegen/validate";
|
|
53
52
|
export * from "./codegen/docs";
|
|
54
53
|
export * from "./runtime";
|
|
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";
|
package/src/lexicon-integrity.ts
CHANGED
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
* Content hashing and integrity verification for lexicon artifacts.
|
|
3
3
|
*/
|
|
4
4
|
import type { BundleSpec } from "./lexicon";
|
|
5
|
+
import { getRuntime } from "./runtime-adapter";
|
|
5
6
|
|
|
6
7
|
export interface ArtifactIntegrity {
|
|
7
|
-
algorithm:
|
|
8
|
+
algorithm: string;
|
|
8
9
|
artifacts: Record<string, string>;
|
|
9
10
|
composite: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Hash a single artifact's content using
|
|
14
|
+
* Hash a single artifact's content using the runtime's hash algorithm.
|
|
14
15
|
*/
|
|
15
16
|
export function hashArtifact(content: string): string {
|
|
16
|
-
return
|
|
17
|
+
return getRuntime().hash(content);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -39,7 +40,7 @@ export function computeIntegrity(spec: BundleSpec): ArtifactIntegrity {
|
|
|
39
40
|
const compositeInput = sorted.map(([k, v]) => `${k}:${v}`).join("\n");
|
|
40
41
|
const composite = hashArtifact(compositeInput);
|
|
41
42
|
|
|
42
|
-
return { algorithm:
|
|
43
|
+
return { algorithm: getRuntime().hashAlgorithm, artifacts, composite };
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
package/src/lexicon-schema.ts
CHANGED
|
@@ -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,11 +97,23 @@ 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
|
*
|
|
103
115
|
* Required lifecycle methods enforce consistency: every lexicon must support
|
|
104
|
-
* generate, validate, coverage,
|
|
116
|
+
* generate, validate, coverage, and package operations.
|
|
105
117
|
*/
|
|
106
118
|
export interface LexiconPlugin {
|
|
107
119
|
// ── Required ──────────────────────────────────────────────
|
|
@@ -123,9 +135,6 @@ export interface LexiconPlugin {
|
|
|
123
135
|
/** Package lexicon into distributable tarball */
|
|
124
136
|
package(options?: { verbose?: boolean; force?: boolean }): Promise<void>;
|
|
125
137
|
|
|
126
|
-
/** List or restore generation snapshots */
|
|
127
|
-
rollback(options?: { restore?: string; verbose?: boolean }): Promise<void>;
|
|
128
|
-
|
|
129
138
|
// ── Optional extensions ───────────────────────────────────
|
|
130
139
|
/** Return lint rules provided by this lexicon */
|
|
131
140
|
lintRules?(): LintRule[];
|
|
@@ -159,7 +168,7 @@ export interface LexiconPlugin {
|
|
|
159
168
|
skills?(): SkillDefinition[];
|
|
160
169
|
|
|
161
170
|
/** Return source file templates for `chant init` project scaffolding */
|
|
162
|
-
initTemplates?():
|
|
171
|
+
initTemplates?(template?: string): InitTemplateSet;
|
|
163
172
|
|
|
164
173
|
/** Optional initialization hook */
|
|
165
174
|
init?(): void | Promise<void>;
|
|
@@ -206,7 +215,6 @@ export function isLexiconPlugin(value: unknown): value is LexiconPlugin {
|
|
|
206
215
|
typeof obj.generate === "function" &&
|
|
207
216
|
typeof obj.validate === "function" &&
|
|
208
217
|
typeof obj.coverage === "function" &&
|
|
209
|
-
typeof obj.package === "function"
|
|
210
|
-
typeof obj.rollback === "function"
|
|
218
|
+
typeof obj.package === "function"
|
|
211
219
|
);
|
|
212
220
|
}
|
package/src/lint/config.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "fs";
|
|
2
2
|
import { join, dirname, resolve } from "path";
|
|
3
|
+
import { createRequire } from "module";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import type { Severity, RuleConfig } from "./rule";
|
|
6
|
+
import { moduleDir, getRuntime } from "../runtime-adapter";
|
|
5
7
|
import strictPreset from "./presets/strict.json";
|
|
6
8
|
|
|
7
9
|
/** Mapping of built-in preset names to their file paths */
|
|
8
10
|
const BUILTIN_PRESETS: Record<string, string> = {
|
|
9
|
-
"@intentius/chant/lint/presets/strict": resolve(import.meta.
|
|
10
|
-
"@intentius/chant/lint/presets/relaxed": resolve(import.meta.
|
|
11
|
+
"@intentius/chant/lint/presets/strict": resolve(moduleDir(import.meta.url), "presets/strict.json"),
|
|
12
|
+
"@intentius/chant/lint/presets/relaxed": resolve(moduleDir(import.meta.url), "presets/relaxed.json"),
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
// ── Zod schemas for lint config validation ─────────────────────────
|
|
@@ -307,11 +309,12 @@ function loadConfigFile(configPath: string, visited: Set<string> = new Set()): L
|
|
|
307
309
|
* @returns Loaded and merged configuration, or default config if not found
|
|
308
310
|
*/
|
|
309
311
|
export function loadConfig(dir: string): LintConfig {
|
|
310
|
-
// Try chant.config.ts first — Bun
|
|
312
|
+
// Try chant.config.ts first — Bun has native require() for .ts, Node uses tsx's loader
|
|
311
313
|
const tsConfigPath = join(dir, "chant.config.ts");
|
|
312
314
|
if (existsSync(tsConfigPath)) {
|
|
313
315
|
try {
|
|
314
|
-
const
|
|
316
|
+
const _require = createRequire(join(dir, "package.json"));
|
|
317
|
+
const mod = _require(tsConfigPath);
|
|
315
318
|
const config = mod.default ?? mod.config ?? mod;
|
|
316
319
|
if (typeof config === "object" && config !== null) {
|
|
317
320
|
// ChantConfig format: extract lint property
|
|
@@ -362,8 +365,7 @@ export function resolveRulesForFile(config: LintConfig, filePath: string): Recor
|
|
|
362
365
|
|
|
363
366
|
for (const override of config.overrides) {
|
|
364
367
|
const matches = override.files.some((pattern) => {
|
|
365
|
-
|
|
366
|
-
return glob.match(filePath);
|
|
368
|
+
return getRuntime().globMatch(pattern, filePath);
|
|
367
369
|
});
|
|
368
370
|
|
|
369
371
|
if (matches) {
|
package/src/lint/declarative.ts
CHANGED
|
@@ -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;
|