@intentius/chant 0.85.0 → 0.86.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/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +11 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +13 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts +23 -0
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +1 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +4 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +14 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts +211 -0
- package/dist/workspace/conformance/index.d.ts.map +1 -0
- package/dist/workspace/conformance/vitest.d.ts +11 -0
- package/dist/workspace/conformance/vitest.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +28 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +40 -0
- package/dist/workspace/declared-kinds.d.ts +43 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +11 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +2 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +45 -8
- package/dist/workspace/intent-joins.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +66 -7
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +31 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +40 -4
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +51 -0
- package/dist/workspace/record-sessions.d.ts.map +1 -0
- package/dist/workspace/record-source.d.ts +2 -0
- package/dist/workspace/record-source.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +65 -4
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-since.d.ts +90 -0
- package/dist/workspace/records-since.d.ts.map +1 -0
- package/dist/workspace/records-write.d.ts +164 -0
- package/dist/workspace/records-write.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +202 -15
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runtimes.d.ts +60 -0
- package/dist/workspace/runtimes.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts +90 -0
- package/dist/workspace/status-gates.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +17 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work.d.ts +56 -0
- package/dist/workspace/work.d.ts.map +1 -0
- package/package.json +19 -1
- package/src/cli/main.ts +48 -3
- package/src/cli/registry.ts +11 -1
- package/src/lifecycle/gate-ledger.ts +14 -0
- package/src/workspace/__fixtures__/sessions.ts +66 -0
- package/src/workspace/checks/records.ts +19 -0
- package/src/workspace/checks.test.ts +2 -0
- package/src/workspace/checks.ts +7 -1
- package/src/workspace/composites.schema.json +65 -3
- package/src/workspace/composites.test.ts +95 -5
- package/src/workspace/composites.ts +28 -7
- package/src/workspace/conformance/__fixture__/app/package.json +7 -0
- package/src/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +32 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +364 -0
- package/src/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
- package/src/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
- package/src/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
- package/src/workspace/conformance/__fixture__/delivery/package.json +7 -0
- package/src/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
- package/src/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
- package/src/workspace/conformance/conformance.test.ts +149 -0
- package/src/workspace/conformance/index.mjs +31 -0
- package/src/workspace/conformance/index.ts +453 -0
- package/src/workspace/conformance/vitest.ts +62 -0
- package/src/workspace/declaration.schema.json +40 -0
- package/src/workspace/declaration.ts +62 -0
- package/src/workspace/declared-kinds.test.ts +321 -0
- package/src/workspace/declared-kinds.ts +76 -0
- package/src/workspace/graph-cli.ts +8 -0
- package/src/workspace/intent-cli.ts +29 -6
- package/src/workspace/intent-joins.test.ts +60 -0
- package/src/workspace/intent-joins.ts +71 -19
- package/src/workspace/intent.schema.json +282 -7
- package/src/workspace/intent.test.ts +97 -0
- package/src/workspace/intent.ts +332 -45
- package/src/workspace/ls.schema.json +34 -0
- package/src/workspace/ls.ts +69 -4
- package/src/workspace/read-contract.test.ts +30 -9
- package/src/workspace/reason-codes.test.ts +15 -4
- package/src/workspace/reason-codes.ts +47 -4
- package/src/workspace/record-assets.test.ts +3 -1
- package/src/workspace/record-sessions.ts +105 -0
- package/src/workspace/record-source.ts +14 -5
- package/src/workspace/records-amend.schema.json +167 -0
- package/src/workspace/records-cli.ts +246 -19
- package/src/workspace/records-contract.test.ts +57 -2
- package/src/workspace/records-formats.test.ts +640 -0
- package/src/workspace/records-new.schema.json +158 -0
- package/src/workspace/records-quorum.test.ts +196 -0
- package/src/workspace/records-review.schema.json +202 -0
- package/src/workspace/records-sessions.test.ts +108 -0
- package/src/workspace/records-since.schema.json +193 -0
- package/src/workspace/records-since.test.ts +174 -0
- package/src/workspace/records-since.ts +259 -0
- package/src/workspace/records-write-contract.test.ts +125 -0
- package/src/workspace/records-write.test.ts +373 -0
- package/src/workspace/records-write.ts +736 -0
- package/src/workspace/records.schema.json +187 -9
- package/src/workspace/records.ts +631 -41
- package/src/workspace/runtimes.ts +107 -0
- package/src/workspace/status-contract.test.ts +163 -0
- package/src/workspace/status-gates.ts +215 -0
- package/src/workspace/status.schema.json +69 -3
- package/src/workspace/status.ts +35 -2
- package/src/workspace/work.test.ts +388 -0
- package/src/workspace/work.ts +163 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -50,6 +50,16 @@
|
|
|
50
50
|
"default": "./src/cli/index.ts"
|
|
51
51
|
},
|
|
52
52
|
"./workspace/*.schema.json": "./src/workspace/*.schema.json",
|
|
53
|
+
"./workspace/conformance": {
|
|
54
|
+
"development": "./src/workspace/conformance/index.ts",
|
|
55
|
+
"types": "./dist/workspace/conformance/index.d.ts",
|
|
56
|
+
"default": "./src/workspace/conformance/index.mjs"
|
|
57
|
+
},
|
|
58
|
+
"./workspace/conformance/vitest": {
|
|
59
|
+
"development": "./src/workspace/conformance/vitest.ts",
|
|
60
|
+
"types": "./dist/workspace/conformance/vitest.d.ts",
|
|
61
|
+
"default": "./src/workspace/conformance/vitest.ts"
|
|
62
|
+
},
|
|
53
63
|
"./cli/*": {
|
|
54
64
|
"development": "./src/cli/*.ts",
|
|
55
65
|
"types": "./dist/cli/*.d.ts",
|
|
@@ -104,6 +114,14 @@
|
|
|
104
114
|
"devDependencies": {
|
|
105
115
|
"@cdktn/hcl2json": "^0.24.0"
|
|
106
116
|
},
|
|
117
|
+
"peerDependencies": {
|
|
118
|
+
"vitest": ">=2"
|
|
119
|
+
},
|
|
120
|
+
"peerDependenciesMeta": {
|
|
121
|
+
"vitest": {
|
|
122
|
+
"optional": true
|
|
123
|
+
}
|
|
124
|
+
},
|
|
107
125
|
"overrides": {
|
|
108
126
|
"esbuild": "^0.28.1"
|
|
109
127
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -375,6 +375,26 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
375
375
|
if (!result.intent || result.intent.startsWith("-")) throw new Error("--intent needs a region: --intent <path[:start-end]>");
|
|
376
376
|
} else if (arg === "--current") {
|
|
377
377
|
result.current = true;
|
|
378
|
+
} else if (arg === "--set") {
|
|
379
|
+
// `chant workspace records amend <id> --set <file|->` (#2670)
|
|
380
|
+
result.set = args[++i];
|
|
381
|
+
if (!result.set || (result.set.startsWith("-") && result.set !== "-")) throw new Error("--set needs a JSON file, or - for standard input: --set <file|->");
|
|
382
|
+
} else if (arg === "--verdict") {
|
|
383
|
+
// `chant workspace records review <id> --verdict agree|dissent|abstain` (#2670)
|
|
384
|
+
result.verdict = args[++i];
|
|
385
|
+
if (!result.verdict || result.verdict.startsWith("-")) throw new Error("--verdict needs agree, dissent or abstain");
|
|
386
|
+
} else if (arg === "--by") {
|
|
387
|
+
// `chant workspace records review <id> --by <principal>` (#2670): whoever the caller says.
|
|
388
|
+
result.by = args[++i];
|
|
389
|
+
if (!result.by || result.by.startsWith("-")) throw new Error("--by needs the reviewer: --by <principal>");
|
|
390
|
+
} else if (arg === "--session") {
|
|
391
|
+
// `chant workspace records review <id> --session <id>` (#2670)
|
|
392
|
+
result.session = args[++i];
|
|
393
|
+
if (!result.session || result.session.startsWith("-")) throw new Error("--session needs a session id: --session <id>");
|
|
394
|
+
} else if (arg === "--prefix") {
|
|
395
|
+
// `chant workspace records new <kind> --prefix <prefix>` (#2670): the id prefix to allocate under.
|
|
396
|
+
result.prefix = args[++i];
|
|
397
|
+
if (!result.prefix || result.prefix.startsWith("-")) throw new Error("--prefix needs an id prefix: --prefix <prefix>");
|
|
378
398
|
} else if (arg === "--require") {
|
|
379
399
|
// `chant workspace records|verify --require attested` (#2547)
|
|
380
400
|
result.require = args[++i];
|
|
@@ -721,8 +741,9 @@ Workspace (level 1, #2524):
|
|
|
721
741
|
beside it and marks the members whose digests differ.
|
|
722
742
|
Read only; never fetches. --json prints the
|
|
723
743
|
read-contract document
|
|
724
|
-
workspace records --kind <kind file> [--current] [--at <rev>] [--base <rev>] [--require attested] [--json]
|
|
725
|
-
|
|
744
|
+
workspace records [--kind <kind file>] [--current] [--at <rev>] [--base <rev>] [--require attested] [--json]
|
|
745
|
+
Without --kind, every record kind the declaration
|
|
746
|
+
names. Read the records a record kind locates, validated
|
|
726
747
|
against its schema, with reason codes for invalid
|
|
727
748
|
ones. --current leaves out superseded records; --at
|
|
728
749
|
reads a commit's git objects. Needs no workspace file.
|
|
@@ -731,9 +752,32 @@ Workspace (level 1, #2524):
|
|
|
731
752
|
--require attested exits 2 if any record is not
|
|
732
753
|
attested. A pinned file that changed is a warning,
|
|
733
754
|
asset-drift or asset-missing
|
|
755
|
+
workspace records [--kind <kind file>] --since <rev> [--at <rev>] [--json]
|
|
756
|
+
What changed in the records between <rev> and --at
|
|
757
|
+
(default: the working tree): new and removed records,
|
|
758
|
+
state transitions, new verdicts, new supersessions
|
|
759
|
+
and changed pins
|
|
734
760
|
workspace records pin <path>
|
|
735
761
|
Print the path from the workspace root and the
|
|
736
762
|
sha256 of a file, for a decision's evidence pin
|
|
763
|
+
workspace records new [<kind file>] --from <file|-> [--prefix <prefix>] [--dry-run]
|
|
764
|
+
Write one new record in the kind's directory from
|
|
765
|
+
the JSON fields given, after validating them as
|
|
766
|
+
records would read them. Without a kind file, the one
|
|
767
|
+
kind the declaration names. Allocates the next id when
|
|
768
|
+
the fields hold none. Prints {path, id} as JSON and
|
|
769
|
+
never commits
|
|
770
|
+
workspace records amend <id> [--kind <kind file>] --set <file|-> [--dry-run]
|
|
771
|
+
Set top-level fields of one record. A closed record
|
|
772
|
+
never changes, and an approved one changes only its
|
|
773
|
+
state (upward), pins and reviews; anything else is
|
|
774
|
+
refused with amend-supersede-instead. Prints
|
|
775
|
+
{path, id, changed}
|
|
776
|
+
workspace records review <id> [--kind <kind file>] --verdict agree|dissent|abstain --by <principal> [--note <text>] [--session <id>] [--dry-run]
|
|
777
|
+
Append a review to one record, dated and bound to
|
|
778
|
+
the digest of the record text. A dissent needs
|
|
779
|
+
--note. The principal is not checked; attestation is
|
|
780
|
+
the seal's job. Prints {path, id, review}
|
|
737
781
|
workspace verify [--base <rev>] [--head <rev>] [--require attested]
|
|
738
782
|
Check the commits in base..head against the signers
|
|
739
783
|
and roles read from base. A change to the signers file
|
|
@@ -784,7 +828,8 @@ Workspace (level 1, #2524):
|
|
|
784
828
|
workspace graph --intent <path[:start-end]> [--at <rev>] [--kind <kind file>...] [--json]
|
|
785
829
|
The intent graph over one region: the commits that
|
|
786
830
|
touched it, the decisions whose constrains cover it,
|
|
787
|
-
the artifacts they pin, and findings with closed codes
|
|
831
|
+
the artifacts they pin, and findings with closed codes.
|
|
832
|
+
Without --kind, every record kind the declaration names
|
|
788
833
|
|
|
789
834
|
Lifecycle (alias: lc):
|
|
790
835
|
lifecycle snapshot <env> Query API, save metadata to orphan branch
|
package/src/cli/registry.ts
CHANGED
|
@@ -291,6 +291,16 @@ export interface ParsedArgs {
|
|
|
291
291
|
intent?: string;
|
|
292
292
|
/** `chant workspace records --current` (#2546): leave out records a closed record supersedes. */
|
|
293
293
|
current?: boolean;
|
|
294
|
+
/** `chant workspace records amend <id> --set <file|->` (#2670): the JSON fields to set. */
|
|
295
|
+
set?: string;
|
|
296
|
+
/** `chant workspace records review <id> --verdict <v>` (#2670): agree, dissent or abstain. */
|
|
297
|
+
verdict?: string;
|
|
298
|
+
/** `chant workspace records review <id> --by <principal>` (#2670): the reviewer, as the caller names them. */
|
|
299
|
+
by?: string;
|
|
300
|
+
/** `chant workspace records review <id> --session <id>` (#2670): the review session the verdict was given in. */
|
|
301
|
+
session?: string;
|
|
302
|
+
/** `chant workspace records new <kind> --prefix <prefix>` (#2670): the id prefix to allocate under. */
|
|
303
|
+
prefix?: string;
|
|
294
304
|
/** `chant workspace records|verify --require attested` (#2547): the provenance level a gate requires. */
|
|
295
305
|
require?: string;
|
|
296
306
|
/**
|
|
@@ -416,7 +426,7 @@ export interface ParsedArgs {
|
|
|
416
426
|
plan?: string;
|
|
417
427
|
/** `chant operator log --op <name>` (#2029) — restrict the tick history to one ConvergeOp by name. Omitted, every discovered ConvergeOp's ticks are merged into one timeline. */
|
|
418
428
|
op?: string;
|
|
419
|
-
/** `chant operator log --since <iso>` (#2029) — only entries at or after this ISO-8601 instant. */
|
|
429
|
+
/** `chant operator log --since <iso>` (#2029) — only entries at or after this ISO-8601 instant. `chant workspace records --since <rev>` (#2673) — the revision to compare the records with. */
|
|
420
430
|
since?: string;
|
|
421
431
|
/** `chant operator log --limit <n>` (#2029) — keep only the newest n entries (still printed oldest-first). */
|
|
422
432
|
limit?: number;
|
|
@@ -47,6 +47,9 @@ import type { GateApprover, GatePolicyDecision, ResolvedGateApproval } from "../
|
|
|
47
47
|
import { readBlobFromPath, readPathSha, readBlobBySha, writeBlobToPath, RefCASConflictError } from "./git";
|
|
48
48
|
|
|
49
49
|
const DIR = "_gates";
|
|
50
|
+
|
|
51
|
+
/** The directory on the `chant/lifecycle` branch that holds gate ledgers, under a member's prefix when it has one. */
|
|
52
|
+
export const GATES_DIR = DIR;
|
|
50
53
|
const APPEND_RETRY_ATTEMPTS = 5;
|
|
51
54
|
|
|
52
55
|
/**
|
|
@@ -350,7 +353,18 @@ export async function readGateLedger(
|
|
|
350
353
|
): Promise<{ resolutions: GateResolutionRecord[]; pending: PendingGateRecord[]; malformed: number }> {
|
|
351
354
|
const content = await readBlobFromPath(DIR, filename(op), opts);
|
|
352
355
|
if (!content) return { resolutions: [], pending: [], malformed: 0 };
|
|
356
|
+
return parseGateLedger(content);
|
|
357
|
+
}
|
|
353
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Split the text of one gate ledger file into its two kinds of line, oldest
|
|
361
|
+
* first, the way {@link readGateLedger} reads it. Exported for a reader that
|
|
362
|
+
* fetches the file itself, such as `chant workspace status` (#2674), which
|
|
363
|
+
* reads every member's ledger by its full path from the workspace root.
|
|
364
|
+
*/
|
|
365
|
+
export function parseGateLedger(
|
|
366
|
+
content: string,
|
|
367
|
+
): { resolutions: GateResolutionRecord[]; pending: PendingGateRecord[]; malformed: number } {
|
|
354
368
|
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
355
369
|
const resolutions: GateResolutionRecord[] = [];
|
|
356
370
|
const pending: PendingGateRecord[] = [];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A repository with the reference workspace's decisions and design member,
|
|
3
|
+
* and helpers that write sessions and review entries into it, for the
|
|
4
|
+
* session and records --since tests (#2673).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cpSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { sessionSeal } from "../record-sessions";
|
|
10
|
+
import { commitAll, git, REPO, scratchDir } from "./contract-repo";
|
|
11
|
+
|
|
12
|
+
export const SESSIONS_KIND = "design/sessions/session.kind.mjs";
|
|
13
|
+
export const DECISIONS_KIND = "decisions/decision.kind.mjs";
|
|
14
|
+
|
|
15
|
+
/** A git repository holding a copy of the reference workspace's decisions and design member. */
|
|
16
|
+
export function sessionsRepo(): string {
|
|
17
|
+
const root = scratchDir("chant-sessions-");
|
|
18
|
+
for (const d of ["decisions", "design"]) cpSync(join(REPO, "reference-workspace", d), join(root, d), { recursive: true });
|
|
19
|
+
git(root, "init", "-q");
|
|
20
|
+
commitAll(root, "reference");
|
|
21
|
+
return root;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A session file's text, sealed when it is closed. */
|
|
25
|
+
export function sessionText(s: { id: string; state: "open" | "closed"; verdicts?: { record: string; principal: string; verdict: string }[]; agenda?: string[] }): string {
|
|
26
|
+
const lines = [
|
|
27
|
+
"---",
|
|
28
|
+
"schema: 1",
|
|
29
|
+
`id: "${s.id}"`,
|
|
30
|
+
`title: "Session ${s.id}"`,
|
|
31
|
+
`state: "${s.state}"`,
|
|
32
|
+
"agenda:",
|
|
33
|
+
...(s.agenda ?? ["ref-001"]).map((r) => ` - record: "${r}"`),
|
|
34
|
+
"attendance:",
|
|
35
|
+
' - principal: "lex00"',
|
|
36
|
+
' class: "person"',
|
|
37
|
+
' - principal: "alice"',
|
|
38
|
+
' class: "person"',
|
|
39
|
+
' - principal: "helper"',
|
|
40
|
+
' class: "agent"',
|
|
41
|
+
'opened: "2026-09-24T18:00:00Z"',
|
|
42
|
+
`closed: ${s.state === "closed" ? '"2026-09-24T19:00:00Z"' : "null"}`,
|
|
43
|
+
...(s.verdicts?.length
|
|
44
|
+
? ["verdicts:", ...s.verdicts.flatMap((v) => [` - record: "${v.record}"`, ` principal: "${v.principal}"`, ` verdict: "${v.verdict}"`])]
|
|
45
|
+
: ["verdicts: []"]),
|
|
46
|
+
...(s.state === "closed" ? ['closed_digest: "SEAL"'] : []),
|
|
47
|
+
"---",
|
|
48
|
+
"",
|
|
49
|
+
`# Session ${s.id}`,
|
|
50
|
+
"",
|
|
51
|
+
];
|
|
52
|
+
const text = lines.join("\n");
|
|
53
|
+
return s.state === "closed" ? text.replace('"SEAL"', `"${sessionSeal(text, "closed_digest")}"`) : text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Rewrite ref-001 with the given review entries, each naming `session`. */
|
|
57
|
+
export function reviewed(root: string, reviewers: string[], session: string, state = "decided"): void {
|
|
58
|
+
const file = join(root, "decisions", "ref-001-how-the-app-is-deployed.md");
|
|
59
|
+
const reviews = reviewers.map((r) => ` - reviewer: "${r}"\n verdict: "agree"\n on: "2026-09-24"\n session: "${session}"`).join("\n");
|
|
60
|
+
writeFileSync(
|
|
61
|
+
file,
|
|
62
|
+
readFileSync(file, "utf-8")
|
|
63
|
+
.replace(/^state: .*$/m, `state: "${state}"`)
|
|
64
|
+
.replace(/^reviews: .*$/m, reviewers.length ? `reviews:\n${reviews}` : "reviews: []"),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* | WSP112 | a pinned file that does not exist |
|
|
14
14
|
* | WSP113 | a pinned file that a superseding record pins at the old hash: the artifact did not follow the decision |
|
|
15
15
|
* | WSP114 | the records of `--kind` can't be read (fixed) |
|
|
16
|
+
* | WSP115 | a record kind the declaration names is missing or does not load as one (fixed, #2680) |
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import type { WorkspaceCheck, WorkspaceDiagnostic } from "../checks";
|
|
@@ -80,4 +81,22 @@ export const RECORD_CHECKS: readonly WorkspaceCheck[] = [
|
|
|
80
81
|
return [{ checkId: this.id, severity: this.severity, message: `--kind ${facts.kind}: ${facts.error.code}: ${facts.error.message}`, pointer: "" }];
|
|
81
82
|
},
|
|
82
83
|
},
|
|
84
|
+
{
|
|
85
|
+
id: "WSP115",
|
|
86
|
+
name: "record-kind-unloadable",
|
|
87
|
+
description: "Every record kind the declaration names is a file that exports a valid recordKind, with the schema it names.",
|
|
88
|
+
severity: "error",
|
|
89
|
+
configurable: false,
|
|
90
|
+
check(ctx) {
|
|
91
|
+
return (ctx.facts?.declaredKinds ?? [])
|
|
92
|
+
.filter((k) => k.reason !== null)
|
|
93
|
+
.map((k) => ({
|
|
94
|
+
checkId: this.id,
|
|
95
|
+
severity: this.severity,
|
|
96
|
+
message: `${k.declared.member === null ? "the workspace" : `member ${k.declared.member}`} declares the record kind ${k.declared.kind}, which can't be loaded: ${k.reason!.code}: ${k.reason!.message}`,
|
|
97
|
+
...(k.declared.member !== null ? { entity: k.declared.member } : {}),
|
|
98
|
+
pointer: `${k.declared.pointer}/kind`,
|
|
99
|
+
}));
|
|
100
|
+
},
|
|
101
|
+
},
|
|
83
102
|
];
|
|
@@ -58,6 +58,8 @@ describe("the WSP catalog", () => {
|
|
|
58
58
|
"link-kind-unknown",
|
|
59
59
|
// --kind names a record kind, and records that can't be read check nothing (#2549).
|
|
60
60
|
"records-unreadable",
|
|
61
|
+
// A declared record kind that doesn't load breaks every reader's default (#2680).
|
|
62
|
+
"record-kind-unloadable",
|
|
61
63
|
]);
|
|
62
64
|
});
|
|
63
65
|
|
package/src/workspace/checks.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* | WSP091 to WSP097 | member links (#2539) |
|
|
28
28
|
* | WSP101 to WSP106 | generated files |
|
|
29
29
|
* | WSP111 to WSP114 | records read with `--kind` (#2549) |
|
|
30
|
+
* | WSP115 | the record kinds the declaration names (#2680) |
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
import { realpathSync } from "node:fs";
|
|
@@ -44,6 +45,7 @@ import { gatherGeneratedFacts, GENERATED_CHECKS, type GeneratedFileFacts } from
|
|
|
44
45
|
import { gatherLedgerFacts, LEDGER_CHECKS, type MemberLedgerFacts } from "./checks/ledgers";
|
|
45
46
|
import { gatherPipelineFacts, PIPELINE_CHECKS, type MemberPipelineFacts } from "./checks/pipelines";
|
|
46
47
|
import { RECORD_CHECKS, type RecordFacts } from "./checks/records";
|
|
48
|
+
import { loadDeclaredKinds, type DeclaredKind } from "./declared-kinds";
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
51
|
* What the checks beyond the declaration read, gathered from the checkout
|
|
@@ -59,6 +61,8 @@ export interface WorkspaceFacts {
|
|
|
59
61
|
generated?: readonly GeneratedFileFacts[];
|
|
60
62
|
/** The records of the kind named with `--kind`, with their pins checked (#2549). */
|
|
61
63
|
records?: RecordFacts;
|
|
64
|
+
/** The record kinds the declaration names, each loaded, or only looked for under `--at` (#2680). */
|
|
65
|
+
declaredKinds?: readonly DeclaredKind[];
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
/** What every declaration check reads. */
|
|
@@ -444,7 +448,9 @@ export async function runDeclarationChecks(
|
|
|
444
448
|
}
|
|
445
449
|
const { registry, problems } = loadKindRegistry(declaration.pins, root);
|
|
446
450
|
const gathered = options.gather === false || options.tree ? {} : await gatherWorkspaceFacts(root, declaration, options);
|
|
447
|
-
|
|
451
|
+
// The declared record kinds load from the working tree; under --at only whether each exists at the revision is checked (#2680).
|
|
452
|
+
const declaredKinds = options.gather === false ? undefined : await loadDeclaredKinds(declaration, tree, root, { load: !options.tree });
|
|
453
|
+
const facts: WorkspaceFacts = { ...gathered, ...(options.records ? { records: options.records } : {}), ...(declaredKinds ? { declaredKinds } : {}) };
|
|
448
454
|
const ctx: WorkspaceCheckContext = { declaration, tree, groups, kinds: registry, kindProblems: problems, facts };
|
|
449
455
|
const findings = runWorkspaceChecks(ctx);
|
|
450
456
|
const { active, suppressed } = applyCheckSettings(declaration, findings);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://intentius.io/chant/schemas/workspace/composites/v1/composites.schema.json",
|
|
4
4
|
"title": "chant workspace graph --composites output",
|
|
5
|
-
"description": "What `chant workspace graph --composites` prints: each composite instance the workspace's members declare, joined to the components whose contract can deploy it (#2662). Each member of kind chant is read through its own `chant graph --format ir` and `chant graph --components --format ir`, under its own toolchain. An instance with no component is listed with an empty `components` array. chant supplies the rows and never picks one: each match says how it was made and how it crosses members. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists. Contract version 1 is written by chant 0.81.0 and newer, and this document was added to it by #2662; a chant without --composites refuses the flag. Every code is in the one closed list of `reason-codes.ts`.",
|
|
5
|
+
"description": "What `chant workspace graph --composites` prints: each composite instance the workspace's members declare, joined to the components whose contract can deploy it (#2662). Each member of kind chant is read through its own `chant graph --format ir` and `chant graph --components --format ir`, under its own toolchain. An instance with no component is listed with an empty `components` array. chant supplies the rows and never picks one: each match says how it was made and how it crosses members. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists. Contract version 1 is written by chant 0.81.0 and newer, and this document was added to it by #2662; a chant without --composites refuses the flag. Each component lists the runtimes it can deploy on, read from its member's chant.config.ts (#2674). Every code is in the one closed list of `reason-codes.ts`.",
|
|
6
6
|
"oneOf": [
|
|
7
7
|
{
|
|
8
8
|
"$ref": "#/$defs/result"
|
|
@@ -130,7 +130,8 @@
|
|
|
130
130
|
"kind",
|
|
131
131
|
"status",
|
|
132
132
|
"reason",
|
|
133
|
-
"chant"
|
|
133
|
+
"chant",
|
|
134
|
+
"runtimeReasons"
|
|
134
135
|
],
|
|
135
136
|
"properties": {
|
|
136
137
|
"name": {
|
|
@@ -187,6 +188,28 @@
|
|
|
187
188
|
"null"
|
|
188
189
|
],
|
|
189
190
|
"description": "The chant version the member's toolchain reported, or null when a chant older than member-run read it."
|
|
191
|
+
},
|
|
192
|
+
"runtimeReasons": {
|
|
193
|
+
"description": "Why the runtimes of the member's components leave something out: its chant.config.ts, or a lexicon it lists, could not be read. Empty for a member not of kind chant, and when nothing was left out.",
|
|
194
|
+
"type": "array",
|
|
195
|
+
"items": {
|
|
196
|
+
"type": "object",
|
|
197
|
+
"required": [
|
|
198
|
+
"code",
|
|
199
|
+
"message"
|
|
200
|
+
],
|
|
201
|
+
"properties": {
|
|
202
|
+
"code": {
|
|
203
|
+
"enum": [
|
|
204
|
+
"runtimes-config-unreadable",
|
|
205
|
+
"runtimes-lexicon-unreadable"
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
"message": {
|
|
209
|
+
"type": "string"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
190
213
|
}
|
|
191
214
|
},
|
|
192
215
|
"if": {
|
|
@@ -327,7 +350,8 @@
|
|
|
327
350
|
"member",
|
|
328
351
|
"archetype",
|
|
329
352
|
"composites",
|
|
330
|
-
"file"
|
|
353
|
+
"file",
|
|
354
|
+
"runtimes"
|
|
331
355
|
],
|
|
332
356
|
"properties": {
|
|
333
357
|
"id": {
|
|
@@ -368,6 +392,14 @@
|
|
|
368
392
|
"null"
|
|
369
393
|
],
|
|
370
394
|
"description": "The component's file from the workspace root, when the member's chant names it."
|
|
395
|
+
},
|
|
396
|
+
"runtimes": {
|
|
397
|
+
"description": "The runtimes the component can deploy on, the list chant run --components --on accepts in its member: local first, then each lexicon the member's chant.config.ts lists whose opRuntime hosts component runs, in config order.",
|
|
398
|
+
"type": "array",
|
|
399
|
+
"minItems": 1,
|
|
400
|
+
"items": {
|
|
401
|
+
"$ref": "#/$defs/runtime"
|
|
402
|
+
}
|
|
371
403
|
}
|
|
372
404
|
}
|
|
373
405
|
},
|
|
@@ -466,6 +498,36 @@
|
|
|
466
498
|
},
|
|
467
499
|
"composites": false
|
|
468
500
|
}
|
|
501
|
+
},
|
|
502
|
+
"runtime": {
|
|
503
|
+
"type": "object",
|
|
504
|
+
"required": [
|
|
505
|
+
"name",
|
|
506
|
+
"lexicon",
|
|
507
|
+
"default",
|
|
508
|
+
"command"
|
|
509
|
+
],
|
|
510
|
+
"properties": {
|
|
511
|
+
"name": {
|
|
512
|
+
"type": "string",
|
|
513
|
+
"description": "What --on takes: local, or the lexicon's name."
|
|
514
|
+
},
|
|
515
|
+
"lexicon": {
|
|
516
|
+
"type": [
|
|
517
|
+
"string",
|
|
518
|
+
"null"
|
|
519
|
+
],
|
|
520
|
+
"description": "The lexicon that hosts the runtime, or null for local."
|
|
521
|
+
},
|
|
522
|
+
"default": {
|
|
523
|
+
"type": "boolean",
|
|
524
|
+
"description": "True for the runtime chant run --components uses without --on. That is local until chant.config.ts can name a default."
|
|
525
|
+
},
|
|
526
|
+
"command": {
|
|
527
|
+
"type": "string",
|
|
528
|
+
"description": "The command that deploys the component on this runtime, run in the member's directory: chant run --components <name>, with --on <runtime> for any runtime but local."
|
|
529
|
+
}
|
|
530
|
+
}
|
|
469
531
|
}
|
|
470
532
|
}
|
|
471
533
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (`read-contract.test.ts`).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { afterAll, describe, expect, test } from "vitest";
|
|
13
13
|
import type { GraphIR, IRNode } from "../graph-ir";
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
COMPOSITES_ERROR_CODES,
|
|
19
19
|
COMPOSITES_OUTPUT_SCHEMA_ID,
|
|
20
20
|
COMPOSITES_REASON_CODES,
|
|
21
|
+
COMPOSITES_RUNTIME_REASON_CODES,
|
|
21
22
|
compositeInstances,
|
|
22
23
|
joinComponents,
|
|
23
24
|
workspaceComposites,
|
|
@@ -54,6 +55,9 @@ const componentIr = (components: { name: string; composites?: string[]; archetyp
|
|
|
54
55
|
{ groups: { byWave: { "wave-1": components.map((c) => c.name) } } },
|
|
55
56
|
);
|
|
56
57
|
|
|
58
|
+
/** The built-in runtime every component has. */
|
|
59
|
+
const local = (name: string) => ({ name: "local", lexicon: null, default: true, command: `chant run --components ${name}` });
|
|
60
|
+
|
|
57
61
|
/**
|
|
58
62
|
* app declares three instances and exports ImageUri; delivery links to it and
|
|
59
63
|
* declares two components; jobs declares an instance and a component of its
|
|
@@ -106,6 +110,7 @@ describe("composites output schema", () => {
|
|
|
106
110
|
expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual([...COMPOSITES_ERROR_CODES]);
|
|
107
111
|
expect(COMPOSITES_ERROR_CODES).toEqual(GRAPH_ERROR_CODES);
|
|
108
112
|
expect(schema.$defs.member.properties.reason.oneOf[1].properties!.code.enum).toEqual([...MEMBER_RUN_REASON_CODES]);
|
|
113
|
+
expect(schema.$defs.member.properties.runtimeReasons.items.properties.code.enum).toEqual([...COMPOSITES_RUNTIME_REASON_CODES]);
|
|
109
114
|
});
|
|
110
115
|
});
|
|
111
116
|
|
|
@@ -117,7 +122,7 @@ describe("the join", () => {
|
|
|
117
122
|
{ ...composite("a/billingTable", "Table", "a/billing"), member: "a" },
|
|
118
123
|
],
|
|
119
124
|
});
|
|
120
|
-
const component = (member: string, name: string, composites: string[] | null = null): ComponentEntry => ({ id: `${member}/${name}`, name, member, archetype: null, composites, file: null });
|
|
125
|
+
const component = (member: string, name: string, composites: string[] | null = null): ComponentEntry => ({ id: `${member}/${name}`, name, member, archetype: null, composites, file: null, runtimes: [] });
|
|
121
126
|
|
|
122
127
|
test("an instance carries every kind and lexicon of its nodes", () => {
|
|
123
128
|
expect(instances.map((i) => [i.id, i.instance, i.kinds, i.lexicons])).toEqual([
|
|
@@ -160,9 +165,9 @@ describe("chant workspace graph --composites on a built workspace", () => {
|
|
|
160
165
|
["docs", "skipped", "kind-not-run"],
|
|
161
166
|
]);
|
|
162
167
|
expect(g.components).toEqual([
|
|
163
|
-
{ id: "delivery/edge", name: "edge", member: "delivery", archetype: "infra", composites: ["StaticSite"], file: "delivery/src/edge.component.ts" },
|
|
164
|
-
{ id: "delivery/loom-backend", name: "loom-backend", member: "delivery", archetype: "service", composites: null, file: "delivery/src/loom-backend.component.ts" },
|
|
165
|
-
{ id: "jobs/queue-runner", name: "queue-runner", member: "jobs", archetype: null, composites: ["WorkQueue", "CacheCluster"], file: "jobs/src/queue-runner.component.ts" },
|
|
168
|
+
{ id: "delivery/edge", name: "edge", member: "delivery", archetype: "infra", composites: ["StaticSite"], file: "delivery/src/edge.component.ts", runtimes: [local("edge")] },
|
|
169
|
+
{ id: "delivery/loom-backend", name: "loom-backend", member: "delivery", archetype: "service", composites: null, file: "delivery/src/loom-backend.component.ts", runtimes: [local("loom-backend")] },
|
|
170
|
+
{ id: "jobs/queue-runner", name: "queue-runner", member: "jobs", archetype: null, composites: ["WorkQueue", "CacheCluster"], file: "jobs/src/queue-runner.component.ts", runtimes: [local("queue-runner")] },
|
|
166
171
|
]);
|
|
167
172
|
const rows = Object.fromEntries(g.composites.map((c) => [c.id, c]));
|
|
168
173
|
expect(Object.keys(rows)).toEqual(["app/backend", "app/cache", "app/site", "jobs/queue"]);
|
|
@@ -242,3 +247,88 @@ describe("why the list is empty", () => {
|
|
|
242
247
|
expect("error" in doc && doc.error.code).toBe("declaration-missing");
|
|
243
248
|
});
|
|
244
249
|
});
|
|
250
|
+
|
|
251
|
+
/** A lexicon module: a LexiconPlugin export, with `opRuntime` when `runtime` says what it hosts. */
|
|
252
|
+
function lexiconModule(name: string, runtime: "components" | "ops" | "none"): string {
|
|
253
|
+
const opRuntime =
|
|
254
|
+
runtime === "none"
|
|
255
|
+
? ""
|
|
256
|
+
: `opRuntime: { name: ${JSON.stringify(name)}, start: async () => { throw new Error("stub"); }${runtime === "components" ? ", runComponents: async () => ({ success: true })" : ""} },`;
|
|
257
|
+
return `const noop = async () => {};
|
|
258
|
+
export const plugin = { name: ${JSON.stringify(name)}, serializer: {}, generate: noop, validate: noop, coverage: noop, package: noop, ${opRuntime} };
|
|
259
|
+
`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
describe("the runtimes each component can deploy on (#2674)", () => {
|
|
263
|
+
/** delivery configures four lexicons: one hosts component runs, one hosts only Op runs, one hosts nothing, one can't load. */
|
|
264
|
+
function hosting(): string {
|
|
265
|
+
const root = fixture();
|
|
266
|
+
writeFileSync(
|
|
267
|
+
join(root, "delivery", "chant.config.ts"),
|
|
268
|
+
`export default {
|
|
269
|
+
lexicons: [
|
|
270
|
+
{ name: "fleet", module: "./lexicons/fleet.ts" },
|
|
271
|
+
{ name: "oponly", module: "./lexicons/oponly.ts" },
|
|
272
|
+
{ name: "plain", module: "./lexicons/plain.ts" },
|
|
273
|
+
{ name: "gone", module: "./lexicons/gone.ts" },
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
`,
|
|
277
|
+
);
|
|
278
|
+
mkdirSync(join(root, "delivery", "lexicons"));
|
|
279
|
+
writeFileSync(join(root, "delivery", "lexicons", "fleet.ts"), lexiconModule("fleet", "components"));
|
|
280
|
+
writeFileSync(join(root, "delivery", "lexicons", "oponly.ts"), lexiconModule("oponly", "ops"));
|
|
281
|
+
writeFileSync(join(root, "delivery", "lexicons", "plain.ts"), lexiconModule("plain", "none"));
|
|
282
|
+
return root;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
test("local is the default, and a configured lexicon whose opRuntime hosts component runs is listed with its --on line", async () => {
|
|
286
|
+
const g = result((await workspaceComposites({ cwd: hosting() })).doc);
|
|
287
|
+
expectValid(g);
|
|
288
|
+
const edge = g.components.find((c) => c.id === "delivery/edge")!;
|
|
289
|
+
expect(edge.runtimes).toEqual([
|
|
290
|
+
{ name: "local", lexicon: null, default: true, command: "chant run --components edge" },
|
|
291
|
+
{ name: "fleet", lexicon: "fleet", default: false, command: "chant run --components edge --on fleet" },
|
|
292
|
+
]);
|
|
293
|
+
expect(g.components.find((c) => c.id === "delivery/loom-backend")!.runtimes.map((r) => r.command)).toEqual([
|
|
294
|
+
"chant run --components loom-backend",
|
|
295
|
+
"chant run --components loom-backend --on fleet",
|
|
296
|
+
]);
|
|
297
|
+
// jobs configures no lexicon, so only local.
|
|
298
|
+
expect(g.components.find((c) => c.id === "jobs/queue-runner")!.runtimes).toEqual([local("queue-runner")]);
|
|
299
|
+
const delivery = g.members.find((m) => m.name === "delivery")!;
|
|
300
|
+
expect(delivery.runtimeReasons.map((r) => r.code)).toEqual(["runtimes-lexicon-unreadable"]);
|
|
301
|
+
expect(delivery.runtimeReasons[0].message).toContain('lexicon "gone"');
|
|
302
|
+
expect(g.members.find((m) => m.name === "docs")!.runtimeReasons).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("a member whose config can't be read lists local only, with a reason", async () => {
|
|
306
|
+
const root = fixture();
|
|
307
|
+
writeFileSync(join(root, "jobs", "chant.config.ts"), 'throw new Error("no config here");\nexport default {};\n');
|
|
308
|
+
const { doc, failed } = await workspaceComposites({ cwd: root, loadPlugin: async () => { throw new Error("not called"); } });
|
|
309
|
+
const g = result(doc);
|
|
310
|
+
expectValid(g);
|
|
311
|
+
expect(failed).toBe(false);
|
|
312
|
+
const jobs = g.members.find((m) => m.name === "jobs")!;
|
|
313
|
+
expect(jobs.runtimeReasons).toEqual([{ code: "runtimes-config-unreadable", message: expect.stringContaining("no config here") }]);
|
|
314
|
+
expect(g.components.find((c) => c.id === "jobs/queue-runner")!.runtimes).toEqual([local("queue-runner")]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("the plugin loader decides from opRuntime.runComponents alone", async () => {
|
|
318
|
+
const root = fixture();
|
|
319
|
+
writeFileSync(join(root, "jobs", "chant.config.ts"), 'export default { lexicons: ["hosted", "bare"] };\n');
|
|
320
|
+
const g = result(
|
|
321
|
+
(
|
|
322
|
+
await workspaceComposites({
|
|
323
|
+
cwd: root,
|
|
324
|
+
loadPlugin: async (name) => (name === "hosted" ? { opRuntime: { runComponents: () => undefined } } : {}),
|
|
325
|
+
})
|
|
326
|
+
).doc,
|
|
327
|
+
);
|
|
328
|
+
expectValid(g);
|
|
329
|
+
expect(g.components.find((c) => c.id === "jobs/queue-runner")!.runtimes.map((r) => [r.name, r.lexicon, r.default])).toEqual([
|
|
330
|
+
["local", null, true],
|
|
331
|
+
["hosted", "hosted", false],
|
|
332
|
+
]);
|
|
333
|
+
});
|
|
334
|
+
});
|