Skip to content

Commit 4e579b4

Browse files
salmad3claude
andcommitted
feat: address all P1 engineering gaps
- Inference engine: 41 unit tests covering type/audience/answers inference, document annotations, tier assignment, immutability - MCP server: list_skills and get_related tools expose knowledge graph edges to agents at runtime - Agent skills: interface contracts with endpoint method, path, parameters, auth requirements, and deprecation status from OpenAPI specs - Coverage agent: outputs adaptation-report.json compatible with nous apply, unifying the two suggestion pipelines - Rate limiter: applied to all MCP tools, not just search_docs - Type safety: OpenAPI spec validation replaces as-unknown-as cast - Apply command: test coverage for heading matching and annotation formatting 714 tests passing across 13 packages. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 396df3b commit 4e579b4

File tree

9 files changed

+913
-6
lines changed

9 files changed

+913
-6
lines changed

packages/agent-metadata/src/emitters/agent-skills.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ function deriveSkills(
105105
? confidences.reduce((a, b) => a + b, 0) / confidences.length
106106
: 0.5;
107107

108+
// Collect interface contracts from API-derived blocks
109+
const endpoints = agentBlocks
110+
.filter((b) => b.endpoint !== undefined)
111+
.map((b) => b.endpoint!);
112+
108113
skills.push({
109114
id: skillId,
110115
name: doc.title,
@@ -115,6 +120,7 @@ function deriveSkills(
115120
examples: examples.length > 0 ? examples : undefined,
116121
confidence: Math.round(avgConfidence * 100) / 100,
117122
tags: tags.size > 0 ? [...tags] : undefined,
123+
endpoints: endpoints.length > 0 ? endpoints : undefined,
118124
});
119125
}
120126

packages/agent-metadata/src/extractors/index.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import type { DocumentNode, NousConfig } from '@nousdev/types';
99
import type { ExtractedMetadata, ExtractedRelationship, DocumentMetadata } from '../types.js';
1010
import { extractDocumentMetadata } from './document.js';
1111
import { extractRelationships } from './relationships.js';
12-
import { extractFromOpenApi } from './openapi.js';
12+
import { extractFromOpenApi, type OpenApiSpec } from './openapi.js';
1313
import { linkSpecAndProse } from './spec-prose-linker.js';
1414
import { buildSpecVocabulary, extractProseKnowledge } from './prose-knowledge.js';
1515

1616
export { extractInlineText, extractBlockText, estimateTokens, countWords } from './text.js';
1717
export { extractDocumentMetadata } from './document.js';
1818
export { inferAnnotations } from './infer-annotations.js';
1919
export { extractRelationships } from './relationships.js';
20-
export { extractFromOpenApi } from './openapi.js';
20+
export { extractFromOpenApi, type OpenApiSpec } from './openapi.js';
2121
export { linkSpecAndProse } from './spec-prose-linker.js';
2222

2323
export interface ExtractAllOptions {
@@ -27,6 +27,28 @@ export interface ExtractAllOptions {
2727
readonly rawTexts?: ReadonlyMap<string, string>;
2828
}
2929

30+
/**
31+
* Runtime guard that validates an unknown record contains the required
32+
* OpenAPI spec fields: openapi, info.title, info.version, and paths.
33+
* Returns the typed spec on success, undefined on failure.
34+
*/
35+
function asOpenApiSpec(value: Record<string, unknown>): OpenApiSpec | undefined {
36+
if (!value || typeof value !== 'object') return undefined;
37+
if (typeof value.openapi !== 'string') return undefined;
38+
39+
const info = value.info;
40+
if (!info || typeof info !== 'object') return undefined;
41+
const infoObj = info as Record<string, unknown>;
42+
if (typeof infoObj.title !== 'string') return undefined;
43+
if (typeof infoObj.version !== 'string') return undefined;
44+
45+
if (value.paths !== undefined && (typeof value.paths !== 'object' || value.paths === null)) {
46+
return undefined;
47+
}
48+
49+
return value as unknown as OpenApiSpec;
50+
}
51+
3052
/** Extract metadata from all documents in a build. */
3153
export function extractAll(
3254
documents: readonly DocumentNode[],
@@ -41,7 +63,9 @@ export function extractAll(
4163
const specRelationships: ExtractedRelationship[] = [];
4264

4365
for (const spec of options?.openapiSpecs ?? []) {
44-
const extracted = extractFromOpenApi(spec as unknown as Parameters<typeof extractFromOpenApi>[0]);
66+
const validated = asOpenApiSpec(spec);
67+
if (!validated) continue;
68+
const extracted = extractFromOpenApi(validated);
4569
specDocs.push(...extracted.documents);
4670
specRelationships.push(...extracted.relationships);
4771
}

packages/agent-metadata/src/extractors/openapi.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {
2424
} from '../types.js';
2525

2626

27-
interface OpenApiSpec {
27+
export interface OpenApiSpec {
2828
readonly openapi: string;
2929
readonly info: {
3030
readonly title: string;
@@ -360,6 +360,22 @@ function groupOperationsIntoDocuments(
360360
},
361361
codeBlocks: [],
362362
sourcePath: `openapi:${tag}`,
363+
endpoint: {
364+
method: op.method,
365+
path: op.path,
366+
summary: op.summary || op.operationId,
367+
parameters: op.parameters.map((p) => ({
368+
name: p.name,
369+
in: p.in,
370+
required: p.required ?? false,
371+
type: p.schema?.type ?? 'string',
372+
description: p.description,
373+
})),
374+
requestBodySchema: op.referencedSchemas[0],
375+
responseSchema: op.referencedSchemas[0],
376+
authRequired: op.securitySchemes.length > 0,
377+
deprecated: op.deprecated,
378+
},
363379
}));
364380

365381
documents.push({

packages/agent-metadata/src/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface ExtractedBlock {
2626
readonly startLine: number;
2727
readonly endLine: number;
2828
};
29+
/** Interface contract for API-derived blocks. */
30+
readonly endpoint?: SkillEndpoint;
2931
}
3032

3133
/** A code block extracted with its language and raw value. */
@@ -340,6 +342,28 @@ export interface AgentSkill {
340342
readonly examples?: readonly string[];
341343
readonly confidence: number;
342344
readonly tags?: readonly string[];
345+
/** Interface contracts for API-derived skills. */
346+
readonly endpoints?: readonly SkillEndpoint[];
347+
}
348+
349+
/** Machine-actionable endpoint definition within a skill. */
350+
export interface SkillEndpoint {
351+
readonly method: string;
352+
readonly path: string;
353+
readonly summary: string;
354+
readonly parameters?: readonly SkillParameter[];
355+
readonly requestBodySchema?: string;
356+
readonly responseSchema?: string;
357+
readonly authRequired: boolean;
358+
readonly deprecated: boolean;
359+
}
360+
361+
export interface SkillParameter {
362+
readonly name: string;
363+
readonly in: string;
364+
readonly required: boolean;
365+
readonly type: string;
366+
readonly description?: string;
343367
}
344368

345369
export interface SkillEdge {

packages/agents/src/coverage-agent.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,38 @@ export async function runCoverageAgent(
202202
suggestions: allSuggestions,
203203
};
204204

205-
// Write report
205+
// Write coverage report
206206
const reportPath = join(outDir, 'coverage-report.json');
207207
await writeFile(reportPath, JSON.stringify(report, null, 2), 'utf-8');
208208

209+
// Also write in adaptation-report format so `nous apply` can consume it
210+
const adaptationSuggestions = allSuggestions.map((s) => ({
211+
chunkId: `coverage-${s.filePath}-${s.section}`.replace(/[^a-z0-9-]/gi, '-'),
212+
documentPath: s.filePath,
213+
section: s.section,
214+
suggestionType: 'add_answers' as const,
215+
detail: s.reasoning,
216+
priority: s.confidence,
217+
suggestedAnnotation: `{answers: "${s.suggestedAnswers}"}`,
218+
}));
219+
220+
const adaptationReport = {
221+
generatedAt: report.generatedAt,
222+
source: 'coverage-agent',
223+
suggestions: adaptationSuggestions,
224+
metrics: {
225+
filesAnalyzed: report.filesAnalyzed,
226+
totalSuggestions: report.totalSuggestions,
227+
},
228+
};
229+
230+
const adaptationPath = join(outDir, 'adaptation-report.json');
231+
await writeFile(adaptationPath, JSON.stringify(adaptationReport, null, 2), 'utf-8');
232+
209233
console.log('');
210234
console.log(`✓ Coverage Report: ${report.totalSuggestions} suggestions across ${report.filesAnalyzed} files`);
211235
console.log(` Saved to ${reportPath}`);
236+
console.log(` Compatible with \`nous apply\`: ${adaptationPath}`);
212237

213238
return report;
214239
}

0 commit comments

Comments
 (0)