Skip to content

Commit d84957a

Browse files
authored
feat: add xport lock-step manifest tooling (#1284)
* feat: add xport lock-step manifest tooling Adds the xport tooling shared across the Socket fleet for declaring cross-repo lockstep dependencies (gitlinks, package versions). Files: scripts/xport.mts — runner scripts/xport-schema.mts — TypeBox source of truth scripts/xport-emit-schema.mts — generates xport.schema.json xport.schema.json — machine-generated, used by validators @sinclair/typebox is already in the pnpm-workspace.yaml catalog (consumed by .claude/hooks/setup-security-tools and packages/build- infra), so root-level imports resolve through hoisting — no new dep needed at the root. Self-landable split from #1279. * fix(xport): address Bugbot findings on schema-emit and severity downgrade - Swap spread order in xport-emit-schema.mts so the comment matches the code: spread XportManifestSchema first, then layer the canonical $schema / $id / title on top. Previously the explicit headers came first and were silently overridden by any matching key the TypeBox schema might gain (TypeBox's SchemaOptions supports title/$schema/$id). - Guard the rejected-loop in xport.mts checkLangParity so a 'rejected' anti-pattern message cannot downgrade an already-'error' severity to 'drift'. Structural problems (port not in sites map, opt-out missing reason) keep their error severity even when the row also matches the rejected anti-pattern.
1 parent 19f9af2 commit d84957a

4 files changed

Lines changed: 1849 additions & 0 deletions

File tree

scripts/xport-emit-schema.mts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @fileoverview Emit `xport.schema.json` from the TypeBox schema.
3+
*
4+
* The TypeBox schema in `scripts/xport-schema.mts` is the source of truth.
5+
* TypeBox schemas are JSON Schema natively — no conversion library needed,
6+
* just serialize the schema object and add the draft-2020-12 meta headers.
7+
*
8+
* Run via `pnpm run xport:emit-schema` when the schema changes.
9+
*/
10+
11+
import { writeFileSync } from 'node:fs'
12+
import path from 'node:path'
13+
import { fileURLToPath } from 'node:url'
14+
15+
import { getDefaultLogger } from '@socketsecurity/lib/logger'
16+
17+
import { XportManifestSchema } from './xport-schema.mts'
18+
19+
const logger = getDefaultLogger()
20+
21+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
22+
const rootDir = path.resolve(__dirname, '..')
23+
const outPath = path.join(rootDir, 'xport.schema.json')
24+
25+
// TypeBox schemas carry JSON Schema shape directly, plus a Symbol-keyed
26+
// [Kind] marker that JSON.stringify drops. Spreading the schema first
27+
// then layering the canonical $schema / $id / title on top gives a clean
28+
// draft-2020-12 document with the Socket-specific headers.
29+
const enriched = {
30+
...XportManifestSchema,
31+
$schema: 'https://json-schema.org/draft/2020-12/schema',
32+
$id: 'https://github.com/SocketDev/xport.schema.json',
33+
title: 'xport lock-step manifest',
34+
}
35+
36+
writeFileSync(outPath, JSON.stringify(enriched, null, 2) + '\n', 'utf8')
37+
logger.success(`wrote ${path.relative(rootDir, outPath)}`)

scripts/xport-schema.mts

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
/**
2+
* @fileoverview TypeBox schema for xport.json — single source of truth.
3+
*
4+
* Everything else is derived:
5+
* - TypeScript types in scripts/xport.mts via `Static<typeof ...>`
6+
* - xport.schema.json (draft 2020-12) via direct JSON.stringify of the
7+
* TypeBox schema, emitted by scripts/xport-emit-schema.mts
8+
* - Runtime validation at harness startup via
9+
* `validateSchema(XportManifestSchema, ...)` from
10+
* `@socketsecurity/lib/validation/validate-schema`
11+
*
12+
* Byte-identical across socket-tui / socket-btm / socket-sdxgen / ultrathink /
13+
* socket-registry / socket-repo-template via sync-scaffolding.mjs.
14+
*/
15+
16+
import { Type, type Static } from '@sinclair/typebox'
17+
18+
// ---------------------------------------------------------------------------
19+
// Shared primitives.
20+
// ---------------------------------------------------------------------------
21+
22+
const IdSchema = Type.String({
23+
pattern: '^[a-z0-9][A-Za-z0-9-]*$',
24+
description:
25+
'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).',
26+
})
27+
28+
const CriticalitySchema = Type.Integer({
29+
minimum: 1,
30+
maximum: 10,
31+
description:
32+
'Stay-in-step importance (1 = cosmetic, 10 = security-sensitive). Harness surfaces high-criticality drift louder.',
33+
})
34+
35+
const UpstreamRefSchema = Type.String({
36+
description: 'Key into the top-level `upstreams` map.',
37+
})
38+
39+
const ConformanceTestSchema = Type.String({
40+
description:
41+
"Path to a test that enforces behavior parity (modulo documented deviations). Strongly recommended — static checks can't catch silent behavioral drift.",
42+
})
43+
44+
const NotesSchema = Type.String({
45+
description:
46+
'Free-form context — why this row exists, what gotchas to watch for.',
47+
})
48+
49+
const PortStatusSchema = Type.Object(
50+
{
51+
status: Type.Union([Type.Literal('implemented'), Type.Literal('opt-out')]),
52+
reason: Type.Optional(
53+
Type.String({
54+
description: 'Required when status is `opt-out`. Explain why.',
55+
}),
56+
),
57+
path: Type.Optional(
58+
Type.String({
59+
description:
60+
"Optional path to the port's implementation of this row. Useful for module-inventory rows where each language points at a different directory.",
61+
}),
62+
),
63+
note: Type.Optional(
64+
Type.String({
65+
description:
66+
"Optional free-form note attached to a specific port's status.",
67+
}),
68+
),
69+
},
70+
{
71+
additionalProperties: false,
72+
description:
73+
'Per-port status for a lang-parity row. `implemented` = port meets assertions; `opt-out` = port consciously skips, requires non-empty `reason`.',
74+
},
75+
)
76+
77+
const UpstreamSchema = Type.Object(
78+
{
79+
submodule: Type.String({
80+
description: 'Submodule path, relative to repo root.',
81+
}),
82+
repo: Type.String({
83+
pattern: '^https?://',
84+
description: 'Upstream repository URL (http:// or https://).',
85+
}),
86+
},
87+
{ additionalProperties: false },
88+
)
89+
90+
const SiteSchema = Type.Object(
91+
{
92+
path: Type.String({
93+
description: "Path to the port's root directory, relative to repo root.",
94+
}),
95+
language: Type.Optional(
96+
Type.String({ description: 'Language label, for human reports.' }),
97+
),
98+
},
99+
{ additionalProperties: false },
100+
)
101+
102+
const FixtureCheckSchema = Type.Object(
103+
{
104+
fixture_path: Type.String(),
105+
snapshot_path: Type.Optional(Type.String()),
106+
diff_tolerance: Type.Optional(
107+
Type.Union([
108+
Type.Literal('exact'),
109+
Type.Literal('line-by-line'),
110+
Type.Literal('semantic'),
111+
]),
112+
),
113+
},
114+
{
115+
additionalProperties: false,
116+
description:
117+
"Golden-input verification. Prefer snapshot-based diffs over hardcoded counts (brittleness lesson from sdxgen's lock-step-features).",
118+
},
119+
)
120+
121+
// ---------------------------------------------------------------------------
122+
// Row kinds.
123+
// ---------------------------------------------------------------------------
124+
125+
const FileForkRowSchema = Type.Object(
126+
{
127+
kind: Type.Literal('file-fork'),
128+
id: IdSchema,
129+
upstream: UpstreamRefSchema,
130+
criticality: Type.Optional(CriticalitySchema),
131+
conformance_test: Type.Optional(ConformanceTestSchema),
132+
notes: Type.Optional(NotesSchema),
133+
local: Type.String({
134+
description: 'Path to our ported file, relative to repo root.',
135+
}),
136+
upstream_path: Type.String({
137+
description: 'Path to the source file within the upstream submodule.',
138+
}),
139+
forked_at_sha: Type.String({
140+
pattern: '^[0-9a-f]{40}$',
141+
description:
142+
'Full 40-char SHA of the upstream commit we forked from. Harness runs `git log <sha>..HEAD -- <upstream_path>` to surface drift.',
143+
}),
144+
deviations: Type.Array(Type.String(), {
145+
minItems: 1,
146+
description:
147+
"Human-readable list of intentional differences. Zero deviations = use upstream directly; don't fork.",
148+
}),
149+
},
150+
{
151+
additionalProperties: false,
152+
description:
153+
'A local file derived from an upstream file with intentional modifications. Drift = upstream moved forward without us.',
154+
},
155+
)
156+
157+
const VersionPinRowSchema = Type.Object(
158+
{
159+
kind: Type.Literal('version-pin'),
160+
id: IdSchema,
161+
upstream: UpstreamRefSchema,
162+
criticality: Type.Optional(CriticalitySchema),
163+
conformance_test: Type.Optional(ConformanceTestSchema),
164+
notes: Type.Optional(NotesSchema),
165+
pinned_sha: Type.String({
166+
pattern: '^[0-9a-f]{40}$',
167+
description: 'Full 40-char SHA the submodule is pinned to.',
168+
}),
169+
pinned_tag: Type.Optional(
170+
Type.String({
171+
description:
172+
'Human-readable release tag (e.g., `v3.2.1`). Optional — the SHA is authoritative.',
173+
}),
174+
),
175+
upgrade_policy: Type.Union(
176+
[
177+
Type.Literal('track-latest'),
178+
Type.Literal('major-gate'),
179+
Type.Literal('locked'),
180+
],
181+
{
182+
description:
183+
'track-latest: any new release is actionable; major-gate: only major bumps require review; locked: explicit decision per upgrade.',
184+
},
185+
),
186+
},
187+
{
188+
additionalProperties: false,
189+
description:
190+
"A submodule pinned to an upstream release. Drift = upstream cut a new release we haven't adopted.",
191+
},
192+
)
193+
194+
const FeatureParityRowSchema = Type.Object(
195+
{
196+
kind: Type.Literal('feature-parity'),
197+
id: IdSchema,
198+
upstream: UpstreamRefSchema,
199+
criticality: CriticalitySchema,
200+
conformance_test: Type.Optional(ConformanceTestSchema),
201+
notes: Type.Optional(NotesSchema),
202+
local_area: Type.String({
203+
description:
204+
'Path to the local module/directory implementing the feature. Code pattern scan targets this directory (excluding test files).',
205+
}),
206+
test_area: Type.Optional(
207+
Type.String({
208+
description:
209+
'Optional path to the directory where tests for this feature live. When absent, the harness searches inside `local_area`.',
210+
}),
211+
),
212+
code_patterns: Type.Optional(
213+
Type.Array(Type.String(), {
214+
description:
215+
'Regex patterns the local implementation must contain. Prefer anchored patterns (function signatures) over loose keywords to avoid comment false positives.',
216+
}),
217+
),
218+
test_patterns: Type.Optional(
219+
Type.Array(Type.String(), {
220+
description: 'Regex patterns the test suite must contain.',
221+
}),
222+
),
223+
fixture_check: Type.Optional(FixtureCheckSchema),
224+
},
225+
{
226+
additionalProperties: false,
227+
description:
228+
'A behavioral feature reimplemented locally to match upstream behavior. Three-pillar validation: code patterns, test patterns, fixture snapshots.',
229+
},
230+
)
231+
232+
const SpecConformanceRowSchema = Type.Object(
233+
{
234+
kind: Type.Literal('spec-conformance'),
235+
id: IdSchema,
236+
upstream: UpstreamRefSchema,
237+
criticality: Type.Optional(CriticalitySchema),
238+
conformance_test: Type.Optional(ConformanceTestSchema),
239+
notes: Type.Optional(NotesSchema),
240+
local_impl: Type.String(),
241+
spec_version: Type.String(),
242+
spec_path: Type.Optional(
243+
Type.String({
244+
description:
245+
'Path within the upstream submodule to the spec document, if applicable.',
246+
}),
247+
),
248+
},
249+
{
250+
additionalProperties: false,
251+
description:
252+
'A local reimplementation of an external specification. Drift = the spec was revised.',
253+
},
254+
)
255+
256+
// Assertions are deliberately untyped — each matrix area defines its own
257+
// assertion shapes. The harness ignores fields it doesn't recognize.
258+
// Historical precedent: ultrathink's xlang-harness.mts treats this as
259+
// `unknown[]`.
260+
const AssertionSchema = Type.Record(Type.String(), Type.Unknown())
261+
262+
const LangParityRowSchema = Type.Object(
263+
{
264+
kind: Type.Literal('lang-parity'),
265+
id: IdSchema,
266+
name: Type.String(),
267+
description: Type.String(),
268+
category: Type.String({
269+
description:
270+
'Grouping tag. `rejected` is reserved for anti-patterns (every port must be opt-out; reintroduction exits 2).',
271+
}),
272+
criticality: Type.Optional(CriticalitySchema),
273+
conformance_test: Type.Optional(ConformanceTestSchema),
274+
notes: Type.Optional(NotesSchema),
275+
assertions: Type.Optional(
276+
Type.Array(AssertionSchema, {
277+
description:
278+
'Open-ended assertion list. Each has a `kind` string the harness dispatches on. Unknown kinds are skipped with a log line.',
279+
}),
280+
),
281+
matrix_files: Type.Optional(
282+
Type.Array(Type.String(), {
283+
description:
284+
'For inventory rows that index other xport-lang-*.json files. Paths relative to this manifest.',
285+
}),
286+
),
287+
ports: Type.Record(Type.String(), PortStatusSchema, {
288+
description: 'Per-site status. Keys must match top-level `sites`.',
289+
}),
290+
},
291+
{
292+
additionalProperties: false,
293+
description:
294+
'N sibling language ports of one spec within a single project. Drift = one port diverged from its siblings.',
295+
},
296+
)
297+
298+
export const RowSchema = Type.Union([
299+
FileForkRowSchema,
300+
VersionPinRowSchema,
301+
FeatureParityRowSchema,
302+
SpecConformanceRowSchema,
303+
LangParityRowSchema,
304+
])
305+
306+
// ---------------------------------------------------------------------------
307+
// Top-level manifest.
308+
// ---------------------------------------------------------------------------
309+
310+
export const XportManifestSchema = Type.Object(
311+
{
312+
$schema: Type.Optional(Type.String()),
313+
description: Type.Optional(Type.String()),
314+
area: Type.Optional(
315+
Type.String({
316+
description:
317+
"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.",
318+
}),
319+
),
320+
includes: Type.Optional(
321+
Type.Array(Type.String(), {
322+
description:
323+
'Relative paths to sub-manifests. Top-level `upstreams` and `sites` maps override any same-keyed entries in included manifests.',
324+
}),
325+
),
326+
upstreams: Type.Optional(
327+
Type.Record(Type.String(), UpstreamSchema, {
328+
description:
329+
'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.',
330+
}),
331+
),
332+
sites: Type.Optional(
333+
Type.Record(Type.String(), SiteSchema, {
334+
description:
335+
'Named sibling ports (typically per-language). Referenced by rows[].ports.<site> on lang-parity rows. Omit when the manifest has no lang-parity rows.',
336+
}),
337+
),
338+
rows: Type.Array(RowSchema),
339+
},
340+
{
341+
description:
342+
'Unified lock-step manifest shared across Socket repos. One schema, all cases — `kind` discriminator on each row selects which flavor of lock-step applies.',
343+
},
344+
)
345+
346+
export type Row = Static<typeof RowSchema>
347+
export type XportManifest = Static<typeof XportManifestSchema>
348+
export type Upstream = Static<typeof UpstreamSchema>
349+
export type Site = Static<typeof SiteSchema>
350+
export type PortStatus = Static<typeof PortStatusSchema>
351+
export type FileForkRow = Static<typeof FileForkRowSchema>
352+
export type VersionPinRow = Static<typeof VersionPinRowSchema>
353+
export type FeatureParityRow = Static<typeof FeatureParityRowSchema>
354+
export type SpecConformanceRow = Static<typeof SpecConformanceRowSchema>
355+
export type LangParityRow = Static<typeof LangParityRowSchema>

0 commit comments

Comments
 (0)