sdlckit 0.2.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 (46) hide show
  1. engine/__init__.py +157 -0
  2. engine/cli.py +246 -0
  3. engine/config/__init__.py +12 -0
  4. engine/config/_secret_detector.py +66 -0
  5. engine/config/loader.py +728 -0
  6. engine/config/plugins.py +224 -0
  7. engine/errors.py +82 -0
  8. engine/init/__init__.py +23 -0
  9. engine/init/config_gen.py +88 -0
  10. engine/init/harness.py +102 -0
  11. engine/init/integration.py +89 -0
  12. engine/init/manifest.py +128 -0
  13. engine/init/scaffolder.py +720 -0
  14. engine/initiative/__init__.py +17 -0
  15. engine/initiative/archive.py +130 -0
  16. engine/initiative/assignment.py +108 -0
  17. engine/initiative/knowledge.py +187 -0
  18. engine/initiative/manifest.py +274 -0
  19. engine/initiative/observation.py +154 -0
  20. engine/initiative/stage_review.py +132 -0
  21. engine/lifecycle/__init__.py +6 -0
  22. engine/lifecycle/context_resolver.py +431 -0
  23. engine/lifecycle/feedback.py +267 -0
  24. engine/lifecycle/manager.py +847 -0
  25. engine/lifecycle/reconciliation.py +308 -0
  26. engine/models/__init__.py +172 -0
  27. engine/models/artifacts.py +93 -0
  28. engine/models/config.py +175 -0
  29. engine/models/feedback.py +85 -0
  30. engine/models/initiative.py +103 -0
  31. engine/models/manifest.py +43 -0
  32. engine/models/plugin.py +45 -0
  33. engine/models/results.py +45 -0
  34. engine/models/skill.py +75 -0
  35. engine/models/state.py +132 -0
  36. engine/normalized_hash.py +21 -0
  37. engine/sage/__init__.py +11 -0
  38. engine/sage/gate_check.py +233 -0
  39. engine/sage/loop_check.py +88 -0
  40. engine/sage/skill_loader.py +276 -0
  41. sdlckit-0.2.0.dist-info/METADATA +226 -0
  42. sdlckit-0.2.0.dist-info/RECORD +46 -0
  43. sdlckit-0.2.0.dist-info/WHEEL +5 -0
  44. sdlckit-0.2.0.dist-info/entry_points.txt +2 -0
  45. sdlckit-0.2.0.dist-info/licenses/LICENSE +190 -0
  46. sdlckit-0.2.0.dist-info/top_level.txt +1 -0
engine/__init__.py ADDED
@@ -0,0 +1,157 @@
1
+ """SDLCKit Engine — config, lifecycle, SAGE loop, initiative orchestration."""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ from engine.config.loader import (
6
+ detect_inline_secrets,
7
+ load_lifecycle_config,
8
+ resolve_env_vars,
9
+ )
10
+ from engine.lifecycle.context_resolver import ContextResolver, build_dependency_graph
11
+ from engine.errors import (
12
+ ConfigError,
13
+ ConfigWarning,
14
+ ContextError,
15
+ CyclicDependencyError,
16
+ PluginError,
17
+ StateTransitionError,
18
+ )
19
+ from engine.lifecycle.feedback import FeedbackManager
20
+ from engine.sage.gate_check import extract_sections, run_gate_pass1
21
+ from engine.sage.loop_check import evaluate_loop_checks
22
+ from engine.normalized_hash import normalized_hash
23
+ from engine.models import (
24
+ Acknowledgment,
25
+ DimensionScore,
26
+ BudgetConfig,
27
+ ConnectorConfig,
28
+ DefaultsConfig,
29
+ DependencyGraph,
30
+ DimensionConfig,
31
+ EngineConfig,
32
+ FeedbackSignal,
33
+ GateCheckConfig,
34
+ GateCheckResult,
35
+ SAGEConfig,
36
+ InputDef,
37
+ LoopCheckConfig,
38
+ LoopCheckResult,
39
+ OverridesConfig,
40
+ PhaseConfig,
41
+ PhaseProvision,
42
+ PhaseRecord,
43
+ PhaseState,
44
+ LifecycleConfig,
45
+ LifecycleState,
46
+ PluginManifest,
47
+ PluginRef,
48
+ ReconciliationBudget,
49
+ ReconciliationPhaseEntry,
50
+ ReconciliationPlan,
51
+ ReconciliationSession,
52
+ RetryPolicy,
53
+ ScorerOutput,
54
+ SkillManifest,
55
+ StageConfig,
56
+ StalenessRecord,
57
+ SkillValidationResult,
58
+ TokenUsage,
59
+ )
60
+ from engine.sage.skill_loader import (
61
+ load_all_skills,
62
+ load_skill,
63
+ scan_required_sections,
64
+ validate_scorer_output,
65
+ validate_skill,
66
+ )
67
+ from engine.config.plugins import (
68
+ get_phase_plugin,
69
+ load_all_plugins,
70
+ load_plugin_manifest,
71
+ )
72
+ from engine.lifecycle.reconciliation import ReconciliationManager
73
+ from engine.lifecycle.manager import StateManager
74
+
75
+ __all__ = [
76
+ # Version
77
+ "__version__",
78
+ # Config Loader
79
+ "load_lifecycle_config",
80
+ "resolve_env_vars",
81
+ "detect_inline_secrets",
82
+ # Plugin Loader
83
+ "load_plugin_manifest",
84
+ "load_all_plugins",
85
+ "get_phase_plugin",
86
+ # State Manager
87
+ "StateManager",
88
+ # Context Resolver
89
+ "ContextResolver",
90
+ "build_dependency_graph",
91
+ # SAGE — Gate Check
92
+ "run_gate_pass1",
93
+ "extract_sections",
94
+ # SAGE — Loop Check
95
+ "evaluate_loop_checks",
96
+ # Feedback & Reconciliation
97
+ "FeedbackManager",
98
+ "ReconciliationManager",
99
+ # Normalized Hash
100
+ "normalized_hash",
101
+ # Errors
102
+ "ConfigError",
103
+ "ConfigWarning",
104
+ "PluginError",
105
+ "StateTransitionError",
106
+ "CyclicDependencyError",
107
+ "ContextError",
108
+ # Models — Config
109
+ "LifecycleConfig",
110
+ "PhaseConfig",
111
+ "SAGEConfig",
112
+ "DimensionConfig",
113
+ "LoopCheckConfig",
114
+ "GateCheckConfig",
115
+ "PluginRef",
116
+ "ConnectorConfig",
117
+ "DefaultsConfig",
118
+ "BudgetConfig",
119
+ "OverridesConfig",
120
+ "EngineConfig",
121
+ "RetryPolicy",
122
+ # Models — Plugin
123
+ "PluginManifest",
124
+ "PhaseProvision",
125
+ "InputDef",
126
+ # Models — State
127
+ "LifecycleState",
128
+ "PhaseRecord",
129
+ "PhaseState",
130
+ "TokenUsage",
131
+ "StalenessRecord",
132
+ "Acknowledgment",
133
+ # Models — Feedback & Reconciliation
134
+ "FeedbackSignal",
135
+ "ReconciliationPlan",
136
+ "ReconciliationPhaseEntry",
137
+ "ReconciliationSession",
138
+ "ReconciliationBudget",
139
+ # Models — Stage
140
+ "StageConfig",
141
+ # Models — Graph
142
+ "DependencyGraph",
143
+ # Models — Results
144
+ "GateCheckResult",
145
+ "LoopCheckResult",
146
+ # SAGE — Skill Loader
147
+ "load_skill",
148
+ "load_all_skills",
149
+ "validate_skill",
150
+ "validate_scorer_output",
151
+ "scan_required_sections",
152
+ # Models — Skill
153
+ "SkillManifest",
154
+ "DimensionScore",
155
+ "ScorerOutput",
156
+ "SkillValidationResult",
157
+ ]
engine/cli.py ADDED
@@ -0,0 +1,246 @@
1
+ """SDLCKit CLI — entry point for sdlckit commands.
2
+
3
+ Provides the `sdlckit` console script registered via pyproject.toml.
4
+ Currently supports a single subcommand:
5
+
6
+ sdlckit init [TARGET] [--force] [--minimal]
7
+
8
+ The CLI parses arguments with argparse, delegates to the appropriate
9
+ engine module, and translates return values into shell exit codes:
10
+ 0 — success (all files processed)
11
+ 1 — partial success (some files skipped due to user modifications)
12
+ 2 — fatal error (target not found, unrecoverable failure)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import sys
19
+
20
+ import engine
21
+
22
+
23
+ def _build_parser() -> argparse.ArgumentParser:
24
+ parser = argparse.ArgumentParser(
25
+ prog="sdlckit",
26
+ description="SDLCKit — AI-governed SDLC lifecycle engine",
27
+ )
28
+ parser.add_argument(
29
+ "--version", "-V",
30
+ action="version",
31
+ version=f"sdlckit {engine.__version__}",
32
+ )
33
+
34
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
35
+
36
+ # init subcommand
37
+ init_parser = subparsers.add_parser(
38
+ "init",
39
+ help="Initialize a repository for SDLCKit",
40
+ description="Scaffold a repository with SDLCKit runtime files, lifecycle config, and integration.",
41
+ )
42
+ init_parser.add_argument(
43
+ "target",
44
+ nargs="?",
45
+ default=".",
46
+ help="Target directory to initialize (default: current directory)",
47
+ )
48
+ init_parser.add_argument(
49
+ "--force", "-f",
50
+ action="store_true",
51
+ help="Overwrite all files including user-modified; creates .bak backups",
52
+ )
53
+ init_parser.add_argument(
54
+ "--minimal", "-m",
55
+ action="store_true",
56
+ help="Install only schemas, lifecycle config, and integration file",
57
+ )
58
+ init_parser.add_argument(
59
+ "--type", "-t",
60
+ dest="init_type",
61
+ choices=["component", "initiative"],
62
+ default="component",
63
+ help="Project type: component (default) or initiative",
64
+ )
65
+ init_parser.add_argument(
66
+ "--harness",
67
+ choices=["claude", "codex", "copilot", "cursor", "windsurf", "generic"],
68
+ default=None,
69
+ help="Target AI harness (auto-detected if omitted)",
70
+ )
71
+
72
+ # observe subcommand
73
+ observe_parser = subparsers.add_parser(
74
+ "observe",
75
+ help="Create a new observation",
76
+ description="Record an operational observation for cross-phase feedback.",
77
+ )
78
+ observe_parser.add_argument("title", help="Observation title")
79
+ observe_parser.add_argument("--description", "-d", default="", help="Observation body text")
80
+ observe_parser.add_argument(
81
+ "--source", "-s",
82
+ choices=["incident", "monitoring", "customer", "post-mortem", "manual"],
83
+ default="manual",
84
+ help="Observation source (default: manual)",
85
+ )
86
+ observe_parser.add_argument(
87
+ "--severity",
88
+ choices=["low", "medium", "high"],
89
+ default="medium",
90
+ help="Severity level (default: medium)",
91
+ )
92
+ observe_parser.add_argument(
93
+ "--components", "-c",
94
+ nargs="+",
95
+ required=True,
96
+ help="Affected component names",
97
+ )
98
+ observe_parser.add_argument("--tags", nargs="*", default=[], help="Optional tags")
99
+ observe_parser.add_argument(
100
+ "--state-dir",
101
+ default=".sdlckit",
102
+ help="State directory (default: .sdlckit)",
103
+ )
104
+
105
+ # assign subcommand
106
+ assign_parser = subparsers.add_parser(
107
+ "assign",
108
+ help="Import an assignment into a component repo",
109
+ description="Import an assignment file, creating a work directory and branch.",
110
+ )
111
+ assign_parser.add_argument("path", help="Path to the assignment markdown file")
112
+ assign_parser.add_argument(
113
+ "--work-dir",
114
+ default=".sdlckit/work",
115
+ help="Work directory (default: .sdlckit/work)",
116
+ )
117
+
118
+ # archive subcommand
119
+ archive_parser = subparsers.add_parser(
120
+ "archive",
121
+ help="Pre-merge cleanup for initiative or component repos",
122
+ description="Archive an initiative or component work item, updating manifests and cleaning up runtime state.",
123
+ )
124
+ archive_parser.add_argument("name", help="Initiative name or work item name to archive")
125
+ archive_parser.add_argument(
126
+ "--mode",
127
+ choices=["initiative", "component"],
128
+ default=None,
129
+ help="Archive mode (auto-detected if not specified)",
130
+ )
131
+ archive_parser.add_argument(
132
+ "--project-root",
133
+ default=".",
134
+ help="Project root directory (default: current directory)",
135
+ )
136
+
137
+ return parser
138
+
139
+
140
+ def main(argv: list[str] | None = None) -> int:
141
+ """CLI entry point. Returns exit code."""
142
+ parser = _build_parser()
143
+ args = parser.parse_args(argv)
144
+
145
+ if args.command is None:
146
+ parser.print_help()
147
+ return 0
148
+
149
+ if args.command == "init":
150
+ if args.init_type == "initiative":
151
+ from engine.init.scaffolder import run_init_initiative
152
+ return run_init_initiative(target_dir=args.target)
153
+ from engine.init.harness import detect_harness
154
+ from engine.init.scaffolder import run_init
155
+ harness = args.harness
156
+ if harness is None:
157
+ from pathlib import Path
158
+ harness = detect_harness(Path(args.target).resolve())
159
+ if harness is None:
160
+ print("Could not auto-detect AI harness. Use --harness to specify one of: "
161
+ "claude, codex, copilot, cursor, windsurf, generic", file=sys.stderr)
162
+ return 2
163
+ return run_init(
164
+ target_dir=args.target,
165
+ force=args.force,
166
+ minimal=args.minimal,
167
+ harness=harness,
168
+ )
169
+
170
+ if args.command == "observe":
171
+ from pathlib import Path
172
+ from engine.initiative.observation import create_observation
173
+ obs_dir = Path(args.state_dir) / "observations"
174
+ try:
175
+ path = create_observation(
176
+ obs_dir,
177
+ title=args.title,
178
+ description=args.description,
179
+ source=args.source,
180
+ severity=args.severity,
181
+ components=args.components,
182
+ tags=args.tags,
183
+ )
184
+ print(f"Observation created: {path}")
185
+ return 0
186
+ except Exception as e:
187
+ print(f"Error: {e}", file=sys.stderr)
188
+ return 2
189
+
190
+ if args.command == "assign":
191
+ from pathlib import Path
192
+ from engine.initiative.assignment import import_assignment
193
+ try:
194
+ dest = import_assignment(Path(args.path), Path(args.work_dir))
195
+ print(f"Assignment imported: {dest}")
196
+ return 0
197
+ except Exception as e:
198
+ print(f"Error: {e}", file=sys.stderr)
199
+ return 2
200
+
201
+ if args.command == "archive":
202
+ from pathlib import Path
203
+ from engine.initiative.archive import archive_component, archive_initiative
204
+ root = Path(args.project_root)
205
+ mode = args.mode
206
+ if mode is None:
207
+ if (root / "sdlc-initiative.yaml").exists():
208
+ mode = "initiative"
209
+ elif (root / "sdlc.yaml").exists():
210
+ mode = "component"
211
+ else:
212
+ print("Error: cannot detect mode — no sdlc.yaml or sdlc-initiative.yaml found", file=sys.stderr)
213
+ return 2
214
+ try:
215
+ if mode == "initiative":
216
+ result = archive_initiative(args.name, root)
217
+ else:
218
+ result = archive_component(args.name, root)
219
+ if result.warnings:
220
+ for w in result.warnings:
221
+ print(f"Warning: {w}", file=sys.stderr)
222
+ if result.manifest_updated:
223
+ print(f"Manifest updated: {args.name} → archived")
224
+ if result.profile_diffs:
225
+ print(f"Profile diffs to review: {len(result.profile_diffs)}")
226
+ for d in result.profile_diffs:
227
+ print(f" {d}")
228
+ if result.knowledge_diffs:
229
+ print(f"Knowledge diffs to review: {len(result.knowledge_diffs)}")
230
+ for d in result.knowledge_diffs:
231
+ print(f" {d}")
232
+ if result.dirs_removed:
233
+ print(f"Directories removed: {len(result.dirs_removed)}")
234
+ for d in result.dirs_removed:
235
+ print(f" {d}")
236
+ return 0
237
+ except Exception as e:
238
+ print(f"Error: {e}", file=sys.stderr)
239
+ return 2
240
+
241
+ parser.print_help()
242
+ return 0
243
+
244
+
245
+ if __name__ == "__main__":
246
+ sys.exit(main())
@@ -0,0 +1,12 @@
1
+ """Configuration loading — lifecycle config, plugins, secret detection."""
2
+
3
+ from engine.config.loader import (
4
+ detect_inline_secrets,
5
+ load_lifecycle_config,
6
+ resolve_env_vars,
7
+ )
8
+ from engine.config.plugins import (
9
+ get_phase_plugin,
10
+ load_all_plugins,
11
+ load_plugin_manifest,
12
+ )
@@ -0,0 +1,66 @@
1
+ """Inline secret pattern detection (internal module)."""
2
+
3
+ import math
4
+ import re
5
+
6
+ KNOWN_PREFIXES = (
7
+ "xoxb-",
8
+ "xoxp-",
9
+ "xoxa-",
10
+ "ghp_",
11
+ "gho_",
12
+ "ghs_",
13
+ "sk-",
14
+ "AKIA",
15
+ "rk_live_",
16
+ "rk_test_",
17
+ "pk_live_",
18
+ "pk_test_",
19
+ )
20
+
21
+ _BASE64_PATTERN = re.compile(r"^[A-Za-z0-9+/=]{32,}$")
22
+ _ALNUM_PATTERN = re.compile(r"^[A-Za-z0-9]{40,}$")
23
+
24
+
25
+ def _shannon_entropy(s: str) -> float:
26
+ """Calculate Shannon entropy in bits per character."""
27
+ if not s:
28
+ return 0.0
29
+ freq: dict[str, int] = {}
30
+ for c in s:
31
+ freq[c] = freq.get(c, 0) + 1
32
+ length = len(s)
33
+ return -sum(
34
+ (count / length) * math.log2(count / length) for count in freq.values()
35
+ )
36
+
37
+
38
+ def detect_inline_secret(value: str) -> bool:
39
+ """Check if a string value matches known secret patterns.
40
+
41
+ Returns True if value matches:
42
+ - Known token prefixes (Slack, GitHub, OpenAI, AWS, Stripe)
43
+ - High-entropy alphanumeric strings (>40 chars, entropy > 4.0 bits/char)
44
+ - Base64 blobs (>32 chars matching base64 character set)
45
+ """
46
+ if not isinstance(value, str):
47
+ return False
48
+
49
+ # Skip env var references — they're the recommended approach
50
+ if value.startswith("${") and value.endswith("}"):
51
+ return False
52
+
53
+ # Check known prefixes
54
+ for prefix in KNOWN_PREFIXES:
55
+ if value.startswith(prefix):
56
+ return True
57
+
58
+ # Check high-entropy alphanumeric strings
59
+ if _ALNUM_PATTERN.match(value) and _shannon_entropy(value) > 4.0:
60
+ return True
61
+
62
+ # Check base64 blobs
63
+ if _BASE64_PATTERN.match(value):
64
+ return True
65
+
66
+ return False