@duckcodeailabs/dql-agent 1.6.21 → 1.6.22

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 (160) hide show
  1. package/dist/agent-run-engine.d.ts +287 -0
  2. package/dist/agent-run-engine.d.ts.map +1 -0
  3. package/dist/agent-run-engine.js +916 -0
  4. package/dist/agent-run-engine.js.map +1 -0
  5. package/dist/agent-run-gates.d.ts +26 -0
  6. package/dist/agent-run-gates.d.ts.map +1 -0
  7. package/dist/agent-run-gates.js +170 -0
  8. package/dist/agent-run-gates.js.map +1 -0
  9. package/dist/agent-run-planner.d.ts +32 -0
  10. package/dist/agent-run-planner.d.ts.map +1 -0
  11. package/dist/agent-run-planner.js +280 -0
  12. package/dist/agent-run-planner.js.map +1 -0
  13. package/dist/answer-loop.d.ts +49 -0
  14. package/dist/answer-loop.d.ts.map +1 -1
  15. package/dist/answer-loop.js +292 -17
  16. package/dist/answer-loop.js.map +1 -1
  17. package/dist/app-builder.d.ts +6 -0
  18. package/dist/app-builder.d.ts.map +1 -1
  19. package/dist/app-builder.js +95 -2
  20. package/dist/app-builder.js.map +1 -1
  21. package/dist/app-planner.d.ts +78 -0
  22. package/dist/app-planner.d.ts.map +1 -0
  23. package/dist/app-planner.js +145 -0
  24. package/dist/app-planner.js.map +1 -0
  25. package/dist/embeddings/provider.d.ts +66 -0
  26. package/dist/embeddings/provider.d.ts.map +1 -0
  27. package/dist/embeddings/provider.js +105 -0
  28. package/dist/embeddings/provider.js.map +1 -0
  29. package/dist/hints/git-store.d.ts +69 -0
  30. package/dist/hints/git-store.d.ts.map +1 -0
  31. package/dist/hints/git-store.js +244 -0
  32. package/dist/hints/git-store.js.map +1 -0
  33. package/dist/hints/retrieval.d.ts +49 -0
  34. package/dist/hints/retrieval.d.ts.map +1 -0
  35. package/dist/hints/retrieval.js +60 -0
  36. package/dist/hints/retrieval.js.map +1 -0
  37. package/dist/hints/store.d.ts +39 -0
  38. package/dist/hints/store.d.ts.map +1 -0
  39. package/dist/hints/store.js +279 -0
  40. package/dist/hints/store.js.map +1 -0
  41. package/dist/hints/types.d.ts +147 -0
  42. package/dist/hints/types.d.ts.map +1 -0
  43. package/dist/hints/types.js +92 -0
  44. package/dist/hints/types.js.map +1 -0
  45. package/dist/index.d.ts +43 -6
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +35 -3
  48. package/dist/index.js.map +1 -1
  49. package/dist/intent-controller.d.ts +63 -0
  50. package/dist/intent-controller.d.ts.map +1 -0
  51. package/dist/intent-controller.js +150 -0
  52. package/dist/intent-controller.js.map +1 -0
  53. package/dist/kg/build.d.ts.map +1 -1
  54. package/dist/kg/build.js +2 -0
  55. package/dist/kg/build.js.map +1 -1
  56. package/dist/kg/sqlite-fts.js +4 -0
  57. package/dist/kg/sqlite-fts.js.map +1 -1
  58. package/dist/kg/types.d.ts +10 -0
  59. package/dist/kg/types.d.ts.map +1 -1
  60. package/dist/metadata/catalog.d.ts +81 -4
  61. package/dist/metadata/catalog.d.ts.map +1 -1
  62. package/dist/metadata/catalog.js +208 -1
  63. package/dist/metadata/catalog.js.map +1 -1
  64. package/dist/metadata/drafts.d.ts +22 -0
  65. package/dist/metadata/drafts.d.ts.map +1 -1
  66. package/dist/metadata/drafts.js +126 -2
  67. package/dist/metadata/drafts.js.map +1 -1
  68. package/dist/metadata/grain-gate.d.ts +61 -0
  69. package/dist/metadata/grain-gate.d.ts.map +1 -0
  70. package/dist/metadata/grain-gate.js +265 -0
  71. package/dist/metadata/grain-gate.js.map +1 -0
  72. package/dist/metadata/identity.d.ts +47 -0
  73. package/dist/metadata/identity.d.ts.map +1 -0
  74. package/dist/metadata/identity.js +147 -0
  75. package/dist/metadata/identity.js.map +1 -0
  76. package/dist/metadata/metric-match.d.ts +93 -0
  77. package/dist/metadata/metric-match.d.ts.map +1 -0
  78. package/dist/metadata/metric-match.js +233 -0
  79. package/dist/metadata/metric-match.js.map +1 -0
  80. package/dist/metadata/sql-grounding.d.ts +149 -0
  81. package/dist/metadata/sql-grounding.d.ts.map +1 -0
  82. package/dist/metadata/sql-grounding.js +397 -0
  83. package/dist/metadata/sql-grounding.js.map +1 -0
  84. package/dist/metadata/sql-retrieval.d.ts +29 -0
  85. package/dist/metadata/sql-retrieval.d.ts.map +1 -0
  86. package/dist/metadata/sql-retrieval.js +79 -0
  87. package/dist/metadata/sql-retrieval.js.map +1 -0
  88. package/dist/narrate.d.ts +52 -0
  89. package/dist/narrate.d.ts.map +1 -0
  90. package/dist/narrate.js +263 -0
  91. package/dist/narrate.js.map +1 -0
  92. package/dist/propose/build-from-prompt.d.ts +160 -0
  93. package/dist/propose/build-from-prompt.d.ts.map +1 -0
  94. package/dist/propose/build-from-prompt.js +815 -0
  95. package/dist/propose/build-from-prompt.js.map +1 -0
  96. package/dist/propose/classify.d.ts +55 -0
  97. package/dist/propose/classify.d.ts.map +1 -0
  98. package/dist/propose/classify.js +167 -0
  99. package/dist/propose/classify.js.map +1 -0
  100. package/dist/propose/config.d.ts +39 -0
  101. package/dist/propose/config.d.ts.map +1 -0
  102. package/dist/propose/config.js +52 -0
  103. package/dist/propose/config.js.map +1 -0
  104. package/dist/propose/dbt-artifacts.d.ts +134 -0
  105. package/dist/propose/dbt-artifacts.d.ts.map +1 -0
  106. package/dist/propose/dbt-artifacts.js +320 -0
  107. package/dist/propose/dbt-artifacts.js.map +1 -0
  108. package/dist/propose/edit-block.d.ts +74 -0
  109. package/dist/propose/edit-block.d.ts.map +1 -0
  110. package/dist/propose/edit-block.js +133 -0
  111. package/dist/propose/edit-block.js.map +1 -0
  112. package/dist/propose/enrich.d.ts +48 -0
  113. package/dist/propose/enrich.d.ts.map +1 -0
  114. package/dist/propose/enrich.js +125 -0
  115. package/dist/propose/enrich.js.map +1 -0
  116. package/dist/propose/generate-sql.d.ts +61 -0
  117. package/dist/propose/generate-sql.d.ts.map +1 -0
  118. package/dist/propose/generate-sql.js +180 -0
  119. package/dist/propose/generate-sql.js.map +1 -0
  120. package/dist/propose/index.d.ts +24 -0
  121. package/dist/propose/index.d.ts.map +1 -0
  122. package/dist/propose/index.js +15 -0
  123. package/dist/propose/index.js.map +1 -0
  124. package/dist/propose/propose.d.ts +226 -0
  125. package/dist/propose/propose.d.ts.map +1 -0
  126. package/dist/propose/propose.js +668 -0
  127. package/dist/propose/propose.js.map +1 -0
  128. package/dist/propose/reflect-block.d.ts +99 -0
  129. package/dist/propose/reflect-block.d.ts.map +1 -0
  130. package/dist/propose/reflect-block.js +220 -0
  131. package/dist/propose/reflect-block.js.map +1 -0
  132. package/dist/propose/write-draft.d.ts +90 -0
  133. package/dist/propose/write-draft.d.ts.map +1 -0
  134. package/dist/propose/write-draft.js +185 -0
  135. package/dist/propose/write-draft.js.map +1 -0
  136. package/dist/providers/claude.d.ts +13 -1
  137. package/dist/providers/claude.d.ts.map +1 -1
  138. package/dist/providers/claude.js +20 -2
  139. package/dist/providers/claude.js.map +1 -1
  140. package/dist/providers/gemini.d.ts +13 -1
  141. package/dist/providers/gemini.d.ts.map +1 -1
  142. package/dist/providers/gemini.js +24 -3
  143. package/dist/providers/gemini.js.map +1 -1
  144. package/dist/providers/index.d.ts +3 -3
  145. package/dist/providers/index.d.ts.map +1 -1
  146. package/dist/providers/index.js +3 -3
  147. package/dist/providers/index.js.map +1 -1
  148. package/dist/research-loop.d.ts +62 -0
  149. package/dist/research-loop.d.ts.map +1 -0
  150. package/dist/research-loop.js +210 -0
  151. package/dist/research-loop.js.map +1 -0
  152. package/dist/skills/defaults.d.ts +37 -0
  153. package/dist/skills/defaults.d.ts.map +1 -0
  154. package/dist/skills/defaults.js +152 -0
  155. package/dist/skills/defaults.js.map +1 -0
  156. package/dist/skills/loader.d.ts +60 -0
  157. package/dist/skills/loader.d.ts.map +1 -1
  158. package/dist/skills/loader.js +164 -2
  159. package/dist/skills/loader.js.map +1 -1
  160. package/package.json +7 -5
@@ -0,0 +1,145 @@
1
+ /**
2
+ * App planner (P1) — turn a business goal into a planned decision surface, not a
3
+ * keyword-ranked grid of blocks.
4
+ *
5
+ * Given a goal ("revenue by region"), the planner decomposes it into the questions
6
+ * an app should answer (the headline number, its trend, breakdowns), matches each
7
+ * to a governed metric + certified block, derives the shared filters that let one
8
+ * control refresh every tile, writes a narrative, and reports COVERAGE + GAPS
9
+ * (questions with no certified block — what the reflect-before-certify loop should
10
+ * draft). Deterministic + offline; the metric match reuses the spec-17 matcher.
11
+ */
12
+ import { matchSemanticMetric } from './metadata/metric-match.js';
13
+ const TIME_DIM_RE = /(_at$|_date$|_time$|_ts$|^date$|^month$|^week$|^day$|ordered_at|created)/i;
14
+ const NUMERIC_CONTROL_RE = /(top[_-]?n|limit|count|number|amount|year|season|score)/i;
15
+ /** Classify a dimension into the dashboard control kind the filter engine renders. */
16
+ function classifyControl(dimension) {
17
+ if (TIME_DIM_RE.test(dimension))
18
+ return 'daterange';
19
+ if (NUMERIC_CONTROL_RE.test(dimension))
20
+ return 'number';
21
+ // A categorical dimension renders as a dropdown the filter-options endpoint fills.
22
+ return 'select';
23
+ }
24
+ function humanize(name) {
25
+ return name.replace(/[_.]+/g, ' ').replace(/\s+/g, ' ').trim();
26
+ }
27
+ /** Pull explicit "by <dimension>" requests from the goal. */
28
+ function extractRequestedDimensions(goal) {
29
+ const dims = [];
30
+ const re = /\bby\s+([a-z_][a-z0-9_]*)/gi;
31
+ let m;
32
+ while ((m = re.exec(goal.toLowerCase()))) {
33
+ const d = m[1];
34
+ if (d && d !== 'the' && !dims.includes(d))
35
+ dims.push(d);
36
+ }
37
+ return dims;
38
+ }
39
+ /** Blocks that wrap this governed metric (so we know which tiles can cover it). */
40
+ function blocksForMetric(blocks, metric) {
41
+ if (!metric)
42
+ return [];
43
+ const wanted = metric.toLowerCase();
44
+ const leaf = wanted.split('.').pop();
45
+ return blocks.filter((b) => {
46
+ const ref = (b.metricRef ?? '').toLowerCase();
47
+ return ref === wanted || ref.split('.').pop() === leaf;
48
+ });
49
+ }
50
+ /** A time dimension available on the metric's backing blocks, if any. */
51
+ function findTimeDimension(blocks) {
52
+ for (const block of blocks) {
53
+ for (const f of [...(block.allowedFilters ?? []), ...(block.dimensions ?? [])]) {
54
+ if (TIME_DIM_RE.test(f))
55
+ return f;
56
+ }
57
+ }
58
+ return undefined;
59
+ }
60
+ /** Non-time filterable dimensions across the metric's blocks. */
61
+ function categoricalDimensions(blocks) {
62
+ const out = [];
63
+ for (const block of blocks) {
64
+ for (const f of block.allowedFilters ?? []) {
65
+ if (!TIME_DIM_RE.test(f) && !out.includes(f))
66
+ out.push(f);
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ function coverSection(question, kind, metric, dimension, metricBlocks) {
72
+ // A block covers the section when it wraps the metric and (for a breakdown) the
73
+ // dimension is one of its declared filters.
74
+ const block = metricBlocks.find((b) => !dimension || (b.allowedFilters ?? []).includes(dimension) || (b.dimensions ?? []).includes(dimension));
75
+ return { question, kind, metric, dimension, blockName: block?.name, covered: Boolean(block) };
76
+ }
77
+ /**
78
+ * Plan an app from a business goal. Reuses the governed-metric matcher to anchor
79
+ * the plan on a real metric, then decomposes into KPI + trend + breakdown sections,
80
+ * maps each to a certified block, and reports coverage + gaps.
81
+ */
82
+ export async function planApp(input) {
83
+ // Anchor on a metric that ALREADY has certified blocks to build from (so the app
84
+ // is assemblable), falling back to the best overall match when none is backed —
85
+ // the unbacked match then surfaces as a gap the build-and-certify loop fills.
86
+ const backedNames = new Set(input.blocks
87
+ .map((b) => b.metricRef?.toLowerCase())
88
+ .filter((ref) => Boolean(ref))
89
+ .flatMap((ref) => [ref, ref.split('.').pop() ?? ref]));
90
+ const backedMetrics = input.metrics.filter((m) => backedNames.has(m.name.toLowerCase()) || backedNames.has(m.name.toLowerCase().split('.').pop() ?? ''));
91
+ const match = (backedMetrics.length > 0 ? await matchSemanticMetric(input.goal, backedMetrics).catch(() => null) : null) ??
92
+ (await matchSemanticMetric(input.goal, input.metrics).catch(() => null));
93
+ const metricName = match?.metric.name;
94
+ const label = humanize(metricName ?? input.goal).slice(0, 60) || 'metric';
95
+ const metricBlocks = blocksForMetric(input.blocks, metricName);
96
+ const sections = [];
97
+ // 1) Headline KPI — the metric overall.
98
+ sections.push(coverSection(`Total ${label}`, 'kpi', metricName, undefined, metricBlocks));
99
+ // 2) Trend — the metric over a time dimension, when one is available.
100
+ const timeDim = findTimeDimension(metricBlocks);
101
+ if (timeDim) {
102
+ sections.push(coverSection(`${label} over time`, 'trend', metricName, timeDim, metricBlocks));
103
+ }
104
+ // 3) Breakdowns — by the requested dimensions, else the top available categoricals.
105
+ const requested = extractRequestedDimensions(input.goal).filter((d) => !TIME_DIM_RE.test(d));
106
+ const available = categoricalDimensions(metricBlocks);
107
+ const breakdownDims = (requested.length > 0 ? requested : available).slice(0, 3);
108
+ for (const dim of breakdownDims) {
109
+ sections.push(coverSection(`${label} by ${humanize(dim)}`, 'breakdown', metricName, dim, metricBlocks));
110
+ }
111
+ const sharedFilters = Array.from(new Set(sections.map((s) => s.dimension).filter((d) => Boolean(d))));
112
+ const covered = sections.filter((s) => s.covered).length;
113
+ const gaps = sections.filter((s) => !s.covered).map((s) => s.question);
114
+ const dimsText = sharedFilters.map(humanize).join(', ');
115
+ // Global filter bar (P3): one control per shared dimension, with the EXPLICIT set
116
+ // of covered tiles it refreshes (a covered section whose backing block declares the
117
+ // dimension). A control that refreshes nothing flags a filter waiting on a block.
118
+ const blockByName = new Map(metricBlocks.map((b) => [b.name, b]));
119
+ const declaresFilter = (section, dim) => {
120
+ const block = section.blockName ? blockByName.get(section.blockName) : undefined;
121
+ if (!block)
122
+ return false;
123
+ return (block.allowedFilters ?? []).includes(dim) || (block.dimensions ?? []).includes(dim);
124
+ };
125
+ const globalFilters = sharedFilters.map((dimension) => ({
126
+ dimension,
127
+ control: classifyControl(dimension),
128
+ refreshes: sections
129
+ .filter((s) => s.covered && declaresFilter(s, dimension))
130
+ .map((s) => s.question),
131
+ }));
132
+ return {
133
+ title: `${label} overview`,
134
+ narrative: `Tracks ${label}: the headline number` +
135
+ (timeDim ? ', its trend over time' : '') +
136
+ (sharedFilters.length > 0 ? `, and a breakdown by ${dimsText}.` : '.') +
137
+ ` One filter set refreshes every tile.`,
138
+ sections,
139
+ sharedFilters,
140
+ globalFilters,
141
+ gaps,
142
+ coverage: sections.length > 0 ? covered / sections.length : 0,
143
+ };
144
+ }
145
+ //# sourceMappingURL=app-planner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-planner.js","sourceRoot":"","sources":["../src/app-planner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pluggable semantic retrieval (Part B).
3
+ *
4
+ * `EmbeddingProvider` is the interface real providers (a local model, an API)
5
+ * plug into. The DEFAULT provider is a deterministic, offline, hashed-token
6
+ * vector — NO heavy model dependency is added. FTS5/BM25 remains the safe
7
+ * default ranker; hybrid ranking blends FTS relevance with cosine similarity
8
+ * from the configured provider.
9
+ *
10
+ * Determinism matters: the default provider must produce identical vectors for
11
+ * identical text so tests are stable offline.
12
+ */
13
+ export interface EmbeddingProvider {
14
+ /** Provider id, surfaced in diagnostics, e.g. "hashed-token-v1". */
15
+ readonly id: string;
16
+ /** Vector dimensionality. */
17
+ readonly dimensions: number;
18
+ /** Embed a batch of texts. Must be deterministic for the default provider. */
19
+ embed(texts: string[]): Promise<number[][]>;
20
+ }
21
+ /** Cosine similarity in [-1, 1]; 0 for degenerate vectors. */
22
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
23
+ /**
24
+ * Deterministic, dependency-free embedding via the hashing trick: each token is
25
+ * hashed to a bucket and a sign; counts accumulate into a fixed-width vector,
26
+ * then the vector is L2-normalised. Captures lexical overlap so semantically
27
+ * similar (shared-token) texts score higher under cosine similarity — enough to
28
+ * make hybrid ranking testable offline without any model.
29
+ */
30
+ export declare class HashedTokenEmbeddingProvider implements EmbeddingProvider {
31
+ readonly id = "hashed-token-v1";
32
+ readonly dimensions: number;
33
+ constructor(dimensions?: number);
34
+ embed(texts: string[]): Promise<number[][]>;
35
+ embedOne(text: string): number[];
36
+ }
37
+ /** The safe default provider: deterministic + offline. */
38
+ export declare function defaultEmbeddingProvider(): EmbeddingProvider;
39
+ export interface HybridRankItem<T> {
40
+ item: T;
41
+ /** Text the item is embedded/searched on. */
42
+ text: string;
43
+ /** FTS/BM25-derived relevance in [0, 1] (higher better). */
44
+ ftsScore: number;
45
+ }
46
+ export interface HybridRankOptions {
47
+ /**
48
+ * Weight on the vector-similarity component in [0, 1]. 0 = pure FTS5 (the safe
49
+ * default). The blended score is `(1 - alpha) * fts + alpha * cosine01`.
50
+ */
51
+ alpha?: number;
52
+ provider?: EmbeddingProvider;
53
+ }
54
+ export interface HybridRanked<T> {
55
+ item: T;
56
+ ftsScore: number;
57
+ vectorScore: number;
58
+ score: number;
59
+ }
60
+ /**
61
+ * Blend FTS5 relevance with embedding cosine similarity. When `alpha` is 0 (the
62
+ * default) this is a pure pass-through of the FTS ordering — FTS5 stays the safe
63
+ * default and embeddings are strictly additive/opt-in.
64
+ */
65
+ export declare function hybridRank<T>(query: string, items: HybridRankItem<T>[], options?: HybridRankOptions): Promise<HybridRanked<T>[]>;
66
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/embeddings/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;CAC7C;AAED,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAYjE;AAQD;;;;;;GAMG;AACH,qBAAa,4BAA6B,YAAW,iBAAiB;IACpE,QAAQ,CAAC,EAAE,qBAAqB;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,UAAU,SAAM;IAItB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IAIjD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;CAejC;AAQD,0DAA0D;AAC1D,wBAAgB,wBAAwB,IAAI,iBAAiB,CAE5D;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,EAAE,CAAC,CAAC;IACR,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAiB5B"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Pluggable semantic retrieval (Part B).
3
+ *
4
+ * `EmbeddingProvider` is the interface real providers (a local model, an API)
5
+ * plug into. The DEFAULT provider is a deterministic, offline, hashed-token
6
+ * vector — NO heavy model dependency is added. FTS5/BM25 remains the safe
7
+ * default ranker; hybrid ranking blends FTS relevance with cosine similarity
8
+ * from the configured provider.
9
+ *
10
+ * Determinism matters: the default provider must produce identical vectors for
11
+ * identical text so tests are stable offline.
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+ /** Cosine similarity in [-1, 1]; 0 for degenerate vectors. */
15
+ export function cosineSimilarity(a, b) {
16
+ if (a.length === 0 || a.length !== b.length)
17
+ return 0;
18
+ let dot = 0;
19
+ let na = 0;
20
+ let nb = 0;
21
+ for (let i = 0; i < a.length; i += 1) {
22
+ dot += a[i] * b[i];
23
+ na += a[i] * a[i];
24
+ nb += b[i] * b[i];
25
+ }
26
+ if (na === 0 || nb === 0)
27
+ return 0;
28
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
29
+ }
30
+ const TOKEN_RE = /[\p{L}\p{N}_]+/gu;
31
+ function tokenize(text) {
32
+ return (text.toLowerCase().match(TOKEN_RE) ?? []).filter((t) => t.length > 1);
33
+ }
34
+ /**
35
+ * Deterministic, dependency-free embedding via the hashing trick: each token is
36
+ * hashed to a bucket and a sign; counts accumulate into a fixed-width vector,
37
+ * then the vector is L2-normalised. Captures lexical overlap so semantically
38
+ * similar (shared-token) texts score higher under cosine similarity — enough to
39
+ * make hybrid ranking testable offline without any model.
40
+ */
41
+ export class HashedTokenEmbeddingProvider {
42
+ id = 'hashed-token-v1';
43
+ dimensions;
44
+ constructor(dimensions = 256) {
45
+ this.dimensions = Math.max(16, dimensions);
46
+ }
47
+ async embed(texts) {
48
+ return texts.map((text) => this.embedOne(text));
49
+ }
50
+ embedOne(text) {
51
+ const vec = new Array(this.dimensions).fill(0);
52
+ for (const token of tokenize(text)) {
53
+ const h = hashToken(token);
54
+ const bucket = h % this.dimensions;
55
+ const sign = (h >>> 8) % 2 === 0 ? 1 : -1;
56
+ vec[bucket] += sign;
57
+ }
58
+ // L2 normalise so cosine reduces to a dot product and magnitudes are comparable.
59
+ let norm = 0;
60
+ for (const v of vec)
61
+ norm += v * v;
62
+ norm = Math.sqrt(norm);
63
+ if (norm === 0)
64
+ return vec;
65
+ return vec.map((v) => v / norm);
66
+ }
67
+ }
68
+ function hashToken(token) {
69
+ const digest = createHash('sha1').update(token).digest();
70
+ // First 4 bytes as an unsigned int.
71
+ return ((digest[0] << 24) | (digest[1] << 16) | (digest[2] << 8) | digest[3]) >>> 0;
72
+ }
73
+ /** The safe default provider: deterministic + offline. */
74
+ export function defaultEmbeddingProvider() {
75
+ return new HashedTokenEmbeddingProvider();
76
+ }
77
+ /**
78
+ * Blend FTS5 relevance with embedding cosine similarity. When `alpha` is 0 (the
79
+ * default) this is a pure pass-through of the FTS ordering — FTS5 stays the safe
80
+ * default and embeddings are strictly additive/opt-in.
81
+ */
82
+ export async function hybridRank(query, items, options = {}) {
83
+ const alpha = clamp01(options.alpha ?? 0);
84
+ if (alpha === 0 || items.length === 0) {
85
+ return items
86
+ .map((entry) => ({ item: entry.item, ftsScore: entry.ftsScore, vectorScore: 0, score: entry.ftsScore }))
87
+ .sort((a, b) => b.score - a.score);
88
+ }
89
+ const provider = options.provider ?? defaultEmbeddingProvider();
90
+ const [queryVec, ...itemVecs] = await provider.embed([query, ...items.map((entry) => entry.text)]);
91
+ return items
92
+ .map((entry, index) => {
93
+ const cosine = cosineSimilarity(queryVec, itemVecs[index] ?? []);
94
+ const cosine01 = (cosine + 1) / 2; // map [-1,1] → [0,1]
95
+ const score = (1 - alpha) * entry.ftsScore + alpha * cosine01;
96
+ return { item: entry.item, ftsScore: entry.ftsScore, vectorScore: cosine01, score };
97
+ })
98
+ .sort((a, b) => b.score - a.score);
99
+ }
100
+ function clamp01(value) {
101
+ if (!Number.isFinite(value))
102
+ return 0;
103
+ return Math.max(0, Math.min(1, value));
104
+ }
105
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/embeddings/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAWzC,8DAA8D;AAC9D,MAAM,UAAU,gBAAgB,CAAC,CAAW,EAAE,CAAW;IACvD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,CAAC,CAAC;IACtD,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACnB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACnC,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,QAAQ,GAAG,kBAAkB,CAAC;AAEpC,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,4BAA4B;IAC9B,EAAE,GAAG,iBAAiB,CAAC;IACvB,UAAU,CAAS;IAE5B,YAAY,UAAU,GAAG,GAAG;QAC1B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAe;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,QAAQ,CAAC,IAAY;QACnB,MAAM,GAAG,GAAG,IAAI,KAAK,CAAS,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACvD,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,MAAM,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YAC3B,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC;YACnC,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;QACtB,CAAC;QACD,iFAAiF;QACjF,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,KAAK,MAAM,CAAC,IAAI,GAAG;YAAE,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,GAAG,CAAC;QAC3B,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,CAAC;CACF;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;IACzD,oCAAoC;IACpC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACtF,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,wBAAwB;IACtC,OAAO,IAAI,4BAA4B,EAAE,CAAC;AAC5C,CAAC;AA0BD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAAa,EACb,KAA0B,EAC1B,UAA6B,EAAE;IAE/B,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC;IAC1C,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtC,OAAO,KAAK;aACT,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;aACvG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC;IACD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,wBAAwB,EAAE,CAAC;IAChE,MAAM,CAAC,QAAQ,EAAE,GAAG,QAAQ,CAAC,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnG,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACpB,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,qBAAqB;QACxD,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;QAC9D,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACtF,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,OAAO,CAAC,KAAa;IAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Git-authoritative store for scoped correction memory.
3
+ *
4
+ * Files (the source of truth, consistent with DQL):
5
+ * - `.dql/traces/<id>.trace.json` — correction traces
6
+ * - `.dql/hints/<id>.hint.yaml` — candidate / approved / rejected hints
7
+ * - `.dql/reviews/<id>.review.yaml` — human review decisions
8
+ *
9
+ * `.dql/cache/agent-kg.sqlite` is a rebuildable index (see {@link HintStore}).
10
+ *
11
+ * The lifecycle:
12
+ * recordCorrectionTrace() → derives a `candidate` hint
13
+ * reviewHint('approved') → flips status to `approved`, writes the review,
14
+ * and reindexes SQLite
15
+ * Approved-only is enforced at retrieval; nothing here auto-applies a hint.
16
+ */
17
+ import type { CorrectionTrace, Hint, HintReview, HintScope } from './types.js';
18
+ export declare function tracesDir(projectRoot: string): string;
19
+ export declare function hintsDir(projectRoot: string): string;
20
+ export declare function reviewsDir(projectRoot: string): string;
21
+ export declare function defaultHintIndexPath(projectRoot: string): string;
22
+ export interface RecordCorrectionTraceInput {
23
+ question: string;
24
+ scope: HintScope;
25
+ wrongAnswer: string;
26
+ correction: string;
27
+ rationale?: string;
28
+ author?: string;
29
+ anchorObjectKey?: string;
30
+ /** Override the derived candidate hint's title. */
31
+ hintTitle?: string;
32
+ /** Override the derived candidate hint's guidance (defaults to the correction). */
33
+ hintGuidance?: string;
34
+ correctedSql?: string;
35
+ tags?: string[];
36
+ }
37
+ export interface RecordCorrectionTraceResult {
38
+ trace: CorrectionTrace;
39
+ hint: Hint;
40
+ }
41
+ /**
42
+ * Record a Tier-2 correction as a Git trace + a derived **candidate** hint.
43
+ * The hint is NOT usable until reviewed/approved.
44
+ */
45
+ export declare function recordCorrectionTrace(projectRoot: string, input: RecordCorrectionTraceInput): RecordCorrectionTraceResult;
46
+ export declare function hintFilePath(projectRoot: string, hintId: string): string;
47
+ export declare function writeHintFile(projectRoot: string, hint: Hint): void;
48
+ export declare function readHintFile(path: string, sourcePath?: string): Hint | null;
49
+ export declare function listHintsFromGit(projectRoot: string): Hint[];
50
+ export declare function getHintFromGit(projectRoot: string, hintId: string): Hint | null;
51
+ export interface ReviewHintInput {
52
+ hintId: string;
53
+ decision: 'approved' | 'rejected';
54
+ reviewer: string;
55
+ note?: string;
56
+ }
57
+ export interface ReviewHintResult {
58
+ hint: Hint;
59
+ review: HintReview;
60
+ }
61
+ /**
62
+ * Approve or reject a candidate hint. Writes the review record, flips the hint
63
+ * file's status, and reindexes SQLite. Approval is the ONLY path that makes a
64
+ * hint usable in normal retrieval.
65
+ */
66
+ export declare function reviewHint(projectRoot: string, input: ReviewHintInput): ReviewHintResult | null;
67
+ /** Rebuild the SQLite hint index from the Git-authoritative files. */
68
+ export declare function reindexHints(projectRoot: string, indexPath?: string): number;
69
+ //# sourceMappingURL=git-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-store.d.ts","sourceRoot":"","sources":["../../src/hints/git-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH,OAAO,KAAK,EACV,eAAe,EACf,IAAI,EACJ,UAAU,EACV,SAAS,EACV,MAAM,YAAY,CAAC;AAMpB,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAErD;AACD,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEpD;AACD,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEhE;AAYD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,eAAe,CAAC;IACvB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,0BAA0B,GAChC,2BAA2B,CA6C7B;AAID,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAsBnE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAiC3E;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,EAAE,CAW5D;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAG/E;AAID,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,UAAU,GAAG,UAAU,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CA+B/F;AAID,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,SAAoC,GAAG,MAAM,CASvG"}
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Git-authoritative store for scoped correction memory.
3
+ *
4
+ * Files (the source of truth, consistent with DQL):
5
+ * - `.dql/traces/<id>.trace.json` — correction traces
6
+ * - `.dql/hints/<id>.hint.yaml` — candidate / approved / rejected hints
7
+ * - `.dql/reviews/<id>.review.yaml` — human review decisions
8
+ *
9
+ * `.dql/cache/agent-kg.sqlite` is a rebuildable index (see {@link HintStore}).
10
+ *
11
+ * The lifecycle:
12
+ * recordCorrectionTrace() → derives a `candidate` hint
13
+ * reviewHint('approved') → flips status to `approved`, writes the review,
14
+ * and reindexes SQLite
15
+ * Approved-only is enforced at retrieval; nothing here auto-applies a hint.
16
+ */
17
+ import { join } from 'node:path';
18
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
19
+ import * as yaml from 'js-yaml';
20
+ import { HintStore } from './store.js';
21
+ const TRACES_DIR = ['.dql', 'traces'];
22
+ const HINTS_DIR = ['.dql', 'hints'];
23
+ const REVIEWS_DIR = ['.dql', 'reviews'];
24
+ export function tracesDir(projectRoot) {
25
+ return join(projectRoot, ...TRACES_DIR);
26
+ }
27
+ export function hintsDir(projectRoot) {
28
+ return join(projectRoot, ...HINTS_DIR);
29
+ }
30
+ export function reviewsDir(projectRoot) {
31
+ return join(projectRoot, ...REVIEWS_DIR);
32
+ }
33
+ export function defaultHintIndexPath(projectRoot) {
34
+ return join(projectRoot, '.dql', 'cache', 'agent-kg.sqlite');
35
+ }
36
+ function genId(prefix) {
37
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
38
+ }
39
+ function nowIso() {
40
+ return new Date().toISOString();
41
+ }
42
+ /**
43
+ * Record a Tier-2 correction as a Git trace + a derived **candidate** hint.
44
+ * The hint is NOT usable until reviewed/approved.
45
+ */
46
+ export function recordCorrectionTrace(projectRoot, input) {
47
+ const traceId = genId('trace');
48
+ const hintId = genId('hint');
49
+ const createdAt = nowIso();
50
+ const trace = {
51
+ id: traceId,
52
+ createdAt,
53
+ question: input.question,
54
+ scope: cleanScope(input.scope),
55
+ wrongAnswer: input.wrongAnswer,
56
+ correction: input.correction,
57
+ rationale: input.rationale,
58
+ author: input.author,
59
+ anchorObjectKey: input.anchorObjectKey,
60
+ derivedHintId: hintId,
61
+ };
62
+ const hint = {
63
+ id: hintId,
64
+ title: input.hintTitle?.trim() || deriveTitle(input.question, input.scope),
65
+ guidance: (input.hintGuidance ?? input.correction).trim(),
66
+ scope: cleanScope(input.scope),
67
+ status: 'candidate',
68
+ traceId,
69
+ correctedSql: input.correctedSql,
70
+ tags: input.tags,
71
+ author: input.author,
72
+ createdAt,
73
+ updatedAt: createdAt,
74
+ };
75
+ mkdirSync(tracesDir(projectRoot), { recursive: true });
76
+ mkdirSync(hintsDir(projectRoot), { recursive: true });
77
+ writeFileSync(join(tracesDir(projectRoot), `${traceId}.trace.json`), `${JSON.stringify(trace, null, 2)}\n`, 'utf-8');
78
+ writeHintFile(projectRoot, hint);
79
+ // Index the candidate so review/dev mode can find it (status gate keeps it out
80
+ // of normal retrieval).
81
+ reindexHints(projectRoot);
82
+ return { trace, hint };
83
+ }
84
+ // --- Hints ------------------------------------------------------------------
85
+ export function hintFilePath(projectRoot, hintId) {
86
+ return join(hintsDir(projectRoot), `${hintId}.hint.yaml`);
87
+ }
88
+ export function writeHintFile(projectRoot, hint) {
89
+ mkdirSync(hintsDir(projectRoot), { recursive: true });
90
+ const doc = {
91
+ id: hint.id,
92
+ title: hint.title,
93
+ guidance: hint.guidance,
94
+ status: hint.status,
95
+ scope: cleanScope(hint.scope),
96
+ traceId: hint.traceId,
97
+ correctedSql: hint.correctedSql,
98
+ tags: hint.tags,
99
+ author: hint.author,
100
+ reviewer: hint.reviewer,
101
+ supersedes: hint.supersedes,
102
+ createdAt: hint.createdAt,
103
+ updatedAt: hint.updatedAt,
104
+ };
105
+ writeFileSync(hintFilePath(projectRoot, hint.id), yaml.dump(stripUndefined(doc), { lineWidth: 100, noRefs: true }), 'utf-8');
106
+ }
107
+ export function readHintFile(path, sourcePath) {
108
+ try {
109
+ const raw = yaml.load(readFileSync(path, 'utf-8'));
110
+ if (!raw || typeof raw !== 'object')
111
+ return null;
112
+ const scope = (raw.scope ?? {});
113
+ const hint = {
114
+ id: String(raw.id ?? ''),
115
+ title: String(raw.title ?? ''),
116
+ guidance: String(raw.guidance ?? ''),
117
+ status: raw.status ?? 'candidate',
118
+ scope: {
119
+ metric: strOrUndef(scope.metric),
120
+ dbtModel: strOrUndef(scope.dbtModel),
121
+ domain: strOrUndef(scope.domain),
122
+ dialect: strOrUndef(scope.dialect),
123
+ term: strOrUndef(scope.term),
124
+ block: strOrUndef(scope.block),
125
+ },
126
+ traceId: strOrUndef(raw.traceId),
127
+ correctedSql: strOrUndef(raw.correctedSql),
128
+ tags: Array.isArray(raw.tags) ? raw.tags.map(String) : undefined,
129
+ author: strOrUndef(raw.author),
130
+ reviewer: strOrUndef(raw.reviewer),
131
+ supersedes: strOrUndef(raw.supersedes),
132
+ createdAt: String(raw.createdAt ?? nowIso()),
133
+ updatedAt: String(raw.updatedAt ?? nowIso()),
134
+ sourcePath,
135
+ };
136
+ if (!hint.id)
137
+ return null;
138
+ return hint;
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ export function listHintsFromGit(projectRoot) {
145
+ const dir = hintsDir(projectRoot);
146
+ if (!existsSync(dir))
147
+ return [];
148
+ const hints = [];
149
+ for (const file of readdirSync(dir)) {
150
+ if (!file.endsWith('.hint.yaml'))
151
+ continue;
152
+ const path = join(dir, file);
153
+ const hint = readHintFile(path, path);
154
+ if (hint)
155
+ hints.push(hint);
156
+ }
157
+ return hints.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
158
+ }
159
+ export function getHintFromGit(projectRoot, hintId) {
160
+ const path = hintFilePath(projectRoot, hintId);
161
+ return existsSync(path) ? readHintFile(path, path) : null;
162
+ }
163
+ /**
164
+ * Approve or reject a candidate hint. Writes the review record, flips the hint
165
+ * file's status, and reindexes SQLite. Approval is the ONLY path that makes a
166
+ * hint usable in normal retrieval.
167
+ */
168
+ export function reviewHint(projectRoot, input) {
169
+ const hint = getHintFromGit(projectRoot, input.hintId);
170
+ if (!hint)
171
+ return null;
172
+ const reviewId = genId('review');
173
+ const createdAt = nowIso();
174
+ const review = {
175
+ id: reviewId,
176
+ hintId: input.hintId,
177
+ decision: input.decision,
178
+ reviewer: input.reviewer,
179
+ note: input.note,
180
+ createdAt,
181
+ };
182
+ mkdirSync(reviewsDir(projectRoot), { recursive: true });
183
+ writeFileSync(join(reviewsDir(projectRoot), `${reviewId}.review.yaml`), yaml.dump(stripUndefined({ ...review }), { lineWidth: 100, noRefs: true }), 'utf-8');
184
+ const updated = {
185
+ ...hint,
186
+ status: input.decision,
187
+ reviewer: input.reviewer,
188
+ updatedAt: createdAt,
189
+ };
190
+ writeHintFile(projectRoot, updated);
191
+ reindexHints(projectRoot);
192
+ return { hint: updated, review };
193
+ }
194
+ // --- Index ------------------------------------------------------------------
195
+ /** Rebuild the SQLite hint index from the Git-authoritative files. */
196
+ export function reindexHints(projectRoot, indexPath = defaultHintIndexPath(projectRoot)) {
197
+ const hints = listHintsFromGit(projectRoot);
198
+ const store = new HintStore(indexPath);
199
+ try {
200
+ store.rebuild(hints);
201
+ return hints.length;
202
+ }
203
+ finally {
204
+ store.close();
205
+ }
206
+ }
207
+ // --- helpers ----------------------------------------------------------------
208
+ function deriveTitle(question, scope) {
209
+ const scopeBits = [scope.metric, scope.dbtModel, scope.domain].filter(Boolean).join('/');
210
+ const q = question.trim().replace(/\s+/g, ' ').slice(0, 60);
211
+ return scopeBits ? `Correction for ${scopeBits}: ${q}` : `Correction: ${q}`;
212
+ }
213
+ function cleanScope(scope) {
214
+ return {
215
+ metric: strOrUndef(scope.metric),
216
+ dbtModel: strOrUndef(scope.dbtModel),
217
+ domain: strOrUndef(scope.domain),
218
+ dialect: strOrUndef(scope.dialect),
219
+ term: strOrUndef(scope.term),
220
+ block: strOrUndef(scope.block),
221
+ };
222
+ }
223
+ function strOrUndef(value) {
224
+ if (value === undefined || value === null)
225
+ return undefined;
226
+ const str = String(value).trim();
227
+ return str.length > 0 ? str : undefined;
228
+ }
229
+ function stripUndefined(obj) {
230
+ const out = {};
231
+ for (const [key, value] of Object.entries(obj)) {
232
+ if (value === undefined)
233
+ continue;
234
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
235
+ const nested = stripUndefined(value);
236
+ if (Object.keys(nested).length > 0)
237
+ out[key] = nested;
238
+ continue;
239
+ }
240
+ out[key] = value;
241
+ }
242
+ return out;
243
+ }
244
+ //# sourceMappingURL=git-store.js.map