Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
602c715cbf | ||
|
|
28df7a0e56 | ||
|
|
811871e978 | ||
|
|
fbf1da7452 | ||
|
|
a350178434 | ||
|
|
7f1dfddb83 | ||
|
|
233215e6c9 | ||
|
|
df2fa0f54b | ||
|
|
1f42d2a821 | ||
|
|
269360a4ea | ||
|
|
44b73b587e | ||
|
|
b85b107f6d | ||
|
|
a48e05585e |
No files matched your search
@@ -2,6 +2,41 @@
|
|||||||
|
|
||||||
Every release, newest first, generated from the commits in each release.
|
Every release, newest first, generated from the commits in each release.
|
||||||
|
|
||||||
|
## v0.1.5 — 2026-09-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- put the recall chip in the header utilities, not the cell the sidebar's expand button owns
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.4 — 2026-09-26
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- register the settings card instead of throwing while building it
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.3 — 2026-09-26
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- draw the settings on the plugin card, not a click deeper
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.2 — 2026-09-26
|
||||||
|
|
||||||
|
### Documented
|
||||||
|
|
||||||
|
- lead the install instructions with the published git spec
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.1 — 2026-09-26
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- remove one machine's addresses and paths from the shipped defaults
|
||||||
|
|
||||||
|
|
||||||
## v0.1.0 — 2026-09-26
|
## v0.1.0 — 2026-09-26
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ about a millisecond; the only per-query network cost is one embedding call.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dsh plugin --profile web add git+https://git.notexpectedyet.com/Not-Expected-Yet/dsh-recall.git
|
||||||
|
```
|
||||||
|
|
||||||
|
That form installs whatever the default branch holds, pinned to a commit in the
|
||||||
|
profile's lockfile. The semver fragment installs the newest release tag instead,
|
||||||
|
and is what makes `dsh plugin update` follow releases — see
|
||||||
|
[Releases](#releases) for the tag and versioning rules:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dsh plugin --profile web add 'git+https://git.notexpectedyet.com/Not-Expected-Yet/dsh-recall.git#semver:^0.1.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
To run your own copy instead, add the directory — that becomes a `link:`
|
||||||
|
dependency which tracks the working tree, so an edit needs no reinstall:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dsh plugin --profile web add /path/to/dsh-recall
|
dsh plugin --profile web add /path/to/dsh-recall
|
||||||
```
|
```
|
||||||
@@ -62,12 +78,14 @@ a fact:
|
|||||||
- id: dsh-recall
|
- id: dsh-recall
|
||||||
name: dsh-recall
|
name: dsh-recall
|
||||||
config:
|
config:
|
||||||
embeddingApiBase: http://10.50.1.2:11434
|
embeddingApiBase: http://127.0.0.1:11434 # replace with your endpoint
|
||||||
```
|
```
|
||||||
|
|
||||||
Or edit every setting from the UI: **Plugins → dsh-recall → Configure**. The
|
Or edit every setting from the UI: **Plugins** in the sidebar → the
|
||||||
plugin ships **two halves**, and the page appears only when both are present and
|
**dsh-recall** card → its settings are on that card, below the description.
|
||||||
the deployment serves the namespace:
|
(Not Settings → *Built-in plugins*: that section is a read-only inventory of
|
||||||
|
what the deployment ships.) The plugin ships **two halves**, and the section
|
||||||
|
appears only when both are present and the deployment serves the namespace:
|
||||||
|
|
||||||
- the **host half** (`index.js`) exports a `Config` schema whose every field is
|
- the **host half** (`index.js`) exports a `Config` schema whose every field is
|
||||||
`.volatile()`. That schema *is* the settings page's description — the Harness
|
`.volatile()`. That schema *is* the settings page's description — the Harness
|
||||||
@@ -76,11 +94,16 @@ the deployment serves the namespace:
|
|||||||
a schema without those markers has no settings surface at all rather than an
|
a schema without those markers has no settings surface at all rather than an
|
||||||
empty one;
|
empty one;
|
||||||
- the **browser half** (`client/client.js`) renders that form into the Plugins
|
- the **browser half** (`client/client.js`) renders that form into the Plugins
|
||||||
panel's per-row configuration seat, `plugins.row.config`, keyed
|
panel's per-bundle configuration seat, `plugins.bundle.config`, keyed by the
|
||||||
`<bundle package>#<row id>` — here `dsh-recall#dsh-recall`. Registering it is
|
bundle's package name. Registering it is what draws the section; without it
|
||||||
what adds the **Configure** control to the plugin's row.
|
there is no settings UI however complete the schema is.
|
||||||
|
|
||||||
A served namespace with no page draws no control, and a page whose namespace is
|
The sibling seat `plugins.row.config` — keyed `<package>#<row id>` — draws one
|
||||||
|
row's configuration on that row's own page. That is the right home for one row
|
||||||
|
of a bundle that declares several; this bundle declares one, so its settings
|
||||||
|
belong on the card.
|
||||||
|
|
||||||
|
A served namespace with no page draws nothing, and a page whose namespace is
|
||||||
not served is deliberately not registered — so both are required. A restart is
|
not served is deliberately not registered — so both are required. A restart is
|
||||||
needed for a newly installed browser half to appear, because client bundle bytes
|
needed for a newly installed browser half to appear, because client bundle bytes
|
||||||
are snapshotted when the profile activates.
|
are snapshotted when the profile activates.
|
||||||
@@ -92,7 +115,7 @@ document, so the value survives a restart and stays visible in
|
|||||||
Every value is also checkable from the tool, without touching a file:
|
Every value is also checkable from the tool, without touching a file:
|
||||||
|
|
||||||
```
|
```
|
||||||
recall action=configure config={"embeddingApiBase":"http://10.50.1.2:11434"}
|
recall action=configure config={"embeddingApiBase":"http://127.0.0.1:11434"}
|
||||||
```
|
```
|
||||||
|
|
||||||
The API key is declared `role('secret')`, so it is stripped from every settings
|
The API key is declared `role('secret')`, so it is stripped from every settings
|
||||||
@@ -123,18 +146,20 @@ Parameters: `query`, `action`, `limit`, `path` (restrict to a subdirectory),
|
|||||||
|
|
||||||
Two surfaces, both reading a host projection rather than polling:
|
Two surfaces, both reading a host projection rather than polling:
|
||||||
|
|
||||||
- a **chip in the session header** — `Recall 3 · 40× · 457 KB kept out`. No price
|
- a **chip in the session header's utilities** — `Recall 3 · 40× · 457 KB kept
|
||||||
on it, deliberately: at the measured rate a whole conversation's avoided
|
out`. No price on it, deliberately: at the measured rate a whole conversation's
|
||||||
context is a fraction of a penny, and a chip advertising that reads as the tool
|
avoided context is a fraction of a penny, and a chip advertising that reads as
|
||||||
being useless rather than as the arithmetic being honest. The tooltip carries
|
the tool being useless rather than as the arithmetic being honest. The tooltip
|
||||||
the full comparison.
|
carries the full comparison.
|
||||||
- a **right-sidebar Recall tab** — the same figures with room for what they are
|
- a **right-sidebar Recall tab** — the same figures with room for what they are
|
||||||
not: *"An estimate, and an upper bound: it assumes a whole file would have been
|
not: *"An estimate, and an upper bound: it assumes a whole file would have been
|
||||||
read each time. Token counts are bytes ÷ 4."*
|
read each time. Token counts are bytes ÷ 4."*
|
||||||
|
|
||||||
Both render nothing when there is nothing to say — a conversation that never used
|
Both render nothing when there is nothing to say — a conversation that never used
|
||||||
recall costs no layout at all, which matters for the header corner because the
|
recall costs no layout at all. The chip is an entry in the header's `utilities`
|
||||||
framework lays it out only while its occupant renders something.
|
list, carrying its own id. The far-right corner is deliberately left alone: it is
|
||||||
|
a single cell that the product's own right-sidebar expand control occupies, so a
|
||||||
|
registration there shadows shipped UI rather than sitting beside it.
|
||||||
|
|
||||||
The data path is `presentationMeta` → the `tool/result` event's `meta` → a
|
The data path is `presentationMeta` → the `tool/result` event's `meta` → a
|
||||||
projection unit (`recallUsage`) with a wire view the browser reads through
|
projection unit (`recallUsage`) with a wire view the browser reads through
|
||||||
@@ -391,11 +416,17 @@ would publish.
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm test # 91 tests, no network required
|
npm test # the whole suite; no network required
|
||||||
node scripts/smoke.mjs [workspace] [query] # live retrieval check
|
node scripts/smoke.mjs [workspace] [query] # live retrieval check
|
||||||
node scripts/measure.mjs [workspace] [query] [budgetMs]
|
node scripts/measure.mjs [workspace] [query] [budgetMs]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Both live scripts default to the endpoint the schema defaults to
|
||||||
|
(`http://127.0.0.1:11434`) and to a model `ollama pull` can fetch, so they need
|
||||||
|
no configuration on a local Ollama. Point them at another host with
|
||||||
|
`RECALL_API_BASE` and `RECALL_MODEL`; `[workspace]` defaults to the working
|
||||||
|
directory.
|
||||||
|
|
||||||
The `lib/` tests run anywhere. `test/config.test.js` and `test/plugin.test.js`
|
The `lib/` tests run anywhere. `test/config.test.js` and `test/plugin.test.js`
|
||||||
import `index.js`, which imports `@deepseek-ai/dsh-tools` and
|
import `index.js`, which imports `@deepseek-ai/dsh-tools` and
|
||||||
`@deepseek-ai/schemastery` — resolvable once the plugin is installed into a
|
`@deepseek-ai/schemastery` — resolvable once the plugin is installed into a
|
||||||
|
|||||||
+58
-25
@@ -4,7 +4,7 @@
|
|||||||
* Three contributions, each into a seat the Harness 0.1.7 composition declares:
|
* Three contributions, each into a seat the Harness 0.1.7 composition declares:
|
||||||
*
|
*
|
||||||
* 1. the plugin's configuration page, in the **Plugins** panel;
|
* 1. the plugin's configuration page, in the **Plugins** panel;
|
||||||
* 2. a one-line usage chip in a conversation's header corner;
|
* 2. a one-line usage chip in a conversation's header utilities;
|
||||||
* 3. a right-sidebar tab with the same figures and room for their caveats.
|
* 3. a right-sidebar tab with the same figures and room for their caveats.
|
||||||
*
|
*
|
||||||
* **Where plugin settings live now.** 0.1.5 drew plugin configuration as cards
|
* **Where plugin settings live now.** 0.1.5 drew plugin configuration as cards
|
||||||
@@ -12,10 +12,11 @@
|
|||||||
* binding `settingsScope` and registering into a `settings.plugin.item` slot.
|
* binding `settingsScope` and registering into a `settings.plugin.item` slot.
|
||||||
* Neither exists in 0.1.7. The generated settings document is keyed by the
|
* Neither exists in 0.1.7. The generated settings document is keyed by the
|
||||||
* plugin's own profile entry id, and a plugin that wants a page renders it as a
|
* plugin's own profile entry id, and a plugin that wants a page renders it as a
|
||||||
* contribution to the Plugins panel: `plugins.row.config`, keyed
|
* contribution to the Plugins panel — `plugins.bundle.config`, keyed by the
|
||||||
* `<bundle package>#<row id>`. That key is what puts a **configure** control on
|
* bundle's package name, which the page draws as a section on the plugin's own
|
||||||
* the plugin's row — without a contribution there is no control and no page,
|
* card. Without a contribution there is no section, however complete the schema
|
||||||
* however complete the schema is.
|
* is; the sibling seat `plugins.row.config` would instead put it one click
|
||||||
|
* deeper, on a single row's page.
|
||||||
*
|
*
|
||||||
* **The page is the framework's form, not a form of our own.** The card used to
|
* **The page is the framework's form, not a form of our own.** The card used to
|
||||||
* reproduce `PluginCard`'s chrome by hand — the `<li>` root, the disclosure
|
* reproduce `PluginCard`'s chrome by hand — the `<li>` root, the disclosure
|
||||||
@@ -70,13 +71,17 @@ window.__ModuleLoader__.load({
|
|||||||
/**
|
/**
|
||||||
* The seat this plugin's configuration page renders in.
|
* The seat this plugin's configuration page renders in.
|
||||||
*
|
*
|
||||||
* Keyed `<bundle package name>#<row id>`, both as the profile's patch
|
* `plugins.bundle.config`, keyed by the BUNDLE's package name: the Plugins
|
||||||
* declares them. For this bundle the two coincide, which is a coincidence
|
* page draws that contribution as a section on the plugin's own card,
|
||||||
* and not a rule — the constants are kept separate so that stays visible.
|
* between its description and its rows. That is where someone opening
|
||||||
|
* "dsh-recall" expects its settings — the alternative seat,
|
||||||
|
* `plugins.row.config` (keyed `<package>#<row id>`), draws them on a page
|
||||||
|
* one click deeper, behind the row's title button, which is the right home
|
||||||
|
* for one row of a bundle that declares several. This bundle declares one
|
||||||
|
* row, so the card is the honest place.
|
||||||
*/
|
*/
|
||||||
const BUNDLE_PACKAGE = 'dsh-recall'
|
const BUNDLE_PACKAGE = 'dsh-recall'
|
||||||
const BUNDLE_ROW = 'dsh-recall'
|
const BUNDLE_CONFIG_KEY = BUNDLE_PACKAGE
|
||||||
const ROW_CONFIG_KEY = `${BUNDLE_PACKAGE}#${BUNDLE_ROW}`
|
|
||||||
|
|
||||||
const TITLE = 'Recall'
|
const TITLE = 'Recall'
|
||||||
const DESCRIPTION =
|
const DESCRIPTION =
|
||||||
@@ -380,6 +385,16 @@ window.__ModuleLoader__.load({
|
|||||||
*/
|
*/
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
this.ctx = ctx
|
this.ctx = ctx
|
||||||
|
// Every field the projection reads must exist BEFORE anything projects.
|
||||||
|
//
|
||||||
|
// `SettingsFormModel.bind()` is not a subscription setter: it builds the
|
||||||
|
// snapshot store from one immediate `project()` call, so the projection
|
||||||
|
// runs once, right here, inside this constructor. Assigning `describe`
|
||||||
|
// after `bind` made that first projection read `undefined.getSnapshot()`
|
||||||
|
// — a TypeError thrown while the inject callback was registering the
|
||||||
|
// card, which aborted the registration and left the plugin with no
|
||||||
|
// settings section at all and nothing in the UI to say so.
|
||||||
|
this.describe = ctx.configForms.describe()
|
||||||
this.form = ctx.configForms.get(SETTINGS_ENTRY)
|
this.form = ctx.configForms.get(SETTINGS_ENTRY)
|
||||||
this.model = new SettingsFormModel(
|
this.model = new SettingsFormModel(
|
||||||
this.form,
|
this.form,
|
||||||
@@ -394,8 +409,6 @@ window.__ModuleLoader__.load({
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
this.store = this.model.bind(() => this.projection())
|
this.store = this.model.bind(() => this.projection())
|
||||||
/** The describe mirror that knows whether a secret is configured. */
|
|
||||||
this.describe = ctx.configForms.describe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Everything the page renders, rebuilt whenever the form or a draft moves. */
|
/** Everything the page renders, rebuilt whenever the form or a draft moves. */
|
||||||
@@ -486,10 +499,12 @@ window.__ModuleLoader__.load({
|
|||||||
/**
|
/**
|
||||||
* The plugin's configuration page.
|
* The plugin's configuration page.
|
||||||
*
|
*
|
||||||
* The Plugins page asks a configuration entry twice: once for a one-liner to
|
* The seat asks for `page`, the form with its save control; the `summary`
|
||||||
* stand in for a missing description (`summary`), and once for the body of
|
* branch answers the other shape a configuration entry can be asked for —
|
||||||
* the plugin's own page (`page`). Only the second draws a save control —
|
* the one-liner standing in for a missing description — because a component
|
||||||
* and only one save writes, because the form discards its drafts on unmount.
|
* registered under a seat that only ever asks for `page` is also the natural
|
||||||
|
* thing to reuse where that shape arrives. Only a save writes, and the form
|
||||||
|
* discards its drafts on unmount.
|
||||||
*/
|
*/
|
||||||
function RecallSettingsCard(props) {
|
function RecallSettingsCard(props) {
|
||||||
if (props.view === 'summary') return DESCRIPTION
|
if (props.view === 'summary') return DESCRIPTION
|
||||||
@@ -605,8 +620,8 @@ window.__ModuleLoader__.load({
|
|||||||
|
|
||||||
function RecallChip(props) {
|
function RecallChip(props) {
|
||||||
const usage = useUsage(props)
|
const usage = useUsage(props)
|
||||||
// The corner is laid out only while its occupant renders something, so a
|
// The utilities list draws nothing for an entry that renders nothing, so
|
||||||
// conversation that never called the tool costs no layout at all.
|
// a conversation that never called the tool costs no layout at all.
|
||||||
if (usage === undefined || !usage.searches) return null
|
if (usage === undefined || !usage.searches) return null
|
||||||
return h(
|
return h(
|
||||||
'span',
|
'span',
|
||||||
@@ -705,11 +720,11 @@ window.__ModuleLoader__.load({
|
|||||||
scoped.effect(
|
scoped.effect(
|
||||||
() =>
|
() =>
|
||||||
scoped.configForms.whileServed([SETTINGS_ENTRY], () =>
|
scoped.configForms.whileServed([SETTINGS_ENTRY], () =>
|
||||||
scoped.slots.inject('plugins.row.config', () =>
|
scoped.slots.inject('plugins.bundle.config', () =>
|
||||||
scoped.slots.register(
|
scoped.slots.register(
|
||||||
{
|
{
|
||||||
name: 'plugins.row.config',
|
name: 'plugins.bundle.config',
|
||||||
key: ROW_CONFIG_KEY,
|
key: BUNDLE_CONFIG_KEY,
|
||||||
inject: () => card.inject(),
|
inject: () => card.inject(),
|
||||||
},
|
},
|
||||||
RecallSettingsCard,
|
RecallSettingsCard,
|
||||||
@@ -720,10 +735,28 @@ window.__ModuleLoader__.load({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// The chip: the header's far-right corner, which the framework lays out
|
// The chip: a right-aligned session utility, next to the header's
|
||||||
// only while its occupant renders something.
|
// far-right corner.
|
||||||
ctx.slots.inject('conversation.session.header.corner', () =>
|
//
|
||||||
ctx.slots.register({ name: 'conversation.session.header.corner' }, (props) => h(RecallChip, props)),
|
// It is in the `utilities` LIST, not the corner, and that is not a
|
||||||
|
// cosmetic choice. The corner is a `single` cell whose occupant the
|
||||||
|
// product already declares — `client-ui-sidebar-right ExpandButton`, the
|
||||||
|
// right sidebar's expand control — and the client catalog reports it as
|
||||||
|
// `replaceRisk: "shadows-shipped-ui"`. A second registration at the same
|
||||||
|
// priority is a hard error inside the slot registry, thrown while the
|
||||||
|
// other plugin's `slots.inject()` callback runs: the renderer catches it,
|
||||||
|
// STOPS that inject controller for the rest of the fiber's life, and
|
||||||
|
// rethrows it uncaught. So squatting there did not merely race — it took
|
||||||
|
// the shipped expand control down and logged an uncaught error. A list
|
||||||
|
// entry carrying its own id cannot collide with anyone, and the framework
|
||||||
|
// draws nothing for an entry that renders nothing.
|
||||||
|
ctx.slots.inject('conversation.session.header.utilities', () =>
|
||||||
|
ctx.slots.register(
|
||||||
|
// `order` ascends left to right, so a high value keeps the chip at
|
||||||
|
// the right-hand end of the utilities, where it was before.
|
||||||
|
{ name: 'conversation.session.header.utilities', id: 'dsh-recall', order: 20 },
|
||||||
|
(props) => h(RecallChip, props),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// The right-sidebar tab is registered in two stages, and the second is
|
// The right-sidebar tab is registered in two stages, and the second is
|
||||||
|
|||||||
+4
-2
@@ -1,7 +1,9 @@
|
|||||||
# The layer this bundle contributes to a dsh profile that lists it.
|
# The layer this bundle contributes to a dsh profile that lists it.
|
||||||
#
|
#
|
||||||
# One row: the dsh-recall host plugin, which registers exactly one tool
|
# One row: the dsh-recall host plugin, which registers exactly one tool
|
||||||
# (`recall`) on `ctx.tools` and one settings namespace on `ctx.settings`.
|
# (`recall`) on `ctx.tools`. Its settings are the `Config` schema it exports —
|
||||||
|
# the Harness keys a form per profile entry by this row's id — so there is no
|
||||||
|
# section to register and nothing to attach at load.
|
||||||
#
|
#
|
||||||
# There is deliberately NO `config:` block. Every value the plugin needs is
|
# There is deliberately NO `config:` block. Every value the plugin needs is
|
||||||
# either discovered at runtime (the workspace, from the calling session) or
|
# either discovered at runtime (the workspace, from the calling session) or
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
# - id: dsh-recall
|
# - id: dsh-recall
|
||||||
# name: dsh-recall
|
# name: dsh-recall
|
||||||
# config:
|
# config:
|
||||||
# embeddingApiBase: http://10.50.1.2:11434
|
# embeddingApiBase: http://127.0.0.1:11434 # replace with your endpoint
|
||||||
|
|
||||||
- insert:
|
- insert:
|
||||||
- id: dsh-recall
|
- id: dsh-recall
|
||||||
|
|||||||
@@ -32,10 +32,11 @@
|
|||||||
* warm-up is available as an explicit background action.
|
* warm-up is available as an explicit background action.
|
||||||
*
|
*
|
||||||
* **Why the endpoint is configuration and not a constant.** The Ollama default
|
* **Why the endpoint is configuration and not a constant.** The Ollama default
|
||||||
* of `127.0.0.1:11434` is a convention, not a fact — this deployment runs Ollama
|
* of `127.0.0.1:11434` is a convention, not a fact — a deployment often runs
|
||||||
* on another host. Baking one machine's address into a bundle layer that other
|
* Ollama on another host. Baking one machine's address into a bundle layer that
|
||||||
* machines apply is exactly the mistake the composition docs warn about, so the
|
* other machines apply is exactly the mistake the composition docs warn about,
|
||||||
* endpoint is a schema default the profile can override, at rest or live.
|
* so the endpoint is a schema default the profile can override, at rest or
|
||||||
|
* live.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFile, stat } from 'node:fs/promises'
|
import { readFile, stat } from 'node:fs/promises'
|
||||||
@@ -965,7 +966,7 @@ export function apply(ctx, config) {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
description:
|
description:
|
||||||
'Scope this whole call — index and search — to one workspace-relative directory, for example ' +
|
'Scope this whole call — index and search — to one workspace-relative directory, for example ' +
|
||||||
'"src/controllers/room/occupancy". Use it when the session is working on one module: the index ' +
|
'"src/services/billing". Use it when the session is working on one module: the index ' +
|
||||||
'then holds only that module, so its near-identical neighbours cannot outrank it and the first ' +
|
'then holds only that module, so its near-identical neighbours cannot outrank it and the first ' +
|
||||||
'search does not wait on the rest of the repository. Empty uses the configured focus, then the ' +
|
'search does not wait on the rest of the repository. Empty uses the configured focus, then the ' +
|
||||||
'nearest enclosing directory that carries a CONTROLLER.md, DEBUG_UI.md or AGENTS.md.',
|
'nearest enclosing directory that carries a CONTROLLER.md, DEBUG_UI.md or AGENTS.md.',
|
||||||
@@ -980,7 +981,7 @@ export function apply(ctx, config) {
|
|||||||
type: 'json',
|
type: 'json',
|
||||||
description:
|
description:
|
||||||
'Settings patch for action "configure", using this plugin\'s own camelCase field names, for ' +
|
'Settings patch for action "configure", using this plugin\'s own camelCase field names, for ' +
|
||||||
'example {"embeddingApiBase":"http://10.50.1.2:11434"}.',
|
'example {"embeddingApiBase":"http://127.0.0.1:11434"}.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dsh-recall",
|
"name": "dsh-recall",
|
||||||
"version": "0.1.0",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Local-embedding code recall for DeepSeek Harness — returns ranked excerpts with line ranges instead of whole files, so the context window stops growing with every read.",
|
"description": "Local-embedding code recall for DeepSeek Harness — returns ranked excerpts with line ranges instead of whole files, so the context window stops growing with every read.",
|
||||||
|
|||||||
+34
-20
@@ -19,38 +19,37 @@ import { mkdtemp, rm } from 'node:fs/promises'
|
|||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
|
|
||||||
import { apply } from '../index.js'
|
import { apply, Config, settingsSnapshot } from '../index.js'
|
||||||
|
|
||||||
const WORKSPACE = process.argv[2] ?? '/root/workspaces/ha-runtime'
|
// Whatever repository you run this against, and whatever you want to ask it.
|
||||||
const QUERY = process.argv[3] ?? 'how does the occupancy state machine decide a room is occupied'
|
// The defaults are the working directory and a query that names a behaviour
|
||||||
|
// rather than a symbol — which is the kind of question the plugin exists for.
|
||||||
|
const WORKSPACE = process.argv[2] ?? process.cwd()
|
||||||
|
const QUERY = process.argv[3] ?? 'where is the retry policy applied'
|
||||||
const BUDGET_MS = Number(process.argv[4] ?? 90_000)
|
const BUDGET_MS = Number(process.argv[4] ?? 90_000)
|
||||||
|
|
||||||
const API_BASE = process.env.RECALL_API_BASE ?? 'http://10.50.1.2:11434'
|
// The endpoint to measure against: the Ollama convention unless RECALL_API_BASE
|
||||||
|
// says otherwise. Nothing here is specific to the machine this was written on.
|
||||||
|
const API_BASE = process.env.RECALL_API_BASE ?? 'http://127.0.0.1:11434'
|
||||||
const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest'
|
const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest'
|
||||||
|
|
||||||
const config = {
|
// The measurement's configuration, stated as the few values it cares about and
|
||||||
|
// resolved through the plugin's own schema — so this script can never drift
|
||||||
|
// from the Config the plugin validates against. A hand-written literal used to
|
||||||
|
// stand here, and it was missing ten fields: harmless while nothing checked,
|
||||||
|
// and a refused call the moment `execute` began validating the section it was
|
||||||
|
// handed.
|
||||||
|
const config = settingsSnapshot(
|
||||||
|
Config({
|
||||||
embeddingApiBase: API_BASE,
|
embeddingApiBase: API_BASE,
|
||||||
embeddingModel: MODEL,
|
embeddingModel: MODEL,
|
||||||
embeddingDimensions: 768,
|
|
||||||
embeddingApiKey: '',
|
|
||||||
embeddingBatchSize: 32,
|
embeddingBatchSize: 32,
|
||||||
embeddingConcurrency: 4,
|
embeddingConcurrency: 4,
|
||||||
embeddingTimeoutMs: 120_000,
|
embeddingTimeoutMs: 120_000,
|
||||||
extensions: [],
|
|
||||||
exclude: [],
|
|
||||||
maxFiles: 20_000,
|
|
||||||
maxFileBytes: 1_048_576,
|
|
||||||
chunkLines: 48,
|
|
||||||
maxChunkLines: 80,
|
|
||||||
overlapLines: 8,
|
|
||||||
defaultLimit: 8,
|
|
||||||
maxLimit: 25,
|
|
||||||
perFileLimit: 2,
|
|
||||||
excerptLines: 40,
|
excerptLines: 40,
|
||||||
minSimilarity: 0.2,
|
|
||||||
syncBudgetMs: BUDGET_MS,
|
syncBudgetMs: BUDGET_MS,
|
||||||
stateDir: '',
|
}),
|
||||||
}
|
)
|
||||||
|
|
||||||
/** The smallest context `apply` touches, with no settings provider mounted. */
|
/** The smallest context `apply` touches, with no settings provider mounted. */
|
||||||
function context() {
|
function context() {
|
||||||
@@ -92,6 +91,21 @@ async function main() {
|
|||||||
const coldMs = Date.now() - coldStart
|
const coldMs = Date.now() - coldStart
|
||||||
console.log('--- FIRST SEARCH (bounded cold start) ---')
|
console.log('--- FIRST SEARCH (bounded cold start) ---')
|
||||||
console.log(`elapsed : ${(coldMs / 1000).toFixed(1)}s (budget ${BUDGET_MS / 1000}s)`)
|
console.log(`elapsed : ${(coldMs / 1000).toFixed(1)}s (budget ${BUDGET_MS / 1000}s)`)
|
||||||
|
|
||||||
|
// A search that did not run has no index or results to report, and the tool
|
||||||
|
// already says why. Without this the first `first.index.*` line throws a
|
||||||
|
// TypeError that buries the actual cause — an endpoint that is not there is
|
||||||
|
// the ordinary case now that the default is the loopback convention.
|
||||||
|
if (first.ok !== true) {
|
||||||
|
console.log(`failed : ${first.errorCode ?? 'UNKNOWN'}`)
|
||||||
|
console.log(`note : ${first.note ?? '(none)'}`)
|
||||||
|
if (first.remedy !== undefined) console.log(`remedy : ${first.remedy}`)
|
||||||
|
console.log('')
|
||||||
|
console.log(`nothing to measure: point RECALL_API_BASE at a reachable endpoint (currently ${API_BASE}).`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`scanned files : ${first.index.scanned}`)
|
console.log(`scanned files : ${first.index.scanned}`)
|
||||||
console.log(`indexed files : ${first.index.files}`)
|
console.log(`indexed files : ${first.index.files}`)
|
||||||
console.log(`chunks : ${first.index.chunks}`)
|
console.log(`chunks : ${first.index.chunks}`)
|
||||||
|
|||||||
@@ -6,10 +6,9 @@
|
|||||||
* cold-start cost they imply are all arithmetic over the chunker, which is
|
* cold-start cost they imply are all arithmetic over the chunker, which is
|
||||||
* deterministic and offline.
|
* deterministic and offline.
|
||||||
*
|
*
|
||||||
* The cold-start projection uses the throughput actually measured against the
|
* The cold-start projection uses a throughput measured against a live endpoint
|
||||||
* real endpoint on this repository: 2,257 chunks embedded in 90s, about 25
|
* during development: 2,257 chunks embedded in 90s, about 25 chunks/s. It is a
|
||||||
* chunks/s. It is a projection from a real figure, not a guess, and it is
|
* projection from a real figure, not a guess, and it is labelled as one.
|
||||||
* labelled as a projection.
|
|
||||||
*
|
*
|
||||||
* node scripts/scope-measure.mjs [workspace]
|
* node scripts/scope-measure.mjs [workspace]
|
||||||
*/
|
*/
|
||||||
@@ -20,9 +19,15 @@ import { join } from 'node:path'
|
|||||||
import { chunkFile } from '../lib/chunk.js'
|
import { chunkFile } from '../lib/chunk.js'
|
||||||
import { listCodeFiles } from '../lib/walk.js'
|
import { listCodeFiles } from '../lib/walk.js'
|
||||||
|
|
||||||
const WORKSPACE = process.argv[2] ?? '/root/workspaces/ha-runtime'
|
// The repository to measure; the working directory by default, so the script
|
||||||
|
// carries no assumption about where anyone keeps their code.
|
||||||
|
const WORKSPACE = process.argv[2] ?? process.cwd()
|
||||||
|
|
||||||
/** Chunks per second, measured on this repository against the live endpoint. */
|
/**
|
||||||
|
* Chunks per second, measured against a live endpoint during development
|
||||||
|
* (2,257 chunks in 90s). A measured constant, not a guess — and a projection
|
||||||
|
* input, which is why the cold-start figures below are labelled as projections.
|
||||||
|
*/
|
||||||
const MEASURED_CHUNKS_PER_SECOND = 2257 / 90
|
const MEASURED_CHUNKS_PER_SECOND = 2257 / 90
|
||||||
|
|
||||||
/** Count files and chunks for one directory, relative to a base for display. */
|
/** Count files and chunks for one directory, relative to a base for display. */
|
||||||
@@ -98,6 +103,18 @@ async function main() {
|
|||||||
if (scopes.length > 10) console.log(` … and ${scopes.length - 10} more`)
|
if (scopes.length > 10) console.log(` … and ${scopes.length - 10} more`)
|
||||||
console.log('')
|
console.log('')
|
||||||
|
|
||||||
|
// A workspace with no marker files has no scopes to compare, and that is a
|
||||||
|
// normal answer rather than a failure — the plugin falls back to the whole
|
||||||
|
// workspace there. Say so and stop, instead of dividing by the scope that
|
||||||
|
// does not exist.
|
||||||
|
if (scopes.length === 0) {
|
||||||
|
console.log('=== WHAT SCOPING BUYS ===')
|
||||||
|
console.log(' nothing here: no directory in this workspace carries a focus marker,')
|
||||||
|
console.log(' so every call indexes the whole workspace. Add a marker file (see')
|
||||||
|
console.log(' focusMarkers in the README) to a module you work on to create one.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const median = scopes[Math.floor(scopes.length / 2)]
|
const median = scopes[Math.floor(scopes.length / 2)]
|
||||||
const totalScoped = scopes.reduce((sum, scope) => sum + scope.chunks, 0)
|
const totalScoped = scopes.reduce((sum, scope) => sum + scope.chunks, 0)
|
||||||
console.log('=== WHAT SCOPING BUYS ===')
|
console.log('=== WHAT SCOPING BUYS ===')
|
||||||
|
|||||||
+4
-1
@@ -19,7 +19,10 @@ import { rankChunks } from '../lib/rank.js'
|
|||||||
import { RecallIndex } from '../lib/store.js'
|
import { RecallIndex } from '../lib/store.js'
|
||||||
import { listCodeFiles } from '../lib/walk.js'
|
import { listCodeFiles } from '../lib/walk.js'
|
||||||
|
|
||||||
const API_BASE = process.env.RECALL_API_BASE ?? 'http://10.50.1.2:11434'
|
// The endpoint to check against. The default is the Ollama convention, which
|
||||||
|
// is what the plugin's own schema defaults to; point RECALL_API_BASE at your
|
||||||
|
// own host when it is somewhere else.
|
||||||
|
const API_BASE = process.env.RECALL_API_BASE ?? 'http://127.0.0.1:11434'
|
||||||
const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest'
|
const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest'
|
||||||
const DIMENSIONS = Number(process.env.RECALL_DIMENSIONS ?? 768)
|
const DIMENSIONS = Number(process.env.RECALL_DIMENSIONS ?? 768)
|
||||||
|
|
||||||
|
|||||||
+50
-25
@@ -7,10 +7,13 @@
|
|||||||
* leaving the page to be eyeballed in a running app, because the failures this
|
* leaving the page to be eyeballed in a running app, because the failures this
|
||||||
* file exists to catch are structural and invisible until someone opens Plugins:
|
* file exists to catch are structural and invisible until someone opens Plugins:
|
||||||
*
|
*
|
||||||
* - the configuration page is registered into `plugins.row.config` under
|
* - the configuration page is registered into `plugins.bundle.config` under
|
||||||
* `<bundle package>#<row id>`. The 0.1.5 seat (`settings.plugin.item`, bound
|
* the bundle's package name, which is the seat the Plugins page draws as a
|
||||||
* through `settingsScope`) does not exist in 0.1.7, so a page left in it
|
* section on the plugin's own card. The 0.1.5 seat (`settings.plugin.item`,
|
||||||
* registers against a slot nobody declares and simply never appears.
|
* bound through `settingsScope`) does not exist in 0.1.7, so a page left in
|
||||||
|
* it registers against a slot nobody declares and simply never appears; the
|
||||||
|
* sibling seat `plugins.row.config` renders one click deeper instead, on a
|
||||||
|
* single row's page, which is not where someone opening the card looks.
|
||||||
* - the right-sidebar tab needs a TYPE registered before its body means
|
* - the right-sidebar tab needs a TYPE registered before its body means
|
||||||
* anything: the keyed body slot is dispatched under the definition's `id`.
|
* anything: the keyed body slot is dispatched under the definition's `id`.
|
||||||
* - the field list here and the `Config` schema in index.js must name the same
|
* - the field list here and the `Config` schema in index.js must name the same
|
||||||
@@ -106,6 +109,11 @@ function makePrimitives(record) {
|
|||||||
|
|
||||||
bind(project) {
|
bind(project) {
|
||||||
this.project = project
|
this.project = project
|
||||||
|
// Faithful to the framework: `bind` is not a subscription setter, it
|
||||||
|
// builds the snapshot store from ONE immediate projection. A stub that
|
||||||
|
// only stored the projection would let a controller read a field it has
|
||||||
|
// not assigned yet — which is exactly the bug this reproduces.
|
||||||
|
project()
|
||||||
return { getSnapshot: () => project(), subscribe: () => () => {} }
|
return { getSnapshot: () => project(), subscribe: () => () => {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +330,7 @@ function mount(options = {}) {
|
|||||||
* does, so the component under test sees the props a real render would give it.
|
* does, so the component under test sees the props a real render would give it.
|
||||||
*/
|
*/
|
||||||
const card = (view) => {
|
const card = (view) => {
|
||||||
const item = byName.get('plugins.row.config')
|
const item = byName.get('plugins.bundle.config')
|
||||||
assert.ok(item !== undefined, 'no settings page was registered')
|
assert.ok(item !== undefined, 'no settings page was registered')
|
||||||
const props = { ...item.meta.inject(), view }
|
const props = { ...item.meta.inject(), view }
|
||||||
delete props.hooks
|
delete props.hooks
|
||||||
@@ -364,13 +372,15 @@ test('the client module claims the slot registry in its inject list', () => {
|
|||||||
assert.deepEqual(module.inject, ['slots'])
|
assert.deepEqual(module.inject, ['slots'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the configuration page claims the 0.1.7 seat, keyed as the patch declares it', () => {
|
test('the configuration page claims the seat the plugin card draws', () => {
|
||||||
const { registered } = mount()
|
const { registered } = mount()
|
||||||
const page = registered.find((item) => item.meta.name === 'plugins.row.config')
|
const page = registered.find((item) => item.meta.name === 'plugins.bundle.config')
|
||||||
assert.ok(page !== undefined, 'the page must register into plugins.row.config')
|
assert.ok(page !== undefined, 'the page must register into plugins.bundle.config')
|
||||||
// `<bundle package name>#<row id>`: the row id is the one this bundle's own
|
// Keyed by the BUNDLE's package name, which is what the Plugins page
|
||||||
// cordis.patch.yml declares, which is what the Plugins page dispatches by.
|
// dispatches this seat with (`entryKey: pkg.name`). The sibling seat,
|
||||||
assert.equal(page.meta.key, 'dsh-recall#dsh-recall')
|
// `plugins.row.config`, would be keyed `<package>#<row id>` and would render
|
||||||
|
// one click deeper, on the row's own page.
|
||||||
|
assert.equal(page.meta.key, 'dsh-recall')
|
||||||
assert.equal(typeof page.meta.inject, 'function')
|
assert.equal(typeof page.meta.inject, 'function')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -385,7 +395,7 @@ test('no page is registered for a namespace the Host does not serve', () => {
|
|||||||
// onto fields nothing would accept.
|
// onto fields nothing would accept.
|
||||||
const { registered } = mount({ served: ['some-other-plugin'] })
|
const { registered } = mount({ served: ['some-other-plugin'] })
|
||||||
assert.equal(
|
assert.equal(
|
||||||
registered.some((item) => item.meta.name === 'plugins.row.config'),
|
registered.some((item) => item.meta.name === 'plugins.bundle.config'),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -395,17 +405,32 @@ test('the chip and the sidebar tab survive a deployment with no settings provide
|
|||||||
// profile without a settings provider loses the page and keeps the rest.
|
// profile without a settings provider loses the page and keeps the rest.
|
||||||
const { registered, tabTypes } = mount({ withConfigForms: false })
|
const { registered, tabTypes } = mount({ withConfigForms: false })
|
||||||
assert.deepEqual(registered.map((item) => item.meta.name).sort(), [
|
assert.deepEqual(registered.map((item) => item.meta.name).sort(), [
|
||||||
'conversation.session.header.corner',
|
'conversation.session.header.utilities',
|
||||||
'sidebar.right.pane.tab',
|
'sidebar.right.pane.tab',
|
||||||
])
|
])
|
||||||
assert.equal(tabTypes.length, 1, 'the tab type is independent of settings')
|
assert.equal(tabTypes.length, 1, 'the tab type is independent of settings')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the header chip is a single-seat contribution with no key', () => {
|
test('the header chip is an additive list entry carrying its own id', () => {
|
||||||
|
// The header's far-right corner is a SINGLE cell, and the product already
|
||||||
|
// occupies it with its right-sidebar expand control — the client catalog
|
||||||
|
// reports that key as `replaceRisk: "shadows-shipped-ui"`. A second
|
||||||
|
// registration at the same priority is a hard error inside the registry,
|
||||||
|
// thrown while the OTHER plugin's `slots.inject()` callback runs: the
|
||||||
|
// renderer catches it, stops that controller for the rest of the fiber's
|
||||||
|
// life, and rethrows it uncaught. So the chip belongs in a list cell, where
|
||||||
|
// a unique id makes it additive.
|
||||||
const { registered } = mount()
|
const { registered } = mount()
|
||||||
const chip = registered.find((item) => item.meta.name === 'conversation.session.header.corner')
|
const chip = registered.find((item) => item.meta.name === 'conversation.session.header.utilities')
|
||||||
assert.ok(chip !== undefined)
|
assert.ok(chip !== undefined)
|
||||||
assert.equal(chip.meta.key, undefined, 'the corner is a single seat, so it takes no key')
|
assert.equal(chip.meta.id, 'dsh-recall', 'a list cell is addressed by its id')
|
||||||
|
assert.equal(typeof chip.meta.order, 'number', 'the order places the cell within the list')
|
||||||
|
assert.equal(chip.meta.key, undefined, 'a list cell takes an id, not a key')
|
||||||
|
assert.deepEqual(
|
||||||
|
registered.filter((item) => item.meta.name === 'conversation.session.header.corner'),
|
||||||
|
[],
|
||||||
|
'the corner is a single cell the product owns; a registration there shadows shipped UI',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the sidebar tab registers a type AND a body under the same id', () => {
|
test('the sidebar tab registers a type AND a body under the same id', () => {
|
||||||
@@ -430,8 +455,8 @@ test('the sidebar tab registers a type AND a body under the same id', () => {
|
|||||||
test('the browser half contributes three seats, none of them the retired 0.1.5 one', () => {
|
test('the browser half contributes three seats, none of them the retired 0.1.5 one', () => {
|
||||||
const { registered } = mount()
|
const { registered } = mount()
|
||||||
assert.deepEqual(registered.map((item) => item.meta.name).sort(), [
|
assert.deepEqual(registered.map((item) => item.meta.name).sort(), [
|
||||||
'conversation.session.header.corner',
|
'conversation.session.header.utilities',
|
||||||
'plugins.row.config',
|
'plugins.bundle.config',
|
||||||
'sidebar.right.pane.tab',
|
'sidebar.right.pane.tab',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -580,17 +605,17 @@ function usageView(overrides = {}) {
|
|||||||
|
|
||||||
test('the header chip renders nothing when the conversation never used it', () => {
|
test('the header chip renders nothing when the conversation never used it', () => {
|
||||||
const { renderSlot } = mount()
|
const { renderSlot } = mount()
|
||||||
// The corner is laid out only while its occupant renders something, so this
|
// The utilities list draws nothing for an entry that renders nothing, so
|
||||||
// is the state that must cost no space at all.
|
// this is the state that must cost no space at all.
|
||||||
assert.equal(renderSlot('conversation.session.header.corner', { useProjection: () => undefined }), null)
|
assert.equal(renderSlot('conversation.session.header.utilities', { useProjection: () => undefined }), null)
|
||||||
assert.equal(renderSlot('conversation.session.header.corner', { useProjection: () => usageView({ searches: 0, excerpts: 0 }) }), null)
|
assert.equal(renderSlot('conversation.session.header.utilities', { useProjection: () => usageView({ searches: 0, excerpts: 0 }) }), null)
|
||||||
// And a host without the projection service at all.
|
// And a host without the projection service at all.
|
||||||
assert.equal(renderSlot('conversation.session.header.corner', {}), null)
|
assert.equal(renderSlot('conversation.session.header.utilities', {}), null)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the header chip leads with what was kept out, not what it cost', () => {
|
test('the header chip leads with what was kept out, not what it cost', () => {
|
||||||
const { renderSlot } = mount()
|
const { renderSlot } = mount()
|
||||||
const chip = renderSlot('conversation.session.header.corner', { useProjection: () => usageView() })
|
const chip = renderSlot('conversation.session.header.utilities', { useProjection: () => usageView() })
|
||||||
assert.ok(chip !== null)
|
assert.ok(chip !== null)
|
||||||
const text = String(chip.props.children)
|
const text = String(chip.props.children)
|
||||||
assert.match(text, /Recall 3/)
|
assert.match(text, /Recall 3/)
|
||||||
@@ -603,7 +628,7 @@ test('the header chip leads with what was kept out, not what it cost', () => {
|
|||||||
|
|
||||||
test('the chip tooltip carries the figures the chip has no room for', () => {
|
test('the chip tooltip carries the figures the chip has no room for', () => {
|
||||||
const { renderSlot } = mount()
|
const { renderSlot } = mount()
|
||||||
const chip = renderSlot('conversation.session.header.corner', { useProjection: () => usageView({ rereadTokens: 500_000 }) })
|
const chip = renderSlot('conversation.session.header.utilities', { useProjection: () => usageView({ rereadTokens: 500_000 }) })
|
||||||
const detail = chip.props.title
|
const detail = chip.props.title
|
||||||
assert.match(detail, /3 searches/)
|
assert.match(detail, /3 searches/)
|
||||||
assert.match(detail, /9 excerpts from 5 files/)
|
assert.match(detail, /9 excerpts from 5 files/)
|
||||||
|
|||||||
+5
-3
@@ -42,8 +42,10 @@ test('Config fills every field from an empty input, so apply can rely on default
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Config keeps an explicit value and fills the rest', () => {
|
test('Config keeps an explicit value and fills the rest', () => {
|
||||||
const resolved = resolve({ embeddingApiBase: 'http://10.50.1.2:11434', embeddingConcurrency: 8 })
|
// A TEST-NET-1 address (RFC 5737): reserved for documentation, so a fixture
|
||||||
assert.equal(resolved.embeddingApiBase, 'http://10.50.1.2:11434')
|
// can never point at a host anybody actually runs.
|
||||||
|
const resolved = resolve({ embeddingApiBase: 'http://192.0.2.10:11434', embeddingConcurrency: 8 })
|
||||||
|
assert.equal(resolved.embeddingApiBase, 'http://192.0.2.10:11434')
|
||||||
assert.equal(resolved.embeddingConcurrency, 8)
|
assert.equal(resolved.embeddingConcurrency, 8)
|
||||||
assert.equal(resolved.embeddingBatchSize, 16)
|
assert.equal(resolved.embeddingBatchSize, 16)
|
||||||
assert.equal(resolved.embeddingTimeoutMs, 30_000)
|
assert.equal(resolved.embeddingTimeoutMs, 30_000)
|
||||||
@@ -135,7 +137,7 @@ test('validateSettings rejects an endpoint that is not a URL', () => {
|
|||||||
// so it is the protocol check that actually catches the common mistake.
|
// so it is the protocol check that actually catches the common mistake.
|
||||||
assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: 'localhost:11434' })), /must be http or https/)
|
assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: 'localhost:11434' })), /must be http or https/)
|
||||||
assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: '' })), /not a URL/)
|
assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: '' })), /not a URL/)
|
||||||
assert.doesNotThrow(() => validateSettings(settingsWith({ embeddingApiBase: 'http://10.50.1.2:11434' })))
|
assert.doesNotThrow(() => validateSettings(settingsWith({ embeddingApiBase: 'http://192.0.2.10:11434' })))
|
||||||
})
|
})
|
||||||
|
|
||||||
test('validateSettings rejects a non-http protocol', () => {
|
test('validateSettings rejects a non-http protocol', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user