@intentius/chant 0.87.0 → 0.88.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/handlers/misc.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +4 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/version.d.ts +8 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/sessions.d.ts +9 -0
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +13 -0
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/environments.d.ts +80 -0
- package/dist/workspace/environments.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +14 -5
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +6 -2
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +41 -0
- package/dist/workspace/records-close.d.ts.map +1 -0
- package/dist/workspace/records-since.d.ts +40 -1
- package/dist/workspace/records-since.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +109 -11
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +40 -12
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runtimes.d.ts +6 -0
- package/dist/workspace/runtimes.d.ts.map +1 -1
- package/dist/workspace/session-kinds.d.ts +28 -0
- package/dist/workspace/session-kinds.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +2 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/trust/seal.d.ts +42 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/misc.ts +1 -9
- package/src/cli/main.ts +20 -8
- package/src/cli/mcp/server.test.ts +14 -1
- package/src/cli/mcp/server.ts +3 -1
- package/src/cli/registry.ts +4 -1
- package/src/cli/version.ts +15 -0
- package/src/workspace/__fixtures__/sessions.ts +41 -0
- package/src/workspace/composites.schema.json +68 -3
- package/src/workspace/composites.test.ts +119 -5
- package/src/workspace/composites.ts +26 -7
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +11 -0
- package/src/workspace/environments.ts +165 -0
- package/src/workspace/read-contract.test.ts +3 -0
- package/src/workspace/reason-codes.test.ts +8 -3
- package/src/workspace/reason-codes.ts +18 -6
- package/src/workspace/record-assets.test.ts +4 -3
- package/src/workspace/records-amend.schema.json +30 -1
- package/src/workspace/records-cli.ts +44 -7
- package/src/workspace/records-close.schema.json +192 -0
- package/src/workspace/records-close.ts +129 -0
- package/src/workspace/records-contract.test.ts +4 -3
- package/src/workspace/records-new.schema.json +25 -0
- package/src/workspace/records-review.schema.json +55 -2
- package/src/workspace/records-sessions-write.test.ts +274 -0
- package/src/workspace/records-since.schema.json +27 -2
- package/src/workspace/records-since.ts +120 -6
- package/src/workspace/records-write-contract.test.ts +5 -1
- package/src/workspace/records-write.test.ts +4 -2
- package/src/workspace/records-write.ts +307 -43
- package/src/workspace/records.schema.json +22 -3
- package/src/workspace/records.ts +73 -22
- package/src/workspace/runtimes.ts +12 -3
- package/src/workspace/session-kinds.ts +79 -0
- package/src/workspace/status.ts +1 -1
- package/src/workspace/trust/record-seal.test.ts +315 -0
- package/src/workspace/trust/seal.test.ts +4 -14
- package/src/workspace/trust/seal.ts +119 -25
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environments a member's components may deploy to, for `chant workspace
|
|
3
|
+
* graph --composites` (#2695), so a reader such as hud can offer staging or
|
|
4
|
+
* prod beside the default without guessing a name.
|
|
5
|
+
*
|
|
6
|
+
* Each environment comes from one of three places, and says which:
|
|
7
|
+
*
|
|
8
|
+
* - `config`: a name the member's `chant.config.ts` declares in
|
|
9
|
+
* `environments`. A pattern such as `pr-*` names no environment by itself
|
|
10
|
+
* and is not listed, but it legalizes the ledger's `pr-42`.
|
|
11
|
+
* - `ledger`: an environment with a release ledger for the member on
|
|
12
|
+
* `chant/lifecycle` (`_members/<member>/<env>/releases.jsonl`, or the flat
|
|
13
|
+
* `<env>/releases.jsonl`, read the way `workspace status` reads it).
|
|
14
|
+
* - `builtin`: `local`, which `chant run --components` uses without `--env`
|
|
15
|
+
* (`../cli/handlers/run.ts`), when neither the config nor the ledger names
|
|
16
|
+
* it.
|
|
17
|
+
*
|
|
18
|
+
* The component contract declares no environments, so every component of a
|
|
19
|
+
* member lists the member's environments.
|
|
20
|
+
*
|
|
21
|
+
* `chant run --env` refuses a name the config's `environments` doesn't cover
|
|
22
|
+
* whenever it declares any (`unknownEnvError` in `../env.ts`), so a ledger
|
|
23
|
+
* environment the config no longer covers is left out, with a reason. The
|
|
24
|
+
* default is `local`: `chant.config.ts` has no default environment, and
|
|
25
|
+
* `chant run` without `--env` deploys to `local` even when the config's
|
|
26
|
+
* `environments` leaves it out.
|
|
27
|
+
*
|
|
28
|
+
* The config is the one `runtimes.ts` loads, in the working tree or the tree
|
|
29
|
+
* `--at` exported. The ledger is always the local `chant/lifecycle` branch,
|
|
30
|
+
* as for `workspace status`, and is never fetched.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { execFileSync } from "node:child_process";
|
|
34
|
+
import { environmentName, isEnvironmentPattern, matchesDeclaredEnvironment } from "../config";
|
|
35
|
+
import type { ReasonCode } from "./reason-codes";
|
|
36
|
+
import { LOCAL_RUNTIME, type MemberRuntimes } from "./runtimes";
|
|
37
|
+
import { ENV_PATTERN, LIFECYCLE_REF, MEMBERS_DIR } from "./status";
|
|
38
|
+
|
|
39
|
+
/** The environment `chant run --components` deploys to without `--env`. */
|
|
40
|
+
export const LOCAL_ENVIRONMENT = "local";
|
|
41
|
+
|
|
42
|
+
/** Why a member's environments are only `local`, or leave out one the ledger has. Closed: part of the read contract. */
|
|
43
|
+
export const ENVIRONMENT_REASON_CODES = [
|
|
44
|
+
/** The member's chant.config.ts could not be read: the same read and the same code as its runtimes. */
|
|
45
|
+
"runtimes-config-unreadable",
|
|
46
|
+
/** The config declares no environments, so only local and the ledger's environments are listed. */
|
|
47
|
+
"environments-none-declared",
|
|
48
|
+
/** The ledger has releases in an environment the config's environments no longer cover, so chant run --env would refuse it. */
|
|
49
|
+
"environments-ledger-undeclared",
|
|
50
|
+
/** The chant/lifecycle branch exists and the member's ledger directories could not be listed. */
|
|
51
|
+
"environments-ledger-unreadable",
|
|
52
|
+
] as const satisfies readonly ReasonCode[];
|
|
53
|
+
export type EnvironmentReasonCode = (typeof ENVIRONMENT_REASON_CODES)[number];
|
|
54
|
+
|
|
55
|
+
export interface EnvironmentReason {
|
|
56
|
+
code: EnvironmentReasonCode;
|
|
57
|
+
message: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One environment a component may deploy to. */
|
|
61
|
+
export interface ComponentEnvironment {
|
|
62
|
+
/** What `--env` takes. */
|
|
63
|
+
name: string;
|
|
64
|
+
/** True for the environment `chant run --components` deploys to without `--env`. */
|
|
65
|
+
default: boolean;
|
|
66
|
+
/** Where the name came from; the first of config, ledger and builtin that names it. */
|
|
67
|
+
source: "config" | "ledger" | "builtin";
|
|
68
|
+
/** The command that deploys the component there on the default runtime, run in the member's directory. */
|
|
69
|
+
command: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The environments a member's ledger has, or why they couldn't be listed. */
|
|
73
|
+
export interface LedgerEnvironments {
|
|
74
|
+
envs: string[];
|
|
75
|
+
reason: EnvironmentReason | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Lists one member's ledger environments; git unless a test swaps it. */
|
|
79
|
+
export type LedgerEnvironmentReader = (member: { name: string; dir: string }, cwd: string) => LedgerEnvironments;
|
|
80
|
+
|
|
81
|
+
/** What one member's config and ledger say about environments. */
|
|
82
|
+
export interface MemberEnvironments {
|
|
83
|
+
/** Each environment in list order, without the per-component command. */
|
|
84
|
+
environments: Omit<ComponentEnvironment, "command">[];
|
|
85
|
+
reasons: EnvironmentReason[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function git(cwd: string, args: string[], input?: string): string {
|
|
89
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", input, stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"] });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function exists(cwd: string, spec: string): boolean {
|
|
93
|
+
try {
|
|
94
|
+
git(cwd, ["rev-parse", "--verify", "--quiet", spec]);
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The environments with a release ledger for `member` on the local
|
|
103
|
+
* `chant/lifecycle` branch: the directories under `_members/<member>/` when it
|
|
104
|
+
* exists (the root member never has one), else the flat layout's top-level
|
|
105
|
+
* directories, that hold a `releases.jsonl`.
|
|
106
|
+
*/
|
|
107
|
+
export const readLedgerEnvironments: LedgerEnvironmentReader = (member, cwd) => {
|
|
108
|
+
if (!exists(cwd, `refs/heads/${LIFECYCLE_REF}`)) return { envs: [], reason: null };
|
|
109
|
+
const own = member.dir !== "." && exists(cwd, `refs/heads/${LIFECYCLE_REF}:${MEMBERS_DIR}/${member.name}`);
|
|
110
|
+
const base = own ? `${MEMBERS_DIR}/${member.name}/` : "";
|
|
111
|
+
try {
|
|
112
|
+
const dirs = git(cwd, ["ls-tree", "--full-tree", `refs/heads/${LIFECYCLE_REF}:${base}`])
|
|
113
|
+
.split("\n")
|
|
114
|
+
.map((line) => line.match(/^\d+ tree [0-9a-f]+\t(.+)$/)?.[1])
|
|
115
|
+
.filter((name): name is string => !!name && ENV_PATTERN.test(name));
|
|
116
|
+
if (dirs.length === 0) return { envs: [], reason: null };
|
|
117
|
+
const checked = git(cwd, ["cat-file", "--batch-check"], dirs.map((d) => `refs/heads/${LIFECYCLE_REF}:${base}${d}/releases.jsonl\n`).join(""))
|
|
118
|
+
.split("\n")
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
return { envs: dirs.filter((_, i) => checked[i] !== undefined && !checked[i].endsWith(" missing")).sort(), reason: null };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const message = (err instanceof Error ? err.message : String(err)).split("\n")[0];
|
|
123
|
+
return { envs: [], reason: { code: "environments-ledger-unreadable", message: `${LIFECYCLE_REF}:${base || "."}: ${message}` } };
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Join a member's config and ledger into its environment list, `local` first, then config order, then the ledger's. */
|
|
128
|
+
export function memberEnvironments(runtimes: MemberRuntimes | undefined, ledger: LedgerEnvironments | undefined): MemberEnvironments {
|
|
129
|
+
const reasons: EnvironmentReason[] = [];
|
|
130
|
+
const declared = runtimes?.environments ?? null;
|
|
131
|
+
if (runtimes && declared === null) {
|
|
132
|
+
const unreadable = runtimes.reasons.find((r) => r.code === "runtimes-config-unreadable");
|
|
133
|
+
reasons.push({ code: "runtimes-config-unreadable", message: unreadable?.message ?? "chant.config.ts could not be read" });
|
|
134
|
+
} else if (declared && declared.length === 0) {
|
|
135
|
+
reasons.push({ code: "environments-none-declared", message: "chant.config.ts declares no environments, so only local and the environments in the ledger are listed" });
|
|
136
|
+
}
|
|
137
|
+
if (ledger?.reason) reasons.push(ledger.reason);
|
|
138
|
+
|
|
139
|
+
const literals = (declared ?? []).map(environmentName).filter((n) => !isEnvironmentPattern(n));
|
|
140
|
+
const ledgerEnvs = ledger?.envs ?? [];
|
|
141
|
+
const covered = (env: string): boolean => !declared || declared.length === 0 || matchesDeclaredEnvironment(declared, env);
|
|
142
|
+
const undeclared = ledgerEnvs.filter((e) => e !== LOCAL_ENVIRONMENT && !covered(e));
|
|
143
|
+
if (undeclared.length > 0) {
|
|
144
|
+
reasons.push({
|
|
145
|
+
code: "environments-ledger-undeclared",
|
|
146
|
+
message: `the ledger has releases in ${undeclared.join(", ")}, which chant.config.ts's environments don't cover, so chant run --env refuses ${undeclared.length === 1 ? "it" : "them"}`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const out: MemberEnvironments["environments"] = [];
|
|
151
|
+
const add = (name: string, source: ComponentEnvironment["source"]) => {
|
|
152
|
+
if (!out.some((e) => e.name === name)) out.push({ name, default: name === LOCAL_ENVIRONMENT, source });
|
|
153
|
+
};
|
|
154
|
+
add(LOCAL_ENVIRONMENT, literals.includes(LOCAL_ENVIRONMENT) ? "config" : ledgerEnvs.includes(LOCAL_ENVIRONMENT) ? "ledger" : "builtin");
|
|
155
|
+
for (const name of literals) add(name, "config");
|
|
156
|
+
for (const name of ledgerEnvs) if (covered(name)) add(name, "ledger");
|
|
157
|
+
return { environments: out, reasons };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The environments one component may deploy to, each with its command line on the member's default runtime. */
|
|
161
|
+
export function componentEnvironments(component: string, member: MemberEnvironments | undefined, runtime = LOCAL_RUNTIME): ComponentEnvironment[] {
|
|
162
|
+
const list = member?.environments ?? [{ name: LOCAL_ENVIRONMENT, default: true, source: "builtin" as const }];
|
|
163
|
+
const on = runtime === LOCAL_RUNTIME ? "" : ` --on ${runtime}`;
|
|
164
|
+
return list.map((e) => ({ ...e, command: `chant run --components ${component}${on}${e.default ? "" : ` --env ${e.name}`}` }));
|
|
165
|
+
}
|
|
@@ -160,6 +160,9 @@ describe("every schema against the reference workspace (#2543)", () => {
|
|
|
160
160
|
// #2674: the reference config's lexicons host no component runs, so local is the only runtime, read at HEAD too.
|
|
161
161
|
expect(doc.components[0].runtimes).toEqual([{ name: "local", lexicon: null, default: true, command: "chant run --components app" }]);
|
|
162
162
|
expect(doc.members.find((m) => m.name === "delivery")!.runtimeReasons).toEqual([]);
|
|
163
|
+
// #2695: the reference config declares no environments, so the app deploys to local only.
|
|
164
|
+
expect(doc.components[0].environments).toEqual([{ name: "local", default: true, source: "builtin", command: "chant run --components app" }]);
|
|
165
|
+
expect(doc.members.find((m) => m.name === "delivery")!.environmentReasons.map((r) => r.code)).toEqual(["environments-none-declared"]);
|
|
163
166
|
expect(doc.reasons).toEqual([]);
|
|
164
167
|
}
|
|
165
168
|
},
|
|
@@ -10,7 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { describe, expect, test } from "vitest";
|
|
11
11
|
import { REPO } from "./__fixtures__/contract-repo";
|
|
12
12
|
import { MEMBER_RUN_REASON_CODES } from "./compose-graph";
|
|
13
|
-
import { COMPOSITES_ERROR_CODES, COMPOSITES_REASON_CODES, COMPOSITES_RUNTIME_REASON_CODES } from "./composites";
|
|
13
|
+
import { COMPOSITES_ENVIRONMENT_REASON_CODES, COMPOSITES_ERROR_CODES, COMPOSITES_REASON_CODES, COMPOSITES_RUNTIME_REASON_CODES } from "./composites";
|
|
14
14
|
import { WORKSPACE_ERROR_CODES } from "./declaration";
|
|
15
15
|
import { GRAPH_ERROR_CODES } from "./graph-cli";
|
|
16
16
|
import { INTENT_ERROR_CODES, INTENT_FINDING_CODES, INTENT_REASON_CODES } from "./intent";
|
|
@@ -19,9 +19,10 @@ import { GROUP_REASON_CODES, MEMBER_REASON_CODES } from "./ls";
|
|
|
19
19
|
import intentSchema from "./intent.schema.json";
|
|
20
20
|
import { isPluginCode, isReasonCode, REASON_CODES, REASONS } from "./reason-codes";
|
|
21
21
|
import { contract } from "./__fixtures__/contract-repo";
|
|
22
|
-
import { READ_ERROR_CODES, RECORD_REASON_CODES, RECORD_WARNING_CODES, REVIEW_REASON_CODES, SEAL_REASON_CODES } from "./records";
|
|
22
|
+
import { READ_ERROR_CODES, RECORD_REASON_CODES, RECORD_WARNING_CODES, REVIEW_REASON_CODES, SEAL_REASON_CODES, SEAL_WARNING_CODES } from "./records";
|
|
23
23
|
import { AMEND_ERROR_CODES, NEW_ERROR_CODES, REVIEW_ERROR_CODES } from "./records-write";
|
|
24
|
-
import {
|
|
24
|
+
import { CLOSE_ERROR_CODES } from "./records-close";
|
|
25
|
+
import { RECORDS_SINCE_ERROR_CODES, RECORDS_SINCE_REASON_CODES } from "./records-since";
|
|
25
26
|
import { STATUS_ERROR_CODES, STATUS_GATE_REASON_CODES, STATUS_REASON_CODES } from "./status";
|
|
26
27
|
import { WORK_WARNING_CODES } from "./work";
|
|
27
28
|
|
|
@@ -42,18 +43,22 @@ const PER_COMMAND: Record<string, readonly string[]> = {
|
|
|
42
43
|
RECORD_WARNING_CODES,
|
|
43
44
|
REVIEW_REASON_CODES,
|
|
44
45
|
SEAL_REASON_CODES,
|
|
46
|
+
SEAL_WARNING_CODES,
|
|
45
47
|
WORK_WARNING_CODES,
|
|
46
48
|
READ_ERROR_CODES,
|
|
47
49
|
NEW_ERROR_CODES,
|
|
48
50
|
AMEND_ERROR_CODES,
|
|
49
51
|
REVIEW_ERROR_CODES,
|
|
52
|
+
CLOSE_ERROR_CODES,
|
|
50
53
|
RECORDS_SINCE_ERROR_CODES,
|
|
54
|
+
RECORDS_SINCE_REASON_CODES,
|
|
51
55
|
INTENT_ERROR_CODES,
|
|
52
56
|
INTENT_FINDING_CODES,
|
|
53
57
|
INTENT_REASON_CODES,
|
|
54
58
|
COMPOSITES_ERROR_CODES,
|
|
55
59
|
COMPOSITES_REASON_CODES,
|
|
56
60
|
COMPOSITES_RUNTIME_REASON_CODES,
|
|
61
|
+
COMPOSITES_ENVIRONMENT_REASON_CODES,
|
|
57
62
|
};
|
|
58
63
|
|
|
59
64
|
/** Every string in an `enum` under a property named `code`, anywhere in a schema. */
|
|
@@ -87,11 +87,13 @@ export const REASONS = {
|
|
|
87
87
|
"review-duplicate": "A later verdict by the same principal replaces this one. Names are compared after NFKC, trimming and lower-casing.",
|
|
88
88
|
"review-older-digest": "The verdict names a digest other than the record's text now: the record changed after the verdict.",
|
|
89
89
|
"review-unattested": "An attestation policy is active at base, and the verdict carries no seal that verifies for its reviewer.",
|
|
90
|
-
// A
|
|
91
|
-
"seal-missing": "The verdict carries no seal.",
|
|
92
|
-
"seal-signer-unlisted": "The reviewer has no key in the signers file at base, so the seal can't count.",
|
|
93
|
-
"seal-signature-invalid": "The seal is malformed, names a signer other than the reviewer, or its signature does not verify over the verdict.",
|
|
90
|
+
// A seal that is not attested (records): a verdict's (#2687) or a record's author seal (#2688). Each reports one of these in its attestation, unless its seal verified.
|
|
91
|
+
"seal-missing": "The verdict, or the record, carries no seal.",
|
|
92
|
+
"seal-signer-unlisted": "The reviewer, or the record's author, has no key in the signers file at base, so the seal can't count.",
|
|
93
|
+
"seal-signature-invalid": "The seal is malformed, names a signer other than the reviewer or author, or its signature does not verify over the verdict or record.",
|
|
94
94
|
"seal-unverifiable": "Nothing here can say whose seal it is: there is no signers file at base, or ssh-keygen is not installed.",
|
|
95
|
+
// A record whose author seal is not attested under a signers file at base (records, #2688). A warning: the record is still read.
|
|
96
|
+
"record-unattested": "A signers file is active at base, and the record names an author whose seal does not verify: it has none, the author has no key in the file, or the signature fails.",
|
|
95
97
|
// A records read that fails (records).
|
|
96
98
|
"kind-unreadable": "The record kind file is missing or could not be imported.",
|
|
97
99
|
"kind-invalid": "The record kind file exports no recordKind, or its shape is wrong.",
|
|
@@ -112,8 +114,14 @@ export const REASONS = {
|
|
|
112
114
|
"review-unsupported": "The kind's schema has no reviews field, so its records take no review.",
|
|
113
115
|
"review-note-required": "A dissent was given with no note: a dissent needs a reason.",
|
|
114
116
|
"review-sign-failed": "--sign was given and no seal could be made: the key can't be read or used, git names no ssh signing key, or ssh-keygen is not installed.",
|
|
117
|
+
"record-sign-failed": "--sign was given and no author seal could be made: the record names no author, the key can't be read or used, git names no ssh signing key, or ssh-keygen is not installed.",
|
|
118
|
+
// A review given in a session (records review --session, #2693).
|
|
119
|
+
"session-unknown": "--session names no session of a session kind whose subjects are the record's kind.",
|
|
120
|
+
"session-not-open": "--session names a session in a closed state, which takes no more verdicts.",
|
|
115
121
|
// records --since that fails (#2673).
|
|
116
|
-
"since-rev-unknown": "--since names no commit.",
|
|
122
|
+
"since-rev-unknown": "--since names no commit, or a session with no opening revision and no commit that added it.",
|
|
123
|
+
"since-session-unknown": "--since has the shape of a session id and names no commit, and no session the kind or the declaration reads has that id.",
|
|
124
|
+
"since-session-open": "--since names a session that is still open, so the comparison runs to the working tree.",
|
|
117
125
|
// The intent graph (graph --intent, #2651): a read that fails.
|
|
118
126
|
"intent-region-invalid": "The region's path, or its line range, does not exist in the tree read.",
|
|
119
127
|
// The intent graph: part of the walk that can't be read. The document is still printed.
|
|
@@ -142,8 +150,12 @@ export const REASONS = {
|
|
|
142
150
|
"composites-none-declared": "The members read declare no composite instance.",
|
|
143
151
|
"composites-no-component": "The members read declare no component, so no composite instance has one.",
|
|
144
152
|
// The runtimes a member's components can deploy on (graph --composites, #2674).
|
|
145
|
-
"runtimes-config-unreadable": "The member's chant.config.ts could not be read, so only the built-in local runtime is listed.",
|
|
153
|
+
"runtimes-config-unreadable": "The member's chant.config.ts could not be read, so only the built-in local runtime is listed, and no environment from the config.",
|
|
146
154
|
"runtimes-lexicon-unreadable": "A lexicon the member's config lists could not be loaded, so it is not listed as a runtime.",
|
|
155
|
+
// The environments a member's components may deploy to (graph --composites, #2695).
|
|
156
|
+
"environments-none-declared": "The member's chant.config.ts declares no environments, so only local and the environments in its ledger are listed.",
|
|
157
|
+
"environments-ledger-undeclared": "The member's ledger has releases in an environment its config's environments don't cover, so chant run --env would refuse it and it is not listed.",
|
|
158
|
+
"environments-ledger-unreadable": "The chant/lifecycle branch exists and the member's ledger environments could not be listed.",
|
|
147
159
|
// The lineage lock (check).
|
|
148
160
|
"lock-invalid": "The lineage lock can't be read.",
|
|
149
161
|
"manual-step-open": "A scope in the lineage lock has an open manual step.",
|
|
@@ -16,7 +16,7 @@ import { cleanScratch, commitAll, contract, declaration, repo, REPO } from "./__
|
|
|
16
16
|
import { runChecks } from "./lineage-check";
|
|
17
17
|
import { constraintCovers, isWorkspacePath, memberHolding, WORKSPACE_PATH_PATTERN } from "./record-assets";
|
|
18
18
|
import { pinFile, queryRecords, type RecordsDocument } from "./records-cli";
|
|
19
|
-
import { parseFrontMatter, RECORD_WARNING_CODES } from "./records";
|
|
19
|
+
import { parseFrontMatter, RECORD_WARNING_CODES, SEAL_WARNING_CODES } from "./records";
|
|
20
20
|
import { WORK_WARNING_CODES } from "./work";
|
|
21
21
|
import { workspaceGraph } from "./graph-cli";
|
|
22
22
|
import graphSchema from "./graph.schema.json";
|
|
@@ -169,8 +169,9 @@ describe("chant workspace records checks each pin", () => {
|
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
test("the schema lists exactly the warning codes", () => {
|
|
172
|
-
// A work kind's records carry the work warnings too (#2683), work-done-gap-open included since records walks a done item's region (#2686)
|
|
173
|
-
|
|
172
|
+
// A work kind's records carry the work warnings too (#2683), work-done-gap-open included since records walks a done item's region (#2686),
|
|
173
|
+
// and records adds record-unattested for an author seal under a signers file at base (#2688).
|
|
174
|
+
expect(recordsSchema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...SEAL_WARNING_CODES]);
|
|
174
175
|
});
|
|
175
176
|
|
|
176
177
|
test("records pin <path> prints the entry's path from the workspace root and the file's hash", () => {
|
|
@@ -67,7 +67,35 @@
|
|
|
67
67
|
"items": {
|
|
68
68
|
"type": "string"
|
|
69
69
|
},
|
|
70
|
-
"description": "The top-level fields whose value differs from the record as it was, in the record's order. Empty when the fields set change nothing, and then nothing is written."
|
|
70
|
+
"description": "The top-level fields whose value differs from the record as it was, in the record's order, with seal last when the author seal was written or removed (#2688). Empty when the fields set change nothing, and then nothing is written."
|
|
71
|
+
},
|
|
72
|
+
"seal": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"description": "Added in contract 1 by #2688. With --sign only: the author seal written into the record's top-level seal field, an ssh signature by the key given (or git's user.signingkey) over <id>\\n<digest>\\n<author>\\n<state>, in the ssh-keygen namespace chant-record. The author is the kind's reviews.decider field (decided_by for decisions), and the digest is the record's digest by the digest rule, which leaves the seal field out. The write does not check the key against the signers file; chant workspace records reports whether the seal verifies.",
|
|
75
|
+
"required": [
|
|
76
|
+
"signer",
|
|
77
|
+
"key",
|
|
78
|
+
"signature"
|
|
79
|
+
],
|
|
80
|
+
"properties": {
|
|
81
|
+
"signer": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "The record's author, as its reviews.decider field names them."
|
|
84
|
+
},
|
|
85
|
+
"key": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"pattern": "^SHA256:[A-Za-z0-9+/]+=*$",
|
|
88
|
+
"description": "The fingerprint of the key that signed, read back from the signature."
|
|
89
|
+
},
|
|
90
|
+
"signature": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "The armored ssh signature."
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"sealDropped": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "Added in contract 1 by #2688. Present when the record carried an author seal, the amendment moved its digest and --sign was not given: the seal no longer covers the text, so it was removed, and this says so. changed then lists seal."
|
|
71
99
|
},
|
|
72
100
|
"dryRun": {
|
|
73
101
|
"type": "boolean",
|
|
@@ -145,6 +173,7 @@
|
|
|
145
173
|
"amend-id-immutable",
|
|
146
174
|
"record-closed",
|
|
147
175
|
"amend-supersede-instead",
|
|
176
|
+
"record-sign-failed",
|
|
148
177
|
"record-unparseable",
|
|
149
178
|
"record-schema-invalid",
|
|
150
179
|
"record-id-duplicate",
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
loadRecordKind,
|
|
35
35
|
normalisePrincipal,
|
|
36
36
|
readRecords,
|
|
37
|
+
RECORD_SEAL_FIELD,
|
|
37
38
|
RecordReadError,
|
|
38
39
|
type LoadedRecordKind,
|
|
39
40
|
type Quorum,
|
|
@@ -43,11 +44,14 @@ import {
|
|
|
43
44
|
type RecordFormat,
|
|
44
45
|
type RecordHistory,
|
|
45
46
|
type SealInput,
|
|
47
|
+
type VerdictAttestation,
|
|
46
48
|
} from "./records";
|
|
47
49
|
import { gitTree, workingTree, type WorkspaceTree } from "./tree";
|
|
48
50
|
import type { DecisionWork } from "./work";
|
|
49
51
|
import { activeAttestors, type ProvenanceLevel } from "./trust/attestor";
|
|
50
52
|
import { policyAtBase, recordProvenance, resolveBase, type BaseSource, type RecordProvenance } from "./trust/provenance";
|
|
53
|
+
import type { TrustPolicy } from "./trust/policy";
|
|
54
|
+
import type { SealedRecord } from "./trust/seal";
|
|
51
55
|
|
|
52
56
|
/** The version of the `records` output this chant writes. */
|
|
53
57
|
export const RECORDS_CONTRACT_VERSION = 1;
|
|
@@ -56,7 +60,7 @@ export const RECORDS_CONTRACT_VERSION = 1;
|
|
|
56
60
|
export const RECORDS_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/records/v1/records.schema.json";
|
|
57
61
|
|
|
58
62
|
const USAGE =
|
|
59
|
-
"chant workspace records [--kind <kind file>] [--current] [--at <rev>] [--base <rev>] [--require attested] [--json] | chant workspace records [--kind <kind file>] --since <rev> [--at <rev>] [--json] | chant workspace records pin <path> | chant workspace records new|amend|review (#2670)";
|
|
63
|
+
"chant workspace records [--kind <kind file>] [--current] [--at <rev>] [--base <rev>] [--require attested] [--json] | chant workspace records [--kind <kind file>] --since <rev|session id> [--at <rev>] [--json] | chant workspace records pin <path> | chant workspace records new|amend|review|close (#2670, #2693)";
|
|
60
64
|
|
|
61
65
|
/** Exit code when the read worked and a record falls below `--require`. */
|
|
62
66
|
export const EXIT_BELOW_REQUIRED = 2;
|
|
@@ -79,9 +83,16 @@ export interface RecordsQuery {
|
|
|
79
83
|
|
|
80
84
|
/**
|
|
81
85
|
* A record as the output carries it: the entry plus its provenance (#2547),
|
|
82
|
-
* and
|
|
86
|
+
* and, when the kind has a reviews list, its quorum (#2671) and what its
|
|
87
|
+
* author seal establishes (#2688).
|
|
83
88
|
*/
|
|
84
|
-
export type RecordView = RecordEntry & {
|
|
89
|
+
export type RecordView = RecordEntry & {
|
|
90
|
+
provenance: RecordProvenance;
|
|
91
|
+
quorum?: Quorum | null;
|
|
92
|
+
/** true when the record's seal verifies for its author against the signers at base; false when it fails, or is missing under an active policy; null when nothing here can say (#2688). */
|
|
93
|
+
attested?: boolean | null;
|
|
94
|
+
attestation?: VerdictAttestation;
|
|
95
|
+
};
|
|
85
96
|
|
|
86
97
|
/** The role in the trust policy whose holders' verdicts the quorum does not count (#2671). */
|
|
87
98
|
export const AGENT_ROLE = "agent";
|
|
@@ -276,7 +287,8 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
276
287
|
// The quorum: the need from the declaration in the tree read, agents,
|
|
277
288
|
// whether verdicts need a seal, and the keys a seal verifies against,
|
|
278
289
|
// all from the policy at base (#2671, #2687).
|
|
279
|
-
const
|
|
290
|
+
const seals = loaded.kind.reviews ? await import("./trust/seal") : undefined;
|
|
291
|
+
const checkVerdictSeal = seals?.checkVerdictSeal;
|
|
280
292
|
const quorumOptions = checkVerdictSeal
|
|
281
293
|
? {
|
|
282
294
|
...declaredQuorum(tree),
|
|
@@ -289,6 +301,7 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
289
301
|
...r,
|
|
290
302
|
provenance: provenance.get(r.path)!,
|
|
291
303
|
...(quorumOptions ? { quorum: computeQuorum(loaded.kind, r, quorumOptions) } : {}),
|
|
304
|
+
...(seals ? authorSeal(loaded.kind, r, policy, seals.checkRecordSeal) : {}),
|
|
292
305
|
}));
|
|
293
306
|
if (loaded.kind.work && query.workGaps !== false && top) await raiseWorkGaps(loaded, records, { root, workspaceRoot, at: query.at });
|
|
294
307
|
return {
|
|
@@ -314,6 +327,29 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
314
327
|
}
|
|
315
328
|
}
|
|
316
329
|
|
|
330
|
+
/**
|
|
331
|
+
* A record's author seal checked against the policy at base (#2688), for a
|
|
332
|
+
* kind with a reviews list: its author is the kind's `reviews.decider` field.
|
|
333
|
+
* Under an active signers file, a record that names an author and is not
|
|
334
|
+
* attested gains the warning `record-unattested`, and is still read: sealing
|
|
335
|
+
* records is opt-in for now. A record that can't be parsed gets neither field.
|
|
336
|
+
*/
|
|
337
|
+
function authorSeal(
|
|
338
|
+
kind: LoadedRecordKind["kind"],
|
|
339
|
+
r: RecordEntry,
|
|
340
|
+
policy: TrustPolicy,
|
|
341
|
+
check: (policy: TrustPolicy, r: SealedRecord) => { attested: boolean | null } & VerdictAttestation,
|
|
342
|
+
): { attested?: boolean | null; attestation?: VerdictAttestation } {
|
|
343
|
+
if (r.data === null) return {};
|
|
344
|
+
const field = kind.reviews!.decider;
|
|
345
|
+
const author = typeof r.data[field] === "string" && (r.data[field] as string).trim() !== "" ? (r.data[field] as string) : null;
|
|
346
|
+
const { attested, ...attestation } = check(policy, { record: r.id, digest: r.digest, author, authorField: field, state: r.state, seal: r.data[RECORD_SEAL_FIELD] });
|
|
347
|
+
if (policy.active && author !== null && attested !== true) {
|
|
348
|
+
r.warnings.push({ code: "record-unattested", message: `an attestation policy is active at base, and ${attestation.message}` });
|
|
349
|
+
}
|
|
350
|
+
return { attested, attestation };
|
|
351
|
+
}
|
|
352
|
+
|
|
317
353
|
/**
|
|
318
354
|
* `work-done-gap-open` on a records read (#2686): for each done work record
|
|
319
355
|
* whose `source` names a finding and a region, walk that region with the
|
|
@@ -383,7 +419,7 @@ export function pinFile(file: string, cwd: string): { path: string; sha256: stri
|
|
|
383
419
|
|
|
384
420
|
export async function runWorkspaceRecords(ctx: CommandContext): Promise<number> {
|
|
385
421
|
const { args } = ctx;
|
|
386
|
-
if (args.extraPositional === "new" || args.extraPositional === "amend" || args.extraPositional === "review") {
|
|
422
|
+
if (args.extraPositional === "new" || args.extraPositional === "amend" || args.extraPositional === "review" || args.extraPositional === "close") {
|
|
387
423
|
return (await import("./records-write")).runRecordsWrite(ctx);
|
|
388
424
|
}
|
|
389
425
|
if (args.extraPositional === "pin") {
|
|
@@ -400,7 +436,7 @@ export async function runWorkspaceRecords(ctx: CommandContext): Promise<number>
|
|
|
400
436
|
return 0;
|
|
401
437
|
}
|
|
402
438
|
if (args.extraPositional) {
|
|
403
|
-
console.error(formatError({ message: `chant workspace records takes no argument but pin, new, amend or
|
|
439
|
+
console.error(formatError({ message: `chant workspace records takes no argument but pin, new, amend, review or close (got ${args.extraPositional})`, hint: USAGE }));
|
|
404
440
|
return 1;
|
|
405
441
|
}
|
|
406
442
|
if (!args.kind) return runDeclaredRecords(args);
|
|
@@ -555,7 +591,8 @@ function formatRecords(records: RecordView[], summary: { total: number; valid: n
|
|
|
555
591
|
const flag = r.valid ? "" : " INVALID";
|
|
556
592
|
const superseded = r.supersededBy ? ` superseded by ${r.supersededBy}` : "";
|
|
557
593
|
const attested = r.provenance.level === "attested" ? ` attested by ${r.provenance.principal}` : "";
|
|
558
|
-
|
|
594
|
+
const sealed = r.attested === true ? ` sealed by ${(r.data?.[RECORD_SEAL_FIELD] as { signer: string }).signer}` : "";
|
|
595
|
+
lines.push(`${(r.id ?? "-").padEnd(idWidth)} ${(r.state ?? "-").padEnd(stateWidth)} ${title}${superseded}${attested}${sealed}${flag}`);
|
|
559
596
|
if (r.ready !== undefined) {
|
|
560
597
|
const blocked = (r.blockedBy ?? []).map((b) => `${b.id} (${b.state ?? "unknown"})`).join(", ");
|
|
561
598
|
const implemented = (r.implements ?? []).map((d) => `${d.id} (${d.state ?? "unknown"})`).join(", ");
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://intentius.io/chant/schemas/workspace/records-close/v1/records-close.schema.json",
|
|
4
|
+
"title": "chant workspace records close output",
|
|
5
|
+
"description": "What `chant workspace records close <session id> [--kind <session kind file>]` prints (#2693): the session's path and id, the fields the close set (the state, and the close time and closing revision when the session kind names fields for them), the seal written and the commit the session closed at, or the reason it wrote nothing. Version 1 of the write contract for records. The command writes one file or none and never commits. Readers ignore fields they do not know; a field is only ever added within a version. The error codes are a closed list, each in the one closed list of `reason-codes.ts`. Contract version 1 of this document is written by chant 0.88.0 and newer; an older chant refuses the close verb.",
|
|
6
|
+
"oneOf": [
|
|
7
|
+
{
|
|
8
|
+
"$ref": "#/$defs/result"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"$ref": "#/$defs/failure"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"$defs": {
|
|
15
|
+
"result": {
|
|
16
|
+
"description": "The record was written, or would be with --dry-run. Exit code 0.",
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": [
|
|
19
|
+
"$schema",
|
|
20
|
+
"contract",
|
|
21
|
+
"kind",
|
|
22
|
+
"path",
|
|
23
|
+
"id",
|
|
24
|
+
"changed",
|
|
25
|
+
"seal",
|
|
26
|
+
"closedRev",
|
|
27
|
+
"dryRun",
|
|
28
|
+
"warnings"
|
|
29
|
+
],
|
|
30
|
+
"properties": {
|
|
31
|
+
"$schema": {
|
|
32
|
+
"const": "https://intentius.io/chant/schemas/workspace/records-close/v1/records-close.schema.json"
|
|
33
|
+
},
|
|
34
|
+
"contract": {
|
|
35
|
+
"const": 1
|
|
36
|
+
},
|
|
37
|
+
"kind": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"required": [
|
|
40
|
+
"name",
|
|
41
|
+
"schema",
|
|
42
|
+
"file"
|
|
43
|
+
],
|
|
44
|
+
"properties": {
|
|
45
|
+
"name": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "The kind's name, such as \"session\"."
|
|
48
|
+
},
|
|
49
|
+
"schema": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "The `$id` of the schema the record was validated against."
|
|
52
|
+
},
|
|
53
|
+
"file": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "The kind file, relative to the repository root, with / separators."
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"path": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "The record file, relative to the repository root (the working directory outside git), with / separators."
|
|
62
|
+
},
|
|
63
|
+
"id": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "The record's id."
|
|
66
|
+
},
|
|
67
|
+
"changed": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"items": {
|
|
70
|
+
"type": "string"
|
|
71
|
+
},
|
|
72
|
+
"description": "The top-level fields whose value differs from the session as it was: its state, the close time, the closing revision and the seal, and the opening revision when it was null and the repository now has a commit."
|
|
73
|
+
},
|
|
74
|
+
"seal": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"required": [
|
|
77
|
+
"field",
|
|
78
|
+
"digest"
|
|
79
|
+
],
|
|
80
|
+
"properties": {
|
|
81
|
+
"field": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "The seal field, as the session kind's session.seal names it, such as closed_digest."
|
|
84
|
+
},
|
|
85
|
+
"digest": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
88
|
+
"description": "The seal written: the lowercase hex SHA-256 of the file with LF line endings and without its seal line, which records checks on every read (session-seal-mismatch)."
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"closedRev": {
|
|
93
|
+
"type": [
|
|
94
|
+
"string",
|
|
95
|
+
"null"
|
|
96
|
+
],
|
|
97
|
+
"pattern": "^[0-9a-f]{40,64}$",
|
|
98
|
+
"description": "The full commit id HEAD named at the close, or null outside git or before the first commit. It is written to the field session.closedRev names, when the kind names one. The caller commits the close, so the commit that carries it comes after this one."
|
|
99
|
+
},
|
|
100
|
+
"dryRun": {
|
|
101
|
+
"type": "boolean",
|
|
102
|
+
"description": "True when --dry-run was given and nothing was written."
|
|
103
|
+
},
|
|
104
|
+
"warnings": {
|
|
105
|
+
"type": "array",
|
|
106
|
+
"description": "The written record's warnings, as chant workspace records reports them. A warning never refuses a write.",
|
|
107
|
+
"items": {
|
|
108
|
+
"$ref": "#/$defs/warning"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"text": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "With --dry-run only: the whole text of the file the command would write."
|
|
114
|
+
},
|
|
115
|
+
"error": false
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"warning": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"required": [
|
|
121
|
+
"code",
|
|
122
|
+
"message"
|
|
123
|
+
],
|
|
124
|
+
"properties": {
|
|
125
|
+
"code": {
|
|
126
|
+
"enum": [
|
|
127
|
+
"asset-drift",
|
|
128
|
+
"asset-missing",
|
|
129
|
+
"asset-stale",
|
|
130
|
+
"record-supersedes-pending",
|
|
131
|
+
"record-no-evidence",
|
|
132
|
+
"review-undigested"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"message": {
|
|
136
|
+
"type": "string"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"failure": {
|
|
141
|
+
"description": "Nothing was written. Exit code 1.",
|
|
142
|
+
"type": "object",
|
|
143
|
+
"required": [
|
|
144
|
+
"$schema",
|
|
145
|
+
"contract",
|
|
146
|
+
"error"
|
|
147
|
+
],
|
|
148
|
+
"properties": {
|
|
149
|
+
"$schema": {
|
|
150
|
+
"const": "https://intentius.io/chant/schemas/workspace/records-close/v1/records-close.schema.json"
|
|
151
|
+
},
|
|
152
|
+
"contract": {
|
|
153
|
+
"const": 1
|
|
154
|
+
},
|
|
155
|
+
"error": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"required": [
|
|
158
|
+
"code",
|
|
159
|
+
"message"
|
|
160
|
+
],
|
|
161
|
+
"properties": {
|
|
162
|
+
"code": {
|
|
163
|
+
"enum": [
|
|
164
|
+
"kind-unreadable",
|
|
165
|
+
"kind-invalid",
|
|
166
|
+
"schema-unreadable",
|
|
167
|
+
"schema-id-mismatch",
|
|
168
|
+
"schema-invalid",
|
|
169
|
+
"location-missing",
|
|
170
|
+
"write-usage-invalid",
|
|
171
|
+
"record-not-found",
|
|
172
|
+
"record-closed",
|
|
173
|
+
"record-unparseable",
|
|
174
|
+
"record-schema-invalid",
|
|
175
|
+
"record-id-duplicate",
|
|
176
|
+
"record-supersedes-unknown",
|
|
177
|
+
"record-supersedes-conflict",
|
|
178
|
+
"session-seal-mismatch",
|
|
179
|
+
"session-verdict-unknown-record"
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
"message": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"description": "What was wrong, and what to do instead when there is something. A session already closed is record-closed; a verdict naming a record the session kind's subjects lack is session-verdict-unknown-record."
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"path": false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|