@intentius/chant 0.81.0 → 0.83.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/content-digest.d.ts +2 -0
- package/dist/content-digest.d.ts.map +1 -1
- package/dist/workspace/checks/records.d.ts +33 -0
- package/dist/workspace/checks/records.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts +8 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +22 -6
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/graph-cli.d.ts +11 -4
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-check.d.ts +5 -2
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-cli.d.ts +5 -1
- package/dist/workspace/lineage-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-init.d.ts +47 -4
- package/dist/workspace/lineage-init.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +23 -2
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade-cli.d.ts +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +8 -2
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +4 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts +108 -0
- package/dist/workspace/record-assets.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +32 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +41 -0
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/template-pins.d.ts +33 -0
- package/dist/workspace/template-pins.d.ts.map +1 -0
- package/dist/workspace/tree.d.ts +5 -0
- package/dist/workspace/tree.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/init.ts +7 -3
- package/src/cli/main.ts +16 -8
- package/src/content-digest.ts +5 -0
- package/src/workspace/behold-kinds.test.ts +10 -14
- package/src/workspace/checks/records.ts +83 -0
- package/src/workspace/checks.test.ts +2 -0
- package/src/workspace/checks.ts +13 -3
- package/src/workspace/compose-graph.test.ts +2 -1
- package/src/workspace/compose-graph.ts +23 -6
- package/src/workspace/graph-cli.ts +32 -6
- package/src/workspace/graph-contract.test.ts +2 -1
- package/src/workspace/graph.schema.json +131 -2
- package/src/workspace/lineage-check.ts +22 -5
- package/src/workspace/lineage-cli.ts +21 -1
- package/src/workspace/lineage-init-dir.test.ts +243 -0
- package/src/workspace/lineage-init.ts +223 -24
- package/src/workspace/lineage-lock.ts +18 -3
- package/src/workspace/lineage-upgrade-cli.ts +2 -2
- package/src/workspace/lineage-upgrade.test.ts +29 -0
- package/src/workspace/lineage-upgrade.ts +130 -13
- package/src/workspace/member-commands.ts +1 -1
- package/src/workspace/reason-codes.test.ts +2 -1
- package/src/workspace/reason-codes.ts +5 -0
- package/src/workspace/record-assets.test.ts +319 -0
- package/src/workspace/record-assets.ts +207 -0
- package/src/workspace/records-cli.ts +117 -14
- package/src/workspace/records.schema.json +33 -1
- package/src/workspace/records.test.ts +50 -0
- package/src/workspace/records.ts +115 -5
- package/src/workspace/template-pins.test.ts +69 -0
- package/src/workspace/template-pins.ts +117 -0
- package/src/workspace/tree.ts +12 -0
package/src/workspace/checks.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* | WSP081 to WSP083 | recorded pipelines |
|
|
27
27
|
* | WSP091 to WSP097 | member links (#2539) |
|
|
28
28
|
* | WSP101 to WSP106 | generated files |
|
|
29
|
+
* | WSP111 to WSP114 | records read with `--kind` (#2549) |
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
32
|
import { realpathSync } from "node:fs";
|
|
@@ -42,6 +43,7 @@ import type { LinkTableRow } from "./links";
|
|
|
42
43
|
import { gatherGeneratedFacts, GENERATED_CHECKS, type GeneratedFileFacts } from "./checks/generated";
|
|
43
44
|
import { gatherLedgerFacts, LEDGER_CHECKS, type MemberLedgerFacts } from "./checks/ledgers";
|
|
44
45
|
import { gatherPipelineFacts, PIPELINE_CHECKS, type MemberPipelineFacts } from "./checks/pipelines";
|
|
46
|
+
import { RECORD_CHECKS, type RecordFacts } from "./checks/records";
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* What the checks beyond the declaration read, gathered from the checkout
|
|
@@ -55,6 +57,8 @@ export interface WorkspaceFacts {
|
|
|
55
57
|
pipelines?: readonly MemberPipelineFacts[];
|
|
56
58
|
/** Each declared or implicit generated file, with what its generator produced. */
|
|
57
59
|
generated?: readonly GeneratedFileFacts[];
|
|
60
|
+
/** The records of the kind named with `--kind`, with their pins checked (#2549). */
|
|
61
|
+
records?: RecordFacts;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/** What every declaration check reads. */
|
|
@@ -77,6 +81,8 @@ export interface WorkspaceCheckContext {
|
|
|
77
81
|
export interface WorkspaceDiagnostic extends PostSynthDiagnostic {
|
|
78
82
|
/** A JSON Pointer into the declaration. */
|
|
79
83
|
pointer: string;
|
|
84
|
+
/** An absolute path, for a finding about a file other than the declaration, such as a record (#2549). It is reported at its first line. */
|
|
85
|
+
file?: string;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
export interface WorkspaceCheck {
|
|
@@ -295,6 +301,8 @@ export const WORKSPACE_CHECKS: readonly WorkspaceCheck[] = [
|
|
|
295
301
|
...LINK_CHECKS,
|
|
296
302
|
// Generated files (#2541).
|
|
297
303
|
...GENERATED_CHECKS,
|
|
304
|
+
// Records read with --kind (#2549).
|
|
305
|
+
...RECORD_CHECKS,
|
|
298
306
|
];
|
|
299
307
|
|
|
300
308
|
const BY_ID = new Map(WORKSPACE_CHECKS.map((c) => [c.id, c]));
|
|
@@ -370,6 +378,8 @@ export interface DeclarationCheckOptions {
|
|
|
370
378
|
* gathered: they describe the working tree, not the revision.
|
|
371
379
|
*/
|
|
372
380
|
tree?: WorkspaceTree;
|
|
381
|
+
/** The records read with `--kind`, in the same tree (#2549). Given with or without `tree`. */
|
|
382
|
+
records?: RecordFacts;
|
|
373
383
|
}
|
|
374
384
|
|
|
375
385
|
/**
|
|
@@ -433,7 +443,8 @@ export async function runDeclarationChecks(
|
|
|
433
443
|
return { file: display(location.file), diagnostics: [diagnostic], suppressed: [], links: [], ok: false };
|
|
434
444
|
}
|
|
435
445
|
const { registry, problems } = loadKindRegistry(declaration.pins, root);
|
|
436
|
-
const
|
|
446
|
+
const gathered = options.gather === false || options.tree ? {} : await gatherWorkspaceFacts(root, declaration, options);
|
|
447
|
+
const facts: WorkspaceFacts = options.records ? { ...gathered, records: options.records } : gathered;
|
|
437
448
|
const ctx: WorkspaceCheckContext = { declaration, tree, groups, kinds: registry, kindProblems: problems, facts };
|
|
438
449
|
const findings = runWorkspaceChecks(ctx);
|
|
439
450
|
const { active, suppressed } = applyCheckSettings(declaration, findings);
|
|
@@ -442,8 +453,7 @@ export async function runDeclarationChecks(
|
|
|
442
453
|
const locate = (pointer: string): TextLocation => (parsed.ok ? parsed.locate(pointer) : { line: 1, column: 1 });
|
|
443
454
|
const file = display(declaration.file);
|
|
444
455
|
const toFinding = (d: WorkspaceDiagnostic): WorkspaceFinding => ({
|
|
445
|
-
file,
|
|
446
|
-
...locate(d.pointer),
|
|
456
|
+
...(d.file !== undefined ? { file: display(relative(root, d.file).split(sep).join("/")), line: 1, column: 1 } : { file, ...locate(d.pointer) }),
|
|
447
457
|
ruleId: d.checkId,
|
|
448
458
|
severity: d.severity,
|
|
449
459
|
message: d.message,
|
|
@@ -7,6 +7,7 @@ import { describe, expect, test } from "vitest";
|
|
|
7
7
|
import { GRAPH_IR_VERSION, type GraphIR } from "../graph-ir";
|
|
8
8
|
import { parseDeclaration } from "./declaration";
|
|
9
9
|
import { composeWorkspaceGraph, readMemberIr, WORKSPACE_GRAPH_VERSION, type ComposedMember } from "./compose-graph";
|
|
10
|
+
import type { LinkTableRow } from "./links";
|
|
10
11
|
|
|
11
12
|
const member = (name: string, dir: string): ComposedMember => ({
|
|
12
13
|
name,
|
|
@@ -136,7 +137,7 @@ describe("the links section (#2539)", () => {
|
|
|
136
137
|
];
|
|
137
138
|
const doc = composeWorkspaceGraph({ name: "acme", root: "/w" }, inputs, { declaration });
|
|
138
139
|
expect(
|
|
139
|
-
doc.links.map((r) => (r.status === "ambiguous" ? "" : `${r.origin} ${r.label} ${r.from ?? r.consumer} -> ${r.to}`)),
|
|
140
|
+
(doc.links as LinkTableRow[]).map((r) => (r.status === "ambiguous" ? "" : `${r.origin} ${r.label} ${r.from ?? r.consumer} -> ${r.to}`)),
|
|
140
141
|
).toEqual([
|
|
141
142
|
"declared exact app -> web/Bucket",
|
|
142
143
|
"inferred:joinKey folded jobs/BUCKET_ARN -> web/Bucket",
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* - `links` holds the member links (#2524 D6, #2539): the declared links and
|
|
17
17
|
* the joins inferred from members' `imports` and `exports` with the core
|
|
18
18
|
* `joinKey()`, labelled `exact` or `folded` (`./links.ts`). A declared link
|
|
19
|
-
* suppresses the inferred edge it covers. `records`
|
|
20
|
-
*
|
|
19
|
+
* suppresses the inferred edge it covers. `records` holds the records
|
|
20
|
+
* read through a record kind (`--kind`, #2549), and their `asset` and
|
|
21
|
+
* `constrains` links follow the member links in `links`.
|
|
21
22
|
*
|
|
22
23
|
* A member's IR with no `version` field comes from a chant older than #2529.
|
|
23
24
|
* It is version 1, and it is upgraded in place by stamping that version. An IR
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
import type { Declaration } from "./declaration";
|
|
32
33
|
import type { KindRegistry } from "./kinds";
|
|
33
34
|
import { graphLinks, type LinkTableRow } from "./links";
|
|
35
|
+
import type { RecordLinkRow } from "./record-assets";
|
|
34
36
|
import type { ReasonCode } from "./reason-codes";
|
|
35
37
|
import { GRAPH_IR_VERSION, type GraphIR, type IRExport, type IRGroups, type IRImport, type IREdge, type IRNode } from "../graph-ir";
|
|
36
38
|
|
|
@@ -92,10 +94,25 @@ export interface WorkspaceGraph {
|
|
|
92
94
|
exports: (IRExport & { member: string })[];
|
|
93
95
|
imports: (IRImport & { member: string })[];
|
|
94
96
|
derivedAttrs?: Record<string, string[]>;
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Member links (#2524 D6, #2539), declared and inferred, then the links of
|
|
99
|
+
* the records read with `--kind` (#2549). Empty when no declaration is given.
|
|
100
|
+
*/
|
|
101
|
+
links: (LinkTableRow | RecordLinkRow)[];
|
|
102
|
+
/** The records read with `--kind` (#2524 D4, #2549). Empty without it. */
|
|
103
|
+
records: GraphRecord[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A record in the composed graph: enough to name it from a link row. */
|
|
107
|
+
export interface GraphRecord {
|
|
108
|
+
/** The record kind's name, such as `decision`. */
|
|
109
|
+
kind: string;
|
|
110
|
+
id: string | null;
|
|
111
|
+
/** From the repository root. */
|
|
112
|
+
path: string;
|
|
113
|
+
state: string | null;
|
|
114
|
+
valid: boolean;
|
|
115
|
+
supersededBy: string | null;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
/** Read one member's `chant graph --format ir` output, upgrading an unversioned (v1) IR in place. */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant workspace graph [dir] [--at <rev>] [--member <name>] [
|
|
3
|
-
* [--env <env>] [--dry-run]` (#2537, #2536): every `chant`
|
|
4
|
-
* through the member's own toolchain and composed into one
|
|
5
|
-
* (`compose-graph.ts`).
|
|
2
|
+
* `chant workspace graph [dir] [--at <rev>] [--member <name>] [--kind <kind file>]
|
|
3
|
+
* [-o <file>] [--env <env>] [--dry-run]` (#2537, #2536): every `chant`
|
|
4
|
+
* member's IR, read through the member's own toolchain and composed into one
|
|
5
|
+
* document (`compose-graph.ts`). With `--kind`, the records of that kind and
|
|
6
|
+
* their asset and constrains links join it (#2549, `record-assets.ts`).
|
|
6
7
|
*
|
|
7
8
|
* The document is part of the read contract, described by `graph.schema.json`
|
|
8
9
|
* beside this file. It is printed for a failure too, with the error's reason
|
|
@@ -28,6 +29,8 @@ import { composeWorkspaceGraph, readMemberIr, type ComposeInput, type WorkspaceG
|
|
|
28
29
|
import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type ErrorLocation, type WorkspaceErrorCode } from "./declaration";
|
|
29
30
|
import { describePlan, emitDocument, executePlan, memberStatus, planJson, planMembers, type MemberPlan, type Toolchain, type UnitResult } from "./member-commands";
|
|
30
31
|
import { loadKindRegistry } from "./kinds";
|
|
32
|
+
import { recordLinkRows } from "./record-assets";
|
|
33
|
+
import { RecordReadError } from "./records";
|
|
31
34
|
import { workingTree } from "./tree";
|
|
32
35
|
import { handToRootChant, locateWorkspace, type LocatedWorkspace } from "./which-chant";
|
|
33
36
|
|
|
@@ -40,7 +43,7 @@ export const GRAPH_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/worksp
|
|
|
40
43
|
/** Why the graph couldn't be read at all: the declaration's codes, `--at`'s included. */
|
|
41
44
|
export const GRAPH_ERROR_CODES = WORKSPACE_ERROR_CODES;
|
|
42
45
|
|
|
43
|
-
const USAGE = "chant workspace graph [dir] [--at <rev>] [--member <name>] [-o <file>] [--env <env>] [--dry-run]";
|
|
46
|
+
const USAGE = "chant workspace graph [dir] [--at <rev>] [--member <name>] [--kind <kind file>] [-o <file>] [--env <env>] [--dry-run]";
|
|
44
47
|
|
|
45
48
|
interface Head {
|
|
46
49
|
$schema: string;
|
|
@@ -64,6 +67,12 @@ export interface GraphQuery {
|
|
|
64
67
|
reader?: Toolchain;
|
|
65
68
|
/** Called with each member's stderr, so the command can pass it on. */
|
|
66
69
|
onStderr?: (text: string) => void;
|
|
70
|
+
/**
|
|
71
|
+
* A record kind file, absolute or relative to `cwd` (#2549). Its records
|
|
72
|
+
* fill `records`, and their asset pins and `constrains` entries become
|
|
73
|
+
* rows of `links`.
|
|
74
|
+
*/
|
|
75
|
+
kind?: string;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
export interface GraphResult {
|
|
@@ -171,7 +180,23 @@ export async function workspaceGraph(query: GraphQuery): Promise<GraphResult> {
|
|
|
171
180
|
// Links (#2539) resolve against the declaration that was read, the revision's for --at, and the kinds installed now.
|
|
172
181
|
const kinds = loadKindRegistry(declaration.pins, located.rootOnDisk).registry;
|
|
173
182
|
const graph = composeWorkspaceGraph({ name: declaration.name, root: located.root }, inputs, { declaration, kinds });
|
|
174
|
-
|
|
183
|
+
let recordsFailed = false;
|
|
184
|
+
if (query.kind !== undefined) {
|
|
185
|
+
// Artifact relationships come from records (#2549): a record's pins and
|
|
186
|
+
// what it constrains, read at the same revision as the declaration.
|
|
187
|
+
const { readRecordsFor } = await import("./records-cli");
|
|
188
|
+
try {
|
|
189
|
+
const read = await readRecordsFor({ kind: query.kind, cwd: query.cwd, at: query.at });
|
|
190
|
+
const name = read.loaded.kind.name;
|
|
191
|
+
graph.records = read.result.records.map((r) => ({ kind: name, id: r.id, path: r.path, state: r.state, valid: r.valid, supersededBy: r.supersededBy }));
|
|
192
|
+
graph.links.push(...recordLinkRows(name, read.result.records, read.loaded.kind.constrains?.field, located.tree, declaration.members));
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (!(err instanceof RecordReadError)) throw err;
|
|
195
|
+
recordsFailed = true;
|
|
196
|
+
query.onStderr?.(`${formatError({ message: `--kind ${query.kind}: ${err.code}: ${err.message}`, hint: USAGE })}\n`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { doc: { ...head, at: located.at, ...graph }, failed: failed || recordsFailed };
|
|
175
200
|
} catch (err) {
|
|
176
201
|
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
177
202
|
return { doc: { ...head, error: { code: err.code, message: err.message, location: err.location ?? null } }, failed: true };
|
|
@@ -209,6 +234,7 @@ export async function runWorkspaceGraph(ctx: CommandContext): Promise<number> {
|
|
|
209
234
|
at: args.at,
|
|
210
235
|
members: args.members,
|
|
211
236
|
args,
|
|
237
|
+
...(args.kind !== undefined ? { kind: resolve(args.kind) } : {}),
|
|
212
238
|
onStderr: (text) => process.stderr.write(text),
|
|
213
239
|
});
|
|
214
240
|
emitDocument(doc, args.output);
|
|
@@ -18,6 +18,7 @@ import { cleanScratch, commitAll, contract, declaration, declaration as declarat
|
|
|
18
18
|
import type { GraphIR } from "../graph-ir";
|
|
19
19
|
import { composeWorkspaceGraph, MEMBER_RUN_REASON_CODES } from "./compose-graph";
|
|
20
20
|
import { parseDeclaration, WORKSPACE_ERROR_CODES } from "./declaration";
|
|
21
|
+
import type { LinkTableRow } from "./links";
|
|
21
22
|
import { GRAPH_CONTRACT_VERSION, GRAPH_ERROR_CODES, GRAPH_OUTPUT_SCHEMA_ID, workspaceGraph, type GraphDocument } from "./graph-cli";
|
|
22
23
|
import schema from "./graph.schema.json";
|
|
23
24
|
|
|
@@ -101,7 +102,7 @@ describe("the links section (#2539)", () => {
|
|
|
101
102
|
);
|
|
102
103
|
const doc = { $schema: GRAPH_OUTPUT_SCHEMA_ID, contract: 1, chant: "0.81.0", at: null, ...graph };
|
|
103
104
|
expectValid(doc);
|
|
104
|
-
expect(doc.links.map((r) => `${r.consumer} ${r.origin} ${r.status}`)).toEqual(["app declared resolved", "app declared missing", "jobs inferred:joinKey ambiguous"]);
|
|
105
|
+
expect((doc.links as LinkTableRow[]).map((r) => `${r.consumer} ${r.origin} ${r.status}`)).toEqual(["app declared resolved", "app declared missing", "jobs inferred:joinKey ambiguous"]);
|
|
105
106
|
});
|
|
106
107
|
});
|
|
107
108
|
|
|
@@ -218,14 +218,17 @@
|
|
|
218
218
|
},
|
|
219
219
|
"links": {
|
|
220
220
|
"type": "array",
|
|
221
|
-
"description": "Member links (#2524 D6, #2539): the ones declared on a consumer and the ones inferred from join keys, resolved against the composed graph. A reader draws a row from its `from` node to its `to` node when both are set.",
|
|
221
|
+
"description": "Member links (#2524 D6, #2539): the ones declared on a consumer and the ones inferred from join keys, resolved against the composed graph. A reader draws a row from its `from` node to its `to` node when both are set. With --kind, the links of the records read follow (#2549): an `asset` row for each file a record pins by hash, and a `constrains` row for each member or path it governs. These rows have `record` and `target` instead of `consumer` and `producer`.",
|
|
222
222
|
"items": {
|
|
223
223
|
"$ref": "#/$defs/link"
|
|
224
224
|
}
|
|
225
225
|
},
|
|
226
226
|
"records": {
|
|
227
227
|
"type": "array",
|
|
228
|
-
"description": "
|
|
228
|
+
"description": "The records read with --kind (#2524 D4, #2549), sorted by path; empty without it. A record link in `links` names its record by id.",
|
|
229
|
+
"items": {
|
|
230
|
+
"$ref": "#/$defs/record"
|
|
231
|
+
}
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
234
|
},
|
|
@@ -655,8 +658,134 @@
|
|
|
655
658
|
}
|
|
656
659
|
}
|
|
657
660
|
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"type": "object",
|
|
664
|
+
"description": "A record link (#2549): from a current record to a file it pins (kind asset) or to a member or path it constrains (kind constrains). Superseded records have none. A reader finds the artifacts behind a file by taking the decisions whose constrains cover it and then their asset rows.",
|
|
665
|
+
"required": [
|
|
666
|
+
"kind",
|
|
667
|
+
"origin",
|
|
668
|
+
"resolves",
|
|
669
|
+
"recordKind",
|
|
670
|
+
"record",
|
|
671
|
+
"recordPath",
|
|
672
|
+
"target",
|
|
673
|
+
"member",
|
|
674
|
+
"status",
|
|
675
|
+
"reason"
|
|
676
|
+
],
|
|
677
|
+
"properties": {
|
|
678
|
+
"kind": {
|
|
679
|
+
"enum": [
|
|
680
|
+
"asset",
|
|
681
|
+
"constrains"
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
"origin": {
|
|
685
|
+
"const": "declared"
|
|
686
|
+
},
|
|
687
|
+
"resolves": {
|
|
688
|
+
"const": "source"
|
|
689
|
+
},
|
|
690
|
+
"recordKind": {
|
|
691
|
+
"type": "string",
|
|
692
|
+
"description": "The record's kind, such as decision."
|
|
693
|
+
},
|
|
694
|
+
"record": {
|
|
695
|
+
"type": "string",
|
|
696
|
+
"description": "The record's id."
|
|
697
|
+
},
|
|
698
|
+
"recordPath": {
|
|
699
|
+
"type": "string",
|
|
700
|
+
"description": "The record file, from the repository root."
|
|
701
|
+
},
|
|
702
|
+
"target": {
|
|
703
|
+
"type": "string",
|
|
704
|
+
"description": "For asset, the pinned path from the workspace root. For constrains, the entry as written: member:<name> or path:<path>."
|
|
705
|
+
},
|
|
706
|
+
"member": {
|
|
707
|
+
"type": [
|
|
708
|
+
"string",
|
|
709
|
+
"null"
|
|
710
|
+
],
|
|
711
|
+
"description": "The member the target names or sits in; null when no member does."
|
|
712
|
+
},
|
|
713
|
+
"status": {
|
|
714
|
+
"enum": [
|
|
715
|
+
"pinned",
|
|
716
|
+
"drifted",
|
|
717
|
+
"missing",
|
|
718
|
+
"stale",
|
|
719
|
+
"resolved"
|
|
720
|
+
],
|
|
721
|
+
"description": "For asset: pinned, drifted, missing or stale, as records reports the pin. For constrains: resolved, or missing when the member is not declared or the path does not exist."
|
|
722
|
+
},
|
|
723
|
+
"reason": {
|
|
724
|
+
"type": [
|
|
725
|
+
"string",
|
|
726
|
+
"null"
|
|
727
|
+
],
|
|
728
|
+
"description": "Why the row is not pinned or resolved, or null."
|
|
729
|
+
},
|
|
730
|
+
"sha256": {
|
|
731
|
+
"type": "string",
|
|
732
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
733
|
+
"description": "For asset: the pinned hash."
|
|
734
|
+
},
|
|
735
|
+
"actual": {
|
|
736
|
+
"type": [
|
|
737
|
+
"string",
|
|
738
|
+
"null"
|
|
739
|
+
],
|
|
740
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
741
|
+
"description": "For asset: the hash of the file in the tree read, or null when it is missing."
|
|
742
|
+
}
|
|
743
|
+
}
|
|
658
744
|
}
|
|
659
745
|
]
|
|
746
|
+
},
|
|
747
|
+
"record": {
|
|
748
|
+
"type": "object",
|
|
749
|
+
"required": [
|
|
750
|
+
"kind",
|
|
751
|
+
"id",
|
|
752
|
+
"path",
|
|
753
|
+
"state",
|
|
754
|
+
"valid",
|
|
755
|
+
"supersededBy"
|
|
756
|
+
],
|
|
757
|
+
"properties": {
|
|
758
|
+
"kind": {
|
|
759
|
+
"type": "string",
|
|
760
|
+
"description": "The record kind's name, such as decision."
|
|
761
|
+
},
|
|
762
|
+
"id": {
|
|
763
|
+
"type": [
|
|
764
|
+
"string",
|
|
765
|
+
"null"
|
|
766
|
+
]
|
|
767
|
+
},
|
|
768
|
+
"path": {
|
|
769
|
+
"type": "string",
|
|
770
|
+
"description": "The record file, from the repository root, with / separators."
|
|
771
|
+
},
|
|
772
|
+
"state": {
|
|
773
|
+
"type": [
|
|
774
|
+
"string",
|
|
775
|
+
"null"
|
|
776
|
+
]
|
|
777
|
+
},
|
|
778
|
+
"valid": {
|
|
779
|
+
"type": "boolean",
|
|
780
|
+
"description": "As chant workspace records reports it."
|
|
781
|
+
},
|
|
782
|
+
"supersededBy": {
|
|
783
|
+
"type": [
|
|
784
|
+
"string",
|
|
785
|
+
"null"
|
|
786
|
+
]
|
|
787
|
+
}
|
|
788
|
+
}
|
|
660
789
|
}
|
|
661
790
|
}
|
|
662
791
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated]`:
|
|
2
|
+
* `chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]`:
|
|
3
3
|
* the lineage checks (#2550, D9), and the declaration checks (#2535, D16)
|
|
4
4
|
* when a declaration sits between the current directory and the git root.
|
|
5
5
|
*
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* check. The declaration checks report `WSP` findings through lint's
|
|
11
11
|
* reporters (`./checks.ts`), and cover member ledgers (#2538), recorded
|
|
12
12
|
* pipelines (#2542) and generated files (#2541) as well as the declaration
|
|
13
|
-
* itself (#2641). `--generated` runs each declared generator too.
|
|
13
|
+
* itself (#2641). `--generated` runs each declared generator too. `--kind`
|
|
14
|
+
* reads a record kind's records and reports the files they pin by hash that
|
|
15
|
+
* have changed since (#2549).
|
|
14
16
|
*
|
|
15
17
|
* `chant workspace upgrade` runs the same checks in its staging worktree
|
|
16
18
|
* before it reaches its gate.
|
|
@@ -23,6 +25,7 @@ import type { CommandContext } from "../cli/registry";
|
|
|
23
25
|
import type { LintDiagnostic, LintRule } from "../lint/rule";
|
|
24
26
|
import { findWorkspaceRoot } from "../project-root";
|
|
25
27
|
import type { DeclarationCheckReport } from "./checks";
|
|
28
|
+
import type { RecordFacts } from "./checks/records";
|
|
26
29
|
import { LOCK_FILE, LockError, parseLock, readLock } from "./lineage-lock";
|
|
27
30
|
import type { ReasonCode } from "./reason-codes";
|
|
28
31
|
import type { WorkspaceTree } from "./tree";
|
|
@@ -81,7 +84,7 @@ export function findingKey(f: CheckFinding): string {
|
|
|
81
84
|
return `${f.code}\0${f.scope ?? ""}\0${f.path ?? ""}`;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
const USAGE = "chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated]";
|
|
87
|
+
const USAGE = "chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]";
|
|
85
88
|
const FORMATS = ["stylish", "json", "sarif"] as const;
|
|
86
89
|
|
|
87
90
|
/** A lock finding as a lint diagnostic, for `--format json` and `--format sarif`. */
|
|
@@ -142,7 +145,7 @@ export type CheckDocument =
|
|
|
142
145
|
* to `cwd`. `runGenerators` is `--generated`; with `at` it does nothing,
|
|
143
146
|
* since the member facts describe the working tree, not the revision.
|
|
144
147
|
*/
|
|
145
|
-
export async function runChecks(cwd: string, at?: string, options: { runGenerators?: boolean } = {}): Promise<CheckDocument> {
|
|
148
|
+
export async function runChecks(cwd: string, at?: string, options: { runGenerators?: boolean; kind?: string } = {}): Promise<CheckDocument> {
|
|
146
149
|
// Loaded here, not at the top: `workspace upgrade` imports this module for checkLineage alone.
|
|
147
150
|
const [{ findDeclarationDir, readDeclaration, readerVersion }, { gitTop, gitTree, resolveCommit, workingTree }] = await Promise.all([
|
|
148
151
|
import("./declaration"),
|
|
@@ -184,9 +187,11 @@ export async function runChecks(cwd: string, at?: string, options: { runGenerato
|
|
|
184
187
|
if (found) {
|
|
185
188
|
const { runDeclarationChecks } = await import("./checks");
|
|
186
189
|
const { dir, tree } = found;
|
|
190
|
+
const records = options.kind !== undefined ? await readRecordFacts(options.kind, cwd, commit) : undefined;
|
|
187
191
|
declaration = await runDeclarationChecks(dir, (file) => relative(cwd, join(dir, file)).split(sep).join("/"), {
|
|
188
192
|
runGenerators: options.runGenerators === true,
|
|
189
193
|
...(commit !== null ? { tree } : {}),
|
|
194
|
+
...(records ? { records } : {}),
|
|
190
195
|
});
|
|
191
196
|
let name: string | null = null;
|
|
192
197
|
try {
|
|
@@ -200,6 +205,18 @@ export async function runChecks(cwd: string, at?: string, options: { runGenerato
|
|
|
200
205
|
return { ...head, at: commit, workspace, ...report, ok, ...(declaration ? { declaration } : {}) };
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
/** The records of the kind file `kind` for the record checks (#2549), at `commit` or in the working tree. */
|
|
209
|
+
async function readRecordFacts(kind: string, cwd: string, commit: string | null): Promise<RecordFacts> {
|
|
210
|
+
const [{ readRecordsFor }, { RecordReadError }] = await Promise.all([import("./records-cli"), import("./records")]);
|
|
211
|
+
try {
|
|
212
|
+
const read = await readRecordsFor({ kind, cwd, ...(commit !== null ? { at: commit } : {}) });
|
|
213
|
+
return { kind, records: read.result.records.map((r) => ({ ...r, file: join(read.root, ...r.path.split("/")) })) };
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (!(err instanceof RecordReadError)) throw err;
|
|
216
|
+
return { kind, error: { code: err.code, message: err.message } };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
203
220
|
export async function runWorkspaceCheck(ctx: CommandContext): Promise<number> {
|
|
204
221
|
const root = process.cwd();
|
|
205
222
|
if (ctx.args.extraPositional) {
|
|
@@ -218,7 +235,7 @@ export async function runWorkspaceCheck(ctx: CommandContext): Promise<number> {
|
|
|
218
235
|
const handed = await handToRootChant(root, ctx.args.at);
|
|
219
236
|
if (handed !== undefined) return handed;
|
|
220
237
|
}
|
|
221
|
-
const doc = await runChecks(root, ctx.args.at, { runGenerators: ctx.args.generated === true });
|
|
238
|
+
const doc = await runChecks(root, ctx.args.at, { runGenerators: ctx.args.generated === true, ...(ctx.args.kind !== undefined ? { kind: ctx.args.kind } : {}) });
|
|
222
239
|
if ("error" in doc) {
|
|
223
240
|
if (ctx.args.json || format === "json") console.log(JSON.stringify(doc, null, 2));
|
|
224
241
|
else console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { formatError, formatSuccess } from "../cli/format";
|
|
13
13
|
import type { CommandContext } from "../cli/registry";
|
|
14
|
-
import { LOCK_FILE, LockError, readLock, resolveManualStep, scopeStatus, writeLock, type ManualStep } from "./lineage-lock";
|
|
14
|
+
import { LOCK_FILE, LockError, readLock, resolveManualStep, scopeStatus, writeLock, type LineageSource, type ManualStep } from "./lineage-lock";
|
|
15
15
|
|
|
16
16
|
const USAGE = "chant workspace lineage [--json] | chant workspace lineage resolve <path>";
|
|
17
17
|
|
|
@@ -20,6 +20,8 @@ export interface LineageScopeView {
|
|
|
20
20
|
kind: "template" | "vendor";
|
|
21
21
|
name?: string;
|
|
22
22
|
template: string;
|
|
23
|
+
/** Where the files came from, as the lock records it: its `type` and the repository, directory, lexicon or URL. */
|
|
24
|
+
source: LineageSource;
|
|
23
25
|
ref?: string;
|
|
24
26
|
address: Record<string, unknown> | null;
|
|
25
27
|
files: number;
|
|
@@ -28,6 +30,22 @@ export interface LineageScopeView {
|
|
|
28
30
|
manualSteps: ManualStep[];
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** One line naming a scope's source kind and where it points (#2647). */
|
|
34
|
+
export function describeSource(source: LineageSource): string {
|
|
35
|
+
switch (source.type) {
|
|
36
|
+
case "git":
|
|
37
|
+
return `git ${source.repo}${source.path ? `#${source.path}` : ""}`;
|
|
38
|
+
case "dir":
|
|
39
|
+
return `dir ${source.path}${source.member ? `#${source.member}` : ""}`;
|
|
40
|
+
case "lexicon":
|
|
41
|
+
return `lexicon ${source.lexicon}/${source.template}`;
|
|
42
|
+
case "local":
|
|
43
|
+
return `local ${source.path}`;
|
|
44
|
+
case "archive":
|
|
45
|
+
return `archive ${source.url}${source.subpath ? `#${source.subpath}` : ""}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
/** The lock as `--json` prints it: one view per scope, with its state in the tree. */
|
|
32
50
|
export function lineageView(root: string): { lock: string; scopes: LineageScopeView[] } | null {
|
|
33
51
|
const lock = readLock(root);
|
|
@@ -39,6 +57,7 @@ export function lineageView(root: string): { lock: string; scopes: LineageScopeV
|
|
|
39
57
|
kind: lineage.kind,
|
|
40
58
|
...(lineage.name !== undefined ? { name: lineage.name } : {}),
|
|
41
59
|
template: lineage.template,
|
|
60
|
+
source: lineage.source,
|
|
42
61
|
...(lineage.ref !== undefined ? { ref: lineage.ref } : {}),
|
|
43
62
|
address: lineage.address,
|
|
44
63
|
files: Object.keys(lineage.files).length,
|
|
@@ -73,6 +92,7 @@ export async function runWorkspaceLineage(ctx: CommandContext): Promise<number>
|
|
|
73
92
|
const pin = s.ref ? `@${s.ref}` : "";
|
|
74
93
|
const at = typeof s.address?.commit === "string" ? ` (${s.address.commit.slice(0, 12)})` : "";
|
|
75
94
|
lines.push(`${s.scope} ${s.kind}${s.name ? ` ${s.name}` : ""} ${s.template}${pin}${at}`);
|
|
95
|
+
lines.push(` source: ${describeSource(s.source)}`);
|
|
76
96
|
lines.push(` ${s.files} file(s), ${s.edited.length} edited, ${s.missing.length} deleted`);
|
|
77
97
|
for (const m of s.manualSteps) lines.push(` manual step: ${s.scope === "." ? "" : `${s.scope}/`}${m.path} (${m.reason})`);
|
|
78
98
|
}
|