@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.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/brief/cli.d.ts.map +1 -1
  3. package/dist/brief/cli.js +8 -2
  4. package/dist/brief/collect.d.ts.map +1 -1
  5. package/dist/brief/collect.js +11 -3
  6. package/dist/brief/freshness.d.ts +17 -2
  7. package/dist/brief/freshness.d.ts.map +1 -1
  8. package/dist/brief/freshness.js +22 -4
  9. package/dist/brief/git-signals.d.ts +32 -5
  10. package/dist/brief/git-signals.d.ts.map +1 -1
  11. package/dist/brief/git-signals.js +169 -39
  12. package/dist/brief/knowledge.d.ts +62 -0
  13. package/dist/brief/knowledge.d.ts.map +1 -0
  14. package/dist/brief/knowledge.js +123 -0
  15. package/dist/brief/reconcile.d.ts +7 -1
  16. package/dist/brief/reconcile.d.ts.map +1 -1
  17. package/dist/brief/reconcile.js +42 -2
  18. package/dist/cli/index.js +1 -1
  19. package/dist/cli/style.d.ts +26 -0
  20. package/dist/cli/style.d.ts.map +1 -0
  21. package/dist/cli/style.js +76 -0
  22. package/dist/cli/ui.d.ts.map +1 -1
  23. package/dist/cli/ui.js +33 -6
  24. package/dist/coordinatorium/cli.d.ts.map +1 -1
  25. package/dist/coordinatorium/cli.js +13 -0
  26. package/dist/finalize/cli.d.ts.map +1 -1
  27. package/dist/finalize/cli.js +30 -13
  28. package/dist/finalize/consistency.d.ts +51 -0
  29. package/dist/finalize/consistency.d.ts.map +1 -0
  30. package/dist/finalize/consistency.js +57 -0
  31. package/dist/finalize/core.d.ts +26 -0
  32. package/dist/finalize/core.d.ts.map +1 -1
  33. package/dist/finalize/core.js +140 -1
  34. package/dist/ideas/cli.d.ts +1 -0
  35. package/dist/ideas/cli.d.ts.map +1 -1
  36. package/dist/ideas/cli.js +14 -3
  37. package/dist/ideas/format.d.ts +31 -1
  38. package/dist/ideas/format.d.ts.map +1 -1
  39. package/dist/ideas/format.js +109 -1
  40. package/dist/ideas/store.d.ts +2 -0
  41. package/dist/ideas/store.d.ts.map +1 -1
  42. package/dist/ideas/store.js +4 -1
  43. package/dist/ideas/types.d.ts +10 -0
  44. package/dist/ideas/types.d.ts.map +1 -1
  45. package/dist/ideas/types.js +3 -0
  46. package/dist/readmodel/dashboard.d.ts.map +1 -1
  47. package/dist/readmodel/dashboard.js +19 -1
  48. package/dist/readmodel/index.d.ts +2 -1
  49. package/dist/readmodel/index.d.ts.map +1 -1
  50. package/dist/readmodel/index.js +1 -0
  51. package/dist/readmodel/roadmap.d.ts +34 -0
  52. package/dist/readmodel/roadmap.d.ts.map +1 -0
  53. package/dist/readmodel/roadmap.js +254 -0
  54. package/dist/readmodel/types.d.ts +88 -0
  55. package/dist/readmodel/types.d.ts.map +1 -1
  56. package/dist/root/safe-write.d.ts +15 -4
  57. package/dist/root/safe-write.d.ts.map +1 -1
  58. package/dist/root/safe-write.js +264 -50
  59. package/dist/ui/assets/index-BJKxUDPD.css +1 -0
  60. package/dist/ui/assets/index-ioC9DPHQ.js +28 -0
  61. package/dist/ui/index.html +2 -2
  62. package/dist/validate/index.d.ts.map +1 -1
  63. package/dist/validate/index.js +4 -3
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +5 -2
  67. package/dist/ui/assets/index-5zXAgig8.css +0 -1
  68. 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
+ }
@@ -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":"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"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/finalize/core.ts"],"names":[],"mappings":"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"}
@@ -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
- return { plan: { status, storyId: p.story.id, digest, partial: someDone && !allDone, checks, targets, diagnostics }, internals };
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);
@@ -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;
@@ -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;CAC5B;AAMD,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CA4BrD;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;AAiBD,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"}
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 KNOWN_FLAGS = [...ADD_FLAGS, STDIN_FLAG];
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}`));
@@ -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;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAyC,KAAK,IAAI,EAAmB,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAS7G,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,GAAG,OAAO,CAmBvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAahD;AAqBD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CA0DhE"}
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"}
@@ -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
  }
@@ -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,CAetD;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,CA4ElG;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"}
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"}
@@ -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.