From dc553ae03367964d20f5a1e378e5a2224ca46454 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 00:31:17 +0300 Subject: [PATCH 01/11] docs: plan stable documentation follow-ups Record the verified shared formatter boundary, mutable hosted-document contract, snapshot preservation, and end-to-end acceptance workflow. --- .../stable-hosted-documentation-followups.md | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 docs/plans/stable-hosted-documentation-followups.md diff --git a/docs/plans/stable-hosted-documentation-followups.md b/docs/plans/stable-hosted-documentation-followups.md new file mode 100644 index 00000000..0e8a0a5b --- /dev/null +++ b/docs/plans/stable-hosted-documentation-followups.md @@ -0,0 +1,308 @@ +# Plan: Stable hosted-documentation follow-ups + +## Status + +- Overall: **READY** +- Phase 1: **READY** + +## Overall objective + +Automatic CLI and MCP follow-ups generated from unified-search evidence must +treat hosted/crawled `documentation_page` HTTP(S) locators as mutable +current-content addresses. They forward the backend-emitted page URL or exact +heading fragment unchanged and omit observed search line bounds. Repository +documentation remains snapshot-addressed and keeps its focused ranges, while +explicit caller-supplied documentation read bounds continue to cross the public +read boundary unchanged. + +### Problem + +GitHits 0.17.1 fixed fragment follow-ups but still attaches a search hit's +`startLine` / `endLine` to a page-only hosted documentation URL. Those values are +display/evidence coordinates from one publication, while the URL resolves the +publisher's current body. If the page changes, the automatic follow-up can read +the wrong current excerpt or fail instead of reading the current page. + +### Verified current state and evidence + +- Planning baseline is clean `origin/main` at + `ce93eb1f80742d88162d8a07ec7c0009f307dfd7`, fetched on 2026-09-17. +- `packages/mcp/src/shared/follow-up-command-text.ts` + `documentationReadLocator()` is the shared pure owner for the read target used + by CLI text and MCP text/JSON. `buildUnifiedSearchSuccessPayload()` uses + `buildSearchHitFollowUpCommand()` for the MCP-syntax JSON `followUp`; CLI text + displays `documentationReadLocator().target` rather than an executable command. + The helper's CLI command syntax remains an internal/tested representation, not + a currently emitted CLI-search field. +- A direct baseline probe against current main produced: + - hosted page: `read target="https://docs.example.test/guide" start_line=81 end_line=93` + - hosted CLI page: `githits read 'https://docs.example.test/guide' --lines 81-93` + - repository doc: the snapshot target with the same range on both surfaces. +- Existing fragment branches already return the exact emitted HTTP(S) fragment + without bounds, including mixed-case schemes. They do not decode, normalize, + or synthesize anchors. +- `packages/mcp/src/shared/unified-search-response.ts` retains the original + locator ranges independently of the generated `followUp`. The fix therefore + need not discard search evidence from JSON. +- Explicit manual reads bypass follow-up selection. MCP + `packages/mcp/src/tools/read.ts`, CLI `src/commands/read.ts`, shared request + validation, and `ReadServiceImpl` already forward caller bounds. Current tests + cover CLI fragment-range override and the public service wire variables; the + phase adds a direct MCP public-tool regression. +- Focused baseline tests passed: 148 tests across the shared follow-up, search + response, MCP read, and CLI read suites. +- Backend contract is already deployed in pkgseer-backend PR #2595 at + `d1d5ef9c377b6f06f5eed81c2c9c473e54ead9d8`: hosted/crawled docs are mutable + current-content URL addresses; repository docs are a separate snapshot + contract; search lines are evidence coordinates; headings select their full + subtree. No backend work is required or in scope. + +### Scope + +- Change shared automatic follow-up generation so an HTTP(S) + `documentation_page` target never inherits observed search bounds. +- Preserve exact page-only target bytes and the existing exact + `docsReadTarget#fragment` promotion rule; do not derive anchors. +- Preserve ranges for `repository_doc` targets and other snapshot-addressed + repository reads. +- Preserve structured locator evidence, manual read arguments, schemas, backend + queries, and read-service behavior. +- Update stable MCP routing guidance, CLI Agent Skill guidance, tool guidance, + and permanent implementation documentation to distinguish mutable hosted URLs + from snapshot-addressed repository documentation. +- Add one change fragment with patch impact for both `githits` and + `@githits/mcp`; do not prepare or execute a release. + +### Non-goals + +- Backend, pkgseer-backend, remote-mcp, or other-worktree changes. +- Docpack IDs, page-row IDs, versions, historical storage, snapshots, retries, + fallback reads, compatibility branches, feature flags, or inferred anchors. +- Curated answers or repository-doc addressing changes. +- Changing explicit `read` / `docs read` line-range semantics. + +### Target architecture and ownership + +`documentationReadLocator()` remains the sole automatic documentation follow-up +selector. For `documentation_page` plus an HTTP(S) effective target, it returns +the exact target without search bounds, except that it continues to promote an +exact backend-emitted `target#nonempty-fragment` source URL unchanged. Every +other documentation locator retains the existing target and evidence bounds. + +The shared follow-up formatter naturally owns this policy because it translates +search evidence into CLI and MCP actions. The read request/service layers own +explicit caller selections and remain unchanged. Putting the rule in either CLI +or MCP entrypoint would duplicate policy; putting it in the read service would +erase the distinction between generated evidence coordinates and intentional +caller bounds. + +```text +search hit locator + -> shared documentationReadLocator + hosted documentation_page HTTP(S): exact URL/fragment, no evidence bounds + repository_doc: exact snapshot target + evidence bounds + -> CLI command / MCP followUp string + +manual read arguments + -> existing read request boundary + -> exact caller target and optional bounds (unchanged) +``` + +### Cross-cutting considerations + +- **Security:** Locator bytes remain opaque and shell quoting remains unchanged; + no URL parsing beyond the existing case-insensitive HTTP(S) classification and + exact fragment-prefix comparison is added. +- **Performance:** This changes one constant-time pure branch and adds no I/O. + No performance claim or benchmark is needed because no performance path is + being optimized. +- **Compatibility:** The structured search locator continues to expose original + evidence ranges. Only generated hosted-doc follow-ups stop replaying them. + Repository-doc and explicit manual ranges stay compatible. +- **Migration/rollback:** No data migration exists. A rollback restores the prior + formatter branch; it would also restore the publication-replacement defect. +- **Operations:** Hosted clients receive the MCP behavior only after the normal + `@githits/mcp` release, remote-mcp dependency update, and deployment. None of + those last-mile steps is authorized here. +- **Documentation:** Update `docs/implementation/tools.md`, + `docs/implementation/cli-commands.md`, and `docs/implementation/unified-read.md`. + Keep `buildMcpQuickStart()` and the terminal guide in + `skills/githits-mcp/SKILL.md` byte-for-byte aligned; update the canonical CLI + files `skills/githits-code/SKILL.md` and + `skills/githits-code/references/code-and-docs.md`, then regenerate/check plugin + assets. Follow the repository-local `githits-plugin-maintenance` workflow and + its required `docs/implementation/plugin-packaging.md` guidance; never author + generated plugin assets directly. + +### Overall assumptions + +- `resultType: DOCUMENTATION_PAGE` is the authoritative discriminator for the + mutable hosted/crawled contract, and `REPOSITORY_DOC` remains the independent + snapshot contract. This is verified by the deployed backend contract supplied + for the task and by current typed result handling. +- `docsReadTarget`, falling back to `pageId`, is the exact read target. The client + must not manufacture a different address. +- An exact emitted `sourceUrl === target + "#" + nonempty-fragment` remains the + only allowed source-URL promotion. + +### Unknowns or product decisions + +None. Current main confirms the requested boundary and the backend contract +settles hosted-versus-repository semantics. + +### Dependencies + +- Deployed backend contract from pkgseer-backend PR #2595. +- Existing shared search formatter, unified read surface, plugin generator, smoke + harness, and agent-eval harness. + +### Planning review + +- Internal pre-flight found that the first draft treated the helper's CLI command + representation as emitted CLI search output. Accepted and corrected: CLI text + displays the shared target, while JSON `followUp` uses MCP syntax. The draft + also now names both canonical CLI skill files and the plugin-maintenance flow. +- Fable plan review was clean. Its final fresh-context check noted only that the + existing shell-quoting test encodes the defective page-only `--lines` suffix; + Phase 1 step 1 already requires rewriting that assertion bounds-free while + retaining its quoting coverage. + +### Overall acceptance criteria + +- Page-only hosted search hits emit bounds-free CLI and MCP follow-ups while + retaining their structured evidence coordinates. +- Hosted fragment hits remain bounds-free and preserve the exact emitted fragment. +- Repository docs keep their snapshot locators and ranges. +- Explicit caller-supplied docs-read ranges reach CLI, MCP, and service boundaries + unchanged. +- Tests model publication replacement by treating hosted search coordinates as + stale evidence and proving they are absent only from the automatic current-URL + action. No snapshots, retries, or compatibility machinery are added. +- Focused and full tests, typecheck, Biome format/lint, build, plugin generation + and validation, package validation, CLI/MCP smoke suites, built smoke suites, + and one available live/dev search-to-read path complete or are reported with + exact environmental limitations. +- Targeted agent evaluation covers the changed stable guidance and its artifacts + are inspected for actual calls, final answer, metrics, and isolation violations. +- The implementation passes internal pre-flight and the required Opus review loop, + is committed in stable increments, pushed, and opened as a draft PR. + +## Phase map + +1. **Phase 1 — hosted current-content follow-ups omit search bounds while every + snapshot/manual range contract remains intact: READY.** + +## Phase 1 detailed plan + +### Status + +**READY** + +### Expected outcome + +Every automatic search-to-read action uses the stable address appropriate to its +source: mutable hosted docs use the exact current-content URL or emitted fragment +without search coordinates, while repository docs keep exact snapshot ranges. +Callers can still intentionally request any valid documentation range. + +### Assumptions + +- The overall assumptions above remain true at implementation start. +- Existing live smoke credentials, if present, can be consumed by repository + smoke commands without being displayed. Their absence is an evidence limit, + not a reason to add a fallback. + +### Unknowns or product decisions + +None. + +### Dependencies + +- Overall dependencies above. + +### Implementation steps + +1. Add failing shared-formatter and response regressions for page-only hosted + URLs with stale publication coordinates, exact fragments, repository-doc + snapshot ranges, and the internal CLI plus emitted MCP command + representations. Add a CLI command-level search regression proving the public + hit displays the exact hosted target without coordinates, and a focused MCP + read-tool assertion that explicit documentation bounds are forwarded unchanged. +2. Tighten `documentationReadLocator()` at the existing boundary: classify only + HTTP(S) `documentation_page` targets as mutable, retain exact fragment + promotion, and omit bounds for their page-only fallback. Leave structured + locators and manual read construction unchanged. +3. Update the search tool descriptor and its contract tests, stable MCP guide and + exact public-skill copy, CLI Agent Skill/reference and packaging assertions, + and the three permanent implementation documents. Keep wording explicit that + generated actions differ from explicit reads. +4. Add a cross-package fixed change fragment (`githits: patch`, + `@githits/mcp: patch`), regenerate plugin assets from canonical inputs, and + inspect every generated diff. +5. Run focused tests first, then the full required validation. Run targeted stable + agent evals because stable instructions/tool guidance change, and inspect their + artifacts rather than treating harness exit status as quality evidence. +6. Run internal pre-flight and Opus review rounds to a clean result, applying and + verifying every valid in-scope finding. Update this plan to actual evidence, + commit review fixes, push, and open a draft PR without merging or releasing. + +### Edge cases and boundaries + +- Mixed-case HTTP(S) schemes receive the same hosted classification. +- A target with an existing nonempty fragment is forwarded byte-for-byte. +- A page-only target is forwarded even when `sourceUrl` is identical, absent, or + a distinct non-promotable URL. +- Only an exact byte-prefix `target#nonempty-fragment` is promoted; encoded, + normalized, reordered, or guessed alternatives are not. +- A repository-doc hit keeps bounds even if it carries URL provenance because its + result type, not provenance shape, owns snapshot semantics. +- Missing `docsReadTarget` continues to fall back to `pageId`; an HTTP(S) page ID + follows hosted current-content semantics, while opaque IDs retain ranges. +- Explicit zero/one/two-sided manual ranges keep current validation and forwarding. + +### Verification strategy + +- Focused unit tests: + - `bun test packages/mcp/src/shared/follow-up-command-text.test.ts` + - `bun test packages/mcp/src/shared/unified-search-response.test.ts` + - `bun test packages/mcp/src/tools/read.test.ts src/commands/read.test.ts packages/core-internal/src/services/read-service.test.ts` + - affected instruction, descriptor, skill-packaging, and parity tests. +- Repository checks: `bun test`, `bun run typecheck`, `bun run format:check`, + `bun run lint`, `bun run build`, `bun run plugins:generate`, + `bun run plugins:check`, and `bun run validate:packages`. +- Smoke checks: `bun run smoke:cli`, `bun run smoke:mcp`, then after build + `bun run smoke:cli:built` and `bun run smoke:mcp:built`. +- Live/dev acceptance: exercise a real docs search that returns a hosted page, + copy the exact displayed CLI `[docs page]` target into `githits read`, and + separately inspect and execute the MCP `followUp` with absent bounds. If CLI + JSON is inspected, record that its shared `followUp` uses MCP syntax. Also + retain the smoke harness's explicit ranged docs read. Use the available + local/dev or live mode only; do not expose auth state or secrets. +- Agent guidance: run targeted `docs-search-followup` workloads for Claude and + Codex when practical, then inspect `tool-calls.json`, `final.json`, + `metrics.json`, and any `isolation-violations.json`. + +### Phase acceptance criteria + +- All overall acceptance criteria hold. +- The code delta stays at the shared formatter owner with no backend/service + behavior change and no new infrastructure. +- Documentation and generated plugin surfaces agree with the implemented rule. +- Review is clean and the draft PR is open with required labels/check status + reported. + +## Phase-boundary reorientation + +This is the only implementation phase. Before implementation, compare HEAD and +the plan to freshly fetched `origin/main`; stop if the shared owner or backend +contract changed. Before PR creation, replace planned verification with actual +results and reconcile any contradictions. No later implementation phase begins +from this plan. + +## Completion and cleanup + +Phase 1 is complete when its acceptance criteria are verified, review is clean, +the plan records actual evidence, and a draft PR is open. Keep this plan through +PR review. After the PR merges, transfer any remaining durable facts to the named +implementation documents (already part of this phase) and delete this temporary +plan in the repository's normal post-merge cleanup increment. From 267f459559232950510851cf63e4bbb81f55821c Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 00:56:04 +0300 Subject: [PATCH 02/11] fix: stabilize hosted documentation follow-ups Drop search evidence ranges from generated follow-ups for mutable hosted documentation URLs while preserving repository snapshot ranges and explicit caller bounds. Update durable CLI, MCP, and Agent Skill guidance for the current-content contract. --- ...le-hosted-documentation-followups.fixed.md | 6 ++ docs/implementation/cli-commands.md | 10 +-- docs/implementation/mcp-cli-parity.md | 5 +- docs/implementation/tools.md | 8 +-- docs/implementation/unified-read.md | 13 +++- packages/mcp/src/mcp/instructions.test.ts | 12 ++++ packages/mcp/src/mcp/instructions.ts | 4 ++ packages/mcp/src/mcp/server.test.ts | 2 + .../src/shared/follow-up-command-text.test.ts | 64 ++++++++++++++++++- .../mcp/src/shared/follow-up-command-text.ts | 19 +++--- .../shared/unified-search-response.test.ts | 41 ++++++++++++ packages/mcp/src/smoke-test.ts | 4 +- .../mcp/src/tools/list-package-docs.test.ts | 2 + packages/mcp/src/tools/list-package-docs.ts | 1 + packages/mcp/src/tools/read.test.ts | 14 ++++ packages/mcp/src/tools/read.ts | 1 + packages/mcp/src/tools/search.test.ts | 18 +++++- packages/mcp/src/tools/search.ts | 2 +- skills/githits-code/SKILL.md | 2 +- .../githits-code/references/code-and-docs.md | 2 +- skills/githits-mcp/SKILL.md | 4 ++ src/cli.test.ts | 10 +++ src/commands/docs/list.ts | 9 +-- src/commands/docs/read.test.ts | 2 +- src/commands/mcp-instructions.test.ts | 6 ++ src/commands/search.test.ts | 11 +++- src/commands/search.ts | 5 ++ src/skills-packaging.test.ts | 5 +- 28 files changed, 242 insertions(+), 40 deletions(-) create mode 100644 changes/stable-hosted-documentation-followups.fixed.md diff --git a/changes/stable-hosted-documentation-followups.fixed.md b/changes/stable-hosted-documentation-followups.fixed.md new file mode 100644 index 00000000..c8b2d25b --- /dev/null +++ b/changes/stable-hosted-documentation-followups.fixed.md @@ -0,0 +1,6 @@ +--- +"githits": patch +"@githits/mcp": patch +--- + +- **Keep hosted documentation follow-ups stable across publications** - Automatic search follow-ups now reuse mutable hosted page URLs or exact emitted fragments without stale search line bounds, while repository-document snapshots and explicit read ranges remain unchanged. diff --git a/docs/implementation/cli-commands.md b/docs/implementation/cli-commands.md index ecb6b40a..4b6c8345 100644 --- a/docs/implementation/cli-commands.md +++ b/docs/implementation/cli-commands.md @@ -224,7 +224,7 @@ Unified search spans indexed dependency and repository code, docs, and explicit The original unified-search plan envisaged hiding partial mode entirely in v1 to make results trustworthy by default. We kept the flag exposed because some agent and CLI flows benefit from "show me what you have so far." The trust contract is preserved by keeping the default atomic across runnable target/source pairs: callers must explicitly opt into a serveable subset, while any unflagged interim evidence still covers every runnable pair and carries its `searchRef` and freshness signals. -**Output.** CLI human output and MCP `text-v1` use one shared outcome-first formatter. The headline combines result count/type breakdown, active or terminal lifecycle, aggregate readiness, and pagination when applicable. Ordinary completed current results collapse to one `Sources: - ` row: code and symbols use compact lane names, while documentation contributors retain canonical `site:` and `github:/@` locators. A source identical to its standalone target is written once; a sole pinned repository source replaces its less-specific ref-less repository target, while an already-pinned target remains beside its resolved commit. Compact repository provenance requires both the repository URL and commit. Documentation without concrete provenance stays in detailed target-state form. Any stale, provisional, coverage, constraint, alternative, suggestion, terminal, or other trust fact keeps every requested target in the same detailed list. Each target identity is followed by deterministic `using`, `searched`, `indexing`, terminal/unavailable, `available`, `indexed`, and constraint segments as applicable. Detailed lanes are `code`, `symbols`, `repository docs`, concrete site docs, and docs. Hits remain a separate numbered ranked evidence list with their follow-up locators: `[1] npm:express@5.2.1 History.md:169-179 [repo doc] - 5.0.0-alpha.4 / 2017-03-01` or `[2] https://expressjs.com/en/4x/api/router/#routerroute [docs page] npm:express - router.route()`. Proven repository snippets use their matched range in the header; path-only hits show an actionable file header without arbitrary chunk coordinates. Explicit symbol hits preserve qualified identity, signature detail, kind, and any differing definition range. A differing indexed range without a definition is labelled as a chunk; equal ranges are printed once. Documentation headers promote an exact indexed `docsReadTarget#fragment` as the read target, otherwise prefer the emitted `docsReadTarget`, and fall back to stable `pageId` only when discovery omits the target; formatter-authored punctuation is ASCII and Unicode in backend payloads passes through unchanged. Executable read command lines and qualified internal IDs stay omitted from default text. Active empty output is `No results yet | indexing | 0/1 ready`; no-snapshot output is `No result snapshot yet | indexing | 0/1 ready`, with the corresponding lower-case lifecycle for other active states. Terminal no-snapshot output is `No result snapshot | failed | 0/1 ready`, and completed output omits lifecycle/readiness. Query-wide warnings appear once after target rows and before hits. There is no separate session row: at most one `Next:` line follows the hit list, and an active `searchRef` appears exactly once there. CLI uses `Next: githits search-status --wait `; MCP uses its own `search_status` syntax. Active follow-up waits use the largest indexing upper bound plus ten seconds, rounded upward to ten seconds and capped at 120. Missing ranges use the 30-second default as a floor; no ranges keep 30 seconds. The examples below show that default. Completed evidence-status retrieval keeps the default. These are advisory total indexing durations, not remaining time or a search ETA; JSON progress preserves the full `indexingEstimates` evidence. See [discovery indexing estimates](tools.md) for the contract and production backend rollout prerequisite. CLI enables ANSI emphasis when supported, but removing ANSI leaves the same hierarchy and wording apart from surface-native actions; line breaks can differ because CLI uses terminal width while MCP defaults to 80 columns. `--json` emits the shared stable success/error envelope used by MCP `search`, including the full initial `query` echo and exact result-bearing `partialResults` Boolean. Repository hit locators preserve legacy target-relative evidence coordinates plus `commitSha`, `repositoryFilePath`, `evidenceRange`, `indexedRange`, and relation-aware `symbolContext`; the preferred `followUp` uses semantic `preferredRead` when available, otherwise the proven definition or focused evidence at the exact served repository snapshot. JSON remains lossless while text is optimized for agent decisions. +**Output.** CLI human output and MCP `text-v1` use one shared outcome-first formatter. The headline combines result count/type breakdown, active or terminal lifecycle, aggregate readiness, and pagination when applicable. Ordinary completed current results collapse to one `Sources: - ` row: code and symbols use compact lane names, while documentation contributors retain canonical `site:` and `github:/@` locators. A source identical to its standalone target is written once; a sole pinned repository source replaces its less-specific ref-less repository target, while an already-pinned target remains beside its resolved commit. Compact repository provenance requires both the repository URL and commit. Documentation without concrete provenance stays in detailed target-state form. Any stale, provisional, coverage, constraint, alternative, suggestion, terminal, or other trust fact keeps every requested target in the same detailed list. Each target identity is followed by deterministic `using`, `searched`, `indexing`, terminal/unavailable, `available`, `indexed`, and constraint segments as applicable. Detailed lanes are `code`, `symbols`, `repository docs`, concrete site docs, and docs. Hits remain a separate numbered ranked evidence list with their follow-up locators: `[1] npm:express@5.2.1 History.md:169-179 [repo doc] - 5.0.0-alpha.4 / 2017-03-01` or `[2] https://expressjs.com/en/4x/api/router/#routerroute [docs page] npm:express - router.route()`. Proven repository snippets use their matched range in the header; path-only hits show an actionable file header without arbitrary chunk coordinates. Explicit symbol hits preserve qualified identity, signature detail, kind, and any differing definition range. A differing indexed range without a definition is labelled as a chunk; equal ranges are printed once. Documentation headers promote an exact indexed `docsReadTarget#fragment` as the read target, otherwise prefer the emitted `docsReadTarget`, and fall back to compatible `pageId` only when discovery omits the target; formatter-authored punctuation is ASCII and Unicode in backend payloads passes through unchanged. Executable read command lines and qualified internal IDs stay omitted from default text. Active empty output is `No results yet | indexing | 0/1 ready`; no-snapshot output is `No result snapshot yet | indexing | 0/1 ready`, with the corresponding lower-case lifecycle for other active states. Terminal no-snapshot output is `No result snapshot | failed | 0/1 ready`, and completed output omits lifecycle/readiness. Query-wide warnings appear once after target rows and before hits. There is no separate session row: at most one `Next:` line follows the hit list, and an active `searchRef` appears exactly once there. CLI uses `Next: githits search-status --wait `; MCP uses its own `search_status` syntax. Active follow-up waits use the largest indexing upper bound plus ten seconds, rounded upward to ten seconds and capped at 120. Missing ranges use the 30-second default as a floor; no ranges keep 30 seconds. The examples below show that default. Completed evidence-status retrieval keeps the default. These are advisory total indexing durations, not remaining time or a search ETA; JSON progress preserves the full `indexingEstimates` evidence. See [discovery indexing estimates](tools.md) for the contract and production backend rollout prerequisite. CLI enables ANSI emphasis when supported, but removing ANSI leaves the same hierarchy and wording apart from surface-native actions; line breaks can differ because CLI uses terminal width while MCP defaults to 80 columns. `--json` emits the shared stable success/error envelope used by MCP `search`, including the full initial `query` echo and exact result-bearing `partialResults` Boolean. Repository hit locators preserve legacy target-relative evidence coordinates plus `commitSha`, `repositoryFilePath`, `evidenceRange`, `indexedRange`, and relation-aware `symbolContext`; the preferred `followUp` uses semantic `preferredRead` when available, otherwise the proven definition or focused evidence at the exact served repository snapshot. JSON remains lossless while text is optimized for agent decisions. Repository code/docs display proven numbered `matchedSource` and enclosing scope metadata, without routine authority captions. A file-path-only hit without matched @@ -773,7 +773,7 @@ githits docs list npm:express --limit 20 githits docs list npm:express --json ``` -Lists hosted/crawled and repository-backed documentation pages for a package. Each row includes the stable page ID, a source badge, any distinct provenance, and a shell-quoted `read` command using the emitted `docsReadTarget`. Active crawled pages therefore use their publisher HTTP(S) URL, while retired crawled and snapshot-pinned repository pages use stable IDs. JSON retains all three locator roles and includes repo URL / git ref / file path for repository-backed docs so callers can follow up with `code read` when source context is needed. +Lists hosted/crawled and repository-backed documentation pages for a package. Each row includes the compatible page ID, a source badge, any distinct provenance, and a shell-quoted `read` command using the emitted `docsReadTarget`. Hosted/crawled HTTP(S) targets address mutable current content; no docpack or page-row identifier is a stable snapshot address. Repository targets remain snapshot-addressed. JSON retains all three locator roles and includes repo URL / git ref / file path for repository-backed docs so callers can follow up with `code read` when source context is needed. The response also retains the backend's exact `codeIndexState`. `PENDING` and `INDEXING` empty results are rendered as preparation still in progress with a replayable `docs list` action, never as “No documentation pages found.” `PROVISIONAL` results keep and render every available page while clearly marking that indexing continues. CLI `--json` and MCP `format: "json"` share the same lifecycle-bearing envelope. @@ -796,11 +796,11 @@ githits docs read --verbose githits docs read --json ``` -Reads a documentation page returned by `docs list` or search results. Use a sufficient search snippet directly; otherwise run its generated follow-up. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. Search text promotes an exact indexed fragment to that displayed target. Historical crawled IDs and snapshot-pinned repository IDs remain compatible. URL reads resolve only active existing content and never enqueue crawling. Default output is content-only for easy piping; `--verbose` adds a metadata header. +Reads a documentation page returned by `docs list` or search results. Use a sufficient search snippet directly; otherwise run its generated follow-up. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. Hosted/crawled HTTP(S) targets address mutable current content. Automatic search follow-ups therefore forward the exact emitted page URL or fragment without the search hit's display/evidence line coordinates. Search text promotes only an exact emitted fragment; it never derives an anchor. Snapshot-pinned repository documentation keeps its target and ranges. Historical crawled IDs remain compatible. URL reads resolve only active existing content and never enqueue crawling. Default output is content-only for easy piping; `--verbose` adds a metadata header. -**Fragments and line ranges.** An HTTP(S) fragment needs no explicit range and resolves exactly one indexed section in the backend. `--lines 10-40`, `--lines 10-`, and `--lines -40` are supported and forward only their written bounds; either bound replaces fragment resolution with a page-relative range. The backend defaults omitted bounds, clamps an end beyond EOF, and rejects invalid starts/ranges. The CLI never strips or normalizes opaque IDs and does not infer publisher anchor slugs. +**Fragments and line ranges.** An HTTP(S) fragment needs no explicit range and resolves the heading plus its full subtree through the next equal-or-higher heading. `--lines 10-40`, `--lines 10-`, and `--lines -40` are supported and forward only their written bounds; either caller-supplied bound intentionally replaces fragment resolution with a page-relative range. The backend defaults omitted bounds, clamps an end beyond EOF, and rejects invalid starts/ranges. The CLI never strips or normalizes opaque IDs and does not infer publisher anchor slugs. -**Output envelope.** `{docsReadTarget, pageId, title?, sourceKind?, sourceUrl?, repoUrl?, gitRef?, filePath?, totalLines, startLine?, endLine?, anchor?, content}`. `pageId` remains the stable replay pointer and `sourceUrl` remains provenance. The range is the actual returned absolute page range, `totalLines` is the whole stored page extent including a trailing empty line, and an empty page has no bounds. `anchor` identifies a successfully resolved indexed section. Verbose text prints each distinct locator once plus range/anchor metadata. Repo-backed docs include exact source metadata for `code read` follow-up. +**Output envelope.** `{docsReadTarget, pageId, title?, sourceKind?, sourceUrl?, repoUrl?, gitRef?, filePath?, totalLines, startLine?, endLine?, anchor?, content}`. `pageId` remains a compatible locator and `sourceUrl` remains provenance. Hosted/crawled locators resolve mutable current content rather than a snapshot; repository targets remain snapshot-addressed. The range is the actual returned absolute page range, `totalLines` is the whole stored page extent including a trailing empty line, and an empty page has no bounds. `anchor` identifies a successfully resolved indexed section. Verbose text prints each distinct locator once plus range/anchor metadata. Repo-backed docs include exact source metadata for `code read` follow-up. `DOCUMENTATION_SECTION_UNRESOLVED` is a non-retryable section error with reason `not_found`, `ambiguous`, `inexact_range`, or `unsupported_format`; page absence remains non-retryable `NOT_FOUND`. This deprecated alias remains on the legacy `getDocPage` root. Compact `githits read ` uses `ReadService` and the backend `Query.read` union, which requires both `CodeContextResult` and `GetDocPageResult` branches plus the selected minimum fields, including `contentRange`. Roll out that backend schema first; compact reads have no old-schema fallback. diff --git a/docs/implementation/mcp-cli-parity.md b/docs/implementation/mcp-cli-parity.md index 5607fdea..ee607e93 100644 --- a/docs/implementation/mcp-cli-parity.md +++ b/docs/implementation/mcp-cli-parity.md @@ -388,11 +388,12 @@ MCP text apart from those supplied command dialects; line breaks can differ because CLI uses the terminal width while MCP uses the 80-column default. Documentation discovery and list envelopes retain three distinct locator roles: -preferred `docsReadTarget`, stable replay `pageId`, and provenance `sourceUrl`. +preferred `docsReadTarget`, compatible `pageId`, and provenance `sourceUrl`. Text and generated read follow-ups prefer `docsReadTarget` and fall back to `pageId` only for discovery results where the target is absent. The compatible MCP argument is `target`; both MCP and CLI pass URL or ID values through -unchanged and return the same ranged content. +unchanged and return the same ranged content. Hosted HTTP(S) locators address +mutable current content; repository locators remain snapshot-addressed. CLI `--json` output and MCP `format: "json"` output remain the structured parity boundary: every diff --git a/docs/implementation/tools.md b/docs/implementation/tools.md index e3f0d821..12a1c454 100644 --- a/docs/implementation/tools.md +++ b/docs/implementation/tools.md @@ -161,7 +161,7 @@ see [Unified read](unified-read.md). | `get_example` | `query`, `language?`, `license_mode?`, `format?` | Find canonical cross-project examples when no single target is the answer. Also use when target-scoped search came up short; verify version-sensitive patterns against source/docs. Markdown includes source provenance, generated references and optional `solution_id`; the language field owns correction guidance. | | `search` | `query`, `target?` (compact string), `targets?` (compact strings), `source?`, `public_only?`, `allow_partial_results?`, `limit?`, `offset?`, `wait_timeout_ms?`, `format?` | Discover relevant docs, code, and symbols in a known public target. Pass `query` and either `target` or `targets`, not both. Constraints belong in `query`; inspect qualifier/source warnings. Continue only with an explicit `searchRef` and `search_status` action. | | `search_status` | `search_ref`, `wait_timeout_ms?`, `format?` | Continue an explicit search reference for progress and results. Pass the returned `searchRef` as `search_ref` only with an explicit continuation action. Partial hits require the original opt-in; terminal or unrecognized statuses are not polled. | -| `docs_list` | `target` (package string), `limit?`, `after?`, `format?` | List package documentation targets for follow-up reads. Packages only, not standalone `site:` targets. Entries retain `docsReadTarget`, stable `pageId`, and provenance `sourceUrl`. Exact Go versions accept both `v`-prefixed and unprefixed forms. Repo-backed entries include exact source metadata for `read` when available. Active empty results remain preparation/indexing outcomes rather than becoming “not found”; provisional results retain already-available pages and lifecycle state. | +| `docs_list` | `target` (package string), `limit?`, `after?`, `format?` | List package documentation targets for follow-up reads. Packages only, not standalone `site:` targets. Entries retain `docsReadTarget`, compatible `pageId`, and provenance `sourceUrl`. Hosted HTTP(S) targets address mutable current content; repository targets are snapshot-addressed. Exact Go versions accept both `v`-prefixed and unprefixed forms. Repo-backed entries include exact source metadata for `read` when available. Active empty results remain preparation/indexing outcomes rather than becoming “not found”; provisional results retain already-available pages and lifecycle state. | | `pkg_info` | `target` (unpinned package string), `verbose?`, `format?` | Assess latest package health and adoption: license, downloads, and activity. Requires an unpinned package target and always returns latest; latest-affected and package-wide history counts are distinct. | | `pkg_vulns` | `target` (package string), `min_severity?`, `advisory_scope?`, `include_withdrawn?`, `include_transitive?`, `verbose?`, `format?` | Check current package advisories. Use current evidence, not memory or cutoff disclaimers; distinguish selected-version risk from package history. Transitive evidence is opt-in and adds graph-analysis cost; selected fields define filter/scope limits. | | `pkg_deps` | `target` (package string), `lifecycle?`, `include_importers?`, `include_issues?`, `max_depth?`, `format?` | Inspect what a package depends on, directly or transitively. Direct runtime dependencies are the default; fields opt into other groups, transitive footprint, importer provenance or issue analysis. Public graphs are not application lockfile/reachability evidence. | @@ -756,7 +756,7 @@ explicit `documentation target unavailable`, `target unavailable`, `read` / `read` command lines, qualified non-follow-up internal result IDs, and kind/category tails are omitted from default text; the emitted target remains because it is the -`read` follow-up locator, and JSON keeps `docsReadTarget`, stable `pageId`, +`read` follow-up locator, and JSON keeps `docsReadTarget`, compatible `pageId`, provenance `sourceUrl`, and the generated follow-up. Discovery falls back to `pageId` only when its nullable `docsReadTarget` is absent. Repository hits without a file path use the explicit `location unavailable` value and do not @@ -774,7 +774,7 @@ Breakdowns use `repo code hit(s)` and `repo symbol(s)` alongside `repo doc(s)` and `docs page(s)`. When more results exist without a next offset, the final field is `more available`. Pagination is not repeated as a bottom paragraph. -**Follow-up — crawled-doc section anchors.** Unified search can label a crawled documentation hit with a matching section title. Callers can use a sufficient search snippet directly; when more context is needed, they follow the generated action or displayed read target. When its emitted `sourceUrl` is byte-for-byte the HTTP(S) `docsReadTarget` plus a nonempty fragment, the shared formatter promotes that exact URL to the displayed read target and generated follow-up and omits the search preview's line bounds. A `docsReadTarget` that already contains a fragment is also passed unchanged without bounds. The backend then resolves exactly one indexed section and reports its absolute page range. Missing, duplicate, windowed/inexact, or unsupported sections return non-retryable `DOCUMENTATION_SECTION_UNRESOLVED` with a reason; they never become `NOT_FOUND` or a successful full-page read. Publisher-only IDs omitted during ingestion remain unavailable. The client never decodes or normalizes locator bytes and does not synthesize website slug rules. +**Follow-up — mutable hosted docs and crawled-doc section anchors.** Hosted/crawled `documentation_page` HTTP(S) targets address mutable current content, while repository documentation is separately snapshot-addressed. Search `startLine` / `endLine` values are display/evidence coordinates, so the shared formatter retains them in structured locator evidence but never attaches them to an automatic hosted-doc follow-up. Page-only actions forward the exact emitted `docsReadTarget` (or HTTP(S) `pageId` fallback) without bounds. When an emitted `sourceUrl` is byte-for-byte the page target plus a nonempty fragment, the formatter promotes that exact URL unchanged; a target already containing a fragment also passes unchanged. Repository-doc actions retain their exact snapshot target and ranges. Explicit caller-supplied read bounds remain intentional page-relative selection and still reach the public read boundary unchanged. A sufficient search snippet needs no read. For a fragment read, the backend resolves the heading and its full subtree through the next equal-or-higher heading and reports its absolute page range. Missing, duplicate, windowed/inexact, or unsupported sections return non-retryable `DOCUMENTATION_SECTION_UNRESOLVED` with a reason; they never become `NOT_FOUND` or a successful full-page read. Publisher-only IDs omitted during ingestion remain unavailable. The client never decodes or normalizes locator bytes and does not synthesize website slug rules. Completed-empty action selection is target-aware: exact terminal lanes with no searched/indexing peer get local recovery, while searched-empty evidence can get @@ -844,7 +844,7 @@ The required backend `contentRange` supplies `startLine`, `endLine`, `totalLines `read` passes the docs `target` string through unchanged, whether it is an emitted HTTP(S) `docsReadTarget` or a historical page ID. Successful JSON -reads retain `docsReadTarget`, stable replay `pageId`, and provenance +reads retain `docsReadTarget`, compatible `pageId`, and provenance `sourceUrl`, plus the actual returned `startLine` / `endLine`, whole-page `totalLines`, and any resolved `anchor`. Opaque and snapshot-pinned IDs are not trimmed, percent-decoded, fragment-stripped, or otherwise normalized. Unknown diff --git a/docs/implementation/unified-read.md b/docs/implementation/unified-read.md index adebd20f..50f5ff36 100644 --- a/docs/implementation/unified-read.md +++ b/docs/implementation/unified-read.md @@ -19,6 +19,14 @@ target bytes, including URL query strings, percent encoding, fragments, and pinn repository locators. Never infer the source from URL host or file extension, or retry a failed read against the other backend. +Automatic search follow-ups select those opaque targets earlier in +`packages/mcp/src/shared/follow-up-command-text.ts`. Hosted/crawled +`documentation_page` HTTP(S) targets are mutable current-content addresses, so +their generated actions forward the exact emitted page URL or fragment without +search display/evidence coordinates. Repository documentation is +snapshot-addressed and retains its exact target and ranges. This automatic-action +policy does not alter explicit `read` arguments handled here. + The MCP tool accepts `target`, optional `path`, `start_line`, `end_line`, `wait_timeout_ms`, and `format`. Targets for code are compact package/repository strings, matching `code_files` and `code_grep`. Existing target parsers still own @@ -56,8 +64,9 @@ fallback for compact reads. ## Sections, windows, and waiting -- A docs URL fragment with no explicit bounds selects its exact indexed section. - Do not synthesize line defaults before that backend call. +- A docs URL fragment with no explicit bounds selects its exact indexed section: + the heading and full subtree through the next equal-or-higher heading. Do not + synthesize line defaults before that backend call. - Either explicit bound overrides a docs fragment with a page-relative range. Returned positions and continuation bounds are absolute page line numbers. - Docs text displays at most 150 selected lines by default, or 300 with an explicit diff --git a/packages/mcp/src/mcp/instructions.test.ts b/packages/mcp/src/mcp/instructions.test.ts index 2833145f..464f75bf 100644 --- a/packages/mcp/src/mcp/instructions.test.ts +++ b/packages/mcp/src/mcp/instructions.test.ts @@ -29,6 +29,18 @@ describe("buildLocalMcpQuickStart", () => { expect(quickStart).toContain( "Use snippets when sufficient; otherwise follow generated", ); + expect(quickStart).toContain( + "Hosted/crawled HTTP(S) docs locators address mutable current content", + ); + expect(quickStart).toContain( + "exact emitted URL or fragment without search line bounds", + ); + expect(quickStart).toContain( + "Repository docs are snapshot-addressed and keep returned ranges", + ); + expect(quickStart).toContain( + "`read` bounds only when intentionally selecting a current page range", + ); expect(quickStart).toContain( "This guide owns shared policy; selected tools own call syntax and exceptions", ); diff --git a/packages/mcp/src/mcp/instructions.ts b/packages/mcp/src/mcp/instructions.ts index 4cefc140..44c34df7 100644 --- a/packages/mcp/src/mcp/instructions.ts +++ b/packages/mcp/src/mcp/instructions.ts @@ -36,6 +36,10 @@ For a package or site docs topic, use \`search\` with \`source:"docs"\`. \`docs_list\` browses package pages, not standalone \`site:\` targets. Use snippets when sufficient; otherwise follow generated \`followUp\` calls. Pass displayed \`[docs page]\` locators unchanged to \`read\`. +Hosted/crawled HTTP(S) docs locators address mutable current content; generated +follow-ups use the exact emitted URL or fragment without search line bounds. +Repository docs are snapshot-addressed and keep returned ranges. Add explicit +\`read\` bounds only when intentionally selecting a current page range. For source, locate paths or matches, then read focused lines; never probe directories with \`read\`. Prefer source, symbols, tests, and call sites for behavioral claims. diff --git a/packages/mcp/src/mcp/server.test.ts b/packages/mcp/src/mcp/server.test.ts index 2c403d14..6154ecd6 100644 --- a/packages/mcp/src/mcp/server.test.ts +++ b/packages/mcp/src/mcp/server.test.ts @@ -108,6 +108,8 @@ const DESCRIPTION_ROUTING: Record< "use code_files", "search/code_grep", "target and path for a file; target alone for a docs page", + "Hosted/crawled HTTP(S) docs targets read mutable current content", + "repository-doc targets address snapshots", "A docs URL fragment needs no bounds", "either bound replaces it with a page-relative range", "exact revisions", diff --git a/packages/mcp/src/shared/follow-up-command-text.test.ts b/packages/mcp/src/shared/follow-up-command-text.test.ts index 8bbe58ed..9aa3575c 100644 --- a/packages/mcp/src/shared/follow-up-command-text.test.ts +++ b/packages/mcp/src/shared/follow-up-command-text.test.ts @@ -154,6 +154,16 @@ function documentationHit( }; } +function repositoryDocumentationHit( + locator: UnifiedSearchHitPayload["locator"], +): UnifiedSearchHitPayload { + return { + type: "repository_doc", + target: "github:example/docs@0123456789abcdef", + locator, + }; +} + describe("buildSearchHitFollowUpCommand documentation targets", () => { it("emits unified read syntax for both documentation follow-up surfaces", () => { const value = documentationHit({ pageId: "legacy-crawled-id" }); @@ -188,6 +198,26 @@ describe("buildSearchHitFollowUpCommand documentation targets", () => { ); }); + it("omits stale search coordinates from a mutable hosted page follow-up", () => { + const docsReadTarget = "https://docs.example.test/guide?q=exact"; + const value = documentationHit({ + pageId: docsReadTarget, + docsReadTarget, + sourceUrl: docsReadTarget, + // These coordinates describe the publication searched, not the body that + // this mutable URL may serve when the generated follow-up is executed. + startLine: 81, + endLine: 93, + }); + + expect(buildSearchHitFollowUpCommand(value)).toBe( + `read target=${JSON.stringify(docsReadTarget)}`, + ); + expect(buildSearchHitFollowUpCommand(value, "cli")).toBe( + `githits read '${docsReadTarget}'`, + ); + }); + it("recognizes a mixed-case HTTP scheme when promoting a source fragment", () => { const docsReadTarget = "HTTPS://docs.example.test/guide?q=exact"; const sourceUrl = `${docsReadTarget}#routing`; @@ -236,7 +266,27 @@ describe("buildSearchHitFollowUpCommand documentation targets", () => { ).toBe(`read target=${JSON.stringify(docsReadTarget)}`); }); - it("shell-quotes publisher URL targets containing spaces and metacharacters", () => { + it("retains snapshot ranges for repository documentation", () => { + const docsReadTarget = + "github:example/docs@0123456789abcdef/guide/routing.md"; + const value = repositoryDocumentationHit({ + pageId: docsReadTarget, + docsReadTarget, + sourceUrl: + "https://github.com/example/docs/blob/0123456789abcdef/guide/routing.md", + startLine: 81, + endLine: 93, + }); + + expect(buildSearchHitFollowUpCommand(value)).toBe( + `read target=${JSON.stringify(docsReadTarget)} start_line=81 end_line=93`, + ); + expect(buildSearchHitFollowUpCommand(value, "cli")).toBe( + `githits read '${docsReadTarget}' --lines 81-93`, + ); + }); + + it("shell-quotes mutable publisher URLs without stale search coordinates", () => { const docsReadTarget = "https://docs.example.test/guide with spaces;$(echo nope)?q='quoted'&x=*"; @@ -251,10 +301,20 @@ describe("buildSearchHitFollowUpCommand documentation targets", () => { "cli", ), ).toBe( - `githits read 'https://docs.example.test/guide with spaces;$(echo nope)?q='"'"'quoted'"'"'&x=*' --lines 10-20`, + `githits read 'https://docs.example.test/guide with spaces;$(echo nope)?q='"'"'quoted'"'"'&x=*'`, ); }); + it("treats an HTTP pageId fallback as a mutable current-content address", () => { + const pageId = "HTTPS://docs.example.test/current"; + + expect( + buildSearchHitFollowUpCommand( + documentationHit({ pageId, startLine: 10, endLine: 20 }), + ), + ).toBe(`read target=${JSON.stringify(pageId)}`); + }); + it("falls back to pageId when discovery omits docsReadTarget", () => { expect( buildSearchHitFollowUpCommand( diff --git a/packages/mcp/src/shared/follow-up-command-text.ts b/packages/mcp/src/shared/follow-up-command-text.ts index 9d216dbc..bc509eda 100644 --- a/packages/mcp/src/shared/follow-up-command-text.ts +++ b/packages/mcp/src/shared/follow-up-command-text.ts @@ -93,16 +93,17 @@ export function documentationReadLocator( ): DocumentationReadLocator { const loc = hit.locator; const target = loc.docsReadTarget ?? loc.pageId ?? ""; - if (hasHttpFragment(target)) return { target }; + if (hit.type === "documentation_page" && isHttpUrl(target)) { + if (hasHttpFragment(target)) return { target }; - const fragmentPrefix = `${target}#`; - if ( - hit.type === "documentation_page" && - isHttpUrl(target) && - loc.sourceUrl?.startsWith(fragmentPrefix) && - loc.sourceUrl.length > fragmentPrefix.length - ) { - return { target: loc.sourceUrl }; + const fragmentPrefix = `${target}#`; + if ( + loc.sourceUrl?.startsWith(fragmentPrefix) && + loc.sourceUrl.length > fragmentPrefix.length + ) { + return { target: loc.sourceUrl }; + } + return { target }; } return { diff --git a/packages/mcp/src/shared/unified-search-response.test.ts b/packages/mcp/src/shared/unified-search-response.test.ts index 4b95ec89..53f6441d 100644 --- a/packages/mcp/src/shared/unified-search-response.test.ts +++ b/packages/mcp/src/shared/unified-search-response.test.ts @@ -1063,6 +1063,47 @@ describe("buildUnifiedSearchSuccessPayload", () => { ); }); + it("retains hosted-page evidence coordinates without replaying them against a later publication", () => { + if (defaultUnifiedSearchOutcome.state !== "completed") { + throw new Error("expected completed outcome fixture"); + } + const hit = defaultUnifiedSearchOutcome.result.results[0]!; + const docsReadTarget = "https://docs.example.test/guide"; + const payload = buildUnifiedSearchSuccessPayload( + params, + "router middleware", + "router middleware", + { + ...defaultUnifiedSearchOutcome, + result: { + ...defaultUnifiedSearchOutcome.result, + results: [ + { + ...hit, + resultType: "DOCUMENTATION_PAGE", + locator: { + pageId: docsReadTarget, + docsReadTarget, + sourceUrl: docsReadTarget, + startLine: 81, + endLine: 93, + }, + }, + ], + }, + }, + ); + + expect(payload.results[0]?.locator).toMatchObject({ + docsReadTarget, + startLine: 81, + endLine: 93, + }); + expect(payload.results[0]?.followUp).toBe( + `read target=${JSON.stringify(docsReadTarget)}`, + ); + }); + it("canonicalizes repository hit target labels containing @ in refs", () => { if (defaultUnifiedSearchOutcome.state !== "completed") { throw new Error("expected completed outcome fixture"); diff --git a/packages/mcp/src/smoke-test.ts b/packages/mcp/src/smoke-test.ts index 68531971..8727461a 100644 --- a/packages/mcp/src/smoke-test.ts +++ b/packages/mcp/src/smoke-test.ts @@ -1076,14 +1076,14 @@ async function runLiveSmoke(caller: McpSmokeCaller): Promise { typeof crawledPage.docsReadTarget === "string" && typeof crawledPage.pageId === "string" && typeof crawledPage.sourceUrl === "string", - "docs_list json missing crawled URL target, stable page ID, or source URL", + "docs_list json missing crawled URL target, compatible page ID, or source URL", ); assert( repoPage && typeof repoPage.docsReadTarget === "string" && typeof repoPage.pageId === "string" && typeof repoPage.sourceUrl === "string", - "docs_list json missing repo-backed target, stable page ID, or source URL", + "docs_list json missing repo-backed target, compatible page ID, or source URL", ); assert( repoPage.docsReadTarget === repoPage.pageId, diff --git a/packages/mcp/src/tools/list-package-docs.test.ts b/packages/mcp/src/tools/list-package-docs.test.ts index 53e9e684..e5b1923c 100644 --- a/packages/mcp/src/tools/list-package-docs.test.ts +++ b/packages/mcp/src/tools/list-package-docs.test.ts @@ -29,6 +29,8 @@ describe("createListPackageDocsTool", () => { ]); expect(tool.schema.target?.description).toContain("Go accepts versions"); expect(tool.description).toContain("`docsReadTarget`"); + expect(tool.description).toContain("mutable current content"); + expect(tool.description).toContain("snapshot-addressed"); }); it("calls service.listPackageDocs with normalised params", async () => { diff --git a/packages/mcp/src/tools/list-package-docs.ts b/packages/mcp/src/tools/list-package-docs.ts index 24d662e9..da6024ba 100644 --- a/packages/mcp/src/tools/list-package-docs.ts +++ b/packages/mcp/src/tools/list-package-docs.ts @@ -48,6 +48,7 @@ const DESCRIPTION = "List package documentation targets for follow-up reads. " + "Package targets only, not standalone `site:` targets. Pass an entry's preferred " + "`docsReadTarget` to `read.target`; historical `pageId` values remain readable. " + + "Hosted HTTP(S) targets address mutable current content; repo targets are snapshot-addressed. " + "Repo-backed entries supply exact `repoUrl` / `gitRef` / `filePath` for source reads." + `\n\n${DOCS_GUARDRAIL}`; diff --git a/packages/mcp/src/tools/read.test.ts b/packages/mcp/src/tools/read.test.ts index 0a5a1f2d..2f85a32c 100644 --- a/packages/mcp/src/tools/read.test.ts +++ b/packages/mcp/src/tools/read.test.ts @@ -59,6 +59,20 @@ describe("unified read contract", () => { }, ); + it("forwards explicit documentation bounds unchanged", async () => { + const { services, tool } = setup(); + const target = "https://docs.example.test/guide#routing"; + + await tool.handler({ target, start_line: 81, end_line: 93 }); + + expect(services.readService.read).toHaveBeenCalledWith({ + target, + startLine: 81, + endLine: 93, + }); + expect(services.readService.read).toHaveBeenCalledTimes(1); + }); + it.each([ "npm:@scope/pkg@1.2.3", "maven:com.google.guava:guava@33.0.0", diff --git a/packages/mcp/src/tools/read.ts b/packages/mcp/src/tools/read.ts index 239ac7c9..24a89ef7 100644 --- a/packages/mcp/src/tools/read.ts +++ b/packages/mcp/src/tools/read.ts @@ -87,6 +87,7 @@ export const DESCRIPTION_BASE: string = "Read an indexed source file or documentation page, including a docs section. " + "Pass target and path for a file; target alone for a docs page. " + "Replaces code_read and docs_read. " + + "Hosted/crawled HTTP(S) docs targets read mutable current content; repository-doc targets address snapshots. " + "A docs URL fragment needs no bounds; either bound replaces it with a page-relative range. " + "Use emitted locators to preserve exact revisions. It does not list directories: use code_files. " + "Read focused windows from search/code_grep; follow returned continuation and error actions. " + diff --git a/packages/mcp/src/tools/search.test.ts b/packages/mcp/src/tools/search.test.ts index 33095a97..f9db932a 100644 --- a/packages/mcp/src/tools/search.test.ts +++ b/packages/mcp/src/tools/search.test.ts @@ -29,15 +29,29 @@ describe("searchTool", () => { "Discover relevant docs, code, and symbols in a known public target. Start here f", ); expect(descriptor?.description).toContain( - "Pass a `[docs page]` target unchanged", + "Hosted `[docs page]` HTTP(S) targets address mutable current content", + ); + expect(descriptor?.description).toContain( + "generated follow-ups omit search line bounds", + ); + expect(descriptor?.description).toContain( + "Repository docs remain snapshot-addressed and keep returned ranges", + ); + expect(descriptor?.description).toContain( + "Explicit `read` bounds are caller-selected ranges", ); - expect(descriptor?.description).not.toContain("fragment needs no bounds"); const readDescriptor = getMcpToolDescriptors().find( (entry) => entry.name === "read", ); expect(readDescriptor?.description).toContain( "A docs URL fragment needs no bounds", ); + expect(readDescriptor?.description).toContain( + "Hosted/crawled HTTP(S) docs targets read mutable current content", + ); + expect(readDescriptor?.description).toContain( + "repository-doc targets address snapshots", + ); }); it("keeps the common path simple and delegates continuation details", () => { diff --git a/packages/mcp/src/tools/search.ts b/packages/mcp/src/tools/search.ts index 184807c6..41c24558 100644 --- a/packages/mcp/src/tools/search.ts +++ b/packages/mcp/src/tools/search.ts @@ -131,7 +131,7 @@ const DESCRIPTION = "Target indexed dependencies and repositories, or standalone docs with `site:`. " + "Put search constraints in `query`; backend validation reports accepted values. Inspect returned warnings and `sourceStatus` when a qualifier is ignored or incompatible with a selected source. `public_only` remains structured and is ignored for docs. " + "A `search` call can return complete results directly. Only when its response supplies both a `searchRef` and a `search_status` action, follow that action with `search_status`; never repeat `search` to poll. Terminal or unrecognized statuses are not polled; follow the response's recovery guidance instead. If the response includes advisory `sourceStatus[].suggestedSiteTargets`, retry one explicitly; do not treat suggestions as aliases or retry automatically. " + - "Use hit content directly when sufficient; follow its generated `followUp` only for more context. Pass a `[docs page]` target unchanged to `read`; for source hits use the returned target, path, and line range." + + "Use hit content directly when sufficient; follow its generated `followUp` only for more context. Hosted `[docs page]` HTTP(S) targets address mutable current content: pass the returned URL or fragment unchanged, and generated follow-ups omit search line bounds. Repository docs remain snapshot-addressed and keep returned ranges. Explicit `read` bounds are caller-selected ranges. For source hits use the returned target, path, and line range." + `\n\n${SEARCH_GUARDRAIL}`; export function createSearchTool( diff --git a/skills/githits-code/SKILL.md b/skills/githits-code/SKILL.md index 32d6d484..632d208a 100644 --- a/skills/githits-code/SKILL.md +++ b/skills/githits-code/SKILL.md @@ -55,7 +55,7 @@ githits docs read - For behavioral claims, prefer source, symbols, tests, and call sites over docs prose. - Package targets inspect published artifacts and omitted versions resolve to the latest release; repository targets inspect repository trees. For source-layout questions, always pin and report the package version or Git ref. - For source work, locate symbols or matches first, then read a focused window with explicit `--lines`. -- For docs reads, use the search snippet when sufficient; otherwise run its generated `followUp`. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. A fragment needs no `--lines`; add bounds only to replace it with a page-relative range. Historical `pageId` works. Use `--json` only for required range/source metadata. +- For docs reads, use the search snippet when sufficient; otherwise run its generated `followUp`. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. Hosted/crawled HTTP(S) targets address mutable current content, so automatic follow-ups forward the exact URL or fragment without search bounds. Repository docs remain snapshot-addressed and keep returned ranges. Add `--lines` only when intentionally selecting a current page range; either bound replaces fragment selection. Historical `pageId` works. Use `--json` only for required range/source metadata. - For multi-step code/docs investigations, keep raw CLI output out of the final answer unless it is the evidence the user needs. - Reuse returned targets, paths, locators, references, and ranges; never invent them. Cite the served target and report stale/provisional evidence, truncation, and coverage limits. - Partial and capped documentation coverage are usable published evidence. Report the disclosed limit, but infer neither indexing progress nor retryability from coverage; follow only `searchRef` and the evidence notice. diff --git a/skills/githits-code/references/code-and-docs.md b/skills/githits-code/references/code-and-docs.md index e4ba4700..c9f229e2 100644 --- a/skills/githits-code/references/code-and-docs.md +++ b/skills/githits-code/references/code-and-docs.md @@ -48,7 +48,7 @@ When grep returns no matches, do not repeat it unchanged. Change or shorten the `githits docs list ` browses available documentation pages. It is not topic search. -For `githits docs read `, use the search snippet when sufficient; otherwise run its generated `followUp`. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. A fragment needs no `--lines` and returns its exact indexed section; add bounds only to replace it with a page-relative range. Historical `pageId` values remain supported. Use `--json` only for required range/source metadata. +For `githits docs read `, use the search snippet when sufficient; otherwise run its generated `followUp`. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. Hosted/crawled HTTP(S) targets address mutable current content, so automatic follow-ups forward the exact URL or fragment without search bounds. Repository docs remain snapshot-addressed and keep returned ranges. Add `--lines` only when intentionally selecting a current page range; either bound replaces fragment selection. Historical `pageId` values remain supported. Use `--json` only for required range/source metadata. For topic search, use `githits search "" --source docs --in `, then run its generated follow-up or pass the displayed text target. diff --git a/skills/githits-mcp/SKILL.md b/skills/githits-mcp/SKILL.md index a862a939..73949d34 100644 --- a/skills/githits-mcp/SKILL.md +++ b/skills/githits-mcp/SKILL.md @@ -46,6 +46,10 @@ For a package or site docs topic, use `search` with `source:"docs"`. `docs_list` browses package pages, not standalone `site:` targets. Use snippets when sufficient; otherwise follow generated `followUp` calls. Pass displayed `[docs page]` locators unchanged to `read`. +Hosted/crawled HTTP(S) docs locators address mutable current content; generated +follow-ups use the exact emitted URL or fragment without search line bounds. +Repository docs are snapshot-addressed and keep returned ranges. Add explicit +`read` bounds only when intentionally selecting a current page range. For source, locate paths or matches, then read focused lines; never probe directories with `read`. Prefer source, symbols, tests, and call sites for behavioral claims. diff --git a/src/cli.test.ts b/src/cli.test.ts index 694df003..59e88189 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -428,5 +428,15 @@ describe("CLI help surface", () => { expect(help).toMatch(/^\s{2}code\b/m); expect(help).toMatch(/^\s{2}docs\b/m); expect(help).toMatch(/^\s{2}pkg\b/m); + + const search = program.commands.find( + (command) => command.name() === "search", + ); + expect(search?.helpInformation()).toContain( + "Hosted/crawled [docs page] HTTP(S) targets address mutable current content", + ); + expect(search?.helpInformation()).toContain( + "Repository docs are snapshot-addressed", + ); }); }); diff --git a/src/commands/docs/list.ts b/src/commands/docs/list.ts index 2882c711..72676f31 100644 --- a/src/commands/docs/list.ts +++ b/src/commands/docs/list.ts @@ -113,10 +113,11 @@ function handleDocsListError(error: unknown, json: boolean): never { const DOCS_LIST_DESCRIPTION = `List package documentation pages from mixed sources. Docs are mixed by default: hosted/crawled docs and repository-backed docs -appear together. Every entry shows its stable page ID, source badge, provenance, -and a ready-to-run command using the preferred docsReadTarget. JSON retains the -target, page ID, and source URL. Repo-backed docs also carry exact file follow-up -metadata. +appear together. Every entry shows its compatible page ID, source badge, +provenance, and a ready-to-run command using the preferred docsReadTarget. +Hosted/crawled HTTP(S) targets address mutable current content; repository-backed +targets are snapshot-addressed. JSON retains the target, page ID, and source URL. +Repo-backed docs also carry exact file follow-up metadata. Package spec: :[@version]. Go accepts exact versions with or without the canonical v prefix.`; diff --git a/src/commands/docs/read.test.ts b/src/commands/docs/read.test.ts index 149d68fd..4bdb47c6 100644 --- a/src/commands/docs/read.test.ts +++ b/src/commands/docs/read.test.ts @@ -145,7 +145,7 @@ describe("docsReadAction", () => { }, ); - it("renders distinct URL target and stable ID once in verbose output", async () => { + it("renders distinct URL target and compatible ID once in verbose output", async () => { const docsReadTarget = "https://expressjs.com/en/guide/routing.html"; const writes: string[] = []; const writeSpy = spyOn(process.stdout, "write").mockImplementation((( diff --git a/src/commands/mcp-instructions.test.ts b/src/commands/mcp-instructions.test.ts index 17b22c06..7ba40b73 100644 --- a/src/commands/mcp-instructions.test.ts +++ b/src/commands/mcp-instructions.test.ts @@ -135,6 +135,12 @@ describe("buildMcpQuickStart", () => { expect(instructions).toContain( "Pass displayed `[docs page]` locators unchanged to `read`", ); + expect(instructions).toContain( + "Hosted/crawled HTTP(S) docs locators address mutable current content", + ); + expect(instructions).toContain( + "Repository docs are snapshot-addressed and keep returned ranges", + ); const reader = getMcpToolDefinitions(createTestServices()).find( (tool) => tool.name === "read", ); diff --git a/src/commands/search.test.ts b/src/commands/search.test.ts index b8b13c8d..9edabb95 100644 --- a/src/commands/search.test.ts +++ b/src/commands/search.test.ts @@ -1810,7 +1810,7 @@ describe("searchAction", () => { } }); - it("shows direct source URLs and retains page IDs for documentation pages", async () => { + it("shows the exact mutable hosted target without search coordinates", async () => { const consoleSpy = spyOn(console, "log").mockImplementation(() => {}); if (defaultUnifiedSearchOutcome.state !== "completed") { @@ -1832,8 +1832,11 @@ describe("searchAction", () => { packageName: "express", version: "5.2.1", pageId: "docs-123", + docsReadTarget: "https://hexdocs.pm/express/getting-started.html", sourceKind: "CRAWLED", sourceUrl: "https://hexdocs.pm/express/getting-started.html", + startLine: 81, + endLine: 93, }, }, ], @@ -1852,10 +1855,12 @@ describe("searchAction", () => { const output = String(consoleSpy.mock.calls[0]?.[0]); expect(output).toContain( - "[1] docs-123 [docs page] npm:express - hexdocs.pm/express/getting-started.html -\n Using Express middleware", + "[1] https://hexdocs.pm/express/getting-started.html [docs page] npm:express -\n Using Express middleware", ); expect(output).toContain("hexdocs.pm/express/getting-started.html"); - expect(output).toContain("docs-123"); + expect(output).not.toContain("docs-123"); + expect(output).not.toContain("81-93"); + expect(output).not.toContain("--lines"); expect(output).toContain("Using Express middleware"); expect(output).not.toContain("source:"); expect(output).not.toContain("npm:express@4.18.2 [docs page]"); diff --git a/src/commands/search.ts b/src/commands/search.ts index 0235f934..b374d5df 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -177,6 +177,11 @@ terminal recovery guidance without a searchRef. Use \`githits example\` for canonical cross-project examples; \`--source symbol\` here returns symbol-shaped hits. +Hosted/crawled [docs page] HTTP(S) targets address mutable current content. +Pass the displayed URL or fragment unchanged to githits read; automatic +follow-ups omit search line coordinates. Repository docs are snapshot-addressed +and keep their returned ranges. Add --lines only for an intentional page range. + The query supports implicit AND, uppercase OR, parens, unary -, "phrases", and qualifiers (kind:, category:, path:, lang:, name:, intent:, registry:, package:, version:, repo:). diff --git a/src/skills-packaging.test.ts b/src/skills-packaging.test.ts index 0ebe1550..70a3cb0a 100644 --- a/src/skills-packaging.test.ts +++ b/src/skills-packaging.test.ts @@ -404,7 +404,10 @@ describe("agent skills packaging", () => { "use the search snippet when sufficient; otherwise run its generated `followUp`", "pass the displayed `[docs page]` target unchanged", "from `docs list`, pass `docsReadTarget`", - "A fragment needs no `--lines`; add bounds only to replace it with a page-relative range", + "Hosted/crawled HTTP(S) targets address mutable current content", + "automatic follow-ups forward the exact URL or fragment without search bounds", + "Repository docs remain snapshot-addressed and keep returned ranges", + "Add `--lines` only when intentionally selecting a current page range", "Use `--json` only for required range/source metadata", ]); expect(content).toContain("githits docs read "); From c4feb8f121f437954c74524f040aa4460a3b7e1b Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 01:00:11 +0300 Subject: [PATCH 03/11] docs: clarify documentation read addresses Describe mutable hosted targets, snapshot-addressed repository docs, and full heading-subtree reads in both current and deprecated CLI help surfaces. --- src/commands/docs/read.ts | 10 ++++++---- src/commands/read.test.ts | 16 ++++++++++++---- src/commands/read.ts | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/commands/docs/read.ts b/src/commands/docs/read.ts index 546d1e88..135f8577 100644 --- a/src/commands/docs/read.ts +++ b/src/commands/docs/read.ts @@ -99,12 +99,14 @@ function handleDocsReadError(error: unknown, json: boolean): never { const DOCS_READ_DESCRIPTION = `Read a documentation page by emitted target or page ID. Pass the displayed [docs page] target from githits search or docsReadTarget from -githits docs list unchanged. Historical page IDs remain accepted. Default output is -content-only for easy piping; pass --verbose for a metadata header. Use --lines -for a bounded line range (e.g. \`--lines 10-40\`, +githits docs list unchanged. Hosted/crawled HTTP(S) targets read mutable current +content; repository docs are snapshot-addressed. Historical page IDs remain +accepted. Default output is content-only for easy piping; pass --verbose for a +metadata header. Use --lines for a bounded line range (e.g. \`--lines 10-40\`, \`--lines 10-\` for open-ended, or \`--lines -40\` for the first 40 lines) — useful when a page is too long to read whole. An HTTP(S) fragment reads its -exact indexed section; explicit --lines bounds override the fragment.`; +heading and full subtree through the next equal-or-higher heading; explicit +--lines bounds override the fragment.`; export function registerDocsReadCommand(docsCommand: Command): Command { return docsCommand diff --git a/src/commands/read.test.ts b/src/commands/read.test.ts index 4c6ddc44..446dbc43 100644 --- a/src/commands/read.test.ts +++ b/src/commands/read.test.ts @@ -29,14 +29,22 @@ describe("top-level read", () => { it("registers new syntax and marks legacy commands deprecated in help", () => { const root = new Command(); const read = registerReadCommand(root); + const readHelp = read.helpInformation(); expect(read.name()).toBe("read"); - expect(read.helpInformation()).toContain("--lines"); + expect(readHelp).toContain("--lines"); + expect(readHelp).toContain("mutable current content"); + expect(readHelp).toMatch(/repository\s+docs are snapshot-addressed/); + expect(readHelp).toContain("full subtree"); expect( registerCodeReadCommand(new Command("code")).helpInformation(), ).toContain("Deprecated: use githits read"); - expect( - registerDocsReadCommand(new Command("docs")).helpInformation(), - ).toContain("Deprecated: use githits read"); + const docsReadHelp = registerDocsReadCommand( + new Command("docs"), + ).helpInformation(); + expect(docsReadHelp).toContain("Deprecated: use githits read"); + expect(docsReadHelp).toMatch(/mutable\s+current\s+content/); + expect(docsReadHelp).toMatch(/repository\s+docs are snapshot-addressed/); + expect(docsReadHelp).toContain("full subtree"); }); it("reads docs fragments unchanged with no default range or wait", async () => { diff --git a/src/commands/read.ts b/src/commands/read.ts index 6ad034ca..bc2fd186 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -99,7 +99,7 @@ export function registerReadCommand(program: Command): Command { .command("read") .summary("Read an indexed file or documentation page") .description( - "Read a file with , or a docs page with its emitted target alone. Pass docs URL fragments unchanged to select their indexed section; --lines overrides the fragment. Default output is complete content for piping. Package and repository targets use the same compact syntax as code files.", + "Read a file with , or a docs page with its emitted target alone. Hosted/crawled HTTP(S) docs targets read mutable current content; repository docs are snapshot-addressed. Pass docs URL fragments unchanged to select the heading and its full subtree through the next equal-or-higher heading; --lines overrides the fragment with an intentional page-relative range. Default output is complete content for piping. Package and repository targets use the same compact syntax as code files.", ) .argument( "[target-or-path]", From cccbd37ad9b2c76131e4a39a1124e5eb4e29bf98 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 01:08:35 +0300 Subject: [PATCH 04/11] test: close documentation follow-up review gaps Document heading-subtree semantics across MCP and Agent Skill surfaces, remove vacuous CLI text assertions, and validate bounds-free hosted follow-ups in live MCP smoke coverage. --- packages/mcp/src/mcp/instructions.test.ts | 3 ++ packages/mcp/src/mcp/instructions.ts | 2 + packages/mcp/src/mcp/server.test.ts | 1 + packages/mcp/src/smoke-test.ts | 42 +++++++++++++++++++ packages/mcp/src/tools/read.ts | 2 +- packages/mcp/src/tools/search.test.ts | 3 ++ skills/githits-code/SKILL.md | 2 +- .../githits-code/references/code-and-docs.md | 2 +- skills/githits-mcp/SKILL.md | 2 + src/commands/search.test.ts | 4 +- src/skills-packaging.test.ts | 1 + 11 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/mcp/src/mcp/instructions.test.ts b/packages/mcp/src/mcp/instructions.test.ts index 464f75bf..473a1c0d 100644 --- a/packages/mcp/src/mcp/instructions.test.ts +++ b/packages/mcp/src/mcp/instructions.test.ts @@ -35,6 +35,9 @@ describe("buildLocalMcpQuickStart", () => { expect(quickStart).toContain( "exact emitted URL or fragment without search line bounds", ); + expect(quickStart).toContain( + "fragment returns its heading and full subtree through the next", + ); expect(quickStart).toContain( "Repository docs are snapshot-addressed and keep returned ranges", ); diff --git a/packages/mcp/src/mcp/instructions.ts b/packages/mcp/src/mcp/instructions.ts index 44c34df7..5f4ffa3c 100644 --- a/packages/mcp/src/mcp/instructions.ts +++ b/packages/mcp/src/mcp/instructions.ts @@ -38,6 +38,8 @@ Use snippets when sufficient; otherwise follow generated \`followUp\` calls. Pass displayed \`[docs page]\` locators unchanged to \`read\`. Hosted/crawled HTTP(S) docs locators address mutable current content; generated follow-ups use the exact emitted URL or fragment without search line bounds. +An HTTP(S) docs fragment returns its heading and full subtree through the next +equal-or-higher heading. Repository docs are snapshot-addressed and keep returned ranges. Add explicit \`read\` bounds only when intentionally selecting a current page range. For source, locate paths or matches, then read focused lines; never probe diff --git a/packages/mcp/src/mcp/server.test.ts b/packages/mcp/src/mcp/server.test.ts index 6154ecd6..9d69afc5 100644 --- a/packages/mcp/src/mcp/server.test.ts +++ b/packages/mcp/src/mcp/server.test.ts @@ -111,6 +111,7 @@ const DESCRIPTION_ROUTING: Record< "Hosted/crawled HTTP(S) docs targets read mutable current content", "repository-doc targets address snapshots", "A docs URL fragment needs no bounds", + "full subtree through the next equal-or-higher heading", "either bound replaces it with a page-relative range", "exact revisions", "does not list directories", diff --git a/packages/mcp/src/smoke-test.ts b/packages/mcp/src/smoke-test.ts index 8727461a..4dca94f1 100644 --- a/packages/mcp/src/smoke-test.ts +++ b/packages/mcp/src/smoke-test.ts @@ -1349,6 +1349,48 @@ async function runLiveSmoke(caller: McpSmokeCaller): Promise { "search json", ); assertRecord(searchJson, "search json"); + + const docsSearchJson = assertJsonResult( + await callTool(caller, "search", { + target: SMOKE_PACKAGE_TARGET, + query: "route handlers", + source: "docs", + limit: 3, + format: "json", + }), + "documentation search json", + ); + assertRecord(docsSearchJson, "documentation search json"); + assert( + Array.isArray(docsSearchJson.results), + "documentation search json missing results", + ); + const hostedDocumentationHits = docsSearchJson.results.filter((entry) => { + if (typeof entry !== "object" || entry === null) return false; + const hit = entry as Record; + if (hit.type !== "documentation_page") return false; + const locator = hit.locator; + return ( + typeof locator === "object" && + locator !== null && + typeof (locator as Record).docsReadTarget === "string" && + /^https?:\/\//i.test( + (locator as Record).docsReadTarget as string, + ) + ); + }) as Array>; + assert( + hostedDocumentationHits.length > 0, + "documentation search json missing hosted documentation_page evidence", + ); + for (const hit of hostedDocumentationHits) { + assert( + typeof hit.followUp === "string" && + !/\b(?:start_line|end_line)=/.test(hit.followUp), + "hosted documentation follow-up replayed search line bounds", + ); + } + const searchRef = typeof searchJson.searchRef === "string" ? searchJson.searchRef : undefined; if (searchRef) { diff --git a/packages/mcp/src/tools/read.ts b/packages/mcp/src/tools/read.ts index 24a89ef7..8b804ceb 100644 --- a/packages/mcp/src/tools/read.ts +++ b/packages/mcp/src/tools/read.ts @@ -88,7 +88,7 @@ export const DESCRIPTION_BASE: string = "Pass target and path for a file; target alone for a docs page. " + "Replaces code_read and docs_read. " + "Hosted/crawled HTTP(S) docs targets read mutable current content; repository-doc targets address snapshots. " + - "A docs URL fragment needs no bounds; either bound replaces it with a page-relative range. " + + "A docs URL fragment needs no bounds and returns its heading with the full subtree through the next equal-or-higher heading; either bound replaces it with a page-relative range. " + "Use emitted locators to preserve exact revisions. It does not list directories: use code_files. " + "Read focused windows from search/code_grep; follow returned continuation and error actions. " + "On INDEXING retry the same target/path with wait_timeout_ms; no content is available yet."; diff --git a/packages/mcp/src/tools/search.test.ts b/packages/mcp/src/tools/search.test.ts index f9db932a..c1a12bad 100644 --- a/packages/mcp/src/tools/search.test.ts +++ b/packages/mcp/src/tools/search.test.ts @@ -46,6 +46,9 @@ describe("searchTool", () => { expect(readDescriptor?.description).toContain( "A docs URL fragment needs no bounds", ); + expect(readDescriptor?.description).toContain( + "full subtree through the next equal-or-higher heading", + ); expect(readDescriptor?.description).toContain( "Hosted/crawled HTTP(S) docs targets read mutable current content", ); diff --git a/skills/githits-code/SKILL.md b/skills/githits-code/SKILL.md index 632d208a..c00c46e2 100644 --- a/skills/githits-code/SKILL.md +++ b/skills/githits-code/SKILL.md @@ -55,7 +55,7 @@ githits docs read - For behavioral claims, prefer source, symbols, tests, and call sites over docs prose. - Package targets inspect published artifacts and omitted versions resolve to the latest release; repository targets inspect repository trees. For source-layout questions, always pin and report the package version or Git ref. - For source work, locate symbols or matches first, then read a focused window with explicit `--lines`. -- For docs reads, use the search snippet when sufficient; otherwise run its generated `followUp`. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. Hosted/crawled HTTP(S) targets address mutable current content, so automatic follow-ups forward the exact URL or fragment without search bounds. Repository docs remain snapshot-addressed and keep returned ranges. Add `--lines` only when intentionally selecting a current page range; either bound replaces fragment selection. Historical `pageId` works. Use `--json` only for required range/source metadata. +- For docs reads, use the search snippet when sufficient; otherwise run its generated `followUp`. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. Hosted/crawled HTTP(S) targets address mutable current content, so automatic follow-ups forward the exact URL or fragment without search bounds. A fragment returns its heading and full subtree through the next equal-or-higher heading. Repository docs remain snapshot-addressed and keep returned ranges. Add `--lines` only when intentionally selecting a current page range; either bound replaces fragment selection. Historical `pageId` works. Use `--json` only for required range/source metadata. - For multi-step code/docs investigations, keep raw CLI output out of the final answer unless it is the evidence the user needs. - Reuse returned targets, paths, locators, references, and ranges; never invent them. Cite the served target and report stale/provisional evidence, truncation, and coverage limits. - Partial and capped documentation coverage are usable published evidence. Report the disclosed limit, but infer neither indexing progress nor retryability from coverage; follow only `searchRef` and the evidence notice. diff --git a/skills/githits-code/references/code-and-docs.md b/skills/githits-code/references/code-and-docs.md index c9f229e2..1be99e0b 100644 --- a/skills/githits-code/references/code-and-docs.md +++ b/skills/githits-code/references/code-and-docs.md @@ -48,7 +48,7 @@ When grep returns no matches, do not repeat it unchanged. Change or shorten the `githits docs list ` browses available documentation pages. It is not topic search. -For `githits docs read `, use the search snippet when sufficient; otherwise run its generated `followUp`. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. Hosted/crawled HTTP(S) targets address mutable current content, so automatic follow-ups forward the exact URL or fragment without search bounds. Repository docs remain snapshot-addressed and keep returned ranges. Add `--lines` only when intentionally selecting a current page range; either bound replaces fragment selection. Historical `pageId` values remain supported. Use `--json` only for required range/source metadata. +For `githits docs read `, use the search snippet when sufficient; otherwise run its generated `followUp`. From text, pass the displayed `[docs page]` target unchanged; from `docs list`, pass `docsReadTarget`. Hosted/crawled HTTP(S) targets address mutable current content, so automatic follow-ups forward the exact URL or fragment without search bounds. A fragment returns its heading and full subtree through the next equal-or-higher heading. Repository docs remain snapshot-addressed and keep returned ranges. Add `--lines` only when intentionally selecting a current page range; either bound replaces fragment selection. Historical `pageId` values remain supported. Use `--json` only for required range/source metadata. For topic search, use `githits search "" --source docs --in `, then run its generated follow-up or pass the displayed text target. diff --git a/skills/githits-mcp/SKILL.md b/skills/githits-mcp/SKILL.md index 73949d34..a7524bf4 100644 --- a/skills/githits-mcp/SKILL.md +++ b/skills/githits-mcp/SKILL.md @@ -48,6 +48,8 @@ Use snippets when sufficient; otherwise follow generated `followUp` calls. Pass displayed `[docs page]` locators unchanged to `read`. Hosted/crawled HTTP(S) docs locators address mutable current content; generated follow-ups use the exact emitted URL or fragment without search line bounds. +An HTTP(S) docs fragment returns its heading and full subtree through the next +equal-or-higher heading. Repository docs are snapshot-addressed and keep returned ranges. Add explicit `read` bounds only when intentionally selecting a current page range. For source, locate paths or matches, then read focused lines; never probe diff --git a/src/commands/search.test.ts b/src/commands/search.test.ts index 9edabb95..72831e18 100644 --- a/src/commands/search.test.ts +++ b/src/commands/search.test.ts @@ -1810,7 +1810,7 @@ describe("searchAction", () => { } }); - it("shows the exact mutable hosted target without search coordinates", async () => { + it("shows the exact mutable hosted target without its internal page ID", async () => { const consoleSpy = spyOn(console, "log").mockImplementation(() => {}); if (defaultUnifiedSearchOutcome.state !== "completed") { @@ -1859,8 +1859,6 @@ describe("searchAction", () => { ); expect(output).toContain("hexdocs.pm/express/getting-started.html"); expect(output).not.toContain("docs-123"); - expect(output).not.toContain("81-93"); - expect(output).not.toContain("--lines"); expect(output).toContain("Using Express middleware"); expect(output).not.toContain("source:"); expect(output).not.toContain("npm:express@4.18.2 [docs page]"); diff --git a/src/skills-packaging.test.ts b/src/skills-packaging.test.ts index 70a3cb0a..c17f0470 100644 --- a/src/skills-packaging.test.ts +++ b/src/skills-packaging.test.ts @@ -406,6 +406,7 @@ describe("agent skills packaging", () => { "from `docs list`, pass `docsReadTarget`", "Hosted/crawled HTTP(S) targets address mutable current content", "automatic follow-ups forward the exact URL or fragment without search bounds", + "fragment returns its heading and full subtree through the next equal-or-higher heading", "Repository docs remain snapshot-addressed and keep returned ranges", "Add `--lines` only when intentionally selecting a current page range", "Use `--json` only for required range/source metadata", From 4425df7436e51ed1daba8f0a32988a8e68dc1fb6 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 01:11:23 +0300 Subject: [PATCH 05/11] test: cover hosted follow-up smoke assertion Teach the shared smoke corpus to return hosted documentation evidence and prove the live assertion rejects generated follow-ups that replay search bounds. --- packages/mcp/src/smoke-test.test.ts | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/mcp/src/smoke-test.test.ts b/packages/mcp/src/smoke-test.test.ts index dffefd6d..ab368360 100644 --- a/packages/mcp/src/smoke-test.test.ts +++ b/packages/mcp/src/smoke-test.test.ts @@ -321,6 +321,32 @@ describe("runMcpSmoke", () => { }); }); + it("rejects hosted documentation follow-ups that replay search bounds", async () => { + const caller = createCaller(async (name, args) => { + if (name === "search" && args.source === "docs") { + return jsonResult({ + completed: true, + results: [ + { + type: "documentation_page", + locator: { + docsReadTarget: SMOKE_CRAWLED_DOC_TARGET, + startLine: 81, + endLine: 93, + }, + followUp: `read target=${JSON.stringify(SMOKE_CRAWLED_DOC_TARGET)} start_line=81 end_line=93`, + }, + ], + }); + } + return smokeResponse(name, args); + }); + + await expect(runMcpSmoke(caller)).rejects.toThrow( + "hosted documentation follow-up replayed search line bounds", + ); + }); + it("rejects arbitrary snippets beneath path-only search hits", async () => { const caller = createCaller(async (name, args) => { if (name === "search" && args.format !== "json") { @@ -1098,6 +1124,23 @@ function smokeJsonResponse( ], }); } + if (args.source === "docs") { + return jsonResult({ + completed: true, + results: [ + { + type: "documentation_page", + locator: { + docsReadTarget: SMOKE_CRAWLED_DOC_TARGET, + sourceUrl: SMOKE_CRAWLED_DOC_TARGET, + startLine: 81, + endLine: 93, + }, + followUp: `read target=${JSON.stringify(SMOKE_CRAWLED_DOC_TARGET)}`, + }, + ], + }); + } return jsonResult({ completed: false, searchRef: "smoke-ref", From a42da6cafb3481f15d81482c13266ef29e6af49e Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 01:14:01 +0300 Subject: [PATCH 06/11] test: stabilize hosted documentation smoke Use the explicit Express documentation site and follow one documented search continuation before requiring hosted evidence, with coverage for the in-progress response shape. --- packages/mcp/src/smoke-test.test.ts | 28 +++++++++++++++++++++++++ packages/mcp/src/smoke-test.ts | 32 +++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/packages/mcp/src/smoke-test.test.ts b/packages/mcp/src/smoke-test.test.ts index ab368360..92f1ace3 100644 --- a/packages/mcp/src/smoke-test.test.ts +++ b/packages/mcp/src/smoke-test.test.ts @@ -347,6 +347,34 @@ describe("runMcpSmoke", () => { ); }); + it("continues an in-progress hosted documentation smoke search once", async () => { + let docsStatusCalls = 0; + const caller = createCaller(async (name, args) => { + if (name === "search" && args.source === "docs") { + return jsonResult({ completed: false, searchRef: "docs-smoke-ref" }); + } + if (name === "search_status" && args.search_ref === "docs-smoke-ref") { + docsStatusCalls += 1; + return jsonResult({ + completed: true, + result: { + results: [ + { + type: "documentation_page", + locator: { docsReadTarget: SMOKE_CRAWLED_DOC_TARGET }, + followUp: `read target=${JSON.stringify(SMOKE_CRAWLED_DOC_TARGET)}`, + }, + ], + }, + }); + } + return smokeResponse(name, args); + }); + + await expect(runMcpSmoke(caller)).resolves.toBeUndefined(); + expect(docsStatusCalls).toBe(1); + }); + it("rejects arbitrary snippets beneath path-only search hits", async () => { const caller = createCaller(async (name, args) => { if (name === "search" && args.format !== "json") { diff --git a/packages/mcp/src/smoke-test.ts b/packages/mcp/src/smoke-test.ts index 4dca94f1..c94e4b6f 100644 --- a/packages/mcp/src/smoke-test.ts +++ b/packages/mcp/src/smoke-test.ts @@ -1350,22 +1350,42 @@ async function runLiveSmoke(caller: McpSmokeCaller): Promise { ); assertRecord(searchJson, "search json"); - const docsSearchJson = assertJsonResult( + let docsSearchJson = assertJsonResult( await callTool(caller, "search", { - target: SMOKE_PACKAGE_TARGET, - query: "route handlers", + target: "site:expressjs.com", + query: "routing", source: "docs", - limit: 3, + limit: 10, + wait_timeout_ms: 60_000, format: "json", }), "documentation search json", ); assertRecord(docsSearchJson, "documentation search json"); + if ( + !Array.isArray(docsSearchJson.results) && + typeof docsSearchJson.searchRef === "string" + ) { + docsSearchJson = assertJsonResult( + await callTool(caller, "search_status", { + search_ref: docsSearchJson.searchRef, + wait_timeout_ms: 60_000, + format: "json", + }), + "documentation search status json", + ); + assertRecord(docsSearchJson, "documentation search status json"); + } + const docsSearchEvidence = + typeof docsSearchJson.result === "object" && docsSearchJson.result !== null + ? docsSearchJson.result + : docsSearchJson; + assertRecord(docsSearchEvidence, "documentation search evidence"); assert( - Array.isArray(docsSearchJson.results), + Array.isArray(docsSearchEvidence.results), "documentation search json missing results", ); - const hostedDocumentationHits = docsSearchJson.results.filter((entry) => { + const hostedDocumentationHits = docsSearchEvidence.results.filter((entry) => { if (typeof entry !== "object" || entry === null) return false; const hit = entry as Record; if (hit.type !== "documentation_page") return false; From c277fa20c9e7a497530a5ea8da98367e94641dc6 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 01:16:46 +0300 Subject: [PATCH 07/11] test: follow real incomplete docs searches Key the hosted-doc smoke continuation on the completion flag and model the required empty results array in its in-progress fixture. --- packages/mcp/src/smoke-test.test.ts | 6 +++++- packages/mcp/src/smoke-test.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/mcp/src/smoke-test.test.ts b/packages/mcp/src/smoke-test.test.ts index 92f1ace3..998ae6e2 100644 --- a/packages/mcp/src/smoke-test.test.ts +++ b/packages/mcp/src/smoke-test.test.ts @@ -351,7 +351,11 @@ describe("runMcpSmoke", () => { let docsStatusCalls = 0; const caller = createCaller(async (name, args) => { if (name === "search" && args.source === "docs") { - return jsonResult({ completed: false, searchRef: "docs-smoke-ref" }); + return jsonResult({ + completed: false, + results: [], + searchRef: "docs-smoke-ref", + }); } if (name === "search_status" && args.search_ref === "docs-smoke-ref") { docsStatusCalls += 1; diff --git a/packages/mcp/src/smoke-test.ts b/packages/mcp/src/smoke-test.ts index c94e4b6f..0c0e2ae1 100644 --- a/packages/mcp/src/smoke-test.ts +++ b/packages/mcp/src/smoke-test.ts @@ -1363,7 +1363,7 @@ async function runLiveSmoke(caller: McpSmokeCaller): Promise { ); assertRecord(docsSearchJson, "documentation search json"); if ( - !Array.isArray(docsSearchJson.results) && + docsSearchJson.completed !== true && typeof docsSearchJson.searchRef === "string" ) { docsSearchJson = assertJsonResult( From 03b5f35d5f2cf74a07b74f8fd7f20b86152bcc49 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 01:20:34 +0300 Subject: [PATCH 08/11] docs: record hosted follow-up verification Capture the completed implementation, validation, smoke limitations, and clean review evidence before draft PR delivery. --- .../stable-hosted-documentation-followups.md | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/docs/plans/stable-hosted-documentation-followups.md b/docs/plans/stable-hosted-documentation-followups.md index 0e8a0a5b..841b50bd 100644 --- a/docs/plans/stable-hosted-documentation-followups.md +++ b/docs/plans/stable-hosted-documentation-followups.md @@ -2,8 +2,8 @@ ## Status -- Overall: **READY** -- Phase 1: **READY** +- Overall: **IMPLEMENTED — draft PR pending** +- Phase 1: **COMPLETE** ## Overall objective @@ -167,6 +167,50 @@ settles hosted-versus-repository semantics. Phase 1 step 1 already requires rewriting that assertion bounds-free while retaining its quoting coverage. +### Implementation and verification evidence + +- The implementation remains at `documentationReadLocator()`. Hosted/crawled + HTTP(S) `documentation_page` targets now forward the exact page URL or exact + emitted fragment without observed bounds. `repository_doc` targets retain + snapshot ranges, structured search locators retain their evidence coordinates, + and explicit CLI/MCP documentation-read bounds still cross the public boundary. +- Stable guidance now describes hosted documentation as mutable current content, + distinguishes repository documentation as snapshot-addressed, and documents + heading-subtree reads through the next equal-or-higher heading. Canonical MCP + guidance and its packaged skill copy remain in exact parity. Plugin generation + produced no unexpected generated changes, and `plugins:check` validates all 10 + assets. +- Regression coverage includes page-only hosted URLs, exact fragments, mixed-case + schemes, repository ranges, retained JSON evidence, CLI output, direct MCP read + bounds, publication-replacement semantics, and the live smoke assertion. The + final full suite passes 4,826 tests across 208 files with zero failures. +- Final static/product checks pass: `bun run typecheck`, `bun run format:check`, + `bun run lint`, `bun run build`, `bun run plugins:check`, + `bun run validate:packages`, `bun run smoke:cli:built`, and + `bun run smoke:mcp:built`. Lint reports only eight pre-existing + `noNonNullAssertion` warnings in + `packages/mcp/src/shared/repository-target.ts` and exits successfully. +- Source CLI unauthenticated smoke and source MCP registration smoke also pass. + The authenticated live CLI suite reached the deployed service but stalled for + more than five minutes on its first package request; a narrower hosted-doc + search likewise made no response progress for more than 90 seconds. Both were + stopped without changing data. Therefore no successful live search-to-read + result is claimed; the production-style built smokes and the smoke harness's + mocked search-to-follow-up regressions are the available local evidence. +- Targeted agent evaluation was attempted. Claude could not start because its + isolated harness was not logged in. Codex invoked `search` and `docs_list` but + the run timed out after 302 seconds while the service calls remained in + progress; its tool-call and metrics artifacts were inspected, and it produced + no final or isolation artifact. No qualitative answer-quality claim is made. +- Internal pre-flight found one missing CLI read-help clarification; it was fixed. + The retained Opus reviewer completed three rounds. Valid findings covering + heading guidance, discriminating CLI assertions, live smoke coverage, and the + real incomplete-search envelope were fixed and rechecked. The final report is + **CLEAN** at commit `c277fa2`; the rejected opaque-`pageId` expansion was not + re-raised because it is outside the verified HTTP(S) contract. +- Stable commits through review are `267f459`, `c4feb8f`, `cccbd37`, `4425df7`, + `a42da6c`, and `c277fa2`, following the committed plan `dc553ae`. + ### Overall acceptance criteria - Page-only hosted search hits emit bounds-free CLI and MCP follow-ups while @@ -190,13 +234,13 @@ settles hosted-versus-repository semantics. ## Phase map 1. **Phase 1 — hosted current-content follow-ups omit search bounds while every - snapshot/manual range contract remains intact: READY.** + snapshot/manual range contract remains intact: COMPLETE.** ## Phase 1 detailed plan ### Status -**READY** +**COMPLETE** ### Expected outcome From 421a5d1978ba35dfa7a3d6598ae991648b9cf144 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 01:21:36 +0300 Subject: [PATCH 09/11] docs: record draft pull request Close the implementation plan with the delivered draft PR while preserving the explicit merge and release boundary. --- docs/plans/stable-hosted-documentation-followups.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/plans/stable-hosted-documentation-followups.md b/docs/plans/stable-hosted-documentation-followups.md index 841b50bd..bff4b173 100644 --- a/docs/plans/stable-hosted-documentation-followups.md +++ b/docs/plans/stable-hosted-documentation-followups.md @@ -2,7 +2,7 @@ ## Status -- Overall: **IMPLEMENTED — draft PR pending** +- Overall: **COMPLETE** - Phase 1: **COMPLETE** ## Overall objective @@ -210,6 +210,10 @@ settles hosted-versus-repository semantics. re-raised because it is outside the verified HTTP(S) contract. - Stable commits through review are `267f459`, `c4feb8f`, `cccbd37`, `4425df7`, `a42da6c`, and `c277fa2`, following the committed plan `dc553ae`. +- Delivery is complete through draft PR + [#405](https://github.com/githits-com/githits-cli/pull/405), labeled `fix` and + `documentation`. Merge, release, publication, and deployment remain outside + this plan and were not performed. ### Overall acceptance criteria From 89c06cc93ef372dc8cbf62643aed6cad5cc6ba28 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 07:29:51 +0300 Subject: [PATCH 10/11] docs: record live documentation verification Replace the earlier keychain-blocked smoke limitation with the successful real-site search-to-read evidence and retain the unrelated full-suite limitation. --- .../stable-hosted-documentation-followups.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/plans/stable-hosted-documentation-followups.md b/docs/plans/stable-hosted-documentation-followups.md index bff4b173..b7eddd94 100644 --- a/docs/plans/stable-hosted-documentation-followups.md +++ b/docs/plans/stable-hosted-documentation-followups.md @@ -191,12 +191,17 @@ settles hosted-versus-repository semantics. `noNonNullAssertion` warnings in `packages/mcp/src/shared/repository-target.ts` and exits successfully. - Source CLI unauthenticated smoke and source MCP registration smoke also pass. - The authenticated live CLI suite reached the deployed service but stalled for - more than five minutes on its first package request; a narrower hosted-doc - search likewise made no response progress for more than 90 seconds. Both were - stopped without changing data. Therefore no successful live search-to-read - result is claimed; the production-style built smokes and the smoke harness's - mocked search-to-follow-up regressions are the available local evidence. + After resolving a local keychain prompt, a live source search for `routing` + against `site:expressjs.com` returned hosted `documentation_page` hits whose + locators retained observed ranges while every generated `followUp` omitted + `start_line` and `end_line`. Human-readable CLI output displayed the exact + hosted target without coordinates, and an unbounded live read of the returned + `https://expressjs.com/en/5x/guide/routing/` target returned the current page + from line 1 through its reported total. The 16,278-line aggregate target also + completed through both source and built CLIs and produced valid JSON. The full + authenticated MCP smoke progressed through live package calls but stopped on + an unrelated `pkg_upgrade_review` success assertion before reaching its docs + block; the focused live search-to-read path itself is verified. - Targeted agent evaluation was attempted. Claude could not start because its isolated harness was not logged in. Codex invoked `search` and `docs_list` but the run timed out after 302 seconds while the service calls remained in From 7f084ee61968b277922c90a0cb7dd67d8a80fa4e Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 18 Sep 2026 10:19:55 +0300 Subject: [PATCH 11/11] docs: record live fragment verification Capture the refreshed Express corpus evidence showing exact fragment forwarding and bounds-free heading-subtree reads. --- docs/plans/stable-hosted-documentation-followups.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/plans/stable-hosted-documentation-followups.md b/docs/plans/stable-hosted-documentation-followups.md index b7eddd94..d16543f1 100644 --- a/docs/plans/stable-hosted-documentation-followups.md +++ b/docs/plans/stable-hosted-documentation-followups.md @@ -199,6 +199,12 @@ settles hosted-versus-repository semantics. `https://expressjs.com/en/5x/guide/routing/` target returned the current page from line 1 through its reported total. The 16,278-line aggregate target also completed through both source and built CLIs and produced valid JSON. The full + Express corpus refresh was then verified with a live `route methods` search: + the hit retained evidence lines 56–66, emitted + `https://expressjs.com/en/5x/guide/routing/#route-methods`, and generated the + exact bounds-free follow-up. Executing that emitted fragment returned lines + 56–112 beginning at `## Route methods`, confirming the full heading subtree + rather than the narrower search coordinates. The full authenticated MCP smoke progressed through live package calls but stopped on an unrelated `pkg_upgrade_review` success assertion before reaching its docs block; the focused live search-to-read path itself is verified.