@typedstandards/civic-typed-harness 0.1.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 +57 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/capture/data-sources.d.ts +58 -0
- package/dist/capture/data-sources.js +119 -0
- package/dist/capture/provenance.d.ts +48 -0
- package/dist/capture/provenance.js +290 -0
- package/dist/capture/skill-metadata.d.ts +23 -0
- package/dist/capture/skill-metadata.js +45 -0
- package/dist/capture/trace.d.ts +104 -0
- package/dist/capture/trace.js +166 -0
- package/dist/format/dathere.d.ts +83 -0
- package/dist/format/dathere.js +111 -0
- package/dist/format/profiles.d.ts +13 -0
- package/dist/format/profiles.js +26 -0
- package/dist/format/sources.d.ts +54 -0
- package/dist/format/sources.js +93 -0
- package/dist/format/vocabulary.d.ts +37 -0
- package/dist/format/vocabulary.js +53 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +30 -0
- package/dist/rubric/adversarial-eval-core.d.ts +27 -0
- package/dist/rubric/adversarial-eval-core.js +128 -0
- package/package.json +67 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type BlobRef } from '@typedstandards/produce-core';
|
|
2
|
+
/** Skill metadata extracted from a trace's `skill_fetch` span. Feeds the
|
|
3
|
+
* envelope's `skillMetadata` block (and the datHere policy's
|
|
4
|
+
* `skillMcpServerUrl` input). */
|
|
5
|
+
export interface ExtractedSkillMetadata {
|
|
6
|
+
systemPromptHash?: string;
|
|
7
|
+
mcpServerUrl?: string;
|
|
8
|
+
skillText?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extract skill metadata from the trace's `skill_fetch` span. Returns `{}`
|
|
12
|
+
* when the trace has no such span (or is not span-shaped) — callers shipping
|
|
13
|
+
* trace-as-BlobRef should supply an explicit override instead (the packager
|
|
14
|
+
* cannot inspect spans it does not have).
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractSkillMetadata(trace: Record<string, unknown>): ExtractedSkillMetadata;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the span-walkable view of a trace input. When the trace is a
|
|
19
|
+
* BlobRef the capture machinery cannot inspect spans (data-source detection,
|
|
20
|
+
* skill extraction, PROV-O construction all degrade gracefully on the empty
|
|
21
|
+
* trace this returns); otherwise the trace object passes through unchanged.
|
|
22
|
+
*/
|
|
23
|
+
export declare function traceForInspection(trace: Record<string, unknown> | BlobRef): Record<string, unknown>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Skill-metadata extraction (CAPTURE group) — OTel `skill_fetch` span
|
|
2
|
+
// walking, relocated from civic-ai-tools-website
|
|
3
|
+
// `src/lib/evidence/packager.ts:303–322` (extraction) and `:354–357` (the
|
|
4
|
+
// trace-as-BlobRef inspection fallback) per the S2 brief §1.
|
|
5
|
+
import { isBlobRef } from '@typedstandards/produce-core';
|
|
6
|
+
/**
|
|
7
|
+
* Extract skill metadata from the trace's `skill_fetch` span. Returns `{}`
|
|
8
|
+
* when the trace has no such span (or is not span-shaped) — callers shipping
|
|
9
|
+
* trace-as-BlobRef should supply an explicit override instead (the packager
|
|
10
|
+
* cannot inspect spans it does not have).
|
|
11
|
+
*/
|
|
12
|
+
export function extractSkillMetadata(trace) {
|
|
13
|
+
try {
|
|
14
|
+
// Untyped walk over the caller's trace shape.
|
|
15
|
+
const spans = trace?.resourceSpans?.[0]?.scopeSpans?.[0]?.spans;
|
|
16
|
+
if (!Array.isArray(spans))
|
|
17
|
+
return {};
|
|
18
|
+
const skillSpan = spans.find((s) => s.name === 'skill_fetch');
|
|
19
|
+
if (!skillSpan)
|
|
20
|
+
return {};
|
|
21
|
+
const attrs = {};
|
|
22
|
+
for (const a of skillSpan.attributes || []) {
|
|
23
|
+
attrs[a.key] = a.value?.stringValue || '';
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
systemPromptHash: attrs['skill.text_hash'] || undefined,
|
|
27
|
+
mcpServerUrl: attrs['skill.mcp_server_url'] || undefined,
|
|
28
|
+
skillText: attrs['skill.text'] || undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the span-walkable view of a trace input. When the trace is a
|
|
37
|
+
* BlobRef the capture machinery cannot inspect spans (data-source detection,
|
|
38
|
+
* skill extraction, PROV-O construction all degrade gracefully on the empty
|
|
39
|
+
* trace this returns); otherwise the trace object passes through unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export function traceForInspection(trace) {
|
|
42
|
+
return isBlobRef(trace)
|
|
43
|
+
? { resourceSpans: [] }
|
|
44
|
+
: trace;
|
|
45
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface SpanEvent {
|
|
2
|
+
timeUnixNano: string;
|
|
3
|
+
name: string;
|
|
4
|
+
attributes: OTelAttribute[];
|
|
5
|
+
}
|
|
6
|
+
export interface OTelAttribute {
|
|
7
|
+
key: string;
|
|
8
|
+
value: {
|
|
9
|
+
stringValue?: string;
|
|
10
|
+
intValue?: string;
|
|
11
|
+
boolValue?: boolean;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface Span {
|
|
15
|
+
traceId: string;
|
|
16
|
+
spanId: string;
|
|
17
|
+
parentSpanId?: string;
|
|
18
|
+
name: string;
|
|
19
|
+
kind: number;
|
|
20
|
+
startTimeUnixNano: string;
|
|
21
|
+
endTimeUnixNano?: string;
|
|
22
|
+
attributes: OTelAttribute[];
|
|
23
|
+
events: SpanEvent[];
|
|
24
|
+
status: {
|
|
25
|
+
code: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface OTelTrace {
|
|
29
|
+
resourceSpans: Array<{
|
|
30
|
+
resource: {
|
|
31
|
+
attributes: OTelAttribute[];
|
|
32
|
+
};
|
|
33
|
+
scopeSpans: Array<{
|
|
34
|
+
scope: {
|
|
35
|
+
name: string;
|
|
36
|
+
version: string;
|
|
37
|
+
};
|
|
38
|
+
spans: Span[];
|
|
39
|
+
}>;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/** SHA-256 hash of a string, returned as hex. Backed by @noble/hashes via
|
|
43
|
+
* verify-core — same digest bytes as `node:crypto`, browser-safe. */
|
|
44
|
+
export declare function hash(content: string): string;
|
|
45
|
+
/** Millisecond clock. Injectable for deterministic tests. */
|
|
46
|
+
export type TraceClock = () => number;
|
|
47
|
+
/** Cryptographically-random byte source. Injectable for deterministic
|
|
48
|
+
* tests. */
|
|
49
|
+
export type TraceRandomBytes = (byteLength: number) => Uint8Array;
|
|
50
|
+
/**
|
|
51
|
+
* TraceBuilder identity + determinism inputs. The identity strings name the
|
|
52
|
+
* capturing service, so they are per-instance config (config-not-constants);
|
|
53
|
+
* the demo values are `CIVICAITOOLS_TRACE_CONFIG` below.
|
|
54
|
+
*/
|
|
55
|
+
export interface TraceBuilderConfig {
|
|
56
|
+
/** OTel resource `service.name`. */
|
|
57
|
+
serviceName: string;
|
|
58
|
+
/** Instrumentation scope name. */
|
|
59
|
+
scopeName: string;
|
|
60
|
+
/** Scope version — also emitted as resource `service.version`. */
|
|
61
|
+
scopeVersion: string;
|
|
62
|
+
/** OTel semantic-conventions version advertised on the resource. */
|
|
63
|
+
semconvVersion: string;
|
|
64
|
+
/** Millisecond clock; defaults to `Date.now`. */
|
|
65
|
+
now?: TraceClock;
|
|
66
|
+
/** Random byte source for trace/span ids; defaults to the runtime's
|
|
67
|
+
* `crypto.getRandomValues` (Web Crypto — Node 18+ and every browser). */
|
|
68
|
+
randomBytes?: TraceRandomBytes;
|
|
69
|
+
}
|
|
70
|
+
/** Demo default: the civicaitools.org reference deployment's capture
|
|
71
|
+
* identity. */
|
|
72
|
+
export declare const CIVICAITOOLS_TRACE_CONFIG: TraceBuilderConfig;
|
|
73
|
+
export declare class TraceBuilder {
|
|
74
|
+
readonly traceId: string;
|
|
75
|
+
readonly rootSpanId: string;
|
|
76
|
+
private spans;
|
|
77
|
+
private readonly config;
|
|
78
|
+
private readonly now;
|
|
79
|
+
private readonly randomBytes;
|
|
80
|
+
constructor(config?: TraceBuilderConfig);
|
|
81
|
+
private randomHex;
|
|
82
|
+
private nowNano;
|
|
83
|
+
/**
|
|
84
|
+
* Start a new span. Returns the span ID.
|
|
85
|
+
* If no parentSpanId is given, the span is parented to the root.
|
|
86
|
+
*/
|
|
87
|
+
startSpan(name: string, parentSpanId?: string, attributes?: Record<string, string | number | boolean>): string;
|
|
88
|
+
/** End a span, optionally merging additional attributes. */
|
|
89
|
+
endSpan(spanId: string, attributes?: Record<string, string | number | boolean>): void;
|
|
90
|
+
/** Record a point-in-time event within an existing span. */
|
|
91
|
+
recordEvent(spanId: string, name: string, attributes?: Record<string, string | number | boolean>): void;
|
|
92
|
+
/**
|
|
93
|
+
* Start the root span. Call this at the very beginning of the analysis.
|
|
94
|
+
* The root span uses the pre-generated rootSpanId.
|
|
95
|
+
*/
|
|
96
|
+
startRoot(name: string, attributes?: Record<string, string | number | boolean>): void;
|
|
97
|
+
/** End the root span. */
|
|
98
|
+
endRoot(attributes?: Record<string, string | number | boolean>): void;
|
|
99
|
+
/**
|
|
100
|
+
* Finalize and return the complete trace as OTel-compatible JSON.
|
|
101
|
+
* Any spans that were started but not ended are closed at finalize time.
|
|
102
|
+
*/
|
|
103
|
+
finalize(): OTelTrace;
|
|
104
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// OTel-compatible trace capture (CAPTURE group) — types + TraceBuilder,
|
|
2
|
+
// relocated whole from civic-ai-tools-website `src/lib/evidence/trace.ts:1–199`
|
|
3
|
+
// per the S2 brief §1, with two changes:
|
|
4
|
+
// - `hash()` is re-backed by the @noble/hashes SHA-256 that verify-core
|
|
5
|
+
// implements and @typedstandards/produce-core re-exports (0.2.0), removing
|
|
6
|
+
// the module's only `node:crypto` use — the harness is browser-safe.
|
|
7
|
+
// - The resource/scope identity constants (`service.name`, scope
|
|
8
|
+
// name/version, semconv version) are typed config inputs with the demo
|
|
9
|
+
// values as the exported default (config-not-constants, S2 brief §2).
|
|
10
|
+
//
|
|
11
|
+
// Clock + RNG are INHERENT to capture (span timestamps and ids are what
|
|
12
|
+
// capture *is*): this module is the sanctioned exception to the package's
|
|
13
|
+
// determinism rule, and both are injectable for deterministic tests.
|
|
14
|
+
import { sha256Hex } from '@typedstandards/produce-core';
|
|
15
|
+
// --- Helpers ---
|
|
16
|
+
function toAttr(key, value) {
|
|
17
|
+
if (typeof value === 'number') {
|
|
18
|
+
return { key, value: { intValue: String(value) } };
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'boolean') {
|
|
21
|
+
return { key, value: { boolValue: value } };
|
|
22
|
+
}
|
|
23
|
+
return { key, value: { stringValue: value } };
|
|
24
|
+
}
|
|
25
|
+
function attrsFromRecord(attrs) {
|
|
26
|
+
if (!attrs)
|
|
27
|
+
return [];
|
|
28
|
+
return Object.entries(attrs).map(([k, v]) => toAttr(k, v));
|
|
29
|
+
}
|
|
30
|
+
/** SHA-256 hash of a string, returned as hex. Backed by @noble/hashes via
|
|
31
|
+
* verify-core — same digest bytes as `node:crypto`, browser-safe. */
|
|
32
|
+
export function hash(content) {
|
|
33
|
+
return sha256Hex(content);
|
|
34
|
+
}
|
|
35
|
+
/** Demo default: the civicaitools.org reference deployment's capture
|
|
36
|
+
* identity. */
|
|
37
|
+
export const CIVICAITOOLS_TRACE_CONFIG = {
|
|
38
|
+
serviceName: 'civic-ai-tools-website',
|
|
39
|
+
scopeName: 'civic-ai-tools-evidence',
|
|
40
|
+
scopeVersion: '0.1.0',
|
|
41
|
+
semconvVersion: '1.30.0',
|
|
42
|
+
};
|
|
43
|
+
function defaultRandomBytes(byteLength) {
|
|
44
|
+
const buf = new Uint8Array(byteLength);
|
|
45
|
+
globalThis.crypto.getRandomValues(buf);
|
|
46
|
+
return buf;
|
|
47
|
+
}
|
|
48
|
+
// --- TraceBuilder ---
|
|
49
|
+
export class TraceBuilder {
|
|
50
|
+
traceId;
|
|
51
|
+
rootSpanId;
|
|
52
|
+
spans = new Map();
|
|
53
|
+
config;
|
|
54
|
+
now;
|
|
55
|
+
randomBytes;
|
|
56
|
+
constructor(config = CIVICAITOOLS_TRACE_CONFIG) {
|
|
57
|
+
this.config = config;
|
|
58
|
+
this.now = config.now ?? Date.now;
|
|
59
|
+
this.randomBytes = config.randomBytes ?? defaultRandomBytes;
|
|
60
|
+
this.traceId = this.randomHex(16); // 128-bit
|
|
61
|
+
this.rootSpanId = this.randomHex(8); // 64-bit
|
|
62
|
+
}
|
|
63
|
+
randomHex(bytes) {
|
|
64
|
+
return Array.from(this.randomBytes(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
65
|
+
}
|
|
66
|
+
nowNano() {
|
|
67
|
+
// millisecond precision expressed as nanoseconds string
|
|
68
|
+
return `${this.now()}000000`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Start a new span. Returns the span ID.
|
|
72
|
+
* If no parentSpanId is given, the span is parented to the root.
|
|
73
|
+
*/
|
|
74
|
+
startSpan(name, parentSpanId, attributes) {
|
|
75
|
+
const spanId = this.randomHex(8);
|
|
76
|
+
this.spans.set(spanId, {
|
|
77
|
+
traceId: this.traceId,
|
|
78
|
+
spanId,
|
|
79
|
+
parentSpanId: parentSpanId ?? this.rootSpanId,
|
|
80
|
+
name,
|
|
81
|
+
kind: 1, // INTERNAL
|
|
82
|
+
startTimeUnixNano: this.nowNano(),
|
|
83
|
+
attributes: attrsFromRecord(attributes),
|
|
84
|
+
events: [],
|
|
85
|
+
status: { code: 0 },
|
|
86
|
+
});
|
|
87
|
+
return spanId;
|
|
88
|
+
}
|
|
89
|
+
/** End a span, optionally merging additional attributes. */
|
|
90
|
+
endSpan(spanId, attributes) {
|
|
91
|
+
const span = this.spans.get(spanId);
|
|
92
|
+
if (!span)
|
|
93
|
+
return;
|
|
94
|
+
span.endTimeUnixNano = this.nowNano();
|
|
95
|
+
if (attributes) {
|
|
96
|
+
span.attributes.push(...attrsFromRecord(attributes));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Record a point-in-time event within an existing span. */
|
|
100
|
+
recordEvent(spanId, name, attributes) {
|
|
101
|
+
const span = this.spans.get(spanId);
|
|
102
|
+
if (!span)
|
|
103
|
+
return;
|
|
104
|
+
span.events.push({
|
|
105
|
+
timeUnixNano: this.nowNano(),
|
|
106
|
+
name,
|
|
107
|
+
attributes: attrsFromRecord(attributes),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Start the root span. Call this at the very beginning of the analysis.
|
|
112
|
+
* The root span uses the pre-generated rootSpanId.
|
|
113
|
+
*/
|
|
114
|
+
startRoot(name, attributes) {
|
|
115
|
+
this.spans.set(this.rootSpanId, {
|
|
116
|
+
traceId: this.traceId,
|
|
117
|
+
spanId: this.rootSpanId,
|
|
118
|
+
// root has no parent
|
|
119
|
+
name,
|
|
120
|
+
kind: 2, // SERVER
|
|
121
|
+
startTimeUnixNano: this.nowNano(),
|
|
122
|
+
attributes: attrsFromRecord(attributes),
|
|
123
|
+
events: [],
|
|
124
|
+
status: { code: 0 },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** End the root span. */
|
|
128
|
+
endRoot(attributes) {
|
|
129
|
+
this.endSpan(this.rootSpanId, attributes);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Finalize and return the complete trace as OTel-compatible JSON.
|
|
133
|
+
* Any spans that were started but not ended are closed at finalize time.
|
|
134
|
+
*/
|
|
135
|
+
finalize() {
|
|
136
|
+
const finalTime = this.nowNano();
|
|
137
|
+
for (const span of this.spans.values()) {
|
|
138
|
+
if (!span.endTimeUnixNano) {
|
|
139
|
+
span.endTimeUnixNano = finalTime;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
resourceSpans: [
|
|
144
|
+
{
|
|
145
|
+
resource: {
|
|
146
|
+
attributes: [
|
|
147
|
+
toAttr('service.name', this.config.serviceName),
|
|
148
|
+
toAttr('service.version', this.config.scopeVersion),
|
|
149
|
+
toAttr('telemetry.sdk.language', 'typescript'),
|
|
150
|
+
toAttr('otel.semconv.version', this.config.semconvVersion),
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
scopeSpans: [
|
|
154
|
+
{
|
|
155
|
+
scope: {
|
|
156
|
+
name: this.config.scopeName,
|
|
157
|
+
version: this.config.scopeVersion,
|
|
158
|
+
},
|
|
159
|
+
spans: Array.from(this.spans.values()),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** The datHere content-profile label (ADR-0004). */
|
|
2
|
+
export declare const DATHERE_CONTENT_PROFILE = "datHere";
|
|
3
|
+
/** Producer Profile auto-derived for the datHere content profile (ADR-0006):
|
|
4
|
+
* compound `<profile-type>/<profile-subtype>`. */
|
|
5
|
+
export declare const DATHERE_PRODUCER_PROFILE = "ai-assisted-analysis/datHere";
|
|
6
|
+
/** Reverse-DNS key of the auto-emitted environment extension. */
|
|
7
|
+
export declare const ENVIRONMENT_EXTENSION_KEY = "org.civicaitools.environment";
|
|
8
|
+
/** Demo default for the environment extension's `host` field: the
|
|
9
|
+
* civicaitools.org reference deployment. Config-not-constants — every other
|
|
10
|
+
* instance supplies its own via `DatHereEnvironmentConfig`. */
|
|
11
|
+
export declare const CIVICAITOOLS_ENVIRONMENT_HOST = "civicaitools.org";
|
|
12
|
+
/** Per-instance configuration for the environment extension. */
|
|
13
|
+
export interface DatHereEnvironmentConfig {
|
|
14
|
+
/** Publishing host recorded in `org.civicaitools.environment.host`. */
|
|
15
|
+
host: string;
|
|
16
|
+
}
|
|
17
|
+
/** The civicaitools.org demo default. */
|
|
18
|
+
export declare const CIVICAITOOLS_ENVIRONMENT_CONFIG: DatHereEnvironmentConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Derive the envelope's `producerProfile` (ADR-0006): the explicit value when
|
|
21
|
+
* supplied, else auto-derived from the datHere content profile, else
|
|
22
|
+
* `undefined` (and thereby unemitted — pre-ADR-0006 canonical JSON stays
|
|
23
|
+
* byte-identical).
|
|
24
|
+
*/
|
|
25
|
+
export declare function deriveProducerProfile(producerProfile?: string, contentProfile?: string): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Select the content-canonicalization rule URI (spec §8.2) for a content
|
|
28
|
+
* profile: `dathere-ag-jupyter/v1` for datHere (fingerprints the executed
|
|
29
|
+
* notebook), `legacy-json/v1` for everything else (whole-package rule).
|
|
30
|
+
*/
|
|
31
|
+
export declare function selectContentCanonicalization(contentProfile?: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Summary-emission decision (ADR-0004 §7): the datHere profile REQUIRES the
|
|
34
|
+
* summary in canonical JSON (covered by the envelope hash); every other
|
|
35
|
+
* profile keeps it off-envelope (DB-row only in the reference app), so their
|
|
36
|
+
* package hashes stay byte-identical to pre-ADR-0004 behavior. Returns the
|
|
37
|
+
* value to pass as the envelope's `summary` field, or `undefined` to omit.
|
|
38
|
+
*/
|
|
39
|
+
export declare function deriveSummaryEmission(contentProfile?: string, summary?: string): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Build the `org.civicaitools.environment` extension content for a datHere
|
|
42
|
+
* package. Per OES §9.1.1 requirement 3 the extension MUST carry
|
|
43
|
+
* `modelVersion`, `temperature`, `mcpServers`, `toolDefinitions`, `host`.
|
|
44
|
+
*
|
|
45
|
+
* Prototype limitations carried over from the reference implementation
|
|
46
|
+
* (known gaps, tightened in follow-up work): `temperature` placeholder `0`,
|
|
47
|
+
* `toolDefinitions` placeholder `[]`; `mcpServers` derives from the trace's
|
|
48
|
+
* skill-fetch span URL (the analysis's primary MCP server). Fields captured
|
|
49
|
+
* honestly: `modelVersion` and `host`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildDatHereEnvironment(model: string, skillMcpServerUrl: string | undefined, config?: DatHereEnvironmentConfig): Record<string, unknown>;
|
|
52
|
+
/** Input to the composite datHere-policy derivation. */
|
|
53
|
+
export interface DatHerePolicyInput {
|
|
54
|
+
/** Model identifier (becomes `environment.modelVersion`). */
|
|
55
|
+
model: string;
|
|
56
|
+
/** Content-profile label; absence means the default profile. */
|
|
57
|
+
contentProfile?: string;
|
|
58
|
+
/** Explicit producerProfile — wins over auto-derivation when supplied. */
|
|
59
|
+
producerProfile?: string;
|
|
60
|
+
/** Candidate summary text (emitted only under the datHere profile). */
|
|
61
|
+
summary?: string;
|
|
62
|
+
/** MCP server URL from the trace's skill-fetch span (capture-side
|
|
63
|
+
* extraction — see src/capture/skill-metadata.ts). */
|
|
64
|
+
skillMcpServerUrl?: string;
|
|
65
|
+
/** Caller-supplied extensions to layer the environment extension onto. */
|
|
66
|
+
extensions?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
/** The derived envelope-field values, ready to spread into produce-core's
|
|
69
|
+
* `EnvelopeInput`. Fields are `undefined` exactly when the reference
|
|
70
|
+
* packager left them unemitted. */
|
|
71
|
+
export interface DatHereEnvelopeFields {
|
|
72
|
+
producerProfile?: string;
|
|
73
|
+
contentCanonicalization: string;
|
|
74
|
+
summary?: string;
|
|
75
|
+
extensions?: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Composite derivation: everything the reference packager derived inline for
|
|
79
|
+
* the datHere policy, as one call. Non-datHere inputs pass through with only
|
|
80
|
+
* the legacy canonicalization rule and any caller-supplied extensions —
|
|
81
|
+
* byte-identical envelopes via the core's conditional spreads.
|
|
82
|
+
*/
|
|
83
|
+
export declare function deriveDatHereEnvelopeFields(input: DatHerePolicyInput, config?: DatHereEnvironmentConfig): DatHereEnvelopeFields;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// datHere envelope policy (FORMAT-EXTENSION group) — the derivations the
|
|
2
|
+
// reference app's packager did inline, relocated from civic-ai-tools-website
|
|
3
|
+
// `src/lib/evidence/packager.ts` per the S2 brief §1:
|
|
4
|
+
// - `:26` DATHERE_PRODUCER_PROFILE constant
|
|
5
|
+
// - `:286–301` `org.civicaitools.environment` extension builder
|
|
6
|
+
// - `:393–395` producerProfile derivation
|
|
7
|
+
// - `:407–410` content-canonicalization rule selection
|
|
8
|
+
// - `:464–466` summary-emission decision
|
|
9
|
+
// - `:477–484` extension layering
|
|
10
|
+
//
|
|
11
|
+
// Output = explicit envelope-field values for @typedstandards/produce-core's
|
|
12
|
+
// `buildEnvelope` (ADR-0021's operating rule: the harness DERIVES, the core
|
|
13
|
+
// ASSEMBLES). Byte discipline: every field is emitted exactly when the
|
|
14
|
+
// reference packager emitted it, so equivalent inputs produce byte-identical
|
|
15
|
+
// canonical JSON through the core's conditional spreads.
|
|
16
|
+
//
|
|
17
|
+
// The canonicalization rule URIs are verify-core's, reached through
|
|
18
|
+
// @typedstandards/produce-core's 0.2.0 re-exports (the harness's single
|
|
19
|
+
// declared dependency); the harness re-emits, never redefines, them.
|
|
20
|
+
import { DATHERE_AG_JUPYTER_CANONICALIZATION, LEGACY_JSON_CANONICALIZATION, } from '@typedstandards/produce-core';
|
|
21
|
+
/** The datHere content-profile label (ADR-0004). */
|
|
22
|
+
export const DATHERE_CONTENT_PROFILE = 'datHere';
|
|
23
|
+
/** Producer Profile auto-derived for the datHere content profile (ADR-0006):
|
|
24
|
+
* compound `<profile-type>/<profile-subtype>`. */
|
|
25
|
+
export const DATHERE_PRODUCER_PROFILE = 'ai-assisted-analysis/datHere';
|
|
26
|
+
/** Reverse-DNS key of the auto-emitted environment extension. */
|
|
27
|
+
export const ENVIRONMENT_EXTENSION_KEY = 'org.civicaitools.environment';
|
|
28
|
+
/** Demo default for the environment extension's `host` field: the
|
|
29
|
+
* civicaitools.org reference deployment. Config-not-constants — every other
|
|
30
|
+
* instance supplies its own via `DatHereEnvironmentConfig`. */
|
|
31
|
+
export const CIVICAITOOLS_ENVIRONMENT_HOST = 'civicaitools.org';
|
|
32
|
+
/** The civicaitools.org demo default. */
|
|
33
|
+
export const CIVICAITOOLS_ENVIRONMENT_CONFIG = {
|
|
34
|
+
host: CIVICAITOOLS_ENVIRONMENT_HOST,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Derive the envelope's `producerProfile` (ADR-0006): the explicit value when
|
|
38
|
+
* supplied, else auto-derived from the datHere content profile, else
|
|
39
|
+
* `undefined` (and thereby unemitted — pre-ADR-0006 canonical JSON stays
|
|
40
|
+
* byte-identical).
|
|
41
|
+
*/
|
|
42
|
+
export function deriveProducerProfile(producerProfile, contentProfile) {
|
|
43
|
+
return (producerProfile
|
|
44
|
+
?? (contentProfile === DATHERE_CONTENT_PROFILE
|
|
45
|
+
? DATHERE_PRODUCER_PROFILE
|
|
46
|
+
: undefined));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Select the content-canonicalization rule URI (spec §8.2) for a content
|
|
50
|
+
* profile: `dathere-ag-jupyter/v1` for datHere (fingerprints the executed
|
|
51
|
+
* notebook), `legacy-json/v1` for everything else (whole-package rule).
|
|
52
|
+
*/
|
|
53
|
+
export function selectContentCanonicalization(contentProfile) {
|
|
54
|
+
return contentProfile === DATHERE_CONTENT_PROFILE
|
|
55
|
+
? DATHERE_AG_JUPYTER_CANONICALIZATION
|
|
56
|
+
: LEGACY_JSON_CANONICALIZATION;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Summary-emission decision (ADR-0004 §7): the datHere profile REQUIRES the
|
|
60
|
+
* summary in canonical JSON (covered by the envelope hash); every other
|
|
61
|
+
* profile keeps it off-envelope (DB-row only in the reference app), so their
|
|
62
|
+
* package hashes stay byte-identical to pre-ADR-0004 behavior. Returns the
|
|
63
|
+
* value to pass as the envelope's `summary` field, or `undefined` to omit.
|
|
64
|
+
*/
|
|
65
|
+
export function deriveSummaryEmission(contentProfile, summary) {
|
|
66
|
+
return contentProfile === DATHERE_CONTENT_PROFILE && summary
|
|
67
|
+
? summary
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the `org.civicaitools.environment` extension content for a datHere
|
|
72
|
+
* package. Per OES §9.1.1 requirement 3 the extension MUST carry
|
|
73
|
+
* `modelVersion`, `temperature`, `mcpServers`, `toolDefinitions`, `host`.
|
|
74
|
+
*
|
|
75
|
+
* Prototype limitations carried over from the reference implementation
|
|
76
|
+
* (known gaps, tightened in follow-up work): `temperature` placeholder `0`,
|
|
77
|
+
* `toolDefinitions` placeholder `[]`; `mcpServers` derives from the trace's
|
|
78
|
+
* skill-fetch span URL (the analysis's primary MCP server). Fields captured
|
|
79
|
+
* honestly: `modelVersion` and `host`.
|
|
80
|
+
*/
|
|
81
|
+
export function buildDatHereEnvironment(model, skillMcpServerUrl, config = CIVICAITOOLS_ENVIRONMENT_CONFIG) {
|
|
82
|
+
const mcpServers = [];
|
|
83
|
+
if (skillMcpServerUrl) {
|
|
84
|
+
mcpServers.push({ url: skillMcpServerUrl });
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
modelVersion: model,
|
|
88
|
+
temperature: 0,
|
|
89
|
+
mcpServers,
|
|
90
|
+
toolDefinitions: [],
|
|
91
|
+
host: config.host,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Composite derivation: everything the reference packager derived inline for
|
|
96
|
+
* the datHere policy, as one call. Non-datHere inputs pass through with only
|
|
97
|
+
* the legacy canonicalization rule and any caller-supplied extensions —
|
|
98
|
+
* byte-identical envelopes via the core's conditional spreads.
|
|
99
|
+
*/
|
|
100
|
+
export function deriveDatHereEnvelopeFields(input, config = CIVICAITOOLS_ENVIRONMENT_CONFIG) {
|
|
101
|
+
const extensions = { ...(input.extensions ?? {}) };
|
|
102
|
+
if (input.contentProfile === DATHERE_CONTENT_PROFILE) {
|
|
103
|
+
extensions[ENVIRONMENT_EXTENSION_KEY] = buildDatHereEnvironment(input.model, input.skillMcpServerUrl, config);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
producerProfile: deriveProducerProfile(input.producerProfile, input.contentProfile),
|
|
107
|
+
contentCanonicalization: selectContentCanonicalization(input.contentProfile),
|
|
108
|
+
summary: deriveSummaryEmission(input.contentProfile, input.summary),
|
|
109
|
+
...(Object.keys(extensions).length > 0 ? { extensions } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type CaptureMethod } from '@typedstandards/produce-core';
|
|
2
|
+
export type { CaptureMethod };
|
|
3
|
+
/** The profile TYPE the civic harness publishes under (the segment before the
|
|
4
|
+
* first `/` of the compound producerProfile). */
|
|
5
|
+
export declare const AI_ASSISTED_ANALYSIS_PROFILE_TYPE = "ai-assisted-analysis";
|
|
6
|
+
/**
|
|
7
|
+
* The captureMethod vocabulary for the `ai-assisted-analysis` profile type —
|
|
8
|
+
* verify-core's Q32 fallback-table entry, re-exported (single-sourced, so the
|
|
9
|
+
* harness cannot drift from what the verifier accepts):
|
|
10
|
+
* `chat-flow-stream` · `claude-code-jsonl-readback` ·
|
|
11
|
+
* `claude-code-self-report` (legacy, deprecated 2026-04-28).
|
|
12
|
+
*/
|
|
13
|
+
export declare const AI_ASSISTED_ANALYSIS_CAPTURE_METHODS: readonly CaptureMethod[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// captureMethod vocabulary surface (FORMAT-EXTENSION group) — the profile
|
|
2
|
+
// constants the civic harness publishes under, re-exported CONSISTENT WITH
|
|
3
|
+
// verify-core's hardcoded fallback table by construction (the values below
|
|
4
|
+
// ARE that table's entry, not a copy).
|
|
5
|
+
//
|
|
6
|
+
// ⚠ Q32 adjacency (open-questions registry): verify-core's per-profile
|
|
7
|
+
// captureMethod vocabulary is a hardcoded fallback table pending versioned,
|
|
8
|
+
// content-addressed guidance bundles. The `ai-assisted-analysis` guidance
|
|
9
|
+
// bundle already lives in civic-ai-tools docs (ADR-0006/ADR-0011), which is
|
|
10
|
+
// one more reason the Q32 versioned-bundle question eventually lands in this
|
|
11
|
+
// package. Flagged, not solved, here — do not grow this module into a bundle
|
|
12
|
+
// mechanism without resolving Q32 first.
|
|
13
|
+
// Reached through @typedstandards/produce-core's 0.2.0 re-exports (the
|
|
14
|
+
// harness's single declared dependency); the table itself is verify-core's.
|
|
15
|
+
import { PROFILE_CAPTURE_VOCAB, } from '@typedstandards/produce-core';
|
|
16
|
+
/** The profile TYPE the civic harness publishes under (the segment before the
|
|
17
|
+
* first `/` of the compound producerProfile). */
|
|
18
|
+
export const AI_ASSISTED_ANALYSIS_PROFILE_TYPE = 'ai-assisted-analysis';
|
|
19
|
+
/**
|
|
20
|
+
* The captureMethod vocabulary for the `ai-assisted-analysis` profile type —
|
|
21
|
+
* verify-core's Q32 fallback-table entry, re-exported (single-sourced, so the
|
|
22
|
+
* harness cannot drift from what the verifier accepts):
|
|
23
|
+
* `chat-flow-stream` · `claude-code-jsonl-readback` ·
|
|
24
|
+
* `claude-code-self-report` (legacy, deprecated 2026-04-28).
|
|
25
|
+
*/
|
|
26
|
+
export const AI_ASSISTED_ANALYSIS_CAPTURE_METHODS = PROFILE_CAPTURE_VOCAB[AI_ASSISTED_ANALYSIS_PROFILE_TYPE];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { DataSourceEntry } from '@typedstandards/produce-core';
|
|
2
|
+
/** One known data source: identity, display, and endpoint coordinates. */
|
|
3
|
+
export interface CivicSourceInfo {
|
|
4
|
+
/** Human-friendly display label (evidence detail page "Data sources"). */
|
|
5
|
+
displayName: string;
|
|
6
|
+
/** PROV agent `dcterms:title`, e.g. "Socrata MCP Server". */
|
|
7
|
+
agentTitle: string;
|
|
8
|
+
/** MCP endpoint URL — the PROV agent's `civic:serverUrl`. */
|
|
9
|
+
serverUrl: string;
|
|
10
|
+
/** `dataSources[].catalogType` value for entries from this source. */
|
|
11
|
+
catalogType: string;
|
|
12
|
+
/**
|
|
13
|
+
* Portal URL for the single AGGREGATE `dataSources` entry emitted when this
|
|
14
|
+
* source's query surface is not dataset-keyed (Data Commons's knowledge
|
|
15
|
+
* graph, Boston OpenContext's CKAN DataStore). ABSENT for dataset-keyed
|
|
16
|
+
* sources (Socrata), which emit one entry per dataset instead — presence of
|
|
17
|
+
* this field is what routes a source down the aggregate path.
|
|
18
|
+
*/
|
|
19
|
+
aggregatePortalUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A source registry: source id → coordinates. Insertion order is emission
|
|
22
|
+
* order for aggregate `dataSources` entries. */
|
|
23
|
+
export type CivicSourceRegistry = Record<string, CivicSourceInfo>;
|
|
24
|
+
/** The civic default registry — the three demo sources. */
|
|
25
|
+
export declare const CIVIC_SOURCE_REGISTRY: CivicSourceRegistry;
|
|
26
|
+
/** The source id untagged/pre-M9.1 captures fall back to. Socrata was the
|
|
27
|
+
* only source when those packages were written. */
|
|
28
|
+
export declare const FALLBACK_SOURCE_ID = "socrata";
|
|
29
|
+
/** Is this source dataset-keyed (one `dataSources` entry per dataset) rather
|
|
30
|
+
* than aggregate? Unknown ids are neither — they contribute no entry. */
|
|
31
|
+
export declare function isDatasetKeyedSource(sourceId: string, registry?: CivicSourceRegistry): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve an MCP tool name to a source id, or `undefined` when unknown.
|
|
34
|
+
* The data-source population function (capture group) takes one of these as
|
|
35
|
+
* a CALLER-SUPPLIED input; `civicToolSourceResolver` below is the civic
|
|
36
|
+
* default. An instance wiring its own MCP layer supplies its own resolver —
|
|
37
|
+
* the resolver is how the app-side MCP registry stays app-side.
|
|
38
|
+
*/
|
|
39
|
+
export type ToolSourceResolver = (toolName: string) => string | undefined;
|
|
40
|
+
/** Civic default tool-name → source-id map (mirrors the demo app's static
|
|
41
|
+
* MCP-layer map; used as a fallback when a trace span carries no
|
|
42
|
+
* `mcp.source` attribute). */
|
|
43
|
+
export declare const CIVIC_TOOL_SOURCE_MAP: Record<string, string>;
|
|
44
|
+
/** The civic default `ToolSourceResolver`. */
|
|
45
|
+
export declare const civicToolSourceResolver: ToolSourceResolver;
|
|
46
|
+
/** Human-friendly display label for a `sourceId`. Unknown ids fall back to a
|
|
47
|
+
* capitalised form of the raw id so new sources render sensibly before the
|
|
48
|
+
* registry is updated. Missing ids coerce to the fallback source (pre-M9.3
|
|
49
|
+
* packages have no `sourceId` on dataSources entries). */
|
|
50
|
+
export declare function displayNameForSource(sourceId: string | undefined | null, registry?: CivicSourceRegistry): string;
|
|
51
|
+
/** Format a `dataSources` array as a compact, de-duplicated summary string
|
|
52
|
+
* (middle-dot separated). Returns `null` when the array is empty or missing,
|
|
53
|
+
* letting callers render a fallback. */
|
|
54
|
+
export declare function formatDataSourcesSummary(entries: DataSourceEntry[] | undefined, registry?: CivicSourceRegistry): string | null;
|