@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset pins and record links (#2549; #2524 D4, D6, D18).
|
|
3
|
+
*
|
|
4
|
+
* Artifact relationships are derived from decisions. A decision record pins
|
|
5
|
+
* the workspace files it rests on in `evidence`, each as `{title, path,
|
|
6
|
+
* sha256}`, and names what it governs in `constrains`, as `member:<name>` or
|
|
7
|
+
* `path:<path>`. There is no direct link from a design artifact to code or to
|
|
8
|
+
* another artifact: a reader walks from a file to the decisions whose
|
|
9
|
+
* `constrains` cover it, and from them to their pinned assets.
|
|
10
|
+
*
|
|
11
|
+
* A pin is checked against the tree a read looks at (the working tree, or a
|
|
12
|
+
* revision's git objects). A file whose bytes hash differently is `drifted`,
|
|
13
|
+
* and one that is gone is `missing`. Neither makes the record invalid: the
|
|
14
|
+
* record is still what was decided, and the drift is reported beside it.
|
|
15
|
+
*
|
|
16
|
+
* Which front-matter fields hold pins and links is the kind's data
|
|
17
|
+
* (`pins.field`, `constrains.field`), so this module never names `evidence`.
|
|
18
|
+
*/
|
|
19
|
+
import type { RecordWarning } from "./records.js";
|
|
20
|
+
import type { WorkspaceTree } from "./tree.js";
|
|
21
|
+
/**
|
|
22
|
+
* A path from the workspace root: `/` separators, no leading `/`, no `.` or
|
|
23
|
+
* `..` segment, no empty segment, no backslash or control character and no
|
|
24
|
+
* trailing `/`. The decision schema holds the same pattern.
|
|
25
|
+
*/
|
|
26
|
+
export declare const WORKSPACE_PATH_PATTERN: string;
|
|
27
|
+
export declare function isWorkspacePath(value: unknown): value is string;
|
|
28
|
+
/**
|
|
29
|
+
* `pinned`: the file hashes to the pin. `drifted`: it doesn't. `missing`: it
|
|
30
|
+
* isn't there. `stale`: it hashes to the pin, which a record this one
|
|
31
|
+
* supersedes pinned too, so the decision changed and the artifact did not.
|
|
32
|
+
*/
|
|
33
|
+
export type PinState = "pinned" | "drifted" | "missing" | "stale";
|
|
34
|
+
/** One pinned file, as the tree read has it. */
|
|
35
|
+
export interface AssetPin {
|
|
36
|
+
/** From the workspace root. */
|
|
37
|
+
path: string;
|
|
38
|
+
/** The hash the record pins. */
|
|
39
|
+
sha256: string;
|
|
40
|
+
/** The hash of the file in the tree read, or null when it is missing. */
|
|
41
|
+
actual: string | null;
|
|
42
|
+
state: PinState;
|
|
43
|
+
}
|
|
44
|
+
/** The well-formed pins in a record's `field` list: entries with a workspace path and a hex sha256. */
|
|
45
|
+
export declare function pinEntries(data: Record<string, unknown> | null, field: string): {
|
|
46
|
+
path: string;
|
|
47
|
+
sha256: string;
|
|
48
|
+
}[];
|
|
49
|
+
/** The hex SHA-256 of the file at `path` in `tree`, or undefined when it is not a file there. */
|
|
50
|
+
export declare function fileDigest(tree: WorkspaceTree, path: string): string | undefined;
|
|
51
|
+
/** Check each pin against `tree`, rooted at the workspace root. */
|
|
52
|
+
export declare function checkPins(pins: {
|
|
53
|
+
path: string;
|
|
54
|
+
sha256: string;
|
|
55
|
+
}[], tree: WorkspaceTree): {
|
|
56
|
+
assets: AssetPin[];
|
|
57
|
+
warnings: RecordWarning[];
|
|
58
|
+
};
|
|
59
|
+
/** The kinds of link a record has in `chant workspace graph`. Closed. */
|
|
60
|
+
export declare const RECORD_LINK_KINDS: readonly ["asset", "constrains"];
|
|
61
|
+
/** A link from a record to a workspace path or member, a row of the graph's `links`. */
|
|
62
|
+
export interface RecordLinkRow {
|
|
63
|
+
kind: (typeof RECORD_LINK_KINDS)[number];
|
|
64
|
+
origin: "declared";
|
|
65
|
+
resolves: "source";
|
|
66
|
+
/** The record's kind, such as `decision`. */
|
|
67
|
+
recordKind: string;
|
|
68
|
+
/** The record's id. */
|
|
69
|
+
record: string;
|
|
70
|
+
/** The record file, from the repository root. */
|
|
71
|
+
recordPath: string;
|
|
72
|
+
/** For `asset`, the pinned path; for `constrains`, the entry as written (`member:<name>` or `path:<path>`). */
|
|
73
|
+
target: string;
|
|
74
|
+
/** The member the target is, or holds it; null when no member does. */
|
|
75
|
+
member: string | null;
|
|
76
|
+
/** `pinned`, `drifted`, `missing` or `stale` for an asset; `resolved` or `missing` for constrains. */
|
|
77
|
+
status: PinState | "resolved";
|
|
78
|
+
reason: string | null;
|
|
79
|
+
/** For an asset: the pinned hash and the hash in the tree read. */
|
|
80
|
+
sha256?: string;
|
|
81
|
+
actual?: string | null;
|
|
82
|
+
}
|
|
83
|
+
/** The member whose directory holds `path` (the deepest one), or null. */
|
|
84
|
+
export declare function memberHolding(path: string, members: readonly {
|
|
85
|
+
name: string;
|
|
86
|
+
dir: string;
|
|
87
|
+
}[]): string | null;
|
|
88
|
+
/** Whether a `path:` constraint covers `file`: the same path, or a directory above it. */
|
|
89
|
+
export declare function constraintCovers(constraint: string, file: string): boolean;
|
|
90
|
+
/** A record as the link rows read it. */
|
|
91
|
+
export interface LinkedRecord {
|
|
92
|
+
id: string | null;
|
|
93
|
+
path: string;
|
|
94
|
+
supersededBy: string | null;
|
|
95
|
+
data: Record<string, unknown> | null;
|
|
96
|
+
assets: AssetPin[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The link rows of the records of one kind: an `asset` row per pin and a
|
|
100
|
+
* `constrains` row per `member:` or `path:` entry. Superseded records and
|
|
101
|
+
* records with no id have none, since their links no longer hold. Paths
|
|
102
|
+
* resolve in `tree` (the workspace root), members in `members`.
|
|
103
|
+
*/
|
|
104
|
+
export declare function recordLinkRows(kindName: string, records: readonly LinkedRecord[], constrainsField: string | undefined, tree: WorkspaceTree, members: readonly {
|
|
105
|
+
name: string;
|
|
106
|
+
dir: string;
|
|
107
|
+
}[]): RecordLinkRow[];
|
|
108
|
+
//# sourceMappingURL=record-assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-assets.d.ts","sourceRoot":"","sources":["../../src/workspace/record-assets.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* nothing is inferred (#2525 rule 1).
|
|
13
13
|
*/
|
|
14
14
|
import type { CommandContext } from "../cli/registry.js";
|
|
15
|
-
import { type ReadErrorCode, type RecordEntry } from "./records.js";
|
|
15
|
+
import { type LoadedRecordKind, type ReadErrorCode, type ReadRecordsResult, type RecordEntry } from "./records.js";
|
|
16
16
|
import { type ProvenanceLevel } from "./trust/attestor.js";
|
|
17
17
|
import { type BaseSource, type RecordProvenance } from "./trust/provenance.js";
|
|
18
18
|
/** The version of the `records` output this chant writes. */
|
|
@@ -54,6 +54,8 @@ export type RecordsDocument = {
|
|
|
54
54
|
file: string;
|
|
55
55
|
};
|
|
56
56
|
at: string | null;
|
|
57
|
+
/** The directory pinned paths resolve in, from the repository root: the workspace holding the kind file, or the repository root (#2549). */
|
|
58
|
+
workspaceRoot: string;
|
|
57
59
|
current: boolean;
|
|
58
60
|
trust: TrustView;
|
|
59
61
|
records: RecordView[];
|
|
@@ -71,8 +73,37 @@ export type RecordsDocument = {
|
|
|
71
73
|
message: string;
|
|
72
74
|
};
|
|
73
75
|
};
|
|
76
|
+
/** A records read, before provenance. */
|
|
77
|
+
export interface RecordsRead {
|
|
78
|
+
loaded: LoadedRecordKind;
|
|
79
|
+
/** The repository root, or the working directory outside git. Record paths are relative to it. */
|
|
80
|
+
root: string;
|
|
81
|
+
/** The git top, or undefined outside git. */
|
|
82
|
+
top: string | undefined;
|
|
83
|
+
at: string | null;
|
|
84
|
+
/** Where pinned paths resolve, relative to `root` ("." for the root itself). */
|
|
85
|
+
workspaceRoot: string;
|
|
86
|
+
result: ReadRecordsResult;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Load the kind and read its records, in the working tree or at `query.at`,
|
|
90
|
+
* with each pin checked in the same tree. Throws a {@link RecordReadError}.
|
|
91
|
+
* `chant workspace graph` and `check` read records through this too.
|
|
92
|
+
*/
|
|
93
|
+
export declare function readRecordsFor(query: Omit<RecordsQuery, "base">): Promise<RecordsRead>;
|
|
74
94
|
/** Run the query and build the document `--json` prints. Never throws a {@link RecordReadError}. */
|
|
75
95
|
export declare function queryRecords(query: RecordsQuery): Promise<RecordsDocument>;
|
|
96
|
+
/**
|
|
97
|
+
* `chant workspace records pin <path>`: the `{path, sha256}` a decision's
|
|
98
|
+
* evidence entry holds for a file, with the path from the workspace root
|
|
99
|
+
* (the nearest declaration above the file, or the repository root).
|
|
100
|
+
*/
|
|
101
|
+
export declare function pinFile(file: string, cwd: string): {
|
|
102
|
+
path: string;
|
|
103
|
+
sha256: string;
|
|
104
|
+
} | {
|
|
105
|
+
error: string;
|
|
106
|
+
};
|
|
76
107
|
export declare function runWorkspaceRecords(ctx: CommandContext): Promise<number>;
|
|
77
108
|
/** Records whose provenance falls below `required`. Only `attested` can be required. */
|
|
78
109
|
export declare function belowRequired(records: RecordView[], required: ProvenanceLevel): RecordView[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"records-cli.d.ts","sourceRoot":"","sources":["../../src/workspace/records-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"records-cli.d.ts","sourceRoot":"","sources":["../../src/workspace/records-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -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. */
|
|
@@ -96,6 +115,10 @@ export interface RecordEntry {
|
|
|
96
115
|
supersededBy: string | null;
|
|
97
116
|
/** The front matter as JSON, or null when it could not be parsed. */
|
|
98
117
|
data: Record<string, unknown> | null;
|
|
118
|
+
/** Each workspace file the record pins, checked against the tree read (#2549). Empty when nothing was checked. */
|
|
119
|
+
assets: AssetPin[];
|
|
120
|
+
/** Findings that leave the record valid, such as a pinned file that changed (#2549). */
|
|
121
|
+
warnings: RecordWarning[];
|
|
99
122
|
}
|
|
100
123
|
export interface ReadRecordsOptions {
|
|
101
124
|
/** Where record paths are reported from, and what `source` reads relative to. */
|
|
@@ -103,6 +126,24 @@ export interface ReadRecordsOptions {
|
|
|
103
126
|
source: RecordSource;
|
|
104
127
|
/** Leave out records a closed record supersedes. */
|
|
105
128
|
current?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* The workspace root the kind's pins resolve in: the working tree, or the
|
|
131
|
+
* revision read (#2549). Without it no pin is checked.
|
|
132
|
+
*/
|
|
133
|
+
assets?: WorkspaceTree;
|
|
134
|
+
/**
|
|
135
|
+
* When files last changed and records were recorded, in the history of the
|
|
136
|
+
* revision read, for `asset-stale` (#2549). Without it only the hashes are
|
|
137
|
+
* compared.
|
|
138
|
+
*/
|
|
139
|
+
history?: RecordHistory;
|
|
140
|
+
}
|
|
141
|
+
/** Commit times, in seconds since the epoch, read from git. */
|
|
142
|
+
export interface RecordHistory {
|
|
143
|
+
/** The last commit that changed `path` (from the workspace root), or null when unknown. */
|
|
144
|
+
fileChanged(path: string): number | null;
|
|
145
|
+
/** The commit that added the record at `path` (from the repository root), or null when it is not committed. */
|
|
146
|
+
recorded(path: string): number | null;
|
|
106
147
|
}
|
|
107
148
|
export interface ReadRecordsResult {
|
|
108
149
|
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/handlers/init.ts
CHANGED
|
@@ -54,14 +54,15 @@ export async function runInitLexicon(ctx: CommandContext): Promise<number> {
|
|
|
54
54
|
* `chant init --from <repo>@<ref>[#<member>] [--param <name>=<value>]... [path]`
|
|
55
55
|
* (#2540, #2627): copy a template repository at a ref, substitute the
|
|
56
56
|
* parameters its `chant.template.json` declares, and record its lineage.
|
|
57
|
+
* `--from <dir>[#<member>]` copies a template directory on disk instead (#2647).
|
|
57
58
|
* The template brings its own lexicons and configuration, so `--lexicon` and
|
|
58
59
|
* `--template` do not apply.
|
|
59
60
|
*/
|
|
60
61
|
async function runInitFrom(ctx: CommandContext): Promise<number> {
|
|
61
62
|
const { args } = ctx;
|
|
62
|
-
const usage = "Usage: chant init --from <repo>@<ref>[#<member>] [--param <name>=<value>]... [path]";
|
|
63
|
+
const usage = "Usage: chant init --from <repo>@<ref>[#<member>] | <dir>[#<member>] [--param <name>=<value>]... [path]";
|
|
63
64
|
if (!args.migrateFrom) {
|
|
64
|
-
console.error(formatError({ message: "--from needs a template: <repo>@<ref>[#<member>]", hint: usage }));
|
|
65
|
+
console.error(formatError({ message: "--from needs a template: <repo>@<ref>[#<member>] or <dir>[#<member>]", hint: usage }));
|
|
65
66
|
return 1;
|
|
66
67
|
}
|
|
67
68
|
if (args.lexicon || args.template) {
|
|
@@ -95,7 +96,10 @@ async function runInitFrom(ctx: CommandContext): Promise<number> {
|
|
|
95
96
|
console.log(formatSuccess("Created:"));
|
|
96
97
|
for (const file of result.createdFiles) console.log(` ${file}`);
|
|
97
98
|
console.log("");
|
|
98
|
-
|
|
99
|
+
const from = result.spec
|
|
100
|
+
? `${result.spec.id} at ${result.spec.ref} (${result.commit!.slice(0, 12)})`
|
|
101
|
+
: `${result.template} (a directory, recorded by digest only)`;
|
|
102
|
+
console.log(`Lineage: ${from}, recorded in .chant/workspace.lock.json`);
|
|
99
103
|
const parameters = Object.entries(result.parameters ?? {});
|
|
100
104
|
if (parameters.length > 0) console.log(`Parameters: ${parameters.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ")}`);
|
|
101
105
|
return 0;
|
package/src/cli/main.ts
CHANGED
|
@@ -360,7 +360,7 @@ 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
366
|
} else if (arg === "--current") {
|
|
@@ -549,7 +549,9 @@ Usage:
|
|
|
549
549
|
Commands:
|
|
550
550
|
init Initialize a new chant project
|
|
551
551
|
(--from <repo>@<ref>[#<member>] copies a template
|
|
552
|
-
repository and records its lineage
|
|
552
|
+
repository and records its lineage, and --from
|
|
553
|
+
<dir>[#<member>] copies a template directory on
|
|
554
|
+
disk; --param
|
|
553
555
|
<name>=<value> sets a parameter the template's
|
|
554
556
|
chant.template.json declares, repeatable)
|
|
555
557
|
init lexicon <name> Scaffold a new lexicon plugin project
|
|
@@ -717,7 +719,11 @@ Workspace (level 1, #2524):
|
|
|
717
719
|
Each record reports its provenance level, judged by
|
|
718
720
|
the signers at --base (default: the target branch);
|
|
719
721
|
--require attested exits 2 if any record is not
|
|
720
|
-
attested
|
|
722
|
+
attested. A pinned file that changed is a warning,
|
|
723
|
+
asset-drift or asset-missing
|
|
724
|
+
workspace records pin <path>
|
|
725
|
+
Print the path from the workspace root and the
|
|
726
|
+
sha256 of a file, for a decision's evidence pin
|
|
721
727
|
workspace verify [--base <rev>] [--head <rev>] [--require attested]
|
|
722
728
|
Check the commits in base..head against the signers
|
|
723
729
|
and roles read from base. A change to the signers file
|
|
@@ -729,18 +735,19 @@ Workspace (level 1, #2524):
|
|
|
729
735
|
manual steps. Needs no workspace file
|
|
730
736
|
workspace lineage resolve <path>
|
|
731
737
|
Close a manual step once the file is merged by hand
|
|
732
|
-
workspace upgrade [<scope>] [--to <ref>] [--allow-code] [--dry-run] [--output <file>]
|
|
738
|
+
workspace upgrade [<scope>] [--to <ref|dir>] [--allow-code] [--dry-run] [--output <file>]
|
|
733
739
|
Bring a lineage scope to a newer template version: fetch
|
|
734
740
|
it, migrate and merge per file in a worktree, run build,
|
|
735
741
|
lint and workspace check there, then gate on the digest
|
|
736
742
|
of the patch (chant approve workspace-upgrade <scope>).
|
|
737
743
|
A second run with the approval applies the patch
|
|
738
|
-
workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated]
|
|
744
|
+
workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]
|
|
739
745
|
Fail on an unreadable lineage lock or an open manual
|
|
740
746
|
step, and, in a declared workspace, on a WSP check of
|
|
741
747
|
the declaration, member ledgers, pipelines or
|
|
742
748
|
generated files. --generated runs declared generators
|
|
743
|
-
and compares their output.
|
|
749
|
+
and compares their output. --kind warns on records
|
|
750
|
+
whose pinned files changed. Needs no workspace file.
|
|
744
751
|
--at reads a commit's git objects; --format json
|
|
745
752
|
prints the read-contract document
|
|
746
753
|
workspace build [dir] [--member <name>] [-o <dir>] [--dry-run]
|
|
@@ -754,11 +761,12 @@ Workspace (level 1, #2524):
|
|
|
754
761
|
workspace audit [dir] [--json] [--member <name>]
|
|
755
762
|
Audit each chant member with its own .chant-audit.json;
|
|
756
763
|
every finding carries a member field
|
|
757
|
-
workspace graph [dir] [--at <rev>] [--member <name>] [-o <file>]
|
|
764
|
+
workspace graph [dir] [--at <rev>] [--member <name>] [--kind <kind file>] [-o <file>]
|
|
758
765
|
Compose each chant member's chant graph into one IR,
|
|
759
766
|
with <member>/<id> ids and groups.byMember: the
|
|
760
767
|
read-contract document. --at <rev> runs each member's
|
|
761
|
-
source as it was at that commit
|
|
768
|
+
source as it was at that commit; --kind adds the
|
|
769
|
+
records' asset and constrains links
|
|
762
770
|
|
|
763
771
|
Lifecycle (alias: lc):
|
|
764
772
|
lifecycle snapshot <env> Query API, save metadata to orphan branch
|
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
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* behold's two closed member kinds, read through chant's workspace (#2545).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* package.
|
|
4
|
+
* Both ship from the terraform lexicon's `./workspace-kinds` subpath, since
|
|
5
|
+
* a choudoufu estate is a Terraform root the lexicon's choudoufu mode reads.
|
|
6
|
+
* A workspace that pins the lexicon lists members of either kind through
|
|
7
|
+
* `chant workspace ls --json`, with the kinds behold gave the same
|
|
8
|
+
* directories, and reading the kinds never imports the lexicon.
|
|
10
9
|
*
|
|
11
|
-
* The installed
|
|
12
|
-
*
|
|
10
|
+
* The installed package is the repo's own: its package.json and kinds file
|
|
11
|
+
* are copied as they ship, and every code entry its `exports` names is
|
|
13
12
|
* replaced by a module that leaves a marker file when it is imported.
|
|
14
13
|
*/
|
|
15
14
|
|
|
@@ -25,7 +24,6 @@ import { workingTree } from "./tree";
|
|
|
25
24
|
|
|
26
25
|
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
27
26
|
const TERRAFORM = join(REPO, "lexicons", "terraform");
|
|
28
|
-
const CHOUDOUFU = join(REPO, "packages", "workspace-kind-choudoufu");
|
|
29
27
|
const FIXTURES = join(TERRAFORM, "src", "__fixtures__");
|
|
30
28
|
|
|
31
29
|
const scratch: string[] = [];
|
|
@@ -65,7 +63,7 @@ function install(root: string, from: string): string {
|
|
|
65
63
|
function workspace(): { root: string; installed: string[] } {
|
|
66
64
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-behold-kinds-")));
|
|
67
65
|
scratch.push(root);
|
|
68
|
-
const installed = [install(root, TERRAFORM)
|
|
66
|
+
const installed = [install(root, TERRAFORM)];
|
|
69
67
|
// Estates in the shapes behold serves: a stock root, a choudoufu root with
|
|
70
68
|
// the sidecar, one with the live block, and a chant project beside .tf files.
|
|
71
69
|
cpSync(join(FIXTURES, "no-backend"), join(root, "estates", "stock"), { recursive: true });
|
|
@@ -82,7 +80,6 @@ function workspace(): { root: string; installed: string[] } {
|
|
|
82
80
|
schema: 1,
|
|
83
81
|
pins: [
|
|
84
82
|
{ package: "@intentius/chant-lexicon-terraform", version: version(TERRAFORM) },
|
|
85
|
-
{ package: "@intentius/workspace-kind-choudoufu", version: version(CHOUDOUFU) },
|
|
86
83
|
],
|
|
87
84
|
members: [
|
|
88
85
|
{ name: "stock", dir: "estates/stock", kind: "terraform" },
|
|
@@ -106,7 +103,7 @@ describe("behold's member kinds from their packages (#2545)", () => {
|
|
|
106
103
|
});
|
|
107
104
|
afterEach(() => vi.restoreAllMocks());
|
|
108
105
|
|
|
109
|
-
test("chant workspace ls --json lists terraform and choudoufu members, readable, and imports
|
|
106
|
+
test("chant workspace ls --json lists terraform and choudoufu members, readable, and never imports the lexicon", async () => {
|
|
110
107
|
const { root, installed } = workspace();
|
|
111
108
|
const code = await runWorkspaceLs({ args: parseArgs(["workspace", "ls", root, "--json"]), plugins: [] } as never);
|
|
112
109
|
expect(code).toBe(0);
|
|
@@ -126,12 +123,11 @@ describe("behold's member kinds from their packages (#2545)", () => {
|
|
|
126
123
|
const { root } = workspace();
|
|
127
124
|
const pins = [
|
|
128
125
|
{ package: "@intentius/chant-lexicon-terraform", version: version(TERRAFORM), path: null },
|
|
129
|
-
{ package: "@intentius/workspace-kind-choudoufu", version: version(CHOUDOUFU), path: null },
|
|
130
126
|
];
|
|
131
127
|
const { registry, problems } = loadKindRegistry(pins, root);
|
|
132
128
|
expect(problems).toEqual([]);
|
|
133
129
|
expect(registry.get("terraform")).toMatchObject({ source: "@intentius/chant-lexicon-terraform", precedence: 400 });
|
|
134
|
-
expect(registry.get("choudoufu")).toMatchObject({ source: "@intentius/
|
|
130
|
+
expect(registry.get("choudoufu")).toMatchObject({ source: "@intentius/chant-lexicon-terraform", precedence: 450 });
|
|
135
131
|
const tree = workingTree(root);
|
|
136
132
|
const winner = (dir: string) => resolveKind(registry, tree, dir).winner?.name;
|
|
137
133
|
expect(winner("estates/stock")).toBe("terraform");
|
|
@@ -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
|
+
];
|