mainflow 0.34.0 → 0.35.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.js +81 -9
- package/dist/{dist-IdbnCDqM.js → dist-BdvWGTsb.js} +47 -4
- package/dist/facts.d.ts +67 -13
- package/dist/facts.js +63 -14
- package/dist/grant-commands.d.ts +1 -1
- package/dist/grant-commands.js +1 -1
- package/dist/hooks.d.ts +63 -9
- package/dist/hooks.js +3 -3
- package/dist/{host-79T-_NMu.js → host-bnHYIwuY.js} +476 -199
- package/dist/host.d.ts +9 -0
- package/dist/host.js +2 -2
- package/dist/{index-ihgXABQc.d.ts → index-DgYB5u1f.d.ts} +52 -1
- package/dist/launch-D80CYPcm.js +327 -0
- package/dist/launch.d.ts +50 -4
- package/dist/launch.js +2 -2
- package/dist/native/mainflow-swap-darwin-arm64 +0 -0
- package/dist/native/mainflow-swap-darwin-x64 +0 -0
- package/dist/{operation--fJnUqvA.js → operation-DVcJgKM9.js} +2 -2
- package/dist/{ui-BcXPk-0P.js → ui-C4h4iLDu.js} +132 -12
- package/dist/web/assets/index-BDnlbKcr.js +82 -0
- package/dist/web/assets/index-Z6UfQ6pW.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/launch-DQVyIbpf.js +0 -205
- package/dist/web/assets/index-CC7l6bNN.css +0 -1
- package/dist/web/assets/index-CDNCPMjD.js +0 -82
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as OFFSET, A as SURVEY_SECTION, B as issueSchema, C as REF_EXAMPLES, D as SESSION_STATUS, E as SESSION_ID, F as configSchema, G as parseRef, H as opsEntrySchema, I as decisionSchema, J as refMainline, K as parseUntil, L as findingSchema, M as TASK_SECTIONS, O as SLUG, P as WORK_STATUS, Q as waitingForRef, R as formatOpsEntry, S as PRIORITIES, T as SERVES_REF, U as parseActionRef, V as mainlineSchema, W as parseOpsLine, X as taskSchema, Y as sessionSchema, Z as waitingForAction, a as DECISION_SECTIONS, at as grantCovers, b as MAINLINE_STATUS, ct as parseGrantTarget, et as stampMs, f as GROUP_SECTIONS, h as ISSUE_SECTIONS, i as DECISION_BASES, it as describeRequest, j as TASK_REQUIRED_SECTIONS, k as SURVEY_LINES, l as FINDING_SECTIONS, n as CLOSED_STATUS, ot as grantForm, p as GROUP_STATUS, q as refExample, r as DECIDERS, s as DEFAULT_CATEGORIES, st as grantRepo, u as FINDING_STATUS, v as MAINLINE_ROLES, w as RESERVED_HUB_NAMES, x as MERGE_STATES, y as MAINLINE_SECTIONS, z as groupSchema } from "./dist-BdvWGTsb.js";
|
|
2
2
|
import { a as LABEL_ROLES, i as FINDING_SEVERITIES, n as FINDING_CLASSES, r as FINDING_FIELDS, t as FINDINGS_FENCE } from "./retro-formats-ECy0gGOL.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
@@ -10,7 +10,7 @@ import { performance } from "node:perf_hooks";
|
|
|
10
10
|
import { gunzipSync, inflateRawSync } from "node:zlib";
|
|
11
11
|
//#region ../core/dist/ops/document.js
|
|
12
12
|
const DATA_DIR = ".mainflow";
|
|
13
|
-
const ENTRY_DIR = /^([
|
|
13
|
+
const ENTRY_DIR = /^([mtdifw]\d{3})-(.+)$/;
|
|
14
14
|
var WriteError = class extends Error {};
|
|
15
15
|
/** The file changed after the caller read it; the caller must reload and retry. */
|
|
16
16
|
var ConflictError = class extends WriteError {};
|
|
@@ -315,6 +315,7 @@ const MAINLINES_DIR = "mainlines";
|
|
|
315
315
|
const CONFIG_FILE = "config.yaml";
|
|
316
316
|
const ISSUES_DIR = "issues";
|
|
317
317
|
const FINDINGS_DIR = "findings";
|
|
318
|
+
const GROUPS_DIR = "groups";
|
|
318
319
|
const SESSIONS_DIR = "local/sessions";
|
|
319
320
|
/** The document inside each mainline and task directory: front matter for attributes, body for prose. */
|
|
320
321
|
const MAINLINE_FILE = "mainline.md";
|
|
@@ -667,6 +668,11 @@ function refIndex(hub) {
|
|
|
667
668
|
title: f.title,
|
|
668
669
|
status: f.status
|
|
669
670
|
});
|
|
671
|
+
for (const g of hub.groups ?? []) index.set(g.id, {
|
|
672
|
+
kind: "group",
|
|
673
|
+
title: g.title,
|
|
674
|
+
status: g.status
|
|
675
|
+
});
|
|
670
676
|
return index;
|
|
671
677
|
}
|
|
672
678
|
const isHubRoot = (dir) => existsSync(join(dir, DATA_DIR, "config.yaml"));
|
|
@@ -697,6 +703,7 @@ function linksInto(hub, hubName) {
|
|
|
697
703
|
}
|
|
698
704
|
for (const i of hub.issues) add(i.id, i.links);
|
|
699
705
|
for (const f of hub.findings) add(f.id, f.links);
|
|
706
|
+
for (const g of hub.groups) add(g.id, g.links);
|
|
700
707
|
return out;
|
|
701
708
|
}
|
|
702
709
|
//#endregion
|
|
@@ -742,7 +749,11 @@ function dependencyGraph(hub) {
|
|
|
742
749
|
}));
|
|
743
750
|
}
|
|
744
751
|
}
|
|
745
|
-
for (const d of [
|
|
752
|
+
for (const d of [
|
|
753
|
+
...hub.issues,
|
|
754
|
+
...hub.findings,
|
|
755
|
+
...hub.groups ?? []
|
|
756
|
+
]) links(d.id, d.file, d.links);
|
|
746
757
|
for (const remote of hub.remotes ?? []) {
|
|
747
758
|
if (!remote.hub) continue;
|
|
748
759
|
for (const { to, link } of linksInto(remote.hub, hub.config.hub)) if (link.rel === "depends-on") edge(`${remote.name}:${link.from}`, to, "link", link.note ? { note: link.note } : {});
|
|
@@ -774,7 +785,7 @@ function findCycles(edges) {
|
|
|
774
785
|
for (const node of [...next.keys()].sort()) if (!state.has(node)) visit(node);
|
|
775
786
|
return [...cycles.values()];
|
|
776
787
|
}
|
|
777
|
-
/** The mainline a reference sits in, keeping another hub's prefix; undefined for issues and
|
|
788
|
+
/** The mainline a reference sits in, keeping another hub's prefix; undefined for issues, findings and groups. */
|
|
778
789
|
function mainlineOf(ref) {
|
|
779
790
|
const parsed = parseRef(ref);
|
|
780
791
|
const m = parsed ? refMainline(parsed) : void 0;
|
|
@@ -885,6 +896,7 @@ const TASK_BODY = body(TASK_SECTIONS);
|
|
|
885
896
|
const DECISION_BODY = body(DECISION_SECTIONS);
|
|
886
897
|
const ISSUE_BODY = body(ISSUE_SECTIONS);
|
|
887
898
|
const FINDING_BODY = body(FINDING_SECTIONS);
|
|
899
|
+
const GROUP_BODY = body(GROUP_SECTIONS);
|
|
888
900
|
/** The text under `## {heading}` up to the next H2, trimmed; null when the heading is missing. */
|
|
889
901
|
function sectionText(markdown, heading) {
|
|
890
902
|
const lines = markdown.split("\n");
|
|
@@ -936,6 +948,33 @@ function withSection(markdown, heading, text) {
|
|
|
936
948
|
}
|
|
937
949
|
//#endregion
|
|
938
950
|
//#region ../core/dist/load.js
|
|
951
|
+
/**
|
|
952
|
+
* Read config.yaml and check its format version; throws a HubError naming what is wrong when the hub cannot be
|
|
953
|
+
* used at all. The interface answers 503 for it before loading anything else (docs/ui.md "壞檔與讀取失敗").
|
|
954
|
+
*/
|
|
955
|
+
function loadConfig(root) {
|
|
956
|
+
const file = join(root, DATA_DIR, "config.yaml");
|
|
957
|
+
const where = relative(root, file);
|
|
958
|
+
let data;
|
|
959
|
+
try {
|
|
960
|
+
data = parse$1(readText(file) ?? "");
|
|
961
|
+
} catch (error) {
|
|
962
|
+
throw new HubError("invalid config.yaml", [{
|
|
963
|
+
file: where,
|
|
964
|
+
field: "",
|
|
965
|
+
message: `cannot parse YAML: ${error.message}`
|
|
966
|
+
}]);
|
|
967
|
+
}
|
|
968
|
+
const result = configSchema.safeParse(data);
|
|
969
|
+
if (!result.success) throw new HubError("invalid config.yaml", result.error.issues.map((issue) => ({
|
|
970
|
+
file: where,
|
|
971
|
+
field: issue.path.join("."),
|
|
972
|
+
message: issue.message
|
|
973
|
+
})));
|
|
974
|
+
const config = result.data;
|
|
975
|
+
if (config.formatVersion !== 3) throw new HubError(config.formatVersion < 3 ? `data format version ${config.formatVersion} is older than this CLI's (3); run "mainflow migrate --dry-run" to preview the conversion, then "mainflow migrate"` : `data format version ${config.formatVersion} is newer than this CLI supports (3); install the matching mainflow version in this hub`, []);
|
|
976
|
+
return config;
|
|
977
|
+
}
|
|
939
978
|
function loadHub(root, options = {}) {
|
|
940
979
|
const problems = [];
|
|
941
980
|
const warnings = [];
|
|
@@ -975,9 +1014,7 @@ function loadHub(root, options = {}) {
|
|
|
975
1014
|
message: `directory name must be "${id}-{slug}" in kebab-case`
|
|
976
1015
|
});
|
|
977
1016
|
};
|
|
978
|
-
const config =
|
|
979
|
-
if (!config) throw new HubError("invalid config.yaml", problems);
|
|
980
|
-
if (config.formatVersion !== 3) throw new HubError(config.formatVersion < 3 ? `data format version ${config.formatVersion} is older than this CLI's (3); run "mainflow migrate --dry-run" to preview the conversion, then "mainflow migrate"` : `data format version ${config.formatVersion} is newer than this CLI supports (3); install the matching mainflow version in this hub`, problems);
|
|
1017
|
+
const config = loadConfig(root);
|
|
981
1018
|
problems.push(...repoPathProblems(root, config));
|
|
982
1019
|
const categories = /* @__PURE__ */ new Set([...DEFAULT_CATEGORIES, ...config.categories ?? []]);
|
|
983
1020
|
/** Read a Markdown document's front matter against `schema`, with the attributes every document shares. */
|
|
@@ -1127,6 +1164,9 @@ function loadHub(root, options = {}) {
|
|
|
1127
1164
|
});
|
|
1128
1165
|
}
|
|
1129
1166
|
const findings = hubDocs(join(dataDir, "findings"), findingSchema);
|
|
1167
|
+
const groups = hubDocs(join(dataDir, "groups"), groupSchema);
|
|
1168
|
+
problems.push(...groupProblems(groups, mainlines, hubIssues, unreadable));
|
|
1169
|
+
warnings.push(...groupWarnings(groups, mainlines, hubIssues));
|
|
1130
1170
|
const ops = [];
|
|
1131
1171
|
const opsPath = join(dataDir, "ops");
|
|
1132
1172
|
for (const name of existsSync(opsPath) ? readdirSync(opsPath).filter((n) => n.endsWith(".log")).sort() : []) readFileSync(join(opsPath, name), "utf8").split("\n").forEach((line, index) => {
|
|
@@ -1150,6 +1190,7 @@ function loadHub(root, options = {}) {
|
|
|
1150
1190
|
mainlines,
|
|
1151
1191
|
issues: hubIssues,
|
|
1152
1192
|
findings,
|
|
1193
|
+
groups,
|
|
1153
1194
|
remotes,
|
|
1154
1195
|
pending,
|
|
1155
1196
|
unreadable,
|
|
@@ -1160,6 +1201,7 @@ function loadHub(root, options = {}) {
|
|
|
1160
1201
|
mainlines,
|
|
1161
1202
|
issues: hubIssues,
|
|
1162
1203
|
findings,
|
|
1204
|
+
groups,
|
|
1163
1205
|
remotes
|
|
1164
1206
|
});
|
|
1165
1207
|
for (const cycle of deps.cycles) {
|
|
@@ -1189,12 +1231,34 @@ function loadHub(root, options = {}) {
|
|
|
1189
1231
|
mainlines,
|
|
1190
1232
|
issues: hubIssues,
|
|
1191
1233
|
findings,
|
|
1234
|
+
groups,
|
|
1192
1235
|
ops,
|
|
1193
1236
|
problems,
|
|
1194
1237
|
warnings,
|
|
1195
1238
|
remotes
|
|
1196
1239
|
};
|
|
1197
1240
|
}
|
|
1241
|
+
/** Members name their group; one that names a group this hub does not have is reported on the member. */
|
|
1242
|
+
function groupProblems(groups, mainlines, issues, unreadable) {
|
|
1243
|
+
const known = new Set(groups.map((g) => g.id));
|
|
1244
|
+
return [...mainlines, ...issues].flatMap((doc) => doc.group && !known.has(doc.group) && !unreadable.has(doc.group) ? [{
|
|
1245
|
+
file: doc.file,
|
|
1246
|
+
field: "group",
|
|
1247
|
+
message: `group ${doc.group} does not exist; create it with "mainflow new group <slug> --title <title>" or name an existing one`
|
|
1248
|
+
}] : []);
|
|
1249
|
+
}
|
|
1250
|
+
/** A group marked done while some of its members are still open: a plan to settle, not a broken file. */
|
|
1251
|
+
function groupWarnings(groups, mainlines, issues) {
|
|
1252
|
+
return groups.flatMap((g) => {
|
|
1253
|
+
if (g.status !== "done") return [];
|
|
1254
|
+
const open = [...mainlines.filter((m) => m.group === g.id && isOpen$2("mainline", m.status)).map((m) => m.id), ...issues.filter((i) => i.group === g.id && i.status !== "closed").map((i) => i.id)];
|
|
1255
|
+
return open.length ? [{
|
|
1256
|
+
file: g.file,
|
|
1257
|
+
field: "status",
|
|
1258
|
+
message: `group ${g.id} is done but ${open.join(", ")} ${open.length === 1 ? "is" : "are"} still open; settle ${open.length === 1 ? "it" : "them"} or reopen the group`
|
|
1259
|
+
}] : [];
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1198
1262
|
/** Load every hub registered in config.yaml, read-only and without its own remotes. */
|
|
1199
1263
|
function loadRemotes(root, config, problems, warnings) {
|
|
1200
1264
|
const file = `${DATA_DIR}/config.yaml`;
|
|
@@ -1263,7 +1327,7 @@ function loadRemotes(root, config, problems, warnings) {
|
|
|
1263
1327
|
* Links are for relations across mainlines; inside one mainline, parent, dependsOn and affects say it.
|
|
1264
1328
|
*/
|
|
1265
1329
|
function checkReferences(input) {
|
|
1266
|
-
const { config, mainlines, issues, findings, remotes, pending, unreadable, problems } = input;
|
|
1330
|
+
const { config, mainlines, issues, findings, groups, remotes, pending, unreadable, problems } = input;
|
|
1267
1331
|
/** A target inside a document that failed to load: its schema problem is already reported. */
|
|
1268
1332
|
const inUnreadable = (target) => [
|
|
1269
1333
|
target,
|
|
@@ -1273,7 +1337,8 @@ function checkReferences(input) {
|
|
|
1273
1337
|
const local = refIndex({
|
|
1274
1338
|
mainlines,
|
|
1275
1339
|
issues,
|
|
1276
|
-
findings
|
|
1340
|
+
findings,
|
|
1341
|
+
groups
|
|
1277
1342
|
});
|
|
1278
1343
|
const remoteIndex = new Map(remotes.map((r) => [r.name, r.hub ? refIndex(r.hub) : null]));
|
|
1279
1344
|
/** The document's own mainline, when it sits in one; links may not point back into it. */
|
|
@@ -1326,6 +1391,7 @@ function checkReferences(input) {
|
|
|
1326
1391
|
}
|
|
1327
1392
|
for (const i of issues) add(i.file, i, i.id);
|
|
1328
1393
|
for (const f of findings) add(f.file, f, f.id);
|
|
1394
|
+
for (const g of groups) add(g.file, g, g.id);
|
|
1329
1395
|
for (const p of pending) {
|
|
1330
1396
|
const parsed = parseRef(p.ref);
|
|
1331
1397
|
if (!parsed) continue;
|
|
@@ -1545,6 +1611,249 @@ function addHub(hub, input) {
|
|
|
1545
1611
|
return draft.result(input.name);
|
|
1546
1612
|
}
|
|
1547
1613
|
//#endregion
|
|
1614
|
+
//#region ../core/dist/ops/hub-items.js
|
|
1615
|
+
function entryFile(hub, dir, id, what) {
|
|
1616
|
+
const file = entryFileIn(hub, dir, id);
|
|
1617
|
+
if (!file) throw new WriteError(`${what} ${id} not found`);
|
|
1618
|
+
return file;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Any reference the schema accepts must also point at something that exists. A reference into another
|
|
1622
|
+
* hub only needs that hub registered here; check verifies the target when that hub is on this machine.
|
|
1623
|
+
*/
|
|
1624
|
+
function requireExisting(hub, ref) {
|
|
1625
|
+
const parsed = parseRef(ref);
|
|
1626
|
+
if (!parsed) throw new WriteError(`"${ref}" is not a reference like ${REF_EXAMPLES}`);
|
|
1627
|
+
const config = readConfigIn(hub);
|
|
1628
|
+
if (parsed.hub !== void 0 && parsed.hub !== config.hub) {
|
|
1629
|
+
if (!config.hubs?.[parsed.hub]) throw new WriteError(`hub "${parsed.hub}" is not registered; run "mainflow hub add ${parsed.hub} <path>" first`);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
switch (parsed.kind) {
|
|
1633
|
+
case "issue":
|
|
1634
|
+
entryFile(hub, ISSUES_DIR, parsed.issue, "issue");
|
|
1635
|
+
return;
|
|
1636
|
+
case "finding":
|
|
1637
|
+
entryFile(hub, FINDINGS_DIR, parsed.finding, "finding");
|
|
1638
|
+
return;
|
|
1639
|
+
case "group":
|
|
1640
|
+
entryFile(hub, GROUPS_DIR, parsed.group, "group");
|
|
1641
|
+
return;
|
|
1642
|
+
default:
|
|
1643
|
+
mainlineDirIn(hub, parsed.mainline);
|
|
1644
|
+
if ("task" in parsed) taskDirIn(hub, parsed.mainline, parsed.task);
|
|
1645
|
+
if (parsed.kind === "decision") decisionFileIn(hub, parsed.mainline, parsed.decision);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
/** A priority as a command gives it, checked against P0 to P3. */
|
|
1649
|
+
const requirePriority = (value) => requireOneOf(value, PRIORITIES, "priority");
|
|
1650
|
+
/**
|
|
1651
|
+
* The group a member names, which must exist in this hub. `none` clears the membership; `null` is how a patch says
|
|
1652
|
+
* the same, so a command can pass the word and an interface the value.
|
|
1653
|
+
*/
|
|
1654
|
+
function requireGroup(hub, value) {
|
|
1655
|
+
const ref = requireRef(value, "group");
|
|
1656
|
+
if (ref.hub !== void 0) throw new WriteError(`a group is one of this hub's, like w001; got "${value}"`);
|
|
1657
|
+
entryFile(hub, GROUPS_DIR, ref.group, "group");
|
|
1658
|
+
return ref.group;
|
|
1659
|
+
}
|
|
1660
|
+
/** Whether a patch clears the group: `none` from a command, or null. */
|
|
1661
|
+
const clearsGroup = (value) => value === null || value === "none";
|
|
1662
|
+
function createIssue(hub, input, ctx) {
|
|
1663
|
+
requireSlug(input.slug);
|
|
1664
|
+
if (input.origin) requireExisting(hub, input.origin);
|
|
1665
|
+
const group = input.group ? requireGroup(hub, input.group) : void 0;
|
|
1666
|
+
const priority = input.priority ? requirePriority(input.priority) : void 0;
|
|
1667
|
+
const { day } = stamps(ctx);
|
|
1668
|
+
const draft = new Draft(hub);
|
|
1669
|
+
const id = draft.number(ctx, {
|
|
1670
|
+
prefix: "i",
|
|
1671
|
+
taken: idsIn(draft, ISSUES_DIR),
|
|
1672
|
+
ref: (id) => id
|
|
1673
|
+
});
|
|
1674
|
+
const issue = validate(issueSchema, {
|
|
1675
|
+
id,
|
|
1676
|
+
title: input.title,
|
|
1677
|
+
status: "open",
|
|
1678
|
+
...priority ? { priority } : {},
|
|
1679
|
+
created: day,
|
|
1680
|
+
...input.origin ? { origin: input.origin } : {},
|
|
1681
|
+
...input.labels?.length ? { labels: input.labels } : {},
|
|
1682
|
+
...group ? { group } : {}
|
|
1683
|
+
}, "issue");
|
|
1684
|
+
const file = `${ISSUES_DIR}/${id}-${input.slug}.md`;
|
|
1685
|
+
draft.write(file, frontMatterText(issue, ISSUE_BODY));
|
|
1686
|
+
return draft.result({
|
|
1687
|
+
id,
|
|
1688
|
+
file
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function setIssue(hub, input) {
|
|
1692
|
+
const { patch } = input;
|
|
1693
|
+
if (patch.group === void 0 && patch.priority === void 0) throw new WriteError(`${input.id} needs --group or --priority, e.g. --group w001`);
|
|
1694
|
+
const draft = new Draft(hub);
|
|
1695
|
+
const group = patch.group === void 0 || clearsGroup(patch.group) ? void 0 : requireGroup(draft, patch.group);
|
|
1696
|
+
const priority = patch.priority === void 0 ? void 0 : requirePriority(patch.priority);
|
|
1697
|
+
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1698
|
+
if (group) i.group = group;
|
|
1699
|
+
else if (clearsGroup(patch.group)) delete i.group;
|
|
1700
|
+
if (priority) i.priority = priority;
|
|
1701
|
+
}, input.expectedVersion));
|
|
1702
|
+
}
|
|
1703
|
+
/** Schedule an issue into the task that will carry it out. */
|
|
1704
|
+
function planIssue(hub, input) {
|
|
1705
|
+
const ref = requireRef(input.task, "task");
|
|
1706
|
+
const draft = new Draft(hub);
|
|
1707
|
+
taskDirIn(draft, ref.mainline, ref.task);
|
|
1708
|
+
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1709
|
+
i.status = "planned";
|
|
1710
|
+
i.plannedIn = input.task;
|
|
1711
|
+
}, input.expectedVersion));
|
|
1712
|
+
}
|
|
1713
|
+
function closeIssue(hub, input, ctx) {
|
|
1714
|
+
if (!input.resolution) throw new WriteError(`closing ${input.id} needs what closed it, e.g. --by "mainflow@3fdd5ac" or --by "不再需要"`);
|
|
1715
|
+
const { day } = stamps(ctx);
|
|
1716
|
+
const draft = new Draft(hub);
|
|
1717
|
+
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1718
|
+
i.status = "closed";
|
|
1719
|
+
i.resolution = input.resolution;
|
|
1720
|
+
i.closedAt ??= day;
|
|
1721
|
+
}, input.expectedVersion));
|
|
1722
|
+
}
|
|
1723
|
+
function reopenIssue(hub, input) {
|
|
1724
|
+
const draft = new Draft(hub);
|
|
1725
|
+
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1726
|
+
i.status = "open";
|
|
1727
|
+
delete i.plannedIn;
|
|
1728
|
+
delete i.resolution;
|
|
1729
|
+
delete i.closedAt;
|
|
1730
|
+
}, input.expectedVersion));
|
|
1731
|
+
}
|
|
1732
|
+
/** Archive or unarchive an issue; the status stays as it is, so a closed issue is visible until archived. */
|
|
1733
|
+
function setIssueArchived(hub, input, ctx) {
|
|
1734
|
+
const { day } = stamps(ctx);
|
|
1735
|
+
const draft = new Draft(hub);
|
|
1736
|
+
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1737
|
+
if (input.archived) i.archivedAt ??= day;
|
|
1738
|
+
else delete i.archivedAt;
|
|
1739
|
+
}, input.expectedVersion));
|
|
1740
|
+
}
|
|
1741
|
+
function createFinding(hub, input, ctx) {
|
|
1742
|
+
requireSlug(input.slug);
|
|
1743
|
+
if (input.origin) requireExisting(hub, input.origin);
|
|
1744
|
+
const { day } = stamps(ctx);
|
|
1745
|
+
const draft = new Draft(hub);
|
|
1746
|
+
const id = draft.number(ctx, {
|
|
1747
|
+
prefix: "f",
|
|
1748
|
+
taken: idsIn(draft, FINDINGS_DIR),
|
|
1749
|
+
ref: (id) => id
|
|
1750
|
+
});
|
|
1751
|
+
const finding = validate(findingSchema, {
|
|
1752
|
+
id,
|
|
1753
|
+
title: input.title,
|
|
1754
|
+
status: "valid",
|
|
1755
|
+
created: day,
|
|
1756
|
+
...input.origin ? { origin: input.origin } : {},
|
|
1757
|
+
...input.labels?.length ? { labels: input.labels } : {}
|
|
1758
|
+
}, "finding");
|
|
1759
|
+
const file = `${FINDINGS_DIR}/${id}-${input.slug}.md`;
|
|
1760
|
+
draft.write(file, frontMatterText(finding, FINDING_BODY));
|
|
1761
|
+
return draft.result({
|
|
1762
|
+
id,
|
|
1763
|
+
file
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
function setFindingStatus(hub, input, ctx) {
|
|
1767
|
+
const value = requireOneOf(input.status, FINDING_STATUS, "finding status");
|
|
1768
|
+
const { day } = stamps(ctx);
|
|
1769
|
+
const draft = new Draft(hub);
|
|
1770
|
+
return draft.result(editFrontMatter(draft, entryFile(draft, FINDINGS_DIR, input.id, "finding"), findingSchema, (f) => {
|
|
1771
|
+
f.status = value;
|
|
1772
|
+
if (value === "obsolete") f.closedAt ??= day;
|
|
1773
|
+
else delete f.closedAt;
|
|
1774
|
+
}, input.expectedVersion));
|
|
1775
|
+
}
|
|
1776
|
+
/** Archive or unarchive a finding; the status stays as it is, so an obsolete finding is visible until archived. */
|
|
1777
|
+
function setFindingArchived(hub, input, ctx) {
|
|
1778
|
+
const { day } = stamps(ctx);
|
|
1779
|
+
const draft = new Draft(hub);
|
|
1780
|
+
return draft.result(editFrontMatter(draft, entryFile(draft, FINDINGS_DIR, input.id, "finding"), findingSchema, (f) => {
|
|
1781
|
+
if (input.archived) f.archivedAt ??= day;
|
|
1782
|
+
else delete f.archivedAt;
|
|
1783
|
+
}, input.expectedVersion));
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* A group gathers mainlines and issues that are taken up together, apart from what depends on what. Its members name
|
|
1787
|
+
* it in their own `group`, so creating a group writes only the group's file.
|
|
1788
|
+
*/
|
|
1789
|
+
function createGroup(hub, input, ctx) {
|
|
1790
|
+
requireSlug(input.slug);
|
|
1791
|
+
if (input.origin) requireExisting(hub, input.origin);
|
|
1792
|
+
const priority = input.priority ? requirePriority(input.priority) : void 0;
|
|
1793
|
+
const { day } = stamps(ctx);
|
|
1794
|
+
const draft = new Draft(hub);
|
|
1795
|
+
const id = draft.number(ctx, {
|
|
1796
|
+
prefix: "w",
|
|
1797
|
+
taken: idsIn(draft, GROUPS_DIR),
|
|
1798
|
+
ref: (id) => id
|
|
1799
|
+
});
|
|
1800
|
+
const group = validate(groupSchema, {
|
|
1801
|
+
id,
|
|
1802
|
+
title: input.title,
|
|
1803
|
+
status: "open",
|
|
1804
|
+
...priority ? { priority } : {},
|
|
1805
|
+
created: day,
|
|
1806
|
+
...input.origin ? { origin: input.origin } : {},
|
|
1807
|
+
...input.labels?.length ? { labels: input.labels } : {},
|
|
1808
|
+
...input.trigger?.trim() ? { trigger: input.trigger.trim() } : {}
|
|
1809
|
+
}, "group");
|
|
1810
|
+
const file = `${GROUPS_DIR}/${id}-${input.slug}.md`;
|
|
1811
|
+
draft.write(file, frontMatterText(group, GROUP_BODY));
|
|
1812
|
+
return draft.result({
|
|
1813
|
+
id,
|
|
1814
|
+
file
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
function setGroup(hub, input, ctx) {
|
|
1818
|
+
const { patch } = input;
|
|
1819
|
+
requireRef(input.id, "group");
|
|
1820
|
+
if (Object.values(patch).every((v) => v === void 0)) throw new WriteError(`${input.id} needs --status, --priority, --trigger or --title, e.g. --status done`);
|
|
1821
|
+
const status = patch.status === void 0 ? void 0 : requireOneOf(patch.status, GROUP_STATUS, "group status");
|
|
1822
|
+
const priority = patch.priority === void 0 ? void 0 : requirePriority(patch.priority);
|
|
1823
|
+
const title = patch.title?.replace(/\s+/g, " ").trim();
|
|
1824
|
+
if (patch.title !== void 0 && !title) throw new WriteError(`${input.id} needs a title, e.g. --title "量測修正"`);
|
|
1825
|
+
const { day } = stamps(ctx);
|
|
1826
|
+
const draft = new Draft(hub);
|
|
1827
|
+
return draft.result(editFrontMatter(draft, entryFile(draft, GROUPS_DIR, input.id, "group"), groupSchema, (g) => {
|
|
1828
|
+
if (status) {
|
|
1829
|
+
g.status = status;
|
|
1830
|
+
stampClosed(g, closes("group", status), day);
|
|
1831
|
+
}
|
|
1832
|
+
if (priority) g.priority = priority;
|
|
1833
|
+
if (title) g.title = title;
|
|
1834
|
+
if (patch.trigger !== void 0) {
|
|
1835
|
+
if (patch.trigger.trim()) g.trigger = patch.trigger.trim();
|
|
1836
|
+
else delete g.trigger;
|
|
1837
|
+
}
|
|
1838
|
+
}, input.expectedVersion));
|
|
1839
|
+
}
|
|
1840
|
+
/** Append one external operation to ops/{YYYY-MM}.log; the log is never rewritten. `at` defaults to the context's time. */
|
|
1841
|
+
function appendOps(hub, input, ctx) {
|
|
1842
|
+
if (input.ref) requireExisting(hub, input.ref);
|
|
1843
|
+
const entry = validate(opsEntrySchema, {
|
|
1844
|
+
...input,
|
|
1845
|
+
at: input.at ?? stamps(ctx).minute
|
|
1846
|
+
}, "ops entry");
|
|
1847
|
+
for (const field of [
|
|
1848
|
+
"target",
|
|
1849
|
+
"operation",
|
|
1850
|
+
"result"
|
|
1851
|
+
]) if (entry[field].includes(" | ")) throw new WriteError(`${field} cannot contain " | ", the ops log's separator`);
|
|
1852
|
+
const draft = new Draft(hub);
|
|
1853
|
+
draft.append(`ops/${entry.at.slice(0, 7)}.log`, `${formatOpsEntry(entry)}\n`);
|
|
1854
|
+
return draft.result(entry);
|
|
1855
|
+
}
|
|
1856
|
+
//#endregion
|
|
1548
1857
|
//#region ../core/dist/ops/mainlines.js
|
|
1549
1858
|
/** Every file below `dir`, sorted, skipping dot files. */
|
|
1550
1859
|
function listFilesIn(hub, dir) {
|
|
@@ -1552,6 +1861,9 @@ function listFilesIn(hub, dir) {
|
|
|
1552
1861
|
}
|
|
1553
1862
|
function createMainline(hub, input, ctx) {
|
|
1554
1863
|
requireSlug(input.slug);
|
|
1864
|
+
const group = input.group ? requireGroup(hub, input.group) : void 0;
|
|
1865
|
+
const role = input.role ? requireOneOf(input.role, MAINLINE_ROLES, "mainline role") : void 0;
|
|
1866
|
+
const priority = input.priority ? requirePriority(input.priority) : void 0;
|
|
1555
1867
|
const { day } = stamps(ctx);
|
|
1556
1868
|
const draft = new Draft(hub);
|
|
1557
1869
|
const id = draft.number(ctx, {
|
|
@@ -1564,7 +1876,10 @@ function createMainline(hub, input, ctx) {
|
|
|
1564
1876
|
id,
|
|
1565
1877
|
title: input.title,
|
|
1566
1878
|
status: "planning",
|
|
1879
|
+
...priority ? { priority } : {},
|
|
1567
1880
|
created: day,
|
|
1881
|
+
...group ? { group } : {},
|
|
1882
|
+
...role ? { role } : {},
|
|
1568
1883
|
criteria: []
|
|
1569
1884
|
}, MAINLINE_FILE);
|
|
1570
1885
|
draft.makeDir(`${dir}/tasks`);
|
|
@@ -1589,16 +1904,50 @@ function requireDone(hub, id, m) {
|
|
|
1589
1904
|
if (!open.length && !proposed.length) return;
|
|
1590
1905
|
throw new WriteError(`${id} cannot be marked done: ${[open.length ? `unmet criteria ${open.join(", ")} (check each with evidence, e.g. criterion check ${open[0]} --evidence <commit>, or reword or drop it in ${MAINLINE_FILE})` : "", proposed.length ? `proposed decisions ${proposed.join(", ")} (decision accept or decision supersede first)` : ""].filter(Boolean).join("; ")}`);
|
|
1591
1906
|
}
|
|
1592
|
-
/**
|
|
1593
|
-
|
|
1907
|
+
/**
|
|
1908
|
+
* Work starting on a mainline that is still planning moves it to active, in the same draft as the work's own
|
|
1909
|
+
* change. It only goes up: any other status is left as it is, and a mainline that cannot be found is skipped.
|
|
1910
|
+
*/
|
|
1911
|
+
function activatePlanning(draft, id) {
|
|
1912
|
+
let path;
|
|
1913
|
+
try {
|
|
1914
|
+
path = mainlineFileIn(draft, id);
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
if (error instanceof WriteError) return;
|
|
1917
|
+
throw error;
|
|
1918
|
+
}
|
|
1919
|
+
const { data } = splitFrontMatter(draft.read(path) ?? "");
|
|
1920
|
+
if (data?.status !== "planning") return;
|
|
1921
|
+
editFrontMatter(draft, path, mainlineSchema, (m) => void (m.status = "active"));
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Change a mainline's status, group, role or priority. Done is refused while criteria are open or decisions are
|
|
1925
|
+
* proposed; the other fields change without conditions, in the same write.
|
|
1926
|
+
*/
|
|
1927
|
+
function setMainline(hub, input, ctx) {
|
|
1594
1928
|
requireRef(input.id, "mainline");
|
|
1595
|
-
|
|
1929
|
+
if ([
|
|
1930
|
+
input.status,
|
|
1931
|
+
input.group,
|
|
1932
|
+
input.role,
|
|
1933
|
+
input.priority
|
|
1934
|
+
].every((v) => v === void 0)) throw new WriteError(`${input.id} needs --status, --group, --role or --priority, e.g. --status paused`);
|
|
1935
|
+
const next = input.status === void 0 ? void 0 : requireOneOf(input.status, MAINLINE_STATUS, "mainline status");
|
|
1936
|
+
const role = input.role === void 0 ? void 0 : requireOneOf(input.role, MAINLINE_ROLES, "mainline role");
|
|
1937
|
+
const priority = input.priority === void 0 ? void 0 : requirePriority(input.priority);
|
|
1596
1938
|
const { day } = stamps(ctx);
|
|
1597
1939
|
const draft = new Draft(hub);
|
|
1940
|
+
const group = input.group === void 0 || clearsGroup(input.group) ? void 0 : requireGroup(draft, input.group);
|
|
1598
1941
|
return draft.result(editFrontMatter(draft, mainlineFileIn(draft, input.id), mainlineSchema, (m) => {
|
|
1599
|
-
if (next
|
|
1600
|
-
|
|
1601
|
-
|
|
1942
|
+
if (next) {
|
|
1943
|
+
if (next === "done" && m.status !== "done") requireDone(draft, input.id, m);
|
|
1944
|
+
m.status = next;
|
|
1945
|
+
stampClosed(m, closes("mainline", next), day);
|
|
1946
|
+
}
|
|
1947
|
+
if (group) m.group = group;
|
|
1948
|
+
else if (clearsGroup(input.group)) delete m.group;
|
|
1949
|
+
if (role) m.role = role;
|
|
1950
|
+
if (priority) m.priority = priority;
|
|
1602
1951
|
}, input.expectedVersion));
|
|
1603
1952
|
}
|
|
1604
1953
|
/** Rename a mainline. References use its ID, so they keep resolving and show the new title. */
|
|
@@ -1724,7 +2073,7 @@ function updateTask(hub, input, ctx) {
|
|
|
1724
2073
|
if (status === "blocked" && !patch.blockedBy?.reason) throw new WriteError(`blocking ${input.ref} needs a reason, e.g. --reason "等使用者提供 API key" --waiting-for 使用者`);
|
|
1725
2074
|
const { day } = stamps(ctx);
|
|
1726
2075
|
const draft = new Draft(hub);
|
|
1727
|
-
|
|
2076
|
+
const task = editFrontMatter(draft, taskFileIn(draft, ref.mainline, ref.task), taskSchema, (t) => {
|
|
1728
2077
|
if (status !== void 0) {
|
|
1729
2078
|
t.status = status;
|
|
1730
2079
|
if (status === "doing") t.startedAt ??= day;
|
|
@@ -1741,7 +2090,9 @@ function updateTask(hub, input, ctx) {
|
|
|
1741
2090
|
if (patch.base !== void 0) t.base = patch.base;
|
|
1742
2091
|
if (merge !== void 0) t.merge = merge;
|
|
1743
2092
|
checkTaskLinks(draft, ref.mainline, t);
|
|
1744
|
-
}, input.expectedVersion)
|
|
2093
|
+
}, input.expectedVersion);
|
|
2094
|
+
if (status === "doing") activatePlanning(draft, ref.mainline);
|
|
2095
|
+
return draft.result(task);
|
|
1745
2096
|
}
|
|
1746
2097
|
/** The criteria a new item serves, as stored: ids of its own mainline's criteria, given as r003 or m001#r003. */
|
|
1747
2098
|
function servedCriteria(hub, mainline, serves) {
|
|
@@ -1786,11 +2137,13 @@ function setChecklistItem(hub, input, ctx) {
|
|
|
1786
2137
|
if (input.done && !input.evidence) throw new WriteError(`checking ${input.ref} needs evidence, e.g. --evidence mainflow@25e4165`);
|
|
1787
2138
|
const { day } = stamps(ctx);
|
|
1788
2139
|
const draft = new Draft(hub);
|
|
1789
|
-
|
|
2140
|
+
const task = editFrontMatter(draft, taskFileIn(draft, ref.mainline, ref.task), taskSchema, (t) => {
|
|
1790
2141
|
const item = t.checklist.find((c) => c.id === ref.item);
|
|
1791
2142
|
if (!item) throw new WriteError(`checklist item ${input.ref} not found`);
|
|
1792
2143
|
setListItem(item, input.done, input.evidence, `${input.ref} is dropped; run item undrop ${input.ref} to bring it back before checking it`, day);
|
|
1793
|
-
}, input.expectedVersion)
|
|
2144
|
+
}, input.expectedVersion);
|
|
2145
|
+
if (input.done) activatePlanning(draft, ref.mainline);
|
|
2146
|
+
return draft.result(task);
|
|
1794
2147
|
}
|
|
1795
2148
|
/** Mark an item as no longer needed, naming what replaced it when something did. */
|
|
1796
2149
|
function dropChecklistItem(hub, input) {
|
|
@@ -1898,161 +2251,6 @@ function supersedeDecision(hub, input, ctx) {
|
|
|
1898
2251
|
}, input.expectedVersion));
|
|
1899
2252
|
}
|
|
1900
2253
|
//#endregion
|
|
1901
|
-
//#region ../core/dist/ops/hub-items.js
|
|
1902
|
-
function entryFile(hub, dir, id, what) {
|
|
1903
|
-
const file = entryFileIn(hub, dir, id);
|
|
1904
|
-
if (!file) throw new WriteError(`${what} ${id} not found`);
|
|
1905
|
-
return file;
|
|
1906
|
-
}
|
|
1907
|
-
/**
|
|
1908
|
-
* Any reference the schema accepts must also point at something that exists. A reference into another
|
|
1909
|
-
* hub only needs that hub registered here; check verifies the target when that hub is on this machine.
|
|
1910
|
-
*/
|
|
1911
|
-
function requireExisting(hub, ref) {
|
|
1912
|
-
const parsed = parseRef(ref);
|
|
1913
|
-
if (!parsed) throw new WriteError(`"${ref}" is not a reference like ${REF_EXAMPLES}`);
|
|
1914
|
-
const config = readConfigIn(hub);
|
|
1915
|
-
if (parsed.hub !== void 0 && parsed.hub !== config.hub) {
|
|
1916
|
-
if (!config.hubs?.[parsed.hub]) throw new WriteError(`hub "${parsed.hub}" is not registered; run "mainflow hub add ${parsed.hub} <path>" first`);
|
|
1917
|
-
return;
|
|
1918
|
-
}
|
|
1919
|
-
switch (parsed.kind) {
|
|
1920
|
-
case "issue":
|
|
1921
|
-
entryFile(hub, ISSUES_DIR, parsed.issue, "issue");
|
|
1922
|
-
return;
|
|
1923
|
-
case "finding":
|
|
1924
|
-
entryFile(hub, FINDINGS_DIR, parsed.finding, "finding");
|
|
1925
|
-
return;
|
|
1926
|
-
default:
|
|
1927
|
-
mainlineDirIn(hub, parsed.mainline);
|
|
1928
|
-
if ("task" in parsed) taskDirIn(hub, parsed.mainline, parsed.task);
|
|
1929
|
-
if (parsed.kind === "decision") decisionFileIn(hub, parsed.mainline, parsed.decision);
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
function createIssue(hub, input, ctx) {
|
|
1933
|
-
requireSlug(input.slug);
|
|
1934
|
-
if (input.origin) requireExisting(hub, input.origin);
|
|
1935
|
-
const { day } = stamps(ctx);
|
|
1936
|
-
const draft = new Draft(hub);
|
|
1937
|
-
const id = draft.number(ctx, {
|
|
1938
|
-
prefix: "i",
|
|
1939
|
-
taken: idsIn(draft, ISSUES_DIR),
|
|
1940
|
-
ref: (id) => id
|
|
1941
|
-
});
|
|
1942
|
-
const issue = validate(issueSchema, {
|
|
1943
|
-
id,
|
|
1944
|
-
title: input.title,
|
|
1945
|
-
status: "open",
|
|
1946
|
-
created: day,
|
|
1947
|
-
...input.origin ? { origin: input.origin } : {},
|
|
1948
|
-
...input.labels?.length ? { labels: input.labels } : {}
|
|
1949
|
-
}, "issue");
|
|
1950
|
-
const file = `${ISSUES_DIR}/${id}-${input.slug}.md`;
|
|
1951
|
-
draft.write(file, frontMatterText(issue, ISSUE_BODY));
|
|
1952
|
-
return draft.result({
|
|
1953
|
-
id,
|
|
1954
|
-
file
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
/** Schedule an issue into the task that will carry it out. */
|
|
1958
|
-
function planIssue(hub, input) {
|
|
1959
|
-
const ref = requireRef(input.task, "task");
|
|
1960
|
-
const draft = new Draft(hub);
|
|
1961
|
-
taskDirIn(draft, ref.mainline, ref.task);
|
|
1962
|
-
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1963
|
-
i.status = "planned";
|
|
1964
|
-
i.plannedIn = input.task;
|
|
1965
|
-
}, input.expectedVersion));
|
|
1966
|
-
}
|
|
1967
|
-
function closeIssue(hub, input, ctx) {
|
|
1968
|
-
if (!input.resolution) throw new WriteError(`closing ${input.id} needs what closed it, e.g. --by "mainflow@3fdd5ac" or --by "不再需要"`);
|
|
1969
|
-
const { day } = stamps(ctx);
|
|
1970
|
-
const draft = new Draft(hub);
|
|
1971
|
-
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1972
|
-
i.status = "closed";
|
|
1973
|
-
i.resolution = input.resolution;
|
|
1974
|
-
i.closedAt ??= day;
|
|
1975
|
-
}, input.expectedVersion));
|
|
1976
|
-
}
|
|
1977
|
-
function reopenIssue(hub, input) {
|
|
1978
|
-
const draft = new Draft(hub);
|
|
1979
|
-
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1980
|
-
i.status = "open";
|
|
1981
|
-
delete i.plannedIn;
|
|
1982
|
-
delete i.resolution;
|
|
1983
|
-
delete i.closedAt;
|
|
1984
|
-
}, input.expectedVersion));
|
|
1985
|
-
}
|
|
1986
|
-
/** Archive or unarchive an issue; the status stays as it is, so a closed issue is visible until archived. */
|
|
1987
|
-
function setIssueArchived(hub, input, ctx) {
|
|
1988
|
-
const { day } = stamps(ctx);
|
|
1989
|
-
const draft = new Draft(hub);
|
|
1990
|
-
return draft.result(editFrontMatter(draft, entryFile(draft, ISSUES_DIR, input.id, "issue"), issueSchema, (i) => {
|
|
1991
|
-
if (input.archived) i.archivedAt ??= day;
|
|
1992
|
-
else delete i.archivedAt;
|
|
1993
|
-
}, input.expectedVersion));
|
|
1994
|
-
}
|
|
1995
|
-
function createFinding(hub, input, ctx) {
|
|
1996
|
-
requireSlug(input.slug);
|
|
1997
|
-
if (input.origin) requireExisting(hub, input.origin);
|
|
1998
|
-
const { day } = stamps(ctx);
|
|
1999
|
-
const draft = new Draft(hub);
|
|
2000
|
-
const id = draft.number(ctx, {
|
|
2001
|
-
prefix: "f",
|
|
2002
|
-
taken: idsIn(draft, FINDINGS_DIR),
|
|
2003
|
-
ref: (id) => id
|
|
2004
|
-
});
|
|
2005
|
-
const finding = validate(findingSchema, {
|
|
2006
|
-
id,
|
|
2007
|
-
title: input.title,
|
|
2008
|
-
status: "valid",
|
|
2009
|
-
created: day,
|
|
2010
|
-
...input.origin ? { origin: input.origin } : {},
|
|
2011
|
-
...input.labels?.length ? { labels: input.labels } : {}
|
|
2012
|
-
}, "finding");
|
|
2013
|
-
const file = `${FINDINGS_DIR}/${id}-${input.slug}.md`;
|
|
2014
|
-
draft.write(file, frontMatterText(finding, FINDING_BODY));
|
|
2015
|
-
return draft.result({
|
|
2016
|
-
id,
|
|
2017
|
-
file
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
function setFindingStatus(hub, input, ctx) {
|
|
2021
|
-
const value = requireOneOf(input.status, FINDING_STATUS, "finding status");
|
|
2022
|
-
const { day } = stamps(ctx);
|
|
2023
|
-
const draft = new Draft(hub);
|
|
2024
|
-
return draft.result(editFrontMatter(draft, entryFile(draft, FINDINGS_DIR, input.id, "finding"), findingSchema, (f) => {
|
|
2025
|
-
f.status = value;
|
|
2026
|
-
if (value === "obsolete") f.closedAt ??= day;
|
|
2027
|
-
else delete f.closedAt;
|
|
2028
|
-
}, input.expectedVersion));
|
|
2029
|
-
}
|
|
2030
|
-
/** Archive or unarchive a finding; the status stays as it is, so an obsolete finding is visible until archived. */
|
|
2031
|
-
function setFindingArchived(hub, input, ctx) {
|
|
2032
|
-
const { day } = stamps(ctx);
|
|
2033
|
-
const draft = new Draft(hub);
|
|
2034
|
-
return draft.result(editFrontMatter(draft, entryFile(draft, FINDINGS_DIR, input.id, "finding"), findingSchema, (f) => {
|
|
2035
|
-
if (input.archived) f.archivedAt ??= day;
|
|
2036
|
-
else delete f.archivedAt;
|
|
2037
|
-
}, input.expectedVersion));
|
|
2038
|
-
}
|
|
2039
|
-
/** Append one external operation to ops/{YYYY-MM}.log; the log is never rewritten. `at` defaults to the context's time. */
|
|
2040
|
-
function appendOps(hub, input, ctx) {
|
|
2041
|
-
if (input.ref) requireExisting(hub, input.ref);
|
|
2042
|
-
const entry = validate(opsEntrySchema, {
|
|
2043
|
-
...input,
|
|
2044
|
-
at: input.at ?? stamps(ctx).minute
|
|
2045
|
-
}, "ops entry");
|
|
2046
|
-
for (const field of [
|
|
2047
|
-
"target",
|
|
2048
|
-
"operation",
|
|
2049
|
-
"result"
|
|
2050
|
-
]) if (entry[field].includes(" | ")) throw new WriteError(`${field} cannot contain " | ", the ops log's separator`);
|
|
2051
|
-
const draft = new Draft(hub);
|
|
2052
|
-
draft.append(`ops/${entry.at.slice(0, 7)}.log`, `${formatOpsEntry(entry)}\n`);
|
|
2053
|
-
return draft.result(entry);
|
|
2054
|
-
}
|
|
2055
|
-
//#endregion
|
|
2056
2254
|
//#region ../core/dist/ops/sections.js
|
|
2057
2255
|
/** The documents that have sections, with the sections each kind is created with. */
|
|
2058
2256
|
const DOCUMENTS = {
|
|
@@ -2060,11 +2258,12 @@ const DOCUMENTS = {
|
|
|
2060
2258
|
task: TASK_SECTIONS,
|
|
2061
2259
|
decision: DECISION_SECTIONS,
|
|
2062
2260
|
issue: ISSUE_SECTIONS,
|
|
2063
|
-
finding: FINDING_SECTIONS
|
|
2261
|
+
finding: FINDING_SECTIONS,
|
|
2262
|
+
group: GROUP_SECTIONS
|
|
2064
2263
|
};
|
|
2065
2264
|
function documentFile(hub, input) {
|
|
2066
2265
|
const ref = parseRef(input);
|
|
2067
|
-
if (!ref || ref.hub !== void 0 || !(ref.kind in DOCUMENTS)) throw new WriteError(`section append takes a mainline, task, decision, issue or
|
|
2266
|
+
if (!ref || ref.hub !== void 0 || !(ref.kind in DOCUMENTS)) throw new WriteError(`section append takes a mainline, task, decision, issue, finding or group of this hub, like m001, m001/t002, m001/d001, i001, f001 or w001; got "${input}"`);
|
|
2068
2267
|
const sections = DOCUMENTS[ref.kind];
|
|
2069
2268
|
const entry = (dir, id, what) => {
|
|
2070
2269
|
const file = entryFileIn(hub, dir, id);
|
|
@@ -2088,6 +2287,10 @@ function documentFile(hub, input) {
|
|
|
2088
2287
|
file: entry(ISSUES_DIR, ref.issue, "issue"),
|
|
2089
2288
|
sections
|
|
2090
2289
|
};
|
|
2290
|
+
case "group": return {
|
|
2291
|
+
file: entry(GROUPS_DIR, ref.group, "group"),
|
|
2292
|
+
sections
|
|
2293
|
+
};
|
|
2091
2294
|
default: return {
|
|
2092
2295
|
file: entry(FINDINGS_DIR, ref.finding, "finding"),
|
|
2093
2296
|
sections
|
|
@@ -2547,6 +2750,7 @@ function updateAction(hub, input, ctx) {
|
|
|
2547
2750
|
if (patch.before) place(s.actions, next, patch.before);
|
|
2548
2751
|
else s.actions.splice(index, 0, next);
|
|
2549
2752
|
});
|
|
2753
|
+
if (status === "doing") for (const m of new Set(updated.serves.map((r) => /^m\d{3}/.exec(r)?.[0]).filter((m) => !!m))) activatePlanning(draft, m);
|
|
2550
2754
|
return draft.result(updated);
|
|
2551
2755
|
}
|
|
2552
2756
|
//#endregion
|
|
@@ -2732,7 +2936,7 @@ const OPERATIONS = {
|
|
|
2732
2936
|
"repo.add": addRepo,
|
|
2733
2937
|
"hub.add": addHub,
|
|
2734
2938
|
"mainline.new": createMainline,
|
|
2735
|
-
"mainline.set":
|
|
2939
|
+
"mainline.set": setMainline,
|
|
2736
2940
|
"mainline.title": setMainlineTitle,
|
|
2737
2941
|
"mainline.archive": setMainlineArchived,
|
|
2738
2942
|
"criterion.add": addCriterion,
|
|
@@ -2748,6 +2952,7 @@ const OPERATIONS = {
|
|
|
2748
2952
|
"decision.accept": acceptDecision,
|
|
2749
2953
|
"decision.supersede": supersedeDecision,
|
|
2750
2954
|
"issue.new": createIssue,
|
|
2955
|
+
"issue.set": setIssue,
|
|
2751
2956
|
"issue.plan": planIssue,
|
|
2752
2957
|
"issue.close": closeIssue,
|
|
2753
2958
|
"issue.reopen": reopenIssue,
|
|
@@ -2755,6 +2960,8 @@ const OPERATIONS = {
|
|
|
2755
2960
|
"finding.new": createFinding,
|
|
2756
2961
|
"finding.set": setFindingStatus,
|
|
2757
2962
|
"finding.archive": setFindingArchived,
|
|
2963
|
+
"group.new": createGroup,
|
|
2964
|
+
"group.set": setGroup,
|
|
2758
2965
|
"ops.add": appendOps,
|
|
2759
2966
|
"section.append": appendSection,
|
|
2760
2967
|
"session.start": startSession,
|
|
@@ -2821,7 +3028,7 @@ function renumberText(text, renumbered, scope) {
|
|
|
2821
3028
|
const parts = [full.size ? `(?<![A-Za-z0-9_:#-])(${alternation([...full.keys()])})${after}` : "(?!)", short.size ? `(?<![A-Za-z0-9_:#/-])(${alternation([...short.keys()])})${after}` : "(?!)"];
|
|
2822
3029
|
return text.replace(new RegExp(parts.join("|"), "g"), (match, whole, id) => whole ? full.get(whole) : id ? short.get(id) : match);
|
|
2823
3030
|
}
|
|
2824
|
-
const ENTRY = /^([
|
|
3031
|
+
const ENTRY = /^([mtdifw]\d{3})(-.+)$/;
|
|
2825
3032
|
/** The scope of a file in the data directory, from its path: the mainline and task directories above it, or its session. */
|
|
2826
3033
|
function scopeOfPath(path) {
|
|
2827
3034
|
const [top, second, third, fourth] = path.split("/");
|
|
@@ -2836,7 +3043,7 @@ function scopeOfPath(path) {
|
|
|
2836
3043
|
}
|
|
2837
3044
|
/**
|
|
2838
3045
|
* The path of a file after renumbering: the directory or file named after a renumbered mainline, task, decision,
|
|
2839
|
-
* issue or
|
|
3046
|
+
* issue, finding or group takes the new id and keeps its slug, e.g. issues/i004-flaky.md for i003 → i004.
|
|
2840
3047
|
*/
|
|
2841
3048
|
function renumberPath(path, renumbered) {
|
|
2842
3049
|
const to = new Map(renumbered.map((r) => [r.from, r.to]));
|
|
@@ -2846,7 +3053,7 @@ function renumberPath(path, renumbered) {
|
|
|
2846
3053
|
if (!entry) return segment;
|
|
2847
3054
|
const id = entry[1];
|
|
2848
3055
|
const mainline = ENTRY.exec(segments[1] ?? "")?.[1];
|
|
2849
|
-
const ref = segments[0] === "mainlines" && i === 1 ? id : segments[0] === "mainlines" && i === 3 && (segments[2] === "tasks" || segments[2] === "decisions") && mainline ? `${mainline}/${id}` : (segments[0] === "issues" || segments[0] === "findings") && i === 1 ? id : void 0;
|
|
3056
|
+
const ref = segments[0] === "mainlines" && i === 1 ? id : segments[0] === "mainlines" && i === 3 && (segments[2] === "tasks" || segments[2] === "decisions") && mainline ? `${mainline}/${id}` : (segments[0] === "issues" || segments[0] === "findings" || segments[0] === "groups") && i === 1 ? id : void 0;
|
|
2850
3057
|
const next = ref === void 0 ? void 0 : to.get(ref);
|
|
2851
3058
|
return next === void 0 ? segment : `${lastId(next)}${entry[2]}`;
|
|
2852
3059
|
}).join("/");
|
|
@@ -3148,14 +3355,15 @@ function pathOf(ref, target) {
|
|
|
3148
3355
|
case "decision": return `/mainlines/${m}/decisions/${child}`;
|
|
3149
3356
|
case "issue": return `/issues?peek=${ref}`;
|
|
3150
3357
|
case "finding": return `/findings?peek=${ref}`;
|
|
3358
|
+
case "group": return `/mainlines?peek=${ref}`;
|
|
3151
3359
|
default: return null;
|
|
3152
3360
|
}
|
|
3153
3361
|
}
|
|
3154
3362
|
const sessionPath = (id) => `/sessions/${id}`;
|
|
3155
3363
|
/** Fenced blocks, inline code, markdown links and bare URLs: text inside them is left as written. */
|
|
3156
3364
|
const PROTECTED = /(```[\s\S]*?(?:```|$)|`[^`\n]*`|\[[^\]\n]*\]\([^)\n]*\)|https?:\/\/\S+)/;
|
|
3157
|
-
/** A file under a mainline directory, or a reference to a mainline, task, decision, issue or
|
|
3158
|
-
const MENTION = /\.mainflow\/mainlines\/(m\d{3})-[\w-]+\/([^\s`'")\]]+\.\w+)|(?<![\w/#.[-])(m\d{3}(?:\/[td]\d{3})?|[
|
|
3365
|
+
/** A file under a mainline directory, or a reference to a mainline, task, decision, issue, finding or group. */
|
|
3366
|
+
const MENTION = /\.mainflow\/mainlines\/(m\d{3})-[\w-]+\/([^\s`'")\]]+\.\w+)|(?<![\w/#.[-])(m\d{3}(?:\/[td]\d{3})?|[ifw]\d{3})(?![\w\]])/g;
|
|
3159
3367
|
/** Links for the references and mainline files the text mentions; references the hub does not have stay as text. */
|
|
3160
3368
|
function linkMentions(text, context) {
|
|
3161
3369
|
const index = refIndex(context.hub);
|
|
@@ -3238,7 +3446,7 @@ function decorateReply(text, context) {
|
|
|
3238
3446
|
return line ? `${body}\n\n${line}\n` : `${body}\n`;
|
|
3239
3447
|
}
|
|
3240
3448
|
/** Cheap test for text that decorateReply could change, so a display hook can skip loading the hub. */
|
|
3241
|
-
const mayDecorate = (text) => /m\d{3}|[
|
|
3449
|
+
const mayDecorate = (text) => /m\d{3}|[ifw]\d{3}|\.mainflow\/mainlines/.test(text) || endsWithMarker(text);
|
|
3242
3450
|
//#endregion
|
|
3243
3451
|
//#region ../core/dist/grants.js
|
|
3244
3452
|
/** Mainline statuses whose grants apply. */
|
|
@@ -7564,6 +7772,7 @@ function reloadCaddy(sites) {
|
|
|
7564
7772
|
if (reload.error) return reload.error.message;
|
|
7565
7773
|
return reload.status === 0 ? null : (reload.stderr || reload.stdout).trim().slice(-300);
|
|
7566
7774
|
}
|
|
7775
|
+
const adminAddress = () => process.env.MAINFLOW_CADDY_ADMIN ?? "http://127.0.0.1:2019";
|
|
7567
7776
|
/**
|
|
7568
7777
|
* Whether Caddy is running, judged by its admin API answering at all. The API refuses requests like
|
|
7569
7778
|
* Node's fetch with 403 ("client is not allowed to access from origin"), so any response counts; only a
|
|
@@ -7571,14 +7780,63 @@ function reloadCaddy(sites) {
|
|
|
7571
7780
|
* running Caddy as stopped). MAINFLOW_CADDY_ADMIN overrides the address.
|
|
7572
7781
|
*/
|
|
7573
7782
|
async function caddyRunning() {
|
|
7574
|
-
const admin = process.env.MAINFLOW_CADDY_ADMIN ?? "http://127.0.0.1:2019";
|
|
7575
7783
|
try {
|
|
7576
|
-
await fetch(`${
|
|
7784
|
+
await fetch(`${adminAddress()}/config/`, { signal: AbortSignal.timeout(500) });
|
|
7577
7785
|
return true;
|
|
7578
7786
|
} catch {
|
|
7579
7787
|
return false;
|
|
7580
7788
|
}
|
|
7581
7789
|
}
|
|
7790
|
+
/** Every `dial` address under `node`. */
|
|
7791
|
+
function collectDials(node, out) {
|
|
7792
|
+
if (Array.isArray(node)) for (const item of node) collectDials(item, out);
|
|
7793
|
+
else if (node && typeof node === "object") for (const [key, value] of Object.entries(node)) if (key === "dial" && typeof value === "string") out.push(value);
|
|
7794
|
+
else collectDials(value, out);
|
|
7795
|
+
}
|
|
7796
|
+
/** Every `dial` address of the routes under `node` whose match names `host`. */
|
|
7797
|
+
function dialsOf(node, host, out = []) {
|
|
7798
|
+
if (Array.isArray(node)) for (const item of node) dialsOf(item, host, out);
|
|
7799
|
+
else if (node && typeof node === "object") {
|
|
7800
|
+
const route = node;
|
|
7801
|
+
if (Array.isArray(route.match) && route.match.some((m) => m.host?.includes(host))) collectDials(route.handle, out);
|
|
7802
|
+
else for (const value of Object.values(route)) dialsOf(value, host, out);
|
|
7803
|
+
}
|
|
7804
|
+
return out;
|
|
7805
|
+
}
|
|
7806
|
+
/**
|
|
7807
|
+
* The upstream addresses the running Caddy has for `host`, read from its admin API (an empty list when it has
|
|
7808
|
+
* no such site), or undefined when the API cannot be read. The request carries an Origin header because the
|
|
7809
|
+
* API answers 403 without one.
|
|
7810
|
+
*/
|
|
7811
|
+
async function runningUpstreams(host) {
|
|
7812
|
+
const admin = adminAddress();
|
|
7813
|
+
try {
|
|
7814
|
+
const response = await fetch(`${admin}/config/`, {
|
|
7815
|
+
headers: { Origin: admin },
|
|
7816
|
+
signal: AbortSignal.timeout(1500)
|
|
7817
|
+
});
|
|
7818
|
+
if (!response.ok) return void 0;
|
|
7819
|
+
return dialsOf(await response.json(), host);
|
|
7820
|
+
} catch {
|
|
7821
|
+
return;
|
|
7822
|
+
}
|
|
7823
|
+
}
|
|
7824
|
+
/**
|
|
7825
|
+
* Remove the mainflow-* links in `sites` whose target is gone. One such link makes every Caddy reload fail,
|
|
7826
|
+
* because the Caddyfile imports the whole directory; hubs deleted without being asked to stop (test hubs)
|
|
7827
|
+
* leave them behind. Other names and links with a target are not touched. Returns the names removed.
|
|
7828
|
+
*/
|
|
7829
|
+
function removeDanglingLinks(sites) {
|
|
7830
|
+
const removed = [];
|
|
7831
|
+
for (const name of readdirSync(sites)) {
|
|
7832
|
+
if (!name.startsWith("mainflow-")) continue;
|
|
7833
|
+
const link = join(sites, name);
|
|
7834
|
+
if (!lstatSync(link, { throwIfNoEntry: false })?.isSymbolicLink() || existsSync(link)) continue;
|
|
7835
|
+
rmSync(link, { force: true });
|
|
7836
|
+
removed.push(name);
|
|
7837
|
+
}
|
|
7838
|
+
return removed;
|
|
7839
|
+
}
|
|
7582
7840
|
const realOf = (path) => existsSync(path) ? realpathSync(path) : resolve(path);
|
|
7583
7841
|
/**
|
|
7584
7842
|
* Point {hub}.mainflow.localhost at `port`. The site block is rewritten and Caddy reloaded only when the
|
|
@@ -7594,23 +7852,36 @@ async function syncHost(root, hub, port, options = {}) {
|
|
|
7594
7852
|
const dir = join(root, DATA_DIR, "local", "caddy");
|
|
7595
7853
|
const file = join(dir, `${hub}.caddy`);
|
|
7596
7854
|
const link = join(sites, `mainflow-${hub}.caddy`);
|
|
7855
|
+
removeDanglingLinks(sites);
|
|
7597
7856
|
const linked = lstatSync(link, { throwIfNoEntry: false });
|
|
7598
7857
|
if (linked) {
|
|
7599
7858
|
const target = linked.isSymbolicLink() ? resolve(dirname(link), readlinkSync(link)) : link;
|
|
7600
7859
|
if (existsSync(target) && realOf(target) !== realOf(file)) throw new WriteError(`host name ${hostName(hub)} is already used by another hub (${link} → ${target}); give one of the hubs a different name in its .mainflow/config.yaml`);
|
|
7601
7860
|
}
|
|
7602
7861
|
const block = siteBlock(hub, port);
|
|
7603
|
-
|
|
7862
|
+
const current = linked && existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
7863
|
+
const pending = join(dir, `${hub}.reload-pending`);
|
|
7864
|
+
const upstreams = await (options.upstreams ?? runningUpstreams)(hostName(hub));
|
|
7865
|
+
const stale = upstreams ? upstreams.length !== 1 || upstreams[0] !== `localhost:${port}` : existsSync(pending);
|
|
7866
|
+
if (current !== block || stale) {
|
|
7604
7867
|
mkdirSync(dir, { recursive: true });
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7868
|
+
if (current !== block) {
|
|
7869
|
+
writeFileSync(file, block);
|
|
7870
|
+
if (linked) rmSync(link);
|
|
7871
|
+
symlinkSync(file, link);
|
|
7872
|
+
}
|
|
7608
7873
|
const failure = (options.reload ?? reloadCaddy)(sites);
|
|
7609
|
-
if (failure)
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7874
|
+
if (failure) {
|
|
7875
|
+
writeFileSync(pending, `${failure}\n`);
|
|
7876
|
+
return {
|
|
7877
|
+
url: null,
|
|
7878
|
+
reason: `caddy reload failed: ${failure}`,
|
|
7879
|
+
reloadFailed: true
|
|
7880
|
+
};
|
|
7881
|
+
}
|
|
7882
|
+
rmSync(pending, { force: true });
|
|
7883
|
+
} else if (upstreams) rmSync(pending, { force: true });
|
|
7884
|
+
else if (!await (options.running ?? caddyRunning)()) return {
|
|
7614
7885
|
url: null,
|
|
7615
7886
|
reason: "Caddy is not running; start it with `brew services start caddy`"
|
|
7616
7887
|
};
|
|
@@ -7625,10 +7896,16 @@ function removeHostLink(root, hub, reload = reloadCaddy) {
|
|
|
7625
7896
|
const sites = caddySites();
|
|
7626
7897
|
if (!sites) return null;
|
|
7627
7898
|
const link = join(sites, `mainflow-${hub}.caddy`);
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7899
|
+
const linked = lstatSync(link, { throwIfNoEntry: false });
|
|
7900
|
+
let removed = false;
|
|
7901
|
+
if (linked?.isSymbolicLink()) {
|
|
7902
|
+
if (resolve(dirname(link), readlinkSync(link)).startsWith(resolve(root) + "/")) {
|
|
7903
|
+
rmSync(link);
|
|
7904
|
+
removed = true;
|
|
7905
|
+
}
|
|
7906
|
+
}
|
|
7907
|
+
if (removeDanglingLinks(sites).length > 0) removed = true;
|
|
7908
|
+
return removed ? reload(sites) : null;
|
|
7632
7909
|
}
|
|
7633
7910
|
/** The macOS app's bundle identifier, from apps/macos/scripts/package.sh. */
|
|
7634
7911
|
const APP_BUNDLE_ID = "space.carlos.mainflow";
|
|
@@ -7662,4 +7939,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
7662
7939
|
return true;
|
|
7663
7940
|
}
|
|
7664
7941
|
//#endregion
|
|
7665
|
-
export {
|
|
7942
|
+
export { ACCESS_HEADERS as $, mayDecorate as A, applyOperation as B, readSession as C, decorateReply as D, matchGrants as E, renumberRequest as F, sectionText as G, HubError as H, renumberFiles as I, linksInto as J, dependencyGraph as K, renumberPath as L, traceTranscript as M, transcriptTitle as N, endsWithMarker as O, applyOffline as P, nodeStorage as Q, renumberText as R, loadSessions as S, grantScope as T, loadConfig as U, checkHubName as V, loadHub as W, nowMinute as X, refIndex as Y, readYaml as Z, initHub as _, DATA_DIR as _t, openInApp as a, comparePaths as at, sessionBriefing as b, splitFrontMatter as bt, siteBlock as c, requestTime as ct, retroReport as d, TASK_FILE as dt, BLOB_HASH as et, retroReportFromReceiver as f, requireSlug as ft, setArchivedMany as g, ConflictError as gt, ARCHIVE_KINDS as h, worktreesOf as ht, hostName as i, SOCKET_MAX_AGE_CLOSE as it, attributeTrace as j, headerLine as k, syncHost as l, textBytes as lt, RETRY_RULES as m, findHubRoot as mt, DEFAULT_UI_PORT as n, MAX_FILE_BYTES as nt, removeDanglingLinks as o, exceedsFileLimit as ot, receiverFromEnv as p, currentBranch as pt, mainlineEdges as q, caddySites as r, ROUTES as rt, removeHostLink as s, readsAsText as st, APP_BUNDLE_ID as t, MAX_BLOB_BYTES as tt, migrateHub as u, MAINLINE_FILE as ut, untilState as v, WriteError as vt, transcriptPath as w, turnReminder as x, duplicateClaimLines as y, contentHash as yt, scopeOfPath as z };
|