@intentius/chant 0.78.0 → 0.80.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.
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +38 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +14 -0
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/command-group.d.ts +6 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +27 -2
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/conflict-check.d.ts +6 -0
- package/dist/cli/conflict-check.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +17 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +17 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -0
- package/dist/cli/handlers/run-generate.d.ts +39 -0
- package/dist/cli/handlers/run-generate.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +9 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +18 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
- package/dist/components/presets/ecs-fargate.d.ts +10 -4
- package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
- package/dist/components/promote.d.ts +211 -0
- package/dist/components/promote.d.ts.map +1 -0
- package/dist/components/unbound-gate-approval.d.ts +24 -0
- package/dist/components/unbound-gate-approval.d.ts.map +1 -0
- package/dist/config.d.ts +82 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/convergence.d.ts +97 -0
- package/dist/discovery/convergence.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +41 -2
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +13 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +65 -0
- package/dist/lexicon-module.d.ts.map +1 -0
- package/dist/lexicon.d.ts +24 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +22 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +26 -0
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +25 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops015-gate-approval.d.ts +14 -0
- package/dist/lint/rules/op/ops015-gate-approval.d.ts.map +1 -0
- package/dist/op/activity-contract-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/builders.d.ts +4 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts +142 -0
- package/dist/op/gate-approval.d.ts.map +1 -0
- package/dist/op/gate.d.ts +54 -0
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +4 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/types.d.ts +7 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/workspace/record-source.d.ts +22 -0
- package/dist/workspace/record-source.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +57 -0
- package/dist/workspace/records-cli.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +118 -0
- package/dist/workspace/records.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/audit/core.ts +13 -2
- package/src/audit/discover.ts +90 -12
- package/src/audit/terraform-state.test.ts +86 -1
- package/src/audit/terraform-state.ts +26 -0
- package/src/cli/command-group.ts +7 -0
- package/src/cli/commands/audit-walk-warnings.test.ts +156 -0
- package/src/cli/commands/audit.ts +93 -19
- package/src/cli/commands/check-lexicon-examples.ts +2 -1
- package/src/cli/commands/doctor.ts +3 -0
- package/src/cli/commands/import-agents.ts +11 -4
- package/src/cli/commands/init.ts +57 -25
- package/src/cli/commands/lint.test.ts +58 -1
- package/src/cli/commands/lint.ts +48 -18
- package/src/cli/commands/onboard.ts +15 -0
- package/src/cli/commands/update.ts +5 -1
- package/src/cli/conflict-check.test.ts +18 -1
- package/src/cli/conflict-check.ts +13 -0
- package/src/cli/discovery-skip.test.ts +168 -0
- package/src/cli/handlers/build.test.ts +11 -0
- package/src/cli/handlers/build.ts +1 -1
- package/src/cli/handlers/dev.ts +3 -0
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +4 -2
- package/src/cli/handlers/lifecycle.ts +2 -1
- package/src/cli/handlers/misc.ts +16 -0
- package/src/cli/handlers/operator.test.ts +85 -0
- package/src/cli/handlers/operator.ts +118 -2
- package/src/cli/handlers/promote.test.ts +261 -0
- package/src/cli/handlers/promote.ts +325 -0
- package/src/cli/handlers/run-generate.test.ts +160 -0
- package/src/cli/handlers/run-generate.ts +133 -0
- package/src/cli/handlers/run.ts +13 -1
- package/src/cli/main.test.ts +33 -1
- package/src/cli/main.ts +78 -16
- package/src/cli/mcp/op-tools.ts +2 -1
- package/src/cli/path-lexicon-messages.test.ts +152 -0
- package/src/cli/plugins.ts +44 -6
- package/src/cli/registry.ts +18 -1
- package/src/components/__fixtures__/alb-ecs-service.json +0 -8
- package/src/components/capability-plugin-loader.ts +3 -1
- package/src/components/cli-support.ts +3 -2
- package/src/components/discover.ts +26 -2
- package/src/components/driver.test.ts +12 -11
- package/src/components/driver.ts +2 -0
- package/src/components/fan-out-support.ts +2 -1
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +7 -7
- package/src/components/presets/ecs-fargate.ts +10 -5
- package/src/components/promote.test.ts +371 -0
- package/src/components/promote.ts +537 -0
- package/src/components/unbound-gate-approval.test.ts +82 -0
- package/src/components/unbound-gate-approval.ts +51 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +128 -11
- package/src/discovery/convergence.test.ts +231 -0
- package/src/discovery/convergence.ts +399 -0
- package/src/discovery/files.test.ts +127 -1
- package/src/discovery/files.ts +97 -9
- package/src/discovery/fold-import.ts +83 -9
- package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
- package/src/graph-ir.ts +14 -0
- package/src/lexicon-module.test.ts +203 -0
- package/src/lexicon-module.ts +117 -0
- package/src/lexicon.ts +26 -0
- package/src/lifecycle/build-ledger-store.ts +11 -1
- package/src/lifecycle/gate-ledger.ts +22 -0
- package/src/lifecycle/legacy-digest.test.ts +114 -0
- package/src/lifecycle/legacy-digest.ts +62 -0
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops015-gate-approval.test.ts +67 -0
- package/src/lint/rules/op/ops015-gate-approval.ts +57 -0
- package/src/op/activity-contract-registry.ts +18 -4
- package/src/op/activity-registry.ts +26 -0
- package/src/op/builders.ts +10 -1
- package/src/op/discover-convergence.test.ts +70 -0
- package/src/op/discover.ts +4 -0
- package/src/op/gate-approval.test.ts +258 -0
- package/src/op/gate-approval.ts +260 -0
- package/src/op/gate.ts +148 -10
- package/src/op/generate-pipeline.ts +2 -1
- package/src/op/index.ts +9 -1
- package/src/op/local-executor.test.ts +66 -0
- package/src/op/local-executor.ts +44 -1
- package/src/op/op-ir.ts +4 -0
- package/src/op/runtimes/local.ts +2 -1
- package/src/op/types.ts +7 -0
- package/src/workspace/record-source.ts +117 -0
- package/src/workspace/records-cli.ts +126 -0
- package/src/workspace/records-contract.test.ts +88 -0
- package/src/workspace/records.schema.json +120 -0
- package/src/workspace/records.test.ts +243 -0
- package/src/workspace/records.ts +383 -0
package/src/config.test.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
environmentEndpoint,
|
|
13
13
|
matchesEnvironmentPattern,
|
|
14
14
|
matchesDeclaredEnvironment,
|
|
15
|
+
resolveDiscoveryGlobs,
|
|
15
16
|
} from "./config";
|
|
16
17
|
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
17
18
|
import { join } from "path";
|
|
@@ -448,3 +449,42 @@ describe("resolveKnowledgeDir (#1864, design #1059)", () => {
|
|
|
448
449
|
expect(resolveKnowledgeDir({ knowledge: {} }, "/proj")).toBe(join("/proj", "knowledge"));
|
|
449
450
|
});
|
|
450
451
|
});
|
|
452
|
+
|
|
453
|
+
describe("exclude / include discovery globs (#2519)", () => {
|
|
454
|
+
test("loads string arrays", async () => {
|
|
455
|
+
writeFileSync(join(TEST_DIR, "chant.config.json"), JSON.stringify({ exclude: ["ops/**"], include: ["ops/keep.ts"] }));
|
|
456
|
+
const { config } = await loadChantConfig(TEST_DIR);
|
|
457
|
+
expect(config.exclude).toEqual(["ops/**"]);
|
|
458
|
+
expect(config.include).toEqual(["ops/keep.ts"]);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test.each([
|
|
462
|
+
["a non-string entry", { exclude: ["ops/**", 42] }, /exclude\.1: .*expected string/],
|
|
463
|
+
["a bare string", { exclude: "ops/**" }, /exclude: .*expected array/],
|
|
464
|
+
["an empty pattern", { include: [""] }, /include\.0: /],
|
|
465
|
+
])("rejects %s loudly", async (_label, raw, message) => {
|
|
466
|
+
writeFileSync(join(TEST_DIR, "chant.config.json"), JSON.stringify(raw));
|
|
467
|
+
await expect(loadChantConfig(TEST_DIR)).rejects.toThrow(message);
|
|
468
|
+
await expect(resolveDiscoveryGlobs(TEST_DIR)).rejects.toThrow(message);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("resolveDiscoveryGlobs is undefined without exclude, so include alone changes nothing", async () => {
|
|
472
|
+
expect(await resolveDiscoveryGlobs(TEST_DIR)).toBeUndefined();
|
|
473
|
+
writeFileSync(join(TEST_DIR, "chant.config.json"), JSON.stringify({ include: ["ops/**"] }));
|
|
474
|
+
expect(await resolveDiscoveryGlobs(TEST_DIR)).toBeUndefined();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("resolveDiscoveryGlobs anchors the globs at the project config's directory, past a lint fragment", async () => {
|
|
478
|
+
const src = join(TEST_DIR, "src");
|
|
479
|
+
mkdirSync(src, { recursive: true });
|
|
480
|
+
writeFileSync(join(src, "chant.config.json"), JSON.stringify({ rules: { COR001: "off" } }));
|
|
481
|
+
writeFileSync(join(TEST_DIR, "chant.config.json"), JSON.stringify({ exclude: ["ops"] }));
|
|
482
|
+
expect(await resolveDiscoveryGlobs(src)).toEqual({ root: TEST_DIR, exclude: ["ops"], include: [] });
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("resolveDiscoveryGlobs leaves an unrelated config error to the command's own load", async () => {
|
|
486
|
+
writeFileSync(join(TEST_DIR, "chant.config.json"), JSON.stringify({ lexicons: [7], exclude: ["ops"] }));
|
|
487
|
+
await expect(loadChantConfig(TEST_DIR)).rejects.toThrow(/lexicons\.0/);
|
|
488
|
+
expect(await resolveDiscoveryGlobs(TEST_DIR)).toBeUndefined();
|
|
489
|
+
});
|
|
490
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { BuildParamsConfig } from "./build-params";
|
|
|
10
10
|
import type { BuildParamProvenance } from "./provenance";
|
|
11
11
|
import { findProjectConfig } from "./project-root";
|
|
12
12
|
import { evaluateProjectConfig } from "./config-sandbox";
|
|
13
|
+
import { lexiconNames, registerLexiconDeclarations, type LexiconDeclaration } from "./lexicon-module";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* One project-declared environment (chant #1166). Historically always a bare
|
|
@@ -125,8 +126,16 @@ const EnvironmentEntrySchema = z.union([
|
|
|
125
126
|
}),
|
|
126
127
|
]);
|
|
127
128
|
|
|
129
|
+
const LexiconEntrySchema = z.union([
|
|
130
|
+
z.string().min(1),
|
|
131
|
+
z.object({
|
|
132
|
+
name: z.string().min(1),
|
|
133
|
+
module: z.string().min(1),
|
|
134
|
+
}).strict(),
|
|
135
|
+
]);
|
|
136
|
+
|
|
128
137
|
export const ChantConfigSchema = z.object({
|
|
129
|
-
lexicons: z.array(
|
|
138
|
+
lexicons: z.array(LexiconEntrySchema).optional(),
|
|
130
139
|
capabilities: z.array(z.string().min(1)).optional(),
|
|
131
140
|
environments: z.array(EnvironmentEntrySchema).optional(),
|
|
132
141
|
sourceDir: z.string().min(1).optional(),
|
|
@@ -184,6 +193,8 @@ export const ChantConfigSchema = z.object({
|
|
|
184
193
|
knowledge: z.object({
|
|
185
194
|
dir: z.string().min(1).optional(),
|
|
186
195
|
}).optional(),
|
|
196
|
+
exclude: z.array(z.string().min(1)).optional(),
|
|
197
|
+
include: z.array(z.string().min(1)).optional(),
|
|
187
198
|
}).passthrough();
|
|
188
199
|
|
|
189
200
|
/**
|
|
@@ -192,8 +203,24 @@ export const ChantConfigSchema = z.object({
|
|
|
192
203
|
* Loaded from `chant.config.ts` (preferred) or `chant.config.json`.
|
|
193
204
|
*/
|
|
194
205
|
export interface ChantConfig {
|
|
195
|
-
/**
|
|
196
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Lexicons to load. A bare name (`"aws"`) loads the package
|
|
208
|
+
* `@intentius/chant-lexicon-aws`. `{ name, module }` (#2520) loads a
|
|
209
|
+
* project-local lexicon from a module path, resolved against the directory
|
|
210
|
+
* holding this config; see `./lexicon-module.ts`.
|
|
211
|
+
*
|
|
212
|
+
* Once loaded, this holds names only: the loader replaces each
|
|
213
|
+
* `{ name, module }` entry with its name and records the path in
|
|
214
|
+
* {@link ChantConfig.lexiconModules}.
|
|
215
|
+
*/
|
|
216
|
+
lexicons?: LexiconDeclaration[];
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Set by the config loader, never written by hand: each path-declared
|
|
220
|
+
* lexicon's name mapped to its absolute module path. Absent when every
|
|
221
|
+
* `lexicons` entry is a bare name.
|
|
222
|
+
*/
|
|
223
|
+
lexiconModules?: Record<string, string>;
|
|
197
224
|
|
|
198
225
|
/**
|
|
199
226
|
* Capability plugin package names to load in addition to the built-in
|
|
@@ -418,14 +445,43 @@ export interface ChantConfig {
|
|
|
418
445
|
/** Bundle directory, relative to the project root. Defaults to `"knowledge"`. */
|
|
419
446
|
dir?: string;
|
|
420
447
|
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Globs, relative to the directory holding this config, naming files that
|
|
451
|
+
* source discovery must skip (#2519): `build`, `lint`, `list`, `explain`
|
|
452
|
+
* and every other command that walks the project for declarations never
|
|
453
|
+
* import or lint a matching file. A pattern matches a file when it matches
|
|
454
|
+
* the file's path or the path of any directory above it, so `"ops"` and
|
|
455
|
+
* `"ops/**"` both skip everything under `ops/`. Matched with picomatch,
|
|
456
|
+
* dot files included. See `./discovery/files.ts`'s `compileDiscoveryFilter`.
|
|
457
|
+
*/
|
|
458
|
+
exclude?: string[];
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Globs that re-admit files {@link exclude} would skip. `include` always
|
|
462
|
+
* wins over `exclude`, and it never narrows discovery on its own: a file
|
|
463
|
+
* no `exclude` pattern matches is discovered whether or not it matches an
|
|
464
|
+
* `include`. It does not re-admit what discovery skips unconditionally
|
|
465
|
+
* (`node_modules`, test files, child projects, the generated and skip
|
|
466
|
+
* markers).
|
|
467
|
+
*/
|
|
468
|
+
include?: string[];
|
|
421
469
|
}
|
|
422
470
|
|
|
423
471
|
/**
|
|
424
472
|
* Resolved project configuration with metadata about how it was loaded.
|
|
425
473
|
*/
|
|
474
|
+
/**
|
|
475
|
+
* A configuration as the loader returns it. `lexicons` holds names only: each
|
|
476
|
+
* `{ name, module }` entry has been replaced by its name and its path moved
|
|
477
|
+
* to `lexiconModules` (chant #2520), so a reader of a loaded config never
|
|
478
|
+
* sees the object form.
|
|
479
|
+
*/
|
|
480
|
+
export type LoadedChantConfig = Omit<ChantConfig, "lexicons"> & { lexicons?: string[] };
|
|
481
|
+
|
|
426
482
|
export interface ResolvedConfig {
|
|
427
483
|
/** The loaded configuration */
|
|
428
|
-
config:
|
|
484
|
+
config: LoadedChantConfig;
|
|
429
485
|
|
|
430
486
|
/** Path to the config file that was loaded, or undefined if defaults */
|
|
431
487
|
configPath?: string;
|
|
@@ -434,7 +490,7 @@ export interface ResolvedConfig {
|
|
|
434
490
|
/**
|
|
435
491
|
* Default configuration when no config file exists.
|
|
436
492
|
*/
|
|
437
|
-
export const DEFAULT_CHANT_CONFIG:
|
|
493
|
+
export const DEFAULT_CHANT_CONFIG: LoadedChantConfig = {};
|
|
438
494
|
|
|
439
495
|
/**
|
|
440
496
|
* Load project configuration from a directory.
|
|
@@ -499,14 +555,53 @@ export async function loadChantConfig(dir: string): Promise<ResolvedConfig> {
|
|
|
499
555
|
* config declaring any project-level key still wins where it stands.
|
|
500
556
|
*/
|
|
501
557
|
export async function loadChantConfigUpward(startDir: string): Promise<ResolvedConfig> {
|
|
558
|
+
const { dir, configPath } = findProjectConfigPastFragments(startDir);
|
|
559
|
+
if (configPath) warnIfFragmentShadowsProjectConfig(configPath);
|
|
560
|
+
return loadChantConfig(dir);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** The walk {@link loadChantConfigUpward} makes, lint-only fragments skipped, without its warning. */
|
|
564
|
+
function findProjectConfigPastFragments(startDir: string): { dir: string; configPath?: string } {
|
|
502
565
|
let { dir, configPath } = findProjectConfig(startDir);
|
|
503
566
|
while (configPath && isLintOnlyFragment(configPath)) {
|
|
504
567
|
const parent = dirname(dir);
|
|
505
568
|
if (parent === dir) break;
|
|
506
569
|
({ dir, configPath } = findProjectConfig(parent));
|
|
507
570
|
}
|
|
508
|
-
|
|
509
|
-
|
|
571
|
+
return { dir, configPath };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** A project's discovery globs (#2519) and the directory they are relative to. */
|
|
575
|
+
export interface DiscoveryGlobs {
|
|
576
|
+
/** The directory holding the config that declared the globs. */
|
|
577
|
+
root: string;
|
|
578
|
+
exclude: string[];
|
|
579
|
+
include: string[];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* The `exclude`/`include` globs of the project `startDir` belongs to, found by
|
|
584
|
+
* the same upward walk as {@link loadChantConfigUpward}. `undefined` when the
|
|
585
|
+
* project declares no `exclude`, which leaves discovery exactly as it was
|
|
586
|
+
* before the keys existed: `include` only ever re-admits what `exclude` skips.
|
|
587
|
+
*
|
|
588
|
+
* A config that fails to load is left to the command's own config load to
|
|
589
|
+
* report, so discovery keeps working where it did before, unless the failure
|
|
590
|
+
* is in `exclude` or `include` themselves: ignoring those would discover the
|
|
591
|
+
* very files the project asked to skip.
|
|
592
|
+
*/
|
|
593
|
+
export async function resolveDiscoveryGlobs(startDir: string): Promise<DiscoveryGlobs | undefined> {
|
|
594
|
+
const { dir, configPath } = findProjectConfigPastFragments(startDir);
|
|
595
|
+
if (!configPath) return undefined;
|
|
596
|
+
let config: ChantConfig;
|
|
597
|
+
try {
|
|
598
|
+
({ config } = await loadChantConfig(dir));
|
|
599
|
+
} catch (err) {
|
|
600
|
+
if (err instanceof InvalidChantConfigError && (err.key === "exclude" || err.key === "include")) throw err;
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
if (!config.exclude || config.exclude.length === 0) return undefined;
|
|
604
|
+
return { root: dir, exclude: config.exclude, include: config.include ?? [] };
|
|
510
605
|
}
|
|
511
606
|
|
|
512
607
|
/**
|
|
@@ -531,7 +626,7 @@ const LINT_FRAGMENT_KEYS = new Set(["$schema", "extends", "rules", "overrides",
|
|
|
531
626
|
*/
|
|
532
627
|
const isCommentKey = (k: string): boolean => k.startsWith("_");
|
|
533
628
|
|
|
534
|
-
function isLintOnlyFragment(configPath: string): boolean {
|
|
629
|
+
export function isLintOnlyFragment(configPath: string): boolean {
|
|
535
630
|
if (!configPath.endsWith("chant.config.json")) return false;
|
|
536
631
|
try {
|
|
537
632
|
const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
|
|
@@ -813,10 +908,20 @@ export function resolveKnowledgeDir(config: ChantConfig, projectPath: string): s
|
|
|
813
908
|
return join(projectPath, config.knowledge?.dir ?? "knowledge");
|
|
814
909
|
}
|
|
815
910
|
|
|
911
|
+
/**
|
|
912
|
+
* A config that failed schema validation. `key` is the top-level key of the
|
|
913
|
+
* first problem. `name` stays `"Error"`, so the printed error reads as before.
|
|
914
|
+
*/
|
|
915
|
+
export class InvalidChantConfigError extends Error {
|
|
916
|
+
constructor(message: string, readonly key?: string) {
|
|
917
|
+
super(message);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
816
921
|
/**
|
|
817
922
|
* Validate and normalize a raw config object into ChantConfig shape.
|
|
818
923
|
*/
|
|
819
|
-
function normalizeConfig(raw: Record<string, unknown>, source?: string):
|
|
924
|
+
function normalizeConfig(raw: Record<string, unknown>, source?: string): LoadedChantConfig {
|
|
820
925
|
if (typeof raw !== "object" || raw === null) {
|
|
821
926
|
return DEFAULT_CHANT_CONFIG;
|
|
822
927
|
}
|
|
@@ -826,8 +931,20 @@ function normalizeConfig(raw: Record<string, unknown>, source?: string): ChantCo
|
|
|
826
931
|
const issue = result.error.issues[0];
|
|
827
932
|
const path = issue.path.length > 0 ? issue.path.join(".") : undefined;
|
|
828
933
|
const loc = source ? ` in ${source}` : "";
|
|
829
|
-
throw new
|
|
934
|
+
throw new InvalidChantConfigError(
|
|
935
|
+
`Invalid chant config${loc}: ${path ? `${path}: ` : ""}${issue.message}`,
|
|
936
|
+
issue.path.length > 0 ? String(issue.path[0]) : undefined,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// chant #2520 — a lexicon declared by module path. The path is recorded for
|
|
941
|
+
// the loaders and the entry becomes its name, so every reader of `lexicons`
|
|
942
|
+
// keeps seeing names. A config of plain names is returned untouched.
|
|
943
|
+
const lexicons = (raw as ChantConfig).lexicons;
|
|
944
|
+
const recorded = registerLexiconDeclarations(lexicons, source !== undefined ? dirname(source) : process.cwd());
|
|
945
|
+
if (Object.keys(recorded).length > 0) {
|
|
946
|
+
return { ...(raw as ChantConfig), lexicons: lexiconNames(lexicons), lexiconModules: recorded };
|
|
830
947
|
}
|
|
831
948
|
|
|
832
|
-
return raw as
|
|
949
|
+
return raw as LoadedChantConfig;
|
|
833
950
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { findInfraFiles } from "./files";
|
|
7
|
+
import { resetDiscoveryWarnings, warnDiscoveryChanges } from "./convergence";
|
|
8
|
+
import { discoverComponents } from "../components/discover";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* chant#2527's warning release. Each category of file whose discovery status
|
|
12
|
+
* changes when the walkers converge gets a warning naming the file, the
|
|
13
|
+
* change and the glob that keeps today's behaviour, while the walker's
|
|
14
|
+
* returned list stays exactly what it is today. Lint's category lives in
|
|
15
|
+
* `../cli/commands/lint.test.ts`, the Op category in
|
|
16
|
+
* `../op/discover-convergence.test.ts`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
let dir: string;
|
|
20
|
+
let stderr: string[];
|
|
21
|
+
|
|
22
|
+
function write(rel: string, body = "export const x = 1;\n"): void {
|
|
23
|
+
mkdirSync(dirname(join(dir, rel)), { recursive: true });
|
|
24
|
+
writeFileSync(join(dir, rel), body);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A project config (it declares a project-level key, so it is no lint-only fragment). */
|
|
28
|
+
function project(rel = ".", extra: Record<string, unknown> = {}): void {
|
|
29
|
+
write(join(rel, "chant.config.json"), JSON.stringify({ lexicons: ["aws"], ...extra }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rel = (files: string[]): string[] => files.map((f) => f.slice(dir.length + 1)).sort();
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-2527-")));
|
|
36
|
+
resetDiscoveryWarnings();
|
|
37
|
+
stderr = [];
|
|
38
|
+
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => void stderr.push(args.join(" ")));
|
|
39
|
+
vi.spyOn(process, "cwd").mockReturnValue(dir);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("source discovery warns about files the converged walker reads differently (#2527)", () => {
|
|
48
|
+
test("a .ts file in a dot-directory", async () => {
|
|
49
|
+
project();
|
|
50
|
+
write("src/app.ts");
|
|
51
|
+
write(".cache/gen.ts");
|
|
52
|
+
|
|
53
|
+
expect(rel(await findInfraFiles(dir))).toEqual([".cache/gen.ts", "src/app.ts"]);
|
|
54
|
+
expect(stderr).toEqual([
|
|
55
|
+
"warning: Source discovery (build, graph, list, explain) under the current directory changes in the next release (chant#2527):\n" +
|
|
56
|
+
" .cache/gen.ts: .cache/ is a dot-directory. Every walker skips dot-directories from the next release. " +
|
|
57
|
+
'To keep reading it after the change, add ".cache" to include in chant.config.json, which the next release honours.',
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a git-ignored .ts file, ignored by a nested .gitignore", async () => {
|
|
62
|
+
execFileSync("git", ["init", "-q"], { cwd: dir });
|
|
63
|
+
project();
|
|
64
|
+
write("src/app.ts");
|
|
65
|
+
write("src/vendored.ts");
|
|
66
|
+
write("src/.gitignore", "vendored.ts\n");
|
|
67
|
+
|
|
68
|
+
expect(rel(await findInfraFiles(dir))).toEqual(["src/app.ts", "src/vendored.ts"]);
|
|
69
|
+
expect(stderr).toHaveLength(1);
|
|
70
|
+
expect(stderr[0]).toContain(
|
|
71
|
+
" src/vendored.ts is git-ignored. Every walker skips git-ignored files from the next release. " +
|
|
72
|
+
'To keep reading it after the change, add "src/vendored.ts" to include in chant.config.json, which the next release honours.',
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("an ignored directory is named once, by its outermost ignored path", async () => {
|
|
77
|
+
execFileSync("git", ["init", "-q"], { cwd: dir });
|
|
78
|
+
project();
|
|
79
|
+
write("src/app.ts");
|
|
80
|
+
write(".gitignore", "vendor/\n");
|
|
81
|
+
for (const name of ["a", "b", "c", "d"]) write(`vendor/lib/${name}.ts`);
|
|
82
|
+
|
|
83
|
+
expect(rel(await findInfraFiles(dir))).toHaveLength(5);
|
|
84
|
+
expect(stderr[0]).toContain(" 4 files under vendor/: vendor/ is git-ignored.");
|
|
85
|
+
expect(stderr[0]).toContain('add "vendor" to include in chant.config.json, which the next release honours.');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a .ts file under dist", async () => {
|
|
89
|
+
project();
|
|
90
|
+
write("src/app.ts");
|
|
91
|
+
write("dist/out.ts");
|
|
92
|
+
|
|
93
|
+
expect(rel(await findInfraFiles(dir))).toEqual(["dist/out.ts", "src/app.ts"]);
|
|
94
|
+
expect(stderr[0]).toContain(
|
|
95
|
+
" dist/out.ts: dist/ is a dist directory. Every walker skips dist from the next release. " +
|
|
96
|
+
'To keep reading it after the change, add "dist" to include in chant.config.json, which the next release honours.',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a nested child project whose chant.config.json declares a project", async () => {
|
|
101
|
+
project();
|
|
102
|
+
write("src/app.ts");
|
|
103
|
+
project("stacks/east");
|
|
104
|
+
write("stacks/east/east.ts");
|
|
105
|
+
// A lint-only fragment is not a boundary: no warning for this one.
|
|
106
|
+
write("stacks/west/chant.config.json", JSON.stringify({ rules: { COR004: "off" } }));
|
|
107
|
+
write("stacks/west/west.ts");
|
|
108
|
+
|
|
109
|
+
expect(rel(await findInfraFiles(dir))).toEqual(["src/app.ts", "stacks/east/east.ts", "stacks/west/west.ts"]);
|
|
110
|
+
expect(stderr).toHaveLength(1);
|
|
111
|
+
expect(stderr[0]).toContain(
|
|
112
|
+
" stacks/east/east.ts: stacks/east/ is a child project with its own chant.config. " +
|
|
113
|
+
"Discovery stops at child projects from the next release. " +
|
|
114
|
+
'To keep reading it after the change, add "stacks/east" to include in chant.config.json, which the next release honours.',
|
|
115
|
+
);
|
|
116
|
+
expect(stderr[0]).not.toContain("west");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("the first child project met is no longer read as the project's own source (the quirk)", async () => {
|
|
120
|
+
project();
|
|
121
|
+
write("app/chant.config.ts", "export default {};\n");
|
|
122
|
+
write("app/main.ts");
|
|
123
|
+
|
|
124
|
+
expect(rel(await findInfraFiles(dir))).toEqual(["app/chant.config.ts", "app/main.ts"]);
|
|
125
|
+
expect(stderr[0]).toContain(
|
|
126
|
+
" app/chant.config.ts, app/main.ts: app/ is a child project with its own chant.config, " +
|
|
127
|
+
"read today as this project's own source because the walk met it first. " +
|
|
128
|
+
"From the next release discovery stops there, as at any child project. " +
|
|
129
|
+
'To keep reading them after the change, add "app" to include in chant.config.json, which the next release honours.',
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("the glob in the config is the fix: with it, nothing is printed and the list is the same", async () => {
|
|
134
|
+
project(".", { include: [".cache", "dist"] });
|
|
135
|
+
write("src/app.ts");
|
|
136
|
+
write(".cache/gen.ts");
|
|
137
|
+
write("dist/out.ts");
|
|
138
|
+
|
|
139
|
+
expect(rel(await findInfraFiles(dir))).toEqual([".cache/gen.ts", "dist/out.ts", "src/app.ts"]);
|
|
140
|
+
expect(stderr).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a clean project prints nothing", async () => {
|
|
144
|
+
execFileSync("git", ["init", "-q"], { cwd: dir });
|
|
145
|
+
project();
|
|
146
|
+
write(".gitignore", "node_modules/\n.chant/types/\n");
|
|
147
|
+
write("src/app.ts");
|
|
148
|
+
write("src/lib/util.ts");
|
|
149
|
+
write("node_modules/dep/index.ts");
|
|
150
|
+
// chant init's generated type stubs: read today, never worth a warning.
|
|
151
|
+
write(".chant/types/core/index.d.ts");
|
|
152
|
+
|
|
153
|
+
expect(rel(await findInfraFiles(dir))).toEqual([".chant/types/core/index.d.ts", "src/app.ts", "src/lib/util.ts"]);
|
|
154
|
+
expect(stderr).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("a project-local lexicon named by module path (#2520) prints nothing", async () => {
|
|
158
|
+
execFileSync("git", ["init", "-q"], { cwd: dir });
|
|
159
|
+
write("chant.config.json", JSON.stringify({ lexicons: [{ name: "local", module: "./lexicon/index.ts" }] }));
|
|
160
|
+
write("lexicon/index.ts");
|
|
161
|
+
write("lexicon/rules/r1.ts");
|
|
162
|
+
write("src/app.ts");
|
|
163
|
+
|
|
164
|
+
expect(rel(await findInfraFiles(dir))).toEqual(["lexicon/index.ts", "lexicon/rules/r1.ts", "src/app.ts"]);
|
|
165
|
+
expect(stderr).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("a project-local lexicon's config still yields the glob when something else warns", async () => {
|
|
169
|
+
write("chant.config.json", JSON.stringify({ lexicons: [{ name: "local", module: "./lexicon/index.ts" }], include: [".cache"] }));
|
|
170
|
+
write("lexicon/index.ts");
|
|
171
|
+
write(".cache/gen.ts");
|
|
172
|
+
write("dist/out.ts");
|
|
173
|
+
|
|
174
|
+
expect(rel(await findInfraFiles(dir))).toEqual([".cache/gen.ts", "dist/out.ts", "lexicon/index.ts"]);
|
|
175
|
+
expect(stderr).toHaveLength(1);
|
|
176
|
+
expect(stderr[0]).not.toContain(".cache");
|
|
177
|
+
expect(stderr[0]).toContain('add "dist" to include in chant.config.json');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("a warning prints once per process, however often the tree is walked", async () => {
|
|
181
|
+
project();
|
|
182
|
+
write(".cache/gen.ts");
|
|
183
|
+
await findInfraFiles(dir);
|
|
184
|
+
await findInfraFiles(dir);
|
|
185
|
+
expect(stderr).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("component discovery outside a project reads every child project next release (#2527)", () => {
|
|
190
|
+
test("the second child project's component is named, with the exclude glob", async () => {
|
|
191
|
+
// No config and no .git/package.json at or above `dir`: outside a project.
|
|
192
|
+
write("one/chant.config.ts", "export default {};\n");
|
|
193
|
+
write("one/one.component.ts");
|
|
194
|
+
write("two/chant.config.ts", "export default {};\n");
|
|
195
|
+
write("two/two.component.ts");
|
|
196
|
+
|
|
197
|
+
const { sourceFiles } = await discoverComponents(dir);
|
|
198
|
+
// Today: whichever child the walk met first, and only that one.
|
|
199
|
+
expect(sourceFiles).toHaveLength(1);
|
|
200
|
+
const [read] = rel(sourceFiles);
|
|
201
|
+
const skipped = read.startsWith("one/") ? "two" : "one";
|
|
202
|
+
|
|
203
|
+
expect(stderr).toHaveLength(1);
|
|
204
|
+
expect(stderr[0]).toContain(
|
|
205
|
+
`warning: Component discovery under the current directory changes in the next release (chant#2527):\n` +
|
|
206
|
+
` ${skipped}/${skipped}.component.ts: ${skipped}/ is a child project, skipped today ` +
|
|
207
|
+
"because the walk met another child project first. From the next release every child project is read, " +
|
|
208
|
+
"since the current directory is not inside a chant project. " +
|
|
209
|
+
`To keep skipping it, add "${skipped}" to exclude in chant.config.ts (a new file).`,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("the audit walk (#2527)", () => {
|
|
215
|
+
test("warns about a git-ignored candidate and leaves Terraform state to TF023", async () => {
|
|
216
|
+
execFileSync("git", ["init", "-q"], { cwd: dir });
|
|
217
|
+
write(".gitignore", ".env\n");
|
|
218
|
+
write(".env", "TOKEN=x\n");
|
|
219
|
+
write("k8s/deploy.yaml", "kind: Deployment\n");
|
|
220
|
+
|
|
221
|
+
const lines = await warnDiscoveryChanges({
|
|
222
|
+
walker: "audit",
|
|
223
|
+
root: dir,
|
|
224
|
+
files: [join(dir, ".env"), join(dir, "k8s/deploy.yaml"), join(dir, ".github/workflows/ci.yml")],
|
|
225
|
+
});
|
|
226
|
+
expect(lines).toEqual([
|
|
227
|
+
" .env is git-ignored. Every walker skips git-ignored files from the next release. " +
|
|
228
|
+
'To keep reading it after the change, add ".env" to include in chant.config.ts (a new file), which the next release honours.',
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
});
|