@event4u/agent-config 14.11.0 → 14.12.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +156 -538
- package/README.md +1 -1
- package/dist/agent-src/contexts/execution/evidence-discipline.md +26 -0
- package/dist/agent-src/skills/code-refactoring/SKILL.md +45 -1
- package/dist/agent-src/skills/code-review/SKILL.md +65 -6
- package/dist/agent-src/skills/command-writing/SKILL.md +35 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -1
- package/dist/agent-src/skills/project-analysis-core/SKILL.md +23 -0
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +29 -12
- package/dist/agent-src/skills/skill-writing/references/procedure.md +24 -2
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +9 -0
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +8 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-L43NCYPB.js → chunk-JCW7ZLHK.js} +5 -0
- package/dist/cli-delegate/{chunk-236PNQZK.js → chunk-JJA2QEWA.js} +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- 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 +10 -10
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1081 -879
- package/dist/install/install.mjs +5 -0
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/server/schemas/settings.js +3 -0
- package/docs/contracts/settings-classes.md +24 -4
- package/docs/guidelines/agent-infra/downstream-changes-mechanics.md +91 -0
- package/docs/guidelines/code-clarity.md +21 -0
- package/docs/guidelines/redundancy-taxonomy.md +180 -0
- package/package.json +6 -2
- package/src/config/agent-settings.template.yml +16 -0
- package/src/config/budgets.yml +19 -0
- package/src/config/ci-local-parity.yml +15 -0
- package/src/config/discovery/prompt-patterns.yml +56 -0
- package/src/config/estate-count-budget.json +8 -3
- package/src/config/gate-coverage.yml +191 -6
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/pack-size-budget.json +39 -12
- package/src/config/preamble-payload-budget.json +97 -71
- package/src/config/routing-coverage-seed.json +29 -0
- package/src/config/suggestion-capture.json +28 -0
- package/src/config/surface-matrix.yml +21 -2
- package/src/config/web-launch-readiness.json +188 -0
- package/src/scripts/_lib/gate_baseline.ts +150 -1
- package/src/scripts/_lib/skill_estate.ts +115 -0
- package/src/scripts/ai_council/argument_exhaustion.ts +130 -0
- package/src/scripts/check_bundle_path_leakage.ts +162 -7
- package/src/scripts/check_command_examples.ts +557 -0
- package/src/scripts/check_estate_count.ts +122 -3
- package/src/scripts/check_preamble_payload_budget.ts +30 -3
- package/src/scripts/check_routing_coverage.ts +352 -0
- package/src/scripts/check_score_contract.ts +505 -0
- package/src/scripts/check_skill_admissions.ts +514 -0
- package/src/scripts/check_web_launch_readiness.ts +705 -0
- package/src/scripts/description_route_check.ts +494 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +29 -2
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/suggestion_capture_hook.ts +368 -0
- package/src/scripts/lint_budget_ownership.ts +54 -4
- package/src/scripts/lint_skill_trigger_corpus.ts +349 -0
- package/src/scripts/pack_install_smoke.ts +363 -0
- package/src/scripts/pr_conflict_census.ts +187 -0
- package/src/scripts/report_pack_delta.ts +204 -0
- package/src/scripts/select_analysis_mode.ts +112 -0
- package/src/scripts/sync_pr_branch.ts +40 -1
- package/src/server/schemas/settings.ts +5 -0
- package/dist/agent-src/skills/accessibility-auditor/evals/evals.json +0 -15
- package/dist/agent-src/skills/agent-security-review/evals/triggers.json +0 -52
- package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +0 -52
- package/dist/agent-src/skills/alerting-doctrine/evals/triggers.json +0 -17
- package/dist/agent-src/skills/analysis-skill-router/evals/evals.json +0 -13
- package/dist/agent-src/skills/brand/evals/triggers.json +0 -17
- package/dist/agent-src/skills/brand-asset-generation/evals/triggers.json +0 -17
- package/dist/agent-src/skills/brand-audit/evals/triggers.json +0 -17
- package/dist/agent-src/skills/brand-identity/evals/triggers.json +0 -17
- package/dist/agent-src/skills/brand-strategy/evals/triggers.json +0 -17
- package/dist/agent-src/skills/brand-to-tokens/evals/triggers.json +0 -17
- package/dist/agent-src/skills/bug-analyzer/evals/evals.json +0 -13
- package/dist/agent-src/skills/canvas-design/evals/triggers.json +0 -17
- package/dist/agent-src/skills/character-consistency/evals/evals.json +0 -16
- package/dist/agent-src/skills/code-intelligence/evals/triggers.json +0 -52
- package/dist/agent-src/skills/code-review/evals/evals.json +0 -100
- package/dist/agent-src/skills/command-routing/evals/evals.json +0 -14
- package/dist/agent-src/skills/complexity-first-planning/evals/triggers.json +0 -17
- package/dist/agent-src/skills/contract-review/evals/triggers.json +0 -17
- package/dist/agent-src/skills/conventional-commits-writing/evals/evals.json +0 -15
- package/dist/agent-src/skills/corpus-grounding/evals/triggers.json +0 -17
- package/dist/agent-src/skills/dashboard-design/evals/evals.json +0 -16
- package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +0 -29
- package/dist/agent-src/skills/dcf-modeling/evals/evals.json +0 -20
- package/dist/agent-src/skills/decision-record/evals/evals.json +0 -20
- package/dist/agent-src/skills/decision-record/evals/triggers.json +0 -87
- package/dist/agent-src/skills/design-intelligence/evals/evals.json +0 -14
- package/dist/agent-src/skills/design-intelligence/evals/triggers.json +0 -17
- package/dist/agent-src/skills/design-system-capture/evals/evals.json +0 -15
- package/dist/agent-src/skills/design-tokens/evals/triggers.json +0 -17
- package/dist/agent-src/skills/design-variations/evals/triggers.json +0 -17
- package/dist/agent-src/skills/developer-like-execution/evals/evals.json +0 -16
- package/dist/agent-src/skills/doc-coauthoring/evals/evals.json +0 -16
- package/dist/agent-src/skills/doc-coauthoring/evals/triggers.json +0 -17
- package/dist/agent-src/skills/docx-authoring/evals/triggers.json +0 -52
- package/dist/agent-src/skills/dpa-review/evals/triggers.json +0 -17
- package/dist/agent-src/skills/editorial-calendar/evals/domain-truth.json +0 -16
- package/dist/agent-src/skills/editorial-calendar/evals/evals.json +0 -16
- package/dist/agent-src/skills/eloquent/evals/triggers.json +0 -17
- package/dist/agent-src/skills/estimate-ticket/evals/evals.json +0 -16
- package/dist/agent-src/skills/estimate-ticket/evals/output-schema.yml +0 -20
- package/dist/agent-src/skills/estimate-ticket/evals/triggers.json +0 -19
- package/dist/agent-src/skills/evaluate-llm-feature/evals/triggers.json +0 -52
- package/dist/agent-src/skills/existing-ui-audit/evals/triggers.json +0 -47
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +0 -27
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +0 -17
- package/dist/agent-src/skills/fe-design/evals/triggers.json +0 -67
- package/dist/agent-src/skills/forecasting/evals/domain-truth.json +0 -29
- package/dist/agent-src/skills/forecasting/evals/evals.json +0 -20
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +0 -17
- package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +0 -52
- package/dist/agent-src/skills/fundraising-narrative/evals/domain-truth.json +0 -16
- package/dist/agent-src/skills/fundraising-narrative/evals/evals.json +0 -16
- package/dist/agent-src/skills/gated-reach/evals/triggers.json +0 -72
- package/dist/agent-src/skills/github-ci/evals/evals.json +0 -25
- package/dist/agent-src/skills/history-design/evals/triggers.json +0 -17
- package/dist/agent-src/skills/html-deck/evals/triggers.json +0 -17
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +0 -57
- package/dist/agent-src/skills/humanizer/evals/triggers.json +0 -17
- package/dist/agent-src/skills/iconography/evals/triggers.json +0 -17
- package/dist/agent-src/skills/image-analyser/evals/triggers.json +0 -17
- package/dist/agent-src/skills/image-creator/evals/triggers.json +0 -17
- package/dist/agent-src/skills/image-editing/evals/triggers.json +0 -17
- package/dist/agent-src/skills/image-generation/evals/triggers.json +0 -17
- package/dist/agent-src/skills/image-provider-routing/evals/triggers.json +0 -17
- package/dist/agent-src/skills/incident-commander/evals/domain-truth.json +0 -16
- package/dist/agent-src/skills/incident-commander/evals/evals.json +0 -20
- package/dist/agent-src/skills/js-library-packaging/evals/triggers.json +0 -17
- package/dist/agent-src/skills/judge-artifact-completeness/evals/triggers.json +0 -17
- package/dist/agent-src/skills/judge-injection-defense/evals/triggers.json +0 -17
- package/dist/agent-src/skills/judge-synthesis/evals/triggers.json +0 -17
- package/dist/agent-src/skills/launch-readiness/evals/evals.json +0 -21
- package/dist/agent-src/skills/learning-tutor/evals/triggers.json +0 -52
- package/dist/agent-src/skills/legal-intake-triage/evals/triggers.json +0 -17
- package/dist/agent-src/skills/legal-practice-profile/evals/triggers.json +0 -17
- package/dist/agent-src/skills/license-compliance-audit/evals/triggers.json +0 -52
- package/dist/agent-src/skills/license-compliance-borrow-check/evals/triggers.json +0 -52
- package/dist/agent-src/skills/license-compliance-credits/evals/triggers.json +0 -52
- package/dist/agent-src/skills/llm-provider-knowledge/evals/triggers.json +0 -20
- package/dist/agent-src/skills/logging-monitoring/evals/evals.json +0 -16
- package/dist/agent-src/skills/logo-generation/evals/triggers.json +0 -17
- package/dist/agent-src/skills/messaging-architecture/evals/domain-truth.json +0 -16
- package/dist/agent-src/skills/messaging-architecture/evals/evals.json +0 -16
- package/dist/agent-src/skills/monorepo-workspace/evals/triggers.json +0 -17
- package/dist/agent-src/skills/nda-triage/evals/triggers.json +0 -17
- package/dist/agent-src/skills/operational-readiness/evals/triggers.json +0 -17
- package/dist/agent-src/skills/pdf-tools/evals/triggers.json +0 -52
- package/dist/agent-src/skills/perf-feedback-craft/evals/evals.json +0 -16
- package/dist/agent-src/skills/persona-improvement/evals/triggers.json +0 -52
- package/dist/agent-src/skills/php-coder/evals/triggers.json +0 -17
- package/dist/agent-src/skills/prediction-pool-optimizer/evals/triggers.json +0 -21
- package/dist/agent-src/skills/prompt-engineering-image/evals/triggers.json +0 -17
- package/dist/agent-src/skills/prompt-engineering-patterns/evals/evals.json +0 -16
- package/dist/agent-src/skills/prompt-validator/evals/triggers.json +0 -17
- package/dist/agent-src/skills/quality-tools/evals/evals.json +0 -15
- package/dist/agent-src/skills/reasoning-orchestrator/evals/triggers.json +0 -17
- package/dist/agent-src/skills/refine-prompt/evals/evals.json +0 -16
- package/dist/agent-src/skills/refine-ticket/evals/evals.json +0 -16
- package/dist/agent-src/skills/refine-ticket/evals/output-schema.yml +0 -16
- package/dist/agent-src/skills/refine-ticket/evals/triggers.json +0 -17
- package/dist/agent-src/skills/release-comms/evals/evals.json +0 -16
- package/dist/agent-src/skills/rice-prioritization/evals/evals.json +0 -20
- package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +0 -29
- package/dist/agent-src/skills/runway-cognition/evals/evals.json +0 -20
- package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +0 -29
- package/dist/agent-src/skills/scenario-modeling/evals/evals.json +0 -20
- package/dist/agent-src/skills/schema-review/evals/triggers.json +0 -17
- package/dist/agent-src/skills/screenshot-hygiene/evals/triggers.json +0 -52
- package/dist/agent-src/skills/security-maturity-assessment/evals/triggers.json +0 -52
- package/dist/agent-src/skills/server-hardening/evals/triggers.json +0 -17
- package/dist/agent-src/skills/skill-writing/evals/triggers.json +0 -17
- package/dist/agent-src/skills/spreadsheet-authoring/evals/triggers.json +0 -47
- package/dist/agent-src/skills/storybook-workshop/evals/triggers.json +0 -17
- package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +0 -60
- package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +0 -52
- package/dist/agent-src/skills/systematic-debugging/evals/evals.json +0 -20
- package/dist/agent-src/skills/test-case-discovery/evals/triggers.json +0 -52
- package/dist/agent-src/skills/test-driven-development/evals/evals.json +0 -16
- package/dist/agent-src/skills/threat-modeling/evals/evals.json +0 -21
- package/dist/agent-src/skills/typography-system/evals/evals.json +0 -14
- package/dist/agent-src/skills/typography-system/evals/triggers.json +0 -17
- package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +0 -40
- package/dist/agent-src/skills/unit-economics-modeling/evals/evals.json +0 -24
- package/dist/agent-src/skills/verify-completion-evidence/evals/evals.json +0 -16
- package/dist/agent-src/skills/verify-repair-loop/evals/output-schema.yml +0 -20
- package/dist/agent-src/skills/verify-repair-loop/evals/triggers.json +0 -17
- package/dist/agent-src/skills/vision-articulation/evals/evals.json +0 -16
- package/dist/agent-src/skills/voice-and-tone-design/evals/evals.json +0 -16
- package/dist/agent-src/skills/wireframe/evals/triggers.json +0 -17
- package/dist/agent-src/skills/workspace-link/evals/triggers.json +0 -17
- package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +0 -38
- package/dist/agent-src/skills/worktree-lifecycle/evals/triggers.json +0 -52
- package/dist/cli/agent-config.js.map +0 -1
- package/dist/cli/bash/runBash.js.map +0 -1
- package/dist/cli/commands/commands.js.map +0 -1
- package/dist/cli/commands/config.js.map +0 -1
- package/dist/cli/commands/doctorShell.js.map +0 -1
- package/dist/cli/commands/mcpServer.js.map +0 -1
- package/dist/cli/commands/packs.js.map +0 -1
- package/dist/cli/commands/profiles.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.js.map +0 -1
- package/dist/cli/commands/rtkDetect.js.map +0 -1
- package/dist/cli/commands/settings.js.map +0 -1
- package/dist/cli/commands/uiAudit.js.map +0 -1
- package/dist/cli/commands/uiRender.js.map +0 -1
- package/dist/cli/commands/uiServe.js.map +0 -1
- package/dist/cli/commands/versions.js.map +0 -1
- package/dist/cli/commands/workspaces.js.map +0 -1
- package/dist/cli/configRoot.js.map +0 -1
- package/dist/cli/discovery/loadManifest.js.map +0 -1
- package/dist/cli/firstRunNotice.js.map +0 -1
- package/dist/cli/help.js.map +0 -1
- package/dist/cli/initRouting.js.map +0 -1
- package/dist/cli/log/logger.js.map +0 -1
- package/dist/cli/main.js.map +0 -1
- package/dist/cli/mcp/content.js.map +0 -1
- package/dist/cli/mcp/dispatch.js.map +0 -1
- package/dist/cli/mcp/stdio.js.map +0 -1
- package/dist/cli/paths.js.map +0 -1
- package/dist/cli/python/knowledge_ingest.js.map +0 -1
- package/dist/cli/python/workspace_analytics.js.map +0 -1
- package/dist/cli/python/workspace_crypto.js.map +0 -1
- package/dist/cli/python/workspace_documents.js.map +0 -1
- package/dist/cli/python/workspace_drive.js.map +0 -1
- package/dist/cli/python/workspace_drive_health.js.map +0 -1
- package/dist/cli/python/workspace_explain.js.map +0 -1
- package/dist/cli/python/workspace_hosts.js.map +0 -1
- package/dist/cli/python/workspace_inbox.js.map +0 -1
- package/dist/cli/python/workspace_render.js.map +0 -1
- package/dist/cli/python/workspace_roles.js.map +0 -1
- package/dist/cli/python/workspace_secrets.js.map +0 -1
- package/dist/cli/python/workspace_sessions.js.map +0 -1
- package/dist/cli/python/workspace_skills.js.map +0 -1
- package/dist/cli/registry.js.map +0 -1
- package/dist/install/agentSwitchDetection.js.map +0 -1
- package/dist/install/agentSwitchProfile.js.map +0 -1
- package/dist/install/atomic.js.map +0 -1
- package/dist/install/claudePathsPlan.js.map +0 -1
- package/dist/install/conflict.js.map +0 -1
- package/dist/install/detect.js.map +0 -1
- package/dist/install/emit_host_rules_cli.js.map +0 -1
- package/dist/install/globalRuleLayers.js.map +0 -1
- package/dist/install/hostLayerFingerprint.js.map +0 -1
- package/dist/install/partitionEligibility.js.map +0 -1
- package/dist/install/paths.js.map +0 -1
- package/dist/install/plan.js.map +0 -1
- package/dist/install/preflight.js.map +0 -1
- package/dist/install/rtkDetection.js.map +0 -1
- package/dist/install/ruleInScope.js.map +0 -1
- package/dist/install/ruleLayerPartition.js.map +0 -1
- package/dist/install/rule_scope.js.map +0 -1
- package/dist/install/rule_scope_cli.js.map +0 -1
- package/dist/install/selectedTools.js.map +0 -1
- package/dist/install/toolDetection.js.map +0 -1
- package/dist/install/txlog.js.map +0 -1
- package/dist/install/types.js.map +0 -1
- package/dist/install/wizard-plan.js.map +0 -1
- package/dist/install/wizardDismissals.js.map +0 -1
- package/dist/scripts/_lib/agent_settings.js.map +0 -1
- package/dist/scripts/_lib/agent_src.js.map +0 -1
- package/dist/scripts/_lib/claude_builtin_names.js.map +0 -1
- package/dist/scripts/_lib/claude_plugin.js.map +0 -1
- package/dist/scripts/_lib/claude_settings_hooks.js.map +0 -1
- package/dist/scripts/_lib/compile_time_toggles.js.map +0 -1
- package/dist/scripts/_lib/environment_detector.js.map +0 -1
- package/dist/scripts/_lib/host_launch.js.map +0 -1
- package/dist/scripts/_lib/install_layout.js.map +0 -1
- package/dist/scripts/_lib/installed_lock.js.map +0 -1
- package/dist/scripts/_lib/lean_projection_mode.js.map +0 -1
- package/dist/scripts/_lib/model_tier.js.map +0 -1
- package/dist/scripts/_lib/prune_empty_dirs.js.map +0 -1
- package/dist/scripts/_lib/scoped_projection.js.map +0 -1
- package/dist/scripts/_lib/script_output.js.map +0 -1
- package/dist/scripts/_lib/spawn_env.js.map +0 -1
- package/dist/scripts/_lib/surface_tiers.js.map +0 -1
- package/dist/scripts/_lib/token_count.js.map +0 -1
- package/dist/scripts/_lib/trigger_eval_floors.js.map +0 -1
- package/dist/scripts/_lib/ui_surface.js.map +0 -1
- package/dist/scripts/_lib/user_global_paths.js.map +0 -1
- package/dist/scripts/ai_council/modes.js.map +0 -1
- package/dist/scripts/ai_council/transport_resolver.js.map +0 -1
- package/dist/scripts/condense.js.map +0 -1
- package/dist/scripts/project_thin_rules.js.map +0 -1
- package/dist/server/app.js.map +0 -1
- package/dist/server/io/atomicMultiWrite.js.map +0 -1
- package/dist/server/io/atomicWrite.js.map +0 -1
- package/dist/server/io/sharedWriteCheck.js.map +0 -1
- package/dist/server/io/substituteTemplate.js.map +0 -1
- package/dist/server/io/yamlIO.js.map +0 -1
- package/dist/server/nicknameResolver.js.map +0 -1
- package/dist/server/port.js.map +0 -1
- package/dist/server/routes/discovery.js.map +0 -1
- package/dist/server/routes/install.js.map +0 -1
- package/dist/server/routes/ping.js.map +0 -1
- package/dist/server/routes/schema.js.map +0 -1
- package/dist/server/routes/settings.js.map +0 -1
- package/dist/server/routes/settingsChanges.js.map +0 -1
- package/dist/server/routes/userMd.js.map +0 -1
- package/dist/server/routes/wizard.js.map +0 -1
- package/dist/server/routes/workspace.js.map +0 -1
- package/dist/server/schemas/settings.js.map +0 -1
- package/dist/server/serverInfo.js.map +0 -1
- package/dist/server/token.js.map +0 -1
- package/dist/server/writeRoot.js.map +0 -1
- package/dist/shared/capabilities.js.map +0 -1
- package/dist/shared/interactiveContext.js.map +0 -1
- package/dist/shared/nicknamePrefill.js.map +0 -1
- package/dist/shared/settingsAsks.js.map +0 -1
- package/dist/shared/settingsCarveOut.js.map +0 -1
- package/dist/shared/settingsClasses.js.map +0 -1
- package/dist/shared/settingsConsent.js.map +0 -1
- package/dist/shared/settingsSurface.js.map +0 -1
- package/dist/shared/skillRanking.js.map +0 -1
- package/dist/shared/userMd/formAdapter.js.map +0 -1
- package/dist/shared/userMd/schema.js.map +0 -1
- package/dist/shared/userMd/utils.js.map +0 -1
- package/dist/ui/assets/index-Cxgy63kz.js.map +0 -1
- package/src/scripts/_lib/adr_frontmatter.test.ts +0 -365
- package/src/scripts/_lib/adversarial_council_gate.test.ts +0 -112
- package/src/scripts/_lib/adversarial_reconcile.test.ts +0 -197
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +0 -150
- package/src/scripts/_lib/secret_detector.test.ts +0 -108
- package/src/scripts/_lib/secret_detector_adversarial.test.ts +0 -116
- package/src/scripts/_lib/ui_authority.test.ts +0 -251
- package/src/scripts/_lib/untrusted_content.test.ts +0 -175
- package/src/scripts/check_secret_leak.test.ts +0 -90
- package/src/scripts/design_slop_rules.test.ts +0 -246
- package/src/scripts/self_review_gate.test.ts +0 -296
- package/src/shared/capabilities.test.ts +0 -43
|
@@ -152,6 +152,7 @@ import { reportScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
|
152
152
|
import { GateLedger } from './_lib/gate_ledger.js';
|
|
153
153
|
import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
|
|
154
154
|
import { materialiseSubtree } from './_lib/base_tree.js';
|
|
155
|
+
import { SKILLS_POSIX, measureSkillEstate } from './_lib/skill_estate.js';
|
|
155
156
|
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
156
157
|
|
|
157
158
|
const GATE = 'check_estate_count';
|
|
@@ -171,11 +172,35 @@ export interface EstateCounts {
|
|
|
171
172
|
active_roadmaps: number;
|
|
172
173
|
later_roadmaps: number;
|
|
173
174
|
open_blockers: number;
|
|
175
|
+
/**
|
|
176
|
+
* Maintained `SKILL.md` files — `lifecycle: deprecated` excluded.
|
|
177
|
+
*
|
|
178
|
+
* A FOURTH corpus on the same machinery, added 2026-08-24 on AI council 2/2
|
|
179
|
+
* over the alternative of a second budget file and a second gate: the
|
|
180
|
+
* floor-from-the-base-ref machinery is the expensive part and should not be
|
|
181
|
+
* written twice, and a second ratchet is a second place to forget.
|
|
182
|
+
*/
|
|
183
|
+
skill_count: number;
|
|
184
|
+
/**
|
|
185
|
+
* Exact-BPE tokens across those skills' `description:` fields.
|
|
186
|
+
*
|
|
187
|
+
* The second DIMENSION, not a companion figure. A count ratchet alone is
|
|
188
|
+
* gameable by merging four large skills into one file: the count falls, the
|
|
189
|
+
* payload a host must carry does not. `0` here means UNRESOLVED, never
|
|
190
|
+
* "no tokens" — see `tokensExact`.
|
|
191
|
+
*/
|
|
192
|
+
skill_description_tokens: number;
|
|
174
193
|
}
|
|
175
194
|
|
|
176
195
|
export type MetricName = keyof EstateCounts;
|
|
177
196
|
|
|
178
|
-
export const METRICS: readonly MetricName[] = [
|
|
197
|
+
export const METRICS: readonly MetricName[] = [
|
|
198
|
+
'active_roadmaps',
|
|
199
|
+
'later_roadmaps',
|
|
200
|
+
'open_blockers',
|
|
201
|
+
'skill_count',
|
|
202
|
+
'skill_description_tokens',
|
|
203
|
+
];
|
|
179
204
|
|
|
180
205
|
export interface EstateBudget {
|
|
181
206
|
/**
|
|
@@ -247,6 +272,16 @@ export interface EstateVerdict {
|
|
|
247
272
|
authorisedGrowth: GrowthFinding[];
|
|
248
273
|
/** The claims found in this change's diff, with their reasons. */
|
|
249
274
|
claims: GrowthClaim[];
|
|
275
|
+
/**
|
|
276
|
+
* Metrics dropped from the compare, each with its reason.
|
|
277
|
+
*
|
|
278
|
+
* Never empty-and-silent: a skill metric drops when the base ref carries no
|
|
279
|
+
* `src/skills` (an old tag, a shallow clone) or when the tokeniser resolved
|
|
280
|
+
* on one side and not the other. Comparing an exact reading against a proxy
|
|
281
|
+
* one would move the number by more than the growth this gate exists to
|
|
282
|
+
* catch, so the metric is stated unavailable rather than guessed.
|
|
283
|
+
*/
|
|
284
|
+
skippedMetrics: { metric: MetricName; reason: string }[];
|
|
250
285
|
/** `null` when the diff half could not run; the reason is printed either way. */
|
|
251
286
|
offsets: OffsetLedger | null;
|
|
252
287
|
offsetSkipReason: string | null;
|
|
@@ -342,13 +377,26 @@ export function countEstate(repoRoot: string): EstateCounts {
|
|
|
342
377
|
const stats = collect(roadmapRoot);
|
|
343
378
|
const openOf = (rows: readonly { open_blockers: readonly unknown[] }[]): number =>
|
|
344
379
|
rows.reduce((n, r) => n + r.open_blockers.length, 0);
|
|
380
|
+
const skills = measureSkillEstate(repoRoot);
|
|
345
381
|
return {
|
|
346
382
|
active_roadmaps: stats.length,
|
|
347
383
|
later_roadmaps: countIn(roadmapRoot, 'later'),
|
|
348
384
|
open_blockers: openOf(stats) + openOf(laterRoadmaps(roadmapRoot)),
|
|
385
|
+
skill_count: skills.skill_count,
|
|
386
|
+
// 0 stands for unresolved. `skillTokensExact` below is what the caller
|
|
387
|
+
// reads to tell that apart from a genuinely empty corpus, and the
|
|
388
|
+
// verdict skips the metric rather than comparing an exact reading
|
|
389
|
+
// against a proxy one — they differ by more than the growth a ratchet
|
|
390
|
+
// is trying to catch.
|
|
391
|
+
skill_description_tokens: skills.skill_description_tokens ?? 0,
|
|
349
392
|
};
|
|
350
393
|
}
|
|
351
394
|
|
|
395
|
+
/** Did the tokeniser resolve under `root`? A ratchet must not mix the two modes. */
|
|
396
|
+
export function skillTokensExact(root: string): boolean {
|
|
397
|
+
return measureSkillEstate(root).skill_description_tokens !== null;
|
|
398
|
+
}
|
|
399
|
+
|
|
352
400
|
/** `agents/roadmaps/<name>.md` — the active top level, never a subdirectory. */
|
|
353
401
|
function isActiveTopLevel(rel: string): boolean {
|
|
354
402
|
const norm = rel.split(path.sep).join('/');
|
|
@@ -506,19 +554,57 @@ export function evaluate(
|
|
|
506
554
|
// property, and unlike a number nobody has to remember to update it.
|
|
507
555
|
let floor: EstateCounts | null = null;
|
|
508
556
|
let floorSkipReason: string | null = null;
|
|
557
|
+
/** Set when the base ref carries no `src/skills` — the skill metrics drop. */
|
|
558
|
+
let skillFloorSkipReason: string | null = null;
|
|
559
|
+
/** Set when either side's tokeniser did not resolve — that ONE metric drops. */
|
|
560
|
+
let skillTokenSkipReason: string | null = null;
|
|
561
|
+
if (!skillTokensExact(repoRoot)) {
|
|
562
|
+
skillTokenSkipReason =
|
|
563
|
+
'the tokeniser did not resolve at HEAD; an exact reading may not be compared ' +
|
|
564
|
+
'against a proxy one';
|
|
565
|
+
}
|
|
509
566
|
if (baseRef === null || baseRef === undefined) {
|
|
510
567
|
floorSkipReason =
|
|
511
568
|
'no base ref resolved (no origin/main, no merge-commit parent) — the floor cannot be measured';
|
|
512
569
|
} else {
|
|
513
570
|
const base = materialiseSubtree(repoRoot, baseRef, ROADMAPS_POSIX);
|
|
571
|
+
// TWO subtrees, because the estate now spans two corpora and the floor
|
|
572
|
+
// must be measured on the base ref's own tree for BOTH. Materialised
|
|
573
|
+
// separately rather than widening the first call: `materialiseSubtree`
|
|
574
|
+
// takes one prefix, and the roadmap tree must stay readable even when
|
|
575
|
+
// the skills read fails (a `src/skills`-less base ref is a real state —
|
|
576
|
+
// an old tag, a shallow clone), in which case the skill metrics skip and
|
|
577
|
+
// the roadmap metrics still ratchet.
|
|
578
|
+
const baseSkills = materialiseSubtree(repoRoot, baseRef, SKILLS_POSIX);
|
|
514
579
|
try {
|
|
515
580
|
if (base.error !== null || base.files === 0) {
|
|
516
581
|
floorSkipReason = `could not read ${ROADMAPS_POSIX} at ${baseRef} — ${base.error ?? 'no files'}`;
|
|
517
582
|
} else {
|
|
518
|
-
|
|
583
|
+
const roadmapFloor = countEstate(base.root);
|
|
584
|
+
if (baseSkills.error !== null || baseSkills.files === 0) {
|
|
585
|
+
// Roadmap floor stands; the skill floor is stated as
|
|
586
|
+
// unavailable and its metrics are dropped from the compare.
|
|
587
|
+
floor = roadmapFloor;
|
|
588
|
+
skillFloorSkipReason =
|
|
589
|
+
`could not read ${SKILLS_POSIX} at ${baseRef} — ` +
|
|
590
|
+
`${baseSkills.error ?? 'no files'}`;
|
|
591
|
+
} else {
|
|
592
|
+
const s = measureSkillEstate(baseSkills.root);
|
|
593
|
+
floor = {
|
|
594
|
+
...roadmapFloor,
|
|
595
|
+
skill_count: s.skill_count,
|
|
596
|
+
skill_description_tokens: s.skill_description_tokens ?? 0,
|
|
597
|
+
};
|
|
598
|
+
if (s.skill_description_tokens === null) {
|
|
599
|
+
skillTokenSkipReason =
|
|
600
|
+
`the tokeniser did not resolve at ${baseRef}; an exact reading may ` +
|
|
601
|
+
'not be compared against a proxy one';
|
|
602
|
+
}
|
|
603
|
+
}
|
|
519
604
|
}
|
|
520
605
|
} finally {
|
|
521
606
|
fs.rmSync(base.root, { recursive: true, force: true });
|
|
607
|
+
fs.rmSync(baseSkills.root, { recursive: true, force: true });
|
|
522
608
|
}
|
|
523
609
|
}
|
|
524
610
|
|
|
@@ -571,11 +657,32 @@ export function evaluate(
|
|
|
571
657
|
active_roadmaps: offsets?.exempt.length ?? 0,
|
|
572
658
|
later_roadmaps: offsets?.parked.length ?? 0,
|
|
573
659
|
open_blockers: 0,
|
|
660
|
+
// The skill corpus gets NO allowance, and that is the whole point of the
|
|
661
|
+
// metric: the defect it addresses is a corpus that grew +8 in one release
|
|
662
|
+
// with nothing objecting. An addition takes the `estate_growth_exempt`
|
|
663
|
+
// claim path — a recorded reason in the diff — or it fails.
|
|
664
|
+
skill_count: 0,
|
|
665
|
+
skill_description_tokens: 0,
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
/** Metrics dropped from the compare, with the reason, never silently. */
|
|
669
|
+
const skippedMetrics: { metric: MetricName; reason: string }[] = [];
|
|
670
|
+
const skipMetric = (metric: MetricName): boolean => {
|
|
671
|
+
if (skillFloorSkipReason !== null && (metric === 'skill_count' || metric === 'skill_description_tokens')) {
|
|
672
|
+
skippedMetrics.push({ metric, reason: skillFloorSkipReason });
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
if (skillTokenSkipReason !== null && metric === 'skill_description_tokens') {
|
|
676
|
+
skippedMetrics.push({ metric, reason: skillTokenSkipReason });
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
return false;
|
|
574
680
|
};
|
|
575
681
|
|
|
576
682
|
const over: GrowthFinding[] = [];
|
|
577
683
|
if (floor !== null) {
|
|
578
684
|
for (const metric of METRICS) {
|
|
685
|
+
if (skipMetric(metric)) continue;
|
|
579
686
|
const live = counts[metric];
|
|
580
687
|
const allowed = floor[metric] + allowance[metric];
|
|
581
688
|
if (live > allowed) {
|
|
@@ -596,6 +703,7 @@ export function evaluate(
|
|
|
596
703
|
growth,
|
|
597
704
|
authorisedGrowth,
|
|
598
705
|
claims,
|
|
706
|
+
skippedMetrics,
|
|
599
707
|
offsets,
|
|
600
708
|
offsetSkipReason,
|
|
601
709
|
unpaid,
|
|
@@ -1010,8 +1118,12 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
1010
1118
|
|
|
1011
1119
|
for (const g of verdict.growth) {
|
|
1012
1120
|
const allow = g.allowance > 0 ? ` (+${String(g.allowance)} allowed here)` : '';
|
|
1121
|
+
// Two corpora share this gate, so the line names which one grew. It used
|
|
1122
|
+
// to say "the roadmap estate" unconditionally, which was already the
|
|
1123
|
+
// wrong noun for `skill_count` the moment that metric landed.
|
|
1124
|
+
const corpus = g.metric.startsWith('skill_') ? 'the skill estate' : 'the roadmap estate';
|
|
1013
1125
|
process.stderr.write(
|
|
1014
|
-
`❌
|
|
1126
|
+
`❌ ${corpus} grew: ${g.metric} ${String(g.floor)} → ${String(g.live)}${allow}.\n` +
|
|
1015
1127
|
` The floor is the measurement at ${verdict.floorRef ?? 'the base ref'}, not a number in a\n` +
|
|
1016
1128
|
' config — so there is no number to edit and nothing to walk. The estate does not\n' +
|
|
1017
1129
|
' walk down by itself, which is the defect this ratchet exists for. Either close,\n' +
|
|
@@ -1022,6 +1134,13 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
1022
1134
|
);
|
|
1023
1135
|
}
|
|
1024
1136
|
|
|
1137
|
+
// A dropped metric is REPORTED, never silent: a reader must be able to tell
|
|
1138
|
+
// "within its ratchet" from "within the ratchet of the metrics that could be
|
|
1139
|
+
// measured". This is the same distinction the floor-skip reason above keeps.
|
|
1140
|
+
for (const s of verdict.skippedMetrics) {
|
|
1141
|
+
process.stdout.write(` ⏭️ ${s.metric} not compared — ${s.reason}\n`);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1025
1144
|
if (verdict.unpaid > 0 && verdict.offsets !== null) {
|
|
1026
1145
|
process.stderr.write(
|
|
1027
1146
|
`❌ ${String(verdict.unpaid)} new active roadmap(s) with no offset in the same change.\n` +
|
|
@@ -91,19 +91,46 @@ export function measureDeterministicPayload(
|
|
|
91
91
|
];
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* A caller-supplied ceiling, and the one direction it may go.
|
|
96
|
+
*
|
|
97
|
+
* road-to-standing-payload-truth 1.1 arms this gate in CI behind a GRACE ceiling
|
|
98
|
+
* equal to the 2026-08-24 measurement, because HEAD is 28.4 % over the design
|
|
99
|
+
* ceiling and blocking at the design number would fail every pull request from
|
|
100
|
+
* the moment the workflow lands — nobody can shed 30,566 tokens inside the PR
|
|
101
|
+
* that arms the gate.
|
|
102
|
+
*
|
|
103
|
+
* **An override may only be LOOSER than the design ceiling, and it is refused if
|
|
104
|
+
* it is tighter.** That sounds backwards for one line and is the whole point: a
|
|
105
|
+
* tighter override would let a caller silently *lower* the bar this file owns,
|
|
106
|
+
* which is the config-weakening shape in reverse — the design ceiling stays the
|
|
107
|
+
* authority, and the override is a dated, expiring concession recorded in
|
|
108
|
+
* `ci_delivery`. Tightening happens by lowering `baseline_tokens`, in the file,
|
|
109
|
+
* with a reason, where a reviewer sees it.
|
|
110
|
+
*/
|
|
111
|
+
export function evaluate(repoRoot?: string, budgetFile?: string, overrideCeiling?: number): BudgetVerdict {
|
|
95
112
|
const budget = readBudget(budgetFile);
|
|
96
113
|
const buckets = measureDeterministicPayload(repoRoot);
|
|
97
114
|
const measured = buckets.reduce((sum, b) => sum + b.tokens, 0);
|
|
98
|
-
const
|
|
115
|
+
const design = Math.round(budget.baseline_tokens * (1 + budget.headroom_pct / 100));
|
|
116
|
+
const ceiling =
|
|
117
|
+
overrideCeiling !== undefined && Number.isFinite(overrideCeiling) && overrideCeiling > design
|
|
118
|
+
? overrideCeiling
|
|
119
|
+
: design;
|
|
99
120
|
return { measured, baseline: budget.baseline_tokens, ceiling, withinBudget: measured <= ceiling, buckets };
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
103
124
|
const json = argv.includes('--format=json') || argv.includes('--json');
|
|
125
|
+
// `--ceiling <n>`: the grace ceiling the CI step reads out of
|
|
126
|
+
// `ci_delivery.grace_ceiling`. Read from the budget file there, never written
|
|
127
|
+
// in the workflow, so the number has exactly one home. A non-numeric or
|
|
128
|
+
// tighter-than-design value is IGNORED rather than honoured — see `evaluate`.
|
|
129
|
+
const ci = argv.indexOf('--ceiling');
|
|
130
|
+
const override = ci !== -1 && argv[ci + 1] !== undefined ? Number(argv[ci + 1]) : undefined;
|
|
104
131
|
let verdict: BudgetVerdict;
|
|
105
132
|
try {
|
|
106
|
-
verdict = evaluate();
|
|
133
|
+
verdict = evaluate(undefined, undefined, override);
|
|
107
134
|
} catch (err) {
|
|
108
135
|
process.stderr.write(`❌ preamble-payload budget: ${(err as Error).message}\n`);
|
|
109
136
|
return 2;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* `check_routing_coverage` — two coverage ratchets, one per routed scope.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-routing-assurance` Phase 0.3. Coverage ratio = corpus cases / routed
|
|
6
|
+
* units, per scope, seeded at the measured current value. **CI fails only on a
|
|
7
|
+
* DECREASE** — the same COUNT-ratchet disposition `lint_trigger_precision.ts`
|
|
8
|
+
* already uses, and for the same reason: the number is a property of the estate,
|
|
9
|
+
* so a rise is somebody's work and a fall is somebody's omission.
|
|
10
|
+
*
|
|
11
|
+
* TWO scopes, never one aggregate, because the aggregate hides the finding:
|
|
12
|
+
*
|
|
13
|
+
* rules 94 / 105 = 0.895 deterministic corpus, gating
|
|
14
|
+
* skills 76 / 299 = 0.254 advisory live harness only
|
|
15
|
+
*
|
|
16
|
+
* The rules surface is ~90 % covered by a corpus that can fail a PR. The skills
|
|
17
|
+
* surface — the one production actually routes on — is 25 %, covered only by a
|
|
18
|
+
* harness that is "advisory only, never gating" (`rule_trigger_eval.ts:4`). One
|
|
19
|
+
* blended figure would read as ~46 % and describe neither.
|
|
20
|
+
*
|
|
21
|
+
* WHY A RATIO AND NOT A COUNT: the denominator moves. Adding 8 skills without
|
|
22
|
+
* adding corpus cases lowers coverage while every count rises, and a count
|
|
23
|
+
* ratchet would call that progress.
|
|
24
|
+
*
|
|
25
|
+
* Exit codes: 0 = at or above seed · 1 = a scope fell · 2 = dead scope.
|
|
26
|
+
*/
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as os from 'node:os';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
import process from 'node:process';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
|
|
33
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
34
|
+
import { type SelfTestCase, runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
35
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
36
|
+
|
|
37
|
+
const HERE = fileURLToPath(import.meta.url);
|
|
38
|
+
const REAL_REPO_ROOT = path.resolve(path.dirname(HERE), '..', '..');
|
|
39
|
+
/**
|
|
40
|
+
* Scan root. The env override exists for `--self-test`, which drives the REAL
|
|
41
|
+
* CLI against a synthetic tree — an in-process call would skip the argv parsing
|
|
42
|
+
* and the entry guard, and a gate resolving its root from `import.meta.url`
|
|
43
|
+
* reads the live checkout whatever the cwd, so every fixture would be green for
|
|
44
|
+
* the wrong reason.
|
|
45
|
+
*/
|
|
46
|
+
const REPO_ROOT = process.env['CHECK_ROUTING_COVERAGE_ROOT'] ?? REAL_REPO_ROOT;
|
|
47
|
+
export const SEED_REL = 'src/config/routing-coverage-seed.json';
|
|
48
|
+
|
|
49
|
+
export type Scope = 'rules' | 'skills';
|
|
50
|
+
|
|
51
|
+
export interface ScopeReading {
|
|
52
|
+
scope: Scope;
|
|
53
|
+
cases: number;
|
|
54
|
+
units: number;
|
|
55
|
+
ratio: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Seed {
|
|
59
|
+
rules: number;
|
|
60
|
+
skills: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Routed rules = every tier-1 and tier-2 entry in the compiled router. */
|
|
64
|
+
export function measureRules(root: string): ScopeReading {
|
|
65
|
+
const routerPath = path.join(root, 'dist', 'router.json');
|
|
66
|
+
let doc: { tier_1?: { id: string }[]; tier_2?: { id: string }[] };
|
|
67
|
+
try {
|
|
68
|
+
doc = JSON.parse(fs.readFileSync(routerPath, 'utf-8')) as typeof doc;
|
|
69
|
+
} catch {
|
|
70
|
+
throw new DeadScopeError(
|
|
71
|
+
'check_routing_coverage',
|
|
72
|
+
'dist/router.json is unreadable — the routed-rule denominator cannot be measured, ' +
|
|
73
|
+
'and a missing router is not a coverage of zero.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const ids = new Set([...(doc.tier_1 ?? []), ...(doc.tier_2 ?? [])].map((r) => r.id));
|
|
77
|
+
const dir = path.join(root, 'tests', 'eval', 'routing-matrix');
|
|
78
|
+
let files: string[];
|
|
79
|
+
try {
|
|
80
|
+
files = fs.readdirSync(dir);
|
|
81
|
+
} catch {
|
|
82
|
+
files = [];
|
|
83
|
+
}
|
|
84
|
+
const stems = new Set(
|
|
85
|
+
files.filter((f) => /\.ya?ml$/.test(f)).map((f) => f.replace(/\.ya?ml$/, '')),
|
|
86
|
+
);
|
|
87
|
+
// Intersection, not the file count: a fixture naming no routed rule covers
|
|
88
|
+
// nothing, and counting it would let the ratio rise on dead files.
|
|
89
|
+
let covered = 0;
|
|
90
|
+
for (const s of stems) if (ids.has(s)) covered += 1;
|
|
91
|
+
return { scope: 'rules', cases: covered, units: ids.size, ratio: ids.size === 0 ? 0 : covered / ids.size };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Routed skills = every SKILL.md; a case = that skill's `evals/triggers.json`. */
|
|
95
|
+
export function measureSkills(root: string): ScopeReading {
|
|
96
|
+
const base = path.join(root, 'src', 'skills');
|
|
97
|
+
let entries: fs.Dirent[];
|
|
98
|
+
try {
|
|
99
|
+
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
100
|
+
} catch {
|
|
101
|
+
throw new DeadScopeError(
|
|
102
|
+
'check_routing_coverage',
|
|
103
|
+
'src/skills is unreadable — the routed-skill denominator cannot be measured.',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
let units = 0;
|
|
107
|
+
let cases = 0;
|
|
108
|
+
for (const e of entries) {
|
|
109
|
+
if (!e.isDirectory()) continue;
|
|
110
|
+
if (!fs.existsSync(path.join(base, e.name, 'SKILL.md'))) continue;
|
|
111
|
+
units += 1;
|
|
112
|
+
if (fs.existsSync(path.join(base, e.name, 'evals', 'triggers.json'))) cases += 1;
|
|
113
|
+
}
|
|
114
|
+
return { scope: 'skills', cases, units, ratio: units === 0 ? 0 : cases / units };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function readSeed(root: string): Seed {
|
|
118
|
+
const p = path.join(root, SEED_REL);
|
|
119
|
+
let raw: string;
|
|
120
|
+
try {
|
|
121
|
+
raw = fs.readFileSync(p, 'utf-8');
|
|
122
|
+
} catch {
|
|
123
|
+
throw new DeadScopeError(
|
|
124
|
+
'check_routing_coverage',
|
|
125
|
+
`${SEED_REL} is missing — a ratchet with no seed passes every tree.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const doc = JSON.parse(raw) as { seed?: Partial<Seed> };
|
|
129
|
+
const s = doc.seed ?? {};
|
|
130
|
+
if (typeof s.rules !== 'number' || typeof s.skills !== 'number') {
|
|
131
|
+
throw new DeadScopeError(
|
|
132
|
+
'check_routing_coverage',
|
|
133
|
+
`${SEED_REL} carries no numeric seed for both scopes.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return { rules: s.rules, skills: s.skills };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface Verdict {
|
|
140
|
+
readings: ScopeReading[];
|
|
141
|
+
seed: Seed;
|
|
142
|
+
fallen: Scope[];
|
|
143
|
+
ledger: GateLedger;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Compare against the seed.
|
|
148
|
+
*
|
|
149
|
+
* Rounded to four decimals before comparing: an un-rounded float makes a ratio
|
|
150
|
+
* that did not change fail on a representation difference, which is a red the
|
|
151
|
+
* contributor cannot act on.
|
|
152
|
+
*/
|
|
153
|
+
/**
|
|
154
|
+
* The comparison precision, shared by the verdict AND the display.
|
|
155
|
+
*
|
|
156
|
+
* Exported because the first version rounded only inside `evaluate` and printed
|
|
157
|
+
* the raw float: rules read 0.895238… against a seed of 0.8952 and rendered `↑`,
|
|
158
|
+
* skills read 0.254180… against 0.2542 and rendered `❌`, while the summary line
|
|
159
|
+
* correctly said green. A gate whose rows contradict its verdict is worse than
|
|
160
|
+
* one that is simply wrong, because the reader cannot tell which half to trust.
|
|
161
|
+
*/
|
|
162
|
+
export function r4(n: number): number {
|
|
163
|
+
return Math.round(n * 10_000) / 10_000;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function evaluate(root = REPO_ROOT): Verdict {
|
|
167
|
+
const seed = readSeed(root);
|
|
168
|
+
const readings = [measureRules(root), measureSkills(root)];
|
|
169
|
+
// The ledger's target is the SCOPE, not the individual unit: this gate's
|
|
170
|
+
// verdict is per scope, so a ledger over 404 units would report 404 rows
|
|
171
|
+
// no one can act on while hiding which of the two actually fell.
|
|
172
|
+
const ledger = new GateLedger('check_routing_coverage');
|
|
173
|
+
ledger.plan(readings.map((r) => r.scope));
|
|
174
|
+
const fallen: Scope[] = [];
|
|
175
|
+
for (const r of readings) {
|
|
176
|
+
if (r4(r.ratio) < r4(seed[r.scope])) {
|
|
177
|
+
fallen.push(r.scope);
|
|
178
|
+
ledger.fail(
|
|
179
|
+
r.scope,
|
|
180
|
+
`${r.ratio.toFixed(4)} < seed ${seed[r.scope].toFixed(4)} ` +
|
|
181
|
+
`(${String(r.cases)}/${String(r.units)})`,
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
ledger.complete(r.scope);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { readings, seed, fallen, ledger };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build a synthetic tree with `r` routed rules of which `rc` carry a fixture,
|
|
192
|
+
* and `s` routed skills of which `sc` carry a triggers file, plus a seed.
|
|
193
|
+
*
|
|
194
|
+
* A REALISTIC corpus rather than a minimal one: both scopes are always
|
|
195
|
+
* populated, because the gate reports two scopes and a fixture that leaves one
|
|
196
|
+
* empty would red for the empty scope no matter what the case under test does.
|
|
197
|
+
*/
|
|
198
|
+
function selfTestRoot(
|
|
199
|
+
tmp: string,
|
|
200
|
+
spec: { r: number; rc: number; s: number; sc: number; seed?: Partial<Seed> | null },
|
|
201
|
+
): string {
|
|
202
|
+
const root = fs.mkdtempSync(path.join(tmp, 'repo-'));
|
|
203
|
+
const ids: { id: string }[] = [];
|
|
204
|
+
const mdir = path.join(root, 'tests', 'eval', 'routing-matrix');
|
|
205
|
+
fs.mkdirSync(mdir, { recursive: true });
|
|
206
|
+
fs.mkdirSync(path.join(root, 'dist'), { recursive: true });
|
|
207
|
+
for (let i = 0; i < spec.r; i += 1) {
|
|
208
|
+
ids.push({ id: `st-rule-${String(i)}` });
|
|
209
|
+
if (i < spec.rc) fs.writeFileSync(path.join(mdir, `st-rule-${String(i)}.yaml`), 'cases: []\n');
|
|
210
|
+
}
|
|
211
|
+
fs.writeFileSync(
|
|
212
|
+
path.join(root, 'dist', 'router.json'),
|
|
213
|
+
JSON.stringify({ tier_1: ids.slice(0, 1), tier_2: ids.slice(1) }),
|
|
214
|
+
);
|
|
215
|
+
for (let i = 0; i < spec.s; i += 1) {
|
|
216
|
+
const d = path.join(root, 'src', 'skills', `st-skill-${String(i)}`);
|
|
217
|
+
fs.mkdirSync(d, { recursive: true });
|
|
218
|
+
fs.writeFileSync(path.join(d, 'SKILL.md'), '---\nname: st\n---\n');
|
|
219
|
+
if (i < spec.sc) {
|
|
220
|
+
fs.mkdirSync(path.join(d, 'evals'), { recursive: true });
|
|
221
|
+
fs.writeFileSync(path.join(d, 'evals', 'triggers.json'), '[]\n');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (spec.seed !== null) {
|
|
225
|
+
fs.mkdirSync(path.join(root, path.dirname(SEED_REL)), { recursive: true });
|
|
226
|
+
fs.writeFileSync(
|
|
227
|
+
path.join(root, SEED_REL),
|
|
228
|
+
JSON.stringify({ seed: spec.seed ?? { rules: spec.rc / spec.r, skills: spec.sc / spec.s } }),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return root;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function selfTest(): number {
|
|
235
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'crc-selftest-'));
|
|
236
|
+
const run = (root: string): number => {
|
|
237
|
+
process.env['CHECK_ROUTING_COVERAGE_ROOT'] = root;
|
|
238
|
+
try {
|
|
239
|
+
return runGateCli(REAL_REPO_ROOT, 'src/scripts/check_routing_coverage.ts', [], root);
|
|
240
|
+
} finally {
|
|
241
|
+
delete process.env['CHECK_ROUTING_COVERAGE_ROOT'];
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const cases: SelfTestCase[] = [
|
|
245
|
+
{
|
|
246
|
+
name: 'a tree exactly at seed on both scopes is accepted',
|
|
247
|
+
expect: 'accept',
|
|
248
|
+
run: () => run(selfTestRoot(tmp, { r: 10, rc: 8, s: 10, sc: 5 })),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'a tree ABOVE seed on both scopes is accepted',
|
|
252
|
+
expect: 'accept',
|
|
253
|
+
run: () =>
|
|
254
|
+
run(selfTestRoot(tmp, { r: 10, rc: 9, s: 10, sc: 6, seed: { rules: 0.8, skills: 0.5 } })),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'a removed rules corpus case is rejected',
|
|
258
|
+
expect: 'reject',
|
|
259
|
+
run: () =>
|
|
260
|
+
run(selfTestRoot(tmp, { r: 10, rc: 7, s: 10, sc: 5, seed: { rules: 0.8, skills: 0.5 } })),
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'a removed skills corpus case is rejected',
|
|
264
|
+
expect: 'reject',
|
|
265
|
+
run: () =>
|
|
266
|
+
run(selfTestRoot(tmp, { r: 10, rc: 8, s: 10, sc: 4, seed: { rules: 0.8, skills: 0.5 } })),
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'ADDING routed units with no corpus cases is rejected — the count-ratchet blind spot',
|
|
270
|
+
expect: 'reject',
|
|
271
|
+
run: () =>
|
|
272
|
+
run(selfTestRoot(tmp, { r: 10, rc: 8, s: 12, sc: 5, seed: { rules: 0.8, skills: 0.5 } })),
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'a missing seed exits non-zero rather than passing every tree',
|
|
276
|
+
expect: 'reject',
|
|
277
|
+
run: () => run(selfTestRoot(tmp, { r: 10, rc: 8, s: 10, sc: 5, seed: null })),
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'an unreadable router exits non-zero rather than reporting coverage of zero',
|
|
281
|
+
expect: 'reject',
|
|
282
|
+
run: () => {
|
|
283
|
+
const root = selfTestRoot(tmp, { r: 10, rc: 8, s: 10, sc: 5 });
|
|
284
|
+
fs.writeFileSync(path.join(root, 'dist', 'router.json'), 'not json');
|
|
285
|
+
return run(root);
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
try {
|
|
290
|
+
return runSelfTest({
|
|
291
|
+
gate: 'check_routing_coverage',
|
|
292
|
+
cases,
|
|
293
|
+
minCases: 7,
|
|
294
|
+
minRejectCases: 5,
|
|
295
|
+
});
|
|
296
|
+
} finally {
|
|
297
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function main(argv: string[] = process.argv.slice(2), root = REPO_ROOT): number {
|
|
302
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
303
|
+
let v: Verdict;
|
|
304
|
+
try {
|
|
305
|
+
v = evaluate(root);
|
|
306
|
+
reportScanned({
|
|
307
|
+
gate: 'check_routing_coverage',
|
|
308
|
+
scanned: v.readings.reduce((n, r) => n + r.units, 0),
|
|
309
|
+
units: 'routed unit(s)',
|
|
310
|
+
roots: ['dist/router.json', 'src/skills'],
|
|
311
|
+
});
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (e instanceof DeadScopeError) {
|
|
314
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
315
|
+
return 2;
|
|
316
|
+
}
|
|
317
|
+
throw e;
|
|
318
|
+
}
|
|
319
|
+
v.ledger.report(argv.includes('--quiet') ? () => undefined : process.stdout.write.bind(process.stdout));
|
|
320
|
+
if (argv.includes('--json')) {
|
|
321
|
+
process.stdout.write(`${JSON.stringify(v, null, 2)}\n`);
|
|
322
|
+
return v.fallen.length > 0 ? 1 : 0;
|
|
323
|
+
}
|
|
324
|
+
for (const r of v.readings) {
|
|
325
|
+
const seed = v.seed[r.scope];
|
|
326
|
+
// Same rounding as the verdict, deliberately: see r4's own comment.
|
|
327
|
+
const mark = r4(r.ratio) < r4(seed) ? '❌' : r4(r.ratio) > r4(seed) ? '↑' : '=';
|
|
328
|
+
process.stdout.write(
|
|
329
|
+
` ${mark} ${r.scope.padEnd(6)} ${String(r.cases).padStart(4)} / ${String(r.units).padEnd(4)} = ` +
|
|
330
|
+
`${r.ratio.toFixed(4)} (seed ${seed.toFixed(4)})\n`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (v.fallen.length === 0) {
|
|
334
|
+
process.stdout.write('✅ routing coverage at or above seed in both scopes.\n');
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
for (const s of v.fallen) {
|
|
338
|
+
const r = v.readings.find((x) => x.scope === s)!;
|
|
339
|
+
process.stderr.write(
|
|
340
|
+
`❌ ${s} coverage fell: ${r.ratio.toFixed(4)} < seed ${v.seed[s].toFixed(4)} ` +
|
|
341
|
+
`(${String(r.cases)} case(s) over ${String(r.units)} routed unit(s)).\n` +
|
|
342
|
+
' A ratio falls two ways and the fix differs: a corpus case was removed ' +
|
|
343
|
+
'(restore it), or units were added without cases (add them). Lowering the seed ' +
|
|
344
|
+
`in ${SEED_REL} is a defect, not a fix.\n`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return 1;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (path.resolve(process.argv[1] ?? '') === path.resolve(HERE)) {
|
|
351
|
+
process.exit(main());
|
|
352
|
+
}
|