From 9fe08315ac0db7dd2454bc3ff42a86ed9370a410 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 14:00:37 +0000 Subject: [PATCH 1/3] docs(adr)+test(dogfood): ADR-0028 -> Deferred with re-open trigger; ADR-0061 search-conformance ledger + HTTP dogfood proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 0028: formally Deferred — literal-prefix model works, no present pain; re-open trigger recorded (real multi-package naming pain). - 0061: close its own acceptance bar — search-conformance ledger (ADR-0060 checkLedger, proof-required) + showcase-search dogfood proof over the real HTTP API: multi-field 'retail'->industry match (Northwind), select label mapping, $searchFields narrowing, terms-AND semantics. --- ...metadata-naming-and-namespace-isolation.md | 2 +- .../dogfood/test/search-conformance.ledger.ts | 53 +++++++++++++++ .../dogfood/test/search-conformance.test.ts | 32 ++++++++++ .../test/showcase-search.dogfood.test.ts | 64 +++++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 packages/dogfood/test/search-conformance.ledger.ts create mode 100644 packages/dogfood/test/search-conformance.test.ts create mode 100644 packages/dogfood/test/showcase-search.dogfood.test.ts diff --git a/docs/adr/0028-metadata-naming-and-namespace-isolation.md b/docs/adr/0028-metadata-naming-and-namespace-isolation.md index 4bd577ba8f..93dc1817fd 100644 --- a/docs/adr/0028-metadata-naming-and-namespace-isolation.md +++ b/docs/adr/0028-metadata-naming-and-namespace-isolation.md @@ -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) diff --git a/packages/dogfood/test/search-conformance.ledger.ts b/packages/dogfood/test/search-conformance.ledger.ts new file mode 100644 index 0000000000..c1684c1837 --- /dev/null +++ b/packages/dogfood/test/search-conformance.ledger.ts @@ -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', + }, +]; diff --git a/packages/dogfood/test/search-conformance.test.ts b/packages/dogfood/test/search-conformance.test.ts new file mode 100644 index 0000000000..5979bbd577 --- /dev/null +++ b/packages/dogfood/test/search-conformance.test.ts @@ -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, join } 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'); + }); +}); diff --git a/packages/dogfood/test/showcase-search.dogfood.test.ts b/packages/dogfood/test/showcase-search.dogfood.test.ts new file mode 100644 index 0000000000..31c5acf0a8 --- /dev/null +++ b/packages/dogfood/test/showcase-search.dogfood.test.ts @@ -0,0 +1,64 @@ +// 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; + + const query = async (body: Record) => { + const res = await stack.api('/data/showcase_account/query', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }); + expect(res.status).toBe(200); + const data = await res.json(); + return (data?.data?.records ?? data?.records ?? []) as Array>; + }; + + beforeAll(async () => { + stack = await bootStack(showcaseStack); + 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) => r.name); + expect(names).toContain('Northwind'); + // Guard the premise that makes this a MULTI-FIELD proof: no account is + // NAMED *retail* — the hit can only have come from the industry field. + for (const n of names) { + expect(String(n).toLowerCase()).not.toContain('retail'); + } + }); + + 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'); + }); +}); From 2de81f28e63e948c4f86ffe34f10878f6f3ada45 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 14:18:16 +0000 Subject: [PATCH 2/3] fix(dogfood): auth the search proof via apiAs; strengthen multi-field evidence; flip ADR-0061 to Accepted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - showcase-search proof: use signIn token + apiAs (stack.api is unauthed); prove the industry hit positively via searchFields:['industry'] instead of assuming no account name contains 'retail' (the seed has 'acme retail' — now used as the cross-field OR control). - ADR-0061 -> Accepted (Tier 1): its own conformance bar is now met (ledger + HTTP dogfood proof, 6 tests green). --- docs/adr/0061-record-search-architecture.md | 2 +- .../test/showcase-search.dogfood.test.ts | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/adr/0061-record-search-architecture.md b/docs/adr/0061-record-search-architecture.md index 76ad5e236d..5ec40ba56f 100644 --- a/docs/adr/0061-record-search-architecture.md +++ b/docs/adr/0061-record-search-architecture.md @@ -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 diff --git a/packages/dogfood/test/showcase-search.dogfood.test.ts b/packages/dogfood/test/showcase-search.dogfood.test.ts index 31c5acf0a8..94376bc560 100644 --- a/packages/dogfood/test/showcase-search.dogfood.test.ts +++ b/packages/dogfood/test/showcase-search.dogfood.test.ts @@ -15,13 +15,10 @@ 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) => { - const res = await stack.api('/data/showcase_account/query', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(body), - }); + 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>; @@ -29,19 +26,25 @@ describe('showcase: $search over the HTTP API (ADR-0061 conformance proof)', () beforeAll(async () => { stack = await bootStack(showcaseStack); - await stack.signIn(); + 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) => r.name); + const names = records.map((r) => String(r.name)); expect(names).toContain('Northwind'); - // Guard the premise that makes this a MULTI-FIELD proof: no account is - // NAMED *retail* — the hit can only have come from the industry field. - for (const n of names) { - expect(String(n).toLowerCase()).not.toContain('retail'); - } + // 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 () => { From 08b6e9886b935094c6afd7829f201fc2659ef7b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 14:23:24 +0000 Subject: [PATCH 3/3] chore(dogfood): drop unused 'join' import (CodeQL) --- packages/dogfood/test/search-conformance.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dogfood/test/search-conformance.test.ts b/packages/dogfood/test/search-conformance.test.ts index 5979bbd577..8007649033 100644 --- a/packages/dogfood/test/search-conformance.test.ts +++ b/packages/dogfood/test/search-conformance.test.ts @@ -8,7 +8,7 @@ // or deleted proof file fails here, closing the declared-but-unenforced loop). import { describe, expect, it } from 'vitest'; -import { dirname, join } from 'node:path'; +import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { checkLedger } from '@objectstack/verify'; import { SEARCH_SURFACE } from './search-conformance.ledger.js';