@event4u/agent-config 14.13.0 → 14.14.1
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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +138 -376
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +39 -2
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1696 -812
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +297 -115
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +211 -1
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +79 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +113 -14
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +234 -9
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +748 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +199 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +142 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The promotion evidence package — `road-to-harness-promotion-bridge` steps
|
|
3
|
+
* 7.1, 7.2, 7.3 and 7.5.
|
|
4
|
+
*
|
|
5
|
+
* ## What this module is, and what it deliberately is not
|
|
6
|
+
*
|
|
7
|
+
* It is a REFUSING VALIDATOR. It reads an untrusted evidence document and either
|
|
8
|
+
* returns a typed package or throws naming the first thing missing. It performs
|
|
9
|
+
* no filesystem write, mints no capability, and expresses no promotion — the
|
|
10
|
+
* capability stays `_lib/promotion_capability.ts` and the structural invariant
|
|
11
|
+
* that nothing bypasses it stays `lint_promotion_paths.ts`.
|
|
12
|
+
*
|
|
13
|
+
* ## 7.1 — one evidence package per promotion, in the fuller form
|
|
14
|
+
*
|
|
15
|
+
* > *The master adopted a 9-field package; the skipped parent's has 14, and the
|
|
16
|
+
* > five extra are exactly the fields that make 3.2, 4.4 and 7.3 auditable:
|
|
17
|
+
* > pathology cell, candidate lineage, mutation dimension, selection results,
|
|
18
|
+
* > sealed result, cost, scope.*
|
|
19
|
+
* > verify: **a promotion attempt with any field absent is refused.**
|
|
20
|
+
*
|
|
21
|
+
* **The step's own arithmetic does not close, and it is implemented rather than
|
|
22
|
+
* silently reconciled.** It says "the five extra" and then names SEVEN fields.
|
|
23
|
+
* Both readings were available — implement five of the seven, or implement all
|
|
24
|
+
* seven — and the conservative one is taken: all seven are REQUIRED, because
|
|
25
|
+
* dropping two would be a narrowing of a transferred step, and narrowing a
|
|
26
|
+
* transferred step is exactly what the split's own preamble forbids. The count
|
|
27
|
+
* discrepancy is recorded here rather than resolved, since resolving it means
|
|
28
|
+
* editing the verbatim text.
|
|
29
|
+
*
|
|
30
|
+
* {@link PROMOTION_EVIDENCE_FIELDS} therefore carries the seven named fields,
|
|
31
|
+
* the candidate id they hang off, and three more that belong to sibling steps in
|
|
32
|
+
* the same package: `governance` (7.2), `rollout` (7.5) and
|
|
33
|
+
* `material_improvement` (7.4). One package, one validator, one refusal path —
|
|
34
|
+
* splitting them into four documents would be the second governance system 7.2
|
|
35
|
+
* forbids, in a different costume.
|
|
36
|
+
*
|
|
37
|
+
* ## 7.2 — route through the existing gate, not a second governance system
|
|
38
|
+
*
|
|
39
|
+
* > verify: **no new governance verb, no new approval path.**
|
|
40
|
+
*
|
|
41
|
+
* The four vocabulary terms are IMPORTED, not redeclared: `AUTHORITY_BASES`,
|
|
42
|
+
* `EVIDENCE_STRENGTHS`, `REOPEN_POLICIES` and `PROTECTED_DIMENSIONS` all come
|
|
43
|
+
* from `_lib/adr_frontmatter.ts`, which is what `check_adr_frontmatter.ts` and
|
|
44
|
+
* `adr_cite_check.ts` already read. A copy would have satisfied the letter and
|
|
45
|
+
* broken the point: two lists that can drift ARE two governance systems.
|
|
46
|
+
*
|
|
47
|
+
* ## 7.3 — promote by scope, with a transfer gate
|
|
48
|
+
*
|
|
49
|
+
* {@link SCOPE_LADDER} is ordered, and the order is its contract. A raise
|
|
50
|
+
* demands independent transfer evidence from a second solver OR a second host
|
|
51
|
+
* configuration; evidence that all comes from one of each is refused, which is
|
|
52
|
+
* the step's second verify clause verbatim.
|
|
53
|
+
*
|
|
54
|
+
* ## 7.5 — roll out by canary, never silently
|
|
55
|
+
*
|
|
56
|
+
* A package that declares it changes a shipped default and cannot name a
|
|
57
|
+
* COMPLETED opt-in bundle is refused. What this can and cannot establish is
|
|
58
|
+
* stated at {@link assertRollout}.
|
|
59
|
+
*/
|
|
60
|
+
import {
|
|
61
|
+
AUTHORITY_BASES,
|
|
62
|
+
EVIDENCE_STRENGTHS,
|
|
63
|
+
PROTECTED_DIMENSIONS,
|
|
64
|
+
REOPEN_POLICIES,
|
|
65
|
+
} from './adr_frontmatter.js';
|
|
66
|
+
import { MUTATION_DIMENSIONS, type MutationDimension, isMutationDimension } from './candidate_record.js';
|
|
67
|
+
|
|
68
|
+
/** Raised on the first missing or malformed field. Never a list — the first one is the finding. */
|
|
69
|
+
export class PromotionEvidenceError extends Error {
|
|
70
|
+
/** The field that failed, so a caller can name it without parsing the message. */
|
|
71
|
+
readonly field: string;
|
|
72
|
+
constructor(field: string, why: string) {
|
|
73
|
+
super(`promotion evidence: '${field}' ${why}`);
|
|
74
|
+
this.name = 'PromotionEvidenceError';
|
|
75
|
+
this.field = field;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- 7.3 — the scope ladder -------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The ladder, in ascending order. The ORDER is the contract: it is the only
|
|
83
|
+
* thing that makes "moving up a level" decidable.
|
|
84
|
+
*/
|
|
85
|
+
export const SCOPE_LADDER = ['episode', 'repo', 'stack', 'profile-pack', 'global'] as const;
|
|
86
|
+
export type ScopeLevel = (typeof SCOPE_LADDER)[number];
|
|
87
|
+
|
|
88
|
+
export function isScopeLevel(v: unknown): v is ScopeLevel {
|
|
89
|
+
return typeof v === 'string' && (SCOPE_LADDER as readonly string[]).includes(v);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function scopeIndex(s: ScopeLevel): number {
|
|
93
|
+
return SCOPE_LADDER.indexOf(s);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One independent replication of the candidate's effect. */
|
|
97
|
+
export interface TransferEvidence {
|
|
98
|
+
/** The host configuration it was reproduced under. */
|
|
99
|
+
readonly configuration: string;
|
|
100
|
+
/** The solver that reproduced it. */
|
|
101
|
+
readonly solver: string;
|
|
102
|
+
/** What was observed. Free text, but non-empty — an unstated result is not evidence. */
|
|
103
|
+
readonly result: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface PromotionScope {
|
|
107
|
+
readonly level: ScopeLevel;
|
|
108
|
+
/** Absent means "not a raise" — the candidate is promoted at the level it was evaluated. */
|
|
109
|
+
readonly raisedFrom?: ScopeLevel;
|
|
110
|
+
readonly transferEvidence: readonly TransferEvidence[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The transfer gate.
|
|
115
|
+
*
|
|
116
|
+
* A raise needs evidence that is INDEPENDENT of the configuration the candidate
|
|
117
|
+
* was evaluated under — a second solver or a second host configuration. Evidence
|
|
118
|
+
* that all shares one solver and one configuration is one observation written
|
|
119
|
+
* several times, and admitting it is how "every promotion goes straight to
|
|
120
|
+
* canonical and the anti-bloat doctrine has no teeth".
|
|
121
|
+
*
|
|
122
|
+
* @throws {PromotionEvidenceError} on a raise without independent evidence.
|
|
123
|
+
*/
|
|
124
|
+
export function assertTransferEvidence(scope: PromotionScope): void {
|
|
125
|
+
if (scope.raisedFrom === undefined) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (scopeIndex(scope.level) <= scopeIndex(scope.raisedFrom)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const solvers = new Set(scope.transferEvidence.map((e) => e.solver));
|
|
132
|
+
const configurations = new Set(scope.transferEvidence.map((e) => e.configuration));
|
|
133
|
+
if (solvers.size < 2 && configurations.size < 2) {
|
|
134
|
+
throw new PromotionEvidenceError(
|
|
135
|
+
'scope',
|
|
136
|
+
`raises ${scope.raisedFrom} -> ${scope.level} on ${String(scope.transferEvidence.length)} ` +
|
|
137
|
+
`observation(s) from ${String(solvers.size)} solver(s) and ${String(configurations.size)} ` +
|
|
138
|
+
'configuration(s). A raise requires a SECOND solver or a SECOND host configuration; ' +
|
|
139
|
+
'one configuration measured repeatedly is one observation, not transfer.',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- 7.5 — the canary rollout -----------------------------------------------
|
|
145
|
+
|
|
146
|
+
/** Ordered: a promotion enters at `opt-in` and reaches `default` only through it. */
|
|
147
|
+
export const ROLLOUT_STAGES = ['opt-in', 'canary', 'default'] as const;
|
|
148
|
+
export type RolloutStage = (typeof ROLLOUT_STAGES)[number];
|
|
149
|
+
|
|
150
|
+
export interface PromotionRollout {
|
|
151
|
+
readonly stage: RolloutStage;
|
|
152
|
+
/** The opt-in candidate bundle this promotion shipped in. */
|
|
153
|
+
readonly bundle: string;
|
|
154
|
+
readonly optInCompleted: boolean;
|
|
155
|
+
/** Does landing this change what a consumer gets without opting in? */
|
|
156
|
+
readonly changesShippedDefault: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The stage gate.
|
|
161
|
+
*
|
|
162
|
+
* > verify: **no promotion changes a shipped default without an opt-in stage.**
|
|
163
|
+
*
|
|
164
|
+
* **What this establishes and what it does not.** It refuses a PACKAGE that
|
|
165
|
+
* declares a shipped-default change with no completed opt-in bundle, and it
|
|
166
|
+
* refuses a package that claims the `default` stage without one. It cannot
|
|
167
|
+
* establish that a package which declares `changesShippedDefault: false` is
|
|
168
|
+
* telling the truth — that is only observable once a promotion path can actually
|
|
169
|
+
* run and the resulting diff can be compared against the shipped defaults, which
|
|
170
|
+
* `blocker: merge-authority` prevents. The mechanism half is built and tested;
|
|
171
|
+
* the observation half is named as absent rather than implied.
|
|
172
|
+
*
|
|
173
|
+
* @throws {PromotionEvidenceError}
|
|
174
|
+
*/
|
|
175
|
+
export function assertRollout(rollout: PromotionRollout): void {
|
|
176
|
+
if (rollout.changesShippedDefault && !rollout.optInCompleted) {
|
|
177
|
+
throw new PromotionEvidenceError(
|
|
178
|
+
'rollout',
|
|
179
|
+
'changes a shipped default with no COMPLETED opt-in stage. Roll out by canary: ship the ' +
|
|
180
|
+
'candidate in an opt-in bundle, complete that stage, and only then change what a ' +
|
|
181
|
+
'consumer gets without asking for it.',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (rollout.stage === 'default' && !rollout.optInCompleted) {
|
|
185
|
+
throw new PromotionEvidenceError(
|
|
186
|
+
'rollout',
|
|
187
|
+
"claims the 'default' stage without a completed opt-in stage. The ladder is " +
|
|
188
|
+
`${ROLLOUT_STAGES.join(' -> ')} and it is not skippable.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (rollout.optInCompleted && rollout.bundle.trim() === '') {
|
|
192
|
+
throw new PromotionEvidenceError(
|
|
193
|
+
'rollout',
|
|
194
|
+
'reports a completed opt-in stage but names no bundle. An unnamed bundle cannot be audited, ' +
|
|
195
|
+
'and an unauditable opt-in is the silent rollout this step exists to prevent.',
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- 7.2 — the governance block, in the existing vocabulary -----------------
|
|
201
|
+
|
|
202
|
+
export interface PromotionGovernance {
|
|
203
|
+
readonly authorityBasis: string;
|
|
204
|
+
readonly evidenceStrength: string;
|
|
205
|
+
readonly reopenPolicy: string;
|
|
206
|
+
readonly protectedDimensions: readonly string[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- 7.1 — the package ------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Every field a promotion attempt must carry. Order is the refusal order, so a
|
|
213
|
+
* caller fixing one at a time walks the list top to bottom.
|
|
214
|
+
*/
|
|
215
|
+
export const PROMOTION_EVIDENCE_FIELDS = [
|
|
216
|
+
'candidate_id',
|
|
217
|
+
'pathology_cell',
|
|
218
|
+
'lineage',
|
|
219
|
+
'dimension',
|
|
220
|
+
'selection',
|
|
221
|
+
'sealed_result',
|
|
222
|
+
'cost',
|
|
223
|
+
'scope',
|
|
224
|
+
'governance',
|
|
225
|
+
'rollout',
|
|
226
|
+
'material_improvement',
|
|
227
|
+
] as const;
|
|
228
|
+
export type PromotionEvidenceField = (typeof PROMOTION_EVIDENCE_FIELDS)[number];
|
|
229
|
+
|
|
230
|
+
export interface PromotionEvidence {
|
|
231
|
+
readonly candidateId: string;
|
|
232
|
+
/** The pathology × subject cell the candidate targets (3.2 attribution). */
|
|
233
|
+
readonly pathologyCell: string;
|
|
234
|
+
/** Ancestor candidate ids, oldest first. May be empty; the KEY is required. */
|
|
235
|
+
readonly lineage: readonly string[];
|
|
236
|
+
readonly dimension: MutationDimension;
|
|
237
|
+
readonly selection: { readonly trials: number; readonly wins: number; readonly summary: string };
|
|
238
|
+
readonly sealedResult: { readonly held: boolean; readonly summary: string };
|
|
239
|
+
readonly cost: { readonly trials: number; readonly spendCents: number };
|
|
240
|
+
readonly scope: PromotionScope;
|
|
241
|
+
readonly governance: PromotionGovernance;
|
|
242
|
+
readonly rollout: PromotionRollout;
|
|
243
|
+
/** 7.4's evidence: what it replaces, what it says, and the measured delta. */
|
|
244
|
+
readonly materialImprovement: {
|
|
245
|
+
readonly baselineText: string;
|
|
246
|
+
readonly candidateText: string;
|
|
247
|
+
readonly deltaPercent: number;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function obj(input: unknown, field: string): Record<string, unknown> {
|
|
252
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
253
|
+
throw new PromotionEvidenceError(field, 'must be a JSON object');
|
|
254
|
+
}
|
|
255
|
+
return input as Record<string, unknown>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function required(o: Record<string, unknown>, key: PromotionEvidenceField): unknown {
|
|
259
|
+
// ABSENT, never defaulted. The verify clause is "a promotion attempt with any
|
|
260
|
+
// field absent is refused", and a default is a value the author did not choose.
|
|
261
|
+
if (!(key in o)) {
|
|
262
|
+
throw new PromotionEvidenceError(key, 'is required and is never defaulted — an absent field is not an empty one');
|
|
263
|
+
}
|
|
264
|
+
return o[key];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function nonEmptyString(v: unknown, field: string): string {
|
|
268
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
269
|
+
throw new PromotionEvidenceError(field, 'must be a non-empty string');
|
|
270
|
+
}
|
|
271
|
+
return v;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function integer(v: unknown, field: string): number {
|
|
275
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) {
|
|
276
|
+
throw new PromotionEvidenceError(field, 'must be a non-negative integer');
|
|
277
|
+
}
|
|
278
|
+
return v;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function oneOf(v: unknown, allowed: readonly string[], field: string): string {
|
|
282
|
+
if (typeof v !== 'string' || !allowed.includes(v)) {
|
|
283
|
+
throw new PromotionEvidenceError(field, `must be one of ${allowed.join(', ')} (got ${JSON.stringify(v)})`);
|
|
284
|
+
}
|
|
285
|
+
return v;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validate an untrusted evidence document — the refusing path.
|
|
290
|
+
*
|
|
291
|
+
* @throws {PromotionEvidenceError} on the first violation, naming the field.
|
|
292
|
+
*/
|
|
293
|
+
export function parsePromotionEvidence(input: unknown): PromotionEvidence {
|
|
294
|
+
const o = obj(input, 'package');
|
|
295
|
+
|
|
296
|
+
const candidateId = nonEmptyString(required(o, 'candidate_id'), 'candidate_id');
|
|
297
|
+
const pathologyCell = nonEmptyString(required(o, 'pathology_cell'), 'pathology_cell');
|
|
298
|
+
|
|
299
|
+
const rawLineage = required(o, 'lineage');
|
|
300
|
+
if (!Array.isArray(rawLineage)) {
|
|
301
|
+
throw new PromotionEvidenceError('lineage', 'must be an array of ancestor candidate ids (use [] for none)');
|
|
302
|
+
}
|
|
303
|
+
const lineage = rawLineage.map((x, i) => nonEmptyString(x, `lineage[${String(i)}]`));
|
|
304
|
+
|
|
305
|
+
const dimension = required(o, 'dimension');
|
|
306
|
+
if (!isMutationDimension(dimension)) {
|
|
307
|
+
throw new PromotionEvidenceError(
|
|
308
|
+
'dimension',
|
|
309
|
+
`must be one of ${MUTATION_DIMENSIONS.join(', ')} — one PRIMARY dimension per candidate (step 3.2)`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const sel = obj(required(o, 'selection'), 'selection');
|
|
314
|
+
const selection = {
|
|
315
|
+
trials: integer(sel['trials'], 'selection.trials'),
|
|
316
|
+
wins: integer(sel['wins'], 'selection.wins'),
|
|
317
|
+
summary: nonEmptyString(sel['summary'], 'selection.summary'),
|
|
318
|
+
};
|
|
319
|
+
if (selection.wins > selection.trials) {
|
|
320
|
+
throw new PromotionEvidenceError('selection', 'reports more wins than trials');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const sealed = obj(required(o, 'sealed_result'), 'sealed_result');
|
|
324
|
+
if (typeof sealed['held'] !== 'boolean') {
|
|
325
|
+
throw new PromotionEvidenceError('sealed_result.held', 'must be a boolean — did the sealed evaluation hold?');
|
|
326
|
+
}
|
|
327
|
+
const sealedResult = { held: sealed['held'], summary: nonEmptyString(sealed['summary'], 'sealed_result.summary') };
|
|
328
|
+
|
|
329
|
+
const rawCost = obj(required(o, 'cost'), 'cost');
|
|
330
|
+
const cost = {
|
|
331
|
+
trials: integer(rawCost['trials'], 'cost.trials'),
|
|
332
|
+
spendCents: integer(rawCost['spend_cents'], 'cost.spend_cents'),
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const rawScope = obj(required(o, 'scope'), 'scope');
|
|
336
|
+
const level = rawScope['level'];
|
|
337
|
+
if (!isScopeLevel(level)) {
|
|
338
|
+
throw new PromotionEvidenceError('scope.level', `must be one of ${SCOPE_LADDER.join(' -> ')}`);
|
|
339
|
+
}
|
|
340
|
+
const rawRaised = rawScope['raised_from'];
|
|
341
|
+
if (rawRaised !== undefined && !isScopeLevel(rawRaised)) {
|
|
342
|
+
throw new PromotionEvidenceError('scope.raised_from', `must be one of ${SCOPE_LADDER.join(' -> ')} when present`);
|
|
343
|
+
}
|
|
344
|
+
const rawTransfer = rawScope['transfer_evidence'];
|
|
345
|
+
if (!Array.isArray(rawTransfer)) {
|
|
346
|
+
throw new PromotionEvidenceError('scope.transfer_evidence', 'must be an array (use [] when this is not a raise)');
|
|
347
|
+
}
|
|
348
|
+
const transferEvidence: TransferEvidence[] = rawTransfer.map((raw, i) => {
|
|
349
|
+
const e = obj(raw, `scope.transfer_evidence[${String(i)}]`);
|
|
350
|
+
return {
|
|
351
|
+
configuration: nonEmptyString(e['configuration'], `scope.transfer_evidence[${String(i)}].configuration`),
|
|
352
|
+
solver: nonEmptyString(e['solver'], `scope.transfer_evidence[${String(i)}].solver`),
|
|
353
|
+
result: nonEmptyString(e['result'], `scope.transfer_evidence[${String(i)}].result`),
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
const scope: PromotionScope =
|
|
357
|
+
rawRaised === undefined
|
|
358
|
+
? { level, transferEvidence }
|
|
359
|
+
: { level, raisedFrom: rawRaised, transferEvidence };
|
|
360
|
+
assertTransferEvidence(scope);
|
|
361
|
+
|
|
362
|
+
const gov = obj(required(o, 'governance'), 'governance');
|
|
363
|
+
const rawDims = gov['protected_dimensions'];
|
|
364
|
+
if (!Array.isArray(rawDims)) {
|
|
365
|
+
throw new PromotionEvidenceError('governance.protected_dimensions', "must be an array (use ['none'])");
|
|
366
|
+
}
|
|
367
|
+
const governance: PromotionGovernance = {
|
|
368
|
+
authorityBasis: oneOf(gov['authority_basis'], AUTHORITY_BASES, 'governance.authority_basis'),
|
|
369
|
+
evidenceStrength: oneOf(gov['evidence_strength'], EVIDENCE_STRENGTHS, 'governance.evidence_strength'),
|
|
370
|
+
reopenPolicy: oneOf(gov['reopen_policy'], REOPEN_POLICIES, 'governance.reopen_policy'),
|
|
371
|
+
protectedDimensions: rawDims.map((d, i) =>
|
|
372
|
+
oneOf(d, PROTECTED_DIMENSIONS, `governance.protected_dimensions[${String(i)}]`),
|
|
373
|
+
),
|
|
374
|
+
};
|
|
375
|
+
if (governance.protectedDimensions.length === 0) {
|
|
376
|
+
throw new PromotionEvidenceError('governance.protected_dimensions', "must name at least one dimension (use ['none'])");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const rol = obj(required(o, 'rollout'), 'rollout');
|
|
380
|
+
if (typeof rol['opt_in_completed'] !== 'boolean' || typeof rol['changes_shipped_default'] !== 'boolean') {
|
|
381
|
+
throw new PromotionEvidenceError('rollout', "needs boolean 'opt_in_completed' and 'changes_shipped_default'");
|
|
382
|
+
}
|
|
383
|
+
const rollout: PromotionRollout = {
|
|
384
|
+
stage: oneOf(rol['stage'], ROLLOUT_STAGES, 'rollout.stage') as RolloutStage,
|
|
385
|
+
bundle: typeof rol['bundle'] === 'string' ? rol['bundle'] : '',
|
|
386
|
+
optInCompleted: rol['opt_in_completed'],
|
|
387
|
+
changesShippedDefault: rol['changes_shipped_default'],
|
|
388
|
+
};
|
|
389
|
+
assertRollout(rollout);
|
|
390
|
+
|
|
391
|
+
const mi = obj(required(o, 'material_improvement'), 'material_improvement');
|
|
392
|
+
const materialImprovement = {
|
|
393
|
+
baselineText: nonEmptyString(mi['baseline_text'], 'material_improvement.baseline_text'),
|
|
394
|
+
candidateText: nonEmptyString(mi['candidate_text'], 'material_improvement.candidate_text'),
|
|
395
|
+
deltaPercent: typeof mi['delta_percent'] === 'number' ? mi['delta_percent'] : Number.NaN,
|
|
396
|
+
};
|
|
397
|
+
if (!Number.isFinite(materialImprovement.deltaPercent)) {
|
|
398
|
+
throw new PromotionEvidenceError('material_improvement.delta_percent', 'must be a finite number');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
candidateId,
|
|
403
|
+
pathologyCell,
|
|
404
|
+
lineage,
|
|
405
|
+
dimension: dimension as MutationDimension,
|
|
406
|
+
selection,
|
|
407
|
+
sealedResult,
|
|
408
|
+
cost,
|
|
409
|
+
scope,
|
|
410
|
+
governance,
|
|
411
|
+
rollout,
|
|
412
|
+
materialImprovement,
|
|
413
|
+
};
|
|
414
|
+
}
|