# Contract 1 — ratification Open Brainy's answer to the API contract published by the accelerated engine (`docs/api-contract.md` + `docs/api-contract.json`, contract version 1). Each item is answered with the code line that proves it, and each promise is stated as a promise rather than a description. *Internal engineering document — no frontmatter, not published.* --- ## 1. Contract version — DECLARED `package.json` carries `"brainyContract": 1`, and the engine states its own: ```ts export const BRAINY_CONTRACT_VERSION = 1 as const export function contractVersion(): number { return BRAINY_CONTRACT_VERSION } ``` `src/utils/version.ts`, re-exported from `src/index.ts`. Two engines can now compare an integer instead of probing prototypes, and a tool can read the package field without importing the engine. Pinned in `tests/integration/filter-operator-conformance.test.ts` ("declares its contract version in code and in package.json") — the code value and the package field can never drift apart silently. --- ## 2. The REQUIRED / OPTIONAL split — RATIFIED, WITH A COMMITMENT **Ratified: 41 required doors of 57.** The promise, stated plainly: > **A REQUIRED door is never removed, never narrowed, and never made optional > without a MAJOR contract bump.** "Narrowed" includes: refusing an input it > used to accept, returning less than it used to return, and changing an > ordering, a cursor encoding, or a refusal's typed code. An OPTIONAL door may > be added in a minor; an optional door **promoted to required** is a major, > because a consumer that relied on feature-detecting it now has a hard > dependency. Two clarifications this engine attaches, so the promise means the same thing on both sides: 1. **A refusal is part of the door.** Contract 1 includes not only that `find({ where })` answers, but that it REFUSES by name for the operators listed as refused. Turning a refusal into a silent empty answer is a narrowing, not a relaxation — the same class of change as removing the door. 2. **Deprecation is not removal.** This engine may mark a required door deprecated in a minor (documented, warned) as long as it keeps working. Only its removal is a major. --- ## 3. `is` / `isNot` / `greaterEqual` / `lessEqual` — A FINDING AGAINST THE SPEC **The specification is wrong about these four, and this engine has never served them.** `docs/filter-operator-conformance.md` (in the accelerated engine's repository — not editable from here) lists them as served aliases. The accepted set is defined in one place: ```ts // src/utils/metadataFilter.ts const VALUE_OPERATORS = new Set([ 'equals', 'eq', 'notEquals', 'ne', 'greaterThan', 'gt', 'greaterThanOrEqual', 'gte', 'lessThan', 'lt', 'lessThanOrEqual', 'lte', 'between', 'oneOf', 'in', 'noneOf', 'contains', 'excludes', 'hasAll', 'length', 'exists', 'missing', 'matches', 'startsWith', 'endsWith' ]) ``` 25 tokens. None of the four appears; `validateWhereFilter()` raises `BrainyError('INVALID_QUERY')` naming the bad operator and listing the valid set, before any index read. A consumer following the spec would have written a filter this engine rejects outright. **Action taken here, since the prose lives in the other repository:** the truth is made machine-checkable rather than re-asserted in another document. The accepted set is asserted token-for-token in `tests/integration/filter-operator-conformance.test.ts`, read out of the engine's own refusal message, and the same set is emitted into `docs/api-contract.json` (item 8). Diff the manifests; the prose can then be corrected from a fact. --- ## 4. The serving-withholding invariant list — CONFIRMED IDENTICAL `index-initialized · durable-state-present · manifest-residency · replay-clean · strand-latch`. Confirmed as this engine's list, and confirmed EXHAUSTIVE for contract 1: these are the only invariants whose failure may withhold serving. Everything else a health report can fail is a `warn` — it names damage without closing a door. The mechanism on this side: `assessProviderHealth()` (`src/utils/indexReadiness.ts`) treats the provider's own `serving` verdict as authoritative and verbatim; an UNLEDGERED family never flips a serving provider to not-ready and never flips a not-serving provider to ready. The read gate refuses PER FAMILY — a metadata read is never refused by an unserving vector leg (`src/brainy.ts`, `ensureFamiliesServing`). **One addition this engine is making, declared here because it changes what a refusal MEANS:** a provider may now report `rebuildInProgress()` — it is rebuilding ITSELF, online, and its doors refuse by name with progress until it is whole. This does not add a withholding invariant (the provider's own `serving: false` is still what withholds); it adds a REASON attached to that withholding, so a caller can tell "temporarily closed, opens by itself" from "broken, needs repairIndex()". Additive, hence a minor. --- ## 5. The compatibility rule — ADOPTED **Minor = additive. Major = breaking.** Adopted verbatim, with the announcement duty attached: > **Every public-surface addition is announced.** The accelerated engine's > package re-exports this engine's surface by enumeration, so it goes red on > any new export BY DESIGN — that redness is the announcement mechanism > working, not a build break to route around. The mechanics that make this checkable rather than remembered: `scripts/emit-contract-manifest.mjs --check` fails when the committed `docs/api-contract.json` no longer matches the built surface. A new export is therefore a red check with a message naming what to do: re-emit and announce. --- ## 6. The 30 storage seam methods — SUPPORTED SURFACE, COMMITTED **Committed: every method in `docs/api-contract.md` §15 is supported surface until Stage 2, and none is removed without a contract major.** They are the seam the accelerated engine's storage adapter implements and the seam its reader replaces piece by piece; removing one mid-programme would break a working pair for no gain. Two qualifications, both stated so neither side is surprised: 1. **Supported ≠ frozen in behaviour.** A seam method may become FASTER, may narrate more, and may start refusing an input that was previously an undefined-behaviour footgun — the last of those is announced as a divergence here before it ships, not discovered by the other engine. 2. **`counts.json`'s ledger is the one seam value that is not an enumeration.** See `docs/canonical-layout-ratification.md` §8: only the all-tier pair carrying `allCountsDerivedBy: 'identity-record'` with `allCountsSuspect: false` may be subtracted against. That rule is part of this commitment. --- ## 7. `hasAll` / `noneOf` / `excludes` — SERVED, NOT RATIFIED AS A DIVERGENCE The accelerated engine was right that it was the correct side, and the divergence is now closed in the right direction: **this engine serves all three on the index path.** The defect underneath was worse than a divergence. The metadata index's operator switch (`src/utils/metadataIndex.ts`) had **no default case**, so any operator without a `case` left the field's match set at its initial `[]` and `find()` returned an empty page. `hasAll`, `noneOf` and `excludes` are documented, accepted by the validator, and implemented in the in-memory matcher — and they answered silently wrong through an index-backed find. - **`hasAll: [a, b]`** — the intersection of each element's posting set. An empty operand array is vacuously true of every row that HAS the field. - **`noneOf: [a, b]`** — the complement of the union of their posting sets. - **`excludes: v`** — the complement of `contains`. **And the other four are now REFUSED BY NAME rather than answered empty.** `startsWith`, `endsWith`, `matches` and `length` cannot be evaluated by an equality/range posting index without reading every row, which is the cost this path exists to avoid. They raise `BrainyError('INVALID_QUERY')` naming the operator, the field, and the reason. This matches the accelerated engine's behaviour for the same four tokens, so the two engines now AGREE on all 25: | class | tokens | |---|---| | served on the index path | between, contains, eq, equals, excludes, exists, greaterThan, greaterThanOrEqual, gt, gte, hasAll, in, lessThan, lessThanOrEqual, lt, lte, missing, ne, noneOf, notEquals, oneOf | | refused by name | endsWith, length, matches, startsWith | Pinned in `tests/integration/filter-operator-conformance.test.ts`: the exact 25-token accepted set, the three now served with their real answers (including an honest zero), and each of the four refusing by name. **This is a behaviour change for any consumer today calling the four refused operators through `find({ where })`.** They received an empty page; they now receive a typed refusal. Converting a wrong answer into a loud refusal is this engine's own law, and the previous behaviour was not a contract anyone could have relied on deliberately — but it is a change, and it is named here rather than discovered. **`knownDivergences` after this change:** the entry `served-beyond-baseline` is RESOLVED (both engines serve all three). The entry `refused-operators-answer-differently` is RESOLVED (both engines refuse the same four by name). Contract 1 has no remaining operator divergence. --- ## 8. This engine's own manifest — EMITTED `docs/api-contract.json`, generated by `scripts/emit-contract-manifest.mjs` from the BUILT surface: the prototype's own methods and accessors, the exported error classes, the operator sets read out of their single definitions, the field-addressing vocabulary read out of `src/db/fieldAddressing.ts`, and the health verdicts. Nothing in it is hand-maintained, so a diff between the two manifests is a diff between two engines rather than between two authors. `node scripts/emit-contract-manifest.mjs --check` fails when the committed manifest is stale — the announcement duty of item 5, made mechanical. **What the manifest deliberately does NOT carry: requirement marking.** Whether a door is required is a commitment, not a property of the surface; it is item 2 of this document. The diff the two sides want — "does Open Brainy still expose every door contract 1 requires?" — is a set-membership check between their `doors[].name` where `requirement === 'required'` and this manifest's `doors[].name`. --- ## A defect this work surfaced but did not fix `src/vfs/VirtualFileSystem.ts` builds a path-prefix filter as `path: { $startsWith: options.path }` — with a `$` prefix. No operator in this engine carries a `$`, so `validateWhereFilter()` rejects it with `INVALID_QUERY` before any index read: **`vfs.searchFiles({ path })` throws today, on every call that passes a path.** It is pre-existing and unrelated to the operator work above (the validator refuses it before the index path is reached), and it is left as a filing rather than fixed here, because the right answer is a design question — a path-prefix search cannot be served by an equality/range index, so it needs either a path-segment index or an explicit in-memory narrow, not a spelling correction. --- ## Summary of what changed in code for this ratification | item | change | |---|---| | 1 | `"brainyContract": 1` in package.json; `contractVersion()` / `BRAINY_CONTRACT_VERSION` exported | | 3 | the accepted 25-token set asserted from the engine's own refusal message, and emitted into the manifest | | 7 | `hasAll` / `noneOf` / `excludes` served on the index path; `startsWith` / `endsWith` / `matches` / `length` refused by name instead of answered empty | | 8 | `scripts/emit-contract-manifest.mjs` + the generated `docs/api-contract.json`, with a `--check` mode |