Relevant is not the same as allowed
Classify before you index, not after.

Proximity is not permission
Most retrieval-augmented generation pipelines are built in an order that quietly treats relevance and permission as the same test. A document lands in an ingestion queue, gets split into chunks, each chunk is embedded into a vector space, and the whole set lands in one index, or a small number of indices keyed loosely by tenant or project. Only after that, when a user issues a query, does anything ask whether this particular person is allowed to see this particular chunk. The access check is a metadata filter bolted onto a similarity search that has already run its arithmetic. That ordering is the structural flaw, not the arithmetic itself.
A cosine similarity score has no concept of a role, a clearance level, or a non-disclosure boundary. It measures how close two vectors sit in a space built from token co-occurrence and semantic structure, nothing else. If a redundancy-planning memo and a redacted board paper discuss the same transaction in similar language, their embeddings will sit close together whether or not the person asking is allowed to read either one. The index has already ranked the forbidden chunk as the best match before any permission check runs. Filtering it out of the results afterwards does not undo the fact that the system computed, ranked, and in many implementations logged that chunk as the top candidate.
Sensitivity tiers come before the splitter, not after
The fix starts earlier than most teams think to look. Classification has to run on the whole document, before chunking, because chunking destroys the context a classifier needs to make the call. A single paragraph pulled out of a contract can read as plain boilerplate; the sensitivity sits in which contract it is, who the counterparties are, and which schedule it belongs to. We treat sensitivity as a small number of discrete tiers (public, internal, restricted, need to know) assigned to the source document as a first pass, using whatever signal the organisation already has: folder provenance, author, an existing data loss prevention label, or a document-level classifier. That first pass is deliberately coarse. It is the cheap, high-confidence step that nothing downstream is allowed to bypass, and it has to exist before the splitter runs, because once a document has been split, the document-level context that made the classification possible is gone.
Chunk-level tags travel with the vector, not beside it
Tiering the document is necessary but not sufficient, because a long document is rarely one sensitivity level all the way through. A technical design document might be internal throughout except for one section referencing a customer's production configuration or a specific vulnerability. If the document is tagged once at the document tier and chunked afterwards, that one sensitive section inherits the same, too permissive, tag as everything around it. So the second pass happens at chunk granularity, after splitting but still before embedding: each chunk inherits the document tier as a floor, and can be tightened further if it contains something that justifies a stricter tag. The tag is written into the same record as the embedding, at the same step, not into a side table that something downstream can forget to join. When the embedding model runs, the vector and its sensitivity tag are produced together, as one unit, destined for one namespace. There should be no path through the pipeline where a vector exists without a tag already attached, because a vector without a tag is a vector nobody has decided whether to protect yet, and in a pipeline running at scale, undecided always resolves to retrievable.
The retrieval gate decides what gets searched, not what gets returned
This is where the ordering argument actually bites. A conventional implementation runs the similarity search against the whole index, takes the top matches back, and only then filters that list against the querying user's permissions, discarding anything they should not see. That is a post-hoc filter on results that have already been computed, which means the ranking itself was computed over forbidden material. Any gap in the filter (a role mapping that was never updated after someone changed teams, a missing tag on a chunk that slipped through classification, a tenant boundary the filter assumes but the index does not actually enforce) returns the forbidden chunk directly, because the vector was already sitting in the same searchable space as everything else.
conventional order:
embed(all chunks) -> one index -> similarity_search(query) -> filter(results, user)
gated order:
classify(document) -> chunk+tag(sensitivity) -> embed -> namespace[tier][tenant]
query -> resolve(user_permissions) -> select namespace(s) -> similarity_search(scoped)The alternative is to make the sensitivity tag a precondition for running the search at all, not a predicate on its output. In practice that means a namespace or shard boundary, scoped by tier and tenant, that the query is restricted into before cosine similarity runs, so a forbidden chunk is never compared against the query vector in the first place. If the comparison never happens, a missing tag further downstream cannot leak it, because the leak path required that comparison to have already run. The gate is a precondition on which index gets searched, not a cleanup step on what the search already found.
Why this is a pipeline change, not a policy document
None of this is solved by writing a data classification policy and asking teams to follow it. The moment anyone embeds a document without running it through the tiering step, the resulting vector is permanently ungoverned, and nothing downstream can retroactively tell that vector what it should have been. The ordering has to be enforced in the ingestion code itself: classify, then chunk and tag, then gate the namespace the chunk lives in, in that sequence, with no code path that embeds before classifying. Inside our Sovereign Intelligence Operating System, we built the retrieval layer around gated namespaces keyed on sensitivity tier, rather than a single flat index with a filter in front of it, because we would rather a chunk simply not exist in a queryable space than rely on every query author remembering to add the right clause. It costs more at ingestion. Classification is slower than dumping everything straight into a splitter, and tiering decisions need a person in the loop whenever the automated classifier is unsure. That cost is the point. It buys a system where relevant and allowed are checked at different points in the pipeline, in a fixed order, instead of being quietly treated as the same question answered once.