Контракт scorer
Задача 2.20 — превратить набор уже измеренных line candidates в локальный, объяснимый и воспроизводимый порядок. Scorer не является ни geometry-owner, ни generator, ни global composition solver.
Local scoring / ranking линейной подсистемы при одном explicit semantic + style target.
Никакой прямой мутации path, stroke, color, grid или foreign-owned state.
Global balance, hierarchy, negative space, global eye-flow, typography, image/crop.
CONSUMES / PROVIDES
LineStructureCandidate[] // from 2.19 MetricResult[] // owner results 2.02–2.18 PenaltyResult[]? Candidate.hardViolations[] Candidate.auditEvidence[] Candidate.correctionCost? Candidate.lineage LineSystemTarget // semantic + style LineScoringPolicy Compo0.seed
LineSystemScoreResult {
status,
rankEligible,
localScore,
rawPositiveScore,
softPenalty,
confidence,
evidenceCoverage,
targetCoverage,
groupBreakdown[],
selectedVotes[],
suppressedVotes[],
appliedPenalties[],
suppressedPenalties[],
rejectedBy[],
unresolvedInputs[],
repairHints[],
dirtyOwners[],
comparisonKey,
audit
}Ownership: что 2.20 имеет право считать
Scorer владеет только агрегатом и аудитом процесса агрегации. Исходная геометрия и локальные fit/penalty остаются у владельцев.
Итог локальной системы
lineSystem.local_score
lineSystem.local_score_confidence
lineSystem.local_rank_key
lineSystem.suppression_log
Owner results
Только нормализованные результаты и penalties, созданные Compo 2.02–2.18. 2.01 остаётся геометрическим descriptor-owner и сам по себе не получает эстетического голоса.
Global composition
global.balance, global.hierarchy, global.negativeSpace, global.eyeFlow, typography, image crop, poster score, semantic truth.
Главное правило
Никакого beautyScore. Локальный scalar нужен только для выбора между line candidates в одинаковом explicit target context. Он не утверждает, что один постер «красивее» другого.
Canonical owner inputs
| Источник | Owner result | Использование в 2.20 |
|---|---|---|
| Compo 2.02–2.05 | orientation / curve / polyline owner-fit, если owner его экспортирует | Descriptors сами по себе не голосуют; voting разрешён только нормализованному owner-result. |
| Compo 2.06 | local visual-weight fitness/composite | Primary vote; raw stroke width и reused contrast остаются evidence. |
| Compo 2.07 | line.roleLengthFit | Primary ROLE_LENGTH vote; raw path length/span — diagnostics. |
| Compo 2.08 | local eye-guide cue result | Только local cue; global eye-flow запрещён. |
| Compo 2.09 | semantic connector / route composite | Только если connector/annotation semantics применимы. |
| Compo 2.10 | intersection owner fitness / penalty / hard violation | Один crossing-defect получает один owner vote или penalty. |
| Compo 2.11 | line-shape contact owner composite | Valid attachment не дублируется generic contact penalty. |
| Compo 2.12 | parallel-group owner composite | Geometry gaps — evidence; stroke rhythm принадлежит 2.17. |
| Compo 2.13 | line.grid.localGridFit | Primary grid-related vote; axis/offset/endpoint components — evidence. |
| Compo 2.14 | line.boundary.localBoundaryScore | Primary boundary vote; component area ratios не превращаются в balance. |
| Compo 2.15–2.18 | owner-designated composite via scoringGroup | 2.20 не угадывает внутренний key; использует metadata contract. |
Composite-first policy: если owner уже экспортирует локальный composite, scorer использует его как основной голос, а дочерние компоненты сохраняет в diagnostics. Это канонический механизм защиты от double counting.
Explicit semantic + style targets
Одна и та же геометрия может быть хорошим connector и плохим divider. Поэтому score не существует без явного target profile.
Semantic target
semanticTarget: {
role: "connector",
subrole: "annotation" | null,
requiredGroups: ["ROLE_LENGTH","CONNECTION"],
optionalGroups: ["FLOW_CUE","GRID_RELATION"],
forbiddenRelations: ["UNEXPLAINED_PENETRATION"],
requiredEvidencePolicy: "rank_with_low_confidence"
}Style target
styleTarget: {
presetId: "technical-clean-v1",
groupMultipliers: {
GRID_RELATION: 1.15,
STROKE_PATTERN: 1.10,
COLOR_TONE: 0.90
},
metricMultipliers: {},
strictness: 0.70
}MetricResult.value. Например, line.grid.localGridFit=.94 остаётся .94; если сетка не нужна, её applicability становится 0 или группа не включается в target.Canonical MetricResult + PenaltyResult
2.20 принимает только owner-нормализованные результаты. Dependency metadata обязательна: без неё нельзя доказуемо различить независимый сигнал и повторный голос по той же evidence lineage.
MetricResult
MetricResult {
key: string,
owner: "Compo2.xx",
value: float, // 0..1; higher = better fit to owner target
applicability: float, // 0..1
confidence: float, // 0..1
status: "ok|warn|fail|na",
provenance:
"DERIVED|STANDARD|EMPIRICAL|INITIAL_HEURISTIC|
IMPLEMENTATION_HEURISTIC|STYLE_PRESET|DEFERRED",
composite: bool,
componentOf: string|null,
dependsOn: string[],
derivedFrom: string[],
scoringGroup: string|null,
independentVoteAllowed: bool,
votePriority: number?,
evidence: object,
repairHints: []
}PenaltyResult
PenaltyResult {
key: string,
owner: "Compo2.xx",
severity: float, // 0..1
applicability: float,
confidence: float,
status: "ok|warn|fail|na",
provenance: string,
componentOf: string|null,
dependsOn: string[],
derivedFrom: string[],
scoringGroup: string|null,
independentVoteAllowed: bool,
evidence: object,
repairHints: []
}Metadata semantics
| Field | Meaning | Scoring consequence |
|---|---|---|
dependsOn | Вычислительная зависимость / recompute order. | Само по себе НЕ подавляет голос. |
derivedFrom | Результат реально использует upstream evidence. | Проверяется на double counting внутри и между scoringGroups. |
componentOf | Метрика уже включена в active composite. | Компонент становится diagnostic-only. |
scoringGroup | Бюджет семейства голосов. | Группа получает ограниченный общий influence. |
independentVoteAllowed | Явное разрешение независимо голосовать при общей evidence lineage. | Без явного разрешения повторный derived signal подавляется. |
Validation / legacy adapter
Hard reject раньше score
Невыполнимый candidate не может компенсировать жёсткое нарушение хорошими локальными метриками.
FEASIBILITY GATE
Не переводится в soft penalty и не может быть «откуплен» высоким grid-fit, хорошим цветом или удачной кривизной.
Допускается только как owner-issued PenaltyResult с evidence, applicability и confidence; тот же дефект нельзя одновременно учитывать внутри composite и отдельным penalty.
Scoring groups: бюджет доказательств
Количество метрик в модуле не должно автоматически давать ему больше влияния. Голосовой бюджет назначается группе, а не числу полей.
| Group | Типичные owners | Когда голосует | Примечание |
|---|---|---|---|
| AXIS_DIRECTION | 2.02 / 2.03 | axis/diagonal role-fit применим | H/V и diagonal ветки взаимно исключаются через applicability. |
| PATH_FORM | 2.04 / 2.05 | curve или polyline | Чистые descriptors arc length / turn angles не голосуют без owner fit. |
| VISUAL_WEIGHT | 2.06 | line rendered | Использовать owner composite/fitness; не пересчитывать contrast. |
| ROLE_LENGTH | 2.07 | target interval/reference span известен | roleLengthFit — primary; raw path length — evidence. |
| FLOW_CUE | 2.08 | target запрашивает local guide cue | Это не global eye-flow. |
| CONNECTION | 2.09 | connector/annotation semantics | Owner composite semantic attachment/route fit. |
| INTERSECTION_SAFETY | 2.10 | есть line-line relations | Чаще owner penalty/fitness, а не «бонус за отсутствие линий». |
| SHAPE_CONTACT | 2.11 | есть object/shape relation | Valid attachment не должен повторно штрафоваться как generic touch. |
| PARALLEL_GROUP | 2.12 | реальная parallel group существует | Gaps — geometry evidence; rhythm принадлежит 2.17. |
| GRID_RELATION | 2.13 | grid включён/запрошен | N/A curve не получает 0. |
| BOUNDARY_PARTITION | 2.14 | divider/frame/boundary semantics | Area ratios не превращать в balance score. |
| DECORATIVE_TECH | 2.15 | owner declares applicability | 2.20 читает export, не угадывает имя метрики. |
| COLOR_TONE | 2.16 | rendered color/opacity available | Не дублировать contrast, если он уже вошёл в 2.06 contextual weight. |
| STROKE_PATTERN | 2.17 | dash/repeat pattern применим | Геометрические gaps 2.12 — evidence, rhythm vote — 2.17. |
| BENDING_TENSION | 2.18 | curvature/tension target применим | Может зависеть от 2.04 geometry, но голосует только своим owner-fit. |
scoringGroup, componentOf, dependsOn, derivedFrom и composite flag. Legacy bridge разрешён только как versioned allowlist для старых 2.02–2.14; неизвестный key остаётся diagnostic-only.Anti double-counting: suppression policy
Double counting проверяется дважды: сначала внутри scoringGroup, затем между scoringGroups. Scorer не угадывает корреляцию по названиям — он работает только с owner metadata и evidence lineage.
Suppression order
1. N/A / applicability = 0 2. confidence = 0 3. invalid / incomplete metadata 4. componentOf active composite 5. duplicate owner+key+version 6. same scoringGroup → primary vote additional sibling only if independentVoteAllowed 7. cross-group derivedFrom / evidence overlap → suppress lower-priority vote unless BOTH explicitly allow independence 8. dependsOn alone → NEVER suppress 9. target says group not applicable 10. global / deferred metric → ignored by 2.20
SuppressedVote
{
key,
owner,
reason:
"COMPONENT_OF_COMPOSITE" |
"GROUP_PRIMARY_SELECTED" |
"CROSS_GROUP_DERIVED_DUPLICATE" |
"DUPLICATE_EVIDENCE" |
"ZERO_APPLICABILITY" |
"ZERO_CONFIDENCE" |
"INCOMPLETE_METADATA" |
"TARGET_NOT_APPLICABLE" |
"GLOBAL_DEFERRED",
suppressedBy?: string,
evidenceRef?: string
}Cross-group rule
Вычислительная зависимость нужна для dirty/recompute graph. Она не доказывает, что две метрики измеряют один и тот же факт.
Metric становится diagnostic-only. COMPO предпочитает потерять голос, а не незаметно посчитать один evidence дважды.
Агрегация: group-capped weighted vote
Количество полей не создаёт дополнительного влияния. Сначала выбираются независимые owner votes, затем их влияние ограничивается бюджетом scoringGroup.
groupVoteCap
1.00 default group budget. Target может задать другой cap явно.
maxSoftPenalty
0.35. Hard violation никогда не конвертируется в soft penalty.
Не в знаменатель
Неприменимая target-group исключается. Отсутствие не превращается в value=0.
Один evidence
Один defect получает один primary positive/penalty representation, если независимость не разрешена явно.
Penalty de-duplication
Penalty проходит ту же lineage-проверку, что и positive vote. Одинаковый evidenceFingerprint не может быть списан дважды даже в разных scoringGroups. Если тот же evidence уже представлен active composite vote, penalty подавляется, пока owner явно не выставит independentVoteAllowed=true с обеих сторон.
Score, confidence и coverage — разные величины
Score отвечает за соответствие target, confidence — за надёжность evidence, coverage — за полноту ожидаемых owner families. Они не смешиваются в один «магический» показатель.
Score
Насколько хорошо доступные применимые owner votes соответствуют explicit target.
Confidence
Насколько надёжны exact / approximate / fallback measurements и owner evaluation.
Coverage
Какая доля target-ожидаемых scoringGroups вообще присутствует как валидное evidence.
Missing required group не получает value=0. При requiredEvidencePolicy="require_all" candidate становится insufficient_evidence и не ранжируется. При rank_with_low_confidence он может остаться в ranking, но с пониженным coverage/confidence.
Owner должен уже отразить качество geometry/evidence в confidence. 2.20 не пытается повторно угадать, была геометрия exact, approximate или fallback.
Deterministic ranking candidates из 2.19
Одинаковые входы, seed, policy и target обязаны давать одинаковый порядок.
Глобальный solver позже имеет право выбрать локально второй candidate, если он лучше работает с hierarchy, balance, negative space, typography, image interaction или global eye-flow. 2.20 не пытается предвосхитить этот слой.
Edge cases
| Case | Policy |
|---|---|
| Все candidates rejected | Score не вычисляется; порядок для audit стабилен только по deterministic tie-break и не означает «лучший дизайн». |
| Нет ни одного валидного vote | status=insufficient_metrics, localScore=null, candidate не получает фиктивный ноль. |
| Target profile отсутствует | Scorer не имеет права интерпретировать descriptors; допускается только diagnostic mode. |
| Owner metadata incomplete | Metric diagnostic-only; запись в unresolvedInputs[]. |
| Два разных semantic requests | Их localScore нельзя напрямую сравнивать как один общий scalar. |
Explainability + corrective loop
Scorer должен возвращать причину проигрыша и владельца, которому разрешено исправление.
Contribution loss
contributionLoss_i = normalizedEffectiveWeight_i × (1 - value_i) lowestPrimaryVotes = sort(selectedVotes by contributionLoss desc) take first 3
Это диагностический приоритет, а не новая метрика качества.
Repair merge
repairHints = mergeUnique( hardViolations.repairHints, lowestPrimaryVotes.repairHints, appliedPenalties.repairHints ) dirtyOwners = unique(repairHints.targetOwner)
2.20 ничего не исправляет самостоятельно.
Candidate generation boundary
Candidate generation остаётся в 2.19: bounded, seeded, reproducible, с lineage, hard filtering, deduplication и diversity pruning. 2.20 может только вернуть correction requests и dirtyOwners для ограниченного regeneration pass.
Визуальная лаборатория
Четыре случая показывают ключевую идею scorer: geometry descriptor сам по себе не «хороший» и не «плохой»; решение появляется только относительно role/target и owner evidence.
Одинаковый угол ≠ одинаковое качество
Angle, curvature, span, stroke width и другие descriptors не получают самостоятельный эстетический vote. Owner интерпретирует их относительно role/target и экспортирует fit.
Suppressed не значит потерян
Подавленный signal остаётся в suppressedVotes[] вместе с owner, value, evidence и reason. Это часть audit trail.
Глобальный solver может переопределить
Локально второй line candidate может победить позже из-за hierarchy, balance, negative space, typography или image interaction.
Live mini-scorer
Детерминированные smoke tests
Это не замена 2.21. Здесь проверяются только invariants самого scorer: N/A, ownership, dependency metadata, suppression, coverage, penalties и deterministic ranking.
| ID | Invariant | Expected |
|---|---|---|
| T01 | N/A не штрафует | metric excluded from denominator |
| T02 | Hard reject | localScore=null; rankEligible=false |
| T03 | Composite-first | component suppressed |
| T04 | Group cap | metric count cannot inflate family weight |
| T05 | Cross-group derived overlap | lower-priority duplicate suppressed |
| T06 | Explicit independent vote | both cross-group votes allowed only when both opt in |
| T07 | dependsOn only | no suppression by dependency alone |
| T08 | Target applicability | disabled group excluded |
| T09 | No global leakage | global.* cannot affect localScore |
| T10 | Confidence tie-break | within epsilon higher confidence wins |
| T11 | Deterministic feasible tie | repeat run → same order |
| T12 | Deterministic rejected tie | no undefined tie-break |
| T13 | Penalty cross-group dedup | same evidence applied once |
| T14 | Penalty vs primary vote dedup | same evidence not charged twice |
| T15 | Missing new-owner metadata | diagnostic-only + unresolvedInputs |
| T16 | Legacy adapter 2.13 | group + independent=false injected deterministically |
| T17 | Evidence coverage | missing expected group lowers confidence, not score via zero |
| T18 | Raw geometry descriptor | 2.01 angle/length cannot vote |
| T19 | Explicit target gate | missing target → diagnostic_only; localScore=null |
DATA FOR LAYOUT ENGINE
Machine-readable contract. Разработчик должен иметь возможность реализовать scorer почти без чтения пояснительного текста.
{
"module": "Compo 2.20",
"name": "line_system_scoring",
"version": "1.2-corrective",
"scope": "local scoring and deterministic ranking of Compo 2.19 line-system candidates under one explicit semantic/style target",
"consumes": [
"Compo2.19.LineStructureCandidate[]",
"MetricResult[] from Compo2.02-Compo2.18",
"PenaltyResult[] from metric owners",
"Candidate.hardViolations[]",
"Candidate.auditEvidence[]",
"Candidate.correctionCost?",
"Candidate.lineage",
"LineSystemTarget.semanticTarget",
"LineSystemTarget.styleTarget",
"LineScoringPolicy",
"Compo0.seed"
],
"provides": [
"LineSystemScoreResult",
"lineSystem.local_score",
"lineSystem.local_score_confidence",
"lineSystem.local_rank_key",
"lineSystem.suppression_log",
"repairHints[]",
"dirtyOwners[]"
],
"owns_metrics": [
"lineSystem.local_score",
"lineSystem.local_score_confidence",
"lineSystem.local_rank_key"
],
"descriptors_only": [
"suppression_log",
"group_breakdown",
"evidenceCoverage",
"targetCoverage",
"comparisonKey",
"audit"
],
"deferred_inputs": [
"global.balance",
"global.hierarchy",
"global.negativeSpace",
"global.eyeFlow",
"typography.*",
"imageCrop.*",
"posterGlobalScore",
"Compo2.23 context overrides"
],
"hard_constraints": [
"hard violation rejects before score",
"foreign mutation forbidden",
"raw geometry descriptors cannot vote",
"normalization belongs to metric owner",
"N/A excluded from vote and denominator",
"global metrics cannot influence localScore"
],
"soft_constraints": [
"owner penalty requires evidence",
"same evidence may not be penalized twice",
"group influence is capped",
"incomplete metadata becomes diagnostic-only"
],
"preferences": [
"composite-first",
"explicit semantic/style target",
"higher-confidence evidence has greater voting influence",
"lower correction cost wins only after score/coverage/confidence tie rules"
],
"overrides_allowed_by": [
"Compo2.23 context override layer",
"future global composition solver",
"explicit StylePreset / semantic target for weights and applicability only"
],
"initial_heuristics": {
"groupVoteCap": 1.0,
"maxSoftPenalty": 0.35,
"scoreEpsilon": 0.015,
"confidenceEpsilon": 0.03
},
"style_preset_values": {
"groupVoteCaps": "optional explicit per-group",
"semanticMultipliers": "optional explicit",
"styleMultipliers": "optional explicit",
"thresholds": "must carry STYLE_PRESET provenance"
},
"metric_policy": {
"range": [
0,
1
],
"higher_is_better": "owner target fit only, not universal beauty",
"normalization_owner": "metric_owner",
"composite_first": true,
"exclude_na_from_denominator": true,
"exclude_zero_confidence": true,
"required_dependency_metadata": [
"componentOf",
"dependsOn",
"derivedFrom",
"scoringGroup",
"independentVoteAllowed"
],
"incomplete_metadata": "diagnostic_only",
"double_counting": "check within scoringGroup and across scoringGroups"
},
"dependency_semantics": {
"componentOf": "suppress component when parent composite votes",
"derivedFrom": "lineage evidence used for duplicate suppression",
"dependsOn": "recompute/order dependency only; does not suppress by itself",
"scoringGroup": "vote-budget family",
"independentVoteAllowed": "shared evidence may vote independently only when both overlapping metrics explicitly opt in"
},
"legacy_metadata_adapter": {
"version": "line-owner-primary-bridge-v2",
"scope": "Compo2.02-Compo2.14 only; explicit primary allowlist; unknown legacy owner metrics are diagnostic-only",
"default_independentVoteAllowed": false,
"new_owners_2_15_2_18": "must export explicit metadata"
},
"group_policy": {
"groupWeight": "min(sum(scoreWeight), groupVoteCap)",
"additional_same_group_vote": "requires explicit independentVoteAllowed on both representations; shared lineage is allowed only by explicit bilateral opt-in; group cap still bounds total influence",
"group_cap_provenance": "INITIAL_HEURISTIC or STYLE_PRESET"
},
"aggregation": {
"policyWeight": "baseWeight * semanticMultiplier * styleMultiplier * applicability",
"scoreWeight": "policyWeight * confidence",
"groupValue": "weightedMean(metric.value by scoreWeight)",
"positiveScore": "weightedMean(groupValue by capped groupWeight)",
"softPenalty": "clamp(sum(deduplicated owner penalty contributions),0,maxSoftPenalty)",
"localScore": "clamp(positiveScore-softPenalty,0,1)"
},
"confidence": {
"groupConfidence": "weightedMean(metric.confidence by policyWeight without confidence factor)",
"sceneConfidence": "weightedMean(groupConfidence by active groupWeight)",
"evidenceCoverage": "presentExpectedGroups / expectedApplicableGroups",
"targetCoverage": "presentRequiredGroups / requiredGroups",
"finalConfidence": "sceneConfidence * evidenceCoverage",
"missing_required_group": "never inject value=0"
},
"penalty_policy": {
"deduplicate_across_groups": true,
"deduplicate_against_selected_votes": true,
"independent_exception": "explicit independentVoteAllowed on both representations"
},
"hard_policy": {
"any_owner_hard_violation": "REJECT",
"hard_violation_compensable_by_score": false,
"scorer_may_invent_hard_violation": false
},
"ranking": [
"feasibility",
"localScore outside scoreEpsilon",
"targetCoverage",
"confidence outside confidenceEpsilon",
"lower correctionCost",
"stable hash(candidateId|seed|lineageHash)"
],
"correction_policy": {
"direct_foreign_mutation": "FORBIDDEN",
"repair_source": "owner-issued repairHints / correction requests",
"orchestrator": "Compo 2.19",
"recompute_when": "dirty owner dependency graph indicates upstream evidence changed",
"recompute": "dirty owners only, then rescore"
},
"tests": [
"T01_NA",
"T02_HARD_REJECT",
"T03_COMPOSITE",
"T04_GROUP_CAP",
"T05_CROSS_GROUP_DERIVED",
"T06_INDEPENDENT_VOTE",
"T07_DEPENDSON_ONLY",
"T08_TARGET_APPLICABILITY",
"T09_NO_GLOBAL_LEAKAGE",
"T10_CONFIDENCE_TIE",
"T11_DETERMINISTIC_TIE",
"T12_REJECTED_TIE",
"T13_PENALTY_DEDUP",
"T14_PENALTY_VOTE_DEDUP",
"T15_MISSING_METADATA",
"T16_LEGACY_ADAPTER",
"T17_EVIDENCE_COVERAGE",
"T18_DESCRIPTOR_NOT_SCORE",
"T19_EXPLICIT_TARGET_GATE"
],
"recompute_when": [
"candidate geometry/relations changed by owner correction",
"semantic/style target changed",
"owner MetricResult/PenaltyResult changed",
"scoring policy or legacy adapter version changed"
],
"finality": "LOCAL_LINE_SYSTEM_RANKING_ONLY",
"invariants": [
"ONE_METRIC_ONE_OWNER",
"N_A_NOT_ZERO",
"LOCAL_NOT_GLOBAL",
"GEOMETRY_NOT_INTERPRETATION",
"DESCRIPTOR_NOT_SCORE",
"COMPOSITE_FIRST",
"NO_BEAUTYSCORE",
"NO_FOREIGN_MUTATION"
]
}