attention-manifest-protocol 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/CHANGELOG.zh-CN.md +6 -0
- package/lib/scan/documentScanner.d.ts +13 -0
- package/lib/scan/documentScanner.js +452 -0
- package/lib/scan/index.d.ts +4 -0
- package/lib/scan/index.js +4 -0
- package/lib/scan/texScanner.d.ts +10 -0
- package/lib/scan/texScanner.js +581 -0
- package/lib/scan/workspaceScanner.d.ts +9 -0
- package/lib/scan/workspaceScanner.js +439 -0
- package/package.json +76 -76
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
6
6
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.2] - 2026-11-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Publish the existing offline workspace, Office/document and TeX scanners through `attention-manifest-protocol/scan`. Inputs remain explicit file contents; the scanners never crawl the filesystem or execute project code.
|
|
13
|
+
- Keep `tools/support` entrypoints as compatibility re-exports. TypeScript, fflate and fast-xml-parser are runtime dependencies of the published scanners.
|
|
14
|
+
|
|
15
|
+
### Validation
|
|
16
|
+
|
|
17
|
+
- 69 workspace, document, TeX, agent and read/write boundary regression tests pass against the relocated implementations.
|
|
18
|
+
|
|
8
19
|
## [0.6.1] - 2026-11-03
|
|
9
20
|
|
|
10
21
|
### Added
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## [0.6.2] - 2026-11-03
|
|
4
|
+
|
|
5
|
+
- 将既有工作区、Office 文档与 TeX 离线扫描器发布为 `attention-manifest-protocol/scan`;输入仍为显式文件内容,不扫描宿主目录、不执行项目代码。
|
|
6
|
+
- 保留 `tools/support` 兼容导出;扫描器使用的 TypeScript、fflate、fast-xml-parser 随 npm 安装。
|
|
7
|
+
- 工作区、文档、TeX、agent 与读写边界的 69 项回归检查通过。
|
|
8
|
+
|
|
3
9
|
本文件记录本项目的所有重要变更。
|
|
4
10
|
|
|
5
11
|
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ContentStore } from "../context/contentStore.js";
|
|
2
|
+
import type { WorkspaceSnapshot } from "../context/workspace.js";
|
|
3
|
+
export interface StructuredWorkspaceFile {
|
|
4
|
+
path: string;
|
|
5
|
+
content: string | Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
type StructuredScanner = (inputs: readonly StructuredWorkspaceFile[], store: ContentStore) => WorkspaceSnapshot;
|
|
8
|
+
/**
|
|
9
|
+
* Sources are canonical relative paths. Code/Markdown retain their existing
|
|
10
|
+
* adapters; modern Office documents add native structural knowledge nodes.
|
|
11
|
+
*/
|
|
12
|
+
export declare const scanStructuredWorkspace: StructuredScanner;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline structural adapters. OOXML archives are read in memory, never
|
|
3
|
+
* extracted to disk. Original packages remain available as scoped details.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { posix } from "node:path";
|
|
7
|
+
import { strFromU8, unzipSync } from "fflate";
|
|
8
|
+
import { XMLBuilder, XMLParser, XMLValidator } from "fast-xml-parser";
|
|
9
|
+
import { encodeDocumentDetail } from "../context/document.js";
|
|
10
|
+
import { workspaceTerms } from "../context/workspace.js";
|
|
11
|
+
import { scanWorkspace } from "./workspaceScanner.js";
|
|
12
|
+
import { addTexStructure } from "./texScanner.js";
|
|
13
|
+
const MaxInputBytes = 16 * 1024 * 1024;
|
|
14
|
+
const MaxExpandedBytes = 32 * 1024 * 1024;
|
|
15
|
+
const Parser = new XMLParser({ preserveOrder: true, ignoreAttributes: false, parseTagValue: false, parseAttributeValue: false, trimValues: false });
|
|
16
|
+
const Builder = new XMLBuilder({ preserveOrder: true, ignoreAttributes: false });
|
|
17
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
18
|
+
const short = (text) => text.replace(/\s+/g, " ").trim().slice(0, 180);
|
|
19
|
+
const cellRange = (cells) => {
|
|
20
|
+
if (cells.length === 0) {
|
|
21
|
+
return "A1";
|
|
22
|
+
}
|
|
23
|
+
const addresses = cells.map((cell) => {
|
|
24
|
+
const match = /^([A-Z]+)(\d+)$/.exec(cell.address);
|
|
25
|
+
return {
|
|
26
|
+
column: match[1],
|
|
27
|
+
rank: [...match[1]].reduce((sum, char) => sum * 26 + char.charCodeAt(0) - 64, 0),
|
|
28
|
+
row: Number(match[2]),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
addresses.sort((left, right) => left.rank - right.rank);
|
|
32
|
+
const rows = addresses.map((address) => address.row);
|
|
33
|
+
const first = `${addresses[0].column}${rows.reduce((minimum, row) => Math.min(minimum, row), Infinity)}`;
|
|
34
|
+
const last = `${addresses.at(-1).column}${rows.reduce((maximum, row) => Math.max(maximum, row), 1)}`;
|
|
35
|
+
return first === last ? first : `${first}:${last}`;
|
|
36
|
+
};
|
|
37
|
+
const local = (name) => name.split(":").at(-1);
|
|
38
|
+
const attr = (node, name) => node === (void 0) ? "" : (node.attrs[name] ?? Object.entries(node.attrs).find(([key]) => local(key) === name)?.[1] ?? "");
|
|
39
|
+
const relationshipId = (node) => node.attrs["r:id"] ?? Object.entries(node.attrs).find(([key]) => key.includes(":") && local(key) === "id")?.[1] ?? "";
|
|
40
|
+
const children = (node, name) => node.children.filter((child) => local(child.name) === name);
|
|
41
|
+
const descendants = (node, name) => node.children.flatMap((child) => [...(local(child.name) === name ? [child] : []), ...descendants(child, name)]);
|
|
42
|
+
const text = (node) => node.text + node.children.map(text).join("");
|
|
43
|
+
const paragraphs = (node) => descendants(node, "p")
|
|
44
|
+
.map((paragraph) => descendants(paragraph, "t").map(text).join(""))
|
|
45
|
+
.join("\n");
|
|
46
|
+
const xml = (node) => Builder.build([node.raw]);
|
|
47
|
+
const elements = (items) => items.flatMap((item) => {
|
|
48
|
+
const name = Object.keys(item).find((key) => key !== ":@" && key !== "#text" && !key.startsWith("?"));
|
|
49
|
+
if (name === (void 0)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const content = item[name];
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
name,
|
|
56
|
+
attrs: Object.fromEntries(Object.entries((item[":@"] ?? {})).map(([key, value]) => [
|
|
57
|
+
key.slice(2),
|
|
58
|
+
value,
|
|
59
|
+
])),
|
|
60
|
+
children: elements(content),
|
|
61
|
+
text: content
|
|
62
|
+
.filter((child) => "#text" in child)
|
|
63
|
+
.map((child) => `${child["#text"]}`)
|
|
64
|
+
.join(""),
|
|
65
|
+
raw: item,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
});
|
|
69
|
+
const parseXml = (content) => {
|
|
70
|
+
if (/<!DOCTYPE|<!ENTITY/i.test(content)) {
|
|
71
|
+
throw new Error("DTD/entity declarations are unsupported");
|
|
72
|
+
}
|
|
73
|
+
let depth = 0;
|
|
74
|
+
const tags = [...content.matchAll(/<[^>]+>/g)];
|
|
75
|
+
for (let tagIndex = 0; tagIndex < tags.length; tagIndex += 1) {
|
|
76
|
+
const match = tags[tagIndex];
|
|
77
|
+
if (/^<\//.test(match[0])) {
|
|
78
|
+
depth -= 1;
|
|
79
|
+
}
|
|
80
|
+
else if (!/^<[!?]/.test(match[0]) && !/\/>$/.test(match[0])) {
|
|
81
|
+
depth += 1;
|
|
82
|
+
}
|
|
83
|
+
if (depth > 128) {
|
|
84
|
+
throw new Error("XML nesting exceeds 128");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (XMLValidator.validate(content) !== true) {
|
|
88
|
+
throw new Error("malformed XML");
|
|
89
|
+
}
|
|
90
|
+
const roots = elements(Parser.parse(content));
|
|
91
|
+
if (roots.length !== 1) {
|
|
92
|
+
throw new Error("XML requires one root");
|
|
93
|
+
}
|
|
94
|
+
return roots[0];
|
|
95
|
+
};
|
|
96
|
+
class Package {
|
|
97
|
+
parts;
|
|
98
|
+
parsed = new Map();
|
|
99
|
+
constructor(bytes) {
|
|
100
|
+
let total = 0;
|
|
101
|
+
const names = new Set();
|
|
102
|
+
this.parts = unzipSync(bytes, {
|
|
103
|
+
filter: (entry) => {
|
|
104
|
+
if (names.has(entry.name) ||
|
|
105
|
+
names.size >= 4096 ||
|
|
106
|
+
entry.name.startsWith("/") ||
|
|
107
|
+
entry.name.includes("\\") ||
|
|
108
|
+
posix.normalize(entry.name) !== entry.name ||
|
|
109
|
+
entry.name.startsWith("../")) {
|
|
110
|
+
throw new Error("invalid, duplicate or excessive ZIP entries");
|
|
111
|
+
}
|
|
112
|
+
names.add(entry.name);
|
|
113
|
+
total += entry.originalSize;
|
|
114
|
+
if (entry.originalSize > 8 * 1024 * 1024 || total > MaxExpandedBytes) {
|
|
115
|
+
throw new Error("OOXML expansion limit exceeded");
|
|
116
|
+
}
|
|
117
|
+
return /\.(?:xml|rels)$/.test(entry.name);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
has(part) {
|
|
122
|
+
return Object.hasOwn(this.parts, part);
|
|
123
|
+
}
|
|
124
|
+
read(part) {
|
|
125
|
+
const cached = this.parsed.get(part);
|
|
126
|
+
if (cached !== (void 0)) {
|
|
127
|
+
return cached;
|
|
128
|
+
}
|
|
129
|
+
if (!this.has(part)) {
|
|
130
|
+
throw new Error(`missing OOXML part ${part}`);
|
|
131
|
+
}
|
|
132
|
+
const root = parseXml(strFromU8(this.parts[part]));
|
|
133
|
+
this.parsed.set(part, root);
|
|
134
|
+
return root;
|
|
135
|
+
}
|
|
136
|
+
relationships(part) {
|
|
137
|
+
const rels = posix.join(posix.dirname(part), "_rels", `${posix.basename(part)}.rels`);
|
|
138
|
+
if (!this.has(rels)) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
return children(this.read(rels), "Relationship").map((node) => ({
|
|
142
|
+
id: attr(node, "Id"),
|
|
143
|
+
type: attr(node, "Type").split("/").at(-1),
|
|
144
|
+
target: attr(node, "TargetMode") === "External"
|
|
145
|
+
? attr(node, "Target")
|
|
146
|
+
: posix.normalize(attr(node, "Target").startsWith("/") ? attr(node, "Target").slice(1) : posix.join(posix.dirname(part), attr(node, "Target"))),
|
|
147
|
+
external: attr(node, "TargetMode") === "External",
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
target(part, id) {
|
|
151
|
+
const relation = this.relationships(part).find((item) => item.id === id);
|
|
152
|
+
if (!relation || relation.external) {
|
|
153
|
+
throw new Error(`missing or external OOXML relationship ${id}`);
|
|
154
|
+
}
|
|
155
|
+
return relation.target;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Sources are canonical relative paths. Code/Markdown retain their existing
|
|
160
|
+
* adapters; modern Office documents add native structural knowledge nodes.
|
|
161
|
+
*/
|
|
162
|
+
export const scanStructuredWorkspace = (inputs, store) => {
|
|
163
|
+
const normalized = inputs.map((file) => ({
|
|
164
|
+
...file,
|
|
165
|
+
path: file.path.replace(/\\/g, "/"),
|
|
166
|
+
}));
|
|
167
|
+
const isText = (path) => /\.(?:md|txt|[cm]?[jt]sx?|json)$/i.test(path);
|
|
168
|
+
const snapshot = scanWorkspace(normalized.map((file) => ({
|
|
169
|
+
path: file.path,
|
|
170
|
+
content: isText(file.path) && !/\.txt$/i.test(file.path)
|
|
171
|
+
? typeof (file.content) === "string"
|
|
172
|
+
? file.content
|
|
173
|
+
: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(file.content)
|
|
174
|
+
: "",
|
|
175
|
+
})), store);
|
|
176
|
+
const revisions = new Map(normalized.map((file) => [file.path, hash(file.content)]));
|
|
177
|
+
snapshot.revision = hash(JSON.stringify([...revisions].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))));
|
|
178
|
+
snapshot.entities
|
|
179
|
+
.filter((entity) => entity.kind === "directory" || (entity.kind === "project" && entity.source.uri === "."))
|
|
180
|
+
.forEach((entity) => {
|
|
181
|
+
entity.source.revision = snapshot.revision;
|
|
182
|
+
});
|
|
183
|
+
const entities = new Map(snapshot.entities.map((entity) => [entity.id, entity]));
|
|
184
|
+
const texInputs = [];
|
|
185
|
+
for (let inputIndex = 0; inputIndex < normalized.length; inputIndex += 1) {
|
|
186
|
+
const input = normalized[inputIndex];
|
|
187
|
+
const path = input.path;
|
|
188
|
+
if (isText(path) && !/\.txt$/i.test(path)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const file = entities.get(`file:${path}`);
|
|
192
|
+
const revision = revisions.get(path);
|
|
193
|
+
file.source.revision = revision;
|
|
194
|
+
const bytes = typeof (input.content) === "string" ? new TextEncoder().encode(input.content) : input.content;
|
|
195
|
+
if (/\.(?:tex|ltx|sty|cls|bib)$/i.test(path)) {
|
|
196
|
+
try {
|
|
197
|
+
if (bytes.byteLength > MaxInputBytes) {
|
|
198
|
+
throw new Error("TeX source exceeds 16 MiB");
|
|
199
|
+
}
|
|
200
|
+
const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
201
|
+
file.detailRef = store.put(content);
|
|
202
|
+
texInputs.push({ path, content });
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
delete file.detailRef;
|
|
206
|
+
snapshot.diagnostics.push({ uri: path, code: "invalid_tex", message: error instanceof Error ? error.message : "TeX decoding failed" });
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (/\.(?:png|jpe?g|eps|svg|webp)$/i.test(path)) {
|
|
211
|
+
delete file.detailRef;
|
|
212
|
+
file.tags = ["document-asset"];
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const added = [];
|
|
216
|
+
const source = (locator) => ({ uri: path, revision, locator });
|
|
217
|
+
const add = (key, parentId, label, locator, detail, body, kind) => {
|
|
218
|
+
const entity = {
|
|
219
|
+
id: `knowledge:${path}#${key}`,
|
|
220
|
+
kind: "knowledge",
|
|
221
|
+
projectId: file.projectId,
|
|
222
|
+
parentId,
|
|
223
|
+
label,
|
|
224
|
+
summary: short(`${label}: ${body}`),
|
|
225
|
+
source: source(locator),
|
|
226
|
+
structureKind: kind,
|
|
227
|
+
detailRef: store.put(encodeDocumentDetail(detail)),
|
|
228
|
+
searchTerms: workspaceTerms(body),
|
|
229
|
+
summaryProvenance: { method: "extractive-v1", sources: [{ uri: path, revision }], children: [] },
|
|
230
|
+
};
|
|
231
|
+
added.push(entity);
|
|
232
|
+
return entity;
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
if (bytes.byteLength > MaxInputBytes) {
|
|
236
|
+
throw new Error("document exceeds 16 MiB input limit");
|
|
237
|
+
}
|
|
238
|
+
if (/\.txt$/i.test(path)) {
|
|
239
|
+
const body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
240
|
+
add("text", file.id, path, { kind: "part", part: path }, { kind: "text", text: body }, body, "document");
|
|
241
|
+
file.detailRef = store.put(body);
|
|
242
|
+
}
|
|
243
|
+
else if (/\.(?:docx|xlsx|pptx)$/i.test(path)) {
|
|
244
|
+
const pkg = new Package(bytes);
|
|
245
|
+
if (/\.docx$/i.test(path)) {
|
|
246
|
+
const part = "word/document.xml";
|
|
247
|
+
const root = pkg.read(part);
|
|
248
|
+
const body = children(root, "body")[0];
|
|
249
|
+
if (!body) {
|
|
250
|
+
throw new Error("Word document body missing");
|
|
251
|
+
}
|
|
252
|
+
const sections = [];
|
|
253
|
+
let ordinal = 0;
|
|
254
|
+
for (let blockIndex = 0; blockIndex < body.children.length; blockIndex += 1) {
|
|
255
|
+
const block = body.children[blockIndex];
|
|
256
|
+
ordinal += 1;
|
|
257
|
+
const name = local(block.name);
|
|
258
|
+
if (!["p", "tbl"].includes(name)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const locator = { kind: "part", part, element: `body/*[${ordinal}]` };
|
|
262
|
+
if (name === "tbl") {
|
|
263
|
+
const rows = children(block, "tr").map((row) => children(row, "tc").map(paragraphs));
|
|
264
|
+
add(`table${ordinal}`, sections.at(-1)?.id ?? file.id, `Table ${ordinal}`, locator, { kind: "table", rows, xml: xml(block) }, rows.flat().join(" "), "table");
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const style = attr(descendants(block, "pStyle")[0], "val");
|
|
268
|
+
const outline = attr(descendants(block, "outlineLvl")[0], "val");
|
|
269
|
+
const level = outline && Number(outline) < 9 ? Number(outline) + 1 : Number(/^Heading([1-9])$/i.exec(style)?.[1] ?? 0);
|
|
270
|
+
const bodyText = descendants(block, "t").map(text).join("");
|
|
271
|
+
if (level > 0) {
|
|
272
|
+
while (sections.length > 0 && sections.at(-1).level >= level) {
|
|
273
|
+
sections.pop();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const node = add(`p${ordinal}`, sections.at(-1)?.id ?? file.id, level ? bodyText : `Paragraph ${ordinal}`, locator, { kind: "text", text: bodyText, xml: xml(block) }, bodyText, level ? "section" : "paragraph");
|
|
277
|
+
if (level > 0) {
|
|
278
|
+
sections.push({ id: node.id, level });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
snapshot.diagnostics.push({
|
|
283
|
+
uri: path,
|
|
284
|
+
code: "docx_structure_partial",
|
|
285
|
+
message: "Main body, standard Heading styles and direct outline levels indexed; custom styles, headers, notes, revisions and images remain in original package",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
else if (/\.xlsx$/i.test(path)) {
|
|
289
|
+
const part = "xl/workbook.xml";
|
|
290
|
+
const root = pkg.read(part);
|
|
291
|
+
const shared = pkg.has("xl/sharedStrings.xml")
|
|
292
|
+
? children(pkg.read("xl/sharedStrings.xml"), "si").map((item) => descendants(item, "t").map(text).join(""))
|
|
293
|
+
: [];
|
|
294
|
+
const sheets = [];
|
|
295
|
+
const sheetElements = descendants(root, "sheet");
|
|
296
|
+
for (let sheetIndex = 0; sheetIndex < sheetElements.length; sheetIndex += 1) {
|
|
297
|
+
const sheet = sheetElements[sheetIndex];
|
|
298
|
+
const name = attr(sheet, "name");
|
|
299
|
+
const sheetPart = pkg.target(part, relationshipId(sheet));
|
|
300
|
+
const sheetXml = pkg.read(sheetPart);
|
|
301
|
+
const cells = descendants(sheetXml, "c").map((cell) => {
|
|
302
|
+
const formula = children(cell, "f")[0];
|
|
303
|
+
const value = children(cell, "v")[0];
|
|
304
|
+
const type = attr(cell, "t") || "n";
|
|
305
|
+
const address = attr(cell, "r");
|
|
306
|
+
if (!/^[A-Z]+[1-9]\d*$/.test(address)) {
|
|
307
|
+
throw new Error("sheet cell requires an A1 address");
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
address,
|
|
311
|
+
type,
|
|
312
|
+
...(value ? { value: type === "s" ? (shared[Number(text(value))] ?? text(value)) : text(value) } : {}),
|
|
313
|
+
...(type === "inlineStr" ? { value: descendants(cell, "t").map(text).join("") } : {}),
|
|
314
|
+
...(formula ? { formula: text(formula), formulaAttributes: { ...formula.attrs } } : {}),
|
|
315
|
+
...(attr(cell, "s") ? { style: attr(cell, "s") } : {}),
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
const range = cellRange(cells);
|
|
319
|
+
const node = add(`sheet:${attr(sheet, "sheetId")}`, file.id, name, { kind: "cells", part: sheetPart, sheet: name, range }, {
|
|
320
|
+
kind: "sheet",
|
|
321
|
+
name,
|
|
322
|
+
cells,
|
|
323
|
+
merges: descendants(sheetXml, "mergeCell").map((item) => attr(item, "ref")),
|
|
324
|
+
xml: xml(sheetXml),
|
|
325
|
+
}, cells.map((cell) => `${cell.address} ${cell.value ?? ""} ${cell.formula ?? ""}`).join("\n"), "sheet");
|
|
326
|
+
sheets.push({ name, node, cells, part: sheetPart });
|
|
327
|
+
}
|
|
328
|
+
for (let sheetIndex = 0; sheetIndex < sheets.length; sheetIndex += 1) {
|
|
329
|
+
const sheet = sheets[sheetIndex];
|
|
330
|
+
for (let cellIndex = 0; cellIndex < sheet.cells.length; cellIndex += 1) {
|
|
331
|
+
const cell = sheet.cells[cellIndex];
|
|
332
|
+
const references = [
|
|
333
|
+
...(cell.formula ?? "")
|
|
334
|
+
.replace(/"(?:[^"]|"")*"/g, "")
|
|
335
|
+
.matchAll(/(?<![\p{L}\p{N}_.\]])(?:'((?:[^']|'')+)'|([\p{L}_][\p{L}\p{N}_.]*))!\$?[A-Z]+\$?[1-9]\d*/gu),
|
|
336
|
+
];
|
|
337
|
+
for (let referenceIndex = 0; referenceIndex < references.length; referenceIndex += 1) {
|
|
338
|
+
const match = references[referenceIndex];
|
|
339
|
+
const targetName = (match[1] ?? match[2]).replace(/''/g, "'");
|
|
340
|
+
const target = sheets.find((item) => item.name.toLowerCase() === targetName.toLowerCase())?.node;
|
|
341
|
+
if (target) {
|
|
342
|
+
snapshot.relations.push({
|
|
343
|
+
from: sheet.node.id,
|
|
344
|
+
to: target.id,
|
|
345
|
+
kind: "references",
|
|
346
|
+
method: "explicit",
|
|
347
|
+
source: source({ kind: "cells", part: sheet.part, sheet: sheet.name, range: cell.address }),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
snapshot.diagnostics.push({
|
|
354
|
+
uri: path,
|
|
355
|
+
code: "formula_graph_partial",
|
|
356
|
+
message: "No recalculation: values are stored caches; graph covers literal cross-sheet A1 references only; named, dynamic, external and shared-formula expansion remain unanalysed. Styles/charts/table definitions remain in original package",
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
const part = "ppt/presentation.xml";
|
|
361
|
+
let ordinal = 0;
|
|
362
|
+
const slideElements = descendants(pkg.read(part), "sldId");
|
|
363
|
+
for (let slideIndex = 0; slideIndex < slideElements.length; slideIndex += 1) {
|
|
364
|
+
const slide = slideElements[slideIndex];
|
|
365
|
+
ordinal += 1;
|
|
366
|
+
const slidePart = pkg.target(part, relationshipId(slide));
|
|
367
|
+
const root = pkg.read(slidePart);
|
|
368
|
+
const objects = [];
|
|
369
|
+
const collect = (element) => {
|
|
370
|
+
for (let childIndex = 0; childIndex < element.children.length; childIndex += 1) {
|
|
371
|
+
const child = element.children[childIndex];
|
|
372
|
+
if (["sp", "pic", "graphicFrame", "cxnSp"].includes(local(child.name))) {
|
|
373
|
+
const properties = descendants(child, "cNvPr")[0];
|
|
374
|
+
objects.push({
|
|
375
|
+
id: attr(properties, "id"),
|
|
376
|
+
kind: local(child.name),
|
|
377
|
+
text: paragraphs(child),
|
|
378
|
+
xml: xml(child),
|
|
379
|
+
locator: { kind: "slide", part: slidePart, slide: ordinal, objectId: attr(properties, "id") },
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
collect(child);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
collect(root);
|
|
388
|
+
const note = pkg
|
|
389
|
+
.relationships(slidePart)
|
|
390
|
+
.find((item) => item.type === "notesSlide" && !item.external);
|
|
391
|
+
const notes = note ? paragraphs(pkg.read(note.target)) : "";
|
|
392
|
+
add(`slide${ordinal}`, file.id, `Slide ${ordinal}`, { kind: "slide", part: slidePart, slide: ordinal }, { kind: "slide", objects, notes, xml: xml(root) }, `${objects.map((object) => object.text).join("\n")}\n${notes}`, "slide");
|
|
393
|
+
}
|
|
394
|
+
snapshot.diagnostics.push({
|
|
395
|
+
uri: path,
|
|
396
|
+
code: "pptx_structure_partial",
|
|
397
|
+
message: "Slide order, objects, native layout XML and speaker notes retained; charts/images/themes remain in original package; no OCR or layout inference",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
add("original", file.id, "Original package", { kind: "part", part: "/" }, {
|
|
401
|
+
kind: "original",
|
|
402
|
+
mediaType: `application/vnd.openxmlformats-officedocument.${/\.docx$/i.test(path) ? "wordprocessingml.document" : /\.xlsx$/i.test(path) ? "spreadsheetml.sheet" : "presentationml.presentation"}`,
|
|
403
|
+
encoding: "base64",
|
|
404
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
405
|
+
}, "Original Office package; allocate this node to retrieve complete binary contents", "document");
|
|
406
|
+
delete file.detailRef;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
snapshot.diagnostics.push({
|
|
410
|
+
uri: path,
|
|
411
|
+
code: "unsupported_document",
|
|
412
|
+
message: "Supported document formats: Markdown, UTF-8 TXT, TeX/LaTeX, BibTeX, DOCX, XLSX, PPTX. Convert PDF/EPUB/legacy Office formats before ingestion",
|
|
413
|
+
});
|
|
414
|
+
delete file.detailRef;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
file.structureKind = "document";
|
|
418
|
+
const childrenByParent = new Map();
|
|
419
|
+
added.forEach((entity) => {
|
|
420
|
+
const siblings = childrenByParent.get(entity.parentId) ?? [];
|
|
421
|
+
siblings.push(entity);
|
|
422
|
+
childrenByParent.set(entity.parentId, siblings);
|
|
423
|
+
});
|
|
424
|
+
for (let entityIndex = added.length - 1; entityIndex >= 0; entityIndex -= 1) {
|
|
425
|
+
const entity = added[entityIndex];
|
|
426
|
+
const children = childrenByParent.get(entity.id) ?? [];
|
|
427
|
+
if (children.length > 0) {
|
|
428
|
+
entity.summary = short(`${entity.summary} ${children.map((child) => child.summary).join("; ")}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (let entityIndex = 0; entityIndex < added.length; entityIndex += 1) {
|
|
432
|
+
const entity = added[entityIndex];
|
|
433
|
+
entity.summaryProvenance.children = (childrenByParent.get(entity.id) ?? []).map((child) => child.id);
|
|
434
|
+
entities.set(entity.id, entity);
|
|
435
|
+
}
|
|
436
|
+
file.summary = short(`${path}: ${(childrenByParent.get(file.id) ?? []).map((entity) => entity.summary).join("; ")}`);
|
|
437
|
+
file.summaryProvenance = {
|
|
438
|
+
method: "extractive-v1",
|
|
439
|
+
sources: [{ uri: path, revision }],
|
|
440
|
+
children: (childrenByParent.get(file.id) ?? []).map((entity) => entity.id),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
delete file.detailRef;
|
|
445
|
+
snapshot.relations = snapshot.relations.filter((edge) => !added.some((entity) => entity.id === edge.from || entity.id === edge.to));
|
|
446
|
+
snapshot.diagnostics.push({ uri: path, code: "invalid_document", message: error instanceof Error ? error.message : "document parsing failed" });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
snapshot.entities = [...entities.values()];
|
|
450
|
+
addTexStructure(snapshot, texInputs, store);
|
|
451
|
+
return snapshot;
|
|
452
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ContentStore } from "../context/contentStore.js";
|
|
2
|
+
import type { WorkspaceSnapshot } from "../context/workspace.js";
|
|
3
|
+
import type { WorkspaceFile } from "./workspaceScanner.js";
|
|
4
|
+
type TexAdapter = (snapshot: WorkspaceSnapshot, inputs: readonly WorkspaceFile[], store: ContentStore) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Adds format facts to the shared snapshot. LaTeX build projects are knowledge
|
|
7
|
+
* views under their root files, not replacements for workspace ownership.
|
|
8
|
+
*/
|
|
9
|
+
export declare const addTexStructure: TexAdapter;
|
|
10
|
+
export {};
|