invar-tools 1.5.0__py3-none-any.whl → 1.6.0__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.
- invar/__init__.py +7 -1
- invar/core/entry_points.py +12 -10
- invar/core/formatter.py +15 -0
- invar/core/models.py +85 -0
- invar/shell/commands/guard.py +36 -9
- invar/shell/config.py +46 -0
- invar/shell/guard_output.py +10 -0
- invar/templates/CLAUDE.md.template +18 -10
- invar/templates/config/CLAUDE.md.jinja +51 -30
- invar/templates/config/context.md.jinja +14 -0
- invar/templates/protocol/INVAR.md +1 -0
- invar/templates/skills/develop/SKILL.md.jinja +2 -1
- {invar_tools-1.5.0.dist-info → invar_tools-1.6.0.dist-info}/METADATA +1 -1
- {invar_tools-1.5.0.dist-info → invar_tools-1.6.0.dist-info}/RECORD +19 -19
- {invar_tools-1.5.0.dist-info → invar_tools-1.6.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.5.0.dist-info → invar_tools-1.6.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.5.0.dist-info → invar_tools-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.5.0.dist-info → invar_tools-1.6.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.5.0.dist-info → invar_tools-1.6.0.dist-info}/licenses/NOTICE +0 -0
invar/__init__.py
CHANGED
|
@@ -8,7 +8,13 @@ This package provides development tools (guard, map, sig).
|
|
|
8
8
|
For runtime contracts only, use invar-runtime instead.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import importlib.metadata
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
__version__ = importlib.metadata.version("invar-tools")
|
|
15
|
+
except importlib.metadata.PackageNotFoundError:
|
|
16
|
+
__version__ = "0.0.0.dev" # Development mode fallback
|
|
17
|
+
|
|
12
18
|
__protocol_version__ = "5.0" # Protocol/spec version (separate from package version)
|
|
13
19
|
|
|
14
20
|
# Re-export from invar-runtime for backwards compatibility
|
invar/core/entry_points.py
CHANGED
|
@@ -100,30 +100,30 @@ def count_escape_hatches(source: str) -> int:
|
|
|
100
100
|
return len(extract_escape_hatches(source))
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
@post(lambda result: all(len(t) ==
|
|
104
|
-
def extract_escape_hatches(source: str) -> list[tuple[str, str]]:
|
|
103
|
+
@post(lambda result: all(len(t) == 3 for t in result)) # Returns (rule, reason, line) tuples
|
|
104
|
+
def extract_escape_hatches(source: str) -> list[tuple[str, str, int]]:
|
|
105
105
|
"""
|
|
106
|
-
Extract @invar:allow markers with their reasons (DX-33
|
|
106
|
+
Extract @invar:allow markers with their reasons and line numbers (DX-33, DX-66).
|
|
107
107
|
|
|
108
108
|
Uses tokenize to only match real comments, not strings/docstrings.
|
|
109
|
-
Returns list of (rule, reason) tuples for cross-file analysis.
|
|
109
|
+
Returns list of (rule, reason, line) tuples for cross-file analysis.
|
|
110
110
|
|
|
111
111
|
Examples:
|
|
112
112
|
>>> extract_escape_hatches("")
|
|
113
113
|
[]
|
|
114
114
|
>>> extract_escape_hatches("# @invar:allow shell_result: API boundary")
|
|
115
|
-
[('shell_result', 'API boundary')]
|
|
115
|
+
[('shell_result', 'API boundary', 1)]
|
|
116
116
|
>>> source = '''
|
|
117
117
|
... # @invar:allow rule1: same reason
|
|
118
118
|
... # @invar:allow rule2: different reason
|
|
119
119
|
... '''
|
|
120
120
|
>>> extract_escape_hatches(source)
|
|
121
|
-
[('rule1', 'same reason'), ('rule2', 'different reason')]
|
|
121
|
+
[('rule1', 'same reason', 2), ('rule2', 'different reason', 3)]
|
|
122
122
|
>>> # DX-33 Option C: Strings containing the pattern should NOT match
|
|
123
123
|
>>> extract_escape_hatches('suggestion = "# @invar:allow rule: reason"')
|
|
124
124
|
[]
|
|
125
125
|
"""
|
|
126
|
-
results: list[tuple[str, str]] = []
|
|
126
|
+
results: list[tuple[str, str, int]] = []
|
|
127
127
|
try:
|
|
128
128
|
# Use iterator-based readline to avoid io.StringIO (forbidden in Core)
|
|
129
129
|
lines = iter(source.splitlines(keepends=True))
|
|
@@ -132,10 +132,12 @@ def extract_escape_hatches(source: str) -> list[tuple[str, str]]:
|
|
|
132
132
|
if tok.type == tokenize.COMMENT:
|
|
133
133
|
match = INVAR_ALLOW_PATTERN.search(tok.string)
|
|
134
134
|
if match:
|
|
135
|
-
|
|
135
|
+
# DX-66: tok.start[0] is the 1-based line number
|
|
136
|
+
results.append((match.group(1), match.group(2), tok.start[0]))
|
|
136
137
|
except Exception:
|
|
137
|
-
# Fall back to regex if tokenization fails
|
|
138
|
-
|
|
138
|
+
# Fall back to regex if tokenization fails - can't get line numbers
|
|
139
|
+
# Return line 0 to indicate unknown position
|
|
140
|
+
return [(r, reason, 0) for r, reason in INVAR_ALLOW_PATTERN.findall(source)]
|
|
139
141
|
return results
|
|
140
142
|
|
|
141
143
|
|
invar/core/formatter.py
CHANGED
|
@@ -256,6 +256,21 @@ def format_guard_agent(report: GuardReport, combined_status: str | None = None)
|
|
|
256
256
|
if report.suggests > 0:
|
|
257
257
|
result["static"]["suggests"] = report.suggests
|
|
258
258
|
result["summary"]["suggests"] = report.suggests
|
|
259
|
+
# DX-66: Add escape hatch summary if any exist
|
|
260
|
+
if report.escape_hatches.count > 0:
|
|
261
|
+
result["escape_hatches"] = {
|
|
262
|
+
"count": report.escape_hatches.count,
|
|
263
|
+
"by_rule": report.escape_hatches.by_rule,
|
|
264
|
+
"details": [
|
|
265
|
+
{
|
|
266
|
+
"file": d.file,
|
|
267
|
+
"line": d.line,
|
|
268
|
+
"rule": d.rule,
|
|
269
|
+
"reason": d.reason,
|
|
270
|
+
}
|
|
271
|
+
for d in report.escape_hatches.details
|
|
272
|
+
],
|
|
273
|
+
}
|
|
259
274
|
return result
|
|
260
275
|
|
|
261
276
|
|
invar/core/models.py
CHANGED
|
@@ -83,6 +83,89 @@ class Violation(BaseModel):
|
|
|
83
83
|
suggestion: str | None = None
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
class EscapeHatchDetail(BaseModel):
|
|
87
|
+
"""
|
|
88
|
+
Detail of a single escape hatch (@invar:allow) marker (DX-66).
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
>>> d = EscapeHatchDetail(file="test.py", line=10, rule="shell_result", reason="API")
|
|
92
|
+
>>> d.line
|
|
93
|
+
10
|
|
94
|
+
>>> # line=0 is valid (fallback when line number unknown)
|
|
95
|
+
>>> d0 = EscapeHatchDetail(file="test.py", line=0, rule="test", reason="fallback")
|
|
96
|
+
>>> d0.line
|
|
97
|
+
0
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
file: str
|
|
101
|
+
line: int = Field(ge=0) # 0 = fallback when line number unknown
|
|
102
|
+
rule: str
|
|
103
|
+
reason: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class EscapeHatchSummary(BaseModel):
|
|
107
|
+
"""
|
|
108
|
+
Summary of escape hatches in the codebase (DX-66).
|
|
109
|
+
|
|
110
|
+
Provides visibility into @invar:allow usage for tracking technical debt.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
>>> summary = EscapeHatchSummary()
|
|
114
|
+
>>> summary.count
|
|
115
|
+
0
|
|
116
|
+
>>> summary.by_rule
|
|
117
|
+
{}
|
|
118
|
+
>>> detail = EscapeHatchDetail(file="test.py", line=10, rule="shell_result", reason="API boundary")
|
|
119
|
+
>>> summary.add(detail)
|
|
120
|
+
>>> summary.count
|
|
121
|
+
1
|
|
122
|
+
>>> summary.by_rule
|
|
123
|
+
{'shell_result': 1}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
details: list[EscapeHatchDetail] = Field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
@post(lambda result: result >= 0)
|
|
130
|
+
def count(self) -> int:
|
|
131
|
+
"""
|
|
132
|
+
Total number of escape hatches.
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
>>> EscapeHatchSummary().count
|
|
136
|
+
0
|
|
137
|
+
"""
|
|
138
|
+
return len(self.details)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
@post(lambda result: all(v >= 0 for v in result.values()))
|
|
142
|
+
def by_rule(self) -> dict[str, int]:
|
|
143
|
+
"""
|
|
144
|
+
Count of escape hatches grouped by rule.
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
>>> EscapeHatchSummary().by_rule
|
|
148
|
+
{}
|
|
149
|
+
"""
|
|
150
|
+
counts: dict[str, int] = {}
|
|
151
|
+
for detail in self.details:
|
|
152
|
+
counts[detail.rule] = counts.get(detail.rule, 0) + 1
|
|
153
|
+
return counts
|
|
154
|
+
|
|
155
|
+
@pre(lambda self, detail: bool(detail.rule) and bool(detail.file))
|
|
156
|
+
def add(self, detail: EscapeHatchDetail) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Add an escape hatch detail to the summary.
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
>>> s = EscapeHatchSummary()
|
|
162
|
+
>>> s.add(EscapeHatchDetail(file="t.py", line=1, rule="r", reason="x"))
|
|
163
|
+
>>> s.count
|
|
164
|
+
1
|
|
165
|
+
"""
|
|
166
|
+
self.details.append(detail)
|
|
167
|
+
|
|
168
|
+
|
|
86
169
|
class GuardReport(BaseModel):
|
|
87
170
|
"""Complete Guard report for a project."""
|
|
88
171
|
|
|
@@ -95,6 +178,8 @@ class GuardReport(BaseModel):
|
|
|
95
178
|
# P24: Contract coverage statistics (Core files only)
|
|
96
179
|
core_functions_total: int = 0
|
|
97
180
|
core_functions_with_contracts: int = 0
|
|
181
|
+
# DX-66: Escape hatch visibility
|
|
182
|
+
escape_hatches: EscapeHatchSummary = Field(default_factory=EscapeHatchSummary)
|
|
98
183
|
|
|
99
184
|
@pre(lambda self, violation: violation.rule and violation.severity) # Valid violation
|
|
100
185
|
def add_violation(self, violation: Violation) -> None:
|
invar/shell/commands/guard.py
CHANGED
|
@@ -25,7 +25,7 @@ from invar import __version__
|
|
|
25
25
|
from invar.core.models import GuardReport, RuleConfig
|
|
26
26
|
from invar.core.rules import check_all_rules
|
|
27
27
|
from invar.core.utils import get_exit_code
|
|
28
|
-
from invar.shell.config import load_config
|
|
28
|
+
from invar.shell.config import find_project_root, load_config
|
|
29
29
|
from invar.shell.fs import scan_project
|
|
30
30
|
from invar.shell.guard_output import output_agent, output_rich
|
|
31
31
|
|
|
@@ -56,13 +56,13 @@ def _count_core_functions(file_info) -> tuple[int, int]:
|
|
|
56
56
|
return (total, with_contracts)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
# @shell_complexity: Core orchestration - iterates files, handles failures, aggregates results
|
|
60
59
|
# @shell_complexity: Core orchestration - iterates files, handles failures, aggregates results
|
|
61
60
|
def _scan_and_check(
|
|
62
61
|
path: Path, config: RuleConfig, only_files: set[Path] | None = None
|
|
63
62
|
) -> Result[GuardReport, str]:
|
|
64
63
|
"""Scan project files and check against rules."""
|
|
65
64
|
from invar.core.entry_points import extract_escape_hatches
|
|
65
|
+
from invar.core.models import EscapeHatchDetail
|
|
66
66
|
from invar.core.review_trigger import check_duplicate_escape_reasons
|
|
67
67
|
from invar.core.shell_architecture import check_complexity_debt
|
|
68
68
|
|
|
@@ -80,10 +80,17 @@ def _scan_and_check(
|
|
|
80
80
|
report.update_coverage(total, with_contracts)
|
|
81
81
|
for violation in check_all_rules(file_info, config):
|
|
82
82
|
report.add_violation(violation)
|
|
83
|
-
# DX-33: Collect escape hatches for cross-file analysis
|
|
83
|
+
# DX-33 + DX-66: Collect escape hatches for cross-file analysis and visibility
|
|
84
84
|
if file_info.source:
|
|
85
|
-
for rule, reason in extract_escape_hatches(file_info.source):
|
|
85
|
+
for rule, reason, line in extract_escape_hatches(file_info.source):
|
|
86
86
|
all_escapes.append((file_info.path, rule, reason))
|
|
87
|
+
# DX-66: Add to escape hatch summary
|
|
88
|
+
report.escape_hatches.add(EscapeHatchDetail(
|
|
89
|
+
file=file_info.path,
|
|
90
|
+
line=line,
|
|
91
|
+
rule=rule,
|
|
92
|
+
reason=reason,
|
|
93
|
+
))
|
|
87
94
|
|
|
88
95
|
# DX-22: Check project-level complexity debt (Fix-or-Explain enforcement)
|
|
89
96
|
for debt_violation in check_complexity_debt(
|
|
@@ -102,7 +109,11 @@ def _scan_and_check(
|
|
|
102
109
|
@app.command()
|
|
103
110
|
def guard(
|
|
104
111
|
path: Path = typer.Argument(
|
|
105
|
-
Path(),
|
|
112
|
+
Path(),
|
|
113
|
+
help="Project directory or single Python file",
|
|
114
|
+
exists=True,
|
|
115
|
+
file_okay=True,
|
|
116
|
+
dir_okay=True,
|
|
106
117
|
),
|
|
107
118
|
strict: bool = typer.Option(False, "--strict", help="Treat warnings as errors"),
|
|
108
119
|
changed: bool = typer.Option(
|
|
@@ -157,6 +168,16 @@ def guard(
|
|
|
157
168
|
)
|
|
158
169
|
from invar.shell.testing import VerificationLevel
|
|
159
170
|
|
|
171
|
+
# DX-65: Handle single file mode
|
|
172
|
+
single_file_mode = path.is_file()
|
|
173
|
+
single_file: Path | None = None
|
|
174
|
+
if single_file_mode:
|
|
175
|
+
if path.suffix != ".py":
|
|
176
|
+
console.print(f"[red]Error:[/red] {path} is not a Python file")
|
|
177
|
+
raise typer.Exit(1)
|
|
178
|
+
single_file = path.resolve()
|
|
179
|
+
path = find_project_root(path)
|
|
180
|
+
|
|
160
181
|
# Load and configure
|
|
161
182
|
config_result = load_config(path)
|
|
162
183
|
if isinstance(config_result, Failure):
|
|
@@ -179,7 +200,9 @@ def guard(
|
|
|
179
200
|
format_contract_coverage_report,
|
|
180
201
|
)
|
|
181
202
|
|
|
182
|
-
|
|
203
|
+
# DX-65: Use single file path if in single file mode
|
|
204
|
+
coverage_path = single_file if single_file else path
|
|
205
|
+
coverage_result = calculate_contract_coverage(coverage_path, changed_only=changed)
|
|
183
206
|
if isinstance(coverage_result, Failure):
|
|
184
207
|
console.print(f"[red]Error:[/red] {coverage_result.failure()}")
|
|
185
208
|
raise typer.Exit(1)
|
|
@@ -194,10 +217,14 @@ def guard(
|
|
|
194
217
|
|
|
195
218
|
raise typer.Exit(0 if report_data.ready_for_build else 1)
|
|
196
219
|
|
|
197
|
-
# Handle --changed mode
|
|
220
|
+
# Handle --changed mode or single file mode (DX-65)
|
|
198
221
|
only_files: set[Path] | None = None
|
|
199
222
|
checked_files: list[Path] = []
|
|
200
|
-
if
|
|
223
|
+
if single_file:
|
|
224
|
+
# DX-65: Single file mode - only check the specified file
|
|
225
|
+
only_files = {single_file}
|
|
226
|
+
checked_files = [single_file]
|
|
227
|
+
elif changed:
|
|
201
228
|
changed_result = handle_changed_mode(path)
|
|
202
229
|
if isinstance(changed_result, Failure):
|
|
203
230
|
if changed_result.failure() == "NO_CHANGES":
|
|
@@ -379,7 +406,7 @@ def _show_verification_level(verification_level) -> None:
|
|
|
379
406
|
@app.command()
|
|
380
407
|
def version() -> None:
|
|
381
408
|
"""Show Invar version."""
|
|
382
|
-
console.print(f"invar {__version__}")
|
|
409
|
+
console.print(f"invar-tools {__version__}")
|
|
383
410
|
|
|
384
411
|
|
|
385
412
|
@app.command("map")
|
invar/shell/config.py
CHANGED
|
@@ -267,6 +267,52 @@ def _find_config_source(project_root: Path) -> Result[tuple[Path | None, ConfigS
|
|
|
267
267
|
return Failure(f"Failed to find config: {e}")
|
|
268
268
|
|
|
269
269
|
|
|
270
|
+
# @shell_complexity: Project root discovery requires checking multiple markers
|
|
271
|
+
def find_project_root(start_path: "Path") -> "Path": # noqa: UP037
|
|
272
|
+
"""
|
|
273
|
+
Find project root by walking up from start_path looking for config files.
|
|
274
|
+
|
|
275
|
+
Looks for (in order): pyproject.toml, invar.toml, .invar/, .git/
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
start_path: Starting path (file or directory)
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Project root directory (absolute path), or start_path's parent if no markers found
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
>>> from pathlib import Path
|
|
285
|
+
>>> import tempfile
|
|
286
|
+
>>> with tempfile.TemporaryDirectory() as tmpdir:
|
|
287
|
+
... root = Path(tmpdir).resolve()
|
|
288
|
+
... (root / "pyproject.toml").touch()
|
|
289
|
+
... subdir = root / "src" / "core"
|
|
290
|
+
... subdir.mkdir(parents=True)
|
|
291
|
+
... found = find_project_root(subdir / "file.py")
|
|
292
|
+
... found == root
|
|
293
|
+
True
|
|
294
|
+
"""
|
|
295
|
+
from pathlib import Path
|
|
296
|
+
|
|
297
|
+
current = Path(start_path).resolve() # Resolve to absolute path
|
|
298
|
+
if current.is_file():
|
|
299
|
+
current = current.parent
|
|
300
|
+
|
|
301
|
+
# Walk up looking for project markers
|
|
302
|
+
for parent in [current, *current.parents]:
|
|
303
|
+
if (parent / "pyproject.toml").exists():
|
|
304
|
+
return parent
|
|
305
|
+
if (parent / "invar.toml").exists():
|
|
306
|
+
return parent
|
|
307
|
+
if (parent / ".invar").is_dir():
|
|
308
|
+
return parent
|
|
309
|
+
if (parent / ".git").exists():
|
|
310
|
+
return parent
|
|
311
|
+
|
|
312
|
+
# Fallback to the starting directory
|
|
313
|
+
return current
|
|
314
|
+
|
|
315
|
+
|
|
270
316
|
def _read_toml(path: Path) -> Result[dict[str, Any], str]:
|
|
271
317
|
"""Read and parse a TOML file."""
|
|
272
318
|
try:
|
invar/shell/guard_output.py
CHANGED
|
@@ -209,6 +209,16 @@ def output_rich(
|
|
|
209
209
|
if issue_parts:
|
|
210
210
|
console.print(f"[dim]Issues: {', '.join(issue_parts)}[/dim]")
|
|
211
211
|
|
|
212
|
+
# DX-66: Escape hatch summary (only show if any exist)
|
|
213
|
+
if report.escape_hatches.count > 0:
|
|
214
|
+
escape_count = report.escape_hatches.count
|
|
215
|
+
by_rule = report.escape_hatches.by_rule
|
|
216
|
+
rule_parts = [f"{count} {rule}" for rule, count in sorted(by_rule.items())]
|
|
217
|
+
console.print(
|
|
218
|
+
f"\n[bold]Escape hatches:[/bold] {escape_count} "
|
|
219
|
+
f"({', '.join(rule_parts)})"
|
|
220
|
+
)
|
|
221
|
+
|
|
212
222
|
# Code Health display (only when guard passes)
|
|
213
223
|
if report.passed and report.files_checked > 0:
|
|
214
224
|
# Calculate health: 100% for 0 warnings, decreases by 5% per warning, min 50%
|
|
@@ -120,18 +120,26 @@ Guard triggers `review_suggested` for: security-sensitive files, escape hatches
|
|
|
120
120
|
|
|
121
121
|
## Workflow Routing (MANDATORY)
|
|
122
122
|
|
|
123
|
-
When user message contains these triggers, you MUST invoke the
|
|
123
|
+
When user message contains these triggers, you MUST use the **Skill tool** to invoke the skill:
|
|
124
124
|
|
|
125
|
-
| Trigger Words | Skill | Notes |
|
|
126
|
-
|
|
127
|
-
| "review", "review and fix" |
|
|
128
|
-
| "implement", "add", "fix", "update" |
|
|
129
|
-
| "why", "explain", "investigate" |
|
|
130
|
-
| "compare", "should we", "design" |
|
|
125
|
+
| Trigger Words | Skill Tool Call | Notes |
|
|
126
|
+
|---------------|-----------------|-------|
|
|
127
|
+
| "review", "review and fix" | `Skill(skill="review")` | Adversarial review with fix loop |
|
|
128
|
+
| "implement", "add", "fix", "update" | `Skill(skill="develop")` | Unless in review context |
|
|
129
|
+
| "why", "explain", "investigate" | `Skill(skill="investigate")` | Research mode, no code changes |
|
|
130
|
+
| "compare", "should we", "design" | `Skill(skill="propose")` | Decision facilitation |
|
|
131
|
+
|
|
132
|
+
**⚠️ CRITICAL: You must call the Skill tool, not just follow the workflow mentally.**
|
|
133
|
+
|
|
134
|
+
The Skill tool reads `.claude/skills/<skill>/SKILL.md` which contains:
|
|
135
|
+
- Detailed phase instructions (USBV breakdown)
|
|
136
|
+
- Error handling rules
|
|
137
|
+
- Timeout policies
|
|
138
|
+
- Incremental development patterns (DX-63)
|
|
131
139
|
|
|
132
140
|
**Violation check (before writing ANY code):**
|
|
133
|
-
- "
|
|
134
|
-
- "
|
|
141
|
+
- "Did I call `Skill(skill="...")`?"
|
|
142
|
+
- "Am I following the SKILL.md instructions?"
|
|
135
143
|
|
|
136
144
|
<!--/invar:managed-->
|
|
137
145
|
|
|
@@ -146,7 +154,7 @@ When user message contains these triggers, you MUST invoke the corresponding ski
|
|
|
146
154
|
<!-- ========================================================================
|
|
147
155
|
USER REGION - EDITABLE
|
|
148
156
|
Add your team conventions and project-specific rules below.
|
|
149
|
-
This section is preserved across invar update and sync
|
|
157
|
+
This section is preserved across `invar update` and `invar dev sync`.
|
|
150
158
|
======================================================================== -->
|
|
151
159
|
<!--/invar:user-->
|
|
152
160
|
|
|
@@ -12,6 +12,24 @@
|
|
|
12
12
|
| **Shell** | Returns `Result[T, E]` from `returns` library |
|
|
13
13
|
| **Flow** | USBV: Understand → Specify → Build → Validate |
|
|
14
14
|
|
|
15
|
+
### Contract Rules (CRITICAL)
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
# ❌ WRONG: Lambda must include ALL parameters
|
|
19
|
+
@pre(lambda x: x >= 0)
|
|
20
|
+
def calc(x: int, y: int = 0): ...
|
|
21
|
+
|
|
22
|
+
# ✅ CORRECT: Include defaults too
|
|
23
|
+
@pre(lambda x, y=0: x >= 0)
|
|
24
|
+
def calc(x: int, y: int = 0): ...
|
|
25
|
+
|
|
26
|
+
# ❌ WRONG: @post cannot access parameters
|
|
27
|
+
@post(lambda result: result > x) # 'x' not available!
|
|
28
|
+
|
|
29
|
+
# ✅ CORRECT: @post only sees 'result'
|
|
30
|
+
@post(lambda result: result >= 0)
|
|
31
|
+
```
|
|
32
|
+
|
|
15
33
|
<!--/invar:critical-->
|
|
16
34
|
|
|
17
35
|
<!--invar:managed version="{{ version }}"-->
|
|
@@ -19,28 +37,13 @@
|
|
|
19
37
|
|
|
20
38
|
> **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Check-In, USBV workflow, and Task Completion requirements.
|
|
21
39
|
|
|
22
|
-
## Check-In
|
|
40
|
+
## Check-In
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
> See [INVAR.md#check-in](./INVAR.md#check-in-required) for full protocol.
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
✓ Check-In: [project] | [branch] | [clean/dirty]
|
|
28
|
-
```
|
|
44
|
+
**Your first message MUST display:** `✓ Check-In: [project] | [branch] | [clean/dirty]`
|
|
29
45
|
|
|
30
|
-
Actions
|
|
31
|
-
1. Read `.invar/context.md` (Key Rules + Current State + Lessons Learned)
|
|
32
|
-
2. Show one-line status
|
|
33
|
-
|
|
34
|
-
Example:
|
|
35
|
-
```
|
|
36
|
-
✓ Check-In: MyProject | main | clean
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**Do NOT execute guard or map at Check-In.**
|
|
40
|
-
Guard is for VALIDATE phase and Final only.
|
|
41
|
-
|
|
42
|
-
This is your sign-in. The user sees it immediately.
|
|
43
|
-
No visible check-in = Session not started.
|
|
46
|
+
**Actions:** Read `.invar/context.md`, then show status. Do NOT run guard at Check-In.
|
|
44
47
|
|
|
45
48
|
---
|
|
46
49
|
|
|
@@ -79,6 +82,14 @@ src/{project}/
|
|
|
79
82
|
| Core | `@pre`/`@post` + doctests, pure (no I/O) |
|
|
80
83
|
| Shell | Returns `Result[T, E]` from `returns` library |
|
|
81
84
|
|
|
85
|
+
### Core vs Shell (Edge Cases)
|
|
86
|
+
|
|
87
|
+
- File/network/env vars → **Shell**
|
|
88
|
+
- `datetime.now()`, `random` → **Inject param** OR Shell
|
|
89
|
+
- Pure logic → **Core**
|
|
90
|
+
|
|
91
|
+
> Full decision tree: [INVAR.md#core-shell](./INVAR.md#decision-tree-core-vs-shell)
|
|
92
|
+
|
|
82
93
|
## Documentation Structure
|
|
83
94
|
|
|
84
95
|
| File | Owner | Edit? | Purpose |
|
|
@@ -89,6 +100,8 @@ src/{project}/
|
|
|
89
100
|
| .invar/project-additions.md | User | Yes | Project rules → injected into CLAUDE.md |
|
|
90
101
|
| .invar/examples/ | Invar | No | **Must read:** Core/Shell patterns, workflow |
|
|
91
102
|
|
|
103
|
+
> **Before writing code:** Check Task Router in `.invar/context.md`
|
|
104
|
+
|
|
92
105
|
## Visible Workflow (DX-30)
|
|
93
106
|
|
|
94
107
|
For complex tasks (3+ functions), show 3 checkpoints in TodoList:
|
|
@@ -159,18 +172,26 @@ Guard triggers `review_suggested` for: security-sensitive files, escape hatches
|
|
|
159
172
|
|
|
160
173
|
## Workflow Routing (MANDATORY)
|
|
161
174
|
|
|
162
|
-
When user message contains these triggers, you MUST invoke the
|
|
175
|
+
When user message contains these triggers, you MUST use the **Skill tool** to invoke the skill:
|
|
176
|
+
|
|
177
|
+
| Trigger Words | Skill Tool Call | Notes |
|
|
178
|
+
|---------------|-----------------|-------|
|
|
179
|
+
| "review", "review and fix" | `Skill(skill="review")` | Adversarial review with fix loop |
|
|
180
|
+
| "implement", "add", "fix", "update" | `Skill(skill="develop")` | Unless in review context |
|
|
181
|
+
| "why", "explain", "investigate" | `Skill(skill="investigate")` | Research mode, no code changes |
|
|
182
|
+
| "compare", "should we", "design" | `Skill(skill="propose")` | Decision facilitation |
|
|
183
|
+
|
|
184
|
+
**⚠️ CRITICAL: You must call the Skill tool, not just follow the workflow mentally.**
|
|
163
185
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
| "compare", "should we", "design" | `/propose` | Decision facilitation |
|
|
186
|
+
The Skill tool reads `.claude/skills/<skill>/SKILL.md` which contains:
|
|
187
|
+
- Detailed phase instructions (USBV breakdown)
|
|
188
|
+
- Error handling rules
|
|
189
|
+
- Timeout policies
|
|
190
|
+
- Incremental development patterns (DX-63)
|
|
170
191
|
|
|
171
192
|
**Violation check (before writing ANY code):**
|
|
172
|
-
- "
|
|
173
|
-
- "
|
|
193
|
+
- "Did I call `Skill(skill="...")`?"
|
|
194
|
+
- "Am I following the SKILL.md instructions?"
|
|
174
195
|
|
|
175
196
|
---
|
|
176
197
|
|
|
@@ -205,7 +226,7 @@ enters /review. Say "skip" to bypass.
|
|
|
205
226
|
<!--invar:project-->
|
|
206
227
|
<!-- ========================================================================
|
|
207
228
|
PROJECT REGION - INVAR PROJECT ONLY
|
|
208
|
-
This section is populated by .invar/project-additions.md via sync
|
|
229
|
+
This section is populated by .invar/project-additions.md via `invar dev sync`.
|
|
209
230
|
For other projects, this region remains empty.
|
|
210
231
|
======================================================================== -->
|
|
211
232
|
<!--/invar:project-->
|
|
@@ -214,7 +235,7 @@ enters /review. Say "skip" to bypass.
|
|
|
214
235
|
<!-- ========================================================================
|
|
215
236
|
USER REGION - EDITABLE
|
|
216
237
|
Add your team conventions and project-specific rules below.
|
|
217
|
-
This section is preserved across invar update and sync
|
|
238
|
+
This section is preserved across `invar update` and `invar dev sync`.
|
|
218
239
|
======================================================================== -->
|
|
219
240
|
<!--/invar:user-->
|
|
220
241
|
|
|
@@ -27,6 +27,20 @@
|
|
|
27
27
|
{% endif -%}
|
|
28
28
|
- Final must show: `✓ Final: guard PASS | ...`
|
|
29
29
|
|
|
30
|
+
## Task Router (DX-62)
|
|
31
|
+
|
|
32
|
+
<!-- Before writing code, check this table -->
|
|
33
|
+
|
|
34
|
+
| If you are about to... | STOP and read first |
|
|
35
|
+
|------------------------|---------------------|
|
|
36
|
+
| Write code in `core/` | `.invar/examples/contracts.py` |
|
|
37
|
+
| Write code in `shell/` | `.invar/examples/core_shell.py` |
|
|
38
|
+
| Add `@pre`/`@post` contracts | `.invar/examples/contracts.py` |
|
|
39
|
+
| Use functional patterns | `.invar/examples/functional.py` |
|
|
40
|
+
| Implement a feature | `.invar/examples/workflow.md` |
|
|
41
|
+
|
|
42
|
+
**Rule:** Match found above? Read the file BEFORE writing code.
|
|
43
|
+
|
|
30
44
|
## Self-Reminder
|
|
31
45
|
|
|
32
46
|
<!-- DX-54: AI should re-read this file periodically -->
|
|
@@ -267,6 +267,7 @@ invar guard # Full: static + doctests + CrossHair + Hypothesis
|
|
|
267
267
|
invar guard --static # Static only (quick debug, ~0.5s)
|
|
268
268
|
invar guard --changed # Modified files only
|
|
269
269
|
invar guard --coverage # Collect branch coverage
|
|
270
|
+
invar guard -c # Contract coverage only (DX-63)
|
|
270
271
|
invar sig <file> # Show contracts + signatures
|
|
271
272
|
invar map --top 10 # Most-referenced symbols
|
|
272
273
|
invar rules # List all rules with detection/hints (JSON)
|
|
@@ -17,7 +17,8 @@ _invar:
|
|
|
17
17
|
|
|
18
18
|
Before any workflow action:
|
|
19
19
|
1. Read `.invar/context.md` (especially Key Rules section)
|
|
20
|
-
2.
|
|
20
|
+
2. **Check Task Router** — read examples before coding in `core/` or `shell/`
|
|
21
|
+
3. Display routing announcement
|
|
21
22
|
|
|
22
23
|
### Routing Announcement
|
|
23
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invar-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
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
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
invar/__init__.py,sha256=
|
|
1
|
+
invar/__init__.py,sha256=HV5W2nywevBhAMgF7TIHdBoiFY4ETWVLBYAt_gZCPHU,1520
|
|
2
2
|
invar/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
invar/core/__init__.py,sha256=01TgQ2bqTFV4VFdksfqXYPa2WUqo-DpUWUkEcIUXFb4,218
|
|
4
4
|
invar/core/contracts.py,sha256=SOyF1KeJ6hrEwfQ09UzMt881OJKDXRbPTslKA6HzdKg,19085
|
|
5
|
-
invar/core/entry_points.py,sha256=
|
|
5
|
+
invar/core/entry_points.py,sha256=1p6GRGTp9kA9spNkGKidFLlzLPheh6JO2XFb68Cr0sE,12209
|
|
6
6
|
invar/core/extraction.py,sha256=mScqEMEEQdsd-Z0jx9g3scK6Z1vI9l-ESjggXPIWHZ4,6112
|
|
7
7
|
invar/core/format_specs.py,sha256=P299aRHFMXyow8STwsvaT6Bg2ALPs2wSy7SByiRZZ-A,5610
|
|
8
8
|
invar/core/format_strategies.py,sha256=LifL97JbsF8WEkVNmQpq2htyFUC3pW21myAjtRGpSxU,5774
|
|
9
|
-
invar/core/formatter.py,sha256=
|
|
9
|
+
invar/core/formatter.py,sha256=rCGZhMpl4dPLrztgKDkNtAvnv2vKfomyIHl_6fThuno,11293
|
|
10
10
|
invar/core/hypothesis_strategies.py,sha256=_MfjG7KxkmJvuPsczr_1JayR_YmiDzU2jJ8fQPoKGgs,16517
|
|
11
11
|
invar/core/inspect.py,sha256=l1knohwpLRHSNySPUjyeBHJusnU0vYiQGj4dMVgQZIo,4381
|
|
12
12
|
invar/core/lambda_helpers.py,sha256=Ap1y7N0wpgCgPHwrs2pd7zD9Qq4Ptfd2iTliprXIkME,6457
|
|
13
|
-
invar/core/models.py,sha256=
|
|
13
|
+
invar/core/models.py,sha256=1bbhLijXHSe-o5SXQhbJgq8_EqPOOgsGKIrnWRwwtYM,13200
|
|
14
14
|
invar/core/must_use.py,sha256=7HnnbT53lb4dOT-1mL64pz0JbQYytuw4eejNVe7iWKY,5496
|
|
15
15
|
invar/core/parser.py,sha256=ucVpGziVzUvbkXT1n_SgOrYdStDEcNBqLuRGqK3_M5g,9205
|
|
16
16
|
invar/core/postcondition_scope.py,sha256=ykjVNqZZ1zItBmI7ebgmLW5vFGE-vpaLRTvSgWaJMgM,5245
|
|
@@ -46,13 +46,13 @@ invar/mcp/__main__.py,sha256=ZcIT2U6xUyGOWucl4jq422BDE3lRLjqyxb9pFylRBdk,219
|
|
|
46
46
|
invar/mcp/server.py,sha256=ay-w2YfSa1kTmBFx3x3jEgmNRC3NFEW0EYuZRt7M39w,12244
|
|
47
47
|
invar/shell/__init__.py,sha256=FFw1mNbh_97PeKPcHIqQpQ7mw-JoIvyLM1yOdxLw5uk,204
|
|
48
48
|
invar/shell/claude_hooks.py,sha256=kxkdF2gwTWcGpglccDi6-8IN1zRwelDG6Lg1VPYQgyA,12912
|
|
49
|
-
invar/shell/config.py,sha256=
|
|
49
|
+
invar/shell/config.py,sha256=6-kbo6--SxfROXoyU-v7InSLR8f_U1Mar_xEOdCXFkY,17633
|
|
50
50
|
invar/shell/contract_coverage.py,sha256=2RiXC9RBV__cKLHu0KKOWRxEgYVQNNAPAdwBjYenNHQ,11780
|
|
51
51
|
invar/shell/coverage.py,sha256=m01o898IFIdBztEBQLwwL1Vt5PWrpUntO4lv4nWEkls,11344
|
|
52
52
|
invar/shell/fs.py,sha256=wVD7DPWsCIJXuTyY_pi-5_LS82mXRdn_grJCOLn9zpU,3699
|
|
53
53
|
invar/shell/git.py,sha256=s6RQxEDQuLrmK3mru88EoYP8__4hiFW8AozlcxmY47E,2784
|
|
54
54
|
invar/shell/guard_helpers.py,sha256=QeYgbW0lgUa9Z_RCjAMG7UJdiMzz5cW48Lb2u-qgQi8,15114
|
|
55
|
-
invar/shell/guard_output.py,sha256=
|
|
55
|
+
invar/shell/guard_output.py,sha256=v3gG5P-_47nIFo8eAMKwdA_hLf2KZ0cQ-45Z6JjKp4w,12520
|
|
56
56
|
invar/shell/mcp_config.py,sha256=-hC7Y5BGuVs285b6gBARk7ZyzVxHwPgXSyt_GoN0jfs,4580
|
|
57
57
|
invar/shell/mutation.py,sha256=Lfyk2b8j8-hxAq-iwAgQeOhr7Ci6c5tRF1TXe3CxQCs,8914
|
|
58
58
|
invar/shell/pattern_integration.py,sha256=pRcjfq3NvMW_tvQCnaXZnD1k5AVEWK8CYOE2jN6VTro,7842
|
|
@@ -62,7 +62,7 @@ invar/shell/template_engine.py,sha256=IzOiGsKVFo0lDUdtg27wMzIJJKToclv151RDZuDnHH
|
|
|
62
62
|
invar/shell/templates.py,sha256=l2En95E8jRVlojdQIqdZgRLVB43f_b1d_AJapKkozwA,15908
|
|
63
63
|
invar/shell/testing.py,sha256=rTNBH0Okh2qtG9ohSXOz487baQ2gXrWT3s_WECW3HJs,11143
|
|
64
64
|
invar/shell/commands/__init__.py,sha256=MEkKwVyjI9DmkvBpJcuumXo2Pg_FFkfEr-Rr3nrAt7A,284
|
|
65
|
-
invar/shell/commands/guard.py,sha256=
|
|
65
|
+
invar/shell/commands/guard.py,sha256=QJhL3vI_PzAPuGbi2GhxOdmHSIXtaoNuJxjgXTW3JRQ,21595
|
|
66
66
|
invar/shell/commands/hooks.py,sha256=W-SOnT4VQyUvXwipozkJwgEYfiOJGz7wksrbcdWegUg,2356
|
|
67
67
|
invar/shell/commands/init.py,sha256=UzHLfAP9ddIY32HrLd32nUveFcHnB0HlbMcIgtfob9Y,18297
|
|
68
68
|
invar/shell/commands/merge.py,sha256=nuvKo8m32-OL-SCQlS4SLKmOZxQ3qj-1nGCx1Pgzifw,8183
|
|
@@ -77,7 +77,7 @@ invar/shell/prove/accept.py,sha256=cnY_6jzU1EBnpLF8-zWUWcXiSXtCwxPsXEYXsSVPG38,3
|
|
|
77
77
|
invar/shell/prove/cache.py,sha256=jbNdrvfLjvK7S0iqugErqeabb4YIbQuwIlcSRyCKbcg,4105
|
|
78
78
|
invar/shell/prove/crosshair.py,sha256=4Z_iIYBlkp-I6FqSYZa89wWB09V4Ouw2PduYhTn6rfw,16525
|
|
79
79
|
invar/shell/prove/hypothesis.py,sha256=QUclOOUg_VB6wbmHw8O2EPiL5qBOeBRqQeM04AVuLw0,9880
|
|
80
|
-
invar/templates/CLAUDE.md.template,sha256=
|
|
80
|
+
invar/templates/CLAUDE.md.template,sha256=eaGU3SyRO_NEifw5b26k3srgQH4jyeujjCJ-HbM36_w,4913
|
|
81
81
|
invar/templates/__init__.py,sha256=cb3ht8KPK5oBn5oG6HsTznujmo9WriJ_P--fVxJwycc,45
|
|
82
82
|
invar/templates/aider.conf.yml.template,sha256=4xzSs3BXzFJvwdhnWbmzSY0yCbfx5oxqnV8ZjehqHBg,853
|
|
83
83
|
invar/templates/context.md.template,sha256=FKyI1ghpqcf4wftyv9-auIFHor8Nm8lETN45Ja-L8Og,2386
|
|
@@ -87,8 +87,8 @@ invar/templates/pre-commit-config.yaml.template,sha256=2qWY3E8iDUqi85jE_X7y0atE8
|
|
|
87
87
|
invar/templates/proposal.md.template,sha256=UP7SpQ7gk8jVlHGLQCSQ5c-kCj1DBQEz8M-vEStK77I,1573
|
|
88
88
|
invar/templates/commands/audit.md,sha256=eXBySlQrVyk054vYQWAZYzj-HgT2QXhpzziw6GlIeGM,4112
|
|
89
89
|
invar/templates/commands/guard.md,sha256=PyeAKfrmlXsgbrTDypQqXmTDKK1JHKhHEQrHqftA7X0,1177
|
|
90
|
-
invar/templates/config/CLAUDE.md.jinja,sha256=
|
|
91
|
-
invar/templates/config/context.md.jinja,sha256=
|
|
90
|
+
invar/templates/config/CLAUDE.md.jinja,sha256=VbtDWxn3H8qiE9-DV1hlG3DJ-GcBQU4ZiUHbFh6Bxxk,7814
|
|
91
|
+
invar/templates/config/context.md.jinja,sha256=_kJ8erEQNJMLDCKrv4BXWkO6OaGzE-zW9biCf7144aY,3103
|
|
92
92
|
invar/templates/config/pre-commit.yaml.jinja,sha256=Qflmii8hngHciSgfa8mIlg3-E3D4b0xflm0-Q-cWcCc,1752
|
|
93
93
|
invar/templates/examples/README.md,sha256=xMcJZ1KEcfLJi5Ope_4FIbqDWKK3mRleAgllvgeNT6I,572
|
|
94
94
|
invar/templates/examples/conftest.py,sha256=uKA4NR7nyZWeSzY0URdZtw5zCcJpU32jNcaSKrI1Mxc,152
|
|
@@ -100,15 +100,15 @@ invar/templates/hooks/PreToolUse.sh.jinja,sha256=D39PaT1eFSjz_Av16xK1atoBZbhLI8t
|
|
|
100
100
|
invar/templates/hooks/Stop.sh.jinja,sha256=3S6lLeAGIu5aPQVRz4jjFS9AfjCD9DdS_jagmkw-x8Q,960
|
|
101
101
|
invar/templates/hooks/UserPromptSubmit.sh.jinja,sha256=eAQqQ-XdOCyhLpF5_1r1z7C-Ej9GQ5Isqbu_2LAtsno,2302
|
|
102
102
|
invar/templates/hooks/__init__.py,sha256=RnnMoQA-8eqbr8Y_1Vu9B8h5vAz4C-vmo8wgdcGYrz0,43
|
|
103
|
-
invar/templates/protocol/INVAR.md,sha256=
|
|
104
|
-
invar/templates/skills/develop/SKILL.md.jinja,sha256=
|
|
103
|
+
invar/templates/protocol/INVAR.md,sha256=ppQhb_-R5YaXAqW1WDMOcXptx-CrAQI_xYxld7YljK8,9998
|
|
104
|
+
invar/templates/skills/develop/SKILL.md.jinja,sha256=3coPSZGh1-YKN9Xc_xcEkfEP3S0XiFMMGF0hJZEaAx8,10562
|
|
105
105
|
invar/templates/skills/investigate/SKILL.md.jinja,sha256=bOLdLMH5WUVBYOo4NpsfyvI6xx7I1lCNr_X-8bMe_kg,2744
|
|
106
106
|
invar/templates/skills/propose/SKILL.md.jinja,sha256=_iDLYN6-cfzA8n0_8sv-Dnpm1xq9IIpcDyM10mU2WUA,2420
|
|
107
107
|
invar/templates/skills/review/SKILL.md.jinja,sha256=e7HULz1jjLOlk2LYejQMk2F-cu7dDIwvh6lWNjx3j-Q,14123
|
|
108
|
-
invar_tools-1.
|
|
109
|
-
invar_tools-1.
|
|
110
|
-
invar_tools-1.
|
|
111
|
-
invar_tools-1.
|
|
112
|
-
invar_tools-1.
|
|
113
|
-
invar_tools-1.
|
|
114
|
-
invar_tools-1.
|
|
108
|
+
invar_tools-1.6.0.dist-info/METADATA,sha256=8_ISE8BAkc9wl7CmsPffsLT2mulMUhDAXF78v08ySPk,16964
|
|
109
|
+
invar_tools-1.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
110
|
+
invar_tools-1.6.0.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
|
|
111
|
+
invar_tools-1.6.0.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
|
|
112
|
+
invar_tools-1.6.0.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
|
|
113
|
+
invar_tools-1.6.0.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
|
|
114
|
+
invar_tools-1.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|