codehere 0.8.0 → 1.0.2
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/README.md +37 -184
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +329 -0
- package/dist/index.js +74 -1004
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
- package/dist/presentation/cli/commands/check-command.js +251 -0
- package/dist/presentation/cli/commands/help-command.js +22 -286
- package/dist/ui.js +19 -438
- package/dist/utils/version.js +0 -12
- package/package.json +18 -27
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/generate-final-report.js +0 -134
- package/dist/application/use-cases/generate-narration.js +0 -128
- package/dist/application/use-cases/generate-thread-title.js +0 -178
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/thread-export.js +0 -155
- package/dist/application/use-cases/verify-claim.js +0 -274
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/entities/task.js +0 -61
- package/dist/domain/services/codebase-detector.js +0 -91
- package/dist/error-handler.js +0 -251
- package/dist/infrastructure/completion/bash-completion.js +0 -25
- package/dist/infrastructure/completion/fish-completion.js +0 -22
- package/dist/infrastructure/completion/zsh-completion.js +0 -30
- package/dist/infrastructure/config/config-manager.js +0 -173
- package/dist/infrastructure/config/product-context.js +0 -67
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -163
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -119
- package/dist/infrastructure/context/registry-bootstrap.js +0 -99
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -113
- package/dist/infrastructure/cost/cost-router.js +0 -104
- package/dist/infrastructure/cost/spend-aggregator.js +0 -147
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
- package/dist/infrastructure/hooks/hook-mode.js +0 -71
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
- package/dist/infrastructure/orchestration/context-handoff.js +0 -144
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -77
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
- package/dist/infrastructure/orchestration/graphify-context.js +0 -254
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1091
- package/dist/infrastructure/orchestration/session-persistence.js +0 -258
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/protocols/verify-page.js +0 -983
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/security/permissions-config.js +0 -234
- package/dist/infrastructure/security/trust-scorer.js +0 -657
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
- package/dist/infrastructure/storage/project-thread-reader.js +0 -172
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/infrastructure/storage/task-repository.js +0 -206
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -126
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -198
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -578
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -116
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -387
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
- package/dist/presentation/cli/commands/setup-command.js +0 -307
- package/dist/presentation/cli/commands/status-command.js +0 -661
- package/dist/presentation/cli/commands/thread-command.js +0 -288
- package/dist/presentation/cli/commands/trust-command.js +0 -306
- package/dist/presentation/cli/commands/verify-command.js +0 -233
- package/dist/presentation/cli/error-display.js +0 -336
- package/dist/presentation/cli/repl.js +0 -797
- package/dist/presentation/cli/task-deliverable.js +0 -127
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -166
package/README.md
CHANGED
|
@@ -1,214 +1,67 @@
|
|
|
1
1
|
# codehere
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
3
|
+
> **Re-runs the tests an AI says passed, and tells you whether they did.**
|
|
4
|
+
> `codehere check` on your machine, or the GitHub Action on every pull request. No account.
|
|
5
5
|
|
|
6
|
-
[](../LICENSE)
|
|
8
|
-
[]()
|
|
9
|
-
[](../docs/TRUTHFUL_STATE_2026-05-19.md)
|
|
6
|
+
[](https://github.com/muhammadegaa/codehere/blob/main/LICENSE)
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
`codehere check` re-runs your project's tests itself and reports the real result, so you do not have to take an agent's "all tests pass" on trust. It detects `npm test`, `node --test`, `pytest`, `go test`, `cargo test` and vitest or jest configs.
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
> **Honest status (2026-05-19)** — The 1200 internal tests verify codehere's code against codehere's own assumptions about what each adapter emits. They do **not** systematically verify against real-adapter outputs in production. Real-adapter contract verification is the v0.7 release work — see [`docs/TRUTHFUL_STATE_2026-05-19.md`](../docs/TRUTHFUL_STATE_2026-05-19.md) for the verified / assumed / unverified classification of every surface.
|
|
16
|
-
|
|
17
|
-
The enterprise tier is owned by [OpenAI Frontier](https://openai.com/index/introducing-openai-frontier/) (8-14 weeks of deploy prereqs, sales-team gated) and [Anthropic Cowork](https://www.anthropic.com/product/claude-cowork) (paid Claude subscription, single-vendor). Codehere fills the gap they structurally can't reach: **the individual / small-team / no-IT-department tier, vendor-neutral**.
|
|
18
|
-
|
|
19
|
-
> See [`docs/STRATEGIC_VISION.md`](../docs/STRATEGIC_VISION.md) Appendix C and [`docs/figures/positioning-matrix.svg`](../docs/figures/positioning-matrix.svg) for the full positioning chart.
|
|
20
|
-
|
|
21
|
-
One layer, every agent, your work stays portable.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Recipe — setup to first audit in 30 minutes
|
|
26
|
-
|
|
27
|
-
> One folder convention, one prompt formula, four ready-to-use prompts.
|
|
28
|
-
> Full version: **[docs/RECIPE.md](../docs/RECIPE.md)**.
|
|
29
|
-
|
|
30
|
-
### Setup (once)
|
|
31
|
-
|
|
32
|
-
| | | |
|
|
33
|
-
|---|---|---|
|
|
34
|
-
| 1 | **Install** | `npm install -g codehere` (Node 20+) — v0.6.0 is the current stable |
|
|
35
|
-
| 2 | **Detect environment** | `codehere setup` — checks API key, finds your repo |
|
|
36
|
-
| 3 | **Install the safety hook** | `codehere hook install` — every Claude Code tool call now audited |
|
|
37
|
-
| 4 | **Install at least one specialist** | Claude Code: `npm i -g @anthropic-ai/claude-code` (Codex / Aider / OpenCode / Gemini also supported · see [docs/integrations/](../docs/integrations/) for Cursor MCP setup) |
|
|
38
|
-
| 5 | **Scaffold this repo** | `cd /your/repo && codehere init` — creates `.codehere/` with the F3 memory tree (working / episodic / semantic / personal / candidates) plus `AGENTS.md` (project briefing) and `skills/` (prompt templates) |
|
|
39
|
-
| 6 | **Fill in AGENTS.md** | 5 minutes. The agent will follow what's written there. |
|
|
40
|
-
| 7 | **Build the symbol map (optional)** | `codehere memory repo-map` — JSON map of every top-level export in your repo, ranked by reference count. Spawned agents read it on entry instead of cold-grepping. |
|
|
41
|
-
|
|
42
|
-
You're done. `codehere start` opens the web UI at `:3773`; you delegate from there.
|
|
43
|
-
|
|
44
|
-
### The prompt formula
|
|
10
|
+
## Install
|
|
45
11
|
|
|
46
12
|
```
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
- **Context** — *"Read .codehere/CONTEXT.md"* + any specific files
|
|
51
|
-
- **End State** — the user-visible change, the test that passes, the file that exists
|
|
52
|
-
- **Constraints** — files not to touch, dependencies not to add, patterns not to use
|
|
53
|
-
|
|
54
|
-
### Folder convention (what `codehere init` writes)
|
|
55
|
-
|
|
56
|
-
The v0.6 layout — F3 project memory:
|
|
57
|
-
|
|
58
|
-
| Path | Purpose | Committed? |
|
|
59
|
-
|---|---|---|
|
|
60
|
-
| `.codehere/AGENTS.md` *(or legacy `CONTEXT.md`)* | What this repo is. Conventions. Style. Constraints. | yes |
|
|
61
|
-
| `.codehere/memory/working/session.md` *(or legacy `HANDOVER.md`)* | What's in flight in the current session. | **no** |
|
|
62
|
-
| `.codehere/memory/episodic/AGENT_LEARNINGS.jsonl` | Symlink to your full session history at `~/.codehere/sessions.jsonl`. | (symlink) |
|
|
63
|
-
| `.codehere/memory/episodic/repo_map.json` | Ranked symbol map. Built by `codehere memory repo-map`. | yes |
|
|
64
|
-
| `.codehere/memory/semantic/lessons.jsonl` | Project-specific lessons promoted from episodic patterns. | yes |
|
|
65
|
-
| `.codehere/memory/personal/preferences.json` | Per-developer prefs. | **no** |
|
|
66
|
-
| `.codehere/skills/<name>/SKILL.md` *(or legacy flat `PROMPTS.md`)* | Prompt templates — one folder per skill. | yes |
|
|
67
|
-
| `.codehere/protocols/permissions.md` + `hook_patterns.json` | Safety policy for the PreToolUse hook. | yes |
|
|
68
|
-
|
|
69
|
-
Upgrading from v0.4.1? Run `codehere memory init` once — it migrates `CONTEXT/HANDOVER/PROMPTS` into the new layout and leaves symlinks at the old paths so back-compat holds. `--dry-run` shows what would move first.
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## What you see when you delegate a task
|
|
74
|
-
|
|
75
|
-
`codehere start` opens `http://127.0.0.1:3773`. Pick an agent, type a task, hit **Assign**. codehere:
|
|
76
|
-
|
|
77
|
-
1. Spawns the agent with the right headless flags
|
|
78
|
-
2. Streams the steps back as readable progress · *"Read src/auth.ts · Edit src/auth.ts · Run npm test"*
|
|
79
|
-
3. For Claude Code tasks: routes every tool call through the codehere safety hook — you see *"🛡 5 safe · 0 blocked"* grow live. For Codex / Aider / OpenCode / Gemini tasks: emits the agent's native event stream (no per-tool blocking yet)
|
|
80
|
-
4. On completion, hands you a **Final report**: files changed with `+/-` lines, trust score per file, test pass/fail, token usage
|
|
81
|
-
|
|
82
|
-
Claude Code tool calls are logged to `~/.codehere/audit/hook-events.jsonl`; spawned-task lifecycle (any adapter) is logged to `~/.codehere/sessions.jsonl`. Open-source MIT. Node 20+.
|
|
83
|
-
|
|
84
|
-
### Just the safety hook (no agents required)
|
|
85
|
-
|
|
86
|
-
If you only want the audit-and-block layer for AI tools you already use:
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
codehere hook install # activates the PreToolUse hook in your global ~/.claude/settings.json
|
|
90
|
-
codehere trust . # static trust scan of the current directory
|
|
91
|
-
codehere report # generates a shareable markdown audit report
|
|
92
|
-
codehere status # quick dashboard · sessions, blocks, trust trends
|
|
13
|
+
npx codehere check
|
|
93
14
|
```
|
|
94
15
|
|
|
95
|
-
The
|
|
96
|
-
|
|
97
|
-
## Memory · F3 (v0.6)
|
|
98
|
-
|
|
99
|
-
Once you've run `codehere init` in a project, every spawned agent — Claude Code, Codex, Aider, OpenCode, or Gemini — automatically reads the project's memory on entry. The agent starts warm with the briefing, recent lessons, and the most-referenced symbols in your codebase, instead of cold-grepping every session.
|
|
16
|
+
The npm release is 1.0.1, which has `check` with `--json` and `--proof`. The options `--base` and `--claim` below are in this repository (version 1.0.2) and are not published yet. To use them now, build from source: `cd agent && npm ci && npm run build && node dist/index.js check`.
|
|
100
17
|
|
|
101
|
-
|
|
102
|
-
1. **`.codehere/AGENTS.md`** — your project briefing (conventions, style, constraints)
|
|
103
|
-
2. **`.codehere/memory/working/session.md`** — what's in flight right now (per-machine, gitignored)
|
|
104
|
-
3. **`.codehere/memory/semantic/lessons.jsonl`** — graduated project-wide lessons
|
|
105
|
-
4. **`.codehere/memory/episodic/repo_map.json`** — top-N most-referenced symbols (run `codehere memory repo-map` to build / refresh)
|
|
18
|
+
## The four commands
|
|
106
19
|
|
|
107
|
-
The injection is token-budget-aware (default ~3000 tokens, drops lower-priority sections first to fit; AGENTS.md is never dropped). Pass `--no-memory-prefix` if you want the bare prompt for a one-off task.
|
|
108
|
-
|
|
109
|
-
**Inspect what would inject** before spawning anything:
|
|
110
|
-
```bash
|
|
111
|
-
codehere memory show injection
|
|
112
20
|
```
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
codehere
|
|
21
|
+
codehere check # re-run the tests here
|
|
22
|
+
codehere check --base origin/main # also read everything this branch changed
|
|
23
|
+
codehere check --claim "<the agent's summary>" # flag test files it named that do not exist
|
|
24
|
+
codehere check --proof # a receipt you can paste into a pull request
|
|
117
25
|
```
|
|
118
26
|
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
codehere memory show # one-line summary per layer
|
|
122
|
-
codehere memory show semantic # numbered lesson list
|
|
123
|
-
codehere memory show episodic # session count + top symbols
|
|
124
|
-
codehere memory show protocols # safety patterns by category
|
|
125
|
-
```
|
|
27
|
+
`codehere help check` lists the options. `--json` adds a `verdict` (`pass`, `fail`, `unverified` or `skipped`) and the receipt text.
|
|
126
28
|
|
|
127
|
-
|
|
128
|
-
- `codehere memory init` — migrate the v0.4.1 layout (`CONTEXT.md` / `HANDOVER.md` / `PROMPTS.md`) into the F3 layout (one shot, leaves back-compat symlinks; `--dry-run` previews)
|
|
129
|
-
- `codehere memory repo-map [--rebuild]` — build the symbol map; currently TS-native MVP
|
|
130
|
-
- `codehere protocols export [--rebuild]` — write the trust-scorer pattern bank to `.codehere/protocols/hook_patterns.json` for inspection / cross-tool ingestion
|
|
29
|
+
Exit codes:
|
|
131
30
|
|
|
132
|
-
|
|
31
|
+
- **0**: the tests ran and passed, no test was weakened, no named test file is missing, and the weakened-test scan finished.
|
|
32
|
+
- **1**: the tests failed; the runner reported no test that ran (0 tests, or every test skipped); the change made a test check nothing (an assertion swapped for `assert True` or `assert.ok(true)`, a skip marker, an assertion commented out, or a `return` above it); or a test file named in `--claim` is not in the repository. Usage errors (an unknown `--base`, a missing folder, an unknown option) also exit 1, with the message on stderr and nothing on stdout, and so does an internal error. Pass the claim as `--claim=<text>`: in `--claim --json` the claim is the text `--json`.
|
|
33
|
+
- **2**: nothing could be verified: the run timed out, no test command was recognized, the runner is not installed, the re-run was turned off, or the weakened-test scan could not run (git is not installed, git could not read the repository, or the diff could not be read).
|
|
133
34
|
|
|
134
|
-
|
|
35
|
+
Settings, as environment variables: `CODEHERE_TEST_CMD` replaces the detected test command (split on whitespace, no shell); `CODEHERE_RERUN_TIMEOUT_MS` sets the time limit in whole milliseconds (default 120000, two minutes, also used for a value that is not a whole number from 1 to 2147483647); `CODEHERE_RERUN_TESTS=0` turns the re-run off. Detection covers `npm test` (a `test` script in `package.json`), `go test ./...`, `cargo test`, `pytest` (with `pytest.ini`, `conftest.py` or a pytest section in `pyproject.toml` or `setup.cfg`), `vitest.config.ts`/`.js`, `jest.config.js`/`.ts`, and `node --test` for `*.test.js` files.
|
|
135
36
|
|
|
136
|
-
|
|
137
|
-
|---|---|
|
|
138
|
-
| [`docs/decisions/`](../docs/decisions/) | ADR registry — every locked architectural decision (audit-first moat, supervisor-agent positioning, local-first, F3 memory layout, generic-CLI adapter, ...) numbered, dated, with rejected alternatives + rollback triggers |
|
|
139
|
-
| [`docs/THREAT_MODEL.md`](../docs/THREAT_MODEL.md) | Every attack surface, current mitigation, residual risk, planned hardening — npm supply chain, hook bypass, symlink redirect, audit log tampering, MCP server, prompt injection |
|
|
140
|
-
| [`docs/CAPABILITIES.md`](../docs/CAPABILITIES.md) | Honest matrix of what each adapter can and cannot do today — Claude Code, Codex, Aider, OpenCode, Gemini, Generic CLI — across spawn, stream, per-tool block, audit, F3 memory, auth |
|
|
141
|
-
| [`agent/REPO_MAP.md`](./REPO_MAP.md) | One-line-per-file index of the 54 source files with their top-level exports — generated, regenerated via `npm run repomap` after renames |
|
|
37
|
+
"No test ran" is read from the runner's own summary for node:test, pytest, jest, vitest and Go's `[no test files]`. A Go package with test files but no tests (`[no tests to run]`), `cargo test` with 0 tests, jest with `--passWithNoTests`, mocha, and a test script that only prints "No tests found" read as a pass. A test script that leaves a background process holding its output open is reported as timed out, even when the tests passed.
|
|
142
38
|
|
|
143
|
-
##
|
|
39
|
+
## GitHub Action
|
|
144
40
|
|
|
145
|
-
|
|
146
|
-
codehere start · launch the task intake web UI at :3773
|
|
147
|
-
codehere task <prompt> · spawn a task from the CLI (requires `start` running)
|
|
148
|
-
codehere kill <sessionId> · kill a running task
|
|
149
|
-
codehere serve · start as MCP server for Claude Desktop, Cursor, etc.
|
|
150
|
-
codehere status · dashboard · sessions, blocked calls, audit trends
|
|
151
|
-
codehere trust [path] · static trust scan · score 0-100, grade A-F
|
|
152
|
-
codehere report · generate a shareable markdown audit report
|
|
153
|
-
codehere hook · install/uninstall/inspect the PreToolUse safety hook
|
|
154
|
-
codehere permission · manage this folder's allow-list (skip false-positive blocks)
|
|
155
|
-
codehere budget · daily-spend ceiling · `set <usd>` blocks paid spawns at the cap
|
|
156
|
-
codehere narrator · customize the PM narrator prompt (~/.codehere/narrator-prompt.md)
|
|
157
|
-
codehere setup · interactive first-run setup wizard
|
|
158
|
-
codehere config · manage configuration and preferences
|
|
159
|
-
codehere health · system health check
|
|
160
|
-
codehere help · show help (use --all for hidden commands)
|
|
161
|
-
codehere completion <shell> · generate bash/zsh/fish completion script
|
|
162
|
-
codehere scan-tool · internal · used by the hook system, not by users
|
|
163
|
-
```
|
|
41
|
+
A GitHub Action that posts the receipt on each pull request is in [`action/`](https://github.com/muhammadegaa/codehere/tree/main/action). It runs `npx codehere@<version> check --json --base origin/<base> --claim=<PR body>`. Its default version is 1.0.2, which is not on npm yet, so until 1.0.2 is published the Action reports `unverified` and the job stays green with a warning. The 120-second default time limit applies, and the Action has no input to change it. With the default `fail-on-unverified: false`, a pull request that makes the suite unrunnable (removes the test script, breaks the runner) gets `unverified` and a green job with a warning, and a `skipped` verdict (the re-run turned off in the workflow) gets a green job with no warning. Set `fail-on-unverified: true` to fail the job on `unverified`. It is not published to the marketplace.
|
|
164
42
|
|
|
165
|
-
|
|
43
|
+
## What it does not catch
|
|
166
44
|
|
|
167
|
-
|
|
45
|
+
- **Weakened tests.** The detector reads Python (`test_*.py`, `*_test.py`), JavaScript and TypeScript (`*.test.*`, `*.spec.*`), Go (`*_test.go`) and PHP (`*Test.php`) test files, and files under `test`, `tests` or `__tests__`. It flags an assertion replaced by an always-true one (`assert True`, `assert.ok(true)`, `assertTrue(True)`, a value compared with itself), a skip marker (including `skipif(True)`, `it.only(`, `test.only(` or `describe.only(` outside strings and `//` comments (including `test.describe.only(`), and in JavaScript, TypeScript and Vue files `fit(` or `fdescribe(` at the start of a statement, after `=>`, or after `await`, `return` or `void`; variants such as `test.only.each` are not flagged, a JavaScript helper or method named `fit` at the start of a statement is flagged, and `.vue` files are read only under `test`, `tests` or `__tests__`), a commented-out assertion and an early `return`. It does not flag a deleted assertion, a deleted test or test file, a loosened expectation, an always-true check written another way, or an edited test script. A very small test file that is renamed and changed in the same commit can read to git as a new file, and a new file is not scanned. Without `--base` it sees only uncommitted changes, so a weakened test that is already committed passes; with `--base <ref>` it sees everything since the branch left that ref. Only test files are diffed; if their diff cannot be read (over 64 MB), the run is reported as unverified. In a folder with no git repository and no `--base`, or before the first commit, the scan is skipped and the output says so. When git is missing or fails on a repository that is there, or `--base` is given outside a repository, the run is reported as unverified.
|
|
46
|
+
- **`--claim`** finds test files named in the summary in plain text, in quotes or backticks, in markdown bold or italics, in brackets, in `path::test` form and with a `:line` suffix. It ignores absolute paths, Windows backslash paths and Java `*Test.java` names. A file counts as present when any file with the same name exists in the checkout (outside `node_modules`, `dist`, `build`, `vendor`, `target` and virtual environments), so a file in the wrong folder is not reported. The search goes 8 folders deep and stops after 50,000 entries, so a named file deeper than that, or in a larger checkout, is reported as missing.
|
|
47
|
+
- **A passing suite with a broken build, and code the tests do not cover.** A receipt says what the test suite showed, not that the software is correct.
|
|
48
|
+
- **Who controls the tests.** `check` runs the project's own test command, so whoever controls that command controls the result, and whoever controls the environment can point the weakened-test scan at another tree (`GIT_DIR`, `GIT_WORK_TREE`). For a pull request from someone you do not trust, a receipt is not evidence. In the Action, use the `pull_request` trigger, not `pull_request_target`.
|
|
168
49
|
|
|
169
|
-
|
|
50
|
+
## What has been measured
|
|
170
51
|
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
- Eval injection · prompt injection · credential theft
|
|
174
|
-
- Destructive shell operations · supply-chain patterns
|
|
52
|
+
- **False-done eval, 22 cases.** 14 false "tests pass" or "all green" claims (6 reconstructed from public posts, 8 planted) and 8 true ones. `codehere check` caught 12 of the 14 false claims, missed 2 (a green suite with a broken build, and green tests with a broken production job), and raised 0 false alarms on the 8 true ones. Result: [`agent/eval/false-done-result.json`](https://github.com/muhammadegaa/codehere/blob/main/agent/eval/false-done-result.json), produced by `node eval/false-done.mjs`. The cases were written to fit the detectors, so this is a small set and not a general rate.
|
|
53
|
+
- **How often agent pull requests carry a failing claim: not measured.** A pre-registered study (S11) had two models label 299 public agent pull requests. They disagreed (Cohen's kappa 0.092), so the study reports no rate. The report, with the method and its limits: [`agent/eval/promise-verification/studies/S11-demand/REPORT.md`](https://github.com/muhammadegaa/codehere/blob/main/agent/eval/promise-verification/studies/S11-demand/REPORT.md).
|
|
175
54
|
|
|
176
|
-
|
|
55
|
+
## Development
|
|
177
56
|
|
|
178
|
-
## MCP server
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
codehere serve
|
|
182
57
|
```
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
- `codehere_audit` · read the local hook audit log
|
|
190
|
-
|
|
191
|
-
`codehere serve --show-config` prints the JSON block to paste into Claude Desktop's MCP settings.
|
|
192
|
-
|
|
193
|
-
## Architecture, story, and contribution
|
|
194
|
-
|
|
195
|
-
For depth beyond this README:
|
|
196
|
-
|
|
197
|
-
- **[docs/STRATEGIC_VISION.md](../docs/STRATEGIC_VISION.md)** · 20-year invariants + v4 positioning (individual-tier vendor-neutral) · figures in `docs/figures/`
|
|
198
|
-
- **[docs/PRD.md](../docs/PRD.md)** · master decision document · features, architecture, roadmap
|
|
199
|
-
- **[docs/SECURITY_AUDIT_2026-05-01.md](../docs/SECURITY_AUDIT_2026-05-01.md)** · first-party security audit · OWASP Top 10 + STRIDE · 0 critical / 0 high · 2 medium fixed inline · methodology disclosed · reproducible
|
|
200
|
-
- **[docs/archive/](../docs/archive/)** · earlier living docs (CODEHERE spec, EXPERIENCE story, AUDIT) preserved for history
|
|
201
|
-
- **[CONTRIBUTING.md](../CONTRIBUTING.md)** · how to contribute · setup, commit discipline, testing philosophy
|
|
202
|
-
- **[SECURITY.md](../SECURITY.md)** · responsible disclosure · `security@codehere.uk`
|
|
203
|
-
- **[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)** · community standards
|
|
204
|
-
- **[CHANGELOG.md](../CHANGELOG.md)** · per-version release notes
|
|
205
|
-
|
|
206
|
-
## Requirements
|
|
207
|
-
|
|
208
|
-
- **Node.js 20+** (pinned in `.nvmrc` and `engines.node`)
|
|
209
|
-
- **macOS or Linux** · Windows via WSL works for development
|
|
210
|
-
- **Optional but recommended:** at least one supported agent (Claude Code, Codex, Aider, OpenCode, or Gemini) installed before running `codehere start`
|
|
58
|
+
cd agent
|
|
59
|
+
npm ci
|
|
60
|
+
npm test # build, then vitest
|
|
61
|
+
npm run lint
|
|
62
|
+
node eval/false-done.mjs
|
|
63
|
+
```
|
|
211
64
|
|
|
212
65
|
## License
|
|
213
66
|
|
|
214
|
-
MIT
|
|
67
|
+
MIT. See [`LICENSE`](https://github.com/muhammadegaa/codehere/blob/main/LICENSE).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did the agent name a test file that is not in the repo?
|
|
3
|
+
*
|
|
4
|
+
* "The webhook tests passed" while the file never existed is a lie a green re-run cannot see:
|
|
5
|
+
* the suite passes because it never contained the test. Given the agent's own words, this lists
|
|
6
|
+
* the test files it named that exist nowhere in the checkout. A file counts as present when any
|
|
7
|
+
* file with the same name exists (so a wrong folder is not flagged). Deterministic, no model.
|
|
8
|
+
* It cannot tell that a named test never ran, only that it is not there.
|
|
9
|
+
*/
|
|
10
|
+
import { readdirSync } from 'node:fs';
|
|
11
|
+
import { basename, join } from 'node:path';
|
|
12
|
+
const TEST_FILE = /^(?:[\w.@-]+\/)*(?:test_[\w.-]+\.py|[\w.-]+_test\.(?:py|go)|[\w.-]+\.(?:test|spec)\.[cm]?[jt]sx?|[\w-]+Test\.php)$/;
|
|
13
|
+
const SKIP_DIRS = new Set([
|
|
14
|
+
'node_modules',
|
|
15
|
+
'.git',
|
|
16
|
+
'dist',
|
|
17
|
+
'build',
|
|
18
|
+
'.venv',
|
|
19
|
+
'venv',
|
|
20
|
+
'__pycache__',
|
|
21
|
+
'vendor',
|
|
22
|
+
'target',
|
|
23
|
+
]);
|
|
24
|
+
const MAX_DEPTH = 8;
|
|
25
|
+
const MAX_ENTRIES = 50_000;
|
|
26
|
+
/** Separators: whitespace, quotes, backticks, brackets of every kind, markdown emphasis and table pipes. */
|
|
27
|
+
const SEPARATORS = /[\s`'"(),;[\]<>{}|*]+/;
|
|
28
|
+
/** `tests/test_x.py::test_y` → `tests/test_x.py`; `a.spec.ts:42:3` → `a.spec.ts`; `_a.test.js_` → `a.test.js`; trailing punctuation dropped. */
|
|
29
|
+
function cleanToken(raw) {
|
|
30
|
+
let t = raw.replace(/^\.\//, '').replace(/::.*$/, '');
|
|
31
|
+
t = t.replace(/[.:…!?]+$/, '').replace(/(:\d+)+$/, '');
|
|
32
|
+
if (t.length > 2 && t.startsWith('_') && t.endsWith('_'))
|
|
33
|
+
t = t.slice(1, -1);
|
|
34
|
+
return t;
|
|
35
|
+
}
|
|
36
|
+
export function extractClaimedTestFiles(text) {
|
|
37
|
+
const found = [];
|
|
38
|
+
for (const raw of text.split(SEPARATORS)) {
|
|
39
|
+
const token = cleanToken(raw);
|
|
40
|
+
if (token.startsWith('/') || token.split('/').includes('..'))
|
|
41
|
+
continue;
|
|
42
|
+
if (TEST_FILE.test(token) && !found.includes(token))
|
|
43
|
+
found.push(token);
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
function namesInRepo(cwd) {
|
|
48
|
+
const names = new Set();
|
|
49
|
+
let seen = 0;
|
|
50
|
+
const walk = (dir, depth) => {
|
|
51
|
+
if (depth > MAX_DEPTH || seen > MAX_ENTRIES)
|
|
52
|
+
return;
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
seen++;
|
|
62
|
+
if (e.isDirectory()) {
|
|
63
|
+
if (!SKIP_DIRS.has(e.name))
|
|
64
|
+
walk(join(dir, e.name), depth + 1);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
names.add(e.name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
walk(cwd, 0);
|
|
72
|
+
return names;
|
|
73
|
+
}
|
|
74
|
+
export function findMissingClaimedTests(cwd, claim) {
|
|
75
|
+
const claimed = extractClaimedTestFiles(claim);
|
|
76
|
+
if (!claimed.length)
|
|
77
|
+
return [];
|
|
78
|
+
const present = namesInRepo(cwd);
|
|
79
|
+
return claimed.filter((p) => !present.has(basename(p)));
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=check-claimed-tests.js.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did the change make a test pass without checking anything?
|
|
3
|
+
*
|
|
4
|
+
* A green re-run proves nothing when the agent got there by editing the test: swapping a failing
|
|
5
|
+
* assertion for `assert True`, skipping the test, commenting the assertion out, or returning above
|
|
6
|
+
* it. This reads `git diff` against HEAD and flags such lines added to test files that already
|
|
7
|
+
* existed. It is a deterministic diff read (no model), so it is measured evidence (DESIGN.md §1).
|
|
8
|
+
* By default it reads uncommitted changes against HEAD. With a base ref it reads everything since
|
|
9
|
+
* the branch left that base (committed or not), which is the view a pull request needs.
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join, resolve } from 'node:path';
|
|
14
|
+
const TEST_PATH = /(^|\/)(test_[^/]*\.py|[^/]*_test\.py|[^/]*\.(test|spec)\.[cm]?[jt]sx?|[^/]*_test\.go|[^/]*Test\.php)$|(^|\/)(__tests__|tests?)\//;
|
|
15
|
+
const TRIVIAL = [
|
|
16
|
+
// `assert True`, `assert(True)`, `assert True, "message"`
|
|
17
|
+
/^assert\s*\(?\s*(True|1|not\s+False)\s*\)?\s*(,.*)?(#.*)?$/,
|
|
18
|
+
/^assert\s+([\w.'"]+)\s*==\s*\1\s*(#.*)?$/,
|
|
19
|
+
/\bexpect\(\s*(true|1)\s*\)\s*\.\s*(toBe|toEqual|toStrictEqual)\(\s*(true|1)\s*\)/,
|
|
20
|
+
/\bexpect\(\s*([\w.'"]+)\s*\)\s*\.\s*(toBe|toEqual|toStrictEqual)\(\s*\1\s*\)/,
|
|
21
|
+
/\bexpect\(\s*true\s*\)\s*\.\s*toBeTruthy\(\)/,
|
|
22
|
+
/\bassert(\.ok)?\(\s*true\s*\)/,
|
|
23
|
+
/\bassert\.(equal|strictEqual|deepEqual|deepStrictEqual)\(\s*([\w.'"]+)\s*,\s*\2\s*\)/,
|
|
24
|
+
/\bassert(Equal|Equals|Same)\(\s*([\w.'"]+)\s*,\s*\2\s*\)/,
|
|
25
|
+
/\bassertTrue\(\s*(true|True)\s*\)/,
|
|
26
|
+
];
|
|
27
|
+
const SKIP = [
|
|
28
|
+
/@pytest\.mark\.(skip|xfail)\b/,
|
|
29
|
+
/@pytest\.mark\.skipif\(\s*True\b/,
|
|
30
|
+
/\bpytest\.(skip|xfail)\(/,
|
|
31
|
+
/@unittest\.skip/,
|
|
32
|
+
/\b(it|test|describe)\.(skip|todo)\(/,
|
|
33
|
+
/\b(xit|xtest|xdescribe)\(/,
|
|
34
|
+
/\bmarkTest(Skipped|Incomplete)\(/,
|
|
35
|
+
/\bt\.Skip(f|Now)?\(/,
|
|
36
|
+
// node:test options: { skip: true } / { skip: 'reason' } / { todo: true }; a conditional skip is left alone
|
|
37
|
+
/\b(skip|todo)\s*:\s*(true\b|['"`])/,
|
|
38
|
+
];
|
|
39
|
+
/** `.only` makes jest, mocha and Playwright skip every other test: the original pattern, on any test file. */
|
|
40
|
+
const ONLY = /\b(it|test|describe)\.only\(/;
|
|
41
|
+
/** `fit` and `fdescribe` do the same, but `fit` is also an ordinary function name: JS, TS and Vue files, at a statement
|
|
42
|
+
* start, after `=>`, or after `void`, `await` or `return`. */
|
|
43
|
+
const JS_FILE = /\.([cm]?[jt]sx?|vue)$/i;
|
|
44
|
+
const JS_FOCUS = /(^|[;{}]\s*|=>\s*|\b(void|await|return)\s+)(fit|fdescribe)\(/;
|
|
45
|
+
/** The line with quoted text emptied and a trailing `//` comment dropped, so neither can pose as a call. */
|
|
46
|
+
const jsCode = (text) => text.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, '""').replace(/\/\/.*$/, '');
|
|
47
|
+
const ASSERTION = /\b(assert\w*|expect|should|self\.assert\w*|this->assert\w*|t\.(Error|Fatal)\w*)\s*[.(]|^\s*assert\s+\S/;
|
|
48
|
+
const BARE_RETURN = /^return\s*;?$/;
|
|
49
|
+
/** A line that opens a branch: a return under it is conditional, not an unconditional skip. */
|
|
50
|
+
const CONDITIONAL = /^(\}\s*)?(if|else|for|while|switch|case|default|catch|try|finally)\b/;
|
|
51
|
+
/** How far below a bare return an assertion still counts as the one it hides. */
|
|
52
|
+
const RETURN_LOOKAHEAD = 8;
|
|
53
|
+
function uncomment(text) {
|
|
54
|
+
const m = /^(?:\/\/+|#+|\/\*+|\*)\s?(.*?)\s*(?:\*\/)?$/.exec(text);
|
|
55
|
+
return m ? m[1].trim() : null;
|
|
56
|
+
}
|
|
57
|
+
/** The commented text is an assertion that the same change removed (alone on its line, or inside a one-line test). */
|
|
58
|
+
function wasCommentedOut(body, removed) {
|
|
59
|
+
const core = body.replace(/;$/, '');
|
|
60
|
+
return core.length >= 12 && ASSERTION.test(core) && removed.some((r) => r.includes(core));
|
|
61
|
+
}
|
|
62
|
+
/** `b/path` or git's C-quoted `"b/p\"ath"` form → `path`. */
|
|
63
|
+
function headerPath(raw) {
|
|
64
|
+
// git appends a tab to the header when the path contains a space.
|
|
65
|
+
const v = raw.slice(4).replace(/\t$/, '');
|
|
66
|
+
if (v.startsWith('b/'))
|
|
67
|
+
return v.slice(2);
|
|
68
|
+
if (v.startsWith('"b/') && v.endsWith('"')) {
|
|
69
|
+
return v.slice(3, -1).replace(/\\(["\\tn])/g, (_, c) => (c === 't' ? '\t' : c === 'n' ? '\n' : c));
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
export function parseTestWeakening(diff, readLines) {
|
|
74
|
+
const found = [];
|
|
75
|
+
let file = null;
|
|
76
|
+
let existing = false;
|
|
77
|
+
let line = 0;
|
|
78
|
+
let removed = [];
|
|
79
|
+
// File headers (`--- a/...`, `+++ b/...`) only appear between `diff --git` and the first hunk, so an added line
|
|
80
|
+
// whose text starts with `++ ` cannot pose as a header.
|
|
81
|
+
let inHeader = false;
|
|
82
|
+
for (const raw of diff.split('\n')) {
|
|
83
|
+
if (raw.startsWith('diff --git ')) {
|
|
84
|
+
file = null;
|
|
85
|
+
existing = true;
|
|
86
|
+
removed = [];
|
|
87
|
+
inHeader = true;
|
|
88
|
+
}
|
|
89
|
+
else if (inHeader && raw.startsWith('--- ')) {
|
|
90
|
+
existing = raw !== '--- /dev/null';
|
|
91
|
+
}
|
|
92
|
+
else if (inHeader && raw.startsWith('+++ ')) {
|
|
93
|
+
const path = headerPath(raw);
|
|
94
|
+
file = path && existing && TEST_PATH.test(path) ? path : null;
|
|
95
|
+
}
|
|
96
|
+
else if (raw.startsWith('@@')) {
|
|
97
|
+
inHeader = false;
|
|
98
|
+
const m = /\+(\d+)/.exec(raw);
|
|
99
|
+
line = m ? Number(m[1]) : 0;
|
|
100
|
+
removed = [];
|
|
101
|
+
}
|
|
102
|
+
else if (raw.startsWith('-')) {
|
|
103
|
+
removed.push(raw.slice(1).trim());
|
|
104
|
+
}
|
|
105
|
+
else if (raw.startsWith('+') && file) {
|
|
106
|
+
const text = raw.slice(1).trim();
|
|
107
|
+
const body = uncomment(text);
|
|
108
|
+
let why = null;
|
|
109
|
+
if (TRIVIAL.some((re) => re.test(text)))
|
|
110
|
+
why = 'trivial-assertion';
|
|
111
|
+
else if (SKIP.some((re) => re.test(text)) ||
|
|
112
|
+
ONLY.test(jsCode(text)) ||
|
|
113
|
+
(JS_FILE.test(file) && JS_FOCUS.test(jsCode(text))))
|
|
114
|
+
why = 'skipped';
|
|
115
|
+
else if (body !== null && wasCommentedOut(body, removed))
|
|
116
|
+
why = 'commented-out';
|
|
117
|
+
else if (BARE_RETURN.test(text)) {
|
|
118
|
+
const lines = readLines?.(file);
|
|
119
|
+
const above = (lines?.[line - 2] ?? '').trim();
|
|
120
|
+
const below = lines?.slice(line, line + RETURN_LOOKAHEAD) ?? [];
|
|
121
|
+
if (!CONDITIONAL.test(above) && below.some((l) => ASSERTION.test(l)))
|
|
122
|
+
why = 'unreachable-assertion';
|
|
123
|
+
}
|
|
124
|
+
if (why)
|
|
125
|
+
found.push({ file, line, why, text: text.slice(0, 80) });
|
|
126
|
+
line++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return found;
|
|
130
|
+
}
|
|
131
|
+
/** The commit the current branch left `base` at, so changes made on `base` since are not blamed on the branch. */
|
|
132
|
+
function resolveBase(cwd, base) {
|
|
133
|
+
if (base.startsWith('-'))
|
|
134
|
+
throw new Error(`Invalid --base "${base}": a ref cannot start with "-".`);
|
|
135
|
+
try {
|
|
136
|
+
return execFileSync('git', ['merge-base', base, 'HEAD'], {
|
|
137
|
+
cwd,
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
140
|
+
}).trim();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new Error(`Unknown --base "${base}": not a ref in this repository.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const MAX_DIFF = 64 * 1024 * 1024;
|
|
147
|
+
/** git with literal paths and unquoted non-ASCII names. */
|
|
148
|
+
function git(cwd, args) {
|
|
149
|
+
// GIT_DIFF_OPTS overrides --unified=0, and context lines would shift every line number after them.
|
|
150
|
+
const env = { ...process.env };
|
|
151
|
+
delete env.GIT_DIFF_OPTS;
|
|
152
|
+
return execFileSync('git', ['-c', 'core.quotepath=false', '--literal-pathspecs', ...args], {
|
|
153
|
+
cwd,
|
|
154
|
+
env,
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
157
|
+
maxBuffer: MAX_DIFF,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function hasGitAbove(cwd) {
|
|
161
|
+
for (let d = resolve(cwd);; d = dirname(d)) {
|
|
162
|
+
if (existsSync(join(d, '.git')))
|
|
163
|
+
return true;
|
|
164
|
+
if (dirname(d) === d)
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Changes in `cwd` against HEAD, or against where the branch left `base` when it is given. Only test files are
|
|
170
|
+
* diffed, so a large change elsewhere cannot push the test diff past the read limit.
|
|
171
|
+
*/
|
|
172
|
+
export function scanTestWeakening(cwd, base) {
|
|
173
|
+
try {
|
|
174
|
+
git(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
if (e.code === 'ENOENT')
|
|
178
|
+
return { findings: [], error: 'git is not installed' };
|
|
179
|
+
// Skipped only when there is no repository at all. Any other git failure (GIT_DIR, ownership checks, a broken
|
|
180
|
+
// .git) means the scan should have run and did not; with --base a repository is required.
|
|
181
|
+
if (!base && !hasGitAbove(cwd))
|
|
182
|
+
return { findings: [], skipped: 'not a git repository' };
|
|
183
|
+
return { findings: [], error: 'git could not read this repository; run `git status` here to see why' };
|
|
184
|
+
}
|
|
185
|
+
if (!base) {
|
|
186
|
+
try {
|
|
187
|
+
git(cwd, ['rev-parse', '--verify', '--quiet', 'HEAD']);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return { findings: [], skipped: 'no commit to compare against yet' };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const from = base ? resolveBase(cwd, base) : 'HEAD';
|
|
194
|
+
// Fixed diff options, so a user's or the change's own git config cannot alter what the parser reads: paths
|
|
195
|
+
// relative to `cwd` (a package folder inside a repository), a/ and b/ prefixes, no external diff or textconv,
|
|
196
|
+
// binary attributes ignored, renames paired.
|
|
197
|
+
const opts = ['--relative', '--no-ext-diff', '--no-textconv', '--text', '--src-prefix=a/', '--dst-prefix=b/', '-M'];
|
|
198
|
+
let tests;
|
|
199
|
+
try {
|
|
200
|
+
// -z name-status: status, path (and for renames and copies a second path), NUL-separated. Both paths of a
|
|
201
|
+
// rename go into the pathspec so git can pair them; a test renamed and weakened is still an existing test.
|
|
202
|
+
const fields = git(cwd, ['diff', ...opts, '--name-status', '-z', from]).split('\0');
|
|
203
|
+
tests = [];
|
|
204
|
+
for (let i = 0; i < fields.length - 1;) {
|
|
205
|
+
const status = fields[i++];
|
|
206
|
+
const paths = /^[RC]/.test(status) ? [fields[i++], fields[i++]] : [fields[i++]];
|
|
207
|
+
if (paths.some((p) => p && TEST_PATH.test(p)))
|
|
208
|
+
tests.push(...paths.filter(Boolean));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return { findings: [], error: 'could not read the diff (the list of changed files)' };
|
|
213
|
+
}
|
|
214
|
+
if (!tests.length)
|
|
215
|
+
return { findings: [] };
|
|
216
|
+
let diff;
|
|
217
|
+
try {
|
|
218
|
+
diff = git(cwd, ['diff', ...opts, '--unified=0', '--no-color', from, '--', ...tests]);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return {
|
|
222
|
+
findings: [],
|
|
223
|
+
error: `could not read the diff of ${tests.length} changed test file(s); it may be larger than 64 MB`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
findings: parseTestWeakening(diff, (f) => {
|
|
228
|
+
try {
|
|
229
|
+
return readFileSync(join(cwd, f), 'utf8').split('\n');
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=detect-test-weakening.js.map
|