@chkit/plugin-ingest 0.2.0-beta.8

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.
Files changed (58) hide show
  1. package/README.md +30 -0
  2. package/dist/destination.d.ts +40 -0
  3. package/dist/destination.d.ts.map +1 -0
  4. package/dist/destination.js +63 -0
  5. package/dist/destination.js.map +1 -0
  6. package/dist/errors.d.ts +33 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/errors.js +133 -0
  9. package/dist/errors.js.map +1 -0
  10. package/dist/executor.d.ts +30 -0
  11. package/dist/executor.d.ts.map +1 -0
  12. package/dist/executor.js +538 -0
  13. package/dist/executor.js.map +1 -0
  14. package/dist/incremental.d.ts +45 -0
  15. package/dist/incremental.d.ts.map +1 -0
  16. package/dist/incremental.js +72 -0
  17. package/dist/incremental.js.map +1 -0
  18. package/dist/index.d.ts +11 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +10 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/journal.d.ts +44 -0
  23. package/dist/journal.d.ts.map +1 -0
  24. package/dist/journal.js +222 -0
  25. package/dist/journal.js.map +1 -0
  26. package/dist/loader.d.ts +12 -0
  27. package/dist/loader.d.ts.map +1 -0
  28. package/dist/loader.js +37 -0
  29. package/dist/loader.js.map +1 -0
  30. package/dist/paginate.d.ts +20 -0
  31. package/dist/paginate.d.ts.map +1 -0
  32. package/dist/paginate.js +29 -0
  33. package/dist/paginate.js.map +1 -0
  34. package/dist/plugin.d.ts +110 -0
  35. package/dist/plugin.d.ts.map +1 -0
  36. package/dist/plugin.js +255 -0
  37. package/dist/plugin.js.map +1 -0
  38. package/dist/queue.d.ts +9 -0
  39. package/dist/queue.d.ts.map +1 -0
  40. package/dist/queue.js +47 -0
  41. package/dist/queue.js.map +1 -0
  42. package/dist/registry.d.ts +48 -0
  43. package/dist/registry.d.ts.map +1 -0
  44. package/dist/registry.js +109 -0
  45. package/dist/registry.js.map +1 -0
  46. package/dist/retry.d.ts +12 -0
  47. package/dist/retry.d.ts.map +1 -0
  48. package/dist/retry.js +69 -0
  49. package/dist/retry.js.map +1 -0
  50. package/dist/testing.d.ts +16 -0
  51. package/dist/testing.d.ts.map +1 -0
  52. package/dist/testing.js +46 -0
  53. package/dist/testing.js.map +1 -0
  54. package/dist/types.d.ts +221 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +2 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +57 -0
@@ -0,0 +1,72 @@
1
+ import { IngestConfigError } from './errors.js';
2
+ /** No incremental selection: every execution reads the whole source. */
3
+ export function fullSync() {
4
+ return {
5
+ id: 'chkit.full_sync',
6
+ version: 1,
7
+ parseState: () => undefined,
8
+ plan: () => undefined,
9
+ };
10
+ }
11
+ /**
12
+ * Bundled timestamp strategy. The upper bound is fixed to the execution cutoff
13
+ * and `range.to` becomes the committed watermark only after the whole window
14
+ * has sink evidence.
15
+ */
16
+ export function timestampWindow(options) {
17
+ if (options.from === undefined) {
18
+ if (!(options.start instanceof Date) || !Number.isFinite(options.start.getTime())) {
19
+ throw new IngestConfigError('timestampWindow: start must be a valid Date.');
20
+ }
21
+ if (!Number.isFinite(options.overlapMs ?? 0) || (options.overlapMs ?? 0) < 0) {
22
+ throw new IngestConfigError('timestampWindow: overlapMs must be a finite non-negative number.');
23
+ }
24
+ }
25
+ else if (options.start !== undefined || options.overlapMs !== undefined) {
26
+ throw new IngestConfigError('timestampWindow: use either start/overlapMs or from, not both.');
27
+ }
28
+ return {
29
+ id: 'chkit.timestamp_window',
30
+ version: 1,
31
+ parseState(raw) {
32
+ if (typeof raw !== 'object' || raw === null || !('watermark' in raw) || typeof raw.watermark !== 'string') {
33
+ throw new IngestConfigError('timestampWindow: committed state has no string "watermark".');
34
+ }
35
+ if (Number.isNaN(Date.parse(raw.watermark))) {
36
+ throw new IngestConfigError(`timestampWindow: committed watermark "${raw.watermark}" is not a timestamp.`);
37
+ }
38
+ return { watermark: raw.watermark };
39
+ },
40
+ plan({ state, cutoff, range }) {
41
+ const to = range?.to ?? cutoff;
42
+ const watermark = state ? new Date(state.watermark) : undefined;
43
+ const from = range?.from ?? (options.from
44
+ ? options.from({ watermark, cutoff })
45
+ : new Date(watermark ? watermark.getTime() - (options.overlapMs ?? 0) : options.start.getTime()));
46
+ if (!Number.isFinite(from.getTime()) || !Number.isFinite(to.getTime())) {
47
+ throw new IngestConfigError('timestampWindow: planned bounds must be valid dates.');
48
+ }
49
+ if (from.getTime() > to.getTime()) {
50
+ throw new IngestConfigError(`timestampWindow: planned lower bound ${from.toISOString()} is after upper bound ${to.toISOString()}.`);
51
+ }
52
+ return { from, to };
53
+ },
54
+ complete({ selection }) {
55
+ return { watermark: selection.to.toISOString() };
56
+ },
57
+ };
58
+ }
59
+ /**
60
+ * Generic provider-owned state (compound cursor, change token, snapshot id…).
61
+ * The reader receives the committed state as its selection and advances it by
62
+ * yielding chunks that carry complete candidate state.
63
+ */
64
+ export function cursorState(options) {
65
+ return {
66
+ id: options.id,
67
+ version: options.version,
68
+ parseState: options.parse,
69
+ plan: ({ state }) => state,
70
+ };
71
+ }
72
+ //# sourceMappingURL=incremental.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental.js","sourceRoot":"","sources":["../src/incremental.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export { ingest, createIngestPlugin, checkGraph, type IngestPlugin, type IngestPluginOptions } from './plugin.js';
2
+ export { defineStream, definePipeline, selectStreams, type SelectedStream } from './registry.js';
3
+ export { fullSync, timestampWindow, cursorState, type TimestampRange, type TimestampWindowOptions, type TimestampWindowState } from './incremental.js';
4
+ export { paginate, type Page } from './paginate.js';
5
+ export { simpleLoader } from './loader.js';
6
+ export { ingestionColumns, rawTable, rawRows, createClickHouseDestination, type RawRow } from './destination.js';
7
+ export { createClickHouseJournal } from './journal.js';
8
+ export { runIngestion, type BackfillRequest, type ExecutionEnv, type ExecutionRequest } from './executor.js';
9
+ export { HttpError, FetchFailure, IngestConfigError } from './errors.js';
10
+ export type { DestinationAdapter, ErrorClassifier, ExecutionResult, FailureClass, FetchContext, IncrementalStrategy, Journal, Loader, LoaderContext, LoaderFactory, PipelineDefinition, ReadContext, RetryOptions, Row, SinkReceipt, SourceChunk, StreamDefinition, StreamResult, } from './types.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACjH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAChG,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,sBAAsB,EAAE,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACtJ,OAAO,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,EAAE,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAChH,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAC5G,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACxE,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,OAAO,EACP,MAAM,EACN,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,GAAG,EACH,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,YAAY,GACb,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { ingest, createIngestPlugin, checkGraph } from './plugin.js';
2
+ export { defineStream, definePipeline, selectStreams } from './registry.js';
3
+ export { fullSync, timestampWindow, cursorState } from './incremental.js';
4
+ export { paginate } from './paginate.js';
5
+ export { simpleLoader } from './loader.js';
6
+ export { ingestionColumns, rawTable, rawRows, createClickHouseDestination } from './destination.js';
7
+ export { createClickHouseJournal } from './journal.js';
8
+ export { runIngestion } from './executor.js';
9
+ export { HttpError, FetchFailure, IngestConfigError } from './errors.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,UAAU,EAA+C,MAAM,aAAa,CAAA;AACjH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAuB,MAAM,eAAe,CAAA;AAChG,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAA+E,MAAM,kBAAkB,CAAA;AACtJ,OAAO,EAAE,QAAQ,EAAa,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,EAAe,MAAM,kBAAkB,CAAA;AAChH,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,YAAY,EAAkE,MAAM,eAAe,CAAA;AAC5G,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,44 @@
1
+ import { type ClickHouseExecutor } from '@chkit/clickhouse';
2
+ import type { CommittedCheckpoint, Journal, JournalEvent } from './types.js';
3
+ export declare const DEFAULT_JOURNAL_TABLE = "_chkit_ingestion_journal";
4
+ export type JournalRow = {
5
+ target_id: string;
6
+ namespace_id: string;
7
+ event_seq: string;
8
+ event_id: string;
9
+ payload_hash: string;
10
+ event_at: string;
11
+ event_kind: string;
12
+ run_id: string;
13
+ work_id: string;
14
+ attempt_no: number;
15
+ batch_id: string;
16
+ expected_checkpoint_version: string;
17
+ checkpoint_version: string;
18
+ checkpoint_json: string;
19
+ work_state: string;
20
+ sink_evidence: string;
21
+ retry_at: string | null;
22
+ error_class: string;
23
+ detail_json: string;
24
+ };
25
+ export interface ClickHouseJournalOptions {
26
+ /** Must allow concurrent queries: use a stateless executor, not a session-bound one. */
27
+ executor: ClickHouseExecutor;
28
+ database: string;
29
+ targetId: string;
30
+ table?: string;
31
+ now?: () => Date;
32
+ }
33
+ /**
34
+ * Append-only, target-linked journal. It is the sole authority for durable
35
+ * ingestion control state: checkpoints are read as a projection over
36
+ * `batch_committed` facts and never stored anywhere else.
37
+ */
38
+ export declare function createClickHouseJournal(options: ClickHouseJournalOptions): Journal;
39
+ export declare function toJournalRow(event: JournalEvent, targetId: string, at: Date): JournalRow;
40
+ /** Stable key order so equal values always serialize identically. */
41
+ export declare function canonicalJson(value: unknown): string;
42
+ export declare function digest(parts: readonly string[]): string;
43
+ export declare function emptyCheckpoint(): CommittedCheckpoint;
44
+ //# sourceMappingURL=journal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.d.ts","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAGzE,OAAO,KAAK,EAAsB,mBAAmB,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEhG,eAAO,MAAM,qBAAqB,6BAA6B,CAAA;AAI/D,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,2BAA2B,EAAE,MAAM,CAAA;IACnC,kBAAkB,EAAE,MAAM,CAAA;IAC1B,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,wFAAwF;IACxF,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CA8GlF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,UAAU,CAwCxF;AAkBD,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGpD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAQvD;AAED,wBAAgB,eAAe,IAAI,mBAAmB,CAErD"}
@@ -0,0 +1,222 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { waitForTable } from '@chkit/clickhouse';
3
+ import { IngestConfigError } from './errors.js';
4
+ export const DEFAULT_JOURNAL_TABLE = '_chkit_ingestion_journal';
5
+ const TABLE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
+ /**
7
+ * Append-only, target-linked journal. It is the sole authority for durable
8
+ * ingestion control state: checkpoints are read as a projection over
9
+ * `batch_committed` facts and never stored anywhere else.
10
+ */
11
+ export function createClickHouseJournal(options) {
12
+ const table = options.table ?? DEFAULT_JOURNAL_TABLE;
13
+ if (!TABLE_NAME_PATTERN.test(table))
14
+ throw new IngestConfigError(`Invalid journal table name "${table}".`);
15
+ if (!TABLE_NAME_PATTERN.test(options.database)) {
16
+ throw new IngestConfigError(`Invalid journal database name "${options.database}".`);
17
+ }
18
+ const qualified = `\`${options.database}\`.\`${table}\``;
19
+ const now = options.now ?? (() => new Date());
20
+ return {
21
+ async ensure() {
22
+ await options.executor.command(journalTableSql(qualified));
23
+ // Replicated/shared catalogs (ObsessionDB) can acknowledge the CREATE
24
+ // before the replica serving the next request knows the table.
25
+ await waitForTable(options.executor, options.database, table);
26
+ },
27
+ async append(event) {
28
+ const row = toJournalRow(event, options.targetId, now());
29
+ await options.executor.insert({
30
+ table: `${options.database}.${table}`,
31
+ values: [row],
32
+ // A retried append of the same deterministic fact is suppressed while
33
+ // the deduplication window lasts; readers canonicalize by event_id anyway.
34
+ settings: { insert_deduplication_token: row.event_id, async_insert: 0 },
35
+ });
36
+ },
37
+ async readCheckpoint(namespaceId) {
38
+ // Physical retry duplicates are allowed, so facts are canonicalized per
39
+ // sequence number first. The history is then validated before anything is
40
+ // projected from it: a checkpoint read from a damaged journal is worthless.
41
+ const facts = `SELECT
42
+ event_seq,
43
+ uniqExact(event_id) AS owners,
44
+ uniqExact(payload_hash) AS payloads,
45
+ any(event_kind) AS fact_kind,
46
+ any(work_state) AS fact_work_state,
47
+ any(expected_checkpoint_version) AS fact_expected,
48
+ any(checkpoint_version) AS fact_version,
49
+ any(checkpoint_json) AS fact_checkpoint
50
+ FROM ${qualified}
51
+ WHERE target_id = ${sqlString(options.targetId)} AND namespace_id = ${sqlString(namespaceId)}
52
+ GROUP BY event_seq`;
53
+ const settings = { select_sequential_consistency: '1' };
54
+ const [health, transitions] = await Promise.all([
55
+ options.executor.query(`SELECT
56
+ max(event_seq) AS head_seq,
57
+ maxIf(event_seq, fact_kind = 'work_finished' AND fact_work_state = 'succeeded') AS last_success_seq,
58
+ count() AS sequences,
59
+ countIf(owners > 1) AS conflicting_owners,
60
+ countIf(payloads > 1) AS drifted,
61
+ argMaxIf(fact_version, event_seq, fact_kind = 'batch_committed') AS checkpoint_version,
62
+ argMaxIf(fact_checkpoint, event_seq, fact_kind = 'batch_committed') AS checkpoint_json
63
+ FROM (${facts})`, settings),
64
+ // Every commit must start from the version the previous commit produced,
65
+ // advance it by at most one, and only change the envelope when it advances.
66
+ // The two reads are not one snapshot; that is sound because V1 runs a
67
+ // single executor process and reads a namespace before appending to it.
68
+ options.executor.query(`SELECT countIf(
69
+ fact_expected != previous_version OR fact_version < fact_expected OR fact_version > fact_expected + 1
70
+ OR (fact_version = fact_expected AND fact_checkpoint != previous_checkpoint)
71
+ ) AS invalid
72
+ FROM (
73
+ SELECT
74
+ fact_expected,
75
+ fact_version,
76
+ fact_checkpoint,
77
+ lagInFrame(fact_version, 1, toUInt64(0)) OVER (ORDER BY event_seq ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS previous_version,
78
+ lagInFrame(fact_checkpoint, 1, '') OVER (ORDER BY event_seq ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS previous_checkpoint
79
+ FROM (${facts})
80
+ WHERE fact_kind = 'batch_committed'
81
+ )`, settings),
82
+ ]);
83
+ const row = health[0];
84
+ if (!row || Number(row.sequences) === 0)
85
+ return emptyCheckpoint();
86
+ const problems = [
87
+ Number(row.head_seq) !== Number(row.sequences) ? `sequence gap (head ${row.head_seq}, ${row.sequences} facts)` : '',
88
+ Number(row.conflicting_owners) > 0 ? `${row.conflicting_owners} sequence number(s) owned by conflicting facts` : '',
89
+ Number(row.drifted) > 0 ? `${row.drifted} fact(s) with drifting payloads` : '',
90
+ Number(transitions[0]?.invalid ?? 0) > 0 ? `${transitions[0]?.invalid} invalid checkpoint transition(s)` : '',
91
+ ].filter((problem) => problem !== '');
92
+ if (problems.length > 0) {
93
+ throw new Error(`Ingestion journal for "${namespaceId}" is not a valid history: ${problems.join('; ')}. Refusing to project a checkpoint from it; more than one executor process may have been active.`);
94
+ }
95
+ return {
96
+ version: Number(row.checkpoint_version),
97
+ envelope: parseEnvelope(row.checkpoint_json),
98
+ headSeq: Number(row.head_seq),
99
+ lastSuccessSeq: Number(row.last_success_seq),
100
+ };
101
+ },
102
+ };
103
+ }
104
+ export function toJournalRow(event, targetId, at) {
105
+ const checkpointJson = event.checkpoint ? canonicalJson(event.checkpoint) : '';
106
+ const detailJson = canonicalJson(event.detail);
107
+ // Identity covers what makes the fact unique; the payload hash covers every
108
+ // authoritative field a retry of that same fact must reproduce. Only the
109
+ // physical append time (event_at) is excluded.
110
+ const eventId = digest([targetId, event.namespaceId, String(event.eventSeq), event.eventKind, event.workId, event.batchId, String(event.attemptNo)]);
111
+ const payload = digest([
112
+ eventId,
113
+ String(event.expectedCheckpointVersion),
114
+ String(event.checkpointVersion),
115
+ checkpointJson,
116
+ event.workState,
117
+ event.sinkEvidence,
118
+ event.errorClass,
119
+ event.runId,
120
+ event.retryAt ? event.retryAt.toISOString() : '',
121
+ detailJson,
122
+ ]);
123
+ return {
124
+ target_id: targetId,
125
+ namespace_id: event.namespaceId,
126
+ event_seq: String(event.eventSeq),
127
+ event_id: eventId,
128
+ payload_hash: BigInt(`0x${payload.slice(0, 16)}`).toString(),
129
+ event_at: toClickHouseDateTime(at),
130
+ event_kind: event.eventKind,
131
+ run_id: event.runId,
132
+ work_id: event.workId,
133
+ attempt_no: event.attemptNo,
134
+ batch_id: event.batchId,
135
+ expected_checkpoint_version: String(event.expectedCheckpointVersion),
136
+ checkpoint_version: String(event.checkpointVersion),
137
+ checkpoint_json: checkpointJson,
138
+ work_state: event.workState,
139
+ sink_evidence: event.sinkEvidence,
140
+ retry_at: event.retryAt ? toClickHouseDateTime(event.retryAt) : null,
141
+ error_class: event.errorClass,
142
+ detail_json: detailJson,
143
+ };
144
+ }
145
+ function parseEnvelope(json) {
146
+ if (json === '')
147
+ return undefined;
148
+ const parsed = JSON.parse(json);
149
+ if (typeof parsed !== 'object' ||
150
+ parsed === null ||
151
+ !('strategy' in parsed) ||
152
+ typeof parsed.strategy !== 'string' ||
153
+ !('version' in parsed) ||
154
+ typeof parsed.version !== 'number') {
155
+ throw new Error('Committed checkpoint is not a valid ChKit checkpoint envelope.');
156
+ }
157
+ return { strategy: parsed.strategy, version: parsed.version, state: 'state' in parsed ? parsed.state : undefined };
158
+ }
159
+ /** Stable key order so equal values always serialize identically. */
160
+ export function canonicalJson(value) {
161
+ // JSON.stringify(undefined) is undefined, not a string.
162
+ return JSON.stringify(sortKeys(value)) ?? 'null';
163
+ }
164
+ export function digest(parts) {
165
+ const hash = createHash('sha256');
166
+ for (const part of parts) {
167
+ hash.update(String(part.length));
168
+ hash.update(':');
169
+ hash.update(part);
170
+ }
171
+ return hash.digest('hex');
172
+ }
173
+ export function emptyCheckpoint() {
174
+ return { version: 0, envelope: undefined, headSeq: 0, lastSuccessSeq: 0 };
175
+ }
176
+ function journalTableSql(qualified) {
177
+ return `CREATE TABLE IF NOT EXISTS ${qualified}
178
+ (
179
+ target_id LowCardinality(String),
180
+ namespace_id String,
181
+ event_seq UInt64,
182
+ event_id String,
183
+ payload_hash UInt64,
184
+ event_at DateTime64(6, 'UTC'),
185
+ event_kind LowCardinality(String),
186
+ run_id String,
187
+ work_id String,
188
+ attempt_no UInt32,
189
+ batch_id String,
190
+ expected_checkpoint_version UInt64,
191
+ checkpoint_version UInt64,
192
+ checkpoint_json String,
193
+ work_state LowCardinality(String),
194
+ sink_evidence LowCardinality(String),
195
+ retry_at Nullable(DateTime64(6, 'UTC')),
196
+ error_class LowCardinality(String),
197
+ detail_json String
198
+ )
199
+ ENGINE = MergeTree
200
+ PARTITION BY toYYYYMM(event_at)
201
+ ORDER BY (target_id, namespace_id, event_seq, event_id)`;
202
+ }
203
+ function sortKeys(value) {
204
+ if (Array.isArray(value))
205
+ return value.map(sortKeys);
206
+ if (value instanceof Date)
207
+ return value.toISOString();
208
+ if (typeof value === 'object' && value !== null) {
209
+ return Object.fromEntries(Object.entries(value)
210
+ .filter(([, entry]) => entry !== undefined)
211
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
212
+ .map(([key, entry]) => [key, sortKeys(entry)]));
213
+ }
214
+ return value;
215
+ }
216
+ function toClickHouseDateTime(date) {
217
+ return date.toISOString().replace('T', ' ').replace('Z', '');
218
+ }
219
+ function sqlString(value) {
220
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
221
+ }
222
+ //# sourceMappingURL=journal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.js","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { LoaderFactory } from './types.js';
2
+ export interface SimpleLoaderOptions {
3
+ /** Rows per physical INSERT. A larger batch is split into deterministic chunks. */
4
+ maxRowsPerInsert?: number;
5
+ }
6
+ /**
7
+ * Direct at-least-once destination writes. Every write unit reuses one stable
8
+ * token across retries: a batch written by one INSERT has one token, a
9
+ * deterministically chunked batch has one distinct token per chunk.
10
+ */
11
+ export declare function simpleLoader(options?: SimpleLoaderOptions): LoaderFactory;
12
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAID;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,aAAa,CA8B7E"}
package/dist/loader.js ADDED
@@ -0,0 +1,37 @@
1
+ import { BATCH_ID_COLUMN, INGESTED_AT_COLUMN, RUN_ID_COLUMN } from './destination.js';
2
+ const DEFAULT_MAX_ROWS_PER_INSERT = 50_000;
3
+ /**
4
+ * Direct at-least-once destination writes. Every write unit reuses one stable
5
+ * token across retries: a batch written by one INSERT has one token, a
6
+ * deterministically chunked batch has one distinct token per chunk.
7
+ */
8
+ export function simpleLoader(options = {}) {
9
+ const maxRows = options.maxRowsPerInsert ?? DEFAULT_MAX_ROWS_PER_INSERT;
10
+ return (ctx) => {
11
+ let rows = 0;
12
+ let writeUnits = 0;
13
+ return {
14
+ ctx,
15
+ async write(batch) {
16
+ for (let offset = 0, unit = 0; offset < batch.rows.length; offset += maxRows, unit += 1) {
17
+ ctx.signal.throwIfAborted();
18
+ const slice = batch.rows.slice(offset, offset + maxRows).map((row) => {
19
+ // Publication time is destination-owned: never let a mapped row supply it.
20
+ const { [INGESTED_AT_COLUMN]: _ignored, ...authored } = row;
21
+ return { ...authored, [BATCH_ID_COLUMN]: batch.batchId, [RUN_ID_COLUMN]: ctx.runId };
22
+ });
23
+ await ctx.destination.insert({ table: ctx.table, rows: slice, token: `${batch.batchId}:${unit}` });
24
+ rows += slice.length;
25
+ writeUnits += 1;
26
+ }
27
+ },
28
+ async finalize() {
29
+ return { evidence: writeUnits === 0 ? 'none_required' : 'clickhouse_ack', rows, writeUnits };
30
+ },
31
+ async abort() {
32
+ // Direct writes cannot be withdrawn; replay reuses the same tokens.
33
+ },
34
+ };
35
+ };
36
+ }
37
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAQrF,MAAM,2BAA2B,GAAG,MAAM,CAAA;AAE1C;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,UAA+B,EAAE;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC,gBAAgB,IAAI,2BAA2B,CAAA;IAEvE,OAAO,CAAC,GAAG,EAAE,EAAE;QACb,IAAI,IAAI,GAAG,CAAC,CAAA;QACZ,IAAI,UAAU,GAAG,CAAC,CAAA;QAElB,OAAO;YACL,GAAG;YACH,KAAK,CAAC,KAAK,CAAC,KAAK;gBACf,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;oBACxF,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE,CAAA;oBAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;wBACnE,2EAA2E;wBAC3E,MAAM,EAAE,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,GAAG,GAAG,CAAA;wBAC3D,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,GAAG,CAAC,KAAK,EAAE,CAAA;oBACtF,CAAC,CAAC,CAAA;oBACF,MAAM,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,OAAO,IAAI,IAAI,EAAE,EAAE,CAAC,CAAA;oBAClG,IAAI,IAAI,KAAK,CAAC,MAAM,CAAA;oBACpB,UAAU,IAAI,CAAC,CAAA;gBACjB,CAAC;YACH,CAAC;YACD,KAAK,CAAC,QAAQ;gBACZ,OAAO,EAAE,QAAQ,EAAE,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,gBAAgB,EAAE,IAAI,EAAE,UAAU,EAAE,CAAA;YAC9F,CAAC;YACD,KAAK,CAAC,KAAK;gBACT,oEAAoE;YACtE,CAAC;SACF,CAAA;IACH,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { AttemptOptions, FetchContext } from './types.js';
2
+ export interface Page<TItem, TCursor> {
3
+ items: readonly TItem[];
4
+ /** Continuation for the next request; `undefined` ends the sequence. */
5
+ next: TCursor | undefined;
6
+ }
7
+ export interface PaginateOptions<TItem, TCursor> {
8
+ context: FetchContext;
9
+ fetchPage(cursor: TCursor | undefined, signal: AbortSignal): Promise<Page<TItem, TCursor>>;
10
+ initial?: TCursor;
11
+ label?: AttemptOptions['label'];
12
+ }
13
+ /**
14
+ * Pull-based pagination over one retryable Promise step per page. Every request
15
+ * runs through the executor attempt capability. A continuation that was already
16
+ * seen (compared by canonical serialization) is rejected, so a cyclic provider
17
+ * cursor cannot loop forever; other non-progress is bounded by execution budgets.
18
+ */
19
+ export declare function paginate<TItem, TCursor>(options: PaginateOptions<TItem, TCursor>): AsyncGenerator<readonly TItem[], void, void>;
20
+ //# sourceMappingURL=paginate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paginate.d.ts","sourceRoot":"","sources":["../src/paginate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9D,MAAM,WAAW,IAAI,CAAC,KAAK,EAAE,OAAO;IAClC,KAAK,EAAE,SAAS,KAAK,EAAE,CAAA;IACvB,wEAAwE;IACxE,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe,CAAC,KAAK,EAAE,OAAO;IAC7C,OAAO,EAAE,YAAY,CAAA;IACrB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAA;IAC1F,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAA;CAChC;AAED;;;;;GAKG;AACH,wBAAuB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAC5C,OAAO,EAAE,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,GACvC,cAAc,CAAC,SAAS,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAoB9C"}
@@ -0,0 +1,29 @@
1
+ import { canonicalJson } from './journal.js';
2
+ /**
3
+ * Pull-based pagination over one retryable Promise step per page. Every request
4
+ * runs through the executor attempt capability. A continuation that was already
5
+ * seen (compared by canonical serialization) is rejected, so a cyclic provider
6
+ * cursor cannot loop forever; other non-progress is bounded by execution budgets.
7
+ */
8
+ export async function* paginate(options) {
9
+ const seen = new Set();
10
+ let cursor = options.initial;
11
+ while (true) {
12
+ options.context.signal.throwIfAborted();
13
+ const current = cursor;
14
+ const page = await options.context.attempt((signal) => options.fetchPage(current, signal), {
15
+ label: options.label,
16
+ });
17
+ if (page.items.length > 0)
18
+ yield page.items;
19
+ if (page.next === undefined || page.next === null)
20
+ return;
21
+ const key = canonicalJson(page.next);
22
+ if (seen.has(key)) {
23
+ throw new Error(`paginate: provider returned a repeated continuation ${key}; refusing to loop.`);
24
+ }
25
+ seen.add(key);
26
+ cursor = page.next;
27
+ }
28
+ }
29
+ //# sourceMappingURL=paginate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paginate.js","sourceRoot":"","sources":["../src/paginate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAgB5C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,QAAQ,CAC7B,OAAwC;IAExC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,IAAI,MAAM,GAAG,OAAO,CAAC,OAAO,CAAA;IAE5B,OAAO,IAAI,EAAE,CAAC;QACZ,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,CAAA;QACvC,MAAM,OAAO,GAAG,MAAM,CAAA;QACtB,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE;YACzF,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB,CAAC,CAAA;QACF,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,CAAC,KAAK,CAAA;QAC3C,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,OAAM;QAEzD,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,uDAAuD,GAAG,qBAAqB,CAAC,CAAA;QAClG,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,MAAM,GAAG,IAAI,CAAC,IAAI,CAAA;IACpB,CAAC;AACH,CAAC"}
@@ -0,0 +1,110 @@
1
+ import { type ClickHouseExecutor } from '@chkit/clickhouse';
2
+ import { type ChxInlinePluginRegistration, type ResolvedChxConfig } from '@chkit/core';
3
+ import { z } from 'zod';
4
+ import type { PipelineDefinition } from './types.js';
5
+ declare const IngestOptionsSchema: z.ZodObject<{
6
+ journalTable: z.ZodDefault<z.ZodString>;
7
+ maxDurationSeconds: z.ZodDefault<z.ZodNumber>;
8
+ prefetchBatches: z.ZodDefault<z.ZodNumber>;
9
+ }, z.core.$strip>;
10
+ type IngestOptions = z.infer<typeof IngestOptionsSchema>;
11
+ export type IngestPluginOptions = Partial<IngestOptions>;
12
+ interface IngestPluginCommandContext {
13
+ args: string[];
14
+ flags: Record<string, string | string[] | boolean | undefined>;
15
+ jsonMode: boolean;
16
+ options: IngestOptions;
17
+ config: ResolvedChxConfig;
18
+ configPath: string;
19
+ print: (value: unknown) => void;
20
+ pluginContext?: {
21
+ executor: ClickHouseExecutor;
22
+ hasExecutor: boolean;
23
+ };
24
+ }
25
+ export declare function createIngestPlugin(options?: IngestPluginOptions): {
26
+ manifest: {
27
+ name: "ingest";
28
+ apiVersion: 1;
29
+ };
30
+ optionsSchema: import("@chkit/core").SafeParseable<{
31
+ journalTable: string;
32
+ maxDurationSeconds: number;
33
+ prefetchBatches: number;
34
+ }>;
35
+ commands: ({
36
+ name: string;
37
+ description: string;
38
+ flags: readonly [{
39
+ readonly name: "--tag";
40
+ readonly type: "string[]";
41
+ readonly description: "Exact tag every selected stream must carry (repeatable, AND)";
42
+ readonly placeholder: "<tag>";
43
+ }, {
44
+ readonly name: "--backfill";
45
+ readonly type: "string";
46
+ readonly description: "Stable backfill id; uses an isolated checkpoint namespace";
47
+ readonly placeholder: "<id>";
48
+ }, {
49
+ readonly name: "--from";
50
+ readonly type: "string";
51
+ readonly description: "Backfill range lower bound (ISO timestamp)";
52
+ readonly placeholder: "<timestamp>";
53
+ }, {
54
+ readonly name: "--to";
55
+ readonly type: "string";
56
+ readonly description: "Backfill range upper bound (ISO timestamp)";
57
+ readonly placeholder: "<timestamp>";
58
+ }, {
59
+ readonly name: "--max-duration";
60
+ readonly type: "string";
61
+ readonly description: "Execution budget in seconds";
62
+ readonly placeholder: "<seconds>";
63
+ }];
64
+ optionsSchema: import("@chkit/core").SafeParseable<{
65
+ journalTable: string;
66
+ maxDurationSeconds: number;
67
+ prefetchBatches: number;
68
+ }>;
69
+ run: (context: IngestPluginCommandContext) => Promise<number | undefined>;
70
+ } | {
71
+ name: string;
72
+ description: string;
73
+ flags: readonly [{
74
+ readonly name: "--tag";
75
+ readonly type: "string[]";
76
+ readonly description: "Exact tag every selected stream must carry (repeatable, AND)";
77
+ readonly placeholder: "<tag>";
78
+ }];
79
+ optionsSchema: import("@chkit/core").SafeParseable<{
80
+ journalTable: string;
81
+ maxDurationSeconds: number;
82
+ prefetchBatches: number;
83
+ }>;
84
+ run: (context: IngestPluginCommandContext) => Promise<number | undefined>;
85
+ })[];
86
+ hooks: {
87
+ onCheck(context: {
88
+ config: ResolvedChxConfig;
89
+ }): Promise<{
90
+ plugin: string;
91
+ evaluated: boolean;
92
+ ok: boolean;
93
+ findings: {
94
+ code: string;
95
+ message: string;
96
+ severity: "info" | "warn" | "error";
97
+ }[];
98
+ }>;
99
+ };
100
+ };
101
+ export type IngestPlugin = ReturnType<typeof createIngestPlugin>;
102
+ export declare function ingest(options?: IngestPluginOptions): ChxInlinePluginRegistration<IngestPlugin, IngestPluginOptions>;
103
+ /** Local, zero-network graph checks: they hold in `--offline` mode too. */
104
+ export declare function checkGraph(pipelines: readonly PipelineDefinition[]): {
105
+ code: string;
106
+ message: string;
107
+ severity: "info" | "warn" | "error";
108
+ }[];
109
+ export {};
110
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqC,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC9F,OAAO,EAIL,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAOvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAIpD,QAAA,MAAM,mBAAmB;;;;iBAKvB,CAAA;AACF,KAAK,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAExD,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;AAcxD,UAAU,0BAA0B;IAClC,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;IAC9D,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,aAAa,CAAA;IACtB,MAAM,EAAE,iBAAiB,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAC/B,aAAa,CAAC,EAAE;QAAE,QAAQ,EAAE,kBAAkB,CAAC;QAAC,WAAW,EAAE,OAAO,CAAA;KAAE,CAAA;CACvE;AAID,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAwHvC;YAAE,MAAM,EAAE,iBAAiB,CAAA;SAAE;;;;;sBAqB1B,MAAM;yBAAW,MAAM;0BAAY,MAAM,GAAG,MAAM,GAAG,OAAO;;;;EAV3F;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAEhE,wBAAgB,MAAM,CAAC,OAAO,GAAE,mBAAwB,GAAG,2BAA2B,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAExH;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,SAAS,EAAE,SAAS,kBAAkB,EAAE;UACnC,MAAM;aAAW,MAAM;cAAY,MAAM,GAAG,MAAM,GAAG,OAAO;IAkB3F"}