Repository navigation
fix(knowledge-ragflow): read source.adapterConfig.datasetId, the declared key - #19251
Conversation
…ared key `extractRagflowOptions` cast the source to a shape carrying `options` and read `options.datasetId`. `KnowledgeSourceSchema` declares `adapterConfig` and is a plain `z.object`, so any path that parses a source drops `options` entirely — the adapter worked only because no path parses. The cast is removed, the adapter reads the declared key, and the refusal names `adapterConfig.datasetId` so a host on the old spelling is told what to write. The published README example moves with it. Ruled: batch #160 item 2, letter A (maintainer 2026-09-18). Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
…duated The three TS2353 errors this package's test-typecheck ledger froze were one defect: the test wrote `options` on a `KnowledgeSource` literal, a key `KnowledgeSourceSchema` does not declare, because the adapter read that spelling. With the adapter on `adapterConfig` the literals moved with it and tsc reports none, so the entry is deleted — the ledger is shrink-only and a graduated file is red until its entry goes. Adds the changeset for the behaviour change. Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a0af214409ba7dd2192eb022aa6a2b7b64c40c14 && git checkout a0af214409ba7dd2192eb022aa6a2b7b64c40c14
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1b82c519df9db93891514b814b4d8e16e42d5d5c 6f17a4f13276ab6798b94eb3064d1f06df6987dc && git checkout -B drift-repro 1b82c519df9db93891514b814b4d8e16e42d5d5c && git merge --no-ff 6f17a4f13276ab6798b94eb3064d1f06df6987dc
node scripts/docs-audit/affected-docs.mjs --json 1b82c519df9db93891514b814b4d8e16e42d5d5c |
…lared options key
`content/docs/ai/knowledge-rag.mdx` configured its `ragflow` knowledge source with
`options: { datasetId }`. `KnowledgeSourceSchema` declares `adapterConfig` and is a plain
`z.object` with no `.passthrough()`, so the key is dropped before any adapter sees it.
Since #19251 (card #18973, ruling batch #160 item 2, letter A) `extractRagflowOptions`
reads `source.adapterConfig` with no fallback, so a source copied from this page is
refused by name at the first upsert / search / delete:
RAGFlow adapter requires source.adapterConfig.datasetId on source '<id>'
One line, one page. No spec change and no ADR-0087 conversion: `options` was never a key
`KnowledgeSourceSchema` accepted, so nothing an author could declare is removed.
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
…lared options key (objectstack-ai#19261) Fixes objectstack-ai#19252 `content/docs/ai/knowledge-rag.mdx:37` configured its `ragflow` knowledge source with the undeclared `options` key. `KnowledgeSourceSchema` (`packages/spec/src/ai/knowledge-source.zod.ts`) declares **`adapterConfig`** and is a plain `z.object` with no `.passthrough()`, so the key is dropped before any adapter sees it. Since objectstack-ai#19251 landed (card objectstack-ai#18973; maintainer ruling batch objectstack-ai#160 item 2, letter A: 「the spec wins」), `extractRagflowOptions` reads `source.adapterConfig` with **no fallback**, so a source copied off this page is refused by name at the first `upsert` / `search` / `delete`. One line, one page: ```diff - options: { datasetId: 'rgf_doc_dataset' }, + adapterConfig: { datasetId: 'rgf_doc_dataset' }, ``` No `packages/spec` change and **no ADR-0087 conversion**, exactly as the ruling states: `options` was never a key `KnowledgeSourceSchema` accepted, so nothing an author could declare is removed. ## Is :37 the only carrier? — measured, not assumed All readings taken on this branch at `6ec845878`, over `content/docs/`: | reading | result | |---|---| | `grep -rn "options: { datasetId" content/docs/` (pre-fix tree) | **1** — this line only | | firing control, same corpus, correct spelling | `content/docs/references/ai/knowledge-source.mdx:83` spells `adapterConfig` ⇒ the outlier is this page, not the docs tree | | positive control, same corpus, same term | `datasetId` anywhere under `content/docs/` ⇒ **1** hit (the same line) ⇒ the one-hit reading is a reading, not a dead search | | widened, different spellings: `grep -rn -A3 "options:" content/docs/` filtered to `datasetId` | **1** — no multi-line or differently-quoted second carrier | | widened, by subject: every `content/docs/` file naming `KnowledgeServicePlugin` / `adapter: 'ragflow'` / `adapter: 'memory'` / `KnowledgeSource` (10 files) read for a source literal | no second knowledge-source example carries `options` | | every `options *:` occurrence under `content/docs/` (41 lines) read | all are `Field.select` option lists, batch `options.atomic`, i18n option maps or a dashboard widget — **none is a knowledge source** | ⇒ this is a single-carrier card, and the single carrier is fixed here. The rest of `content/docs/**` is clean on this defect. ## The corrected example parses, and the old spelling is shown dropping A throwaway script (⛔ not committed; removed, `git diff HEAD` empty afterwards) lifted the `product_docs` literal **out of the page by content** — not retyped — and parsed it against the built `KnowledgeSourceSchema`: ``` --- block lifted from content/docs/ai/knowledge-rag.mdx (lines 33-37) --- id: 'product_docs', label: 'Product docs', adapter: 'ragflow', source: { kind: 'http', urls: ['https://docs.example.com/sitemap.xml'] }, adapterConfig: { datasetId: 'rgf_doc_dataset' }, === LEG A: the page as it now stands === safeParse success: true parsed adapterConfig: {"datasetId":"rgf_doc_dataset"} adapter's read of source.adapterConfig.datasetId: rgf_doc_dataset === LEG B: ABLATION — the pre-fix spelling, in this scratch object only === safeParse success: true unrecognized_keys issues: none post-parse keys: adapter,adapterConfig,aiExposed,id,label,refresh,source 'options' survives the parse: false parsed adapterConfig: {} adapter's read of source.adapterConfig.datasetId: [MISSING -- RAGFlow adapter requires source.adapterConfig.datasetId on source 'product_docs'] ``` *(one byte-level edit to the transcript above: the script's own placeholder brackets around the MISSING line are rendered here as square brackets, because this repo's rule is that angle-bracket-shaped fragments are spelled out in words before they go into a GitHub body — fences do not protect them.)*⚠️ **The observed ablation direction is a silent drop, not a red parse** — worth stating because it is the defect's whole shape. `KnowledgeSourceSchema` is a plain `z.object`, so the old spelling still `safeParse`s green with **zero** `unrecognized_keys`; `options` simply is not in the post-parse keys and `adapterConfig` falls back to its `{}` default. The loud part arrives one layer later, at the adapter's named refusal. That is precisely why a reader could copy this page and get no warning until the first call. ## Verification - **Derived gate family**, run in this act, ⛔ not taken from the dispatch: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths — derived from the real change set vs merge base `7d0f911da`) ⇒ **39** commands. **All 39 run, all 39 green.** Four first refused with `PREREQUISITE NOT MET` / unbuilt-package (exit 3, exit 1 — ⛔ not findings): `check:doc-formula-expressions`, `check:doc-security-posture`, `check:docs-transcript-drift` (`@objectstack/lint`, `@objectstack/formula` unbuilt) and `check:skill-examples` (`@objectstack/client-react` `dist` held no declarations). Those four packages were built, and all four gates re-run **green**. - `check:nul-bytes` green; plus an out-of-gate self-scan for control bytes on the changed file — no match. - **MDX still builds**: `pnpm --filter @objectstack/docs run postinstall` (`fumadocs-mdx`, the docs collection compile) exit **0**, working tree clean afterwards. - **Lint, narrowed — and the narrowing is itself measured** (at `6ec845878`): ① the inspected population read from eslint's own config is `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` (and `['**/*.{ts,tsx,mts,cts}']`) — **`.mdx` appears in no `files` entry**; ② `eslint --no-inline-config --format json content/docs/ai/knowledge-rag.mdx` ⇒ 1 file entry, **0 errors**, its one message being `File ignored because no matching configuration was supplied.`; ③ invariance for untouched files: `eslint.config.mjs` declares **no** `parserOptions.project` and **no** `projectService`, so type-aware linting is off and this diff cannot move any untouched file's verdict. ⇒ the diff's only file lies wholly outside eslint's population; the repo-wide `pnpm lint` sweep is CI's run. - ⊘ **NOT MEASURED, by the derivation's own account**: the two path-scheduled CI jobs (`Test Core`, `Build Docs`) — CI's own shell in CI's environment, no local invocation exists — and the two workflow-valued families (`check-shard-attestation`, `check-test-completeness`). ## changeset `skip-changeset`. The criterion is whether anything **published** moves, not whether a changeset exists. Measured: `content/docs/**` is consumed only by `apps/docs` (`@objectstack/docs`, `"private": true`, no `files[]`), and a scan of **every** `package.json` in the repo found **zero** packages whose `files[]` ships anything under `content/`. ⇒ no published artifact moves. ## Acceptance notes - **Noted, not filed** — the corrected block on this page carries no `os:check` marker, so `check:skill-examples` (258 marked blocks across 106 files; 216 `os:check` markers under `content/docs/`) never type-checked it, which is why the wrong key could sit in a published page uncaught. ⛔ Out of this card's surface and ⛔ not one of the three fileable classes (it is a coverage gap, not a reproducible defect — the block is correct as of this PR). Successor: the next card that extends the compile-checked prose corpus into `content/docs/ai/`. - `.changeset/18973-ragflow-reads-declared-adapter-config.md:13` also contains the string `options: { datasetId: … }`. It is the **before** side of that changeset's migration example — correct as written, and ⛔ deliberately not touched. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18973
Clause-②: no
Ruled on-card: batch #160 item 2, letter A, maintainer 「同意」 2026-09-18 (
issuecomment-5729652660) — "the spec wins:@objectstack/knowledge-ragflowreadssource.adapterConfig.datasetId, its README says the same, andKnowledgeSourceSchemais untouched".packages/specis untouched here, as ruled.What was wrong
extractRagflowOptionscast the source to a shape it does not have and readoptions.datasetId:KnowledgeSourceSchemadeclaresadapterConfigfor adapter-specific configuration and is a plainz.objectwith no.passthrough(), so any path that parses a source dropsoptionsbefore 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.extractRagflowOptionsreadssource.adapterConfig, a declared, already-typed property, so no cast is needed at all. There is no fallback that also readsoptions(Prime Directive Add comprehensive test suite for Zod schema validation #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 toadapterConfig, and the block now compiles against the package (evidence below).src/__tests__/ragflow-adapter.test.ts— the threeKnowledgeSourceliterals 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 legacyoptionsspelling is refused and reaches no transport at all.test-typecheck-debt.json— emptied. See below; this was not an incidental repair.@objectstack/knowledge-ragflowpatch, behaviour: reads the declared key, with theFROM→TOmapping 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 onorigin/main— the definition at:60and 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 wholesourceand 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.tsafter the change returns one line —pass options.fetchin the adapter constructor's own error, which is aboutKnowledgeRagflowAdapterOptionsand not about aKnowledgeSource.The type-debt ledger was this same defect, frozen
packages/plugins/knowledge-ragflow/test-typecheck-debt.jsonpinned 3 errors in one file, allTS2353: 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 onadapterConfigthe file graduates to zero, and the ledger is shrink-only — a graduated file is red until its entry is deleted.check:test-typechecksaid so in as many words, and the entry is now gone. The file is kept with an emptyentriesmap (it is the per-file ledger the gate reads for this package), and its authored_noterecords the graduation; the note survives regeneration, verified by running the generator twice.Two shipped surfaces already said
adapterConfigThe 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 callsKnowledgeSourceSchema.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-blocksis a census, not a gate, so a bare green from it is worth little — it was run in both directions, at6f17a4f13:options(viascripts/ablation-replace.mjs, anchor hit 1→0, blobc37eadb4744e→55123d3e9226)TS2353 x1The instrument's own
FIRING_CONTROLreported 5 diagnostics on both runs, so the zero is a reading and not a dead search. Restore was proven byte-identical bygit hash-object(c37eadb4744e…before and after) withgit diff HEADclean, not by the wrapper's exit code.Gates.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 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-loadsandcheck: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, thencheck:test-typecheck, which reports 0 files / 0 errors / 0 pinned signatures).pnpm lintover the whole repo → exit 0.Acceptance notes
Noted here, not fixed, not filed by this PR:
content/docs/ai/knowledge-rag.mdx:37still writesoptions: { datasetId: 'rgf_doc_dataset' }on aragflowsource. 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/), andcontent/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.mdcloses with "One block is deliberately left", describing this README. That changeset belongs to PR docs(readme): make the published README TypeScript examples compile #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.Errors, 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.KnowledgeSourceSchema⇒ p1) is still unmet in runtime code:git grep KnowledgeSourceSchemaoutsidepackages/specreturns zero importers inpackages/**/*.ts(positive control: 8,741*Schema.parse|safeParsecall sites repo-wide). The only parse call sites arepackages/spec's own tests and a published-skill example.Generated by Claude Code