@kontourai/lookout 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -38
- package/dist/src/check-result.d.ts +2 -2
- package/dist/src/check-runner.d.ts +1 -1
- package/dist/src/check-runner.js +42 -6
- package/dist/src/cli.d.ts +2 -2
- package/dist/src/cli.js +10 -10
- package/dist/src/coverage.d.ts +58 -0
- package/dist/src/coverage.js +39 -0
- package/dist/src/{survey-emission.d.ts → drift-emission.d.ts} +16 -19
- package/dist/src/drift-emission.js +71 -0
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.js +4 -2
- package/dist/src/observe-extract-diff.d.ts +90 -0
- package/dist/src/observe-extract-diff.js +154 -0
- package/dist/src/registry.d.ts +16 -4
- package/dist/src/registry.js +107 -41
- package/dist/src/snapshot-store.d.ts +11 -1
- package/dist/src/snapshot-store.js +18 -1
- package/package.json +4 -4
- package/dist/src/survey-emission.js +0 -138
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type SurveyInput } from "@kontourai/survey";
|
|
2
1
|
import type { LookoutSource } from "./registry.js";
|
|
3
2
|
import { type ProposalDiffEvent, type ProposalSetDiff, type ProposalSetDiffInput, type ProposalSetFacts, type ProposalSetObservation } from "./proposal-diff.js";
|
|
4
3
|
import type { ObservationCheckAnchor, ObservationStore, StoredProposalObservationV1 } from "./observation-store.js";
|
|
@@ -11,7 +10,7 @@ export interface BaselineEstablishedFact {
|
|
|
11
10
|
readonly resolution: "observation";
|
|
12
11
|
readonly proposalCount: number;
|
|
13
12
|
}
|
|
14
|
-
export type
|
|
13
|
+
export type DriftFact = BaselineEstablishedFact | {
|
|
15
14
|
readonly kind: "proposal-set-facts";
|
|
16
15
|
readonly priorSnapshotRef: string;
|
|
17
16
|
readonly currentSnapshotRef: string;
|
|
@@ -19,37 +18,38 @@ export type EmissionFact = BaselineEstablishedFact | {
|
|
|
19
18
|
readonly resolution: "observation";
|
|
20
19
|
readonly value: ProposalSetFacts;
|
|
21
20
|
};
|
|
22
|
-
export interface
|
|
21
|
+
export interface DriftSuccess {
|
|
23
22
|
readonly sourceId: string;
|
|
24
23
|
readonly events: readonly ProposalDiffEvent[];
|
|
25
|
-
readonly facts: readonly
|
|
26
|
-
|
|
24
|
+
readonly facts: readonly DriftFact[];
|
|
25
|
+
/** The prior observation this drift was diffed against, or null on a first-ever (baseline) observation. */
|
|
26
|
+
readonly priorObservationId: string | null;
|
|
27
27
|
readonly committedObservation: StoredProposalObservationV1;
|
|
28
28
|
readonly warnings: readonly string[];
|
|
29
29
|
}
|
|
30
|
-
export type
|
|
31
|
-
export interface
|
|
32
|
-
readonly kind:
|
|
30
|
+
export type DriftErrorKind = "invalid-input" | "prior-state-error" | "diff-error" | "persistence-error" | "serialization-error" | "unexpected";
|
|
31
|
+
export interface DriftError {
|
|
32
|
+
readonly kind: DriftErrorKind;
|
|
33
33
|
readonly message: string;
|
|
34
34
|
readonly cause?: unknown;
|
|
35
35
|
}
|
|
36
|
-
export type
|
|
36
|
+
export type DriftResult = {
|
|
37
37
|
readonly ok: true;
|
|
38
|
-
readonly value:
|
|
38
|
+
readonly value: DriftSuccess;
|
|
39
39
|
} | {
|
|
40
40
|
readonly ok: false;
|
|
41
|
-
readonly error:
|
|
41
|
+
readonly error: DriftError;
|
|
42
42
|
};
|
|
43
|
-
export interface
|
|
43
|
+
export interface EmitDriftInput<E> {
|
|
44
44
|
readonly source: LookoutSource;
|
|
45
45
|
readonly current: ProposalSetObservation;
|
|
46
46
|
readonly check: ObservationCheckAnchor;
|
|
47
47
|
readonly callbacks: Omit<ProposalSetDiffInput<E>, "prior" | "current">;
|
|
48
48
|
}
|
|
49
|
-
export interface
|
|
50
|
-
emit(input:
|
|
49
|
+
export interface DriftEmitter<E> {
|
|
50
|
+
emit(input: EmitDriftInput<E>): Promise<DriftResult>;
|
|
51
51
|
}
|
|
52
|
-
export interface
|
|
52
|
+
export interface CreateDriftEmitterOptions<E> {
|
|
53
53
|
readonly store: ObservationStore;
|
|
54
54
|
readonly now?: () => string;
|
|
55
55
|
readonly diff?: (input: ProposalSetDiffInput<E>) => {
|
|
@@ -61,8 +61,5 @@ export interface CreateSurveyEmitterOptions<E> {
|
|
|
61
61
|
readonly message: string;
|
|
62
62
|
};
|
|
63
63
|
};
|
|
64
|
-
/** Test seam. Production callers must leave this as observation. */
|
|
65
|
-
readonly resolution?: "observation";
|
|
66
|
-
readonly transformSurveyInput?: (input: SurveyInput) => SurveyInput;
|
|
67
64
|
}
|
|
68
|
-
export declare function
|
|
65
|
+
export declare function createDriftEmitter<E>(options: CreateDriftEmitterOptions<E>): DriftEmitter<E>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { diffProposalSets } from "./proposal-diff.js";
|
|
2
|
+
function stableJson(value) {
|
|
3
|
+
if (Array.isArray(value))
|
|
4
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
5
|
+
if (value && typeof value === "object")
|
|
6
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
|
|
7
|
+
return JSON.stringify(value) ?? "undefined";
|
|
8
|
+
}
|
|
9
|
+
function normalizeDiff(value) {
|
|
10
|
+
const sorted = (items) => [...items].sort((a, b) => stableJson(a).localeCompare(stableJson(b)));
|
|
11
|
+
return {
|
|
12
|
+
events: sorted(value.events),
|
|
13
|
+
facts: {
|
|
14
|
+
retainedProposalOccurrences: sorted(value.facts.retainedProposalOccurrences),
|
|
15
|
+
addedProposalOccurrences: sorted(value.facts.addedProposalOccurrences),
|
|
16
|
+
removedProposalOccurrences: sorted(value.facts.removedProposalOccurrences),
|
|
17
|
+
provenanceChanges: sorted(value.facts.provenanceChanges),
|
|
18
|
+
removedEntities: [...value.facts.removedEntities].sort(),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createDriftEmitter(options) {
|
|
23
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
24
|
+
const diff = options.diff ?? diffProposalSets;
|
|
25
|
+
return {
|
|
26
|
+
async emit(input) {
|
|
27
|
+
try {
|
|
28
|
+
if (!input.source || input.source.id !== input.current?.sourceId || input.check?.currentSnapshotRef !== input.current?.snapshotRef) {
|
|
29
|
+
return { ok: false, error: { kind: "invalid-input", message: "Registry source, observation, and check anchor must agree" } };
|
|
30
|
+
}
|
|
31
|
+
const loaded = await options.store.loadLatest(input.source.id);
|
|
32
|
+
if (!loaded.ok)
|
|
33
|
+
return { ok: false, error: { kind: "prior-state-error", message: loaded.error.message, cause: loaded.error } };
|
|
34
|
+
const recordedAt = now();
|
|
35
|
+
const priorObservationId = loaded.value?.observationId ?? null;
|
|
36
|
+
let events = [];
|
|
37
|
+
let facts;
|
|
38
|
+
if (loaded.value === null) {
|
|
39
|
+
facts = [{ kind: "baseline-established", sourceId: input.source.id, snapshotRef: input.current.snapshotRef, observedAt: input.current.observedAt, origin: input.source.kind, resolution: "observation", proposalCount: input.current.proposals.length }];
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
let derived;
|
|
43
|
+
try {
|
|
44
|
+
derived = diff({ prior: { sourceId: loaded.value.sourceId, snapshotRef: loaded.value.snapshotRef, observedAt: loaded.value.observedAt, proposals: loaded.value.proposals }, current: input.current, ...input.callbacks });
|
|
45
|
+
}
|
|
46
|
+
catch (cause) {
|
|
47
|
+
return { ok: false, error: { kind: "diff-error", message: "Proposal diff threw", cause } };
|
|
48
|
+
}
|
|
49
|
+
if (!derived.ok)
|
|
50
|
+
return { ok: false, error: { kind: "diff-error", message: derived.error.message, cause: derived.error } };
|
|
51
|
+
const normalized = normalizeDiff(derived.value);
|
|
52
|
+
events = normalized.events;
|
|
53
|
+
facts = [{ kind: "proposal-set-facts", priorSnapshotRef: loaded.value.snapshotRef, currentSnapshotRef: input.current.snapshotRef, origin: input.source.kind, resolution: "observation", value: normalized.facts }];
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
JSON.stringify({ events, facts });
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
return { ok: false, error: { kind: "serialization-error", message: "Drift result is not serializable", cause } };
|
|
60
|
+
}
|
|
61
|
+
const committed = await options.store.commit({ observation: input.current, recordedAt, check: input.check }, loaded.value?.observationId ?? null);
|
|
62
|
+
if (!committed.ok)
|
|
63
|
+
return { ok: false, error: { kind: "persistence-error", message: committed.error.message, cause: committed.error } };
|
|
64
|
+
return { ok: true, value: { sourceId: input.source.id, events, facts, priorObservationId, committedObservation: committed.value, warnings: committed.warnings ?? [] } };
|
|
65
|
+
}
|
|
66
|
+
catch (cause) {
|
|
67
|
+
return { ok: false, error: { kind: "unexpected", message: "Drift emission failed", cause } };
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ export type { ChangedResult, CheckResult, CheckResultCommon, ErrorResult, Lookou
|
|
|
6
6
|
export { defaultProviderResolver } from "./provider-resolution.js";
|
|
7
7
|
export type { ProviderResolver } from "./provider-resolution.js";
|
|
8
8
|
export { loadRegistry, LookoutRegistry, parseRegistry, RegistryValidationError, } from "./registry.js";
|
|
9
|
-
export type { LookoutRegistryDocument, LookoutSource, LookoutSourceKind, RenderPolicy, } from "./registry.js";
|
|
10
|
-
export { createLookoutSnapshotStore } from "./snapshot-store.js";
|
|
9
|
+
export type { ExtractableLookoutSource, LookoutRegistryDocument, LookoutSource, LookoutSourceKind, RenderPolicy, StructuredFileFormat, StructuredFileLookoutSource, } from "./registry.js";
|
|
10
|
+
export { createLookoutSnapshotStore, resolveLookoutSnapshot, } from "./snapshot-store.js";
|
|
11
|
+
export type { ResolveLookoutSnapshotOptions } from "./snapshot-store.js";
|
|
12
|
+
export type { SnapshotSourceRefResolution } from "@kontourai/forage/fetch";
|
|
11
13
|
export { canonicalValueKey } from "./canonical-value.js";
|
|
12
14
|
export type { CanonicalValueKey, CanonicalValueFormatVersion, CanonicalValueResult, DiffKernelError, DiffKernelErrorKind, DiffResult, IdentityResult, } from "./canonical-value.js";
|
|
13
15
|
export { compareStructural, diffKeyedMultiset } from "./structural-diff.js";
|
|
@@ -16,5 +18,9 @@ export { diffProposalSets, extractionProposalIdentity } from "./proposal-diff.js
|
|
|
16
18
|
export type { FieldChangedEvent, FieldChangeKind, NewEntityAppearedEvent, ProposalDiffEvent, ProposalEvidence, ProposalIdentity, ProposalOccurrencePair, ProposalSetDiff, ProposalSetDiffInput, ProposalSetFacts, ProposalSetObservation, ProvenanceChangeFact, } from "./proposal-diff.js";
|
|
17
19
|
export { createObservationStore } from "./observation-store.js";
|
|
18
20
|
export type { CreateObservationStoreOptions, ObservationCheckAnchor, ObservationStore, ObservationStoreError, ObservationStoreErrorKind, ObservationStoreResult, ProposalObservationRecordInput, StoredProposalObservationV1 } from "./observation-store.js";
|
|
19
|
-
export {
|
|
20
|
-
export type { BaselineEstablishedFact,
|
|
21
|
+
export { createDriftEmitter } from "./drift-emission.js";
|
|
22
|
+
export type { BaselineEstablishedFact, CreateDriftEmitterOptions, DriftEmitter, DriftError, DriftErrorKind, DriftFact, DriftResult, DriftSuccess, EmitDriftInput } from "./drift-emission.js";
|
|
23
|
+
export { checkSchemaCoverage } from "./coverage.js";
|
|
24
|
+
export type { SchemaCoverageGap, SchemaCoverageResult } from "./coverage.js";
|
|
25
|
+
export { createObserveExtractDiff } from "./observe-extract-diff.js";
|
|
26
|
+
export type { ObserveExtractAcquisition, ObserveExtractAttempt, ObserveExtractDiff, ObserveExtractDiffOptions, ObserveExtractError, ObserveExtractErrorKind, ObserveExtractExtraction, ObserveExtractExtractionInput, ObserveExtractObservation, ObserveExtractObservationIdentity, ObserveExtractOutcome, ObserveExtractProviderFailure, ObserveExtractRecorder, ObserveExtractResult, ObserveExtractSource, ObserveExtractSourceSnapshot, } from "./observe-extract-diff.js";
|
package/dist/src/index.js
CHANGED
|
@@ -2,9 +2,11 @@ export { runCli } from "./cli.js";
|
|
|
2
2
|
export { createCheckRunner } from "./check-runner.js";
|
|
3
3
|
export { defaultProviderResolver } from "./provider-resolution.js";
|
|
4
4
|
export { loadRegistry, LookoutRegistry, parseRegistry, RegistryValidationError, } from "./registry.js";
|
|
5
|
-
export { createLookoutSnapshotStore } from "./snapshot-store.js";
|
|
5
|
+
export { createLookoutSnapshotStore, resolveLookoutSnapshot, } from "./snapshot-store.js";
|
|
6
6
|
export { canonicalValueKey } from "./canonical-value.js";
|
|
7
7
|
export { compareStructural, diffKeyedMultiset } from "./structural-diff.js";
|
|
8
8
|
export { diffProposalSets, extractionProposalIdentity } from "./proposal-diff.js";
|
|
9
9
|
export { createObservationStore } from "./observation-store.js";
|
|
10
|
-
export {
|
|
10
|
+
export { createDriftEmitter } from "./drift-emission.js";
|
|
11
|
+
export { checkSchemaCoverage } from "./coverage.js";
|
|
12
|
+
export { createObserveExtractDiff } from "./observe-extract-diff.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ExtractionPartial, ExtractionProviderFailure, ExtractionResult, PreparedArtifact } from "@kontourai/traverse";
|
|
2
|
+
import type { CheckResult } from "./check-result.js";
|
|
3
|
+
import type { ProposalSetObservation } from "./proposal-diff.js";
|
|
4
|
+
import type { LookoutSource } from "./registry.js";
|
|
5
|
+
/** Acquisition is supplied by the caller; Lookout does not add another fetcher. */
|
|
6
|
+
export interface ObserveExtractAcquisition {
|
|
7
|
+
check(source: LookoutSource): Promise<CheckResult>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Extraction is supplied by the caller. The input carries only immutable source
|
|
11
|
+
* identity, leaving snapshot resolution, preparation, and provider selection
|
|
12
|
+
* outside Lookout.
|
|
13
|
+
*/
|
|
14
|
+
export interface ObserveExtractExtraction {
|
|
15
|
+
extract(input: ObserveExtractExtractionInput): Promise<ExtractionResult>;
|
|
16
|
+
}
|
|
17
|
+
export interface ObserveExtractExtractionInput {
|
|
18
|
+
readonly source: LookoutSource;
|
|
19
|
+
readonly snapshotRef: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ObserveExtractAttempt {
|
|
22
|
+
readonly extractedAt: string;
|
|
23
|
+
readonly providerCalls: number;
|
|
24
|
+
readonly totalTokensUsed: number;
|
|
25
|
+
readonly partial?: ExtractionPartial;
|
|
26
|
+
readonly providerFailures?: readonly ObserveExtractProviderFailure[];
|
|
27
|
+
}
|
|
28
|
+
/** Provider-neutral durable classification; diagnostic payloads stay at the capability boundary. */
|
|
29
|
+
export interface ObserveExtractProviderFailure {
|
|
30
|
+
readonly kind: ExtractionProviderFailure["kind"];
|
|
31
|
+
readonly retryable: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface ObserveExtractSourceSnapshot {
|
|
34
|
+
readonly priorSnapshotRef: string | null;
|
|
35
|
+
readonly currentSnapshotRef: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ObserveExtractSource {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly url: string;
|
|
40
|
+
readonly kind: LookoutSource["kind"];
|
|
41
|
+
}
|
|
42
|
+
export type ObserveExtractOutcome = "acquisition-error" | "unchanged" | "completed" | "partial" | "partial-provider-failure" | "provider-failure" | "extraction-failure";
|
|
43
|
+
/**
|
|
44
|
+
* One source observation, ready for caller-owned durable recording. Raw
|
|
45
|
+
* provider responses and source bodies are intentionally not copied here.
|
|
46
|
+
*/
|
|
47
|
+
export interface ObserveExtractObservation {
|
|
48
|
+
readonly source: ObserveExtractSource;
|
|
49
|
+
readonly check: CheckResult;
|
|
50
|
+
readonly outcome: ObserveExtractOutcome;
|
|
51
|
+
readonly sourceSnapshot: ObserveExtractSourceSnapshot | null;
|
|
52
|
+
readonly preparedArtifact: PreparedArtifact | null;
|
|
53
|
+
readonly proposalSet: ProposalSetObservation | null;
|
|
54
|
+
readonly attempt: ObserveExtractAttempt | null;
|
|
55
|
+
}
|
|
56
|
+
export interface ObserveExtractObservationIdentity {
|
|
57
|
+
readonly observationId: string;
|
|
58
|
+
readonly priorObservationId: string | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Lookout passes each completed observation to a caller-owned recorder. The
|
|
62
|
+
* recorder controls durable storage and continuity while this composition never
|
|
63
|
+
* supplies or configures the injected acquisition or extraction capabilities.
|
|
64
|
+
*/
|
|
65
|
+
export interface ObserveExtractRecorder {
|
|
66
|
+
record(observation: ObserveExtractObservation): Promise<ObserveExtractObservationIdentity>;
|
|
67
|
+
}
|
|
68
|
+
export interface ObserveExtractDiffOptions {
|
|
69
|
+
readonly acquisition: ObserveExtractAcquisition;
|
|
70
|
+
readonly extraction: ObserveExtractExtraction;
|
|
71
|
+
readonly recorder: ObserveExtractRecorder;
|
|
72
|
+
}
|
|
73
|
+
export type ObserveExtractErrorKind = "acquisition-threw" | "recording-failed" | "dependency-contract";
|
|
74
|
+
export interface ObserveExtractError {
|
|
75
|
+
readonly kind: ObserveExtractErrorKind;
|
|
76
|
+
readonly message: string;
|
|
77
|
+
readonly cause?: unknown;
|
|
78
|
+
}
|
|
79
|
+
export type ObserveExtractResult = {
|
|
80
|
+
readonly ok: true;
|
|
81
|
+
readonly value: ObserveExtractObservation & ObserveExtractObservationIdentity;
|
|
82
|
+
} | {
|
|
83
|
+
readonly ok: false;
|
|
84
|
+
readonly error: ObserveExtractError;
|
|
85
|
+
readonly observation?: ObserveExtractObservation;
|
|
86
|
+
};
|
|
87
|
+
export interface ObserveExtractDiff {
|
|
88
|
+
observe(source: LookoutSource): Promise<ObserveExtractResult>;
|
|
89
|
+
}
|
|
90
|
+
export declare function createObserveExtractDiff(options: ObserveExtractDiffOptions): ObserveExtractDiff;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { validatePreparedArtifact } from "@kontourai/traverse";
|
|
2
|
+
export function createObserveExtractDiff(options) {
|
|
3
|
+
return {
|
|
4
|
+
async observe(source) {
|
|
5
|
+
try {
|
|
6
|
+
let check;
|
|
7
|
+
try {
|
|
8
|
+
check = await options.acquisition.check(source);
|
|
9
|
+
}
|
|
10
|
+
catch (cause) {
|
|
11
|
+
return { ok: false, error: error("acquisition-threw", "Acquisition capability threw", cause) };
|
|
12
|
+
}
|
|
13
|
+
if (!isCheckResult(check)) {
|
|
14
|
+
return { ok: false, error: error("dependency-contract", "Acquisition capability returned an invalid check result") };
|
|
15
|
+
}
|
|
16
|
+
if (check.sourceId !== source.id || check.sourceUrl !== source.url) {
|
|
17
|
+
return { ok: false, error: error("dependency-contract", "Acquisition result does not identify the requested source") };
|
|
18
|
+
}
|
|
19
|
+
if (check.kind === "error") {
|
|
20
|
+
return record(options.recorder, baseObservation(source, check, "acquisition-error", null, null, null, null));
|
|
21
|
+
}
|
|
22
|
+
if (check.kind === "unchanged-304" || check.kind === "unchanged-hash") {
|
|
23
|
+
return record(options.recorder, baseObservation(source, check, "unchanged", snapshotFor(check), null, null, null));
|
|
24
|
+
}
|
|
25
|
+
const sourceSnapshot = snapshotFor(check);
|
|
26
|
+
let extraction;
|
|
27
|
+
try {
|
|
28
|
+
extraction = await options.extraction.extract({ source, snapshotRef: sourceSnapshot.currentSnapshotRef });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return record(options.recorder, baseObservation(source, check, "extraction-failure", sourceSnapshot, null, null, null));
|
|
32
|
+
}
|
|
33
|
+
if (!isExtractionResult(extraction)) {
|
|
34
|
+
return { ok: false, error: error("dependency-contract", "Extraction capability returned an invalid extraction result") };
|
|
35
|
+
}
|
|
36
|
+
const attempt = attemptFor(extraction);
|
|
37
|
+
const proposalSet = {
|
|
38
|
+
sourceId: source.id,
|
|
39
|
+
snapshotRef: sourceSnapshot.currentSnapshotRef,
|
|
40
|
+
observedAt: extraction.extractedAt,
|
|
41
|
+
proposals: extraction.proposals,
|
|
42
|
+
};
|
|
43
|
+
const outcome = outcomeFor(extraction);
|
|
44
|
+
if (outcome !== "extraction-failure") {
|
|
45
|
+
if (extraction.preparedArtifact === undefined) {
|
|
46
|
+
return { ok: false, error: error("dependency-contract", "Extraction result is missing its prepared artifact") };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (extraction.preparedArtifact !== undefined) {
|
|
50
|
+
const validation = validatePreparedArtifact(extraction.preparedArtifact);
|
|
51
|
+
if (validation.status !== "valid") {
|
|
52
|
+
return { ok: false, error: error("dependency-contract", `Extraction result has an invalid prepared artifact: ${validation.status}`) };
|
|
53
|
+
}
|
|
54
|
+
if (validation.artifact.sourceSnapshotRef !== sourceSnapshot.currentSnapshotRef) {
|
|
55
|
+
return { ok: false, error: error("dependency-contract", "Prepared artifact does not identify the current source snapshot") };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return record(options.recorder, baseObservation(source, check, outcome, sourceSnapshot, extraction.preparedArtifact ?? null, proposalSet, attempt));
|
|
59
|
+
}
|
|
60
|
+
catch (cause) {
|
|
61
|
+
return { ok: false, error: error("dependency-contract", "Observe-extract composition could not inspect a capability result", cause) };
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function baseObservation(source, check, outcome, sourceSnapshot, preparedArtifact, proposalSet, attempt) {
|
|
67
|
+
return {
|
|
68
|
+
source: { id: source.id, url: source.url, kind: source.kind },
|
|
69
|
+
check,
|
|
70
|
+
outcome,
|
|
71
|
+
sourceSnapshot,
|
|
72
|
+
preparedArtifact,
|
|
73
|
+
proposalSet,
|
|
74
|
+
attempt,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function record(recorder, observation) {
|
|
78
|
+
try {
|
|
79
|
+
const identity = await recorder.record(observation);
|
|
80
|
+
if (!isIdentity(identity)) {
|
|
81
|
+
return { ok: false, error: error("dependency-contract", "Observation recorder returned an invalid identity"), observation };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
value: {
|
|
86
|
+
...observation,
|
|
87
|
+
observationId: identity.observationId,
|
|
88
|
+
priorObservationId: identity.priorObservationId,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (cause) {
|
|
93
|
+
return { ok: false, error: error("recording-failed", "Observation recorder failed", cause), observation };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function snapshotFor(check) {
|
|
97
|
+
if (check.kind === "unchanged-304")
|
|
98
|
+
return { priorSnapshotRef: check.snapshotRef, currentSnapshotRef: check.snapshotRef };
|
|
99
|
+
return { priorSnapshotRef: check.priorSnapshotRef, currentSnapshotRef: check.currentSnapshotRef };
|
|
100
|
+
}
|
|
101
|
+
function attemptFor(result) {
|
|
102
|
+
return {
|
|
103
|
+
extractedAt: result.extractedAt,
|
|
104
|
+
providerCalls: result.providerCalls,
|
|
105
|
+
totalTokensUsed: result.totalTokensUsed,
|
|
106
|
+
...(result.partial === undefined ? {} : { partial: result.partial }),
|
|
107
|
+
...(result.providerFailures === undefined ? {} : {
|
|
108
|
+
providerFailures: result.providerFailures.map(({ kind, retryable }) => ({ kind, retryable })),
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function outcomeFor(result) {
|
|
113
|
+
const hasProviderFailures = (result.providerFailures?.length ?? 0) > 0;
|
|
114
|
+
if (result.partial !== undefined && hasProviderFailures)
|
|
115
|
+
return "partial-provider-failure";
|
|
116
|
+
if (hasProviderFailures)
|
|
117
|
+
return "provider-failure";
|
|
118
|
+
if (result.error !== undefined)
|
|
119
|
+
return "extraction-failure";
|
|
120
|
+
return result.partial === undefined ? "completed" : "partial";
|
|
121
|
+
}
|
|
122
|
+
function isCheckResult(value) {
|
|
123
|
+
if (typeof value !== "object" || value === null)
|
|
124
|
+
return false;
|
|
125
|
+
const result = value;
|
|
126
|
+
if (typeof result.sourceId !== "string" || typeof result.sourceUrl !== "string" ||
|
|
127
|
+
typeof result.checkedAt !== "string" || !Array.isArray(result.warnings))
|
|
128
|
+
return false;
|
|
129
|
+
if (result.kind === "unchanged-304")
|
|
130
|
+
return typeof result.snapshotRef === "string";
|
|
131
|
+
if (result.kind === "unchanged-hash")
|
|
132
|
+
return typeof result.priorSnapshotRef === "string" && typeof result.currentSnapshotRef === "string";
|
|
133
|
+
if (result.kind === "changed")
|
|
134
|
+
return (result.priorSnapshotRef === null || typeof result.priorSnapshotRef === "string") &&
|
|
135
|
+
typeof result.currentSnapshotRef === "string" && (result.changeBasis === "initial" || result.changeBasis === "hash");
|
|
136
|
+
return result.kind === "error" && (result.origin === "forage" || result.origin === "lookout") && result.error !== undefined;
|
|
137
|
+
}
|
|
138
|
+
function isExtractionResult(value) {
|
|
139
|
+
if (typeof value !== "object" || value === null)
|
|
140
|
+
return false;
|
|
141
|
+
const result = value;
|
|
142
|
+
return Array.isArray(result.proposals) && typeof result.extractedAt === "string" &&
|
|
143
|
+
Number.isFinite(result.providerCalls) && Number.isFinite(result.totalTokensUsed);
|
|
144
|
+
}
|
|
145
|
+
function isIdentity(value) {
|
|
146
|
+
if (typeof value !== "object" || value === null)
|
|
147
|
+
return false;
|
|
148
|
+
const identity = value;
|
|
149
|
+
return typeof identity.observationId === "string" && identity.observationId !== "" &&
|
|
150
|
+
(identity.priorObservationId === null || (typeof identity.priorObservationId === "string" && identity.priorObservationId !== ""));
|
|
151
|
+
}
|
|
152
|
+
function error(kind, text, cause) {
|
|
153
|
+
return { kind, message: text, ...(cause === undefined ? {} : { cause }) };
|
|
154
|
+
}
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import type { TargetFieldSchema } from "@kontourai/traverse";
|
|
2
|
-
export type LookoutSourceKind = "web-page" | "api-record";
|
|
2
|
+
export type LookoutSourceKind = "web-page" | "api-record" | "structured-file";
|
|
3
|
+
export type StructuredFileFormat = "yaml" | "json" | "csv";
|
|
3
4
|
export type RenderPolicy = "never" | "on-shell-warning" | "always";
|
|
4
|
-
|
|
5
|
+
interface LookoutSourceBase {
|
|
5
6
|
id: string;
|
|
6
|
-
kind: LookoutSourceKind;
|
|
7
7
|
url: string;
|
|
8
|
-
targetSchema: TargetFieldSchema[];
|
|
9
8
|
cadenceHint: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ExtractableLookoutSource extends LookoutSourceBase {
|
|
11
|
+
kind: "web-page" | "api-record";
|
|
12
|
+
targetSchema: TargetFieldSchema[];
|
|
10
13
|
renderPolicy: RenderPolicy;
|
|
14
|
+
format?: never;
|
|
15
|
+
}
|
|
16
|
+
export interface StructuredFileLookoutSource extends LookoutSourceBase {
|
|
17
|
+
kind: "structured-file";
|
|
18
|
+
format: StructuredFileFormat;
|
|
19
|
+
targetSchema?: never;
|
|
20
|
+
renderPolicy?: never;
|
|
11
21
|
}
|
|
22
|
+
export type LookoutSource = ExtractableLookoutSource | StructuredFileLookoutSource;
|
|
12
23
|
export interface LookoutRegistryDocument {
|
|
13
24
|
version: 1;
|
|
14
25
|
sources: LookoutSource[];
|
|
@@ -27,3 +38,4 @@ export declare class LookoutRegistry {
|
|
|
27
38
|
}
|
|
28
39
|
export declare function loadRegistry(registryPath?: string): Promise<LookoutRegistry>;
|
|
29
40
|
export declare function parseRegistry(value: unknown): LookoutRegistry;
|
|
41
|
+
export {};
|
package/dist/src/registry.js
CHANGED
|
@@ -49,55 +49,120 @@ export function parseRegistry(value) {
|
|
|
49
49
|
const ids = new Map();
|
|
50
50
|
const sources = [];
|
|
51
51
|
value.sources.forEach((candidate, index) => {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const id = candidate.id;
|
|
58
|
-
const label = typeof id === "string" && id.trim() ? `${context} (id "${id}")` : context;
|
|
59
|
-
if (typeof id !== "string" || id.trim() === "") {
|
|
60
|
-
issues.push(`${context}.id must be a non-empty string`);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
const first = ids.get(id);
|
|
64
|
-
if (first !== undefined)
|
|
65
|
-
issues.push(`${label}.id duplicates sources[${first}].id`);
|
|
66
|
-
else
|
|
67
|
-
ids.set(id, index);
|
|
68
|
-
}
|
|
69
|
-
if (candidate.kind !== "web-page" && candidate.kind !== "api-record") {
|
|
70
|
-
issues.push(`${label}.kind must be "web-page" or "api-record"`);
|
|
71
|
-
}
|
|
72
|
-
if (!isHttpUrl(candidate.url)) {
|
|
73
|
-
issues.push(`${label}.url must be an absolute HTTP(S) URL`);
|
|
74
|
-
}
|
|
75
|
-
if (typeof candidate.cadenceHint !== "string" || candidate.cadenceHint.trim() === "") {
|
|
76
|
-
issues.push(`${label}.cadenceHint must be a non-empty string`);
|
|
77
|
-
}
|
|
78
|
-
if (candidate.renderPolicy !== "never" &&
|
|
79
|
-
candidate.renderPolicy !== "on-shell-warning" &&
|
|
80
|
-
candidate.renderPolicy !== "always") {
|
|
81
|
-
issues.push(`${label}.renderPolicy must be "never", "on-shell-warning", or "always"`);
|
|
82
|
-
}
|
|
83
|
-
validateTargetSchema(candidate.targetSchema, label, issues);
|
|
84
|
-
if (typeof id === "string" && id.trim() !== "" &&
|
|
85
|
-
(candidate.kind === "web-page" || candidate.kind === "api-record") &&
|
|
86
|
-
isHttpUrl(candidate.url) &&
|
|
87
|
-
typeof candidate.cadenceHint === "string" && candidate.cadenceHint.trim() !== "" &&
|
|
88
|
-
(candidate.renderPolicy === "never" || candidate.renderPolicy === "on-shell-warning" || candidate.renderPolicy === "always") &&
|
|
89
|
-
Array.isArray(candidate.targetSchema)) {
|
|
90
|
-
sources.push(candidate);
|
|
91
|
-
}
|
|
52
|
+
const source = parseSource(candidate, index, ids, issues);
|
|
53
|
+
if (source !== undefined)
|
|
54
|
+
sources.push(source);
|
|
92
55
|
});
|
|
93
56
|
if (issues.length > 0)
|
|
94
57
|
throw new RegistryValidationError(issues);
|
|
95
58
|
return new LookoutRegistry(sources);
|
|
96
59
|
}
|
|
60
|
+
function parseSource(candidate, index, ids, issues) {
|
|
61
|
+
const context = `sources[${index}]`;
|
|
62
|
+
if (!isRecord(candidate)) {
|
|
63
|
+
issues.push(`${context} must be an object`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const firstIssue = issues.length;
|
|
67
|
+
const common = validateCommonSource(candidate, index, ids, issues);
|
|
68
|
+
const source = candidate.kind === "structured-file"
|
|
69
|
+
? parseStructuredFileSource(candidate, common, issues)
|
|
70
|
+
: candidate.kind === "web-page" || candidate.kind === "api-record"
|
|
71
|
+
? parseExtractableSource(candidate, candidate.kind, common, issues)
|
|
72
|
+
: invalidSourceKind(candidate, common.label, issues);
|
|
73
|
+
return issues.length === firstIssue ? source : undefined;
|
|
74
|
+
}
|
|
75
|
+
function validateCommonSource(candidate, index, ids, issues) {
|
|
76
|
+
const context = `sources[${index}]`;
|
|
77
|
+
const id = typeof candidate.id === "string" && candidate.id.trim() !== "" ? candidate.id : undefined;
|
|
78
|
+
const label = id === undefined ? context : `${context} (id "${id}")`;
|
|
79
|
+
if (id === undefined) {
|
|
80
|
+
issues.push(`${context}.id must be a non-empty string`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const first = ids.get(id);
|
|
84
|
+
if (first !== undefined)
|
|
85
|
+
issues.push(`${label}.id duplicates sources[${first}].id`);
|
|
86
|
+
else
|
|
87
|
+
ids.set(id, index);
|
|
88
|
+
}
|
|
89
|
+
const url = isHttpUrl(candidate.url) ? candidate.url : undefined;
|
|
90
|
+
if (url === undefined) {
|
|
91
|
+
issues.push(`${label}.url must be an absolute HTTP(S) URL`);
|
|
92
|
+
}
|
|
93
|
+
const cadenceHint = typeof candidate.cadenceHint === "string" && candidate.cadenceHint.trim() !== ""
|
|
94
|
+
? candidate.cadenceHint
|
|
95
|
+
: undefined;
|
|
96
|
+
if (cadenceHint === undefined) {
|
|
97
|
+
issues.push(`${label}.cadenceHint must be a non-empty string`);
|
|
98
|
+
}
|
|
99
|
+
return { id, url, cadenceHint, label };
|
|
100
|
+
}
|
|
101
|
+
function parseStructuredFileSource(candidate, common, issues) {
|
|
102
|
+
if (!isStructuredFileFormat(candidate.format)) {
|
|
103
|
+
issues.push(`${common.label}.format must be "yaml", "json", or "csv"`);
|
|
104
|
+
}
|
|
105
|
+
if (candidate.targetSchema !== undefined) {
|
|
106
|
+
issues.push(`${common.label}.targetSchema is not allowed for structured-file sources`);
|
|
107
|
+
}
|
|
108
|
+
if (candidate.renderPolicy !== undefined) {
|
|
109
|
+
issues.push(`${common.label}.renderPolicy is not allowed for structured-file sources`);
|
|
110
|
+
}
|
|
111
|
+
if (common.id === undefined || common.url === undefined || common.cadenceHint === undefined ||
|
|
112
|
+
!isStructuredFileFormat(candidate.format))
|
|
113
|
+
return undefined;
|
|
114
|
+
return {
|
|
115
|
+
id: common.id,
|
|
116
|
+
kind: "structured-file",
|
|
117
|
+
format: candidate.format,
|
|
118
|
+
url: common.url,
|
|
119
|
+
cadenceHint: common.cadenceHint,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function parseExtractableSource(candidate, kind, common, issues) {
|
|
123
|
+
if (!isRenderPolicy(candidate.renderPolicy)) {
|
|
124
|
+
issues.push(`${common.label}.renderPolicy must be "never", "on-shell-warning", or "always"`);
|
|
125
|
+
}
|
|
126
|
+
const targetSchema = validateTargetSchema(candidate.targetSchema, common.label, issues)
|
|
127
|
+
? candidate.targetSchema
|
|
128
|
+
: undefined;
|
|
129
|
+
if (candidate.format !== undefined) {
|
|
130
|
+
issues.push(`${common.label}.format is only allowed for structured-file sources`);
|
|
131
|
+
}
|
|
132
|
+
if (common.id === undefined || common.url === undefined || common.cadenceHint === undefined ||
|
|
133
|
+
!isRenderPolicy(candidate.renderPolicy) || targetSchema === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
return {
|
|
136
|
+
id: common.id,
|
|
137
|
+
kind,
|
|
138
|
+
url: common.url,
|
|
139
|
+
targetSchema,
|
|
140
|
+
cadenceHint: common.cadenceHint,
|
|
141
|
+
renderPolicy: candidate.renderPolicy,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function invalidSourceKind(candidate, label, issues) {
|
|
145
|
+
issues.push(`${label}.kind must be "web-page", "api-record", or "structured-file"`);
|
|
146
|
+
if (!isRenderPolicy(candidate.renderPolicy)) {
|
|
147
|
+
issues.push(`${label}.renderPolicy must be "never", "on-shell-warning", or "always"`);
|
|
148
|
+
}
|
|
149
|
+
validateTargetSchema(candidate.targetSchema, label, issues);
|
|
150
|
+
if (candidate.format !== undefined) {
|
|
151
|
+
issues.push(`${label}.format is only allowed for structured-file sources`);
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
function isStructuredFileFormat(value) {
|
|
156
|
+
return value === "yaml" || value === "json" || value === "csv";
|
|
157
|
+
}
|
|
158
|
+
function isRenderPolicy(value) {
|
|
159
|
+
return value === "never" || value === "on-shell-warning" || value === "always";
|
|
160
|
+
}
|
|
97
161
|
function validateTargetSchema(value, context, issues) {
|
|
162
|
+
const firstIssue = issues.length;
|
|
98
163
|
if (!Array.isArray(value)) {
|
|
99
164
|
issues.push(`${context}.targetSchema must be an array`);
|
|
100
|
-
return;
|
|
165
|
+
return false;
|
|
101
166
|
}
|
|
102
167
|
const allowedTypes = new Set(["string", "number", "boolean", "date", "enum", "array", "object"]);
|
|
103
168
|
value.forEach((field, index) => {
|
|
@@ -124,6 +189,7 @@ function validateTargetSchema(value, context, issues) {
|
|
|
124
189
|
issues.push(`${label}.inferenceType must be "explicit" or "inferred"`);
|
|
125
190
|
}
|
|
126
191
|
});
|
|
192
|
+
return issues.length === firstIssue;
|
|
127
193
|
}
|
|
128
194
|
function isRecord(value) {
|
|
129
195
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
import type { SnapshotStore } from "@kontourai/
|
|
1
|
+
import type { SnapshotStore } from "@kontourai/forage";
|
|
2
|
+
import { type SnapshotSourceRefResolution } from "@kontourai/forage/fetch";
|
|
2
3
|
export declare function createLookoutSnapshotStore(root?: string): SnapshotStore;
|
|
4
|
+
export type ResolveLookoutSnapshotOptions = {
|
|
5
|
+
store: SnapshotStore;
|
|
6
|
+
root?: never;
|
|
7
|
+
} | {
|
|
8
|
+
store?: never;
|
|
9
|
+
root?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Replay one Lookout-emitted durable reference without any network access. */
|
|
12
|
+
export declare function resolveLookoutSnapshot(reference: string, options?: ResolveLookoutSnapshotOptions): Promise<SnapshotSourceRefResolution>;
|