@intentius/chant 0.82.0 → 0.84.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 +4 -0
- package/dist/cli/registry.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 +15 -4
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +17 -0
- package/dist/workspace/intent-cli.d.ts.map +1 -0
- package/dist/workspace/intent-joins.d.ts +93 -0
- package/dist/workspace/intent-joins.d.ts.map +1 -0
- package/dist/workspace/intent.d.ts +285 -0
- package/dist/workspace/intent.d.ts.map +1 -0
- package/dist/workspace/lineage-check.d.ts +5 -2
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-init.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +8 -0
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +19 -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 +47 -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/main.test.ts +11 -0
- package/src/cli/main.ts +22 -6
- package/src/cli/registry.ts +4 -0
- package/src/content-digest.ts +5 -0
- 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 +40 -6
- package/src/workspace/graph-contract.test.ts +2 -1
- package/src/workspace/graph.schema.json +131 -2
- package/src/workspace/intent-cli.ts +95 -0
- package/src/workspace/intent-joins.ts +165 -0
- package/src/workspace/intent.schema.json +1078 -0
- package/src/workspace/intent.test.ts +448 -0
- package/src/workspace/intent.ts +941 -0
- package/src/workspace/lineage-check.ts +22 -5
- package/src/workspace/lineage-init.ts +5 -1
- package/src/workspace/lineage-lock.ts +7 -0
- package/src/workspace/lineage-upgrade.test.ts +29 -0
- package/src/workspace/lineage-upgrade.ts +21 -3
- package/src/workspace/member-commands.ts +1 -1
- package/src/workspace/read-contract.test.ts +16 -1
- package/src/workspace/reason-codes.test.ts +7 -2
- package/src/workspace/reason-codes.ts +23 -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 +116 -6
- package/src/workspace/template-pins.test.ts +69 -0
- package/src/workspace/template-pins.ts +117 -0
- package/src/workspace/tree.ts +12 -0
|
@@ -16,13 +16,25 @@
|
|
|
16
16
|
* runs. The level-0 goldens (#2526) fail if a level-0 command loads it.
|
|
17
17
|
*/
|
|
18
18
|
import { z } from "zod";
|
|
19
|
+
import { type AssetPin } from "./record-assets.js";
|
|
19
20
|
import type { RecordSource } from "./record-source.js";
|
|
21
|
+
import type { WorkspaceTree } from "./tree.js";
|
|
20
22
|
/**
|
|
21
23
|
* Why one record is not valid. The list is closed: a reader may switch on it,
|
|
22
24
|
* and a new code is a contract change (#2536).
|
|
23
25
|
*/
|
|
24
26
|
export declare const RECORD_REASON_CODES: readonly ["record-unparseable", "record-schema-invalid", "record-id-duplicate", "record-supersedes-unknown", "record-supersedes-conflict"];
|
|
25
27
|
export type RecordReasonCode = (typeof RECORD_REASON_CODES)[number];
|
|
28
|
+
/**
|
|
29
|
+
* Why a record carries a warning. Closed, like the reason codes. A warning
|
|
30
|
+
* never makes a record invalid, and `--current` still lists the record.
|
|
31
|
+
*/
|
|
32
|
+
export declare const RECORD_WARNING_CODES: readonly ["asset-drift", "asset-missing", "asset-stale", "record-supersedes-pending"];
|
|
33
|
+
export type RecordWarningCode = (typeof RECORD_WARNING_CODES)[number];
|
|
34
|
+
export interface RecordWarning {
|
|
35
|
+
code: RecordWarningCode;
|
|
36
|
+
message: string;
|
|
37
|
+
}
|
|
26
38
|
/**
|
|
27
39
|
* Why the read as a whole failed. Also closed. The command exits 1 with one of
|
|
28
40
|
* these and returns no records.
|
|
@@ -53,6 +65,13 @@ export declare const recordKindSchema: z.ZodObject<{
|
|
|
53
65
|
field: z.ZodString;
|
|
54
66
|
key: z.ZodString;
|
|
55
67
|
}, z.core.$strict>;
|
|
68
|
+
approval: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
69
|
+
pins: z.ZodOptional<z.ZodObject<{
|
|
70
|
+
field: z.ZodString;
|
|
71
|
+
}, z.core.$strict>>;
|
|
72
|
+
constrains: z.ZodOptional<z.ZodObject<{
|
|
73
|
+
field: z.ZodString;
|
|
74
|
+
}, z.core.$strict>>;
|
|
56
75
|
}, z.core.$strict>;
|
|
57
76
|
export type RecordKind = z.infer<typeof recordKindSchema>;
|
|
58
77
|
/** A kind as loaded: its data, where it came from, and its schema. */
|
|
@@ -64,6 +83,12 @@ export interface LoadedRecordKind {
|
|
|
64
83
|
dir: string;
|
|
65
84
|
schema: Record<string, unknown>;
|
|
66
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Import the kind file at `path` through the path loader lexicons use (#2520).
|
|
88
|
+
* The kind is registered under a name no lexicon package can have (npm names
|
|
89
|
+
* hold no `:`), imported and unregistered again, so no lexicon lookup sees it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function importKindModule(path: string): Promise<Record<string, unknown>>;
|
|
67
92
|
/** Load a kind file and the schema it names. `path` is resolved against `cwd`. */
|
|
68
93
|
export declare function loadRecordKind(path: string, cwd?: string): Promise<LoadedRecordKind>;
|
|
69
94
|
export type FrontMatter = {
|
|
@@ -96,6 +121,10 @@ export interface RecordEntry {
|
|
|
96
121
|
supersededBy: string | null;
|
|
97
122
|
/** The front matter as JSON, or null when it could not be parsed. */
|
|
98
123
|
data: Record<string, unknown> | null;
|
|
124
|
+
/** Each workspace file the record pins, checked against the tree read (#2549). Empty when nothing was checked. */
|
|
125
|
+
assets: AssetPin[];
|
|
126
|
+
/** Findings that leave the record valid, such as a pinned file that changed (#2549). */
|
|
127
|
+
warnings: RecordWarning[];
|
|
99
128
|
}
|
|
100
129
|
export interface ReadRecordsOptions {
|
|
101
130
|
/** Where record paths are reported from, and what `source` reads relative to. */
|
|
@@ -103,6 +132,24 @@ export interface ReadRecordsOptions {
|
|
|
103
132
|
source: RecordSource;
|
|
104
133
|
/** Leave out records a closed record supersedes. */
|
|
105
134
|
current?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* The workspace root the kind's pins resolve in: the working tree, or the
|
|
137
|
+
* revision read (#2549). Without it no pin is checked.
|
|
138
|
+
*/
|
|
139
|
+
assets?: WorkspaceTree;
|
|
140
|
+
/**
|
|
141
|
+
* When files last changed and records were recorded, in the history of the
|
|
142
|
+
* revision read, for `asset-stale` (#2549). Without it only the hashes are
|
|
143
|
+
* compared.
|
|
144
|
+
*/
|
|
145
|
+
history?: RecordHistory;
|
|
146
|
+
}
|
|
147
|
+
/** Commit times, in seconds since the epoch, read from git. */
|
|
148
|
+
export interface RecordHistory {
|
|
149
|
+
/** The last commit that changed `path` (from the workspace root), or null when unknown. */
|
|
150
|
+
fileChanged(path: string): number | null;
|
|
151
|
+
/** The commit that added the record at `path` (from the repository root), or null when it is not committed. */
|
|
152
|
+
recorded(path: string): number | null;
|
|
106
153
|
}
|
|
107
154
|
export interface ReadRecordsResult {
|
|
108
155
|
records: RecordEntry[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../src/workspace/records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../src/workspace/records.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-pinning records after template parameters are substituted (#2549, #2627).
|
|
3
|
+
*
|
|
4
|
+
* A template's decision records may pin a file of the template by hash. When
|
|
5
|
+
* `chant init --from` or `chant workspace upgrade` fills that file's
|
|
6
|
+
* `{{chant:<name>}}` placeholders, the copy's bytes differ from the
|
|
7
|
+
* template's, and every copy would report the pin as drifted on day one. So
|
|
8
|
+
* after substitution, each pin that held in the template and names a
|
|
9
|
+
* substituted file gets the hash of the substituted content.
|
|
10
|
+
*
|
|
11
|
+
* A record is any Markdown file whose front matter has an `evidence` list
|
|
12
|
+
* with path pins. Its paths resolve from the nearest directory above it that
|
|
13
|
+
* holds a workspace declaration, as `chant workspace records` resolves them.
|
|
14
|
+
* A pin that did not hold in the template stays as it is: re-pinning follows
|
|
15
|
+
* the substitution and never hides drift the template already had. Only the
|
|
16
|
+
* `sha256` value on the pin's line changes; the rest of the file is kept byte
|
|
17
|
+
* for byte.
|
|
18
|
+
*/
|
|
19
|
+
/** A record whose pins were rewritten: its path in the template, and the pinned paths, from its workspace root. */
|
|
20
|
+
export interface RepinnedRecord {
|
|
21
|
+
record: string;
|
|
22
|
+
paths: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Rewrite, in `substituted`, the pins of every record that name a file in
|
|
26
|
+
* `changed` and held against `original`. Returns the files with the records
|
|
27
|
+
* rewritten, and which records were.
|
|
28
|
+
*/
|
|
29
|
+
export declare function repinSubstituted(original: ReadonlyMap<string, Buffer>, substituted: ReadonlyMap<string, Buffer>, changed: readonly string[]): {
|
|
30
|
+
files: Map<string, Buffer>;
|
|
31
|
+
repinned: RepinnedRecord[];
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=template-pins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-pins.d.ts","sourceRoot":"","sources":["../../src/workspace/template-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,mHAAmH;AACnH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAYD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB;IAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,cAAc,EAAE,CAAA;CAAE,CA+B5D"}
|
package/dist/workspace/tree.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface WorkspaceTree {
|
|
|
17
17
|
}[] | undefined;
|
|
18
18
|
/** The text of the file at `path`. Throws when it can't be read. */
|
|
19
19
|
read(path: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The bytes of the file at `path`, for hashing (#2549). Throws when it can't
|
|
22
|
+
* be read. A tree without it is read as UTF-8 text.
|
|
23
|
+
*/
|
|
24
|
+
bytes?(path: string): Uint8Array;
|
|
20
25
|
}
|
|
21
26
|
/** Join tree-relative path parts, leaving out `""` and `"."`. */
|
|
22
27
|
export declare function joinPath(...parts: string[]): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;IAC/C,kGAAkG;IAClG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,EAAE,GAAG,SAAS,CAAC;IACzE,oEAAoE;IACpE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;IAC/C,kGAAkG;IAClG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,EAAE,GAAG,SAAS,CAAC;IACzE,oEAAoE;IACpE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B;;;OAGG;IACH,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;CAClC;AAED,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAEnD;AAED,qCAAqC;AACrC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAwCvD;AAMD,6EAA6E;AAC7E,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMtD;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO1E;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,aAAa,CAmC/E;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD"}
|
package/package.json
CHANGED
package/src/cli/main.test.ts
CHANGED
|
@@ -688,6 +688,17 @@ describe("workspace records (#2546)", () => {
|
|
|
688
688
|
});
|
|
689
689
|
});
|
|
690
690
|
|
|
691
|
+
describe("workspace graph --intent (#2651)", () => {
|
|
692
|
+
test("parses the region and every --kind, in order", () => {
|
|
693
|
+
const args = parseArgs(["workspace", "graph", "--intent", "app/server.mjs:3-7", "--kind", "a.kind.mjs", "--kind", "b.kind.mjs", "--at", "HEAD", "--json"]);
|
|
694
|
+
expect(args).toMatchObject({ command: "workspace", path: "graph", intent: "app/server.mjs:3-7", kinds: ["a.kind.mjs", "b.kind.mjs"], kind: "b.kind.mjs", at: "HEAD", json: true });
|
|
695
|
+
expect(parseArgs(["workspace", "graph", "--intent=app"]).intent).toBe("app");
|
|
696
|
+
expect(() => parseArgs(["workspace", "graph", "--intent"])).toThrow(/--intent needs a region/);
|
|
697
|
+
expect(() => parseArgs(["workspace", "graph", "--intent", "--json"])).toThrow(/--intent needs a region/);
|
|
698
|
+
expect(resolveCommand(args, commandRegistry)?.def.name).toBe("workspace graph");
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
691
702
|
describe("workspace init and ls (#2534)", () => {
|
|
692
703
|
test("resolve to their commands in the one workspace group, with the directory as extra positional", () => {
|
|
693
704
|
const ls = parseArgs(["workspace", "ls", "examples", "--at", "HEAD", "--json"]);
|
package/src/cli/main.ts
CHANGED
|
@@ -360,9 +360,15 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
360
360
|
} else if (arg === "--at") {
|
|
361
361
|
result.at = args[++i];
|
|
362
362
|
} else if (arg === "--kind") {
|
|
363
|
-
// `chant workspace records --kind <kind file>` (#2546)
|
|
363
|
+
// `chant workspace records|graph|check --kind <kind file>` (#2546, #2549)
|
|
364
364
|
result.kind = args[++i];
|
|
365
365
|
if (!result.kind || result.kind.startsWith("-")) throw new Error("--kind needs a kind file: --kind <path>");
|
|
366
|
+
// Repeatable for `workspace graph --intent` (#2651); the others read the last one.
|
|
367
|
+
(result.kinds ??= []).push(result.kind);
|
|
368
|
+
} else if (arg === "--intent") {
|
|
369
|
+
// `chant workspace graph --intent <path[:start-end]>` (#2651)
|
|
370
|
+
result.intent = args[++i];
|
|
371
|
+
if (!result.intent || result.intent.startsWith("-")) throw new Error("--intent needs a region: --intent <path[:start-end]>");
|
|
366
372
|
} else if (arg === "--current") {
|
|
367
373
|
result.current = true;
|
|
368
374
|
} else if (arg === "--require") {
|
|
@@ -719,7 +725,11 @@ Workspace (level 1, #2524):
|
|
|
719
725
|
Each record reports its provenance level, judged by
|
|
720
726
|
the signers at --base (default: the target branch);
|
|
721
727
|
--require attested exits 2 if any record is not
|
|
722
|
-
attested
|
|
728
|
+
attested. A pinned file that changed is a warning,
|
|
729
|
+
asset-drift or asset-missing
|
|
730
|
+
workspace records pin <path>
|
|
731
|
+
Print the path from the workspace root and the
|
|
732
|
+
sha256 of a file, for a decision's evidence pin
|
|
723
733
|
workspace verify [--base <rev>] [--head <rev>] [--require attested]
|
|
724
734
|
Check the commits in base..head against the signers
|
|
725
735
|
and roles read from base. A change to the signers file
|
|
@@ -737,12 +747,13 @@ Workspace (level 1, #2524):
|
|
|
737
747
|
lint and workspace check there, then gate on the digest
|
|
738
748
|
of the patch (chant approve workspace-upgrade <scope>).
|
|
739
749
|
A second run with the approval applies the patch
|
|
740
|
-
workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated]
|
|
750
|
+
workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]
|
|
741
751
|
Fail on an unreadable lineage lock or an open manual
|
|
742
752
|
step, and, in a declared workspace, on a WSP check of
|
|
743
753
|
the declaration, member ledgers, pipelines or
|
|
744
754
|
generated files. --generated runs declared generators
|
|
745
|
-
and compares their output.
|
|
755
|
+
and compares their output. --kind warns on records
|
|
756
|
+
whose pinned files changed. Needs no workspace file.
|
|
746
757
|
--at reads a commit's git objects; --format json
|
|
747
758
|
prints the read-contract document
|
|
748
759
|
workspace build [dir] [--member <name>] [-o <dir>] [--dry-run]
|
|
@@ -756,11 +767,16 @@ Workspace (level 1, #2524):
|
|
|
756
767
|
workspace audit [dir] [--json] [--member <name>]
|
|
757
768
|
Audit each chant member with its own .chant-audit.json;
|
|
758
769
|
every finding carries a member field
|
|
759
|
-
workspace graph [dir] [--at <rev>] [--member <name>] [-o <file>]
|
|
770
|
+
workspace graph [dir] [--at <rev>] [--member <name>] [--kind <kind file>] [-o <file>]
|
|
760
771
|
Compose each chant member's chant graph into one IR,
|
|
761
772
|
with <member>/<id> ids and groups.byMember: the
|
|
762
773
|
read-contract document. --at <rev> runs each member's
|
|
763
|
-
source as it was at that commit
|
|
774
|
+
source as it was at that commit; --kind adds the
|
|
775
|
+
records' asset and constrains links
|
|
776
|
+
workspace graph --intent <path[:start-end]> [--at <rev>] [--kind <kind file>...] [--json]
|
|
777
|
+
The intent graph over one region: the commits that
|
|
778
|
+
touched it, the decisions whose constrains cover it,
|
|
779
|
+
the artifacts they pin, and findings with closed codes
|
|
764
780
|
|
|
765
781
|
Lifecycle (alias: lc):
|
|
766
782
|
lifecycle snapshot <env> Query API, save metadata to orphan branch
|
package/src/cli/registry.ts
CHANGED
|
@@ -283,6 +283,10 @@ export interface ParsedArgs {
|
|
|
283
283
|
at?: string;
|
|
284
284
|
/** `chant workspace records --kind <path>` (#2546): the record kind file to read records through. */
|
|
285
285
|
kind?: string;
|
|
286
|
+
/** Every `--kind` given, in order: `chant workspace graph --intent` reads each (#2651). */
|
|
287
|
+
kinds?: string[];
|
|
288
|
+
/** `chant workspace graph --intent <path[:start-end]>` (#2651): the region the intent graph is over. */
|
|
289
|
+
intent?: string;
|
|
286
290
|
/** `chant workspace records --current` (#2546): leave out records a closed record supersedes. */
|
|
287
291
|
current?: boolean;
|
|
288
292
|
/** `chant workspace records|verify --require attested` (#2547): the provenance level a gate requires. */
|
package/src/content-digest.ts
CHANGED
|
@@ -19,3 +19,8 @@ import { createHash } from "node:crypto";
|
|
|
19
19
|
export function contentDigest(input: string): string {
|
|
20
20
|
return `sha256:${createHash("sha256").update(input, "utf8").digest("hex")}`;
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
/** Lowercase hex SHA-256 of `bytes`, with no prefix: the form a decision's evidence pin holds (#2549). */
|
|
24
|
+
export function sha256Hex(bytes: Uint8Array): string {
|
|
25
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
26
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record checks, WSP111 to WSP114 (#2549; #2524 D4, D18).
|
|
3
|
+
*
|
|
4
|
+
* With `--kind <kind file>`, `chant workspace check` reads the records the
|
|
5
|
+
* kind locates, in the tree it checks, and reports the files they pin by hash
|
|
6
|
+
* that have changed or gone since. A decision stays valid when its evidence
|
|
7
|
+
* drifts, so drift is a warning: it asks for the decision to be looked at
|
|
8
|
+
* again, and the declaration's `checks` can raise it to an error.
|
|
9
|
+
*
|
|
10
|
+
* | Id | Finds |
|
|
11
|
+
* |---|---|
|
|
12
|
+
* | WSP111 | a pinned file whose bytes no longer hash to the pinned sha256 |
|
|
13
|
+
* | WSP112 | a pinned file that does not exist |
|
|
14
|
+
* | WSP113 | a pinned file that a superseding record pins at the old hash: the artifact did not follow the decision |
|
|
15
|
+
* | WSP114 | the records of `--kind` can't be read (fixed) |
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { WorkspaceCheck, WorkspaceDiagnostic } from "../checks";
|
|
19
|
+
import type { ReadErrorCode, RecordEntry } from "../records";
|
|
20
|
+
|
|
21
|
+
/** The records `check --kind` read, or why they could not be read. */
|
|
22
|
+
export type RecordFacts =
|
|
23
|
+
| { kind: string; records: readonly (RecordEntry & { file: string })[] }
|
|
24
|
+
| { kind: string; error: { code: ReadErrorCode; message: string } };
|
|
25
|
+
|
|
26
|
+
function warningFindings(check: WorkspaceCheck, facts: RecordFacts | undefined, code: "asset-drift" | "asset-missing" | "asset-stale"): WorkspaceDiagnostic[] {
|
|
27
|
+
if (!facts || "error" in facts) return [];
|
|
28
|
+
const out: WorkspaceDiagnostic[] = [];
|
|
29
|
+
for (const r of facts.records) {
|
|
30
|
+
// A superseded record's evidence no longer backs anything current.
|
|
31
|
+
if (r.supersededBy !== null) continue;
|
|
32
|
+
for (const w of r.warnings) {
|
|
33
|
+
if (w.code !== code) continue;
|
|
34
|
+
out.push({ checkId: check.id, severity: check.severity, message: `${r.id ?? r.path}: ${w.message}`, pointer: "", file: r.file });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const RECORD_CHECKS: readonly WorkspaceCheck[] = [
|
|
41
|
+
{
|
|
42
|
+
id: "WSP111",
|
|
43
|
+
name: "record-asset-drift",
|
|
44
|
+
description: "Every file a current record pins by hash still hashes to the pinned sha256. A changed file asks for the record to be revisited.",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
configurable: true,
|
|
47
|
+
check(ctx) {
|
|
48
|
+
return warningFindings(this, ctx.facts?.records, "asset-drift");
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "WSP112",
|
|
53
|
+
name: "record-asset-missing",
|
|
54
|
+
description: "Every file a current record pins by hash exists.",
|
|
55
|
+
severity: "warning",
|
|
56
|
+
configurable: true,
|
|
57
|
+
check(ctx) {
|
|
58
|
+
return warningFindings(this, ctx.facts?.records, "asset-missing");
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "WSP113",
|
|
63
|
+
name: "record-asset-stale",
|
|
64
|
+
description: "No current record pins a file at the same hash as a record it supersedes while the file is unchanged since: when a decision changes, the artifacts it rests on follow.",
|
|
65
|
+
severity: "warning",
|
|
66
|
+
configurable: true,
|
|
67
|
+
check(ctx) {
|
|
68
|
+
return warningFindings(this, ctx.facts?.records, "asset-stale");
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "WSP114",
|
|
73
|
+
name: "records-unreadable",
|
|
74
|
+
description: "The records of the kind named with --kind can be read.",
|
|
75
|
+
severity: "error",
|
|
76
|
+
configurable: false,
|
|
77
|
+
check(ctx) {
|
|
78
|
+
const facts = ctx.facts?.records;
|
|
79
|
+
if (!facts || !("error" in facts)) return [];
|
|
80
|
+
return [{ checkId: this.id, severity: this.severity, message: `--kind ${facts.kind}: ${facts.error.code}: ${facts.error.message}`, pointer: "" }];
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
];
|
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,13 @@
|
|
|
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`).
|
|
7
|
+
*
|
|
8
|
+
* With `--intent <path[:start-end]>` it prints the intent graph over one
|
|
9
|
+
* region instead (#2651, `intent.ts`), a document of its own in the read
|
|
10
|
+
* contract.
|
|
6
11
|
*
|
|
7
12
|
* The document is part of the read contract, described by `graph.schema.json`
|
|
8
13
|
* beside this file. It is printed for a failure too, with the error's reason
|
|
@@ -28,6 +33,8 @@ import { composeWorkspaceGraph, readMemberIr, type ComposeInput, type WorkspaceG
|
|
|
28
33
|
import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type ErrorLocation, type WorkspaceErrorCode } from "./declaration";
|
|
29
34
|
import { describePlan, emitDocument, executePlan, memberStatus, planJson, planMembers, type MemberPlan, type Toolchain, type UnitResult } from "./member-commands";
|
|
30
35
|
import { loadKindRegistry } from "./kinds";
|
|
36
|
+
import { recordLinkRows } from "./record-assets";
|
|
37
|
+
import { RecordReadError } from "./records";
|
|
31
38
|
import { workingTree } from "./tree";
|
|
32
39
|
import { handToRootChant, locateWorkspace, type LocatedWorkspace } from "./which-chant";
|
|
33
40
|
|
|
@@ -40,7 +47,8 @@ export const GRAPH_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/worksp
|
|
|
40
47
|
/** Why the graph couldn't be read at all: the declaration's codes, `--at`'s included. */
|
|
41
48
|
export const GRAPH_ERROR_CODES = WORKSPACE_ERROR_CODES;
|
|
42
49
|
|
|
43
|
-
const USAGE =
|
|
50
|
+
const USAGE =
|
|
51
|
+
"chant workspace graph [dir] [--at <rev>] [--member <name>] [--kind <kind file>] [-o <file>] [--env <env>] [--dry-run] | chant workspace graph --intent <path[:start-end]> [--at <rev>] [--kind <kind file>...] [--json]";
|
|
44
52
|
|
|
45
53
|
interface Head {
|
|
46
54
|
$schema: string;
|
|
@@ -64,6 +72,12 @@ export interface GraphQuery {
|
|
|
64
72
|
reader?: Toolchain;
|
|
65
73
|
/** Called with each member's stderr, so the command can pass it on. */
|
|
66
74
|
onStderr?: (text: string) => void;
|
|
75
|
+
/**
|
|
76
|
+
* A record kind file, absolute or relative to `cwd` (#2549). Its records
|
|
77
|
+
* fill `records`, and their asset pins and `constrains` entries become
|
|
78
|
+
* rows of `links`.
|
|
79
|
+
*/
|
|
80
|
+
kind?: string;
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
export interface GraphResult {
|
|
@@ -171,7 +185,23 @@ export async function workspaceGraph(query: GraphQuery): Promise<GraphResult> {
|
|
|
171
185
|
// Links (#2539) resolve against the declaration that was read, the revision's for --at, and the kinds installed now.
|
|
172
186
|
const kinds = loadKindRegistry(declaration.pins, located.rootOnDisk).registry;
|
|
173
187
|
const graph = composeWorkspaceGraph({ name: declaration.name, root: located.root }, inputs, { declaration, kinds });
|
|
174
|
-
|
|
188
|
+
let recordsFailed = false;
|
|
189
|
+
if (query.kind !== undefined) {
|
|
190
|
+
// Artifact relationships come from records (#2549): a record's pins and
|
|
191
|
+
// what it constrains, read at the same revision as the declaration.
|
|
192
|
+
const { readRecordsFor } = await import("./records-cli");
|
|
193
|
+
try {
|
|
194
|
+
const read = await readRecordsFor({ kind: query.kind, cwd: query.cwd, at: query.at });
|
|
195
|
+
const name = read.loaded.kind.name;
|
|
196
|
+
graph.records = read.result.records.map((r) => ({ kind: name, id: r.id, path: r.path, state: r.state, valid: r.valid, supersededBy: r.supersededBy }));
|
|
197
|
+
graph.links.push(...recordLinkRows(name, read.result.records, read.loaded.kind.constrains?.field, located.tree, declaration.members));
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (!(err instanceof RecordReadError)) throw err;
|
|
200
|
+
recordsFailed = true;
|
|
201
|
+
query.onStderr?.(`${formatError({ message: `--kind ${query.kind}: ${err.code}: ${err.message}`, hint: USAGE })}\n`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { doc: { ...head, at: located.at, ...graph }, failed: failed || recordsFailed };
|
|
175
205
|
} catch (err) {
|
|
176
206
|
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
177
207
|
return { doc: { ...head, error: { code: err.code, message: err.message, location: err.location ?? null } }, failed: true };
|
|
@@ -191,6 +221,9 @@ export async function runWorkspaceGraph(ctx: CommandContext): Promise<number> {
|
|
|
191
221
|
const handed = await handToRootChant(cwd, args.at);
|
|
192
222
|
if (handed !== undefined) return handed;
|
|
193
223
|
|
|
224
|
+
// The intent graph over one region (#2651) is its own document.
|
|
225
|
+
if (args.intent !== undefined) return (await import("./intent-cli")).runWorkspaceIntent(ctx, cwd);
|
|
226
|
+
|
|
194
227
|
if (args.dryRun) {
|
|
195
228
|
let plan: MemberPlan;
|
|
196
229
|
try {
|
|
@@ -209,6 +242,7 @@ export async function runWorkspaceGraph(ctx: CommandContext): Promise<number> {
|
|
|
209
242
|
at: args.at,
|
|
210
243
|
members: args.members,
|
|
211
244
|
args,
|
|
245
|
+
...(args.kind !== undefined ? { kind: resolve(args.kind) } : {}),
|
|
212
246
|
onStderr: (text) => process.stderr.write(text),
|
|
213
247
|
});
|
|
214
248
|
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
|
|