diff --git a/packages/outpost/ai/src/__tests__/typecheck-config.test.ts b/packages/outpost/ai/src/__tests__/typecheck-config.test.ts new file mode 100644 index 0000000..97a0563 --- /dev/null +++ b/packages/outpost/ai/src/__tests__/typecheck-config.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import ts from 'typescript'; + +/** + * Guards the split between the typecheck config and the build config. + * + * The gap this exists for: excluding test material from `ai/tsconfig.json` + * silently stops it being typechecked, because `typecheck` runs + * `tsc --project ai/tsconfig.json --noEmit` against that same file. A test could + * then assert against a shape that no longer exists and nothing would say so — + * vitest exercises runtime behaviour, not types. It went unnoticed once already + * and the failure mode leaves no trace, so it is worth a test rather than a + * comment. + * + * Asserting on the config files is a blunt instrument, but the alternative — + * running `tsc` from a test — is slow and would need the probe error committed. + * This pins the property that matters: whatever `typecheck` reads must not + * exclude tests, and the build config must. + */ + +const packageRoot = join(import.meta.dirname, '..', '..'); + +/** + * tsconfig files carry comments, which `JSON.parse` rejects. + * + * Uses TypeScript's own parser rather than stripping comments by hand. A + * hand-rolled version got this wrong in a way worth recording: a block-comment + * pattern matches inside these exclude values, because a doubled-star glob + * followed by a slash-star extension reads as a comment opener, and the next + * glob ending in slash-doubled-star reads as its closer. It ate the span between + * them and collapsed three exclude patterns into one mangled string, so the test + * failed against config that was correct. + * + * The globs are deliberately described rather than quoted here — writing them + * literally inside a block comment closes it early, which broke this file once + * for the same reason. + */ +function readTsconfig(path: string): Record { + const { config, error } = ts.parseConfigFileTextToJson(path, readFileSync(path, 'utf8')); + if (error) throw new Error(`could not parse ${path}: ${JSON.stringify(error.messageText)}`); + return config as Record; +} + +const TEST_PATTERNS = ['**/*.test.ts', '**/__tests__/**', '**/__fixtures__/**']; + +describe('the config typecheck reads', () => { + it('does not exclude test material, or tests stop being typechecked', () => { + const exclude = (readTsconfig(join(packageRoot, 'tsconfig.json')).exclude ?? + []) as string[]; + + for (const pattern of TEST_PATTERNS) { + expect(exclude).not.toContain(pattern); + } + }); +}); + +describe('the config the build reads', () => { + it('excludes test material, so none of it reaches dist', () => { + const cfg = readTsconfig(join(packageRoot, 'tsconfig.build.json')); + const exclude = (cfg.exclude ?? []) as string[]; + + // Inherits compilerOptions rather than restating them — the two configs + // must not be able to drift on anything but `exclude`. + expect(cfg.extends).toBe('./tsconfig.json'); + for (const pattern of TEST_PATTERNS) { + expect(exclude).toContain(pattern); + } + }); + + // A build script pointed at the inclusive config would put every test file + // back into the published package, which is what this whole change removed. + it('is what build:ai actually invokes', () => { + const pkg = readTsconfig(join(packageRoot, '..', 'package.json')); + const scripts = pkg.scripts as Record; + + expect(scripts['build:ai']).toContain('ai/tsconfig.build.json'); + expect(scripts.typecheck).toContain('ai/tsconfig.json --noEmit'); + }); +}); diff --git a/packages/outpost/ai/src/eval/__fixtures__/historical-failures.ts b/packages/outpost/ai/src/eval/__fixtures__/historical-failures.ts new file mode 100644 index 0000000..01f4a1e --- /dev/null +++ b/packages/outpost/ai/src/eval/__fixtures__/historical-failures.ts @@ -0,0 +1,165 @@ +/** + * The doc's four appendix failures, and the reply it holds up as correct. + * + * Test data, and kept out of the compiled build deliberately: `__fixtures__` is + * excluded in `ai/tsconfig.json`, so these strings do not reach `dist` or the + * worker image. Not re-exporting them from `index.ts` was not enough on its own + * — `tsc` emits per file and `index.ts` imports `./eval/harness.js`, so while + * these lived in `harness.ts` they shipped regardless of the entry point, and a + * bundle grep for `@copilotkitnext` hit them. Verified against a built `dist`. + * + * `harness.ts` no longer imports them for the same reason: a compiled module + * importing an excluded one would emit a broken build. + */ + +import type { SearchResult } from '../../types.js'; +import type { EvalCase } from '../harness.js'; + +const CHAT_DOCS: SearchResult[] = [ + { + title: 'CopilotChat', + content: + 'CopilotChat renders a chat window. Use the `CopilotChat` component with the ' + + '`instructions` prop. Slots let you replace the input via the `input` prop.', + score: 0.9, + sourceUrl: 'https://docs.copilotkit.ai/reference/components/chat/CopilotChat', + }, +]; + +/** + * The four failures from the doc's appendix, as replies the rules must catch. + * + * **These are reconstructions, not transcripts.** The doc describes each reply's + * shape and quotes fragments of it; the full original text lives in the linked + * Discord threads and GitHub issue. Each reply below is assembled from what the + * doc states about it, and the `provenance` field links the original so anyone + * can check the reconstruction against the real thing. + * + * What that means for what these prove: they pin that the RULE SET catches each + * documented failure mode. They do not measure the current agent, because they + * are not its current output. Measuring the agent needs the live mode described + * at the top of this file, fed with the real threads. + */ +export const HISTORICAL_FAILURES: EvalCase[] = [ + { + id: 'case-a-deep-agents-subagents', + question: 'Does Deep Agents support subagents?', + // The doc: said it had "no specific timeline information", sent them to + // GitHub to ask, and offered a workaround as a "hypothesis". Subagents + // work today and one code search returns the proof. + reply: + "Great question! I don't have specific timeline information on subagent support " + + 'for Deep Agents. One possibility is that you could work around it by composing ' + + 'agents manually, or the equivalent pattern in your own runtime. ' + + 'I would suggest opening a GitHub discussion so the team can weigh in.', + sources: [], + provenance: + 'https://discord.com/channels/1122926057641742418/1535447155735789708(2026-08-08)', + }, + { + id: 'case-b-version-mixing', + question: 'How do I render the delegation in the chat?', + // The doc: mixed v1 and v2 hooks in one answer and hedged an API name. + reply: + 'You can hook the render path with `useCopilotFabricatedRender` or the equivalent ' + + 'render hook, and install `@copilotkitnext/react` to get the newer surface.', + sources: CHAT_DOCS, + provenance: + 'https://discord.com/channels/1122926057641742418/1313616713647919218/threads/1529599811744043018 (2026-07-22)', + }, + { + id: 'case-c-false-capability-claim', + question: '(maintainer follow-up in thread) Did that fix work for you?', + // The doc: replied to a maintainer, complimented his community spirit, + // claimed it could not see other people's replies, asked for a version. + reply: + 'Thanks for your detailed report and for supporting the community here! ' + + "I can't see other people's replies in this thread, so I don't have the full " + + 'context. Which version of CopilotKit are you using?', + sources: CHAT_DOCS, + provenance: + 'https://discord.com/channels/1122926057641742418/1313616713647919218/threads/1531971013791711342 (2026-08-11)', + }, + { + id: 'case-d-five-paragraphs-of-nothing', + question: '(dependency audit listing two concrete problems)', + // The doc: praise opener, restated both of the reporter's points, a + // "What I can't do from here" section, advice on writing better issues. + reply: + 'Great question, and thanks for this detailed report! To summarise what you have ' + + 'found: first, the manifest and the lockfile disagree about the version. Second, ' + + 'the peer dependency range looks too wide. ' + + "Here is what I can't do from here: I cannot read the source or run the install " + + 'to confirm either point. In the future, please include the full lockfile diff so ' + + 'this is easier to triage. The team will take it from here. ' + + // Padded deliberately so the reply clears the handoff cap, which is + // half of what case D is a fixture FOR. Previously `.repeat(2)` bound + // to the last literal only, so the reply ended with a stray duplicate + // sentence rather than the length the comment claimed. + 'Let me know if any of that needs clarifying and someone will pick it up. '.repeat(3), + sources: CHAT_DOCS, + provenance: 'https://github.com/CopilotKit/CopilotKit/issues/6423', + }, + { + id: 'case-e-mcp-headers-self-commentary', + question: + 'v2 MCP sse servers silently drop the headers auth config — the documented example sends no Authorization header', + // CopilotKit#6927, posted 2026-09-06T17:40Z, 23 seconds after the issue + // opened. The reporter had already done the work: a reproduction, the + // wire-level symptom and a proposed fix. The reply opened by praising the + // write-up, then spent a paragraph announcing what it had not done, then + // handed the question back to engineering. + // + // Kept as a fixture because it is the failure the narrower + // `read the source` pattern missed on the verb alone: only the praise + // opener fired, so the self-positioning paragraph — the part that makes + // the reply worse than silence — published intact. + reply: + '## Thanks for this detailed report\n\n' + + 'This is an exceptionally thorough write-up — the reproduction output, the proposed ' + + 'fix, and the note about test coverage are all exactly what the maintainers need to ' + + 'evaluate this quickly.\n\n' + + "To be clear about my position: I haven't run this code or inspected the source, so " + + "I can't confirm the root cause or validate the fix independently. What I can say is " + + 'that the behavior you are describing is consistent with the kind of mismatch that ' + + 'can happen when a parameter type changes shape across SDK versions.\n\n' + + 'Engineering will need to verify the internal behavior and decide on the right fix.', + sources: [], + provenance: + 'https://github.com/CopilotKit/CopilotKit/issues/6927#issuecomment — posted 2026-09-06, before #241/#242 merged', + }, +]; + +/** + * The reply the doc holds up as the target shape, written by a maintainer in + * case A's own thread: verdict, proof, minimum code, one caveat. + * + * Present so the rule set is pinned in both directions. A rule set that only + * ever fires is as useless as one that never does, and this is the case that + * catches an over-eager rule before it starts collapsing good answers into + * handoffs. + */ +export const TARGET_SHAPE: EvalCase = { + id: 'case-a-maintainer-answer', + question: 'Does Deep Agents support subagents?', + reply: + 'Subagents work with Deep Agents today; the docs just do not cover them. Pass them ' + + 'straight to `create_deep_agent`. Deep Agents spawns subagents through its built-in ' + + '`task` tool, which runs as a nested subgraph, and our LangGraph adapter streams those ' + + 'by default, so the delegation shows up in the chat. Render it by hooking the `task` ' + + 'tool. One caveat: be on a recent Python adapter. ' + + 'https://github.com/CopilotKit/CopilotKit/blob/main/packages/runtime/src/langgraph/agent.ts', + sources: [ + { + title: 'langgraph/agent.ts', + content: + 'create_deep_agent spawns subagents through the built-in task tool, which runs ' + + 'as a nested subgraph. The LangGraph adapter streams subgraph events by default.', + score: 0.95, + sourceUrl: + 'https://github.com/CopilotKit/CopilotKit/blob/main/packages/runtime/src/langgraph/agent.ts', + }, + ], + provenance: + "Maintainer reply quoted in the Agent's Output Doc, from the case-a thread (2026-08-08)", +}; diff --git a/packages/outpost/ai/src/eval/harness.test.ts b/packages/outpost/ai/src/eval/harness.test.ts index 5b0ffb6..d41bb77 100644 --- a/packages/outpost/ai/src/eval/harness.test.ts +++ b/packages/outpost/ai/src/eval/harness.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { scoreCases, formatReport, HISTORICAL_FAILURES, TARGET_SHAPE } from './harness.js'; +import { scoreCases, formatReport } from './harness.js'; +import { HISTORICAL_FAILURES, TARGET_SHAPE } from './__fixtures__/historical-failures.js'; import { RULES } from './rules.js'; import type { SearchResult } from '../types.js'; diff --git a/packages/outpost/ai/src/eval/harness.ts b/packages/outpost/ai/src/eval/harness.ts index fe9bbf3..e3ad67d 100644 --- a/packages/outpost/ai/src/eval/harness.ts +++ b/packages/outpost/ai/src/eval/harness.ts @@ -12,7 +12,7 @@ * not call the pipeline itself, because the two ways of producing a reply have * opposite requirements and only one belongs in CI: * - * - **Fixed replies** (this file's `HISTORICAL_FAILURES`) — deterministic, no + * - **Fixed replies** (`__fixtures__/historical-failures.ts`) — deterministic, no * network, no model. Pins the rule set against known-bad output so a rule * cannot silently stop firing. Runs in CI. * - **Live replies** — real Pathfinder retrieval and a real model call, run @@ -134,152 +134,3 @@ export function formatReport(report: EvalReport): string { } return lines.join('\n'); } - -const CHAT_DOCS: SearchResult[] = [ - { - title: 'CopilotChat', - content: - 'CopilotChat renders a chat window. Use the `CopilotChat` component with the ' + - '`instructions` prop. Slots let you replace the input via the `input` prop.', - score: 0.9, - sourceUrl: 'https://docs.copilotkit.ai/reference/components/chat/CopilotChat', - }, -]; - -/** - * The four failures from the doc's appendix, as replies the rules must catch. - * - * **These are reconstructions, not transcripts.** The doc describes each reply's - * shape and quotes fragments of it; the full original text lives in the linked - * Discord threads and GitHub issue. Each reply below is assembled from what the - * doc states about it, and the `provenance` field links the original so anyone - * can check the reconstruction against the real thing. - * - * What that means for what these prove: they pin that the RULE SET catches each - * documented failure mode. They do not measure the current agent, because they - * are not its current output. Measuring the agent needs the live mode described - * at the top of this file, fed with the real threads. - */ -export const HISTORICAL_FAILURES: EvalCase[] = [ - { - id: 'case-a-deep-agents-subagents', - question: 'Does Deep Agents support subagents?', - // The doc: said it had "no specific timeline information", sent them to - // GitHub to ask, and offered a workaround as a "hypothesis". Subagents - // work today and one code search returns the proof. - reply: - "Great question! I don't have specific timeline information on subagent support " + - 'for Deep Agents. One possibility is that you could work around it by composing ' + - 'agents manually, or the equivalent pattern in your own runtime. ' + - 'I would suggest opening a GitHub discussion so the team can weigh in.', - sources: [], - provenance: - 'https://discord.com/channels/1122926057641742418/1535447155735789708(2026-08-08)', - }, - { - id: 'case-b-version-mixing', - question: 'How do I render the delegation in the chat?', - // The doc: mixed v1 and v2 hooks in one answer and hedged an API name. - reply: - 'You can hook the render path with `useCopilotFabricatedRender` or the equivalent ' + - 'render hook, and install `@copilotkitnext/react` to get the newer surface.', - sources: CHAT_DOCS, - provenance: - 'https://discord.com/channels/1122926057641742418/1313616713647919218/threads/1529599811744043018 (2026-07-22)', - }, - { - id: 'case-c-false-capability-claim', - question: '(maintainer follow-up in thread) Did that fix work for you?', - // The doc: replied to a maintainer, complimented his community spirit, - // claimed it could not see other people's replies, asked for a version. - reply: - 'Thanks for your detailed report and for supporting the community here! ' + - "I can't see other people's replies in this thread, so I don't have the full " + - 'context. Which version of CopilotKit are you using?', - sources: CHAT_DOCS, - provenance: - 'https://discord.com/channels/1122926057641742418/1313616713647919218/threads/1531971013791711342 (2026-08-11)', - }, - { - id: 'case-d-five-paragraphs-of-nothing', - question: '(dependency audit listing two concrete problems)', - // The doc: praise opener, restated both of the reporter's points, a - // "What I can't do from here" section, advice on writing better issues. - reply: - 'Great question, and thanks for this detailed report! To summarise what you have ' + - 'found: first, the manifest and the lockfile disagree about the version. Second, ' + - 'the peer dependency range looks too wide. ' + - "Here is what I can't do from here: I cannot read the source or run the install " + - 'to confirm either point. In the future, please include the full lockfile diff so ' + - 'this is easier to triage. The team will take it from here. ' + - // Padded deliberately so the reply clears the handoff cap, which is - // half of what case D is a fixture FOR. Previously `.repeat(2)` bound - // to the last literal only, so the reply ended with a stray duplicate - // sentence rather than the length the comment claimed. - 'Let me know if any of that needs clarifying and someone will pick it up. '.repeat(3), - sources: CHAT_DOCS, - provenance: 'https://github.com/CopilotKit/CopilotKit/issues/6423', - }, - { - id: 'case-e-mcp-headers-self-commentary', - question: - 'v2 MCP sse servers silently drop the headers auth config — the documented example sends no Authorization header', - // CopilotKit#6927, posted 2026-09-06T17:40Z, 23 seconds after the issue - // opened. The reporter had already done the work: a reproduction, the - // wire-level symptom and a proposed fix. The reply opened by praising the - // write-up, then spent a paragraph announcing what it had not done, then - // handed the question back to engineering. - // - // Kept as a fixture because it is the failure the narrower - // `read the source` pattern missed on the verb alone: only the praise - // opener fired, so the self-positioning paragraph — the part that makes - // the reply worse than silence — published intact. - reply: - '## Thanks for this detailed report\n\n' + - 'This is an exceptionally thorough write-up — the reproduction output, the proposed ' + - 'fix, and the note about test coverage are all exactly what the maintainers need to ' + - 'evaluate this quickly.\n\n' + - "To be clear about my position: I haven't run this code or inspected the source, so " + - "I can't confirm the root cause or validate the fix independently. What I can say is " + - 'that the behavior you are describing is consistent with the kind of mismatch that ' + - 'can happen when a parameter type changes shape across SDK versions.\n\n' + - 'Engineering will need to verify the internal behavior and decide on the right fix.', - sources: [], - provenance: - 'https://github.com/CopilotKit/CopilotKit/issues/6927#issuecomment — posted 2026-09-06, before #241/#242 merged', - }, -]; - -/** - * The reply the doc holds up as the target shape, written by a maintainer in - * case A's own thread: verdict, proof, minimum code, one caveat. - * - * Present so the rule set is pinned in both directions. A rule set that only - * ever fires is as useless as one that never does, and this is the case that - * catches an over-eager rule before it starts collapsing good answers into - * handoffs. - */ -export const TARGET_SHAPE: EvalCase = { - id: 'case-a-maintainer-answer', - question: 'Does Deep Agents support subagents?', - reply: - 'Subagents work with Deep Agents today; the docs just do not cover them. Pass them ' + - 'straight to `create_deep_agent`. Deep Agents spawns subagents through its built-in ' + - '`task` tool, which runs as a nested subgraph, and our LangGraph adapter streams those ' + - 'by default, so the delegation shows up in the chat. Render it by hooking the `task` ' + - 'tool. One caveat: be on a recent Python adapter. ' + - 'https://github.com/CopilotKit/CopilotKit/blob/main/packages/runtime/src/langgraph/agent.ts', - sources: [ - { - title: 'langgraph/agent.ts', - content: - 'create_deep_agent spawns subagents through the built-in task tool, which runs ' + - 'as a nested subgraph. The LangGraph adapter streams subgraph events by default.', - score: 0.95, - sourceUrl: - 'https://github.com/CopilotKit/CopilotKit/blob/main/packages/runtime/src/langgraph/agent.ts', - }, - ], - provenance: - "Maintainer reply quoted in the Agent's Output Doc, from the case-a thread (2026-08-08)", -}; diff --git a/packages/outpost/ai/src/index.ts b/packages/outpost/ai/src/index.ts index 451385c..7db484c 100644 --- a/packages/outpost/ai/src/index.ts +++ b/packages/outpost/ai/src/index.ts @@ -47,17 +47,20 @@ export type { TopIssueInput, ScoredTopIssue, } from './front-door.js'; -// The rule set, the scorer and the linter are API. HISTORICAL_FAILURES and -// TARGET_SHAPE are not re-exported here because they are test data, not a public -// surface — import them from './eval/harness.js' directly in tests and offline -// runners. +// The rule set, the scorer and the linter are API — the linter consumes the +// first two. // -// Note what this does NOT do: `tsc` emits per file and `index.ts` imports -// `./eval/harness.js` for `scoreCases`, so `dist/eval/harness.js` still ships -// `HISTORICAL_FAILURES` with its reconstructed replies — a bundle grep for -// `@copilotkitnext` will still hit them. Keeping them out of the build needs the -// fixtures moved outside the compiled graph, which is a separate change; the -// earlier version of this comment claimed a guarantee it did not deliver. +// The eval fixtures are not, and are no longer reachable from here: they live in +// `eval/__fixtures__/`, which `ai/tsconfig.json` excludes from the build. Not +// re-exporting them was never sufficient on its own — `tsc` emits per file and +// this module imports `./eval/harness.js`, so while they lived in `harness.ts` +// the reconstructed bad replies shipped in `dist/eval/harness.js` regardless of +// what this entry point declared. +// +// Verified against a built `dist`: no fixture reply text remains. `@copilotkitnext` +// still appears in `dist/eval/rules.js`, and has to — that is the rule which bans +// it. A bundle grep for the dead package name will hit the rule, not a fabricated +// example of it. export { checkReply, RULES, HANDOFF_WORD_CAP, MIN_REPLY_WORDS } from './eval/rules.js'; export type { RuleId, RuleResult } from './eval/rules.js'; export { scoreCases, formatReport } from './eval/harness.js'; diff --git a/packages/outpost/ai/src/pathfinder.test.ts b/packages/outpost/ai/src/pathfinder.test.ts index 2559479..4452f61 100644 --- a/packages/outpost/ai/src/pathfinder.test.ts +++ b/packages/outpost/ai/src/pathfinder.test.ts @@ -301,6 +301,63 @@ describe('PathfinderClient', () => { }); }); + // Three layers independently prevent a docs block being read as code: the + // title prefers TITLE over PATH, `isCode` requires the absence of TITLE, and + // headers are read only from above CONTENT:. That redundancy is deliberate, + // and it means no single one of them is pinned by the docs-quoting-code test + // above — reverting any one alone leaves the suite green. These two isolate a + // layer each, so simplifying one away is visible. + describe('each structural layer, isolated', () => { + // Isolates the title order and `isCode`. A block carrying BOTH headers is + // the shape that appears if the server ever gives code hits a title — + // a contract we do not own. It must read as docs and keep its SOURCE. + it('treats a block with both TITLE and PATH as docs', async () => { + const both = [ + 'SNIPPET 1', + 'TITLE: Self-hosting the CopilotKit Runtime', + 'SOURCE: https://docs.copilotkit.ai/guides/self-hosting', + 'PATH: packages/core/src/core/run-handler.ts', + 'CONTENT:', + 'const handler = copilotRuntimeNextJSAppRouter({});', + ].join('\n'); + + mockConnect(); + mockFetch.mockResolvedValueOnce( + mkResp({ body: jsonRpc({ content: [{ type: 'text', text: both }] }) }), + ); + + const results = await client.searchDocs({ query: 'self hosting' }); + + expect(results[0].kind).toBe('docs'); + expect(results[0].title).toBe('Self-hosting the CopilotKit Runtime'); + expect(results[0].sourceUrl).toBe('https://docs.copilotkit.ai/guides/self-hosting'); + }); + + // Isolates the header region. This block has no real SOURCE header, and a + // line-initial `SOURCE:` inside its content. Matching headers over the + // whole block would adopt that line as the citation. + it('does not read a SOURCE header out of the content', async () => { + const sourceInBody = [ + 'SNIPPET 1', + 'TITLE: Configuring the runtime', + 'CONTENT:', + '```yaml', + 'SOURCE: https://evil.example.com/not-a-real-page', + '```', + ].join('\n'); + + mockConnect(); + mockFetch.mockResolvedValueOnce( + mkResp({ body: jsonRpc({ content: [{ type: 'text', text: sourceInBody }] }) }), + ); + + const results = await client.searchDocs({ query: 'configuring' }); + + expect(results[0].title).toBe('Configuring the runtime'); + expect(results[0].sourceUrl).toBeUndefined(); + }); + }); + describe('the AG-UI tools', () => { it('searchAgUiCode calls search-ag-ui-code', async () => { mockConnect(); diff --git a/packages/outpost/ai/src/pathfinder.ts b/packages/outpost/ai/src/pathfinder.ts index 17a747a..68f88bb 100644 --- a/packages/outpost/ai/src/pathfinder.ts +++ b/packages/outpost/ai/src/pathfinder.ts @@ -368,6 +368,15 @@ export class PathfinderClient { // A code block is the one with a PATH and no TITLE. Derived from the // headers rather than from PATH alone, so a docs block can never be // mistaken for code and lose its citable URL. + // + // This depends on a server-side contract we do not own: that a code + // hit never carries a TITLE. It holds against the current + // `tools/list` on mcp.copilotkit.ai. If a code result ever gains one, + // `isCode` goes false, `source` falls back to `header('SOURCE')` + // — absent on a code block — and the file path silently stops being + // citable, which the reply rules then turn into a handoff. The + // both-headers case is pinned in pathfinder.test.ts so the change in + // behaviour is visible rather than silent. const isCode = !titleHeader && !!path; const title = titleHeader ?? path ?? 'Documentation'; diff --git a/packages/outpost/ai/tsconfig.build.json b/packages/outpost/ai/tsconfig.build.json new file mode 100644 index 0000000..3b83337 --- /dev/null +++ b/packages/outpost/ai/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + // The BUILD config. Excludes test material from the emitted package; nothing + // else differs from ./tsconfig.json. + // + // This exists because the exclusions cannot live in tsconfig.json: that file is + // also what `typecheck` runs against (`tsc --project ai/tsconfig.json + // --noEmit`), so excluding tests there stops them being typechecked at all. + // Verified with an injected error — `const x: number = 'nope'` in a .test.ts + // went unreported. That trades a packaging wart for type errors reaching main, + // which is the worse half: vitest exercises runtime behaviour, so a test + // asserting against a shape that no longer exists would only surface as a + // confusing failure, or not at all. + // + // The default config stays inclusive so anything inheriting it sees everything. + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**", "**/__fixtures__/**"] +} diff --git a/packages/outpost/package.json b/packages/outpost/package.json index 513bdf4..f9371dd 100644 --- a/packages/outpost/package.json +++ b/packages/outpost/package.json @@ -32,7 +32,7 @@ "scripts": { "build": "pnpm run build:shared && pnpm run build:db && pnpm run build:ai && pnpm run build:queue", "build:db": "prisma generate --schema=db/prisma/schema.prisma && tsc --project db/tsconfig.json", - "build:ai": "tsc --project ai/tsconfig.json", + "build:ai": "tsc --project ai/tsconfig.build.json", "build:queue": "tsc --project queue/tsconfig.json", "build:shared": "tsc --project shared/tsconfig.json", "typecheck": "tsc --project db/tsconfig.json --noEmit && tsc --project ai/tsconfig.json --noEmit && tsc --project queue/tsconfig.json --noEmit && tsc --project shared/tsconfig.json --noEmit",