docbridge 0.14.0 → 0.16.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.0] - 2026-10-12
11
+
12
+ ### Added
13
+
14
+ - A language entry under `include.code` accepts an optional `exclude` array.
15
+ Matched files that an `exclude` pattern selects are not managed, such as
16
+ Flutter's `*.g.dart` and `*.freezed.dart` files.
17
+
18
+ ## [0.15.0] - 2026-10-12
19
+
20
+ ### Added
21
+
22
+ - `doc_anchor_not_found` suggests the closest anchor in the same Markdown file
23
+ with `Did you mean`, for both `@doc` annotations and link manifest entries.
24
+ Anchors in other doc files are not suggested, and no suggestion is added when
25
+ no anchor is close enough.
26
+
10
27
  ## [0.14.0] - 2026-10-08
11
28
 
12
29
  ### Changed
@@ -568,7 +585,9 @@ Initial release of the SpecLink CLI.
568
585
  - `speclink check` command with `--root`, `--json`, and `--audit` options.
569
586
  - `speclink --version` (alias `-v`) and `speclink --help` (alias `-h`).
570
587
 
571
- [Unreleased]: https://github.com/salan70/docbridge/compare/v0.14.0...HEAD
588
+ [Unreleased]: https://github.com/salan70/docbridge/compare/v0.16.0...HEAD
589
+ [0.16.0]: https://github.com/salan70/docbridge/releases/tag/v0.16.0
590
+ [0.15.0]: https://github.com/salan70/docbridge/releases/tag/v0.15.0
572
591
  [0.14.0]: https://github.com/salan70/docbridge/releases/tag/v0.14.0
573
592
  [0.13.1]: https://github.com/salan70/docbridge/releases/tag/v0.13.1
574
593
  [0.13.0]: https://github.com/salan70/docbridge/releases/tag/v0.13.0
@@ -1,4 +1,4 @@
1
- 2db42ddea8060f988cea4933696e73d652ae5b807a8e8ff00249375faf2b0be4 docbridge-swift-scanner
2
- f3da86ea5a0c1c92fa1e1f077251bec490e294d257b09cd03a46c8ce382aad19 docbridge_dart_scanner
1
+ fbe811d3c84850751dca02c4b9ed6c71404d1b6e6666f4e0955054b12f4448e2 docbridge-swift-scanner
2
+ a49fb9474445a3d7d35ab332a1660c3ddd50dbe4ed11adb81c19d99568abb9fa docbridge_dart_scanner
3
3
  7fc3e6842b3e1bf425b8fd63e241eee7e64875f16be7ba8d1ae4a25baa7f2338 docbridge-rust-scanner
4
- 1646b06856b0420acdcb82d1692b7afea83fe2b7666adeb6412c99782dfc8cae docbridge-go-scanner
4
+ 81a1f97936f7b503131a1380dbcdbe610c5662ffa86284af754c7e6dfd792f21 docbridge-go-scanner
@@ -1,4 +1,4 @@
1
- 0c023cfacede26a217a62420f9da1206b93c228122a3685469685a3db89f7735 docbridge-swift-scanner
1
+ 86af421dafc4dbff3498893c7600e44e6bdb230177ae63a0639267ebba78ed94 docbridge-swift-scanner
2
2
  b75b14006ca792ed3ca8faf7ceee9e4d33c895f27dd416b640818cf8a4bdd9c8 docbridge_dart_scanner
3
3
  e0da7b8cc6ee23704f5fe8514cc309f0bb60380c77b55e3bc5ed113abd1f3478 docbridge-rust-scanner
4
- 5b8aee5b85a38679875c473b2dc32f021f022cba242785bb0fd94ee6fda0a6bf docbridge-go-scanner
4
+ c89df71f1762fa25da26de1ffbd511c9ebc967e088d9653115b944c7a9b02c00 docbridge-go-scanner
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/bin/linux-x64/docbridge-swift-scanner
3
3
  size: 40932352 bytes
4
- sha256: 0c023cfacede26a217a62420f9da1206b93c228122a3685469685a3db89f7735
4
+ sha256: 86af421dafc4dbff3498893c7600e44e6bdb230177ae63a0639267ebba78ed94
package/dist/index.js CHANGED
@@ -175581,7 +175581,7 @@ import { join as join15, resolve as resolve5 } from "node:path";
175581
175581
  // package.json
175582
175582
  var package_default = {
175583
175583
  name: "docbridge",
175584
- version: "0.14.0",
175584
+ version: "0.16.0",
175585
175585
  description: "A CLI that links Markdown specifications and code declarations. Runs on Node.js and Bun.",
175586
175586
  keywords: [
175587
175587
  "bun",
@@ -175829,7 +175829,7 @@ function collectCodeFiles(projectRoot, codeInclude) {
175829
175829
  if (entry === undefined) {
175830
175830
  continue;
175831
175831
  }
175832
- for (const relPath of collectLanguageFiles(projectRoot, language, entry.patterns)) {
175832
+ for (const relPath of collectLanguageFiles(projectRoot, language, entry)) {
175833
175833
  if (seen.has(relPath)) {
175834
175834
  continue;
175835
175835
  }
@@ -175847,7 +175847,7 @@ function codeFileOwners(projectRoot, codeInclude) {
175847
175847
  if (entry === undefined) {
175848
175848
  continue;
175849
175849
  }
175850
- for (const relPath of collectLanguageFiles(projectRoot, language, entry.patterns)) {
175850
+ for (const relPath of collectLanguageFiles(projectRoot, language, entry)) {
175851
175851
  const existing = owners.get(relPath);
175852
175852
  if (existing === undefined) {
175853
175853
  owners.set(relPath, [language]);
@@ -175858,8 +175858,9 @@ function codeFileOwners(projectRoot, codeInclude) {
175858
175858
  }
175859
175859
  return owners;
175860
175860
  }
175861
- function collectLanguageFiles(projectRoot, language, patterns) {
175862
- return collectFiles(projectRoot, patterns).filter((relPath) => !hasExcludedSuffix(language, relPath));
175861
+ function collectLanguageFiles(projectRoot, language, entry) {
175862
+ const excluded = entry.exclude ?? [];
175863
+ return collectFiles(projectRoot, entry.patterns).filter((relPath) => !hasExcludedSuffix(language, relPath) && !excluded.some((pattern) => matchGlob(pattern, relPath)));
175863
175864
  }
175864
175865
 
175865
175866
  // src/config/scanner-runtimes.ts
@@ -175918,7 +175919,7 @@ var EMPTY_CONFIG = {
175918
175919
  };
175919
175920
  var KNOWN_TOP_LEVEL_KEYS = new Set(["$schema", "include", "scanners"]);
175920
175921
  var KNOWN_INCLUDE_KEYS = new Set(["code", "docs"]);
175921
- var KNOWN_CODE_ENTRY_KEYS = new Set(["patterns", "visibility"]);
175922
+ var KNOWN_CODE_ENTRY_KEYS = new Set(["patterns", "exclude", "visibility"]);
175922
175923
  var LANGUAGE_VISIBILITY = {
175923
175924
  typescript: ["public", "protected", "private"],
175924
175925
  swift: ["public", "open", "internal"],
@@ -176047,6 +176048,9 @@ function validateCodeEntry(language, value, diagnostics) {
176047
176048
  }
176048
176049
  const before = diagnostics.length;
176049
176050
  validatePatternArray(value.patterns, `code.${language}.patterns`, LANGUAGE_SUFFIXES[language], EXCLUDED_SUFFIXES[language], diagnostics);
176051
+ if ("exclude" in value) {
176052
+ validateExcludePatterns(language, value.exclude, diagnostics);
176053
+ }
176050
176054
  if ("visibility" in value) {
176051
176055
  if (!isStringArray(value.visibility)) {
176052
176056
  diagnostics.push(configDiagnostic2("config_invalid_value", `${target}.visibility`, `\`${target}.visibility\` must be an array of strings.`));
@@ -176058,6 +176062,9 @@ function validateCodeEntry(language, value, diagnostics) {
176058
176062
  return;
176059
176063
  }
176060
176064
  const entry = { patterns: value.patterns };
176065
+ if (isStringArray(value.exclude)) {
176066
+ entry.exclude = value.exclude;
176067
+ }
176061
176068
  if (isStringArray(value.visibility)) {
176062
176069
  entry.visibility = value.visibility;
176063
176070
  }
@@ -176071,6 +176078,14 @@ function validateVisibilityOptions(language, values, target, diagnostics) {
176071
176078
  }
176072
176079
  }
176073
176080
  }
176081
+ function validateExcludePatterns(language, value, diagnostics) {
176082
+ const target = `include.code.${language}.exclude`;
176083
+ if (!Array.isArray(value)) {
176084
+ diagnostics.push(configDiagnostic2("config_invalid_value", target, `\`${target}\` must be an array of strings.`));
176085
+ return;
176086
+ }
176087
+ validatePatternStrings(value, `code.${language}.exclude`, LANGUAGE_SUFFIXES[language], EXCLUDED_SUFFIXES[language], diagnostics);
176088
+ }
176074
176089
  function validatePatternArray(value, field, suffixes, excludedSuffixes, diagnostics) {
176075
176090
  const target = `include.${field}`;
176076
176091
  if (!Array.isArray(value)) {
@@ -176081,6 +176096,10 @@ function validatePatternArray(value, field, suffixes, excludedSuffixes, diagnost
176081
176096
  diagnostics.push(configDiagnostic2("config_invalid_value", target, `\`${target}\` must contain at least one pattern.`));
176082
176097
  return;
176083
176098
  }
176099
+ validatePatternStrings(value, field, suffixes, excludedSuffixes, diagnostics);
176100
+ }
176101
+ function validatePatternStrings(value, field, suffixes, excludedSuffixes, diagnostics) {
176102
+ const target = `include.${field}`;
176084
176103
  for (const pattern of value) {
176085
176104
  if (typeof pattern !== "string") {
176086
176105
  diagnostics.push(configDiagnostic2("config_invalid_value", target, `\`${target}\` patterns must be strings.`));
@@ -177171,6 +177190,51 @@ function pluralize(word, count) {
177171
177190
  return count === 1 ? word : `${word}s`;
177172
177191
  }
177173
177192
 
177193
+ // src/shared/suggest.ts
177194
+ function nearestMatch(input, candidates) {
177195
+ const ranked = candidates.map((candidate, index) => ({
177196
+ candidate,
177197
+ distance: editDistance(input, candidate),
177198
+ index
177199
+ })).toSorted((left, right) => left.distance - right.distance || left.index - right.index);
177200
+ const abbreviation = ranked.find(({ candidate }) => isOrderedAbbreviation(input, candidate));
177201
+ if (abbreviation !== undefined) {
177202
+ return abbreviation.candidate;
177203
+ }
177204
+ const best = ranked[0];
177205
+ if (best === undefined) {
177206
+ return;
177207
+ }
177208
+ const closeEnough = best.distance <= Math.max(1, Math.floor(best.candidate.length / 2));
177209
+ return closeEnough ? best.candidate : undefined;
177210
+ }
177211
+ function isOrderedAbbreviation(input, candidate) {
177212
+ if (input.length < 3 || input.length >= candidate.length) {
177213
+ return false;
177214
+ }
177215
+ let candidateIndex = 0;
177216
+ for (const character of input) {
177217
+ const matchIndex = candidate.indexOf(character, candidateIndex);
177218
+ if (matchIndex === -1) {
177219
+ return false;
177220
+ }
177221
+ candidateIndex = matchIndex + 1;
177222
+ }
177223
+ return true;
177224
+ }
177225
+ function editDistance(left, right) {
177226
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
177227
+ for (let leftIndex = 1;leftIndex <= left.length; leftIndex += 1) {
177228
+ const current = [leftIndex];
177229
+ for (let rightIndex = 1;rightIndex <= right.length; rightIndex += 1) {
177230
+ const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
177231
+ current[rightIndex] = Math.min((current[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + substitutionCost);
177232
+ }
177233
+ previous = current;
177234
+ }
177235
+ return previous[right.length] ?? 0;
177236
+ }
177237
+
177174
177238
  // src/link/resolver.ts
177175
177239
  function resolveLinks(input) {
177176
177240
  const diagnostics = [];
@@ -177178,10 +177242,12 @@ function resolveLinks(input) {
177178
177242
  const docFilePaths = new Set(input.docFiles.map((file) => file.filePath));
177179
177243
  const codeFilePaths = new Set(input.codeFiles.map((file) => file.filePath));
177180
177244
  const docAnchorEndpoints = new Set;
177245
+ const docAnchorsByFile = new Map;
177181
177246
  for (const file of input.docFiles) {
177182
177247
  for (const anchor of file.anchors) {
177183
177248
  docAnchorEndpoints.add(anchor.endpoint);
177184
177249
  }
177250
+ docAnchorsByFile.set(file.filePath, file.anchors.map((anchor) => anchor.anchor));
177185
177251
  }
177186
177252
  const docToCodePairs = new Set;
177187
177253
  for (const file of input.docFiles) {
@@ -177216,7 +177282,7 @@ function resolveLinks(input) {
177216
177282
  continue;
177217
177283
  }
177218
177284
  if (!docAnchorEndpoints.has(docEndpoint)) {
177219
- diagnostics.push(relationshipDiagnostic("doc_anchor_not_found", link.source, docEndpoint, `Doc anchor ${docEndpoint} referenced by ${link.source} does not exist.`, link));
177285
+ diagnostics.push(relationshipDiagnostic("doc_anchor_not_found", link.source, docEndpoint, withAnchorSuggestion(`Doc anchor ${docEndpoint} referenced by ${link.source} does not exist.`, docEndpoint, docAnchorsByFile), link));
177220
177286
  continue;
177221
177287
  }
177222
177288
  if (!docToCodePairs.has(pairKey(docEndpoint, link.source))) {
@@ -177330,6 +177396,11 @@ function unlinkedDocSectionDiagnostic(node, anchor) {
177330
177396
  }
177331
177397
  return diagnostic;
177332
177398
  }
177399
+ function withAnchorSuggestion(message, docEndpoint, docAnchorsByFile) {
177400
+ const docFilePath = filePathOf(docEndpoint);
177401
+ const suggestion = nearestMatch(fragmentOf(docEndpoint), docAnchorsByFile.get(docFilePath) ?? []);
177402
+ return suggestion === undefined ? message : `${message} Did you mean \`${docFilePath}#${suggestion}\`?`;
177403
+ }
177333
177404
  function pairKey(source, target) {
177334
177405
  return `${source}->${target}`;
177335
177406
  }
@@ -177348,51 +177419,6 @@ function relationshipDiagnostic(code, source, target, message, link) {
177348
177419
  return diagnostic;
177349
177420
  }
177350
177421
 
177351
- // src/shared/suggest.ts
177352
- function nearestMatch(input, candidates) {
177353
- const ranked = candidates.map((candidate, index) => ({
177354
- candidate,
177355
- distance: editDistance(input, candidate),
177356
- index
177357
- })).toSorted((left, right) => left.distance - right.distance || left.index - right.index);
177358
- const abbreviation = ranked.find(({ candidate }) => isOrderedAbbreviation(input, candidate));
177359
- if (abbreviation !== undefined) {
177360
- return abbreviation.candidate;
177361
- }
177362
- const best = ranked[0];
177363
- if (best === undefined) {
177364
- return;
177365
- }
177366
- const closeEnough = best.distance <= Math.max(1, Math.floor(best.candidate.length / 2));
177367
- return closeEnough ? best.candidate : undefined;
177368
- }
177369
- function isOrderedAbbreviation(input, candidate) {
177370
- if (input.length < 3 || input.length >= candidate.length) {
177371
- return false;
177372
- }
177373
- let candidateIndex = 0;
177374
- for (const character of input) {
177375
- const matchIndex = candidate.indexOf(character, candidateIndex);
177376
- if (matchIndex === -1) {
177377
- return false;
177378
- }
177379
- candidateIndex = matchIndex + 1;
177380
- }
177381
- return true;
177382
- }
177383
- function editDistance(left, right) {
177384
- let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
177385
- for (let leftIndex = 1;leftIndex <= left.length; leftIndex += 1) {
177386
- const current = [leftIndex];
177387
- for (let rightIndex = 1;rightIndex <= right.length; rightIndex += 1) {
177388
- const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
177389
- current[rightIndex] = Math.min((current[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + substitutionCost);
177390
- }
177391
- previous = current;
177392
- }
177393
- return previous[right.length] ?? 0;
177394
- }
177395
-
177396
177422
  // src/link/manifest-apply.ts
177397
177423
  function applyLinkManifest(input) {
177398
177424
  if (input.manifest.entries.length === 0) {
@@ -177476,7 +177502,7 @@ class ApplyState {
177476
177502
  return false;
177477
177503
  }
177478
177504
  if (!state.anchors.has(entry.docEndpoint)) {
177479
- this.report("doc_anchor_not_found", entry.docEndpoint, `Doc anchor ${entry.docEndpoint} declared in the link manifest does not exist.`, entry, "doc");
177505
+ this.report("doc_anchor_not_found", entry.docEndpoint, this.docAnchorNotFoundMessage(entry, state), entry, "doc");
177480
177506
  return false;
177481
177507
  }
177482
177508
  this.markAnnotated(state, entry.docEndpoint);
@@ -177546,6 +177572,12 @@ class ApplyState {
177546
177572
  const suggestion = nearestMatch(entry.code.fragment, candidates);
177547
177573
  return suggestion === undefined ? base : `${base} Did you mean \`${suggestion}\`?`;
177548
177574
  }
177575
+ docAnchorNotFoundMessage(entry, state) {
177576
+ const base = `Doc anchor ${entry.docEndpoint} declared in the link manifest does not exist.`;
177577
+ const candidates = state.file.anchors.map((anchor) => anchor.anchor);
177578
+ const suggestion = nearestMatch(entry.doc.fragment, candidates);
177579
+ return suggestion === undefined ? base : `${base} Did you mean \`${entry.doc.filePath}#${suggestion}\`?`;
177580
+ }
177549
177581
  report(code, target, message, entry, side, language) {
177550
177582
  const range = side === "code" ? entry.codeRange : entry.docRange;
177551
177583
  const diagnostic = {
@@ -179805,7 +179837,7 @@ class Project {
179805
179837
  if (hasExcludedSuffix(language, relPath)) {
179806
179838
  continue;
179807
179839
  }
179808
- if (entry.patterns.some((pattern) => matchGlob(pattern, relPath))) {
179840
+ if (entry.patterns.some((pattern) => matchGlob(pattern, relPath)) && !(entry.exclude ?? []).some((pattern) => matchGlob(pattern, relPath))) {
179809
179841
  seen.add(relPath);
179810
179842
  all.push({ language, relPath });
179811
179843
  }
@@ -69,7 +69,9 @@ source file may match only one language.
69
69
  | `ruby` | `.rb` | `public`, `protected`, `private` | `public` | CRuby 3.3 or later |
70
70
  | `java` | `.java` | `public`, `protected`, `package`, `private` | `public` | JDK 17 or later |
71
71
 
72
- Patterns must end with one of the language's extensions. The npm package
72
+ Patterns must end with one of the language's extensions. Each language also
73
+ accepts an optional `exclude` array that removes matched files, such as generated
74
+ code; see [Excluded files](#excluded-files). The npm package
73
75
  bundles the Swift, Dart, Rust, and Go scanners for `darwin-arm64` and
74
76
  `linux-x64`; TypeScript, JavaScript, and Markdown need no scanner binary.
75
77
  Python, Ruby, and Java are scanned by workers in the package that run on the
@@ -136,9 +138,29 @@ DocBridge there.
136
138
 
137
139
  ## Excluded files
138
140
 
139
- The configuration has no `exclude` property and no glob negation. Narrow the
140
- positive include patterns when tests, fixtures, generated files, or general
141
- documentation should stay outside the graph:
141
+ Use `exclude` in a language entry to remove matched files that should not be
142
+ managed, such as generated code. Flutter's `build_runner` outputs `*.g.dart` and
143
+ `*.freezed.dart` are the usual case:
144
+
145
+ ```json
146
+ {
147
+ "include": {
148
+ "code": {
149
+ "dart": {
150
+ "patterns": ["lib/**/*.dart"],
151
+ "exclude": ["lib/**/*.g.dart", "lib/**/*.freezed.dart"]
152
+ }
153
+ },
154
+ "docs": ["docs/**/*.md"]
155
+ }
156
+ }
157
+ ```
158
+
159
+ Each `exclude` pattern follows the `patterns` rules and must end with the
160
+ language's extension. The configuration has no glob negation, so `exclude` is the
161
+ only way to remove files that `patterns` matches. Narrow the positive include
162
+ patterns when tests, fixtures, or general documentation should stay outside the
163
+ graph:
142
164
 
143
165
  ```json
144
166
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docbridge",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "A CLI that links Markdown specifications and code declarations. Runs on Node.js and Bun.",
5
5
  "keywords": [
6
6
  "bun",
@@ -78,6 +78,13 @@
78
78
  "pattern": "^(?!.*\\.d\\.[cm]?ts$).*\\.(ts|tsx|mts|cts)$"
79
79
  }
80
80
  },
81
+ "exclude": {
82
+ "type": "array",
83
+ "items": {
84
+ "type": "string",
85
+ "pattern": "^(?!.*\\.d\\.[cm]?ts$).*\\.(ts|tsx|mts|cts)$"
86
+ }
87
+ },
81
88
  "visibility": {
82
89
  "type": "array",
83
90
  "items": {
@@ -100,6 +107,13 @@
100
107
  "pattern": "\\.swift$"
101
108
  }
102
109
  },
110
+ "exclude": {
111
+ "type": "array",
112
+ "items": {
113
+ "type": "string",
114
+ "pattern": "\\.swift$"
115
+ }
116
+ },
103
117
  "visibility": {
104
118
  "type": "array",
105
119
  "items": {
@@ -122,6 +136,13 @@
122
136
  "pattern": "\\.dart$"
123
137
  }
124
138
  },
139
+ "exclude": {
140
+ "type": "array",
141
+ "items": {
142
+ "type": "string",
143
+ "pattern": "\\.dart$"
144
+ }
145
+ },
125
146
  "visibility": {
126
147
  "type": "array",
127
148
  "items": {
@@ -144,6 +165,13 @@
144
165
  "pattern": "\\.rs$"
145
166
  }
146
167
  },
168
+ "exclude": {
169
+ "type": "array",
170
+ "items": {
171
+ "type": "string",
172
+ "pattern": "\\.rs$"
173
+ }
174
+ },
147
175
  "visibility": {
148
176
  "type": "array",
149
177
  "items": {
@@ -166,6 +194,13 @@
166
194
  "pattern": "\\.go$"
167
195
  }
168
196
  },
197
+ "exclude": {
198
+ "type": "array",
199
+ "items": {
200
+ "type": "string",
201
+ "pattern": "\\.go$"
202
+ }
203
+ },
169
204
  "visibility": {
170
205
  "type": "array",
171
206
  "items": {
@@ -188,6 +223,13 @@
188
223
  "pattern": "\\.(js|jsx|mjs|cjs)$"
189
224
  }
190
225
  },
226
+ "exclude": {
227
+ "type": "array",
228
+ "items": {
229
+ "type": "string",
230
+ "pattern": "\\.(js|jsx|mjs|cjs)$"
231
+ }
232
+ },
191
233
  "visibility": {
192
234
  "type": "array",
193
235
  "items": {
@@ -210,6 +252,13 @@
210
252
  "pattern": "\\.py$"
211
253
  }
212
254
  },
255
+ "exclude": {
256
+ "type": "array",
257
+ "items": {
258
+ "type": "string",
259
+ "pattern": "\\.py$"
260
+ }
261
+ },
213
262
  "visibility": {
214
263
  "type": "array",
215
264
  "items": {
@@ -232,6 +281,13 @@
232
281
  "pattern": "\\.rb$"
233
282
  }
234
283
  },
284
+ "exclude": {
285
+ "type": "array",
286
+ "items": {
287
+ "type": "string",
288
+ "pattern": "\\.rb$"
289
+ }
290
+ },
235
291
  "visibility": {
236
292
  "type": "array",
237
293
  "items": {
@@ -254,6 +310,13 @@
254
310
  "pattern": "\\.java$"
255
311
  }
256
312
  },
313
+ "exclude": {
314
+ "type": "array",
315
+ "items": {
316
+ "type": "string",
317
+ "pattern": "\\.java$"
318
+ }
319
+ },
257
320
  "visibility": {
258
321
  "type": "array",
259
322
  "items": {