@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
|
|
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
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|