@event4u/agent-config 9.20.0 → 9.22.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 +86 -0
- package/dist/agent-src/commands/worktree/cleanup.md +17 -0
- package/dist/agent-src/commands/worktree/status.md +6 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +5 -5
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.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 +58 -2
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +47 -29
- package/package.json +1 -1
- package/src/config/gate-coverage.yml +22 -4
- package/src/config/gate-violation-baselines.json +3 -8
- package/src/scripts/_lib/bench_ab_activation.ts +368 -0
- package/src/scripts/_lib/gate_population.ts +70 -0
- package/src/scripts/_lib/roadmap_granularity.ts +157 -0
- package/src/scripts/_lib/scan_scope.ts +44 -0
- package/src/scripts/_lib/stdin.ts +82 -0
- package/src/scripts/audit_adr_coverage.ts +38 -0
- package/src/scripts/audit_auto_rules.ts +17 -3
- package/src/scripts/audit_command_surface.ts +51 -0
- package/src/scripts/audit_initial_context.ts +39 -3
- package/src/scripts/audit_mcp_tools.ts +25 -0
- package/src/scripts/audit_overlap.ts +24 -1
- package/src/scripts/audit_skill_descriptions.ts +19 -0
- package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
- package/src/scripts/bench_ab_task_runner.ts +18 -0
- package/src/scripts/bench_ab_v2_run.ts +147 -6
- package/src/scripts/bench_ab_v2_stats.ts +74 -2
- package/src/scripts/check_adr_frontmatter.ts +23 -3
- package/src/scripts/check_always_budget.ts +24 -0
- package/src/scripts/check_artefact_checksums.ts +24 -1
- package/src/scripts/check_artefact_count_messaging.ts +20 -0
- package/src/scripts/check_augmentignore.ts +22 -15
- package/src/scripts/check_backstop_debt.ts +22 -0
- package/src/scripts/check_beta_review_markers.ts +22 -1
- package/src/scripts/check_bridge_derivation.ts +22 -1
- package/src/scripts/check_bundle_path_leakage.ts +26 -0
- package/src/scripts/check_ci_strict_superset.ts +21 -3
- package/src/scripts/check_claims.ts +22 -0
- package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
- package/src/scripts/check_cluster_patterns.ts +26 -4
- package/src/scripts/check_command_count_messaging.ts +20 -4
- package/src/scripts/check_comparison.ts +18 -0
- package/src/scripts/check_condensed_paths.ts +23 -2
- package/src/scripts/check_consumer_scope_flip.ts +21 -2
- package/src/scripts/check_council_config_location.ts +20 -0
- package/src/scripts/check_council_layout.ts +22 -2
- package/src/scripts/check_council_references.ts +23 -1
- package/src/scripts/check_dependency_floors.ts +19 -3
- package/src/scripts/check_discovery_determinism.ts +26 -0
- package/src/scripts/check_enforcement_coverage.ts +20 -0
- package/src/scripts/check_evaluator_budgets.ts +23 -0
- package/src/scripts/check_finding_dispositions.ts +29 -0
- package/src/scripts/check_gate_coverage.ts +63 -22
- package/src/scripts/check_gate_paths.ts +30 -5
- package/src/scripts/check_generated_artefact_headers.ts +21 -0
- package/src/scripts/check_generator_output_coverage.ts +23 -0
- package/src/scripts/check_gitignore_freshness.ts +23 -0
- package/src/scripts/check_host_loadability.ts +34 -8
- package/src/scripts/check_installer_import_purity.ts +22 -8
- package/src/scripts/check_kernel_prefix_stability.ts +19 -1
- package/src/scripts/check_kernel_rule_bundle.ts +23 -0
- package/src/scripts/check_knowledge_cards.ts +13 -0
- package/src/scripts/check_knowledge_pages.ts +23 -0
- package/src/scripts/check_knowledge_sharing.ts +27 -0
- package/src/scripts/check_md_language.ts +24 -0
- package/src/scripts/check_media_deps.ts +23 -0
- package/src/scripts/check_memory.ts +26 -1
- package/src/scripts/check_memory_contradiction.ts +16 -1
- package/src/scripts/check_memory_proposal.ts +15 -0
- package/src/scripts/check_memory_similarity.ts +16 -0
- package/src/scripts/check_module_management_neutral.ts +19 -11
- package/src/scripts/check_no_conflict_markers.ts +22 -1
- package/src/scripts/check_no_external_sources.ts +26 -1
- package/src/scripts/check_no_local_settings_committed.ts +37 -10
- package/src/scripts/check_no_new_legacy_path.ts +30 -1
- package/src/scripts/check_one_off_location.ts +43 -0
- package/src/scripts/check_originality_freshness.ts +25 -1
- package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
- package/src/scripts/check_pack_size.ts +22 -0
- package/src/scripts/check_preamble_payload_budget.ts +35 -5
- package/src/scripts/check_proposal.ts +24 -1
- package/src/scripts/check_public_catalog_links.ts +27 -0
- package/src/scripts/check_public_links.ts +19 -0
- package/src/scripts/check_quality_regression.ts +19 -0
- package/src/scripts/check_reach_channels.ts +44 -0
- package/src/scripts/check_reach_staleness.ts +27 -0
- package/src/scripts/check_release_adjacent_health.ts +28 -0
- package/src/scripts/check_release_highlights.ts +34 -0
- package/src/scripts/check_release_includes_discovery.ts +25 -6
- package/src/scripts/check_release_pr_shape.ts +18 -1
- package/src/scripts/check_release_published.ts +21 -0
- package/src/scripts/check_release_surface_equality.ts +22 -0
- package/src/scripts/check_release_trunk_sync.ts +19 -0
- package/src/scripts/check_reply_consistency.ts +22 -0
- package/src/scripts/check_roadmap_trackable.ts +20 -0
- package/src/scripts/check_role_doc_links.ts +23 -0
- package/src/scripts/check_rule_invariants.ts +14 -0
- package/src/scripts/check_secret_leak.ts +47 -0
- package/src/scripts/check_skill_gaps.ts +32 -0
- package/src/scripts/check_skill_requires.ts +19 -0
- package/src/scripts/check_source_pointer_freshness.ts +20 -0
- package/src/scripts/check_structural_breaking.ts +29 -2
- package/src/scripts/check_surface_tiers.ts +34 -0
- package/src/scripts/check_template_pin_drift.ts +23 -1
- package/src/scripts/check_test_coverage_diff.ts +17 -1
- package/src/scripts/check_token_optimizer_freshness.ts +18 -3
- package/src/scripts/check_token_quality_golden.ts +21 -0
- package/src/scripts/check_token_regression.ts +25 -0
- package/src/scripts/check_tracked_but_ignored.ts +26 -0
- package/src/scripts/check_trigger_eval_presence.ts +23 -1
- package/src/scripts/check_trigger_evals.ts +36 -0
- package/src/scripts/check_trunk_drift.ts +23 -0
- package/src/scripts/check_update_banner.ts +24 -0
- package/src/scripts/gate_scope_classify.ts +513 -0
- package/src/scripts/hook_manifest.yaml +1 -1
- package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/lint_agent_security.ts +19 -0
- package/src/scripts/lint_agent_skill_names.ts +30 -0
- package/src/scripts/lint_agents_layout.ts +20 -0
- package/src/scripts/lint_agents_md.ts +20 -0
- package/src/scripts/lint_archived_skills.ts +24 -3
- package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
- package/src/scripts/lint_bench_ab.ts +25 -0
- package/src/scripts/lint_bench_corpus.ts +20 -4
- package/src/scripts/lint_breaking_changes_index.ts +24 -0
- package/src/scripts/lint_budget_ownership.ts +20 -3
- package/src/scripts/lint_changelog_rollback.ts +24 -0
- package/src/scripts/lint_command_flow_coverage.ts +22 -0
- package/src/scripts/lint_command_tiers.ts +29 -5
- package/src/scripts/lint_commit_subjects.ts +15 -0
- package/src/scripts/lint_confusables.ts +24 -0
- package/src/scripts/lint_consumer_internal_refs.ts +24 -0
- package/src/scripts/lint_design_quality.ts +22 -0
- package/src/scripts/lint_design_slop.ts +23 -0
- package/src/scripts/lint_discovery_manifest.ts +23 -0
- package/src/scripts/lint_discovery_vocabulary.ts +19 -0
- package/src/scripts/lint_documented_commands.ts +23 -0
- package/src/scripts/lint_empty_roadmaps.ts +23 -0
- package/src/scripts/lint_eval_fixture_citations.ts +19 -0
- package/src/scripts/lint_eval_freshness.ts +35 -0
- package/src/scripts/lint_examples.ts +18 -3
- package/src/scripts/lint_explain_trace.ts +23 -0
- package/src/scripts/lint_featured_skills.ts +26 -1
- package/src/scripts/lint_flows.ts +19 -0
- package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
- package/src/scripts/lint_ghostwriter_source.ts +26 -6
- package/src/scripts/lint_glama_drift.ts +24 -0
- package/src/scripts/lint_global_paths.ts +24 -0
- package/src/scripts/lint_governed_writes.ts +20 -0
- package/src/scripts/lint_hidden_unicode.ts +25 -0
- package/src/scripts/lint_hook_concern_budget.ts +39 -6
- package/src/scripts/lint_hook_manifest.ts +22 -0
- package/src/scripts/lint_instruction_smuggling.ts +23 -0
- package/src/scripts/lint_knowledge_scale.ts +32 -2
- package/src/scripts/lint_legal_pack.ts +31 -0
- package/src/scripts/lint_marketplace.ts +26 -0
- package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
- package/src/scripts/lint_mcp_config_security.ts +24 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
- package/src/scripts/lint_memory_tripwire.ts +35 -0
- package/src/scripts/lint_missions.ts +23 -7
- package/src/scripts/lint_model_tier_coverage.ts +18 -0
- package/src/scripts/lint_namespace_collisions.ts +19 -0
- package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
- package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
- package/src/scripts/lint_one_off_age.ts +38 -2
- package/src/scripts/lint_orchestration_dsl.ts +23 -3
- package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
- package/src/scripts/lint_originality.ts +22 -4
- package/src/scripts/lint_originality_shingles.ts +28 -3
- package/src/scripts/lint_output_slop.ts +19 -0
- package/src/scripts/lint_override_kernel_guard.ts +30 -0
- package/src/scripts/lint_pack_first_win.ts +23 -0
- package/src/scripts/lint_pack_risk_class.ts +19 -3
- package/src/scripts/lint_persistence.ts +19 -0
- package/src/scripts/lint_persona_governance.ts +18 -3
- package/src/scripts/lint_positioning.ts +23 -0
- package/src/scripts/lint_pre_migration_refs.ts +24 -0
- package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
- package/src/scripts/lint_provenance.ts +21 -0
- package/src/scripts/lint_provenance_vocabulary.ts +24 -1
- package/src/scripts/lint_readme_jargon.ts +21 -3
- package/src/scripts/lint_readme_serial_comma.ts +21 -3
- package/src/scripts/lint_readme_size.ts +21 -3
- package/src/scripts/lint_regression.ts +28 -0
- package/src/scripts/lint_roadmap_blockers.ts +43 -0
- package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
- package/src/scripts/lint_roadmap_complexity.ts +47 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
- package/src/scripts/lint_rule_interactions.ts +16 -2
- package/src/scripts/lint_rule_tiers.ts +19 -6
- package/src/scripts/lint_showcase_sessions.ts +19 -3
- package/src/scripts/lint_skill_descriptions.ts +19 -0
- package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
- package/src/scripts/lint_skill_originality.ts +18 -3
- package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
- package/src/scripts/lint_skill_tools.ts +84 -145
- package/src/scripts/lint_spawn_payload.ts +27 -1
- package/src/scripts/lint_store_boundary.ts +19 -0
- package/src/scripts/lint_subagent_determinism.ts +23 -1
- package/src/scripts/lint_supported_tools_matrix.ts +18 -0
- package/src/scripts/lint_surface_matrix.ts +19 -3
- package/src/scripts/lint_ticket_buildable.ts +23 -0
- package/src/scripts/lint_topics_yaml.ts +19 -12
- package/src/scripts/lint_trigger_precision.ts +27 -0
- package/src/scripts/lint_trust_coherence.ts +21 -0
- package/src/scripts/lint_ui_stack_bundles.ts +17 -3
- package/src/scripts/lint_value_dashboard.ts +19 -3
- package/src/scripts/lint_versioned_cache.ts +19 -1
- package/src/scripts/lint_workflow_security.ts +22 -14
- package/src/scripts/pr_url_reminder_hook.ts +1 -1
- package/src/scripts/skill_collision_clusters.ts +37 -3
- package/src/scripts/skill_discovery.ts +20 -0
- package/src/scripts/skill_eval_coverage.ts +22 -0
- package/src/scripts/skill_overlap.ts +32 -6
- package/src/scripts/skill_preview.ts +31 -0
- package/src/scripts/skill_trigger_eval.ts +18 -0
- package/src/scripts/skill_usage_collect.ts +21 -0
- package/src/scripts/skill_usage_report.ts +21 -0
- package/src/scripts/sweep_dead_scan_roots.ts +11 -3
- package/src/scripts/verify_physical_move.ts +24 -0
- package/src/scripts/worktree_cleanup_check.ts +319 -4
- package/src/scripts/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -53,6 +53,18 @@ import { parse as parseYaml } from 'yaml';
|
|
|
53
53
|
|
|
54
54
|
import * as v1 from './bench_ab_task_runner.js';
|
|
55
55
|
import * as scoring from './_lib/bench_ab_scoring_v2.js';
|
|
56
|
+
import {
|
|
57
|
+
SweepBudget,
|
|
58
|
+
activation_verdict,
|
|
59
|
+
audit_activation,
|
|
60
|
+
expected_injection,
|
|
61
|
+
is_bare_alias,
|
|
62
|
+
tier_for_model,
|
|
63
|
+
verify_model_id,
|
|
64
|
+
type TierRates,
|
|
65
|
+
type TokensBreakdown,
|
|
66
|
+
} from './_lib/bench_ab_activation.js';
|
|
67
|
+
import { load_pricing } from './_lib/bench_cost.js';
|
|
56
68
|
|
|
57
69
|
const _HERE = fileURLToPath(import.meta.url);
|
|
58
70
|
|
|
@@ -66,6 +78,7 @@ const CORPUS_PATH = path.join(REPO_ROOT, 'internal', 'bench', 'corpora', 'ab-tra
|
|
|
66
78
|
const FIXTURES_ROOT = path.join(REPO_ROOT, 'internal', 'bench', 'ab');
|
|
67
79
|
const REPORTS_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'reports', 'ab-v2');
|
|
68
80
|
const CHECKPOINTS_DIR = path.join(REPORTS_DIR, 'checkpoints');
|
|
81
|
+
const PRICING_PATH = path.join(REPO_ROOT, 'internal', 'bench', 'pricing.yaml');
|
|
69
82
|
// CRITICAL (2026-06-15): clones MUST live OUTSIDE the agent-config repo. A clone
|
|
70
83
|
// under the repo lets Claude discover the repo's own project surface (CLAUDE.md /
|
|
71
84
|
// AGENTS.md / .claude/rules+skills) walking up from the cwd — so the `vanilla`
|
|
@@ -461,6 +474,31 @@ export function run_live_codex(
|
|
|
461
474
|
};
|
|
462
475
|
}
|
|
463
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Delta #1–#3 — stamp the measurement-integrity block onto a trial record.
|
|
479
|
+
*
|
|
480
|
+
* `tokens_breakdown` is preserved here (delta #2) because it is the ONLY
|
|
481
|
+
* observable that can see a plugin-arm collapse: `injected_chars` is the length
|
|
482
|
+
* of a file the harness wrote itself, and it is 0 for every arm that arrives
|
|
483
|
+
* through global settings. Discarding the breakdown made the audit
|
|
484
|
+
* unreconstructable even post-hoc.
|
|
485
|
+
*/
|
|
486
|
+
export function integrity_fields(run: Dict, spec: ArmSpec, injected_chars: number, model: string | null): Dict {
|
|
487
|
+
const breakdown = (run['tokens_breakdown'] ?? {}) as TokensBreakdown | Record<string, never>;
|
|
488
|
+
const models_seen = Array.isArray(run['models_seen']) ? (run['models_seen'] as string[]) : [];
|
|
489
|
+
return {
|
|
490
|
+
tokens_breakdown: breakdown,
|
|
491
|
+
models_seen,
|
|
492
|
+
activation: activation_verdict({
|
|
493
|
+
expected: expected_injection(spec),
|
|
494
|
+
tokens_breakdown: breakdown,
|
|
495
|
+
injected_chars,
|
|
496
|
+
errored: _pyBool(run['errored']),
|
|
497
|
+
}),
|
|
498
|
+
model_check: model ? verify_model_id(model, models_seen) : { ok: true, reason: 'no model pinned' },
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
464
502
|
export function run_one(task: Dict, arm: string, opts: RunOneOpts): Dict {
|
|
465
503
|
const spec = ARMS[arm] as ArmSpec;
|
|
466
504
|
if (spec.recursive) {
|
|
@@ -501,6 +539,7 @@ export function run_one(task: Dict, arm: string, opts: RunOneOpts): Dict {
|
|
|
501
539
|
discipline_pass: score.discipline_pass,
|
|
502
540
|
metrics: trajectory_metrics(run, score),
|
|
503
541
|
injected_chars: sp_text ? sp_text.length : 0,
|
|
542
|
+
...integrity_fields(run, spec, sp_text ? sp_text.length : 0, opts.model),
|
|
504
543
|
};
|
|
505
544
|
}
|
|
506
545
|
|
|
@@ -618,6 +657,10 @@ export function run_one_recursive(
|
|
|
618
657
|
injected_chars: last_verdict_len,
|
|
619
658
|
depth_reached: depth,
|
|
620
659
|
stop_reason,
|
|
660
|
+
// `injected_chars` here counts the re-attempt VERDICT text, not an arm
|
|
661
|
+
// injection — the recursive arm's channel is the plugin. Pass 0 so the
|
|
662
|
+
// audit reads the arm's real channel instead of flagging the verdict.
|
|
663
|
+
...integrity_fields(run, ARMS['package-recursive'] as ArmSpec, 0, opts.model),
|
|
621
664
|
};
|
|
622
665
|
}
|
|
623
666
|
|
|
@@ -767,17 +810,28 @@ export function collect_records(
|
|
|
767
810
|
run_fn: (task: Dict, arm: string, seed: number) => Dict,
|
|
768
811
|
ckpt: CheckpointIO | null = null,
|
|
769
812
|
log: (msg: string) => void = (m) => process.stderr.write(m),
|
|
770
|
-
|
|
813
|
+
/**
|
|
814
|
+
* Delta #4 — sweep-level spend guard. Called after each executed run with
|
|
815
|
+
* that run's record; a non-null return aborts the sweep. Records collected
|
|
816
|
+
* so far are still returned (and are already on the checkpoint), so an
|
|
817
|
+
* abort costs no completed work.
|
|
818
|
+
*/
|
|
819
|
+
guard: ((record: Dict) => string | null) | null = null,
|
|
820
|
+
): { records: Dict[]; executed: number; reused: number; aborted: string | null } {
|
|
771
821
|
const total = tasks.length * arms.length * seeds;
|
|
772
822
|
let done = 0;
|
|
773
823
|
let executed = 0;
|
|
774
824
|
let reused = 0;
|
|
825
|
+
let aborted: string | null = null;
|
|
775
826
|
const records: Dict[] = [];
|
|
776
827
|
for (const task of tasks) {
|
|
777
828
|
const per_arm: Record<string, Dict[]> = {};
|
|
778
829
|
for (const arm of arms) {
|
|
779
830
|
const seed_runs: Dict[] = [];
|
|
780
831
|
for (let seed = 0; seed < seeds; seed += 1) {
|
|
832
|
+
if (aborted) {
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
781
835
|
done += 1;
|
|
782
836
|
const key = run_key(String(task['id']), arm, seed);
|
|
783
837
|
const cached = ckpt?.completed.get(key);
|
|
@@ -794,6 +848,7 @@ export function collect_records(
|
|
|
794
848
|
append_checkpoint(ckpt.path, key, r);
|
|
795
849
|
}
|
|
796
850
|
seed_runs.push(r);
|
|
851
|
+
aborted = guard?.(r) ?? null;
|
|
797
852
|
}
|
|
798
853
|
per_arm[arm] = seed_runs;
|
|
799
854
|
}
|
|
@@ -803,8 +858,11 @@ export function collect_records(
|
|
|
803
858
|
rule: task['rule'],
|
|
804
859
|
arms: per_arm,
|
|
805
860
|
});
|
|
861
|
+
if (aborted) {
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
806
864
|
}
|
|
807
|
-
return { records, executed, reused };
|
|
865
|
+
return { records, executed, reused, aborted };
|
|
808
866
|
}
|
|
809
867
|
|
|
810
868
|
export function main(argv: string[] | null = null): number {
|
|
@@ -841,11 +899,44 @@ export function main(argv: string[] | null = null): number {
|
|
|
841
899
|
}
|
|
842
900
|
}
|
|
843
901
|
|
|
902
|
+
// Delta #3 — refuse a bare alias BEFORE any spend. An alias resolves
|
|
903
|
+
// server-side to whatever is current, so a report produced under one is
|
|
904
|
+
// unreproducible: nothing in it records which model actually answered.
|
|
905
|
+
if (is_bare_alias(args.model)) {
|
|
906
|
+
process.stderr.write(
|
|
907
|
+
`bench_ab_v2: refusing bare model alias "${args.model}" — pin a full id ` +
|
|
908
|
+
`(e.g. claude-sonnet-4-6). An alias makes the report unreproducible.\n`,
|
|
909
|
+
);
|
|
910
|
+
return 2;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Delta #4 — sweep-level cap. `--max-budget-usd` is PER RUN, so 30×4×3 at
|
|
914
|
+
// --budget 3.5 has a $1,260 ceiling with nothing to stop it. Prices come
|
|
915
|
+
// from internal/bench/pricing.yaml; an unrecognised model tier leaves the
|
|
916
|
+
// guard inert rather than silently mispricing the sweep.
|
|
917
|
+
//
|
|
918
|
+
// Resolved HERE, with the other argument validation, rather than next to
|
|
919
|
+
// its use below: a cap the harness cannot price is a bad argument, and a
|
|
920
|
+
// bad argument must be reported whether or not the host CLI is installed.
|
|
921
|
+
// Behind the CLI-presence check it was unreachable on any machine without
|
|
922
|
+
// `claude` — including CI.
|
|
923
|
+
const [rates] = load_pricing(PRICING_PATH);
|
|
924
|
+
const tier = tier_for_model(args.model);
|
|
925
|
+
const tier_rates: TierRates | null = tier && rates[tier] ? (rates[tier] as TierRates) : null;
|
|
926
|
+
if (args.max_usd !== null && tier_rates === null) {
|
|
927
|
+
process.stderr.write(
|
|
928
|
+
`bench_ab_v2: --max-usd given but no pricing row matches model "${args.model}" ` +
|
|
929
|
+
`(${_relToRootPosix(PRICING_PATH)}) — the sweep cap cannot be enforced.\n`,
|
|
930
|
+
);
|
|
931
|
+
return 2;
|
|
932
|
+
}
|
|
933
|
+
|
|
844
934
|
if (args.mode === 'dry-run') {
|
|
845
935
|
process.stdout.write(
|
|
846
936
|
`bench_ab_v2: DRY — ${tasks.length} tasks × ${arms.length} arms × ` +
|
|
847
937
|
`${args.seeds} seeds = ${tasks.length * arms.length * args.seeds} runs ` +
|
|
848
|
-
`(model=${args.model}, budget=${_pyNumStr(args.budget)}
|
|
938
|
+
`(model=${args.model}, budget=${_pyNumStr(args.budget)}, ` +
|
|
939
|
+
`sweep cap=${args.max_usd === null ? 'none' : `$${_pyNumStr(args.max_usd)}`}). No spend.\n`,
|
|
849
940
|
);
|
|
850
941
|
return 0;
|
|
851
942
|
}
|
|
@@ -891,7 +982,9 @@ export function main(argv: string[] | null = null): number {
|
|
|
891
982
|
ckpt = { path: ckpt_path, completed };
|
|
892
983
|
}
|
|
893
984
|
|
|
894
|
-
const
|
|
985
|
+
const budget = new SweepBudget(args.max_usd, tier_rates);
|
|
986
|
+
|
|
987
|
+
const { records, aborted } = collect_records(
|
|
895
988
|
tasks,
|
|
896
989
|
arms,
|
|
897
990
|
args.seeds,
|
|
@@ -905,8 +998,17 @@ export function main(argv: string[] | null = null): number {
|
|
|
905
998
|
host: args.host,
|
|
906
999
|
}),
|
|
907
1000
|
ckpt,
|
|
1001
|
+
undefined,
|
|
1002
|
+
(r) => budget.add(r['tokens_breakdown'] as TokensBreakdown | undefined),
|
|
908
1003
|
);
|
|
909
1004
|
|
|
1005
|
+
// Delta #1 — the activation audit. Runs BEFORE the report is judged usable:
|
|
1006
|
+
// the paired plugin direction needs the whole record set, so it cannot be a
|
|
1007
|
+
// per-trial check alone. `vanilla` is the baseline; every arm that is
|
|
1008
|
+
// supposed to carry a surface the baseline lacks is a lift arm.
|
|
1009
|
+
const lift_arms = arms.filter((a) => a !== 'vanilla' && expected_injection(ARMS[a] as ArmSpec) !== 'none');
|
|
1010
|
+
const audit = audit_activation(records, { baseline_arm: 'vanilla', lift_arms });
|
|
1011
|
+
|
|
910
1012
|
const stamp = v1.utc_stamp();
|
|
911
1013
|
const payload: Dict = {
|
|
912
1014
|
schema: 'ab-bench-v2/0.1',
|
|
@@ -915,9 +1017,17 @@ export function main(argv: string[] | null = null): number {
|
|
|
915
1017
|
seeds: args.seeds,
|
|
916
1018
|
arms,
|
|
917
1019
|
budget_usd_per_run: args.budget,
|
|
1020
|
+
sweep_cap_usd: args.max_usd,
|
|
1021
|
+
sweep_spend_usd: new PyFloat(Number(budget.spent_usd.toFixed(4))),
|
|
918
1022
|
placebo_chars,
|
|
919
1023
|
corpus: 'ab-trackb-v2',
|
|
920
1024
|
host: args.host,
|
|
1025
|
+
activation_audit: {
|
|
1026
|
+
checked: audit.checked,
|
|
1027
|
+
skipped: audit.skipped,
|
|
1028
|
+
violations: audit.violations as unknown as Json,
|
|
1029
|
+
},
|
|
1030
|
+
aborted: aborted ?? null,
|
|
921
1031
|
records,
|
|
922
1032
|
};
|
|
923
1033
|
const out = path.join(REPORTS_DIR, `${stamp}-ab-v2-paired.json`);
|
|
@@ -928,8 +1038,31 @@ export function main(argv: string[] | null = null): number {
|
|
|
928
1038
|
fs.rmSync(ckpt.path);
|
|
929
1039
|
}
|
|
930
1040
|
process.stdout.write(
|
|
931
|
-
`bench_ab_v2: wrote ${_relToRootPosix(out)} ` +
|
|
1041
|
+
`bench_ab_v2: wrote ${_relToRootPosix(out)} ` +
|
|
1042
|
+
`(${records.length} tasks, ${total} runs, spend $${budget.spent_usd.toFixed(2)})\n`,
|
|
932
1043
|
);
|
|
1044
|
+
|
|
1045
|
+
// The report is always written — an aborted or integrity-failed sweep still
|
|
1046
|
+
// cost money and its raw runs stay inspectable. The EXIT CODE is what makes
|
|
1047
|
+
// the failure non-ignorable, so a caller cannot mistake an invalid sweep for
|
|
1048
|
+
// a publishable one.
|
|
1049
|
+
if (aborted) {
|
|
1050
|
+
process.stderr.write(`bench_ab_v2: ${aborted}\n`);
|
|
1051
|
+
return 2;
|
|
1052
|
+
}
|
|
1053
|
+
if (audit.violations.length > 0) {
|
|
1054
|
+
process.stderr.write(
|
|
1055
|
+
`bench_ab_v2: ACTIVATION AUDIT FAILED — ${audit.violations.length} violation(s) ` +
|
|
1056
|
+
`over ${audit.checked} checked pair(s). This report is NOT publishable.\n`,
|
|
1057
|
+
);
|
|
1058
|
+
for (const v of audit.violations.slice(0, 20)) {
|
|
1059
|
+
process.stderr.write(` ${v.kind}: ${v.task_id} · ${v.arm} · seed ${v.seed} — ${v.detail}\n`);
|
|
1060
|
+
}
|
|
1061
|
+
if (audit.violations.length > 20) {
|
|
1062
|
+
process.stderr.write(` … ${audit.violations.length - 20} more (see the report)\n`);
|
|
1063
|
+
}
|
|
1064
|
+
return 2;
|
|
1065
|
+
}
|
|
933
1066
|
return 0;
|
|
934
1067
|
}
|
|
935
1068
|
|
|
@@ -942,6 +1075,8 @@ interface ParsedArgs {
|
|
|
942
1075
|
limit: number;
|
|
943
1076
|
model: string;
|
|
944
1077
|
budget: number;
|
|
1078
|
+
/** Sweep-wide USD cap (delta #4); `null` = uncapped, the pre-existing behaviour. */
|
|
1079
|
+
max_usd: number | null;
|
|
945
1080
|
timeout: number;
|
|
946
1081
|
mode: string;
|
|
947
1082
|
host: string;
|
|
@@ -960,13 +1095,14 @@ function parse_args(argv: string[]): ParsedArgs {
|
|
|
960
1095
|
limit: 0,
|
|
961
1096
|
model: 'claude-sonnet-4-6',
|
|
962
1097
|
budget: 1.0,
|
|
1098
|
+
max_usd: null,
|
|
963
1099
|
timeout: 180,
|
|
964
1100
|
mode: 'live',
|
|
965
1101
|
host: 'claude',
|
|
966
1102
|
checkpoint: true,
|
|
967
1103
|
fresh: false,
|
|
968
1104
|
};
|
|
969
|
-
const usage = `usage: ${prog} [-h] [--arms ARMS] [--seeds SEEDS] [--tasks TASKS] [--limit LIMIT] [--model MODEL] [--budget BUDGET] [--timeout TIMEOUT] [--mode {live,dry-run}] [--host {claude,codex}] [--no-checkpoint] [--fresh]\n`;
|
|
1105
|
+
const usage = `usage: ${prog} [-h] [--arms ARMS] [--seeds SEEDS] [--tasks TASKS] [--limit LIMIT] [--model MODEL] [--budget BUDGET] [--max-usd MAX_USD] [--timeout TIMEOUT] [--mode {live,dry-run}] [--host {claude,codex}] [--no-checkpoint] [--fresh]\n`;
|
|
970
1106
|
const argErr = (msg: string): never => {
|
|
971
1107
|
process.stderr.write(usage);
|
|
972
1108
|
process.stderr.write(`${prog}: error: ${msg}\n`);
|
|
@@ -1014,6 +1150,11 @@ function parse_args(argv: string[]): ParsedArgs {
|
|
|
1014
1150
|
i += 1;
|
|
1015
1151
|
} else if (a.startsWith('--budget=')) {
|
|
1016
1152
|
out.budget = _pyFloatArg(a.slice('--budget='.length), '--budget');
|
|
1153
|
+
} else if (a === '--max-usd') {
|
|
1154
|
+
out.max_usd = _pyFloatArg(need(i, '--max-usd'), '--max-usd');
|
|
1155
|
+
i += 1;
|
|
1156
|
+
} else if (a.startsWith('--max-usd=')) {
|
|
1157
|
+
out.max_usd = _pyFloatArg(a.slice('--max-usd='.length), '--max-usd');
|
|
1017
1158
|
} else if (a === '--timeout') {
|
|
1018
1159
|
out.timeout = _pyInt(need(i, '--timeout'), '--timeout');
|
|
1019
1160
|
i += 1;
|
|
@@ -291,8 +291,37 @@ export function compare(records: Dict[], arm_t: string, arm_b: string): Dict {
|
|
|
291
291
|
let dis_t_sum = 0.0;
|
|
292
292
|
let dis_b_sum = 0.0;
|
|
293
293
|
let disn = 0.0;
|
|
294
|
+
// Delta #5 — attrition. A dropped pair is not missing-at-random: a budget
|
|
295
|
+
// cap or a timeout fires preferentially on the arm doing MORE work, so
|
|
296
|
+
// silently excluding those pairs biases the surviving sample toward the
|
|
297
|
+
// baseline. Report which side died and in which status bucket, so a reader
|
|
298
|
+
// can see whether the drops are balanced before trusting the estimate.
|
|
299
|
+
let pairs_seen = 0;
|
|
300
|
+
let dropped_treatment_only = 0;
|
|
301
|
+
let dropped_baseline_only = 0;
|
|
302
|
+
let dropped_both = 0;
|
|
303
|
+
const dropped_buckets: Record<string, number> = {};
|
|
294
304
|
for (const { rt, rb } of _pairs(records, arm_t, arm_b)) {
|
|
295
|
-
|
|
305
|
+
pairs_seen += 1;
|
|
306
|
+
const t_err = _pyTruthy(rt['errored']);
|
|
307
|
+
const b_err = _pyTruthy(rb['errored']);
|
|
308
|
+
if (t_err || b_err) {
|
|
309
|
+
if (t_err && b_err) {
|
|
310
|
+
dropped_both += 1;
|
|
311
|
+
} else if (t_err) {
|
|
312
|
+
dropped_treatment_only += 1;
|
|
313
|
+
} else {
|
|
314
|
+
dropped_baseline_only += 1;
|
|
315
|
+
}
|
|
316
|
+
for (const r of [t_err ? rt : null, b_err ? rb : null]) {
|
|
317
|
+
if (r === null) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const bucket = _strOr(_dictOr(r['metrics'])['status_bucket'], 'unknown');
|
|
321
|
+
dropped_buckets[bucket] = (dropped_buckets[bucket] ?? 0) + 1;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!t_err && !b_err) {
|
|
296
325
|
const t = _pyTruthy(rt['capability_pass']);
|
|
297
326
|
const bb = _pyTruthy(rb['capability_pass']);
|
|
298
327
|
capn += 1;
|
|
@@ -341,6 +370,18 @@ export function compare(records: Dict[], arm_t: string, arm_b: string): Dict {
|
|
|
341
370
|
rank_biserial: PF(wil.rank_biserial),
|
|
342
371
|
n_nonzero: wil.n,
|
|
343
372
|
},
|
|
373
|
+
attrition: {
|
|
374
|
+
pairs_seen,
|
|
375
|
+
pairs_analysed: capn,
|
|
376
|
+
pairs_dropped: pairs_seen - capn,
|
|
377
|
+
dropped_treatment_only,
|
|
378
|
+
dropped_baseline_only,
|
|
379
|
+
dropped_both,
|
|
380
|
+
// Positive = the treatment arm died more often than the baseline,
|
|
381
|
+
// i.e. the surviving sample is biased toward the baseline.
|
|
382
|
+
drop_asymmetry: dropped_treatment_only - dropped_baseline_only,
|
|
383
|
+
dropped_by_status_bucket: dropped_buckets,
|
|
384
|
+
},
|
|
344
385
|
};
|
|
345
386
|
}
|
|
346
387
|
|
|
@@ -509,7 +550,9 @@ export function to_markdown(analysis: Dict, payload: Dict): string {
|
|
|
509
550
|
);
|
|
510
551
|
L.push(
|
|
511
552
|
'> 4. **Paired design**, errored runs excluded; McNemar (capability) + ' +
|
|
512
|
-
'Wilcoxon signed-rank (discipline) + effect sizes.'
|
|
553
|
+
'Wilcoxon signed-rank (discipline) + effect sizes. Exclusion is NOT ' +
|
|
554
|
+
'assumed harmless — see each comparison’s Table 4 for how many pairs ' +
|
|
555
|
+
'were dropped and whether the drops favour one arm.',
|
|
513
556
|
);
|
|
514
557
|
L.push(
|
|
515
558
|
'> 5. **Not comparable to SWE-bench / GAIA / Fable scores** — a different ' +
|
|
@@ -597,6 +640,29 @@ export function to_markdown(analysis: Dict, payload: Dict): string {
|
|
|
597
640
|
L.push('|---|---|---|---|');
|
|
598
641
|
L.push(`| mean tokens | ${_thousands(tb)} | ${_thousands(tt)} | ${_thousandsSigned(tt - tb)} |`);
|
|
599
642
|
L.push('');
|
|
643
|
+
const at = _dictOr(cmp['attrition']);
|
|
644
|
+
L.push('### Table 4 — attrition (dropped pairs are not missing-at-random)');
|
|
645
|
+
L.push('');
|
|
646
|
+
L.push('| pairs seen | analysed | dropped | treatment-only | baseline-only | both | asymmetry |');
|
|
647
|
+
L.push('|---|---|---|---|---|---|---|');
|
|
648
|
+
L.push(
|
|
649
|
+
`| ${_pyStr(at['pairs_seen'])} | ${_pyStr(at['pairs_analysed'])} | ${_pyStr(at['pairs_dropped'])} ` +
|
|
650
|
+
`| ${_pyStr(at['dropped_treatment_only'])} | ${_pyStr(at['dropped_baseline_only'])} ` +
|
|
651
|
+
`| ${_pyStr(at['dropped_both'])} | ${_fmtSignedInt(at['drop_asymmetry'])} |`,
|
|
652
|
+
);
|
|
653
|
+
const dropBuckets = Object.entries(_dictOr(at['dropped_by_status_bucket']))
|
|
654
|
+
.map(([k, v]) => `${k}:${_pyStr(v)}`)
|
|
655
|
+
.join(', ');
|
|
656
|
+
L.push('');
|
|
657
|
+
L.push(`Dropped runs by status bucket: ${dropBuckets || '_none_'}.`);
|
|
658
|
+
L.push('');
|
|
659
|
+
L.push(
|
|
660
|
+
'> A positive asymmetry means the treatment arm errored out more often than ' +
|
|
661
|
+
'the baseline. Budget caps and timeouts fire preferentially on the arm doing ' +
|
|
662
|
+
'more work, so the surviving sample is then biased TOWARD the baseline and the ' +
|
|
663
|
+
'measured lift is a floor, not an estimate.',
|
|
664
|
+
);
|
|
665
|
+
L.push('');
|
|
600
666
|
}
|
|
601
667
|
L.push('## Status buckets (trajectory)');
|
|
602
668
|
L.push('');
|
|
@@ -655,6 +721,12 @@ function _fmtSigned3(v: unknown): string {
|
|
|
655
721
|
return body.startsWith('-') ? body : `+${body}`;
|
|
656
722
|
}
|
|
657
723
|
|
|
724
|
+
/** `f"{n:+d}"` over an int-ish JSON value (attrition asymmetry). */
|
|
725
|
+
function _fmtSignedInt(v: unknown): string {
|
|
726
|
+
const n = _pyIntTrunc(_pyFloat(v));
|
|
727
|
+
return n < 0 ? String(n) : `+${n}`;
|
|
728
|
+
}
|
|
729
|
+
|
|
658
730
|
/** `f"{n:,}"` over `int(value)`. */
|
|
659
731
|
function _thousands(n: number): string {
|
|
660
732
|
return _pyThousands(_pyIntTrunc(n));
|
|
@@ -35,6 +35,8 @@ import * as fs from 'node:fs';
|
|
|
35
35
|
import * as path from 'node:path';
|
|
36
36
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
37
37
|
|
|
38
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
39
|
+
|
|
38
40
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
39
41
|
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
40
42
|
const ADR_DIR = path.join(REPO_ROOT, 'docs', 'decisions');
|
|
@@ -149,6 +151,27 @@ export function check(dir: string = ADR_DIR): AdrFinding[] {
|
|
|
149
151
|
function main(argv: string[]): number {
|
|
150
152
|
const as_json = argv.includes('--json');
|
|
151
153
|
const findings = check();
|
|
154
|
+
const total = fs.existsSync(ADR_DIR)
|
|
155
|
+
? fs.readdirSync(ADR_DIR).filter((f) => /^ADR-.*\.md$/.test(f)).length
|
|
156
|
+
: 0;
|
|
157
|
+
|
|
158
|
+
// `check()` returns findings, never a count, so a moved `docs/decisions/`
|
|
159
|
+
// yields an empty list — reported as "0 ADR(s) · 0 error(s)" and green.
|
|
160
|
+
// Exit 1 is the violation code; 2 stays reserved for usage/env errors.
|
|
161
|
+
try {
|
|
162
|
+
assertScanned({
|
|
163
|
+
gate: 'check_adr_frontmatter',
|
|
164
|
+
scanned: total,
|
|
165
|
+
units: 'ADR file(s)',
|
|
166
|
+
roots: ['docs/decisions'],
|
|
167
|
+
});
|
|
168
|
+
} catch (exc) {
|
|
169
|
+
if (exc instanceof DeadScopeError) {
|
|
170
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
throw exc;
|
|
174
|
+
}
|
|
152
175
|
|
|
153
176
|
if (as_json) {
|
|
154
177
|
process.stdout.write(JSON.stringify({ findings }, null, 2) + '\n');
|
|
@@ -157,9 +180,6 @@ function main(argv: string[]): number {
|
|
|
157
180
|
|
|
158
181
|
const errors = findings.filter((f) => f.level === 'error');
|
|
159
182
|
const warns = findings.filter((f) => f.level === 'warn');
|
|
160
|
-
const total = fs.existsSync(ADR_DIR)
|
|
161
|
-
? fs.readdirSync(ADR_DIR).filter((f) => /^ADR-.*\.md$/.test(f)).length
|
|
162
|
-
: 0;
|
|
163
183
|
|
|
164
184
|
process.stdout.write(
|
|
165
185
|
`ADR frontmatter: ${total} ADR(s) · ${errors.length} error(s) · ` +
|
|
@@ -29,6 +29,8 @@ import process from 'node:process';
|
|
|
29
29
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
30
30
|
import YAML from 'yaml';
|
|
31
31
|
|
|
32
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
|
+
|
|
32
34
|
// src/scripts/check_always_budget.ts → parents[2] is the repo root
|
|
33
35
|
// (mirrors the Python module's Path(__file__).resolve().parents[2]).
|
|
34
36
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
@@ -552,6 +554,28 @@ function main(): number {
|
|
|
552
554
|
return 3;
|
|
553
555
|
}
|
|
554
556
|
|
|
557
|
+
// The scanned unit is every projected rule, not the `type: always` subset the
|
|
558
|
+
// budget is measured over: `isDir` above catches a MISSING projection, but a
|
|
559
|
+
// present-and-empty one (a sync that produced no rules) reaches here, and
|
|
560
|
+
// "no always-rules" cannot distinguish that from a tree whose rules are all
|
|
561
|
+
// `type: auto`. Exit 3 stays — this file's documented internal-error code,
|
|
562
|
+
// already used for the empty case, and the Taskfile fails on any non-zero, so
|
|
563
|
+
// this was never a silent green.
|
|
564
|
+
try {
|
|
565
|
+
assertScanned({
|
|
566
|
+
gate: 'check_always_budget',
|
|
567
|
+
scanned: allRules().length,
|
|
568
|
+
units: 'projected rule file(s)',
|
|
569
|
+
roots: [path.relative(REPO_ROOT, RULES_DIR)],
|
|
570
|
+
});
|
|
571
|
+
} catch (exc) {
|
|
572
|
+
if (exc instanceof DeadScopeError) {
|
|
573
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
574
|
+
return 3;
|
|
575
|
+
}
|
|
576
|
+
throw exc;
|
|
577
|
+
}
|
|
578
|
+
|
|
555
579
|
const rules = allwaysRules();
|
|
556
580
|
if (rules.length === 0) {
|
|
557
581
|
process.stderr.write(`❌ no always-rules found under ${RULES_DIR}\n`);
|
|
@@ -33,6 +33,7 @@ import * as fs from 'node:fs';
|
|
|
33
33
|
import * as path from 'node:path';
|
|
34
34
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
35
|
|
|
36
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
36
37
|
import {
|
|
37
38
|
apply_schema_defaults,
|
|
38
39
|
load_schema,
|
|
@@ -153,6 +154,16 @@ function _check(manifestPath: string): [number, string[]] {
|
|
|
153
154
|
const artefacts = Array.isArray(manifest['artefacts'])
|
|
154
155
|
? (manifest['artefacts'] as Json[])
|
|
155
156
|
: [];
|
|
157
|
+
// A readable manifest with an empty (or non-array) `artefacts` reports
|
|
158
|
+
// "0 artefact checksums verified" and exits green — the drift this gate
|
|
159
|
+
// exists to catch would be invisible in exactly the case where the
|
|
160
|
+
// generator produced nothing.
|
|
161
|
+
assertScanned({
|
|
162
|
+
gate: 'check_artefact_checksums',
|
|
163
|
+
scanned: artefacts.length,
|
|
164
|
+
units: 'manifest artefact(s)',
|
|
165
|
+
roots: [manifestPath],
|
|
166
|
+
});
|
|
156
167
|
for (const artRaw of artefacts) {
|
|
157
168
|
const art = (artRaw ?? {}) as JsonObject;
|
|
158
169
|
const rel = art['path'];
|
|
@@ -238,7 +249,19 @@ function parse_args(argv: readonly string[]): ParsedArgs {
|
|
|
238
249
|
function main(argv: readonly string[]): number {
|
|
239
250
|
const args = parse_args(argv);
|
|
240
251
|
|
|
241
|
-
|
|
252
|
+
let code: number;
|
|
253
|
+
let errors: string[];
|
|
254
|
+
try {
|
|
255
|
+
[code, errors] = _check(args.manifest);
|
|
256
|
+
} catch (exc) {
|
|
257
|
+
if (exc instanceof DeadScopeError) {
|
|
258
|
+
// 1 is this gate's only non-zero code, and it already covers the
|
|
259
|
+
// sibling could-not-run case (`manifest not found`).
|
|
260
|
+
process.stderr.write(`error: ${exc.message}\n`);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
throw exc;
|
|
264
|
+
}
|
|
242
265
|
if (code !== 0) {
|
|
243
266
|
for (const e of errors.slice(0, 20)) {
|
|
244
267
|
process.stderr.write(`error: ${e}\n`);
|
|
@@ -32,6 +32,7 @@ import * as fs from 'node:fs';
|
|
|
32
32
|
import * as path from 'node:path';
|
|
33
33
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
34
34
|
|
|
35
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
35
36
|
import { canonical_counts } from './check_command_count_messaging.js';
|
|
36
37
|
import { count, TARGETS } from './update_counts.js';
|
|
37
38
|
|
|
@@ -253,6 +254,25 @@ export function anchor_coverage_gaps(root: string = ROOT): CoverageGap[] {
|
|
|
253
254
|
|
|
254
255
|
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
255
256
|
const QUIET = argv.includes('--quiet');
|
|
257
|
+
// This gate walks no tree — it guards a hand-maintained surface list, and
|
|
258
|
+
// every missing entry is skipped with a warning so nothing hard-breaks on a
|
|
259
|
+
// doc rename. That tolerance is also the failure mode: a repo-wide docs
|
|
260
|
+
// move leaves every surface "skipped" and the gate reporting all counts in
|
|
261
|
+
// sync. Deleting these paths must fail, never quieten, the gate.
|
|
262
|
+
try {
|
|
263
|
+
assertWatchlistResolves({
|
|
264
|
+
gate: 'check_artefact_count_messaging',
|
|
265
|
+
candidates: [...SURFACES, ...STRUCTURED_SURFACES.map((s) => s.file)],
|
|
266
|
+
repoRoot: ROOT,
|
|
267
|
+
});
|
|
268
|
+
} catch (exc) {
|
|
269
|
+
if (exc instanceof DeadScopeError) {
|
|
270
|
+
// 1 is this gate's only non-zero code.
|
|
271
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
throw exc;
|
|
275
|
+
}
|
|
256
276
|
const expected: Record<string, number> = {};
|
|
257
277
|
for (const [kind] of KIND_PATTERNS) {
|
|
258
278
|
expected[kind] = canonical_for(kind);
|
|
@@ -24,6 +24,8 @@ import * as fs from 'node:fs';
|
|
|
24
24
|
import * as path from 'node:path';
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
26
|
|
|
27
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
28
|
+
|
|
27
29
|
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
28
30
|
|
|
29
31
|
const STALE_DAYS = 90;
|
|
@@ -33,15 +35,6 @@ const MIN_USEFUL_LINES = 5;
|
|
|
33
35
|
const _HERE = fileURLToPath(import.meta.url);
|
|
34
36
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
35
37
|
|
|
36
|
-
function _exists(p: string): boolean {
|
|
37
|
-
try {
|
|
38
|
-
fs.statSync(p);
|
|
39
|
-
return true;
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
38
|
function _emit(notes: readonly string[]): void {
|
|
46
39
|
if (notes.length === 0) {
|
|
47
40
|
if (!QUIET) {
|
|
@@ -60,12 +53,26 @@ function check(): number {
|
|
|
60
53
|
const target = path.join(REPO_ROOT, '.augmentignore');
|
|
61
54
|
const notes: string[] = [];
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
// The whole scan scope is this one file, so its absence IS the dead-scope
|
|
57
|
+
// case. It used to surface as a scaffolding hint on stdout, which reads the
|
|
58
|
+
// same whether the file was never written or the root moved. The exit code
|
|
59
|
+
// stays 0 because "always exits 0" is this file's pinned contract (advisory,
|
|
60
|
+
// not a gate) — there is no failure code to choose between.
|
|
61
|
+
try {
|
|
62
|
+
assertWatchlistResolves({
|
|
63
|
+
gate: 'check_augmentignore',
|
|
64
|
+
candidates: ['.augmentignore'],
|
|
65
|
+
repoRoot: REPO_ROOT,
|
|
66
|
+
});
|
|
67
|
+
} catch (exc) {
|
|
68
|
+
if (exc instanceof DeadScopeError) {
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
`⚠️ ${exc.message}\n` +
|
|
71
|
+
' Run `/optimize augmentignore` to scaffold it.\n',
|
|
72
|
+
);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
throw exc;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
const mtimeMs = fs.statSync(target).mtimeMs;
|
|
@@ -45,6 +45,8 @@ import * as fs from 'node:fs';
|
|
|
45
45
|
import * as path from 'node:path';
|
|
46
46
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
47
|
|
|
48
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
49
|
+
|
|
48
50
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
49
51
|
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
50
52
|
const BASELINE = path.join(REPO_ROOT, 'internal', 'reports', 'rule-backstop-debt.json');
|
|
@@ -107,6 +109,26 @@ function main(argv: string[]): number {
|
|
|
107
109
|
const as_json = argv.includes('--json');
|
|
108
110
|
const write = argv.includes('--write-baseline');
|
|
109
111
|
|
|
112
|
+
// This file owns no corpus — it ratchets five NAMED gates, so its scope is
|
|
113
|
+
// that list. A renamed or deleted gate script still spawns, fails, and lands
|
|
114
|
+
// in `unparsed`, whose message sends the reader to fix a regex that is not
|
|
115
|
+
// the problem. Asserted before `--write-baseline` too: a baseline anchored
|
|
116
|
+
// over phantom gates is a ratchet that can never rise. Exit 2 (usage / env
|
|
117
|
+
// error) rather than 1 — the ratchet could not run, no count rose.
|
|
118
|
+
try {
|
|
119
|
+
assertWatchlistResolves({
|
|
120
|
+
gate: 'check_backstop_debt',
|
|
121
|
+
candidates: GATES.map((g) => `src/scripts/${g.script}.ts`),
|
|
122
|
+
repoRoot: REPO_ROOT,
|
|
123
|
+
});
|
|
124
|
+
} catch (exc) {
|
|
125
|
+
if (exc instanceof DeadScopeError) {
|
|
126
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
127
|
+
return 2;
|
|
128
|
+
}
|
|
129
|
+
throw exc;
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
const measured = GATES.map((g) => measure(g));
|
|
111
133
|
|
|
112
134
|
if (as_json) {
|