invar-tools 1.17.17__py3-none-any.whl → 1.17.19__py3-none-any.whl

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.
@@ -16,6 +16,7 @@
16
16
  import { ESLint } from 'eslint';
17
17
  import { resolve, dirname } from 'path';
18
18
  import { statSync, realpathSync } from 'fs';
19
+ import { spawnSync } from 'child_process';
19
20
  import { fileURLToPath } from 'url';
20
21
  import { createRequire } from 'module';
21
22
  import plugin from './index.js';
@@ -27,12 +28,49 @@ const require = createRequire(import.meta.url);
27
28
 
28
29
  function resolveTsParser(projectPath) {
29
30
  try {
30
- return require.resolve('@typescript-eslint/parser', { paths: [projectPath, __dirname] });
31
+ const tseslintEntry = require.resolve('typescript-eslint', { paths: [projectPath] });
32
+ if (tseslintEntry) {
33
+ const tseslintRoot = dirname(dirname(tseslintEntry));
34
+ return require.resolve('@typescript-eslint/parser', { paths: [tseslintRoot] });
35
+ }
36
+ }
37
+ catch {
38
+ }
39
+
40
+ try {
41
+ return require.resolve('@typescript-eslint/parser', { paths: [projectPath] });
42
+ }
43
+ catch {
44
+ }
45
+
46
+ try {
47
+ return require.resolve('@typescript-eslint/parser', { paths: [__dirname] });
31
48
  }
32
49
  catch {
33
50
  return null;
34
51
  }
35
52
  }
53
+ function _gitLsFiles(projectPath) {
54
+ const check = spawnSync('git', ['-C', projectPath, 'rev-parse', '--is-inside-work-tree'], {
55
+ encoding: 'utf8',
56
+ timeout: 2000,
57
+ });
58
+ if (check.status !== 0) {
59
+ return null;
60
+ }
61
+
62
+ const ls = spawnSync('git', ['-C', projectPath, 'ls-files', '-z', '--', '*.ts', '*.tsx'], {
63
+ encoding: 'utf8',
64
+ timeout: 15000,
65
+ });
66
+ if (ls.status !== 0 || !ls.stdout) {
67
+ return null;
68
+ }
69
+
70
+ const files = ls.stdout.split('\0').filter(Boolean);
71
+ return files.length > 0 ? files : null;
72
+ }
73
+
36
74
  function parseArgs(args) {
37
75
  const projectPath = args.find(arg => !arg.startsWith('--')) || '.';
38
76
  const configArg = args.find(arg => arg.startsWith('--config='));
@@ -107,20 +145,51 @@ async function main() {
107
145
  }
108
146
  const tsParser = resolveTsParser(projectPath);
109
147
  if (!tsParser) {
110
- console.error("ESLint failed: Failed to load parser '@typescript-eslint/parser'.");
111
- console.error("Install it in your project (recommended), or use npx-based ESLint.");
112
- console.error("Example: pnpm add -D @typescript-eslint/parser");
148
+ console.error("ESLint failed: Failed to load TypeScript parser.");
149
+ console.error("Install either 'typescript-eslint' or '@typescript-eslint/parser' in your project.");
150
+ process.exit(1);
151
+ }
152
+
153
+ let filesToLint;
154
+ let lintCwd = projectPath;
155
+ let globInputPaths = true;
156
+ try {
157
+ const stats = statSync(projectPath);
158
+ if (stats.isFile()) {
159
+ lintCwd = dirname(projectPath);
160
+ filesToLint = [projectPath];
161
+ globInputPaths = false;
162
+ }
163
+ else if (stats.isDirectory()) {
164
+ const gitFiles = _gitLsFiles(projectPath);
165
+ if (gitFiles) {
166
+ filesToLint = gitFiles;
167
+ globInputPaths = false;
168
+ }
169
+ else {
170
+ filesToLint = [
171
+ "**/*.ts",
172
+ "**/*.tsx",
173
+ ];
174
+ }
175
+ }
176
+ else {
177
+ console.error(`Error: Path is neither a file nor a directory: ${projectPath}`);
178
+ process.exit(1);
179
+ }
180
+ }
181
+ catch (error) {
182
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
183
+ console.error(`Error: Cannot access path: ${errorMessage}`);
113
184
  process.exit(1);
114
185
  }
115
186
 
116
- // Create ESLint instance with programmatic configuration
117
- // Use __dirname (where CLI is located) for module resolution
118
- // This allows ESLint to find embedded node_modules in site-packages
119
187
  const eslint = new ESLint({
120
- useEslintrc: false, // Don't load .eslintrc files
121
- cwd: projectPath, // Use project directory as working directory (fix for timeout issue)
122
- resolvePluginsRelativeTo: __dirname, // Resolve plugins from embedded location
188
+ useEslintrc: false,
189
+ cwd: lintCwd,
190
+ resolvePluginsRelativeTo: __dirname,
123
191
  errorOnUnmatchedPattern: false,
192
+ globInputPaths,
124
193
  baseConfig: {
125
194
  parser: tsParser,
126
195
  parserOptions: {
@@ -130,49 +199,23 @@ async function main() {
130
199
  plugins: ['@invar'],
131
200
  rules: selectedConfig.rules,
132
201
  ignorePatterns: [
133
- // Explicit ignores to prevent scanning generated/cached directories
134
202
  '**/node_modules/**',
135
203
  '**/.next/**',
136
204
  '**/dist/**',
137
205
  '**/build/**',
138
206
  '**/.cache/**',
139
207
  '**/coverage/**',
208
+ '**/.turbo/**',
209
+ '**/.vercel/**',
210
+ '**/playwright-report/**',
211
+ '**/test-results/**',
140
212
  ],
141
213
  },
142
214
  plugins: {
143
215
  '@invar': plugin, // Register plugin directly
144
216
  },
145
- }); // Type assertion for ESLint config complexity
146
- // Lint the project - detect if path is a file or directory
147
- // ESLint defaults to .js only, so we need glob patterns for .ts/.tsx
148
- let filesToLint;
149
- try {
150
- const stats = statSync(projectPath);
151
- // Note: Advisory check for optimization - TOCTOU race condition is acceptable
152
- // because ESLint will handle file system changes gracefully during actual linting
153
- if (stats.isFile()) {
154
- // Single file - lint it directly
155
- filesToLint = [projectPath];
156
- }
157
- else if (stats.isDirectory()) {
158
- // Directory - use relative glob patterns for TypeScript files
159
- // Note: Focus on TypeScript files as this is a TypeScript Guard tool
160
- // Use relative patterns (no projectPath prefix) since cwd is set to projectPath
161
- filesToLint = [
162
- "**/*.ts",
163
- "**/*.tsx",
164
- ];
165
- }
166
- else {
167
- console.error(`Error: Path is neither a file nor a directory: ${projectPath}`);
168
- process.exit(1);
169
- }
170
- }
171
- catch (error) {
172
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
173
- console.error(`Error: Cannot access path: ${errorMessage}`);
174
- process.exit(1);
175
- }
217
+ });
218
+
176
219
  const results = await eslint.lintFiles(filesToLint);
177
220
  // Output in standard ESLint JSON format (compatible with guard_ts.py)
178
221
  const formatter = await eslint.loadFormatter('json');
@@ -77,49 +77,58 @@ function matchesEnforcePattern(filePath, patterns) {
77
77
  function isZodType(typeAnnotation) {
78
78
  return ZOD_TYPE_PATTERNS.some(pattern => pattern.test(typeAnnotation));
79
79
  }
80
- function hasParseCall(body, paramName) {
80
+ function collectParseArgs(body, visitorKeys) {
81
+ const parsed = new Set();
81
82
  if (!body)
82
- return false;
83
- let found = false;
84
- const MAX_DEPTH = 50; // Prevent stack overflow on deeply nested types
85
- const visit = (node, depth = 0) => {
86
- if (found)
87
- return;
83
+ return parsed;
84
+
85
+ const MAX_DEPTH = 50;
86
+ const stack = [{ node: body, depth: 0 }];
87
+
88
+ while (stack.length > 0) {
89
+ const current = stack.pop();
90
+ if (!current)
91
+ continue;
92
+ const node = current.node;
93
+ const depth = current.depth;
94
+ if (!node || typeof node !== 'object')
95
+ continue;
88
96
  if (depth > MAX_DEPTH)
89
- return; // Depth limit to prevent stack overflow
97
+ continue;
98
+
90
99
  if (node.type === 'CallExpression') {
91
100
  const callee = node.callee;
92
- if (callee.type === 'MemberExpression') {
101
+ if (callee && callee.type === 'MemberExpression') {
93
102
  const property = callee.property;
94
- if (property.type === 'Identifier' &&
95
- (property.name === 'parse' || property.name === 'safeParse')) {
96
- // Check if argument is our param
97
- if (node.arguments.some(arg => arg.type === 'Identifier' && arg.name === paramName)) {
98
- found = true;
99
- return;
103
+ if (property && property.type === 'Identifier' && (property.name === 'parse' || property.name === 'safeParse')) {
104
+ for (const arg of node.arguments || []) {
105
+ if (arg && arg.type === 'Identifier') {
106
+ parsed.add(arg.name);
107
+ }
100
108
  }
101
109
  }
102
110
  }
103
111
  }
104
- // Recursively visit children with depth tracking
105
- for (const key of Object.keys(node)) {
112
+
113
+ const keys = (visitorKeys && node.type && visitorKeys[node.type]) || [];
114
+ for (const key of keys) {
106
115
  const value = node[key];
107
- if (value && typeof value === 'object') {
108
- if (Array.isArray(value)) {
109
- for (const item of value) {
110
- if (item && typeof item === 'object' && 'type' in item) {
111
- visit(item, depth + 1);
112
- }
116
+ if (!value)
117
+ continue;
118
+ if (Array.isArray(value)) {
119
+ for (const item of value) {
120
+ if (item && typeof item === 'object' && item.type) {
121
+ stack.push({ node: item, depth: depth + 1 });
113
122
  }
114
123
  }
115
- else if ('type' in value) {
116
- visit(value, depth + 1);
117
- }
124
+ }
125
+ else if (typeof value === 'object' && value.type) {
126
+ stack.push({ node: value, depth: depth + 1 });
118
127
  }
119
128
  }
120
- };
121
- visit(body);
122
- return found;
129
+ }
130
+
131
+ return parsed;
123
132
  }
124
133
  export const requireSchemaValidation = {
125
134
  meta: {
@@ -206,49 +215,54 @@ export const requireSchemaValidation = {
206
215
  }
207
216
  function checkFunction(node, params) {
208
217
  const functionName = getFunctionName(node);
209
- // Skip if shouldn't check based on mode
210
218
  if (!shouldCheck(functionName)) {
211
219
  return;
212
220
  }
221
+
213
222
  const body = 'body' in node ? node.body : null;
223
+ const zodParams = params.filter((p) => p.typeAnnotation && isZodType(p.typeAnnotation) && p.name && p.name !== '{...}' && p.name !== '[...]');
224
+ if (zodParams.length === 0) {
225
+ return;
226
+ }
227
+
228
+ const parsedArgs = collectParseArgs(body, sourceCode.visitorKeys);
214
229
  const isRiskFunction = isHighRiskFunction(functionName, filename);
215
- for (const param of params) {
216
- if (param.typeAnnotation && isZodType(param.typeAnnotation)) {
217
- if (!hasParseCall(body, param.name)) {
218
- // Extract schema name from type annotation (e.g., "z.infer<typeof UserSchema>" -> "UserSchema")
219
- const schemaMatch = param.typeAnnotation.match(/typeof\s+(\w+)/);
220
- const schemaName = schemaMatch ? schemaMatch[1] : 'Schema';
221
- const validatedVarName = `validated${param.name.charAt(0).toUpperCase()}${param.name.slice(1)}`;
222
- context.report({
223
- node: node,
224
- messageId: isRiskFunction ? 'missingValidationRisk' : 'missingValidation',
225
- data: {
226
- name: param.name,
227
- functionName: functionName,
228
- },
229
- suggest: [
230
- {
231
- messageId: 'addParseCall',
232
- data: { name: param.name },
233
- fix(fixer) {
234
- // Find the opening brace of the function body
235
- if (!body || body.type !== 'BlockStatement')
236
- return null;
237
- const blockBody = body;
238
- if (!blockBody.body || blockBody.body.length === 0)
239
- return null;
240
- const firstStatement = blockBody.body[0];
241
- // Detect indentation from the first statement
242
- const firstStatementStart = firstStatement.loc?.start.column ?? 2;
243
- const indent = ' '.repeat(firstStatementStart);
244
- const parseCode = `const ${validatedVarName} = ${schemaName}.parse(${param.name});\n${indent}`;
245
- return fixer.insertTextBefore(firstStatement, parseCode);
246
- },
247
- },
248
- ],
249
- });
250
- }
230
+
231
+ for (const param of zodParams) {
232
+ if (parsedArgs.has(param.name)) {
233
+ continue;
251
234
  }
235
+
236
+ const schemaMatch = param.typeAnnotation.match(/typeof\s+(\w+)/);
237
+ const schemaName = schemaMatch ? schemaMatch[1] : 'Schema';
238
+ const validatedVarName = `validated${param.name.charAt(0).toUpperCase()}${param.name.slice(1)}`;
239
+
240
+ context.report({
241
+ node: node,
242
+ messageId: isRiskFunction ? 'missingValidationRisk' : 'missingValidation',
243
+ data: {
244
+ name: param.name,
245
+ functionName: functionName,
246
+ },
247
+ suggest: [
248
+ {
249
+ messageId: 'addParseCall',
250
+ data: { name: param.name },
251
+ fix(fixer) {
252
+ if (!body || body.type !== 'BlockStatement')
253
+ return null;
254
+ const blockBody = body;
255
+ if (!blockBody.body || blockBody.body.length === 0)
256
+ return null;
257
+ const firstStatement = blockBody.body[0];
258
+ const firstStatementStart = firstStatement.loc?.start.column ?? 2;
259
+ const indent = ' '.repeat(firstStatementStart);
260
+ const parseCode = `const ${validatedVarName} = ${schemaName}.parse(${param.name});\n${indent}`;
261
+ return fixer.insertTextBefore(firstStatement, parseCode);
262
+ },
263
+ },
264
+ ],
265
+ });
252
266
  }
253
267
  }
254
268
  /**
@@ -374,6 +374,19 @@ def _check_tool_available(tool: str, check_args: list[str]) -> bool:
374
374
  # =============================================================================
375
375
 
376
376
 
377
+ def _is_invar_package_dir(package_dir: Path, package_name: str) -> bool:
378
+ package_json = package_dir / "package.json"
379
+ if not package_json.exists():
380
+ return False
381
+
382
+ try:
383
+ data = json.loads(package_json.read_text(encoding="utf-8"))
384
+ except (OSError, json.JSONDecodeError):
385
+ return False
386
+
387
+ return data.get("name") == f"@invar/{package_name}"
388
+
389
+
377
390
  # @shell_complexity: Path discovery with fallback logic
378
391
  def _get_invar_package_cmd(package_name: str, project_path: Path) -> list[str]:
379
392
  """Get command to run an @invar/* package.
@@ -395,11 +408,11 @@ def _get_invar_package_cmd(package_name: str, project_path: Path) -> list[str]:
395
408
  resolved_path = project_path.resolve()
396
409
 
397
410
  local_cli = resolved_path / "typescript" / "packages" / package_name / "dist" / "cli.js"
398
- if local_cli.exists():
411
+ if local_cli.exists() and _is_invar_package_dir(local_cli.parent.parent, package_name):
399
412
  return ["node", str(local_cli)]
400
413
 
401
414
  local_cli = resolved_path / "packages" / package_name / "dist" / "cli.js"
402
- if local_cli.exists():
415
+ if local_cli.exists() and _is_invar_package_dir(local_cli.parent.parent, package_name):
403
416
  return ["node", str(local_cli)]
404
417
 
405
418
  # Priority 2: Embedded tools (from pip install)
@@ -417,7 +430,7 @@ def _get_invar_package_cmd(package_name: str, project_path: Path) -> list[str]:
417
430
  check_path = resolved_path
418
431
  for _ in range(5): # Max 5 levels up
419
432
  candidate = check_path / f"typescript/packages/{package_name}/dist/cli.js"
420
- if candidate.exists():
433
+ if candidate.exists() and _is_invar_package_dir(candidate.parent.parent, package_name):
421
434
  return ["node", str(candidate)]
422
435
  parent = check_path.parent
423
436
  if parent == check_path:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invar-tools
3
- Version: 1.17.17
3
+ Version: 1.17.19
4
4
  Summary: AI-native software engineering tools with design-by-contract verification
5
5
  Project-URL: Homepage, https://github.com/tefx/invar
6
6
  Project-URL: Documentation, https://github.com/tefx/invar#readme
@@ -56,7 +56,7 @@ invar/node_tools/.gitignore,sha256=M2kz8Iw7Kzmi44mKo1r7_HOZMh79a7dFDdRrqXyaEhI,5
56
56
  invar/node_tools/MANIFEST,sha256=2Z2at-27MK8K7DSjOjjtR4faTbt6eCiKQuEfvP_lwH8,145
57
57
  invar/node_tools/__init__.py,sha256=HzILh3jtP28Lm2jZwss1SY65ECxbtw2J2uFpXQA6Y94,1740
58
58
  invar/node_tools/ts-query.js,sha256=fEc_f0JT_Mb18dEoA4_vJoazvd7Lqv_rsed4eHSAbCg,13303
59
- invar/node_tools/eslint-plugin/cli.js,sha256=KbqACF8NEB7RRfybDPNqSwjB2KYWI97stdEpkE36kZY,8042
59
+ invar/node_tools/eslint-plugin/cli.js,sha256=jgRHHHDRbyQiVbIWgmaxlsA9PlDvonWFEEYf9_XPaZ4,8481
60
60
  invar/node_tools/eslint-plugin/cli.js.map,sha256=AZt_DYAD6NnHGjR6oxXfGYaOHc6-vfDs0ENvYsbw5Ro,4489
61
61
  invar/node_tools/eslint-plugin/index.js,sha256=atrI37dn1yDznQrgKY-1xIhLnCdvJQTU53123ZI_9J4,5132
62
62
  invar/node_tools/eslint-plugin/index.js.map,sha256=MlFuNso9sZ4v9ggGJLyMq4Pt_JdEzP1OyouyCCki1wA,2344
@@ -2616,7 +2616,7 @@ invar/node_tools/eslint-plugin/rules/require-complete-validation.js,sha256=ZGeAE
2616
2616
  invar/node_tools/eslint-plugin/rules/require-complete-validation.js.map,sha256=UhEr4fql0TObHt3ya8DydqbFlig5CSDFKIIJZSf74rA,2497
2617
2617
  invar/node_tools/eslint-plugin/rules/require-jsdoc-example.js,sha256=Uql3FKdnTHFBMh_OkwGuOdMBzf4k94QHOaWo9frAcSM,4488
2618
2618
  invar/node_tools/eslint-plugin/rules/require-jsdoc-example.js.map,sha256=00cKhvu890f-8UXPZcEAaX44IjCxxw12E4l_gsi93JM,2960
2619
- invar/node_tools/eslint-plugin/rules/require-schema-validation.js,sha256=gdJaRr9-j8ZmUr8-u_MnafeQ4I92OoIq3RzUXeqrpBg,12372
2619
+ invar/node_tools/eslint-plugin/rules/require-schema-validation.js,sha256=6kRnMrTBuqeHmNOdBz78jZ0WBZgTmIUBoR_0ozWlK4k,12155
2620
2620
  invar/node_tools/eslint-plugin/rules/require-schema-validation.js.map,sha256=akTSZQq-OC6ZvwMklIBRaxsesJ9w6GHaPhRTJ_ihmaE,8211
2621
2621
  invar/node_tools/eslint-plugin/rules/shell-complexity.js,sha256=Bhc9i7ILYMNNAUvxf9l7PlwhC_Neb69Jl1nUNABu2jg,11260
2622
2622
  invar/node_tools/eslint-plugin/rules/shell-complexity.js.map,sha256=0oJoDxXpXg2VTrwGMa-UDZ6te15Sv9vq-LgwWjJB5Q4,7140
@@ -2697,7 +2697,7 @@ invar/shell/prove/__init__.py,sha256=ZqlbmyMFJf6yAle8634jFuPRv8wNvHps8loMlOJyf8A
2697
2697
  invar/shell/prove/accept.py,sha256=cnY_6jzU1EBnpLF8-zWUWcXiSXtCwxPsXEYXsSVPG38,3717
2698
2698
  invar/shell/prove/cache.py,sha256=jbNdrvfLjvK7S0iqugErqeabb4YIbQuwIlcSRyCKbcg,4105
2699
2699
  invar/shell/prove/crosshair.py,sha256=XhJDsQWIriX9SuqeflUYvJgp9gJTDH7I7Uka6zjNzZ0,16734
2700
- invar/shell/prove/guard_ts.py,sha256=mq6_bNLBosVlkHf8KQ3EaWUtLUkh0Jeggh-YKybvD2I,37363
2700
+ invar/shell/prove/guard_ts.py,sha256=8InEWFd6oqFxBUEaAdlqOvPi43p0VZKxJIrAaF0eReU,37936
2701
2701
  invar/shell/prove/hypothesis.py,sha256=QUclOOUg_VB6wbmHw8O2EPiL5qBOeBRqQeM04AVuLw0,9880
2702
2702
  invar/templates/CLAUDE.md.template,sha256=eaGU3SyRO_NEifw5b26k3srgQH4jyeujjCJ-HbM36_w,4913
2703
2703
  invar/templates/__init__.py,sha256=cb3ht8KPK5oBn5oG6HsTznujmo9WriJ_P--fVxJwycc,45
@@ -2778,10 +2778,10 @@ invar/templates/skills/invar-reflect/template.md,sha256=Rr5hvbllvmd8jSLf_0ZjyKt6
2778
2778
  invar/templates/skills/investigate/SKILL.md.jinja,sha256=cp6TBEixBYh1rLeeHOR1yqEnFqv1NZYePORMnavLkQI,3231
2779
2779
  invar/templates/skills/propose/SKILL.md.jinja,sha256=6BuKiCqO1AEu3VtzMHy1QWGqr_xqG9eJlhbsKT4jev4,3463
2780
2780
  invar/templates/skills/review/SKILL.md.jinja,sha256=ET5mbdSe_eKgJbi2LbgFC-z1aviKcHOBw7J5Q28fr4U,14105
2781
- invar_tools-1.17.17.dist-info/METADATA,sha256=8zrGXhNN7aKCrwqXggDn00ZYT10BSUzN2r89ruFtaqw,28596
2782
- invar_tools-1.17.17.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
- invar_tools-1.17.17.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
- invar_tools-1.17.17.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
- invar_tools-1.17.17.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
- invar_tools-1.17.17.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
- invar_tools-1.17.17.dist-info/RECORD,,
2781
+ invar_tools-1.17.19.dist-info/METADATA,sha256=ecuhzoJZslp81KIMpPROD2LH-owOo4p8viFpMB5uKbw,28596
2782
+ invar_tools-1.17.19.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
+ invar_tools-1.17.19.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
+ invar_tools-1.17.19.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
+ invar_tools-1.17.19.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
+ invar_tools-1.17.19.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
+ invar_tools-1.17.19.dist-info/RECORD,,