@nikovirtala/adr 0.5.2 → 0.5.3

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.
@@ -1,5 +1,5 @@
1
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, countOccurrences, diffTrees, escapeMarkdown, eventDir, eventPath, eventTrailers, extractSections, finding, firstLine, foldLog, foldText, 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, searchExcerpt, searchTerms, serializeEvent, slugify, sortFindings, sourceLogRef, sourcesOfKinds, stagedRenames, templateHistoryRef, transitions, verifyLog, withoutPaths, worktreeReader, writeEvents, writeIndexCopy, writeNewFile } from "@nikovirtala/ledger";
2
+ import { CommitFailedError, ConfigError, ENVELOPE_VERSION, EventExistsError, FileExistsError, InvalidArgumentError, MemoryTree, NO_NUMBERING, UsageError, applySetOp, bodyFileName, bodySha256, classifyPath, commitWithRecovery, compareEvents, compileKind, countOccurrences, diffTrees, escapeMarkdown, eventDir, eventPath, eventTrailers, extractSections, finding, firstLine, foldLog, foldText, followRenames, globMatcher, headBytes, headTree, historyTemplates, holdsText, indexCheck, indexReader, invalidateGitOperation, 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, searchExcerpt, searchTerms, serializeEvent, slugify, sortFindings, sourceLogRef, sourcesOfKinds, stagedRenames, templateHistoryRef, transitions, verifyLog, withGitOperation, 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
@@ -230,14 +230,16 @@ async function openDecisionLog(o) {
230
230
  * copy, which may number acceptances that exist only there (design 2b D1, §4.6).
231
231
  */
232
232
  async function readDecisions(log) {
233
- return readDecisionsFrom(log.tree, {
234
- layout: log.layout,
235
- adr: log.adr,
236
- numbering: await numberDecisions(log),
237
- ref: log.ref,
238
- commit: log.commit,
239
- notes: log.notes,
240
- sources: (log.config?.sources ?? []).map((s) => s.name)
233
+ return withGitOperation(async () => {
234
+ return readDecisionsFrom(log.tree, {
235
+ layout: log.layout,
236
+ adr: log.adr,
237
+ numbering: await numberDecisions(log),
238
+ ref: log.ref,
239
+ commit: log.commit,
240
+ notes: log.notes,
241
+ sources: (log.config?.sources ?? []).map((s) => s.name)
242
+ });
241
243
  });
242
244
  }
243
245
  /** The display numbers `readDecisions` uses, with its shallow-clone fallback to `index.md`. */
@@ -357,25 +359,27 @@ function resolveDecision(read, input) {
357
359
  * Throws `RefNotFoundError` when either revision doesn't resolve.
358
360
  */
359
361
  async function changedSince(log, rev) {
360
- const base = await refReader(log.git, rev);
361
- const head = await refReader(log.git, log.ref ?? "HEAD");
362
- const events = (await diffTrees(base, head, `${log.layout.root}/${log.layout.events}`)).flatMap((c) => {
363
- if (c.status !== "added") return [];
364
- const p = classifyPath(log.layout, c.path);
365
- if (p.area !== "event" || p.subject === null || p.file.includes("/")) return [];
366
- const name = parseEventFileName(p.file);
367
- return name === null ? [] : [{
368
- subject: p.subject,
369
- type: name.type,
370
- path: c.path
371
- }];
362
+ return withGitOperation(async () => {
363
+ const base = await refReader(log.git, rev);
364
+ const head = await refReader(log.git, log.ref ?? "HEAD");
365
+ const events = (await diffTrees(base, head, `${log.layout.root}/${log.layout.events}`)).flatMap((c) => {
366
+ if (c.status !== "added") return [];
367
+ const p = classifyPath(log.layout, c.path);
368
+ if (p.area !== "event" || p.subject === null || p.file.includes("/")) return [];
369
+ const name = parseEventFileName(p.file);
370
+ return name === null ? [] : [{
371
+ subject: p.subject,
372
+ type: name.type,
373
+ path: c.path
374
+ }];
375
+ });
376
+ events.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
377
+ return {
378
+ rev,
379
+ commit: base.commit ?? "",
380
+ events
381
+ };
372
382
  });
373
- events.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
374
- return {
375
- rev,
376
- commit: base.commit ?? "",
377
- events
378
- };
379
383
  }
380
384
  // src/log-index.ts
381
385
  /**
@@ -454,24 +458,26 @@ const DECISION_INDEX = (root, views) => ({
454
458
  * numbers can't be projected.
455
459
  */
456
460
  async function renderDecisionIndex(git, o) {
457
- const { layout, history } = o;
458
- const mergeHead = o.tree.commit === null && history !== null ? await git.resolveCommit("MERGE_HEAD") : null;
459
- const numbering = await projectNumbering(git, {
460
- kind: adrKind,
461
- layout,
462
- history,
463
- tree: o.tree,
464
- mainRef: o.adr.mainBranch,
465
- merging: mergeHead === null ? [] : [mergeHead],
466
- recorded: async () => history === null ? null : readIndexNumbers(await refReader(git, history), indexPath(layout))
461
+ return withGitOperation(async () => {
462
+ const { layout, history } = o;
463
+ const mergeHead = o.tree.commit === null && history !== null ? await git.resolveCommit("MERGE_HEAD") : null;
464
+ const numbering = await projectNumbering(git, {
465
+ kind: adrKind,
466
+ layout,
467
+ history,
468
+ tree: o.tree,
469
+ mainRef: o.adr.mainBranch,
470
+ merging: mergeHead === null ? [] : [mergeHead],
471
+ recorded: async () => history === null ? null : readIndexNumbers(await refReader(git, history), indexPath(layout))
472
+ });
473
+ if (!numbering.available) return null;
474
+ const views = (await readDecisionsFrom(o.tree, {
475
+ layout,
476
+ adr: o.adr,
477
+ numbering
478
+ })).projection.views;
479
+ return renderIndex(DECISION_INDEX(layout.root, views), [...views.values()], numbering);
467
480
  });
468
- if (!numbering.available) return null;
469
- const views = (await readDecisionsFrom(o.tree, {
470
- layout,
471
- adr: o.adr,
472
- numbering
473
- })).projection.views;
474
- return renderIndex(DECISION_INDEX(layout.root, views), [...views.values()], numbering);
475
481
  }
476
482
  /** At most this many paths are named in the unstaged-files `index/stale` message. */
477
483
  const UNSTAGED_NAMED = 5;
@@ -543,115 +549,117 @@ function decisionIndexCheck(log) {
543
549
  * library entry.
544
550
  */
545
551
  async function runDecisionIndex(log, o = {}) {
546
- const path = indexPath(log.layout);
547
- const root = log.layout.root;
548
- const check = o.check === true;
549
- const notes = [...log.notes, ...o.notes ?? []];
550
- if (log.adr.index !== "committed") throw new UsageError("adr index needs [log] index = \"committed\" in adr.toml");
551
- if (check) {
552
- if (o.commit === true || o.message !== void 0 && o.message !== null) throw new UsageError("--check writes nothing; drop --commit and --message");
553
- const findings = await verifyLog({
554
- git: log.git,
555
- kind: adrKind,
552
+ return withGitOperation(async () => {
553
+ const path = indexPath(log.layout);
554
+ const root = log.layout.root;
555
+ const check = o.check === true;
556
+ const notes = [...log.notes, ...o.notes ?? []];
557
+ if (log.adr.index !== "committed") throw new UsageError("adr index needs [log] index = \"committed\" in adr.toml");
558
+ if (check) {
559
+ if (o.commit === true || o.message !== void 0 && o.message !== null) throw new UsageError("--check writes nothing; drop --commit and --message");
560
+ const findings = await verifyLog({
561
+ git: log.git,
562
+ kind: adrKind,
563
+ layout: log.layout,
564
+ mode: {
565
+ type: "full",
566
+ ...log.ref === null ? {} : { head: log.ref }
567
+ },
568
+ checks: [decisionIndexCheck(log)]
569
+ });
570
+ return {
571
+ doc: {
572
+ format: "adr-index/v1",
573
+ path,
574
+ check: true,
575
+ fresh: findings.length === 0,
576
+ written: [],
577
+ commit: null,
578
+ findings,
579
+ notes
580
+ },
581
+ refreshed: false
582
+ };
583
+ }
584
+ if (log.ref !== null || log.tree.label !== "worktree") throw new UsageError("adr index writes the work tree; use --ref only with --check");
585
+ const { git } = log;
586
+ const commit = o.commit ?? log.adr.autoCommit;
587
+ if (commit && (await git.identity() === null || await git.committerIdentity() === null)) throw new ConfigError("--commit needs a git identity; set git user.name and user.email");
588
+ const head = await git.resolveCommit("HEAD");
589
+ const render = (tree, history) => renderDecisionIndex(git, {
556
590
  layout: log.layout,
557
- mode: {
558
- type: "full",
559
- ...log.ref === null ? {} : { head: log.ref }
560
- },
561
- checks: [decisionIndexCheck(log)]
591
+ adr: log.adr,
592
+ tree,
593
+ history
562
594
  });
563
- return {
564
- doc: {
565
- format: "adr-index/v1",
566
- path,
567
- check: true,
568
- fresh: findings.length === 0,
569
- written: [],
570
- commit: null,
571
- findings,
572
- notes
573
- },
574
- refreshed: false
595
+ const shallow = () => new UsageError("numbers need history in this shallow clone; run git fetch --unshallow");
596
+ let written = false;
597
+ const place = async (text) => {
598
+ const r = await writeIndexCopy(git, path, text);
599
+ if (r.stage) await git.add([path]);
600
+ written ||= r.wrote;
601
+ return r.wrote || r.stage;
575
602
  };
576
- }
577
- if (log.ref !== null || log.tree.label !== "worktree") throw new UsageError("adr index writes the work tree; use --ref only with --check");
578
- const { git } = log;
579
- const commit = o.commit ?? log.adr.autoCommit;
580
- if (commit && (await git.identity() === null || await git.committerIdentity() === null)) throw new ConfigError("--commit needs a git identity; set git user.name and user.email");
581
- const head = await git.resolveCommit("HEAD");
582
- const render = (tree, history) => renderDecisionIndex(git, {
583
- layout: log.layout,
584
- adr: log.adr,
585
- tree,
586
- history
587
- });
588
- const shallow = () => new UsageError("numbers need history in this shallow clone; run git fetch --unshallow");
589
- let written = false;
590
- const place = async (text) => {
591
- const r = await writeIndexCopy(git, path, text);
592
- if (r.stage) await git.add([path]);
593
- written ||= r.wrote;
594
- return r.wrote || r.stage;
595
- };
596
- if (!commit) {
597
- const text = await render(worktreeReader(git), head);
603
+ if (!commit) {
604
+ const text = await render(worktreeReader(git), head);
605
+ if (text === null) throw shallow();
606
+ return {
607
+ doc: {
608
+ format: "adr-index/v1",
609
+ path,
610
+ check: false,
611
+ fresh: !await place(text),
612
+ written: written ? [path] : [],
613
+ commit: null,
614
+ findings: [],
615
+ notes
616
+ },
617
+ refreshed: false
618
+ };
619
+ }
620
+ const text = await render(await headTree(git, head), head);
598
621
  if (text === null) throw shallow();
622
+ const fresh = holdsText(await headBytes(git, path), text);
623
+ let sha = null;
624
+ if (!fresh) {
625
+ await place(text);
626
+ const message = o.message ?? renderCommitMessage(log.adr.commitSubject, {
627
+ title: "the decision index",
628
+ vars: {
629
+ action: "regenerate",
630
+ id: "index.md",
631
+ display: "index.md",
632
+ command: "index"
633
+ }
634
+ }).message;
635
+ sha = await commitWithRecovery(git, {
636
+ message,
637
+ only: [path],
638
+ rerun: INDEX_RERUN,
639
+ after: INDEX_AFTER(root)
640
+ });
641
+ }
642
+ let refreshed = false;
643
+ try {
644
+ const again = await render(worktreeReader(git), sha ?? head);
645
+ if (again !== null) refreshed = await place(again);
646
+ } catch (err) {
647
+ notes.push(`${path} may be stale: ${firstLine(err)}; run adr index`);
648
+ }
599
649
  return {
600
650
  doc: {
601
651
  format: "adr-index/v1",
602
652
  path,
603
653
  check: false,
604
- fresh: !await place(text),
654
+ fresh,
605
655
  written: written ? [path] : [],
606
- commit: null,
656
+ commit: sha,
607
657
  findings: [],
608
658
  notes
609
659
  },
610
- refreshed: false
660
+ refreshed
611
661
  };
612
- }
613
- const text = await render(await headTree(git, head), head);
614
- if (text === null) throw shallow();
615
- const fresh = holdsText(await headBytes(git, path), text);
616
- let sha = null;
617
- if (!fresh) {
618
- await place(text);
619
- const message = o.message ?? renderCommitMessage(log.adr.commitSubject, {
620
- title: "the decision index",
621
- vars: {
622
- action: "regenerate",
623
- id: "index.md",
624
- display: "index.md",
625
- command: "index"
626
- }
627
- }).message;
628
- sha = await commitWithRecovery(git, {
629
- message,
630
- only: [path],
631
- rerun: INDEX_RERUN,
632
- after: INDEX_AFTER(root)
633
- });
634
- }
635
- let refreshed = false;
636
- try {
637
- const again = await render(worktreeReader(git), sha ?? head);
638
- if (again !== null) refreshed = await place(again);
639
- } catch (err) {
640
- notes.push(`${path} may be stale: ${firstLine(err)}; run adr index`);
641
- }
642
- return {
643
- doc: {
644
- format: "adr-index/v1",
645
- path,
646
- check: false,
647
- fresh,
648
- written: written ? [path] : [],
649
- commit: sha,
650
- findings: [],
651
- notes
652
- },
653
- refreshed
654
- };
662
+ });
655
663
  }
656
664
  /**
657
665
  * `adr index [--check] [--commit | --no-commit] [-m <message>]` (design 2b §4.5): renders
@@ -742,36 +750,38 @@ const SOURCE_NAME$1 = /^[a-z][a-z0-9-]{0,31}$/;
742
750
  * name that isn't configured or whose kind adr doesn't read.
743
751
  */
744
752
  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
753
+ return withGitOperation(async () => {
754
+ if (!SOURCE_NAME$1.test(name)) throw new UsageError(`--source must match ${SOURCE_NAME$1.source}: ${name}`);
755
+ const { sources, skipped } = decisionSourceConfigs(log);
756
+ const other = skipped.find((s) => s.name === name);
757
+ if (other !== void 0) throw new UsageError(`source ${name} has kind ${other.kind}, which adr doesn't read`);
758
+ const config = sources.find((s) => s.name === name);
759
+ if (config === void 0) {
760
+ const known = [...sources, ...skipped].map((s) => s.name);
761
+ throw new UsageError(known.length === 0 ? `unknown source ${name}; there are no sources` : `unknown source ${name}; the sources are ${known.join(", ")}`);
762
+ }
763
+ const [r] = await readSources(log.git, [config], { fetch: "cache" });
764
+ const base = {
765
+ adr: log.adr,
766
+ notes: [...log.notes],
767
+ source: name
768
+ };
769
+ if (r?.source === void 0 || r.source === null) return readDecisionsFrom(new MemoryTree({}), {
770
+ ...base,
771
+ layout: adrKind.layoutAt(config.root ?? adrKind.def.defaultRoot),
772
+ numbering: NO_NUMBERING,
773
+ notes: [...base.notes, `${name} not fetched; run adr sources fetch`]
774
+ });
775
+ const ref = sourceLogRef(r.source, {
776
+ layout: adrKind.def.layout,
777
+ defaultRoot: adrKind.def.defaultRoot
778
+ });
779
+ return readDecisionsFrom(ref.tree, {
780
+ ...base,
781
+ layout: ref.layout,
782
+ numbering: indexNumbering(await readIndexNumbers(ref.tree, `${ref.layout.root}/index.md`)),
783
+ commit: r.source.commit
784
+ });
775
785
  });
776
786
  }
777
787
  /** The built-in templates by name: the file text `adr new` materializes. */
@@ -2254,81 +2264,83 @@ function commitMessage(log, command, read, plan, c) {
2254
2264
  * every plan with `dryRun` (its views are the plan's).
2255
2265
  */
2256
2266
  async function write(log, command, plan, o, deps, afterStage) {
2257
- if (log.ref !== null) throw new InvalidArgumentError("writes read the work tree; open the log without a ref");
2258
- if (log.tree.label !== "worktree") throw new InvalidArgumentError("writes read the work tree; open the log with the worktree reader");
2259
- const commit = o.commit ?? log.adr.autoCommit;
2260
- if (commit) await checkCommitIdentity(log);
2261
- const notes = [...log.notes, ...o.notes ?? []];
2262
- for (let attempt = 0;; attempt++) {
2263
- const read = await readDecisions(log);
2264
- const ref = await templateHistoryRef(log.git, { type: "full" });
2265
- const r = await plan(read, {
2266
- now: deps.now,
2267
- ...deps.random === void 0 ? {} : { random: deps.random },
2268
- templates: historyTemplates(log.git, log.tree, `${read.root}/templates`, { ref }),
2269
- builtins: BUILTIN_TEMPLATES
2270
- });
2271
- if (!r.ok) return {
2272
- format: "adr-write/v1",
2273
- command,
2274
- ok: false,
2275
- decision: r.decision,
2276
- affected: [],
2277
- written: [],
2278
- commit: null,
2279
- findings: r.findings,
2280
- notes
2281
- };
2282
- if (r.plan.events.length === 0 && Object.keys(r.plan.files).length === 0 && Object.keys(r.plan.replace ?? {}).length === 0 || o.dryRun === true) return {
2283
- format: "adr-write/v1",
2284
- command,
2285
- ok: true,
2286
- decision: r.plan.decision,
2287
- affected: r.plan.affected,
2288
- written: [],
2289
- commit: null,
2290
- findings: r.plan.findings,
2291
- notes: [...notes, ...r.plan.notes ?? []]
2292
- };
2293
- if (commit && r.plan.commit === null) throw new InvalidArgumentError("this write can't be committed");
2294
- const given = o.message ?? null;
2295
- try {
2296
- const applied = await deps.apply(log, r.plan, {
2297
- commit,
2298
- ...!commit ? {} : given !== null ? {
2299
- message: given,
2300
- mergeTrailers: true
2301
- } : { compose: (c) => commitMessage(log, command, read, r.plan, c) },
2302
- ...afterStage === void 0 ? {} : { afterStage: afterStage(r.plan, commit) },
2303
- ...log.adr.index === "committed" && o.index !== "skip" ? { index: (tree, history) => renderDecisionIndex(log.git, {
2304
- layout: log.layout,
2305
- adr: log.adr,
2306
- tree,
2307
- history
2308
- }) } : {}
2267
+ return withGitOperation(async () => {
2268
+ if (log.ref !== null) throw new InvalidArgumentError("writes read the work tree; open the log without a ref");
2269
+ if (log.tree.label !== "worktree") throw new InvalidArgumentError("writes read the work tree; open the log with the worktree reader");
2270
+ const commit = o.commit ?? log.adr.autoCommit;
2271
+ if (commit) await checkCommitIdentity(log);
2272
+ const notes = [...log.notes, ...o.notes ?? []];
2273
+ for (let attempt = 0;; attempt++) {
2274
+ const read = await readDecisions(log);
2275
+ const ref = await templateHistoryRef(log.git, { type: "full" });
2276
+ const r = await plan(read, {
2277
+ now: deps.now,
2278
+ ...deps.random === void 0 ? {} : { random: deps.random },
2279
+ templates: historyTemplates(log.git, log.tree, `${read.root}/templates`, { ref }),
2280
+ builtins: BUILTIN_TEMPLATES
2309
2281
  });
2310
- const views = await viewsAfter(log, r.plan, applied.commit);
2311
- const own = [
2312
- ...r.plan.notes ?? [],
2313
- ...applied.notes,
2314
- ...applied.commit !== null && applied.rendered?.note != null ? [applied.rendered.note] : []
2315
- ];
2316
- return {
2282
+ if (!r.ok) return {
2283
+ format: "adr-write/v1",
2284
+ command,
2285
+ ok: false,
2286
+ decision: r.decision,
2287
+ affected: [],
2288
+ written: [],
2289
+ commit: null,
2290
+ findings: r.findings,
2291
+ notes
2292
+ };
2293
+ if (r.plan.events.length === 0 && Object.keys(r.plan.files).length === 0 && Object.keys(r.plan.replace ?? {}).length === 0 || o.dryRun === true) return {
2317
2294
  format: "adr-write/v1",
2318
2295
  command,
2319
2296
  ok: true,
2320
- decision: views.decision,
2321
- affected: views.affected,
2322
- written: applied.written,
2323
- commit: applied.commit,
2324
- findings: applied.findings,
2325
- notes: [...notes, ...own]
2297
+ decision: r.plan.decision,
2298
+ affected: r.plan.affected,
2299
+ written: [],
2300
+ commit: null,
2301
+ findings: r.plan.findings,
2302
+ notes: [...notes, ...r.plan.notes ?? []]
2326
2303
  };
2327
- } catch (err) {
2328
- if (err instanceof CommitFailedError) throw err.withNotes([...notes, ...r.plan.notes ?? []]);
2329
- if (!(err instanceof EventExistsError) || attempt > 0) throw err;
2304
+ if (commit && r.plan.commit === null) throw new InvalidArgumentError("this write can't be committed");
2305
+ const given = o.message ?? null;
2306
+ try {
2307
+ const applied = await deps.apply(log, r.plan, {
2308
+ commit,
2309
+ ...!commit ? {} : given !== null ? {
2310
+ message: given,
2311
+ mergeTrailers: true
2312
+ } : { compose: (c) => commitMessage(log, command, read, r.plan, c) },
2313
+ ...afterStage === void 0 ? {} : { afterStage: afterStage(r.plan, commit) },
2314
+ ...log.adr.index === "committed" && o.index !== "skip" ? { index: (tree, history) => renderDecisionIndex(log.git, {
2315
+ layout: log.layout,
2316
+ adr: log.adr,
2317
+ tree,
2318
+ history
2319
+ }) } : {}
2320
+ });
2321
+ const views = await viewsAfter(log, r.plan, applied.commit);
2322
+ const own = [
2323
+ ...r.plan.notes ?? [],
2324
+ ...applied.notes,
2325
+ ...applied.commit !== null && applied.rendered?.note != null ? [applied.rendered.note] : []
2326
+ ];
2327
+ return {
2328
+ format: "adr-write/v1",
2329
+ command,
2330
+ ok: true,
2331
+ decision: views.decision,
2332
+ affected: views.affected,
2333
+ written: applied.written,
2334
+ commit: applied.commit,
2335
+ findings: applied.findings,
2336
+ notes: [...notes, ...own]
2337
+ };
2338
+ } catch (err) {
2339
+ if (err instanceof CommitFailedError) throw err.withNotes([...notes, ...r.plan.notes ?? []]);
2340
+ if (!(err instanceof EventExistsError) || attempt > 0) throw err;
2341
+ }
2330
2342
  }
2331
- }
2343
+ });
2332
2344
  }
2333
2345
  const sameBytes = (a, b) => a.byteLength === b.byteLength && a.every((x, i) => x === b[i]);
2334
2346
  async function readIfPresent(path) {
@@ -2355,6 +2367,7 @@ function editorStep(log, edit, onNote) {
2355
2367
  };
2356
2368
  const abs = join(log.git.root ?? "", d.path);
2357
2369
  const note = await edit(abs);
2370
+ invalidateGitOperation();
2358
2371
  const bytes = await readIfPresent(abs);
2359
2372
  let commit;
2360
2373
  if (bytes === null) {
@@ -2403,7 +2416,9 @@ function editorStep(log, edit, onNote) {
2403
2416
  * refused (`ok: false`, nothing written); after `edit` the commit is skipped with a note.
2404
2417
  */
2405
2418
  async function proposeDecision(log, input, o = {}, deps = DEFAULT_DEPS) {
2406
- return write(log, "new", (read, c) => planPropose(read, input, c, { refuseIncomplete: (o.commit ?? log.adr.autoCommit) && o.edit === void 0 }), o, deps, o.edit === void 0 ? void 0 : editorStep(log, o.edit, o.onNote));
2419
+ return withGitOperation(async () => {
2420
+ return write(log, "new", (read, c) => planPropose(read, input, c, { refuseIncomplete: (o.commit ?? log.adr.autoCommit) && o.edit === void 0 }), o, deps, o.edit === void 0 ? void 0 : editorStep(log, o.edit, o.onNote));
2421
+ });
2407
2422
  }
2408
2423
  /**
2409
2424
  * `adr accept`: lints the draft, hashes the body, writes `accepted` and a `superseded` event on
@@ -2421,7 +2436,12 @@ async function acceptDecision(log, input, o = {}, deps = DEFAULT_DEPS) {
2421
2436
  const id = plain.plan.subject;
2422
2437
  const offer = scopeToCopy(read, id);
2423
2438
  if (offer.globs.length === 0) return plain;
2424
- if (copy === null) copy = typeof copyScope === "function" ? await copyScope(offer) : copyScope === "copy";
2439
+ if (copy === null) {
2440
+ if (typeof copyScope === "function") {
2441
+ copy = await copyScope(offer);
2442
+ invalidateGitOperation();
2443
+ } else copy = copyScope === "copy";
2444
+ }
2425
2445
  if (copy) return planAccept(read, input, c, {
2426
2446
  copyGlobs: offer.globs,
2427
2447
  tracked
@@ -2494,10 +2514,12 @@ async function scopeDecision(log, input, o = {}, deps = DEFAULT_DEPS) {
2494
2514
  * refused (`ok: false`, nothing written), as for `proposeDecision` without an editor.
2495
2515
  */
2496
2516
  async function updateDraft(log, input, o = {}, deps = DEFAULT_DEPS) {
2497
- const read = await readDecisions(log);
2498
- const view = viewOf(read, resolveDecision(read, input.id));
2499
- if (view.body_path !== null && (await log.git.unstagedPaths([view.body_path])).length > 0) throw new UsageError(`${view.body_path} has unstaged changes; stage or discard them, or edit the draft by hand`);
2500
- return write(log, "update", (read, c) => planUpdateDraft(read, input, c, { refuseIncomplete: o.commit ?? log.adr.autoCommit }), o, deps);
2517
+ return withGitOperation(async () => {
2518
+ const read = await readDecisions(log);
2519
+ const view = viewOf(read, resolveDecision(read, input.id));
2520
+ if (view.body_path !== null && (await log.git.unstagedPaths([view.body_path])).length > 0) throw new UsageError(`${view.body_path} has unstaged changes; stage or discard them, or edit the draft by hand`);
2521
+ return write(log, "update", (read, c) => planUpdateDraft(read, input, c, { refuseIncomplete: o.commit ?? log.adr.autoCommit }), o, deps);
2522
+ });
2501
2523
  }
2502
2524
  const FOLLOW_NO_COMMIT_NOTE = "--follow-renames never commits; commit the scoped events with the renames";
2503
2525
  /**
@@ -2508,29 +2530,31 @@ const FOLLOW_NO_COMMIT_NOTE = "--follow-renames never commits; commit the scoped
2508
2530
  * `InvalidArgumentError`s. `dryRun` plans the same events and notes without writing anything.
2509
2531
  */
2510
2532
  async function followScopeRenames(log, input, o = {}, deps = DEFAULT_DEPS) {
2511
- if (o.commit === true) throw new InvalidArgumentError("followScopeRenames never commits; don't pass commit: true");
2512
- if (o.message !== void 0 && o.message !== null) throw new InvalidArgumentError("followScopeRenames never commits, so it takes no message");
2513
- const renames = await stagedRenames(log.git);
2514
- const staged = renames.length === 0 ? {
2515
- renames,
2516
- before: [],
2517
- after: []
2518
- } : {
2519
- renames,
2520
- before: await filePaths(await refReader(log.git, "HEAD")),
2521
- after: await filePaths(await indexReader(log.git))
2522
- };
2523
- const doc = await write(log, "scope", (read, c) => planFollowRenames(read, {
2524
- id: input.id ?? null,
2525
- actor: input.actor
2526
- }, staged, c), {
2527
- ...o,
2528
- commit: false
2529
- }, deps);
2530
- return log.adr.autoCommit && doc.ok && doc.affected.length > 0 ? {
2531
- ...doc,
2532
- notes: [...doc.notes, FOLLOW_NO_COMMIT_NOTE]
2533
- } : doc;
2533
+ return withGitOperation(async () => {
2534
+ if (o.commit === true) throw new InvalidArgumentError("followScopeRenames never commits; don't pass commit: true");
2535
+ if (o.message !== void 0 && o.message !== null) throw new InvalidArgumentError("followScopeRenames never commits, so it takes no message");
2536
+ const renames = await stagedRenames(log.git);
2537
+ const staged = renames.length === 0 ? {
2538
+ renames,
2539
+ before: [],
2540
+ after: []
2541
+ } : {
2542
+ renames,
2543
+ before: await filePaths(await refReader(log.git, "HEAD")),
2544
+ after: await filePaths(await indexReader(log.git))
2545
+ };
2546
+ const doc = await write(log, "scope", (read, c) => planFollowRenames(read, {
2547
+ id: input.id ?? null,
2548
+ actor: input.actor
2549
+ }, staged, c), {
2550
+ ...o,
2551
+ commit: false
2552
+ }, deps);
2553
+ return log.adr.autoCommit && doc.ok && doc.affected.length > 0 ? {
2554
+ ...doc,
2555
+ notes: [...doc.notes, FOLLOW_NO_COMMIT_NOTE]
2556
+ } : doc;
2557
+ });
2534
2558
  }
2535
2559
  // src/views.ts
2536
2560
  /** The `numbering` key of `adr-list/v1`, `adr-show/v1` and `adr-for/v1`. */
@@ -2581,37 +2605,39 @@ const decoder$1 = new TextDecoder();
2581
2605
  * Throws `UsageError` for an unknown, unreadable or unfoldable decision.
2582
2606
  */
2583
2607
  async function showDecision(read, id, o = {}) {
2584
- const view = read.projection.views.get(id);
2585
- if (view === void 0) {
2586
- if (read.projection.unreadable.includes(id)) throw new UsageError(`decision ${id} uses a newer format; upgrade adr`);
2587
- if (read.projection.unfoldable.includes(id)) throw new UsageError(`decision ${id} can't be read; run adr verify`);
2588
- throw new UsageError(`unknown decision ${id}`);
2589
- }
2590
- const loaded = read.folded.subjects.get(id)?.loaded;
2591
- const body = loaded?.body ?? null;
2592
- const events = o.history === true ? (loaded?.events ?? []).flatMap((e) => e.event === null ? [] : [{
2593
- path: e.path,
2594
- event: e.event
2595
- }]).sort((a, b) => compareEvents({
2596
- ...a.event,
2597
- path: a.path
2598
- }, {
2599
- ...b.event,
2600
- path: b.path
2601
- })).map((e) => e.event) : null;
2602
- return {
2603
- format: "adr-show/v1",
2604
- root: read.root,
2605
- ref: read.ref,
2606
- commit: read.commit,
2607
- numbering: numberingJson(read),
2608
- header: stateHeader(view, read),
2609
- decision: view,
2610
- body: body === null ? null : decoder$1.decode(body.bytes),
2611
- events,
2612
- findings: read.findings.filter((f) => f.subject === id),
2613
- notes: read.notes
2614
- };
2608
+ return withGitOperation(async () => {
2609
+ const view = read.projection.views.get(id);
2610
+ if (view === void 0) {
2611
+ if (read.projection.unreadable.includes(id)) throw new UsageError(`decision ${id} uses a newer format; upgrade adr`);
2612
+ if (read.projection.unfoldable.includes(id)) throw new UsageError(`decision ${id} can't be read; run adr verify`);
2613
+ throw new UsageError(`unknown decision ${id}`);
2614
+ }
2615
+ const loaded = read.folded.subjects.get(id)?.loaded;
2616
+ const body = loaded?.body ?? null;
2617
+ const events = o.history === true ? (loaded?.events ?? []).flatMap((e) => e.event === null ? [] : [{
2618
+ path: e.path,
2619
+ event: e.event
2620
+ }]).sort((a, b) => compareEvents({
2621
+ ...a.event,
2622
+ path: a.path
2623
+ }, {
2624
+ ...b.event,
2625
+ path: b.path
2626
+ })).map((e) => e.event) : null;
2627
+ return {
2628
+ format: "adr-show/v1",
2629
+ root: read.root,
2630
+ ref: read.ref,
2631
+ commit: read.commit,
2632
+ numbering: numberingJson(read),
2633
+ header: stateHeader(view, read),
2634
+ decision: view,
2635
+ body: body === null ? null : decoder$1.decode(body.bytes),
2636
+ events,
2637
+ findings: read.findings.filter((f) => f.subject === id),
2638
+ notes: read.notes
2639
+ };
2640
+ });
2615
2641
  }
2616
2642
  const day = (at) => (at ?? "").slice(0, 10);
2617
2643
  /** The reason on the applied event that produced the decision's current status, if any. */
@@ -2700,95 +2726,97 @@ async function repoPath(root, cwd, path) {
2700
2726
  * repository are `UsageError`s. Paths need not be tracked.
2701
2727
  */
2702
2728
  async function decisionsFor(read, log, o) {
2703
- const root = log.git.root;
2704
- if (root === null) throw new UsageError("adr for needs a work tree");
2705
- if (o.paths.length === 0 || o.paths.length > MAX_PATHS) throw new UsageError(`adr for takes 1 to ${MAX_PATHS} paths`);
2706
- let index = null;
2707
- const indexed = () => {
2708
- index ??= indexReader(log.git);
2709
- return index;
2710
- };
2711
- const hasUnder = async (tree, path) => (await tree.list(path)).some((e) => e.path !== path);
2712
- const paths = [];
2713
- for (const given of o.paths) {
2714
- const path = await repoPath(root, o.cwd ?? root, given);
2715
- let directory = path === "";
2716
- if (!directory && read.ref === null) {
2717
- const st = await lstat(join(root, path)).catch(() => null);
2718
- directory = st === null ? await hasUnder(await indexed(), path) : st.isDirectory();
2719
- } else if (!directory) directory = await hasUnder(read.tree, path);
2720
- if (directory) throw new UsageError(`${given} is a directory; adr for takes file paths, for example adr for $(git ls-files ${given})`);
2721
- if (!paths.includes(path)) paths.push(path);
2722
- }
2723
- const atRef = read.ref !== null && read.commit !== null;
2724
- const trackedIn = atRef ? read.tree : await indexed();
2725
- const tracked = /* @__PURE__ */ new Set();
2726
- for (const p of paths) if ((await trackedIn.list(p)).some((e) => e.path === p)) tracked.add(p);
2727
- const anchors = await scanAnchors(log.git, {
2728
- keyword: read.adr.anchorKeyword,
2729
- tree: atRef ? { commit: read.commit } : "worktree",
2730
- paths,
2731
- excludeDirs: [read.layout.root],
2732
- exclude: read.adr.anchorExclude
2733
- });
2734
- const views = read.projection.views;
2735
- const lookup = scopeLookup({
2736
- subjects: [...views.values()].filter((v) => GLOBS_APPLY.has(v.status)).map((v) => ({
2737
- id: v.id,
2738
- globs: v.applies_to
2739
- })),
2740
- anchors
2741
- });
2742
- const listed = /* @__PURE__ */ new Set();
2743
- const out = paths.map((path) => {
2744
- const decisions = [];
2745
- const unresolved = [];
2746
- for (const m of lookup(path)) {
2747
- const view = m.source === null ? views.get(m.id) : void 0;
2748
- if (view === void 0) {
2749
- for (const v of m.via) {
2750
- if (v.type !== "anchor") continue;
2751
- unresolved.push({
2752
- ref: m.source === null ? m.id : `${m.source}:${m.id}`,
2753
- line: v.line,
2754
- reason: m.source === null ? "not-found" : "other-log"
2755
- });
2756
- }
2757
- continue;
2758
- }
2759
- decisions.push({
2760
- view,
2761
- via: m.via
2762
- });
2763
- listed.add(view.id);
2729
+ return withGitOperation(async () => {
2730
+ const root = log.git.root;
2731
+ if (root === null) throw new UsageError("adr for needs a work tree");
2732
+ if (o.paths.length === 0 || o.paths.length > MAX_PATHS) throw new UsageError(`adr for takes 1 to ${MAX_PATHS} paths`);
2733
+ let index = null;
2734
+ const indexed = () => {
2735
+ index ??= indexReader(log.git);
2736
+ return index;
2737
+ };
2738
+ const hasUnder = async (tree, path) => (await tree.list(path)).some((e) => e.path !== path);
2739
+ const paths = [];
2740
+ for (const given of o.paths) {
2741
+ const path = await repoPath(root, o.cwd ?? root, given);
2742
+ let directory = path === "";
2743
+ if (!directory && read.ref === null) {
2744
+ const st = await lstat(join(root, path)).catch(() => null);
2745
+ directory = st === null ? await hasUnder(await indexed(), path) : st.isDirectory();
2746
+ } else if (!directory) directory = await hasUnder(read.tree, path);
2747
+ if (directory) throw new UsageError(`${given} is a directory; adr for takes file paths, for example adr for $(git ls-files ${given})`);
2748
+ if (!paths.includes(path)) paths.push(path);
2764
2749
  }
2765
- decisions.sort((a, b) => compareFor(a.view, b.view));
2766
- unresolved.sort((a, b) => a.line - b.line);
2767
- return {
2768
- path,
2769
- tracked: tracked.has(path),
2770
- decisions: decisions.map(({ view: v, via }) => ({
2750
+ const atRef = read.ref !== null && read.commit !== null;
2751
+ const trackedIn = atRef ? read.tree : await indexed();
2752
+ const tracked = /* @__PURE__ */ new Set();
2753
+ for (const p of paths) if ((await trackedIn.list(p)).some((e) => e.path === p)) tracked.add(p);
2754
+ const anchors = await scanAnchors(log.git, {
2755
+ keyword: read.adr.anchorKeyword,
2756
+ tree: atRef ? { commit: read.commit } : "worktree",
2757
+ paths,
2758
+ excludeDirs: [read.layout.root],
2759
+ exclude: read.adr.anchorExclude
2760
+ });
2761
+ const views = read.projection.views;
2762
+ const lookup = scopeLookup({
2763
+ subjects: [...views.values()].filter((v) => GLOBS_APPLY.has(v.status)).map((v) => ({
2771
2764
  id: v.id,
2772
- display: v.display,
2773
- title: v.title,
2774
- status: v.status,
2775
- effective: v.effective,
2776
- warning: stateHeader(v, read).warning,
2777
- via
2765
+ globs: v.applies_to
2778
2766
  })),
2779
- unresolved
2767
+ anchors
2768
+ });
2769
+ const listed = /* @__PURE__ */ new Set();
2770
+ const out = paths.map((path) => {
2771
+ const decisions = [];
2772
+ const unresolved = [];
2773
+ for (const m of lookup(path)) {
2774
+ const view = m.source === null ? views.get(m.id) : void 0;
2775
+ if (view === void 0) {
2776
+ for (const v of m.via) {
2777
+ if (v.type !== "anchor") continue;
2778
+ unresolved.push({
2779
+ ref: m.source === null ? m.id : `${m.source}:${m.id}`,
2780
+ line: v.line,
2781
+ reason: m.source === null ? "not-found" : "other-log"
2782
+ });
2783
+ }
2784
+ continue;
2785
+ }
2786
+ decisions.push({
2787
+ view,
2788
+ via: m.via
2789
+ });
2790
+ listed.add(view.id);
2791
+ }
2792
+ decisions.sort((a, b) => compareFor(a.view, b.view));
2793
+ unresolved.sort((a, b) => a.line - b.line);
2794
+ return {
2795
+ path,
2796
+ tracked: tracked.has(path),
2797
+ decisions: decisions.map(({ view: v, via }) => ({
2798
+ id: v.id,
2799
+ display: v.display,
2800
+ title: v.title,
2801
+ status: v.status,
2802
+ effective: v.effective,
2803
+ warning: stateHeader(v, read).warning,
2804
+ via
2805
+ })),
2806
+ unresolved
2807
+ };
2808
+ });
2809
+ return {
2810
+ format: "adr-for/v1",
2811
+ root: read.root,
2812
+ ref: read.ref,
2813
+ commit: read.commit,
2814
+ numbering: numberingJson(read),
2815
+ paths: out,
2816
+ findings: read.findings.filter((f) => f.subject === null || listed.has(f.subject)),
2817
+ notes: listingNotes(read)
2780
2818
  };
2781
2819
  });
2782
- return {
2783
- format: "adr-for/v1",
2784
- root: read.root,
2785
- ref: read.ref,
2786
- commit: read.commit,
2787
- numbering: numberingJson(read),
2788
- paths: out,
2789
- findings: read.findings.filter((f) => f.subject === null || listed.has(f.subject)),
2790
- notes: listingNotes(read)
2791
- };
2792
2820
  }
2793
2821
  // src/search.ts
2794
2822
  const ACTIVE = /* @__PURE__ */ new Set(["proposed", "accepted"]);
@@ -2895,4 +2923,4 @@ function searchDecisions(read, o) {
2895
2923
  }
2896
2924
  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 };
2897
2925
 
2898
- //# sourceMappingURL=search-DDDuzVfg.js.map
2926
+ //# sourceMappingURL=search-DLUlAP11.js.map