@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.
Files changed (73) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/cli/registry.d.ts +4 -0
  3. package/dist/cli/registry.d.ts.map +1 -1
  4. package/dist/content-digest.d.ts +2 -0
  5. package/dist/content-digest.d.ts.map +1 -1
  6. package/dist/workspace/checks/records.d.ts +33 -0
  7. package/dist/workspace/checks/records.d.ts.map +1 -0
  8. package/dist/workspace/checks.d.ts +8 -0
  9. package/dist/workspace/checks.d.ts.map +1 -1
  10. package/dist/workspace/compose-graph.d.ts +22 -6
  11. package/dist/workspace/compose-graph.d.ts.map +1 -1
  12. package/dist/workspace/graph-cli.d.ts +15 -4
  13. package/dist/workspace/graph-cli.d.ts.map +1 -1
  14. package/dist/workspace/intent-cli.d.ts +17 -0
  15. package/dist/workspace/intent-cli.d.ts.map +1 -0
  16. package/dist/workspace/intent-joins.d.ts +93 -0
  17. package/dist/workspace/intent-joins.d.ts.map +1 -0
  18. package/dist/workspace/intent.d.ts +285 -0
  19. package/dist/workspace/intent.d.ts.map +1 -0
  20. package/dist/workspace/lineage-check.d.ts +5 -2
  21. package/dist/workspace/lineage-check.d.ts.map +1 -1
  22. package/dist/workspace/lineage-init.d.ts.map +1 -1
  23. package/dist/workspace/lineage-lock.d.ts +8 -0
  24. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  25. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  26. package/dist/workspace/reason-codes.d.ts +19 -0
  27. package/dist/workspace/reason-codes.d.ts.map +1 -1
  28. package/dist/workspace/record-assets.d.ts +108 -0
  29. package/dist/workspace/record-assets.d.ts.map +1 -0
  30. package/dist/workspace/records-cli.d.ts +32 -1
  31. package/dist/workspace/records-cli.d.ts.map +1 -1
  32. package/dist/workspace/records.d.ts +47 -0
  33. package/dist/workspace/records.d.ts.map +1 -1
  34. package/dist/workspace/template-pins.d.ts +33 -0
  35. package/dist/workspace/template-pins.d.ts.map +1 -0
  36. package/dist/workspace/tree.d.ts +5 -0
  37. package/dist/workspace/tree.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/cli/main.test.ts +11 -0
  40. package/src/cli/main.ts +22 -6
  41. package/src/cli/registry.ts +4 -0
  42. package/src/content-digest.ts +5 -0
  43. package/src/workspace/checks/records.ts +83 -0
  44. package/src/workspace/checks.test.ts +2 -0
  45. package/src/workspace/checks.ts +13 -3
  46. package/src/workspace/compose-graph.test.ts +2 -1
  47. package/src/workspace/compose-graph.ts +23 -6
  48. package/src/workspace/graph-cli.ts +40 -6
  49. package/src/workspace/graph-contract.test.ts +2 -1
  50. package/src/workspace/graph.schema.json +131 -2
  51. package/src/workspace/intent-cli.ts +95 -0
  52. package/src/workspace/intent-joins.ts +165 -0
  53. package/src/workspace/intent.schema.json +1078 -0
  54. package/src/workspace/intent.test.ts +448 -0
  55. package/src/workspace/intent.ts +941 -0
  56. package/src/workspace/lineage-check.ts +22 -5
  57. package/src/workspace/lineage-init.ts +5 -1
  58. package/src/workspace/lineage-lock.ts +7 -0
  59. package/src/workspace/lineage-upgrade.test.ts +29 -0
  60. package/src/workspace/lineage-upgrade.ts +21 -3
  61. package/src/workspace/member-commands.ts +1 -1
  62. package/src/workspace/read-contract.test.ts +16 -1
  63. package/src/workspace/reason-codes.test.ts +7 -2
  64. package/src/workspace/reason-codes.ts +23 -0
  65. package/src/workspace/record-assets.test.ts +319 -0
  66. package/src/workspace/record-assets.ts +207 -0
  67. package/src/workspace/records-cli.ts +117 -14
  68. package/src/workspace/records.schema.json +33 -1
  69. package/src/workspace/records.test.ts +50 -0
  70. package/src/workspace/records.ts +116 -6
  71. package/src/workspace/template-pins.test.ts +69 -0
  72. package/src/workspace/template-pins.ts +117 -0
  73. package/src/workspace/tree.ts +12 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated]`:
2
+ * `chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]`:
3
3
  * the lineage checks (#2550, D9), and the declaration checks (#2535, D16)
4
4
  * when a declaration sits between the current directory and the git root.
5
5
  *
@@ -10,7 +10,9 @@
10
10
  * check. The declaration checks report `WSP` findings through lint's
11
11
  * reporters (`./checks.ts`), and cover member ledgers (#2538), recorded
12
12
  * pipelines (#2542) and generated files (#2541) as well as the declaration
13
- * itself (#2641). `--generated` runs each declared generator too.
13
+ * itself (#2641). `--generated` runs each declared generator too. `--kind`
14
+ * reads a record kind's records and reports the files they pin by hash that
15
+ * have changed since (#2549).
14
16
  *
15
17
  * `chant workspace upgrade` runs the same checks in its staging worktree
16
18
  * before it reaches its gate.
@@ -23,6 +25,7 @@ import type { CommandContext } from "../cli/registry";
23
25
  import type { LintDiagnostic, LintRule } from "../lint/rule";
24
26
  import { findWorkspaceRoot } from "../project-root";
25
27
  import type { DeclarationCheckReport } from "./checks";
28
+ import type { RecordFacts } from "./checks/records";
26
29
  import { LOCK_FILE, LockError, parseLock, readLock } from "./lineage-lock";
27
30
  import type { ReasonCode } from "./reason-codes";
28
31
  import type { WorkspaceTree } from "./tree";
@@ -81,7 +84,7 @@ export function findingKey(f: CheckFinding): string {
81
84
  return `${f.code}\0${f.scope ?? ""}\0${f.path ?? ""}`;
82
85
  }
83
86
 
84
- const USAGE = "chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated]";
87
+ const USAGE = "chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]";
85
88
  const FORMATS = ["stylish", "json", "sarif"] as const;
86
89
 
87
90
  /** A lock finding as a lint diagnostic, for `--format json` and `--format sarif`. */
@@ -142,7 +145,7 @@ export type CheckDocument =
142
145
  * to `cwd`. `runGenerators` is `--generated`; with `at` it does nothing,
143
146
  * since the member facts describe the working tree, not the revision.
144
147
  */
145
- export async function runChecks(cwd: string, at?: string, options: { runGenerators?: boolean } = {}): Promise<CheckDocument> {
148
+ export async function runChecks(cwd: string, at?: string, options: { runGenerators?: boolean; kind?: string } = {}): Promise<CheckDocument> {
146
149
  // Loaded here, not at the top: `workspace upgrade` imports this module for checkLineage alone.
147
150
  const [{ findDeclarationDir, readDeclaration, readerVersion }, { gitTop, gitTree, resolveCommit, workingTree }] = await Promise.all([
148
151
  import("./declaration"),
@@ -184,9 +187,11 @@ export async function runChecks(cwd: string, at?: string, options: { runGenerato
184
187
  if (found) {
185
188
  const { runDeclarationChecks } = await import("./checks");
186
189
  const { dir, tree } = found;
190
+ const records = options.kind !== undefined ? await readRecordFacts(options.kind, cwd, commit) : undefined;
187
191
  declaration = await runDeclarationChecks(dir, (file) => relative(cwd, join(dir, file)).split(sep).join("/"), {
188
192
  runGenerators: options.runGenerators === true,
189
193
  ...(commit !== null ? { tree } : {}),
194
+ ...(records ? { records } : {}),
190
195
  });
191
196
  let name: string | null = null;
192
197
  try {
@@ -200,6 +205,18 @@ export async function runChecks(cwd: string, at?: string, options: { runGenerato
200
205
  return { ...head, at: commit, workspace, ...report, ok, ...(declaration ? { declaration } : {}) };
201
206
  }
202
207
 
208
+ /** The records of the kind file `kind` for the record checks (#2549), at `commit` or in the working tree. */
209
+ async function readRecordFacts(kind: string, cwd: string, commit: string | null): Promise<RecordFacts> {
210
+ const [{ readRecordsFor }, { RecordReadError }] = await Promise.all([import("./records-cli"), import("./records")]);
211
+ try {
212
+ const read = await readRecordsFor({ kind, cwd, ...(commit !== null ? { at: commit } : {}) });
213
+ return { kind, records: read.result.records.map((r) => ({ ...r, file: join(read.root, ...r.path.split("/")) })) };
214
+ } catch (err) {
215
+ if (!(err instanceof RecordReadError)) throw err;
216
+ return { kind, error: { code: err.code, message: err.message } };
217
+ }
218
+ }
219
+
203
220
  export async function runWorkspaceCheck(ctx: CommandContext): Promise<number> {
204
221
  const root = process.cwd();
205
222
  if (ctx.args.extraPositional) {
@@ -218,7 +235,7 @@ export async function runWorkspaceCheck(ctx: CommandContext): Promise<number> {
218
235
  const handed = await handToRootChant(root, ctx.args.at);
219
236
  if (handed !== undefined) return handed;
220
237
  }
221
- const doc = await runChecks(root, ctx.args.at, { runGenerators: ctx.args.generated === true });
238
+ const doc = await runChecks(root, ctx.args.at, { runGenerators: ctx.args.generated === true, ...(ctx.args.kind !== undefined ? { kind: ctx.args.kind } : {}) });
222
239
  if ("error" in doc) {
223
240
  if (ctx.args.json || format === "json") console.log(JSON.stringify(doc, null, 2));
224
241
  else console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
@@ -36,6 +36,7 @@ import {
36
36
  } from "./lineage-lock";
37
37
  import { MIGRATIONS_DIR } from "./lineage-migrations";
38
38
  import { TEMPLATE_MANIFEST, readManifest, resolveParameters, substituteParameters } from "./template-manifest";
39
+ import { repinSubstituted, type RepinnedRecord } from "./template-pins";
39
40
 
40
41
  // ── The template spec ────────────────────────────────────────────────────────
41
42
 
@@ -413,11 +414,13 @@ export async function initFromCommand(options: InitFromOptions): Promise<InitFro
413
414
  // so a refused --param leaves the target untouched.
414
415
  let parameters: Record<string, string>;
415
416
  let contents: Map<string, Buffer>;
417
+ let repinned: RepinnedRecord[];
416
418
  try {
417
419
  const raw = new Map([...fetched.files].map(([path, f]) => [path, f.data]));
418
420
  const manifest = readManifest(raw);
419
421
  parameters = resolveParameters(manifest, options.params ?? {});
420
- contents = substituteParameters(raw, manifest, parameters);
422
+ // Records that pin a substituted file get its new hash, so a copy's pins hold (#2549).
423
+ ({ files: contents, repinned } = repinSubstituted(raw, substituteParameters(raw, manifest, parameters), manifest?.files ?? []));
421
424
  } catch (err) {
422
425
  return { success: false, createdFiles, warnings, error: (err as Error).message };
423
426
  }
@@ -442,6 +445,7 @@ export async function initFromCommand(options: InitFromOptions): Promise<InitFro
442
445
 
443
446
  const common = {
444
447
  parameters,
448
+ ...(repinned.length > 0 ? { repinned: repinned.filter((r) => written.has(r.record)) } : {}),
445
449
  migrations: [],
446
450
  files: fileEntries(written, declaredFilesAt(targetDir)),
447
451
  manualSteps: [],
@@ -133,6 +133,12 @@ const LineageSchema = z
133
133
  address: AddressSchema.nullable(),
134
134
  /** The parameter values the template was instantiated with. Always empty for vendor scopes. */
135
135
  parameters: z.record(z.string(), z.unknown()),
136
+ /**
137
+ * Records whose evidence pins were re-pinned to the substituted content
138
+ * of a parameterised file (#2549): each record's path in the scope, and the
139
+ * pinned paths. An upgrade re-pins the base and the target the same way.
140
+ */
141
+ repinned: z.array(z.object({ record: z.string().min(1), paths: z.array(z.string().min(1)) }).strict()).optional(),
136
142
  /** Migrations applied since instantiation (#2550). */
137
143
  migrations: z.array(z.string()),
138
144
  /** Per file, relative to the scope directory, in sorted order. */
@@ -249,6 +255,7 @@ function canonical(lock: LineageLock): LineageLock {
249
255
  ...(s.ref !== undefined ? { ref: s.ref } : {}),
250
256
  address: s.address,
251
257
  parameters: s.parameters,
258
+ ...(s.repinned !== undefined ? { repinned: s.repinned } : {}),
252
259
  migrations: s.migrations,
253
260
  files,
254
261
  manualSteps: [...s.manualSteps].sort((a, b) => a.path.localeCompare(b.path)),
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
9
9
  import { execFileSync } from "node:child_process";
10
+ import { createHash } from "node:crypto";
10
11
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
11
12
  import { tmpdir } from "node:os";
12
13
  import { dirname, join } from "node:path";
@@ -455,6 +456,34 @@ describe("chant workspace upgrade with template parameters (#2627)", () => {
455
456
  }
456
457
  });
457
458
 
459
+ test("a record that pins a substituted file is re-pinned on the base and the target, so it updates with no manual step (#2549)", async () => {
460
+ const sha = (s: string) => createHash("sha256").update(s).digest("hex");
461
+ const record = (hash: string) => `---\nschema: 1\nid: "tpl-001"\nevidence:\n - title: "readme"\n path: "README.md"\n sha256: "${hash}"\n---\n`;
462
+ // A second project, from a v1 whose record pins the template's README.
463
+ tpl = join(root, "rtpl");
464
+ proj = join(root, "rproj");
465
+ mkdirSync(tpl);
466
+ git(tpl, ["init", "-q", "-b", "main"]);
467
+ const manifest = JSON.stringify({ parameters: { name: { type: "string", default: "Starter" } }, files: ["README.md"] });
468
+ release("v1.0.0", { "chant.template.json": manifest, "README.md": "# {{chant:name}}\n", "decisions/tpl-001-x.md": record(sha("# {{chant:name}}\n")) });
469
+ const made = await initFromCommand({ from: `${tpl}@v1.0.0`, path: proj, params: { name: "Acme" } });
470
+ expect(made.error).toBeUndefined();
471
+ expect(read(proj, "decisions/tpl-001-x.md")).toBe(record(sha("# Acme\n")));
472
+ expect(readLock(proj)!.scopes["."].repinned).toEqual([{ record: "decisions/tpl-001-x.md", paths: ["README.md"] }]);
473
+ git(proj, ["init", "-q", "-b", "main"]);
474
+ commitProject("init");
475
+
476
+ release("v2.0.0", { "README.md": "# {{chant:name}} v2\n", "decisions/tpl-001-x.md": record(sha("# {{chant:name}} v2\n")) });
477
+ const staged = await stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing });
478
+ try {
479
+ expect(staged.manualSteps).toEqual([]);
480
+ expect(read(staged.worktreeProject, "decisions/tpl-001-x.md")).toBe(record(sha("# Acme v2\n")));
481
+ expect(readLock(staged.worktreeProject)!.scopes["."].repinned).toEqual([{ record: "decisions/tpl-001-x.md", paths: ["README.md"] }]);
482
+ } finally {
483
+ staged.dispose();
484
+ }
485
+ });
486
+
458
487
  test("an upgrade to the same version changes nothing", async () => {
459
488
  const staged = await stageUpgrade({ root: proj, to: "v1.0.0", runChant: passing });
460
489
  try {
@@ -59,6 +59,7 @@ import { mergeFile } from "./lineage-merge";
59
59
  import { assertCodeAllowed, planMigrations, runMigration, splitMigrations, type LoadedMigration } from "./lineage-migrations";
60
60
  import { applyUpstream, type UpdateResult } from "./lineage-update";
61
61
  import { carryParameters, readManifest, substituteParameters } from "./template-manifest";
62
+ import { repinSubstituted, type RepinnedRecord } from "./template-pins";
62
63
 
63
64
  /** The kind the patch digest is taken under, so it never collides with another kind of plan. */
64
65
  export const UPGRADE_PLAN_KIND = "workspace-upgrade";
@@ -184,6 +185,8 @@ interface Upstream {
184
185
  tree?: string;
185
186
  /** The parameter values substituted into `files` (#2627), for a git or directory source. */
186
187
  parameters?: Record<string, string>;
188
+ /** The records re-pinned to substituted files (#2549). */
189
+ repinned?: RepinnedRecord[];
187
190
  /** The source the lock records after the upgrade, when it moves (a directory source). */
188
191
  source?: LineageSource;
189
192
  }
@@ -191,13 +194,20 @@ interface Upstream {
191
194
  /**
192
195
  * The template's files as the project would have them (#2627): the manifest
193
196
  * removed and the parameters substituted, with the recorded values and the
194
- * defaults of parameters this version adds.
197
+ * defaults of parameters this version adds. Records that pin a substituted
198
+ * file are re-pinned as init re-pinned them (#2549), for the base and the
199
+ * target alike, so an unedited record compares equal to its base.
195
200
  */
196
- function instantiate(raw: Map<string, Buffer>, recorded: Record<string, unknown>, label: string): { files: Map<string, Buffer>; parameters: Record<string, string> } {
201
+ function instantiate(
202
+ raw: Map<string, Buffer>,
203
+ recorded: Record<string, unknown>,
204
+ label: string,
205
+ ): { files: Map<string, Buffer>; parameters: Record<string, string>; repinned: RepinnedRecord[] } {
197
206
  try {
198
207
  const manifest = readManifest(raw);
199
208
  const parameters = carryParameters(manifest, recorded);
200
- return { files: substituteParameters(raw, manifest, parameters), parameters };
209
+ const { files, repinned } = repinSubstituted(raw, substituteParameters(raw, manifest, parameters), manifest?.files ?? []);
210
+ return { files, parameters, repinned };
201
211
  } catch (err) {
202
212
  throw new UpgradeError(`${label}: ${(err as Error).message.replace(/; pass --param .*$/, "")}`);
203
213
  }
@@ -264,6 +274,7 @@ function fetchGit(root: string, lineage: Lineage, ref: string): Upstream {
264
274
  commit,
265
275
  tree: target.tree,
266
276
  parameters: instantiated.parameters,
277
+ repinned: instantiated.repinned,
267
278
  };
268
279
  } finally {
269
280
  rmSync(scratch, { recursive: true, force: true });
@@ -336,6 +347,7 @@ function readDir(root: string, lineage: Lineage, to: string | undefined): Upstre
336
347
  migrations: split.migrations,
337
348
  modules: split.modules,
338
349
  parameters: instantiated.parameters,
350
+ repinned: instantiated.repinned,
339
351
  source: { type: "dir", path: recordedDirPath(target.path, target.abs, root), ...(member ? { member } : {}) },
340
352
  };
341
353
  }
@@ -561,6 +573,12 @@ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgra
561
573
  for (const path of result.written) if (upstream.executable.has(path)) chmodSync(join(scopeDir, path), 0o755);
562
574
  if (ref !== undefined) staged.ref = ref;
563
575
  if (upstream.parameters) staged.parameters = upstream.parameters;
576
+ if (upstream.repinned) {
577
+ // The records this version re-pins, among the files the scope keeps (#2549).
578
+ const kept = upstream.repinned.filter((r) => staged.files[r.record] !== undefined || existsSync(join(scopeDir, r.record)));
579
+ if (kept.length > 0) staged.repinned = kept;
580
+ else delete staged.repinned;
581
+ }
564
582
  if (upstream.commit) staged.address = { ...staged.address!, commit: upstream.commit, tree: upstream.tree };
565
583
  if (upstream.source) staged.source = upstream.source;
566
584
  writeLock(worktreeProject, next);
@@ -379,7 +379,7 @@ const USAGE: Record<WorkspaceVerb, string> = {
379
379
  build: "chant workspace build [dir] [--member <name>] [-o <dir>] [--format json|yaml] [--env <env>] [--param k=v] [--dry-run]",
380
380
  lint: "chant workspace lint [dir] [--member <name>] [--format stylish|json|sarif] [-o <file>] [--fix] [--dry-run]",
381
381
  audit: "chant workspace audit [dir] [--member <name>] [--format stylish|json] [-o <file>] [--tier <tier>] [--fail-on <level>] [--dry-run]",
382
- graph: "chant workspace graph [dir] [--member <name>] [-o <file>] [--env <env>] [--dry-run]",
382
+ graph: "chant workspace graph [dir] [--member <name>] [--kind <kind file>] [-o <file>] [--env <env>] [--dry-run]",
383
383
  };
384
384
 
385
385
  export function describePlan(plan: MemberPlan): string {
@@ -22,6 +22,8 @@ import { contract, git, REPO, validSchema } from "./__fixtures__/contract-repo";
22
22
  import checkSchema from "./check.schema.json";
23
23
  import { workspaceGraph } from "./graph-cli";
24
24
  import graphSchema from "./graph.schema.json";
25
+ import { intentGraph } from "./intent";
26
+ import intentSchema from "./intent.schema.json";
25
27
  import { runChecks } from "./lineage-check";
26
28
  import { listWorkspace } from "./ls";
27
29
  import lsSchema from "./ls.schema.json";
@@ -35,7 +37,7 @@ import statusSchema from "./status.schema.json";
35
37
  const FIXTURE = join(REPO, "reference-workspace");
36
38
  const TIMEOUT = 240_000;
37
39
 
38
- const SCHEMAS = { ls: lsSchema, graph: graphSchema, check: checkSchema, status: statusSchema, records: recordsSchema };
40
+ const SCHEMAS = { ls: lsSchema, graph: graphSchema, check: checkSchema, status: statusSchema, records: recordsSchema, intent: intentSchema };
39
41
 
40
42
  /** This checkout's chant, started the way the CLI starts it, for members with no toolchain of their own. */
41
43
  const reader: Toolchain = {
@@ -116,6 +118,19 @@ describe("every schema against the reference workspace (#2543)", () => {
116
118
  expect(doc.members.map((m) => m.name)).toEqual(["app", "delivery", "design-client", "design"]);
117
119
  });
118
120
 
121
+ test("graph --intent, in the working tree and at HEAD (#2651)", async () => {
122
+ const { expectValid } = contract(intentSchema);
123
+ for (const at of [undefined, "HEAD"]) {
124
+ const { doc, failed } = await intentGraph({ cwd: FIXTURE, region: "app/src/server.mjs:19", at, kinds: ["decisions/decision.kind.mjs"] });
125
+ expectValid(doc);
126
+ if ("error" in doc) throw new Error(doc.error.message);
127
+ expect(failed).toBe(false);
128
+ expect(doc.workspace).toEqual({ name: "reference", root: "reference-workspace" });
129
+ expect(doc.at).toBe(at ? head : null);
130
+ expect(doc.region).toBe("region:app/src/server.mjs:19");
131
+ }
132
+ });
133
+
119
134
  test("records, in the working tree and at HEAD", async () => {
120
135
  const { expectValid } = contract(recordsSchema);
121
136
  for (const at of [undefined, "HEAD"]) {
@@ -12,10 +12,11 @@ import { REPO } from "./__fixtures__/contract-repo";
12
12
  import { MEMBER_RUN_REASON_CODES } from "./compose-graph";
13
13
  import { WORKSPACE_ERROR_CODES } from "./declaration";
14
14
  import { GRAPH_ERROR_CODES } from "./graph-cli";
15
+ import { INTENT_ERROR_CODES, INTENT_FINDING_CODES, INTENT_REASON_CODES } from "./intent";
15
16
  import { CHECK_CODES, CHECK_ERROR_CODES } from "./lineage-check";
16
17
  import { GROUP_REASON_CODES, MEMBER_REASON_CODES } from "./ls";
17
18
  import { isReasonCode, REASON_CODES, REASONS } from "./reason-codes";
18
- import { READ_ERROR_CODES, RECORD_REASON_CODES } from "./records";
19
+ import { READ_ERROR_CODES, RECORD_REASON_CODES, RECORD_WARNING_CODES } from "./records";
19
20
  import { STATUS_ERROR_CODES, STATUS_REASON_CODES } from "./status";
20
21
 
21
22
  const HERE = import.meta.dirname;
@@ -31,7 +32,11 @@ const PER_COMMAND: Record<string, readonly string[]> = {
31
32
  STATUS_REASON_CODES,
32
33
  STATUS_ERROR_CODES,
33
34
  RECORD_REASON_CODES,
35
+ RECORD_WARNING_CODES,
34
36
  READ_ERROR_CODES,
37
+ INTENT_ERROR_CODES,
38
+ INTENT_FINDING_CODES,
39
+ INTENT_REASON_CODES,
35
40
  };
36
41
 
37
42
  /** Every string in an `enum` under a property named `code`, anywhere in a schema. */
@@ -93,7 +98,7 @@ describe("the closed list of reason codes", () => {
93
98
  });
94
99
 
95
100
  test("no source file emits a code outside the list", () => {
96
- const emitted = /(?:\bcode:\s*|(?:WorkspaceReadError|RecordReadError|StatusError)\(\s*)"([a-z0-9-]+)"/g;
101
+ const emitted = /(?:\bcode:\s*|(?:WorkspaceReadError|RecordReadError|StatusError|IntentError)\(\s*)"([a-z0-9-]+)"/g;
97
102
  let seen = 0;
98
103
  for (const file of sourceFiles(HERE)) {
99
104
  const text = readFileSync(file, "utf-8");
@@ -59,6 +59,11 @@ export const REASONS = {
59
59
  "record-id-duplicate": "Another record earlier in path order has the same id.",
60
60
  "record-supersedes-unknown": "A supersedes link names an id no record has.",
61
61
  "record-supersedes-conflict": "A second closed record supersedes a record another one already superseded.",
62
+ // A record that is valid but warned about (records, #2549).
63
+ "asset-drift": "A file the record pins by hash has changed: its bytes no longer hash to the pinned sha256.",
64
+ "asset-missing": "A file the record pins by hash does not exist in the tree read.",
65
+ "asset-stale": "A file the record pins is unchanged at the hash a record it supersedes pinned: the decision changed and the artifact did not follow.",
66
+ "record-supersedes-pending": "A supersedes link from a record whose state is weaker than the record it names, so the link has no effect yet.",
62
67
  // A records read that fails (records).
63
68
  "kind-unreadable": "The record kind file is missing or could not be imported.",
64
69
  "kind-invalid": "The record kind file exports no recordKind, or its shape is wrong.",
@@ -66,6 +71,24 @@ export const REASONS = {
66
71
  "schema-id-mismatch": "The schema's $id differs from the id the record kind names.",
67
72
  "schema-invalid": "The record schema itself does not compile.",
68
73
  "location-missing": "The records directory does not exist, in the tree or at the revision.",
74
+ // The intent graph (graph --intent, #2651): a read that fails.
75
+ "intent-region-invalid": "The region's path, or its line range, does not exist in the tree read.",
76
+ // The intent graph: part of the walk that can't be read. The document is still printed.
77
+ "intent-history-shallow": "The repository is a shallow clone, so the region's history stops at the clone's boundary.",
78
+ "intent-plugin-failed": "A kind file's commitJoins, which joins commits to units, contracts and evidence, failed for a commit.",
79
+ // The intent graph: findings, each a node in the graph.
80
+ "intent-commit-undecided": "A commit changed the region when no decision constrained it at path granularity.",
81
+ "intent-commit-bare": "A commit names no unit, no pull request and no decision covering the region at its time.",
82
+ "intent-pin-drifted": "A decision's pinned artifact no longer hashes to the pin.",
83
+ "intent-pin-missing": "A decision's pinned artifact does not exist in the tree read.",
84
+ "intent-artifact-unpinned": "An artifact decisions in the graph pinned, which no current decision pins.",
85
+ "intent-decision-superseded-live": "Every decision constraining the region is superseded.",
86
+ "intent-decision-provisional": "The current decisions constraining the region are all in states their kind does not close, such as decided.",
87
+ "intent-constraint-coarse": "The region is constrained only through its member, not by path.",
88
+ "intent-constraint-lost": "A decision's path constraint names a path that does not exist in the tree read.",
89
+ "intent-evidence-unpinned": "A decision's evidence has no hash: a URL, or a path with no sha256.",
90
+ "intent-trailer-unverified": "A commit carries a trailer a plugin says claims authorship, and the commit is not attested.",
91
+ "intent-region-unconstrained": "No decision constrains the region at any granularity.",
69
92
  // The lineage lock (check).
70
93
  "lock-invalid": "The lineage lock can't be read.",
71
94
  "manual-step-open": "A scope in the lineage lock has an open manual step.",