diff --git a/.forgejo/workflows/delta-gate.yml b/.forgejo/workflows/delta-gate.yml deleted file mode 100644 index c320594e..00000000 --- a/.forgejo/workflows/delta-gate.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: Delta Gate - -# On-demand candidate-vs-control gate on the capped functional CI lane -# (label: gate-functional). That lane is Bun-only host-mode — there is no -# Node.js runtime available to it, so this workflow deliberately avoids every -# JS-based action (checkout/setup-node/setup-bun/upload-artifact all require -# one) and does everything with plain git + bun in shell steps instead. -# -# Verdict lines a caller should grep for in the run log: -# COLLECTED patch= control= — collection-truncation guard inputs -# NEW-RED-COUNT: — failures on candidate absent from control -# DELTA-GATE: CLEAN | NEW REDS | INVALID | STOPPED-BY-REGISTRY-TRIPWIRE -# -# The lane's own housekeeping stops the runner and drops a marker file when -# host pressure (I/O, registry latency, disk budget) trips — never ours to -# interpret as a red or a green. The final step checks for that marker before -# it says anything about pass/fail. - -on: - workflow_dispatch: - inputs: - candidate: - description: 'Candidate ref (branch or sha) to gate' - required: true - type: string - control: - description: 'Control sha to diff against' - required: true - type: string - # workflow_dispatch needs Actions-unit write on the dispatching credential; - # push does not (it runs from the pushed ref's own tree), so a plain push - # to a release or CI branch is the fallback trigger while that grant is - # outstanding — see the ref-resolution step below for what it gates against. - push: - branches: ['rel/**', 'ci/**'] - -concurrency: - group: delta-gate - cancel-in-progress: false - -jobs: - delta-gate: - name: Delta gate — candidate vs control - runs-on: gate-functional - timeout-minutes: 120 - steps: - - name: Resolve candidate/control refs - id: refs - run: | - candidate="${{ github.event.inputs.candidate }}" - control="${{ github.event.inputs.control }}" - # workflow_dispatch supplies both explicitly; a push event carries - # neither — fall back to the pushed commit as candidate and the - # last released, known-good tip (10.4.9) as control, so a plain - # push still produces a meaningful gate instead of an empty ref. - if [ -z "$candidate" ]; then candidate="${{ github.sha }}"; fi - if [ -z "$control" ]; then control="eec90bdd"; fi - echo "candidate=$candidate" >> "$GITHUB_OUTPUT" - echo "control=$control" >> "$GITHUB_OUTPUT" - echo "Resolved (trigger=${{ github.event_name }}): candidate=$candidate control=$control" - - - name: Clean any residue from a prior run - run: rm -rf "ob-cand-${{ github.run_id }}" "ob-ctrl-${{ github.run_id }}" "/tmp/ob-${{ github.run_id }}-"* - - - name: Clone + test — candidate - id: patch - run: | - set -o pipefail - git clone --quiet "https://source.soulcraft.com/soulcraftlabs/open-brainy.git" "ob-cand-${{ github.run_id }}" - cd "ob-cand-${{ github.run_id }}" - git checkout --quiet "${{ steps.refs.outputs.candidate }}" - git log --oneline -1 - bun install - rc=0 - bun x vitest run > "/tmp/ob-${{ github.run_id }}-patch.log" 2>&1 || rc=$? - echo "PATCH-RC:$rc" - grep -aE "Tests .*(passed|failed)" "/tmp/ob-${{ github.run_id }}-patch.log" | tail -1 - grep -aE "^ FAIL |^\s+×" "/tmp/ob-${{ github.run_id }}-patch.log" | sed -E "s/ [0-9]+ms$//" | sed -E "s/^\s+//" | sort -u > "/tmp/ob-${{ github.run_id }}-patch.fail" - echo "PATCH-FAILING:$(wc -l < "/tmp/ob-${{ github.run_id }}-patch.fail")" - - - name: Clone + test — control - id: control - run: | - set -o pipefail - git clone --quiet "https://source.soulcraft.com/soulcraftlabs/open-brainy.git" "ob-ctrl-${{ github.run_id }}" - cd "ob-ctrl-${{ github.run_id }}" - git checkout --quiet "${{ steps.refs.outputs.control }}" - git log --oneline -1 - bun install - rc=0 - bun x vitest run > "/tmp/ob-${{ github.run_id }}-control.log" 2>&1 || rc=$? - echo "CONTROL-RC:$rc" - grep -aE "Tests .*(passed|failed)" "/tmp/ob-${{ github.run_id }}-control.log" | tail -1 - grep -aE "^ FAIL |^\s+×" "/tmp/ob-${{ github.run_id }}-control.log" | sed -E "s/ [0-9]+ms$//" | sed -E "s/^\s+//" | sort -u > "/tmp/ob-${{ github.run_id }}-control.fail" - echo "CONTROL-FAILING:$(wc -l < "/tmp/ob-${{ github.run_id }}-control.fail")" - - - name: Delta gate verdict - if: always() - run: | - set -o pipefail - - # The lane's own tripwire wins over anything we would otherwise say: - # a bare failure/timeout above with this marker present is host - # pressure, never a real red and never a real green. - if [ -f /srv/gate-lane/TRIPWIRE-STOPPED ]; then - echo "DELTA-GATE: STOPPED-BY-REGISTRY-TRIPWIRE" - head -1 /srv/gate-lane/TRIPWIRE-STOPPED - exit 3 - fi - - patch_log="/tmp/ob-${{ github.run_id }}-patch.log" - control_log="/tmp/ob-${{ github.run_id }}-control.log" - patch_fail="/tmp/ob-${{ github.run_id }}-patch.fail" - control_fail="/tmp/ob-${{ github.run_id }}-control.fail" - - if [ ! -s "$patch_log" ] || [ ! -s "$control_log" ]; then - echo "DELTA-GATE: INVALID — a leg produced no log (see the two steps above for the real cause)" - exit 2 - fi - - pt=$(grep -aoE "\(([0-9]+)\)$" "$patch_log" | tail -1 | tr -d "()") - ct=$(grep -aoE "\(([0-9]+)\)$" "$control_log" | tail -1 | tr -d "()") - echo "COLLECTED patch=${pt:-0} control=${ct:-0}" - if [ "${pt:-0}" -lt 3000 ] || [ "${ct:-0}" -lt 3000 ]; then - echo "DELTA-GATE: INVALID — truncated collection" - exit 2 - fi - - echo "=== NEW REDS ===" - comm -23 "$patch_fail" "$control_fail" - new=$(comm -23 "$patch_fail" "$control_fail" | wc -l) - echo "NEW-RED-COUNT:$new" - - echo "=== full candidate fail list ===" - cat "$patch_fail" - echo "=== full control fail list ===" - cat "$control_fail" - - if [ "$new" -eq 0 ]; then - echo "DELTA-GATE: CLEAN" - else - echo "DELTA-GATE: NEW REDS" - exit 1 - fi - - - name: Clean up (mind the lane's disk budget) - if: always() - run: rm -rf "ob-cand-${{ github.run_id }}" "ob-ctrl-${{ github.run_id }}" "/tmp/ob-${{ github.run_id }}-"* diff --git a/src/brainy.ts b/src/brainy.ts index ffc2d5cd..64e8707e 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -1040,17 +1040,6 @@ export class Brainy implements BrainyInterface { } } - /** - * Factory hook for the generation store, so an engine built on top of this - * reference implementation can substitute a `GenerationStore` that keeps - * the same behavioural contract (for example, one backed by a native - * implementation) — overriding it never changes this engine's own - * behaviour, since the default implementation is unchanged. - */ - protected createGenerationStore(storage: BaseStorage): GenerationStore { - return new GenerationStore(storage) - } - /** * Initialize Brainy. * @@ -1308,7 +1297,7 @@ export class Brainy implements BrainyInterface { // guarantees indexes never observe rolled-back state. Reader-mode // instances skip recovery (readers never write; the next writer // repairs). - this.generationStore = this.createGenerationStore(this.storage) + this.generationStore = new GenerationStore(this.storage) const generationOpenResult = await step( 'generation-store.open', 'reading the generation manifest and committed ranges, opening the fact log and the ' + diff --git a/src/plugin.ts b/src/plugin.ts index 64abfe26..027a5367 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -460,10 +460,8 @@ export interface MetadataIndexProvider { * @param params - The find params, already normalized by `find()` * (natural-language parsed, `connected` anchors resolved to canonical ids, * an empty `where` dropped). - * @param hiddenIds - Ids this read must not return. The contract is the ANSWER, not the - * mechanism: a provider may subtract this set before paging, or derive the - * same exclusion from the params' visibility tiers itself — either way the - * page must equal the engine's own answer with none of these ids in it. + * @param hiddenIds - Ids this read must not return; apply BEFORE paging so + * `limit` stays exact. * @param graphIndex - The active graph provider, for a `connected` plan. * @returns The page's ids plus the stage that emptied it, or `null`. */ diff --git a/tests/integration/generation-store-factory.test.ts b/tests/integration/generation-store-factory.test.ts deleted file mode 100644 index 08b62619..00000000 --- a/tests/integration/generation-store-factory.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @module tests/integration/generation-store-factory - * @description Pins the `createGenerationStore` protected factory hook on - * `Brainy` ({@link Brainy.createGenerationStore}). The hook exists so an - * engine built on top of this reference implementation can substitute a - * `GenerationStore` that keeps the same behavioural contract; this suite - * proves two things: - * - * 1. A subclass overriding the hook is the ONLY path that constructs the - * generation store — it is called exactly once, with the same storage - * instance `performInit` holds — and the store the brain actually uses - * is the one the override returned. - * 2. The default (non-overridden) path is unaffected — proven here by - * confirming the base class still produces a plain `GenerationStore` - * wired to `brain.storage`, and separately by running the existing - * `db-mvcc` and `brainy-core.integration` suites unmodified against this - * change (they exercise generation-store behaviour end to end). - */ - -import { describe, it, expect, afterEach } from 'vitest' -import { Brainy } from '../../src/brainy.js' -import { GenerationStore } from '../../src/db/generationStore.js' -import type { BaseStorage } from '../../src/storage/baseStorage.js' - -/** Typed access to the brain's private storage + generation-store fields (test injection point). */ -function internalsOf(brain: Brainy): { storage: BaseStorage; generationStore: GenerationStore } { - return brain as unknown as { storage: BaseStorage; generationStore: GenerationStore } -} - -/** - * A `GenerationStore` subclass that counts its own construction and - * remembers the storage instance it was built with, so the test can prove - * the hook is the sole construction path without mocking the module. - */ -class SpyGenerationStore extends GenerationStore { - static constructCount = 0 - static lastStorage: BaseStorage | undefined - - constructor(storage: BaseStorage) { - super(storage) - SpyGenerationStore.constructCount++ - SpyGenerationStore.lastStorage = storage - } -} - -/** A Brainy subclass overriding the factory hook — stands in for an engine built on the reference. */ -class BrainyWithSpyStore extends Brainy { - hookCallCount = 0 - hookStorageArg: BaseStorage | undefined - - protected override createGenerationStore(storage: BaseStorage): GenerationStore { - this.hookCallCount++ - this.hookStorageArg = storage - return new SpyGenerationStore(storage) - } -} - -describe('Brainy.createGenerationStore — protected factory hook', () => { - const brains: Brainy[] = [] - - afterEach(async () => { - SpyGenerationStore.constructCount = 0 - SpyGenerationStore.lastStorage = undefined - for (const brain of brains.splice(0)) { - try { - await brain.close() - } catch { - // already closed by the test - } - } - }) - - it('a subclass override is the sole construction path: called once, same storage instance, its store is the one the brain uses', async () => { - const brain = new BrainyWithSpyStore({ requireSubtype: false, storage: { type: 'memory' } }) - await brain.init() - brains.push(brain) - - // Called exactly once, through the hook. - expect(brain.hookCallCount).toBe(1) - expect(SpyGenerationStore.constructCount).toBe(1) - - // Same storage instance the base class holds — not a copy, not a different adapter. - const { storage, generationStore } = internalsOf(brain) - expect(brain.hookStorageArg).toBe(storage) - expect(SpyGenerationStore.lastStorage).toBe(storage) - - // The store the brain actually uses is the one the override returned. - expect(generationStore).toBeInstanceOf(SpyGenerationStore) - }) - - it('the default (non-overridden) path still produces a plain GenerationStore wired to the same storage', async () => { - const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } }) - await brain.init() - brains.push(brain) - - const { storage, generationStore } = internalsOf(brain) - expect(generationStore).toBeInstanceOf(GenerationStore) - // The default implementation constructs from the same storage the brain holds. - expect((generationStore as unknown as { storage: BaseStorage }).storage).toBe(storage) - }) -})