@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/README.md +2 -2
- package/assets/agents-docs/domain.md +2 -2
- package/assets/ralph.workflow.js +1 -1
- package/assets/skills/launchrail/launch-design-validation/SKILL.md +1 -1
- package/assets/skills/launchrail/launch-grill/domain-modeling.md +1 -1
- package/assets/skills/launchrail/launch-spec/SKILL.md +1 -1
- package/assets/skills/launchrail/launch-tickets/SKILL.md +1 -1
- package/dist/commands/adr.d.ts +8 -37
- package/dist/commands/adr.js +12 -98
- package/dist/commands/adr.js.map +1 -1
- package/dist/commands/doctor.js +15 -38
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +0 -15
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/sync.js +9 -7
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.js +16 -13
- package/dist/index.js.map +1 -1
- package/dist/lib/adr.d.ts +57 -35
- package/dist/lib/adr.js +147 -68
- package/dist/lib/adr.js.map +1 -1
- package/dist/lib/migrations.js +72 -19
- package/dist/lib/migrations.js.map +1 -1
- package/dist/lib/seeds.js +4 -4
- package/package.json +1 -1
- package/dist/lib/adrMergeDriver.d.ts +0 -73
- package/dist/lib/adrMergeDriver.js +0 -223
- package/dist/lib/adrMergeDriver.js.map +0 -1
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
|
|
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
|
|
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
|
|
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()
|
|
139
|
+
process.exit(runAdrIndex({ cwd: process.cwd() }).code);
|
|
140
140
|
}
|
|
141
141
|
if (args[1] === "merge-driver") {
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
|
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":";AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
|
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
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 [
|
|
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
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|