@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.
Files changed (90) hide show
  1. package/README.md +24 -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 +17 -5
  6. package/dist/brief/freshness.d.ts +50 -7
  7. package/dist/brief/freshness.d.ts.map +1 -1
  8. package/dist/brief/freshness.js +89 -17
  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/markdown-context.d.ts +22 -0
  16. package/dist/brief/markdown-context.d.ts.map +1 -0
  17. package/dist/brief/markdown-context.js +66 -0
  18. package/dist/brief/reconcile.d.ts +7 -1
  19. package/dist/brief/reconcile.d.ts.map +1 -1
  20. package/dist/brief/reconcile.js +42 -2
  21. package/dist/brief/story.d.ts +2 -10
  22. package/dist/brief/story.d.ts.map +1 -1
  23. package/dist/brief/story.js +21 -7
  24. package/dist/cli/index.js +1 -1
  25. package/dist/cli/style.d.ts +26 -0
  26. package/dist/cli/style.d.ts.map +1 -0
  27. package/dist/cli/style.js +76 -0
  28. package/dist/cli/ui.d.ts.map +1 -1
  29. package/dist/cli/ui.js +33 -6
  30. package/dist/coordinatorium/cli.d.ts.map +1 -1
  31. package/dist/coordinatorium/cli.js +13 -0
  32. package/dist/finalize/cli.d.ts.map +1 -1
  33. package/dist/finalize/cli.js +30 -13
  34. package/dist/finalize/consistency.d.ts +51 -0
  35. package/dist/finalize/consistency.d.ts.map +1 -0
  36. package/dist/finalize/consistency.js +57 -0
  37. package/dist/finalize/core.d.ts +28 -0
  38. package/dist/finalize/core.d.ts.map +1 -1
  39. package/dist/finalize/core.js +186 -41
  40. package/dist/ideas/cli.d.ts +1 -0
  41. package/dist/ideas/cli.d.ts.map +1 -1
  42. package/dist/ideas/cli.js +14 -3
  43. package/dist/ideas/format.d.ts +31 -1
  44. package/dist/ideas/format.d.ts.map +1 -1
  45. package/dist/ideas/format.js +109 -1
  46. package/dist/ideas/store.d.ts +2 -0
  47. package/dist/ideas/store.d.ts.map +1 -1
  48. package/dist/ideas/store.js +4 -1
  49. package/dist/ideas/types.d.ts +10 -0
  50. package/dist/ideas/types.d.ts.map +1 -1
  51. package/dist/ideas/types.js +3 -0
  52. package/dist/readmodel/dashboard.d.ts.map +1 -1
  53. package/dist/readmodel/dashboard.js +19 -1
  54. package/dist/readmodel/index.d.ts +2 -1
  55. package/dist/readmodel/index.d.ts.map +1 -1
  56. package/dist/readmodel/index.js +1 -0
  57. package/dist/readmodel/roadmap.d.ts +34 -0
  58. package/dist/readmodel/roadmap.d.ts.map +1 -0
  59. package/dist/readmodel/roadmap.js +254 -0
  60. package/dist/readmodel/types.d.ts +88 -0
  61. package/dist/readmodel/types.d.ts.map +1 -1
  62. package/dist/root/safe-write.d.ts +15 -4
  63. package/dist/root/safe-write.d.ts.map +1 -1
  64. package/dist/root/safe-write.js +264 -50
  65. package/dist/ui/apple-touch-icon.png +0 -0
  66. package/dist/ui/assets/index-BJKxUDPD.css +1 -0
  67. package/dist/ui/assets/index-ioC9DPHQ.js +28 -0
  68. package/dist/ui/assets/monolith-mark-Ct0ammrU.webp +0 -0
  69. package/dist/ui/favicon-16x16.png +0 -0
  70. package/dist/ui/favicon-32x32.png +0 -0
  71. package/dist/ui/favicon.ico +0 -0
  72. package/dist/ui/index.html +6 -2
  73. package/dist/update/cli.d.ts +1 -1
  74. package/dist/update/cli.d.ts.map +1 -1
  75. package/dist/update/cli.js +10 -3
  76. package/dist/update/index.d.ts +9 -0
  77. package/dist/update/index.d.ts.map +1 -1
  78. package/dist/update/index.js +50 -1
  79. package/dist/update/legacy-skills.d.ts +15 -0
  80. package/dist/update/legacy-skills.d.ts.map +1 -0
  81. package/dist/update/legacy-skills.js +20 -0
  82. package/dist/validate/index.d.ts.map +1 -1
  83. package/dist/validate/index.js +4 -3
  84. package/dist/version.d.ts +1 -1
  85. package/dist/version.js +1 -1
  86. package/package.json +5 -2
  87. package/templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md +183 -0
  88. package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +19 -3
  89. package/dist/ui/assets/index-C4NhMaq7.js +0 -28
  90. 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":"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"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/finalize/core.ts"],"names":[],"mappings":"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"}
@@ -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
- /** Boundaries as in brief/story.ts: the next heading or a `---` rule ends a section. */
173
- function findSection(lines, re) {
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
- lines.forEach((l, i) => {
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 = lines[i].trim();
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 cur = findSection(lines, CURRENT_RE);
203
- const last = findSection(lines, LAST_RE);
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
- let fence = null;
302
- let comment = false;
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
- if (exp === 'none')
616
- (ok = ex.hasCurrentStorySection && /no active story/i.test(excerpt)), (want = 'no active story');
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 (!excerpt.includes(exp.contains))
621
- (ok = false), (why = ' The expected text is not in the Current Story section.');
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
- 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 };
665
810
  }
666
811
  export function planFinalization(targetDir, proposal) {
667
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"}