@native-knowledge/knowledge-sdk 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.
Files changed (206) hide show
  1. package/LICENSE +202 -21
  2. package/NOTICE +29 -0
  3. package/README.md +26 -38
  4. package/dist/{types/sdk/agent-entry.d.ts → agent-entry.d.ts} +3 -3
  5. package/dist/agent-entry.js +153 -0
  6. package/dist/{types/sdk/agent-guidance-error.d.ts → agent-guidance-error.d.ts} +1 -1
  7. package/dist/agent-guidance-error.js +46 -0
  8. package/dist/auth.d.ts +124 -0
  9. package/dist/auth.js +165 -0
  10. package/dist/{types/sdk/index.d.ts → index.d.ts} +15 -14
  11. package/dist/index.js +159 -6570
  12. package/dist/internal/contracts/artifact.js +103 -0
  13. package/dist/internal/contracts/collection-control.js +69 -0
  14. package/dist/internal/contracts/collection.js +174 -0
  15. package/dist/internal/contracts/common.js +16 -0
  16. package/dist/internal/contracts/currentness.js +221 -0
  17. package/dist/internal/contracts/event.js +35 -0
  18. package/dist/internal/contracts/evidence.js +171 -0
  19. package/dist/{types → internal}/contracts/index.d.ts +0 -1
  20. package/dist/internal/contracts/index.js +54 -0
  21. package/dist/internal/contracts/material-capability.js +8 -0
  22. package/dist/internal/contracts/material-carrier.js +15 -0
  23. package/dist/internal/contracts/material-context.js +9 -0
  24. package/dist/internal/contracts/material-read.js +12 -0
  25. package/dist/internal/contracts/observation.js +39 -0
  26. package/dist/internal/contracts/persistence.js +239 -0
  27. package/dist/internal/contracts/project-intent.js +121 -0
  28. package/dist/internal/contracts/projection.js +194 -0
  29. package/dist/internal/contracts/public-reference.js +87 -0
  30. package/dist/internal/contracts/reference-record.js +55 -0
  31. package/dist/internal/contracts/remote-file.js +159 -0
  32. package/dist/internal/contracts/resource-scope.js +38 -0
  33. package/dist/internal/contracts/review-packet.js +61 -0
  34. package/dist/internal/contracts/review-plane.js +89 -0
  35. package/dist/internal/contracts/rich-document.js +184 -0
  36. package/dist/internal/contracts/runtime-compatibility.js +55 -0
  37. package/dist/internal/contracts/runtime.js +68 -0
  38. package/dist/internal/contracts/scheduler.js +256 -0
  39. package/dist/internal/contracts/signal.js +77 -0
  40. package/dist/internal/contracts/source-governance.js +194 -0
  41. package/dist/internal/contracts/source.js +32 -0
  42. package/dist/internal/contracts/tenant-budget.js +34 -0
  43. package/dist/internal/contracts/tenant-collection-request.js +58 -0
  44. package/dist/internal/contracts/tenant-collection.js +66 -0
  45. package/dist/internal/contracts/tenant-currentness.js +26 -0
  46. package/dist/internal/contracts/tenant-management.js +27 -0
  47. package/dist/internal/contracts/tenant-material-api.js +46 -0
  48. package/dist/internal/contracts/tenant-material-json-schema.js +16 -0
  49. package/dist/internal/contracts/tenant-pdf-text.js +13 -0
  50. package/dist/internal/contracts/tenant-project-control.js +19 -0
  51. package/dist/internal/contracts/tenant-public-reference.js +26 -0
  52. package/dist/internal/contracts/tenant-resource-api.js +70 -0
  53. package/dist/internal/contracts/tenant-resource.js +118 -0
  54. package/dist/internal/contracts/tenant-rich-document.js +17 -0
  55. package/dist/internal/contracts/tenant-source-binding.js +33 -0
  56. package/dist/internal/contracts/tenant-source-bridge.js +9 -0
  57. package/dist/internal/contracts/tenant-source-cutover.js +26 -0
  58. package/dist/internal/contracts/tenant-source-document.js +38 -0
  59. package/dist/internal/contracts/tenant-source-lease.js +5 -0
  60. package/dist/internal/contracts/tenant-source.js +83 -0
  61. package/dist/internal/contracts/tenant-web-collection.js +118 -0
  62. package/dist/internal/contracts/tenant-web-content-plan.js +39 -0
  63. package/dist/internal/contracts/tenant-web-content-result.js +15 -0
  64. package/dist/internal/contracts/tenant-web-content.js +16 -0
  65. package/dist/internal/contracts/tenant-web-crawl.js +55 -0
  66. package/dist/internal/contracts/tenant-web-dynamic.js +68 -0
  67. package/dist/internal/contracts/tenant-web-results.js +41 -0
  68. package/dist/internal/contracts/tenant-web-workflow.js +29 -0
  69. package/dist/internal/serving-contracts/agent-catalog.js +56 -0
  70. package/dist/internal/serving-contracts/agent-entry.js +57 -0
  71. package/dist/internal/serving-contracts/agent-guidance-shapes.js +12 -0
  72. package/dist/internal/serving-contracts/agent-input-hints.js +104 -0
  73. package/dist/internal/serving-contracts/body-search.js +16 -0
  74. package/dist/internal/serving-contracts/consumer.js +45 -0
  75. package/dist/internal/serving-contracts/core.js +76 -0
  76. package/dist/internal/serving-contracts/http.js +64 -0
  77. package/dist/{types → internal}/serving-contracts/index.d.ts +1 -0
  78. package/dist/internal/serving-contracts/index.js +25 -0
  79. package/dist/internal/serving-contracts/management.js +15 -0
  80. package/dist/internal/serving-contracts/material-capabilities.js +14 -0
  81. package/dist/internal/serving-contracts/material-index-status.js +5 -0
  82. package/dist/internal/serving-contracts/mcp.js +140 -0
  83. package/dist/internal/serving-contracts/tenant-governance.js +13 -0
  84. package/dist/internal/serving-contracts/tenant-management.js +89 -0
  85. package/dist/internal/serving-contracts/tenant-project.js +39 -0
  86. package/dist/internal/serving-contracts/tenant-runtime.js +37 -0
  87. package/dist/internal/serving-contracts/tenant-session.d.ts +87 -0
  88. package/dist/internal/serving-contracts/tenant-session.js +8 -0
  89. package/dist/internal/serving-contracts/tenant-web-content.js +20 -0
  90. package/dist/internal/serving-contracts/tenant-web-crawl.js +13 -0
  91. package/dist/internal/serving-contracts/tenant-web-dynamic.js +20 -0
  92. package/dist/internal/serving-contracts/tenant-web-workflow.js +36 -0
  93. package/dist/internal/serving-contracts/tenant-web.js +53 -0
  94. package/dist/internal/serving-contracts/tenant-workspace.js +28 -0
  95. package/dist/internal/serving-contracts/workflow.js +37 -0
  96. package/dist/material-stream.js +29 -0
  97. package/dist/{types/sdk/oauth.d.ts → oauth.d.ts} +18 -18
  98. package/dist/oauth.js +539 -0
  99. package/dist/{types/sdk/resource.d.ts → resource.d.ts} +5 -5
  100. package/dist/resource.js +123 -0
  101. package/dist/session.d.ts +4 -0
  102. package/dist/session.js +3 -0
  103. package/dist/tenant-client.d.ts +169 -0
  104. package/dist/tenant-client.js +58 -0
  105. package/dist/{types/sdk/tenant-management.d.ts → tenant-management.d.ts} +6 -6
  106. package/dist/tenant-management.js +149 -0
  107. package/dist/{types/sdk/tenant-project.d.ts → tenant-project.d.ts} +1 -1
  108. package/dist/tenant-project.js +63 -0
  109. package/dist/{types/sdk/tenant-runtime.d.ts → tenant-runtime.d.ts} +1 -1
  110. package/dist/tenant-runtime.js +17 -0
  111. package/dist/{types/sdk/tenant-web-content.d.ts → tenant-web-content.d.ts} +1 -1
  112. package/dist/tenant-web-content.js +70 -0
  113. package/dist/{types/sdk/tenant-web-crawl.d.ts → tenant-web-crawl.d.ts} +1 -1
  114. package/dist/tenant-web-crawl.js +39 -0
  115. package/dist/{types/sdk/tenant-web-dynamic.d.ts → tenant-web-dynamic.d.ts} +1 -1
  116. package/dist/tenant-web-dynamic.js +39 -0
  117. package/dist/{types/sdk/tenant-web-workflow.d.ts → tenant-web-workflow.d.ts} +1 -1
  118. package/dist/tenant-web-workflow.js +63 -0
  119. package/dist/{types/sdk/tenant-web.d.ts → tenant-web.d.ts} +1 -1
  120. package/dist/tenant-web.js +39 -0
  121. package/dist/{types/sdk/tenant-workspace.d.ts → tenant-workspace.d.ts} +1 -1
  122. package/dist/tenant-workspace.js +21 -0
  123. package/dist/token-provider.d.ts +14 -0
  124. package/dist/token-provider.js +21 -0
  125. package/package.json +27 -15
  126. package/dist/types/contracts/resource-transfer.d.ts +0 -24
  127. package/dist/types/sdk/resource-integrity.d.ts +0 -21
  128. package/dist/types/sdk/transport.d.ts +0 -16
  129. /package/dist/{types → internal}/contracts/artifact.d.ts +0 -0
  130. /package/dist/{types → internal}/contracts/collection-control.d.ts +0 -0
  131. /package/dist/{types → internal}/contracts/collection.d.ts +0 -0
  132. /package/dist/{types → internal}/contracts/common.d.ts +0 -0
  133. /package/dist/{types → internal}/contracts/currentness.d.ts +0 -0
  134. /package/dist/{types → internal}/contracts/event.d.ts +0 -0
  135. /package/dist/{types → internal}/contracts/evidence.d.ts +0 -0
  136. /package/dist/{types → internal}/contracts/material-capability.d.ts +0 -0
  137. /package/dist/{types → internal}/contracts/material-carrier.d.ts +0 -0
  138. /package/dist/{types → internal}/contracts/material-context.d.ts +0 -0
  139. /package/dist/{types → internal}/contracts/material-read.d.ts +0 -0
  140. /package/dist/{types → internal}/contracts/observation.d.ts +0 -0
  141. /package/dist/{types → internal}/contracts/persistence.d.ts +0 -0
  142. /package/dist/{types → internal}/contracts/project-intent.d.ts +0 -0
  143. /package/dist/{types → internal}/contracts/projection.d.ts +0 -0
  144. /package/dist/{types → internal}/contracts/public-reference.d.ts +0 -0
  145. /package/dist/{types → internal}/contracts/reference-record.d.ts +0 -0
  146. /package/dist/{types → internal}/contracts/remote-file.d.ts +0 -0
  147. /package/dist/{types → internal}/contracts/resource-scope.d.ts +0 -0
  148. /package/dist/{types → internal}/contracts/review-packet.d.ts +0 -0
  149. /package/dist/{types → internal}/contracts/review-plane.d.ts +0 -0
  150. /package/dist/{types → internal}/contracts/rich-document.d.ts +0 -0
  151. /package/dist/{types → internal}/contracts/runtime-compatibility.d.ts +0 -0
  152. /package/dist/{types → internal}/contracts/runtime.d.ts +0 -0
  153. /package/dist/{types → internal}/contracts/scheduler.d.ts +0 -0
  154. /package/dist/{types → internal}/contracts/signal.d.ts +0 -0
  155. /package/dist/{types → internal}/contracts/source-governance.d.ts +0 -0
  156. /package/dist/{types → internal}/contracts/source.d.ts +0 -0
  157. /package/dist/{types → internal}/contracts/tenant-budget.d.ts +0 -0
  158. /package/dist/{types → internal}/contracts/tenant-collection-request.d.ts +0 -0
  159. /package/dist/{types → internal}/contracts/tenant-collection.d.ts +0 -0
  160. /package/dist/{types → internal}/contracts/tenant-currentness.d.ts +0 -0
  161. /package/dist/{types → internal}/contracts/tenant-management.d.ts +0 -0
  162. /package/dist/{types → internal}/contracts/tenant-material-api.d.ts +0 -0
  163. /package/dist/{types → internal}/contracts/tenant-pdf-text.d.ts +0 -0
  164. /package/dist/{types → internal}/contracts/tenant-project-control.d.ts +0 -0
  165. /package/dist/{types → internal}/contracts/tenant-public-reference.d.ts +0 -0
  166. /package/dist/{types → internal}/contracts/tenant-resource-api.d.ts +0 -0
  167. /package/dist/{types → internal}/contracts/tenant-resource.d.ts +0 -0
  168. /package/dist/{types → internal}/contracts/tenant-rich-document.d.ts +0 -0
  169. /package/dist/{types → internal}/contracts/tenant-source-binding.d.ts +0 -0
  170. /package/dist/{types → internal}/contracts/tenant-source-bridge.d.ts +0 -0
  171. /package/dist/{types → internal}/contracts/tenant-source-cutover.d.ts +0 -0
  172. /package/dist/{types → internal}/contracts/tenant-source-document.d.ts +0 -0
  173. /package/dist/{types → internal}/contracts/tenant-source-lease.d.ts +0 -0
  174. /package/dist/{types → internal}/contracts/tenant-source.d.ts +0 -0
  175. /package/dist/{types → internal}/contracts/tenant-web-collection.d.ts +0 -0
  176. /package/dist/{types → internal}/contracts/tenant-web-content-plan.d.ts +0 -0
  177. /package/dist/{types → internal}/contracts/tenant-web-content-result.d.ts +0 -0
  178. /package/dist/{types → internal}/contracts/tenant-web-content.d.ts +0 -0
  179. /package/dist/{types → internal}/contracts/tenant-web-crawl.d.ts +0 -0
  180. /package/dist/{types → internal}/contracts/tenant-web-dynamic.d.ts +0 -0
  181. /package/dist/{types → internal}/contracts/tenant-web-results.d.ts +0 -0
  182. /package/dist/{types → internal}/contracts/tenant-web-workflow.d.ts +0 -0
  183. /package/dist/{types → internal}/serving-contracts/agent-catalog.d.ts +0 -0
  184. /package/dist/{types → internal}/serving-contracts/agent-entry.d.ts +0 -0
  185. /package/dist/{types → internal}/serving-contracts/agent-guidance-shapes.d.ts +0 -0
  186. /package/dist/{types → internal}/serving-contracts/agent-input-hints.d.ts +0 -0
  187. /package/dist/{types → internal}/serving-contracts/body-search.d.ts +0 -0
  188. /package/dist/{types → internal}/serving-contracts/consumer.d.ts +0 -0
  189. /package/dist/{types → internal}/serving-contracts/core.d.ts +0 -0
  190. /package/dist/{types → internal}/serving-contracts/http.d.ts +0 -0
  191. /package/dist/{types → internal}/serving-contracts/management.d.ts +0 -0
  192. /package/dist/{types → internal}/serving-contracts/material-capabilities.d.ts +0 -0
  193. /package/dist/{types → internal}/serving-contracts/material-index-status.d.ts +0 -0
  194. /package/dist/{types → internal}/serving-contracts/mcp.d.ts +0 -0
  195. /package/dist/{types → internal}/serving-contracts/tenant-governance.d.ts +0 -0
  196. /package/dist/{types → internal}/serving-contracts/tenant-management.d.ts +0 -0
  197. /package/dist/{types → internal}/serving-contracts/tenant-project.d.ts +0 -0
  198. /package/dist/{types → internal}/serving-contracts/tenant-runtime.d.ts +0 -0
  199. /package/dist/{types → internal}/serving-contracts/tenant-web-content.d.ts +0 -0
  200. /package/dist/{types → internal}/serving-contracts/tenant-web-crawl.d.ts +0 -0
  201. /package/dist/{types → internal}/serving-contracts/tenant-web-dynamic.d.ts +0 -0
  202. /package/dist/{types → internal}/serving-contracts/tenant-web-workflow.d.ts +0 -0
  203. /package/dist/{types → internal}/serving-contracts/tenant-web.d.ts +0 -0
  204. /package/dist/{types → internal}/serving-contracts/tenant-workspace.d.ts +0 -0
  205. /package/dist/{types → internal}/serving-contracts/workflow.d.ts +0 -0
  206. /package/dist/{types/sdk/material-stream.d.ts → material-stream.d.ts} +0 -0
@@ -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 }); }
@@ -0,0 +1,55 @@
1
+ /** 可执行静态网站任务;独立于冻结的 S1 意图,Tenant 是唯一归属。 */
2
+ import { z } from 'zod';
3
+ import { createHash } from 'node:crypto';
4
+ import { tenantStorageKey } from './resource-scope.js';
5
+ import { tenantWebIntentSchema, tenantWebUrlSchema, tenantWebJobRefSchema, tenantWebCanonical } from './tenant-web-collection.js';
6
+ import { tenantSourceSemanticsSchema } from './tenant-source.js';
7
+ import { tenantCollectionCaptureSchema, tenantCollectionCycleSchema } from './tenant-collection.js';
8
+ import { isoDateTimeSchema, sha256Schema } from './common.js';
9
+ export const TENANT_WEB_CRAWL_CAPABILITY = 'TENANT_WEB_CRAWL_V1';
10
+ export const tenantWebPolicySchema = tenantSourceSemanticsSchema.shape.sourcePolicy.refine(v => v.acquisition === 'ALLOW' && v.propagation === 'TENANT_ONLY');
11
+ export const tenantWebCrawlSchema = z.object({ schemaVersion: z.literal(2), intent: tenantWebIntentSchema, policy: tenantWebPolicySchema }).strict();
12
+ export function tenantWebAdmissionKey(tenantId, jobRef) { tenantWebJobRefSchema.parse(jobRef); if (new URL(jobRef).hostname !== tenantStorageKey(tenantId))
13
+ throw new Error('TENANT_WEB_SCOPE_FORBIDDEN'); return createHash('sha256').update(JSON.stringify(['TENANT_WEB_ADMISSION_V1', tenantId, jobRef])).digest('hex'); }
14
+ export const tenantWebTargetSchema = z.object({ ordinal: z.number().int().min(0).max(9999), url: tenantWebUrlSchema, parent: tenantWebUrlSchema.nullable(), depth: z.number().int().min(0).max(10), role: z.enum(['PAGE', 'SITEMAP', 'FEED']) }).strict();
15
+ export const tenantWebOutcomeSchema = z.object({ ordinal: tenantWebTargetSchema.shape.ordinal, batchId: z.string().regex(/^batch_[a-f0-9]{64}$/).nullable(), capture: tenantCollectionCaptureSchema.nullable(), registered: z.boolean(), errorCode: z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/).nullable(), reused: z.boolean() }).strict().refine(v => !v.registered || v.capture !== null);
16
+ export const tenantWebHeadSchema = z.object({ schemaVersion: z.literal(1), seq: z.number().int().nonnegative(), queued: z.number().int().min(0).max(10000), cursor: z.number().int().min(0).max(10000), stage: z.enum(['ACQUIRE', 'DISCOVER']), after: z.number().int().nonnegative(), attempts: z.number().int().min(0).max(3), currentAcquired: z.boolean(), acquired: z.number().int().min(0).max(10000), registered: z.number().int().min(0).max(10000), 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(), gaps: z.array(z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/)).max(32), state: z.enum(['QUEUED', 'RUNNING', 'COMPLETE', 'PARTIAL']), finishedAt: isoDateTimeSchema.nullable(), notBefore: isoDateTimeSchema.nullable() }).strict().refine(v => v.cursor <= v.queued && v.registered <= v.acquired && v.acquired <= v.queued && (['COMPLETE', 'PARTIAL'].includes(v.state) === (v.finishedAt !== null)) && (!v.reservation || v.state === 'RUNNING' && v.chargedBytes >= v.reservation.bytes && v.chargedMs >= v.reservation.ms) && (!v.notBefore || v.state === 'RUNNING' && !v.reservation) && (v.state !== 'PARTIAL' || v.gaps.length > 0) && (v.state !== 'COMPLETE' || v.queued > 0 && v.cursor === v.queued && v.gaps.length === 0 && v.registered === v.queued && !v.reservation && !v.notBefore));
17
+ export const tenantWebChildOwnerSchema = z.object({ schemaVersion: z.literal(1), jobRef: tenantWebJobRefSchema, ordinal: tenantWebTargetSchema.shape.ordinal, requestId: tenantCollectionCycleSchema, batchId: z.string().regex(/^batch_[a-f0-9]{64}$/) }).strict();
18
+ export const tenantWebCrawlStatusSchema = z.object({ requestId: tenantCollectionCycleSchema, jobRef: tenantWebJobRefSchema, specRevision: sha256Schema, state: tenantWebHeadSchema.shape.state, phase: z.literal('STATIC_ACQUISITION'), requirementsState: z.literal('DECLARED_NOT_EVALUATED'), indexState: z.literal('NOT_EVALUATED'), coverage: z.object({ total: z.number().int().nonnegative().nullable(), discovered: z.number().int().nonnegative(), acquired: z.number().int().nonnegative(), registered: z.number().int().nonnegative(), pending: z.number().int().nonnegative(), gaps: tenantWebHeadSchema.shape.gaps }).strict(), budget: z.object({ chargedBytes: z.number().int().nonnegative(), chargedMs: z.number().int().nonnegative(), conservativeReservation: z.boolean() }).strict(), finishedAt: isoDateTimeSchema.nullable(), nextEligibleAt: isoDateTimeSchema.nullable() }).strict();
19
+ export function tenantWebCrawlStatus(job, head) { return tenantWebCrawlStatusSchema.parse({ requestId: job.intent.requestId, jobRef: job.intent.jobRef, specRevision: job.intent.specRevision, state: head.state, phase: 'STATIC_ACQUISITION', requirementsState: 'DECLARED_NOT_EVALUATED', indexState: 'NOT_EVALUATED', coverage: { total: head.state === 'COMPLETE' ? head.queued : null, discovered: head.queued, acquired: head.acquired, registered: head.registered, pending: head.queued - head.cursor, gaps: head.gaps }, budget: { chargedBytes: head.chargedBytes, chargedMs: head.chargedMs, conservativeReservation: head.reservation !== null }, finishedAt: head.finishedAt, nextEligibleAt: head.notBefore }); }
20
+ export function tenantWebPathMatches(pattern, pathname) { return pattern.endsWith('/**') ? (pathname === pattern.slice(0, -3) || pathname.startsWith(pattern.slice(0, -2))) : pathname === pattern; }
21
+ export function tenantWebInScope(spec, raw, options = {}) {
22
+ const parsed = tenantWebUrlSchema.safeParse(raw);
23
+ if (!parsed.success)
24
+ return false;
25
+ const url = new URL(parsed.data), scope = spec.scope;
26
+ if (!scope.allowedOrigins.includes(url.origin))
27
+ return false;
28
+ if (options.robots)
29
+ return url.pathname === '/robots.txt' && !url.search;
30
+ let pathname;
31
+ try {
32
+ pathname = decodeURIComponent(url.pathname);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ if (/[\\\x00-\x1f]/.test(pathname) || /%[0-9a-f]{2}/i.test(pathname))
38
+ return false;
39
+ // GitLab and similar public APIs encode a slash inside an identifier. Only an exact
40
+ // caller-listed URL without path filters can use it; section/prefix matching stays closed.
41
+ if (/%2f/i.test(url.pathname) && (!['SINGLE_PAGE', 'URL_LIST'].includes(scope.mode) || scope.includePaths.length > 0 || scope.excludePaths.length > 0))
42
+ return false;
43
+ if (scope.excludePaths.some(p => tenantWebPathMatches(p, pathname)))
44
+ return false;
45
+ if (options.bootstrap && raw === spec.url && ['SITEMAP', 'FEED'].includes(scope.mode))
46
+ return true;
47
+ if (scope.includePaths.length && !scope.includePaths.some(p => tenantWebPathMatches(p, pathname)))
48
+ return false;
49
+ if (scope.mode === 'SINGLE_PAGE')
50
+ return raw === spec.url;
51
+ if (scope.mode === 'URL_LIST')
52
+ return raw === spec.url || scope.urls.includes(raw);
53
+ return true;
54
+ }
55
+ export function sameTenantWebDelegation(a, b) { return a.intent.tenantId === b.intent.tenantId && tenantWebCanonical(a.policy) === tenantWebCanonical(b.policy) && tenantWebCanonical(a.intent.authority) === tenantWebCanonical(b.intent.authority) && tenantWebCanonical(a.intent.actor) === tenantWebCanonical(b.intent.actor); }
@@ -0,0 +1,68 @@
1
+ /** S3 opt-in contracts. Frozen Provider21/22 objects are never silently upgraded. */
2
+ import { createHash } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import { tenantWebSpecSchema, tenantWebIntentSchema, tenantWebCanonical, tenantWebJobRef, tenantWebUrlSchema } from './tenant-web-collection.js';
5
+ import { tenantWebPolicySchema, tenantWebTargetSchema, tenantWebOutcomeSchema, tenantWebHeadSchema, tenantWebChildOwnerSchema, tenantWebCrawlStatusSchema, tenantWebInScope } from './tenant-web-crawl.js';
6
+ import { parseTenantResourceRef, tenantStorageKey, tenantResourceRefs } from './resource-scope.js';
7
+ import { isoDateTimeSchema, sha256Schema } from './common.js';
8
+ export const TENANT_WEB_DYNAMIC_CAPABILITY = 'TENANT_WEB_DYNAMIC_V1';
9
+ const selector = z.string().trim().min(1).max(512).refine(s => !s.includes('\0'));
10
+ const origin = tenantWebUrlSchema.refine(s => new URL(s).origin === s);
11
+ export const tenantWebRenderSchema = z.object({
12
+ mode: z.enum(['HTTP_ONLY', 'AUTO', 'ALWAYS']).default('HTTP_ONLY'),
13
+ requiredSelector: selector.nullable().default(null), minTextChars: z.number().int().min(0).max(100000).default(80),
14
+ resourceOrigins: z.array(origin).max(16).default([]),
15
+ interaction: z.object({ kind: z.enum(['NONE', 'NEXT_PAGE', 'LOAD_MORE', 'SCROLL']), selector: selector.nullable().default(null), endSelector: selector.nullable().default(null), maxSteps: z.number().int().min(0).max(100).default(0) }).strict().default({ kind: 'NONE', selector: null, endSelector: null, maxSteps: 0 }),
16
+ }).strict().superRefine((v, c) => {
17
+ if (v.interaction.kind !== 'NONE' && (v.mode === 'HTTP_ONLY' || v.interaction.maxSteps === 0))
18
+ c.addIssue({ code: 'custom', message: 'Interactions require a browser and a positive step bound' });
19
+ if (['NEXT_PAGE', 'LOAD_MORE'].includes(v.interaction.kind) && !v.interaction.selector)
20
+ c.addIssue({ code: 'custom', message: 'Button interactions require a selector' });
21
+ if (v.interaction.kind === 'NONE' && v.interaction.maxSteps !== 0)
22
+ c.addIssue({ code: 'custom', message: 'NONE cannot request actions' });
23
+ if (new Set(v.resourceOrigins).size !== v.resourceOrigins.length)
24
+ c.addIssue({ code: 'custom', message: 'Resource origins must be unique' });
25
+ });
26
+ export const tenantWebDynamicSpecSchema = z.object({ ...tenantWebSpecSchema.shape, schemaVersion: z.literal(2), render: tenantWebRenderSchema.default({ mode: 'HTTP_ONLY', requiredSelector: null, minTextChars: 80, resourceOrigins: [], interaction: { kind: 'NONE', selector: null, endSelector: null, maxSteps: 0 } }) }).strict().superRefine((v, c) => {
27
+ const { render: ignored, ...base } = v;
28
+ const old = tenantWebSpecSchema.safeParse({ ...base, schemaVersion: 1 });
29
+ if (!old.success)
30
+ for (const issue of old.error.issues)
31
+ c.addIssue({ ...issue, path: issue.path });
32
+ if (v.render.interaction.maxSteps > v.limits.maxInteractions)
33
+ c.addIssue({ code: 'custom', message: 'Page actions exceed the job interaction budget' });
34
+ if (v.render.interaction.kind === 'NEXT_PAGE' && !v.scope.followPagination)
35
+ c.addIssue({ code: 'custom', message: 'NEXT_PAGE requires pagination scope' });
36
+ });
37
+ export const tenantWebDynamicSpecRevision = (v) => 'sha256:' + createHash('sha256').update(tenantWebCanonical(tenantWebDynamicSpecSchema.parse(v))).digest('hex');
38
+ export const tenantWebDynamicIntentSchema = z.object({ ...tenantWebIntentSchema.shape, schemaVersion: z.literal(2), requestedSpec: tenantWebDynamicSpecSchema, spec: tenantWebDynamicSpecSchema }).strict().superRefine((v, c) => {
39
+ if (v.actor.tenantId !== v.tenantId || v.jobRef !== tenantWebJobRef(v.tenantId, v.requestId) || v.specRevision !== tenantWebDynamicSpecRevision(v.spec) || v.inputRevision !== tenantWebDynamicSpecRevision(v.requestedSpec))
40
+ c.addIssue({ code: 'custom', message: 'Frozen Tenant intent identity mismatch' });
41
+ if (v.spec.limits.maxResourceBytes > Math.min(v.requestedSpec.limits.maxResourceBytes, v.requestedSpec.limits.maxTotalBytes) || tenantWebCanonical({ ...v.spec, limits: { ...v.spec.limits, maxResourceBytes: v.requestedSpec.limits.maxResourceBytes } }) !== tenantWebCanonical(v.requestedSpec))
42
+ c.addIssue({ code: 'custom', message: 'Only resource limit narrowing is permitted' });
43
+ });
44
+ export const tenantWebDynamicJobSchema = z.object({ schemaVersion: z.literal(3), intent: tenantWebDynamicIntentSchema, policy: tenantWebPolicySchema }).strict();
45
+ export const tenantWebDynamicTargetSchema = z.object({ ...tenantWebTargetSchema.shape, ordinal: z.number().int().min(0).max(10999), role: z.enum(['PAGE', 'SITEMAP', 'FEED', 'ATTACHMENT']), attachment: z.enum(['PDF', 'IMAGE', 'AUDIO', 'VIDEO', 'OTHER']).nullable().default(null) }).strict().refine(v => (v.role === 'ATTACHMENT') === (v.attachment !== null));
46
+ export const tenantWebDynamicOutcomeSchema = z.object({ ...tenantWebOutcomeSchema.shape, ordinal: tenantWebDynamicTargetSchema.shape.ordinal }).strict().refine(v => !v.registered || v.capture !== null);
47
+ const count = z.number().int().min(0).max(11000);
48
+ export const tenantWebDynamicHeadSchema = z.object({ ...tenantWebHeadSchema.shape,
49
+ schemaVersion: z.literal(2), queued: count, cursor: count, acquired: count, registered: count,
50
+ phase: z.enum(['RENDER', 'LINKS', 'PDF']).default('RENDER'), renderIndex: z.number().int().min(0).max(101), linkState: z.number().int().min(-1).max(101),
51
+ browserPages: z.number().int().min(0).max(10000), pagesQueued: z.number().int().min(0).max(10000), filesQueued: z.number().int().min(0).max(1000), renderedStates: count, pdfExtracted: z.number().int().min(0).max(1000), interactions: z.number().int().min(0).max(1000),
52
+ reservation: z.object({ bytes: z.number().int().nonnegative(), ms: z.number().int().nonnegative(), interactions: z.number().int().min(0).max(1000) }).strict().nullable(),
53
+ }).strict().refine(v => v.queued === v.pagesQueued + v.filesQueued && v.cursor <= v.queued && v.registered <= v.acquired && v.acquired <= v.queued && (['COMPLETE', 'PARTIAL'].includes(v.state) === (v.finishedAt !== null)) && (!v.reservation || v.state === 'RUNNING' && v.chargedBytes >= v.reservation.bytes && v.chargedMs >= v.reservation.ms && v.interactions >= v.reservation.interactions) && (!v.notBefore || v.state === 'RUNNING' && !v.reservation) && (v.state !== 'PARTIAL' || v.gaps.length > 0) && (v.state !== 'COMPLETE' || v.queued > 0 && v.cursor === v.queued && v.gaps.length === 0 && v.registered === v.queued && !v.reservation && !v.notBefore));
54
+ export const tenantWebDynamicChildOwnerSchema = z.object({ ...tenantWebChildOwnerSchema.shape, schemaVersion: z.literal(2), ordinal: tenantWebDynamicTargetSchema.shape.ordinal }).strict();
55
+ export const tenantWebDynamicStatusSchema = z.object({ ...tenantWebCrawlStatusSchema.shape, phase: z.literal('DYNAMIC_ACQUISITION'), ocr: z.object({ capability: z.literal('UNAVAILABLE'), budgetState: z.literal('NOT_CONFIGURED'), processedPages: z.literal(0) }).strict(), dynamic: z.object({ browserPages: z.number().int().nonnegative(), pagesQueued: z.number().int().nonnegative(), filesQueued: z.number().int().nonnegative(), renderedStates: z.number().int().nonnegative(), pdfExtracted: z.number().int().nonnegative(), interactions: z.number().int().nonnegative() }).strict() }).strict();
56
+ export function tenantWebDynamicStatus(job, h) { return tenantWebDynamicStatusSchema.parse({ requestId: job.intent.requestId, jobRef: job.intent.jobRef, specRevision: job.intent.specRevision, state: h.state, phase: 'DYNAMIC_ACQUISITION', ocr: { capability: 'UNAVAILABLE', budgetState: 'NOT_CONFIGURED', processedPages: 0 }, requirementsState: 'DECLARED_NOT_EVALUATED', indexState: 'NOT_EVALUATED', coverage: { total: h.state === 'COMPLETE' ? h.queued : null, discovered: h.queued, acquired: h.acquired, registered: h.registered, pending: h.queued - h.cursor, gaps: h.gaps }, budget: { chargedBytes: h.chargedBytes, chargedMs: h.chargedMs, conservativeReservation: h.reservation !== null }, dynamic: { browserPages: h.browserPages, pagesQueued: h.pagesQueued, filesQueued: h.filesQueued, renderedStates: h.renderedStates, pdfExtracted: h.pdfExtracted, interactions: h.interactions }, finishedAt: h.finishedAt, nextEligibleAt: h.notBefore }); }
57
+ export function tenantWebAttachmentInScope(spec, url, robots = false) { return tenantWebInScope({ url: spec.url, scope: { ...spec.scope, mode: 'SITE_SECTION' } }, url, { robots }); }
58
+ /** Browser records refer to retained immutable evidence, never inline runtime bodies. */
59
+ export const tenantWebBrowserResponseSchema = z.object({ url: tenantWebUrlSchema, finalUrl: tenantWebUrlSchema, status: z.number().int().min(200).max(299), headers: z.record(z.string(), z.string().max(8192)), ref: z.string().min(1).max(1024), hash: sha256Schema, observedAt: isoDateTimeSchema }).strict();
60
+ export const tenantWebBrowserStateSchema = z.object({ schemaVersion: z.literal(1), ordinal: tenantWebDynamicTargetSchema.shape.ordinal, index: z.number().int().min(0).max(100), url: tenantWebUrlSchema, domRef: z.string().min(1).max(1024), domHash: sha256Schema, visibleRef: z.string().min(1).max(1024).optional(), visibleHash: sha256Schema.optional(), observedAt: isoDateTimeSchema, responseCount: z.number().int().min(0).max(64), navigationCount: z.number().int().min(0).max(64), actions: z.number().int().min(0).max(1000), done: z.boolean(), gap: z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/).nullable(), interactionRef: z.string().min(1).max(1024) }).strict();
61
+ export function tenantWebPartIdentity(tenantId, parentRef, part) {
62
+ const parent = parseTenantResourceRef(parentRef);
63
+ if (!part || part.length > 256 || !parent || parent.storageKey !== tenantStorageKey(tenantId) || parent.resourceType !== 'evidence')
64
+ throw new Error('TENANT_WEB_PART_IDENTITY');
65
+ const resourceId = 'web_' + createHash('sha256').update(tenantWebCanonical(['TENANT_WEB_PART_V1', tenantId, parentRef, part])).digest('hex');
66
+ return { resourceId, version: 1, ...tenantResourceRefs({ tenantId, resourceType: 'evidence', resourceId, version: 1 }) };
67
+ }
68
+ export const tenantWebBrowserCapabilitySchema = z.object({ executablePath: z.string().min(1).max(4096).refine(s => s.startsWith('/') && !s.includes('\0')), navigationTimeoutMs: z.number().int().min(1000).max(30000).default(15000), settleMs: z.number().int().min(100).max(3000).default(300), maxRequests: z.number().int().min(1).max(64).default(32), captureVisible: z.boolean().optional() }).strict();
@@ -0,0 +1,41 @@
1
+ /** 结果与覆盖独立于意图受理;仅有原件/字段出处才允许声明满足要求。 */
2
+ import { z } from 'zod';
3
+ import { isoDateTimeSchema, sha256Schema } from './common.js';
4
+ import { tenantResourceRefSchema, parseTenantResourceRef } from './resource-scope.js';
5
+ import { tenantWebJobRefSchema, tenantWebUrlSchema } from './tenant-web-collection.js';
6
+ const count = z.number().int().nonnegative().safe();
7
+ export const tenantWebFieldResultSchema = z.object({
8
+ name: z.string().min(1).max(64), state: z.enum(['PRESENT', 'MISSING', 'UNSUPPORTED']), value: z.json().nullable(),
9
+ citations: z.array(z.object({ evidenceRef: tenantResourceRefSchema, contentHash: sha256Schema, locator: z.string().min(1).max(2048), excerpt: z.string().min(1).max(4000) }).strict()).max(16),
10
+ reason: z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/).nullable(),
11
+ }).strict().superRefine((v, ctx) => {
12
+ if (v.state === 'PRESENT' ? (v.value === null || v.citations.length === 0 || v.reason !== null) : (v.value !== null || v.reason === null))
13
+ ctx.addIssue({ code: 'custom', message: 'Present values require citations; gaps require a reason without an invented value' });
14
+ if (v.citations.some(c => parseTenantResourceRef(c.evidenceRef)?.resourceType !== 'evidence'))
15
+ ctx.addIssue({ code: 'custom', message: 'Citations require actual Evidence identities' });
16
+ });
17
+ export const tenantWebResultSchema = z.object({
18
+ schemaVersion: z.literal(1), jobRef: tenantWebJobRefSchema, evidenceRef: tenantResourceRefSchema,
19
+ sourceUrl: tenantWebUrlSchema, contentHash: sha256Schema, observedAt: isoDateTimeSchema,
20
+ documentRef: tenantResourceRefSchema.nullable(), fields: z.array(tenantWebFieldResultSchema).max(64),
21
+ }).strict().superRefine((v, ctx) => {
22
+ const key = new URL(v.jobRef).hostname, mine = (ref) => parseTenantResourceRef(ref)?.storageKey === key;
23
+ if (!mine(v.evidenceRef) || parseTenantResourceRef(v.evidenceRef)?.resourceType !== 'evidence' || v.documentRef && (!mine(v.documentRef) || parseTenantResourceRef(v.documentRef)?.resourceType !== 'source_document') || v.fields.some(f => f.citations.some(c => !mine(c.evidenceRef))))
24
+ ctx.addIssue({ code: 'custom', message: 'Results and their typed evidence/document must retain the owning Tenant' });
25
+ });
26
+ export const tenantWebCoverageSchema = z.object({
27
+ schemaVersion: z.literal(1), jobRef: tenantWebJobRefSchema, scopeRevision: sha256Schema,
28
+ state: z.enum(['NOT_STARTED', 'PARTIAL', 'COMPLETE', 'UNKNOWN']), total: count.nullable(),
29
+ discovered: count, acquired: count, extracted: count, registered: count, pending: count,
30
+ frontierExhausted: z.boolean(), limitReached: z.boolean(),
31
+ gaps: z.array(z.object({ code: z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/), url: tenantWebUrlSchema.nullable() }).strict()).max(100),
32
+ }).strict().superRefine((v, ctx) => {
33
+ if (v.state === 'COMPLETE' && (!v.frontierExhausted || v.pending !== 0 || v.limitReached || v.gaps.length > 0))
34
+ ctx.addIssue({ code: 'custom', message: 'A truncated, pending or incomplete scope cannot be complete' });
35
+ if (v.state === 'COMPLETE' && (v.total !== v.discovered || v.acquired !== v.discovered || v.registered !== v.acquired))
36
+ ctx.addIssue({ code: 'custom', message: 'Completion requires a known exhausted target count and all originals registered' });
37
+ if (v.state === 'NOT_STARTED' && (v.discovered !== 0 || v.acquired !== 0 || v.extracted !== 0 || v.registered !== 0 || v.pending !== 0))
38
+ ctx.addIssue({ code: 'custom', message: 'Not-started coverage cannot carry completed work' });
39
+ if (v.acquired > v.discovered || v.extracted > v.acquired || v.registered > v.acquired || v.pending > v.discovered || v.total !== null && v.total < v.discovered)
40
+ ctx.addIssue({ code: 'custom', message: 'Coverage counters must describe actual bounded work' });
41
+ });
@@ -0,0 +1,29 @@
1
+ /** S5 operational state; acquisition/content evidence and business review remain separate. */
2
+ import { z } from 'zod';
3
+ import { tenantWebContentJobSchema } from './tenant-web-content.js';
4
+ import { tenantWebJobRefSchema, tenantWebCanonical } from './tenant-web-collection.js';
5
+ import { tenantCollectionCycleSchema } from './tenant-collection.js';
6
+ import { isoDateTimeSchema } from './common.js';
7
+ export const TENANT_WEB_WORKFLOW_CAPABILITY = 'TENANT_WEB_WORKFLOW_V1';
8
+ export const tenantWebScheduleSchema = z.discriminatedUnion('mode', [z.object({ mode: z.literal('ONCE') }).strict(), z.object({ mode: z.literal('PERIODIC'), intervalSeconds: z.number().int().min(60).max(2592000) }).strict()]);
9
+ export const tenantWebWorkflowJobSchema = z.object({ ...tenantWebContentJobSchema.shape, schemaVersion: z.literal(5), schedule: tenantWebScheduleSchema, cycle: z.object({ kind: z.enum(['INITIAL', 'MANUAL_REFRESH', 'DUE']), rootJobRef: tenantWebJobRefSchema, previousJobRef: tenantWebJobRefSchema.nullable(), ordinal: z.number().int().nonnegative().safe(), requestedEpoch: z.number().int().nonnegative().safe() }).strict() }).strict().superRefine((v, c) => {
10
+ const old = tenantWebContentJobSchema.safeParse({ schemaVersion: 4, intent: v.intent, policy: v.policy, contentPlan: v.contentPlan, contentRevision: v.contentRevision, retainedFrom: v.retainedFrom });
11
+ if (!old.success)
12
+ c.addIssue({ code: 'custom', message: 'Invalid frozen content intent' });
13
+ const wrongTenant = [v.cycle.rootJobRef, v.cycle.previousJobRef].some(ref => ref && new URL(ref).hostname !== new URL(v.intent.jobRef).hostname), wrongCycle = v.cycle.kind === 'INITIAL' ? (v.cycle.rootJobRef !== v.intent.jobRef || v.cycle.previousJobRef !== null || v.cycle.ordinal !== 0 || v.cycle.requestedEpoch !== 0) : (v.cycle.rootJobRef === v.intent.jobRef || v.cycle.previousJobRef === null || v.cycle.ordinal < 1 || v.retainedFrom !== null);
14
+ if (wrongTenant || wrongCycle)
15
+ c.addIssue({ code: 'custom', message: 'Cycle must retain its exact Tenant/root/previous identity' });
16
+ if (v.retainedFrom && v.schedule.mode !== 'ONCE')
17
+ c.addIssue({ code: 'custom', message: 'Retained derivation cannot enable periodic source observations' });
18
+ });
19
+ export function tenantWebExecutableContent(job) { return tenantWebContentJobSchema.parse({ schemaVersion: 4, intent: job.intent, policy: job.policy, contentPlan: job.contentPlan, contentRevision: job.contentRevision, retainedFrom: job.retainedFrom }); }
20
+ export const tenantWebLifecycleSchema = z.object({ schemaVersion: z.literal(1), seq: z.number().int().nonnegative().safe(), epoch: z.number().int().nonnegative().safe(), state: z.enum(['ACTIVE', 'PAUSED', 'CANCELLED']), currentJobRef: tenantWebJobRefSchema, cycleOrdinal: z.number().int().nonnegative().safe(), lastObservedAt: isoDateTimeSchema.nullable(), updatedAt: isoDateTimeSchema }).strict().refine(v => v.epoch <= v.seq && v.cycleOrdinal <= v.seq);
21
+ export const tenantWebControlInputSchema = z.object({ jobRef: tenantWebJobRefSchema, requestId: tenantCollectionCycleSchema, expectedEpoch: z.number().int().nonnegative().safe(), action: z.enum(['PAUSE', 'RESUME', 'CANCEL']) }).strict();
22
+ export const tenantWebControlOperationSchema = z.object({ ...tenantWebControlInputSchema.shape, actor: tenantWebWorkflowJobSchema.shape.intent.shape.actor, authority: tenantWebWorkflowJobSchema.shape.intent.shape.authority }).strict();
23
+ export const tenantWebLifecycleReceiptSchema = z.object({ operation: tenantWebControlOperationSchema, committed: tenantWebLifecycleSchema }).strict();
24
+ export const tenantWebWorkflowStatusSchema = z.object({ jobRef: tenantWebJobRefSchema, requestId: tenantCollectionCycleSchema, rootJobRef: tenantWebJobRefSchema, executionJobRef: tenantWebJobRefSchema, state: z.enum(['QUEUED', 'RUNNING', 'COMPLETE', 'PARTIAL', 'PAUSED', 'CANCELLED']), control: tenantWebLifecycleSchema, cycle: tenantWebWorkflowJobSchema.shape.cycle, schedule: tenantWebScheduleSchema, nextDueAt: isoDateTimeSchema.nullable(), stages: z.object({ intent: z.literal('PERSISTED'), acquisition: z.object({ state: z.enum(['QUEUED', 'RUNNING', 'COMPLETE', 'PARTIAL']), discovered: z.number().int().nonnegative(), acquired: z.number().int().nonnegative(), registered: z.number().int().nonnegative(), pending: z.number().int().nonnegative(), gaps: z.array(z.string()).max(32) }).strict(), content: z.object({ state: z.enum(['PENDING', 'SATISFIED', 'PARTIAL']), processed: z.number().int().nonnegative(), registered: z.number().int().nonnegative(), gaps: z.array(z.string()).max(32) }).strict(), index: z.literal('NOT_PROVEN_QUERY_TENANT_INDEX_STATUS'), readability: z.literal('NOT_PROVEN_UNTIL_ACTUAL_READ'), review: z.literal('UNREVIEWED') }).strict(), budget: z.object({ chargedBytes: z.number().int().nonnegative(), chargedMs: z.number().int().nonnegative(), conservativeReservation: z.boolean() }).strict() }).strict().superRefine((v, c) => { const key = new URL(v.jobRef).hostname; if ([v.rootJobRef, v.executionJobRef, v.control.currentJobRef, v.cycle.rootJobRef].some(ref => new URL(ref).hostname !== key))
25
+ c.addIssue({ code: 'custom', message: 'Exact Tenant workflow required' }); });
26
+ export function tenantWebNextDue(root, head, current) { if (root.schedule.mode !== 'PERIODIC' || head.state !== 'ACTIVE')
27
+ return null; return new Date(Math.max(Date.parse(head.lastObservedAt ?? current.intent.submittedAt), Date.parse(current.intent.submittedAt)) + root.schedule.intervalSeconds * 1000).toISOString(); }
28
+ export const sameTenantWebWorkflow = (a, b) => tenantWebCanonical(a.intent.actor) === tenantWebCanonical(b.intent.actor) && tenantWebCanonical(a.intent.authority) === tenantWebCanonical(b.intent.authority) && tenantWebCanonical(a.policy) === tenantWebCanonical(b.policy) && a.intent.tenantId === b.intent.tenantId && a.intent.specRevision === b.intent.specRevision && a.contentRevision === b.contentRevision && tenantWebCanonical(a.schedule) === tenantWebCanonical(b.schedule);
29
+ export const tenantWebLifecycleEventSchema = z.object({ before: tenantWebLifecycleSchema, after: tenantWebLifecycleSchema, operation: tenantWebControlOperationSchema.nullable() }).strict().refine(v => v.after.seq === v.before.seq + 1 && v.after.epoch === v.before.epoch + (v.operation ? 1 : 0) && (!v.before.lastObservedAt || v.after.lastObservedAt !== null && v.after.lastObservedAt >= v.before.lastObservedAt) && v.after.cycleOrdinal >= v.before.cycleOrdinal && v.after.cycleOrdinal <= v.before.cycleOrdinal + 1 && (v.operation !== null || v.after.state === v.before.state) && (!v.operation || v.operation.expectedEpoch === v.before.epoch && v.after.cycleOrdinal === v.before.cycleOrdinal && v.after.currentJobRef === v.before.currentJobRef && v.after.state === (v.operation.action === 'PAUSE' ? 'PAUSED' : v.operation.action === 'RESUME' ? 'ACTIVE' : 'CANCELLED')) && (v.before.state !== 'CANCELLED' || v.after.state === 'CANCELLED') && (v.after.cycleOrdinal !== v.before.cycleOrdinal ? v.before.state === 'ACTIVE' && v.after.state === 'ACTIVE' && v.after.currentJobRef !== v.before.currentJobRef : v.after.currentJobRef === v.before.currentJobRef));