agent-doc-system 0.0.0-stage → 1.1.0
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/INSTALL_FOR_AGENTS.md +60 -0
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/adapters/ansible.mjs +306 -0
- package/adapters/cache.mjs +27 -0
- package/adapters/contract-consumers.mjs +56 -0
- package/adapters/database.mjs +116 -0
- package/adapters/deploy-config.mjs +34 -0
- package/adapters/dockerfile.mjs +26 -0
- package/adapters/events.mjs +145 -0
- package/adapters/generic.mjs +86 -0
- package/adapters/github-actions.mjs +106 -0
- package/adapters/go.mjs +91 -0
- package/adapters/graphql.mjs +28 -0
- package/adapters/index.mjs +56 -0
- package/adapters/node.mjs +118 -0
- package/adapters/object-store.mjs +64 -0
- package/adapters/oidc.mjs +56 -0
- package/adapters/openapi.mjs +50 -0
- package/adapters/pnpm.mjs +42 -0
- package/adapters/protobuf.mjs +35 -0
- package/adapters/registry.mjs +27 -0
- package/adapters/resource-client.mjs +61 -0
- package/adapters/turborepo.mjs +135 -0
- package/adapters/typescript.mjs +85 -0
- package/adapters/wrangler.mjs +206 -0
- package/bin/agentdoc.mjs +494 -0
- package/bin/commands.mjs +169 -0
- package/bin/usage.txt +28 -0
- package/core/acceptances.mjs +69 -0
- package/core/audit.mjs +456 -0
- package/core/authority/classes.mjs +39 -0
- package/core/authority/engine.mjs +266 -0
- package/core/authority/facts.mjs +41 -0
- package/core/codes.mjs +126 -0
- package/core/compile.mjs +1018 -0
- package/core/contracts.mjs +121 -0
- package/core/derive.mjs +102 -0
- package/core/descriptors.mjs +489 -0
- package/core/determinism.mjs +95 -0
- package/core/discover.mjs +147 -0
- package/core/facts.mjs +53 -0
- package/core/fsx.mjs +348 -0
- package/core/graph.mjs +260 -0
- package/core/impact.mjs +175 -0
- package/core/indexes.mjs +165 -0
- package/core/jsonschema.mjs +298 -0
- package/core/normative.mjs +171 -0
- package/core/observations.mjs +129 -0
- package/core/provenance.mjs +83 -0
- package/core/query.mjs +384 -0
- package/core/relations.mjs +286 -0
- package/core/scaffold.mjs +322 -0
- package/core/secrets.mjs +92 -0
- package/core/sourcescan.mjs +88 -0
- package/core/toml.mjs +163 -0
- package/core/yaml.mjs +477 -0
- package/docs/ADAPTERS.md +207 -0
- package/docs/AUTHORITY.md +269 -0
- package/docs/CI.md +138 -0
- package/docs/COMPARISON-KODA.md +164 -0
- package/docs/EVALS.md +166 -0
- package/docs/MIGRATION.md +261 -0
- package/docs/PROVENANCE.md +133 -0
- package/docs/QUERY.md +180 -0
- package/docs/SPEC.md +339 -0
- package/docs/VERIFICATION.md +28 -0
- package/docs/assets/readme/agentdoc-hero.webp +0 -0
- package/docs/assets/readme/agentdoc-in-action.webp +0 -0
- package/docs/assets/readme/how-agentdoc-works.webp +0 -0
- package/docs/assets/readme/why-agentdoc.webp +0 -0
- package/docs/ci/generic-ci.sh +28 -0
- package/docs/ci/github-actions.yml +35 -0
- package/evals/harness.mjs +426 -0
- package/evals/scenarios/fixture-a-01-change-api.json +9 -0
- package/evals/scenarios/fixture-a-02-change-db.json +9 -0
- package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
- package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
- package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
- package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
- package/evals/scenarios/fixture-b-04-journey.json +9 -0
- package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
- package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
- package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
- package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
- package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
- package/evals/scenarios/koda-02-change-db-schema.json +20 -0
- package/evals/scenarios/koda-03-change-auth.json +21 -0
- package/evals/scenarios/koda-04-add-event-producer.json +19 -0
- package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
- package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
- package/evals/scenarios/koda-07-change-shared-package.json +16 -0
- package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
- package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
- package/evals/thresholds.json +11 -0
- package/package.json +46 -4
- package/schemas/api.schema.json +84 -0
- package/schemas/common.schema.json +147 -0
- package/schemas/component.schema.json +44 -0
- package/schemas/config.schema.json +801 -0
- package/schemas/domain.schema.json +14 -0
- package/schemas/graph.schema.json +1084 -0
- package/schemas/journeys.schema.json +47 -0
- package/schemas/observation.schema.json +143 -0
- package/schemas/resource.schema.json +28 -0
- package/schemas/system.schema.json +26 -0
- package/skills/agent-doc-system/SKILL.md +117 -0
- package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
- package/skills/agent-doc-system/references/cli.md +97 -0
- package/skills/agent-doc-system/references/create-migrate.md +113 -0
- package/skills/agent-doc-system/references/maintenance.md +52 -0
- package/templates/ADR.md +26 -0
- package/templates/ARCHITECTURE.md +19 -0
- package/templates/CONSTRAINTS.md +17 -0
- package/templates/PRODUCT.md +17 -0
- package/templates/api.yaml +10 -0
- package/templates/ci.yml +31 -0
- package/templates/component.yaml +14 -0
- package/templates/domain.yaml +6 -0
- package/templates/journey.yaml +7 -0
- package/templates/observation.yaml +19 -0
- package/templates/resource.yaml +8 -0
- package/templates/system.yaml +10 -0
- package/templates/warning-acceptance.yaml +14 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Database adapter: migration and schema evidence.
|
|
2
|
+
//
|
|
3
|
+
// Two distinct outcomes, deliberately kept apart:
|
|
4
|
+
// - an authored logical-database Resource that matches → a resolved binding
|
|
5
|
+
// and a uses-resource relation;
|
|
6
|
+
// - no match → a *candidate* fact plus a warning. Migration authority alone
|
|
7
|
+
// does not establish database ownership, so the system refuses to invent
|
|
8
|
+
// the edge. This mirrors a real review in the reference project where a
|
|
9
|
+
// library shipped migrations but explicitly did not own a database.
|
|
10
|
+
import { Adapter } from "./registry.mjs";
|
|
11
|
+
import { firstExistingFile, findMigrationDirs } from "../core/sourcescan.mjs";
|
|
12
|
+
|
|
13
|
+
const ORM_CONFIG = [
|
|
14
|
+
"drizzle.config.ts", "drizzle.config.js", "drizzle.config.mjs",
|
|
15
|
+
"prisma/schema.prisma", "schema.prisma",
|
|
16
|
+
"knexfile.js", "knexfile.ts", "sequelize.config.js", "ormconfig.json",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export class DatabaseAdapter extends Adapter {
|
|
20
|
+
static adapterName = "database";
|
|
21
|
+
constructor() {
|
|
22
|
+
super({ name: "database", version: "1.0.0", kind: "source" });
|
|
23
|
+
}
|
|
24
|
+
extract(ctx, unit) {
|
|
25
|
+
const { repo, prov, compRef } = ctx;
|
|
26
|
+
const root = unit.root;
|
|
27
|
+
const migrationDirs = findMigrationDirs(repo, root);
|
|
28
|
+
const ormConfig = firstExistingFile(repo, root, ORM_CONFIG);
|
|
29
|
+
if (!migrationDirs.length && !ormConfig) return {};
|
|
30
|
+
|
|
31
|
+
let evidencePath = ormConfig;
|
|
32
|
+
if (migrationDirs.length) {
|
|
33
|
+
evidencePath = repo.walk(migrationDirs[0])[0] || migrationDirs[0];
|
|
34
|
+
}
|
|
35
|
+
const pr = prov.add("observed", evidencePath, "migration-extractor", ["der:" + compRef + ":/bindings"]);
|
|
36
|
+
|
|
37
|
+
// A component that both migrates and opens a connection owns the database.
|
|
38
|
+
const opensConnection = hasConnectionEvidence(ctx, unit);
|
|
39
|
+
if (!opensConnection) {
|
|
40
|
+
ctx.diagnostics.push({
|
|
41
|
+
severity: "warning",
|
|
42
|
+
code: "AGENTDOC_RESOURCE_CANDIDATE",
|
|
43
|
+
subject: compRef,
|
|
44
|
+
refs: [compRef],
|
|
45
|
+
message:
|
|
46
|
+
"migration or schema authority under '" + root + "' has no matching authored logical-database Resource, " +
|
|
47
|
+
"and no connection-pool evidence: migration authority alone does not establish database ownership",
|
|
48
|
+
paths: [evidencePath],
|
|
49
|
+
});
|
|
50
|
+
ctx.addFact(compRef, "resource.ownership", null, {
|
|
51
|
+
evidenceClass: "DERIVED", confidence: "candidate", provRecs: [pr],
|
|
52
|
+
semantics: "a schema exists here, but which component owns the logical database is unresolved",
|
|
53
|
+
});
|
|
54
|
+
return { migrationAuthority: evidencePath };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const name = compRef.split("/")[1];
|
|
58
|
+
const target = ctx.sources.byKindName.get("Resource/" + name + "-postgres")
|
|
59
|
+
|| ctx.sources.byKindName.get("Resource/" + name + "-database")
|
|
60
|
+
|| ctx.sources.byKindName.get("Resource/" + name + "-db")
|
|
61
|
+
|| findDatabaseResource(ctx, name);
|
|
62
|
+
if (!target) {
|
|
63
|
+
ctx.diagnostics.push({
|
|
64
|
+
severity: "warning",
|
|
65
|
+
code: "AGENTDOC_RESOURCE_CANDIDATE",
|
|
66
|
+
subject: compRef,
|
|
67
|
+
refs: [compRef],
|
|
68
|
+
message:
|
|
69
|
+
"component '" + name + "' has migration and connection authority but no authored logical-database Resource; " +
|
|
70
|
+
"declare one or record why the database is owned elsewhere",
|
|
71
|
+
paths: [evidencePath],
|
|
72
|
+
});
|
|
73
|
+
ctx.addFact(compRef, "resource.ownership", null, {
|
|
74
|
+
evidenceClass: "DERIVED", confidence: "candidate", provRecs: [pr],
|
|
75
|
+
semantics: "this component opens a database connection, but no authored Resource claims ownership",
|
|
76
|
+
});
|
|
77
|
+
return { migrationAuthority: evidencePath };
|
|
78
|
+
}
|
|
79
|
+
ctx.addFact(compRef, "resource.ownership", target.name, {
|
|
80
|
+
evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
|
|
81
|
+
semantics: "migration and connection authority in this root resolve to the authored logical database",
|
|
82
|
+
});
|
|
83
|
+
// Migrations and a connection string are the same dependency a declared
|
|
84
|
+
// client matcher would find, so the edge carries the same instance key and
|
|
85
|
+
// the two routes merge into one fact.
|
|
86
|
+
const client = ctx.clientKeyForResourceType("logical-database");
|
|
87
|
+
const attrs = client ? { via: "migrations+connection", client } : { via: "migrations+connection" };
|
|
88
|
+
ctx.addEdge("usesResource", compRef, target.ref, attrs, [pr], "DERIVED");
|
|
89
|
+
return {
|
|
90
|
+
migrationAuthority: evidencePath,
|
|
91
|
+
bindings: [{ kind: "database", logicalResourceRef: target.ref, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) }],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const POOL_HINT = /(createPool|new Pool|pg\.Pool|psycopg2?\.connect|sql\.Open|mysql\.createConnection|Pool\(|DataSource|prisma\.(datasource|client)|DATABASE_URL|DB_DSN)/;
|
|
97
|
+
|
|
98
|
+
function hasConnectionEvidence(ctx, unit) {
|
|
99
|
+
for (const f of ctx.repo.walk(unit.root)) {
|
|
100
|
+
if (/\.(ts|tsx|js|mjs|go|py|rb|java|kt|rs|ex|cs|php)$/.test(f)) {
|
|
101
|
+
if (/(^|\/)(tests?|__tests__)\//.test(f)) continue;
|
|
102
|
+
if (POOL_HINT.test(ctx.repo.readText(f))) return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A Resource whose name starts with the component name and whose type is a
|
|
109
|
+
// database. A deterministic naming convention, not a guess: an ambiguous match
|
|
110
|
+
// is left unresolved.
|
|
111
|
+
function findDatabaseResource(ctx, name) {
|
|
112
|
+
const cands = ctx.sources.entities.filter(
|
|
113
|
+
(e) => e.kind === "Resource" && e.doc.spec.type === "logical-database" && e.name.startsWith(name)
|
|
114
|
+
);
|
|
115
|
+
return cands.length === 1 ? cands[0] : null;
|
|
116
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Deployment-config adapter: platform manifests that make a root deployable.
|
|
2
|
+
// Only the file's existence is treated as evidence; no value from these files
|
|
3
|
+
// is ever reported as deployed state.
|
|
4
|
+
import { Adapter } from "./registry.mjs";
|
|
5
|
+
|
|
6
|
+
const DEPLOY_FILES = [
|
|
7
|
+
"fly.toml", "app.yaml", "render.yaml", "railway.json", "vercel.json",
|
|
8
|
+
"netlify.toml", "heroku.yml", "Procfile", "service.yaml", "k8s/deployment.yaml",
|
|
9
|
+
"chart/Chart.yaml", "serverless.yml", "serverless.yaml", "ansible.cfg",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export class DeployConfigAdapter extends Adapter {
|
|
13
|
+
static adapterName = "deploy-config";
|
|
14
|
+
constructor() {
|
|
15
|
+
super({ name: "deploy-config", version: "1.0.0", kind: "unit-marker" });
|
|
16
|
+
}
|
|
17
|
+
detect(ctx, root) {
|
|
18
|
+
for (const n of DEPLOY_FILES) {
|
|
19
|
+
if (ctx.repo.exists(root + "/" + n)) return { path: root + "/" + n, shape: "deployable" };
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
extract(ctx, unit) {
|
|
24
|
+
const { repo, prov, compRef } = ctx;
|
|
25
|
+
const out = { deployConfigs: [] };
|
|
26
|
+
for (const n of DEPLOY_FILES) {
|
|
27
|
+
const p = unit.root + "/" + n;
|
|
28
|
+
if (!repo.exists(p)) continue;
|
|
29
|
+
prov.add("observed", p, "deploy-extractor", ["der:" + compRef + ":/artifact"]);
|
|
30
|
+
out.deployConfigs.push(p);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Dockerfile adapter: a Dockerfile is deployable-artifact evidence.
|
|
2
|
+
import { Adapter } from "./registry.mjs";
|
|
3
|
+
|
|
4
|
+
export class DockerfileAdapter extends Adapter {
|
|
5
|
+
static adapterName = "dockerfile";
|
|
6
|
+
constructor() {
|
|
7
|
+
super({ name: "dockerfile", version: "1.0.0", kind: "unit-marker" });
|
|
8
|
+
}
|
|
9
|
+
detect(ctx, root) {
|
|
10
|
+
for (const n of ["Dockerfile", "Dockerfile.prod", "Containerfile"]) {
|
|
11
|
+
if (ctx.repo.exists(root + "/" + n)) return { path: root + "/" + n, shape: "deployable" };
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
extract(ctx, unit) {
|
|
16
|
+
const { repo, prov, compRef } = ctx;
|
|
17
|
+
const out = { dockerfiles: [] };
|
|
18
|
+
for (const n of ["Dockerfile", "Dockerfile.prod", "Containerfile"]) {
|
|
19
|
+
const p = unit.root + "/" + n;
|
|
20
|
+
if (!repo.exists(p)) continue;
|
|
21
|
+
prov.add("observed", p, "deploy-extractor", ["der:" + compRef + ":/artifact"]);
|
|
22
|
+
out.dockerfiles.push(p);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Event subject adapter.
|
|
2
|
+
//
|
|
3
|
+
// Discovers event subjects and their producers and consumers from source,
|
|
4
|
+
// filtered by the project's declared event subject prefixes. Two things matter
|
|
5
|
+
// here:
|
|
6
|
+
//
|
|
7
|
+
// 1. No hard-coded project vocabulary. Koda's `koda.` prefix became a
|
|
8
|
+
// configuration value; without prefixes the extractor falls back to a
|
|
9
|
+
// conservative three-or-more-segment shape, which is what subject-based
|
|
10
|
+
// brokers actually use.
|
|
11
|
+
// 2. Producer and consumer roles are evidence, not assumption. A file that
|
|
12
|
+
// merely mentions a subject is neither.
|
|
13
|
+
import { Adapter } from "./registry.mjs";
|
|
14
|
+
import { sourceFiles as sourceFilesOf } from "../core/sourcescan.mjs";
|
|
15
|
+
|
|
16
|
+
const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts|mjs|cjs|go|py|rb|java|kt|rs|ex|exs|cs|php)$/;
|
|
17
|
+
const TEST_PATH = /(^|\/)(tests?|__tests__|spec)\//;
|
|
18
|
+
const TEST_FILE = /\.(test|spec)\./;
|
|
19
|
+
|
|
20
|
+
// A subject-shaped token: three or more dot-separated lowercase segments, with
|
|
21
|
+
// optional trailing wildcards. Two-segment values are usually scopes, not
|
|
22
|
+
// subjects, and are excluded for exactly that reason.
|
|
23
|
+
// A subject is three or more dot-separated segments: `orders.created.v1`. A
|
|
24
|
+
// declared family is two or more segments plus a wildcard: `orders.created.*`.
|
|
25
|
+
// Both are real and both are indexed. A bare `orders.*` is NOT accepted: a
|
|
26
|
+
// wildcard at the top level is a catch-all, not a subject, and letting it in
|
|
27
|
+
// would fold every subject in the system into one meaningless family.
|
|
28
|
+
const SUBJECT_TOKEN = /["'`]([a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)+(?:[.*>])?|[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)+\.(?:\*|>))["'`]/g;
|
|
29
|
+
// A named subject constant: its name is what a producer file actually mentions,
|
|
30
|
+
// while the pattern lives in another unit. Resolving the name is what lets a
|
|
31
|
+
// component that publishes "LESSON_PUBLISHED" be credited with producing the
|
|
32
|
+
// subject the shared package defines.
|
|
33
|
+
const SUBJECT_CONST = /\b([A-Z][A-Z0-9_]*_SUBJECT)\b\s*[=:]\s*["'`]([a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+){2,}(?:[.*>])?)["'`]/g;
|
|
34
|
+
// Any identifier bound to a subject literal, in any naming style. A producer
|
|
35
|
+
// file usually references the constant by name; the literal itself lives in the
|
|
36
|
+
// shared unit that defines the vocabulary.
|
|
37
|
+
const SUBJECT_DECL = /\b([A-Za-z_$][\w$]*)\b\s*(?::\s*string\s*)?=\s*["'`]([a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+){2,}(?:[.*>])?)["'`]/g;
|
|
38
|
+
const CONST_REF = /\b([A-Za-z_$][\w$]*)\b/g;
|
|
39
|
+
|
|
40
|
+
const PUBLISH_HINT = /\b(publish|emit|enqueue|produce|outbox|Publish\(|Emit\()/;
|
|
41
|
+
const CONSUME_HINT = /\b(subscribe|consume|onMessage|handler|WithFilterSubject|FilterSubjects|busloop|subscriber|ProcessBatch|dlq|DLQ|ack|Nak\()/;
|
|
42
|
+
|
|
43
|
+
export class EventsAdapter extends Adapter {
|
|
44
|
+
static adapterName = "events";
|
|
45
|
+
constructor() {
|
|
46
|
+
super({ name: "events", version: "1.0.0", kind: "source" });
|
|
47
|
+
}
|
|
48
|
+
extract(ctx, unit) {
|
|
49
|
+
const { repo, prov, compRef } = ctx;
|
|
50
|
+
const prefixes = (ctx.cfg.discovery.eventPatternPrefixes || []).filter(Boolean);
|
|
51
|
+
const contract = ctx.primaryEventContract();
|
|
52
|
+
if (!contract) return {};
|
|
53
|
+
// Constants declared anywhere in the repository, and which of those
|
|
54
|
+
// declarations this unit can actually see (itself plus what it depends on).
|
|
55
|
+
const consts = new Map();
|
|
56
|
+
const collectDecls = (root) => {
|
|
57
|
+
for (const f of sourceFilesOf(ctx.repo, root)) {
|
|
58
|
+
const text = ctx.repo.readText(f);
|
|
59
|
+
for (const m of text.matchAll(SUBJECT_DECL)) consts.set(m[1], m[2]);
|
|
60
|
+
for (const m of text.matchAll(SUBJECT_CONST)) consts.set(m[1], m[2]);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
collectDecls(unit.root);
|
|
64
|
+
// A unit can only be credited with a subject it can see: its own sources,
|
|
65
|
+
// plus those of the units it declares a dependency on, transitively. The
|
|
66
|
+
// dependency map is built from manifests before extraction, so this does
|
|
67
|
+
// not depend on adapter execution order.
|
|
68
|
+
const visible = ctx.transitiveDependencies(compRef);
|
|
69
|
+
for (const other of ctx.discovery.eligible) {
|
|
70
|
+
if (other.component && visible.has(other.component.ref) && other.component.ref !== compRef) collectDecls(other.root);
|
|
71
|
+
}
|
|
72
|
+
const families = new Set();
|
|
73
|
+
for (const f of repo.walk(unit.root)) {
|
|
74
|
+
if (!SRC_EXT.test(f)) continue;
|
|
75
|
+
if (TEST_PATH.test(f) || TEST_FILE.test(f)) continue;
|
|
76
|
+
const text = repo.readText(f);
|
|
77
|
+
const publish = PUBLISH_HINT.test(text);
|
|
78
|
+
const consume = CONSUME_HINT.test(text);
|
|
79
|
+
if (!publish && !consume) continue;
|
|
80
|
+
const found = new Set();
|
|
81
|
+
for (const m of text.matchAll(SUBJECT_TOKEN)) {
|
|
82
|
+
const s = m[1];
|
|
83
|
+
if (prefixes.length && !prefixes.some((p) => s.startsWith(p))) continue;
|
|
84
|
+
found.add(s);
|
|
85
|
+
}
|
|
86
|
+
for (const m of text.matchAll(CONST_REF)) {
|
|
87
|
+
const pattern = consts.get(m[1]);
|
|
88
|
+
if (pattern && pattern !== m[0] && (!prefixes.length || prefixes.some((p) => pattern.startsWith(p)))) found.add(pattern);
|
|
89
|
+
}
|
|
90
|
+
for (const s of found) {
|
|
91
|
+
if (s.endsWith(".*") || s.endsWith(".>")) families.add(s);
|
|
92
|
+
}
|
|
93
|
+
for (const s of found) {
|
|
94
|
+
const pr = prov.add("observed", f, "event-extractor", ["evt:" + s]);
|
|
95
|
+
if (publish) ctx.addEvent(s, "producer", compRef, pr, contract);
|
|
96
|
+
if (consume) ctx.addEvent(s, "consumer", compRef, pr, contract);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// The family set is only needed to fold literals; the fold itself happens in
|
|
100
|
+
// the index builder, where the contract and the resolution state are known.
|
|
101
|
+
void families;
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
// A declared event catalog is a strong, human-maintained statement of which
|
|
105
|
+
// component subscribes to which subjects. The catalog file is the contract.
|
|
106
|
+
contracts(ctx) {
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const ec of ctx.cfg.discovery.eventContracts || []) {
|
|
109
|
+
// A contract may be a file, a directory, or a module prefix.
|
|
110
|
+
const files = ctx.repo.filesWithPrefix(ec.path);
|
|
111
|
+
if (!files.length) {
|
|
112
|
+
ctx.errors.push({
|
|
113
|
+
code: "AGENTDOC_PATH_UNRESOLVED",
|
|
114
|
+
message: "declared event contract path does not resolve: " + ec.path,
|
|
115
|
+
path: ec.path,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const patterns = ec.format === "code" ? harvestFromCode(ctx, ec, files) : Object.keys(safeJson(ctx, ec));
|
|
120
|
+
// Provenance must name a real file; the contract may be a directory or a
|
|
121
|
+
// module prefix.
|
|
122
|
+
const evidenceFile = files.find((f) => SRC_EXT.test(f) && !/\.(test|spec)\./.test(f)) || files[0];
|
|
123
|
+
const pr = ctx.prov.add("declared", evidenceFile, "event-catalog-extractor", patterns.map((p) => "evt:" + p));
|
|
124
|
+
for (const p of patterns.sort()) ctx.addEvent(p, "consumer", ec.consumer, pr, { format: ec.format === "code" ? "code" : "declared-catalog", ref: ec.path });
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function safeJson(ctx, ec) {
|
|
131
|
+
try {
|
|
132
|
+
return ctx.repo.readJson(ec.path);
|
|
133
|
+
} catch {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function harvestFromCode(ctx, ec, files) {
|
|
139
|
+
const out = new Set();
|
|
140
|
+
for (const f of files) {
|
|
141
|
+
if (!SRC_EXT.test(f)) continue;
|
|
142
|
+
for (const m of ctx.repo.readText(f).matchAll(SUBJECT_TOKEN)) out.add(m[1]);
|
|
143
|
+
}
|
|
144
|
+
return [...out];
|
|
145
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Generic adapter: language- and toolchain-agnostic facts only.
|
|
2
|
+
//
|
|
3
|
+
// Nothing here knows a language, a package manager, a provider or a folder
|
|
4
|
+
// name. It contributes the two things any repository can be said to have:
|
|
5
|
+
// conventional documentation (handled in core/derive.mjs) and health contract
|
|
6
|
+
// route literals found in source.
|
|
7
|
+
import { Adapter } from "./registry.mjs";
|
|
8
|
+
import { resolveClientResources } from "./resource-client.mjs";
|
|
9
|
+
import { sourceFiles } from "../core/sourcescan.mjs";
|
|
10
|
+
|
|
11
|
+
// Conservative: a health route is an absolute string literal that looks like a
|
|
12
|
+
// liveness or readiness endpoint. Anything more specific belongs to a
|
|
13
|
+
// framework adapter.
|
|
14
|
+
const HEALTH_LITERAL = /["'`](\/(?:healthz|health|livez|readyz|ready|ping|status))["'`]/;
|
|
15
|
+
|
|
16
|
+
export class GenericAdapter extends Adapter {
|
|
17
|
+
static adapterName = "generic";
|
|
18
|
+
constructor() {
|
|
19
|
+
super({ name: "generic", version: "1.0.0", kind: "source" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
extract(ctx, unit) {
|
|
23
|
+
const { repo, prov, compRef } = ctx;
|
|
24
|
+
const derived = { healthContracts: [] };
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const ports = new Set();
|
|
27
|
+
const portFiles = new Map();
|
|
28
|
+
const allSrc = sourceFiles(repo, unit.root, { test: false });
|
|
29
|
+
for (const f of allSrc) {
|
|
30
|
+
const text = repo.readText(f);
|
|
31
|
+
const pm = /ListenAndServe\(\s*":(\d{2,5})"|\bPORT\s*=\s*"?(\d{2,5})"?|\bport:\s*(\d{2,5})\b/.exec(text);
|
|
32
|
+
if (pm) {
|
|
33
|
+
const p = pm[1] || pm[2] || pm[3];
|
|
34
|
+
ports.add(p);
|
|
35
|
+
if (!portFiles.has(p)) portFiles.set(p, f);
|
|
36
|
+
}
|
|
37
|
+
const m = HEALTH_LITERAL.exec(text);
|
|
38
|
+
if (!m) continue;
|
|
39
|
+
const route = m[1];
|
|
40
|
+
if (seen.has(route)) continue;
|
|
41
|
+
seen.add(route);
|
|
42
|
+
const pr = prov.add("observed", f, "health-extractor", ["der:" + compRef + ":/healthContracts"]);
|
|
43
|
+
derived.healthContracts.push({ path: f, route, semantics: "liveness", provenanceIds: null, _provs: new Set([pr]) });
|
|
44
|
+
}
|
|
45
|
+
if (derived.healthContracts.length > 1) derived.healthContracts.sort((a, b) => (a.route < b.route ? -1 : 1));
|
|
46
|
+
// A port is only a fact when the source states exactly one. Two literals
|
|
47
|
+
// mean the component binds more than one, and a single number would be a
|
|
48
|
+
// confident half-truth.
|
|
49
|
+
// Declared external services front Resources; resolve those bindings.
|
|
50
|
+
// Provenance must name a file that exists. The dependency evidence is the
|
|
51
|
+
// manifest when there is one, and otherwise the unit's descriptor.
|
|
52
|
+
const manifest = ["package.json", "go.mod", "Cargo.toml", "pyproject.toml", "pom.xml"]
|
|
53
|
+
.map((n) => unit.root + "/" + n)
|
|
54
|
+
.find((p) => repo.exists(p)) || ctx.componentEntity().file;
|
|
55
|
+
const clientProv = prov.add("observed", manifest, "client-resolver", ["rel:pending", "der:" + compRef + ":/bindings"]);
|
|
56
|
+
resolveClientResources(ctx, unit, compRef, clientProv);
|
|
57
|
+
|
|
58
|
+
// Capability detectors are declared, not hard-coded: a manifest cannot tell
|
|
59
|
+
// you a component can use a browser API, but a source literal can.
|
|
60
|
+
for (const cap of (ctx.cfg.discovery.capabilities || [])) {
|
|
61
|
+
let re;
|
|
62
|
+
try {
|
|
63
|
+
re = new RegExp(cap.match);
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const f of allSrc) {
|
|
68
|
+
if (!re.test(repo.readText(f))) continue;
|
|
69
|
+
const cpr = prov.add("observed", f, "capability-extractor", ["cap:" + compRef + "|" + cap.name]);
|
|
70
|
+
ctx.addCapability(compRef, cap.name, cpr, "DERIVED");
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (ports.size === 1) {
|
|
76
|
+
const p = [...ports][0];
|
|
77
|
+
const pr = prov.add("observed", portFiles.get(p) || ctx.componentEntity().file, "port-extractor", ["der:" + compRef + ":/port"]);
|
|
78
|
+
derived.port = Number(p);
|
|
79
|
+
ctx.addFact(compRef, "component.port", Number(p), {
|
|
80
|
+
evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
|
|
81
|
+
semantics: "the single TCP port the source binds a listener to",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return derived;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// GitHub Actions adapter.
|
|
2
|
+
//
|
|
3
|
+
// CI is a verification authority: a workflow that names a component's paths is
|
|
4
|
+
// the check that must be run for that component. Recording the mapping means
|
|
5
|
+
// an agent changing a component is told which gate covers it, instead of
|
|
6
|
+
// guessing or running everything.
|
|
7
|
+
import { Adapter } from "./registry.mjs";
|
|
8
|
+
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
9
|
+
|
|
10
|
+
export class GitHubActionsAdapter extends Adapter {
|
|
11
|
+
static adapterName = "github-actions";
|
|
12
|
+
constructor() {
|
|
13
|
+
super({ name: "github-actions", version: "1.0.0", kind: "ci" });
|
|
14
|
+
}
|
|
15
|
+
// No detect(): a workflow directory is not a unit marker. It is not evidence
|
|
16
|
+
// of an importable or deployable artifact, and a marker path is hashed as a
|
|
17
|
+
// file for provenance, so a directory marker can only fail. GitHub runs only
|
|
18
|
+
// the repository-root .github/workflows; a nested one (vendored upstream, a
|
|
19
|
+
// subtree) is inert and is ignored here too.
|
|
20
|
+
contracts(ctx) {
|
|
21
|
+
const dir = ".github/workflows";
|
|
22
|
+
if (!ctx.repo.isDir(dir)) return [];
|
|
23
|
+
const out = [];
|
|
24
|
+
const files = ctx.repo.walk(dir).filter((f) => /\.ya?ml$/.test(f)).sort();
|
|
25
|
+
const units = ctx.discovery.eligible.filter((u) => u.component);
|
|
26
|
+
for (const f of files) {
|
|
27
|
+
let text = "";
|
|
28
|
+
try {
|
|
29
|
+
text = ctx.repo.readText(f);
|
|
30
|
+
} catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let doc = null;
|
|
34
|
+
try {
|
|
35
|
+
doc = parseYamlDocuments(text, f)[0]?.doc;
|
|
36
|
+
} catch {
|
|
37
|
+
doc = null;
|
|
38
|
+
}
|
|
39
|
+
const steps = [];
|
|
40
|
+
if (doc && doc.jobs) {
|
|
41
|
+
for (const [jobName, job] of Object.entries(doc.jobs)) {
|
|
42
|
+
for (const step of (job && job.steps) || []) {
|
|
43
|
+
const run = [step.run, step.uses].filter(Boolean).join(" ");
|
|
44
|
+
if (!run) continue;
|
|
45
|
+
steps.push({ job: jobName, run: String(run) });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const covered = new Set();
|
|
50
|
+
for (const u of units) {
|
|
51
|
+
if (text.includes(u.root + "/") || (u.pkgName && text.includes(u.pkgName))) covered.add(u.component.ref);
|
|
52
|
+
}
|
|
53
|
+
const pr = ctx.prov.add("observed", f, "ci-extractor", []);
|
|
54
|
+
const subjects = [];
|
|
55
|
+
steps.forEach((s, i) => {
|
|
56
|
+
if (!/[a-z0-9]/i.test(s.run)) return;
|
|
57
|
+
const command = s.run.trim().slice(0, 600);
|
|
58
|
+
// A step that installs dependencies or checks out code is not a check.
|
|
59
|
+
// Recording it as one would bury the commands that actually gate.
|
|
60
|
+
const tier = ciTier(command);
|
|
61
|
+
if (!tier) return;
|
|
62
|
+
const id = "ci:" + f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + s.job + ":" + i;
|
|
63
|
+
if (covered.size) {
|
|
64
|
+
ctx.addVerification({
|
|
65
|
+
id,
|
|
66
|
+
componentRefs: [...covered].sort(),
|
|
67
|
+
tier,
|
|
68
|
+
command,
|
|
69
|
+
configPaths: [f],
|
|
70
|
+
provRecs: [pr],
|
|
71
|
+
});
|
|
72
|
+
subjects.push("ver:" + id);
|
|
73
|
+
} else {
|
|
74
|
+
// A gate that names no component is repository-wide: it applies to
|
|
75
|
+
// everything, so it is recorded once as a gate rather than
|
|
76
|
+
// duplicated onto every component.
|
|
77
|
+
// The graph id must not itself start with the provenance prefix, or
|
|
78
|
+
// the subject key becomes ambiguous. "repo-gate:" is unambiguous.
|
|
79
|
+
const gid = "repo-gate:" + f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + s.job + ":" + i;
|
|
80
|
+
ctx.addGate({ id: gid, tier: ciTier(command), command, configPaths: [f], provRecs: [pr] });
|
|
81
|
+
subjects.push("gate:" + gid);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
for (const s of subjects) pr.subjects.add(s);
|
|
85
|
+
void steps;
|
|
86
|
+
void covered;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Returns null for a step that is not a check. Install, checkout, cache and
|
|
93
|
+
// upload steps gate nothing and would only add noise to a routing surface.
|
|
94
|
+
const NOT_A_CHECK = /^\s*(?:-\s*)?(?:uses:\s*actions\/(?:checkout|cache|upload|download|setup-|artifact)|run:\s*(?:\w*\s*)?(?:pnpm|npm|yarn|bun)\s+(?:i|install|ci)\b)/i;
|
|
95
|
+
|
|
96
|
+
function ciTier(run) {
|
|
97
|
+
if (NOT_A_CHECK.test(run.trim())) return null;
|
|
98
|
+
if (/^uses:/.test(run.trim()) && !/check|lint|test|build|audit|scan/i.test(run)) return null;
|
|
99
|
+
if (/e2e|playwright|cypress/.test(run)) return "e2e";
|
|
100
|
+
if (/contract|openapi|api:check|drift|catalog/.test(run)) return "contract";
|
|
101
|
+
if (/integration/.test(run)) return "integration";
|
|
102
|
+
if (/lint|typecheck|type-check|golangci|vet/.test(run)) return "static";
|
|
103
|
+
if (/\btest\b|pytest|vitest|jest|go test/.test(run)) return "unit";
|
|
104
|
+
if (/\bbuild\b|tsc|compile/.test(run)) return "build";
|
|
105
|
+
return null;
|
|
106
|
+
}
|
package/adapters/go.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Go adapter: go.mod / go.work as unit markers, module graph as build
|
|
2
|
+
// relations, and the go toolchain's own verification commands.
|
|
3
|
+
import { Adapter } from "./registry.mjs";
|
|
4
|
+
|
|
5
|
+
export class GoAdapter extends Adapter {
|
|
6
|
+
static adapterName = "go";
|
|
7
|
+
constructor() {
|
|
8
|
+
super({ name: "go", version: "1.0.0", kind: "unit-marker" });
|
|
9
|
+
}
|
|
10
|
+
roots(ctx) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const ws = "go.work";
|
|
13
|
+
if (!ctx.repo.exists(ws)) return out;
|
|
14
|
+
const text = ctx.repo.readText(ws);
|
|
15
|
+
const block = /use\s*\(([^)]*)\)/s.exec(text);
|
|
16
|
+
const candidates = block
|
|
17
|
+
? block[1].split("\n").map((l) => l.replace(/\/\/.*$/, "").trim()).filter(Boolean)
|
|
18
|
+
: [...text.matchAll(/^\s*use\s+(\S+)/gm)].map((m) => m[1]);
|
|
19
|
+
for (const raw of candidates) {
|
|
20
|
+
const d = raw.replace(/^\.\//, "").replace(/\/$/, "");
|
|
21
|
+
if (!d || d.startsWith("//") || d.includes("..")) continue;
|
|
22
|
+
if (!ctx.repo.isDir(d)) continue;
|
|
23
|
+
if (!ctx.repo.exists(d + "/go.mod")) continue;
|
|
24
|
+
out.push({ root: d, deployable: ctx.repo.isDir(d + "/cmd"), importable: !ctx.repo.isDir(d + "/cmd") });
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
detect(ctx, root) {
|
|
29
|
+
const p = root + "/go.mod";
|
|
30
|
+
if (!ctx.repo.exists(p)) return null;
|
|
31
|
+
return { path: p, shape: ctx.repo.isDir(root + "/cmd") ? "deployable" : "importable" };
|
|
32
|
+
}
|
|
33
|
+
extract(ctx, unit) {
|
|
34
|
+
const { repo, prov, compRef } = ctx;
|
|
35
|
+
const p = unit.root + "/go.mod";
|
|
36
|
+
if (!repo.exists(p)) return {};
|
|
37
|
+
const derived = { languages: new Set(["go"]), runtimes: new Set(["go"]) };
|
|
38
|
+
const text = repo.readText(p);
|
|
39
|
+
const pr = prov.add("observed", p, "gomod-extractor", ["der:" + compRef + ":/runtimes", "der:" + compRef + ":/languages"]);
|
|
40
|
+
|
|
41
|
+
for (const m of text.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)) {
|
|
42
|
+
const mod = m[1];
|
|
43
|
+
const target = ctx.unitByGoModule.get(mod);
|
|
44
|
+
if (target && target.component && target.component.ref !== compRef) {
|
|
45
|
+
ctx.addEdge("buildDependsOn", compRef, target.component.ref, { via: "go-module" }, [pr], "DERIVED");
|
|
46
|
+
} else {
|
|
47
|
+
for (const x of ctx.externalMatchers()) {
|
|
48
|
+
if (!x.re.test(mod)) continue;
|
|
49
|
+
ctx.addExternal(compRef, x.name, x.mechanism, x.role, pr, "DERIVED");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Workspace members depended on by import alone. A Go workspace member can
|
|
54
|
+
// have no require line at all, so the import graph is the only evidence.
|
|
55
|
+
for (const importPath of ctx.importIndex.goPathsByUnit.get(unit) || []) {
|
|
56
|
+
for (const [mod, target] of ctx.unitByGoModule) {
|
|
57
|
+
if (target === unit || !target.component) continue;
|
|
58
|
+
if (importPath !== mod && importPath.startsWith(mod + "/")) {
|
|
59
|
+
ctx.addEdge("buildDependsOn", compRef, target.component.ref, { via: "go-import" }, [pr], "DERIVED");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hasTests = repo.walk(unit.root).some((f) => /_test\.go$/.test(f));
|
|
65
|
+
ctx.addVerification({
|
|
66
|
+
id: compRef + ":go-test",
|
|
67
|
+
componentRefs: [compRef],
|
|
68
|
+
tier: "unit",
|
|
69
|
+
command: "go test ./...",
|
|
70
|
+
configPaths: [p],
|
|
71
|
+
provRecs: [pr],
|
|
72
|
+
});
|
|
73
|
+
ctx.addVerification({
|
|
74
|
+
id: compRef + ":go-vet",
|
|
75
|
+
componentRefs: [compRef],
|
|
76
|
+
tier: "static",
|
|
77
|
+
command: "go vet ./...",
|
|
78
|
+
configPaths: [p],
|
|
79
|
+
provRecs: [pr],
|
|
80
|
+
});
|
|
81
|
+
if (hasTests) derived.hasTests = true;
|
|
82
|
+
derived.goModule = unit.goModule;
|
|
83
|
+
return derived;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function goExternalName(mod) {
|
|
88
|
+
const parts = mod.split("/");
|
|
89
|
+
const tail = parts[parts.length - 1].replace(/\.v\d+$/, "");
|
|
90
|
+
return (tail || parts[0]).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "go-module";
|
|
91
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// GraphQL adapter: SDL as the canonical contract plus deterministic consumer
|
|
2
|
+
// resolution.
|
|
3
|
+
import { Adapter } from "./registry.mjs";
|
|
4
|
+
import { resolveContractConsumers, isBuildConfig } from "./contract-consumers.mjs";
|
|
5
|
+
|
|
6
|
+
export class GraphQLAdapter extends Adapter {
|
|
7
|
+
static adapterName = "graphql";
|
|
8
|
+
constructor() {
|
|
9
|
+
super({ name: "graphql", version: "1.0.0", kind: "contract" });
|
|
10
|
+
}
|
|
11
|
+
contracts(ctx) {
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const api of ctx.sources.entities.filter((e) => e.kind === "API" && ["graphql", "graphql-sdl"].includes(e.doc.spec.type))) {
|
|
14
|
+
const ref = api.doc.spec.contract.ref;
|
|
15
|
+
const text = ctx.repo.readText(ref);
|
|
16
|
+
const types = [...text.matchAll(/^\s*(?:type|interface|input|enum|scalar|union)\s+([A-Za-z_][A-Za-z0-9_]*)/gm)].map((m) => m[1]);
|
|
17
|
+
const queries = [...text.matchAll(/^\s*(?:extend\s+)?type\s+Query\s*\{([\s\S]*?)\n\}/gm)]
|
|
18
|
+
.flatMap((m) => [...m[1].matchAll(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[(:]/gm)].map((x) => x[1]));
|
|
19
|
+
ctx.addContract({ apiRef: api.ref, ref, types: [...new Set(types)].sort(), queries: [...new Set(queries)].sort() });
|
|
20
|
+
const consumers = resolveContractConsumers(ctx, ref, api.doc.spec.provider);
|
|
21
|
+
for (const [consumerRef, files] of consumers) {
|
|
22
|
+
const provs = files.map((f) => ctx.prov.add(isBuildConfig(f) ? "validated" : "observed", f, "api-contract-extractor", ["rel:pending"]));
|
|
23
|
+
ctx.addEdge("consumesApi", consumerRef, api.ref, { via: files.some(isBuildConfig) ? "codegen" : "source-reference" }, provs, "DERIVED");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
}
|