@wemuda/launchrail 1.19.0 → 1.20.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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
2
3
  import { AVAILABLE_MODULES, runAdd } from "./commands/add.js";
3
- import { runAdrIndex, runAdrMergeDriver } from "./commands/adr.js";
4
+ import { runAdrIndex } from "./commands/adr.js";
4
5
  import { printDiff, runDiff } from "./commands/diff.js";
5
6
  import { runDev } from "./commands/dev.js";
6
7
  import { runDoctor, printDoctor } from "./commands/doctor.js";
@@ -24,7 +25,7 @@ Commands:
24
25
  diff Preview upstream changes
25
26
  sync Synchronize managed capabilities and run migrations
26
27
  eject Stop managing a selected module or file
27
- adr Maintain the decision-record registry (adr index [--check])
28
+ adr Print the decision-record index (adr index)
28
29
  promote Inspect potential reusable local improvements
29
30
 
30
31
  Options:
@@ -53,8 +54,7 @@ eject usage:
53
54
  launchrail eject --all [--dry-run] Vendor mode: eject everything
54
55
 
55
56
  adr usage:
56
- launchrail adr index [--check] Regenerate docs/adr/README.md's index table from the records (--check: report only)
57
- launchrail adr merge-driver <O> <A> <B> [P] Resolve a docs/adr/README.md index conflict by regeneration (git invokes this; install via init / sync / doctor)`;
57
+ launchrail adr index Print the ADR index from the records in docs/adr/ (never written to a file)`;
58
58
  const NOT_IMPLEMENTED = ["promote"];
59
59
  const args = process.argv.slice(2);
60
60
  const command = args[0];
@@ -136,19 +136,22 @@ if (command === "eject") {
136
136
  }
137
137
  if (command === "adr") {
138
138
  if (args[1] === "index") {
139
- process.exit(runAdrIndex({ cwd: process.cwd(), check: flags.has("--check") }).code);
139
+ process.exit(runAdrIndex({ cwd: process.cwd() }).code);
140
140
  }
141
141
  if (args[1] === "merge-driver") {
142
- // Invoked by git as the merge driver bound in .gitattributes; args are
143
- // git's %O %A %B %P (base, ours, theirs, path).
144
- const [base, ours, theirs, path] = [args[2], args[3], args[4], args[5]];
145
- if (!base || !ours || !theirs) {
146
- console.error("launchrail: usage: launchrail adr merge-driver <base> <ours> <theirs> [path]");
142
+ // Retired with the committed index (2026-10-01-adr-index-is-printed-not-committed).
143
+ // A clone that still binds it — an older branch's .gitattributes, before
144
+ // sync or doctor drop the clone's config — gets git's own text merge, so a
145
+ // conflict shows the usual markers instead of an untouched file.
146
+ const [base, ours, theirs] = [args[2], args[3], args[4]];
147
+ if (!base || !ours || !theirs)
147
148
  process.exit(2);
148
- }
149
- process.exit(runAdrMergeDriver({ cwd: process.cwd(), base, ours, theirs, path }).code);
149
+ const merged = spawnSync("git", ["merge-file", "-L", "ours", "-L", "base", "-L", "theirs", ours, base, theirs], {
150
+ stdio: "inherit",
151
+ });
152
+ process.exit(merged.status ?? 1);
150
153
  }
151
- console.error("launchrail: usage: launchrail adr index [--check]");
154
+ console.error("launchrail: usage: launchrail adr index");
152
155
  process.exit(1);
153
156
  }
154
157
  if (NOT_IMPLEMENTED.includes(command)) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/lib/adr.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  export declare const ADR_DIR = "docs/adr";
2
2
  export declare const ADR_TEMPLATE_FILENAME = "0000-template.md";
3
3
  export declare const ADR_REGISTRY_PATH = "docs/adr/README.md";
4
+ /**
5
+ * The markers that once bracketed the generated table committed into the
6
+ * registry. The index is printed now, never committed
7
+ * (2026-10-01-adr-index-is-printed-not-committed); the markers survive only so
8
+ * the migration and doctor can find a table an older version left behind.
9
+ */
4
10
  export declare const ADR_INDEX_START = "<!-- adr-index:start \u2014 generated by `launchrail adr index`; edit the records, not this table -->";
5
11
  export declare const ADR_INDEX_END = "<!-- adr-index:end -->";
6
12
  export interface AdrEntry {
@@ -23,14 +29,6 @@ export interface AdrEntry {
23
29
  export declare function isAdrRecordFilename(file: string): boolean;
24
30
  /** Parse one record's file and source into an entry; null when the filename is not a record. */
25
31
  export declare function parseAdrEntry(file: string, source: string): AdrEntry | null;
26
- /**
27
- * The record list built from an explicit set of file→source pairs, sorted by
28
- * filename exactly as scanAdrs sorts the directory — numbered records first,
29
- * dated records after them in date order. The merge driver uses this to
30
- * assemble the corpus from the working tree plus the records only the incoming
31
- * side carries, which are not yet on disk when git invokes the driver.
32
- */
33
- export declare function entriesFromSources(sources: Iterable<readonly [string, string]>): AdrEntry[];
34
32
  /**
35
33
  * The decision records already in a repository's docs/adr/ (template and
36
34
  * registry excluded), sorted by filename — numbered records first, dated
@@ -46,8 +44,6 @@ export declare function adrDuplicates(entries: AdrEntry[]): {
46
44
  id: string;
47
45
  files: string[];
48
46
  }[];
49
- /** Records whose filename never appears in the registry — rows the index is missing. */
50
- export declare function unindexedAdrs(registrySource: string, entries: AdrEntry[]): AdrEntry[];
51
47
  type Relation = "supersedes" | "amends" | "extends";
52
48
  /**
53
49
  * The relation graph declared across the corpus. A record declares what it
@@ -63,40 +59,54 @@ export declare function adrRelations(entries: AdrEntry[]): Map<string, Record<Re
63
59
  * their successor(s), the one fact a reader must not miss.
64
60
  */
65
61
  export declare function adrStatusCell(entry: AdrEntry, entries: AdrEntry[], graph?: Map<string, Record<Relation, AdrEntry[]>>): string;
66
- /** The generated index table, markers included. */
67
- export declare function adrIndexTable(entries: AdrEntry[]): string;
68
- /**
69
- * A single-line placeholder the merge driver substitutes for the index region
70
- * while it 3-way merges the registry's prose, then swaps back out for the freshly
71
- * regenerated table. Comment-shaped so it is inert if it ever leaks into a file,
72
- * and identical across all three sides so the prose merge sees no index diff.
73
- */
74
- export declare const ADR_INDEX_SENTINEL = "<!-- launchrail:adr-index-merge-placeholder -->";
75
62
  /**
76
- * Replace the registry's index region with `replacement`: everything between the
77
- * markers when they exist; otherwise the first table under `## Index`, with the
78
- * region newline-padded (a registry written before the generator migrates on its
79
- * first run). Null when the registry has neither — there is nothing to replace.
63
+ * The index table: one row per record in timeline order, each with its live
64
+ * status. Printed by `launchrail adr index`, never committed — a table every
65
+ * ADR-adding branch rewrites is a table parallel branches conflict over
66
+ * (2026-10-01-adr-index-is-printed-not-committed).
80
67
  */
81
- export declare function replaceIndexRegion(registrySource: string, replacement: string): string | null;
68
+ export declare function adrIndexTable(entries: AdrEntry[]): string;
69
+ /** Printed under the index when some record carries no `## Status` yet (an adopted corpus). */
70
+ export declare const ADR_UNCLASSIFIED_NOTE = "Rows marked **Unclassified** carry no `## Status` section yet. Classify each as you next touch its area: give the record a status line (`Accepted`, `Superseded by [id](file)`, \u2026).";
71
+ /** What the registry's `## Index` section says in place of a committed table. */
72
+ export declare const ADR_INDEX_POINTER = "The index is generated from the records each time it is read, and never committed \u2014 so branches that add ADRs in parallel have no shared table to conflict over. Print it with `npx @wemuda/launchrail adr index`: every record in date order, with its live status (superseded, amended, extended) derived from the records' own `## Status` lines.";
82
73
  /**
83
- * The registry with its index table regenerated in place. Between the markers
84
- * when they exist; otherwise the first table under `## Index` is replaced and
85
- * the markers introduced around it. Null when the registry has neither.
74
+ * The registry with the generated table an older Launchrail committed between
75
+ * the markers replaced by {@link ADR_INDEX_POINTER}. Only the marked region is
76
+ * touched — those rows were always Launchrail's, regenerated wholesale on every
77
+ * `adr index` — so a hand-kept table without markers, and every other line,
78
+ * stays the project's. Null when there is no marked table.
86
79
  */
87
- export declare function withRegeneratedIndex(registrySource: string, entries: AdrEntry[]): string | null;
80
+ export declare function withoutCommittedIndex(registrySource: string): string | null;
88
81
  /** The seeded ADR template: date-and-slug titled, teaching the forward-declared relation model. */
89
- export declare const ADR_TEMPLATE = "# Short decision title\n\n## Status\nProposed | Accepted | Superseded by [slug](YYYY-MM-DD-slug.md)\n\nName here what this record supersedes, amends, or extends, linking the earlier record by file (`Accepted \u2014 amends [slug](YYYY-MM-DD-slug.md): what changed`). The registry index ([README.md](README.md)) derives the reverse links, so an earlier record need not be edited when this one amends it; when this record is superseded later, this line is rewritten to name the successor. Re-run `launchrail adr index` after any change here.\n\n## Context\nWhat requirement or constraint requires a decision?\n\n## Decision\nWhat was selected?\n\n## Alternatives considered\nWhat realistic alternatives were rejected?\n\n## Consequences\nWhat becomes easier, harder, or constrained?\n\n## Revisit when\nWhat change would justify reconsidering this decision?\n";
82
+ export declare const ADR_TEMPLATE = "# Short decision title\n\n## Status\nProposed | Accepted | Superseded by [slug](YYYY-MM-DD-slug.md)\n\nName here what this record supersedes, amends, or extends, linking the earlier record by file (`Accepted \u2014 amends [slug](YYYY-MM-DD-slug.md): what changed`). The registry index (`launchrail adr index`) derives the reverse links, so an earlier record need not be edited when this one amends it; when this record is superseded later, this line is rewritten to name the successor.\n\n## Context\nWhat requirement or constraint requires a decision?\n\n## Decision\nWhat was selected?\n\n## Alternatives considered\nWhat realistic alternatives were rejected?\n\n## Consequences\nWhat becomes easier, harder, or constrained?\n\n## Revisit when\nWhat change would justify reconsidering this decision?\n";
83
+ /** The template seeded while the index was committed — it pointed at the registry file and asked for a re-run (byte-stable until the printed index). */
84
+ export declare const COMMITTED_INDEX_ADR_TEMPLATE = "# Short decision title\n\n## Status\nProposed | Accepted | Superseded by [slug](YYYY-MM-DD-slug.md)\n\nName here what this record supersedes, amends, or extends, linking the earlier record by file (`Accepted \u2014 amends [slug](YYYY-MM-DD-slug.md): what changed`). The registry index ([README.md](README.md)) derives the reverse links, so an earlier record need not be edited when this one amends it; when this record is superseded later, this line is rewritten to name the successor. Re-run `launchrail adr index` after any change here.\n\n## Context\nWhat requirement or constraint requires a decision?\n\n## Decision\nWhat was selected?\n\n## Alternatives considered\nWhat realistic alternatives were rejected?\n\n## Consequences\nWhat becomes easier, harder, or constrained?\n\n## Revisit when\nWhat change would justify reconsidering this decision?\n";
90
85
  /** The `# ADR-NNNN:`-titled template seeded before the dated-identifier ADR (byte-stable until then). */
91
86
  export declare const PRE_DATE_SLUG_ADR_TEMPLATE = "# ADR-NNNN: Short decision title\n\n## Status\nProposed | Accepted | Accepted \u2014 amended by ADR-NNNN | Superseded by ADR-NNNN\n\nWhen a later ADR amends or supersedes this one, update this line and the registry index ([README.md](README.md)) in the same commit.\n\n## Context\nWhat requirement or constraint requires a decision?\n\n## Decision\nWhat was selected?\n\n## Alternatives considered\nWhat realistic alternatives were rejected?\n\n## Consequences\nWhat becomes easier, harder, or constrained?\n\n## Revisit when\nWhat change would justify reconsidering this decision?\n";
87
+ /**
88
+ * The registry bullets Launchrail seeded about the committed index table, byte
89
+ * for byte: the dated-identifier version, then the merge-driver version. Each
90
+ * tells agents to regenerate and commit a table that no longer exists, so the
91
+ * printed-index migration swaps exactly these lines for the current bullet.
92
+ */
93
+ export declare const COMMITTED_INDEX_BULLETS: string[];
94
+ /** The current registry bullet about the index — the one the committed-index bullets heal to. */
95
+ export declare const PRINTED_INDEX_BULLET = "- The index is **printed, never committed**: `launchrail adr index` builds it from the records each time it runs, so adding or re-statusing a record touches only that record \u2014 there is no table to regenerate, commit, or merge.";
92
96
  /** The registry's project-owned summary of how records are minted; defers to the managed contract. */
93
- export declare const ADR_MAINTAINING_SECTION = "## Maintaining this registry\n\n- New ADRs copy [0000-template.md](0000-template.md) to `YYYY-MM-DD-short-slug.md` \u2014 the date the decision was made, then a slug unique in this directory. There is no sequence number to claim, so parallel branches never collide, and nothing is renumbered.\n- The index table between the markers is **generated**: run `launchrail adr index` after adding or re-statusing a record, and commit the result. Never hand-edit the rows; the rest of this file is yours. Parallel branches do not collide on it \u2014 Launchrail installs a git merge driver (`launchrail doctor` registers it in each clone) that rebuilds the table from the records on merge or rebase, so both branches' new rows land in date order with no conflict. If you ever do see a conflict here \u2014 a fresh clone before `launchrail doctor` ran, say \u2014 never resolve it by editing rows: run `launchrail adr index && git add docs/adr/README.md` and continue the merge or rebase.\n- A new ADR declares what it supersedes, amends, or extends in its own `## Status` line, linking the earlier record by file. The index derives the reverse links, so amending an ADR does not require editing it. A superseded ADR's `## Status` line is still rewritten to name its successor \u2014 that is the one fact a reader of the record alone must not miss.\n- Never delete or rename an ADR once it is referenced; superseded ADRs are historical records other documents link to.\n- The naming and relation mechanics above summarize a contract Launchrail keeps current in the managed workflow instructions (`.launchrail/CLAUDE.generated.md`); if this seeded summary ever drifts from that managed contract, the managed contract is what holds.";
97
+ export declare const ADR_MAINTAINING_SECTION = "## Maintaining this registry\n\n- New ADRs copy [0000-template.md](0000-template.md) to `YYYY-MM-DD-short-slug.md` \u2014 the date the decision was made, then a slug unique in this directory. There is no sequence number to claim, so parallel branches never collide, and nothing is renumbered.\n- The index is **printed, never committed**: `launchrail adr index` builds it from the records each time it runs, so adding or re-statusing a record touches only that record \u2014 there is no table to regenerate, commit, or merge.\n- A new ADR declares what it supersedes, amends, or extends in its own `## Status` line, linking the earlier record by file. The index derives the reverse links, so amending an ADR does not require editing it. A superseded ADR's `## Status` line is still rewritten to name its successor \u2014 that is the one fact a reader of the record alone must not miss.\n- Never delete or rename an ADR once it is referenced; superseded ADRs are historical records other documents link to.\n- The naming and relation mechanics above summarize a contract Launchrail keeps current in the managed workflow instructions (`.launchrail/CLAUDE.generated.md`); if this seeded summary ever drifts from that managed contract, the managed contract is what holds.";
94
98
  /**
95
99
  * The "Maintaining this registry" section seeded before the dated-identifier ADR —
96
100
  * the one that told agents to "take the next free number". Byte-stable from
97
101
  * ADR-0031 until that ADR, so `healRegistryMinting` matches it exactly.
98
102
  */
99
103
  export declare const PRE_DATE_SLUG_MAINTAINING_SECTION = "## Maintaining this registry\n\n- New ADRs copy [0000-template.md](0000-template.md), take the next free number (`NNNN-short-slug.md` \u2014 check both this index and the files on disk), and add their row here **in the same commit**. The shared row turns two branches minting the same number into a visible merge conflict instead of a silent collision.\n- When a new ADR supersedes or amends an earlier one \u2014 including reversing part of the earlier one's context \u2014 update the earlier ADR's `## Status` line and its row here in the same commit.\n- Never delete or renumber an ADR once it is referenced; superseded ADRs are historical records other documents link to.";
104
+ /**
105
+ * The registry with each seeded committed-index bullet swapped for the printed-
106
+ * index one. Only exact lines Launchrail wrote are touched; a project's own
107
+ * wording stays. Null when there is nothing to heal.
108
+ */
109
+ export declare function healCommittedIndexBullet(source: string): string | null;
100
110
  export type MintingGuidanceState = "healed" | "current" | "modified" | "absent";
101
111
  /**
102
112
  * Correct a registry's minting guidance in place. Returns `healed` (with the
@@ -111,11 +121,23 @@ export declare function healRegistryMinting(source: string): {
111
121
  next: string;
112
122
  };
113
123
  /**
114
- * The seeded ADR registry. When init/sync adopts a repository that already has
115
- * decision records, the index prefills from them; a fresh repository gets the
116
- * empty index. Seeded once, then project-owned — but the table between the
117
- * markers is regenerated by `launchrail adr index`, so the project maintains
118
- * the records and the live picture, never the rows.
124
+ * The seeded ADR registry: the reading doctrine, a pointer to the printed index,
125
+ * a live picture the project writes, and the minting summary. Seeded once, then
126
+ * project-owned. It carries no table — the index is printed from the records on
127
+ * demand — so its content depends on the corpus only for the live-picture prompt.
119
128
  */
120
129
  export declare function adrRegistryContent(entries: AdrEntry[]): string;
130
+ export declare const GITATTRIBUTES_FILENAME = ".gitattributes";
131
+ export declare const RETIRED_ADR_MERGE_DRIVER_NAME = "launchrail-adr-index";
132
+ export declare const RETIRED_ADR_MERGE_ATTRIBUTE_LINE = "docs/adr/README.md merge=launchrail-adr-index";
133
+ /**
134
+ * `.gitattributes` without the line Launchrail added: the remaining content, or
135
+ * `""` when that line was all the file held (delete it). Null when the line is
136
+ * absent — a project's own attribute rules are never touched.
137
+ */
138
+ export declare function withoutRetiredMergeAttribute(cwd: string): string | null;
139
+ /** Whether this clone's git config still defines the retired driver. */
140
+ export declare function hasRetiredMergeDriverConfig(cwd: string): boolean;
141
+ /** Drop the retired driver from this clone's git config. Idempotent; returns whether anything was removed. */
142
+ export declare function removeRetiredMergeDriverConfig(cwd: string): boolean;
121
143
  export {};
package/dist/lib/adr.js CHANGED
@@ -1,8 +1,15 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  export const ADR_DIR = "docs/adr";
4
5
  export const ADR_TEMPLATE_FILENAME = "0000-template.md";
5
6
  export const ADR_REGISTRY_PATH = `${ADR_DIR}/README.md`;
7
+ /**
8
+ * The markers that once bracketed the generated table committed into the
9
+ * registry. The index is printed now, never committed
10
+ * (2026-10-01-adr-index-is-printed-not-committed); the markers survive only so
11
+ * the migration and doctor can find a table an older version left behind.
12
+ */
6
13
  export const ADR_INDEX_START = "<!-- adr-index:start — generated by `launchrail adr index`; edit the records, not this table -->";
7
14
  export const ADR_INDEX_END = "<!-- adr-index:end -->";
8
15
  /** Legacy numbered records: `0031-adr-registry.md`. A dated filename is not a numbered one. */
@@ -46,22 +53,6 @@ export function parseAdrEntry(file, source) {
46
53
  return null;
47
54
  return { ...parsed, file, title: adrTitle(source, file), status: adrStatus(source) };
48
55
  }
49
- /**
50
- * The record list built from an explicit set of file→source pairs, sorted by
51
- * filename exactly as scanAdrs sorts the directory — numbered records first,
52
- * dated records after them in date order. The merge driver uses this to
53
- * assemble the corpus from the working tree plus the records only the incoming
54
- * side carries, which are not yet on disk when git invokes the driver.
55
- */
56
- export function entriesFromSources(sources) {
57
- const entries = [];
58
- for (const [file, source] of [...sources].sort(([a], [b]) => a.localeCompare(b))) {
59
- const entry = parseAdrEntry(file, source);
60
- if (entry)
61
- entries.push(entry);
62
- }
63
- return entries;
64
- }
65
56
  /**
66
57
  * The decision records already in a repository's docs/adr/ (template and
67
58
  * registry excluded), sorted by filename — numbered records first, dated
@@ -85,7 +76,13 @@ export function scanAdrs(cwd) {
85
76
  }
86
77
  sources.push([file, source]);
87
78
  }
88
- return entriesFromSources(sources);
79
+ const entries = [];
80
+ for (const [file, source] of sources.sort(([a], [b]) => a.localeCompare(b))) {
81
+ const entry = parseAdrEntry(file, source);
82
+ if (entry)
83
+ entries.push(entry);
84
+ }
85
+ return entries;
89
86
  }
90
87
  /**
91
88
  * Ids (numbers or slugs) claimed by more than one record, each with the files
@@ -97,10 +94,6 @@ export function adrDuplicates(entries) {
97
94
  byId.set(entry.id, [...(byId.get(entry.id) ?? []), entry.file]);
98
95
  return [...byId.entries()].filter(([, files]) => files.length > 1).map(([id, files]) => ({ id, files }));
99
96
  }
100
- /** Records whose filename never appears in the registry — rows the index is missing. */
101
- export function unindexedAdrs(registrySource, entries) {
102
- return entries.filter((entry) => !registrySource.includes(entry.file));
103
- }
104
97
  const FORWARD = { supersedes: "supersedes", amends: "amends", extends: "extends" };
105
98
  const BACKWARD = {
106
99
  "superseded by": "supersedes",
@@ -174,8 +167,9 @@ function stateOf(status) {
174
167
  return "Unclassified";
175
168
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
176
169
  }
170
+ /** Links resolve from the repository root, where `launchrail adr index` runs. */
177
171
  function idLink(entry) {
178
- return `[${entry.id}](${entry.file})`;
172
+ return `[${entry.id}](${ADR_DIR}/${entry.file})`;
179
173
  }
180
174
  /**
181
175
  * One record's status cell: its state, the relations it declares, and the
@@ -215,51 +209,35 @@ export function adrStatusCell(entry, entries, graph = adrRelations(entries)) {
215
209
  parts.push(`extended by ${list(received.extends)}`);
216
210
  return parts.join("; ");
217
211
  }
218
- /** The generated index table, markers included. */
212
+ /**
213
+ * The index table: one row per record in timeline order, each with its live
214
+ * status. Printed by `launchrail adr index`, never committed — a table every
215
+ * ADR-adding branch rewrites is a table parallel branches conflict over
216
+ * (2026-10-01-adr-index-is-printed-not-committed).
217
+ */
219
218
  export function adrIndexTable(entries) {
220
219
  const graph = adrRelations(entries);
221
220
  const header = "| ADR | Decided | Title | Status |\n| --- | --- | --- | --- |";
222
221
  const rows = entries.map((e) => `| ${idLink(e)} | ${e.date ?? "—"} | ${e.title.replace(/\|/g, "\\|")} | ${adrStatusCell(e, entries, graph)} |`);
223
- return [ADR_INDEX_START, header, ...rows, ADR_INDEX_END].join("\n");
222
+ return [header, ...rows].join("\n");
224
223
  }
224
+ /** Printed under the index when some record carries no `## Status` yet (an adopted corpus). */
225
+ export const ADR_UNCLASSIFIED_NOTE = "Rows marked **Unclassified** carry no `## Status` section yet. Classify each as you next touch its area: give the record a status line (`Accepted`, `Superseded by [id](file)`, …).";
226
+ /** What the registry's `## Index` section says in place of a committed table. */
227
+ export const ADR_INDEX_POINTER = "The index is generated from the records each time it is read, and never committed — so branches that add ADRs in parallel have no shared table to conflict over. Print it with `npx @wemuda/launchrail adr index`: every record in date order, with its live status (superseded, amended, extended) derived from the records' own `## Status` lines.";
225
228
  /**
226
- * A single-line placeholder the merge driver substitutes for the index region
227
- * while it 3-way merges the registry's prose, then swaps back out for the freshly
228
- * regenerated table. Comment-shaped so it is inert if it ever leaks into a file,
229
- * and identical across all three sides so the prose merge sees no index diff.
230
- */
231
- export const ADR_INDEX_SENTINEL = "<!-- launchrail:adr-index-merge-placeholder -->";
232
- /**
233
- * Replace the registry's index region with `replacement`: everything between the
234
- * markers when they exist; otherwise the first table under `## Index`, with the
235
- * region newline-padded (a registry written before the generator migrates on its
236
- * first run). Null when the registry has neither — there is nothing to replace.
229
+ * The registry with the generated table an older Launchrail committed between
230
+ * the markers replaced by {@link ADR_INDEX_POINTER}. Only the marked region is
231
+ * touched — those rows were always Launchrail's, regenerated wholesale on every
232
+ * `adr index` — so a hand-kept table without markers, and every other line,
233
+ * stays the project's. Null when there is no marked table.
237
234
  */
238
- export function replaceIndexRegion(registrySource, replacement) {
235
+ export function withoutCommittedIndex(registrySource) {
239
236
  const start = registrySource.indexOf("<!-- adr-index:start");
240
237
  const end = registrySource.indexOf(ADR_INDEX_END);
241
- if (start !== -1 && end !== -1 && end > start) {
242
- return registrySource.slice(0, start) + replacement + registrySource.slice(end + ADR_INDEX_END.length);
243
- }
244
- const heading = /^##\s+Index\s*$/m.exec(registrySource);
245
- if (!heading)
238
+ if (start === -1 || end === -1 || end < start)
246
239
  return null;
247
- const afterHeading = heading.index + heading[0].length;
248
- const rest = registrySource.slice(afterHeading);
249
- const tableMatch = /\n(\|[^\n]*\n)+/.exec(rest);
250
- if (!tableMatch)
251
- return null;
252
- const before = registrySource.slice(0, afterHeading + tableMatch.index);
253
- const after = rest.slice(tableMatch.index + tableMatch[0].length);
254
- return `${before}\n${replacement}\n${after}`;
255
- }
256
- /**
257
- * The registry with its index table regenerated in place. Between the markers
258
- * when they exist; otherwise the first table under `## Index` is replaced and
259
- * the markers introduced around it. Null when the registry has neither.
260
- */
261
- export function withRegeneratedIndex(registrySource, entries) {
262
- return replaceIndexRegion(registrySource, adrIndexTable(entries));
240
+ return registrySource.slice(0, start) + ADR_INDEX_POINTER + registrySource.slice(end + ADR_INDEX_END.length);
263
241
  }
264
242
  // --- Minting guidance: a managed contract, a seeded summary, a one-time heal ----
265
243
  //
@@ -277,6 +255,29 @@ export const ADR_TEMPLATE = `# Short decision title
277
255
  ## Status
278
256
  Proposed | Accepted | Superseded by [slug](YYYY-MM-DD-slug.md)
279
257
 
258
+ Name here what this record supersedes, amends, or extends, linking the earlier record by file (\`Accepted — amends [slug](YYYY-MM-DD-slug.md): what changed\`). The registry index (\`launchrail adr index\`) derives the reverse links, so an earlier record need not be edited when this one amends it; when this record is superseded later, this line is rewritten to name the successor.
259
+
260
+ ## Context
261
+ What requirement or constraint requires a decision?
262
+
263
+ ## Decision
264
+ What was selected?
265
+
266
+ ## Alternatives considered
267
+ What realistic alternatives were rejected?
268
+
269
+ ## Consequences
270
+ What becomes easier, harder, or constrained?
271
+
272
+ ## Revisit when
273
+ What change would justify reconsidering this decision?
274
+ `;
275
+ /** The template seeded while the index was committed — it pointed at the registry file and asked for a re-run (byte-stable until the printed index). */
276
+ export const COMMITTED_INDEX_ADR_TEMPLATE = `# Short decision title
277
+
278
+ ## Status
279
+ Proposed | Accepted | Superseded by [slug](YYYY-MM-DD-slug.md)
280
+
280
281
  Name here what this record supersedes, amends, or extends, linking the earlier record by file (\`Accepted — amends [slug](YYYY-MM-DD-slug.md): what changed\`). The registry index ([README.md](README.md)) derives the reverse links, so an earlier record need not be edited when this one amends it; when this record is superseded later, this line is rewritten to name the successor. Re-run \`launchrail adr index\` after any change here.
281
282
 
282
283
  ## Context
@@ -317,11 +318,23 @@ What becomes easier, harder, or constrained?
317
318
  ## Revisit when
318
319
  What change would justify reconsidering this decision?
319
320
  `;
321
+ /**
322
+ * The registry bullets Launchrail seeded about the committed index table, byte
323
+ * for byte: the dated-identifier version, then the merge-driver version. Each
324
+ * tells agents to regenerate and commit a table that no longer exists, so the
325
+ * printed-index migration swaps exactly these lines for the current bullet.
326
+ */
327
+ export const COMMITTED_INDEX_BULLETS = [
328
+ "- The index table between the markers is **generated**: run `launchrail adr index` after adding or re-statusing a record (and after merging), and commit the result. Never hand-edit the rows; the rest of this file is yours.",
329
+ "- The index table between the markers is **generated**: run `launchrail adr index` after adding or re-statusing a record, and commit the result. Never hand-edit the rows; the rest of this file is yours. Parallel branches do not collide on it — Launchrail installs a git merge driver (`launchrail doctor` registers it in each clone) that rebuilds the table from the records on merge or rebase, so both branches' new rows land in date order with no conflict. If you ever do see a conflict here — a fresh clone before `launchrail doctor` ran, say — never resolve it by editing rows: run `launchrail adr index && git add docs/adr/README.md` and continue the merge or rebase.",
330
+ ];
331
+ /** The current registry bullet about the index — the one the committed-index bullets heal to. */
332
+ export const PRINTED_INDEX_BULLET = "- The index is **printed, never committed**: `launchrail adr index` builds it from the records each time it runs, so adding or re-statusing a record touches only that record — there is no table to regenerate, commit, or merge.";
320
333
  /** The registry's project-owned summary of how records are minted; defers to the managed contract. */
321
334
  export const ADR_MAINTAINING_SECTION = `## Maintaining this registry
322
335
 
323
336
  - New ADRs copy [0000-template.md](0000-template.md) to \`YYYY-MM-DD-short-slug.md\` — the date the decision was made, then a slug unique in this directory. There is no sequence number to claim, so parallel branches never collide, and nothing is renumbered.
324
- - The index table between the markers is **generated**: run \`launchrail adr index\` after adding or re-statusing a record, and commit the result. Never hand-edit the rows; the rest of this file is yours. Parallel branches do not collide on it — Launchrail installs a git merge driver (\`launchrail doctor\` registers it in each clone) that rebuilds the table from the records on merge or rebase, so both branches' new rows land in date order with no conflict. If you ever do see a conflict here — a fresh clone before \`launchrail doctor\` ran, say — never resolve it by editing rows: run \`launchrail adr index && git add docs/adr/README.md\` and continue the merge or rebase.
337
+ ${PRINTED_INDEX_BULLET}
325
338
  - A new ADR declares what it supersedes, amends, or extends in its own \`## Status\` line, linking the earlier record by file. The index derives the reverse links, so amending an ADR does not require editing it. A superseded ADR's \`## Status\` line is still rewritten to name its successor — that is the one fact a reader of the record alone must not miss.
326
339
  - Never delete or rename an ADR once it is referenced; superseded ADRs are historical records other documents link to.
327
340
  - The naming and relation mechanics above summarize a contract Launchrail keeps current in the managed workflow instructions (\`.launchrail/CLAUDE.generated.md\`); if this seeded summary ever drifts from that managed contract, the managed contract is what holds.`;
@@ -335,6 +348,22 @@ export const PRE_DATE_SLUG_MAINTAINING_SECTION = `## Maintaining this registry
335
348
  - New ADRs copy [0000-template.md](0000-template.md), take the next free number (\`NNNN-short-slug.md\` — check both this index and the files on disk), and add their row here **in the same commit**. The shared row turns two branches minting the same number into a visible merge conflict instead of a silent collision.
336
349
  - When a new ADR supersedes or amends an earlier one — including reversing part of the earlier one's context — update the earlier ADR's \`## Status\` line and its row here in the same commit.
337
350
  - Never delete or renumber an ADR once it is referenced; superseded ADRs are historical records other documents link to.`;
351
+ /**
352
+ * The registry with each seeded committed-index bullet swapped for the printed-
353
+ * index one. Only exact lines Launchrail wrote are touched; a project's own
354
+ * wording stays. Null when there is nothing to heal.
355
+ */
356
+ export function healCommittedIndexBullet(source) {
357
+ const lines = source.split("\n");
358
+ let healed = false;
359
+ const next = lines.map((line) => {
360
+ if (!COMMITTED_INDEX_BULLETS.includes(line.trimEnd()))
361
+ return line;
362
+ healed = true;
363
+ return PRINTED_INDEX_BULLET;
364
+ });
365
+ return healed ? next.join("\n") : null;
366
+ }
338
367
  const MAINTAINING_HEADING = "## Maintaining this registry";
339
368
  /** Slice a registry into the text before/at/after its "Maintaining this registry" section (to the next `## ` heading or EOF). */
340
369
  function maintainingSlice(source) {
@@ -368,28 +397,24 @@ export function healRegistryMinting(source) {
368
397
  return { state: "modified", next: source };
369
398
  }
370
399
  /**
371
- * The seeded ADR registry. When init/sync adopts a repository that already has
372
- * decision records, the index prefills from them; a fresh repository gets the
373
- * empty index. Seeded once, then project-owned — but the table between the
374
- * markers is regenerated by `launchrail adr index`, so the project maintains
375
- * the records and the live picture, never the rows.
400
+ * The seeded ADR registry: the reading doctrine, a pointer to the printed index,
401
+ * a live picture the project writes, and the minting summary. Seeded once, then
402
+ * project-owned. It carries no table — the index is printed from the records on
403
+ * demand — so its content depends on the corpus only for the live-picture prompt.
376
404
  */
377
405
  export function adrRegistryContent(entries) {
378
- const unclassifiedNote = entries.some((e) => e.status === "")
379
- ? "\n\nRows marked **Unclassified** carry no `## Status` section yet. Classify each as you next touch its area: give the record a status line (`Accepted`, `Superseded by [id](file)`, …) and re-run `launchrail adr index`."
380
- : "";
381
406
  const livePicture = entries.length === 0
382
407
  ? "_No decisions recorded yet. When ADRs land, summarize here how they compose into the current system, and name the few a newcomer should read first._"
383
408
  : "_Not yet written. Summarize how the accepted decisions compose into the current system, and name the few ADRs a newcomer should read first._";
384
409
  return `# ADR registry
385
410
 
386
- The index of every architecture decision record in this repository. Read this first, then open only the ADRs that touch the area you are working in — the index is the cheap surface; the records are depth.
411
+ Every architecture decision record in this repository lives in this directory. Start from the index, then open only the ADRs that touch the area you are working in — the index is the cheap surface; the records are depth.
387
412
 
388
413
  An ADR records a decision and the context it was made in. It is **not documentation of the current system**: never treat an ADR as evidence that a component exists or still works as described — the code is the source of truth for what exists today.
389
414
 
390
415
  ## Index
391
416
 
392
- ${adrIndexTable(entries)}${unclassifiedNote}
417
+ ${ADR_INDEX_POINTER}
393
418
 
394
419
  ## The live picture
395
420
 
@@ -400,4 +425,58 @@ ${livePicture}
400
425
  ${ADR_MAINTAINING_SECTION}
401
426
  `;
402
427
  }
428
+ // --- Retired: the index merge driver -------------------------------------------
429
+ //
430
+ // While the index was committed, Launchrail bound it to a git merge driver: one
431
+ // line in the project's `.gitattributes`, plus a per-clone git config entry
432
+ // (2026-09-21-adr-index-merge-driver). With nothing generated left to merge, the
433
+ // printed-index migration removes the line Launchrail added, and sync / doctor
434
+ // drop the clone's config so a stale binding can never invoke a removed command.
435
+ export const GITATTRIBUTES_FILENAME = ".gitattributes";
436
+ export const RETIRED_ADR_MERGE_DRIVER_NAME = "launchrail-adr-index";
437
+ export const RETIRED_ADR_MERGE_ATTRIBUTE_LINE = `${ADR_REGISTRY_PATH} merge=${RETIRED_ADR_MERGE_DRIVER_NAME}`;
438
+ /**
439
+ * `.gitattributes` without the line Launchrail added: the remaining content, or
440
+ * `""` when that line was all the file held (delete it). Null when the line is
441
+ * absent — a project's own attribute rules are never touched.
442
+ */
443
+ export function withoutRetiredMergeAttribute(cwd) {
444
+ const path = join(cwd, GITATTRIBUTES_FILENAME);
445
+ if (!existsSync(path))
446
+ return null;
447
+ const lines = readFileSync(path, "utf8").split("\n");
448
+ const kept = lines.filter((line) => line.trim() !== RETIRED_ADR_MERGE_ATTRIBUTE_LINE);
449
+ if (kept.length === lines.length)
450
+ return null;
451
+ const rest = kept.join("\n");
452
+ return rest.trim() === "" ? "" : rest;
453
+ }
454
+ /** Whether this clone's git config still defines the retired driver. */
455
+ export function hasRetiredMergeDriverConfig(cwd) {
456
+ try {
457
+ execFileSync("git", ["config", "--local", "--get-regexp", `^merge\\.${RETIRED_ADR_MERGE_DRIVER_NAME}\\.`], {
458
+ cwd,
459
+ stdio: "ignore",
460
+ });
461
+ return true;
462
+ }
463
+ catch {
464
+ return false;
465
+ }
466
+ }
467
+ /** Drop the retired driver from this clone's git config. Idempotent; returns whether anything was removed. */
468
+ export function removeRetiredMergeDriverConfig(cwd) {
469
+ if (!hasRetiredMergeDriverConfig(cwd))
470
+ return false;
471
+ try {
472
+ execFileSync("git", ["config", "--local", "--remove-section", `merge.${RETIRED_ADR_MERGE_DRIVER_NAME}`], {
473
+ cwd,
474
+ stdio: "ignore",
475
+ });
476
+ return true;
477
+ }
478
+ catch {
479
+ return false;
480
+ }
481
+ }
403
482
  //# sourceMappingURL=adr.js.map