@aldus-runtime/regression 0.1.0

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 (48) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/dist/blindspots.d.ts +107 -0
  4. package/dist/blindspots.d.ts.map +1 -0
  5. package/dist/blindspots.js +169 -0
  6. package/dist/blindspots.js.map +1 -0
  7. package/dist/corpus.d.ts +184 -0
  8. package/dist/corpus.d.ts.map +1 -0
  9. package/dist/corpus.js +254 -0
  10. package/dist/corpus.js.map +1 -0
  11. package/dist/errors.d.ts +44 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +40 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +28 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +36 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/metrics.d.ts +134 -0
  20. package/dist/metrics.d.ts.map +1 -0
  21. package/dist/metrics.js +125 -0
  22. package/dist/metrics.js.map +1 -0
  23. package/dist/policy.d.ts +140 -0
  24. package/dist/policy.d.ts.map +1 -0
  25. package/dist/policy.js +148 -0
  26. package/dist/policy.js.map +1 -0
  27. package/dist/promotion.d.ts +108 -0
  28. package/dist/promotion.d.ts.map +1 -0
  29. package/dist/promotion.js +130 -0
  30. package/dist/promotion.js.map +1 -0
  31. package/dist/report.d.ts +42 -0
  32. package/dist/report.d.ts.map +1 -0
  33. package/dist/report.js +116 -0
  34. package/dist/report.js.map +1 -0
  35. package/dist/scope.d.ts +57 -0
  36. package/dist/scope.d.ts.map +1 -0
  37. package/dist/scope.js +81 -0
  38. package/dist/scope.js.map +1 -0
  39. package/package.json +49 -0
  40. package/src/blindspots.ts +199 -0
  41. package/src/corpus.ts +311 -0
  42. package/src/errors.ts +49 -0
  43. package/src/index.ts +104 -0
  44. package/src/metrics.ts +293 -0
  45. package/src/policy.ts +267 -0
  46. package/src/promotion.ts +336 -0
  47. package/src/report.ts +154 -0
  48. package/src/scope.ts +100 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Human/evaluator comparison and the §12.1 metrics.
3
+ *
4
+ * Every name here is chosen to keep one distinction visible: these numbers describe **agreement
5
+ * with human labels on one corpus**, not correctness. §12 states that "machine pass MUST NOT be
6
+ * presented as semantic correctness", and the easiest way to violate that is to call a field
7
+ * `accuracy` and let a reader draw the obvious conclusion. So the aggregate is
8
+ * `agreementWithHumanLabels`, and nothing in this module is named `correct`.
9
+ *
10
+ * The metrics are those §12.1 requires be considered: recall, false-positive rate,
11
+ * severity-weighted false negatives, and harm from unnecessary automatic correction — the last
12
+ * kept separate from the false-positive rate on purpose (see `policy.ts`).
13
+ */
14
+ import type { DefectCorpus, EvaluatorRun } from "./corpus.js";
15
+ import { type PromotionPolicy } from "./policy.js";
16
+ import { type ScopeSelector } from "./scope.js";
17
+ /** How one case and one outcome agreed or disagreed. */
18
+ export type CaseVerdict =
19
+ /** Human said defective, evaluator flagged it. */
20
+ "truePositive"
21
+ /** Human said defective, evaluator did not flag it. A miss. */
22
+ | "falseNegative"
23
+ /** Human said clean, evaluator flagged it. A spurious flag. */
24
+ | "falsePositive"
25
+ /** Human said clean, evaluator did not flag it. */
26
+ | "trueNegative";
27
+ /** One case paired with what the evaluator said about it. */
28
+ export interface CaseComparison {
29
+ /** The case. */
30
+ caseId: string;
31
+ /** Scope of the case. */
32
+ scope: Readonly<Record<string, string>>;
33
+ /** How they agreed. */
34
+ verdict: CaseVerdict;
35
+ /** Severity of the case, when a human labelled it defective. */
36
+ severity: string | undefined;
37
+ /** Severity weight, when applicable. */
38
+ severityWeight: number;
39
+ /**
40
+ * Whether a true positive was found under a different category than the human assigned.
41
+ *
42
+ * §12.3 structures findings by category, so an evaluator that flags the right case for the
43
+ * wrong reason has not fully agreed. Counted separately rather than downgraded to a false
44
+ * negative — it did catch the case — but a promotion reader should see it.
45
+ */
46
+ categoryMismatch: boolean;
47
+ /** Harm a spurious flag would cause here; zero unless this is a false positive. */
48
+ unnecessaryCorrectionHarm: number;
49
+ }
50
+ /** Metrics over one slice of the corpus. */
51
+ export interface SliceMetrics {
52
+ /** Which slice. */
53
+ selector: ScopeSelector;
54
+ /** Stable key for the slice. */
55
+ key: string;
56
+ /** Total cases in the slice. */
57
+ cases: number;
58
+ /** Cases a human labelled defective. */
59
+ defectiveCases: number;
60
+ /** Cases a human labelled clean. */
61
+ cleanCases: number;
62
+ /** Distinct human labellers represented. */
63
+ labellers: number;
64
+ /** Confusion counts. */
65
+ truePositives: number;
66
+ falseNegatives: number;
67
+ falsePositives: number;
68
+ trueNegatives: number;
69
+ /** True positives found under the wrong category (contract §12.3). */
70
+ categoryMismatches: number;
71
+ /** `truePositives / defectiveCases`; `undefined` when there are no defective cases. */
72
+ recall: number | undefined;
73
+ /** `falsePositives / cleanCases`; `undefined` when there are no clean cases. */
74
+ falsePositiveRate: number | undefined;
75
+ /** Severity-weighted recall (contract §12.1); `undefined` when there is nothing to weigh. */
76
+ severityWeightedRecall: number | undefined;
77
+ /** Total severity weight of missed defects (contract §12.1). */
78
+ severityWeightedFalseNegatives: number;
79
+ /** Total harm from spurious flags (contract §12.1, §12.4). */
80
+ unnecessaryCorrectionHarm: number;
81
+ /** Mean harm per clean case; `undefined` when there are no clean cases. */
82
+ meanUnnecessaryCorrectionHarm: number | undefined;
83
+ /**
84
+ * Fraction of cases where the evaluator and the human agreed.
85
+ *
86
+ * Named for what it measures. This is **not** accuracy, and a reader who treats it as one has
87
+ * made exactly the mistake §12 forbids.
88
+ */
89
+ agreementWithHumanLabels: number | undefined;
90
+ /** Case ids in this slice, for blind-spot cross-referencing. */
91
+ caseIds: readonly string[];
92
+ }
93
+ /** The full comparison: per-slice metrics, and a descriptive whole-corpus figure. */
94
+ export interface ComparisonReport {
95
+ /** Evaluator compared. */
96
+ evaluatorId: string;
97
+ /** Version compared. Metrics from different versions are not comparable. */
98
+ evaluatorVersion: string;
99
+ /** Corpus compared against. */
100
+ corpusId: string;
101
+ /**
102
+ * Whole-corpus metrics.
103
+ *
104
+ * **Descriptive only.** Promotion is decided per slice and never from this figure — ADR-0010
105
+ * decision 3. It is reported because a reader wants the shape of the corpus, not because it is
106
+ * evidence.
107
+ */
108
+ wholeCorpus: SliceMetrics;
109
+ /** Per-slice metrics (contract §12.1). Promotion is decided from these. */
110
+ slices: readonly SliceMetrics[];
111
+ /** Every case comparison, for callers that need the detail. */
112
+ comparisons: readonly CaseComparison[];
113
+ /** Cases in the corpus the evaluator run did not report on. */
114
+ unevaluatedCaseIds: readonly string[];
115
+ }
116
+ /** Options for {@link compareRun}. */
117
+ export interface CompareOptions {
118
+ /**
119
+ * Dimension groupings to slice by. Defaults to each observed dimension individually.
120
+ *
121
+ * See `deriveScopeSelectors` for why the cross-product is not the default.
122
+ */
123
+ scopeGroupings?: readonly (readonly string[])[];
124
+ }
125
+ /**
126
+ * Compare an evaluator run against a corpus (contract §12.1).
127
+ *
128
+ * @throws {AldusError} `ALDUS_OUTCOME_UNKNOWN_CASE` when the run reports on a case the corpus
129
+ * does not contain. Refused rather than ignored: an outcome for an unknown case means the run
130
+ * and the corpus disagree about what was tested, and silently dropping it would compute metrics
131
+ * over a set neither party described.
132
+ */
133
+ export declare function compareRun(corpus: DefectCorpus, run: EvaluatorRun, policy: PromotionPolicy, options?: CompareOptions): ComparisonReport;
134
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Human/evaluator comparison and the §12.1 metrics.
3
+ *
4
+ * Every name here is chosen to keep one distinction visible: these numbers describe **agreement
5
+ * with human labels on one corpus**, not correctness. §12 states that "machine pass MUST NOT be
6
+ * presented as semantic correctness", and the easiest way to violate that is to call a field
7
+ * `accuracy` and let a reader draw the obvious conclusion. So the aggregate is
8
+ * `agreementWithHumanLabels`, and nothing in this module is named `correct`.
9
+ *
10
+ * The metrics are those §12.1 requires be considered: recall, false-positive rate,
11
+ * severity-weighted false negatives, and harm from unnecessary automatic correction — the last
12
+ * kept separate from the false-positive rate on purpose (see `policy.ts`).
13
+ */
14
+ import { RegressionErrorCodes, regressionError } from "./errors.js";
15
+ import { correctionHarm, severityWeight } from "./policy.js";
16
+ import { deriveScopeSelectors, scopeKey, scopeMatches, WHOLE_CORPUS_SLICE, } from "./scope.js";
17
+ /** Compare one case to one outcome. */
18
+ function compareCase(entry, outcome, policy) {
19
+ const flagged = outcome?.flagged ?? false;
20
+ const weight = entry.severity === undefined ? 0 : severityWeight(policy, entry.severity);
21
+ let verdict;
22
+ if (entry.defective)
23
+ verdict = flagged ? "truePositive" : "falseNegative";
24
+ else
25
+ verdict = flagged ? "falsePositive" : "trueNegative";
26
+ const humanCategories = new Set(entry.findings.map((finding) => finding.category));
27
+ const evaluatorCategories = new Set((outcome?.findings ?? []).map((finding) => finding.category));
28
+ const categoryMismatch = verdict === "truePositive" &&
29
+ humanCategories.size > 0 &&
30
+ evaluatorCategories.size > 0 &&
31
+ ![...evaluatorCategories].some((category) => humanCategories.has(category));
32
+ const harm = verdict === "falsePositive"
33
+ ? correctionHarm(policy, entry.correctionOnFlag ?? policy.defaultCorrectionClass)
34
+ : 0;
35
+ return {
36
+ caseId: entry.caseId,
37
+ scope: entry.scope,
38
+ verdict,
39
+ severity: entry.severity,
40
+ severityWeight: weight,
41
+ categoryMismatch,
42
+ unnecessaryCorrectionHarm: harm,
43
+ };
44
+ }
45
+ /** Aggregate comparisons into metrics for one slice. */
46
+ function summarise(selector, comparisons, labellers) {
47
+ const count = (verdict) => comparisons.filter((comparison) => comparison.verdict === verdict).length;
48
+ const truePositives = count("truePositive");
49
+ const falseNegatives = count("falseNegative");
50
+ const falsePositives = count("falsePositive");
51
+ const trueNegatives = count("trueNegative");
52
+ const defectiveCases = truePositives + falseNegatives;
53
+ const cleanCases = falsePositives + trueNegatives;
54
+ const cases = comparisons.length;
55
+ const caughtWeight = comparisons
56
+ .filter((comparison) => comparison.verdict === "truePositive")
57
+ .reduce((total, comparison) => total + comparison.severityWeight, 0);
58
+ const missedWeight = comparisons
59
+ .filter((comparison) => comparison.verdict === "falseNegative")
60
+ .reduce((total, comparison) => total + comparison.severityWeight, 0);
61
+ const defectiveWeight = caughtWeight + missedWeight;
62
+ const harm = comparisons.reduce((total, comparison) => total + comparison.unnecessaryCorrectionHarm, 0);
63
+ return {
64
+ selector,
65
+ key: scopeKey(selector),
66
+ cases,
67
+ defectiveCases,
68
+ cleanCases,
69
+ labellers: labellers.size,
70
+ truePositives,
71
+ falseNegatives,
72
+ falsePositives,
73
+ trueNegatives,
74
+ categoryMismatches: comparisons.filter((comparison) => comparison.categoryMismatch).length,
75
+ recall: defectiveCases === 0 ? undefined : truePositives / defectiveCases,
76
+ falsePositiveRate: cleanCases === 0 ? undefined : falsePositives / cleanCases,
77
+ severityWeightedRecall: defectiveWeight === 0 ? undefined : caughtWeight / defectiveWeight,
78
+ severityWeightedFalseNegatives: missedWeight,
79
+ unnecessaryCorrectionHarm: harm,
80
+ meanUnnecessaryCorrectionHarm: cleanCases === 0 ? undefined : harm / cleanCases,
81
+ agreementWithHumanLabels: cases === 0 ? undefined : (truePositives + trueNegatives) / cases,
82
+ caseIds: comparisons.map((comparison) => comparison.caseId),
83
+ };
84
+ }
85
+ /**
86
+ * Compare an evaluator run against a corpus (contract §12.1).
87
+ *
88
+ * @throws {AldusError} `ALDUS_OUTCOME_UNKNOWN_CASE` when the run reports on a case the corpus
89
+ * does not contain. Refused rather than ignored: an outcome for an unknown case means the run
90
+ * and the corpus disagree about what was tested, and silently dropping it would compute metrics
91
+ * over a set neither party described.
92
+ */
93
+ export function compareRun(corpus, run, policy, options = {}) {
94
+ const byCaseId = new Map(corpus.cases.map((entry) => [entry.caseId, entry]));
95
+ for (const outcome of run.outcomes) {
96
+ if (!byCaseId.has(outcome.caseId)) {
97
+ throw regressionError(RegressionErrorCodes.OUTCOME_UNKNOWN_CASE, `The evaluator run reports on case "${outcome.caseId}", which corpus ` +
98
+ `"${corpus.corpusId}" does not contain.`, {
99
+ category: "validation",
100
+ details: { caseId: outcome.caseId, corpusId: corpus.corpusId },
101
+ });
102
+ }
103
+ }
104
+ const outcomeByCaseId = new Map(run.outcomes.map((outcome) => [outcome.caseId, outcome]));
105
+ const comparisons = corpus.cases.map((entry) => compareCase(entry, outcomeByCaseId.get(entry.caseId), policy));
106
+ const labellersFor = (subset) => new Set(subset.map((entry) => entry.labelledBy.id));
107
+ const selectors = deriveScopeSelectors(corpus.cases.map((entry) => entry.scope), options.scopeGroupings);
108
+ const slices = selectors.map((selector) => {
109
+ const matching = corpus.cases.filter((entry) => scopeMatches(entry.scope, selector));
110
+ const matchingIds = new Set(matching.map((entry) => entry.caseId));
111
+ return summarise(selector, comparisons.filter((comparison) => matchingIds.has(comparison.caseId)), labellersFor(matching));
112
+ });
113
+ return {
114
+ evaluatorId: run.evaluatorId,
115
+ evaluatorVersion: run.evaluatorVersion,
116
+ corpusId: corpus.corpusId,
117
+ wholeCorpus: summarise(WHOLE_CORPUS_SLICE, comparisons, labellersFor(corpus.cases)),
118
+ slices,
119
+ comparisons,
120
+ unevaluatedCaseIds: corpus.cases
121
+ .filter((entry) => !outcomeByCaseId.has(entry.caseId))
122
+ .map((entry) => entry.caseId),
123
+ };
124
+ }
125
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Promotion policy: the evidence bar an evaluator must clear (architecture contract §12.1,
3
+ * §25 item 9; ADR-0010).
4
+ *
5
+ * §12.1 says an evaluator "MAY become blocking only after it is calibrated against human-labeled
6
+ * examples" and lists what promotion should consider. It does not say how much evidence is
7
+ * enough — §25 item 9 records that as an open question. ADR-0010 closes it with the defaults
8
+ * below.
9
+ *
10
+ * **The defaults are uncalibrated.** Nobody has run a real corpus through them. They are a
11
+ * starting point chosen to fail closed, and every verdict computed against them says so in its
12
+ * output — see `PromotionVerdict.policyOrigin`. A threshold presented as authoritative when
13
+ * nothing validated it would be its own failure of the honesty §12 demands.
14
+ */
15
+ /**
16
+ * How much a missed defect of each severity costs.
17
+ *
18
+ * §12.1 requires "severity-weighted false negatives", not a count: a missed unsupported claim
19
+ * and a missed cosmetic wobble are not one unit of the same currency. Levels are caller-named
20
+ * (see `severityLevel`), so weights are supplied rather than assumed.
21
+ */
22
+ export type SeverityWeights = Readonly<Record<string, number>>;
23
+ /**
24
+ * How much harm an unnecessary automatic correction does, by correction class.
25
+ *
26
+ * §12.1 names "asymmetric harm caused by unnecessary automatic correction" as a consideration
27
+ * *separate from* the false-positive rate, and §12.4 explains why: repairs differ by layer. A
28
+ * spurious flag that regenerates one TTS segment costs a request. A spurious flag that revises
29
+ * narration invalidates the Content Freeze and every approval downstream of it (§13.1). Charging
30
+ * both to one precision figure would hide exactly the asymmetry §12.1 asks to be weighed.
31
+ */
32
+ export type CorrectionHarmWeights = Readonly<Record<string, number>>;
33
+ /** Thresholds a scope slice must clear to be reported as promotable. */
34
+ export interface PromotionThresholds {
35
+ /** Minimum labelled cases in the slice. */
36
+ minCases: number;
37
+ /** Minimum cases a human labelled defective. Recall over three positives is not a measurement. */
38
+ minDefectiveCases: number;
39
+ /** Minimum cases a human labelled clean. Without these, the false-positive rate is unmeasured. */
40
+ minCleanCases: number;
41
+ /** Minimum recall, in `[0, 1]` (contract §12.1). */
42
+ minRecall: number;
43
+ /** Minimum severity-weighted recall, in `[0, 1]` (contract §12.1). */
44
+ minSeverityWeightedRecall: number;
45
+ /** Maximum false-positive rate, in `[0, 1]` (contract §12.1). */
46
+ maxFalsePositiveRate: number;
47
+ /**
48
+ * Maximum mean unnecessary-correction harm per clean case (contract §12.1).
49
+ *
50
+ * Separate from `maxFalsePositiveRate` on purpose: an evaluator can clear the rate while every
51
+ * one of its few false positives triggers a cascading rewrite.
52
+ */
53
+ maxUnnecessaryCorrectionHarm: number;
54
+ /**
55
+ * Minimum distinct human labellers in the slice.
56
+ *
57
+ * Defaults to 1 and is documented as a floor rather than a recommendation: a single-labeller
58
+ * corpus has no inter-rater signal at all, so its labels are one person's judgement presented
59
+ * as an oracle. Raise this once more than one labeller exists.
60
+ */
61
+ minLabellers: number;
62
+ }
63
+ /** A full promotion policy. */
64
+ export interface PromotionPolicy {
65
+ /** Thresholds every scope slice must clear. */
66
+ thresholds: PromotionThresholds;
67
+ /** Weight per severity level (contract §12.1). */
68
+ severityWeights: SeverityWeights;
69
+ /** Harm weight per correction class (contract §12.1, §12.4). */
70
+ correctionHarmWeights: CorrectionHarmWeights;
71
+ /** Correction class applied to a case that names none. */
72
+ defaultCorrectionClass: string;
73
+ /**
74
+ * Whether an open blind spot in a slice disqualifies it regardless of metrics.
75
+ *
76
+ * Defaults to `true`. §12.1 lists "known blind spots" alongside the numeric considerations,
77
+ * and a blind spot is by definition a failure the corpus did not sample — so good metrics are
78
+ * not evidence against it, they are evidence that the corpus did not look.
79
+ */
80
+ openBlindSpotDisqualifies: boolean;
81
+ /**
82
+ * Whether the policy came from this package's uncalibrated defaults.
83
+ *
84
+ * Carried into every verdict so a reader can tell an evidence-based bar from a placeholder.
85
+ */
86
+ origin: PolicyOrigin;
87
+ }
88
+ /** Where a policy's numbers came from. */
89
+ export type PolicyOrigin =
90
+ /** This package's defaults. Not validated against any real corpus (ADR-0010). */
91
+ "default-uncalibrated"
92
+ /** Supplied by an adopter. */
93
+ | "configured";
94
+ /**
95
+ * The default correction classes, transcribed from contract §12.4's repair ladder.
96
+ *
97
+ * §12.4 orders repairs by "the smallest safe layer", and the weights follow that ordering: a
98
+ * scoped regeneration is cheap and reversible, a narration rewrite invalidates the Content
99
+ * Freeze and everything downstream (§13.1), and escalating to a human costs attention rather
100
+ * than content. These are defaults an adopter replaces.
101
+ */
102
+ export declare const DEFAULT_CORRECTION_HARM_WEIGHTS: CorrectionHarmWeights;
103
+ /**
104
+ * Default thresholds (ADR-0010, closing contract §25 item 9).
105
+ *
106
+ * Chosen to fail closed. A blocking evaluator that misses defects is worse than an advisory one,
107
+ * because §12 warns that "machine pass MUST NOT be presented as semantic correctness" — and a
108
+ * hard gate that passes is precisely such a presentation. **Uncalibrated:** see the module
109
+ * comment.
110
+ */
111
+ export declare const DEFAULT_PROMOTION_THRESHOLDS: PromotionThresholds;
112
+ /**
113
+ * A policy built from this package's uncalibrated defaults.
114
+ *
115
+ * `severityWeights` is empty: severity levels are caller-named, so there is nothing to default
116
+ * to. A corpus using an unweighted severity is refused rather than counted as zero — see
117
+ * `ALDUS_SEVERITY_UNWEIGHTED`.
118
+ */
119
+ export declare function defaultPromotionPolicy(severityWeights: SeverityWeights, overrides?: Partial<Omit<PromotionPolicy, "origin">>): PromotionPolicy;
120
+ /**
121
+ * Reject a policy whose numbers cannot mean what they claim.
122
+ *
123
+ * @throws {AldusError} `ALDUS_POLICY_INVALID`.
124
+ */
125
+ export declare function assertPolicyValid(policy: PromotionPolicy): void;
126
+ /**
127
+ * Weight of a severity level.
128
+ *
129
+ * @throws {AldusError} `ALDUS_SEVERITY_UNWEIGHTED` when the policy has no weight for it. Refused
130
+ * rather than defaulted to zero: a silently unweighted severity drops a missed defect out of the
131
+ * severity-weighted recall §12.1 requires, and the metric still reports a plausible number.
132
+ */
133
+ export declare function severityWeight(policy: PromotionPolicy, severity: string): number;
134
+ /**
135
+ * Harm weight of a correction class.
136
+ *
137
+ * @throws {AldusError} `ALDUS_CORRECTION_CLASS_UNWEIGHTED` when the policy has no weight for it.
138
+ */
139
+ export declare function correctionHarm(policy: PromotionPolicy, correctionClass: string): number;
140
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAErE,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,kGAAkG;IAClG,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kGAAkG;IAClG,aAAa,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,yBAAyB,EAAE,MAAM,CAAC;IAClC,iEAAiE;IACjE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,4BAA4B,EAAE,MAAM,CAAC;IACrC;;;;;;OAMG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,+BAA+B;AAC/B,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,UAAU,EAAE,mBAAmB,CAAC;IAChC,kDAAkD;IAClD,eAAe,EAAE,eAAe,CAAC;IACjC,gEAAgE;IAChE,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,0DAA0D;IAC1D,sBAAsB,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,yBAAyB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,0CAA0C;AAC1C,MAAM,MAAM,YAAY;AACtB,iFAAiF;AAC/E,sBAAsB;AACxB,8BAA8B;GAC5B,YAAY,CAAC;AAEjB;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,EAAE,qBAa7C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,EAAE,mBAS1C,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,eAAe,EAAE,eAAe,EAChC,SAAS,GAAE,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAM,GACvD,eAAe,CAajB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAiD/D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAehF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAavF"}
package/dist/policy.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Promotion policy: the evidence bar an evaluator must clear (architecture contract §12.1,
3
+ * §25 item 9; ADR-0010).
4
+ *
5
+ * §12.1 says an evaluator "MAY become blocking only after it is calibrated against human-labeled
6
+ * examples" and lists what promotion should consider. It does not say how much evidence is
7
+ * enough — §25 item 9 records that as an open question. ADR-0010 closes it with the defaults
8
+ * below.
9
+ *
10
+ * **The defaults are uncalibrated.** Nobody has run a real corpus through them. They are a
11
+ * starting point chosen to fail closed, and every verdict computed against them says so in its
12
+ * output — see `PromotionVerdict.policyOrigin`. A threshold presented as authoritative when
13
+ * nothing validated it would be its own failure of the honesty §12 demands.
14
+ */
15
+ import { RegressionErrorCodes, regressionError } from "./errors.js";
16
+ /**
17
+ * The default correction classes, transcribed from contract §12.4's repair ladder.
18
+ *
19
+ * §12.4 orders repairs by "the smallest safe layer", and the weights follow that ordering: a
20
+ * scoped regeneration is cheap and reversible, a narration rewrite invalidates the Content
21
+ * Freeze and everything downstream (§13.1), and escalating to a human costs attention rather
22
+ * than content. These are defaults an adopter replaces.
23
+ */
24
+ export const DEFAULT_CORRECTION_HARM_WEIGHTS = {
25
+ /** Reported, nothing changes automatically. The advisory case (§12 level 2). */
26
+ advisory: 0,
27
+ /** A human is asked to look (§12.4 "escalate to human"). */
28
+ escalate: 0.5,
29
+ /** Regenerate only the affected segment (§12.4). */
30
+ regenerateSegment: 1,
31
+ /** Change provider mapping without rewriting content (§12.4). */
32
+ remapProvider: 2,
33
+ /** Change the PerformanceScript without altering approved claims (§12.4). */
34
+ reperform: 3,
35
+ /** Revise narration, invalidating the Content Freeze and dependent approvals (§12.4, §13.1). */
36
+ reviseNarration: 10,
37
+ };
38
+ /**
39
+ * Default thresholds (ADR-0010, closing contract §25 item 9).
40
+ *
41
+ * Chosen to fail closed. A blocking evaluator that misses defects is worse than an advisory one,
42
+ * because §12 warns that "machine pass MUST NOT be presented as semantic correctness" — and a
43
+ * hard gate that passes is precisely such a presentation. **Uncalibrated:** see the module
44
+ * comment.
45
+ */
46
+ export const DEFAULT_PROMOTION_THRESHOLDS = {
47
+ minCases: 50,
48
+ minDefectiveCases: 20,
49
+ minCleanCases: 20,
50
+ minRecall: 0.95,
51
+ minSeverityWeightedRecall: 0.98,
52
+ maxFalsePositiveRate: 0.05,
53
+ maxUnnecessaryCorrectionHarm: 0.1,
54
+ minLabellers: 1,
55
+ };
56
+ /**
57
+ * A policy built from this package's uncalibrated defaults.
58
+ *
59
+ * `severityWeights` is empty: severity levels are caller-named, so there is nothing to default
60
+ * to. A corpus using an unweighted severity is refused rather than counted as zero — see
61
+ * `ALDUS_SEVERITY_UNWEIGHTED`.
62
+ */
63
+ export function defaultPromotionPolicy(severityWeights, overrides = {}) {
64
+ const policy = {
65
+ thresholds: { ...DEFAULT_PROMOTION_THRESHOLDS, ...overrides.thresholds },
66
+ severityWeights: overrides.severityWeights ?? severityWeights,
67
+ correctionHarmWeights: overrides.correctionHarmWeights ?? DEFAULT_CORRECTION_HARM_WEIGHTS,
68
+ defaultCorrectionClass: overrides.defaultCorrectionClass ?? "advisory",
69
+ openBlindSpotDisqualifies: overrides.openBlindSpotDisqualifies ?? true,
70
+ // Any override of a threshold or weight means an adopter has taken a position, so the
71
+ // verdict should stop describing itself as uncalibrated.
72
+ origin: Object.keys(overrides).length === 0 ? "default-uncalibrated" : "configured",
73
+ };
74
+ assertPolicyValid(policy);
75
+ return policy;
76
+ }
77
+ /**
78
+ * Reject a policy whose numbers cannot mean what they claim.
79
+ *
80
+ * @throws {AldusError} `ALDUS_POLICY_INVALID`.
81
+ */
82
+ export function assertPolicyValid(policy) {
83
+ const { thresholds } = policy;
84
+ const rates = [
85
+ ["minRecall", thresholds.minRecall],
86
+ ["minSeverityWeightedRecall", thresholds.minSeverityWeightedRecall],
87
+ ["maxFalsePositiveRate", thresholds.maxFalsePositiveRate],
88
+ ];
89
+ for (const [name, value] of rates) {
90
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
91
+ throw regressionError(RegressionErrorCodes.POLICY_INVALID, `Promotion threshold "${name}" must be a rate in [0, 1]; received ${value}.`, { category: "validation", details: { threshold: name, value } });
92
+ }
93
+ }
94
+ const counts = [
95
+ ["minCases", thresholds.minCases],
96
+ ["minDefectiveCases", thresholds.minDefectiveCases],
97
+ ["minCleanCases", thresholds.minCleanCases],
98
+ ["minLabellers", thresholds.minLabellers],
99
+ ];
100
+ for (const [name, value] of counts) {
101
+ if (!Number.isInteger(value) || value < 0) {
102
+ throw regressionError(RegressionErrorCodes.POLICY_INVALID, `Promotion threshold "${name}" must be a non-negative integer; received ${value}.`, { category: "validation", details: { threshold: name, value } });
103
+ }
104
+ }
105
+ if (thresholds.minDefectiveCases + thresholds.minCleanCases > thresholds.minCases) {
106
+ throw regressionError(RegressionErrorCodes.POLICY_INVALID, "minCases is smaller than minDefectiveCases + minCleanCases, so no slice could ever " +
107
+ "satisfy all three at once.", { category: "validation", details: { thresholds } });
108
+ }
109
+ if (!Number.isFinite(thresholds.maxUnnecessaryCorrectionHarm) ||
110
+ thresholds.maxUnnecessaryCorrectionHarm < 0) {
111
+ throw regressionError(RegressionErrorCodes.POLICY_INVALID, "maxUnnecessaryCorrectionHarm must be a non-negative finite number.", { category: "validation", details: { thresholds } });
112
+ }
113
+ }
114
+ /**
115
+ * Weight of a severity level.
116
+ *
117
+ * @throws {AldusError} `ALDUS_SEVERITY_UNWEIGHTED` when the policy has no weight for it. Refused
118
+ * rather than defaulted to zero: a silently unweighted severity drops a missed defect out of the
119
+ * severity-weighted recall §12.1 requires, and the metric still reports a plausible number.
120
+ */
121
+ export function severityWeight(policy, severity) {
122
+ const weight = policy.severityWeights[severity];
123
+ if (weight === undefined) {
124
+ throw regressionError(RegressionErrorCodes.SEVERITY_UNWEIGHTED, `The promotion policy assigns no weight to severity "${severity}". Weighting it as zero ` +
125
+ "would drop every case at that severity out of severity-weighted recall while still " +
126
+ "reporting a number (architecture contract §12.1).", {
127
+ category: "validation",
128
+ details: { severity, known: Object.keys(policy.severityWeights).sort() },
129
+ });
130
+ }
131
+ return weight;
132
+ }
133
+ /**
134
+ * Harm weight of a correction class.
135
+ *
136
+ * @throws {AldusError} `ALDUS_CORRECTION_CLASS_UNWEIGHTED` when the policy has no weight for it.
137
+ */
138
+ export function correctionHarm(policy, correctionClass) {
139
+ const weight = policy.correctionHarmWeights[correctionClass];
140
+ if (weight === undefined) {
141
+ throw regressionError(RegressionErrorCodes.CORRECTION_CLASS_UNWEIGHTED, `The promotion policy assigns no harm weight to correction class "${correctionClass}".`, {
142
+ category: "validation",
143
+ details: { correctionClass, known: Object.keys(policy.correctionHarmWeights).sort() },
144
+ });
145
+ }
146
+ return weight;
147
+ }
148
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAsFpE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAA0B;IACpE,gFAAgF;IAChF,QAAQ,EAAE,CAAC;IACX,4DAA4D;IAC5D,QAAQ,EAAE,GAAG;IACb,oDAAoD;IACpD,iBAAiB,EAAE,CAAC;IACpB,iEAAiE;IACjE,aAAa,EAAE,CAAC;IAChB,6EAA6E;IAC7E,SAAS,EAAE,CAAC;IACZ,gGAAgG;IAChG,eAAe,EAAE,EAAE;CACpB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAwB;IAC/D,QAAQ,EAAE,EAAE;IACZ,iBAAiB,EAAE,EAAE;IACrB,aAAa,EAAE,EAAE;IACjB,SAAS,EAAE,IAAI;IACf,yBAAyB,EAAE,IAAI;IAC/B,oBAAoB,EAAE,IAAI;IAC1B,4BAA4B,EAAE,GAAG;IACjC,YAAY,EAAE,CAAC;CAChB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,eAAgC,EAChC,SAAS,GAA6C,EAAE;IAExD,MAAM,MAAM,GAAoB;QAC9B,UAAU,EAAE,EAAE,GAAG,4BAA4B,EAAE,GAAG,SAAS,CAAC,UAAU,EAAE;QACxE,eAAe,EAAE,SAAS,CAAC,eAAe,IAAI,eAAe;QAC7D,qBAAqB,EAAE,SAAS,CAAC,qBAAqB,IAAI,+BAA+B;QACzF,sBAAsB,EAAE,SAAS,CAAC,sBAAsB,IAAI,UAAU;QACtE,yBAAyB,EAAE,SAAS,CAAC,yBAAyB,IAAI,IAAI;QACtE,sFAAsF;QACtF,yDAAyD;QACzD,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,YAAY;KACpF,CAAC;IACF,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAuB;IACvD,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC;IAC9B,MAAM,KAAK,GAAuB;QAChC,CAAC,WAAW,EAAE,UAAU,CAAC,SAAS,CAAC;QACnC,CAAC,2BAA2B,EAAE,UAAU,CAAC,yBAAyB,CAAC;QACnE,CAAC,sBAAsB,EAAE,UAAU,CAAC,oBAAoB,CAAC;KAC1D,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACtD,MAAM,eAAe,CACnB,oBAAoB,CAAC,cAAc,EACnC,wBAAwB,IAAI,wCAAwC,KAAK,GAAG,EAC5E,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAChE,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAuB;QACjC,CAAC,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC;QACjC,CAAC,mBAAmB,EAAE,UAAU,CAAC,iBAAiB,CAAC;QACnD,CAAC,eAAe,EAAE,UAAU,CAAC,aAAa,CAAC;QAC3C,CAAC,cAAc,EAAE,UAAU,CAAC,YAAY,CAAC;KAC1C,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,eAAe,CACnB,oBAAoB,CAAC,cAAc,EACnC,wBAAwB,IAAI,8CAA8C,KAAK,GAAG,EAClF,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAChE,CAAC;QACJ,CAAC;IACH,CAAC;IACD,IAAI,UAAU,CAAC,iBAAiB,GAAG,UAAU,CAAC,aAAa,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAC;QAClF,MAAM,eAAe,CACnB,oBAAoB,CAAC,cAAc,EACnC,qFAAqF;YACnF,4BAA4B,EAC9B,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,EAAE,CACpD,CAAC;IACJ,CAAC;IACD,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,4BAA4B,CAAC;QACzD,UAAU,CAAC,4BAA4B,GAAG,CAAC,EAC3C,CAAC;QACD,MAAM,eAAe,CACnB,oBAAoB,CAAC,cAAc,EACnC,oEAAoE,EACpE,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,EAAE,CACpD,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,MAAuB,EAAE,QAAgB;IACtE,MAAM,MAAM,GAAG,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,eAAe,CACnB,oBAAoB,CAAC,mBAAmB,EACxC,uDAAuD,QAAQ,0BAA0B;YACvF,qFAAqF;YACrF,mDAAmD,EACrD;YACE,QAAQ,EAAE,YAAY;YACtB,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,IAAI,EAAE,EAAE;SACzE,CACF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAuB,EAAE,eAAuB;IAC7E,MAAM,MAAM,GAAG,MAAM,CAAC,qBAAqB,CAAC,eAAe,CAAC,CAAC;IAC7D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,eAAe,CACnB,oBAAoB,CAAC,2BAA2B,EAChD,oEAAoE,eAAe,IAAI,EACvF;YACE,QAAQ,EAAE,YAAY;YACtB,OAAO,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,IAAI,EAAE,EAAE;SACtF,CACF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}