@intentius/chant 0.87.0 → 0.89.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/handlers/serve.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +10 -1
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-plugins.d.ts +40 -0
- package/dist/cli/mcp/workspace-plugins.d.ts.map +1 -0
- package/dist/cli/registry.d.ts +6 -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/handlers/serve.ts +10 -1
- package/src/cli/main.test.ts +7 -0
- package/src/cli/main.ts +60 -9
- package/src/cli/mcp/server.test.ts +14 -1
- package/src/cli/mcp/server.ts +13 -2
- package/src/cli/mcp/workspace-plugins.ts +123 -0
- package/src/cli/registry.ts +6 -1
- package/src/cli/serve-mcp-workspace.test.ts +142 -0
- 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
|
@@ -138,12 +138,12 @@
|
|
|
138
138
|
"items": { "$ref": "#/$defs/asset" }
|
|
139
139
|
},
|
|
140
140
|
"warnings": {
|
|
141
|
-
"description": "Added in contract 1 by #2549. Findings that leave the record valid: a pinned file that changed, went missing or did not follow a superseding decision, a supersedes link that has no effect yet, (added by #2654) an evidence list that is empty, (added by #2672) a verdict that names no digest, or (added by #2683) a work record's links and dates. A work record never carries record-no-evidence; a done one with no evidence carries work-done-unpinned. valid and --current do not look at them.",
|
|
141
|
+
"description": "Added in contract 1 by #2549. Findings that leave the record valid: a pinned file that changed, went missing or did not follow a superseding decision, a supersedes link that has no effect yet, (added by #2654) an evidence list that is empty, (added by #2672) a verdict that names no digest, or (added by #2683) a work record's links and dates, or (added by #2688) a record whose author seal is not attested under a signers file at base. A work record never carries record-no-evidence; a done one with no evidence carries work-done-unpinned. valid and --current do not look at them.",
|
|
142
142
|
"type": "array",
|
|
143
143
|
"items": { "$ref": "#/$defs/warning" }
|
|
144
144
|
},
|
|
145
145
|
"digest": {
|
|
146
|
-
"description": "Added in contract 1 by #2672. The lowercase hex SHA-256 of the record file's text, with LF line endings and without the kind's reviews block in the front matter, or for a kind of format json without the top-level reviews
|
|
146
|
+
"description": "Added in contract 1 by #2672. The lowercase hex SHA-256 of the record file's text, with LF line endings and without the kind's reviews block and its top-level seal block (#2688) in the front matter, or for a kind of format json without the top-level reviews and seal members, each with one comma beside it (#2664). A record with no seal hashes as it did before #2688. A review verdict names it in its digest field, and stops counting once the record's digest moves on; a record's author seal signs it. For a kind with no reviews list, the digest of the whole text.",
|
|
147
147
|
"type": "string",
|
|
148
148
|
"pattern": "^[0-9a-f]{64}$"
|
|
149
149
|
},
|
|
@@ -151,6 +151,14 @@
|
|
|
151
151
|
"description": "Added in contract 1 by #2671, on every record of a kind with a reviews list. Null when the front matter could not be parsed.",
|
|
152
152
|
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/quorum" }]
|
|
153
153
|
},
|
|
154
|
+
"attested": {
|
|
155
|
+
"description": "Added in contract 1 by #2688, on every parsed record of a kind with a reviews list. true when the record's author seal (its top-level seal field) verifies, against the signers file at base, for the author the kind's reviews.decider field names (decided_by for decisions). false when a signers file is active at base and the record names an author and carries no seal, or when a seal fails. null when nothing here can say: no seal and no signers file, a seal checked with no signers file, no ssh-keygen, or a record that names no author and carries no seal. Independent of provenance, which judges the commit that last changed the file.",
|
|
156
|
+
"type": ["boolean", "null"]
|
|
157
|
+
},
|
|
158
|
+
"attestation": {
|
|
159
|
+
"description": "Added in contract 1 by #2688, beside attested. What the record's author seal establishes, and why when it is not attested. Under an active signers file, a record that names an author and is not attested also carries the warning record-unattested; it is still read and still valid.",
|
|
160
|
+
"$ref": "#/$defs/attestation"
|
|
161
|
+
},
|
|
154
162
|
"citedBy": {
|
|
155
163
|
"description": "Added in contract 1 by #2673, on every record of a session kind and on no other. The review entries of the records the session kind names as its subjects (decisions, for the reference session kind) whose session field names this record's id, in path order and then list order.",
|
|
156
164
|
"type": "array",
|
|
@@ -207,7 +215,8 @@
|
|
|
207
215
|
"work-implements-undecided",
|
|
208
216
|
"work-done-unpinned",
|
|
209
217
|
"work-closed-without-date",
|
|
210
|
-
"work-done-gap-open"
|
|
218
|
+
"work-done-gap-open",
|
|
219
|
+
"record-unattested"
|
|
211
220
|
]
|
|
212
221
|
},
|
|
213
222
|
"message": { "type": "string" }
|
|
@@ -274,6 +283,16 @@
|
|
|
274
283
|
}
|
|
275
284
|
}
|
|
276
285
|
},
|
|
286
|
+
"attestation": {
|
|
287
|
+
"description": "Added in contract 1 by #2688. What a record's author seal establishes, and why when it is not attested: the same shape as a verdict's attestation (#2687), which is kept inline in verdict.",
|
|
288
|
+
"type": "object",
|
|
289
|
+
"required": ["message"],
|
|
290
|
+
"properties": {
|
|
291
|
+
"code": { "enum": ["seal-missing", "seal-signer-unlisted", "seal-signature-invalid", "seal-unverifiable"], "description": "Absent exactly when attested is true." },
|
|
292
|
+
"message": { "type": "string" },
|
|
293
|
+
"key": { "type": "string", "description": "The fingerprint of the key that made the signature, when the signature was checked." }
|
|
294
|
+
}
|
|
295
|
+
},
|
|
277
296
|
"concern": {
|
|
278
297
|
"type": "object",
|
|
279
298
|
"required": ["index", "principal", "reviewer", "note"],
|
package/src/workspace/records.ts
CHANGED
|
@@ -110,22 +110,54 @@ export const REVIEW_REASON_CODES = [
|
|
|
110
110
|
export type ReviewReasonCode = (typeof REVIEW_REASON_CODES)[number];
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* Why a
|
|
114
|
-
* codes. Every verdict in the quorum
|
|
115
|
-
*
|
|
113
|
+
* Why a seal is not attested: a verdict's (#2687), or a record's author seal
|
|
114
|
+
* (#2688). Closed, like the reason codes. Every verdict in the quorum, and
|
|
115
|
+
* every parsed record of a kind with a reviews list, carries one of these in
|
|
116
|
+
* its `attestation`, unless its seal verified.
|
|
116
117
|
*/
|
|
117
118
|
export const SEAL_REASON_CODES = [
|
|
118
|
-
/** The verdict carries no seal. */
|
|
119
|
+
/** The verdict or record carries no seal. */
|
|
119
120
|
"seal-missing",
|
|
120
|
-
/** The reviewer has no key in the signers file at base. */
|
|
121
|
+
/** The reviewer or author has no key in the signers file at base. */
|
|
121
122
|
"seal-signer-unlisted",
|
|
122
|
-
/** The seal is malformed, names another signer, or its signature does not verify over the verdict. */
|
|
123
|
+
/** The seal is malformed, names another signer, or its signature does not verify over the verdict or record. */
|
|
123
124
|
"seal-signature-invalid",
|
|
124
125
|
/** Nothing here can say whose seal it is: there is no signers file at base, or ssh-keygen is not installed. */
|
|
125
126
|
"seal-unverifiable",
|
|
126
127
|
] as const satisfies readonly ReasonCode[];
|
|
127
128
|
export type SealCode = (typeof SEAL_REASON_CODES)[number];
|
|
128
129
|
|
|
130
|
+
/**
|
|
131
|
+
* A warning about a record's own seal (#2688), raised by `records` with the
|
|
132
|
+
* policy at base, never by a write. Closed, like the reason codes.
|
|
133
|
+
*/
|
|
134
|
+
export const SEAL_WARNING_CODES = [
|
|
135
|
+
/**
|
|
136
|
+
* A signers file is active at base, the record names its author (the
|
|
137
|
+
* kind's `reviews.decider` field), and its seal does not verify for that
|
|
138
|
+
* author: it has none, the author has no key in the file, or the signature
|
|
139
|
+
* fails. The record is still read, and still valid.
|
|
140
|
+
*/
|
|
141
|
+
"record-unattested",
|
|
142
|
+
] as const satisfies readonly ReasonCode[];
|
|
143
|
+
export type SealWarningCode = (typeof SEAL_WARNING_CODES)[number];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The top-level field that holds a record's author seal (#2688), on a kind
|
|
147
|
+
* with a reviews list. Like the reviews block, the record's digest leaves it
|
|
148
|
+
* out, so sealing a record never moves its digest.
|
|
149
|
+
*/
|
|
150
|
+
export const RECORD_SEAL_FIELD = "seal";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The top-level fields {@link recordTextDigest} leaves out for a kind: its
|
|
154
|
+
* reviews list and {@link RECORD_SEAL_FIELD} when it has a reviews list, and
|
|
155
|
+
* nothing when it has none (#2672, #2688).
|
|
156
|
+
*/
|
|
157
|
+
export function digestFields(kind: Pick<RecordKind, "reviews">): string[] | null {
|
|
158
|
+
return kind.reviews ? [kind.reviews.field, RECORD_SEAL_FIELD] : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
/** What a verdict's seal establishes (#2687). See `trust/seal.ts`. */
|
|
130
162
|
export interface VerdictAttestation {
|
|
131
163
|
/** Why the verdict is not attested. Absent when its seal verified. */
|
|
@@ -136,8 +168,8 @@ export interface VerdictAttestation {
|
|
|
136
168
|
}
|
|
137
169
|
|
|
138
170
|
export interface RecordWarning {
|
|
139
|
-
/** A work kind's records also carry the codes of `WORK_WARNING_CODES` (#2683). */
|
|
140
|
-
code: RecordWarningCode | WorkWarningCode;
|
|
171
|
+
/** A work kind's records also carry the codes of `WORK_WARNING_CODES` (#2683), and `records` adds `SEAL_WARNING_CODES` (#2688). */
|
|
172
|
+
code: RecordWarningCode | WorkWarningCode | SealWarningCode;
|
|
141
173
|
message: string;
|
|
142
174
|
}
|
|
143
175
|
|
|
@@ -274,12 +306,21 @@ export const recordKindSchema = z
|
|
|
274
306
|
* (relative to this kind file's directory). The entries of that kind's
|
|
275
307
|
* reviews list (its `reviews.field`, or `reviews`) name a session in
|
|
276
308
|
* `session`. Optional.
|
|
309
|
+
*
|
|
310
|
+
* `openedRev`, `closedRev` and `closedOn` (#2693) name the fields that
|
|
311
|
+
* hold the commit a session opened at, the commit it closed at, and when
|
|
312
|
+
* it closed. `records new` writes the first, `records close` the other
|
|
313
|
+
* two, and `records --since <session id>` reads the revisions. Each is
|
|
314
|
+
* optional: without it, nothing writes that field.
|
|
277
315
|
*/
|
|
278
316
|
session: z
|
|
279
317
|
.object({
|
|
280
318
|
verdicts: z.string().min(1),
|
|
281
319
|
seal: z.string().min(1),
|
|
282
320
|
subjects: z.object({ kind: z.string().min(1) }).strict(),
|
|
321
|
+
openedRev: z.string().min(1).optional(),
|
|
322
|
+
closedRev: z.string().min(1).optional(),
|
|
323
|
+
closedOn: z.string().min(1).optional(),
|
|
283
324
|
})
|
|
284
325
|
.strict()
|
|
285
326
|
.optional(),
|
|
@@ -584,34 +625,43 @@ function nonJson(v: unknown, at: string, seen: Set<object>): string | undefined
|
|
|
584
625
|
* to the file changes it, so a verdict given before an amendment stops
|
|
585
626
|
* counting.
|
|
586
627
|
*
|
|
628
|
+
* `field` names the top-level fields left out: one name, or a list. For a
|
|
629
|
+
* kind with a reviews list the list is the reviews field and `seal`, the
|
|
630
|
+
* record's author seal (#2688; {@link digestFields}), so sealing a record
|
|
631
|
+
* leaves its digest where it was too. A record with no `seal` hashes exactly
|
|
632
|
+
* as it did before author seals existed.
|
|
633
|
+
*
|
|
587
634
|
* The rule, which a hand-editor can follow with a text editor and
|
|
588
635
|
* `sha256sum`:
|
|
589
636
|
*
|
|
590
637
|
* 1. Line endings become LF (CRLF and a lone CR each become one LF).
|
|
591
638
|
* 2. When the text starts with a `---` line and a later line is exactly
|
|
592
|
-
* `---`, the lines between them are the front matter. In it,
|
|
593
|
-
* that starts, at column 0, with the
|
|
639
|
+
* `---`, the lines between them are the front matter. In it, each line
|
|
640
|
+
* that starts, at column 0, with one of the keys (bare, or in single or
|
|
594
641
|
* double quotes), optional spaces or tabs and a `:`, is removed, and so
|
|
595
642
|
* is every line after it, up to the closing `---`, that is empty or
|
|
596
643
|
* starts with a space, a tab, `#` or `-`. Removal stops at the first
|
|
597
644
|
* other line. Everything else, the `---` lines and the body included, is
|
|
598
|
-
* kept byte for byte.
|
|
645
|
+
* kept byte for byte. Which key is removed first makes no difference.
|
|
599
646
|
* 3. The digest is the SHA-256 of the result's UTF-8 bytes.
|
|
600
647
|
*
|
|
601
|
-
* Text with no front matter, or
|
|
602
|
-
* A writer that adds a verdict must change only the reviews block: a
|
|
648
|
+
* Text with no front matter, or none of the keys, is hashed after step 1
|
|
649
|
+
* alone. A writer that adds a verdict must change only the reviews block: a
|
|
603
650
|
* reformatted front matter is a new digest, and every earlier verdict stops
|
|
604
651
|
* counting.
|
|
605
652
|
*/
|
|
606
|
-
export function recordTextDigest(text: string, field: string | null = "reviews", format: RecordFormat = "markdown-front-matter"): string {
|
|
653
|
+
export function recordTextDigest(text: string, field: string | readonly string[] | null = "reviews", format: RecordFormat = "markdown-front-matter"): string {
|
|
607
654
|
const lf = text.replace(/\r\n?/g, "\n");
|
|
608
|
-
const
|
|
655
|
+
const fields = field === null ? [] : typeof field === "string" ? [field] : field;
|
|
656
|
+
const kept = format === "json" ? fields.reduce(withoutMember, lf) : withoutBlocks(lf, fields);
|
|
609
657
|
return createHash("sha256").update(kept, "utf8").digest("hex");
|
|
610
658
|
}
|
|
611
659
|
|
|
612
660
|
/**
|
|
613
661
|
* `text` with the top-level member `field` removed from a JSON record, by the
|
|
614
|
-
* rule a hand-editor follows for a JSON record (ws-053)
|
|
662
|
+
* rule a hand-editor follows for a JSON record (ws-053). With several fields
|
|
663
|
+
* (the reviews member, then `seal`, #2688) the rule is applied to each in
|
|
664
|
+
* turn, to the text the previous one left:
|
|
615
665
|
*
|
|
616
666
|
* 1. Line endings become LF, as for Markdown.
|
|
617
667
|
* 2. When the text is a JSON record (one object, no repeated member name) and
|
|
@@ -654,14 +704,15 @@ function withoutMember(text: string, field: string): string {
|
|
|
654
704
|
return text.slice(0, start) + text.slice(end);
|
|
655
705
|
}
|
|
656
706
|
|
|
657
|
-
/** `text` with the top-level `
|
|
658
|
-
function
|
|
707
|
+
/** `text` with the top-level blocks of `fields` removed from its front matter, by the rule {@link recordTextDigest} states. */
|
|
708
|
+
function withoutBlocks(text: string, fields: readonly string[]): string {
|
|
709
|
+
if (fields.length === 0) return text;
|
|
659
710
|
const lines = text.split("\n");
|
|
660
711
|
if (lines[0] !== "---") return text;
|
|
661
712
|
const close = lines.indexOf("---", 1);
|
|
662
713
|
if (close < 0) return text;
|
|
663
|
-
const
|
|
664
|
-
const starts = new RegExp(`^(?:${
|
|
714
|
+
const keys = fields.map((f) => f.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).map((k) => `${k}|"${k}"|'${k}'`);
|
|
715
|
+
const starts = new RegExp(`^(?:${keys.join("|")})[ \\t]*:(?:[ \\t]|$)`);
|
|
665
716
|
const out: string[] = [];
|
|
666
717
|
for (let i = 0; i < lines.length; i++) {
|
|
667
718
|
if (i > 0 && i < close && starts.test(lines[i])) {
|
|
@@ -876,7 +927,7 @@ export interface RecordEntry {
|
|
|
876
927
|
assets: AssetPin[];
|
|
877
928
|
/** Findings that leave the record valid, such as a pinned file that changed (#2549). */
|
|
878
929
|
warnings: RecordWarning[];
|
|
879
|
-
/** {@link recordTextDigest} of the file's text, without the kind's reviews list when it has
|
|
930
|
+
/** {@link recordTextDigest} of the file's text, without the kind's reviews list and author seal when it has a reviews list (#2672, #2688). */
|
|
880
931
|
digest: string;
|
|
881
932
|
/** For a session kind only: the subject records' review entries that name this session (#2673). */
|
|
882
933
|
citedBy?: SessionCitation[];
|
|
@@ -1041,7 +1092,7 @@ export async function readRecords(loaded: LoadedRecordKind, options: ReadRecords
|
|
|
1041
1092
|
data: null,
|
|
1042
1093
|
assets: [],
|
|
1043
1094
|
warnings: [],
|
|
1044
|
-
digest: recordTextDigest(text, kind
|
|
1095
|
+
digest: recordTextDigest(text, digestFields(kind), kind.format),
|
|
1045
1096
|
};
|
|
1046
1097
|
entries.push(entry);
|
|
1047
1098
|
if (kind.session) texts.set(path, text);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
+
import type { EnvironmentDeclaration } from "../config";
|
|
18
19
|
import { lexiconNames } from "../lexicon-module";
|
|
19
20
|
import type { ReasonCode } from "./reason-codes";
|
|
20
21
|
|
|
@@ -54,6 +55,11 @@ export interface MemberRuntimes {
|
|
|
54
55
|
/** The default runtime's name. */
|
|
55
56
|
default: string;
|
|
56
57
|
reasons: RuntimeReason[];
|
|
58
|
+
/**
|
|
59
|
+
* The config's `environments`, read in the same load (#2695): `[]` when it
|
|
60
|
+
* declares none, null when the config couldn't be read.
|
|
61
|
+
*/
|
|
62
|
+
environments: EnvironmentDeclaration[] | null;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/** Loads a lexicon plugin by name; `loadPlugin` from the CLI unless a test swaps it. */
|
|
@@ -65,11 +71,14 @@ const firstLine = (err: unknown): string => (err instanceof Error ? err.message
|
|
|
65
71
|
export async function readMemberRuntimes(dir: string, load?: PluginLoader): Promise<MemberRuntimes> {
|
|
66
72
|
const reasons: RuntimeReason[] = [];
|
|
67
73
|
let names: string[];
|
|
74
|
+
let environments: EnvironmentDeclaration[];
|
|
68
75
|
try {
|
|
69
76
|
const { loadChantConfig } = await import("../config");
|
|
70
|
-
|
|
77
|
+
const { config } = await loadChantConfig(dir);
|
|
78
|
+
names = lexiconNames(config.lexicons ?? []);
|
|
79
|
+
environments = config.environments ?? [];
|
|
71
80
|
} catch (err) {
|
|
72
|
-
return { lexicons: [], default: LOCAL_RUNTIME, reasons: [{ code: "runtimes-config-unreadable", message: `chant.config.ts: ${firstLine(err)}` }] };
|
|
81
|
+
return { lexicons: [], default: LOCAL_RUNTIME, reasons: [{ code: "runtimes-config-unreadable", message: `chant.config.ts: ${firstLine(err)}` }], environments: null };
|
|
73
82
|
}
|
|
74
83
|
const loader = load ?? (await import("../cli/plugins")).loadPlugin;
|
|
75
84
|
const lexicons: string[] = [];
|
|
@@ -81,7 +90,7 @@ export async function readMemberRuntimes(dir: string, load?: PluginLoader): Prom
|
|
|
81
90
|
reasons.push({ code: "runtimes-lexicon-unreadable", message: `lexicon "${name}": ${firstLine(err)}` });
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
|
-
return { lexicons, default: LOCAL_RUNTIME, reasons };
|
|
93
|
+
return { lexicons, default: LOCAL_RUNTIME, reasons, environments };
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/** The runtimes one component can deploy on, `local` first, each with its command line. */
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding review-session kinds and the commits a session names (#2693), for
|
|
3
|
+
* the commands that act on a session by its id: `records review --session`,
|
|
4
|
+
* `records close` and `records --since <session id>`.
|
|
5
|
+
*
|
|
6
|
+
* A session kind is found through the workspace declaration nearest above
|
|
7
|
+
* the working directory, or named directly. Nothing here writes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { WorkspaceReadError } from "./declaration";
|
|
13
|
+
import { gitRoot, resolveRevision } from "./record-source";
|
|
14
|
+
import { loadRecordKind, RecordReadError, type LoadedRecordKind } from "./records";
|
|
15
|
+
import { declaredKindFiles, realpathOr } from "./records-cli";
|
|
16
|
+
|
|
17
|
+
/** The shape of an id `records --since` looks up as a session before reading it as a revision: `S-0002`, `ws-052`. */
|
|
18
|
+
export const SESSION_ID = /^[A-Za-z][A-Za-z0-9]*-[0-9]+$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Every session kind among `extra` (kind files, resolved against `cwd`) and
|
|
22
|
+
* the kinds the declaration nearest above `cwd` names, each once, in that
|
|
23
|
+
* order. A kind that can't be loaded, or a declaration that can't be read,
|
|
24
|
+
* adds nothing: the read of that kind reports it.
|
|
25
|
+
*/
|
|
26
|
+
export async function findSessionKinds(cwd: string, extra: string[] = []): Promise<LoadedRecordKind[]> {
|
|
27
|
+
const files = [...extra.map((f) => resolve(cwd, f))];
|
|
28
|
+
try {
|
|
29
|
+
files.push(...declaredKindFiles(cwd).map((k) => k.file));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
32
|
+
}
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
const out: LoadedRecordKind[] = [];
|
|
35
|
+
for (const f of files) {
|
|
36
|
+
const real = realpathOr(f);
|
|
37
|
+
if (seen.has(real)) continue;
|
|
38
|
+
seen.add(real);
|
|
39
|
+
try {
|
|
40
|
+
const loaded = await loadRecordKind(f, cwd);
|
|
41
|
+
if (loaded.kind.session) out.push(loaded);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (!(err instanceof RecordReadError)) throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The session kinds whose `session.subjects.kind` is the kind file `subjectFile`. */
|
|
50
|
+
export async function sessionKindsFor(subjectFile: string, cwd: string): Promise<LoadedRecordKind[]> {
|
|
51
|
+
const target = realpathOr(resolve(cwd, subjectFile));
|
|
52
|
+
return (await findSessionKinds(cwd)).filter((k) => realpathOr(resolve(dirname(k.file), k.kind.session!.subjects.kind)) === target);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The full commit id HEAD names in the repository holding `dir`, or null outside git or before the first commit. */
|
|
56
|
+
export function headCommit(dir: string): string | null {
|
|
57
|
+
const top = gitRoot(realpathOr(dir));
|
|
58
|
+
if (!top) return null;
|
|
59
|
+
try {
|
|
60
|
+
return resolveRevision(top, "HEAD");
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof RecordReadError) return null;
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Commit ids from `git log <args> --format=%H -- <path>` in the repository at
|
|
69
|
+
* `top`, newest first; empty when git knows nothing of the path.
|
|
70
|
+
*/
|
|
71
|
+
export function commitsTouching(top: string, path: string, args: string[]): string[] {
|
|
72
|
+
try {
|
|
73
|
+
return execFileSync("git", ["log", ...args, "--format=%H", "--", path], { cwd: top, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] })
|
|
74
|
+
.split("\n")
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/workspace/status.ts
CHANGED
|
@@ -91,7 +91,7 @@ export const STATUS_ERROR_CODES = [
|
|
|
91
91
|
export type StatusErrorCode = (typeof STATUS_ERROR_CODES)[number];
|
|
92
92
|
|
|
93
93
|
/** Environment names a ledger directory can have: no `/`, no leading `_` or `.` (those are chant's own directories). */
|
|
94
|
-
const ENV_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
94
|
+
export const ENV_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
95
95
|
|
|
96
96
|
export interface StatusRelease {
|
|
97
97
|
component: string;
|