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

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.
@@ -0,0 +1,5 @@
1
+ export { parseProposal, validateRelPath, PROPOSAL_SCHEMA_VERSION } from './proposal.js';
2
+ export type { FinalizationProposal, ProposalParse, ExpectedCurrentStory } from './proposal.js';
3
+ export { planFinalization, applyFinalization, readFinalizeConfig, renderCapsule } from './core.js';
4
+ export type { ApplyResult, ApplyStatus, Check, CheckStatus, FinalizationPlan, FinalizeConfig, FinalizeOptions, PlanStatus, PlannedTarget, TargetKind, TargetState, } from './core.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/finalize/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxF,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACnG,YAAY,EACV,WAAW,EACX,WAAW,EACX,KAAK,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,UAAU,EACV,aAAa,EACb,UAAU,EACV,WAAW,GACZ,MAAM,WAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { parseProposal, validateRelPath, PROPOSAL_SCHEMA_VERSION } from './proposal.js';
2
+ export { planFinalization, applyFinalization, readFinalizeConfig, renderCapsule } from './core.js';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Finalization proposal (DOCSYNC-001): the typed, strictly validated input an
3
+ * AI assistant or developer prepares. It carries the *content* of the
4
+ * finalization; MONOLITH decides whether and where it may be applied.
5
+ *
6
+ * Validation is structural and syntactic only. Whether the content is *true*
7
+ * (the summary matches the code) is the owner's judgement, taken when they
8
+ * approve the plan digest — never something this module claims to check.
9
+ */
10
+ export declare const PROPOSAL_SCHEMA_VERSION = 1;
11
+ /**
12
+ * What the Current Story section of active.md must hold before the transition.
13
+ * There is deliberately no "any": a proposal cannot switch the guard off and
14
+ * silently replace somebody else's active story. `contains` must name the
15
+ * story being finalized (checked in `parseProposal`).
16
+ */
17
+ export type ExpectedCurrentStory = 'none' | {
18
+ contains: string;
19
+ };
20
+ export interface FinalizationProposal {
21
+ schemaVersion: 1;
22
+ story: {
23
+ /** Story identifier, e.g. `DOCSYNC-001` or `1.3`. Used as the idempotency key. */
24
+ id: string;
25
+ title: string;
26
+ /** Phase directory name under `memory/working/phases/`, e.g. `phase-1`. */
27
+ phase: string;
28
+ /** Text of the `## ` heading in story-index.md the entry belongs to. */
29
+ phaseHeading: string;
30
+ /** Capsule file base name (without `.md`), e.g. `story-docsync-001-finalization`. */
31
+ slug: string;
32
+ /** `YYYY-MM-DD`. Supplied by the caller: MONOLITH never invents a finalization date. */
33
+ date: string;
34
+ };
35
+ /** What the Current Story section of active.md must look like before the transition. */
36
+ expectedCurrentStory: ExpectedCurrentStory;
37
+ work: {
38
+ objective: string;
39
+ implementationSummary: string;
40
+ filesChanged: string[];
41
+ tests: string;
42
+ keyDecisions: string;
43
+ adrs: string[];
44
+ lessons: string;
45
+ };
46
+ /** One-paragraph summary for story-index.md. */
47
+ summary: string;
48
+ keyOutputs: string[];
49
+ history: {
50
+ phaseName: string;
51
+ decision: string;
52
+ build: string;
53
+ };
54
+ /** Complete replacement of HANDOFF.md. Optional: omitted means HANDOFF.md is not touched. */
55
+ handoff?: {
56
+ content: string;
57
+ };
58
+ /** Extra documentation edits; each path must be allowed by `monolith.config.json` `finalization`. */
59
+ docUpdates?: Array<{
60
+ path: string;
61
+ newContent: string;
62
+ reason: string;
63
+ }>;
64
+ }
65
+ export type ProposalParse = {
66
+ ok: true;
67
+ proposal: FinalizationProposal;
68
+ } | {
69
+ ok: false;
70
+ errors: string[];
71
+ };
72
+ export declare const LIMITS: {
73
+ readonly idLen: 64;
74
+ readonly titleLen: 200;
75
+ readonly lineLen: 1000;
76
+ readonly blockLen: 20000;
77
+ readonly listItems: 200;
78
+ readonly docUpdates: 20;
79
+ readonly docBytes: number;
80
+ };
81
+ /**
82
+ * True when `text` holds `id` as a whole Story ID token (case-insensitive).
83
+ * Tokens are maximal runs of the Story ID alphabet [A-Za-z0-9._-]; trailing
84
+ * `.`/`_`/`-` (sentence punctuation) are dropped. So `S-1` is a token of
85
+ * "S-1 in progress" or "(S-1)", but never of "S-10", "XS-1", "1.10" or "11.1".
86
+ */
87
+ export declare function hasStoryIdToken(text: string, id: string): boolean;
88
+ export declare function validDate(s: string): boolean;
89
+ /**
90
+ * Lexical validation of a project-relative document path (R4). Rejects
91
+ * absolute paths, `..`, backslashes, NUL, empty segments and VCS internals.
92
+ * Symlinks are NOT decided here: they are refused by the safe read/write
93
+ * primitives, which look at the real file system.
94
+ */
95
+ export declare function validateRelPath(p: unknown): string | null;
96
+ export declare function parseProposal(raw: unknown): ProposalParse;
97
+ //# sourceMappingURL=proposal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal.d.ts","sourceRoot":"","sources":["../../src/finalize/proposal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjE,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,CAAC,CAAC;IACjB,KAAK,EAAE;QACL,kFAAkF;QAClF,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,2EAA2E;QAC3E,KAAK,EAAE,MAAM,CAAC;QACd,wEAAwE;QACxE,YAAY,EAAE,MAAM,CAAC;QACrB,qFAAqF;QACrF,IAAI,EAAE,MAAM,CAAC;QACb,wFAAwF;QACxF,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,wFAAwF;IACxF,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,IAAI,EAAE;QACJ,SAAS,EAAE,MAAM,CAAC;QAClB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,MAAM,EAAE,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE,6FAA6F;IAC7F,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,qGAAqG;IACrG,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1E;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,oBAAoB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE3G,eAAO,MAAM,MAAM;;;;;;;;CAQT,CAAC;AAIX;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAGjE;AAaD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5C;AAuBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAUzD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CA2GzD"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Finalization proposal (DOCSYNC-001): the typed, strictly validated input an
3
+ * AI assistant or developer prepares. It carries the *content* of the
4
+ * finalization; MONOLITH decides whether and where it may be applied.
5
+ *
6
+ * Validation is structural and syntactic only. Whether the content is *true*
7
+ * (the summary matches the code) is the owner's judgement, taken when they
8
+ * approve the plan digest — never something this module claims to check.
9
+ */
10
+ export const PROPOSAL_SCHEMA_VERSION = 1;
11
+ export const LIMITS = {
12
+ idLen: 64,
13
+ titleLen: 200,
14
+ lineLen: 1000,
15
+ blockLen: 20000,
16
+ listItems: 200,
17
+ docUpdates: 20,
18
+ docBytes: 1024 * 1024,
19
+ };
20
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
21
+ /**
22
+ * True when `text` holds `id` as a whole Story ID token (case-insensitive).
23
+ * Tokens are maximal runs of the Story ID alphabet [A-Za-z0-9._-]; trailing
24
+ * `.`/`_`/`-` (sentence punctuation) are dropped. So `S-1` is a token of
25
+ * "S-1 in progress" or "(S-1)", but never of "S-10", "XS-1", "1.10" or "11.1".
26
+ */
27
+ export function hasStoryIdToken(text, id) {
28
+ const want = id.toLowerCase();
29
+ return (text.match(/[A-Za-z0-9._-]+/g) ?? []).some((t) => t.replace(/[._-]+$/, '').toLowerCase() === want);
30
+ }
31
+ const SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,99}$/;
32
+ const PHASE_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
33
+ const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
34
+ function isObject(v) {
35
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
36
+ }
37
+ function noUnknown(obj, allowed, where, errors) {
38
+ for (const k of Object.keys(obj))
39
+ if (!allowed.includes(k))
40
+ errors.push(`${where}: unknown field "${k}".`);
41
+ }
42
+ export function validDate(s) {
43
+ const m = DATE_RE.exec(s);
44
+ if (m === null)
45
+ return false;
46
+ const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
47
+ return d.getUTCFullYear() === Number(m[1]) && d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
48
+ }
49
+ function text(v, where, errors, opts) {
50
+ if (typeof v !== 'string') {
51
+ errors.push(`${where}: must be a string.`);
52
+ return '';
53
+ }
54
+ if (v.includes('\0'))
55
+ errors.push(`${where}: must not contain NUL.`);
56
+ if (!opts.allowEmpty && v.trim().length === 0)
57
+ errors.push(`${where}: must not be empty.`);
58
+ if (v.length > opts.max)
59
+ errors.push(`${where}: longer than ${opts.max} characters.`);
60
+ if (!opts.multiline && /[\r\n\u2028\u2029]/.test(v))
61
+ errors.push(`${where}: must be a single line.`);
62
+ return v;
63
+ }
64
+ function lines(v, where, errors) {
65
+ if (!Array.isArray(v)) {
66
+ errors.push(`${where}: must be an array of strings.`);
67
+ return [];
68
+ }
69
+ if (v.length > LIMITS.listItems)
70
+ errors.push(`${where}: more than ${LIMITS.listItems} items.`);
71
+ return v.map((x, i) => text(x, `${where}[${i}]`, errors, { multiline: false, max: LIMITS.lineLen }));
72
+ }
73
+ /**
74
+ * Lexical validation of a project-relative document path (R4). Rejects
75
+ * absolute paths, `..`, backslashes, NUL, empty segments and VCS internals.
76
+ * Symlinks are NOT decided here: they are refused by the safe read/write
77
+ * primitives, which look at the real file system.
78
+ */
79
+ export function validateRelPath(p) {
80
+ if (typeof p !== 'string' || p.length === 0 || p.length > 260)
81
+ return 'path must be a non-empty string of at most 260 characters';
82
+ if (p.includes('\0') || p.includes('\\'))
83
+ return 'path must not contain NUL or backslashes';
84
+ if (p.startsWith('/') || /^[A-Za-z]:/.test(p))
85
+ return 'path must be relative to the project root';
86
+ const segs = p.split('/');
87
+ if (segs.some((s) => s === '' || s === '.' || s === '..'))
88
+ return 'path must not contain empty, "." or ".." segments';
89
+ // `.git` at ANY depth (nested repositories, submodule checkouts), any case
90
+ // (case-insensitive file systems), and trailing dot/space (Windows aliases).
91
+ if (segs.some((s) => /^\.git[. ]*$/i.test(s)) || segs.includes('node_modules'))
92
+ return 'path must not contain a .git (any case, any depth) or node_modules component';
93
+ return null;
94
+ }
95
+ export function parseProposal(raw) {
96
+ const errors = [];
97
+ if (!isObject(raw))
98
+ return { ok: false, errors: ['proposal must be a JSON object.'] };
99
+ noUnknown(raw, ['schemaVersion', 'story', 'expectedCurrentStory', 'work', 'summary', 'keyOutputs', 'history', 'handoff', 'docUpdates'], 'proposal', errors);
100
+ if (raw.schemaVersion !== PROPOSAL_SCHEMA_VERSION)
101
+ errors.push(`schemaVersion: must be ${PROPOSAL_SCHEMA_VERSION}.`);
102
+ const story = isObject(raw.story) ? raw.story : (errors.push('story: must be an object.'), {});
103
+ noUnknown(story, ['id', 'title', 'phase', 'phaseHeading', 'slug', 'date'], 'story', errors);
104
+ const id = text(story.id, 'story.id', errors, { multiline: false, max: LIMITS.idLen });
105
+ if (id && !ID_RE.test(id))
106
+ errors.push('story.id: must match [A-Za-z0-9][A-Za-z0-9._-]{0,63}.');
107
+ const title = text(story.title, 'story.title', errors, { multiline: false, max: LIMITS.titleLen });
108
+ const phase = text(story.phase, 'story.phase', errors, { multiline: false, max: 32 });
109
+ if (phase && !PHASE_RE.test(phase))
110
+ errors.push('story.phase: must match [a-z0-9][a-z0-9-]{0,31}.');
111
+ const phaseHeading = text(story.phaseHeading, 'story.phaseHeading', errors, { multiline: false, max: LIMITS.titleLen });
112
+ if (phaseHeading.startsWith('#'))
113
+ errors.push('story.phaseHeading: give the heading text without leading "#".');
114
+ const slug = text(story.slug, 'story.slug', errors, { multiline: false, max: 100 });
115
+ if (slug && !SLUG_RE.test(slug))
116
+ errors.push('story.slug: must match [a-z0-9][a-z0-9._-]{0,99}.');
117
+ const date = text(story.date, 'story.date', errors, { multiline: false, max: 10 });
118
+ if (date && !validDate(date))
119
+ errors.push('story.date: must be a real date YYYY-MM-DD.');
120
+ let expected = 'none';
121
+ const e = raw.expectedCurrentStory;
122
+ if (e === 'none')
123
+ expected = e;
124
+ else if (isObject(e) && typeof e.contains === 'string' && e.contains.trim() !== '' && Object.keys(e).length === 1 && e.contains.length <= LIMITS.lineLen) {
125
+ expected = { contains: e.contains };
126
+ if (id && !hasStoryIdToken(e.contains, id)) {
127
+ errors.push(`expectedCurrentStory.contains: must contain the story id "${id}" as a whole token (not as part of a longer id), so the guard cannot match another story.`);
128
+ }
129
+ }
130
+ else if (e === 'any') {
131
+ errors.push('expectedCurrentStory: "any" is not accepted (it would allow replacing an unrelated active story). Use "none" or {"contains": "<text containing the story id>"}.');
132
+ }
133
+ else
134
+ errors.push('expectedCurrentStory: must be "none" or {"contains": "<text containing the story id>"}.');
135
+ const w = isObject(raw.work) ? raw.work : (errors.push('work: must be an object.'), {});
136
+ noUnknown(w, ['objective', 'implementationSummary', 'filesChanged', 'tests', 'keyDecisions', 'adrs', 'lessons'], 'work', errors);
137
+ const block = (v, name) => text(v, `work.${name}`, errors, { multiline: true, max: LIMITS.blockLen });
138
+ const work = {
139
+ objective: block(w.objective, 'objective'),
140
+ implementationSummary: block(w.implementationSummary, 'implementationSummary'),
141
+ filesChanged: lines(w.filesChanged, 'work.filesChanged', errors),
142
+ tests: block(w.tests, 'tests'),
143
+ keyDecisions: block(w.keyDecisions, 'keyDecisions'),
144
+ adrs: lines(w.adrs, 'work.adrs', errors),
145
+ lessons: block(w.lessons, 'lessons'),
146
+ };
147
+ const summary = text(raw.summary, 'summary', errors, { multiline: false, max: LIMITS.blockLen });
148
+ const keyOutputs = lines(raw.keyOutputs, 'keyOutputs', errors);
149
+ const h = isObject(raw.history) ? raw.history : (errors.push('history: must be an object.'), {});
150
+ noUnknown(h, ['phaseName', 'decision', 'build'], 'history', errors);
151
+ const history = {
152
+ phaseName: text(h.phaseName, 'history.phaseName', errors, { multiline: false, max: LIMITS.titleLen }),
153
+ decision: text(h.decision, 'history.decision', errors, { multiline: false, max: LIMITS.blockLen }),
154
+ build: text(h.build, 'history.build', errors, { multiline: false, max: LIMITS.lineLen }),
155
+ };
156
+ let handoff;
157
+ if (raw.handoff !== undefined) {
158
+ if (!isObject(raw.handoff))
159
+ errors.push('handoff: must be an object {"content": "..."}.');
160
+ else {
161
+ noUnknown(raw.handoff, ['content'], 'handoff', errors);
162
+ const content = text(raw.handoff.content, 'handoff.content', errors, { multiline: true, max: LIMITS.docBytes });
163
+ handoff = { content };
164
+ }
165
+ }
166
+ let docUpdates;
167
+ if (raw.docUpdates !== undefined) {
168
+ if (!Array.isArray(raw.docUpdates))
169
+ errors.push('docUpdates: must be an array.');
170
+ else {
171
+ if (raw.docUpdates.length > LIMITS.docUpdates)
172
+ errors.push(`docUpdates: more than ${LIMITS.docUpdates} entries.`);
173
+ docUpdates = [];
174
+ const seen = new Set();
175
+ raw.docUpdates.forEach((d, i) => {
176
+ const where = `docUpdates[${i}]`;
177
+ if (!isObject(d)) {
178
+ errors.push(`${where}: must be an object.`);
179
+ return;
180
+ }
181
+ noUnknown(d, ['path', 'newContent', 'reason'], where, errors);
182
+ const bad = validateRelPath(d.path);
183
+ if (bad !== null)
184
+ errors.push(`${where}.path: ${bad}.`);
185
+ const path = typeof d.path === 'string' ? d.path : '';
186
+ if (seen.has(path))
187
+ errors.push(`${where}.path: "${path}" is listed twice.`);
188
+ seen.add(path);
189
+ docUpdates.push({
190
+ path,
191
+ newContent: text(d.newContent, `${where}.newContent`, errors, { multiline: true, max: LIMITS.docBytes }),
192
+ reason: text(d.reason, `${where}.reason`, errors, { multiline: false, max: LIMITS.lineLen }),
193
+ });
194
+ });
195
+ }
196
+ }
197
+ if (errors.length > 0)
198
+ return { ok: false, errors };
199
+ const proposal = {
200
+ schemaVersion: 1,
201
+ story: { id, title, phase, phaseHeading, slug, date },
202
+ expectedCurrentStory: expected,
203
+ work,
204
+ summary,
205
+ keyOutputs,
206
+ history,
207
+ };
208
+ if (handoff)
209
+ proposal.handoff = handoff;
210
+ if (docUpdates && docUpdates.length > 0)
211
+ proposal.docUpdates = docUpdates;
212
+ return { ok: true, proposal };
213
+ }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const MONOLITH_VERSION = "0.1.0-alpha.1";
1
+ export declare const MONOLITH_VERSION = "0.1.0-alpha.2";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const MONOLITH_VERSION = '0.1.0-alpha.1';
1
+ export const MONOLITH_VERSION = '0.1.0-alpha.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latysh-development/monolith",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "MONOLITH: Cognitive Infrastructure for AI. An open-source toolkit for observable project state, structured knowledge, and explicit execution governance.",
5
5
  "author": "Dmytro Latysh",
6
6
  "license": "MIT",
@@ -15,6 +15,17 @@ referenced_by:
15
15
 
16
16
  # Active Working Memory
17
17
 
18
+ <!--
19
+ Starting a story: replace "No active story." in the "Current Story" section below with
20
+ a field line at the very start of a line (no indent, list marker or quote), e.g.
21
+ **Story:** <story-id> — <title>
22
+ The first word after "**Story:**" is the story id: letters, digits, "." "_" "-",
23
+ starting with a letter or digit, matched exactly (case-sensitive). `monolith finalize`
24
+ identifies the active story ONLY by this one line. Ids in descriptions, headings,
25
+ code fences or comments do not count; zero or several "**Story:**" lines mean it
26
+ refuses to finalize. Keep the field above any "---" line or heading in that section.
27
+ -->
28
+
18
29
  ## Active Phase
19
30
 
20
31
  No active phase.
@@ -22,12 +22,62 @@ Triggered by: "finalize story", "complete story", "mark story done"
22
22
 
23
23
  ## Pre-conditions
24
24
 
25
- - [ ] Active story exists in `memory/working/active.md`
25
+ - [ ] Active story exists in `memory/working/active.md`, identified by exactly one `**Story:** <id>` line in `## Current Story`
26
26
  - [ ] All story tasks are complete
27
27
  - [ ] No blocking issues open
28
28
 
29
29
  ---
30
30
 
31
+ ## Deterministic Execution (`monolith finalize`)
32
+
33
+ Where the CLI provides `monolith finalize`, use it instead of editing the memory
34
+ files by hand (Steps 2-6 below describe what it writes; Step 1 and 5.5 stay yours).
35
+
36
+ 0. **Structural story identity (required before `plan`).** `monolith finalize` knows
37
+ which story is active only from one field line in the `## Current Story` section of
38
+ `memory/working/active.md`, at the very start of the line (no indent, list marker or quote):
39
+
40
+ ```markdown
41
+ ## Current Story
42
+
43
+ **Story:** S-1 — Example story
44
+ ```
45
+
46
+ The first word after `**Story:**` is the story id and must equal `proposal.story.id`
47
+ exactly (case-sensitive; letters, digits, `.` `_` `-`, starting with a letter or digit).
48
+ Text after the id is free. Never infer the id from a heading, a description, "Working on X",
49
+ a `Previous:` line, a code fence or a comment: those carry no identity, and zero, duplicate
50
+ or conflicting `**Story:**` lines make `plan` report `NOT_READY` (`expected-state` FAIL) and
51
+ `apply` `REFUSED`. Keep the field above any `---` line or heading in that section.
52
+ If `active.md` predates this field (e.g. `Working on STORY-123`), do not guess and do not
53
+ rewrite it silently: tell the owner the one line that is missing, add it only with their
54
+ consent, then run `plan` again. `expectedCurrentStory` is `{"contains": "<text naming the
55
+ story id>"}` for an active story and `"none"` only when `## Current Story` says "No active
56
+ story."; neither replaces the `**Story:**` field. Do not bypass this by hand-editing the memory
57
+ files or the integrity/governance results to make a check pass.
58
+ 1. Complete Step 1 and write `memory/working/integrity-result.md` and
59
+ `memory/working/governance-result.md` for THIS story. Each file needs exactly one
60
+ `**Status:** PASS` (or `FAIL`), exactly one `**Story:** <id> ...` whose first word is
61
+ the story id, and exactly one `**Date:** YYYY-MM-DD` (not after the finalization date,
62
+ at most 7 days before it). Repeated, contradictory or malformed fields, another story,
63
+ or a stale date are never PASS. It is your own report, not an independent check.
64
+ 2. Write a finalization proposal (JSON, schema version 1: story, expectedCurrentStory,
65
+ work, summary, keyOutputs, history, optional handoff / docUpdates). Content is
66
+ yours; targets and paths are MONOLITH's. `expectedCurrentStory` is `"none"` or
67
+ `{"contains": "<text naming the story id>"}`; `"any"` does not exist. A story id is
68
+ finalized once: a second proposal for the same id with other wording is a conflict.
69
+ 3. `monolith finalize plan --proposal <file>` — read-only. Shows every precondition as
70
+ PASS / FAIL / NOT_VERIFIED, every target, and a digest. Show it to the owner.
71
+ 4. Only after the owner approves: `monolith finalize apply --proposal <file> --approve <digest>`.
72
+ Never run `apply` on your own authority and never declare the story finalized
73
+ unless it printed `FINALIZED` or `ALREADY_FINALIZED`. `INCOMPLETE` or `REFUSED`
74
+ means it is NOT finalized: report the output, then re-plan.
75
+
76
+ Documents to keep in sync beyond the memory files go in `docUpdates`; they are written only
77
+ if `monolith.config.json` lists them in `finalization.requiredDocs` / `allowedDocs`.
78
+
79
+ ---
80
+
31
81
  ## Steps
32
82
 
33
83
  ### Step 1 — Verify integrity