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
|
@@ -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)*
|