aravind changes

This commit is contained in:
2026-08-25 16:37:05 +05:30
parent cadea4bd92
commit b6f8655909
27 changed files with 5058 additions and 163 deletions

View File

@@ -129,6 +129,125 @@ these would leave the shim with methods that 404. See §11 (D6).
---
## 2A. Owliver suggestions
`GET /api/v1/owliver/suggestions?page={surface}&query={typed}`
The one endpoint here that serves no resource. It answers "what could I usefully
ask on this page?" for the Owliver panel, which calls it while the user types —
so it reads no table, opens no transaction, calls no model, and its whole answer
is computed from a static catalogue in `internal/owliver`.
It is not on the public allowlist. Which readings exist depends on the caller's
role, so there is no anonymous answer to give.
### Request
| Parameter | Required | Meaning |
| --- | --- | --- |
| `page` | yes | A surface id from the closed page vocabulary — the same one `internal/definition` validates a definition's `pages:` against. Aliases (`hired`, `forge`, `new-position`) and loose spellings (`Talent Pool`) resolve to the canonical id. |
| `query` | no | What the user has typed so far. |
Any other parameter is `invalid_query`. **Nothing about the caller is accepted
here** — role, organization and user are read from the session, and a request
that names one is refused rather than ignored.
An unknown `page` is `invalid_query`, with the frontend's own wording:
`Unsupported page: {value}. Supported pages: {…}.` An absent, blank or
too-short `query` is **not** an error — there is simply nothing to rank yet.
### Response
```json
{
"data": {
"suggestions": [
{ "text": "Which position has the strongest pipeline?", "intent": "position-strength" },
{ "text": "Show hiring activity as a flow", "intent": "hiring-operations", "capability": "flow" }
]
}
}
```
`data.suggestions` is always an array — `[]` when nothing matches, never `null`
and never an error. There is no `meta`: the list is capped rather than paged.
| Field | Meaning |
| --- | --- |
| `text` | The question, as the user reads it. |
| `intent` | The **frontend capability id** the panel dispatches on, verbatim from the manifests in `src/components/ai-assistant/capabilities/`. Not a backend identifier, and never invented here. |
| `capability` | The section type to draw the answer as, from the closed `OWLIVER_CAPABILITIES` vocabulary. Present only when the query asked for one ("as a flow", "summarize"); absent otherwise. |
Nothing internal is exposed: no matching terms, no resource names, no scores, no
policy detail. `text` is always catalogue wording — no part of the query is
echoed back into a suggestion.
### Selection
Five stages, each of which only ever removes:
```
the page's catalogue → permission → relevance → deduplicate → top 3
```
- **Page.** Intents are keyed by surface, so a suggestion from another page
cannot appear. The same word answers differently per page by construction:
`pipeline` on `positions` is about which role converts, on `candidates` it is
the funnel the applicants are in.
- **Permission.** Each intent declares the resources it reads, and those are
checked against the policy table in §9A — *before* ranking, so a refused
reading is never scored. Two gates, not one: the role must be allowed the
operation, and for an organization-wide reading the role's rows must not be
narrowed. Talent may list job applications; talent may not be offered "which
position has the strongest pipeline?", because their view of that resource is
their own rows. No role list is written down here — see §9A.1.
- **Relevance.** Deterministic keyword ranking over the intent's own terms.
Exact token, then multi-word phrase, then prefix (so a half-typed word still
matches), then extension. Ties break on catalogue order, so the same request
always answers identically. A query naming only a section type ranks the
page's readings; once it names a subject, readings that merely *support* that
shape are dropped rather than used as padding.
- **Deduplicate.** One suggestion per intent id, and no two with the same text.
- **Cap.** Three. Nothing is added to reach three.
A query with fewer than two letters or digits after normalization returns `[]`.
### Normalization
The query is truncated to 200 characters, lower-cased, and reduced to letters,
digits and single spaces — every other character becomes a space rather than
being stripped, so nothing can be glued into a token that was not typed as one.
There is no injection surface to defend: the normalized text is compared against
a fixed table of literals and never reaches SQL, a template, a shell or a log
message. Hostile input is ranked like any other text, and can only ever produce
entries the catalogue already holds.
### Where the catalogue comes from
The frontend owns the vocabulary. Owliver's capabilities are declared per page
context in `src/components/ai-assistant/capabilities/`; `internal/owliver`
transcribes the id and the page, and adds the two things a manifest does not
carry — the words that mean a user is reaching for that reading, and the records
it reads. Same pattern as `internal/definition/vocabulary.go`, and for the same
reason: one vocabulary, named from both ends, with a test on this side that
fails when an id here names no capability there.
**No table, no migration.** The suggestions are derived from definitions that
already exist. Persistence would only be warranted if suggestions became
admin-managed, and nothing in the product asks for that today.
### Not covered
The seven workspace and configuration surfaces — `settings`, `workspace`,
`workspace-agents`, `workspace-skills`, `workspace-skill-configure`,
`skill-development`, `workspace-agent-configure` — are valid pages with no
entries. Their panel answers from the registries rather than from workforce
records, so there is nothing to rank a typed query against. They return `[]`,
which is the honest answer, not a validation error.
---
## 3. Request schemas
### 3.1 Create — `POST /{resource}`