reference-implementation-architecture Specification
Purpose
Define the durable architecture and boundary rules for the PDPP reference implementation in this repository without competing with the normative PDPP protocol specs.
Requirements
Requirement: The reference implementation remains a forkable substrate
The forkable implementation substrate SHALL live in reference-implementation/ and SHALL remain usable without the website runtime.
Scenario: An implementer evaluates the reference
- WHEN an implementer clones the repository to study or fork the reference implementation
- THEN they SHALL be able to run and understand the core reference substrate from
reference-implementation/without depending onapps/web
Scenario: The website changes independently
- WHEN the website or docs application changes its internal implementation
- THEN the forkable reference substrate SHALL remain the authoritative runnable implementation artifact rather than becoming coupled to website-only code paths
Requirement: The website is a downstream consumer
apps/web SHALL act as a downstream consumer of the reference implementation and SHALL not define the primary reference contract.
Scenario: A bridge route exists for the website
- WHEN
apps/webexposes a bridge route to the reference implementation - THEN that bridge SHALL reflect the current reference contract honestly and SHALL not invent a stronger or different protocol contract than the underlying reference implementation exposes
Scenario: The website needs traces or examples
- WHEN the website renders traces, examples, or demos derived from the reference implementation
- THEN those artifacts SHALL be treated as derived explanatory surfaces rather than as the implementation boundary itself
Requirement: Native and polyfill realizations stay honest
The reference implementation SHALL support both native-provider and polyfill realizations over one engine substrate while keeping their public source identity honest.
Scenario: A native provider request is staged
- WHEN a client requests data from a native provider realization
- THEN the public request and public artifacts SHALL identify that source with
provider_idrather than with a publicconnector_id
Scenario: A polyfill request is staged
- WHEN a client requests data from a connector-based or collected realization
- THEN the public request and public artifacts SHALL identify that source with
connector_id
Scenario: Internal storage remains connector-shaped
- WHEN the implementation needs connector-shaped or storage-specific internal identifiers
- THEN those identifiers MAY remain internal implementation details, but they SHALL not leak into native-provider public artifacts unless explicitly documented as reference-only internals
Scenario: Native mode is configured
- WHEN the reference implementation starts in native-provider mode
- THEN the native manifest SHALL include explicit
provider_idand structuredstorage_binding - AND startup SHALL derive native provider identity and storage binding from that manifest rather than from separate native override flags
Requirement: CLI and tests are first-class consumers
The CLI and executable tests SHALL consume the real public or reference-designated surfaces of the implementation rather than private database shortcuts or website-only glue.
Scenario: The CLI needs to inspect a reference object
- WHEN the CLI needs trace, grant, run, owner, or provider information
- THEN it SHALL use the relevant public or explicitly reference-designated HTTP surface rather than bypassing the server through direct database access
Scenario: The test suite verifies behavior
- WHEN executable tests prove reference behavior
- THEN those tests SHALL prefer black-box interaction with the running reference surfaces unless a narrower white-box test is intentionally justified for implementation internals
Requirement: Reference-only surfaces are explicit
Debugging, replay, trace, and operator-control surfaces that are useful for the reference implementation but are not part of core PDPP SHALL be explicitly marked as reference-only.
Scenario: A trace or timeline endpoint is exposed
- WHEN the implementation exposes trace, timeline, or similar introspection surfaces
- THEN those surfaces SHALL be clearly described as reference-only artifacts rather than as core PDPP protocol requirements
Scenario: The current _ref read surface is treated as stable substrate
- WHEN the implementation exposes the current reference-designated event-spine readers
- THEN the durable
_refread surface SHALL stay limited to:GET /_ref/traces/:traceIdGET /_ref/grants/:grantId/timelineGET /_ref/runs/:runId/timelineGET /_ref/traces(list, filter, paginate)GET /_ref/grants(list, filter, paginate)GET /_ref/runs(list, filter, paginate)GET /_ref/search?q=...(id-aware read-only jump helper)GET /_ref/dataset/summary(dashboard overview dataset summary)
Scenario: The dashboard summarizes dataset credibility
- WHEN the reference dashboard renders a dataset summary or credibility overview
- THEN it MAY consume
GET /_ref/dataset/summary - AND that route SHALL remain documented as a reference-only read surface rather than as a public PDPP API
Scenario: A later control-plane phase widens _ref mutation narrowly
- WHEN a later control-plane phase needs a truthful operator mutation surface for a live bounded collection run
- THEN the reference MAY add an owner-only
_refmutation endpoint limited to:POST /_ref/runs/:runId/interaction
- AND that route SHALL be documented as reference-only control-plane behavior rather than as a public PDPP API
- AND the reference SHALL NOT widen
_refinto broader mutation/control endpoints in the same tranche without a further explicit OpenSpec change
Scenario: Run timelines expose checkpoint staging separately from checkpoint commit
- WHEN the reference runtime receives
STATEduring a bounded collection run - THEN the
_refrun timeline SHALL distinguish checkpoint staging from checkpoint commit so the checkpointed-streaming model is visible in reference artifacts rather than implied only by runtime internals
Scenario: Runtime validation failures remain inspectable in the reference substrate
- WHEN a bounded collection run fails because the runtime rejects connector output or an interaction handler response before
DONE - THEN the durable
_refrun timeline SHALL still recordrun.failedwith an explicit machine-readable reason instead of leaving that failure visible only as a thrown local error
Requirement: Reference control-plane mutations require owner session when enabled
The reference implementation SHALL require the placeholder owner session on reference-only _ref mutation routes when owner auth is enabled. When owner auth is disabled, the reference implementation SHALL preserve the current open local-dev behavior for those routes.
Scenario: Owner auth is enabled and a mutation has no session
- WHEN a caller submits a
_refmutation request without a valid owner-session cookie whilePDPP_OWNER_PASSWORDis configured - THEN the reference SHALL reject the request with
401 owner_session_required - AND the route handler SHALL NOT perform the requested mutation
Scenario: Owner auth is enabled and a mutation has a session
- WHEN a caller submits a
_refmutation request with a valid owner-session cookie whilePDPP_OWNER_PASSWORDis configured - THEN the reference SHALL process the mutation according to the route's existing behavior
Scenario: Owner auth is disabled
- WHEN a caller submits a
_refmutation request while placeholder owner auth is disabled - THEN the reference SHALL preserve the open local-dev behavior for that mutation route
Scenario: Reference read routes remain inspection surfaces
- WHEN a caller requests an existing
_refread route - THEN this change SHALL NOT require owner-session authentication for that read route
Requirement: Run interaction control is owner-only and ephemeral
The reference implementation SHALL treat dashboard-submitted responses to live run interactions as owner-only, reference-only control-plane actions for the current active run. Submitted values SHALL satisfy the current pending interaction only and SHALL NOT become durable credential storage.
Scenario: A pending interaction is answered successfully
- WHEN an owner submits
POST /_ref/runs/:runId/interactionfor the current pending interaction withstatus: "success"and any requireddata - THEN the reference SHALL deliver a matching
INTERACTION_RESPONSEback to the live run - AND the run timeline SHALL continue to expose only the existing safe
run.interaction_completedmetadata rather than the submitted secret values
Scenario: A pending interaction is cancelled
- WHEN an owner submits
POST /_ref/runs/:runId/interactionfor the current pending interaction withstatus: "cancelled" - THEN the reference SHALL deliver a matching cancelled
INTERACTION_RESPONSEback to the live run - AND the runtime SHALL remain the authority for any resulting run failure or completion behavior
Scenario: A stale or non-current interaction response is submitted
- WHEN a caller submits an interaction response for an unknown run, a non-active run, a run with no current pending interaction, or an
interaction_idthat no longer matches the current pending interaction - THEN the reference SHALL reject the request honestly instead of fabricating an interaction completion
Scenario: A dashboard-submitted credential is processed
- WHEN an owner submits credentials or OTP data through the run interaction control endpoint
- THEN the reference SHALL use those values only to satisfy the current pending interaction
- AND it SHALL NOT write those values to
.env.local, durable SQLite state, or other long-lived reference configuration as part of this control-plane action
Requirement: The Collection boundary stays explicit
The reference implementation SHALL keep the Collection boundary explicit across core semantics, Collection Profile semantics, and runtime-only behavior.
Scenario: Shared collection semantics are classified
- WHEN behavior concerns RECORD envelopes, streams, scope, tombstones, or state/checkpoint semantics shared across collection and disclosure paths
- THEN those semantics SHALL be treated as core/shared semantics rather than as ad hoc runtime details
Scenario: Bounded-run collection behavior is classified
- WHEN behavior concerns START, INTERACTION, RECORD, STATE, DONE, binding matching, or run-scoped lifecycle rules for collected/polyfill sources
- THEN that behavior SHALL be treated as Collection Profile behavior rather than as native-provider contract surface
Scenario: Orchestrator behavior is classified
- WHEN behavior concerns scheduling, retry, credential storage, webhook adaptation, batch import, or multi-connector coordination
- THEN it SHALL be treated as runtime/orchestrator behavior unless and until a concrete interoperability need justifies a new profile
Scenario: The reference makes an optimistic collection choice before the spec is fully frozen
- WHEN the reference implementation enforces a strong Collection Profile behavior before the PDPP spec is fully settled
- THEN that behavior SHALL be labeled as either an interoperability requirement to be pushed into the Collection Profile spec or as a reference-only choice that does not yet claim normative status
Requirement: Open design questions stay explicit
The reference implementation SHALL keep unresolved design questions explicit in OpenSpec whenever implementation work materially narrows the plausible design space without fully settling the normative PDPP answer.
Scenario: Collection run durability semantics are still unsettled
- WHEN the reference implementation behaves like a checkpointed streaming system where writes may become durable before checkpoint commit
- THEN OpenSpec SHALL record that open question explicitly rather than implying that the Collection Profile already guarantees atomic run semantics
Scenario: Cross-stream checkpoint flush semantics are still unsettled
- WHEN the reference runtime flushes and stages checkpoint input only for the stream named in a
STATEmessage while leaving other buffered streams untouched - THEN OpenSpec SHALL record whether that per-stream checkpoint boundary is intended to become Collection Profile normativity or remain a reference/runtime choice
Scenario: Failed DONE checkpoint semantics are still unsettled
- WHEN the reference runtime receives
DONEwithstatus: "failed"after one or more streams have already staged checkpoint input - THEN OpenSpec SHALL record whether that failed terminal status is intended to become a normative no-checkpoint-commit boundary or remain a reference/runtime choice
Scenario: Cross-stream checkpoint commit failures after successful DONE remain unsettled
- WHEN the reference runtime reaches
DONE(status="succeeded")but a later checkpoint persistence write fails after one or more earlier stream checkpoints have already committed - THEN OpenSpec SHALL record whether partial cross-stream checkpoint commit is an acceptable reference/runtime outcome or whether successful terminal runs are expected to provide stronger atomic checkpoint guarantees
Scenario: Post-DONE protocol violations still interact with checkpointed streaming
- WHEN a connector emits additional messages after
DONEand the reference runtime invalidates the run as a protocol violation after some writes may already be durable - THEN OpenSpec SHALL record whether that terminal violation is intended to preserve already-flushed writes under the checkpointed-streaming model or whether stronger atomic rollback guarantees should exist
Scenario: Connector-reported terminal counters are validated before the spec is fully settled
- WHEN the reference runtime rejects a run because connector-reported terminal counters such as
DONE.records_emitteddo not match the runtime-observed run output - THEN OpenSpec SHALL record whether those counter validations are intended to become Collection Profile normativity or remain a strong reference/runtime validation choice
Scenario: Interaction terminal-status semantics are still unsettled
- WHEN the reference runtime auto-responds to an
INTERACTIONrequest with terminal statuses such astimeoutorcancelled - THEN OpenSpec SHALL record whether those terminal-status semantics are intended to become Collection Profile normativity or remain reference/runtime-only choices
Scenario: Progress and skip lifecycle artifacts narrow the event-spine boundary without settling profile normativity
- WHEN the reference runtime turns connector
PROGRESSandSKIP_RESULTmessages into durable_refrun events - THEN OpenSpec SHALL record whether those messages are intended to remain reference/runtime observability artifacts only or eventually become part of a stronger Collection Profile or sibling-profile contract
Scenario: Connector-declared terminal error details narrow the collection/runtime boundary without settling profile normativity
- WHEN the reference runtime preserves validated connector-declared
DONE.errordetails only for failed or cancelled terminal states, rejects contradictory success terminals, and rejects unsupported terminal-error fields beyond the current minimal shape - THEN OpenSpec SHALL record whether those terminal error details are intended to become Collection Profile normativity, including whether they are failure-only, or remain reference/runtime-only observability fields
Scenario: Provider-connect launch scope is intentionally broader than a single trust path
- WHEN the reference implementation supports multiple provider-connect paths such as owner self-export, pre-registered clients, and protected DCR
- THEN OpenSpec SHALL record which trust/bootstrap paths are part of the launch reference target and which remain open design questions
Scenario: Pending-consent manifest pinning remains explicit
- WHEN the reference implementation pins staged pending-consent requests to the manifest version resolved at
/oauth/par - THEN OpenSpec SHALL record whether that manifest-version pinning is intended to become part of the provider-connect contract or remain a stronger reference-only hardening choice
Scenario: Pending-consent client registration re-resolution remains explicit
- WHEN the reference implementation re-resolves the registered client during consent display and approval instead of trusting the staged pending-consent client snapshot
- THEN OpenSpec SHALL record whether consent-time client re-resolution is intended to become part of the provider-connect contract or remain a stronger reference-only hardening choice
Scenario: Internal/native honesty is not fully settled
- WHEN the reference implementation keeps connector-shaped internal seams while presenting a provider-first native public contract
- THEN OpenSpec SHALL record whether that split is considered an acceptable long-term implementation detail or an area for future internal realignment
Scenario: Grant persistence uses structured storage binding only
- WHEN the reference implementation persists grant storage bindings
- THEN it SHALL use the explicit structured
storage_binding_jsonmodel rather than a second scalar compatibility column
Scenario: Pending consent and grant reads require current structured bindings
- WHEN the reference implementation reads persisted pending-consent requests or grant-bound disclosure state
- THEN it SHALL require explicit structured
source_binding,storage_binding, andgrant.sourcedata rather than self-healing malformed persisted rows from ambient native configuration
Scenario: Reference-only observability surfaces may grow into a control plane
- WHEN trace, timeline, replay, or other
_refsurfaces exist before a full control plane is designed - THEN OpenSpec SHALL record which of those surfaces are durable reference-only boundaries and which future operator/control-plane questions remain unresolved
Requirement: OpenSpec architecture stays project-scoped
This architecture specification SHALL define repository-level implementation boundaries and SHALL not become a second normative PDPP protocol specification.
Scenario: Root PDPP stream metadata semantics are settled
- WHEN the root PDPP specifications define
GET /v1/streams/{stream}as returning full source stream metadata rather than a grant-projected view - THEN the reference implementation SHALL keep
stream_metadatasource-level - AND it SHALL enforce grants through authorization, queries, and record disclosure rather than by projecting the stream metadata document itself
Scenario: A protocol semantic changes
- WHEN a change alters normative PDPP protocol semantics
- THEN the relevant root PDPP spec file SHALL be updated and this architecture spec MAY only describe the resulting implementation impact at a project boundary level
Scenario: Architecture guidance needs protocol context
- WHEN this architecture spec depends on protocol concepts such as grants,
authorization_details, collection runs, or owner tokens - THEN it SHALL rely on the root PDPP specs as the normative source for those concepts rather than redefining them here
Requirement: The RS read-path for enumerated routes SHALL not materialize unbounded result arrays
The resource-server SHALL NOT execute a query whose result is an unbounded scan of a JSON-column table on the read paths covered by this change, which are:
GET /v1/streams/:stream/records(includingexpand=…)GET /v1/streams/:stream/records/:id(includingexpand=…)GET /_ref/runs,GET /_ref/grants,GET /_ref/traces,GET /_ref/searchGET /_ref/records/timeline
For these paths:
- Access-control filters (
time_range,resources) SHALL be expressed as SQLWHEREclauses that constrain the scan at the storage layer. - Pagination (
limit,cursor, and per-parent limits inexpand) SHALL be applied at the SQL layer viaORDER BY+LIMITor window functions, not by loading the full set andslice-ing in application code. - When a handler needs to iterate results, it SHALL stream via the driver's iterator API (e.g.
Statement.iterate()inbetter-sqlite3) and stop as soon as the bounded page is assembled. - Child-stream expansion SHALL filter the child scan by the parent page's foreign-key values in SQL, not fetch the whole child stream and group in application code.
Handlers MAY parse JSON columns into objects for the rows that survive into the response, but SHALL NOT parse JSON for rows that would be filtered out by access control.
This Requirement applies to the read paths enumerated above. Other read paths (if any) are out of scope for this change; bringing them under the same invariant is a follow-up.
Scenario: A grant narrows visibility via time_range
- WHEN a client queries
/v1/streams/<s>/recordsunder a grant that declarestime_range: { since, until }on stream<s> - THEN the SQL query the RS issues SHALL include a predicate that compares the row's
consent_time_fieldagainstsince/until - AND rows outside the window SHALL NOT be parsed, materialized, or allocated in application memory
Scenario: A grant narrows visibility via resources
- WHEN a client queries
/v1/streams/<s>/recordsunder a grant that declares a non-emptyresourcesallowlist on stream<s> - THEN the SQL query SHALL include an
INpredicate against the allowed record keys - AND rows whose keys are not in the allowlist SHALL NOT be parsed or materialized
Scenario: Pagination is pushed into SQL
- WHEN a client queries
/v1/streams/<s>/recordswithlimit=Nand optionallycursor - THEN the SQL query SHALL emit
ORDER BY <cursor_field, primary_key>andLIMIT N+1 - AND the RS SHALL read at most
N+1rows from the driver, not the full filtered set
Scenario: Expansion pushes child-stream narrowing into SQL
- WHEN a client queries
/v1/streams/<s>/records?expand=<relation>&expand_limit[<relation>]=Kwith a page of N parent rows - THEN the SQL query over the child stream SHALL filter by
WHERE child.foreign_key IN (…N parent keys…)plus the child grant'stime_range/resourcesconstraints - AND the child query SHALL fetch at most
N × (K + 1)rows forhas_manyexpansions (via window function) orNrows forhas_oneexpansions - AND the RS SHALL NOT scan the child stream's full table
Scenario: Correlation-key listing pages in SQL
- WHEN a client lists
/_ref/runs,/_ref/grants, or/_ref/traceswith a page size - THEN the SQL query SHALL aggregate in-SQL via
GROUP BYand paginate in-SQL viaORDER BY+LIMIT+ cursor - AND the RS SHALL NOT materialize the full
spine_eventstable to group in application code
Scenario: Records timeline applies time window in SQL
- WHEN a client queries
/_ref/records/timeline?since=A&until=B - THEN for each
(connector, stream)pair it scans, the SQL query SHALL include a predicate againstCOALESCE(NULLIF(json_extract(record_json, '$.<semantic_field>'), ''), emitted_at)(native mode) oremitted_at(emitted mode) between the normalizedA/Bboundaries - AND the query SHALL apply a per-pair SQL
LIMIT - AND the RS SHALL NOT scan and JSON-parse rows outside the window
Note — deferred standing defenses: additional runtime defenses (per-route in-flight concurrency cap with coupled dashboard 503 retry + partial-failure coordination, response-size budget hook, process-supervisor mandate) were considered and deferred because the read-path rewrite above resolved the measured crash pathology on its own (5/5 repro runs survived post-fix; old-space peak dropped from 600–730 MB to ~14 MB). They remain open follow-ups, to be taken up only when a measured remaining problem justifies the scope. See openspec/changes/archive/2026-04-24-fix-rs-query-memory-pressure/ (proposal.md §Follow-ups and tasks.md §6) for the full rationale, intended shapes, and implementation notes.
Requirement: The reference implementation SHALL emit a structured completion log for every request
When the reference implementation is running as a server (whether as a CLI entrypoint or as a library-embedded instance), every inbound HTTP request SHALL produce a single completion log record containing req_id, HTTP method, path, statusCode, and responseTime in milliseconds. The record SHALL be emitted at info level. The record's structured field set SHALL be identical across all environments; only terminal formatting MAY differ between development and production.
Scenario: Successful request produces a completion record
- WHEN a client calls an AS or RS endpoint
- THEN exactly one request-completion log record SHALL be emitted
- AND that record SHALL include
req_id, method, path,statusCode, andresponseTime
Scenario: Completion record shape is environment-independent
- WHEN the same request is served under
NODE_ENV=productionand under non-production - THEN the set of structured fields in the completion record SHALL be identical across both environments
Requirement: Request-scoped logs SHALL carry protocol trace correlation
The reference implementation SHALL rebind the Fastify request-scoped logger to include trace_id, scenario_id, actor_type, and actor_id as child-logger fields once those values are resolved for the request. Any log record emitted by handler code after that rebind SHALL carry those fields when they are present for the request. Requests for which no trace_id is established (e.g. static metadata endpoints that do not participate in the event spine) SHALL NOT have a synthetic trace_id fabricated.
Scenario: Handler log line inherits trace correlation
- WHEN a handler resolves a
trace_idfor a request and then callsrequest.log.info(...) - THEN the emitted record SHALL include both
req_idandtrace_id
Scenario: No trace resolved means no trace_id field
- WHEN a request completes without the reference implementation establishing a
trace_idfor it - THEN the completion record SHALL NOT include a
trace_idfield
Requirement: The reference implementation SHALL redact known secret paths in log output
Structured log output SHALL NOT contain the plaintext of access tokens, refresh tokens, device codes, user codes, the Authorization header value, or the interaction_response payload used in hosted-UI flows. Redaction SHALL be configured declaratively at the logger, not performed per call site.
Scenario: A handler logs an object containing a token
- WHEN a handler passes an object with
access_tokenorrefresh_tokeninto a log call - THEN the emitted record SHALL show the token value as
<redacted>(or equivalent censor value), not the plaintext
Scenario: An Authorization header is captured by the default request serializer
- WHEN the logger's request serializer records request headers
- THEN the
Authorizationheader value SHALL appear redacted, not in plaintext
Requirement: The CLI entrypoint SHALL produce a final structured log record on crash or signal
When reference-implementation/server/index.js is run as a CLI entrypoint, it SHALL install process-level handlers for uncaughtException, unhandledRejection, SIGTERM, and SIGINT. Each handler SHALL emit exactly one log record before the process exits. These handlers SHALL NOT be installed when server/index.js is imported as a library (for example, from a test harness); the reference implementation SHALL NOT register global process.on listeners from any code path other than the CLI entrypoint block.
Scenario: Uncaught exception at the CLI entrypoint
- WHEN the CLI is running and code in a request handler or background task throws and the error is not otherwise caught
- THEN exactly one
fatallog record SHALL be emitted on stdout with the error name, message, and stack before the process exits with a non-zero code
Scenario: Unhandled promise rejection at the CLI entrypoint
- WHEN the CLI is running and a promise rejection propagates to the top level
- THEN exactly one
fatallog record SHALL be emitted on stdout with the rejection reason and stack before the process exits with a non-zero code
Scenario: Termination signal at the CLI entrypoint
- WHEN the CLI process receives
SIGTERMorSIGINT - THEN exactly one
infolog record SHALL be emitted on stdout naming the signal before the process performs graceful shutdown and exits
Scenario: Library import does not pollute the process
- WHEN a test or another Node program imports
startServerfromserver/index.jsand calls it one or more times - THEN the reference implementation SHALL NOT add any listeners to
processforuncaughtException,unhandledRejection,SIGTERM, orSIGINT
Requirement: Log shape SHALL be JSON; development output MAY be pretty-printed for the terminal only
The reference implementation SHALL produce log records as JSON objects at the point of emission. In production (NODE_ENV === 'production') those JSON objects SHALL be written to stdout verbatim as one JSON line per record. In all other environments a terminal-local transform MAY reformat records for human reading. The transform SHALL NOT add, remove, or rename structured fields in the underlying record.
Scenario: Production deployment emits JSON lines
- WHEN the reference implementation starts with
NODE_ENV=production - THEN every log line on stdout SHALL be a single JSON object consumable by a downstream log aggregator without further parsing
Scenario: Local dev emits pretty-printed lines carrying the same structured fields
- WHEN the reference implementation starts without
NODE_ENV=production - THEN log output MAY be pretty-printed for the terminal
- AND every structured field present in the production JSON form SHALL remain observable in the pretty form
Requirement: Log field names SHALL be compatible with the OpenTelemetry log data model
The reference implementation's log records SHALL use trace_id (not traceId, trace, or traceID) for the protocol event-spine identifier. The field span_id SHALL be reserved for future OpenTelemetry alignment and SHALL NOT be repurposed for other concepts. Request identifiers SHALL be named req_id.
Scenario: A reviewer inspects log output for OTel compatibility
- WHEN a reviewer reads log records produced by the reference implementation
- THEN trace identifiers SHALL appear under the field name
trace_id - AND the field name
span_idSHALL NOT be used for anything other than an OTel-shaped span identifier if emitted
Requirement: The reference SHALL realize the lexical-retrieval extension over a single internal enforcement path
The reference implementation SHALL realize the public lexical-retrieval extension defined in the lexical-retrieval capability through one internal helper that performs grant resolution, plan construction, and grant-safe snippet generation in the same code path. The public GET /v1/search route handler SHALL delegate to that helper. Reference-internal callers (including the website dashboard) SHALL reach lexical retrieval through the same public route over HTTP, not through a parallel direct-database path. The reference SHALL NOT define a second lexical retrieval contract.
Scenario: The dashboard searches owner records
- WHEN the website dashboard search page renders results for an owner
- THEN it SHALL obtain those results by calling the public
GET /v1/searchendpoint of the resource server with the dashboard's owner-bound bearer token - AND it SHALL NOT compute results by fanning out per-stream record-list calls and substring-matching their JSON payloads in application code
Scenario: A second internal callsite is proposed
- WHEN any reference-side caller (CLI, dashboard, future operator surface) needs lexical retrieval over authorized records
- THEN that caller SHALL go through
GET /v1/search(or, in-process, the single internal helper that the route delegates to) - AND SHALL NOT reach into the FTS5 index, manifest validator, or grant resolver to assemble its own lexical retrieval contract
Requirement: The reference's manifest validator SHALL enforce the v1 lexical_fields shape
When a connector manifest declares query.search.lexical_fields on any stream, the reference's manifest validator SHALL enforce the v1 shape constraints. The validator SHALL reject manifests whose declarations would let the public extension search anything other than top-level scalar string fields named in the stream's schema.
Scenario: A manifest declares a nested path as a lexical field
- WHEN a manifest declares
query.search.lexical_fields: ["data.body"](a nested path) on a stream - THEN the reference's manifest validator SHALL reject registration of that manifest
Scenario: A manifest declares an array-typed schema field as a lexical field
- WHEN a manifest declares
query.search.lexical_fields: ["tags"]and the stream's schema liststagsastype: "array" - THEN the reference's manifest validator SHALL reject registration of that manifest
Scenario: A manifest declares a non-existent field as a lexical field
- WHEN a manifest declares
query.search.lexical_fields: ["nonexistent"]andnonexistentis not inschema.properties - THEN the reference's manifest validator SHALL reject registration of that manifest
Scenario: A manifest declares an empty lexical_fields array
- WHEN a manifest declares
query.search.lexical_fields: [] - THEN the reference's manifest validator SHALL reject registration of that manifest
Requirement: The reference SHALL publish the capabilities.lexical_retrieval advertisement on its existing protected-resource metadata document
When the reference exposes the lexical-retrieval extension, the existing RFC 9728 protected-resource metadata document the reference already serves SHALL include a capabilities.lexical_retrieval object carrying all six required keys. The reference SHALL NOT introduce a new metadata document for this advertisement, and SHALL NOT publish the advertisement on the authorization-server metadata document.
Scenario: The advertisement is co-located with existing RS metadata
- WHEN a client retrieves the reference's protected-resource metadata document
- THEN the response SHALL include
capabilities.lexical_retrievalwithsupported,endpoint,cross_stream,snippets,default_limit, andmax_limit - AND the reference SHALL NOT serve the advertisement from a separately discoverable metadata document
Scenario: A reference fork wishes to publish the extension as unsupported
- WHEN a reference fork or test harness configures the reference to omit the extension
- THEN the protected-resource metadata document SHALL either omit
capabilities.lexical_retrievalentirely or include it withsupported: false
Requirement: The reference's lexical retrieval index SHALL index only declared lexical_fields
The reference's local search backing (a SQLite FTS5 virtual table) SHALL contain entries only for (stream, record_key, field) tuples where field appears in the corresponding stream's query.search.lexical_fields declaration. Records of streams that do not declare lexical_fields SHALL NOT contribute index rows. Non-declared fields of records of streams that do declare lexical_fields SHALL NOT contribute index rows.
Scenario: A non-participating stream has new records
- WHEN new records arrive for a stream whose manifest does not declare
query.search.lexical_fields - THEN the FTS5 lexical search index SHALL NOT receive new rows for that stream
Scenario: A participating stream has new records
- WHEN new records arrive for a stream whose manifest declares
lexical_fields: ["a", "b"] - THEN the FTS5 lexical search index SHALL receive exactly two rows for each record (one per declared field)
- AND SHALL NOT receive rows for any other field of that record
Scenario: The index drifts from the records table
- WHEN the reference starts and detects a mismatch between the records table and the FTS5 index for one or more participating streams
- THEN the reference SHALL rebuild the index from the records table for the affected streams
Requirement: The reference SHALL realize owner-token lexical retrieval through cross-connector fan-out
The reference scopes owner reads of records and stream metadata per connector. The reference SHALL realize owner-token lexical retrieval by fanning out across every owner-visible connector internally and merging results, so that the public GET /v1/search request shape stays identical for owner-token and client-token callers (no public connector_id query parameter). Each search_result returned to an owner-token caller SHALL carry the originating connector via connector_id so the caller can hydrate the record under the correct per-connector owner read scope. The reference SHALL emit a record_url that includes the canonical owner-mode connector_id query parameter for owner-token callers.
Scenario: An owner searches across two connectors that both expose the same stream name
- WHEN an owner-token caller invokes
GET /v1/search?q=alphaon a reference instance with two owner-visible connectorsC1andC2, both of which expose amessagesstream that declareslexical_fields: ["text"]and both of which contain a record matchingalpha - THEN the response SHALL include hits from BOTH connectors
- AND each hit SHALL carry its originating
connector_id("C1"for hits fromC1,"C2"for hits fromC2) - AND the response SHALL NOT silently scope to a single connector
Scenario: An owner request includes connector_id
- WHEN an owner-token caller invokes
GET /v1/search?q=alpha&connector_id=C1 - THEN the reference SHALL reject the request with
invalid_request_erroridentifyingconnector_idas the rejected parameter - AND SHALL NOT silently use
connector_idto scope the search
Scenario: An owner-mode record_url is hydrated
- WHEN an owner-token caller takes the
record_urlfrom a/v1/searchhit and issues a GET against it under the same owner token - THEN the reference SHALL return the canonical record envelope at
GET /v1/streams/{stream}/records/{record_key}for the connector identified by the URL'sconnector_idquery parameter
Requirement: The reference's lexical retrieval index SHALL include connector identity in every row
Because the reference's owner reads are per-connector, the lexical retrieval index SHALL include the originating connector_id on every indexed row so that owner-mode hits can be attributed to a connector for hydration. Insert/update/delete maintenance for a record SHALL include that record's connector_id. Reference search results SHALL carry the indexed connector_id through to the search_result.connector_id field of the public response.
Scenario: Records for two connectors are indexed
- WHEN records arrive for stream
messagesfrom connectorsC1andC2, both of which declarelexical_fields: ["text"] - THEN the FTS5 lexical search index SHALL contain rows attributed to
C1forC1's records and rows attributed toC2forC2's records - AND SHALL NOT silently merge rows under a single shared connector identity
Scenario: A search result is attributed to its originating connector
- WHEN the reference returns a
search_resultto a caller - THEN that result's
connector_idSHALL be theconnector_idrecorded on the matching index row at insert time - AND the reference SHALL NOT fabricate
connector_idfrom configuration or from the caller's identity
Requirement: The reference SHALL keep /_ref/search distinct from /v1/search
The reference SHALL NOT alias /_ref/search to /v1/search, SHALL NOT serve the public lexical retrieval contract from /_ref/search, and SHALL NOT advertise /_ref/search as the public lexical retrieval endpoint. The reference's source code SHALL note /_ref/search's reference-only status near its handler so future readers cannot mistake it for the public surface.
Scenario: A client requests /_ref/search
- WHEN a client calls
/_ref/search?q=... - THEN the response SHALL be the existing reference-only spine artifact-and-id-jump shape
- AND the response SHALL NOT match the public
search_resultlist envelope returned by/v1/search
Scenario: A reader inspects the reference source
- WHEN a reader reads the source for
/_ref/searchinreference-implementation/server/index.js - THEN an inline comment SHALL identify the route as reference-only and SHALL point readers to
GET /v1/searchfor the public lexical retrieval surface
Requirement: The reference SHALL realize the semantic-retrieval experimental extension over a single internal enforcement path
The reference implementation SHALL realize the public semantic-retrieval extension defined in the semantic-retrieval capability through one internal helper that performs grant resolution, plan construction, embedding invocation, vector-index lookup, and grant-safe snippet generation in the same code path. The public GET /v1/search/semantic route handler SHALL delegate to that helper. Reference-internal callers (including the website dashboard) SHALL reach semantic retrieval through the same public route over HTTP, not through a parallel direct-database path. The reference SHALL NOT define a second semantic retrieval contract.
Scenario: The dashboard helper reaches semantic retrieval through the public route
- WHEN a reference-side caller in
apps/web/src/app/dashboard/lib/rs-client.tsrequests semantic retrieval over owner records - THEN it SHALL obtain those results by calling the public
GET /v1/search/semanticendpoint with an owner-bound bearer token - AND it SHALL NOT compute semantic results by reaching into the vector index or the embedding backend directly
Scenario: A second internal callsite is proposed
- WHEN any reference-side caller (CLI, dashboard, future operator surface) needs semantic retrieval over authorized records
- THEN that caller SHALL go through
GET /v1/search/semantic(or, in-process, the single internal helper that the route delegates to) - AND SHALL NOT reach into the vector index, the embedding backend, the manifest validator, or the grant resolver to assemble its own semantic retrieval contract
Requirement: The reference's manifest validator SHALL enforce the v1 semantic_fields shape independently of lexical_fields
When a connector manifest declares query.search.semantic_fields on any stream, the reference's manifest validator SHALL enforce the v1 shape constraints. The validator SHALL reject manifests whose declarations would let the public extension embed or match anything other than top-level scalar string fields named in the stream's schema. The semantic_fields enforcement SHALL run independently of lexical_fields enforcement: either, both, or neither MAY be declared on a stream.
Scenario: A manifest declares a nested path as a semantic field
- WHEN a manifest declares
query.search.semantic_fields: ["data.body"]on a stream - THEN the reference's manifest validator SHALL reject registration of that manifest
Scenario: A manifest declares an array-typed schema field as a semantic field
- WHEN a manifest declares
query.search.semantic_fields: ["tags"]and the stream's schema liststagsastype: "array" - THEN the reference's manifest validator SHALL reject registration of that manifest
Scenario: A manifest declares a blob-typed schema field as a semantic field
- WHEN a manifest declares
query.search.semantic_fields: ["attachment"]and the stream's schema listsattachmentas a blob reference - THEN the reference's manifest validator SHALL reject registration of that manifest
Scenario: A manifest declares a non-existent field as a semantic field
- WHEN a manifest declares
query.search.semantic_fields: ["nonexistent"]andnonexistentis not inschema.properties - THEN the reference's manifest validator SHALL reject registration of that manifest
Scenario: A manifest declares an empty semantic_fields array
- WHEN a manifest declares
query.search.semantic_fields: [] - THEN the reference's manifest validator SHALL reject registration of that manifest
Scenario: A manifest declares only semantic_fields (no lexical_fields)
- WHEN a manifest declares
query.search.semantic_fields: ["text"]on a stream and does NOT declarequery.search.lexical_fieldson that stream - THEN the reference's manifest validator SHALL accept the manifest
- AND the stream SHALL participate in semantic retrieval but not lexical retrieval
Scenario: A manifest declares lexical_fields and semantic_fields with different contents
- WHEN a manifest declares
query.search.lexical_fields: ["title", "subject"]andquery.search.semantic_fields: ["title", "body"]on a stream - THEN the reference's manifest validator SHALL accept the manifest
- AND lexical retrieval SHALL match only over
["title", "subject"]on that stream - AND semantic retrieval SHALL match only over
["title", "body"]on that stream
Requirement: The reference SHALL publish the capabilities.semantic_retrieval advertisement on its existing protected-resource metadata document with truthful experimental stability
When the reference exposes the semantic-retrieval extension, the existing RFC 9728 protected-resource metadata document the reference already serves SHALL include a capabilities.semantic_retrieval object carrying all required keys. The stability key SHALL be the literal string "experimental" in v1. The reference SHALL NOT introduce a new metadata document for this advertisement, and SHALL NOT publish the advertisement on the authorization-server metadata document. The reference SHALL NOT publish supported: true unless both an embedding backend and a vector index are configured and available.
Scenario: The advertisement is co-located with existing RS metadata
- WHEN a client retrieves the reference's protected-resource metadata document
- THEN the response SHALL include
capabilities.semantic_retrievalwith the required keys when the extension is exposed - AND the reference SHALL NOT serve the advertisement from a separately discoverable metadata document
Scenario: The advertisement carries the experimental stability marker
- WHEN the reference publishes
capabilities.semantic_retrieval.supported: true - THEN the same advertisement SHALL include
stability: "experimental" - AND the reference SHALL NOT publish
stability: "stable"on this extension in v1
Scenario: The advertisement declares text-only query input
- WHEN the reference publishes
capabilities.semantic_retrieval.supported: true - THEN the same advertisement SHALL include
query_input: "text"in v1 - AND SHALL NOT include
query_input: "vector"orquery_input: "hybrid"in v1
Scenario: The advertisement declares lexical_blending: false in this tranche
- WHEN the reference publishes
capabilities.semantic_retrieval.supported: truein this tranche - THEN the same advertisement SHALL include
lexical_blending: false - AND every result emitted on
GET /v1/search/semanticSHALL carryretrieval_mode: "semantic"
Scenario: The advertisement's model, dimensions, and distance_metric come from the configured backend
- WHEN the reference assembles the
capabilities.semantic_retrievalobject - THEN the
modelvalue SHALL be the server-declared model identifier returned by the configured embedding backend - AND the
dimensionsvalue SHALL be the integer dimension returned by the configured embedding backend - AND the
distance_metricvalue SHALL be one of"cosine","dot", or"l2"returned by the configured embedding backend - AND these values SHALL NOT be set from static configuration unrelated to the backend actually in use
Scenario: A reference instance with no embedding backend configured
- WHEN the reference is started without an embedding backend or without a vector index
- THEN the protected-resource metadata document SHALL either omit
capabilities.semantic_retrievalentirely or include it withsupported: false - AND the reference SHALL NOT register the
GET /v1/search/semanticroute - AND requests to
GET /v1/search/semanticSHALL return404ornot_found_error
Scenario: The advertisement is discoverable without a grant
- WHEN an unauthenticated client requests the reference's protected-resource metadata document
- THEN the
capabilities.semantic_retrievaladvertisement, if present, SHALL be returned without requiring a bearer token
Scenario: The advertisement is independent of the lexical retrieval advertisement
- WHEN the reference publishes protected-resource metadata
- THEN the presence or absence of
capabilities.semantic_retrievalSHALL be independent of the presence or absence ofcapabilities.lexical_retrieval - AND toggling one SHALL NOT toggle the other
Requirement: The reference's vector index SHALL embed and store only declared semantic_fields
The reference's local vector index SHALL contain entries only for (stream, record_key, field, connector_id) tuples where field appears in the corresponding stream's query.search.semantic_fields declaration. Records of streams that do not declare semantic_fields SHALL NOT contribute index rows. Non-declared fields of records of streams that do declare semantic_fields SHALL NOT be embedded and SHALL NOT contribute index rows.
Scenario: A non-participating stream has new records
- WHEN new records arrive for a stream whose manifest does not declare
query.search.semantic_fields - THEN the reference's vector index SHALL NOT receive new rows for that stream
- AND the embedding backend SHALL NOT be invoked for records of that stream
Scenario: A participating stream has new records
- WHEN new records arrive for a stream whose manifest declares
semantic_fields: ["a", "b"] - THEN the reference's vector index SHALL receive exactly two rows for each record (one per declared field)
- AND SHALL NOT receive rows for any other field of that record
Scenario: A stream loses its semantic_fields declaration
- WHEN a manifest update removes
query.search.semantic_fieldsfrom a stream - THEN the reference SHALL remove all vector-index rows for that stream
- AND the stream SHALL contribute zero hits on subsequent semantic searches
Requirement: The reference's vector index SHALL include connector identity on every row
Because the reference's owner reads are per-connector, the vector index SHALL include the originating connector_id on every indexed row so that owner-mode hits can be attributed to a connector for hydration. Insert/update/delete maintenance for a record SHALL include that record's connector_id. Reference semantic search results SHALL carry the indexed connector_id through to the search_result.connector_id field of the public response.
Scenario: Records for two connectors are indexed
- WHEN records arrive for stream
messagesfrom connectorsC1andC2, both of which declaresemantic_fields: ["text"] - THEN the reference's vector index SHALL contain rows attributed to
C1forC1's records and rows attributed toC2forC2's records - AND SHALL NOT silently merge rows under a single shared connector identity
Scenario: A search result is attributed to its originating connector
- WHEN the reference returns a
search_resultto a caller - THEN that result's
connector_idSHALL be theconnector_idrecorded on the matching index row at insert time - AND the reference SHALL NOT fabricate
connector_idfrom configuration or from the caller's identity
Requirement: The reference SHALL report index_state honestly and rebuild on drift
The reference SHALL persist per-(connector_id, stream) metadata describing the declared semantic_fields fingerprint and the backend's model_id, dimensions, and distance_metric at insert time. The reference SHALL detect drift on startup and on every connector registration/update, and SHALL report index_state in the capability advertisement honestly.
Scenario: semantic_fields fingerprint changes
- WHEN a manifest update changes the declared
semantic_fieldsset for a(connector_id, stream)tuple in a way that changes the sorted JSON fingerprint - THEN the reference SHALL report
index_state: "stale"in the advertisement until a rebuild for that(connector_id, stream)restores coverage - AND the reference SHALL rebuild the index for the affected
(connector_id, stream)and remove stale rows - AND the rebuild SHALL be maintained in JavaScript at the record write/update/delete call sites, not by SQLite triggers
Scenario: The configured embedding backend's model_id changes
- WHEN the configured embedding backend's
model_iddisagrees with themodel_idpersisted insemantic_search_metafor any row - THEN the reference SHALL report
index_state: "stale"in the advertisement until a rebuild restores coverage
Scenario: The configured embedding backend's dimensions or distance_metric changes
- WHEN the configured embedding backend's
dimensionsordistance_metricdisagrees with persisted metadata - THEN the reference SHALL report
index_state: "stale"in the advertisement until a rebuild restores coverage
Scenario: The index is actively rebuilding
- WHEN the reference is rebuilding the vector index for any reason
- THEN the reference SHALL report
index_state: "building"in the advertisement until rebuild completes
Scenario: Steady state
- WHEN no drift signal is active and no rebuild is in progress
- THEN the reference SHALL report
index_state: "built"in the advertisement
Requirement: The reference's default semantic index SHALL persist across process restarts
The reference's default vector index SHALL store embeddings persistently in the same SQLite database used by the rest of the reference, so that semantic coverage survives process restart. The reference SHALL prefer sqlite-vec as the default persistent backend when its SQLite extension can be loaded, and SHALL fall back to a persistent SQLite-BLOB flat backend (same database, BLOB-columned table, distance computed in JavaScript) when sqlite-vec cannot be loaded. Both backends SHALL implement the same VectorIndex interface. Neither backend SHALL require ephemeral in-process state for capabilities.semantic_retrieval.supported: true.
Scenario: sqlite-vec loads successfully at init
- WHEN the reference opens its
better-sqlite3database at startup andsqliteVec.load(db)succeeds - THEN the reference SHALL use the
sqlite-vec-backedVectorIndeximplementation (avec0virtual table in the same database) - AND the reference SHALL log a startup line identifying the chosen backend as
sqlite-vec - AND subsequent
upsert,delete, andquerycalls SHALL operate against thevec0virtual table
Scenario: sqlite-vec fails to load at init
- WHEN the reference opens its
better-sqlite3database at startup andsqliteVec.load(db)throws (platform has no published binary, the environment forbids loading SQLite extensions, or any other load error) - THEN the reference SHALL NOT crash at startup
- AND the reference SHALL log a warning identifying
sqlite-vecas unavailable and the fallback backend as active - AND the reference SHALL use the persistent SQLite-BLOB flat
VectorIndeximplementation (rows in a standard SQLite table, distance computed in JavaScript) - AND the BLOB-flat backend SHALL expose the same interface and the same persistence semantics as the
sqlite-vecbackend
Scenario: Vectors persist across process restart (sqlite-vec path)
- WHEN the reference ingests records for a participating
(connector_id, stream)with declaredsemantic_fields, then the process is stopped and a fresh process is started against the samePDPP_DB_PATH - THEN the advertisement SHALL report
capabilities.semantic_retrieval.supported: truewithindex_state: "built"immediately, without running a rebuild - AND
GET /v1/search/semanticSHALL return hits for previously-ingested records - AND the reference SHALL NOT require re-ingest from the connector to make those records searchable again
Scenario: Vectors persist across process restart (BLOB-flat path)
- WHEN the reference is forced onto the BLOB-flat fallback and the same stop/start sequence as above is performed
- THEN the same end-to-end behavior SHALL hold:
index_state: "built", hits return, no re-ingest
Scenario: supported: true does not depend on ephemeral in-process state
- WHEN the reference advertises
capabilities.semantic_retrieval.supported: true - THEN the advertisement SHALL be backed by a persistent store on disk
- AND a clean restart SHALL NOT cause
supported: trueto becomesupported: falseabsent some other failure
Requirement: The reference SHALL backfill the semantic index from records on startup without requiring re-ingest
Records are the source of truth for semantic retrieval in the reference. The reference SHALL provide a startup backfill path that detects drift per (connector_id, stream) and rebuilds the vector index from records already stored in the better-sqlite3 database. The backfill SHALL NOT call back into any connector and SHALL NOT require re-ingest of raw data.
Scenario: Startup with no drift
- WHEN the reference starts and the persisted
semantic_search_metafingerprint,model_id,dimensions, anddistance_metricall match the currently configured backend, and the row-count band check is satisfied - THEN the reference SHALL advertise
index_state: "built"immediately - AND the reference SHALL NOT run a rebuild
Scenario: Startup after a drift signal
- WHEN the reference starts and any drift signal (fingerprint change, backend identity change, or row-count band divergence) is active
- THEN the reference SHALL advertise
index_state: "stale"initially andindex_state: "building"while the rebuild runs, and SHALL advertiseindex_state: "built"once the rebuild completes - AND the rebuild SHALL read records from the records table and re-embed their declared
semantic_fieldsusing the currently configured backend - AND the rebuild SHALL NOT call back into the originating connector, re-ingest raw data, or require any network traffic beyond calls to the configured embedding backend for re-embedding
Scenario: Historical records become searchable again after restart
- WHEN the reference is restarted on a database that already contains records for a participating stream
- THEN those historical records SHALL be searchable via
GET /v1/search/semanticeither immediately (no-drift case) or after the startup backfill completes (drift case) - AND the reference SHALL NOT require a connector re-sync to make historical records searchable
Requirement: The reference SHALL NOT substitute a non-semantic fallback behind GET /v1/search/semantic
The reference SHALL NOT produce results on GET /v1/search/semantic by invoking lexical retrieval (or any other non-semantic matching path) while emitting retrieval_mode: "semantic" or retrieval_mode: "hybrid" on those results. When the vector index reports index_state: "building" or "stale", or when the embedding backend is otherwise unable to produce honest semantic results, the reference SHALL return zero or partial results rather than substituting a non-semantic fallback. The module reference-implementation/server/search-semantic.js SHALL NOT import the lexical retrieval helper.
Scenario: The vector index is stale
- WHEN
vectorIndex.state()returns"stale" - THEN
GET /v1/search/semanticSHALL return zero or partial results - AND SHALL NOT invoke the lexical retrieval helper
- AND any results returned SHALL still carry
retrieval_mode: "semantic"(because the reference returns honest semantic results, just fewer of them)
Scenario: The vector index is building
- WHEN
vectorIndex.state()returns"building" - THEN
GET /v1/search/semanticSHALL return zero or partial results - AND SHALL NOT invoke the lexical retrieval helper
Scenario: The no-fallback invariant is visible in source
- WHEN a reader inspects
reference-implementation/server/search-semantic.js - THEN the file SHALL NOT import from
reference-implementation/server/search.js(the lexical helper) - AND the no-fallback invariant SHALL be verifiable by a static grep
Requirement: The reference SHALL realize owner-token semantic retrieval through cross-connector fan-out
The reference scopes owner reads of records and stream metadata per connector. The reference SHALL realize owner-token semantic retrieval by fanning out across every owner-visible connector internally and merging results, so that the public GET /v1/search/semantic request shape stays identical for owner-token and client-token callers (no public connector_id query parameter). Each search_result returned to an owner-token caller SHALL carry the originating connector via connector_id so the caller can hydrate the record under the correct per-connector owner read scope. The reference SHALL emit a record_url that includes the canonical owner-mode connector_id query parameter for owner-token callers.
Scenario: An owner searches across two connectors that both expose the same stream name
- WHEN an owner-token caller invokes
GET /v1/search/semantic?q=alphaon a reference instance with two owner-visible connectorsC1andC2, both of which expose amessagesstream that declaressemantic_fields: ["text"]and both of which contain a matching record - THEN the response SHALL include hits from BOTH connectors
- AND each hit SHALL carry its originating
connector_id("C1"for hits fromC1,"C2"for hits fromC2) - AND the response SHALL NOT silently scope to a single connector
Scenario: An owner request includes connector_id
- WHEN an owner-token caller invokes
GET /v1/search/semantic?q=alpha&connector_id=C1 - THEN the reference SHALL reject the request with
invalid_request_erroridentifyingconnector_idas the rejected parameter - AND SHALL NOT silently use
connector_idto scope the search
Scenario: An owner-mode record_url is hydrated
- WHEN an owner-token caller takes the
record_urlfrom a/v1/search/semantichit and issues a GET against it under the same owner token - THEN the reference SHALL return the canonical record envelope at
GET /v1/streams/{stream}/records/{record_key}for the connector identified by the URL'sconnector_idquery parameter
Requirement: The reference SHALL produce grant-safe verbatim snippets, never model-generated text
When the reference includes a snippet on a search_result, the snippet's text SHALL be a verbatim contiguous substring of the matched field's stored value for the hit record. The reference SHALL NOT produce snippets by summarizing, paraphrasing, translating, or otherwise synthesizing text via the embedding backend or any other model. If a verbatim excerpt cannot be produced for a hit, the reference SHALL omit the snippet from that result rather than fabricate one.
Scenario: A snippet is a verbatim substring
- WHEN the reference emits a
snippeton a result for a record whose storedtextfield is a given stringS - THEN the snippet's
textSHALL be a contiguous substring ofS - AND the snippet's
textSHALL NOT be a paraphrase, summary, translation, or synthesized variant of any portion ofS
Scenario: Snippets drawn from ungranted or undeclared fields are omitted
- WHEN a candidate snippet's source field is outside the caller's grant projection OR outside the stream's declared
semantic_fields - THEN the reference SHALL omit the snippet from that result
- AND SHALL NOT substitute a snippet derived from that field by any means
Requirement: The reference SHALL treat embedding and vector-index backends as pluggable implementation details behind a fixed internal interface
The reference SHALL expose pluggable interfaces for the embedding backend and vector index inside reference-implementation/server/search-semantic.js. The reference's default embedding backend SHALL be a deterministic local stub that runs without external network access and identifies itself honestly in the advertisement's model field. The reference's default vector index SHALL be persistent across process restarts (see the separate "The reference's default semantic index SHALL persist across process restarts" requirement). Hosted embedding providers and alternate persistent vector backends SHALL be supportable as drop-in replacements without any change to the public contract, the spec delta, or the handler shape.
Scenario: The reference runs offline without a configured hosted provider
- WHEN the reference is started with the default stub embedding backend and the default persistent vector index
- THEN the reference SHALL advertise
capabilities.semantic_retrieval.supported: truewith a truthfulmodelidentifier that names itself as the reference stub - AND the advertised
modelSHALL NOT impersonate the model identifier of a hosted provider - AND the reference SHALL NOT require network access beyond the local
better-sqlite3database to serveGET /v1/search/semantic
Scenario: A hosted provider is configured
- WHEN an operator configures a hosted embedding backend that implements the
EmbeddingBackendinterface - THEN the reference SHALL advertise that backend's
model,dimensions, anddistance_metricincapabilities.semantic_retrieval - AND the reference SHALL NOT require a change to the handler, the spec delta, or any other public contract
Scenario: The reference SHALL NOT bake hosted-provider credentials into source
- WHEN a reader inspects the reference source for the embedding backend
- THEN no hosted-provider API key, endpoint, or secret SHALL be code-resident
- AND any hosted-provider configuration SHALL come from operator-supplied runtime configuration
Requirement: The reference SHALL mark GET /v1/search/semantic as experimental in source
The reference's source for the public semantic retrieval route SHALL include an inline comment band that identifies the surface as experimental and unstable, and SHALL cross-reference the advertisement's stability key and the public docs page. This makes the experimental status visible to any reader of the code, not just the advertisement.
Scenario: A reader inspects the semantic retrieval route source
- WHEN a reader reads the source for
app.get('/v1/search/semantic', …)inreference-implementation/server/index.js - THEN an inline comment SHALL identify the route as experimental and unstable
- AND the comment SHALL cross-reference
capabilities.semantic_retrieval.stabilityand the public docs page
Requirement: The reference SHALL keep GET /v1/search/semantic distinct from GET /v1/search and from reference-only surfaces
The reference SHALL NOT alias GET /v1/search/semantic to GET /v1/search, SHALL NOT serve the lexical retrieval contract from GET /v1/search/semantic, and SHALL NOT serve the semantic retrieval contract from GET /v1/search or from any reference-only surface such as /_ref/search. The three surfaces SHALL remain independent.
Scenario: A client requests /v1/search
- WHEN a client calls
/v1/search?q=... - THEN the response SHALL be the lexical retrieval contract defined by the
lexical-retrievalextension - AND the response SHALL NOT include
retrieval_mode(which is a semantic-retrieval-specific field)
Scenario: A client requests /v1/search/semantic
- WHEN a client calls
/v1/search/semantic?q=... - THEN the response SHALL be the semantic retrieval contract defined by the
semantic-retrievalextension - AND every result SHALL carry
retrieval_mode: "semantic"(or, if a future tranche enables hybrid blending,"hybrid")
Scenario: A client requests /_ref/search
- WHEN a client calls
/_ref/search?q=... - THEN the response SHALL be the existing reference-only spine artifact-and-id-jump shape
- AND the response SHALL NOT match the public
search_resultlist envelope returned by either/v1/searchor/v1/search/semantic
Requirement: Public aggregations SHALL be single-stream and grant-safe
The reference implementation SHALL expose public aggregation only for one stream at a time. Aggregation input fields, grouping fields, and filters SHALL be authorized under the caller's grant or owner scope before evaluation.
Scenario: Client counts granted records
- WHEN a client token authorized for
<stream>requests a count aggregation for<stream> - THEN the response SHALL count only records visible under that grant
- AND fields outside the grant SHALL NOT influence the result
Scenario: Cross-stream aggregation is requested
- WHEN a client requests an aggregation across multiple streams
- THEN the reference SHALL reject the request unless a later accepted change defines cross-stream semantics
Requirement: Public aggregations SHALL be manifest-declared
The reference implementation SHALL evaluate only aggregation operations and fields declared by the stream manifest. Undeclared fields, non-scalar fields, arrays, objects, blobs, and high-cardinality fields that are not explicitly declared SHALL be rejected.
Scenario: Declared numeric sum is accepted
- WHEN a stream declares a numeric field as summable
- AND the caller is authorized for that field
- THEN the client MAY request a sum aggregation over that field
Scenario: Undeclared field is rejected
- WHEN a client requests an aggregation over a field absent from the stream's aggregation declaration
- THEN the reference SHALL reject the request with a clear query error
Requirement: Public aggregations SHALL reuse record-list filter semantics
Aggregation requests SHALL use the same exact and declared range filter validation as record-list requests. Unsupported, unauthorized, or malformed filters SHALL fail with the same error class as record-list filtering.
Scenario: Date-windowed aggregation
- WHEN a client requests an aggregation with
filter[date][gte]=... - AND the field and operator are declared under
query.range_filters - THEN the aggregation SHALL apply the same coercion and comparison semantics as record-list filtering
Requirement: Grouped aggregation results SHALL be bounded and deterministic
Grouped aggregation responses SHALL enforce a maximum bucket limit and deterministic ordering. If the request exceeds the allowed limit or requests grouping by an unsupported field, the reference SHALL reject it.
Scenario: Grouped count with limit
- WHEN a client requests
group_by=<field>&limit=N - AND
<field>is declared groupable - THEN the response SHALL contain at most
Ngroup buckets - AND the ordering SHALL be documented and deterministic
Requirement: The reference SHALL hydrate Gmail attachments as content-addressed blobs
When the Gmail attachments stream is requested, the reference Gmail connector SHALL fetch each attachment's MIME part bytes from IMAP, compute a SHA-256 content hash over the exact bytes to be served, upload the bytes through the reference blob upload surface, and emit the attachment record with a visible blob_ref that resolves through GET /v1/blobs/{blob_id}. Successful hydrated attachment records SHALL include content_sha256 matching the blob hash, byte size, MIME type, and hydration_status: "hydrated".
The connector SHALL NOT inline attachment bytes into the attachment record or the message_bodies stream. Attachment primary keys SHALL remain stable across hydration backfills.
Scenario: A requested Gmail attachment is hydrated
- WHEN the Gmail connector processes a message with an attachment and the
attachmentsstream is requested - THEN it SHALL download the attachment MIME part bytes
- AND it SHALL compute
content_sha256over those bytes - AND it SHALL upload the bytes as a content-addressed blob
- AND it SHALL emit an
attachmentsrecord whose visibleblob_ref.blob_idresolves to those bytes
Scenario: A Gmail attachment cannot be hydrated
- WHEN the Gmail connector can emit attachment metadata but cannot download or upload the attachment bytes for a bounded per-attachment reason
- THEN it MAY emit the attachment metadata with
hydration_statusset to"failed"or"deferred" - AND it SHALL NOT emit a fake
blob_id, fakecontent_sha256, or fetchableblob_ref - AND it SHALL continue processing other attachments and messages when doing so is safe
Scenario: Message bodies are queried separately
- WHEN a caller requests Gmail
message_bodies - THEN the response SHALL expose email body text/HTML according to the
message_bodiesstream contract - AND it SHALL NOT include Gmail attachment bytes
- AND attachment byte retrieval SHALL require the caller to read the relevant
attachmentsrecord and its visibleblob_ref
Requirement: The reference SHALL expose connector-facing blob upload without weakening blob fetch authorization
The reference SHALL provide a connector-facing blob upload path that allows authorized connector/runtime code to upload bytes for a specific connector_id, stream, and record_key. The upload path SHALL return the canonical blob_id, sha256, size_bytes, and mime_type that records can expose through blob_ref. Uploading the same bytes for the same record binding SHALL be idempotent.
The reference SHALL continue to authorize GET /v1/blobs/{blob_id} by resolving the blob's bound record and requiring that record to be visible under the caller's grant with a matching visible data.blob_ref.blob_id. A caller SHALL NOT gain blob access by guessing a blob_id, by reading attachment metadata without blob_ref, or by holding access to a different record that does not reference the blob.
Scenario: A connector uploads the same attachment twice
- WHEN connector/runtime code uploads identical attachment bytes for the same Gmail attachment record more than once
- THEN the reference SHALL return the same canonical blob identity
- AND it SHALL NOT create duplicate logical blobs for that record binding
Scenario: A caller can see the attachment blob reference
- WHEN a caller is authorized to read a Gmail
attachmentsrecord including itsblob_reffield - AND that
blob_ref.blob_idpoints at an uploaded blob - THEN record-list and record-detail responses SHALL decorate the visible
blob_refwith a fetch URL for/v1/blobs/{blob_id} - AND
GET /v1/blobs/{blob_id}SHALL return the blob bytes with truthful content metadata
Scenario: A caller cannot see the attachment blob reference
- WHEN a caller is authorized to read Gmail attachment metadata but is not authorized to read the
blob_reffield - THEN the caller SHALL NOT receive a blob fetch URL in record-list, record-detail, or expanded-record responses
- AND
GET /v1/blobs/{blob_id}for that blob SHALL fail asblob_not_found
Requirement: The reference SHALL backfill Gmail attachment blob linkage idempotently
The Gmail connector SHALL treat metadata ingestion and byte hydration as separate completion facts. A message or attachment that has already been seen in an incremental run SHALL still be eligible for hydration if its attachment record lacks a hydrated blob_ref. Backfill runs SHALL re-emit the same attachment primary key with blob linkage once bytes are available.
Scenario: Existing metadata-only attachments are backfilled
- WHEN the reference contains Gmail
attachmentsrecords emitted before blob hydration existed - AND a later Gmail connector run can download and upload the attachment bytes
- THEN the connector SHALL emit updated records with the same primary keys
- AND those records SHALL gain hydrated
blob_refandcontent_sha256fields
Scenario: Already-hydrated attachments are seen again
- WHEN an incremental Gmail run encounters an attachment whose bytes were already uploaded
- THEN the connector SHALL preserve the attachment primary key
- AND the blob upload/read path SHALL behave idempotently
- AND the run SHALL NOT create duplicate attachment records or duplicate logical blob identities
Requirement: Stream metadata SHALL expose normalized field-level query capabilities
The reference implementation SHALL expose a field_capabilities object on stream metadata. Each entry SHALL be keyed by a top-level schema field name and SHALL describe the field schema, grant usability, exact-filter support, range-filter operators, lexical-search participation, and semantic-search participation derived from the stream manifest and active bearer context.
Scenario: Owner discovers queryable fields
- WHEN an owner token requests
GET /v1/streams/<stream> - THEN the response SHALL include
field_capabilities - AND fields declared under
query.range_filtersSHALL list their supported range operators - AND fields declared under
query.search.lexical_fieldsorquery.search.semantic_fieldsSHALL identify their retrieval participation
Scenario: Client grant limits usable fields
- WHEN a client token requests
GET /v1/streams/<stream> - AND the stream manifest declares a query capability on a field outside the client's grant projection
- THEN the field capability entry SHALL NOT mark that capability as usable under the current token
- AND the response SHALL preserve enough reason information for the client to avoid issuing a doomed query
Requirement: Stream metadata SHALL expose normalized expansion capabilities
The reference implementation SHALL expose an expand_capabilities list on stream metadata derived from query.expand[] and matching relationships[]. Each expansion entry SHALL include relation name, related stream, cardinality, and declared limit metadata when present.
Scenario: Expandable relation is discoverable
- WHEN a stream declares a relation in both
relationships[]andquery.expand[] - THEN stream metadata SHALL include that relation in
expand_capabilities - AND the entry SHALL identify the related stream and whether the relation is
has_oneorhas_many
Scenario: Descriptive relationship is not public expansion
- WHEN a stream has a
relationships[]entry that is absent fromquery.expand[] - THEN the relation MAY remain visible as descriptive metadata
- AND it SHALL NOT be listed as an enabled expansion capability
Requirement: Public record expansion SHALL be declaration-gated and one-hop
The reference implementation SHALL expose expand[] only for relations that the parent stream declares in both relationships[] and query.expand[]. Expansion SHALL support only one relation hop in this change. Unknown relation names, undeclared relation names, nested relation paths, malformed expand values, and expand_limit entries without a matching requested relation SHALL fail with invalid_expand.
Scenario: Declared relation is accepted
- WHEN a client queries
GET /v1/streams/<parent>/records?expand=<relation>and<parent>declares<relation>in bothrelationships[]andquery.expand[] - THEN the reference SHALL attempt to hydrate
<relation>using the declared related stream and foreign key
Scenario: Unknown or undeclared relation is rejected
- WHEN a client queries
GET /v1/streams/<parent>/records?expand=<relation>and<relation>is absent from eitherrelationships[]orquery.expand[]on<parent> - THEN the reference SHALL reject the request with
invalid_expand
Scenario: Nested expansion is rejected
- WHEN a client queries
GET /v1/streams/<parent>/records?expand=child.grandchild - THEN the reference SHALL reject the request with
invalid_expand
Requirement: Public record expansion SHALL be grant-safe
The reference implementation SHALL authorize and project expanded records using the related stream's grant entry. If the caller can read the parent stream but lacks grant access to the related stream, the request SHALL fail with insufficient_scope. Expanded child records SHALL expose only fields visible under the child stream grant.
Scenario: Related stream is outside the grant
- WHEN a client queries a granted parent stream with
expand=<relation> - AND
<relation>points to a related stream that is not present in the caller's grant - THEN the reference SHALL reject the request with
insufficient_scope
Scenario: Child projection is narrower than child schema
- WHEN a client queries a granted parent stream with
expand=<relation> - AND the caller's grant for the related stream includes only a subset of child fields
- THEN each expanded child record SHALL include only the granted child fields plus the record envelope fields required by the record response shape
Requirement: Public record expansion SHALL have list and detail parity
The reference implementation SHALL apply the same declared expansion semantics to record-list and record-detail reads. A relation that is expandable on GET /v1/streams/<stream>/records SHALL also be expandable on GET /v1/streams/<stream>/records/<id> with the same grant, projection, missing-child, and limit behavior.
Scenario: List read expands a declared relation
- WHEN a client queries
GET /v1/streams/<stream>/records?expand=<relation> - THEN each returned parent record SHALL include the expanded relation under
expanded.<relation>when the request is otherwise valid
Scenario: Detail read expands a declared relation
- WHEN a client queries
GET /v1/streams/<stream>/records/<id>?expand=<relation> - THEN the returned parent record SHALL include the expanded relation under
expanded.<relation>when the request is otherwise valid
Requirement: Public record expansion SHALL bound has-many children with expand_limit
For a has_many relation, the reference implementation SHALL apply the relation's declared default_limit when the caller omits expand_limit[<relation>], SHALL reject non-positive or over-maximum limits with invalid_expand, and SHALL return a list object containing data and has_more. expand_limit SHALL NOT apply to non-has_many relations.
Scenario: Default limit applies
- WHEN a client expands a
has_manyrelation withoutexpand_limit[<relation>] - THEN the reference SHALL use the relation's declared
default_limit
Scenario: Caller requests a valid lower limit
- WHEN a client expands a
has_manyrelation withexpand_limit[<relation>]=N - AND
Nis positive and does not exceed the relation's declaredmax_limit - THEN the expanded relation SHALL contain at most
Nchild records - AND
has_moreSHALL indicate whether additional matching child records exist beyondN
Scenario: Caller requests an invalid limit
- WHEN a client expands a relation with a non-positive limit, an over-maximum limit, or a limit on a non-
has_manyrelation - THEN the reference SHALL reject the request with
invalid_expand
Requirement: Public record expansion SHALL represent missing children without failing
The reference implementation SHALL treat missing related records as data absence, not as a query error. For has_one relations, a parent with no matching child SHALL expose expanded.<relation> as null. For has_many relations, a parent with no matching children SHALL expose an empty list object with has_more: false.
Scenario: Missing has-one child
- WHEN a parent record is returned for a valid
has_oneexpansion - AND no related child record matches the parent key
- THEN the parent record SHALL include
expanded.<relation>: null
Scenario: Missing has-many children
- WHEN a parent record is returned for a valid
has_manyexpansion - AND no related child records match the parent key
- THEN the parent record SHALL include
expanded.<relation>as a list object with an emptydataarray andhas_more: false
Requirement: Manifest validation SHALL reject unsafe query.expand declarations
The reference implementation SHALL reject or fail validation for manifests that declare query.expand[] entries that cannot be safely served by the reference expansion engine. Each enabled expansion SHALL match a relationships[] entry on the same parent stream, reference an existing child stream, use a top-level child schema property as the declared foreign_key, and declare positive integer limits with default_limit <= max_limit when limits are present.
Scenario: query.expand does not match a relationship
- WHEN a manifest stream declares
query.expand: [{ "name": "attachments" }] - AND the same stream has no
relationships[]entry namedattachments - THEN manifest validation SHALL fail
Scenario: Foreign key is absent from the child stream
- WHEN a manifest stream enables expansion for a relationship whose declared related stream lacks the relationship's
foreign_keyin its top-level schema properties - THEN manifest validation SHALL fail
Scenario: Expansion limits are invalid
- WHEN a manifest stream enables expansion with a non-positive
default_limit, a non-positivemax_limit, or adefault_limitgreater thanmax_limit - THEN manifest validation SHALL fail
Requirement: Gmail parent-child expansions SHALL cover message body and attachment metadata
The first-party Gmail manifest SHALL enable safe parent-to-child expansion from messages to message_bodies and from messages to attachments when the related streams are granted. Gmail attachment expansion under this change SHALL expose attachment metadata records only and SHALL NOT imply attachment byte hydration, blob_ref availability, extracted text, or blob fetch authorization.
Scenario: Message expands body content when granted
- WHEN a client with grants for Gmail
messagesandmessage_bodiesqueriesGET /v1/streams/messages/records?expand=message_bodies - THEN each returned message record SHALL include its granted message body record under
expanded.message_bodieswhen present - AND the expanded body record SHALL be projected according to the
message_bodiesgrant
Scenario: Message expands attachment metadata when granted
- WHEN a client with grants for Gmail
messagesandattachmentsqueriesGET /v1/streams/messages/records?expand=attachments - THEN each returned message record SHALL include granted attachment metadata records under
expanded.attachments - AND the response SHALL NOT include attachment bytes unless a separate blob-hydration change later defines and grants them
Scenario: Message-to-thread reverse expansion remains out of scope
- WHEN a client queries Gmail
messageswithexpand=thread - THEN the reference SHALL reject the request with
invalid_expandunless a later accepted change defines reverse or belongs-to expansion semantics
Scenario: Thread expands messages in the safe direction
- WHEN the Gmail manifest declares a parent-to-child
threadsrelation tomessagesusingmessages.thread_idas the child foreign key - AND a client with grants for Gmail
threadsandmessagesqueriesGET /v1/streams/threads/records?expand=messages - THEN each returned thread record SHALL include granted message records under
expanded.messages
Requirement: Reference semantic retrieval readiness SHALL distinguish backend readiness from corpus participation
The reference implementation SHALL treat semantic backend/index readiness and semantic corpus participation as separate operational facts. A ready embedding backend and built vector index SHALL NOT by themselves imply that the first-party corpus has any searchable semantic coverage.
Scenario: Backend is ready but no stream participates
- WHEN the reference has an available semantic embedding backend and a built vector index
- AND zero loaded first-party streams declare usable
query.search.semantic_fields - THEN reference diagnostics SHALL report zero semantic participation explicitly
- AND the dashboard SHALL surface that as a warning rather than presenting semantic retrieval as a useful corpus feature
Scenario: Streams participate
- WHEN loaded manifests declare usable semantic fields
- THEN reference diagnostics SHALL report participating connectors, streams, and fields
- AND the reported participation SHALL be derived from loaded manifests and validator-accepted top-level string fields
Requirement: First-party polyfill manifests SHALL provide honest semantic field coverage where natural-language fields exist
The reference implementation SHALL declare query.search.semantic_fields in first-party polyfill manifests for top-level string fields that are suitable for semantic retrieval. The declaration SHALL remain independent from lexical fields and SHALL NOT include nested paths, arrays, blobs, non-string scalars, or fields absent from the stream schema.
Scenario: A natural-language top-level string field exists
- WHEN a first-party polyfill stream contains a top-level string field whose value is natural-language record content
- THEN the implementation SHALL either declare that field in
query.search.semantic_fieldsor document why the field is intentionally excluded
Scenario: A field is not safe for semantic embedding
- WHEN a stream field is nested, array-shaped, blob-backed, non-string, identifier-like, or otherwise unsuitable for semantic matching
- THEN the implementation SHALL NOT declare that field in
query.search.semantic_fields
Requirement: Reference semantic retrieval SHALL offer an operational local embedding backend and a deterministic test backend
The reference implementation SHALL support a production-like local embedding backend for operational semantic retrieval while preserving the deterministic stub backend for tests, CI, and exact-match contract checks. The operational backend SHALL require no hosted API key by default.
Scenario: Operational semantic retrieval is enabled
- WHEN the reference is configured to use the operational local embedding backend
- THEN the semantic capability metadata and deployment diagnostics SHALL identify the configured model, dimensions, distance metric, and language bias
- AND semantic index drift SHALL be detected when any of those backend identity fields change
Scenario: Tests use the deterministic stub
- WHEN tests or CI configure the deterministic stub backend
- THEN the reference SHALL preserve deterministic exact-match behavior for stable assertions
- AND tests SHALL NOT rely on paraphrase, synonym, multilingual, or conceptual-similarity behavior from the stub
Requirement: Reference semantic retrieval SHALL support operator-configured multilingual embedding profiles
The reference implementation SHALL allow an operator to configure one active semantic embedding profile, including a documented multilingual profile suitable for Italian-language data. The public semantic retrieval API SHALL remain server-configured and SHALL NOT expose caller-selected model parameters.
Scenario: Operator configures a multilingual profile
- WHEN an operator configures a multilingual embedding profile
- THEN semantic capability metadata and deployment diagnostics SHALL identify the active profile and its language bias
- AND existing semantic index coverage SHALL be marked stale until rebuilt with that profile
Scenario: Caller requests a model directly
- WHEN a caller passes a model selector to
GET /v1/search/semantic - THEN the public endpoint SHALL continue rejecting the request according to the semantic retrieval contract
- AND the configured model SHALL remain an operator/server decision
Scenario: Multiple simultaneous profiles are desired
- WHEN an operator wants concurrent indexes for multiple embedding profiles
- THEN this reference change SHALL NOT claim support for query-time model fan-out
- AND that requirement SHALL be handled by a future OpenSpec change because it affects index identity, cursor validity, and ranking/merge semantics
Requirement: Reference deployment diagnostics SHALL expose semantic retrieval health without leaking secrets
The reference dashboard SHALL provide a read-only deployment diagnostics surface that makes semantic retrieval readiness inspectable by an operator. The diagnostics SHALL include semantic backend status, vector index status, active semantic backfill progress when present, model/profile identity, language bias, participating semantic fields, manifest provenance, database/index topology, and relevant environment configuration with secret values redacted.
Scenario: Operator opens deployment diagnostics
- WHEN an operator opens the deployment diagnostics page
- THEN the page SHALL show whether semantic retrieval is enabled, which backend/index are active, the current index state, and which connectors/streams/fields participate
- AND the page SHALL show warnings for zero participation, stale index, unavailable backend, missing model cache, disabled model download, and vector-index fallback when applicable
Scenario: Semantic backfill is active
- WHEN the reference is rebuilding the semantic index in the background
- THEN deployment diagnostics SHALL report the active connector and stream when known
- AND the dashboard SHALL show bounded progress such as records scanned, total records for the current stream when known, indexed vectors, stream-check counts, and last update time
Scenario: Diagnostics include environment configuration
- WHEN diagnostics display environment-derived configuration
- THEN secret values SHALL be redacted
- AND the page SHALL distinguish present, absent, defaulted, and redacted values where that provenance is known
Requirement: Existing first-party local databases SHALL reconcile semantic coverage changes
The reference implementation SHALL reconcile first-party manifest semantic-field changes into existing local polyfill databases and SHALL rebuild semantic index coverage from stored records without requiring connector re-ingest.
Scenario: A first-party manifest gains semantic fields
- WHEN an existing local database starts with a first-party manifest that now declares additional
semantic_fields - THEN the reference SHALL update the persisted first-party manifest according to the existing reconcile rules
- AND semantic backfill SHALL index existing stored records for the new declared fields
Scenario: The embedding profile changes
- WHEN the configured embedding profile changes for an existing local database
- THEN semantic index metadata SHALL mark affected coverage stale
- AND rebuild SHALL derive replacement embeddings from stored records rather than from connector re-ingest
Scenario: Semantic backfill is interrupted
- WHEN a semantic stream rebuild is interrupted after persisting some record-field vectors but before completion metadata is written
- AND the next rebuild sees matching semantic fields and backend storage identity
- THEN the reference SHALL resume without deleting matching partial vectors
- AND the rebuild SHALL embed only missing record-field pairs before writing completed index metadata
- AND incomplete progress without an active backfill SHALL NOT advertise the semantic index as built
Requirement: The public query surface SHALL expose a minimal connector discovery floor
The reference Resource Server SHALL expose GET /v1/connectors as a bearer-authenticated public query endpoint for discovering connector or source boundaries visible under the caller's token. The endpoint SHALL return a list envelope whose items identify visible connector-backed sources by connector_id and include stream summaries plus coarse capability hints. The endpoint SHALL NOT inline full stream schemas; callers SHALL use GET /v1/streams/{stream} for full source-level stream metadata.
Scenario: Owner discovers polyfill connectors
- WHEN an owner-token caller in polyfill mode requests
GET /v1/connectors - THEN the response SHALL include connector-backed sources visible to that owner token without requiring a
connector_idquery parameter - AND each connector-backed item SHALL include its
connector_id - AND declared streams with no stored records SHALL remain discoverable with zero record count and unknown freshness
Scenario: Client discovers its granted connector
- WHEN a client-token caller requests
GET /v1/connectors - THEN the response SHALL include only the source bound to that active grant
- AND the response SHALL include only grant-authorized stream names for that source
- AND the response SHALL NOT expose unrelated registered connectors or streams outside the grant
Scenario: Discovery does not leak grant internals
- WHEN a client-token caller's grant narrows fields, resources, or time range
- THEN
GET /v1/connectorsSHALL NOT expose the grant's field list, resource list, time range, client claims, or grant identifier in the response body - AND record counts and freshness SHALL remain computed under existing grant enforcement rules
Scenario: Discovery points to existing metadata authority
- WHEN a caller needs a stream schema, primary key, cursor field, relationships, views, or field-level query declarations
- THEN
GET /v1/connectorsSHALL provide enough stream identity and capability hints for the caller to request existing per-stream metadata - AND the full metadata authority SHALL remain
GET /v1/streams/{stream}rather than the connector discovery response
Requirement: The reference record-list query SHALL expose an initial changes bookmark sentinel
The reference implementation SHALL accept changes_since=beginning on GET /v1/streams/{stream}/records as a public initial changes bookmark sentinel. The sentinel SHALL behave like an opaque changes cursor positioned at the beginning of retained history and SHALL return the normal changes response shape, including next_changes_since.
Clients SHALL NOT need to construct internal version-0 cursor payloads to start incremental sync.
Scenario: A client starts incremental sync from the beginning
- WHEN a client queries
/v1/streams/<s>/records?changes_since=beginning - THEN the reference SHALL return records whose grant-authorized projections changed since the beginning of retained history
- AND the response SHALL include
next_changes_sincewhen the request succeeds - AND the response SHALL NOT expose or require construction of the internal version-0 cursor representation
Scenario: The initial changes response is paginated
- WHEN a client queries
/v1/streams/<s>/records?changes_since=beginning&limit=Nand additional visible changes remain - THEN the reference SHALL include
next_cursoronly as a page-continuation cursor for the same changes session - AND the response SHALL include
next_changes_sinceas the opaque bookmark for a future changes session
Scenario: A client sends a raw timestamp
- WHEN a client queries
/v1/streams/<s>/records?changes_since=2026-04-24T00:00:00Z - THEN the reference SHALL reject the request as an invalid changes cursor
- AND timestamp-based changes semantics SHALL remain unsupported unless a separate change defines them
Requirement: Changes bookmark documentation SHALL distinguish page cursors from changes cursors
The public documentation for GET /v1/streams/{stream}/records SHALL distinguish record-list page cursors from changes bookmarks. Documentation SHALL tell clients to use next_cursor only with the cursor query parameter and next_changes_since only with the changes_since query parameter.
Scenario: A client reads change-tracking guidance
- WHEN documentation explains how to continue a paginated record or changes response
- THEN it SHALL identify
next_cursoras a page-continuation token for thecursorparameter - AND it SHALL NOT tell clients to use
next_cursoraschanges_since
Scenario: A client reads incremental sync guidance
- WHEN documentation explains how to continue a later incremental sync session
- THEN it SHALL identify
next_changes_sinceas the opaque token to pass aschanges_since
Requirement: The reference implementation SHALL implement filtered retrieval through the public search surfaces
The reference implementation SHALL implement stream-scoped filters on GET /v1/search and GET /v1/search/semantic through the public endpoints, reusing the same filter validation semantics as record listing. Filtered retrieval SHALL remain grant-safe and SHALL NOT introduce a second filter grammar.
Scenario: Lexical retrieval applies a declared range filter
- WHEN a caller invokes
GET /v1/searchwithq, exactly onestreams[]value, and a declaredfilter[field][gte|gt|lte|lt] - THEN the reference SHALL validate the filter against the stream metadata and caller authorization
- AND every returned result SHALL hydrate to a visible record satisfying that filter
Scenario: Semantic retrieval applies a declared range filter
- WHEN a caller invokes
GET /v1/search/semanticwithq, exactly onestreams[]value, and a declaredfilter[field][gte|gt|lte|lt] - THEN the reference SHALL validate the filter against the stream metadata and caller authorization
- AND every returned result SHALL hydrate to a visible record satisfying that filter
Scenario: Filter validation fails
- WHEN a search request contains a filter without exactly one
streams[]value, an unauthorized field, an undeclared range field, an unsupported range operator, or a malformed filter value - THEN the reference SHALL reject the request before returning retrieval results
- AND the reference SHALL NOT return partial results from streams or connectors where the filter happened to be valid
Scenario: Forbidden retrieval controls remain rejected
- WHEN a caller passes expansion, sort, ranking knobs, connector-specific query parameters, model selectors, raw vectors, score/debug parameters, or DSL-shaped parameters to a retrieval endpoint
- THEN the reference SHALL reject those parameters according to the relevant retrieval contract
- AND filtered retrieval SHALL NOT be used as a backdoor to widen the public query surface
Requirement: Docker support SHALL provide an opt-in development hot-reload mode
The reference Docker support SHALL provide an opt-in Compose development mode that supports iterative source edits without rebuilding production images for each change.
Scenario: Docker dev mode starts
- WHEN an operator starts the Docker development override
- THEN the web service SHALL run a development server with source hot reload
- AND the reference service SHALL restart or reload when server source files change
- AND the composed public/internal URL topology SHALL remain the same as the default Docker stack
Scenario: Docker dev mode is accessed through another host
- WHEN an operator accesses Docker development mode through a LAN IP, hostname, or reverse proxy
- THEN the web service SHALL provide a documented configuration knob for additional Next development origins
- AND Docker development documentation SHALL state that reverse proxies must forward WebSocket upgrade traffic for Next HMR
Scenario: Docker dev mode runs connector flows
- WHEN the reference service runs inside the Docker development override
- THEN it SHALL load the repo-root local development env file when present
- AND connector credentials from that file SHALL be available to
controller-managed connector runs without requiring production images to load
.env.local
Scenario: Docker smoke mode remains reproducible
- WHEN an operator runs the default Docker smoke validation
- THEN it SHALL continue to build and run the production-style Docker stack
- AND it SHALL NOT require the development override
Requirement: Public Docker images SHALL be built and published from CI
The reference implementation SHALL provide a CI workflow that builds public Docker images for the supported Docker runtime targets and publishes them only from trusted refs.
Scenario: A pull request changes Docker-relevant files
- WHEN CI runs for a pull request that changes Docker-relevant files
- THEN CI SHALL build the supported Docker image targets
- AND CI SHALL NOT push images to a public registry from the pull request
Scenario: A trusted ref is built
- WHEN CI runs for a trusted publishing ref such as the default branch or a version tag
- THEN CI SHALL build the supported Docker image targets
- AND CI SHALL push the resulting images to the configured public registry
Scenario: Image publication runs
- WHEN CI publishes Docker images
- THEN the workflow SHALL use runtime CI credentials or the platform token
- AND it SHALL NOT require committed registry credentials
- AND it SHALL NOT bake owner passwords, connector credentials, SQLite data, embedding cache contents, or browser profile state into the image layers
Requirement: Public Docker images SHALL carry useful tags and metadata
Published reference Docker images SHALL include documented tags and metadata that support both convenient testing and reproducible operation.
Scenario: An operator chooses an image tag
- WHEN an operator reads the Docker documentation
- THEN the documentation SHALL explain which tags are moving tags
- AND it SHALL explain which tags or digests are appropriate for reproducible self-hosting
Scenario: CI publishes image metadata
- WHEN CI pushes a Docker image
- THEN the image SHALL include OCI metadata that identifies the source repository and image role
- AND the workflow SHALL request SBOM or provenance metadata when the registry and builder support it
Requirement: Docker documentation SHALL support pull-based self-hosting
The reference documentation SHALL describe how to run the reference stack from public images without requiring a local source build.
Scenario: An operator starts from public images
- WHEN an operator follows the Docker documentation for public images
- THEN they SHALL be told how to prepare runtime environment configuration
- AND they SHALL be told how to pull images and start the Compose stack
- AND they SHALL be told where the browser-facing origin is expected to be
Scenario: An operator persists state
- WHEN an operator follows the Docker documentation for public images
- THEN the documentation SHALL identify the persisted SQLite database, embedding cache, and browser connector/session state locations
- AND it SHALL distinguish persisted runtime state from image contents
Scenario: An operator upgrades images
- WHEN an operator updates a public-image deployment
- THEN the documentation SHALL describe how to pull newer images and restart the Compose stack without deleting persisted runtime volumes
Scenario: A contributor develops with Docker
- WHEN a contributor reads the Docker documentation
- THEN the documentation SHALL distinguish public-image operation from local image builds, smoke validation, and opt-in Docker hot reload
Requirement: Deployment diagnostics SHALL surface lexical backfill progress
The reference deployment diagnostics surface SHALL report active lexical index backfill progress when the reference server is rebuilding lexical search indexes.
Scenario: Lexical backfill is active
- WHEN a lexical index backfill is actively scanning or rebuilding records
- THEN
/_ref/deploymentSHALL include the current lexical backfill job - AND the report SHALL include enough progress data for the dashboard to show the connector, stream, phase, scanned records, total records when known, written index rows, and updated timestamp
- AND the report SHALL include a warning that lexical search results may be partial while the rebuild is active
Scenario: Lexical backfill is inactive
- WHEN no lexical index backfill is active
- THEN
/_ref/deploymentSHALL report no active lexical backfill progress - AND it SHALL NOT emit a lexical rebuilding warning
Scenario: Dashboard renders lexical progress
- WHEN
/dashboard/deploymentreceives lexical backfill progress - THEN it SHALL render browser-visible progress without requiring operators to inspect container logs
Requirement: Docker assembly SHALL preserve reference architecture boundaries
The reference implementation SHALL provide a Docker or Docker Compose path that assembles the live reference stack without redefining PDPP protocol behavior, hiding control-plane behavior, or making the website the implementation boundary.
Scenario: Docker starts the live reference stack
- WHEN an operator starts the supported Docker assembly
- THEN the assembly SHALL run the reference AS/RS process and the browser-facing web app as the current reference architecture defines them
- AND the AS SHALL listen on port
7662 - AND the RS SHALL listen on port
7663 - AND the web app SHALL listen on port
3000
Scenario: Docker is used as assembly
- WHEN a reviewer evaluates Docker artifacts for the reference implementation
- THEN those artifacts SHALL be documented as deployment assembly for the reference stack
- AND they SHALL NOT be described as PDPP protocol requirements or as an alternate control-plane contract
Requirement: Docker builds SHALL use the monorepo toolchain
Docker builds for the supported reference stack SHALL use the repo-root pnpm workspace through Corepack and SHALL use a Debian/Ubuntu-based Node image compatible with the reference's native dependencies.
Scenario: Dependencies are installed in Docker
- WHEN a Docker image installs JavaScript dependencies
- THEN it SHALL install from the repository root using the checked-in pnpm workspace and lockfile
- AND it SHALL NOT run package-local
npm installcommands that create a dependency graph different from local development
Scenario: Native dependencies are built in Docker
- WHEN a Docker image builds or loads native dependencies such as SQLite or browser-automation dependencies
- THEN the base image SHALL be Debian/Ubuntu-based Node rather than Alpine
- AND the Node version SHALL be compatible with the repo's runtime floor for
node:sqlite
Requirement: Docker topology SHALL distinguish public and internal URLs
The Docker assembly SHALL keep browser-facing reference origin configuration separate from container-internal AS/RS service URLs.
Scenario: Composed mode is configured in Docker
- WHEN the Docker stack runs in composed mode
- THEN
PDPP_REFERENCE_ORIGINSHALL identify the external browser-facing origin - AND
PDPP_AS_URLSHALL identify the container-internal AS URL - AND
PDPP_RS_URLSHALL identify the container-internal RS URL
Scenario: Services call each other inside Docker
- WHEN one container calls the AS or RS container
- THEN it SHALL use Docker service DNS or another explicit internal URL
- AND it SHALL NOT rely on
localhostto mean another container
Scenario: Browser-facing metadata is emitted
- WHEN the AS or RS emits public metadata, device verification URLs, or pending-consent authorization URLs in composed Docker mode
- THEN those URLs SHALL use
PDPP_REFERENCE_ORIGIN - AND they SHALL NOT leak internal Docker service names as browser-facing URLs
Requirement: Docker runtime state SHALL be persistent and explicit
The Docker assembly SHALL document and provide persistence for the state required by real reference operation.
Scenario: Reference data is written
- WHEN the Docker stack writes reference records, grants, runs, or semantic vectors
- THEN the configured SQLite database path SHALL be backed by a persisted volume or documented host bind mount
Scenario: Semantic embeddings are used
- WHEN the Docker stack uses the local semantic embedding backend
- THEN the embedding model cache path SHALL be persisted or documented as intentionally ephemeral
- AND first-boot model download behavior SHALL be documented
Scenario: Browser connectors are used
- WHEN browser-based polyfill connectors run inside or alongside the Docker stack
- THEN browser profiles, daemon files, and connector session state SHALL have a persisted volume or documented host bind mount
- AND the documentation SHALL state that browser connectors depend on persistent profiles and upstream anti-bot behavior
Requirement: Docker secrets SHALL be runtime-provided
The Docker assembly SHALL keep owner passwords, connector credentials, tokens, cookies, and other secrets out of built image layers.
Scenario: A secret is needed by the Docker stack
- WHEN the Docker stack needs
PDPP_OWNER_PASSWORD, connector credentials, tokens, cookies, or dynamic-client-registration secrets - THEN those values SHALL be supplied at runtime through environment variables, env files, or Docker secrets
- AND they SHALL NOT be baked into Dockerfiles, image layers, committed Compose defaults, or generated static assets
Scenario: Deployment diagnostics render Docker env
- WHEN the dashboard deployment diagnostics render secret-bearing Docker environment variables
- THEN secret values SHALL be redacted before reaching the dashboard
Requirement: Docker support SHALL include a smoke validation path
The supported Docker path SHALL include a reproducible smoke validation that does not require real third-party connector credentials.
Scenario: Docker smoke validation runs
- WHEN an operator or CI job runs the Docker smoke validation
- THEN it SHALL verify that the browser-facing web origin responds
- AND it SHALL verify that AS and RS metadata are reachable through the composed origin
- AND it SHALL verify that browser-facing metadata does not expose internal Docker service URLs
Scenario: Owner auth is configured during Docker smoke validation
- WHEN
PDPP_OWNER_PASSWORDis configured for the Docker smoke validation - THEN dashboard access SHALL either redirect unauthenticated requests to
/owner/loginor pass after a valid owner session is established
Requirement: The reference implementation SHALL use better-sqlite3 as its SQLite driver
The reference implementation SHALL access SQLite via better-sqlite3. It SHALL
NOT depend on @databases/sqlite or the legacy sqlite3 N-API binding for any
runtime SQLite code path.
Scenario: Fresh install includes only the chosen driver
- WHEN a developer installs the reference implementation dependencies
- THEN
better-sqlite3SHALL be installed as a direct dependency - AND
@databases/sqliteSHALL NOT be required for reference runtime SQLite access
Scenario: Sustained dashboard workload does not crash the server
- WHEN a client issues concurrent requests to
/dashboard/records,/dashboard/search?q=..., and/planning/changesfor ten or more rounds - THEN the reference server process SHALL remain alive throughout
- AND SHALL NOT emit
SIGSEGV,SIGABRT, orfree(): invalid sizeabnormal termination
Requirement: Pre-existing databases SHALL continue to open and operate
The reference implementation SHALL open and operate against SQLite files that worked with the previous driver. No schema changes, data migration, or file-format change SHALL be required solely because of the driver swap.
Scenario: Existing polyfill substrate continues to serve records
- WHEN the reference implementation starts against a pre-existing polyfill SQLite database
- THEN it SHALL open the file without a driver-level migration
- AND it SHALL serve existing records and spine events from that file via the
/v1and/_refHTTP surfaces with the same response shapes as before
Requirement: Partial connector runs SHALL expose known gaps
The reference runtime SHALL expose machine-readable known gaps when a connector run skips streams, records, or source regions that were in requested scope but not collected.
Scenario: A stream is skipped because credentials are missing
- WHEN a connector cannot collect a requested stream because required credentials or interaction are absent
- THEN the run timeline SHALL record the skipped stream and reason
- AND the operator surface SHALL distinguish that gap from a successful complete collection
Requirement: Partial data SHALL NOT be represented as complete
The reference implementation SHALL NOT present records from an incomplete connector run as evidence that the requested scope was fully collected unless the run has no known gaps for that scope.
Scenario: A connector flushes records before a later stream fails
- WHEN a run flushes records for one stream and then fails or skips another requested stream
- THEN the flushed records MAY remain queryable
- AND reference diagnostics SHALL preserve that the latest run had known gaps
Requirement: Recovery hints SHALL be bounded and non-secret
Known-gap and skip diagnostics SHALL include bounded recovery hints when the runtime or connector can identify a next step, but SHALL NOT persist credentials, OTPs, cookies, raw page contents, or other secrets.
Scenario: A manual login is required
- WHEN a connector requires a manual login or anti-bot resolution before it can continue
- THEN the run timeline MAY expose a recovery hint such as
manual_action_required - AND it SHALL NOT persist submitted credentials or browser session secrets