diff --git a/CHANGELOG.md b/CHANGELOG.md index db1f969..83038ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ Every entry says what changed as a caller sees it, why, and what to watch for ### Added +- **`performance/projects/restore/check.mjs`** verifies selected annotation reconstruction against + published Core0.374 with independent IDs, stale-preview refusal and proposed state preserved. + It records release provenance and does not reset a live project or claim full backup coverage. + - **`performance/projects/snapshot/check.mjs`** checks the public read-only snapshot SDK with invented data, preserving proposed state and alias provenance while omitting source bodies. It does not reset or restore a project. diff --git a/performance/projects/PROCESS.md b/performance/projects/PROCESS.md index 36666ee..ae7608d 100644 --- a/performance/projects/PROCESS.md +++ b/performance/projects/PROCESS.md @@ -12,6 +12,9 @@ retained bytes; it does not run the applications or establish that a historical ## Existing evidence and its limits +The [published selected restore check](restore/README.md) uses independent synthetic stores and explicit +bindings. It verifies a stale preview and repeat refusal, not a complete project backup or live reset. + The [published snapshot check](snapshot/README.md) exercises a selected annotation read with invented data. It is functional release acceptance, separate from retrieval and extraction scores. It does not establish a complete project backup or a safe reset/restore path. diff --git a/performance/projects/README.md b/performance/projects/README.md index 6ae3d29..d492122 100644 --- a/performance/projects/README.md +++ b/performance/projects/README.md @@ -1,5 +1,8 @@ # Projects +- [Published selected restore](restore/README.md): independent destination bindings, stale preview refusal, + proposed-state preservation and repeat refusal against the released SDK. + - [Published snapshots](snapshot/README.md): invented-data checks of the released read-only annotation SDK; selected scope, source-body omission and proposed-state preservation. - [Installed observation](installed-observation/README.md): allowlisted totals from a private fixed-window diff --git a/performance/projects/restore/README.md b/performance/projects/restore/README.md new file mode 100644 index 0000000..e7a99b6 --- /dev/null +++ b/performance/projects/restore/README.md @@ -0,0 +1,42 @@ +# Published selected annotation restore + +Project gathering needs a way to reconstruct selected annotations in an independent test store before +broader extraction comparisons. This check asks whether the released SDK preserves explicit reference +bindings and proposed state, rejects a stale preview and refuses a repeated restore. It supports +adoption by Memo; it does not establish that a live project can be cleared safely. + +## Reproduction + +Linux x64, Node24.19.0, independently installed published `@wirecat/cli-core@0.374.0`, lifecycle scripts +disabled. Run from this checkout with temporary state on the same line: + +```sh +T=$(mktemp -d -p /var/tmp) && HOME=$T XDG_CONFIG_HOME=$T/c XDG_DATA_HOME=$T/d XDG_CACHE_HOME=$T/k MESSAGING_STORE=$T/store WIRECAT_STATE_DIR=$T/s TMPDIR=$T NPM_CONFIG_USERCONFIG=/dev/null npm install --prefix "$T/package" --ignore-scripts --no-audit --no-fund @wirecat/cli-core@0.374.0 && HOME=$T XDG_CONFIG_HOME=$T/c XDG_DATA_HOME=$T/d XDG_CACHE_HOME=$T/k MESSAGING_STORE=$T/store WIRECAT_STATE_DIR=$T/s TMPDIR=$T node performance/projects/restore/check.mjs "$T/package/node_modules/@wirecat/cli-core/dist/store/index.js" +``` + +The driver accepts only a package entry, creates its own invented source and target and uses public +store APIs. It seeds destination padding so project, document and author IDs differ, while retaining +an unrelated project's facts. Snapshot and native preview run after both seed writers close. +The store port operates on its existing connection, so it takes no destination filename. + +## Result and limits + +All retained assertions pass in [result.json](result.json): + +- Snapshot and preview preserve source bytes; preview also preserves target bytes. +- Explicit different-ID bindings restore one alias, one proposed fact and one evidence link. +- The bound destination author and two unrelated facts survive. +- A namespace change invalidates the first preview without inserting selected facts. +- A fresh check permits restore; a repeated restore refuses without duplicating the fact. +- External source bodies are omitted; no implicit store or owner data is used. + +The registry tarball matches the release CI artifact byte-for-byte, SHA-256 +`3cb5bb585803c28420f9a31588fdc4dfd7bf96c6a70d5363531fa815d241be79`. +The source commit, release run, integrity and installed entry hash accompany the assertions. +The earlier [Core0.373 snapshot result](../snapshot/README.md) remains unchanged. + +This is functional acceptance of selected project descriptor metadata, aliases, facts and generic +links. Notes, specialized meeting memberships, tasks, memories, labels and findings remain omitted. +It does not measure retrieval quality, model extraction, performance, connector authorization, +full project backup, injected mid-transaction rollback or concurrent processes. Those require +separate fixtures; no live source was reset. Memo CLI/MCP acceptance is a separate host check. diff --git a/performance/projects/restore/check.mjs b/performance/projects/restore/check.mjs new file mode 100644 index 0000000..ceb5995 --- /dev/null +++ b/performance/projects/restore/check.mjs @@ -0,0 +1,131 @@ +import assert from "node:assert/strict" +import { existsSync, mkdtempSync, readFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join, resolve } from "node:path" +import { pathToFileURL } from "node:url" + +const entry = process.argv[2] +assert(entry, "pass the independently installed published package store entry") +assert(process.env.HOME && process.env.MESSAGING_STORE && process.env.WIRECAT_STATE_DIR) +assert.equal(existsSync(process.env.MESSAGING_STORE), false) +const sdk = await import(pathToFileURL(resolve(entry)).href) +const directory = mkdtempSync(join(tmpdir(), "published-project-restore-")) +const source = join(directory, "source.db"), + target = join(directory, "target.db") +const body = "INVENTED_EXTERNAL_BODY_SENTINEL" +let project, destination, unrelated, sourceDocument, targetDocument, sourceFact, targetFact +const seed = async (path, targetSide) => { + const store = await sdk.openStore({ path, now: () => 1700000000000 }) + try { + if (targetSide) { + unrelated = await store.knowledge.addProject({ name: "Unrelated Example", key: "PAD" }) + await store.facts.add({ type: "statement", statement: "Padding", project: unrelated.ref, by: { bot: "padding" } }) + targetFact = await store.facts.add({ + type: "statement", + statement: "Keep this unrelated fact", + project: unrelated.ref, + by: { bot: "target-reviewer" }, + }) + } + const selected = await store.knowledge.addProject({ + name: "Research Example", + key: "REX", + ...(targetSide ? {} : { description: "Invented portable project" }), + }) + const folder = await store.notes.addFolder({ name: "Invented sources" }) + if (targetSide) + await store.notes.saveFileNote({ folderId: folder.id, path: "padding.md", title: "Padding", text: "Other text" }) + const { note } = await store.notes.saveFileNote({ + folderId: folder.id, + path: "evidence.md", + title: "Evidence", + text: body, + }) + if (targetSide) { + destination = selected + targetDocument = note + } else { + project = selected + sourceDocument = note + await store.knowledge.addVocabularyAlias({ kind: "project", id: selected.id }, "Example study") + sourceFact = await store.facts.add({ + type: "statement", + statement: "A report awaits review", + project: selected.ref, + by: { bot: "source-reviewer" }, + evidence: [note.ref], + }) + } + } finally { + await store.close() + } +} +await seed(source, false) +await seed(target, true) +assert.notEqual(project.ref, destination.ref) +assert.notEqual(sourceDocument.ref, targetDocument.ref) +assert.notEqual(sourceFact.author.id, targetFact.author.id) +const sourceBefore = readFileSync(source), + targetBefore = readFileSync(target) +const snapshot = await sdk.snapshotProjectAnnotations({ source, project: project.ref }) +const json = JSON.stringify(snapshot.snapshot) +assert(!json.includes(body)) +assert.equal(snapshot.snapshot.coverage.tasks, "omitted") +assert.equal(snapshot.snapshot.coverage.meeting_memberships, "omitted") +const bindings = { + [project.ref]: destination.ref, + [sourceDocument.ref]: targetDocument.ref, + [`bot:${sourceFact.author.id}`]: `bot:${targetFact.author.id}`, +} +const request = { snapshot: json, project: destination.ref, bindings } +const input = { ...request, target } +const first = await sdk.previewProjectAnnotationSnapshot(input) +assert.equal(first.ready, true) +assert.deepEqual(readFileSync(source), sourceBefore) +assert.deepEqual(readFileSync(target), targetBefore) +const writer = await sdk.openStore({ path: target }) +let receipt +try { + await writer.knowledge.addVocabularyAlias({ kind: "project", id: unrelated.id }, "Independent alias") + await assert.rejects(() => writer.projectAnnotations.restore({ ...request, fingerprint: first.fingerprint }), { + code: "validation_error", + message: /stale|conflict/, + }) + assert.equal((await writer.facts.list({ project: destination.ref })).length, 0) + const current = await writer.projectAnnotations.preview(request) + assert.equal(current.ready, true) + receipt = await writer.projectAnnotations.restore({ ...request, fingerprint: current.fingerprint }) + assert.deepEqual(receipt.counts, { aliases: 1, facts: 1, links: 1 }) + const facts = await writer.facts.list({ project: destination.ref }) + assert.equal(facts.length, 1) + assert.equal(facts[0].status, "proposed") + assert.equal(facts[0].author.id, targetFact.author.id) + assert.equal((await writer.facts.list({ project: unrelated.ref })).length, 2) + await assert.rejects(() => writer.projectAnnotations.restore({ ...request, fingerprint: current.fingerprint }), { + code: "validation_error", + message: /stale|conflict/, + }) + assert.equal((await writer.facts.list({ project: destination.ref })).length, 1) +} finally { + await writer.close() +} +assert.deepEqual(readFileSync(source), sourceBefore) +assert.equal(existsSync(process.env.MESSAGING_STORE), false) +process.stdout.write( + `${JSON.stringify({ + public_sdk: true, + snapshot_version: 2, + independent_ids: true, + source_unchanged: true, + preview_target_unchanged: true, + stale_preview_refused: true, + proposed_state_preserved: true, + author_binding_preserved: true, + unrelated_facts_preserved: true, + repeat_refused_without_duplicates: true, + external_bodies_omitted: true, + implicit_store_created: false, + owner_data_used: false, + restored_counts: receipt.counts, + })}\n`, +) diff --git a/performance/projects/restore/result.json b/performance/projects/restore/result.json new file mode 100644 index 0000000..fe8a29c --- /dev/null +++ b/performance/projects/restore/result.json @@ -0,0 +1,38 @@ +{ + "purpose": "Published selected-annotation restore acceptance, not a retrieval or extraction score", + "package": { + "name": "@wirecat/cli-core", + "version": "0.374.0", + "source_commit": "48c2ea40d3923908be9804ed1ca04b902ed699e1", + "release_run": 39440238477, + "tarball_sha256": "3cb5bb585803c28420f9a31588fdc4dfd7bf96c6a70d5363531fa815d241be79", + "integrity": "sha512-mwgahiGauzF44DPrbq5V2hF4st/BBNQ9IerqWC3qdryV639tS5GLBX/2D0Qed6G5Dyfi1JhCyrK865bD9MhnWQ==", + "registry_matches_validated": true, + "entry_sha256": "897f6881365236ebcf085f67ae86d11b74a0dad18e60db4cdc9c75572bbd6834" + }, + "runtime": { + "node": "v24.19.0", + "platform": "linux", + "arch": "x64" + }, + "checks": { + "public_sdk": true, + "snapshot_version": 2, + "independent_ids": true, + "source_unchanged": true, + "preview_target_unchanged": true, + "stale_preview_refused": true, + "proposed_state_preserved": true, + "author_binding_preserved": true, + "unrelated_facts_preserved": true, + "repeat_refused_without_duplicates": true, + "external_bodies_omitted": true, + "implicit_store_created": false, + "owner_data_used": false, + "restored_counts": { + "aliases": 1, + "facts": 1, + "links": 1 + } + } +}