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,601 @@
1
+ /**
2
+ * Invar Functional Pattern Examples (DX-61) - TypeScript
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. Branded Types - Semantic clarity for primitive types
10
+ * 2. Validation - Error accumulation instead of fail-fast
11
+ * 3. NonEmpty - Type-safe 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
+
22
+ import { z } from 'zod';
23
+ import { Result, ok, err } from 'neverthrow';
24
+
25
+ // =============================================================================
26
+ // Pattern 1: Branded Types for Semantic Clarity (P0)
27
+ // =============================================================================
28
+
29
+ // BEFORE: Easy to confuse parameters - all are just "string"
30
+ // function findSymbolBad(
31
+ // modulePath: string,
32
+ // symbolName: string,
33
+ // filePattern: string,
34
+ // ): Symbol { ... }
35
+
36
+ // AFTER: Self-documenting, type-checker catches mistakes
37
+ // Using branded types (also called nominal types or opaque types)
38
+
39
+ declare const ModulePathBrand: unique symbol;
40
+ declare const SymbolNameBrand: unique symbol;
41
+ declare const FilePatternBrand: unique symbol;
42
+
43
+ type ModulePath = string & { readonly [ModulePathBrand]: typeof ModulePathBrand };
44
+ type SymbolName = string & { readonly [SymbolNameBrand]: typeof SymbolNameBrand };
45
+ type FilePattern = string & { readonly [FilePatternBrand]: typeof FilePatternBrand };
46
+
47
+ // Constructors for branded types
48
+ const ModulePath = (value: string): ModulePath => value as ModulePath;
49
+ const SymbolName = (value: string): SymbolName => value as SymbolName;
50
+ const FilePattern = (value: string): FilePattern => value as FilePattern;
51
+
52
+ interface Symbol {
53
+ readonly name: string;
54
+ readonly line: number;
55
+ }
56
+
57
+ /**
58
+ * Find symbol by module path and name.
59
+ *
60
+ * With branded types, swapping arguments is a type error:
61
+ * findSymbol(name, path) // Type error!
62
+ *
63
+ * @example
64
+ * findSymbol(ModulePath("src/core"), SymbolName("calculate"))
65
+ * // => { name: 'calculate', line: 42 }
66
+ */
67
+ export function findSymbol(path: ModulePath, name: SymbolName): Symbol {
68
+ // Demo implementation
69
+ return { name: name, line: 42 };
70
+ }
71
+
72
+ // =============================================================================
73
+ // Pattern 2: Validation for Error Accumulation (P0)
74
+ // =============================================================================
75
+
76
+ interface Config {
77
+ readonly path: string;
78
+ readonly maxLines: number;
79
+ readonly enabled: boolean;
80
+ }
81
+
82
+ // BEFORE: User sees one error at a time
83
+ function validateConfigBad(data: unknown): Result<Config, string> {
84
+ const obj = data as Record<string, unknown>;
85
+ if (!('path' in obj)) {
86
+ return err("Missing 'path'");
87
+ }
88
+ if (!('maxLines' in obj)) {
89
+ return err("Missing 'maxLines'"); // Never reached if path missing
90
+ }
91
+ if (!('enabled' in obj)) {
92
+ return err("Missing 'enabled'");
93
+ }
94
+ return ok(obj as unknown as Config);
95
+ }
96
+
97
+ // AFTER: User sees all errors at once
98
+ /**
99
+ * Good: Accumulating validation.
100
+ *
101
+ * Collect all errors, return them together. User can fix everything
102
+ * in one iteration. Much better UX!
103
+ *
104
+ * @example
105
+ * validateConfigGood({})
106
+ * // => Err(["Missing 'path'", "Missing 'maxLines'", "Missing 'enabled'"])
107
+ *
108
+ * @example
109
+ * validateConfigGood({ path: "/tmp", maxLines: 100, enabled: true })
110
+ * // => Ok({ path: '/tmp', maxLines: 100, enabled: true })
111
+ */
112
+ export function validateConfigGood(
113
+ data: unknown
114
+ ): Result<Config, string[]> {
115
+ const obj = data as Record<string, unknown>;
116
+ const errors: string[] = [];
117
+
118
+ // Collect ALL errors, don't return early
119
+ if (!('path' in obj)) {
120
+ errors.push("Missing 'path'");
121
+ } else if (!obj.path) {
122
+ errors.push("path cannot be empty");
123
+ }
124
+
125
+ if (!('maxLines' in obj)) {
126
+ errors.push("Missing 'maxLines'");
127
+ } else if (typeof obj.maxLines !== 'number') {
128
+ errors.push("maxLines must be a number");
129
+ } else if (obj.maxLines < 0) {
130
+ errors.push("maxLines must be >= 0");
131
+ }
132
+
133
+ if (!('enabled' in obj)) {
134
+ errors.push("Missing 'enabled'");
135
+ }
136
+
137
+ if (errors.length > 0) {
138
+ return err(errors);
139
+ }
140
+
141
+ return ok({
142
+ path: obj.path as string,
143
+ maxLines: obj.maxLines as number,
144
+ enabled: obj.enabled as boolean,
145
+ });
146
+ }
147
+
148
+ // =============================================================================
149
+ // Pattern 3: NonEmpty for Type Safety (P0)
150
+ // =============================================================================
151
+
152
+ /**
153
+ * Array guaranteed to have at least one element.
154
+ *
155
+ * Instead of runtime checks like `if (!items.length) throw`,
156
+ * use the type system to guarantee non-emptiness.
157
+ */
158
+ interface NonEmptyArray<T> {
159
+ readonly head: T;
160
+ readonly tail: readonly T[];
161
+ }
162
+
163
+ const NonEmptyArray = {
164
+ /**
165
+ * Safely construct from array.
166
+ *
167
+ * @example
168
+ * NonEmptyArray.fromArray([])
169
+ * // => Err('Cannot create NonEmptyArray from empty array')
170
+ *
171
+ * @example
172
+ * NonEmptyArray.fromArray([1, 2, 3])
173
+ * // => Ok({ head: 1, tail: [2, 3] })
174
+ */
175
+ fromArray<T>(items: readonly T[]): Result<NonEmptyArray<T>, string> {
176
+ if (items.length === 0) {
177
+ return err('Cannot create NonEmptyArray from empty array');
178
+ }
179
+ return ok({ head: items[0], tail: items.slice(1) });
180
+ },
181
+
182
+ /**
183
+ * Get first element (always safe - guaranteed non-empty).
184
+ */
185
+ first<T>(ne: NonEmptyArray<T>): T {
186
+ return ne.head;
187
+ },
188
+
189
+ /**
190
+ * Get all elements.
191
+ */
192
+ toArray<T>(ne: NonEmptyArray<T>): readonly T[] {
193
+ return [ne.head, ...ne.tail];
194
+ },
195
+
196
+ /**
197
+ * Get length (always >= 1).
198
+ */
199
+ length<T>(ne: NonEmptyArray<T>): number {
200
+ return 1 + ne.tail.length;
201
+ },
202
+ };
203
+
204
+ // BEFORE: Defensive runtime check
205
+ function summarizeBad(items: string[]): string {
206
+ if (items.length === 0) {
207
+ throw new Error("Cannot summarize empty array");
208
+ }
209
+ return `First: ${items[0]}, Total: ${items.length}`;
210
+ }
211
+
212
+ // AFTER: Type-safe, no check needed
213
+ /**
214
+ * Good: Type guarantees non-empty.
215
+ *
216
+ * No runtime check needed - if you have a NonEmptyArray,
217
+ * it's guaranteed to have at least one element.
218
+ *
219
+ * @example
220
+ * const ne = NonEmptyArray.fromArray(["a", "b", "c"]).unwrap();
221
+ * summarizeGood(ne)
222
+ * // => 'First: a, Total: 3'
223
+ */
224
+ export function summarizeGood(items: NonEmptyArray<string>): string {
225
+ return `First: ${NonEmptyArray.first(items)}, Total: ${NonEmptyArray.length(items)}`;
226
+ }
227
+
228
+ // =============================================================================
229
+ // Pattern 4: Literal Types for Finite Value Sets (P0)
230
+ // =============================================================================
231
+
232
+ // BEFORE: Runtime validation for finite set
233
+ function setLogLevelBad(level: string): void {
234
+ if (!["debug", "info", "warning", "error"].includes(level)) {
235
+ throw new Error(`Invalid log level: ${level}`);
236
+ }
237
+ // ... set the level
238
+ }
239
+
240
+ // AFTER: Compile-time safety with literal types
241
+ type LogLevel = "debug" | "info" | "warning" | "error";
242
+
243
+ /**
244
+ * Good: Type checker catches invalid values.
245
+ *
246
+ * @example
247
+ * setLogLevelGood("debug")
248
+ * // => 'Log level set to: debug'
249
+ *
250
+ * // This would be a type error:
251
+ * // setLogLevelGood("invalid") // Error!
252
+ */
253
+ export function setLogLevelGood(level: LogLevel): string {
254
+ return `Log level set to: ${level}`;
255
+ }
256
+
257
+ // =============================================================================
258
+ // Pattern 5: Exhaustive Match (P0)
259
+ // =============================================================================
260
+
261
+ const Status = {
262
+ PENDING: 'pending',
263
+ RUNNING: 'running',
264
+ DONE: 'done',
265
+ FAILED: 'failed',
266
+ } as const;
267
+
268
+ type StatusType = typeof Status[keyof typeof Status];
269
+
270
+ // Helper for exhaustive matching
271
+ function assertNever(x: never): never {
272
+ throw new Error(`Unexpected value: ${x}`);
273
+ }
274
+
275
+ // BEFORE: Missing cases fail silently
276
+ function statusMessageBad(status: StatusType): string {
277
+ switch (status) {
278
+ case Status.PENDING:
279
+ return "Waiting to start";
280
+ case Status.RUNNING:
281
+ return "In progress";
282
+ // DONE and FAILED missing - falls through to default!
283
+ }
284
+ return "unknown";
285
+ }
286
+
287
+ // AFTER: Compiler catches missing cases
288
+ /**
289
+ * Good: Exhaustive match with assertNever.
290
+ *
291
+ * If a new status is added, type checker reports an error
292
+ * because assertNever expects type never, but gets the new status.
293
+ *
294
+ * @example
295
+ * statusMessageGood(Status.PENDING)
296
+ * // => 'Waiting to start'
297
+ *
298
+ * @example
299
+ * statusMessageGood(Status.DONE)
300
+ * // => 'Completed successfully'
301
+ */
302
+ export function statusMessageGood(status: StatusType): string {
303
+ switch (status) {
304
+ case Status.PENDING:
305
+ return "Waiting to start";
306
+ case Status.RUNNING:
307
+ return "In progress";
308
+ case Status.DONE:
309
+ return "Completed successfully";
310
+ case Status.FAILED:
311
+ return "Task failed";
312
+ default:
313
+ return assertNever(status); // Type error if cases are missing!
314
+ }
315
+ }
316
+
317
+ // =============================================================================
318
+ // Pattern 6: Smart Constructor with Zod (P1)
319
+ // =============================================================================
320
+
321
+ // BEFORE: Can create invalid objects
322
+ interface EmailBad {
323
+ value: string; // No validation, can be any string
324
+ }
325
+
326
+ // AFTER: Validation at construction with Zod
327
+ const EmailSchema = z.string()
328
+ .min(1, "Email cannot be empty")
329
+ .refine(s => s.includes("@"), "Email must contain @")
330
+ .refine(
331
+ s => s.includes("@") && s.split("@")[1].includes("."),
332
+ "Email domain must have a dot"
333
+ );
334
+
335
+ type EmailValue = z.infer<typeof EmailSchema>;
336
+
337
+ interface Email {
338
+ readonly value: EmailValue;
339
+ }
340
+
341
+ const Email = {
342
+ /**
343
+ * Validate and construct email.
344
+ *
345
+ * Invalid emails can never exist - construction fails.
346
+ *
347
+ * @example
348
+ * Email.create("user@example.com")
349
+ * // => Ok({ value: 'user@example.com' })
350
+ *
351
+ * @example
352
+ * Email.create("not-an-email")
353
+ * // => Err('Email must contain @')
354
+ */
355
+ create(value: string): Result<Email, string> {
356
+ const result = EmailSchema.safeParse(value);
357
+ if (!result.success) {
358
+ return err(result.error.errors[0].message);
359
+ }
360
+ return ok({ value: result.data });
361
+ },
362
+ };
363
+
364
+ // =============================================================================
365
+ // Pattern 7: Structured Error (P1)
366
+ // =============================================================================
367
+
368
+ // BEFORE: String error messages with embedded data
369
+ function parseBad(text: string, line: number): Result<string, string> {
370
+ if (!text) {
371
+ return err(`Parse error at line ${line}: unexpected EOF`);
372
+ }
373
+ return ok(text);
374
+ }
375
+
376
+ // AFTER: Structured error type
377
+ interface ParseError {
378
+ readonly message: string;
379
+ readonly line: number;
380
+ readonly column: number;
381
+ }
382
+
383
+ /**
384
+ * Good: Structured error for programmatic handling.
385
+ *
386
+ * Code can extract line number for highlighting,
387
+ * message for display, etc.
388
+ *
389
+ * @example
390
+ * const result = parseGood("", 42);
391
+ * if (result.isErr()) {
392
+ * result.error.line // => 42
393
+ * result.error.message // => 'unexpected EOF'
394
+ * }
395
+ *
396
+ * @example
397
+ * parseGood("valid", 1)
398
+ * // => Ok('valid')
399
+ */
400
+ export function parseGood(text: string, line: number): Result<string, ParseError> {
401
+ if (!text) {
402
+ return err({ message: "unexpected EOF", line, column: 0 });
403
+ }
404
+ return ok(text);
405
+ }
406
+
407
+ // =============================================================================
408
+ // Pattern 8: Promise → ResultAsync Conversion
409
+ // =============================================================================
410
+
411
+ import { ResultAsync } from 'neverthrow';
412
+
413
+ // BEFORE: Promise that throws
414
+ async function fetchUserBad(id: string): Promise<{ id: string; name: string }> {
415
+ const response = await fetch(`/api/users/${id}`);
416
+ if (!response.ok) {
417
+ throw new Error(`HTTP ${response.status}`); // Throws!
418
+ }
419
+ return response.json();
420
+ }
421
+
422
+ // AFTER: ResultAsync for typed errors
423
+ interface ApiError {
424
+ readonly code: string;
425
+ readonly message: string;
426
+ readonly status?: number;
427
+ }
428
+
429
+ /**
430
+ * Good: ResultAsync for async operations.
431
+ *
432
+ * Errors are typed and explicit, not thrown.
433
+ *
434
+ * @example
435
+ * const result = await fetchUserGood("123");
436
+ * if (result.isOk()) {
437
+ * console.log(result.value.name);
438
+ * } else {
439
+ * console.error(result.error.message);
440
+ * }
441
+ */
442
+ function fetchUserGood(
443
+ id: string
444
+ ): ResultAsync<{ id: string; name: string }, ApiError> {
445
+ return ResultAsync.fromPromise(
446
+ fetch(`/api/users/${id}`).then(async (response) => {
447
+ if (!response.ok) {
448
+ throw { status: response.status };
449
+ }
450
+ return response.json();
451
+ }),
452
+ (error): ApiError => {
453
+ const e = error as { status?: number };
454
+ return {
455
+ code: 'fetch_failed',
456
+ message: `Failed to fetch user ${id}`,
457
+ status: e.status,
458
+ };
459
+ }
460
+ );
461
+ }
462
+
463
+ // =============================================================================
464
+ // Pattern 9: null/undefined → Result Conversion
465
+ // =============================================================================
466
+
467
+ // BEFORE: Returns null, caller must check
468
+ function findItemBad(
469
+ items: readonly { id: string }[],
470
+ id: string
471
+ ): { id: string } | null {
472
+ return items.find(item => item.id === id) ?? null;
473
+ }
474
+
475
+ // AFTER: Result with specific error
476
+ interface NotFoundError {
477
+ readonly type: 'not_found';
478
+ readonly id: string;
479
+ }
480
+
481
+ /**
482
+ * Good: Result instead of null.
483
+ *
484
+ * @example
485
+ * const result = findItemGood([{ id: "1" }], "1");
486
+ * // => Ok({ id: "1" })
487
+ *
488
+ * @example
489
+ * const result = findItemGood([{ id: "1" }], "2");
490
+ * // => Err({ type: "not_found", id: "2" })
491
+ */
492
+ export function findItemGood(
493
+ items: readonly { id: string }[],
494
+ id: string
495
+ ): Result<{ id: string }, NotFoundError> {
496
+ const item = items.find(i => i.id === id);
497
+ if (!item) {
498
+ return err({ type: 'not_found', id });
499
+ }
500
+ return ok(item);
501
+ }
502
+
503
+ // =============================================================================
504
+ // Pattern 10: ResultAsync Chaining
505
+ // =============================================================================
506
+
507
+ interface User {
508
+ readonly id: string;
509
+ readonly name: string;
510
+ readonly profileId: string;
511
+ }
512
+
513
+ interface Profile {
514
+ readonly id: string;
515
+ readonly avatar: string;
516
+ }
517
+
518
+ // Mock async functions returning ResultAsync
519
+ function getUser(id: string): ResultAsync<User, ApiError> {
520
+ return ResultAsync.fromPromise(
521
+ Promise.resolve({ id, name: 'Demo', profileId: 'p1' }),
522
+ (): ApiError => ({ code: 'user_error', message: 'Failed to get user' })
523
+ );
524
+ }
525
+
526
+ function getProfile(id: string): ResultAsync<Profile, ApiError> {
527
+ return ResultAsync.fromPromise(
528
+ Promise.resolve({ id, avatar: '/avatar.png' }),
529
+ (): ApiError => ({ code: 'profile_error', message: 'Failed to get profile' })
530
+ );
531
+ }
532
+
533
+ /**
534
+ * Good: ResultAsync chaining for sequential async operations.
535
+ *
536
+ * @example
537
+ * const result = await getUserWithAvatar("123");
538
+ * if (result.isOk()) {
539
+ * console.log(result.value);
540
+ * // => { userId: "123", userName: "Demo", avatar: "/avatar.png" }
541
+ * }
542
+ */
543
+ export function getUserWithAvatar(
544
+ userId: string
545
+ ): ResultAsync<{ userId: string; userName: string; avatar: string }, ApiError> {
546
+ return getUser(userId).andThen((user) =>
547
+ getProfile(user.profileId).map((profile) => ({
548
+ userId: user.id,
549
+ userName: user.name,
550
+ avatar: profile.avatar,
551
+ }))
552
+ );
553
+ }
554
+
555
+ // =============================================================================
556
+ // Pattern 11: Combining Multiple ResultAsync
557
+ // =============================================================================
558
+
559
+ interface CombinedData {
560
+ readonly user: User;
561
+ readonly profile: Profile;
562
+ }
563
+
564
+ /**
565
+ * Good: Parallel ResultAsync with combine.
566
+ *
567
+ * Both requests run in parallel, fails fast if either fails.
568
+ *
569
+ * @example
570
+ * const result = await fetchUserAndProfile("u1", "p1");
571
+ * if (result.isOk()) {
572
+ * const [user, profile] = result.value;
573
+ * }
574
+ */
575
+ export function fetchUserAndProfile(
576
+ userId: string,
577
+ profileId: string
578
+ ): ResultAsync<[User, Profile], ApiError> {
579
+ return ResultAsync.combine([
580
+ getUser(userId),
581
+ getProfile(profileId),
582
+ ]);
583
+ }
584
+
585
+ // =============================================================================
586
+ // Summary: When to Use Each Pattern
587
+ // =============================================================================
588
+
589
+ // | Pattern | Use When |
590
+ // |-------------------|---------------------------------------------|
591
+ // | Branded Types | 3+ params of same primitive type |
592
+ // | Validation | Multiple independent validations |
593
+ // | NonEmptyArray | Functions that require non-empty input |
594
+ // | Literal Types | Parameter with finite valid values |
595
+ // | ExhaustiveMatch | Matching on union types or const objects |
596
+ // | SmartConstructor | Types with invariants (use Zod) |
597
+ // | StructuredError | Errors with metadata (line, column, etc.) |
598
+ // | Promise→ResultAsync| Async operations with typed errors |
599
+ // | null→Result | Functions returning null for "not found" |
600
+ // | ResultAsync Chain | Sequential async with error propagation |
601
+ // | ResultAsync.combine| Parallel async operations |
@@ -0,0 +1,95 @@
1
+ # Visible Workflow Example (TypeScript)
2
+
3
+ Complete example of USBV workflow with checkpoints for complex tasks.
4
+
5
+ ## Scenario
6
+
7
+ User: "Add rate limiting to the API endpoints"
8
+
9
+ ## Workflow Execution
10
+
11
+ ```
12
+ Agent: "Entering /develop for: Add rate limiting to API endpoints
13
+
14
+ ✓ Check-In: MyApp | feature/rate-limit | clean
15
+
16
+ □ [UNDERSTAND]
17
+ - Task: Add rate limiting to API endpoints
18
+ - Scope: All routes in src/myapp/shell/api.ts
19
+ - Constraint: Must be configurable per-route
20
+ - Existing: No rate limiting currently
21
+
22
+ □ [SPECIFY] RateLimiter class (Core)
23
+ // Zod schema for constructor
24
+ const RateLimiterConfig = z.object({
25
+ maxRequests: z.number().positive(),
26
+ windowSeconds: z.number().positive(),
27
+ });
28
+
29
+ /**
30
+ * Check if request is allowed under rate limit.
31
+ *
32
+ * @example
33
+ * const limiter = new RateLimiter({ maxRequests: 2, windowSeconds: 60 });
34
+ * limiter.isAllowed('user1'); // => true
35
+ * limiter.isAllowed('user1'); // => true
36
+ * limiter.isAllowed('user1'); // => false (3rd request)
37
+ */
38
+ isAllowed(key: string): boolean { ... }
39
+
40
+ □ [SPECIFY] rateLimit middleware (Shell)
41
+ /**
42
+ * Express middleware for rate limiting.
43
+ * Returns Result<void, RateLimitError>.
44
+ */
45
+ function rateLimit(config: RateLimiterConfig): RequestHandler { ... }
46
+
47
+ [BUILD - implementing RateLimiter...]
48
+ [BUILD - implementing middleware...]
49
+ [BUILD - applying to routes...]
50
+
51
+ □ [VALIDATE]
52
+ - Guard: PASS (0 errors, 0 warnings)
53
+ - Tests: All passing (vitest)
54
+ - Integration: Manual test with curl
55
+
56
+ ✓ Final: guard PASS | 0 errors, 0 warnings
57
+
58
+ Development complete. Added rate limiting:
59
+ - Created: src/myapp/core/rate-limiter.ts
60
+ - Modified: src/myapp/shell/api.ts (middleware applied)
61
+ - Config: RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW in .env"
62
+ ```
63
+
64
+ ## Key Points
65
+
66
+ 1. **Check-In first** - Shows project, branch, git status (no guard/map here)
67
+ 2. **UNDERSTAND visible** - Task scope and constraints clear
68
+ 3. **SPECIFY visible** - Zod schemas and JSDoc shown BEFORE implementation
69
+ 4. **BUILD hidden** - Internal work, not in checkpoints
70
+ 5. **VALIDATE visible** - Guard results and integration status
71
+ 6. **Final last** - Runs guard and completes the session
72
+
73
+ ## TypeScript Contract Patterns
74
+
75
+ | Pattern | Example |
76
+ |---------|---------|
77
+ | **Precondition** | `z.number().positive()` |
78
+ | **Postcondition** | Return type validation with Zod |
79
+ | **Examples** | JSDoc `@example` blocks |
80
+ | **Error handling** | `Result<T, E>` from neverthrow |
81
+
82
+ ## When to Use
83
+
84
+ | Complexity | Use Visible Workflow? |
85
+ |------------|----------------------|
86
+ | 3+ functions | Yes |
87
+ | Architectural changes | Yes |
88
+ | New Core module | Yes |
89
+ | Single-line fix | No |
90
+ | Documentation only | No |
91
+ | Trivial refactoring | No |
92
+
93
+ ---
94
+
95
+ *Example for the Invar Protocol v5.0 (TypeScript)*