Skip to content

Commit a1f6816

Browse files
os-zhuangclaude
andauthored
test(dogfood): ADR-0061 search-conformance ledger + HTTP proof (flip to Accepted); ADR-0028 → Deferred (#3065)
* docs(adr)+test(dogfood): ADR-0028 -> Deferred with re-open trigger; ADR-0061 search-conformance ledger + HTTP dogfood proof - 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. * fix(dogfood): auth the search proof via apiAs; strengthen multi-field evidence; flip ADR-0061 to Accepted - 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). * chore(dogfood): drop unused 'join' import (CodeQL) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3cdda79 commit a1f6816

5 files changed

Lines changed: 154 additions & 2 deletions

docs/adr/0028-metadata-naming-and-namespace-isolation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0028: Metadata Naming & Namespace Isolation — Derived Physical Names, Namespace-Scoped Identity, and a Single Kernel Contract
22

3-
**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.
3+
**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.
44
**Deciders**: ObjectStack Protocol Architects
55
**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.
66
**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)

docs/adr/0061-record-search-architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0061: Record Search Architecture — metadata-driven `$search` resolution
22

3-
**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.
3+
**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`.
44
**Deciders**: ObjectStack Protocol Architects
55
**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)
66
**Consumers**: spec, objectql, driver-sql, driver-memory, driver-mongodb, rest, objectui (`fields`, `plugin-list`, `app-shell`), verify, dogfood
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { ConformanceRow } from '@objectstack/verify';
4+
//
5+
// ADR-0061 §Conformance — Record-Search Conformance ledger (ADR-0060 pattern).
6+
//
7+
// The ADR's own landing bar: `$search` is "landed" iff a driver-reachable
8+
// executor runs it AND a dogfood proof asserts a MULTI-FIELD match over the
9+
// real HTTP API (e.g. searching "retail" returns an account matched by
10+
// `industry`, not `name`). This ledger is the durable encoding of that bar:
11+
// every search-related declarable surface sits in exactly one honest state
12+
// (ADR-0049), names its executor site, and the enforced rows reference the
13+
// HTTP-level proof (`showcase-search.dogfood.test.ts`).
14+
//
15+
// Tier 2 (FTS / relevance / external engines) is deliberately ABSENT from this
16+
// ledger rather than carried as an aspirational row — per ADR-0049 a surface
17+
// that does not exist in the spec cannot be "declared but unenforced". When a
18+
// driver grows `fullTextSearch: true`, add its row here with a proof.
19+
20+
export const SEARCH_SURFACE: ConformanceRow[] = [
21+
{
22+
id: 'search-executor',
23+
summary: '`$search` server-resolved cross-field executor (terms AND-ed, fields OR-ed, case-insensitive `$contains`)',
24+
surface: 'spec/api/query.zod.ts:$search (QueryParams `search`)',
25+
state: 'enforced',
26+
enforcement: 'objectql/src/engine.ts (find AST expansion) → objectql/src/search-filter.ts expandSearchToFilter',
27+
proof: 'showcase-search.dogfood.test.ts',
28+
},
29+
{
30+
id: 'searchable-fields-object',
31+
summary: '`object.searchableFields` — canonical allowed-search-field set; auto-default (name/title + short text, secret/PII/json excluded) when unset',
32+
surface: 'spec/data/object.zod.ts:searchableFields',
33+
state: 'enforced',
34+
enforcement: 'objectql/src/search-filter.ts resolveSearchFields / autoDefaultFields',
35+
proof: 'showcase-search.dogfood.test.ts',
36+
},
37+
{
38+
id: 'search-fields-override',
39+
summary: '`$searchFields` per-query narrowing — validated against the allowed set, can never widen it',
40+
surface: 'spec/api/query.zod.ts:$searchFields',
41+
state: 'enforced',
42+
enforcement: 'objectql/src/search-filter.ts resolveSearchFields (intersection)',
43+
proof: 'showcase-search.dogfood.test.ts',
44+
},
45+
{
46+
id: 'search-select-label-mapping',
47+
summary: 'select/status label→value mapping — a human label in the query matches option values',
48+
surface: 'spec/data/field.zod.ts:options (select) × $search',
49+
state: 'enforced',
50+
enforcement: 'objectql/src/search-filter.ts optionValuesMatching / fieldClausesForTerm',
51+
proof: 'showcase-search.dogfood.test.ts',
52+
},
53+
];
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0061 §Conformance — the Record-Search Conformance ledger is a CHECKED
4+
// artifact (ADR-0060 `checkLedger`): shared invariants + every enforced row
5+
// must carry an existing proof file. The proof itself
6+
// (`showcase-search.dogfood.test.ts`) asserts the ADR's landing bar over the
7+
// real HTTP API; this test asserts the ledger cannot silently rot (a renamed
8+
// or deleted proof file fails here, closing the declared-but-unenforced loop).
9+
10+
import { describe, expect, it } from 'vitest';
11+
import { dirname } from 'node:path';
12+
import { fileURLToPath } from 'node:url';
13+
import { checkLedger } from '@objectstack/verify';
14+
import { SEARCH_SURFACE } from './search-conformance.ledger.js';
15+
16+
const HERE = dirname(fileURLToPath(import.meta.url));
17+
18+
describe('ADR-0061 record-search conformance ledger', () => {
19+
it('is sound and every enforced row carries an existing proof', () => {
20+
const problems = checkLedger(SEARCH_SURFACE, {
21+
proofRoot: HERE,
22+
proofRequiredForEnforced: true,
23+
});
24+
expect(problems).toEqual([]);
25+
});
26+
27+
it('covers the executor surface exactly once', () => {
28+
const ids = SEARCH_SURFACE.map((r) => r.id);
29+
expect(new Set(ids).size).toBe(ids.length);
30+
expect(ids).toContain('search-executor');
31+
});
32+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0061 §Conformance — the dogfood proof behind the search-conformance
4+
// ledger: a MULTI-FIELD `$search` match over the REAL HTTP API. The ADR's own
5+
// example, verbatim: searching "retail" must return an account matched by
6+
// `industry`, not `name` (the showcase seed's Northwind row carries
7+
// `industry: 'retail'` and no account name contains "retail"). Also proves
8+
// the `$searchFields` narrowing (can only narrow, never widen) so the
9+
// executor's security posture is pinned at the HTTP level, not just in
10+
// `search-filter.test.ts` unit tests.
11+
12+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
13+
import showcaseStack from '@objectstack/example-showcase';
14+
import { bootStack, type VerifyStack } from '@objectstack/verify';
15+
16+
describe('showcase: $search over the HTTP API (ADR-0061 conformance proof)', () => {
17+
let stack: VerifyStack;
18+
let token: string;
19+
20+
const query = async (body: Record<string, unknown>) => {
21+
const res = await stack.apiAs(token, 'POST', '/data/showcase_account/query', body);
22+
expect(res.status).toBe(200);
23+
const data = await res.json();
24+
return (data?.data?.records ?? data?.records ?? []) as Array<Record<string, unknown>>;
25+
};
26+
27+
beforeAll(async () => {
28+
stack = await bootStack(showcaseStack);
29+
token = await stack.signIn();
30+
}, 60_000);
31+
afterAll(async () => { await stack?.stop(); });
32+
33+
it('multi-field match: "retail" returns Northwind via industry, not name', async () => {
34+
const records = await query({ search: 'retail' });
35+
const names = records.map((r) => String(r.name));
36+
expect(names).toContain('Northwind');
37+
// Guard the premise: Northwind's NAME does not contain "retail", so its
38+
// presence in the result can only come from a non-name field…
39+
expect(names.find((n) => n === 'Northwind')!.toLowerCase()).not.toContain('retail');
40+
// …and prove it positively: restricting the same search to `industry`
41+
// alone still returns Northwind — the hit IS the industry field.
42+
const viaIndustry = await query({ search: 'retail', searchFields: ['industry'] });
43+
expect(viaIndustry.map((r) => r.name)).toContain('Northwind');
44+
// (The seed also has an account literally NAMED "acme retail" — a useful
45+
// control: the unrestricted search returns it via `name`, proving the
46+
// cross-field OR spans both fields in one query.)
47+
expect(names.some((n) => n.toLowerCase().includes('retail'))).toBe(true);
48+
});
49+
50+
it('select label→value mapping: the capitalized label "Retail" also matches', async () => {
51+
const records = await query({ search: 'Retail' });
52+
expect(records.map((r) => r.name)).toContain('Northwind');
53+
});
54+
55+
it('$searchFields narrows: "retail" restricted to name matches nothing', async () => {
56+
const records = await query({ search: 'retail', searchFields: ['name'] });
57+
expect(records.map((r) => r.name)).not.toContain('Northwind');
58+
});
59+
60+
it('terms AND: "retail northwind" still matches; "retail contoso" does not', async () => {
61+
const both = await query({ search: 'retail northwind' });
62+
expect(both.map((r) => r.name)).toContain('Northwind');
63+
const cross = await query({ search: 'retail contoso' });
64+
expect(cross.map((r) => r.name)).not.toContain('Northwind');
65+
expect(cross.map((r) => r.name)).not.toContain('Contoso');
66+
});
67+
});

0 commit comments

Comments
 (0)