mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
fix: preserve independent persistence type graphs
This commit is contained in:
@@ -15,8 +15,8 @@
|
||||
en: 5fca19ac2bb1ddac
|
||||
zh: 00d78fe7e80ba4f7
|
||||
/cookbook-reviewing-persistence-type-changes/optional-inspect-the-change:
|
||||
en: 39443a54ee68b24a
|
||||
zh: cdece79db4977116
|
||||
en: 3f85edb803afe870
|
||||
zh: 545c651b9e5ccb56
|
||||
/cookbook-reviewing-persistence-type-changes/1-record-the-change:
|
||||
en: 342bd49412db070e
|
||||
zh: a8d14958e9a637d9
|
||||
|
||||
@@ -41,7 +41,7 @@ pnpm --silent run persistence-review --before .artifacts/base.schema.json --afte
|
||||
|
||||
Record the commits supplying those files with the report. Add `--json` for structured output. This read-only comparison groups shared changes with their affected roots and uses actual literal `kind`/`form` values instead of union positions. Ambiguous alternatives remain separate additions and removals. Its compatibility section copies every root's authoritative classifier result; the structural explanation does not replace acknowledgement checks. Current catalog labels and declaration names are descriptive metadata; structural anchors and fingerprints identify types.
|
||||
|
||||
The current machine inventory stores complete graphs in `roots`; each `types` entry contains only `digest`, `names`, and `sources`. Readers reconstruct type graphs from root subgraphs by digest and also accept historical entries with an explicit `schema`. `formatVersion` identifies normalization rules; storage compaction does not change root fingerprints or require an acknowledgement.
|
||||
The current machine inventory stores complete graphs in `roots`. A `types` entry contains `digest`, `names`, and `sources`, plus an explicit `schema` when roots cannot reconstruct that graph exactly. Readers resolve omitted graphs from root subgraphs by digest and validate explicit graphs directly, preserving every type and its metadata. Historical full entries remain readable. `formatVersion` identifies normalization rules; storage compaction does not change root fingerprints or require an acknowledgement.
|
||||
|
||||
<a id="acknowledge"></a>
|
||||
## 1. Record the change
|
||||
|
||||
@@ -41,7 +41,7 @@ pnpm --silent run persistence-review --before .artifacts/base.schema.json --afte
|
||||
|
||||
在报告旁记录这些文件对应的 commit。添加 `--json` 可获取结构化输出。此只读比较将共享变更与受影响的根类型归组,使用实际字面量 `kind`/`form` 值代替联合类型位置。无法唯一匹配的候选项保留为独立的新增与删除。兼容性部分复制每个根类型的权威分类结果;结构说明不替代确认检查。当前目录标签和声明名称是描述元数据;结构锚点和指纹标识类型。
|
||||
|
||||
当前机器清单在 `roots` 中保存完整图;每个 `types` 条目只包含 `digest`、`names` 和 `sources`。读取器按摘要从根的子图重建类型图,同时接受带有显式 `schema` 的历史条目。`formatVersion` 标识规范化规则;存储压缩不改变根指纹,也不需要确认记录。
|
||||
当前机器清单在 `roots` 中保存完整图。每个 `types` 条目包含 `digest`、`names` 和 `sources`;若根无法精确重建该图,条目还会保存显式 `schema`。读取器按摘要从根的子图恢复省略的图,并直接验证显式图,保留每个类型及其元数据。历史完整条目仍然可读。`formatVersion` 标识规范化规则;存储压缩不改变根指纹,也不需要确认记录。
|
||||
|
||||
<a id="acknowledge"></a>
|
||||
## 1. 记录变更
|
||||
|
||||
@@ -1260,8 +1260,11 @@
|
||||
en: a0d0018c68abc564
|
||||
zh: 299d86dbba56b528
|
||||
/session-persistence-event-catalog/resolved-persistence-types/boolean:
|
||||
en: b121d42640099ec1
|
||||
zh: b121d42640099ec1
|
||||
en: 40616917f0b8f9cc
|
||||
zh: 40616917f0b8f9cc
|
||||
/session-persistence-event-catalog/resolved-persistence-types/false:
|
||||
en: 0e3a8e3c2c62bdd7
|
||||
zh: 0e3a8e3c2c62bdd7
|
||||
/session-persistence-event-catalog/resolved-persistence-types/never~2:
|
||||
en: a35333b49c104e92
|
||||
zh: a35333b49c104e92
|
||||
|
||||
@@ -5176,6 +5176,14 @@ SHA-256: `aa93010d0e521ad7c42273f58d5c9b8bf1dfc94edf1b805a3971b364a35c1877`
|
||||
|
||||
`boolean`
|
||||
|
||||
<a id="persistence-type-sha256-0c4205180f75b9ee1e0d878bd9a9851628b5dc18448486389d9c5fdc3f1ccda3"></a>
|
||||
|
||||
### `false`
|
||||
|
||||
SHA-256: `0c4205180f75b9ee1e0d878bd9a9851628b5dc18448486389d9c5fdc3f1ccda3`
|
||||
|
||||
`false`
|
||||
|
||||
<a id="persistence-type-sha256-fcf427290c6213c2a7feb57492d832ce324f5b9737a759f424dab69e6bd101a0"></a>
|
||||
|
||||
### `never`
|
||||
|
||||
@@ -5178,6 +5178,14 @@ SHA-256: `aa93010d0e521ad7c42273f58d5c9b8bf1dfc94edf1b805a3971b364a35c1877`
|
||||
|
||||
`boolean`
|
||||
|
||||
<a id="persistence-type-sha256-0c4205180f75b9ee1e0d878bd9a9851628b5dc18448486389d9c5fdc3f1ccda3"></a>
|
||||
|
||||
### `false`
|
||||
|
||||
SHA-256: `0c4205180f75b9ee1e0d878bd9a9851628b5dc18448486389d9c5fdc3f1ccda3`
|
||||
|
||||
`false`
|
||||
|
||||
<a id="persistence-type-sha256-fcf427290c6213c2a7feb57492d832ce324f5b9737a759f424dab69e6bd101a0"></a>
|
||||
|
||||
### `never`
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 14c57f30e676f6e3
|
||||
zh: b1a0ccf26fab3727
|
||||
/session-persistence-formats/format-references:
|
||||
en: 528c6482cbafbc3e
|
||||
zh: 6215d623ca04df65
|
||||
en: 45dba01dd6d69c80
|
||||
zh: c366d1a3991118c9
|
||||
/session-persistence-formats/scope-and-evidence:
|
||||
en: b220062139ddac1e
|
||||
zh: 7c4c0efe05dafa9c
|
||||
|
||||
@@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant.
|
||||
| 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 |
|
||||
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 |
|
||||
| 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 |
|
||||
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 586 |
|
||||
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 587 |
|
||||
|
||||
<!-- persistence-format-index:end -->
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式
|
||||
| 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 |
|
||||
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 |
|
||||
| 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 |
|
||||
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 586 |
|
||||
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 587 |
|
||||
|
||||
<!-- persistence-format-index:end -->
|
||||
|
||||
|
||||
Generated
+14
@@ -21808,6 +21808,20 @@
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "0c4205180f75b9ee1e0d878bd9a9851628b5dc18448486389d9c5fdc3f1ccda3",
|
||||
"schema": {
|
||||
"root": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "0d47a8f3d847243c4488e755ad4cab7a04ac5d4621b9c81865524cf3f4d8536f",
|
||||
"names": [],
|
||||
|
||||
@@ -109,7 +109,7 @@ function validateInventory(inventory: PersistenceSchemaInventory, version: numbe
|
||||
for (const type of inventory.types) {
|
||||
if (listed.has(type.digest)) throw new Error(`${label}: duplicate schema type ${type.digest}`)
|
||||
listed.add(type.digest)
|
||||
// Legacy current inventories can retain types erased by normalization; the generator gate checks current output.
|
||||
// The current extractor can retain types erased by normalization; its generator gate checks that inventory.
|
||||
if (!current && !reachable.has(type.digest)) throw new Error(`${label}: unreferenced schema type ${type.digest}`)
|
||||
remaining.delete(type.digest)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
|
||||
/** Recorded reader promise for explicitly attributed source additions. */
|
||||
export interface SourceCompatibility {
|
||||
@@ -71,8 +72,10 @@ export interface PersistenceSchemaInventory {
|
||||
readonly types: readonly PersistenceType[]
|
||||
}
|
||||
|
||||
/** On-disk type metadata; complete graphs are stored only in roots. */
|
||||
export type PersistenceTypeIndex = Omit<PersistenceType, 'schema'>
|
||||
/** Type metadata with an explicit graph when roots cannot reconstruct it exactly. */
|
||||
export interface PersistenceTypeIndex extends Omit<PersistenceType, 'schema'> {
|
||||
readonly schema?: CanonicalSchema
|
||||
}
|
||||
|
||||
/** Compact inventory representation; formatVersion still pins normalization. */
|
||||
export interface PersistenceSchemaSnapshot {
|
||||
@@ -98,15 +101,21 @@ export function reachableSchemaTypes(schemas: readonly CanonicalSchema[]): Map<s
|
||||
}
|
||||
|
||||
/**
|
||||
* Omit redundant type graphs from a current-source inventory for serialization.
|
||||
* @param inventory - complete inventory whose types are reachable from its roots.
|
||||
* @returns root graphs and their type metadata, preserving inventory order.
|
||||
* Omit type graphs exactly reconstructable from roots, retaining all other graphs.
|
||||
* @param inventory - expanded inventory, including types erased from roots by normalization.
|
||||
* @returns a lossless snapshot preserving root graphs, type order, and declaration metadata.
|
||||
*/
|
||||
export function persistenceSchemaSnapshot(inventory: PersistenceSchemaInventory): PersistenceSchemaSnapshot {
|
||||
const reachable = reachableSchemaTypes(inventory.roots.map(root => root.schema))
|
||||
return {
|
||||
formatVersion: inventory.formatVersion,
|
||||
roots: inventory.roots,
|
||||
types: inventory.types.map(({ digest, names, sources }) => ({ digest, names, sources })),
|
||||
types: inventory.types.map(({ digest, schema, names, sources }) => ({
|
||||
digest,
|
||||
...(isDeepStrictEqual(schema, reachable.get(digest)) ? {} : { schema }),
|
||||
names,
|
||||
sources,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,25 @@ describe('compact persistence snapshots', () => {
|
||||
expect(parseHistoricalPersistenceSnapshot(snapshot)).toEqual(original)
|
||||
})
|
||||
|
||||
it.each([true, false])('retains independent recursive types with root graphs=%s', (withRoots) => {
|
||||
const base = inventory()
|
||||
const schema = canonicalizeSchema([
|
||||
{ kind: 'object', properties: [{ name: 'next', type: 0, optional: true }, { name: 'flag', type: 1, optional: false }], indices: [] },
|
||||
{ kind: 'literal', value: false },
|
||||
], 0)
|
||||
const independent = { digest: schemaDigest(schema), schema, names: ['example.ts#Independent', 'example.ts#Alias'], sources: ['example.ts'] }
|
||||
const original = { ...base, roots: withRoots ? base.roots : [], types: [independent, ...base.types] }
|
||||
const snapshot = persistenceSchemaSnapshot(original)
|
||||
expect(snapshot.types[0]).toEqual(independent)
|
||||
if (withRoots) expect(snapshot.types.slice(1).every(type => type.schema === undefined)).toBe(true)
|
||||
else expect(snapshot.types).toEqual(original.types)
|
||||
const restored = parsePersistenceSnapshot(JSON.parse(JSON.stringify(snapshot)))
|
||||
expect(restored).toEqual(original)
|
||||
expect(parseHistoricalPersistenceSnapshot(snapshot)).toEqual(original)
|
||||
expect(persistenceSchemaSnapshot(restored)).toEqual(snapshot)
|
||||
expect(renderPersistenceSchemaDefinitions(restored)).toBe(renderPersistenceSchemaDefinitions(original))
|
||||
})
|
||||
|
||||
it('reads legacy full graphs and keeps partial acknowledgement inventories empty', () => {
|
||||
const original = inventory()
|
||||
expect(parsePersistenceSnapshot(JSON.parse(JSON.stringify(original)))).toEqual(original)
|
||||
|
||||
@@ -56,17 +56,6 @@ function event(inventory: PersistenceSchemaInventory, name = 'test/record'): str
|
||||
}
|
||||
|
||||
describe('persistent source type extraction', () => {
|
||||
it('indexes normalized reachable types without erased boolean literals', () => {
|
||||
const root = fixture('export interface Payload { enabled: boolean; next?: Payload }')
|
||||
const inventory = extractPersistenceSchema(root)
|
||||
const reachable = new Set(inventory.roots.flatMap(root => root.schema.nodes
|
||||
.map((_, index) => schemaDigest(canonicalizeSchema(root.schema.nodes, index)))))
|
||||
expect(new Set(inventory.types.map(type => type.digest))).toEqual(reachable)
|
||||
expect(inventory.types.some(type => type.schema.nodes[0]?.kind === 'primitive' && type.schema.nodes[0].type === 'boolean')).toBe(true)
|
||||
expect(inventory.types.some(type => type.schema.nodes[0]?.kind === 'literal' && type.schema.nodes[0].value === false)).toBe(false)
|
||||
expect(inventory.types.some(type => type.names.some(name => name.endsWith('#Payload')))).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps authored names and anonymous declaration locations without synthesized traversal names', () => {
|
||||
const root = fixture('interface Section { text: string }\nexport interface Payload { inserted: { source: { kind: "hooks-codex"; sections: Section[] } }[] }')
|
||||
const inventory = extractPersistenceSchema(root)
|
||||
|
||||
@@ -9,7 +9,6 @@ import { validSourceCompatibility } from './persistence-source-policy.ts'
|
||||
import {
|
||||
canonicalizeSchema,
|
||||
schemaChildren,
|
||||
reachableSchemaTypes,
|
||||
schemaDigest,
|
||||
schemaHasCompatibility,
|
||||
type PersistenceRoot,
|
||||
@@ -437,18 +436,15 @@ class SchemaExtractor {
|
||||
for (const child of schemaChildren(node)) visit(child)
|
||||
}
|
||||
for (const input of inputs) visit(input.node)
|
||||
const types = new Map<string, { schema: PersistenceType['schema']; names: Set<string>; sources: Set<string> }>(
|
||||
[...reachableSchemaTypes(roots.map(root => root.schema))].map(([digest, schema]) =>
|
||||
[digest, { schema, names: new Set<string>(), sources: new Set<string>() }]),
|
||||
)
|
||||
const types = new Map<string, { schema: PersistenceType['schema']; names: Set<string>; sources: Set<string> }>()
|
||||
for (const id of found) {
|
||||
const schema = canonicalizeSchema(this.nodes, id)
|
||||
const digest = schemaDigest(schema)
|
||||
const item = types.get(digest)
|
||||
if (item === undefined) continue
|
||||
const item = types.get(digest) ?? { schema, names: new Set<string>(), sources: new Set<string>() }
|
||||
const declarationMetadata = this.declarationMetadata.get(id)
|
||||
for (const name of declarationMetadata?.names ?? []) item.names.add(name)
|
||||
for (const source of declarationMetadata?.sources ?? []) item.sources.add(source)
|
||||
types.set(digest, item)
|
||||
}
|
||||
return {
|
||||
formatVersion: roots.some(root => schemaHasCompatibility(root.schema)) ? 2 : 1,
|
||||
|
||||
Reference in New Issue
Block a user