From 2b024387241a9fc3c381a52e68ea73ea27fd10bd Mon Sep 17 00:00:00 2001
From: enixCode <58286681+enixCode@users.noreply.github.com>
Date: Fri, 17 Apr 2026 15:36:48 +0200
Subject: [PATCH] add AGENTS.md, modern rules dirs, and auto-gitignore
- init: add AGENTS.md (cross-tool standard), .cursor/rules/humancov.mdc,
and .windsurf/rules/humancov.md support alongside legacy paths
- ignore: auto-load .gitignore (resolution: defaults -> .gitignore -> .humancov-ignore)
- README: move AI Tool Setup section to top, switch install to --save-dev
- docs: align site with README (Pre-commit Hook, Ignore Files, Manifest)
- CLAUDE.md: document GitHub Flow workflow
- tests: +8 init tests, +2 ignore tests (58/58 passing)
build with cc
---
.humancov | 1 +
CLAUDE.md | 77 ++++++++++++++++++++++-
README.md | 144 ++++++++++++++++++++------------------------
docs/index.html | 130 ++++++++++++++++++++++++++++++++++++---
src/ignore.js | 14 +++--
src/init.js | 65 +++++++++++++++-----
test/ignore.test.js | 23 +++++++
test/init.test.js | 110 +++++++++++++++++++++++++++++++++
8 files changed, 457 insertions(+), 107 deletions(-)
create mode 100644 test/init.test.js
diff --git a/.humancov b/.humancov
index 0de90ad..407d850 100644
--- a/.humancov
+++ b/.humancov
@@ -14,6 +14,7 @@ test/badge.test.js ai false false claude-code -
test/cli.test.js ai false false claude-code -
test/comments.test.js ai false false claude-code -
test/ignore.test.js ai false false claude-code -
+test/init.test.js ai false false claude-code -
test/manifest.test.js ai false false claude-code -
test/parser.test.js ai false false claude-code -
test/report.test.js ai false false claude-code -
diff --git a/CLAUDE.md b/CLAUDE.md
index dafd289..c0cd29d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,3 +1,76 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Build and Run
+
+```bash
+npm install # install deps (single dep: `ignore`)
+npm test # run all tests (node:test, no framework)
+node --test test/parser.test.js # run a single test file
+node bench/perf.js # run benchmarks
+npx humancov scan # run the CLI locally
+npm run ci # run CI locally via `act` (requires act installed)
+```
+
+No build step - pure ESM (`"type": "module"` in package.json), runs directly with Node >= 18.
+
+## Architecture
+
+The CLI entry point is `bin/humancov.js` - a thin dispatcher that parses args and delegates to modules in `src/`:
+
+- `scanner.js` - core scan logic. Lists files via `git ls-files` (falls back to recursive walk), filters through ignore patterns, calls parser on each file, aggregates summary stats.
+- `parser.js` - reads first 20 lines of a file, extracts `AI-Provenance-*` headers using comment-syntax-aware parsing.
+- `comments.js` - maps file extensions to comment prefixes (`//`, `#`, `
-```
-```css
-/* AI-Provenance-Origin: ai */ /* CSS, SCSS, Less */
-```
-```sql
--- AI-Provenance-Origin: ai -- SQL, Lua
-```
+| Key | Required | Values |
+|---|---|---|
+| `Origin` | yes | `ai`, `human`, `mixed` |
+| `Generator` | no | tool name (`claude-code`, `copilot`, `codex`...) |
+| `Reviewed` | yes | `true`, `false`, `partial` |
+| `Tested` | no | `true`, `false`, `partial` |
+| `Confidence` | no | `high`, `medium`, `low` |
+| `Notes` | no | free-text |
---
## Ignore Files
-Create `.humancov-ignore` at repo root (gitignore syntax) to exclude files from scanning:
+humancov automatically respects your **`.gitignore`** - no extra setup needed. Anything ignored by git is ignored by humancov.
+
+For humancov-specific ignores on top of `.gitignore`, create `.humancov-ignore` at repo root (same gitignore syntax):
```gitignore
*.md
@@ -153,7 +162,9 @@ dist/
coverage/
```
-Default ignores: `node_modules/`, `.git/`, `.humancov`, `.humancov-ignore`, `*.md`, `*.lock`, `LICENSE`, `.gitignore`, `.github/`.
+**Resolution order:** built-in defaults → `.gitignore` → `.humancov-ignore`.
+
+Built-in defaults: `node_modules/`, `.git/`, `.humancov`, `.humancov-ignore`, `*.md`, `*.lock`, `LICENSE`, `.gitignore`, `.github/`.
---
@@ -192,11 +203,10 @@ Exits with code 1 if the reviewed percentage is below the threshold.
A pre-commit hook keeps the badge and `.humancov` manifest up to date automatically on every commit.
-**Option A - via `humancov init`:**
-
Run `humancov init` and your AI tool will propose installing the hook for you.
-**Option B - manual setup:**
+
+Manual setup
Create `.git/hooks/pre-commit` with this content:
@@ -233,6 +243,8 @@ chmod +x .git/hooks/pre-commit
> **Note:** `.git/hooks/` is local and not shared via git. Each contributor needs to set up the hook on their machine. If your team uses [husky](https://typicode.github.io/husky/) or a similar tool, add the hook content to your shared hooks directory instead.
+
+
---
## Manifest
@@ -275,30 +287,6 @@ Aggregate stats and output report
---
-## Performance
-
-Benchmarked on a standard machine with Node >= 18. Run `node bench/perf.js` to reproduce.
-
-| Operation | Scope | Avg |
-|---|---|---|
-| Comment prefix lookup | 10,000 lookups | ~12ms |
-| Ignore pattern loading | single load | ~0.1ms |
-| Badge URL generation | 1,000 generations | ~0.1ms |
-| Header parsing | 7 files | ~3ms |
-| Full repo scan | 8 tracked files | ~90ms |
-
-**Scaling (scanFiles):**
-
-| Files | Avg | Min | Max |
-|---|---|---|---|
-| 100 | ~120ms | ~80ms | ~220ms |
-| 500 | ~175ms | ~145ms | ~295ms |
-| 1,000 | ~265ms | ~220ms | ~340ms |
-
-Scales near-linearly. The main cost is `git ls-files` + file I/O in the scanner - utility modules add negligible overhead.
-
----
-
## Spec
See the [AI-Provenance Spec v0.1](https://github.com/enixCode/humancov/blob/main/CLAUDE.md) for the full specification.
diff --git a/docs/index.html b/docs/index.html
index 98fd79d..a9a6214 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -497,7 +497,7 @@
Review and track
Teach your AI to tag its code.
-
Run humancov init to detect your AI tool config files and inject provenance instructions automatically.
+
Run humancov init to detect your AI tool config files (or modern rule directories) and inject provenance instructions automatically.
+
@@ -523,7 +532,7 @@ Teach your AI to tag its code.
@@ -548,10 +557,11 @@ Teach your AI to tag its code.
$ humancov init
done : CLAUDE.md (Claude Code instructions added)
- done : .cursorrules (Cursor instructions added)
+ done : AGENTS.md (cross-tool instructions added)
+ done : .cursor/rules/humancov.mdc (Cursor rule file created)
skip : .windsurfrules (already has AI-Provenance instructions)
-2 file(s) updated.
+3 file(s) updated.
@@ -565,15 +575,55 @@ Teach your AI to tag its code.
Ready in seconds.
- npm install -g humancov
+ npm install --save-dev humancov
click to copy
- Requires Node >= 18. Zero config. Single dependency.
+ Install as a dev dependency. Requires Node >= 18. Zero config. Single dependency.
+
+
+
+
+ Automation
+
Pre-commit hook.
+
+
+
A local git hook that auto-updates the badge and .humancov manifest before every commit. Zero manual work to keep your README and manifest in sync.
+
+
+
+
1
+
+
Runs on every git commit
+
Regenerates .humancov from your file headers and stages it.
+
+
+
+
2
+
+
Updates the README badge
+
Replaces the human-reviewed shields.io URL with the fresh percentage.
+
+
+
+
3
+
+
Stages the changes
+
Both .humancov and README.md updates land in the same commit.
+
+
+
+
+
Run humancov init and your AI tool will propose installing the hook for you. Manual setup: create .git/hooks/pre-commit, paste the snippet from the README, and chmod +x it.
+
Note: .git/hooks/ is local and not shared via git. Each contributor installs it once on their machine (or uses husky to share it).
+
+
+
+
+
+
+
+
+ Ignore
+
Skip files from scans.
+
+
+
humancov auto-respects your .gitignore - zero setup. For humancov-specific exclusions on top, create .humancov-ignore at the repo root (same gitignore syntax ).
+
Resolution order: built-in defaults → .gitignore → .humancov-ignore.
+
+
+
+
+
+
+
.humancov-ignore
+
+
+
# exclude docs, lockfiles, build output
+*.md
+LICENSE
+*.lock
+dist/
+coverage/
+
+
+
+ Default ignores: node_modules/, .git/, .humancov, .humancov-ignore, *.md, *.lock, LICENSE, .gitignore, .github/.
+
+
+
+
@@ -738,6 +821,34 @@
Enforce in your pipeline.
+
+
+
+
+ Manifest
+
The .humancov file.
+
+
+
humancov manifest generates a TSV summary of all files with provenance headers. File headers are the source of truth - if both exist and conflict, headers win .
+
+
+
+
+
# .humancov
+# file origin reviewed tested generator confidence
+src/scanner.js ai true false claude-code high
+src/parser.js ai false false claude-code -
+lib/utils.py human true true - -
+
+
+
+
+
@@ -758,7 +869,8 @@
All commands.
$ humancov scan --badge # shields.io badge URL
$ humancov scan --check 80 # CI gate: fail if < 80%
$ humancov manifest # generate .humancov file
-$ humancov init # add instructions to AI tool configs
+$ humancov init # add instructions to AI tool configs
+$ humancov --version # print version
@@ -800,7 +912,7 @@ All commands.
// Copy install command
function copyInstall(el) {
- navigator.clipboard.writeText('npm install -g humancov');
+ navigator.clipboard.writeText('npm install --save-dev humancov');
const hint = el.querySelector('.copy-hint');
hint.textContent = 'copied!';
setTimeout(() => hint.textContent = 'click to copy', 1500);
diff --git a/src/ignore.js b/src/ignore.js
index d248d5a..ef0d007 100644
--- a/src/ignore.js
+++ b/src/ignore.js
@@ -19,15 +19,19 @@ const DEFAULTS = [
'.github/',
];
+const IGNORE_FILES = ['.gitignore', '.humancov-ignore'];
+
export function loadIgnore(rootDir) {
const ig = ignore();
ig.add(DEFAULTS);
- try {
- const content = readFileSync(join(rootDir, '.humancov-ignore'), 'utf8');
- ig.add(content);
- } catch {
- // no ignore file - use defaults only
+ for (const file of IGNORE_FILES) {
+ try {
+ const content = readFileSync(join(rootDir, file), 'utf8');
+ ig.add(content);
+ } catch {
+ // file not present - skip
+ }
}
return ig;
diff --git a/src/init.js b/src/init.js
index 5c123df..1928caa 100644
--- a/src/init.js
+++ b/src/init.js
@@ -3,14 +3,17 @@
// AI-Provenance-Reviewed: false
// AI-Provenance-Tested: false
-import { existsSync, readFileSync, appendFileSync } from 'node:fs';
-import { join } from 'node:path';
+import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { join, dirname } from 'node:path';
const TOOL_CONFIGS = [
- { file: 'CLAUDE.md', generator: 'claude-code', name: 'Claude Code' },
- { file: '.cursorrules', generator: 'cursor', name: 'Cursor' },
- { file: '.windsurfrules', generator: 'windsurf', name: 'Windsurf' },
- { file: '.github/copilot-instructions.md', generator: 'copilot', name: 'GitHub Copilot' },
+ { file: 'CLAUDE.md', generator: 'claude-code', name: 'Claude Code', action: 'append' },
+ { file: 'AGENTS.md', generator: 'agents-md', name: 'AGENTS.md (cross-tool)', action: 'append' },
+ { file: '.cursorrules', generator: 'cursor', name: 'Cursor (legacy)', action: 'append' },
+ { file: '.cursor/rules/humancov.mdc', generator: 'cursor', name: 'Cursor (rules dir)', action: 'create-if-dir', dir: '.cursor/rules', format: 'mdc' },
+ { file: '.windsurfrules', generator: 'windsurf', name: 'Windsurf (legacy)', action: 'append' },
+ { file: '.windsurf/rules/humancov.md', generator: 'windsurf', name: 'Windsurf (rules dir)', action: 'create-if-dir', dir: '.windsurf/rules' },
+ { file: '.github/copilot-instructions.md', generator: 'copilot', name: 'GitHub Copilot', action: 'append' },
];
const HOOK_CONTENT = [
@@ -78,24 +81,58 @@ ${HOOK_INSTRUCTION}
`;
}
+function buildMdcFile(generator) {
+ return `---
+description: humancov AI-Provenance headers
+alwaysApply: true
+---
+${buildInstructions(generator)}`;
+}
+
export function runInit(rootDir) {
let updated = 0;
- for (const { file, generator, name } of TOOL_CONFIGS) {
+ for (const config of TOOL_CONFIGS) {
+ const { file, generator, name, action } = config;
const filePath = join(rootDir, file);
- if (!existsSync(filePath)) continue;
+ if (action === 'append') {
+ if (!existsSync(filePath)) continue;
- const content = readFileSync(filePath, 'utf8');
+ const content = readFileSync(filePath, 'utf8');
+ if (content.includes('AI-Provenance-Origin')) {
+ console.log(` skip: ${file} (already has AI-Provenance instructions)`);
+ continue;
+ }
- if (content.includes('AI-Provenance-Origin')) {
- console.log(` skip: ${file} (already has AI-Provenance instructions)`);
+ appendFileSync(filePath, buildInstructions(generator));
+ console.log(` done: ${file} (${name} instructions added)`);
+ updated++;
continue;
}
- appendFileSync(filePath, buildInstructions(generator));
- console.log(` done: ${file} (${name} instructions added)`);
- updated++;
+ if (action === 'create-if-dir') {
+ const dirPath = join(rootDir, config.dir);
+ if (!existsSync(dirPath)) continue;
+
+ if (existsSync(filePath)) {
+ const content = readFileSync(filePath, 'utf8');
+ if (content.includes('AI-Provenance-Origin')) {
+ console.log(` skip: ${file} (already has AI-Provenance instructions)`);
+ continue;
+ }
+ appendFileSync(filePath, buildInstructions(generator));
+ console.log(` done: ${file} (${name} instructions appended)`);
+ updated++;
+ continue;
+ }
+
+ mkdirSync(dirname(filePath), { recursive: true });
+ const body = config.format === 'mdc' ? buildMdcFile(generator) : buildInstructions(generator).trimStart();
+ writeFileSync(filePath, body);
+ console.log(` done: ${file} (${name} file created)`);
+ updated++;
+ }
}
if (updated === 0) {
diff --git a/test/ignore.test.js b/test/ignore.test.js
index 39f04cb..18befe9 100644
--- a/test/ignore.test.js
+++ b/test/ignore.test.js
@@ -65,4 +65,27 @@ describe('loadIgnore', () => {
assert.ok(!ig.ignores('src/app.js'));
cleanup();
});
+
+ it('loads .gitignore patterns automatically', () => {
+ setup();
+ writeFileSync(join(TMP, '.gitignore'), 'build/\n*.log\nsecret.txt\n', 'utf8');
+ const ig = loadIgnore(TMP);
+ assert.ok(ig.ignores('build/output.js'));
+ assert.ok(ig.ignores('debug.log'));
+ assert.ok(ig.ignores('secret.txt'));
+ // defaults still apply
+ assert.ok(ig.ignores('node_modules/foo.js'));
+ cleanup();
+ });
+
+ it('combines .gitignore and .humancov-ignore', () => {
+ setup();
+ writeFileSync(join(TMP, '.gitignore'), 'build/\n', 'utf8');
+ writeFileSync(join(TMP, '.humancov-ignore'), 'vendor/\n', 'utf8');
+ const ig = loadIgnore(TMP);
+ assert.ok(ig.ignores('build/x.js'));
+ assert.ok(ig.ignores('vendor/x.js'));
+ assert.ok(!ig.ignores('src/app.js'));
+ cleanup();
+ });
});
diff --git a/test/init.test.js b/test/init.test.js
new file mode 100644
index 0000000..304b366
--- /dev/null
+++ b/test/init.test.js
@@ -0,0 +1,110 @@
+// AI-Provenance-Origin: ai
+// AI-Provenance-Generator: claude-code
+// AI-Provenance-Reviewed: false
+// AI-Provenance-Tested: false
+
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { runInit } from '../src/init.js';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const TMP = join(__dirname, '..', '.test-tmp-init');
+
+function setup() {
+ rmSync(TMP, { recursive: true, force: true });
+ mkdirSync(TMP, { recursive: true });
+}
+
+function cleanup() {
+ rmSync(TMP, { recursive: true, force: true });
+}
+
+describe('runInit', () => {
+ it('appends instructions to existing CLAUDE.md', () => {
+ setup();
+ writeFileSync(join(TMP, 'CLAUDE.md'), '# My project\n');
+ runInit(TMP);
+ const content = readFileSync(join(TMP, 'CLAUDE.md'), 'utf8');
+ assert.ok(content.includes('AI-Provenance-Origin'));
+ assert.ok(content.includes('claude-code'));
+ cleanup();
+ });
+
+ it('appends instructions to existing AGENTS.md', () => {
+ setup();
+ writeFileSync(join(TMP, 'AGENTS.md'), '# Agents\n');
+ runInit(TMP);
+ const content = readFileSync(join(TMP, 'AGENTS.md'), 'utf8');
+ assert.ok(content.includes('AI-Provenance-Origin'));
+ assert.ok(content.includes('agents-md'));
+ cleanup();
+ });
+
+ it('skips files that already contain AI-Provenance', () => {
+ setup();
+ writeFileSync(join(TMP, 'CLAUDE.md'), '# My project\nAI-Provenance-Origin: ai\n');
+ runInit(TMP);
+ const content = readFileSync(join(TMP, 'CLAUDE.md'), 'utf8');
+ const matches = content.match(/AI-Provenance-Origin/g) || [];
+ assert.equal(matches.length, 1);
+ cleanup();
+ });
+
+ it('does not create files that do not exist (for append-only configs)', () => {
+ setup();
+ runInit(TMP);
+ assert.ok(!existsSync(join(TMP, 'CLAUDE.md')));
+ assert.ok(!existsSync(join(TMP, 'AGENTS.md')));
+ assert.ok(!existsSync(join(TMP, '.cursorrules')));
+ cleanup();
+ });
+
+ it('creates .cursor/rules/humancov.mdc when .cursor/rules dir exists', () => {
+ setup();
+ mkdirSync(join(TMP, '.cursor', 'rules'), { recursive: true });
+ runInit(TMP);
+ const filePath = join(TMP, '.cursor', 'rules', 'humancov.mdc');
+ assert.ok(existsSync(filePath));
+ const content = readFileSync(filePath, 'utf8');
+ assert.ok(content.startsWith('---'));
+ assert.ok(content.includes('alwaysApply: true'));
+ assert.ok(content.includes('AI-Provenance-Origin'));
+ cleanup();
+ });
+
+ it('creates .windsurf/rules/humancov.md when .windsurf/rules dir exists', () => {
+ setup();
+ mkdirSync(join(TMP, '.windsurf', 'rules'), { recursive: true });
+ runInit(TMP);
+ const filePath = join(TMP, '.windsurf', 'rules', 'humancov.md');
+ assert.ok(existsSync(filePath));
+ const content = readFileSync(filePath, 'utf8');
+ assert.ok(content.includes('AI-Provenance-Origin'));
+ cleanup();
+ });
+
+ it('skips rules dir files when parent dir does not exist', () => {
+ setup();
+ runInit(TMP);
+ assert.ok(!existsSync(join(TMP, '.cursor', 'rules', 'humancov.mdc')));
+ assert.ok(!existsSync(join(TMP, '.windsurf', 'rules', 'humancov.md')));
+ cleanup();
+ });
+
+ it('handles multiple configs in one run', () => {
+ setup();
+ writeFileSync(join(TMP, 'CLAUDE.md'), '# Claude\n');
+ writeFileSync(join(TMP, 'AGENTS.md'), '# Agents\n');
+ writeFileSync(join(TMP, '.cursorrules'), '# Cursor legacy\n');
+ mkdirSync(join(TMP, '.windsurf', 'rules'), { recursive: true });
+ runInit(TMP);
+ assert.ok(readFileSync(join(TMP, 'CLAUDE.md'), 'utf8').includes('AI-Provenance-Origin'));
+ assert.ok(readFileSync(join(TMP, 'AGENTS.md'), 'utf8').includes('AI-Provenance-Origin'));
+ assert.ok(readFileSync(join(TMP, '.cursorrules'), 'utf8').includes('AI-Provenance-Origin'));
+ assert.ok(existsSync(join(TMP, '.windsurf', 'rules', 'humancov.md')));
+ cleanup();
+ });
+});