agent-doc-system 0.0.0-stage → 1.0.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 +151 -2
- 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 +55 -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 +478 -0
- package/bin/usage.txt +26 -0
- package/core/acceptances.mjs +69 -0
- package/core/audit.mjs +407 -0
- package/core/authority/classes.mjs +39 -0
- package/core/authority/engine.mjs +243 -0
- package/core/authority/facts.mjs +41 -0
- package/core/codes.mjs +119 -0
- package/core/compile.mjs +881 -0
- package/core/contracts.mjs +121 -0
- package/core/derive.mjs +102 -0
- package/core/descriptors.mjs +486 -0
- package/core/determinism.mjs +95 -0
- package/core/discover.mjs +147 -0
- package/core/facts.mjs +53 -0
- package/core/fsx.mjs +342 -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/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 +475 -0
- package/docs/ADAPTERS.md +172 -0
- package/docs/AUTHORITY.md +260 -0
- package/docs/CI.md +136 -0
- package/docs/COMPARISON-KODA.md +164 -0
- package/docs/EVALS.md +166 -0
- package/docs/MIGRATION.md +193 -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/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 +107 -0
- package/schemas/component.schema.json +40 -0
- package/schemas/config.schema.json +685 -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 +113 -0
- package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
- package/skills/agent-doc-system/references/cli.md +89 -0
- package/skills/agent-doc-system/references/create-migrate.md +101 -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
package/core/query.mjs
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// Context router.
|
|
2
|
+
//
|
|
3
|
+
// The job is routing, not documentation concatenation. Given a source path or
|
|
4
|
+
// an entity ref, return the smallest set of facts an agent needs to work
|
|
5
|
+
// safely on that subject — and refuse to answer from a stale graph.
|
|
6
|
+
//
|
|
7
|
+
// Everything returned is budgeted: relations, interfaces, resources, journeys
|
|
8
|
+
// and verification entries are all capped, with the overflow reported rather
|
|
9
|
+
// than silently dropped, so an agent can see that it is seeing a slice.
|
|
10
|
+
import { AgentDocError, CODES } from "./codes.mjs";
|
|
11
|
+
import { assertNoSecretsInText } from "./secrets.mjs";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BUDGET = {
|
|
14
|
+
relations: 40,
|
|
15
|
+
events: 20,
|
|
16
|
+
externals: 15,
|
|
17
|
+
capabilities: 10,
|
|
18
|
+
verification: 12,
|
|
19
|
+
journeys: 10,
|
|
20
|
+
conflicts: 20,
|
|
21
|
+
assertions: 25,
|
|
22
|
+
provenance: 20,
|
|
23
|
+
contracts: 10,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class ContextIndex {
|
|
27
|
+
constructor(graph) {
|
|
28
|
+
this.graph = graph;
|
|
29
|
+
this.byRef = new Map(graph.entities.map((e) => [e.ref, e]));
|
|
30
|
+
this.byPath = new Map();
|
|
31
|
+
for (const e of graph.entities) {
|
|
32
|
+
const sp = e.derived && e.derived.sourcePath;
|
|
33
|
+
if (!sp) continue;
|
|
34
|
+
this.byPath.set(sp, e);
|
|
35
|
+
// Longest path first so a nested path resolves to its own component
|
|
36
|
+
// rather than to a parent.
|
|
37
|
+
}
|
|
38
|
+
// Documentation and constraint files a descriptor links resolve to their
|
|
39
|
+
// component, even when they live outside the source root. Following a
|
|
40
|
+
// stale document to the code it describes is exactly the migration case.
|
|
41
|
+
this.byDoc = new Map();
|
|
42
|
+
for (const e of graph.entities) {
|
|
43
|
+
const ctxPaths = [
|
|
44
|
+
...(((e.entity.spec || {}).context || {}).docs || []),
|
|
45
|
+
...(((e.entity.spec || {}).context || {}).constraints || []),
|
|
46
|
+
...(((e.entity.spec || {}).context || {}).runbooks || []),
|
|
47
|
+
...(e.derived || {}).docs || [],
|
|
48
|
+
...(e.derived || {}).constraints || [],
|
|
49
|
+
...(e.derived || {}).runbooks || [],
|
|
50
|
+
];
|
|
51
|
+
for (const p of ctxPaths) if (!this.byDoc.has(p)) this.byDoc.set(p, e);
|
|
52
|
+
}
|
|
53
|
+
this.paths = [...this.byPath.keys()].sort((a, b) => b.length - a.length);
|
|
54
|
+
this.relationsByRef = new Map();
|
|
55
|
+
for (const r of graph.relations) {
|
|
56
|
+
if (!this.relationsByRef.has(r.sourceRef)) this.relationsByRef.set(r.sourceRef, []);
|
|
57
|
+
if (!this.relationsByRef.has(r.targetRef)) this.relationsByRef.set(r.targetRef, []);
|
|
58
|
+
this.relationsByRef.get(r.sourceRef).push(r);
|
|
59
|
+
this.relationsByRef.get(r.targetRef).push(r);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A contract file resolves to the API entity that claims it, so "change this
|
|
64
|
+
// endpoint" routes to the boundary rather than to nothing.
|
|
65
|
+
contractFor(needle) {
|
|
66
|
+
const norm = needle.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
67
|
+
let best = null;
|
|
68
|
+
for (const c of (this.graph.contracts || [])) {
|
|
69
|
+
if (norm === c.ref || norm.startsWith(c.ref)) {
|
|
70
|
+
if (!best || c.ref.length > best.ref.length) best = c;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return best;
|
|
74
|
+
}
|
|
75
|
+
resolve(needle) {
|
|
76
|
+
if (this.byRef.has(needle)) return this.byRef.get(needle);
|
|
77
|
+
const norm = needle.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
78
|
+
if (this.byDoc.has(norm)) return this.byDoc.get(norm);
|
|
79
|
+
if (this.byPath.has(norm)) return this.byPath.get(norm);
|
|
80
|
+
for (const p of this.paths) {
|
|
81
|
+
if (norm === p || norm.startsWith(p + "/")) return this.byPath.get(p);
|
|
82
|
+
}
|
|
83
|
+
for (const p of this.paths) {
|
|
84
|
+
if (needle.startsWith(p + "/")) return this.byPath.get(p);
|
|
85
|
+
}
|
|
86
|
+
if (this.byDoc.has(needle)) return this.byDoc.get(needle);
|
|
87
|
+
const c = this.contractFor(needle);
|
|
88
|
+
return c ? this.byRef.get(c.apiRef) || null : null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function contractsOf(graph) {
|
|
93
|
+
return graph.contracts || [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function queryContext(graph, needle, opts = {}) {
|
|
97
|
+
const budget = { ...DEFAULT_BUDGET, ...(opts.budget || {}) };
|
|
98
|
+
const idx = new ContextIndex(graph);
|
|
99
|
+
const entity = idx.resolve(needle);
|
|
100
|
+
if (!entity) {
|
|
101
|
+
throw new AgentDocError(
|
|
102
|
+
CODES.REF_UNRESOLVED,
|
|
103
|
+
"no entity resolves for '" + needle + "' — run `agentdoc audit` to see what the catalog knows about this repository"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const ref = entity.ref;
|
|
107
|
+
const g = graph;
|
|
108
|
+
const take = (arr, n) => ({ items: arr.slice(0, n), total: arr.length, truncated: arr.length > n });
|
|
109
|
+
|
|
110
|
+
const relations = idx.relationsByRef.get(ref) || [];
|
|
111
|
+
const neighbourRefs = new Set();
|
|
112
|
+
for (const r of relations) neighbourRefs.add(r.targetRef);
|
|
113
|
+
|
|
114
|
+
const placement = relations
|
|
115
|
+
.filter((r) => (r.type === "partOf" || r.type === "hasPart") && (r.sourceRef === ref || r.targetRef === ref))
|
|
116
|
+
.map((r) => (r.sourceRef === ref ? r.targetRef : r.sourceRef));
|
|
117
|
+
const systems = [...new Set(placement.filter((p) => p.startsWith("system:")))];
|
|
118
|
+
const domains = [...new Set(placement.filter((p) => p.startsWith("domain:")))];
|
|
119
|
+
|
|
120
|
+
// For a component these are the APIs it serves; for an API entity the subject
|
|
121
|
+
// *is* the provided surface.
|
|
122
|
+
const providedApis = entity.kind === "API"
|
|
123
|
+
? [ref]
|
|
124
|
+
: g.relations.filter((r) => r.type === "providesApi" && r.sourceRef === ref).map((r) => r.targetRef);
|
|
125
|
+
const consumedApis = g.relations.filter((r) => r.type === "consumesApi" && r.sourceRef === ref).map((r) => r.targetRef);
|
|
126
|
+
// When the subject IS an API, the interesting question is who consumes it.
|
|
127
|
+
const apiConsumers = entity.kind === "API"
|
|
128
|
+
? g.relations.filter((r) => r.type === "consumesApi" && r.targetRef === ref).map((r) => r.sourceRef)
|
|
129
|
+
: [];
|
|
130
|
+
|
|
131
|
+
const providerOf = (apiRef) => {
|
|
132
|
+
const r = g.relations.find((x) => x.type === "apiProvidedBy" && x.sourceRef === apiRef);
|
|
133
|
+
return r ? r.targetRef : null;
|
|
134
|
+
};
|
|
135
|
+
const apiProvider = entity.kind === "API" ? providerOf(ref) : null;
|
|
136
|
+
|
|
137
|
+
const apiRefs = new Set([...providedApis, ...consumedApis]);
|
|
138
|
+
const srcPath = (entity.derived && entity.derived.sourcePath) || null;
|
|
139
|
+
const events = g.interfaces.events.filter(
|
|
140
|
+
(e) =>
|
|
141
|
+
e.producers.includes(ref) ||
|
|
142
|
+
e.consumers.includes(ref) ||
|
|
143
|
+
// A contract library that ships the subject catalog owns those subjects,
|
|
144
|
+
// even when another component is the one that subscribes to them.
|
|
145
|
+
(srcPath && e.contract && e.contract.ref.startsWith(srcPath + "/"))
|
|
146
|
+
);
|
|
147
|
+
const externals = g.externalDependencies.filter((e) => e.componentRef === ref);
|
|
148
|
+
const capabilities = g.capabilities.filter((e) => e.componentRef === ref);
|
|
149
|
+
// For a contract subject, the checks that matter are the provider's and the
|
|
150
|
+
// consumers': changing an endpoint is proved by running their suites.
|
|
151
|
+
const verificationScope = entity.kind === "API"
|
|
152
|
+
? [ref, ...apiConsumers, ...(apiProvider ? [apiProvider] : [])]
|
|
153
|
+
: [ref];
|
|
154
|
+
const verification = g.verification.filter((e) => e.componentRefs.some((r) => verificationScope.includes(r)));
|
|
155
|
+
const journeys = g.journeys.filter((j) => j.components.includes(ref));
|
|
156
|
+
const contracts = g.contracts.filter((c) => c.apiRef && apiRefs.has(c.apiRef));
|
|
157
|
+
const conflicts = g.conflicts.filter((c) => c.subject === ref);
|
|
158
|
+
const assertions = g.assertions.filter((a) => a.subject === ref);
|
|
159
|
+
const resources = g.relations
|
|
160
|
+
.filter((r) => r.type === "usesResource" && r.sourceRef === ref)
|
|
161
|
+
.map((r) => r.targetRef);
|
|
162
|
+
const affectedByResources = g.relations
|
|
163
|
+
.filter((r) => r.type === "usesResource" && r.targetRef === ref)
|
|
164
|
+
.map((r) => r.sourceRef);
|
|
165
|
+
|
|
166
|
+
const provIds = new Set();
|
|
167
|
+
const collect = (obj) => {
|
|
168
|
+
for (const id of obj.provenanceIds || []) provIds.add(id);
|
|
169
|
+
};
|
|
170
|
+
collect(entity);
|
|
171
|
+
relations.forEach(collect);
|
|
172
|
+
events.forEach(collect);
|
|
173
|
+
externals.forEach(collect);
|
|
174
|
+
verification.forEach(collect);
|
|
175
|
+
journeys.forEach(collect);
|
|
176
|
+
contracts.forEach(collect);
|
|
177
|
+
conflicts.forEach(collect);
|
|
178
|
+
assertions.forEach(collect);
|
|
179
|
+
const provenance = g.provenance.filter((p) => provIds.has(p.id));
|
|
180
|
+
|
|
181
|
+
const journeyPeers = new Set();
|
|
182
|
+
for (const j of journeys) for (const c of j.components) if (c !== ref) journeyPeers.add(c);
|
|
183
|
+
|
|
184
|
+
const neighbourSummaries = [...neighbourRefs]
|
|
185
|
+
.filter((r) => r !== ref)
|
|
186
|
+
.sort()
|
|
187
|
+
.map((r) => {
|
|
188
|
+
const e = idx.byRef.get(r);
|
|
189
|
+
return e ? { ref: r, kind: e.kind, name: e.name, description: e.entity.metadata.description } : { ref: r };
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const out = {
|
|
193
|
+
query: { needle, resolved: ref, namespace: ref.split("/")[0].split(":")[1] },
|
|
194
|
+
entity: {
|
|
195
|
+
ref,
|
|
196
|
+
kind: entity.kind,
|
|
197
|
+
name: entity.name,
|
|
198
|
+
description: entity.entity.metadata.description,
|
|
199
|
+
spec: entity.entity.spec || {},
|
|
200
|
+
sourcePaths: entity.sourcePaths,
|
|
201
|
+
derived: entity.derived || {},
|
|
202
|
+
},
|
|
203
|
+
placement: { systems, domains },
|
|
204
|
+
relations: take(
|
|
205
|
+
relations.map((r) => ({
|
|
206
|
+
type: r.type,
|
|
207
|
+
direction: r.sourceRef === ref ? "out" : "in",
|
|
208
|
+
other: r.sourceRef === ref ? r.targetRef : r.sourceRef,
|
|
209
|
+
evidenceClass: r.evidenceClass,
|
|
210
|
+
attributes: Object.keys(r.attributes || {}).length ? r.attributes : undefined,
|
|
211
|
+
provenanceIds: r.provenanceIds,
|
|
212
|
+
})),
|
|
213
|
+
budget.relations
|
|
214
|
+
),
|
|
215
|
+
neighbours: neighbourSummaries,
|
|
216
|
+
apis: {
|
|
217
|
+
provided: providedApis.map((a) => ({ api: a, contract: contractRef(g, a) })),
|
|
218
|
+
consumed: consumedApis.map((a) => ({ api: a, provider: providerOf(a), contract: contractRef(g, a) })),
|
|
219
|
+
provider: apiProvider,
|
|
220
|
+
consumers: [...new Set(apiConsumers)].sort(),
|
|
221
|
+
contract: entity.kind === "API" ? contractRef(g, ref) : null,
|
|
222
|
+
},
|
|
223
|
+
events: take(
|
|
224
|
+
events.map((e) => ({ pattern: e.pattern, producers: e.producers, consumers: e.consumers, resolution: e.resolution, contract: e.contract })),
|
|
225
|
+
budget.events
|
|
226
|
+
),
|
|
227
|
+
resources: { used: resources, usedBy: affectedByResources },
|
|
228
|
+
externalDependencies: take(
|
|
229
|
+
externals.map((e) => ({ name: e.name, role: e.role, mechanism: e.mechanism, evidenceClass: e.evidenceClass })),
|
|
230
|
+
budget.externals
|
|
231
|
+
),
|
|
232
|
+
capabilities: take(capabilities.map((c) => ({ name: c.name, evidenceClass: c.evidenceClass })), budget.capabilities),
|
|
233
|
+
constraints: {
|
|
234
|
+
docs: (entity.derived && entity.derived.docs) || [],
|
|
235
|
+
constraints: (entity.derived && entity.derived.constraints) || [],
|
|
236
|
+
runbooks: (entity.derived && entity.derived.runbooks) || [],
|
|
237
|
+
},
|
|
238
|
+
verification: take(
|
|
239
|
+
verification.map((v) => ({ id: v.id, tier: v.tier, command: v.command, configPaths: v.configPaths })),
|
|
240
|
+
budget.verification
|
|
241
|
+
),
|
|
242
|
+
journeys: take(
|
|
243
|
+
journeys.map((j) => ({ id: j.id, description: j.description, components: j.components })),
|
|
244
|
+
budget.journeys
|
|
245
|
+
),
|
|
246
|
+
// A journey is a claim that these components act together. Touching one of
|
|
247
|
+
// them means the others are in scope, and naming them is cheaper for the
|
|
248
|
+
// agent than rediscovering the coupling.
|
|
249
|
+
journeyPeers: [...journeyPeers].sort(),
|
|
250
|
+
contracts: take(contracts, budget.contracts),
|
|
251
|
+
authority: {
|
|
252
|
+
conflicts: take(
|
|
253
|
+
conflicts.map((c) => ({
|
|
254
|
+
id: c.id, key: c.key, kind: c.kind, status: c.status,
|
|
255
|
+
detail: c.detail, election: c.election,
|
|
256
|
+
assertions: c.assertionIds.map((id) => {
|
|
257
|
+
const a = g.assertions.find((x) => x.id === id);
|
|
258
|
+
return a ? { id: a.id, evidenceClass: a.evidenceClass, confidence: a.confidence, value: a.value, status: a.status, semantics: a.semantics, observed: a.observed } : { id };
|
|
259
|
+
}),
|
|
260
|
+
})),
|
|
261
|
+
budget.conflicts
|
|
262
|
+
),
|
|
263
|
+
assertions: take(
|
|
264
|
+
assertions.map((a) => ({
|
|
265
|
+
id: a.id, key: a.key, value: a.value, evidenceClass: a.evidenceClass,
|
|
266
|
+
confidence: a.confidence, status: a.status, semantics: a.semantics,
|
|
267
|
+
})),
|
|
268
|
+
budget.assertions
|
|
269
|
+
),
|
|
270
|
+
observations: g.observations
|
|
271
|
+
.filter((o) => o.facts.some((f) => f.subject === ref))
|
|
272
|
+
.map((o) => ({
|
|
273
|
+
id: o.id, environment: o.environment, sourceSystem: o.sourceSystem,
|
|
274
|
+
collector: o.collector, capturedAt: o.capturedAt, maxAgeDays: o.maxAgeDays,
|
|
275
|
+
evidenceBundle: o.evidenceBundle,
|
|
276
|
+
facts: o.facts.filter((f) => f.subject === ref),
|
|
277
|
+
})),
|
|
278
|
+
},
|
|
279
|
+
provenance: take(provenance.map((p) => ({ id: p.id, class: p.class, path: p.path, extractor: p.extractor.name, contentHash: p.contentHash })), budget.provenance),
|
|
280
|
+
globalGates: g.gates.map((x) => ({ id: x.id, tier: x.tier, command: x.command })),
|
|
281
|
+
// Everything a reviewer committed to re-examining, in one place. A review
|
|
282
|
+
// condition nobody can see is a review condition nobody performs.
|
|
283
|
+
reviewCommitments: [
|
|
284
|
+
...g.conflicts
|
|
285
|
+
.filter((c) => c.status !== "unresolved")
|
|
286
|
+
.map((c) => ({
|
|
287
|
+
kind: c.kind, subject: c.subject, key: c.key, status: c.status,
|
|
288
|
+
basis: c.election.basis, ruleId: c.election.ruleId,
|
|
289
|
+
// The governing authority rule's own re-examination condition. When
|
|
290
|
+
// there is no rule the fact stayed unresolved, and `null` says exactly
|
|
291
|
+
// that — it is not a missing value to be papered over with a pointer
|
|
292
|
+
// to a record that was never written.
|
|
293
|
+
reviewWhen: c.election.reviewWhen ?? null,
|
|
294
|
+
})),
|
|
295
|
+
...g.diagnostics
|
|
296
|
+
.filter((d) => d.acceptance)
|
|
297
|
+
.map((d) => ({
|
|
298
|
+
kind: d.code, subject: d.subject || null, status: "accepted",
|
|
299
|
+
classification: d.acceptance.classification,
|
|
300
|
+
reason: d.acceptance.reason, reviewWhen: d.acceptance.reviewWhen,
|
|
301
|
+
})),
|
|
302
|
+
...g.assertions
|
|
303
|
+
.filter((a) => a.review)
|
|
304
|
+
.map((a) => ({
|
|
305
|
+
kind: "reviewed-override", subject: a.subject, key: a.key, status: a.status,
|
|
306
|
+
reason: a.review.reason, reviewWhen: a.review.reviewWhen,
|
|
307
|
+
})),
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
// Truncation is reported for every capped section, including the nested ones
|
|
311
|
+
// inside `authority`. A silently shortened conflict list is the most damaging
|
|
312
|
+
// cut this command could make.
|
|
313
|
+
out.truncated = [];
|
|
314
|
+
for (const [k, v] of Object.entries(out)) {
|
|
315
|
+
if (v && typeof v === "object" && v.truncated) out.truncated.push(k);
|
|
316
|
+
if (k === "authority" && v) {
|
|
317
|
+
for (const [ak, av] of Object.entries(v)) {
|
|
318
|
+
if (av && typeof av === "object" && av.truncated) out.truncated.push("authority." + ak);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
assertNoSecretsInText(JSON.stringify(out), "query result");
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function contractRef(graph, apiRef) {
|
|
327
|
+
const e = graph.entities.find((x) => x.ref === apiRef);
|
|
328
|
+
return e ? e.entity.spec.contract : null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Compact markdown rendering for a prompt or a human. Same routing, less tokens.
|
|
332
|
+
export function renderContext(ctx) {
|
|
333
|
+
const L = [];
|
|
334
|
+
const e = ctx.entity;
|
|
335
|
+
L.push("# " + e.ref + " (" + e.kind + ")");
|
|
336
|
+
L.push(e.description);
|
|
337
|
+
if (e.derived.sourcePath) L.push("- source: " + e.derived.sourcePath);
|
|
338
|
+
if (e.derived.runtimes?.length) L.push("- runtimes: " + e.derived.runtimes.join(", "));
|
|
339
|
+
if (e.derived.languages?.length) L.push("- languages: " + e.derived.languages.join(", "));
|
|
340
|
+
if (ctx.placement.systems.length) L.push("- system: " + ctx.placement.systems.join(", "));
|
|
341
|
+
if (ctx.placement.domains.length) L.push("- domain: " + ctx.placement.domains.join(", "));
|
|
342
|
+
if (ctx.apis.provided.length) L.push("- provides: " + ctx.apis.provided.map((a) => a.api).join(", "));
|
|
343
|
+
if (ctx.apis.consumed.length) L.push("- consumes: " + ctx.apis.consumed.map((a) => a.api + " (via " + a.provider + ")").join(", "));
|
|
344
|
+
if (ctx.resources.used.length) L.push("- uses resource: " + ctx.resources.used.join(", "));
|
|
345
|
+
if (ctx.events.items.length) L.push("- events: " + ctx.events.items.map((x) => x.pattern + " [" + x.resolution + "]").join(", "));
|
|
346
|
+
if (ctx.externalDependencies.items.length) L.push("- external: " + ctx.externalDependencies.items.map((x) => x.name + " (" + x.role + ")").join(", "));
|
|
347
|
+
if (ctx.capabilities.items.length) L.push("- capabilities: " + ctx.capabilities.items.map((x) => x.name).join(", "));
|
|
348
|
+
if (ctx.constraints.constraints.length) L.push("- CONSTRAINTS: " + ctx.constraints.constraints.join(", "));
|
|
349
|
+
if (ctx.constraints.docs.length) L.push("- docs: " + ctx.constraints.docs.join(", "));
|
|
350
|
+
if (ctx.constraints.runbooks.length) L.push("- runbooks: " + ctx.constraints.runbooks.join(", "));
|
|
351
|
+
if (ctx.verification.items.length) {
|
|
352
|
+
L.push("- verify:");
|
|
353
|
+
for (const v of ctx.verification.items) L.push(" - [" + v.tier + "] " + v.command);
|
|
354
|
+
}
|
|
355
|
+
if (ctx.journeys.items.length) L.push("- journeys: " + ctx.journeys.items.map((j) => j.id).join(", "));
|
|
356
|
+
if (ctx.authority.conflicts.items.length) {
|
|
357
|
+
L.push("- CONFLICTS (do not assume one is true):");
|
|
358
|
+
for (const c of ctx.authority.conflicts.items) {
|
|
359
|
+
L.push(" - " + c.key + " [" + c.kind + "/" + c.status + "] " + (c.detail || ""));
|
|
360
|
+
for (const a of c.assertions) L.push(" - " + a.evidenceClass + "/" + a.confidence + ": " + JSON.stringify(a.value));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (ctx.reviewCommitments.length) {
|
|
364
|
+
L.push("- REVIEW COMMITMENTS (re-check when the condition is met):");
|
|
365
|
+
for (const r of ctx.reviewCommitments) {
|
|
366
|
+
// An absent condition is stated as absent. Printing a pointer to a record
|
|
367
|
+
// that does not exist is worse than printing nothing: it looks actionable
|
|
368
|
+
// and is not.
|
|
369
|
+
const when = r.reviewWhen
|
|
370
|
+
? r.reviewWhen
|
|
371
|
+
: "NO REVIEW CONDITION RECORDED — " + r.kind + " " + (r.subject || "") + " " + (r.key || "") +
|
|
372
|
+
" is settled with none; re-examine it now";
|
|
373
|
+
L.push(" - " + (r.subject || "repository") + " " + (r.key || r.kind) + ": " + when);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const obs = ctx.authority.observations;
|
|
377
|
+
if (obs.length) {
|
|
378
|
+
L.push("- RUNTIME OBSERVATIONS:");
|
|
379
|
+
for (const o of obs) L.push(" - " + o.environment + " via " + o.sourceSystem + " at " + o.capturedAt + " (evidence " + o.evidenceBundle + ")");
|
|
380
|
+
}
|
|
381
|
+
const neighbours = ctx.neighbours.filter((n) => n.ref !== e.ref);
|
|
382
|
+
if (neighbours.length) L.push("- neighbours: " + neighbours.map((n) => n.ref).join(", "));
|
|
383
|
+
return L.join("\n");
|
|
384
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Relation materialisation.
|
|
2
|
+
//
|
|
3
|
+
// No relation is ever authored. Placement comes from authored placement fields,
|
|
4
|
+
// provider edges from the API descriptor, everything else from deterministic
|
|
5
|
+
// extractors or bounded reviewed overrides. Inversions are generated, the kind
|
|
6
|
+
// pair is validated, and duplicates merge their provenance instead of forking
|
|
7
|
+
// the graph.
|
|
8
|
+
import { CODES, AgentDocError } from "./codes.mjs";
|
|
9
|
+
import { PROV_TO_EVIDENCE } from "./authority/classes.mjs";
|
|
10
|
+
|
|
11
|
+
export const INVERSE = Object.freeze({
|
|
12
|
+
partOf: "hasPart",
|
|
13
|
+
hasPart: "partOf",
|
|
14
|
+
providesApi: "apiProvidedBy",
|
|
15
|
+
apiProvidedBy: "providesApi",
|
|
16
|
+
consumesApi: "apiConsumedBy",
|
|
17
|
+
apiConsumedBy: "consumesApi",
|
|
18
|
+
buildDependsOn: "buildDependencyOf",
|
|
19
|
+
buildDependencyOf: "buildDependsOn",
|
|
20
|
+
testDependsOn: "testDependencyOf",
|
|
21
|
+
testDependencyOf: "testDependsOn",
|
|
22
|
+
runtimeCalls: "runtimeCalledBy",
|
|
23
|
+
runtimeCalledBy: "runtimeCalls",
|
|
24
|
+
usesResource: "resourceUsedBy",
|
|
25
|
+
resourceUsedBy: "usesResource",
|
|
26
|
+
schedules: "scheduledBy",
|
|
27
|
+
scheduledBy: "schedules",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const KIND_PAIRS = Object.freeze({
|
|
31
|
+
partOf: { Component: ["System", "Domain"], System: ["Domain"], Resource: ["System"] },
|
|
32
|
+
hasPart: { System: ["Component", "Resource"], Domain: ["Component", "System"] },
|
|
33
|
+
providesApi: { Component: ["API"] },
|
|
34
|
+
apiProvidedBy: { API: ["Component"] },
|
|
35
|
+
consumesApi: { Component: ["API"] },
|
|
36
|
+
apiConsumedBy: { API: ["Component"] },
|
|
37
|
+
buildDependsOn: { Component: ["Component"] },
|
|
38
|
+
buildDependencyOf: { Component: ["Component"] },
|
|
39
|
+
testDependsOn: { Component: ["Component"] },
|
|
40
|
+
testDependencyOf: { Component: ["Component"] },
|
|
41
|
+
runtimeCalls: { Component: ["Component"] },
|
|
42
|
+
runtimeCalledBy: { Component: ["Component"] },
|
|
43
|
+
usesResource: { Component: ["Resource"] },
|
|
44
|
+
resourceUsedBy: { Resource: ["Component"] },
|
|
45
|
+
schedules: { Component: ["Component"] },
|
|
46
|
+
scheduledBy: { Component: ["Component"] },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// A closed whitelist of attribute keys per relation type. An unknown key is a
|
|
50
|
+
// compiler error, so the graph cannot accumulate unvalidated payload — and a
|
|
51
|
+
// new relation type cannot silently start carrying arbitrary data.
|
|
52
|
+
export const ATTRIBUTE_KEYS = Object.freeze({
|
|
53
|
+
partOf: [], hasPart: [],
|
|
54
|
+
providesApi: [], apiProvidedBy: [],
|
|
55
|
+
consumesApi: ["via"], apiConsumedBy: ["via"],
|
|
56
|
+
buildDependsOn: ["via", "package"], buildDependencyOf: ["via", "package"],
|
|
57
|
+
testDependsOn: ["via", "package"], testDependencyOf: ["via", "package"],
|
|
58
|
+
runtimeCalls: ["transport", "contractRef", "deliverySemantics"],
|
|
59
|
+
runtimeCalledBy: ["transport", "contractRef", "deliverySemantics"],
|
|
60
|
+
usesResource: ["via", "client"], resourceUsedBy: ["via", "client"],
|
|
61
|
+
schedules: ["via", "route"], scheduledBy: ["via", "route"],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Attributes that record *how* an edge was found, not what the edge is.
|
|
65
|
+
// `via` is the derivation route ("npm-manifest", "source-import",
|
|
66
|
+
// "reviewed-override"). It must not take part in a relation's identity: if it
|
|
67
|
+
// did, the single fact "accounts consumes the OCR admin API" would appear once
|
|
68
|
+
// per adapter that noticed it, and a repository's edge count would measure
|
|
69
|
+
// adapter overlap rather than architecture. `package` and `route` are the
|
|
70
|
+
// opposite case — they name the specific instance, so two of them are two real
|
|
71
|
+
// edges and are kept apart deliberately.
|
|
72
|
+
const DERIVATION_ATTRS = new Set(["via"]);
|
|
73
|
+
|
|
74
|
+
// The identity of a relation: what it asserts, ignoring how it was found.
|
|
75
|
+
export function relationIdentity(e) {
|
|
76
|
+
const attrs = {};
|
|
77
|
+
for (const k of Object.keys(e.attributes || {}).sort()) {
|
|
78
|
+
if (!DERIVATION_ATTRS.has(k)) attrs[k] = e.attributes[k];
|
|
79
|
+
}
|
|
80
|
+
return e.type + "|" + e.sourceRef + "|" + e.targetRef + "|" + canonicalAttrs(attrs);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function canonicalAttrs(attrs) {
|
|
84
|
+
return Object.keys(attrs || {}).sort().map((k) => k + "=" + JSON.stringify(attrs[k])).join("|");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildRelations(ctx) {
|
|
88
|
+
const { entities, byRef } = ctx.sources;
|
|
89
|
+
const { refs } = ctx;
|
|
90
|
+
const errors = [];
|
|
91
|
+
const forward = [];
|
|
92
|
+
|
|
93
|
+
const add = (type, sourceRef, targetRef, attributes, provRecs, evidenceClass) => {
|
|
94
|
+
forward.push({
|
|
95
|
+
type,
|
|
96
|
+
sourceRef,
|
|
97
|
+
targetRef,
|
|
98
|
+
attributes: attributes || {},
|
|
99
|
+
provs: new Set(provRecs.filter(Boolean)),
|
|
100
|
+
evidenceClass: evidenceClass || evidenceOf(provRecs),
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// placement
|
|
105
|
+
for (const e of entities) {
|
|
106
|
+
const spec = e.doc.spec || {};
|
|
107
|
+
const p = () => ctx.prov.add("declared", e.file, "descriptor-reader", ["rel:pending"]);
|
|
108
|
+
if (e.kind === "Component") {
|
|
109
|
+
if (spec.system) add("partOf", e.ref, refs.refFor("System", spec.system), {}, [p()], "AUTHORED");
|
|
110
|
+
else if (spec.domain) add("partOf", e.ref, refs.refFor("Domain", spec.domain), {}, [p()], "AUTHORED");
|
|
111
|
+
} else if (e.kind === "System" && spec.domain) {
|
|
112
|
+
add("partOf", e.ref, refs.refFor("Domain", spec.domain), {}, [p()], "AUTHORED");
|
|
113
|
+
} else if (e.kind === "Resource" && spec.system) {
|
|
114
|
+
add("partOf", e.ref, refs.refFor("System", spec.system), {}, [p()], "AUTHORED");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// provider edges
|
|
119
|
+
for (const e of entities.filter((x) => x.kind === "API")) {
|
|
120
|
+
add("providesApi", e.doc.spec.provider, e.ref, {}, [ctx.prov.add("declared", e.file, "api-contract-extractor", ["rel:pending"])], "AUTHORED");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// extractor edges
|
|
124
|
+
for (const e of ctx.edges) add(e.type, e.sourceRef, e.targetRef, e.attributes, [...e.provs], e.evidenceClass);
|
|
125
|
+
|
|
126
|
+
// reviewed overrides
|
|
127
|
+
for (const e of ctx.overrideEdges) add(e.type, e.sourceRef, e.targetRef, e.attributes, [...e.provs], "REVIEWED_OVERRIDE");
|
|
128
|
+
|
|
129
|
+
// An API contract owns the boundary: a runtime call that duplicates the
|
|
130
|
+
// canonical contract is a modelling error, not a second fact.
|
|
131
|
+
const apiProviderOf = new Map();
|
|
132
|
+
const contractOf = new Map();
|
|
133
|
+
for (const e of entities.filter((x) => x.kind === "API")) {
|
|
134
|
+
apiProviderOf.set(e.ref, e.doc.spec.provider);
|
|
135
|
+
if (e.doc.spec.contract?.ref) contractOf.set(e.ref, e.doc.spec.contract.ref);
|
|
136
|
+
}
|
|
137
|
+
const consumed = new Map();
|
|
138
|
+
for (const e of forward) {
|
|
139
|
+
if (e.type !== "consumesApi") continue;
|
|
140
|
+
if (!consumed.has(e.sourceRef)) consumed.set(e.sourceRef, new Set());
|
|
141
|
+
consumed.get(e.sourceRef).add(e.targetRef);
|
|
142
|
+
}
|
|
143
|
+
const filtered = forward.filter((e) => {
|
|
144
|
+
if (e.type !== "runtimeCalls") return true;
|
|
145
|
+
for (const apiRef of consumed.get(e.sourceRef) || []) {
|
|
146
|
+
if (apiProviderOf.get(apiRef) !== e.targetRef) continue;
|
|
147
|
+
if (contractOf.get(apiRef) && contractOf.get(apiRef) === e.attributes?.contractRef) {
|
|
148
|
+
errors.push(new AgentDocError(
|
|
149
|
+
CODES.RELATION_CONTRACT_DUP,
|
|
150
|
+
"runtime call " + e.sourceRef + " -> " + e.targetRef + " duplicates canonical API contract " + contractOf.get(apiRef),
|
|
151
|
+
{ ref: e.sourceRef }
|
|
152
|
+
));
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const all = [];
|
|
160
|
+
for (const e of filtered) {
|
|
161
|
+
all.push(e);
|
|
162
|
+
const inv = INVERSE[e.type];
|
|
163
|
+
if (!inv) {
|
|
164
|
+
errors.push(new AgentDocError(CODES.RELATION_KIND, "unknown relation type '" + e.type + "'", { ref: e.sourceRef }));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
all.push({ type: inv, sourceRef: e.targetRef, targetRef: e.sourceRef, attributes: e.attributes, provs: e.provs, evidenceClass: e.evidenceClass });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const kindOf = (ref) => refs.kindOfRef(ref);
|
|
171
|
+
for (const e of all) {
|
|
172
|
+
const sk = kindOf(e.sourceRef);
|
|
173
|
+
const tk = kindOf(e.targetRef);
|
|
174
|
+
if (!sk || !tk || !byRef.has(e.sourceRef) || !byRef.has(e.targetRef)) {
|
|
175
|
+
errors.push(new AgentDocError(
|
|
176
|
+
CODES.RELATION_UNRESOLVED,
|
|
177
|
+
"relation endpoint does not resolve to a real entity: " + e.sourceRef + " -> " + e.targetRef
|
|
178
|
+
));
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const allowed = KIND_PAIRS[e.type]?.[sk] || [];
|
|
182
|
+
if (!allowed.includes(tk)) {
|
|
183
|
+
errors.push(new AgentDocError(CODES.RELATION_KIND, "invalid kind pair for " + e.type + ": " + sk + " -> " + tk, { ref: e.sourceRef }));
|
|
184
|
+
}
|
|
185
|
+
const allowedAttrs = ATTRIBUTE_KEYS[e.type];
|
|
186
|
+
if (!allowedAttrs) {
|
|
187
|
+
errors.push(new AgentDocError(CODES.RELATION_KIND, "unknown relation type '" + e.type + "'", { ref: e.sourceRef }));
|
|
188
|
+
} else {
|
|
189
|
+
const attrKeys = Object.keys(e.attributes || {});
|
|
190
|
+
for (const k of attrKeys) {
|
|
191
|
+
if (!allowedAttrs.includes(k)) {
|
|
192
|
+
errors.push(new AgentDocError(
|
|
193
|
+
CODES.RELATION_KIND,
|
|
194
|
+
"relation " + e.type + " does not allow attribute '" + k + "' (allowed: " + allowedAttrs.join(", ") + ")",
|
|
195
|
+
{ ref: e.sourceRef }
|
|
196
|
+
));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (e.type === "runtimeCalls" || e.type === "runtimeCalledBy") {
|
|
200
|
+
const a = e.attributes || {};
|
|
201
|
+
if (!["http", "service-binding", "signed-http", "grpc", "queue", "in-process"].includes(a.transport)) {
|
|
202
|
+
errors.push(new AgentDocError(CODES.RELATION_KIND, e.type + " requires a known transport", { ref: e.sourceRef }));
|
|
203
|
+
}
|
|
204
|
+
if (!a.contractRef) {
|
|
205
|
+
errors.push(new AgentDocError(CODES.RELATION_KIND, e.type + " requires contractRef", { ref: e.sourceRef }));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const seen = new Map();
|
|
212
|
+
const out = [];
|
|
213
|
+
const vias = new Map();
|
|
214
|
+
for (const e of all) {
|
|
215
|
+
const key = relationIdentity(e);
|
|
216
|
+
if (seen.has(key)) {
|
|
217
|
+
const prev = seen.get(key);
|
|
218
|
+
for (const p of e.provs) prev.provs.add(p);
|
|
219
|
+
// A corroborated edge keeps the label its evidence supports, recomputed
|
|
220
|
+
// from the merged provenance rather than promoted by a fixed order.
|
|
221
|
+
prev.evidenceClass = evidenceOf([...prev.provs]);
|
|
222
|
+
// Every derivation route that found it, so merging never discards the
|
|
223
|
+
// fact that a second, independent adapter agreed.
|
|
224
|
+
const v = vias.get(key);
|
|
225
|
+
if (v && e.attributes && e.attributes.via) v.add(e.attributes.via);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const canon = {};
|
|
229
|
+
for (const k of Object.keys(e.attributes || {}).sort()) canon[k] = e.attributes[k];
|
|
230
|
+
e.attributes = canon;
|
|
231
|
+
if (canon.via !== undefined) {
|
|
232
|
+
vias.set(key, new Set([canon.via]));
|
|
233
|
+
delete e.attributes.via;
|
|
234
|
+
}
|
|
235
|
+
seen.set(key, e);
|
|
236
|
+
out.push(e);
|
|
237
|
+
}
|
|
238
|
+
// Re-attach the sorted derivation routes.
|
|
239
|
+
for (const e of out) {
|
|
240
|
+
const key = relationIdentity(e);
|
|
241
|
+
const v = vias.get(key);
|
|
242
|
+
if (v) {
|
|
243
|
+
const canon = {};
|
|
244
|
+
canon.via = [...v].sort();
|
|
245
|
+
for (const k of Object.keys(e.attributes).sort()) canon[k] = e.attributes[k];
|
|
246
|
+
e.attributes = canon;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
out.sort((a, b) => {
|
|
250
|
+
const ka = relationIdentity(a);
|
|
251
|
+
const kb = relationIdentity(b);
|
|
252
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const keys = new Set(out.map(relationIdentity));
|
|
256
|
+
for (const e of out) {
|
|
257
|
+
const invKey = relationIdentity({ type: INVERSE[e.type], sourceRef: e.targetRef, targetRef: e.sourceRef, attributes: e.attributes });
|
|
258
|
+
if (!keys.has(invKey)) {
|
|
259
|
+
errors.push(new AgentDocError(CODES.RELATION_MISSING_INVERSE, "missing inverse for " + e.type + " " + e.sourceRef + " -> " + e.targetRef));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { relations: out, errors };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// The label on a relation records which classes back it.
|
|
266
|
+
//
|
|
267
|
+
// It does NOT pick a winner by strength. A precedence list here is a global
|
|
268
|
+
// precedence order hiding in a helper — the exact thing the authority model
|
|
269
|
+
// forbids — and it was actively wrong: a reviewed override corroborated by a
|
|
270
|
+
// source reference was relabelled AUTHORED, so the same fact read as a weaker,
|
|
271
|
+
// purely-authored claim on the relation while its assertion read
|
|
272
|
+
// REVIEWED_OVERRIDE. The graph contradicted itself about its own highest-
|
|
273
|
+
// authority input.
|
|
274
|
+
//
|
|
275
|
+
// So: when the evidence agrees there is one class and it is reported. When it
|
|
276
|
+
// disagrees, this elects nothing. REVIEWED_OVERRIDE is reported when a human
|
|
277
|
+
// interpretation is among the classes, because that is not an election between
|
|
278
|
+
// competing claims — it records that someone reviewed this specific edge — and
|
|
279
|
+
// suppressing it would hide the review. The full class set is always recoverable
|
|
280
|
+
// from the provenance records.
|
|
281
|
+
function evidenceOf(provRecs) {
|
|
282
|
+
const classes = new Set((provRecs || []).map((p) => p && p.cls).filter(Boolean).map((c) => PROV_TO_EVIDENCE[c]).filter(Boolean));
|
|
283
|
+
if (classes.size <= 1) return classes.size === 1 ? [...classes][0] : "DERIVED";
|
|
284
|
+
if (classes.has("REVIEWED_OVERRIDE")) return "REVIEWED_OVERRIDE";
|
|
285
|
+
return "DERIVED";
|
|
286
|
+
}
|