diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e84432..f80eb565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to Context are documented here. +## 0.7.43 - 2026-10-01 + +- Upgrade repository evidence enrichment to ABI 2, attaching compact references to individual read items without replacing original content. +- Preserve batch item identity for repeated paths and ranges; report evidence diagnostics without treating registered references as verified source reads. +- Adapt remote query guidance to bounded default reads, file-only search and per-file match limits while preserving local retrieval paths. +- Validate the bundled plugin ABI and retain known official artifact digests for safe workspace upgrades. + ## 0.7.42 - 2026-09-30 - Read approved knowledge and recorded code evidence remotely without requiring a checkout; preserve local and offline retrieval paths. diff --git a/bun.lock b/bun.lock index b54ecf17..269743c4 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,7 @@ }, "packages/context": { "name": "@c4a/context", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "yaml": "^2.5.1", "zod": "^3.23.8", @@ -26,7 +26,7 @@ }, "packages/context-cli": { "name": "@c4a/context-cli", - "version": "0.7.42", + "version": "0.7.43", "bin": { "context": "dist/cli.js", }, @@ -73,7 +73,7 @@ }, "packages/core": { "name": "@c4a/core", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "picomatch": "^4.0.4", "yaml": "^2.4.5", @@ -85,7 +85,7 @@ }, "packages/dev-cli": { "name": "@c4a/dev-cli", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/context": "workspace:*", "@c4a/core": "workspace:*", @@ -97,7 +97,7 @@ }, "packages/extract": { "name": "@c4a/extract", - "version": "0.7.42", + "version": "0.7.43", "bin": { "c4a-extract-code": "./dist/bin/c4a-extract-code.js", }, @@ -112,7 +112,7 @@ }, "packages/extract-contract": { "name": "@c4a/extract-contract", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "graphql": "^16.14.2", @@ -122,7 +122,7 @@ }, "packages/extract-go": { "name": "@c4a/extract-go", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "@c4a/extract": "workspace:*", @@ -132,7 +132,7 @@ }, "packages/extract-mdx": { "name": "@c4a/extract-mdx", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "remark-mdx": "^3.1.1", @@ -144,7 +144,7 @@ }, "packages/extract-proto": { "name": "@c4a/extract-proto", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "zod": "^3.23.8", @@ -152,7 +152,7 @@ }, "packages/extract-rush": { "name": "@c4a/extract-rush", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "typescript": "^5.5.4", @@ -162,7 +162,7 @@ }, "packages/extract-sql": { "name": "@c4a/extract-sql", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "node-sql-parser": "^5.4.0", @@ -171,7 +171,7 @@ }, "packages/extract-style": { "name": "@c4a/extract-style", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "postcss": "^8.5.26", @@ -183,7 +183,7 @@ }, "packages/extract-thrift": { "name": "@c4a/extract-thrift", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "zod": "^3.23.8", @@ -191,7 +191,7 @@ }, "packages/extract-ts": { "name": "@c4a/extract-ts", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "@c4a/core": "workspace:*", "@c4a/extract": "workspace:*", @@ -202,7 +202,7 @@ }, "packages/tui": { "name": "@c4a/tui", - "version": "0.7.42", + "version": "0.7.43", "dependencies": { "ink": "^5.0.0", "react": "^18.3.1", diff --git a/package.json b/package.json index f768d4af..e62f92fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "context", - "version": "0.7.42", + "version": "0.7.43", "packageManager": "bun@1.3.9", "repository": { "type": "git", diff --git a/packages/context-cli/context-workflow/provider.yaml b/packages/context-cli/context-workflow/provider.yaml index 6632488b..2715e0af 100644 --- a/packages/context-cli/context-workflow/provider.yaml +++ b/packages/context-cli/context-workflow/provider.yaml @@ -1,6 +1,6 @@ schema: agent-graph.provider.v1 id: c4a/context -version: 0.7.42 +version: 0.7.43 name: Context workflow description: Internal work contract for Context knowledge workspaces. graphs: diff --git a/packages/context-cli/package.json b/packages/context-cli/package.json index 59256d08..5f88f3c9 100644 --- a/packages/context-cli/package.json +++ b/packages/context-cli/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/context-cli", "description": "Local runtime and Agent integration for traceable knowledge production", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/context-cli/scripts/build-evidence.ts b/packages/context-cli/scripts/build-evidence.ts index bf6beb37..1ccf6d94 100644 --- a/packages/context-cli/scripts/build-evidence.ts +++ b/packages/context-cli/scripts/build-evidence.ts @@ -14,7 +14,8 @@ execFileSync("cargo", ["build", "--locked", "--release", "--target", "wasm32-unk const bytes = await readFile(resolve(crate, "target/wasm32-unknown-unknown/release/context_evidence_wasm.wasm")); const module = new WebAssembly.Module(bytes); const metadata = WebAssembly.Module.customSections(module, "sourcegraph.plugin.v1"); -if (metadata.length !== 1 || JSON.parse(new TextDecoder().decode(metadata[0])).name !== "context-evidence") { +const declaration = metadata.length === 1 ? JSON.parse(new TextDecoder().decode(metadata[0])) : undefined; +if (declaration?.name !== "context-evidence" || declaration.abi_version !== 2) { throw new Error("Missing evidence plugin metadata"); } const imports = WebAssembly.Module.imports(module); diff --git a/packages/context-cli/scripts/evidence-wasm.test.mjs b/packages/context-cli/scripts/evidence-wasm.test.mjs index a69e8b40..c5f97ef6 100644 --- a/packages/context-cli/scripts/evidence-wasm.test.mjs +++ b/packages/context-cli/scripts/evidence-wasm.test.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; +import { parse } from 'yaml'; const bytes = readFileSync(new URL('../dist/evidence/context-evidence.sourcegraph.wasm', import.meta.url)); const module = new WebAssembly.Module(bytes); @@ -15,8 +16,11 @@ function fixtures(refs) { return { 'knowledge/example.md': article, 'knowledge/structure.yaml': JSON.stringify(structure(refs)), 'sources/repo/index.yaml': JSON.stringify(repo) }; } function input(start = 2, end = 2, args = {}) { - return { operation: 'read', args, files: [{ path: 'knowledge/example.md', start_line: start, end_line: end, content: article.split('\n').slice(start - 1, end).join('\n'), truncated: false }] }; + return { abi_version: 2, operation: 'read', args, files: [{ item_id: 'read-0', path: 'knowledge/example.md', start_line: start, end_line: end, content: article.split('\n').slice(start - 1, end).join('\n'), truncated: false }] }; } +const body = result => result.attachments.map(a => parse(a.text)); +const refs = result => body(result).flatMap(b => b.references ?? []); +const issues = result => body(result).flatMap(b => b.diagnostics ?? []); function host(files) { let instance; let error = ''; @@ -56,7 +60,7 @@ test('real artifact metadata and imports', () => { const sections = WebAssembly.Module.customSections(module, 'sourcegraph.plugin.v1'); assert.equal(sections.length, 1); const metadata = JSON.parse(decoder.decode(sections[0])); - assert.equal(metadata.abi_version, 1); + assert.equal(metadata.abi_version, 2); assert.equal(metadata.name, 'context-evidence'); assert.equal(metadata.default_enabled, true); assert.deepEqual(metadata.operations, ['read', 'read_many']); @@ -67,26 +71,28 @@ test('shared section fixtures agree with the Context marker contract', () => { const cases = JSON.parse(readFileSync(new URL('../../context-evidence-wasm/fixtures/sections.json', import.meta.url), 'utf8')); for (const item of cases) { const files = fixtures(); files['knowledge/example.md'] = item.text; - const map = structure(); map.articles[0].sections = (item.ids ?? []).map(id => ({ id, references: [] })); + const map = structure(); map.articles[0].sections = (item.ids ?? []).map(id => ({ id, references: [{...reference(), locator:{path:`src/${id}.ts`,start_line:1,end_line:1}}] })); files['knowledge/structure.yaml'] = JSON.stringify(map); const result = host(files).run(input(1, item.text.split('\n').length)); - if (item.error) assert.ok(result.issues); else assert.deepEqual(result.sections.map(s => s.section_id), item.ids); + if (item.error) assert.ok(issues(result).length); else assert.equal(refs(result).length, item.ids.length); } }); test('joins exact returned section with registered source, not knowledge revision', () => { const h = host(fixtures()); const result = h.run(input()); - assert.deepEqual(result, { sections: [{ path: 'knowledge/example.md', section_id: 'first', references: [{ url: `https://example.org/team/source/blob/${'b'.repeat(40)}/module/src/example.ts#L3-L9` }] }] }); + assert.deepEqual(Object.keys(result), ['attachments']); + assert.equal(result.attachments[0].item_id, 'read-0'); + assert.deepEqual(body(result), [{references:[`https://example.org/team/source/blob/${'b'.repeat(40)}/module/src/example.ts#L3-L9`]}]); assert.equal(h.calls.length, 3); assert.deepEqual(h.run(input()), result); assert.equal(h.calls.length, 3, 'warm instance does not reread immutable metadata'); - assert.equal(h.run(input(6, 6)).sections[0].section_id, 'second'); + assert.deepEqual(h.run(input(6, 6)), {attachments:[]}); }); test('optional digest and monorepo root', () => { const files = Object.fromEntries(Object.entries(fixtures()).map(([p, v]) => [`docs/${p}`, v])); const request = input(2, 2, { workspace_root: 'docs', include_digest: true }); request.files[0].path = 'docs/knowledge/example.md'; - assert.equal(host(files).run(request).sections[0].references[0].content_digest, reference().content_digest); + assert.equal(refs(host(files).run(request))[0].content_digest, reference().content_digest); }); test('source URLs encode file segments and normalize credential-free Git transports', () => { for (const remote of ['https://github.com/team/source.git', 'git@github.com:team/source.git', 'ssh://git@github.com/team/source.git']) { @@ -94,14 +100,14 @@ test('source URLs encode file segments and normalize credential-free Git transpo const files = fixtures([ref]); const registry = structuredClone(repo); registry.sources[0].modules[0].git.remote = remote; files['sources/repo/index.yaml'] = JSON.stringify(registry); - assert.deepEqual(host(files).run(input()).sections[0].references, [{url:`https://github.com/team/source/blob/${'b'.repeat(40)}/module/src/a%20%23%E4%B8%AD%E6%96%87%25.ts#L3`}]); + assert.deepEqual(refs(host(files).run(input())), [`https://github.com/team/source/blob/${'b'.repeat(40)}/module/src/a%20%23%E4%B8%AD%E6%96%87%25.ts#L3`]); } }); test('unsupported routes and nonimmutable revisions keep lossless structured evidence', () => { for (const [remote, revision] of [['https://bitbucket.org/team/source','b'.repeat(40)], ['ssh://git@example.org:2222/team/source','b'.repeat(40)], ['https://example.org/team/source','main'], ['https://example.org/team/source','abc1234']]) { const files=fixtures(); const registry=structuredClone(repo); registry.sources[0].modules[0].git={remote,ref:revision};files['sources/repo/index.yaml']=JSON.stringify(registry); - const result=host(files).run(input()).sections[0].references[0]; + const result=refs(host(files).run(input()))[0]; assert.equal(result.url,undefined);assert.equal(result.remote,remote);assert.equal(result.ref,revision);assert.equal(result.path,'module/src/example.ts');assert.equal(result.source_ref,source); } }); @@ -109,16 +115,49 @@ test('batch reads do not repeat metadata or leak previous output', () => { const h = host(fixtures()); const request = input(); request.operation = 'read_many'; - request.files.push(input(6, 6).files[0]); - assert.equal(h.run(request).sections.length, 2); + request.files.push({...input(6, 6).files[0],item_id:'read-1'}); + assert.deepEqual(h.run(request).attachments.map(a=>a.item_id), ['read-0']); assert.equal(h.calls.length, 3); - assert.deepEqual(h.run({ operation: 'read_many', files: [] }), { sections: [] }); + assert.deepEqual(h.run({ abi_version:2, operation: 'read_many', files: [] }), { attachments: [] }); +}); +test('ABI and item identity violations fail before any host read', () => { + const changes = [r => delete r.abi_version, r => r.abi_version = 1, + r => delete r.files[0].item_id, r => r.files[0].item_id = '', + r => r.files[0].item_id = 'bad\nvalue', r => r.files[0].item_id = 'x'.repeat(257), + r => r.files.push({...r.files[0]})]; + for (const change of changes) { + const h = host(fixtures()); const request = input(); change(request); + const result = h.run(request); + assert.ok(issues(result).length); + assert.equal(result.attachments[0].item_id, undefined); + assert.equal(h.calls.length, 0); + } +}); +test('same path ranges and duplicate reads retain independent attachment identities', () => { + const files = fixtures(); const map = structure([reference(), reference()]); + map.articles[0].sections[1].references = [{...reference(), locator:{path:'src/second.ts',start_line:20,end_line:30}}]; + files['knowledge/structure.yaml'] = JSON.stringify(map); + const request = input(); request.operation = 'read_many'; + request.files.push({...input(6,6).files[0],item_id:'read-1'}, {...request.files[0],item_id:'read-2'}); + const result = host(files).run(request); + assert.deepEqual(result.attachments.map(a=>a.item_id), ['read-0','read-1','read-2']); + assert.equal(body(result)[0].references.length, 1); + assert.ok(body(result)[1].references[0].endsWith('/src/second.ts#L20-L30')); + assert.equal(result.attachments[0].text, result.attachments[2].text); +}); +test('a failed file keeps diagnostics local and preserves successful siblings', () => { + const request = input(); request.operation = 'read_many'; + request.files.push({...request.files[0], item_id:'read-1',path:'knowledge/missing.md'}); + const result = host(fixtures()).run(request); + assert.equal(refs(result).length, 1); + assert.equal(result.attachments[1].item_id, 'read-1'); + assert.ok(parse(result.attachments[1].text).diagnostics.length); }); test('partial references preserve siblings and identify missing registration', () => { const result = host(fixtures([reference(), reference('repo:20260901/missing')])).run(input()); - assert.equal(result.sections[0].references.length, 1); - assert.equal(result.issues[0].code, 'SOURCE_UNRESOLVED'); - assert.equal(result.issues[0].source_ref, 'repo:20260901/missing'); + assert.equal(refs(result).length, 1); + assert.equal(issues(result)[0].code, 'SOURCE_UNRESOLVED'); + assert.equal(issues(result)[0].source_ref, 'repo:20260901/missing'); assert.equal('status' in result, false); }); test('missing and invalid metadata are not zero matches; host errors are sanitized', () => { @@ -126,7 +165,7 @@ test('missing and invalid metadata are not zero matches; host errors are sanitiz const files = fixtures(); if (bad === undefined) delete files['knowledge/structure.yaml']; else files['knowledge/structure.yaml'] = bad; const result = host(files).run(input()); - assert.equal(result.issues[0].code, 'EVIDENCE_UNAVAILABLE'); + assert.equal(issues(result)[0].code, 'EVIDENCE_UNAVAILABLE'); assert.equal(JSON.stringify(result).includes('private host'), false); } }); @@ -136,55 +175,55 @@ test('ambiguous sources and unsafe paths do not create evidence', () => { const changed = structuredClone(repo); change(changed); files['sources/repo/index.yaml'] = JSON.stringify(changed); const result = host(files).run(input()); - assert.equal(result.sections[0].references.length, 0); - assert.equal(result.issues[0].code, 'SOURCE_UNRESOLVED'); + assert.equal(refs(result).length, 0); + assert.equal(issues(result)[0].code, 'SOURCE_UNRESOLVED'); assert.equal(JSON.stringify(result).includes('secret'), false); } const h = host(fixtures()); - assert.ok(h.run(input(2, 2, { workspace_root: '../escape' })).issues); + assert.ok(issues(h.run(input(2, 2, { workspace_root: '../escape' }))).length); assert.equal(h.calls.length, 0); }); test('CRLF, fences and entity-encoded IDs follow section semantics', () => { const content = '```md\r\n\r\n```\r\n\r\nbody\r\n~~~\r\n\r\n~~~\r\n'; const files = fixtures(); files['knowledge/example.md'] = content; - assert.equal(host(files).run(input(5, 5)).sections[0].section_id, 'first'); - assert.deepEqual(host(files).run(input(2, 2)), { sections: [] }); + assert.equal(refs(host(files).run(input(5, 5))).length, 1); + assert.deepEqual(host(files).run(input(2, 2)), { attachments: [] }); files['knowledge/example.md'] = '\nbody\n'; const map = structure(); map.articles[0].sections[0].id = 'a&b'; files['knowledge/structure.yaml'] = JSON.stringify(map); - assert.equal(host(files).run(input()).sections[0].section_id, 'a&b'); + assert.equal(refs(host(files).run(input())).length, 1); }); test('malformed and duplicate article markers are diagnosed', () => { for (const content of ['', '', `${article}\n${article}`, '']) { const files = fixtures(); files['knowledge/example.md'] = content; - assert.ok(host(files).run(input()).issues); + assert.ok(issues(host(files).run(input())).length); } }); test('file, document, note and session sources retain their distinct locations', () => { - const refs = ['file:20260901/input', 'lark:20260901/doc', 'note:20260901/note.md', 'sessions:20260901/session.md'].map(reference); - const files = fixtures(refs); + const references = ['file:20260901/input', 'lark:20260901/doc', 'note:20260901/note.md', 'sessions:20260901/session.md'].map(reference); + const files = fixtures(references); files['sources/file/index.yaml'] = JSON.stringify({ sources: [{ name: '20260901', modules: [{ name: 'input' }] }] }); files['sources/lark/index.yaml'] = JSON.stringify({ sources: [{ name: '20260901', modules: [{ name: 'doc', url: 'https://example.org/doc/1' }] }] }); files['sources/note/20260901/note.md'] = 'saved note'; files['sources/sessions/20260901/session.md'] = 'saved session'; const result = host(files).run(input()); - assert.equal(result.issues, undefined); - assert.equal(result.sections[0].references[0].manifest, 'sources/file/20260901/manifest.json'); - assert.equal(result.sections[0].references[1].url, 'https://example.org/doc/1'); - assert.equal(result.sections[0].references[2].materialized_at, 'sources/note/20260901/note.md'); + assert.deepEqual(issues(result), []); + assert.equal(refs(result)[0].manifest, 'sources/file/20260901/manifest.json'); + assert.equal(refs(result)[1].url, 'https://example.org/doc/1'); + assert.equal(refs(result)[2].materialized_at, 'sources/note/20260901/note.md'); }); test('legacy unmarked articles and non-knowledge files do not invent sections', () => { const files = fixtures(); files['knowledge/example.md'] = '# Legacy\nPlain text'; - assert.deepEqual(host(files).run(input()), { sections: [] }); + assert.deepEqual(host(files).run(input()), { attachments: [] }); const request = input(); request.files[0].path = 'src/file.ts'; - const h = host(files); assert.deepEqual(h.run(request), { sections: [] }); assert.equal(h.calls.length, 0); + const h = host(files); assert.deepEqual(h.run(request), { attachments: [] }); assert.equal(h.calls.length, 0); }); test('invalid args and unsupported operations are contained', () => { const defaultArgs = input(); defaultArgs.args = null; - assert.equal(host(fixtures()).run(defaultArgs).sections[0].section_id, 'first'); - assert.ok(host(fixtures()).run(input(2, 2, { extra: true })).issues); + assert.equal(refs(host(fixtures()).run(defaultArgs)).length, 1); + assert.ok(issues(host(fixtures()).run(input(2, 2, { extra: true }))).length); const request = input(); request.operation = 'execute'; - assert.ok(host(fixtures()).run(request).issues); + assert.ok(issues(host(fixtures()).run(request)).length); }); test('duplicate keys, recursive aliases and excessive YAML depth fail without false evidence', () => { @@ -195,15 +234,15 @@ test('duplicate keys, recursive aliases and excessive YAML depth fail without fa ]) { const files = fixtures(); files['knowledge/structure.yaml'] = yaml; const result = host(files).run(input()); - assert.ok(result.issues); - assert.deepEqual(result.sections, []); + assert.ok(issues(result).length); + assert.deepEqual(refs(result), []); } }); test('oversized metadata and output preserve valid diagnostic JSON', () => { const files = fixtures(); files['knowledge/structure.yaml'] = ' '.repeat(8 * 1024 * 1024 + 1); - assert.ok(host(files).run(input()).issues); + assert.ok(issues(host(files).run(input())).length); const refs = Array.from({ length: 1300 }, (_, i) => ({ ...reference(), locator: { path: `src/file-${i}.ts`, start_line: 1, end_line: 1 } })); - assert.equal(host(fixtures(refs)).run(input()).issues[0].code, 'OUTPUT_LIMIT'); + assert.equal(issues(host(fixtures(refs)).run(input()))[0].code, 'OUTPUT_LIMIT'); }); test('representative metadata volume is cached without returning the catalog', () => { const files = fixtures(); const map = structure(); @@ -213,7 +252,7 @@ test('representative metadata volume is cached without returning the catalog', ( } files['knowledge/structure.yaml'] = JSON.stringify(map); const h = host(files); const result = h.run(input()); - assert.equal(result.sections.length, 1); + assert.equal(result.attachments.length, 1); assert.ok(JSON.stringify(result).length < 1000); const reads = h.calls.length; for (let i = 0; i < 25; i++) assert.deepEqual(h.run(input()), result); diff --git a/packages/context-cli/src/project/evidencePlugin.ts b/packages/context-cli/src/project/evidencePlugin.ts index f665e136..9af57ea4 100644 --- a/packages/context-cli/src/project/evidencePlugin.ts +++ b/packages/context-cli/src/project/evidencePlugin.ts @@ -43,7 +43,8 @@ async function readArtifact(artifact: string): Promise<{ bytes: Buffer; manifest } const module = new WebAssembly.Module(bytes); const metadata = WebAssembly.Module.customSections(module, "sourcegraph.plugin.v1"); - if (metadata.length !== 1 || JSON.parse(new TextDecoder().decode(metadata[0])).name !== "context-evidence") { + const declaration = metadata.length === 1 ? JSON.parse(new TextDecoder().decode(metadata[0])) : undefined; + if (declaration?.name !== "context-evidence" || declaration.abi_version !== 2) { throw new Error("Invalid bundled evidence plugin metadata"); } return { bytes, manifest }; diff --git a/packages/context-cli/src/project/indexerBaseContracts.ts b/packages/context-cli/src/project/indexerBaseContracts.ts index abff066b..e694a2ef 100644 --- a/packages/context-cli/src/project/indexerBaseContracts.ts +++ b/packages/context-cli/src/project/indexerBaseContracts.ts @@ -21,7 +21,7 @@ import { bundledMarkdownReaderQuestionContracts } from "./indexerBaseMarkdownAuthoringCatalog.js"; const BASE_CONTRACT_VERSION = "1.1.0"; -export const BUNDLED_INDEXER_PARSER_PACKAGE_VERSION = "0.7.42"; +export const BUNDLED_INDEXER_PARSER_PACKAGE_VERSION = "0.7.43"; const BUNDLED_PARSER_REQUIREMENTS = buildIndexerParserCapabilityRequirements( BUNDLED_INDEXER_PARSER_PACKAGE_VERSION, ); diff --git a/packages/context-evidence-wasm/README.md b/packages/context-evidence-wasm/README.md index 8517125d..3f1eb72d 100644 --- a/packages/context-evidence-wasm/README.md +++ b/packages/context-evidence-wasm/README.md @@ -9,7 +9,7 @@ with the `wasm32-unknown-unknown` target and the committed Cargo lockfile. End u receive the compiled artifact with the CLI and do not need Rust. The binary embeds redistribution notices in the `context.licenses` custom section. -## ABI 1 +## ABI 2 Exports: `memory`, `alloc(length: i32) -> i32`, `dealloc(pointer: i32, length: i32)`, `enrich(pointer: i32, length: i32) -> i64`. @@ -37,9 +37,10 @@ Input JSON: ```json { + "abi_version": 2, "operation": "read", "args": {"workspace_root": ".", "include_digest": false}, - "files": [{"path": "knowledge/example.md", "start_line": 2, + "files": [{"item_id": "read-0", "path": "knowledge/example.md", "start_line": 2, "end_line": 8, "content": "actual returned text", "truncated": false}] } ``` @@ -50,7 +51,7 @@ fields are ignored; unknown plugin args are rejected. Empty args can be omitted or null (a host's absent argument map). For scoped repositories the host supplies files.path relative to the registered plugin root, with root/repository_path as additional context. Host reads and -workspace_root use that same base. The host wraps results in scopes[{root,data}]; +workspace_root use that same base; prefix root only when resolving knowledge/snapshot locations, never external source repository paths. A full-repository host uses the Git root. Plugin metadata lives in custom section `sourcegraph.plugin.v1`. @@ -60,11 +61,22 @@ an explicit `plugins: []` disables enhancement, and a nonempty list selects only the named plugins. Hosts without default selection still require explicit `plugins: [{"name":"context-evidence"}]`. Business arguments remain optional. -Output contains `sections: [{path, section_id, references}]`, and only on failure -`issues: [{code, message, path?, section_id?, source_ref?}]`. No success flag. +Output is `{"attachments":[{"item_id":"read-0","text":"references:\n- https://example.org/source"}]}`. +The host assigns unique opaque item IDs, including repeated paths or ranges, and +appends each text unchanged to that item's original content. IDs are nonempty, +at most 256 UTF-8 bytes and contain no control characters. They survive scoped +invocation splitting. The host does not parse references or other business fields. +Attachments without an item ID apply to the batch; unknown IDs are diagnostics, +never guessed associations. Empty evidence returns `{"attachments":[]}`. +The text is YAML with deduplicated references per item and diagnostics on failure. +Normal output exposes no section IDs, plugin labels or success flags. Sections +remain internal to selecting evidence for the actual returned range. +Metadata and input both require `abi_version: 2`; low-level exports remain +unchanged. Old JSON contracts are rejected, not interpreted. MCP renders only +standard content text blocks, preserving original text even if enrichment fails. References are recorded metadata, not proof that the source has been reread. Repository references with a full 40/64-digit commit and a supported remote shape -return `{url}` (plus `content_digest` only when requested). They use Context's +return URL strings (objects with `url` and `content_digest` when requested). They use Context's blob-compatible web URL convention with encoded file segments and line anchors. No host reachability is implied. Unsupported remote shapes, known incompatible routes and nonimmutable refs retain the original diff --git a/packages/context-evidence-wasm/official-digests.json b/packages/context-evidence-wasm/official-digests.json index fe51488c..e40a94ec 100644 --- a/packages/context-evidence-wasm/official-digests.json +++ b/packages/context-evidence-wasm/official-digests.json @@ -1 +1,4 @@ -[] +[ + "d0afc61569c824be6c0f819d3be4a26cab954cba6508ae654b69a9d1c7a43550", + "2edd8fb97eea337877468d2ff42f8d1973f69d1d29ed8dc60fda13a11572130b" +] diff --git a/packages/context-evidence-wasm/plugin.json b/packages/context-evidence-wasm/plugin.json index 22782924..a2781c75 100644 --- a/packages/context-evidence-wasm/plugin.json +++ b/packages/context-evidence-wasm/plugin.json @@ -2,8 +2,8 @@ "name": "context-evidence", "title": "Context evidence", "description": "Attach recorded section sources to knowledge reads; does not verify original source contents.", - "version": "0.2.0", - "abi_version": 1, + "version": "0.3.0", + "abi_version": 2, "operations": ["read", "read_many"], "default_enabled": true, "input_schema": { diff --git a/packages/context-evidence-wasm/src/abi.rs b/packages/context-evidence-wasm/src/abi.rs index 06c42487..f6e0f58d 100644 --- a/packages/context-evidence-wasm/src/abi.rs +++ b/packages/context-evidence-wasm/src/abi.rs @@ -1,5 +1,4 @@ -use crate::{Engine, Input, Reader, MAX_FILE}; -use serde_json::json; +use crate::{diagnostic, Engine, Input, Reader, MAX_FILE}; use std::cell::RefCell; #[used] @@ -64,12 +63,15 @@ pub unsafe extern "C" fn dealloc(pointer: u32, length: u32) { #[no_mangle] pub unsafe extern "C" fn enrich(pointer: u32, length: u32) -> u64 { let result = if length > 1024 * 1024 { - json!({"issues":[{"code":"INVALID_INPUT","message":"Input exceeds plugin budget"}]}) + diagnostic("INVALID_INPUT", "Input exceeds plugin budget") } else { let bytes = std::slice::from_raw_parts(pointer as *const u8, length as usize); match serde_json::from_slice::(bytes) { Ok(input) => ENGINE.with(|engine| engine.borrow_mut().enrich(input, &mut Host)), - Err(_) => json!({"issues":[{"code":"INVALID_INPUT","message":"Invalid plugin input"}]}), + Err(_) => diagnostic( + "INVALID_INPUT", + "Expected ABI 2 input with item_id on every file and valid plugin arguments", + ), } }; let bytes = serde_json::to_vec(&result).unwrap().into_boxed_slice(); diff --git a/packages/context-evidence-wasm/src/lib.rs b/packages/context-evidence-wasm/src/lib.rs index a2c72814..09abe6eb 100644 --- a/packages/context-evidence-wasm/src/lib.rs +++ b/packages/context-evidence-wasm/src/lib.rs @@ -17,6 +17,7 @@ pub trait Reader { #[derive(Deserialize)] pub struct Input { + pub abi_version: u32, pub operation: String, #[serde(default)] pub args: Option, @@ -32,6 +33,7 @@ pub struct Args { } #[derive(Deserialize)] pub struct File { + pub item_id: String, pub path: String, pub start_line: u64, pub end_line: u64, @@ -77,6 +79,43 @@ fn issue(code: &str, message: &str, path: &str) -> Value { json!({"code": code, "message": message, "path": path}) } +pub fn diagnostic(code: &str, message: &str) -> Value { + json!({"attachments":[{"text":format!("diagnostics:\n- {}", json!({"code":code,"message":message}))}]}) +} + +fn attachment(item_id: &str, sections: Vec, issues: Vec) -> Option { + let mut seen = HashSet::new(); + let mut references = Vec::new(); + for section in sections { + for reference in section["references"].as_array().into_iter().flatten() { + let value = if reference + .as_object() + .is_some_and(|o| o.len() == 1 && o.contains_key("url")) + { + reference["url"].clone() + } else { + reference.clone() + }; + if seen.insert(value.to_string()) { + references.push(value); + } + } + } + if references.is_empty() && issues.is_empty() { + return None; + } + let mut body = serde_json::Map::new(); + if !references.is_empty() { + body.insert("references".into(), json!(references)); + } + if !issues.is_empty() { + body.insert("diagnostics".into(), json!(issues)); + } + // Serialize, rather than interpolate, repository-controlled strings. + let text = serde_yaml_ng::to_string(&body).expect("JSON values are YAML serializable"); + Some(json!({"item_id":item_id,"text":text})) +} + impl Engine { fn trim_cache(&mut self, size: usize) { if self.cache_bytes.saturating_add(size) > MAX_CACHE { @@ -102,19 +141,27 @@ impl Engine { } pub fn enrich(&mut self, input: Input, reader: &mut impl Reader) -> Value { let mut output = Vec::new(); - let mut issues = Vec::new(); let args = input.args.unwrap_or_default(); let root = args.workspace_root.as_deref().unwrap_or("."); let root = if root == "." { "" } else { root }; - if (!root.is_empty() && !safe_path(root)) + let mut ids = HashSet::new(); + if input.abi_version != 2 + || (!root.is_empty() && !safe_path(root)) || !matches!(input.operation.as_str(), "read" | "read_many") || input.files.len() > 10 + || input.files.iter().any(|f| { + f.item_id.is_empty() + || f.item_id.len() > 256 + || f.item_id.chars().any(char::is_control) + || !ids.insert(f.item_id.clone()) + }) { - return json!({"issues":[{"code":"INVALID_INPUT","message":"Invalid operation, workspace root or batch size"}]}); + return diagnostic("INVALID_INPUT", "Expected ABI 2, unique nonempty item IDs, a safe workspace root and at most 10 read items"); } let prefix = join(root, "knowledge/"); let structure_path = join(root, "knowledge/structure.yaml"); for file in input.files { + let mut issues = Vec::new(); let Some(article_path) = file.path.strip_prefix(&prefix) else { continue; }; @@ -187,17 +234,23 @@ impl Engine { } Ok(results) })(); - match result { - Ok(values) => output.extend(values), - Err(message) => issues.push(issue("EVIDENCE_UNAVAILABLE", &message, &file.path)), + let values = match result { + Ok(values) => values, + Err(message) => { + issues.push(issue("EVIDENCE_UNAVAILABLE", &message, &file.path)); + Vec::new() + } + }; + if let Some(value) = attachment(&file.item_id, values, issues) { + output.push(value); } } - let mut result = json!({"sections":output}); - if !issues.is_empty() { - result["issues"] = json!(issues); - } + let result = json!({"attachments":output}); if result.to_string().len() > MAX_OUTPUT { - return json!({"issues":[{"code":"OUTPUT_LIMIT","message":"Evidence exceeds output budget; read a narrower range"}]}); + return diagnostic( + "OUTPUT_LIMIT", + "Evidence exceeds output budget; read a narrower range", + ); } result } diff --git a/packages/context-evidence-wasm/src/sources.rs b/packages/context-evidence-wasm/src/sources.rs index d74386f6..4651cea6 100644 --- a/packages/context-evidence-wasm/src/sources.rs +++ b/packages/context-evidence-wasm/src/sources.rs @@ -30,18 +30,30 @@ fn code_url(remote: &str, revision: &str, path: &str, start: u64, end: u64) -> O } else if let Some(rest) = remote.strip_prefix("git@") { let (host, repo) = rest.split_once(':')?; ("https", format!("{host}/{repo}")) - } else { return None; }; + } else { + return None; + }; let (host, repo) = rest.split_once('/')?; - if host.is_empty() || !host.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-')) { + if host.is_empty() + || !host + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-')) + { return None; } let repo = repo.trim_end_matches('/'); let repo = repo.strip_suffix(".git").unwrap_or(repo); - if !safe_path(repo) || !repo.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'/' | b'-' | b'_' | b'.')) { + if !safe_path(repo) + || !repo + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'/' | b'-' | b'_' | b'.')) + { return None; } // These providers do not use the default blob route. - if matches!(host, "bitbucket.org" | "dev.azure.com" | "gitlab.com") { return None; } + if matches!(host, "bitbucket.org" | "dev.azure.com" | "gitlab.com") { + return None; + } let mut encoded = String::new(); for byte in path.bytes() { if byte.is_ascii_alphanumeric() || matches!(byte, b'/' | b'-' | b'_' | b'.' | b'~') { @@ -51,8 +63,14 @@ fn code_url(remote: &str, revision: &str, path: &str, start: u64, end: u64) -> O write!(&mut encoded, "%{byte:02X}").ok()?; } } - let anchor = if start == end { format!("L{start}") } else { format!("L{start}-L{end}") }; - Some(format!("{scheme}://{host}/{repo}/blob/{revision}/{encoded}#{anchor}")) + let anchor = if start == end { + format!("L{start}") + } else { + format!("L{start}-L{end}") + }; + Some(format!( + "{scheme}://{host}/{repo}/blob/{revision}/{encoded}#{anchor}" + )) } impl Engine { @@ -155,7 +173,9 @@ impl Engine { out["ref"] = json!(revision); if let Some(url) = code_url(remote, revision, &join(subpath, path), start, end) { let mut compact = json!({"url":url}); - if digest { compact["content_digest"] = out["content_digest"].clone(); } + if digest { + compact["content_digest"] = out["content_digest"].clone(); + } return Ok(compact); } } else { diff --git a/packages/context/package.json b/packages/context/package.json index 35a70274..5f981e61 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/context", "description": "Declarative SDK for Context knowledge sources, workflows, review, and package outputs", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index b5320039..a1e25688 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/core", "description": "Shared extraction types, schemas, and utilities for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/dev-cli/package.json b/packages/dev-cli/package.json index 3d89e1b2..4609e641 100644 --- a/packages/dev-cli/package.json +++ b/packages/dev-cli/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/dev-cli", "description": "Developer menu for Context build, verification, link, and release preparation", - "version": "0.7.42", + "version": "0.7.43", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extract-contract/package.json b/packages/extract-contract/package.json index 39acca64..d9067e08 100644 --- a/packages/extract-contract/package.json +++ b/packages/extract-contract/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-contract", "description": "OpenAPI and GraphQL contract catalog adapter for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-go/package.json b/packages/extract-go/package.json index 02f87fb4..f85e44cc 100644 --- a/packages/extract-go/package.json +++ b/packages/extract-go/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-go", "description": "Go extraction plugin and structural index for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-mdx/package.json b/packages/extract-mdx/package.json index 2a1b2887..81d9a99c 100644 --- a/packages/extract-mdx/package.json +++ b/packages/extract-mdx/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-mdx", "description": "MDX component and example catalog bridge for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-proto/package.json b/packages/extract-proto/package.json index 3923f1bb..54035601 100644 --- a/packages/extract-proto/package.json +++ b/packages/extract-proto/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-proto", "description": "Protocol Buffers IDL catalog parser for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-rush/package.json b/packages/extract-rush/package.json index 6326676f..eae9369b 100644 --- a/packages/extract-rush/package.json +++ b/packages/extract-rush/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-rush", "description": "Rush workspace structural index for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-sql/package.json b/packages/extract-sql/package.json index 2702f72a..a7efe969 100644 --- a/packages/extract-sql/package.json +++ b/packages/extract-sql/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-sql", "description": "Dialect-bound lightweight SQL evidence adapter for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-style/package.json b/packages/extract-style/package.json index b127ca31..35027f84 100644 --- a/packages/extract-style/package.json +++ b/packages/extract-style/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-style", "description": "Lightweight CSS and SCSS evidence adapter for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-thrift/package.json b/packages/extract-thrift/package.json index 565dec0a..c0123a6c 100644 --- a/packages/extract-thrift/package.json +++ b/packages/extract-thrift/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-thrift", "description": "Apache Thrift IDL catalog parser for Context", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-ts/package.json b/packages/extract-ts/package.json index 0e8f52cd..cbc17c84 100644 --- a/packages/extract-ts/package.json +++ b/packages/extract-ts/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-ts", "description": "TypeScript and JavaScript extraction plugin for the Context ExtractionResult v2 contract", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract/package.json b/packages/extract/package.json index ced48cb9..48a3432e 100644 --- a/packages/extract/package.json +++ b/packages/extract/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract", "description": "Language-plugin framework and repository runner for Context code evidence", - "version": "0.7.42", + "version": "0.7.43", "type": "module", "license": "MIT", "engines": { diff --git a/packages/tui/package.json b/packages/tui/package.json index 4e91f49d..2416c079 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/tui", "description": "Shared terminal UI components (Ink + React) for Context development tools", - "version": "0.7.42", + "version": "0.7.43", "private": true, "type": "module", "license": "MIT", diff --git a/plugins/context/repo-install/claude/.claude-plugin/plugin.json b/plugins/context/repo-install/claude/.claude-plugin/plugin.json index 15847c2b..f43c3146 100644 --- a/plugins/context/repo-install/claude/.claude-plugin/plugin.json +++ b/plugins/context/repo-install/claude/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "c4a", "description": "Start or continue a project-local knowledge workspace through one graph-routed entry.", - "version": "0.7.42", + "version": "0.7.43", "author": { "name": "c4a" }, diff --git a/plugins/context/repo-install/claude/commands/context-inspect-search.md b/plugins/context/repo-install/claude/commands/context-inspect-search.md index fc47614c..854bd3fa 100644 --- a/plugins/context/repo-install/claude/commands/context-inspect-search.md +++ b/plugins/context/repo-install/claude/commands/context-inspect-search.md @@ -128,6 +128,18 @@ matching sections with their conditions, not snippets alone. Batch known related reads within returned limits. Use the remote evidence checks below for knowledge as well as code, including per-item errors, truncation and coverage. A search miss is not evidence that the repository lacks the knowledge. +When the connected schema supports it, use `output: "files"` only when locating +paths is enough; use content excerpts when relevance needs context, bounding +them with `max_lines_per_file` as needed. Do not add a mandatory file-list pass +before every search. Omitted matches and truncation do not establish absence. +If results are too broad, narrow by article path or a distinctive term before +reading more output. For zero hits, simplify combined conditions using the +service's documented syntax; do not assume unsupported OR/regex syntax worked. +Follow the connected tool schema: use search-hit ranges when known; otherwise +omit bounds if the service supplies a bounded default (currently up to 500 lines). +For services requiring endpoints, supply `start_line` and `end_line` per item. +Start with a bounded first window +and follow continuation only when needed, not the whole file by default. Consume recorded evidence already attached to ordinary `read`/`read_many` responses; the repository plugin may be enabled by default. Do not reread just @@ -139,8 +151,12 @@ This returns section sources with the original text; reuse them instead of separately looking up structure and registries. No plugin discovery/help call is required when the name and workspace are known. These are registered references, not verification that original sources were read. Keep original read errors and -truncation checks; inspect extension `issues` only for affected evidence gaps. -Source references may contain a ready commit-specific `url` instead of separate +truncation checks; inspect appended diagnostics only for affected evidence gaps. +Consume `references:` appended to each returned file. These may cover several +sections in that read: not every reference supports every +sentence. Narrow the read or inspect the source when attribution matters. +References may be ready +commit-specific URL strings or contain a `url` instead of separate repository/ref/path/line fields. Reuse it directly for that registered evidence; do not look up registries merely to reconstruct it. For necessary code reads, extract the exact repository, full commit, percent-decoded path and line range @@ -148,8 +164,7 @@ from its recognized blob route into the existing MCP read fields. The service does not implicitly accept a URL as a read request. Do not guess unfamiliar URL formats; use targeted source metadata when needed. A URL is not proof of a read, and evidence read at another commit needs its own citation. -For a scoped repository, consume `extensions["context-evidence"].scopes[].data` -with its `root`: knowledge and snapshot paths are relative to that plugin root; +For a scoped repository, knowledge and snapshot paths are relative to the plugin root; source-code reference paths already include their own source subpath. Do not prepend the knowledge scope to external source paths or repeat either prefix. If the service rejects the plugin option, retry the ordinary read once. If the diff --git a/plugins/context/repo-install/claude/skills/context-inspect-search/SKILL.md b/plugins/context/repo-install/claude/skills/context-inspect-search/SKILL.md index 4a3475c4..be9a3996 100644 --- a/plugins/context/repo-install/claude/skills/context-inspect-search/SKILL.md +++ b/plugins/context/repo-install/claude/skills/context-inspect-search/SKILL.md @@ -127,6 +127,18 @@ matching sections with their conditions, not snippets alone. Batch known related reads within returned limits. Use the remote evidence checks below for knowledge as well as code, including per-item errors, truncation and coverage. A search miss is not evidence that the repository lacks the knowledge. +When the connected schema supports it, use `output: "files"` only when locating +paths is enough; use content excerpts when relevance needs context, bounding +them with `max_lines_per_file` as needed. Do not add a mandatory file-list pass +before every search. Omitted matches and truncation do not establish absence. +If results are too broad, narrow by article path or a distinctive term before +reading more output. For zero hits, simplify combined conditions using the +service's documented syntax; do not assume unsupported OR/regex syntax worked. +Follow the connected tool schema: use search-hit ranges when known; otherwise +omit bounds if the service supplies a bounded default (currently up to 500 lines). +For services requiring endpoints, supply `start_line` and `end_line` per item. +Start with a bounded first window +and follow continuation only when needed, not the whole file by default. Consume recorded evidence already attached to ordinary `read`/`read_many` responses; the repository plugin may be enabled by default. Do not reread just @@ -138,8 +150,12 @@ This returns section sources with the original text; reuse them instead of separately looking up structure and registries. No plugin discovery/help call is required when the name and workspace are known. These are registered references, not verification that original sources were read. Keep original read errors and -truncation checks; inspect extension `issues` only for affected evidence gaps. -Source references may contain a ready commit-specific `url` instead of separate +truncation checks; inspect appended diagnostics only for affected evidence gaps. +Consume `references:` appended to each returned file. These may cover several +sections in that read: not every reference supports every +sentence. Narrow the read or inspect the source when attribution matters. +References may be ready +commit-specific URL strings or contain a `url` instead of separate repository/ref/path/line fields. Reuse it directly for that registered evidence; do not look up registries merely to reconstruct it. For necessary code reads, extract the exact repository, full commit, percent-decoded path and line range @@ -147,8 +163,7 @@ from its recognized blob route into the existing MCP read fields. The service does not implicitly accept a URL as a read request. Do not guess unfamiliar URL formats; use targeted source metadata when needed. A URL is not proof of a read, and evidence read at another commit needs its own citation. -For a scoped repository, consume `extensions["context-evidence"].scopes[].data` -with its `root`: knowledge and snapshot paths are relative to that plugin root; +For a scoped repository, knowledge and snapshot paths are relative to the plugin root; source-code reference paths already include their own source subpath. Do not prepend the knowledge scope to external source paths or repeat either prefix. If the service rejects the plugin option, retry the ordinary read once. If the diff --git a/plugins/context/repo-install/codex/.codex-plugin/plugin.json b/plugins/context/repo-install/codex/.codex-plugin/plugin.json index d58dccaf..2f40035e 100644 --- a/plugins/context/repo-install/codex/.codex-plugin/plugin.json +++ b/plugins/context/repo-install/codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "c4a", - "version": "0.7.42", + "version": "0.7.43", "description": "Start or continue a project-local knowledge workspace through one graph-routed entry.", "author": { "name": "c4a" }, "homepage": "https://github.com/context4ai/c4a", @@ -10,7 +10,7 @@ "skills": "./skills/", "interface": { "displayName": "C4A Context", - "shortDescription": "Initialize and advance a local, source-linked project knowledge workspace.\nv0.7.42", + "shortDescription": "Initialize and advance a local, source-linked project knowledge workspace.\nv0.7.43", "longDescription": "Create a Context workspace and use agent-guided next steps to register sources, run extraction, review candidates, build package outputs, and verify health without silently mutating source repositories.", "developerName": "c4a", "category": "Productivity", diff --git a/plugins/context/repo-install/codex/skills/context-inspect-search/SKILL.md b/plugins/context/repo-install/codex/skills/context-inspect-search/SKILL.md index 4a3475c4..be9a3996 100644 --- a/plugins/context/repo-install/codex/skills/context-inspect-search/SKILL.md +++ b/plugins/context/repo-install/codex/skills/context-inspect-search/SKILL.md @@ -127,6 +127,18 @@ matching sections with their conditions, not snippets alone. Batch known related reads within returned limits. Use the remote evidence checks below for knowledge as well as code, including per-item errors, truncation and coverage. A search miss is not evidence that the repository lacks the knowledge. +When the connected schema supports it, use `output: "files"` only when locating +paths is enough; use content excerpts when relevance needs context, bounding +them with `max_lines_per_file` as needed. Do not add a mandatory file-list pass +before every search. Omitted matches and truncation do not establish absence. +If results are too broad, narrow by article path or a distinctive term before +reading more output. For zero hits, simplify combined conditions using the +service's documented syntax; do not assume unsupported OR/regex syntax worked. +Follow the connected tool schema: use search-hit ranges when known; otherwise +omit bounds if the service supplies a bounded default (currently up to 500 lines). +For services requiring endpoints, supply `start_line` and `end_line` per item. +Start with a bounded first window +and follow continuation only when needed, not the whole file by default. Consume recorded evidence already attached to ordinary `read`/`read_many` responses; the repository plugin may be enabled by default. Do not reread just @@ -138,8 +150,12 @@ This returns section sources with the original text; reuse them instead of separately looking up structure and registries. No plugin discovery/help call is required when the name and workspace are known. These are registered references, not verification that original sources were read. Keep original read errors and -truncation checks; inspect extension `issues` only for affected evidence gaps. -Source references may contain a ready commit-specific `url` instead of separate +truncation checks; inspect appended diagnostics only for affected evidence gaps. +Consume `references:` appended to each returned file. These may cover several +sections in that read: not every reference supports every +sentence. Narrow the read or inspect the source when attribution matters. +References may be ready +commit-specific URL strings or contain a `url` instead of separate repository/ref/path/line fields. Reuse it directly for that registered evidence; do not look up registries merely to reconstruct it. For necessary code reads, extract the exact repository, full commit, percent-decoded path and line range @@ -147,8 +163,7 @@ from its recognized blob route into the existing MCP read fields. The service does not implicitly accept a URL as a read request. Do not guess unfamiliar URL formats; use targeted source metadata when needed. A URL is not proof of a read, and evidence read at another commit needs its own citation. -For a scoped repository, consume `extensions["context-evidence"].scopes[].data` -with its `root`: knowledge and snapshot paths are relative to that plugin root; +For a scoped repository, knowledge and snapshot paths are relative to the plugin root; source-code reference paths already include their own source subpath. Do not prepend the knowledge scope to external source paths or repeat either prefix. If the service rejects the plugin option, retry the ordinary read once. If the diff --git a/plugins/context/repo-install/cursor/.cursor-plugin/plugin.json b/plugins/context/repo-install/cursor/.cursor-plugin/plugin.json index b8e2b0dc..9fc61fcb 100644 --- a/plugins/context/repo-install/cursor/.cursor-plugin/plugin.json +++ b/plugins/context/repo-install/cursor/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "c4a", "displayName": "C4A Context", - "version": "0.7.42", + "version": "0.7.43", "description": "Start or continue a project-local knowledge workspace through one graph-routed entry.", "author": { "name": "Context4AI", diff --git a/plugins/context/repo-install/cursor/commands/c4a-context-inspect-search.md b/plugins/context/repo-install/cursor/commands/c4a-context-inspect-search.md index e3fda309..c88ac3a3 100644 --- a/plugins/context/repo-install/cursor/commands/c4a-context-inspect-search.md +++ b/plugins/context/repo-install/cursor/commands/c4a-context-inspect-search.md @@ -126,6 +126,18 @@ matching sections with their conditions, not snippets alone. Batch known related reads within returned limits. Use the remote evidence checks below for knowledge as well as code, including per-item errors, truncation and coverage. A search miss is not evidence that the repository lacks the knowledge. +When the connected schema supports it, use `output: "files"` only when locating +paths is enough; use content excerpts when relevance needs context, bounding +them with `max_lines_per_file` as needed. Do not add a mandatory file-list pass +before every search. Omitted matches and truncation do not establish absence. +If results are too broad, narrow by article path or a distinctive term before +reading more output. For zero hits, simplify combined conditions using the +service's documented syntax; do not assume unsupported OR/regex syntax worked. +Follow the connected tool schema: use search-hit ranges when known; otherwise +omit bounds if the service supplies a bounded default (currently up to 500 lines). +For services requiring endpoints, supply `start_line` and `end_line` per item. +Start with a bounded first window +and follow continuation only when needed, not the whole file by default. Consume recorded evidence already attached to ordinary `read`/`read_many` responses; the repository plugin may be enabled by default. Do not reread just @@ -137,8 +149,12 @@ This returns section sources with the original text; reuse them instead of separately looking up structure and registries. No plugin discovery/help call is required when the name and workspace are known. These are registered references, not verification that original sources were read. Keep original read errors and -truncation checks; inspect extension `issues` only for affected evidence gaps. -Source references may contain a ready commit-specific `url` instead of separate +truncation checks; inspect appended diagnostics only for affected evidence gaps. +Consume `references:` appended to each returned file. These may cover several +sections in that read: not every reference supports every +sentence. Narrow the read or inspect the source when attribution matters. +References may be ready +commit-specific URL strings or contain a `url` instead of separate repository/ref/path/line fields. Reuse it directly for that registered evidence; do not look up registries merely to reconstruct it. For necessary code reads, extract the exact repository, full commit, percent-decoded path and line range @@ -146,8 +162,7 @@ from its recognized blob route into the existing MCP read fields. The service does not implicitly accept a URL as a read request. Do not guess unfamiliar URL formats; use targeted source metadata when needed. A URL is not proof of a read, and evidence read at another commit needs its own citation. -For a scoped repository, consume `extensions["context-evidence"].scopes[].data` -with its `root`: knowledge and snapshot paths are relative to that plugin root; +For a scoped repository, knowledge and snapshot paths are relative to the plugin root; source-code reference paths already include their own source subpath. Do not prepend the knowledge scope to external source paths or repeat either prefix. If the service rejects the plugin option, retry the ordinary read once. If the diff --git a/plugins/context/repo-install/cursor/skills/context-inspect-search/SKILL.md b/plugins/context/repo-install/cursor/skills/context-inspect-search/SKILL.md index 4a3475c4..be9a3996 100644 --- a/plugins/context/repo-install/cursor/skills/context-inspect-search/SKILL.md +++ b/plugins/context/repo-install/cursor/skills/context-inspect-search/SKILL.md @@ -127,6 +127,18 @@ matching sections with their conditions, not snippets alone. Batch known related reads within returned limits. Use the remote evidence checks below for knowledge as well as code, including per-item errors, truncation and coverage. A search miss is not evidence that the repository lacks the knowledge. +When the connected schema supports it, use `output: "files"` only when locating +paths is enough; use content excerpts when relevance needs context, bounding +them with `max_lines_per_file` as needed. Do not add a mandatory file-list pass +before every search. Omitted matches and truncation do not establish absence. +If results are too broad, narrow by article path or a distinctive term before +reading more output. For zero hits, simplify combined conditions using the +service's documented syntax; do not assume unsupported OR/regex syntax worked. +Follow the connected tool schema: use search-hit ranges when known; otherwise +omit bounds if the service supplies a bounded default (currently up to 500 lines). +For services requiring endpoints, supply `start_line` and `end_line` per item. +Start with a bounded first window +and follow continuation only when needed, not the whole file by default. Consume recorded evidence already attached to ordinary `read`/`read_many` responses; the repository plugin may be enabled by default. Do not reread just @@ -138,8 +150,12 @@ This returns section sources with the original text; reuse them instead of separately looking up structure and registries. No plugin discovery/help call is required when the name and workspace are known. These are registered references, not verification that original sources were read. Keep original read errors and -truncation checks; inspect extension `issues` only for affected evidence gaps. -Source references may contain a ready commit-specific `url` instead of separate +truncation checks; inspect appended diagnostics only for affected evidence gaps. +Consume `references:` appended to each returned file. These may cover several +sections in that read: not every reference supports every +sentence. Narrow the read or inspect the source when attribution matters. +References may be ready +commit-specific URL strings or contain a `url` instead of separate repository/ref/path/line fields. Reuse it directly for that registered evidence; do not look up registries merely to reconstruct it. For necessary code reads, extract the exact repository, full commit, percent-decoded path and line range @@ -147,8 +163,7 @@ from its recognized blob route into the existing MCP read fields. The service does not implicitly accept a URL as a read request. Do not guess unfamiliar URL formats; use targeted source metadata when needed. A URL is not proof of a read, and evidence read at another commit needs its own citation. -For a scoped repository, consume `extensions["context-evidence"].scopes[].data` -with its `root`: knowledge and snapshot paths are relative to that plugin root; +For a scoped repository, knowledge and snapshot paths are relative to the plugin root; source-code reference paths already include their own source subpath. Do not prepend the knowledge scope to external source paths or repeat either prefix. If the service rejects the plugin option, retry the ordinary read once. If the diff --git a/plugins/context/repo-install/skills/context-inspect-search/SKILL.md b/plugins/context/repo-install/skills/context-inspect-search/SKILL.md index 4a3475c4..be9a3996 100644 --- a/plugins/context/repo-install/skills/context-inspect-search/SKILL.md +++ b/plugins/context/repo-install/skills/context-inspect-search/SKILL.md @@ -127,6 +127,18 @@ matching sections with their conditions, not snippets alone. Batch known related reads within returned limits. Use the remote evidence checks below for knowledge as well as code, including per-item errors, truncation and coverage. A search miss is not evidence that the repository lacks the knowledge. +When the connected schema supports it, use `output: "files"` only when locating +paths is enough; use content excerpts when relevance needs context, bounding +them with `max_lines_per_file` as needed. Do not add a mandatory file-list pass +before every search. Omitted matches and truncation do not establish absence. +If results are too broad, narrow by article path or a distinctive term before +reading more output. For zero hits, simplify combined conditions using the +service's documented syntax; do not assume unsupported OR/regex syntax worked. +Follow the connected tool schema: use search-hit ranges when known; otherwise +omit bounds if the service supplies a bounded default (currently up to 500 lines). +For services requiring endpoints, supply `start_line` and `end_line` per item. +Start with a bounded first window +and follow continuation only when needed, not the whole file by default. Consume recorded evidence already attached to ordinary `read`/`read_many` responses; the repository plugin may be enabled by default. Do not reread just @@ -138,8 +150,12 @@ This returns section sources with the original text; reuse them instead of separately looking up structure and registries. No plugin discovery/help call is required when the name and workspace are known. These are registered references, not verification that original sources were read. Keep original read errors and -truncation checks; inspect extension `issues` only for affected evidence gaps. -Source references may contain a ready commit-specific `url` instead of separate +truncation checks; inspect appended diagnostics only for affected evidence gaps. +Consume `references:` appended to each returned file. These may cover several +sections in that read: not every reference supports every +sentence. Narrow the read or inspect the source when attribution matters. +References may be ready +commit-specific URL strings or contain a `url` instead of separate repository/ref/path/line fields. Reuse it directly for that registered evidence; do not look up registries merely to reconstruct it. For necessary code reads, extract the exact repository, full commit, percent-decoded path and line range @@ -147,8 +163,7 @@ from its recognized blob route into the existing MCP read fields. The service does not implicitly accept a URL as a read request. Do not guess unfamiliar URL formats; use targeted source metadata when needed. A URL is not proof of a read, and evidence read at another commit needs its own citation. -For a scoped repository, consume `extensions["context-evidence"].scopes[].data` -with its `root`: knowledge and snapshot paths are relative to that plugin root; +For a scoped repository, knowledge and snapshot paths are relative to the plugin root; source-code reference paths already include their own source subpath. Do not prepend the knowledge scope to external source paths or repeat either prefix. If the service rejects the plugin option, retry the ordinary read once. If the diff --git a/plugins/context/skills/context-inspect-search/SKILL.md b/plugins/context/skills/context-inspect-search/SKILL.md index 4a3475c4..be9a3996 100644 --- a/plugins/context/skills/context-inspect-search/SKILL.md +++ b/plugins/context/skills/context-inspect-search/SKILL.md @@ -127,6 +127,18 @@ matching sections with their conditions, not snippets alone. Batch known related reads within returned limits. Use the remote evidence checks below for knowledge as well as code, including per-item errors, truncation and coverage. A search miss is not evidence that the repository lacks the knowledge. +When the connected schema supports it, use `output: "files"` only when locating +paths is enough; use content excerpts when relevance needs context, bounding +them with `max_lines_per_file` as needed. Do not add a mandatory file-list pass +before every search. Omitted matches and truncation do not establish absence. +If results are too broad, narrow by article path or a distinctive term before +reading more output. For zero hits, simplify combined conditions using the +service's documented syntax; do not assume unsupported OR/regex syntax worked. +Follow the connected tool schema: use search-hit ranges when known; otherwise +omit bounds if the service supplies a bounded default (currently up to 500 lines). +For services requiring endpoints, supply `start_line` and `end_line` per item. +Start with a bounded first window +and follow continuation only when needed, not the whole file by default. Consume recorded evidence already attached to ordinary `read`/`read_many` responses; the repository plugin may be enabled by default. Do not reread just @@ -138,8 +150,12 @@ This returns section sources with the original text; reuse them instead of separately looking up structure and registries. No plugin discovery/help call is required when the name and workspace are known. These are registered references, not verification that original sources were read. Keep original read errors and -truncation checks; inspect extension `issues` only for affected evidence gaps. -Source references may contain a ready commit-specific `url` instead of separate +truncation checks; inspect appended diagnostics only for affected evidence gaps. +Consume `references:` appended to each returned file. These may cover several +sections in that read: not every reference supports every +sentence. Narrow the read or inspect the source when attribution matters. +References may be ready +commit-specific URL strings or contain a `url` instead of separate repository/ref/path/line fields. Reuse it directly for that registered evidence; do not look up registries merely to reconstruct it. For necessary code reads, extract the exact repository, full commit, percent-decoded path and line range @@ -147,8 +163,7 @@ from its recognized blob route into the existing MCP read fields. The service does not implicitly accept a URL as a read request. Do not guess unfamiliar URL formats; use targeted source metadata when needed. A URL is not proof of a read, and evidence read at another commit needs its own citation. -For a scoped repository, consume `extensions["context-evidence"].scopes[].data` -with its `root`: knowledge and snapshot paths are relative to that plugin root; +For a scoped repository, knowledge and snapshot paths are relative to the plugin root; source-code reference paths already include their own source subpath. Do not prepend the knowledge scope to external source paths or repeat either prefix. If the service rejects the plugin option, retry the ordinary read once. If the