@event4u/agent-config 9.13.0 → 9.15.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 +113 -67
- package/README.md +9 -1
- package/dist/agent-src/commands/agents/audit.md +7 -7
- package/dist/agent-src/commands/agents/optimize.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
- package/dist/agent-src/commands/chat-history.md +1 -0
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/council.md +1 -1
- package/dist/agent-src/commands/ghostwriter.md +1 -1
- package/dist/agent-src/commands/memory.md +2 -1
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +150 -0
- package/dist/agent-src/commands/optimize/skills.md +5 -5
- package/dist/agent-src/commands/optimize.md +12 -6
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/prediction-pool.md +4 -4
- package/dist/agent-src/commands/research/deep.md +1 -1
- package/dist/agent-src/commands/roadmap.md +2 -1
- package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
- package/dist/agent-src/commands/tdd.md +1 -0
- package/dist/agent-src/commands/tests.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +5 -5
- package/dist/agent-src/commands/video/from-script.md +1 -1
- package/dist/agent-src/commands/video/from-song.md +1 -1
- package/dist/agent-src/commands/video/scene.md +1 -1
- package/dist/agent-src/commands/video/stitch.md +1 -1
- package/dist/agent-src/commands/video/storyboard.md +1 -1
- package/dist/agent-src/commands/worktree/create.md +2 -1
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
- package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
- package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
- package/dist/agent-src/contexts/override-system.md +1 -1
- package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
- package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +2 -1
- package/dist/agent-src/rules/brand-consistency.md +1 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +1 -0
- package/dist/agent-src/rules/content-quoting-floor.md +1 -0
- package/dist/agent-src/rules/cross-source-consistency.md +0 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +0 -1
- package/dist/agent-src/rules/icon-consistency.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/invite-challenge.md +1 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/dist/agent-src/rules/media-governance-routing.md +4 -3
- package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
- package/dist/agent-src/rules/no-attribution-footers.md +1 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
- package/dist/agent-src/rules/onboarding-gate.md +1 -0
- package/dist/agent-src/rules/output-discipline.md +1 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
- package/dist/agent-src/rules/preservation-guard.md +1 -0
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
- package/dist/agent-src/rules/role-mode-adherence.md +1 -0
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
- package/dist/agent-src/rules/session-canary.md +9 -2
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/source-of-truth.md +1 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/token-budget-discipline.md +1 -0
- package/dist/agent-src/rules/token-efficiency.md +1 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
- package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
- package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
- package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
- package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/command.md +1 -1
- package/dist/agent-src/templates/copilot-instructions.md +3 -3
- package/dist/agent-src/templates/rule.md +2 -2
- package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/dist/agent-src/templates/skill-archive-note.md +5 -5
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/agent-config.js +66 -19
- package/dist/cli/agent-config.js.map +1 -1
- package/dist/cli/main.js +9 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/python/workspace_drive.js +2 -0
- package/dist/cli/python/workspace_drive.js.map +1 -1
- package/dist/cli/python/workspace_drive_health.js +2 -0
- package/dist/cli/python/workspace_drive_health.js.map +1 -1
- package/dist/cli/python/workspace_hosts.js +2 -0
- package/dist/cli/python/workspace_hosts.js.map +1 -1
- package/dist/cli/python/workspace_sessions.js +2 -0
- package/dist/cli/python/workspace_sessions.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
- package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
- package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-H574KOUH.js} +340 -462
- package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
- package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
- package/dist/cli-delegate/cmd_conformance.js +29 -23
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +163 -103
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +729 -430
- package/dist/install/install.mjs +700 -625
- package/dist/install/rule_scope.js +18 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +564 -424
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
- package/dist/scripts/_lib/scoped_projection.js +223 -0
- package/dist/scripts/_lib/scoped_projection.js.map +1 -0
- package/dist/scripts/_lib/surface_tiers.js +125 -0
- package/dist/scripts/_lib/surface_tiers.js.map +1 -0
- package/dist/scripts/_lib/token_count.js +166 -0
- package/dist/scripts/_lib/token_count.js.map +1 -0
- package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
- package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
- package/dist/scripts/condense.js +139 -9
- package/dist/scripts/condense.js.map +1 -1
- package/dist/scripts/project_thin_rules.js +450 -0
- package/dist/scripts/project_thin_rules.js.map +1 -0
- package/dist/server/routes/workspace.js +2 -0
- package/dist/server/routes/workspace.js.map +1 -1
- package/dist/server/schemas/settings.js +9 -3
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/index-DJY_vQVn.js +40 -0
- package/dist/ui/assets/index-DJY_vQVn.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/package.json +11 -11
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/src/config/agent-settings.template.yml +59 -11
- package/src/config/ci-local-parity.yml +8 -0
- package/src/config/discovery/unassigned-artefacts.yml +23 -25
- package/src/config/evaluator-budgets.json +35 -5
- package/src/config/gate-coverage.yml +147 -0
- package/src/config/gate-violation-baselines.json +25 -0
- package/src/install/rule_scope.ts +27 -2
- package/src/scripts/_cli/cmd_conformance.ts +13 -24
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
- package/src/scripts/_lib/exec_evidence.ts +5 -0
- package/src/scripts/_lib/gate_baseline.ts +213 -0
- package/src/scripts/_lib/scoped_projection.ts +262 -0
- package/src/scripts/_lib/tier_budget_routing.ts +234 -0
- package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
- package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
- package/src/scripts/_lib/trigger_routers.ts +17 -5
- package/src/scripts/adr/regenerate_index.ts +4 -3
- package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
- package/src/scripts/ai_council/compile_corpus.ts +1 -1
- package/src/scripts/audit_cloud_compatibility.ts +37 -2
- package/src/scripts/audit_initial_context.ts +53 -19
- package/src/scripts/audit_likelihood.ts +31 -2
- package/src/scripts/audit_user_type_axis.ts +58 -11
- package/src/scripts/bench_hook_latency.ts +172 -41
- package/src/scripts/build_mcp_catalog.ts +12 -1
- package/src/scripts/check_always_budget.ts +122 -11
- package/src/scripts/check_artefact_count_messaging.ts +128 -15
- package/src/scripts/check_augment_description_cap.ts +24 -3
- package/src/scripts/check_ci_strict_superset.ts +150 -0
- package/src/scripts/check_cluster_patterns.ts +100 -0
- package/src/scripts/check_consumer_scope_flip.ts +26 -4
- package/src/scripts/check_evaluator_budgets.ts +198 -19
- package/src/scripts/check_gate_coverage.ts +337 -1
- package/src/scripts/check_generator_output_coverage.ts +2 -1
- package/src/scripts/check_iron_law_prominence.ts +1 -1
- package/src/scripts/check_kernel_rule_bundle.ts +1 -1
- package/src/scripts/check_no_new_legacy_path.ts +167 -3
- package/src/scripts/check_pack_size.ts +18 -4
- package/src/scripts/check_portability.ts +37 -7
- package/src/scripts/check_references.ts +19 -3
- package/src/scripts/check_release_pr_shape.ts +13 -0
- package/src/scripts/check_token_regression.ts +9 -2
- package/src/scripts/condense.ts +155 -12
- package/src/scripts/cost/budget.mjs +63 -2
- package/src/scripts/count_scoped_projection.ts +104 -0
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/generate_capability_matrix.ts +8 -0
- package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
- package/src/scripts/hook_manifest.yaml +17 -3
- package/src/scripts/hooks/block_config_weakening.ts +337 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/probe_throttle.ts +75 -0
- package/src/scripts/install-hooks.sh +24 -2
- package/src/scripts/install.ts +15 -92
- package/src/scripts/lint_agents_md.ts +9 -3
- package/src/scripts/lint_archived_skills.ts +1 -1
- package/src/scripts/lint_artefact_frontmatter.ts +185 -37
- package/src/scripts/lint_command_routing.ts +21 -52
- package/src/scripts/lint_command_verbs.ts +47 -28
- package/src/scripts/lint_context_spine_usage.ts +38 -12
- package/src/scripts/lint_framework_leakage.ts +25 -6
- package/src/scripts/lint_handoffs.ts +49 -2
- package/src/scripts/lint_media_policy_linkage.ts +62 -30
- package/src/scripts/lint_namespace.ts +92 -7
- package/src/scripts/lint_new_skill_gate.ts +39 -31
- package/src/scripts/lint_originality_shingles.ts +7 -1
- package/src/scripts/lint_override_kernel_guard.ts +21 -3
- package/src/scripts/lint_pack_boundaries.ts +461 -91
- package/src/scripts/lint_pack_dependencies.ts +24 -57
- package/src/scripts/lint_persona_governance.ts +1 -1
- package/src/scripts/lint_role_experiences.ts +34 -5
- package/src/scripts/lint_trigger_precision.ts +142 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
- package/src/scripts/mcp_server/tools.ts +1 -1
- package/src/scripts/prepack-check.mjs +40 -1
- package/src/scripts/prepack_router_targets.d.mts +7 -0
- package/src/scripts/prepack_router_targets.mjs +93 -0
- package/src/scripts/print_required_checks.ts +72 -18
- package/src/scripts/profile_staleness_hook.ts +12 -0
- package/src/scripts/record_evaluator_measurements.mjs +105 -0
- package/src/scripts/recruit_preflight.sh +1 -1
- package/src/scripts/release.ts +105 -1
- package/src/scripts/router_target_paths.d.mts +4 -0
- package/src/scripts/router_target_paths.mjs +69 -0
- package/src/scripts/router_telemetry.ts +52 -1
- package/src/scripts/routing_doctor.ts +485 -0
- package/src/scripts/rule_trigger_eval.ts +403 -0
- package/src/scripts/schemas/rule.schema.json +5 -1
- package/src/scripts/session_canary_hook.ts +96 -5
- package/src/scripts/skill_linter.ts +90 -42
- package/src/scripts/smoke/skills.sh +1 -1
- package/src/scripts/sweep_dead_scan_roots.ts +356 -10
- package/src/scripts/trigger_coverage.ts +13 -2
- package/src/scripts/update_counts.ts +91 -12
- package/src/scripts/validate_telegraph_carveouts.ts +1 -1
- package/src/scripts/wrapper_freshness_hook.ts +14 -0
- package/src/server/schemas/settings.ts +19 -5
- package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
- package/dist/ui/assets/index-BpSQt5vm.js +0 -40
- package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
|
@@ -3,17 +3,38 @@
|
|
|
3
3
|
* check_evaluator_budgets — enforce the outside-in evaluator budgets
|
|
4
4
|
* (road-to-credible-install Phase 6) against a measurements JSON.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Three failure modes per metric:
|
|
7
7
|
* 1. absolute: measured > budgets[metric].max
|
|
8
8
|
* 2. regression: measured > last_measured * (1 + regression_pct/100)
|
|
9
9
|
* — fails EVEN WHEN still under the absolute budget (the >10%
|
|
10
10
|
* creep rule), so slow rot cannot hide under a generous cap.
|
|
11
|
+
* 3. drift: a DETERMINISTIC metric disagrees with the committed
|
|
12
|
+
* measurement record the nightly publishes. `cli_help_command_count`
|
|
13
|
+
* drifted 74 → 80 against a value frozen at 79 with nobody seeing it;
|
|
14
|
+
* the record makes the day a number moves the day it shows in a diff
|
|
15
|
+
* (road-to-gates-that-can-fail Phase 5).
|
|
16
|
+
*
|
|
17
|
+
* ## Posture — warn on main, fail on release
|
|
18
|
+
*
|
|
19
|
+
* A hard fail on `main` turns every legitimate command addition into a
|
|
20
|
+
* blocked merge, which is how budgets get quietly raised with a cushion
|
|
21
|
+
* instead of consciously. So budget / regression / drift findings are
|
|
22
|
+
* ADVISORY outside a release context and BLOCKING inside one. This is not a
|
|
23
|
+
* weakening: before the Phase-4 path triggers, the umbrella ran on release
|
|
24
|
+
* PRs and the nightly only — the surfaces that now warn never saw the gate
|
|
25
|
+
* at all. The posture IS the delivery route the council adopted for nightly
|
|
26
|
+
* visibility, not a second mechanism beside it.
|
|
27
|
+
*
|
|
28
|
+
* Structural findings (a metric with NO measurement supplied) are ALWAYS
|
|
29
|
+
* blocking, in either posture: that is a harness reporting green on nothing,
|
|
30
|
+
* which is the whole class this roadmap exists to end.
|
|
11
31
|
*
|
|
12
32
|
* Usage:
|
|
13
33
|
* ./scripts-run src/scripts/check_evaluator_budgets --measurements <file.json>
|
|
34
|
+
* [--recorded <file.json>] [--posture warn|fail]
|
|
14
35
|
* (measurements: flat { metric: number } — produced by evaluator_umbrella.sh)
|
|
15
36
|
*
|
|
16
|
-
* Exit codes: 0 green · 1
|
|
37
|
+
* Exit codes: 0 green (or advisory findings only) · 1 blocking finding · 2 internal error.
|
|
17
38
|
*/
|
|
18
39
|
import * as fs from 'node:fs';
|
|
19
40
|
import * as path from 'node:path';
|
|
@@ -22,6 +43,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
43
|
const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
23
44
|
const BUDGETS_PATH = path.join(REPO_ROOT, 'src', 'config', 'evaluator-budgets.json');
|
|
24
45
|
|
|
46
|
+
/** Committed measurement record the nightly refreshes (Phase 5 step 1). */
|
|
47
|
+
export const RECORD_REL = path.join('agents', 'evidence', 'metrics', 'evaluator-measurements.json');
|
|
48
|
+
|
|
25
49
|
interface BudgetEntry {
|
|
26
50
|
max: number;
|
|
27
51
|
last_measured: number;
|
|
@@ -30,51 +54,174 @@ interface BudgetEntry {
|
|
|
30
54
|
* (absolute cap is their real gate); sizes/counts keep the tight
|
|
31
55
|
* global default. */
|
|
32
56
|
regression_pct?: number;
|
|
57
|
+
/** `false` for wall-clock metrics: they cannot be compared run-to-run,
|
|
58
|
+
* so they are excluded from the drift check. Missing = deterministic. */
|
|
59
|
+
deterministic?: boolean;
|
|
33
60
|
}
|
|
34
61
|
interface BudgetsDoc {
|
|
35
62
|
regression_pct: number;
|
|
36
63
|
budgets: Record<string, BudgetEntry>;
|
|
37
64
|
}
|
|
38
65
|
|
|
39
|
-
|
|
66
|
+
/** Shape of the committed measurement record. */
|
|
67
|
+
export interface MeasurementRecord {
|
|
68
|
+
schema_version: number;
|
|
69
|
+
recorded_at: string;
|
|
70
|
+
git_sha?: string;
|
|
71
|
+
run_url?: string;
|
|
72
|
+
conditions?: string;
|
|
73
|
+
measurements: Record<string, number | string>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type Severity =
|
|
77
|
+
/** Always blocks — the harness itself is broken. */
|
|
78
|
+
| 'structural'
|
|
79
|
+
/** Blocks only under the `fail` posture. */
|
|
80
|
+
| 'advisory';
|
|
81
|
+
|
|
82
|
+
export interface Finding {
|
|
83
|
+
metric: string;
|
|
84
|
+
severity: Severity;
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function evaluateFindings(
|
|
40
89
|
budgetsDoc: BudgetsDoc,
|
|
41
90
|
measurements: Record<string, number>,
|
|
42
|
-
):
|
|
43
|
-
const
|
|
91
|
+
): Finding[] {
|
|
92
|
+
const findings: Finding[] = [];
|
|
44
93
|
for (const [metric, entry] of Object.entries(budgetsDoc.budgets)) {
|
|
45
94
|
const measured = measurements[metric];
|
|
46
95
|
if (measured === undefined || Number.isNaN(measured)) {
|
|
47
|
-
|
|
96
|
+
findings.push({
|
|
97
|
+
metric,
|
|
98
|
+
severity: 'structural',
|
|
99
|
+
message: `${metric}: no measurement supplied (method: ${entry.method})`,
|
|
100
|
+
});
|
|
48
101
|
continue;
|
|
49
102
|
}
|
|
50
103
|
if (measured > entry.max) {
|
|
51
|
-
|
|
104
|
+
findings.push({
|
|
105
|
+
metric,
|
|
106
|
+
severity: 'advisory',
|
|
107
|
+
message: `${metric}: measured ${measured} exceeds budget ${entry.max}`,
|
|
108
|
+
});
|
|
52
109
|
continue;
|
|
53
110
|
}
|
|
54
111
|
const pct = entry.regression_pct ?? budgetsDoc.regression_pct;
|
|
55
112
|
const ceiling = entry.last_measured * (1 + pct / 100);
|
|
56
113
|
if (entry.last_measured > 0 && measured > ceiling) {
|
|
57
|
-
|
|
58
|
-
|
|
114
|
+
findings.push({
|
|
115
|
+
metric,
|
|
116
|
+
severity: 'advisory',
|
|
117
|
+
message:
|
|
118
|
+
`${metric}: measured ${measured} regressed >${pct}% vs ` +
|
|
59
119
|
`last_measured ${entry.last_measured} (ceiling ${ceiling.toFixed(2)}) — ` +
|
|
60
120
|
'fails even under the absolute budget',
|
|
61
|
-
);
|
|
121
|
+
});
|
|
62
122
|
}
|
|
63
123
|
}
|
|
64
|
-
return
|
|
124
|
+
return findings;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Back-compat surface: the flat message list. Kept because the budget shape
|
|
129
|
+
* and the per-metric rules are pinned by tests through this function.
|
|
130
|
+
*/
|
|
131
|
+
export function evaluate(budgetsDoc: BudgetsDoc, measurements: Record<string, number>): string[] {
|
|
132
|
+
return evaluateFindings(budgetsDoc, measurements).map((f) => f.message);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Compare a fresh measurement set against the committed record.
|
|
137
|
+
*
|
|
138
|
+
* Only DETERMINISTIC metrics participate: comparing wall-clock timings
|
|
139
|
+
* run-to-run produces noise, and a noisy comparison gets ignored — which
|
|
140
|
+
* reproduces the failure the record exists to fix. A metric absent from
|
|
141
|
+
* either side is skipped (a missing measurement is already a structural
|
|
142
|
+
* finding above; a metric absent from the record is simply not recorded yet).
|
|
143
|
+
*/
|
|
144
|
+
export function driftFindings(
|
|
145
|
+
budgetsDoc: BudgetsDoc,
|
|
146
|
+
measurements: Record<string, number>,
|
|
147
|
+
record: MeasurementRecord | null,
|
|
148
|
+
): Finding[] {
|
|
149
|
+
if (record === null) return [];
|
|
150
|
+
const findings: Finding[] = [];
|
|
151
|
+
for (const [metric, entry] of Object.entries(budgetsDoc.budgets)) {
|
|
152
|
+
if (entry.deterministic === false) continue;
|
|
153
|
+
const fresh = measurements[metric];
|
|
154
|
+
const recorded = record.measurements[metric];
|
|
155
|
+
if (fresh === undefined || recorded === undefined) continue;
|
|
156
|
+
if (typeof recorded !== 'number') continue;
|
|
157
|
+
if (fresh === recorded) continue;
|
|
158
|
+
findings.push({
|
|
159
|
+
metric,
|
|
160
|
+
severity: 'advisory',
|
|
161
|
+
message:
|
|
162
|
+
`${metric}: measured ${fresh} but the committed record says ${recorded} ` +
|
|
163
|
+
`(recorded ${record.recorded_at}` +
|
|
164
|
+
`${record.git_sha ? ` @ ${record.git_sha.slice(0, 8)}` : ''}) — refresh ` +
|
|
165
|
+
`${RECORD_REL} in the PR that moves the number, or explain the move`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return findings;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type Posture = 'warn' | 'fail';
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Where are we, and does a budget finding block?
|
|
175
|
+
*
|
|
176
|
+
* Release context → `fail`. Local (no CI env) → `fail`, deliberately: a
|
|
177
|
+
* maintainer running this by hand — including `release.ts` — wants the strict
|
|
178
|
+
* answer, and a local run blocks nobody else. Everything else (main, the
|
|
179
|
+
* nightly, a feature PR) → `warn`.
|
|
180
|
+
*/
|
|
181
|
+
export function detectPosture(
|
|
182
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
183
|
+
override?: string,
|
|
184
|
+
): { posture: Posture; reason: string } {
|
|
185
|
+
if (override === 'warn' || override === 'fail') {
|
|
186
|
+
return { posture: override, reason: `--posture ${override}` };
|
|
187
|
+
}
|
|
188
|
+
const head = env['GITHUB_HEAD_REF'] ?? '';
|
|
189
|
+
const base = env['GITHUB_BASE_REF'] ?? '';
|
|
190
|
+
const ref = env['GITHUB_REF'] ?? '';
|
|
191
|
+
if (head.startsWith('release/')) {
|
|
192
|
+
return { posture: 'fail', reason: `release PR (head ${head})` };
|
|
193
|
+
}
|
|
194
|
+
if (base.startsWith('release/')) {
|
|
195
|
+
return { posture: 'fail', reason: `PR into a release branch (base ${base})` };
|
|
196
|
+
}
|
|
197
|
+
if (ref.startsWith('refs/heads/release/') || ref.startsWith('refs/tags/')) {
|
|
198
|
+
return { posture: 'fail', reason: `release ref (${ref})` };
|
|
199
|
+
}
|
|
200
|
+
if (!env['GITHUB_ACTIONS']) {
|
|
201
|
+
return { posture: 'fail', reason: 'local run (no CI env) — strict by default' };
|
|
202
|
+
}
|
|
203
|
+
return { posture: 'warn', reason: `non-release CI context (${ref || 'unknown ref'})` };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function argValue(argv: string[], flag: string): string | undefined {
|
|
207
|
+
const i = argv.indexOf(flag);
|
|
208
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
65
209
|
}
|
|
66
210
|
|
|
67
211
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
process.stderr.write(
|
|
212
|
+
const measurementsPath = argValue(argv, '--measurements');
|
|
213
|
+
if (measurementsPath === undefined) {
|
|
214
|
+
process.stderr.write(
|
|
215
|
+
'usage: check_evaluator_budgets --measurements <file.json> ' +
|
|
216
|
+
'[--recorded <file.json>] [--posture warn|fail]\n',
|
|
217
|
+
);
|
|
71
218
|
return 2;
|
|
72
219
|
}
|
|
73
220
|
let budgetsDoc: BudgetsDoc;
|
|
74
221
|
let measurements: Record<string, number>;
|
|
75
222
|
try {
|
|
76
223
|
budgetsDoc = JSON.parse(fs.readFileSync(BUDGETS_PATH, 'utf-8')) as BudgetsDoc;
|
|
77
|
-
measurements = JSON.parse(fs.readFileSync(
|
|
224
|
+
measurements = JSON.parse(fs.readFileSync(measurementsPath, 'utf-8')) as Record<
|
|
78
225
|
string,
|
|
79
226
|
number
|
|
80
227
|
>;
|
|
@@ -82,13 +229,45 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
82
229
|
process.stderr.write(`check_evaluator_budgets: ${(e as Error).message}\n`);
|
|
83
230
|
return 2;
|
|
84
231
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
232
|
+
|
|
233
|
+
// The committed record is optional: a fresh checkout before the first
|
|
234
|
+
// nightly has none, and a missing record must not fake a green drift check.
|
|
235
|
+
const recordPath = argValue(argv, '--recorded') ?? path.join(REPO_ROOT, RECORD_REL);
|
|
236
|
+
let record: MeasurementRecord | null = null;
|
|
237
|
+
try {
|
|
238
|
+
record = JSON.parse(fs.readFileSync(recordPath, 'utf-8')) as MeasurementRecord;
|
|
239
|
+
} catch {
|
|
240
|
+
process.stdout.write(
|
|
241
|
+
`ℹ️ no measurement record at ${recordPath} — drift check skipped ` +
|
|
242
|
+
'(the nightly publishes it)\n',
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { posture, reason } = detectPosture(process.env, argValue(argv, '--posture'));
|
|
247
|
+
const findings = [
|
|
248
|
+
...evaluateFindings(budgetsDoc, measurements),
|
|
249
|
+
...driftFindings(budgetsDoc, measurements, record),
|
|
250
|
+
];
|
|
251
|
+
const blocking = findings.filter((f) => f.severity === 'structural' || posture === 'fail');
|
|
252
|
+
const advisory = findings.filter((f) => !blocking.includes(f));
|
|
253
|
+
|
|
254
|
+
for (const f of advisory) {
|
|
255
|
+
process.stderr.write(`⚠️ evaluator budget (advisory, posture=${posture}): ${f.message}\n`);
|
|
256
|
+
}
|
|
257
|
+
for (const f of blocking) {
|
|
258
|
+
process.stderr.write(`❌ evaluator budget: ${f.message}\n`);
|
|
259
|
+
}
|
|
260
|
+
if (blocking.length > 0) {
|
|
261
|
+
process.stderr.write(
|
|
262
|
+
`check_evaluator_budgets: ${blocking.length} blocking finding(s) — ` +
|
|
263
|
+
`posture ${posture} (${reason})\n`,
|
|
264
|
+
);
|
|
88
265
|
return 1;
|
|
89
266
|
}
|
|
90
267
|
process.stdout.write(
|
|
91
|
-
`✅ evaluator budgets met (${Object.keys(budgetsDoc.budgets).length} metrics
|
|
268
|
+
`✅ evaluator budgets met (${Object.keys(budgetsDoc.budgets).length} metrics` +
|
|
269
|
+
`${advisory.length > 0 ? `, ${advisory.length} advisory finding(s)` : ''}) — ` +
|
|
270
|
+
`posture ${posture} (${reason})\n`,
|
|
92
271
|
);
|
|
93
272
|
return 0;
|
|
94
273
|
}
|
|
@@ -23,13 +23,25 @@
|
|
|
23
23
|
* the manifest is empty or unreadable, and it REPORTS `pending` gates (listed but
|
|
24
24
|
* not yet emitting a count) instead of skipping them silently.
|
|
25
25
|
*
|
|
26
|
+
* MUTATION CANARY (`--canary`, road-to-gates-that-can-fail Phase 7). Coverage
|
|
27
|
+
* proves a gate READ something; it cannot prove the gate can still FAIL. The
|
|
28
|
+
* canary plants one declared violation per gate, runs the gate, and records
|
|
29
|
+
* red/green — a gate that stays green over a real planted defect is dead by
|
|
30
|
+
* definition. It runs the same contract as the review-side canary in
|
|
31
|
+
* `docs/contracts/adversarial-review-protocol.md` § 6 (rotating class, sealed
|
|
32
|
+
* record, NEVER SHIPS) and is deliberately kept OFF the default CI path: it
|
|
33
|
+
* mutates the tree, so it is an operator-invoked biannual experiment, not a
|
|
34
|
+
* per-PR gate. Every plant is reverted in a `finally`.
|
|
35
|
+
*
|
|
26
36
|
* CLI:
|
|
27
37
|
* ./scripts-run src/scripts/check_gate_coverage # enforce
|
|
28
38
|
* ./scripts-run src/scripts/check_gate_coverage --list # show the manifest
|
|
29
39
|
* ./scripts-run src/scripts/check_gate_coverage --format json
|
|
40
|
+
* ./scripts-run src/scripts/check_gate_coverage --canary [--ledger <file>]
|
|
30
41
|
*
|
|
31
42
|
* Exit codes: 0 all enforced gates cleared their floor · 1 a gate is blind,
|
|
32
|
-
* collapsed, or silent
|
|
43
|
+
* collapsed, or silent (in `--canary`: a gate stayed green, or the ledger
|
|
44
|
+
* disagrees with the census) · 2 the manifest itself is missing/empty/malformed.
|
|
33
45
|
*/
|
|
34
46
|
import * as fs from 'node:fs';
|
|
35
47
|
import * as path from 'node:path';
|
|
@@ -42,16 +54,36 @@ import { runCountedProbe } from './_lib/counted_probe.js';
|
|
|
42
54
|
const _HERE = fileURLToPath(import.meta.url);
|
|
43
55
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
44
56
|
const MANIFEST = path.join(REPO_ROOT, 'src/config/gate-coverage.yml');
|
|
57
|
+
const CENSUS = path.join(REPO_ROOT, 'agents/evidence/reports/gate-scope-census.md');
|
|
45
58
|
|
|
46
59
|
/** The one line a gate must emit. Deliberately narrow: no format guessing. */
|
|
47
60
|
const SCANNED_RE = /^\s*scanned:\s*(\d+)\s*$/m;
|
|
48
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A declared, revertible violation for one gate (Phase 7).
|
|
64
|
+
*
|
|
65
|
+
* Only ONE operation shape exists on purpose: create a file that does not exist,
|
|
66
|
+
* delete it afterwards. An in-place edit would need byte-exact restoration of a
|
|
67
|
+
* tracked file, and a canary that can corrupt the tree is worse than no canary —
|
|
68
|
+
* the contract's "never ships" rule binds this implementation too.
|
|
69
|
+
*/
|
|
70
|
+
export interface CanarySpec {
|
|
71
|
+
/** Rotating-class label, per adversarial-review-protocol § 6. */
|
|
72
|
+
class: string;
|
|
73
|
+
/** Repo-relative path to create. MUST NOT already exist. */
|
|
74
|
+
path: string;
|
|
75
|
+
/** File body planted at that path. */
|
|
76
|
+
content: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
export interface GateSpec {
|
|
50
80
|
id: string;
|
|
51
81
|
argv: string[];
|
|
52
82
|
min_scanned: number;
|
|
53
83
|
corpus: string;
|
|
54
84
|
status: 'enforced' | 'pending';
|
|
85
|
+
/** Absent = this gate has no canary recipe; reported, never hidden. */
|
|
86
|
+
canary?: CanarySpec;
|
|
55
87
|
/**
|
|
56
88
|
* Exit code meaning "my prerequisite is absent, so I inspected nothing" — e.g.
|
|
57
89
|
* check_site_links exits 2 when the site is unbuilt or stale. Without this the
|
|
@@ -117,10 +149,27 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
|
|
|
117
149
|
? {}
|
|
118
150
|
: { unavailable_exit: _require_int(e['unavailable_exit'], id, i) }),
|
|
119
151
|
...(e['note'] === undefined ? {} : { note: String(e['note']) }),
|
|
152
|
+
...(e['canary'] === undefined ? {} : { canary: _require_canary(e['canary'], id, i) }),
|
|
120
153
|
};
|
|
121
154
|
});
|
|
122
155
|
}
|
|
123
156
|
|
|
157
|
+
function _require_canary(v: unknown, id: string, i: number): CanarySpec {
|
|
158
|
+
const c = v as Record<string, unknown>;
|
|
159
|
+
const cls = String(c?.['class'] ?? '');
|
|
160
|
+
const p = String(c?.['path'] ?? '');
|
|
161
|
+
const content = String(c?.['content'] ?? '');
|
|
162
|
+
// A half-declared recipe would plant nothing and read as a passing canary —
|
|
163
|
+
// the exact false green this whole file exists to prevent.
|
|
164
|
+
if (cls === '' || p === '' || content === '') {
|
|
165
|
+
throw new Error(`gates[${String(i)}] (${id}): canary needs class, path and content`);
|
|
166
|
+
}
|
|
167
|
+
if (path.isAbsolute(p) || p.split('/').includes('..')) {
|
|
168
|
+
throw new Error(`gates[${String(i)}] (${id}): canary path must be repo-relative and contain no '..'`);
|
|
169
|
+
}
|
|
170
|
+
return { class: cls, path: p, content };
|
|
171
|
+
}
|
|
172
|
+
|
|
124
173
|
/** Extract the contract line. Returns null when the gate emitted no count. */
|
|
125
174
|
export function parse_scanned(output: string): number | null {
|
|
126
175
|
const m = SCANNED_RE.exec(output);
|
|
@@ -215,6 +264,10 @@ export function main(argv: readonly string[]): number {
|
|
|
215
264
|
return 2;
|
|
216
265
|
}
|
|
217
266
|
|
|
267
|
+
if (argv.includes('--canary')) {
|
|
268
|
+
return run_canary_mode(specs, argv);
|
|
269
|
+
}
|
|
270
|
+
|
|
218
271
|
if (listOnly) {
|
|
219
272
|
for (const s of specs) {
|
|
220
273
|
process.stdout.write(
|
|
@@ -283,6 +336,289 @@ function probe_pending(s: GateSpec): { scanned: number | null; crashed: boolean
|
|
|
283
336
|
return run_gate(s);
|
|
284
337
|
}
|
|
285
338
|
|
|
339
|
+
// ── Mutation canary (Phase 7) ──────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
export type CanaryVerdict = 'red' | 'green' | 'no_recipe' | 'crashed';
|
|
342
|
+
|
|
343
|
+
export interface CanaryResult {
|
|
344
|
+
id: string;
|
|
345
|
+
verdict: CanaryVerdict;
|
|
346
|
+
class: string | null;
|
|
347
|
+
exit_code: number | null;
|
|
348
|
+
message: string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Plant one declared violation, run the gate, revert.
|
|
353
|
+
*
|
|
354
|
+
* The revert is in a `finally` and deletes exactly the file this function
|
|
355
|
+
* created — never a pre-existing one, which is why an occupied path aborts
|
|
356
|
+
* instead of overwriting.
|
|
357
|
+
*/
|
|
358
|
+
export function run_canary(spec: GateSpec, repoRoot = REPO_ROOT): CanaryResult {
|
|
359
|
+
const c = spec.canary;
|
|
360
|
+
if (c === undefined) {
|
|
361
|
+
return {
|
|
362
|
+
id: spec.id,
|
|
363
|
+
verdict: 'no_recipe',
|
|
364
|
+
class: null,
|
|
365
|
+
exit_code: null,
|
|
366
|
+
message: 'no canary recipe declared — this gate is UNPROVEN, not proven working',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const abs = path.join(repoRoot, c.path);
|
|
370
|
+
if (fs.existsSync(abs)) {
|
|
371
|
+
return {
|
|
372
|
+
id: spec.id,
|
|
373
|
+
verdict: 'crashed',
|
|
374
|
+
class: c.class,
|
|
375
|
+
exit_code: null,
|
|
376
|
+
message: `canary path ${c.path} already exists — refusing to overwrite a real file`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
let planted = false;
|
|
380
|
+
// Directories the plant had to create. An empty `src/skills/<x>/` left behind
|
|
381
|
+
// is invisible to `git status` but is still residue, and a skill directory
|
|
382
|
+
// with no SKILL.md is itself a defect the next gate run would report.
|
|
383
|
+
const madeDirs: string[] = [];
|
|
384
|
+
for (let d = path.dirname(abs); !fs.existsSync(d) && d.startsWith(repoRoot); d = path.dirname(d)) {
|
|
385
|
+
madeDirs.push(d);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
389
|
+
fs.writeFileSync(abs, c.content, 'utf8');
|
|
390
|
+
planted = true;
|
|
391
|
+
const runner = path.join(repoRoot, 'scripts-run');
|
|
392
|
+
const r = runCountedProbe(runner, [`src/scripts/${spec.id}`, ...spec.argv], { cwd: repoRoot });
|
|
393
|
+
if (r.failure !== null && r.status === null) {
|
|
394
|
+
return {
|
|
395
|
+
id: spec.id,
|
|
396
|
+
verdict: 'crashed',
|
|
397
|
+
class: c.class,
|
|
398
|
+
exit_code: null,
|
|
399
|
+
message: `gate could not be executed under the plant: ${r.failure}`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const code = r.status ?? 0;
|
|
403
|
+
return code !== 0
|
|
404
|
+
? {
|
|
405
|
+
id: spec.id,
|
|
406
|
+
verdict: 'red',
|
|
407
|
+
class: c.class,
|
|
408
|
+
exit_code: code,
|
|
409
|
+
message: `caught the planted ${c.class} defect (exit ${String(code)})`,
|
|
410
|
+
}
|
|
411
|
+
: {
|
|
412
|
+
id: spec.id,
|
|
413
|
+
verdict: 'green',
|
|
414
|
+
class: c.class,
|
|
415
|
+
exit_code: 0,
|
|
416
|
+
message:
|
|
417
|
+
`stayed GREEN over a planted ${c.class} defect at ${c.path} — ` +
|
|
418
|
+
'the gate is dead by definition. This row is a defect ticket',
|
|
419
|
+
};
|
|
420
|
+
} finally {
|
|
421
|
+
// NEVER SHIPS (adversarial-review-protocol § 6): the plant is removed even
|
|
422
|
+
// when the gate crashes or the process is interrupted mid-run.
|
|
423
|
+
if (planted) {
|
|
424
|
+
try {
|
|
425
|
+
fs.rmSync(abs, { force: true });
|
|
426
|
+
// Innermost first — rmdir only succeeds while the directory is empty,
|
|
427
|
+
// so a dir that gained other content is left alone.
|
|
428
|
+
for (const d of madeDirs) {
|
|
429
|
+
try {
|
|
430
|
+
fs.rmdirSync(d);
|
|
431
|
+
} catch {
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
process.stderr.write(`❌ canary could not remove its plant at ${c.path} — REMOVE IT BY HAND\n`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export interface Disagreement {
|
|
443
|
+
gate: string;
|
|
444
|
+
kind: 'dead_gate' | 'census_stale';
|
|
445
|
+
detail: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Read the scan-scope census into `gate -> units`.
|
|
450
|
+
*
|
|
451
|
+
* Column-position-agnostic: it locates the gate column and the units column by
|
|
452
|
+
* header text, so a census regenerated with a different column set still
|
|
453
|
+
* cross-checks. `null` = the census recorded no measurable count.
|
|
454
|
+
*/
|
|
455
|
+
export function parse_census(md: string): Map<string, number | null> {
|
|
456
|
+
const out = new Map<string, number | null>();
|
|
457
|
+
const rows = md.split('\n').filter((l) => l.trim().startsWith('|'));
|
|
458
|
+
const cells = (l: string): string[] =>
|
|
459
|
+
l.split('|').slice(1, -1).map((c) => c.trim());
|
|
460
|
+
let gateCol = -1;
|
|
461
|
+
let unitCol = -1;
|
|
462
|
+
for (const line of rows) {
|
|
463
|
+
const cs = cells(line);
|
|
464
|
+
const head = cs.map((c) => c.toLowerCase());
|
|
465
|
+
const g = head.findIndex((c) => c === 'gate');
|
|
466
|
+
const u = head.findIndex((c) => c.startsWith('unit'));
|
|
467
|
+
if (g >= 0 && u >= 0) {
|
|
468
|
+
gateCol = g;
|
|
469
|
+
unitCol = u;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (gateCol < 0 || cs.length <= Math.max(gateCol, unitCol)) continue;
|
|
473
|
+
const idm = /`([A-Za-z0-9_.-]+)`/.exec(cs[gateCol] as string);
|
|
474
|
+
if (idm === null) continue;
|
|
475
|
+
const raw = cs[unitCol] as string;
|
|
476
|
+
const num = /(\d[\d,]*)/.exec(raw.replace(/\*/g, ''));
|
|
477
|
+
out.set(idm[1] as string, num === null ? null : Number((num[1] as string).replace(/,/g, '')));
|
|
478
|
+
}
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Make the two artefacts disagree loudly (Phase 7 step 2).
|
|
484
|
+
*
|
|
485
|
+
* The census says what a gate READS; the canary says whether anything it reads
|
|
486
|
+
* can make it FAIL. Each alone is satisfiable by a broken gate; together they
|
|
487
|
+
* are not.
|
|
488
|
+
*/
|
|
489
|
+
export function cross_check(
|
|
490
|
+
results: readonly CanaryResult[],
|
|
491
|
+
census: ReadonlyMap<string, number | null>,
|
|
492
|
+
): Disagreement[] {
|
|
493
|
+
const out: Disagreement[] = [];
|
|
494
|
+
for (const r of results) {
|
|
495
|
+
const units = census.get(r.id);
|
|
496
|
+
if (units === undefined) continue; // not censused — nothing to disagree with
|
|
497
|
+
if (r.verdict === 'green' && units !== null && units > 0) {
|
|
498
|
+
out.push({
|
|
499
|
+
gate: r.id,
|
|
500
|
+
kind: 'dead_gate',
|
|
501
|
+
detail:
|
|
502
|
+
`census records ${String(units)} unit(s) read, but the canary could not make it fail — ` +
|
|
503
|
+
'a gate that reads a live corpus and cannot go red is dead',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (r.verdict === 'red' && (units === null || units === 0)) {
|
|
507
|
+
out.push({
|
|
508
|
+
gate: r.id,
|
|
509
|
+
kind: 'census_stale',
|
|
510
|
+
detail:
|
|
511
|
+
'the canary made it fail, so it reads a live corpus, but the census records no units — ' +
|
|
512
|
+
're-run the census; its row is stale',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const CANARY_ICON: Record<CanaryVerdict, string> = {
|
|
520
|
+
red: '✅',
|
|
521
|
+
green: '❌',
|
|
522
|
+
no_recipe: '⚠️',
|
|
523
|
+
crashed: '❌',
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
export function render_ledger(
|
|
527
|
+
results: readonly CanaryResult[],
|
|
528
|
+
disagreements: readonly Disagreement[],
|
|
529
|
+
totalGateScripts: number,
|
|
530
|
+
cycleId: string,
|
|
531
|
+
): string {
|
|
532
|
+
const recipes = results.filter((r) => r.verdict !== 'no_recipe');
|
|
533
|
+
const red = results.filter((r) => r.verdict === 'red');
|
|
534
|
+
const green = results.filter((r) => r.verdict === 'green');
|
|
535
|
+
const lines = [
|
|
536
|
+
`# Gate-surface canary ledger — ${cycleId}`,
|
|
537
|
+
'',
|
|
538
|
+
'Produced by `./scripts-run src/scripts/check_gate_coverage --canary`. Governed by',
|
|
539
|
+
'[`adversarial-review-protocol`](../../../../docs/contracts/adversarial-review-protocol.md)',
|
|
540
|
+
'§ 6 — biannual cadence, rotating class, sealed record, never ships.',
|
|
541
|
+
'',
|
|
542
|
+
'## Coverage (stated, not implied)',
|
|
543
|
+
'',
|
|
544
|
+
`- Gate scripts in \`src/scripts/\`: **${String(totalGateScripts)}**`,
|
|
545
|
+
`- Listed in \`src/config/gate-coverage.yml\`: **${String(results.length)}**`,
|
|
546
|
+
`- Carrying a canary recipe: **${String(recipes.length)}**`,
|
|
547
|
+
`- RED (caught the plant): **${String(red.length)}** · GREEN (dead): **${String(green.length)}**`,
|
|
548
|
+
'',
|
|
549
|
+
'Every gate outside the recipe count is UNPROVEN by this experiment. That is a',
|
|
550
|
+
'gap, not a pass.',
|
|
551
|
+
'',
|
|
552
|
+
'## Per-gate ledger',
|
|
553
|
+
'',
|
|
554
|
+
'| Gate | Class | Verdict | Exit | Detail |',
|
|
555
|
+
'|---|---|---|---:|---|',
|
|
556
|
+
];
|
|
557
|
+
for (const r of results) {
|
|
558
|
+
lines.push(
|
|
559
|
+
`| \`${r.id}\` | ${r.class ?? '—'} | ${CANARY_ICON[r.verdict]} ${r.verdict.toUpperCase()} ` +
|
|
560
|
+
`| ${r.exit_code === null ? '—' : String(r.exit_code)} | ${r.message} |`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
lines.push('', '## Cross-check against the scan-scope census', '');
|
|
564
|
+
if (disagreements.length === 0) {
|
|
565
|
+
lines.push('No disagreement: every censused gate with a recipe went red, and every red gate');
|
|
566
|
+
lines.push('has a non-zero census count.');
|
|
567
|
+
} else {
|
|
568
|
+
lines.push('| Gate | Kind | Detail |', '|---|---|---|');
|
|
569
|
+
for (const d of disagreements) lines.push(`| \`${d.gate}\` | **${d.kind}** | ${d.detail} |`);
|
|
570
|
+
}
|
|
571
|
+
lines.push('');
|
|
572
|
+
return lines.join('\n');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Count of gate-shaped scripts, for the honest denominator in the ledger. */
|
|
576
|
+
export function count_gate_scripts(dir = path.join(REPO_ROOT, 'src/scripts')): number {
|
|
577
|
+
try {
|
|
578
|
+
return fs
|
|
579
|
+
.readdirSync(dir)
|
|
580
|
+
.filter((f) => /^(lint|check|audit)_.*\.ts$/.test(f) && !f.endsWith('.d.ts')).length;
|
|
581
|
+
} catch {
|
|
582
|
+
return 0;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function run_canary_mode(specs: readonly GateSpec[], argv: readonly string[]): number {
|
|
587
|
+
const results = specs.map((s) => run_canary(s));
|
|
588
|
+
const censusMd = fs.existsSync(CENSUS) ? fs.readFileSync(CENSUS, 'utf8') : '';
|
|
589
|
+
const disagreements = cross_check(results, parse_census(censusMd));
|
|
590
|
+
|
|
591
|
+
for (const r of results) {
|
|
592
|
+
process.stdout.write(` ${CANARY_ICON[r.verdict]} ${r.id}: ${r.message}\n`);
|
|
593
|
+
}
|
|
594
|
+
for (const d of disagreements) {
|
|
595
|
+
process.stdout.write(` ❌ DISAGREEMENT [${d.kind}] ${d.gate}: ${d.detail}\n`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const cycleId = `gate-surface-${new Date().toISOString().slice(0, 10)}`;
|
|
599
|
+
const li = argv.indexOf('--ledger');
|
|
600
|
+
if (li >= 0 && argv[li + 1] !== undefined) {
|
|
601
|
+
const out = path.resolve(REPO_ROOT, argv[li + 1] as string);
|
|
602
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
603
|
+
fs.writeFileSync(out, render_ledger(results, disagreements, count_gate_scripts(), cycleId), 'utf8');
|
|
604
|
+
process.stdout.write(`\nledger: ${path.relative(REPO_ROOT, out)}\n`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const recipes = results.filter((r) => r.verdict !== 'no_recipe').length;
|
|
608
|
+
const green = results.filter((r) => r.verdict === 'green' || r.verdict === 'crashed').length;
|
|
609
|
+
process.stdout.write(
|
|
610
|
+
`\nscanned: ${String(results.length)}\n` +
|
|
611
|
+
` recipes ${String(recipes)} · unproven ${String(results.length - recipes)} · ` +
|
|
612
|
+
`green(dead) ${String(green)} · disagreements ${String(disagreements.length)}\n`,
|
|
613
|
+
);
|
|
614
|
+
if (green > 0 || disagreements.length > 0) {
|
|
615
|
+
process.stdout.write('❌ canary found dead gate(s) or a census disagreement — each row is a defect ticket.\n');
|
|
616
|
+
return 1;
|
|
617
|
+
}
|
|
618
|
+
process.stdout.write('✅ every gate with a recipe went red over its planted defect.\n');
|
|
619
|
+
return 0;
|
|
620
|
+
}
|
|
621
|
+
|
|
286
622
|
function _isCliEntry(): boolean {
|
|
287
623
|
if (process.argv[1] === undefined) return false;
|
|
288
624
|
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
@@ -35,7 +35,8 @@ const GENERATOR_OUTPUT_ROOTS = [
|
|
|
35
35
|
{ root: '.claude/rules', generator: 'generate_rule_symlinks', tool: 'claude-code' },
|
|
36
36
|
{ root: '.claude/personas', generator: 'generate_persona_symlinks', tool: 'claude-code' },
|
|
37
37
|
{ root: '.claude/user-types', generator: 'generate_user_type_symlinks', tool: 'claude-code' },
|
|
38
|
-
{ root: '.claude/skills', generator: 'generate_claude_skills + generate_plugin_command_skills', tool: 'claude-code' },
|
|
38
|
+
{ root: '.claude/skills', generator: 'generate_claude_skills + generate_plugin_command_skills + generate_claude_commands', tool: 'claude-code' },
|
|
39
|
+
{ root: '.claude/commands', generator: 'generate_claude_project_commands', tool: 'claude-code' },
|
|
39
40
|
{ root: '.claude/agents', generator: 'generate_claude_subagents', tool: 'claude-code' },
|
|
40
41
|
{ root: '.augment', generator: 'sync_augment (condense main)', tool: 'augment' },
|
|
41
42
|
{ root: '.cursor/rules', generator: 'generate_cursor_mdc_rules', tool: 'cursor' },
|