@event4u/agent-config 15.0.0 → 16.1.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 +301 -347
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +77 -29
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/user-interaction.md +31 -30
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
- package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
- package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +918 -325
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +5 -3
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +14 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +87 -40
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +2601 -1073
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2008 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1612 -667
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_lock.js +1 -1
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/io/yamlIO.js +50 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +28 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/host-injection-effect.json +1 -1
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/preamble-payload-budget.json +12 -7
- package/src/config/preamble-payload-exceptions.json +28 -0
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_dispatch.bash +3 -1
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/base_ref_payload.ts +315 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/dropped_decision.ts +142 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/installed_lock.ts +1 -1
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
- package/src/scripts/_lib/release_findings_ingest.ts +324 -0
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
- package/src/scripts/_lib/review_baseline.ts +254 -0
- package/src/scripts/_lib/review_skipped_record.ts +61 -1
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +51 -4
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_beta_review_markers.ts +35 -4
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_finding_dispositions.ts +167 -16
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_held_object_arrivals.ts +381 -0
- package/src/scripts/check_preamble_payload_budget.ts +474 -124
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_release_published.ts +1 -1
- package/src/scripts/check_reply_consistency.ts +86 -17
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -12
- package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/review_baseline_hook.ts +130 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
- package/src/scripts/hooks_doctor.ts +89 -1
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +107 -207
- package/src/scripts/interruption_report.ts +8 -2
- package/src/scripts/janitor.ts +15 -1
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_turn_end_gate.ts +302 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +66 -162
- package/src/scripts/release_drill.ts +460 -15
- package/src/scripts/release_env.ts +1 -1
- package/src/scripts/release_publication.ts +52 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_held_object_arrivals.ts +456 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/sync_agent_settings.ts +140 -18
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/io/yamlIO.ts +48 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -3,38 +3,63 @@
|
|
|
3
3
|
*
|
|
4
4
|
* WHY THIS EXISTS
|
|
5
5
|
* ---------------
|
|
6
|
-
* `src/config/preamble-payload-budget.json`
|
|
7
|
-
* never move UP."* `grace_ceiling_history` in the same file
|
|
8
|
-
* twice — 138,212 → 138,273 on 2026-09-02 and 138,273 → 138,490 on
|
|
9
|
-
* each with a careful prose justification. ADR-264 resolved that
|
|
10
|
-
* against the practice: the sentence stands, the two raises are
|
|
11
|
-
* by contradictory wording, and standing-rule growth needs a
|
|
12
|
-
* reduction elsewhere.
|
|
6
|
+
* `src/config/preamble-payload-budget.json` said of its grace ceiling: *"It may
|
|
7
|
+
* never move UP."* `grace_ceiling_history` in the same file recorded it moving
|
|
8
|
+
* up twice — 138,212 → 138,273 on 2026-09-02 and 138,273 → 138,490 on
|
|
9
|
+
* 2026-09-08 — each with a careful prose justification. ADR-264 resolved that
|
|
10
|
+
* contradiction against the practice: the sentence stands, the two raises are
|
|
11
|
+
* violations caused by contradictory wording, and standing-rule growth needs a
|
|
12
|
+
* compensating reduction elsewhere.
|
|
13
13
|
*
|
|
14
14
|
* That decision then had no enforcement. A sentence in a JSON string is not a
|
|
15
|
-
* gate, and the file's own history
|
|
16
|
-
* This module is the enforcement
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* gate, and the file's own history was the evidence: nothing objected either
|
|
16
|
+
* time. This module is the enforcement.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IT BOUNDS NOW — AND WHY THE SUBJECT CHANGED
|
|
19
|
+
* -------------------------------------------------
|
|
20
|
+
* It used to bound one number, `ci_delivery.grace_ceiling`. That key is GONE
|
|
21
|
+
* (`road-to-delivery-for-every-host` 4.4): the ceiling is no longer stored, it
|
|
22
|
+
* is MEASURED at the base ref as `max(design_ceiling, base + active grants)`.
|
|
23
|
+
* A measured ceiling captures every merged reduction automatically, which a
|
|
24
|
+
* stored one never did.
|
|
25
|
+
*
|
|
26
|
+
* Moving the ceiling out of the config did not remove the need for a
|
|
27
|
+
* shrink-only bound — it moved the attack surface. Three numbers in the tree
|
|
28
|
+
* can still widen the effective ceiling without adding a single token of
|
|
29
|
+
* payload, and all three are now bounded:
|
|
30
|
+
*
|
|
31
|
+
* · `design_ceiling`, derived from `baseline_tokens` × `headroom_pct`. It is
|
|
32
|
+
* the FLOOR of the measured ceiling, so raising it raises the ceiling
|
|
33
|
+
* directly. ADR-264's sentence applies to it verbatim.
|
|
34
|
+
* · every existing exception's `granted_tokens` — enlarging a live grant is
|
|
35
|
+
* a raise wearing a break-glass label.
|
|
36
|
+
* · every existing exception's `watermark` — the watermark is what pins the
|
|
37
|
+
* base while a grant is live, so raising it launders the grant into the
|
|
38
|
+
* next base, which is precisely the failure the watermark exists to stop.
|
|
39
|
+
*
|
|
40
|
+
* A NEW exception id is deliberately NOT a violation: recording a fresh,
|
|
41
|
+
* approved, dated grant is the break-glass path working. Editing one that
|
|
42
|
+
* already existed is not.
|
|
20
43
|
*
|
|
21
44
|
* WHAT IT CANNOT DO, STATED PLAINLY
|
|
22
45
|
* ---------------------------------
|
|
23
46
|
* The change under review can edit this file. Both council seats named that as
|
|
24
|
-
* the defect fatal to
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
47
|
+
* the defect fatal to a mechanism that GRANTS budget, and it is not fatal to
|
|
48
|
+
* one that only REFUSES: disabling this check requires visibly editing it, and
|
|
49
|
+
* every gate in this repository already rests on that same review. A ratchet
|
|
50
|
+
* whose only failure mode is "someone deleted the ratchet" is strictly better
|
|
51
|
+
* than the sentence it replaces, which had no failure mode because it did
|
|
52
|
+
* nothing. The 2026-09-10 verdict added one mitigation on top and named its
|
|
53
|
+
* limit in the same breath: CI runs this code AS IT EXISTS AT THE BASE REF, so
|
|
54
|
+
* a same-PR weakening cannot measure its own change — *"the mechanism does not
|
|
55
|
+
* prevent the exploit; it makes the exploit auditable."*
|
|
33
56
|
*/
|
|
34
57
|
import { spawnSync } from 'node:child_process';
|
|
35
58
|
|
|
36
|
-
/** Repo-relative path of the budget config the
|
|
59
|
+
/** Repo-relative path of the budget config the bounds live in. */
|
|
37
60
|
export const BUDGET_CONFIG_PATH = 'src/config/preamble-payload-budget.json';
|
|
61
|
+
/** Repo-relative path of the break-glass ledger the grant bounds live in. */
|
|
62
|
+
export const EXCEPTIONS_CONFIG_PATH = 'src/config/preamble-payload-exceptions.json';
|
|
38
63
|
|
|
39
64
|
/** Injection seam: run git, capture output, never throw on a non-zero exit. */
|
|
40
65
|
export type GitRunner = (args: readonly string[], cwd: string) => {
|
|
@@ -48,28 +73,68 @@ export const realGit: GitRunner = (args, cwd) => {
|
|
|
48
73
|
return { ok: r.status === 0, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
|
|
49
74
|
};
|
|
50
75
|
|
|
76
|
+
/** Named shrink-only numbers, keyed by a stable bound id. */
|
|
77
|
+
export type BoundSet = Record<string, number>;
|
|
78
|
+
|
|
51
79
|
export interface BoundsRatchetVerdict {
|
|
52
80
|
ok: boolean;
|
|
53
81
|
violations: string[];
|
|
54
|
-
/** The
|
|
55
|
-
|
|
82
|
+
/** The bounds read at the base ref, or `null` when they could not be read. */
|
|
83
|
+
baseBounds: BoundSet | null;
|
|
56
84
|
/** The ref compared against, or `null` when none resolved. */
|
|
57
85
|
baseRef: string | null;
|
|
58
86
|
/** Why the comparison was skipped, when it was. Printed, never silent. */
|
|
59
87
|
note: string | null;
|
|
60
88
|
/**
|
|
61
|
-
* `false` means the
|
|
89
|
+
* `false` means the bounds could not be READ, not that one rose.
|
|
62
90
|
*
|
|
63
91
|
* Two refusals reach a caller with `ok: false` and they need opposite
|
|
64
|
-
* actions: a risen
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* `ok: false` that predates this field is a genuine rise.
|
|
92
|
+
* actions: a risen bound is fixed by lowering the addition, an unverifiable
|
|
93
|
+
* one by repairing the checkout. A completion review caught both being
|
|
94
|
+
* rendered as "ROSE", which would send an operator to shrink a rule over
|
|
95
|
+
* what was a fetch problem.
|
|
69
96
|
*/
|
|
70
97
|
verified: boolean;
|
|
71
98
|
}
|
|
72
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The bound map, derived identically from head and base configs.
|
|
102
|
+
*
|
|
103
|
+
* ONE function for both sides, on purpose: two derivations of one boundary is
|
|
104
|
+
* the drift shape that let the grace ceiling rise twice under a sentence
|
|
105
|
+
* forbidding it. `budget` and `exceptions` are parsed JSON, or `null` when the
|
|
106
|
+
* file is absent — an absent exceptions ledger contributes no bounds, which is
|
|
107
|
+
* correct: there are no grants to protect.
|
|
108
|
+
*/
|
|
109
|
+
export function boundsFrom(budget: unknown, exceptions: unknown): BoundSet | null {
|
|
110
|
+
const b = (budget ?? null) as Record<string, unknown> | null;
|
|
111
|
+
if (b === null) return null;
|
|
112
|
+
const baseline = Number(b['baseline_tokens']);
|
|
113
|
+
const headroom = Number(b['headroom_pct']);
|
|
114
|
+
if (!Number.isFinite(baseline) || !Number.isFinite(headroom)) return null;
|
|
115
|
+
// `design_ceiling` is the whole config-side bound now. The retained stored
|
|
116
|
+
// allowance stage 1 kept beside it was deleted in stage 2 (ADR-276), so a
|
|
117
|
+
// tree at or below design is bounded by this number and a tree above it is
|
|
118
|
+
// bounded by its own base measurement — neither of which lives in a field
|
|
119
|
+
// anyone can edit upward.
|
|
120
|
+
const out: BoundSet = { design_ceiling: Math.round(baseline * (1 + headroom / 100)) };
|
|
121
|
+
|
|
122
|
+
const ex = (exceptions ?? null) as Record<string, unknown> | null;
|
|
123
|
+
const list = ex === null ? null : ex['exceptions'];
|
|
124
|
+
if (Array.isArray(list)) {
|
|
125
|
+
for (const item of list) {
|
|
126
|
+
const e = (item ?? {}) as Record<string, unknown>;
|
|
127
|
+
const id = e['id'];
|
|
128
|
+
if (typeof id !== 'string' || id.trim() === '') continue;
|
|
129
|
+
const granted = Number(e['granted_tokens']);
|
|
130
|
+
const watermark = Number(e['watermark']);
|
|
131
|
+
if (Number.isFinite(granted)) out[`exception:${id}:granted_tokens`] = granted;
|
|
132
|
+
if (Number.isFinite(watermark)) out[`exception:${id}:watermark`] = watermark;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
73
138
|
function showAtRef(git: GitRunner, repoRoot: string, ref: string, relPath: string): string | null {
|
|
74
139
|
const r = git(['show', `${ref}:${relPath}`], repoRoot);
|
|
75
140
|
return r.ok ? r.stdout : null;
|
|
@@ -78,29 +143,24 @@ function showAtRef(git: GitRunner, repoRoot: string, ref: string, relPath: strin
|
|
|
78
143
|
/**
|
|
79
144
|
* A condition that stops the comparison: skip when advisory, refuse when not.
|
|
80
145
|
*
|
|
81
|
-
* One helper for all
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
146
|
+
* One helper for all causes rather than an inline branch each, so the posture
|
|
147
|
+
* cannot drift between them — the failure this closes was four independent
|
|
148
|
+
* `ok: true` returns that all had to be found before any of them could be
|
|
149
|
+
* trusted.
|
|
85
150
|
*/
|
|
86
|
-
function unverified(
|
|
87
|
-
reason: string,
|
|
88
|
-
requireBase: boolean,
|
|
89
|
-
baseGraceCeiling: number | null,
|
|
90
|
-
baseRef: string | null,
|
|
91
|
-
): BoundsRatchetVerdict {
|
|
151
|
+
function unverified(reason: string, requireBase: boolean, baseRef: string | null): BoundsRatchetVerdict {
|
|
92
152
|
if (requireBase) {
|
|
93
153
|
return {
|
|
94
154
|
ok: false,
|
|
95
155
|
verified: false,
|
|
96
|
-
|
|
156
|
+
baseBounds: null,
|
|
97
157
|
baseRef,
|
|
98
|
-
note: `${reason}, so the shrink-only
|
|
158
|
+
note: `${reason}, so the shrink-only bounds were NOT verified in this run`,
|
|
99
159
|
violations: [
|
|
100
|
-
`the shrink-only
|
|
160
|
+
`the shrink-only bounds could not be verified: ${reason}. This run is `
|
|
101
161
|
+ 'ENFORCING, so an unverifiable bound refuses rather than skips — an '
|
|
102
162
|
+ 'unreadable base would otherwise grant budget on an infrastructure '
|
|
103
|
-
+ 'failure. This is NOT a
|
|
163
|
+
+ 'failure. This is NOT a bound rise: fix the checkout (a full fetch '
|
|
104
164
|
+ 'of the base ref) rather than lowering the addition.',
|
|
105
165
|
],
|
|
106
166
|
};
|
|
@@ -109,33 +169,29 @@ function unverified(
|
|
|
109
169
|
ok: true,
|
|
110
170
|
verified: false,
|
|
111
171
|
violations: [],
|
|
112
|
-
|
|
172
|
+
baseBounds: null,
|
|
113
173
|
baseRef,
|
|
114
|
-
note: `${reason}, so the shrink-only
|
|
174
|
+
note: `${reason}, so the shrink-only bounds were NOT verified in this run`,
|
|
115
175
|
};
|
|
116
176
|
}
|
|
117
177
|
|
|
118
178
|
/**
|
|
119
|
-
* Compare
|
|
179
|
+
* Compare every shrink-only bound in this tree against the one at the base ref.
|
|
120
180
|
*
|
|
121
181
|
* TWO POSTURES, AND WHICH ONE APPLIES IS THE CALLER'S TO DECLARE.
|
|
122
182
|
*
|
|
123
|
-
* Advisory (the default
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* built.
|
|
183
|
+
* Advisory (the default): a base ref or base config that cannot be read SKIPS,
|
|
184
|
+
* reported in both the human and JSON output so it cannot pass for a check.
|
|
185
|
+
* That default is not laziness and the original reasoning still holds — a
|
|
186
|
+
* shallow clone, a first commit and a detached build all legitimately have no
|
|
187
|
+
* base, and a gate that reds on them gets switched off, which is how a
|
|
188
|
+
* repository loses the enforcement it just built.
|
|
130
189
|
*
|
|
131
190
|
* Enforcing (`requireBase: true`): the same conditions REFUSE. An AI council
|
|
132
191
|
* (2/2, 2026-09-10) made this blocking for the move to a base-measured ceiling,
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* infrastructure failure — the exact fail-open the reserve decision refused.
|
|
137
|
-
* anthropic's wording was "replaced or mode-gated"; this is the mode gate, so
|
|
138
|
-
* that no existing caller silently changes behaviour.
|
|
192
|
+
* because the asymmetry inverts once the base ref stops being a cross-check and
|
|
193
|
+
* becomes the ceiling itself. Today an unreadable base costs a comparison;
|
|
194
|
+
* there it would grant an unbounded budget on an infrastructure failure.
|
|
139
195
|
*
|
|
140
196
|
* The flag is the caller's rather than derived from `GITHUB_ACTIONS` here: this
|
|
141
197
|
* module cannot tell an enforcing CI run from a diagnostic one on the same
|
|
@@ -154,67 +210,124 @@ export function assertBoundsDidNotRise(opts: {
|
|
|
154
210
|
*/
|
|
155
211
|
requireBase?: boolean;
|
|
156
212
|
/**
|
|
157
|
-
* The
|
|
213
|
+
* The shrink-only numbers as they stand in THIS tree.
|
|
158
214
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
215
|
+
* Passed in rather than re-read here so head and base go through
|
|
216
|
+
* `boundsFrom` from the caller's own configuration objects — including an
|
|
217
|
+
* effective value the caller may have taken from a flag rather than the
|
|
218
|
+
* file, which is the second path a bound could otherwise be widened by.
|
|
163
219
|
*/
|
|
164
|
-
|
|
220
|
+
headBounds: BoundSet;
|
|
165
221
|
}): BoundsRatchetVerdict {
|
|
166
222
|
const git = opts.git ?? realGit;
|
|
167
223
|
const requireBase = opts.requireBase === true;
|
|
168
224
|
if (opts.baseRef === null || opts.baseRef.trim() === '') {
|
|
169
|
-
return unverified('no base ref resolved', requireBase, null
|
|
225
|
+
return unverified('no base ref resolved', requireBase, null);
|
|
170
226
|
}
|
|
171
227
|
const cfg = showAtRef(git, opts.repoRoot, opts.baseRef, BUDGET_CONFIG_PATH);
|
|
172
228
|
if (cfg === null) {
|
|
173
|
-
return unverified(
|
|
174
|
-
`${BUDGET_CONFIG_PATH} could not be read at ${opts.baseRef}`,
|
|
175
|
-
requireBase,
|
|
176
|
-
null,
|
|
177
|
-
opts.baseRef,
|
|
178
|
-
);
|
|
229
|
+
return unverified(`${BUDGET_CONFIG_PATH} could not be read at ${opts.baseRef}`, requireBase, opts.baseRef);
|
|
179
230
|
}
|
|
180
|
-
let
|
|
231
|
+
let baseBudget: unknown;
|
|
181
232
|
try {
|
|
182
|
-
|
|
183
|
-
const delivery = (parsed['ci_delivery'] ?? {}) as Record<string, unknown>;
|
|
184
|
-
const n = Number(delivery['grace_ceiling']);
|
|
185
|
-
baseGrace = Number.isFinite(n) ? n : null;
|
|
233
|
+
baseBudget = JSON.parse(cfg);
|
|
186
234
|
} catch {
|
|
235
|
+
return unverified(`${BUDGET_CONFIG_PATH} at ${opts.baseRef} is not parseable`, requireBase, opts.baseRef);
|
|
236
|
+
}
|
|
237
|
+
// An absent exceptions ledger at the base ref is normal — it did not exist
|
|
238
|
+
// before this mechanism landed, and a ref predating it has no grants to
|
|
239
|
+
// protect. An unparseable one is not, and refuses like a bad budget.
|
|
240
|
+
const exRaw = showAtRef(git, opts.repoRoot, opts.baseRef, EXCEPTIONS_CONFIG_PATH);
|
|
241
|
+
let baseExceptions: unknown = null;
|
|
242
|
+
if (exRaw !== null) {
|
|
243
|
+
try {
|
|
244
|
+
baseExceptions = JSON.parse(exRaw);
|
|
245
|
+
} catch {
|
|
246
|
+
return unverified(
|
|
247
|
+
`${EXCEPTIONS_CONFIG_PATH} at ${opts.baseRef} is not parseable`,
|
|
248
|
+
requireBase,
|
|
249
|
+
opts.baseRef,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const baseBounds = boundsFrom(baseBudget, baseExceptions);
|
|
255
|
+
if (baseBounds === null) {
|
|
187
256
|
return unverified(
|
|
188
|
-
|
|
257
|
+
`there are no shrink-only bounds at ${opts.baseRef} to ratchet against`,
|
|
189
258
|
requireBase,
|
|
190
|
-
null,
|
|
191
259
|
opts.baseRef,
|
|
192
260
|
);
|
|
193
261
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
262
|
+
|
|
263
|
+
const violations: string[] = [];
|
|
264
|
+
for (const [name, head] of Object.entries(opts.headBounds)) {
|
|
265
|
+
const base = baseBounds[name];
|
|
266
|
+
// A bound absent at the base is a NEW one — a freshly recorded grant,
|
|
267
|
+
// which is the break-glass path working rather than a rise.
|
|
268
|
+
if (base === undefined) continue;
|
|
269
|
+
if (head > base) violations.push(explain(name, base, head, opts.baseRef));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// DELETION CANNOT DISCHARGE DEBT. A grant that existed at the base and is
|
|
273
|
+
// gone at head was not repaid, it was erased — the 2026-09-11 council's
|
|
274
|
+
// blocking finding #3, and the one failure a shrink-only comparison misses
|
|
275
|
+
// by construction, because a removed number is not a smaller number.
|
|
276
|
+
// Only `exception:` bounds are checked for deletion: `design_ceiling` is
|
|
277
|
+
// always present, and the stored allowance that used to need an exemption
|
|
278
|
+
// here no longer exists.
|
|
279
|
+
const reportedDeletions = new Set<string>();
|
|
280
|
+
for (const name of Object.keys(baseBounds)) {
|
|
281
|
+
if (!name.startsWith('exception:')) continue;
|
|
282
|
+
if (opts.headBounds[name] !== undefined) continue;
|
|
283
|
+
const m = /^exception:(.+):(?:granted_tokens|watermark)$/.exec(name);
|
|
284
|
+
const id = m === null ? name : String(m[1]);
|
|
285
|
+
// One record carries two bounds; report the record once.
|
|
286
|
+
if (reportedDeletions.has(id)) continue;
|
|
287
|
+
reportedDeletions.add(id);
|
|
288
|
+
violations.push(
|
|
289
|
+
`standing-payload exception '${id}' existed at ${opts.baseRef} and is gone at head. A deleted ` +
|
|
290
|
+
'record is not a repaid one: repayment is established by the payload returning to the ' +
|
|
291
|
+
'watermark, and erasing the entry erases the debt along with the evidence of it. Mark it ' +
|
|
292
|
+
'repaid once the measurement supports that, or leave it in place.',
|
|
200
293
|
);
|
|
201
294
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
295
|
+
|
|
296
|
+
if (violations.length > 0) {
|
|
297
|
+
return { ok: false, verified: true, baseBounds, baseRef: opts.baseRef, note: null, violations };
|
|
298
|
+
}
|
|
299
|
+
return { ok: true, verified: true, violations: [], baseBounds, baseRef: opts.baseRef, note: null };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** One refusal message per bound kind — a generic "a bound rose" sends the
|
|
303
|
+
* reader nowhere, and the three kinds have three different fixes. */
|
|
304
|
+
function explain(name: string, base: number, head: number, baseRef: string): string {
|
|
305
|
+
const rose = `rose from ${String(base)} to ${String(head)} against ${baseRef}`;
|
|
306
|
+
if (name === 'design_ceiling') {
|
|
307
|
+
return (
|
|
308
|
+
`the standing-payload design ceiling ${rose}. It derives from baseline_tokens × ` +
|
|
309
|
+
`headroom_pct in ${BUDGET_CONFIG_PATH}, and it is the FLOOR of the measured ceiling, so ` +
|
|
310
|
+
'raising it widens every future budget without adding a token of payload. ADR-264 decided ' +
|
|
311
|
+
'that standing-rule growth needs a compensating reduction elsewhere, not a bigger ceiling. ' +
|
|
312
|
+
'Lower the addition, migrate the prose out of the standing rule, or reduce another standing ' +
|
|
313
|
+
'asset by the same amount.'
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const m = /^exception:(.+):(granted_tokens|watermark)$/.exec(name);
|
|
317
|
+
if (m !== null && m[2] === 'granted_tokens') {
|
|
318
|
+
return (
|
|
319
|
+
`standing-payload exception '${String(m[1])}' had its grant ${rose}. Enlarging a grant that ` +
|
|
320
|
+
'already exists is a ceiling raise wearing a break-glass label. Record a NEW approved, dated ' +
|
|
321
|
+
'exception instead, so the enlargement carries its own approver, reason and expiry.'
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (m !== null) {
|
|
325
|
+
return (
|
|
326
|
+
`standing-payload exception '${String(m[1])}' had its watermark ${rose}. The watermark pins the ` +
|
|
327
|
+
'base while the grant is live, and raising it launders the granted tokens into the next base — ' +
|
|
328
|
+
'which is exactly the failure the watermark exists to prevent. It is the payload measured ' +
|
|
329
|
+
'BEFORE the grant and does not change afterwards.'
|
|
330
|
+
);
|
|
218
331
|
}
|
|
219
|
-
return
|
|
332
|
+
return `the shrink-only bound '${name}' ${rose}, and ${BUDGET_CONFIG_PATH} says it may not move upward.`;
|
|
220
333
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared classification for the two test gates —
|
|
3
|
+
* `road-to-adversarial-verification-and-long-runs` Phase 2.4.
|
|
4
|
+
*
|
|
5
|
+
* `check_test_delta` and `check_test_weakening` ask different questions about
|
|
6
|
+
* the same diff, and both need the same answer to "is this path code?" and "is
|
|
7
|
+
* this path a test?". Two copies of that answer is how one gate starts counting
|
|
8
|
+
* a file the other does not, so the predicates live here once.
|
|
9
|
+
*
|
|
10
|
+
* Pure, and `node:`-free, so either gate and their tests may import it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** A changed path that is production code this repository ships or runs. */
|
|
14
|
+
export function isCodePath(p: string): boolean {
|
|
15
|
+
const f = p.replace(/\\/g, '/');
|
|
16
|
+
if (isTestPath(f)) return false;
|
|
17
|
+
if (!/\.(ts|tsx|js|mjs|cjs)$/.test(f)) return false;
|
|
18
|
+
if (f.endsWith('.d.ts')) return false;
|
|
19
|
+
// Generated trees are projections of `src/`, never an independent change.
|
|
20
|
+
if (/^(dist|\.augment|\.claude|\.cursor|node_modules)\//.test(f)) return false;
|
|
21
|
+
return /^(src|scripts)\//.test(f);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A changed path that carries tests. */
|
|
25
|
+
export function isTestPath(p: string): boolean {
|
|
26
|
+
const f = p.replace(/\\/g, '/');
|
|
27
|
+
if (/^tests\//.test(f)) return true;
|
|
28
|
+
return /\.(test|spec)\.(ts|tsx|js|mjs|cjs)$/.test(f);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An independent-verdict artefact — the thing that makes a weakening legitimate.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately a PATH test and not a content test. Whether the artefact actually
|
|
35
|
+
* contains an independent verdict is exactly the judgement `evaluator-independence`
|
|
36
|
+
* says no gate can make, so this gate checks that one was COMMITTED and leaves
|
|
37
|
+
* its quality to the review. Claiming more would be the coverage inflation that
|
|
38
|
+
* rule was written over.
|
|
39
|
+
*/
|
|
40
|
+
export function isVerdictArtefact(p: string): boolean {
|
|
41
|
+
const f = p.replace(/\\/g, '/');
|
|
42
|
+
return /^agents\/evidence\/reviews\//.test(f) && f.endsWith('.md');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One file's hunks, as a unified diff splits them. */
|
|
46
|
+
export interface FileDiff {
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly added: readonly string[];
|
|
49
|
+
readonly removed: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Split a unified diff into per-file added/removed line lists.
|
|
54
|
+
*
|
|
55
|
+
* Tolerant by construction: an unparseable header is skipped rather than thrown
|
|
56
|
+
* on, because a gate that crashes on an unusual diff blocks the fix for the
|
|
57
|
+
* diff that produced it. `+++ b/<path>` is the authority for the path — a rename
|
|
58
|
+
* shows the destination, which is the file whose assertions now count.
|
|
59
|
+
*/
|
|
60
|
+
export function parseUnifiedDiff(diff: string): FileDiff[] {
|
|
61
|
+
const out: FileDiff[] = [];
|
|
62
|
+
let path: string | null = null;
|
|
63
|
+
let oldPath: string | null = null;
|
|
64
|
+
let added: string[] = [];
|
|
65
|
+
let removed: string[] = [];
|
|
66
|
+
const flush = (): void => {
|
|
67
|
+
if (path !== null) out.push({ path, added, removed });
|
|
68
|
+
added = [];
|
|
69
|
+
removed = [];
|
|
70
|
+
};
|
|
71
|
+
for (const line of diff.split('\n')) {
|
|
72
|
+
if (line.startsWith('diff --git ')) {
|
|
73
|
+
flush();
|
|
74
|
+
path = null;
|
|
75
|
+
oldPath = null;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (line.startsWith('--- ')) {
|
|
79
|
+
const p = line.slice(4).trim();
|
|
80
|
+
oldPath = p === '/dev/null' ? null : p.replace(/^a\//, '');
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (line.startsWith('+++ ')) {
|
|
84
|
+
const p = line.slice(4).trim();
|
|
85
|
+
// A DELETION (`+++ /dev/null`) keeps the OLD path rather than
|
|
86
|
+
// dropping the file. Reported by an independent review of the change
|
|
87
|
+
// that introduced this parser: deleting a test file is the maximal
|
|
88
|
+
// form of the weakening this gate exists to see, and `git rm
|
|
89
|
+
// tests/x.test.ts` was the cheapest way past it — the deletion even
|
|
90
|
+
// counted as a `tests/` path for the sibling delta gate. A file whose
|
|
91
|
+
// every assertion is removed at once is not zero signal.
|
|
92
|
+
path = p === '/dev/null' ? oldPath : p.replace(/^b\//, '');
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (line.startsWith('@@')) continue;
|
|
96
|
+
if (path === null) continue;
|
|
97
|
+
if (line.startsWith('+')) added.push(line.slice(1));
|
|
98
|
+
else if (line.startsWith('-')) removed.push(line.slice(1));
|
|
99
|
+
}
|
|
100
|
+
flush();
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Lines that assert something.
|
|
106
|
+
*
|
|
107
|
+
* Framework-spread on purpose — this suite runs vitest, and the rule it enforces
|
|
108
|
+
* ships to consumers running pytest, PHPUnit and Go. A gate that only recognises
|
|
109
|
+
* one framework reports every other project as assertion-free, which is a false
|
|
110
|
+
* green and worse than no gate.
|
|
111
|
+
*/
|
|
112
|
+
const ASSERTION = /\b(expect|assert|assertThat|self\.assert|should\.|\.toBe|\.toEqual|\.toMatch|assertEquals|assertTrue|require\.(New|Equal|NoError))/;
|
|
113
|
+
|
|
114
|
+
/** Lines that disable a test rather than assert with it. */
|
|
115
|
+
const SUPPRESSION =
|
|
116
|
+
/(\b(it|test|describe|context)\s*\.\s*(skip|todo|failing)\b|\bxit\b|\bxdescribe\b|@pytest\.mark\.(skip|xfail)|\bt\.Skip\(|->markTestSkipped\(|@group\s+skip)/;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A line whose match is DATA, not code — a diff line held inside a string.
|
|
120
|
+
*
|
|
121
|
+
* Found by this gate reporting its own test file: a suite that tests a
|
|
122
|
+
* suppression detector necessarily contains suppression-shaped strings as
|
|
123
|
+
* fixtures (`'+@pytest.mark.skip\n'`), and counting those made the gate report
|
|
124
|
+
* the one file whose job is to prove it works. Generalised rather than
|
|
125
|
+
* special-cased to that path — every gate that reads diffs will have a test
|
|
126
|
+
* suite shaped exactly this way, and "the gate exempts its own tests" is the
|
|
127
|
+
* carve-out that makes a detector unfalsifiable.
|
|
128
|
+
*
|
|
129
|
+
* The discriminator is a quote immediately followed by a diff marker: a string
|
|
130
|
+
* literal whose content begins `+` or `-`. Real test code does not open a string
|
|
131
|
+
* with a diff marker; a diff fixture does nothing else.
|
|
132
|
+
*/
|
|
133
|
+
const DIFF_LINE_AS_DATA = /['"`][+-]/;
|
|
134
|
+
|
|
135
|
+
/** Counted evidence about one test file's diff. */
|
|
136
|
+
export interface WeakeningSignal {
|
|
137
|
+
readonly path: string;
|
|
138
|
+
/** Assertions removed minus assertions added. Positive means a net loss. */
|
|
139
|
+
readonly assertionsLost: number;
|
|
140
|
+
/** Suppressions added minus suppressions removed. Positive means new skips. */
|
|
141
|
+
readonly suppressionsGained: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Measure a test file's diff as NET counts, never as raw removals.
|
|
146
|
+
*
|
|
147
|
+
* This is the discriminator the gate lives or dies on. Editing an assertion
|
|
148
|
+
* removes one line and adds another, so a gate counting raw removals reds every
|
|
149
|
+
* legitimate test edit — and a gate that reds every PR gets its exemption widened
|
|
150
|
+
* until it finds nothing, which is Risk 4 of the roadmap that commissioned it.
|
|
151
|
+
* A NET loss is a different claim: the file asserts less than it did.
|
|
152
|
+
*/
|
|
153
|
+
export function weakeningSignal(file: FileDiff): WeakeningSignal {
|
|
154
|
+
const count = (lines: readonly string[], re: RegExp): number =>
|
|
155
|
+
lines.filter((l) => re.test(l) && !DIFF_LINE_AS_DATA.test(l)).length;
|
|
156
|
+
return {
|
|
157
|
+
path: file.path,
|
|
158
|
+
assertionsLost: count(file.removed, ASSERTION) - count(file.added, ASSERTION),
|
|
159
|
+
suppressionsGained: count(file.added, SUPPRESSION) - count(file.removed, SUPPRESSION),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** True when a signal is worth reporting at all. */
|
|
164
|
+
export function isWeakening(s: WeakeningSignal): boolean {
|
|
165
|
+
return s.assertionsLost > 0 || s.suppressionsGained > 0;
|
|
166
|
+
}
|