invar-tools 1.8.0__py3-none-any.whl → 1.10.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.
Files changed (110) hide show
  1. invar/__init__.py +8 -0
  2. invar/core/language.py +88 -0
  3. invar/core/models.py +106 -0
  4. invar/core/patterns/detector.py +6 -1
  5. invar/core/patterns/p0_exhaustive.py +15 -3
  6. invar/core/patterns/p0_literal.py +15 -3
  7. invar/core/patterns/p0_newtype.py +15 -3
  8. invar/core/patterns/p0_nonempty.py +15 -3
  9. invar/core/patterns/p0_validation.py +15 -3
  10. invar/core/patterns/registry.py +5 -1
  11. invar/core/patterns/types.py +5 -1
  12. invar/core/property_gen.py +4 -0
  13. invar/core/rules.py +84 -18
  14. invar/core/sync_helpers.py +27 -1
  15. invar/core/ts_parsers.py +286 -0
  16. invar/core/ts_sig_parser.py +307 -0
  17. invar/node_tools/MANIFEST +7 -0
  18. invar/node_tools/__init__.py +51 -0
  19. invar/node_tools/fc-runner/cli.js +77 -0
  20. invar/node_tools/quick-check/cli.js +28 -0
  21. invar/node_tools/ts-analyzer/cli.js +480 -0
  22. invar/shell/claude_hooks.py +35 -12
  23. invar/shell/commands/guard.py +36 -1
  24. invar/shell/commands/init.py +82 -3
  25. invar/shell/commands/perception.py +157 -33
  26. invar/shell/commands/skill.py +187 -0
  27. invar/shell/commands/template_sync.py +65 -13
  28. invar/shell/commands/uninstall.py +60 -12
  29. invar/shell/commands/update.py +6 -14
  30. invar/shell/contract_coverage.py +1 -0
  31. invar/shell/fs.py +66 -13
  32. invar/shell/pi_hooks.py +6 -0
  33. invar/shell/prove/guard_ts.py +899 -0
  34. invar/shell/skill_manager.py +353 -0
  35. invar/shell/template_engine.py +28 -4
  36. invar/shell/templates.py +4 -4
  37. invar/templates/claude-md/python/critical-rules.md +33 -0
  38. invar/templates/claude-md/python/quick-reference.md +24 -0
  39. invar/templates/claude-md/typescript/critical-rules.md +40 -0
  40. invar/templates/claude-md/typescript/quick-reference.md +24 -0
  41. invar/templates/claude-md/universal/check-in.md +25 -0
  42. invar/templates/claude-md/universal/skills.md +73 -0
  43. invar/templates/claude-md/universal/workflow.md +55 -0
  44. invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
  45. invar/templates/config/AGENT.md.jinja +58 -0
  46. invar/templates/config/CLAUDE.md.jinja +16 -209
  47. invar/templates/config/context.md.jinja +19 -0
  48. invar/templates/examples/{README.md → python/README.md} +2 -0
  49. invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
  50. invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
  51. invar/templates/examples/python/core_shell.py +227 -0
  52. invar/templates/examples/python/functional.py +613 -0
  53. invar/templates/examples/typescript/README.md +31 -0
  54. invar/templates/examples/typescript/contracts.ts +163 -0
  55. invar/templates/examples/typescript/core_shell.ts +374 -0
  56. invar/templates/examples/typescript/functional.ts +601 -0
  57. invar/templates/examples/typescript/workflow.md +95 -0
  58. invar/templates/hooks/PostToolUse.sh.jinja +10 -1
  59. invar/templates/hooks/PreToolUse.sh.jinja +38 -0
  60. invar/templates/hooks/Stop.sh.jinja +1 -1
  61. invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
  62. invar/templates/hooks/pi/invar.ts.jinja +9 -0
  63. invar/templates/manifest.toml +7 -6
  64. invar/templates/onboard/assessment.md.jinja +214 -0
  65. invar/templates/onboard/patterns/python.md +347 -0
  66. invar/templates/onboard/patterns/typescript.md +452 -0
  67. invar/templates/onboard/roadmap.md.jinja +168 -0
  68. invar/templates/protocol/INVAR.md.jinja +51 -0
  69. invar/templates/protocol/python/architecture-examples.md +41 -0
  70. invar/templates/protocol/python/contracts-syntax.md +56 -0
  71. invar/templates/protocol/python/markers.md +44 -0
  72. invar/templates/protocol/python/tools.md +24 -0
  73. invar/templates/protocol/python/troubleshooting.md +38 -0
  74. invar/templates/protocol/typescript/architecture-examples.md +52 -0
  75. invar/templates/protocol/typescript/contracts-syntax.md +73 -0
  76. invar/templates/protocol/typescript/markers.md +48 -0
  77. invar/templates/protocol/typescript/tools.md +65 -0
  78. invar/templates/protocol/typescript/troubleshooting.md +104 -0
  79. invar/templates/protocol/universal/architecture.md +36 -0
  80. invar/templates/protocol/universal/completion.md +14 -0
  81. invar/templates/protocol/universal/contracts-concept.md +37 -0
  82. invar/templates/protocol/universal/header.md +17 -0
  83. invar/templates/protocol/universal/session.md +17 -0
  84. invar/templates/protocol/universal/six-laws.md +10 -0
  85. invar/templates/protocol/universal/usbv.md +14 -0
  86. invar/templates/protocol/universal/visible-workflow.md +25 -0
  87. invar/templates/skills/develop/SKILL.md.jinja +39 -3
  88. invar/templates/skills/extensions/_registry.yaml +93 -0
  89. invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
  90. invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
  91. invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
  92. invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
  93. invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
  94. invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
  95. invar/templates/skills/extensions/security/SKILL.md +382 -0
  96. invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
  97. invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
  98. invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
  99. invar/templates/skills/review/SKILL.md.jinja +331 -71
  100. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/METADATA +304 -12
  101. invar_tools-1.10.0.dist-info/RECORD +173 -0
  102. invar/templates/examples/core_shell.py +0 -127
  103. invar/templates/protocol/INVAR.md +0 -310
  104. invar_tools-1.8.0.dist-info/RECORD +0 -116
  105. /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
  106. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/WHEEL +0 -0
  107. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/entry_points.txt +0 -0
  108. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE +0 -0
  109. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE-GPL +0 -0
  110. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,613 @@
1
+ """
2
+ Invar Functional Pattern Examples (DX-61)
3
+
4
+ Reference patterns for higher-quality code. These are SUGGESTIONS, not requirements.
5
+ Guard will suggest them when it detects opportunities for improvement.
6
+
7
+ Patterns covered:
8
+ P0 (Core):
9
+ 1. NewType - Semantic clarity for primitive types
10
+ 2. Validation - Error accumulation instead of fail-fast
11
+ 3. NonEmpty - Compile-time safety for non-empty collections
12
+ 4. Literal - Type-safe finite value sets
13
+ 5. ExhaustiveMatch - Catch missing cases at compile time
14
+
15
+ P1 (Extended):
16
+ 6. SmartConstructor - Validation at construction time
17
+ 7. StructuredError - Typed errors for programmatic handling
18
+
19
+ Managed by Invar - do not edit directly.
20
+ """
21
+ # @invar:allow missing_contract: Educational file with intentional "bad" examples
22
+ # @invar:allow partial_contract: Educational file with intentional "bad" examples
23
+ # @invar:allow contract_quality_ratio: Educational file - coverage intentionally low
24
+ # @invar:allow file_size: Educational file with comprehensive pattern examples
25
+ # @invar:allow internal_import: Demo functions show self-contained examples
26
+
27
+ import json
28
+ from dataclasses import dataclass
29
+ from enum import Enum
30
+ from typing import Generic, Literal, NewType, TypeVar, assert_never
31
+
32
+ from invar_runtime import post, pre
33
+ from returns.result import Failure, Result, Success
34
+
35
+ T = TypeVar("T")
36
+
37
+
38
+ # =============================================================================
39
+ # Pattern 1: NewType for Semantic Clarity (P0)
40
+ # =============================================================================
41
+
42
+ # BEFORE: Easy to confuse parameters - all are just "str"
43
+ # def find_symbol_bad(
44
+ # module_path: str,
45
+ # symbol_name: str,
46
+ # file_pattern: str,
47
+ # ) -> Symbol:
48
+ # ...
49
+
50
+ # AFTER: Self-documenting, type-checker catches mistakes
51
+ ModulePath = NewType("ModulePath", str)
52
+ SymbolName = NewType("SymbolName", str)
53
+ FilePattern = NewType("FilePattern", str)
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class Symbol:
58
+ """Example symbol for demonstration."""
59
+
60
+ name: str
61
+ line: int
62
+
63
+
64
+ @pre(lambda path, name: len(path) > 0 and len(name) > 0)
65
+ @post(lambda result: result is not None)
66
+ def find_symbol(path: ModulePath, name: SymbolName) -> Symbol:
67
+ """
68
+ Find symbol by module path and name.
69
+
70
+ With NewType, swapping arguments is a type error:
71
+ find_symbol(name, path) # Type checker catches this!
72
+
73
+ >>> find_symbol(ModulePath("src/core"), SymbolName("calculate"))
74
+ Symbol(name='calculate', line=42)
75
+ """
76
+ # Demo implementation
77
+ return Symbol(name=name, line=42)
78
+
79
+
80
+ # =============================================================================
81
+ # Pattern 2: Validation for Error Accumulation (P0)
82
+ # =============================================================================
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class Config:
87
+ """Example config for validation demo."""
88
+
89
+ path: str
90
+ max_lines: int
91
+ enabled: bool
92
+
93
+
94
+ # BEFORE: User sees one error at a time
95
+ def validate_config_bad(data: dict) -> Result[Config, str]:
96
+ """
97
+ Bad: Fail-fast validation.
98
+
99
+ If multiple fields are invalid, user only sees the first error.
100
+ They fix it, run again, see the next error. Frustrating!
101
+ """
102
+ if "path" not in data:
103
+ return Failure("Missing 'path'")
104
+ if "max_lines" not in data:
105
+ return Failure("Missing 'max_lines'") # Never reached if path missing
106
+ if "enabled" not in data:
107
+ return Failure("Missing 'enabled'")
108
+ return Success(Config(**data))
109
+
110
+
111
+ # AFTER: User sees all errors at once
112
+ def validate_config_good(data: dict) -> Result[Config, list[str]]:
113
+ """
114
+ Good: Accumulating validation.
115
+
116
+ Collect all errors, return them together. User can fix everything
117
+ in one iteration. Much better UX!
118
+
119
+ >>> validate_config_good({})
120
+ Failure(["Missing 'path'", "Missing 'max_lines'", "Missing 'enabled'"])
121
+
122
+ >>> validate_config_good({"path": "/tmp", "max_lines": 100, "enabled": True})
123
+ Success(Config(path='/tmp', max_lines=100, enabled=True))
124
+
125
+ >>> result = validate_config_good({"path": ""})
126
+ >>> isinstance(result, Failure)
127
+ True
128
+ >>> "path cannot be empty" in result.failure()
129
+ True
130
+ """
131
+ errors: list[str] = []
132
+
133
+ # Collect ALL errors, don't return early
134
+ if "path" not in data:
135
+ errors.append("Missing 'path'")
136
+ elif not data["path"]:
137
+ errors.append("path cannot be empty")
138
+
139
+ if "max_lines" not in data:
140
+ errors.append("Missing 'max_lines'")
141
+ elif not isinstance(data["max_lines"], int):
142
+ errors.append("max_lines must be an integer")
143
+ elif data["max_lines"] < 0:
144
+ errors.append("max_lines must be >= 0")
145
+
146
+ if "enabled" not in data:
147
+ errors.append("Missing 'enabled'")
148
+
149
+ if errors:
150
+ return Failure(errors)
151
+
152
+ return Success(
153
+ Config(
154
+ path=data["path"],
155
+ max_lines=data["max_lines"],
156
+ enabled=data["enabled"],
157
+ )
158
+ )
159
+
160
+
161
+ # =============================================================================
162
+ # Pattern 3: NonEmpty for Compile-Time Safety (P0)
163
+ # =============================================================================
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class NonEmpty(Generic[T]):
168
+ """
169
+ List guaranteed to have at least one element.
170
+
171
+ Instead of runtime checks like `if not items: raise`,
172
+ use the type system to guarantee non-emptiness.
173
+
174
+ >>> ne = NonEmpty.from_list([1, 2, 3])
175
+ >>> isinstance(ne, Success)
176
+ True
177
+ >>> ne.unwrap().first
178
+ 1
179
+ """
180
+
181
+ head: T
182
+ tail: tuple[T, ...]
183
+
184
+ @property
185
+ def first(self) -> T:
186
+ """Always safe - guaranteed non-empty by construction."""
187
+ return self.head
188
+
189
+ @property
190
+ def all(self) -> tuple[T, ...]:
191
+ """Get all elements as tuple."""
192
+ return (self.head, *self.tail)
193
+
194
+ def __len__(self) -> int:
195
+ """Length is always >= 1."""
196
+ return 1 + len(self.tail)
197
+
198
+ @classmethod
199
+ def from_list(cls, items: list[T]) -> Result["NonEmpty[T]", str]:
200
+ """
201
+ Safely construct from list.
202
+
203
+ >>> NonEmpty.from_list([])
204
+ Failure('Cannot create NonEmpty from empty list')
205
+
206
+ >>> NonEmpty.from_list([1, 2, 3])
207
+ Success(NonEmpty(head=1, tail=(2, 3)))
208
+ """
209
+ if not items:
210
+ return Failure("Cannot create NonEmpty from empty list")
211
+ return Success(cls(head=items[0], tail=tuple(items[1:])))
212
+
213
+
214
+ # BEFORE: Defensive runtime check
215
+ def summarize_bad(items: list[str]) -> str:
216
+ """
217
+ Bad: Runtime check that could be compile-time.
218
+
219
+ The `if not items` check is defensive but the type system
220
+ can't help prevent calling with empty list.
221
+ """
222
+ if not items:
223
+ raise ValueError("Cannot summarize empty list")
224
+ return f"First: {items[0]}, Total: {len(items)}"
225
+
226
+
227
+ # AFTER: Type-safe, no check needed
228
+ @post(lambda result: "First:" in result)
229
+ def summarize_good(items: NonEmpty[str]) -> str:
230
+ """
231
+ Good: Type guarantees non-empty.
232
+
233
+ No runtime check needed - if you have a NonEmpty,
234
+ it's guaranteed to have at least one element.
235
+
236
+ >>> ne = NonEmpty.from_list(["a", "b", "c"]).unwrap()
237
+ >>> summarize_good(ne)
238
+ 'First: a, Total: 3'
239
+ """
240
+ return f"First: {items.first}, Total: {len(items)}"
241
+
242
+
243
+ # =============================================================================
244
+ # Pattern 4: Literal for Finite Value Sets (P0)
245
+ # =============================================================================
246
+
247
+ # BEFORE: Runtime validation for finite set
248
+ def set_log_level_bad(level: str) -> None:
249
+ """
250
+ Bad: Runtime check for finite values.
251
+
252
+ Type checker can't know that only certain strings are valid.
253
+ Invalid values discovered at runtime.
254
+ """
255
+ if level not in ("debug", "info", "warning", "error"):
256
+ raise ValueError(f"Invalid log level: {level}")
257
+ # ... set the level
258
+
259
+
260
+ # AFTER: Compile-time safety with Literal
261
+ LogLevel = Literal["debug", "info", "warning", "error"]
262
+
263
+
264
+ def set_log_level_good(level: LogLevel) -> str:
265
+ """
266
+ Good: Type checker catches invalid values.
267
+
268
+ >>> set_log_level_good("debug")
269
+ 'Log level set to: debug'
270
+
271
+ # This would be a type error (caught by mypy/pyright):
272
+ # set_log_level_good("invalid") # Error!
273
+ """
274
+ return f"Log level set to: {level}"
275
+
276
+
277
+ # =============================================================================
278
+ # Pattern 5: Exhaustive Match (P0)
279
+ # =============================================================================
280
+
281
+
282
+ class Status(Enum):
283
+ """Task status for exhaustive match demo."""
284
+
285
+ PENDING = "pending"
286
+ RUNNING = "running"
287
+ DONE = "done"
288
+ FAILED = "failed"
289
+
290
+
291
+ # BEFORE: Missing cases fail silently
292
+ def status_message_bad(status: Status) -> str:
293
+ """
294
+ Bad: Non-exhaustive match.
295
+
296
+ If a new status is added (e.g., CANCELLED), this code
297
+ silently returns "unknown" instead of failing to compile.
298
+ """
299
+ match status:
300
+ case Status.PENDING:
301
+ return "Waiting to start"
302
+ case Status.RUNNING:
303
+ return "In progress"
304
+ # DONE and FAILED missing - falls through to default!
305
+ return "unknown"
306
+
307
+
308
+ # AFTER: Compiler catches missing cases
309
+ def status_message_good(status: Status) -> str:
310
+ """
311
+ Good: Exhaustive match with assert_never.
312
+
313
+ If a new status is added, type checker reports an error
314
+ because assert_never expects type Never, but gets the new status.
315
+
316
+ >>> status_message_good(Status.PENDING)
317
+ 'Waiting to start'
318
+ >>> status_message_good(Status.DONE)
319
+ 'Completed successfully'
320
+ >>> status_message_good(Status.FAILED)
321
+ 'Task failed'
322
+ """
323
+ match status:
324
+ case Status.PENDING:
325
+ return "Waiting to start"
326
+ case Status.RUNNING:
327
+ return "In progress"
328
+ case Status.DONE:
329
+ return "Completed successfully"
330
+ case Status.FAILED:
331
+ return "Task failed"
332
+ case _:
333
+ assert_never(status) # Type error if cases are missing!
334
+
335
+
336
+ # =============================================================================
337
+ # Pattern 6: Smart Constructor (P1)
338
+ # =============================================================================
339
+
340
+
341
+ # BEFORE: Can create invalid objects
342
+ @dataclass
343
+ class EmailBad:
344
+ """
345
+ Bad: No validation at construction.
346
+
347
+ EmailBad("not-an-email") creates an invalid object.
348
+ Validation happens elsewhere, if at all.
349
+ """
350
+
351
+ value: str
352
+
353
+
354
+ # AFTER: Validation at construction
355
+ @dataclass(frozen=True)
356
+ class Email:
357
+ """
358
+ Good: Smart constructor validates on creation.
359
+
360
+ Invalid emails can never exist - construction fails.
361
+
362
+ >>> Email.create("user@example.com")
363
+ Success(Email(_value='user@example.com'))
364
+
365
+ >>> Email.create("not-an-email")
366
+ Failure('Email must contain @')
367
+
368
+ >>> Email.create("")
369
+ Failure('Email cannot be empty')
370
+ """
371
+
372
+ _value: str
373
+
374
+ @classmethod
375
+ def create(cls, value: str) -> Result["Email", str]:
376
+ """Validate and construct email."""
377
+ if not value:
378
+ return Failure("Email cannot be empty")
379
+ if "@" not in value:
380
+ return Failure("Email must contain @")
381
+ if "." not in value.split("@")[1]:
382
+ return Failure("Email domain must have a dot")
383
+ return Success(cls(_value=value))
384
+
385
+ @property
386
+ def value(self) -> str:
387
+ """Expose validated value."""
388
+ return self._value
389
+
390
+
391
+ # =============================================================================
392
+ # Pattern 7: Structured Error (P1)
393
+ # =============================================================================
394
+
395
+
396
+ # BEFORE: String error messages with embedded data
397
+ def parse_bad(text: str, line: int) -> Result[str, str]:
398
+ """
399
+ Bad: Error information embedded in string.
400
+
401
+ The line number is in the message but not accessible
402
+ for programmatic handling (highlighting, jumping to line).
403
+ """
404
+ if not text:
405
+ return Failure(f"Parse error at line {line}: unexpected EOF")
406
+ return Success(text)
407
+
408
+
409
+ # AFTER: Structured error type
410
+ @dataclass(frozen=True)
411
+ class ParseError:
412
+ """
413
+ Good: Structured error with accessible fields.
414
+
415
+ Code can extract line number for highlighting,
416
+ message for display, etc.
417
+ """
418
+
419
+ message: str
420
+ line: int
421
+ column: int = 0
422
+
423
+
424
+ def parse_good(text: str, line: int) -> Result[str, ParseError]:
425
+ """
426
+ Good: Structured error for programmatic handling.
427
+
428
+ >>> result = parse_good("", 42)
429
+ >>> isinstance(result, Failure)
430
+ True
431
+ >>> result.failure().line
432
+ 42
433
+ >>> result.failure().message
434
+ 'unexpected EOF'
435
+
436
+ >>> parse_good("valid", 1)
437
+ Success('valid')
438
+ """
439
+ if not text:
440
+ return Failure(ParseError(message="unexpected EOF", line=line))
441
+ return Success(text)
442
+
443
+
444
+ # =============================================================================
445
+ # Pattern 8: Optional → Result Conversion
446
+ # =============================================================================
447
+
448
+
449
+ def find_user_bad(user_id: str) -> dict | None:
450
+ """
451
+ Bad: Optional return hides error reason.
452
+
453
+ Caller can't distinguish "not found" from "invalid id" from "db error".
454
+ """
455
+ # Demo: return None for missing
456
+ if not user_id:
457
+ return None
458
+ return {"id": user_id, "name": "Demo"}
459
+
460
+
461
+ @dataclass(frozen=True)
462
+ class UserNotFoundError:
463
+ """Specific error: user doesn't exist."""
464
+
465
+ user_id: str
466
+
467
+
468
+ @dataclass(frozen=True)
469
+ class InvalidUserIdError:
470
+ """Specific error: invalid ID format."""
471
+
472
+ user_id: str
473
+ reason: str
474
+
475
+
476
+ UserError = UserNotFoundError | InvalidUserIdError
477
+
478
+
479
+ def find_user_good(user_id: str) -> Result[dict, UserError]:
480
+ """
481
+ Good: Result with specific error types.
482
+
483
+ >>> find_user_good("")
484
+ Failure(InvalidUserIdError(user_id='', reason='empty id'))
485
+
486
+ >>> find_user_good("unknown")
487
+ Failure(UserNotFoundError(user_id='unknown'))
488
+
489
+ >>> find_user_good("user123")
490
+ Success({'id': 'user123', 'name': 'Demo'})
491
+ """
492
+ if not user_id:
493
+ return Failure(InvalidUserIdError(user_id=user_id, reason="empty id"))
494
+ if user_id == "unknown":
495
+ return Failure(UserNotFoundError(user_id=user_id))
496
+ return Success({"id": user_id, "name": "Demo"})
497
+
498
+
499
+ # =============================================================================
500
+ # Pattern 9: try/except → Result Conversion
501
+ # =============================================================================
502
+
503
+
504
+ def parse_json_bad(text: str) -> dict:
505
+ """
506
+ Bad: Exceptions for expected errors.
507
+
508
+ JSON parsing failure is expected (user input), not exceptional.
509
+ """
510
+ return json.loads(text) # Raises JSONDecodeError
511
+
512
+
513
+ @dataclass(frozen=True)
514
+ class JsonParseError:
515
+ """Structured JSON parse error."""
516
+
517
+ message: str
518
+ position: int
519
+
520
+
521
+ def parse_json_good(text: str) -> Result[dict, JsonParseError]:
522
+ """
523
+ Good: Result for expected failures.
524
+
525
+ >>> parse_json_good('{"a": 1}')
526
+ Success({'a': 1})
527
+
528
+ >>> result = parse_json_good('invalid')
529
+ >>> isinstance(result, Failure)
530
+ True
531
+ >>> result.failure().message
532
+ 'Expecting value: line 1 column 1 (char 0)'
533
+ """
534
+ try:
535
+ return Success(json.loads(text))
536
+ except json.JSONDecodeError as e:
537
+ return Failure(JsonParseError(message=str(e), position=e.pos))
538
+
539
+
540
+ # =============================================================================
541
+ # Pattern 10: Result Chaining with bind/map
542
+ # =============================================================================
543
+
544
+
545
+ def process_pipeline_bad(raw: str) -> str:
546
+ """
547
+ Bad: Nested try/except for sequential operations.
548
+
549
+ Error handling scattered, hard to follow the happy path.
550
+ """
551
+ try:
552
+ data = json.loads(raw)
553
+ except json.JSONDecodeError:
554
+ raise ValueError("Invalid JSON")
555
+
556
+ try:
557
+ name = data["user"]["name"]
558
+ except KeyError:
559
+ raise ValueError("Missing user.name")
560
+
561
+ return name.upper()
562
+
563
+
564
+ def extract_name(data: dict) -> Result[str, str]:
565
+ """Extract user.name from dict."""
566
+ try:
567
+ return Success(data["user"]["name"])
568
+ except KeyError:
569
+ return Failure("Missing user.name")
570
+
571
+
572
+ def process_pipeline_good(raw: str) -> Result[str, str]:
573
+ """
574
+ Good: Result chaining for sequential operations.
575
+
576
+ Happy path is clear: parse → extract → transform.
577
+ Errors propagate automatically.
578
+
579
+ >>> process_pipeline_good('{"user": {"name": "alice"}}')
580
+ Success('ALICE')
581
+
582
+ >>> process_pipeline_good('invalid json')
583
+ Failure('Expecting value: line 1 column 1 (char 0)')
584
+
585
+ >>> process_pipeline_good('{"other": 1}')
586
+ Failure('Missing user.name')
587
+ """
588
+ # Parse JSON → extract name → uppercase
589
+ # Each step returns Result, errors propagate automatically
590
+ try:
591
+ data = json.loads(raw)
592
+ except json.JSONDecodeError as e:
593
+ return Failure(str(e))
594
+
595
+ return extract_name(data).map(lambda name: name.upper())
596
+
597
+
598
+ # =============================================================================
599
+ # Summary: When to Use Each Pattern
600
+ # =============================================================================
601
+
602
+ # | Pattern | Use When |
603
+ # |-------------------|---------------------------------------------|
604
+ # | NewType | 3+ params of same primitive type |
605
+ # | Validation | Multiple independent validations |
606
+ # | NonEmpty | Functions that require non-empty input |
607
+ # | Literal | Parameter with finite valid values |
608
+ # | ExhaustiveMatch | Matching on enums |
609
+ # | SmartConstructor | Types with invariants |
610
+ # | StructuredError | Errors with metadata (line, column, etc.) |
611
+ # | Optional→Result | Functions returning None for failures |
612
+ # | try/except→Result | Wrapping exceptions as Result |
613
+ # | Result Chaining | Sequential operations with error propagation|
@@ -0,0 +1,31 @@
1
+ # Invar Examples (TypeScript)
2
+
3
+ Reference patterns for TypeScript projects using the Invar Protocol.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |------|---------|
9
+ | `contracts.ts` | Zod schema patterns for pre/postconditions |
10
+ | `core_shell.ts` | Core/Shell separation with neverthrow |
11
+ | `functional.ts` | Functional patterns (Branded Types, NonEmpty, Exhaustive Match) |
12
+ | `workflow.md` | Complete USBV workflow example |
13
+
14
+ ## Dependencies
15
+
16
+ ```bash
17
+ npm install zod neverthrow
18
+ ```
19
+
20
+ ## Quick Reference
21
+
22
+ | Concept | TypeScript Pattern |
23
+ |---------|-------------------|
24
+ | Precondition | `z.number().positive()` |
25
+ | Postcondition | Return type validation |
26
+ | Examples | JSDoc `@example` blocks |
27
+ | Shell errors | `Result<T, E>` from neverthrow |
28
+
29
+ ---
30
+
31
+ *Managed by Invar - regenerated on `invar update`*