@nikovirtala/adr 0.2.0 → 0.3.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 +30 -9
- package/dist/cli.js +195 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{kind-BcHjCHvi.js → kind-DjvA3dyA.js} +2 -6
- package/dist/{kind-BcHjCHvi.js.map → kind-DjvA3dyA.js.map} +1 -1
- package/dist/kind.js +1 -1
- package/dist/{write-9IaqHRrY.js → scope-CLqH-q1y.js} +1439 -552
- package/dist/scope-CLqH-q1y.js.map +1 -0
- package/dist/types/cli/hooks.d.ts +6 -0
- package/dist/types/cli/index-command.d.ts +3 -0
- package/dist/types/cli/list.d.ts +1 -1
- package/dist/types/cli/open.d.ts +2 -0
- package/dist/types/cli/text.d.ts +6 -0
- package/dist/types/commit-actions.d.ts +72 -0
- package/dist/types/config.d.ts +7 -1
- package/dist/types/hooks.d.ts +59 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/init.d.ts +7 -1
- package/dist/types/log-index.d.ts +117 -0
- package/dist/types/log.d.ts +34 -1
- package/dist/types/verify.d.ts +6 -4
- package/dist/types/views.d.ts +7 -3
- package/dist/types/write.d.ts +28 -10
- package/package.json +2 -2
- package/dist/write-9IaqHRrY.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Architecture decision records kept as event logs in git, built on [`@nikovirtala/ledger`](https://www.npmjs.com/package/@nikovirtala/ledger). A decision is a Markdown body plus append-only event files. Its status (proposed, accepted, rejected, withdrawn, deprecated, superseded) is computed from the events, the body is frozen by its hash once accepted, and supersession chains always lead to the decision in force. See the [design doc](https://github.com/nikovirtala/ark/blob/main/docs/01-adr-v1.md).
|
|
4
4
|
|
|
5
|
-
Released at 0.x. Phase 1 ("Core") brought the decision kind, the library and the `adr` CLI with `new`, `accept`, `supersede`, `list`, `show` and `verify`. Phase 2a ("Commands and Scope") adds `init`, `reject`, `withdraw`, `deprecate`, `note`, `link`, `scope` and `for`, `accept --copy-scope`, Conventional Commits messages for `--commit`, and `ARK_ON_BEHALF_OF` for agents.
|
|
5
|
+
Released at 0.x. Phase 1 ("Core") brought the decision kind, the library and the `adr` CLI with `new`, `accept`, `supersede`, `list`, `show` and `verify`. Phase 2a ("Commands and Scope") adds `init`, `reject`, `withdraw`, `deprecate`, `note`, `link`, `scope` and `for`, `accept --copy-scope`, Conventional Commits messages for `--commit`, and `ARK_ON_BEHALF_OF` for agents. Phase 2b ("Guardrails") adds git hooks (`adr hooks`), a generated `index.md` that every write keeps fresh (`adr index`), display numbers in shallow clones read from that index, `list --since`, a recovery hint when git refuses a `--commit`, and verify checks for scope globs, code anchors, changes under inactive decisions and narrowed scopes.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -18,6 +18,7 @@ adr needs Node 24 or newer (`engines: >=24`). On older Node, npm prints an `EBAD
|
|
|
18
18
|
```
|
|
19
19
|
docs/adr/
|
|
20
20
|
adr.toml # optional config (or adr.toml at the repository root)
|
|
21
|
+
index.md # generated, with [log] index = "committed"
|
|
21
22
|
decisions/k3f9q8a1-use-aurora-for-orders.md
|
|
22
23
|
events/k3f9q8a1/20261016T081030Z-p4d2-proposed.yaml
|
|
23
24
|
events/k3f9q8a1/20261016T091512Z-7aq5-accepted.yaml
|
|
@@ -62,6 +63,7 @@ adr for services/orders/api.ts --ref v1.2.0
|
|
|
62
63
|
|
|
63
64
|
# Read: list with computed status, show one decision with its state header.
|
|
64
65
|
adr list --status accepted
|
|
66
|
+
adr list --since ORIG_HEAD # only decisions with events added since a revision
|
|
65
67
|
adr show ADR-0001 --history
|
|
66
68
|
adr list --ref v1.2.0 # time travel: the log as of any ref
|
|
67
69
|
|
|
@@ -69,15 +71,28 @@ adr list --ref v1.2.0 # time travel: the log as of any ref
|
|
|
69
71
|
adr verify
|
|
70
72
|
adr verify --base origin/main --format github
|
|
71
73
|
adr verify --staged
|
|
74
|
+
|
|
75
|
+
# The generated index (with [log] index = "committed"): regenerate it, or check that it is current.
|
|
76
|
+
adr index
|
|
77
|
+
adr index --check
|
|
78
|
+
|
|
79
|
+
# Git hooks: install adr in ledger's shared hook shims, or print the config for a hook manager.
|
|
80
|
+
adr hooks install
|
|
81
|
+
adr hooks print --manager lefthook
|
|
72
82
|
```
|
|
73
83
|
|
|
74
84
|
- `new` takes `--template <name>` (default `nygard`; built-ins `nygard`, `madr`, `y-statement`, or your own `<root>/templates/<name>.md`), `--section <id>=<markdown>` (repeatable), `--supersedes <decision>` (repeatable), and `--edit`/`--no-edit`. It opens `$VISUAL` or `$EDITOR` with `--edit`, or by default in an interactive terminal; an editor failure never loses your edits and never commits content you didn't confirm.
|
|
75
85
|
- `new --no-edit --commit` (or `--commit` with no editor) refuses a draft with unfilled required sections: exit 1, nothing written. Without `--commit` the draft is staged with a warning on stderr; after an editor, a draft that still has unfilled sections stays staged and isn't committed.
|
|
76
86
|
- `accept` of a decision that supersedes others offers their scope globs: `--copy-scope` copies them in the same write, `--no-copy-scope` doesn't, and without either it asks `[y/N]` in a terminal and otherwise prints the `adr scope … --add …` command that would copy them.
|
|
77
|
-
- `scope` takes `--set <glob>` (repeatable; replaces the set), `--add`/`--remove <glob>` (repeatable) or `--clear`, and stores the set sorted and deduplicated. It notes a glob that matches no tracked file and a narrowing of an accepted decision. `scope --follow-renames [<decision>]` updates the globs of every decision (or one) for the staged renames, so a moved file stays governed, and only stages.
|
|
87
|
+
- `scope` takes `--set <glob>` (repeatable; replaces the set), `--add`/`--remove <glob>` (repeatable) or `--clear`, and stores the set sorted and deduplicated. It notes a glob that matches no tracked file and a narrowing of an accepted decision. verify requires the globs of proposed and accepted decisions to match tracked files (`scope/glob-unmatched`), so with hooks installed `adr scope <proposed> --set 'src/payments/**' --commit` for code not written yet writes and stages the scope, then fails in pre-commit with the recovery hint below, and its note says so. To finish it, commit the code the glob should match first (`git add` it, then `git commit -m "<subject>" -- <its paths>`) and then run the printed command, or `git add` the code and append its paths after `--` in the printed command. `scope --follow-renames [<decision>]` updates the globs of every decision (or one) for the staged renames, so a moved file stays governed, and only stages.
|
|
78
88
|
- `for <path>…` lists the decisions governing each file, from scope globs and code anchors, with a warning for decisions that aren't in force. A code anchor is a comment such as `// adr:<id>` or `# adr:<source>:<id>`: the token counts only after a comment leader on the same line (a heuristic), and anchors in binary files, submodules, the log directory and files matched by `[scope] anchor_exclude` are not found.
|
|
79
89
|
- Writes stage their files and don't commit, unless you pass `--commit` or set `auto_commit`. A commit holds every file of the decisions it touches (body, events, template), so `adr verify --ref <commit>` is clean, and never your unrelated staged work. It refuses with exit 2 when a file it needs has unstaged changes or isn't tracked. Its `ADR: <id>` and `ADR-Event: <type>` trailers name one event each, in a fixed order, whatever your `trailer.*` config.
|
|
80
90
|
- `--commit` messages follow Conventional Commits: the subject comes from `[commit] subject` (default `docs(adr): {action} {title}`, for example `docs(adr): propose to use Aurora for orders`, `docs(adr): decide to use Aurora for orders`, `docs(adr): set the scope of ADR-0003`). The description starts with a lowercase letter and the rest keeps its natural casing (`Use` becomes `use`; `CLI`, `ARK_AGENT`, `GraphQL` and `Node 24` stay). `new`, `accept`, `reject` and `withdraw` name the decision by its title (`propose to`, `decide to`, `decide not to`, `withdraw the proposal to`), while `deprecate`, `supersede`, `note`, `link` and `scope` name it by its number or id. A title that would make the description longer than 72 characters is shortened with `…`, at a word boundary when one is near the limit, the body then starts with `Full title: <title>`, and a note says so; numbers and ids are never shortened. `-m/--message <text>` replaces the subject and body as given; a message that ends in its own trailer block as git reads it, such as `Co-authored-by:` or `Refs:` lines, gets adr's trailers appended to that paragraph. The trailers are always the last paragraph, so `git log --grep "ADR: <id>"` finds a decision's commits.
|
|
91
|
+
- When git refuses a `--commit` (a failing hook, signing), the write stays written and staged, and adr exits 2 (`commit-failed`) with a recovery hint: git's error, the staged paths, the trailers, and the exact command that makes the commit, `git -C <top> commit --cleanup=whitespace -F <git dir>/ark/COMMIT_MSG --only -- <paths>`, with the full message saved in that file (edit it if needed, but keep the trailers). Don't run the adr command again: the log already holds the change, so a rerun would be refused, do nothing, or write it a second time. When `index.md` is among the paths, the hint ends with `Then run adr index --no-commit to restore other decisions' entries in docs/adr/index.md.` The notes the write would have reported are printed as warnings before the hint.
|
|
92
|
+
- With `[log] index = "committed"`, every write keeps `<root>/index.md` current: the accepted and proposed decisions, then the superseded, deprecated, rejected and withdrawn ones, each with its number, linked title, status and date, and a last line recording the numbers. A `--commit` renders it from exactly the tree the commit holds and includes it whenever the commit changes it; the work-tree copy keeps other decisions' uncommitted entries. Numbers are projected: on a branch they are the numbers its acceptances will get once it merges, after main's (`[log] main_branch`), also when main was merged into the branch. When a write leaves the staged `index.md` listing another decision's untracked or unstaged files, it notes them, since the next plain commit's hook reports `index/stale` until they are staged. `adr index` regenerates and stages it, `adr index --commit` commits it as `docs(adr): regenerate the decision index`, and `adr index --check` exits 1 when it is stale or missing.
|
|
93
|
+
- `list --since <rev>` lists only the decisions with event files added between `<rev>` and HEAD (or `--ref`), each with the new event types, such as `[new: accepted]`; it prints nothing when the log didn't change.
|
|
94
|
+
- In a shallow clone, numbers need history the clone lacks. With a committed index, adr reads them from `index.md` in the commit being read (`--ref`, else HEAD) and notes `numbers come from docs/adr/index.md (shallow clone)`; without one, decisions display by id until you run `git fetch --unshallow`. Nothing is fetched automatically.
|
|
95
|
+
- `hooks install` registers adr in ledger's shims, one per hook and shared with other ark tools: `pre-commit` runs `adr hooks run pre-commit`, `commit-msg` runs `adr hooks trailers` (adds `ADR:` and `ADR-Event:` trailers when event files are staged, unless the message has them or a merge is in progress), `pre-push` runs `adr verify --base @{upstream} --ref HEAD`, which checks the current branch's commits against its upstream whichever refs the push names (without an upstream, against `origin`'s default branch; with no remote, the whole log), and `post-merge` runs `adr list --since ORIG_HEAD`. It never overwrites a foreign hook (it writes the other hooks, exits 2 and names the line to add to that hook) and writes nothing when `core.hooksPath` is set (exit 2, pointing to `adr hooks print --manager lefthook|husky|pre-commit` for the hook manager in use). `hooks uninstall` removes adr from the shims. The pre-commit steps are rename following (`[hooks] scope_follow_renames`), the index (checked, or regenerated and staged with `[hooks] index = "stage"`), then `verify --staged`. The hook stages only into the commit's own index: for `git commit <paths>`, a rename that changes a decision's scope stops the commit and names the command to run. When the staged `index.md` lists decision files that aren't staged, the `index/stale` finding names them; stage them, or move them out of the work tree and run `adr index`.
|
|
81
96
|
- `show` leads with a state header. A decision that isn't in force says so and names what to read instead, for example `SUPERSEDED by ADR-0002 (accepted 2026-10-18). Do not apply this decision; read ADR-0002 instead.`
|
|
82
97
|
|
|
83
98
|
Every command takes `--json`, `--root <dir>` (the log directory) and `--help`; reads also take `--ref <ref>`, and writes refuse it.
|
|
@@ -85,10 +100,10 @@ Every command takes `--json`, `--root <dir>` (the log directory) and `--help`; r
|
|
|
85
100
|
## Exit codes and JSON
|
|
86
101
|
|
|
87
102
|
- **0**: success.
|
|
88
|
-
- **1**: failing findings from `verify`, or a write the log's rules forbid (an illegal transition, a draft that fails its template lint, an agent's acceptance, an unaccepted successor). Nothing is written, and the findings are in the output.
|
|
89
|
-
- **2**: the tool can't run: bad usage (unknown command, flag or decision, a missing argument, `--ref` on a write), config, environment or internal errors. Nothing is written.
|
|
103
|
+
- **1**: failing findings from `verify` (or `index --check`, or a hook), or a write the log's rules forbid (an illegal transition, a draft that fails its template lint, an agent's acceptance, an unaccepted successor). Nothing is written, and the findings are in the output.
|
|
104
|
+
- **2**: the tool can't run: bad usage (unknown command, flag or decision, a missing argument, `--ref` on a write), config, environment or internal errors. Nothing is written, except when git refuses a `--commit` (`commit-failed`): the write is then staged and the error says how to finish the commit.
|
|
90
105
|
|
|
91
|
-
With `--json`, stdout carries exactly one JSON document: `adr-list/v1`, `adr-show/v1`, `adr-write/v1` (for every write command), `adr-for/v1`, `adr-init/v1`, ledger's `ledger-findings/v1` for `verify`, or `ark-error/v1` (`{ "format": "ark-error/v1", "error": { "code", "message" } }`) when the command exits 2
|
|
106
|
+
With `--json`, stdout carries exactly one JSON document: `adr-list/v1` (with `since`, `null` without `--since`), `adr-show/v1`, `adr-write/v1` (for every write command), `adr-for/v1`, `adr-init/v1`, `adr-index/v1`, `adr-hooks/v1`, ledger's `ledger-findings/v1` for `verify`, or `ark-error/v1` (`{ "format": "ark-error/v1", "error": { "code", "message" } }`) when the command exits 2; a `commit-failed` error also has the write's `notes`. Every key is always present, with `null` where a value is unknown. A `numbering` object's `reason` is `"index"` when a shallow clone's numbers come from `index.md`. In a shallow clone with a committed index, `adr-write/v1`'s `decision.number` comes from the index in HEAD, which on a branch is the number the acceptance will get once the branch merges. Warnings and notes go to stderr.
|
|
92
107
|
|
|
93
108
|
## Config (`adr.toml`)
|
|
94
109
|
|
|
@@ -99,7 +114,7 @@ Every key is optional.
|
|
|
99
114
|
root = "docs/adr" # read from <repo>/adr.toml; the default
|
|
100
115
|
default_template = "nygard"
|
|
101
116
|
main_branch = "origin/main" # the ref numbers come from; default origin/HEAD, origin/main, main, …
|
|
102
|
-
index = "none" #
|
|
117
|
+
index = "none" # or "committed": adr index, fresh on every write, checked in verify
|
|
103
118
|
|
|
104
119
|
[policy]
|
|
105
120
|
accept_requires_human = true # refuse acceptances by an agent
|
|
@@ -108,6 +123,7 @@ mcp_can_decide = false # validated now, used in phase 3
|
|
|
108
123
|
|
|
109
124
|
[verify]
|
|
110
125
|
fail_on = "error" # or "warning"
|
|
126
|
+
governed_by_inactive = "warning" # or "error": a change under a superseded or deprecated decision
|
|
111
127
|
|
|
112
128
|
[commit]
|
|
113
129
|
subject = "docs(adr): {action} {title}" # the default; --message overrides it
|
|
@@ -115,8 +131,14 @@ subject = "docs(adr): {action} {title}" # the default; --message overrides it
|
|
|
115
131
|
[scope]
|
|
116
132
|
anchor_keyword = "adr" # the default: anchors read adr:<id>
|
|
117
133
|
anchor_exclude = ["**/*.md"] # files never searched for anchors (docs that show examples); default []
|
|
134
|
+
|
|
135
|
+
[hooks]
|
|
136
|
+
scope_follow_renames = "stage" # stage, warn or off: pre-commit's rename following
|
|
137
|
+
index = "check" # check, or stage to regenerate index.md in pre-commit
|
|
118
138
|
```
|
|
119
139
|
|
|
140
|
+
verify also checks code against the log: `scope/glob-invalid` (error) for a stored glob that doesn't parse, `scope/glob-unmatched` (error) for a glob of a proposed or accepted decision that matches no tracked file, `scope/anchor-missing` (error) for an `adr:<id>` anchor naming no decision in the log (qualified anchors are checked in ark's step 4), `decision/governed-by-inactive` (a warning, or an error with `governed_by_inactive = "error"`) for a changed file governed by a superseded or deprecated decision, and `decision/scope-narrowed` (warning) when a new scope stops covering files that still exist. Deprecated decisions' globs no longer have to match files. With `index = "committed"`, `index/missing` and `index/stale` are errors, and `index/unchecked` warns in a shallow clone without a recorded index.
|
|
141
|
+
|
|
120
142
|
`[commit] subject` is one line with `{title}` (what names the decision: its title with an ordinary capitalized first word lowercased (`Use` → `use`, so a single-capital proper noun such as `Aurora` loses that capital; `CLI`, `GraphQL` and `ARK_AGENT` stay), or its number or id), `{raw_title}` (the same with its case kept), `{action}`, `{id}`, `{display}` and `{command}`. It is checked when the config is read: at most one title placeholder, after the type and scope, and room within 72 characters for the title (20 characters) and, uncut, for the longest number-based name (`ADR-0001 to a decision in another log`); an invalid template exits 2. Whatever the template, the description's first letter is lowercased.
|
|
121
143
|
|
|
122
144
|
The config is `<repo>/adr.toml`, whose `[log] root` chooses the log directory, or `<root>/adr.toml`. `--root` uses a repository-level file that names the same root, and overrides one that names another root with a warning.
|
|
@@ -140,7 +162,7 @@ const { decisions } = listDecisions(read, { status: ["accepted"] });
|
|
|
140
162
|
const findings = await verifyDecisions(log, { type: "full" });
|
|
141
163
|
```
|
|
142
164
|
|
|
143
|
-
`initDecisionLog`, `proposeDecision`, `acceptDecision`, `rejectDecision`, `withdrawDecision`, `deprecateDecision`, `supersedeDecision`, `noteDecision`, `linkDecision`, `scopeDecision` and `followScopeRenames` write, `decisionsFor` answers `adr for`, `resolveDecision` turns an id or number into an id, and `stateHeader` gives the header `show` prints.
|
|
165
|
+
`initDecisionLog`, `proposeDecision`, `acceptDecision`, `rejectDecision`, `withdrawDecision`, `deprecateDecision`, `supersedeDecision`, `noteDecision`, `linkDecision`, `scopeDecision` and `followScopeRenames` write, `decisionsFor` answers `adr for`, `resolveDecision` turns an id or number into an id, and `stateHeader` gives the header `show` prints. `indexDecisionLog` and `renderDecisionIndex` regenerate or render the index, and `installDecisionHooks`, `uninstallDecisionHooks`, `printDecisionHooks`, `runPreCommitHook` and `addHookTrailers` are the hook commands.
|
|
144
166
|
|
|
145
167
|
`@nikovirtala/adr/kind` exports only the decision kind (`adrKind`, made with ledger's `defineKind`) and its types, without any CLI code, for tools that fold decision logs themselves, such as issues linking to decisions:
|
|
146
168
|
|
|
@@ -150,7 +172,6 @@ import { adrKind } from "@nikovirtala/adr/kind";
|
|
|
150
172
|
|
|
151
173
|
## Not yet included
|
|
152
174
|
|
|
153
|
-
- **Reference checks are local.** `verify` checks that `supersedes`, `superseded.by` and `linked.to` name decisions in the same log. Qualified references to other logs (`<source>:<id>`, written by `adr link`) are skipped.
|
|
154
|
-
- **Phase 2b:** git hooks (pre-commit, commit-msg, post-merge, pre-push), a hint for recovering a failed commit, a generated index (and the number fallback for shallow clones), `list --since`, and CI checks for index freshness, scope globs, code anchors, decisions governed by inactive ones and scope narrowing.
|
|
175
|
+
- **Reference checks are local.** `verify` checks that `supersedes`, `superseded.by` and `linked.to` name decisions in the same log. Qualified references to other logs (`<source>:<id>`, written by `adr link`) and qualified anchors are skipped until ark's step 4.
|
|
155
176
|
- **Phase 3:** an MCP server, a SKILL.md for agents, and search.
|
|
156
177
|
- **ark's step 4, with issues:** cross-log reference checks at the pinned ref and listing across sources; until then `link` stores a qualified target unchecked.
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
2
|
+
import { A as openDecisionLog, C as adrChecks, D as runDecisionIndex, N as resolveDecision, O as changedSince, S as withdrawDecision, _ as proposeDecision, a as initDecisionLog, c as installDecisionHooks, d as uninstallDecisionHooks, f as acceptDecision, g as noteDecision, h as linkDecision, j as readDecisions, k as numberingNote, l as printDecisionHooks, m as followScopeRenames, n as listDecisions, o as addHookTrailers, p as deprecateDecision, r as showDecision, t as decisionsFor, u as runPreCommitHook, v as rejectDecision, x as supersedeDecision, y as scopeDecision } from "./scope-CLqH-q1y.js";
|
|
3
|
+
import { n as DECISION_STATUSES, t as adrKind } from "./kind-DjvA3dyA.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { UsageError, renderStateHeader, resolveAttribution, stagedRenames, templateHistoryRef } from "@nikovirtala/ledger";
|
|
5
|
+
import { UsageError, exitCode, findingsReport, formatText, renderStateHeader, resolveAttribution, stagedRenames, templateHistoryRef } from "@nikovirtala/ledger";
|
|
6
|
+
import { resolve } from "node:path";
|
|
6
7
|
import { COMMIT_OPTIONS, commitChoice, confirm, processIO, runCli, verifyCommand } from "@nikovirtala/ledger/cli";
|
|
7
8
|
import { spawn } from "node:child_process";
|
|
8
9
|
// src/cli/open.ts
|
|
@@ -27,6 +28,8 @@ async function displayAfterWrite(log) {
|
|
|
27
28
|
const read = await readDecisions(log);
|
|
28
29
|
return { display: (id) => read.projection.views.get(id)?.display ?? id };
|
|
29
30
|
}
|
|
31
|
+
/** `notes` with `note` appended when there is one. */
|
|
32
|
+
const notesWith = (notes, note) => note === null ? notes : [...notes, note];
|
|
30
33
|
// src/cli/text.ts
|
|
31
34
|
/** A table: each column padded to its widest value, trailing spaces removed. */
|
|
32
35
|
function columns(rows) {
|
|
@@ -39,6 +42,21 @@ function columns(rows) {
|
|
|
39
42
|
/** `adr list`: one row per decision, `→ <effective>` when it differs from the decision. */
|
|
40
43
|
function listText(doc, display) {
|
|
41
44
|
if (doc.decisions.length === 0) return "No decisions.";
|
|
45
|
+
return listRows(doc, display).join("\n");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `adr list --since <rev>` (design 2b D7): a heading, then each row followed by the event types
|
|
49
|
+
* added since `rev`; nothing at all when no decision changed, so the post-merge hook stays quiet.
|
|
50
|
+
*/
|
|
51
|
+
function sinceText(doc, since, display) {
|
|
52
|
+
if (doc.decisions.length === 0) return "";
|
|
53
|
+
const rows = listRows(doc, display);
|
|
54
|
+
return [`Changed since ${since.rev} (${since.commit.slice(0, 7)}):`, ...doc.decisions.flatMap((v, i) => {
|
|
55
|
+
const types = [...new Set(since.events.filter((e) => e.subject === v.id).map((e) => e.type))];
|
|
56
|
+
return [rows[i] ?? "", ` [new: ${types.join(", ")}]`];
|
|
57
|
+
})].join("\n");
|
|
58
|
+
}
|
|
59
|
+
function listRows(doc, display) {
|
|
42
60
|
return columns(doc.decisions.map((v) => {
|
|
43
61
|
const arrow = v.effective === null ? "→ (cycle)" : v.effective === v.id ? "" : `→ ${display(v.effective)}`;
|
|
44
62
|
return [
|
|
@@ -48,7 +66,7 @@ function listText(doc, display) {
|
|
|
48
66
|
v.title,
|
|
49
67
|
arrow
|
|
50
68
|
];
|
|
51
|
-
}));
|
|
69
|
+
})).split("\n");
|
|
52
70
|
}
|
|
53
71
|
/** `ADR-0003 (m2x7p4d2)`, or the id alone when it has no number. */
|
|
54
72
|
function named(id, display) {
|
|
@@ -132,7 +150,7 @@ function unfilledWarning(doc) {
|
|
|
132
150
|
function findingLines(findings) {
|
|
133
151
|
return findings.map((f) => ` ${f.file === null ? "" : `${f.file}: `}${f.message} [${f.rule}]`);
|
|
134
152
|
}
|
|
135
|
-
const short = (sha) => sha.slice(0, 7);
|
|
153
|
+
const short$1 = (sha) => sha.slice(0, 7);
|
|
136
154
|
/**
|
|
137
155
|
* `adr scope --follow-renames`: one `<named>: <old globs> → <new globs>` line per changed
|
|
138
156
|
* decision (`affected`), whether or not a decision was given.
|
|
@@ -178,7 +196,7 @@ function writeText(doc, display, o) {
|
|
|
178
196
|
if (o.followRenames !== void 0) return followRenamesText(doc, display, o.followRenames);
|
|
179
197
|
const v = doc.decision;
|
|
180
198
|
if (v === null) return "";
|
|
181
|
-
const committed = doc.commit === null ? null : `Committed ${short(doc.commit)}`;
|
|
199
|
+
const committed = doc.commit === null ? null : `Committed ${short$1(doc.commit)}`;
|
|
182
200
|
const recorded = committed === null ? "Staged; commit to record it." : `${committed}.`;
|
|
183
201
|
switch (doc.command) {
|
|
184
202
|
case "new": return [
|
|
@@ -287,7 +305,7 @@ function initText(doc) {
|
|
|
287
305
|
`Initialized ${doc.root}`,
|
|
288
306
|
...doc.written.map((p) => ` ${p}`),
|
|
289
307
|
...doc.kept.map((p) => `Kept ${p}`),
|
|
290
|
-
...doc.commit === null ? [] : [`Committed ${short(doc.commit)}.`],
|
|
308
|
+
...doc.commit === null ? [] : [`Committed ${short$1(doc.commit)}.`],
|
|
291
309
|
"Next: adr new \"<title>\""
|
|
292
310
|
].join("\n");
|
|
293
311
|
}
|
|
@@ -366,12 +384,12 @@ const acceptCommand = {
|
|
|
366
384
|
}
|
|
367
385
|
};
|
|
368
386
|
// src/cli/deprecate.ts
|
|
369
|
-
const USAGE$
|
|
387
|
+
const USAGE$5 = "deprecate <decision> --reason <text> [--commit | --no-commit] [-m <message>]";
|
|
370
388
|
/** `adr deprecate` (design §6.5): an accepted decision no longer in force. */
|
|
371
389
|
const deprecateCommand = {
|
|
372
390
|
name: "deprecate",
|
|
373
391
|
summary: "deprecate an accepted decision",
|
|
374
|
-
usage: USAGE$
|
|
392
|
+
usage: USAGE$5,
|
|
375
393
|
options: {
|
|
376
394
|
reason: { type: "string" },
|
|
377
395
|
...COMMIT_OPTIONS
|
|
@@ -384,7 +402,7 @@ const deprecateCommand = {
|
|
|
384
402
|
ref: "reject",
|
|
385
403
|
async run({ io, flags, values, positionals }) {
|
|
386
404
|
const input = positionals[0] ?? "";
|
|
387
|
-
if (typeof values.reason !== "string" || values.reason.trim() === "") throw new UsageError(`missing --reason <text>; usage: adr ${USAGE$
|
|
405
|
+
if (typeof values.reason !== "string" || values.reason.trim() === "") throw new UsageError(`missing --reason <text>; usage: adr ${USAGE$5}`);
|
|
388
406
|
const reason = typeof values.reason === "string" ? values.reason.trim() : null;
|
|
389
407
|
const log = await openForCli(io, flags);
|
|
390
408
|
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
@@ -435,7 +453,143 @@ const forCommand = {
|
|
|
435
453
|
exit: 0,
|
|
436
454
|
json: doc,
|
|
437
455
|
text: forText(doc, display, read.adr.anchorKeyword),
|
|
438
|
-
notes: doc.notes
|
|
456
|
+
notes: notesWith(doc.notes, numberingNote(read, { unavailable: false }))
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
// src/cli/hooks.ts
|
|
461
|
+
const USAGE$4 = "hooks install | uninstall | print --manager lefthook|husky|pre-commit | run pre-commit | trailers <message-file>";
|
|
462
|
+
const MANAGERS = [
|
|
463
|
+
"lefthook",
|
|
464
|
+
"husky",
|
|
465
|
+
"pre-commit"
|
|
466
|
+
];
|
|
467
|
+
const outcome = (doc, text, notes) => ({
|
|
468
|
+
exit: 0,
|
|
469
|
+
json: doc,
|
|
470
|
+
text,
|
|
471
|
+
notes
|
|
472
|
+
});
|
|
473
|
+
/**
|
|
474
|
+
* `adr hooks install | uninstall | print --manager … | run pre-commit | trailers <message-file>`
|
|
475
|
+
* (design 2b D4, D8, §4.3): adr's git hooks through ledger's shims, shared with other ark tools.
|
|
476
|
+
*/
|
|
477
|
+
const hooksCommand = {
|
|
478
|
+
name: "hooks",
|
|
479
|
+
summary: "install, remove or print adr's git hooks, and run their steps",
|
|
480
|
+
usage: USAGE$4,
|
|
481
|
+
options: { manager: { type: "string" } },
|
|
482
|
+
positionals: {
|
|
483
|
+
min: 1,
|
|
484
|
+
max: 2,
|
|
485
|
+
names: ["action", "argument"]
|
|
486
|
+
},
|
|
487
|
+
ref: "reject",
|
|
488
|
+
async run({ io, flags, values, positionals }) {
|
|
489
|
+
const [action = "", argument] = positionals;
|
|
490
|
+
const manager = values.manager;
|
|
491
|
+
if (manager !== void 0 && action !== "print") throw new UsageError(`--manager goes with hooks print only; usage: adr ${USAGE$4}`);
|
|
492
|
+
const noArgument = () => {
|
|
493
|
+
if (argument !== void 0) throw new UsageError(`unexpected argument ${JSON.stringify(argument)}; usage: adr ${USAGE$4}`);
|
|
494
|
+
};
|
|
495
|
+
switch (action) {
|
|
496
|
+
case "install": {
|
|
497
|
+
noArgument();
|
|
498
|
+
const log = await openForCli(io, flags);
|
|
499
|
+
const doc = await installDecisionHooks(log);
|
|
500
|
+
const lines = [...[...doc.written, ...doc.unchanged].map((p) => `${doc.written.includes(p) ? "wrote" : "unchanged"} ${p}`).sort((a, b) => order(a) - order(b)), `adr hooks: ${doc.registrations.map((r) => r.hook).join(", ")}`];
|
|
501
|
+
return outcome(doc, lines.join("\n"), log.notes);
|
|
502
|
+
}
|
|
503
|
+
case "uninstall": {
|
|
504
|
+
noArgument();
|
|
505
|
+
const log = await openForCli(io, flags);
|
|
506
|
+
const doc = await uninstallDecisionHooks(log);
|
|
507
|
+
const text = doc.written.length === 0 ? "adr hooks were not installed" : doc.written.map((p) => `removed adr from ${p}`).join("\n");
|
|
508
|
+
return outcome(doc, text, log.notes);
|
|
509
|
+
}
|
|
510
|
+
case "print": {
|
|
511
|
+
noArgument();
|
|
512
|
+
if (manager === void 0) throw new UsageError("hooks print needs --manager lefthook, husky or pre-commit");
|
|
513
|
+
if (!MANAGERS.includes(manager)) throw new UsageError(`unknown hook manager ${JSON.stringify(manager)}; use lefthook, husky or pre-commit`);
|
|
514
|
+
const log = await openForCli(io, flags);
|
|
515
|
+
const doc = printDecisionHooks(log, manager);
|
|
516
|
+
return outcome(doc, doc.snippet ?? "", log.notes);
|
|
517
|
+
}
|
|
518
|
+
case "run": {
|
|
519
|
+
if (argument !== "pre-commit") throw new UsageError("adr hooks run supports pre-commit");
|
|
520
|
+
const worktree = await openForCli(io, flags);
|
|
521
|
+
const index = await openForCli(io, flags, { reader: "index" });
|
|
522
|
+
const r = await runPreCommitHook({
|
|
523
|
+
worktree,
|
|
524
|
+
index,
|
|
525
|
+
env: io.env
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
exit: r.exit,
|
|
529
|
+
json: findingsReport(r.findings, { failOn: r.failOn }),
|
|
530
|
+
text: r.findings.length === 0 ? "" : formatText(r.findings),
|
|
531
|
+
notes: r.notes
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
case "trailers": {
|
|
535
|
+
if (argument === void 0) throw new UsageError(`missing argument <message-file>; usage: adr ${USAGE$4}`);
|
|
536
|
+
const log = await openForCli(io, flags);
|
|
537
|
+
const doc = await addHookTrailers(log, resolve(io.cwd, argument));
|
|
538
|
+
return outcome(doc, "", log.notes);
|
|
539
|
+
}
|
|
540
|
+
default: throw new UsageError(`unknown hooks action ${JSON.stringify(action)}; use install, uninstall, print, run or trailers`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
/** A shim line's rank in hook order, for the install text. */
|
|
545
|
+
function order(line) {
|
|
546
|
+
return [
|
|
547
|
+
"pre-commit",
|
|
548
|
+
"commit-msg",
|
|
549
|
+
"pre-push",
|
|
550
|
+
"post-merge"
|
|
551
|
+
].findIndex((h) => line.endsWith(`/${h}`));
|
|
552
|
+
}
|
|
553
|
+
// src/cli/index-command.ts
|
|
554
|
+
const short = (sha) => sha.slice(0, 7);
|
|
555
|
+
/** The text of `adr index` (design 2b §4.5). */
|
|
556
|
+
function indexText({ doc, refreshed }) {
|
|
557
|
+
const { path } = doc;
|
|
558
|
+
if (doc.check) return doc.findings.length === 0 ? `${path} is up to date` : formatText(doc.findings);
|
|
559
|
+
if (doc.commit !== null) return [`Committed ${path} as ${short(doc.commit)}`, ...refreshed ? [`Wrote ${path} with uncommitted log changes`] : []].join("\n");
|
|
560
|
+
if (refreshed) return `${path} is up to date in HEAD; wrote the work tree's copy, which includes uncommitted log changes`;
|
|
561
|
+
if (doc.fresh) return `${path} is up to date`;
|
|
562
|
+
return doc.written.length > 0 ? `Wrote ${path}` : `Staged ${path}`;
|
|
563
|
+
}
|
|
564
|
+
/** `adr index [--check] [--commit | --no-commit] [-m <message>]` (design 2b §4.5). */
|
|
565
|
+
const indexCommand = {
|
|
566
|
+
name: "index",
|
|
567
|
+
summary: "regenerate the decision index (index.md), or check that it is current",
|
|
568
|
+
usage: "index [--check] [--commit | --no-commit] [-m <message>]",
|
|
569
|
+
options: {
|
|
570
|
+
check: { type: "boolean" },
|
|
571
|
+
...COMMIT_OPTIONS
|
|
572
|
+
},
|
|
573
|
+
positionals: {
|
|
574
|
+
min: 0,
|
|
575
|
+
max: 0,
|
|
576
|
+
names: []
|
|
577
|
+
},
|
|
578
|
+
ref: "read",
|
|
579
|
+
async run({ io, flags, values }) {
|
|
580
|
+
const check = values.check === true;
|
|
581
|
+
if (check && (values.commit === true || typeof values.message === "string")) throw new UsageError("--check writes nothing; drop --commit and --message");
|
|
582
|
+
if (!check && flags.ref !== null) throw new UsageError("adr index writes the work tree; use --ref only with --check");
|
|
583
|
+
const log = await openForCli(io, flags);
|
|
584
|
+
const run = await runDecisionIndex(log, {
|
|
585
|
+
check,
|
|
586
|
+
...check ? {} : commitChoice(values, { autoCommit: log.adr.autoCommit })
|
|
587
|
+
});
|
|
588
|
+
return {
|
|
589
|
+
exit: check ? exitCode(run.doc.findings, { failOn: log.adr.failOn }) : 0,
|
|
590
|
+
json: run.doc,
|
|
591
|
+
text: indexText(run),
|
|
592
|
+
notes: run.doc.notes
|
|
439
593
|
};
|
|
440
594
|
}
|
|
441
595
|
};
|
|
@@ -524,15 +678,25 @@ function statuses(values) {
|
|
|
524
678
|
return s;
|
|
525
679
|
});
|
|
526
680
|
}
|
|
527
|
-
/** `
|
|
681
|
+
/** `--since`'s revision: non-empty and not an option. */
|
|
682
|
+
function sinceRev(values) {
|
|
683
|
+
const v = values.since;
|
|
684
|
+
if (typeof v !== "string") return null;
|
|
685
|
+
if (v.trim() === "" || v.startsWith("-")) throw new UsageError("--since must be a revision");
|
|
686
|
+
return v;
|
|
687
|
+
}
|
|
688
|
+
/** `adr list [--status <status>]… [--since <rev>]` (design §5.6, 2b D7). */
|
|
528
689
|
const listCommand = {
|
|
529
690
|
name: "list",
|
|
530
691
|
summary: "list decisions with their computed status",
|
|
531
|
-
usage: "list [--status <status>]…",
|
|
532
|
-
options: {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
692
|
+
usage: "list [--status <status>]… [--since <rev>]",
|
|
693
|
+
options: {
|
|
694
|
+
status: {
|
|
695
|
+
type: "string",
|
|
696
|
+
multiple: true
|
|
697
|
+
},
|
|
698
|
+
since: { type: "string" }
|
|
699
|
+
},
|
|
536
700
|
positionals: {
|
|
537
701
|
min: 0,
|
|
538
702
|
max: 0,
|
|
@@ -541,10 +705,17 @@ const listCommand = {
|
|
|
541
705
|
ref: "read",
|
|
542
706
|
async run({ io, flags, values }) {
|
|
543
707
|
const status = statuses(values);
|
|
544
|
-
const
|
|
545
|
-
const
|
|
708
|
+
const rev = sinceRev(values);
|
|
709
|
+
const log = await openForCli(io, flags);
|
|
710
|
+
const since = rev === null ? null : await changedSince(log, rev);
|
|
711
|
+
const read = await readDecisions(log);
|
|
712
|
+
const doc = listDecisions(read, {
|
|
713
|
+
status,
|
|
714
|
+
since
|
|
715
|
+
});
|
|
546
716
|
const notes = [...doc.notes];
|
|
547
|
-
|
|
717
|
+
const numbering = numberingNote(read, { unavailable: true });
|
|
718
|
+
if (numbering !== null && doc.decisions.length > 0) notes.push(numbering);
|
|
548
719
|
if (doc.findings.length > 0) notes.push(`the decision log has ${plural(doc.findings.length, "problem", "problems")}; run adr verify`);
|
|
549
720
|
if (doc.unreadable.length > 0) notes.push(`${plural(doc.unreadable.length, "decision uses", "decisions use")} a newer format; upgrade adr`);
|
|
550
721
|
if (doc.unfoldable.length > 0) notes.push(`${plural(doc.unfoldable.length, "decision", "decisions")} can't be read; run adr verify`);
|
|
@@ -552,7 +723,7 @@ const listCommand = {
|
|
|
552
723
|
return {
|
|
553
724
|
exit: 0,
|
|
554
725
|
json: doc,
|
|
555
|
-
text: listText(doc, display),
|
|
726
|
+
text: doc.since === null ? listText(doc, display) : sinceText(doc, doc.since, display),
|
|
556
727
|
notes
|
|
557
728
|
};
|
|
558
729
|
}
|
|
@@ -917,7 +1088,7 @@ const showCommand = {
|
|
|
917
1088
|
exit: 0,
|
|
918
1089
|
json: doc,
|
|
919
1090
|
text: showText(doc, display),
|
|
920
|
-
notes: doc.notes
|
|
1091
|
+
notes: notesWith(doc.notes, numberingNote(read, { unavailable: false }))
|
|
921
1092
|
};
|
|
922
1093
|
}
|
|
923
1094
|
};
|
|
@@ -1042,6 +1213,8 @@ function adrCli(version) {
|
|
|
1042
1213
|
noteCommand,
|
|
1043
1214
|
linkCommand,
|
|
1044
1215
|
scopeCommand,
|
|
1216
|
+
indexCommand,
|
|
1217
|
+
hooksCommand,
|
|
1045
1218
|
forCommand,
|
|
1046
1219
|
listCommand,
|
|
1047
1220
|
showCommand,
|