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.
70 lines
3.6 KiB
TypeScript
70 lines
3.6 KiB
TypeScript
/** Literal source groups and the supported reader promise saved in schema history. */
|
|
|
|
import type { SchemaNode, SchemaProperty, SourceCompatibility } from './persistence-schema-model.ts'
|
|
|
|
// Reserve role and shared-consumer identities, frozen rename targets, and request-only producer identities against reuse.
|
|
// Existing kinds outside this set also reject retroactive qualification during comparison.
|
|
const RESERVED_SOURCE_KINDS = new Set([
|
|
'user', 'model', 'tool', 'system-prompt', 'compact-checkpoint', 'dsh-session-title-llm', 'tool-registry', 'runtime-context', 'plugin',
|
|
'auto-review', 'compact-basic', 'code-mode',
|
|
])
|
|
|
|
/**
|
|
* Group source alternatives by their required literal wire discriminator.
|
|
* @param nodes - self-contained type graph.
|
|
* @param index - source property value.
|
|
* @returns kind groups, or undefined when any alternative is not a closed source record.
|
|
*/
|
|
export function sourceKindGroups(nodes: readonly SchemaNode[], index: number): Map<string, number[]> | undefined {
|
|
const source = nodes[index]
|
|
const alternatives = source?.kind === 'union' ? source.types : [index]
|
|
const groups = new Map<string, number[]>()
|
|
for (const alternative of alternatives) {
|
|
const node = nodes[alternative]
|
|
if (node?.kind !== 'object') return undefined
|
|
const property = node.properties.find(property => property.name === 'kind')
|
|
const kind = property === undefined ? undefined : nodes[property.type]
|
|
if (property?.optional !== false || kind?.kind !== 'literal' || typeof kind.value !== 'string' || kind.value.length === 0) return undefined
|
|
const group = groups.get(kind.value) ?? []
|
|
group.push(alternative)
|
|
groups.set(kind.value, group)
|
|
}
|
|
return groups
|
|
}
|
|
|
|
/**
|
|
* Validate the supported policy and its containing message/source declaration.
|
|
* @param nodes - self-contained graph.
|
|
* @param owner - object carrying the explicitly bound source property.
|
|
* @param property - bound source property.
|
|
* @returns whether the metadata authorizes source-group comparison.
|
|
*/
|
|
export function validSourceCompatibility(
|
|
nodes: readonly SchemaNode[], owner: SchemaNode, property: SchemaProperty,
|
|
): boolean {
|
|
const policy = property.compatibility
|
|
if (policy === undefined || property.name !== 'source' || property.optional || owner.kind !== 'object') return false
|
|
const roleProperty = owner.properties.find(property => property.name === 'role')
|
|
const role = roleProperty === undefined ? undefined : nodes[roleProperty.type]
|
|
if (roleProperty?.optional !== false || role?.kind !== 'literal'
|
|
|| !(role.value === 'user' && policy.binding === 'session.user-message.source'
|
|
|| role.value === 'developer' && policy.binding === 'session.developer-message.source')) return false
|
|
const groups = sourceKindGroups(nodes, property.type)
|
|
return groups !== undefined && policy.attributionKinds.every(kind => groups.has(kind) && !RESERVED_SOURCE_KINDS.has(kind))
|
|
}
|
|
|
|
/**
|
|
* Check that both recorded promises agree and no existing kind changes qualification.
|
|
* Both inputs are validated v1 policies with fixed policy, discriminator, and preservation fields.
|
|
* @param before - predecessor promise.
|
|
* @param after - successor promise.
|
|
* @param existingKinds - predecessor wire kinds, including semantic groups.
|
|
* @returns whether the additive rule applies to this transition.
|
|
*/
|
|
export function matchingSourceCompatibility(
|
|
before: SourceCompatibility, after: SourceCompatibility, existingKinds: Iterable<string>,
|
|
): boolean {
|
|
return before.binding === after.binding
|
|
&& [...existingKinds].every(kind => before.attributionKinds.includes(kind) === after.attributionKinds.includes(kind))
|
|
}
|