Skip to content

Commit 65fe58f

Browse files
committed
mason: enforce mc:protected regions after maintain-docs dreamer task
1 parent 49bb68c commit 65fe58f

6 files changed

Lines changed: 374 additions & 0 deletions

File tree

packages/plugin/src/features/magic-context/dreamer/dreamer.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/// <reference types="bun-types" />
22

33
import { afterAll, afterEach, describe, expect, it, mock, spyOn } from "bun:test";
4+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5+
import { tmpdir } from "node:os";
6+
import { join } from "node:path";
47
import type { PluginContext } from "../../../plugin/types";
58
import * as shared from "../../../shared";
69
import { Database } from "../../../shared/sqlite";
@@ -275,6 +278,75 @@ describe("dreamer", () => {
275278
]);
276279
});
277280

281+
it("maintain-docs restores protected regions after dreamer mutates ARCHITECTURE.md", async () => {
282+
db = createTestDb();
283+
const docsDir = mkdtempSync(join(tmpdir(), "mc-maintain-docs-"));
284+
const startMarker = "<!-- mc:protected START — hand-authored cache-stability core. -->";
285+
const endMarker = "<!-- mc:protected END -->";
286+
const protectedBody = "invariant body bytes";
287+
const originalArch = [
288+
"# Architecture",
289+
"",
290+
"outside before",
291+
startMarker,
292+
protectedBody,
293+
endMarker,
294+
"outside after",
295+
].join("\n");
296+
writeFileSync(join(docsDir, "ARCHITECTURE.md"), originalArch, "utf8");
297+
298+
const client = createDreamClient();
299+
const promptSyncSpy = spyOn(
300+
shared,
301+
"promptSyncWithModelSuggestionRetry",
302+
).mockImplementation(async () => {
303+
const tampered = [
304+
"# Architecture",
305+
"",
306+
"outside before edited",
307+
startMarker,
308+
"TAMPERED protected bytes",
309+
endMarker,
310+
"outside after edited",
311+
].join("\n");
312+
writeFileSync(join(docsDir, "ARCHITECTURE.md"), tampered, "utf8");
313+
});
314+
315+
try {
316+
const result = await runDream({
317+
db,
318+
client,
319+
projectIdentity: "/repo/project",
320+
tasks: ["maintain-docs"],
321+
taskTimeoutMinutes: 5,
322+
maxRuntimeMinutes: 10,
323+
parentSessionId: "parent-1",
324+
sessionDirectory: docsDir,
325+
});
326+
327+
expect(result.tasks.map((t) => t.name)).toEqual(["maintain-docs"]);
328+
expect(result.tasks[0]?.error).toBeUndefined();
329+
330+
const onDisk = readFileSync(join(docsDir, "ARCHITECTURE.md"), "utf8");
331+
expect(onDisk).toContain("outside before edited");
332+
expect(onDisk).toContain("outside after edited");
333+
expect(onDisk).toContain(protectedBody);
334+
expect(onDisk).not.toContain("TAMPERED protected bytes");
335+
const origBlock = originalArch.slice(
336+
originalArch.indexOf(startMarker),
337+
originalArch.indexOf(endMarker) + endMarker.length,
338+
);
339+
const diskBlock = onDisk.slice(
340+
onDisk.indexOf(startMarker),
341+
onDisk.indexOf(endMarker) + endMarker.length,
342+
);
343+
expect(diskBlock).toBe(origBlock);
344+
} finally {
345+
promptSyncSpy.mockRestore();
346+
rmSync(docsDir, { recursive: true, force: true });
347+
}
348+
});
349+
278350
it("trips circuit breaker after three consecutive identical model failures", async () => {
279351
db = createTestDb();
280352
const createdSessionIds: string[] = [];

packages/plugin/src/features/magic-context/dreamer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./lease";
2+
export * from "./protected-regions";
23
export * from "./queue";
34
export * from "./runner";
45
export * from "./scheduler";
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { log } from "../../../shared/logger";
4+
import { enforceProtectedRegions } from "./protected-regions";
5+
6+
export const MAINTAIN_DOCS_SNAPSHOT_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"] as const;
7+
8+
export type MaintainDocsDocSnapshot = Map<string, string>;
9+
10+
/** Read canonical pre-task bytes for maintain-docs enforcement. */
11+
export function snapshotMaintainDocsFiles(docsDir: string): MaintainDocsDocSnapshot {
12+
const snapshot = new Map<string, string>();
13+
for (const name of MAINTAIN_DOCS_SNAPSHOT_FILES) {
14+
const path = join(docsDir, name);
15+
try {
16+
if (existsSync(path)) {
17+
snapshot.set(name, readFileSync(path, "utf8"));
18+
}
19+
} catch {
20+
// best-effort snapshot
21+
}
22+
}
23+
return snapshot;
24+
}
25+
26+
/**
27+
* After maintain-docs, re-read on-disk docs and restore protected regions from the pre-task snapshot.
28+
* Best-effort: read/write failures are logged, not thrown.
29+
*/
30+
export function enforceMaintainDocsProtectedRegions(args: {
31+
docsDir: string;
32+
snapshot: MaintainDocsDocSnapshot;
33+
}): void {
34+
for (const [fileName, original] of args.snapshot) {
35+
const path = join(args.docsDir, fileName);
36+
try {
37+
const current = readFileSync(path, "utf8");
38+
const { text, violated } = enforceProtectedRegions(original, current);
39+
if (!violated) {
40+
continue;
41+
}
42+
writeFileSync(path, text, "utf8");
43+
log(
44+
`[dreamer] maintain-docs altered a protected region in ${fileName} — restored from pre-task snapshot`,
45+
);
46+
} catch (error) {
47+
log(
48+
`[dreamer] maintain-docs protected-region enforcement failed for ${fileName}: ${error}`,
49+
);
50+
}
51+
}
52+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/// <reference types="bun-types" />
2+
3+
import { describe, expect, it } from "bun:test";
4+
import { enforceProtectedRegions, extractProtectedBlocks } from "./protected-regions";
5+
6+
const START = "<!-- mc:protected START — hand-authored cache-stability core. Only humans edit. -->";
7+
const END = "<!-- mc:protected END -->";
8+
9+
function doc(outsideBefore: string, protectedBody: string, outsideAfter: string): string {
10+
return `${outsideBefore}\n${START}\n${protectedBody}\n${END}\n${outsideAfter}`;
11+
}
12+
13+
describe("extractProtectedBlocks", () => {
14+
it("returns empty when no markers", () => {
15+
expect(extractProtectedBlocks("# Hello\n\nWorld")).toEqual([]);
16+
});
17+
});
18+
19+
describe("enforceProtectedRegions", () => {
20+
it("identical protected region → no change, violated:false", () => {
21+
const original = doc("intro", "cache core", "outro");
22+
const candidate = doc("intro", "cache core", "outro");
23+
const result = enforceProtectedRegions(original, candidate);
24+
expect(result).toEqual({ text: candidate, violated: false });
25+
});
26+
27+
it("edited text outside the region + untouched region → keeps edits, violated:false", () => {
28+
const original = doc("intro", "cache core", "outro");
29+
const candidate = doc("intro v2", "cache core", "outro v2");
30+
const result = enforceProtectedRegions(original, candidate);
31+
expect(result.violated).toBe(false);
32+
expect(result.text).toBe(candidate);
33+
expect(result.text).toContain("intro v2");
34+
expect(result.text).toContain("outro v2");
35+
});
36+
37+
it("altered protected region → restored byte-identical, violated:true, surrounding edits preserved", () => {
38+
const original = doc("intro", "cache core", "outro");
39+
const candidate = doc("intro v2", "cache CORE", "outro v2");
40+
const result = enforceProtectedRegions(original, candidate);
41+
expect(result.violated).toBe(true);
42+
expect(result.text).toContain("intro v2");
43+
expect(result.text).toContain("outro v2");
44+
expect(result.text).toContain("cache core");
45+
expect(result.text).not.toContain("cache CORE");
46+
const origBlock = extractProtectedBlocks(original)[0]?.block;
47+
const resultBlock = extractProtectedBlocks(result.text)[0]?.block;
48+
expect(resultBlock).toBe(origBlock);
49+
});
50+
51+
it("deleted protected markers → whole candidate rejected, returns original, violated:true", () => {
52+
const original = doc("intro", "cache core", "outro");
53+
const candidate = "intro v2\nno markers\noutro v2";
54+
const result = enforceProtectedRegions(original, candidate);
55+
expect(result).toEqual({ text: original, violated: true });
56+
});
57+
58+
it("no protected region in original → candidate passed through unchanged", () => {
59+
const original = "# STRUCTURE\n\nNo protected blocks here.";
60+
const candidate = "# STRUCTURE\n\nUpdated tree.";
61+
const result = enforceProtectedRegions(original, candidate);
62+
expect(result).toEqual({ text: candidate, violated: false });
63+
});
64+
65+
it("multiple protected blocks → each matched by start-marker identity and independently enforced", () => {
66+
const startA = "<!-- mc:protected START block A -->";
67+
const startB = "<!-- mc:protected START block B -->";
68+
const original = ["head", startA, "body A", END, "mid", startB, "body B", END, "tail"].join(
69+
"\n",
70+
);
71+
72+
const candidate = [
73+
"head edited",
74+
startA,
75+
"body A TAMPERED",
76+
END,
77+
"mid edited",
78+
startB,
79+
"body B",
80+
END,
81+
"tail edited",
82+
].join("\n");
83+
84+
const result = enforceProtectedRegions(original, candidate);
85+
expect(result.violated).toBe(true);
86+
expect(result.text).toContain("head edited");
87+
expect(result.text).toContain("mid edited");
88+
expect(result.text).toContain("body A\n");
89+
expect(result.text).not.toContain("body A TAMPERED");
90+
expect(extractProtectedBlocks(result.text)[0]?.block).toBe(
91+
extractProtectedBlocks(original)[0]?.block,
92+
);
93+
expect(extractProtectedBlocks(result.text)[1]?.block).toBe(
94+
extractProtectedBlocks(original)[1]?.block,
95+
);
96+
});
97+
98+
it("missing one of multiple blocks → rejects whole candidate", () => {
99+
const startA = "<!-- mc:protected START block A -->";
100+
const startB = "<!-- mc:protected START block B -->";
101+
const original = [startA, "a", END, "x", startB, "b", END].join("\n");
102+
const candidate = [startA, "a", END, "x only one block"].join("\n");
103+
const result = enforceProtectedRegions(original, candidate);
104+
expect(result).toEqual({ text: original, violated: true });
105+
});
106+
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
export interface EnforceProtectedRegionsResult {
2+
/** The text to persist (candidate, repaired candidate, or original on reject). */
3+
text: string;
4+
violated: boolean;
5+
}
6+
7+
const PROTECTED_START_TOKEN = "mc:protected START";
8+
const PROTECTED_END_TOKEN = "mc:protected END";
9+
10+
export interface ProtectedBlock {
11+
/** Full identifying start-marker line (the line containing mc:protected START). */
12+
startMarkerLine: string;
13+
/** Bytes from START line through END line inclusive. */
14+
block: string;
15+
}
16+
17+
/** Extract every mc:protected region from `text`, keyed by the full START marker line. */
18+
export function extractProtectedBlocks(text: string): ProtectedBlock[] {
19+
const lines = text.split("\n");
20+
const blocks: ProtectedBlock[] = [];
21+
let i = 0;
22+
while (i < lines.length) {
23+
const line = lines[i];
24+
if (line.includes(PROTECTED_START_TOKEN)) {
25+
const startMarkerLine = line;
26+
const startIdx = i;
27+
while (i < lines.length && !lines[i].includes(PROTECTED_END_TOKEN)) {
28+
i += 1;
29+
}
30+
if (i >= lines.length) {
31+
break;
32+
}
33+
const endIdx = i;
34+
const block = lines.slice(startIdx, endIdx + 1).join("\n");
35+
blocks.push({ startMarkerLine, block });
36+
i += 1;
37+
continue;
38+
}
39+
i += 1;
40+
}
41+
return blocks;
42+
}
43+
44+
function findCandidateBlockSpan(
45+
candidate: string,
46+
startMarkerLine: string,
47+
): { start: number; end: number; block: string } | null {
48+
const lines = candidate.split("\n");
49+
for (let i = 0; i < lines.length; i++) {
50+
if (lines[i] !== startMarkerLine) {
51+
continue;
52+
}
53+
const startIdx = i;
54+
while (i < lines.length && !lines[i].includes(PROTECTED_END_TOKEN)) {
55+
i += 1;
56+
}
57+
if (i >= lines.length) {
58+
return null;
59+
}
60+
const endIdx = i;
61+
const block = lines.slice(startIdx, endIdx + 1).join("\n");
62+
return { start: startIdx, end: endIdx, block };
63+
}
64+
return null;
65+
}
66+
67+
function spliceProtectedBlock(
68+
text: string,
69+
startMarkerLine: string,
70+
replacementBlock: string,
71+
): string {
72+
const lines = text.split("\n");
73+
for (let i = 0; i < lines.length; i++) {
74+
if (lines[i] !== startMarkerLine) {
75+
continue;
76+
}
77+
const startIdx = i;
78+
while (i < lines.length && !lines[i].includes(PROTECTED_END_TOKEN)) {
79+
i += 1;
80+
}
81+
if (i >= lines.length) {
82+
return text;
83+
}
84+
const endIdx = i;
85+
const replacementLines = replacementBlock.split("\n");
86+
const next = [...lines.slice(0, startIdx), ...replacementLines, ...lines.slice(endIdx + 1)];
87+
return next.join("\n");
88+
}
89+
return text;
90+
}
91+
92+
/**
93+
* Enforce that every mc:protected region present in `original` is byte-identical
94+
* in `candidate`. Returns the text to actually write and whether a violation was repaired.
95+
*/
96+
export function enforceProtectedRegions(
97+
original: string,
98+
candidate: string,
99+
): EnforceProtectedRegionsResult {
100+
const originalBlocks = extractProtectedBlocks(original);
101+
if (originalBlocks.length === 0) {
102+
return { text: candidate, violated: false };
103+
}
104+
105+
let text = candidate;
106+
let violated = false;
107+
108+
for (const { startMarkerLine, block: originalBlock } of originalBlocks) {
109+
const span = findCandidateBlockSpan(text, startMarkerLine);
110+
if (!span) {
111+
return { text: original, violated: true };
112+
}
113+
if (span.block !== originalBlock) {
114+
text = spliceProtectedBlock(text, startMarkerLine, originalBlock);
115+
violated = true;
116+
}
117+
}
118+
119+
return { text, violated };
120+
}

0 commit comments

Comments
 (0)