roc-policy is a precompiled, read-only source-policy utility. It checks
repository conventions that compiler diagnostics, formatting and behavioral
tests do not establish. The tools release builds binaries for x86_64 Linux,
ARM64 Linux and Apple Silicon macOS. The Linux binaries are static musl
executables. Building and testing the implementation happens upstream; source
checking downstream requires no Roc compiler, Python, Git or Nix.
Risks and controls for source-policy evidence§
A rule can inspect the wrong files, an override can hide a violation, or an incomplete scan can appear successful. Make rule scope and analysis completeness observable. Explicit roots, validated profiles, explained effective configuration and stable findings let maintainers inspect what the checker selected. Malformed configuration and incomplete analysis fail separately from policy findings; fixtures verify exclusions and conflicting settings. A clean source-policy report establishes only the rules actually checked. It is not behavioral test coverage or proof that the product has no security defects.
Declare policy in Blueprint§
The default authority is repository_policy in Blueprint.roc. Schema 2 composes
shared defaults with repository rules and explicit overrides. The surrounding
file remains normal Roc configuration; add the binding beside its existing
config, without replacing its Blueprint platform declaration:
tooling = { compiler: "nightly-<date>-<commit>" }
repository_policy = {
schema_version: 2,
roots: ["package", "examples", "scripts", "docs"],
exclude: ["**/generated/**"],
test_roots: ["package"],
profiles: [{ name: "base", builtin: "roc-package", version: 1 }],
rules: [
{ id: "project/compiler", kind: CompilerPin, include: ["scripts/**"], value: tooling.compiler },
{ id: "project/no_internal_imports", kind: ForbiddenImport, include: ["package/core/**"], name: "support/Internal" },
{ id: "prose/worth_noting", kind: Phrase, include: ["docs/**"], channels: [Prose], phrase: "it is worth noting" },
],
overrides: [
{
rule: "base/spelling",
reason: "Published ecosystem name",
changes: { accepted_words: { add: ["rocpkgs"] } },
},
{
rule: "base/public_docs",
include: ["package/fixtures/**"],
reason: "Imported fixtures preserve upstream source",
changes: { level: Off },
},
{
rule: "prose/worth_noting",
include: ["docs/quotations/**"],
require_match: False,
reason: "Quoted source retains its original wording",
changes: { level: Warning },
},
],
}
An environment may reference the same tooling.compiler when declaring its
compiler. The utility resolves local constants, forward references, record puns
and field access without evaluating Blueprint. It does not prove that arbitrary
Blueprint expressions actually use those facts. Configuration composition is
implemented by the policy resolver, separate from generic constant extraction.
Supported data are literal records, lists, strings, booleans, plain decimal integers and bare tags, plus references, field access and parentheses. Strings support Roc escapes including Unicode scalars. Calls, arithmetic, functions, record updates, interpolation and imported values in the selected dependency graph are rejected. Unrelated functions and imports may remain in the file. The complete file must parse without diagnostics. Duplicate definitions/fields, cycles, unknown fields, irrelevant options and resource-limit violations fail.
The generic constant-data module has no profile loading or policy-specific behavior. It limits each source to 1 MiB, declarations/work to 10,000, depth to 64 and expanded-value accounting to 1 MiB. Policy composition admits at most 32 profiles, 10,000 final rule instances and 10,000 overrides.
Shared profiles and rule identities§
A rule’s kind selects an implementation. Its required id identifies one
configured policy instance. The diagnostic code identifies the finding category.
For example, two Phrase rules can share diagnostic code text/phrase while
having independently overridable IDs. IDs use slash-separated components of
letters, digits, dots, underscores and hyphens; empty, . and .. components
are invalid. Duplicate IDs fail instead of replacing earlier declarations.
Profiles load in declared order, followed by local rule declarations. Their rule
IDs are namespaced by the profile’s name: rule spelling in profile base
becomes base/spelling. A local rule cannot accidentally replace that ID. Change
its settings through an explicit override instead.
roc-policy list-profiles lists profiles embedded in the binary. The immutable
roc-package version 1 supplies public_docs for package/**/*.roc and
spelling for supported Roc/prose extensions. Both rules are intentionally
optional across repository layouts. Embedded profiles need no sidecar files;
the reviewed binary selects their bytes, and explanations include their SHA-256.
Unknown names and versions fail.
Alternatively, a profile descriptor may contain name, repository-relative
path, definition and the reviewed lowercase 64-character sha256. The file
must define a bounded record such as:
profile = {
schema_version: 2,
rules: [
{ id: "spelling", kind: Spelling, channels: [Comments, Prose] },
],
}
The loader verifies the exact file bytes before interpreting the chosen binding. A missing file, symlink, unsafe path or mismatched digest fails; no download or external hashing tool is invoked. Profiles contain rules only, without nested profile loading, discovery roots or overrides. Profile paths are anchored at the explicit repository root. This permits independent policy-data releases while keeping profile selection, pins and repository decisions in Blueprint.
Shared profiles are reusable defaults that repository authors may override. Mandatory organizational requirements require an independently trusted CI policy/invocation. Selecting a profile in a repository-controlled Blueprint is not evidence that such external requirements were enforced.
Ordered overrides§
Overrides target one exact rule ID. Every override requires a nonempty reason;
unknown IDs and unknown or irrelevant changes fail. Without include/exclude,
an override applies repository-wide. With file selectors, it applies to matching
paths. All matching overrides apply in written order, including global overrides:
later scalar values win, without hidden path-specificity precedence.
level is Off, Warning or Error, defaulting to Error. There is no separate
enabled flag. Off suppresses the selected rule. Warnings are reported without
failing by default; set repository max_warnings: 0 to reject any warning, or a
nonnegative number to allow that many. Configuration errors, I/O failures and
incomplete analysis remain exit 2 regardless of rule level.
Collection changes use explicit operations:
changes: {
accepted_words: { add: ["rocpkgs"], remove: ["oldterm"] },
channels: { replace: [Comments, Prose] },
corrections: { add: [{ word: "mispeling", replacement: "misspelling" }] },
}
replace discards inherited values and cannot be combined with add or remove.
add extends sets without duplicates; remove subtracts matching entries.
Vocabulary and correction keys fold ASCII case; paths remain case-sensitive.
Adding a correction replaces the inherited correction with the same normalized
key. Removing a custom correction exposes the bundled correction, if any; use
accepted_words to accept the word instead. Contradictory operations and duplicate
keys in one layer fail. There is no implicit list concatenation or deep merge.
Overrides may change level and the options supported by their rule kind.
Repository-wide overrides may also change rule include/exclude collections
and require_match. File-scoped overrides cannot change rule IDs, kinds, selectors or the
aggregate rule require_match assertion. Discovery roots and profile pins are not patchable through rules.
Rules require an applicable source file by default. An intentionally optional
rule may declare require_match: False, useful in profiles shared across
different layouts. Overrides likewise require a selected target/file by default;
set their require_match: False for intentional future or optional exceptions.
This catches renamed paths and stale exceptions without making every shared rule
mandatory. It checks applicability, not whether an exception suppressed a finding.
Commands and paths§
roc-policy check --root .
roc-policy check --root . --format json
roc-policy list-profiles
roc-policy explain-config --root .
roc-policy explain-config --root . --path package/Thing.roc
roc-policy manifest --root . > expected-tests.json
roc-policy run-tests --root . --compiler /absolute/path/to/roc
--config selects a file relative to the root and --definition selects a
binding; defaults are Blueprint.roc and repository_policy. All configured
commands use this same authority. manifest and explain-config emit JSON.
Explanation includes profile digests, rule IDs, origins, effective options,
selector decisions and ordered override traces. Without --path it shows global
settings; a path shows effective settings and why rules are selected, excluded
or disabled. It need not open the source file to explain its configuration.
roots and test_roots are exact repository-relative files/directories.
Schema 2 rule and override selectors use root-relative slash-separated globs:
* matches within one component, ? matches one Unicode scalar, and a complete
** component matches zero or more path components. Thus **/*.roc includes
root files and package/** includes the directory and descendants. Literal
patterns match exactly. Dotfiles match normally. Unsupported bracket/brace,
negation and escape syntax, absolute paths, controls and dot segments fail.
Patterns and matched paths are bounded to 4096 bytes and 128 components.
Global exclude prunes discovery before opening sources or descending into
excluded directories. Rule exclusions affect that rule alone. File overrides do
not resurrect globally excluded files. There is no implicit Git ignore behavior,
parent-configuration cascade or dependence on the current directory once the
explicit root is fixed. Global exclusions also apply to test discovery, but do
not bypass explicit configuration/profile reads or their integrity checks.
Discovery is deterministic and overlapping roots are deduplicated. Sources are
.roc, .md, .markdown, .adoc, .txt and .rst; accounting uses only .roc.
Selected sources reject symlinks and special files. Input files are limited to
8 MiB and inventory traversal to 100,000 paths.
Exit 0 means completed checks within the warning allowance, 1 means error-level
findings or too many warnings, and 2 means invalid input or incomplete analysis.
Diagnostic JSON uses schema 2 and includes rule_id, level, origin and trace
alongside the category rule. Text locations use one-based Unicode scalar
columns; JSON retains half-open UTF-8 byte spans.
Rules§
CompilerPin, NoCompilerPin and Dependency inspect app/package/platform
headers, not lookalike fields in bodies, comments or strings. A required
compiler/dependency rule aimed at an exact entrypoint reports a missing header.
Directory-wide required rules apply only to header-bearing entrypoints, allowing
ordinary helper modules. They fail configuration if no entrypoint is applicable.
A dependency value is an exact decoded string; alternate relative spellings
are distinct values.
ForbiddenImport matches the written path or its dot/slash descendants, ignoring
trivia and preserving aliases' original paths. It does not resolve bindings,
reexports, indirect calls or a complete project dependency graph.
PublicDocs identifies explicit module exposures and filename-based nominal
modules, including nested public associated definitions. It requires nonempty
attached ## documentation, supporting documentation before an annotation or
its definition. Private helpers and local declarations are excluded. Package
uppercase exposure inventories refer to other module files; select those source
files for checking. Unresolved imported/qualified/wildcard public surfaces return
documentation/incomplete, never an assertion that they are documented.
Spelling embeds the pinned typos v1.44.0 correction map (93,861 entries)
through a Roc file import. It requires no dictionary file, download or Rust
toolchain at runtime. This is correction lookup rather than unknown-word
detection; it does not reproduce the typos CLI heuristics or dialect rules.
The exact upstream CSV, MIT notice, provenance and offline verification script
are maintained in tools/roc-policy/vendor/typos. roc-policy --licenses
prints the embedded notice, which is also included among release assets.
channels selects Comments,
Identifiers, Strings and Prose; defaults are comments, identifiers and prose.
Identifiers split snake case, camel case, acronym transitions and digits.
accepted_words exempts word components and accepted_identifiers exempts whole
Roc identifiers, without accepting their components elsewhere. Add corrections
with corrections: [{ word: "mispeling", replacement: "misspelling" }]; entries
replace the same bundled key. Keys must be single words. Case folding is ASCII;
Unicode bytes and source offsets are preserved. String channels inspect raw
string parts rather than evaluating escape sequences.
Phrase requires phrase, optionally replacement, and uses the same channels
as spelling. Exceptions target the particular phrase rule ID. Whole-word matching folds ASCII case and normalizes whitespace
across consecutive comment/documentation lines; internal and surrounding
punctuation must match. Patterns must contain a matchable word. There are no
implicit banned phrases. A policy enforces chosen language conventions, not
inference about authorship or prose quality.
Test accounting§
manifest discovers static file-level and associated-block expect declarations
under test_roots, defaulting to roots. It emits schema 1 JSON with files,
each containing path, exact source, and tests with byte start and end.
It is a source inventory, with no execution statuses. Inline/local expectations
are valid assertions but are not separate required static tests. A static test
sharing its starting line with another expectation is rejected because the
compiler’s line output cannot distinguish them. Empty selected static inventories
fail explicitly.
run-tests is an optional discovery-driven runner requiring an explicit compiler
path. It invokes roc test <absolute-source-path> --verbose for each inventoried
file, leaving normal compiler caching enabled. Optional test_main names a
relative application file used to resolve package aliases with --main=....
For every static test, exactly one PASS: <absolute-source-path>:<line> record
must be reported. Tests in imported local modules and executed inline assertions
may add records; they do not inflate the selected file’s static count. The total
number of PASS records must also agree with the compiler’s success summary.
Missing or duplicated required locations fail, even when the overall count is
unchanged. A successful-looking output fragment cannot override a failed process.
Normal cached results are accepted. Cache correctness is the compiler’s
responsibility; this utility does not routinely pass --no-cache. Every child
must exit successfully within 60 seconds and uses bounded output capture.
Inventoried sources are checked again after all invocations; changed sources,
ambiguous locations, malformed output and incomplete parses fail. No shell
command is constructed.
This checks compiler-reported source lines, not native test IDs, exact span receipts, code coverage or authenticated execution provenance. It does not prove that a different CI entrypoint selects these tests. It discovers and invokes each selected test-bearing file itself, so large import graphs can cause repeated compiler invocations; normal caching reduces that cost. The adapter is tested against the compiler its tools release records and depends on that compiler’s human-readable verbose output. An incompatible output format fails explicitly.
The runner can execute project code and invoke the compiler’s normal dependency
behavior. Use it in unprivileged jobs. check and manifest require no compiler,
execute no project code, and invoke no external helpers.
Distribution boundary§
Install a reviewed tools release and verify its content digest before invoking
it. Blueprint can manage the binary through Programs and Fetch, with its
bytes pinned by Blueprint.lock; see
utility provisioning.
Content hashes establish expected bytes, not build provenance.