@dzhechkov/harness-core 0.8.6 → 0.8.10
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/.dz-manifest.json +257 -53
- package/README.md +48 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +12 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/compounding.d.ts +54 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +221 -1
- package/dist/compounding.js.map +1 -1
- package/dist/eta.d.ts +92 -0
- package/dist/eta.d.ts.map +1 -0
- package/dist/eta.js +488 -0
- package/dist/eta.js.map +1 -0
- package/dist/feature-adr-checkpoints.js +1 -1
- package/dist/feature-adr-decision-recall.d.ts +167 -0
- package/dist/feature-adr-decision-recall.d.ts.map +1 -0
- package/dist/feature-adr-decision-recall.js +519 -0
- package/dist/feature-adr-decision-recall.js.map +1 -0
- package/dist/feature-adr-landing.d.ts +37 -0
- package/dist/feature-adr-landing.d.ts.map +1 -0
- package/dist/feature-adr-landing.js +59 -0
- package/dist/feature-adr-landing.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +2 -2
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +7 -11
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard-promotion.d.ts +41 -0
- package/dist/guard-promotion.d.ts.map +1 -1
- package/dist/guard-promotion.js +218 -4
- package/dist/guard-promotion.js.map +1 -1
- package/dist/guard-volume.d.ts +108 -0
- package/dist/guard-volume.d.ts.map +1 -0
- package/dist/guard-volume.js +536 -0
- package/dist/guard-volume.js.map +1 -0
- package/dist/guard.d.ts +9 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +55 -4
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.d.ts +25 -0
- package/dist/integration-apply.d.ts.map +1 -0
- package/dist/integration-apply.js +299 -0
- package/dist/integration-apply.js.map +1 -0
- package/dist/integration-evidence.d.ts +46 -0
- package/dist/integration-evidence.d.ts.map +1 -0
- package/dist/integration-evidence.js +44 -0
- package/dist/integration-evidence.js.map +1 -0
- package/dist/integration-probe-worker.d.ts +22 -0
- package/dist/integration-probe-worker.d.ts.map +1 -0
- package/dist/integration-probe-worker.js +334 -0
- package/dist/integration-probe-worker.js.map +1 -0
- package/dist/integrations-verify.d.ts +60 -0
- package/dist/integrations-verify.d.ts.map +1 -0
- package/dist/integrations-verify.js +194 -0
- package/dist/integrations-verify.js.map +1 -0
- package/dist/lesson-generalization.d.ts +29 -0
- package/dist/lesson-generalization.d.ts.map +1 -0
- package/dist/lesson-generalization.js +84 -0
- package/dist/lesson-generalization.js.map +1 -0
- package/dist/operations.d.ts +27 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +178 -2
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +27 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +211 -45
- package/dist/patterns.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +20 -17
- package/dist/setup.js.map +1 -1
- package/dist/target-integrations.d.ts +65 -0
- package/dist/target-integrations.d.ts.map +1 -0
- package/dist/target-integrations.js +152 -0
- package/dist/target-integrations.js.map +1 -0
- package/dist/vector-tier.d.ts +6 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +32 -7
- package/dist/vector-tier.js.map +1 -1
- package/package.json +9 -8
- package/sbom.json +562 -52
- package/src/amendment-trace.ts +12 -1
- package/src/compounding.ts +300 -1
- package/src/eta.ts +590 -0
- package/src/feature-adr-checkpoints.ts +1 -1
- package/src/feature-adr-decision-recall.ts +652 -0
- package/src/feature-adr-landing.ts +109 -0
- package/src/feature-adr-routing.ts +7 -11
- package/src/guard-promotion.ts +245 -4
- package/src/guard-volume.ts +752 -0
- package/src/guard.ts +70 -4
- package/src/index.ts +63 -2
- package/src/integration-apply.ts +332 -0
- package/src/integration-evidence.ts +89 -0
- package/src/integration-probe-worker.ts +310 -0
- package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
- package/src/integrations-verify.ts +258 -0
- package/src/lesson-generalization.ts +115 -0
- package/src/operations.ts +199 -2
- package/src/patterns.ts +252 -43
- package/src/setup.ts +20 -17
- package/src/target-integrations.ts +225 -0
- package/src/vector-tier.ts +44 -14
package/src/amendment-trace.ts
CHANGED
|
@@ -339,7 +339,18 @@ export const AMENDMENT_VACUITY_NOTE =
|
|
|
339
339
|
* are named later than ideation guesses); the SUBJECT may not.
|
|
340
340
|
*/
|
|
341
341
|
export function amendmentSubject(raw: string): string {
|
|
342
|
-
|
|
342
|
+
// BOTH arrows, for the same reason extractTestIds accepts both: the corpus carries `->` and `→`
|
|
343
|
+
// from two authors, and splitting on only one leaves the whole Confirmation sentence inside the
|
|
344
|
+
// "subject". Two documents that word their Confirmation differently — which they are entitled to,
|
|
345
|
+
// since only the SUBJECT must survive verbatim — then read as a subject change. MEASURED
|
|
346
|
+
// 2026-08-30 on os-matrix-pack-smoke: all 9 amendments resolved to real tests, yet all 9 reported
|
|
347
|
+
// "the plan describes a DIFFERENT change"; the ideation used `->` throughout.
|
|
348
|
+
// The pointer region starts at `Confirmation:` — everything from there on is HOW the amendment is
|
|
349
|
+
// proven (which fixture, which arrow form, which test name), and the rule says only the pointer may
|
|
350
|
+
// differ between the two documents. Splitting at the arrow alone left the Confirmation PROSE inside
|
|
351
|
+
// the subject, so two documents describing the same fixture in different words read as a subject
|
|
352
|
+
// change. Both arrows are still handled, for corpora that omit the `Confirmation:` lead-in.
|
|
353
|
+
const withoutPointer = raw.split(/Confirmation\s*:/i)[0]?.split(/(?:\u2192|->)\s*tests?\s/)[0] ?? '';
|
|
343
354
|
// Strip ONLY the row's furniture: bullet/table marks, the bold id, an optional `(source)` tag and
|
|
344
355
|
// a colon. An earlier version consumed up to 80 characters after the id, which ate the SUBJECT
|
|
345
356
|
// itself whenever a row carried no `(source):` tag — the checker then compared two truncations
|
package/src/compounding.ts
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { EVENT_CHAIN_SCOPE, verifyEventChainText } from './event-chain.js';
|
|
19
|
+
import {
|
|
20
|
+
isOffsetIsoTimestamp,
|
|
21
|
+
type PromotionAcceptanceEvidence,
|
|
22
|
+
type PromotionRunEvidence,
|
|
23
|
+
} from './guard-promotion.js';
|
|
19
24
|
|
|
20
25
|
// ── Seeded statistics (verbatim-shape port from darwin-mode bench/stats.ts) ──
|
|
21
26
|
|
|
@@ -151,8 +156,52 @@ export function replayableInstances(
|
|
|
151
156
|
|
|
152
157
|
export interface GuardEvent {
|
|
153
158
|
readonly ts: string;
|
|
159
|
+
readonly op?: 'publish' | 'teach' | 'consolidate' | 'reindex';
|
|
154
160
|
readonly verdict: string;
|
|
155
161
|
readonly rules: readonly string[]; // violated rule ids
|
|
162
|
+
readonly violations?: readonly { readonly rule: string; readonly contentAnchor?: string }[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type FunnelEvidenceSource<T> =
|
|
166
|
+
| { readonly status: 'measured'; readonly rows: readonly T[] }
|
|
167
|
+
| { readonly status: 'not-measured'; readonly reason: string };
|
|
168
|
+
|
|
169
|
+
export interface LessonToRuleFunnelFacts {
|
|
170
|
+
readonly promotionRuns: FunnelEvidenceSource<PromotionRunEvidence>;
|
|
171
|
+
readonly guardAudits: FunnelEvidenceSource<GuardEvent>;
|
|
172
|
+
readonly promotionAcceptances?: readonly PromotionAcceptanceEvidence[];
|
|
173
|
+
readonly truncatedPromotionPeriods?: readonly string[];
|
|
174
|
+
readonly acceptanceHistoryComplete?: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type LessonToRuleStage = 'eligible' | 'attempted' | 'accepted' | 'executions';
|
|
178
|
+
export type FunnelStageMeasurement =
|
|
179
|
+
| { readonly status: 'measured'; readonly value: number }
|
|
180
|
+
| { readonly status: 'not-measured'; readonly reason: string };
|
|
181
|
+
|
|
182
|
+
export interface LessonToRuleFunnelPeriod {
|
|
183
|
+
readonly period: string;
|
|
184
|
+
readonly eligible: FunnelStageMeasurement;
|
|
185
|
+
readonly attempted: FunnelStageMeasurement;
|
|
186
|
+
readonly accepted: FunnelStageMeasurement;
|
|
187
|
+
readonly executions: FunnelStageMeasurement;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface LessonToRuleFunnelFinding {
|
|
191
|
+
readonly predecessor: LessonToRuleStage;
|
|
192
|
+
readonly stage: LessonToRuleStage;
|
|
193
|
+
readonly fromPeriod: string;
|
|
194
|
+
readonly toPeriod: string;
|
|
195
|
+
readonly counts: readonly {
|
|
196
|
+
readonly period: string;
|
|
197
|
+
readonly predecessor: number;
|
|
198
|
+
readonly successor: number;
|
|
199
|
+
}[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface LessonToRuleFunnelReport {
|
|
203
|
+
readonly periods: readonly LessonToRuleFunnelPeriod[];
|
|
204
|
+
readonly findings: readonly LessonToRuleFunnelFinding[];
|
|
156
205
|
}
|
|
157
206
|
|
|
158
207
|
/**
|
|
@@ -175,6 +224,8 @@ export interface CompoundingFacts {
|
|
|
175
224
|
readonly evidenceLogs?: readonly EvidenceLogFact[];
|
|
176
225
|
/** Depth of the command-invocation corpus. `null` means no readable log, never zero-by-default. */
|
|
177
226
|
readonly cmdUsageDepthDays?: number | null;
|
|
227
|
+
/** Prospective route observations; absence is explicit NOT MEASURED, never an empty funnel. */
|
|
228
|
+
readonly lessonToRule?: LessonToRuleFunnelFacts;
|
|
178
229
|
}
|
|
179
230
|
|
|
180
231
|
// ── The report ──────────────────────────────────────────────────────
|
|
@@ -244,11 +295,202 @@ export interface CompoundingReport {
|
|
|
244
295
|
readonly guardTrajectory: readonly GuardRuleTrajectory[];
|
|
245
296
|
readonly replay: ReplayReadiness;
|
|
246
297
|
readonly instrumentation: InstrumentationHealth;
|
|
298
|
+
readonly lessonToRuleFunnel: LessonToRuleFunnelReport;
|
|
247
299
|
/** The one-line honest answer. */
|
|
248
300
|
readonly verdict: string;
|
|
249
301
|
}
|
|
250
302
|
|
|
251
303
|
const APPLY_LEG_STALE_DAYS = 7;
|
|
304
|
+
const FUNNEL_MONTH_LIMIT = 12;
|
|
305
|
+
|
|
306
|
+
const LESSON_TO_RULE_FUNNEL_POLICY = {
|
|
307
|
+
unavailable: (reason: string): FunnelStageMeasurement => ({ status: 'not-measured', reason }),
|
|
308
|
+
successor: (stage: LessonToRuleStage): LessonToRuleStage => stage,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
function utcMonth(ts: string): string | null {
|
|
312
|
+
return isOffsetIsoTimestamp(ts) ? new Date(Date.parse(ts)).toISOString().slice(0, 7) : null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function monthOffset(period: string, delta: number): string {
|
|
316
|
+
const [year, month] = period.split('-').map(Number);
|
|
317
|
+
return new Date(Date.UTC(year!, month! - 1 + delta, 1)).toISOString().slice(0, 7);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function funnelPeriods(facts: LessonToRuleFunnelFacts, nowTs: string): string[] {
|
|
321
|
+
const evidenceMonths: string[] = [];
|
|
322
|
+
if (facts.promotionRuns.status === 'measured') {
|
|
323
|
+
for (const row of facts.promotionRuns.rows) {
|
|
324
|
+
const period = utcMonth(row.ts);
|
|
325
|
+
if (period !== null) evidenceMonths.push(period);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (facts.guardAudits.status === 'measured') {
|
|
329
|
+
for (const row of facts.guardAudits.rows) {
|
|
330
|
+
const period = utcMonth(row.ts);
|
|
331
|
+
if (period !== null) evidenceMonths.push(period);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const period of facts.truncatedPromotionPeriods ?? []) evidenceMonths.push(period);
|
|
335
|
+
const current = utcMonth(nowTs) ?? evidenceMonths.sort().at(-1) ?? '1970-01';
|
|
336
|
+
const floor = monthOffset(current, -(FUNNEL_MONTH_LIMIT - 1));
|
|
337
|
+
const earliest = evidenceMonths.filter((period) => period >= floor && period <= current).sort()[0] ?? current;
|
|
338
|
+
const periods: string[] = [];
|
|
339
|
+
for (let period = earliest; period <= current; period = monthOffset(period, 1)) periods.push(period);
|
|
340
|
+
return periods;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function promotionMeasurements(
|
|
344
|
+
facts: LessonToRuleFunnelFacts,
|
|
345
|
+
period: string,
|
|
346
|
+
): Pick<LessonToRuleFunnelPeriod, 'eligible' | 'attempted' | 'accepted'> {
|
|
347
|
+
if (facts.promotionRuns.status === 'not-measured') {
|
|
348
|
+
const unavailable = LESSON_TO_RULE_FUNNEL_POLICY.unavailable(facts.promotionRuns.reason);
|
|
349
|
+
return { eligible: unavailable, attempted: unavailable, accepted: unavailable };
|
|
350
|
+
}
|
|
351
|
+
if (facts.truncatedPromotionPeriods?.includes(period) === true) {
|
|
352
|
+
const unavailable = LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`promotion-history-pruned:${period}`);
|
|
353
|
+
return { eligible: unavailable, attempted: unavailable, accepted: unavailable };
|
|
354
|
+
}
|
|
355
|
+
const runs = facts.promotionRuns.rows.filter((row) => utcMonth(row.ts) === period);
|
|
356
|
+
if (runs.length === 0) {
|
|
357
|
+
const unavailable = LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`promotion-run-not-recorded:${period}`);
|
|
358
|
+
return { eligible: unavailable, attempted: unavailable, accepted: unavailable };
|
|
359
|
+
}
|
|
360
|
+
if (runs.some((row) => row.complete !== true)) {
|
|
361
|
+
const unavailable = LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`promotion-run-incomplete:${period}`);
|
|
362
|
+
return { eligible: unavailable, attempted: unavailable, accepted: unavailable };
|
|
363
|
+
}
|
|
364
|
+
const eligible = new Set<string>();
|
|
365
|
+
const attempted = new Set<string>();
|
|
366
|
+
const accepted = new Set<string>();
|
|
367
|
+
for (const run of runs) {
|
|
368
|
+
for (const candidate of run.candidates) {
|
|
369
|
+
if (candidate.eligible !== true) continue;
|
|
370
|
+
eligible.add(candidate.candidateAnchor);
|
|
371
|
+
if (typeof candidate.ruleContentAnchor !== 'string' || candidate.ruleContentAnchor === '') continue;
|
|
372
|
+
attempted.add(candidate.candidateAnchor);
|
|
373
|
+
if (candidate.verdict === 'promote') accepted.add(candidate.candidateAnchor);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
eligible: { status: 'measured', value: eligible.size },
|
|
378
|
+
attempted: { status: 'measured', value: attempted.size },
|
|
379
|
+
accepted: { status: 'measured', value: accepted.size },
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function executionMeasurement(
|
|
384
|
+
facts: LessonToRuleFunnelFacts,
|
|
385
|
+
period: string,
|
|
386
|
+
): FunnelStageMeasurement {
|
|
387
|
+
if (facts.promotionRuns.status === 'not-measured') {
|
|
388
|
+
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(facts.promotionRuns.reason);
|
|
389
|
+
}
|
|
390
|
+
if (facts.guardAudits.status === 'not-measured') {
|
|
391
|
+
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(facts.guardAudits.reason);
|
|
392
|
+
}
|
|
393
|
+
const periodAudits = facts.guardAudits.rows.filter((row) => utcMonth(row.ts) === period);
|
|
394
|
+
if (periodAudits.length === 0) {
|
|
395
|
+
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`guard-audit-not-recorded:${period}`);
|
|
396
|
+
}
|
|
397
|
+
const audits = periodAudits.filter((row) => row.op === 'publish');
|
|
398
|
+
if (audits.length === 0) {
|
|
399
|
+
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`guard-publish-not-recorded:${period}`);
|
|
400
|
+
}
|
|
401
|
+
const violations = audits.flatMap((audit) => {
|
|
402
|
+
const observed: readonly { readonly rule: string; readonly contentAnchor?: string }[] =
|
|
403
|
+
audit.violations ?? audit.rules.map((rule) => ({ rule }));
|
|
404
|
+
return observed.map((violation) => ({ ...violation, auditTs: Date.parse(audit.ts) }));
|
|
405
|
+
});
|
|
406
|
+
if (violations.some((item) => item.rule.startsWith('promoted-') && !item.contentAnchor)) {
|
|
407
|
+
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`guard-audit-anchor-missing:${period}`);
|
|
408
|
+
}
|
|
409
|
+
const acceptances = facts.promotionAcceptances ?? facts.promotionRuns.rows.flatMap((run) =>
|
|
410
|
+
run.complete !== true
|
|
411
|
+
? []
|
|
412
|
+
: run.candidates
|
|
413
|
+
.filter((candidate) => candidate.eligible === true && candidate.verdict === 'promote' && candidate.ruleContentAnchor)
|
|
414
|
+
.map((candidate) => ({ ruleContentAnchor: candidate.ruleContentAnchor!, acceptedTs: run.ts })),
|
|
415
|
+
);
|
|
416
|
+
const acceptedAt = new Map<string, number>();
|
|
417
|
+
for (const acceptance of acceptances) {
|
|
418
|
+
const acceptedTs = Date.parse(acceptance.acceptedTs);
|
|
419
|
+
const prior = acceptedAt.get(acceptance.ruleContentAnchor);
|
|
420
|
+
if (Number.isFinite(acceptedTs) && (prior === undefined || acceptedTs < prior)) {
|
|
421
|
+
acceptedAt.set(acceptance.ruleContentAnchor, acceptedTs);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const unattributedPromoted = violations.some((item) =>
|
|
425
|
+
item.rule.startsWith('promoted-') &&
|
|
426
|
+
typeof item.contentAnchor === 'string' &&
|
|
427
|
+
(acceptedAt.get(item.contentAnchor) === undefined || acceptedAt.get(item.contentAnchor)! > item.auditTs),
|
|
428
|
+
);
|
|
429
|
+
const acceptanceHistoryComplete = facts.acceptanceHistoryComplete ??
|
|
430
|
+
facts.promotionRuns.rows.every((run) => run.complete === true);
|
|
431
|
+
if (unattributedPromoted && !acceptanceHistoryComplete) {
|
|
432
|
+
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`promotion-acceptance-history-incomplete:${period}`);
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
status: 'measured',
|
|
436
|
+
value: violations.filter((item) =>
|
|
437
|
+
typeof item.contentAnchor === 'string' &&
|
|
438
|
+
acceptedAt.has(item.contentAnchor) &&
|
|
439
|
+
acceptedAt.get(item.contentAnchor)! <= item.auditTs,
|
|
440
|
+
).length,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function funnelFindings(periods: readonly LessonToRuleFunnelPeriod[]): LessonToRuleFunnelFinding[] {
|
|
445
|
+
const edges: readonly [LessonToRuleStage, LessonToRuleStage][] = [
|
|
446
|
+
['eligible', 'attempted'],
|
|
447
|
+
['attempted', 'accepted'],
|
|
448
|
+
['accepted', 'executions'],
|
|
449
|
+
];
|
|
450
|
+
const findings: LessonToRuleFunnelFinding[] = [];
|
|
451
|
+
for (const [predecessor, successor] of edges) {
|
|
452
|
+
let streak: LessonToRuleFunnelPeriod[] = [];
|
|
453
|
+
const emit = (): void => {
|
|
454
|
+
if (streak.length < 3) return;
|
|
455
|
+
const observed = streak.slice(-3);
|
|
456
|
+
findings.push({
|
|
457
|
+
predecessor,
|
|
458
|
+
stage: LESSON_TO_RULE_FUNNEL_POLICY.successor(successor),
|
|
459
|
+
fromPeriod: observed[0]!.period,
|
|
460
|
+
toPeriod: observed[2]!.period,
|
|
461
|
+
counts: observed.map((row) => ({
|
|
462
|
+
period: row.period,
|
|
463
|
+
predecessor: (row[predecessor] as { status: 'measured'; value: number }).value,
|
|
464
|
+
successor: (row[successor] as { status: 'measured'; value: number }).value,
|
|
465
|
+
})),
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
for (const row of periods) {
|
|
469
|
+
const before = row[predecessor];
|
|
470
|
+
const after = row[successor];
|
|
471
|
+
if (before.status === 'measured' && after.status === 'measured' && before.value > 0 && after.value === 0) {
|
|
472
|
+
streak.push(row);
|
|
473
|
+
} else {
|
|
474
|
+
emit();
|
|
475
|
+
streak = [];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
emit();
|
|
479
|
+
}
|
|
480
|
+
return findings;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function assembleLessonToRuleFunnel(
|
|
484
|
+
facts: LessonToRuleFunnelFacts,
|
|
485
|
+
nowTs: string,
|
|
486
|
+
): LessonToRuleFunnelReport {
|
|
487
|
+
const periods = funnelPeriods(facts, nowTs).map((period): LessonToRuleFunnelPeriod => ({
|
|
488
|
+
period,
|
|
489
|
+
...promotionMeasurements(facts, period),
|
|
490
|
+
executions: executionMeasurement(facts, period),
|
|
491
|
+
}));
|
|
492
|
+
return { periods, findings: funnelFindings(periods) };
|
|
493
|
+
}
|
|
252
494
|
|
|
253
495
|
export function assembleCompoundingReport(facts: CompoundingFacts): CompoundingReport {
|
|
254
496
|
const { lessons, usage, guard } = facts;
|
|
@@ -367,6 +609,14 @@ export function assembleCompoundingReport(facts: CompoundingFacts): CompoundingR
|
|
|
367
609
|
: null,
|
|
368
610
|
};
|
|
369
611
|
|
|
612
|
+
const lessonToRuleFunnel = assembleLessonToRuleFunnel(
|
|
613
|
+
facts.lessonToRule ?? {
|
|
614
|
+
promotionRuns: { status: 'not-measured', reason: 'promotion-journal-not-provided' },
|
|
615
|
+
guardAudits: { status: 'not-measured', reason: 'guard-audit-not-provided' },
|
|
616
|
+
},
|
|
617
|
+
facts.nowTs,
|
|
618
|
+
);
|
|
619
|
+
|
|
370
620
|
const improvedRules = trajectory.filter((t) => t.improved).length;
|
|
371
621
|
const verdict = [
|
|
372
622
|
`pool: ${injectedEver}/${total} lessons ever injected (${Math.round(pool.writeOnlyRatio * 100)}% write-only under the strict bar)`,
|
|
@@ -382,7 +632,43 @@ export function assembleCompoundingReport(facts: CompoundingFacts): CompoundingR
|
|
|
382
632
|
]),
|
|
383
633
|
].join(' · ');
|
|
384
634
|
|
|
385
|
-
return { pool, guardTrajectory: trajectory, replay, instrumentation, verdict };
|
|
635
|
+
return { pool, guardTrajectory: trajectory, replay, instrumentation, lessonToRuleFunnel, verdict };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function renderFunnelMeasurement(stage: LessonToRuleStage, value: FunnelStageMeasurement): string {
|
|
639
|
+
return value.status === 'measured'
|
|
640
|
+
? `${stage} ${value.value}`
|
|
641
|
+
: `${stage} NOT MEASURED (${value.reason})`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function renderPromotionMeasurements(row: LessonToRuleFunnelPeriod): string {
|
|
645
|
+
const { eligible, attempted, accepted } = row;
|
|
646
|
+
if (
|
|
647
|
+
eligible.status === 'not-measured' &&
|
|
648
|
+
attempted.status === 'not-measured' &&
|
|
649
|
+
accepted.status === 'not-measured' &&
|
|
650
|
+
eligible.reason === attempted.reason &&
|
|
651
|
+
eligible.reason === accepted.reason
|
|
652
|
+
) {
|
|
653
|
+
return `eligible/attempted/accepted NOT MEASURED (${eligible.reason})`;
|
|
654
|
+
}
|
|
655
|
+
return [
|
|
656
|
+
renderFunnelMeasurement('eligible', row.eligible),
|
|
657
|
+
renderFunnelMeasurement('attempted', row.attempted),
|
|
658
|
+
renderFunnelMeasurement('accepted', row.accepted),
|
|
659
|
+
].join(' · ');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function renderFunnelPeriodMeasurements(row: LessonToRuleFunnelPeriod): string {
|
|
663
|
+
const values = [row.eligible, row.attempted, row.accepted, row.executions] as const;
|
|
664
|
+
const unavailable = values.filter((value) => value.status === 'not-measured');
|
|
665
|
+
if (
|
|
666
|
+
unavailable.length === values.length &&
|
|
667
|
+
unavailable.every((value) => value.reason === unavailable[0]!.reason)
|
|
668
|
+
) {
|
|
669
|
+
return `eligible/attempted/accepted/executions NOT MEASURED (${unavailable[0]!.reason})`;
|
|
670
|
+
}
|
|
671
|
+
return `${renderPromotionMeasurements(row)} · ${renderFunnelMeasurement('executions', row.executions)}`;
|
|
386
672
|
}
|
|
387
673
|
|
|
388
674
|
export function renderCompoundingReport(r: CompoundingReport): string {
|
|
@@ -420,6 +706,19 @@ export function renderCompoundingReport(r: CompoundingReport): string {
|
|
|
420
706
|
);
|
|
421
707
|
if (r.instrumentation.chains.length > 0) out.push(` scope: ${EVENT_CHAIN_SCOPE}`);
|
|
422
708
|
out.push('');
|
|
709
|
+
out.push(' LESSON → RULE FUNNEL (calendar month, observed traffic only):');
|
|
710
|
+
for (const row of r.lessonToRuleFunnel.periods) {
|
|
711
|
+
out.push(
|
|
712
|
+
` ${row.period} · ${renderFunnelPeriodMeasurements(row)}`,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
for (const finding of r.lessonToRuleFunnel.findings) {
|
|
716
|
+
const counts = finding.counts
|
|
717
|
+
.map((row) => `${row.period} ${finding.predecessor} ${row.predecessor} → ${finding.stage} ${row.successor}`)
|
|
718
|
+
.join('; ');
|
|
719
|
+
out.push(` FLOW STOP ${finding.stage} (${finding.fromPeriod}..${finding.toPeriod}): ${counts}`);
|
|
720
|
+
}
|
|
721
|
+
out.push('');
|
|
423
722
|
out.push(` VERDICT: ${r.verdict}`);
|
|
424
723
|
return out.join('\n');
|
|
425
724
|
}
|