@adminiumjs/llm 0.1.0-rc.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 (126) hide show
  1. package/LICENSE +661 -0
  2. package/dist/apply/diff.d.ts +79 -0
  3. package/dist/apply/diff.d.ts.map +1 -0
  4. package/dist/apply/diff.js +459 -0
  5. package/dist/apply/diff.js.map +1 -0
  6. package/dist/apply/index.d.ts +6 -0
  7. package/dist/apply/index.d.ts.map +1 -0
  8. package/dist/apply/index.js +6 -0
  9. package/dist/apply/index.js.map +1 -0
  10. package/dist/apply/normalize.d.ts +55 -0
  11. package/dist/apply/normalize.d.ts.map +1 -0
  12. package/dist/apply/normalize.js +232 -0
  13. package/dist/apply/normalize.js.map +1 -0
  14. package/dist/apply/plan.d.ts +206 -0
  15. package/dist/apply/plan.d.ts.map +1 -0
  16. package/dist/apply/plan.js +422 -0
  17. package/dist/apply/plan.js.map +1 -0
  18. package/dist/apply/suggestion-id.d.ts +103 -0
  19. package/dist/apply/suggestion-id.d.ts.map +1 -0
  20. package/dist/apply/suggestion-id.js +121 -0
  21. package/dist/apply/suggestion-id.js.map +1 -0
  22. package/dist/crypto.d.ts +48 -0
  23. package/dist/crypto.d.ts.map +1 -0
  24. package/dist/crypto.js +35 -0
  25. package/dist/crypto.js.map +1 -0
  26. package/dist/index.d.ts +17 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +22 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/prompt/builder.d.ts +27 -0
  31. package/dist/prompt/builder.d.ts.map +1 -0
  32. package/dist/prompt/builder.js +263 -0
  33. package/dist/prompt/builder.js.map +1 -0
  34. package/dist/prompt/chunker.d.ts +89 -0
  35. package/dist/prompt/chunker.d.ts.map +1 -0
  36. package/dist/prompt/chunker.js +383 -0
  37. package/dist/prompt/chunker.js.map +1 -0
  38. package/dist/prompt/index.d.ts +14 -0
  39. package/dist/prompt/index.d.ts.map +1 -0
  40. package/dist/prompt/index.js +14 -0
  41. package/dist/prompt/index.js.map +1 -0
  42. package/dist/prompt/serializer.d.ts +138 -0
  43. package/dist/prompt/serializer.d.ts.map +1 -0
  44. package/dist/prompt/serializer.js +178 -0
  45. package/dist/prompt/serializer.js.map +1 -0
  46. package/dist/prompt/templates/v1.d.ts +41 -0
  47. package/dist/prompt/templates/v1.d.ts.map +1 -0
  48. package/dist/prompt/templates/v1.js +312 -0
  49. package/dist/prompt/templates/v1.js.map +1 -0
  50. package/dist/prompt/token-estimate.d.ts +16 -0
  51. package/dist/prompt/token-estimate.d.ts.map +1 -0
  52. package/dist/prompt/token-estimate.js +20 -0
  53. package/dist/prompt/token-estimate.js.map +1 -0
  54. package/dist/prompt/types.d.ts +134 -0
  55. package/dist/prompt/types.d.ts.map +1 -0
  56. package/dist/prompt/types.js +44 -0
  57. package/dist/prompt/types.js.map +1 -0
  58. package/dist/providers/anthropic.d.ts +3 -0
  59. package/dist/providers/anthropic.d.ts.map +1 -0
  60. package/dist/providers/anthropic.js +65 -0
  61. package/dist/providers/anthropic.js.map +1 -0
  62. package/dist/providers/factory.d.ts +3 -0
  63. package/dist/providers/factory.d.ts.map +1 -0
  64. package/dist/providers/factory.js +37 -0
  65. package/dist/providers/factory.js.map +1 -0
  66. package/dist/providers/http.d.ts +56 -0
  67. package/dist/providers/http.d.ts.map +1 -0
  68. package/dist/providers/http.js +148 -0
  69. package/dist/providers/http.js.map +1 -0
  70. package/dist/providers/index.d.ts +15 -0
  71. package/dist/providers/index.d.ts.map +1 -0
  72. package/dist/providers/index.js +15 -0
  73. package/dist/providers/index.js.map +1 -0
  74. package/dist/providers/model-catalog.d.ts +21 -0
  75. package/dist/providers/model-catalog.d.ts.map +1 -0
  76. package/dist/providers/model-catalog.js +121 -0
  77. package/dist/providers/model-catalog.js.map +1 -0
  78. package/dist/providers/ollama.d.ts +3 -0
  79. package/dist/providers/ollama.d.ts.map +1 -0
  80. package/dist/providers/ollama.js +55 -0
  81. package/dist/providers/ollama.js.map +1 -0
  82. package/dist/providers/openai-compatible.d.ts +3 -0
  83. package/dist/providers/openai-compatible.d.ts.map +1 -0
  84. package/dist/providers/openai-compatible.js +50 -0
  85. package/dist/providers/openai-compatible.js.map +1 -0
  86. package/dist/providers/openai.d.ts +18 -0
  87. package/dist/providers/openai.d.ts.map +1 -0
  88. package/dist/providers/openai.js +68 -0
  89. package/dist/providers/openai.js.map +1 -0
  90. package/dist/providers/types.d.ts +112 -0
  91. package/dist/providers/types.d.ts.map +1 -0
  92. package/dist/providers/types.js +55 -0
  93. package/dist/providers/types.js.map +1 -0
  94. package/dist/response/errors.d.ts +60 -0
  95. package/dist/response/errors.d.ts.map +1 -0
  96. package/dist/response/errors.js +101 -0
  97. package/dist/response/errors.js.map +1 -0
  98. package/dist/response/extract.d.ts +38 -0
  99. package/dist/response/extract.d.ts.map +1 -0
  100. package/dist/response/extract.js +77 -0
  101. package/dist/response/extract.js.map +1 -0
  102. package/dist/response/index.d.ts +8 -0
  103. package/dist/response/index.d.ts.map +1 -0
  104. package/dist/response/index.js +8 -0
  105. package/dist/response/index.js.map +1 -0
  106. package/dist/response/merge.d.ts +53 -0
  107. package/dist/response/merge.d.ts.map +1 -0
  108. package/dist/response/merge.js +348 -0
  109. package/dist/response/merge.js.map +1 -0
  110. package/dist/response/referential.d.ts +113 -0
  111. package/dist/response/referential.d.ts.map +1 -0
  112. package/dist/response/referential.js +556 -0
  113. package/dist/response/referential.js.map +1 -0
  114. package/dist/response/schema.d.ts +506 -0
  115. package/dist/response/schema.d.ts.map +1 -0
  116. package/dist/response/schema.js +191 -0
  117. package/dist/response/schema.js.map +1 -0
  118. package/dist/response/validate.d.ts +29 -0
  119. package/dist/response/validate.d.ts.map +1 -0
  120. package/dist/response/validate.js +167 -0
  121. package/dist/response/validate.js.map +1 -0
  122. package/dist/types.d.ts +95 -0
  123. package/dist/types.d.ts.map +1 -0
  124. package/dist/types.js +49 -0
  125. package/dist/types.js.map +1 -0
  126. package/package.json +30 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Validation error model shared by the whole response pipeline
3
+ * (06-llm-assist.md §7.2). Every stage — extract, parse, version, Zod, locale,
4
+ * referential, run-binding — reports through the ONE {@link LlmValidationError}
5
+ * shape so the paste UI and the CLI `--dry-run` can render a single precise
6
+ * per-path list (§7.2).
7
+ *
8
+ * Two axes matter downstream:
9
+ * - `code` — the machine code (§7.2/§7.3 tables), rendered verbatim.
10
+ * - `severity` — how the pipeline reacts:
11
+ * • `fatal` → rejects the whole run (stays `awaiting_response` / `failed`).
12
+ * • `item` → drops only the offending suggestion; the rest applies
13
+ * (acceptance criterion 4 — a hallucinated table must not
14
+ * discard 200 good labels).
15
+ * • `warning` → nothing dropped; surfaced as information (icon fallback,
16
+ * run-id mismatch).
17
+ *
18
+ * This module is pure data + string formatting: no Zod, no engine, no I/O — so
19
+ * it is safe for `@adminium/dashboard` (browser) to pull transitively.
20
+ */
21
+ /** Every code the pipeline can emit (§7.2 stages + §7.3 referential list). */
22
+ export const LLM_VALIDATION_CODES = [
23
+ // Stage 1–4 — fatal (reject the run)
24
+ 'LLM_JSON_PARSE',
25
+ 'LLM_TRUNCATED',
26
+ 'LLM_VERSION_MISMATCH',
27
+ 'LLM_MODEL_DECLINED',
28
+ 'LLM_SCHEMA_INVALID',
29
+ // Stage 5 — per-item
30
+ 'LLM_LOCALE_KEYS',
31
+ // Stage 6 — referential, per-item (§7.3)
32
+ 'LLM_UNKNOWN_TABLE',
33
+ 'LLM_UNKNOWN_COLUMN',
34
+ 'LLM_BAD_DISPLAY_COLUMN',
35
+ 'LLM_NOT_AN_ENUM',
36
+ 'LLM_ENUM_VALUES',
37
+ 'LLM_UNKNOWN_RELATION',
38
+ 'LLM_RELATION_INVALID',
39
+ 'LLM_UNKNOWN_TEMPLATE',
40
+ 'LLM_UNKNOWN_WIDGET',
41
+ 'LLM_WIDGET_BINDING',
42
+ 'LLM_GROUP_INVALID',
43
+ // Warnings — nothing dropped
44
+ 'LLM_UNKNOWN_ICON',
45
+ 'LLM_RUN_MISMATCH',
46
+ ];
47
+ const FATAL_CODES = new Set([
48
+ 'LLM_JSON_PARSE',
49
+ 'LLM_TRUNCATED',
50
+ 'LLM_VERSION_MISMATCH',
51
+ 'LLM_MODEL_DECLINED',
52
+ 'LLM_SCHEMA_INVALID',
53
+ ]);
54
+ const WARNING_CODES = new Set(['LLM_UNKNOWN_ICON', 'LLM_RUN_MISMATCH']);
55
+ /** The fixed severity of a code (§7.2 "Failure class" column). */
56
+ export function severityOf(code) {
57
+ if (FATAL_CODES.has(code))
58
+ return 'fatal';
59
+ if (WARNING_CODES.has(code))
60
+ return 'warning';
61
+ return 'item';
62
+ }
63
+ export function isFatal(error) {
64
+ return error.severity === 'fatal';
65
+ }
66
+ /**
67
+ * Render a Zod (or hand-built) path array as the pipeline's JSON-path string.
68
+ * Numeric segments become `[n]`; string segments join with `.`; the first
69
+ * segment carries no leading dot (`tables[3].table`, not `.tables[3].table`).
70
+ */
71
+ export function formatJsonPath(segments) {
72
+ let out = '';
73
+ for (const seg of segments) {
74
+ if (typeof seg === 'number') {
75
+ out += `[${seg}]`;
76
+ continue;
77
+ }
78
+ const key = typeof seg === 'symbol' ? (seg.description ?? seg.toString()) : seg;
79
+ out += out === '' ? key : `.${key}`;
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * Build a validation error, stamping the code's fixed severity. `hint` /
85
+ * `suggestionId` are set only when provided (respects
86
+ * `exactOptionalPropertyTypes`).
87
+ */
88
+ export function makeError(code, path, message, extra) {
89
+ const error = {
90
+ code,
91
+ severity: severityOf(code),
92
+ path,
93
+ message,
94
+ };
95
+ if (extra?.hint !== undefined)
96
+ error.hint = extra.hint;
97
+ if (extra?.suggestionId !== undefined)
98
+ error.suggestionId = extra.suggestionId;
99
+ return error;
100
+ }
101
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/response/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,qCAAqC;IACrC,gBAAgB;IAChB,eAAe;IACf,sBAAsB;IACtB,oBAAoB;IACpB,oBAAoB;IACpB,qBAAqB;IACrB,iBAAiB;IACjB,yCAAyC;IACzC,mBAAmB;IACnB,oBAAoB;IACpB,wBAAwB;IACxB,iBAAiB;IACjB,iBAAiB;IACjB,sBAAsB;IACtB,sBAAsB;IACtB,sBAAsB;IACtB,oBAAoB;IACpB,oBAAoB;IACpB,mBAAmB;IACnB,6BAA6B;IAC7B,kBAAkB;IAClB,kBAAkB;CACV,CAAC;AAwBX,MAAM,WAAW,GAAG,IAAI,GAAG,CAAoB;IAC7C,gBAAgB;IAChB,eAAe;IACf,sBAAsB;IACtB,oBAAoB;IACpB,oBAAoB;CACrB,CAAC,CAAC;AAEH,MAAM,aAAa,GAAG,IAAI,GAAG,CAAoB,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAE3F,kEAAkE;AAClE,MAAM,UAAU,UAAU,CAAC,IAAuB;IAChD,IAAI,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC;IAC1C,IAAI,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAyB;IAC/C,OAAO,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,QAAiD;IAC9E,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;YAC5B,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC;YAClB,SAAS;QACX,CAAC;QACD,MAAM,GAAG,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAChF,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CACvB,IAAuB,EACvB,IAAY,EACZ,OAAe,EACf,KAAgD;IAEhD,MAAM,KAAK,GAAuB;QAChC,IAAI;QACJ,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC;QAC1B,IAAI;QACJ,OAAO;KACR,CAAC;IACF,IAAI,KAAK,EAAE,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACvD,IAAI,KAAK,EAAE,YAAY,KAAK,SAAS;QAAE,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;IAC/E,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Stage 1 — tolerant JSON extraction (06-llm-assist.md §7.2).
3
+ *
4
+ * Models wrap the single JSON object the prompt demands (§5 output rule 1) in
5
+ * things it did not ask for: a ```` ```json ```` fence, a "Here's the JSON:"
6
+ * preamble, a "Hope this helps!" epilogue. This recovers the object without a
7
+ * JSON parser: find the first `{`, then walk to the brace that closes it while
8
+ * ignoring braces inside strings. Everything before the first `{` and after its
9
+ * matching `}` — fences, prose — is discarded.
10
+ *
11
+ * The walk doubles as a truncation detector: if the opening brace never closes
12
+ * (or a string never terminates), the response was cut off mid-object, which is
13
+ * a distinct `LLM_TRUNCATED` failure (the direct-path runner retries it with a
14
+ * raised token ceiling before counting a repair attempt — §7.5).
15
+ *
16
+ * Pure string scanning: no `JSON.parse` here (that is stage 2), so a
17
+ * brace-balanced but otherwise malformed object (trailing comma, single quotes)
18
+ * is returned for stage 2 to reject as `LLM_JSON_PARSE`.
19
+ */
20
+ import { type LlmValidationError } from './errors.js';
21
+ export interface ExtractSuccess {
22
+ ok: true;
23
+ /** The recovered `{ … }` substring; hand this to `JSON.parse` (stage 2). */
24
+ json: string;
25
+ }
26
+ export interface ExtractFailure {
27
+ ok: false;
28
+ error: LlmValidationError;
29
+ }
30
+ export type ExtractResult = ExtractSuccess | ExtractFailure;
31
+ /**
32
+ * Recover the single top-level JSON object from a raw model reply. Strips a
33
+ * wrapping code fence and any leading/trailing prose; reports `LLM_JSON_PARSE`
34
+ * when there is no object at all and `LLM_TRUNCATED` when one starts but never
35
+ * closes.
36
+ */
37
+ export declare function extractJsonObject(raw: string): ExtractResult;
38
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../src/response/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,KAAK,kBAAkB,EAAa,MAAM,aAAa,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,IAAI,CAAC;IACT,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAED,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,cAAc,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAsD5D"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Stage 1 — tolerant JSON extraction (06-llm-assist.md §7.2).
3
+ *
4
+ * Models wrap the single JSON object the prompt demands (§5 output rule 1) in
5
+ * things it did not ask for: a ```` ```json ```` fence, a "Here's the JSON:"
6
+ * preamble, a "Hope this helps!" epilogue. This recovers the object without a
7
+ * JSON parser: find the first `{`, then walk to the brace that closes it while
8
+ * ignoring braces inside strings. Everything before the first `{` and after its
9
+ * matching `}` — fences, prose — is discarded.
10
+ *
11
+ * The walk doubles as a truncation detector: if the opening brace never closes
12
+ * (or a string never terminates), the response was cut off mid-object, which is
13
+ * a distinct `LLM_TRUNCATED` failure (the direct-path runner retries it with a
14
+ * raised token ceiling before counting a repair attempt — §7.5).
15
+ *
16
+ * Pure string scanning: no `JSON.parse` here (that is stage 2), so a
17
+ * brace-balanced but otherwise malformed object (trailing comma, single quotes)
18
+ * is returned for stage 2 to reject as `LLM_JSON_PARSE`.
19
+ */
20
+ import { makeError } from './errors.js';
21
+ /**
22
+ * Recover the single top-level JSON object from a raw model reply. Strips a
23
+ * wrapping code fence and any leading/trailing prose; reports `LLM_JSON_PARSE`
24
+ * when there is no object at all and `LLM_TRUNCATED` when one starts but never
25
+ * closes.
26
+ */
27
+ export function extractJsonObject(raw) {
28
+ const trimmed = raw.trim();
29
+ if (trimmed.length === 0) {
30
+ return fail('LLM_JSON_PARSE', 'The response was empty; expected a single JSON object.');
31
+ }
32
+ const start = trimmed.indexOf('{');
33
+ if (start === -1) {
34
+ return fail('LLM_JSON_PARSE', 'No JSON object was found in the response (no opening "{"). Expected a single JSON object.');
35
+ }
36
+ let depth = 0;
37
+ let inString = false;
38
+ let escaped = false;
39
+ let end = -1;
40
+ for (let i = start; i < trimmed.length; i += 1) {
41
+ const ch = trimmed[i];
42
+ if (inString) {
43
+ if (escaped) {
44
+ escaped = false;
45
+ }
46
+ else if (ch === '\\') {
47
+ escaped = true;
48
+ }
49
+ else if (ch === '"') {
50
+ inString = false;
51
+ }
52
+ continue;
53
+ }
54
+ if (ch === '"') {
55
+ inString = true;
56
+ continue;
57
+ }
58
+ if (ch === '{') {
59
+ depth += 1;
60
+ }
61
+ else if (ch === '}') {
62
+ depth -= 1;
63
+ if (depth === 0) {
64
+ end = i;
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ if (end === -1) {
70
+ return fail('LLM_TRUNCATED', 'The JSON object is unbalanced — an opening brace is never closed, so the response was likely truncated. Regenerate with a higher output-token limit.');
71
+ }
72
+ return { ok: true, json: trimmed.slice(start, end + 1) };
73
+ }
74
+ function fail(code, message) {
75
+ return { ok: false, error: makeError(code, '', message) };
76
+ }
77
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/response/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAA2B,SAAS,EAAE,MAAM,aAAa,CAAC;AAejE;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,gBAAgB,EAAE,wDAAwD,CAAC,CAAC;IAC1F,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;QACjB,OAAO,IAAI,CACT,gBAAgB,EAChB,2FAA2F,CAC5F,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC;IAEb,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,GAAG,KAAK,CAAC;YAClB,CAAC;iBAAM,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBACvB,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;iBAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBACtB,QAAQ,GAAG,KAAK,CAAC;YACnB,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACtB,KAAK,IAAI,CAAC,CAAC;YACX,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;gBAChB,GAAG,GAAG,CAAC,CAAC;gBACR,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;QACf,OAAO,IAAI,CACT,eAAe,EACf,sJAAsJ,CACvJ,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED,SAAS,IAAI,CAAC,IAAwC,EAAE,OAAe;IACrE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,8 @@
1
+ /** `@adminium/llm` response contract + validation barrel — 06-llm-assist.md §6, §7. */
2
+ export * from './schema.js';
3
+ export * from './errors.js';
4
+ export * from './extract.js';
5
+ export * from './referential.js';
6
+ export * from './validate.js';
7
+ export * from './merge.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/response/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,8 @@
1
+ /** `@adminium/llm` response contract + validation barrel — 06-llm-assist.md §6, §7. */
2
+ export * from './schema.js';
3
+ export * from './errors.js';
4
+ export * from './extract.js';
5
+ export * from './referential.js';
6
+ export * from './validate.js';
7
+ export * from './merge.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/response/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { LlmResponseV1 } from './schema.js';
2
+ import type { DashboardSuggestion, NavGroupSuggestion } from './schema.js';
3
+ /**
4
+ * Consolidate nav-group proposals from every chunk into one coherent set where
5
+ * **each table appears in exactly one group** (§4.5). Same-id groups union their
6
+ * tables; a table proposed under several groups is assigned to the group with
7
+ * the highest confidence (ties by id) and removed from the rest; empty groups
8
+ * drop out. If more than 7 groups survive, the lowest-priority groups' tables
9
+ * fold into the last kept group so the cap holds without orphaning any table.
10
+ */
11
+ export declare function consolidateNavGroups(groups: readonly NavGroupSuggestion[]): NavGroupSuggestion[];
12
+ /**
13
+ * Consolidate dashboard proposals: same-id dashboards union tables and widgets
14
+ * (widgets dedupe by binding, keeping the highest confidence), widgets are
15
+ * re-ranked by confidence and capped at 8, and the dashboards themselves are
16
+ * ranked by total widget confidence and capped at 6. Fully deterministic.
17
+ */
18
+ export declare function consolidateDashboards(dashboards: readonly DashboardSuggestion[]): DashboardSuggestion[];
19
+ /**
20
+ * Reduce per-chunk `LlmResponseV1` results into one validated response. Purely
21
+ * deterministic and independent of the order of `responses`. Responses that the
22
+ * model declined (an `error` key set) contribute nothing; if every response
23
+ * declined, the first declared error propagates.
24
+ */
25
+ export declare function mergeChunkResponses(responses: readonly LlmResponseV1[]): LlmResponseV1;
26
+ export interface MergePromptOptions {
27
+ /** Full qualified table-name list for the whole schema, injected into the prompt (§4.5). */
28
+ allTableNames: readonly string[];
29
+ }
30
+ export interface MergePromptArtifact {
31
+ system: string;
32
+ user: string;
33
+ /** BYO flattening (`=== SYSTEM ===` / `=== USER ===`), same as map-phase prompts. */
34
+ byo: string;
35
+ tokenEstimate: number;
36
+ /** Chunk count injected as `{{TOTAL}}`. */
37
+ total: number;
38
+ }
39
+ /**
40
+ * Build the `PROMPT_MERGE_V1` reduce prompt (§4.5): it embeds only each chunk's
41
+ * partial `navGroups`/`dashboards` and the full table-name list, reusing the v1
42
+ * system section verbatim. Used only for the optional LLM consolidation of the
43
+ * global sections when a run was chunked.
44
+ */
45
+ export declare function buildMergePrompt(responses: readonly LlmResponseV1[], options: MergePromptOptions): MergePromptArtifact;
46
+ /**
47
+ * Fold an LLM merge-prompt response's consolidated global sections onto the
48
+ * deterministic per-table merge, re-running the deterministic global
49
+ * consolidation defensively (so the output still guarantees one group per table
50
+ * and honors the caps even if the model drifted).
51
+ */
52
+ export declare function applyGlobalMerge(merged: LlmResponseV1, mergeResponse: LlmResponseV1): LlmResponseV1;
53
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/response/merge.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAE,aAAa,EAAkB,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EAEV,mBAAmB,EAGnB,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AA+HrB;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,GAAG,kBAAkB,EAAE,CAwEhG;AA0BD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE,GAAG,mBAAmB,EAAE,CAwDvG;AAID;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,CAqBtF;AAID,MAAM,WAAW,kBAAkB;IACjC,4FAA4F;IAC5F,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAOD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,SAAS,aAAa,EAAE,EACnC,OAAO,EAAE,kBAAkB,GAC1B,mBAAmB,CAgBrB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,GAAG,aAAa,CAMnG"}
@@ -0,0 +1,348 @@
1
+ /**
2
+ * `merge.ts` — deterministic chunk reduce + the `PROMPT_MERGE_V1` orchestration
3
+ * (06-llm-assist.md §4.5, acceptance criterion 7).
4
+ *
5
+ * The map phase (see `prompt/chunker.ts`) produces one `LlmResponseV1` per chunk.
6
+ * This module reduces them back to one:
7
+ *
8
+ * 1. **Deterministic code reduce** ({@link mergeChunkResponses}). Per-table
9
+ * sections (`tables`, `enums`, per-column `pii`, `microcopy`, `relations`) are
10
+ * disjoint by table across chunks and merged by key. Duplicate inferred
11
+ * relations (both endpoints stubbed in one chunk, real in another) dedupe by
12
+ * endpoints keeping the highest confidence. Global sections (`navGroups`,
13
+ * `dashboards`) are consolidated so **every table lands in exactly one nav
14
+ * group** and no group/dashboard cap is exceeded. Every tie-break is
15
+ * content-derived, so the result is invariant under the order of the input
16
+ * responses (acceptance criterion 7: "order-independent").
17
+ *
18
+ * 2. **LLM reduce prompt** ({@link buildMergePrompt}). For a richer cross-chunk
19
+ * grouping, `PROMPT_MERGE_V1` (§4.5, shipped verbatim in `prompt/templates`)
20
+ * receives only the per-chunk `navGroups`/`dashboards` plus the full
21
+ * table-name list and returns one consolidated global result;
22
+ * {@link applyGlobalMerge} folds that back onto the deterministic per-table
23
+ * merge (re-running the deterministic global consolidation defensively).
24
+ *
25
+ * Determinism: no `Date.now`/`Math.random`; canonical (sorted-key) JSON drives
26
+ * every tie-break. Browser-safe: only pure-TS/Zod imports.
27
+ */
28
+ import { flattenByo } from '../prompt/builder.js';
29
+ import { toPromptJson } from '../prompt/serializer.js';
30
+ import { PROMPT_MERGE_V1 } from '../prompt/templates/v1.js';
31
+ import { estimateTokens } from '../prompt/token-estimate.js';
32
+ import { LlmResponseV1, SCHEMA_VERSION } from './schema.js';
33
+ const KEY_SEP = '\x00';
34
+ // ─── Canonical serialization for content-derived tie-breaks ──────────────────
35
+ function sortKeys(value) {
36
+ if (Array.isArray(value))
37
+ return value.map(sortKeys);
38
+ if (value !== null && typeof value === 'object') {
39
+ const record = value;
40
+ const out = {};
41
+ for (const key of Object.keys(record).sort())
42
+ out[key] = sortKeys(record[key]);
43
+ return out;
44
+ }
45
+ return value;
46
+ }
47
+ /** Order-independent canonical JSON (keys sorted recursively) for tie-breaking. */
48
+ function canonicalJson(value) {
49
+ return JSON.stringify(sortKeys(value));
50
+ }
51
+ /**
52
+ * True when `candidate` should win over `incumbent`: higher confidence, else the
53
+ * lexicographically smaller canonical form. A total order → the same winner
54
+ * regardless of which response was seen first.
55
+ */
56
+ function outranks(candidateConfidence, incumbentConfidence, candidate, incumbent) {
57
+ if (candidateConfidence !== incumbentConfidence)
58
+ return candidateConfidence > incumbentConfidence;
59
+ return canonicalJson(candidate) < canonicalJson(incumbent);
60
+ }
61
+ // ─── Per-table sections (disjoint by key) ────────────────────────────────────
62
+ function mergeTables(responses) {
63
+ const byTable = new Map();
64
+ for (const response of responses) {
65
+ for (const table of response.tables) {
66
+ const incumbent = byTable.get(table.table);
67
+ if (incumbent === undefined || outranks(table.confidence, incumbent.confidence, table, incumbent)) {
68
+ byTable.set(table.table, table);
69
+ }
70
+ }
71
+ }
72
+ return [...byTable.values()].sort((a, b) => a.table.localeCompare(b.table));
73
+ }
74
+ function mergeEnums(responses) {
75
+ const byColumn = new Map();
76
+ for (const response of responses) {
77
+ for (const suggestion of response.enums) {
78
+ const key = `${suggestion.table}${KEY_SEP}${suggestion.column}`;
79
+ const incumbent = byColumn.get(key);
80
+ if (incumbent === undefined || outranks(suggestion.confidence, incumbent.confidence, suggestion, incumbent)) {
81
+ byColumn.set(key, suggestion);
82
+ }
83
+ }
84
+ }
85
+ return [...byColumn.entries()]
86
+ .sort((a, b) => a[0].localeCompare(b[0]))
87
+ .map(([, value]) => value);
88
+ }
89
+ function endpointKey(relation) {
90
+ return [
91
+ relation.fromTable,
92
+ relation.fromColumns.join(','),
93
+ relation.toTable,
94
+ relation.toColumns.join(','),
95
+ ].join(KEY_SEP);
96
+ }
97
+ function mergeConfirmed(responses) {
98
+ const byEndpoints = new Map();
99
+ for (const response of responses) {
100
+ for (const relation of response.relations.confirmed) {
101
+ const key = endpointKey(relation);
102
+ const incumbent = byEndpoints.get(key);
103
+ if (incumbent === undefined || outranks(relation.confidence, incumbent.confidence, relation, incumbent)) {
104
+ byEndpoints.set(key, relation);
105
+ }
106
+ }
107
+ }
108
+ return [...byEndpoints.entries()]
109
+ .sort((a, b) => a[0].localeCompare(b[0]))
110
+ .map(([, value]) => value);
111
+ }
112
+ function mergeInferred(responses) {
113
+ const byEndpoints = new Map();
114
+ for (const response of responses) {
115
+ for (const relation of response.relations.inferred) {
116
+ const key = endpointKey(relation);
117
+ const incumbent = byEndpoints.get(key);
118
+ if (incumbent === undefined || outranks(relation.confidence, incumbent.confidence, relation, incumbent)) {
119
+ byEndpoints.set(key, relation);
120
+ }
121
+ }
122
+ }
123
+ return [...byEndpoints.entries()]
124
+ .sort((a, b) => a[0].localeCompare(b[0]))
125
+ .map(([, value]) => value);
126
+ }
127
+ function mergeNotes(responses) {
128
+ const notes = new Set();
129
+ for (const response of responses) {
130
+ for (const note of response.notes ?? [])
131
+ notes.add(note);
132
+ }
133
+ return [...notes].sort((a, b) => a.localeCompare(b)).slice(0, 5);
134
+ }
135
+ /** Max nav groups allowed by the response schema (§6.1: `navGroups.max(7)`). */
136
+ const MAX_NAV_GROUPS = 7;
137
+ /**
138
+ * Consolidate nav-group proposals from every chunk into one coherent set where
139
+ * **each table appears in exactly one group** (§4.5). Same-id groups union their
140
+ * tables; a table proposed under several groups is assigned to the group with
141
+ * the highest confidence (ties by id) and removed from the rest; empty groups
142
+ * drop out. If more than 7 groups survive, the lowest-priority groups' tables
143
+ * fold into the last kept group so the cap holds without orphaning any table.
144
+ */
145
+ export function consolidateNavGroups(groups) {
146
+ const byId = new Map();
147
+ for (const group of groups) {
148
+ const incumbent = byId.get(group.id);
149
+ if (incumbent === undefined) {
150
+ byId.set(group.id, {
151
+ id: group.id,
152
+ label: group.label,
153
+ icon: group.icon,
154
+ confidence: group.confidence,
155
+ tables: new Set(group.tables),
156
+ });
157
+ continue;
158
+ }
159
+ for (const table of group.tables)
160
+ incumbent.tables.add(table);
161
+ const swapLabel = outranks(group.confidence, incumbent.confidence, { label: group.label, icon: group.icon }, { label: incumbent.label, icon: incumbent.icon });
162
+ if (swapLabel) {
163
+ incumbent.label = group.label;
164
+ incumbent.icon = group.icon;
165
+ }
166
+ incumbent.confidence = Math.max(incumbent.confidence, group.confidence);
167
+ }
168
+ const accs = [...byId.values()];
169
+ // One group per table: pick the best owning group deterministically.
170
+ const owner = new Map();
171
+ for (const acc of accs) {
172
+ for (const table of acc.tables) {
173
+ const current = owner.get(table);
174
+ if (current === undefined) {
175
+ owner.set(table, acc);
176
+ continue;
177
+ }
178
+ const better = acc.confidence > current.confidence ||
179
+ (acc.confidence === current.confidence && acc.id.localeCompare(current.id) < 0);
180
+ if (better)
181
+ owner.set(table, acc);
182
+ }
183
+ }
184
+ for (const acc of accs) {
185
+ acc.tables = new Set([...acc.tables].filter((table) => owner.get(table) === acc));
186
+ }
187
+ let ranked = accs
188
+ .filter((acc) => acc.tables.size > 0)
189
+ .sort((a, b) => b.confidence - a.confidence || a.id.localeCompare(b.id));
190
+ if (ranked.length > MAX_NAV_GROUPS) {
191
+ const kept = ranked.slice(0, MAX_NAV_GROUPS);
192
+ const sink = kept[kept.length - 1];
193
+ if (sink !== undefined) {
194
+ for (const acc of ranked.slice(MAX_NAV_GROUPS)) {
195
+ for (const table of acc.tables)
196
+ sink.tables.add(table);
197
+ }
198
+ }
199
+ ranked = kept;
200
+ }
201
+ return ranked.map((acc, index) => ({
202
+ id: acc.id,
203
+ label: acc.label,
204
+ icon: acc.icon,
205
+ order: index,
206
+ tables: [...acc.tables].sort((a, b) => a.localeCompare(b)),
207
+ confidence: acc.confidence,
208
+ }));
209
+ }
210
+ /** Max dashboards / widgets-per-dashboard allowed by the schema (§6.1). */
211
+ const MAX_DASHBOARDS = 6;
212
+ const MAX_WIDGETS = 8;
213
+ function widgetKey(widget) {
214
+ return [
215
+ widget.widget,
216
+ widget.table,
217
+ widget.metricColumn ?? '',
218
+ widget.dimensionColumn ?? '',
219
+ widget.timeColumn ?? '',
220
+ widget.agg ?? '',
221
+ ].join(KEY_SEP);
222
+ }
223
+ /**
224
+ * Consolidate dashboard proposals: same-id dashboards union tables and widgets
225
+ * (widgets dedupe by binding, keeping the highest confidence), widgets are
226
+ * re-ranked by confidence and capped at 8, and the dashboards themselves are
227
+ * ranked by total widget confidence and capped at 6. Fully deterministic.
228
+ */
229
+ export function consolidateDashboards(dashboards) {
230
+ const byId = new Map();
231
+ for (const dashboard of dashboards) {
232
+ const metaKey = canonicalJson({ domain: dashboard.domain, label: dashboard.label });
233
+ let acc = byId.get(dashboard.id);
234
+ if (acc === undefined) {
235
+ acc = {
236
+ id: dashboard.id,
237
+ domain: dashboard.domain,
238
+ label: dashboard.label,
239
+ metaKey,
240
+ tables: new Set(),
241
+ widgets: new Map(),
242
+ };
243
+ byId.set(dashboard.id, acc);
244
+ }
245
+ else if (metaKey < acc.metaKey) {
246
+ acc.domain = dashboard.domain;
247
+ acc.label = dashboard.label;
248
+ acc.metaKey = metaKey;
249
+ }
250
+ for (const table of dashboard.tables)
251
+ acc.tables.add(table);
252
+ for (const widget of dashboard.widgets) {
253
+ const key = widgetKey(widget);
254
+ const incumbent = acc.widgets.get(key);
255
+ if (incumbent === undefined || outranks(widget.confidence, incumbent.confidence, widget, incumbent)) {
256
+ acc.widgets.set(key, widget);
257
+ }
258
+ }
259
+ }
260
+ const built = [...byId.values()].map((acc) => {
261
+ const widgets = [...acc.widgets.values()]
262
+ .sort((a, b) => b.confidence - a.confidence ||
263
+ a.widget.localeCompare(b.widget) ||
264
+ a.table.localeCompare(b.table) ||
265
+ widgetKey(a).localeCompare(widgetKey(b)))
266
+ .slice(0, MAX_WIDGETS)
267
+ .map((widget, index) => ({ ...widget, rank: index + 1 }));
268
+ const score = widgets.reduce((sum, widget) => sum + widget.confidence, 0);
269
+ return { acc, widgets, score };
270
+ });
271
+ return built
272
+ .sort((a, b) => b.score - a.score || a.acc.id.localeCompare(b.acc.id))
273
+ .slice(0, MAX_DASHBOARDS)
274
+ .map(({ acc, widgets }, index) => ({
275
+ id: acc.id,
276
+ domain: acc.domain,
277
+ label: acc.label,
278
+ order: index,
279
+ tables: [...acc.tables].sort((a, b) => a.localeCompare(b)),
280
+ widgets,
281
+ }));
282
+ }
283
+ // ─── Deterministic reduce (public) ───────────────────────────────────────────
284
+ /**
285
+ * Reduce per-chunk `LlmResponseV1` results into one validated response. Purely
286
+ * deterministic and independent of the order of `responses`. Responses that the
287
+ * model declined (an `error` key set) contribute nothing; if every response
288
+ * declined, the first declared error propagates.
289
+ */
290
+ export function mergeChunkResponses(responses) {
291
+ const active = responses.filter((response) => response.error === undefined);
292
+ if (active.length === 0) {
293
+ const declined = responses.find((response) => response.error !== undefined);
294
+ if (declined?.error !== undefined) {
295
+ return LlmResponseV1.parse({ schema_version: SCHEMA_VERSION, error: declined.error });
296
+ }
297
+ return LlmResponseV1.parse({ schema_version: SCHEMA_VERSION });
298
+ }
299
+ const notes = mergeNotes(active);
300
+ const base = {
301
+ schema_version: SCHEMA_VERSION,
302
+ tables: mergeTables(active),
303
+ enums: mergeEnums(active),
304
+ relations: { confirmed: mergeConfirmed(active), inferred: mergeInferred(active) },
305
+ navGroups: consolidateNavGroups(active.flatMap((response) => response.navGroups)),
306
+ dashboards: consolidateDashboards(active.flatMap((response) => response.dashboards)),
307
+ };
308
+ return LlmResponseV1.parse(notes.length > 0 ? { ...base, notes } : base);
309
+ }
310
+ function fillToken(text, token, value) {
311
+ // Function replacer so a `$` inside injected JSON is never a replacement pattern.
312
+ return text.replace(token, () => value);
313
+ }
314
+ /**
315
+ * Build the `PROMPT_MERGE_V1` reduce prompt (§4.5): it embeds only each chunk's
316
+ * partial `navGroups`/`dashboards` and the full table-name list, reusing the v1
317
+ * system section verbatim. Used only for the optional LLM consolidation of the
318
+ * global sections when a run was chunked.
319
+ */
320
+ export function buildMergePrompt(responses, options) {
321
+ const total = responses.length;
322
+ const partials = responses.map((response) => ({
323
+ navGroups: response.navGroups,
324
+ dashboards: response.dashboards,
325
+ }));
326
+ const tableNames = [...new Set(options.allTableNames)].sort((a, b) => a.localeCompare(b));
327
+ const system = PROMPT_MERGE_V1.system;
328
+ let user = PROMPT_MERGE_V1.user;
329
+ user = fillToken(user, '{{TOTAL}}', String(total));
330
+ user = fillToken(user, '{{ALL_TABLE_NAMES_JSON}}', toPromptJson(tableNames));
331
+ user = fillToken(user, '{{PARTIAL_GROUPS_AND_DASHBOARDS_JSON}}', toPromptJson(partials));
332
+ const byo = flattenByo(system, user);
333
+ return { system, user, byo, tokenEstimate: estimateTokens(byo), total };
334
+ }
335
+ /**
336
+ * Fold an LLM merge-prompt response's consolidated global sections onto the
337
+ * deterministic per-table merge, re-running the deterministic global
338
+ * consolidation defensively (so the output still guarantees one group per table
339
+ * and honors the caps even if the model drifted).
340
+ */
341
+ export function applyGlobalMerge(merged, mergeResponse) {
342
+ return LlmResponseV1.parse({
343
+ ...merged,
344
+ navGroups: consolidateNavGroups(mergeResponse.navGroups),
345
+ dashboards: consolidateDashboards(mergeResponse.dashboards),
346
+ });
347
+ }
348
+ //# sourceMappingURL=merge.js.map