@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 +76 -0
- package/README.md +8 -3
- package/dist/capture/run-attributes.d.ts +5 -0
- package/dist/capture/run-attributes.js +40 -4
- package/dist/format/dathere.d.ts +20 -4
- package/dist/format/dathere.js +51 -1
- package/dist/rubric/adversarial-eval-core.js +35 -1
- package/package.json +3 -3
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.
|
|
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
|
|
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
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
}
|
package/dist/format/dathere.d.ts
CHANGED
|
@@ -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
|
|
141
|
-
* was offered exactly when its reason is `configured`
|
|
142
|
-
|
|
143
|
-
|
|
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),
|
package/dist/format/dathere.js
CHANGED
|
@@ -149,12 +149,15 @@ function checkRunSettings(run) {
|
|
|
149
149
|
}
|
|
150
150
|
return run;
|
|
151
151
|
}
|
|
152
|
-
/** The
|
|
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(
|
|
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.
|
|
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.
|
|
36
|
+
"@typedstandards/produce-core": "^0.9.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@typedstandards/verify-core": "^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",
|