Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Every entry says what changed as a caller sees it, why, and what to watch for

### Added

- The installed Drive research check now verifies person links, stale-note revision refusal,
scoped source-backed memories and local source retirement through the released CLI. It
checks persisted memory staleness and owner-note preservation with synthetic data only.
- Controlled Drive acceptance: an explicit-profile, isolated-workspace checker for cached text,
hierarchy, source identity, exact search, Markdown, local annotations and optional syncs.
A private fixture manifest and owner-started test-account login are required for live use;
Expand Down
32 changes: 32 additions & 0 deletions performance/documents/offline-catalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ Negative cases cover wrong scope, wrong-source search hits, pending sync, change
lost notes, unknown input, existing output and error-output canary exclusion. No Google login,
conversion fidelity, remote mutations, real embeddings or OAuth verification were measured.

### Installed knowledge lifecycle

On 2026-10-15, the same exact npm package and synthetic fixture passed seven further behavior
groups. The question was whether the document knowledge features work together through the
released CLI, including source retirement, rather than only through individual service tests.
Reproduce with the `live-installed.mjs` command above; it now also runs
[`knowledge-installed.mjs`](knowledge-installed.mjs).

| Behavior | Result |
| --- | --- |
| Explicitly link an imported document to a seeded person and filter by that person | Passed |
| Edit a local note, then try its old revision | Stale edit refused; updated text preserved |
| Create and confirm a memory | Proposed first; source subject/evidence and explicit work scope retained |
| List/search that memory within its scope and source | Work result found; personal scope empty |
| Remove the selected root and reconcile | Catalog, imported list, keyword search and source snapshot emptied |
| Inspect persisted knowledge after source retirement | Confirmed memory stale; owner-written note preserved |
| Reconcile again | No further removals |

The existing eight initial and eight baseline checks still passed before the lifecycle cases.
Setup: Linux, Node 24.19.0, Drive 0.7.0, Core 0.278.0 and framework 0.28.2. The person is seeded
through the shared store's public API; links, notes, memories and reconciliation use the actual
compiled Drive CLI. Persisted memory/note outcomes are inspected through the public store API.
No SQL or Drive source imports are used.

All processes use isolated state and the HTTP/browser/keyring denial wrapper. Reconciliation
calls `sync` without `--offline` only after `folders remove` leaves no selected roots: there is
no Google source to read, and the denial wrapper would fail an unexpected request. This is a
local source-retirement test, not a live Google move, trash or permission-change test. Automatic
editor identity association, project links, AI extraction and provider conversion fidelity remain
outside these results. Source removal intentionally exercises snapshot deletion; all other test
artifacts and reports remain available for inspection.

## Setup and reproduction

Synthetic data only: one root, one nested folder, one indexed PDF fixture and a second explicit
Expand Down
80 changes: 80 additions & 0 deletions performance/documents/offline-catalog/knowledge-installed.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import assert from "node:assert/strict"
import { existsSync } from "node:fs"
import { join } from "node:path"

export const verifyKnowledge = async ({ invoke, openStore, env, workspace, reference }) => {
const checks = []
const item = (words, options) => {
const answer = invoke(words, options)
assert.ok(answer.item)
return answer.item
}
const list = (words) => {
const answer = invoke(words)
assert.ok(Array.isArray(answer.items))
return answer.items
}
const store = await openStore({ path: env.DRIVE_STORE, env })
let person
try {
const account = { provider: "synthetic-messenger", account: "synthetic-account" }
await store.saveAccount(account, { name: "Synthetic test account" })
await store.savePeople(account, [{ id: "synthetic-editor", name: "Rin Example" }])
person = await store.personOf({ provider: account.provider, id: "synthetic-editor" })
assert.ok(person)
} finally {
await store.close()
}
const personRef = `person:${person.uid}`
item(["relationships", "add", "doc", personRef, "--kind", "related-to"])
assert.ok(item(["documents", "context", "doc"]).links.some((link) => link.to === personRef))
assert.ok(list(["documents", "list", "--person", personRef]).some((doc) => doc.id === "doc"))
checks.push("explicit-person-link-and-filter")

const note = item(["notes", "add", "doc", "Synthetic lifecycle note"])
const edited = item(["notes", "edit", "doc", note.ref, "Updated lifecycle note", "--revision", String(note.revision)])
assert.ok(edited.revision > note.revision)
const refused = invoke(["notes", "edit", "doc", note.ref, "Stale overwrite", "--revision", String(note.revision)], {
failure: true,
})
assert.ok(refused.error?.code)
assert.equal(item(["notes", "show", "doc", note.ref]).text, "Updated lifecycle note")
checks.push("note-edit-refuses-stale-revision")

const memory = item(["memories", "add", "doc", "Synthetic lifecyclemarker decision", "--scope", "work"])
assert.equal(memory.subject, reference)
assert.deepEqual(memory.evidence, [reference])
assert.equal(memory.status, "proposed")
assert.equal(memory.scope, "work")
assert.equal(item(["memories", "confirm", "doc", memory.ref]).status, "confirmed")
checks.push("memory-evidence-and-review")
assert.equal(list(["memories", "list", "doc", "--scope", "personal"]).length, 0)
assert.ok(list(["memories", "list", "doc", "--scope", "work"]).some((entry) => entry.ref === memory.ref))
const found = item(["memories", "search", "doc", "lifecyclemarker", "--scope", "work"])
assert.ok(found.items.some((entry) => entry.ref === memory.ref))
checks.push("memory-scope-and-source-search")

item(["folders", "remove", "root"])
// With no selected roots, this sync reconciles local sources; the provider/keyring guard remains installed.
const reconciled = item(["sync"], { offline: false })
assert.equal(reconciled.removed, 1)
assert.equal(reconciled.pending, 0)
assert.equal(list(["catalog", "list"]).length, 0)
assert.equal(list(["documents", "list"]).length, 0)
assert.equal(item(["search", "wirecatbodymarker", "--exact"]).hits.length, 0)
assert.equal(invoke(["documents", "show", "doc"], { failure: true }).error.code, "not_found")
assert.equal(existsSync(join(workspace, "state/profiles/synthetic/documents/doc.json")), false)
checks.push("scope-removal-reconciles-catalog-search-and-snapshot")

const retained = await openStore({ path: env.DRIVE_STORE, env })
try {
assert.equal((await retained.memories.get(memory.ref)).status, "stale")
assert.equal((await retained.notes.note(note.ref)).text, "Updated lifecycle note")
} finally {
await retained.close()
}
checks.push("removed-source-stales-memory-and-keeps-owner-note")
assert.equal(item(["sync"], { offline: false }).removed, 0)
checks.push("scope-removal-is-idempotent")
return checks
}
17 changes: 16 additions & 1 deletion performance/documents/offline-catalog/live-installed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createRequire } from "node:module"
import { tmpdir } from "node:os"
import { join, resolve } from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url"
import { verifyKnowledge } from "./knowledge-installed.mjs"
import { initialize, workspaceEnvironment } from "./live-input.mjs"

const [input, ...rest] = process.argv.slice(2)
Expand Down Expand Up @@ -147,6 +148,20 @@ const run = (output, extra) => {
const first = join(root, "first.json")
const annotated = run(first, ["--annotate"])
const repeated = run(join(root, "repeat.json"), ["--baseline", first])
const invoke = (words, { offline = true, failure = false } = {}) => {
assert.ok(offline || words.join(" ") === "sync")
const result = spawnSync(
process.execPath,
[join(wrapper, "dist/bin/drive.js"), "synthetic", ...(offline ? ["--offline"] : []), "--json", ...words],
{ env, encoding: "utf8", timeout: 30000 },
)
assert.equal(result.error, undefined)
assert.equal(result.status === 0, !failure, "synthetic lifecycle command failed")
const answer = JSON.parse(result.stdout.trim() || result.stderr.trim())
if (!failure) assert.equal(answer.meta.complete, true)
return answer
}
const knowledge = await verifyKnowledge({ invoke, openStore, env, workspace, reference: note.ref })
process.stdout.write(
`${JSON.stringify({ passed: true, data: "synthetic", live_google: false, drive: metadata.version, core: metadata.dependencies["@wirecat/cli-core"], first_checks: annotated.checks.length, repeat_checks: repeated.checks.length })}\n`,
`${JSON.stringify({ passed: true, data: "synthetic", live_google: false, drive: metadata.version, core: metadata.dependencies["@wirecat/cli-core"], first_checks: annotated.checks.length, repeat_checks: repeated.checks.length, knowledge_checks: knowledge })}\n`,
)
Loading