@latysh-development/monolith 0.1.0-alpha.3 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/brief/cli.d.ts.map +1 -1
- package/dist/brief/cli.js +8 -2
- package/dist/brief/collect.d.ts.map +1 -1
- package/dist/brief/collect.js +11 -3
- package/dist/brief/freshness.d.ts +17 -2
- package/dist/brief/freshness.d.ts.map +1 -1
- package/dist/brief/freshness.js +22 -4
- package/dist/brief/git-signals.d.ts +32 -5
- package/dist/brief/git-signals.d.ts.map +1 -1
- package/dist/brief/git-signals.js +169 -39
- package/dist/brief/knowledge.d.ts +62 -0
- package/dist/brief/knowledge.d.ts.map +1 -0
- package/dist/brief/knowledge.js +123 -0
- package/dist/brief/reconcile.d.ts +7 -1
- package/dist/brief/reconcile.d.ts.map +1 -1
- package/dist/brief/reconcile.js +42 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/style.d.ts +26 -0
- package/dist/cli/style.d.ts.map +1 -0
- package/dist/cli/style.js +76 -0
- package/dist/cli/ui.d.ts.map +1 -1
- package/dist/cli/ui.js +33 -6
- package/dist/coordinatorium/cli.d.ts.map +1 -1
- package/dist/coordinatorium/cli.js +13 -0
- package/dist/finalize/cli.d.ts.map +1 -1
- package/dist/finalize/cli.js +30 -13
- package/dist/finalize/consistency.d.ts +51 -0
- package/dist/finalize/consistency.d.ts.map +1 -0
- package/dist/finalize/consistency.js +57 -0
- package/dist/finalize/core.d.ts +26 -0
- package/dist/finalize/core.d.ts.map +1 -1
- package/dist/finalize/core.js +140 -1
- package/dist/ideas/cli.d.ts +1 -0
- package/dist/ideas/cli.d.ts.map +1 -1
- package/dist/ideas/cli.js +14 -3
- package/dist/ideas/format.d.ts +31 -1
- package/dist/ideas/format.d.ts.map +1 -1
- package/dist/ideas/format.js +109 -1
- package/dist/ideas/store.d.ts +2 -0
- package/dist/ideas/store.d.ts.map +1 -1
- package/dist/ideas/store.js +4 -1
- package/dist/ideas/types.d.ts +10 -0
- package/dist/ideas/types.d.ts.map +1 -1
- package/dist/ideas/types.js +3 -0
- package/dist/readmodel/dashboard.d.ts.map +1 -1
- package/dist/readmodel/dashboard.js +19 -1
- package/dist/readmodel/index.d.ts +2 -1
- package/dist/readmodel/index.d.ts.map +1 -1
- package/dist/readmodel/index.js +1 -0
- package/dist/readmodel/roadmap.d.ts +34 -0
- package/dist/readmodel/roadmap.d.ts.map +1 -0
- package/dist/readmodel/roadmap.js +254 -0
- package/dist/readmodel/types.d.ts +88 -0
- package/dist/readmodel/types.d.ts.map +1 -1
- package/dist/root/safe-write.d.ts +15 -4
- package/dist/root/safe-write.d.ts.map +1 -1
- package/dist/root/safe-write.js +264 -50
- package/dist/ui/assets/index-BJKxUDPD.css +1 -0
- package/dist/ui/assets/index-ioC9DPHQ.js +28 -0
- package/dist/ui/index.html +2 -2
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +4 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/dist/ui/assets/index-5zXAgig8.css +0 -1
- package/dist/ui/assets/index-CQgoMYLM.js +0 -28
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consistency assessment of the state documents around a finalization (FIN-002).
|
|
3
|
+
*
|
|
4
|
+
* `finalize` writes a fixed, small set of documents (story-index.md, active.md,
|
|
5
|
+
* and whatever the owner listed in `docUpdates` / HANDOFF). It deliberately does
|
|
6
|
+
* NOT write roadmap-index.md, so a roadmap row that still says IN PROGRESS for
|
|
7
|
+
* the story that was just finalized used to survive a successful `apply`
|
|
8
|
+
* unnoticed. This module reports such disagreements; it never edits anything
|
|
9
|
+
* and never changes the outcome of the operation.
|
|
10
|
+
*
|
|
11
|
+
* It reuses `findContradictions()` of `monolith brief` — the single definition
|
|
12
|
+
* of "state documents disagree" — and feeds it the text the documents have (or
|
|
13
|
+
* will have, for a plan) after finalization. Four things are kept apart:
|
|
14
|
+
* - operation success → plan/apply `status` (unchanged)
|
|
15
|
+
* - managed documents → written by this finalization
|
|
16
|
+
* - external documents → not written by it; remediation is advice only
|
|
17
|
+
* - completeness of the evidence → NOT_VERIFIED when a document cannot be read
|
|
18
|
+
*/
|
|
19
|
+
import { findContradictions, parseActiveStoryClaim, parseRoadmapActiveStory, parseRoadmapStatuses, parseStoryIndexStatuses } from '../brief/freshness.js';
|
|
20
|
+
import { extractCurrentStory } from '../brief/story.js';
|
|
21
|
+
const SCOPE = 'Checks only structural agreement between active.md, roadmap-index.md and story-index.md (the same rules as `monolith brief`). It does not read prose, HANDOFF.md, or any other project document.';
|
|
22
|
+
const DOC_DIR = { 'active.md': 'memory/working/', 'story-index.md': 'memory/working/', 'roadmap-index.md': 'context/maps/' };
|
|
23
|
+
export function assessConsistency(input) {
|
|
24
|
+
const notVerified = [];
|
|
25
|
+
if (input.active === null)
|
|
26
|
+
notVerified.push('active.md is missing or unreadable');
|
|
27
|
+
if (input.roadmap === null)
|
|
28
|
+
notVerified.push('roadmap-index.md is missing or unreadable');
|
|
29
|
+
if (input.storyIndex === null)
|
|
30
|
+
notVerified.push('story-index.md is missing or unreadable');
|
|
31
|
+
const story = input.active === null ? null : extractCurrentStory(input.active);
|
|
32
|
+
const activeStory = story !== null && story.hasCurrentStorySection ? parseActiveStoryClaim(story.excerpt) : { kind: 'unknown' };
|
|
33
|
+
if (input.active !== null && activeStory.kind === 'unknown')
|
|
34
|
+
notVerified.push('the active story could not be determined from active.md');
|
|
35
|
+
const found = findContradictions({
|
|
36
|
+
activeStory,
|
|
37
|
+
roadmapActiveStory: input.roadmap === null ? null : parseRoadmapActiveStory(input.roadmap),
|
|
38
|
+
roadmapStatuses: input.roadmap === null ? new Map() : parseRoadmapStatuses(input.roadmap),
|
|
39
|
+
storyIndexStatuses: input.storyIndex === null ? new Map() : parseStoryIndexStatuses(input.storyIndex),
|
|
40
|
+
});
|
|
41
|
+
const written = new Set(input.writtenPaths);
|
|
42
|
+
const findings = found.map((c) => {
|
|
43
|
+
const externalDocuments = c.documents.filter((d) => !(input.managedRoot !== undefined && written.has(`${input.managedRoot}/${DOC_DIR[d] ?? ''}${d}`)));
|
|
44
|
+
return {
|
|
45
|
+
message: c.message,
|
|
46
|
+
documents: c.documents,
|
|
47
|
+
externalDocuments,
|
|
48
|
+
remediation: externalDocuments.length === 0
|
|
49
|
+
? `Both documents are written by this finalization; review the proposal.`
|
|
50
|
+
: `${externalDocuments.join(', ')} ${externalDocuments.length === 1 ? 'is' : 'are'} not written by finalization. Update ${externalDocuments.length === 1 ? 'it' : 'them'} yourself, or list ${externalDocuments.length === 1 ? 'it' : 'them'} in monolith.config.json finalization.allowedDocs and pass a docUpdates entry in a new proposal.`,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
// A concrete contradiction is reported as such even when other evidence is missing;
|
|
54
|
+
// CONSISTENT requires that nothing was left unchecked.
|
|
55
|
+
const status = findings.length > 0 ? 'INCONSISTENT' : notVerified.length > 0 ? 'NOT_VERIFIED' : 'CONSISTENT';
|
|
56
|
+
return { status, projected: input.projected, findings, notVerified, scope: SCOPE };
|
|
57
|
+
}
|
package/dist/finalize/core.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { type ConfigRead, type FsOps, type WriteWarning } from '../root/index.js';
|
|
23
23
|
import { type LockOptions } from '../coordinatorium/lock.js';
|
|
24
|
+
import { type ConsistencyAssessment } from './consistency.js';
|
|
24
25
|
import { type FinalizationProposal } from './proposal.js';
|
|
25
26
|
export type CheckStatus = 'PASS' | 'FAIL' | 'NOT_VERIFIED';
|
|
26
27
|
export type TargetKind = 'capsule' | 'execution-history' | 'story-index' | 'doc' | 'handoff' | 'active';
|
|
@@ -56,6 +57,12 @@ export interface FinalizationPlan {
|
|
|
56
57
|
checks: Check[];
|
|
57
58
|
targets: PlannedTarget[];
|
|
58
59
|
diagnostics: string[];
|
|
60
|
+
/**
|
|
61
|
+
* FIN-002: whether the state documents agree with each other in the state
|
|
62
|
+
* this plan leads to (projected before apply, actual once every target is
|
|
63
|
+
* done). Informational: it never changes `status`, the digest or exit codes.
|
|
64
|
+
*/
|
|
65
|
+
consistency?: ConsistencyAssessment;
|
|
59
66
|
}
|
|
60
67
|
export interface ApplyResult {
|
|
61
68
|
status: ApplyStatus;
|
|
@@ -86,6 +93,25 @@ export interface FinalizeConfig {
|
|
|
86
93
|
}
|
|
87
94
|
export declare function readFinalizeConfig(config: ConfigRead): FinalizeConfig;
|
|
88
95
|
export declare function renderCapsule(p: FinalizationProposal): string;
|
|
96
|
+
/**
|
|
97
|
+
* Format-equivalence of a capsule (FIN-001, made context-aware by A4-FIN-01).
|
|
98
|
+
* The capsule is created once and never overwritten, so "already present" used
|
|
99
|
+
* to mean byte-identical; a formatter pass (Prettier adds a blank line after
|
|
100
|
+
* every heading) then turned an unchanged capsule into a CONFLICT. Equivalence
|
|
101
|
+
* is deliberately narrow and NOT a general whitespace-insensitive comparison.
|
|
102
|
+
* Ignored: line endings (CRLF/LF), trailing whitespace on a line that is plain
|
|
103
|
+
* prose, a single blank line directly after an ATX heading, and trailing blank
|
|
104
|
+
* lines at end of file. NOT ignored, because whitespace carries meaning there:
|
|
105
|
+
* - anything inside a fenced code block (``` or ~~~), an indented code block /
|
|
106
|
+
* 4-space-or-tab-indented line, an HTML comment, or YAML frontmatter
|
|
107
|
+
* (`---` ... `---` at the very top; an unterminated one protects the whole
|
|
108
|
+
* file) — those lines are compared verbatim;
|
|
109
|
+
* - a Markdown hard line break (two or more trailing spaces before a line that
|
|
110
|
+
* continues the paragraph): kept as a break (any run of 2+ spaces is the
|
|
111
|
+
* same break); a backslash break is not whitespace and is always compared;
|
|
112
|
+
* - blank lines anywhere else (paragraph breaks).
|
|
113
|
+
*/
|
|
114
|
+
export declare function capsuleCanonical(s: string): string;
|
|
89
115
|
/** Evidence older than this many days before the finalization date is not accepted. */
|
|
90
116
|
export declare const EVIDENCE_MAX_AGE_DAYS = 7;
|
|
91
117
|
export type ActiveIdentity = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/finalize/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,EAUL,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAG1E,OAAO,EAA8B,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEtF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;AAC3D,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,mBAAmB,GAAG,aAAa,GAAG,KAAK,GAAG,SAAS,GAAG,QAAQ,CAAC;AACxG,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;AACrF,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,WAAW,GAAG,mBAAmB,CAAC;AACrE,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,mBAAmB,GAAG,SAAS,GAAG,YAAY,CAAC;AAEvF,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,sGAAsG;IACtG,MAAM,EAAE,MAAM,CAAC;IACf,8GAA8G;IAC9G,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/finalize/core.ts"],"names":[],"mappings":"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"}
|
package/dist/finalize/core.js
CHANGED
|
@@ -26,6 +26,7 @@ import { RootContainmentError, SafeWriteError, createFile, openParent, projectBo
|
|
|
26
26
|
import { acquireLock } from '../coordinatorium/lock.js';
|
|
27
27
|
import { extractCurrentStory } from '../brief/story.js';
|
|
28
28
|
import { declaresNoActiveStory, maskNonContent } from '../brief/markdown-context.js';
|
|
29
|
+
import { assessConsistency } from './consistency.js';
|
|
29
30
|
import { validDate, validateRelPath } from './proposal.js';
|
|
30
31
|
const MAX_DOC_BYTES = 4 * 1024 * 1024;
|
|
31
32
|
const LOCK_NAME = 'finalize.lock';
|
|
@@ -105,28 +106,143 @@ export function renderCapsule(p) {
|
|
|
105
106
|
`# STORY ${s.id} — ${s.title}`,
|
|
106
107
|
'',
|
|
107
108
|
'## Objective',
|
|
109
|
+
'',
|
|
108
110
|
w.objective,
|
|
109
111
|
'',
|
|
110
112
|
'## Implementation Summary',
|
|
113
|
+
'',
|
|
111
114
|
w.implementationSummary,
|
|
112
115
|
'',
|
|
113
116
|
'## Files Changed',
|
|
117
|
+
'',
|
|
114
118
|
bullets(w.filesChanged),
|
|
115
119
|
'',
|
|
116
120
|
'## Tests',
|
|
121
|
+
'',
|
|
117
122
|
w.tests,
|
|
118
123
|
'',
|
|
119
124
|
'## Key Decisions',
|
|
125
|
+
'',
|
|
120
126
|
w.keyDecisions,
|
|
121
127
|
'',
|
|
122
128
|
'## ADRs Written',
|
|
129
|
+
'',
|
|
123
130
|
bullets(w.adrs),
|
|
124
131
|
'',
|
|
125
132
|
'## Lessons Learned',
|
|
133
|
+
'',
|
|
126
134
|
w.lessons,
|
|
127
135
|
'',
|
|
128
136
|
].join('\n');
|
|
129
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Format-equivalence of a capsule (FIN-001, made context-aware by A4-FIN-01).
|
|
140
|
+
* The capsule is created once and never overwritten, so "already present" used
|
|
141
|
+
* to mean byte-identical; a formatter pass (Prettier adds a blank line after
|
|
142
|
+
* every heading) then turned an unchanged capsule into a CONFLICT. Equivalence
|
|
143
|
+
* is deliberately narrow and NOT a general whitespace-insensitive comparison.
|
|
144
|
+
* Ignored: line endings (CRLF/LF), trailing whitespace on a line that is plain
|
|
145
|
+
* prose, a single blank line directly after an ATX heading, and trailing blank
|
|
146
|
+
* lines at end of file. NOT ignored, because whitespace carries meaning there:
|
|
147
|
+
* - anything inside a fenced code block (``` or ~~~), an indented code block /
|
|
148
|
+
* 4-space-or-tab-indented line, an HTML comment, or YAML frontmatter
|
|
149
|
+
* (`---` ... `---` at the very top; an unterminated one protects the whole
|
|
150
|
+
* file) — those lines are compared verbatim;
|
|
151
|
+
* - a Markdown hard line break (two or more trailing spaces before a line that
|
|
152
|
+
* continues the paragraph): kept as a break (any run of 2+ spaces is the
|
|
153
|
+
* same break); a backslash break is not whitespace and is always compared;
|
|
154
|
+
* - blank lines anywhere else (paragraph breaks).
|
|
155
|
+
*/
|
|
156
|
+
export function capsuleCanonical(s) {
|
|
157
|
+
const lines = norm(s).split('\n');
|
|
158
|
+
const verbatim = lines.map(() => false);
|
|
159
|
+
const fenceOpen = (l) => {
|
|
160
|
+
const m = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(l);
|
|
161
|
+
return m !== null && !(m[1][0] === '`' && m[2].includes('`')) ? m : null;
|
|
162
|
+
};
|
|
163
|
+
let i = 0;
|
|
164
|
+
if (lines[0] === '---') {
|
|
165
|
+
const close = lines.findIndex((l, k) => k > 0 && (l === '---' || l === '...'));
|
|
166
|
+
const end = close === -1 ? lines.length - 1 : close; // unterminated: cannot tell where it ends
|
|
167
|
+
for (let k = 0; k <= end; k++)
|
|
168
|
+
verbatim[k] = true;
|
|
169
|
+
i = end + 1;
|
|
170
|
+
}
|
|
171
|
+
let fence = null;
|
|
172
|
+
let comment = false;
|
|
173
|
+
let htmlBlock = false; // an HTML block runs to the next blank line; its content is not Markdown
|
|
174
|
+
for (; i < lines.length; i++) {
|
|
175
|
+
const l = lines[i];
|
|
176
|
+
if (htmlBlock) {
|
|
177
|
+
if (l.trim() === '')
|
|
178
|
+
htmlBlock = false;
|
|
179
|
+
else {
|
|
180
|
+
verbatim[i] = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (fence !== null) {
|
|
185
|
+
verbatim[i] = true;
|
|
186
|
+
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(l);
|
|
187
|
+
if (close !== null && close[1][0] === fence.ch && close[1].length >= fence.len)
|
|
188
|
+
fence = null;
|
|
189
|
+
}
|
|
190
|
+
else if (comment) {
|
|
191
|
+
verbatim[i] = true;
|
|
192
|
+
if (l.includes('-->'))
|
|
193
|
+
comment = false;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const open = fenceOpen(l);
|
|
197
|
+
if (open !== null) {
|
|
198
|
+
verbatim[i] = true;
|
|
199
|
+
fence = { ch: open[1][0], len: open[1].length };
|
|
200
|
+
}
|
|
201
|
+
else if (/^ {0,3}<!--/.test(l)) {
|
|
202
|
+
verbatim[i] = true;
|
|
203
|
+
if (!l.includes('-->', l.indexOf('<!--') + 4))
|
|
204
|
+
comment = true;
|
|
205
|
+
}
|
|
206
|
+
else if (/^ {0,3}<[A-Za-z\/!?]/.test(l)) {
|
|
207
|
+
verbatim[i] = true;
|
|
208
|
+
htmlBlock = true;
|
|
209
|
+
}
|
|
210
|
+
else if (/^ {0,3}>/.test(l)) {
|
|
211
|
+
verbatim[i] = true; // blockquote: may hold fences/code whose whitespace we cannot track
|
|
212
|
+
}
|
|
213
|
+
else if (/^(?: {4}|\t)/.test(l)) {
|
|
214
|
+
verbatim[i] = true; // indented code block (or an indented continuation): compared as written
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const out = [];
|
|
219
|
+
const outVerbatim = [];
|
|
220
|
+
let skippable = false; // a blank line right after an ATX heading, at most one
|
|
221
|
+
for (let k = 0; k < lines.length; k++) {
|
|
222
|
+
let l = lines[k];
|
|
223
|
+
if (verbatim[k]) {
|
|
224
|
+
out.push(l);
|
|
225
|
+
outVerbatim.push(true);
|
|
226
|
+
skippable = false;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const next = k + 1 < lines.length ? lines[k + 1] : undefined;
|
|
230
|
+
const hardBreak = / {2}$/.test(l) && l.trim() !== '' && !/^#{1,6}[ \t]/.test(l) && next !== undefined && next.trim() !== '' && !/^#{1,6}([ \t]|$)/.test(next) && fenceOpen(next) === null;
|
|
231
|
+
l = l.replace(/[ \t]+$/, '') + (hardBreak ? ' ' : '');
|
|
232
|
+
if (l === '' && skippable) {
|
|
233
|
+
skippable = false;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
skippable = /^#{1,6}[ \t]/.test(l);
|
|
237
|
+
out.push(l);
|
|
238
|
+
outVerbatim.push(false);
|
|
239
|
+
}
|
|
240
|
+
while (out.length > 0 && out[out.length - 1] === '' && !outVerbatim[outVerbatim.length - 1]) {
|
|
241
|
+
out.pop();
|
|
242
|
+
outVerbatim.pop();
|
|
243
|
+
}
|
|
244
|
+
return out.join('\n');
|
|
245
|
+
}
|
|
130
246
|
const capsulePathOf = (root, p) => `${root}/memory/working/phases/${p.story.phase}/stories/${p.story.slug}.md`;
|
|
131
247
|
function historyHeading(p) {
|
|
132
248
|
return `## ${p.story.id} — ${p.story.title} (${p.story.date})`;
|
|
@@ -496,6 +612,8 @@ function buildInternals(ctx, p) {
|
|
|
496
612
|
}
|
|
497
613
|
else if (d.content === content)
|
|
498
614
|
out.push({ target: { ...t, state: 'done', reason: 'capsule already present with identical content', base: 'DONE' } });
|
|
615
|
+
else if (capsuleCanonical(d.content) === capsuleCanonical(content))
|
|
616
|
+
out.push({ target: { ...t, state: 'done', reason: 'capsule already present; differs from the rendered form only in formatting (line endings, trailing whitespace, blank lines after headings) — left untouched', base: 'DONE' } });
|
|
499
617
|
else
|
|
500
618
|
out.push({ target: { ...t, state: 'conflict', reason: 'a different file already exists at the capsule path; it is never overwritten', base: d.sha } });
|
|
501
619
|
}
|
|
@@ -667,7 +785,28 @@ function plan(ctx, p) {
|
|
|
667
785
|
evidence: [integrity.sha, governance.sha],
|
|
668
786
|
config: [ctx.config.requiredDocs, ctx.config.allowedDocs],
|
|
669
787
|
}));
|
|
670
|
-
|
|
788
|
+
const finalText = (rel) => {
|
|
789
|
+
const it = internals.find((i) => i.target.path === rel);
|
|
790
|
+
if (it?.write)
|
|
791
|
+
return it.write.content;
|
|
792
|
+
const d = readDoc(ctx.projectRoot, rel);
|
|
793
|
+
return d.kind === 'present' ? d.content : null;
|
|
794
|
+
};
|
|
795
|
+
let consistency;
|
|
796
|
+
try {
|
|
797
|
+
consistency = assessConsistency({
|
|
798
|
+
active: finalText(`${ctx.root}/memory/working/active.md`),
|
|
799
|
+
roadmap: finalText(`${ctx.root}/context/maps/roadmap-index.md`),
|
|
800
|
+
storyIndex: finalText(`${ctx.root}/memory/working/story-index.md`),
|
|
801
|
+
writtenPaths: targets.filter((t) => t.kind !== 'capsule' && t.kind !== 'execution-history' && (t.state === 'pending' || t.state === 'done')).map((t) => t.path),
|
|
802
|
+
projected: !allDone,
|
|
803
|
+
managedRoot: ctx.root,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
catch (err) {
|
|
807
|
+
consistency = { status: 'NOT_VERIFIED', projected: !allDone, findings: [], notVerified: [`the assessment failed: ${err.message}`], scope: 'not assessed' };
|
|
808
|
+
}
|
|
809
|
+
return { plan: { status, storyId: p.story.id, digest, partial: someDone && !allDone, checks, targets, diagnostics, consistency }, internals };
|
|
671
810
|
}
|
|
672
811
|
export function planFinalization(targetDir, proposal) {
|
|
673
812
|
const c = makeContext(targetDir);
|
package/dist/ideas/cli.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface AddFlags {
|
|
|
11
11
|
description?: string;
|
|
12
12
|
context?: string;
|
|
13
13
|
descriptionStdin?: boolean;
|
|
14
|
+
paths?: string[];
|
|
14
15
|
}
|
|
15
16
|
/** Strict: every flag needs a value, none may repeat, unknown flags and stray arguments are errors. */
|
|
16
17
|
export declare function parseAddArgs(args: string[]): AddFlags;
|
package/dist/ideas/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/ideas/cli.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAiBvC,UAAU,QAAQ;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/ideas/cli.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAiBvC,UAAU,QAAQ;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAOD,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CAmCrD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,GAAE,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB1G;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,EAAE,KAAK,GAAE,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhJ;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB1F;AAkBD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1F;AAED,6CAA6C;AAC7C,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe7F"}
|
package/dist/ideas/cli.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { reportWriteWarning } from '../root/index.js';
|
|
10
10
|
import { addIdea, getIdea, ideaStore, listIdeas } from './store.js';
|
|
11
11
|
import { IDEA_LIMITS, IdeaError } from './types.js';
|
|
12
|
-
const ADD_USAGE = 'Usage: monolith idea add --title "..." (--description "..." | --description-stdin) [--context "..."]';
|
|
12
|
+
const ADD_USAGE = 'Usage: monolith idea add --title "..." (--description "..." | --description-stdin) [--context "..."] [--path <project-relative document file> ...]';
|
|
13
13
|
/** UTF-8 needs at most 4 bytes per UTF-16 code unit's worth of text; anything larger cannot fit the description limit. */
|
|
14
14
|
const STDIN_MAX_BYTES = IDEA_LIMITS.description * 4;
|
|
15
15
|
const LIST_USAGE = 'Usage: monolith idea list';
|
|
@@ -21,7 +21,8 @@ function printable(s) {
|
|
|
21
21
|
}
|
|
22
22
|
const ADD_FLAGS = ['--title', '--description', '--context'];
|
|
23
23
|
const STDIN_FLAG = '--description-stdin';
|
|
24
|
-
const
|
|
24
|
+
const PATH_FLAG = '--path';
|
|
25
|
+
const KNOWN_FLAGS = [...ADD_FLAGS, STDIN_FLAG, PATH_FLAG];
|
|
25
26
|
/** Strict: every flag needs a value, none may repeat, unknown flags and stray arguments are errors. */
|
|
26
27
|
export function parseAddArgs(args) {
|
|
27
28
|
const out = {};
|
|
@@ -36,6 +37,14 @@ export function parseAddArgs(args) {
|
|
|
36
37
|
out.descriptionStdin = true;
|
|
37
38
|
continue;
|
|
38
39
|
}
|
|
40
|
+
if (flag === PATH_FLAG) {
|
|
41
|
+
const v = args[i + 1];
|
|
42
|
+
if (v === undefined || KNOWN_FLAGS.includes(v))
|
|
43
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${flag} needs a value.\n${ADD_USAGE}`);
|
|
44
|
+
(out.paths ??= []).push(v);
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
39
48
|
const key = flag.slice(2);
|
|
40
49
|
if (key in out)
|
|
41
50
|
throw new IdeaError('IDEA_INVALID_INPUT', `${flag} was given more than once.\n${ADD_USAGE}`);
|
|
@@ -92,7 +101,7 @@ export async function runIdeaAdd(args, targetDir = process.cwd(), stdin = proces
|
|
|
92
101
|
const flags = parseAddArgs(args);
|
|
93
102
|
const description = flags.descriptionStdin ? await readStdinText(stdin) : flags.description;
|
|
94
103
|
const store = ideaStore(targetDir);
|
|
95
|
-
const { idea, path } = addIdea(store, { title: flags.title, description, context: flags.context }, { onWarning: (w) => reportWriteWarning(w) });
|
|
104
|
+
const { idea, path } = addIdea(store, { title: flags.title, description, context: flags.context, ...(flags.paths !== undefined ? { paths: flags.paths } : {}) }, { onWarning: (w) => reportWriteWarning(w) });
|
|
96
105
|
console.log(`Saved ${idea.id} [${idea.status}]: ${printable(idea.title)}`);
|
|
97
106
|
console.log(` file: ${path}`);
|
|
98
107
|
console.log('Current task and focus are unchanged. Recall with `monolith idea list` / `monolith idea show ' + idea.id + '`.');
|
|
@@ -130,6 +139,8 @@ function formatIdea(i) {
|
|
|
130
139
|
];
|
|
131
140
|
if (i.tags !== undefined && i.tags.length > 0)
|
|
132
141
|
lines.push(` tags: ${i.tags.map(printable).join(', ')}`);
|
|
142
|
+
if (i.paths !== undefined && i.paths.length > 0)
|
|
143
|
+
lines.push(` paths: ${i.paths.map(printable).join(', ')}`);
|
|
133
144
|
lines.push(' context: ' + (i.context === undefined ? '(none)' : ''));
|
|
134
145
|
if (i.context !== undefined)
|
|
135
146
|
lines.push(...i.context.split('\n').map((l) => ` ${l}`));
|
package/dist/ideas/format.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* createdAt: "2026-..."
|
|
10
10
|
* context: "..." (optional)
|
|
11
11
|
* tags: ["a", "b"] (optional)
|
|
12
|
+
* paths: ["docs/x.md"] (optional, KA-001: exact document files of the registered artifact; see attributionPathProblem)
|
|
12
13
|
* ---
|
|
13
14
|
*
|
|
14
15
|
* ## Description
|
|
@@ -21,6 +22,35 @@
|
|
|
21
22
|
* The description is the body: no escaping, no trimming.
|
|
22
23
|
*/
|
|
23
24
|
import { type Idea, type NewIdea } from './types.js';
|
|
25
|
+
/**
|
|
26
|
+
* KA-001: a registered artifact path. Project-relative, `/`-separated, no
|
|
27
|
+
* traversal, no globs; a trailing `/` makes it a directory prefix. Returns the
|
|
28
|
+
* reason it is invalid, or null.
|
|
29
|
+
*/
|
|
30
|
+
export declare function invalidArtifactPath(p: unknown): string | null;
|
|
31
|
+
/** True when `file` is covered by one of the registered `paths` (exact file, or `dir/` prefix). */
|
|
32
|
+
export declare function artifactPathsCover(paths: readonly string[], file: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Extensions of documents that can be registered as independent knowledge.
|
|
35
|
+
* Deliberately an allow-list of *document* formats, not of directories: a
|
|
36
|
+
* strategy paper may live anywhere, but source code, configuration and
|
|
37
|
+
* scripts never qualify, whatever directory they are in.
|
|
38
|
+
*/
|
|
39
|
+
export declare const KNOWLEDGE_DOCUMENT_EXTENSIONS: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* A4-SEC-03: the policy for a path that is to ATTRIBUTE commits to independent
|
|
42
|
+
* knowledge (stricter than `invalidArtifactPath`, which only says the path is
|
|
43
|
+
* well-formed). A registered path must be
|
|
44
|
+
* - one exact file: a `dir/` prefix would turn every later change under it into
|
|
45
|
+
* "knowledge" (`src/`, the project root, ...);
|
|
46
|
+
* - a document (`KNOWLEDGE_DOCUMENT_EXTENSIONS`): registering `src/pay.ts` would
|
|
47
|
+
* let an idea id launder ordinary code;
|
|
48
|
+
* - outside the MONOLITH root (case-insensitive; `.monolith` is always refused
|
|
49
|
+
* too): governance state is never independent knowledge.
|
|
50
|
+
* Returns the reason a path is refused, or null. Registration says WHICH file a
|
|
51
|
+
* commit may be explained by; it does not say the change was reviewed or allowed.
|
|
52
|
+
*/
|
|
53
|
+
export declare function attributionPathProblem(p: string, monolithRoot?: string): string | null;
|
|
24
54
|
export declare function isIdeaId(value: string): boolean;
|
|
25
55
|
export declare function ideaNumber(id: string): number;
|
|
26
56
|
export declare function formatIdeaId(n: number): string;
|
|
@@ -28,7 +58,7 @@ export declare function formatIdeaId(n: number): string;
|
|
|
28
58
|
export declare function ideaIdFromFileName(name: string): string | null;
|
|
29
59
|
export declare function ideaFileName(id: string): string;
|
|
30
60
|
/** Validates user input; returns it unchanged (content is never trimmed or rewritten). */
|
|
31
|
-
export declare function validateNewIdea(input: NewIdea): NewIdea;
|
|
61
|
+
export declare function validateNewIdea(input: NewIdea, monolithRoot?: string): NewIdea;
|
|
32
62
|
export declare function serializeIdea(idea: Idea): string;
|
|
33
63
|
/**
|
|
34
64
|
* Parse an idea file. `expectedId` is the id implied by the file name; a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/ideas/format.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/ideas/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAyC,KAAK,IAAI,EAAmB,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAS7G;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAY7D;AAED,mGAAmG;AACnG,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAElF;AAED;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAA0C,CAAC;AAOtG;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBtF;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAI7C;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG/C;AAaD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CA4B9E;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAchD;AAqBD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAsEhE"}
|
package/dist/ideas/format.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* createdAt: "2026-..."
|
|
10
10
|
* context: "..." (optional)
|
|
11
11
|
* tags: ["a", "b"] (optional)
|
|
12
|
+
* paths: ["docs/x.md"] (optional, KA-001: exact document files of the registered artifact; see attributionPathProblem)
|
|
12
13
|
* ---
|
|
13
14
|
*
|
|
14
15
|
* ## Description
|
|
@@ -27,6 +28,85 @@ const FILE_RE = /^(IDEA-\d{3,9})\.md$/;
|
|
|
27
28
|
// C0 controls, DEL and C1 controls (a title is shown in a terminal).
|
|
28
29
|
// eslint-disable-next-line no-control-regex
|
|
29
30
|
const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
31
|
+
/**
|
|
32
|
+
* KA-001: a registered artifact path. Project-relative, `/`-separated, no
|
|
33
|
+
* traversal, no globs; a trailing `/` makes it a directory prefix. Returns the
|
|
34
|
+
* reason it is invalid, or null.
|
|
35
|
+
*/
|
|
36
|
+
export function invalidArtifactPath(p) {
|
|
37
|
+
if (typeof p !== 'string' || p === '')
|
|
38
|
+
return 'path must be a non-empty string';
|
|
39
|
+
if (p.length > IDEA_LIMITS.pathLength)
|
|
40
|
+
return `path is too long (> ${IDEA_LIMITS.pathLength} characters)`;
|
|
41
|
+
if (CONTROL_RE.test(p))
|
|
42
|
+
return 'path must not contain control characters';
|
|
43
|
+
if (p.includes('"'))
|
|
44
|
+
return 'path must not contain a double quote';
|
|
45
|
+
if (p.includes('\\'))
|
|
46
|
+
return 'path must use "/" separators';
|
|
47
|
+
if (p.startsWith('/') || /^[A-Za-z]:/.test(p))
|
|
48
|
+
return 'path must be project-relative';
|
|
49
|
+
if (/[*?[\]{}]/.test(p))
|
|
50
|
+
return 'path must not contain glob characters';
|
|
51
|
+
if (p !== p.trim())
|
|
52
|
+
return 'path must not start or end with whitespace';
|
|
53
|
+
const segments = p.endsWith('/') ? p.slice(0, -1).split('/') : p.split('/');
|
|
54
|
+
if (segments.some((x) => x === '' || x === '.' || x === '..'))
|
|
55
|
+
return 'path must not contain empty, "." or ".." segments';
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/** True when `file` is covered by one of the registered `paths` (exact file, or `dir/` prefix). */
|
|
59
|
+
export function artifactPathsCover(paths, file) {
|
|
60
|
+
return paths.some((p) => (p.endsWith('/') ? file.startsWith(p) : file === p));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extensions of documents that can be registered as independent knowledge.
|
|
64
|
+
* Deliberately an allow-list of *document* formats, not of directories: a
|
|
65
|
+
* strategy paper may live anywhere, but source code, configuration and
|
|
66
|
+
* scripts never qualify, whatever directory they are in.
|
|
67
|
+
*/
|
|
68
|
+
export const KNOWLEDGE_DOCUMENT_EXTENSIONS = ['.md', '.mdx', '.markdown', '.adoc'];
|
|
69
|
+
/** Directories whose documents steer tools, agents or dependencies; never knowledge (compared lower-case). */
|
|
70
|
+
const KNOWLEDGE_DENIED_DIRS = ['.git', '.claude', '.cursor', '.github', '.vscode', '.husky', 'node_modules'];
|
|
71
|
+
/** Agent-instruction file names (compared lower-case): changing them changes agent behaviour. */
|
|
72
|
+
const KNOWLEDGE_DENIED_NAMES = ['agents.md', 'agent.md', 'claude.md', 'gemini.md', 'skill.md', 'conventions.md', 'codeowners.md', 'copilot-instructions.md', '.cursorrules', '.windsurfrules.md'];
|
|
73
|
+
/**
|
|
74
|
+
* A4-SEC-03: the policy for a path that is to ATTRIBUTE commits to independent
|
|
75
|
+
* knowledge (stricter than `invalidArtifactPath`, which only says the path is
|
|
76
|
+
* well-formed). A registered path must be
|
|
77
|
+
* - one exact file: a `dir/` prefix would turn every later change under it into
|
|
78
|
+
* "knowledge" (`src/`, the project root, ...);
|
|
79
|
+
* - a document (`KNOWLEDGE_DOCUMENT_EXTENSIONS`): registering `src/pay.ts` would
|
|
80
|
+
* let an idea id launder ordinary code;
|
|
81
|
+
* - outside the MONOLITH root (case-insensitive; `.monolith` is always refused
|
|
82
|
+
* too): governance state is never independent knowledge.
|
|
83
|
+
* Returns the reason a path is refused, or null. Registration says WHICH file a
|
|
84
|
+
* commit may be explained by; it does not say the change was reviewed or allowed.
|
|
85
|
+
*/
|
|
86
|
+
export function attributionPathProblem(p, monolithRoot) {
|
|
87
|
+
const syntax = invalidArtifactPath(p);
|
|
88
|
+
if (syntax !== null)
|
|
89
|
+
return syntax;
|
|
90
|
+
if (p.endsWith('/'))
|
|
91
|
+
return 'a directory prefix is not allowed; register the exact document file';
|
|
92
|
+
const lower = p.toLowerCase();
|
|
93
|
+
for (const root of new Set(['.monolith', (monolithRoot ?? '.monolith').replace(/^(\.\/)+/, '').replace(/\/+$/, '').toLowerCase()])) {
|
|
94
|
+
if (root !== '' && root !== '.' && (lower === root || lower.startsWith(`${root}/`)))
|
|
95
|
+
return `a path inside the MONOLITH root (${root}) is never independent knowledge`;
|
|
96
|
+
}
|
|
97
|
+
if (/[\u200b-\u200f\u2028-\u202e\u2060-\u2064\ufeff\u00a0]/.test(p))
|
|
98
|
+
return 'path must not contain invisible or look-alike whitespace characters';
|
|
99
|
+
const segs = lower.split('/');
|
|
100
|
+
const hiddenDir = segs.slice(0, -1).find((x) => x.startsWith('.') || KNOWLEDGE_DENIED_DIRS.includes(x));
|
|
101
|
+
if (hiddenDir !== undefined)
|
|
102
|
+
return `a path under ${hiddenDir}/ (hidden or tool/dependency directory) configures tools or dependencies and is never independent knowledge`;
|
|
103
|
+
if (KNOWLEDGE_DENIED_NAMES.includes(segs[segs.length - 1]))
|
|
104
|
+
return 'agent-instruction files cannot be registered as independent knowledge';
|
|
105
|
+
if (!KNOWLEDGE_DOCUMENT_EXTENSIONS.some((e) => lower.endsWith(e))) {
|
|
106
|
+
return `only document files (${KNOWLEDGE_DOCUMENT_EXTENSIONS.join(', ')}) can be registered, not source code or configuration`;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
30
110
|
export function isIdeaId(value) {
|
|
31
111
|
return ID_RE.test(value);
|
|
32
112
|
}
|
|
@@ -57,7 +137,7 @@ function wellFormed(s) {
|
|
|
57
137
|
return !LONE_SURROGATE_RE.test(s);
|
|
58
138
|
}
|
|
59
139
|
/** Validates user input; returns it unchanged (content is never trimmed or rewritten). */
|
|
60
|
-
export function validateNewIdea(input) {
|
|
140
|
+
export function validateNewIdea(input, monolithRoot) {
|
|
61
141
|
const { title, description, context } = input;
|
|
62
142
|
if (typeof title !== 'string' || title.trim() === '')
|
|
63
143
|
throw invalid('title must not be empty.');
|
|
@@ -77,6 +157,19 @@ export function validateNewIdea(input) {
|
|
|
77
157
|
throw invalid(`context is too long (${context.length} > ${IDEA_LIMITS.context} characters).`);
|
|
78
158
|
}
|
|
79
159
|
}
|
|
160
|
+
if (input.paths !== undefined) {
|
|
161
|
+
if (!Array.isArray(input.paths) || input.paths.length === 0)
|
|
162
|
+
throw invalid('paths, when given, must be a non-empty list.');
|
|
163
|
+
if (input.paths.length > IDEA_LIMITS.paths)
|
|
164
|
+
throw invalid(`too many paths (${input.paths.length} > ${IDEA_LIMITS.paths}).`);
|
|
165
|
+
for (const p of input.paths) {
|
|
166
|
+
const bad = attributionPathProblem(p, monolithRoot);
|
|
167
|
+
if (bad !== null)
|
|
168
|
+
throw invalid(`paths: ${bad} (${JSON.stringify(String(p).slice(0, 80))}).`);
|
|
169
|
+
}
|
|
170
|
+
if (new Set(input.paths).size !== input.paths.length)
|
|
171
|
+
throw invalid('paths must not repeat.');
|
|
172
|
+
}
|
|
80
173
|
for (const [name, v] of [['title', title], ['description', description], ['context', context]]) {
|
|
81
174
|
if (v !== undefined && !wellFormed(v))
|
|
82
175
|
throw invalid(`${name} contains an unpaired surrogate and cannot be stored losslessly.`);
|
|
@@ -96,6 +189,8 @@ export function serializeIdea(idea) {
|
|
|
96
189
|
lines.push(`context: ${JSON.stringify(idea.context)}`);
|
|
97
190
|
if (idea.tags !== undefined && idea.tags.length > 0)
|
|
98
191
|
lines.push(`tags: ${JSON.stringify(idea.tags)}`);
|
|
192
|
+
if (idea.paths !== undefined && idea.paths.length > 0)
|
|
193
|
+
lines.push(`paths: ${JSON.stringify(idea.paths)}`);
|
|
99
194
|
lines.push('---');
|
|
100
195
|
return `${lines.join('\n')}\n${BODY_HEADING}${idea.description}\n`;
|
|
101
196
|
}
|
|
@@ -190,5 +285,18 @@ export function parseIdea(expectedId, text) {
|
|
|
190
285
|
}
|
|
191
286
|
idea.tags = tags;
|
|
192
287
|
}
|
|
288
|
+
if ('paths' in fm) {
|
|
289
|
+
let paths;
|
|
290
|
+
try {
|
|
291
|
+
paths = JSON.parse(fm.paths);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
throw corrupt(expectedId, '"paths" is not a JSON array of strings.');
|
|
295
|
+
}
|
|
296
|
+
if (!Array.isArray(paths) || paths.length > IDEA_LIMITS.paths || !paths.every((x) => invalidArtifactPath(x) === null)) {
|
|
297
|
+
throw corrupt(expectedId, '"paths" is not a JSON array of valid project-relative paths.');
|
|
298
|
+
}
|
|
299
|
+
idea.paths = paths;
|
|
300
|
+
}
|
|
193
301
|
return idea;
|
|
194
302
|
}
|
package/dist/ideas/store.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { type Idea, type IdeaEntry, type NewIdea } from './types.js';
|
|
|
26
26
|
export interface IdeaStore {
|
|
27
27
|
projectRoot: string;
|
|
28
28
|
rootAbs: string;
|
|
29
|
+
/** Validated relative MONOLITH root (`.monolith` by default). */
|
|
30
|
+
monolithRoot: string;
|
|
29
31
|
dirAbs: string;
|
|
30
32
|
boundary: WriteBoundary;
|
|
31
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/ideas/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAWL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAA0B,KAAK,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAQ7F,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/ideas/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAWL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAA0B,KAAK,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAQ7F,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAgBtD;AAED,MAAM,WAAW,cAAc;IAC7B,4BAA4B;IAC5B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,8EAA8E;IAC9E,GAAG,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACrB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AA+DD,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,cAAmB,GAAG,aAAa,CA6ElG;AAsBD,8GAA8G;AAC9G,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAM1D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,CAiBvD"}
|
package/dist/ideas/store.js
CHANGED
|
@@ -45,6 +45,7 @@ export function ideaStore(targetDir) {
|
|
|
45
45
|
return {
|
|
46
46
|
projectRoot: root.projectRoot,
|
|
47
47
|
rootAbs: root.rootAbs,
|
|
48
|
+
monolithRoot: root.monolithRoot,
|
|
48
49
|
dirAbs: join(root.rootAbs, IDEAS_REL),
|
|
49
50
|
boundary: projectBoundary(root.projectRoot),
|
|
50
51
|
};
|
|
@@ -115,7 +116,7 @@ function reserveId(markers, id, createdAt, opts) {
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
export function addIdea(store, input, opts = {}) {
|
|
118
|
-
validateNewIdea(input);
|
|
119
|
+
validateNewIdea(input, store.monolithRoot);
|
|
119
120
|
// `add` only creates `memory/long-term/ideas` inside an existing instance; it never invents `.monolith/`.
|
|
120
121
|
let rootStat;
|
|
121
122
|
try {
|
|
@@ -164,6 +165,8 @@ export function addIdea(store, input, opts = {}) {
|
|
|
164
165
|
const idea = { id, title: input.title, description: input.description, createdAt, status: 'inbox' };
|
|
165
166
|
if (input.context !== undefined)
|
|
166
167
|
idea.context = input.context;
|
|
168
|
+
if (input.paths !== undefined)
|
|
169
|
+
idea.paths = input.paths;
|
|
167
170
|
const text = serializeIdea(idea);
|
|
168
171
|
// The character limits do not bound the file (UTF-8 and JSON escaping expand text), and the reader refuses
|
|
169
172
|
// files over `fileBytes`: refuse here, before an id is reserved, so nothing is saved that cannot be read back.
|