@mnemonik/shared 7.144.2 → 7.145.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 (113) hide show
  1. package/dist/ast/grammars.d.ts +6 -6
  2. package/dist/ast/grammars.d.ts.map +1 -1
  3. package/dist/ast/grammars.js +51 -10
  4. package/dist/ast/grammars.js.map +1 -1
  5. package/dist/sourceRedaction.d.ts.map +1 -1
  6. package/dist/sourceRedaction.js +743 -64
  7. package/dist/sourceRedaction.js.map +1 -1
  8. package/package.json +2 -1
  9. package/queries/clojure.tags.scm +1 -0
  10. package/queries/dart.tags.scm +3 -0
  11. package/queries/erlang.tags.scm +2 -0
  12. package/queries/graphql.tags.scm +3 -0
  13. package/queries/haskell.tags.scm +3 -0
  14. package/queries/julia.tags.scm +1 -0
  15. package/queries/kotlin.tags.scm +3 -0
  16. package/queries/ocaml.tags.scm +2 -0
  17. package/queries/ocaml_interface.tags.scm +1 -0
  18. package/queries/perl.tags.scm +2 -0
  19. package/queries/protobuf.tags.scm +5 -0
  20. package/queries/r.tags.scm +2 -0
  21. package/queries/zig.tags.scm +2 -0
  22. package/scripts/vendor-grammars.ts +586 -35
  23. package/src/ast/grammars.ts +51 -10
  24. package/src/sourceRedaction.ts +765 -12
  25. package/wasm/bash.wasm.gz +0 -0
  26. package/wasm/c-sharp.wasm.gz +0 -0
  27. package/wasm/c.wasm.gz +0 -0
  28. package/wasm/clojure.LICENSE +21 -0
  29. package/wasm/clojure.tags.scm +1 -0
  30. package/wasm/clojure.wasm.gz +0 -0
  31. package/wasm/cmake.wasm.gz +0 -0
  32. package/wasm/cpp.wasm.gz +0 -0
  33. package/wasm/dart.LICENSE +20 -0
  34. package/wasm/dart.tags.scm +3 -0
  35. package/wasm/dart.wasm.gz +0 -0
  36. package/wasm/elixir.wasm.gz +0 -0
  37. package/wasm/erlang.LICENSE +201 -0
  38. package/wasm/erlang.tags.scm +2 -0
  39. package/wasm/erlang.wasm.gz +0 -0
  40. package/wasm/go.wasm.gz +0 -0
  41. package/wasm/graphql.LICENSE +21 -0
  42. package/wasm/graphql.tags.scm +3 -0
  43. package/wasm/graphql.wasm.gz +0 -0
  44. package/wasm/groovy.wasm.gz +0 -0
  45. package/wasm/haskell.LICENSE +21 -0
  46. package/wasm/haskell.tags.scm +3 -0
  47. package/wasm/haskell.wasm.gz +0 -0
  48. package/wasm/java.wasm.gz +0 -0
  49. package/wasm/javascript.wasm.gz +0 -0
  50. package/wasm/julia.LICENSE +21 -0
  51. package/wasm/julia.tags.scm +1 -0
  52. package/wasm/julia.wasm.gz +0 -0
  53. package/wasm/kotlin.LICENSE +9 -0
  54. package/wasm/kotlin.tags.scm +3 -0
  55. package/wasm/kotlin.wasm.gz +0 -0
  56. package/wasm/lua.wasm.gz +0 -0
  57. package/wasm/nix.wasm.gz +0 -0
  58. package/wasm/objcpp.wasm.gz +0 -0
  59. package/wasm/ocaml.LICENSE +21 -0
  60. package/wasm/ocaml.tags.scm +2 -0
  61. package/wasm/ocaml.wasm.gz +0 -0
  62. package/wasm/ocaml_interface.LICENSE +21 -0
  63. package/wasm/ocaml_interface.tags.scm +1 -0
  64. package/wasm/ocaml_interface.wasm.gz +0 -0
  65. package/wasm/perl.LICENSE +7 -0
  66. package/wasm/perl.tags.scm +2 -0
  67. package/wasm/perl.wasm.gz +0 -0
  68. package/wasm/php.wasm.gz +0 -0
  69. package/wasm/powershell.wasm.gz +0 -0
  70. package/wasm/protobuf.LICENSE +21 -0
  71. package/wasm/protobuf.tags.scm +5 -0
  72. package/wasm/protobuf.wasm.gz +0 -0
  73. package/wasm/python.wasm.gz +0 -0
  74. package/wasm/r.LICENSE +9 -0
  75. package/wasm/r.tags.scm +2 -0
  76. package/wasm/r.wasm.gz +0 -0
  77. package/wasm/ruby.wasm.gz +0 -0
  78. package/wasm/rust.wasm.gz +0 -0
  79. package/wasm/scala.wasm.gz +0 -0
  80. package/wasm/solidity.wasm.gz +0 -0
  81. package/wasm/sql.wasm.gz +0 -0
  82. package/wasm/swift.wasm.gz +0 -0
  83. package/wasm/terraform.wasm.gz +0 -0
  84. package/wasm/tsx.wasm.gz +0 -0
  85. package/wasm/typescript.wasm.gz +0 -0
  86. package/wasm/zig.LICENSE +29 -0
  87. package/wasm/zig.tags.scm +2 -0
  88. package/wasm/zig.wasm.gz +0 -0
  89. package/wasm/bash.wasm +0 -0
  90. package/wasm/c-sharp.wasm +0 -0
  91. package/wasm/c.wasm +0 -0
  92. package/wasm/cmake.wasm +0 -0
  93. package/wasm/cpp.wasm +0 -0
  94. package/wasm/elixir.wasm +0 -0
  95. package/wasm/go.wasm +0 -0
  96. package/wasm/groovy.wasm +0 -0
  97. package/wasm/java.wasm +0 -0
  98. package/wasm/javascript.wasm +0 -0
  99. package/wasm/lua.wasm +0 -0
  100. package/wasm/nix.wasm +0 -0
  101. package/wasm/objcpp.wasm +0 -0
  102. package/wasm/php.wasm +0 -0
  103. package/wasm/powershell.wasm +0 -0
  104. package/wasm/python.wasm +0 -0
  105. package/wasm/ruby.wasm +0 -0
  106. package/wasm/rust.wasm +0 -0
  107. package/wasm/scala.wasm +0 -0
  108. package/wasm/solidity.wasm +0 -0
  109. package/wasm/sql.wasm +0 -0
  110. package/wasm/swift.wasm +0 -0
  111. package/wasm/terraform.wasm +0 -0
  112. package/wasm/tsx.wasm +0 -0
  113. package/wasm/typescript.wasm +0 -0
@@ -1,7 +1,7 @@
1
1
  import { parseVueSource, walkVueTemplate, VUE_FIDELITY_MARKER } from './vueSource.js';
2
2
  import { basename, extname } from 'node:path';
3
3
  import { Parser, type Node, type Tree } from 'web-tree-sitter';
4
- import { loadGrammar, objcppSyntaxIssue } from './ast/grammars.js';
4
+ import { loadGrammar, objcppSyntaxIssue, type AstLanguageId } from './ast/grammars.js';
5
5
  import { MAX_AST_PARSE_BYTES } from './ast/astChunker.js';
6
6
  import { commandLineCredentialRanges, redactCredentials } from './redactCredentials.js';
7
7
  import {
@@ -19,8 +19,42 @@ function scrubPayload(text: string): string {
19
19
  export async function scrubSourceSecrets(text: string, filePath: string): Promise<string> {
20
20
  const extension = extname(filePath).toLowerCase();
21
21
  if (extension === '.vue') return scrubVueSource(text);
22
+ if (extension === '.svelte') return scrubSvelteSource(text);
23
+ const bundledLanguages: Partial<Record<string, AstLanguageId>> = {
24
+ '.go': 'go',
25
+ '.java': 'java',
26
+ '.cs': 'csharp',
27
+ '.scala': 'scala',
28
+ '.sc': 'scala',
29
+ '.rb': 'ruby',
30
+ '.php': 'php',
31
+ '.ex': 'elixir',
32
+ '.exs': 'elixir',
33
+ '.sol': 'solidity',
34
+ '.kt': 'kotlin',
35
+ '.kts': 'kotlin',
36
+ '.dart': 'dart',
37
+ '.erl': 'erlang',
38
+ '.hrl': 'erlang',
39
+ '.hs': 'haskell',
40
+ '.ml': 'ocaml',
41
+ '.mli': 'ocaml_interface',
42
+ '.clj': 'clojure',
43
+ '.cljs': 'clojure',
44
+ '.cljc': 'clojure',
45
+ '.jl': 'julia',
46
+ '.pl': 'perl',
47
+ '.pm': 'perl',
48
+ '.r': 'r',
49
+ '.zig': 'zig',
50
+ '.proto': 'protobuf',
51
+ '.graphql': 'graphql',
52
+ '.gql': 'graphql',
53
+ };
54
+ const bundledLanguage = bundledLanguages[extension];
22
55
  const language =
23
- extension === '.cmake' || basename(filePath) === 'CMakeLists.txt'
56
+ bundledLanguage ??
57
+ (extension === '.cmake' || basename(filePath) === 'CMakeLists.txt'
24
58
  ? 'cmake'
25
59
  : ['.ts', '.mts', '.cts'].includes(extension)
26
60
  ? 'typescript'
@@ -28,7 +62,7 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
28
62
  ? 'tsx'
29
63
  : ['.js', '.jsx', '.mjs', '.cjs'].includes(extension)
30
64
  ? 'javascript'
31
- : ['.py', '.pyi'].includes(extension)
65
+ : ['.py', '.pyi', '.bzl'].includes(extension)
32
66
  ? 'python'
33
67
  : ['.sh', '.bash', '.zsh'].includes(extension)
34
68
  ? 'bash'
@@ -56,10 +90,34 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
56
90
  ? 'groovy'
57
91
  : ['.ps1', '.psm1'].includes(extension)
58
92
  ? 'powershell'
59
- : null;
60
- const fallback = (): string => {
61
- const clean = scrubPayload(text);
93
+ : null);
94
+ const fallback = (source = text, payloadsScrubbed = false): string => {
95
+ // A completed AST payload pass must not be reinterpreted as prose (e.g. proto field numbers).
96
+ const clean = payloadsScrubbed ? source : scrubPayload(source);
62
97
  if (!text) return clean;
98
+ if (bundledLanguage) {
99
+ const marker = `/* mnemonik:${bundledLanguage}-source-fidelity-unavailable */`;
100
+ const nativeMarker: Partial<Record<AstLanguageId, string>> = {
101
+ erlang: `% mnemonik:${bundledLanguage}-source-fidelity-unavailable`,
102
+ haskell: `-- mnemonik:${bundledLanguage}-source-fidelity-unavailable`,
103
+ ocaml: `(* mnemonik:${bundledLanguage}-source-fidelity-unavailable *)`,
104
+ ocaml_interface: `(* mnemonik:${bundledLanguage}-source-fidelity-unavailable *)`,
105
+ clojure: `; mnemonik:${bundledLanguage}-source-fidelity-unavailable`,
106
+ zig: `// mnemonik:${bundledLanguage}-source-fidelity-unavailable`,
107
+ };
108
+ const comment =
109
+ nativeMarker[bundledLanguage] ??
110
+ (['ruby', 'elixir', 'julia', 'perl', 'r', 'graphql'].includes(bundledLanguage)
111
+ ? marker.replace('/* ', '# ').replace(' */', '')
112
+ : bundledLanguage === 'php'
113
+ ? `<?php ${marker} ?>`
114
+ : marker);
115
+ return clean.startsWith(comment) ? clean : comment + '\n' + clean;
116
+ }
117
+ if (extension === '.bzl') {
118
+ const marker = '# mnemonik:starlark-source-fidelity-unavailable\n';
119
+ return clean.startsWith(marker) ? clean : marker + clean;
120
+ }
63
121
  if (language === 'powershell') {
64
122
  const marker = '# mnemonik:powershell-source-fidelity-unavailable\n';
65
123
  return clean.startsWith(marker) ? clean : marker + clean;
@@ -89,12 +147,20 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
89
147
  parser = new Parser();
90
148
  parser.setLanguage(grammar.ok.language);
91
149
  tree = parser.parse(text);
92
- if (!tree || tree.rootNode.hasError) return fallback();
150
+ if (!tree) return fallback();
151
+ if (tree.rootNode.hasError) {
152
+ // These bindings have no prose-style credential '=' label. Recover AST-proven payloads;
153
+ // opaque ERROR spans are scrubbed conservatively and always remain unavailable.
154
+ return bundledLanguage && ['clojure', 'r', 'protobuf', 'graphql'].includes(bundledLanguage)
155
+ ? fallback(redactBundledNode(tree.rootNode, bundledLanguage, true), true)
156
+ : fallback();
157
+ }
93
158
  const root = tree.rootNode;
94
159
  if (language === 'objcpp' && objcppSyntaxIssue(root)) return fallback();
95
160
 
96
- const redacted =
97
- language === 'cmake'
161
+ const redacted = bundledLanguage
162
+ ? redactBundledNode(root, bundledLanguage)
163
+ : language === 'cmake'
98
164
  ? redactCmakeNode(root)
99
165
  : language === 'bash'
100
166
  ? redactShellNode(root, commandLineCredentialRanges(text))
@@ -117,7 +183,16 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
117
183
  : language === 'powershell'
118
184
  ? redactPowershellNode(root)
119
185
  : redactNode(root);
120
- return text.slice(0, root.startIndex) + redacted + text.slice(root.endIndex);
186
+ const clean = text.slice(0, root.startIndex) + redacted + text.slice(root.endIndex);
187
+ if (bundledLanguage && clean !== text) {
188
+ const proof = parser.parse(clean);
189
+ try {
190
+ if (!proof || proof.rootNode.hasError) return fallback(clean);
191
+ } finally {
192
+ proof?.delete();
193
+ }
194
+ }
195
+ return clean;
121
196
  } catch {
122
197
  return fallback();
123
198
  } finally {
@@ -126,6 +201,531 @@ export async function scrubSourceSecrets(text: string, filePath: string): Promis
126
201
  }
127
202
  }
128
203
 
204
+ /** Bundled L1 grammars expose literal payloads separately from executable expressions. */
205
+ function redactBundledNode(root: Node, language: AstLanguageId, incomplete = false): string {
206
+ const labeled = new Set<number>();
207
+ const heredocs: boolean[] = [];
208
+ const label = (node: Node | null): boolean => {
209
+ if (!node) return false;
210
+ if (language === 'julia' && node.type === 'typed_expression')
211
+ return label(node.firstNamedChild);
212
+ if (language === 'perl' && node.type === 'variable_declaration')
213
+ return label(node.childForFieldName('variable'));
214
+ if (language === 'perl' && node.type === 'scalar') return label(node.firstNamedChild);
215
+ if (
216
+ language === 'kotlin' &&
217
+ ['navigation_expression', 'directly_assignable_expression'].includes(node.type)
218
+ ) {
219
+ const last = node.lastNamedChild;
220
+ return label(
221
+ last?.type === 'navigation_suffix' ? last.firstNamedChild : node.firstNamedChild
222
+ );
223
+ }
224
+ if (language === 'dart' && node.type === 'assignable_expression') {
225
+ const last = node.lastNamedChild;
226
+ return label(
227
+ last?.type === 'unconditional_assignable_selector'
228
+ ? last.firstNamedChild
229
+ : node.firstNamedChild
230
+ );
231
+ }
232
+ if (
233
+ [
234
+ 'expression',
235
+ 'literal_element',
236
+ 'variable_name',
237
+ 'assignable_expression',
238
+ 'directly_assignable_expression',
239
+ ].includes(node.type)
240
+ )
241
+ return label(node.firstNamedChild);
242
+ const field =
243
+ node.childForFieldName('field') ??
244
+ node.childForFieldName('member') ??
245
+ node.childForFieldName('property') ??
246
+ node.childForFieldName('name') ??
247
+ node;
248
+ return SECRET_LABEL_RE.test(
249
+ field.text
250
+ .trim()
251
+ .replace(/^[$@]+|["':]+$/g, '')
252
+ .replace(/^["']/, '')
253
+ );
254
+ };
255
+ const mark = (key: Node | null, value: Node | null): void => {
256
+ if (!key || !value) return;
257
+ if (label(key)) labeled.add(value.id);
258
+ else if (key.type === 'tuple_pattern' && value.type === 'tuple_expression')
259
+ key.namedChildren.forEach((part, i) => mark(part, value.namedChildren[i] ?? null));
260
+ };
261
+ const payload = (text: string, sensitive: boolean): string =>
262
+ scrubPayload(text)
263
+ .split(/(\r\n|[\r\n])/)
264
+ .map((part, i) =>
265
+ i % 2
266
+ ? part
267
+ : part.replace(
268
+ /^(\s*)([\s\S]*?)(\s*)$/,
269
+ (_match, lead: string, body: string, tail: string) =>
270
+ lead + literalPayload(body, sensitive) + tail
271
+ )
272
+ )
273
+ .join('');
274
+ const visit = (node: Node, sensitive = false): string => {
275
+ sensitive ||= labeled.has(node.id);
276
+ const original = node.text;
277
+ const type = node.type;
278
+ if (incomplete && type === 'ERROR') return redactSecretValue(original);
279
+ if (
280
+ [
281
+ 'comment',
282
+ 'line_comment',
283
+ 'block_comment',
284
+ 'documentation_comment',
285
+ 'multiline_comment',
286
+ ].includes(type)
287
+ ) {
288
+ if (language === 'clojure') {
289
+ if (original.startsWith(';')) return ';' + scrubPayload(original.slice(1));
290
+ if (original.startsWith('#!')) return '#!' + scrubPayload(original.slice(2));
291
+ // Reader-discard and (comment ...) contain forms, not line-comment payloads.
292
+ let clean = '';
293
+ let offset = node.startIndex;
294
+ for (const child of node.namedChildren) {
295
+ clean +=
296
+ original.slice(offset - node.startIndex, child.startIndex - node.startIndex) +
297
+ visit(child);
298
+ offset = child.endIndex;
299
+ }
300
+ return clean + original.slice(offset - node.startIndex);
301
+ }
302
+ if (language === 'erlang') return '%' + scrubPayload(original.slice(1));
303
+ if (language === 'haskell')
304
+ return original.startsWith('{-')
305
+ ? '{-' + scrubPayload(original.slice(2, -2)) + '-}'
306
+ : '--' + scrubPayload(original.slice(2));
307
+ if (language === 'ocaml' || language === 'ocaml_interface')
308
+ return '(*' + scrubPayload(original.slice(2, -2)) + '*)';
309
+ if (original.startsWith('#')) return '#' + scrubPayload(original.slice(1));
310
+ return original.startsWith('/*')
311
+ ? '/*' + scrubPayload(original.slice(2, -2)) + '*/'
312
+ : '//' + scrubPayload(original.slice(2));
313
+ }
314
+ if (
315
+ ['erlang', 'haskell', 'ocaml', 'ocaml_interface', 'clojure'].includes(language) &&
316
+ ['char', 'character'].includes(type)
317
+ ) {
318
+ if (sensitive || scrubPayload(original) !== original)
319
+ throw new Error('Restricted literal payload');
320
+ return original;
321
+ }
322
+ if ((language === 'ocaml' || language === 'ocaml_interface') && type === 'quoted_string') {
323
+ const opening = /^\{([a-z_]*)\|/.exec(original);
324
+ const closing = opening ? '|' + opening[1] + '}' : '';
325
+ if (!opening || !original.endsWith(closing))
326
+ throw new Error('Opaque quoted literal boundary');
327
+ return (
328
+ opening[0] +
329
+ payload(original.slice(opening[0].length, -closing.length), sensitive) +
330
+ closing
331
+ );
332
+ }
333
+ if (language === 'perl' && type === 'interpolated_string_literal') {
334
+ const content = node.childForFieldName('content');
335
+ return content
336
+ ? original.slice(0, content.startIndex - node.startIndex) +
337
+ visit(content, sensitive) +
338
+ original.slice(content.endIndex - node.startIndex)
339
+ : original;
340
+ }
341
+ if (language === 'perl' && type === 'string_content') {
342
+ let clean = '';
343
+ let offset = node.startIndex;
344
+ for (const child of node.namedChildren.filter(
345
+ (c) => !['escape_sequence', 'escaped_delimiter'].includes(c.type)
346
+ )) {
347
+ clean +=
348
+ payload(
349
+ original.slice(offset - node.startIndex, child.startIndex - node.startIndex),
350
+ sensitive
351
+ ) + visit(child);
352
+ offset = child.endIndex;
353
+ }
354
+ return clean + payload(original.slice(offset - node.startIndex), sensitive);
355
+ }
356
+ if (type === 'heredoc_beginning') {
357
+ heredocs.push(sensitive);
358
+ return original;
359
+ }
360
+ if (language === 'ruby' && type === 'heredoc_body') sensitive = heredocs.shift() ?? false;
361
+ if (type === 'hex_string_literal')
362
+ return sensitive
363
+ ? original.replace(
364
+ /(["'])([0-9a-fA-F_\s]*)\1/g,
365
+ (_match, quote: string, body: string) =>
366
+ quote + body.replace(/[0-9a-fA-F_]/g, '') + quote
367
+ )
368
+ : original;
369
+ // shortcut: restricted character/symbol literals use fallback; add native placeholders if needed.
370
+ if (['character_literal', 'rune_literal', 'simple_symbol', 'atom'].includes(type)) {
371
+ if (sensitive || scrubPayload(original) !== original)
372
+ throw new Error('Restricted literal payload');
373
+ return original;
374
+ }
375
+ if (
376
+ sensitive &&
377
+ /^(?:int|float|imaginary|integer|real|decimal_integer|hex_integer|octal_integer|binary_integer|decimal_floating_point|hex_floating_point|number)(?:_literal)?$/.test(
378
+ type
379
+ )
380
+ )
381
+ return '"' + redactSecretValue(original) + '"';
382
+ const string =
383
+ [
384
+ 'string',
385
+ 'interpreted_string_literal',
386
+ 'raw_string_literal',
387
+ 'verbatim_string_literal',
388
+ 'encapsed_string',
389
+ 'unicode_string_literal',
390
+ 'string_value',
391
+ 'interpolated_string',
392
+ 'sigil',
393
+ ].includes(type) ||
394
+ (type === 'string_literal' && language !== 'solidity');
395
+ if (string) {
396
+ const first = node.firstChild;
397
+ const last = node.lastChild;
398
+ const quotedStart =
399
+ node.childForFieldName('quoted_start') ??
400
+ (language === 'r' ? node.childForFieldName('open') : null);
401
+ const quotedEnd =
402
+ node.childForFieldName('quoted_end') ??
403
+ (language === 'r' ? node.childForFieldName('close') : null);
404
+ const opening = quotedStart ?? first;
405
+ const closing = quotedEnd ?? last;
406
+ const match = /^(?:r|@|unicode)?("{3,}|'{3,}|"|'|`)/.exec(original);
407
+ const start = quotedStart
408
+ ? quotedStart.endIndex
409
+ : opening && /^(?:%[qQwWiIxrs]?[^\w\s]|["'`]+)$/.test(opening.text)
410
+ ? opening.endIndex
411
+ : match
412
+ ? node.startIndex + match[0].length
413
+ : null;
414
+ const end = quotedEnd
415
+ ? quotedEnd.startIndex
416
+ : match?.[1]
417
+ ? node.endIndex - match[1].length
418
+ : closing && closing.id !== opening?.id
419
+ ? closing.startIndex
420
+ : null;
421
+ if (start === null || end === null || end < start) throw new Error('Opaque literal boundary');
422
+ const substitutions = node.children.filter(
423
+ (child) =>
424
+ child.startIndex >= start &&
425
+ child.endIndex <= end &&
426
+ ![
427
+ 'string_content',
428
+ 'content',
429
+ 'string_fragment',
430
+ 'multiline_string_fragment',
431
+ 'raw_string_content',
432
+ 'raw_string_literal_content',
433
+ 'interpreted_string_literal_content',
434
+ 'string_literal_content',
435
+ 'quoted_content',
436
+ 'escape_sequence',
437
+ ].includes(child.type)
438
+ );
439
+ let result = original.slice(0, start - node.startIndex);
440
+ let offset = start;
441
+ for (const child of substitutions) {
442
+ result += payload(
443
+ original.slice(offset - node.startIndex, child.startIndex - node.startIndex),
444
+ sensitive
445
+ );
446
+ result += visit(child); // Runtime interpolation never inherits credential sensitivity.
447
+ offset = child.endIndex;
448
+ }
449
+ return (
450
+ result +
451
+ payload(original.slice(offset - node.startIndex, end - node.startIndex), sensitive) +
452
+ original.slice(end - node.startIndex)
453
+ );
454
+ }
455
+ if (['string_content', 'heredoc_content', 'quoted_content', 'nowdoc_body'].includes(type))
456
+ return payload(original, sensitive);
457
+ if (language === 'julia' && type === 'assignment')
458
+ mark(node.firstNamedChild, node.lastNamedChild);
459
+ if (language === 'r' && type === 'binary_operator') {
460
+ const op = node.childForFieldName('operator')?.text;
461
+ if (['<-', '<<-', '=', ':='].includes(op ?? ''))
462
+ mark(node.childForFieldName('lhs'), node.childForFieldName('rhs'));
463
+ if (['->', '->>'].includes(op ?? ''))
464
+ mark(node.childForFieldName('rhs'), node.childForFieldName('lhs'));
465
+ }
466
+ if (language === 'r' && ['parameter', 'argument'].includes(type))
467
+ mark(
468
+ node.childForFieldName('name'),
469
+ node.childForFieldName('default') ?? node.childForFieldName('value')
470
+ );
471
+ if (language === 'zig' && ['assignment_statement', 'field_declaration'].includes(type))
472
+ mark(
473
+ node.childForFieldName('name'),
474
+ node.childForFieldName('expression') ?? node.childForFieldName('default')
475
+ );
476
+ if (language === 'protobuf' && type === 'field') {
477
+ const name = node.namedChildren.find((c) => c.type === 'identifier');
478
+ for (const option of node.namedChildren.find((c) => c.type === 'field_options')
479
+ ?.namedChildren ?? []) {
480
+ if (
481
+ option.type !== 'field_option' ||
482
+ option.namedChildren.find((c) => c.type === 'identifier')?.text !== 'default'
483
+ )
484
+ continue;
485
+ const constant = option.namedChildren.find((c) => c.type === 'constant');
486
+ mark(name ?? null, constant?.namedChildren.find((c) => c.type === 'string') ?? null);
487
+ }
488
+ }
489
+ if (language === 'graphql' && type === 'object_field')
490
+ mark(
491
+ node.namedChildren.find((c) => c.type === 'name') ?? null,
492
+ node.namedChildren.find((c) => c.type === 'value') ?? null
493
+ );
494
+ if (
495
+ language === 'graphql' &&
496
+ ['input_value_definition', 'variable_definition'].includes(type)
497
+ ) {
498
+ const name =
499
+ type === 'input_value_definition'
500
+ ? node.namedChildren.find((c) => c.type === 'name')
501
+ : node.namedChildren.find((c) => c.type === 'variable')?.firstNamedChild;
502
+ const value = node.namedChildren.find((c) => c.type === 'default_value')?.firstNamedChild;
503
+ mark(name ?? null, value ?? null);
504
+ }
505
+ if (language === 'erlang' && type === 'match_expr')
506
+ mark(node.childForFieldName('lhs'), node.childForFieldName('rhs'));
507
+ if (language === 'haskell' && type === 'bind') {
508
+ const name = node.childForFieldName('name');
509
+ mark(name, node.childForFieldName('expression'));
510
+ for (const match of node.childrenForFieldName('match'))
511
+ mark(name, match.childForFieldName('expression'));
512
+ }
513
+ if ((language === 'ocaml' || language === 'ocaml_interface') && type === 'let_binding')
514
+ mark(node.childForFieldName('pattern'), node.childForFieldName('body'));
515
+ if (language === 'clojure' && type === 'list') {
516
+ const forms = node.namedChildren.filter((c) => c.type !== 'comment');
517
+ if (
518
+ forms[0]?.type === 'symbol' &&
519
+ forms[0].text === 'def' &&
520
+ forms[1]?.type === 'symbol' &&
521
+ forms.length >= 3
522
+ )
523
+ mark(forms[1], forms.at(-1) ?? null);
524
+ if (forms[0]?.type === 'symbol' && forms[0].text === 'let' && forms[1]?.type === 'vector') {
525
+ const bindings = forms[1].namedChildren.filter((c) => c.type !== 'comment');
526
+ for (let i = 0; i + 1 < bindings.length; i += 2)
527
+ mark(bindings[i] ?? null, bindings[i + 1] ?? null);
528
+ }
529
+ }
530
+ if (language === 'kotlin' && type === 'assignment')
531
+ mark(node.firstNamedChild, node.lastNamedChild);
532
+ if (language === 'dart' && type === 'assignment_expression_without_cascade')
533
+ mark(node.childForFieldName('left'), node.childForFieldName('right'));
534
+ if (language === 'kotlin' && type === 'property_declaration') {
535
+ const key = node.namedChildren.find(
536
+ (c) => c.type === 'variable_declaration'
537
+ )?.firstNamedChild;
538
+ const equals = node.children.findIndex((c) => c.type === '=');
539
+ if (equals >= 0) mark(key ?? null, node.children[equals + 1] ?? null);
540
+ }
541
+ if (
542
+ language === 'dart' &&
543
+ [
544
+ 'initialized_variable_definition',
545
+ 'initialized_identifier',
546
+ 'static_final_declaration',
547
+ ].includes(type)
548
+ ) {
549
+ const equals = node.children.findIndex((c) => c.type === '=');
550
+ if (equals >= 0)
551
+ mark(
552
+ node.childForFieldName('name') ?? node.firstNamedChild,
553
+ node.children[equals + 1] ?? null
554
+ );
555
+ }
556
+ // These grammars place defaults beside the parameter rather than inside it.
557
+ if (
558
+ (language === 'kotlin' && type === 'function_value_parameters') ||
559
+ (language === 'dart' && type === 'optional_formal_parameters')
560
+ ) {
561
+ node.children.forEach((child, i, children) => {
562
+ if (!['=', ':'].includes(child.type)) return;
563
+ const parameter = children[i - 1];
564
+ mark(
565
+ parameter?.childForFieldName('name') ?? parameter?.firstNamedChild ?? null,
566
+ children[i + 1] ?? null
567
+ );
568
+ });
569
+ }
570
+ if (language === 'dart' && type === 'named_argument')
571
+ mark(node.firstNamedChild?.firstNamedChild ?? null, node.lastNamedChild);
572
+ if (
573
+ language === 'kotlin' &&
574
+ type === 'value_argument' &&
575
+ node.children.some((c) => c.type === '=')
576
+ )
577
+ mark(node.firstNamedChild, node.lastNamedChild);
578
+ if (type === 'var_spec' || type === 'const_spec') {
579
+ const names = node.childrenForFieldName('name');
580
+ const values = node.childForFieldName('value')?.namedChildren ?? [];
581
+ names.forEach((name, i) => mark(name, values[i] ?? null));
582
+ } else if (
583
+ language === 'go' &&
584
+ ['short_var_declaration', 'assignment_statement'].includes(type)
585
+ ) {
586
+ const names = node.childForFieldName('left')?.namedChildren ?? [];
587
+ const values = node.childForFieldName('right')?.namedChildren ?? [];
588
+ names.forEach((name, i) => mark(name, values[i] ?? null));
589
+ } else if (type === 'variable_declarator') {
590
+ mark(
591
+ node.childForFieldName('name'),
592
+ node.childForFieldName('value') ?? node.namedChildren[1] ?? null
593
+ );
594
+ } else if (language === 'php' && type === 'property_element') {
595
+ mark(node.childForFieldName('name'), node.childForFieldName('default_value'));
596
+ } else if (language === 'csharp' && type === 'property_declaration') {
597
+ mark(node.childForFieldName('name'), node.childForFieldName('value'));
598
+ } else if (['val_definition', 'var_definition'].includes(type)) {
599
+ mark(node.childForFieldName('pattern'), node.childForFieldName('value'));
600
+ } else if (
601
+ ['assignment', 'assignment_expression', 'operator_assignment'].includes(type) ||
602
+ (language === 'elixir' &&
603
+ type === 'binary_operator' &&
604
+ node.childForFieldName('operator')?.text === '=')
605
+ ) {
606
+ mark(node.childForFieldName('left'), node.childForFieldName('right'));
607
+ } else if (['pair', 'keyed_element'].includes(type)) {
608
+ mark(node.childForFieldName('key'), node.childForFieldName('value'));
609
+ } else if (
610
+ language === 'php' &&
611
+ type === 'array_element_initializer' &&
612
+ node.children.some((c) => c.type === '=>')
613
+ ) {
614
+ mark(node.namedChildren[0] ?? null, node.namedChildren[1] ?? null);
615
+ } else if (type === 'state_variable_declaration') {
616
+ mark(node.childForFieldName('name'), node.childForFieldName('value'));
617
+ } else if (language === 'solidity' && type === 'variable_declaration_statement') {
618
+ mark(
619
+ node.namedChildren
620
+ .find((c) => c.type === 'variable_declaration')
621
+ ?.childForFieldName('name') ?? null,
622
+ node.childForFieldName('value')
623
+ );
624
+ }
625
+ if (
626
+ [
627
+ 'parameter',
628
+ 'optional_parameter',
629
+ 'simple_parameter',
630
+ 'class_parameter',
631
+ 'named_argument',
632
+ ].includes(type)
633
+ ) {
634
+ const equals = node.children.findIndex((child) => child.type === '=');
635
+ if (equals >= 0)
636
+ mark(
637
+ node.childForFieldName('name') ?? node.firstNamedChild,
638
+ node.children[equals + 1] ?? null
639
+ );
640
+ }
641
+ let result = '';
642
+ let offset = node.startIndex;
643
+ for (const child of node.namedChildren) {
644
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
645
+ const carry =
646
+ sensitive &&
647
+ (((type !== 'list' || language !== 'clojure') &&
648
+ [
649
+ 'expression',
650
+ 'expression_list',
651
+ 'literal_element',
652
+ 'literal_value',
653
+ 'array',
654
+ 'array_creation_expression',
655
+ 'hash',
656
+ 'list',
657
+ 'keywords',
658
+ 'tuple_expression',
659
+ 'initializer_expression',
660
+ 'collection_expression',
661
+ 'parenthesized_expression',
662
+ 'unary_expression',
663
+ 'interpolated_string_expression',
664
+ 'string_literal',
665
+ 'heredoc',
666
+ 'heredoc_body',
667
+ 'nowdoc',
668
+ 'regex',
669
+ 'quoted_atom',
670
+ 'delimited_symbol',
671
+ ].includes(type)) ||
672
+ (language === 'clojure' && ['vector', 'hash_map', 'set'].includes(type)) ||
673
+ (language === 'erlang' && type === 'binary' && child.type === 'bin_element') ||
674
+ (language === 'erlang' &&
675
+ type === 'bin_element' &&
676
+ child.id === node.childForFieldName('element')?.id) ||
677
+ (language === 'haskell' &&
678
+ type === 'conditional' &&
679
+ [node.childForFieldName('then')?.id, node.childForFieldName('else')?.id].includes(
680
+ child.id
681
+ )) ||
682
+ (['ocaml', 'ocaml_interface'].includes(language) &&
683
+ ((type === 'if_expression' && ['then_clause', 'else_clause'].includes(child.type)) ||
684
+ (['then_clause', 'else_clause'].includes(type) &&
685
+ child.id === node.childForFieldName('expression')?.id))) ||
686
+ (language === 'haskell' && type === 'literal') ||
687
+ (language === 'graphql' &&
688
+ ['value', 'list_value', 'object_value', 'object_field'].includes(type)) ||
689
+ (['kotlin', 'dart'].includes(language) &&
690
+ [
691
+ 'collection_literal',
692
+ 'list_literal',
693
+ 'set_or_map_literal',
694
+ 'additive_expression',
695
+ 'if_null_expression',
696
+ 'elvis_expression',
697
+ ].includes(type)) ||
698
+ (type === 'composite_literal' && child.id === node.childForFieldName('body')?.id) ||
699
+ (type === 'array_element_initializer' && child.id === node.lastNamedChild?.id) ||
700
+ (['binary_expression', 'binary_operator'].includes(type) &&
701
+ ['+', '.', '<>', '||', '??'].includes(
702
+ node.childForFieldName('operator')?.text ?? ''
703
+ )) ||
704
+ (['conditional_expression', 'ternary_expression'].includes(type) &&
705
+ child.id !== node.childForFieldName('condition')?.id) ||
706
+ (['pair', 'keyed_element'].includes(type) &&
707
+ child.id === node.childForFieldName('value')?.id) ||
708
+ ([
709
+ 'method_invocation',
710
+ 'member_access_expression',
711
+ 'field_expression',
712
+ 'field_access',
713
+ 'member_expression',
714
+ ].includes(type) &&
715
+ child.id === node.childForFieldName('object')?.id) ||
716
+ (['invocation_expression', 'call_expression', 'call'].includes(type) &&
717
+ [
718
+ node.childForFieldName('function')?.id,
719
+ node.childForFieldName('receiver')?.id,
720
+ ].includes(child.id)));
721
+ result += visit(child, carry);
722
+ offset = child.endIndex;
723
+ }
724
+ return result + original.slice(offset - node.startIndex);
725
+ };
726
+ return visit(root);
727
+ }
728
+
129
729
  /** CMake substitutions remain source expressions; only AST argument/comment payloads are scrubbed. */
130
730
  function redactCmakeNode(node: Node, sensitive = false): string {
131
731
  const original = node.text;
@@ -780,7 +1380,11 @@ function sqlSecretLabel(node: Node | null): boolean {
780
1380
  return sqlSecretLabel(node.childForFieldName('name') ?? node.firstNamedChild);
781
1381
  // This dialect-neutral grammar sometimes calls PostgreSQL quoted identifiers literals.
782
1382
  if (node.type === 'identifier' || (node.type === 'literal' && node.text.startsWith('"')))
783
- return SECRET_LABEL_RE.test(node.text.replace(/^"|"$/g, '').replaceAll('""', '"'));
1383
+ return SECRET_LABEL_RE.test(
1384
+ node.text.startsWith('`')
1385
+ ? node.text.slice(1, -1).replaceAll('``', '`')
1386
+ : node.text.replace(/^"|"$/g, '').replaceAll('""', '"')
1387
+ );
784
1388
  return false;
785
1389
  }
786
1390
 
@@ -824,6 +1428,16 @@ function redactSqlNode(node: Node, sensitive = false): string {
824
1428
  const argumentLabel =
825
1429
  node.type === 'function_argument' &&
826
1430
  sqlSecretLabel(node.namedChildren.find((child) => child.type === 'identifier') ?? null);
1431
+ const argumentDefaultMarker = argumentLabel
1432
+ ? node.children.find((child) => ['keyword_default', '='].includes(child.type))
1433
+ : undefined;
1434
+ const argumentDefaultValue = argumentDefaultMarker
1435
+ ? node.namedChildren.find(
1436
+ (child) =>
1437
+ child.startIndex >= argumentDefaultMarker.endIndex &&
1438
+ !['comment', 'marginalia'].includes(child.type)
1439
+ )
1440
+ : undefined;
827
1441
  const aliasLabel = node.type === 'term' && sqlSecretLabel(node.childForFieldName('alias'));
828
1442
  const insert = node.type === 'list' && node.parent?.type === 'insert' ? node.parent : null;
829
1443
  const columns = insert?.namedChildren.find((child) => child.type === 'list');
@@ -856,7 +1470,7 @@ function redactSqlNode(node: Node, sensitive = false): string {
856
1470
  (labeled && child.id === right?.id) ||
857
1471
  (node.type === 'binary_expression' && sqlSecretLabel(right) && child.id === left?.id) ||
858
1472
  (columnLabel && previous?.type === 'keyword_default') ||
859
- (argumentLabel && child.type === 'literal') ||
1473
+ child.id === argumentDefaultValue?.id ||
860
1474
  (aliasLabel && child.id === node.childForFieldName('value')?.id) ||
861
1475
  child.id === node.childForFieldName('password')?.id ||
862
1476
  sqlSecretLabel(columnValues[index] ?? null)
@@ -1375,3 +1989,142 @@ async function scrubVueSource(text: string): Promise<string> {
1375
1989
  parser.delete();
1376
1990
  }
1377
1991
  }
1992
+
1993
+ /** Official component offsets delimit payloads; existing JS/TS rules handle script expressions. */
1994
+ async function scrubSvelteSource(text: string): Promise<string> {
1995
+ const marker = '<!-- mnemonik:svelte-source-fidelity-unavailable -->\n';
1996
+ const fallback = (): string => (text.startsWith(marker) ? '' : marker) + scrubPayload(text);
1997
+ if (!text) return text;
1998
+ if (text.includes('mnemonik:svelte-source-fidelity-unavailable')) return fallback();
1999
+ if (Buffer.byteLength(text, 'utf8') > MAX_AST_PARSE_BYTES) return fallback();
2000
+ let parser: Parser | undefined;
2001
+ try {
2002
+ const deadline = performance.now() + 4_000;
2003
+ const { parse } = await import('svelte/compiler');
2004
+ const ast = parse(text, { modern: true });
2005
+ const edits: Array<{ start: number; end: number; clean: string }> = [];
2006
+ let faithful = true;
2007
+ parser = new Parser();
2008
+ const js = await loadGrammar('javascript');
2009
+ const ts = await loadGrammar('typescript');
2010
+ if ('unavailable' in js || 'unavailable' in ts) return fallback();
2011
+ parser.setLanguage(js.ok.language);
2012
+ const activeParser = parser;
2013
+ const syntax = (source: string, expression = false, sensitive = false): string => {
2014
+ const prefix = expression ? `const ${sensitive ? 'password' : '__mnemonik_svelte'} = (` : '';
2015
+ const suffix = expression ? '\n);' : '';
2016
+ const full = prefix + source + suffix;
2017
+ const tree = activeParser.parse(full, null, {
2018
+ progressCallback: () => performance.now() > deadline,
2019
+ });
2020
+ try {
2021
+ if (!tree || tree.rootNode.hasError || tree.rootNode.descendantCount > 200_000) {
2022
+ faithful = false;
2023
+ return sensitive ? literalPayload(source, true) : scrubPayload(source);
2024
+ }
2025
+ const clean =
2026
+ full.slice(0, tree.rootNode.startIndex) +
2027
+ redactNode(tree.rootNode) +
2028
+ full.slice(tree.rootNode.endIndex);
2029
+ return clean.slice(prefix.length, clean.length - suffix.length);
2030
+ } finally {
2031
+ tree?.delete();
2032
+ }
2033
+ };
2034
+ const edit = (start: unknown, end: unknown, clean: (raw: string) => string): void => {
2035
+ if (
2036
+ typeof start !== 'number' ||
2037
+ typeof end !== 'number' ||
2038
+ start < 0 ||
2039
+ end < start ||
2040
+ end > text.length
2041
+ )
2042
+ throw new Error('Component offsets unavailable');
2043
+ edits.push({ start, end, clean: clean(text.slice(start, end)) });
2044
+ };
2045
+ let templateTs = false;
2046
+ for (const script of [ast.module, ast.instance]) {
2047
+ if (!script) continue;
2048
+ const lang = script.attributes.find((a) => a.name === 'lang');
2049
+ const language = !lang
2050
+ ? 'js'
2051
+ : Array.isArray(lang.value) && lang.value.length === 1 && lang.value[0]?.type === 'Text'
2052
+ ? lang.value[0].data
2053
+ : null;
2054
+ if (!['js', 'ts'].includes(language ?? '') || script.attributes.some((a) => a.name === 'src'))
2055
+ return fallback();
2056
+ parser.setLanguage(language === 'ts' ? ts.ok.language : js.ok.language);
2057
+ const offsets = script.content as unknown as { start?: unknown; end?: unknown };
2058
+ edit(offsets.start, offsets.end, (raw) => syntax(raw));
2059
+ if (script === ast.instance) templateTs = language === 'ts';
2060
+ }
2061
+ parser.setLanguage(templateTs ? ts.ok.language : js.ok.language);
2062
+ const pending: Array<{ value: unknown; sensitive: boolean }> = [
2063
+ { value: ast.fragment, sensitive: false },
2064
+ ];
2065
+ let count = 0;
2066
+ while (pending.length) {
2067
+ if (++count > 200_000 || performance.now() > deadline) return fallback();
2068
+ const entry = pending.pop();
2069
+ if (!entry || !entry.value || typeof entry.value !== 'object') continue;
2070
+ if (Array.isArray(entry.value)) {
2071
+ for (const value of entry.value as unknown[])
2072
+ pending.push({ value, sensitive: entry.sensitive });
2073
+ continue;
2074
+ }
2075
+ const node = entry.value as Record<string, unknown>;
2076
+ const type = typeof node.type === 'string' ? node.type : '';
2077
+ if (type === 'Text') {
2078
+ edit(node.start, node.end, (raw) => literalPayload(raw, entry.sensitive));
2079
+ continue;
2080
+ }
2081
+ if (type === 'Comment') {
2082
+ edit(node.start, node.end, (raw) => '<!--' + scrubPayload(raw.slice(4, -3)) + '-->');
2083
+ continue;
2084
+ }
2085
+ if (
2086
+ /Expression$/.test(type) ||
2087
+ ['Identifier', 'Literal', 'TemplateLiteral', 'VariableDeclaration'].includes(type)
2088
+ ) {
2089
+ edit(node.start, node.end, (raw) =>
2090
+ syntax(raw, type !== 'VariableDeclaration', entry.sensitive)
2091
+ );
2092
+ continue;
2093
+ }
2094
+ const sensitive =
2095
+ entry.sensitive ||
2096
+ (type === 'Attribute' && typeof node.name === 'string' && SECRET_LABEL_RE.test(node.name));
2097
+ for (const [key, value] of Object.entries(node))
2098
+ if (key !== 'parent' && value && typeof value === 'object')
2099
+ pending.push({ value, sensitive });
2100
+ }
2101
+ if (ast.css)
2102
+ edit(ast.css.start, ast.css.end, (raw) => {
2103
+ const clean = scrubPayload(raw);
2104
+ if (clean !== raw) faithful = false;
2105
+ return clean;
2106
+ });
2107
+ // Unparsed tags and boundaries cannot establish fidelity if policy changes them.
2108
+ edits.sort((a, b) => a.start - b.start);
2109
+ let offset = 0;
2110
+ let clean = '';
2111
+ for (const e of edits) {
2112
+ if (e.start < offset) return fallback();
2113
+ const gap = text.slice(offset, e.start);
2114
+ const cleanGap = scrubPayload(gap);
2115
+ if (gap !== cleanGap) faithful = false;
2116
+ clean += cleanGap + e.clean;
2117
+ offset = e.end;
2118
+ }
2119
+ const tail = text.slice(offset);
2120
+ const cleanTail = scrubPayload(tail);
2121
+ if (tail !== cleanTail) faithful = false;
2122
+ clean += cleanTail;
2123
+ parse(clean, { modern: true });
2124
+ return faithful ? clean : marker + clean;
2125
+ } catch {
2126
+ return fallback();
2127
+ } finally {
2128
+ parser?.delete();
2129
+ }
2130
+ }