Skip to content

Commit cb005e0

Browse files
huangyiireneclaude
andauthored
fix(knowledge-ragflow): read source.adapterConfig.datasetId, the declared key (#19251)
Fixes #18973 Clause-②: no Ruled on-card: batch #160 item 2, **letter A**, maintainer 「同意」 2026-09-18 (`issuecomment-5729652660`) — "the spec wins: `@objectstack/knowledge-ragflow` reads `source.adapterConfig.datasetId`, its README says the same, and `KnowledgeSourceSchema` is untouched". `packages/spec` is untouched here, as ruled. ## What was wrong `extractRagflowOptions` cast the source to a shape it does not have and read `options.datasetId`: ```ts // RECORD_OF_UNKNOWN stands for the record-of-string-to-unknown generic; the // angle-bracket spelling is avoided because this platform rewrites such // fragments in a body, inside a fence as readily as outside one. const opts = ((source as unknown as { options?: RECORD_OF_UNKNOWN }).options ?? {}) as RECORD_OF_UNKNOWN; ``` `KnowledgeSourceSchema` declares `adapterConfig` for adapter-specific configuration and is a plain `z.object` with no `.passthrough()`, so any path that parses a source drops `options` before an adapter sees it. The adapter worked only because nothing parses a source today. The published README documented the undeclared spelling, which made it the one block of #18915's 44 that could not be repaired: correcting the word alone would have compiled and stopped working. ## What changed - **`src/index.ts`** — the cast is gone. `extractRagflowOptions` reads `source.adapterConfig`, a declared, already-typed property, so no cast is needed at all. There is no fallback that also reads `options` (Prime Directive #12 — no lenient consumer). The refusal now names the declared key: `RAGFlow adapter requires source.adapterConfig.datasetId on source 'SOURCE_ID'` (the source's own id interpolated), so a host on the old spelling is told what to write instead of retrieving nothing. - **`README.md`** — the example and the "Source binding" sentence move to `adapterConfig`, and the block now compiles against the package (evidence below). - **`src/__tests__/ragflow-adapter.test.ts`** — the three `KnowledgeSource` literals move with it; the refusal test now pins the message text (the ruling relies on it naming the key, so the wording is contractual here), and a new test pins that a source carrying only the legacy `options` spelling is refused and reaches no transport at all. - **`test-typecheck-debt.json`** — emptied. See below; this was not an incidental repair. - **Changeset** — `@objectstack/knowledge-ragflow` patch, behaviour: reads the declared key, with the `FROM` → `TO` mapping in the body. ## Four call sites, one repair — and a correction to the dispatch order The card body names one call site (`:108`). There are four sites on `origin/main` — the definition at `:60` and calls at `:108`, `:136`, `:144` — and the dispatch order carried that as "a repair that fixes one call site is not the repair". Measured on this branch's base: **no call site needed editing.** All three calls pass the whole `source` and destructure the result; the undeclared key was read in exactly one place, the definition's cast. The four-site count is correct and the inference drawn from it is not: the repair is one function body, and it covers all three callers. `grep -n "options" src/index.ts` after the change returns one line — `pass options.fetch` in the adapter constructor's own error, which is about `KnowledgeRagflowAdapterOptions` and not about a `KnowledgeSource`. ## The type-debt ledger was this same defect, frozen `packages/plugins/knowledge-ragflow/test-typecheck-debt.json` pinned 3 errors in one file, all `TS2353: Object literal may only specify known properties, and 'options' does not exist in type '…'`. Those three errors *were* this card: the test wrote the undeclared key because the adapter read it. With the adapter on `adapterConfig` the file graduates to zero, and the ledger is shrink-only — a graduated file is red until its entry is deleted. `check:test-typecheck` said so in as many words, and the entry is now gone. The file is kept with an empty `entries` map (it is the per-file ledger the gate reads for this package), and its authored `_note` records the graduation; the note survives regeneration, verified by running the generator twice. ## Two shipped surfaces already said `adapterConfig` The adapter was the outlier, not the schema. Both of these ship today: - `packages/services/service-settings/src/manifests/knowledge.manifest.ts:97` — "Per-source values on **KnowledgeSource.adapterConfig** take precedence", in all four translated locales. - `skills/objectstack-ai/SKILL.md` — "they belong to the adapter (`adapterConfig`) or application code", above an example that calls `KnowledgeSourceSchema.parse({ … adapter: 'ragflow' … })`. Neither is touched by this PR; they are cited because they make the ruled direction the one that leaves the repo self-consistent. ## Evidence **The README block compiles, and the measurement can fail.** `measure-markdown-ts-blocks` is a census, not a gate, so a bare green from it is worth little — it was run in both directions, at `6f17a4f13`: | run | result | |---|---| | README as landed here | 1 file / 1 TS block, **RAW fail 0, TOLERANT fail 0, WELL-FORMED AND WRONG 0** | | README ablated back to `options` (via `scripts/ablation-replace.mjs`, anchor hit 1→0, blob `c37eadb4744e` → `55123d3e9226`) | **TOLERANT fail 1 (100%), WELL-FORMED AND WRONG 1, `TS2353 x1`** | The instrument's own `FIRING_CONTROL` reported 5 diagnostics on both runs, so the zero is a reading and not a dead search. Restore was proven byte-identical by `git hash-object` (`c37eadb4744e…` before and after) with `git diff HEAD` clean, not by the wrapper's exit code. **Gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived **60 families** from this change set; all 60 were run and all recorded **exit 0**, reconciled with `--ran`: `60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN` — a derived zero, since every family recorded its code. Two answered **exit 3 (PREREQUISITE NOT MET — not a pass)** on the first pass, `check:dual-build-cjs-loads` and `check:type-check-debt`; both state the same prerequisite, a built workspace. It was cleared (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 72/72 successful) and both re-run green. **Per-package.** `pnpm --filter @objectstack/knowledge-ragflow test` → 10 passed (was 9). `pnpm --filter @objectstack/knowledge-ragflow typecheck` → exit 0 across both of its legs (`tsc --noEmit`, then `check:test-typecheck`, which reports 0 files / 0 errors / 0 pinned signatures). `pnpm lint` over the whole repo → exit 0. ## Acceptance notes Noted here, not fixed, not filed by this PR: - **`content/docs/ai/knowledge-rag.mdx:37` still writes `options: { datasetId: 'rgf_doc_dataset' }`** on a `ragflow` source. It is the same defect in a second, hand-written document, and after this PR it is a live one: an author copying it now gets a source the adapter refuses by name. It is outside this card's file surface (`packages/plugins/knowledge-ragflow/`), and `content/docs/**` brings its own gate family, so it is reported to the dispatching seat to file rather than ridden in here. - **`.changeset/18915-published-readme-examples-compile.md` closes with "One block is deliberately left"**, describing this README. That changeset belongs to PR #18968 and covers 20 other packages; its text becomes stale when both land. Left alone deliberately — it accurately records what that PR did — and the closure is stated in this PR's own changeset instead. - **This adapter throws bare `Error`s, with no ADR-0112 envelope** (`code` / `status`) on any refusal, so the new refusal test pins the message text rather than an envelope. Introducing an envelope on this seam is a contract decision well outside a patch to one adapter's key spelling. - Triage's escalation condition on the card (a real path parsing `KnowledgeSourceSchema` ⇒ p1) is **still unmet in runtime code**: `git grep KnowledgeSourceSchema` outside `packages/spec` returns zero importers in `packages/**/*.ts` (positive control: 8,741 `*Schema.parse|safeParse` call sites repo-wide). The only parse call sites are `packages/spec`'s own tests and a published-skill example. --- _Generated by [Claude Code](https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF)_ --------- Co-authored-by: claude <noreply@anthropic.com>
1 parent 7056ca5 commit cb005e0

5 files changed

Lines changed: 64 additions & 21 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/knowledge-ragflow": patch
3+
---
4+
5+
The RAGFlow adapter now reads the declared key: a source's RAGFlow binding comes from `adapterConfig.datasetId`, not `options.datasetId`.
6+
7+
`KnowledgeSourceSchema` declares `adapterConfig` for adapter-specific configuration and is a plain `z.object` — it carries no `.passthrough()`, so any path that parses a source drops `options` before an adapter ever sees it. The adapter read `options` through a cast, which worked only because no path parses a source today. The cast is gone; there is no fallback that also reads `options` (Prime Directive #12 — one strict contract, no lenient consumer).
8+
9+
Migration, `FROM` → `TO`, one line per source:
10+
11+
```ts
12+
// FROM
13+
{ id: 'product_docs', adapter: 'ragflow', options: { datasetId: 'rgf_…' } }
14+
// TO
15+
{ id: 'product_docs', adapter: 'ragflow', adapterConfig: { datasetId: 'rgf_…' } }
16+
```
17+
18+
The same move applies to `rerankModel`, `similarityThreshold` and `vectorSimilarityWeight`, which the adapter reads from the same bag. A source left on the old spelling is refused by name — `RAGFlow adapter requires source.adapterConfig.datasetId on source '<id>'` — rather than silently retrieving nothing, so the upgrade is self-describing at the first call. Nothing an author could declare is removed: `options` was never a key `KnowledgeSourceSchema` accepted, which is why this carries no ADR-0087 conversion.
19+
20+
The package's published `README.md` moves with the adapter and now compiles against it — it was the one block of the 44 that #18915 could not repair, because correcting the spelling alone would have compiled and stopped working.
21+
22+
Clause-②: no

‎packages/plugins/knowledge-ragflow/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ kernel.use(new KnowledgeServicePlugin({
2525
label: 'Product documentation',
2626
adapter: 'ragflow',
2727
source: { kind: 'http', urls: ['https://docs.example.com/sitemap.xml'] },
28-
options: { datasetId: 'rgf_doc_dataset_id' }, // RAGFlow dataset to bind
28+
adapterConfig: { datasetId: 'rgf_doc_dataset_id' }, // RAGFlow dataset to bind
2929
}],
3030
}));
3131
kernel.use(new KnowledgeRagflowPlugin({
@@ -36,7 +36,7 @@ kernel.use(new KnowledgeRagflowPlugin({
3636

3737
## Source binding
3838

39-
Each `KnowledgeSource` must include `options.datasetId` pointing to a pre-created RAGFlow dataset. The adapter doesn't create datasets — operators do that once in the RAGFlow UI, where they pick the chunking method, embedding model, and rerank policy.
39+
Each `KnowledgeSource` must include `adapterConfig.datasetId` pointing to a pre-created RAGFlow dataset. `adapterConfig` is the key `KnowledgeSourceSchema` declares for adapter-specific configuration; a source that spells it anything else loses it on any parsing path, and the adapter refuses it by name. The adapter doesn't create datasets — operators do that once in the RAGFlow UI, where they pick the chunking method, embedding model, and rerank policy.
4040

4141
## What the adapter does
4242

‎packages/plugins/knowledge-ragflow/src/__tests__/ragflow-adapter.test.ts‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const source: KnowledgeSource = {
99
label: 'Docs',
1010
adapter: 'ragflow',
1111
source: { kind: 'http', urls: ['https://docs.example.com'] } as KnowledgeSource['source'],
12-
options: { datasetId: 'ds_42' },
12+
adapterConfig: { datasetId: 'ds_42' },
1313
};
1414

1515
function fakeFetch(handler: (url: string, init?: any) => unknown): { fetch: FetchLike; calls: Array<{ url: string; init: any }> } {
@@ -30,11 +30,30 @@ function fakeFetch(handler: (url: string, init?: any) => unknown): { fetch: Fetc
3030
}
3131

3232
describe('KnowledgeRagflowAdapter', () => {
33-
it('rejects sources without datasetId', async () => {
33+
it('rejects sources without datasetId, naming the declared key', async () => {
3434
const { fetch } = fakeFetch(() => ({}));
3535
const a = new KnowledgeRagflowAdapter({ endpoint: 'http://x', apiKey: 'k', fetch });
36-
const bad: KnowledgeSource = { ...source, options: {} as Record<string, unknown> };
37-
await expect(a.search('q', { source: bad, topK: 1 })).rejects.toThrow(/datasetId/);
36+
const bad: KnowledgeSource = { ...source, adapterConfig: {} };
37+
// The refusal text is the migration notice a host reads, so it is pinned:
38+
// it must name `adapterConfig.datasetId`, the key the schema declares.
39+
await expect(a.search('q', { source: bad, topK: 1 })).rejects.toThrow(
40+
/source\.adapterConfig\.datasetId/,
41+
);
42+
});
43+
44+
it('does not read the undeclared `options` spelling', async () => {
45+
// `KnowledgeSourceSchema` is a plain `z.object`: it declares `adapterConfig`
46+
// and drops `options` on any parsing path. The adapter reads the declared
47+
// key only — no lenient fallback (Prime Directive #12). A host still on the
48+
// old spelling is refused loudly rather than served with silence.
49+
const { fetch, calls } = fakeFetch(() => ({}));
50+
const a = new KnowledgeRagflowAdapter({ endpoint: 'http://x', apiKey: 'k', fetch });
51+
const { adapterConfig: _dropped, ...rest } = source;
52+
const legacy = { ...rest, options: { datasetId: 'ds_42' } } as unknown as KnowledgeSource;
53+
await expect(a.search('q', { source: legacy, topK: 1 })).rejects.toThrow(
54+
/source\.adapterConfig\.datasetId/,
55+
);
56+
expect(calls).toHaveLength(0);
3857
});
3958

4059
it('upsert deletes-then-creates chunks and stamps objectstack metadata', async () => {
@@ -106,7 +125,7 @@ describe('KnowledgeRagflowAdapter', () => {
106125
const a = new KnowledgeRagflowAdapter({ endpoint: 'http://r', apiKey: 'k', fetch });
107126
const s: KnowledgeSource = {
108127
...source,
109-
options: { datasetId: 'ds_42', rerankModel: 'bge-reranker', similarityThreshold: 0.6 },
128+
adapterConfig: { datasetId: 'ds_42', rerankModel: 'bge-reranker', similarityThreshold: 0.6 },
110129
};
111130
await a.search('q', { source: s, topK: 3, filter: { tag: 'a' } });
112131
const body = JSON.parse(calls[0].init.body);

‎packages/plugins/knowledge-ragflow/src/index.ts‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,29 @@ interface RagflowSourceOptions {
5757
vectorSimilarityWeight?: number;
5858
}
5959

60+
/**
61+
* Reads this source's RAGFlow binding from `adapterConfig` — the key
62+
* `KnowledgeSourceSchema` declares for adapter-specific configuration.
63+
* That schema is a plain `z.object`, so any parsing path drops keys it
64+
* does not declare; the adapter therefore reads the declared key and no
65+
* other spelling (Prime Directive #12 — no lenient consumer).
66+
*/
6067
function extractRagflowOptions(source: KnowledgeSource): RagflowSourceOptions {
61-
const opts = ((source as unknown as { options?: Record<string, unknown> }).options ?? {}) as
62-
Record<string, unknown>;
63-
const datasetId = opts.datasetId;
68+
const cfg = source.adapterConfig ?? {};
69+
const datasetId = cfg.datasetId;
6470
if (typeof datasetId !== 'string' || !datasetId) {
6571
throw new Error(
66-
`RAGFlow adapter requires source.options.datasetId on source '${source.id}'`,
72+
`RAGFlow adapter requires source.adapterConfig.datasetId on source '${source.id}'`,
6773
);
6874
}
6975
return {
7076
datasetId,
71-
rerankModel: typeof opts.rerankModel === 'string' ? opts.rerankModel : undefined,
77+
rerankModel: typeof cfg.rerankModel === 'string' ? cfg.rerankModel : undefined,
7278
similarityThreshold:
73-
typeof opts.similarityThreshold === 'number' ? opts.similarityThreshold : undefined,
79+
typeof cfg.similarityThreshold === 'number' ? cfg.similarityThreshold : undefined,
7480
vectorSimilarityWeight:
75-
typeof opts.vectorSimilarityWeight === 'number'
76-
? opts.vectorSimilarityWeight
81+
typeof cfg.vectorSimilarityWeight === 'number'
82+
? cfg.vectorSimilarityWeight
7783
: undefined,
7884
};
7985
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
{
22
"_comment": "Per-file tsc error debt of the @objectstack/knowledge-ragflow TEST layer (#5286). `tsconfig.test.json` compiles `src/**/*.test.ts` — which `tsconfig.json` excludes and therefore no gate ever read — and every file below still carries errors from before that gate existed. THIS FIELD IS GENERATED: every regeneration rewrites it from scripts/check-test-typecheck.mts, and the EXACT ratchet below requires a regeneration on every repair — so an edit made here is gone by the next one. Anything true of THIS package goes in the sibling `_note` field, which is authored, is preserved verbatim, and is never written by the generator (#12624). This comment states NO cause for the errors, deliberately: the classes differ per package and per file, they move as the debt is paid down, and a cause written here is rewritten verbatim into every ledger by every regeneration — so it outlives its own repair and cannot be corrected in the file where it is read. Measure instead, before repairing anything: `tsc --noEmit --pretty false -p tsconfig.test.json` in the package prints the real classes with their TS codes. Each entry maps a file to its per-SIGNATURE error counts, never to a bare total (#13470): a signature is the TS code plus the diagnostic message with structural type blobs collapsed, and it carries NO line or column — so the pin survives edits that move code around, and only stops matching when the error itself becomes a different error. EXACT ratchet, judged by re-running tsc: a file that gains errors is red, a file that loses them is red until its number is re-recorded, a file that reaches zero is red until its entry is deleted, a signature that ARRIVES or VANISHES is red even when the file total is unchanged, and a file NOT listed here may have no errors at all. Regenerate with: pnpm --filter @objectstack/knowledge-ragflow gen:test-typecheck-debt",
3-
"_note": "STARTING LEDGER, opened by #14062 under the director ruling of 2026-09-01 (maintainer verbatim: 「同意」), which carries the #5286 maintainer authority for it. 3 errors in 1 file, all PRE-EXISTING — and this package was silent for a DIFFERENT reason than its siblings: its `tsconfig.json` never excluded tests, so a tsc program would have read them, but the package declared NO `typecheck` script at all, and `turbo run typecheck` cannot run a script that does not exist. #14062 added one naming this gate. ⛔ That is not the repo-wide 'packages missing a `typecheck` script' carry-over, which the same ruling holds separate (item 5): this is the one invocation path #14062's own instrument needs in order to run here at all.",
4-
"entries": {
5-
"src/__tests__/ragflow-adapter.test.ts": {
6-
"TS2353: Object literal may only specify known properties, and 'options' does not exist in type '…'.": 3
7-
}
8-
}
3+
"_note": "STARTING LEDGER, opened by #14062 under the director ruling of 2026-09-01 (maintainer verbatim: 「同意」), which carries the #5286 maintainer authority for it. 3 errors in 1 file, all PRE-EXISTING — and this package was silent for a DIFFERENT reason than its siblings: its `tsconfig.json` never excluded tests, so a tsc program would have read them, but the package declared NO `typecheck` script at all, and `turbo run typecheck` cannot run a script that does not exist. #14062 added one naming this gate. ⛔ That is not the repo-wide 'packages missing a `typecheck` script' carry-over, which the same ruling holds separate (item 5): this is the one invocation path #14062's own instrument needs in order to run here at all. GRADUATED — the ledger is now empty. All 3 errors were one defect: the test wrote `options` on a `KnowledgeSource` literal, a key `KnowledgeSourceSchema` does not declare, because the adapter read that spelling. The adapter now reads the declared `adapterConfig` (ruled: batch #160 item 2, letter A) and the literals moved with it, so tsc reports none. ⛔ This file is kept, not deleted: it is the per-file ledger `check:test-typecheck` reads for this package, and an empty `entries` is the pin that the test layer owes nothing.",
4+
"entries": {}
95
}

0 commit comments

Comments
 (0)