@g48in/reqfile-colocation

中文文档
0.4.0发布于 2026/11/02MIT OR Apache-2.0ES Module构建来源已验证近 30 天下载 7752

reqfile checker for COLOCATION: each feature owns its code, exposes an interface, and can be deleted on its own

原始使用文档
文档目录12 节
  1. Use it
  2. The checker
  3. How it reads code
  4. Cache
  5. Blocking rules
  6. colocation.yaml (optional, strict)
  7. Measures
  8. On real repositories
  9. Limits
  10. Adopting it as a blocking check
  11. Measure it
  12. Release

reqfile-colocation

A shareable reqfile requirement: COLOCATION, each feature owns its code, exposes an interface, and can be deleted on its own.

This is a spike of what a community requirement package looks like: the requirement, its checker and its labeled benchmark, in one repository with the same layout as a requirement inside any project.

Reqfile.yaml                      # the requirement, same format as a local one
.reqfile/COLOCATION/examples/     # labeled benchmark: example.yaml (label, expected findings, rationale) + files/
src/                              # the checker, in TypeScript, published to npm

Use it

Try it, then adopt it (reqfile 0.3 or later, with Bun installed):

reqfile eval --use gabsn/[email protected]    # its score on its examples
reqfile check --use gabsn/[email protected]   # its findings on your code
reqfile add gabsn/[email protected]           # writes the line below

which writes:

code:
  - { id: COLOCATION, use: gabsn/reqfile-colocation@<commit> }  # v0.4.0

The checks run bunx @g48in/[email protected] verify (blocking) and ... suggest (advisory): Bun fetches the checker and its dependencies (TypeScript 6, web-tree-sitter, yaml) from npm once and caches them. Python repositories need python3. With Node only, use npx -y instead of bunx.

The checker

reqfile-colocation runs from the repository root and reads the whole repository (respecting .gitignore). It has two commands, declared as two checks of COLOCATION:

  • verify, blocking. Rules read from the resolved dependency graph and a few explicit conventions (an index.ts or re-exporting __init__.py is an interface, test files are recognized by name, a workflow step of 8 or more lines is a feature's logic). A finding is a fact about resolved code under these conventions, not a proof of an architectural conclusion: the conventions are style choices, documented below. Every judged file ends in one of three states, counted on stderr:

    • compliant: a SARIF pass result;
    • violation: a SARIF fail result naming its rule, file and line;
    • unverifiable: a local import that resolves to nothing, a file that does not parse, a mod x; without its file, a stale exception, Python files without python3. Listed on stderr; the run exits 2, which reqfile reports as an error that fails the run.

    Exit 0 only when every file was verified and no rule is broken; 1 when there are violations and everything was verified; 2 when anything was not.

  • suggest, advisory. Heuristics that need to know which files form one feature, which the code does not say: ownership read from importers, one feature split across tier folders (split: a shared folder name and a dependency, as app/garden and infra/garden, or oss/widget beside widget/evals), items used only below their file, grab-bag files, features loose in a folder, layer-named folders. Each finding carries a probability; none blocks.

How it reads code

LanguageParserResolutionBoundary (interface)
TypeScript, JavaScriptTypeScript 6's syntax treeTypeScript's module resolution with the nearest tsconfig/jsconfig (extends, paths, baseUrl); workspace packages by their package.json name, exports, main; assets through aliasesa folder whose index.* exports
Pythonpython3's astthe importer's folder, pyproject/setup roots and their src/, the repository root; a regular package wins over a namespace folder; names bound under if/try counta package whose __init__.py re-exports or defines __all__; a name it exports goes through it
Rusttree-sitter's Rust grammarthe module tree from every crate root through mod (with #[path]); use trees, qualified paths, paths inside macro argumentsrustc enforces visibility, so no path that compiles is a bypass

Dependencies outside imports count too: paths in workflow steps, shell scripts, package.json scripts/bin/exports, task and runner files (mise.toml, Makefile, Dockerfile, wrangler.toml...), manifests and string literals. A path into another package depends on that package.

Cache

A second run on an unchanged tree only checks that it is unchanged, and an edited file is parsed again alone (since 0.4.0). The cache lives in $REQFILE_CACHE/colocation, else in the git folder, .git/reqfile/colocation, shared by every worktree, beside reqfile's own answers. It follows the repository: a CI job starts cold unless it keeps that folder between runs. Every entry is keyed on what the analysis reads:

  • What a file says on its own (its imports, path literals, parse error) is keyed by its git blob hash and its name: an edited file is parsed again, the others are not (TypeScript, JavaScript and Python; Rust files are parsed again whenever the result is recomputed).
  • The whole result, analysis and report, is keyed on the checker's own code, its runtime and dependencies' versions, the repository's folder and the blob hash of every file git lists: the index's, else the bytes the analysis reads (modified and untracked files, symlinks through to their target, files marked assume-unchanged or skip-worktree). Any change recomputes it: module resolution and Rust parsing then run in full. Module resolution also reads what git does not list (node_modules, ignored build output, a tsconfig extended from a package): each such lookup is recorded with its answer, and the result is reused only if every lookup still answers the same. verify then suggest on one tree share one analysis.

Evidence that the cache does not change a report: on every example of the four frozen corpora and on arkadia, cold and warm runs print byte for byte what 0.3.0 prints, version aside (unchanged trees); src/cache.test.ts changes one input at a time (an edit, a revert, a symlink's target, a flagged file, tsconfig and package.json, a new, deleted or ignored file, an installed package) and checks that the warm run prints what a cold one does. Not covered: git clean/smudge filters, whose working copy differs from the blob a fact is keyed on. Delete the folder to start cold.

On arkadia (about 2,300 files, 1,000 of them TypeScript; medians of 5 interleaved runs on a laptop):

Run0.3.00.4.0
verify, empty cache1.54 s1.38 s
verify then suggest, empty cache3.23 s1.84 s
verify, nothing changed1.54 s0.20 s
verify, one file edited1.54 s0.76 s

Blocking rules

RuleBreaks when
interfacean import from outside a boundary lands on an implementation file instead of its interface (a package's own files may reach past its root index; generated code is exempt)
testsa test lives outside the folder of the code it tests (its namesake, else the production code it imports); a test of entry points or of several features of a package may live at the package root; Rust tests/, benches/, examples/ and migrations are tool-required
entrya workflow step runs 8 or more lines of shell on the repository's folders instead of calling a script there (8 is a style convention for "thin", not a proof that the lines are a feature's logic)
rootsunder a root declared in colocation.yaml, a file several of its features use is neither declared shared nor inside one feature

colocation.yaml (optional, strict)

roots:                       # make ownership checkable where it matters
  - path: src                # each direct file or folder of src/ is a feature
    shared: [src/format.rs]  # the modules several features share, by decision
exceptions:                  # explicit, limited, reported in every run
  - path: test/**
    rule: tests
    reason: the deployment image runs test/ only

An unknown key, a declared path that does not exist, or an exception that matches no finding fails the run (exit 2): exceptions cannot outlive the code they excuse. Excepted findings stay in the SARIF as accepted suppressions.

Measures

All versions on the same frozen corpus (corpus-v4, 73 examples): 45 development examples (the last five from a principal engineer's review), 16 + 12 confirmation examples written by two independent authors from the requirement alone, never used for tuning (two relabeled ok by the author's decision on cargo's tests/, see LABELS.md). Caught violations / false alarms on correct examples:

VersionDevelopmentConfirmation 1Confirmation 2
0.1.310/20, 3/250/7, 4/91/5, 0/7
0.2.017/20, 4/251/7, 3/91/5, 2/7
0.3.0 verify (blocking)8/20, 0/251/7, 0/91/5, 0/7
0.3.0 verify + suggest20/20, 1/252/7, 2/92/5, 2/7

Confirmation 2 was first measured once with the frozen candidate (0 caught, 0 false alarms). Its miss violation-ts-monorepo-emails showed that workspace packages were not resolved without node_modules; the fix that followed catches it, so the 1/5 above is contaminated by having seen the case.

What the numbers say: the blocking check raised no false alarm on 41 correct examples, 25 of them development examples it was tuned on and 16 confirmation examples it was not; on confirmation it catches few violations (2 of 12, one of them contaminated). Most violations in the corpus are about ownership (which files form one feature), which the code alone does not decide. 0.2.0's 16/19 in development fell to 2/12 on independent cases: its heuristics fitted their own examples.

On real repositories

verify on eight repositories (arkadia, reqfile-colocation, voxrouter, platform, skoolradar, brain, mini, focustree): 0.3 s to 2 s each; every unverifiable import left is a real broken import (3 in voxrouter). Independent reviewers opened the code behind random samples of findings:

RuleTrueFalseUnsure
tests1402
interface (two rounds)1413
entry41 (then fixed: 8-line threshold)1
split (then moved to suggest, see below)912
owner, read from importers1161 → moved to suggest

A reviewer also searched 36 random unflagged files for missed violations and found 17, most needing feature knowledge (tiers named alike, file-name prefixes) or links only an import back into src/ shows; interface bypasses by tests and test-tree support files were fixed afterwards. Details: LABELS.md and DESIGN.md.

Limits

  • Ownership is not in the code: a domain used by one surface may be that surface's, or a domain the surface merely uses. verify decides it only under declared roots; elsewhere suggest points at it.
  • split is advisory: a shared folder name and a dependency do not prove one feature's identity. Two domains each with an api/ layer, one calling the other's interface, read as a split; adding unrelated api/ folders makes the finding disappear (names repeated more than three times are layers). It becomes blockable only when the features or tiers are declared.
  • A split between files of different names (src/public/wrap.ts and src/internal/break-words.ts) is not seen.
  • Path references are read from literal paths: a path computed at run time, and fixtures only tests name by path, are not seen.
  • A shared tool and one of its users named alike (platform/agent-evals, voxrouter/agent-evals) reads as a split when no other code uses the tool.
  • Python dynamic imports and Rust items generated by macros are not read.
  • Inline logic in a workflow that names no repository path is not counted.

Adopting it as a blocking check

Make the verify check mode: blocking (the default) in a repository when:

  1. on the frozen corpus its rules raise no false alarm, development and confirmation alike (met by 0.3.0: 0/35);
  2. on that repository it exits 0 or 1, never 2: every local import resolves, or an exception says why not;
  3. every finding there has been reviewed as true, fixed, or excepted with a reason;
  4. a sample of unflagged files reviewed by hand shows no miss of a blocking rule;
  5. it runs in under 10 s there.

Keep suggest advisory. Ownership becomes blocking only through roots.

Measure it

bun install && bun test          # unit and end-to-end tests of the checker
reqfile eval                     # the labeled corpus, as published

Each violation names in findings every file a correct checker must flag (reqfile requires all of them). split: holdout marks the confirmation examples, written by independent authors and never used for tuning. reqfile eval runs the version named in Reqfile.yaml; to measure the working copy, bun run build and point the checks at node dist/main.js.

Release

Bump version in package.json and in the run: of Reqfile.yaml, then push a tag vX.Y.Z. CI tests, builds and publishes to npm through trusted publishing, without a stored token.

版本与发布记录

@g48in/reqfile-colocation 共发布 4 个版本,首次发布于 2026/09/28;过去 12 个月发布了 4 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.4.0latest2026/11/0214 天前
0.3.02026/10/061 个月前
0.1.32026/09/291 个月前
0.1.12026/09/281 个月前

关于 @g48in/reqfile-colocation 的常见问题

@g48in/reqfile-colocation 是什么?

reqfile checker for COLOCATION: each feature owns its code, exposes an interface, and can be deleted on its own

如何安装 @g48in/reqfile-colocation?

在项目目录运行 npm install @g48in/reqfile-colocation,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 reqfile-colocation,可以用 npx reqfile-colocation 直接运行。

@g48in/reqfile-colocation 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/g48in__reqfile-colocation 是否存在。

@g48in/reqfile-colocation 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@g48in/reqfile-colocation 可以商用吗?

@g48in/reqfile-colocation 使用 MIT OR Apache-2.0 许可。可在 MIT、Apache-2.0 中任选其一遵守。选择 MIT 时,宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@g48in/reqfile-colocation 的下载量有多少?

根据 npm 官方下载统计,@g48in/reqfile-colocation 在 2026/10/16 – 2026/11/14 这 30 天内被下载 7,752 次,日均约 258 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@g48in/reqfile-colocation 还在维护吗?

最新版本 0.4.0 发布于 2026/11/02(14 天前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/09/28。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。