changeledger 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/AGENTS.md +25 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/changeledger.mjs +375 -0
- package/package.json +72 -0
- package/src/atomic-write.mjs +134 -0
- package/src/change.mjs +102 -0
- package/src/check.mjs +536 -0
- package/src/commands/agent.mjs +256 -0
- package/src/commands/check.mjs +48 -0
- package/src/commands/graduate.mjs +105 -0
- package/src/commands/init.mjs +43 -0
- package/src/commands/new.mjs +164 -0
- package/src/commands/register.mjs +28 -0
- package/src/commands/release.mjs +138 -0
- package/src/commands/view.mjs +52 -0
- package/src/config.mjs +76 -0
- package/src/contract.mjs +100 -0
- package/src/git.mjs +73 -0
- package/src/lifecycle.mjs +57 -0
- package/src/metrics.mjs +143 -0
- package/src/paths.mjs +12 -0
- package/src/registry.mjs +55 -0
- package/src/release.mjs +71 -0
- package/src/repo.mjs +122 -0
- package/src/slug.mjs +12 -0
- package/src/spec.mjs +12 -0
- package/src/viewer/domain.mjs +133 -0
- package/src/viewer/public/api.js +25 -0
- package/src/viewer/public/app-state.js +87 -0
- package/src/viewer/public/app.js +717 -0
- package/src/viewer/public/index.html +64 -0
- package/src/viewer/public/security.js +65 -0
- package/src/viewer/public/state.js +31 -0
- package/src/viewer/public/styles.css +1062 -0
- package/src/viewer/public/templates.js +9 -0
- package/src/viewer/public/view-parts.js +162 -0
- package/src/viewer/public/view-renderers.js +191 -0
- package/src/viewer/server/router.mjs +200 -0
- package/src/viewer/server/security.mjs +27 -0
- package/src/writer.mjs +151 -0
- package/src/yaml.mjs +75 -0
- package/templates/AGENTS.md +540 -0
- package/templates/config.yml +55 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# AGENTS.md — ChangeLedger (project's own contract)
|
|
2
|
+
|
|
3
|
+
This is the ChangeLedger repo itself. It dogfoods its own format: changes live
|
|
4
|
+
under `.changeledger/changes/`, persistent truth under `.changeledger/specs/`.
|
|
5
|
+
|
|
6
|
+
<!-- changeledger -->
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> This repo uses **ChangeLedger**. Read and follow [`.changeledger/AGENTS.md`](.changeledger/AGENTS.md)
|
|
9
|
+
> (the change contract). If it is missing, run `changeledger register`.
|
|
10
|
+
|
|
11
|
+
The **canonical ChangeLedger contract** (the convention every consuming repo
|
|
12
|
+
follows) is shipped as [`templates/AGENTS.md`](templates/AGENTS.md) and linked
|
|
13
|
+
into each repo as `.changeledger/AGENTS.md`. Edit the convention there, not here.
|
|
14
|
+
|
|
15
|
+
## Project-specific notes
|
|
16
|
+
|
|
17
|
+
- Managed with **pnpm**; lint/format via **Biome**. Runtime dependencies are
|
|
18
|
+
allowed only when they are mature and justified: the CLI uses `yaml` for
|
|
19
|
+
config/frontmatter parsing, and the viewer uses `lit-html`, `marked`,
|
|
20
|
+
`dompurify` and `mermaid` for templating, Markdown, sanitization and diagrams.
|
|
21
|
+
- `pnpm verify` (lint + test + `changeledger check`) is the full quality gate. The
|
|
22
|
+
versioned `hooks/pre-commit` runs `lint-staged`, `pnpm test` and `changeledger check`
|
|
23
|
+
so staged formatting stays compatible with partial commits.
|
|
24
|
+
- The contract you edit for the convention is `templates/AGENTS.md` — it is the
|
|
25
|
+
artifact that `changeledger init`/`register` symlink into other repos.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roberto Ruiz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# ChangeLedger (`changeledger`)
|
|
2
|
+
|
|
3
|
+
> Turn conversations into buildable changes.
|
|
4
|
+
|
|
5
|
+
ChangeLedger is a local-first workflow for planning software changes with coding
|
|
6
|
+
agents before implementation begins. Features, bugs, audits and refactors become
|
|
7
|
+
reviewable documents with an enforced lifecycle, acceptance criteria, tasks and
|
|
8
|
+
persistent product truth.
|
|
9
|
+
|
|
10
|
+
Its human layer is a local viewer: instead of reading scattered Markdown files,
|
|
11
|
+
you get a searchable board across all registered projects, rendered
|
|
12
|
+
specifications and dependency diagrams.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
ChangeLedger requires **Node.js 24 or newer**. Install the CLI globally:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install --global changeledger
|
|
20
|
+
# or: pnpm add --global changeledger
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
In a repository that already has an `AGENTS.md`:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
changeledger init
|
|
27
|
+
changeledger view
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`changeledger init` creates `.changeledger/`, gives the project a stable identity and links the
|
|
31
|
+
installed agent contract. The repository keeps only its configuration and
|
|
32
|
+
documents; the CLI and viewer remain in the global package.
|
|
33
|
+
|
|
34
|
+
## The workflow
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
conversation → draft → human approval → implementation → review
|
|
38
|
+
→ human validation → persistent specification
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A typical change starts like this:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
changeledger new feature oauth-login "Add OAuth login"
|
|
45
|
+
changeledger view
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The agent completes the generated stages under `.changeledger/changes/`. You approve the
|
|
49
|
+
draft in the viewer before implementation. From there, ChangeLedger validates
|
|
50
|
+
lifecycle transitions, task traceability and the final human acceptance gate.
|
|
51
|
+
When the work is done, its lasting truth graduates into `.changeledger/specs/`.
|
|
52
|
+
|
|
53
|
+
The contract is agent-agnostic: Codex, Claude Code, opencode, Copilot, Cursor and
|
|
54
|
+
other tools discover it through the repository's `AGENTS.md` reference.
|
|
55
|
+
|
|
56
|
+
## Changes and specs
|
|
57
|
+
|
|
58
|
+
- **Changes** describe a delta: why it is needed, what was learned, the chosen
|
|
59
|
+
design, acceptance criteria, implementation tasks and execution log.
|
|
60
|
+
- **Specs** describe the current system after completed changes graduate. They
|
|
61
|
+
have no work lifecycle and remain concise, durable product truth.
|
|
62
|
+
|
|
63
|
+
With the default Definition of Ready policy, `changeledger check` verifies that acceptance
|
|
64
|
+
criteria are test-grade and mapped to actionable tasks. Repositories doing
|
|
65
|
+
exploratory work can set `tdd: false`; the complete rules live in
|
|
66
|
+
[`AGENTS.md`](AGENTS.md).
|
|
67
|
+
|
|
68
|
+
## Essential commands
|
|
69
|
+
|
|
70
|
+
### Set up and inspect
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
changeledger init # initialize and register the current repository
|
|
74
|
+
changeledger register # relink a moved or freshly cloned repository
|
|
75
|
+
changeledger view # view every registered project
|
|
76
|
+
changeledger view . # view only the current project
|
|
77
|
+
changeledger check [id] # validate the repository or one change
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Work with changes
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
changeledger new <type> <slug> <title> # create a draft change
|
|
84
|
+
changeledger list [--status S] [--type T]
|
|
85
|
+
changeledger show <id> [--json]
|
|
86
|
+
changeledger status <id> <status>
|
|
87
|
+
changeledger task <id> done|block <n> [reason]
|
|
88
|
+
changeledger log <id> <message>
|
|
89
|
+
changeledger review <id> pass
|
|
90
|
+
changeledger review <id> fail --retry "<reason>"
|
|
91
|
+
changeledger review <id> fail --block "<reason>"
|
|
92
|
+
changeledger discard <id> <reason>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Preserve completed truth
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
changeledger graduate <id> <spec-slug> # create a persistent spec
|
|
99
|
+
changeledger graduate <id> <spec-slug> --into # update an existing spec's provenance
|
|
100
|
+
changeledger graduate <id> --skip [reason] # record that no spec is needed
|
|
101
|
+
changeledger archive --graduated [--dry-run] # hide resolved changes from the board
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Run `changeledger --help` or `changeledger <command> --help` for the complete command reference.
|
|
105
|
+
|
|
106
|
+
## Release planning
|
|
107
|
+
|
|
108
|
+
ChangeLedger can calculate a portable SemVer release from completed changes
|
|
109
|
+
without assuming a package ecosystem:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
changeledger release init 0.1.0 # adopt an existing published version once
|
|
113
|
+
changeledger release plan --json # calculate the next version without writing
|
|
114
|
+
changeledger release record 0.2.0 # record the calculated release manifest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The CLI decides which changes belong to the release and calculates their
|
|
118
|
+
effective `release_impact`. The operating agent applies that version to the
|
|
119
|
+
project's own surfaces—`package.json`, `pubspec.yaml`, `Cargo.toml`, Gradle,
|
|
120
|
+
Xcode or a monorepo—then runs the local quality gates and release workflow.
|
|
121
|
+
|
|
122
|
+
## Repository integration
|
|
123
|
+
|
|
124
|
+
`changeledger check` exits non-zero on contract errors, so it can be used in Git hooks and
|
|
125
|
+
CI:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
changeledger check || exit 1
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The contract itself ships with the CLI and is linked as `.changeledger/AGENTS.md`. It is
|
|
132
|
+
per-machine and gitignored, so upgrades do not leave committed copies behind.
|
|
133
|
+
Run `changeledger register` after cloning, moving a repository or updating the global
|
|
134
|
+
installation.
|
|
135
|
+
|
|
136
|
+
## Compatibility and security
|
|
137
|
+
|
|
138
|
+
- Node.js **24+**.
|
|
139
|
+
- Tested on Linux, macOS and Windows.
|
|
140
|
+
- On Windows, contract linking uses a symlink when permitted and falls back to a
|
|
141
|
+
copy when Developer Mode or administrator privileges are unavailable.
|
|
142
|
+
- The viewer binds to loopback and treats repository content as untrusted input.
|
|
143
|
+
See [`SECURITY.md`](SECURITY.md) for the threat model and private reporting
|
|
144
|
+
instructions.
|
|
145
|
+
|
|
146
|
+
## Project status
|
|
147
|
+
|
|
148
|
+
ChangeLedger is usable and self-hosting, but remains **pre-1.0**. Expect the
|
|
149
|
+
contract and CLI to evolve while the core workflow settles. Upgrade to the
|
|
150
|
+
latest `0.x` release to receive fixes.
|
|
151
|
+
|
|
152
|
+
Contributions are welcome. Development setup, quality gates and repository
|
|
153
|
+
conventions live in [`CONTRIBUTING.md`](CONTRIBUTING.md). The canonical agent
|
|
154
|
+
contract is [`AGENTS.md`](AGENTS.md).
|
|
155
|
+
|
|
156
|
+
## Language policy
|
|
157
|
+
|
|
158
|
+
Structure is always English: frontmatter keys, enum values, stage headings, CLI
|
|
159
|
+
commands and filenames. Generated content follows the repository's configured
|
|
160
|
+
`language`.
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import {
|
|
4
|
+
archive,
|
|
5
|
+
archiveGraduated,
|
|
6
|
+
discard,
|
|
7
|
+
list,
|
|
8
|
+
log,
|
|
9
|
+
owner,
|
|
10
|
+
review,
|
|
11
|
+
show,
|
|
12
|
+
status,
|
|
13
|
+
task,
|
|
14
|
+
} from '../src/commands/agent.mjs';
|
|
15
|
+
import { check } from '../src/commands/check.mjs';
|
|
16
|
+
import { graduate, pendingGraduation, skipGraduation } from '../src/commands/graduate.mjs';
|
|
17
|
+
import { init } from '../src/commands/init.mjs';
|
|
18
|
+
import { newChange } from '../src/commands/new.mjs';
|
|
19
|
+
import { registerRepo } from '../src/commands/register.mjs';
|
|
20
|
+
import { initReleaseHistory, recordRelease, releasePlan } from '../src/commands/release.mjs';
|
|
21
|
+
import { view } from '../src/commands/view.mjs';
|
|
22
|
+
import { nowUtc } from '../src/paths.mjs';
|
|
23
|
+
|
|
24
|
+
const USAGE = `ChangeLedger (changeledger)
|
|
25
|
+
|
|
26
|
+
changeledger init set up .changeledger/ in the current repo (+ register it)
|
|
27
|
+
changeledger register (re)link this repo's path in the global registry
|
|
28
|
+
changeledger new <type> <slug> <title> scaffold a new change (slug is the English filename)
|
|
29
|
+
changeledger view [port] launch the local viewer (default port 4040)
|
|
30
|
+
changeledger check [id] [--json] validate the repo or one change
|
|
31
|
+
changeledger status <id> <status> move a change's lifecycle status
|
|
32
|
+
changeledger discard <id> "<reason>" discard a change (terminal; keeps the record)
|
|
33
|
+
changeledger review <id> pass independent review passed → in-validation
|
|
34
|
+
changeledger review <id> fail --retry|--block "<reason>" review failed → in-progress|blocked
|
|
35
|
+
changeledger owner <id> <name|-> set or clear a change's owner
|
|
36
|
+
changeledger archive <id> / unarchive <id> hide/show a change in the viewer
|
|
37
|
+
changeledger archive --graduated [--dry-run] archive done changes already graduated/skipped
|
|
38
|
+
changeledger log <id> <message> append a timestamped Log entry
|
|
39
|
+
changeledger task <id> done|block <n> [reason] mark a Plan task
|
|
40
|
+
changeledger list [--status S] [--type T] [--json] list changes
|
|
41
|
+
changeledger show <id> [--json] print a change
|
|
42
|
+
changeledger graduate <change-id> <spec-slug> graduate a change to a new spec
|
|
43
|
+
changeledger graduate <change-id> <spec-slug> --into graduate into an existing spec
|
|
44
|
+
changeledger graduate <change-id> --skip [reason] mark graduation reviewed, no spec
|
|
45
|
+
changeledger graduate --pending list done changes not yet reviewed
|
|
46
|
+
changeledger release init <version> initialize release history at X.Y.Z
|
|
47
|
+
changeledger release plan [--json] calculate the next portable SemVer release
|
|
48
|
+
changeledger release record <version> record the calculated release manifest`;
|
|
49
|
+
|
|
50
|
+
const program = new Command();
|
|
51
|
+
|
|
52
|
+
function action(fn) {
|
|
53
|
+
return async (...args) => {
|
|
54
|
+
try {
|
|
55
|
+
await fn(...args);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(`Error: ${e.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.name('changeledger')
|
|
65
|
+
.description('ChangeLedger (changeledger)')
|
|
66
|
+
.helpOption('-h, --help', 'display help for command')
|
|
67
|
+
.addHelpText('after', `\n${USAGE}`);
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('init')
|
|
71
|
+
.description('set up .changeledger/ in the current repo (+ register it)')
|
|
72
|
+
.action(
|
|
73
|
+
action(() => {
|
|
74
|
+
const dir = init();
|
|
75
|
+
console.log(`Initialized ChangeLedger at ${dir}`);
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command('register')
|
|
81
|
+
.description("(re)link this repo's path in the global registry")
|
|
82
|
+
.action(
|
|
83
|
+
action(() => {
|
|
84
|
+
const { id, path: p } = registerRepo();
|
|
85
|
+
console.log(`Registered ${id} → ${p}`);
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('new')
|
|
91
|
+
.description('scaffold a new change')
|
|
92
|
+
.argument('<type>')
|
|
93
|
+
.argument('<slug>')
|
|
94
|
+
.argument('<title...>')
|
|
95
|
+
.option('--owner <name>', 'set the initial owner')
|
|
96
|
+
.action(
|
|
97
|
+
action((type, slug, titleParts, options) => {
|
|
98
|
+
const title = titleParts.join(' ').trim();
|
|
99
|
+
const file = newChange({ type, slug, title, owner: options.owner, now: nowUtc() });
|
|
100
|
+
console.log(`Created ${file}`);
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command('view')
|
|
106
|
+
.description('launch the local viewer')
|
|
107
|
+
.argument('[args...]')
|
|
108
|
+
.action(action((args) => view(args)));
|
|
109
|
+
|
|
110
|
+
program
|
|
111
|
+
.command('check')
|
|
112
|
+
.description('validate the repo or one change')
|
|
113
|
+
.argument('[id]')
|
|
114
|
+
.option('--json', 'print JSON')
|
|
115
|
+
.action((id, options) => {
|
|
116
|
+
try {
|
|
117
|
+
const args = [...(id ? [id] : []), ...(options.json ? ['--json'] : [])];
|
|
118
|
+
process.exit(check(args));
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error(`Error: ${e.message}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
program
|
|
126
|
+
.command('status')
|
|
127
|
+
.description("move a change's lifecycle status")
|
|
128
|
+
.argument('<id>')
|
|
129
|
+
.argument('<status>')
|
|
130
|
+
.action(
|
|
131
|
+
action((id, st) => {
|
|
132
|
+
status(id, st);
|
|
133
|
+
console.log(`#${id} → ${st}`);
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command('discard')
|
|
139
|
+
.description('discard a change (terminal; keeps the record and reason)')
|
|
140
|
+
.argument('<id>')
|
|
141
|
+
.argument('<reason...>')
|
|
142
|
+
.action(
|
|
143
|
+
action((id, reasonParts) => {
|
|
144
|
+
discard(id, reasonParts.join(' ').trim());
|
|
145
|
+
console.log(`#${id} → discarded`);
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
program
|
|
150
|
+
.command('review')
|
|
151
|
+
.description('record an independent review verdict')
|
|
152
|
+
.argument('<id>')
|
|
153
|
+
.argument('<verdict>', 'pass|fail')
|
|
154
|
+
.argument('[reason...]')
|
|
155
|
+
.option('--retry', 'route a failed review back to in-progress')
|
|
156
|
+
.option('--block', 'route a failed review to blocked')
|
|
157
|
+
.addHelpText(
|
|
158
|
+
'after',
|
|
159
|
+
[
|
|
160
|
+
'',
|
|
161
|
+
'Examples:',
|
|
162
|
+
' changeledger review <id> pass',
|
|
163
|
+
' changeledger review <id> fail --retry "<reason>"',
|
|
164
|
+
' changeledger review <id> fail --block "<reason>"',
|
|
165
|
+
].join('\n'),
|
|
166
|
+
)
|
|
167
|
+
.action(
|
|
168
|
+
action((id, verdict, reasonParts, options) => {
|
|
169
|
+
const mode = options.retry ? 'retry' : options.block ? 'block' : undefined;
|
|
170
|
+
const reason = reasonParts.join(' ').trim() || undefined;
|
|
171
|
+
review(id, verdict, { mode, reason });
|
|
172
|
+
console.log(`#${id} review ${verdict}${mode ? ` --${mode}` : ''}`);
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
program
|
|
177
|
+
.command('owner')
|
|
178
|
+
.description("set or clear a change's owner")
|
|
179
|
+
.argument('<id>')
|
|
180
|
+
.argument('<name>')
|
|
181
|
+
.action(
|
|
182
|
+
action((id, name) => {
|
|
183
|
+
owner(id, name);
|
|
184
|
+
console.log(`#${id} owner → ${name === '-' ? '(cleared)' : name}`);
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
program
|
|
189
|
+
.command('archive')
|
|
190
|
+
.description('hide a change in the viewer, or archive all graduated done changes')
|
|
191
|
+
.argument('[id]')
|
|
192
|
+
.option('--graduated', 'archive done changes already graduated or skipped')
|
|
193
|
+
.option('--dry-run', 'show what would be archived without writing')
|
|
194
|
+
.action(
|
|
195
|
+
action((id, options) => {
|
|
196
|
+
if (options.graduated) {
|
|
197
|
+
if (id) throw new Error('archive --graduated does not take an id');
|
|
198
|
+
const archived = archiveGraduated({ dryRun: options.dryRun });
|
|
199
|
+
for (const c of archived) console.log(`#${c.id} ${c.title}`);
|
|
200
|
+
console.log(
|
|
201
|
+
`${options.dryRun ? 'Would archive' : 'Archived'} ${archived.length} change(s)`,
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (options.dryRun) throw new Error('--dry-run requires --graduated');
|
|
206
|
+
if (!id) throw new Error('archive requires <id> or --graduated');
|
|
207
|
+
archive(id, true);
|
|
208
|
+
console.log(`#${id} archived`);
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
program
|
|
213
|
+
.command('unarchive')
|
|
214
|
+
.description('show a change in the viewer')
|
|
215
|
+
.argument('<id>')
|
|
216
|
+
.action(
|
|
217
|
+
action((id) => {
|
|
218
|
+
archive(id, false);
|
|
219
|
+
console.log(`#${id} unarchived`);
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
program
|
|
224
|
+
.command('log')
|
|
225
|
+
.description('append a timestamped Log entry')
|
|
226
|
+
.argument('<id>')
|
|
227
|
+
.argument('<message...>')
|
|
228
|
+
.action(
|
|
229
|
+
action((id, messageParts) => {
|
|
230
|
+
log(id, messageParts.join(' ').trim());
|
|
231
|
+
console.log(`logged on #${id}`);
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
program
|
|
236
|
+
.command('task')
|
|
237
|
+
.description('mark a Plan task')
|
|
238
|
+
.argument('<id>')
|
|
239
|
+
.argument('<action>', 'done|block')
|
|
240
|
+
.argument('<n>')
|
|
241
|
+
.argument('[reason...]')
|
|
242
|
+
.action(
|
|
243
|
+
action((id, taskAction, nStr, reasonParts) => {
|
|
244
|
+
const n = Number(nStr);
|
|
245
|
+
task(id, taskAction, n, reasonParts.join(' ').trim());
|
|
246
|
+
console.log(`task #${n} on #${id} → ${taskAction}`);
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
program
|
|
251
|
+
.command('list')
|
|
252
|
+
.description('list changes')
|
|
253
|
+
.option('--status <status>', 'filter by status')
|
|
254
|
+
.option('--type <type>', 'filter by type')
|
|
255
|
+
.option('--json', 'print JSON')
|
|
256
|
+
.action(
|
|
257
|
+
action((options) => {
|
|
258
|
+
const items = list({ status: options.status, type: options.type });
|
|
259
|
+
if (options.json) {
|
|
260
|
+
console.log(JSON.stringify(items, null, 2));
|
|
261
|
+
} else {
|
|
262
|
+
for (const c of items) console.log(`${String(c.status).padEnd(12)} #${c.id} ${c.title}`);
|
|
263
|
+
}
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
program
|
|
268
|
+
.command('show')
|
|
269
|
+
.description('print a change')
|
|
270
|
+
.argument('<id>')
|
|
271
|
+
.option('--json', 'print JSON')
|
|
272
|
+
.action(
|
|
273
|
+
action((id, options) => {
|
|
274
|
+
const c = show(id);
|
|
275
|
+
if (options.json) console.log(JSON.stringify(c, null, 2));
|
|
276
|
+
else console.log(`#${c.id} ${c.frontmatter.title} [${c.frontmatter.status}]`);
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
program
|
|
281
|
+
.command('graduate')
|
|
282
|
+
.description('graduate a done change to persistent truth')
|
|
283
|
+
.argument('[change-id]')
|
|
284
|
+
.argument('[spec-slug]')
|
|
285
|
+
.argument('[reason...]')
|
|
286
|
+
.option('--into', 'graduate into an existing spec')
|
|
287
|
+
.option('--skip', 'mark graduation reviewed without a spec')
|
|
288
|
+
.option('--pending', 'list done changes not yet reviewed')
|
|
289
|
+
.addHelpText(
|
|
290
|
+
'after',
|
|
291
|
+
[
|
|
292
|
+
'',
|
|
293
|
+
'Examples:',
|
|
294
|
+
' changeledger graduate <change-id> <spec-slug>',
|
|
295
|
+
' changeledger graduate <change-id> <spec-slug> --into',
|
|
296
|
+
' changeledger graduate <change-id> --skip [reason]',
|
|
297
|
+
' changeledger graduate --pending',
|
|
298
|
+
].join('\n'),
|
|
299
|
+
)
|
|
300
|
+
.action(
|
|
301
|
+
action((id, slug, reasonParts, options) => {
|
|
302
|
+
if (options.pending) {
|
|
303
|
+
const items = pendingGraduation();
|
|
304
|
+
if (!items.length) console.log('No changes pending graduation.');
|
|
305
|
+
for (const c of items) console.log(`#${c.id} ${c.title}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (options.skip) {
|
|
309
|
+
if (!id) throw new Error('Usage: changeledger graduate <change-id> --skip [reason]');
|
|
310
|
+
const reason = [slug, ...reasonParts].filter(Boolean).join(' ').trim();
|
|
311
|
+
skipGraduation(id, reason);
|
|
312
|
+
console.log(`#${id} graduation skipped`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (!id || !slug) throw new Error('Usage: changeledger graduate <change-id> <spec-slug>');
|
|
316
|
+
const file = graduate(id, slug, process.cwd(), { into: options.into });
|
|
317
|
+
console.log(`Graduated #${id} → ${file}`);
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const releaseCommand = program
|
|
322
|
+
.command('release')
|
|
323
|
+
.description('plan and record portable SemVer releases');
|
|
324
|
+
|
|
325
|
+
releaseCommand
|
|
326
|
+
.command('init')
|
|
327
|
+
.description('initialize release history from the current published version')
|
|
328
|
+
.argument('<version>')
|
|
329
|
+
.action(
|
|
330
|
+
action((version) => {
|
|
331
|
+
const { file, manifest } = initReleaseHistory(version);
|
|
332
|
+
console.log(`Initialized release ${manifest.version} baseline → ${file}`);
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
releaseCommand
|
|
337
|
+
.command('plan')
|
|
338
|
+
.description('calculate the next release without writing files')
|
|
339
|
+
.option('--json', 'print a stable JSON plan')
|
|
340
|
+
.action(
|
|
341
|
+
action((options) => {
|
|
342
|
+
const plan = releasePlan();
|
|
343
|
+
if (options.json) {
|
|
344
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!plan.releasable) {
|
|
348
|
+
console.log(
|
|
349
|
+
`No release required from ${plan.currentVersion}: ${plan.changes.length} pending change(s), highest impact none.`,
|
|
350
|
+
);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
console.log(`${plan.currentVersion} → ${plan.nextVersion} (${plan.impact})`);
|
|
354
|
+
for (const change of plan.changes) {
|
|
355
|
+
console.log(` #${change.id} [${change.releaseImpact}] ${change.title}`);
|
|
356
|
+
}
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
releaseCommand
|
|
361
|
+
.command('record')
|
|
362
|
+
.description('record the currently calculated release')
|
|
363
|
+
.argument('<version>')
|
|
364
|
+
.action(
|
|
365
|
+
action((version) => {
|
|
366
|
+
const { file, manifest } = recordRelease(version);
|
|
367
|
+
console.log(`Recorded release ${manifest.version} → ${file}`);
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (process.argv.length <= 2) {
|
|
372
|
+
console.log(USAGE);
|
|
373
|
+
} else {
|
|
374
|
+
program.parse();
|
|
375
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "changeledger",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Turn conversations into buildable changes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"changeledger": "bin/changeledger.mjs"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/rarc88/changeledger#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/rarc88/changeledger.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/rarc88/changeledger/issues"
|
|
16
|
+
},
|
|
17
|
+
"author": "Roberto Ruiz (https://github.com/rarc88)",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"spec",
|
|
20
|
+
"specification",
|
|
21
|
+
"documentation",
|
|
22
|
+
"sdd",
|
|
23
|
+
"spec-driven-development",
|
|
24
|
+
"change-management",
|
|
25
|
+
"requirements",
|
|
26
|
+
"tdd",
|
|
27
|
+
"agents",
|
|
28
|
+
"ai-agents",
|
|
29
|
+
"cli",
|
|
30
|
+
"workflow"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"lint": "biome check",
|
|
34
|
+
"format": "biome check --write",
|
|
35
|
+
"test": "node --test",
|
|
36
|
+
"test:coverage": "node --test --experimental-test-coverage",
|
|
37
|
+
"audit:deps": "pnpm audit --audit-level moderate",
|
|
38
|
+
"check": "node bin/changeledger.mjs check",
|
|
39
|
+
"verify": "pnpm lint && pnpm test && pnpm check",
|
|
40
|
+
"prepublishOnly": "pnpm verify"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=24"
|
|
44
|
+
},
|
|
45
|
+
"packageManager": "pnpm@10.31.0",
|
|
46
|
+
"exports": {
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"bin",
|
|
51
|
+
"src",
|
|
52
|
+
"templates",
|
|
53
|
+
"AGENTS.md"
|
|
54
|
+
],
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"lint-staged": {
|
|
57
|
+
"*.{js,mjs,json,jsonc,css}": "biome check --write --no-errors-on-unmatched"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@biomejs/biome": "^2.5.0",
|
|
61
|
+
"jsdom": "^29.1.1",
|
|
62
|
+
"lint-staged": "^17.0.7"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"commander": "^14.0.3",
|
|
66
|
+
"dompurify": "^3.4.11",
|
|
67
|
+
"lit-html": "^3.3.3",
|
|
68
|
+
"marked": "^18.0.5",
|
|
69
|
+
"mermaid": "^11.15.0",
|
|
70
|
+
"yaml": "^2.9.0"
|
|
71
|
+
}
|
|
72
|
+
}
|