@untangle-code/jobs 0.0.0-stage → 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +138 -2
  4. package/THIRD_PARTY_LICENSES.md +24 -0
  5. package/dist/abort.d.ts +14 -0
  6. package/dist/abort.d.ts.map +1 -0
  7. package/dist/abort.js +35 -0
  8. package/dist/abort.js.map +1 -0
  9. package/dist/bounded-queue.d.ts +200 -0
  10. package/dist/bounded-queue.d.ts.map +1 -0
  11. package/dist/bounded-queue.js +338 -0
  12. package/dist/bounded-queue.js.map +1 -0
  13. package/dist/checkpoint-store.d.ts +30 -0
  14. package/dist/checkpoint-store.d.ts.map +1 -0
  15. package/dist/checkpoint-store.js +131 -0
  16. package/dist/checkpoint-store.js.map +1 -0
  17. package/dist/events.d.ts +29 -0
  18. package/dist/events.d.ts.map +1 -0
  19. package/dist/events.js +66 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/index.d.ts +22 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +30 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/resume.d.ts +45 -0
  26. package/dist/resume.d.ts.map +1 -0
  27. package/dist/resume.js +60 -0
  28. package/dist/resume.js.map +1 -0
  29. package/dist/runner.d.ts +148 -0
  30. package/dist/runner.d.ts.map +1 -0
  31. package/dist/runner.js +686 -0
  32. package/dist/runner.js.map +1 -0
  33. package/dist/stages.d.ts +146 -0
  34. package/dist/stages.d.ts.map +1 -0
  35. package/dist/stages.js +2 -0
  36. package/dist/stages.js.map +1 -0
  37. package/dist/state-machine.d.ts +52 -0
  38. package/dist/state-machine.d.ts.map +1 -0
  39. package/dist/state-machine.js +114 -0
  40. package/dist/state-machine.js.map +1 -0
  41. package/package.json +61 -3
  42. package/src/abort.ts +37 -0
  43. package/src/bounded-queue.ts +462 -0
  44. package/src/checkpoint-store.ts +171 -0
  45. package/src/events.ts +71 -0
  46. package/src/index.ts +37 -0
  47. package/src/resume.ts +106 -0
  48. package/src/runner.ts +916 -0
  49. package/src/stages.ts +167 -0
  50. package/src/state-machine.ts +132 -0
package/src/stages.ts ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Narrow stage interfaces of the index pipeline (architecture.md §13.1).
3
+ *
4
+ * This package builds the pipeline MECHANICS — state machine, queues,
5
+ * backpressure, checkpoints, resume, cancellation, generation commit. The
6
+ * domain-specific stages (scanner, parser, chunker, embedder, stores) are
7
+ * INJECTED behind these interfaces; `@untangle-code/core` wires the real
8
+ * implementations later, tests wire deterministic fakes from
9
+ * `@untangle-code/testkit` ports.
10
+ *
11
+ * Every method takes trailing `PortCallOptions` and must respect
12
+ * `options.signal` (spec.md §22); the runner additionally only ever acts on
13
+ * results at safe batch boundaries (architecture.md §21.3).
14
+ */
15
+ import type { ParseQuality, PortCallOptions } from "@untangle-code/domain";
16
+
17
+ /** One file the discovery stage planned for (re)indexing. */
18
+ export interface PlannedFileV1 {
19
+ fileId: string;
20
+ /** "/"-normalized repo-relative path. */
21
+ filePath: string;
22
+ /** Content hash — source of truth for change detection (spec.md §12.1). */
23
+ contentHash: string;
24
+ }
25
+
26
+ /** Result of the discovery stage (scanner + manifest comparison). */
27
+ export interface DiscoveryResultV1 {
28
+ /** Files that need work in this run (changed or new). */
29
+ planned: PlannedFileV1[];
30
+ /**
31
+ * Every currently searchable source file, including unchanged files.
32
+ * This is kept separate from the resume checkpoint manifest: the latter
33
+ * proves confirmed writes, while this snapshot identifies the corpus the
34
+ * completed generation represents.
35
+ */
36
+ corpusManifestHashes: Record<string, string>;
37
+ totalFiles: number;
38
+ unchangedFiles: number;
39
+ ignoredFiles: number;
40
+ }
41
+
42
+ /** Ensures Qdrant/Ollama/collections are ready (spec.md §11.1 stage 2). */
43
+ export interface InfrastructureStage {
44
+ ensureReady(options?: PortCallOptions): Promise<{
45
+ /** Effective endpoints with secrets redacted (spec.md §3.2). */
46
+ endpoints: Record<string, string>;
47
+ }>;
48
+ }
49
+
50
+ export interface DiscoveryStage {
51
+ discover(options?: PortCallOptions): Promise<DiscoveryResultV1>;
52
+ }
53
+
54
+ /** One prepared text ready for embedding, keyed by its future point id. */
55
+ export interface PreparedTextV1 {
56
+ pointId: string;
57
+ text: string;
58
+ payload?: Record<string, unknown>;
59
+ }
60
+
61
+ /** Parse + chunk + metrics output for one file (stages 4–5 of spec.md §11.1). */
62
+ export interface AnalyzedFileV1 {
63
+ file: PlannedFileV1;
64
+ parseQuality: ParseQuality;
65
+ chunkCount: number;
66
+ symbolCount: number;
67
+ preparedTexts: PreparedTextV1[];
68
+ /** Estimated in-memory size — feeds the queue byte budget (§13.2). */
69
+ estimatedBytes: number;
70
+ }
71
+
72
+ export interface AnalysisStage {
73
+ analyze(file: PlannedFileV1, options?: PortCallOptions): Promise<AnalyzedFileV1>;
74
+ }
75
+
76
+ /** One embedded point ready for upsert. */
77
+ export interface EmbeddedPointV1 {
78
+ pointId: string;
79
+ fileId: string;
80
+ vector: number[];
81
+ payload?: Record<string, unknown>;
82
+ }
83
+
84
+ /** Result of embedding one file batch. */
85
+ export interface EmbeddedBatchV1 {
86
+ files: PlannedFileV1[];
87
+ points: EmbeddedPointV1[];
88
+ cacheHits: number;
89
+ truncatedCount: number;
90
+ /** Estimated in-memory size — feeds the queue byte budget (§13.2). */
91
+ estimatedBytes: number;
92
+ }
93
+
94
+ export interface EmbeddingStage {
95
+ embedBatch(batch: AnalyzedFileV1[], options?: PortCallOptions): Promise<EmbeddedBatchV1>;
96
+ }
97
+
98
+ /**
99
+ * Confirmation of one CONFIRMED upsert. The runner persists a checkpoint
100
+ * only after receiving this (spec.md §10.5) — a storage stage must therefore
101
+ * resolve only once the store acknowledged the write.
102
+ */
103
+ export interface UpsertConfirmationV1 {
104
+ collection: string;
105
+ pointCount: number;
106
+ serializedBytes: number;
107
+ /** Confirmed point ids per file id — recorded in the checkpoint (§13.3). */
108
+ storedPointIdsByFile: Record<string, string[]>;
109
+ }
110
+
111
+ export interface StorageStage {
112
+ upsertBatch(batch: EmbeddedBatchV1, options?: PortCallOptions): Promise<UpsertConfirmationV1>;
113
+ }
114
+
115
+ /** Result of building/patching one graph (stage `building_graphs`). */
116
+ export interface GraphBuildResultV1 {
117
+ graph: "file" | "symbol";
118
+ nodeCount: number;
119
+ edgeCount: number;
120
+ }
121
+
122
+ export interface GraphStage {
123
+ buildGraphs(options?: PortCallOptions): Promise<GraphBuildResultV1[]>;
124
+ }
125
+
126
+ /** Optional context-artifact indexing (stage `indexing_context`). */
127
+ export interface ContextStage {
128
+ indexContext(options?: PortCallOptions): Promise<void>;
129
+ }
130
+
131
+ /** Input the runner hands to the generation commit (architecture.md §13.4). */
132
+ export interface GenerationCommitInputV1 {
133
+ jobId: string;
134
+ generationId: string;
135
+ workspaceId: string;
136
+ repoId: string;
137
+ refId: string;
138
+ effectiveProfileId: string;
139
+ plannedFileIds: string[];
140
+ confirmedFileIds: string[];
141
+ storedGraphs: string[];
142
+ }
143
+
144
+ export interface GenerationCommitResultV1 {
145
+ /** Must be "completed" for the job to finish as completed (§13.4). */
146
+ metadataStatus: string;
147
+ }
148
+
149
+ export interface GenerationCommitStage {
150
+ commit(
151
+ input: GenerationCommitInputV1,
152
+ options?: PortCallOptions,
153
+ ): Promise<GenerationCommitResultV1>;
154
+ }
155
+
156
+ /** The full injected stage set of one index job. */
157
+ export interface IndexJobStages {
158
+ infrastructure: InfrastructureStage;
159
+ discovery: DiscoveryStage;
160
+ analysis: AnalysisStage;
161
+ embedding: EmbeddingStage;
162
+ storage: StorageStage;
163
+ graphs: GraphStage;
164
+ /** Optional — a workspace without context artifacts skips the stage's work. */
165
+ context?: ContextStage;
166
+ commit: GenerationCommitStage;
167
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Full-index job state machine — exactly the stages and transitions of
3
+ * spec.md §11.1, including the error states `failed`, `cancel_requested`
4
+ * and `cancelled`. Illegal transitions are REJECTED with a typed error,
5
+ * never silently ignored.
6
+ */
7
+ import { INDEX_JOB_STAGES, type IndexJobStage } from "@untangle-code/domain";
8
+ import { indexIncompleteError } from "@untangle-code/observability";
9
+
10
+ /**
11
+ * The linear pipeline chain of spec.md §11.1 (`queued` → … → `completed`).
12
+ * Derived from domain's `INDEX_JOB_STAGES` (the first twelve entries are the
13
+ * pipeline in order; the last three are the error states).
14
+ */
15
+ export const JOB_PIPELINE_STAGES: readonly IndexJobStage[] = INDEX_JOB_STAGES.slice(
16
+ 0,
17
+ INDEX_JOB_STAGES.indexOf("completed") + 1,
18
+ );
19
+
20
+ /** Terminal stages: no transition leaves them. */
21
+ export const TERMINAL_JOB_STAGES: readonly IndexJobStage[] = ["completed", "failed", "cancelled"];
22
+
23
+ /** True when `stage` is terminal (`completed`, `failed` or `cancelled`). */
24
+ export function isTerminalJobStage(stage: IndexJobStage): boolean {
25
+ return TERMINAL_JOB_STAGES.includes(stage);
26
+ }
27
+
28
+ const PIPELINE_INDEX: ReadonlyMap<IndexJobStage, number> = new Map(
29
+ JOB_PIPELINE_STAGES.map((stage, index) => [stage, index]),
30
+ );
31
+
32
+ /** Index of `stage` in the pipeline chain, or null for the error states. */
33
+ export function pipelineIndexOf(stage: IndexJobStage): number | null {
34
+ return PIPELINE_INDEX.get(stage) ?? null;
35
+ }
36
+
37
+ /**
38
+ * Pure transition rule of spec.md §11.1:
39
+ *
40
+ * - along the pipeline chain, only the single next stage is reachable;
41
+ * - `failed` and `cancel_requested` are reachable from every non-terminal
42
+ * pipeline stage;
43
+ * - `cancel_requested` may only move to `cancelled` (or `failed`, when the
44
+ * cancellation cleanup itself fails);
45
+ * - `completed`, `failed` and `cancelled` are terminal;
46
+ * - self-transitions are not transitions.
47
+ */
48
+ export function canTransition(from: IndexJobStage, to: IndexJobStage): boolean {
49
+ if (from === to) {
50
+ return false;
51
+ }
52
+ if (isTerminalJobStage(from)) {
53
+ return false;
54
+ }
55
+ if (from === "cancel_requested") {
56
+ return to === "cancelled" || to === "failed";
57
+ }
58
+ // `from` is now a non-terminal pipeline stage.
59
+ if (to === "failed" || to === "cancel_requested") {
60
+ return true;
61
+ }
62
+ const fromIndex = pipelineIndexOf(from);
63
+ const toIndex = pipelineIndexOf(to);
64
+ return fromIndex !== null && toIndex !== null && toIndex === fromIndex + 1;
65
+ }
66
+
67
+ /**
68
+ * Mutable job stage holder enforcing spec.md §11.1. An illegal transition
69
+ * throws `UntangleError` with code `INDEX_INCOMPLETE` (the job would be left
70
+ * in an inconsistent state) — it never mutates and is never ignored.
71
+ */
72
+ export class JobStateMachine {
73
+ #stage: IndexJobStage;
74
+
75
+ public constructor(initial: IndexJobStage = "queued") {
76
+ if (!INDEX_JOB_STAGES.includes(initial)) {
77
+ throw indexIncompleteError(`unknown job stage "${initial}"`, {
78
+ retryable: false,
79
+ operation: "job_stage_transition",
80
+ context: { stage: initial },
81
+ });
82
+ }
83
+ this.#stage = initial;
84
+ }
85
+
86
+ public get stage(): IndexJobStage {
87
+ return this.#stage;
88
+ }
89
+
90
+ /** Applies one legal transition or throws; never a silent no-op. */
91
+ public transition(to: IndexJobStage): void {
92
+ if (!canTransition(this.#stage, to)) {
93
+ throw indexIncompleteError(`illegal job stage transition "${this.#stage}" -> "${to}"`, {
94
+ retryable: false,
95
+ operation: "job_stage_transition",
96
+ context: { from: this.#stage, to },
97
+ });
98
+ }
99
+ this.#stage = to;
100
+ }
101
+
102
+ /**
103
+ * Walks the pipeline chain forward, one legal transition at a time, until
104
+ * `target` is reached. A `target` at or before the current stage is a
105
+ * no-op (used for stage high-water marks in the concurrent pipeline,
106
+ * see README "Entscheidungen"). Throws when the machine is in an error
107
+ * state or `target` is not a pipeline stage.
108
+ */
109
+ public advanceTo(target: IndexJobStage): void {
110
+ const targetIndex = pipelineIndexOf(target);
111
+ const currentIndex = pipelineIndexOf(this.#stage);
112
+ if (targetIndex === null || currentIndex === null) {
113
+ throw indexIncompleteError(
114
+ `cannot advance from "${this.#stage}" to "${target}": not a pipeline stage`,
115
+ {
116
+ retryable: false,
117
+ operation: "job_stage_transition",
118
+ context: { from: this.#stage, to: target },
119
+ },
120
+ );
121
+ }
122
+ if (targetIndex <= currentIndex) {
123
+ return;
124
+ }
125
+ for (let index = currentIndex + 1; index <= targetIndex; index += 1) {
126
+ const next = JOB_PIPELINE_STAGES[index];
127
+ if (next !== undefined) {
128
+ this.transition(next);
129
+ }
130
+ }
131
+ }
132
+ }