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 +79 -0
- package/dist/external/insight-provider.d.ts +16 -0
- package/dist/external/insight-provider.js +241 -0
- package/dist/micro-contracts.bundle.mjs +1161 -461
- package/dist/micro-contracts.bundle.mjs.map +4 -4
- package/docs/cli-reference.md +51 -0
- package/package.json +14 -3
- 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]
|
|
@@ -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
|