diff --git a/.claude/hooks/setup-security-tools/index.mts b/.claude/hooks/setup-security-tools/index.mts index 725a99fe..f68ed596 100644 --- a/.claude/hooks/setup-security-tools/index.mts +++ b/.claude/hooks/setup-security-tools/index.mts @@ -17,6 +17,8 @@ import process from 'node:process' import { fileURLToPath } from 'node:url' import { PackageURL } from '@socketregistry/packageurl-js' +import { Type } from '@sinclair/typebox' + import { whichSync } from '@socketsecurity/lib/bin' import { downloadBinary } from '@socketsecurity/lib/dlx/binary' import { downloadPackage } from '@socketsecurity/lib/dlx/package' @@ -25,37 +27,37 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger' import { normalizePath } from '@socketsecurity/lib/paths/normalize' import { getSocketHomePath } from '@socketsecurity/lib/paths/socket' import { spawn } from '@socketsecurity/lib/spawn' -import { z } from 'zod' +import { parseSchema } from '@socketsecurity/lib/validation/validate-schema' const logger = getDefaultLogger() // ── Tool config loaded from external-tools.json (self-contained) ── -const checksumEntrySchema = z.object({ - asset: z.string(), - sha256: z.string(), +const checksumEntrySchema = Type.Object({ + asset: Type.String(), + sha256: Type.String(), }) -const toolSchema = z.object({ - description: z.string().optional(), - version: z.string().optional(), - purl: z.string().optional(), - integrity: z.string().optional(), - repository: z.string().optional(), - release: z.string().optional(), - checksums: z.record(z.string(), checksumEntrySchema).optional(), - ecosystems: z.array(z.string()).optional(), +const toolSchema = Type.Object({ + description: Type.Optional(Type.String()), + version: Type.Optional(Type.String()), + purl: Type.Optional(Type.String()), + integrity: Type.Optional(Type.String()), + repository: Type.Optional(Type.String()), + release: Type.Optional(Type.String()), + checksums: Type.Optional(Type.Record(Type.String(), checksumEntrySchema)), + ecosystems: Type.Optional(Type.Array(Type.String())), }) -const configSchema = z.object({ - description: z.string().optional(), - tools: z.record(z.string(), toolSchema), +const configSchema = Type.Object({ + description: Type.Optional(Type.String()), + tools: Type.Record(Type.String(), toolSchema), }) const __dirname = path.dirname(fileURLToPath(import.meta.url)) const configPath = path.join(__dirname, 'external-tools.json') const rawConfig = JSON.parse(readFileSync(configPath, 'utf8')) -const config = configSchema.parse(rawConfig) +const config = parseSchema(configSchema, rawConfig) const AGENTSHIELD = config.tools['agentshield']! const ZIZMOR = config.tools['zizmor']! diff --git a/.claude/hooks/setup-security-tools/package.json b/.claude/hooks/setup-security-tools/package.json index 996f77ce..26a4cf9e 100644 --- a/.claude/hooks/setup-security-tools/package.json +++ b/.claude/hooks/setup-security-tools/package.json @@ -4,7 +4,7 @@ "type": "module", "main": "./index.mts", "dependencies": { - "@socketsecurity/lib": "catalog:", - "zod": "4.1.12" + "@sinclair/typebox": "catalog:", + "@socketsecurity/lib": "catalog:" } } diff --git a/package.json b/package.json index efcfb3d7..b0938217 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@mdn/browser-compat-data": "catalog:", "@npmcli/package-json": "catalog:", "@npmcli/promise-spawn": "catalog:", + "@sinclair/typebox": "catalog:", "@socketregistry/is-unicode-supported": "workspace:*", "@socketregistry/packageurl-js": "catalog:", "@socketsecurity/lib": "catalog:", @@ -129,8 +130,7 @@ "vitest": "catalog:", "which": "catalog:", "yargs-parser": "catalog:", - "yoctocolors-cjs": "catalog:", - "zod": "catalog:" + "yoctocolors-cjs": "catalog:" }, "typeCoverage": { "cache": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 236b5eb0..d4f108f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,9 @@ catalogs: '@npmcli/promise-spawn': specifier: 8.0.3 version: 8.0.3 + '@sinclair/typebox': + specifier: 0.34.49 + version: 0.34.49 '@socketregistry/packageurl-js': specifier: 1.4.2 version: 1.4.2 @@ -391,9 +394,6 @@ catalogs: yoctocolors-cjs: specifier: 2.1.3 version: 2.1.3 - zod: - specifier: 4.3.6 - version: 4.3.6 overrides: '@types/node': 24.9.2 @@ -486,6 +486,9 @@ importers: '@npmcli/promise-spawn': specifier: 'catalog:' version: 8.0.3 + '@sinclair/typebox': + specifier: 'catalog:' + version: 0.34.49 '@socketregistry/is-unicode-supported': specifier: workspace:* version: link:packages/npm/is-unicode-supported @@ -678,9 +681,6 @@ importers: yoctocolors-cjs: specifier: 'catalog:' version: 2.1.3 - zod: - specifier: 'catalog:' - version: 4.3.6 .claude/hooks/check-new-deps: dependencies: @@ -700,12 +700,12 @@ importers: .claude/hooks/setup-security-tools: dependencies: + '@sinclair/typebox': + specifier: 'catalog:' + version: 0.34.49 '@socketsecurity/lib': specifier: 'catalog:' version: 5.20.1(typescript@5.9.2) - zod: - specifier: 4.1.12 - version: 4.1.12 packages/npm/abab: {} @@ -2274,6 +2274,9 @@ packages: resolution: {integrity: sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==} engines: {node: ^20.17.0 || >=22.9.0} + '@sinclair/typebox@0.34.49': + resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + '@sindresorhus/chunkify@2.0.0': resolution: {integrity: sha512-srajPSoMTC98FETCJIeXJhJqB77IRPJSu8g907jLuuioLORHZJ3YAOY2DsP5ebrZrjOrAwjqf+Cgkg/I8TGPpw==} engines: {node: '>=18'} @@ -4110,12 +4113,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.1.12: - resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} - - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - snapshots: '@antfu/ni@27.0.1': @@ -5053,6 +5050,8 @@ snapshots: '@sigstore/core': 3.2.0 '@sigstore/protobuf-specs': 0.5.0 + '@sinclair/typebox@0.34.49': {} + '@sindresorhus/chunkify@2.0.0': {} '@sindresorhus/df@1.0.1': {} @@ -6898,7 +6897,3 @@ snapshots: yoctocolors-cjs@2.1.3: {} zod@3.25.76: {} - - zod@4.1.12: {} - - zod@4.3.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6186bb79..9908f3b0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,6 +26,7 @@ catalog: '@mdn/browser-compat-data': 7.1.5 '@npmcli/package-json': 7.0.0 '@npmcli/promise-spawn': 8.0.3 + '@sinclair/typebox': 0.34.49 '@socketregistry/packageurl-js': 1.4.2 '@socketsecurity/lib': 5.20.1 '@types/fs-extra': 11.0.4 @@ -100,7 +101,6 @@ catalog: which: 5.0.0 yargs-parser: 22.0.0 yoctocolors-cjs: 2.1.3 - zod: 4.3.6 # Wait 7 days (10080 minutes) before installing newly published packages. minimumReleaseAge: 10080 diff --git a/scripts/xport-emit-schema.mts b/scripts/xport-emit-schema.mts index 30bf1ea1..5bc6a1e6 100644 --- a/scripts/xport-emit-schema.mts +++ b/scripts/xport-emit-schema.mts @@ -1,19 +1,17 @@ /** - * @fileoverview Emit `xport.schema.json` from the zod schema. + * @fileoverview Emit `xport.schema.json` from the TypeBox schema. * - * The zod schema in `scripts/xport-schema.mts` is the source of truth. This - * script writes the draft-2020-12 JSON Schema that consumers without zod - * (editors, external validators) consume via the `$schema` reference in - * `xport.json`. + * The TypeBox schema in `scripts/xport-schema.mts` is the source of truth. + * TypeBox schemas are JSON Schema natively — no conversion library needed, + * just serialize the schema object and add the draft-2020-12 meta headers. * - * Run via `pnpm run xport:emit-schema` when the zod schema changes. + * Run via `pnpm run xport:emit-schema` when the schema changes. */ import { writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { z } from 'zod' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { XportManifestSchema } from './xport-schema.mts' @@ -24,16 +22,15 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootDir = path.resolve(__dirname, '..') const outPath = path.join(rootDir, 'xport.schema.json') -const json = z.toJSONSchema(XportManifestSchema, { - target: 'draft-2020-12', -}) - -// Add the canonical $id for portability across Socket repos. +// TypeBox schemas carry JSON Schema shape directly, plus a Symbol-keyed +// [Kind] marker that JSON.stringify drops. Spreading the schema first +// then layering the canonical $schema / $id / title on top gives a clean +// draft-2020-12 document with the Socket-specific headers. const enriched = { $schema: 'https://json-schema.org/draft/2020-12/schema', $id: 'https://github.com/SocketDev/xport.schema.json', title: 'xport lock-step manifest', - ...json, + ...XportManifestSchema, } writeFileSync(outPath, JSON.stringify(enriched, null, 2) + '\n', 'utf8') diff --git a/scripts/xport-schema.mts b/scripts/xport-schema.mts index 9cb176ef..aa6c0d04 100644 --- a/scripts/xport-schema.mts +++ b/scripts/xport-schema.mts @@ -1,275 +1,301 @@ /** - * @fileoverview zod schema for xport.json — single source of truth. + * @fileoverview TypeBox schema for xport.json — single source of truth. * * Everything else is derived: - * - TypeScript types in scripts/xport.mts via `z.infer` - * - xport.schema.json (draft 2020-12) via `z.toJSONSchema()`, emitted by - * scripts/xport-emit-schema.mts - * - Runtime validation at harness startup via `XportManifestSchema.parse()` + * - TypeScript types in scripts/xport.mts via `Static` + * - xport.schema.json (draft 2020-12) via direct JSON.stringify of the + * TypeBox schema, emitted by scripts/xport-emit-schema.mts + * - Runtime validation at harness startup via + * `validateSchema(XportManifestSchema, ...)` from + * `@socketsecurity/lib/validation/validate-schema` * * Byte-identical across socket-tui / socket-btm / socket-sdxgen / ultrathink / * socket-registry / socket-repo-template via sync-scaffolding.mjs. */ -import { z } from 'zod' +import { Type, type Static } from '@sinclair/typebox' // --------------------------------------------------------------------------- // Shared primitives. // --------------------------------------------------------------------------- -const IdSchema = z - .string() - .regex(/^[a-z0-9][A-Za-z0-9-]*$/) - .describe( +const IdSchema = Type.String({ + pattern: '^[a-z0-9][A-Za-z0-9-]*$', + description: 'Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name).', - ) +}) -const CriticalitySchema = z - .number() - .int() - .min(1) - .max(10) - .describe( +const CriticalitySchema = Type.Integer({ + minimum: 1, + maximum: 10, + description: 'Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder.', - ) +}) -const UpstreamRefSchema = z - .string() - .describe('Key into the top-level `upstreams` map.') +const UpstreamRefSchema = Type.String({ + description: 'Key into the top-level `upstreams` map.', +}) -const ConformanceTestSchema = z - .string() - .describe( +const ConformanceTestSchema = Type.String({ + description: "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift.", - ) +}) -const NotesSchema = z - .string() - .describe( +const NotesSchema = Type.String({ + description: 'Free-form context — why this row exists, what gotchas to watch for.', - ) +}) -const ShaSchema = z - .string() - .regex(/^[0-9a-f]{40}$/) - .describe('Full 40-char git SHA.') - -const PortStatusSchema = z - .object({ - status: z.enum(['implemented', 'opt-out']), - reason: z - .string() - .optional() - .describe('Required when status is `opt-out`. Explain why.'), - path: z - .string() - .optional() - .describe( - "Optional path to the port's implementation of this row. Useful for module-inventory rows where each language points at a different directory.", - ), - note: z - .string() - .optional() - .describe( - "Optional free-form note attached to a specific port's status.", - ), - }) - .strict() - .describe( - 'Per-port status for a lang-parity row. `implemented` = port meets assertions; `opt-out` = port consciously skips, requires non-empty `reason`.', - ) +const PortStatusSchema = Type.Object( + { + status: Type.Union([Type.Literal('implemented'), Type.Literal('opt-out')]), + reason: Type.Optional( + Type.String({ + description: 'Required when status is `opt-out`. Explain why.', + }), + ), + path: Type.Optional( + Type.String({ + description: + "Optional path to the port's implementation of this row. Useful for module-inventory rows where each language points at a different directory.", + }), + ), + note: Type.Optional( + Type.String({ + description: + "Optional free-form note attached to a specific port's status.", + }), + ), + }, + { + additionalProperties: false, + description: + 'Per-port status for a lang-parity row. `implemented` = port meets assertions; `opt-out` = port consciously skips, requires non-empty `reason`.', + }, +) -const UpstreamSchema = z - .object({ - submodule: z.string().describe('Submodule path, relative to repo root.'), - repo: z.string().url(), - }) - .strict() +const UpstreamSchema = Type.Object( + { + submodule: Type.String({ + description: 'Submodule path, relative to repo root.', + }), + repo: Type.String({ + pattern: '^https?://', + description: 'Upstream repository URL (http:// or https://).', + }), + }, + { additionalProperties: false }, +) -const SiteSchema = z - .object({ - path: z - .string() - .describe("Path to the port's root directory, relative to repo root."), - language: z - .string() - .optional() - .describe('Language label, for human reports.'), - }) - .strict() +const SiteSchema = Type.Object( + { + path: Type.String({ + description: "Path to the port's root directory, relative to repo root.", + }), + language: Type.Optional( + Type.String({ description: 'Language label, for human reports.' }), + ), + }, + { additionalProperties: false }, +) -const FixtureCheckSchema = z - .object({ - fixture_path: z.string(), - snapshot_path: z.string().optional(), - diff_tolerance: z.enum(['exact', 'line-by-line', 'semantic']).optional(), - }) - .strict() - .describe( - "Golden-input verification. Prefer snapshot-based diffs over hardcoded counts (brittleness lesson from sdxgen's lock-step-features).", - ) +const FixtureCheckSchema = Type.Object( + { + fixture_path: Type.String(), + snapshot_path: Type.Optional(Type.String()), + diff_tolerance: Type.Optional( + Type.Union([ + Type.Literal('exact'), + Type.Literal('line-by-line'), + Type.Literal('semantic'), + ]), + ), + }, + { + additionalProperties: false, + description: + "Golden-input verification. Prefer snapshot-based diffs over hardcoded counts (brittleness lesson from sdxgen's lock-step-features).", + }, +) // --------------------------------------------------------------------------- // Row kinds. // --------------------------------------------------------------------------- -const FileForkRowSchema = z - .object({ - kind: z.literal('file-fork'), +const FileForkRowSchema = Type.Object( + { + kind: Type.Literal('file-fork'), id: IdSchema, upstream: UpstreamRefSchema, - criticality: CriticalitySchema.optional(), - conformance_test: ConformanceTestSchema.optional(), - notes: NotesSchema.optional(), - local: z - .string() - .describe('Path to our ported file, relative to repo root.'), - upstream_path: z - .string() - .describe('Path to the source file within the upstream submodule.'), - forked_at_sha: ShaSchema.describe( - 'Full 40-char SHA of the upstream commit we forked from. Harness runs `git log ..HEAD -- ` to surface drift.', - ), - deviations: z - .array(z.string()) - .min(1) - .describe( + criticality: Type.Optional(CriticalitySchema), + conformance_test: Type.Optional(ConformanceTestSchema), + notes: Type.Optional(NotesSchema), + local: Type.String({ + description: 'Path to our ported file, relative to repo root.', + }), + upstream_path: Type.String({ + description: 'Path to the source file within the upstream submodule.', + }), + forked_at_sha: Type.String({ + pattern: '^[0-9a-f]{40}$', + description: + 'Full 40-char SHA of the upstream commit we forked from. Harness runs `git log ..HEAD -- ` to surface drift.', + }), + deviations: Type.Array(Type.String(), { + minItems: 1, + description: "Human-readable list of intentional differences. Zero deviations = use upstream directly; don't fork.", - ), - }) - .strict() - .describe( - 'A local file derived from an upstream file with intentional modifications. Drift = upstream moved forward without us.', - ) + }), + }, + { + additionalProperties: false, + description: + 'A local file derived from an upstream file with intentional modifications. Drift = upstream moved forward without us.', + }, +) -const VersionPinRowSchema = z - .object({ - kind: z.literal('version-pin'), +const VersionPinRowSchema = Type.Object( + { + kind: Type.Literal('version-pin'), id: IdSchema, upstream: UpstreamRefSchema, - criticality: CriticalitySchema.optional(), - conformance_test: ConformanceTestSchema.optional(), - notes: NotesSchema.optional(), - pinned_sha: ShaSchema.describe( - 'Full 40-char SHA the submodule is pinned to.', + criticality: Type.Optional(CriticalitySchema), + conformance_test: Type.Optional(ConformanceTestSchema), + notes: Type.Optional(NotesSchema), + pinned_sha: Type.String({ + pattern: '^[0-9a-f]{40}$', + description: 'Full 40-char SHA the submodule is pinned to.', + }), + pinned_tag: Type.Optional( + Type.String({ + description: + 'Human-readable release tag (e.g., `v3.2.1`). Optional — the SHA is authoritative.', + }), ), - pinned_tag: z - .string() - .optional() - .describe( - 'Human-readable release tag (e.g., `v3.2.1`). Optional — the SHA is authoritative.', - ), - upgrade_policy: z - .enum(['track-latest', 'major-gate', 'locked']) - .describe( - 'track-latest: any new release is actionable; major-gate: only major bumps require review; locked: explicit decision per upgrade.', - ), - }) - .strict() - .describe( - "A submodule pinned to an upstream release. Drift = upstream cut a new release we haven't adopted.", - ) + upgrade_policy: Type.Union( + [ + Type.Literal('track-latest'), + Type.Literal('major-gate'), + Type.Literal('locked'), + ], + { + description: + 'track-latest: any new release is actionable; major-gate: only major bumps require review; locked: explicit decision per upgrade.', + }, + ), + }, + { + additionalProperties: false, + description: + "A submodule pinned to an upstream release. Drift = upstream cut a new release we haven't adopted.", + }, +) -const FeatureParityRowSchema = z - .object({ - kind: z.literal('feature-parity'), +const FeatureParityRowSchema = Type.Object( + { + kind: Type.Literal('feature-parity'), id: IdSchema, upstream: UpstreamRefSchema, criticality: CriticalitySchema, - conformance_test: ConformanceTestSchema.optional(), - notes: NotesSchema.optional(), - local_area: z - .string() - .describe( + conformance_test: Type.Optional(ConformanceTestSchema), + notes: Type.Optional(NotesSchema), + local_area: Type.String({ + description: 'Path to the local module/directory implementing the feature. Code pattern scan targets this directory (excluding test files).', - ), - test_area: z - .string() - .optional() - .describe( - 'Optional path to the directory where tests for this feature live. When absent, the harness searches inside `local_area`.', - ), - code_patterns: z - .array(z.string()) - .optional() - .describe( - 'Regex patterns the local implementation must contain. Prefer anchored patterns (function signatures) over loose keywords to avoid comment false positives.', - ), - test_patterns: z - .array(z.string()) - .optional() - .describe('Regex patterns the test suite must contain.'), - fixture_check: FixtureCheckSchema.optional(), - }) - .strict() - .describe( - 'A behavioral feature reimplemented locally to match upstream behavior. Three-pillar validation: code patterns, test patterns, fixture snapshots.', - ) + }), + test_area: Type.Optional( + Type.String({ + description: + 'Optional path to the directory where tests for this feature live. When absent, the harness searches inside `local_area`.', + }), + ), + code_patterns: Type.Optional( + Type.Array(Type.String(), { + description: + 'Regex patterns the local implementation must contain. Prefer anchored patterns (function signatures) over loose keywords to avoid comment false positives.', + }), + ), + test_patterns: Type.Optional( + Type.Array(Type.String(), { + description: 'Regex patterns the test suite must contain.', + }), + ), + fixture_check: Type.Optional(FixtureCheckSchema), + }, + { + additionalProperties: false, + description: + 'A behavioral feature reimplemented locally to match upstream behavior. Three-pillar validation: code patterns, test patterns, fixture snapshots.', + }, +) -const SpecConformanceRowSchema = z - .object({ - kind: z.literal('spec-conformance'), +const SpecConformanceRowSchema = Type.Object( + { + kind: Type.Literal('spec-conformance'), id: IdSchema, upstream: UpstreamRefSchema, - criticality: CriticalitySchema.optional(), - conformance_test: ConformanceTestSchema.optional(), - notes: NotesSchema.optional(), - local_impl: z.string(), - spec_version: z.string(), - spec_path: z - .string() - .optional() - .describe( - 'Path within the upstream submodule to the spec document, if applicable.', - ), - }) - .strict() - .describe( - 'A local reimplementation of an external specification. Drift = the spec was revised.', - ) + criticality: Type.Optional(CriticalitySchema), + conformance_test: Type.Optional(ConformanceTestSchema), + notes: Type.Optional(NotesSchema), + local_impl: Type.String(), + spec_version: Type.String(), + spec_path: Type.Optional( + Type.String({ + description: + 'Path within the upstream submodule to the spec document, if applicable.', + }), + ), + }, + { + additionalProperties: false, + description: + 'A local reimplementation of an external specification. Drift = the spec was revised.', + }, +) // Assertions are deliberately untyped — each matrix area defines its own // assertion shapes. The harness ignores fields it doesn't recognize. // Historical precedent: ultrathink's xlang-harness.mts treats this as // `unknown[]`. -const AssertionSchema = z.record(z.string(), z.unknown()) +const AssertionSchema = Type.Record(Type.String(), Type.Unknown()) -const LangParityRowSchema = z - .object({ - kind: z.literal('lang-parity'), +const LangParityRowSchema = Type.Object( + { + kind: Type.Literal('lang-parity'), id: IdSchema, - name: z.string(), - description: z.string(), - category: z - .string() - .describe( + name: Type.String(), + description: Type.String(), + category: Type.String({ + description: 'Grouping tag. `rejected` is reserved for anti-patterns (every port must be opt-out; reintroduction exits 2).', - ), - criticality: CriticalitySchema.optional(), - conformance_test: ConformanceTestSchema.optional(), - notes: NotesSchema.optional(), - assertions: z - .array(AssertionSchema) - .optional() - .describe( - 'Open-ended assertion list. Each has a `kind` string the harness dispatches on. Unknown kinds are skipped with a log line.', - ), - matrix_files: z - .array(z.string()) - .optional() - .describe( - 'For inventory rows that index other xport-lang-*.json files. Paths relative to this manifest.', - ), - ports: z - .record(z.string(), PortStatusSchema) - .describe('Per-site status. Keys must match top-level `sites`.'), - }) - .strict() - .describe( - 'N sibling language ports of one spec within a single project. Drift = one port diverged from its siblings.', - ) + }), + criticality: Type.Optional(CriticalitySchema), + conformance_test: Type.Optional(ConformanceTestSchema), + notes: Type.Optional(NotesSchema), + assertions: Type.Optional( + Type.Array(AssertionSchema, { + description: + 'Open-ended assertion list. Each has a `kind` string the harness dispatches on. Unknown kinds are skipped with a log line.', + }), + ), + matrix_files: Type.Optional( + Type.Array(Type.String(), { + description: + 'For inventory rows that index other xport-lang-*.json files. Paths relative to this manifest.', + }), + ), + ports: Type.Record(Type.String(), PortStatusSchema, { + description: 'Per-site status. Keys must match top-level `sites`.', + }), + }, + { + additionalProperties: false, + description: + 'N sibling language ports of one spec within a single project. Drift = one port diverged from its siblings.', + }, +) -export const RowSchema = z.discriminatedUnion('kind', [ +export const RowSchema = Type.Union([ FileForkRowSchema, VersionPinRowSchema, FeatureParityRowSchema, @@ -281,47 +307,49 @@ export const RowSchema = z.discriminatedUnion('kind', [ // Top-level manifest. // --------------------------------------------------------------------------- -export const XportManifestSchema = z - .object({ - $schema: z.string().optional(), - description: z.string().optional(), - area: z - .string() - .optional() - .describe( - "Optional label for this manifest file. Used as a grouping key in harness output. Defaults to 'root' for the top-level file and to the filename stem for included files.", - ), - includes: z - .array(z.string()) - .optional() - .describe( - 'Relative paths to sub-manifests. Top-level `upstreams` and `sites` maps override any same-keyed entries in included manifests.', - ), - upstreams: z - .record(z.string(), UpstreamSchema) - .optional() - .describe( - 'Named upstream submodules. Referenced by rows[].upstream on file-fork, version-pin, feature-parity, spec-conformance rows. Omit when the manifest only has lang-parity rows.', - ), - sites: z - .record(z.string(), SiteSchema) - .optional() - .describe( - 'Named sibling ports (typically per-language). Referenced by rows[].ports. on lang-parity rows. Omit when the manifest has no lang-parity rows.', - ), - rows: z.array(RowSchema), - }) - .describe( - 'Unified lock-step manifest shared across Socket repos. One schema, all cases — `kind` discriminator on each row selects which flavor of lock-step applies.', - ) +export const XportManifestSchema = Type.Object( + { + $schema: Type.Optional(Type.String()), + description: Type.Optional(Type.String()), + area: Type.Optional( + Type.String({ + description: + "Optional label for this manifest file. Used as a grouping key in harness output. Defaults to 'root' for the top-level file and to the filename stem for included files.", + }), + ), + includes: Type.Optional( + Type.Array(Type.String(), { + description: + 'Relative paths to sub-manifests. Top-level `upstreams` and `sites` maps override any same-keyed entries in included manifests.', + }), + ), + upstreams: Type.Optional( + Type.Record(Type.String(), UpstreamSchema, { + description: + 'Named upstream submodules. Referenced by rows[].upstream on file-fork, version-pin, feature-parity, spec-conformance rows. Omit when the manifest only has lang-parity rows.', + }), + ), + sites: Type.Optional( + Type.Record(Type.String(), SiteSchema, { + description: + 'Named sibling ports (typically per-language). Referenced by rows[].ports. on lang-parity rows. Omit when the manifest has no lang-parity rows.', + }), + ), + rows: Type.Array(RowSchema), + }, + { + description: + 'Unified lock-step manifest shared across Socket repos. One schema, all cases — `kind` discriminator on each row selects which flavor of lock-step applies.', + }, +) -export type Row = z.infer -export type XportManifest = z.infer -export type Upstream = z.infer -export type Site = z.infer -export type PortStatus = z.infer -export type FileForkRow = z.infer -export type VersionPinRow = z.infer -export type FeatureParityRow = z.infer -export type SpecConformanceRow = z.infer -export type LangParityRow = z.infer +export type Row = Static +export type XportManifest = Static +export type Upstream = Static +export type Site = Static +export type PortStatus = Static +export type FileForkRow = Static +export type VersionPinRow = Static +export type FeatureParityRow = Static +export type SpecConformanceRow = Static +export type LangParityRow = Static diff --git a/scripts/xport.mts b/scripts/xport.mts index 7aeb4d8c..e20c2dfa 100644 --- a/scripts/xport.mts +++ b/scripts/xport.mts @@ -42,6 +42,7 @@ import { fileURLToPath } from 'node:url' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawnSync } from '@socketsecurity/lib/spawn' +import { validateSchema } from '@socketsecurity/lib/validation/validate-schema' import { XportManifestSchema, @@ -150,16 +151,16 @@ function readManifest(manifestPath: string): Manifest { logger.fail(` ${e instanceof Error ? e.message : String(e)}`) process.exit(1) } - const result = XportManifestSchema.safeParse(raw) - if (!result.success) { - logger.error(`xport: schema validation failed for ${manifestPath}`) - for (const issue of result.error.issues) { - const loc = issue.path.length ? issue.path.join('.') : '' - logger.fail(` ${loc}: ${issue.message}`) - } - process.exit(1) + const result = validateSchema(XportManifestSchema, raw) + if (result.ok) { + return result.value + } + logger.error(`xport: schema validation failed for ${manifestPath}`) + for (const issue of result.errors) { + const loc = issue.path.length ? issue.path.join('.') : '' + logger.fail(` ${loc}: ${issue.message}`) } - return result.data + process.exit(1) } /** @@ -700,7 +701,8 @@ function checkLangParity( } if (row.category === 'rejected') { - for (const [port, state] of Object.entries(row.ports)) { + for (const port of Object.keys(row.ports)) { + const state = row.ports[port]! if (state.status !== 'opt-out') { base.severity = 'drift' messages.push( diff --git a/xport.schema.json b/xport.schema.json index e55320a2..6cbd8019 100644 --- a/xport.schema.json +++ b/xport.schema.json @@ -2,7 +2,9 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/SocketDev/xport.schema.json", "title": "xport lock-step manifest", + "description": "Unified lock-step manifest shared across Socket repos. One schema, all cases — `kind` discriminator on each row selects which flavor of lock-step applies.", "type": "object", + "required": ["rows"], "properties": { "$schema": { "type": "string" @@ -24,203 +26,217 @@ "upstreams": { "description": "Named upstream submodules. Referenced by rows[].upstream on file-fork, version-pin, feature-parity, spec-conformance rows. Omit when the manifest only has lang-parity rows.", "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "properties": { - "submodule": { - "type": "string", - "description": "Submodule path, relative to repo root." - }, - "repo": { - "type": "string", - "format": "uri" + "patternProperties": { + "^(.*)$": { + "additionalProperties": false, + "type": "object", + "required": ["submodule", "repo"], + "properties": { + "submodule": { + "description": "Submodule path, relative to repo root.", + "type": "string" + }, + "repo": { + "pattern": "^https?://", + "description": "Upstream repository URL (http:// or https://).", + "type": "string" + } } - }, - "required": ["submodule", "repo"], - "additionalProperties": false + } } }, "sites": { "description": "Named sibling ports (typically per-language). Referenced by rows[].ports. on lang-parity rows. Omit when the manifest has no lang-parity rows.", "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to the port's root directory, relative to repo root." - }, - "language": { - "description": "Language label, for human reports.", - "type": "string" + "patternProperties": { + "^(.*)$": { + "additionalProperties": false, + "type": "object", + "required": ["path"], + "properties": { + "path": { + "description": "Path to the port's root directory, relative to repo root.", + "type": "string" + }, + "language": { + "description": "Language label, for human reports.", + "type": "string" + } } - }, - "required": ["path"], - "additionalProperties": false + } } }, "rows": { "type": "array", "items": { - "oneOf": [ + "anyOf": [ { + "additionalProperties": false, + "description": "A local file derived from an upstream file with intentional modifications. Drift = upstream moved forward without us.", "type": "object", + "required": [ + "kind", + "id", + "upstream", + "local", + "upstream_path", + "forked_at_sha", + "deviations" + ], "properties": { "kind": { - "type": "string", - "const": "file-fork" + "const": "file-fork", + "type": "string" }, "id": { - "type": "string", "pattern": "^[a-z0-9][A-Za-z0-9-]*$", - "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name)." + "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name).", + "type": "string" }, "upstream": { - "type": "string", - "description": "Key into the top-level `upstreams` map." + "description": "Key into the top-level `upstreams` map.", + "type": "string" }, "criticality": { - "type": "integer", "minimum": 1, "maximum": 10, - "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder." + "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder.", + "type": "integer" }, "conformance_test": { - "type": "string", - "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift." + "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift.", + "type": "string" }, "notes": { - "type": "string", - "description": "Free-form context — why this row exists, what gotchas to watch for." + "description": "Free-form context — why this row exists, what gotchas to watch for.", + "type": "string" }, "local": { - "type": "string", - "description": "Path to our ported file, relative to repo root." + "description": "Path to our ported file, relative to repo root.", + "type": "string" }, "upstream_path": { - "type": "string", - "description": "Path to the source file within the upstream submodule." + "description": "Path to the source file within the upstream submodule.", + "type": "string" }, "forked_at_sha": { - "type": "string", "pattern": "^[0-9a-f]{40}$", - "description": "Full 40-char SHA of the upstream commit we forked from. Harness runs `git log ..HEAD -- ` to surface drift." + "description": "Full 40-char SHA of the upstream commit we forked from. Harness runs `git log ..HEAD -- ` to surface drift.", + "type": "string" }, "deviations": { "minItems": 1, + "description": "Human-readable list of intentional differences. Zero deviations = use upstream directly; don't fork.", "type": "array", "items": { "type": "string" - }, - "description": "Human-readable list of intentional differences. Zero deviations = use upstream directly; don't fork." + } } - }, + } + }, + { + "additionalProperties": false, + "description": "A submodule pinned to an upstream release. Drift = upstream cut a new release we haven't adopted.", + "type": "object", "required": [ "kind", "id", "upstream", - "local", - "upstream_path", - "forked_at_sha", - "deviations" + "pinned_sha", + "upgrade_policy" ], - "additionalProperties": false, - "description": "A local file derived from an upstream file with intentional modifications. Drift = upstream moved forward without us." - }, - { - "type": "object", "properties": { "kind": { - "type": "string", - "const": "version-pin" + "const": "version-pin", + "type": "string" }, "id": { - "type": "string", "pattern": "^[a-z0-9][A-Za-z0-9-]*$", - "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name)." + "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name).", + "type": "string" }, "upstream": { - "type": "string", - "description": "Key into the top-level `upstreams` map." + "description": "Key into the top-level `upstreams` map.", + "type": "string" }, "criticality": { - "type": "integer", "minimum": 1, "maximum": 10, - "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder." + "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder.", + "type": "integer" }, "conformance_test": { - "type": "string", - "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift." + "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift.", + "type": "string" }, "notes": { - "type": "string", - "description": "Free-form context — why this row exists, what gotchas to watch for." + "description": "Free-form context — why this row exists, what gotchas to watch for.", + "type": "string" }, "pinned_sha": { - "type": "string", "pattern": "^[0-9a-f]{40}$", - "description": "Full 40-char SHA the submodule is pinned to." + "description": "Full 40-char SHA the submodule is pinned to.", + "type": "string" }, "pinned_tag": { "description": "Human-readable release tag (e.g., `v3.2.1`). Optional — the SHA is authoritative.", "type": "string" }, "upgrade_policy": { - "type": "string", - "enum": ["track-latest", "major-gate", "locked"], - "description": "track-latest: any new release is actionable; major-gate: only major bumps require review; locked: explicit decision per upgrade." + "description": "track-latest: any new release is actionable; major-gate: only major bumps require review; locked: explicit decision per upgrade.", + "anyOf": [ + { + "const": "track-latest", + "type": "string" + }, + { + "const": "major-gate", + "type": "string" + }, + { + "const": "locked", + "type": "string" + } + ] } - }, - "required": [ - "kind", - "id", - "upstream", - "pinned_sha", - "upgrade_policy" - ], - "additionalProperties": false, - "description": "A submodule pinned to an upstream release. Drift = upstream cut a new release we haven't adopted." + } }, { + "additionalProperties": false, + "description": "A behavioral feature reimplemented locally to match upstream behavior. Three-pillar validation: code patterns, test patterns, fixture snapshots.", "type": "object", + "required": ["kind", "id", "upstream", "criticality", "local_area"], "properties": { "kind": { - "type": "string", - "const": "feature-parity" + "const": "feature-parity", + "type": "string" }, "id": { - "type": "string", "pattern": "^[a-z0-9][A-Za-z0-9-]*$", - "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name)." + "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name).", + "type": "string" }, "upstream": { - "type": "string", - "description": "Key into the top-level `upstreams` map." + "description": "Key into the top-level `upstreams` map.", + "type": "string" }, "criticality": { - "type": "integer", "minimum": 1, "maximum": 10, - "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder." + "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder.", + "type": "integer" }, "conformance_test": { - "type": "string", - "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift." + "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift.", + "type": "string" }, "notes": { - "type": "string", - "description": "Free-form context — why this row exists, what gotchas to watch for." + "description": "Free-form context — why this row exists, what gotchas to watch for.", + "type": "string" }, "local_area": { - "type": "string", - "description": "Path to the local module/directory implementing the feature. Code pattern scan targets this directory (excluding test files)." + "description": "Path to the local module/directory implementing the feature. Code pattern scan targets this directory (excluding test files).", + "type": "string" }, "test_area": { "description": "Optional path to the directory where tests for this feature live. When absent, the harness searches inside `local_area`.", @@ -241,7 +257,10 @@ } }, "fixture_check": { + "additionalProperties": false, + "description": "Golden-input verification. Prefer snapshot-based diffs over hardcoded counts (brittleness lesson from sdxgen's lock-step-features).", "type": "object", + "required": ["fixture_path"], "properties": { "fixture_path": { "type": "string" @@ -250,48 +269,63 @@ "type": "string" }, "diff_tolerance": { - "type": "string", - "enum": ["exact", "line-by-line", "semantic"] + "anyOf": [ + { + "const": "exact", + "type": "string" + }, + { + "const": "line-by-line", + "type": "string" + }, + { + "const": "semantic", + "type": "string" + } + ] } - }, - "required": ["fixture_path"], - "additionalProperties": false, - "description": "Golden-input verification. Prefer snapshot-based diffs over hardcoded counts (brittleness lesson from sdxgen's lock-step-features)." + } } - }, - "required": ["kind", "id", "upstream", "criticality", "local_area"], - "additionalProperties": false, - "description": "A behavioral feature reimplemented locally to match upstream behavior. Three-pillar validation: code patterns, test patterns, fixture snapshots." + } }, { + "additionalProperties": false, + "description": "A local reimplementation of an external specification. Drift = the spec was revised.", "type": "object", + "required": [ + "kind", + "id", + "upstream", + "local_impl", + "spec_version" + ], "properties": { "kind": { - "type": "string", - "const": "spec-conformance" + "const": "spec-conformance", + "type": "string" }, "id": { - "type": "string", "pattern": "^[a-z0-9][A-Za-z0-9-]*$", - "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name)." + "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name).", + "type": "string" }, "upstream": { - "type": "string", - "description": "Key into the top-level `upstreams` map." + "description": "Key into the top-level `upstreams` map.", + "type": "string" }, "criticality": { - "type": "integer", "minimum": 1, "maximum": 10, - "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder." + "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder.", + "type": "integer" }, "conformance_test": { - "type": "string", - "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift." + "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift.", + "type": "string" }, "notes": { - "type": "string", - "description": "Free-form context — why this row exists, what gotchas to watch for." + "description": "Free-form context — why this row exists, what gotchas to watch for.", + "type": "string" }, "local_impl": { "type": "string" @@ -303,28 +337,29 @@ "description": "Path within the upstream submodule to the spec document, if applicable.", "type": "string" } - }, + } + }, + { + "additionalProperties": false, + "description": "N sibling language ports of one spec within a single project. Drift = one port diverged from its siblings.", + "type": "object", "required": [ "kind", "id", - "upstream", - "local_impl", - "spec_version" + "name", + "description", + "category", + "ports" ], - "additionalProperties": false, - "description": "A local reimplementation of an external specification. Drift = the spec was revised." - }, - { - "type": "object", "properties": { "kind": { - "type": "string", - "const": "lang-parity" + "const": "lang-parity", + "type": "string" }, "id": { - "type": "string", "pattern": "^[a-z0-9][A-Za-z0-9-]*$", - "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name)." + "description": "Stable identifier, unique within the manifest. Starts with lowercase letter or digit; remaining characters are letters/digits/hyphens. Kebab-case preferred, but camelCase segments are allowed (e.g. `export-findNodeAt` when the id mirrors an API name).", + "type": "string" }, "name": { "type": "string" @@ -333,32 +368,31 @@ "type": "string" }, "category": { - "type": "string", - "description": "Grouping tag. `rejected` is reserved for anti-patterns (every port must be opt-out; reintroduction exits 2)." + "description": "Grouping tag. `rejected` is reserved for anti-patterns (every port must be opt-out; reintroduction exits 2).", + "type": "string" }, "criticality": { - "type": "integer", "minimum": 1, "maximum": 10, - "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder." + "description": "Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder.", + "type": "integer" }, "conformance_test": { - "type": "string", - "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift." + "description": "Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift.", + "type": "string" }, "notes": { - "type": "string", - "description": "Free-form context — why this row exists, what gotchas to watch for." + "description": "Free-form context — why this row exists, what gotchas to watch for.", + "type": "string" }, "assertions": { "description": "Open-ended assertion list. Each has a `kind` string the harness dispatches on. Unknown kinds are skipped with a log line.", "type": "array", "items": { "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} + "patternProperties": { + "^(.*)$": {} + } } }, "matrix_files": { @@ -369,53 +403,47 @@ } }, "ports": { + "description": "Per-site status. Keys must match top-level `sites`.", "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["implemented", "opt-out"] - }, - "reason": { - "description": "Required when status is `opt-out`. Explain why.", - "type": "string" - }, - "path": { - "description": "Optional path to the port's implementation of this row. Useful for module-inventory rows where each language points at a different directory.", - "type": "string" - }, - "note": { - "description": "Optional free-form note attached to a specific port's status.", - "type": "string" + "patternProperties": { + "^(.*)$": { + "additionalProperties": false, + "description": "Per-port status for a lang-parity row. `implemented` = port meets assertions; `opt-out` = port consciously skips, requires non-empty `reason`.", + "type": "object", + "required": ["status"], + "properties": { + "status": { + "anyOf": [ + { + "const": "implemented", + "type": "string" + }, + { + "const": "opt-out", + "type": "string" + } + ] + }, + "reason": { + "description": "Required when status is `opt-out`. Explain why.", + "type": "string" + }, + "path": { + "description": "Optional path to the port's implementation of this row. Useful for module-inventory rows where each language points at a different directory.", + "type": "string" + }, + "note": { + "description": "Optional free-form note attached to a specific port's status.", + "type": "string" + } } - }, - "required": ["status"], - "additionalProperties": false, - "description": "Per-port status for a lang-parity row. `implemented` = port meets assertions; `opt-out` = port consciously skips, requires non-empty `reason`." - }, - "description": "Per-site status. Keys must match top-level `sites`." + } + } } - }, - "required": [ - "kind", - "id", - "name", - "description", - "category", - "ports" - ], - "additionalProperties": false, - "description": "N sibling language ports of one spec within a single project. Drift = one port diverged from its siblings." + } } ] } } - }, - "required": ["rows"], - "additionalProperties": false, - "description": "Unified lock-step manifest shared across Socket repos. One schema, all cases — `kind` discriminator on each row selects which flavor of lock-step applies." + } }