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,56 @@
|
|
|
1
|
+
// Adapter registry. Adapters are looked up by name and instantiated once.
|
|
2
|
+
// An unknown adapter name is a configuration error, never a silent no-op: a
|
|
3
|
+
// project that thinks it has protobuf discovery must not lose it quietly.
|
|
4
|
+
import { AgentDocError, CODES } from "../core/codes.mjs";
|
|
5
|
+
import { GenericAdapter } from "./generic.mjs";
|
|
6
|
+
import { NodeAdapter } from "./node.mjs";
|
|
7
|
+
import { PnpmAdapter } from "./pnpm.mjs";
|
|
8
|
+
import { TurborepoAdapter } from "./turborepo.mjs";
|
|
9
|
+
import { TypeScriptAdapter } from "./typescript.mjs";
|
|
10
|
+
import { GoAdapter } from "./go.mjs";
|
|
11
|
+
import { OpenApiAdapter } from "./openapi.mjs";
|
|
12
|
+
import { ProtobufAdapter } from "./protobuf.mjs";
|
|
13
|
+
import { GraphQLAdapter } from "./graphql.mjs";
|
|
14
|
+
import { OidcAdapter } from "./oidc.mjs";
|
|
15
|
+
import { GitHubActionsAdapter } from "./github-actions.mjs";
|
|
16
|
+
import { DockerfileAdapter } from "./dockerfile.mjs";
|
|
17
|
+
import { WranglerAdapter } from "./wrangler.mjs";
|
|
18
|
+
import { DatabaseAdapter } from "./database.mjs";
|
|
19
|
+
import { EventsAdapter } from "./events.mjs";
|
|
20
|
+
import { DeployConfigAdapter } from "./deploy-config.mjs";
|
|
21
|
+
import { ObjectStoreAdapter } from "./object-store.mjs";
|
|
22
|
+
import { CacheAdapter } from "./cache.mjs";
|
|
23
|
+
import { AnsibleAdapter } from "./ansible.mjs";
|
|
24
|
+
|
|
25
|
+
const REGISTRY = new Map();
|
|
26
|
+
for (const C of [
|
|
27
|
+
GenericAdapter, NodeAdapter, PnpmAdapter, TurborepoAdapter, TypeScriptAdapter,
|
|
28
|
+
GoAdapter, OpenApiAdapter, ProtobufAdapter, GraphQLAdapter, OidcAdapter,
|
|
29
|
+
GitHubActionsAdapter, DockerfileAdapter, WranglerAdapter, DatabaseAdapter,
|
|
30
|
+
EventsAdapter, DeployConfigAdapter, ObjectStoreAdapter, CacheAdapter, AnsibleAdapter,
|
|
31
|
+
]) {
|
|
32
|
+
REGISTRY.set(C.prototype.constructor.adapterName, new C());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function adapterNames() {
|
|
36
|
+
return [...REGISTRY.keys()].sort();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadAdapters(names) {
|
|
40
|
+
// The generic adapter is always on: documentation conventions and health
|
|
41
|
+
// contract detection are language-agnostic and cost nothing.
|
|
42
|
+
const out = [REGISTRY.get("generic")];
|
|
43
|
+
for (const n of names || []) {
|
|
44
|
+
const a = REGISTRY.get(n);
|
|
45
|
+
if (!a) {
|
|
46
|
+
throw new AgentDocError(
|
|
47
|
+
CODES.CONFIG,
|
|
48
|
+
"unknown adapter '" + n + "'; available adapters: " + adapterNames().join(", ")
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (out.includes(a)) continue;
|
|
52
|
+
out.push(a);
|
|
53
|
+
}
|
|
54
|
+
// Deterministic order, independent of how the config listed them.
|
|
55
|
+
return out.slice().sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
|
|
56
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Node adapter: package.json as the unit marker and manifest authority.
|
|
2
|
+
import { Adapter } from "./registry.mjs";
|
|
3
|
+
|
|
4
|
+
// A script name maps to a verification tier. Deliberately conservative and
|
|
5
|
+
// project-neutral: a script that does not look like a check is not a check.
|
|
6
|
+
const TIERS = [
|
|
7
|
+
[/^(test|tests)$|^test:unit$|^test:unit:/, "unit"],
|
|
8
|
+
[/^test:integration$|^test:integration:/, "integration"],
|
|
9
|
+
[/^test:contract$|^test:contract:/, "contract"],
|
|
10
|
+
[/^test:e2e$|^test:e2e:/, "e2e"],
|
|
11
|
+
[/^test:browser$/, "unit"],
|
|
12
|
+
[/^(lint|typecheck|check-types|type-check)$/, "static"],
|
|
13
|
+
[/^(build|compile)$/, "build"],
|
|
14
|
+
];
|
|
15
|
+
const IGNORED_SCRIPT = /^(dev|start|serve|watch|preview|clean|postinstall|preinstall|prepare|format|format:fix)$/;
|
|
16
|
+
|
|
17
|
+
export class NodeAdapter extends Adapter {
|
|
18
|
+
static adapterName = "node";
|
|
19
|
+
constructor() {
|
|
20
|
+
super({ name: "node", version: "1.0.0", kind: "unit-marker" });
|
|
21
|
+
}
|
|
22
|
+
detect(ctx, root) {
|
|
23
|
+
const p = root + "/package.json";
|
|
24
|
+
if (!ctx.repo.exists(p)) return null;
|
|
25
|
+
let pkg;
|
|
26
|
+
try {
|
|
27
|
+
pkg = ctx.repo.readJson(p);
|
|
28
|
+
} catch {
|
|
29
|
+
return null; // malformed manifest is not authority
|
|
30
|
+
}
|
|
31
|
+
const importable = Boolean(pkg.exports || pkg.main || pkg.bin || pkg.types);
|
|
32
|
+
return { path: p, shape: importable ? "both" : "deployable" };
|
|
33
|
+
}
|
|
34
|
+
extract(ctx, unit) {
|
|
35
|
+
const { repo, prov, compRef } = ctx;
|
|
36
|
+
const root = unit.root;
|
|
37
|
+
const manifestPath = root + "/package.json";
|
|
38
|
+
if (!repo.exists(manifestPath)) return {};
|
|
39
|
+
const derived = { languages: new Set(["javascript"]), runtimes: new Set(["node"]) };
|
|
40
|
+
const pkg = unit.pkg || repo.readJson(manifestPath);
|
|
41
|
+
|
|
42
|
+
const pr = prov.add("observed", manifestPath, "manifest-extractor", [
|
|
43
|
+
"der:" + compRef + ":/runtimes", "der:" + compRef + ":/languages", "der:" + compRef + ":/artifact",
|
|
44
|
+
]);
|
|
45
|
+
const prodDeps = new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.optionalDependencies || {})]);
|
|
46
|
+
const devDeps = new Set(Object.keys(pkg.devDependencies || {}));
|
|
47
|
+
|
|
48
|
+
// Only declared external services are classified. A package in a manifest
|
|
49
|
+
// is a build fact, not an architectural dependency, and recording six
|
|
50
|
+
// hundred of them would make the index useless.
|
|
51
|
+
for (const d of [...prodDeps].sort()) {
|
|
52
|
+
for (const x of ctx.externalMatchers()) {
|
|
53
|
+
if (!x.re.test(d)) continue;
|
|
54
|
+
ctx.addExternal(compRef, x.name, x.mechanism, x.role, pr, "DERIVED");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Workspace resolution: a dependency naming another discovered unit is a
|
|
59
|
+
// build relation, not an external dependency.
|
|
60
|
+
for (const d of [...prodDeps, ...devDeps].sort()) {
|
|
61
|
+
const target = ctx.unitByPkgName.get(d);
|
|
62
|
+
if (!target || !target.component) continue;
|
|
63
|
+
const isProd = prodDeps.has(d);
|
|
64
|
+
ctx.addEdge(isProd ? "buildDependsOn" : "testDependsOn", compRef, target.component.ref,
|
|
65
|
+
{ via: "npm-manifest", package: d }, [pr], "DERIVED");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Source imports are stronger evidence than the manifest, and catch
|
|
69
|
+
// subpath imports the manifest cannot express.
|
|
70
|
+
const sibling = new Map();
|
|
71
|
+
for (const spec of ctx.unitImports.keys()) {
|
|
72
|
+
const parts = spec.split("/");
|
|
73
|
+
const base = spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
74
|
+
const target = ctx.unitByPkgName.get(base);
|
|
75
|
+
if (!target || !target.component || target.component.ref === compRef) continue;
|
|
76
|
+
if (prodDeps.has(base)) continue;
|
|
77
|
+
if (!sibling.has(target)) sibling.set(target, new Set());
|
|
78
|
+
for (const f of ctx.unitImports.get(spec)) sibling.get(target).add(f);
|
|
79
|
+
}
|
|
80
|
+
for (const [target, files] of [...sibling.entries()].sort((a, b) => (a[0].root < b[0].root ? -1 : 1))) {
|
|
81
|
+
const provs = [...files].sort().map((f) => prov.add("observed", f, "import-extractor", ["rel:pending"]));
|
|
82
|
+
// The import names the same package the manifest route names, so it
|
|
83
|
+
// carries the same instance key. Without it, a dependency found by both
|
|
84
|
+
// routes shipped as two edges — the manifest one saying `package`, the
|
|
85
|
+
// import one not — which measures adapter overlap rather than architecture.
|
|
86
|
+
const pkgName = target.pkgName;
|
|
87
|
+
const attrs = pkgName ? { via: "source-import", package: pkgName } : { via: "source-import" };
|
|
88
|
+
ctx.addEdge(devDeps.has(pkgName) ? "testDependsOn" : "buildDependsOn",
|
|
89
|
+
compRef, target.component.ref, attrs, provs, "DERIVED");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [name, cmd] of Object.entries(pkg.scripts || {}).sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
93
|
+
if (IGNORED_SCRIPT.test(name)) continue;
|
|
94
|
+
const tier = TIERS.find(([re]) => re.test(name));
|
|
95
|
+
if (!tier) continue;
|
|
96
|
+
const vpr = prov.add("observed", manifestPath, "verification-extractor", ["ver:" + compRef + ":" + name]);
|
|
97
|
+
ctx.addVerification({
|
|
98
|
+
id: compRef + ":" + name,
|
|
99
|
+
componentRefs: [compRef],
|
|
100
|
+
tier: tier[1],
|
|
101
|
+
command: String(cmd).trim(),
|
|
102
|
+
configPaths: [manifestPath],
|
|
103
|
+
provRecs: [vpr],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return derived;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Normalise a package name into a graph-safe external dependency name.
|
|
111
|
+
export function externalName(d) {
|
|
112
|
+
const n = d
|
|
113
|
+
.replace(/^@[^/]+\//, "")
|
|
114
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
115
|
+
.replace(/^-+|-+$/g, "")
|
|
116
|
+
.toLowerCase();
|
|
117
|
+
return /^[a-z0-9]/.test(n) ? n.slice(0, 60) : "pkg-" + n.replace(/[^a-z0-9]/g, "").slice(0, 50);
|
|
118
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Object-store adapter: bucket bindings and literal bucket names.
|
|
2
|
+
//
|
|
3
|
+
// A bucket literal in a component's source is deterministic evidence that the
|
|
4
|
+
// component touches that store. It is only turned into a resource relation when
|
|
5
|
+
// exactly one authored logical-dataset Resource matches — an ambiguous literal
|
|
6
|
+
// is left unresolved rather than guessed at.
|
|
7
|
+
import { Adapter } from "./registry.mjs";
|
|
8
|
+
|
|
9
|
+
const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts|go|py|rb|java|kt|rs)$/;
|
|
10
|
+
const TEST_PATH = /(^|\/)(tests?|__tests__|spec)\//;
|
|
11
|
+
const TEST_FILE = /\.(test|spec)\./;
|
|
12
|
+
const BUCKET_LITERAL = /["'`]([a-z][a-z0-9-]{4,60})["'`]/g;
|
|
13
|
+
const STORE_HINT = /(@aws-sdk\/client-s3|minio|google\.cloud\.storage|\bS3\b|bucket|Bucket|BlobService|createBucket)/i;
|
|
14
|
+
|
|
15
|
+
export class ObjectStoreAdapter extends Adapter {
|
|
16
|
+
static adapterName = "object-store";
|
|
17
|
+
constructor() {
|
|
18
|
+
super({ name: "object-store", version: "1.0.0", kind: "source" });
|
|
19
|
+
}
|
|
20
|
+
extract(ctx, unit) {
|
|
21
|
+
const { repo, prov, compRef } = ctx;
|
|
22
|
+
const datasets = ctx.sources.entities.filter((e) => e.kind === "Resource" && e.doc.spec.type === "logical-dataset");
|
|
23
|
+
if (!datasets.length) return {};
|
|
24
|
+
const usesStore = ctx.unitImports && [...ctx.unitImports.keys()].some((k) => /s3|minio|storage|blob/i.test(k));
|
|
25
|
+
const bindings = [];
|
|
26
|
+
const claimed = new Set();
|
|
27
|
+
for (const f of repo.walk(unit.root)) {
|
|
28
|
+
if (!SRC_EXT.test(f)) continue;
|
|
29
|
+
if (TEST_PATH.test(f) || TEST_FILE.test(f)) continue;
|
|
30
|
+
const text = repo.readText(f);
|
|
31
|
+
if (!STORE_HINT.test(text)) continue;
|
|
32
|
+
for (const m of text.matchAll(BUCKET_LITERAL)) {
|
|
33
|
+
const lit = m[1];
|
|
34
|
+
const matches = datasets.filter((d) => lit.includes(token(d.name)) || d.name.includes(token(lit)));
|
|
35
|
+
if (matches.length !== 1) {
|
|
36
|
+
if (matches.length > 1) {
|
|
37
|
+
ctx.diagnostics.push({
|
|
38
|
+
severity: "warning",
|
|
39
|
+
code: "AGENTDOC_BINDING_AMBIGUOUS",
|
|
40
|
+
subject: compRef,
|
|
41
|
+
refs: [compRef],
|
|
42
|
+
message: "bucket literal '" + lit + "' in " + f + " matches " + matches.length + " authored logical-dataset Resources; left unresolved",
|
|
43
|
+
paths: [f],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (claimed.has(matches[0].ref)) continue;
|
|
49
|
+
claimed.add(matches[0].ref);
|
|
50
|
+
const pr = prov.add("observed", f, "binding-extractor", ["der:" + compRef + ":/bindings", "rel:pending"]);
|
|
51
|
+
bindings.push({ kind: "object-store", logicalResourceRef: matches[0].ref, locator: { bucket: lit }, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) });
|
|
52
|
+
const client = ctx.clientKeyForResourceType("logical-dataset");
|
|
53
|
+
const attrs = client ? { via: "bucket-literal", client } : { via: "bucket-literal" };
|
|
54
|
+
ctx.addEdge("usesResource", compRef, matches[0].ref, attrs, [pr], "DERIVED");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!usesStore && !bindings.length) return {};
|
|
58
|
+
return bindings.length ? { bindings } : { objectStoreClientOnly: true };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function token(name) {
|
|
63
|
+
return name.split("-")[0];
|
|
64
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// OIDC adapter.
|
|
2
|
+
//
|
|
3
|
+
// The canonical authority for an OIDC surface is the published standard plus
|
|
4
|
+
// the provider's discovery document, both of which live outside the repository
|
|
5
|
+
// (EXTERNAL_STANDARD). What the repository can establish is narrower and is
|
|
6
|
+
// recorded as such: which units implement a client against the declared
|
|
7
|
+
// discovery path, and the fact that the provider exposes that path.
|
|
8
|
+
//
|
|
9
|
+
// A discovery document fetched from a live deployment is an OBSERVED_RUNTIME
|
|
10
|
+
// fact and belongs in an ObservationSet, not here.
|
|
11
|
+
import { Adapter } from "./registry.mjs";
|
|
12
|
+
import { sourceFiles } from "../core/sourcescan.mjs";
|
|
13
|
+
|
|
14
|
+
// A component is an OIDC client when it both names the discovery surface and
|
|
15
|
+
// shows the shape of a token request. One of the two alone is not enough: a
|
|
16
|
+
// comment mentioning /oidc/token, or a bare import of an OAuth helper in a
|
|
17
|
+
// component that never calls the identity provider, would both produce a false
|
|
18
|
+
// edge, and a false edge is worse than a missing one.
|
|
19
|
+
const DISCOVERY_HINT = /(\.well-known\/openid-configuration|jwks_uri|token_endpoint|\/oidc\/token|oauth2?\/token)/i;
|
|
20
|
+
// Both naming conventions: the wire names (snake_case, as in JSON) and the
|
|
21
|
+
// field names a typed client uses (camelCase, as in Go and TypeScript).
|
|
22
|
+
const TOKEN_REQUEST_HINT = /(grant_?type|client_?id|client_?secret|jwks|authoriz|id_?token|access_?token|userinfo|refresh_?token)/i;
|
|
23
|
+
|
|
24
|
+
export class OidcAdapter extends Adapter {
|
|
25
|
+
static adapterName = "oidc";
|
|
26
|
+
constructor() {
|
|
27
|
+
super({ name: "oidc", version: "1.0.0", kind: "contract" });
|
|
28
|
+
}
|
|
29
|
+
contracts(ctx) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const api of ctx.sources.entities.filter((e) => e.kind === "API" && e.doc.spec.type === "oidc")) {
|
|
32
|
+
const discoveryPath = api.doc.spec.contract.discoveryPath;
|
|
33
|
+
const providerFile = api.file;
|
|
34
|
+
const providerRef = api.doc.spec.provider;
|
|
35
|
+
const hosts = new Map();
|
|
36
|
+
for (const unit of ctx.discovery.eligible) {
|
|
37
|
+
if (!unit.component) continue;
|
|
38
|
+
for (const f of sourceFiles(ctx.repo, unit.root, { test: false })) {
|
|
39
|
+
const text = ctx.repo.readText(f);
|
|
40
|
+
const namesDiscovery = text.includes(discoveryPath) || DISCOVERY_HINT.test(text);
|
|
41
|
+
if (namesDiscovery && TOKEN_REQUEST_HINT.test(text)) {
|
|
42
|
+
hosts.set(unit.component.ref, f);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
hosts.delete(providerRef);
|
|
48
|
+
for (const [ref, file] of [...hosts.entries()].sort()) {
|
|
49
|
+
const pr = ctx.prov.add("validated", file, "oidc-extractor", ["rel:pending"]);
|
|
50
|
+
ctx.addEdge("consumesApi", ref, api.ref, { via: "oidc-client" }, [pr], "DERIVED");
|
|
51
|
+
}
|
|
52
|
+
ctx.addContract({ apiRef: api.ref, ref: providerFile, standard: api.doc.spec.contract.standard, discoveryPath, consumers: [...hosts.keys()].sort() });
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// OpenAPI adapter.
|
|
2
|
+
//
|
|
3
|
+
// The catalog points at the canonical contract; it never duplicates it. What
|
|
4
|
+
// the adapter adds is (a) the operation inventory an agent needs to find a
|
|
5
|
+
// handler without reading the whole spec, and (b) deterministic consumer
|
|
6
|
+
// resolution from the contract reference.
|
|
7
|
+
import { Adapter } from "./registry.mjs";
|
|
8
|
+
import { resolveContractConsumers, isBuildConfig } from "./contract-consumers.mjs";
|
|
9
|
+
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
10
|
+
import { parse as parseToml } from "../core/toml.mjs";
|
|
11
|
+
|
|
12
|
+
const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
13
|
+
|
|
14
|
+
export class OpenApiAdapter extends Adapter {
|
|
15
|
+
static adapterName = "openapi";
|
|
16
|
+
constructor() {
|
|
17
|
+
super({ name: "openapi", version: "1.0.0", kind: "contract" });
|
|
18
|
+
}
|
|
19
|
+
contracts(ctx) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const api of ctx.sources.entities.filter((e) => e.kind === "API" && e.doc.spec.type === "openapi")) {
|
|
22
|
+
const ref = api.doc.spec.contract.ref;
|
|
23
|
+
let doc = null;
|
|
24
|
+
try {
|
|
25
|
+
doc = parseYamlDocuments(ctx.repo.readText(ref), ref)[0]?.doc || JSON.parse(ctx.repo.readText(ref, { track: false }));
|
|
26
|
+
} catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const operations = [];
|
|
30
|
+
for (const [route, item] of Object.entries((doc && doc.paths) || {})) {
|
|
31
|
+
for (const m of METHODS) {
|
|
32
|
+
if (item && typeof item === "object" && item[m]) {
|
|
33
|
+
operations.push({ method: m.toUpperCase(), route, operationId: item[m].operationId || null });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
operations.sort((a, b) => (a.route + a.method < b.route + b.method ? -1 : 1));
|
|
38
|
+
ctx.addContract({ apiRef: api.ref, ref, operations, title: doc?.info?.title || null });
|
|
39
|
+
|
|
40
|
+
const consumers = resolveContractConsumers(ctx, ref, api.doc.spec.provider);
|
|
41
|
+
for (const [ref2, files] of consumers) {
|
|
42
|
+
const provs = files.map((f) => ctx.prov.add(isBuildConfig(f) ? "validated" : "observed", f, "api-contract-extractor", ["rel:pending"]));
|
|
43
|
+
ctx.addEdge("consumesApi", ref2, api.ref, { via: files.some(isBuildConfig) ? "codegen" : "source-reference" }, provs, "DERIVED");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { parseToml };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// pnpm workspace adapter: contributes workspace member roots so the descriptor
|
|
2
|
+
// globs do not have to know the layout, and records the workspace file as
|
|
3
|
+
// evidence for the repository as a whole.
|
|
4
|
+
import { Adapter } from "./registry.mjs";
|
|
5
|
+
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
6
|
+
|
|
7
|
+
export class PnpmAdapter extends Adapter {
|
|
8
|
+
static adapterName = "pnpm";
|
|
9
|
+
constructor() {
|
|
10
|
+
super({ name: "pnpm", version: "1.0.0", kind: "workspace" });
|
|
11
|
+
}
|
|
12
|
+
roots(ctx) {
|
|
13
|
+
const out = [];
|
|
14
|
+
const ws = "pnpm-workspace.yaml";
|
|
15
|
+
if (!ctx.repo.exists(ws)) return out;
|
|
16
|
+
let docs;
|
|
17
|
+
try {
|
|
18
|
+
docs = parseYamlDocuments(ctx.repo.readText(ws), ws);
|
|
19
|
+
} catch {
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
const globs = (docs[0] && docs[0].doc && docs[0].doc.packages) || [];
|
|
23
|
+
ctx.repo.readText(ws);
|
|
24
|
+
for (const g of Array.isArray(globs) ? globs : []) {
|
|
25
|
+
if (typeof g !== "string" || g.includes("..")) continue;
|
|
26
|
+
const segs = g.split("/").filter((s) => s.length && s !== "**");
|
|
27
|
+
const starAt = segs.findIndex((s) => s === "*");
|
|
28
|
+
if (starAt < 0) continue;
|
|
29
|
+
const base = segs.slice(0, starAt).join("/");
|
|
30
|
+
if (!base || !ctx.repo.isDir(base)) continue;
|
|
31
|
+
// One level of expansion is enough to enumerate members; deeper layouts
|
|
32
|
+
// are picked up by the descriptor globs.
|
|
33
|
+
for (const entry of ctx.repo.listDir(base)) {
|
|
34
|
+
if (!entry.endsWith("/")) continue;
|
|
35
|
+
const dir = base + "/" + entry.replace(/\/$/, "");
|
|
36
|
+
if (!ctx.repo.isDir(dir)) continue;
|
|
37
|
+
if (ctx.repo.exists(dir + "/package.json")) out.push({ root: dir, importable: true, deployable: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Protobuf adapter: .proto files as canonical contracts, with the service and
|
|
2
|
+
// method inventory an agent needs and deterministic consumer resolution.
|
|
3
|
+
import { Adapter } from "./registry.mjs";
|
|
4
|
+
import { resolveContractConsumers, isBuildConfig } from "./contract-consumers.mjs";
|
|
5
|
+
|
|
6
|
+
export class ProtobufAdapter extends Adapter {
|
|
7
|
+
static adapterName = "protobuf";
|
|
8
|
+
constructor() {
|
|
9
|
+
super({ name: "protobuf", 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" && e.doc.spec.type === "protobuf")) {
|
|
14
|
+
const ref = api.doc.spec.contract.ref;
|
|
15
|
+
const text = ctx.repo.readText(ref);
|
|
16
|
+
const methods = [];
|
|
17
|
+
const packages = [];
|
|
18
|
+
for (const m of text.matchAll(/^\s*package\s+([A-Za-z0-9_.]+)\s*;/gm)) packages.push(m[1]);
|
|
19
|
+
for (const m of text.matchAll(/service\s+([A-Za-z0-9_]+)\s*\{([\s\S]*?)\n\}/g)) {
|
|
20
|
+
for (const r of m[2].matchAll(/rpc\s+([A-Za-z0-9_]+)\s*\(\s*(stream\s+)?([A-Za-z0-9_.]+)/g)) {
|
|
21
|
+
methods.push({ service: m[1], method: r[1], request: r[3], streaming: Boolean(r[2]) });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
methods.sort((a, b) => ((a.service + a.method) < (b.service + b.method) ? -1 : 1));
|
|
25
|
+
ctx.addContract({ apiRef: api.ref, ref, services: [...new Set(text.match(/service\s+([A-Za-z0-9_]+)/g) || [])].length, methods, packages: [...new Set(packages)].sort() });
|
|
26
|
+
|
|
27
|
+
const consumers = resolveContractConsumers(ctx, ref, api.doc.spec.provider);
|
|
28
|
+
for (const [consumerRef, files] of consumers) {
|
|
29
|
+
const provs = files.map((f) => ctx.prov.add(isBuildConfig(f) ? "validated" : "observed", f, "api-contract-extractor", ["rel:pending"]));
|
|
30
|
+
ctx.addEdge("consumesApi", consumerRef, api.ref, { via: files.some(isBuildConfig) ? "codegen" : "source-reference" }, provs, "DERIVED");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Adapter contract.
|
|
2
|
+
//
|
|
3
|
+
// An adapter knows one technology's vocabulary: how to recognise a unit root,
|
|
4
|
+
// how to read its manifest, and which deterministic facts it can extract. It
|
|
5
|
+
// must NOT know about the repository's folder layout, its domain names, its
|
|
6
|
+
// event names or its providers. Everything project-specific arrives through
|
|
7
|
+
// agentdoc.config.yaml.
|
|
8
|
+
//
|
|
9
|
+
// Two phases per unit:
|
|
10
|
+
// detect(repo, root) -> does this root host a unit of my kind?
|
|
11
|
+
// extract(ctx, unit) -> deterministic facts + provenance
|
|
12
|
+
//
|
|
13
|
+
// Adapters are pure: same checkout, same facts, same order.
|
|
14
|
+
export class Adapter {
|
|
15
|
+
constructor({ name, version = "1.0.0", kind = "generic" }) {
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.version = version;
|
|
18
|
+
this.kind = kind; // generic | unit-marker | manifest | source | contract | ci
|
|
19
|
+
}
|
|
20
|
+
// Unit-marker adapters answer: does `root` contain a unit I recognise?
|
|
21
|
+
// Returning a marker path is what makes the root eligible.
|
|
22
|
+
detect(ctx, root) { return null; }
|
|
23
|
+
// Manifest/source adapters contribute facts for an already-eligible unit.
|
|
24
|
+
extract(ctx, unit) {}
|
|
25
|
+
// Contract adapters validate and index contract files listed by config.
|
|
26
|
+
contracts(ctx) { return []; }
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Client-of-resource resolution.
|
|
2
|
+
//
|
|
3
|
+
// A component that depends on a declared external service also *uses* the
|
|
4
|
+
// resource that service fronts, when exactly one authored Resource of the
|
|
5
|
+
// declared type exists. A message-bus client binds to the event bus; a cache
|
|
6
|
+
// client to the cache.
|
|
7
|
+
//
|
|
8
|
+
// The mapping is declared in configuration rather than inferred, because
|
|
9
|
+
// "this npm package talks to our bus" is a fact about the project, not about
|
|
10
|
+
// the package. When more than one Resource of the type exists the fact is left
|
|
11
|
+
// unresolved: an ambiguous match must never become an edge.
|
|
12
|
+
|
|
13
|
+
const POOL_HINT = /(createPool|new Pool|pg\.Pool|pgxpool|sql\.Open|mysql\.createConnection|Pool\(|DataSource|prisma\.(datasource|client)|DATABASE_URL|DB_DSN|redis|minio|Client\()/;
|
|
14
|
+
|
|
15
|
+
export function resolveClientResources(ctx, unit, compRef, provRec) {
|
|
16
|
+
const declared = ctx.externalMatchers().filter((x) => x.resourceType);
|
|
17
|
+
if (!declared.length) return [];
|
|
18
|
+
// The rule is "this unit depends on this service", so it is tested against
|
|
19
|
+
// the unit's production dependency names, not against its source text.
|
|
20
|
+
const deps = ctx.productionDependencies(compRef);
|
|
21
|
+
if (!deps.length) return [];
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const x of declared) {
|
|
24
|
+
if (!deps.some((d) => x.re.test(d))) continue;
|
|
25
|
+
const cands = ctx.sources.entities.filter(
|
|
26
|
+
(e) => e.kind === "Resource" && e.doc.spec.type === x.resourceType
|
|
27
|
+
);
|
|
28
|
+
if (cands.length !== 1) {
|
|
29
|
+
if (cands.length > 1) {
|
|
30
|
+
ctx.diagnostics.push({
|
|
31
|
+
severity: "warning",
|
|
32
|
+
code: "AGENTDOC_BINDING_AMBIGUOUS",
|
|
33
|
+
subject: compRef,
|
|
34
|
+
refs: [compRef],
|
|
35
|
+
message:
|
|
36
|
+
"component '" + compRef + "' depends on " + x.name + " and there are " + cands.length +
|
|
37
|
+
" authored Resources of type " + x.resourceType + "; the binding is left unresolved",
|
|
38
|
+
paths: [],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const target = cands[0];
|
|
44
|
+
// The matcher name is *data* — which declared client produced the edge — not
|
|
45
|
+
// a derivation route. Encoding it in `via` would put it outside the relation's
|
|
46
|
+
// identity, so a component depending on two matchers of the same Resource type
|
|
47
|
+
// would merge into one edge and the graph would state the dependency once
|
|
48
|
+
// instead of twice. `via` names the mechanism; `client` names the instance.
|
|
49
|
+
ctx.addEdge("usesResource", compRef, target.ref, { via: "declared-client", client: x.name }, [provRec], "DERIVED");
|
|
50
|
+
ctx.addFact(compRef, "resource.ownership", target.name, {
|
|
51
|
+
evidenceClass: "DERIVED",
|
|
52
|
+
confidence: "deterministic",
|
|
53
|
+
provRecs: [provRec],
|
|
54
|
+
semantics: "the component depends on " + x.name + ", declared as a client of the " + x.resourceType + " Resource",
|
|
55
|
+
});
|
|
56
|
+
out.push(target.ref);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { POOL_HINT };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Turborepo adapter: records task-graph driven verification commands and the
|
|
2
|
+
// pipeline they belong to. Turborepo knows how to run a task for one package
|
|
3
|
+
// and how to filter by dependency graph, which makes it the most precise
|
|
4
|
+
// verification answer available for a monorepo unit.
|
|
5
|
+
import { Adapter } from "./registry.mjs";
|
|
6
|
+
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
7
|
+
|
|
8
|
+
// Whether a declared task is a check is decided by `taskTier` alone, not by a
|
|
9
|
+
// second, narrower pattern. Two lists meant two places to disagree: a task that
|
|
10
|
+
// `TIERS` recognised — `e2e`, `playwright`, `drift:detect`, `generate:types` —
|
|
11
|
+
// was silently dropped because this filter had never heard of it, and the
|
|
12
|
+
// "untiered task" diagnostic could not fire for it either, since it sits after
|
|
13
|
+
// the filter. A verification entry that vanishes without a word is worse than
|
|
14
|
+
// one that is visibly untiered.
|
|
15
|
+
|
|
16
|
+
export class TurborepoAdapter extends Adapter {
|
|
17
|
+
static adapterName = "turborepo";
|
|
18
|
+
constructor() {
|
|
19
|
+
super({ name: "turborepo", version: "1.0.0", kind: "ci" });
|
|
20
|
+
}
|
|
21
|
+
detect(ctx, root) {
|
|
22
|
+
const p = root + "/turbo.json";
|
|
23
|
+
if (!ctx.repo.exists(p)) return null;
|
|
24
|
+
return { path: p, shape: "importable" };
|
|
25
|
+
}
|
|
26
|
+
extract(ctx, unit) {
|
|
27
|
+
const { repo, prov, compRef } = ctx;
|
|
28
|
+
const p = nearestTurboConfig(repo, unit.root);
|
|
29
|
+
if (!p) return {};
|
|
30
|
+
let doc;
|
|
31
|
+
try {
|
|
32
|
+
doc = JSON.parse(repo.readText(p));
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
// Every declared task is considered; `taskTier` decides which are checks.
|
|
37
|
+
// Filtering by a second pattern first is what silently dropped the tasks
|
|
38
|
+
// that pattern had never heard of.
|
|
39
|
+
const tasks = Object.keys(doc.tasks || doc.pipeline || {}).sort();
|
|
40
|
+
if (!tasks.length) return {};
|
|
41
|
+
const pr = prov.add("observed", p, "taskgraph-extractor", tasks.map((t) => "ver:tg:" + compRef + ":" + t));
|
|
42
|
+
const pkgName = unit.pkgName;
|
|
43
|
+
const scripts = Object.keys((unit.pkg || {}).scripts || {});
|
|
44
|
+
const emitted = [];
|
|
45
|
+
for (const t of tasks) {
|
|
46
|
+
// A task-graph command is only real if this unit actually declares the
|
|
47
|
+
// task. `turbo run test --filter=./apps/notifications` is not a command
|
|
48
|
+
// when that package has no `test` script, and emitting it would put an
|
|
49
|
+
// unrunnable command in front of an agent as if it were the answer.
|
|
50
|
+
if (!scripts.includes(t)) continue;
|
|
51
|
+
const tier = taskTier(t);
|
|
52
|
+
if (!tier) {
|
|
53
|
+
// A task with no tier is usually an ordinary package script (`dev`,
|
|
54
|
+
// `docs`, `release`) and not a missing verification entry, so it is
|
|
55
|
+
// dropped silently. A task that *looks* like a check but cannot be
|
|
56
|
+
// tiered is a gap worth reporting, and this is the only place that can
|
|
57
|
+
// tell the two apart.
|
|
58
|
+
// A check task with no tier would otherwise vanish with no trace, and a
|
|
59
|
+
// verification entry that is silently missing is worse than one that is
|
|
60
|
+
// visibly untiered: nothing is left to notice the gap.
|
|
61
|
+
ctx.diagnostics.push({
|
|
62
|
+
severity: "warning",
|
|
63
|
+
code: "AGENTDOC_VERIFICATION_UNTIERED",
|
|
64
|
+
subject: compRef,
|
|
65
|
+
refs: [compRef, p],
|
|
66
|
+
message:
|
|
67
|
+
"check task '" + t + "' has no recognisable tier and was not recorded as a verification entry. " +
|
|
68
|
+
"Name the task test/lint/typecheck/build/e2e/integration/contract, or extend the adapter.",
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const filter = pkgName ? " --filter=" + pkgName : " --filter=./" + unit.root;
|
|
73
|
+
ctx.addVerification({
|
|
74
|
+
id: "tg:" + compRef + ":" + t,
|
|
75
|
+
componentRefs: [compRef],
|
|
76
|
+
tier,
|
|
77
|
+
command: "turbo run " + t + filter,
|
|
78
|
+
configPaths: [p],
|
|
79
|
+
provRecs: [pr],
|
|
80
|
+
});
|
|
81
|
+
emitted.push(t);
|
|
82
|
+
}
|
|
83
|
+
for (const t of tasks) if (!emitted.includes(t)) pr.subjects.delete("ver:tg:" + compRef + ":" + t);
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
contracts() {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The tier is derived from the whole task name, never from a fragment of it. A
|
|
92
|
+
// mis-tiered check is worse than an absent one: it is copied into a change set
|
|
93
|
+
// as though it proved something it does not, and an agent will run the cheap
|
|
94
|
+
// thing and believe the expensive thing was covered.
|
|
95
|
+
// A watch task is an interactive session, not a check. Recording `test:watch`
|
|
96
|
+
// as a runnable verification command hands an agent a process that never exits.
|
|
97
|
+
const NOT_A_CHECK = /(^|[:_-])(watch|dev|serve|start|preview)([:_-]|$)/;
|
|
98
|
+
|
|
99
|
+
const TIERS = [
|
|
100
|
+
// Order matters: the most specific reading of a name wins. `e2e-lite` is an
|
|
101
|
+
// integration suite that happens to contain "e2e", so it must be read before
|
|
102
|
+
// the e2e row rather than after it.
|
|
103
|
+
[/(^|[:_-])e2e-lite([:_-]|$)/, "integration"],
|
|
104
|
+
[/(^|[:_-])(e2e|playwright|cypress|acceptance)([:_-]|$)/, "e2e"],
|
|
105
|
+
[/(^|[:_-])(contract|openapi|drift|schema)([:_-]|$)/, "contract"],
|
|
106
|
+
[/(^|[:_-])(integration|itest|e2e-lite)([:_-]|$)/, "integration"],
|
|
107
|
+
[/(^|[:_-])(lint|typecheck|check-types|type-check|golangci|vet|fmt|format|audit)([:_-]|$)/, "static"],
|
|
108
|
+
[/(^|[:_-])(unit|jest|vitest|mocha)([:_-]|$)/, "unit"],
|
|
109
|
+
[/(^|[:_-])(build|compile|generate|bundle)([:_-]|$)/, "build"],
|
|
110
|
+
// A bare `test`/`tests`/`verify`/`check` is a unit test by convention. A
|
|
111
|
+
// repository that disagrees should name the tier, as above. This row is last
|
|
112
|
+
// on purpose: `check:prettier` and `verify:lint` are formatting and lint
|
|
113
|
+
// checks wearing a `check` prefix, and reading them as unit tests would put a
|
|
114
|
+
// trivial command in front of an agent as if it covered behaviour.
|
|
115
|
+
[/(^|[:_-])(test|tests|verify|check)([:_-]|$)/, "unit"],
|
|
116
|
+
];
|
|
117
|
+
function taskTier(t) {
|
|
118
|
+
if (NOT_A_CHECK.test(t)) return null;
|
|
119
|
+
for (const [re, tier] of TIERS) if (re.test(t)) return tier;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// A task graph normally lives at the workspace root, not in each package.
|
|
124
|
+
function nearestTurboConfig(repo, root) {
|
|
125
|
+
let dir = root;
|
|
126
|
+
for (let i = 0; i < 12; i++) {
|
|
127
|
+
const p = dir ? dir + "/turbo.json" : "turbo.json";
|
|
128
|
+
if (repo.exists(p)) return p;
|
|
129
|
+
if (!dir) break;
|
|
130
|
+
dir = dir.split("/").slice(0, -1).join("/");
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { parseYamlDocuments };
|