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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,261 @@
1
+ /**
2
+ * Terraform/HCL adapter (architecture.md §20: "Ressourcen, Module,
3
+ * Variablen").
4
+ *
5
+ * Same brace-depth line scanning approach as the Protobuf adapter: a
6
+ * top-level `resource "type" "name" {`, `data "type" "name" {`, `module
7
+ * "name" {`, `variable "name" {`, `output "name" {`, `provider "name" {`,
8
+ * or `locals {` block runs to its matching `}`. Whitespace/comments
9
+ * between blocks are not chunked — `buildArtifactSkipLedger` surfaces
10
+ * those gaps with reason `"whitespace_or_comment"`, never silently.
11
+ *
12
+ * The brace counter skips `"..."` string literals, `#`/`//`/`/* *\/`
13
+ * comments and heredoc bodies (`<<EOT` … `EOT`, WP-118) (WP-81 review, same defect class as the Protobuf adapter's own
14
+ * fix: a `{`/`}` inside a multi-line `/* *\/` comment must never move the
15
+ * depth counter, or it desyncs permanently and "runs to EOF", silently
16
+ * swallowing every following top-level block). `scanLine` carries the
17
+ * open-block-comment state across lines through one continuous scan — the
18
+ * SAME scan that finds block boundaries, not a separate look-ahead with its
19
+ * own independent counter, so there is exactly one source of truth for "is
20
+ * this brace really code right now".
21
+ *
22
+ * KNOWN, UNFIXED gap in the same defect class (documented, not silent):
23
+ * `BLOCK_PATTERNS` match against a full physical LINE (`^\s*keyword...`), so
24
+ * two block openers sharing one physical line (`resource "a" "b" {} module
25
+ * "m" {}`) — valid but extremely unidiomatic HCL that `terraform fmt` would
26
+ * never produce — only detects the first; the second's bytes still land
27
+ * inside the first block's `content` (nothing is dropped from the document,
28
+ * only mis-titled), rather than getting its own chunk. Fixing this fully
29
+ * would need the same byte-precise, sub-line splitting the SQL adapter now
30
+ * uses for multiple statements per line; left as an open issue given how
31
+ * rare well-formed HCL makes this shape.
32
+ */
33
+ import type {
34
+ ArtifactAdapter,
35
+ ArtifactChunk,
36
+ ArtifactDocument,
37
+ ArtifactInput,
38
+ ArtifactLink,
39
+ } from "@untangle-code/domain";
40
+
41
+ import type { DelimiterScanOptions } from "../text/delimiter-balance.js";
42
+ import { describeDelimiterImbalances, findDelimiterImbalances } from "../text/delimiter-balance.js";
43
+ import { buildLineIndex, splitLines } from "../text/line-index.js";
44
+
45
+ import type { ChunkDraft, ChunkPlausibilityCheck, SkipLedgerReporter } from "./shared.js";
46
+ import { buildChunksReportingSkips, buildDocuments } from "./shared.js";
47
+
48
+ const TERRAFORM_EXTENSION = /\.(tf|tf\.json|hcl)$/i;
49
+
50
+ interface BlockPattern {
51
+ kind: string;
52
+ re: RegExp;
53
+ title: (match: RegExpExecArray) => string;
54
+ }
55
+
56
+ const BLOCK_PATTERNS: BlockPattern[] = [
57
+ {
58
+ kind: "terraform_resource",
59
+ re: /^\s*resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/,
60
+ title: (m) => `${m[1]!}.${m[2]!}`,
61
+ },
62
+ {
63
+ kind: "terraform_data",
64
+ re: /^\s*data\s+"([^"]+)"\s+"([^"]+)"\s*\{/,
65
+ title: (m) => `${m[1]!}.${m[2]!}`,
66
+ },
67
+ { kind: "terraform_module", re: /^\s*module\s+"([^"]+)"\s*\{/, title: (m) => m[1]! },
68
+ { kind: "terraform_variable", re: /^\s*variable\s+"([^"]+)"\s*\{/, title: (m) => m[1]! },
69
+ { kind: "terraform_output", re: /^\s*output\s+"([^"]+)"\s*\{/, title: (m) => m[1]! },
70
+ { kind: "terraform_provider", re: /^\s*provider\s+"([^"]+)"\s*\{/, title: (m) => m[1]! },
71
+ { kind: "terraform_locals", re: /^\s*locals\s*\{/, title: () => "locals" },
72
+ ];
73
+
74
+ /**
75
+ * Öffner eines Heredocs (`<<EOT` / `<<-EOT`); die Gruppe ist die Endmarke.
76
+ * Sticky, damit er an einer Position geprüft werden kann, ohne die Zeile zu
77
+ * kopieren.
78
+ */
79
+ const HEREDOC_OPEN = /<<-?([A-Za-z_][A-Za-z0-9_]*)/y;
80
+
81
+ /** Cross-line scanner state: whether the scan is currently inside an open `/* *\/` block comment or heredoc. */
82
+ interface ScanState {
83
+ inBlockComment: boolean;
84
+ /** Endmarke des offenen Heredocs, sonst `undefined`. */
85
+ heredocTag: string | undefined;
86
+ }
87
+
88
+ /**
89
+ * Net brace delta for one line, skipping `"..."` string literals and
90
+ * `#`/`//`/`/* *\/` comments. `state.inBlockComment` is mutated for a
91
+ * comment that is still open at the end of this line — the next call for
92
+ * the following line continues from exactly that state. String state is
93
+ * NOT carried across lines: an HCL double-quoted string cannot contain a
94
+ * literal newline (only `\n` escapes), so it is always fully open and
95
+ * closed within one physical line.
96
+ */
97
+ function scanLine(line: string, state: ScanState): number {
98
+ // WP-118: Ein Heredoc ist ROHTEXT. Ein `{` in einem eingebetteten
99
+ // Shell-Skript (`user_data = <<-EOT ... EOT`) hat den Klammerzähler
100
+ // bisher dauerhaft verschoben — der laufende Block „lief bis Dateiende"
101
+ // und verschluckte jeden folgenden Top-Level-Block, bei lückenloser
102
+ // Byte-Abdeckung und damit ohne einen einzigen Ledger-Eintrag. Genau
103
+ // dieselbe Blindstelle wie die des SQL-Zerlegers, nur mit anderem
104
+ // Auslöser.
105
+ if (state.heredocTag !== undefined) {
106
+ if (line.trim() === state.heredocTag) state.heredocTag = undefined;
107
+ return 0;
108
+ }
109
+ let delta = 0;
110
+ let inDouble = false;
111
+ for (let i = 0; i < line.length; i += 1) {
112
+ const c = line[i];
113
+ const next = line[i + 1];
114
+ if (state.inBlockComment) {
115
+ if (c === "*" && next === "/") {
116
+ state.inBlockComment = false;
117
+ i += 1;
118
+ }
119
+ continue;
120
+ }
121
+ if (inDouble) {
122
+ if (c === '"' && line[i - 1] !== "\\") inDouble = false;
123
+ continue;
124
+ }
125
+ if (c === "#" || (c === "/" && next === "/")) break; // line comment: rest of the line is never code
126
+ if (c === "/" && next === "*") {
127
+ state.inBlockComment = true;
128
+ i += 1;
129
+ continue;
130
+ }
131
+ if (c === '"') {
132
+ inDouble = true;
133
+ continue;
134
+ }
135
+ if (c === "<" && next === "<") {
136
+ HEREDOC_OPEN.lastIndex = i;
137
+ const heredoc = HEREDOC_OPEN.exec(line);
138
+ if (heredoc !== null) {
139
+ state.heredocTag = heredoc[1]!;
140
+ return delta; // alles hinter dem Öffner gehört zum Heredoc-Inhalt
141
+ }
142
+ }
143
+ if (c === "{") delta += 1;
144
+ else if (c === "}") delta -= 1;
145
+ }
146
+ return delta;
147
+ }
148
+
149
+ function terraformDrafts(lines: string[]): ChunkDraft[] {
150
+ const drafts: ChunkDraft[] = [];
151
+ const state: ScanState = { inBlockComment: false, heredocTag: undefined };
152
+ let depth = 0;
153
+ let pending: { kind: string; title: string; startLine: number } | undefined;
154
+
155
+ for (let i = 0; i < lines.length; i += 1) {
156
+ const line = lines[i] ?? "";
157
+ if (
158
+ depth === 0 &&
159
+ pending === undefined &&
160
+ !state.inBlockComment &&
161
+ state.heredocTag === undefined
162
+ ) {
163
+ for (const pattern of BLOCK_PATTERNS) {
164
+ const match = pattern.re.exec(line);
165
+ if (match) {
166
+ pending = { kind: pattern.kind, title: pattern.title(match), startLine: i + 1 };
167
+ break;
168
+ }
169
+ }
170
+ }
171
+ depth += scanLine(line, state);
172
+ if (pending !== undefined && depth === 0) {
173
+ drafts.push({
174
+ kind: pending.kind,
175
+ startLine: pending.startLine,
176
+ endLine: i + 1,
177
+ title: pending.title,
178
+ });
179
+ pending = undefined;
180
+ }
181
+ }
182
+ if (pending !== undefined) {
183
+ // Unterminated top-level block (malformed source, or a still-open
184
+ // block comment at EOF): degrade to "runs to EOF" rather than dropping
185
+ // it — never silently, see the module doc.
186
+ drafts.push({
187
+ kind: pending.kind,
188
+ startLine: pending.startLine,
189
+ endLine: Math.max(lines.length, pending.startLine),
190
+ title: pending.title,
191
+ });
192
+ }
193
+ return drafts;
194
+ }
195
+
196
+ /**
197
+ * Trennzeichen-Konventionen für die Plausibilitätsprüfung (WP-118).
198
+ * Heredocs (`<<EOF` / `<<-EOF`) sind Rohtext: eingebettete Skripte
199
+ * enthalten regelmäßig unpaarige Klammern und Anführungszeichen, die kein
200
+ * Befund sind. Der Prüfer überliest sie deshalb vollständig — und meldet
201
+ * dafür ein Heredoc, dessen Endmarke im Fragment fehlt.
202
+ */
203
+ const TERRAFORM_SCAN_OPTIONS: DelimiterScanOptions = {
204
+ quotes: [
205
+ { open: '"', close: '"', backslashEscapes: true, doubledEscapes: false, spansLines: false },
206
+ ],
207
+ brackets: [
208
+ { open: "{", close: "}" },
209
+ { open: "(", close: ")" },
210
+ { open: "[", close: "]" },
211
+ ],
212
+ lineComments: ["#", "//"],
213
+ blockComment: { open: "/*", close: "*/" },
214
+ heredocs: "hcl",
215
+ };
216
+
217
+ /**
218
+ * WP-118 (dieselbe Blindstelle wie in `sql.ts`): ein unabgeschlossener
219
+ * Top-Level-Block wurde bisher still zu „läuft bis Dateiende" degradiert —
220
+ * lückenlose Byte-Abdeckung, kein leerer Chunk, also kein Ledger-Eintrag,
221
+ * obwohl der Klammerzähler nachweislich aus dem Tritt war. Dasselbe gilt
222
+ * für ein Heredoc, dessen `{` den Zähler verschoben hat (der Zähler dieses
223
+ * Adapters kennt Heredocs nicht). Beides ist jetzt ein sichtbarer Befund.
224
+ */
225
+ const checkTerraformPlausibility: ChunkPlausibilityCheck = (chunk) => {
226
+ const imbalances = findDelimiterImbalances(chunk.content, TERRAFORM_SCAN_OPTIONS);
227
+ return imbalances.length === 0 ? undefined : describeDelimiterImbalances(imbalances);
228
+ };
229
+
230
+ export interface TerraformAdapterOptions {
231
+ /** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. */
232
+ onSkipLedgerGap?: SkipLedgerReporter;
233
+ }
234
+
235
+ export function createTerraformAdapter(options: TerraformAdapterOptions = {}): ArtifactAdapter {
236
+ return {
237
+ supports(type: string, path: string): boolean {
238
+ return type === "terraform" || (type === "auto" && TERRAFORM_EXTENSION.test(path));
239
+ },
240
+
241
+ async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
242
+ return buildDocuments(input, "terraform");
243
+ },
244
+
245
+ chunk(document: ArtifactDocument): ArtifactChunk[] {
246
+ const { lines } = splitLines(document.content);
247
+ const index = buildLineIndex(document.content);
248
+ return buildChunksReportingSkips(
249
+ document,
250
+ index,
251
+ terraformDrafts(lines),
252
+ options.onSkipLedgerGap,
253
+ checkTerraformPlausibility,
254
+ );
255
+ },
256
+
257
+ links(): ArtifactLink[] {
258
+ return [];
259
+ },
260
+ };
261
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Generic text fallback adapter (spec.md §19.2 "sicherer Text-Fallback";
3
+ * architecture.md §22.2 "Parserfehler degradieren auf Textanalyse").
4
+ *
5
+ * `supports()` matches everything the registry hasn't already routed to a
6
+ * more specific adapter — it is always registered LAST, as the registry's
7
+ * catch-all, so no file is ever left unindexable just because its format
8
+ * has no dedicated adapter (rule 4 / E4: nothing is silently skipped, it
9
+ * is degraded to fixed-size line windows instead).
10
+ *
11
+ * Windows are contiguous and non-overlapping, covering the ENTIRE file —
12
+ * `buildArtifactSkipLedger` reports zero gaps for a text-fallback document,
13
+ * by construction.
14
+ */
15
+ import type {
16
+ ArtifactAdapter,
17
+ ArtifactChunk,
18
+ ArtifactDocument,
19
+ ArtifactInput,
20
+ ArtifactLink,
21
+ } from "@untangle-code/domain";
22
+
23
+ import { buildLineIndex } from "../text/line-index.js";
24
+
25
+ import type { ChunkDraft, SkipLedgerReporter } from "./shared.js";
26
+ import { buildChunksReportingSkips, buildDocuments } from "./shared.js";
27
+
28
+ /** Lines per window — generous enough to keep a chunk's content meaningfully self-contained, small enough to stay embeddable. */
29
+ export const TEXT_WINDOW_LINES = 200;
30
+
31
+ function windowDrafts(lineCount: number): ChunkDraft[] {
32
+ const total = Math.max(lineCount, 1);
33
+ const drafts: ChunkDraft[] = [];
34
+ for (let start = 1; start <= total; start += TEXT_WINDOW_LINES) {
35
+ const end = Math.min(start + TEXT_WINDOW_LINES - 1, total);
36
+ drafts.push({ kind: "text_window", startLine: start, endLine: end });
37
+ }
38
+ return drafts;
39
+ }
40
+
41
+ export interface TextFallbackAdapterOptions {
42
+ /** Overrides how a real skip-ledger finding surfaces from `chunk()` (default: a stderr warning) — see `coverage.ts`'s `SkipLedgerReporter`. Never actually fires here: windows are gapless by construction (see module doc). */
43
+ onSkipLedgerGap?: SkipLedgerReporter;
44
+ }
45
+
46
+ export function createTextFallbackAdapter(
47
+ options: TextFallbackAdapterOptions = {},
48
+ ): ArtifactAdapter {
49
+ return {
50
+ supports(): boolean {
51
+ return true; // catch-all; the registry only reaches this adapter once nothing else claimed the file.
52
+ },
53
+
54
+ async read(input: ArtifactInput): Promise<ArtifactDocument[]> {
55
+ return buildDocuments(input, "text");
56
+ },
57
+
58
+ chunk(document: ArtifactDocument): ArtifactChunk[] {
59
+ const index = buildLineIndex(document.content);
60
+ return buildChunksReportingSkips(
61
+ document,
62
+ index,
63
+ windowDrafts(index.lineCount),
64
+ options.onSkipLedgerGap,
65
+ );
66
+ },
67
+
68
+ links(): ArtifactLink[] {
69
+ return [];
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Cross-artifact configuration validation (spec.md §19.2: "Name MUSS
3
+ * projektweit eindeutig sein.").
4
+ */
5
+ import type { ContextArtifactConfig } from "@untangle-code/domain";
6
+ import { configInvalidError } from "@untangle-code/observability";
7
+
8
+ /**
9
+ * Validates a whole project's `ContextArtifactConfig` list: every `name`
10
+ * must be non-empty and unique across the list (case-sensitive — two
11
+ * artifacts differing only by case are almost certainly a copy/paste bug,
12
+ * and case-insensitive uniqueness would silently reject that legitimate
13
+ * edge case too, so this stays deliberately literal).
14
+ *
15
+ * Throws `UntangleError("CONFIG_INVALID", …)` (rule 8) with every offending
16
+ * name listed in `context.duplicateNames`, not just the first one found, so
17
+ * a single fix-and-rerun cycle can catch everything at once.
18
+ */
19
+ export function validateArtifactConfigs(configs: readonly ContextArtifactConfig[]): void {
20
+ const seen = new Map<string, number>();
21
+ const emptyNameCount = configs.filter((config) => config.name.trim().length === 0).length;
22
+ if (emptyNameCount > 0) {
23
+ throw configInvalidError(
24
+ `${emptyNameCount} context artifact config(s) have an empty "name" (spec.md §19.2 requires a project-wide unique name)`,
25
+ { context: { emptyNameCount } },
26
+ );
27
+ }
28
+ for (const config of configs) {
29
+ seen.set(config.name, (seen.get(config.name) ?? 0) + 1);
30
+ }
31
+ const duplicateNames = [...seen.entries()]
32
+ .filter(([, count]) => count > 1)
33
+ .map(([name]) => name)
34
+ .sort();
35
+ if (duplicateNames.length > 0) {
36
+ throw configInvalidError(
37
+ `context artifact name(s) are not project-wide unique (spec.md §19.2): ${duplicateNames.join(", ")}`,
38
+ { context: { duplicateNames } },
39
+ );
40
+ }
41
+ }