@event4u/agent-config 12.1.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 +164 -186
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- 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-routing.md +148 -33
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +34 -12
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- 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 +70 -4
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -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/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- 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-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +73 -30
- 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 +7 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +2653 -1365
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2679 -887
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.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/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- 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 +70 -4
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +140 -12
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- 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/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -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 +278 -5
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- 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 +101 -164
- 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/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +369 -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/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +99 -6
- package/src/scripts/hooks/concern_registry.ts +6 -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/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +43 -58
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- 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/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
- package/src/config/budget-routing.json +0 -7
|
@@ -80,6 +80,27 @@ import * as path from 'node:path';
|
|
|
80
80
|
/** Catalogue roots tried in order; the first that exists wins. */
|
|
81
81
|
export const DEFAULT_CATALOGUE_ROOTS = ['.claude/skills', 'src/skills'] as const;
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* First existing catalogue root under `workspaceRoot`, or `null`.
|
|
85
|
+
*
|
|
86
|
+
* `.claude/skills` before `src/skills` because a CONSUMER install carries the
|
|
87
|
+
* former and only a maintainer checkout carries the latter. Shared by the
|
|
88
|
+
* `skill-route` concern and the `suggest_skill_for_task` MCP handler on
|
|
89
|
+
* purpose: two resolvers over one catalogue is how a ranker and the tool that
|
|
90
|
+
* exposes it start ranking different trees.
|
|
91
|
+
*/
|
|
92
|
+
export function resolveSkillsRoot(workspaceRoot: string): string | null {
|
|
93
|
+
for (const candidate of DEFAULT_CATALOGUE_ROOTS) {
|
|
94
|
+
const abs = path.join(workspaceRoot, candidate);
|
|
95
|
+
try {
|
|
96
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return abs;
|
|
97
|
+
} catch {
|
|
98
|
+
// Unreadable candidate is not a match; try the next.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
83
104
|
/** Where observations accumulate. Append-only, one JSON object per line. */
|
|
84
105
|
export const OBSERVATION_LOG = path.join(
|
|
85
106
|
'agents',
|
|
@@ -356,12 +377,27 @@ export function analyzeSelector(
|
|
|
356
377
|
* `budget-strip-and-drop` — the host strips EVERY description, then drops N
|
|
357
378
|
* entries from the list entirely. There is no per-entry selector to find; the
|
|
358
379
|
* quantity that matters is N.
|
|
380
|
+
*
|
|
381
|
+
* `none` — the host truncated in NO way on this run: every entry arrived and
|
|
382
|
+
* every description survived. It is a mode a record must be able to state
|
|
383
|
+
* explicitly, because the alternative is leaving the field absent, and absent
|
|
384
|
+
* reads as `per-entry` — a mode this run did not exercise. A `none` record is
|
|
385
|
+
* the only shape that can say "the same host that strips today did not strip
|
|
386
|
+
* here" without inventing a mechanism.
|
|
359
387
|
*/
|
|
360
|
-
export type TruncationMode = 'per-entry' | 'budget-strip-and-drop';
|
|
388
|
+
export type TruncationMode = 'per-entry' | 'budget-strip-and-drop' | 'none';
|
|
361
389
|
|
|
362
390
|
/** Where the observation half came from. See the header. */
|
|
363
391
|
export type ObservationSource = 'self-report' | 'host-event';
|
|
364
392
|
|
|
393
|
+
/**
|
|
394
|
+
* The projection mode the observed install was deployed under.
|
|
395
|
+
*
|
|
396
|
+
* A closed enum, never a free-form label: the record type's privacy contract
|
|
397
|
+
* is that no field can hold arbitrary text, and this one is no exception.
|
|
398
|
+
*/
|
|
399
|
+
export type ProjectionMode = 'scoped' | 'legacy-all';
|
|
400
|
+
|
|
365
401
|
/** One append-only observation record. No field can hold free-form content. */
|
|
366
402
|
export interface ObservationRecord {
|
|
367
403
|
schema: 1;
|
|
@@ -390,6 +426,15 @@ export interface ObservationRecord {
|
|
|
390
426
|
* fire on command growth that causes no truncation at all.
|
|
391
427
|
*/
|
|
392
428
|
projected_skill_count?: number;
|
|
429
|
+
/**
|
|
430
|
+
* Projection mode the observed install was deployed under.
|
|
431
|
+
*
|
|
432
|
+
* Absent on every record written before the mode was captured, and absence
|
|
433
|
+
* is NOT `legacy-all`: those observations were taken without asking, so
|
|
434
|
+
* defaulting them either way would relabel a reading nobody took. A
|
|
435
|
+
* consumer comparing modes must skip a record that carries none.
|
|
436
|
+
*/
|
|
437
|
+
projection_mode?: ProjectionMode;
|
|
393
438
|
/**
|
|
394
439
|
* Entries the host dropped wholesale, read from the host's own event.
|
|
395
440
|
* Only ever present on a `budget-strip-and-drop` record — a `per-entry`
|
|
@@ -409,10 +454,27 @@ export function observationSourceOf(record: ObservationRecord): ObservationSourc
|
|
|
409
454
|
return record.observation_source ?? 'self-report';
|
|
410
455
|
}
|
|
411
456
|
|
|
457
|
+
/**
|
|
458
|
+
* The self-report path's record.
|
|
459
|
+
*
|
|
460
|
+
* `projectedSkillCount` and `projectionMode` are optional in the SAME shape the
|
|
461
|
+
* two host-event builders use — omitted rather than defaulted, because absence
|
|
462
|
+
* is not `legacy-all` and never was (see `ObservationRecord.projection_mode`).
|
|
463
|
+
*
|
|
464
|
+
* They were missing here until 2026-08-18, and the omission was load-bearing in
|
|
465
|
+
* one direction: the self-report path is the ONLY one that fills `bare_names`,
|
|
466
|
+
* so it is the only source the pointable-but-bare join below can read — and
|
|
467
|
+
* every claude observation in the corpus therefore carried no scope at all,
|
|
468
|
+
* while the codex records beside them did. A series that changes scope
|
|
469
|
+
* mid-flight without recording it is not comparable, which is exactly what
|
|
470
|
+
* claim 7 of the roadmap that asked for the field says.
|
|
471
|
+
*/
|
|
412
472
|
export function buildObservationRecord(
|
|
413
473
|
report: SelectorReport,
|
|
414
474
|
host: string,
|
|
415
475
|
observedAt: string,
|
|
476
|
+
projectedSkillCount?: number,
|
|
477
|
+
projectionMode?: ProjectionMode,
|
|
416
478
|
): ObservationRecord {
|
|
417
479
|
return {
|
|
418
480
|
schema: 1,
|
|
@@ -426,6 +488,8 @@ export function buildObservationRecord(
|
|
|
426
488
|
separating_candidates: report.candidates.filter((c) => c.separates).map((c) => c.id),
|
|
427
489
|
truncation_mode: 'per-entry',
|
|
428
490
|
observation_source: 'self-report',
|
|
491
|
+
...(projectedSkillCount === undefined ? {} : { projected_skill_count: projectedSkillCount }),
|
|
492
|
+
...(projectionMode === undefined ? {} : { projection_mode: projectionMode }),
|
|
429
493
|
};
|
|
430
494
|
}
|
|
431
495
|
|
|
@@ -507,6 +571,7 @@ export function buildHostEventRecord(
|
|
|
507
571
|
entriesOffered: number,
|
|
508
572
|
event: HostBudgetEvent,
|
|
509
573
|
projectedSkillCount?: number,
|
|
574
|
+
projectionMode?: ProjectionMode,
|
|
510
575
|
): ObservationRecord {
|
|
511
576
|
return {
|
|
512
577
|
schema: 1,
|
|
@@ -533,6 +598,55 @@ export function buildHostEventRecord(
|
|
|
533
598
|
observation_source: 'host-event',
|
|
534
599
|
dropped_count: event.droppedCount,
|
|
535
600
|
...(projectedSkillCount === undefined ? {} : { projected_skill_count: projectedSkillCount }),
|
|
601
|
+
...(projectionMode === undefined ? {} : { projection_mode: projectionMode }),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* A run on which the host published NO budget event and the caller asserted
|
|
607
|
+
* that this is a genuine no-truncation observation.
|
|
608
|
+
*
|
|
609
|
+
* The assertion is the caller's and the record says so by carrying
|
|
610
|
+
* `observation_source: 'host-event'` together with `truncation_mode: 'none'`:
|
|
611
|
+
* the host's channel was read, and what it contained was nothing. That pairing
|
|
612
|
+
* is what distinguishes this from a broken parser, which the CLI refuses to
|
|
613
|
+
* record at all without the explicit flag.
|
|
614
|
+
*
|
|
615
|
+
* `dropped_count: 0` is written rather than omitted, and it is safe: every
|
|
616
|
+
* consumer of a dropped count filters on `budget-strip-and-drop`, so a `none`
|
|
617
|
+
* record can never become a measured limit or a deploy-time warning.
|
|
618
|
+
*/
|
|
619
|
+
export function buildNoTruncationRecord(
|
|
620
|
+
host: string,
|
|
621
|
+
observedAt: string,
|
|
622
|
+
entriesOffered: number,
|
|
623
|
+
projectedSkillCount?: number,
|
|
624
|
+
projectionMode?: ProjectionMode,
|
|
625
|
+
): ObservationRecord {
|
|
626
|
+
return {
|
|
627
|
+
schema: 1,
|
|
628
|
+
observed_at: observedAt,
|
|
629
|
+
host,
|
|
630
|
+
entries_total: entriesOffered,
|
|
631
|
+
// BOTH counts stay 0, and neither is a claim that nothing arrived
|
|
632
|
+
// described. The host published no per-entry breakdown at all — it
|
|
633
|
+
// published nothing, which is what `truncation_mode: 'none'` records.
|
|
634
|
+
// Writing `described_count: entriesOffered` would look like a
|
|
635
|
+
// measurement and be an inference; 0 here means "not counted", exactly
|
|
636
|
+
// as it does on the strip-and-drop record beside it.
|
|
637
|
+
bare_count: 0,
|
|
638
|
+
described_count: 0,
|
|
639
|
+
bare_names: [],
|
|
640
|
+
// Nothing arrived bare, so no property separated bare from described —
|
|
641
|
+
// which is what `no-selector` states. Unlike the strip-and-drop case,
|
|
642
|
+
// here the label is literally true rather than merely available.
|
|
643
|
+
verdict: 'no-selector',
|
|
644
|
+
separating_candidates: [],
|
|
645
|
+
truncation_mode: 'none',
|
|
646
|
+
observation_source: 'host-event',
|
|
647
|
+
dropped_count: 0,
|
|
648
|
+
...(projectedSkillCount === undefined ? {} : { projected_skill_count: projectedSkillCount }),
|
|
649
|
+
...(projectionMode === undefined ? {} : { projection_mode: projectionMode }),
|
|
536
650
|
};
|
|
537
651
|
}
|
|
538
652
|
|
|
@@ -588,6 +702,119 @@ export function measureCatalogueVolume(host: string, root: string): CatalogueVol
|
|
|
588
702
|
};
|
|
589
703
|
}
|
|
590
704
|
|
|
705
|
+
/* ------------------------------------------------------------------ *
|
|
706
|
+
* Projection modes — what a host would be offered under `scoped`
|
|
707
|
+
* versus under `legacy-all`, side by side.
|
|
708
|
+
* ------------------------------------------------------------------ */
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* The two SKILL counts a package projects, plus the pack closure behind the
|
|
712
|
+
* smaller one.
|
|
713
|
+
*
|
|
714
|
+
* SKILLS, not artefacts, and the distinction is load-bearing rather than
|
|
715
|
+
* pedantic: a controlled probe on 2026-08-15 moved the measured host's dropped
|
|
716
|
+
* count by 0 when 60 commands were added and by +53 when 60 skills were, so a
|
|
717
|
+
* threshold read off artefact totals would fire on growth that causes no
|
|
718
|
+
* truncation at all.
|
|
719
|
+
*
|
|
720
|
+
* Both numbers come from ONE walk in the caller, partitioned by the
|
|
721
|
+
* installer's own prune predicate, so `scoped + prunedUnderScoped ===
|
|
722
|
+
* legacyAll` holds by construction rather than by two counters agreeing.
|
|
723
|
+
*/
|
|
724
|
+
export interface ProjectionModeCounts {
|
|
725
|
+
/** Skills a default `projection.mode: scoped` install deploys. */
|
|
726
|
+
scoped: number;
|
|
727
|
+
/** Every skill in the catalogue — what `legacy-all` deploys. */
|
|
728
|
+
legacyAll: number;
|
|
729
|
+
/** `legacyAll - scoped`; the reduction `scoped` buys. */
|
|
730
|
+
prunedUnderScoped: number;
|
|
731
|
+
/** Pack ids active by default under `scoped`, sorted. */
|
|
732
|
+
activePacks: string[];
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Which mode an installed host root is consistent with.
|
|
737
|
+
*
|
|
738
|
+
* `indeterminate` is a real answer and the common one. A host root is not
|
|
739
|
+
* required to hold exactly what this package projects — another suite, a
|
|
740
|
+
* plugin, or a stale install all put skills there — so a count matching
|
|
741
|
+
* neither number says the root was not installed from this tree at this
|
|
742
|
+
* revision, never that the install is broken. And when the two modes yield the
|
|
743
|
+
* same number (no pack-tagged skills), NO root can discriminate them, so every
|
|
744
|
+
* row is `indeterminate` by construction rather than by measurement.
|
|
745
|
+
*/
|
|
746
|
+
export type ProjectionModeMatch = 'scoped' | 'legacy-all' | 'indeterminate';
|
|
747
|
+
|
|
748
|
+
/** One host's installed skill count, read against the two mode counts. */
|
|
749
|
+
export interface HostProjectionRow {
|
|
750
|
+
host: string;
|
|
751
|
+
root: string;
|
|
752
|
+
/** Skills present in the host root right now — file-measured. */
|
|
753
|
+
installedSkills: number;
|
|
754
|
+
matches: ProjectionModeMatch;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Classify one measured host root against the package's two mode counts.
|
|
759
|
+
*
|
|
760
|
+
* Deliberately an equality test and nothing cleverer. A nearest-neighbour rule
|
|
761
|
+
* would label every root with whichever number happens to be closer, which
|
|
762
|
+
* reports a mode for a root that carries neither — the confident-number
|
|
763
|
+
* failure this module's header forbids twice over.
|
|
764
|
+
*/
|
|
765
|
+
export function classifyHostProjection(
|
|
766
|
+
volume: CatalogueVolume,
|
|
767
|
+
counts: ProjectionModeCounts,
|
|
768
|
+
): HostProjectionRow {
|
|
769
|
+
const ambiguous = counts.scoped === counts.legacyAll;
|
|
770
|
+
let matches: ProjectionModeMatch = 'indeterminate';
|
|
771
|
+
if (!ambiguous) {
|
|
772
|
+
if (volume.skillEntries === counts.scoped) matches = 'scoped';
|
|
773
|
+
else if (volume.skillEntries === counts.legacyAll) matches = 'legacy-all';
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
host: volume.host,
|
|
777
|
+
root: volume.root,
|
|
778
|
+
installedSkills: volume.skillEntries,
|
|
779
|
+
matches,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** The side-by-side report. Measurement only — it flips no default. */
|
|
784
|
+
export function formatProjectionModes(
|
|
785
|
+
counts: ProjectionModeCounts,
|
|
786
|
+
rows: readonly HostProjectionRow[],
|
|
787
|
+
): string {
|
|
788
|
+
const lines = [
|
|
789
|
+
'projected SKILL counts by projection mode (package side, one walk):',
|
|
790
|
+
` scoped: ${counts.scoped}`,
|
|
791
|
+
` legacy-all: ${counts.legacyAll}`,
|
|
792
|
+
` pruned under scoped: ${counts.prunedUnderScoped}`,
|
|
793
|
+
` active packs under scoped: ${counts.activePacks.length}`,
|
|
794
|
+
'',
|
|
795
|
+
];
|
|
796
|
+
if (rows.length === 0) {
|
|
797
|
+
lines.push(
|
|
798
|
+
'No host root measured. Pass --host-root <dir> (repeatable) to read what',
|
|
799
|
+
'each host currently holds against the two numbers above.',
|
|
800
|
+
);
|
|
801
|
+
return lines.join('\n');
|
|
802
|
+
}
|
|
803
|
+
lines.push('installed host roots:');
|
|
804
|
+
for (const row of [...rows].sort((a, b) => a.host.localeCompare(b.host))) {
|
|
805
|
+
lines.push(` ${row.host}: ${row.installedSkills} skills — consistent with ${row.matches}`);
|
|
806
|
+
}
|
|
807
|
+
lines.push(
|
|
808
|
+
'',
|
|
809
|
+
'This is the PROJECTION half only, and it is a measurement: no default was',
|
|
810
|
+
'read, changed, or recommended here. `indeterminate` means the root carries',
|
|
811
|
+
'neither count — another suite, a plugin, or a stale install all produce it.',
|
|
812
|
+
'No host limit is extrapolated from another host, and no delivered or',
|
|
813
|
+
'survivor count is computed anywhere.',
|
|
814
|
+
);
|
|
815
|
+
return lines.join('\n');
|
|
816
|
+
}
|
|
817
|
+
|
|
591
818
|
/* ------------------------------------------------------------------ *
|
|
592
819
|
* Known limits — read off recorded observations, never invented.
|
|
593
820
|
* ------------------------------------------------------------------ */
|
|
@@ -623,15 +850,54 @@ export interface KnownHostLimit {
|
|
|
623
850
|
observedAt: string;
|
|
624
851
|
}
|
|
625
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Is `candidate` the record a host's headline should quote, given `incumbent`?
|
|
855
|
+
*
|
|
856
|
+
* Shared by `knownHostLimits` and `formatPerHostVerdicts` because they used to
|
|
857
|
+
* disagree, and the disagreement was visible in one run: two arms of the same
|
|
858
|
+
* experiment recorded on the same date made `--limits` print `dropped 402` in
|
|
859
|
+
* the verdict block and `dropped 330` in the measured-truncations block, for
|
|
860
|
+
* one host on one day. Two reducers over one log must not break a tie in
|
|
861
|
+
* opposite directions.
|
|
862
|
+
*
|
|
863
|
+
* Later date always wins. On an EQUAL date the larger drop wins, which is also
|
|
864
|
+
* the conservative direction this module already takes elsewhere: over-warning
|
|
865
|
+
* about truncation is safe, under-warning is the failure. It is also what keeps
|
|
866
|
+
* a `scoped` arm from being quoted at a `legacy-all` install — the scoped arm
|
|
867
|
+
* is by construction the smaller number.
|
|
868
|
+
*/
|
|
869
|
+
function _supersedes(candidate: ObservationRecord, incumbent: ObservationRecord): boolean {
|
|
870
|
+
if (candidate.observed_at !== incumbent.observed_at) {
|
|
871
|
+
return candidate.observed_at > incumbent.observed_at;
|
|
872
|
+
}
|
|
873
|
+
return (candidate.dropped_count ?? 0) > (incumbent.dropped_count ?? 0);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** The record whose truncation a host's headline quotes, per `_supersedes`. */
|
|
877
|
+
export function headlineRecordPerHost(
|
|
878
|
+
records: readonly ObservationRecord[],
|
|
879
|
+
): Map<string, ObservationRecord> {
|
|
880
|
+
const out = new Map<string, ObservationRecord>();
|
|
881
|
+
for (const record of records) {
|
|
882
|
+
const previous = out.get(record.host);
|
|
883
|
+
if (previous === undefined || _supersedes(record, previous)) {
|
|
884
|
+
out.set(record.host, record);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return out;
|
|
888
|
+
}
|
|
889
|
+
|
|
626
890
|
/** The most recent observed truncation per host. */
|
|
627
891
|
export function knownHostLimits(records: readonly ObservationRecord[]): Map<string, KnownHostLimit> {
|
|
628
892
|
const out = new Map<string, KnownHostLimit>();
|
|
893
|
+
const chosen = new Map<string, ObservationRecord>();
|
|
629
894
|
for (const record of records) {
|
|
630
895
|
if (truncationModeOf(record) !== 'budget-strip-and-drop') continue;
|
|
631
896
|
if (typeof record.dropped_count !== 'number') continue;
|
|
632
897
|
if (record.dropped_count <= 0) continue;
|
|
633
|
-
const previous =
|
|
634
|
-
if (previous !== undefined && previous
|
|
898
|
+
const previous = chosen.get(record.host);
|
|
899
|
+
if (previous !== undefined && !_supersedes(record, previous)) continue;
|
|
900
|
+
chosen.set(record.host, record);
|
|
635
901
|
out.set(record.host, {
|
|
636
902
|
host: record.host,
|
|
637
903
|
droppedEntries: record.dropped_count,
|
|
@@ -799,10 +1065,16 @@ export function formatPerHostVerdicts(records: readonly ObservationRecord[]): st
|
|
|
799
1065
|
byHost.set(record.host, bucket);
|
|
800
1066
|
}
|
|
801
1067
|
|
|
1068
|
+
const headline = headlineRecordPerHost(records);
|
|
1069
|
+
|
|
802
1070
|
const lines: string[] = [];
|
|
803
1071
|
for (const host of [...byHost.keys()].sort()) {
|
|
804
1072
|
const bucket = byHost.get(host)!;
|
|
805
|
-
|
|
1073
|
+
// One selector for both reducers — see `_supersedes`. The previous
|
|
1074
|
+
// `>=` reduce here kept the FIRST of two same-date rows while
|
|
1075
|
+
// `knownHostLimits` kept the LAST, so one run printed two different
|
|
1076
|
+
// drop counts for one host on one day.
|
|
1077
|
+
const latest = headline.get(host)!;
|
|
806
1078
|
const mode = truncationModeOf(latest);
|
|
807
1079
|
lines.push(
|
|
808
1080
|
`${host}: ${bucket.length} observation(s) · mode ${mode} · ` +
|
|
@@ -816,7 +1088,7 @@ export function formatPerHostVerdicts(records: readonly ObservationRecord[]): st
|
|
|
816
1088
|
);
|
|
817
1089
|
}
|
|
818
1090
|
|
|
819
|
-
const modes = new Set([...
|
|
1091
|
+
const modes = new Set([...headline.values()].map((r) => truncationModeOf(r)));
|
|
820
1092
|
lines.push('');
|
|
821
1093
|
lines.push(
|
|
822
1094
|
modes.size > 1
|
|
@@ -860,3 +1132,4 @@ export function formatReport(report: SelectorReport): string {
|
|
|
860
1132
|
);
|
|
861
1133
|
return lines.join('\n');
|
|
862
1134
|
}
|
|
1135
|
+
|