@event4u/agent-config 13.0.0 → 14.0.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +160 -333
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
- package/dist/agent-src/rules/ui-audit-gate.md +25 -8
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
- package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +40 -18
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +2018 -1320
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1652 -892
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +111 -5
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +20 -0
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +80 -143
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/capture_skill_catalogue.ts +264 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/hook_manifest.yaml +72 -5
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/release.ts +38 -53
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill_catalogue_series — the SERIES layer over the observation log.
|
|
3
|
+
*
|
|
4
|
+
* `skill_catalogue` answers "what did this one observation measure". This
|
|
5
|
+
* module answers the three questions that need MORE than one record: is a
|
|
6
|
+
* host's series stale, how far is the corpus from the bar it is filling
|
|
7
|
+
* toward, and does the host's delivery diverge from the disk the runtime
|
|
8
|
+
* ranker reads.
|
|
9
|
+
*
|
|
10
|
+
* Split out of `skill_catalogue.ts` when that file crossed the 1,500-line
|
|
11
|
+
* source ceiling by four lines. Extraction rather than a raised baseline is
|
|
12
|
+
* the ratchet's own doctrine — and the seam is a real one rather than a
|
|
13
|
+
* convenient cut: nothing here is imported by `install.ts`, so the
|
|
14
|
+
* deploy-time warning path keeps exactly the surface it had.
|
|
15
|
+
*
|
|
16
|
+
* The two laws of the parent module hold here without restatement, and both
|
|
17
|
+
* are load-bearing in this file specifically:
|
|
18
|
+
*
|
|
19
|
+
* A ZERO IS NEVER INFERRED FROM SILENCE. A host that publishes no
|
|
20
|
+
* per-entry list is SKIPPED by the join, never scored 0 — its empty
|
|
21
|
+
* `bare_names` records that nothing was enumerated, not that nothing was
|
|
22
|
+
* bare. An unparseable date reads as DUE, never as fresh.
|
|
23
|
+
*
|
|
24
|
+
* NOTHING IS POOLED ACROSS HOSTS. Cadence is per host and the D-4 headline
|
|
25
|
+
* is per host, for the reason `formatPerHostVerdicts` states: two hosts
|
|
26
|
+
* truncate by different mechanisms, so one number over both describes
|
|
27
|
+
* neither.
|
|
28
|
+
*
|
|
29
|
+
* PRIVACY BY CONSTRUCTION is inherited: every type here holds skill names,
|
|
30
|
+
* integers, host labels and closed enums. None can hold free-form content.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
type HostProjectionRow,
|
|
35
|
+
type ObservationRecord,
|
|
36
|
+
type ObservationSource,
|
|
37
|
+
type ProjectionMode,
|
|
38
|
+
headlineRecordPerHost,
|
|
39
|
+
observationSourceOf,
|
|
40
|
+
truncationModeOf,
|
|
41
|
+
} from './skill_catalogue.js';
|
|
42
|
+
|
|
43
|
+
/* ------------------------------------------------------------------ *
|
|
44
|
+
* The cadence — is an observation due, and how far is the corpus?
|
|
45
|
+
* ------------------------------------------------------------------ */
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Days after which one host's series is stale and an observation is due.
|
|
49
|
+
*
|
|
50
|
+
* A **stated default, not a measured optimum** — said plainly rather than
|
|
51
|
+
* implying a derivation it does not have. Nothing in the corpus measures how
|
|
52
|
+
* fast a host's truncation behaviour changes; what is known is that it changes
|
|
53
|
+
* at all (the codex drop moved 393 → 401 → 402 → 330 across four readings, the
|
|
54
|
+
* last of them because OUR projection scope changed, not the host).
|
|
55
|
+
*
|
|
56
|
+
* *Revisit-if:* two consecutive rounds a week apart record identical figures on
|
|
57
|
+
* every host (too frequent — widen it), or a host limit is observed to move
|
|
58
|
+
* inside one interval (too slow — narrow it). Either falsifies the number, not
|
|
59
|
+
* the obligation to keep the series current.
|
|
60
|
+
*/
|
|
61
|
+
export const OBSERVATION_CADENCE_DAYS = 7;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The volume and host bars the corpus is filling toward.
|
|
65
|
+
*
|
|
66
|
+
* **Quoted, never invented here.** Both come from the parent blocker's own
|
|
67
|
+
* wording — "≥ 20 observations across ≥ 2 hosts" — recorded in
|
|
68
|
+
* `agents/evidence/investigations/skill-catalogue-codex-truncation.md` § 1 and
|
|
69
|
+
* § 5 and in the blocker that still carries it. This module reports progress
|
|
70
|
+
* against them; it does not get to move them.
|
|
71
|
+
*/
|
|
72
|
+
export const OBSERVATION_VOLUME_BAR = 20;
|
|
73
|
+
export const OBSERVATION_HOST_BAR = 2;
|
|
74
|
+
|
|
75
|
+
/** Whole days from `fromISO` to `toISO`; negative when `toISO` is earlier. */
|
|
76
|
+
function wholeDaysBetween(fromISO: string, toISO: string): number {
|
|
77
|
+
const from = Date.parse(`${fromISO}T00:00:00Z`);
|
|
78
|
+
const to = Date.parse(`${toISO}T00:00:00Z`);
|
|
79
|
+
if (Number.isNaN(from) || Number.isNaN(to)) return Number.NaN;
|
|
80
|
+
return Math.round((to - from) / 86_400_000);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One host's standing in the series. Counts only; no free-form field. */
|
|
84
|
+
export interface CadenceRow {
|
|
85
|
+
host: string;
|
|
86
|
+
/** The source the host's own latest record came from. */
|
|
87
|
+
source: ObservationSource;
|
|
88
|
+
observations: number;
|
|
89
|
+
lastObservedAt: string;
|
|
90
|
+
/** Whole days since that record; `NaN` when its stamp is unparseable. */
|
|
91
|
+
daysSince: number;
|
|
92
|
+
due: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Observations of this host carrying NO `projection_mode`. Not a defect to
|
|
95
|
+
* backfill — those readings were taken without asking, and relabelling one
|
|
96
|
+
* afterwards would invent a measurement. They are simply not comparable
|
|
97
|
+
* across modes, so the count is published rather than hidden.
|
|
98
|
+
*/
|
|
99
|
+
unscoped: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Per-host cadence, never pooled — the same reason `formatPerHostVerdicts`
|
|
104
|
+
* refuses to pool: two hosts truncate by different mechanisms, so "the corpus
|
|
105
|
+
* is fresh" is not a statement either host makes on its own.
|
|
106
|
+
*
|
|
107
|
+
* A host with no record at all cannot appear here: this reads the log, and a
|
|
108
|
+
* host that was never observed has nothing to be stale. The volume bar below
|
|
109
|
+
* is what surfaces that gap.
|
|
110
|
+
*/
|
|
111
|
+
export function cadenceStatus(
|
|
112
|
+
records: readonly ObservationRecord[],
|
|
113
|
+
todayISO: string,
|
|
114
|
+
): CadenceRow[] {
|
|
115
|
+
const byHost = new Map<string, ObservationRecord[]>();
|
|
116
|
+
for (const record of records) {
|
|
117
|
+
const bucket = byHost.get(record.host) ?? [];
|
|
118
|
+
bucket.push(record);
|
|
119
|
+
byHost.set(record.host, bucket);
|
|
120
|
+
}
|
|
121
|
+
const headline = headlineRecordPerHost(records);
|
|
122
|
+
|
|
123
|
+
const rows: CadenceRow[] = [];
|
|
124
|
+
for (const host of [...byHost.keys()].sort()) {
|
|
125
|
+
const bucket = byHost.get(host)!;
|
|
126
|
+
const latest = headline.get(host)!;
|
|
127
|
+
const daysSince = wholeDaysBetween(latest.observed_at, todayISO);
|
|
128
|
+
rows.push({
|
|
129
|
+
host,
|
|
130
|
+
source: observationSourceOf(latest),
|
|
131
|
+
observations: bucket.length,
|
|
132
|
+
lastObservedAt: latest.observed_at,
|
|
133
|
+
daysSince,
|
|
134
|
+
// An unparseable stamp reads as DUE, never as fresh: a broken date
|
|
135
|
+
// and a current one must not look alike, which is the same
|
|
136
|
+
// zero-from-silence law the host-event parser follows.
|
|
137
|
+
due: Number.isNaN(daysSince) || daysSince >= OBSERVATION_CADENCE_DAYS,
|
|
138
|
+
unscoped: bucket.filter((r) => r.projection_mode === undefined).length,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return rows;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The cadence as an operator report: who is due, and how far the corpus is from
|
|
146
|
+
* the bar it is filling toward.
|
|
147
|
+
*
|
|
148
|
+
* Prints the exact next command per host rather than describing it. The codex
|
|
149
|
+
* side is deterministic and scriptable; the claude side is self-report and only
|
|
150
|
+
* an agent reading its own context can produce it — the asymmetry is stated
|
|
151
|
+
* here because a report that hid it would read as "both are automatable".
|
|
152
|
+
*/
|
|
153
|
+
export function formatCadenceStatus(
|
|
154
|
+
rows: readonly CadenceRow[],
|
|
155
|
+
todayISO: string,
|
|
156
|
+
): string {
|
|
157
|
+
const lines: string[] = [];
|
|
158
|
+
lines.push(`cadence: one observation per host every ${OBSERVATION_CADENCE_DAYS} day(s) · today ${todayISO}`);
|
|
159
|
+
lines.push('');
|
|
160
|
+
|
|
161
|
+
if (rows.length === 0) {
|
|
162
|
+
lines.push('no observations recorded yet — every host is due.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
const age = Number.isNaN(row.daysSince)
|
|
167
|
+
? `stamp unparseable (${row.lastObservedAt})`
|
|
168
|
+
: `${row.daysSince} day(s) ago`;
|
|
169
|
+
lines.push(
|
|
170
|
+
`${row.due ? '⚠️ ' : '✅'} ${row.host}: ${row.observations} observation(s) · ` +
|
|
171
|
+
`latest ${row.lastObservedAt} (${age}) · source ${row.source}`,
|
|
172
|
+
);
|
|
173
|
+
if (row.unscoped > 0) {
|
|
174
|
+
lines.push(
|
|
175
|
+
` ${row.unscoped} of them carry no projection scope — not comparable across modes, and not backfillable.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const total = rows.reduce((sum, row) => sum + row.observations, 0);
|
|
181
|
+
lines.push('');
|
|
182
|
+
lines.push(
|
|
183
|
+
`volume: ${total}/${OBSERVATION_VOLUME_BAR} observation(s) across ${rows.length}/${OBSERVATION_HOST_BAR} host(s) — ` +
|
|
184
|
+
(total >= OBSERVATION_VOLUME_BAR && rows.length >= OBSERVATION_HOST_BAR
|
|
185
|
+
? 'both bars met.'
|
|
186
|
+
: 'the bar the parent blocker states, quoted not invented.'),
|
|
187
|
+
);
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Whether a round may carry a `--projection-mode`, and why not when it may not. */
|
|
192
|
+
export interface ScopeFlagDecision {
|
|
193
|
+
/** The measured mode, or `null` when none may be claimed. */
|
|
194
|
+
mode: ProjectionMode | null;
|
|
195
|
+
/** One line stating what was measured, or why no mode is claimable. */
|
|
196
|
+
reason: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Decide whether a capture round may name a projection mode.
|
|
201
|
+
*
|
|
202
|
+
* Exported and pure BECAUSE it is the load-bearing correction: the cadence
|
|
203
|
+
* output first printed `--projection-mode <scoped|legacy-all>` as a
|
|
204
|
+
* placeholder, and a placeholder invites the operator to pick — on a root
|
|
205
|
+
* matching neither count, either pick is the relabelling
|
|
206
|
+
* `ObservationRecord.projection_mode` forbids. A decision nothing pins is a
|
|
207
|
+
* decision a later edit regresses silently, so both omit-with-a-reason branches
|
|
208
|
+
* are fixtured.
|
|
209
|
+
*
|
|
210
|
+
* `rootExists: false` and `indeterminate` are deliberately distinct reasons.
|
|
211
|
+
* They produce the same output — no flag — from different facts, and collapsing
|
|
212
|
+
* them would report "this install's scope is unmeasurable" for a host that is
|
|
213
|
+
* simply not installed.
|
|
214
|
+
*/
|
|
215
|
+
export function scopeFlagDecision(
|
|
216
|
+
root: string,
|
|
217
|
+
rootExists: boolean,
|
|
218
|
+
row: HostProjectionRow | null,
|
|
219
|
+
): ScopeFlagDecision {
|
|
220
|
+
if (!rootExists || row === null) {
|
|
221
|
+
return {
|
|
222
|
+
mode: null,
|
|
223
|
+
reason: `${root} is not installed here, so the scope of the observed install cannot be measured from this machine.`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (row.matches === 'indeterminate') {
|
|
227
|
+
return {
|
|
228
|
+
mode: null,
|
|
229
|
+
reason:
|
|
230
|
+
`${root} holds ${row.installedSkills} skills, which matches neither projection count — ` +
|
|
231
|
+
'another suite, a plugin, or a stale install all produce that. Recording a mode here would label a reading nobody took.',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
mode: row.matches,
|
|
236
|
+
reason: `scope measured off ${root} (${row.installedSkills} skills).`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ------------------------------------------------------------------ *
|
|
241
|
+
* D-4 — host truth against disk truth: pointable but bare.
|
|
242
|
+
* ------------------------------------------------------------------ */
|
|
243
|
+
|
|
244
|
+
/** One per-entry observation joined against the ranker's on-disk catalogue. */
|
|
245
|
+
export interface PointableBareRow {
|
|
246
|
+
host: string;
|
|
247
|
+
observedAt: string;
|
|
248
|
+
bareTotal: number;
|
|
249
|
+
/** Bare entries the ranker can still name — the D-4 divergence. */
|
|
250
|
+
pointableBare: number;
|
|
251
|
+
/** Bare entries absent from the ranker's catalogue; it cannot name them. */
|
|
252
|
+
unpointableBare: number;
|
|
253
|
+
pointableNames: string[];
|
|
254
|
+
unpointableNames: string[];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Intersect the bare names of every per-entry observation with the catalogue the
|
|
259
|
+
* runtime ranker reads.
|
|
260
|
+
*
|
|
261
|
+
* The defect this counts (D-4): `skill-route` ranks the on-disk tree, so a skill
|
|
262
|
+
* the host truncated is still rankable and still pointable — and the pointer
|
|
263
|
+
* then names a skill whose description the model never received. That is worse
|
|
264
|
+
* than silence, because the pointer reads as a delivered capability.
|
|
265
|
+
*
|
|
266
|
+
* **Only `per-entry` records are joined, and skipping the others is the point.**
|
|
267
|
+
* A `budget-strip-and-drop` host publishes no per-entry list at all, so its
|
|
268
|
+
* `bare_names` is empty because nothing was enumerated — not because nothing was
|
|
269
|
+
* bare. Emitting a row of 0 for it would be a zero inferred from silence, which
|
|
270
|
+
* this module's header forbids in the one place it would be easiest to do.
|
|
271
|
+
*
|
|
272
|
+
* A row of 0 on a per-entry record IS a legitimate answer: it means every entry
|
|
273
|
+
* the host degraded is also absent from the ranker's tree, so the ranker cannot
|
|
274
|
+
* point at one.
|
|
275
|
+
*/
|
|
276
|
+
/** What one join pass produced, including what it refused to read. */
|
|
277
|
+
export interface PointableBareJoin {
|
|
278
|
+
rows: PointableBareRow[];
|
|
279
|
+
/** Records skipped because their host publishes no per-entry list. */
|
|
280
|
+
skippedNonPerEntry: number;
|
|
281
|
+
/**
|
|
282
|
+
* Per-entry records skipped because `bare_names` was not an array.
|
|
283
|
+
*
|
|
284
|
+
* `readObservationLog` produces records by an unchecked `JSON.parse … as`,
|
|
285
|
+
* and the log is append-only with more than one producer, so a malformed
|
|
286
|
+
* line is a real state. Counting it separately keeps it from disappearing
|
|
287
|
+
* into the same silence as a legitimately empty join — the sibling reducer
|
|
288
|
+
* `knownHostLimits` guards its own field for the same reason.
|
|
289
|
+
*/
|
|
290
|
+
skippedMalformed: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function joinPointableBare(
|
|
294
|
+
records: readonly ObservationRecord[],
|
|
295
|
+
rankerCatalogueNames: readonly string[],
|
|
296
|
+
): PointableBareJoin {
|
|
297
|
+
const rankable = new Set(rankerCatalogueNames);
|
|
298
|
+
const rows: PointableBareRow[] = [];
|
|
299
|
+
let skippedNonPerEntry = 0;
|
|
300
|
+
let skippedMalformed = 0;
|
|
301
|
+
for (const record of records) {
|
|
302
|
+
if (truncationModeOf(record) !== 'per-entry') {
|
|
303
|
+
skippedNonPerEntry += 1;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!Array.isArray(record.bare_names)) {
|
|
307
|
+
skippedMalformed += 1;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const pointable = record.bare_names.filter((name) => rankable.has(name));
|
|
311
|
+
const unpointable = record.bare_names.filter((name) => !rankable.has(name));
|
|
312
|
+
rows.push({
|
|
313
|
+
host: record.host,
|
|
314
|
+
observedAt: record.observed_at,
|
|
315
|
+
bareTotal: record.bare_names.length,
|
|
316
|
+
pointableBare: pointable.length,
|
|
317
|
+
unpointableBare: unpointable.length,
|
|
318
|
+
pointableNames: pointable,
|
|
319
|
+
unpointableNames: unpointable,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return { rows, skippedNonPerEntry, skippedMalformed };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The latest joined row per host, by `observedAt`.
|
|
327
|
+
*
|
|
328
|
+
* The headline is stated PER HOST and off the latest row, never as one maximum
|
|
329
|
+
* across the corpus. A pooled max lets a superseded observation supply the
|
|
330
|
+
* number while the current one reads 0, with no host or date attached to it —
|
|
331
|
+
* which is the exact failure `_supersedes` was introduced for (two reducers
|
|
332
|
+
* broke a same-date tie in opposite directions and printed two drop counts for
|
|
333
|
+
* one host on one day) and which `formatPerHostVerdicts` refuses on the stated
|
|
334
|
+
* grounds that two hosts truncate by different mechanisms.
|
|
335
|
+
*/
|
|
336
|
+
export function latestPointableBarePerHost(
|
|
337
|
+
rows: readonly PointableBareRow[],
|
|
338
|
+
): Map<string, PointableBareRow> {
|
|
339
|
+
const out = new Map<string, PointableBareRow>();
|
|
340
|
+
for (const row of rows) {
|
|
341
|
+
const held = out.get(row.host);
|
|
342
|
+
if (held === undefined || row.observedAt >= held.observedAt) out.set(row.host, row);
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The join as an operator report. Zero is printed as a result, not as absence. */
|
|
348
|
+
export function formatPointableBare(
|
|
349
|
+
join: PointableBareJoin,
|
|
350
|
+
catalogueRoot: string,
|
|
351
|
+
catalogueSize: number,
|
|
352
|
+
): string {
|
|
353
|
+
const { rows, skippedNonPerEntry, skippedMalformed } = join;
|
|
354
|
+
const lines: string[] = [];
|
|
355
|
+
lines.push(`ranker catalogue: ${catalogueRoot} (${catalogueSize} entries)`);
|
|
356
|
+
lines.push('');
|
|
357
|
+
|
|
358
|
+
if (rows.length === 0) {
|
|
359
|
+
lines.push(
|
|
360
|
+
'no per-entry observation to join. This is NOT a count of zero: only a host that',
|
|
361
|
+
'enumerates which entries arrived bare can be joined, and none has been recorded.',
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const row of rows) {
|
|
366
|
+
lines.push(
|
|
367
|
+
`${row.host} ${row.observedAt}: ${row.pointableBare} pointable-but-bare of ${row.bareTotal} bare`,
|
|
368
|
+
);
|
|
369
|
+
if (row.pointableBare > 0) {
|
|
370
|
+
lines.push(` the ranker can name: ${row.pointableNames.join(', ')}`);
|
|
371
|
+
}
|
|
372
|
+
if (row.unpointableBare > 0) {
|
|
373
|
+
lines.push(
|
|
374
|
+
` bare and NOT in the ranker's catalogue (${row.unpointableBare}): ${row.unpointableNames.join(', ')}`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (skippedNonPerEntry > 0) {
|
|
380
|
+
lines.push('');
|
|
381
|
+
lines.push(
|
|
382
|
+
`skipped ${skippedNonPerEntry} observation(s) whose host publishes no per-entry list —`,
|
|
383
|
+
'their empty `bare_names` records that nothing was enumerated, not that nothing was bare.',
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (skippedMalformed > 0) {
|
|
387
|
+
lines.push('');
|
|
388
|
+
lines.push(
|
|
389
|
+
`⚠️ skipped ${skippedMalformed} per-entry record(s) carrying no \`bare_names\` array —`,
|
|
390
|
+
'malformed log line(s). Reported rather than absorbed: a skipped record and a joined',
|
|
391
|
+
'record that found nothing must not look alike.',
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
lines.push('');
|
|
396
|
+
if (rows.length === 0) {
|
|
397
|
+
lines.push('D-4 divergence: unmeasured.');
|
|
398
|
+
return lines.join('\n');
|
|
399
|
+
}
|
|
400
|
+
lines.push('D-4 divergence, per host, off that host\'s LATEST joined observation:');
|
|
401
|
+
for (const [host, row] of [...latestPointableBarePerHost(rows)].sort((a, b) =>
|
|
402
|
+
a[0].localeCompare(b[0]),
|
|
403
|
+
)) {
|
|
404
|
+
lines.push(
|
|
405
|
+
row.pointableBare > 0
|
|
406
|
+
? ` ${host} (${row.observedAt}): ${row.pointableBare} skill(s) the ranker may point at while the model never received their description.`
|
|
407
|
+
: ` ${host} (${row.observedAt}): 0 — the ranker points at nothing this host degraded.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return lines.join('\n');
|
|
411
|
+
}
|