@typedstandards/civic-typed-harness 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,82 @@
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.9.0 — 2026-10-18
7
+
8
+ The reader's states and the flows-statement reference (sprint R6, H2;
9
+ [ADR-0033](../../docs/adr/0033-composed-harness-record-fields.md)'s
10
+ 2026-10-18 amendment, ruling D2 = A of
11
+ [civic-ai-tools-website#632](https://github.com/npstorey/civic-ai-tools-website/issues/632)),
12
+ and the evaluator prompt's aggregate sources
13
+ ([civic-ai-tools#265](https://github.com/npstorey/civic-ai-tools/issues/265)).
14
+ Signed bytes move only on a new record that passes a new reason or a
15
+ `flowsStatement`; a record packaged without them has the same bytes, and no
16
+ golden fixture moves.
17
+
18
+ - **Two `sourcesOffered` reasons.** `SourceOfferedReason` and
19
+ `SOURCE_OFFERED_REASONS` gain `reader-off` (the source was configured, and
20
+ the reader turned it off) and `not-consented` (the source was configured,
21
+ and the reader has given no consent for it), after the four of ADR-0033 §E
22
+ in §E's order. A source was offered exactly when its reason is `configured`
23
+ or `selected`, as before. The derivation's refusal message lists the six.
24
+ - **`flowsStatement` in the composition block.** `CompositionInput`,
25
+ `DatHerePolicyInput` and `ExtractedRunAttributes` gain an optional
26
+ `flowsStatement: { url, hash, date }`, typed as the new
27
+ `FlowsStatementInput` on the two inputs. `extractRunAttributes` reads it
28
+ from the root-span attribute `analysis.flows_statement`, JSON text of one
29
+ object whose fields are exactly the three strings, and refuses anything
30
+ else with `RunAttributeError`. The derivation states it as the block's last
31
+ key, keys in the order `url`, `hash`, `date`, and refuses with
32
+ `RecordFieldError` a `url` that is not an absolute `http:` or `https:` URL
33
+ without credentials or whitespace, a `hash` that is not lowercase hex
34
+ SHA-256, a `date` that is not a calendar date, and any other key. The
35
+ record refers to the statement by address, hash and date, never by content
36
+ (ADR-0036 §D). Absent, the key is absent.
37
+ - **The evaluator prompt names an aggregate source.** `buildEvaluationPrompt`
38
+ rendered every `dataSources` entry by its `datasetUrl`, and an aggregate
39
+ entry (Data Commons, Boston, a hosted source) carries none, so the
40
+ evaluator read `- undefined (accessed …)` for it. An entry without a
41
+ `datasetUrl` now reads `- <sourceId> (<catalogType>, <portalUrl>) (accessed
42
+ <time>)`, with `dataset <datasetId>` added when it carries one; a missing
43
+ field is left out or stated in fixed words, so no line reads `undefined`.
44
+ An entry with a `datasetUrl` reads exactly as before. Measured across the
45
+ release boundary, as 0.4.1 was: the published 0.8.0 and this build render
46
+ a dataset-only package and an empty one byte-identically, and render a
47
+ package with two aggregate entries differently.
48
+ - **`RUBRIC_VERSION_SHA256` does not move:**
49
+ `b62b193c992fd430cc82dff9a80a057bbe0b2b656fbdace450ca1a5d99fffa02` in the
50
+ published 0.8.0 and in this build. It is the SHA-256 of the rubric text
51
+ alone, and the prompt template is outside it, so an evaluation either side
52
+ of this release carries the same `promptSetVersion` while the evaluator of
53
+ a record with an aggregate entry is shown different input.
54
+ [civic-ai-tools#207](https://github.com/npstorey/civic-ai-tools/issues/207),
55
+ that gap, stays open.
56
+ - No dependency changes.
57
+
58
+ ## 0.8.0 — 2026-10-16
59
+
60
+ The dependency floor (sprint LAYOUT-WEB, F0).
61
+
62
+ - **Dependency floor raised:** `@typedstandards/produce-core` `^0.8.0` →
63
+ `^0.9.0`, and the `@typedstandards/verify-core` devDependency `^0.13.0` →
64
+ `^0.14.0`, the version produce-core 0.9.0 resolves. Under 0.x semver
65
+ `^0.8.0` excludes 0.9.0, so a consumer that pins produce-core `^0.9.0`
66
+ would install a second copy of each core beside this package's.
67
+ - **What the two releases bring.** produce-core 0.9.0 adds `buildLayoutNode`,
68
+ which builds a `content/layout/v1` node, with `CONTENT_LAYOUT` and five
69
+ types, and an optional `layoutNodeId` on `attestation/publishes/v1`,
70
+ emitted only when supplied; an attestation built without it has the same
71
+ bytes as one built by 0.8.0. verify-core 0.14.0 registers
72
+ `content/layout/v1` (check #12 reads `ok` for a layout node) and adds
73
+ `parseStrictJson` and `RepeatedMemberNameError`. The harness reads none of
74
+ these.
75
+ - The lockfile resolves produce-core 0.9.0 and verify-core 0.14.0, one copy
76
+ each, and no other entry moves. The golden byte-compat suite passes
77
+ unchanged.
78
+ - A consumer that pins produce-core 0.8.x no longer shares its copy with this
79
+ package.
80
+ - **README:** the runtime-dependency line names `^0.9.0`.
81
+
6
82
  ## 0.7.0 — 2026-10-15
7
83
 
8
84
  - **The seam inside the harness** ([civic-ai-tools#244](https://github.com/npstorey/civic-ai-tools/issues/244) P2).
package/README.md CHANGED
@@ -46,7 +46,7 @@ const graph = buildProvenanceGraph(trace, input, CIVICAITOOLS_PROVENANCE_CONFIG)
46
46
 
47
47
  Likewise: `buildDatHereEnvironment` / `deriveDatHereEnvelopeFields` take `CIVICAITOOLS_ENVIRONMENT_CONFIG`, `new TraceBuilder(...)` takes `CIVICAITOOLS_TRACE_CONFIG`, and `isDatasetKeyedSource` / `displayNameForSource` / `formatDataSourcesSummary` take `CIVIC_SOURCE_REGISTRY`. The acceptance condition is encoded in `src/required-config.assert.ts`: reintroducing any of the removed defaults fails `npm run typecheck`.
48
48
 
49
- ## What a record states about its run (0.7.0, [ADR-0033](../../docs/adr/0033-composed-harness-record-fields.md))
49
+ ## What a record states about its run (0.7.0 and 0.9.0, [ADR-0033](../../docs/adr/0033-composed-harness-record-fields.md))
50
50
 
51
51
  **Every call is attributed to its own source.** A tool span's source is its `mcp.source` tag; an untagged span resolves by its tool name through the resolver (`ProvenanceConfig.resolver`, default `civicToolSourceResolver`); a span neither identifies is attributed to `UNKNOWN_SOURCE_ID` (`'unknown'`), never to Socrata. The same order holds in `dataSources[]`, where an unknown call mints no entry. A caller may still pass `fallbackSourceId` explicitly, for example `'socrata'` to reproduce an earlier record of an untagged span.
52
52
 
@@ -63,6 +63,7 @@ Likewise: `buildDatHereEnvironment` / `deriveDatHereEnvelopeFields` take `CIVICA
63
63
  | `analysis.tool_definitions` | `toolDefinitions` | `org.civicaitools.environment.toolDefinitions` |
64
64
  | `analysis.sources_offered` | `sourcesOffered` | `org.civicaitools.composition.sourcesOffered` |
65
65
  | `analysis.components` | `components` | `org.civicaitools.composition.components` |
66
+ | `analysis.flows_statement` (one object, `{ url, hash, date }`) | `flowsStatement` | `org.civicaitools.composition.flowsStatement`, the block's last key |
66
67
 
67
68
  ```ts
68
69
  const fields = deriveDatHereEnvelopeFields(
@@ -71,13 +72,17 @@ const fields = deriveDatHereEnvelopeFields(
71
72
  );
72
73
  ```
73
74
 
74
- An absent attribute is an absent field. A present attribute that does not parse is refused (`RunAttributeError`), and a value outside the record's form — a date that is not a calendar date, a reason outside `configured` / `not-configured` / `selected` / `not-selected`, a guidance hash that is not lowercase hex SHA-256, an origin outside `endpoint` / `inline` / `fallback` — is refused by the derivation (`RecordFieldError`), never dropped. The `org.civicaitools.composition` block is emitted under every content profile, appended after the other extensions, and only when one of its fields is passed: a package built with none of these inputs has 0.6.0's extensions bytes, except that a datHere environment block no longer states the two placeholders.
75
+ An absent attribute is an absent field. A present attribute that does not parse is refused (`RunAttributeError`), and a value outside the record's form — a date that is not a calendar date, a reason outside `configured` / `not-configured` / `selected` / `not-selected` / `reader-off` / `not-consented`, a guidance hash that is not lowercase hex SHA-256, an origin outside `endpoint` / `inline` / `fallback`, a flows statement that is not exactly an absolute `http:` or `https:` `url` with no credentials, a lowercase hex SHA-256 `hash` and a calendar `date` — is refused by the derivation (`RecordFieldError`), never dropped. The `org.civicaitools.composition` block is emitted under every content profile, appended after the other extensions, and only when one of its fields is passed: a package built with none of these inputs has 0.6.0's extensions bytes, except that a datHere environment block no longer states the two placeholders.
76
+
77
+ **The reader's states and the flows statement (0.9.0, ADR-0033's 2026-10-18 amendment).** Two reasons follow the four: `reader-off` (the source was configured, and the reader turned it off) and `not-consented` (the source was configured, and the reader has given no consent for it). A source was offered exactly when its reason is `configured` or `selected`. `flowsStatement` refers to the instance's flows statement by address, hash and date, and the record never carries the statement itself. A record packaged without either has the same bytes.
78
+
79
+ **The evaluator prompt (0.9.0, [civic-ai-tools#265](https://github.com/npstorey/civic-ai-tools/issues/265)).** `buildEvaluationPrompt` lists each `dataSources` entry by what it carries. An entry with a `datasetUrl` reads as before; an aggregate entry, which has none, reads `- <sourceId> (<catalogType>, <portalUrl>) (accessed <time>)`, where 0.8.0 printed `undefined`. `RUBRIC_VERSION_SHA256` does not move: it is the SHA-256 of the rubric text alone, and the prompt template is outside it ([civic-ai-tools#207](https://github.com/npstorey/civic-ai-tools/issues/207), open).
75
80
 
76
81
  ## Purity contract (harness-grade)
77
82
 
78
83
  No I/O, no network, no environment reads, no Node built-ins, browser-safe — anywhere in the package. Clock and RNG are permitted **in capture modules only** (span timestamps and ids are what capture *is*) and are injectable for deterministic tests. Enforced mechanically twice: `eslint.config.mjs` (`no-restricted-imports` / `-globals` / `-properties` / `-syntax`) and `src/purity.test.ts` (browser-safety + determinism + module-boundary checks under `node --test`).
79
84
 
80
- Runtime dependency: **`@typedstandards/produce-core` (`^0.8.0`) only.** The verify-core primitives the harness consumes (`sha256Hex`, `isBlobRef`, the canonicalization-rule URIs, the Q32 vocabulary table, the `CaptureMethod` type) are reached through produce-core's 0.2.0 re-exports rather than imported from `@typedstandards/verify-core` directly — one declared dependency, no phantom, and still one hashing implementation across producer, harness, and verifier by construction (produce-core re-exports the same bindings verify-core defines, verified by reference identity).
85
+ Runtime dependency: **`@typedstandards/produce-core` (`^0.9.0`) only.** The verify-core primitives the harness consumes (`sha256Hex`, `isBlobRef`, the canonicalization-rule URIs, the Q32 vocabulary table, the `CaptureMethod` type) are reached through produce-core's 0.2.0 re-exports rather than imported from `@typedstandards/verify-core` directly — one declared dependency, no phantom, and still one hashing implementation across producer, harness, and verifier by construction (produce-core re-exports the same bindings verify-core defines, verified by reference identity).
81
86
 
82
87
  > **Q32 adjacency** (open-questions registry): the captureMethod vocabulary re-exported from `src/format/profiles.ts` is verify-core's hardcoded fallback table, pending versioned, content-addressed guidance bundles. Flagged, not solved, here.
83
88
 
@@ -20,6 +20,11 @@ export interface ExtractedRunAttributes {
20
20
  guidanceHash?: string;
21
21
  guidanceOrigin?: string;
22
22
  }>;
23
+ flowsStatement?: {
24
+ url: string;
25
+ hash: string;
26
+ date: string;
27
+ };
23
28
  }
24
29
  /** A root-span attribute that is present but not the shape ADR-0033 names. */
25
30
  export declare class RunAttributeError extends Error {
@@ -1,7 +1,8 @@
1
1
  // Run-attribute extraction (CAPTURE group) — reads the facts the producer
2
2
  // recorded on the trace's ROOT span at run time (ADR-0033 §D, §E): the date
3
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.
4
+ // sources it was offered, the components that composed its prompt, and the
5
+ // flows statement in force (by reference: address, hash and date).
5
6
  //
6
7
  // WHY THE TRACE (civic-ai-tools-website#495). A record is packaged after the
7
8
  // run, sometimes much later; a fact read at packaging is packaging-time state.
@@ -10,9 +11,11 @@
10
11
  //
11
12
  // SHAPE, NOT POLICY. This module checks that each attribute parses as the
12
13
  // 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.
14
+ // are strings; for `analysis.flows_statement`, of one object of exactly three
15
+ // strings) and returns it under the derivation's input names. The values —
16
+ // the date form, the reason set, the hash form, the origin set, the address
17
+ // form — are policy, checked by the format group's derivation, which refuses
18
+ // a bad one by name.
16
19
  //
17
20
  // An ABSENT attribute is an absent field. A PRESENT attribute that does not
18
21
  // parse is refused with `RunAttributeError`, never read as absent: absence
@@ -72,6 +75,34 @@ function objectArray(attribute, text, required, optional = []) {
72
75
  });
73
76
  return parsed;
74
77
  }
78
+ /** Parse an attribute as JSON text of ONE plain object whose fields are
79
+ * exactly `fields`, each a string: a missing field, a non-string field and
80
+ * an extra field are each refused. The field set is closed because the
81
+ * object lands in the record whole; an extra field would otherwise either
82
+ * reach signed bytes unchecked or be dropped silently. */
83
+ function exactStringObject(attribute, text, fields) {
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(text);
87
+ }
88
+ catch {
89
+ throw new RunAttributeError(attribute, 'is not JSON text');
90
+ }
91
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
92
+ throw new RunAttributeError(attribute, 'is not a JSON object');
93
+ }
94
+ const entry = parsed;
95
+ for (const field of fields) {
96
+ if (typeof entry[field] !== 'string') {
97
+ throw new RunAttributeError(attribute, `has no string ${field}`);
98
+ }
99
+ }
100
+ const extra = Object.keys(entry).filter((k) => !fields.includes(k));
101
+ if (extra.length > 0) {
102
+ throw new RunAttributeError(attribute, `has a field outside ${fields.join(', ')}: ${extra.map((k) => JSON.stringify(k)).join(', ')}`);
103
+ }
104
+ return entry;
105
+ }
75
106
  /**
76
107
  * Read the run attributes from a trace's root span. Returns `{}` for a trace
77
108
  * with no root span — including a BlobRef trace's inspection view
@@ -101,5 +132,10 @@ export function extractRunAttributes(trace) {
101
132
  if (components !== undefined) {
102
133
  out.components = objectArray('analysis.components', components, ['id'], ['version', 'guidanceHash', 'guidanceOrigin']);
103
134
  }
135
+ const flows = stringAttr(root, 'analysis.flows_statement');
136
+ if (flows !== undefined) {
137
+ const { url, hash, date } = exactStringObject('analysis.flows_statement', flows, ['url', 'hash', 'date']);
138
+ out.flowsStatement = { url, hash, date };
139
+ }
104
140
  return out;
105
141
  }
@@ -134,13 +134,19 @@ export interface DatHerePolicyInput {
134
134
  sourcesOffered?: readonly SourceOfferedInput[];
135
135
  /** What composed the prompt (composition block). */
136
136
  components?: readonly CompositionComponentInput[];
137
+ /** The instance's flows statement in force for the run, by reference
138
+ * (composition block; ADR-0033 §E as amended 2026-10-18). */
139
+ flowsStatement?: FlowsStatementInput;
137
140
  /** Caller-supplied extensions to layer the environment extension onto. */
138
141
  extensions?: Record<string, unknown>;
139
142
  }
140
- /** Why a source was or was not offered to the model (ADR-0033 §E). A source
141
- * was offered exactly when its reason is `configured` or `selected`. */
142
- export type SourceOfferedReason = 'configured' | 'not-configured' | 'selected' | 'not-selected';
143
- /** The four reasons, in the order ADR-0033 §E states them. */
143
+ /** Why a source was or was not offered to the model (ADR-0033 §E, amended
144
+ * 2026-10-18). A source was offered exactly when its reason is `configured`
145
+ * or `selected`; `reader-off` and `not-consented` are the reader's states
146
+ * for a configured source that was not offered. */
147
+ export type SourceOfferedReason = 'configured' | 'not-configured' | 'selected' | 'not-selected' | 'reader-off' | 'not-consented';
148
+ /** The six reasons: the four in the order ADR-0033 §E states them, then the
149
+ * two its 2026-10-18 amendment adds. */
144
150
  export declare const SOURCE_OFFERED_REASONS: readonly SourceOfferedReason[];
145
151
  /** One `sourcesOffered` entry as a caller passes it. `reason` is a string so
146
152
  * the trace extractor's output is accepted as is; the derivation refuses a
@@ -167,11 +173,21 @@ export interface CompositionComponentInput {
167
173
  guidanceHash?: string;
168
174
  guidanceOrigin?: string;
169
175
  }
176
+ /** The instance's flows statement, by reference (ADR-0033 §E as amended
177
+ * 2026-10-18; ADR-0036 §D): the address it is served at, the lowercase hex
178
+ * SHA-256 that is its version, and its `YYYY-MM-DD` date. The record states
179
+ * these three and never the statement's content. */
180
+ export interface FlowsStatementInput {
181
+ url: string;
182
+ hash: string;
183
+ date: string;
184
+ }
170
185
  /** The composition block's inputs (ADR-0033 §E). */
171
186
  export interface CompositionInput {
172
187
  assertedDate?: string;
173
188
  sourcesOffered?: readonly SourceOfferedInput[];
174
189
  components?: readonly CompositionComponentInput[];
190
+ flowsStatement?: FlowsStatementInput;
175
191
  }
176
192
  /**
177
193
  * Build the `org.civicaitools.composition` extension content (ADR-0033 §E),
@@ -149,12 +149,15 @@ function checkRunSettings(run) {
149
149
  }
150
150
  return run;
151
151
  }
152
- /** The four reasons, in the order ADR-0033 §E states them. */
152
+ /** The six reasons: the four in the order ADR-0033 §E states them, then the
153
+ * two its 2026-10-18 amendment adds. */
153
154
  export const SOURCE_OFFERED_REASONS = [
154
155
  'configured',
155
156
  'not-configured',
156
157
  'selected',
157
158
  'not-selected',
159
+ 'reader-off',
160
+ 'not-consented',
158
161
  ];
159
162
  /** The three origins. */
160
163
  export const GUIDANCE_ORIGINS = ['endpoint', 'inline', 'fallback'];
@@ -178,6 +181,30 @@ function isCalendarDate(value) {
178
181
  function nonEmptyString(value) {
179
182
  return typeof value === 'string' && value.length > 0;
180
183
  }
184
+ /** The flows statement's keys, in the record's order. */
185
+ const FLOWS_STATEMENT_KEYS = ['url', 'hash', 'date'];
186
+ /** Any whitespace or control character: a URL parser strips or drops these,
187
+ * so a string carrying one is not the address it parses to. */
188
+ const WHITESPACE_OR_CONTROL = /[\s\u0000-\u001f\u007f]/;
189
+ /** An absolute `http:` or `https:` URL, as written (the scheme followed by
190
+ * `//`), with no credentials and no whitespace or control character. `http:`
191
+ * is accepted because a local instance serves its statement from
192
+ * `http://localhost`. */
193
+ function isStatementUrl(value) {
194
+ if (typeof value !== 'string' || !/^https?:\/\//i.test(value) || WHITESPACE_OR_CONTROL.test(value)) {
195
+ return false;
196
+ }
197
+ let parsed;
198
+ try {
199
+ parsed = new URL(value);
200
+ }
201
+ catch {
202
+ return false;
203
+ }
204
+ return (parsed.protocol === 'https:' || parsed.protocol === 'http:')
205
+ && parsed.username === ''
206
+ && parsed.password === '';
207
+ }
181
208
  /**
182
209
  * Build the `org.civicaitools.composition` extension content (ADR-0033 §E),
183
210
  * or `undefined` when no field is passed (the block is then not emitted, and
@@ -244,6 +271,28 @@ export function buildCompositionExtension(input) {
244
271
  };
245
272
  });
246
273
  }
274
+ // The block's last key, so a block built without it keeps its bytes.
275
+ if (input.flowsStatement !== undefined) {
276
+ const statement = input.flowsStatement;
277
+ if (statement === null || typeof statement !== 'object' || Array.isArray(statement)) {
278
+ throw new RecordFieldError('flowsStatement', 'must be an object { url, hash, date }');
279
+ }
280
+ const extra = Object.keys(statement).filter((k) => !FLOWS_STATEMENT_KEYS.includes(k));
281
+ if (extra.length > 0) {
282
+ throw new RecordFieldError('flowsStatement', `carries url, hash and date only, never the statement itself; got ${extra.map((k) => JSON.stringify(k)).join(', ')}`);
283
+ }
284
+ const { url, hash, date } = statement;
285
+ if (!isStatementUrl(url)) {
286
+ throw new RecordFieldError('flowsStatement', `url must be an absolute http: or https: URL with no credentials; got ${JSON.stringify(url)}`);
287
+ }
288
+ if (!(typeof hash === 'string' && SHA256_HEX_FORM.test(hash))) {
289
+ throw new RecordFieldError('flowsStatement', `hash must be a lowercase hex SHA-256; got ${JSON.stringify(hash)}`);
290
+ }
291
+ if (!(typeof date === 'string' && isCalendarDate(date))) {
292
+ throw new RecordFieldError('flowsStatement', `date must be a YYYY-MM-DD calendar date; got ${JSON.stringify(date)}`);
293
+ }
294
+ block.flowsStatement = { url, hash, date };
295
+ }
247
296
  return Object.keys(block).length > 0 ? block : undefined;
248
297
  }
249
298
  /**
@@ -272,6 +321,7 @@ export function deriveDatHereEnvelopeFields(input, config) {
272
321
  assertedDate: input.assertedDate,
273
322
  sourcesOffered: input.sourcesOffered,
274
323
  components: input.components,
324
+ flowsStatement: input.flowsStatement,
275
325
  });
276
326
  if (composition !== undefined) {
277
327
  extensions[COMPOSITION_EXTENSION_KEY] = composition;
@@ -81,6 +81,40 @@ function wasRecordedRejected(query) {
81
81
  * reading `→ ? rows` invited it to treat absent data as merely unrecorded
82
82
  * (civic-ai-tools#203). */
83
83
  const REJECTED_CALL_RENDERING = 'REJECTED by the source — no data was returned, and no row count is claimed';
84
+ /** A non-empty string field of a `dataSources` entry, or `undefined`. The
85
+ * entry is read as stored: a package from an older producer may lack a
86
+ * field the current type requires (the first packager wrote no `sourceId`). */
87
+ function entryText(entry, field) {
88
+ const value = entry[field];
89
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
90
+ }
91
+ /** One `## Data Sources` line (civic-ai-tools#265). An entry carrying a
92
+ * `datasetUrl` renders exactly as before 0.9.0. An entry without one (an
93
+ * aggregate source: its registry row names a portal, not a dataset) renders
94
+ * by what it carries: `sourceId`, then `catalogType`, `portalUrl` and any
95
+ * `datasetId` in parentheses. Every value comes from the entry's own signed
96
+ * fields, and no other producer text is interpolated; a missing one is
97
+ * either left out or stated in this module's fixed words, so no line reads
98
+ * `undefined`. */
99
+ function renderDataSource(ds) {
100
+ const entry = ds;
101
+ // Both interpolations of the 0.8.0 line keep their test (a string, of any
102
+ // length), so every line 0.8.0 rendered from two strings is byte-identical.
103
+ const accessed = typeof entry.accessTimestamp === 'string'
104
+ ? `(accessed ${entry.accessTimestamp})`
105
+ : '(access time not recorded)';
106
+ if (typeof entry.datasetUrl === 'string') {
107
+ return ` - ${entry.datasetUrl} ${accessed}`;
108
+ }
109
+ const sourceId = entryText(entry, 'sourceId') ?? 'source not recorded';
110
+ const datasetId = entryText(entry, 'datasetId');
111
+ const where = [
112
+ entryText(entry, 'catalogType'),
113
+ entryText(entry, 'portalUrl'),
114
+ datasetId !== undefined ? `dataset ${datasetId}` : undefined,
115
+ ].filter((part) => part !== undefined);
116
+ return ` - ${sourceId}${where.length > 0 ? ` (${where.join(', ')})` : ''} ${accessed}`;
117
+ }
84
118
  /** Build the user-turn evaluation content from a package's signed fields.
85
119
  * Takes `RecordPackage` — produce-core 0.3.0's settlement-era name for the
86
120
  * same object (spec Appendix J, migration class alias-and-deprecate). The
@@ -99,7 +133,7 @@ export function buildEvaluationPrompt(pkg) {
99
133
  })
100
134
  .join('\n');
101
135
  const dataSources = pkg.dataSources
102
- .map(ds => ` - ${ds.datasetUrl} (accessed ${ds.accessTimestamp})`)
136
+ .map(renderDataSource)
103
137
  .join('\n');
104
138
  return `## Original Prompt
105
139
  ${pkg.prompt.text || '[prompt text not available]'}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typedstandards/civic-typed-harness",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
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,10 +33,10 @@
33
33
  "prepublishOnly": "npm run clean && npm run build"
34
34
  },
35
35
  "dependencies": {
36
- "@typedstandards/produce-core": "^0.8.0"
36
+ "@typedstandards/produce-core": "^0.9.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@typedstandards/verify-core": "^0.13.0",
39
+ "@typedstandards/verify-core": "^0.14.0",
40
40
  "@types/node": "^22.20.2",
41
41
  "@typescript-eslint/parser": "^8",
42
42
  "eslint": "^9",