@onedotmint/charterleaf 0.2.1 → 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_SNIPPET.md +10 -25
- package/README.md +140 -44
- package/bin/charterleaf.js +0 -0
- package/lib/charterleaf.js +63 -110
- package/package.json +1 -1
- package/skills/charterleaf/SKILL.md +122 -31
- package/templates/change.md +10 -0
package/AGENTS_SNIPPET.md
CHANGED
|
@@ -1,29 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Project References
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use this file as the project navigation entry.
|
|
4
|
+
Document where authoritative information lives — project state,
|
|
5
|
+
validation, resources, history, and other project-specific sources.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- `specs/decisions/` — important architectural choices and revisit conditions.
|
|
8
|
-
- `specs/changes/` — lightweight active deltas.
|
|
9
|
-
- `specs/constitution.md` — project-wide guardrails only.
|
|
7
|
+
Durable constraints live under `specs/`. For work on a concrete
|
|
8
|
+
repository path:
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
charterleaf related <path>
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Do not create specs for temporary implementation details, current tasks, debugging notes, routine refactors, or trivial fixes with no durable implication. For a durable bug fix, use `Current` / `Expected` when useful and explicitly state what must `PRESERVE`.
|
|
18
|
-
|
|
19
|
-
After non-trivial implementation or debugging, consider whether durable project knowledge was discovered. If so, record only the durable result:
|
|
20
|
-
|
|
21
|
-
- observable behavior, contract, invariant, or non-goal → capability spec;
|
|
22
|
-
- long-lived implementation constraint, boundary, recurring pitfall, or required verification practice → engineering spec;
|
|
23
|
-
- important architectural choice, rationale, or revisit condition → decision.
|
|
24
|
-
|
|
25
|
-
If nothing durable was learned, do not modify specs. Do not record current task status, implementation progress, temporary debugging notes, one-off commands, session history, Todo items, or transient observations. Specs accumulate durable project knowledge, not development activity.
|
|
26
|
-
|
|
27
|
-
For meaningful durable behavior/contract changes, use one change file with only the needed `ADD`, `MODIFY`, `REMOVE`, and/or `PRESERVE` sections. After acceptance and implementation, merge durable results into living specs and delete the change file. Git is the history.
|
|
28
|
-
|
|
29
|
-
Charterleaf is a knowledge layer, not a workflow layer. It must not manage tasks, planning, agents, execution, review, memory, compression, semantic search, code/spec verification, release lifecycle, or Git history.
|
|
12
|
+
Read authoritative owners progressively. If the current source is
|
|
13
|
+
sufficient, stop. Keep one owner per fact and point to it elsewhere
|
|
14
|
+
instead of copying the same fact into multiple documents.
|
package/README.md
CHANGED
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
A tiny specification layer for coding agents.
|
|
4
4
|
|
|
5
|
+
Given a concrete repository path, Charterleaf answers one question:
|
|
6
|
+
|
|
7
|
+
> Which durable constraints should I read before touching this code?
|
|
8
|
+
|
|
5
9
|
Durable project intent, deterministic routing, and structural linting —
|
|
6
10
|
without workflow orchestration.
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
## Two layers
|
|
9
13
|
|
|
10
|
-
Charterleaf
|
|
14
|
+
Charterleaf sits beside a project's own navigation, not on top of it.
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
- **A project reference protocol** — a tracked `AGENTS.md` entry plus
|
|
17
|
+
the project's own documents — tells an agent *where* authoritative
|
|
18
|
+
information lives: current state, validation, resources, history,
|
|
19
|
+
source roots.
|
|
20
|
+
- **Charterleaf** tells an agent *which durable constraints* matter when
|
|
21
|
+
work lands on a specific repository path.
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
Charterleaf complements project navigation; it does not replace
|
|
24
|
+
project-owned sources of state, evidence, or workflow. It stores and
|
|
25
|
+
routes durable constraints; it never becomes the owner of a fact that
|
|
26
|
+
belongs to a project file.
|
|
19
27
|
|
|
20
28
|
## Install
|
|
21
29
|
|
|
@@ -35,21 +43,87 @@ No Pi extension or Pi runtime dependency is used.
|
|
|
35
43
|
|
|
36
44
|
## CLI
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
Charterleaf exposes exactly two deterministic commands. Run them from a
|
|
47
|
+
repository root containing `specs/`:
|
|
39
48
|
|
|
40
49
|
```bash
|
|
41
|
-
charterleaf
|
|
42
|
-
charterleaf related src/auth/session.ts
|
|
50
|
+
charterleaf related <path>
|
|
43
51
|
charterleaf lint
|
|
44
52
|
```
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
There is no `map` command. Discovery is handled by a scoped miss (below)
|
|
55
|
+
and by the project's own reference protocol.
|
|
56
|
+
|
|
57
|
+
### related \<path\>
|
|
58
|
+
|
|
59
|
+
Routes the supplied repo-relative path (`/` or `\` separators) to durable
|
|
60
|
+
constraints:
|
|
61
|
+
|
|
62
|
+
- `specs/constitution.md` is **Global**: returned whenever it exists,
|
|
63
|
+
regardless of frontmatter. It needs no `id` and no `applies_to`.
|
|
64
|
+
- `capabilities/` and `engineering/` specs are **Scoped**: returned when
|
|
65
|
+
any `applies_to` glob matches.
|
|
66
|
+
- Active changes whose `affects` reference a matched spec id are appended
|
|
67
|
+
under `Active changes`.
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
Global
|
|
71
|
+
specs/constitution.md
|
|
72
|
+
|
|
73
|
+
Scoped
|
|
74
|
+
specs/capabilities/auth.md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`applies_to` is a deterministic routing hint, not a completeness proof.
|
|
78
|
+
"No match" only ever means "no living spec's `applies_to` glob matched
|
|
79
|
+
this path" — never "no constraints apply". When nothing matches, the
|
|
80
|
+
command states that explicitly and lists available living specs by `id`,
|
|
81
|
+
path, and `applies_to` only, so discovery stays possible without reading
|
|
82
|
+
every spec:
|
|
47
83
|
|
|
48
|
-
|
|
84
|
+
```text
|
|
85
|
+
Global
|
|
86
|
+
specs/constitution.md
|
|
87
|
+
|
|
88
|
+
Scoped
|
|
89
|
+
No applies_to glob matched this path.
|
|
90
|
+
|
|
91
|
+
Available living specs
|
|
92
|
+
auth.session
|
|
93
|
+
specs/capabilities/auth.md
|
|
94
|
+
applies_to: src/auth/**
|
|
95
|
+
|
|
96
|
+
engineering.backend
|
|
97
|
+
specs/engineering/backend.md
|
|
98
|
+
applies_to: src/**/*.ts
|
|
99
|
+
```
|
|
49
100
|
|
|
50
|
-
`
|
|
101
|
+
`related` never inspects the filesystem for the supplied path; it accepts
|
|
102
|
+
the caller's repo-relative string. A scoped miss is success (exit code
|
|
103
|
+
0), not an error.
|
|
51
104
|
|
|
52
|
-
|
|
105
|
+
### lint
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
charterleaf lint
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`lint` performs mechanical structural checks only: frontmatter shape,
|
|
112
|
+
living Spec IDs, required `applies_to` on living specs, duplicate spec
|
|
113
|
+
and requirement IDs, change references, static `ADD` / `MODIFY` /
|
|
114
|
+
`REMOVE` references, changes whose `ADD` / `MODIFY` / `REMOVE` sections
|
|
115
|
+
are all empty (HTML comments do not count), and conflicting active
|
|
116
|
+
changes that touch the same requirement ID. Success prints
|
|
117
|
+
`0 structural errors`.
|
|
118
|
+
|
|
119
|
+
`lint` does **not** verify:
|
|
120
|
+
|
|
121
|
+
- semantic correctness;
|
|
122
|
+
- code/spec agreement;
|
|
123
|
+
- routing coverage;
|
|
124
|
+
- project compliance;
|
|
125
|
+
- whether a change is actually complete;
|
|
126
|
+
- whether an Agent read the specs.
|
|
53
127
|
|
|
54
128
|
## Project knowledge
|
|
55
129
|
|
|
@@ -62,53 +136,75 @@ specs/
|
|
|
62
136
|
└── changes/
|
|
63
137
|
```
|
|
64
138
|
|
|
65
|
-
- `constitution.md` — rare project-wide
|
|
66
|
-
|
|
67
|
-
- `
|
|
68
|
-
-
|
|
69
|
-
- `
|
|
139
|
+
- `constitution.md` — rare project-wide durable boundaries. Global
|
|
140
|
+
constraint; not routed by `applies_to`.
|
|
141
|
+
- `capabilities/` — durable observable behavior, contracts, invariants,
|
|
142
|
+
non-goals.
|
|
143
|
+
- `engineering/` — durable implementation boundaries, long-lived
|
|
144
|
+
constraints, recurring pitfalls, verification practice.
|
|
145
|
+
- `decisions/` — important architectural rationale and revisit
|
|
146
|
+
conditions.
|
|
147
|
+
- `changes/` — optional staging for an accepted but not-yet-implemented
|
|
148
|
+
contract. See below.
|
|
70
149
|
|
|
71
|
-
|
|
150
|
+
## Writing specs
|
|
72
151
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
152
|
+
A spec records a constraint whose absence would plausibly cause a future
|
|
153
|
+
developer or agent to make a wrong decision. It should be non-obvious,
|
|
154
|
+
durable, **already true**, and mistake-preventing. Good fits: contracts,
|
|
155
|
+
invariants, architecture boundaries, non-goals, recurring pitfalls,
|
|
156
|
+
accepted rationale, verification constraints.
|
|
157
|
+
|
|
158
|
+
Do not write specs for current state, future design, aspirations, todos,
|
|
159
|
+
implementation plans, session notes, progress, temporary debugging,
|
|
160
|
+
navigation, or history. What you hope to become is a plan, not a living
|
|
161
|
+
spec.
|
|
76
162
|
|
|
77
|
-
|
|
163
|
+
## changes/ is optional staging
|
|
78
164
|
|
|
79
|
-
|
|
165
|
+
Use `specs/changes/<name>.md` only when an accepted future contract must
|
|
166
|
+
temporarily coexist with current living reality — for example when
|
|
167
|
+
implementation happens in another session or by another agent, human
|
|
168
|
+
review comes first, or a multi-step migration is underway. A change must
|
|
169
|
+
contain at least one of `ADD`, `MODIFY`, `REMOVE`; `PRESERVE` alone is
|
|
170
|
+
not a change.
|
|
80
171
|
|
|
81
|
-
|
|
172
|
+
Otherwise, implement, verify, and update the living spec if durable truth
|
|
173
|
+
changed. After an accepted change is implemented, merge the durable
|
|
174
|
+
result into living specs and delete the change file. Git keeps history.
|
|
82
175
|
|
|
83
|
-
|
|
176
|
+
## Agent conventions
|
|
84
177
|
|
|
85
|
-
|
|
178
|
+
The complete Agent convention lives in
|
|
179
|
+
[`skills/charterleaf/SKILL.md`](skills/charterleaf/SKILL.md): adoption
|
|
180
|
+
gate, progressive retrieval, `related` and `lint` semantics, spec
|
|
181
|
+
writing discipline, and durable learning.
|
|
86
182
|
|
|
87
|
-
|
|
183
|
+
For agents that read project instructions, the minimal reference
|
|
184
|
+
bootstrap is [`AGENTS_SNIPPET.md`](AGENTS_SNIPPET.md).
|
|
88
185
|
|
|
89
|
-
|
|
186
|
+
Charterleaf core does not depend on Pi. Any coding agent that can read
|
|
187
|
+
Markdown and run shell commands can use it.
|
|
90
188
|
|
|
91
189
|
## Boundaries
|
|
92
190
|
|
|
93
191
|
Charterleaf has no:
|
|
94
192
|
|
|
95
|
-
- planning
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- daemon or background watcher
|
|
104
|
-
- plugin/extension runtime
|
|
105
|
-
- Git/history abstraction
|
|
193
|
+
- project state, tasks, planning, or workflow;
|
|
194
|
+
- memory, session lifecycle, evidence, or history management;
|
|
195
|
+
- scope registry, owner registry, or project bootstrap command;
|
|
196
|
+
- semantic search, embeddings, or code/spec semantic verification;
|
|
197
|
+
- repository coverage analysis or filesystem awareness in `related`;
|
|
198
|
+
- database, index, cache, daemon, or watcher;
|
|
199
|
+
- plugin/extension runtime;
|
|
200
|
+
- Git abstraction.
|
|
106
201
|
|
|
107
|
-
The
|
|
202
|
+
The two CLI commands are helpers, not workflow gates.
|
|
108
203
|
|
|
109
204
|
## Development
|
|
110
205
|
|
|
111
|
-
Requires Node.js 20 or newer. There are no runtime or development
|
|
206
|
+
Requires Node.js 20 or newer. There are no runtime or development
|
|
207
|
+
dependencies and no build step.
|
|
112
208
|
|
|
113
209
|
```bash
|
|
114
210
|
npm test
|
package/bin/charterleaf.js
CHANGED
|
File without changes
|
package/lib/charterleaf.js
CHANGED
|
@@ -163,8 +163,6 @@ function related(repoPath, cwd = process.cwd(), streams = {}) {
|
|
|
163
163
|
|
|
164
164
|
const target = normalizeRepoPath(repoPath);
|
|
165
165
|
const documents = [];
|
|
166
|
-
const matches = [];
|
|
167
|
-
const matchedIds = new Set();
|
|
168
166
|
const errors = [];
|
|
169
167
|
|
|
170
168
|
for (const filePath of walkMarkdown(specRoot)) {
|
|
@@ -176,23 +174,24 @@ function related(repoPath, cwd = process.cwd(), streams = {}) {
|
|
|
176
174
|
errors.push(`ERROR ${display}: ${error.message}`);
|
|
177
175
|
continue;
|
|
178
176
|
}
|
|
179
|
-
documents.push({
|
|
177
|
+
documents.push({
|
|
178
|
+
filePath,
|
|
179
|
+
display,
|
|
180
|
+
relative: path.relative(specRoot, filePath).replaceAll('\\', '/'),
|
|
181
|
+
frontmatter: parsed.frontmatter || {},
|
|
182
|
+
});
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const patterns = frontmatter.applies_to;
|
|
185
|
+
const livingSpecs = documents.filter((document) => isLivingSpec(document.filePath, specRoot));
|
|
186
|
+
const scoped = [];
|
|
187
|
+
for (const document of livingSpecs) {
|
|
188
|
+
const patterns = document.frontmatter.applies_to;
|
|
189
|
+
if (patterns === undefined) continue;
|
|
186
190
|
if (!Array.isArray(patterns) || !patterns.every((item) => typeof item === 'string')) {
|
|
187
|
-
errors.push(`ERROR ${display}: applies_to must be a list of paths`);
|
|
191
|
+
errors.push(`ERROR ${document.display}: applies_to must be a list of paths`);
|
|
188
192
|
continue;
|
|
189
193
|
}
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
matches.push(display);
|
|
193
|
-
if (typeof frontmatter.id === 'string' && frontmatter.id.trim()) {
|
|
194
|
-
matchedIds.add(frontmatter.id.trim());
|
|
195
|
-
}
|
|
194
|
+
if (patterns.some((pattern) => globMatches(pattern, target))) scoped.push(document);
|
|
196
195
|
}
|
|
197
196
|
|
|
198
197
|
if (errors.length) {
|
|
@@ -200,6 +199,12 @@ function related(repoPath, cwd = process.cwd(), streams = {}) {
|
|
|
200
199
|
return 2;
|
|
201
200
|
}
|
|
202
201
|
|
|
202
|
+
const matchedIds = new Set();
|
|
203
|
+
for (const document of scoped) {
|
|
204
|
+
const id = typeof document.frontmatter.id === 'string' ? document.frontmatter.id.trim() : '';
|
|
205
|
+
if (id) matchedIds.add(id);
|
|
206
|
+
}
|
|
207
|
+
|
|
203
208
|
const activeChanges = [];
|
|
204
209
|
if (matchedIds.size) {
|
|
205
210
|
const changeRoot = path.join(specRoot, 'changes');
|
|
@@ -213,93 +218,40 @@ function related(repoPath, cwd = process.cwd(), streams = {}) {
|
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
|
|
216
|
-
|
|
221
|
+
scoped.sort((left, right) => (left.display < right.display ? -1 : left.display > right.display ? 1 : 0));
|
|
217
222
|
activeChanges.sort();
|
|
218
|
-
const output = [...matches];
|
|
219
|
-
for (const change of activeChanges) {
|
|
220
|
-
if (!output.includes(change)) output.push(change);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
for (const match of output) write(stdout, match);
|
|
224
|
-
if (!output.length) write(stdout, 'No related specs.');
|
|
225
|
-
return 0;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function writeLivingSpecs(stdout, documents) {
|
|
229
|
-
for (const document of documents) {
|
|
230
|
-
const id = typeof document.frontmatter.id === 'string' ? document.frontmatter.id.trim() : '';
|
|
231
|
-
const appliesTo = Array.isArray(document.frontmatter.applies_to)
|
|
232
|
-
? document.frontmatter.applies_to.join(', ')
|
|
233
|
-
: String(document.frontmatter.applies_to || '');
|
|
234
|
-
write(stdout, ` ${id || '(missing id)'}`);
|
|
235
|
-
write(stdout, ` ${document.display}`);
|
|
236
|
-
write(stdout, ` applies_to: ${appliesTo}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function map(cwd = process.cwd(), streams = {}) {
|
|
241
|
-
const stdout = streams.stdout || process.stdout;
|
|
242
|
-
const stderr = streams.stderr || process.stderr;
|
|
243
|
-
const specRoot = path.join(cwd, 'specs');
|
|
244
|
-
if (!fs.existsSync(specRoot) || !fs.statSync(specRoot).isDirectory()) {
|
|
245
|
-
write(stderr, 'ERROR specs/: not found');
|
|
246
|
-
return 2;
|
|
247
|
-
}
|
|
248
223
|
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
filePath,
|
|
257
|
-
display,
|
|
258
|
-
frontmatter: frontmatter || {},
|
|
259
|
-
relative: path.relative(specRoot, filePath).replaceAll('\\', '/'),
|
|
260
|
-
});
|
|
261
|
-
} catch (error) {
|
|
262
|
-
errors.push(`ERROR ${display}: ${error.message}`);
|
|
263
|
-
}
|
|
224
|
+
const blocks = [];
|
|
225
|
+
const constitution = documents.find((document) => document.relative === 'constitution.md');
|
|
226
|
+
if (constitution) blocks.push(['Global', [` ${constitution.display}`]]);
|
|
227
|
+
if (scoped.length) {
|
|
228
|
+
blocks.push(['Scoped', scoped.map((document) => ` ${document.display}`)]);
|
|
229
|
+
} else {
|
|
230
|
+
blocks.push(['Scoped', [' No applies_to glob matched this path.']]);
|
|
264
231
|
}
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
return 2;
|
|
232
|
+
if (activeChanges.length) {
|
|
233
|
+
blocks.push(['Active changes', activeChanges.map((change) => ` ${change}`)]);
|
|
268
234
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
235
|
+
if (!scoped.length && livingSpecs.length) {
|
|
236
|
+
const lines = [];
|
|
237
|
+
livingSpecs.forEach((document, index) => {
|
|
238
|
+
if (index) lines.push('');
|
|
239
|
+
const id = typeof document.frontmatter.id === 'string' ? document.frontmatter.id.trim() : '';
|
|
240
|
+
const appliesTo = Array.isArray(document.frontmatter.applies_to)
|
|
241
|
+
? document.frontmatter.applies_to.join(', ')
|
|
242
|
+
: String(document.frontmatter.applies_to ?? '');
|
|
243
|
+
lines.push(` ${id || '(missing id)'}`);
|
|
244
|
+
lines.push(` ${document.display}`);
|
|
245
|
+
lines.push(` applies_to: ${appliesTo}`);
|
|
246
|
+
});
|
|
247
|
+
blocks.push(['Available living specs', lines]);
|
|
281
248
|
}
|
|
282
249
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
write(stdout, '');
|
|
289
|
-
write(stdout, 'Engineering');
|
|
290
|
-
writeLivingSpecs(stdout, engineering);
|
|
291
|
-
write(stdout, '');
|
|
292
|
-
write(stdout, 'Decisions');
|
|
293
|
-
for (const document of decisions) write(stdout, ` ${document.display}`);
|
|
294
|
-
write(stdout, '');
|
|
295
|
-
write(stdout, 'Active changes');
|
|
296
|
-
for (const document of changes) {
|
|
297
|
-
const affects = Array.isArray(document.frontmatter.affects)
|
|
298
|
-
? document.frontmatter.affects.join(', ')
|
|
299
|
-
: String(document.frontmatter.affects || '');
|
|
300
|
-
write(stdout, ` ${document.display}`);
|
|
301
|
-
write(stdout, ` affects: ${affects}`);
|
|
302
|
-
}
|
|
250
|
+
blocks.forEach(([title, lines], index) => {
|
|
251
|
+
if (index) write(stdout, '');
|
|
252
|
+
write(stdout, title);
|
|
253
|
+
for (const line of lines) write(stdout, line);
|
|
254
|
+
});
|
|
303
255
|
return 0;
|
|
304
256
|
}
|
|
305
257
|
|
|
@@ -317,6 +269,10 @@ function requirementDefinitions(text) {
|
|
|
317
269
|
return found;
|
|
318
270
|
}
|
|
319
271
|
|
|
272
|
+
function stripHtmlComments(text) {
|
|
273
|
+
return String(text).replace(/<!--[\s\S]*?(?:-->|$)/g, '');
|
|
274
|
+
}
|
|
275
|
+
|
|
320
276
|
function changeSections(body) {
|
|
321
277
|
const sections = new Map();
|
|
322
278
|
let current = null;
|
|
@@ -454,6 +410,14 @@ function lint(cwd = process.cwd(), streams = {}) {
|
|
|
454
410
|
const display = relativeDisplay(filePath, cwd);
|
|
455
411
|
const { frontmatter: rawFrontmatter, body } = parsed.get(filePath);
|
|
456
412
|
const frontmatter = rawFrontmatter || {};
|
|
413
|
+
const sections = changeSections(body);
|
|
414
|
+
const hasDelta = ['ADD', 'MODIFY', 'REMOVE'].some(
|
|
415
|
+
(section) => stripHtmlComments(sections[section] || '').trim().length > 0,
|
|
416
|
+
);
|
|
417
|
+
if (!hasDelta) {
|
|
418
|
+
errors.push([display, 'change must contain at least one ADD, MODIFY, or REMOVE section']);
|
|
419
|
+
}
|
|
420
|
+
|
|
457
421
|
const affects = frontmatter.affects;
|
|
458
422
|
if (!Array.isArray(affects) || !affects.every((item) => typeof item === 'string' && item.length > 0)) {
|
|
459
423
|
continue;
|
|
@@ -470,7 +434,6 @@ function lint(cwd = process.cwd(), streams = {}) {
|
|
|
470
434
|
}
|
|
471
435
|
}
|
|
472
436
|
|
|
473
|
-
const sections = changeSections(body);
|
|
474
437
|
for (const section of ['ADD', 'MODIFY', 'REMOVE']) {
|
|
475
438
|
for (const reqId of new Set(allRequirementIds(sections[section] || ''))) {
|
|
476
439
|
if (!changeTouches.has(reqId)) changeTouches.set(reqId, new Map());
|
|
@@ -515,10 +478,10 @@ function lint(cwd = process.cwd(), streams = {}) {
|
|
|
515
478
|
});
|
|
516
479
|
for (const [display, message] of errors) write(stdout, `ERROR ${display}: ${message}`);
|
|
517
480
|
if (errors.length) {
|
|
518
|
-
write(stdout, `${errors.length} errors`);
|
|
481
|
+
write(stdout, `${errors.length} structural errors`);
|
|
519
482
|
return 1;
|
|
520
483
|
}
|
|
521
|
-
write(stdout, '0 errors');
|
|
484
|
+
write(stdout, '0 structural errors');
|
|
522
485
|
return 0;
|
|
523
486
|
}
|
|
524
487
|
|
|
@@ -527,9 +490,8 @@ function helpText() {
|
|
|
527
490
|
'Usage: charterleaf <command> [args]',
|
|
528
491
|
'',
|
|
529
492
|
'Commands:',
|
|
530
|
-
'
|
|
531
|
-
'
|
|
532
|
-
' lint check Charterleaf structural conventions',
|
|
493
|
+
' related <path> find durable constraints matching a repo-relative path',
|
|
494
|
+
' lint report structural errors in specs/',
|
|
533
495
|
].join('\n');
|
|
534
496
|
}
|
|
535
497
|
|
|
@@ -548,14 +510,6 @@ function main(argv = process.argv.slice(2), cwd = process.cwd(), streams = {}) {
|
|
|
548
510
|
if (!argv.length) return usageError('a command is required', streams);
|
|
549
511
|
|
|
550
512
|
const [command, ...rest] = argv;
|
|
551
|
-
if (command === 'map') {
|
|
552
|
-
if (rest.length === 1 && (rest[0] === '--help' || rest[0] === '-h')) {
|
|
553
|
-
write(streams.stdout || process.stdout, 'Usage: charterleaf map');
|
|
554
|
-
return 0;
|
|
555
|
-
}
|
|
556
|
-
if (rest.length) return usageError('map takes no arguments', streams);
|
|
557
|
-
return map(path.resolve(cwd), streams);
|
|
558
|
-
}
|
|
559
513
|
if (command === 'related') {
|
|
560
514
|
if (rest.length === 1 && (rest[0] === '--help' || rest[0] === '-h')) {
|
|
561
515
|
write(streams.stdout || process.stdout, 'Usage: charterleaf related <path>');
|
|
@@ -581,7 +535,6 @@ module.exports = {
|
|
|
581
535
|
globMatches,
|
|
582
536
|
lint,
|
|
583
537
|
main,
|
|
584
|
-
map,
|
|
585
538
|
normalizeRepoPath,
|
|
586
539
|
parseFrontmatter,
|
|
587
540
|
related,
|
package/package.json
CHANGED
|
@@ -5,54 +5,145 @@ description: A tiny specification layer for durable project intent and constrain
|
|
|
5
5
|
|
|
6
6
|
# Charterleaf
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Adoption gate
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Before using Charterleaf in a repository, check whether `specs/` exists.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
charterleaf related <path>
|
|
14
|
-
```
|
|
12
|
+
If no `specs/` directory exists:
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
- do not run Charterleaf;
|
|
15
|
+
- do not create `specs/`;
|
|
16
|
+
- do not recommend adoption automatically;
|
|
17
|
+
- continue using the repository's normal instructions.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Charterleaf is opt-in.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
charterleaf map
|
|
22
|
-
```
|
|
21
|
+
## Boundary
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
Charterleaf answers one question:
|
|
25
24
|
|
|
26
|
-
For
|
|
25
|
+
> For this repository path, which durable constraints should I read?
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
A project's own reference protocol — its `AGENTS.md` entry and the
|
|
28
|
+
project-owned documents it points to — answers a different question:
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
Constitution > Active Change > Living Spec > Current Code
|
|
32
|
-
```
|
|
30
|
+
> Where do this project's authoritative facts live?
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
Charterleaf owns only: spec taxonomy, durable constraint routing
|
|
33
|
+
(`related`), and structural lint (`lint`).
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
Charterleaf does not own: current project state, tasks, workflow,
|
|
36
|
+
planning, memory, evidence, history, session lifecycle, project-wide
|
|
37
|
+
consistency, semantic code/spec verification, or repository state
|
|
38
|
+
synchronization.
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
## Progressive retrieval
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
1. **Identify the need.** Orientation, current state, evidence,
|
|
43
|
+
validation, resources, implementation detail, or durable constraint?
|
|
44
|
+
2. **Locate the owner.** If the owner is known, read it directly. If not,
|
|
45
|
+
read the project `AGENTS.md` / project instructions. Do not scan the
|
|
46
|
+
whole repository's Markdown first.
|
|
47
|
+
3. **Enough?** If the authoritative information you have is sufficient,
|
|
48
|
+
stop reading. Do not keep reading for completeness.
|
|
49
|
+
4. **Expand progressively.** Only when information is insufficient, the
|
|
50
|
+
owner is unclear, sources conflict, the work area is unknown, or
|
|
51
|
+
verification is needed — and only one layer at a time.
|
|
52
|
+
5. **Concrete path.** Once work lands on a concrete repository path, run
|
|
53
|
+
`charterleaf related <path>` and read the returned constraints.
|
|
54
|
+
6. **Work.** Normal implementation, learning, design, or debugging. The
|
|
55
|
+
reference protocol does not manage workflow.
|
|
56
|
+
7. **Write back.** Changed current fact → its single owner. New evidence
|
|
57
|
+
→ the evidence owner. New or changed durable constraint → `specs/`.
|
|
58
|
+
Historical event → project history or Git. Future plan → planning
|
|
59
|
+
docs. Never copy one fact into several files: one owner per fact,
|
|
60
|
+
other documents point to it.
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
## related semantics
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
`charterleaf related <path>`:
|
|
47
65
|
|
|
48
|
-
|
|
66
|
+
- always returns `specs/constitution.md` as Global when it exists;
|
|
67
|
+
- routes `capabilities/` and `engineering/` specs whose `applies_to`
|
|
68
|
+
glob matches the path;
|
|
69
|
+
- appends active changes whose `affects` reference a returned spec id.
|
|
49
70
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
`applies_to` is a **routing hint**, not a completeness proof. The message
|
|
72
|
+
`No applies_to glob matched this path.` means exactly that — it never
|
|
73
|
+
means "no constraints apply". On a scoped miss the command lists
|
|
74
|
+
available living specs by `id`, path, and `applies_to` only. Use that
|
|
75
|
+
list to find the right area, then rerun with the concrete path. The
|
|
76
|
+
command never prints spec bodies, ranks relevance, or guesses.
|
|
53
77
|
|
|
54
|
-
|
|
78
|
+
## lint semantics
|
|
55
79
|
|
|
56
|
-
|
|
80
|
+
`charterleaf lint` reports mechanical structural problems only:
|
|
81
|
+
frontmatter shape, living-spec ids and `applies_to`, duplicate spec and
|
|
82
|
+
requirement ids, change references, changes whose `ADD` / `MODIFY` /
|
|
83
|
+
`REMOVE` sections are all empty (HTML comments do not count), and
|
|
84
|
+
conflicting active changes.
|
|
57
85
|
|
|
58
|
-
|
|
86
|
+
It does **not** verify semantic correctness, code/spec agreement,
|
|
87
|
+
routing coverage, project compliance, whether a change is complete, or
|
|
88
|
+
whether an Agent read the specs. Success prints `0 structural errors`.
|
|
89
|
+
Run it after editing specs.
|
|
90
|
+
|
|
91
|
+
## Spec writing discipline
|
|
92
|
+
|
|
93
|
+
A spec records a constraint whose absence would plausibly cause a future
|
|
94
|
+
developer or agent to make a wrong decision. It should be non-obvious,
|
|
95
|
+
durable, **already true**, and mistake-preventing.
|
|
96
|
+
|
|
97
|
+
Good fits: contracts, invariants, architecture boundaries, non-goals,
|
|
98
|
+
recurring pitfalls, accepted architectural rationale, verification
|
|
99
|
+
constraints.
|
|
100
|
+
|
|
101
|
+
Not specs: current state, future design, aspirations, todos,
|
|
102
|
+
implementation plans, session notes, progress, temporary debugging,
|
|
103
|
+
navigation, history. "Already true" matters — what you hope to become is
|
|
104
|
+
a plan, not a living spec.
|
|
105
|
+
|
|
106
|
+
Place a constraint where it belongs:
|
|
107
|
+
|
|
108
|
+
- rare project-wide durable boundary → `specs/constitution.md`;
|
|
109
|
+
- observable behavior, contract, invariant, or non-goal →
|
|
110
|
+
`specs/capabilities/`;
|
|
111
|
+
- implementation boundary, long-lived constraint, recurring pitfall, or
|
|
112
|
+
verification practice → `specs/engineering/`;
|
|
113
|
+
- important architectural rationale and revisit condition →
|
|
114
|
+
`specs/decisions/`.
|
|
115
|
+
|
|
116
|
+
Keep specs compressed. Prefer durable contracts and pitfalls over
|
|
117
|
+
commentary, and never invent aspirational standards.
|
|
118
|
+
|
|
119
|
+
## changes/ is optional staging
|
|
120
|
+
|
|
121
|
+
Use `specs/changes/<name>.md` only when an accepted future contract must
|
|
122
|
+
temporarily coexist with current living reality — for example when
|
|
123
|
+
implementation happens in another session or by another agent, human
|
|
124
|
+
review comes first, or a multi-step migration is underway. A change must
|
|
125
|
+
contain at least one of `ADD`, `MODIFY`, `REMOVE`; `PRESERVE` alone is
|
|
126
|
+
not a change.
|
|
127
|
+
|
|
128
|
+
Otherwise: implement, verify, and update the living spec if durable truth
|
|
129
|
+
changed. After an accepted change is implemented, merge the durable
|
|
130
|
+
result into living specs and delete the change file. Git keeps history.
|
|
131
|
+
|
|
132
|
+
## Durable learning
|
|
133
|
+
|
|
134
|
+
After non-trivial implementation or debugging, ask once: did this work
|
|
135
|
+
reveal durable knowledge whose absence could cause a future mistake?
|
|
136
|
+
|
|
137
|
+
If not, change nothing. If yes, record only the durable constraint in
|
|
138
|
+
the matching category above.
|
|
139
|
+
|
|
140
|
+
Never record current progress, temporary debug notes, one-off commands,
|
|
141
|
+
session history, todos, or transient observations. Specs accumulate
|
|
142
|
+
durable constraints, not development activity.
|
|
143
|
+
|
|
144
|
+
## Non-goals
|
|
145
|
+
|
|
146
|
+
Do not use Charterleaf to manage tasks, planning, workflow, memory,
|
|
147
|
+
evidence, history, repository state, or scope registries. Do not ask it
|
|
148
|
+
to verify semantics, coverage, or project compliance. It finds
|
|
149
|
+
constraints; it does not manage the project.
|
package/templates/change.md
CHANGED
|
@@ -5,6 +5,16 @@ affects:
|
|
|
5
5
|
|
|
6
6
|
# <Change name>
|
|
7
7
|
|
|
8
|
+
<!-- Optional staging. Use only when an accepted future contract must
|
|
9
|
+
coexist with current living reality — implementation in another session or
|
|
10
|
+
by another agent, human review first, or a multi-step migration.
|
|
11
|
+
|
|
12
|
+
A change must contain at least one of ADD, MODIFY, or REMOVE.
|
|
13
|
+
PRESERVE alone is not a change.
|
|
14
|
+
|
|
15
|
+
After implementation, merge the durable result into living specs and
|
|
16
|
+
delete this file. Git keeps history. -->
|
|
17
|
+
|
|
8
18
|
## Intent
|
|
9
19
|
|
|
10
20
|
<What intentional behavior/contract change is being made, and why?>
|