changeledger 0.6.4 → 0.7.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 +13 -8
- package/bin/changeledger.mjs +135 -43
- package/package.json +1 -1
- package/src/check.mjs +69 -4
- package/src/commands/context.mjs +95 -15
- package/src/commands/view.mjs +23 -2
- package/src/contract.mjs +9 -6
- package/src/lifecycle.mjs +16 -0
- package/src/metrics.mjs +6 -3
- package/templates/contract/core.md +18 -13
- package/templates/contract/readiness.md +5 -2
package/AGENTS.md
CHANGED
|
@@ -5,12 +5,15 @@ under `.changeledger/changes/`, persistent truth under `.changeledger/specs/`.
|
|
|
5
5
|
|
|
6
6
|
<!-- changeledger -->
|
|
7
7
|
> [!IMPORTANT]
|
|
8
|
-
> This repo uses **ChangeLedger**.
|
|
9
|
-
> `changeledger context` directly
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
> ChangeLedger; do not proceed from memory.
|
|
8
|
+
> This repo uses **ChangeLedger**. Immediately after reading this file — before
|
|
9
|
+
> planning, investigating, or acting — run `changeledger context` directly and
|
|
10
|
+
> read its complete output through the `CHANGELEDGER CONTEXT END` line. If that
|
|
11
|
+
> line is missing, the output was truncated: stop and re-run the command
|
|
12
|
+
> directly, without pipes or filters. If the command is unavailable, stop and
|
|
13
|
+
> restore/install ChangeLedger; do not proceed from memory.
|
|
14
|
+
>
|
|
15
|
+
> Do not create or modify files without an authorized change; the core context
|
|
16
|
+
> defines the workflow, the task contexts, and the narrow operational exception.
|
|
14
17
|
|
|
15
18
|
The canonical ChangeLedger contract is split into task-focused fragments under
|
|
16
19
|
[`templates/contract/`](templates/contract/). The deterministic
|
|
@@ -25,8 +28,10 @@ bootstrap above, not a linked or copied contract.
|
|
|
25
28
|
optional integrations.
|
|
26
29
|
- Managed with **pnpm**; lint/format via **Biome**. Runtime dependencies are
|
|
27
30
|
allowed only when they are mature and justified: the CLI uses `yaml` for
|
|
28
|
-
config/frontmatter parsing
|
|
29
|
-
|
|
31
|
+
config/frontmatter parsing and `commander` for argument/option/subcommand
|
|
32
|
+
parsing with built-in errors and help, and the viewer uses `lit-html`,
|
|
33
|
+
`marked`, `dompurify` and `mermaid` for templating, Markdown, sanitization and
|
|
34
|
+
diagrams.
|
|
30
35
|
- `pnpm verify` (lint + test + `changeledger check`) is the full quality gate. The
|
|
31
36
|
versioned `hooks/pre-commit` runs `lint-staged`, `pnpm test` and `changeledger check`
|
|
32
37
|
so staged formatting stays compatible with partial commits.
|
package/bin/changeledger.mjs
CHANGED
|
@@ -34,31 +34,13 @@ const { version } = createRequire(import.meta.url)('../package.json');
|
|
|
34
34
|
|
|
35
35
|
const USAGE = `ChangeLedger (changeledger)
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
changeledger
|
|
40
|
-
changeledger
|
|
41
|
-
changeledger
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
changeledger discard <id> "<reason>" discard a change (terminal; keeps the record)
|
|
45
|
-
changeledger review <id> pass independent review passed → in-validation
|
|
46
|
-
changeledger review <id> fail --retry|--block "<reason>" review failed → in-progress|blocked
|
|
47
|
-
changeledger owner <id> <name|-> set or clear a change's owner
|
|
48
|
-
changeledger archive <id> / unarchive <id> hide/show a change in the viewer
|
|
49
|
-
changeledger archive --graduated [--dry-run] archive done changes already graduated/skipped
|
|
50
|
-
changeledger log <id> <message> append a timestamped Log entry
|
|
51
|
-
changeledger task <id> done|block <n> [reason] mark a Plan task
|
|
52
|
-
changeledger list [--status S] [--type T] [--json] list changes
|
|
53
|
-
changeledger show <id> [--json] print a change
|
|
54
|
-
changeledger graduate <change-id> <spec-slug> --new create a spec scaffold to refine
|
|
55
|
-
changeledger graduate <change-id> <spec-slug> --into graduate into an existing spec
|
|
56
|
-
changeledger graduate <change-id> --skip [reason] mark graduation reviewed, no spec
|
|
57
|
-
changeledger graduate --pending list done changes not yet reviewed
|
|
58
|
-
changeledger config migrate [--dry-run] migrate .changeledger/config.yml to schema ${SUPPORTED_SCHEMA_VERSION}
|
|
59
|
-
changeledger release init <version> initialize release history at X.Y.Z
|
|
60
|
-
changeledger release plan [--json] calculate the next portable SemVer release
|
|
61
|
-
changeledger release record <version> record the calculated release manifest`;
|
|
37
|
+
Run \`changeledger context\` first in any repo — it is the mandatory bootstrap.
|
|
38
|
+
|
|
39
|
+
changeledger init | register | new | view | check | context
|
|
40
|
+
changeledger status | discard | review | owner | archive | unarchive
|
|
41
|
+
changeledger log | task | list | show | graduate | config | release
|
|
42
|
+
|
|
43
|
+
Run \`changeledger <command> --help\` for that command's syntax, values and examples.`;
|
|
62
44
|
|
|
63
45
|
const program = new Command();
|
|
64
46
|
|
|
@@ -78,7 +60,11 @@ program
|
|
|
78
60
|
.description('ChangeLedger (changeledger)')
|
|
79
61
|
.version(version, '-v, --version', 'output the installed version (-V also accepted)')
|
|
80
62
|
.helpOption('-h, --help', 'display help for command')
|
|
81
|
-
.addHelpText(
|
|
63
|
+
.addHelpText(
|
|
64
|
+
'after',
|
|
65
|
+
'\nRun `changeledger context` first in any repo — it is the mandatory bootstrap.\n' +
|
|
66
|
+
"Run `changeledger <command> --help` for that command's syntax, values and examples.",
|
|
67
|
+
);
|
|
82
68
|
|
|
83
69
|
program
|
|
84
70
|
.command('init')
|
|
@@ -103,10 +89,18 @@ program
|
|
|
103
89
|
program
|
|
104
90
|
.command('new')
|
|
105
91
|
.description('scaffold a new change')
|
|
106
|
-
.argument('<type>')
|
|
107
|
-
.argument('<slug>')
|
|
108
|
-
.argument('<title...>')
|
|
109
|
-
.option('--owner <name>', 'set the initial owner')
|
|
92
|
+
.argument('<type>', 'a type key configured in .changeledger/config.yml (types:)')
|
|
93
|
+
.argument('<slug>', 'English filename slug, e.g. self-describing-cli-help')
|
|
94
|
+
.argument('<title...>', 'content title, written in the repo language (config.yml: language)')
|
|
95
|
+
.option('--owner <name>', 'set the initial owner (defaults to unassigned)')
|
|
96
|
+
.addHelpText(
|
|
97
|
+
'after',
|
|
98
|
+
[
|
|
99
|
+
'',
|
|
100
|
+
'Example:',
|
|
101
|
+
' changeledger new feature self-describing-cli-help "Self-describing CLI help"',
|
|
102
|
+
].join('\n'),
|
|
103
|
+
)
|
|
110
104
|
.action(
|
|
111
105
|
action((type, slug, titleParts, options) => {
|
|
112
106
|
const title = titleParts.join(' ').trim();
|
|
@@ -117,8 +111,19 @@ program
|
|
|
117
111
|
|
|
118
112
|
program
|
|
119
113
|
.command('view')
|
|
120
|
-
.description('launch the local viewer')
|
|
121
|
-
.argument('[args...]')
|
|
114
|
+
.description('launch the local viewer (all registered projects, or one repo with `.`)')
|
|
115
|
+
.argument('[args...]', 'optional "." for local-only mode and/or a port (default 4040)')
|
|
116
|
+
.addHelpText(
|
|
117
|
+
'after',
|
|
118
|
+
[
|
|
119
|
+
'',
|
|
120
|
+
'Examples:',
|
|
121
|
+
' changeledger view # every registered project, port 4040',
|
|
122
|
+
' changeledger view . # only the current repo, port 4040',
|
|
123
|
+
' changeledger view 4041 # every registered project, port 4041',
|
|
124
|
+
' changeledger view . 4041 # only the current repo, port 4041',
|
|
125
|
+
].join('\n'),
|
|
126
|
+
)
|
|
122
127
|
.action(action((args) => view(args)));
|
|
123
128
|
|
|
124
129
|
program
|
|
@@ -139,14 +144,61 @@ program
|
|
|
139
144
|
program
|
|
140
145
|
.command('context')
|
|
141
146
|
.description('print deterministic task context')
|
|
142
|
-
.argument(
|
|
147
|
+
.argument(
|
|
148
|
+
'[mode-or-change-id]',
|
|
149
|
+
'spec|implement|review|release, or a change id (pack inferred from its status)',
|
|
150
|
+
)
|
|
151
|
+
.addHelpText(
|
|
152
|
+
'after',
|
|
153
|
+
[
|
|
154
|
+
'',
|
|
155
|
+
'With no argument: prints the mandatory bootstrap core. Always run this first —',
|
|
156
|
+
'every mode and change id below is incremental and extends the core already read,',
|
|
157
|
+
'it never replaces it.',
|
|
158
|
+
'',
|
|
159
|
+
'Explicit modes (pass one literally):',
|
|
160
|
+
' spec author or refine a change',
|
|
161
|
+
' implement execute an approved change',
|
|
162
|
+
' review independently verify completed work',
|
|
163
|
+
' release plan portable delivery metadata',
|
|
164
|
+
'',
|
|
165
|
+
'Change id (e.g. changeledger context 20260630-225212): loads the pack inferred',
|
|
166
|
+
"from that change's current lifecycle status — you never choose this pack",
|
|
167
|
+
'yourself. Lifecycle overlays such as blocked, validation, close and discarded',
|
|
168
|
+
'are inferred the same way from the change id; they are not modes you pass',
|
|
169
|
+
'explicitly.',
|
|
170
|
+
'',
|
|
171
|
+
'Examples:',
|
|
172
|
+
' changeledger context',
|
|
173
|
+
' changeledger context spec',
|
|
174
|
+
' changeledger context implement',
|
|
175
|
+
' changeledger context review',
|
|
176
|
+
' changeledger context release',
|
|
177
|
+
' changeledger context 20260630-225212',
|
|
178
|
+
].join('\n'),
|
|
179
|
+
)
|
|
143
180
|
.action(action((input) => context(input)));
|
|
144
181
|
|
|
145
182
|
program
|
|
146
183
|
.command('status')
|
|
147
|
-
.description("move a change's lifecycle status")
|
|
184
|
+
.description("move a change's lifecycle status (agent-owned, non-terminal moves only)")
|
|
148
185
|
.argument('<id>')
|
|
149
|
-
.argument(
|
|
186
|
+
.argument(
|
|
187
|
+
'<status>',
|
|
188
|
+
'a status configured in .changeledger/config.yml (statuses:), e.g. approved, in-progress, in-review, blocked',
|
|
189
|
+
)
|
|
190
|
+
.addHelpText(
|
|
191
|
+
'after',
|
|
192
|
+
[
|
|
193
|
+
'',
|
|
194
|
+
'Terminal moves are not accepted here: use `changeledger discard <id> "<reason>"`',
|
|
195
|
+
'to discard, and human validation in the viewer to reach done.',
|
|
196
|
+
'',
|
|
197
|
+
'Examples:',
|
|
198
|
+
' changeledger status <id> in-progress',
|
|
199
|
+
' changeledger status <id> blocked',
|
|
200
|
+
].join('\n'),
|
|
201
|
+
)
|
|
150
202
|
.action(
|
|
151
203
|
action((id, st) => {
|
|
152
204
|
status(id, st);
|
|
@@ -197,7 +249,16 @@ program
|
|
|
197
249
|
.command('owner')
|
|
198
250
|
.description("set or clear a change's owner")
|
|
199
251
|
.argument('<id>')
|
|
200
|
-
.argument('<name>')
|
|
252
|
+
.argument('<name>', 'owner handle, or "-" to clear it')
|
|
253
|
+
.addHelpText(
|
|
254
|
+
'after',
|
|
255
|
+
[
|
|
256
|
+
'',
|
|
257
|
+
'Examples:',
|
|
258
|
+
' changeledger owner <id> jdoe',
|
|
259
|
+
' changeledger owner <id> - # clears the owner',
|
|
260
|
+
].join('\n'),
|
|
261
|
+
)
|
|
201
262
|
.action(
|
|
202
263
|
action((id, name) => {
|
|
203
264
|
owner(id, name);
|
|
@@ -208,9 +269,19 @@ program
|
|
|
208
269
|
program
|
|
209
270
|
.command('archive')
|
|
210
271
|
.description('hide a change in the viewer, or archive all graduated done changes')
|
|
211
|
-
.argument('[id]')
|
|
212
|
-
.option('--graduated', 'archive done
|
|
213
|
-
.option('--dry-run', '
|
|
272
|
+
.argument('[id]', 'a change id; mutually exclusive with --graduated')
|
|
273
|
+
.option('--graduated', 'archive every done change already graduated or skipped (takes no id)')
|
|
274
|
+
.option('--dry-run', 'preview --graduated without writing; requires --graduated')
|
|
275
|
+
.addHelpText(
|
|
276
|
+
'after',
|
|
277
|
+
[
|
|
278
|
+
'',
|
|
279
|
+
'Examples:',
|
|
280
|
+
' changeledger archive <id>',
|
|
281
|
+
' changeledger archive --graduated',
|
|
282
|
+
' changeledger archive --graduated --dry-run',
|
|
283
|
+
].join('\n'),
|
|
284
|
+
)
|
|
214
285
|
.action(
|
|
215
286
|
action((id, options) => {
|
|
216
287
|
if (options.graduated) {
|
|
@@ -257,8 +328,17 @@ program
|
|
|
257
328
|
.description('mark a Plan task')
|
|
258
329
|
.argument('<id>')
|
|
259
330
|
.argument('<action>', 'done|block')
|
|
260
|
-
.argument('<n>')
|
|
261
|
-
.argument('[reason...]')
|
|
331
|
+
.argument('<n>', 'the Plan task index, 1-based, in document order')
|
|
332
|
+
.argument('[reason...]', 'required when action is block; ignored when action is done')
|
|
333
|
+
.addHelpText(
|
|
334
|
+
'after',
|
|
335
|
+
[
|
|
336
|
+
'',
|
|
337
|
+
'Examples:',
|
|
338
|
+
' changeledger task <id> done 1',
|
|
339
|
+
' changeledger task <id> block 2 "waiting on design decision"',
|
|
340
|
+
].join('\n'),
|
|
341
|
+
)
|
|
262
342
|
.action(
|
|
263
343
|
action((id, taskAction, nStr, reasonParts) => {
|
|
264
344
|
const n = Number(nStr);
|
|
@@ -270,9 +350,21 @@ program
|
|
|
270
350
|
program
|
|
271
351
|
.command('list')
|
|
272
352
|
.description('list changes')
|
|
273
|
-
.option(
|
|
274
|
-
|
|
353
|
+
.option(
|
|
354
|
+
'--status <status>',
|
|
355
|
+
'filter by a status configured in .changeledger/config.yml (statuses:)',
|
|
356
|
+
)
|
|
357
|
+
.option('--type <type>', 'filter by a type configured in .changeledger/config.yml (types:)')
|
|
275
358
|
.option('--json', 'print JSON')
|
|
359
|
+
.addHelpText(
|
|
360
|
+
'after',
|
|
361
|
+
[
|
|
362
|
+
'',
|
|
363
|
+
'Examples:',
|
|
364
|
+
' changeledger list --status approved',
|
|
365
|
+
' changeledger list --type feature --json',
|
|
366
|
+
].join('\n'),
|
|
367
|
+
)
|
|
276
368
|
.action(
|
|
277
369
|
action((options) => {
|
|
278
370
|
const items = list({ status: options.status, type: options.type });
|
package/package.json
CHANGED
package/src/check.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Pure validator: takes a loaded repo ({ config, changes }) and returns
|
|
2
2
|
// { errors, warnings }. No IO — the `changeledger check` command does the IO and printing.
|
|
3
3
|
|
|
4
|
+
import { CANONICAL_STATUSES, canTransition, parseLogEvent } from './lifecycle.mjs';
|
|
4
5
|
import { compareVersions, parseVersion, RELEASE_IMPACTS } from './release.mjs';
|
|
5
6
|
|
|
6
7
|
const REQUIRED = ['id', 'title', 'type', 'status', 'created', 'depends_on'];
|
|
@@ -88,6 +89,7 @@ export function checkRepo({ config, changes, specs = [], releases = [] }, opts =
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
checkCoverage(c, fm, active, config, warn, err);
|
|
92
|
+
checkLifecycleSequence(c, fm, err);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
// Aggregate checks only make sense over the whole repo.
|
|
@@ -288,10 +290,73 @@ function checkConflictMarkers(c, err) {
|
|
|
288
290
|
}
|
|
289
291
|
}
|
|
290
292
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
293
|
+
// Lifecycle history recorded under the previous contract (before
|
|
294
|
+
// `in-validation` became the universal human gate) stays readable without
|
|
295
|
+
// relaxing the current graph for new work (20260630-225210 CR3). Two bounded
|
|
296
|
+
// allowances, both covered by fixtures mirroring real history:
|
|
297
|
+
// - LEGACY_EDGES: closing/skipping edges old flows wrote literally.
|
|
298
|
+
// - Gap resync: old writers did not log every early move (draft → approved,
|
|
299
|
+
// approved → in-progress). An explicit `status:` origin may fast-forward the
|
|
300
|
+
// reconstruction, but only forward and only across pre-review states —
|
|
301
|
+
// implied review/validation origins always require the exact sequence.
|
|
302
|
+
const LEGACY_EDGES = new Set(['in-review→done', 'in-progress→done', 'draft→in-progress']);
|
|
303
|
+
const LEGACY_RESYNC_RANK = { draft: 0, approved: 1, 'in-progress': 2 };
|
|
304
|
+
const CANONICAL = new Set(CANONICAL_STATUSES);
|
|
305
|
+
|
|
306
|
+
// Replays `## Log` lifecycle events from `draft` against the transition graph
|
|
307
|
+
// in lifecycle.mjs. Stops at the first inconsistency to avoid cascading noise.
|
|
308
|
+
// Changes using non-canonical statuses are left alone — the graph cannot
|
|
309
|
+
// reason about states it does not model.
|
|
310
|
+
function checkLifecycleSequence(c, fm, err) {
|
|
311
|
+
const lines = (c.text ?? '').split('\n');
|
|
312
|
+
let inLog = false;
|
|
313
|
+
let current = 'draft';
|
|
314
|
+
let events = 0;
|
|
315
|
+
for (let i = 0; i < lines.length; i++) {
|
|
316
|
+
const line = lines[i];
|
|
317
|
+
if (/^##\s/.test(line)) {
|
|
318
|
+
inLog = /^##\s+Log\s*$/.test(line);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (!inLog) continue;
|
|
322
|
+
const event = parseLogEvent(line);
|
|
323
|
+
if (!event) continue;
|
|
324
|
+
if (!CANONICAL.has(event.from) || !CANONICAL.has(event.to)) return;
|
|
325
|
+
events += 1;
|
|
326
|
+
if (event.from !== current) {
|
|
327
|
+
const legacyGap =
|
|
328
|
+
event.explicit &&
|
|
329
|
+
LEGACY_RESYNC_RANK[current] !== undefined &&
|
|
330
|
+
LEGACY_RESYNC_RANK[event.from] !== undefined &&
|
|
331
|
+
LEGACY_RESYNC_RANK[event.from] > LEGACY_RESYNC_RANK[current];
|
|
332
|
+
if (!legacyGap) {
|
|
333
|
+
err(
|
|
334
|
+
c,
|
|
335
|
+
`Log line ${i + 1}: transition "${event.from} → ${event.to}" starts from "${event.from}" but the reconstructed status is "${current}"`,
|
|
336
|
+
);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
current = event.from;
|
|
340
|
+
}
|
|
341
|
+
if (!canTransition(event.from, event.to) && !LEGACY_EDGES.has(`${event.from}→${event.to}`)) {
|
|
342
|
+
err(c, `Log line ${i + 1}: invalid lifecycle transition "${event.from} → ${event.to}"`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
current = event.to;
|
|
346
|
+
}
|
|
347
|
+
if (events && CANONICAL.has(fm.status) && current !== fm.status) {
|
|
348
|
+
err(c, `Log reconstructs status "${current}" but frontmatter says "${fm.status}"`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Definition-of-Ready coverage: when `tdd` is on (default), a change whose type
|
|
353
|
+
// activates `## Specification` is checked in draft, approved and in-progress.
|
|
354
|
+
// Draft reports everything as warnings. In approved/in-progress, readiness
|
|
355
|
+
// defects (criterion missing Given/When/Then, reference to an unknown
|
|
356
|
+
// criterion, CR-bearing task without target+verification) are errors, while
|
|
357
|
+
// coverage gaps (uncovered criterion, non-support task without a CR) stay
|
|
358
|
+
// warnings. Only the Given/When/Then structure is machine-checkable; semantic
|
|
359
|
+
// test-grade quality remains the documenting agent's judgment.
|
|
295
360
|
function checkCoverage(c, fm, active, config, warn, err = () => {}) {
|
|
296
361
|
if (config?.tdd === false) return;
|
|
297
362
|
if (!active?.includes('specification')) return;
|
package/src/commands/context.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseChange } from '../change.mjs';
|
|
4
|
-
import { findChangeledgerDir } from '../config.mjs';
|
|
5
|
-
import { contractTemplatesDir } from '../paths.mjs';
|
|
4
|
+
import { findChangeledgerDir, loadConfig } from '../config.mjs';
|
|
5
|
+
import { contractTemplatesDir, packageRoot } from '../paths.mjs';
|
|
6
6
|
import { resolveChange } from '../repo.mjs';
|
|
7
7
|
|
|
8
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version;
|
|
9
|
+
const END_DELIMITER =
|
|
10
|
+
'===== CHANGELEDGER CONTEXT END — if this line is missing, the output was truncated: stop and re-run =====';
|
|
8
11
|
const MODES = ['implement', 'review', 'spec', 'release'];
|
|
9
12
|
const MODE_CONTEXT = {
|
|
10
|
-
implement: ['implement', 'delegation', '
|
|
11
|
-
review: ['review', '
|
|
13
|
+
implement: ['implement', 'delegation', 'handoff'],
|
|
14
|
+
review: ['review', 'handoff'],
|
|
12
15
|
spec: ['spec', 'delegation', 'readiness'],
|
|
13
16
|
release: ['release'],
|
|
14
17
|
};
|
|
@@ -23,30 +26,102 @@ const STATUS_CONTEXT = {
|
|
|
23
26
|
discarded: { mode: 'discarded', fragments: ['discarded'] },
|
|
24
27
|
};
|
|
25
28
|
const INCREMENTAL_NOTICE = `This incremental context extends the complete core context already read.
|
|
26
|
-
If the
|
|
29
|
+
If you have not read the core output through its \`CHANGELEDGER CONTEXT END\` line, stop and run \`changeledger context\` directly before proceeding.`;
|
|
27
30
|
|
|
28
31
|
function fragment(name) {
|
|
29
32
|
return fs.readFileSync(path.join(contractTemplatesDir, `${name}.md`), 'utf8').trim();
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
function
|
|
33
|
-
const
|
|
35
|
+
function beginDelimiter(mode, changeId) {
|
|
36
|
+
const change = changeId ? ` — change: #${changeId}` : '';
|
|
37
|
+
return `===== CHANGELEDGER CONTEXT BEGIN — mode: ${mode}${change} — v${VERSION} =====`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Resolved defaults so an agent never reads `.changeledger/config.yml` raw to
|
|
41
|
+
// discover the repo's effective policy. Keep these aligned with the shipped
|
|
42
|
+
// template config and the Definition of Ready contract.
|
|
43
|
+
const DEFAULT_LANGUAGE = 'en';
|
|
44
|
+
const DEFAULT_TDD = true;
|
|
45
|
+
|
|
46
|
+
function effectiveLanguage(config) {
|
|
47
|
+
return config?.language ?? DEFAULT_LANGUAGE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function effectiveTdd(config) {
|
|
51
|
+
const value = config?.tdd ?? DEFAULT_TDD;
|
|
52
|
+
return value ? 'on' : 'off';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The transversal policy line every composition anchors on: effective language
|
|
56
|
+
// and tdd with defaults already resolved.
|
|
57
|
+
function transversalPolicy(config) {
|
|
58
|
+
return `Effective policy: language=${effectiveLanguage(config)} — tdd=${effectiveTdd(config)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Type-specific policy for change-id contexts: adds review requirement and the
|
|
62
|
+
// active stages the type actually uses, so the agent does not infer them.
|
|
63
|
+
function changePolicyBlock(config, type) {
|
|
64
|
+
const typeConfig = config?.types?.[type] ?? {};
|
|
65
|
+
const reviewRequired = typeConfig.review_required === true ? 'yes' : 'no';
|
|
66
|
+
const stages = Array.isArray(typeConfig.stages) ? typeConfig.stages.join(', ') : '';
|
|
67
|
+
const lines = [
|
|
68
|
+
`${transversalPolicy(config)} — review_required(${type})=${reviewRequired}`,
|
|
69
|
+
`Active stages(${type})=${stages}`,
|
|
70
|
+
];
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// One line per local dependency (id, title, status); external `project:id`
|
|
75
|
+
// references stay references, never pretending local resolution.
|
|
76
|
+
function dependencyBlock(config, repoRoot, dependsOn, cwd) {
|
|
77
|
+
if (!Array.isArray(dependsOn) || dependsOn.length === 0) return undefined;
|
|
78
|
+
const lines = dependsOn.map((raw) => {
|
|
79
|
+
const dep = String(raw);
|
|
80
|
+
if (dep.includes(':')) return `- #${dep} — external reference (not resolved locally)`;
|
|
81
|
+
try {
|
|
82
|
+
const resolved = resolveChange(cwd, dep);
|
|
83
|
+
const { frontmatter } = parseChange(fs.readFileSync(resolved.file, 'utf8'));
|
|
84
|
+
return `- #${dep} — ${frontmatter.title} — ${frontmatter.status}`;
|
|
85
|
+
} catch {
|
|
86
|
+
return `- #${dep} — unresolved local dependency`;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return `## Dependencies\n\n${lines.join('\n')}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compose(mode, fragments, options = {}) {
|
|
93
|
+
const {
|
|
94
|
+
changeText,
|
|
95
|
+
incremental = true,
|
|
96
|
+
changeId = undefined,
|
|
97
|
+
policy = undefined,
|
|
98
|
+
dependencies = undefined,
|
|
99
|
+
} = options;
|
|
100
|
+
const sections = [beginDelimiter(mode, changeId)];
|
|
34
101
|
if (incremental) sections.push(INCREMENTAL_NOTICE);
|
|
102
|
+
if (policy) sections.push(policy);
|
|
35
103
|
sections.push(...fragments.map(fragment));
|
|
104
|
+
if (dependencies) sections.push(dependencies);
|
|
36
105
|
if (changeText) sections.push('---\n\n# Selected change\n', changeText.trim());
|
|
106
|
+
sections.push(END_DELIMITER);
|
|
37
107
|
return `${sections.join('\n\n')}\n`;
|
|
38
108
|
}
|
|
39
109
|
|
|
40
110
|
function requireRepo(cwd) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
111
|
+
const dir = findChangeledgerDir(cwd);
|
|
112
|
+
if (!dir) throw new Error('Not a ChangeLedger repo. Run `changeledger init` first.');
|
|
113
|
+
return dir;
|
|
44
114
|
}
|
|
45
115
|
|
|
46
116
|
export function buildContext(input, cwd = process.cwd()) {
|
|
47
|
-
requireRepo(cwd);
|
|
48
|
-
|
|
49
|
-
if (
|
|
117
|
+
const changeledgerDir = requireRepo(cwd);
|
|
118
|
+
const config = loadConfig(changeledgerDir);
|
|
119
|
+
if (!input) {
|
|
120
|
+
return compose('core', ['core'], { incremental: false, policy: transversalPolicy(config) });
|
|
121
|
+
}
|
|
122
|
+
if (MODES.includes(input)) {
|
|
123
|
+
return compose(input, MODE_CONTEXT[input], { policy: transversalPolicy(config) });
|
|
124
|
+
}
|
|
50
125
|
|
|
51
126
|
let resolved;
|
|
52
127
|
try {
|
|
@@ -58,10 +133,15 @@ export function buildContext(input, cwd = process.cwd()) {
|
|
|
58
133
|
}
|
|
59
134
|
|
|
60
135
|
const text = fs.readFileSync(resolved.file, 'utf8');
|
|
61
|
-
const status = parseChange(text).frontmatter
|
|
136
|
+
const { id, status, type, depends_on: dependsOn } = parseChange(text).frontmatter;
|
|
62
137
|
const selected = STATUS_CONTEXT[status];
|
|
63
138
|
if (!selected) throw new Error(`No context mapping for change status "${status}"`);
|
|
64
|
-
return compose(selected.mode, selected.fragments,
|
|
139
|
+
return compose(selected.mode, selected.fragments, {
|
|
140
|
+
changeText: text,
|
|
141
|
+
changeId: id,
|
|
142
|
+
policy: changePolicyBlock(config, type),
|
|
143
|
+
dependencies: dependencyBlock(config, resolved.repoRoot, dependsOn, cwd),
|
|
144
|
+
});
|
|
65
145
|
}
|
|
66
146
|
|
|
67
147
|
export function context(input, cwd = process.cwd(), output = console.log) {
|
package/src/commands/view.mjs
CHANGED
|
@@ -21,8 +21,28 @@ export {
|
|
|
21
21
|
} from '../viewer/domain.mjs';
|
|
22
22
|
export { createRequestListener, hostnameOf, isAuthorizedWrite, isLocalHost, staticFile };
|
|
23
23
|
|
|
24
|
+
// Explicit grammar: `.` selects local-only mode, a bare integer selects the
|
|
25
|
+
// port (default 4040), both may combine, and anything else is rejected
|
|
26
|
+
// instead of being silently ignored.
|
|
27
|
+
function parseViewArgs(args) {
|
|
28
|
+
let localOnly = false;
|
|
29
|
+
let port = 4040;
|
|
30
|
+
const unknown = [];
|
|
31
|
+
for (const a of args) {
|
|
32
|
+
if (a === '.') localOnly = true;
|
|
33
|
+
else if (/^\d+$/.test(a)) port = Number(a);
|
|
34
|
+
else unknown.push(a);
|
|
35
|
+
}
|
|
36
|
+
if (unknown.length) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Unknown argument(s) for "changeledger view": ${unknown.join(', ')} — usage: changeledger view [.] [port]`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return { localOnly, port };
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
export async function view(args = [], cwd = process.cwd()) {
|
|
25
|
-
const localOnly = args
|
|
45
|
+
const { localOnly, port: requestedPort } = parseViewArgs(args);
|
|
26
46
|
resolveProjects(cwd, localOnly); // fail fast if local mode outside a repo
|
|
27
47
|
|
|
28
48
|
const token = crypto.randomBytes(16).toString('hex');
|
|
@@ -31,10 +51,11 @@ export async function view(args = [], cwd = process.cwd()) {
|
|
|
31
51
|
server.headersTimeout = 10_000;
|
|
32
52
|
|
|
33
53
|
const host = '127.0.0.1';
|
|
34
|
-
const port = await listen(server, host,
|
|
54
|
+
const port = await listen(server, host, requestedPort);
|
|
35
55
|
const url = `http://${host}:${port}`;
|
|
36
56
|
console.log(`ChangeLedger viewer → ${url} (Ctrl+C to stop)`);
|
|
37
57
|
openBrowser(url);
|
|
58
|
+
return server;
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
function listen(server, host, port, attempts = 10) {
|
package/src/contract.mjs
CHANGED
|
@@ -36,12 +36,15 @@ const LEGACY_CONTRACT_HASHES = new Set([
|
|
|
36
36
|
|
|
37
37
|
export const REFERENCE = `${MARKER}
|
|
38
38
|
> [!IMPORTANT]
|
|
39
|
-
> This repo uses **ChangeLedger**.
|
|
40
|
-
> \`changeledger context\` directly
|
|
41
|
-
>
|
|
42
|
-
>
|
|
43
|
-
>
|
|
44
|
-
> ChangeLedger; do not proceed from memory.
|
|
39
|
+
> This repo uses **ChangeLedger**. Immediately after reading this file — before
|
|
40
|
+
> planning, investigating, or acting — run \`changeledger context\` directly and
|
|
41
|
+
> read its complete output through the \`CHANGELEDGER CONTEXT END\` line. If that
|
|
42
|
+
> line is missing, the output was truncated: stop and re-run the command
|
|
43
|
+
> directly, without pipes or filters. If the command is unavailable, stop and
|
|
44
|
+
> restore/install ChangeLedger; do not proceed from memory.
|
|
45
|
+
>
|
|
46
|
+
> Do not create or modify files without an authorized change; the core context
|
|
47
|
+
> defines the workflow, the task contexts, and the narrow operational exception.
|
|
45
48
|
`;
|
|
46
49
|
|
|
47
50
|
export const contractLink = (changeledgerDir) => path.join(changeledgerDir, 'AGENTS.md');
|
package/src/lifecycle.mjs
CHANGED
|
@@ -55,3 +55,19 @@ export function assertTransition(from, to, { type, reviewRequired = false } = {}
|
|
|
55
55
|
throw new Error(`${type} changes must be reviewed before validation — move to in-review first`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
// A lifecycle event recorded in `## Log`. `status:` lines carry an explicit
|
|
60
|
+
// origin; review/validation verdict lines imply it (the writer only emits them
|
|
61
|
+
// from in-review / in-validation). Non-lifecycle entries (owner, graduation,
|
|
62
|
+
// free notes) return null.
|
|
63
|
+
const LOG_EVENT =
|
|
64
|
+
/\*\*([^*]+)\*\*\s*—\s*(?:status:\s*([a-z-]+)\s*→\s*([a-z-]+)|(review)\s*→\s*([a-z-]+)|(validation)\s*→\s*([a-z-]+))/;
|
|
65
|
+
|
|
66
|
+
export function parseLogEvent(line) {
|
|
67
|
+
const m = line.match(LOG_EVENT);
|
|
68
|
+
if (!m) return null;
|
|
69
|
+
const at = m[1].trim();
|
|
70
|
+
if (m[2]) return { at, from: m[2], to: m[3], explicit: true };
|
|
71
|
+
if (m[4]) return { at, from: 'in-review', to: m[5], explicit: false };
|
|
72
|
+
return { at, from: 'in-validation', to: m[7], explicit: false };
|
|
73
|
+
}
|
package/src/metrics.mjs
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
// Everything is reconstructed from `created` plus the `## Log` status
|
|
4
4
|
// transitions. The viewer renders the result.
|
|
5
5
|
|
|
6
|
+
import { parseLogEvent } from './lifecycle.mjs';
|
|
7
|
+
|
|
6
8
|
const ACTIVE = ['approved', 'in-progress', 'in-review', 'in-validation', 'blocked'];
|
|
7
9
|
|
|
8
10
|
function logBody(change) {
|
|
9
11
|
return (change.stages ?? []).find((s) => s.key === 'log')?.body ?? '';
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
// Same event grammar as `changeledger check`'s sequence validation — one shared
|
|
15
|
+
// parser so metrics and validation cannot drift (20260630-225210 CR5).
|
|
12
16
|
function logTransition(line) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
return { at: m[1].trim(), state: m[2].trim() };
|
|
17
|
+
const event = parseLogEvent(line);
|
|
18
|
+
return event ? { at: event.at, state: event.to } : null;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
// The moment a change reached `done`: the last lifecycle transition to done, or
|
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
Documents under `.changeledger/` are the source of truth. Code is their
|
|
4
4
|
reflection. Work is planned and documented before code is written.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Read complete context before acting
|
|
7
7
|
|
|
8
|
-
Running `changeledger context` is discovery, not compliance by itself.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
modifying files.
|
|
8
|
+
Running `changeledger context` is discovery, not compliance by itself. Read the
|
|
9
|
+
complete output through the `CHANGELEDGER CONTEXT END` line, then follow the
|
|
10
|
+
current mode. If that line is missing, the output was truncated. Stop and re-run
|
|
11
|
+
the command directly, without pipes or filters, before creating or modifying
|
|
12
|
+
files.
|
|
14
13
|
|
|
15
14
|
1. Work starts with conversation. Read-only investigation may clarify a request,
|
|
16
15
|
but create no change or implementation artifact until there is enough clarity
|
|
@@ -27,8 +26,9 @@ modifying files.
|
|
|
27
26
|
human validation.
|
|
28
27
|
7. Stop at `in-validation`. The agent never accepts on the human's behalf.
|
|
29
28
|
8. After human acceptance, reload `changeledger context <id>` for the `done`
|
|
30
|
-
change, then graduate persistent truth
|
|
31
|
-
`changeledger graduate <id> --skip [reason]`; archive
|
|
29
|
+
change, then graduate persistent truth (a new spec is a two-step `--new`
|
|
30
|
+
then `--into`) or run `changeledger graduate <id> --skip [reason]`; archive
|
|
31
|
+
only after that decision.
|
|
32
32
|
|
|
33
33
|
If no approved or in-progress change applies, do not silently edit repository
|
|
34
34
|
files. Create or update a change, or ask the human whether a purely operational,
|
|
@@ -44,7 +44,8 @@ optional and preferred for error-prone operations such as timestamps, lifecycle
|
|
|
44
44
|
transitions and task markers.
|
|
45
45
|
|
|
46
46
|
Delegate only with a clear boundary and benefit. Each delegation prompt states
|
|
47
|
-
ownership, expected output and integration criterion;
|
|
47
|
+
at least ownership, expected output and integration criterion; the task context
|
|
48
|
+
carries the full prompt contract. Coding agents must know
|
|
48
49
|
they share the codebase and must not revert others' work. Do not over-shard or
|
|
49
50
|
overlap write surfaces without an explicit integration plan. Size the model to
|
|
50
51
|
the task's difficulty and risk.
|
|
@@ -81,8 +82,11 @@ later reconsideration needs a newly authorized change.
|
|
|
81
82
|
|
|
82
83
|
Valid modes: implement, review, spec, release.
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
context
|
|
85
|
+
Escalate to a mode before acting. Before documenting, run
|
|
86
|
+
`changeledger context spec`. Before executing, run `changeledger context
|
|
87
|
+
implement` or `changeledger context <change-id>`. Run each only after reading
|
|
88
|
+
the complete base output. Every mode and change-id context extends the core
|
|
89
|
+
context already read; it never repeats it.
|
|
86
90
|
|
|
87
91
|
- `changeledger context spec`: author or refine a change.
|
|
88
92
|
- `changeledger context implement`: execute an approved change.
|
|
@@ -98,4 +102,5 @@ Prefer structured CLI queries before scanning files:
|
|
|
98
102
|
- `changeledger graduate --pending`: find accepted changes whose graduation decision is unresolved.
|
|
99
103
|
|
|
100
104
|
Run `changeledger help` or `changeledger <command> --help` for exact CLI syntax.
|
|
101
|
-
Structure is always English
|
|
105
|
+
Structure is always English. Each context delivers the effective policy that
|
|
106
|
+
applies to its task, so you never read `.changeledger/config.yml` raw to operate.
|
|
@@ -37,6 +37,9 @@ evidence in the task instead of listing every possible manual phrase in config.
|
|
|
37
37
|
|
|
38
38
|
`changeledger check` reports missing Given/When/Then, uncovered or unknown CRs,
|
|
39
39
|
tasks without traceability and CR-bearing tasks without configured target and
|
|
40
|
-
verification.
|
|
41
|
-
`in-progress
|
|
40
|
+
verification. Every diagnostic is a warning in `draft`. In `approved` or
|
|
41
|
+
`in-progress`, readiness defects (missing Given/When/Then, unknown criterion
|
|
42
|
+
references, CR-bearing tasks without target and verification) are errors, while
|
|
43
|
+
coverage gaps (uncovered criteria, non-support tasks without a CR) stay
|
|
44
|
+
warnings. Truly operational `(support)` tasks are exempt; observable
|
|
42
45
|
implementation is not.
|