From a48e05585e8c58d5b910f5480553862ae6fb3d65 Mon Sep 17 00:00:00 2001 From: NotExpectedYet Date: Sat, 26 Sep 2026 06:47:25 +0100 Subject: [PATCH] fix: remove one machine's addresses and paths from the shipped defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin was developed against an Ollama on a private LAN address, and that address had reached places that ship: - the `config` parameter description the MODEL reads on every request; - the commented example in `cordis.patch.yml`, the layer every profile applies; - `scripts/smoke.mjs` and `scripts/measure.mjs` endpoint defaults; - both measure scripts' default workspace, which was an absolute local path and named a private repository; - README examples and test fixtures. All of them now use `http://127.0.0.1:11434`, which is Ollama's documented default and belongs to no particular machine — kept rather than emptied because `validateSettings` requires an http(s) URL, so an empty default would turn every call into INVALID_SETTINGS until someone configured it. Test fixtures use 192.0.2.10 instead: TEST-NET-1 is reserved for documentation and cannot point at a host anybody runs. The `focus` and `config` examples are generic, and the workspace defaults are the working directory. Two script bugs surfaced once the defaults stopped pointing at a live box, and are fixed here. `measure.mjs` built its own configuration literal, which was ten fields short of the schema — harmless while nothing validated it, and a refused call the moment `execute` began checking the section it was handed; it now resolves its configuration through the plugin's own `Config`, so it cannot drift again. It also crashed on a failed search (`first.index.scanned` of a result that has no index) instead of reporting the endpoint it could not reach, which is now the ordinary case. `scope-measure.mjs` divided by a scope that does not exist in any workspace without focus markers, and now says so. Also corrects `cordis.patch.yml`'s description of what the row registers: in 0.1.7 the exported `Config` is the settings registration, not a namespace on `ctx.settings`. --- README.md | 12 +++++-- cordis.patch.yml | 6 ++-- index.js | 13 ++++---- scripts/measure.mjs | 68 +++++++++++++++++++++++---------------- scripts/scope-measure.mjs | 29 +++++++++++++---- scripts/smoke.mjs | 5 ++- test/config.test.js | 8 +++-- 7 files changed, 93 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 89b734d..122d2b4 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ a fact: - id: dsh-recall name: dsh-recall 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 @@ -92,7 +92,7 @@ document, so the value survives a restart and stays visible in 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 @@ -391,11 +391,17 @@ would publish. ## Development ```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/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` import `index.js`, which imports `@deepseek-ai/dsh-tools` and `@deepseek-ai/schemastery` — resolvable once the plugin is installed into a diff --git a/cordis.patch.yml b/cordis.patch.yml index 57d91d3..0ec6f3f 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -1,7 +1,9 @@ # The layer this bundle contributes to a dsh profile that lists it. # # 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 # either discovered at runtime (the workspace, from the calling session) or @@ -14,7 +16,7 @@ # - id: dsh-recall # name: dsh-recall # config: -# embeddingApiBase: http://10.50.1.2:11434 +# embeddingApiBase: http://127.0.0.1:11434 # replace with your endpoint - insert: - id: dsh-recall diff --git a/index.js b/index.js index d6d9c01..b4e5d7d 100644 --- a/index.js +++ b/index.js @@ -32,10 +32,11 @@ * warm-up is available as an explicit background action. * * **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 - * on another host. Baking one machine's address into a bundle layer that other - * machines apply is exactly the mistake the composition docs warn about, so the - * endpoint is a schema default the profile can override, at rest or live. + * of `127.0.0.1:11434` is a convention, not a fact — a deployment often runs + * Ollama on another host. Baking one machine's address into a bundle layer that + * other machines apply is exactly the mistake the composition docs warn about, + * so the endpoint is a schema default the profile can override, at rest or + * live. */ import { readFile, stat } from 'node:fs/promises' @@ -965,7 +966,7 @@ export function apply(ctx, config) { type: 'string', description: '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 ' + '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.', @@ -980,7 +981,7 @@ export function apply(ctx, config) { type: 'json', description: '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: { diff --git a/scripts/measure.mjs b/scripts/measure.mjs index 7938a1d..6c23822 100644 --- a/scripts/measure.mjs +++ b/scripts/measure.mjs @@ -19,38 +19,37 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' 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' -const QUERY = process.argv[3] ?? 'how does the occupancy state machine decide a room is occupied' +// Whatever repository you run this against, and whatever you want to ask it. +// 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 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 config = { - embeddingApiBase: API_BASE, - embeddingModel: MODEL, - embeddingDimensions: 768, - embeddingApiKey: '', - embeddingBatchSize: 32, - embeddingConcurrency: 4, - 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, - minSimilarity: 0.2, - syncBudgetMs: BUDGET_MS, - stateDir: '', -} +// 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, + embeddingModel: MODEL, + embeddingBatchSize: 32, + embeddingConcurrency: 4, + embeddingTimeoutMs: 120_000, + excerptLines: 40, + syncBudgetMs: BUDGET_MS, + }), +) /** The smallest context `apply` touches, with no settings provider mounted. */ function context() { @@ -92,6 +91,21 @@ async function main() { const coldMs = Date.now() - coldStart console.log('--- FIRST SEARCH (bounded cold start) ---') 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(`indexed files : ${first.index.files}`) console.log(`chunks : ${first.index.chunks}`) diff --git a/scripts/scope-measure.mjs b/scripts/scope-measure.mjs index 019cefd..99d4adb 100644 --- a/scripts/scope-measure.mjs +++ b/scripts/scope-measure.mjs @@ -6,10 +6,9 @@ * cold-start cost they imply are all arithmetic over the chunker, which is * deterministic and offline. * - * The cold-start projection uses the throughput actually measured against the - * real endpoint on this repository: 2,257 chunks embedded in 90s, about 25 - * chunks/s. It is a projection from a real figure, not a guess, and it is - * labelled as a projection. + * The cold-start projection uses a throughput measured against a live endpoint + * during development: 2,257 chunks embedded in 90s, about 25 chunks/s. It is a + * projection from a real figure, not a guess, and it is labelled as one. * * node scripts/scope-measure.mjs [workspace] */ @@ -20,9 +19,15 @@ import { join } from 'node:path' import { chunkFile } from '../lib/chunk.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 /** 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`) 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 totalScoped = scopes.reduce((sum, scope) => sum + scope.chunks, 0) console.log('=== WHAT SCOPING BUYS ===') diff --git a/scripts/smoke.mjs b/scripts/smoke.mjs index 608f67a..fe519de 100644 --- a/scripts/smoke.mjs +++ b/scripts/smoke.mjs @@ -19,7 +19,10 @@ import { rankChunks } from '../lib/rank.js' import { RecallIndex } from '../lib/store.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 DIMENSIONS = Number(process.env.RECALL_DIMENSIONS ?? 768) diff --git a/test/config.test.js b/test/config.test.js index 3c0369d..4ebc3f0 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -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', () => { - const resolved = resolve({ embeddingApiBase: 'http://10.50.1.2:11434', embeddingConcurrency: 8 }) - assert.equal(resolved.embeddingApiBase, 'http://10.50.1.2:11434') + // A TEST-NET-1 address (RFC 5737): reserved for documentation, so a fixture + // 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.embeddingBatchSize, 16) 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. assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: 'localhost:11434' })), /must be http or https/) 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', () => {