Files
deepseek-harness/scripts/persistence-schema-model.spec.ts
T
Tianyi Cui a6981c56f6 feat(persistence): add stable catalogs and policy-aware schema review
Traversal ordinals and first-owner paths gave shared persistence types
unstable display names. Keep authored names and source locations as metadata,
use structural digests for identity, and render complete kind/form groups
with bounded array labels. Preserve historical rendering and stored graphs.

Record supported source compatibility promises with their property binding,
policy version, discriminator, preservation rule, and qualified kind set.
The extractor and classifier require explicit opt-ins in both compared
schemas; ordinary or unsupported changes retain strict classification.
Policy-bearing graphs use the format-2 fingerprint domain while unannotated
graphs keep their format-1 fingerprints. Runtime owners activate bindings
only with the corresponding reader behavior; this tooling layer activates
none and changes no package source or Session generation.

Provide a read-only before/after review command that groups shared structural
evidence without replacing authoritative compatibility diagnostics. Match
changed alternatives only through unique literal discriminators and retain
ambiguous additions and removals as independent facts.

At the X baseline, all 61 roots and 472 schemas, digests, and source fields
are unchanged. The inventory remains format 1 and removes only 296 synthetic
names. Existing acknowledgement history still matches every root. All 195
focused tooling tests, Host build/full lint, and bilingual pairing pass.
2026-09-18 19:05:32 +08:00

110 lines
6.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { canonicalizeSchema, isArbitraryJsonSchema, schemaDigest, type SchemaNode, type SourceCompatibility } from './persistence-schema-model.ts'
const string: SchemaNode = { kind: 'primitive', type: 'string' }
const number: SchemaNode = { kind: 'primitive', type: 'number' }
describe('canonical persisted type graphs', () => {
it('ignores source ordering and shared versus duplicated subtypes', () => {
const left: SchemaNode[] = [
{ kind: 'object', properties: [{ name: 'b', type: 1, optional: false }, { name: 'a', type: 1, optional: true }], indices: [] }, string,
]
const right: SchemaNode[] = [
string, string,
{ kind: 'object', properties: [{ name: 'a', type: 0, optional: true }, { name: 'b', type: 1, optional: false }], indices: [] },
]
expect(canonicalizeSchema(left, 0)).toEqual(canonicalizeSchema(right, 2))
})
it('minimizes differently factored recursive aliases to the same graph', () => {
const self: SchemaNode[] = [
{ kind: 'object', properties: [{ name: 'next', type: 0, optional: true }, { name: 'value', type: 1, optional: false }], indices: [] }, string,
]
const mutual: SchemaNode[] = [
{ kind: 'object', properties: [{ name: 'value', type: 2, optional: false }, { name: 'next', type: 1, optional: true }], indices: [] },
{ kind: 'object', properties: [{ name: 'next', type: 0, optional: true }, { name: 'value', type: 3, optional: false }], indices: [] }, string, string,
]
const normalized = canonicalizeSchema(self, 0)
expect(canonicalizeSchema(mutual, 0)).toEqual(normalized)
expect(canonicalizeSchema(normalized.nodes, 0)).toEqual(normalized)
mutual[3] = number
expect(schemaDigest(canonicalizeSchema(mutual, 0))).not.toBe(schemaDigest(normalized))
})
it('flattens reordered unions and removes structurally equal alternatives', () => {
const flat: SchemaNode[] = [{ kind: 'union', types: [1, 2] }, string, number]
const factored: SchemaNode[] = [string, number, { kind: 'union', types: [0, 3] }, { kind: 'union', types: [1, 0] }]
expect(canonicalizeSchema(flat, 0)).toEqual(canonicalizeSchema(factored, 2))
expect(canonicalizeSchema([{ kind: 'union', types: [1, 2] }, string, string], 0)).toEqual(canonicalizeSchema([string], 0))
expect(canonicalizeSchema([{ kind: 'union', types: [1, 2] }, { kind: 'literal', value: true }, { kind: 'literal', value: false }], 0))
.toEqual(canonicalizeSchema([{ kind: 'primitive', type: 'boolean' }], 0))
})
it('ignores recursive union, field, index signature and graph numbering order together', () => {
const original: SchemaNode[] = [
{ kind: 'union', types: [1, 2, 3] },
{ kind: 'object', properties: [{ name: 'label', type: 5, optional: false }, { name: 'next', type: 0, optional: true }], indices: [] },
{ kind: 'array', element: 0 },
{ kind: 'object', properties: [], indices: [{ key: 5, value: 0 }, { key: 4, value: 1 }] },
number,
string,
]
const reordered: SchemaNode[] = [
string,
{ kind: 'object', properties: [], indices: [{ key: 3, value: 5 }, { key: 0, value: 4 }] },
{ kind: 'array', element: 4 },
number,
{ kind: 'union', types: [1, 2, 5] },
{ kind: 'object', properties: [{ name: 'next', type: 4, optional: true }, { name: 'label', type: 0, optional: false }], indices: [] },
]
expect(canonicalizeSchema(reordered, 4)).toEqual(canonicalizeSchema(original, 0))
})
it('preserves tuple positions, element absence, and rest elements', () => {
const baseline: SchemaNode[] = [{ kind: 'tuple', elements: [{ type: 1, optional: false, rest: false }, { type: 2, optional: true, rest: false }] }, string, number]
const reversed: SchemaNode[] = [{ kind: 'tuple', elements: [{ type: 2, optional: false, rest: false }, { type: 1, optional: true, rest: false }] }, string, number]
const rest: SchemaNode[] = [{ kind: 'tuple', elements: [{ type: 1, optional: false, rest: false }, { type: 2, optional: false, rest: true }] }, string, number]
const digest = schemaDigest(canonicalizeSchema(baseline, 0))
expect(schemaDigest(canonicalizeSchema(reversed, 0))).not.toBe(digest)
expect(schemaDigest(canonicalizeSchema(rest, 0))).not.toBe(digest)
})
it('recognizes arbitrary recursive JSON structurally and retains its definition', () => {
const json: SchemaNode[] = [
{ kind: 'union', types: [1, 2, 3, 4, 5, 6] },
{ kind: 'primitive', type: 'null' }, { kind: 'primitive', type: 'boolean' }, number, string,
{ kind: 'array', element: 0 }, { kind: 'object', properties: [], indices: [{ key: 4, value: 0 }] },
]
const schema = canonicalizeSchema(json, 0)
expect(isArbitraryJsonSchema(schema)).toBe(true)
expect(schema.nodes.some(node => node.kind === 'array' && node.element === 0)).toBe(true)
json[0] = { kind: 'union', types: [1, 2, 3, 4, 5] }
expect(isArbitraryJsonSchema(canonicalizeSchema(json, 0))).toBe(false)
})
it('rejects missing references and unproductive union cycles', () => {
expect(() => canonicalizeSchema([{ kind: 'array', element: 5 }], 0)).toThrow('missing node 5')
expect(() => canonicalizeSchema([{ kind: 'union', types: [0] }], 0)).toThrow('union cycle')
})
})
describe('policy-aware graph normalization', () => {
it('normalizes recorded policy fields and kind order without changing unrelated graph digests', () => {
const policy: SourceCompatibility = { version: 1, policy: 'session-source-attribution', binding: 'session.user-message.source',
discriminator: 'kind', unknownKinds: 'preserve', attributionKinds: ['b', 'a'] }
const object = (compatibility: SourceCompatibility): SchemaNode => ({ kind: 'object', indices: [], properties: [
{ name: 'source', type: 1, optional: false, compatibility },
] })
const reordered = { attributionKinds: ['a', 'b', 'a'], unknownKinds: policy.unknownKinds, discriminator: policy.discriminator,
binding: policy.binding, policy: policy.policy, version: policy.version }
const left = canonicalizeSchema([object(policy), string], 0)
const right = canonicalizeSchema([object(reordered), string], 0)
expect(left).toEqual(right)
expect(schemaDigest(left)).toBe(schemaDigest(right))
const unannotated = canonicalizeSchema([{ kind: 'object', indices: [], properties: [{ name: 'source', type: 1, optional: false }] }, string], 0)
expect(schemaDigest(left)).not.toBe(schemaDigest(unannotated))
expect(schemaDigest(canonicalizeSchema(left.nodes, 1))).toBe(schemaDigest(canonicalizeSchema([string], 0)))
})
})