Skip to content

rubric

strands_teams.rubric

Single scoring rubric shared by the narrative-scoring tool and quality hook.

NarrativeRubric is the one place the scoring dimensions, their threshold tables, and the revision threshold (default 3.5) live. Both the score_narrative-equivalent coordinator tool (AC-2-6) and the quality hook (AC-3-3) call :meth:NarrativeRubric.score, deduping the two pre-split formulas that drifted apart (the pre-split team-tools module used discrete threshold tables; hooks.py:509-539 used continuous min(5, count/divisor) divisors). This module keeps the threshold-table intent as the single canonical scoring; the continuous variant is subsumed (same dimensions, same 3.5 threshold) per explore.yaml hidden coupling #5.

Dimensions are data-driven: each :class:RubricDimension names a measure (a regex pattern or a counter callable), an ascending (min_count, score) threshold table, and a suggestion template. The domain regexes (file:line, mermaid, file-path) ship here as importable defaults so a use case just references :data:DEFAULT_NARRATIVE_RUBRIC rather than hardcoding them — but a use case may build its own NarrativeRubric with different dimensions.

FILE_LINE_PATTERN module-attribute

FILE_LINE_PATTERN = '\\w+\\.\\w+:\\d+'

Matches file.ext:123 style file:line references (specificity).

HEADING_PATTERN module-attribute

HEADING_PATTERN = '^#{2,3}\\s'

Matches level-⅔ markdown headings (structure). Needs re.MULTILINE.

MERMAID_PATTERN module-attribute

MERMAID_PATTERN = '```mermaid'

Matches a mermaid code fence (diagrams). Case-insensitive.

FILE_PATH_PATTERN module-attribute

FILE_PATH_PATTERN = (
    "(?:\\.?/)?(?:[\\w.-]+/)+[\\w.-]+\\.\\w+"
)

Matches slash-separated file paths (cross-references). Counted as unique.

DEFAULT_REVISION_THRESHOLD module-attribute

DEFAULT_REVISION_THRESHOLD = 3.5

Average below which an artifact needs a revision/enrichment pass.

DEFAULT_NARRATIVE_RUBRIC module-attribute

DEFAULT_NARRATIVE_RUBRIC = NarrativeRubric(
    dimensions=(
        RubricDimension(
            name="specificity",
            pattern=FILE_LINE_PATTERN,
            thresholds=(
                (0, 1.0),
                (5, 2.0),
                (10, 3.0),
                (20, 4.0),
                (30, 5.0),
            ),
            suggestion="Add more file:line references (currently {count}).",
        ),
        RubricDimension(
            name="structure",
            pattern=HEADING_PATTERN,
            flags=re.MULTILINE,
            thresholds=(
                (0, 1.0),
                (3, 2.0),
                (5, 3.0),
                (8, 4.0),
                (11, 5.0),
            ),
            suggestion="Add more section headings for structure (currently {count}).",
        ),
        RubricDimension(
            name="diagrams",
            pattern=MERMAID_PATTERN,
            flags=re.IGNORECASE,
            thresholds=((0, 1.0), (1, 3.0), (2, 5.0)),
            suggestion="Add at least one mermaid diagram for call flow or architecture.",
            weak_score=1.0,
        ),
        RubricDimension(
            name="depth",
            counter=count_lines,
            thresholds=(
                (0, 1.0),
                (20, 2.0),
                (50, 3.0),
                (100, 4.0),
                (200, 5.0),
            ),
            suggestion="Expand content depth (currently {count} lines).",
        ),
        RubricDimension(
            name="cross_references",
            pattern=FILE_PATH_PATTERN,
            unique=True,
            thresholds=(
                (0, 1.0),
                (3, 2.0),
                (8, 3.0),
                (15, 4.0),
                (25, 5.0),
            ),
            suggestion="Cross-reference more files/modules (currently {count} unique paths).",
        ),
    )
)

The default five-dimension narrative rubric (revision threshold 3.5).

RubricDimension dataclass

RubricDimension(
    name,
    thresholds,
    suggestion,
    pattern=None,
    flags=0,
    unique=False,
    counter=None,
    weak_score=2.0,
)

One scored dimension of a narrative artifact.

A dimension measures a count from the text (via a regex pattern or a counter callable), then maps that count to a 0-5 score using an ascending (min_count, score) threshold table: the score is the highest table entry whose min_count does not exceed the measured count.

Attributes:

Name Type Description
name str

Dimension key (e.g., "specificity").

thresholds tuple[tuple[int, float], ...]

Ascending (min_count, score) pairs. The first pair should use min_count=0 to define the floor score.

suggestion str

Improvement hint; may contain a {count} placeholder.

pattern str | None

Regex whose match count is the measure. Ignored if counter is set.

flags int

Regex flags applied to pattern (e.g., re.MULTILINE).

unique bool

If True, count distinct matches rather than all matches.

counter Callable[[str], int] | None

Callable measuring the count directly (overrides pattern).

weak_score float

Emit suggestion when the dimension scores at or below this.

measure

measure(text)

Return the raw count this dimension measures from text.

Source code in packages/strands-teams/src/strands_teams/rubric.py
def measure(self, text: str) -> int:
    """Return the raw count this dimension measures from ``text``."""
    if self.counter is not None:
        return self.counter(text)
    if self.pattern is None:
        msg = f"RubricDimension {self.name!r} has neither pattern nor counter"
        raise ValueError(msg)
    matches = re.findall(self.pattern, text, self.flags)
    return len(set(matches)) if self.unique else len(matches)

score_count

score_count(count)

Map a measured count to its 0-5 score via the threshold table.

Source code in packages/strands-teams/src/strands_teams/rubric.py
def score_count(self, count: int) -> float:
    """Map a measured ``count`` to its 0-5 score via the threshold table."""
    score = 0.0
    for min_count, value in self.thresholds:
        if count >= min_count:
            score = value
    return score

DimensionScore dataclass

DimensionScore(name, count, score)

The scored result for one dimension.

ScoreReport dataclass

ScoreReport(
    scores,
    total,
    needs_revision,
    suggestions,
    counts=dict(),
)

Result of scoring a text against a :class:NarrativeRubric.

NarrativeRubric dataclass

NarrativeRubric(
    dimensions,
    revision_threshold=DEFAULT_REVISION_THRESHOLD,
)

The single rubric object: dimensions + revision threshold.

Attributes:

Name Type Description
dimensions tuple[RubricDimension, ...]

Ordered scoring dimensions.

revision_threshold float

Average (and per-artifact) score below which the artifact needs a revision pass (default 3.5).

score

score(text)

Score text across every dimension.

Parameters:

Name Type Description Default
text str

The narrative artifact content to score.

required

Returns:

Name Type Description
A ScoreReport

class:ScoreReport with per-dimension scores, the rounded total

ScoreReport

average, the needs_revision flag (average below the revision

ScoreReport

threshold), and improvement suggestions for weak dimensions.

Source code in packages/strands-teams/src/strands_teams/rubric.py
def score(self, text: str) -> ScoreReport:
    """Score ``text`` across every dimension.

    Args:
        text: The narrative artifact content to score.

    Returns:
        A :class:`ScoreReport` with per-dimension scores, the rounded total
        average, the ``needs_revision`` flag (average below the revision
        threshold), and improvement suggestions for weak dimensions.
    """
    scores: dict[str, float] = {}
    counts: dict[str, int] = {}
    suggestions: list[str] = []
    for dim in self.dimensions:
        count = dim.measure(text)
        value = dim.score_count(count)
        scores[dim.name] = value
        counts[dim.name] = count
        if value <= dim.weak_score:
            suggestions.append(dim.suggestion.format(count=count))
    total = round(sum(scores.values()) / len(scores), 2) if scores else 0.0
    return ScoreReport(
        scores=scores,
        total=total,
        needs_revision=total < self.revision_threshold,
        suggestions=suggestions,
        counts=counts,
    )

count_lines

count_lines(text)

Count the lines in text (depth measure).

Source code in packages/strands-teams/src/strands_teams/rubric.py
def count_lines(text: str) -> int:
    """Count the lines in ``text`` (depth measure)."""
    return len(text.splitlines())