@latysh-development/monolith 0.1.0-alpha.2 → 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 +24 -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 +17 -5
- package/dist/brief/freshness.d.ts +50 -7
- package/dist/brief/freshness.d.ts.map +1 -1
- package/dist/brief/freshness.js +89 -17
- 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/markdown-context.d.ts +22 -0
- package/dist/brief/markdown-context.d.ts.map +1 -0
- package/dist/brief/markdown-context.js +66 -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/brief/story.d.ts +2 -10
- package/dist/brief/story.d.ts.map +1 -1
- package/dist/brief/story.js +21 -7
- 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 +28 -0
- package/dist/finalize/core.d.ts.map +1 -1
- package/dist/finalize/core.js +186 -41
- 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/apple-touch-icon.png +0 -0
- package/dist/ui/assets/index-BJKxUDPD.css +1 -0
- package/dist/ui/assets/index-ioC9DPHQ.js +28 -0
- package/dist/ui/assets/monolith-mark-Ct0ammrU.webp +0 -0
- package/dist/ui/favicon-16x16.png +0 -0
- package/dist/ui/favicon-32x32.png +0 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/index.html +6 -2
- package/dist/update/cli.d.ts +1 -1
- package/dist/update/cli.d.ts.map +1 -1
- package/dist/update/cli.js +10 -3
- package/dist/update/index.d.ts +9 -0
- package/dist/update/index.d.ts.map +1 -1
- package/dist/update/index.js +50 -1
- package/dist/update/legacy-skills.d.ts +15 -0
- package/dist/update/legacy-skills.d.ts.map +1 -0
- package/dist/update/legacy-skills.js +20 -0
- 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/templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md +183 -0
- package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +19 -3
- package/dist/ui/assets/index-C4NhMaq7.js +0 -28
- package/dist/ui/assets/index-DMZ7ZvZY.css +0 -1
|
@@ -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;
|
|
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
|
@@ -25,6 +25,8 @@ import { dirname, join } from 'path';
|
|
|
25
25
|
import { RootContainmentError, SafeWriteError, createFile, openParent, projectBoundary, readConfigFile, readProjectFile, replaceFile, resolveMonolithRoot, } from '../root/index.js';
|
|
26
26
|
import { acquireLock } from '../coordinatorium/lock.js';
|
|
27
27
|
import { extractCurrentStory } from '../brief/story.js';
|
|
28
|
+
import { declaresNoActiveStory, maskNonContent } from '../brief/markdown-context.js';
|
|
29
|
+
import { assessConsistency } from './consistency.js';
|
|
28
30
|
import { validDate, validateRelPath } from './proposal.js';
|
|
29
31
|
const MAX_DOC_BYTES = 4 * 1024 * 1024;
|
|
30
32
|
const LOCK_NAME = 'finalize.lock';
|
|
@@ -104,28 +106,143 @@ export function renderCapsule(p) {
|
|
|
104
106
|
`# STORY ${s.id} — ${s.title}`,
|
|
105
107
|
'',
|
|
106
108
|
'## Objective',
|
|
109
|
+
'',
|
|
107
110
|
w.objective,
|
|
108
111
|
'',
|
|
109
112
|
'## Implementation Summary',
|
|
113
|
+
'',
|
|
110
114
|
w.implementationSummary,
|
|
111
115
|
'',
|
|
112
116
|
'## Files Changed',
|
|
117
|
+
'',
|
|
113
118
|
bullets(w.filesChanged),
|
|
114
119
|
'',
|
|
115
120
|
'## Tests',
|
|
121
|
+
'',
|
|
116
122
|
w.tests,
|
|
117
123
|
'',
|
|
118
124
|
'## Key Decisions',
|
|
125
|
+
'',
|
|
119
126
|
w.keyDecisions,
|
|
120
127
|
'',
|
|
121
128
|
'## ADRs Written',
|
|
129
|
+
'',
|
|
122
130
|
bullets(w.adrs),
|
|
123
131
|
'',
|
|
124
132
|
'## Lessons Learned',
|
|
133
|
+
'',
|
|
125
134
|
w.lessons,
|
|
126
135
|
'',
|
|
127
136
|
].join('\n');
|
|
128
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
|
+
}
|
|
129
246
|
const capsulePathOf = (root, p) => `${root}/memory/working/phases/${p.story.phase}/stories/${p.story.slug}.md`;
|
|
130
247
|
function historyHeading(p) {
|
|
131
248
|
return `## ${p.story.id} — ${p.story.title} (${p.story.date})`;
|
|
@@ -169,11 +286,16 @@ function activeBodies(root, p) {
|
|
|
169
286
|
// Text transforms (pure)
|
|
170
287
|
// ---------------------------------------------------------------------------
|
|
171
288
|
const eolOf = (s) => (s.includes('\r\n') ? '\r\n' : '\n');
|
|
172
|
-
/**
|
|
173
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Boundaries as in brief/story.ts: the next heading or a `---` rule ends a
|
|
291
|
+
* section, and headings inside code fences or HTML comments do not count.
|
|
292
|
+
* `structural` is the same text with fenced / commented lines blanked (same
|
|
293
|
+
* line count), so reading and writing agree on which section is "Current Story".
|
|
294
|
+
*/
|
|
295
|
+
function findSection(lines, structural, re) {
|
|
174
296
|
const hits = [];
|
|
175
|
-
|
|
176
|
-
if (re.test(l.trim()))
|
|
297
|
+
structural.forEach((l, i) => {
|
|
298
|
+
if (re.test(l.trim()) && l.trim() === lines[i].trim())
|
|
177
299
|
hits.push(i);
|
|
178
300
|
});
|
|
179
301
|
if (hits.length === 0)
|
|
@@ -182,7 +304,7 @@ function findSection(lines, re) {
|
|
|
182
304
|
return 'ambiguous';
|
|
183
305
|
let end = lines.length;
|
|
184
306
|
for (let i = hits[0] + 1; i < lines.length; i++) {
|
|
185
|
-
const t =
|
|
307
|
+
const t = structural[i].trim();
|
|
186
308
|
if (/^#{1,6}\s+/.test(t) || /^-{3,}\s*$/.test(t)) {
|
|
187
309
|
end = i;
|
|
188
310
|
break;
|
|
@@ -199,8 +321,9 @@ const CURRENT_RE = /^##\s+Current Story\s*$/i;
|
|
|
199
321
|
const LAST_RE = /^##\s+Last Finalization\s*$/i;
|
|
200
322
|
function editActive(content, bodies) {
|
|
201
323
|
const lines = content.split('\n');
|
|
202
|
-
const
|
|
203
|
-
const
|
|
324
|
+
const structural = maskNonContent(content).split('\n');
|
|
325
|
+
const cur = findSection(lines, structural, CURRENT_RE);
|
|
326
|
+
const last = findSection(lines, structural, LAST_RE);
|
|
204
327
|
for (const [name, s] of [['## Current Story', cur], ['## Last Finalization', last]]) {
|
|
205
328
|
if (s === 'absent')
|
|
206
329
|
return { state: 'conflict', reason: `active.md has no "${name}" section; it is never created automatically.` };
|
|
@@ -295,37 +418,13 @@ const FIX_HINT = 'To finalize, put exactly one line "**Story:** <id> — <title>
|
|
|
295
418
|
* evidence files and `## Last Finalization`); its FIRST token is the id. Ids
|
|
296
419
|
* mentioned in titles, descriptions, comments, code fences or other fields carry
|
|
297
420
|
* no authority. Zero, several, or malformed field lines → no identity (fail closed).
|
|
421
|
+
* A section whose first visible line says "no active story" AND that has a
|
|
422
|
+
* structural field is contradictory → no identity (PSC-004A).
|
|
298
423
|
*/
|
|
299
424
|
export function activeStoryIdentity(currentStoryBody) {
|
|
300
|
-
const found =
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
for (const raw of norm(currentStoryBody).split('\n')) {
|
|
304
|
-
const line = raw.trim();
|
|
305
|
-
if (comment) {
|
|
306
|
-
if (line.includes('-->'))
|
|
307
|
-
comment = false;
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
const f = /^(`{3,}|~{3,})/.exec(line);
|
|
311
|
-
if (fence !== null) {
|
|
312
|
-
if (f !== null && f[1][0] === fence[0] && f[1].length >= fence.length && line === f[1])
|
|
313
|
-
fence = null;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (f !== null) {
|
|
317
|
-
fence = f[1];
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (line.startsWith('<!--')) {
|
|
321
|
-
if (!line.includes('-->'))
|
|
322
|
-
comment = true;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
const m = /^\*\*Story:\*\*[ \t]*(.*)$/.exec(raw.replace(/\r$/, ''));
|
|
326
|
-
if (m !== null)
|
|
327
|
-
found.push(m[1].trim());
|
|
328
|
-
}
|
|
425
|
+
const found = structuralStoryFields(currentStoryBody);
|
|
426
|
+
if (found.length > 0 && declaresNoActiveStory(currentStoryBody))
|
|
427
|
+
return { ok: false, reason: `the Current Story section says there is no active story but also has ${found.length === 1 ? 'a "**Story:**" line' : `${found.length} "**Story:**" lines`} (${found.map((x) => `"${x.slice(0, 40)}"`).join(', ')}): contradictory identity. ${FIX_HINT}` };
|
|
329
428
|
if (found.length === 0)
|
|
330
429
|
return { ok: false, reason: `the Current Story section has no "**Story:** <id>" line, so the active story cannot be identified (ids in free text, headings, comments or code blocks are not identity). ${FIX_HINT}` };
|
|
331
430
|
if (found.length > 1)
|
|
@@ -335,6 +434,19 @@ export function activeStoryIdentity(currentStoryBody) {
|
|
|
335
434
|
return { ok: false, reason: `the "**Story:**" line ("${found[0].slice(0, 60)}") does not start with a valid story id. ${FIX_HINT}` };
|
|
336
435
|
return { ok: true, id };
|
|
337
436
|
}
|
|
437
|
+
/** Values of the structural `**Story:**` field lines (outside code fences and HTML comments). */
|
|
438
|
+
function structuralStoryFields(currentStoryBody) {
|
|
439
|
+
const found = [];
|
|
440
|
+
// Same fence / HTML-comment grammar as the brief's story extraction and
|
|
441
|
+
// freshness readers (one shared helper): text inside a code fence or an HTML
|
|
442
|
+
// comment, including a comment opened mid-line, is never identity.
|
|
443
|
+
for (const raw of maskNonContent(norm(currentStoryBody)).split('\n')) {
|
|
444
|
+
const m = /^\*\*Story:\*\*[ \t]*(.*)$/.exec(raw.replace(/\r$/, ''));
|
|
445
|
+
if (m !== null)
|
|
446
|
+
found.push(m[1].trim());
|
|
447
|
+
}
|
|
448
|
+
return found;
|
|
449
|
+
}
|
|
338
450
|
/**
|
|
339
451
|
* Attested evidence. Contract: exactly one `**Status:**` (PASS or FAIL, nothing
|
|
340
452
|
* else on the line), exactly one `**Story:**` whose FIRST token is the story id,
|
|
@@ -500,6 +612,8 @@ function buildInternals(ctx, p) {
|
|
|
500
612
|
}
|
|
501
613
|
else if (d.content === content)
|
|
502
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' } });
|
|
503
617
|
else
|
|
504
618
|
out.push({ target: { ...t, state: 'conflict', reason: 'a different file already exists at the capsule path; it is never overwritten', base: d.sha } });
|
|
505
619
|
}
|
|
@@ -612,13 +726,23 @@ function plan(ctx, p) {
|
|
|
612
726
|
let ok;
|
|
613
727
|
let want;
|
|
614
728
|
let why = '';
|
|
615
|
-
|
|
616
|
-
|
|
729
|
+
const visible = maskNonContent(excerpt);
|
|
730
|
+
if (exp === 'none') {
|
|
731
|
+
// Same rule as the brief: the first visible line says "no active story"
|
|
732
|
+
// and no structural **Story:** field (valid, duplicate or malformed)
|
|
733
|
+
// contradicts it (PSC-004A). Text inside code fences or comments is
|
|
734
|
+
// not a statement.
|
|
735
|
+
(ok = ex.hasCurrentStorySection && declaresNoActiveStory(excerpt) && structuralStoryFields(excerpt).length === 0), (want = 'no active story');
|
|
736
|
+
if (!ok && ex.hasCurrentStorySection && declaresNoActiveStory(excerpt))
|
|
737
|
+
why = ' The section also has a "**Story:**" field: contradictory identity. ' + FIX_HINT;
|
|
738
|
+
}
|
|
617
739
|
else {
|
|
618
740
|
const ident = activeStoryIdentity(excerpt);
|
|
619
741
|
want = `Current Story containing "${exp.contains}" whose "**Story:**" field is exactly ${p.story.id}`;
|
|
620
|
-
if (
|
|
621
|
-
(ok = false), (why = '
|
|
742
|
+
if (ex.ambiguous)
|
|
743
|
+
(ok = false), (why = ' active.md has more than one "## Current Story" section outside code blocks and comments; the active story is ambiguous. ' + FIX_HINT);
|
|
744
|
+
else if (!visible.includes(exp.contains))
|
|
745
|
+
(ok = false), (why = ` The expected text is not in the Current Story section${ident.ok ? '.' : `, and ${ident.reason}`}`);
|
|
622
746
|
else if (!ident.ok)
|
|
623
747
|
(ok = false), (why = ` ${ident.reason}`);
|
|
624
748
|
else if (ident.id !== p.story.id)
|
|
@@ -661,7 +785,28 @@ function plan(ctx, p) {
|
|
|
661
785
|
evidence: [integrity.sha, governance.sha],
|
|
662
786
|
config: [ctx.config.requiredDocs, ctx.config.allowedDocs],
|
|
663
787
|
}));
|
|
664
|
-
|
|
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 };
|
|
665
810
|
}
|
|
666
811
|
export function planFinalization(targetDir, proposal) {
|
|
667
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"}
|