From e7ffbc057c7992f901cbeab47aeac0c73ab425e7 Mon Sep 17 00:00:00 2001 From: Thomas Chung Date: Wed, 22 Jul 2026 17:19:29 -0700 Subject: [PATCH] fix(lint): code-fence-wrap detector and fixer regex now agree (#1597) The code-fence-wrap detector in lintContent used the /m multiline flag, so ^/$ matched start/end of any line. The rule fired on any page that simply contained a ```markdown code block, not only pages wrapped end-to-end. The matching fixer in fixContent has no /m flag, so it can only strip whole-file wrappers. Result: detected issues were marked fixable: true, yet fixContent could never strip them. `gbrain dream` reported "0 fix(es) applied, N remaining" perpetually for the rule. Drops the /m flag from the detector so detector and fixer stay in sync. Whole-file wrapper detection is preserved; inner code blocks no longer trigger the rule. Real-world impact: a brain with 5 docs pages containing markdown examples (skill READMEs, decision registry, journal templates) reports 5 phantom "fixable: true" issues every dream cycle, never converging. After this fix, the dream-cycle lint phase reports only real-and-unfixable issues (missing frontmatter, missing title/type) which is the intended behavior. Two regression tests added in test/lint.test.ts: - Page contains a single inner ```markdown block - Page contains multiple inner ```markdown blocks Both assert no code-fence-wrap issue is reported. The existing "detects wrapping code fences" test (true-positive case) continues to pass; total tests in the file are 18 -> 20. Co-authored-by: Thomas Chung Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Time Attakc <89218912+time-attack@users.noreply.github.com> --- src/commands/lint.ts | 7 ++++++- test/lint.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/commands/lint.ts b/src/commands/lint.ts index e14e05456..54b81c290 100644 --- a/src/commands/lint.ts +++ b/src/commands/lint.ts @@ -127,7 +127,12 @@ export function lintContent(content: string, filePath: string, opts: LintContent } // Rule: Wrapping code fences (```markdown ... ```) - if (content.match(/^```(?:markdown|md)\s*\n/m) && content.match(/\n```\s*$/m)) { + // Detector intentionally has NO /m flag so ^/$ match start/end of the whole + // file, not inner lines. Keeps detector in sync with fixContent() below, + // which also has no /m flag. Without this, lint reports "fixable" false + // positives on any page that simply contains a ```markdown code block, but + // fixContent can never strip them (its regex only matches whole-file wrappers). + if (content.match(/^```(?:markdown|md)\s*\n/) && content.match(/\n```\s*$/)) { issues.push({ file: filePath, line: 1, rule: 'code-fence-wrap', message: 'Page wrapped in ```markdown code fences (LLM artifact)', diff --git a/test/lint.test.ts b/test/lint.test.ts index ca38b8d23..f843d5859 100644 --- a/test/lint.test.ts +++ b/test/lint.test.ts @@ -32,6 +32,29 @@ describe('lintContent', () => { expect(issues.some(i => i.rule === 'code-fence-wrap')).toBe(true); }); + test('no false positive: page CONTAINS an inner ```markdown code block', () => { + // Real-world case: a docs/SKILL page that shows a markdown example inline. + // Before this fix, the detector used the /m flag so ^/$ matched start/end + // of any line, which fired on any file that simply contained a ```markdown + // line. But fixContent's regex has no /m flag and can only strip whole-file + // wrappers, so the issue was reported as "fixable: true" yet never fixed. + const content = + '---\ntitle: Skill\n---\n\n# Skill\n\nExample input shape:\n\n' + + '```markdown\n# Inner page\nContent.\n```\n\nThat ends the example.\n'; + const issues = lintContent(content, 'test.md'); + expect(issues.filter(i => i.rule === 'code-fence-wrap')).toHaveLength(0); + }); + + test('no false positive: multiple inner ```markdown blocks', () => { + // Documentation pages frequently include several markdown examples. + const content = + '---\ntitle: Examples\n---\n\n# Examples\n\nFirst:\n\n' + + '```markdown\nfoo\n```\n\nSecond:\n\n' + + '```markdown\nbar\n```\n\nDone.\n'; + const issues = lintContent(content, 'test.md'); + expect(issues.filter(i => i.rule === 'code-fence-wrap')).toHaveLength(0); + }); + test('detects placeholder dates', () => { const content = '---\ntitle: Test\ntype: person\ncreated: YYYY-MM-DD\n---\n\n# Test'; const issues = lintContent(content, 'test.md');