feat(contract): declare contract 1, serve three operators, refuse four by name
Open Brainy's side of the API contract the accelerated engine published.
DECLARED: package.json carries "brainyContract": 1 and the engine states its
own via contractVersion() / BRAINY_CONTRACT_VERSION — two engines compare an
integer instead of probing prototypes, and a tool reads the package field
without importing the engine. Pinned so the two can never drift apart.
SERVED: hasAll, noneOf and excludes now work on the index path. The defect
underneath was worse than the reported divergence — the metadata index's
operator switch 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.
Documented, validator-accepted, matcher-implemented operators answering
silently wrong. hasAll intersects each element's posting set (an empty operand
is vacuously true of every row that has the field), noneOf complements their
union, excludes complements contains.
REFUSED BY NAME: startsWith, endsWith, matches and length raise
INVALID_QUERY naming the operator, the field and the reason. An equality/range
posting index cannot evaluate a substring, a pattern or an array length without
reading every row — which is the cost this path exists to avoid — so it refuses
rather than answering an empty page. Both engines now agree on all 25 tokens
and contract 1 has no remaining operator divergence. This is a visible change
for a consumer calling those four through find({ where }): an empty page
becomes a typed refusal.
EMITTED: scripts/emit-contract-manifest.mjs generates docs/api-contract.json
from the BUILT surface — prototype doors, exported error classes, the operator
sets read out of their single definitions, the field-addressing vocabulary, the
health verdicts. Nothing hand-maintained, so a diff between two manifests is a
diff between two engines. `--check` fails on a stale manifest, which makes the
announce-every-addition duty mechanical rather than remembered.
RATIFIED in docs/contract-1-ratification.md: the 41-of-57 required split with
the promise spelled out (a refusal is part of a door; deprecation is not
removal), the serving-withholding list confirmed exhaustive and identical, the
minor/major rule adopted with the announcement duty, the 30 storage seam
methods committed as supported surface until Stage 2, and a finding filed
against the spec — is / isNot / greaterEqual / lessEqual are listed there as
served aliases and have never existed in this engine, which throws
INVALID_QUERY on all four.
This commit is contained in:
parent
29a2e8c9e7
commit
f758d7dc42
10 changed files with 2172 additions and 3 deletions
1545
docs/api-contract.json
Normal file
1545
docs/api-contract.json
Normal file
File diff suppressed because it is too large
Load diff
229
docs/contract-1-ratification.md
Normal file
229
docs/contract-1-ratification.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# 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<string>([
|
||||
'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`.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
Loading…
Add table
Add a link
Reference in a new issue