@untangle-code/context-artifacts 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +247 -2
  4. package/THIRD_PARTY_LICENSES.md +24 -0
  5. package/dist/adapters/asyncapi.d.ts +20 -0
  6. package/dist/adapters/asyncapi.d.ts.map +1 -0
  7. package/dist/adapters/asyncapi.js +95 -0
  8. package/dist/adapters/asyncapi.js.map +1 -0
  9. package/dist/adapters/docker.d.ts +43 -0
  10. package/dist/adapters/docker.d.ts.map +1 -0
  11. package/dist/adapters/docker.js +97 -0
  12. package/dist/adapters/docker.js.map +1 -0
  13. package/dist/adapters/kubernetes.d.ts +22 -0
  14. package/dist/adapters/kubernetes.d.ts.map +1 -0
  15. package/dist/adapters/kubernetes.js +56 -0
  16. package/dist/adapters/kubernetes.js.map +1 -0
  17. package/dist/adapters/markdown.d.ts +35 -0
  18. package/dist/adapters/markdown.d.ts.map +1 -0
  19. package/dist/adapters/markdown.js +36 -0
  20. package/dist/adapters/markdown.js.map +1 -0
  21. package/dist/adapters/openapi.d.ts +21 -0
  22. package/dist/adapters/openapi.d.ts.map +1 -0
  23. package/dist/adapters/openapi.js +101 -0
  24. package/dist/adapters/openapi.js.map +1 -0
  25. package/dist/adapters/protobuf.d.ts +34 -0
  26. package/dist/adapters/protobuf.d.ts.map +1 -0
  27. package/dist/adapters/protobuf.js +221 -0
  28. package/dist/adapters/protobuf.js.map +1 -0
  29. package/dist/adapters/shared.d.ts +65 -0
  30. package/dist/adapters/shared.d.ts.map +1 -0
  31. package/dist/adapters/shared.js +95 -0
  32. package/dist/adapters/shared.js.map +1 -0
  33. package/dist/adapters/sql.d.ts +104 -0
  34. package/dist/adapters/sql.d.ts.map +1 -0
  35. package/dist/adapters/sql.js +427 -0
  36. package/dist/adapters/sql.js.map +1 -0
  37. package/dist/adapters/terraform.d.ts +40 -0
  38. package/dist/adapters/terraform.d.ts.map +1 -0
  39. package/dist/adapters/terraform.js +186 -0
  40. package/dist/adapters/terraform.js.map +1 -0
  41. package/dist/adapters/text.d.ts +24 -0
  42. package/dist/adapters/text.d.ts.map +1 -0
  43. package/dist/adapters/text.js +31 -0
  44. package/dist/adapters/text.js.map +1 -0
  45. package/dist/config-validation.d.ts +18 -0
  46. package/dist/config-validation.d.ts.map +1 -0
  47. package/dist/config-validation.js +30 -0
  48. package/dist/config-validation.js.map +1 -0
  49. package/dist/coverage.d.ts +114 -0
  50. package/dist/coverage.d.ts.map +1 -0
  51. package/dist/coverage.js +0 -0
  52. package/dist/coverage.js.map +1 -0
  53. package/dist/ids.d.ts +12 -0
  54. package/dist/ids.d.ts.map +1 -0
  55. package/dist/ids.js +42 -0
  56. package/dist/ids.js.map +1 -0
  57. package/dist/index.d.ts +55 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +18 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/registry.d.ts +51 -0
  62. package/dist/registry.d.ts.map +1 -0
  63. package/dist/registry.js +86 -0
  64. package/dist/registry.js.map +1 -0
  65. package/dist/staleness.d.ts +44 -0
  66. package/dist/staleness.d.ts.map +1 -0
  67. package/dist/staleness.js +35 -0
  68. package/dist/staleness.js.map +1 -0
  69. package/dist/text/delimiter-balance.d.ts +95 -0
  70. package/dist/text/delimiter-balance.d.ts.map +1 -0
  71. package/dist/text/delimiter-balance.js +209 -0
  72. package/dist/text/delimiter-balance.js.map +1 -0
  73. package/dist/text/line-index.d.ts +61 -0
  74. package/dist/text/line-index.d.ts.map +1 -0
  75. package/dist/text/line-index.js +75 -0
  76. package/dist/text/line-index.js.map +1 -0
  77. package/dist/text/yaml-lite.d.ts +73 -0
  78. package/dist/text/yaml-lite.d.ts.map +1 -0
  79. package/dist/text/yaml-lite.js +486 -0
  80. package/dist/text/yaml-lite.js.map +1 -0
  81. package/package.json +61 -3
  82. package/src/adapters/__golden__/adr-0001-sections.json +68 -0
  83. package/src/adapters/__golden__/app-compose-services.json +24 -0
  84. package/src/adapters/__golden__/app-image-stages.json +24 -0
  85. package/src/adapters/__golden__/infra-main-sections.json +46 -0
  86. package/src/adapters/__golden__/orders-events-operations.json +24 -0
  87. package/src/adapters/__golden__/orders-proto-sections.json +57 -0
  88. package/src/adapters/__golden__/orders-schema-statements.json +68 -0
  89. package/src/adapters/__golden__/users-api-operations.json +46 -0
  90. package/src/adapters/__golden__/web-manifests-resources.json +24 -0
  91. package/src/adapters/asyncapi.ts +147 -0
  92. package/src/adapters/docker.ts +170 -0
  93. package/src/adapters/kubernetes.ts +103 -0
  94. package/src/adapters/markdown.ts +88 -0
  95. package/src/adapters/openapi.ts +156 -0
  96. package/src/adapters/protobuf.ts +286 -0
  97. package/src/adapters/shared.ts +151 -0
  98. package/src/adapters/sql.ts +555 -0
  99. package/src/adapters/terraform.ts +261 -0
  100. package/src/adapters/text.ts +72 -0
  101. package/src/config-validation.ts +41 -0
  102. package/src/coverage.ts +336 -0
  103. package/src/ids.ts +57 -0
  104. package/src/index.ts +105 -0
  105. package/src/registry.ts +155 -0
  106. package/src/staleness.ts +69 -0
  107. package/src/text/delimiter-balance.ts +295 -0
  108. package/src/text/line-index.ts +108 -0
  109. package/src/text/yaml-lite.ts +565 -0
@@ -0,0 +1,555 @@
1
+ /**
2
+ * SQL schema adapter (architecture.md §20: "Tabellen, Views, Indizes,
3
+ * Constraints als semantische Abschnitte").
4
+ *
5
+ * Splits the file into top-level statements (terminated by a `;` that is
6
+ * not inside a string literal, a quoted identifier, or a comment), then
7
+ * classifies each statement by its leading keywords. An unrecognized
8
+ * statement (e.g. `INSERT`, `GRANT`) still becomes a chunk — kind
9
+ * `"sql_statement"` — rather than being dropped; nothing in a schema file
10
+ * is silently skipped (rule 4 / BUILD_PLAN §12 E4).
11
+ *
12
+ * Statement boundaries are tracked as exact BYTE offsets, not just line
13
+ * numbers (WP-81 review finding): two statements can share one physical
14
+ * line (`CREATE TABLE a (id int); CREATE TABLE b (id int);`), and a
15
+ * line-only span can't tell them apart — either the second collapses into
16
+ * an empty/inverted range (a phantom chunk carrying no content) or its
17
+ * bytes get silently absorbed into the first statement's chunk, which
18
+ * still reports zero gap (byte coverage looks complete) even though the
19
+ * second statement's identity was lost. `splitStatements` below scans the
20
+ * document's own UTF-8 bytes once, so every statement — including a second
21
+ * one sharing its predecessor's line — gets its own precise, non-empty,
22
+ * non-overlapping byte range that `ChunkDraft.startByte`/`endByte`
23
+ * (`adapters/shared.ts`) carries straight through, instead of snapping to
24
+ * whole-line boundaries.
25
+ *
26
+ * The file's LAST statement is not required to end in `;` — a schema file
27
+ * missing its final terminator (a common paste/truncation mistake) still
28
+ * gets its own chunk for that trailing statement; nothing after the last
29
+ * real `;` is ever silently dropped.
30
+ *
31
+ * WP-118: Der Zerleger kannte lange nur die verdoppelte
32
+ * Anführungszeichen-Konvention (`''`) als Escape. Ein Literal mit `\'` und
33
+ * einem `;` darin — in Dumps alltäglich — zerriss deshalb EIN Statement in
34
+ * zwei syntaktisch kaputte Fragmente, und zwar bei NULL Ledger-Warnungen:
35
+ * die Byte-Abdeckung war lückenlos, die Fragmente weder leer noch
36
+ * überlappend. Zwei Änderungen beheben das:
37
+ *
38
+ * 1. Die Escape-Behandlung ist dialektbewusst ({@link SqlDialect}), mit
39
+ * einem benannten, begründeten Standardwert ({@link DEFAULT_SQL_DIALECT})
40
+ * statt einer Vermutung über die vorliegende Datei. Zusätzlich erkennt
41
+ * der Scanner `$tag$`-Literale, in denen ein `;` genauso alltäglich ist.
42
+ * 2. Wichtiger: `sqlPlausibilityCheck` prüft JEDES fertige Fragment auf
43
+ * offene Zeichenketten, offene Kommentare und unausgeglichene Klammern.
44
+ * Damit greift E4 auch dann, wenn nichts FEHLT, sondern etwas FALSCH
45
+ * GESCHNITTEN ist — unabhängig davon, ob der Dialekt richtig gewählt
46
+ * war. Eine falsche Dialektwahl ist danach sichtbar, nicht still.
47
+ */
48
+ import type {
49
+ ArtifactAdapter,
50
+ ArtifactChunk,
51
+ ArtifactDocument,
52
+ ArtifactInput,
53
+ ArtifactLink,
54
+ } from "@untangle-code/domain";
55
+
56
+ import type { DelimiterScanOptions, QuoteRule } from "../text/delimiter-balance.js";
57
+ import { describeDelimiterImbalances, findDelimiterImbalances } from "../text/delimiter-balance.js";
58
+ import {
59
+ buildLineIndex,
60
+ lineNumberForByte,
61
+ sliceBytes,
62
+ type LineIndex,
63
+ } from "../text/line-index.js";
64
+
65
+ import type { ChunkDraft, ChunkPlausibilityCheck, SkipLedgerReporter } from "./shared.js";
66
+ import { buildChunks, buildChunksReportingSkips, buildDocuments } from "./shared.js";
67
+
68
+ const SQL_EXTENSION = /\.sql$/i;
69
+
70
+ /**
71
+ * Escape-Konvention für Zeichenketten-Literale. Die SQL-Dialekte
72
+ * WIDERSPRECHEN sich hier, und zwar unauflösbar:
73
+ *
74
+ * - `"doubled-quotes-only"` — Standard-SQL: das EINZIGE Escape in einem
75
+ * Literal ist das verdoppelte Anführungszeichen (`'it''s'`). Ein `\` ist
76
+ * ein ganz normales Zeichen; `'it\'` ist eine ABGESCHLOSSENE Zeichenkette
77
+ * mit einem Backslash am Ende.
78
+ * - `"backslash-escapes"` — die verbreitete Erweiterung: zusätzlich zum
79
+ * verdoppelten Anführungszeichen maskiert `\'` ein Anführungszeichen,
80
+ * `` ` `` klammert Bezeichner und `#` leitet einen Zeilenkommentar ein.
81
+ *
82
+ * Dieselbe Datei bedeutet also je nach Dialekt etwas anderes, und der
83
+ * Dialekt steht NICHT in der Datei. Deshalb wird hier nicht geraten: die
84
+ * Wahl ist eine explizite Option mit einem benannten Standardwert, und
85
+ * jedes Ergebnis, das unter der gewählten Konvention unsinnig aussieht,
86
+ * erzeugt einen sichtbaren Befund (siehe `sqlPlausibilityCheck`).
87
+ */
88
+ export type SqlDialect = "backslash-escapes" | "doubled-quotes-only";
89
+
90
+ /**
91
+ * Standardwert: `"backslash-escapes"` — bewusst NICHT der Standard-SQL-Dialekt.
92
+ *
93
+ * Begründung (asymmetrische Fehlerkosten für einen ZERLEGER, nicht für
94
+ * einen Ausführer): dieser Adapter führt kein SQL aus, er schneidet nur
95
+ * Abschnitte. Der teure Fehler ist der Schnitt MITTEN durch ein Statement,
96
+ * denn er erzeugt zwei syntaktisch kaputte Chunks, die niemand mehr als
97
+ * ein Statement erkennt (WP-118). Der billige Fehler ist der zu späte
98
+ * Schnitt: zwei Statements landen in einem Chunk, der immer noch gültiges,
99
+ * vollständiges SQL enthält.
100
+ *
101
+ * - Nimmt man `\'` als Escape und die Datei war Standard-SQL, kann nur ein
102
+ * Literal betroffen sein, das unmittelbar vor seinem Schlusszeichen auf
103
+ * `\` endet (`'C:\'`) — selten, und das Ergebnis ist eine offene
104
+ * Zeichenkette, die die Plausibilitätsprüfung meldet.
105
+ * - Nimmt man `\'` NICHT als Escape und die Datei nutzt es, zerreißt jedes
106
+ * Literal mit `\'` und einem folgenden `;` ein Statement — genau der
107
+ * gemeldete Defekt, und in Dumps alltäglich.
108
+ *
109
+ * Wer Standard-SQL indexiert und die Backtick-/`#`-Erweiterungen
110
+ * ausschließen will, setzt `dialect: "doubled-quotes-only"`.
111
+ */
112
+ export const DEFAULT_SQL_DIALECT: SqlDialect = "backslash-escapes";
113
+
114
+ interface StatementSpan {
115
+ startByte: number;
116
+ endByte: number;
117
+ startLine: number;
118
+ endLine: number;
119
+ }
120
+
121
+ function isAsciiWhitespaceByte(byte: number): boolean {
122
+ return (
123
+ byte === 0x20 ||
124
+ byte === 0x09 ||
125
+ byte === 0x0a ||
126
+ byte === 0x0d ||
127
+ byte === 0x0b ||
128
+ byte === 0x0c
129
+ );
130
+ }
131
+
132
+ /**
133
+ * After a statement-terminating `;` (buffer offset `semicolonEnd`, already
134
+ * past the `;` itself), decides how far the just-CLOSED statement's span
135
+ * should extend: through the rest of this physical line — its trailing
136
+ * whitespace, an optional trailing `-- ...` line comment, and the newline
137
+ * itself — when nothing else remains before that newline. This matches how
138
+ * a statement "owns" the rest of its own terminator line in the common
139
+ * case, where a schema file has exactly one statement per line (the exact
140
+ * byte a hand-formatted schema's next statement starts on is then, as
141
+ * before, the first byte of the following line).
142
+ *
143
+ * When real content follows the `;` on the SAME line instead — a second
144
+ * statement sharing that line — this returns `semicolonEnd` UNCHANGED, so
145
+ * that content starts its own span right where it begins rather than being
146
+ * silently folded into the span that just closed (the WP-81 review's
147
+ * "two statements, one line" defect: `TABLE_RE`/`VIEW_RE`/etc. would only
148
+ * ever see the FIRST statement's text, and the second statement's own span
149
+ * would either come out empty or never exist at all).
150
+ */
151
+ function extendPastTrailingLineNoise(buffer: Buffer, semicolonEnd: number): number {
152
+ let j = semicolonEnd;
153
+ for (;;) {
154
+ while (j < buffer.length && buffer[j] !== 0x0a && isAsciiWhitespaceByte(buffer[j]!)) {
155
+ j += 1;
156
+ }
157
+ if (j < buffer.length && buffer[j] === 0x2d && buffer[j + 1] === 0x2d) {
158
+ // "--": a trailing line comment on the terminator's own line is also
159
+ // benign noise, not a new statement — skip to (not past) its newline.
160
+ while (j < buffer.length && buffer[j] !== 0x0a) {
161
+ j += 1;
162
+ }
163
+ continue; // a comment could be followed by more whitespace before the newline
164
+ }
165
+ break;
166
+ }
167
+ if (j >= buffer.length) return buffer.length;
168
+ if (buffer[j] === 0x0a) return j + 1;
169
+ return semicolonEnd; // real content follows on the same line: do not extend past the `;`
170
+ }
171
+
172
+ /** Byte-Konstanten des Scanners — alles Einzelbyte-ASCII, siehe `splitStatements`. */
173
+ const BYTE_NEWLINE = 0x0a;
174
+ const BYTE_HASH = 0x23;
175
+ const BYTE_DOLLAR = 0x24;
176
+ const BYTE_SINGLE_QUOTE = 0x27;
177
+ const BYTE_DOUBLE_QUOTE = 0x22;
178
+ const BYTE_BACKTICK = 0x60;
179
+ const BYTE_BACKSLASH = 0x5c;
180
+ const BYTE_STAR = 0x2a;
181
+ const BYTE_SLASH = 0x2f;
182
+ const BYTE_DASH = 0x2d;
183
+ const BYTE_SEMICOLON = 0x3b;
184
+ const BYTE_UNDERSCORE = 0x5f;
185
+
186
+ function isDigitByte(byte: number): boolean {
187
+ return byte >= 0x30 && byte <= 0x39;
188
+ }
189
+
190
+ function isLetterByte(byte: number): boolean {
191
+ return (byte >= 0x41 && byte <= 0x5a) || (byte >= 0x61 && byte <= 0x7a);
192
+ }
193
+
194
+ /** Zeichen, die in einem `$tag$`-Etikett vorkommen dürfen (ohne `$` selbst). */
195
+ function isTagByte(byte: number | undefined): boolean {
196
+ return (
197
+ byte !== undefined && (isLetterByte(byte) || isDigitByte(byte) || byte === BYTE_UNDERSCORE)
198
+ );
199
+ }
200
+
201
+ /**
202
+ * Länge eines `$tag$`-Öffners an Position `i` (0, wenn dort keiner steht).
203
+ *
204
+ * Ein Dollar-Literal (`$$ … $$`, `$body$ … $body$`) ist der übliche Weg,
205
+ * einen Funktionsrumpf einzubetten — und darin steht fast immer ein `;`.
206
+ * Ohne diese Erkennung zerreißt jeder Funktionsrumpf in Fragmente, also
207
+ * exakt derselbe Defekt wie bei den Backslash-Escapes, nur mit anderem
208
+ * Auslöser. Erkannt wird das dialektübergreifend: `$tag$` hat außerhalb
209
+ * einer Zeichenkette in keinem gängigen Dialekt eine andere Bedeutung, die
210
+ * einen Statement-Zerleger interessieren müsste.
211
+ *
212
+ * Ein vorangehendes Bezeichnerzeichen verhindert die Erkennung — sonst
213
+ * würde der Bezeichner `my$col$x` fälschlich ein Literal öffnen.
214
+ */
215
+ function dollarQuoteLength(buffer: Buffer, i: number): number {
216
+ if (buffer[i] !== BYTE_DOLLAR) return 0;
217
+ const previous = i > 0 ? buffer[i - 1] : undefined;
218
+ if (previous !== undefined && (isTagByte(previous) || previous === BYTE_DOLLAR)) return 0;
219
+ let j = i + 1;
220
+ const first = buffer[j];
221
+ if (first !== undefined && (isLetterByte(first) || first === BYTE_UNDERSCORE)) {
222
+ j += 1;
223
+ while (j < buffer.length && isTagByte(buffer[j])) j += 1;
224
+ }
225
+ if (buffer[j] !== BYTE_DOLLAR) return 0;
226
+ return j + 1 - i;
227
+ }
228
+
229
+ /** Ob an Position `i` genau das Etikett steht, das bei `tagStart` geöffnet wurde. */
230
+ function matchesDollarTag(buffer: Buffer, i: number, tagStart: number, tagLength: number): boolean {
231
+ if (i + tagLength > buffer.length) return false;
232
+ for (let k = 0; k < tagLength; k += 1) {
233
+ if (buffer[i + k] !== buffer[tagStart + k]) return false;
234
+ }
235
+ return true;
236
+ }
237
+
238
+ /**
239
+ * Splits `index`'s underlying document into top-level statement byte spans
240
+ * (terminated by `;` outside strings/comments — see the module doc for why
241
+ * this is byte-, not line-, precise). Scans the raw UTF-8 `Buffer` directly
242
+ * (never the decoded JS string): every byte this function compares against
243
+ * (`'`, `"`, `` ` ``, `$`, `#`, `;`, `-`, `/`, `*`, `\\`, ASCII whitespace) is
244
+ * single-byte ASCII, and any UTF-8 continuation/multibyte-lead byte is
245
+ * `>= 0x80` and never matches one of those comparisons — so scanning raw
246
+ * bytes instead of UTF-16 code units never misreads a multibyte character
247
+ * (e.g. inside a comment or string) as one of these ASCII control
248
+ * characters. Das gilt auch für das Überspringen des Zeichens nach einem
249
+ * `\\`: übersprungen wird ein einzelnes Byte, und die restlichen Bytes
250
+ * eines Mehrbyte-Zeichens sind `>= 0x80` und lösen selbst nichts aus.
251
+ *
252
+ * `dialect` entscheidet über die Escape-Konvention (siehe {@link SqlDialect}).
253
+ * Falsch geschnittene Ergebnisse bleiben trotzdem möglich — deshalb prüft
254
+ * `sqlPlausibilityCheck` jedes fertige Fragment nach (WP-118, E4).
255
+ */
256
+ function splitStatements(index: LineIndex, dialect: SqlDialect): StatementSpan[] {
257
+ const { buffer } = index;
258
+ const backslashEscapes = dialect === "backslash-escapes";
259
+ const backtickIdentifiers = backslashEscapes;
260
+ const hashLineComments = backslashEscapes;
261
+
262
+ const spans: StatementSpan[] = [];
263
+ let statementStartByte = 0;
264
+ let statementHasContent = false;
265
+ /** Offenes Anführungszeichen (`'`, `"` oder `` ` ``), sonst `undefined`. */
266
+ let quoteByte: number | undefined;
267
+ /** Länge des offenen `$tag$`-Etiketts (0 = kein Dollar-Literal offen). */
268
+ let dollarTagLength = 0;
269
+ let dollarTagStart = 0;
270
+ let inLineComment = false;
271
+ let inBlockComment = false;
272
+
273
+ const flush = (endByteExclusive: number): void => {
274
+ if (!statementHasContent) return;
275
+ spans.push({
276
+ startByte: statementStartByte,
277
+ endByte: endByteExclusive,
278
+ startLine: lineNumberForByte(index, statementStartByte),
279
+ endLine: lineNumberForByte(index, endByteExclusive - 1),
280
+ });
281
+ };
282
+
283
+ for (let i = 0; i < buffer.length; i += 1) {
284
+ const c = buffer[i]!;
285
+ const next = buffer[i + 1];
286
+
287
+ if (inBlockComment) {
288
+ if (c === BYTE_STAR && next === BYTE_SLASH) {
289
+ inBlockComment = false;
290
+ i += 1;
291
+ }
292
+ continue;
293
+ }
294
+ if (inLineComment) {
295
+ if (c === BYTE_NEWLINE) inLineComment = false;
296
+ continue;
297
+ }
298
+ if (dollarTagLength > 0) {
299
+ if (c === BYTE_DOLLAR && matchesDollarTag(buffer, i, dollarTagStart, dollarTagLength)) {
300
+ i += dollarTagLength - 1;
301
+ dollarTagLength = 0;
302
+ }
303
+ statementHasContent = true;
304
+ continue;
305
+ }
306
+ if (quoteByte !== undefined) {
307
+ // Reihenfolge: erst Backslash (verschluckt AUCH ein direkt folgendes
308
+ // Anführungszeichen), dann das verdoppelte Anführungszeichen, dann
309
+ // das schließende. Innerhalb von `` ` `` ist `\` kein Escape — dort
310
+ // ist die Verdopplung die einzige Konvention.
311
+ if (backslashEscapes && quoteByte !== BYTE_BACKTICK && c === BYTE_BACKSLASH) {
312
+ i += 1;
313
+ } else if (c === quoteByte && next === quoteByte) {
314
+ i += 1;
315
+ } else if (c === quoteByte) {
316
+ quoteByte = undefined;
317
+ }
318
+ statementHasContent = true;
319
+ continue;
320
+ }
321
+ if (c === BYTE_DASH && next === BYTE_DASH) {
322
+ inLineComment = true;
323
+ i += 1;
324
+ continue;
325
+ }
326
+ if (hashLineComments && c === BYTE_HASH) {
327
+ inLineComment = true;
328
+ continue;
329
+ }
330
+ if (c === BYTE_SLASH && next === BYTE_STAR) {
331
+ inBlockComment = true;
332
+ i += 1;
333
+ continue;
334
+ }
335
+ const dollarLength = dollarQuoteLength(buffer, i);
336
+ if (dollarLength > 0) {
337
+ dollarTagStart = i;
338
+ dollarTagLength = dollarLength;
339
+ i += dollarLength - 1;
340
+ statementHasContent = true;
341
+ continue;
342
+ }
343
+ if (
344
+ c === BYTE_SINGLE_QUOTE ||
345
+ c === BYTE_DOUBLE_QUOTE ||
346
+ (backtickIdentifiers && c === BYTE_BACKTICK)
347
+ ) {
348
+ quoteByte = c;
349
+ statementHasContent = true;
350
+ continue;
351
+ }
352
+ if (c === BYTE_SEMICOLON) {
353
+ const endByteExclusive = extendPastTrailingLineNoise(buffer, i + 1);
354
+ flush(endByteExclusive);
355
+ statementStartByte = endByteExclusive;
356
+ statementHasContent = false;
357
+ i = endByteExclusive - 1;
358
+ continue;
359
+ }
360
+ if (!isAsciiWhitespaceByte(c)) {
361
+ statementHasContent = true;
362
+ }
363
+ }
364
+ // A final statement with no terminating `;` (the file's last statement
365
+ // missing its semicolon) still becomes its own span — never silently
366
+ // dropped (rule 4 / BUILD_PLAN §12 E4). Trailing whitespace-only content
367
+ // after the true last `;` reaches here with `statementHasContent` still
368
+ // `false` and correctly produces no extra span.
369
+ flush(buffer.length);
370
+ return spans;
371
+ }
372
+
373
+ /**
374
+ * Trennzeichen-Konventionen für die Plausibilitätsprüfung, abgeleitet aus
375
+ * demselben Dialekt wie die Zerlegung. Geprüft werden nur runde Klammern —
376
+ * eckige Klammern sind in manchen Dialekten Bezeichner-Klammern und in
377
+ * anderen Array-Syntax; sie zu prüfen brächte Fehlalarme statt Befunde.
378
+ */
379
+ function sqlScanOptions(dialect: SqlDialect): DelimiterScanOptions {
380
+ const backslashEscapes = dialect === "backslash-escapes";
381
+ const quotes: QuoteRule[] = [
382
+ { open: "'", close: "'", backslashEscapes, doubledEscapes: true, spansLines: true },
383
+ { open: '"', close: '"', backslashEscapes, doubledEscapes: true, spansLines: true },
384
+ // Backtick-Bezeichner kennt der Zerleger nur im Backslash-Dialekt, der
385
+ // Prüfer dagegen IMMER — er darf misstrauischer sein als der Zerleger
386
+ // (siehe `text/delimiter-balance.ts`). Genau so wird die falsche
387
+ // Dialektwahl sichtbar: eine Datei mit `` `a;b` `` wird unter
388
+ // `"doubled-quotes-only"` falsch geschnitten, und beide Fragmente
389
+ // melden dann einen offenen Backtick, statt still zu bleiben. In
390
+ // Standard-SQL ist ein einzelner Backtick ohnehin ungültig, ein
391
+ // Fehlalarm also nicht möglich.
392
+ { open: "`", close: "`", backslashEscapes: false, doubledEscapes: true, spansLines: true },
393
+ ];
394
+ return {
395
+ quotes,
396
+ brackets: [{ open: "(", close: ")" }],
397
+ lineComments: backslashEscapes ? ["--", "#"] : ["--"],
398
+ blockComment: { open: "/*", close: "*/" },
399
+ dollarQuotedStrings: true,
400
+ };
401
+ }
402
+
403
+ /**
404
+ * Die eigentliche Antwort auf WP-118: ein Statement-Fragment mit offener
405
+ * Zeichenkette, offenem Kommentar oder unausgeglichenen Klammern ist ein
406
+ * Befund — auch dann, wenn die Byte-Abdeckung lückenlos ist, weil nichts
407
+ * FEHLT, sondern etwas FALSCH GESCHNITTEN wurde.
408
+ */
409
+ function sqlPlausibilityCheck(dialect: SqlDialect): ChunkPlausibilityCheck {
410
+ const scanOptions = sqlScanOptions(dialect);
411
+ return (chunk) => {
412
+ const imbalances = findDelimiterImbalances(chunk.content, scanOptions);
413
+ return imbalances.length === 0 ? undefined : describeDelimiterImbalances(imbalances);
414
+ };
415
+ }
416
+
417
+ const TABLE_RE =
418
+ /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`[]?([\w.]+)["`\]]?/i;
419
+ const VIEW_RE =
420
+ /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?["`[]?([\w.]+)["`\]]?/i;
421
+ const INDEX_RE =
422
+ /^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`[]?([\w.]+)["`\]]?\s+ON\s+["`[]?([\w.]+)["`\]]?/i;
423
+ const ALTER_CONSTRAINT_RE =
424
+ /^\s*ALTER\s+TABLE\s+["`[]?([\w.]+)["`\]]?\s+ADD\s+CONSTRAINT\s+["`[]?([\w.]+)["`\]]?/i;
425
+
426
+ /** Strips leading whitespace and leading `--`/`/* *\/` comments so classification regexes only ever see real SQL. */
427
+ function stripLeadingComments(text: string): string {
428
+ let remaining = text;
429
+ for (;;) {
430
+ const trimmed = remaining.replace(/^\s+/, "");
431
+ if (trimmed.startsWith("--")) {
432
+ const newlineIndex = trimmed.indexOf("\n");
433
+ remaining = newlineIndex === -1 ? "" : trimmed.slice(newlineIndex + 1);
434
+ continue;
435
+ }
436
+ if (trimmed.startsWith("/*")) {
437
+ const closeIndex = trimmed.indexOf("*/");
438
+ remaining = closeIndex === -1 ? "" : trimmed.slice(closeIndex + 2);
439
+ continue;
440
+ }
441
+ return trimmed;
442
+ }
443
+ }
444
+
445
+ function classify(rawStatementText: string): { kind: string; title: string } {
446
+ const statementText = stripLeadingComments(rawStatementText);
447
+ const table = TABLE_RE.exec(statementText);
448
+ if (table) return { kind: "sql_table", title: table[1]! };
449
+ const view = VIEW_RE.exec(statementText);
450
+ if (view) return { kind: "sql_view", title: view[1]! };
451
+ const index = INDEX_RE.exec(statementText);
452
+ if (index) return { kind: "sql_index", title: `${index[1]!} ON ${index[2]!}` };
453
+ const constraint = ALTER_CONSTRAINT_RE.exec(statementText);
454
+ if (constraint)
455
+ return { kind: "sql_constraint", title: `${constraint[2]!} ON ${constraint[1]!}` };
456
+ const firstWord = /^\s*(\w+)/.exec(statementText)?.[1] ?? "statement";
457
+ return { kind: "sql_statement", title: firstWord.toUpperCase() };
458
+ }
459
+
460
+ function sqlDrafts(index: LineIndex, dialect: SqlDialect): ChunkDraft[] {
461
+ return splitStatements(index, dialect).map((span) => {
462
+ const text = sliceBytes(index, span.startByte, span.endByte);
463
+ const { kind, title } = classify(text);
464
+ return {
465
+ kind,
466
+ startLine: span.startLine,
467
+ endLine: span.endLine,
468
+ title,
469
+ startByte: span.startByte,
470
+ endByte: span.endByte,
471
+ };
472
+ });
473
+ }
474
+
475
+ /** Table name a chunk defines, when its kind is `"sql_table"` — used by `links()`. */
476
+ function tableNameOf(chunk: ArtifactChunk): string | undefined {
477
+ return chunk.kind === "sql_table" ? chunk.title : undefined;
478
+ }
479
+
480
+ /** `snake_case`/`kebab-case` table name → PascalCase-ish repository class name guess. */
481
+ function guessRepositoryName(tableName: string): string {
482
+ const bareName = tableName.includes(".") ? tableName.split(".").at(-1)! : tableName;
483
+ const pascal = bareName
484
+ .split(/[_-]+/)
485
+ .filter((part) => part.length > 0)
486
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
487
+ .join("");
488
+ return `${pascal}Repository`;
489
+ }
490
+
491
+ export interface SqlAdapterOptions {
492
+ /** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. */
493
+ onSkipLedgerGap?: SkipLedgerReporter;
494
+ /**
495
+ * Escape-Konvention der Zeichenketten-Literale. Standard:
496
+ * {@link DEFAULT_SQL_DIALECT} (`"backslash-escapes"`) — die Begründung
497
+ * dieser Wahl steht dort. Der Dialekt wird NICHT aus dem Dateiinhalt
498
+ * erraten; er ist eine Entscheidung des Aufrufers.
499
+ */
500
+ dialect?: SqlDialect;
501
+ }
502
+
503
+ export function createSqlAdapter(options: SqlAdapterOptions = {}): ArtifactAdapter {
504
+ const dialect = options.dialect ?? DEFAULT_SQL_DIALECT;
505
+ const checkPlausibility = sqlPlausibilityCheck(dialect);
506
+ return {
507
+ supports(type: string, path: string): boolean {
508
+ return type === "sql" || (type === "auto" && SQL_EXTENSION.test(path));
509
+ },
510
+
511
+ async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
512
+ return buildDocuments(input, "sql");
513
+ },
514
+
515
+ chunk(document: ArtifactDocument): ArtifactChunk[] {
516
+ const index = buildLineIndex(document.content);
517
+ return buildChunksReportingSkips(
518
+ document,
519
+ index,
520
+ sqlDrafts(index, dialect),
521
+ options.onSkipLedgerGap,
522
+ checkPlausibility,
523
+ );
524
+ },
525
+
526
+ /**
527
+ * Heuristic-only (architecture.md §20): guesses that table `orders`
528
+ * has a corresponding `OrdersRepository` code symbol, purely from a
529
+ * naming convention. `targetId` is deliberately left unset — this
530
+ * adapter has no code-symbol index to resolve against, only the
531
+ * repository-adapter graph-linking stage (a different WP) can turn
532
+ * `targetName` into a real `targetId`. Confidence is fixed and
533
+ * conservative: this guess is right often enough to be worth surfacing,
534
+ * wrong often enough that a caller MUST treat it as a hint, never a fact.
535
+ */
536
+ links(document: ArtifactDocument): ArtifactLink[] {
537
+ const index = buildLineIndex(document.content);
538
+ const chunks = buildChunks(document, index, sqlDrafts(index, dialect));
539
+ const links: ArtifactLink[] = [];
540
+ for (const chunk of chunks) {
541
+ const tableName = tableNameOf(chunk);
542
+ if (tableName === undefined) continue;
543
+ links.push({
544
+ fromChunkId: chunk.chunkId,
545
+ targetKind: "symbol",
546
+ targetName: guessRepositoryName(tableName),
547
+ heuristic: true,
548
+ confidence: 0.4,
549
+ note: `naming-convention guess: SQL table "${tableName}" → repository symbol, not a resolved reference`,
550
+ });
551
+ }
552
+ return links;
553
+ },
554
+ };
555
+ }