micro-contracts 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli-contract.yaml CHANGED
@@ -650,6 +650,61 @@ command_sets:
650
650
  idempotent: true
651
651
  side_effects: []
652
652
 
653
+ # ── insights ─────────────────────────────────────
654
+ insights:
655
+ summary: Emit ExternalInsight JSON for agent-contracts-analyzer.
656
+ description: >-
657
+ Reads x-micro-contracts-depend-on from OpenAPI specs and outputs
658
+ structured dependency edges and anchor mappings as JSON on stdout.
659
+ Used by agent-contracts-analyzer CommandProvider integration.
660
+ usage:
661
+ - micro-contracts insights --format json
662
+ - micro-contracts insights --format json --project-root .
663
+
664
+ options:
665
+ - name: format
666
+ description: Output format (json only).
667
+ value_name: format
668
+ schema:
669
+ type: string
670
+ default: json
671
+
672
+ - name: project-root
673
+ description: Project root directory containing micro-contracts.config.yaml.
674
+ value_name: path
675
+ schema:
676
+ type: string
677
+ default: .
678
+
679
+ - name: config
680
+ aliases: [c]
681
+ description: Path to config file (micro-contracts.config.yaml).
682
+ value_name: path
683
+ schema:
684
+ type: string
685
+ file:
686
+ mode: read
687
+ exists: false
688
+ media_type: application/yaml
689
+ encoding: utf-8
690
+
691
+ exits:
692
+ '0':
693
+ description: ExternalInsight JSON written to stdout.
694
+ stdout:
695
+ format: json
696
+
697
+ '1':
698
+ description: Insights generation failed.
699
+ stderr:
700
+ format: text
701
+
702
+ x-agent:
703
+ risk_level: low
704
+ requires_confirmation: false
705
+ idempotent: true
706
+ side_effects: []
707
+
653
708
  # ── guardrails-init ──────────────────────────────
654
709
  guardrails-init:
655
710
  path: [guardrails-init]
@@ -0,0 +1,16 @@
1
+ /**
2
+ * External Insight Provider for agent-contracts-analyzer (Issue #24).
3
+ * Exposes x-micro-contracts-depend-on as ExternalInsight edges and anchor mappings.
4
+ */
5
+ import type { ExternalInsight, InsightProvider, InsightQuery } from "agent-contracts-analyzer";
6
+ export declare const MICRO_CONTRACTS_INSIGHT_SOURCE = "micro-contracts";
7
+ export interface BuildInsightOptions {
8
+ configPath?: string;
9
+ }
10
+ export declare function buildExternalInsight(projectRoot: string, options?: BuildInsightOptions): ExternalInsight;
11
+ export declare class MicroContractsInsightProvider implements InsightProvider {
12
+ readonly name = "micro-contracts";
13
+ provide(query: InsightQuery): Promise<ExternalInsight>;
14
+ }
15
+ export declare const microContractsInsightProvider: MicroContractsInsightProvider;
16
+ //# sourceMappingURL=insight-provider.d.ts.map
@@ -0,0 +1,241 @@
1
+ /**
2
+ * External Insight Provider for agent-contracts-analyzer (Issue #24).
3
+ * Exposes x-micro-contracts-depend-on as ExternalInsight edges and anchor mappings.
4
+ */
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { createRequire } from "module";
8
+ import { loadConfig, loadOpenAPISpec } from "../generator/index.js";
9
+ import { extractDependencies } from "../types.js";
10
+ import { isMultiModuleConfig, resolveModuleConfig } from "../types.js";
11
+ const require = createRequire(import.meta.url);
12
+ const pkg = require("../../package.json");
13
+ export const MICRO_CONTRACTS_INSIGHT_SOURCE = "micro-contracts";
14
+ const EVIDENCE_KIND = "api_contract_declaration";
15
+ const EDGE_WEIGHT = 0.9;
16
+ function findConfigFile(projectRoot) {
17
+ const candidates = [
18
+ "micro-contracts.config.yaml",
19
+ "micro-contracts.config.yml",
20
+ "api-framework.config.yaml",
21
+ "api-framework.config.yml",
22
+ ];
23
+ for (const candidate of candidates) {
24
+ const configPath = path.join(projectRoot, candidate);
25
+ if (fs.existsSync(configPath))
26
+ return configPath;
27
+ }
28
+ return null;
29
+ }
30
+ function toProjectRelative(projectRoot, absolutePath) {
31
+ const rel = path.relative(projectRoot, absolutePath);
32
+ return rel.startsWith("..") ? absolutePath : rel.split(path.sep).join("/");
33
+ }
34
+ function dependencyDomainId(dep) {
35
+ return dep.raw;
36
+ }
37
+ function serviceApiSymbolId(contractOutputRel, service, method) {
38
+ return `${contractOutputRel}/services/${service}ServiceApi.ts#${method}`;
39
+ }
40
+ function findDeclarationLine(fileContent, needle) {
41
+ const lines = fileContent.split("\n");
42
+ for (let i = 0; i < lines.length; i++) {
43
+ if (lines[i].includes(needle)) {
44
+ return i + 1;
45
+ }
46
+ }
47
+ return undefined;
48
+ }
49
+ function buildEvidence(detail, openapiRel, openapiContent, needle, symbolId) {
50
+ const line = needle ? findDeclarationLine(openapiContent, needle) : undefined;
51
+ return {
52
+ kind: EVIDENCE_KIND,
53
+ detail,
54
+ filePath: openapiRel,
55
+ ...(line !== undefined ? { line } : {}),
56
+ ...(symbolId !== undefined ? { symbolId } : {}),
57
+ };
58
+ }
59
+ function collectOperationIndex(spec) {
60
+ const index = new Map();
61
+ const httpMethods = ["get", "post", "put", "patch", "delete"];
62
+ for (const pathItem of Object.values(spec.paths)) {
63
+ for (const httpMethod of httpMethods) {
64
+ const operation = pathItem[httpMethod];
65
+ if (!operation?.operationId)
66
+ continue;
67
+ const service = operation["x-micro-contracts-service"];
68
+ const method = operation["x-micro-contracts-method"];
69
+ if (typeof service === "string" && typeof method === "string") {
70
+ index.set(operation.operationId, { service, method });
71
+ }
72
+ }
73
+ }
74
+ return index;
75
+ }
76
+ function operationDomainId(moduleName, service, method) {
77
+ return `${moduleName}.${service}.${method}`;
78
+ }
79
+ function edgeKey(from, to, kind) {
80
+ return `${from}\0${to}\0${kind}`;
81
+ }
82
+ function buildApiRefSymbolAnchor(contractOutputRel, dep) {
83
+ const symbolId = serviceApiSymbolId(contractOutputRel, dep.service, dep.method);
84
+ return {
85
+ symbolId,
86
+ filePath: `${contractOutputRel}/services/${dep.service}ServiceApi.ts`,
87
+ startLine: 1,
88
+ endLine: 1,
89
+ };
90
+ }
91
+ export function buildExternalInsight(projectRoot, options = {}) {
92
+ const resolvedRoot = path.resolve(projectRoot);
93
+ const configPath = options.configPath
94
+ ? path.resolve(options.configPath)
95
+ : findConfigFile(resolvedRoot);
96
+ if (!configPath) {
97
+ throw new Error("micro-contracts.config.yaml not found. Run from project root or pass --config.");
98
+ }
99
+ const config = loadConfig(configPath);
100
+ if (!isMultiModuleConfig(config)) {
101
+ throw new Error("Insight provider requires multi-module config with modules.");
102
+ }
103
+ const configDir = path.dirname(configPath);
104
+ const defaults = config.defaults ?? {};
105
+ const moduleContexts = [];
106
+ const contractOutputByModule = new Map();
107
+ for (const [moduleName, moduleConfig] of Object.entries(config.modules)) {
108
+ const resolved = resolveModuleConfig(moduleName, moduleConfig, defaults);
109
+ const openapiAbs = path.resolve(configDir, resolved.openapi);
110
+ if (!fs.existsSync(openapiAbs)) {
111
+ continue;
112
+ }
113
+ const spec = loadOpenAPISpec(openapiAbs);
114
+ const openapiRel = toProjectRelative(resolvedRoot, openapiAbs);
115
+ const contractRel = toProjectRelative(resolvedRoot, path.resolve(resolvedRoot, resolved.contractOutput));
116
+ contractOutputByModule.set(moduleName, contractRel);
117
+ moduleContexts.push({
118
+ name: moduleName,
119
+ openapiRel,
120
+ resolved,
121
+ spec,
122
+ });
123
+ }
124
+ const edges = [];
125
+ const edgeSeen = new Set();
126
+ const anchorByDomain = new Map();
127
+ const ensureModuleAnchor = (ctx) => {
128
+ if (anchorByDomain.has(ctx.name))
129
+ return;
130
+ anchorByDomain.set(ctx.name, {
131
+ domainId: ctx.name,
132
+ filePaths: [ctx.openapiRel, ctx.resolved.contractOutput],
133
+ });
134
+ };
135
+ const ensureApiRefAnchor = (dep) => {
136
+ const domainId = dependencyDomainId(dep);
137
+ if (anchorByDomain.has(domainId))
138
+ return;
139
+ const contractRel = contractOutputByModule.get(dep.module);
140
+ if (!contractRel)
141
+ return;
142
+ const targetOpenapi = moduleContexts.find((m) => m.name === dep.module);
143
+ const filePaths = targetOpenapi
144
+ ? [targetOpenapi.openapiRel, contractRel]
145
+ : [contractRel];
146
+ anchorByDomain.set(domainId, {
147
+ domainId,
148
+ filePaths,
149
+ symbolIds: [serviceApiSymbolId(contractRel, dep.service, dep.method)],
150
+ symbols: [buildApiRefSymbolAnchor(contractRel, dep)],
151
+ });
152
+ };
153
+ const ensureOperationAnchor = (moduleName, service, method, contractRel, openapiRel) => {
154
+ const domainId = operationDomainId(moduleName, service, method);
155
+ if (anchorByDomain.has(domainId))
156
+ return;
157
+ const symbolId = serviceApiSymbolId(contractRel, service, method);
158
+ anchorByDomain.set(domainId, {
159
+ domainId,
160
+ filePaths: [openapiRel, contractRel],
161
+ symbolIds: [symbolId],
162
+ symbols: [
163
+ {
164
+ symbolId,
165
+ filePath: `${contractRel}/services/${service}ServiceApi.ts`,
166
+ startLine: 1,
167
+ endLine: 1,
168
+ },
169
+ ],
170
+ });
171
+ };
172
+ for (const ctx of moduleContexts) {
173
+ ensureModuleAnchor(ctx);
174
+ const openapiContent = fs.readFileSync(path.resolve(resolvedRoot, ctx.openapiRel), "utf-8");
175
+ const deps = extractDependencies(ctx.spec);
176
+ const contractRel = contractOutputByModule.get(ctx.name);
177
+ const opIndex = collectOperationIndex(ctx.spec);
178
+ for (const dep of deps.moduleLevelDeps) {
179
+ const from = ctx.name;
180
+ const to = dependencyDomainId(dep);
181
+ const kind = "api_dependency";
182
+ const key = edgeKey(from, to, kind);
183
+ if (edgeSeen.has(key))
184
+ continue;
185
+ edgeSeen.add(key);
186
+ ensureApiRefAnchor(dep);
187
+ edges.push({
188
+ from,
189
+ to,
190
+ kind,
191
+ propagation: "forward",
192
+ weight: EDGE_WEIGHT,
193
+ evidence: [
194
+ buildEvidence(`module declares dependency on ${dep.raw}`, ctx.openapiRel, openapiContent, dep.raw),
195
+ ],
196
+ });
197
+ }
198
+ for (const [operationId, opDeps] of deps.operationLevelDeps) {
199
+ const opMeta = opIndex.get(operationId);
200
+ if (!opMeta)
201
+ continue;
202
+ const from = operationDomainId(ctx.name, opMeta.service, opMeta.method);
203
+ ensureOperationAnchor(ctx.name, opMeta.service, opMeta.method, contractRel, ctx.openapiRel);
204
+ for (const dep of opDeps) {
205
+ const to = dependencyDomainId(dep);
206
+ const kind = "api_operation_dependency";
207
+ const key = edgeKey(from, to, kind);
208
+ if (edgeSeen.has(key))
209
+ continue;
210
+ edgeSeen.add(key);
211
+ ensureApiRefAnchor(dep);
212
+ const fromSymbolId = serviceApiSymbolId(contractRel, opMeta.service, opMeta.method);
213
+ edges.push({
214
+ from,
215
+ to,
216
+ kind,
217
+ propagation: "forward",
218
+ weight: EDGE_WEIGHT,
219
+ evidence: [
220
+ buildEvidence(`operation ${from} declares dependency on ${dep.raw}`, ctx.openapiRel, openapiContent, dep.raw, fromSymbolId),
221
+ ],
222
+ });
223
+ }
224
+ }
225
+ }
226
+ return {
227
+ source: MICRO_CONTRACTS_INSIGHT_SOURCE,
228
+ sourceVersion: pkg.version,
229
+ generatedAt: new Date().toISOString(),
230
+ edges,
231
+ anchorMapping: [...anchorByDomain.values()],
232
+ };
233
+ }
234
+ export class MicroContractsInsightProvider {
235
+ name = MICRO_CONTRACTS_INSIGHT_SOURCE;
236
+ async provide(query) {
237
+ return buildExternalInsight(query.projectRoot);
238
+ }
239
+ }
240
+ export const microContractsInsightProvider = new MicroContractsInsightProvider();
241
+ //# sourceMappingURL=insight-provider.js.map