@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
@@ -62,6 +62,7 @@ export function reconcile(ev, result) {
62
62
  }
63
63
  // 2. Git-derived evidence.
64
64
  const untrackedWork = [];
65
+ const independentKnowledge = [];
65
66
  let mentionedWork = [];
66
67
  if (ev.git.status === 'unavailable') {
67
68
  findings.push({ kind: 'unknown', code: 'GIT_UNAVAILABLE', summary: reasonMessage(result, 'GIT_UNAVAILABLE'), evidence: [] });
@@ -77,8 +78,20 @@ export function reconcile(ev, result) {
77
78
  const activity = ev.git.commitsSince.filter((c) => c.files.some((f) => !ev.stateFiles.has(f)));
78
79
  const byId = new Map();
79
80
  const unattributed = [];
81
+ const knowledgeBy = new Map();
80
82
  for (const c of activity) {
81
83
  const ids = extractWorkItemIds(c.subject);
84
+ const k = ids.length === 0 ? ev.knowledge?.attributed.get(c.sha) : undefined;
85
+ if (k !== undefined) {
86
+ const key = k.ideaIds.join(', ');
87
+ const entry = knowledgeBy.get(key) ?? { paths: [], lines: [] };
88
+ entry.lines.push(commitLine(c));
89
+ for (const p of k.paths)
90
+ if (!entry.paths.includes(p))
91
+ entry.paths.push(p);
92
+ knowledgeBy.set(key, entry);
93
+ continue;
94
+ }
82
95
  if (ids.length === 0)
83
96
  unattributed.push(c);
84
97
  for (const id of ids)
@@ -117,11 +130,21 @@ export function reconcile(ev, result) {
117
130
  kind: 'suspected',
118
131
  code: 'UNATTRIBUTED_ACTIVITY',
119
132
  summary: `${unattributed.length} commit(s) after the last state update change project files without naming a work item; whether the state documents cover them cannot be determined.`,
120
- evidence: unattributed.slice(0, MAX_COMMITS_PER_ITEM).map(commitLine),
133
+ evidence: unattributed.slice(0, MAX_COMMITS_PER_ITEM).map((c) => {
134
+ const why = ev.knowledge?.rejected.get(c.sha);
135
+ return why === undefined ? commitLine(c) : `${commitLine(c)} — names an idea but is not attributed: ${why.reason}`;
136
+ }),
121
137
  });
122
138
  actions.push(`Review the ${unattributed.length} commit(s) that name no work item and decide whether the recorded state covers them.`);
123
139
  }
124
- if (activity.length > 0 && ev.activeStory.kind === 'none') {
140
+ for (const [ids, entry] of knowledgeBy) {
141
+ independentKnowledge.push({ ideaIds: ids.split(', '), paths: entry.paths, commits: entry.lines });
142
+ }
143
+ if (knowledgeBy.size > 0) {
144
+ limitations.push('Commits attributed to independent knowledge are explained by a registered idea and its declared document paths; this is attribution, not approval or review, and says nothing about any work item or the active story.');
145
+ }
146
+ const explainedByKnowledge = [...knowledgeBy.values()].reduce((n, e) => n + e.lines.length, 0);
147
+ if (activity.length - explainedByKnowledge > 0 && ev.activeStory.kind === 'none') {
125
148
  findings.push({
126
149
  kind: 'suspected',
127
150
  code: 'ACTIVITY_WITHOUT_ACTIVE_STORY',
@@ -131,6 +154,11 @@ export function reconcile(ev, result) {
131
154
  review('active.md', 'no active story recorded while git shows later activity');
132
155
  actions.push('Review the active story: confirm whether any work is current. No active story does not mean the project is complete.');
133
156
  }
157
+ if (ev.git.shallow === true) {
158
+ findings.push({ kind: 'unknown', code: 'GIT_HISTORY_SHALLOW', summary: reasonMessage(result, 'GIT_HISTORY_SHALLOW'), evidence: [] });
159
+ limitations.push('The repository is a shallow clone; history before its boundary is absent, so the report cannot show what happened earlier.');
160
+ actions.push('Deepen the clone (git fetch --unshallow) or run this from a full checkout, then re-run.');
161
+ }
134
162
  if (ev.git.truncated) {
135
163
  findings.push({ kind: 'unknown', code: 'GIT_HISTORY_TRUNCATED', summary: reasonMessage(result, 'GIT_HISTORY_TRUNCATED'), evidence: [] });
136
164
  limitations.push('Git history after the last state update was cut at the read limit; older commits are not examined.');
@@ -156,6 +184,7 @@ export function reconcile(ev, result) {
156
184
  findings,
157
185
  untrackedWork,
158
186
  mentionedWork,
187
+ independentKnowledge,
159
188
  documents,
160
189
  limitations,
161
190
  actions,
@@ -219,6 +248,17 @@ export function renderReconciliation(report, maxChars) {
219
248
  }
220
249
  sections.push(bullets(title, lines));
221
250
  }
251
+ if (report.independentKnowledge.length > 0) {
252
+ const lines = [];
253
+ for (const k of report.independentKnowledge.slice(0, MAX_LISTED_ITEMS)) {
254
+ lines.push(`- ${san(k.ideaIds.join(', '))} (${k.commits.length} commit(s))`);
255
+ const shownPaths = k.paths.slice(0, MAX_LISTED_ITEMS).map(san).join(', ');
256
+ lines.push(` declared paths: ${shownPaths}${k.paths.length > MAX_LISTED_ITEMS ? ` and ${k.paths.length - MAX_LISTED_ITEMS} more` : ''}`);
257
+ for (const c of k.commits.slice(0, MAX_COMMITS_PER_ITEM))
258
+ lines.push(` ${san(c)}`);
259
+ }
260
+ sections.push(bullets('Attributed to registered independent knowledge (no work item, active story unaffected):', lines));
261
+ }
222
262
  if (report.documents.length > 0) {
223
263
  const lines = [];
224
264
  for (const d of report.documents)
@@ -1,18 +1,10 @@
1
- /**
2
- * Extracts the "## Current Story" section from
3
- * `.monolith/memory/working/active.md` — confirmed structure only, no
4
- * heuristic Markdown interpretation (section 5 of the brief contract).
5
- *
6
- * Only two things are recognized: a line that is exactly a level-1..6
7
- * heading, and a line that is exactly `---` (used in active.md as a visual
8
- * section separator). The "Current Story" section's content is everything
9
- * between its own heading line and the next such boundary.
10
- */
11
1
  export interface StoryExtraction {
12
2
  hasCurrentStorySection: boolean;
13
3
  excerpt: string | null;
14
4
  /** 1-indexed inclusive line range, e.g. "24-29". */
15
5
  lineRange: string | null;
6
+ /** More than one real "## Current Story" heading; no section is returned. */
7
+ ambiguous?: boolean;
16
8
  }
17
9
  export declare function extractCurrentStory(content: string): StoryExtraction;
18
10
  //# sourceMappingURL=story.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"story.d.ts","sourceRoot":"","sources":["../../src/brief/story.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,MAAM,WAAW,eAAe;IAC9B,sBAAsB,EAAE,OAAO,CAAC;IAChC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAoDpE"}
1
+ {"version":3,"file":"story.d.ts","sourceRoot":"","sources":["../../src/brief/story.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,eAAe;IAC9B,sBAAsB,EAAE,OAAO,CAAC;IAChC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CA0DpE"}
@@ -7,25 +7,39 @@
7
7
  * heading, and a line that is exactly `---` (used in active.md as a visual
8
8
  * section separator). The "Current Story" section's content is everything
9
9
  * between its own heading line and the next such boundary.
10
+ *
11
+ * Headings and separators are structural only outside fenced code blocks and
12
+ * HTML comments (PSC-004): a `## Current Story` shown inside a Markdown
13
+ * example is not the section. If more than one real heading exists the
14
+ * section is ambiguous and is NOT read (`ambiguous: true`, no section): no
15
+ * consumer may guess which one is the project's current story.
10
16
  */
17
+ import { maskNonContent } from './markdown-context.js';
11
18
  const CURRENT_STORY_HEADING_RE = /^##\s+Current Story\s*$/i;
12
19
  const HEADING_RE = /^#{1,6}\s+/;
13
20
  const RULE_RE = /^-{3,}\s*$/;
14
21
  export function extractCurrentStory(content) {
15
22
  const lines = content.split('\n');
16
- let headingLineIdx = -1;
23
+ // Same line count as `lines`; fenced / commented text is blank.
24
+ const structural = maskNonContent(content).split('\n');
25
+ const headings = [];
17
26
  for (let i = 0; i < lines.length; i++) {
18
- if (CURRENT_STORY_HEADING_RE.test(lines[i].trim())) {
19
- headingLineIdx = i;
20
- break;
21
- }
27
+ // The heading line must be untouched by masking: a line carrying (or
28
+ // opening) an HTML comment is not a heading, so no comment state can leak
29
+ // from it into the section body.
30
+ if (CURRENT_STORY_HEADING_RE.test(structural[i].trim()) && structural[i].trim() === lines[i].trim())
31
+ headings.push(i);
22
32
  }
23
- if (headingLineIdx === -1) {
33
+ if (headings.length === 0) {
24
34
  return { hasCurrentStorySection: false, excerpt: null, lineRange: null };
25
35
  }
36
+ if (headings.length > 1) {
37
+ return { hasCurrentStorySection: false, excerpt: null, lineRange: null, ambiguous: true };
38
+ }
39
+ const headingLineIdx = headings[0];
26
40
  let endIdx = lines.length;
27
41
  for (let i = headingLineIdx + 1; i < lines.length; i++) {
28
- const trimmed = lines[i].trim();
42
+ const trimmed = structural[i].trim();
29
43
  if (HEADING_RE.test(trimmed) || RULE_RE.test(trimmed)) {
30
44
  endIdx = i;
31
45
  break;
package/dist/cli/index.js CHANGED
@@ -73,7 +73,7 @@ Durable usage:
73
73
  monolith durable retention --retain-days N [--compact-after N] [--apply]
74
74
 
75
75
  Idea usage:
76
- monolith idea add --title "..." (--description "..." | --description-stdin) [--context "..."]
76
+ monolith idea add --title "..." (--description "..." | --description-stdin) [--context "..."] [--path <project-relative document file> ...]
77
77
  monolith idea list
78
78
  monolith idea show <id>
79
79
 
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Semantic terminal colour (CLI-UX-001). Presentation only.
3
+ *
4
+ * Colour never carries meaning alone: every status word (PASS, FAIL, FRESH, …)
5
+ * is printed as text first and merely tinted. ANSI is emitted only to an
6
+ * interactive terminal; `NO_COLOR` (any non-empty value), `TERM=dumb`, pipes,
7
+ * redirects and `--json` output stay plain. No dependency, no state.
8
+ */
9
+ export type Tone = 'good' | 'warn' | 'bad' | 'dim' | 'head';
10
+ /** Status word → tone. Words not listed are never tinted. */
11
+ export declare const STATUS_TONE: Readonly<Record<string, Tone>>;
12
+ export interface TextStream {
13
+ isTTY?: boolean;
14
+ }
15
+ /** True only for an interactive terminal that has not opted out. */
16
+ export declare function colorEnabled(stream?: TextStream, env?: NodeJS.ProcessEnv): boolean;
17
+ export declare function paint(text: string, tone: Tone, enabled: boolean): string;
18
+ /** Tints a bare status word; unknown words and a disabled style return it unchanged. */
19
+ export declare function statusWord(word: string, enabled: boolean): string;
20
+ /**
21
+ * Tints status tokens in a block of already-rendered text: the value after
22
+ * `Label: ` on the given labels, and `[STATUS]` markers at line start. Text is
23
+ * otherwise untouched, so disabling colour returns the input byte for byte.
24
+ */
25
+ export declare function tintStatuses(text: string, enabled: boolean, labels?: readonly string[]): string;
26
+ //# sourceMappingURL=style.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/cli/style.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAU5D,6DAA6D;AAC7D,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CA2BtD,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,MAAM,GAAE,UAA2B,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/G;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAExE;AAED,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAGjE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAOnG"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Semantic terminal colour (CLI-UX-001). Presentation only.
3
+ *
4
+ * Colour never carries meaning alone: every status word (PASS, FAIL, FRESH, …)
5
+ * is printed as text first and merely tinted. ANSI is emitted only to an
6
+ * interactive terminal; `NO_COLOR` (any non-empty value), `TERM=dumb`, pipes,
7
+ * redirects and `--json` output stay plain. No dependency, no state.
8
+ */
9
+ const CODES = {
10
+ good: '32', // green
11
+ warn: '33', // yellow
12
+ bad: '31', // red
13
+ dim: '90', // grey
14
+ head: '1;35', // bold purple
15
+ };
16
+ /** Status word → tone. Words not listed are never tinted. */
17
+ export const STATUS_TONE = {
18
+ PASS: 'good',
19
+ COMPLETE: 'good',
20
+ COMPLETED: 'good',
21
+ FRESH: 'good',
22
+ READY: 'good',
23
+ FINALIZED: 'good',
24
+ ALREADY_FINALIZED: 'good',
25
+ CONSISTENT: 'good',
26
+ done: 'good',
27
+ NOT_VERIFIED: 'warn',
28
+ UNKNOWN: 'warn',
29
+ WARNING: 'warn',
30
+ UPDATE: 'warn',
31
+ INCOMPLETE: 'warn',
32
+ pending: 'warn',
33
+ FAIL: 'bad',
34
+ CONFLICT: 'bad',
35
+ conflict: 'bad',
36
+ STALE: 'bad',
37
+ INCONSISTENT: 'bad',
38
+ REFUSED: 'bad',
39
+ NOT_READY: 'bad',
40
+ missing: 'bad',
41
+ unreadable: 'bad',
42
+ UNCHANGED: 'dim',
43
+ SKIP: 'dim',
44
+ };
45
+ /** True only for an interactive terminal that has not opted out. */
46
+ export function colorEnabled(stream = process.stdout, env = process.env) {
47
+ if (stream.isTTY !== true)
48
+ return false;
49
+ if ((env.NO_COLOR ?? '') !== '')
50
+ return false;
51
+ if (env.TERM === 'dumb')
52
+ return false;
53
+ return true;
54
+ }
55
+ export function paint(text, tone, enabled) {
56
+ return enabled ? `\u001b[${CODES[tone]}m${text}\u001b[0m` : text;
57
+ }
58
+ /** Tints a bare status word; unknown words and a disabled style return it unchanged. */
59
+ export function statusWord(word, enabled) {
60
+ const tone = STATUS_TONE[word];
61
+ return tone === undefined ? word : paint(word, tone, enabled);
62
+ }
63
+ /**
64
+ * Tints status tokens in a block of already-rendered text: the value after
65
+ * `Label: ` on the given labels, and `[STATUS]` markers at line start. Text is
66
+ * otherwise untouched, so disabling colour returns the input byte for byte.
67
+ */
68
+ export function tintStatuses(text, enabled, labels = []) {
69
+ if (!enabled)
70
+ return text;
71
+ let out = text.replace(/^(\s*)\[([A-Za-z_]+)\]/gm, (m, sp, w) => (STATUS_TONE[w] ? `${sp}[${statusWord(w, true)}]` : m));
72
+ for (const label of labels) {
73
+ out = out.replace(new RegExp(`^(${label}: )([A-Za-z_]+)`, 'gm'), (_m, pre, w) => `${pre}${statusWord(w, true)}`);
74
+ }
75
+ return out;
76
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/cli/ui.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;GAWG;AAWH,kDAAkD;AAClD,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAE3C,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,qFAAqF;IACrF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAMD,yGAAyG;AACzG,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAqB/D;AAiBD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuBvE;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoD1E;AAED,sGAAsG;AACtG,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA8C3D"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/cli/ui.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;GAWG;AAYH,kDAAkD;AAClD,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAE3C,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,qFAAqF;IACrF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAMD,yGAAyG;AACzG,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAqB/D;AA2CD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqBvE;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuD1E;AAED,sGAAsG;AACtG,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA8C3D"}
package/dist/cli/ui.js CHANGED
@@ -17,6 +17,7 @@ import { dirname, join, resolve } from 'path';
17
17
  import { fileURLToPath } from 'url';
18
18
  import { startDashboardServer, loadUiAssets, UiAssetsError } from '../http/index.js';
19
19
  import { RootContainmentError, resolveMonolithRoot } from '../root/index.js';
20
+ import { colorEnabled, paint, statusWord } from './style.js';
20
21
  import { DEFAULT_DEV_UI_PORT, DEFAULT_UI_PORT, UI_DEV_USAGE, UI_USAGE, parseUiArgs } from './ui-args.js';
21
22
  /** Thrown for expected, user-fixable failures. */
22
23
  export class UiLaunchError extends Error {
@@ -47,6 +48,31 @@ export function assertMonolithProject(projectRoot) {
47
48
  throw new UiLaunchError(`No MONOLITH instance found in ${projectRoot} (expected directory ${rootAbs}). Run 'monolith init' here, or cd into a MONOLITH project.`);
48
49
  }
49
50
  }
51
+ /**
52
+ * The launch block (CLI-UX-001). Printed only after the server is actually
53
+ * listening, so READY is never shown for a failed start (failures throw before
54
+ * this and are reported on stderr). The first line is the alpha.3 machine-readable
55
+ * `MONOLITH Mission Control: <url>` (scripts that parse it keep working); the URL
56
+ * also stands on a line of its own for copying. The CLI does not open a browser
57
+ * and says so by asking the user to.
58
+ */
59
+ function announce(log, info) {
60
+ const on = colorEnabled();
61
+ for (const line of [
62
+ // Compatibility (A4-CLI-02): the first line stays the machine-readable `<title>: <url>` that alpha.3 scripts parse.
63
+ `${paint(info.title, 'head', on)}: ${info.url}`,
64
+ `Status: ${statusWord('READY', on)}`,
65
+ '',
66
+ 'Open in your browser:',
67
+ ` ${info.url}`,
68
+ '',
69
+ `Project: ${info.projectRoot}`,
70
+ info.note,
71
+ 'Press Ctrl+C to stop.',
72
+ ]) {
73
+ log(line);
74
+ }
75
+ }
50
76
  async function listen(projectRoot, port, assets) {
51
77
  try {
52
78
  return await startDashboardServer({ projectRoot, port, assets });
@@ -77,9 +103,7 @@ export async function launchUi(opts) {
77
103
  throw err;
78
104
  }
79
105
  const server = await listen(projectRoot, opts.port, assets);
80
- log(`MONOLITH Mission Control: ${server.url}`);
81
- log(`Project: ${projectRoot}`);
82
- log('Read-only. Listening on 127.0.0.1 only. Press Ctrl+C to stop.');
106
+ announce(log, { title: 'MONOLITH Mission Control', url: server.url, projectRoot, note: 'Read-only. Listening on 127.0.0.1 only.' });
83
107
  return {
84
108
  url: server.url,
85
109
  stop: () => server.close(),
@@ -124,9 +148,12 @@ export async function launchUiDev(opts) {
124
148
  await stop();
125
149
  return code;
126
150
  });
127
- log(`MONOLITH Mission Control (dev, HMR): http://127.0.0.1:${opts.port}`);
128
- log(`Project: ${projectRoot} (dashboard API on ${api.url}, read-only)`);
129
- log('Listening on 127.0.0.1 only. Press Ctrl+C to stop.');
151
+ announce(log, {
152
+ title: 'MONOLITH Mission Control (dev, HMR)',
153
+ url: `http://127.0.0.1:${opts.port}`,
154
+ projectRoot,
155
+ note: `Read-only dashboard API on ${api.url}. Listening on 127.0.0.1 only.`,
156
+ });
130
157
  return { url: `http://127.0.0.1:${opts.port}`, stop, ended };
131
158
  }
132
159
  /** CLI entry for `monolith ui ...`. Resolves with the process exit code once the launch has ended. */
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/coordinatorium/cli.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AAEH,OAAO,EAAgE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAChH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAwBhD;;;;;;;;;;;;GAYG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAG,SAAS,CAAU;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;gBAEZ,QAAQ,EAAE,cAAc,GAAG,iBAAiB;CAiBzD;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAS5E;AA8BD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAS9F;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB9F;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7F;AAeD,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnG;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAepG;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/F"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/coordinatorium/cli.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AAEH,OAAO,EAAgE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAChH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAwBhD;;;;;;;;;;;;GAYG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAG,SAAS,CAAU;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;gBAEZ,QAAQ,EAAE,cAAc,GAAG,iBAAiB;CAiBzD;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAS5E;AA8BD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAS9F;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB9F;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7F;AA8BD,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBnG;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAepG;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/F"}
@@ -155,7 +155,20 @@ function formatCheckpoint(c) {
155
155
  ];
156
156
  return lines.join('\n');
157
157
  }
158
+ const CHECKPOINT_VALUE_FLAGS = ['--summary', '--remaining', '--context'];
159
+ /** `mission checkpoint` takes only its documented options; an unknown `--option` is an error, not silently dropped. */
160
+ function rejectUnknownCheckpointFlags(args) {
161
+ for (let i = 1; i < args.length; i++) {
162
+ if (CHECKPOINT_VALUE_FLAGS.includes(args[i]))
163
+ i++; // its value, whatever it looks like
164
+ else if (args[i].startsWith('--')) {
165
+ throw new Error(`Unknown option ${JSON.stringify(args[i].slice(0, 40))} for mission checkpoint.\n` +
166
+ 'Usage: monolith mission checkpoint <missionId> --summary "..." [--remaining "..." ...] [--context "..."]');
167
+ }
168
+ }
169
+ }
158
170
  export async function runMissionCheckpoint(args, targetDir = process.cwd()) {
171
+ rejectUnknownCheckpointFlags(args);
159
172
  const missionId = args[0];
160
173
  const summary = parseFlag(args, '--summary');
161
174
  if (!missionId || !summary) {
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/finalize/cli.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAuFH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBnG"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/finalize/cli.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAsGH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBnG"}
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { lstatSync, readFileSync } from 'fs';
11
11
  import { applyFinalization, planFinalization } from './core.js';
12
+ import { colorEnabled, tintStatuses } from '../cli/style.js';
12
13
  import { parseProposal } from './proposal.js';
13
14
  const USAGE = `Usage:
14
15
  monolith finalize plan --proposal <file|-> [--json]
@@ -74,32 +75,48 @@ async function readProposal(src) {
74
75
  throw new Error(`invalid proposal:\n${parsed.errors.map((e) => ` - ${e}`).join('\n')}`);
75
76
  return parsed.proposal;
76
77
  }
78
+ function printConsistency(c) {
79
+ if (c === undefined)
80
+ return;
81
+ say(`\nState consistency (${c.projected ? 'projected after apply' : 'actual'}): ${c.status}`);
82
+ for (const f of c.findings)
83
+ say(` [CONFLICT] ${printable(f.message)}\n → ${printable(f.remediation)}`);
84
+ for (const n of c.notVerified)
85
+ say(` [NOT_VERIFIED] ${printable(n)}`);
86
+ if (c.status !== 'CONSISTENT')
87
+ say(' This does not change the outcome of the finalization; MONOLITH edits none of the documents above on its own.');
88
+ say(` Scope: ${printable(c.scope)}`);
89
+ }
90
+ const say = (line) => console.log(tintStatuses(line, colorEnabled(), ['Finalization plan for [^:]+', 'Finalization of [^:]+', 'State consistency [^:]+']));
77
91
  function printPlan(pl) {
78
- console.log(`Finalization plan for ${printable(pl.storyId)}: ${pl.status}${pl.partial ? ' (PARTIAL: an earlier run was interrupted; apply rolls it forward)' : ''}`);
79
- console.log('\nPreconditions:');
92
+ say(`Finalization plan for ${printable(pl.storyId)}: ${pl.status}${pl.partial ? ' (PARTIAL: an earlier run was interrupted; apply rolls it forward)' : ''}`);
93
+ say('\nPreconditions:');
80
94
  for (const c of pl.checks)
81
- console.log(` [${c.status}] ${c.id}: ${printable(c.detail)}\n source: ${printable(c.source)}`);
82
- console.log('\nTargets (in write order, active.md last):');
95
+ say(` [${c.status}] ${c.id}: ${printable(c.detail)}\n source: ${printable(c.source)}`);
96
+ say('\nTargets (in write order, active.md last):');
83
97
  for (const t of pl.targets)
84
- console.log(` [${t.state}] ${t.id} → ${printable(t.path)}${t.state === 'done' ? '' : ` — ${printable(t.reason)}`}`);
98
+ say(` [${t.state}] ${t.id} → ${printable(t.path)}${t.state === 'done' ? '' : ` — ${printable(t.reason)}`}`);
85
99
  for (const d of pl.diagnostics)
86
- console.log(`\nNote: ${printable(d)}`);
100
+ say(`\nNote: ${printable(d)}`);
101
+ printConsistency(pl.consistency);
87
102
  if (pl.status === 'READY')
88
- console.log(`\nDigest: ${pl.digest}\nOwner approval: monolith finalize apply --proposal <same file> --approve ${pl.digest}\nThe digest shows this plan is unchanged; it does not prove who approves it. Do not run apply unless the owner approved this exact plan.`);
103
+ say(`\nDigest: ${pl.digest}\nOwner approval: monolith finalize apply --proposal <same file> --approve ${pl.digest}\nThe digest shows this plan is unchanged; it does not prove who approves it. Do not run apply unless the owner approved this exact plan.`);
89
104
  }
90
105
  function printApply(r) {
91
- console.log(`Finalization of ${printable(r.storyId)}: ${r.status}`);
106
+ say(`Finalization of ${printable(r.storyId)}: ${r.status}`);
92
107
  if (r.applied.length > 0)
93
- console.log(` written: ${r.applied.join(', ')}`);
108
+ say(` written: ${r.applied.join(', ')}`);
94
109
  if (r.status !== 'FINALIZED' && r.pending.length > 0)
95
- console.log(` not written: ${r.pending.join(', ')}`);
110
+ say(` not written: ${r.pending.join(', ')}`);
96
111
  if (r.failed)
97
- console.log(` failed: ${r.failed.target}${r.failed.publication ? ` [${r.failed.publication}]` : ''}: ${printable(r.failed.message)}`);
112
+ say(` failed: ${r.failed.target}${r.failed.publication ? ` [${r.failed.publication}]` : ''}: ${printable(r.failed.message)}`);
98
113
  for (const d of r.diagnostics)
99
- console.log(` ${printable(d)}`);
114
+ say(` ${printable(d)}`);
100
115
  if (r.status === 'REFUSED')
101
116
  for (const c of r.plan.checks.filter((x) => x.status !== 'PASS'))
102
- console.log(` [${c.status}] ${c.id}: ${printable(c.detail)}`);
117
+ say(` [${c.status}] ${c.id}: ${printable(c.detail)}`);
118
+ if (r.status === 'FINALIZED' || r.status === 'ALREADY_FINALIZED')
119
+ printConsistency(r.plan.consistency);
103
120
  }
104
121
  export async function runFinalizeCommand(args, targetDir = process.cwd()) {
105
122
  const sub = args[0];
@@ -0,0 +1,51 @@
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
+ export type ConsistencyStatus = 'CONSISTENT' | 'INCONSISTENT' | 'NOT_VERIFIED';
20
+ export interface ConsistencyFinding {
21
+ message: string;
22
+ /** State documents (file names) that disagree. */
23
+ documents: string[];
24
+ /** The subset of `documents` that this finalization does not write. */
25
+ externalDocuments: string[];
26
+ /** What a person can do; MONOLITH does not do it. */
27
+ remediation: string;
28
+ }
29
+ export interface ConsistencyAssessment {
30
+ status: ConsistencyStatus;
31
+ /** true: computed from the texts a pending plan would write; false: read from disk. */
32
+ projected: boolean;
33
+ findings: ConsistencyFinding[];
34
+ /** Why the check could not be completed (NOT_VERIFIED). */
35
+ notVerified: string[];
36
+ /** What this check does not cover. */
37
+ scope: string;
38
+ }
39
+ export interface ConsistencyInput {
40
+ /** Text of each state document as it is / will be; null = missing or unreadable. */
41
+ active: string | null;
42
+ roadmap: string | null;
43
+ storyIndex: string | null;
44
+ /** Project-relative paths this finalization writes (or has written). */
45
+ writtenPaths: string[];
46
+ projected: boolean;
47
+ /** The MONOLITH root (project-relative); documents are matched by their full canonical path. */
48
+ managedRoot?: string;
49
+ }
50
+ export declare function assessConsistency(input: ConsistencyInput): ConsistencyAssessment;
51
+ //# sourceMappingURL=consistency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consistency.d.ts","sourceRoot":"","sources":["../../src/finalize/consistency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,cAAc,GAAG,cAAc,CAAC;AAE/E,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,uFAAuF;IACvF,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,2DAA2D;IAC3D,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,oFAAoF;IACpF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wEAAwE;IACxE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,qBAAqB,CAmChF"}
@@ -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 = {
@@ -101,6 +127,8 @@ export type ActiveIdentity = {
101
127
  * evidence files and `## Last Finalization`); its FIRST token is the id. Ids
102
128
  * mentioned in titles, descriptions, comments, code fences or other fields carry
103
129
  * no authority. Zero, several, or malformed field lines → no identity (fail closed).
130
+ * A section whose first visible line says "no active story" AND that has a
131
+ * structural field is contradictory → no identity (PSC-004A).
104
132
  */
105
133
  export declare function activeStoryIdentity(currentStoryBody: string): ActiveIdentity;
106
134
  export declare function planFinalization(targetDir: string, proposal: FinalizationProposal): FinalizationPlan;