@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.
Files changed (67) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/content-digest.d.ts +2 -0
  3. package/dist/content-digest.d.ts.map +1 -1
  4. package/dist/workspace/checks/records.d.ts +33 -0
  5. package/dist/workspace/checks/records.d.ts.map +1 -0
  6. package/dist/workspace/checks.d.ts +8 -0
  7. package/dist/workspace/checks.d.ts.map +1 -1
  8. package/dist/workspace/compose-graph.d.ts +22 -6
  9. package/dist/workspace/compose-graph.d.ts.map +1 -1
  10. package/dist/workspace/graph-cli.d.ts +11 -4
  11. package/dist/workspace/graph-cli.d.ts.map +1 -1
  12. package/dist/workspace/lineage-check.d.ts +5 -2
  13. package/dist/workspace/lineage-check.d.ts.map +1 -1
  14. package/dist/workspace/lineage-cli.d.ts +5 -1
  15. package/dist/workspace/lineage-cli.d.ts.map +1 -1
  16. package/dist/workspace/lineage-init.d.ts +47 -4
  17. package/dist/workspace/lineage-init.d.ts.map +1 -1
  18. package/dist/workspace/lineage-lock.d.ts +23 -2
  19. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  20. package/dist/workspace/lineage-upgrade-cli.d.ts +1 -1
  21. package/dist/workspace/lineage-upgrade.d.ts +8 -2
  22. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  23. package/dist/workspace/reason-codes.d.ts +4 -0
  24. package/dist/workspace/reason-codes.d.ts.map +1 -1
  25. package/dist/workspace/record-assets.d.ts +108 -0
  26. package/dist/workspace/record-assets.d.ts.map +1 -0
  27. package/dist/workspace/records-cli.d.ts +32 -1
  28. package/dist/workspace/records-cli.d.ts.map +1 -1
  29. package/dist/workspace/records.d.ts +41 -0
  30. package/dist/workspace/records.d.ts.map +1 -1
  31. package/dist/workspace/template-pins.d.ts +33 -0
  32. package/dist/workspace/template-pins.d.ts.map +1 -0
  33. package/dist/workspace/tree.d.ts +5 -0
  34. package/dist/workspace/tree.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/cli/handlers/init.ts +7 -3
  37. package/src/cli/main.ts +16 -8
  38. package/src/content-digest.ts +5 -0
  39. package/src/workspace/behold-kinds.test.ts +10 -14
  40. package/src/workspace/checks/records.ts +83 -0
  41. package/src/workspace/checks.test.ts +2 -0
  42. package/src/workspace/checks.ts +13 -3
  43. package/src/workspace/compose-graph.test.ts +2 -1
  44. package/src/workspace/compose-graph.ts +23 -6
  45. package/src/workspace/graph-cli.ts +32 -6
  46. package/src/workspace/graph-contract.test.ts +2 -1
  47. package/src/workspace/graph.schema.json +131 -2
  48. package/src/workspace/lineage-check.ts +22 -5
  49. package/src/workspace/lineage-cli.ts +21 -1
  50. package/src/workspace/lineage-init-dir.test.ts +243 -0
  51. package/src/workspace/lineage-init.ts +223 -24
  52. package/src/workspace/lineage-lock.ts +18 -3
  53. package/src/workspace/lineage-upgrade-cli.ts +2 -2
  54. package/src/workspace/lineage-upgrade.test.ts +29 -0
  55. package/src/workspace/lineage-upgrade.ts +130 -13
  56. package/src/workspace/member-commands.ts +1 -1
  57. package/src/workspace/reason-codes.test.ts +2 -1
  58. package/src/workspace/reason-codes.ts +5 -0
  59. package/src/workspace/record-assets.test.ts +319 -0
  60. package/src/workspace/record-assets.ts +207 -0
  61. package/src/workspace/records-cli.ts +117 -14
  62. package/src/workspace/records.schema.json +33 -1
  63. package/src/workspace/records.test.ts +50 -0
  64. package/src/workspace/records.ts +115 -5
  65. package/src/workspace/template-pins.test.ts +69 -0
  66. package/src/workspace/template-pins.ts +117 -0
  67. 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;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,EAAgD,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAC/G,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAA+C,KAAK,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEzH,6DAA6D;AAC7D,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,gFAAgF,CAAC;AAItH,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;CACb;AAED,gFAAgF;AAChF,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG;IAAE,UAAU,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAExE,gDAAgD;AAChD,MAAM,WAAW,SAAS;IACxB,gFAAgF;IAChF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,uFAAuF;IACvF,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wEAAwE;AACxE,MAAM,MAAM,eAAe,GACvB;IACE,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF,GACD;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE3F,oGAAoG;AACpG,wBAAsB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CA4ChF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAmC9E;AAUD,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,eAAe,GAAG,UAAU,EAAE,CAE5F;AAoBD,yCAAyC;AACzC,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAS9E"}
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;AAIpD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,4IAWU,CAAC;AAC3C,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE;;;GAGG;AACH,eAAO,MAAM,gBAAgB,2KAiBa,CAAC;AAC3C,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,qBAAa,eAAgB,SAAQ,KAAK;IAEtC,QAAQ,CAAC,IAAI,EAAE,aAAa;gBAAnB,IAAI,EAAE,aAAa,EAC5B,OAAO,EAAE,MAAM;CAKlB;AAMD,oDAAoD;AACpD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;kBAoCzB,CAAC;AAEL,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAmBD,kFAAkF;AAClF,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiCzG;AAID,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExG;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAgB1D;AA0BD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,0DAA0D;IAC1D,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,sFAAsF;IACtF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,oDAAoD;IACpD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF;AAsBD,oEAAoE;AACpE,wBAAsB,WAAW,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAgFnH"}
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"}
@@ -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;CAC5B;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,CAqCvD;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,CA+B/E;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.81.0",
3
+ "version": "0.83.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -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
- console.log(`Lineage: ${result.spec!.id} at ${result.spec!.ref} (${result.commit!.slice(0, 12)}), recorded in .chant/workspace.lock.json`);
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; --param
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. Needs no workspace file.
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
@@ -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
- * `terraform` ships from the terraform lexicon's `./workspace-kinds` subpath,
5
- * and `choudoufu` from the kinds-only package
6
- * `@intentius/workspace-kind-choudoufu`. A workspace that pins both lists
7
- * members of either kind through `chant workspace ls --json`, with the kinds
8
- * behold gave the same directories, and reading the kinds imports neither
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 packages are the repo's own: their package.json and kinds
12
- * file are copied as they ship, and every code entry their `exports` name is
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), install(root, CHOUDOUFU)];
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 neither package", async () => {
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/workspace-kind-choudoufu", precedence: 450 });
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
+ ];
@@ -56,6 +56,8 @@ describe("the WSP catalog", () => {
56
56
  "flat-ledger-environment-shared",
57
57
  "link-target-unknown",
58
58
  "link-kind-unknown",
59
+ // --kind names a record kind, and records that can't be read check nothing (#2549).
60
+ "records-unreadable",
59
61
  ]);
60
62
  });
61
63