Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/adr/0028-metadata-naming-and-namespace-isolation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0028: Metadata Naming & Namespace Isolation — Derived Physical Names, Namespace-Scoped Identity, and a Single Kernel Contract

**Status**: Proposed (2026-06-01) — NOT STARTED (2026-07-16 audit): the target model (namespace as identity dimension, short authored names, derived physical names, `namingMode` dual-read, `sys` reservation, namespaced transport segments) is entirely unbuilt; what exists is the superseded literal-prefix current-state contract (`spec/kernel/namespace-prefix.ts`, self-labelled) plus unrelated ADR-0048 conflict scaffolding.
**Status**: Deferred (2026-07-16; proposed 2026-06-01) — the target model (namespace as identity dimension, short authored names, derived physical names, `namingMode` dual-read, `sys` reservation, namespaced transport segments) is entirely unbuilt, and adopting it now would be a platform-wide migration (physical table derivation, transport routes, dual-read compatibility) with no present pain to justify it: the literal-prefix current-state contract (`spec/kernel/namespace-prefix.ts`) is enforced, collision-proof (ADR-0028/0048), and working. **Re-open trigger**: real multi-package ecosystem naming pain — e.g. a marketplace package whose natural short names are unusable under literal prefixing, or cross-environment portability requiring namespace remapping. Until then this stays a recorded design, not planned work.
**Deciders**: ObjectStack Protocol Architects
**Supersedes**: the hand-written object-namespace-prefix authoring rule documented in `packages/spec/src/kernel/manifest.zod.ts` (the `namespace` field) and enforced by `validateNamespacePrefix()` in `packages/spec/src/stack.zod.ts` — there is no standalone ADR for that rule today.
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0008](./0008-metadata-repository-and-change-log.md) (Repository · ChangeLog · Cache · Registry; `MetaRef = org/type/name`), [ADR-0010](./0010-metadata-protection-model.md) (protection model), [ADR-0019](./0019-app-as-consumer-unit.md) (app as the consumer-installable unit), [ADR-0025](./0025-plugin-package-distribution.md) (package distribution), [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) (**prerequisite** — kernel object ownership; D5/D6 below assume the kernel is properly owned per ADR-0029)
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0061-record-search-architecture.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0061: Record Search Architecture — metadata-driven `$search` resolution

**Status**: Proposed — recommended for acceptance (2026-06-21). **Tier 1 (`$search` + `object.searchableFields`) has since been implemented & is live; Tier 2 (FTS/relevance) deferred.** Verified 2026-07-16: Tier 1 claim TRUE (`objectql/src/search-filter.ts` + engine wiring + unit tests, incl. `$searchFields` narrowing and select label-mapping) — but this ADR's own acceptance bar (a `search-conformance` ledger entry + dogfood multi-field HTTP proof, per ADR-0060 closure) is still unmet, so the header stays Proposed until that lands.
**Status**: Accepted (2026-06-21; Tier 1) — implemented and conformance-closed per this ADR's own bar: server-resolved `$search` executor (`objectql/src/search-filter.ts` + engine wiring), `object.searchableFields` + `$searchFields` narrowing, select label→value mapping; **`search-conformance` ledger** (`dogfood/test/search-conformance.ledger.ts`, ADR-0060 `checkLedger`, proof-required) + **dogfood multi-field HTTP proof** (`showcase-search.dogfood.test.ts`: "retail"→Northwind via `industry`, label mapping, narrowing, terms-AND). Tier 2 (FTS / relevance / external engines) remains deferred — add its ledger row with a proof when a driver ships `fullTextSearch: true`.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: ADR-0045 (additive materialization & visibility gate — search must be visibility-aware), ADR-0049 (enforce-or-remove / no unenforced declaration), ADR-0054 (runtime proof per enforced surface), ADR-0060 (conformance ledger as a platform pattern)
**Consumers**: spec, objectql, driver-sql, driver-memory, driver-mongodb, rest, objectui (`fields`, `plugin-list`, `app-shell`), verify, dogfood
Expand Down
53 changes: 53 additions & 0 deletions packages/dogfood/test/search-conformance.ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import type { ConformanceRow } from '@objectstack/verify';
//
// ADR-0061 §Conformance — Record-Search Conformance ledger (ADR-0060 pattern).
//
// The ADR's own landing bar: `$search` is "landed" iff a driver-reachable
// executor runs it AND a dogfood proof asserts a MULTI-FIELD match over the
// real HTTP API (e.g. searching "retail" returns an account matched by
// `industry`, not `name`). This ledger is the durable encoding of that bar:
// every search-related declarable surface sits in exactly one honest state
// (ADR-0049), names its executor site, and the enforced rows reference the
// HTTP-level proof (`showcase-search.dogfood.test.ts`).
//
// Tier 2 (FTS / relevance / external engines) is deliberately ABSENT from this
// ledger rather than carried as an aspirational row — per ADR-0049 a surface
// that does not exist in the spec cannot be "declared but unenforced". When a
// driver grows `fullTextSearch: true`, add its row here with a proof.

export const SEARCH_SURFACE: ConformanceRow[] = [
{
id: 'search-executor',
summary: '`$search` server-resolved cross-field executor (terms AND-ed, fields OR-ed, case-insensitive `$contains`)',
surface: 'spec/api/query.zod.ts:$search (QueryParams `search`)',
state: 'enforced',
enforcement: 'objectql/src/engine.ts (find AST expansion) → objectql/src/search-filter.ts expandSearchToFilter',
proof: 'showcase-search.dogfood.test.ts',
},
{
id: 'searchable-fields-object',
summary: '`object.searchableFields` — canonical allowed-search-field set; auto-default (name/title + short text, secret/PII/json excluded) when unset',
surface: 'spec/data/object.zod.ts:searchableFields',
state: 'enforced',
enforcement: 'objectql/src/search-filter.ts resolveSearchFields / autoDefaultFields',
proof: 'showcase-search.dogfood.test.ts',
},
{
id: 'search-fields-override',
summary: '`$searchFields` per-query narrowing — validated against the allowed set, can never widen it',
surface: 'spec/api/query.zod.ts:$searchFields',
state: 'enforced',
enforcement: 'objectql/src/search-filter.ts resolveSearchFields (intersection)',
proof: 'showcase-search.dogfood.test.ts',
},
{
id: 'search-select-label-mapping',
summary: 'select/status label→value mapping — a human label in the query matches option values',
surface: 'spec/data/field.zod.ts:options (select) × $search',
state: 'enforced',
enforcement: 'objectql/src/search-filter.ts optionValuesMatching / fieldClausesForTerm',
proof: 'showcase-search.dogfood.test.ts',
},
];
32 changes: 32 additions & 0 deletions packages/dogfood/test/search-conformance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// ADR-0061 §Conformance — the Record-Search Conformance ledger is a CHECKED
// artifact (ADR-0060 `checkLedger`): shared invariants + every enforced row
// must carry an existing proof file. The proof itself
// (`showcase-search.dogfood.test.ts`) asserts the ADR's landing bar over the
// real HTTP API; this test asserts the ledger cannot silently rot (a renamed
// or deleted proof file fails here, closing the declared-but-unenforced loop).

import { describe, expect, it } from 'vitest';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { checkLedger } from '@objectstack/verify';
import { SEARCH_SURFACE } from './search-conformance.ledger.js';

const HERE = dirname(fileURLToPath(import.meta.url));

describe('ADR-0061 record-search conformance ledger', () => {
it('is sound and every enforced row carries an existing proof', () => {
const problems = checkLedger(SEARCH_SURFACE, {
proofRoot: HERE,
proofRequiredForEnforced: true,
});
expect(problems).toEqual([]);
});

it('covers the executor surface exactly once', () => {
const ids = SEARCH_SURFACE.map((r) => r.id);
expect(new Set(ids).size).toBe(ids.length);
expect(ids).toContain('search-executor');
});
});
67 changes: 67 additions & 0 deletions packages/dogfood/test/showcase-search.dogfood.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// ADR-0061 §Conformance — the dogfood proof behind the search-conformance
// ledger: a MULTI-FIELD `$search` match over the REAL HTTP API. The ADR's own
// example, verbatim: searching "retail" must return an account matched by
// `industry`, not `name` (the showcase seed's Northwind row carries
// `industry: 'retail'` and no account name contains "retail"). Also proves
// the `$searchFields` narrowing (can only narrow, never widen) so the
// executor's security posture is pinned at the HTTP level, not just in
// `search-filter.test.ts` unit tests.

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import showcaseStack from '@objectstack/example-showcase';
import { bootStack, type VerifyStack } from '@objectstack/verify';

describe('showcase: $search over the HTTP API (ADR-0061 conformance proof)', () => {
let stack: VerifyStack;
let token: string;

const query = async (body: Record<string, unknown>) => {
const res = await stack.apiAs(token, 'POST', '/data/showcase_account/query', body);
expect(res.status).toBe(200);
const data = await res.json();
return (data?.data?.records ?? data?.records ?? []) as Array<Record<string, unknown>>;
};

beforeAll(async () => {
stack = await bootStack(showcaseStack);
token = await stack.signIn();
}, 60_000);
afterAll(async () => { await stack?.stop(); });

it('multi-field match: "retail" returns Northwind via industry, not name', async () => {
const records = await query({ search: 'retail' });
const names = records.map((r) => String(r.name));
expect(names).toContain('Northwind');
// Guard the premise: Northwind's NAME does not contain "retail", so its
// presence in the result can only come from a non-name field…
expect(names.find((n) => n === 'Northwind')!.toLowerCase()).not.toContain('retail');
// …and prove it positively: restricting the same search to `industry`
// alone still returns Northwind — the hit IS the industry field.
const viaIndustry = await query({ search: 'retail', searchFields: ['industry'] });
expect(viaIndustry.map((r) => r.name)).toContain('Northwind');
// (The seed also has an account literally NAMED "acme retail" — a useful
// control: the unrestricted search returns it via `name`, proving the
// cross-field OR spans both fields in one query.)
expect(names.some((n) => n.toLowerCase().includes('retail'))).toBe(true);
});

it('select label→value mapping: the capitalized label "Retail" also matches', async () => {
const records = await query({ search: 'Retail' });
expect(records.map((r) => r.name)).toContain('Northwind');
});

it('$searchFields narrows: "retail" restricted to name matches nothing', async () => {
const records = await query({ search: 'retail', searchFields: ['name'] });
expect(records.map((r) => r.name)).not.toContain('Northwind');
});

it('terms AND: "retail northwind" still matches; "retail contoso" does not', async () => {
const both = await query({ search: 'retail northwind' });
expect(both.map((r) => r.name)).toContain('Northwind');
const cross = await query({ search: 'retail contoso' });
expect(cross.map((r) => r.name)).not.toContain('Northwind');
expect(cross.map((r) => r.name)).not.toContain('Contoso');
});
});