@typedstandards/civic-typed-harness 0.3.1 → 0.4.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,246 @@
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.4.1 — 2026-09-12
7
+
8
+ Patch: one existing export changes its output for one input shape, and no
9
+ export is added, removed or retyped. That is measured across the release
10
+ boundary rather than asserted — the published `0.4.0` and this build each
11
+ expose 49 runtime exports from the package root, none added, none removed,
12
+ none changing runtime type, and no declared top-level name in `dist/*.d.ts`
13
+ differing between them. The tarball carries the same 28 files, and 22 of the
14
+ 24 under `dist/` are byte-identical to `0.4.0`; the two that are not are
15
+ `dist/rubric/adversarial-eval-core.js` and its `.d.ts`, the latter differing
16
+ by a doc comment alone.
17
+
18
+ **What this release claims about golden bytes, and what it does not.** Every
19
+ golden byte is unchanged. This time that sentence is not the evidence, and
20
+ saying so is the point: **no golden fixture reaches the export that changed.**
21
+ `buildEvaluationPrompt` is named in two test files, both under `src/rubric/`,
22
+ and in neither golden suite. `__fixtures__/website-golden.json` carries no
23
+ `queries` array at all; `__fixtures__/reference-golden.json` carries eight and
24
+ **zero** occurrences of the `failed` key that is this release's only new
25
+ branch. So the golden suites are green for this change the way they are green
26
+ for a change to a file they never import, and their green is not evidence
27
+ about it. This is the same narrowing
28
+ [civic-ai-tools#199](https://github.com/npstorey/civic-ai-tools/issues/199)
29
+ records for the graph builder, met from the other side.
30
+
31
+ What **is** evidence is a direct measurement of the changed export either side
32
+ of the release boundary: `buildEvaluationPrompt` imported from the published
33
+ `0.4.0` and from this build, run over the same package.
34
+
35
+ - A package in which **no entry records `failed: true`** renders
36
+ **byte-identical** on both — 492 bytes, `sha256 ed77c84e…` — over five entry
37
+ shapes: `resultRows: 12`, `resultRows: 0`, no `resultRows` at all,
38
+ `failed: false`, and a bare `failureKind` carrying no `failed`.
39
+ - The same package plus one entry recorded `failed: true` renders
40
+ **differently** (539 bytes against 607). That case is the reason the first
41
+ one is worth reporting: an instrument that answers "identical" whatever it is
42
+ fed has not measured anything, and this one is shown answering "different"
43
+ on the shape it must flag.
44
+
45
+ **The evaluator reads a rejected call as rejected**
46
+ ([civic-ai-tools#203](https://github.com/npstorey/civic-ai-tools/issues/203),
47
+ Wave N11 P-H1). Behaviour change, confined to one input shape.
48
+
49
+ - **The line that changes.** A `queries[]` entry the producer recorded as
50
+ rejected rendered `→ ? rows` — the same line an entry whose row count was
51
+ simply never recorded produces. A rejection and an unrecorded count were one
52
+ line to the evaluator, and the rubric asks that evaluator to cross-check the
53
+ answer's figures against the data the tool calls returned, so the conflation
54
+ invited it to read data that was never returned as merely uncounted. Its
55
+ score becomes a signed attestation. The entry now reads `→ REJECTED by the
56
+ source — no data was returned, and no row count is claimed`.
57
+ - **`failed: true` is the whole assertion.** `failureKind` is declared on the
58
+ shape and **deliberately not rendered**: it is producer-controlled text, and
59
+ the turn built here is read by a model whose score is signed, so the
60
+ rejection is stated in this module's own fixed words, interpolating nothing
61
+ the producer wrote and claiming no row count.
62
+ - **Absence stays absence.** An entry that records no outcome renders exactly
63
+ as it did before the fields existed. `failed: false` is not a rejection, and
64
+ a `failureKind` with no `failed` is not a rejection — the posture
65
+ `ToolCallSummary.failed` already takes in `capture/data-sources.ts`.
66
+ - **No dependency moves.** The two keys are read off a locally-declared shape
67
+ rather than imported, following that same file: produce-core `0.3.0`, which
68
+ the lockfile resolves, names neither key. No manifest change, no lockfile
69
+ bump, no new dependency.
70
+
71
+ **`RUBRIC_VERSION_SHA256` does not move, and the claim around it is corrected
72
+ rather than left standing**
73
+ ([civic-ai-tools#207](https://github.com/npstorey/civic-ai-tools/issues/207)).
74
+ The hash is `sha256Hex(EVALUATION_RUBRIC)` — the rubric text alone. It does not
75
+ cover `buildEvaluationPrompt`, the template that assembles that rubric with the
76
+ package's tool calls, data sources and model into the turn the evaluator
77
+ actually reads. So this release changes what an evaluator is shown without
78
+ changing the value an attestation pins as `methodology.promptSetVersion`: two
79
+ evaluations either side of it carry the same value, measured here as
80
+ `b62b193c992fd430cc82dff9a80a057bbe0b2b656fbdace450ca1a5d99fffa02` on both the
81
+ published `0.4.0` and this build.
82
+
83
+ - **The overclaim is retired in the same change.** ADR-0015 §3, Q26's
84
+ resolution note and the two module comments said the value pinned the whole
85
+ prompt set that produced the scores. They now say it pins the rubric
86
+ *wording*, and that the prompt template is outside the hash. The phrase they
87
+ used for the wider claim appears nowhere in the tree at this release.
88
+ - **The gap is filed, not resolved.** #207 records what the hash covers, what
89
+ it does not, and the two spec-level options — widen the hash, or add a second
90
+ field — with no recommendation. Which one is right is a specification
91
+ decision, not a package's.
92
+
93
+ A consumer sees none of this until its own lockfile moves: `^0.4.0` admits
94
+ `0.4.1`, so the manifest range need not change and the refresh is the whole of
95
+ the upgrade.
96
+
97
+ ## 0.4.0 — 2026-09-04
98
+
99
+ Minor: two optional input fields are added and no existing export is removed
100
+ or retyped. `fallbackPortal` and `ProvenanceInput.portal`, inert since 0.3.1,
101
+ are widened and deprecated rather than dropped — removing either is breaking
102
+ and waits for a major.
103
+
104
+ **What this release claims about golden bytes, and what it does not.** Every
105
+ golden byte is unchanged — both vocabulary eras, all eight
106
+ golden-reproduction cases, and `__fixtures__/website-golden.json`. That
107
+ sentence has appeared in every release note since 0.3.0, and it is narrower
108
+ than it reads: **`__fixtures__/reference-golden.json` carries no spans at
109
+ all** — `spanId` and `scopeSpans` each occur zero times across its eight
110
+ cases, whose traces are empty `resourceSpans` arrays or BlobRefs. So those
111
+ eight cases are green for anything the tool-span loop does, including the
112
+ rejection marker this release adds, and their green is not evidence about it.
113
+
114
+ `website-golden.json` is the only golden fixture that exercises span-derived
115
+ output: five tool-call activities and four data-response descriptions. It is
116
+ the fixture that proves this release's byte-stability claim, and the one the
117
+ conditional spread of `civic:failed` was measured against — driven red by
118
+ making that spread unconditional, which moves it. Read
119
+ [civic-ai-tools#199](https://github.com/npstorey/civic-ai-tools/issues/199)
120
+ before citing a green golden suite as evidence about the graph builder.
121
+
122
+ **The reader-facing source name is measured, not changed**
123
+ ([civic-ai-tools#194](https://github.com/npstorey/civic-ai-tools/issues/194),
124
+ Wave N10 P-H3). Tests only: no exported behaviour changes, and every golden
125
+ byte — both vocabulary eras, all eight golden-reproduction cases — is
126
+ unchanged.
127
+
128
+ - **The change #194 asks for moves frozen golden bytes.** A data response the
129
+ builder cannot describe by a portal is described by its source's registry
130
+ `agentTitle` (`Data response from Socrata MCP Server`) — the agent's title,
131
+ implementation language on a reader-facing surface. Reading the registry's
132
+ `displayName` instead — the reader-facing name, in the registry beside
133
+ `agentTitle` since the package's first release — is a one-expression change,
134
+ and it rewrites two lines inside `__fixtures__/website-golden.json`'s
135
+ captured graph: the data-commons and boston-opencontext data responses, both
136
+ on the agent-title branch. Three whole-graph byte-parity assertions read
137
+ those two lines. A golden fixture is never edited to match new behaviour, so
138
+ the change waits on a decision about whether those bytes move.
139
+ - **The vocabulary era cannot carry it.** `CivicVocabulary` binds exactly two
140
+ literals — the `civic:` namespace and the id-scheme prefix
141
+ (`makeCivicVocabulary(ns, urnPrefix)`) — and both era suites assert the two
142
+ eras differ by those and by nothing else. A description's wording is neither
143
+ of them and is not era-scoped: the same prefix is emitted under both eras, so
144
+ gating it on the era would make prior-era reproduction and settlement-era
145
+ emission disagree about a reader's wording rather than about an identifier.
146
+ - **Two instruments, both able to fail.** `provenance.test.ts` now pins the
147
+ agent node's `dcterms:title` to the registry `agentTitle` — those are signed
148
+ bytes in both golden fixtures and the one thing that does not move under
149
+ either decision — and names the two golden descriptions at stake, asserting
150
+ each reads by agent title today and *not* by display name. The sources they
151
+ drive all carry two distinct registry names, which is what makes the
152
+ assertions able to fail; a builder changed under either one turns it red.
153
+ - **What holds still either way.** The portal branch
154
+ (`Data response from data.cityofnewyork.us`) and the unknown-source fallback
155
+ (`Data response from euro stat`, the raw source id) move under neither
156
+ decision.
157
+
158
+ **The PROV-O activity for a rejected call says it was rejected**
159
+ ([civic-ai-tools#193](https://github.com/npstorey/civic-ai-tools/issues/193),
160
+ Wave N10 P-H2). Additive: no existing export is removed or retyped, and every
161
+ golden byte — both vocabulary eras, all eight golden-reproduction cases — is
162
+ unchanged.
163
+
164
+ - **Two new activity terms.** `CIVIC_TERM_FAILED` (`civic:failed`) and
165
+ `CIVIC_TERM_FAILURE_KIND` (`civic:failureKind`) are declared in
166
+ `src/format/vocabulary.ts` and exported from the package root. A `civic:`
167
+ property name is vocabulary as much as the namespace it hangs under, so the
168
+ capture-side builder imports them rather than spelling them; `purity.test.ts`
169
+ now lists both, which is what makes that a claim able to fail.
170
+ - **`buildProvenanceGraph` reads the span's failure.** A tool span the producer
171
+ ended with the boolean `error: true` yields a tool-call activity carrying
172
+ `civic:failed: true`, and `civic:failureKind` with the span's `error.kind`
173
+ verbatim when it carried one. Previously the builder read nine `tool.*` /
174
+ `mcp.*` attributes and never asked about the outcome, so a call the source
175
+ REFUSED and one that answered were the same node with the same description —
176
+ and, a rejected call having no response hash, the absent data-response entity
177
+ was the only trace of the rejection in the graph. `error.kind` is the one
178
+ attribute name across this package and the reference producer.
179
+ - **`error` is the assertion, `error.kind` only a label on one.** A span
180
+ carrying a kind and no assertion is not a rejection and yields neither key —
181
+ the posture `ToolCallSummary.failed` / `failureKind` already takes.
182
+ - **A new boolean attribute reader.** The module's string attribute reader
183
+ returns `stringValue ?? intValue` and cannot see `boolValue` at all, so it
184
+ returned `undefined` for a span that really did record a rejection.
185
+ `getBoolAttr` is a separate, strictly-typed reader rather than a widening of
186
+ the string one: the nine attributes that reader serves are strings by
187
+ contract, and a truthiness test would read the string `"false"` as an
188
+ assertion of failure. Only the boolean `true` marks an activity.
189
+ - **The description states no cause.** `dcterms:description` is byte-unchanged
190
+ — it states what the call WAS, and the marker states how it ended. The
191
+ classified kind is the only cause the graph will ever carry: the reference
192
+ producer stopped writing a rejection's raw text onto the span in this same
193
+ wave, and the builder does not read it back in one layer up.
194
+ - **Byte consequence.** Both terms are spread conditionally and appended after
195
+ every key the activity already carried, exactly as `civic:durationMs` is. A
196
+ span that recorded no rejection yields the 0.3.1 key list in the 0.3.1 order,
197
+ and `civic:failed: false` is never emitted — a producer that stated "not
198
+ failed" and one that stated nothing must read the same, which is what makes a
199
+ marker that IS present mean something. A rejected span carries no
200
+ `tool.duration_ms` from the reference producer today
201
+ ([civic-ai-tools-website#413](https://github.com/npstorey/civic-ai-tools-website/issues/413)),
202
+ so `civic:durationMs` does not fire beside the two new keys.
203
+
204
+ **A call the record states as failed asserts no access**
205
+ ([civic-ai-tools#192](https://github.com/npstorey/civic-ai-tools/issues/192),
206
+ Wave N10 P-H1). Additive: no existing export is removed, no existing caller
207
+ changes, and every golden byte — both vocabulary eras, all eight
208
+ golden-reproduction cases — is unchanged.
209
+
210
+ - **`ToolCallSummary` can see the rejection.** Two new OPTIONAL fields:
211
+ `failed?: boolean`, the producer's assertion that the source rejected this
212
+ call, and `failureKind?: string`, the producer's own open label for why.
213
+ The harness never interprets the label, and `buildDataSources` never reads
214
+ it — `failed` is the assertion, `failureKind` only a label on one. A
215
+ summary carrying neither is exactly the 0.3.1 shape, and absence means
216
+ "not recorded as failed", never "succeeded".
217
+ - **`buildDataSources` mints nothing from a rejected call.** A call whose
218
+ summary carries `failed: true` contributes no dataset-keyed entry for the
219
+ dataset it never read, and marks no aggregate source accessed. Previously
220
+ the population could not see the failure at all, so a rejected call minted
221
+ its dataset's entry and, on an aggregate source, marked that source
222
+ accessed at a timestamp — inside the bytes a publisher signs. The rejected
223
+ call keeps its POSITION in the walk (calls pair to spans by index), and it
224
+ remains on the PROV-O graph's tool-call activities and in the caller's own
225
+ `queries[]`. A source with any non-rejected call is still accessed; a
226
+ dataset a successful call also read still gets its entry.
227
+ - **The two inert inputs are marked deprecated, not removed.**
228
+ `buildDataSources`'s `fallbackPortal` — accepted and not consulted since
229
+ 0.3.1 — now also accepts `undefined`, so a caller that has stopped
230
+ consulting it can stop supplying a value; it stays third of five positional
231
+ parameters, because dropping a positional parameter is breaking.
232
+ `ProvenanceInput.portal` becomes optional and carries `@deprecated`;
233
+ removing it outright would make the reference app's object literal an
234
+ excess-property error. Both removals wait for a major.
235
+ - **Byte consequence.** A package built from a record that states no failure
236
+ is byte-identical to one built by 0.3.1 — the walk reaches the new branch
237
+ only when a summary carries `failed: true`. A package built from a record
238
+ that DOES state a failure loses the `dataSources` entries that failure
239
+ never earned; that is the defect being fixed, and the wave re-emits
240
+ nothing.
241
+ - **Type-level gate.** `src/capture/data-sources.assert.ts` pins the shape at
242
+ compile time. `tsconfig.json` excludes `src/**/*.test.ts`, so the suite
243
+ type-checks nothing — a test can drive a field the type does not have and
244
+ `npm run typecheck` stays green.
245
+
6
246
  ## 0.3.1 — 2026-09-02
7
247
 
8
248
  **The graph states what the span carried, and states absence as absence**
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ // Type-level acceptance gate for the 0.4.0 additive change (civic-ai-tools#192):
2
+ // `ToolCallSummary` can see a rejection the producer recorded, and BOTH new
3
+ // fields are OPTIONAL — a caller that records no outcome passes neither and
4
+ // compiles exactly as it did at 0.3.1.
5
+ //
6
+ // Why a file rather than a test: `tsconfig.json` EXCLUDES `src/**/*.test.ts`,
7
+ // so the suite type-checks nothing. A test can drive a field the type does not
8
+ // have and `npm run typecheck` stays green — which is exactly what happened
9
+ // while this phase's red instrument was the only thing pinning the shape. The
10
+ // type is pinned here instead, mirroring `src/required-config.assert.ts`.
11
+ //
12
+ // Compile-time only: nothing here is exported, and the guard is never invoked.
13
+ import { buildDataSources } from "./data-sources.js";
14
+ function pinsToolCallSummaryShape() {
15
+ // The 0.3.1 shape is still a ToolCallSummary — both new fields are optional.
16
+ const unrecorded = { name: 'get_data', args: {} };
17
+ // A recorded outcome is expressible, with or without a producer label.
18
+ const rejected = { name: 'get_data', args: {}, failed: true };
19
+ const succeeded = { name: 'get_data', args: {}, failed: false };
20
+ const labelled = {
21
+ name: 'get_data',
22
+ args: {},
23
+ failed: true,
24
+ failureKind: 'unavailable',
25
+ };
26
+ // @ts-expect-error #192 — `failed` is the producer's boolean assertion, never a string
27
+ const wrongFailedType = { name: 'get_data', args: {}, failed: 'yes' };
28
+ // @ts-expect-error #192 — `failureKind` is an open producer label (string), never a flag
29
+ const wrongKindType = { name: 'get_data', args: {}, failureKind: true };
30
+ // `fallbackPortal` stays positional third of five for callers that still
31
+ // pass it, and since 0.4.0 accepts `undefined` from callers that do not.
32
+ buildDataSources([unrecorded, rejected, succeeded, labelled], trace, 'portal.example', 'now');
33
+ buildDataSources([unrecorded], trace, undefined, 'now');
34
+ void wrongFailedType;
35
+ void wrongKindType;
36
+ }
37
+ void pinsToolCallSummaryShape;
@@ -6,6 +6,22 @@ export type { DataSourceEntry };
6
6
  export interface ToolCallSummary {
7
7
  name: string;
8
8
  args: Record<string, unknown>;
9
+ /** Did the producer record this call as REJECTED by the source? A call
10
+ * recorded as failed asserts no access: it contributes no dataset-keyed
11
+ * entry and marks no aggregate source accessed (see `buildDataSources`).
12
+ *
13
+ * Optional, and absent is absent: a producer that records no outcome
14
+ * passes neither this nor `failureKind`, and gets exactly the entries it
15
+ * got before the fields existed. Absence means "not recorded as failed",
16
+ * never "succeeded". Added 0.4.0. */
17
+ failed?: boolean;
18
+ /** The producer's own label for why the call was rejected, carried so a
19
+ * caller can hand its record through unchanged. An open string with no
20
+ * normative vocabulary: the harness never interprets it, and this module
21
+ * never reads it. `failed` is the assertion and `failureKind` only a label
22
+ * on one — a summary carrying a kind but no `failed` is not treated as a
23
+ * rejection. Added 0.4.0. */
24
+ failureKind?: string;
9
25
  }
10
26
  interface TraceSpan {
11
27
  name: string;
@@ -57,8 +73,19 @@ export declare function resolveToolSource(toolCall: ToolCallSummary, span: Trace
57
73
  * dataset-keyed entries (first-seen order), then aggregate sources in
58
74
  * registry insertion order — matching the reference implementation.
59
75
  *
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.
76
+ * A call the producer recorded as FAILED (`ToolCallSummary.failed`) asserts
77
+ * no access and contributes nothing on either branch: no dataset-keyed entry
78
+ * for a dataset it never read, and no accessed-marking of its aggregate
79
+ * source. It keeps its POSITION in the walk, because calls are paired to
80
+ * spans by index. The call is still on the PROV-O graph's tool-call
81
+ * activities and in the caller's own `queries[]` — what it is not is an
82
+ * assertion, inside signed bytes, that a source was reached at a timestamp.
83
+ *
84
+ * @param fallbackPortal DEPRECATED, and inert since 0.3.1: an entry states
85
+ * the portal the call carried, never the run's. It stays third of five
86
+ * positional parameters so existing callers keep compiling, and since 0.4.0
87
+ * it also accepts `undefined`, which is what a caller that has stopped
88
+ * consulting it should pass. Dropping it is a breaking change and waits for
89
+ * a major.
63
90
  */
64
- export declare function buildDataSources(toolCalls: ToolCallSummary[], trace: Record<string, unknown>, fallbackPortal: string, now: string, options?: DataSourceOptions): DataSourceEntry[];
91
+ export declare function buildDataSources(toolCalls: ToolCallSummary[], trace: Record<string, unknown>, fallbackPortal: string | undefined, now: string, options?: DataSourceOptions): DataSourceEntry[];
@@ -61,9 +61,20 @@ export function resolveToolSource(toolCall, span, resolver = civicToolSourceReso
61
61
  * dataset-keyed entries (first-seen order), then aggregate sources in
62
62
  * registry insertion order — matching the reference implementation.
63
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.
64
+ * A call the producer recorded as FAILED (`ToolCallSummary.failed`) asserts
65
+ * no access and contributes nothing on either branch: no dataset-keyed entry
66
+ * for a dataset it never read, and no accessed-marking of its aggregate
67
+ * source. It keeps its POSITION in the walk, because calls are paired to
68
+ * spans by index. The call is still on the PROV-O graph's tool-call
69
+ * activities and in the caller's own `queries[]` — what it is not is an
70
+ * assertion, inside signed bytes, that a source was reached at a timestamp.
71
+ *
72
+ * @param fallbackPortal DEPRECATED, and inert since 0.3.1: an entry states
73
+ * the portal the call carried, never the run's. It stays third of five
74
+ * positional parameters so existing callers keep compiling, and since 0.4.0
75
+ * it also accepts `undefined`, which is what a caller that has stopped
76
+ * consulting it should pass. Dropping it is a breaking change and waits for
77
+ * a major.
67
78
  */
68
79
  export function buildDataSources(toolCalls, trace, fallbackPortal, now, options = {}) {
69
80
  const resolver = options.resolver ?? civicToolSourceResolver;
@@ -74,6 +85,12 @@ export function buildDataSources(toolCalls, trace, fallbackPortal, now, options
74
85
  const aggregateAccessed = new Set();
75
86
  for (let i = 0; i < toolCalls.length; i++) {
76
87
  const tc = toolCalls[i];
88
+ // A call the producer recorded as rejected reached no data, so it mints
89
+ // no entry on either branch below. The walk keeps its index rather than
90
+ // filtering the list: `resolveToolSource` pairs a call to `toolSpans[i]`,
91
+ // and a filtered list would shift every later call onto the wrong span.
92
+ if (tc.failed)
93
+ continue;
77
94
  const source = resolveToolSource(tc, toolSpans[i], resolver, fallbackSourceId);
78
95
  if (isDatasetKeyedSource(source, registry)) {
79
96
  const datasetId = tc.args.dataset_id;
@@ -18,10 +18,14 @@ export interface ProvenanceInput {
18
18
  /** The run's selected portal. Accepted and NOT consulted by the graph
19
19
  * builder since 0.3.1: the graph states the portal a tool span carried
20
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. */
24
- portal: string;
21
+ * substitutes the run's portal for one the call did not address.
22
+ *
23
+ * @deprecated Inert since 0.3.1 and optional since 0.4.0 — a caller that
24
+ * has stopped consulting it should stop passing it. The field stays in
25
+ * the type so callers that do pass it (the reference app does, as an
26
+ * object literal) keep compiling; removing it outright would make that
27
+ * literal an excess-property error, so removal waits for a major. */
28
+ portal?: string;
25
29
  }
26
30
  /** Instance configuration for the graph build (config-not-constants). */
27
31
  export interface ProvenanceConfig {
@@ -17,7 +17,7 @@
17
17
  // order is preserved throughout — the legacy hash chain's byte contract).
18
18
  import { makeProvGraph, makeEntityNode, makeActivityNode, makeAgentNode, provRef, provUsed, provWasGeneratedBy, provWasDerivedFrom, provWasAssociatedWith, xsdDateTime, } from '@typedstandards/produce-core';
19
19
  import { hash } from "./trace.js";
20
- import { CIVIC_VOCABULARY, CIVICAITOOLS_PLATFORM_AGENT, } from "../format/vocabulary.js";
20
+ import { CIVIC_VOCABULARY, CIVICAITOOLS_PLATFORM_AGENT, CIVIC_TERM_FAILED, CIVIC_TERM_FAILURE_KIND, } 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
23
  * the reference app — never applied as a default, and never spread into
@@ -33,6 +33,30 @@ function getAttr(attrs, key) {
33
33
  const attr = attrs.find(a => a.key === key);
34
34
  return attr?.value?.stringValue ?? attr?.value?.intValue ?? undefined;
35
35
  }
36
+ /**
37
+ * Read an attribute the producer wrote as a BOOLEAN.
38
+ *
39
+ * `getAttr` above returns `stringValue ?? intValue` and cannot see the third
40
+ * OTel value shape at all — `TraceBuilder` encodes a boolean as
41
+ * `{ boolValue }` (trace.ts), so `getAttr(attrs, 'error')` returns `undefined`
42
+ * for a span the producer really did end with `error: true`. This is a
43
+ * separate reader rather than a widening of `getAttr` on purpose: the nine
44
+ * attributes `getAttr` serves are strings by contract and feed hashes,
45
+ * descriptions and `Number()`, and teaching it a fourth return shape would
46
+ * change what all nine yield for value shapes nobody has measured.
47
+ *
48
+ * The comparison is strict, and only `true` counts. A truthiness test over
49
+ * `stringValue` would read the string `"false"` as an assertion of failure —
50
+ * marking a call that ANSWERED as refused, inside bytes a publisher signs.
51
+ * The cost of the strictness is that a producer encoding this attribute as a
52
+ * string gets no marker; the reference producer does not, and under-reading a
53
+ * foreign encoding leaves the graph silent, where over-reading one would make
54
+ * it lie.
55
+ */
56
+ function getBoolAttr(attrs, key) {
57
+ const attr = attrs.find(a => a.key === key);
58
+ return typeof attr?.value?.boolValue === 'boolean' ? attr.value.boolValue : undefined;
59
+ }
36
60
  function nanoToIso(nano) {
37
61
  const ms = Math.floor(Number(nano) / 1_000_000);
38
62
  return new Date(ms).toISOString();
@@ -267,6 +291,27 @@ export function buildProvenanceGraph(trace, input, config) {
267
291
  // the call. In multi-source analyses each call may target a different
268
292
  // agent (e.g. socrata for `get_data`, data-commons for `get_observations`).
269
293
  const durationMs = getAttr(span.attributes, 'tool.duration_ms');
294
+ // Did the SOURCE REFUSE this call? Before 0.4.0 the graph never asked, so
295
+ // a refused call and one that answered were the same node with the same
296
+ // description — the absence of a data-response entity was the only trace
297
+ // of the rejection, and absence stated as nothing is not absence stated.
298
+ //
299
+ // `error` is the ASSERTION and `error.kind` only a LABEL on one. A span
300
+ // carrying a kind and no assertion is not a rejection, so the kind is read
301
+ // and stated only inside the failure branch — the posture
302
+ // `ToolCallSummary.failed`/`failureKind` already takes on the other input.
303
+ // `error.kind` is the one attribute name across this package and the
304
+ // reference producer (Wave N10 D5); neither side invents a second.
305
+ //
306
+ // The kind is the ONLY cause this graph will ever state. The producer
307
+ // stopped writing a rejection's raw text onto the span in the same wave —
308
+ // that text is authored by the source, can name a host, a port or a stack
309
+ // frame, and the trace travels inline inside the bytes an instance signs.
310
+ // Reading it here would put it back one layer up; the classified vocabulary
311
+ // widens instead. Nor does the description change: it states what the call
312
+ // WAS, and the marker below states how it ended.
313
+ const failed = getBoolAttr(span.attributes, 'error');
314
+ const failureKind = failed === true ? getAttr(span.attributes, 'error.kind') : undefined;
270
315
  graph.push(makeActivityNode(toolCallUrn, {
271
316
  // Names the tool only when the span did.
272
317
  'dcterms:description': toolName
@@ -282,6 +327,17 @@ export function buildProvenanceGraph(trace, input, config) {
282
327
  ? { 'prov:endedAtTime': xsdDateTime(nanoToIso(span.endTimeUnixNano)) }
283
328
  : {}),
284
329
  ...(durationMs ? { 'civic:durationMs': Number(durationMs) } : {}),
330
+ // Appended LAST and spread conditionally, exactly as `civic:durationMs`
331
+ // above is: a span that recorded no rejection yields the key list it
332
+ // yielded at 0.3.1, in the same order, and property insertion order is
333
+ // the legacy chain's byte contract. `false` is never emitted — a
334
+ // producer that stated "not failed" and one that stated nothing must
335
+ // read the same, which is what makes a marker that IS present mean
336
+ // something. A rejected span carries no `tool.duration_ms` from the
337
+ // reference producer today (civic-ai-tools-website#413), so
338
+ // `civic:durationMs` simply does not fire beside these two.
339
+ ...(failed === true ? { [CIVIC_TERM_FAILED]: true } : {}),
340
+ ...(failureKind ? { [CIVIC_TERM_FAILURE_KIND]: failureKind } : {}),
285
341
  }));
286
342
  }
287
343
  // --- Final output relationships ---
@@ -11,6 +11,30 @@ export declare const PRIOR_ERA_CIVIC_NS = "https://civicaitools.org/ns/evidence/
11
11
  /** The prior-era id-scheme root, frozen inside records signed before the
12
12
  * 2026-08-19 settlement. Never emitted for new packages. */
13
13
  export declare const PRIOR_ERA_CIVIC_URN_PREFIX = "urn:civic-evidence";
14
+ /**
15
+ * `civic:failed` — a tool-call activity whose call the SOURCE REFUSED.
16
+ *
17
+ * Emitted with the boolean `true` and only when the producer stated the
18
+ * rejection; never emitted as `false`. A literal `false` would assert an
19
+ * outcome, and "recorded as not-failed" must stay indistinguishable from
20
+ * "nothing recorded" — the same absent-is-absent posture `ToolCallSummary`
21
+ * takes for `failed`. A reader that finds no key learns that the record
22
+ * states nothing, which is the truth about every package minted before 0.4.0.
23
+ */
24
+ export declare const CIVIC_TERM_FAILED = "civic:failed";
25
+ /**
26
+ * `civic:failureKind` — the producer's own classified label for WHY a call
27
+ * carrying {@link CIVIC_TERM_FAILED} was refused.
28
+ *
29
+ * An open string with no normative vocabulary here: the harness states the
30
+ * label verbatim and never interprets or re-derives it. It is a label on an
31
+ * assertion, not the assertion — a record that carries a kind and no failure
32
+ * is not a rejection, and the term is emitted only alongside
33
+ * {@link CIVIC_TERM_FAILED}. The reference producer's four values are
34
+ * `timeout`, `unavailable`, `not_configured` and `unknown`; a producer with a
35
+ * wider vocabulary widens this field rather than putting prose in it.
36
+ */
37
+ export declare const CIVIC_TERM_FAILURE_KIND = "civic:failureKind";
14
38
  /**
15
39
  * One era of the civic vocabulary: the two literals plus the emitters bound
16
40
  * to them. The vocabulary is not deployment configuration (a deployment does
@@ -32,6 +32,45 @@ export const PRIOR_ERA_CIVIC_NS = 'https://civicaitools.org/ns/evidence/';
32
32
  /** The prior-era id-scheme root, frozen inside records signed before the
33
33
  * 2026-08-19 settlement. Never emitted for new packages. */
34
34
  export const PRIOR_ERA_CIVIC_URN_PREFIX = 'urn:civic-evidence';
35
+ // --- Term names ---
36
+ //
37
+ // A `civic:` property name is vocabulary as much as the namespace it hangs
38
+ // under: it is the word a reader of a signed graph interprets, so it is
39
+ // declared here and imported by the builder that emits it, never spelled as a
40
+ // literal inside capture/ (`purity.test.ts` enforces that for the terms below).
41
+ // Both terms are era-independent — the settlement moved the NAMESPACE, not the
42
+ // property names — so they carry no era qualifier and are minted for the first
43
+ // time after it.
44
+ //
45
+ // The settlement-era property names that predate Wave N10 (`civic:sourceId`,
46
+ // `civic:durationMs`, `civic:datasetId` and their siblings) are still inline
47
+ // literals in the capture-side builder. That is a real inconsistency, recorded
48
+ // rather than fixed in passing: moving them is a byte-sensitive change across
49
+ // every golden fixture and belongs to a phase of its own.
50
+ /**
51
+ * `civic:failed` — a tool-call activity whose call the SOURCE REFUSED.
52
+ *
53
+ * Emitted with the boolean `true` and only when the producer stated the
54
+ * rejection; never emitted as `false`. A literal `false` would assert an
55
+ * outcome, and "recorded as not-failed" must stay indistinguishable from
56
+ * "nothing recorded" — the same absent-is-absent posture `ToolCallSummary`
57
+ * takes for `failed`. A reader that finds no key learns that the record
58
+ * states nothing, which is the truth about every package minted before 0.4.0.
59
+ */
60
+ export const CIVIC_TERM_FAILED = 'civic:failed';
61
+ /**
62
+ * `civic:failureKind` — the producer's own classified label for WHY a call
63
+ * carrying {@link CIVIC_TERM_FAILED} was refused.
64
+ *
65
+ * An open string with no normative vocabulary here: the harness states the
66
+ * label verbatim and never interprets or re-derives it. It is a label on an
67
+ * assertion, not the assertion — a record that carries a kind and no failure
68
+ * is not a rejection, and the term is emitted only alongside
69
+ * {@link CIVIC_TERM_FAILED}. The reference producer's four values are
70
+ * `timeout`, `unavailable`, `not_configured` and `unknown`; a producer with a
71
+ * wider vocabulary widens this field rather than putting prose in it.
72
+ */
73
+ export const CIVIC_TERM_FAILURE_KIND = 'civic:failureKind';
35
74
  /** Bind the emitters to one era's two literals. */
36
75
  export function makeCivicVocabulary(ns, urnPrefix) {
37
76
  return {
@@ -6,7 +6,10 @@ export declare const EVALUATION_CRITERIA: readonly ["dataSourceIdentification",
6
6
  export declare const EVALUATION_RUBRIC = "You are an independent evaluator assessing an AI-generated civic data analysis.\n\nYou will receive:\n1. The original prompt (question asked)\n2. The tool calls made (MCP queries to Socrata open data portals)\n3. The AI's final output/analysis\n\nEvaluate the analysis against these 6 criteria, scoring each 1-10:\n\na) **Data Source Identification** (1-10): Does the output correctly identify the data source(s) and time period? Are dataset IDs, portal domains, and date ranges accurate?\n\nb) **Quantitative Claim Support** (1-10): Are the quantitative claims (numbers, percentages, rankings) supported by the data returned in the tool calls? Cross-check key figures against the raw data.\n\nc) **Confounders and Bias** (1-10): Does the analysis acknowledge obvious confounders, selection biases, or framing issues? Are there lurking variables or cherry-picked timeframes?\n\nd) **Geographic Scope** (1-10): Is the geographic scope appropriate for the question? Does the analysis avoid over-generalizing from one jurisdiction?\n\ne) **Limitations Noted** (1-10): Are limitations and caveats noted? Does the analysis flag data quality issues, missing fields, or incomplete coverage?\n\nf) **Contradictory Conclusion** (1-10): Could the same data reasonably support a contradictory conclusion? Does the analysis consider alternative interpretations?\n\nRespond in this exact JSON format (no markdown fences, just raw JSON):\n{\n \"dataSourceIdentification\": { \"score\": <1-10>, \"comment\": \"<1-2 sentences>\" },\n \"quantitativeClaimSupport\": { \"score\": <1-10>, \"comment\": \"<1-2 sentences>\" },\n \"confoundersAndBias\": { \"score\": <1-10>, \"comment\": \"<1-2 sentences>\" },\n \"geographicScope\": { \"score\": <1-10>, \"comment\": \"<1-2 sentences>\" },\n \"limitationsNoted\": { \"score\": <1-10>, \"comment\": \"<1-2 sentences>\" },\n \"contradictoryConclusion\": { \"score\": <1-10>, \"comment\": \"<1-2 sentences>\" },\n \"overallScore\": <average of all 6 scores, one decimal>,\n \"assessment\": \"<2-4 sentence overall assessment>\"\n}";
7
7
  /** The methodology's `promptSetVersion`: SHA-256 of the rubric text, computed
8
8
  * once at module load. Any wording change to the rubric produces a new
9
- * version, so an attestation pins the exact prompt set that scored it. */
9
+ * version, so an attestation pins the rubric WORDING that scored it — and
10
+ * that wording only. The prompt template around it (`buildEvaluationPrompt`)
11
+ * is outside this hash, so one value does not identify one set of evaluator
12
+ * inputs (civic-ai-tools#207). */
10
13
  export declare const RUBRIC_VERSION_SHA256: string;
11
14
  /** Build the user-turn evaluation content from a package's signed fields.
12
15
  * Takes `RecordPackage` — produce-core 0.3.0's settlement-era name for the
@@ -5,8 +5,14 @@
5
5
  // brief §1. The model runner and DB/blob emission (`adversarial-eval.ts`)
6
6
  // stay app-side — this module never performs I/O.
7
7
  //
8
- // ⚠ RUBRIC_VERSION_SHA256 is a Q26-pinned version hash: attestations pin the
9
- // exact prompt set that scored them by this value. The rubric text below is
8
+ // ⚠ RUBRIC_VERSION_SHA256 is a Q26-pinned version hash over THE RUBRIC TEXT
9
+ // ALONE (`sha256Hex(EVALUATION_RUBRIC)`). It does NOT cover
10
+ // `buildEvaluationPrompt` below — the template that assembles the rubric with
11
+ // the package's tool calls, data sources and model into the turn the evaluator
12
+ // actually reads. So two evaluations either side of a change to that template
13
+ // carry the SAME `methodology.promptSetVersion` while the evaluator saw
14
+ // different inputs; civic-ai-tools#207 files that gap for the specification to
15
+ // settle, and this module does not resolve it. The rubric text below is
10
16
  // byte-exact with the reference implementation (the relocation changes no
11
17
  // hashes), and the test suite asserts the literal digest. Any wording change
12
18
  // to EVALUATION_RUBRIC is a NEW rubric version — never edit the text without
@@ -57,8 +63,24 @@ Respond in this exact JSON format (no markdown fences, just raw JSON):
57
63
  }`;
58
64
  /** The methodology's `promptSetVersion`: SHA-256 of the rubric text, computed
59
65
  * once at module load. Any wording change to the rubric produces a new
60
- * version, so an attestation pins the exact prompt set that scored it. */
66
+ * version, so an attestation pins the rubric WORDING that scored it — and
67
+ * that wording only. The prompt template around it (`buildEvaluationPrompt`)
68
+ * is outside this hash, so one value does not identify one set of evaluator
69
+ * inputs (civic-ai-tools#207). */
61
70
  export const RUBRIC_VERSION_SHA256 = sha256Hex(EVALUATION_RUBRIC);
71
+ /** Did the producer record this entry as REJECTED by the source? Only a
72
+ * literal `failed: true` is a rejection (see `RecordedCallOutcome`). */
73
+ function wasRecordedRejected(query) {
74
+ return query.failed === true;
75
+ }
76
+ /** What the evaluator is told about a call the source rejected. A FIXED
77
+ * string: it interpolates nothing the producer wrote, and it claims no row
78
+ * count — there were no rows to count, which is neither "zero rows" nor "an
79
+ * unknown number of rows". The rubric asks the evaluator to cross-check
80
+ * figures against the data returned in the tool calls, so a rejected call
81
+ * reading `→ ? rows` invited it to treat absent data as merely unrecorded
82
+ * (civic-ai-tools#203). */
83
+ const REJECTED_CALL_RENDERING = 'REJECTED by the source — no data was returned, and no row count is claimed';
62
84
  /** Build the user-turn evaluation content from a package's signed fields.
63
85
  * Takes `RecordPackage` — produce-core 0.3.0's settlement-era name for the
64
86
  * same object (spec Appendix J, migration class alias-and-deprecate). The
@@ -70,7 +92,10 @@ export function buildEvaluationPrompt(pkg) {
70
92
  const argStr = Object.entries(q.arguments)
71
93
  .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
72
94
  .join(', ');
73
- return ` ${i + 1}. ${q.tool}(${argStr}) → ${q.resultRows ?? '?'} rows`;
95
+ const outcome = wasRecordedRejected(q)
96
+ ? REJECTED_CALL_RENDERING
97
+ : `${q.resultRows ?? '?'} rows`;
98
+ return ` ${i + 1}. ${q.tool}(${argStr}) → ${outcome}`;
74
99
  })
75
100
  .join('\n');
76
101
  const dataSources = pkg.dataSources
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typedstandards/civic-typed-harness",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",