Files
0pt/scripts/verify-doc0-source-wire.ts
admin b3239487b7
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 10s
CI (SIAX Cloud) / sonar (pull_request) Skipped
Security Scan (SIAX Cloud) / security (push) Successful in 13s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / security (push) Successful in 26s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / quality (push) Successful in 1m9s
CI (SIAX Cloud) / quality (pull_request) Successful in 2m26s
CI (SIAX Cloud) / integration (pull_request) Successful in 2m33s
feat(document-workbench): wire customer upload, structuring, credits and readiness
- POST /api/0pt/documents/upload: real multipart bytes into DOC0 through the
  durable attempt journal and the existing coordinator; verified tenant/actor
  only, project authorization fail-closed.
- POST /api/0pt/documents/structure: DOC0 segments -> INF0 -> evidence-bound
  validation -> optional JSON/CSV export, with credit reserve/settle.
- Credits: balance, server-owned price book, Stripe Checkout and idempotent
  webhook reconciliation via a Stripe re-read of session + payment intent.
- /portal/dokument and /portal/krediter: SIAX-styled workspace.
- @siax/access: universal, framework-neutral OpenFGA project relationship
  primitive reusable by DOC0/N0D/other products.
- Readiness: real Redis PING probe instead of presence-only; queue is now a
  live dependency when configured.
- Dockerfile: copy every workspace manifest so --frozen-lockfile works.
- Format/build hygiene: .prettierignore, .worktrees ignored, valid manifest YAML.
2026-11-02 14:46:10 +01:00

58 lines
5.6 KiB
TypeScript

/** Run with DOC0_CHECKOUT=/absolute/path "$DOC0_CHECKOUT/node_modules/.bin/tsx" scripts/verify-doc0-source-wire.ts.
* In-memory test only: proves the current DOC0 response shape, not production auth or storage. */
import { createHash } from 'node:crypto';
import { pathToFileURL } from 'node:url';
import { isAbsolute, resolve } from 'node:path';
import { execFileSync } from 'node:child_process';
import { Doc0SourceClient } from '../src/document-workbench/doc0-source-client';
import { structureDocumentBatch } from '../src/document-workbench/structure-document-batch';
import { exportValidatedBatch } from '../src/document-workbench/structured-batch';
async function main() {
const checkout = process.env.DOC0_CHECKOUT;
if (!checkout || !isAbsolute(checkout)) throw new Error('DOC0_CHECKOUT must name an absolute local checkout');
const load = (path: string) => import(pathToFileURL(resolve(checkout, path)).href);
const [{ buildServer }, { InMemoryDocumentStore }, { InMemoryDocumentVersionStore }, { InMemoryParseStore }, { segmentText }] = await Promise.all([
load('src/api/app.ts'), load('src/domains/document/store.ts'), load('src/domains/document-version/store.ts'),
load('src/domains/parse/store.ts'), load('src/parsing/segments.ts'),
]);
process.env.AUTH_ENFORCE = 'false';
Object.assign(process.env, { NODE_ENV: 'test' });
const documents = new InMemoryDocumentStore(), versions = new InMemoryDocumentVersionStore(), parses = new InMemoryParseStore();
const input = { tenantId: 't-test', projectId: 'p-test', actorId: 'actor', requestId: 'req', receipt: {
documentId: 'doc', versionId: 'v1', checksumSha256: createHash('sha256').update('fixture bytes').digest('hex'), pipelineStage: 'parsed',
} };
await documents.create({ tenantId: 't-test', projectId: 'p-test', documentId: 'doc', appId: 'test', displayName: 'fixture', documentType: 'unknown', classification: 'internal', tags: [], createdBy: 'actor' });
await versions.append({ tenantId: 't-test', documentId: 'doc', versionId: 'v1', versionNumber: 1, checksumSha256: input.receipt.checksumSha256, sizeBytes: 13, mimeType: 'text/plain', st0reArtifactRef: 'fixture', pipelineStage: 'parsed', extractedMetadata: {}, customMetadata: {}, createdBy: 'actor' });
const parts = segmentText('Acme AB\n\nInvoice 123');
await parses.appendDocumentText({ tenantId: 't-test', documentId: 'doc', versionId: 'v1', documentTextId: 'txt', parserRunId: 'run', charCount: parts.charCount, segmentCount: parts.segments.length, segmentsTruncated: parts.truncated, textSha256: parts.textSha256 });
await parses.appendTextSegments(parts.segments.map((s: object, i: number) => ({ ...s, tenantId: 't-test', documentId: 'doc', versionId: 'v1', documentTextId: 'txt', parserRunId: 'run', segmentId: `seg-${i}`, sourceVersionSha256: input.receipt.checksumSha256 })));
const app = buildServer({ documentStore: documents, versionStore: versions, parseStore: parses });
const fetchImpl: typeof fetch = async (url, init) => {
const target = new URL(String(url));
const out = await app.inject({ method: 'GET', url: target.pathname + target.search, headers: Object.fromEntries(new Headers(init?.headers).entries()) });
return new Response(out.body, { status: out.statusCode, headers: { 'content-type': 'application/json' } });
};
try {
const client = new Doc0SourceClient({ baseUrl: 'https://doc0.example', serviceToken: 'fixture', fetchImpl });
const result = await client.read(input);
if (result.sources.length !== 2 || result.sources[0]?.text !== 'Acme AB' || result.sources[1]?.text !== 'Invoice 123') throw new Error('DOC0 wire mismatch');
const fields = [{ name: 'value', type: 'string' as const, required: true }];
const records = result.sources.map((source) => ({ value: { value: source.text }, evidence: [{ field: 'value', sourceId: source.id, start: 0, end: source.text.length, transform: 'exact' }] }));
const request = {
context: { tenantId: 't-test', projectId: 'p-test', actorId: 'actor', capability: 'inf0.complete', correlationId: 'req' },
admission: { tenantId: 't-test', projectId: 'p-test', jobId: 'j', modelId: 'local_only', dataClass: 'high', maxInputTokens: 200, maxOutputTokens: 100, maximumCredits: 5 },
policy: { tenantId: 't-test', projectId: 'p-test', allowedModelIds: ['local_only'], allowedDataClasses: ['high'], destination: 'internal' as const, externalTransferAllowed: false, maxInputTokens: 200, maxOutputTokens: 100 },
reservation: { tenantId: 't-test', projectId: 'p-test', jobId: 'j', status: 'reserved' as const, credits: 5 },
};
const structured = await structureDocumentBatch({ request, fields, sources: result.sources, instructions: 'Extract values', countTokens: async () => 50,
gateway: { complete: async () => ({ status: 'model_stopped', content: JSON.stringify({ records }), modelExecuted: 'fixture', providerKind: 'local', promptTokens: 50, completionTokens: 20, totalTokens: 70 }) } });
if (structured.status !== 'mechanically_validated') throw new Error('model fixture evidence failed');
const exported = exportValidatedBatch({ format: 'json', fields, sources: result.sources, records: structured.records });
if (exported.status !== 'ready' || JSON.parse(exported.content).length !== 2) throw new Error('export fixture failed');
const revision = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: checkout, encoding: 'utf8' }).trim();
console.log(`DOC0 ${revision}: actual route through 0pt source and validated export; in-memory stores, test auth and model fixture`);
} finally { await app.close(); }
}
main().catch((error: unknown) => { console.error(error); process.exitCode = 1; });