@native-knowledge/cli 0.1.0-rc.1 → 0.1.0-rc.3

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 (94) hide show
  1. package/LICENSE +202 -21
  2. package/NOTICE +29 -0
  3. package/dist/agent-entry-cli.js +90 -0
  4. package/dist/auth-commands.js +102 -0
  5. package/dist/cli.js +106 -4874
  6. package/dist/commands.js +41 -0
  7. package/dist/download.js +45 -0
  8. package/dist/errors.js +7 -0
  9. package/dist/input.js +89 -0
  10. package/dist/internal/contracts/artifact.js +103 -0
  11. package/dist/internal/contracts/collection-control.js +69 -0
  12. package/dist/internal/contracts/collection.js +174 -0
  13. package/dist/internal/contracts/common.js +16 -0
  14. package/dist/internal/contracts/currentness.js +221 -0
  15. package/dist/internal/contracts/event.js +35 -0
  16. package/dist/internal/contracts/evidence.js +171 -0
  17. package/dist/internal/contracts/index.js +55 -0
  18. package/dist/internal/contracts/material-capability.js +8 -0
  19. package/dist/internal/contracts/material-carrier.js +15 -0
  20. package/dist/internal/contracts/material-context.js +9 -0
  21. package/dist/internal/contracts/material-read.js +12 -0
  22. package/dist/internal/contracts/observation.js +39 -0
  23. package/dist/internal/contracts/persistence.js +239 -0
  24. package/dist/internal/contracts/project-intent.js +121 -0
  25. package/dist/internal/contracts/projection.js +194 -0
  26. package/dist/internal/contracts/public-reference.js +87 -0
  27. package/dist/internal/contracts/reference-record.js +55 -0
  28. package/dist/internal/contracts/remote-file.js +159 -0
  29. package/dist/internal/contracts/resource-scope.js +38 -0
  30. package/dist/internal/contracts/resource-transfer.js +11 -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 +137 -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 +7 -17
  94. package/README.md +0 -28
@@ -0,0 +1,118 @@
1
+ /** 租户原生不可变信封保留类型事实;不以项目占位或审核状态判断原文读取。 */
2
+ import { z } from 'zod';
3
+ import { createHash } from 'node:crypto';
4
+ import { accessClassificationSchema, idSchema, isoDateTimeSchema, sha256Schema, stableUriSchema } from './common.js';
5
+ import { artifactManifestSchema } from './artifact.js';
6
+ import { evidenceRefSchema } from './evidence.js';
7
+ import { referenceCorpusSummarySchema } from './reference-record.js';
8
+ import { knowledgeActorContextSchema } from './project-intent.js';
9
+ import { materialResourceTypeSchema } from './material-carrier.js';
10
+ import { materialTenantIdSchema } from './material-context.js';
11
+ import { tenantResourceIdentitySchema, tenantResourceRefs, tenantStorageKey, tenantResourceRefSchema } from './resource-scope.js';
12
+ // 取原有字段验证器,旧 schema/DTO 原样保留;新信封的身份关系在下方集中验证。
13
+ const artifactFacts = z.object({ ...artifactManifestSchema.shape }).omit({ schemaVersion: true, projectKey: true, projectRef: true, ref: true, identityRef: true, contentHash: true, mediaType: true, byteLength: true, access: true, createdAt: true }).extend({ sourceMaterialRefs: z.array(stableUriSchema).max(256).optional() }).strict();
14
+ const evidenceFacts = z.object({ ...evidenceRefSchema.shape }).omit({ schemaVersion: true, projectKey: true, projectRef: true, recoveryProvenance: true, contentHash: true, mediaType: true, byteLength: true, access: true }).extend({ parentEvidenceRef: tenantResourceRefSchema.optional() }).strict();
15
+ const referenceFacts = z.object({ ...referenceCorpusSummarySchema.shape }).omit({ schemaVersion: true, projectKey: true, projectRef: true, recordRef: true, access: true }).strict();
16
+ export const tenantResourceFactsSchema = z.discriminatedUnion('type', [
17
+ z.object({ type: z.literal('ARTIFACT'), metadata: artifactFacts }).strict(),
18
+ z.object({ type: z.literal('EVIDENCE'), metadata: evidenceFacts }).strict(),
19
+ z.object({ type: z.literal('REFERENCE'), metadata: referenceFacts }).strict(),
20
+ z.object({ type: z.literal('CARRIER'), metadata: z.object({ kind: z.string().min(1).max(128), title: z.string().max(512), summary: z.string().max(1024), sourceRefs: z.array(stableUriSchema).min(1).max(128), observedAt: isoDateTimeSchema }).strict() }).strict(),
21
+ ]);
22
+ export const tenantResourceOriginSchema = z.discriminatedUnion('type', [
23
+ z.object({ type: z.literal('TENANT_AUTHOR'), actor: knowledgeActorContextSchema, submittedAt: isoDateTimeSchema }).strict(),
24
+ z.object({ type: z.literal('TENANT_SOURCE'), sourceId: idSchema, sourceRef: stableUriSchema, sourceRevisionRef: stableUriSchema, collector: evidenceRefSchema.shape.collector, runtimeNodeId: idSchema, observedAt: isoDateTimeSchema }).strict(),
25
+ ]);
26
+ export const tenantResourceManifestSchema = z.object({
27
+ schemaVersion: z.literal(2), ownerTenantId: materialTenantIdSchema,
28
+ resourceId: tenantResourceIdentitySchema.shape.resourceId, resourceType: materialResourceTypeSchema,
29
+ version: tenantResourceIdentitySchema.shape.version, ref: stableUriSchema, identityRef: stableUriSchema,
30
+ contentHash: sha256Schema, byteLength: z.number().int().nonnegative().safe(), mediaType: z.string().trim().min(1).max(256),
31
+ access: accessClassificationSchema, createdAt: isoDateTimeSchema,
32
+ origin: tenantResourceOriginSchema, facts: tenantResourceFactsSchema,
33
+ }).strict().superRefine((value, ctx) => {
34
+ const expected = tenantResourceRefs({ tenantId: value.ownerTenantId, resourceType: value.resourceType, resourceId: value.resourceId, version: value.version });
35
+ for (const name of ['ref', 'identityRef'])
36
+ if (value[name] !== expected[name])
37
+ ctx.addIssue({ code: 'custom', path: [name], message: 'Tenant resource identity mismatch' });
38
+ const kind = value.facts.type.toLowerCase();
39
+ if (value.facts.type === 'CARRIER' ? ['artifact', 'evidence', 'reference', 'media'].includes(value.resourceType) : value.resourceType !== kind)
40
+ ctx.addIssue({ code: 'custom', path: ['resourceType'], message: 'Resource type must retain its factual carrier' });
41
+ if (value.origin.type === 'TENANT_AUTHOR') {
42
+ if (value.origin.actor.tenantId !== value.ownerTenantId)
43
+ ctx.addIssue({ code: 'custom', path: ['origin'], message: 'Author must belong to the resource Tenant' });
44
+ }
45
+ else {
46
+ const prefix = `tenant-source://${tenantStorageKey(value.ownerTenantId)}/${encodeURIComponent(value.origin.sourceId)}`;
47
+ if (value.origin.sourceRef !== prefix || !value.origin.sourceRevisionRef.startsWith(prefix + '/v/') || !/^[a-f0-9]{64}$/.test(value.origin.sourceRevisionRef.slice(prefix.length + 3)))
48
+ ctx.addIssue({ code: 'custom', path: ['origin'], message: 'Source must retain exact Tenant/source/revision identity' });
49
+ }
50
+ if (value.facts.type === 'ARTIFACT') {
51
+ const m = value.facts.metadata;
52
+ if (m.artifactId !== value.resourceId || m.version !== value.version)
53
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Artifact identity/version mismatch' });
54
+ if (m.validUntil && m.observedAt && Date.parse(m.validUntil) < Date.parse(m.observedAt))
55
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Invalid Artifact validity interval' });
56
+ if (m.kind === 'SIGNAL_GENERATION' && (m.version !== 1 || m.status !== 'GENERATED') || ['KNOWLEDGE', 'INSIGHT'].includes(m.kind) && m.status === 'GENERATED')
57
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Invalid Artifact lifecycle' });
58
+ const previous = value.version > 1 ? tenantResourceRefs({ tenantId: value.ownerTenantId, resourceType: value.resourceType, resourceId: value.resourceId, version: value.version - 1 }).ref : null;
59
+ if (m.supersedesRef !== previous)
60
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Artifact must retain immediate predecessor' });
61
+ }
62
+ if (value.facts.type === 'EVIDENCE') {
63
+ const m = value.facts.metadata;
64
+ if (m.parentEvidenceRef) {
65
+ const parent = new URL(m.parentEvidenceRef);
66
+ if (parent.hostname !== tenantStorageKey(value.ownerTenantId) || !parent.pathname.startsWith('/evidence/') || m.parentEvidenceRef === value.ref)
67
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Media parent must be another native Tenant Evidence' });
68
+ }
69
+ if (m.evidenceId !== value.resourceId || value.version !== 1 || Boolean(m.sourceSetRef) !== Boolean(m.sourceItemId) || Boolean(m.collectionBatchRef) !== Boolean(m.collectionTaskId))
70
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Evidence identity/bindings mismatch' });
71
+ if (value.origin.type === 'TENANT_SOURCE' && (m.sourceId !== value.origin.sourceId || m.runtimeNodeId !== value.origin.runtimeNodeId || m.observedAt !== value.origin.observedAt || m.sourceRevisionRef !== value.origin.sourceRevisionRef || JSON.stringify(m.collector) !== JSON.stringify(value.origin.collector)))
72
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Evidence must preserve acquisition provenance' });
73
+ }
74
+ if (value.facts.type === 'REFERENCE') {
75
+ const m = value.facts.metadata;
76
+ if (m.version !== value.version || m.effectiveFrom && m.effectiveUntil && Date.parse(m.effectiveUntil) <= Date.parse(m.effectiveFrom))
77
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Reference version/validity mismatch' });
78
+ const previous = value.version > 1 ? tenantResourceRefs({ tenantId: value.ownerTenantId, resourceType: value.resourceType, resourceId: value.resourceId, version: value.version - 1 }).ref : null;
79
+ if (m.supersedesRef !== previous)
80
+ ctx.addIssue({ code: 'custom', path: ['facts'], message: 'Reference must retain immediate predecessor' });
81
+ }
82
+ });
83
+ export const tenantProjectionIntentSchema = z.object({ schemaVersion: z.literal(2), ref: tenantResourceRefSchema, manifestRevision: sha256Schema, createdAt: isoDateTimeSchema }).strict();
84
+ /** 存储、Catalog 与 Identity 共用同一 manifest 身份;undefined 等价于 JSON 省略。 */
85
+ export function tenantResourceManifestRevision(raw) {
86
+ const json = JSON.stringify(tenantResourceManifestSchema.parse(raw));
87
+ if (Buffer.byteLength(json + '\n') > 64 * 1024)
88
+ throw new Error('TENANT_RESOURCE_MANIFEST_TOO_LARGE');
89
+ const canonical = (value) => { if (value === null || typeof value !== 'object')
90
+ return JSON.stringify(value); if (Array.isArray(value))
91
+ return '[' + value.map(canonical).join(',') + ']'; const row = value; return '{' + Object.keys(row).sort().map(key => JSON.stringify(key) + ':' + canonical(row[key])).join(',') + '}'; };
92
+ return 'sha256:' + createHash('sha256').update(canonical(JSON.parse(json))).digest('hex');
93
+ }
94
+ /** Reference 语义摘要与 CAS 字节摘要分开;只正规化集合字段,不改原始 payload 字节。 */
95
+ export function tenantReferenceRecordDigest(tenantId, access, metadata, payload) {
96
+ const parsed = tenantResourceFactsSchema.parse({ type: 'REFERENCE', metadata });
97
+ if (parsed.type !== 'REFERENCE')
98
+ throw new Error('TENANT_REFERENCE_FACTS_REQUIRED');
99
+ const m = parsed.metadata, selectors = m.selectors ? Object.fromEntries(Object.keys(m.selectors).sort().map(key => [key, [...new Set(m.selectors[key])].sort()])) : undefined;
100
+ const basis = { tenantId: materialTenantIdSchema.parse(tenantId), corpusKey: m.corpusKey, recordKey: m.recordKey, recordType: m.recordType, access: accessClassificationSchema.parse(access), authority: m.authority, jurisdiction: m.jurisdiction, ...(selectors ? { selectors } : {}), sourceRefs: [...new Set(m.sourceRefs)].sort(), effectiveFrom: m.effectiveFrom, effectiveUntil: m.effectiveUntil, payload };
101
+ let nodes = 0;
102
+ const canonical = (value, depth = 0) => {
103
+ if (++nodes > 1_000_000 || depth > 64)
104
+ throw new Error('TENANT_REFERENCE_JSON_BUDGET_EXCEEDED');
105
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
106
+ return JSON.stringify(value);
107
+ if (typeof value === 'number' && Number.isFinite(value))
108
+ return JSON.stringify(value);
109
+ if (Array.isArray(value))
110
+ return '[' + value.map(item => canonical(item, depth + 1)).join(',') + ']';
111
+ if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
112
+ const row = value;
113
+ return '{' + Object.keys(row).sort().map(key => JSON.stringify(key) + ':' + canonical(row[key], depth + 1)).join(',') + '}';
114
+ }
115
+ throw new Error('TENANT_REFERENCE_JSON_INVALID');
116
+ };
117
+ return 'sha256:' + createHash('sha256').update(canonical(basis)).digest('hex');
118
+ }
@@ -0,0 +1,17 @@
1
+ /** 原生受治理正文引用真实资料类型;Reference/开放载体不被伪装为 Evidence。 */
2
+ import { z } from 'zod';
3
+ import { richDocumentSchema, publicationProfileSchema, validateRichDocumentContent } from './rich-document.js';
4
+ import { materialTenantIdSchema } from './material-context.js';
5
+ import { stableUriSchema } from './common.js';
6
+ export const TENANT_RICH_DOCUMENT_MEDIA_TYPE = 'application/vnd.knowledge-v2.tenant-rich-document+json';
7
+ export const tenantDocumentCitationSchema = z.object({ blockOrdinal: z.number().int().nonnegative(), sourceRefs: z.array(stableUriSchema).min(1).max(32) }).strict().superRefine((value, ctx) => { if (new Set(value.sourceRefs).size !== value.sourceRefs.length)
8
+ ctx.addIssue({ code: 'custom', path: ['sourceRefs'], message: 'Citation refs must be unique' }); });
9
+ export const tenantCompositionDraftSchema = z.object({ ...richDocumentSchema.shape }).pick({ summary: true, byline: true, language: true, blocks: true }).extend({ summary: z.string().trim().min(1).max(4000), byline: richDocumentSchema.shape.byline.optional(), language: richDocumentSchema.shape.language.optional(), blocks: richDocumentSchema.shape.blocks.min(1), citations: z.array(tenantDocumentCitationSchema).min(1).max(10000) }).strict();
10
+ const base = z.object({ ...richDocumentSchema.shape }).omit({ projectKey: true, projectRef: true, format: true, role: true, publicationProfile: true, sourceEvidenceRefs: true, primarySourceEvidenceRef: true, citations: true });
11
+ export const tenantRichDocumentSchema = base.extend({ format: z.literal('TENANT_RICH_DOCUMENT_V1'), tenantId: materialTenantIdSchema, role: z.enum(['KNOWLEDGE', 'INSIGHT']), publicationProfile: publicationProfileSchema, sourceRefs: z.array(stableUriSchema).min(1).max(256), primarySourceRef: stableUriSchema, representationRefs: z.array(stableUriSchema).max(64).default([]), citations: z.array(tenantDocumentCitationSchema).max(10000).default([]) }).strict().superRefine((value, ctx) => {
12
+ // 只把字段适配到共同结构规则;身份、真实载体类型与权限由应用层逐 ref 验证。
13
+ validateRichDocumentContent({ sourceEvidenceRefs: value.sourceRefs, primarySourceEvidenceRef: value.primarySourceRef, mediaEvidenceRefs: value.mediaEvidenceRefs, blocks: value.blocks, citations: value.citations.map(citation => ({ blockOrdinal: citation.blockOrdinal, evidenceRefs: citation.sourceRefs })) }, ctx, { primary: 'primarySourceRef', citation: 'sourceRefs' });
14
+ if (new Set(value.sourceRefs).size !== value.sourceRefs.length)
15
+ ctx.addIssue({ code: 'custom', path: ['sourceRefs'], message: 'Sources must be unique' });
16
+ });
17
+ export const tenantPublicationReadinessSchema = z.object({ schemaVersion: z.literal(2), profile: publicationProfileSchema, ready: z.boolean(), reasons: z.array(z.enum(['ROLE_MISMATCH', 'TITLE_REQUIRED', 'SUMMARY_REQUIRED', 'TEXT_TOO_SHORT', 'STRUCTURE_TOO_THIN', 'SOURCE_REQUIRED', 'CITATION_COVERAGE_REQUIRED', 'SOURCE_MISMATCH', 'MEDIA_UNAVAILABLE'])).max(16), metrics: z.object({ textCharacters: z.number().int().nonnegative(), blockCount: z.number().int().nonnegative(), textBlockCount: z.number().int().nonnegative(), mediaBlockCount: z.number().int().nonnegative(), sourceCount: z.number().int().nonnegative(), mediaEvidenceCount: z.number().int().nonnegative(), citationCount: z.number().int().nonnegative(), citedBlockCount: z.number().int().nonnegative() }).strict() }).strict();
@@ -0,0 +1,33 @@
1
+ /** 项目用途上下文不参与来源获取等价,不是授权 grant 或可执行过滤 DSL。 */
2
+ import { z } from 'zod';
3
+ import { idSchema, stableUriSchema } from './common.js';
4
+ export const tenantSourceBindingContextSchema = z.object({
5
+ schemaVersion: z.literal(1), projectIntentRef: stableUriSchema.nullable().default(null),
6
+ scopeIds: z.array(idSchema).max(256).default([]), sourceFamilies: z.array(idSchema).max(256).default([]), attentionNotes: z.string().max(4000).nullable().default(null),
7
+ }).strict().superRefine((value, ctx) => {
8
+ for (const key of ['scopeIds', 'sourceFamilies'])
9
+ if (new Set(value[key]).size !== value[key].length)
10
+ ctx.addIssue({ code: 'custom', path: [key], message: 'Project demand selectors must be unique' });
11
+ if ((value.scopeIds.length || value.sourceFamilies.length) && !value.projectIntentRef)
12
+ ctx.addIssue({ code: 'custom', path: ['projectIntentRef'], message: 'Scoped demand must retain an exact Project Intent revision' });
13
+ if (value.projectIntentRef && !/^project-intent:\/\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\/[a-f0-9]{64}$/.test(value.projectIntentRef))
14
+ ctx.addIssue({ code: 'custom', path: ['projectIntentRef'], message: 'Exact Project Intent revision required' });
15
+ });
16
+ import { projectKeySchema, isoDateTimeSchema } from './common.js';
17
+ import { knowledgeActorContextSchema, collectionRefreshSchema } from './project-intent.js';
18
+ import { materialTenantIdSchema } from './material-context.js';
19
+ import { tenantSourceIdSchema } from './tenant-source.js';
20
+ import { tenantStorageKey } from './resource-scope.js';
21
+ export const tenantSourceBindingRevisionSchema = z.object({
22
+ schemaVersion: z.literal(1), tenantId: materialTenantIdSchema, projectKey: projectKeySchema, sourceId: tenantSourceIdSchema, revisionRef: stableUriSchema,
23
+ generation: z.number().int().positive().safe(), purpose: z.string().max(4000), refresh: collectionRefreshSchema, enabled: z.boolean(), demandContext: tenantSourceBindingContextSchema,
24
+ origin: z.enum(['MIGRATED_V13', 'EXPLICIT_V14']), actor: knowledgeActorContextSchema.nullable(), changedAt: isoDateTimeSchema.nullable(),
25
+ }).strict().superRefine((v, ctx) => {
26
+ const prefix = `tenant-source://${tenantStorageKey(v.tenantId)}/${v.sourceId}/v/`;
27
+ if (!v.revisionRef.startsWith(prefix) || !/^[a-f0-9]{64}$/.test(v.revisionRef.slice(prefix.length)))
28
+ ctx.addIssue({ code: 'custom', path: ['revisionRef'], message: 'Binding must retain exact Tenant Source identity' });
29
+ if (v.demandContext.projectIntentRef && !v.demandContext.projectIntentRef.startsWith(`project-intent://${v.projectKey}/`))
30
+ ctx.addIssue({ code: 'custom', path: ['demandContext'], message: 'Demand context Project mismatch' });
31
+ if (v.origin === 'MIGRATED_V13' ? (v.actor !== null || v.changedAt !== null) : (!v.actor || v.actor.tenantId !== v.tenantId || !v.changedAt))
32
+ ctx.addIssue({ code: 'custom', path: ['origin'], message: 'Migration cannot invent actor/time; explicit changes require attribution' });
33
+ });
@@ -0,0 +1,9 @@
1
+ /** 显式桥接预检确认获取语义;该请求不授予启停或修改工作归属的权限。 */
2
+ import { z } from 'zod';
3
+ import { projectKeySchema, stableUriSchema } from './common.js';
4
+ import { tenantSourceIdSchema, tenantSourceSemanticsSchema } from './tenant-source.js';
5
+ export const tenantSourceBridgePreviewInputSchema = z.object({
6
+ projectKey: projectKeySchema, legacySourceId: tenantSourceIdSchema, expectedLegacyRevisionRef: stableUriSchema,
7
+ nativeSourceId: tenantSourceIdSchema, expectedNativeRevisionRef: stableUriSchema,
8
+ confirmAcquisitionSemantics: tenantSourceSemanticsSchema, maxResourceBytes: z.number().int().min(1024).max(64 * 1024 * 1024),
9
+ }).strict();
@@ -0,0 +1,26 @@
1
+ /** Host 维护切换的冻结事实;不从回执推断真实进程已退出。 */
2
+ import { z } from 'zod';
3
+ import { createHash } from 'node:crypto';
4
+ import { idSchema, isoDateTimeSchema, stableUriSchema } from './common.js';
5
+ import { projectSourceSnapshotSchema, knowledgeActorContextSchema } from './project-intent.js';
6
+ import { tenantCollectionPlanSchema } from './tenant-collection.js';
7
+ import { tenantSourceBindingRevisionSchema, tenantSourceBindingContextSchema } from './tenant-source-binding.js';
8
+ import { tenantStorageKey } from './resource-scope.js';
9
+ import { materialTenantIdSchema } from './material-context.js';
10
+ export const SOURCE_CUTOVER_FENCE_CAPABILITY = 'SOURCE_CUTOVER_FENCE_V1';
11
+ export const sourceCutoverIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/);
12
+ export const sourceCutoverKey = (id) => createHash('sha256').update(sourceCutoverIdSchema.parse(id)).digest('hex');
13
+ export const sourceCutoverRef = (tenantId, id) => `tenant-source-cutover://${tenantStorageKey(tenantId)}/${sourceCutoverKey(id)}`;
14
+ export const sourceCutoverIntentSchema = z.object({
15
+ schemaVersion: z.literal(1), cutoverId: sourceCutoverIdSchema, cutoverRef: stableUriSchema, tenantId: materialTenantIdSchema, actor: knowledgeActorContextSchema, createdAt: isoDateTimeSchema,
16
+ legacySource: projectSourceSnapshotSchema, plan: tenantCollectionPlanSchema, existingBinding: tenantSourceBindingRevisionSchema.nullable(),
17
+ demandContext: tenantSourceBindingContextSchema,
18
+ }).strict().superRefine((v, ctx) => {
19
+ if (v.cutoverRef !== sourceCutoverRef(v.tenantId, v.cutoverId) || v.actor.tenantId !== v.tenantId || v.legacySource.actor.tenantId !== v.tenantId || v.plan.tenantId !== v.tenantId || v.legacySource.targetId !== v.plan.source.targetId || v.legacySource.access !== v.plan.source.semantics.access || v.demandContext.projectIntentRef !== v.legacySource.projectIntentRef)
20
+ ctx.addIssue({ code: 'custom', message: 'Cutover must retain exact owner, source and Project demand' });
21
+ if (v.existingBinding && (v.existingBinding.tenantId !== v.tenantId || v.existingBinding.projectKey !== v.legacySource.projectKey || v.existingBinding.sourceId !== v.plan.source.sourceId))
22
+ ctx.addIssue({ code: 'custom', path: ['existingBinding'], message: 'Prior binding belongs to another context' });
23
+ });
24
+ export const sourceCutoverStepNameSchema = z.enum(['LEGACY_STOPPED', 'DRAINED', 'ACTIVATED', 'NATIVE_STOPPED', 'NATIVE_DRAINED', 'RESTORE_AUTHORIZED', 'LEGACY_RESTORED', 'ROLLED_BACK']);
25
+ export const sourceCutoverStepSchema = z.object({ schemaVersion: z.literal(1), cutoverRef: stableUriSchema, step: sourceCutoverStepNameSchema, actor: knowledgeActorContextSchema, at: isoDateTimeSchema, details: z.record(z.string().max(64), z.union([z.string().max(4096), z.number().finite(), z.boolean(), z.null()])).refine(v => Object.keys(v).length <= 32) }).strict();
26
+ export const sourceCutoverRuntimeProofSchema = z.object({ nodeId: idSchema, codeRevision: z.string().regex(/^[A-Za-z0-9._-]{7,64}$/), capability: z.literal(SOURCE_CUTOVER_FENCE_CAPABILITY), unmanagedExecutions: z.literal('QUIESCED'), observedAt: isoDateTimeSchema }).strict();
@@ -0,0 +1,38 @@
1
+ /** 原生来源投影保留抽取结构,不借用 Project 身份或知识审核状态。 */
2
+ import { z } from 'zod';
3
+ import { createHash } from 'node:crypto';
4
+ import { richDocumentSchema, validateRichDocumentContent } from './rich-document.js';
5
+ import { materialTenantIdSchema } from './material-context.js';
6
+ import { tenantResourceRefs, parseTenantResourceRef, tenantStorageKey } from './resource-scope.js';
7
+ import { sha256Schema, stableUriSchema } from './common.js';
8
+ export const TENANT_SOURCE_DOCUMENT_MEDIA_TYPE = 'application/vnd.knowledge-v2.tenant-source-document+json';
9
+ const base = z.object({ ...richDocumentSchema.shape }).omit({ projectKey: true, projectRef: true, format: true, role: true, publicationProfile: true });
10
+ export const tenantSourceDocumentSchema = base.extend({ format: z.literal('TENANT_RICH_SOURCE_DOCUMENT_V1'), role: z.literal('SOURCE_MIRROR'), tenantId: materialTenantIdSchema }).strict().superRefine((v, ctx) => {
11
+ validateRichDocumentContent(v, ctx);
12
+ const issue = (message) => ctx.addIssue({ code: 'custom', message });
13
+ if (v.sourceEvidenceRefs.length !== 1 || v.sourceEvidenceRefs[0] !== v.primarySourceEvidenceRef)
14
+ issue('Source mirror requires one exact parent');
15
+ const key = tenantStorageKey(v.tenantId), refs = [...v.sourceEvidenceRefs, ...v.mediaEvidenceRefs];
16
+ if (new Set(refs).size !== refs.length || refs.some(ref => { const p = parseTenantResourceRef(ref); return !p || p.storageKey !== key || p.resourceType !== 'evidence'; }))
17
+ issue('Source/media must retain exact Tenant Evidence identity');
18
+ const media = new Set(v.mediaEvidenceRefs);
19
+ for (const b of v.blocks) {
20
+ if ('availability' in b && (b.evidenceRef && (!media.has(b.evidenceRef) || b.mediaRef !== b.evidenceRef) || b.kind === 'VIDEO' && b.posterEvidenceRef && !media.has(b.posterEvidenceRef)))
21
+ issue('Media must identify a declared Evidence');
22
+ }
23
+ });
24
+ export function tenantSourceDocumentIdentity(tenantId, parentRef, documentContentHash) {
25
+ const parent = parseTenantResourceRef(parentRef);
26
+ if (!parent || parent.storageKey !== tenantStorageKey(tenantId) || parent.resourceType !== 'evidence')
27
+ throw new Error('TENANT_DERIVATION_PARENT_INVALID');
28
+ const resourceId = 'doc_' + createHash('sha256').update(JSON.stringify([parentRef, sha256Schema.parse(documentContentHash)])).digest('hex');
29
+ return { resourceId, ...tenantResourceRefs({ tenantId, resourceType: 'source_document', resourceId, version: 1 }) };
30
+ }
31
+ export function tenantSourceMediaIdentity(tenantId, parentRef, kind, url) {
32
+ stableUriSchema.parse(url);
33
+ const parent = parseTenantResourceRef(parentRef);
34
+ if (!parent || parent.storageKey !== tenantStorageKey(tenantId) || parent.resourceType !== 'evidence' || !['IMAGE', 'VIDEO', 'AUDIO'].includes(kind))
35
+ throw new Error('TENANT_DERIVATION_PARENT_INVALID');
36
+ const resourceId = 'media_' + createHash('sha256').update(JSON.stringify([parentRef, kind, url])).digest('hex');
37
+ return { resourceId, ...tenantResourceRefs({ tenantId, resourceType: 'evidence', resourceId, version: 1 }) };
38
+ }
@@ -0,0 +1,5 @@
1
+ /** 预算和计划修订不能为同一 Tenant Source 创建并行获取 owner;复用 RuntimeStore 租约。 */
2
+ import { createHash } from 'node:crypto';
3
+ import { materialTenantIdSchema } from './material-context.js';
4
+ import { tenantSourceIdSchema } from './tenant-source.js';
5
+ export function tenantSourceLeaseKey(tenantId, sourceId) { return 'tenant-source:' + createHash('sha256').update(JSON.stringify(['TENANT_SOURCE_LEASE_V1', materialTenantIdSchema.parse(tenantId), tenantSourceIdSchema.parse(sourceId)])).digest('hex'); }
@@ -0,0 +1,83 @@
1
+ /** 来源获取语义不包含用途、Project、调度启用或秘密;同 URL 不代表同一次获取权限。 */
2
+ import { z } from 'zod';
3
+ import { createHash } from 'node:crypto';
4
+ import { acquisitionTargetSchema } from './project-intent.js';
5
+ import { accessClassificationSchema, sha256Schema, stableUriSchema } from './common.js';
6
+ import { materialTenantIdSchema } from './material-context.js';
7
+ import { tenantStorageKey } from './resource-scope.js';
8
+ export const tenantSourceIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/);
9
+ export const tenantSourceCredentialSchema = z.discriminatedUnion('type', [
10
+ z.object({ type: z.literal('PUBLIC') }).strict(),
11
+ z.object({ type: z.literal('BOUND'), bindingRef: stableUriSchema, revision: sha256Schema }).strict(),
12
+ ]);
13
+ /** Shared Source/configuration rule: secrets never become acquisition locator parameters. */
14
+ export const isTenantSourceCredentialParameter = (key) => /^(?:password|passwd|secret|cookie|authorization|auth|token|access[_-]?token|refresh[_-]?token|api[_-]?key|signature|x-amz-signature|x-goog-signature)$/i.test(key);
15
+ export const tenantSourceSemanticsSchema = z.object({
16
+ target: acquisitionTargetSchema,
17
+ credential: tenantSourceCredentialSchema,
18
+ executionBinding: z.object({ bindingRef: stableUriSchema, revision: sha256Schema }).strict().nullable().default(null),
19
+ access: accessClassificationSchema,
20
+ sourcePolicy: z.object({ ref: stableUriSchema, revision: sha256Schema, acquisition: z.enum(['ALLOW', 'DENY']), propagation: z.enum(['TENANT_ONLY', 'SHARE_ALLOWED', 'PUBLIC_ALLOWED']) }).strict(),
21
+ region: z.string().trim().min(1).max(128).nullable(),
22
+ representation: z.object({ format: z.enum(['RAW', 'RICH_DOCUMENT_V1']), extractor: z.object({ id: z.string().trim().min(1).max(128), version: z.string().trim().min(1).max(128) }).strict().nullable() }).strict(),
23
+ }).strict().superRefine((value, ctx) => {
24
+ if ((value.representation.format === 'RAW') !== (value.representation.extractor === null))
25
+ ctx.addIssue({ code: 'custom', path: ['representation'], message: 'Derived representation requires its exact registered extractor' });
26
+ if ((value.target.kind !== 'URL' || value.credential.type === 'BOUND') && value.executionBinding === null)
27
+ ctx.addIssue({ code: 'custom', path: ['executionBinding'], message: 'FILE, EXECUTOR and authenticated acquisition require an exact runtime binding revision' });
28
+ if (value.target.kind === 'URL') {
29
+ const url = new URL(value.target.locator);
30
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || value.target.locator.length > 2048)
31
+ ctx.addIssue({ code: 'custom', path: ['target'], message: 'URL target must use bounded HTTP without embedded credentials' });
32
+ if ([...url.searchParams.keys()].some(isTenantSourceCredentialParameter))
33
+ ctx.addIssue({ code: 'custom', path: ['target'], message: 'Credential query parameters belong in a runtime binding' });
34
+ }
35
+ if (value.target.kind === 'EXECUTOR' && Object.keys(value.target.parameters).some(key => /(?:password|secret|cookie|authorization|access[_-]?token|refresh[_-]?token)/i.test(key)))
36
+ ctx.addIssue({ code: 'custom', path: ['target', 'parameters'], message: 'Secrets belong in a runtime credential binding' });
37
+ });
38
+ export const tenantSourceBasisSchema = z.object({
39
+ schemaVersion: z.literal(1), tenantId: materialTenantIdSchema, sourceId: tenantSourceIdSchema,
40
+ title: z.string().trim().min(1).max(512), semantics: tenantSourceSemanticsSchema,
41
+ }).strict().superRefine((value, ctx) => {
42
+ if (value.semantics.credential.type === 'BOUND') {
43
+ const prefix = `tenant-credential://${tenantStorageKey(value.tenantId)}/`, ref = value.semantics.credential.bindingRef;
44
+ if (!ref.startsWith(prefix) || !tenantSourceIdSchema.safeParse(ref.slice(prefix.length)).success)
45
+ ctx.addIssue({ code: 'custom', path: ['semantics', 'credential', 'bindingRef'], message: 'Credential reference must belong to the exact Tenant' });
46
+ }
47
+ if (value.semantics.executionBinding !== null) {
48
+ const prefix = `tenant-execution://${tenantStorageKey(value.tenantId)}/`, ref = value.semantics.executionBinding.bindingRef;
49
+ if (!ref.startsWith(prefix) || !tenantSourceIdSchema.safeParse(ref.slice(prefix.length)).success)
50
+ ctx.addIssue({ code: 'custom', path: ['semantics', 'executionBinding', 'bindingRef'], message: 'Execution binding must belong to the exact Tenant' });
51
+ }
52
+ });
53
+ export function tenantSourceRef(tenantId, sourceId) { return `tenant-source://${tenantStorageKey(tenantId)}/${tenantSourceIdSchema.parse(sourceId)}`; }
54
+ export function parseTenantSourceRevisionRef(ref) {
55
+ const match = /^tenant-source:\/\/([a-f0-9]{64})\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})\/v\/([a-f0-9]{64})$/.exec(ref);
56
+ return match ? { storageKey: match[1], sourceId: match[2], revision: `sha256:${match[3]}` } : null;
57
+ }
58
+ const canonical = (value) => { if (value === null || typeof value !== 'object')
59
+ return JSON.stringify(value); if (Array.isArray(value))
60
+ return '[' + value.map(canonical).join(',') + ']'; const row = value; return '{' + Object.keys(row).sort().map(key => JSON.stringify(key) + ':' + canonical(row[key])).join(',') + '}'; };
61
+ const hash = (value) => 'sha256:' + createHash('sha256').update(canonical(value)).digest('hex');
62
+ /** 已正规化 basis 的唯一指纹算法,canonical reader 与 Identity 关系校验共用。 */
63
+ export function tenantSourceFingerprints(basis) {
64
+ const sourceRef = tenantSourceRef(basis.tenantId, basis.sourceId), revision = hash(basis);
65
+ return { sourceRef, revision, revisionRef: `${sourceRef}/v/${revision.slice(7)}`, semanticKey: hash({ tenantId: basis.tenantId, semantics: basis.semantics }), targetId: hash(basis.semantics.target) };
66
+ }
67
+ export const tenantSourceSnapshotSchema = tenantSourceBasisSchema.safeExtend({
68
+ sourceRef: stableUriSchema, revision: sha256Schema, revisionRef: stableUriSchema, semanticKey: sha256Schema, targetId: sha256Schema,
69
+ }).strict().superRefine((value, ctx) => {
70
+ const ref = tenantSourceRef(value.tenantId, value.sourceId);
71
+ if (value.sourceRef !== ref || value.revisionRef !== `${ref}/v/${value.revision.slice(7)}`)
72
+ ctx.addIssue({ code: 'custom', path: ['revisionRef'], message: 'Source revision must retain exact Tenant and Source identity' });
73
+ if (value.semantics.target.kind === 'URL') {
74
+ const url = new URL(value.semantics.target.locator);
75
+ url.hash = '';
76
+ if (url.toString() !== value.semantics.target.locator)
77
+ ctx.addIssue({ code: 'custom', path: ['semantics', 'target'], message: 'Snapshot URL must be canonical' });
78
+ }
79
+ const { sourceRef, revision, revisionRef, semanticKey, targetId, ...basis } = value, expected = tenantSourceFingerprints(basis);
80
+ for (const key of ['sourceRef', 'revision', 'revisionRef', 'semanticKey', 'targetId'])
81
+ if (value[key] !== expected[key])
82
+ ctx.addIssue({ code: 'custom', path: [key], message: 'Source fingerprint mismatch' });
83
+ });
@@ -0,0 +1,118 @@
1
+ /** Tenant 网站采集意图;范围和内容要求不携带 Project 或执行权限。 */
2
+ import { createHash } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import { isoDateTimeSchema, sha256Schema } from './common.js';
5
+ import { materialTenantIdSchema } from './material-context.js';
6
+ import { tenantStorageKey } from './resource-scope.js';
7
+ import { knowledgeActorContextSchema } from './project-intent.js';
8
+ import { isTenantSourceCredentialParameter } from './tenant-source.js';
9
+ import { tenantCollectionCycleSchema, tenantCollectionMaxBytesSchema } from './tenant-collection.js';
10
+ import { tenantCollectionRequestAuthoritySchema } from './tenant-collection-request.js';
11
+ export const TENANT_WEB_SPEC_CAPABILITY = 'TENANT_WEB_COLLECTION_SPEC_V1';
12
+ export const tenantWebUrlSchema = z.string().url().max(2048).superRefine((raw, ctx) => {
13
+ let url;
14
+ try {
15
+ url = new URL(raw);
16
+ }
17
+ catch {
18
+ ctx.addIssue({ code: 'custom', message: 'A valid HTTP URL is required' });
19
+ return;
20
+ }
21
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash || url.port || [...url.searchParams.keys()].some(isTenantSourceCredentialParameter))
22
+ ctx.addIssue({ code: 'custom', message: 'Public HTTP URL without credentials, nonstandard port or fragment required' });
23
+ });
24
+ const originSchema = tenantWebUrlSchema.superRefine((value, ctx) => { try {
25
+ if (new URL(value).origin === value)
26
+ return;
27
+ }
28
+ catch { /* Report a schema issue, never an arbitrary URL parser error. */ } ctx.addIssue({ code: 'custom', message: 'An exact HTTP origin is required' }); });
29
+ const pathSchema = z.string().min(1).max(512).superRefine((value, ctx) => {
30
+ let decoded = '';
31
+ try {
32
+ decoded = decodeURIComponent(value);
33
+ }
34
+ catch { /* Invalid encoding fails closed below. */ }
35
+ if (!decoded.startsWith('/') || decoded.startsWith('//') || /[?#\\\x00-\x1f]/.test(decoded) || decoded.split('/').some(part => part === '.' || part === '..') || decoded.replace(/\/\*\*$/, '').includes('*'))
36
+ ctx.addIssue({ code: 'custom', message: 'Absolute path or terminal /** prefix required' });
37
+ });
38
+ export const tenantWebScopeSchema = z.object({
39
+ mode: z.enum(['SINGLE_PAGE', 'URL_LIST', 'SITE_SECTION', 'SITEMAP', 'FEED']),
40
+ allowedOrigins: z.array(originSchema).max(16).default([]),
41
+ includePaths: z.array(pathSchema).max(32).default([]),
42
+ excludePaths: z.array(pathSchema).max(32).default([]),
43
+ urls: z.array(tenantWebUrlSchema).max(64).default([]),
44
+ followPagination: z.boolean().default(false),
45
+ followDetailLinks: z.boolean().default(false),
46
+ }).strict();
47
+ const field = z.object({ name: z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/), type: z.enum(['string', 'number', 'boolean', 'object', 'array']), required: z.boolean().default(false) }).strict();
48
+ export const tenantWebRequirementsSchema = z.object({
49
+ instruction: z.string().trim().min(1).max(4000),
50
+ fields: z.array(field).max(64).default([]),
51
+ outputs: z.array(z.enum(['RAW', 'DOCUMENT', 'RECORDS'])).min(1).max(3).default(['RAW', 'DOCUMENT']),
52
+ attachments: z.array(z.enum(['PDF', 'IMAGE', 'AUDIO', 'VIDEO', 'OTHER'])).max(5).default([]),
53
+ }).strict().superRefine((value, ctx) => { if (new Set(value.fields.map(f => f.name)).size !== value.fields.length)
54
+ ctx.addIssue({ code: 'custom', message: 'Field names must be unique' }); });
55
+ export const tenantWebDefaultLimits = { maxPages: 100, maxDepth: 3, maxResourceBytes: 16 * 1024 * 1024, maxTotalBytes: 64 * 1024 * 1024, maxRuntimeMs: 300000, maxInteractions: 100, maxFiles: 100 };
56
+ export const tenantWebLimitsSchema = z.object({
57
+ maxPages: z.number().int().min(1).max(10000).default(100),
58
+ maxDepth: z.number().int().min(0).max(10).default(3),
59
+ maxResourceBytes: tenantCollectionMaxBytesSchema.default(16 * 1024 * 1024),
60
+ maxTotalBytes: z.number().int().min(1024).max(1024 * 1024 * 1024).default(64 * 1024 * 1024),
61
+ maxRuntimeMs: z.number().int().min(1000).max(900000).default(300000),
62
+ maxInteractions: z.number().int().min(0).max(1000).default(100),
63
+ maxFiles: z.number().int().min(0).max(1000).default(100),
64
+ }).strict();
65
+ export const tenantWebSpecSchema = z.object({
66
+ schemaVersion: z.literal(1), url: tenantWebUrlSchema, scope: tenantWebScopeSchema,
67
+ requirements: tenantWebRequirementsSchema, limits: tenantWebLimitsSchema.default(tenantWebDefaultLimits),
68
+ refresh: z.object({ mode: z.literal('ONCE') }).strict().default({ mode: 'ONCE' }),
69
+ }).strict().superRefine((value, ctx) => {
70
+ try {
71
+ const origins = value.scope.allowedOrigins.length ? value.scope.allowedOrigins : [new URL(value.url).origin];
72
+ if (!origins.includes(new URL(value.url).origin) || value.scope.urls.some(url => !origins.includes(new URL(url).origin)))
73
+ ctx.addIssue({ code: 'custom', path: ['scope'], message: 'Entry and explicit targets must remain in the declared origins' });
74
+ }
75
+ catch {
76
+ ctx.addIssue({ code: 'custom', path: ['scope'], message: 'Valid entry and target URLs are required' });
77
+ }
78
+ if ((value.scope.mode === 'URL_LIST') !== (value.scope.urls.length > 0))
79
+ ctx.addIssue({ code: 'custom', path: ['scope', 'urls'], message: 'Only URL_LIST requires explicit URLs' });
80
+ if (value.scope.mode === 'SITE_SECTION' && value.scope.includePaths.length === 0)
81
+ ctx.addIssue({ code: 'custom', path: ['scope', 'includePaths'], message: 'A site section requires an explicit path boundary' });
82
+ if (value.scope.mode === 'SINGLE_PAGE' && (value.scope.followPagination || value.scope.followDetailLinks))
83
+ ctx.addIssue({ code: 'custom', path: ['scope'], message: 'Single page cannot enable discovery' });
84
+ });
85
+ export function tenantWebCanonical(value) {
86
+ if (value === null || typeof value !== 'object')
87
+ return JSON.stringify(value);
88
+ if (Array.isArray(value))
89
+ return '[' + value.map(tenantWebCanonical).join(',') + ']';
90
+ const row = value;
91
+ return '{' + Object.keys(row).filter(key => row[key] !== undefined).sort().map(key => JSON.stringify(key) + ':' + tenantWebCanonical(row[key])).join(',') + '}';
92
+ }
93
+ const hash = (value) => createHash('sha256').update(tenantWebCanonical(value)).digest('hex');
94
+ export function tenantWebSpecRevision(spec) { return 'sha256:' + hash(tenantWebSpecSchema.parse(spec)); }
95
+ export function tenantWebRequestKey(requestId) { return hash(tenantCollectionCycleSchema.parse(requestId)); }
96
+ export function tenantWebJobRef(tenantId, requestId) { return `tenant-web-collection://${tenantStorageKey(tenantId)}/${tenantWebRequestKey(requestId)}`; }
97
+ export const tenantWebJobRefSchema = z.string().regex(/^tenant-web-collection:\/\/[a-f0-9]{64}\/[a-f0-9]{64}$/);
98
+ export const tenantWebIntentSchema = z.object({
99
+ schemaVersion: z.literal(1), tenantId: materialTenantIdSchema, requestId: tenantCollectionCycleSchema,
100
+ jobRef: tenantWebJobRefSchema, inputRevision: sha256Schema, specRevision: sha256Schema, requestedSpec: tenantWebSpecSchema, spec: tenantWebSpecSchema,
101
+ actor: knowledgeActorContextSchema, authority: tenantCollectionRequestAuthoritySchema, submittedAt: isoDateTimeSchema,
102
+ }).strict().superRefine((value, ctx) => {
103
+ if (value.actor.tenantId !== value.tenantId || value.jobRef !== tenantWebJobRef(value.tenantId, value.requestId) || value.specRevision !== tenantWebSpecRevision(value.spec) || value.inputRevision !== tenantWebSpecRevision(value.requestedSpec))
104
+ ctx.addIssue({ code: 'custom', message: 'Intent must preserve exact Tenant and frozen specification identity' });
105
+ if (value.spec.limits.maxResourceBytes > Math.min(value.requestedSpec.limits.maxResourceBytes, value.requestedSpec.limits.maxTotalBytes) || tenantWebCanonical({ ...value.spec, limits: { ...value.spec.limits, maxResourceBytes: value.requestedSpec.limits.maxResourceBytes } }) !== tenantWebCanonical(value.requestedSpec))
106
+ ctx.addIssue({ code: 'custom', message: 'Intake may only narrow the resource byte limit' });
107
+ });
108
+ /** S1 仅证明意图持久化;不制造待采列表、原件、时效或准入成功。 */
109
+ export const tenantWebSummarySchema = z.object({
110
+ requestId: tenantCollectionCycleSchema, jobRef: tenantWebJobRefSchema, specRevision: sha256Schema, submittedAt: isoDateTimeSchema,
111
+ phase: z.literal('WEB_INTENT_PERSISTED'), state: z.literal('ACCEPTED'), execution: z.literal('UNAVAILABLE'),
112
+ reason: z.literal('S2_NOT_IMPLEMENTED'), completion: z.null(), networkValidated: z.literal(false), budgetReserved: z.literal(false),
113
+ requirementsState: z.literal('DECLARED_NOT_EVALUATED'),
114
+ coverage: z.object({ state: z.literal('NOT_STARTED'), total: z.null(), discovered: z.literal(0), acquired: z.literal(0), extracted: z.literal(0), registered: z.literal(0) }).strict(),
115
+ }).strict().superRefine((value, ctx) => {
116
+ if (new URL(value.jobRef).pathname !== `/${tenantWebRequestKey(value.requestId)}`)
117
+ ctx.addIssue({ code: 'custom', message: 'Summary request identity must match its exact intent reference' });
118
+ });
@@ -0,0 +1,39 @@
1
+ /** Read-only extraction plans; no executable selectors, scripts, prompts or authority. */
2
+ import { createHash } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import { tenantWebCanonical, tenantWebRequirementsSchema } from './tenant-web-collection.js';
5
+ export const TENANT_WEB_CONTENT_CAPABILITY = 'TENANT_WEB_CONTENT_V1';
6
+ export const TENANT_WEB_CONTENT_EXTRACTOR = { id: 'native-knowledge.web-content', version: '1.0.0' };
7
+ // A deliberately bounded CSS subset: compound tag/id/classes, descendant and child only.
8
+ const atom = '(?:[A-Za-z][A-Za-z0-9_-]*|\\*)?(?:[.#][A-Za-z_][A-Za-z0-9_-]*)*';
9
+ export const tenantWebContentSelectorSchema = z.string().trim().min(1).max(256).refine(s => s.split(/\s*>\s*|\s+/).length <= 8 && new RegExp('^' + atom + '(?:\\s*>\\s*' + atom + '|\\s+' + atom + ')*$').test(s) && s.split(/\s*>\s*|\s+/).every(Boolean), 'Unsupported read-only selector').describe('Read-only CSS subset: tag/id/classes, descendant or child (>) only; max 8 segments, no pseudos or scripts');
10
+ const rule = z.object({ name: z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/), selector: tenantWebContentSelectorSchema, value: z.enum(['TEXT', 'ATTRIBUTE', 'JSON']).default('TEXT'), attribute: z.enum(['href', 'src', 'content', 'datetime', 'value', 'title', 'alt']).nullable().default(null) }).strict().refine(v => (v.value === 'ATTRIBUTE') === (v.attribute !== null));
11
+ export const tenantWebContentPlanSchema = z.object({ schemaVersion: z.literal(1), compiler: z.enum(['CALLER_PLAN_V1', 'BOUNDED_LANGUAGE_V1']), extractor: z.object({ id: z.literal('native-knowledge.web-content'), version: z.literal('1.0.0') }).strict(), body: z.boolean(), tables: z.boolean(), links: z.boolean(), recordSelector: tenantWebContentSelectorSchema.nullable(), fields: z.array(z.object({ ...tenantWebRequirementsSchema.shape.fields.unwrap().element.shape, ...rule.shape })).max(64), maxRecords: z.number().int().min(1).max(100).default(50) }).strict().superRefine((v, c) => { if (v.fields.some(f => (f.value === 'ATTRIBUTE') !== (f.attribute !== null)) || new Set(v.fields.map(f => f.name)).size !== v.fields.length)
12
+ c.addIssue({ code: 'custom', message: 'Invalid field rule' }); });
13
+ export const tenantWebContentInputSchema = z.discriminatedUnion('mode', [z.object({ mode: z.literal('NATURAL_LANGUAGE') }).strict(), z.object({ mode: z.literal('COMPILED'), extractor: z.object({ id: z.literal('native-knowledge.web-content'), version: z.literal('1.0.0') }).strict().default(TENANT_WEB_CONTENT_EXTRACTOR), body: z.boolean().default(false), tables: z.boolean().default(false), links: z.boolean().default(false), recordSelector: tenantWebContentSelectorSchema.nullable().default(null), fields: z.array(rule).max(64).default([]), maxRecords: z.number().int().min(1).max(100).default(50) }).strict()]).describe('NATURAL_LANGUAGE supports bounded body/tables/links combinations in Chinese or English. Otherwise provide COMPILED rules; no model calls.');
14
+ export function compileTenantWebContent(requirements, raw) {
15
+ const input = tenantWebContentInputSchema.parse(raw);
16
+ let body = false, tables = false, links = false;
17
+ if (input.mode === 'NATURAL_LANGUAGE') {
18
+ const words = requirements.instruction.toLowerCase().replace(/^(?:请)?(?:采集|提取|获取|collect|extract)\s*/, '').split(/\s*(?:、|,|,|和|及|与|\band\b)\s*/).map(w => w.trim()).filter(Boolean);
19
+ for (const word of words) {
20
+ if (['正文', 'body', 'text', 'body text'].includes(word))
21
+ body = true;
22
+ else if (['表格', 'tables', 'table'].includes(word))
23
+ tables = true;
24
+ else if (['链接', 'links'].includes(word))
25
+ links = true;
26
+ else
27
+ throw new Error('TENANT_WEB_SEMANTIC_COMPILER_UNAVAILABLE');
28
+ }
29
+ if (!words.length || requirements.fields.length)
30
+ throw new Error('TENANT_WEB_SEMANTIC_COMPILER_UNAVAILABLE');
31
+ }
32
+ const rules = input.mode === 'COMPILED' ? input.fields : [];
33
+ if (rules.length !== requirements.fields.length || rules.some(f => !requirements.fields.some(v => v.name === f.name)))
34
+ throw new Error('TENANT_WEB_CONTENT_FIELD_PLAN_REQUIRED');
35
+ if (requirements.outputs.includes('RECORDS') && !requirements.fields.length)
36
+ throw new Error('TENANT_WEB_CONTENT_FIELD_PLAN_REQUIRED');
37
+ return tenantWebContentPlanSchema.parse({ schemaVersion: 1, compiler: input.mode === 'COMPILED' ? 'CALLER_PLAN_V1' : 'BOUNDED_LANGUAGE_V1', extractor: TENANT_WEB_CONTENT_EXTRACTOR, body: input.mode === 'COMPILED' ? input.body : body, tables: input.mode === 'COMPILED' ? input.tables : tables, links: input.mode === 'COMPILED' ? input.links : links, recordSelector: input.mode === 'COMPILED' ? input.recordSelector : null, fields: requirements.fields.map(f => ({ ...f, ...rules.find(r => r.name === f.name) })), maxRecords: input.mode === 'COMPILED' ? input.maxRecords : 50 });
38
+ }
39
+ export const tenantWebContentRevision = (plan) => 'sha256:' + createHash('sha256').update(tenantWebCanonical(tenantWebContentPlanSchema.parse(plan))).digest('hex');
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ import { isoDateTimeSchema, sha256Schema } from './common.js';
3
+ import { materialTenantIdSchema } from './material-context.js';
4
+ import { tenantResourceRefSchema, parseTenantResourceRef, tenantStorageKey } from './resource-scope.js';
5
+ import { tenantWebJobRefSchema, tenantWebUrlSchema } from './tenant-web-collection.js';
6
+ import { tenantWebFieldResultSchema } from './tenant-web-results.js';
7
+ import { TENANT_WEB_CONTENT_EXTRACTOR } from './tenant-web-content-plan.js';
8
+ export const TENANT_WEB_CONTENT_MEDIA_TYPE = 'application/vnd.knowledge-v2.tenant-web-content+json';
9
+ export const tenantWebContentDocumentSchema = z.object({ schemaVersion: z.literal(1), format: z.literal('TENANT_WEB_CONTENT_V1'), tenantId: materialTenantIdSchema, jobRef: tenantWebJobRefSchema, planRevision: sha256Schema, extractor: z.object({ id: z.literal(TENANT_WEB_CONTENT_EXTRACTOR.id), version: z.literal(TENANT_WEB_CONTENT_EXTRACTOR.version) }).strict(), dependencies: z.array(z.object({ id: z.string().min(1).max(128), version: z.string().min(1).max(64) }).strict()).min(1).max(8), propagation: z.literal('TENANT_ONLY'), reviewState: z.literal('UNREVIEWED'), parentEvidenceRef: tenantResourceRefSchema, sourceContentHash: sha256Schema, sourceByteLength: z.number().int().nonnegative(), sourceMediaType: z.string().min(1).max(256), sourceUri: tenantWebUrlSchema, observedAt: isoDateTimeSchema, extractedAt: isoDateTimeSchema, outputs: z.array(tenantWebFieldResultSchema).max(4), records: z.array(z.object({ ordinal: z.number().int().min(0).max(99), fields: z.array(tenantWebFieldResultSchema).max(64) }).strict()).max(100), gaps: z.array(z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/)).max(32) }).strict().superRefine((v, c) => {
10
+ const key = tenantStorageKey(v.tenantId), parent = parseTenantResourceRef(v.parentEvidenceRef);
11
+ if (parent?.storageKey !== key || parent.resourceType !== 'evidence' || new URL(v.jobRef).hostname !== key || [...v.outputs, ...v.records.flatMap(r => r.fields)].some(f => f.citations.some(cite => cite.evidenceRef !== v.parentEvidenceRef || cite.contentHash !== v.sourceContentHash)))
12
+ c.addIssue({ code: 'custom', message: 'Content must cite its exact Tenant original' });
13
+ if (v.records.some((r, i) => r.ordinal !== i))
14
+ c.addIssue({ code: 'custom', message: 'Record order must be exact' });
15
+ });
@@ -0,0 +1,16 @@
1
+ /** S4 composes frozen S3 acquisition with a separately versioned derived plan. */
2
+ import { z } from 'zod';
3
+ import { sha256Schema, isoDateTimeSchema } from './common.js';
4
+ import { tenantWebDynamicJobSchema, tenantWebDynamicHeadSchema, tenantWebDynamicStatusSchema, tenantWebDynamicStatus } from './tenant-web-dynamic.js';
5
+ import { tenantWebJobRefSchema } from './tenant-web-collection.js';
6
+ import { tenantWebContentPlanSchema, tenantWebContentRevision } from './tenant-web-content-plan.js';
7
+ import { tenantResourceRefSchema, parseTenantResourceRef } from './resource-scope.js';
8
+ export const tenantWebContentJobSchema = z.object({ ...tenantWebDynamicJobSchema.shape, schemaVersion: z.literal(4), contentPlan: tenantWebContentPlanSchema, contentRevision: sha256Schema, retainedFrom: tenantWebJobRefSchema.nullable() }).strict().superRefine((v, c) => {
9
+ if (v.contentRevision !== tenantWebContentRevision(v.contentPlan) || v.retainedFrom && (v.retainedFrom === v.intent.jobRef || new URL(v.retainedFrom).hostname !== new URL(v.intent.jobRef).hostname) || JSON.stringify(v.contentPlan.fields.map(({ name, type, required }) => ({ name, type, required }))) !== JSON.stringify(v.intent.spec.requirements.fields))
10
+ c.addIssue({ code: 'custom', message: 'Frozen content plan identity mismatch' });
11
+ });
12
+ const count = z.number().int().min(0).max(22000);
13
+ export const tenantWebContentHeadSchema = z.object({ schemaVersion: z.literal(1), seq: z.number().int().nonnegative(), cursor: count, variant: z.number().int().min(-1).max(100), processed: count, registered: count, missing: count, attempts: z.number().int().min(0).max(3), chargedBytes: z.number().int().nonnegative(), chargedMs: z.number().int().nonnegative(), reservation: z.object({ bytes: z.number().int().nonnegative(), ms: z.number().int().nonnegative() }).strict().nullable(), state: z.enum(['QUEUED', 'RUNNING', 'COMPLETE', 'PARTIAL']), gaps: z.array(z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/)).max(32), finishedAt: isoDateTimeSchema.nullable() }).strict().refine(v => v.registered <= v.processed && v.missing <= v.processed && (['COMPLETE', 'PARTIAL'].includes(v.state) === (v.finishedAt !== null)) && (v.state !== 'COMPLETE' || !v.gaps.length && !v.missing && !v.reservation) && (v.state !== 'PARTIAL' || v.gaps.length > 0));
14
+ export const tenantWebContentOutcomeSchema = z.object({ schemaVersion: z.literal(1), index: count, ordinal: z.number().int().min(0).max(10999), variant: z.number().int().min(-1).max(100), documentRef: tenantResourceRefSchema.nullable(), gaps: tenantWebContentHeadSchema.shape.gaps }).strict().refine(v => !v.documentRef || parseTenantResourceRef(v.documentRef)?.resourceType === 'source_document');
15
+ export const tenantWebContentStatusSchema = z.object({ ...tenantWebDynamicStatusSchema.shape, phase: z.enum(['DYNAMIC_ACQUISITION', 'CONTENT_DERIVATION']), requirementsState: z.enum(['PENDING', 'SATISFIED', 'PARTIAL']), contentRevision: sha256Schema, content: z.object({ processed: count, registered: count, missing: count, gaps: tenantWebContentHeadSchema.shape.gaps, compiler: tenantWebContentPlanSchema.shape.compiler, model: z.literal('DISABLED_NO_MODEL_CALLS') }).strict() }).strict();
16
+ export function tenantWebContentStatus(job, a, h) { const base = tenantWebDynamicStatus(tenantWebDynamicJobSchema.parse({ schemaVersion: 3, intent: job.intent, policy: job.policy }), a), acquiring = !['COMPLETE', 'PARTIAL'].includes(a.state) && !job.retainedFrom; return tenantWebContentStatusSchema.parse({ ...base, contentRevision: job.contentRevision, phase: acquiring ? 'DYNAMIC_ACQUISITION' : 'CONTENT_DERIVATION', state: acquiring ? a.state : h.state, requirementsState: ['COMPLETE', 'PARTIAL'].includes(h.state) ? h.state === 'COMPLETE' && a.state === 'COMPLETE' ? 'SATISFIED' : 'PARTIAL' : 'PENDING', content: { processed: h.processed, registered: h.registered, missing: h.missing, gaps: h.gaps, compiler: job.contentPlan.compiler, model: 'DISABLED_NO_MODEL_CALLS' }, budget: { chargedBytes: (job.retainedFrom ? 0 : a.chargedBytes) + h.chargedBytes, chargedMs: (job.retainedFrom ? 0 : a.chargedMs) + h.chargedMs, conservativeReservation: !job.retainedFrom && a.reservation !== null || h.reservation !== null }, finishedAt: h.finishedAt, nextEligibleAt: acquiring ? a.notBefore : null }); }