crucible-forge 0.1.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.
- crucible/__init__.py +57 -0
- crucible/_spec.py +20 -0
- crucible/api/__init__.py +25 -0
- crucible/api/dispatcher.py +59 -0
- crucible/api/input_validation.py +54 -0
- crucible/api/phase_classification.py +14 -0
- crucible/api/phases/__init__.py +19 -0
- crucible/api/phases/_common.py +103 -0
- crucible/api/phases/cross_validation.py +70 -0
- crucible/api/phases/epic_decomposition.py +40 -0
- crucible/api/phases/epic_expansion.py +99 -0
- crucible/api/phases/planning_spec.py +82 -0
- crucible/api/phases/ticket_decomposition.py +47 -0
- crucible/api/phases/ticket_expansion.py +98 -0
- crucible/api/scope_resolver.py +70 -0
- crucible/api/validate.py +83 -0
- crucible/api/version.py +7 -0
- crucible/checks/__init__.py +14 -0
- crucible/checks/coverage.py +148 -0
- crucible/checks/na_validation.py +77 -0
- crucible/config/__init__.py +35 -0
- crucible/config/data/defaults.yml +198 -0
- crucible/config/defaults.py +185 -0
- crucible/config/load.py +45 -0
- crucible/config/merge.py +39 -0
- crucible/config/overrides.py +34 -0
- crucible/config/resolver.py +66 -0
- crucible/config/schema.py +106 -0
- crucible/config/snapshot.py +11 -0
- crucible/cross_validation/__init__.py +108 -0
- crucible/cross_validation/blueprint.py +61 -0
- crucible/cross_validation/dependency_graph.py +212 -0
- crucible/cross_validation/emission.py +13 -0
- crucible/cross_validation/files.py +154 -0
- crucible/cross_validation/ratios/__init__.py +0 -0
- crucible/cross_validation/topology.py +118 -0
- crucible/cross_validation/waves.py +237 -0
- crucible/engines/__init__.py +20 -0
- crucible/engines/ratios.py +85 -0
- crucible/engines/wave_calculator.py +85 -0
- crucible/guidance/__init__.py +0 -0
- crucible/guidance/composer/__init__.py +44 -0
- crucible/guidance/composer/patterns.py +304 -0
- crucible/guidance/engine/__init__.py +21 -0
- crucible/guidance/engine/applicability.py +24 -0
- crucible/guidance/engine/finding_engine.py +193 -0
- crucible/guidance/engine/resolver.py +73 -0
- crucible/guidance/engine/structural_checks.py +123 -0
- crucible/guidance/engine/value_resolver.py +31 -0
- crucible/guidance/fixed_prompts.py +52 -0
- crucible/guidance/format/__init__.py +68 -0
- crucible/guidance/format/individual_formatter.py +126 -0
- crucible/guidance/format/operations_vocabulary.py +29 -0
- crucible/guidance/format/templates.py +299 -0
- crucible/guidance/format/truncation.py +19 -0
- crucible/guidance/rubric/__init__.py +88 -0
- crucible/guidance/rubric/data/rubric.json +1414 -0
- crucible/models/__init__.py +91 -0
- crucible/models/_base.py +23 -0
- crucible/models/auxiliary.py +71 -0
- crucible/models/blueprint.py +21 -0
- crucible/models/enums.py +157 -0
- crucible/models/epic.py +40 -0
- crucible/models/planning.py +104 -0
- crucible/models/specification.py +59 -0
- crucible/models/ticket.py +53 -0
- crucible/py.typed +0 -0
- crucible/scoring/__init__.py +47 -0
- crucible/scoring/cascade.py +118 -0
- crucible/scoring/field_declarations.py +72 -0
- crucible/scoring/global_.py +69 -0
- crucible/scoring/per_entity.py +31 -0
- crucible/scoring/per_field.py +135 -0
- crucible/scoring/quality_checks/__init__.py +147 -0
- crucible/scoring/tiers.py +10 -0
- crucible/scoring/topology.py +73 -0
- crucible/structural/__init__.py +49 -0
- crucible/structural/_strict_schema.py +283 -0
- crucible/structural/duplicate_order.py +54 -0
- crucible/structural/entity_count_check.py +157 -0
- crucible/structural/format.py +78 -0
- crucible/structural/presence.py +41 -0
- crucible/structural/version_resolver.py +17 -0
- crucible/types/__init__.py +112 -0
- crucible/types/_base.py +30 -0
- crucible/types/cascade.py +20 -0
- crucible/types/config.py +36 -0
- crucible/types/context.py +30 -0
- crucible/types/enums.py +42 -0
- crucible/types/finding.py +49 -0
- crucible/types/guidance.py +41 -0
- crucible/types/operations.py +31 -0
- crucible/types/phase.py +33 -0
- crucible/types/result.py +170 -0
- crucible/types/rubric.py +48 -0
- crucible/types/scope.py +40 -0
- crucible_forge-0.1.0.dist-info/METADATA +204 -0
- crucible_forge-0.1.0.dist-info/RECORD +100 -0
- crucible_forge-0.1.0.dist-info/WHEEL +4 -0
- crucible_forge-0.1.0.dist-info/licenses/LICENSE +201 -0
crucible/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""crucible — deterministic validation engine for OpenSpec v1.1 specifications.
|
|
2
|
+
|
|
3
|
+
Faithful Python port of the SpecForge ``@specforge/validator`` engine.
|
|
4
|
+
|
|
5
|
+
``validate(spec, context)`` is the single entry point. All four return layers
|
|
6
|
+
(``structural``, ``scoring``, ``crossValidation``, ``guidance``) are complete
|
|
7
|
+
and verified byte-for-byte against the reference engine.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from . import models, types
|
|
13
|
+
from .api import validate
|
|
14
|
+
from .api.input_validation import ValidatorInputError
|
|
15
|
+
from .config import (
|
|
16
|
+
CONFIG_DEFAULTS,
|
|
17
|
+
PLANNING_CONFIG_DOMAIN,
|
|
18
|
+
PLANNING_CONFIG_SCHEMA_VERSION,
|
|
19
|
+
ConfigValidationError,
|
|
20
|
+
PlanningConfigOverridesSchema,
|
|
21
|
+
PlanningConfigResolver,
|
|
22
|
+
ValidatorConfigSchema,
|
|
23
|
+
ValidatorConfigSnapshotSchema,
|
|
24
|
+
load_defaults,
|
|
25
|
+
load_from_file,
|
|
26
|
+
load_partial_from_file,
|
|
27
|
+
merge_config,
|
|
28
|
+
)
|
|
29
|
+
from .models import Blueprint, Epic, Specification, Ticket
|
|
30
|
+
from .structural import validate_structural
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CONFIG_DEFAULTS",
|
|
34
|
+
"PLANNING_CONFIG_DOMAIN",
|
|
35
|
+
"PLANNING_CONFIG_SCHEMA_VERSION",
|
|
36
|
+
"Blueprint",
|
|
37
|
+
"ConfigValidationError",
|
|
38
|
+
"Epic",
|
|
39
|
+
"PlanningConfigOverridesSchema",
|
|
40
|
+
"PlanningConfigResolver",
|
|
41
|
+
"Specification",
|
|
42
|
+
"Ticket",
|
|
43
|
+
"ValidatorConfigSchema",
|
|
44
|
+
"ValidatorConfigSnapshotSchema",
|
|
45
|
+
"ValidatorInputError",
|
|
46
|
+
"__version__",
|
|
47
|
+
"load_defaults",
|
|
48
|
+
"load_from_file",
|
|
49
|
+
"load_partial_from_file",
|
|
50
|
+
"merge_config",
|
|
51
|
+
"models",
|
|
52
|
+
"types",
|
|
53
|
+
"validate",
|
|
54
|
+
"validate_structural",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
__version__ = "0.1.0"
|
crucible/_spec.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Spec input normalization.
|
|
2
|
+
|
|
3
|
+
The engine works on a camelCase ``dict`` (mirroring the TS value-resolver and
|
|
4
|
+
the JSON seeds). ``validate()`` and ``validate_structural()`` accept either a
|
|
5
|
+
:class:`crucible.models.Specification` or a plain mapping; both normalize here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
SpecInput = BaseModel | dict[str, Any]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def to_spec_dict(spec: SpecInput) -> dict[str, Any]:
|
|
18
|
+
if isinstance(spec, BaseModel):
|
|
19
|
+
return spec.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
20
|
+
return dict(spec)
|
crucible/api/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Validator API layer (port of ``@specforge/validator`` ``src/api``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .input_validation import ValidatorInputError, validate_input
|
|
6
|
+
from .phase_classification import (
|
|
7
|
+
is_human_judgment_phase,
|
|
8
|
+
phase_includes_epics,
|
|
9
|
+
phase_includes_tickets,
|
|
10
|
+
)
|
|
11
|
+
from .scope_resolver import resolve_epic_scope, resolve_ticket_scope
|
|
12
|
+
from .validate import validate
|
|
13
|
+
from .version import get_validator_version
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ValidatorInputError",
|
|
17
|
+
"get_validator_version",
|
|
18
|
+
"is_human_judgment_phase",
|
|
19
|
+
"phase_includes_epics",
|
|
20
|
+
"phase_includes_tickets",
|
|
21
|
+
"resolve_epic_scope",
|
|
22
|
+
"resolve_ticket_scope",
|
|
23
|
+
"validate",
|
|
24
|
+
"validate_input",
|
|
25
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Phase dispatch (port of ``api/dispatcher.ts``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..types.config import ValidatorConfig
|
|
9
|
+
from ..types.phase import SinglePhase
|
|
10
|
+
from ..types.result import ValidationResult, ValidationResultAll, ValidationResultAllMeta
|
|
11
|
+
from .phases import (
|
|
12
|
+
validate_cross_validation,
|
|
13
|
+
validate_epic_decomposition,
|
|
14
|
+
validate_epic_expansion,
|
|
15
|
+
validate_planning_spec,
|
|
16
|
+
validate_ticket_decomposition,
|
|
17
|
+
validate_ticket_expansion,
|
|
18
|
+
)
|
|
19
|
+
from .version import get_validator_version
|
|
20
|
+
|
|
21
|
+
_PHASES = {
|
|
22
|
+
"planning_spec": validate_planning_spec,
|
|
23
|
+
"epic_decomposition": validate_epic_decomposition,
|
|
24
|
+
"epic_expansion": validate_epic_expansion,
|
|
25
|
+
"ticket_decomposition": validate_ticket_decomposition,
|
|
26
|
+
"ticket_expansion": validate_ticket_expansion,
|
|
27
|
+
"cross_validation": validate_cross_validation,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_single(
|
|
32
|
+
spec: dict[str, Any],
|
|
33
|
+
phase: SinglePhase,
|
|
34
|
+
active_entity_id: str | list[str] | None,
|
|
35
|
+
config: ValidatorConfig,
|
|
36
|
+
) -> ValidationResult:
|
|
37
|
+
fn = _PHASES.get(phase)
|
|
38
|
+
if fn is None:
|
|
39
|
+
raise ValueError(f"Unknown phase: {phase}")
|
|
40
|
+
return fn(spec, active_entity_id, config)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate_all(
|
|
44
|
+
spec: dict[str, Any],
|
|
45
|
+
active_entity_id: str | list[str] | None,
|
|
46
|
+
config: ValidatorConfig,
|
|
47
|
+
) -> ValidationResultAll:
|
|
48
|
+
by_phase = {
|
|
49
|
+
phase: fn(spec, active_entity_id, config) for phase, fn in _PHASES.items()
|
|
50
|
+
}
|
|
51
|
+
passed = all(r.passed for r in by_phase.values())
|
|
52
|
+
return ValidationResultAll(
|
|
53
|
+
passed=passed,
|
|
54
|
+
by_phase=by_phase,
|
|
55
|
+
meta=ValidationResultAllMeta(
|
|
56
|
+
validator_version=get_validator_version(),
|
|
57
|
+
generated_at=datetime.now(UTC).isoformat(),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Context input validation (port of ``api/input-validation.ts``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
_VALID_PHASES = {
|
|
8
|
+
"planning_spec",
|
|
9
|
+
"epic_decomposition",
|
|
10
|
+
"epic_expansion",
|
|
11
|
+
"ticket_decomposition",
|
|
12
|
+
"ticket_expansion",
|
|
13
|
+
"cross_validation",
|
|
14
|
+
"all",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Only the per-entity SCORE phases require an active entity.
|
|
18
|
+
_PHASES_REQUIRING_ENTITY_ID = {"epic_expansion", "ticket_expansion"}
|
|
19
|
+
|
|
20
|
+
_VALID_RETURNS = {"structural", "scoring", "crossValidation", "guidance"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidatorInputError(Exception):
|
|
24
|
+
"""Raised when ``validate`` receives an invalid context."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, message: str) -> None:
|
|
27
|
+
super().__init__(f"[Validator] {message}")
|
|
28
|
+
self.name = "ValidatorInputError"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_input(context: dict[str, Any]) -> None:
|
|
32
|
+
phase = context.get("phase")
|
|
33
|
+
if phase is not None and phase not in _VALID_PHASES:
|
|
34
|
+
raise ValidatorInputError(
|
|
35
|
+
f"Invalid phase '{phase}'. Must be one of: {', '.join(sorted(_VALID_PHASES))}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
active = context.get("activeEntityId")
|
|
39
|
+
has_entity_id = len(active) > 0 if isinstance(active, list) else active is not None
|
|
40
|
+
|
|
41
|
+
if phase is not None and phase in _PHASES_REQUIRING_ENTITY_ID and not has_entity_id:
|
|
42
|
+
raise ValidatorInputError(f"Phase '{phase}' requires activeEntityId")
|
|
43
|
+
|
|
44
|
+
returns = context.get("returns")
|
|
45
|
+
if returns:
|
|
46
|
+
for r in returns:
|
|
47
|
+
if r not in _VALID_RETURNS:
|
|
48
|
+
raise ValidatorInputError(
|
|
49
|
+
f"Invalid return layer '{r}'. Must be one of: "
|
|
50
|
+
f"{', '.join(sorted(_VALID_RETURNS))}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if not context.get("config"):
|
|
54
|
+
raise ValidatorInputError("context.config is required")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Phase classification helpers (port of ``api/phase-classification.ts``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..scoring.cascade import phase_includes_epics, phase_includes_tickets
|
|
6
|
+
from ..types.phase import ValidationPhase
|
|
7
|
+
|
|
8
|
+
_HUMAN_JUDGMENT_PHASES = {"epic_decomposition", "ticket_decomposition"}
|
|
9
|
+
|
|
10
|
+
__all__ = ["is_human_judgment_phase", "phase_includes_epics", "phase_includes_tickets"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_human_judgment_phase(phase: ValidationPhase) -> bool:
|
|
14
|
+
return phase != "all" and phase in _HUMAN_JUDGMENT_PHASES
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Per-phase assembly functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .cross_validation import validate_cross_validation
|
|
6
|
+
from .epic_decomposition import validate_epic_decomposition
|
|
7
|
+
from .epic_expansion import validate_epic_expansion
|
|
8
|
+
from .planning_spec import validate_planning_spec
|
|
9
|
+
from .ticket_decomposition import validate_ticket_decomposition
|
|
10
|
+
from .ticket_expansion import validate_ticket_expansion
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"validate_cross_validation",
|
|
14
|
+
"validate_epic_decomposition",
|
|
15
|
+
"validate_epic_expansion",
|
|
16
|
+
"validate_planning_spec",
|
|
17
|
+
"validate_ticket_decomposition",
|
|
18
|
+
"validate_ticket_expansion",
|
|
19
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Shared helpers for phase assembly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ...guidance.composer import compose_findings
|
|
9
|
+
from ...guidance.engine import execute_rubric
|
|
10
|
+
from ...guidance.format import format_guidance
|
|
11
|
+
from ...scoring.global_ import GlobalScoreBreakdown as _InternalBreakdown
|
|
12
|
+
from ...scoring.per_field import PerFieldResult
|
|
13
|
+
from ...types.cascade import CascadeFailure, CascadeFloors
|
|
14
|
+
from ...types.context import PhaseContext
|
|
15
|
+
from ...types.guidance import GuidanceResult
|
|
16
|
+
from ...types.result import (
|
|
17
|
+
GlobalScoreBreakdown,
|
|
18
|
+
PerFieldScore,
|
|
19
|
+
ScoringResultActive,
|
|
20
|
+
ValidationResultMeta,
|
|
21
|
+
)
|
|
22
|
+
from ..version import get_validator_version
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_guidance(spec: dict[str, Any], context: PhaseContext) -> GuidanceResult:
|
|
26
|
+
findings = execute_rubric(spec, context)
|
|
27
|
+
composites = compose_findings(findings, context, spec)
|
|
28
|
+
composed_ids = {f.rubric_entry_id for c in composites for f in c.grouped_findings}
|
|
29
|
+
individuals = [
|
|
30
|
+
f
|
|
31
|
+
for f in findings
|
|
32
|
+
if f.rubric_entry_id not in composed_ids and f.status != "fulfilled"
|
|
33
|
+
]
|
|
34
|
+
return format_guidance(composites, individuals, context, spec)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def now_iso() -> str:
|
|
38
|
+
return datetime.now(UTC).isoformat()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_meta(
|
|
42
|
+
*,
|
|
43
|
+
active_entity_id: str | list[str] | None = None,
|
|
44
|
+
resolution: str | None = None,
|
|
45
|
+
warnings: list[str] | None = None,
|
|
46
|
+
) -> ValidationResultMeta:
|
|
47
|
+
kwargs: dict[str, object] = {
|
|
48
|
+
"validator_version": get_validator_version(),
|
|
49
|
+
"generated_at": now_iso(),
|
|
50
|
+
}
|
|
51
|
+
if active_entity_id is not None:
|
|
52
|
+
kwargs["active_entity_id"] = active_entity_id
|
|
53
|
+
if resolution is not None:
|
|
54
|
+
kwargs["resolution"] = resolution
|
|
55
|
+
if warnings:
|
|
56
|
+
kwargs["warnings"] = warnings
|
|
57
|
+
return ValidationResultMeta(**kwargs)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def per_field_to_scores(per_field: dict[str, PerFieldResult]) -> dict[str, PerFieldScore]:
|
|
61
|
+
return {
|
|
62
|
+
k: PerFieldScore(earned=v.earned, possible=v.possible, tier=v.tier)
|
|
63
|
+
for k, v in per_field.items()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def breakdown_model(b: _InternalBreakdown) -> GlobalScoreBreakdown:
|
|
68
|
+
return GlobalScoreBreakdown(
|
|
69
|
+
spec_block=b.spec_block,
|
|
70
|
+
epic_block=b.epic_block,
|
|
71
|
+
ticket_block=b.ticket_block,
|
|
72
|
+
epic_avg=b.epic_avg,
|
|
73
|
+
ticket_avg=b.ticket_avg,
|
|
74
|
+
weights=b.weights,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_active_scoring(
|
|
79
|
+
*,
|
|
80
|
+
local_score: float,
|
|
81
|
+
per_field: dict[str, PerFieldScore],
|
|
82
|
+
breakdown: _InternalBreakdown,
|
|
83
|
+
cascade_floors: CascadeFloors,
|
|
84
|
+
cascade_failures: list[CascadeFailure],
|
|
85
|
+
gate_result: str,
|
|
86
|
+
threshold: float,
|
|
87
|
+
per_entity_score: dict[str, float] | None = None,
|
|
88
|
+
) -> ScoringResultActive:
|
|
89
|
+
kwargs: dict[str, object] = {
|
|
90
|
+
"skipped": False,
|
|
91
|
+
"local_score": local_score,
|
|
92
|
+
"global_score": breakdown.global_score,
|
|
93
|
+
"per_field": per_field,
|
|
94
|
+
"topology_penalties": breakdown.topology_penalty,
|
|
95
|
+
"gate_result": gate_result,
|
|
96
|
+
"threshold": threshold,
|
|
97
|
+
"global_score_breakdown": breakdown_model(breakdown),
|
|
98
|
+
"cascade_floors": cascade_floors,
|
|
99
|
+
"cascade_failures": cascade_failures,
|
|
100
|
+
}
|
|
101
|
+
if per_entity_score is not None:
|
|
102
|
+
kwargs["per_entity_score"] = per_entity_score
|
|
103
|
+
return ScoringResultActive(**kwargs)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""cross_validation phase (port of ``api/phases/cross-validation.ts``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ...cross_validation import run_cross_validation
|
|
9
|
+
from ...cross_validation.emission import CVEmission
|
|
10
|
+
from ...structural import validate_structural
|
|
11
|
+
from ...types.config import ValidatorConfig
|
|
12
|
+
from ...types.guidance import GuidanceCrossValidationEntry, GuidanceEntry, GuidanceResult
|
|
13
|
+
from ...types.result import ValidationResult
|
|
14
|
+
from ._common import build_meta
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _adapt_emission(emission: CVEmission) -> GuidanceCrossValidationEntry:
|
|
18
|
+
f = emission.finding
|
|
19
|
+
kwargs: dict[str, Any] = {
|
|
20
|
+
"pattern_id": "cross-validation",
|
|
21
|
+
"category": f.category,
|
|
22
|
+
"severity": f.severity,
|
|
23
|
+
"field_path": f.field,
|
|
24
|
+
"primary_entity_id": f.primary_entity_id,
|
|
25
|
+
"entity_ids": f.entity_ids,
|
|
26
|
+
"message": emission.guidance,
|
|
27
|
+
"operations": f.operations,
|
|
28
|
+
"points_lost": 0,
|
|
29
|
+
"global_impact_on_fix": 0,
|
|
30
|
+
}
|
|
31
|
+
if f.context is not None:
|
|
32
|
+
kwargs["context"] = f.context
|
|
33
|
+
return GuidanceCrossValidationEntry(**kwargs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_cross_validation(
|
|
37
|
+
spec: dict[str, Any], active_entity_id: str | list[str] | None, config: ValidatorConfig
|
|
38
|
+
) -> ValidationResult:
|
|
39
|
+
warnings: list[str] = []
|
|
40
|
+
if active_entity_id is not None:
|
|
41
|
+
warnings.append(
|
|
42
|
+
f"cross_validation phase ignores activeEntityId; received: "
|
|
43
|
+
f"{json.dumps(active_entity_id)}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
structural = validate_structural(spec, config, "cross_validation")
|
|
47
|
+
run = run_cross_validation(spec, config, "cross_validation")
|
|
48
|
+
cross_validation = run.result
|
|
49
|
+
|
|
50
|
+
per_entity: dict[str, list[GuidanceEntry]] = {}
|
|
51
|
+
for emission in run.emissions:
|
|
52
|
+
adapted = _adapt_emission(emission)
|
|
53
|
+
for entity_id in emission.finding.entity_ids:
|
|
54
|
+
per_entity.setdefault(entity_id, []).append(adapted)
|
|
55
|
+
guidance = GuidanceResult(per_entity=per_entity)
|
|
56
|
+
|
|
57
|
+
passed = (
|
|
58
|
+
not structural.invalid_fields
|
|
59
|
+
and not any(f.severity == "error" for f in structural.findings)
|
|
60
|
+
and not cross_validation.findings
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return ValidationResult(
|
|
64
|
+
phase="cross_validation",
|
|
65
|
+
passed=passed,
|
|
66
|
+
structural=structural,
|
|
67
|
+
cross_validation=cross_validation,
|
|
68
|
+
guidance=guidance,
|
|
69
|
+
meta=build_meta(active_entity_id=active_entity_id, warnings=warnings or None),
|
|
70
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""epic_decomposition phase (port of ``api/phases/epic-decomposition.ts``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...engines.ratios import check_blueprint_epic_ratio
|
|
8
|
+
from ...guidance.fixed_prompts import build_epic_decomposition_guidance
|
|
9
|
+
from ...structural import validate_structural
|
|
10
|
+
from ...types.config import ValidatorConfig
|
|
11
|
+
from ...types.result import CrossValidationResult, ScoringResultSkipped, ValidationResult
|
|
12
|
+
from ._common import build_meta
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_epic_decomposition(
|
|
16
|
+
spec: dict[str, Any], _active_entity_id: str | list[str] | None, config: ValidatorConfig
|
|
17
|
+
) -> ValidationResult:
|
|
18
|
+
structural = validate_structural(spec, config, "epic_decomposition")
|
|
19
|
+
ratio_findings = check_blueprint_epic_ratio(spec, config)
|
|
20
|
+
|
|
21
|
+
passed = (
|
|
22
|
+
not ratio_findings
|
|
23
|
+
and not structural.invalid_fields
|
|
24
|
+
and not any(f.severity == "error" for f in structural.findings)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return ValidationResult(
|
|
28
|
+
phase="epic_decomposition",
|
|
29
|
+
passed=passed,
|
|
30
|
+
structural=structural,
|
|
31
|
+
scoring=ScoringResultSkipped(),
|
|
32
|
+
cross_validation=CrossValidationResult(
|
|
33
|
+
skipped=False,
|
|
34
|
+
findings=ratio_findings,
|
|
35
|
+
ran_checks=["blueprint-epic-ratio"],
|
|
36
|
+
skipped_checks=[],
|
|
37
|
+
),
|
|
38
|
+
guidance=build_epic_decomposition_guidance(spec),
|
|
39
|
+
meta=build_meta(),
|
|
40
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""epic_expansion phase (port of ``api/phases/epic-expansion.ts``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...checks.coverage import validate_nfr_coverage, validate_requirement_coverage
|
|
8
|
+
from ...checks.na_validation import validate_na_epic
|
|
9
|
+
from ...guidance.rubric import get_rubric_for_phase
|
|
10
|
+
from ...scoring import (
|
|
11
|
+
CascadeGateInput,
|
|
12
|
+
compute_cascade_floors,
|
|
13
|
+
compute_entity_score,
|
|
14
|
+
compute_global_score,
|
|
15
|
+
evaluate_cascade_gate,
|
|
16
|
+
)
|
|
17
|
+
from ...structural import validate_structural
|
|
18
|
+
from ...types.config import ValidatorConfig
|
|
19
|
+
from ...types.context import PhaseContext
|
|
20
|
+
from ...types.result import CrossValidationResult, ValidationResult
|
|
21
|
+
from ..scope_resolver import resolve_epic_scope
|
|
22
|
+
from ._common import build_active_scoring, build_guidance, build_meta
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_epic_expansion(
|
|
26
|
+
spec: dict[str, Any], active_entity_id: str | list[str] | None, config: ValidatorConfig
|
|
27
|
+
) -> ValidationResult:
|
|
28
|
+
scoped_epics = resolve_epic_scope(spec, active_entity_id)
|
|
29
|
+
structural = validate_structural(spec, config, "epic_expansion")
|
|
30
|
+
|
|
31
|
+
epic_entries = [
|
|
32
|
+
e for e in get_rubric_for_phase("epic_expansion") if e.field_path.startswith("epic.")
|
|
33
|
+
]
|
|
34
|
+
per_entity_score: dict[str, float] = {}
|
|
35
|
+
scores: list[float] = []
|
|
36
|
+
for epic in scoped_epics:
|
|
37
|
+
result = compute_entity_score(epic, epic_entries, config)
|
|
38
|
+
per_entity_score[epic["id"]] = result.local_score
|
|
39
|
+
scores.append(result.local_score)
|
|
40
|
+
avg_local = 100.0 if not scores else sum(scores) / len(scores)
|
|
41
|
+
|
|
42
|
+
breakdown = compute_global_score(spec, config["scoring"]["weights"], config)
|
|
43
|
+
cascade_floors = compute_cascade_floors(config)
|
|
44
|
+
cascade = evaluate_cascade_gate(
|
|
45
|
+
CascadeGateInput(
|
|
46
|
+
global_score=breakdown.global_score,
|
|
47
|
+
spec_block=breakdown.spec_block,
|
|
48
|
+
epic_block=breakdown.epic_block,
|
|
49
|
+
phase="epic_expansion",
|
|
50
|
+
floors=cascade_floors,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
threshold = config["thresholds"]["epic"]
|
|
55
|
+
gate_result = "pass" if avg_local >= threshold and cascade.gate_result == "pass" else "fail"
|
|
56
|
+
|
|
57
|
+
scoring = build_active_scoring(
|
|
58
|
+
local_score=avg_local,
|
|
59
|
+
per_field={},
|
|
60
|
+
breakdown=breakdown,
|
|
61
|
+
cascade_floors=cascade_floors,
|
|
62
|
+
cascade_failures=cascade.cascade_failures,
|
|
63
|
+
gate_result=gate_result,
|
|
64
|
+
threshold=threshold,
|
|
65
|
+
per_entity_score=per_entity_score,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
na_findings = [f for e in scoped_epics for f in validate_na_epic(e, config)]
|
|
69
|
+
coverage_findings = [
|
|
70
|
+
*validate_requirement_coverage(spec, scoped_epics, config),
|
|
71
|
+
*validate_nfr_coverage(spec, scoped_epics, config),
|
|
72
|
+
]
|
|
73
|
+
all_local = [*na_findings, *coverage_findings]
|
|
74
|
+
|
|
75
|
+
passed = (
|
|
76
|
+
gate_result == "pass"
|
|
77
|
+
and not structural.invalid_fields
|
|
78
|
+
and not any(f.severity == "error" for f in structural.findings)
|
|
79
|
+
and not all_local
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
guidance = build_guidance(
|
|
83
|
+
spec, PhaseContext(phase="epic_expansion", config=config, active_entity_id=active_entity_id)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return ValidationResult(
|
|
87
|
+
phase="epic_expansion",
|
|
88
|
+
passed=passed,
|
|
89
|
+
structural=structural,
|
|
90
|
+
scoring=scoring,
|
|
91
|
+
cross_validation=CrossValidationResult(
|
|
92
|
+
skipped=False,
|
|
93
|
+
findings=all_local,
|
|
94
|
+
ran_checks=["na-validation", "requirement-coverage", "nfr-coverage"],
|
|
95
|
+
skipped_checks=[],
|
|
96
|
+
),
|
|
97
|
+
guidance=guidance,
|
|
98
|
+
meta=build_meta(active_entity_id=active_entity_id),
|
|
99
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""planning_spec phase (port of ``api/phases/planning-spec.ts``).
|
|
2
|
+
|
|
3
|
+
Guidance layer deferred (C7) — ``guidance`` is left unset.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ...checks.na_validation import validate_na_spec
|
|
11
|
+
from ...guidance.rubric import get_rubric_for_phase
|
|
12
|
+
from ...scoring import (
|
|
13
|
+
CascadeGateInput,
|
|
14
|
+
compute_cascade_floors,
|
|
15
|
+
compute_entity_score,
|
|
16
|
+
compute_global_score,
|
|
17
|
+
evaluate_cascade_gate,
|
|
18
|
+
)
|
|
19
|
+
from ...structural import validate_structural
|
|
20
|
+
from ...types.config import ValidatorConfig
|
|
21
|
+
from ...types.context import PhaseContext
|
|
22
|
+
from ...types.result import ValidationResult
|
|
23
|
+
from ._common import build_active_scoring, build_guidance, build_meta, per_field_to_scores
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_planning_spec(
|
|
27
|
+
spec: dict[str, Any], active_entity_id: str | list[str] | None, config: ValidatorConfig
|
|
28
|
+
) -> ValidationResult:
|
|
29
|
+
structural = validate_structural(spec, config, "planning_spec")
|
|
30
|
+
|
|
31
|
+
spec_entries = [
|
|
32
|
+
e for e in get_rubric_for_phase("planning_spec") if e.field_path.startswith("specification.")
|
|
33
|
+
]
|
|
34
|
+
entity_score = compute_entity_score(spec, spec_entries, config)
|
|
35
|
+
|
|
36
|
+
breakdown = compute_global_score(spec, config["scoring"]["weights"], config)
|
|
37
|
+
cascade_floors = compute_cascade_floors(config)
|
|
38
|
+
cascade = evaluate_cascade_gate(
|
|
39
|
+
CascadeGateInput(
|
|
40
|
+
global_score=breakdown.global_score,
|
|
41
|
+
spec_block=breakdown.spec_block,
|
|
42
|
+
epic_block=breakdown.epic_block,
|
|
43
|
+
phase="planning_spec",
|
|
44
|
+
floors=cascade_floors,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
threshold = config["thresholds"]["specification"]
|
|
49
|
+
gate_result = (
|
|
50
|
+
"pass" if entity_score.local_score >= threshold and cascade.gate_result == "pass" else "fail"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
scoring = build_active_scoring(
|
|
54
|
+
local_score=entity_score.local_score,
|
|
55
|
+
per_field=per_field_to_scores(entity_score.per_field),
|
|
56
|
+
breakdown=breakdown,
|
|
57
|
+
cascade_floors=cascade_floors,
|
|
58
|
+
cascade_failures=cascade.cascade_failures,
|
|
59
|
+
gate_result=gate_result,
|
|
60
|
+
threshold=threshold,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
na_findings = validate_na_spec(spec, config)
|
|
64
|
+
passed = (
|
|
65
|
+
gate_result == "pass"
|
|
66
|
+
and not structural.invalid_fields
|
|
67
|
+
and not any(f.severity == "error" for f in structural.findings)
|
|
68
|
+
and not na_findings
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
guidance = build_guidance(
|
|
72
|
+
spec, PhaseContext(phase="planning_spec", config=config, active_entity_id=active_entity_id)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return ValidationResult(
|
|
76
|
+
phase="planning_spec",
|
|
77
|
+
passed=passed,
|
|
78
|
+
structural=structural,
|
|
79
|
+
scoring=scoring,
|
|
80
|
+
guidance=guidance,
|
|
81
|
+
meta=build_meta(active_entity_id=active_entity_id),
|
|
82
|
+
)
|