@typedstandards/civic-typed-harness 0.6.0 → 0.7.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/CHANGELOG.md +101 -0
- package/README.md +32 -4
- package/dist/capture/data-sources.d.ts +24 -21
- package/dist/capture/data-sources.js +19 -22
- package/dist/capture/provenance.d.ts +80 -35
- package/dist/capture/provenance.js +76 -68
- package/dist/capture/run-attributes.d.ts +35 -0
- package/dist/capture/run-attributes.js +105 -0
- package/dist/capture/trace.d.ts +2 -4
- package/dist/capture/trace.js +3 -9
- package/dist/civic/data-sources.d.ts +58 -0
- package/dist/civic/data-sources.js +63 -0
- package/dist/civic/provenance.d.ts +63 -0
- package/dist/civic/provenance.js +86 -0
- package/dist/civic/run-servers.d.ts +11 -0
- package/dist/civic/run-servers.js +35 -0
- package/dist/civic/trace.d.ts +4 -0
- package/dist/civic/trace.js +13 -0
- package/dist/format/dathere.d.ts +99 -8
- package/dist/format/dathere.js +158 -13
- package/dist/format/sources.d.ts +12 -2
- package/dist/format/sources.js +12 -2
- package/dist/format/vocabulary.d.ts +21 -0
- package/dist/format/vocabulary.js +26 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +19 -4
- package/package.json +3 -3
- /package/dist/{capture → civic}/data-sources.assert.d.ts +0 -0
- /package/dist/{capture → civic}/data-sources.assert.js +0 -0
|
@@ -1,33 +1,23 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Provenance builder (CAPTURE group) — walks an OTel trace and maps the
|
|
2
2
|
// analysis pipeline (LLM inference, MCP tool calls, data responses) to a
|
|
3
3
|
// W3C PROV-O JSON-LD graph. Relocated whole from civic-ai-tools-website
|
|
4
4
|
// `src/lib/evidence/provenance.ts:65–392` per the S2 brief §1, rebuilt on
|
|
5
5
|
// @typedstandards/produce-core's generic ProvGraph/ProvNode types and
|
|
6
|
-
// node/edge helpers.
|
|
7
|
-
// imported from the format-extension group — this module WALKS, it does not
|
|
8
|
-
// define vocabulary (the package's internal module boundary).
|
|
6
|
+
// node/edge helpers.
|
|
9
7
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// the
|
|
17
|
-
//
|
|
8
|
+
// THE SEAM (civic-ai-tools#244 P2). This module imports nothing civic: the
|
|
9
|
+
// vocabulary (id scheme and `@context`), the term names it keys properties
|
|
10
|
+
// with, the source registry, the fallback and skill source ids and the
|
|
11
|
+
// dataset-keyed fact all arrive in `ProvenanceBuildConfig`, every one of them
|
|
12
|
+
// required. It WALKS; it neither defines vocabulary nor applies a default.
|
|
13
|
+
// The civic values are applied one layer up, in `src/civic/provenance.ts`,
|
|
14
|
+
// which exports the package's `buildProvenanceGraph`, `ProvenanceConfig` and
|
|
15
|
+
// `CIVICAITOOLS_PROVENANCE_CONFIG` under their existing names and
|
|
16
|
+
// signatures. Given the civic values, this builder emits what it emitted
|
|
17
|
+
// before the seam, byte for byte (property insertion order is preserved
|
|
18
|
+
// throughout — the legacy hash chain's byte contract).
|
|
18
19
|
import { makeProvGraph, makeEntityNode, makeActivityNode, makeAgentNode, provRef, provUsed, provWasGeneratedBy, provWasDerivedFrom, provWasAssociatedWith, xsdDateTime, } from '@typedstandards/produce-core';
|
|
19
20
|
import { hash } from "./trace.js";
|
|
20
|
-
import { CIVIC_VOCABULARY, CIVICAITOOLS_PLATFORM_AGENT, CIVIC_TERM_COMPLETION_TOKENS, CIVIC_TERM_CONTENT_HASH, CIVIC_TERM_CROISSANT_METADATA_URL, CIVIC_TERM_DATASET_ID, CIVIC_TERM_DATASET_URL, CIVIC_TERM_DURATION_MS, CIVIC_TERM_FAILED, CIVIC_TERM_FAILURE_KIND, CIVIC_TERM_OPERATION_TYPE, CIVIC_TERM_PORTAL_DOMAIN, CIVIC_TERM_PROMPT_TOKENS, CIVIC_TERM_RESPONSE_ROWS, CIVIC_TERM_SERVER_URL, CIVIC_TERM_SOURCE_ID, CIVIC_TERM_TOOL_NAME, CIVIC_TERM_URL, } from "../format/vocabulary.js";
|
|
21
|
-
import { CIVIC_SOURCE_REGISTRY, FALLBACK_SOURCE_ID, isDatasetKeyedSource, } from "../format/sources.js";
|
|
22
|
-
/** The civicaitools.org reference deployment's values. Passed explicitly by
|
|
23
|
-
* the reference app — never applied as a default, and never spread into
|
|
24
|
-
* another instance's config, which would assert infrastructure that
|
|
25
|
-
* instance doesn't run. */
|
|
26
|
-
export const CIVICAITOOLS_PROVENANCE_CONFIG = {
|
|
27
|
-
platformAgent: CIVICAITOOLS_PLATFORM_AGENT,
|
|
28
|
-
sourceRegistry: CIVIC_SOURCE_REGISTRY,
|
|
29
|
-
modelAgentDescription: 'Large language model via OpenRouter',
|
|
30
|
-
};
|
|
31
21
|
// --- Helpers ---
|
|
32
22
|
function getAttr(attrs, key) {
|
|
33
23
|
const attr = attrs.find(a => a.key === key);
|
|
@@ -71,24 +61,22 @@ function nanoToIso(nano) {
|
|
|
71
61
|
* - Prompt, skill guidance, data responses, output → prov:Entity
|
|
72
62
|
* - LLM model, MCP server, platform → prov:Agent
|
|
73
63
|
*/
|
|
74
|
-
export function
|
|
64
|
+
export function buildProvenanceGraphWith(trace, input, config) {
|
|
75
65
|
const otel = trace;
|
|
76
66
|
const spans = otel?.resourceSpans?.[0]?.scopeSpans?.[0]?.spans || [];
|
|
77
67
|
// `input.portal` is deliberately not read — see `ProvenanceInput.portal`.
|
|
78
68
|
const { packageId, promptHash, promptText, outputText, model } = input;
|
|
79
69
|
const outputHash = input.outputHash ?? hash(outputText ?? '');
|
|
80
70
|
const registry = config.sourceRegistry;
|
|
81
|
-
const fallbackSourceId = config
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
// never mixes eras.
|
|
86
|
-
const vocab = config.vocabulary ?? CIVIC_VOCABULARY;
|
|
71
|
+
const { fallbackSourceId, skillSourceId, terms } = config;
|
|
72
|
+
// Every id and the `@context` below route through the one vocabulary, so a
|
|
73
|
+
// graph never mixes eras.
|
|
74
|
+
const vocab = config.vocabulary;
|
|
87
75
|
const graph = [];
|
|
88
76
|
// --- Entities ---
|
|
89
77
|
// User prompt
|
|
90
78
|
graph.push(makeEntityNode(vocab.urn(packageId, 'prompt', promptHash), {
|
|
91
|
-
[
|
|
79
|
+
[terms.contentHash]: `sha256:${promptHash}`,
|
|
92
80
|
'dcterms:description': 'User query prompt',
|
|
93
81
|
...(promptText ? { 'prov:value': promptText } : {}),
|
|
94
82
|
}));
|
|
@@ -101,13 +89,13 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
101
89
|
const skillServerUrl = skillSpan ? getAttr(skillSpan.attributes, 'skill.mcp_server_url') : undefined;
|
|
102
90
|
if (skillHash) {
|
|
103
91
|
graph.push(makeEntityNode(vocab.urn(packageId, 'skill', skillHash), {
|
|
104
|
-
[
|
|
92
|
+
[terms.contentHash]: `sha256:${skillHash}`,
|
|
105
93
|
'dcterms:description': 'Composed MCP skill guidance (system prompt)',
|
|
106
94
|
}, ['prov:Plan']));
|
|
107
95
|
}
|
|
108
96
|
// Final output
|
|
109
97
|
graph.push(makeEntityNode(vocab.urn(packageId, 'output', outputHash), {
|
|
110
|
-
[
|
|
98
|
+
[terms.contentHash]: `sha256:${outputHash}`,
|
|
111
99
|
'dcterms:description': 'AI-generated analysis output',
|
|
112
100
|
}));
|
|
113
101
|
// --- Agents ---
|
|
@@ -122,11 +110,13 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
122
110
|
: {}),
|
|
123
111
|
}, ['prov:SoftwareAgent']));
|
|
124
112
|
// MCP server agents — one per distinct data source that appears in the
|
|
125
|
-
// trace.
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// the
|
|
113
|
+
// trace. A tool span's source is its `mcp.source` tag; a span with no tag
|
|
114
|
+
// (records captured before source tagging) resolves by its tool name
|
|
115
|
+
// through the caller's resolver; a span neither identifies takes the
|
|
116
|
+
// caller's `fallbackSourceId` (the civic layer's default is `unknown`, the
|
|
117
|
+
// value the reference app writes for a tool it cannot resolve — ADR-0033
|
|
118
|
+
// §A). Agent coordinates come from the source registry; the skill-fetch
|
|
119
|
+
// span URL overrides the skill source's server URL.
|
|
130
120
|
//
|
|
131
121
|
// THE ADDRESS IS THE OPERATOR'S, OR IT IS ABSENT (civic-ai-tools#205, 0.5.0).
|
|
132
122
|
// `serverUrl` is optional on a registry entry and there is no stand-in for a
|
|
@@ -143,10 +133,18 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
143
133
|
serverUrl: id === skillSourceId ? skillServerUrl || info.serverUrl : info.serverUrl,
|
|
144
134
|
};
|
|
145
135
|
}
|
|
136
|
+
// The ONE resolution of a span's source, used for its agent node, its
|
|
137
|
+
// attribution and its `civic:sourceId` alike.
|
|
138
|
+
function sourceOfSpan(attributes) {
|
|
139
|
+
const tagged = getAttr(attributes, 'mcp.source');
|
|
140
|
+
if (tagged)
|
|
141
|
+
return tagged;
|
|
142
|
+
const toolName = getAttr(attributes, 'tool.name');
|
|
143
|
+
return (toolName ? config.resolver(toolName) : undefined) || fallbackSourceId;
|
|
144
|
+
}
|
|
146
145
|
const sourcesInTrace = new Set();
|
|
147
146
|
for (const span of toolSpans) {
|
|
148
|
-
|
|
149
|
-
sourcesInTrace.add(source);
|
|
147
|
+
sourcesInTrace.add(sourceOfSpan(span.attributes));
|
|
150
148
|
}
|
|
151
149
|
for (const sourceId of sourcesInTrace) {
|
|
152
150
|
const meta = sourceAgentMap[sourceId] ?? {
|
|
@@ -157,23 +155,22 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
157
155
|
'dcterms:title': meta.title,
|
|
158
156
|
// Conditional spread in place, so a known address keeps its
|
|
159
157
|
// position in the key order the legacy chain hashes.
|
|
160
|
-
...(meta.serverUrl ? { [
|
|
161
|
-
[
|
|
158
|
+
...(meta.serverUrl ? { [terms.serverUrl]: meta.serverUrl } : {}),
|
|
159
|
+
[terms.sourceId]: sourceId,
|
|
162
160
|
}, ['prov:SoftwareAgent']));
|
|
163
161
|
}
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
162
|
+
// A call is attributed to ITS OWN source's agent: the registry's agent
|
|
163
|
+
// when the registry carries the source, otherwise the agent node emitted
|
|
164
|
+
// above for it (ADR-0033 §A). Until 0.7.0 an id the registry lacked was
|
|
165
|
+
// attributed to the fallback source's agent instead — which, in a run where
|
|
166
|
+
// that source made no call, named an agent the graph did not contain.
|
|
167
167
|
function agentUrnForSource(sourceId) {
|
|
168
|
-
|
|
169
|
-
return (sourceAgentMap[id]?.urn
|
|
170
|
-
?? sourceAgentMap[fallbackSourceId]?.urn
|
|
171
|
-
?? vocab.sourceAgentUrn(encodeURIComponent(id)));
|
|
168
|
+
return sourceAgentMap[sourceId]?.urn ?? vocab.sourceAgentUrn(encodeURIComponent(sourceId));
|
|
172
169
|
}
|
|
173
170
|
// Platform
|
|
174
171
|
graph.push(makeAgentNode(vocab.platformUrn(config.platformAgent.id), {
|
|
175
172
|
'dcterms:title': config.platformAgent.title,
|
|
176
|
-
[
|
|
173
|
+
[terms.url]: config.platformAgent.url,
|
|
177
174
|
}, ['prov:SoftwareAgent']));
|
|
178
175
|
// --- Activities ---
|
|
179
176
|
// LLM inference spans
|
|
@@ -198,8 +195,8 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
198
195
|
...(span.endTimeUnixNano
|
|
199
196
|
? { 'prov:endedAtTime': xsdDateTime(nanoToIso(span.endTimeUnixNano)) }
|
|
200
197
|
: {}),
|
|
201
|
-
...(promptTokens ? { [
|
|
202
|
-
...(completionTokens ? { [
|
|
198
|
+
...(promptTokens ? { [terms.promptTokens]: Number(promptTokens) } : {}),
|
|
199
|
+
...(completionTokens ? { [terms.completionTokens]: Number(completionTokens) } : {}),
|
|
203
200
|
}));
|
|
204
201
|
}
|
|
205
202
|
// MCP tool call spans.
|
|
@@ -230,9 +227,15 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
230
227
|
const datasetId = getAttr(span.attributes, 'tool.dataset_id');
|
|
231
228
|
// Absent when the span carried none — never the run's portal.
|
|
232
229
|
const portalDomain = getAttr(span.attributes, 'tool.portal_domain');
|
|
233
|
-
const toolSource =
|
|
230
|
+
const toolSource = sourceOfSpan(span.attributes);
|
|
234
231
|
const toolAgentUrn = agentUrnForSource(toolSource);
|
|
235
|
-
|
|
232
|
+
// The call's other identities (ADR-0033 §B): the name sent on the wire,
|
|
233
|
+
// the alias of the source and the server's id. Absent when the span
|
|
234
|
+
// carried none — never derived from the exposed name.
|
|
235
|
+
const upstreamToolName = getAttr(span.attributes, 'tool.upstream_name');
|
|
236
|
+
const sourceAlias = getAttr(span.attributes, 'mcp.alias');
|
|
237
|
+
const serverId = getAttr(span.attributes, 'mcp.server.id');
|
|
238
|
+
const toolSourceDatasetKeyed = config.isDatasetKeyed(toolSource);
|
|
236
239
|
// Query entity (tool arguments). Find the most recent inference span
|
|
237
240
|
// before this tool call to link as generator.
|
|
238
241
|
const toolStart = Number(span.startTimeUnixNano);
|
|
@@ -241,10 +244,10 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
241
244
|
.sort((a, b) => Number(b.endTimeUnixNano || '0') - Number(a.endTimeUnixNano || '0'))[0];
|
|
242
245
|
const queryUrn = vocab.urn(packageId, 'query', queryHash);
|
|
243
246
|
graph.push(makeEntityNode(queryUrn, {
|
|
244
|
-
[
|
|
247
|
+
[terms.contentHash]: `sha256:${queryHash}`,
|
|
245
248
|
// Omitted — not placeholdered — when the span named no tool.
|
|
246
|
-
...(toolName ? { [
|
|
247
|
-
[
|
|
249
|
+
...(toolName ? { [terms.toolName]: toolName } : {}),
|
|
250
|
+
[terms.operationType]: opType,
|
|
248
251
|
'dcterms:description': `MCP tool arguments (${opType})`,
|
|
249
252
|
...(precedingInference
|
|
250
253
|
? provWasGeneratedBy(vocab.urn(packageId, 'inference', precedingInference.spanId))
|
|
@@ -272,9 +275,9 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
272
275
|
: `Data response from ${sourceAgentMap[toolSource]?.title || toolSource}`;
|
|
273
276
|
const responseRows = getAttr(span.attributes, 'tool.response_rows');
|
|
274
277
|
graph.push(makeEntityNode(dataUrn, {
|
|
275
|
-
[
|
|
278
|
+
[terms.contentHash]: `sha256:${responseHash}`,
|
|
276
279
|
'dcterms:description': description,
|
|
277
|
-
[
|
|
280
|
+
[terms.sourceId]: toolSource,
|
|
278
281
|
...provWasGeneratedBy(toolCallUrn),
|
|
279
282
|
// Croissant 1.1 placeholder — only meaningful for dataset-keyed
|
|
280
283
|
// sources today. The dataset id is stated whenever the span carried
|
|
@@ -282,17 +285,17 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
282
285
|
// the portal as well. Key order is the byte contract.
|
|
283
286
|
...(toolSourceDatasetKeyed && datasetId
|
|
284
287
|
? {
|
|
285
|
-
[
|
|
288
|
+
[terms.datasetId]: datasetId,
|
|
286
289
|
...(portalDomain
|
|
287
290
|
? {
|
|
288
|
-
[
|
|
289
|
-
[
|
|
291
|
+
[terms.portalDomain]: portalDomain,
|
|
292
|
+
[terms.datasetUrl]: `https://${portalDomain}/d/${datasetId}`,
|
|
290
293
|
}
|
|
291
294
|
: {}),
|
|
292
|
-
[
|
|
295
|
+
[terms.croissantMetadataUrl]: null, // hook for future Croissant integration
|
|
293
296
|
}
|
|
294
297
|
: {}),
|
|
295
|
-
...(responseRows ? { [
|
|
298
|
+
...(responseRows ? { [terms.responseRows]: Number(responseRows) } : {}),
|
|
296
299
|
}));
|
|
297
300
|
}
|
|
298
301
|
// Tool call activity — associated with the MCP source agent that handled
|
|
@@ -325,7 +328,7 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
325
328
|
'dcterms:description': toolName
|
|
326
329
|
? `MCP tool call: ${toolName} (${opType})`
|
|
327
330
|
: `MCP tool call (${opType})`,
|
|
328
|
-
[
|
|
331
|
+
[terms.sourceId]: toolSource,
|
|
329
332
|
...provUsed([queryUrn]),
|
|
330
333
|
...provWasAssociatedWith(toolAgentUrn),
|
|
331
334
|
...(span.startTimeUnixNano
|
|
@@ -334,7 +337,7 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
334
337
|
...(span.endTimeUnixNano
|
|
335
338
|
? { 'prov:endedAtTime': xsdDateTime(nanoToIso(span.endTimeUnixNano)) }
|
|
336
339
|
: {}),
|
|
337
|
-
...(durationMs ? { [
|
|
340
|
+
...(durationMs ? { [terms.durationMs]: Number(durationMs) } : {}),
|
|
338
341
|
// Appended LAST and spread conditionally, exactly as `civic:durationMs`
|
|
339
342
|
// above is: a span that recorded no rejection yields the key list it
|
|
340
343
|
// yielded at 0.3.1, in the same order, and property insertion order is
|
|
@@ -353,8 +356,13 @@ export function buildProvenanceGraph(trace, input, config) {
|
|
|
353
356
|
// since 0.3.1, because that conditional is unchanged and this one is
|
|
354
357
|
// still appended after it. `capture/provenance.test.ts` drives that
|
|
355
358
|
// combination and pins the whole key list for it.
|
|
356
|
-
...(failed === true ? { [
|
|
357
|
-
...(failureKind ? { [
|
|
359
|
+
...(failed === true ? { [terms.failed]: true } : {}),
|
|
360
|
+
...(failureKind ? { [terms.failureKind]: failureKind } : {}),
|
|
361
|
+
// Appended after everything above, each only when the span carried
|
|
362
|
+
// it, so a span without them yields the 0.6.0 key list (ADR-0033 §B).
|
|
363
|
+
...(upstreamToolName ? { [terms.upstreamToolName]: upstreamToolName } : {}),
|
|
364
|
+
...(sourceAlias ? { [terms.sourceAlias]: sourceAlias } : {}),
|
|
365
|
+
...(serverId ? { [terms.serverId]: serverId } : {}),
|
|
358
366
|
}));
|
|
359
367
|
}
|
|
360
368
|
// --- Final output relationships ---
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** One server the run had, as the root span recorded it. */
|
|
2
|
+
export interface RunMcpServer {
|
|
3
|
+
url: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
}
|
|
6
|
+
/** What the root span carried, under the derivation's input names. A field
|
|
7
|
+
* is absent when its attribute is. */
|
|
8
|
+
export interface ExtractedRunAttributes {
|
|
9
|
+
assertedDate?: string;
|
|
10
|
+
runMcpServers?: RunMcpServer[];
|
|
11
|
+
toolDefinitions?: Array<Record<string, unknown>>;
|
|
12
|
+
sourcesOffered?: Array<{
|
|
13
|
+
sourceId: string;
|
|
14
|
+
alias: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}>;
|
|
17
|
+
components?: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
guidanceHash?: string;
|
|
21
|
+
guidanceOrigin?: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/** A root-span attribute that is present but not the shape ADR-0033 names. */
|
|
25
|
+
export declare class RunAttributeError extends Error {
|
|
26
|
+
/** The span attribute that failed to parse. */
|
|
27
|
+
readonly attribute: string;
|
|
28
|
+
constructor(attribute: string, message: string);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read the run attributes from a trace's root span. Returns `{}` for a trace
|
|
32
|
+
* with no root span — including a BlobRef trace's inspection view
|
|
33
|
+
* (`traceForInspection`), whose caller passes the fields as inputs instead.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractRunAttributes(trace: Record<string, unknown>): ExtractedRunAttributes;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Run-attribute extraction (CAPTURE group) — reads the facts the producer
|
|
2
|
+
// recorded on the trace's ROOT span at run time (ADR-0033 §D, §E): the date
|
|
3
|
+
// the prompt asserted, the servers the run had, the tools it offered, the
|
|
4
|
+
// sources it was offered and the components that composed its prompt.
|
|
5
|
+
//
|
|
6
|
+
// WHY THE TRACE (civic-ai-tools-website#495). A record is packaged after the
|
|
7
|
+
// run, sometimes much later; a fact read at packaging is packaging-time state.
|
|
8
|
+
// The trace is the one artifact that travels from the run to the package, so
|
|
9
|
+
// the producer writes each run-time fact there and this module reads it back.
|
|
10
|
+
//
|
|
11
|
+
// SHAPE, NOT POLICY. This module checks that each attribute parses as the
|
|
12
|
+
// shape ADR-0033 names (JSON text of an array of objects whose named fields
|
|
13
|
+
// are strings) and returns it under the derivation's input names. The values
|
|
14
|
+
// — the date form, the reason set, the hash form, the origin set — are policy,
|
|
15
|
+
// checked by the format group's derivation, which refuses a bad one by name.
|
|
16
|
+
//
|
|
17
|
+
// An ABSENT attribute is an absent field. A PRESENT attribute that does not
|
|
18
|
+
// parse is refused with `RunAttributeError`, never read as absent: absence
|
|
19
|
+
// here would hand the caller back to its packaging-time state, which is the
|
|
20
|
+
// defect this module exists to remove.
|
|
21
|
+
/** A root-span attribute that is present but not the shape ADR-0033 names. */
|
|
22
|
+
export class RunAttributeError extends Error {
|
|
23
|
+
/** The span attribute that failed to parse. */
|
|
24
|
+
attribute;
|
|
25
|
+
constructor(attribute, message) {
|
|
26
|
+
super(`${attribute}: ${message}`);
|
|
27
|
+
this.name = 'RunAttributeError';
|
|
28
|
+
this.attribute = attribute;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The trace's root span: the first span with no parent. */
|
|
32
|
+
function rootSpan(trace) {
|
|
33
|
+
// Untyped walk over the caller's trace shape, as skill extraction does.
|
|
34
|
+
const spans = trace?.resourceSpans?.[0]?.scopeSpans?.[0]?.spans;
|
|
35
|
+
if (!Array.isArray(spans))
|
|
36
|
+
return undefined;
|
|
37
|
+
return spans.find((s) => s && !s.parentSpanId);
|
|
38
|
+
}
|
|
39
|
+
function stringAttr(span, key) {
|
|
40
|
+
const attr = span.attributes?.find((a) => a.key === key);
|
|
41
|
+
return attr?.value?.stringValue;
|
|
42
|
+
}
|
|
43
|
+
/** Parse an attribute as JSON text of an array of plain objects, each of
|
|
44
|
+
* whose `required` fields is a string and each of whose `optional` fields,
|
|
45
|
+
* when present, is a string. */
|
|
46
|
+
function objectArray(attribute, text, required, optional = []) {
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new RunAttributeError(attribute, 'is not JSON text');
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(parsed)) {
|
|
55
|
+
throw new RunAttributeError(attribute, 'is not a JSON array');
|
|
56
|
+
}
|
|
57
|
+
parsed.forEach((entry, i) => {
|
|
58
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
59
|
+
throw new RunAttributeError(attribute, `entry ${i} is not an object`);
|
|
60
|
+
}
|
|
61
|
+
for (const field of required) {
|
|
62
|
+
if (typeof entry[field] !== 'string') {
|
|
63
|
+
throw new RunAttributeError(attribute, `entry ${i} has no string ${field}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const field of optional) {
|
|
67
|
+
const value = entry[field];
|
|
68
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
69
|
+
throw new RunAttributeError(attribute, `entry ${i}: ${field} is not a string`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read the run attributes from a trace's root span. Returns `{}` for a trace
|
|
77
|
+
* with no root span — including a BlobRef trace's inspection view
|
|
78
|
+
* (`traceForInspection`), whose caller passes the fields as inputs instead.
|
|
79
|
+
*/
|
|
80
|
+
export function extractRunAttributes(trace) {
|
|
81
|
+
const root = rootSpan(trace);
|
|
82
|
+
if (!root)
|
|
83
|
+
return {};
|
|
84
|
+
const out = {};
|
|
85
|
+
const assertedDate = stringAttr(root, 'analysis.asserted_date');
|
|
86
|
+
if (assertedDate !== undefined)
|
|
87
|
+
out.assertedDate = assertedDate;
|
|
88
|
+
const servers = stringAttr(root, 'analysis.mcp_servers');
|
|
89
|
+
if (servers !== undefined) {
|
|
90
|
+
out.runMcpServers = objectArray('analysis.mcp_servers', servers, ['url'], ['name']);
|
|
91
|
+
}
|
|
92
|
+
const tools = stringAttr(root, 'analysis.tool_definitions');
|
|
93
|
+
if (tools !== undefined) {
|
|
94
|
+
out.toolDefinitions = objectArray('analysis.tool_definitions', tools, []);
|
|
95
|
+
}
|
|
96
|
+
const offered = stringAttr(root, 'analysis.sources_offered');
|
|
97
|
+
if (offered !== undefined) {
|
|
98
|
+
out.sourcesOffered = objectArray('analysis.sources_offered', offered, ['sourceId', 'alias', 'reason']);
|
|
99
|
+
}
|
|
100
|
+
const components = stringAttr(root, 'analysis.components');
|
|
101
|
+
if (components !== undefined) {
|
|
102
|
+
out.components = objectArray('analysis.components', components, ['id'], ['version', 'guidanceHash', 'guidanceOrigin']);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
package/dist/capture/trace.d.ts
CHANGED
|
@@ -50,7 +50,8 @@ export type TraceRandomBytes = (byteLength: number) => Uint8Array;
|
|
|
50
50
|
/**
|
|
51
51
|
* TraceBuilder identity + determinism inputs. The identity strings name the
|
|
52
52
|
* capturing service, so they are per-instance config (config-not-constants);
|
|
53
|
-
* the reference deployment's values are `CIVICAITOOLS_TRACE_CONFIG`
|
|
53
|
+
* the reference deployment's values are `CIVICAITOOLS_TRACE_CONFIG`
|
|
54
|
+
* (src/civic/trace.ts).
|
|
54
55
|
* The identity fields are required; `now`/`randomBytes` are operational
|
|
55
56
|
* fallbacks, not identity, and default to the runtime's clock and CSPRNG.
|
|
56
57
|
*/
|
|
@@ -69,9 +70,6 @@ export interface TraceBuilderConfig {
|
|
|
69
70
|
* `crypto.getRandomValues` (Web Crypto — Node 18+ and every browser). */
|
|
70
71
|
randomBytes?: TraceRandomBytes;
|
|
71
72
|
}
|
|
72
|
-
/** The civicaitools.org reference deployment's capture identity. Passed
|
|
73
|
-
* explicitly by the reference app — never applied as a default. */
|
|
74
|
-
export declare const CIVICAITOOLS_TRACE_CONFIG: TraceBuilderConfig;
|
|
75
73
|
export declare class TraceBuilder {
|
|
76
74
|
readonly traceId: string;
|
|
77
75
|
readonly rootSpanId: string;
|
package/dist/capture/trace.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// name/version, semconv version) are REQUIRED typed config inputs
|
|
9
9
|
// (config-not-constants, S2 brief §2); the reference deployment's values
|
|
10
10
|
// are exported as `CIVICAITOOLS_TRACE_CONFIG` for explicit use, never
|
|
11
|
-
// applied as a default.
|
|
11
|
+
// applied as a default. Since 0.7.0 that constant lives in
|
|
12
|
+
// src/civic/trace.ts (civic-ai-tools#246): capture defines no deployment
|
|
13
|
+
// value.
|
|
12
14
|
//
|
|
13
15
|
// Clock + RNG are INHERENT to capture (span timestamps and ids are what
|
|
14
16
|
// capture *is*): this module is the sanctioned exception to the package's
|
|
@@ -34,14 +36,6 @@ function attrsFromRecord(attrs) {
|
|
|
34
36
|
export function hash(content) {
|
|
35
37
|
return sha256Hex(content);
|
|
36
38
|
}
|
|
37
|
-
/** The civicaitools.org reference deployment's capture identity. Passed
|
|
38
|
-
* explicitly by the reference app — never applied as a default. */
|
|
39
|
-
export const CIVICAITOOLS_TRACE_CONFIG = {
|
|
40
|
-
serviceName: 'civic-ai-tools-website',
|
|
41
|
-
scopeName: 'civic-ai-tools-evidence',
|
|
42
|
-
scopeVersion: '0.1.0',
|
|
43
|
-
semconvVersion: '1.30.0',
|
|
44
|
-
};
|
|
45
39
|
function defaultRandomBytes(byteLength) {
|
|
46
40
|
const buf = new Uint8Array(byteLength);
|
|
47
41
|
globalThis.crypto.getRandomValues(buf);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type DataSourceEntry, type ToolCallSummary, type TraceSpan } from '../capture/data-sources.ts';
|
|
2
|
+
import { type CivicSourceRegistry, type ToolSourceResolver } from '../format/sources.ts';
|
|
3
|
+
export type { DataSourceEntry, ToolCallSummary } from '../capture/data-sources.ts';
|
|
4
|
+
/** Optional knobs for `resolveToolSource` / `buildDataSources`. Defaults are
|
|
5
|
+
* the civic demo values. */
|
|
6
|
+
export interface DataSourceOptions {
|
|
7
|
+
/** Tool-name → source-id resolver (fallback when a span carries no
|
|
8
|
+
* `mcp.source` attribute). Default: the civic map. */
|
|
9
|
+
resolver?: ToolSourceResolver;
|
|
10
|
+
/** Source registry driving catalog types, endpoints, and the
|
|
11
|
+
* dataset-keyed vs aggregate split. Default: the civic registry. */
|
|
12
|
+
registry?: CivicSourceRegistry;
|
|
13
|
+
/** Source id for calls neither the trace nor the resolver can identify.
|
|
14
|
+
* Default `unknown` (`UNKNOWN_SOURCE_ID`, ADR-0033 §A): not a registry
|
|
15
|
+
* entry, so such a call mints no entry. Until 0.7.0 the default was
|
|
16
|
+
* `socrata`. */
|
|
17
|
+
fallbackSourceId?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the MCP source for a tool call. Prefers the `mcp.source` attribute
|
|
21
|
+
* recorded on the matching `mcp_tool_call` span (the trace is the source of
|
|
22
|
+
* truth); falls back to the resolver's static mapping for packages written
|
|
23
|
+
* before source tagging or callers that ship an empty trace.
|
|
24
|
+
*
|
|
25
|
+
* Tool calls are paired to spans by index — the reference capture emits one
|
|
26
|
+
* span per call in order, so positional matching is exact in the normal
|
|
27
|
+
* flow. When the counts diverge, the static resolver still identifies the
|
|
28
|
+
* source.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveToolSource(toolCall: ToolCallSummary, span: TraceSpan | undefined, resolver?: ToolSourceResolver, fallbackSourceId?: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Build the per-source evidence-package `dataSources` array.
|
|
33
|
+
*
|
|
34
|
+
* Dataset-keyed sources (Socrata) contribute one entry per unique
|
|
35
|
+
* `dataset_id` observed across tool calls that also carried a `portal`
|
|
36
|
+
* argument; a dataset-keyed call that carried no portal contributes NO
|
|
37
|
+
* entry. Aggregate sources (Data Commons, Boston OpenContext — registry
|
|
38
|
+
* entries carrying `aggregatePortalUrl`) contribute a single entry when any
|
|
39
|
+
* of their tool calls was made. Unknown source ids contribute no entry. Each
|
|
40
|
+
* entry is tagged with `sourceId` so downstream consumers can distinguish
|
|
41
|
+
* provenance. Emission order: the dataset-keyed entries (first-seen order),
|
|
42
|
+
* then aggregate sources in registry insertion order — matching the
|
|
43
|
+
* reference implementation.
|
|
44
|
+
*
|
|
45
|
+
* A call the producer recorded as FAILED (`ToolCallSummary.failed`) asserts
|
|
46
|
+
* no access and contributes nothing on either branch; it keeps its position
|
|
47
|
+
* in the walk, because calls are paired to spans by index. The walk itself,
|
|
48
|
+
* and why each branch is shaped as it is, is `buildDataSourcesWith` in
|
|
49
|
+
* capture/data-sources.ts.
|
|
50
|
+
*
|
|
51
|
+
* @param fallbackPortal DEPRECATED, and inert since 0.3.1: an entry states
|
|
52
|
+
* the portal the call carried, never the run's. It stays third of five
|
|
53
|
+
* positional parameters so existing callers keep compiling, and since 0.4.0
|
|
54
|
+
* it also accepts `undefined`, which is what a caller that has stopped
|
|
55
|
+
* consulting it should pass. Dropping it is a breaking change and waits for
|
|
56
|
+
* a major.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildDataSources(toolCalls: ToolCallSummary[], trace: Record<string, unknown>, fallbackPortal: string | undefined, now: string, options?: DataSourceOptions): DataSourceEntry[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Civic data-source population (CIVIC layer) — the package's
|
|
2
|
+
// `buildDataSources`, `resolveToolSource` and `DataSourceOptions`, under the
|
|
3
|
+
// names and signatures they had when they lived in capture/data-sources.ts.
|
|
4
|
+
//
|
|
5
|
+
// The seam (civic-ai-tools#244 P2): capture/data-sources.ts walks the calls
|
|
6
|
+
// and the trace and takes every civic input as a parameter; this module
|
|
7
|
+
// supplies the civic defaults — `civicToolSourceResolver`,
|
|
8
|
+
// `CIVIC_SOURCE_REGISTRY`, `UNKNOWN_SOURCE_ID` and the format group's
|
|
9
|
+
// `isDatasetKeyedSource` — wherever the caller leaves one out.
|
|
10
|
+
import { buildDataSourcesWith, resolveToolSourceWith, } from "../capture/data-sources.js";
|
|
11
|
+
import { CIVIC_SOURCE_REGISTRY, UNKNOWN_SOURCE_ID, civicToolSourceResolver, isDatasetKeyedSource, } from "../format/sources.js";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the MCP source for a tool call. Prefers the `mcp.source` attribute
|
|
14
|
+
* recorded on the matching `mcp_tool_call` span (the trace is the source of
|
|
15
|
+
* truth); falls back to the resolver's static mapping for packages written
|
|
16
|
+
* before source tagging or callers that ship an empty trace.
|
|
17
|
+
*
|
|
18
|
+
* Tool calls are paired to spans by index — the reference capture emits one
|
|
19
|
+
* span per call in order, so positional matching is exact in the normal
|
|
20
|
+
* flow. When the counts diverge, the static resolver still identifies the
|
|
21
|
+
* source.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveToolSource(toolCall, span, resolver = civicToolSourceResolver, fallbackSourceId = UNKNOWN_SOURCE_ID) {
|
|
24
|
+
return resolveToolSourceWith(toolCall, span, resolver, fallbackSourceId);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the per-source evidence-package `dataSources` array.
|
|
28
|
+
*
|
|
29
|
+
* Dataset-keyed sources (Socrata) contribute one entry per unique
|
|
30
|
+
* `dataset_id` observed across tool calls that also carried a `portal`
|
|
31
|
+
* argument; a dataset-keyed call that carried no portal contributes NO
|
|
32
|
+
* entry. Aggregate sources (Data Commons, Boston OpenContext — registry
|
|
33
|
+
* entries carrying `aggregatePortalUrl`) contribute a single entry when any
|
|
34
|
+
* of their tool calls was made. Unknown source ids contribute no entry. Each
|
|
35
|
+
* entry is tagged with `sourceId` so downstream consumers can distinguish
|
|
36
|
+
* provenance. Emission order: the dataset-keyed entries (first-seen order),
|
|
37
|
+
* then aggregate sources in registry insertion order — matching the
|
|
38
|
+
* reference implementation.
|
|
39
|
+
*
|
|
40
|
+
* A call the producer recorded as FAILED (`ToolCallSummary.failed`) asserts
|
|
41
|
+
* no access and contributes nothing on either branch; it keeps its position
|
|
42
|
+
* in the walk, because calls are paired to spans by index. The walk itself,
|
|
43
|
+
* and why each branch is shaped as it is, is `buildDataSourcesWith` in
|
|
44
|
+
* capture/data-sources.ts.
|
|
45
|
+
*
|
|
46
|
+
* @param fallbackPortal DEPRECATED, and inert since 0.3.1: an entry states
|
|
47
|
+
* the portal the call carried, never the run's. It stays third of five
|
|
48
|
+
* positional parameters so existing callers keep compiling, and since 0.4.0
|
|
49
|
+
* it also accepts `undefined`, which is what a caller that has stopped
|
|
50
|
+
* consulting it should pass. Dropping it is a breaking change and waits for
|
|
51
|
+
* a major.
|
|
52
|
+
*/
|
|
53
|
+
export function buildDataSources(toolCalls, trace, fallbackPortal, now, options = {}) {
|
|
54
|
+
const resolver = options.resolver ?? civicToolSourceResolver;
|
|
55
|
+
const registry = options.registry ?? CIVIC_SOURCE_REGISTRY;
|
|
56
|
+
const fallbackSourceId = options.fallbackSourceId ?? UNKNOWN_SOURCE_ID;
|
|
57
|
+
return buildDataSourcesWith(toolCalls, trace, now, {
|
|
58
|
+
resolver,
|
|
59
|
+
registry,
|
|
60
|
+
fallbackSourceId,
|
|
61
|
+
isDatasetKeyed: (sourceId) => isDatasetKeyedSource(sourceId, registry),
|
|
62
|
+
});
|
|
63
|
+
}
|