specfact-cli 0.48.2__py3-none-any.whl → 0.49.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.
specfact_cli/__init__.py CHANGED
@@ -76,6 +76,6 @@ def _install_progressive_disclosure() -> None:
76
76
  # keeps missing-command and missing-parameter UX consistent outside the root CLI too.
77
77
  _install_progressive_disclosure()
78
78
 
79
- __version__ = "0.48.2"
79
+ __version__ = "0.49.1"
80
80
 
81
81
  __all__ = ["__version__"]
@@ -128,10 +128,11 @@ class CodeAnalyzer:
128
128
  def _resolve_analyzer_entry_point(repo_path: Path, entry_point: Path | None) -> Path | None:
129
129
  if entry_point is None:
130
130
  return None
131
- resolved = entry_point if entry_point.is_absolute() else (repo_path / entry_point).resolve()
131
+ resolved_repo_path = repo_path.resolve()
132
+ resolved = entry_point.resolve() if entry_point.is_absolute() else (resolved_repo_path / entry_point).resolve()
132
133
  if not resolved.exists():
133
134
  raise ValueError(f"Entry point does not exist: {resolved}")
134
- if not str(resolved).startswith(str(repo_path)):
135
+ if not resolved.is_relative_to(resolved_repo_path):
135
136
  raise ValueError(f"Entry point must be within repository: {resolved}")
136
137
  return resolved
137
138
 
@@ -37,6 +37,22 @@ from specfact_cli.models.project import (
37
37
  SectionLock,
38
38
  )
39
39
  from specfact_cli.models.protocol import Protocol, Transition
40
+ from specfact_cli.models.requirements import (
41
+ REQUIREMENTS_INPUT_SCHEMA_VERSION,
42
+ BusinessRule,
43
+ RequirementCompletenessFinding,
44
+ RequirementCompletenessSeverity,
45
+ RequirementConstraint,
46
+ RequirementConstraintType,
47
+ RequirementEvidenceLink,
48
+ RequirementEvidenceLinkType,
49
+ RequirementInput,
50
+ RequirementsInputExtensionPayload,
51
+ RequirementSourceReference,
52
+ RequirementSourceType,
53
+ load_requirements_input_extension,
54
+ requirements_input_extension_payload,
55
+ )
40
56
  from specfact_cli.models.sdd import (
41
57
  SDDCoverageThresholds,
42
58
  SDDEnforcementBudget,
@@ -49,6 +65,7 @@ from specfact_cli.models.source_tracking import SourceTracking
49
65
 
50
66
 
51
67
  __all__ = [
68
+ "REQUIREMENTS_INPUT_SCHEMA_VERSION",
52
69
  "AdapterType",
53
70
  "ArtifactMapping",
54
71
  "BridgeConfig",
@@ -57,6 +74,7 @@ __all__ = [
57
74
  "BundleManifest",
58
75
  "BundleVersions",
59
76
  "Business",
77
+ "BusinessRule",
60
78
  "ChangeArchive",
61
79
  "ChangeProposal",
62
80
  "ChangeTracking",
@@ -84,6 +102,16 @@ __all__ = [
84
102
  "Protocol",
85
103
  "ProtocolIndex",
86
104
  "Release",
105
+ "RequirementCompletenessFinding",
106
+ "RequirementCompletenessSeverity",
107
+ "RequirementConstraint",
108
+ "RequirementConstraintType",
109
+ "RequirementEvidenceLink",
110
+ "RequirementEvidenceLinkType",
111
+ "RequirementInput",
112
+ "RequirementSourceReference",
113
+ "RequirementSourceType",
114
+ "RequirementsInputExtensionPayload",
87
115
  "SDDCoverageThresholds",
88
116
  "SDDEnforcementBudget",
89
117
  "SDDHow",
@@ -100,4 +128,6 @@ __all__ = [
100
128
  "TemplateSection",
101
129
  "Transition",
102
130
  "ValidationReport",
131
+ "load_requirements_input_extension",
132
+ "requirements_input_extension_payload",
103
133
  ]
@@ -0,0 +1,183 @@
1
+ """Requirement evidence input models.
2
+
3
+ These models normalize upstream requirement context for validation evidence.
4
+ They do not make SpecFact the authoring system or source of truth for
5
+ requirements.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import StrEnum
11
+ from typing import Any, Literal, cast
12
+
13
+ from beartype import beartype
14
+ from icontract import ensure, require
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+
17
+
18
+ REQUIREMENTS_INPUT_SCHEMA_VERSION = "1"
19
+
20
+
21
+ class RequirementSourceType(StrEnum):
22
+ """Supported upstream source reference types."""
23
+
24
+ ISSUE = "issue"
25
+ DOCUMENT = "document"
26
+ OPENSPEC_CHANGE = "openspec_change"
27
+ SPECKIT_SPEC = "speckit_spec"
28
+ FILE = "file"
29
+ BACKLOG_ITEM = "backlog_item"
30
+
31
+
32
+ class RequirementEvidenceLinkType(StrEnum):
33
+ """Kinds of downstream evidence linked from a requirement input."""
34
+
35
+ ARCHITECTURE = "architecture"
36
+ SPEC = "spec"
37
+ CODE = "code"
38
+ TEST = "test"
39
+ VALIDATION = "validation"
40
+ REQUIREMENT = "requirement"
41
+
42
+
43
+ class RequirementConstraintType(StrEnum):
44
+ """Supported requirement constraint categories."""
45
+
46
+ PERFORMANCE = "performance"
47
+ SECURITY = "security"
48
+ INTEGRATION = "integration"
49
+ COMPLIANCE = "compliance"
50
+ UX = "ux"
51
+ OPERATIONAL = "operational"
52
+
53
+
54
+ class RequirementCompletenessSeverity(StrEnum):
55
+ """Advisory completeness finding severity."""
56
+
57
+ INFO = "info"
58
+ WARNING = "warning"
59
+ ERROR = "error"
60
+
61
+
62
+ @beartype
63
+ class RequirementSourceReference(BaseModel):
64
+ """Reference to an upstream requirement source."""
65
+
66
+ model_config = ConfigDict(use_enum_values=True)
67
+
68
+ source_type: RequirementSourceType = Field(..., description="Type of upstream requirement source")
69
+ locator: str = Field(..., min_length=1, description="Source locator such as URL, file path, or change ID")
70
+ title: str | None = Field(default=None, description="Human-readable source title")
71
+ revision: str | None = Field(default=None, description="Optional source revision, commit, or version")
72
+
73
+
74
+ @beartype
75
+ class BusinessRule(BaseModel):
76
+ """Business rule imported from upstream requirement context."""
77
+
78
+ rule_id: str = Field(..., min_length=1, description="Stable business rule identifier")
79
+ name: str = Field(..., min_length=1, description="Rule name")
80
+ given: str = Field(..., min_length=1, description="Given clause for validation scenario context")
81
+ when: str = Field(..., min_length=1, description="When clause for validation scenario context")
82
+ then: str = Field(..., min_length=1, description="Then clause for validation scenario context")
83
+ priority: str | None = Field(default=None, description="Optional upstream priority such as MoSCoW")
84
+
85
+
86
+ @beartype
87
+ class RequirementConstraint(BaseModel):
88
+ """Constraint imported from upstream requirement context."""
89
+
90
+ model_config = ConfigDict(use_enum_values=True)
91
+
92
+ constraint_id: str = Field(..., min_length=1, description="Stable constraint identifier")
93
+ constraint_type: RequirementConstraintType = Field(..., description="Constraint category")
94
+ statement: str = Field(..., min_length=1, description="Constraint statement")
95
+ validation_criteria: list[str] = Field(default_factory=list, description="Criteria used to validate constraint")
96
+
97
+
98
+ @beartype
99
+ class RequirementEvidenceLink(BaseModel):
100
+ """Link from requirement input to downstream validation evidence."""
101
+
102
+ model_config = ConfigDict(use_enum_values=True)
103
+
104
+ link_type: RequirementEvidenceLinkType = Field(..., description="Evidence target category")
105
+ target: str = Field(..., min_length=1, description="Evidence target locator")
106
+ relation: str = Field(default="references", min_length=1, description="Relationship to the target")
107
+
108
+
109
+ @beartype
110
+ class RequirementCompletenessFinding(BaseModel):
111
+ """Profile-aware completeness finding stored as advisory evidence."""
112
+
113
+ model_config = ConfigDict(use_enum_values=True)
114
+
115
+ profile: str = Field(..., min_length=1, description="Profile that produced this finding")
116
+ severity: RequirementCompletenessSeverity = Field(..., description="Finding severity")
117
+ field_path: str = Field(..., min_length=1, description="Requirement field path")
118
+ message: str = Field(..., min_length=1, description="Human-readable completeness guidance")
119
+
120
+
121
+ @beartype
122
+ class RequirementInput(BaseModel):
123
+ """Normalized requirement context consumed by validation evidence."""
124
+
125
+ schema_version: Literal["1"] = Field(..., description="Requirement input schema version")
126
+ requirement_id: str = Field(..., min_length=1, description="Stable requirement identifier")
127
+ title: str = Field(..., min_length=1, description="Requirement title")
128
+ summary: str | None = Field(default=None, description="Optional upstream requirement summary")
129
+ sources: list[RequirementSourceReference] = Field(
130
+ ...,
131
+ min_length=1,
132
+ description="Upstream requirement source references",
133
+ )
134
+ business_rules: list[BusinessRule] = Field(default_factory=list, description="Imported business rules")
135
+ constraints: list[RequirementConstraint] = Field(default_factory=list, description="Imported constraints")
136
+ evidence_links: list[RequirementEvidenceLink] = Field(default_factory=list, description="Evidence target links")
137
+ completeness_findings: list[RequirementCompletenessFinding] = Field(
138
+ default_factory=list,
139
+ description="Profile-aware advisory completeness findings",
140
+ )
141
+
142
+
143
+ @beartype
144
+ class RequirementsInputExtensionPayload(BaseModel):
145
+ """Serializable ProjectBundle extension payload for requirement inputs."""
146
+
147
+ schema_version: Literal["1"] = Field(..., description="Requirements extension payload schema version")
148
+ requirements: list[RequirementInput] = Field(default_factory=list, description="Requirement input records")
149
+
150
+
151
+ def _payload_has_schema_version(result: dict[str, Any]) -> bool:
152
+ return result.get("schema_version") == REQUIREMENTS_INPUT_SCHEMA_VERSION
153
+
154
+
155
+ def _payload_has_requirements_list(result: dict[str, Any]) -> bool:
156
+ return isinstance(result.get("requirements"), list)
157
+
158
+
159
+ def _payload_is_valid_extension(payload: dict[str, Any]) -> bool:
160
+ try:
161
+ RequirementsInputExtensionPayload.model_validate(payload)
162
+ except ValueError:
163
+ return False
164
+ return True
165
+
166
+
167
+ @beartype
168
+ @require(lambda records: all(isinstance(record, RequirementInput) for record in records))
169
+ @ensure(_payload_has_schema_version)
170
+ @ensure(_payload_has_requirements_list)
171
+ def requirements_input_extension_payload(records: list[RequirementInput]) -> dict[str, Any]:
172
+ """Return a JSON-serializable ProjectBundle extension payload."""
173
+ payload = RequirementsInputExtensionPayload(schema_version=REQUIREMENTS_INPUT_SCHEMA_VERSION, requirements=records)
174
+ return cast(dict[str, Any], payload.model_dump(mode="json"))
175
+
176
+
177
+ @beartype
178
+ @require(lambda payload: isinstance(payload, dict))
179
+ @require(_payload_is_valid_extension)
180
+ @ensure(lambda result: all(isinstance(record, RequirementInput) for record in result))
181
+ def load_requirements_input_extension(payload: dict[str, Any]) -> list[RequirementInput]:
182
+ """Load requirement input records from a ProjectBundle extension payload."""
183
+ return RequirementsInputExtensionPayload.model_validate(payload).requirements
@@ -91,11 +91,26 @@ def _append_explicit_module_roots(roots: list[tuple[str, Path]]) -> None:
91
91
  roots.append(("custom", path))
92
92
 
93
93
 
94
+ def _should_include_workspace_project_root(options: _DiscoveryRootOptions, legacy: bool) -> bool:
95
+ only_user_root_is_explicit = (
96
+ options.user_root is not None
97
+ and options.builtin_root is None
98
+ and options.marketplace_root is None
99
+ and options.custom_root is None
100
+ )
101
+ return options.project_base_path is not None or legacy or only_user_root_is_explicit
102
+
103
+
94
104
  def _discovery_root_list(options: _DiscoveryRootOptions) -> list[tuple[str, Path]]:
95
105
  from specfact_cli.registry.module_packages import get_modules_root, get_workspace_modules_root
96
106
 
107
+ legacy = _resolve_include_legacy_roots(options)
97
108
  effective_builtin_root = options.builtin_root or get_modules_root()
98
- effective_project_root = get_workspace_modules_root(options.project_base_path)
109
+ effective_project_root = (
110
+ get_workspace_modules_root(options.project_base_path)
111
+ if _should_include_workspace_project_root(options, legacy)
112
+ else None
113
+ )
99
114
  effective_user_root = options.user_root or USER_MODULES_ROOT
100
115
  effective_marketplace_root = options.marketplace_root or MARKETPLACE_MODULES_ROOT
101
116
  effective_custom_root = options.custom_root or CUSTOM_MODULES_ROOT
@@ -119,7 +134,6 @@ def _discovery_root_list(options: _DiscoveryRootOptions) -> list[tuple[str, Path
119
134
  ]
120
135
  )
121
136
 
122
- legacy = _resolve_include_legacy_roots(options)
123
137
  if legacy:
124
138
  _append_legacy_module_roots(roots)
125
139
  return roots
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.48.2
3
+ Version: 0.49.1
4
4
  Summary: AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, and spec/contract evidence for AI-assisted and brownfield delivery.
5
5
  Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
6
6
  Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
@@ -1,4 +1,4 @@
1
- specfact_cli/__init__.py,sha256=T-PnD8QbK4vSRTj5J_c5wvngcQDPQQjm8edqWjp27fc,2764
1
+ specfact_cli/__init__.py,sha256=7OrDXEh52sHjgy1H02roL4E79_jj3YaU1TrTSil0TRk,2764
2
2
  specfact_cli/__main__.py,sha256=EpP5xlutNYI5AuMlMw8fGt1TywZUR1-CXHpzpWKyNuo,141
3
3
  specfact_cli/cli.py,sha256=IlAtbxyaLxhj2FCteq7P1GRxME4nl_3OiRb3gY9gKDw,53060
4
4
  specfact_cli/runtime.py,sha256=4tEcOsz2bE6-KuZF5nuHvHuwinO7l8fNAGzrra9pNrc,13474
@@ -20,7 +20,7 @@ specfact_cli/agents/registry.py,sha256=NE2WzNUpgNDAhl5BIqxVoAPyjLALdcf13Jwddah7i
20
20
  specfact_cli/agents/sync_agent.py,sha256=AWp0k5AGaaKJvhpQ7rss3vCxG78hYRgskNU1LBfNABo,4351
21
21
  specfact_cli/analyzers/__init__.py,sha256=p-fms9MQ_euOMklUXNBk_LH4htlWSWldkFvXF1jreMw,357
22
22
  specfact_cli/analyzers/ambiguity_scanner.py,sha256=1rxDPjf56C4MrypJWutkXhbV1lS7uN0m7UHo--LCM8c,40334
23
- specfact_cli/analyzers/code_analyzer.py,sha256=LH1_nkOa2i9zqUEo8bAHqxyB9HHAgcbc0i-bYRSPbTo,84125
23
+ specfact_cli/analyzers/code_analyzer.py,sha256=nxf7aB_-ujYRyZwGKr7lmzf2Y46JtTZDFYRyRf6f39w,84196
24
24
  specfact_cli/analyzers/constitution_evidence_extractor.py,sha256=gkgSFfkDsQ3RtXFkTudz30XwUZ3qrhhg6ph256nb_-s,22110
25
25
  specfact_cli/analyzers/contract_extractor.py,sha256=MuuMCwsHbKnLZRQvihnxs2-Y5f2za6IxbugwEVJDuNQ,16895
26
26
  specfact_cli/analyzers/control_flow_analyzer.py,sha256=4xHFcdgrASFPDeLUEkSULii3ylRPdl__09ER3t2hrc0,10930
@@ -94,7 +94,7 @@ specfact_cli/merge/__init__.py,sha256=GQDDqCeJw0YbtNmZzIFKXyviFyeoAPvBfPSAPGx0F2
94
94
  specfact_cli/merge/resolver.py,sha256=Z5rEPLETjIPwuJmX-Q8tF7_t0eRQ5A2P3Cj3GjIgjCw,21904
95
95
  specfact_cli/migrations/__init__.py,sha256=ymRFb3SSq8YMVhOwqFjWDp9jVQDwpE-QBioVhkihzmA,421
96
96
  specfact_cli/migrations/plan_migrator.py,sha256=VO-_mpt-_rhT9XPAFHI32wY_3YluTtU2oCyOvMDt0-g,7875
97
- specfact_cli/models/__init__.py,sha256=p03_0CWEe_eHzF37ZcWOHX1dwZxs3y_fcSlvUdl_Ckc,2457
97
+ specfact_cli/models/__init__.py,sha256=TPXYV5HDd7vIPYXpjZv6WwKyoKCicgwDnwvZyP8I36I,3436
98
98
  specfact_cli/models/backlog_item.py,sha256=lZ4oWzUCgyB0X0EKd-X2NI2XdoSQ4znT95xX7DNHnVA,5589
99
99
  specfact_cli/models/bridge.py,sha256=D79o6KacsQ8vuBGZWGFsb8brx6lBmboHLIUEYNdkdEE,21356
100
100
  specfact_cli/models/capabilities.py,sha256=o2Ob_7vpfteS_-GPMv54eZ-sV13rhnQywrFAzvQGiJU,1298
@@ -109,6 +109,7 @@ specfact_cli/models/plan.py,sha256=6vP2MW-n8L9yxM4RQ6SeE-LQUtnB1eRk1pdXCxObJ6I,1
109
109
  specfact_cli/models/project.py,sha256=pYEtTLmZqv3Dnd_9FB6zHQ1dw5xRwyZN_5uU8DUNkQg,37205
110
110
  specfact_cli/models/protocol.py,sha256=s9SpUIH9qf0W0Io_wWEYjERTdf1VOowq6rj20I3p9To,900
111
111
  specfact_cli/models/quality.py,sha256=c12xINjFoW8mEZMNH4iwdS0ZJeKaJaV6-4decFisYMY,1050
112
+ specfact_cli/models/requirements.py,sha256=RFl0ZuMf-dHQ2ZVSVmjBo5514JaGuZ00TmAoobQLKt0,7217
112
113
  specfact_cli/models/sdd.py,sha256=5UMfoUkcXnkR9khTZZf1UJq84avyvLC8TdMbVVFQUvg,5978
113
114
  specfact_cli/models/source_tracking.py,sha256=sKm-_tJL0rANqKKwMeBV5mXjDVIflb4_S4WV23e6Tmg,4538
114
115
  specfact_cli/models/task.py,sha256=Xa0B1EPCXrFaXGRxMyjFWRG79-a_DPlVSDeZGET9F5E,4560
@@ -146,7 +147,7 @@ specfact_cli/registry/help_cache.py,sha256=fsy8gSUni3yIRFAzrmb-ybmum2EB6uL5Ys2a8
146
147
  specfact_cli/registry/marketplace_client.py,sha256=BH1WKr80UccV4VX-N2sL0IxQIC_EtmAbv_XVI28szZQ,13113
147
148
  specfact_cli/registry/metadata.py,sha256=mSIW8UZgBzBdiAH_52K8GL-VBMgZoyGQXIXg0bsl1Oc,806
148
149
  specfact_cli/registry/module_availability.py,sha256=_fWsXUcWVY-aVhkjtKrYfb3Qkr80KsZ8P0y75PXCW-8,8115
149
- specfact_cli/registry/module_discovery.py,sha256=ga4w8OCVySExDGeWYp_Qo6OjSjU5OVDPWO5g2tqgX6A,8822
150
+ specfact_cli/registry/module_discovery.py,sha256=kQ9DxfxxHRBaGncjDg8j6Owc3npIWcYOA_5MDurV5Ic,9317
150
151
  specfact_cli/registry/module_grouping.py,sha256=Qly2YKxlsn1Wt9hDRlHMMZeui85w3KbbT86VAJ4Mz2o,3186
151
152
  specfact_cli/registry/module_installer.py,sha256=72OlptZI43UrHr-3_5a6QyyL7vBU51ojuBpM5AnH5vc,45148
152
153
  specfact_cli/registry/module_lifecycle.py,sha256=17QhSER6lQV4JeTn3rvF6lsFRTIjT_UrIzMGpVveFEw,9541
@@ -287,8 +288,8 @@ specfact_cli/resources/templates/policies/kanban.yaml,sha256=9jt7YYfZ8e8aB5skY2D
287
288
  specfact_cli/resources/templates/policies/mixed.yaml,sha256=-fkrT-TLKLTBzjKeKr8bEJHFC-nh1TlyLOEFfgRdSi8,243
288
289
  specfact_cli/resources/templates/policies/safe.yaml,sha256=uiwe5_ABvaU4nmq6rsLUn8zRotkBITItPUsdPZPTHTw,157
289
290
  specfact_cli/resources/templates/policies/scrum.yaml,sha256=6FHYOntkjCxHAn-ga14IQzA1PvYUJgxfLsEQYtWJILM,188
290
- specfact_cli-0.48.2.dist-info/METADATA,sha256=DMH1iUe0ytrB7rMelRHOxJfH1pTlrx6amYVQb-H8wf0,25589
291
- specfact_cli-0.48.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
292
- specfact_cli-0.48.2.dist-info/entry_points.txt,sha256=pwDD5Ttu6AF3p3T05M4G0aR2BELFyUu3214kjMRGEow,96
293
- specfact_cli-0.48.2.dist-info/licenses/LICENSE,sha256=nWX_6oozyEFlF3mDaSWyBD9HLpyLRmCYYqdcgoVOrOk,11361
294
- specfact_cli-0.48.2.dist-info/RECORD,,
291
+ specfact_cli-0.49.1.dist-info/METADATA,sha256=8svwBEohFvLuqWv7MknkjBJeUvylfTQqTYiy8IOQb-M,25589
292
+ specfact_cli-0.49.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
293
+ specfact_cli-0.49.1.dist-info/entry_points.txt,sha256=pwDD5Ttu6AF3p3T05M4G0aR2BELFyUu3214kjMRGEow,96
294
+ specfact_cli-0.49.1.dist-info/licenses/LICENSE,sha256=nWX_6oozyEFlF3mDaSWyBD9HLpyLRmCYYqdcgoVOrOk,11361
295
+ specfact_cli-0.49.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any