elspais 0.9.1__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.
elspais/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ elspais - Requirements validation and traceability tools
3
+
4
+ L-Space is the ultimate library, connecting all libraries everywhere
5
+ through the sheer weight of accumulated knowledge.
6
+ — Terry Pratchett
7
+
8
+ elspais validates requirement formats, generates traceability matrices,
9
+ and supports multi-repository requirement management with configurable
10
+ ID patterns and validation rules.
11
+ """
12
+
13
+ from importlib.metadata import version, PackageNotFoundError
14
+
15
+ try:
16
+ __version__ = version("elspais")
17
+ except PackageNotFoundError:
18
+ __version__ = "0.0.0+unknown" # Not installed
19
+ __author__ = "Anspar"
20
+ __license__ = "MIT"
21
+
22
+ from elspais.core.models import Assertion, ContentRule, ParsedRequirement, Requirement
23
+ from elspais.core.patterns import PatternValidator
24
+ from elspais.core.rules import RuleEngine, RuleViolation, Severity
25
+
26
+ __all__ = [
27
+ "__version__",
28
+ "Assertion",
29
+ "ContentRule",
30
+ "Requirement",
31
+ "ParsedRequirement",
32
+ "PatternValidator",
33
+ "RuleEngine",
34
+ "RuleViolation",
35
+ "Severity",
36
+ ]
elspais/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ Allow running elspais as a module: python -m elspais
3
+ """
4
+
5
+ from elspais.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
elspais/cli.py ADDED
@@ -0,0 +1,525 @@
1
+ """
2
+ elspais.cli - Command-line interface.
3
+
4
+ Main entry point for the elspais CLI tool.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+ from elspais import __version__
13
+ from elspais.commands import analyze, config_cmd, edit, hash_cmd, index, init, rules_cmd, trace, validate
14
+
15
+
16
+ def create_parser() -> argparse.ArgumentParser:
17
+ """Create the argument parser."""
18
+ parser = argparse.ArgumentParser(
19
+ prog="elspais",
20
+ description="Requirements validation and traceability tools (L-Space)",
21
+ formatter_class=argparse.RawDescriptionHelpFormatter,
22
+ epilog="""
23
+ Examples:
24
+ elspais validate # Validate all requirements
25
+ elspais trace --format html # Generate HTML traceability matrix
26
+ elspais hash update # Update all requirement hashes
27
+ elspais init # Create .elspais.toml configuration
28
+ """,
29
+ )
30
+
31
+ # Global options
32
+ parser.add_argument(
33
+ "--version",
34
+ action="version",
35
+ version=f"elspais {__version__}",
36
+ )
37
+ parser.add_argument(
38
+ "--config",
39
+ type=Path,
40
+ help="Path to configuration file",
41
+ metavar="PATH",
42
+ )
43
+ parser.add_argument(
44
+ "--spec-dir",
45
+ type=Path,
46
+ help="Override spec directory",
47
+ metavar="PATH",
48
+ )
49
+ parser.add_argument(
50
+ "-v", "--verbose",
51
+ action="store_true",
52
+ help="Verbose output",
53
+ )
54
+ parser.add_argument(
55
+ "-q", "--quiet",
56
+ action="store_true",
57
+ help="Suppress non-error output",
58
+ )
59
+
60
+ # Subcommands
61
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
62
+
63
+ # validate command
64
+ validate_parser = subparsers.add_parser(
65
+ "validate",
66
+ help="Validate requirements format, links, and hashes",
67
+ )
68
+ validate_parser.add_argument(
69
+ "--fix",
70
+ action="store_true",
71
+ help="Auto-fix fixable issues",
72
+ )
73
+ validate_parser.add_argument(
74
+ "--core-repo",
75
+ type=Path,
76
+ help="Path to core repository (for associated repo validation)",
77
+ metavar="PATH",
78
+ )
79
+ validate_parser.add_argument(
80
+ "--skip-rule",
81
+ action="append",
82
+ help="Skip specific validation rules",
83
+ metavar="RULE",
84
+ )
85
+ validate_parser.add_argument(
86
+ "-j", "--json",
87
+ action="store_true",
88
+ help="Output requirements as JSON (hht_diary compatible format)",
89
+ )
90
+ validate_parser.add_argument(
91
+ "--tests",
92
+ action="store_true",
93
+ help="Force test scanning even if disabled in config",
94
+ )
95
+ validate_parser.add_argument(
96
+ "--no-tests",
97
+ action="store_true",
98
+ help="Skip test scanning",
99
+ )
100
+
101
+ # trace command
102
+ trace_parser = subparsers.add_parser(
103
+ "trace",
104
+ help="Generate traceability matrix",
105
+ )
106
+ trace_parser.add_argument(
107
+ "--format",
108
+ choices=["markdown", "html", "csv", "both"],
109
+ default="both",
110
+ help="Output format (default: both)",
111
+ )
112
+ trace_parser.add_argument(
113
+ "--output",
114
+ type=Path,
115
+ help="Output file path",
116
+ metavar="PATH",
117
+ )
118
+
119
+ # hash command
120
+ hash_parser = subparsers.add_parser(
121
+ "hash",
122
+ help="Manage requirement hashes",
123
+ )
124
+ hash_subparsers = hash_parser.add_subparsers(dest="hash_action")
125
+
126
+ hash_subparsers.add_parser(
127
+ "verify",
128
+ help="Verify hashes without changes",
129
+ )
130
+
131
+ hash_update = hash_subparsers.add_parser(
132
+ "update",
133
+ help="Update hashes",
134
+ )
135
+ hash_update.add_argument(
136
+ "req_id",
137
+ nargs="?",
138
+ help="Specific requirement ID to update",
139
+ )
140
+ hash_update.add_argument(
141
+ "--dry-run",
142
+ action="store_true",
143
+ help="Show changes without applying",
144
+ )
145
+
146
+ # index command
147
+ index_parser = subparsers.add_parser(
148
+ "index",
149
+ help="Manage INDEX.md file",
150
+ )
151
+ index_subparsers = index_parser.add_subparsers(dest="index_action")
152
+
153
+ index_subparsers.add_parser(
154
+ "validate",
155
+ help="Validate INDEX.md accuracy",
156
+ )
157
+ index_subparsers.add_parser(
158
+ "regenerate",
159
+ help="Regenerate INDEX.md from scratch",
160
+ )
161
+
162
+ # analyze command
163
+ analyze_parser = subparsers.add_parser(
164
+ "analyze",
165
+ help="Analyze requirement hierarchy",
166
+ )
167
+ analyze_subparsers = analyze_parser.add_subparsers(dest="analyze_action")
168
+
169
+ analyze_subparsers.add_parser(
170
+ "hierarchy",
171
+ help="Show requirement hierarchy tree",
172
+ )
173
+ analyze_subparsers.add_parser(
174
+ "orphans",
175
+ help="Find orphaned requirements",
176
+ )
177
+ analyze_subparsers.add_parser(
178
+ "coverage",
179
+ help="Implementation coverage report",
180
+ )
181
+
182
+ # version command
183
+ version_parser = subparsers.add_parser(
184
+ "version",
185
+ help="Show version and check for updates",
186
+ )
187
+ version_parser.add_argument(
188
+ "check",
189
+ nargs="?",
190
+ help="Check for updates",
191
+ )
192
+
193
+ # init command
194
+ init_parser = subparsers.add_parser(
195
+ "init",
196
+ help="Create .elspais.toml configuration",
197
+ )
198
+ init_parser.add_argument(
199
+ "--type",
200
+ choices=["core", "associated"],
201
+ help="Repository type",
202
+ )
203
+ init_parser.add_argument(
204
+ "--associated-prefix",
205
+ help="Associated repo prefix (e.g., CAL)",
206
+ metavar="PREFIX",
207
+ )
208
+ init_parser.add_argument(
209
+ "--force",
210
+ action="store_true",
211
+ help="Overwrite existing configuration",
212
+ )
213
+
214
+ # edit command
215
+ edit_parser = subparsers.add_parser(
216
+ "edit",
217
+ help="Edit requirements in-place (implements, status, move)",
218
+ )
219
+ edit_parser.add_argument(
220
+ "--req-id",
221
+ help="Requirement ID to edit",
222
+ metavar="ID",
223
+ )
224
+ edit_parser.add_argument(
225
+ "--implements",
226
+ help="New Implements value (comma-separated, empty string to clear)",
227
+ metavar="REFS",
228
+ )
229
+ edit_parser.add_argument(
230
+ "--status",
231
+ help="New Status value",
232
+ metavar="STATUS",
233
+ )
234
+ edit_parser.add_argument(
235
+ "--move-to",
236
+ help="Move requirement to file (relative to spec dir)",
237
+ metavar="FILE",
238
+ )
239
+ edit_parser.add_argument(
240
+ "--from-json",
241
+ help="Batch edit from JSON file (- for stdin)",
242
+ metavar="FILE",
243
+ )
244
+ edit_parser.add_argument(
245
+ "--dry-run",
246
+ action="store_true",
247
+ help="Show changes without applying",
248
+ )
249
+ edit_parser.add_argument(
250
+ "--validate-refs",
251
+ action="store_true",
252
+ help="Validate that implements references exist",
253
+ )
254
+
255
+ # config command
256
+ config_parser = subparsers.add_parser(
257
+ "config",
258
+ help="View and modify configuration",
259
+ )
260
+ config_subparsers = config_parser.add_subparsers(dest="config_action")
261
+
262
+ # config show
263
+ config_show = config_subparsers.add_parser(
264
+ "show",
265
+ help="Show current configuration",
266
+ )
267
+ config_show.add_argument(
268
+ "--section",
269
+ help="Show only a specific section (e.g., 'patterns', 'rules.format')",
270
+ metavar="SECTION",
271
+ )
272
+ config_show.add_argument(
273
+ "-j", "--json",
274
+ action="store_true",
275
+ help="Output as JSON",
276
+ )
277
+
278
+ # config get
279
+ config_get = config_subparsers.add_parser(
280
+ "get",
281
+ help="Get a configuration value",
282
+ )
283
+ config_get.add_argument(
284
+ "key",
285
+ help="Configuration key (dot-notation, e.g., 'patterns.prefix')",
286
+ )
287
+ config_get.add_argument(
288
+ "-j", "--json",
289
+ action="store_true",
290
+ help="Output as JSON",
291
+ )
292
+
293
+ # config set
294
+ config_set = config_subparsers.add_parser(
295
+ "set",
296
+ help="Set a configuration value",
297
+ )
298
+ config_set.add_argument(
299
+ "key",
300
+ help="Configuration key (dot-notation, e.g., 'patterns.prefix')",
301
+ )
302
+ config_set.add_argument(
303
+ "value",
304
+ help="Value to set (type auto-detected: true/false, numbers, JSON arrays/objects, or string)",
305
+ )
306
+
307
+ # config unset
308
+ config_unset = config_subparsers.add_parser(
309
+ "unset",
310
+ help="Remove a configuration key",
311
+ )
312
+ config_unset.add_argument(
313
+ "key",
314
+ help="Configuration key to remove",
315
+ )
316
+
317
+ # config add
318
+ config_add = config_subparsers.add_parser(
319
+ "add",
320
+ help="Add a value to an array configuration",
321
+ )
322
+ config_add.add_argument(
323
+ "key",
324
+ help="Configuration key for array (e.g., 'directories.code')",
325
+ )
326
+ config_add.add_argument(
327
+ "value",
328
+ help="Value to add to the array",
329
+ )
330
+
331
+ # config remove
332
+ config_remove = config_subparsers.add_parser(
333
+ "remove",
334
+ help="Remove a value from an array configuration",
335
+ )
336
+ config_remove.add_argument(
337
+ "key",
338
+ help="Configuration key for array (e.g., 'directories.code')",
339
+ )
340
+ config_remove.add_argument(
341
+ "value",
342
+ help="Value to remove from the array",
343
+ )
344
+
345
+ # config path
346
+ config_subparsers.add_parser(
347
+ "path",
348
+ help="Show path to configuration file",
349
+ )
350
+
351
+ # rules command
352
+ rules_parser = subparsers.add_parser(
353
+ "rules",
354
+ help="View and manage content rules",
355
+ )
356
+ rules_subparsers = rules_parser.add_subparsers(dest="rules_action")
357
+
358
+ # rules list
359
+ rules_subparsers.add_parser(
360
+ "list",
361
+ help="List configured content rules",
362
+ )
363
+
364
+ # rules show
365
+ rules_show = rules_subparsers.add_parser(
366
+ "show",
367
+ help="Show content of a content rule file",
368
+ )
369
+ rules_show.add_argument(
370
+ "file",
371
+ help="Content rule file name (e.g., 'AI-AGENT.md')",
372
+ )
373
+
374
+ # mcp command
375
+ mcp_parser = subparsers.add_parser(
376
+ "mcp",
377
+ help="MCP server commands (requires elspais[mcp])",
378
+ formatter_class=argparse.RawDescriptionHelpFormatter,
379
+ epilog="""
380
+ Claude Code Configuration:
381
+ Add to ~/.claude/claude_desktop_config.json:
382
+
383
+ {
384
+ "mcpServers": {
385
+ "elspais": {
386
+ "command": "elspais",
387
+ "args": ["mcp", "serve"],
388
+ "cwd": "/path/to/your/project"
389
+ }
390
+ }
391
+ }
392
+
393
+ Set "cwd" to the directory containing your .elspais.toml config.
394
+
395
+ Resources:
396
+ requirements://all List all requirements
397
+ requirements://{id} Get requirement details
398
+ requirements://level/{level} Filter by PRD/OPS/DEV
399
+ content-rules://list List content rules
400
+ content-rules://{file} Get content rule content
401
+ config://current Current configuration
402
+
403
+ Tools:
404
+ validate Run validation rules
405
+ parse_requirement Parse requirement text
406
+ search Search requirements by pattern
407
+ get_requirement Get requirement details
408
+ analyze Analyze hierarchy/orphans/coverage
409
+ """,
410
+ )
411
+ mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_action")
412
+
413
+ # mcp serve
414
+ mcp_serve = mcp_subparsers.add_parser(
415
+ "serve",
416
+ help="Start MCP server",
417
+ )
418
+ mcp_serve.add_argument(
419
+ "--transport",
420
+ choices=["stdio", "sse", "streamable-http"],
421
+ default="stdio",
422
+ help="Transport type (default: stdio)",
423
+ )
424
+
425
+ return parser
426
+
427
+
428
+ def main(argv: Optional[List[str]] = None) -> int:
429
+ """
430
+ Main entry point for the CLI.
431
+
432
+ Args:
433
+ argv: Command line arguments (defaults to sys.argv[1:])
434
+
435
+ Returns:
436
+ Exit code (0 for success, non-zero for failure)
437
+ """
438
+ parser = create_parser()
439
+ args = parser.parse_args(argv)
440
+
441
+ # Handle no command
442
+ if not args.command:
443
+ parser.print_help()
444
+ return 0
445
+
446
+ try:
447
+ # Dispatch to command handlers
448
+ if args.command == "validate":
449
+ return validate.run(args)
450
+ elif args.command == "trace":
451
+ return trace.run(args)
452
+ elif args.command == "hash":
453
+ return hash_cmd.run(args)
454
+ elif args.command == "index":
455
+ return index.run(args)
456
+ elif args.command == "analyze":
457
+ return analyze.run(args)
458
+ elif args.command == "version":
459
+ return version_command(args)
460
+ elif args.command == "init":
461
+ return init.run(args)
462
+ elif args.command == "edit":
463
+ return edit.run(args)
464
+ elif args.command == "config":
465
+ return config_cmd.run(args)
466
+ elif args.command == "rules":
467
+ return rules_cmd.run(args)
468
+ elif args.command == "mcp":
469
+ return mcp_command(args)
470
+ else:
471
+ parser.print_help()
472
+ return 1
473
+
474
+ except KeyboardInterrupt:
475
+ print("\nOperation cancelled.", file=sys.stderr)
476
+ return 130
477
+ except Exception as e:
478
+ if args.verbose:
479
+ raise
480
+ print(f"Error: {e}", file=sys.stderr)
481
+ return 1
482
+
483
+
484
+ def version_command(args: argparse.Namespace) -> int:
485
+ """Handle version command."""
486
+ print(f"elspais {__version__}")
487
+
488
+ if args.check:
489
+ print("Checking for updates...")
490
+ # TODO: Implement update check
491
+ print("Update check not yet implemented.")
492
+
493
+ return 0
494
+
495
+
496
+ def mcp_command(args: argparse.Namespace) -> int:
497
+ """Handle MCP server commands."""
498
+ try:
499
+ from elspais.mcp.server import run_server
500
+ except ImportError:
501
+ print("Error: MCP dependencies not installed.", file=sys.stderr)
502
+ print("Install with: pip install elspais[mcp]", file=sys.stderr)
503
+ return 1
504
+
505
+ if args.mcp_action == "serve":
506
+ working_dir = Path.cwd()
507
+ if hasattr(args, "spec_dir") and args.spec_dir:
508
+ working_dir = args.spec_dir.parent
509
+
510
+ print(f"Starting elspais MCP server...")
511
+ print(f"Working directory: {working_dir}")
512
+ print(f"Transport: {args.transport}")
513
+
514
+ try:
515
+ run_server(working_dir=working_dir, transport=args.transport)
516
+ except KeyboardInterrupt:
517
+ print("\nServer stopped.")
518
+ return 0
519
+ else:
520
+ print("Usage: elspais mcp serve")
521
+ return 1
522
+
523
+
524
+ if __name__ == "__main__":
525
+ sys.exit(main())
@@ -0,0 +1,12 @@
1
+ """
2
+ elspais.commands - CLI command implementations
3
+ """
4
+
5
+ __all__ = [
6
+ "validate",
7
+ "trace",
8
+ "hash_cmd",
9
+ "index",
10
+ "analyze",
11
+ "init",
12
+ ]