* docs(#3025): MCP tool schema as a context-budget concern Adds documentation covering the largest GSD cost lever that GSD itself does not own: MCP tool schema injection. Every enabled MCP server adds its schema to every turn (often 20k+ tokens for heavyweight servers like browser/playwright, mac-tools, etc.), which can dwarf whatever `model_profile` tuning saves. Two doc surfaces (per the bundle-docs-with-code skill depth gradient): 1. get-shit-done/references/context-budget.md - New "MCP Tool Schema Cost (Harness Concern)" section. - Explains schemas-per-turn cost framing. - Names enabledMcpjsonServers / disabledMcpjsonServers and .claude/settings.json explicitly. - Pre-phase audit checklist: browser/playwright, platform-specific, cross-project/stale, duplicate/shadow. - Explicit "GSD does not manage MCP enablement — harness concern" statement so users don't hunt for a GSD setting. - Links to Anthropic Claude Code MCP docs as canonical reference. - Notes compounding interaction with model_profile (additive levers). 2. docs/USER-GUIDE.md - New task-oriented "Trim MCP servers to reduce per-turn cost" section above "Using Non-Claude Runtimes". - Same checklist condensed. - Cross-link to context-budget.md for the full reference. Tests: - tests/feat-3025-mcp-token-budget-docs.test.cjs (12 cases) parses both docs into typed semantic-flag records and asserts behavioral invariants (mentions key, includes audit, names harness, etc.) rather than substring-matching prose. Adheres to CONTRIBUTING.md no-source-grep — section can be reworded freely as long as the required semantics survive. - Markdownlint pre-flight tests (MD040 fence language, MD056 table column count) per the bundle-docs-with-code skill so CR can't ratchet on prose nitpicks across multiple review rounds. Verification: - 12/12 pass on regression test - 6857/6857 full suite (12 net new) - lint-no-source-grep clean (377 test files) Companion to #3023 (per-phase-type model map) and #3024 (dynamic routing). Together they cover the three biggest cost levers users ask about; this issue covers the one GSD does not own. Closes #3025 * docs(#3025): batch 3 CR fixes — pr id, relative link, named flag CodeRabbit on PR #3032 (3 minor — 2 inline + 1 nitpick), all in one push per the bundle-docs-with-code skill (avoid per-round nitpick ratchet): 1. Inline (Minor) — .changeset/mcp-token-budget-docs.md:3 `pr: TBD` → `pr: 3032` so changeset tooling can link the entry. 2. Inline (Minor) — docs/USER-GUIDE.md:1101 Used a hardcoded `https://github.com/.../blob/main/...` URL for the cross-link to `context-budget.md`. Rest of USER-GUIDE.md uses relative links. Switched to `../get-shit-done/references/context- budget.md#mcp-tool-schema-cost-harness-concern` so feature-branch work shows the right content and rename-resilience is preserved. 3. Nitpick — tests/feat-3025-mcp-token-budget-docs.test.cjs:234 The cross-link assertion used an inline `/context-budget/i.test(...)` while every other invariant in the file lived as a named flag in `parseMcpBudgetSection`. Per CONTRIBUTING.md no-source-grep, added `crossLinksContextBudget` to the parser and asserted on `parsed.crossLinksContextBudget` so the cross-link rule sits next to its siblings. Verification: - 12/12 pass on regression test (no count change; refactor only) - No source code changes, only docs + tests * test(#3025): strip inline markdown before phrase-match (CR nitpick) CodeRabbit caught that the `explainsHarnessNotGsd` primary regex branch couldn't match "GSD does **not** manage" in context-budget.md because the markdown bold markers (`**`) sit between contiguous words. The test passed today only via the fallback `harness (concern|setting|controlled)` branch — the primary branch was effectively dead code. Fix: strip inline markdown emphasis (`**`, `*`, `~~`) and inline- code backticks before any phrase-matching in `parseMcpBudgetSection`. All seven flag computations now run against the stripped text so markdown formatting can't silently invalidate any invariant. Underscores are intentionally NOT stripped — `model_profile` and other snake_case identifiers must survive intact for the mentionsModelProfileInteraction check to find them. Verification: 12/12 still pass; primary branches now fire on real markdown content rather than relying on fallbacks. * test(#3025): guard markdownlint tests against null section (CR nitpick) CodeRabbit caught that the MD040 and MD056 markdownlint pre-flight tests called `section.match(...)` and `section.split('\n')` directly on the value returned by `extractSection`, which returns null when no matching header is found. If the MCP section is ever removed (regression), both tests would throw `TypeError: Cannot read properties of null` instead of producing a clean assertion failure naming the actual problem. The semantic tests above are protected because parseMcpBudgetSection short-circuits to a typed-falsy record on null input. The markdownlint tests bypassed that guard since they need raw section text, not parsed flags. Added `assert.ok(section, ...)` preconditions to both so a missing section produces a meaningful failure message. No content changes; defensive programming only. Verification: 12/12 still pass.
This commit is contained in:
5
.changeset/mcp-token-budget-docs.md
Normal file
5
.changeset/mcp-token-budget-docs.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 3032
|
||||
---
|
||||
**Documentation: MCP tool schema as a context-budget concern (#3025).** Adds new sections to `get-shit-done/references/context-budget.md` and `docs/USER-GUIDE.md` explaining that every enabled MCP server injects its tool schema into every turn — heavyweight servers (browser/playwright, Mac-tools, Windows-tools) can cost 20k+ tokens each, often dwarfing what `model_profile` tuning saves. The toggle lives in `.claude/settings.json` (`enabledMcpjsonServers` / `disabledMcpjsonServers`) and is a Claude Code harness concern, not a GSD concern. Includes a pre-phase audit checklist (browser, platform-specific, cross-project, duplicates) and notes the multiplier interaction with `model_profile`. Companion to #3023 (per-phase-type model map) and #3024 (dynamic routing); together they cover the three biggest cost levers.
|
||||
@@ -1077,6 +1077,29 @@ To turn it off, set `dynamic_routing.enabled: false` (the default) — behavior
|
||||
|
||||
For the full agent → tier mapping and resolution-precedence rules, see [Dynamic Routing](CONFIGURATION.md#dynamic-routing-with-failure-tier-escalation-dynamic_routing--added-in-v140) in the configuration reference.
|
||||
|
||||
### Trim MCP servers to reduce per-turn cost (the biggest lever GSD doesn't own)
|
||||
|
||||
Before tuning `model_profile` or `models.<phase_type>`, audit which **MCP servers** your harness has enabled. Every enabled MCP server injects its tool schema into every turn — heavyweight servers like browser/playwright tools or platform-specific helpers can cost 20k+ tokens each, often dwarfing whatever GSD's resolver can save.
|
||||
|
||||
This is a **harness setting**, not a GSD setting. The toggle lives in `.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabledMcpjsonServers": ["context7"],
|
||||
"disabledMcpjsonServers": ["playwright", "mac-tools"]
|
||||
}
|
||||
```
|
||||
|
||||
Quick audit before a long phase:
|
||||
|
||||
- Are any browser / playwright tools enabled when this phase has no UI work?
|
||||
- Are any platform-specific tools (Mac-tools, Windows-tools, OS-specific) enabled when not needed?
|
||||
- Are any project-specific MCPs from a different project still enabled here?
|
||||
|
||||
Each disabled server removes its schema from every subsequent turn for the rest of the session. Trimming MCPs **compounds** with `model_profile` tuning — both levers are additive, and MCP savings show up immediately across every subagent the orchestrator spawns.
|
||||
|
||||
For the full audit, harness reference, and the composition note with `model_profile`, see [MCP Tool Schema Cost](../get-shit-done/references/context-budget.md#mcp-tool-schema-cost-harness-concern) in the bundled `context-budget.md` reference.
|
||||
|
||||
### Using Non-Claude Runtimes (Codex, OpenCode, Gemini CLI, Kilo)
|
||||
|
||||
If you installed GSD for a non-Claude runtime, the installer already configured model resolution so all agents use the runtime's default model. No manual setup is needed. Specifically, the installer sets `resolve_model_ids: "omit"` in your config, which tells GSD to skip Anthropic model ID resolution and let the runtime choose its own default model.
|
||||
|
||||
@@ -47,3 +47,39 @@ Quality degrades gradually before panic thresholds fire. Watch for these early s
|
||||
- **Skipped steps** -- agent omits protocol steps it would normally follow. If an agent's success criteria has 8 items but it only reports 5, suspect context pressure.
|
||||
|
||||
When delegating to agents, the orchestrator cannot verify semantic correctness of agent output -- only structural completeness. This is a fundamental limitation. Mitigate with must_haves.truths and spot-check verification.
|
||||
|
||||
## MCP Tool Schema Cost (Harness Concern)
|
||||
|
||||
Every enabled MCP server injects its tool schema into **every turn**, regardless of whether you call any of its tools. Heavyweight servers can cost 20k+ tokens per turn each — often dwarfing whatever GSD itself can save through `model_profile` tuning. This is a Claude Code harness concern, not a GSD concern: GSD does **not** manage MCP enablement. The toggle lives in `.claude/settings.json` under `enabledMcpjsonServers` and `disabledMcpjsonServers`.
|
||||
|
||||
### Why this is the biggest cost lever you don't own
|
||||
|
||||
Tool schemas count against the same context budget as model context, prompts, and conversation history. If a project has 5 unused MCP servers averaging 5k tokens of schema each, every turn pays a 25k-token tax before the assistant reads a single project file. Trimming MCPs has a **multiplier effect** that compounds with whichever `model_profile` you've chosen — every-turn overhead drops regardless of which model is in use.
|
||||
|
||||
### Pre-Phase MCP Audit
|
||||
|
||||
Before starting a long phase (especially `/gsd-execute-phase`, `/gsd-plan-phase`, or anything that fans out across many subagents), run this audit:
|
||||
|
||||
- [ ] **Browser / playwright tools enabled?** If this phase has no UI work, disable them. They're among the heaviest per-turn schemas.
|
||||
- [ ] **Platform-specific tools enabled?** Mac-tools / Windows-tools / OS-specific helpers should be disabled when not actively needed for the phase at hand.
|
||||
- [ ] **Cross-project / stale MCPs?** Servers added for a different project that are still enabled here. These are often forgotten and pay a per-turn tax for zero benefit.
|
||||
- [ ] **Duplicate or shadow servers?** Two MCPs offering similar tools (e.g. two different filesystem helpers). Keep one.
|
||||
|
||||
Each item disabled removes its schema from every subsequent turn for the rest of the session.
|
||||
|
||||
### How to toggle
|
||||
|
||||
The keys live in `.claude/settings.json` (project) or `~/.claude/settings.json` (global) — **not** in `.planning/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabledMcpjsonServers": ["context7"],
|
||||
"disabledMcpjsonServers": ["playwright", "mac-tools"]
|
||||
}
|
||||
```
|
||||
|
||||
Either list works — `enabledMcpjsonServers` is an explicit allow-list, `disabledMcpjsonServers` is a block-list against the default. See the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp) for the canonical reference; this section just flags it as a context-budget lever GSD users routinely overlook.
|
||||
|
||||
### Composition with model_profile
|
||||
|
||||
Trimming MCPs and tuning `model_profile` are independent levers that **compound**. Disabling a 25k-token MCP saves 25k per turn whether you're running `quality` (opus everywhere) or `budget` (sonnet/haiku); the savings are additive, not in lieu of model tuning. Don't pick one — do both, and audit MCPs first because the per-turn savings show up immediately and stack across every subagent the orchestrator spawns.
|
||||
|
||||
301
tests/feat-3025-mcp-token-budget-docs.test.cjs
Normal file
301
tests/feat-3025-mcp-token-budget-docs.test.cjs
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* Documentation regression test for issue #3025 — MCP token-budget guidance.
|
||||
*
|
||||
* Verifies that get-shit-done/references/context-budget.md contains the
|
||||
* structural elements the issue requires:
|
||||
*
|
||||
* 1. A section explaining MCP/tool schemas as a context-budget concern
|
||||
* 2. References to the harness-side toggles (enabledMcpjsonServers,
|
||||
* disabledMcpjsonServers in .claude/settings.json)
|
||||
* 3. A pre-phase audit checklist (browser/playwright, platform-specific,
|
||||
* project-specific)
|
||||
* 4. An explicit note that GSD does NOT manage MCP enablement — this is
|
||||
* a Claude Code harness concern (with a cross-link)
|
||||
* 5. Note the interaction with model_profile (compounding levers)
|
||||
*
|
||||
* Tests parse the doc into a typed section record (parseMcpSection) and
|
||||
* assert on flag booleans, not raw text matches. Adheres to
|
||||
* CONTRIBUTING.md "no-source-grep" — describes invariants, not wording,
|
||||
* so the prose can be reworded freely as long as the semantics survive.
|
||||
*
|
||||
* Companion to docs/USER-GUIDE.md task section, which is exercised by the
|
||||
* same parser shape (separate test below).
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const CONTEXT_BUDGET_MD = path.join(ROOT, 'get-shit-done', 'references', 'context-budget.md');
|
||||
const USER_GUIDE_MD = path.join(ROOT, 'docs', 'USER-GUIDE.md');
|
||||
|
||||
/**
|
||||
* Extract the MCP-budget section from a markdown file by header text.
|
||||
* Returns null if the section is missing. Section runs from the matching
|
||||
* `## ` header up to the next `## ` header (or EOF).
|
||||
*/
|
||||
function extractSection(filePath, headerSubstring) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
let inSection = false;
|
||||
let startDepth = 0;
|
||||
const collected = [];
|
||||
for (const line of lines) {
|
||||
const headerMatch = /^(#+)\s/.exec(line);
|
||||
if (headerMatch) {
|
||||
const depth = headerMatch[1].length;
|
||||
if (inSection) {
|
||||
// Section ends at a header at the same or shallower depth.
|
||||
// Subsections at deeper depth are part of the section.
|
||||
if (depth <= startDepth) break;
|
||||
} else if (line.toLowerCase().includes(headerSubstring.toLowerCase())) {
|
||||
inSection = true;
|
||||
startDepth = depth;
|
||||
}
|
||||
}
|
||||
if (inSection) collected.push(line);
|
||||
}
|
||||
return collected.length > 0 ? collected.join('\n') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the MCP-budget section into a typed semantic-flag record.
|
||||
* Each flag answers a single behavioral question that #3025 requires
|
||||
* the prose to encode.
|
||||
*/
|
||||
function parseMcpBudgetSection(section) {
|
||||
if (!section || typeof section !== 'string') {
|
||||
return {
|
||||
ok: false,
|
||||
sectionLength: 0,
|
||||
explainsMcpAsBudgetConcern: false,
|
||||
namesEnabledMcpjsonServers: false,
|
||||
namesDisabledMcpjsonServers: false,
|
||||
namesClaudeSettingsJson: false,
|
||||
includesPrePhaseAudit: false,
|
||||
auditMentionsBrowserOrPlaywright: false,
|
||||
auditMentionsPlatformSpecific: false,
|
||||
auditMentionsCrossProject: false,
|
||||
explainsHarnessNotGsd: false,
|
||||
mentionsModelProfileInteraction: false,
|
||||
crossLinksContextBudget: false,
|
||||
};
|
||||
}
|
||||
// CR follow-up: strip inline markdown emphasis (`**`, `*`, `~~`) and
|
||||
// backticks before phrase-matching so e.g. "GSD does **not** manage"
|
||||
// is caught by the primary `gsd does not manage` alternative below.
|
||||
// WITHOUT this, the markdown-bold breaks the contiguous match and the
|
||||
// test only passes via the fallback branch (silent dead code).
|
||||
// Underscores are intentionally NOT stripped — `model_profile` and
|
||||
// other snake_case identifiers must survive intact so the
|
||||
// model_profile interaction check still finds them.
|
||||
const stripped = section.replace(/\*{1,3}|~{2}|`/g, '');
|
||||
// (1) Explains MCP as budget concern — must mention BOTH "MCP" / "tool
|
||||
// schema" AND a token/cost framing.
|
||||
const explainsMcpAsBudgetConcern =
|
||||
/\bmcp\b|tool schema|tool schemas/i.test(stripped) &&
|
||||
/\btoken|context budget|per[- ]turn|cost\b/i.test(stripped);
|
||||
// (2) Names the harness keys verbatim
|
||||
const namesEnabledMcpjsonServers = /enabledMcpjsonServers/.test(stripped);
|
||||
const namesDisabledMcpjsonServers = /disabledMcpjsonServers/.test(stripped);
|
||||
// (3) Names the settings file location
|
||||
const namesClaudeSettingsJson = /\.claude\/settings\.json/.test(stripped);
|
||||
// (4) Audit checklist — must mention all three classes the issue
|
||||
// calls out, plus a "before this phase / pre-phase" framing
|
||||
const includesPrePhaseAudit =
|
||||
/audit|checklist|review (your )?mcp|before (starting|beginning) (a |the )?phase/i.test(stripped);
|
||||
const auditMentionsBrowserOrPlaywright = /\bbrowser\b|playwright/i.test(stripped);
|
||||
const auditMentionsPlatformSpecific = /platform[- ]specific|mac[- ]?tools|windows[- ]?tools|os[- ]specific/i.test(stripped);
|
||||
const auditMentionsCrossProject = /(other|different|cross[- ])\s*project|stale (project )?mcp/i.test(stripped);
|
||||
// (5) Harness vs GSD distinction — must explicitly state GSD doesn't
|
||||
// own this knob and point at the harness
|
||||
const explainsHarnessNotGsd =
|
||||
/(gsd does(?:n[''’]t| not) (own|manage|control)|harness (concern|setting|controlled)|not a gsd (setting|knob))/i.test(stripped);
|
||||
// (6) Compounding with model_profile
|
||||
const mentionsModelProfileInteraction =
|
||||
/model[_ ]profile/i.test(stripped) &&
|
||||
/compound|multiplier|stack|every[- ]turn|regardless of (which )?model|in addition/i.test(stripped);
|
||||
// (7) Cross-link to the canonical reference doc — task-guide section
|
||||
// must point readers at context-budget.md for the full audit. Encoded
|
||||
// as a named flag (CR follow-up) so the assertion sits alongside the
|
||||
// other parsed invariants rather than as a one-off inline regex.
|
||||
const crossLinksContextBudget = /context-budget/i.test(stripped);
|
||||
return {
|
||||
ok: true,
|
||||
sectionLength: section.length,
|
||||
explainsMcpAsBudgetConcern,
|
||||
namesEnabledMcpjsonServers,
|
||||
namesDisabledMcpjsonServers,
|
||||
namesClaudeSettingsJson,
|
||||
includesPrePhaseAudit,
|
||||
auditMentionsBrowserOrPlaywright,
|
||||
auditMentionsPlatformSpecific,
|
||||
auditMentionsCrossProject,
|
||||
explainsHarnessNotGsd,
|
||||
mentionsModelProfileInteraction,
|
||||
crossLinksContextBudget,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── context-budget.md ──────────────────────────────────────────────────────
|
||||
|
||||
describe('#3025 context-budget.md: MCP token-budget section exists with required content', () => {
|
||||
test('the file exists', () => {
|
||||
assert.ok(fs.existsSync(CONTEXT_BUDGET_MD), `expected file at ${CONTEXT_BUDGET_MD}`);
|
||||
});
|
||||
|
||||
test('has a section header that mentions MCP', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
assert.ok(section, 'must have a `## ...MCP...` heading; section was not found');
|
||||
});
|
||||
|
||||
test('explains MCP/tool schemas as a context-budget concern (#3025 requirement 1)', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
const parsed = parseMcpBudgetSection(section);
|
||||
assert.equal(parsed.explainsMcpAsBudgetConcern, true,
|
||||
`must explain MCP/tool schemas as a token/context-budget concern; section was:\n${section}`);
|
||||
});
|
||||
|
||||
test('names enabledMcpjsonServers and disabledMcpjsonServers (#3025 requirement 2)', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
const parsed = parseMcpBudgetSection(section);
|
||||
assert.equal(parsed.namesEnabledMcpjsonServers, true,
|
||||
'section must reference `enabledMcpjsonServers` so users know the exact key');
|
||||
assert.equal(parsed.namesDisabledMcpjsonServers, true,
|
||||
'section must reference `disabledMcpjsonServers` for parity');
|
||||
assert.equal(parsed.namesClaudeSettingsJson, true,
|
||||
'section must name `.claude/settings.json` as the location of the toggle');
|
||||
});
|
||||
|
||||
test('includes a pre-phase audit checklist with all three classes (#3025 requirement 3)', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
const parsed = parseMcpBudgetSection(section);
|
||||
assert.equal(parsed.includesPrePhaseAudit, true,
|
||||
'section must include audit/checklist framing for pre-phase MCP review');
|
||||
assert.equal(parsed.auditMentionsBrowserOrPlaywright, true,
|
||||
'audit must mention browser/playwright tools as a candidate for disabling');
|
||||
assert.equal(parsed.auditMentionsPlatformSpecific, true,
|
||||
'audit must mention platform-specific tools (Mac/Windows/OS-specific)');
|
||||
assert.equal(parsed.auditMentionsCrossProject, true,
|
||||
'audit must mention stale/cross-project MCPs from other projects');
|
||||
});
|
||||
|
||||
test('explains GSD does not own MCP enablement — harness concern (#3025 requirement 4)', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
const parsed = parseMcpBudgetSection(section);
|
||||
assert.equal(parsed.explainsHarnessNotGsd, true,
|
||||
'section must explicitly state GSD does not manage MCP enablement (harness concern)');
|
||||
});
|
||||
|
||||
test('notes interaction with model_profile (compounding levers) (#3025 requirement 5)', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
const parsed = parseMcpBudgetSection(section);
|
||||
assert.equal(parsed.mentionsModelProfileInteraction, true,
|
||||
'section must note that trimming MCPs compounds with model_profile choice');
|
||||
});
|
||||
|
||||
test('full semantic record matches the #3025 contract — typed snapshot', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
const parsed = parseMcpBudgetSection(section);
|
||||
const contract = {
|
||||
ok: parsed.ok,
|
||||
explainsMcpAsBudgetConcern: parsed.explainsMcpAsBudgetConcern,
|
||||
namesEnabledMcpjsonServers: parsed.namesEnabledMcpjsonServers,
|
||||
namesDisabledMcpjsonServers: parsed.namesDisabledMcpjsonServers,
|
||||
namesClaudeSettingsJson: parsed.namesClaudeSettingsJson,
|
||||
includesPrePhaseAudit: parsed.includesPrePhaseAudit,
|
||||
auditMentionsBrowserOrPlaywright: parsed.auditMentionsBrowserOrPlaywright,
|
||||
auditMentionsPlatformSpecific: parsed.auditMentionsPlatformSpecific,
|
||||
auditMentionsCrossProject: parsed.auditMentionsCrossProject,
|
||||
explainsHarnessNotGsd: parsed.explainsHarnessNotGsd,
|
||||
mentionsModelProfileInteraction: parsed.mentionsModelProfileInteraction,
|
||||
};
|
||||
assert.deepStrictEqual(contract, {
|
||||
ok: true,
|
||||
explainsMcpAsBudgetConcern: true,
|
||||
namesEnabledMcpjsonServers: true,
|
||||
namesDisabledMcpjsonServers: true,
|
||||
namesClaudeSettingsJson: true,
|
||||
includesPrePhaseAudit: true,
|
||||
auditMentionsBrowserOrPlaywright: true,
|
||||
auditMentionsPlatformSpecific: true,
|
||||
auditMentionsCrossProject: true,
|
||||
explainsHarnessNotGsd: true,
|
||||
mentionsModelProfileInteraction: true,
|
||||
}, 'context-budget.md MCP section contract violated');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── docs/USER-GUIDE.md task section ────────────────────────────────────────
|
||||
|
||||
describe('#3025 docs/USER-GUIDE.md: companion task section exists', () => {
|
||||
test('USER-GUIDE.md has an MCP-trimming task section', () => {
|
||||
const section = extractSection(USER_GUIDE_MD, 'mcp');
|
||||
assert.ok(section,
|
||||
'USER-GUIDE.md must have a `### ...MCP...` task section so users find it via the guide');
|
||||
});
|
||||
|
||||
test('USER-GUIDE.md task section names the harness key and cross-links the reference', () => {
|
||||
const section = extractSection(USER_GUIDE_MD, 'mcp');
|
||||
const parsed = parseMcpBudgetSection(section);
|
||||
assert.equal(parsed.namesEnabledMcpjsonServers, true,
|
||||
'task section must mention the harness key by name');
|
||||
// Cross-link to the reference doc — assert on the parsed flag so
|
||||
// the invariant lives alongside the other named flags (CR follow-up
|
||||
// on the no-source-grep standard).
|
||||
assert.equal(parsed.crossLinksContextBudget, true,
|
||||
'task section must cross-link to context-budget.md');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── markdownlint pre-flight (per bundle-docs-with-code skill) ──────────────
|
||||
|
||||
describe('#3025 markdownlint pre-flight: MD040 + MD056', () => {
|
||||
test('every fenced code block in the new MCP section has a language tag (MD040)', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
// Guard: extractSection returns null when the section is missing.
|
||||
// Without this, `section.match(...)` would throw a TypeError instead
|
||||
// of producing a meaningful assertion failure (CR follow-up).
|
||||
assert.ok(section, 'MCP section not found in context-budget.md — cannot check MD040');
|
||||
const fences = (section.match(/^```([a-zA-Z0-9_+-]*)?\s*$/gm) || []);
|
||||
// Pairs of fences open/close; odd-indexed ones close blocks. Every
|
||||
// OPENING fence must have a language tag. Closing fences are bare ```.
|
||||
// Walk pairs: even index = opener, odd = closer.
|
||||
const openers = fences.filter((_, i) => i % 2 === 0);
|
||||
const missing = openers.filter((line) => /^```\s*$/.test(line));
|
||||
assert.deepStrictEqual(missing, [],
|
||||
`every fenced code block opener must have a language tag (MD040). Missing: ${JSON.stringify(missing)}`);
|
||||
});
|
||||
|
||||
test('every markdown table row in the new MCP section has the same column count as its header (MD056)', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
// Guard: same null-section concern as MD040 above (CR follow-up).
|
||||
assert.ok(section, 'MCP section not found in context-budget.md — cannot check MD056');
|
||||
const lines = section.split('\n');
|
||||
// Walk through and detect tables: header row followed by a separator
|
||||
// (--- pattern) followed by data rows. Count `|` per line.
|
||||
const issues = [];
|
||||
for (let i = 0; i < lines.length - 1; i += 1) {
|
||||
const header = lines[i];
|
||||
const sep = lines[i + 1];
|
||||
if (!/^\s*\|.*\|\s*$/.test(header)) continue;
|
||||
if (!/^\s*\|[\s\-:|]+\|\s*$/.test(sep)) continue;
|
||||
const headerCols = (header.match(/\|/g) || []).length;
|
||||
// Walk data rows
|
||||
for (let j = i + 2; j < lines.length; j += 1) {
|
||||
const row = lines[j];
|
||||
if (!/^\s*\|.*\|\s*$/.test(row)) break;
|
||||
const rowCols = (row.match(/\|/g) || []).length;
|
||||
if (rowCols !== headerCols) {
|
||||
issues.push({ line: j, expected: headerCols, actual: rowCols, row });
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(issues, [],
|
||||
`table rows must match header column count (MD056). Issues: ${JSON.stringify(issues, null, 2)}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user