From ea5883494036ddb30d138fdb381d54b2d9808b49 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:29:20 +0800 Subject: [PATCH] refactor(domain): own digest and prefixed identifier shapes in one module (#547) --- src/application/ProjectPermissionStore.ts | 3 +- src/contracts/sessionToolSchemas.ts | 3 +- src/contracts/toolOutputSchemaPrimitives.ts | 3 +- src/domain/analysisProfile.ts | 3 +- src/domain/analysisSnapshot.ts | 5 +- src/domain/androidApplication.ts | 9 +-- src/domain/appleApplication.ts | 9 +-- src/domain/artifactComparison.ts | 7 +- src/domain/artifactGraph.ts | 13 ++-- src/domain/artifactInspection.ts | 25 +++---- src/domain/browserObservationSchemas.ts | 3 +- src/domain/browserScenarioCaptureValues.ts | 2 +- src/domain/bundleComparison.ts | 5 +- src/domain/callPathSchemas.ts | 3 +- src/domain/changedBehavior.ts | 3 +- src/domain/completionLedgerGeneration.ts | 5 +- src/domain/conformancePackage.ts | 3 +- src/domain/digests.test.ts | 67 +++++++++++++++++++ src/domain/digests.ts | 42 ++++++++++++ src/domain/electronObservation.ts | 5 +- src/domain/evidence.ts | 7 +- src/domain/evidenceCompletionLedger.ts | 5 +- src/domain/functionComparisonSchemas.ts | 5 +- src/domain/javascriptApplicationAnalysis.ts | 5 +- .../javascriptApplicationEvidenceSchemas.ts | 7 +- .../javascriptApplicationGraphSchemas.ts | 15 +++-- ...riptApplicationVersionComparisonSchemas.ts | 13 ++-- src/domain/javascriptExportInstrumentation.ts | 3 +- .../javascriptExportShapeComparisonSchemas.ts | 11 +-- src/domain/javascriptFeatureTraceSchemas.ts | 15 +++-- src/domain/javascriptReplay.ts | 2 +- src/domain/javascriptRuntimeObservation.ts | 3 +- .../javascriptRuntimeReconciliationSchemas.ts | 21 +++--- src/domain/javascriptSemanticGraphSchemas.ts | 13 ++-- src/domain/javascriptSemanticQuerySchemas.ts | 11 +-- src/domain/javascriptSemanticTraceSchemas.ts | 3 +- src/domain/managedApplicationGraph.ts | 7 +- src/domain/managedArtifact.ts | 2 +- src/domain/managedMemberComparison.ts | 11 +-- .../managedNativeVerificationSchemas.ts | 9 +-- src/domain/managedReconstruction.ts | 7 +- src/domain/managedRuntimeCorrelation.ts | 7 +- src/domain/nodeRuntimeCharacterization.ts | 2 +- src/domain/reconstructionCoverage.ts | 5 +- .../reconstructionObligationLedgerSchemas.ts | 7 +- src/domain/reconstructionReadinessSchemas.ts | 7 +- src/domain/reconstructionVerification.ts | 3 +- .../reconstructionVerificationSchemas.ts | 7 +- src/domain/referenceSourceGraph.ts | 5 +- src/domain/residualUnknown.ts | 5 +- src/domain/runtimeCharacterization.ts | 2 +- src/domain/runtimeIdentification.ts | 9 +-- src/domain/sourceToBundleComparisonSchemas.ts | 13 ++-- src/domain/sourceToBundleSignals.ts | 3 +- src/domain/staticRuntimeCorrelation.ts | 5 +- src/domain/webMcpDiscovery.ts | 3 +- 56 files changed, 314 insertions(+), 162 deletions(-) create mode 100644 src/domain/digests.test.ts create mode 100644 src/domain/digests.ts diff --git a/src/application/ProjectPermissionStore.ts b/src/application/ProjectPermissionStore.ts index 576bb954e..7a4d3e0dd 100644 --- a/src/application/ProjectPermissionStore.ts +++ b/src/application/ProjectPermissionStore.ts @@ -23,6 +23,7 @@ import { type PermissionGrant, } from "../domain/permissionPolicy.js"; import { err, ok, type Result } from "../domain/result.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const grantSchema = z.object({ grant_id: z.string().min(1), @@ -39,7 +40,7 @@ const grantSchema = z.object({ }); const storeSchema = z.object({ - project_id: z.string().regex(/^project_[a-f0-9]{64}$/u), + project_id: prefixedDigestSchema("project"), project_root: z.string(), grants: z.array(grantSchema), }); diff --git a/src/contracts/sessionToolSchemas.ts b/src/contracts/sessionToolSchemas.ts index dd0462ad0..d0684bf22 100644 --- a/src/contracts/sessionToolSchemas.ts +++ b/src/contracts/sessionToolSchemas.ts @@ -19,6 +19,7 @@ import { closeBinaryInputSchema, } from "./sessionLifecycleInputs.js"; import { binarySessionInputSchema } from "./sessionStatusContract.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; /** Return the current canonical Evidence bundle inline. */ export const getEvidenceBundleInputSchema = z.strictObject({}); @@ -58,7 +59,7 @@ export const listUnknownsInputSchema = z.strictObject({ /** Exact residual-unknown identity to revalidate. */ export const verifyUnknownResolutionInputSchema = z.strictObject({ - unknown_id: z.string().regex(/^unk_[a-f0-9]{64}$/u), + unknown_id: prefixedDigestSchema("unk"), }); export { diff --git a/src/contracts/toolOutputSchemaPrimitives.ts b/src/contracts/toolOutputSchemaPrimitives.ts index d3dc0fd63..489e544b5 100644 --- a/src/contracts/toolOutputSchemaPrimitives.ts +++ b/src/contracts/toolOutputSchemaPrimitives.ts @@ -14,6 +14,7 @@ import { type ProviderRejectionCode, } from "./providerSelection.js"; import { analysisErrorProjectionSchema } from "./errorSchemas.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; /** Inline result with its complete Evidence record. */ export const inlineEvidenceRecordSchema = evidenceEnvelopeSchema.omit({ @@ -23,7 +24,7 @@ export const inlineEvidenceRecordSchema = evidenceEnvelopeSchema.omit({ export const evidenceResultOf = (schema: z.ZodType) => z.strictObject({ result: schema, - evidence_id: z.string().regex(/^ev_[a-f0-9]{64}$/u), + evidence_id: prefixedDigestSchema("ev"), evidence: inlineEvidenceRecordSchema, }); diff --git a/src/domain/analysisProfile.ts b/src/domain/analysisProfile.ts index d6efd17f8..6719e2ccd 100644 --- a/src/domain/analysisProfile.ts +++ b/src/domain/analysisProfile.ts @@ -4,8 +4,7 @@ import canonicalize from "canonicalize"; import { z } from "zod"; import { jsonObjectSchema, type JsonValue } from "./jsonValue.js"; - -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); +import { digestSchema } from "./../domain/digests.js"; /** Concrete provider identity committed by an analysis profile. */ export const committedProviderSchema = z.object({ diff --git a/src/domain/analysisSnapshot.ts b/src/domain/analysisSnapshot.ts index 1c4059ec1..65f6b7ec5 100644 --- a/src/domain/analysisSnapshot.ts +++ b/src/domain/analysisSnapshot.ts @@ -26,8 +26,9 @@ import { jsonValueSchema, type JsonValue, } from "./jsonValue.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const architectureSchema = z.enum(["x86", "x86_64", "arm", "arm64"]); const formatSchema = z.enum([ "analysis-database", @@ -86,7 +87,7 @@ const bindingSchema = z }); }); const entrySchema = z.object({ - query_id: z.string().regex(/^query_[a-f0-9]{64}$/u), + query_id: prefixedDigestSchema("query"), operation: z.string().min(1), parameters: jsonObjectSchema, execution: z.object({ diff --git a/src/domain/androidApplication.ts b/src/domain/androidApplication.ts index 882f9999b..8fe7fca6b 100644 --- a/src/domain/androidApplication.ts +++ b/src/domain/androidApplication.ts @@ -5,13 +5,14 @@ import { z } from "zod"; import { parseArtifactInventoryEvidence } from "./artifactInventoryEvidence.js"; import { evidenceSchema } from "./evidence.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const pathSchema = z.string().min(1); const componentSchema = z.strictObject({ path: pathSchema, - artifact_id: z.string().regex(/^art_[a-f0-9]{64}$/u), + artifact_id: prefixedDigestSchema("art"), sha256: digestSchema, format: z.string().min(1), }); @@ -23,7 +24,7 @@ export const androidApplicationProjectionInputSchema = z.strictObject({ /** Deterministic, execution-free Android application inventory projection. */ export const androidApplicationProjectionResultSchema = z.strictObject({ - projection_id: z.string().regex(/^adp_[a-f0-9]{64}$/u), + projection_id: prefixedDigestSchema("adp"), root_sha256: digestSchema, root_format: z.literal("apk"), source_evidence_ids: z.array(evidenceIdSchema).min(1), diff --git a/src/domain/appleApplication.ts b/src/domain/appleApplication.ts index 93ba4426b..cf130e4f5 100644 --- a/src/domain/appleApplication.ts +++ b/src/domain/appleApplication.ts @@ -5,14 +5,15 @@ import { z } from "zod"; import { parseArtifactInventoryEvidence } from "./artifactInventoryEvidence.js"; import { evidenceSchema } from "./evidence.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const pathSchema = z.string().min(1); const componentSchema = z.strictObject({ path: pathSchema, - artifact_id: z.string().regex(/^art_[a-f0-9]{64}$/u), + artifact_id: prefixedDigestSchema("art"), sha256: digestSchema, format: z.string().min(1), }); @@ -24,7 +25,7 @@ export const appleApplicationProjectionInputSchema = z.strictObject({ /** Deterministic, execution-free Apple application inventory projection. */ export const appleApplicationProjectionResultSchema = z.strictObject({ - projection_id: z.string().regex(/^aap_[a-f0-9]{64}$/u), + projection_id: prefixedDigestSchema("aap"), root_sha256: digestSchema, root_format: z.literal("ipa"), source_evidence_ids: z.array(evidenceIdSchema).min(1), diff --git a/src/domain/artifactComparison.ts b/src/domain/artifactComparison.ts index 7672e2b5c..733465f47 100644 --- a/src/domain/artifactComparison.ts +++ b/src/domain/artifactComparison.ts @@ -11,8 +11,9 @@ import { parseArtifactInventoryEvidence, type InventorySet, } from "./artifactInventoryEvidence.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const comparisonStatusSchema = z.enum([ "unchanged", "added", @@ -73,8 +74,8 @@ const artifactChangeSchema = z.object({ /** Deterministic artifact comparison with every change returned inline. */ export const artifactComparisonResultSchema = z.object({ status: comparisonStatusSchema, - left_manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u), - right_manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u), + left_manifest_id: prefixedDigestSchema("agm"), + right_manifest_id: prefixedDigestSchema("agm"), summary: z.object({ unchanged: z.number().int().min(0), added: z.number().int().min(0), diff --git a/src/domain/artifactGraph.ts b/src/domain/artifactGraph.ts index 97891f379..0017dfcad 100644 --- a/src/domain/artifactGraph.ts +++ b/src/domain/artifactGraph.ts @@ -1,12 +1,13 @@ import { z } from "zod"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u); -const artifactIdSchema = z.string().regex(/^art_[a-f0-9]{64}$/u); -const occurrenceIdSchema = z.string().regex(/^occ_[a-f0-9]{64}$/u); -const edgeIdSchema = z.string().regex(/^edge_[a-f0-9]{64}$/u); -const manifestIdSchema = z.string().regex(/^agm_[a-f0-9]{64}$/u); -const extractionIdSchema = z.string().regex(/^aex_[a-f0-9]{64}$/u); -const contradictionIdSchema = z.string().regex(/^ic_[a-f0-9]{64}$/u); +const artifactIdSchema = prefixedDigestSchema("art"); +const occurrenceIdSchema = prefixedDigestSchema("occ"); +const edgeIdSchema = prefixedDigestSchema("edge"); +const manifestIdSchema = prefixedDigestSchema("agm"); +const extractionIdSchema = prefixedDigestSchema("aex"); +const contradictionIdSchema = prefixedDigestSchema("ic"); const boundedRelativePathSchema = z .string() .min(1) diff --git a/src/domain/artifactInspection.ts b/src/domain/artifactInspection.ts index b1b95d6e7..3cbb3d221 100644 --- a/src/domain/artifactInspection.ts +++ b/src/domain/artifactInspection.ts @@ -9,13 +9,14 @@ import { } from "./artifactGraph.js"; import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js"; import { jsonValueSchema } from "./jsonValue.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const textSchema = z.string().min(1); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const artifactIdSchema = z.string().regex(/^art_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); +const artifactIdSchema = prefixedDigestSchema("art"); const inspectionObservationSchema = z.strictObject({ - observation_id: z.string().regex(/^aio_[a-f0-9]{64}$/u), + observation_id: prefixedDigestSchema("aio"), kind: z.enum(["root-manifest", "artifact", "occurrence", "integrity"]), subject: textSchema, value: jsonValueSchema, @@ -23,7 +24,7 @@ const inspectionObservationSchema = z.strictObject({ }); const inspectionRelationshipSchema = z.strictObject({ - relationship_id: z.string().regex(/^air_[a-f0-9]{64}$/u), + relationship_id: prefixedDigestSchema("air"), relation: z.enum([ "contains", "extracts", @@ -35,13 +36,13 @@ const inspectionRelationshipSchema = z.strictObject({ ]), source_artifact_id: artifactIdSchema, target_artifact_id: artifactIdSchema, - occurrence_id: z.string().regex(/^occ_[a-f0-9]{64}$/u), + occurrence_id: prefixedDigestSchema("occ"), logical_path: textSchema.nullable(), evidence_id: evidenceIdSchema, }); const inspectionHypothesisSchema = z.strictObject({ - hypothesis_id: z.string().regex(/^aih_[a-f0-9]{64}$/u), + hypothesis_id: prefixedDigestSchema("aih"), statement: textSchema, confidence: z.enum(["medium", "low"]), basis_evidence_ids: z.array(evidenceIdSchema).min(1), @@ -49,9 +50,9 @@ const inspectionHypothesisSchema = z.strictObject({ }); const inspectionContradictionSchema = z.strictObject({ - contradiction_id: z.string().regex(/^aic_[a-f0-9]{64}$/u), + contradiction_id: prefixedDigestSchema("aic"), statement: textSchema, - occurrence_id: z.string().regex(/^occ_[a-f0-9]{64}$/u), + occurrence_id: prefixedDigestSchema("occ"), declared_sha256: z.string().regex(/^[a-f0-9]{64}$/u), observed_sha256: z.string().regex(/^[a-f0-9]{64}$/u), evidence_id: evidenceIdSchema, @@ -69,7 +70,7 @@ const nextProbeSchema = z.strictObject({ }); const unexploredBranchSchema = z.strictObject({ - branch_id: z.string().regex(/^aib_[a-f0-9]{64}$/u), + branch_id: prefixedDigestSchema("aib"), reason: z.enum(["format-specific-analysis-required", "unknown-format"]), detail: textSchema, next_probe: nextProbeSchema.nullable(), @@ -78,9 +79,9 @@ const unexploredBranchSchema = z.strictObject({ /** Provider-neutral inspection result with its atomic source Evidence. */ export const artifactInspectionResultSchema = z.strictObject({ - inspection_id: z.string().regex(/^ai_[a-f0-9]{64}$/u), + inspection_id: prefixedDigestSchema("ai"), subject: z.strictObject({ - manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u), + manifest_id: prefixedDigestSchema("agm"), root_artifact_id: artifactIdSchema, root_sha256: z.string().regex(/^[a-f0-9]{64}$/u), root_format: textSchema, @@ -88,7 +89,7 @@ export const artifactInspectionResultSchema = z.strictObject({ substeps: z .array( z.strictObject({ - substep_id: z.string().regex(/^ais_[a-f0-9]{64}$/u), + substep_id: prefixedDigestSchema("ais"), operation: z.literal("inventory_artifact"), status: z.literal("completed"), evidence_id: evidenceIdSchema, diff --git a/src/domain/browserObservationSchemas.ts b/src/domain/browserObservationSchemas.ts index 0494cf5bd..2bd4d3ad0 100644 --- a/src/domain/browserObservationSchemas.ts +++ b/src/domain/browserObservationSchemas.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { browserCompletenessSchema } from "./browserCompleteness.js"; import { webTextArtifactSchema } from "./webContentArtifact.js"; import { jsonShapeSchema } from "./jsonShape.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const browserVersionSchema = z.object({ product: z.string(), @@ -96,7 +97,7 @@ const browserScriptSchema = z.object({ source: browserScriptSourceSchema, }); const browserResourceSchema = z.object({ - resource_key: z.string().regex(/^res_[a-f0-9]{64}$/u), + resource_key: prefixedDigestSchema("res"), url: z.string(), origin: z.string().nullable(), type: z.string(), diff --git a/src/domain/browserScenarioCaptureValues.ts b/src/domain/browserScenarioCaptureValues.ts index 41a2bda1a..4d29442c5 100644 --- a/src/domain/browserScenarioCaptureValues.ts +++ b/src/domain/browserScenarioCaptureValues.ts @@ -5,8 +5,8 @@ import { z } from "zod"; import { emptyArraySchema } from "./emptyArraySchema.js"; import { sanitizedBrowserUrlSchema } from "./browserObservation.js"; import { webScreenshotArtifactSchema } from "./webScreenshot.js"; +import { digestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const textArtifactSchema = z .strictObject({ sha256: digestSchema, diff --git a/src/domain/bundleComparison.ts b/src/domain/bundleComparison.ts index c0190cbc9..c7e2080ea 100644 --- a/src/domain/bundleComparison.ts +++ b/src/domain/bundleComparison.ts @@ -8,9 +8,10 @@ import { type EvidenceBundle, } from "./evidenceBundle.js"; import type { ResidualUnknown } from "./residualUnknown.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const classificationSchema = z.enum([ "added", "removed", diff --git a/src/domain/callPathSchemas.ts b/src/domain/callPathSchemas.ts index 208efb0e2..2d964f78f 100644 --- a/src/domain/callPathSchemas.ts +++ b/src/domain/callPathSchemas.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { emptyArraySchema } from "./emptyArraySchema.js"; import { evidenceSchema } from "./evidence.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const addressSchema = z .string() @@ -16,7 +17,7 @@ const inputAddressSchema = z .replace(/%[0-9a-fA-F]{2}/gu, (escape) => escape.toUpperCase()); return `${space}0x${BigInt(address.slice(separator)).toString(16)}`; }); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); /** Normalize a hexadecimal offset while preserving its optional address space. */ export const parseCallPathAddress = (input: unknown): string => diff --git a/src/domain/changedBehavior.ts b/src/domain/changedBehavior.ts index 9feb8cdd6..5c6bb08ca 100644 --- a/src/domain/changedBehavior.ts +++ b/src/domain/changedBehavior.ts @@ -9,8 +9,9 @@ import { PROCESS_COMPARISON_DIMENSIONS, processCaptureComparisonSchema, } from "./processCapture.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); /** Input for aggregating existing comparison Evidence. */ export const changedBehaviorInputSchema = z.object({ diff --git a/src/domain/completionLedgerGeneration.ts b/src/domain/completionLedgerGeneration.ts index c6ee2c9e2..5afe3d9d5 100644 --- a/src/domain/completionLedgerGeneration.ts +++ b/src/domain/completionLedgerGeneration.ts @@ -8,9 +8,10 @@ import { evidenceCompletionRecordSchema, type EvidenceCompletionLedger, } from "./evidenceCompletionLedger.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const identifierSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._-]*$/u); -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const identitySchema = z.strictObject({ id: identifierSchema, version: z.string().min(1), @@ -110,7 +111,7 @@ const manifestClaimSchema = reportClaimSchema.omit({ evidence_ids: true, }); const completionManifestObjectSchema = z.strictObject({ - manifest_id: z.string().regex(/^ecm_[a-f0-9]{64}$/u), + manifest_id: prefixedDigestSchema("ecm"), verifier: identitySchema, environment: environmentSchema, skill_digests: z.array(skillDigestSchema).min(1), diff --git a/src/domain/conformancePackage.ts b/src/domain/conformancePackage.ts index e00e1778a..0bf6720ee 100644 --- a/src/domain/conformancePackage.ts +++ b/src/domain/conformancePackage.ts @@ -6,8 +6,9 @@ import { z } from "zod"; import { evidenceEnvelopeSchema } from "./evidence.js"; import { evidenceBundleSchema } from "./evidenceBundle.js"; import { err, ok, type Result } from "./result.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const conformancePackageIdSchema = z.string().regex(/^cp_[a-f0-9]{64}$/u); +const conformancePackageIdSchema = prefixedDigestSchema("cp"); const scenarioIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._-]*$/u); diff --git a/src/domain/digests.test.ts b/src/domain/digests.test.ts new file mode 100644 index 000000000..9593cef6f --- /dev/null +++ b/src/domain/digests.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; + +import { + digestSchema, + isDigest, + isPrefixedDigest, + prefixedDigestSchema, + SHA256_PATTERN, +} from "./digests.js"; + +const DIGEST = "a".repeat(64); +const OTHER_DIGEST = "b".repeat(64); + +describe("digest shapes", () => { + it("accepts a lowercase hex sha256", () => { + expect(digestSchema.parse(DIGEST)).toBe(DIGEST); + expect(isDigest(DIGEST)).toBe(true); + }); + + it.each([ + ["uppercase", DIGEST.toUpperCase()], + ["too short", "a".repeat(63)], + ["too long", "a".repeat(65)], + ["non-hex", "g".repeat(64)], + ["prefixed", `ev_${DIGEST}`], + ])("rejects a %s digest", (_label, value) => { + expect(digestSchema.safeParse(value).success).toBe(false); + expect(isDigest(value)).toBe(false); + }); + + it("rejects non-string values", () => { + for (const value of [null, undefined, 42, {}, []]) + expect(isDigest(value)).toBe(false); + }); + + it("keeps identifier families apart", () => { + expect(prefixedDigestSchema("ev").parse(`ev_${DIGEST}`)).toBe( + `ev_${DIGEST}`, + ); + // The point of the prefix: an identifier from one family must not satisfy + // another, or a mis-pasted id would resolve to the wrong object. + expect(prefixedDigestSchema("ev").safeParse(`art_${DIGEST}`).success).toBe( + false, + ); + expect(prefixedDigestSchema("ev").safeParse(DIGEST).success).toBe(false); + expect(isPrefixedDigest(`ev_${DIGEST}`, "ev")).toBe(true); + expect(isPrefixedDigest(`ev_${DIGEST}`, "art")).toBe(false); + }); + + it("distinguishes nested prefixes from their common ancestor", () => { + // `jag_node_` must not validate as `jag_` even though it starts with it. + expect( + prefixedDigestSchema("jag").safeParse(`jag_node_${DIGEST}`).success, + ).toBe(false); + expect(prefixedDigestSchema("jag_node").parse(`jag_node_${DIGEST}`)).toBe( + `jag_node_${DIGEST}`, + ); + expect(prefixedDigestSchema("jag").parse(`jag_${OTHER_DIGEST}`)).toBe( + `jag_${OTHER_DIGEST}`, + ); + }); + + it("exposes the pattern for callers that cannot use zod", () => { + expect(SHA256_PATTERN.test(DIGEST)).toBe(true); + expect(SHA256_PATTERN.test(DIGEST.toUpperCase())).toBe(false); + }); +}); diff --git a/src/domain/digests.ts b/src/domain/digests.ts new file mode 100644 index 000000000..f46beccc2 --- /dev/null +++ b/src/domain/digests.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +/** + * Canonical digest and identifier shapes. + * + * These were previously a hand-copied regex in ~30 modules: 32 local + * `digestSchema` definitions plus a distinct literal for each of roughly + * thirty `prefix_hash` identifier families. Nothing kept them in step, so a + * change to what counts as a digest had to be found and repeated by hand. + * + * One owner means the shape is stated once, and a new identifier family + * cannot accidentally invent its own spelling of "lowercase sha256". + */ + +/** Lowercase hex SHA-256, as every digest in the system is emitted. */ +export const SHA256_PATTERN = /^[a-f0-9]{64}$/u; + +/** A bare artifact/content digest. */ +export const digestSchema = z.string().regex(SHA256_PATTERN); + +/** + * A namespaced identifier: `_<64 lowercase hex>`. Prefixes keep + * identifiers from different families apart at a glance and make a + * mis-pasted identifier from another family fail validation rather than + * resolve to the wrong object. + */ +export const prefixedDigestSchema = (prefix: string): z.ZodString => + z + .string() + .regex( + new RegExp(`^${prefix}_[a-f0-9]{64}$`, "u"), + `Expected a ${prefix}_ prefixed lowercase sha256 digest`, + ); + +/** True when `value` is a bare digest. */ +export const isDigest = (value: unknown): value is string => + typeof value === "string" && SHA256_PATTERN.test(value); + +/** True when `value` is a digest carrying `prefix`. */ +export const isPrefixedDigest = (value: unknown, prefix: string): boolean => + typeof value === "string" && + prefixedDigestSchema(prefix).safeParse(value).success; diff --git a/src/domain/electronObservation.ts b/src/domain/electronObservation.ts index 442a4c3a5..fb7d398fc 100644 --- a/src/domain/electronObservation.ts +++ b/src/domain/electronObservation.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { browserCompletenessSchema } from "./browserCompleteness.js"; import { browserEndpointSchema } from "./browserObservation.js"; import { webTextArtifactSchema } from "./webContentArtifact.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const approvedElectronInput = { cdp_endpoint: browserEndpointSchema, @@ -102,7 +103,7 @@ export const electronPageInspectionSchema = z.object({ total: z.number().int().min(0), items: z.array( z.object({ - script_key: z.string().regex(/^electron_script_[a-f0-9]{64}$/u), + script_key: prefixedDigestSchema("electron_script"), frame_id: z.string().nullable().default(null), file_path: z.string(), cdp_hash: z.string(), @@ -115,7 +116,7 @@ export const electronPageInspectionSchema = z.object({ }), resources: z.array( z.object({ - resource_key: z.string().regex(/^electron_resource_[a-f0-9]{64}$/u), + resource_key: prefixedDigestSchema("electron_resource"), file_path: z.string(), type: z.string(), mime_type: z.string(), diff --git a/src/domain/evidence.ts b/src/domain/evidence.ts index 648ff3557..016a6101f 100644 --- a/src/domain/evidence.ts +++ b/src/domain/evidence.ts @@ -9,8 +9,9 @@ import { } from "./analysisProfile.js"; import type { BinaryTarget } from "./binaryTarget.js"; import { jsonValueSchema, type JsonValue } from "./jsonValue.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); /** Provider identity schema shared by evidence-bearing persistence formats. */ export const providerSchema = z.object({ id: z.string().min(1), @@ -82,7 +83,7 @@ const executionEnvironmentSchema = z.object({ const evidenceBaseSchema = z .object({ - evidence_id: z.string().regex(/^ev_[a-f0-9]{64}$/u), + evidence_id: prefixedDigestSchema("ev"), subject: subjectSchema.nullable(), provider: providerSchema, predicate_type: z.string().min(1), @@ -95,7 +96,7 @@ const evidenceBaseSchema = z environment: executionEnvironmentSchema.nullable(), limitations: z.array(z.string()), locations: z.array(evidenceLocationSchema), - evidence_links: z.array(z.string().regex(/^ev_[a-f0-9]{64}$/u)), + evidence_links: z.array(prefixedDigestSchema("ev")), }) .strict(); diff --git a/src/domain/evidenceCompletionLedger.ts b/src/domain/evidenceCompletionLedger.ts index 65bcab213..50bec7060 100644 --- a/src/domain/evidenceCompletionLedger.ts +++ b/src/domain/evidenceCompletionLedger.ts @@ -2,8 +2,9 @@ import { createHash } from "node:crypto"; import canonicalize from "canonicalize"; import { z } from "zod"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const claimIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._-]*$/u); /** Terminal outcome recorded for one verifier claim. */ @@ -33,7 +34,7 @@ const completionSummarySchema = z.strictObject({ }); const completionLedgerObjectSchema = z.strictObject({ - ledger_id: z.string().regex(/^ecl_[a-f0-9]{64}$/u), + ledger_id: prefixedDigestSchema("ecl"), records: z.array(evidenceCompletionRecordSchema).min(1), summary: completionSummarySchema, }); diff --git a/src/domain/functionComparisonSchemas.ts b/src/domain/functionComparisonSchemas.ts index dae6ceddd..24b7ed1fb 100644 --- a/src/domain/functionComparisonSchemas.ts +++ b/src/domain/functionComparisonSchemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; import { evidenceSchema } from "./evidence.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const comparisonStatusSchema = z.enum([ "unchanged", @@ -17,8 +19,7 @@ const dimensionNameSchema = z.enum([ "strings_names", "cfg", ]); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); /** Inputs for explicit function-to-function comparison. */ export const functionComparisonInputSchema = z.strictObject({ diff --git a/src/domain/javascriptApplicationAnalysis.ts b/src/domain/javascriptApplicationAnalysis.ts index 68f687d06..baa873e5b 100644 --- a/src/domain/javascriptApplicationAnalysis.ts +++ b/src/domain/javascriptApplicationAnalysis.ts @@ -4,9 +4,10 @@ import { z } from "zod"; import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; import { javaScriptSemanticGraphSchema } from "./javascriptSemanticGraph.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const countSchema = z.number().int().min(0); -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); /** Public target-free request for static JavaScript application analysis. */ export const analyzeJavaScriptApplicationInputSchema = z.strictObject({ @@ -61,7 +62,7 @@ export const javascriptApplicationAnalysisResultSchema = z input_path: z.string().min(1), format: z.enum(["asar", "directory"]), root_artifact_sha256: digestSchema, - inventory_manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u), + inventory_manifest_id: prefixedDigestSchema("agm"), inventory_graph_sha256: digestSchema, graph: javascriptApplicationGraphSchema, summary: electronBoundarySummarySchema, diff --git a/src/domain/javascriptApplicationEvidenceSchemas.ts b/src/domain/javascriptApplicationEvidenceSchemas.ts index 9e330c5bc..2b5cf1566 100644 --- a/src/domain/javascriptApplicationEvidenceSchemas.ts +++ b/src/domain/javascriptApplicationEvidenceSchemas.ts @@ -1,6 +1,7 @@ import { z } from "zod"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const boundedTextSchema = z.string().min(1); const boundedKeySchema = z.string().min(1); const unsafePathSegments = new Set(["", ".", ".."]); @@ -68,7 +69,7 @@ const unavailableFactSchema = z.strictObject({ const availableArtifactSchema = z .strictObject({ available: z.literal(true), - artifact_id: z.string().regex(/^art_[a-f0-9]{64}$/u), + artifact_id: prefixedDigestSchema("art"), sha256: digestSchema, }) .superRefine((artifact, context) => { @@ -265,7 +266,7 @@ const applicationGraphEvidenceBaseSchema = z.strictObject({ extractor: applicationExtractorSchema, coverage: applicationCoverageSchema, limitations: z.array(boundedTextSchema), - evidence_ids: z.array(z.string().regex(/^ev_[a-f0-9]{64}$/u)), + evidence_ids: z.array(prefixedDigestSchema("ev")), }); type EvidenceValue = z.infer; diff --git a/src/domain/javascriptApplicationGraphSchemas.ts b/src/domain/javascriptApplicationGraphSchemas.ts index bc778532e..93a36b54b 100644 --- a/src/domain/javascriptApplicationGraphSchemas.ts +++ b/src/domain/javascriptApplicationGraphSchemas.ts @@ -6,6 +6,7 @@ import { applicationNodeIdentitySchema, } from "./javascriptApplicationEvidenceSchemas.js"; import { jsonValueSchema } from "./jsonValue.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const boundedTextSchema = z.string().min(1); @@ -84,7 +85,7 @@ const applicationNodeObservationInputSchema = z.strictObject({ /** One immutable, evidence-bearing observation attached to an entity. */ export const applicationNodeObservationSchema = applicationNodeObservationInputSchema.extend({ - observation_id: z.string().regex(/^jag_observation_[a-f0-9]{64}$/u), + observation_id: prefixedDigestSchema("jag_observation"), identifier_strategy: z.strictObject({ strategy: z.literal("semantic-content-sha256"), stability: z.literal("observation-exact"), @@ -100,7 +101,7 @@ export const applicationNodeInputSchema = z.strictObject({ /** One stable application entity with one or more bounded observations. */ export const applicationNodeSchema = z.strictObject({ - node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u), + node_id: prefixedDigestSchema("jag_node"), kind: applicationNodeKindSchema, identity: applicationNodeIdentitySchema, observations: z.array(applicationNodeObservationSchema).min(1), @@ -108,8 +109,8 @@ export const applicationNodeSchema = z.strictObject({ /** One directed relationship before its semantic identifier is derived. */ export const applicationEdgeInputSchema = z.strictObject({ - source_node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u), - target_node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u), + source_node_id: prefixedDigestSchema("jag_node"), + target_node_id: prefixedDigestSchema("jag_node"), relation: applicationRelationSchema, properties: applicationPropertiesSchema, evidence: applicationGraphEvidenceSchema, @@ -117,7 +118,7 @@ export const applicationEdgeInputSchema = z.strictObject({ /** One directed, evidence-bearing application relationship. */ export const applicationEdgeSchema = applicationEdgeInputSchema.extend({ - edge_id: z.string().regex(/^jag_edge_[a-f0-9]{64}$/u), + edge_id: prefixedDigestSchema("jag_edge"), identifier_strategy: z.strictObject({ strategy: z.literal("semantic-content-sha256"), stability: z.literal("relationship-exact"), @@ -127,7 +128,7 @@ export const applicationEdgeSchema = applicationEdgeInputSchema.extend({ /** Graph content before its top-level semantic identifier is derived. */ export const javascriptApplicationGraphInputSchema = z.strictObject({ schema: z.literal("JavaScriptApplicationGraph"), - root_node_ids: z.array(z.string().regex(/^jag_node_[a-f0-9]{64}$/u)).min(1), + root_node_ids: z.array(prefixedDigestSchema("jag_node")).min(1), nodes: z.array(applicationNodeSchema).min(1), edges: z.array(applicationEdgeSchema), coverage: applicationCoverageSchema, @@ -137,7 +138,7 @@ export const javascriptApplicationGraphInputSchema = z.strictObject({ /** Strict stored shape for a JavaScript Application Graph. */ export const javascriptApplicationGraphRecordSchema = javascriptApplicationGraphInputSchema.extend({ - graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u), + graph_id: prefixedDigestSchema("jag"), }); /** Immutable entity in a JavaScript Application Graph. */ diff --git a/src/domain/javascriptApplicationVersionComparisonSchemas.ts b/src/domain/javascriptApplicationVersionComparisonSchemas.ts index 340da1b29..1fcd2ac43 100644 --- a/src/domain/javascriptApplicationVersionComparisonSchemas.ts +++ b/src/domain/javascriptApplicationVersionComparisonSchemas.ts @@ -4,9 +4,10 @@ import { emptyArraySchema } from "./emptyArraySchema.js"; import { evidenceSchema } from "./evidence.js"; import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "./javascriptApplicationGraphSchemas.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const nodeIdSchema = z.string().regex(/^jag_node_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); +const nodeIdSchema = prefixedDigestSchema("jag_node"); const textSchema = z.string().min(1); /** Two authenticated application versions and their native observations. */ @@ -93,7 +94,7 @@ const rightAmbiguousMatchSchema = z.strictObject({ }); const comparisonItemContextShape = { - item_id: z.string().regex(/^javc_item_[a-f0-9]{64}$/u), + item_id: prefixedDigestSchema("javc_item"), node_kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS), dimensions: z.array( z.enum([ @@ -158,15 +159,15 @@ const comparisonCoverageSchema = z.strictObject({ /** Tiered module/entity matching plus its complete cross-version change graph. */ export const applicationVersionComparisonResultSchema = z .strictObject({ - comparison_id: z.string().regex(/^javc_[a-f0-9]{64}$/u), + comparison_id: prefixedDigestSchema("javc"), left: z.strictObject({ evidence_id: evidenceIdSchema, - graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u), + graph_id: prefixedDigestSchema("jag"), root_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u), }), right: z.strictObject({ evidence_id: evidenceIdSchema, - graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u), + graph_id: prefixedDigestSchema("jag"), root_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u), }), summary: z.strictObject({ diff --git a/src/domain/javascriptExportInstrumentation.ts b/src/domain/javascriptExportInstrumentation.ts index 9a56f5877..da962159b 100644 --- a/src/domain/javascriptExportInstrumentation.ts +++ b/src/domain/javascriptExportInstrumentation.ts @@ -1,8 +1,7 @@ import { createHash } from "node:crypto"; import { z } from "zod"; - -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); +import { digestSchema } from "./../domain/digests.js"; export const javascriptExportInstrumentationInputSchema = z.strictObject({ artifact_path: z.string().min(1), diff --git a/src/domain/javascriptExportShapeComparisonSchemas.ts b/src/domain/javascriptExportShapeComparisonSchemas.ts index e8857daa0..075fabd67 100644 --- a/src/domain/javascriptExportShapeComparisonSchemas.ts +++ b/src/domain/javascriptExportShapeComparisonSchemas.ts @@ -1,10 +1,11 @@ import { z } from "zod"; import { evidenceSchema } from "./evidence.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const graphIdSchema = z.string().regex(/^jag_[a-f0-9]{64}$/u); -const nodeIdSchema = z.string().regex(/^jag_node_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); +const graphIdSchema = prefixedDigestSchema("jag"); +const nodeIdSchema = prefixedDigestSchema("jag_node"); const textSchema = z.string().min(1); const selectorTextSchema = textSchema; const semanticPrimitiveSchema = z.union([ @@ -229,7 +230,7 @@ const discriminantSchema = z.strictObject({ }); const comparisonChangeSchema = z.strictObject({ - change_id: z.string().regex(/^jesc_change_[a-f0-9]{64}$/u), + change_id: prefixedDigestSchema("jesc_change"), status: z.enum(["added", "removed", "changed", "unknown"]), path: jsonPointerSchema, discriminant: discriminantSchema.nullable(), @@ -243,7 +244,7 @@ const comparisonChangeSchema = z.strictObject({ /** Static export-return comparison with explicit unknown semantics. */ export const javaScriptExportShapeComparisonResultSchema = z.strictObject({ - comparison_id: z.string().regex(/^jesc_[a-f0-9]{64}$/u), + comparison_id: prefixedDigestSchema("jesc"), left: selectorResultSchema, right: selectorResultSchema, summary: z.strictObject({ diff --git a/src/domain/javascriptFeatureTraceSchemas.ts b/src/domain/javascriptFeatureTraceSchemas.ts index 5b2cfdd8f..2a992bb9b 100644 --- a/src/domain/javascriptFeatureTraceSchemas.ts +++ b/src/domain/javascriptFeatureTraceSchemas.ts @@ -2,11 +2,12 @@ import { z } from "zod"; import { evidenceSchema, providerSchema } from "./evidence.js"; import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const nodeIdSchema = z.string().regex(/^jag_node_[a-f0-9]{64}$/u); -const edgeIdSchema = z.string().regex(/^jag_edge_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); +const nodeIdSchema = prefixedDigestSchema("jag_node"); +const edgeIdSchema = prefixedDigestSchema("jag_edge"); const boundedTextSchema = z.string().min(1); /** Literal starting point for one application feature trace. */ @@ -51,7 +52,7 @@ const seedMatchSchema = z.strictObject({ }); const tracePathSchema = z.strictObject({ - path_id: z.string().regex(/^jatp_[a-f0-9]{64}$/u), + path_id: prefixedDigestSchema("jatp"), start_node_id: nodeIdSchema, end_node_id: nodeIdSchema, end_kind: z.string().min(1), @@ -83,9 +84,9 @@ const nativeHandoffSchema = z.strictObject({ /** Evidence-preserving complete reachable subgraph and native-analysis handoffs. */ export const applicationFeatureTraceResultSchema = z.strictObject({ - trace_id: z.string().regex(/^jatr_[a-f0-9]{64}$/u), + trace_id: prefixedDigestSchema("jatr"), source_evidence_id: evidenceIdSchema, - source_graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u), + source_graph_id: prefixedDigestSchema("jag"), seed: applicationFeatureSeedSchema, direction: z.enum(["outgoing", "incoming", "both"]), seed_matches: z.array(seedMatchSchema), diff --git a/src/domain/javascriptReplay.ts b/src/domain/javascriptReplay.ts index afe9e6e90..6480c248a 100644 --- a/src/domain/javascriptReplay.ts +++ b/src/domain/javascriptReplay.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import { evidenceEnvelopeSchema } from "./evidence.js"; +import { digestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const absolutePathSchema = z.string().startsWith("/"); const aliasSchema = z .string() diff --git a/src/domain/javascriptRuntimeObservation.ts b/src/domain/javascriptRuntimeObservation.ts index 4d27dd561..cbed67155 100644 --- a/src/domain/javascriptRuntimeObservation.ts +++ b/src/domain/javascriptRuntimeObservation.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { browserEndpointSchema } from "./browserObservation.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const observationTextSchema = z.string().min(1); @@ -102,7 +103,7 @@ export type JavaScriptRuntimeTargetList = z.infer< >; const javascriptRuntimeScriptSchema = z.strictObject({ - script_key: z.string().regex(/^v8_script_[a-f0-9]{64}$/u), + script_key: prefixedDigestSchema("v8_script"), location: javascriptRuntimeLocationSchema, execution_context_key: z.string().nullable(), cdp_hash: z.string().nullable(), diff --git a/src/domain/javascriptRuntimeReconciliationSchemas.ts b/src/domain/javascriptRuntimeReconciliationSchemas.ts index 5961423af..ea37badfe 100644 --- a/src/domain/javascriptRuntimeReconciliationSchemas.ts +++ b/src/domain/javascriptRuntimeReconciliationSchemas.ts @@ -8,10 +8,11 @@ import { compareCodePoints, javascriptApplicationGraphSchema, } from "./javascriptApplicationGraph.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const nodeIdSchema = z.string().regex(/^jag_node_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); +const nodeIdSchema = prefixedDigestSchema("jag_node"); const boundedTextSchema = z.string().min(1); const safePrefixSchema = z .string() @@ -137,10 +138,10 @@ export const reconcileJavaScriptRuntimeInputSchema = z }); const layerSummarySchema = z.strictObject({ - layer_id: z.string().regex(/^jrl_[a-f0-9]{64}$/u), + layer_id: prefixedDigestSchema("jrl"), role: z.enum(["application", "cache", "assets"]), evidence_id: evidenceIdSchema, - graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u), + graph_id: prefixedDigestSchema("jag"), root_artifact_sha256: digestSchema, input_path: z.string().min(1), format: z.enum(["asar", "directory"]), @@ -178,7 +179,7 @@ const reconciliationReasonSchema = z.enum([ ]); const reconciliationItemShape = { - reconciliation_id: z.string().regex(/^jrr_item_[a-f0-9]{64}$/u), + reconciliation_id: prefixedDigestSchema("jrr_item"), entity_kind: z.enum(["target", "frame", "script", "worker"]), runtime_evidence_id: evidenceIdSchema, runtime_node_id: nodeIdSchema, @@ -196,7 +197,7 @@ const reconciliationItemShape = { candidate_static_count: z.number().int().min(0), candidate_static_nodes: z.array( z.strictObject({ - static_layer_id: z.string().regex(/^jrl_[a-f0-9]{64}$/u), + static_layer_id: prefixedDigestSchema("jrl"), static_node_id: nodeIdSchema, }), ), @@ -204,7 +205,7 @@ const reconciliationItemShape = { const reconciliationItemSchema = z.union([ z.strictObject({ ...reconciliationItemShape, - static_layer_id: z.string().regex(/^jrl_[a-f0-9]{64}$/u), + static_layer_id: prefixedDigestSchema("jrl"), static_node_id: nodeIdSchema, status: z.literal("matched"), }), @@ -217,7 +218,7 @@ const reconciliationItemSchema = z.union([ ]); const staticLoadStateSchema = z.strictObject({ - static_layer_id: z.string().regex(/^jrl_[a-f0-9]{64}$/u), + static_layer_id: prefixedDigestSchema("jrl"), static_node_id: nodeIdSchema, kind: z.enum(["javascript-asset", "javascript-chunk", "javascript-module"]), status: z.enum([ @@ -238,7 +239,7 @@ const staticLoadStateSchema = z.strictObject({ /** Deterministic static/passive-runtime reconciliation with a combined JAG. */ export const javascriptRuntimeReconciliationResultSchema = z.strictObject({ - reconciliation_id: z.string().regex(/^jrr_[a-f0-9]{64}$/u), + reconciliation_id: prefixedDigestSchema("jrr"), static_layers: z.array(layerSummarySchema).min(1), runtime_captures: z.array(captureSummarySchema).min(1), graph: javascriptApplicationGraphSchema, diff --git a/src/domain/javascriptSemanticGraphSchemas.ts b/src/domain/javascriptSemanticGraphSchemas.ts index b92111054..674f74109 100644 --- a/src/domain/javascriptSemanticGraphSchemas.ts +++ b/src/domain/javascriptSemanticGraphSchemas.ts @@ -2,10 +2,11 @@ import { z } from "zod"; import { applicationGraphEvidenceSchema } from "./javascriptApplicationEvidenceSchemas.js"; import { jsonValueSchema } from "./jsonValue.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const boundedTextSchema = z.string().min(1); -const semanticNodeIdSchema = z.string().regex(/^jsrg_node_[a-f0-9]{64}$/u); +const semanticNodeIdSchema = prefixedDigestSchema("jsrg_node"); const semanticRelationIdSchema = z .string() .regex(/^jsrg_relation_[a-f0-9]{64}$/u); @@ -203,7 +204,7 @@ export const javaScriptSemanticNodeInputSchema = z.strictObject({ kind: z.enum(JAVASCRIPT_SEMANTIC_NODE_KINDS), identity: semanticNodeIdentitySchema, function_node_id: semanticNodeIdSchema.nullable(), - application_node_ids: z.array(z.string().regex(/^jag_node_[a-f0-9]{64}$/u)), + application_node_ids: z.array(prefixedDigestSchema("jag_node")), label: z.string().min(1).nullable(), properties: semanticPropertiesSchema, evidence: applicationGraphEvidenceSchema, @@ -300,7 +301,7 @@ export const javaScriptSemanticFingerprintInputSchema = z.strictObject({ /** One canonical function fingerprint record. */ export const javaScriptSemanticFingerprintSchema = javaScriptSemanticFingerprintInputSchema.extend({ - fingerprint_id: z.string().regex(/^jsrg_fingerprint_[a-f0-9]{64}$/u), + fingerprint_id: prefixedDigestSchema("jsrg_fingerprint"), fingerprint_sha256: digestSchema, }); @@ -331,7 +332,7 @@ const graphCoverageSchema = z.strictObject({ export const javaScriptSemanticGraphInputSchema = z.strictObject({ schema: z.literal("JavaScriptSemanticRelationGraph"), root_artifact_sha256: digestSchema, - application_graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u), + application_graph_id: prefixedDigestSchema("jag"), root_node_ids: z.array(semanticNodeIdSchema).min(1), nodes: z.array(javaScriptSemanticNodeSchema).min(1), relations: z.array(javaScriptSemanticRelationSchema), @@ -344,7 +345,7 @@ export const javaScriptSemanticGraphInputSchema = z.strictObject({ /** Stored JavaScript Semantic Relation Graph. */ export const javaScriptSemanticGraphRecordSchema = javaScriptSemanticGraphInputSchema.extend({ - graph_id: z.string().regex(/^jsrg_[a-f0-9]{64}$/u), + graph_id: prefixedDigestSchema("jsrg"), }); /** Canonical semantic node. */ diff --git a/src/domain/javascriptSemanticQuerySchemas.ts b/src/domain/javascriptSemanticQuerySchemas.ts index 4e62cf3c5..4c7dfb429 100644 --- a/src/domain/javascriptSemanticQuerySchemas.ts +++ b/src/domain/javascriptSemanticQuerySchemas.ts @@ -8,9 +8,10 @@ import { javaScriptSemanticUnknownSchema, } from "./javascriptSemanticGraphSchemas.js"; import { jsonValueSchema } from "./jsonValue.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const semanticNodeIdSchema = z.string().regex(/^jsrg_node_[a-f0-9]{64}$/u); +const semanticNodeIdSchema = prefixedDigestSchema("jsrg_node"); const literalSeedSchema = z.strictObject({ kind: z.literal("literal"), @@ -32,7 +33,7 @@ export const javaScriptSemanticQuerySeedSchema = z.discriminatedUnion("kind", [ }), z.strictObject({ kind: z.literal("application-node"), - node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u), + node_id: prefixedDigestSchema("jag_node"), }), literalSeedSchema, z.strictObject({ @@ -87,8 +88,8 @@ export const javaScriptSemanticQueryInputSchema = z.strictObject({ /** Deterministic semantic trace result. */ export const javaScriptSemanticQueryResultSchema = z.strictObject({ - query_id: z.string().regex(/^jsrq_[a-f0-9]{64}$/u), - source_graph_id: z.string().regex(/^jsrg_[a-f0-9]{64}$/u), + query_id: prefixedDigestSchema("jsrq"), + source_graph_id: prefixedDigestSchema("jsrg"), seed: javaScriptSemanticQuerySeedSchema, direction: javaScriptSemanticQueryInputSchema.shape.direction, status: z.enum(["found", "no-match", "ambiguous", "partial", "unsupported"]), diff --git a/src/domain/javascriptSemanticTraceSchemas.ts b/src/domain/javascriptSemanticTraceSchemas.ts index 20a94f935..147c7362b 100644 --- a/src/domain/javascriptSemanticTraceSchemas.ts +++ b/src/domain/javascriptSemanticTraceSchemas.ts @@ -5,8 +5,9 @@ import { javaScriptSemanticQueryInputSchema, javaScriptSemanticQueryResultSchema, } from "./javascriptSemanticQuerySchemas.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); /** Authenticated application Evidence plus one bounded semantic query. */ export const traceJavaScriptSemanticsInputSchema = z.strictObject({ diff --git a/src/domain/managedApplicationGraph.ts b/src/domain/managedApplicationGraph.ts index 6b63538c5..f584dca63 100644 --- a/src/domain/managedApplicationGraph.ts +++ b/src/domain/managedApplicationGraph.ts @@ -30,9 +30,10 @@ import { addBoundaryNodes, addMemberNodes, } from "./managedApplicationGraphNodes.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const textSchema = z.string().min(1); /** Authenticated managed Evidence records projected into the application graph. */ @@ -57,7 +58,7 @@ export const projectManagedApplicationGraphInputSchema = z /** Managed-code projection result containing a validated application graph. */ export const managedApplicationGraphResultSchema = z.strictObject({ - projection_id: z.string().regex(/^magp_[a-f0-9]{64}$/u), + projection_id: prefixedDigestSchema("magp"), root_artifact_sha256: digestSchema, source_evidence: z.strictObject({ managed_artifact_evidence_id: evidenceIdSchema.nullable(), diff --git a/src/domain/managedArtifact.ts b/src/domain/managedArtifact.ts index 01298fd84..a29225f22 100644 --- a/src/domain/managedArtifact.ts +++ b/src/domain/managedArtifact.ts @@ -1,6 +1,6 @@ import { z } from "zod"; +import { digestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u); const offsetSchema = z.number().int().min(0); diff --git a/src/domain/managedMemberComparison.ts b/src/domain/managedMemberComparison.ts index b554d9594..2baa1ff28 100644 --- a/src/domain/managedMemberComparison.ts +++ b/src/domain/managedMemberComparison.ts @@ -18,9 +18,10 @@ import { buildMethodItems, } from "./managedMemberComparisonItems.js"; import { keyMembers, sha256 } from "./managedMemberComparisonMatch.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u); const boundedTextSchema = z.string().min(1); @@ -120,7 +121,7 @@ const comparisonItemContextShape = { const methodComparisonItemSchema = comparisonItemSchema( { ...comparisonItemContextShape, - item_id: z.string().regex(/^mmc_method_[a-f0-9]{64}$/u), + item_id: prefixedDigestSchema("mmc_method"), dimensions: z.array( z.enum([ "signature", @@ -139,14 +140,14 @@ const methodComparisonItemSchema = comparisonItemSchema( const fieldComparisonItemSchema = comparisonItemSchema( { ...comparisonItemContextShape, - item_id: z.string().regex(/^mmc_field_[a-f0-9]{64}$/u), + item_id: prefixedDigestSchema("mmc_field"), }, memberIdentitySchema.omit({ normalized_il_sha256: true }), ); /** Obfuscation-resistant, execution-free managed member comparison. */ export const managedMemberComparisonResultSchema = z.strictObject({ - comparison_id: z.string().regex(/^mmc_[a-f0-9]{64}$/u), + comparison_id: prefixedDigestSchema("mmc"), algorithm: z.strictObject({ name: z.literal("rea-managed-member-comparison"), token_identity: z.literal("build-local"), diff --git a/src/domain/managedNativeVerificationSchemas.ts b/src/domain/managedNativeVerificationSchemas.ts index fe38e6fd8..3bf680182 100644 --- a/src/domain/managedNativeVerificationSchemas.ts +++ b/src/domain/managedNativeVerificationSchemas.ts @@ -3,9 +3,10 @@ import { z } from "zod"; import { emptyArraySchema } from "./emptyArraySchema.js"; import { evidenceSchema } from "./evidence.js"; import { cliMetadataGuidSchema } from "./managedArtifact.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u); const textSchema = z.string().min(1); @@ -22,7 +23,7 @@ const nativeSymbolSchema = z.strictObject({ export type NativeSymbol = z.infer; const pinvokeVerificationContextShape = { - item_id: z.string().regex(/^mnv_pinvoke_[a-f0-9]{64}$/u), + item_id: prefixedDigestSchema("mnv_pinvoke"), managed: z.strictObject({ token: tokenSchema, member_token: tokenSchema.nullable(), @@ -126,7 +127,7 @@ const verificationCoverageSchema = z.strictObject({ /** Provider-neutral managed/native verification result. */ export const managedNativeVerificationResultSchema = z .strictObject({ - verification_id: z.string().regex(/^mnv_[a-f0-9]{64}$/u), + verification_id: prefixedDigestSchema("mnv"), algorithm: z.strictObject({ name: z.literal("rea-managed-native-verification"), token_identity: z.literal("build-local"), diff --git a/src/domain/managedReconstruction.ts b/src/domain/managedReconstruction.ts index b21df6c75..b056ba467 100644 --- a/src/domain/managedReconstruction.ts +++ b/src/domain/managedReconstruction.ts @@ -10,9 +10,10 @@ import { type ManagedMemberInspection, } from "./managedArtifact.js"; import type { JsonValue } from "./jsonValue.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u); const textSchema = z.string().min(1); @@ -78,7 +79,7 @@ const importedMethodSchema = z.strictObject({ }); export const managedReconstructionImportResultSchema = z.strictObject({ - reconstruction_id: z.string().regex(/^mre_[a-f0-9]{64}$/u), + reconstruction_id: prefixedDigestSchema("mre"), phase: z.literal("reconstruction-import"), executed: z.literal(false), static_observation: z.strictObject({ diff --git a/src/domain/managedRuntimeCorrelation.ts b/src/domain/managedRuntimeCorrelation.ts index c5661c20e..001c89fa7 100644 --- a/src/domain/managedRuntimeCorrelation.ts +++ b/src/domain/managedRuntimeCorrelation.ts @@ -9,9 +9,10 @@ import { managedMemberInspectionSchema, } from "./managedArtifact.js"; import type { JsonValue } from "./jsonValue.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u); const boundedTextSchema = z.string().min(1); @@ -56,7 +57,7 @@ export const managedRuntimeCorrelationInputSchema = z.strictObject({ }); export const managedRuntimeCorrelationResultSchema = z.strictObject({ - correlation_id: z.string().regex(/^mrc_[a-f0-9]{64}$/u), + correlation_id: prefixedDigestSchema("mrc"), phase: z.literal("admission-plan"), executed: z.literal(false), authority_model: z.strictObject({ diff --git a/src/domain/nodeRuntimeCharacterization.ts b/src/domain/nodeRuntimeCharacterization.ts index ca72eb227..9d8e69edf 100644 --- a/src/domain/nodeRuntimeCharacterization.ts +++ b/src/domain/nodeRuntimeCharacterization.ts @@ -11,8 +11,8 @@ import { } from "./javascriptExportInstrumentation.js"; import { runtimeCharacterizationPlanSchema } from "./runtimeCharacterization.js"; import { evidenceRecordSchema } from "./evidence.js"; +import { digestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const nodeCharacterizationExpectedEffectSchema = z.enum([ "pure", "observation-only", diff --git a/src/domain/reconstructionCoverage.ts b/src/domain/reconstructionCoverage.ts index 53878e676..d2320f38b 100644 --- a/src/domain/reconstructionCoverage.ts +++ b/src/domain/reconstructionCoverage.ts @@ -12,9 +12,10 @@ import { recommendedReconstructionProbes, type ReconstructionEvaluationContext, } from "./reconstructionCoverageEvaluation.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const stableIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._:/-]{0,199}$/u); const boundedTextSchema = z.string().trim().min(1); diff --git a/src/domain/reconstructionObligationLedgerSchemas.ts b/src/domain/reconstructionObligationLedgerSchemas.ts index a55f4623d..18a9d3aba 100644 --- a/src/domain/reconstructionObligationLedgerSchemas.ts +++ b/src/domain/reconstructionObligationLedgerSchemas.ts @@ -1,9 +1,10 @@ import { z } from "zod"; import { evidenceBundleSchema } from "./evidenceBundle.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const stableIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._:/-]*$/u); const textSchema = z.string().trim().min(1); @@ -235,7 +236,7 @@ const countBySchema = ( export const reconstructionObligationLedgerSchema = z.strictObject({ schema: z.literal("ReconstructionObligationLedger"), - ledger_id: z.string().regex(/^rol_[a-f0-9]{64}$/u), + ledger_id: prefixedDigestSchema("rol"), closure_digest: digestSchema, status: z.enum(["ready", "open", "failed", "unknown"]), coverage: z.strictObject({ diff --git a/src/domain/reconstructionReadinessSchemas.ts b/src/domain/reconstructionReadinessSchemas.ts index 3dca5a359..84be830b7 100644 --- a/src/domain/reconstructionReadinessSchemas.ts +++ b/src/domain/reconstructionReadinessSchemas.ts @@ -2,9 +2,10 @@ import { z } from "zod"; import { evidenceBundleSchema } from "./evidenceBundle.js"; import { reconstructionObligationLedgerSchema } from "./reconstructionObligationLedgerSchemas.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const stableIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._:/-]*$/u); const boundedTextSchema = z.string().trim().min(1); @@ -244,7 +245,7 @@ const findingSchema = z.strictObject({ export const reconstructionReadinessReportSchema = z.strictObject({ schema: z.literal("ReconstructionReadinessReport"), - report_id: z.string().regex(/^rr_[a-f0-9]{64}$/u), + report_id: prefixedDigestSchema("rr"), source_digest: digestSchema, report_digest: digestSchema, status: readinessStatusSchema, diff --git a/src/domain/reconstructionVerification.ts b/src/domain/reconstructionVerification.ts index 7f12ba831..47d44d259 100644 --- a/src/domain/reconstructionVerification.ts +++ b/src/domain/reconstructionVerification.ts @@ -31,12 +31,13 @@ import { } from "./reconstructionUnknowns.js"; import type { ResidualUnknown } from "./residualUnknown.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); export { reconstructionVerificationInputSchema, reconstructionVerificationResultSchema, } from "./reconstructionVerificationSchemas.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; export type { ReconstructionVerificationResult } from "./reconstructionVerificationSchemas.js"; const providers = { diff --git a/src/domain/reconstructionVerificationSchemas.ts b/src/domain/reconstructionVerificationSchemas.ts index b050182a9..f79d673e0 100644 --- a/src/domain/reconstructionVerificationSchemas.ts +++ b/src/domain/reconstructionVerificationSchemas.ts @@ -1,8 +1,9 @@ import { z } from "zod"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const unknownIdSchema = z.string().regex(/^unk_[a-f0-9]{64}$/u); -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); +const unknownIdSchema = prefixedDigestSchema("unk"); const claimIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._-]*$/u); const titleSchema = z.string().trim().min(1); const verificationStatusSchema = z.enum(["pass", "fail", "unknown"]); diff --git a/src/domain/referenceSourceGraph.ts b/src/domain/referenceSourceGraph.ts index ea8d60efe..95e0ed0d2 100644 --- a/src/domain/referenceSourceGraph.ts +++ b/src/domain/referenceSourceGraph.ts @@ -5,7 +5,8 @@ import canonicalize from "canonicalize"; import { z } from "zod"; import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; const boundedTextSchema = z.string().min(1); const relativePathSchema = z .string() @@ -200,7 +201,7 @@ const historicalSourceGraphInputSchema = z.strictObject({ const historicalSourceManifestBaseSchema = z.strictObject({ schema: z.literal("HistoricalSourceManifest"), authority: z.literal("historical-reference"), - manifest_id: z.string().regex(/^hsm_[a-f0-9]{64}$/u), + manifest_id: prefixedDigestSchema("hsm"), graph_sha256: digestSchema, root_sha256: digestSchema, inventory_state: z.enum(["complete", "partial", "unknown"]), diff --git a/src/domain/residualUnknown.ts b/src/domain/residualUnknown.ts index db02159ea..396f72dcb 100644 --- a/src/domain/residualUnknown.ts +++ b/src/domain/residualUnknown.ts @@ -2,9 +2,10 @@ import { createHash } from "node:crypto"; import canonicalize from "canonicalize"; import { z } from "zod"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const unknownIdSchema = z.string().regex(/^unk_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); +const unknownIdSchema = prefixedDigestSchema("unk"); const text = z.string().trim().min(1); const environmentRequirementSchema = z.object({ id: z.string().min(1).nullable(), diff --git a/src/domain/runtimeCharacterization.ts b/src/domain/runtimeCharacterization.ts index 13bba12ad..666a9fc3e 100644 --- a/src/domain/runtimeCharacterization.ts +++ b/src/domain/runtimeCharacterization.ts @@ -2,8 +2,8 @@ import { createHash } from "node:crypto"; import canonicalize from "canonicalize"; import { z } from "zod"; +import { digestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const stableIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._:/-]*$/u); const runtimeIdentitySchema = z.strictObject({ diff --git a/src/domain/runtimeIdentification.ts b/src/domain/runtimeIdentification.ts index 787035ecf..f67dd7ef4 100644 --- a/src/domain/runtimeIdentification.ts +++ b/src/domain/runtimeIdentification.ts @@ -5,9 +5,10 @@ import { z } from "zod"; import { parseArtifactInventoryEvidence } from "./artifactInventoryEvidence.js"; import { evidenceSchema } from "./evidence.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const runtimeFamilySchema = z.enum([ "android", "apple", @@ -18,7 +19,7 @@ const runtimeFamilySchema = z.enum([ ]); const observationSchema = z.strictObject({ path: z.string().min(1), - artifact_id: z.string().regex(/^art_[a-f0-9]{64}$/u), + artifact_id: prefixedDigestSchema("art"), sha256: digestSchema, format: z.string().min(1), }); @@ -54,7 +55,7 @@ const runtimeIdentificationInputSchema = z.strictObject({ /** Provider-neutral runtime identification with explicit tooling availability. */ export const runtimeIdentificationResultSchema = z.strictObject({ - identification_id: z.string().regex(/^rid_[a-f0-9]{64}$/u), + identification_id: prefixedDigestSchema("rid"), root_sha256: digestSchema, root_format: z.string().min(1), source_evidence_ids: z.array(evidenceIdSchema).min(1), diff --git a/src/domain/sourceToBundleComparisonSchemas.ts b/src/domain/sourceToBundleComparisonSchemas.ts index 98351e41b..82b194cbc 100644 --- a/src/domain/sourceToBundleComparisonSchemas.ts +++ b/src/domain/sourceToBundleComparisonSchemas.ts @@ -3,10 +3,11 @@ import { z } from "zod"; import { evidenceSchema } from "./evidence.js"; import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "./javascriptApplicationGraphSchemas.js"; import { historicalSourceGraphSchema } from "./referenceSourceGraph.js"; +import { digestSchema } from "./../domain/digests.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); -const nodeIdSchema = z.string().regex(/^jag_node_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); +const nodeIdSchema = prefixedDigestSchema("jag_node"); const textSchema = z.string().min(1); export const SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS = [ @@ -49,7 +50,7 @@ const sourceToBundleCandidateSchema = z.strictObject({ }); const sourceToBundleItemSchema = z.strictObject({ - mapping_id: z.string().regex(/^stbc_item_[a-f0-9]{64}$/u), + mapping_id: prefixedDigestSchema("stbc_item"), source_path: textSchema, source_sha256: digestSchema.nullable(), source_language: textSchema.nullable(), @@ -70,14 +71,14 @@ const sourceToBundleItemSchema = z.strictObject({ /** Deterministic, evidence-bearing historical-source to shipped-bundle comparison. */ export const sourceToBundleComparisonResultSchema = z.strictObject({ - comparison_id: z.string().regex(/^stbc_[a-f0-9]{64}$/u), + comparison_id: prefixedDigestSchema("stbc"), reference: z.strictObject({ root_sha256: digestSchema, inventory_state: z.enum(["complete", "partial", "unknown"]), }), application: z.strictObject({ evidence_id: evidenceIdSchema, - graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u), + graph_id: prefixedDigestSchema("jag"), root_artifact_sha256: digestSchema, }), scoring: z.strictObject({ diff --git a/src/domain/sourceToBundleSignals.ts b/src/domain/sourceToBundleSignals.ts index a182acf52..cdb6d5d4b 100644 --- a/src/domain/sourceToBundleSignals.ts +++ b/src/domain/sourceToBundleSignals.ts @@ -7,6 +7,7 @@ import { type SourceToBundleCandidate, type SourceToBundleSignal, } from "./sourceToBundleComparisonSchemas.js"; +import { isDigest } from "./digests.js"; type SourceFile = Extract< HistoricalSourceGraph["entries"][number], @@ -310,7 +311,5 @@ const addIndexValue = ( index.set(key, values); }; -const isDigest = (value: string): boolean => /^[a-f0-9]{64}$/u.test(value); - const compareText = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0; diff --git a/src/domain/staticRuntimeCorrelation.ts b/src/domain/staticRuntimeCorrelation.ts index 507e75fef..3b7f390b7 100644 --- a/src/domain/staticRuntimeCorrelation.ts +++ b/src/domain/staticRuntimeCorrelation.ts @@ -11,8 +11,9 @@ import { comparisonStatusSchema, processCaptureComparisonSchema, } from "./processCapture.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; -const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u); +const evidenceIdSchema = prefixedDigestSchema("ev"); const functionDimensionSchema = z.enum([ "identity", "pseudocode", @@ -92,7 +93,7 @@ export const staticRuntimeCorrelationInputSchema = z .strict(); const correlationItemSchema = z.object({ - correlation_id: z.string().regex(/^cor_[a-f0-9]{64}$/u), + correlation_id: prefixedDigestSchema("cor"), static: z.object({ comparison_evidence_id: evidenceIdSchema, selector: staticSelectorSchema, diff --git a/src/domain/webMcpDiscovery.ts b/src/domain/webMcpDiscovery.ts index bdd1d7f30..d48413e22 100644 --- a/src/domain/webMcpDiscovery.ts +++ b/src/domain/webMcpDiscovery.ts @@ -6,6 +6,7 @@ import { browserEndpointSchema, } from "./browserObservation.js"; import { jsonShapeSchema } from "./jsonShape.js"; +import { prefixedDigestSchema } from "./../domain/digests.js"; /** Input for passive discovery of page-registered WebMCP tools. */ export const discoverWebMcpToolsInputSchema = z.strictObject({ @@ -27,7 +28,7 @@ const browserVersionSchema = z.object({ }); const webMcpToolSchema = z.object({ - tool_key: z.string().regex(/^webmcp_[a-f0-9]{64}$/u), + tool_key: prefixedDigestSchema("webmcp"), name: z.string(), description: z.string(), frame_id: z.string(),