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 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
- __version__ = "1.0.0"
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
@@ -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) == 2 for t in result)) # Returns (rule, reason) tuples
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 Option E).
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
- results.append((match.group(1), match.group(2)))
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 (invalid syntax, non-printable chars, etc.)
138
- return INVAR_ALLOW_PATTERN.findall(source)
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:
@@ -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(), help="Project root directory", exists=True, file_okay=False, dir_okay=True
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
- coverage_result = calculate_contract_coverage(path, changed_only=changed)
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 changed:
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:
@@ -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 corresponding skill:
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" | `/review` | Adversarial review with fix loop |
128
- | "implement", "add", "fix", "update" | `/develop` | Unless in review context |
129
- | "why", "explain", "investigate" | `/investigate` | Research mode, no code changes |
130
- | "compare", "should we", "design" | `/propose` | Decision facilitation |
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
- - "Am I in a workflow?"
134
- - "Did I invoke the correct skill?"
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-self.
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 (DX-54)
40
+ ## Check-In
23
41
 
24
- Your first message MUST display:
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 corresponding skill:
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
- | Trigger Words | Skill | Notes |
165
- |---------------|-------|-------|
166
- | "review", "review and fix" | `/review` | Adversarial review with fix loop |
167
- | "implement", "add", "fix", "update" | `/develop` | Unless in review context |
168
- | "why", "explain", "investigate" | `/investigate` | Research mode, no code changes |
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
- - "Am I in a workflow?"
173
- - "Did I invoke the correct skill?"
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-self.
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-self.
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. Display routing announcement
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.5.0
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=Bzp8MpdH-uUNe6qY9ITv6LLOn0HUomtIToQ0zUS1mkg,1342
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=gB6h2O9gp9larxDWVA2IUclLadMF_H8eJErnscYm0GU,12012
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=YS7Ni5duz04S1DamsaAksqTBBqXzxUTvf6O6LsxjROE,10772
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=5R1w15O8klEMMC0g1zgds6nGFpE6uJFSRkIbXFLNOpo,10795
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=cixlq47h8HYa9Ku-JOo66KCUyrf59R0NX_Yb7A1fAv4,16134
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=WrbIjy9wZ3TVAP79PMYTPwthT-_9DwmnfS3lpfeaq2M,12101
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=eKDfOcksRM4Zg0eybi5u93cXJ42OBR3ts0um5tCB5lA,20590
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=p2xFzbZ8NluumH9c03GkAQaqSOvgrG5SqyKU9qXz2HY,4498
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=788jlWOcSOS7xOEpTFr9H7OKYvEMmyJnDQ-oE4AG8r0,6855
91
- invar/templates/config/context.md.jinja,sha256=eyjNwvtSyDCImOw6up4eqfE_ylYWv2HCHMQ1M1qJtGQ,2563
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=Yjwvs0PK9sDUtNExXtS2g7nRV2LcUV22Xa0fY_K-_-0,9940
104
- invar/templates/skills/develop/SKILL.md.jinja,sha256=09zKj8eTDi47boDzooEE5cBwnV3TiXy8k7Zfs9enrx4,10482
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.5.0.dist-info/METADATA,sha256=0hzwMUBWAjfGOQc9SbF-9YxZwoaNf4R6TLUbjlIU7po,16964
109
- invar_tools-1.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
110
- invar_tools-1.5.0.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
111
- invar_tools-1.5.0.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
112
- invar_tools-1.5.0.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
113
- invar_tools-1.5.0.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
114
- invar_tools-1.5.0.dist-info/RECORD,,
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,,