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 +55 -0
- package/dist/external/insight-provider.d.ts +16 -0
- package/dist/external/insight-provider.js +241 -0
- package/dist/micro-contracts.bundle.mjs +1097 -460
- package/dist/micro-contracts.bundle.mjs.map +4 -4
- package/docs/cli-reference.md +47 -0
- package/package.json +15 -4
- package/vendor/README.md +9 -0
- package/vendor/agent-contracts-analyzer-0.1.1.tgz +0 -0
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
|