@kontourai/lookout 0.3.4 → 0.3.6

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 CHANGED
@@ -279,6 +279,40 @@ Forage's SSRF-guarded fetcher), `fetchOptions`, and
279
279
  `clock` — so checks run with no live network or timers in tests. Injecting either
280
280
  `fetchSource` or `fetchOptions.fetch` overrides the default guarded transport.
281
281
 
282
+ ### Bring your own source store
283
+
284
+ The registry file is a convenience, not the contract. What check
285
+ classification, drift emission, and observation lineage actually consume is
286
+ source identity — enumerate sources, resolve one by exact id — and that
287
+ contract is the `SourceStore` interface: `list()` and `get(id)`, each
288
+ returning its value directly or as a promise. `LookoutRegistry` (what
289
+ `loadRegistry` returns) is the file-backed implementation; nothing downstream
290
+ can tell implementations apart.
291
+
292
+ Two application shapes motivate the seam. An application whose canonical
293
+ source of truth is its own database — reviewed rows in Postgres or D1, not a
294
+ JSON file — builds `LookoutSource` values from those rows and should not have
295
+ to adopt a second store of record to inherit lineage, continuity, and CHECK
296
+ classification. And a host that owns its own acquisition and run state wants
297
+ to hand Lookout sources it already holds in memory. Both were previously
298
+ re-implementing identity handling around the registry; either can now do:
299
+
300
+ ```ts
301
+ import { inMemorySourceStore } from "@kontourai/lookout";
302
+
303
+ const store = inMemorySourceStore(sourcesBuiltFromYourRows);
304
+ const results = await runner.checkAll(await store.list());
305
+ ```
306
+
307
+ `inMemorySourceStore` applies exactly the validation `loadRegistry` applies
308
+ to a file — duplicate ids, URL shape, per-kind field rules, every issue
309
+ reported at once — so source-id uniqueness is enforced structurally rather
310
+ than by review convention. A database-backed application can also implement
311
+ `SourceStore` directly (both methods may be async) and query lazily;
312
+ `runCli`'s `loadRegistry` option accepts any `SourceStore`. Keep ids stable
313
+ either way: snapshot identity and observation lineage key off `id`, so
314
+ changing one starts a new history.
315
+
282
316
  ## Observe changed sources without re-extracting unchanged ones
283
317
 
284
318
  `createObserveExtractDiff` is an optional library composition for callers that
package/dist/src/cli.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import type { Writable } from "node:stream";
2
2
  import { type CheckRunner } from "./check-runner.js";
3
- import { type LookoutRegistry } from "./registry.js";
3
+ import type { SourceStore } from "./source-store.js";
4
4
  import { type DriftResult } from "./drift-emission.js";
5
5
  export interface RunCliOptions {
6
6
  argv?: string[];
7
7
  stdout?: Pick<Writable, "write">;
8
8
  stderr?: Pick<Writable, "write">;
9
- loadRegistry?: (path?: string) => Promise<LookoutRegistry>;
9
+ loadRegistry?(path?: string): Promise<SourceStore>;
10
10
  runner?: CheckRunner;
11
11
  readObservation?: (path: string) => Promise<unknown>;
12
- emitDrift?: (sourceId: string, value: unknown, registry: LookoutRegistry, observationRoot?: string) => Promise<DriftResult>;
12
+ emitDrift?(sourceId: string, value: unknown, store: SourceStore, observationRoot?: string): Promise<DriftResult>;
13
13
  }
14
14
  export declare function runCli(options?: RunCliOptions): Promise<number>;
package/dist/src/cli.js CHANGED
@@ -13,16 +13,16 @@ export async function runCli(options = {}) {
13
13
  stderr.write(`${parsed}\n`);
14
14
  return 2;
15
15
  }
16
- let registry;
16
+ let store;
17
17
  try {
18
- registry = await (options.loadRegistry ?? loadRegistry)(parsed.registryPath);
18
+ store = await (options.loadRegistry ?? loadRegistry)(parsed.registryPath);
19
19
  }
20
20
  catch (error) {
21
21
  stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
22
22
  return 1;
23
23
  }
24
24
  if (parsed.command === "emit-drift") {
25
- const source = registry.get(parsed.id);
25
+ const source = await store.get(parsed.id);
26
26
  if (!source) {
27
27
  stderr.write(`Unknown source id: ${parsed.id}\n`);
28
28
  return 1;
@@ -35,7 +35,7 @@ export async function runCli(options = {}) {
35
35
  stderr.write(`Could not read observation: ${error instanceof Error ? error.message : String(error)}\n`);
36
36
  return 1;
37
37
  }
38
- const result = await (options.emitDrift ?? emitDrift)(parsed.id, value, registry, parsed.observationRoot);
38
+ const result = await (options.emitDrift ?? emitDrift)(parsed.id, value, store, parsed.observationRoot);
39
39
  if (!result.ok) {
40
40
  stderr.write(`${result.error.kind}: ${result.error.message}\n`);
41
41
  return 1;
@@ -47,12 +47,12 @@ export async function runCli(options = {}) {
47
47
  store: createLookoutSnapshotStore(parsed.snapshotRoot),
48
48
  });
49
49
  if (parsed.all) {
50
- const results = await runner.checkAll(registry.list());
50
+ const results = await runner.checkAll(await store.list());
51
51
  for (const result of results)
52
52
  stdout.write(`${JSON.stringify(result)}\n`);
53
53
  return 0;
54
54
  }
55
- const source = registry.get(parsed.id);
55
+ const source = await store.get(parsed.id);
56
56
  if (!source) {
57
57
  stderr.write(`Unknown source id: ${parsed.id}\n`);
58
58
  return 1;
@@ -164,10 +164,10 @@ function cliEntities(observation) {
164
164
  }
165
165
  return [...grouped].map(([key, proposals]) => ({ key, proposals }));
166
166
  }
167
- async function emitDrift(sourceId, value, registry, observationRoot) {
167
+ async function emitDrift(sourceId, value, store, observationRoot) {
168
168
  if (!value || typeof value !== "object" || Array.isArray(value))
169
169
  return { ok: false, error: { kind: "invalid-input", message: "Observation document must be an object" } };
170
170
  const document = value;
171
- const source = registry.get(sourceId);
171
+ const source = (await store.get(sourceId));
172
172
  return createDriftEmitter({ store: createObservationStore({ root: observationRoot }) }).emit({ source, current: document.observation, check: document.check, callbacks: { selectEntities: cliEntities, entityIdentity: (entity) => entity.key, proposalsFor: (entity) => entity.proposals, fieldIdentity: (_entity, proposal) => proposal.fieldPath } });
173
173
  }
@@ -6,6 +6,8 @@ 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 { inMemorySourceStore } from "./source-store.js";
10
+ export type { SourceStore } from "./source-store.js";
9
11
  export type { ExtractableLookoutSource, LookoutRegistryDocument, LookoutSource, LookoutSourceKind, RenderPolicy, StructuredFileFormat, StructuredFileLookoutSource, } from "./registry.js";
10
12
  export { createLookoutSnapshotStore, resolveLookoutSnapshot, } from "./snapshot-store.js";
11
13
  export type { ResolveLookoutSnapshotOptions } from "./snapshot-store.js";
package/dist/src/index.js CHANGED
@@ -2,6 +2,7 @@ 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 { inMemorySourceStore } from "./source-store.js";
5
6
  export { createLookoutSnapshotStore, resolveLookoutSnapshot, } from "./snapshot-store.js";
6
7
  export { canonicalValueKey } from "./canonical-value.js";
7
8
  export { compareStructural, diffKeyedMultiset } from "./structural-diff.js";
@@ -0,0 +1,35 @@
1
+ import { type LookoutRegistry, type LookoutSource } from "./registry.js";
2
+ /**
3
+ * The seam between where sources live and everything Lookout does with them.
4
+ *
5
+ * Check classification, drift emission, and observation lineage all key off a
6
+ * source's identity (`LookoutSource.id`), not off the file the source was
7
+ * declared in. A `SourceStore` is the minimal contract those parts actually
8
+ * consume: enumerate sources and resolve one by exact id. The bundled file
9
+ * registry (`loadRegistry` / `LookoutRegistry`) is one implementation; an
10
+ * application whose canonical source of truth is its own database can
11
+ * implement this directly, or materialize rows through
12
+ * {@link inMemorySourceStore}, and still inherit lineage, continuity, and
13
+ * CHECK classification.
14
+ *
15
+ * Methods may return values synchronously or as promises so that an
16
+ * in-memory store stays allocation-free while a database-backed store can
17
+ * query lazily. Callers must `await` both methods.
18
+ */
19
+ export interface SourceStore {
20
+ /** Every source, in the store's canonical order. */
21
+ list(): readonly LookoutSource[] | Promise<readonly LookoutSource[]>;
22
+ /** Exact-id lookup; `undefined` when the id is not present. */
23
+ get(id: string): LookoutSource | undefined | Promise<LookoutSource | undefined>;
24
+ }
25
+ /**
26
+ * Build a validated in-memory {@link SourceStore} from sources an application
27
+ * constructed itself (for example, from its own database rows).
28
+ *
29
+ * Runs exactly the validation `loadRegistry` applies to a registry file —
30
+ * duplicate ids, URL shape, per-kind field rules — and throws the same
31
+ * `RegistryValidationError` reporting every issue at once. The returned store
32
+ * is a `LookoutRegistry`, so file-backed and in-memory sources are
33
+ * indistinguishable downstream.
34
+ */
35
+ export declare function inMemorySourceStore(sources: readonly LookoutSource[]): LookoutRegistry;
@@ -0,0 +1,14 @@
1
+ import { parseRegistry } from "./registry.js";
2
+ /**
3
+ * Build a validated in-memory {@link SourceStore} from sources an application
4
+ * constructed itself (for example, from its own database rows).
5
+ *
6
+ * Runs exactly the validation `loadRegistry` applies to a registry file —
7
+ * duplicate ids, URL shape, per-kind field rules — and throws the same
8
+ * `RegistryValidationError` reporting every issue at once. The returned store
9
+ * is a `LookoutRegistry`, so file-backed and in-memory sources are
10
+ * indistinguishable downstream.
11
+ */
12
+ export function inMemorySourceStore(sources) {
13
+ return parseRegistry({ version: 1, sources });
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/lookout",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "A small source registry and drift-check runner built on Forage snapshots.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,23 +35,26 @@
35
35
  "build": "rm -rf dist && tsc",
36
36
  "prepare": "npm run build",
37
37
  "typecheck": "tsc --noEmit",
38
- "test": "npm run build && node --test dist/tests/*.test.js",
38
+ "test": "npm run build && node scripts/run-tests.mjs",
39
39
  "verify": "npm run check:content-boundary && npm run check:decisions && npm run typecheck && npm test && npm run check:pack && npm run check:consumer",
40
40
  "check:content-boundary": "node scripts/check-content-boundary.cjs",
41
41
  "check:decisions": "node scripts/check-decisions.cjs check",
42
42
  "gen:decisions-index": "node scripts/check-decisions.cjs gen-index",
43
43
  "check:pack": "node scripts/check-package-contents.mjs",
44
- "check:consumer": "node scripts/check-installed-consumer.mjs"
44
+ "check:consumer": "node scripts/check-installed-consumer.mjs",
45
+ "workflow:sidecar": "flow-agents-workflow-sidecar",
46
+ "workflow:validate-artifacts": "flow-agents-validate-artifacts"
45
47
  },
46
48
  "dependencies": {
47
49
  "@kontourai/datum": "0.7.0",
48
50
  "@kontourai/forage": "0.5.0",
49
- "@kontourai/traverse": "0.24.0"
51
+ "@kontourai/traverse": "0.25.0"
50
52
  },
51
53
  "devDependencies": {
52
- "@kontourai/survey": "2.1.0",
53
- "@types/node": "^25.6.0",
54
- "typescript": "^5.8.0"
54
+ "@kontourai/flow-agents": "5.5.0",
55
+ "@kontourai/survey": "2.4.0",
56
+ "@types/node": "^26.1.2",
57
+ "typescript": "^7.0.2"
55
58
  },
56
59
  "engines": {
57
60
  "node": ">=22"