@native-knowledge/cli 0.1.0-rc.1 → 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.
- package/LICENSE +202 -21
- package/NOTICE +29 -0
- package/README.md +26 -19
- package/dist/agent-entry-cli.js +88 -0
- package/dist/auth-commands.js +102 -0
- package/dist/cli.js +101 -4874
- package/dist/commands.js +17 -0
- package/dist/download.js +45 -0
- package/dist/errors.js +7 -0
- package/dist/input.js +89 -0
- package/dist/internal/contracts/artifact.js +103 -0
- package/dist/internal/contracts/collection-control.js +69 -0
- package/dist/internal/contracts/collection.js +174 -0
- package/dist/internal/contracts/common.js +16 -0
- package/dist/internal/contracts/currentness.js +221 -0
- package/dist/internal/contracts/event.js +35 -0
- package/dist/internal/contracts/evidence.js +171 -0
- package/dist/internal/contracts/index.js +54 -0
- package/dist/internal/contracts/material-capability.js +8 -0
- package/dist/internal/contracts/material-carrier.js +15 -0
- package/dist/internal/contracts/material-context.js +9 -0
- package/dist/internal/contracts/material-read.js +12 -0
- package/dist/internal/contracts/observation.js +39 -0
- package/dist/internal/contracts/persistence.js +239 -0
- package/dist/internal/contracts/project-intent.js +121 -0
- package/dist/internal/contracts/projection.js +194 -0
- package/dist/internal/contracts/public-reference.js +87 -0
- package/dist/internal/contracts/reference-record.js +55 -0
- package/dist/internal/contracts/remote-file.js +159 -0
- package/dist/internal/contracts/resource-scope.js +38 -0
- package/dist/internal/contracts/review-packet.js +61 -0
- package/dist/internal/contracts/review-plane.js +89 -0
- package/dist/internal/contracts/rich-document.js +184 -0
- package/dist/internal/contracts/runtime-compatibility.js +55 -0
- package/dist/internal/contracts/runtime.js +68 -0
- package/dist/internal/contracts/scheduler.js +256 -0
- package/dist/internal/contracts/signal.js +77 -0
- package/dist/internal/contracts/source-governance.js +194 -0
- package/dist/internal/contracts/source.js +32 -0
- package/dist/internal/contracts/tenant-budget.js +34 -0
- package/dist/internal/contracts/tenant-collection-request.js +58 -0
- package/dist/internal/contracts/tenant-collection.js +66 -0
- package/dist/internal/contracts/tenant-currentness.js +26 -0
- package/dist/internal/contracts/tenant-management.js +27 -0
- package/dist/internal/contracts/tenant-material-api.js +46 -0
- package/dist/internal/contracts/tenant-material-json-schema.js +16 -0
- package/dist/internal/contracts/tenant-pdf-text.js +13 -0
- package/dist/internal/contracts/tenant-project-control.js +19 -0
- package/dist/internal/contracts/tenant-public-reference.js +26 -0
- package/dist/internal/contracts/tenant-resource-api.js +70 -0
- package/dist/internal/contracts/tenant-resource.js +118 -0
- package/dist/internal/contracts/tenant-rich-document.js +17 -0
- package/dist/internal/contracts/tenant-source-binding.js +33 -0
- package/dist/internal/contracts/tenant-source-bridge.js +9 -0
- package/dist/internal/contracts/tenant-source-cutover.js +26 -0
- package/dist/internal/contracts/tenant-source-document.js +38 -0
- package/dist/internal/contracts/tenant-source-lease.js +5 -0
- package/dist/internal/contracts/tenant-source.js +83 -0
- package/dist/internal/contracts/tenant-web-collection.js +118 -0
- package/dist/internal/contracts/tenant-web-content-plan.js +39 -0
- package/dist/internal/contracts/tenant-web-content-result.js +15 -0
- package/dist/internal/contracts/tenant-web-content.js +16 -0
- package/dist/internal/contracts/tenant-web-crawl.js +55 -0
- package/dist/internal/contracts/tenant-web-dynamic.js +68 -0
- package/dist/internal/contracts/tenant-web-results.js +41 -0
- package/dist/internal/contracts/tenant-web-workflow.js +29 -0
- package/dist/internal/serving-contracts/agent-catalog.js +56 -0
- package/dist/internal/serving-contracts/agent-entry.js +57 -0
- package/dist/internal/serving-contracts/agent-guidance-shapes.js +12 -0
- package/dist/internal/serving-contracts/agent-input-hints.js +104 -0
- package/dist/internal/serving-contracts/body-search.js +16 -0
- package/dist/internal/serving-contracts/consumer.js +45 -0
- package/dist/internal/serving-contracts/core.js +76 -0
- package/dist/internal/serving-contracts/http.js +64 -0
- package/dist/internal/serving-contracts/index.js +25 -0
- package/dist/internal/serving-contracts/management.js +15 -0
- package/dist/internal/serving-contracts/material-capabilities.js +14 -0
- package/dist/internal/serving-contracts/material-index-status.js +5 -0
- package/dist/internal/serving-contracts/mcp.js +140 -0
- package/dist/internal/serving-contracts/tenant-governance.js +13 -0
- package/dist/internal/serving-contracts/tenant-management.js +89 -0
- package/dist/internal/serving-contracts/tenant-project.js +39 -0
- package/dist/internal/serving-contracts/tenant-runtime.js +37 -0
- package/dist/internal/serving-contracts/tenant-session.js +8 -0
- package/dist/internal/serving-contracts/tenant-web-content.js +20 -0
- package/dist/internal/serving-contracts/tenant-web-crawl.js +13 -0
- package/dist/internal/serving-contracts/tenant-web-dynamic.js +20 -0
- package/dist/internal/serving-contracts/tenant-web-workflow.js +36 -0
- package/dist/internal/serving-contracts/tenant-web.js +53 -0
- package/dist/internal/serving-contracts/tenant-workspace.js +28 -0
- package/dist/internal/serving-contracts/workflow.js +37 -0
- package/dist/profile-store.js +142 -0
- package/package.json +9 -13
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Canonical Reference facts and producer annotations; no consumer admission policy. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { accessClassificationSchema, idSchema, isoDateTimeSchema, projectKeySchema, schemaVersionSchema, sha256Schema, stableUriSchema } from './common.js';
|
|
4
|
+
export const referenceRecordTypeSchema = z.string().trim().min(1).max(128).regex(/^[A-Z][A-Z0-9_]*$/);
|
|
5
|
+
export const referenceCurrentnessActionSchema = z.enum(['ALLOW', 'REVIEW_REQUIRED', 'DENY']);
|
|
6
|
+
export const referenceSelectorKeySchema = z.string().trim().min(1).max(64).regex(/^[A-Za-z][A-Za-z0-9._-]{0,63}$/);
|
|
7
|
+
export const referenceSelectorValueSchema = z.string().trim().min(1).max(256);
|
|
8
|
+
export const referenceSelectorsSchema = z.record(referenceSelectorKeySchema, z.array(referenceSelectorValueSchema).min(1).max(256)).superRefine((value, ctx) => {
|
|
9
|
+
const keys = Object.keys(value);
|
|
10
|
+
if (keys.length > 32)
|
|
11
|
+
ctx.addIssue({ code: 'custom', message: 'Reference selectors cannot exceed 32 keys' });
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
const values = value[key] ?? [];
|
|
14
|
+
if (new Set(values).size !== values.length)
|
|
15
|
+
ctx.addIssue({ code: 'custom', path: [key], message: 'Reference selector values must be unique' });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
export const referenceCurrentnessStateSchema = z.enum(['CURRENT', 'DUE', 'STALE', 'UNAVAILABLE', 'NOT_COVERED']);
|
|
19
|
+
export const referenceCurrentnessSchema = z.object({
|
|
20
|
+
state: referenceCurrentnessStateSchema, action: referenceCurrentnessActionSchema, assessedAt: isoDateTimeSchema, validUntil: isoDateTimeSchema.nullable(),
|
|
21
|
+
}).strict();
|
|
22
|
+
function validateReferenceIdentity(value, ctx) {
|
|
23
|
+
const expected = `reference://${value.projectKey}/${encodeURIComponent(value.corpusKey)}/${encodeURIComponent(value.recordKey)}/v/${value.version}`;
|
|
24
|
+
if (value.recordRef !== expected)
|
|
25
|
+
ctx.addIssue({ code: 'custom', path: ['recordRef'], message: 'recordRef must match project/corpus/recordKey/version' });
|
|
26
|
+
if (value.version === 1 && value.supersedesRef !== null)
|
|
27
|
+
ctx.addIssue({ code: 'custom', path: ['supersedesRef'], message: 'first reference version cannot supersede' });
|
|
28
|
+
if (value.version > 1) {
|
|
29
|
+
const previous = `reference://${value.projectKey}/${encodeURIComponent(value.corpusKey)}/${encodeURIComponent(value.recordKey)}/v/${value.version - 1}`;
|
|
30
|
+
if (value.supersedesRef !== previous)
|
|
31
|
+
ctx.addIssue({ code: 'custom', path: ['supersedesRef'], message: 'reference version must supersede immediately previous version' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export const referenceCorpusSummarySchema = z.object({
|
|
35
|
+
schemaVersion: schemaVersionSchema, corpusKey: projectKeySchema, recordKey: idSchema, recordRef: stableUriSchema, recordType: referenceRecordTypeSchema, projectKey: projectKeySchema, projectRef: stableUriSchema.optional(),
|
|
36
|
+
version: z.number().int().positive(), supersedesRef: stableUriSchema.nullable(),
|
|
37
|
+
access: accessClassificationSchema, authority: z.string().trim().min(1).max(1000), jurisdiction: z.string().trim().min(1).max(128).nullable(), selectors: referenceSelectorsSchema.optional(),
|
|
38
|
+
sourceRefs: z.array(stableUriSchema).min(1).max(256), digest: sha256Schema, effectiveFrom: isoDateTimeSchema.nullable(), effectiveUntil: isoDateTimeSchema.nullable(),
|
|
39
|
+
currentness: referenceCurrentnessSchema,
|
|
40
|
+
}).strict().superRefine((value, ctx) => {
|
|
41
|
+
if (value.projectRef) {
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(value.projectRef);
|
|
44
|
+
if (url.protocol !== 'knowledge-project:' || url.hostname.toLowerCase() !== value.projectKey.toLowerCase())
|
|
45
|
+
ctx.addIssue({ code: 'custom', path: ['projectRef'], message: 'projectRef project must match Reference project' });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
ctx.addIssue({ code: 'custom', path: ['projectRef'], message: 'Invalid projectRef' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (value.effectiveFrom && value.effectiveUntil && Date.parse(value.effectiveUntil) <= Date.parse(value.effectiveFrom))
|
|
52
|
+
ctx.addIssue({ code: 'custom', path: ['effectiveUntil'], message: 'effectiveUntil must be after effectiveFrom' });
|
|
53
|
+
validateReferenceIdentity(value, ctx);
|
|
54
|
+
});
|
|
55
|
+
export const referenceCorpusRecordSchema = referenceCorpusSummarySchema.extend({ payload: z.json() }).strict();
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { idSchema, isoDateTimeSchema, projectKeySchema, schemaVersionSchema, sha256Schema, stableUriSchema } from './common.js';
|
|
3
|
+
export const remoteArchiveFormatSchema = z.enum(['TAR_GZ', 'ZIP']);
|
|
4
|
+
export const remoteManifestFormatSchema = z.enum(['FILES_V1', 'LEGACY_PAGES_V1']);
|
|
5
|
+
export const remoteBundlePathSchema = z.string().trim().min(1).max(1024).superRefine((value, ctx) => {
|
|
6
|
+
if (value.includes('\0') || value.includes('\\') || value.startsWith('/') || value.endsWith('/')) {
|
|
7
|
+
ctx.addIssue({ code: 'custom', message: 'remote bundle member path must be a relative file path' });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const parts = value.split('/');
|
|
11
|
+
if (parts.some((part) => !part || part === '.' || part === '..')) {
|
|
12
|
+
ctx.addIssue({ code: 'custom', message: 'remote bundle member path must not contain empty, dot or parent segments' });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
export const remoteFileBundleDeclarationSchema = z.object({
|
|
16
|
+
schemaVersion: schemaVersionSchema,
|
|
17
|
+
projectKey: projectKeySchema,
|
|
18
|
+
artifactSha256: sha256Schema,
|
|
19
|
+
byteLength: z.number().int().positive(),
|
|
20
|
+
archiveFormat: remoteArchiveFormatSchema,
|
|
21
|
+
manifestPath: remoteBundlePathSchema,
|
|
22
|
+
manifestSha256: sha256Schema,
|
|
23
|
+
artifactKind: idSchema,
|
|
24
|
+
familyId: idSchema,
|
|
25
|
+
targetId: idSchema,
|
|
26
|
+
jurisdiction: z.string().trim().min(1).max(128).nullable(),
|
|
27
|
+
sourceWorker: idSchema,
|
|
28
|
+
remoteArtifactRef: stableUriSchema.nullable(),
|
|
29
|
+
declaredAt: isoDateTimeSchema,
|
|
30
|
+
}).strict();
|
|
31
|
+
export const remoteFileManifestEntrySchema = z.object({
|
|
32
|
+
path: remoteBundlePathSchema,
|
|
33
|
+
sha256: sha256Schema,
|
|
34
|
+
byteLength: z.number().int().nonnegative(),
|
|
35
|
+
mediaType: z.string().trim().min(1).max(256).nullable().default(null),
|
|
36
|
+
sourceUrl: z.string().url().max(2048).nullable().default(null),
|
|
37
|
+
}).strict();
|
|
38
|
+
export const remoteFileBundleManifestSchema = z.object({
|
|
39
|
+
schemaVersion: schemaVersionSchema,
|
|
40
|
+
projectKey: projectKeySchema,
|
|
41
|
+
artifactKind: idSchema,
|
|
42
|
+
familyId: idSchema,
|
|
43
|
+
targetId: idSchema,
|
|
44
|
+
jurisdiction: z.string().trim().min(1).max(128).nullable(),
|
|
45
|
+
generatedAt: isoDateTimeSchema,
|
|
46
|
+
files: z.array(remoteFileManifestEntrySchema).min(1).max(10000),
|
|
47
|
+
}).strict().superRefine((value, ctx) => {
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
value.files.forEach((entry, index) => {
|
|
50
|
+
if (seen.has(entry.path))
|
|
51
|
+
ctx.addIssue({ code: 'custom', path: ['files', index, 'path'], message: 'duplicate manifest file path' });
|
|
52
|
+
seen.add(entry.path);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
export const remoteVerifiedFileSchema = z.object({
|
|
56
|
+
path: remoteBundlePathSchema,
|
|
57
|
+
contentHash: sha256Schema,
|
|
58
|
+
byteLength: z.number().int().nonnegative(),
|
|
59
|
+
mediaType: z.string().trim().min(1).max(256).nullable(),
|
|
60
|
+
sourceUrl: z.string().url().max(2048).nullable(),
|
|
61
|
+
}).strict();
|
|
62
|
+
export const remoteFileVerificationReceiptSchema = z.object({
|
|
63
|
+
schemaVersion: schemaVersionSchema,
|
|
64
|
+
verificationRef: stableUriSchema,
|
|
65
|
+
projectKey: projectKeySchema,
|
|
66
|
+
declarationDigest: sha256Schema,
|
|
67
|
+
artifactSha256: sha256Schema,
|
|
68
|
+
byteLength: z.number().int().positive(),
|
|
69
|
+
archiveFormat: remoteArchiveFormatSchema,
|
|
70
|
+
manifestPath: remoteBundlePathSchema,
|
|
71
|
+
manifestSha256: sha256Schema,
|
|
72
|
+
manifestFormat: remoteManifestFormatSchema,
|
|
73
|
+
manifestGeneratedAt: isoDateTimeSchema.nullable(),
|
|
74
|
+
artifactKind: idSchema,
|
|
75
|
+
familyId: idSchema,
|
|
76
|
+
targetId: idSchema,
|
|
77
|
+
jurisdiction: z.string().trim().min(1).max(128).nullable(),
|
|
78
|
+
sourceWorker: idSchema,
|
|
79
|
+
remoteArtifactRef: stableUriSchema.nullable(),
|
|
80
|
+
verifierNodeId: idSchema,
|
|
81
|
+
verifiedAt: isoDateTimeSchema,
|
|
82
|
+
files: z.array(remoteVerifiedFileSchema).min(1).max(10000),
|
|
83
|
+
}).strict().superRefine((value, ctx) => {
|
|
84
|
+
const expected = `remote-file-verification://${value.projectKey}/${value.artifactSha256.slice('sha256:'.length)}`;
|
|
85
|
+
if (value.verificationRef !== expected)
|
|
86
|
+
ctx.addIssue({ code: 'custom', path: ['verificationRef'], message: 'verificationRef must match project and artifact hash' });
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
value.files.forEach((entry, index) => {
|
|
89
|
+
if (seen.has(entry.path))
|
|
90
|
+
ctx.addIssue({ code: 'custom', path: ['files', index, 'path'], message: 'duplicate verified file path' });
|
|
91
|
+
seen.add(entry.path);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
export const remoteFileProjectSourceBindingSchema = z.object({
|
|
95
|
+
schemaVersion: schemaVersionSchema,
|
|
96
|
+
bindingRef: stableUriSchema,
|
|
97
|
+
verificationRef: stableUriSchema,
|
|
98
|
+
projectKey: projectKeySchema,
|
|
99
|
+
artifactSha256: sha256Schema,
|
|
100
|
+
projectRef: stableUriSchema,
|
|
101
|
+
projectIntentRef: stableUriSchema,
|
|
102
|
+
sourceId: idSchema,
|
|
103
|
+
sourceRef: stableUriSchema,
|
|
104
|
+
sourceRevisionId: sha256Schema,
|
|
105
|
+
sourceRevisionRef: stableUriSchema,
|
|
106
|
+
projectSourceTargetId: sha256Schema,
|
|
107
|
+
boundAt: isoDateTimeSchema,
|
|
108
|
+
}).strict().superRefine((value, ctx) => {
|
|
109
|
+
const artifactDigest = value.artifactSha256.slice('sha256:'.length);
|
|
110
|
+
const revisionDigest = value.sourceRevisionId.slice('sha256:'.length);
|
|
111
|
+
const expectedVerification = `remote-file-verification://${value.projectKey}/${artifactDigest}`;
|
|
112
|
+
const expectedBinding = `remote-file-source-binding://${value.projectKey}/${artifactDigest}/${revisionDigest}`;
|
|
113
|
+
const expectedSource = `project-source://${value.projectKey}/${value.sourceId}`;
|
|
114
|
+
const expectedRevision = `${expectedSource}/v/${revisionDigest}`;
|
|
115
|
+
if (value.verificationRef !== expectedVerification)
|
|
116
|
+
ctx.addIssue({ code: 'custom', path: ['verificationRef'], message: 'verificationRef must match project and artifact hash' });
|
|
117
|
+
if (value.bindingRef !== expectedBinding)
|
|
118
|
+
ctx.addIssue({ code: 'custom', path: ['bindingRef'], message: 'bindingRef must match project, artifact hash and source revision' });
|
|
119
|
+
if (value.sourceRef !== expectedSource)
|
|
120
|
+
ctx.addIssue({ code: 'custom', path: ['sourceRef'], message: 'sourceRef must match project and source id' });
|
|
121
|
+
if (value.sourceRevisionRef !== expectedRevision)
|
|
122
|
+
ctx.addIssue({ code: 'custom', path: ['sourceRevisionRef'], message: 'sourceRevisionRef must match source and revision id' });
|
|
123
|
+
});
|
|
124
|
+
export const remoteFileIngestReceiptSchema = z.object({
|
|
125
|
+
schemaVersion: schemaVersionSchema,
|
|
126
|
+
ingestRef: stableUriSchema,
|
|
127
|
+
verificationRef: stableUriSchema,
|
|
128
|
+
projectKey: projectKeySchema,
|
|
129
|
+
artifactSha256: sha256Schema,
|
|
130
|
+
sourceBindingRef: stableUriSchema.optional(),
|
|
131
|
+
consumerKey: idSchema,
|
|
132
|
+
resultRefs: z.array(stableUriSchema).min(1).max(256),
|
|
133
|
+
ingestedAt: isoDateTimeSchema,
|
|
134
|
+
}).strict().superRefine((value, ctx) => {
|
|
135
|
+
const digest = value.artifactSha256.slice('sha256:'.length);
|
|
136
|
+
const expectedVerification = `remote-file-verification://${value.projectKey}/${digest}`;
|
|
137
|
+
let sourceRevisionDigest = null;
|
|
138
|
+
if (value.sourceBindingRef) {
|
|
139
|
+
const prefix = `remote-file-source-binding://${value.projectKey}/${digest}/`;
|
|
140
|
+
if (!value.sourceBindingRef.startsWith(prefix))
|
|
141
|
+
ctx.addIssue({ code: 'custom', path: ['sourceBindingRef'], message: 'sourceBindingRef must match project and artifact hash' });
|
|
142
|
+
else {
|
|
143
|
+
const candidate = value.sourceBindingRef.slice(prefix.length);
|
|
144
|
+
if (!/^[a-f0-9]{64}$/.test(candidate))
|
|
145
|
+
ctx.addIssue({ code: 'custom', path: ['sourceBindingRef'], message: 'sourceBindingRef must end with source revision digest' });
|
|
146
|
+
else
|
|
147
|
+
sourceRevisionDigest = candidate;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const expectedIngest = sourceRevisionDigest
|
|
151
|
+
? `remote-file-ingest://${value.projectKey}/${digest}/${encodeURIComponent(value.consumerKey)}/${sourceRevisionDigest}`
|
|
152
|
+
: `remote-file-ingest://${value.projectKey}/${digest}/${encodeURIComponent(value.consumerKey)}`;
|
|
153
|
+
if (value.verificationRef !== expectedVerification)
|
|
154
|
+
ctx.addIssue({ code: 'custom', path: ['verificationRef'], message: 'verificationRef must match project and artifact hash' });
|
|
155
|
+
if (value.ingestRef !== expectedIngest)
|
|
156
|
+
ctx.addIssue({ code: 'custom', path: ['ingestRef'], message: 'ingestRef must match project, artifact hash, consumer and optional source binding' });
|
|
157
|
+
if (new Set(value.resultRefs).size !== value.resultRefs.length)
|
|
158
|
+
ctx.addIssue({ code: 'custom', path: ['resultRefs'], message: 'resultRefs must be unique' });
|
|
159
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** 存储/执行范围显式区分租户与历史项目;范围本身不是授权凭据。 */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { projectKeySchema, stableUriSchema } from './common.js';
|
|
5
|
+
import { materialTenantIdSchema } from './material-context.js';
|
|
6
|
+
export const resourceScopeSchema = z.discriminatedUnion('type', [
|
|
7
|
+
z.object({ type: z.literal('TENANT'), tenantId: materialTenantIdSchema }).strict(),
|
|
8
|
+
z.object({ type: z.literal('PROJECT'), tenantId: materialTenantIdSchema, projectKey: projectKeySchema }).strict(),
|
|
9
|
+
]);
|
|
10
|
+
export const materialAuthorityPolicySchema = z.enum(['LEGACY_PROJECT', 'TENANT_NATIVE', 'TENANT_PROJECT_SCOPE']);
|
|
11
|
+
export function tenantStorageKey(tenantId) {
|
|
12
|
+
return createHash('sha256').update(materialTenantIdSchema.parse(tenantId), 'utf8').digest('hex');
|
|
13
|
+
}
|
|
14
|
+
export const tenantResourceIdentitySchema = z.object({
|
|
15
|
+
tenantId: materialTenantIdSchema,
|
|
16
|
+
resourceType: z.string().regex(/^[a-z][a-z0-9._-]{0,63}$/),
|
|
17
|
+
resourceId: z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/),
|
|
18
|
+
version: z.number().int().positive().safe(),
|
|
19
|
+
}).strict();
|
|
20
|
+
export function tenantResourceRefs(input) {
|
|
21
|
+
const value = tenantResourceIdentitySchema.parse(input);
|
|
22
|
+
const identityRef = `tenant-resource://${tenantStorageKey(value.tenantId)}/${value.resourceType}/${value.resourceId}`;
|
|
23
|
+
return { identityRef, ref: `${identityRef}/v/${value.version}` };
|
|
24
|
+
}
|
|
25
|
+
/** 必须精确匹配规范身份;URL 的容错正规化不能改变对象定位。 */
|
|
26
|
+
export function parseTenantResourceRef(ref) {
|
|
27
|
+
if (typeof ref !== 'string' || ref.length > 2048)
|
|
28
|
+
return null;
|
|
29
|
+
const match = /^tenant-resource:\/\/([a-f0-9]{64})\/([a-z][a-z0-9._-]{0,63})\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})\/v\/([1-9][0-9]*)$/.exec(ref);
|
|
30
|
+
if (!match)
|
|
31
|
+
return null;
|
|
32
|
+
const version = Number(match[4]);
|
|
33
|
+
if (!Number.isSafeInteger(version))
|
|
34
|
+
return null;
|
|
35
|
+
return { storageKey: match[1], resourceType: match[2], resourceId: match[3], version };
|
|
36
|
+
}
|
|
37
|
+
export const tenantResourceRefSchema = stableUriSchema.refine(value => parseTenantResourceRef(value) !== null, 'Noncanonical tenant resource ref');
|
|
38
|
+
export const TENANT_CORE_CAPABILITIES = { resource: 'TENANT_RESOURCE_V1', authority: 'TENANT_AUTHORITY_V1', source: 'TENANT_SOURCE_V1', workflow: 'TENANT_WORKFLOW_V1', budget: 'TENANT_BUDGET_V1' };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { accessClassificationSchema, idSchema, isoDateTimeSchema, projectKeySchema, schemaVersionSchema, sha256Schema, stableUriSchema, } from './common.js';
|
|
3
|
+
import { currentnessAssessmentSchema } from './currentness.js';
|
|
4
|
+
import { evidenceReviewAttachmentSchema } from './evidence.js';
|
|
5
|
+
export const reviewPacketEvidenceSchema = z.object({
|
|
6
|
+
evidenceRef: stableUriSchema,
|
|
7
|
+
evidenceId: idSchema,
|
|
8
|
+
sourceId: idSchema,
|
|
9
|
+
sourceIds: z.array(idSchema).max(64).default([]),
|
|
10
|
+
projectRef: stableUriSchema.optional(),
|
|
11
|
+
sourceRevisionRef: stableUriSchema.optional(),
|
|
12
|
+
collectionBatchRef: stableUriSchema.optional(),
|
|
13
|
+
collectionTaskId: idSchema.optional(),
|
|
14
|
+
canonicalUri: stableUriSchema.optional(),
|
|
15
|
+
observedAt: isoDateTimeSchema,
|
|
16
|
+
access: accessClassificationSchema,
|
|
17
|
+
contentHash: sha256Schema,
|
|
18
|
+
mediaType: z.string().trim().min(1).max(256).optional(),
|
|
19
|
+
byteLength: z.number().int().nonnegative().optional(),
|
|
20
|
+
currentness: currentnessAssessmentSchema.optional(),
|
|
21
|
+
attachment: evidenceReviewAttachmentSchema,
|
|
22
|
+
}).strict().superRefine((value, ctx) => {
|
|
23
|
+
if (value.attachment.evidenceRef !== value.evidenceRef)
|
|
24
|
+
ctx.addIssue({ code: 'custom', path: ['attachment', 'evidenceRef'], message: 'attachment evidenceRef must match evidence row' });
|
|
25
|
+
if (value.attachment.contentHash !== value.contentHash)
|
|
26
|
+
ctx.addIssue({ code: 'custom', path: ['attachment', 'contentHash'], message: 'attachment contentHash must match evidence row' });
|
|
27
|
+
if (value.mediaType && value.attachment.mediaType !== value.mediaType.toLowerCase())
|
|
28
|
+
ctx.addIssue({ code: 'custom', path: ['attachment', 'mediaType'], message: 'attachment mediaType must match evidence row' });
|
|
29
|
+
if (value.byteLength !== undefined && value.attachment.byteLength !== value.byteLength)
|
|
30
|
+
ctx.addIssue({ code: 'custom', path: ['attachment', 'byteLength'], message: 'attachment byteLength must match evidence row' });
|
|
31
|
+
});
|
|
32
|
+
export const reviewPacketSchema = z.object({
|
|
33
|
+
schemaVersion: schemaVersionSchema,
|
|
34
|
+
packetType: z.literal('KNOWLEDGE_REVIEW_PACKET'),
|
|
35
|
+
projectKey: projectKeySchema,
|
|
36
|
+
projectRef: stableUriSchema.optional(),
|
|
37
|
+
sourceEvent: z.object({
|
|
38
|
+
eventId: idSchema,
|
|
39
|
+
type: z.enum(['SOURCE_CHANGED', 'BOUNDARY_RISK', 'CURRENTNESS_RISK']),
|
|
40
|
+
createdAt: isoDateTimeSchema,
|
|
41
|
+
runtimeNodeId: idSchema.optional(),
|
|
42
|
+
sourceRef: stableUriSchema.optional(),
|
|
43
|
+
sourceRevisionRef: stableUriSchema.optional(),
|
|
44
|
+
collectionBatchRef: stableUriSchema.optional(),
|
|
45
|
+
contentHash: sha256Schema.optional(),
|
|
46
|
+
}).strict(),
|
|
47
|
+
evidence: z.array(reviewPacketEvidenceSchema).max(256),
|
|
48
|
+
reviewPolicy: z.object({
|
|
49
|
+
semanticSynthesisRequired: z.literal(true),
|
|
50
|
+
rawChunkApprovalForbidden: z.literal(true),
|
|
51
|
+
highRiskAutoApprovalForbidden: z.literal(true),
|
|
52
|
+
decisions: z.tuple([z.literal('PASS'), z.literal('REWORK'), z.literal('REJECT')]),
|
|
53
|
+
}).strict(),
|
|
54
|
+
}).strict().superRefine((value, ctx) => {
|
|
55
|
+
if (value.projectRef)
|
|
56
|
+
for (let i = 0; i < value.evidence.length; i += 1) {
|
|
57
|
+
const row = value.evidence[i];
|
|
58
|
+
if (row.projectRef && row.projectRef !== value.projectRef)
|
|
59
|
+
ctx.addIssue({ code: 'custom', path: ['evidence', i, 'projectRef'], message: 'Evidence projectRef must match packet projectRef' });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { accessClassificationSchema, idSchema, isoDateTimeSchema, projectKeySchema, schemaVersionSchema, sha256Schema, stableUriSchema, } from './common.js';
|
|
3
|
+
import { artifactKindSchema } from './artifact.js';
|
|
4
|
+
import { driveRelativePathSchema } from './projection.js';
|
|
5
|
+
export const reviewPlaneStageSchema = z.enum([
|
|
6
|
+
'PROGRAM',
|
|
7
|
+
'INBOX',
|
|
8
|
+
'REVIEWED',
|
|
9
|
+
'USER_APPROVED',
|
|
10
|
+
'PROMOTION_RECEIPTS',
|
|
11
|
+
'GUIDES',
|
|
12
|
+
'REJECTED_SUPERSEDED',
|
|
13
|
+
]);
|
|
14
|
+
export const reviewPlaneWorkspaceSchema = z.object({
|
|
15
|
+
schemaVersion: schemaVersionSchema,
|
|
16
|
+
projectKey: projectKeySchema,
|
|
17
|
+
provider: z.literal('GOOGLE_DRIVE'),
|
|
18
|
+
rootPath: driveRelativePathSchema,
|
|
19
|
+
folders: z.object({
|
|
20
|
+
PROGRAM: driveRelativePathSchema,
|
|
21
|
+
INBOX: driveRelativePathSchema,
|
|
22
|
+
REVIEWED: driveRelativePathSchema,
|
|
23
|
+
USER_APPROVED: driveRelativePathSchema,
|
|
24
|
+
PROMOTION_RECEIPTS: driveRelativePathSchema,
|
|
25
|
+
GUIDES: driveRelativePathSchema.optional(),
|
|
26
|
+
REJECTED_SUPERSEDED: driveRelativePathSchema,
|
|
27
|
+
}).strict(),
|
|
28
|
+
}).strict().superRefine((value, ctx) => {
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const [key, folder] of Object.entries(value.folders)) {
|
|
31
|
+
if (folder === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
if (seen.has(folder))
|
|
34
|
+
ctx.addIssue({ code: 'custom', path: ['folders', key], message: 'review-plane folders must be unique' });
|
|
35
|
+
seen.add(folder);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
export const reviewPlaneApprovalSchema = z.object({
|
|
39
|
+
schemaVersion: schemaVersionSchema,
|
|
40
|
+
approvalId: idSchema,
|
|
41
|
+
projectKey: projectKeySchema,
|
|
42
|
+
sourceProvider: z.literal('GOOGLE_DRIVE'),
|
|
43
|
+
sourcePath: driveRelativePathSchema,
|
|
44
|
+
sourceFileId: z.string().trim().min(1).max(512).nullable(),
|
|
45
|
+
sourceContentHash: sha256Schema,
|
|
46
|
+
sourceArtifactRef: stableUriSchema.nullable(),
|
|
47
|
+
reviewedAt: isoDateTimeSchema,
|
|
48
|
+
approvedAt: isoDateTimeSchema,
|
|
49
|
+
decision: z.literal('PASS'),
|
|
50
|
+
approvalAuthority: z.literal('EXPLICIT_HUMAN'),
|
|
51
|
+
hostDisposition: z.enum(['APPROVED_RESEARCH', 'APPROVED_KNOWLEDGE', 'APPROVED_INSIGHT']),
|
|
52
|
+
targetArtifactId: idSchema,
|
|
53
|
+
targetVersion: z.number().int().positive(),
|
|
54
|
+
targetTitle: z.string().trim().min(1).max(500),
|
|
55
|
+
mediaType: z.string().trim().min(1).max(256),
|
|
56
|
+
observedAt: isoDateTimeSchema.nullable(),
|
|
57
|
+
validUntil: isoDateTimeSchema.nullable(),
|
|
58
|
+
access: accessClassificationSchema,
|
|
59
|
+
evidenceRefs: z.array(stableUriSchema).max(256),
|
|
60
|
+
approvalEvidenceRefs: z.array(stableUriSchema).max(256),
|
|
61
|
+
supersedesRef: stableUriSchema.nullable(),
|
|
62
|
+
}).strict().superRefine((value, ctx) => {
|
|
63
|
+
if (Date.parse(value.approvedAt) < Date.parse(value.reviewedAt))
|
|
64
|
+
ctx.addIssue({ code: 'custom', path: ['approvedAt'], message: 'approvedAt must not precede reviewedAt' });
|
|
65
|
+
if (value.validUntil && value.observedAt && Date.parse(value.validUntil) < Date.parse(value.observedAt))
|
|
66
|
+
ctx.addIssue({ code: 'custom', path: ['validUntil'], message: 'validUntil must not precede observedAt' });
|
|
67
|
+
if (value.targetVersion === 1 && value.supersedesRef !== null)
|
|
68
|
+
ctx.addIssue({ code: 'custom', path: ['supersedesRef'], message: 'version 1 cannot supersede' });
|
|
69
|
+
if (value.targetVersion > 1 && value.supersedesRef === null)
|
|
70
|
+
ctx.addIssue({ code: 'custom', path: ['supersedesRef'], message: 'later version requires exact predecessor ref' });
|
|
71
|
+
});
|
|
72
|
+
export const reviewPlanePromotionReceiptSchema = z.object({
|
|
73
|
+
schemaVersion: schemaVersionSchema,
|
|
74
|
+
projectKey: projectKeySchema,
|
|
75
|
+
approvalId: idSchema,
|
|
76
|
+
sourcePath: driveRelativePathSchema,
|
|
77
|
+
sourceFileId: z.string().trim().min(1).max(512).nullable(),
|
|
78
|
+
sourceContentHash: sha256Schema,
|
|
79
|
+
promotedAt: isoDateTimeSchema,
|
|
80
|
+
targetKind: artifactKindSchema,
|
|
81
|
+
draftRef: stableUriSchema,
|
|
82
|
+
readyRef: stableUriSchema,
|
|
83
|
+
reviewRef: stableUriSchema,
|
|
84
|
+
approvedRef: stableUriSchema,
|
|
85
|
+
approvedContentHash: sha256Schema,
|
|
86
|
+
exactBytesVerified: z.literal(true),
|
|
87
|
+
humanApprovalVerified: z.literal(true),
|
|
88
|
+
automaticPromotion: z.literal(false),
|
|
89
|
+
}).strict();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { idSchema, isoDateTimeSchema, projectKeySchema, schemaVersionSchema, sha256Schema, stableUriSchema } from './common.js';
|
|
3
|
+
export const RICH_DOCUMENT_MEDIA_TYPE = 'application/vnd.knowledge-v2.rich-document+json';
|
|
4
|
+
export const richDocumentRoleSchema = z.enum(['SOURCE_MIRROR', 'KNOWLEDGE', 'INSIGHT']);
|
|
5
|
+
export const publicationProfileSchema = z.enum(['HUMAN_ARTICLE_V1', 'KNOWLEDGE_CARD_V1']);
|
|
6
|
+
export const publicationReadinessReasonSchema = z.enum(['ROLE_MISMATCH', 'TITLE_REQUIRED', 'SUMMARY_REQUIRED', 'TEXT_TOO_SHORT', 'STRUCTURE_TOO_THIN', 'SOURCE_EVIDENCE_REQUIRED', 'CITATION_COVERAGE_REQUIRED', 'EVIDENCE_MISMATCH', 'MEDIA_UNAVAILABLE']);
|
|
7
|
+
export const richDocumentMediaAvailabilitySchema = z.enum(['MIRRORED', 'REMOTE_ONLY', 'UNAVAILABLE']);
|
|
8
|
+
const ordinalSchema = z.number().int().nonnegative().max(100_000);
|
|
9
|
+
const humanText = (max) => z.string().max(max);
|
|
10
|
+
const httpUrlSchema = z.string().url().max(2048).superRefine((value, ctx) => { const protocol = new URL(value).protocol; if (!['http:', 'https:'].includes(protocol))
|
|
11
|
+
ctx.addIssue({ code: 'custom', message: 'rich document media/source URL must use HTTP(S)' }); });
|
|
12
|
+
const richInlineSchema = z.object({ text: humanText(64_000), href: httpUrlSchema.nullable() }).strict();
|
|
13
|
+
const inlines = z.array(richInlineSchema).max(10_000).optional();
|
|
14
|
+
const textBlockBase = { ordinal: ordinalSchema };
|
|
15
|
+
export const richHeadingBlockSchema = z.object({ ...textBlockBase, kind: z.literal('HEADING'), level: z.number().int().min(1).max(6), text: humanText(16_000), inlines }).strict();
|
|
16
|
+
export const richParagraphBlockSchema = z.object({ ...textBlockBase, kind: z.literal('PARAGRAPH'), text: humanText(64_000), inlines }).strict();
|
|
17
|
+
export const richListItemBlockSchema = z.object({ ...textBlockBase, kind: z.literal('LIST_ITEM'), text: humanText(32_000), ordered: z.boolean(), level: z.number().int().min(0).max(16), index: z.number().int().positive().nullable(), inlines }).strict();
|
|
18
|
+
export const richQuoteBlockSchema = z.object({ ...textBlockBase, kind: z.literal('QUOTE'), text: humanText(64_000), inlines }).strict();
|
|
19
|
+
export const richCodeBlockSchema = z.object({ ...textBlockBase, kind: z.literal('CODE'), text: humanText(128_000), language: z.string().trim().min(1).max(128).nullable() }).strict();
|
|
20
|
+
export const richDividerBlockSchema = z.object({ ...textBlockBase, kind: z.literal('DIVIDER') }).strict();
|
|
21
|
+
export const richTableBlockSchema = z.object({ ...textBlockBase, kind: z.literal('TABLE'), rows: z.array(z.object({ cells: z.array(z.object({ text: humanText(16_000), header: z.boolean() }).strict()).max(128) }).strict()).min(1).max(1000) }).strict();
|
|
22
|
+
const mediaBase = {
|
|
23
|
+
ordinal: ordinalSchema,
|
|
24
|
+
sourceUrl: httpUrlSchema,
|
|
25
|
+
canonicalUrl: httpUrlSchema.nullable(),
|
|
26
|
+
availability: richDocumentMediaAvailabilitySchema,
|
|
27
|
+
mediaRef: stableUriSchema.nullable(),
|
|
28
|
+
evidenceRef: stableUriSchema.nullable(),
|
|
29
|
+
contentHash: sha256Schema.nullable(),
|
|
30
|
+
mediaType: z.string().trim().min(1).max(256).nullable(),
|
|
31
|
+
byteLength: z.number().int().nonnegative().nullable(),
|
|
32
|
+
caption: humanText(16_000).nullable(),
|
|
33
|
+
};
|
|
34
|
+
function mediaConsistency(value, ctx) {
|
|
35
|
+
const mirrored = Boolean(value.mediaRef && value.evidenceRef && value.contentHash && value.mediaType && value.byteLength !== null);
|
|
36
|
+
if (value.availability === 'MIRRORED' && !mirrored)
|
|
37
|
+
ctx.addIssue({ code: 'custom', message: 'MIRRORED media requires EvidenceRef, hash, mediaType and byteLength' });
|
|
38
|
+
if (value.availability !== 'MIRRORED' && (value.mediaRef || value.evidenceRef || value.contentHash || value.mediaType || value.byteLength !== null))
|
|
39
|
+
ctx.addIssue({ code: 'custom', message: 'non-MIRRORED media cannot claim mirrored Evidence' });
|
|
40
|
+
}
|
|
41
|
+
export const richImageBlockSchema = z.object({ ...mediaBase, kind: z.literal('IMAGE'), alt: humanText(4000).nullable(), width: z.number().int().positive().max(100_000).nullable(), height: z.number().int().positive().max(100_000).nullable() }).strict().superRefine(mediaConsistency);
|
|
42
|
+
export const richVideoBlockSchema = z.object({ ...mediaBase, kind: z.literal('VIDEO'), title: humanText(4000).nullable(), posterUrl: httpUrlSchema.nullable(), posterEvidenceRef: stableUriSchema.nullable() }).strict().superRefine(mediaConsistency);
|
|
43
|
+
export const richAudioBlockSchema = z.object({ ...mediaBase, kind: z.literal('AUDIO'), title: humanText(4000).nullable() }).strict().superRefine(mediaConsistency);
|
|
44
|
+
export const richEmbedBlockSchema = z.object({ ...textBlockBase, kind: z.literal('EMBED'), sourceUrl: httpUrlSchema, provider: z.string().trim().min(1).max(128).nullable(), title: humanText(4000).nullable(), caption: humanText(16_000).nullable() }).strict();
|
|
45
|
+
export const richDocumentBlockSchema = z.discriminatedUnion('kind', [
|
|
46
|
+
richHeadingBlockSchema, richParagraphBlockSchema, richListItemBlockSchema, richQuoteBlockSchema, richCodeBlockSchema,
|
|
47
|
+
richImageBlockSchema, richVideoBlockSchema, richAudioBlockSchema, richEmbedBlockSchema, richDividerBlockSchema, richTableBlockSchema,
|
|
48
|
+
]);
|
|
49
|
+
export const richDocumentCitationSchema = z.object({
|
|
50
|
+
blockOrdinal: ordinalSchema,
|
|
51
|
+
evidenceRefs: z.array(stableUriSchema).min(1).max(32),
|
|
52
|
+
}).strict().superRefine((value, ctx) => { if (new Set(value.evidenceRefs).size !== value.evidenceRefs.length)
|
|
53
|
+
ctx.addIssue({ code: 'custom', path: ['evidenceRefs'], message: 'citation EvidenceRefs must be unique' }); });
|
|
54
|
+
export const richCompositionDraftSchema = z.object({
|
|
55
|
+
summary: humanText(4000),
|
|
56
|
+
byline: humanText(1000).nullable().optional(),
|
|
57
|
+
language: z.string().trim().min(1).max(64).nullable().optional(),
|
|
58
|
+
blocks: z.array(richDocumentBlockSchema).min(1).max(10_000),
|
|
59
|
+
citations: z.array(richDocumentCitationSchema).min(1).max(10_000),
|
|
60
|
+
}).strict();
|
|
61
|
+
export function validateRichDocumentContent(value, ctx, names = { primary: 'primarySourceEvidenceRef', citation: 'evidenceRefs' }) {
|
|
62
|
+
if (!value.sourceEvidenceRefs.includes(value.primarySourceEvidenceRef))
|
|
63
|
+
ctx.addIssue({ code: 'custom', path: [names.primary], message: 'primary source must be included in sourceEvidenceRefs' });
|
|
64
|
+
for (let index = 0; index < value.blocks.length; index += 1)
|
|
65
|
+
if (value.blocks[index].ordinal !== index)
|
|
66
|
+
ctx.addIssue({ code: 'custom', path: ['blocks', index, 'ordinal'], message: 'block ordinals must be contiguous and equal array position' });
|
|
67
|
+
for (let index = 0; index < value.blocks.length; index += 1) {
|
|
68
|
+
const block = value.blocks[index];
|
|
69
|
+
if ('inlines' in block && block.inlines && block.inlines.map(part => part.text).join('') !== block.text)
|
|
70
|
+
ctx.addIssue({ code: 'custom', path: ['blocks', index, 'inlines'], message: 'inline text must equal block text' });
|
|
71
|
+
}
|
|
72
|
+
const claimed = new Set(value.mediaEvidenceRefs);
|
|
73
|
+
if (claimed.size !== value.mediaEvidenceRefs.length)
|
|
74
|
+
ctx.addIssue({ code: 'custom', path: ['mediaEvidenceRefs'], message: 'mediaEvidenceRefs must be unique' });
|
|
75
|
+
for (let index = 0; index < value.blocks.length; index += 1) {
|
|
76
|
+
const block = value.blocks[index];
|
|
77
|
+
if (['IMAGE', 'VIDEO', 'AUDIO'].includes(block.kind)) {
|
|
78
|
+
const media = block;
|
|
79
|
+
if (media.evidenceRef && !claimed.has(media.evidenceRef))
|
|
80
|
+
ctx.addIssue({ code: 'custom', path: ['blocks', index, 'evidenceRef'], message: 'mirrored media EvidenceRef must be listed in mediaEvidenceRefs' });
|
|
81
|
+
if (media.kind === 'VIDEO' && media.posterEvidenceRef && !claimed.has(media.posterEvidenceRef))
|
|
82
|
+
ctx.addIssue({ code: 'custom', path: ['blocks', index, 'posterEvidenceRef'], message: 'video poster EvidenceRef must be listed in mediaEvidenceRefs' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const citationOrdinals = new Set(), allowedCitationRefs = new Set([...value.sourceEvidenceRefs, ...value.mediaEvidenceRefs]);
|
|
86
|
+
for (let index = 0; index < value.citations.length; index += 1) {
|
|
87
|
+
const citation = value.citations[index];
|
|
88
|
+
if (citation.blockOrdinal >= value.blocks.length)
|
|
89
|
+
ctx.addIssue({ code: 'custom', path: ['citations', index, 'blockOrdinal'], message: 'citation block ordinal must identify an existing block' });
|
|
90
|
+
if (citationOrdinals.has(citation.blockOrdinal))
|
|
91
|
+
ctx.addIssue({ code: 'custom', path: ['citations', index, 'blockOrdinal'], message: 'only one citation record is allowed per block' });
|
|
92
|
+
citationOrdinals.add(citation.blockOrdinal);
|
|
93
|
+
for (const ref of citation.evidenceRefs)
|
|
94
|
+
if (!allowedCitationRefs.has(ref))
|
|
95
|
+
ctx.addIssue({ code: 'custom', path: ['citations', index, names.citation], message: 'citation EvidenceRef must be a source or media EvidenceRef' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export const richDocumentSchema = z.object({
|
|
99
|
+
schemaVersion: schemaVersionSchema,
|
|
100
|
+
format: z.literal('RICH_DOCUMENT_V1'),
|
|
101
|
+
role: richDocumentRoleSchema,
|
|
102
|
+
publicationProfile: publicationProfileSchema.nullable().default(null),
|
|
103
|
+
projectKey: projectKeySchema,
|
|
104
|
+
projectRef: stableUriSchema,
|
|
105
|
+
sourceEvidenceRefs: z.array(stableUriSchema).min(1).max(256),
|
|
106
|
+
primarySourceEvidenceRef: stableUriSchema,
|
|
107
|
+
sourceContentHash: sha256Schema,
|
|
108
|
+
sourceUri: stableUriSchema,
|
|
109
|
+
sourceMediaType: z.string().trim().min(1).max(256),
|
|
110
|
+
sourceByteLength: z.number().int().nonnegative(),
|
|
111
|
+
title: humanText(1000).nullable(),
|
|
112
|
+
byline: humanText(1000).nullable(),
|
|
113
|
+
publishedAt: isoDateTimeSchema.nullable(),
|
|
114
|
+
language: z.string().trim().min(1).max(64).nullable(),
|
|
115
|
+
topics: z.array(z.string().trim().min(1).max(160)).max(32).optional(),
|
|
116
|
+
summary: humanText(4000).nullable(),
|
|
117
|
+
textContent: humanText(2_000_000),
|
|
118
|
+
blocks: z.array(richDocumentBlockSchema).max(10_000),
|
|
119
|
+
mediaEvidenceRefs: z.array(stableUriSchema).max(512),
|
|
120
|
+
citations: z.array(richDocumentCitationSchema).max(10_000).default([]),
|
|
121
|
+
extractedAt: isoDateTimeSchema,
|
|
122
|
+
extractor: z.object({ id: idSchema, version: z.string().trim().min(1).max(128) }).strict(),
|
|
123
|
+
}).strict().superRefine((value, ctx) => {
|
|
124
|
+
if (value.role === 'SOURCE_MIRROR' && value.publicationProfile !== null)
|
|
125
|
+
ctx.addIssue({ code: 'custom', path: ['publicationProfile'], message: 'source mirror cannot claim a publication profile' });
|
|
126
|
+
if (value.role !== 'SOURCE_MIRROR' && value.publicationProfile === null)
|
|
127
|
+
ctx.addIssue({ code: 'custom', path: ['publicationProfile'], message: 'published Knowledge/Insight requires a publication profile' });
|
|
128
|
+
try {
|
|
129
|
+
const u = new URL(value.projectRef);
|
|
130
|
+
if (u.protocol !== 'knowledge-project:' || u.hostname.toLowerCase() !== value.projectKey.toLowerCase())
|
|
131
|
+
ctx.addIssue({ code: 'custom', path: ['projectRef'], message: 'projectRef project must match rich document project' });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
ctx.addIssue({ code: 'custom', path: ['projectRef'], message: 'invalid projectRef' });
|
|
135
|
+
}
|
|
136
|
+
validateRichDocumentContent(value, ctx);
|
|
137
|
+
});
|
|
138
|
+
export const richSourceDocumentManifestSchema = z.object({
|
|
139
|
+
schemaVersion: schemaVersionSchema,
|
|
140
|
+
projectKey: projectKeySchema,
|
|
141
|
+
documentRef: stableUriSchema,
|
|
142
|
+
sourceEvidenceRef: stableUriSchema,
|
|
143
|
+
sourceContentHash: sha256Schema,
|
|
144
|
+
documentContentHash: sha256Schema,
|
|
145
|
+
documentByteLength: z.number().int().positive(),
|
|
146
|
+
extractedAt: isoDateTimeSchema,
|
|
147
|
+
extractor: z.object({ id: idSchema, version: z.string().trim().min(1).max(128) }).strict(),
|
|
148
|
+
}).strict().superRefine((value, ctx) => { try {
|
|
149
|
+
const u = new URL(value.documentRef);
|
|
150
|
+
if (u.protocol !== 'rich-document:' || u.hostname.toLowerCase() !== value.projectKey.toLowerCase() || !/^\/[a-f0-9]{64}\/[a-f0-9]{64}$/.test(u.pathname))
|
|
151
|
+
ctx.addIssue({ code: 'custom', path: ['documentRef'], message: 'invalid Rich Source Document identity' });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
ctx.addIssue({ code: 'custom', path: ['documentRef'], message: 'invalid Rich Source Document identity' });
|
|
155
|
+
} });
|
|
156
|
+
export const richSourceDocumentPointerSchema = z.object({
|
|
157
|
+
schemaVersion: schemaVersionSchema,
|
|
158
|
+
projectKey: projectKeySchema,
|
|
159
|
+
sourceEvidenceRef: stableUriSchema,
|
|
160
|
+
documentRef: stableUriSchema,
|
|
161
|
+
documentContentHash: sha256Schema,
|
|
162
|
+
manifestRelativePath: z.string().trim().min(1).max(4096).refine(value => !value.startsWith('/') && !value.split('/').includes('..'), 'manifestRelativePath must remain project-relative'),
|
|
163
|
+
updatedAt: isoDateTimeSchema,
|
|
164
|
+
}).strict();
|
|
165
|
+
export const publicationReadinessReportSchema = z.object({
|
|
166
|
+
schemaVersion: schemaVersionSchema,
|
|
167
|
+
profile: publicationProfileSchema,
|
|
168
|
+
ready: z.boolean(),
|
|
169
|
+
reasons: z.array(publicationReadinessReasonSchema).max(16),
|
|
170
|
+
metrics: z.object({ textCharacters: z.number().int().nonnegative(), blockCount: z.number().int().nonnegative(), textBlockCount: z.number().int().nonnegative(), mediaBlockCount: z.number().int().nonnegative(), sourceEvidenceCount: z.number().int().nonnegative(), citationCount: z.number().int().nonnegative(), citedBlockCount: z.number().int().nonnegative() }).strict(),
|
|
171
|
+
}).strict();
|
|
172
|
+
export const richMediaLinkSchema = z.object({
|
|
173
|
+
schemaVersion: schemaVersionSchema,
|
|
174
|
+
projectKey: projectKeySchema,
|
|
175
|
+
mediaRef: stableUriSchema,
|
|
176
|
+
sourceEvidenceRef: stableUriSchema,
|
|
177
|
+
mediaEvidenceRef: stableUriSchema,
|
|
178
|
+
contentHash: sha256Schema,
|
|
179
|
+
mediaType: z.string().trim().min(1).max(256),
|
|
180
|
+
byteLength: z.number().int().nonnegative(),
|
|
181
|
+
sourceUrl: httpUrlSchema,
|
|
182
|
+
canonicalUrl: httpUrlSchema,
|
|
183
|
+
linkedAt: isoDateTimeSchema,
|
|
184
|
+
}).strict();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Release admission contract. Intermediate, unreleased wave snapshots are not rollback targets. */
|
|
2
|
+
export const tenantCoreReadCapabilities = [
|
|
3
|
+
'TENANT_RESOURCE_V1', 'TENANT_SOURCE_V1', 'TENANT_SOURCE_DOCUMENT_V2', 'TENANT_WORKFLOW_V1',
|
|
4
|
+
'TENANT_ROUTES_V1', 'TENANT_CURRENTNESS_V2', 'TENANT_PUBLIC_REFERENCE_V1',
|
|
5
|
+
'TENANT_PROJECT_CONTEXT_V1', 'TENANT_OAUTH_MANAGEMENT_V1', 'TENANT_RUNTIME_STATUS_V1',
|
|
6
|
+
];
|
|
7
|
+
export const tenantCoreWriteCapabilities = ['TENANT_COLLECTION_V2', 'TENANT_ADMISSION_STOP_V1', 'TENANT_ADMISSION_V2'];
|
|
8
|
+
export function runtimeCompatibility(component) {
|
|
9
|
+
return { schemaVersion: 1, component, identityReadMinimum: 11, identityReadMaximum: 21,
|
|
10
|
+
capabilities: component === 'core' ? [...tenantCoreReadCapabilities, ...tenantCoreWriteCapabilities, 'TENANT_NATIVE_CLIENT_AUTH_V1'] : [...tenantCoreReadCapabilities, 'TENANT_NATIVE_CLIENT_AUTH_V1'],
|
|
11
|
+
tenantProviders: component === 'core' ? [16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] : [], requiredCoreProviders: component === 'admin-web' ? [20] : [],
|
|
12
|
+
budgetReadVersions: component === 'core' ? [2] : [], budgetMigrationVersions: component === 'core' ? [1] : [] };
|
|
13
|
+
}
|
|
14
|
+
/** Conservative published compatibility floor: native Identity >=12 requires the complete current Tenant reader. */
|
|
15
|
+
export function assertRuntimeFormatCompatibility(raw, facts, component, mode) {
|
|
16
|
+
const fail = (code) => { throw new Error('RUNTIME_COMPATIBILITY_' + code); };
|
|
17
|
+
if (!['core', 'admin-web'].includes(component) || !['INSTALL', 'START', 'ROLLBACK', 'RECOVERY'].includes(mode))
|
|
18
|
+
fail('CONTEXT_INVALID');
|
|
19
|
+
if (!Number.isSafeInteger(facts.identityVersion) || facts.identityVersion < 11 || facts.identityVersion > 21)
|
|
20
|
+
fail('IDENTITY_UNSUPPORTED');
|
|
21
|
+
if (facts.budgetVersion !== null && ![1, 2].includes(facts.budgetVersion))
|
|
22
|
+
fail('BUDGET_UNSUPPORTED');
|
|
23
|
+
if (raw === undefined) {
|
|
24
|
+
if (facts.identityVersion >= 12 || facts.budgetVersion !== null)
|
|
25
|
+
fail('DECLARATION_REQUIRED');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
29
|
+
fail('DECLARATION_INVALID');
|
|
30
|
+
const value = raw, expected = runtimeCompatibility(component), keys = Object.keys(expected);
|
|
31
|
+
if (Object.keys(value).length !== keys.length || Object.keys(value).some(key => !keys.includes(key)) || value.schemaVersion !== 1 || value.component !== component)
|
|
32
|
+
fail('DECLARATION_INVALID');
|
|
33
|
+
if (!Number.isSafeInteger(value.identityReadMinimum) || !Number.isSafeInteger(value.identityReadMaximum) || value.identityReadMinimum < 11 || value.identityReadMaximum > 21 || value.identityReadMinimum > value.identityReadMaximum)
|
|
34
|
+
fail('DECLARATION_INVALID');
|
|
35
|
+
for (const key of ['capabilities', 'tenantProviders', 'requiredCoreProviders', 'budgetReadVersions', 'budgetMigrationVersions']) {
|
|
36
|
+
const list = value[key], allowed = expected[key];
|
|
37
|
+
if (!Array.isArray(list) || new Set(list).size !== list.length || list.some(item => !allowed.includes(item)))
|
|
38
|
+
fail('DECLARATION_INVALID');
|
|
39
|
+
}
|
|
40
|
+
if (facts.identityVersion < value.identityReadMinimum || facts.identityVersion > value.identityReadMaximum)
|
|
41
|
+
fail('IDENTITY_UNSUPPORTED');
|
|
42
|
+
if (facts.identityVersion >= 12 && expected.capabilities.some(cap => (facts.identityVersion >= 21 || cap !== 'TENANT_NATIVE_CLIENT_AUTH_V1') && !value.capabilities.includes(cap)))
|
|
43
|
+
fail('CAPABILITY_REQUIRED');
|
|
44
|
+
// New Provider21/22 are advertised, but historical Identity >=12 does not imply the new opt-in writer.
|
|
45
|
+
if (facts.identityVersion >= 12 && ['tenantProviders', 'requiredCoreProviders'].some(key => (key === 'tenantProviders' && component === 'core' ? [16, 17, 18, 19, 20] : expected[key]).some(version => !value[key].includes(version))))
|
|
46
|
+
fail('PROVIDER_REQUIRED');
|
|
47
|
+
if (facts.identityVersion >= 21 && component === 'core' && !value.tenantProviders.includes(26))
|
|
48
|
+
fail('PROVIDER_REQUIRED');
|
|
49
|
+
if (component === 'core' && facts.budgetVersion !== null && !value.capabilities.includes('TENANT_ADMISSION_V2'))
|
|
50
|
+
fail('CAPABILITY_REQUIRED');
|
|
51
|
+
if (component === 'core' && facts.budgetVersion !== null && !value.budgetReadVersions.includes(facts.budgetVersion)) {
|
|
52
|
+
if (mode !== 'INSTALL' || !value.budgetMigrationVersions.includes(facts.budgetVersion))
|
|
53
|
+
fail('BUDGET_MIGRATION_REQUIRED');
|
|
54
|
+
}
|
|
55
|
+
}
|