# Ignore rules and path semantics

Scribe applies the same repository path rules on every platform. Ignore rules
control discovery of untracked files; they never hide tracked changes.

## Canonical paths

A path is UTF-8, `/`-separated, relative, with no empty, `.`, or `..`
components, at most 8192 bytes, components at most 255 bytes. Rejected on
every platform at add and submit time: backslashes, control characters,
`< > : " | ? *`, reserved device names (`CON`, `PRN`, `AUX`, `NUL`, `COM1`-`COM9`,
`LPT1`-`LPT9`, `COM¹`/`COM²`/`COM³`, `LPT¹`/`LPT²`/`LPT³`, with or without extension), components ending in a dot or
space, and any `.scribe` component immediately followed by `_state` (both
compared under the same fold as collisions). A change set additionally refuses
a *file* named `.scribe`, since every working copy holds that name as a
directory. Symbolic links, junctions, and reparse points are never followed,
added, or synced through; they are reported.

`.scribe/` is Scribe's namespace inside the repository, not a reserved
directory. Only `.scribe/_state/` is local, machine-specific workspace state,
hard-reserved by Scribe itself rather than by an ignore rule: `scribe add
.scribe/_state/...` is refused as reserved local workspace state, and sync or
checkout may change anything else under `.scribe/` but never deletes or
overwrites `.scribe/_state/`. Everything else under `.scribe/` — at the root or
anywhere deeper, such as `x/.scribe/flows/build.yml` — is ordinary versioned
repository content. Secrets are never written anywhere under `.scribe/`, at any
level of the namespace.

Case and normalization collisions: the server keeps a shared snapshot index of
NFC-normalized paths using pinned Unicode 17.0.0 full case folding after NFD,
plus the NTFS dotless-i equivalence. `ß`, `ẞ`, and `ss` fold together, as do `ſ`
and `s` or `ς` and `σ`. Each branch pins its own root, and the server rejects a submission
that would create two paths mapping to the same key (`path_collision`),
including within one submission. Windows/NTFS and macOS/APFS would otherwise
map them to one file. Matching of ignore rules is case-sensitive on every
platform; lock rules, by contrast, match under this collision fold (see
[lock rules](/docs/repository-config.md#lock-rules)).

Long paths: Windows paths over 240 characters are opened with the `\\?\`
extended-length prefix. A 4,000-character path is tested on Linux and Windows
(`platform_behaviour.rs`); macOS caps most syscalls at 1,024 bytes and is not
tested for long paths.

Executable bits are stored in file records; applied on Unix, stored only on
Windows.

## Ignore files

- Syntax: Git-style via the `ignore` crate's matcher, with comments, blank
  lines, `\#` and `\!` escapes, `*`, `?`, character classes, `**`,
  trailing-slash directory patterns, leading-slash anchoring to the rule
  file's directory, negation, UTF-8, spaces, LF or CRLF, trailing-space
  escaping. Unclosed character classes are literal (as in Git). A malformed
  rule (for example an invalid range) is reported rather than treating the file
  as empty. Status/scan retain available findings and exit 1 when discovery is
  incomplete; do not interpret that partial report as a clean workspace.
- Sources and precedence, lowest to highest: `--global-ignore <file>`;
  workspace-local `.scribe/_state/ignore`; versioned `.scribeignore` files from the
  root to the deepest applicable directory. Within one file the last matching
  rule wins. Deeper versioned files override shallower ones.
- Excluded parents: an ignored directory is not traversed, so a negation in a
  deeper file cannot recover its contents. `scribe ignore explain` reports
  `via_parent` in that case.
- Ignore rules affect only untracked discovery and recursive `add`. Tracked
  files remain eligible for scanning; `scribe status --scan` reports their
  modifications and deletions regardless of rules. Rules never delete data and
  are not access control. Versioned ignore files are ordinary tracked files.
- Scribe does not read `.gitignore` or global Git configuration, and does not
  apply implicit hidden-file rules or game-engine ignore templates.
- `scribe add --force <path>` overrides ignore matching only; it never
  overrides containment, the `.scribe/_state/` reservation, links, or name
  validity.
- `scribe ignore explain <path>` prints ignored/not ignored, the deciding
  file, line, and pattern, and whether a parent directory decided.

`.scribeignore` and `.scribe/config.toml` have different authority. Ignore files govern
local untracked discovery. Only the accepted `locks` section of `.scribe/config.toml` on `main`
governs mandatory locks across all branches; nested or locally edited lock
settings do not override it. Profiles in that same config are branch-scoped;
they do not change ignore behavior. See [mandatory lock rules](/docs/client-usage.md#mandatory-lock-rules).

## Fingerprints

The baseline records size and modification time as reported by the filesystem,
together with the moment that observation was taken. Equal fingerprints skip
hashing in `status` only when the recorded modification time is older than the
observation by the filesystem's timestamp granularity, so a same-length save
inside that window cannot pass as unchanged; `status --verify` always hashes.
Fingerprints are caches, not proof of unchanged bytes. Resolution differs by
filesystem (NTFS 100 ns ticks; some filesystems 1 or 2 s), so only exact
equality is meaningful.

Default status inspects opened and workflow paths only; `--scan` adds unopened
tracked changes and untracked discovery, and `--verify` hashes contents. A
directory at an expected file path, or a file blocking an ancestor directory,
is `not_file`. Unsafe links and inspection failures are reported as per-path
problems, not mistaken for ordinary missing files. See
[status and scanning](/docs/client-usage.md#status-and-scanning).

## Concurrent external writers

Submit re-stats a file after hashing it and re-hashes every chunk range while
uploading, so a file that changes during a submit is refused. A before/after
stat is not an atomic snapshot: a writer that modifies a file between the
final stat and the upload read is caught by the chunk digest check; a writer
that modifies and restores bytes within one fingerprint tick is not. Editors
should be quiescent during submit.
