@nikovirtala/adr 0.4.0 → 0.5.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/README.md +15 -9
- package/dist/cli.js +47 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.js +5 -5
- package/dist/{init-YgW7g9Fx.js → init-CaKKCINh.js} +79 -16
- package/dist/init-CaKKCINh.js.map +1 -0
- package/dist/{kind-DjvA3dyA.js → kind-C3DiBuUp.js} +44 -11
- package/dist/kind-C3DiBuUp.js.map +1 -0
- package/dist/kind.js +1 -1
- package/dist/{list-Dt4KGxvj.js → list-CJi9vZqJ.js} +3 -3
- package/dist/{list-Dt4KGxvj.js.map → list-CJi9vZqJ.js.map} +1 -1
- package/dist/{search-CwIPlNGd.js → search-CTspg2pd.js} +290 -88
- package/dist/search-CTspg2pd.js.map +1 -0
- package/dist/{server-CXCO5zfp.js → server-D3ySzyj6.js} +20 -8
- package/dist/server-D3ySzyj6.js.map +1 -0
- package/dist/types/cli/list.d.ts +4 -1
- package/dist/types/cli/search.d.ts +4 -1
- package/dist/types/commit-actions.d.ts +21 -1
- package/dist/types/hooks.d.ts +3 -2
- package/dist/types/index.d.ts +2 -1
- package/dist/types/kind/rules.d.ts +23 -2
- package/dist/types/kind.d.ts +1 -1
- package/dist/types/log-index.d.ts +0 -18
- package/dist/types/log.d.ts +6 -3
- package/dist/types/search.d.ts +2 -0
- package/dist/types/sources.d.ts +35 -0
- package/dist/types/verify.d.ts +20 -4
- package/dist/types/views.d.ts +2 -0
- package/dist/types/write.d.ts +37 -4
- package/package.json +2 -2
- package/dist/init-YgW7g9Fx.js.map +0 -1
- package/dist/kind-DjvA3dyA.js.map +0 -1
- package/dist/search-CwIPlNGd.js.map +0 -1
- package/dist/server-CXCO5zfp.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as adrKind } from "./kind-
|
|
2
|
-
import { CommitFailedError, ConfigError, ENVELOPE_VERSION, EventExistsError, FileExistsError, InvalidArgumentError, MemoryTree, UsageError, applySetOp, bodyFileName, bodySha256, classifyPath, commitWithRecovery, compareEvents, compileKind, diffTrees, escapeMarkdown, eventDir, eventPath, extractSections, finding, foldLog, followRenames, globMatcher, historyTemplates, indexCheck, indexReader, invariantsCheck, isSubjectId, lintDocument, lintTemplate, loadFindings, loadLog, newSubjectId, nextEventIdentity, numberLog, openToolLog, overlayTree, parseCommitTemplate, parseEventFileName, parseGlob, parseTemplate, projectLog, projectNumbering, readCommitTemplate, readIndexNumbers, refReader, relateCheck, renderCommitMessage, renderIndex, renderTemplate, replaceFile, replaceSections, resolveSubjectInput, saveCommitRecovery, scanAnchors, scopeLookup, serializeEvent, slugify, sortFindings, stagedRenames, templateHistoryRef, transitions, verifyLog, worktreeReader, writeEvents,
|
|
1
|
+
import { t as adrKind } from "./kind-C3DiBuUp.js";
|
|
2
|
+
import { CommitFailedError, ConfigError, ENVELOPE_VERSION, EventExistsError, FileExistsError, InvalidArgumentError, MemoryTree, NO_NUMBERING, UsageError, applySetOp, bodyFileName, bodySha256, classifyPath, commitWithRecovery, compareEvents, compileKind, diffTrees, escapeMarkdown, eventDir, eventPath, eventTrailers, extractSections, finding, firstLine, foldLog, followRenames, globMatcher, headBytes, headTree, historyTemplates, holdsText, indexCheck, indexReader, invariantsCheck, isSubjectId, joinActions, lintDocument, lintTemplate, loadFindings, loadLog, newSubjectId, nextEventIdentity, numberLog, openToolLog, overlayTree, parseCommitTemplate, parseEventFileName, parseGlob, parseTemplate, projectLog, projectNumbering, readCommitTemplate, readIndexNumbers, readSources, refReader, relateCheck, renderCommitMessage, renderIndex, renderTemplate, replaceFile, replaceSections, resolveSubjectInput, saveCommitRecovery, scanAnchors, scopeLookup, serializeEvent, slugify, sortFindings, sourceLogRef, sourcesOfKinds, stagedRenames, templateHistoryRef, transitions, verifyLog, withoutPaths, worktreeReader, writeEvents, writeIndexCopy, writeNewFile } from "@nikovirtala/ledger";
|
|
3
3
|
import { lstat, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
5
5
|
// src/commit-actions.ts
|
|
@@ -45,9 +45,33 @@ const ACTIONS = {
|
|
|
45
45
|
names: "display"
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* The verb of each display-named event type in a batch subject (?6, design §4.4), and the order
|
|
50
|
+
* they are joined in: `link and scope ADR-0003`, `add a note to` last so it reads before the name.
|
|
51
|
+
*/
|
|
52
|
+
const BATCH_ACTIONS = {
|
|
53
|
+
deprecated: "deprecate",
|
|
54
|
+
superseded: "supersede",
|
|
55
|
+
linked: "link",
|
|
56
|
+
scoped: "scope",
|
|
57
|
+
noted: "add a note to"
|
|
58
|
+
};
|
|
59
|
+
const BATCH_ORDER = [
|
|
60
|
+
"deprecated",
|
|
61
|
+
"superseded",
|
|
62
|
+
"linked",
|
|
63
|
+
"scoped",
|
|
64
|
+
"noted"
|
|
65
|
+
];
|
|
66
|
+
/** `{action}` of a batch of more than three verbs. */
|
|
67
|
+
const UPDATE_ACTION = "update";
|
|
48
68
|
/** `{action}` of a draft update (`adr_update_draft`), whose title slot holds the display. */
|
|
49
69
|
const DRAFT_ACTION = "update the draft of";
|
|
50
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* The longest `{action}` that goes with a title, which `[commit] subject` must leave a title room
|
|
72
|
+
* for. A batch action (up to `LONGEST_BATCH`) only ever goes with a display, so `COMMIT_NAMES`
|
|
73
|
+
* covers it instead.
|
|
74
|
+
*/
|
|
51
75
|
const LONGEST_ACTION = Math.max(...Object.values(ACTIONS).map((a) => a.action.length), 10, 19);
|
|
52
76
|
/** What a link's title slot names after the display when the target is in another log. */
|
|
53
77
|
const CROSS_LOG_LINK = "to a decision in another log";
|
|
@@ -77,6 +101,10 @@ const COMMIT_NAMES = Object.freeze([
|
|
|
77
101
|
{
|
|
78
102
|
length: 18,
|
|
79
103
|
vars: { action: 10 }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
length: 13,
|
|
107
|
+
vars: { action: 6 }
|
|
80
108
|
}
|
|
81
109
|
]);
|
|
82
110
|
// src/config.ts
|
|
@@ -224,17 +252,17 @@ async function numberDecisions(log) {
|
|
|
224
252
|
} } : {}
|
|
225
253
|
});
|
|
226
254
|
}
|
|
227
|
-
const compiled$
|
|
255
|
+
const compiled$2 = compileKind(adrKind);
|
|
228
256
|
/** The git-free core of `readDecisions`; tests pass a MemoryTree and a hand-made `Numbering`. */
|
|
229
257
|
async function readDecisionsFrom(tree, o) {
|
|
230
258
|
const { layout, adr, numbering } = o;
|
|
231
259
|
const snapshot = await loadLog(tree, {
|
|
232
|
-
kind: compiled$
|
|
260
|
+
kind: compiled$2,
|
|
233
261
|
layout
|
|
234
262
|
});
|
|
235
263
|
const folded = foldLog(adrKind, snapshot);
|
|
236
264
|
const ctx = {
|
|
237
|
-
kind: compiled$
|
|
265
|
+
kind: compiled$2,
|
|
238
266
|
layout,
|
|
239
267
|
base: null,
|
|
240
268
|
head: snapshot,
|
|
@@ -246,7 +274,10 @@ async function readDecisionsFrom(tree, o) {
|
|
|
246
274
|
base: null
|
|
247
275
|
}
|
|
248
276
|
};
|
|
249
|
-
const options = {
|
|
277
|
+
const options = {
|
|
278
|
+
acceptRequiresHuman: adr.acceptRequiresHuman,
|
|
279
|
+
sources: null
|
|
280
|
+
};
|
|
250
281
|
const findings = [];
|
|
251
282
|
for (const check of [
|
|
252
283
|
loadFindings,
|
|
@@ -269,16 +300,17 @@ async function readDecisionsFrom(tree, o) {
|
|
|
269
300
|
kind: adrKind.name
|
|
270
301
|
}))),
|
|
271
302
|
notes: o.notes ?? [],
|
|
272
|
-
sources: o.sources ?? []
|
|
303
|
+
sources: o.sources ?? [],
|
|
304
|
+
source: o.source ?? null
|
|
273
305
|
};
|
|
274
306
|
}
|
|
275
307
|
/**
|
|
276
|
-
* The note listing reads add
|
|
277
|
-
*
|
|
308
|
+
* The note listing reads add for a log with `[[sources]]`: the other logs are listed with
|
|
309
|
+
* `--source`, from cached copies. `null` without sources, and for a `--source` read.
|
|
278
310
|
*/
|
|
279
311
|
function sourcesNote(read) {
|
|
280
|
-
if (read.sources.length === 0) return null;
|
|
281
|
-
return `
|
|
312
|
+
if (read.sources.length === 0 || read.source !== null) return null;
|
|
313
|
+
return `the logs in [[sources]] (${read.sources.join(", ")}) are listed with --source <name>, from cached copies; run adr sources fetch to update them`;
|
|
282
314
|
}
|
|
283
315
|
/** `read.notes`, then the sources note when there is one. */
|
|
284
316
|
function listingNotes(read) {
|
|
@@ -506,37 +538,6 @@ function decisionIndexCheck(log) {
|
|
|
506
538
|
}
|
|
507
539
|
};
|
|
508
540
|
}
|
|
509
|
-
const encoder$1 = new TextEncoder();
|
|
510
|
-
/** Whether `bytes` (null: absent) hold exactly `text`. */
|
|
511
|
-
function holdsText(bytes, text) {
|
|
512
|
-
if (bytes === null) return false;
|
|
513
|
-
const want = encoder$1.encode(text);
|
|
514
|
-
return bytes.byteLength === want.byteLength && bytes.every((x, i) => x === want[i]);
|
|
515
|
-
}
|
|
516
|
-
/** HEAD's copy of `path`; `null` when it is absent or HEAD is unborn. */
|
|
517
|
-
async function headBytes(git, path) {
|
|
518
|
-
const head = await git.resolveCommit("HEAD");
|
|
519
|
-
return head === null ? null : (await refReader(git, head)).read(path);
|
|
520
|
-
}
|
|
521
|
-
/** HEAD's tree, or an empty tree on an unborn HEAD. */
|
|
522
|
-
async function headTree(git, head) {
|
|
523
|
-
return head === null ? new MemoryTree({}) : refReader(git, head);
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Brings the work-tree copy of `path` to `text` (atomically, when it differs) and reports whether
|
|
527
|
-
* the staged copy differs, so the caller stages it. `old` is the work-tree copy before the write
|
|
528
|
-
* (`null` when absent), for rollback.
|
|
529
|
-
*/
|
|
530
|
-
async function writeIndexCopy(git, path, text) {
|
|
531
|
-
const old = await worktreeReader(git).read(path);
|
|
532
|
-
return {
|
|
533
|
-
wrote: !holdsText(old, text) && await writeIndexFile(git, path, text),
|
|
534
|
-
old,
|
|
535
|
-
stage: !holdsText(await (await indexReader(git)).read(path), text)
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
/** The first line of an error's message, for notes and headlines. */
|
|
539
|
-
const firstLine = (err) => (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
|
|
540
541
|
/**
|
|
541
542
|
* `adr index` (design 2b §4.5) with what the text output needs; `indexDecisionLog` is the
|
|
542
543
|
* library entry.
|
|
@@ -662,6 +663,117 @@ async function runDecisionIndex(log, o = {}) {
|
|
|
662
663
|
async function indexDecisionLog(log, o = {}) {
|
|
663
664
|
return (await runDecisionIndex(log, o)).doc;
|
|
664
665
|
}
|
|
666
|
+
// src/sources.ts
|
|
667
|
+
/** The kinds adr reads from `[[sources]]`: only decision logs. */
|
|
668
|
+
const DECISION_SOURCE_KINDS = ["adr"];
|
|
669
|
+
const compiled$1 = compileKind(adrKind);
|
|
670
|
+
/** Numbers recorded in a source's committed index, as a `Numbering`. */
|
|
671
|
+
function indexNumbering(byId) {
|
|
672
|
+
if (byId === null) return NO_NUMBERING;
|
|
673
|
+
return {
|
|
674
|
+
available: true,
|
|
675
|
+
reason: "index",
|
|
676
|
+
ref: null,
|
|
677
|
+
commit: null,
|
|
678
|
+
byId,
|
|
679
|
+
byNumber: new Map([...byId].map(([id, n]) => [n, id]))
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/** The `[[sources]]` entries adr reads and the ones it skips, in config order. */
|
|
683
|
+
function decisionSourceConfigs(log) {
|
|
684
|
+
const all = log.config?.sources ?? [];
|
|
685
|
+
const sources = sourcesOfKinds(all, DECISION_SOURCE_KINDS);
|
|
686
|
+
return {
|
|
687
|
+
sources,
|
|
688
|
+
skipped: all.filter((s) => !sources.includes(s))
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Reads the decision logs of the `kind = "adr"` `[[sources]]` entries (other kinds are never
|
|
693
|
+
* fetched): from the local cache only with `fetch: "cache"` (the default), else fetched first
|
|
694
|
+
* (`readSources`; a failed fetch throws `SourceFetchError`). A cached source's decisions are folded
|
|
695
|
+
* with adr's kind and numbered from its committed `index.md` (the cache is a depth-1 clone, so
|
|
696
|
+
* history numbering can't work), else shown by id.
|
|
697
|
+
*/
|
|
698
|
+
async function readDecisionSources(log, o = {}) {
|
|
699
|
+
const { sources } = decisionSourceConfigs(log);
|
|
700
|
+
const reads = await readSources(log.git, sources, { fetch: o.fetch ?? "cache" });
|
|
701
|
+
const logs = /* @__PURE__ */ new Map();
|
|
702
|
+
const notes = [];
|
|
703
|
+
for (const r of reads) {
|
|
704
|
+
if (r.source === null) {
|
|
705
|
+
logs.set(r.config.name, {
|
|
706
|
+
subjects: null,
|
|
707
|
+
commit: null
|
|
708
|
+
});
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const ref = sourceLogRef(r.source, {
|
|
712
|
+
layout: adrKind.def.layout,
|
|
713
|
+
defaultRoot: adrKind.def.defaultRoot
|
|
714
|
+
});
|
|
715
|
+
const snapshot = await loadLog(ref.tree, {
|
|
716
|
+
kind: compiled$1,
|
|
717
|
+
layout: ref.layout
|
|
718
|
+
});
|
|
719
|
+
if (snapshot.subjects.size === 0) notes.push(`source ${r.config.name} has no decision log at ${ref.layout.root} in ${r.source.commit.slice(0, 12)}`);
|
|
720
|
+
const numbering = indexNumbering(await readIndexNumbers(ref.tree, `${ref.layout.root}/index.md`));
|
|
721
|
+
const projected = projectLog(adrKind, foldLog(adrKind, snapshot), { numbering }).views;
|
|
722
|
+
logs.set(r.config.name, {
|
|
723
|
+
commit: r.source.commit,
|
|
724
|
+
subjects: new Map([...projected].map(([id, v]) => [id, {
|
|
725
|
+
status: v.status,
|
|
726
|
+
display: v.display,
|
|
727
|
+
title: v.title
|
|
728
|
+
}]))
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
logs,
|
|
733
|
+
statuses: reads.map((r) => r.status),
|
|
734
|
+
notes
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
const SOURCE_NAME$1 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
738
|
+
/**
|
|
739
|
+
* The cached decision log of the `[[sources]]` entry `name`, as a `DecisionsRead` for `adr list
|
|
740
|
+
* --source` and `adr search --source` (cache only, never the network). An uncached source reads as
|
|
741
|
+
* an empty log with the note `<name> not fetched; run adr sources fetch`. Throws `UsageError` for a
|
|
742
|
+
* name that isn't configured or whose kind adr doesn't read.
|
|
743
|
+
*/
|
|
744
|
+
async function readSourceDecisions(log, name) {
|
|
745
|
+
if (!SOURCE_NAME$1.test(name)) throw new UsageError(`--source must match ${SOURCE_NAME$1.source}: ${name}`);
|
|
746
|
+
const { sources, skipped } = decisionSourceConfigs(log);
|
|
747
|
+
const other = skipped.find((s) => s.name === name);
|
|
748
|
+
if (other !== void 0) throw new UsageError(`source ${name} has kind ${other.kind}, which adr doesn't read`);
|
|
749
|
+
const config = sources.find((s) => s.name === name);
|
|
750
|
+
if (config === void 0) {
|
|
751
|
+
const known = [...sources, ...skipped].map((s) => s.name);
|
|
752
|
+
throw new UsageError(known.length === 0 ? `unknown source ${name}; there are no sources` : `unknown source ${name}; the sources are ${known.join(", ")}`);
|
|
753
|
+
}
|
|
754
|
+
const [r] = await readSources(log.git, [config], { fetch: "cache" });
|
|
755
|
+
const base = {
|
|
756
|
+
adr: log.adr,
|
|
757
|
+
notes: [...log.notes],
|
|
758
|
+
source: name
|
|
759
|
+
};
|
|
760
|
+
if (r?.source === void 0 || r.source === null) return readDecisionsFrom(new MemoryTree({}), {
|
|
761
|
+
...base,
|
|
762
|
+
layout: adrKind.layoutAt(config.root ?? adrKind.def.defaultRoot),
|
|
763
|
+
numbering: NO_NUMBERING,
|
|
764
|
+
notes: [...base.notes, `${name} not fetched; run adr sources fetch`]
|
|
765
|
+
});
|
|
766
|
+
const ref = sourceLogRef(r.source, {
|
|
767
|
+
layout: adrKind.def.layout,
|
|
768
|
+
defaultRoot: adrKind.def.defaultRoot
|
|
769
|
+
});
|
|
770
|
+
return readDecisionsFrom(ref.tree, {
|
|
771
|
+
...base,
|
|
772
|
+
layout: ref.layout,
|
|
773
|
+
numbering: indexNumbering(await readIndexNumbers(ref.tree, `${ref.layout.root}/index.md`)),
|
|
774
|
+
commit: r.source.commit
|
|
775
|
+
});
|
|
776
|
+
}
|
|
665
777
|
/** The built-in templates by name: the file text `adr new` materializes. */
|
|
666
778
|
const BUILTIN_TEMPLATES = Object.freeze({
|
|
667
779
|
nygard: `---
|
|
@@ -1250,9 +1362,12 @@ async function planAccept(read, input, c, o = {}) {
|
|
|
1250
1362
|
const involved = sorted([id, ...targets]);
|
|
1251
1363
|
const { scope, eventDirs } = commitScope(read, involved);
|
|
1252
1364
|
const bodyPath = view.body_path ?? "";
|
|
1365
|
+
const globs = copied === null ? view.applies_to : copied.data.set;
|
|
1366
|
+
const unmatched = o.tracked === void 0 ? [] : unmatchedGlobs(globs, o.tracked);
|
|
1253
1367
|
return {
|
|
1254
1368
|
ok: true,
|
|
1255
1369
|
plan: {
|
|
1370
|
+
...unmatched.length === 0 ? {} : { notes: unmatched.map((g) => `${g} matches no tracked file; from this commit verify reports scope/glob-unmatched until a file matches, so commit the files with it or change the scope`) },
|
|
1256
1371
|
command: "accept",
|
|
1257
1372
|
subject: id,
|
|
1258
1373
|
files,
|
|
@@ -1480,8 +1595,9 @@ const LINK_RELS = [
|
|
|
1480
1595
|
const SOURCE_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
1481
1596
|
/**
|
|
1482
1597
|
* Plans `adr link` (design §6.7): `linked` in every state. A qualified target must name one of
|
|
1483
|
-
* `o.sources` (the log's `[[sources]]` names)
|
|
1484
|
-
*
|
|
1598
|
+
* `o.sources` (the log's `[[sources]]` names) of kind `adr` (`o.logs`, from the local cache): a
|
|
1599
|
+
* cached source must hold the id, else `UsageError`; into an uncached one the link is written with
|
|
1600
|
+
* a note that it isn't checked. A link that already exists plans nothing, with a note.
|
|
1485
1601
|
*/
|
|
1486
1602
|
async function planLink(read, input, c, o = {}) {
|
|
1487
1603
|
const rel = input.rel ?? "relates-to";
|
|
@@ -1493,12 +1609,19 @@ async function planLink(read, input, c, o = {}) {
|
|
|
1493
1609
|
let shown;
|
|
1494
1610
|
let described;
|
|
1495
1611
|
let named;
|
|
1612
|
+
let foreignNote = null;
|
|
1496
1613
|
const at = target.indexOf(":");
|
|
1497
1614
|
if (at >= 0) {
|
|
1498
1615
|
const source = target.slice(0, at);
|
|
1499
1616
|
const local = target.slice(at + 1);
|
|
1500
1617
|
if (!SOURCE_NAME.test(source) || !isSubjectId(local)) throw new UsageError(`invalid decision ${JSON.stringify(input.to)}`);
|
|
1501
1618
|
if (!(o.sources ?? []).includes(source)) throw new UsageError(`unknown source ${source}; add a [[sources]] entry named ${source}`);
|
|
1619
|
+
const kind = o.kinds?.get(source);
|
|
1620
|
+
if (kind !== void 0 && kind !== "adr") throw new UsageError(`source ${source} has kind ${kind}, which adr doesn't read`);
|
|
1621
|
+
const cached = o.logs?.get(source);
|
|
1622
|
+
if (cached?.subjects !== null && cached?.subjects !== void 0) {
|
|
1623
|
+
if (!cached.subjects.has(local)) throw new UsageError(`${target} is not in source ${source} (pinned at ${(cached.commit ?? "").slice(0, 12)})`);
|
|
1624
|
+
} else if (o.logs !== void 0) foreignNote = `${source} not fetched; the reference isn't checked`;
|
|
1502
1625
|
to = target;
|
|
1503
1626
|
shown = target;
|
|
1504
1627
|
described = `${rel} ${target}.`;
|
|
@@ -1549,7 +1672,8 @@ async function planLink(read, input, c, o = {}) {
|
|
|
1549
1672
|
...r.plan.commit,
|
|
1550
1673
|
title: named,
|
|
1551
1674
|
body: [described]
|
|
1552
|
-
}
|
|
1675
|
+
},
|
|
1676
|
+
notes: [...r.plan.notes ?? [], ...foreignNote === null ? [] : [foreignNote]]
|
|
1553
1677
|
}
|
|
1554
1678
|
};
|
|
1555
1679
|
}
|
|
@@ -1608,7 +1732,7 @@ async function planScope(read, input, c, o = {}) {
|
|
|
1608
1732
|
}
|
|
1609
1733
|
};
|
|
1610
1734
|
if (o.tracked !== void 0) {
|
|
1611
|
-
const checked = (adrKind.def.scope?.
|
|
1735
|
+
const checked = (adrKind.def.scope?.requireMatchIn ?? []).includes(view.status) ? "; verify (hooks and CI) reports scope/glob-unmatched until a file matches" : view.status === "proposed" ? `; verify reports scope/glob-unmatched once ${view.display} is accepted, until a file matches` : "";
|
|
1612
1736
|
notes.push(...unmatchedGlobs(result.set, o.tracked).map((g) => `${g} matches no tracked file${checked}`));
|
|
1613
1737
|
}
|
|
1614
1738
|
if (result.removed.length > 0 && (view.status === "accepted" || view.status === "deprecated")) notes.push("this narrows the scope of an accepted decision; if its Scope section no longer reads true, propose a superseding decision instead");
|
|
@@ -1797,7 +1921,8 @@ async function unstagedIndexNote(log, at, staged, history, render) {
|
|
|
1797
1921
|
*/
|
|
1798
1922
|
async function applyPlan(log, plan, o) {
|
|
1799
1923
|
const { git } = log;
|
|
1800
|
-
if (o.commit && o.message === void 0) throw new InvalidArgumentError("applyPlan needs a message to commit");
|
|
1924
|
+
if (o.commit && o.message === void 0 && o.compose === void 0) throw new InvalidArgumentError("applyPlan needs a message to commit");
|
|
1925
|
+
if (o.message !== void 0 && o.compose !== void 0) throw new InvalidArgumentError("applyPlan takes a message or compose, not both");
|
|
1801
1926
|
const stage = new Set(plan.stage);
|
|
1802
1927
|
if (o.commit) {
|
|
1803
1928
|
const unstaged = new Set(await git.unstagedPaths(plan.commitScope));
|
|
@@ -1902,16 +2027,24 @@ async function applyPlan(log, plan, o) {
|
|
|
1902
2027
|
}
|
|
1903
2028
|
const stale = (err) => `${at} may be stale: ${firstLine(err)}; run adr index`;
|
|
1904
2029
|
let sha = null;
|
|
2030
|
+
let rendered = null;
|
|
1905
2031
|
if (commit) {
|
|
1906
2032
|
const extra = await extraOf();
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2033
|
+
const extraRefs = stagedEventRefs(log.layout, extra);
|
|
2034
|
+
const planRefs = plan.events.map((e) => ({
|
|
2035
|
+
subject: e.subject,
|
|
2036
|
+
type: e.type
|
|
2037
|
+
}));
|
|
2038
|
+
rendered = o.compose === void 0 ? null : o.compose({
|
|
2039
|
+
plan: planRefs,
|
|
2040
|
+
extra: extraRefs,
|
|
2041
|
+
tree: await commitTree(extra)
|
|
1911
2042
|
});
|
|
1912
|
-
const message = o.message ?? "";
|
|
2043
|
+
const message = rendered?.message ?? o.message ?? "";
|
|
1913
2044
|
const mergeTrailers = o.mergeTrailers === true;
|
|
1914
|
-
const
|
|
2045
|
+
const pairs = eventTrailers([...extraRefs, ...planRefs], TRAILER_KEYS);
|
|
2046
|
+
const named = new Set(pairs.filter(([k]) => k === "ADR").map(([, v]) => v));
|
|
2047
|
+
const trailers = [...pairs, ...plan.events.length === 0 ? plan.trailers.filter(([k, v]) => k !== "ADR" || !named.has(v)) : []];
|
|
1915
2048
|
const sameEvents = sorted(extra.filter(isYaml)).join("\0") === sorted(extra0.filter(isYaml)).join("\0");
|
|
1916
2049
|
if (text !== null && o.index !== void 0 && o.afterStage !== void 0 && !sameEvents) try {
|
|
1917
2050
|
const again = await o.index(await commitTree(extra), head);
|
|
@@ -1990,22 +2123,40 @@ async function applyPlan(log, plan, o) {
|
|
|
1990
2123
|
],
|
|
1991
2124
|
commit: sha,
|
|
1992
2125
|
findings,
|
|
1993
|
-
notes
|
|
2126
|
+
notes,
|
|
2127
|
+
rendered
|
|
1994
2128
|
};
|
|
1995
2129
|
}
|
|
2130
|
+
/** The `ADR`/`ADR-Event` trailer keys. */
|
|
2131
|
+
const TRAILER_KEYS = {
|
|
2132
|
+
subject: "ADR",
|
|
2133
|
+
event: "ADR-Event"
|
|
2134
|
+
};
|
|
2135
|
+
/**
|
|
2136
|
+
* The staged event files among `paths` as (decision, type) refs, in path order (by decision,
|
|
2137
|
+
* then event time), deduplicated by decision and type.
|
|
2138
|
+
*/
|
|
2139
|
+
function stagedEventRefs(layout, paths) {
|
|
2140
|
+
const found = [];
|
|
2141
|
+
for (const p of sorted(paths.filter(isYaml))) {
|
|
2142
|
+
const c = classifyPath(layout, p);
|
|
2143
|
+
if (c.area !== "event" || c.subject === null || c.file.includes("/")) continue;
|
|
2144
|
+
const name = parseEventFileName(c.file);
|
|
2145
|
+
if (name !== null) found.push({
|
|
2146
|
+
subject: c.subject,
|
|
2147
|
+
type: name.type
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2151
|
+
return found.filter((r) => {
|
|
2152
|
+
const key = `${r.subject}\0${r.type}`;
|
|
2153
|
+
if (seen.has(key)) return false;
|
|
2154
|
+
seen.add(key);
|
|
2155
|
+
return true;
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
1996
2158
|
/** The new bytes of each file a plan replaces. */
|
|
1997
2159
|
const replaceBytes = (plan) => Object.fromEntries(Object.entries(plan.replace ?? {}).map(([p, r]) => [p, r.bytes]));
|
|
1998
|
-
/** `tree` without the paths in `gone` (a staged deletion in a commit's `only` list). */
|
|
1999
|
-
function withoutPaths(tree, gone) {
|
|
2000
|
-
if (gone.size === 0) return tree;
|
|
2001
|
-
return {
|
|
2002
|
-
label: tree.label,
|
|
2003
|
-
commit: null,
|
|
2004
|
-
list: async (prefix) => (await tree.list(prefix)).filter((e) => !gone.has(e.path)),
|
|
2005
|
-
read: async (path) => gone.has(path) ? null : tree.read(path),
|
|
2006
|
-
readMany: (paths) => tree.readMany(paths.filter((p) => !gone.has(p)))
|
|
2007
|
-
};
|
|
2008
|
-
}
|
|
2009
2160
|
const DEFAULT_DEPS = {
|
|
2010
2161
|
now: Date.now,
|
|
2011
2162
|
apply: applyPlan
|
|
@@ -2031,7 +2182,6 @@ async function viewsAfter(log, plan, sha) {
|
|
|
2031
2182
|
affected: plan.affected.map(view)
|
|
2032
2183
|
};
|
|
2033
2184
|
}
|
|
2034
|
-
const SHORTENED_NOTE = "commit subject shortened to 72 characters; the full title is in the commit body";
|
|
2035
2185
|
/**
|
|
2036
2186
|
* `--commit` needs an author and a committer identity; checked before anything is written, since
|
|
2037
2187
|
* `GIT_AUTHOR_*` alone satisfies the author while `git commit` still fails on the committer.
|
|
@@ -2039,24 +2189,63 @@ const SHORTENED_NOTE = "commit subject shortened to 72 characters; the full titl
|
|
|
2039
2189
|
async function checkCommitIdentity(log) {
|
|
2040
2190
|
if (await log.git.identity() === null || await log.git.committerIdentity() === null) throw new ConfigError("--commit needs a git identity; set git user.name and user.email");
|
|
2041
2191
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2192
|
+
const STATUS_TYPES = /* @__PURE__ */ new Set([
|
|
2193
|
+
"proposed",
|
|
2194
|
+
"accepted",
|
|
2195
|
+
"rejected",
|
|
2196
|
+
"withdrawn"
|
|
2197
|
+
]);
|
|
2198
|
+
/**
|
|
2199
|
+
* The commit's action and title slot (ADR-0008, ?6), chosen once the staged events the commit
|
|
2200
|
+
* sweeps in are known: when `plan` or `extra` holds a status event (proposed, accepted, rejected,
|
|
2201
|
+
* withdrawn), the title form of the last one, since that is what the commit decides. Otherwise
|
|
2202
|
+
* the plan's own subject when the extra events add no other verb; with two or three verbs those
|
|
2203
|
+
* joined (`link and scope ADR-0003`), and beyond three (or where the joined verbs don't fit a
|
|
2204
|
+
* custom template) `update ADR-0003`. Every single-action subject stays the plan's own, byte for
|
|
2205
|
+
* byte.
|
|
2206
|
+
*/
|
|
2207
|
+
function composeSubject(read, parts, c) {
|
|
2208
|
+
const status = [...c.extra, ...c.plan].filter((e) => STATUS_TYPES.has(e.type)).at(-1);
|
|
2209
|
+
if (status !== void 0) {
|
|
2210
|
+
if (c.plan.some((e) => e.subject === status.subject && e.type === status.type)) return {
|
|
2211
|
+
action: parts.action,
|
|
2212
|
+
title: parts.title
|
|
2213
|
+
};
|
|
2214
|
+
return {
|
|
2215
|
+
action: ACTIONS[status.type].action,
|
|
2216
|
+
title: read.projection.views.get(status.subject)?.title ?? status.subject
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
const primary = c.plan[0]?.type;
|
|
2220
|
+
const verbs = /* @__PURE__ */ new Set();
|
|
2221
|
+
for (const t of [primary, ...c.extra.map((e) => e.type)]) if (t !== void 0 && Object.hasOwn(BATCH_ACTIONS, t)) verbs.add(t);
|
|
2222
|
+
if (verbs.size <= 1 || primary === void 0 || !verbs.has(primary)) return {
|
|
2223
|
+
action: parts.action,
|
|
2224
|
+
title: parts.title
|
|
2225
|
+
};
|
|
2226
|
+
const ordered = BATCH_ORDER.filter((t) => verbs.has(t)).map((t) => BATCH_ACTIONS[t]);
|
|
2227
|
+
return {
|
|
2228
|
+
action: ordered.length > 3 ? UPDATE_ACTION : joinActions(ordered),
|
|
2229
|
+
title: parts.display
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
/** The plan's commit message from `[commit] subject` (design §4), with `composeSubject`'s slot. */
|
|
2233
|
+
function commitMessage(log, command, read, plan, c) {
|
|
2044
2234
|
const parts = plan.commit;
|
|
2045
2235
|
if (parts === null) throw new InvalidArgumentError("this write can't be committed");
|
|
2046
|
-
const
|
|
2047
|
-
|
|
2236
|
+
const { action, title } = composeSubject(read, parts, c);
|
|
2237
|
+
const render = (a) => renderCommitMessage(log.adr.commitSubject, {
|
|
2238
|
+
title,
|
|
2048
2239
|
vars: {
|
|
2049
|
-
action:
|
|
2240
|
+
action: a,
|
|
2050
2241
|
id: parts.id,
|
|
2051
2242
|
display: parts.display,
|
|
2052
2243
|
command
|
|
2053
2244
|
},
|
|
2054
2245
|
body: parts.body
|
|
2055
2246
|
});
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
shortened: r.shortened
|
|
2059
|
-
};
|
|
2247
|
+
const r = render(action);
|
|
2248
|
+
return r.shortened && title === parts.display && action !== parts.action ? render(UPDATE_ACTION) : r;
|
|
2060
2249
|
}
|
|
2061
2250
|
/**
|
|
2062
2251
|
* Plan, then apply, with the single `EventExistsError` retry (§5.4 steps 1–5). `o.message`
|
|
@@ -2101,15 +2290,15 @@ async function write(log, command, plan, o, deps, afterStage) {
|
|
|
2101
2290
|
findings: r.plan.findings,
|
|
2102
2291
|
notes: [...notes, ...r.plan.notes ?? []]
|
|
2103
2292
|
};
|
|
2104
|
-
|
|
2293
|
+
if (commit && r.plan.commit === null) throw new InvalidArgumentError("this write can't be committed");
|
|
2105
2294
|
const given = o.message ?? null;
|
|
2106
2295
|
try {
|
|
2107
2296
|
const applied = await deps.apply(log, r.plan, {
|
|
2108
2297
|
commit,
|
|
2109
|
-
|
|
2110
|
-
message: given
|
|
2111
|
-
mergeTrailers:
|
|
2112
|
-
},
|
|
2298
|
+
...!commit ? {} : given !== null ? {
|
|
2299
|
+
message: given,
|
|
2300
|
+
mergeTrailers: true
|
|
2301
|
+
} : { compose: (c) => commitMessage(log, command, read, r.plan, c) },
|
|
2113
2302
|
...afterStage === void 0 ? {} : { afterStage: afterStage(r.plan, commit) },
|
|
2114
2303
|
...log.adr.index === "committed" && o.index !== "skip" ? { index: (tree, history) => renderDecisionIndex(log.git, {
|
|
2115
2304
|
layout: log.layout,
|
|
@@ -2122,7 +2311,7 @@ async function write(log, command, plan, o, deps, afterStage) {
|
|
|
2122
2311
|
const own = [
|
|
2123
2312
|
...r.plan.notes ?? [],
|
|
2124
2313
|
...applied.notes,
|
|
2125
|
-
...applied.commit !== null &&
|
|
2314
|
+
...applied.commit !== null && applied.rendered?.note != null ? [applied.rendered.note] : []
|
|
2126
2315
|
];
|
|
2127
2316
|
return {
|
|
2128
2317
|
format: "adr-write/v1",
|
|
@@ -2225,14 +2414,18 @@ async function proposeDecision(log, input, o = {}, deps = DEFAULT_DEPS) {
|
|
|
2225
2414
|
async function acceptDecision(log, input, o = {}, deps = DEFAULT_DEPS) {
|
|
2226
2415
|
const copyScope = o.copyScope ?? "suggest";
|
|
2227
2416
|
let copy = null;
|
|
2417
|
+
const tracked = await trackedFiles(log);
|
|
2228
2418
|
return write(log, "accept", async (read, c) => {
|
|
2229
|
-
const plain = await planAccept(read, input, c);
|
|
2419
|
+
const plain = await planAccept(read, input, c, { tracked });
|
|
2230
2420
|
if (!plain.ok) return plain;
|
|
2231
2421
|
const id = plain.plan.subject;
|
|
2232
2422
|
const offer = scopeToCopy(read, id);
|
|
2233
2423
|
if (offer.globs.length === 0) return plain;
|
|
2234
2424
|
if (copy === null) copy = typeof copyScope === "function" ? await copyScope(offer) : copyScope === "copy";
|
|
2235
|
-
if (copy) return planAccept(read, input, c, {
|
|
2425
|
+
if (copy) return planAccept(read, input, c, {
|
|
2426
|
+
copyGlobs: offer.globs,
|
|
2427
|
+
tracked
|
|
2428
|
+
});
|
|
2236
2429
|
if (copyScope === "skip") return plain;
|
|
2237
2430
|
const notes = [...plain.plan.notes ?? [], copyScopeNote(id, offer)];
|
|
2238
2431
|
return {
|
|
@@ -2275,8 +2468,15 @@ async function noteDecision(log, input, o = {}, deps = DEFAULT_DEPS) {
|
|
|
2275
2468
|
* Qualified targets name the log's `[[sources]]`; a link that exists already writes nothing.
|
|
2276
2469
|
*/
|
|
2277
2470
|
async function linkDecision(log, input, o = {}, deps = DEFAULT_DEPS) {
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2471
|
+
const configs = log.config?.sources ?? [];
|
|
2472
|
+
const sources = configs.map((s) => s.name);
|
|
2473
|
+
const kinds = new Map(configs.map((s) => [s.name, s.kind]));
|
|
2474
|
+
const logs = input.to.includes(":") ? (await readDecisionSources(log, { fetch: "cache" })).logs : void 0;
|
|
2475
|
+
return write(log, "link", (read, c) => planLink(read, input, c, {
|
|
2476
|
+
sources,
|
|
2477
|
+
kinds,
|
|
2478
|
+
...logs === void 0 ? {} : { logs }
|
|
2479
|
+
}), o, deps);
|
|
2280
2480
|
}
|
|
2281
2481
|
/**
|
|
2282
2482
|
* `adr scope <id> --set|--add|--remove …`: writes one `scoped` event with the new set, stages it,
|
|
@@ -2362,6 +2562,7 @@ function listDecisions(read, o = {}) {
|
|
|
2362
2562
|
const listed = new Set(decisions.map((v) => v.id));
|
|
2363
2563
|
return {
|
|
2364
2564
|
format: "adr-list/v1",
|
|
2565
|
+
source: read.source,
|
|
2365
2566
|
root: read.root,
|
|
2366
2567
|
ref: read.ref,
|
|
2367
2568
|
commit: read.commit,
|
|
@@ -2746,6 +2947,7 @@ function searchDecisions(read, o) {
|
|
|
2746
2947
|
const listed = new Set(results.map((r) => r.decision.id));
|
|
2747
2948
|
return {
|
|
2748
2949
|
format: "adr-search/v1",
|
|
2950
|
+
source: read.source,
|
|
2749
2951
|
root: read.root,
|
|
2750
2952
|
ref: read.ref,
|
|
2751
2953
|
commit: read.commit,
|
|
@@ -2760,6 +2962,6 @@ function searchDecisions(read, o) {
|
|
|
2760
2962
|
notes: listingNotes(read)
|
|
2761
2963
|
};
|
|
2762
2964
|
}
|
|
2763
|
-
export {
|
|
2965
|
+
export { numberDecisions as A, INDEX_AFTER as C, renderDecisionIndex as D, indexPath as E, resolveDecision as F, sourcesNote as I, DEFAULT_ADR_CONFIG as L, openDecisionLog as M, readDecisions as N, runDecisionIndex as O, readDecisionsFrom as P, parseAdrConfig as R, DECISION_INDEX as S, indexDecisionLog as T, withdrawDecision as _, stateHeader as a, readDecisionSources as b, followScopeRenames as c, proposeDecision as d, rejectDecision as f, updateDraft as g, supersedeDecision as h, showDecision as i, numberingNote as j, changedSince as k, linkDecision as l, scopeToCopy as m, decisionsFor as n, acceptDecision as o, scopeDecision as p, listDecisions as r, deprecateDecision as s, searchDecisions as t, noteDecision as u, BUILTIN_TEMPLATES as v, decisionIndexCheck as w, readSourceDecisions as x, decisionSourceConfigs as y };
|
|
2764
2966
|
|
|
2765
|
-
//# sourceMappingURL=search-
|
|
2967
|
+
//# sourceMappingURL=search-CTspg2pd.js.map
|