invar-tools 1.8.0__py3-none-any.whl → 1.11.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 +8 -0
- invar/core/doc_edit.py +187 -0
- invar/core/doc_parser.py +563 -0
- invar/core/language.py +88 -0
- invar/core/models.py +106 -0
- invar/core/patterns/detector.py +6 -1
- invar/core/patterns/p0_exhaustive.py +15 -3
- invar/core/patterns/p0_literal.py +15 -3
- invar/core/patterns/p0_newtype.py +15 -3
- invar/core/patterns/p0_nonempty.py +15 -3
- invar/core/patterns/p0_validation.py +15 -3
- invar/core/patterns/registry.py +5 -1
- invar/core/patterns/types.py +5 -1
- invar/core/property_gen.py +4 -0
- invar/core/rules.py +84 -18
- invar/core/sync_helpers.py +27 -1
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +310 -0
- invar/mcp/handlers.py +408 -0
- invar/mcp/server.py +288 -143
- invar/node_tools/MANIFEST +7 -0
- invar/node_tools/__init__.py +51 -0
- invar/node_tools/fc-runner/cli.js +77 -0
- invar/node_tools/quick-check/cli.js +28 -0
- invar/node_tools/ts-analyzer/cli.js +480 -0
- invar/shell/claude_hooks.py +35 -12
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +41 -1
- invar/shell/commands/init.py +154 -16
- invar/shell/commands/perception.py +157 -33
- invar/shell/commands/skill.py +187 -0
- invar/shell/commands/template_sync.py +65 -13
- invar/shell/commands/uninstall.py +60 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/doc_tools.py +459 -0
- invar/shell/fs.py +67 -13
- invar/shell/pi_hooks.py +6 -0
- invar/shell/prove/crosshair.py +3 -0
- invar/shell/prove/guard_ts.py +902 -0
- invar/shell/skill_manager.py +355 -0
- invar/shell/template_engine.py +28 -4
- invar/shell/templates.py +4 -4
- invar/templates/claude-md/python/critical-rules.md +33 -0
- invar/templates/claude-md/python/quick-reference.md +24 -0
- invar/templates/claude-md/typescript/critical-rules.md +40 -0
- invar/templates/claude-md/typescript/quick-reference.md +24 -0
- invar/templates/claude-md/universal/check-in.md +25 -0
- invar/templates/claude-md/universal/skills.md +73 -0
- invar/templates/claude-md/universal/workflow.md +55 -0
- invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
- invar/templates/config/AGENT.md.jinja +58 -0
- invar/templates/config/CLAUDE.md.jinja +16 -209
- invar/templates/config/context.md.jinja +19 -0
- invar/templates/examples/{README.md → python/README.md} +2 -0
- invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
- invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
- invar/templates/examples/python/core_shell.py +227 -0
- invar/templates/examples/python/functional.py +613 -0
- invar/templates/examples/typescript/README.md +31 -0
- invar/templates/examples/typescript/contracts.ts +163 -0
- invar/templates/examples/typescript/core_shell.ts +374 -0
- invar/templates/examples/typescript/functional.ts +601 -0
- invar/templates/examples/typescript/workflow.md +95 -0
- invar/templates/hooks/PostToolUse.sh.jinja +10 -1
- invar/templates/hooks/PreToolUse.sh.jinja +38 -0
- invar/templates/hooks/Stop.sh.jinja +1 -1
- invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
- invar/templates/hooks/pi/invar.ts.jinja +9 -0
- invar/templates/manifest.toml +7 -6
- invar/templates/onboard/assessment.md.jinja +214 -0
- invar/templates/onboard/patterns/python.md +347 -0
- invar/templates/onboard/patterns/typescript.md +452 -0
- invar/templates/onboard/roadmap.md.jinja +168 -0
- invar/templates/protocol/INVAR.md.jinja +51 -0
- invar/templates/protocol/python/architecture-examples.md +41 -0
- invar/templates/protocol/python/contracts-syntax.md +56 -0
- invar/templates/protocol/python/markers.md +44 -0
- invar/templates/protocol/python/tools.md +24 -0
- invar/templates/protocol/python/troubleshooting.md +38 -0
- invar/templates/protocol/typescript/architecture-examples.md +52 -0
- invar/templates/protocol/typescript/contracts-syntax.md +73 -0
- invar/templates/protocol/typescript/markers.md +48 -0
- invar/templates/protocol/typescript/tools.md +65 -0
- invar/templates/protocol/typescript/troubleshooting.md +104 -0
- invar/templates/protocol/universal/architecture.md +36 -0
- invar/templates/protocol/universal/completion.md +14 -0
- invar/templates/protocol/universal/contracts-concept.md +37 -0
- invar/templates/protocol/universal/header.md +17 -0
- invar/templates/protocol/universal/session.md +17 -0
- invar/templates/protocol/universal/six-laws.md +10 -0
- invar/templates/protocol/universal/usbv.md +14 -0
- invar/templates/protocol/universal/visible-workflow.md +25 -0
- invar/templates/skills/develop/SKILL.md.jinja +85 -3
- invar/templates/skills/extensions/_registry.yaml +93 -0
- invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
- invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
- invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
- invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
- invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
- invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
- invar/templates/skills/extensions/security/SKILL.md +382 -0
- invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
- invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
- invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
- invar/templates/skills/review/SKILL.md.jinja +220 -248
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/METADATA +336 -12
- invar_tools-1.11.0.dist-info/RECORD +178 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.8.0.dist-info/RECORD +0 -116
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/NOTICE +0 -0
invar/__init__.py
CHANGED
|
@@ -32,6 +32,7 @@ from invar_runtime import (
|
|
|
32
32
|
NoNone,
|
|
33
33
|
Percentage,
|
|
34
34
|
Positive,
|
|
35
|
+
RelationViolation,
|
|
35
36
|
ResourceWarning,
|
|
36
37
|
Sorted,
|
|
37
38
|
SortedNonEmpty,
|
|
@@ -42,8 +43,11 @@ from invar_runtime import (
|
|
|
42
43
|
must_use,
|
|
43
44
|
post,
|
|
44
45
|
pre,
|
|
46
|
+
relates,
|
|
47
|
+
relates_multi,
|
|
45
48
|
skip_property_test,
|
|
46
49
|
strategy,
|
|
50
|
+
to_post_contract,
|
|
47
51
|
)
|
|
48
52
|
|
|
49
53
|
__all__ = [
|
|
@@ -60,6 +64,7 @@ __all__ = [
|
|
|
60
64
|
"NonNegative",
|
|
61
65
|
"Percentage",
|
|
62
66
|
"Positive",
|
|
67
|
+
"RelationViolation",
|
|
63
68
|
"ResourceWarning",
|
|
64
69
|
"Sorted",
|
|
65
70
|
"SortedNonEmpty",
|
|
@@ -70,6 +75,9 @@ __all__ = [
|
|
|
70
75
|
"must_use",
|
|
71
76
|
"post",
|
|
72
77
|
"pre",
|
|
78
|
+
"relates",
|
|
79
|
+
"relates_multi",
|
|
73
80
|
"skip_property_test",
|
|
74
81
|
"strategy",
|
|
82
|
+
"to_post_contract",
|
|
75
83
|
]
|
invar/core/doc_edit.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown document editing functions.
|
|
3
|
+
|
|
4
|
+
DX-76 Phase A-2: Section-level document editing.
|
|
5
|
+
Core module - pure logic, no I/O.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Literal
|
|
11
|
+
|
|
12
|
+
from deal import pre
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from invar.core.doc_parser import Section
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pre(lambda source, section, new_content, keep_heading=True: section.line_start >= 1)
|
|
19
|
+
@pre(lambda source, section, new_content, keep_heading=True: section.line_end >= section.line_start)
|
|
20
|
+
@pre(lambda source, section, new_content, keep_heading=True: section.line_end <= len(source.split("\n")))
|
|
21
|
+
def replace_section(
|
|
22
|
+
source: str,
|
|
23
|
+
section: Section,
|
|
24
|
+
new_content: str,
|
|
25
|
+
keep_heading: bool = True,
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Replace a section's content with new content.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
source: Original document source
|
|
31
|
+
section: Section to replace
|
|
32
|
+
new_content: New content to insert
|
|
33
|
+
keep_heading: If True, preserve the original heading line
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Modified document source
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
>>> from invar.core.doc_parser import Section
|
|
40
|
+
>>> source = "# Title\\n\\nOld content\\n\\n# Next"
|
|
41
|
+
>>> section = Section("Title", "title", 1, 1, 3, 20, "title", [])
|
|
42
|
+
>>> result = replace_section(source, section, "New content")
|
|
43
|
+
>>> "# Title" in result
|
|
44
|
+
True
|
|
45
|
+
>>> "New content" in result
|
|
46
|
+
True
|
|
47
|
+
>>> "Old content" not in result
|
|
48
|
+
True
|
|
49
|
+
|
|
50
|
+
>>> # Without keeping heading
|
|
51
|
+
>>> result2 = replace_section(source, section, "# New Title\\n\\nNew content", keep_heading=False)
|
|
52
|
+
>>> "# New Title" in result2
|
|
53
|
+
True
|
|
54
|
+
"""
|
|
55
|
+
lines = source.split("\n")
|
|
56
|
+
|
|
57
|
+
# Calculate line indices (0-indexed)
|
|
58
|
+
if keep_heading:
|
|
59
|
+
# Keep the heading line, replace content after it
|
|
60
|
+
start_idx = section.line_start # Skip heading (0-indexed after heading)
|
|
61
|
+
end_idx = section.line_end # 1-indexed inclusive
|
|
62
|
+
else:
|
|
63
|
+
# Replace everything including heading
|
|
64
|
+
start_idx = section.line_start - 1 # 0-indexed
|
|
65
|
+
end_idx = section.line_end # 1-indexed inclusive
|
|
66
|
+
|
|
67
|
+
# Split new content into lines
|
|
68
|
+
new_lines = new_content.split("\n") if new_content else []
|
|
69
|
+
|
|
70
|
+
# Build result
|
|
71
|
+
result_lines = lines[:start_idx] + new_lines + lines[end_idx:]
|
|
72
|
+
|
|
73
|
+
return "\n".join(result_lines)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pre(lambda source, anchor, content, position="after": anchor.line_start >= 1)
|
|
77
|
+
@pre(lambda source, anchor, content, position="after": anchor.line_end <= len(source.split("\n")))
|
|
78
|
+
@pre(lambda source, anchor, content, position="after": position in ("before", "after", "first_child", "last_child"))
|
|
79
|
+
def insert_section(
|
|
80
|
+
source: str,
|
|
81
|
+
anchor: Section,
|
|
82
|
+
content: str,
|
|
83
|
+
position: Literal["before", "after", "first_child", "last_child"] = "after",
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Insert new content relative to an anchor section.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
source: Original document source
|
|
89
|
+
anchor: Reference section for insertion
|
|
90
|
+
content: Content to insert (should include heading if adding a section)
|
|
91
|
+
position: Where to insert relative to anchor:
|
|
92
|
+
- "before": Before the anchor section
|
|
93
|
+
- "after": After the anchor section (including children)
|
|
94
|
+
- "first_child": As first child of anchor
|
|
95
|
+
- "last_child": As last child of anchor
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Modified document source
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
>>> from invar.core.doc_parser import Section
|
|
102
|
+
>>> source = "# Title\\n\\nContent\\n\\n# Next"
|
|
103
|
+
>>> anchor = Section("Title", "title", 1, 1, 3, 20, "title", [])
|
|
104
|
+
>>> result = insert_section(source, anchor, "\\n## Subsection\\n\\nNew text", "after")
|
|
105
|
+
>>> "## Subsection" in result
|
|
106
|
+
True
|
|
107
|
+
|
|
108
|
+
>>> # Insert before
|
|
109
|
+
>>> result2 = insert_section(source, anchor, "# Preface\\n\\nIntro\\n", "before")
|
|
110
|
+
>>> result2.startswith("# Preface")
|
|
111
|
+
True
|
|
112
|
+
"""
|
|
113
|
+
lines = source.split("\n")
|
|
114
|
+
content_lines = content.split("\n") if content else []
|
|
115
|
+
|
|
116
|
+
if position == "before":
|
|
117
|
+
# Insert before anchor's line_start
|
|
118
|
+
insert_idx = anchor.line_start - 1 # 0-indexed
|
|
119
|
+
elif position == "after":
|
|
120
|
+
# Insert after anchor's line_end
|
|
121
|
+
insert_idx = anchor.line_end # After this line (0-indexed = line_end since it's 1-indexed)
|
|
122
|
+
elif position == "first_child":
|
|
123
|
+
# Insert right after the heading line
|
|
124
|
+
insert_idx = anchor.line_start # Right after heading (0-indexed)
|
|
125
|
+
else: # last_child
|
|
126
|
+
# Insert before the end of the section (before next sibling or EOF)
|
|
127
|
+
insert_idx = anchor.line_end # At the end of section
|
|
128
|
+
|
|
129
|
+
# Build result
|
|
130
|
+
result_lines = lines[:insert_idx] + content_lines + lines[insert_idx:]
|
|
131
|
+
|
|
132
|
+
return "\n".join(result_lines)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pre(lambda source, section, include_children=True: section.line_start >= 1)
|
|
136
|
+
@pre(lambda source, section, include_children=True: section.line_end >= section.line_start)
|
|
137
|
+
@pre(lambda source, section, include_children=True: section.line_end <= len(source.split("\n")))
|
|
138
|
+
def delete_section(source: str, section: Section, include_children: bool = True) -> str:
|
|
139
|
+
"""Delete a section from the document.
|
|
140
|
+
|
|
141
|
+
Removes content from line_start to line_end inclusive.
|
|
142
|
+
When include_children=False, preserves child sections.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
source: Original document source
|
|
146
|
+
section: Section to delete
|
|
147
|
+
include_children: If True, delete children too (default)
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Modified document source
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
>>> from invar.core.doc_parser import Section
|
|
154
|
+
>>> source = "# Keep\\n\\n# Delete\\n\\nContent\\n\\n# Also Keep"
|
|
155
|
+
>>> section = Section("Delete", "delete", 1, 3, 5, 20, "delete", [])
|
|
156
|
+
>>> result = delete_section(source, section)
|
|
157
|
+
>>> "# Keep" in result
|
|
158
|
+
True
|
|
159
|
+
>>> "# Delete" not in result
|
|
160
|
+
True
|
|
161
|
+
>>> "# Also Keep" in result
|
|
162
|
+
True
|
|
163
|
+
|
|
164
|
+
>>> # Without children - preserves child sections
|
|
165
|
+
>>> parent = Section("Parent", "parent", 1, 1, 4, 100, "parent", [
|
|
166
|
+
... Section("Child", "child", 2, 3, 4, 50, "parent/child", [])
|
|
167
|
+
... ])
|
|
168
|
+
>>> src = "# Parent\\nIntro\\n## Child\\nBody"
|
|
169
|
+
>>> result = delete_section(src, parent, include_children=False)
|
|
170
|
+
>>> "# Parent" not in result
|
|
171
|
+
True
|
|
172
|
+
>>> "## Child" in result
|
|
173
|
+
True
|
|
174
|
+
"""
|
|
175
|
+
lines = source.split("\n")
|
|
176
|
+
start_idx = section.line_start - 1
|
|
177
|
+
|
|
178
|
+
if include_children or not section.children:
|
|
179
|
+
end_idx = section.line_end
|
|
180
|
+
else:
|
|
181
|
+
# Stop before first child
|
|
182
|
+
first_child_line = section.children[0].line_start
|
|
183
|
+
end_idx = first_child_line - 1
|
|
184
|
+
|
|
185
|
+
result_lines = lines[:start_idx] + lines[end_idx:]
|
|
186
|
+
|
|
187
|
+
return "\n".join(result_lines)
|