@latysh-development/monolith 0.1.0-alpha.2 → 0.1.0-alpha.3

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 (38) hide show
  1. package/README.md +24 -1
  2. package/dist/brief/collect.d.ts.map +1 -1
  3. package/dist/brief/collect.js +6 -2
  4. package/dist/brief/freshness.d.ts +33 -5
  5. package/dist/brief/freshness.d.ts.map +1 -1
  6. package/dist/brief/freshness.js +67 -13
  7. package/dist/brief/markdown-context.d.ts +22 -0
  8. package/dist/brief/markdown-context.d.ts.map +1 -0
  9. package/dist/brief/markdown-context.js +66 -0
  10. package/dist/brief/story.d.ts +2 -10
  11. package/dist/brief/story.d.ts.map +1 -1
  12. package/dist/brief/story.js +21 -7
  13. package/dist/finalize/core.d.ts +2 -0
  14. package/dist/finalize/core.d.ts.map +1 -1
  15. package/dist/finalize/core.js +46 -40
  16. package/dist/ui/apple-touch-icon.png +0 -0
  17. package/dist/ui/assets/index-5zXAgig8.css +1 -0
  18. package/dist/ui/assets/{index-C4NhMaq7.js → index-CQgoMYLM.js} +5 -5
  19. package/dist/ui/assets/monolith-mark-Ct0ammrU.webp +0 -0
  20. package/dist/ui/favicon-16x16.png +0 -0
  21. package/dist/ui/favicon-32x32.png +0 -0
  22. package/dist/ui/favicon.ico +0 -0
  23. package/dist/ui/index.html +6 -2
  24. package/dist/update/cli.d.ts +1 -1
  25. package/dist/update/cli.d.ts.map +1 -1
  26. package/dist/update/cli.js +10 -3
  27. package/dist/update/index.d.ts +9 -0
  28. package/dist/update/index.d.ts.map +1 -1
  29. package/dist/update/index.js +50 -1
  30. package/dist/update/legacy-skills.d.ts +15 -0
  31. package/dist/update/legacy-skills.d.ts.map +1 -0
  32. package/dist/update/legacy-skills.js +20 -0
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +1 -1
  36. package/templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md +183 -0
  37. package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +19 -3
  38. package/dist/ui/assets/index-DMZ7ZvZY.css +0 -1
package/README.md CHANGED
@@ -101,6 +101,29 @@ monolith ui # Mission Control at http://127.0.0.1:4173 ; Ctrl+C to stop
101
101
  instance, and it does not open a browser — open the printed address yourself.
102
102
  All commands: [CLI reference](./docs/reference/cli.md).
103
103
 
104
+ ### 🧠 One More Thing: Give Your AI Chat the Context!
105
+
106
+ MONOLITH is installed. Now make sure your AI assistant knows how to use it.
107
+
108
+ Open `.monolith/context/MONOLITH-CHAT-CONTEXT.md` and copy its contents into your ChatGPT Project instructions, Claude conversation, or another AI workspace you use to coordinate development.
109
+
110
+ It explains MONOLITH's capabilities, project memory, workflows, and limitations.
111
+
112
+ **Your coding agent gets the protocols. Your working chat gets the context.**
113
+
114
+ `monolith init` creates this file. Print it from the project root (the directory where you ran `monolith init`):
115
+
116
+ ```bash
117
+ cat .monolith/context/MONOLITH-CHAT-CONTEXT.md
118
+ ```
119
+
120
+ If `monolithRoot` in `monolith.config.json` is not `.monolith`, use that directory instead.
121
+ Projects initialized with an earlier MONOLITH version do not get this file from
122
+ `monolith update`; copy it from the template in this repository (replace
123
+ `{{MONOLITH_ROOT}}` with `.monolith`, otherwise `monolith validate` reports an
124
+ unreplaced placeholder). You can also read the template before installing:
125
+ [`templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md`](./templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md).
126
+
104
127
  ## Updating an Existing Project
105
128
 
106
129
  If a project was initialised with an earlier MONOLITH version, bring it up to the current agent integration (agent entry point and Idea Capture) with:
@@ -160,7 +183,7 @@ listing — not an official partnership, endorsement or certification.
160
183
 
161
184
  ## Project Status
162
185
 
163
- **Alpha** (`0.1.0-alpha.2`). The API and CLI are experimental and may change
186
+ **Alpha** (`0.1.0-alpha.3`). The API and CLI are experimental and may change
164
187
  without notice. Published to npm under the alpha dist-tag. Durable State and the Coordinatorium
165
188
  (`monolith durable`, `monolith mission`) are experimental and are not part of
166
189
  the Quick Start.
@@ -1 +1 @@
1
- {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/brief/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwBH,OAAO,KAAK,EACV,UAAU,EAOX,MAAM,YAAY,CAAC;AAiBpB,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,QAAa,CAAC;AA8TnD,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqEpE"}
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/brief/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAyBH,OAAO,KAAK,EACV,UAAU,EAOX,MAAM,YAAY,CAAC;AAiBpB,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,QAAa,CAAC;AAiUnD,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqEpE"}
@@ -16,6 +16,7 @@ import { readBriefConfig } from './config.js';
16
16
  import { resolveMonolithRootValue, checkContainedFile, readBoundedFileStable, isReadSecurityViolation, findWriteArtifacts, describeWriteArtifacts, } from '../root/index.js';
17
17
  import { discoverDocs } from './docs.js';
18
18
  import { extractCurrentStory } from './story.js';
19
+ import { maskNonContent } from './markdown-context.js';
19
20
  import { selectActiveMissions, selectRecentFinishedMissions, selectLatestCheckpoint, hasCheckpointOrderMismatch, } from './selectors.js';
20
21
  import { stateFilePath, validateState, createInitialState, MAX_STATE_BYTES } from '../coordinatorium/index.js';
21
22
  import { ACTIVE_MD_REL, PARKED_IDEAS_LIMIT, PROTOCOLS_DIR, ROADMAP_INDEX_REL, STORY_INDEX_REL, HANDOFF_REL } from './constants.js';
@@ -202,7 +203,9 @@ function collectStory(targetDir, monolithRoot, root, diagnostics) {
202
203
  diagnostics.push({
203
204
  code: 'ACTIVE_MD_NO_CURRENT_STORY',
204
205
  severity: 'info',
205
- message: `${relPath} exists but has no "## Current Story" section.`,
206
+ message: extraction.ambiguous
207
+ ? `${relPath} has more than one "## Current Story" section outside code blocks and comments; the section is ambiguous and was not read.`
208
+ : `${relPath} exists but has no "## Current Story" section.`,
206
209
  });
207
210
  }
208
211
  return {
@@ -273,7 +276,8 @@ function collectFreshness(targetDir, monolithRoot, root, story) {
273
276
  roadmapActiveStory: roadmap === null ? null : parseRoadmapActiveStory(roadmap),
274
277
  roadmapStatuses: roadmap === null ? new Map() : parseRoadmapStatuses(roadmap),
275
278
  storyIndexStatuses: storyIndex === null ? new Map() : parseStoryIndexStatuses(storyIndex),
276
- recordedText: [...content.values()].filter((c) => c !== null).join('\n'),
279
+ // Ids shown only inside code fences or HTML comments are examples, not records.
280
+ recordedText: [...content.values()].filter((c) => c !== null).map(maskNonContent).join('\n'),
277
281
  stateFiles: new Set(stateFiles),
278
282
  };
279
283
  return { result: evaluateFreshness(evidence), evidence };
@@ -19,6 +19,7 @@
19
19
  * absence of an active story alone never produce STALE, and the
20
20
  * absence of an active story never means "project complete".
21
21
  */
22
+ import { maskNonContent } from './markdown-context.js';
22
23
  export type FreshnessStatus = 'FRESH' | 'STALE' | 'UNKNOWN';
23
24
  export type FreshnessReasonCode = 'STATE_CONTRADICTION' | 'UNRECORDED_WORK' | 'GIT_UNAVAILABLE' | 'STATE_NOT_IN_GIT' | 'GIT_HISTORY_TRUNCATED' | 'UNATTRIBUTED_ACTIVITY' | 'NO_ACTIVITY_SINCE_STATE_UPDATE' | 'ACTIVITY_RECORDED';
24
25
  export interface FreshnessReason {
@@ -57,8 +58,14 @@ export type StoryClaim = {
57
58
  } | {
58
59
  kind: 'ids';
59
60
  ids: string[];
60
- } | {
61
+ }
62
+ /**
63
+ * `contradiction`: the section both says "no active story" and has a
64
+ * structural `**Story:**` field (PSC-004A) — the field value(s), verbatim.
65
+ */
66
+ | {
61
67
  kind: 'unknown';
68
+ contradiction?: string[];
62
69
  };
63
70
  export interface FreshnessEvidence {
64
71
  git: GitSignals;
@@ -92,13 +99,34 @@ export interface FreshnessResult {
92
99
  }
93
100
  export declare function extractWorkItemIds(text: string): string[];
94
101
  export declare function mentionsId(text: string, id: string): boolean;
95
- /** Interprets the `## Current Story` excerpt of active.md. */
102
+ export { maskNonContent };
103
+ /**
104
+ * Interprets the `## Current Story` excerpt of active.md.
105
+ *
106
+ * Identity comes from the structural `**Story:** <id> …` field only (first
107
+ * token = id), exactly as DOCSYNC reads it. Ids in titles, prose, code fences
108
+ * or comments are never identity. Zero usable, several or malformed fields →
109
+ * `unknown` (never a guess, never STALE on its own).
110
+ *
111
+ * Compatibility: a historical section with no `**Story:**` field at all is
112
+ * still read when its first line is a *bold declaration* of exactly one
113
+ * work-item id — `**WIDGET-001 — title**`, `**WIDGET-001**` or
114
+ * `**WIDGET-001** — status` (the form old active.md files used). An unbolded
115
+ * line (`STORY-001 was completed yesterday.`, `STORY-001 — done`) is
116
+ * indistinguishable from a sentence that merely mentions a story, so it is
117
+ * `unknown`, never a confirmed identity.
118
+ *
119
+ * "No active story" as the first visible line is `none` only when the section
120
+ * has no structural `**Story:**` field at all; with one (valid, duplicate or
121
+ * malformed) the section contradicts itself → `unknown` with `contradiction`
122
+ * (PSC-004A, same rule as DOCSYNC's `activeStoryIdentity()`).
123
+ */
96
124
  export declare function parseActiveStoryClaim(excerpt: string | null): StoryClaim;
97
- /** `Active Story: <text>` line of roadmap-index.md. */
125
+ /** `Active Story: <text>` line of roadmap-index.md (outside code fences and comments). */
98
126
  export declare function parseRoadmapActiveStory(content: string): string | null;
99
- /** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
127
+ /** Table rows `| id | title | STATUS | date |` of roadmap-index.md (outside code fences and comments). */
100
128
  export declare function parseRoadmapStatuses(content: string): Map<string, string>;
101
- /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
129
+ /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md (outside code fences and comments). */
102
130
  export declare function parseStoryIndexStatuses(content: string): Map<string, string>;
103
131
  export declare function evaluateFreshness(ev: FreshnessEvidence): FreshnessResult;
104
132
  //# sourceMappingURL=freshness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAyB,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE9E,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5D,MAAM,MAAM,mBAAmB,GAC3B,qBAAqB,GACrB,iBAAiB,GACjB,iBAAiB,GACjB,kBAAkB,GAClB,uBAAuB,GACvB,uBAAuB,GACvB,gCAAgC,GAChC,mBAAmB,CAAC;AAExB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,8FAA8F;IAC9F,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG,sBAAsB,GAAG,YAAY,CAAC;AAE/F,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GACvD;IACE,MAAM,EAAE,IAAI,CAAC;IACb,gFAAgF;IAChF,eAAe,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACtD,gFAAgF;IAChF,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAEN,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE;AAChC;;;GAGG;GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,UAAU,CAAC;IAChB,iHAAiH;IACjH,WAAW,EAAE,UAAU,CAAC;IACxB,6GAA6G;IAC7G,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qGAAqG;IACrG,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,+GAA+G;IAC/G,YAAY,EAAE,MAAM,CAAC;IACrB,sGAAsG;IACtG,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,eAAe,GAAG,UAAU,GAAG,SAAS,CAAC;IACtD,eAAe,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACtD,6FAA6F;IAC7F,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kGAAkG;IAClG,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAyBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOzD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAG5D;AAID,OAAO,EAAE,cAAc,EAAE,CAAC;AAW1B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,CAuBxE;AAED,0FAA0F;AAC1F,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGtE;AAED,0GAA0G;AAC1G,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAYzE;AAED,mHAAmH;AACnH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAgB5E;AAqED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,iBAAiB,GAAG,eAAe,CA2GxE"}
@@ -19,6 +19,8 @@
19
19
  * absence of an active story alone never produce STALE, and the
20
20
  * absence of an active story never means "project complete".
21
21
  */
22
+ import { isIdeaId } from '../ideas/format.js';
23
+ import { declaresNoActiveStory, maskNonContent } from './markdown-context.js';
22
24
  // ---------------------------------------------------------------------------
23
25
  // Work-item ids
24
26
  // ---------------------------------------------------------------------------
@@ -30,8 +32,14 @@
30
32
  const WORK_ITEM_SOURCE = String.raw `(?:[A-Z]{2,}-)+\d{3}[A-Z]?(?:\.\d+)*(?:-[A-Z])?`;
31
33
  /** Decision / lesson records are not work items. */
32
34
  const NON_WORK_PREFIXES = ['ADR-', 'DEC-', 'LESSON-'];
35
+ /**
36
+ * Ideas live in their own namespace (`IDEA-NNN`, src/ideas/format.ts) and are
37
+ * stored in the idea store, not in the state documents. Only the exact idea
38
+ * grammar is excluded: `IDEA-001A`, `IDEA-001B.1` or `IDEA-003-R` are story
39
+ * ids that merely share the prefix and stay work items.
40
+ */
33
41
  function isWorkItem(id) {
34
- return !NON_WORK_PREFIXES.some((p) => id.startsWith(p));
42
+ return !NON_WORK_PREFIXES.some((p) => id.startsWith(p)) && !isIdeaId(id);
35
43
  }
36
44
  export function extractWorkItemIds(text) {
37
45
  const re = new RegExp(`(?<![A-Za-z0-9-])${WORK_ITEM_SOURCE}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`, 'g');
@@ -46,29 +54,69 @@ export function mentionsId(text, id) {
46
54
  const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
55
  return new RegExp(`(?<![A-Za-z0-9-])${escaped}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`).test(text);
48
56
  }
57
+ // `maskNonContent` lives in ./markdown-context.ts (shared with story extraction
58
+ // and DOCSYNC) and is re-exported here for existing importers.
59
+ export { maskNonContent };
49
60
  // ---------------------------------------------------------------------------
50
61
  // Parsers for the state documents (confirmed structure only)
51
62
  // ---------------------------------------------------------------------------
52
63
  const NONE_RE = /^\W*(?:no active story|none|unknown)\b|^\s*[—-]\s*$/i;
53
- /** Interprets the `## Current Story` excerpt of active.md. */
64
+ /** Same id grammar as DOCSYNC's `activeStoryIdentity()`. */
65
+ const STORY_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
66
+ /**
67
+ * Interprets the `## Current Story` excerpt of active.md.
68
+ *
69
+ * Identity comes from the structural `**Story:** <id> …` field only (first
70
+ * token = id), exactly as DOCSYNC reads it. Ids in titles, prose, code fences
71
+ * or comments are never identity. Zero usable, several or malformed fields →
72
+ * `unknown` (never a guess, never STALE on its own).
73
+ *
74
+ * Compatibility: a historical section with no `**Story:**` field at all is
75
+ * still read when its first line is a *bold declaration* of exactly one
76
+ * work-item id — `**WIDGET-001 — title**`, `**WIDGET-001**` or
77
+ * `**WIDGET-001** — status` (the form old active.md files used). An unbolded
78
+ * line (`STORY-001 was completed yesterday.`, `STORY-001 — done`) is
79
+ * indistinguishable from a sentence that merely mentions a story, so it is
80
+ * `unknown`, never a confirmed identity.
81
+ *
82
+ * "No active story" as the first visible line is `none` only when the section
83
+ * has no structural `**Story:**` field at all; with one (valid, duplicate or
84
+ * malformed) the section contradicts itself → `unknown` with `contradiction`
85
+ * (PSC-004A, same rule as DOCSYNC's `activeStoryIdentity()`).
86
+ */
54
87
  export function parseActiveStoryClaim(excerpt) {
55
88
  if (excerpt === null)
56
89
  return { kind: 'unknown' };
57
- const firstLine = excerpt.split('\n').find((l) => l.trim() !== '') ?? '';
58
- if (/^\W*no active story\b/i.test(firstLine))
59
- return { kind: 'none' };
60
- const ids = extractWorkItemIds(excerpt);
61
- return ids.length > 0 ? { kind: 'ids', ids } : { kind: 'unknown' };
90
+ const masked = maskNonContent(excerpt);
91
+ const firstLine = masked.split('\n').find((l) => l.trim() !== '') ?? '';
92
+ const fields = masked
93
+ .split('\n')
94
+ .map((l) => /^\*\*Story:\*\*[ \t]*(.*)$/.exec(l.replace(/\r$/, '')))
95
+ .filter((m) => m !== null)
96
+ .map((m) => m[1].trim());
97
+ if (declaresNoActiveStory(excerpt))
98
+ return fields.length === 0 ? { kind: 'none' } : { kind: 'unknown', contradiction: fields };
99
+ if (fields.length > 0) {
100
+ if (fields.length > 1)
101
+ return { kind: 'unknown' };
102
+ const id = /^(\S+)(?:\s|$)/.exec(fields[0])?.[1];
103
+ return id !== undefined && STORY_ID_RE.test(id) ? { kind: 'ids', ids: [id] } : { kind: 'unknown' };
104
+ }
105
+ const line = firstLine.trim();
106
+ const idEnd = String.raw `(?![A-Za-z0-9]|\.\d|-[A-Z0-9])`;
107
+ const declaration = new RegExp(`^\\*\\*(${WORK_ITEM_SOURCE})${idEnd}\\*\\*(?:\\s+[—–-](?:\\s.*)?)?$`).exec(line) ??
108
+ new RegExp(`^\\*\\*(${WORK_ITEM_SOURCE})${idEnd}\\s+[—–-]\\s.*\\*\\*$`).exec(line);
109
+ return declaration !== null && extractWorkItemIds(line).length === 1 ? { kind: 'ids', ids: [declaration[1]] } : { kind: 'unknown' };
62
110
  }
63
- /** `Active Story: <text>` line of roadmap-index.md. */
111
+ /** `Active Story: <text>` line of roadmap-index.md (outside code fences and comments). */
64
112
  export function parseRoadmapActiveStory(content) {
65
- const m = /^Active Story:\s*(.+?)\s*$/m.exec(content);
113
+ const m = /^Active Story:\s*(.+?)\s*$/m.exec(maskNonContent(content));
66
114
  return m ? m[1] : null;
67
115
  }
68
- /** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
116
+ /** Table rows `| id | title | STATUS | date |` of roadmap-index.md (outside code fences and comments). */
69
117
  export function parseRoadmapStatuses(content) {
70
118
  const out = new Map();
71
- for (const line of content.split('\n')) {
119
+ for (const line of maskNonContent(content).split('\n')) {
72
120
  const cells = line.split('|').map((c) => c.trim());
73
121
  // leading and trailing '|' produce empty first/last cells
74
122
  if (cells.length < 5 || cells[0] !== '' || cells[cells.length - 1] !== '')
@@ -82,10 +130,10 @@ export function parseRoadmapStatuses(content) {
82
130
  }
83
131
  return out;
84
132
  }
85
- /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
133
+ /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md (outside code fences and comments). */
86
134
  export function parseStoryIndexStatuses(content) {
87
135
  const out = new Map();
88
- const lines = content.split('\n');
136
+ const lines = maskNonContent(content).split('\n');
89
137
  for (let i = 0; i < lines.length; i++) {
90
138
  const h = /^###\s+(?:STORY\s+)?([A-Za-z0-9][A-Za-z0-9.\-]*)\s+—/.exec(lines[i]);
91
139
  if (!h)
@@ -117,6 +165,12 @@ function isDone(status) {
117
165
  }
118
166
  function findContradictions(ev) {
119
167
  const out = [];
168
+ if (ev.activeStory.kind === 'unknown' && ev.activeStory.contradiction !== undefined) {
169
+ out.push({
170
+ message: `active.md records no active story, but its Current Story section also has a **Story:** field (${ev.activeStory.contradiction.map((f) => `"${f.slice(0, 40)}"`).join(', ')}).`,
171
+ documents: ['active.md'],
172
+ });
173
+ }
120
174
  if (ev.activeStory.kind === 'none' && ev.roadmapActiveStory !== null && !NONE_RE.test(ev.roadmapActiveStory)) {
121
175
  out.push({
122
176
  message: `active.md records no active story, but roadmap-index.md names "${ev.roadmapActiveStory}".`,
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Markdown context shared by the Brief, Freshness, Reconcile and DOCSYNC
3
+ * readers of the project state documents. Pure, deterministic, no I/O.
4
+ */
5
+ /**
6
+ * True when the first visible line of a Current Story excerpt (outside code
7
+ * fences and HTML comments) declares that there is no active story. Shared by
8
+ * the Brief and DOCSYNC (PSC-004A): such a declaration next to a structural
9
+ * `**Story:**` field is a contradiction, never a confirmed "none" and never
10
+ * a confirmed identity.
11
+ */
12
+ export declare function declaresNoActiveStory(excerpt: string): boolean;
13
+ /**
14
+ * Blanks every line (or part of a line) that sits inside a fenced code block
15
+ * or an HTML comment, keeping the line count. Fence grammar matches DOCSYNC's
16
+ * `activeStoryIdentity()`: a line that starts with ``` or ~~~ opens a fence,
17
+ * and only a line made of the same character, at least as long, closes it.
18
+ * An unterminated fence or comment hides the rest of the text — an example
19
+ * that cannot be delimited is never read as a claim.
20
+ */
21
+ export declare function maskNonContent(text: string): string;
22
+ //# sourceMappingURL=markdown-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-context.d.ts","sourceRoot":"","sources":["../../src/brief/markdown-context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuCnD"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Markdown context shared by the Brief, Freshness, Reconcile and DOCSYNC
3
+ * readers of the project state documents. Pure, deterministic, no I/O.
4
+ */
5
+ /**
6
+ * True when the first visible line of a Current Story excerpt (outside code
7
+ * fences and HTML comments) declares that there is no active story. Shared by
8
+ * the Brief and DOCSYNC (PSC-004A): such a declaration next to a structural
9
+ * `**Story:**` field is a contradiction, never a confirmed "none" and never
10
+ * a confirmed identity.
11
+ */
12
+ export function declaresNoActiveStory(excerpt) {
13
+ const first = maskNonContent(excerpt.replace(/\r\n/g, '\n')).split('\n').find((l) => l.trim() !== '') ?? '';
14
+ return /^\W*no active story\b/i.test(first);
15
+ }
16
+ /**
17
+ * Blanks every line (or part of a line) that sits inside a fenced code block
18
+ * or an HTML comment, keeping the line count. Fence grammar matches DOCSYNC's
19
+ * `activeStoryIdentity()`: a line that starts with ``` or ~~~ opens a fence,
20
+ * and only a line made of the same character, at least as long, closes it.
21
+ * An unterminated fence or comment hides the rest of the text — an example
22
+ * that cannot be delimited is never read as a claim.
23
+ */
24
+ export function maskNonContent(text) {
25
+ let fence = null;
26
+ let comment = false;
27
+ const out = [];
28
+ for (const raw of text.split('\n')) {
29
+ const line = raw.trim();
30
+ const f = /^(`{3,}|~{3,})/.exec(line);
31
+ if (fence !== null) {
32
+ if (f !== null && f[1][0] === fence[0] && f[1].length >= fence.length && line === f[1])
33
+ fence = null;
34
+ out.push('');
35
+ continue;
36
+ }
37
+ if (!comment && f !== null) {
38
+ fence = f[1];
39
+ out.push('');
40
+ continue;
41
+ }
42
+ let kept = '';
43
+ let i = 0;
44
+ while (i < raw.length) {
45
+ if (comment) {
46
+ const end = raw.indexOf('-->', i);
47
+ if (end === -1)
48
+ break;
49
+ comment = false;
50
+ i = end + 3;
51
+ }
52
+ else {
53
+ const start = raw.indexOf('<!--', i);
54
+ if (start === -1) {
55
+ kept += raw.slice(i);
56
+ break;
57
+ }
58
+ kept += raw.slice(i, start);
59
+ comment = true;
60
+ i = start + 4;
61
+ }
62
+ }
63
+ out.push(kept);
64
+ }
65
+ return out.join('\n');
66
+ }
@@ -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;
@@ -101,6 +101,8 @@ export type ActiveIdentity = {
101
101
  * evidence files and `## Last Finalization`); its FIRST token is the id. Ids
102
102
  * mentioned in titles, descriptions, comments, code fences or other fields carry
103
103
  * no authority. Zero, several, or malformed field lines → no identity (fail closed).
104
+ * A section whose first visible line says "no active story" AND that has a
105
+ * structural field is contradictory → no identity (PSC-004A).
104
106
  */
105
107
  export declare function activeStoryIdentity(currentStoryBody: string): ActiveIdentity;
106
108
  export declare function planFinalization(targetDir: string, proposal: FinalizationProposal): FinalizationPlan;
@@ -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,7 @@ 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';
28
29
  import { validDate, validateRelPath } from './proposal.js';
29
30
  const MAX_DOC_BYTES = 4 * 1024 * 1024;
30
31
  const LOCK_NAME = 'finalize.lock';
@@ -169,11 +170,16 @@ function activeBodies(root, p) {
169
170
  // Text transforms (pure)
170
171
  // ---------------------------------------------------------------------------
171
172
  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) {
173
+ /**
174
+ * Boundaries as in brief/story.ts: the next heading or a `---` rule ends a
175
+ * section, and headings inside code fences or HTML comments do not count.
176
+ * `structural` is the same text with fenced / commented lines blanked (same
177
+ * line count), so reading and writing agree on which section is "Current Story".
178
+ */
179
+ function findSection(lines, structural, re) {
174
180
  const hits = [];
175
- lines.forEach((l, i) => {
176
- if (re.test(l.trim()))
181
+ structural.forEach((l, i) => {
182
+ if (re.test(l.trim()) && l.trim() === lines[i].trim())
177
183
  hits.push(i);
178
184
  });
179
185
  if (hits.length === 0)
@@ -182,7 +188,7 @@ function findSection(lines, re) {
182
188
  return 'ambiguous';
183
189
  let end = lines.length;
184
190
  for (let i = hits[0] + 1; i < lines.length; i++) {
185
- const t = lines[i].trim();
191
+ const t = structural[i].trim();
186
192
  if (/^#{1,6}\s+/.test(t) || /^-{3,}\s*$/.test(t)) {
187
193
  end = i;
188
194
  break;
@@ -199,8 +205,9 @@ const CURRENT_RE = /^##\s+Current Story\s*$/i;
199
205
  const LAST_RE = /^##\s+Last Finalization\s*$/i;
200
206
  function editActive(content, bodies) {
201
207
  const lines = content.split('\n');
202
- const cur = findSection(lines, CURRENT_RE);
203
- const last = findSection(lines, LAST_RE);
208
+ const structural = maskNonContent(content).split('\n');
209
+ const cur = findSection(lines, structural, CURRENT_RE);
210
+ const last = findSection(lines, structural, LAST_RE);
204
211
  for (const [name, s] of [['## Current Story', cur], ['## Last Finalization', last]]) {
205
212
  if (s === 'absent')
206
213
  return { state: 'conflict', reason: `active.md has no "${name}" section; it is never created automatically.` };
@@ -295,37 +302,13 @@ const FIX_HINT = 'To finalize, put exactly one line "**Story:** <id> — <title>
295
302
  * evidence files and `## Last Finalization`); its FIRST token is the id. Ids
296
303
  * mentioned in titles, descriptions, comments, code fences or other fields carry
297
304
  * no authority. Zero, several, or malformed field lines → no identity (fail closed).
305
+ * A section whose first visible line says "no active story" AND that has a
306
+ * structural field is contradictory → no identity (PSC-004A).
298
307
  */
299
308
  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
- }
309
+ const found = structuralStoryFields(currentStoryBody);
310
+ if (found.length > 0 && declaresNoActiveStory(currentStoryBody))
311
+ 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
312
  if (found.length === 0)
330
313
  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
314
  if (found.length > 1)
@@ -335,6 +318,19 @@ export function activeStoryIdentity(currentStoryBody) {
335
318
  return { ok: false, reason: `the "**Story:**" line ("${found[0].slice(0, 60)}") does not start with a valid story id. ${FIX_HINT}` };
336
319
  return { ok: true, id };
337
320
  }
321
+ /** Values of the structural `**Story:**` field lines (outside code fences and HTML comments). */
322
+ function structuralStoryFields(currentStoryBody) {
323
+ const found = [];
324
+ // Same fence / HTML-comment grammar as the brief's story extraction and
325
+ // freshness readers (one shared helper): text inside a code fence or an HTML
326
+ // comment, including a comment opened mid-line, is never identity.
327
+ for (const raw of maskNonContent(norm(currentStoryBody)).split('\n')) {
328
+ const m = /^\*\*Story:\*\*[ \t]*(.*)$/.exec(raw.replace(/\r$/, ''));
329
+ if (m !== null)
330
+ found.push(m[1].trim());
331
+ }
332
+ return found;
333
+ }
338
334
  /**
339
335
  * Attested evidence. Contract: exactly one `**Status:**` (PASS or FAIL, nothing
340
336
  * else on the line), exactly one `**Story:**` whose FIRST token is the story id,
@@ -612,13 +608,23 @@ function plan(ctx, p) {
612
608
  let ok;
613
609
  let want;
614
610
  let why = '';
615
- if (exp === 'none')
616
- (ok = ex.hasCurrentStorySection && /no active story/i.test(excerpt)), (want = 'no active story');
611
+ const visible = maskNonContent(excerpt);
612
+ if (exp === 'none') {
613
+ // Same rule as the brief: the first visible line says "no active story"
614
+ // and no structural **Story:** field (valid, duplicate or malformed)
615
+ // contradicts it (PSC-004A). Text inside code fences or comments is
616
+ // not a statement.
617
+ (ok = ex.hasCurrentStorySection && declaresNoActiveStory(excerpt) && structuralStoryFields(excerpt).length === 0), (want = 'no active story');
618
+ if (!ok && ex.hasCurrentStorySection && declaresNoActiveStory(excerpt))
619
+ why = ' The section also has a "**Story:**" field: contradictory identity. ' + FIX_HINT;
620
+ }
617
621
  else {
618
622
  const ident = activeStoryIdentity(excerpt);
619
623
  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.');
624
+ if (ex.ambiguous)
625
+ (ok = false), (why = ' active.md has more than one "## Current Story" section outside code blocks and comments; the active story is ambiguous. ' + FIX_HINT);
626
+ else if (!visible.includes(exp.contains))
627
+ (ok = false), (why = ` The expected text is not in the Current Story section${ident.ok ? '.' : `, and ${ident.reason}`}`);
622
628
  else if (!ident.ok)
623
629
  (ok = false), (why = ` ${ident.reason}`);
624
630
  else if (ident.id !== p.story.id)
Binary file