@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.
- package/README.md +24 -1
- package/dist/brief/collect.d.ts.map +1 -1
- package/dist/brief/collect.js +6 -2
- package/dist/brief/freshness.d.ts +33 -5
- package/dist/brief/freshness.d.ts.map +1 -1
- package/dist/brief/freshness.js +67 -13
- package/dist/brief/markdown-context.d.ts +22 -0
- package/dist/brief/markdown-context.d.ts.map +1 -0
- package/dist/brief/markdown-context.js +66 -0
- package/dist/brief/story.d.ts +2 -10
- package/dist/brief/story.d.ts.map +1 -1
- package/dist/brief/story.js +21 -7
- package/dist/finalize/core.d.ts +2 -0
- package/dist/finalize/core.d.ts.map +1 -1
- package/dist/finalize/core.js +46 -40
- package/dist/ui/apple-touch-icon.png +0 -0
- package/dist/ui/assets/index-5zXAgig8.css +1 -0
- package/dist/ui/assets/{index-C4NhMaq7.js → index-CQgoMYLM.js} +5 -5
- package/dist/ui/assets/monolith-mark-Ct0ammrU.webp +0 -0
- package/dist/ui/favicon-16x16.png +0 -0
- package/dist/ui/favicon-32x32.png +0 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/index.html +6 -2
- package/dist/update/cli.d.ts +1 -1
- package/dist/update/cli.d.ts.map +1 -1
- package/dist/update/cli.js +10 -3
- package/dist/update/index.d.ts +9 -0
- package/dist/update/index.d.ts.map +1 -1
- package/dist/update/index.js +50 -1
- package/dist/update/legacy-skills.d.ts +15 -0
- package/dist/update/legacy-skills.d.ts.map +1 -0
- package/dist/update/legacy-skills.js +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md +183 -0
- package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +19 -3
- package/dist/ui/assets/index-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.
|
|
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;
|
|
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"}
|
package/dist/brief/collect.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;
|
|
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"}
|
package/dist/brief/freshness.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
package/dist/brief/story.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/brief/story.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 (
|
|
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 =
|
|
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/finalize/core.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,EAUL,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/finalize/core.ts"],"names":[],"mappings":"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"}
|
package/dist/finalize/core.js
CHANGED
|
@@ -25,6 +25,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
|
-
/**
|
|
173
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
203
|
-
const
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
for (const raw of norm(currentStoryBody).split('\n')) {
|
|
304
|
-
const line = raw.trim();
|
|
305
|
-
if (comment) {
|
|
306
|
-
if (line.includes('-->'))
|
|
307
|
-
comment = false;
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
const f = /^(`{3,}|~{3,})/.exec(line);
|
|
311
|
-
if (fence !== null) {
|
|
312
|
-
if (f !== null && f[1][0] === fence[0] && f[1].length >= fence.length && line === f[1])
|
|
313
|
-
fence = null;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (f !== null) {
|
|
317
|
-
fence = f[1];
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (line.startsWith('<!--')) {
|
|
321
|
-
if (!line.includes('-->'))
|
|
322
|
-
comment = true;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
const m = /^\*\*Story:\*\*[ \t]*(.*)$/.exec(raw.replace(/\r$/, ''));
|
|
326
|
-
if (m !== null)
|
|
327
|
-
found.push(m[1].trim());
|
|
328
|
-
}
|
|
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
|
-
|
|
616
|
-
|
|
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 (
|
|
621
|
-
(ok = false), (why = '
|
|
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
|