@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.
@@ -1,33 +1,23 @@
1
- // Civic provenance builder (CAPTURE group) — walks an OTel trace and maps the
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. Every `civic:` term, urn scheme, and agent coordinate is
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
- // Config-not-constants: the platform-agent identity, the source-agent
11
- // registry (server URLs), and the model-agent description are typed config
12
- // inputs, and the config is REQUIRED — no deployment identity is ever
13
- // applied silently (ADR-0024 posture at the domain layer).
14
- // `CIVICAITOOLS_PROVENANCE_CONFIG` is the reference deployment's values,
15
- // which the reference app passes explicitly; with it, the builder reproduces
16
- // the reference implementation's output byte-for-byte (property insertion
17
- // order is preserved throughout — the legacy hash chain's byte contract).
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 buildProvenanceGraph(trace, input, config) {
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.fallbackSourceId ?? FALLBACK_SOURCE_ID;
82
- const skillSourceId = config.skillSourceId ?? FALLBACK_SOURCE_ID;
83
- // Vocabulary era — settlement-era unless a prior-era reproduction asks
84
- // otherwise. Every id and the `@context` below route through it, so a graph
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
- [CIVIC_TERM_CONTENT_HASH]: `sha256:${promptHash}`,
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
- [CIVIC_TERM_CONTENT_HASH]: `sha256:${skillHash}`,
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
- [CIVIC_TERM_CONTENT_HASH]: `sha256:${outputHash}`,
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. Tool spans carry `mcp.source`; records captured before
126
- // source tagging have no source attribute, so they fall back to the
127
- // configured fallback source (socrata — the only source at the time) for
128
- // backwards compatibility. Agent coordinates come from the source registry;
129
- // the skill-fetch span URL overrides the skill source's server URL.
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
- const source = getAttr(span.attributes, 'mcp.source') || fallbackSourceId;
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 ? { [CIVIC_TERM_SERVER_URL]: meta.serverUrl } : {}),
161
- [CIVIC_TERM_SOURCE_ID]: sourceId,
158
+ ...(meta.serverUrl ? { [terms.serverUrl]: meta.serverUrl } : {}),
159
+ [terms.sourceId]: sourceId,
162
160
  }, ['prov:SoftwareAgent']));
163
161
  }
164
- // Resolve a source id to its agent URN, falling back to the configured
165
- // fallback source for records that never tagged the span. (Reference
166
- // behavior preserved: unknown ids resolve to the fallback source's agent.)
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
- const id = sourceId || fallbackSourceId;
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
- [CIVIC_TERM_URL]: config.platformAgent.url,
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 ? { [CIVIC_TERM_PROMPT_TOKENS]: Number(promptTokens) } : {}),
202
- ...(completionTokens ? { [CIVIC_TERM_COMPLETION_TOKENS]: Number(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 = getAttr(span.attributes, 'mcp.source') || fallbackSourceId;
230
+ const toolSource = sourceOfSpan(span.attributes);
234
231
  const toolAgentUrn = agentUrnForSource(toolSource);
235
- const toolSourceDatasetKeyed = isDatasetKeyedSource(toolSource, registry);
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
- [CIVIC_TERM_CONTENT_HASH]: `sha256:${queryHash}`,
247
+ [terms.contentHash]: `sha256:${queryHash}`,
245
248
  // Omitted — not placeholdered — when the span named no tool.
246
- ...(toolName ? { [CIVIC_TERM_TOOL_NAME]: toolName } : {}),
247
- [CIVIC_TERM_OPERATION_TYPE]: opType,
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
- [CIVIC_TERM_CONTENT_HASH]: `sha256:${responseHash}`,
278
+ [terms.contentHash]: `sha256:${responseHash}`,
276
279
  'dcterms:description': description,
277
- [CIVIC_TERM_SOURCE_ID]: toolSource,
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
- [CIVIC_TERM_DATASET_ID]: datasetId,
288
+ [terms.datasetId]: datasetId,
286
289
  ...(portalDomain
287
290
  ? {
288
- [CIVIC_TERM_PORTAL_DOMAIN]: portalDomain,
289
- [CIVIC_TERM_DATASET_URL]: `https://${portalDomain}/d/${datasetId}`,
291
+ [terms.portalDomain]: portalDomain,
292
+ [terms.datasetUrl]: `https://${portalDomain}/d/${datasetId}`,
290
293
  }
291
294
  : {}),
292
- [CIVIC_TERM_CROISSANT_METADATA_URL]: null, // hook for future Croissant integration
295
+ [terms.croissantMetadataUrl]: null, // hook for future Croissant integration
293
296
  }
294
297
  : {}),
295
- ...(responseRows ? { [CIVIC_TERM_RESPONSE_ROWS]: Number(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
- [CIVIC_TERM_SOURCE_ID]: toolSource,
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 ? { [CIVIC_TERM_DURATION_MS]: Number(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 ? { [CIVIC_TERM_FAILED]: true } : {}),
357
- ...(failureKind ? { [CIVIC_TERM_FAILURE_KIND]: 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
+ }
@@ -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` below.
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;
@@ -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
+ }