@typedstandards/civic-typed-harness 0.3.0 → 0.3.1

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 CHANGED
@@ -3,6 +3,57 @@
3
3
  Factual record of what changed per published version. Section references are
4
4
  to the Typed Standards specification unless noted otherwise.
5
5
 
6
+ ## 0.3.1 — 2026-09-02
7
+
8
+ **The graph states what the span carried, and states absence as absence**
9
+ ([civic-ai-tools-website#384](https://github.com/npstorey/civic-ai-tools-website/issues/384),
10
+ Wave N9 P-H1). Patch, not breaking: no existing export is removed or
11
+ retyped, and every golden byte — both vocabulary eras, all eight
12
+ golden-reproduction cases — is unchanged.
13
+
14
+ - **`buildProvenanceGraph` no longer invents a tool name.** An
15
+ `mcp_tool_call` span with no `tool.name` yields a query entity with no
16
+ `civic:toolName` key (omitted, not placeholdered) and a tool-call activity
17
+ described as `MCP tool call (<operation type>)`. Previously the builder
18
+ substituted `get_data`.
19
+ - **`buildProvenanceGraph` no longer attributes a data response to the run's
20
+ portal.** `civic:portalDomain` and `civic:datasetUrl` are emitted only when
21
+ the span carried both `tool.portal_domain` and `tool.dataset_id`. A
22
+ dataset-keyed span with no portal is described by its source agent's
23
+ registry title (`Data response from Socrata MCP Server`), the form
24
+ aggregate and unknown sources already took; a span with a dataset id and
25
+ no portal states `civic:datasetId` and mints no URL. Previously the builder
26
+ substituted `ProvenanceInput.portal` — the run's selected portal — which
27
+ attributed every `search` and `fetch` response to a portal the call never
28
+ addressed (the Socrata server's `search` and `fetch` take no portal and
29
+ answer from the portal that server is configured for). The graph does not
30
+ parse tool arguments: a portal embedded in a `fetch` id is not a portal the
31
+ span carried.
32
+ - **`buildDataSources` no longer mints an entry on `fallbackPortal`.** A
33
+ dataset-keyed call whose arguments carry `dataset_id` but no `portal`
34
+ contributes no `dataSources` entry — `DataSourceEntry.portalUrl` is a
35
+ required string in produce-core, so an entry with no portal is not a shape
36
+ this package can emit — and the call remains on the graph's tool-call
37
+ activities. Calls that carry a portal (every `get_data` call from the
38
+ reference producer, whose loop injects the run portal before the record is
39
+ built) are unchanged.
40
+ - **Unchanged signatures, two inputs now unused.** `ProvenanceInput.portal`
41
+ and the `fallbackPortal` parameter of `buildDataSources` keep their exact
42
+ types and positions, are accepted, and are not consulted; both say so at
43
+ the declaration.
44
+ - **Byte consequence.** A package produced by this version from a trace
45
+ whose tool spans all carry `tool.name`, and carry `tool.portal_domain`
46
+ wherever they carry a response hash, is byte-identical to one produced by
47
+ 0.3.0. Where a span carried less, the 0.3.0 output asserted a value the
48
+ span did not, and this version's output differs from it by exactly that
49
+ assertion. Packages already signed under 0.3.0 are untouched and remain
50
+ verifiable exactly as published.
51
+ - **Dependency range widened, nothing resolved differently.**
52
+ `@typedstandards/produce-core` is accepted at `^0.3.0 || ^0.4.0` so a
53
+ consumer that takes produce-core 0.4.0 (an additive minor) resolves one
54
+ copy rather than nesting a second under this package. The lockfile here
55
+ still resolves 0.3.0.
56
+
6
57
  ## 0.3.0 — 2026-08-20
7
58
 
8
59
  **Settlement-era civic vocabulary + produce-core 0.3.0**
@@ -47,12 +47,18 @@ export declare function resolveToolSource(toolCall: ToolCallSummary, span: Trace
47
47
  * Build the per-source evidence-package `dataSources` array.
48
48
  *
49
49
  * Dataset-keyed sources (Socrata) contribute one entry per unique
50
- * `dataset_id` observed across tool calls. Aggregate sources (Data Commons,
50
+ * `dataset_id` observed across tool calls that also carried a `portal`
51
+ * argument; a dataset-keyed call that carried no portal contributes NO
52
+ * entry (the loop body says why). Aggregate sources (Data Commons,
51
53
  * Boston OpenContext — registry entries carrying `aggregatePortalUrl`)
52
54
  * contribute a single entry when any of their tool calls was made. Unknown
53
55
  * source ids contribute no entry. Each entry is tagged with `sourceId` so
54
56
  * downstream consumers can distinguish provenance. Emission order: the
55
57
  * dataset-keyed entries (first-seen order), then aggregate sources in
56
58
  * registry insertion order — matching the reference implementation.
59
+ *
60
+ * `fallbackPortal` is accepted and NOT consulted since 0.3.1: an entry
61
+ * states the portal the call carried, never the run's. The parameter stays
62
+ * in the signature so existing callers keep compiling.
57
63
  */
58
64
  export declare function buildDataSources(toolCalls: ToolCallSummary[], trace: Record<string, unknown>, fallbackPortal: string, now: string, options?: DataSourceOptions): DataSourceEntry[];
@@ -51,13 +51,19 @@ export function resolveToolSource(toolCall, span, resolver = civicToolSourceReso
51
51
  * Build the per-source evidence-package `dataSources` array.
52
52
  *
53
53
  * Dataset-keyed sources (Socrata) contribute one entry per unique
54
- * `dataset_id` observed across tool calls. Aggregate sources (Data Commons,
54
+ * `dataset_id` observed across tool calls that also carried a `portal`
55
+ * argument; a dataset-keyed call that carried no portal contributes NO
56
+ * entry (the loop body says why). Aggregate sources (Data Commons,
55
57
  * Boston OpenContext — registry entries carrying `aggregatePortalUrl`)
56
58
  * contribute a single entry when any of their tool calls was made. Unknown
57
59
  * source ids contribute no entry. Each entry is tagged with `sourceId` so
58
60
  * downstream consumers can distinguish provenance. Emission order: the
59
61
  * dataset-keyed entries (first-seen order), then aggregate sources in
60
62
  * registry insertion order — matching the reference implementation.
63
+ *
64
+ * `fallbackPortal` is accepted and NOT consulted since 0.3.1: an entry
65
+ * states the portal the call carried, never the run's. The parameter stays
66
+ * in the signature so existing callers keep compiling.
61
67
  */
62
68
  export function buildDataSources(toolCalls, trace, fallbackPortal, now, options = {}) {
63
69
  const resolver = options.resolver ?? civicToolSourceResolver;
@@ -71,8 +77,20 @@ export function buildDataSources(toolCalls, trace, fallbackPortal, now, options
71
77
  const source = resolveToolSource(tc, toolSpans[i], resolver, fallbackSourceId);
72
78
  if (isDatasetKeyedSource(source, registry)) {
73
79
  const datasetId = tc.args.dataset_id;
74
- const portal = tc.args.portal || fallbackPortal;
75
- if (datasetId) {
80
+ const portal = tc.args.portal;
81
+ // An entry is minted only from what the call carried: a dataset id AND
82
+ // a portal. A dataset-keyed call with a dataset id and no portal
83
+ // contributes no entry. `DataSourceEntry.portalUrl` is a required
84
+ // string (produce-core), so "an entry with no portal" is not a shape
85
+ // this package can emit; and substituting the run's portal
86
+ // (`fallbackPortal`, before 0.3.1) attributed the call to a portal it
87
+ // never addressed. Omission is the honest shape — the call is still on
88
+ // the PROV-O graph's tool-call activities, stated without a portal.
89
+ // The branch is latent for the reference producer, whose loop injects
90
+ // the run portal into `get_data` arguments before the record is built
91
+ // (run-tool-loop.ts:799 at the time of writing); any caller whose
92
+ // summary carries `dataset_id` without `portal` reaches it.
93
+ if (datasetId && portal) {
76
94
  let byDataset = datasetKeyed.get(source);
77
95
  if (!byDataset) {
78
96
  byDataset = new Map();
@@ -15,6 +15,12 @@ export interface ProvenanceInput {
15
15
  * itself is exactly this value by construction. */
16
16
  outputHash?: string;
17
17
  model: string;
18
+ /** The run's selected portal. Accepted and NOT consulted by the graph
19
+ * builder since 0.3.1: the graph states the portal a tool span carried
20
+ * (`tool.portal_domain`) and states absence as absence — it never
21
+ * substitutes the run's portal for one the call did not address. The
22
+ * field stays in the type so callers that pass it (the reference app
23
+ * does, as an object literal) keep compiling. */
18
24
  portal: string;
19
25
  }
20
26
  /** Instance configuration for the graph build (config-not-constants). */
@@ -45,7 +51,9 @@ export interface ProvenanceConfig {
45
51
  vocabulary?: CivicVocabulary;
46
52
  }
47
53
  /** The civicaitools.org reference deployment's values. Passed explicitly by
48
- * the reference app — never applied as a default. */
54
+ * the reference app — never applied as a default, and never spread into
55
+ * another instance's config, which would assert infrastructure that
56
+ * instance doesn't run. */
49
57
  export declare const CIVICAITOOLS_PROVENANCE_CONFIG: ProvenanceConfig;
50
58
  /**
51
59
  * Build a W3C PROV-O JSON-LD graph from an OTel trace and package metadata.
@@ -20,7 +20,9 @@ import { hash } from "./trace.js";
20
20
  import { CIVIC_VOCABULARY, CIVICAITOOLS_PLATFORM_AGENT, } from "../format/vocabulary.js";
21
21
  import { CIVIC_SOURCE_REGISTRY, FALLBACK_SOURCE_ID, isDatasetKeyedSource, } from "../format/sources.js";
22
22
  /** The civicaitools.org reference deployment's values. Passed explicitly by
23
- * the reference app — never applied as a default. */
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. */
24
26
  export const CIVICAITOOLS_PROVENANCE_CONFIG = {
25
27
  platformAgent: CIVICAITOOLS_PLATFORM_AGENT,
26
28
  sourceRegistry: CIVIC_SOURCE_REGISTRY,
@@ -48,7 +50,8 @@ function nanoToIso(nano) {
48
50
  export function buildProvenanceGraph(trace, input, config) {
49
51
  const otel = trace;
50
52
  const spans = otel?.resourceSpans?.[0]?.scopeSpans?.[0]?.spans || [];
51
- const { packageId, promptHash, promptText, outputText, model, portal } = input;
53
+ // `input.portal` is deliberately not read — see `ProvenanceInput.portal`.
54
+ const { packageId, promptHash, promptText, outputText, model } = input;
52
55
  const outputHash = input.outputHash ?? hash(outputText ?? '');
53
56
  const registry = config.sourceRegistry;
54
57
  const fallbackSourceId = config.fallbackSourceId ?? FALLBACK_SOURCE_ID;
@@ -167,17 +170,34 @@ export function buildProvenanceGraph(trace, input, config) {
167
170
  ...(completionTokens ? { 'civic:completionTokens': Number(completionTokens) } : {}),
168
171
  }));
169
172
  }
170
- // MCP tool call spans
173
+ // MCP tool call spans.
174
+ //
175
+ // Every node derived from a span states what the span carried, and states
176
+ // absence as absence: a span with no `tool.name` yields nodes that name no
177
+ // tool, and a span with no `tool.portal_domain` yields a data response
178
+ // attributed to no portal. Nothing here substitutes the run's selected
179
+ // portal (`input.portal`, accepted and unused since 0.3.1) or a default
180
+ // tool name for a value the producer did not write. The reference
181
+ // producer's loop always writes `tool.name`, and writes
182
+ // `tool.portal_domain` only when the call's arguments carried a portal —
183
+ // which its portal injection does for `get_data` and for no other tool.
184
+ // The Socrata server's `search` (one argument, `query`) and `fetch` (one
185
+ // argument, `id`) address the portal that server is configured for, which
186
+ // the producer does not know. A `fetch` id may embed a portal, but that
187
+ // grammar belongs to the server: the graph does not parse arguments, so an
188
+ // id is never a portal the span carried.
171
189
  const dataResponseUrns = [];
172
190
  for (const span of toolSpans) {
173
191
  const toolCallUrn = vocab.urn(packageId, 'tool-call', span.spanId);
174
192
  const argsStr = getAttr(span.attributes, 'tool.arguments') || '{}';
175
193
  const queryHash = hash(argsStr);
176
194
  const responseHash = getAttr(span.attributes, 'tool.response_hash');
177
- const toolName = getAttr(span.attributes, 'tool.name') || 'get_data';
195
+ // Absent when the span carried none — never defaulted.
196
+ const toolName = getAttr(span.attributes, 'tool.name');
178
197
  const opType = getAttr(span.attributes, 'tool.operation_type') || 'unknown';
179
198
  const datasetId = getAttr(span.attributes, 'tool.dataset_id');
180
- const portalDomain = getAttr(span.attributes, 'tool.portal_domain') || portal;
199
+ // Absent when the span carried none — never the run's portal.
200
+ const portalDomain = getAttr(span.attributes, 'tool.portal_domain');
181
201
  const toolSource = getAttr(span.attributes, 'mcp.source') || fallbackSourceId;
182
202
  const toolAgentUrn = agentUrnForSource(toolSource);
183
203
  const toolSourceDatasetKeyed = isDatasetKeyedSource(toolSource, registry);
@@ -190,7 +210,8 @@ export function buildProvenanceGraph(trace, input, config) {
190
210
  const queryUrn = vocab.urn(packageId, 'query', queryHash);
191
211
  graph.push(makeEntityNode(queryUrn, {
192
212
  'civic:contentHash': `sha256:${queryHash}`,
193
- 'civic:toolName': toolName,
213
+ // Omitted — not placeholdered — when the span named no tool.
214
+ ...(toolName ? { 'civic:toolName': toolName } : {}),
194
215
  'civic:operationType': opType,
195
216
  'dcterms:description': `MCP tool arguments (${opType})`,
196
217
  ...(precedingInference
@@ -201,10 +222,20 @@ export function buildProvenanceGraph(trace, input, config) {
201
222
  if (responseHash) {
202
223
  const dataUrn = vocab.urn(packageId, 'data', responseHash);
203
224
  dataResponseUrns.push(dataUrn);
204
- // Description varies by source — dataset-keyed sources (Socrata) have
205
- // a portal domain; aggregate/unknown sources are described by their
206
- // agent title.
207
- const description = toolSourceDatasetKeyed
225
+ // Description: the portal the span carried, when it carried one;
226
+ // otherwise the agent that answered the call, by its registry title —
227
+ // the form aggregate and unknown sources already take. A dataset-keyed
228
+ // source whose span carried a dataset id but no portal takes the
229
+ // agent-title form too, with the dataset id stated below as
230
+ // `civic:datasetId` and no URL minted (a dataset URL needs a host the
231
+ // span did not carry). That branch is latent by construction for the
232
+ // reference producer — its loop injects the run portal into `get_data`
233
+ // arguments before the span opens (run-tool-loop.ts:799 at the time of
234
+ // writing), and `get_data` is the only tool whose arguments carry a
235
+ // dataset id — but any producer that writes `tool.dataset_id` without
236
+ // `tool.portal_domain` reaches it, so it is stated honestly rather
237
+ // than left dead.
238
+ const description = toolSourceDatasetKeyed && portalDomain
208
239
  ? `Data response from ${portalDomain}`
209
240
  : `Data response from ${sourceAgentMap[toolSource]?.title || toolSource}`;
210
241
  const responseRows = getAttr(span.attributes, 'tool.response_rows');
@@ -214,12 +245,18 @@ export function buildProvenanceGraph(trace, input, config) {
214
245
  'civic:sourceId': toolSource,
215
246
  ...provWasGeneratedBy(toolCallUrn),
216
247
  // Croissant 1.1 placeholder — only meaningful for dataset-keyed
217
- // sources today.
248
+ // sources today. The dataset id is stated whenever the span carried
249
+ // one; the portal and the dataset URL only when the span carried
250
+ // the portal as well. Key order is the byte contract.
218
251
  ...(toolSourceDatasetKeyed && datasetId
219
252
  ? {
220
253
  'civic:datasetId': datasetId,
221
- 'civic:portalDomain': portalDomain,
222
- 'civic:datasetUrl': `https://${portalDomain}/d/${datasetId}`,
254
+ ...(portalDomain
255
+ ? {
256
+ 'civic:portalDomain': portalDomain,
257
+ 'civic:datasetUrl': `https://${portalDomain}/d/${datasetId}`,
258
+ }
259
+ : {}),
223
260
  'civic:croissantMetadataUrl': null, // hook for future Croissant integration
224
261
  }
225
262
  : {}),
@@ -231,7 +268,10 @@ export function buildProvenanceGraph(trace, input, config) {
231
268
  // agent (e.g. socrata for `get_data`, data-commons for `get_observations`).
232
269
  const durationMs = getAttr(span.attributes, 'tool.duration_ms');
233
270
  graph.push(makeActivityNode(toolCallUrn, {
234
- 'dcterms:description': `MCP tool call: ${toolName} (${opType})`,
271
+ // Names the tool only when the span did.
272
+ 'dcterms:description': toolName
273
+ ? `MCP tool call: ${toolName} (${opType})`
274
+ : `MCP tool call (${opType})`,
235
275
  'civic:sourceId': toolSource,
236
276
  ...provUsed([queryUrn]),
237
277
  ...provWasAssociatedWith(toolAgentUrn),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typedstandards/civic-typed-harness",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Civic-domain harness for Typed Standards evidence packages — the DOMAIN layer of the ADR-0021 format/domain line: civic PROV-O vocabulary and provenance builder, datHere envelope-policy derivations, and the capture machinery (trace builder, skill-metadata extraction, data-source population, adversarial rubric core). Derives; @typedstandards/produce-core assembles.",
5
5
  "license": "MIT",
6
6
  "author": "Nathan Storey",
@@ -33,7 +33,7 @@
33
33
  "prepublishOnly": "npm run clean && npm run build"
34
34
  },
35
35
  "dependencies": {
36
- "@typedstandards/produce-core": "^0.3.0"
36
+ "@typedstandards/produce-core": "^0.3.0 || ^0.4.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@typedstandards/verify-core": "^0.9.0",