Skip to content

Commit 0527531

Browse files
iguangfeiclaude
andcommitted
fix(cloud): prstGeom 不再造出幽灵节点,嵌入对象核对改数个数
带表格的真实文档暴露了两处相互掩盖的缺陷: 1. **prstGeom 被当成内容下钻。** 它是预设几何描述符,和 style / xfrm 一样描述形状怎么画。 但它自带 type(如 flowChartMagneticDisk),而建节点的判据是子串命中——"flowchart…" 里就含着 "chart"。实测一份文档因此多出 18 个空节点(flowchartmagneticdisk × 9、 flowchartpunchedtape × 4、flowchartmanualinput × 4、chart × 1 的其中一部分)。 加进 WALK_SKIP_KEYS 后这些节点全部消失。 2. **嵌入对象核对被上面那些幽灵节点骗过。** 原判据是「产物里有没有 chart/graphic_frame 之类的节点」,一个幽灵 chart 就足以让它收声。实测这份文档:源里 1 张图表 + 1 个 SmartArt + 12 个嵌入对象,产物只有一个图表空壳,却判 pass。 改成数个数:probe 给得出部件数时要求「表示的数量 ≥ 部件数」,给不出时才退回布尔判据。 同一份文档现在报 embedded_object_undetected {parts: 2, represented: 1} → degraded。 EMBEDDED_TYPES 补上 diagram / graphic,与 ShapeType 对齐。 回归:99 页那份仍是 heading 96、embedded_object_undetected 一条;带表格那份的表格内容 与本地逐字一致(table 1 / row 7 / cell 14,MD 8 行)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9715225 commit 0527531

3 files changed

Lines changed: 32 additions & 10 deletions

File tree

‎src/ir/cloud-adapter.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,15 @@ function walkCloud(value: unknown, nodes: DeckIrNode[], hash: string, locator: s
8787
}
8888
}
8989

90-
/** 遍历时不再下钻的键:要么已在节点上表达,要么由更专门的分支接管。 */
91-
const WALK_SKIP_KEYS = ['style', 'xfrm', 'txBody', 'text', 't', 'name', 'id', 'type'] as const;
90+
/**
91+
* 遍历时不再下钻的键:要么已在节点上表达,要么由更专门的分支接管。
92+
*
93+
* `prstGeom` 是预设几何描述符,和 `style` / `xfrm` 一样描述的是形状怎么画,不是内容。
94+
* 下钻进去会凭空造出节点:它自带 `type`(如 `flowChartMagneticDisk`),而建节点的判据是
95+
* 子串命中 —— "flowchart…" 里就含着 "chart"。实测一份文档因此多出 18 个空节点,还把
96+
* 「产物是否表示了图表」的判断带偏。
97+
*/
98+
const WALK_SKIP_KEYS = ['style', 'xfrm', 'txBody', 'text', 't', 'name', 'id', 'type', 'prstGeom'] as const;
9299

93100
/**
94101
* 把云端表格结构展开成行列节点;认不出行列就返回 false,交回通用遍历。

‎src/quality/assessment.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function assessCandidate(candidate: ParseCandidate, probe?: DeckProbeRepo
7575

7676
/** 解析器表示或报告嵌入对象的方式;命中任一种就说明它知道这些东西存在。 */
7777
const EMBEDDED_REPORTED = new Set(['embedded_object_unsupported', 'graphic_frame_partial', 'chart_partial', 'smartart_partial', 'media_unsupported']);
78-
const EMBEDDED_TYPES = new Set(['graphic_frame', 'chart', 'opaque']);
78+
const EMBEDDED_TYPES = new Set(['graphic_frame', 'chart', 'diagram', 'graphic', 'opaque']);
7979
/** 只在 OOXML 上判:probe 的嵌入对象事实是按 OOXML 部件数得出的,别的格式没有可比口径。 */
8080
const EMBEDDED_FORMATS = new Set(['pptx', 'docx']);
8181

@@ -96,17 +96,20 @@ function exactValue(probe: DeckProbeReport | undefined, target: string): unknown
9696
*/
9797
export function crossCheckEmbeddedObjects(candidate: ParseCandidate, probe?: DeckProbeReport): void {
9898
if (!probe || !EMBEDDED_FORMATS.has(candidate.ir.format)) return;
99-
const parts = ['powerpoint.chart_part_count', 'powerpoint.smartart_data_part_count']
99+
const partCount = ['powerpoint.chart_part_count', 'powerpoint.smartart_data_part_count']
100100
.map((target) => exactValue(probe, target))
101-
.filter((value): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0);
102-
const partCount = parts.reduce((sum, value) => sum + value, 0);
101+
.filter((value): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0)
102+
.reduce((sum, value) => sum + value, 0);
103103
if (partCount === 0 && exactValue(probe, 'security.has_embedded_files') !== true) return;
104104
const quality = candidate.ir.quality;
105105
if (quality.checks.some((check) => EMBEDDED_REPORTED.has(check.code))) return;
106-
if (candidate.ir.document.nodes.some((node) => EMBEDDED_TYPES.has(node.type))) return;
106+
const represented = candidate.ir.document.nodes.filter((node) => EMBEDDED_TYPES.has(node.type)).length;
107+
// 数个数而不是「有没有」:一份文档里 1 张图表 + 1 个 SmartArt,产物只给出一个图表空壳,
108+
// 布尔判据会就此收声,SmartArt 与嵌入对象的缺失就再没人提。部件数拿不到时退回布尔判据。
109+
if (partCount > 0 ? represented >= partCount : represented > 0) return;
107110
quality.checks.push({ code: 'embedded_object_undetected', severity: 'warning',
108-
message: `The source contains embedded objects${partCount ? ` (${partCount} chart/SmartArt parts)` : ''}, but this result neither represents nor reports them; their content cannot be confirmed present.`,
109-
...(partCount ? { detail: { parts: partCount } } : {}) });
111+
message: `The source contains embedded objects${partCount ? ` (${partCount} chart/SmartArt parts, ${represented} represented)` : ''}, but this result neither represents nor reports them; their content cannot be confirmed present.`,
112+
...(partCount ? { detail: { parts: partCount, represented } } : {}) });
110113
quality.status = 'degraded';
111114
// 适配器把同一个 quality 对象同时挂在 candidate 与 ir 上;显式对齐,不依赖这个巧合。
112115
candidate.quality = quality;

‎tests/unit/quality-review.test.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@ describe('review: source vs result cross-check', () => {
132132
expect(subject.ir.quality.checks.map(c => c.code)).toEqual(['embedded_object_unsupported']);
133133
});
134134

135+
it('still reports when the result represents fewer parts than the source has', () => {
136+
// 实测一份文档:源里 1 张图表 + 1 个 SmartArt,产物只给出一个图表空壳。
137+
// 按「有没有」判会就此收声,SmartArt 与嵌入对象的缺失便再没人提。
138+
const subject = candidate([node('n1', '', { type: 'chart' })]);
139+
crossCheckEmbeddedObjects(subject, probe({
140+
'powerpoint.chart_part_count': { status: 'resolved', confidence: 'exact', value: 1 },
141+
'powerpoint.smartart_data_part_count': { status: 'resolved', confidence: 'exact', value: 1 },
142+
}));
143+
expect(subject.ir.quality.checks[0]?.detail).toEqual({ parts: 2, represented: 1 });
144+
expect(subject.ir.quality.status).toBe('degraded');
145+
});
146+
135147
it('stays quiet when the result represents them as nodes', () => {
136148
const subject = candidate([node('n1', '', { type: 'graphic_frame' })]);
137149
crossCheckEmbeddedObjects(subject, embeddedProbe);
@@ -145,7 +157,7 @@ describe('review: source vs result cross-check', () => {
145157
'powerpoint.chart_part_count': { status: 'resolved', confidence: 'exact', value: 2 },
146158
'powerpoint.smartart_data_part_count': { status: 'resolved', confidence: 'exact', value: 3 },
147159
}));
148-
expect(counted.ir.quality.checks[0]?.detail).toEqual({ parts: 5 });
160+
expect(counted.ir.quality.checks[0]?.detail).toEqual({ parts: 5, represented: 0 });
149161

150162
const blind = candidate([node('n1', 'body')]);
151163
crossCheckEmbeddedObjects(blind, undefined);

0 commit comments

Comments
 (0)