@ai-sdlc/orchestrator 0.10.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/dist/adapters.d.ts +42 -3
  2. package/dist/adapters.js +133 -3
  3. package/dist/admission-composite.d.ts +112 -1
  4. package/dist/admission-composite.js +85 -4
  5. package/dist/admission-enrichment.d.ts +12 -3
  6. package/dist/admission-enrichment.js +64 -13
  7. package/dist/admission-score.d.ts +30 -0
  8. package/dist/admission-score.js +4 -1
  9. package/dist/backlog-adapter.d.ts +45 -0
  10. package/dist/backlog-adapter.js +145 -1
  11. package/dist/calibration.d.ts +81 -0
  12. package/dist/calibration.js +76 -0
  13. package/dist/cli/commands/init-features.d.ts +301 -2
  14. package/dist/cli/commands/init-features.js +634 -7
  15. package/dist/cli/commands/init-templates.d.ts +198 -1
  16. package/dist/cli/commands/init-templates.js +943 -1
  17. package/dist/cli/commands/init.d.ts +45 -0
  18. package/dist/cli/commands/init.js +147 -5
  19. package/dist/cli/commands/run.js +9 -1
  20. package/dist/cli/index.d.ts +11 -0
  21. package/dist/cli/index.js +42 -8
  22. package/dist/compliance/composer.d.ts +79 -0
  23. package/dist/compliance/composer.js +258 -0
  24. package/dist/compliance/errors.d.ts +64 -0
  25. package/dist/compliance/errors.js +85 -0
  26. package/dist/compliance/loader.d.ts +52 -0
  27. package/dist/compliance/loader.js +124 -0
  28. package/dist/compliance/types.d.ts +184 -0
  29. package/dist/compliance/types.js +41 -0
  30. package/dist/compliance-clearance.d.ts +269 -0
  31. package/dist/compliance-clearance.js +269 -0
  32. package/dist/config.js +17 -0
  33. package/dist/cost-tracker.d.ts +22 -0
  34. package/dist/cost-tracker.js +41 -0
  35. package/dist/database/adapters/external.js +5 -1
  36. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  37. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  38. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  39. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  40. package/dist/embedding/cross-provider.d.ts +78 -0
  41. package/dist/embedding/cross-provider.js +75 -0
  42. package/dist/embedding/deprecation.d.ts +151 -0
  43. package/dist/embedding/deprecation.js +229 -0
  44. package/dist/embedding/errors.d.ts +90 -0
  45. package/dist/embedding/errors.js +150 -0
  46. package/dist/embedding/index.d.ts +29 -0
  47. package/dist/embedding/index.js +24 -0
  48. package/dist/embedding/pipeline-load.d.ts +146 -0
  49. package/dist/embedding/pipeline-load.js +178 -0
  50. package/dist/embedding/registry.d.ts +45 -0
  51. package/dist/embedding/registry.js +61 -0
  52. package/dist/embedding/stale-vector.d.ts +110 -0
  53. package/dist/embedding/stale-vector.js +92 -0
  54. package/dist/embedding/storage/index.d.ts +51 -0
  55. package/dist/embedding/storage/index.js +43 -0
  56. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  57. package/dist/embedding/storage/jsonl-backend.js +332 -0
  58. package/dist/embedding/storage/types.d.ts +135 -0
  59. package/dist/embedding/storage/types.js +13 -0
  60. package/dist/embedding/types.d.ts +180 -0
  61. package/dist/embedding/types.js +10 -0
  62. package/dist/execute.d.ts +29 -2
  63. package/dist/execute.js +171 -30
  64. package/dist/index.d.ts +14 -2
  65. package/dist/index.js +19 -2
  66. package/dist/journey/inheritance-validator.d.ts +396 -0
  67. package/dist/journey/inheritance-validator.js +370 -0
  68. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  69. package/dist/journey/state-id-drift-rule.js +245 -0
  70. package/dist/journey-sa2-router.d.ts +395 -0
  71. package/dist/journey-sa2-router.js +308 -0
  72. package/dist/runners/review-agent.js +6 -2
  73. package/dist/runners/runner-registry.d.ts +36 -0
  74. package/dist/runners/runner-registry.js +90 -0
  75. package/dist/runtime/attestations.d.ts +173 -13
  76. package/dist/runtime/attestations.js +252 -40
  77. package/dist/runtime/index.d.ts +1 -1
  78. package/dist/runtime/index.js +1 -1
  79. package/dist/sa-scoring/layer3-llm.js +6 -1
  80. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  81. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  82. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  83. package/dist/sa-scoring/revision-proposal.js +417 -0
  84. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  85. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  86. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  87. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  88. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  89. package/dist/signal-ingestion/adapters/manual.js +112 -0
  90. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  91. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  92. package/dist/signal-ingestion/classifier.d.ts +205 -0
  93. package/dist/signal-ingestion/classifier.js +494 -0
  94. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  95. package/dist/signal-ingestion/clustering-types.js +14 -0
  96. package/dist/signal-ingestion/clustering.d.ts +200 -0
  97. package/dist/signal-ingestion/clustering.js +413 -0
  98. package/dist/signal-ingestion/config.d.ts +351 -0
  99. package/dist/signal-ingestion/config.js +587 -0
  100. package/dist/signal-ingestion/d1.d.ts +252 -0
  101. package/dist/signal-ingestion/d1.js +235 -0
  102. package/dist/signal-ingestion/errors.d.ts +73 -0
  103. package/dist/signal-ingestion/errors.js +108 -0
  104. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  105. package/dist/signal-ingestion/governance-events.js +189 -0
  106. package/dist/signal-ingestion/index.d.ts +35 -0
  107. package/dist/signal-ingestion/index.js +53 -0
  108. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  109. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  110. package/dist/signal-ingestion/registry.d.ts +40 -0
  111. package/dist/signal-ingestion/registry.js +137 -0
  112. package/dist/signal-ingestion/residency.d.ts +227 -0
  113. package/dist/signal-ingestion/residency.js +238 -0
  114. package/dist/signal-ingestion/significance.d.ts +554 -0
  115. package/dist/signal-ingestion/significance.js +555 -0
  116. package/dist/signal-ingestion/types.d.ts +191 -0
  117. package/dist/signal-ingestion/types.js +8 -0
  118. package/dist/substrate/drift-composition.d.ts +270 -0
  119. package/dist/substrate/drift-composition.js +306 -0
  120. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  121. package/dist/substrate/drift-tui-surface.js +102 -0
  122. package/dist/substrate/identity-class.d.ts +176 -0
  123. package/dist/substrate/identity-class.js +201 -0
  124. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  125. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  126. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  127. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  128. package/dist/tessellation/rule-registry.d.ts +269 -0
  129. package/dist/tessellation/rule-registry.js +92 -0
  130. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  131. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  132. package/dist/tessellation-admission.d.ts +162 -0
  133. package/dist/tessellation-admission.js +146 -0
  134. package/dist/tessellation-drift.d.ts +246 -0
  135. package/dist/tessellation-drift.js +250 -0
  136. package/dist/validate-config.js +13 -0
  137. package/dist/validate-issue.js +2 -2
  138. package/dist/variant/cardinality-activation.d.ts +126 -0
  139. package/dist/variant/cardinality-activation.js +101 -0
  140. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  141. package/dist/variant/deprecation-lifecycle.js +208 -0
  142. package/dist/variant/drift-extension.d.ts +136 -0
  143. package/dist/variant/drift-extension.js +164 -0
  144. package/dist/variant/engineering-review.d.ts +185 -0
  145. package/dist/variant/engineering-review.js +142 -0
  146. package/dist/variant/index.d.ts +32 -0
  147. package/dist/variant/index.js +32 -0
  148. package/dist/variant/inheritance-validator.d.ts +165 -0
  149. package/dist/variant/inheritance-validator.js +139 -0
  150. package/dist/variant/internal-adopter/index.d.ts +11 -0
  151. package/dist/variant/internal-adopter/index.js +10 -0
  152. package/dist/variant/internal-adopter/products.d.ts +156 -0
  153. package/dist/variant/internal-adopter/products.js +366 -0
  154. package/dist/variant-admission.d.ts +316 -0
  155. package/dist/variant-admission.js +247 -0
  156. package/package.json +10 -8
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Default JSONL storage backend per RFC-0019 §8.2.
3
+ *
4
+ * Storage layout:
5
+ * <artifactsDir>/_embeddings/
6
+ * └── openai-text-embedding-3-small-2024-01-25.jsonl (one per provider+version)
7
+ *
8
+ * Files are named `<safeProvider>-<safeModelVersion>.jsonl` where each component
9
+ * is sanitized to `[a-zA-Z0-9._-]`. The directory itself is the index — `scan()`
10
+ * directory-walks `<embeddingsDir>/*.jsonl` and entries carry their own
11
+ * `(embeddingProvider, embeddingModelVersion)` provenance for filtering.
12
+ *
13
+ * This removes the read-modify-write race on a previous `_index.json` file
14
+ * (Iter 2 MAJOR #2): two concurrent first-writes for different
15
+ * (provider, modelVersion) tuples can no longer clobber each other because the
16
+ * index lookup is now `existsSync(<filePath>)` rather than a JSON file rewrite.
17
+ *
18
+ * Write semantics: ALL writes use an atomic read→append→temp-then-rename pattern
19
+ * (Iter 2 CRITICAL #1). The previous `appendFileSync` short path for sub-PIPE_BUF
20
+ * lines was based on a misreading of POSIX — PIPE_BUF only applies to pipes/
21
+ * FIFOs/sockets, not regular files. The unified temp-then-rename path holds
22
+ * regardless of write size and guarantees readers never see partial lines.
23
+ *
24
+ * Scale escalation thresholds (RFC-0019 OQ-1 re-walkthrough):
25
+ * > 100K entries per provider+version → emit operator-visible signal
26
+ * > p95 read latency 250ms → emit operator-visible signal
27
+ * See docs/operations/embedding-providers.md#scale-escalation for the
28
+ * JSONL→sqlite swap runbook.
29
+ *
30
+ * Concurrency contract:
31
+ * - write(): atomic temp-file-then-rename ensures readers see complete lines.
32
+ * NOTE: concurrent writes to the same file may still race at the read+append
33
+ * step (last-rename-wins). Single-writer-per-(provider,modelVersion) is the
34
+ * safe usage; multi-writer requires a per-file mutex layered above.
35
+ * - read(): linear scan on a stable file; safe to run concurrently with writes.
36
+ * - delete(): rewrites the file atomically after filtering out the entry.
37
+ *
38
+ * This backend is intentionally NOT optimized for million-vector scales.
39
+ * The interface is EmbeddingStorageBackend; adopters who hit the thresholds
40
+ * above can swap in a sqlite backend with zero consumer-code changes.
41
+ */
42
+ import type { EmbeddingStorageBackend, VectorStoreEntry, VectorStoreFilter } from './types.js';
43
+ /** Scale escalation thresholds per RFC-0019 OQ-1 re-walkthrough. */
44
+ export declare const SCALE_ESCALATION_MAX_ENTRIES = 100000;
45
+ export declare const SCALE_ESCALATION_P95_READ_MS = 250;
46
+ /** Scale escalation signal emitted when thresholds are crossed. */
47
+ export interface ScaleEscalationSignal {
48
+ type: 'count-exceeded' | 'p95-latency-exceeded';
49
+ provider: string;
50
+ modelVersion: string;
51
+ currentCount?: number;
52
+ currentP95Ms?: number;
53
+ thresholdCount?: number;
54
+ thresholdP95Ms?: number;
55
+ runbook: string;
56
+ }
57
+ /**
58
+ * JSONL embedding storage backend (default for RFC-0019 v1).
59
+ *
60
+ * Construct with the path to the artifacts directory:
61
+ * const backend = new JsonlEmbeddingStorageBackend('/path/to/.ai-sdlc/artifacts');
62
+ *
63
+ * The backend creates `<artifactsDir>/_embeddings/` on first use.
64
+ */
65
+ export declare class JsonlEmbeddingStorageBackend implements EmbeddingStorageBackend {
66
+ readonly name = "jsonl";
67
+ private readonly embeddingsDir;
68
+ /**
69
+ * Optional callback for operator-visible scale-escalation signals.
70
+ * Wire to your logging/telemetry layer; defaults to console.warn.
71
+ */
72
+ onScaleEscalation?: (signal: ScaleEscalationSignal) => void;
73
+ constructor(artifactsDir: string, options?: {
74
+ onScaleEscalation?: (signal: ScaleEscalationSignal) => void;
75
+ });
76
+ /** Ensure the embeddings directory exists (lazy init). */
77
+ private ensureDir;
78
+ /** Derive the slug used as the JSONL filename stem. */
79
+ private slug;
80
+ /** Return the JSONL file path for a given provider+version. */
81
+ private jsonlPath;
82
+ /** List all JSONL file paths in the embeddings directory. */
83
+ private listJsonlFiles;
84
+ /**
85
+ * Compute the SHA-256 hash of the source text.
86
+ * Exposed as a static helper so callers can pre-compute hashes.
87
+ */
88
+ static hashText(text: string): string;
89
+ /**
90
+ * Write an entry to the JSONL file for its (provider, modelVersion) tuple.
91
+ *
92
+ * All writes use atomic read→append→temp-then-rename: read existing file
93
+ * content, concatenate the new line, write to a temp file, then atomically
94
+ * rename over the target. This guarantees readers never see partial lines.
95
+ *
96
+ * The textHash is computed from the text if not already set (caller convenience).
97
+ *
98
+ * Single-writer-per-file safety: concurrent writes to the SAME
99
+ * (provider, modelVersion) target may still race at the read+append step
100
+ * (last-rename-wins). The intended usage is single-writer-per-tuple; if you
101
+ * need multi-writer-per-tuple, layer a per-file mutex above this backend.
102
+ */
103
+ write(entry: VectorStoreEntry): Promise<void>;
104
+ /**
105
+ * Read an entry by (textHash, provider, modelVersion).
106
+ * Returns null when not found.
107
+ *
108
+ * JSONL backend: O(n) linear scan. P95 latency should be <100ms for ≤100K entries.
109
+ * Emits a scale-escalation signal when p95 latency exceeds 250ms.
110
+ */
111
+ read(textHash: string, provider: string, modelVersion: string): Promise<VectorStoreEntry | null>;
112
+ /**
113
+ * Scan all entries matching an optional filter.
114
+ * Yields entries from each JSONL file discovered in the embeddings directory.
115
+ */
116
+ scan(filter?: VectorStoreFilter): AsyncIterable<VectorStoreEntry>;
117
+ /**
118
+ * Delete a specific entry by (textHash, provider, modelVersion).
119
+ * Rewrites the JSONL file atomically after filtering out the entry.
120
+ * No-op when the entry does not exist.
121
+ */
122
+ delete(textHash: string, provider: string, modelVersion: string): Promise<void>;
123
+ /**
124
+ * Count entries matching an optional filter.
125
+ * Used by the scale-escalation heuristic.
126
+ */
127
+ count(filter?: VectorStoreFilter): Promise<number>;
128
+ /**
129
+ * Garbage-collect entries older than `retentionDays` from the specified
130
+ * provider+version JSONL file (or all files if not specified).
131
+ *
132
+ * Entries with `writtenAt` older than `cutoffDate` are removed.
133
+ * The JSONL file is rewritten atomically via temp-then-rename.
134
+ *
135
+ * @param retentionDays - Number of days to retain entries (default 90).
136
+ * @param filter - Optional provider/modelVersion filter; absent = all files.
137
+ * @returns Count of removed entries.
138
+ */
139
+ gc(retentionDays?: number, filter?: VectorStoreFilter): Promise<number>;
140
+ /**
141
+ * Internal GC implementation; accepts a cutoff Date for testability.
142
+ */
143
+ gcWithCutoffDate(cutoff: Date, filter?: VectorStoreFilter): Promise<number>;
144
+ private _gcWithCutoff;
145
+ /** Emit a scale-escalation signal when count exceeds the threshold. */
146
+ private _maybeEmitScaleSignal;
147
+ /** Emit a scale-escalation signal when read latency exceeds the threshold. */
148
+ private _maybeEmitLatencySignal;
149
+ }
150
+ //# sourceMappingURL=jsonl-backend.d.ts.map
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Default JSONL storage backend per RFC-0019 §8.2.
3
+ *
4
+ * Storage layout:
5
+ * <artifactsDir>/_embeddings/
6
+ * └── openai-text-embedding-3-small-2024-01-25.jsonl (one per provider+version)
7
+ *
8
+ * Files are named `<safeProvider>-<safeModelVersion>.jsonl` where each component
9
+ * is sanitized to `[a-zA-Z0-9._-]`. The directory itself is the index — `scan()`
10
+ * directory-walks `<embeddingsDir>/*.jsonl` and entries carry their own
11
+ * `(embeddingProvider, embeddingModelVersion)` provenance for filtering.
12
+ *
13
+ * This removes the read-modify-write race on a previous `_index.json` file
14
+ * (Iter 2 MAJOR #2): two concurrent first-writes for different
15
+ * (provider, modelVersion) tuples can no longer clobber each other because the
16
+ * index lookup is now `existsSync(<filePath>)` rather than a JSON file rewrite.
17
+ *
18
+ * Write semantics: ALL writes use an atomic read→append→temp-then-rename pattern
19
+ * (Iter 2 CRITICAL #1). The previous `appendFileSync` short path for sub-PIPE_BUF
20
+ * lines was based on a misreading of POSIX — PIPE_BUF only applies to pipes/
21
+ * FIFOs/sockets, not regular files. The unified temp-then-rename path holds
22
+ * regardless of write size and guarantees readers never see partial lines.
23
+ *
24
+ * Scale escalation thresholds (RFC-0019 OQ-1 re-walkthrough):
25
+ * > 100K entries per provider+version → emit operator-visible signal
26
+ * > p95 read latency 250ms → emit operator-visible signal
27
+ * See docs/operations/embedding-providers.md#scale-escalation for the
28
+ * JSONL→sqlite swap runbook.
29
+ *
30
+ * Concurrency contract:
31
+ * - write(): atomic temp-file-then-rename ensures readers see complete lines.
32
+ * NOTE: concurrent writes to the same file may still race at the read+append
33
+ * step (last-rename-wins). Single-writer-per-(provider,modelVersion) is the
34
+ * safe usage; multi-writer requires a per-file mutex layered above.
35
+ * - read(): linear scan on a stable file; safe to run concurrently with writes.
36
+ * - delete(): rewrites the file atomically after filtering out the entry.
37
+ *
38
+ * This backend is intentionally NOT optimized for million-vector scales.
39
+ * The interface is EmbeddingStorageBackend; adopters who hit the thresholds
40
+ * above can swap in a sqlite backend with zero consumer-code changes.
41
+ */
42
+ import { createHash, randomUUID } from 'node:crypto';
43
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from 'node:fs';
44
+ import { join } from 'node:path';
45
+ /** Scale escalation thresholds per RFC-0019 OQ-1 re-walkthrough. */
46
+ export const SCALE_ESCALATION_MAX_ENTRIES = 100_000;
47
+ export const SCALE_ESCALATION_P95_READ_MS = 250;
48
+ /** Default runbook URL for the scale-escalation signal. */
49
+ const SCALE_RUNBOOK = 'docs/operations/embedding-providers.md#scale-escalation';
50
+ /**
51
+ * JSONL embedding storage backend (default for RFC-0019 v1).
52
+ *
53
+ * Construct with the path to the artifacts directory:
54
+ * const backend = new JsonlEmbeddingStorageBackend('/path/to/.ai-sdlc/artifacts');
55
+ *
56
+ * The backend creates `<artifactsDir>/_embeddings/` on first use.
57
+ */
58
+ export class JsonlEmbeddingStorageBackend {
59
+ name = 'jsonl';
60
+ embeddingsDir;
61
+ /**
62
+ * Optional callback for operator-visible scale-escalation signals.
63
+ * Wire to your logging/telemetry layer; defaults to console.warn.
64
+ */
65
+ onScaleEscalation;
66
+ constructor(artifactsDir, options) {
67
+ this.embeddingsDir = join(artifactsDir, '_embeddings');
68
+ if (options?.onScaleEscalation) {
69
+ this.onScaleEscalation = options.onScaleEscalation;
70
+ }
71
+ }
72
+ /** Ensure the embeddings directory exists (lazy init). */
73
+ ensureDir() {
74
+ if (!existsSync(this.embeddingsDir)) {
75
+ mkdirSync(this.embeddingsDir, { recursive: true });
76
+ }
77
+ }
78
+ /** Derive the slug used as the JSONL filename stem. */
79
+ slug(provider, modelVersion) {
80
+ // Sanitize: replace characters unsafe in filenames with '-'.
81
+ const safe = (s) => s.replace(/[^a-zA-Z0-9._-]/g, '-');
82
+ return `${safe(provider)}-${safe(modelVersion)}`;
83
+ }
84
+ /** Return the JSONL file path for a given provider+version. */
85
+ jsonlPath(provider, modelVersion) {
86
+ return join(this.embeddingsDir, `${this.slug(provider, modelVersion)}.jsonl`);
87
+ }
88
+ /** List all JSONL file paths in the embeddings directory. */
89
+ listJsonlFiles() {
90
+ if (!existsSync(this.embeddingsDir))
91
+ return [];
92
+ return readdirSync(this.embeddingsDir)
93
+ .filter((name) => name.endsWith('.jsonl'))
94
+ .map((name) => join(this.embeddingsDir, name));
95
+ }
96
+ /**
97
+ * Compute the SHA-256 hash of the source text.
98
+ * Exposed as a static helper so callers can pre-compute hashes.
99
+ */
100
+ static hashText(text) {
101
+ return createHash('sha256').update(text, 'utf-8').digest('hex');
102
+ }
103
+ /**
104
+ * Write an entry to the JSONL file for its (provider, modelVersion) tuple.
105
+ *
106
+ * All writes use atomic read→append→temp-then-rename: read existing file
107
+ * content, concatenate the new line, write to a temp file, then atomically
108
+ * rename over the target. This guarantees readers never see partial lines.
109
+ *
110
+ * The textHash is computed from the text if not already set (caller convenience).
111
+ *
112
+ * Single-writer-per-file safety: concurrent writes to the SAME
113
+ * (provider, modelVersion) target may still race at the read+append step
114
+ * (last-rename-wins). The intended usage is single-writer-per-tuple; if you
115
+ * need multi-writer-per-tuple, layer a per-file mutex above this backend.
116
+ */
117
+ async write(entry) {
118
+ this.ensureDir();
119
+ // Normalize: compute textHash if caller omitted it.
120
+ const normalized = {
121
+ ...entry,
122
+ textHash: entry.textHash || JsonlEmbeddingStorageBackend.hashText(entry.text),
123
+ writtenAt: entry.writtenAt || new Date().toISOString(),
124
+ };
125
+ const line = JSON.stringify(normalized) + '\n';
126
+ const filePath = this.jsonlPath(normalized.embeddingProvider, normalized.embeddingModelVersion);
127
+ // Always temp-then-rename for atomicity (Iter 2 CRITICAL #1 / MAJOR #3).
128
+ const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
129
+ const tmp = `${filePath}.${randomUUID()}.tmp`;
130
+ writeFileSync(tmp, existing + line, 'utf-8');
131
+ renameSync(tmp, filePath);
132
+ // Check scale escalation threshold after write.
133
+ // Count is computed lazily — only when near the threshold to avoid O(n) overhead.
134
+ await this._maybeEmitScaleSignal(normalized.embeddingProvider, normalized.embeddingModelVersion);
135
+ }
136
+ /**
137
+ * Read an entry by (textHash, provider, modelVersion).
138
+ * Returns null when not found.
139
+ *
140
+ * JSONL backend: O(n) linear scan. P95 latency should be <100ms for ≤100K entries.
141
+ * Emits a scale-escalation signal when p95 latency exceeds 250ms.
142
+ */
143
+ async read(textHash, provider, modelVersion) {
144
+ const startMs = Date.now();
145
+ const filePath = this.jsonlPath(provider, modelVersion);
146
+ if (!existsSync(filePath)) {
147
+ return null;
148
+ }
149
+ const content = readFileSync(filePath, 'utf-8');
150
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
151
+ for (const line of lines) {
152
+ try {
153
+ const entry = JSON.parse(line);
154
+ if (entry.textHash === textHash) {
155
+ const elapsedMs = Date.now() - startMs;
156
+ await this._maybeEmitLatencySignal(provider, modelVersion, elapsedMs);
157
+ return entry;
158
+ }
159
+ }
160
+ catch {
161
+ // Skip malformed lines.
162
+ }
163
+ }
164
+ const elapsedMs = Date.now() - startMs;
165
+ await this._maybeEmitLatencySignal(provider, modelVersion, elapsedMs);
166
+ return null;
167
+ }
168
+ /**
169
+ * Scan all entries matching an optional filter.
170
+ * Yields entries from each JSONL file discovered in the embeddings directory.
171
+ */
172
+ async *scan(filter) {
173
+ for (const filePath of this.listJsonlFiles()) {
174
+ const content = readFileSync(filePath, 'utf-8');
175
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
176
+ for (const line of lines) {
177
+ try {
178
+ const entry = JSON.parse(line);
179
+ if (filter?.provider && entry.embeddingProvider !== filter.provider)
180
+ continue;
181
+ if (filter?.modelVersion && entry.embeddingModelVersion !== filter.modelVersion)
182
+ continue;
183
+ yield entry;
184
+ }
185
+ catch {
186
+ // Skip malformed lines.
187
+ }
188
+ }
189
+ }
190
+ }
191
+ /**
192
+ * Delete a specific entry by (textHash, provider, modelVersion).
193
+ * Rewrites the JSONL file atomically after filtering out the entry.
194
+ * No-op when the entry does not exist.
195
+ */
196
+ async delete(textHash, provider, modelVersion) {
197
+ const filePath = this.jsonlPath(provider, modelVersion);
198
+ if (!existsSync(filePath))
199
+ return;
200
+ const content = readFileSync(filePath, 'utf-8');
201
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
202
+ const remaining = lines.filter((line) => {
203
+ try {
204
+ const entry = JSON.parse(line);
205
+ return entry.textHash !== textHash;
206
+ }
207
+ catch {
208
+ return true; // keep malformed lines (don't silently drop data)
209
+ }
210
+ });
211
+ // Atomically rewrite the file.
212
+ const tmp = `${filePath}.${randomUUID()}.tmp`;
213
+ writeFileSync(tmp, remaining.join('\n') + (remaining.length > 0 ? '\n' : ''), 'utf-8');
214
+ renameSync(tmp, filePath);
215
+ }
216
+ /**
217
+ * Count entries matching an optional filter.
218
+ * Used by the scale-escalation heuristic.
219
+ */
220
+ async count(filter) {
221
+ let total = 0;
222
+ for await (const _entry of this.scan(filter)) {
223
+ total++;
224
+ }
225
+ return total;
226
+ }
227
+ /**
228
+ * Garbage-collect entries older than `retentionDays` from the specified
229
+ * provider+version JSONL file (or all files if not specified).
230
+ *
231
+ * Entries with `writtenAt` older than `cutoffDate` are removed.
232
+ * The JSONL file is rewritten atomically via temp-then-rename.
233
+ *
234
+ * @param retentionDays - Number of days to retain entries (default 90).
235
+ * @param filter - Optional provider/modelVersion filter; absent = all files.
236
+ * @returns Count of removed entries.
237
+ */
238
+ async gc(retentionDays = 90, filter) {
239
+ const cutoff = new Date();
240
+ cutoff.setDate(cutoff.getDate() - retentionDays);
241
+ return this._gcWithCutoff(cutoff, filter);
242
+ }
243
+ /**
244
+ * Internal GC implementation; accepts a cutoff Date for testability.
245
+ */
246
+ async gcWithCutoffDate(cutoff, filter) {
247
+ return this._gcWithCutoff(cutoff, filter);
248
+ }
249
+ async _gcWithCutoff(cutoff, filter) {
250
+ let removed = 0;
251
+ for (const filePath of this.listJsonlFiles()) {
252
+ const content = readFileSync(filePath, 'utf-8');
253
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
254
+ const surviving = [];
255
+ for (const line of lines) {
256
+ try {
257
+ const entry = JSON.parse(line);
258
+ // Apply provider/modelVersion filter if set.
259
+ if (filter?.provider && entry.embeddingProvider !== filter.provider) {
260
+ surviving.push(line);
261
+ continue;
262
+ }
263
+ if (filter?.modelVersion && entry.embeddingModelVersion !== filter.modelVersion) {
264
+ surviving.push(line);
265
+ continue;
266
+ }
267
+ const writtenAt = new Date(entry.writtenAt);
268
+ if (writtenAt < cutoff) {
269
+ removed++;
270
+ }
271
+ else {
272
+ surviving.push(line);
273
+ }
274
+ }
275
+ catch {
276
+ surviving.push(line); // keep malformed lines
277
+ }
278
+ }
279
+ if (surviving.length !== lines.length) {
280
+ // Rewrite atomically only when something was removed.
281
+ const tmp = `${filePath}.${randomUUID()}.tmp`;
282
+ writeFileSync(tmp, surviving.join('\n') + (surviving.length > 0 ? '\n' : ''), 'utf-8');
283
+ renameSync(tmp, filePath);
284
+ }
285
+ }
286
+ return removed;
287
+ }
288
+ /** Emit a scale-escalation signal when count exceeds the threshold. */
289
+ async _maybeEmitScaleSignal(provider, modelVersion) {
290
+ // Only check periodically — count() is O(n) so we don't want it on every write.
291
+ // Sample 1% of writes to avoid performance impact at high write rates.
292
+ if (Math.random() > 0.01)
293
+ return;
294
+ const currentCount = await this.count({ provider, modelVersion });
295
+ if (currentCount > SCALE_ESCALATION_MAX_ENTRIES) {
296
+ const signal = {
297
+ type: 'count-exceeded',
298
+ provider,
299
+ modelVersion,
300
+ currentCount,
301
+ thresholdCount: SCALE_ESCALATION_MAX_ENTRIES,
302
+ runbook: SCALE_RUNBOOK,
303
+ };
304
+ if (this.onScaleEscalation) {
305
+ this.onScaleEscalation(signal);
306
+ }
307
+ else {
308
+ console.warn(`[embedding-storage] scale-escalation: ${provider}/${modelVersion} has ${currentCount} entries (threshold: ${SCALE_ESCALATION_MAX_ENTRIES}). See ${SCALE_RUNBOOK}`);
309
+ }
310
+ }
311
+ }
312
+ /** Emit a scale-escalation signal when read latency exceeds the threshold. */
313
+ async _maybeEmitLatencySignal(provider, modelVersion, elapsedMs) {
314
+ if (elapsedMs > SCALE_ESCALATION_P95_READ_MS) {
315
+ const signal = {
316
+ type: 'p95-latency-exceeded',
317
+ provider,
318
+ modelVersion,
319
+ currentP95Ms: elapsedMs,
320
+ thresholdP95Ms: SCALE_ESCALATION_P95_READ_MS,
321
+ runbook: SCALE_RUNBOOK,
322
+ };
323
+ if (this.onScaleEscalation) {
324
+ this.onScaleEscalation(signal);
325
+ }
326
+ else {
327
+ console.warn(`[embedding-storage] scale-escalation: read for ${provider}/${modelVersion} took ${elapsedMs}ms (threshold: ${SCALE_ESCALATION_P95_READ_MS}ms). See ${SCALE_RUNBOOK}`);
328
+ }
329
+ }
330
+ }
331
+ }
332
+ //# sourceMappingURL=jsonl-backend.js.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * EmbeddingStorageBackend interface per RFC-0019 §8.3.
3
+ *
4
+ * Pluggable storage substrate for vector embeddings. The default implementation
5
+ * is JSONL (shipped here); adopters MAY swap in sqlite, pgvector, Qdrant, etc.
6
+ * by implementing this interface and wiring it via the backend factory.
7
+ *
8
+ * The interface is intentionally minimal — reads, writes, scans, deletes,
9
+ * and counts. Higher-level semantics (provenance enforcement, stale-vector
10
+ * policy, GC orchestration) live in the consumers, not the backend.
11
+ */
12
+ /**
13
+ * Stale-vector policy per RFC-0019 OQ-2 re-walkthrough. Defines the
14
+ * behaviour when a vector is read but is stale relative to the adapter's
15
+ * current `(provider, modelVersion)`. Consumers MAY pin a stricter
16
+ * value at the API site (e.g., RFC-0009 Eτ_tessellation_drift pins
17
+ * `'fail-loud'` regardless of org default).
18
+ */
19
+ export type EmbeddingStaleVectorPolicy = 'lazy-re-embed' | 'fail-loud' | 'warn';
20
+ /**
21
+ * A single stored vector entry per RFC-0019 §8.1.
22
+ *
23
+ * Both `embeddingProvider` and `embeddingModelVersion` MUST be set on every
24
+ * write. Reads that find entries without these fields should be treated as
25
+ * legacy/corrupt and deleted by the next GC pass.
26
+ */
27
+ export interface VectorStoreEntry {
28
+ /** The embedding vector. Length MUST equal the adapter's `dimensions`. */
29
+ vector: number[];
30
+ /**
31
+ * Canonical adapter name at write time per RFC-0019 §2.3.
32
+ * Example: 'openai-text-embedding-3-small'.
33
+ * Part of the vector's identity — cross-provider comparisons are invalid.
34
+ */
35
+ embeddingProvider: string;
36
+ /**
37
+ * Adapter model version at write time per RFC-0019 §2.3.
38
+ * Example: '2024-01-25' (OpenAI snapshot date).
39
+ * Used to detect cross-version stale vectors for re-embed.
40
+ */
41
+ embeddingModelVersion: string;
42
+ /**
43
+ * ISO 8601 timestamp when this entry was written.
44
+ * Used by mtime-based GC — entries older than gcRetentionDays are removed.
45
+ */
46
+ writtenAt: string;
47
+ /**
48
+ * Original source text (REQUIRED).
49
+ * Needed for re-embed during migration (cli-embedding-bump).
50
+ * Storing the text adds disk overhead but keeps migration tractable.
51
+ */
52
+ text: string;
53
+ /**
54
+ * SHA-256 hash of `text` — used as the lookup key for read-side dedup.
55
+ * Computing hash on every lookup is wasteful; storing it eliminates the cost.
56
+ * Also usable for content-addressable storage layouts.
57
+ */
58
+ textHash: string;
59
+ /**
60
+ * Adopter-defined metadata. Opaque to the framework.
61
+ * Examples: { sourceDoc: 'rfc-0009.md', shardId: 'OQ-6' }
62
+ */
63
+ metadata?: Record<string, unknown>;
64
+ }
65
+ /**
66
+ * Filter object for scan() and count() operations.
67
+ * All fields are optional; absent = match all.
68
+ */
69
+ export interface VectorStoreFilter {
70
+ /** Restrict to entries from this embedding provider. */
71
+ provider?: string;
72
+ /** Restrict to entries from this model version. */
73
+ modelVersion?: string;
74
+ }
75
+ /**
76
+ * Pluggable storage backend interface per RFC-0019 §8.3.
77
+ *
78
+ * The default implementation is JSONL (JsonlEmbeddingStorageBackend).
79
+ * Adopters who need indexed lookups beyond JSONL's linear-scan capability
80
+ * (rough threshold: >100K entries per provider+version, or p95 read >250ms)
81
+ * should swap in a sqlite or vector-DB backend.
82
+ */
83
+ export interface EmbeddingStorageBackend {
84
+ /**
85
+ * Canonical backend name.
86
+ * Built-in names: 'jsonl'.
87
+ * Adopter backends should use a unique name (e.g., 'sqlite', 'pgvector').
88
+ */
89
+ readonly name: string;
90
+ /**
91
+ * Write an entry to the store.
92
+ *
93
+ * Implementations MUST be safe to call concurrently from multiple async
94
+ * contexts. JSONL backend uses atomic append semantics (write-temp-rename).
95
+ *
96
+ * @param entry - The entry to write.
97
+ */
98
+ write(entry: VectorStoreEntry): Promise<void>;
99
+ /**
100
+ * Read an entry by textHash + provider + modelVersion triple.
101
+ * Returns null when no matching entry exists.
102
+ *
103
+ * JSONL backend: O(n) linear scan — acceptable up to ~100K entries.
104
+ *
105
+ * @param textHash - SHA-256 hash of the source text.
106
+ * @param provider - Adapter name (e.g., 'openai-text-embedding-3-small').
107
+ * @param modelVersion - Adapter model version (e.g., '2024-01-25').
108
+ */
109
+ read(textHash: string, provider: string, modelVersion: string): Promise<VectorStoreEntry | null>;
110
+ /**
111
+ * Scan all entries matching an optional filter.
112
+ * Returns an async iterator yielding one entry at a time.
113
+ * Implementations MUST yield in the order entries appear in the store.
114
+ *
115
+ * @param filter - Optional filter; absent fields match all.
116
+ */
117
+ scan(filter?: VectorStoreFilter): AsyncIterable<VectorStoreEntry>;
118
+ /**
119
+ * Delete a specific entry by textHash + provider + modelVersion triple.
120
+ * No-op when the entry does not exist.
121
+ *
122
+ * @param textHash - SHA-256 hash of the source text.
123
+ * @param provider - Adapter name.
124
+ * @param modelVersion - Adapter model version.
125
+ */
126
+ delete(textHash: string, provider: string, modelVersion: string): Promise<void>;
127
+ /**
128
+ * Count entries matching an optional filter.
129
+ * Used by the scale-escalation heuristic — emits operator signal at >100K.
130
+ *
131
+ * @param filter - Optional filter; absent fields match all.
132
+ */
133
+ count(filter?: VectorStoreFilter): Promise<number>;
134
+ }
135
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * EmbeddingStorageBackend interface per RFC-0019 §8.3.
3
+ *
4
+ * Pluggable storage substrate for vector embeddings. The default implementation
5
+ * is JSONL (shipped here); adopters MAY swap in sqlite, pgvector, Qdrant, etc.
6
+ * by implementing this interface and wiring it via the backend factory.
7
+ *
8
+ * The interface is intentionally minimal — reads, writes, scans, deletes,
9
+ * and counts. Higher-level semantics (provenance enforcement, stale-vector
10
+ * policy, GC orchestration) live in the consumers, not the backend.
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=types.js.map