micro-contracts 0.17.1 → 0.17.3

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]
@@ -828,6 +883,12 @@ command_sets:
828
883
  enum: [json, text, yaml]
829
884
  default: text
830
885
 
886
+ - name: log-file
887
+ aliases: [l]
888
+ description: Write agent progress log to this file path.
889
+ schema:
890
+ type: string
891
+
831
892
  exits:
832
893
  '0':
833
894
  description: Audit completed, no blocking findings.
@@ -950,6 +1011,12 @@ command_sets:
950
1011
  enum: [json, text, yaml]
951
1012
  default: text
952
1013
 
1014
+ - name: log-file
1015
+ aliases: [l]
1016
+ description: Write agent progress log to this file path.
1017
+ schema:
1018
+ type: string
1019
+
953
1020
  exits:
954
1021
  '0':
955
1022
  description: Review completed, no blocking findings.
@@ -1073,6 +1140,12 @@ command_sets:
1073
1140
  enum: [json, text, yaml]
1074
1141
  default: json
1075
1142
 
1143
+ - name: log-file
1144
+ aliases: [l]
1145
+ description: Write agent progress log to this file path.
1146
+ schema:
1147
+ type: string
1148
+
1076
1149
  exits:
1077
1150
  '0':
1078
1151
  description: Proposal generated successfully.
@@ -1200,6 +1273,12 @@ command_sets:
1200
1273
  enum: [json, text, yaml]
1201
1274
  default: text
1202
1275
 
1276
+ - name: log-file
1277
+ aliases: [l]
1278
+ description: Write agent progress log to this file path.
1279
+ schema:
1280
+ type: string
1281
+
1203
1282
  exits:
1204
1283
  '0':
1205
1284
  description: Audit completed, no blocking findings.
@@ -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