@native-knowledge/cli 0.0.0-stage → 0.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +29 -0
  3. package/README.md +34 -2
  4. package/dist/agent-entry-cli.js +88 -0
  5. package/dist/auth-commands.js +102 -0
  6. package/dist/cli.js +104 -0
  7. package/dist/commands.js +17 -0
  8. package/dist/download.js +45 -0
  9. package/dist/errors.js +7 -0
  10. package/dist/input.js +89 -0
  11. package/dist/internal/contracts/artifact.js +103 -0
  12. package/dist/internal/contracts/collection-control.js +69 -0
  13. package/dist/internal/contracts/collection.js +174 -0
  14. package/dist/internal/contracts/common.js +16 -0
  15. package/dist/internal/contracts/currentness.js +221 -0
  16. package/dist/internal/contracts/event.js +35 -0
  17. package/dist/internal/contracts/evidence.js +171 -0
  18. package/dist/internal/contracts/index.js +54 -0
  19. package/dist/internal/contracts/material-capability.js +8 -0
  20. package/dist/internal/contracts/material-carrier.js +15 -0
  21. package/dist/internal/contracts/material-context.js +9 -0
  22. package/dist/internal/contracts/material-read.js +12 -0
  23. package/dist/internal/contracts/observation.js +39 -0
  24. package/dist/internal/contracts/persistence.js +239 -0
  25. package/dist/internal/contracts/project-intent.js +121 -0
  26. package/dist/internal/contracts/projection.js +194 -0
  27. package/dist/internal/contracts/public-reference.js +87 -0
  28. package/dist/internal/contracts/reference-record.js +55 -0
  29. package/dist/internal/contracts/remote-file.js +159 -0
  30. package/dist/internal/contracts/resource-scope.js +38 -0
  31. package/dist/internal/contracts/review-packet.js +61 -0
  32. package/dist/internal/contracts/review-plane.js +89 -0
  33. package/dist/internal/contracts/rich-document.js +184 -0
  34. package/dist/internal/contracts/runtime-compatibility.js +55 -0
  35. package/dist/internal/contracts/runtime.js +68 -0
  36. package/dist/internal/contracts/scheduler.js +256 -0
  37. package/dist/internal/contracts/signal.js +77 -0
  38. package/dist/internal/contracts/source-governance.js +194 -0
  39. package/dist/internal/contracts/source.js +32 -0
  40. package/dist/internal/contracts/tenant-budget.js +34 -0
  41. package/dist/internal/contracts/tenant-collection-request.js +58 -0
  42. package/dist/internal/contracts/tenant-collection.js +66 -0
  43. package/dist/internal/contracts/tenant-currentness.js +26 -0
  44. package/dist/internal/contracts/tenant-management.js +27 -0
  45. package/dist/internal/contracts/tenant-material-api.js +46 -0
  46. package/dist/internal/contracts/tenant-material-json-schema.js +16 -0
  47. package/dist/internal/contracts/tenant-pdf-text.js +13 -0
  48. package/dist/internal/contracts/tenant-project-control.js +19 -0
  49. package/dist/internal/contracts/tenant-public-reference.js +26 -0
  50. package/dist/internal/contracts/tenant-resource-api.js +70 -0
  51. package/dist/internal/contracts/tenant-resource.js +118 -0
  52. package/dist/internal/contracts/tenant-rich-document.js +17 -0
  53. package/dist/internal/contracts/tenant-source-binding.js +33 -0
  54. package/dist/internal/contracts/tenant-source-bridge.js +9 -0
  55. package/dist/internal/contracts/tenant-source-cutover.js +26 -0
  56. package/dist/internal/contracts/tenant-source-document.js +38 -0
  57. package/dist/internal/contracts/tenant-source-lease.js +5 -0
  58. package/dist/internal/contracts/tenant-source.js +83 -0
  59. package/dist/internal/contracts/tenant-web-collection.js +118 -0
  60. package/dist/internal/contracts/tenant-web-content-plan.js +39 -0
  61. package/dist/internal/contracts/tenant-web-content-result.js +15 -0
  62. package/dist/internal/contracts/tenant-web-content.js +16 -0
  63. package/dist/internal/contracts/tenant-web-crawl.js +55 -0
  64. package/dist/internal/contracts/tenant-web-dynamic.js +68 -0
  65. package/dist/internal/contracts/tenant-web-results.js +41 -0
  66. package/dist/internal/contracts/tenant-web-workflow.js +29 -0
  67. package/dist/internal/serving-contracts/agent-catalog.js +56 -0
  68. package/dist/internal/serving-contracts/agent-entry.js +57 -0
  69. package/dist/internal/serving-contracts/agent-guidance-shapes.js +12 -0
  70. package/dist/internal/serving-contracts/agent-input-hints.js +104 -0
  71. package/dist/internal/serving-contracts/body-search.js +16 -0
  72. package/dist/internal/serving-contracts/consumer.js +45 -0
  73. package/dist/internal/serving-contracts/core.js +76 -0
  74. package/dist/internal/serving-contracts/http.js +64 -0
  75. package/dist/internal/serving-contracts/index.js +25 -0
  76. package/dist/internal/serving-contracts/management.js +15 -0
  77. package/dist/internal/serving-contracts/material-capabilities.js +14 -0
  78. package/dist/internal/serving-contracts/material-index-status.js +5 -0
  79. package/dist/internal/serving-contracts/mcp.js +140 -0
  80. package/dist/internal/serving-contracts/tenant-governance.js +13 -0
  81. package/dist/internal/serving-contracts/tenant-management.js +89 -0
  82. package/dist/internal/serving-contracts/tenant-project.js +39 -0
  83. package/dist/internal/serving-contracts/tenant-runtime.js +37 -0
  84. package/dist/internal/serving-contracts/tenant-session.js +8 -0
  85. package/dist/internal/serving-contracts/tenant-web-content.js +20 -0
  86. package/dist/internal/serving-contracts/tenant-web-crawl.js +13 -0
  87. package/dist/internal/serving-contracts/tenant-web-dynamic.js +20 -0
  88. package/dist/internal/serving-contracts/tenant-web-workflow.js +36 -0
  89. package/dist/internal/serving-contracts/tenant-web.js +53 -0
  90. package/dist/internal/serving-contracts/tenant-workspace.js +28 -0
  91. package/dist/internal/serving-contracts/workflow.js +37 -0
  92. package/dist/profile-store.js +142 -0
  93. package/package.json +26 -3
package/dist/errors.js ADDED
@@ -0,0 +1,7 @@
1
+ import { AuthError, TenantManagementSdkError, ResourceSdkError, TenantOperationUnknownError } from '@native-knowledge/knowledge-sdk';
2
+ import { agentEntryFailure } from './agent-entry-cli.js';
3
+ /** A CLI failure never authorizes replay; retained phases remain partial completion. */
4
+ export function cliFailure(e) {
5
+ const status = e instanceof AuthError || e instanceof TenantManagementSdkError || e instanceof ResourceSdkError ? e.status : 0, raw = e instanceof AuthError ? e.code : e instanceof TenantManagementSdkError || e instanceof ResourceSdkError ? e.code : e instanceof Error && e.name === 'ZodError' ? 'CLI_INPUT_INVALID' : e instanceof Error && /^[A-Z][A-Z0-9_]{1,100}$/.test(e.message) ? e.message : 'CLI_OPERATION_FAILED', code = /^[A-Z][A-Z0-9_]{1,100}$/.test(raw) ? raw : 'CLI_OPERATION_FAILED', exit = (e instanceof TenantManagementSdkError || e instanceof TenantOperationUnknownError) && e.completedPhase ? 7 : code.includes('UNKNOWN') || code.includes('INCOMPLETE') ? 7 : status === 403 || code.includes('FORBIDDEN') ? 4 : status === 409 || code.includes('CONFLICT') ? 5 : status >= 500 || code.includes('TRANSPORT') ? 6 : code.includes('AUTH_') ? 3 : 2;
6
+ return { exit, error: { code, status, ...(agentEntryFailure(e) ?? {}), retryable: false, nextActions: code === 'AUTH_LOGOUT_REQUIRED' ? [{ command: 'native-knowledge auth logout', automatic: false }] : code === 'AUTH_CREDENTIAL_SOURCE_CONFLICT' ? [{ command: 'native-knowledge help', automatic: false }] : e instanceof TenantOperationUnknownError && e.requestId ? [{ tool: 'tenant_operation_status', arguments: { requestId: e.requestId }, automatic: false, requiredScopes: ['knowledge.workflow.read'] }] : exit === 3 ? [{ command: 'native-knowledge auth login', automatic: false }] : code === 'CLI_LOCAL_ACCESS_REMOVED' ? [{ command: 'native-knowledge help', automatic: false, documentation: 'docs/operations/tenant-cli-migration.md' }] : [], ...((e instanceof TenantManagementSdkError || e instanceof TenantOperationUnknownError) ? { completedPhase: e.completedPhase, ref: e.ref } : {}), ...(e instanceof TenantOperationUnknownError ? { requestId: e.requestId } : {}) } };
7
+ }
package/dist/input.js ADDED
@@ -0,0 +1,89 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { AuthError } from '@native-knowledge/knowledge-sdk';
4
+ import { localFlags, retired } from './commands.js';
5
+ const values = ['server', 'profile', 'config-dir', 'client-id', 'scope', 'authority-version', 'callback-port', 'timeout-seconds', 'token-file', 'tenant-id', 'input-file', 'input-json', 'arguments-file', 'content-file', 'tool', 'name', 'ref', 'query', 'after', 'limit', 'project-key', 'project-ref', 'source-id', 'request-id', 'job-ref', 'expected-epoch', 'expected-version', 'expected-generation', 'action', 'output-file', 'output', 'range', 'expected-content-hash', 'kind', 'index', 'state-index', 'resource-type', 'source-project', 'ref-prefix', 'after-ordinal', 'document-ref', 'category', 'goal'];
6
+ const booleans = ['human', 'json', 'help', 'non-interactive', 'no-browser', 'allow-insecure-http', 'head', 'all', 'guided'];
7
+ export function parse(argv) { const words = [], flags = new Map(); for (let i = 0; i < argv.length; i++) {
8
+ const s = argv[i];
9
+ if (!s.startsWith('--')) {
10
+ words.push(s);
11
+ continue;
12
+ }
13
+ const k = s.slice(2);
14
+ if (retired.has(words.join(' ')) || localFlags.has(k))
15
+ throw new AuthError('CLI_LOCAL_ACCESS_REMOVED');
16
+ if (!values.includes(k) && !booleans.includes(k))
17
+ throw new AuthError('CLI_INPUT_INVALID');
18
+ const v = booleans.includes(k) ? 'true' : argv[++i];
19
+ if (!v || v.startsWith('--') || flags.has(k) && k !== 'scope')
20
+ throw new AuthError('CLI_INPUT_INVALID');
21
+ flags.set(k, [...(flags.get(k) ?? []), v]);
22
+ } return { command: words.join(' '), flags }; }
23
+ export function one(f, k, required = false) { const v = f.get(k)?.[0]; if (required && !v)
24
+ throw new AuthError('CLI_INPUT_REQUIRED'); return v; }
25
+ export function integer(f, k, fallback, min, max) { const raw = one(f, k), n = raw === undefined ? fallback : Number(raw); if (!Number.isSafeInteger(n) || n < min || n > max)
26
+ throw new AuthError('CLI_INPUT_INVALID'); return n; }
27
+ export async function smallFile(file, max) { const h = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW); try {
28
+ const s = await h.stat();
29
+ if (!s.isFile() || s.size > max)
30
+ throw new AuthError('CLI_INPUT_BUDGET');
31
+ const b = Buffer.alloc(max + 1);
32
+ let n = 0;
33
+ while (n < b.length) {
34
+ const p = await h.read(b, n, b.length - n, n);
35
+ if (!p.bytesRead)
36
+ break;
37
+ n += p.bytesRead;
38
+ }
39
+ if (n > max)
40
+ throw new AuthError('CLI_INPUT_BUDGET');
41
+ return b.subarray(0, n);
42
+ }
43
+ finally {
44
+ await h.close();
45
+ } }
46
+ export async function argumentsFor(f, command) {
47
+ const file = one(f, 'input-file') ?? one(f, 'arguments-file'), json = one(f, 'input-json');
48
+ if (file && json || f.has('input-file') && f.has('arguments-file'))
49
+ throw new AuthError('CLI_INPUT_CONFLICT');
50
+ let input = {};
51
+ if (file || json) {
52
+ const text = file ? (await smallFile(file, 131072)).toString('utf8') : json;
53
+ if (Buffer.byteLength(text) > 131072)
54
+ throw new AuthError('CLI_INPUT_BUDGET');
55
+ try {
56
+ input = JSON.parse(text);
57
+ }
58
+ catch {
59
+ throw new AuthError('CLI_INPUT_INVALID');
60
+ }
61
+ if (!input || typeof input !== 'object' || Array.isArray(input))
62
+ throw new AuthError('CLI_INPUT_INVALID');
63
+ }
64
+ for (const k of ['ref', 'query', 'after', 'limit', 'project-key', 'project-ref', 'source-id', 'request-id', 'job-ref', 'expected-epoch', 'expected-version', 'expected-generation', 'action', 'kind', 'index', 'state-index', 'resource-type', 'source-project', 'ref-prefix', 'after-ordinal', 'document-ref']) {
65
+ const v = one(f, k);
66
+ if (v === undefined)
67
+ continue;
68
+ const camel = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
69
+ if (Object.hasOwn(input, camel))
70
+ throw new AuthError('CLI_INPUT_CONFLICT');
71
+ input[camel] = ['limit', 'expected-epoch', 'expected-version', 'expected-generation', 'index', 'state-index', 'after-ordinal'].includes(k) ? integer(f, k, 0, 0, Number.MAX_SAFE_INTEGER) : v;
72
+ }
73
+ if (command.startsWith('web-collect ') && ['pause', 'resume', 'cancel'].includes(command.split(' ')[1])) {
74
+ if (input.action !== undefined)
75
+ throw new AuthError('CLI_INPUT_CONFLICT');
76
+ input.action = command.split(' ')[1].toUpperCase();
77
+ }
78
+ if (command === 'insight compose') {
79
+ if (input.outputKind !== undefined && input.outputKind !== 'INSIGHT')
80
+ throw new AuthError('CLI_INPUT_CONFLICT');
81
+ input.outputKind = 'INSIGHT';
82
+ }
83
+ if (f.has('content-file')) {
84
+ if (!['agent submit', 'artifact draft', 'tool call'].includes(command) || Object.hasOwn(input, 'contentBase64'))
85
+ throw new AuthError('CLI_INPUT_CONFLICT');
86
+ input.contentBase64 = (await smallFile(one(f, 'content-file', true), 65536)).toString('base64');
87
+ }
88
+ return input;
89
+ }
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+ import { accessClassificationSchema, isoDateTimeSchema, projectKeySchema, schemaVersionSchema, sha256Schema, stableUriSchema, } from './common.js';
3
+ export const artifactIdSchema = z.string().trim().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
4
+ export const artifactKindSchema = z.enum([
5
+ 'SIGNAL_GENERATION',
6
+ 'RESEARCH',
7
+ 'REVIEW',
8
+ 'KNOWLEDGE',
9
+ 'INSIGHT',
10
+ ]);
11
+ export const artifactStatusSchema = z.enum([
12
+ 'GENERATED',
13
+ 'DRAFT',
14
+ 'READY_FOR_REVIEW',
15
+ 'REWORK_REQUIRED',
16
+ 'APPROVED',
17
+ 'REJECTED',
18
+ 'SUPERSEDED',
19
+ ]);
20
+ export const artifactManifestSchema = z.object({
21
+ schemaVersion: schemaVersionSchema,
22
+ artifactId: artifactIdSchema,
23
+ kind: artifactKindSchema,
24
+ projectKey: projectKeySchema,
25
+ projectRef: stableUriSchema.optional(),
26
+ version: z.number().int().positive(),
27
+ ref: stableUriSchema,
28
+ identityRef: stableUriSchema,
29
+ status: artifactStatusSchema,
30
+ access: accessClassificationSchema.default('INTERNAL'),
31
+ title: z.string().trim().min(1).max(500).optional(),
32
+ createdAt: isoDateTimeSchema,
33
+ observedAt: isoDateTimeSchema.nullable().default(null),
34
+ validUntil: isoDateTimeSchema.nullable().default(null),
35
+ contentHash: sha256Schema,
36
+ mediaType: z.string().trim().min(1).max(256),
37
+ byteLength: z.number().int().nonnegative(),
38
+ evidenceRefs: z.array(stableUriSchema).max(256).default([]),
39
+ relatedRefs: z.array(stableUriSchema).max(256).default([]),
40
+ supersedesRef: stableUriSchema.nullable().default(null),
41
+ }).strict().superRefine((value, ctx) => {
42
+ if (value.projectRef) {
43
+ try {
44
+ const url = new URL(value.projectRef);
45
+ if (url.protocol !== 'knowledge-project:' || url.hostname.toLowerCase() !== value.projectKey.toLowerCase())
46
+ ctx.addIssue({ code: 'custom', path: ['projectRef'], message: 'projectRef project must match Artifact project' });
47
+ }
48
+ catch {
49
+ ctx.addIssue({ code: 'custom', path: ['projectRef'], message: 'Invalid projectRef' });
50
+ }
51
+ }
52
+ if (value.validUntil && value.observedAt && Date.parse(value.validUntil) < Date.parse(value.observedAt)) {
53
+ ctx.addIssue({ code: 'custom', path: ['validUntil'], message: 'validUntil must not precede observedAt' });
54
+ }
55
+ if (value.kind === 'SIGNAL_GENERATION' && (value.version !== 1 || value.status !== 'GENERATED')) {
56
+ ctx.addIssue({ code: 'custom', path: ['version'], message: 'Signal generations are immutable generated artifacts at version 1' });
57
+ }
58
+ if (['KNOWLEDGE', 'INSIGHT'].includes(value.kind) && value.status === 'GENERATED') {
59
+ ctx.addIssue({ code: 'custom', path: ['status'], message: 'Knowledge/Insight cannot use GENERATED status' });
60
+ }
61
+ });
62
+ export const artifactWriteMetadataSchema = z.object({
63
+ schemaVersion: schemaVersionSchema,
64
+ artifactId: artifactIdSchema,
65
+ kind: artifactKindSchema,
66
+ projectKey: projectKeySchema,
67
+ projectRef: stableUriSchema.optional(),
68
+ version: z.number().int().positive(),
69
+ status: artifactStatusSchema,
70
+ access: accessClassificationSchema.default('INTERNAL'),
71
+ title: z.string().trim().min(1).max(500).optional(),
72
+ createdAt: isoDateTimeSchema,
73
+ observedAt: isoDateTimeSchema.nullable().default(null),
74
+ validUntil: isoDateTimeSchema.nullable().default(null),
75
+ mediaType: z.string().trim().min(1).max(256),
76
+ evidenceRefs: z.array(stableUriSchema).max(256).default([]),
77
+ relatedRefs: z.array(stableUriSchema).max(256).default([]),
78
+ supersedesRef: stableUriSchema.nullable().default(null),
79
+ }).strict().superRefine((value, ctx) => {
80
+ if (value.validUntil && value.observedAt && Date.parse(value.validUntil) < Date.parse(value.observedAt)) {
81
+ ctx.addIssue({ code: 'custom', path: ['validUntil'], message: 'validUntil must not precede observedAt' });
82
+ }
83
+ if (value.kind === 'SIGNAL_GENERATION') {
84
+ ctx.addIssue({ code: 'custom', path: ['kind'], message: 'Signal generations are produced only by the signal composer' });
85
+ }
86
+ if (value.kind === 'REVIEW') {
87
+ ctx.addIssue({ code: 'custom', path: ['kind'], message: 'Review artifacts are produced only by the review lifecycle' });
88
+ }
89
+ if (['KNOWLEDGE', 'INSIGHT'].includes(value.kind) && value.status !== 'DRAFT') {
90
+ ctx.addIssue({ code: 'custom', path: ['status'], message: 'Knowledge/Insight manual writes must enter as DRAFT' });
91
+ }
92
+ if (value.kind === 'RESEARCH' && value.status !== 'DRAFT') {
93
+ ctx.addIssue({ code: 'custom', path: ['status'], message: 'Research manual writes must enter as DRAFT' });
94
+ }
95
+ });
96
+ export const reviewDecisionSchema = z.object({
97
+ schemaVersion: schemaVersionSchema,
98
+ targetRef: stableUriSchema,
99
+ decision: z.enum(['PASS', 'REWORK', 'REJECT']),
100
+ reviewedAt: isoDateTimeSchema,
101
+ summary: z.string().trim().min(1).max(16000),
102
+ evidenceRefs: z.array(stableUriSchema).max(256).default([]),
103
+ }).strict();
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ import { idSchema, isoDateTimeSchema, projectKeySchema, schemaVersionSchema, stableUriSchema } from './common.js';
3
+ export const knowledgeControlScopeSchema = z.enum([
4
+ 'knowledge.read',
5
+ 'knowledge.collect',
6
+ 'knowledge.source.manage',
7
+ 'knowledge.review',
8
+ 'knowledge.publish',
9
+ ]);
10
+ export const collectionControlRequestIdSchema = idSchema.regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/);
11
+ export const collectionControlCreateRequestSchema = z.object({
12
+ projectKey: projectKeySchema,
13
+ requestId: collectionControlRequestIdSchema,
14
+ sourceIds: z.array(idSchema).min(1).max(1_000),
15
+ }).strict().transform((value) => ({
16
+ ...value,
17
+ sourceIds: [...new Set(value.sourceIds)].sort(),
18
+ }));
19
+ export const collectionControlAuditSchema = z.object({
20
+ schemaVersion: schemaVersionSchema,
21
+ projectKey: projectKeySchema,
22
+ requestId: collectionControlRequestIdSchema,
23
+ batchId: idSchema,
24
+ batchRef: stableUriSchema,
25
+ tenantId: idSchema,
26
+ clientId: idSchema,
27
+ requestedAt: isoDateTimeSchema,
28
+ sourceIds: z.array(idSchema).min(1).max(1_000),
29
+ sourceRevisionRefs: z.array(stableUriSchema).min(1).max(1_000),
30
+ admission: z.literal('ACCEPTED'),
31
+ }).strict().superRefine((value, ctx) => {
32
+ if (value.sourceIds.length !== value.sourceRevisionRefs.length)
33
+ ctx.addIssue({
34
+ code: 'custom',
35
+ path: ['sourceRevisionRefs'],
36
+ message: 'sourceRevisionRefs must align with sourceIds',
37
+ });
38
+ });
39
+ export const collectionControlStatusSchema = z.enum(['PENDING', 'SUCCEEDED', 'PARTIAL', 'FAILED']);
40
+ export const collectionControlRunSummarySchema = z.object({
41
+ runId: idSchema,
42
+ runtimeNodeId: idSchema,
43
+ startedAt: isoDateTimeSchema,
44
+ finishedAt: isoDateTimeSchema,
45
+ status: z.enum(['SUCCEEDED', 'PARTIAL', 'FAILED']),
46
+ }).strict();
47
+ export const collectionControlRequestProjectionSchema = z.object({
48
+ schemaVersion: schemaVersionSchema,
49
+ projectKey: projectKeySchema,
50
+ requestId: collectionControlRequestIdSchema,
51
+ batchId: idSchema,
52
+ batchRef: stableUriSchema,
53
+ trigger: z.literal('MANUAL'),
54
+ status: collectionControlStatusSchema,
55
+ requestedAt: isoDateTimeSchema,
56
+ requestedBy: z.object({ tenantId: idSchema, clientId: idSchema }).strict(),
57
+ sourceIds: z.array(idSchema).min(1).max(1_000),
58
+ sourceRevisionRefs: z.array(stableUriSchema).min(1).max(1_000),
59
+ run: collectionControlRunSummarySchema.nullable(),
60
+ }).strict();
61
+ export const collectionControlCreateResponseSchema = z.object({
62
+ schemaVersion: schemaVersionSchema,
63
+ inserted: z.boolean(),
64
+ request: collectionControlRequestProjectionSchema,
65
+ }).strict();
66
+ export const collectionControlGetResponseSchema = z.object({
67
+ schemaVersion: schemaVersionSchema,
68
+ request: collectionControlRequestProjectionSchema,
69
+ }).strict();
@@ -0,0 +1,174 @@
1
+ import { z } from 'zod';
2
+ import { idSchema, isoDateTimeSchema, projectKeySchema, schemaVersionSchema, sha256Schema, stableUriSchema, } from './common.js';
3
+ import { acquisitionTargetSchema, collectionRefreshSchema } from './project-intent.js';
4
+ export const collectionBatchSourceBindingSchema = z.object({
5
+ sourceId: idSchema,
6
+ sourceRef: stableUriSchema,
7
+ sourceRevisionRef: stableUriSchema,
8
+ targetId: sha256Schema,
9
+ }).strict();
10
+ /** The collector-facing task deliberately contains no project/source-family semantics. */
11
+ export const collectionTaskSchema = z.object({
12
+ taskId: idSchema,
13
+ targetId: sha256Schema,
14
+ target: acquisitionTargetSchema,
15
+ }).strict();
16
+ export const collectionBatchTriggerSchema = z.enum(['MANUAL', 'DUE']);
17
+ export const collectionBatchSchema = z.object({
18
+ schemaVersion: schemaVersionSchema,
19
+ projectKey: projectKeySchema,
20
+ projectRef: stableUriSchema,
21
+ projectIntentRef: stableUriSchema,
22
+ requestId: idSchema,
23
+ batchId: idSchema,
24
+ batchRef: stableUriSchema,
25
+ trigger: collectionBatchTriggerSchema.default('MANUAL'),
26
+ createdAt: isoDateTimeSchema,
27
+ sourceBindings: z.array(collectionBatchSourceBindingSchema).min(1).max(10_000),
28
+ tasks: z.array(collectionTaskSchema).min(1).max(10_000),
29
+ }).strict().superRefine((value, ctx) => {
30
+ const sourceIds = new Set(), bindingTargets = new Set(), taskIds = new Set(), taskTargets = new Set();
31
+ for (let i = 0; i < value.sourceBindings.length; i += 1) {
32
+ const row = value.sourceBindings[i];
33
+ if (sourceIds.has(row.sourceId))
34
+ ctx.addIssue({ code: 'custom', path: ['sourceBindings', i, 'sourceId'], message: 'sourceId must be unique in a batch' });
35
+ if (bindingTargets.has(row.targetId))
36
+ ctx.addIssue({ code: 'custom', path: ['sourceBindings', i, 'targetId'], message: 'targetId must be unique in project source bindings' });
37
+ sourceIds.add(row.sourceId);
38
+ bindingTargets.add(row.targetId);
39
+ }
40
+ for (let i = 0; i < value.tasks.length; i += 1) {
41
+ const row = value.tasks[i];
42
+ if (taskIds.has(row.taskId))
43
+ ctx.addIssue({ code: 'custom', path: ['tasks', i, 'taskId'], message: 'taskId must be unique in a batch' });
44
+ if (taskTargets.has(row.targetId))
45
+ ctx.addIssue({ code: 'custom', path: ['tasks', i, 'targetId'], message: 'targetId must be unique in a batch' });
46
+ taskIds.add(row.taskId);
47
+ taskTargets.add(row.targetId);
48
+ }
49
+ for (let i = 0; i < value.tasks.length; i += 1)
50
+ if (!bindingTargets.has(value.tasks[i].targetId))
51
+ ctx.addIssue({ code: 'custom', path: ['tasks', i, 'targetId'], message: 'task target must have a source binding' });
52
+ for (let i = 0; i < value.sourceBindings.length; i += 1)
53
+ if (!taskTargets.has(value.sourceBindings[i].targetId))
54
+ ctx.addIssue({ code: 'custom', path: ['sourceBindings', i, 'targetId'], message: 'source binding must have a collector task' });
55
+ });
56
+ export const collectorBatchViewSchema = z.object({
57
+ schemaVersion: schemaVersionSchema,
58
+ batchId: idSchema,
59
+ tasks: z.array(collectionTaskSchema).min(1).max(10_000),
60
+ }).strict();
61
+ export const sourceCollectionDueReasonSchema = z.enum([
62
+ 'DISABLED',
63
+ 'MANUAL',
64
+ 'PENDING_BATCH',
65
+ 'SPECIALIZED_EXECUTOR',
66
+ 'SPECIALIZED_COLLECTOR',
67
+ 'NEVER_ATTEMPTED',
68
+ 'INTERVAL_WAIT',
69
+ 'INTERVAL_ELAPSED',
70
+ ]);
71
+ export const sourceCollectionDueEntrySchema = z.object({
72
+ sourceId: idSchema,
73
+ sourceRef: stableUriSchema,
74
+ sourceRevisionRef: stableUriSchema,
75
+ enabled: z.boolean(),
76
+ refresh: collectionRefreshSchema,
77
+ lastAttemptAt: isoDateTimeSchema.nullable(),
78
+ lastState: z.enum(['ACQUIRED', 'ACCESS_CONTROLLED', 'HTTP_TERMINAL', 'FAILED']).nullable(),
79
+ lastBatchRef: stableUriSchema.nullable(),
80
+ lastRunId: idSchema.nullable(),
81
+ pendingBatchRef: stableUriSchema.nullable(),
82
+ nextDueAt: isoDateTimeSchema.nullable(),
83
+ due: z.boolean(),
84
+ reason: sourceCollectionDueReasonSchema,
85
+ }).strict().superRefine((value, ctx) => {
86
+ if (value.reason === 'PENDING_BATCH' && !value.pendingBatchRef)
87
+ ctx.addIssue({ code: 'custom', path: ['pendingBatchRef'], message: 'PENDING_BATCH requires pendingBatchRef' });
88
+ if (value.reason !== 'PENDING_BATCH' && value.pendingBatchRef)
89
+ ctx.addIssue({ code: 'custom', path: ['pendingBatchRef'], message: 'pendingBatchRef is only valid for PENDING_BATCH' });
90
+ if (value.due && !['NEVER_ATTEMPTED', 'INTERVAL_ELAPSED'].includes(value.reason))
91
+ ctx.addIssue({ code: 'custom', path: ['due'], message: 'due source must be never attempted or interval elapsed' });
92
+ if (value.refresh.mode === 'MANUAL' && value.nextDueAt !== null)
93
+ ctx.addIssue({ code: 'custom', path: ['nextDueAt'], message: 'MANUAL source has no automatic nextDueAt' });
94
+ });
95
+ export const projectCollectionDuePlanSchema = z.object({
96
+ schemaVersion: schemaVersionSchema,
97
+ projectKey: projectKeySchema,
98
+ projectRef: stableUriSchema,
99
+ projectIntentRef: stableUriSchema,
100
+ assessedAt: isoDateTimeSchema,
101
+ sourceCount: z.number().int().nonnegative(),
102
+ dueSourceCount: z.number().int().nonnegative(),
103
+ dueSourceIds: z.array(idSchema).max(10_000),
104
+ sources: z.array(sourceCollectionDueEntrySchema).max(10_000),
105
+ }).strict().superRefine((value, ctx) => {
106
+ const due = value.sources.filter((row) => row.due).map((row) => row.sourceId);
107
+ if (value.sourceCount !== value.sources.length)
108
+ ctx.addIssue({ code: 'custom', path: ['sourceCount'], message: 'sourceCount must match sources' });
109
+ if (value.dueSourceCount !== due.length)
110
+ ctx.addIssue({ code: 'custom', path: ['dueSourceCount'], message: 'dueSourceCount must match due sources' });
111
+ if (JSON.stringify(value.dueSourceIds) !== JSON.stringify(due))
112
+ ctx.addIssue({ code: 'custom', path: ['dueSourceIds'], message: 'dueSourceIds must match ordered due sources' });
113
+ });
114
+ export const collectionTaskRunResultSchema = z.object({
115
+ taskId: idSchema,
116
+ targetId: sha256Schema,
117
+ sourceId: idSchema,
118
+ sourceRevisionRef: stableUriSchema,
119
+ observedAt: isoDateTimeSchema,
120
+ state: z.enum(['ACQUIRED', 'ACCESS_CONTROLLED', 'HTTP_TERMINAL', 'FAILED']),
121
+ finalLocator: stableUriSchema.nullable(),
122
+ httpStatus: z.number().int().min(100).max(599).nullable(),
123
+ evidenceRef: stableUriSchema.nullable(),
124
+ contentHash: sha256Schema.nullable(),
125
+ byteLength: z.number().int().nonnegative().nullable(),
126
+ mediaType: z.string().trim().min(1).max(256).nullable(),
127
+ reusedEvidence: z.boolean(),
128
+ relatedEvidenceRefs: z.array(stableUriSchema).max(512).default([]),
129
+ richDocumentRef: stableUriSchema.nullable().default(null),
130
+ projectionJobId: idSchema.optional(),
131
+ projectionState: z.enum(['PENDING', 'SUCCEEDED', 'PARTIAL', 'FAILED', 'NOT_REQUIRED']).optional(),
132
+ errorCode: z.string().trim().min(1).max(256).nullable(),
133
+ }).strict().superRefine((value, ctx) => {
134
+ const acquired = value.state === 'ACQUIRED';
135
+ if (acquired && (!value.evidenceRef || !value.contentHash || value.byteLength === null || !value.mediaType))
136
+ ctx.addIssue({ code: 'custom', path: ['evidenceRef'], message: 'ACQUIRED result requires persisted Evidence identity' });
137
+ if (!acquired && (value.evidenceRef || value.contentHash || value.byteLength !== null || value.mediaType || value.reusedEvidence || value.relatedEvidenceRefs.length || value.richDocumentRef))
138
+ ctx.addIssue({ code: 'custom', path: ['evidenceRef'], message: 'non-ACQUIRED result cannot claim persisted Evidence' });
139
+ if (value.projectionState === 'PENDING' && (!value.projectionJobId || value.richDocumentRef || value.relatedEvidenceRefs.length || value.errorCode))
140
+ ctx.addIssue({ code: 'custom', path: ['projectionState'], message: 'pending projection requires a job and no derived result' });
141
+ if (value.projectionState === 'SUCCEEDED' && !value.richDocumentRef)
142
+ ctx.addIssue({ code: 'custom', path: ['projectionState'], message: 'successful projection requires a document' });
143
+ if (value.projectionState === 'PARTIAL' && (!acquired || !value.errorCode || !value.richDocumentRef))
144
+ ctx.addIssue({ code: 'custom', path: ['projectionState'], message: 'partial projection requires acquired Evidence, document and error' });
145
+ if (value.projectionState === 'NOT_REQUIRED' && value.richDocumentRef)
146
+ ctx.addIssue({ code: 'custom', path: ['projectionState'], message: 'unrequired projection cannot claim a document' });
147
+ if (value.projectionState === 'FAILED' && (!acquired || !value.errorCode || value.richDocumentRef))
148
+ ctx.addIssue({ code: 'custom', path: ['projectionState'], message: 'failed projection requires acquired raw Evidence, error and no document' });
149
+ if (!acquired && value.projectionState)
150
+ ctx.addIssue({ code: 'custom', path: ['projectionState'], message: 'projection requires acquired raw Evidence' });
151
+ if (value.state === 'FAILED' && !value.errorCode)
152
+ ctx.addIssue({ code: 'custom', path: ['errorCode'], message: 'FAILED result requires errorCode' });
153
+ });
154
+ export const collectionRunReceiptSchema = z.object({
155
+ schemaVersion: schemaVersionSchema,
156
+ projectKey: projectKeySchema,
157
+ projectRef: stableUriSchema,
158
+ batchId: idSchema,
159
+ batchRef: stableUriSchema,
160
+ runId: idSchema,
161
+ runtimeNodeId: idSchema,
162
+ startedAt: isoDateTimeSchema,
163
+ finishedAt: isoDateTimeSchema,
164
+ status: z.enum(['SUCCEEDED', 'PARTIAL', 'FAILED']),
165
+ results: z.array(collectionTaskRunResultSchema).min(1).max(10_000),
166
+ }).strict().superRefine((value, ctx) => {
167
+ if (Date.parse(value.finishedAt) < Date.parse(value.startedAt))
168
+ ctx.addIssue({ code: 'custom', path: ['finishedAt'], message: 'finishedAt must not precede startedAt' });
169
+ const acquired = value.results.filter((row) => row.state === 'ACQUIRED').length;
170
+ const incomplete = value.results.some(row => row.projectionState === 'FAILED' || row.projectionState === 'PARTIAL');
171
+ const expected = acquired === 0 ? 'FAILED' : acquired === value.results.length && !incomplete ? 'SUCCEEDED' : 'PARTIAL';
172
+ if (value.status !== expected)
173
+ ctx.addIssue({ code: 'custom', path: ['status'], message: `status must be ${expected} for task results` });
174
+ });
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ export const schemaVersionSchema = z.literal(1);
3
+ export const idSchema = z.string().trim().min(1).max(256);
4
+ export const projectKeySchema = z.string().trim().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
5
+ export const isoDateTimeSchema = z.iso.datetime({ offset: true });
6
+ export const sha256Schema = z.string().regex(/^sha256:[a-f0-9]{64}$/);
7
+ export const stableUriSchema = z.string().trim().min(1).max(2048).regex(/^[a-z][a-z0-9+.-]*:\/\/.+/i);
8
+ export const accessClassificationSchema = z.enum([
9
+ 'PUBLIC',
10
+ 'PUBLIC_METADATA',
11
+ 'TENANT',
12
+ 'INTERNAL',
13
+ 'FDE_INTERNAL',
14
+ 'ACCESS_CONTROLLED',
15
+ 'UNKNOWN',
16
+ ]);