@event4u/agent-config 14.17.0 → 14.19.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 +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* detect_verification_tampering — did the path to green weaken the verification?
|
|
4
|
+
*
|
|
5
|
+
* `verify-repair-loop` states the gap it cannot close in its own text: a loop
|
|
6
|
+
* optimising for `pass_count >= N` "can game the metric — delete tests, skip
|
|
7
|
+
* assertions, weaken checks — and still hit the threshold" (`SKILL.md:53-54`),
|
|
8
|
+
* and "the numeric gate cannot catch this" (`:185`). Its answer is a
|
|
9
|
+
* fresh-context judge, which is a good answer and a probabilistic one. This is
|
|
10
|
+
* the deterministic half: given the diff between a failing run and the next
|
|
11
|
+
* attempt, decide by shape whether the verification got weaker.
|
|
12
|
+
*
|
|
13
|
+
* No model call. No network. The input is a unified diff, a commit message, and
|
|
14
|
+
* optionally a recorded runner verdict.
|
|
15
|
+
*
|
|
16
|
+
* THIS IS NOT A CI GATE, AND THE NAME SAYS SO.
|
|
17
|
+
*
|
|
18
|
+
* It is `detect_*`, not `check_*` / `lint_*`, and that is a claim about scope
|
|
19
|
+
* rather than a way around a counter. `_lib/gate_population.ts` classifies a
|
|
20
|
+
* script as gate-shaped by the prefixes `lint|check|audit|skill|verify`, and
|
|
21
|
+
* this script is deliberately outside that set for the reason
|
|
22
|
+
* `road-to-deterministic-defect-detectors` step 1.3 states: the tree's own
|
|
23
|
+
* measured lesson is that reach, not content, is the scarce thing, so a new
|
|
24
|
+
* always-on gate is the opposite of the lesson. It is invoked BY THE LOOP, on
|
|
25
|
+
* the loop's own diff, and it is registered in no CI task closure — so it adds
|
|
26
|
+
* no row to `gate-coverage.yml` and no member to the `check_gate_completeness`
|
|
27
|
+
* population. Promoting it is a later decision that owes a measured
|
|
28
|
+
* false-positive rate first, exactly as step 3.2 requires of its sibling.
|
|
29
|
+
*
|
|
30
|
+
* SELF-HONESTY.
|
|
31
|
+
*
|
|
32
|
+
* This detector must not commit the defects it detects. It contains no
|
|
33
|
+
* `catch` that discards its error, no skipped test, and no assertion it
|
|
34
|
+
* weakened to pass: `--self-test` drives the real CLI over the committed
|
|
35
|
+
* corpus and refuses to pass below a floor of cases, so deleting a case is
|
|
36
|
+
* not a route to green.
|
|
37
|
+
*
|
|
38
|
+
* CLI:
|
|
39
|
+
* detect_verification_tampering --diff <file> [--message <s>|--message-file <f>]
|
|
40
|
+
* detect_verification_tampering --diff <file> --verdict <verdict.json>
|
|
41
|
+
* detect_verification_tampering --corpus # run the whole fixture manifest
|
|
42
|
+
* detect_verification_tampering --self-test # drive the real CLI over it
|
|
43
|
+
*
|
|
44
|
+
* Exit codes — the outcome, not just pass/fail:
|
|
45
|
+
* 0 clean · 1 violations (a `block` finding) · 2 usage error or crash
|
|
46
|
+
* 3 warn-only violations · 4 stale_verdict
|
|
47
|
+
*/
|
|
48
|
+
import * as fs from 'node:fs';
|
|
49
|
+
import * as path from 'node:path';
|
|
50
|
+
import process from 'node:process';
|
|
51
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
52
|
+
|
|
53
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
54
|
+
import { parseUnifiedDiff, type DiffFile, type DiffHunk, type DiffLine } from './_lib/unified_diff.js';
|
|
55
|
+
import {
|
|
56
|
+
assertWellFormed,
|
|
57
|
+
detectorSpec,
|
|
58
|
+
locate,
|
|
59
|
+
parseAllowDeclarations,
|
|
60
|
+
type LoopOutcome,
|
|
61
|
+
type TamperDetectorId,
|
|
62
|
+
type TamperFinding,
|
|
63
|
+
} from './_lib/tamper_vocabulary.js';
|
|
64
|
+
|
|
65
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
66
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
67
|
+
const CORPUS = path.join(REPO_ROOT, 'tests/fixtures/tamper-corpus');
|
|
68
|
+
const SELF_TEST_MIN_CASES = 14;
|
|
69
|
+
const SELF_TEST_MIN_REJECT = 7;
|
|
70
|
+
|
|
71
|
+
/* ------------------------------------------------------------------ shapes */
|
|
72
|
+
|
|
73
|
+
/** A line that asserts something, in any of the stacks this tree touches. */
|
|
74
|
+
const ASSERTION_RE =
|
|
75
|
+
/(\bexpect\s*\(|\bassert[A-Za-z]*\s*\(|\bassert\s+|->assert[A-Za-z]*\s*\(|\bself\.assert[A-Za-z]*\s*\(|\.should\b|\bt\.(?:Error|Fatal)f?\s*\(|\brequire\.[A-Za-z]+\s*\()/;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Assertion strength, high to low. The ladder is the whole of
|
|
79
|
+
* `assertion-weakened`: an exact-value check that becomes an existence check
|
|
80
|
+
* still passes, and still passes for the broken output.
|
|
81
|
+
*/
|
|
82
|
+
const STRENGTH_TIERS: ReadonlyArray<readonly [number, RegExp]> = [
|
|
83
|
+
[
|
|
84
|
+
3,
|
|
85
|
+
/\.(?:toBe|toEqual|toStrictEqual|toHaveBeenCalledWith)\s*\(|\bassert(?:Equals|Same|Is|Identical)\s*\(|\bassertEquals\b|\bassert\s+[^=<>!]+==/,
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
2,
|
|
89
|
+
/\.(?:toContain|toMatch|toHaveLength|toBeGreaterThan|toBeLessThan|toBeCloseTo)\s*\(|\bassert(?:Contains|Count|GreaterThan|LessThan|Matches)\s*\(/,
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
1,
|
|
93
|
+
/\.(?:toBeDefined|toBeTruthy|toBeFalsy|toBeNull|toBeUndefined|toBeInstanceOf)\s*\(|\bassert(?:NotNull|NotEmpty|True|False|IsNotNone|IsNone)\s*\(/,
|
|
94
|
+
],
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const SKIP_RE =
|
|
98
|
+
/(\b(?:it|test|describe|context|suite)\s*\.\s*(?:skip|only)\s*\(|\bx(?:it|describe|test)\s*\(|@pytest\.mark\.(?:skip|skipif|xfail)|@unittest\.skip|->markTestSkipped\s*\(|\bt\.Skip(?:Now)?\s*\(|#\[ignore\]|\.only\s*\()/;
|
|
99
|
+
|
|
100
|
+
/** The WHOLE line is an unconditional exit. A guarded `if (x) return;` is not. */
|
|
101
|
+
const UNCONDITIONAL_EXIT_RE =
|
|
102
|
+
/^\s*(?:return(?:\s+(?:true|None|nil|null))?\s*;?|process\.exit\(\s*0\s*\)\s*;?|sys\.exit\(\s*0?\s*\)|os\.Exit\(\s*0\s*\)\s*;?|exit\(\s*0\s*\)\s*;?)\s*$/;
|
|
103
|
+
|
|
104
|
+
const MOCK_RE =
|
|
105
|
+
/(\b(?:vi|jest)\s*\.\s*mock\s*\(|\bsinon\s*\.\s*(?:stub|mock)\s*\(|\bmock\.patch\s*\(|@patch\s*\(|monkeypatch\.setattr\s*\(|Mockery::mock\s*\(|->shouldReceive\s*\(|->createMock\s*\(|\bcreateMock\s*\()/;
|
|
106
|
+
|
|
107
|
+
/** Identifiers that appear in every mock call and prove nothing. */
|
|
108
|
+
const MOCK_API_WORDS = new Set([
|
|
109
|
+
'mock', 'jest', 'sinon', 'patch', 'stub', 'vitest', 'monkeypatch', 'setattr',
|
|
110
|
+
'Mockery', 'createMock', 'shouldReceive', 'return', 'const', 'function',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const TEST_PATH_RE =
|
|
114
|
+
/(^|\/)(?:tests?|spec|specs|__tests__)\//i;
|
|
115
|
+
const TEST_FILE_RE =
|
|
116
|
+
/(?:[._-](?:test|spec)\.[A-Za-z]+$|(?:^|\/)test_[^/]+\.py$|Test\.php$|_test\.go$)/;
|
|
117
|
+
|
|
118
|
+
function isTestPath(p: string): boolean {
|
|
119
|
+
return TEST_PATH_RE.test(p) || TEST_FILE_RE.test(p);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isAssertion(text: string): boolean {
|
|
123
|
+
return ASSERTION_RE.test(text);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 3 exact · 2 relational · 1 existence · 0 unranked assertion. */
|
|
127
|
+
export function assertionStrength(text: string): number {
|
|
128
|
+
for (const [tier, re] of STRENGTH_TIERS) {
|
|
129
|
+
if (re.test(text)) return tier;
|
|
130
|
+
}
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Strip literals so two spellings of the same assertion compare equal. */
|
|
135
|
+
export function normalizeAssertion(text: string): string {
|
|
136
|
+
return text
|
|
137
|
+
.replace(/'[^']*'|"[^"]*"|`[^`]*`/g, 'S')
|
|
138
|
+
.replace(/\b\d+(?:\.\d+)?\b/g, 'N')
|
|
139
|
+
.replace(/\s+/g, ' ')
|
|
140
|
+
.trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* --------------------------------------------------------------- detectors */
|
|
144
|
+
|
|
145
|
+
function finding(
|
|
146
|
+
id: TamperDetectorId,
|
|
147
|
+
file: string,
|
|
148
|
+
l: DiffLine,
|
|
149
|
+
): TamperFinding {
|
|
150
|
+
return assertWellFormed({
|
|
151
|
+
id,
|
|
152
|
+
severity: detectorSpec(id).severity,
|
|
153
|
+
file,
|
|
154
|
+
line: l.line,
|
|
155
|
+
side: l.side === 'removed' ? 'removed' : 'added',
|
|
156
|
+
evidence: l.text.trim(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function scanDeletedFile(file: DiffFile): TamperFinding[] {
|
|
161
|
+
// A wholly deleted file yields exactly ONE finding. Reporting every
|
|
162
|
+
// assertion inside it as separately removed would bury the real claim
|
|
163
|
+
// under its own consequences.
|
|
164
|
+
if (!isTestPath(file.path)) return [];
|
|
165
|
+
const firstHunk = file.hunks[0];
|
|
166
|
+
const firstLine = firstHunk?.lines[0];
|
|
167
|
+
return [
|
|
168
|
+
assertWellFormed({
|
|
169
|
+
id: 'test-file-deleted',
|
|
170
|
+
severity: detectorSpec('test-file-deleted').severity,
|
|
171
|
+
file: file.path,
|
|
172
|
+
line: firstLine?.line ?? 1,
|
|
173
|
+
side: 'removed',
|
|
174
|
+
evidence: `whole test file deleted (${String(firstHunk?.lines.length ?? 0)} lines)`,
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function scanHunkAssertions(filePath: string, hunk: DiffHunk): TamperFinding[] {
|
|
180
|
+
const removed = hunk.lines.filter((l) => l.side === 'removed' && isAssertion(l.text));
|
|
181
|
+
const added = hunk.lines.filter((l) => l.side === 'added' && isAssertion(l.text));
|
|
182
|
+
if (removed.length === 0) return [];
|
|
183
|
+
|
|
184
|
+
const out: TamperFinding[] = [];
|
|
185
|
+
const pairedAdded = new Set<DiffLine>();
|
|
186
|
+
const pairedRemoved = new Set<DiffLine>();
|
|
187
|
+
|
|
188
|
+
// 1. Same assertion, different literal → the expectation moved onto the output.
|
|
189
|
+
for (const r of removed) {
|
|
190
|
+
const rNorm = normalizeAssertion(r.text);
|
|
191
|
+
for (const a of added) {
|
|
192
|
+
if (pairedAdded.has(a)) continue;
|
|
193
|
+
if (normalizeAssertion(a.text) === rNorm && a.text.trim() !== r.text.trim()) {
|
|
194
|
+
out.push(finding('expectation-rewritten', filePath, a));
|
|
195
|
+
pairedAdded.add(a);
|
|
196
|
+
pairedRemoved.add(r);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const restRemoved = removed.filter((r) => !pairedRemoved.has(r));
|
|
203
|
+
const restAdded = added.filter((a) => !pairedAdded.has(a));
|
|
204
|
+
if (restRemoved.length === 0) return out;
|
|
205
|
+
|
|
206
|
+
// 2. Something replaced it, lower on the ladder → weakened.
|
|
207
|
+
if (restAdded.length > 0) {
|
|
208
|
+
const strongestRemoved = Math.max(...restRemoved.map((r) => assertionStrength(r.text)));
|
|
209
|
+
for (const a of restAdded) {
|
|
210
|
+
if (assertionStrength(a.text) < strongestRemoved) {
|
|
211
|
+
out.push(finding('assertion-weakened', filePath, a));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 3. Nothing replaced it → removed outright.
|
|
218
|
+
for (const r of restRemoved) {
|
|
219
|
+
out.push(finding('assertion-removed', filePath, r));
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function scanHunkSkips(filePath: string, hunk: DiffHunk): TamperFinding[] {
|
|
225
|
+
return hunk.lines
|
|
226
|
+
.filter((l) => l.side === 'added' && SKIP_RE.test(l.text))
|
|
227
|
+
.map((l) => finding('verification-skipped', filePath, l));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function scanHunkEarlyExit(filePath: string, hunk: DiffHunk): TamperFinding[] {
|
|
231
|
+
const out: TamperFinding[] = [];
|
|
232
|
+
hunk.lines.forEach((l, i) => {
|
|
233
|
+
if (l.side !== 'added' || !UNCONDITIONAL_EXIT_RE.test(l.text)) return;
|
|
234
|
+
// Only a finding when verification survives BELOW it — otherwise the
|
|
235
|
+
// return is an ordinary end-of-function line.
|
|
236
|
+
const survives = hunk.lines
|
|
237
|
+
.slice(i + 1)
|
|
238
|
+
.some((below) => below.side !== 'removed' && isAssertion(below.text));
|
|
239
|
+
if (survives) out.push(finding('early-exit-injected', filePath, l));
|
|
240
|
+
});
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function identifiers(text: string): string[] {
|
|
245
|
+
return (text.match(/[A-Za-z_][A-Za-z0-9_]{3,}/g) ?? []).filter((w) => !MOCK_API_WORDS.has(w));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function scanFileMocks(file: DiffFile): TamperFinding[] {
|
|
249
|
+
const removedText: string[] = [];
|
|
250
|
+
for (const h of file.hunks) {
|
|
251
|
+
for (const l of h.lines) {
|
|
252
|
+
if (l.side === 'removed') removedText.push(l.text);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (removedText.length === 0) return [];
|
|
256
|
+
const out: TamperFinding[] = [];
|
|
257
|
+
for (const h of file.hunks) {
|
|
258
|
+
for (const l of h.lines) {
|
|
259
|
+
if (l.side !== 'added' || !MOCK_RE.test(l.text)) continue;
|
|
260
|
+
const hit = identifiers(l.text).some((word) =>
|
|
261
|
+
removedText.some((r) => r.includes(word)),
|
|
262
|
+
);
|
|
263
|
+
if (hit) out.push(finding('subject-mocked-away', file.path, l));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Every finding in one diff, before the allow declarations are applied. */
|
|
270
|
+
export function scanDiff(diffText: string): TamperFinding[] {
|
|
271
|
+
const files = parseUnifiedDiff(diffText);
|
|
272
|
+
const out: TamperFinding[] = [];
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
if (file.deleted) {
|
|
275
|
+
out.push(...scanDeletedFile(file));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (file.added) continue;
|
|
279
|
+
for (const hunk of file.hunks) {
|
|
280
|
+
out.push(...scanHunkAssertions(file.path, hunk));
|
|
281
|
+
out.push(...scanHunkSkips(file.path, hunk));
|
|
282
|
+
out.push(...scanHunkEarlyExit(file.path, hunk));
|
|
283
|
+
}
|
|
284
|
+
out.push(...scanFileMocks(file));
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* -------------------------------------------------- phantom verification */
|
|
290
|
+
|
|
291
|
+
export interface RecordedVerdict {
|
|
292
|
+
status?: string;
|
|
293
|
+
ran_at?: string;
|
|
294
|
+
last_edit_at?: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A green verdict whose runner finished before the last edit is not evidence.
|
|
299
|
+
*
|
|
300
|
+
* Reported for a red verdict too: a stale red is not evidence of failure
|
|
301
|
+
* either, and passing it through would make the outcome depend on which way a
|
|
302
|
+
* run over the wrong tree happened to land.
|
|
303
|
+
*/
|
|
304
|
+
export function verdictIsStale(v: RecordedVerdict): boolean {
|
|
305
|
+
if (typeof v.ran_at !== 'string' || typeof v.last_edit_at !== 'string') return false;
|
|
306
|
+
const ran = Date.parse(v.ran_at);
|
|
307
|
+
const edited = Date.parse(v.last_edit_at);
|
|
308
|
+
if (Number.isNaN(ran) || Number.isNaN(edited)) return false;
|
|
309
|
+
return ran < edited;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* --------------------------------------------------------------- analysis */
|
|
313
|
+
|
|
314
|
+
export interface Analysis {
|
|
315
|
+
outcome: LoopOutcome;
|
|
316
|
+
findings: TamperFinding[];
|
|
317
|
+
suppressed: TamperFinding[];
|
|
318
|
+
malformedAllows: string[];
|
|
319
|
+
staleVerdict: boolean;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function analyze(
|
|
323
|
+
diffText: string,
|
|
324
|
+
message: string,
|
|
325
|
+
verdict: RecordedVerdict | null,
|
|
326
|
+
): Analysis {
|
|
327
|
+
const { allowed, malformed } = parseAllowDeclarations(message);
|
|
328
|
+
const all = scanDiff(diffText);
|
|
329
|
+
const findings = all.filter((f) => !allowed.has(f.id));
|
|
330
|
+
const suppressed = all.filter((f) => allowed.has(f.id));
|
|
331
|
+
const stale = verdict !== null && verdictIsStale(verdict);
|
|
332
|
+
|
|
333
|
+
let outcome: LoopOutcome;
|
|
334
|
+
if (stale) {
|
|
335
|
+
// Stale beats everything: nothing downstream of a verdict that never
|
|
336
|
+
// saw the change is worth grading.
|
|
337
|
+
outcome = 'stale_verdict';
|
|
338
|
+
} else if (malformed.length > 0) {
|
|
339
|
+
// A declaration that does not parse suppressed nothing and must not
|
|
340
|
+
// read to its author as an accepted exception.
|
|
341
|
+
outcome = 'violations';
|
|
342
|
+
} else if (findings.some((f) => f.severity === 'block')) {
|
|
343
|
+
outcome = 'violations';
|
|
344
|
+
} else if (findings.length > 0) {
|
|
345
|
+
outcome = 'violations';
|
|
346
|
+
} else {
|
|
347
|
+
outcome = 'clean';
|
|
348
|
+
}
|
|
349
|
+
return { outcome, findings, suppressed, malformedAllows: malformed, staleVerdict: stale };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function exitCodeFor(a: Analysis): number {
|
|
353
|
+
if (a.staleVerdict) return 4;
|
|
354
|
+
if (a.malformedAllows.length > 0) return 1;
|
|
355
|
+
if (a.findings.some((f) => f.severity === 'block')) return 1;
|
|
356
|
+
if (a.findings.length > 0) return 3;
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function render(a: Analysis): string {
|
|
361
|
+
const lines: string[] = [`outcome: ${a.outcome}`];
|
|
362
|
+
if (a.staleVerdict) {
|
|
363
|
+
lines.push(
|
|
364
|
+
'stale_verdict — the recorded runner finished BEFORE the last edit in the loop, ' +
|
|
365
|
+
'so its exit code describes a tree that no longer exists. Re-run before claiming anything.',
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
for (const m of a.malformedAllows) lines.push(`malformed declaration: ${m} (nothing was suppressed)`);
|
|
369
|
+
for (const f of a.findings) {
|
|
370
|
+
lines.push(`${f.severity.toUpperCase()} ${f.id} ${locate(f)} ${f.evidence}`);
|
|
371
|
+
lines.push(` why: ${detectorSpec(f.id).why}`);
|
|
372
|
+
}
|
|
373
|
+
for (const f of a.suppressed) {
|
|
374
|
+
lines.push(`allowed ${f.id} ${locate(f)} (declared in the commit message)`);
|
|
375
|
+
}
|
|
376
|
+
if (a.findings.length === 0 && !a.staleVerdict && a.malformedAllows.length === 0) {
|
|
377
|
+
lines.push('the path to green did not weaken the verification');
|
|
378
|
+
}
|
|
379
|
+
return lines.join('\n');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* ----------------------------------------------------------------- corpus */
|
|
383
|
+
|
|
384
|
+
interface Fixture {
|
|
385
|
+
id: string;
|
|
386
|
+
diff: string;
|
|
387
|
+
kind: 'positive' | 'negative';
|
|
388
|
+
expect: string[];
|
|
389
|
+
message: string;
|
|
390
|
+
why: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function loadCorpus(root: string = CORPUS): Fixture[] {
|
|
394
|
+
const raw = fs.readFileSync(path.join(root, 'manifest.json'), 'utf-8');
|
|
395
|
+
const parsed = JSON.parse(raw) as { fixtures: Fixture[] };
|
|
396
|
+
return parsed.fixtures;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function runCorpus(write: (s: string) => void): number {
|
|
400
|
+
const fixtures = loadCorpus();
|
|
401
|
+
let failed = 0;
|
|
402
|
+
for (const f of fixtures) {
|
|
403
|
+
const diffText = fs.readFileSync(path.join(CORPUS, f.diff), 'utf-8');
|
|
404
|
+
const a = analyze(diffText, f.message, null);
|
|
405
|
+
const got = a.findings.map((x) => x.id).sort();
|
|
406
|
+
const want = [...f.expect].sort();
|
|
407
|
+
const ok = JSON.stringify(got) === JSON.stringify(want);
|
|
408
|
+
if (!ok) failed += 1;
|
|
409
|
+
write(
|
|
410
|
+
`${ok ? 'OK ' : 'FAIL'} ${f.kind.padEnd(8)} ${f.id.padEnd(34)} ` +
|
|
411
|
+
`expect [${want.join(', ')}] got [${got.join(', ')}]\n`,
|
|
412
|
+
);
|
|
413
|
+
if (!ok) {
|
|
414
|
+
for (const x of a.findings) write(` ${x.id} ${locate(x)} ${x.evidence}\n`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
write(`\nscanned: ${String(fixtures.length)}\n`);
|
|
418
|
+
write(`${String(fixtures.length - failed)}/${String(fixtures.length)} fixtures behaved\n`);
|
|
419
|
+
return failed > 0 ? 1 : 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* -------------------------------------------------------------- self-test */
|
|
423
|
+
|
|
424
|
+
function selfTest(): number {
|
|
425
|
+
const rel = 'src/scripts/detect_verification_tampering.ts';
|
|
426
|
+
const fixtures = loadCorpus();
|
|
427
|
+
const cases: SelfTestCase[] = fixtures.map((f) => ({
|
|
428
|
+
name: `${f.kind}/${f.id}`,
|
|
429
|
+
expect: f.kind === 'positive' ? 'reject' : 'accept',
|
|
430
|
+
run: () =>
|
|
431
|
+
runGateCli(
|
|
432
|
+
REPO_ROOT,
|
|
433
|
+
rel,
|
|
434
|
+
['--diff', path.join(CORPUS, f.diff), '--message', f.message],
|
|
435
|
+
REPO_ROOT,
|
|
436
|
+
),
|
|
437
|
+
}));
|
|
438
|
+
for (const [name, file, expect] of [
|
|
439
|
+
['verdict/stale-green', 'stale-green.json', 'reject'],
|
|
440
|
+
['verdict/stale-red', 'stale-red.json', 'reject'],
|
|
441
|
+
['verdict/fresh-green', 'fresh-green.json', 'accept'],
|
|
442
|
+
] as const) {
|
|
443
|
+
cases.push({
|
|
444
|
+
name,
|
|
445
|
+
expect,
|
|
446
|
+
run: () =>
|
|
447
|
+
runGateCli(
|
|
448
|
+
REPO_ROOT,
|
|
449
|
+
rel,
|
|
450
|
+
[
|
|
451
|
+
'--diff',
|
|
452
|
+
path.join(CORPUS, 'diffs/neg-assertion-added.diff'),
|
|
453
|
+
'--verdict',
|
|
454
|
+
path.join(CORPUS, 'verdicts', file),
|
|
455
|
+
],
|
|
456
|
+
REPO_ROOT,
|
|
457
|
+
),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
cases.push({
|
|
461
|
+
name: 'declaration/unknown-id-suppresses-nothing',
|
|
462
|
+
expect: 'reject',
|
|
463
|
+
run: () =>
|
|
464
|
+
runGateCli(
|
|
465
|
+
REPO_ROOT,
|
|
466
|
+
rel,
|
|
467
|
+
[
|
|
468
|
+
'--diff',
|
|
469
|
+
path.join(CORPUS, 'diffs/test-file-deleted.diff'),
|
|
470
|
+
'--message',
|
|
471
|
+
'fix: green it\n\ntamper-allow: test-files-deleted — a plausible reason with a typo in the id',
|
|
472
|
+
],
|
|
473
|
+
REPO_ROOT,
|
|
474
|
+
),
|
|
475
|
+
});
|
|
476
|
+
return runSelfTest({
|
|
477
|
+
gate: 'detect_verification_tampering',
|
|
478
|
+
cases,
|
|
479
|
+
minCases: SELF_TEST_MIN_CASES,
|
|
480
|
+
minRejectCases: SELF_TEST_MIN_REJECT,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* ------------------------------------------------------------------- main */
|
|
485
|
+
|
|
486
|
+
function usage(): never {
|
|
487
|
+
process.stderr.write(
|
|
488
|
+
'usage: detect_verification_tampering --diff <file> [--message <s>|--message-file <f>]\n' +
|
|
489
|
+
' [--verdict <verdict.json>] [--json]\n' +
|
|
490
|
+
' detect_verification_tampering --corpus | --self-test\n',
|
|
491
|
+
);
|
|
492
|
+
process.exit(2);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function main(argv: readonly string[]): number {
|
|
496
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
497
|
+
if (argv.includes('--corpus')) return runCorpus((s) => process.stdout.write(s));
|
|
498
|
+
|
|
499
|
+
let diffPath: string | null = null;
|
|
500
|
+
let message = '';
|
|
501
|
+
let verdictPath: string | null = null;
|
|
502
|
+
let json = false;
|
|
503
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
504
|
+
const a = argv[i];
|
|
505
|
+
if (a === '--diff') { diffPath = argv[++i] ?? null; }
|
|
506
|
+
else if (a === '--message') { message = argv[++i] ?? ''; }
|
|
507
|
+
else if (a === '--message-file') { message = fs.readFileSync(argv[++i] ?? '', 'utf-8'); }
|
|
508
|
+
else if (a === '--verdict') { verdictPath = argv[++i] ?? null; }
|
|
509
|
+
else if (a === '--json') { json = true; }
|
|
510
|
+
else usage();
|
|
511
|
+
}
|
|
512
|
+
if (diffPath === null) usage();
|
|
513
|
+
|
|
514
|
+
const diffText = fs.readFileSync(diffPath, 'utf-8');
|
|
515
|
+
const verdict =
|
|
516
|
+
verdictPath === null
|
|
517
|
+
? null
|
|
518
|
+
: (JSON.parse(fs.readFileSync(verdictPath, 'utf-8')) as RecordedVerdict);
|
|
519
|
+
const analysis = analyze(diffText, message, verdict);
|
|
520
|
+
process.stdout.write(
|
|
521
|
+
(json ? JSON.stringify(analysis, null, 2) : render(analysis)) + '\n',
|
|
522
|
+
);
|
|
523
|
+
return exitCodeFor(analysis);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const invokedDirectly =
|
|
527
|
+
process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
528
|
+
if (invokedDirectly) {
|
|
529
|
+
try {
|
|
530
|
+
process.exit(main(process.argv.slice(2)));
|
|
531
|
+
} catch (err) {
|
|
532
|
+
// Rethrown as an exit-2 crash with the error text preserved. Never
|
|
533
|
+
// swallowed: a detector that reported clean because it threw would be
|
|
534
|
+
// the phantom green this file exists to catch.
|
|
535
|
+
process.stderr.write(`detect_verification_tampering crashed: ${String(err)}\n`);
|
|
536
|
+
process.exit(2);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
@@ -690,7 +690,7 @@ function renderHygieneSection(hygiene: HygieneReadResult): string[] {
|
|
|
690
690
|
out.push(`_Source: tried ${hygiene.checkedPaths.map((p) => `\`${p}\``).join(', then ')}_`);
|
|
691
691
|
out.push('');
|
|
692
692
|
if (hygiene.state === null) {
|
|
693
|
-
out.push('no data — none of the candidate paths exist yet (no PostToolUse hook has run this session, or
|
|
693
|
+
out.push('no data — none of the candidate paths exist yet (no PostToolUse hook has run this session, or no PostToolUse hook is bound on this host)');
|
|
694
694
|
return out;
|
|
695
695
|
}
|
|
696
696
|
const s = hygiene.state;
|
|
@@ -136,7 +136,7 @@ const _FN_SPEC: Record<string, FnSpec> = {
|
|
|
136
136
|
// Surfaces the INSTALLER provides outside generate_tools() (so the derivation
|
|
137
137
|
// guard stays pure). Marked with a † footnote in the rendered matrix.
|
|
138
138
|
const _INSTALL_TIME_CELLS: Record<string, Record<string, string>> = {
|
|
139
|
-
rules: { copilot: 'adapter' }, // .github/copilot-instructions.md (aggregated,
|
|
139
|
+
rules: { copilot: 'adapter' }, // .github/copilot-instructions.md (aggregated, the installer)
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
// Python: re.compile(r"\b(generate_[A-Za-z0-9_]+)\s*\(").
|