@cyber-dash-tech/revela 0.17.20 → 0.17.22
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/README.md +25 -3
- package/README.zh-CN.md +25 -3
- package/designs/monet/DESIGN.md +58 -38
- package/lib/design/designs.ts +113 -2
- package/lib/document-materials/extract.ts +189 -6
- package/lib/domain/domains.ts +221 -1
- package/lib/material-intake.ts +494 -0
- package/lib/runtime/index.ts +114 -1
- package/package.json +1 -1
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/hooks/hooks.json +10 -0
- package/plugins/revela/hooks/revela_guard.ts +19 -0
- package/plugins/revela/hooks/revela_material_notice.ts +58 -0
- package/plugins/revela/hooks/revela_post_write_notice.ts +37 -6
- package/plugins/revela/mcp/revela-server.ts +152 -0
- package/plugins/revela/skills/revela-design/SKILL.md +4 -2
- package/plugins/revela/skills/revela-domain/SKILL.md +13 -1
- package/plugins/revela/skills/revela-init/SKILL.md +18 -8
- package/plugins/revela/skills/revela-upgrade/SKILL.md +33 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs"
|
|
2
|
+
import { basename, extname, isAbsolute, join, relative, resolve, sep } from "path"
|
|
3
|
+
import { extractDocumentMaterials, type DocumentMaterialsResult } from "./document-materials/extract"
|
|
4
|
+
import { sourceMaterialMetadata, sourceMaterialType } from "./source-materials"
|
|
5
|
+
import type { SourceMaterial } from "./decks-state"
|
|
6
|
+
|
|
7
|
+
export type MaterialIntakeStatus =
|
|
8
|
+
| "scanned"
|
|
9
|
+
| "extracted"
|
|
10
|
+
| "reviewed"
|
|
11
|
+
| "text_only_read"
|
|
12
|
+
| "skipped"
|
|
13
|
+
| "unsupported"
|
|
14
|
+
| "failed"
|
|
15
|
+
|
|
16
|
+
export interface MaterialRegistryEntry {
|
|
17
|
+
sourcePath: string
|
|
18
|
+
type: string
|
|
19
|
+
fingerprint?: string
|
|
20
|
+
size?: number
|
|
21
|
+
lastModified?: string
|
|
22
|
+
status: MaterialIntakeStatus
|
|
23
|
+
requiresExtraction: boolean
|
|
24
|
+
allowedReadPath?: string | null
|
|
25
|
+
extraction?: {
|
|
26
|
+
manifestPath?: string
|
|
27
|
+
textPath?: string
|
|
28
|
+
readViewPath?: string
|
|
29
|
+
cacheDir?: string
|
|
30
|
+
imageCount: number
|
|
31
|
+
tableCount: number
|
|
32
|
+
} | null
|
|
33
|
+
review?: {
|
|
34
|
+
reviewPath: string
|
|
35
|
+
reviewedAt: string
|
|
36
|
+
reviewedPaths: string[]
|
|
37
|
+
summary: string
|
|
38
|
+
} | null
|
|
39
|
+
warnings?: string[]
|
|
40
|
+
firstSeen: string
|
|
41
|
+
lastChecked: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MaterialRegistry {
|
|
45
|
+
version: 1
|
|
46
|
+
updatedAt: string
|
|
47
|
+
sources: MaterialRegistryEntry[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MaterialIngestTask {
|
|
51
|
+
path: string
|
|
52
|
+
materialType: string
|
|
53
|
+
needsExtraction: boolean
|
|
54
|
+
suggestedAction: "read_directly" | "extract_then_read"
|
|
55
|
+
status: MaterialIntakeStatus
|
|
56
|
+
allowedReadPath?: string | null
|
|
57
|
+
note: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PrepareLocalMaterialsInput {
|
|
61
|
+
workspaceRoot?: string
|
|
62
|
+
path?: string
|
|
63
|
+
maxDepth?: number
|
|
64
|
+
autoExtract?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PrepareLocalMaterialsResult {
|
|
68
|
+
ok: true
|
|
69
|
+
workspaceRoot: string
|
|
70
|
+
registryPath: string
|
|
71
|
+
found: number
|
|
72
|
+
files: SourceMaterial[]
|
|
73
|
+
suggestedTasks: MaterialIngestTask[]
|
|
74
|
+
extractions: DocumentMaterialsResult[]
|
|
75
|
+
warnings: string[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RecordMaterialReviewInput {
|
|
79
|
+
workspaceRoot?: string
|
|
80
|
+
sourcePath: string
|
|
81
|
+
reviewedPaths: string[]
|
|
82
|
+
reviewSummary: string
|
|
83
|
+
narrativeDecisions: Array<{
|
|
84
|
+
kind: "merged" | "gap" | "ignored" | "deferred"
|
|
85
|
+
target?: string
|
|
86
|
+
rationale: string
|
|
87
|
+
}>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RecordMaterialReviewResult {
|
|
91
|
+
ok: true
|
|
92
|
+
path: string
|
|
93
|
+
registryPath: string
|
|
94
|
+
sourcePath: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CheckMaterialIntakeInput {
|
|
98
|
+
workspaceRoot?: string
|
|
99
|
+
strictness?: "authoring" | "readiness" | "render"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface CheckMaterialIntakeResult {
|
|
103
|
+
ok: boolean
|
|
104
|
+
registryPath: string
|
|
105
|
+
warnings: string[]
|
|
106
|
+
sources: Array<MaterialRegistryEntry & { recommendedNextAction?: string }>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const DOC_EXTENSIONS = new Set([".pdf", ".docx", ".doc", ".xlsx", ".xls", ".pptx", ".ppt", ".csv", ".md", ".txt"])
|
|
110
|
+
const EXCLUDE_DIRS = new Set(["node_modules", ".git", "dist", ".opencode", "researches", "revela-narrative", "designs", "domains"])
|
|
111
|
+
const EXCLUDE_FILENAMES = new Set(["AGENTS.md", "DECKS.md", "README.md", "README.zh-CN.md"])
|
|
112
|
+
const EXTRACTION_EXTENSIONS = new Set(["pdf", "ppt", "pptx", "doc", "docx", "xls", "xlsx"])
|
|
113
|
+
const SUPPORTED_EXTRACTION_EXTENSIONS = new Set(["pdf", "pptx", "docx", "xlsx"])
|
|
114
|
+
|
|
115
|
+
export function materialRegistryPath(workspaceRoot: string): string {
|
|
116
|
+
return join(workspaceRoot, ".opencode", "revela", "material-intake", "registry.json")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function readMaterialRegistry(workspaceRoot: string): MaterialRegistry {
|
|
120
|
+
const path = materialRegistryPath(workspaceRoot)
|
|
121
|
+
if (!existsSync(path)) return { version: 1, updatedAt: new Date(0).toISOString(), sources: [] }
|
|
122
|
+
return JSON.parse(readFileSync(path, "utf-8")) as MaterialRegistry
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function writeMaterialRegistry(workspaceRoot: string, registry: MaterialRegistry): string {
|
|
126
|
+
const path = materialRegistryPath(workspaceRoot)
|
|
127
|
+
mkdirSync(join(workspaceRoot, ".opencode", "revela", "material-intake"), { recursive: true })
|
|
128
|
+
writeFileSync(path, JSON.stringify({ ...registry, updatedAt: new Date().toISOString() }, null, 2), "utf-8")
|
|
129
|
+
return workspaceRelative(path, workspaceRoot)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function prepareLocalMaterials(input: PrepareLocalMaterialsInput = {}): Promise<PrepareLocalMaterialsResult> {
|
|
133
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
134
|
+
const scanRoot = scanRootFor(workspaceRoot, input.path)
|
|
135
|
+
const files = scanWorkspaceSources(workspaceRoot, scanRoot, input.maxDepth ?? 2)
|
|
136
|
+
let registry = readMaterialRegistry(workspaceRoot)
|
|
137
|
+
const now = new Date().toISOString()
|
|
138
|
+
const extractions: DocumentMaterialsResult[] = []
|
|
139
|
+
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
registry = upsertRegistryEntry(registry, materialEntryFromSource(file, now))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (input.autoExtract ?? true) {
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const type = (file.type || sourceMaterialType(file.path)).toLowerCase()
|
|
147
|
+
if (!EXTRACTION_EXTENSIONS.has(type)) continue
|
|
148
|
+
const result = await extractAndUpdateRegistry({ workspaceRoot, file: file.path }, registry)
|
|
149
|
+
registry = result.registry
|
|
150
|
+
extractions.push(result.extraction)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const registryPath = writeMaterialRegistry(workspaceRoot, registry)
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
workspaceRoot,
|
|
158
|
+
registryPath,
|
|
159
|
+
found: files.length,
|
|
160
|
+
files,
|
|
161
|
+
suggestedTasks: registry.sources.map((entry) => ingestTask(entry)),
|
|
162
|
+
extractions,
|
|
163
|
+
warnings: intakeWarnings(registry.sources),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function extractMaterial(input: { workspaceRoot?: string; file: string }): Promise<DocumentMaterialsResult> {
|
|
168
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
169
|
+
const registry = readMaterialRegistry(workspaceRoot)
|
|
170
|
+
const result = await extractAndUpdateRegistry({ workspaceRoot, file: input.file }, registry)
|
|
171
|
+
writeMaterialRegistry(workspaceRoot, result.registry)
|
|
172
|
+
return result.extraction
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function recordMaterialReview(input: RecordMaterialReviewInput): RecordMaterialReviewResult {
|
|
176
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
177
|
+
const registry = readMaterialRegistry(workspaceRoot)
|
|
178
|
+
const entry = registry.sources.find((item) => item.sourcePath === normalizePath(input.sourcePath))
|
|
179
|
+
const sourcePath = entry?.sourcePath ?? normalizePath(input.sourcePath)
|
|
180
|
+
const reviewPath = writeReviewMarkdown(workspaceRoot, {
|
|
181
|
+
sourcePath,
|
|
182
|
+
fingerprint: entry?.fingerprint,
|
|
183
|
+
extraction: entry?.extraction ?? null,
|
|
184
|
+
reviewedPaths: input.reviewedPaths.map(normalizePath),
|
|
185
|
+
reviewSummary: input.reviewSummary,
|
|
186
|
+
narrativeDecisions: input.narrativeDecisions,
|
|
187
|
+
})
|
|
188
|
+
const now = new Date().toISOString()
|
|
189
|
+
const nextEntry: MaterialRegistryEntry = {
|
|
190
|
+
...(entry ?? {
|
|
191
|
+
sourcePath,
|
|
192
|
+
type: sourceMaterialType(sourcePath),
|
|
193
|
+
status: "scanned",
|
|
194
|
+
requiresExtraction: EXTRACTION_EXTENSIONS.has(sourceMaterialType(sourcePath)),
|
|
195
|
+
firstSeen: now,
|
|
196
|
+
lastChecked: now,
|
|
197
|
+
}),
|
|
198
|
+
status: "reviewed",
|
|
199
|
+
review: {
|
|
200
|
+
reviewPath,
|
|
201
|
+
reviewedAt: now,
|
|
202
|
+
reviewedPaths: input.reviewedPaths.map(normalizePath),
|
|
203
|
+
summary: input.reviewSummary,
|
|
204
|
+
},
|
|
205
|
+
warnings: [],
|
|
206
|
+
lastChecked: now,
|
|
207
|
+
}
|
|
208
|
+
const updated = upsertRegistryEntry(registry, nextEntry)
|
|
209
|
+
const registryPath = writeMaterialRegistry(workspaceRoot, updated)
|
|
210
|
+
return { ok: true, path: reviewPath, registryPath, sourcePath }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function checkMaterialIntake(input: CheckMaterialIntakeInput = {}): CheckMaterialIntakeResult {
|
|
214
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
215
|
+
const registry = readMaterialRegistry(workspaceRoot)
|
|
216
|
+
const sources = registry.sources.map((source) => {
|
|
217
|
+
const recommendedNextAction = recommendedAction(source, input.strictness ?? "authoring")
|
|
218
|
+
return recommendedNextAction ? { ...source, recommendedNextAction } : source
|
|
219
|
+
})
|
|
220
|
+
const warnings = intakeWarnings(registry.sources)
|
|
221
|
+
return {
|
|
222
|
+
ok: warnings.length === 0,
|
|
223
|
+
registryPath: workspaceRelative(materialRegistryPath(workspaceRoot), workspaceRoot),
|
|
224
|
+
warnings,
|
|
225
|
+
sources,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function materialIntakeNoticeForCommand(input: { workspaceRoot?: string; command: string }): string | null {
|
|
230
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
231
|
+
const registry = readMaterialRegistry(workspaceRoot)
|
|
232
|
+
const command = input.command
|
|
233
|
+
const rawOfficeRead = /\b(textutil|pandoc|strings|unzip)\b/.test(command) && /\.(docx|doc|pptx|ppt|xlsx|xls|pdf)\b/i.test(command)
|
|
234
|
+
const matched = registry.sources.filter((entry) => entry.requiresExtraction && command.includes(entry.sourcePath))
|
|
235
|
+
if (!rawOfficeRead && matched.length === 0) return null
|
|
236
|
+
|
|
237
|
+
const paths = matched.length > 0
|
|
238
|
+
? matched.map((entry) => entry.sourcePath)
|
|
239
|
+
: registry.sources.filter((entry) => entry.requiresExtraction).map((entry) => entry.sourcePath)
|
|
240
|
+
const unique = [...new Set(paths)].slice(0, 5)
|
|
241
|
+
return [
|
|
242
|
+
"Revela material intake notice:",
|
|
243
|
+
unique.length > 0
|
|
244
|
+
? `Scanned source(s) require Revela extraction before narrative intake: ${unique.map((path) => `\`${path}\``).join(", ")}.`
|
|
245
|
+
: "This command appears to read an Office/PDF source directly.",
|
|
246
|
+
"Use `revela_extract_document_materials` and read the returned `read_view_path` so embedded images and manifests are considered.",
|
|
247
|
+
"If this is intentionally text-only, mark it as degraded intake in the init report; do not treat it as complete material review.",
|
|
248
|
+
].join("\n")
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function extractAndUpdateRegistry(
|
|
252
|
+
input: { workspaceRoot: string; file: string },
|
|
253
|
+
registry: MaterialRegistry,
|
|
254
|
+
): Promise<{ extraction: DocumentMaterialsResult; registry: MaterialRegistry }> {
|
|
255
|
+
const sourcePath = normalizePath(input.file)
|
|
256
|
+
const extraction = await extractDocumentMaterials(sourcePath, input.workspaceRoot)
|
|
257
|
+
const existing = registry.sources.find((entry) => entry.sourcePath === sourcePath)
|
|
258
|
+
const now = new Date().toISOString()
|
|
259
|
+
const type = extraction.type === "other" ? sourceMaterialType(sourcePath) : extraction.type
|
|
260
|
+
const unsupported = EXTRACTION_EXTENSIONS.has(type) && !SUPPORTED_EXTRACTION_EXTENSIONS.has(type)
|
|
261
|
+
const status: MaterialIntakeStatus = extraction.status === "processed"
|
|
262
|
+
? "extracted"
|
|
263
|
+
: extraction.status === "failed"
|
|
264
|
+
? "failed"
|
|
265
|
+
: unsupported
|
|
266
|
+
? "unsupported"
|
|
267
|
+
: "skipped"
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
extraction,
|
|
271
|
+
registry: upsertRegistryEntry(registry, {
|
|
272
|
+
...(existing ?? {
|
|
273
|
+
sourcePath,
|
|
274
|
+
type,
|
|
275
|
+
requiresExtraction: EXTRACTION_EXTENSIONS.has(type),
|
|
276
|
+
firstSeen: now,
|
|
277
|
+
}),
|
|
278
|
+
sourcePath,
|
|
279
|
+
type,
|
|
280
|
+
status,
|
|
281
|
+
requiresExtraction: EXTRACTION_EXTENSIONS.has(type),
|
|
282
|
+
allowedReadPath: extraction.read_view_path ?? extraction.text_path ?? null,
|
|
283
|
+
extraction: extraction.status === "processed"
|
|
284
|
+
? {
|
|
285
|
+
manifestPath: extraction.manifest_path,
|
|
286
|
+
textPath: extraction.text_path,
|
|
287
|
+
readViewPath: extraction.read_view_path,
|
|
288
|
+
cacheDir: extraction.cache_dir,
|
|
289
|
+
imageCount: extraction.images?.length ?? 0,
|
|
290
|
+
tableCount: extraction.tables?.length ?? 0,
|
|
291
|
+
}
|
|
292
|
+
: null,
|
|
293
|
+
warnings: extractionWarnings(extraction, unsupported),
|
|
294
|
+
lastChecked: now,
|
|
295
|
+
}),
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function scanWorkspaceSources(workspaceRoot: string, scanRoot: string, maxDepth: number): SourceMaterial[] {
|
|
300
|
+
const results: SourceMaterial[] = []
|
|
301
|
+
scanDir(scanRoot, workspaceRoot, results, maxDepth, 0)
|
|
302
|
+
return results.sort((a, b) => a.path.localeCompare(b.path))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function scanDir(dir: string, workspaceRoot: string, results: SourceMaterial[], maxDepth: number, depth: number): void {
|
|
306
|
+
if (depth > maxDepth || !existsSync(dir)) return
|
|
307
|
+
let entries: string[]
|
|
308
|
+
try {
|
|
309
|
+
entries = readdirSync(dir)
|
|
310
|
+
} catch {
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
if (entry.startsWith(".") || EXCLUDE_DIRS.has(entry)) continue
|
|
316
|
+
const fullPath = join(dir, entry)
|
|
317
|
+
let stat
|
|
318
|
+
try {
|
|
319
|
+
stat = statSync(fullPath)
|
|
320
|
+
} catch {
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
if (stat.isDirectory()) {
|
|
324
|
+
scanDir(fullPath, workspaceRoot, results, maxDepth, depth + 1)
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
if (!stat.isFile() || EXCLUDE_FILENAMES.has(entry) || entry.startsWith("~$")) continue
|
|
328
|
+
if (!DOC_EXTENSIONS.has(extname(entry).toLowerCase())) continue
|
|
329
|
+
results.push({ ...sourceMaterialMetadata(fullPath, workspaceRoot), status: "discovered" })
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function materialEntryFromSource(source: SourceMaterial, now: string): MaterialRegistryEntry {
|
|
334
|
+
const type = (source.type || sourceMaterialType(source.path)).toLowerCase()
|
|
335
|
+
return {
|
|
336
|
+
sourcePath: source.path,
|
|
337
|
+
type,
|
|
338
|
+
fingerprint: source.fingerprint,
|
|
339
|
+
size: source.size,
|
|
340
|
+
lastModified: source.lastModified,
|
|
341
|
+
status: "scanned",
|
|
342
|
+
requiresExtraction: EXTRACTION_EXTENSIONS.has(type),
|
|
343
|
+
allowedReadPath: EXTRACTION_EXTENSIONS.has(type) ? null : source.path,
|
|
344
|
+
extraction: null,
|
|
345
|
+
review: null,
|
|
346
|
+
warnings: [],
|
|
347
|
+
firstSeen: now,
|
|
348
|
+
lastChecked: now,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function upsertRegistryEntry(registry: MaterialRegistry, entry: MaterialRegistryEntry): MaterialRegistry {
|
|
353
|
+
const sourcePath = normalizePath(entry.sourcePath)
|
|
354
|
+
const existingIndex = registry.sources.findIndex((item) => item.sourcePath === sourcePath)
|
|
355
|
+
const existing = existingIndex >= 0 ? registry.sources[existingIndex] : undefined
|
|
356
|
+
const unchangedFingerprint = Boolean(existing?.fingerprint && entry.fingerprint && existing.fingerprint === entry.fingerprint)
|
|
357
|
+
const scanRefresh = entry.status === "scanned" && unchangedFingerprint
|
|
358
|
+
const next: MaterialRegistryEntry = {
|
|
359
|
+
...existing,
|
|
360
|
+
...entry,
|
|
361
|
+
sourcePath,
|
|
362
|
+
status: scanRefresh ? existing!.status : entry.status,
|
|
363
|
+
allowedReadPath: scanRefresh ? existing!.allowedReadPath : entry.allowedReadPath,
|
|
364
|
+
extraction: scanRefresh ? existing!.extraction : entry.extraction,
|
|
365
|
+
firstSeen: existing?.firstSeen ?? entry.firstSeen,
|
|
366
|
+
review: scanRefresh ? existing!.review : entry.review === undefined ? existing?.review : entry.review,
|
|
367
|
+
}
|
|
368
|
+
const sources = [...registry.sources]
|
|
369
|
+
if (existingIndex >= 0) sources[existingIndex] = next
|
|
370
|
+
else sources.push(next)
|
|
371
|
+
return { version: 1, updatedAt: new Date().toISOString(), sources: sources.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath)) }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function ingestTask(entry: MaterialRegistryEntry): MaterialIngestTask {
|
|
375
|
+
return {
|
|
376
|
+
path: entry.sourcePath,
|
|
377
|
+
materialType: entry.type,
|
|
378
|
+
needsExtraction: entry.requiresExtraction,
|
|
379
|
+
suggestedAction: entry.requiresExtraction ? "extract_then_read" : "read_directly",
|
|
380
|
+
status: entry.status,
|
|
381
|
+
allowedReadPath: entry.allowedReadPath ?? null,
|
|
382
|
+
note: entry.requiresExtraction
|
|
383
|
+
? "Read the extracted read_view_path after Revela extraction; do not read the original Office/PDF source for narrative intake."
|
|
384
|
+
: "Read directly when relevant and record narrative meaning only after source content is actually inspected.",
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function extractionWarnings(result: DocumentMaterialsResult, unsupported: boolean): string[] {
|
|
389
|
+
if (unsupported) return ["This source type is discovered but not supported for Revela extraction in v1."]
|
|
390
|
+
if (result.status === "failed") return [`Extraction failed: ${result.reason ?? "unknown error"}`]
|
|
391
|
+
if (result.status === "skipped") return [`Extraction skipped: ${result.reason ?? "unsupported file type"}`]
|
|
392
|
+
return []
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function intakeWarnings(sources: MaterialRegistryEntry[]): string[] {
|
|
396
|
+
const warnings: string[] = []
|
|
397
|
+
for (const source of sources) {
|
|
398
|
+
if (!source.requiresExtraction) continue
|
|
399
|
+
if (source.status === "scanned") warnings.push(`${source.sourcePath} was scanned but not extracted through Revela material extraction.`)
|
|
400
|
+
else if (source.status === "extracted") warnings.push(`${source.sourcePath} was extracted but has no recorded material review.`)
|
|
401
|
+
else if (source.status === "text_only_read") warnings.push(`${source.sourcePath} was read as text-only; embedded images or structure may not have been considered.`)
|
|
402
|
+
else if (source.status === "unsupported") warnings.push(`${source.sourcePath} is not supported for extraction; convert it to a supported format such as .docx/.pptx/.xlsx when needed.`)
|
|
403
|
+
else if (source.status === "failed") warnings.push(`${source.sourcePath} extraction failed and should not be treated as complete intake.`)
|
|
404
|
+
}
|
|
405
|
+
return warnings
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function recommendedAction(source: MaterialRegistryEntry, strictness: "authoring" | "readiness" | "render"): string | undefined {
|
|
409
|
+
if (!source.requiresExtraction) return undefined
|
|
410
|
+
if (source.status === "scanned") return "Call `revela_extract_document_materials`, then read the returned `read_view_path`."
|
|
411
|
+
if (source.status === "extracted") return strictness === "authoring"
|
|
412
|
+
? "Read `allowedReadPath`, then call `revela_record_material_review`."
|
|
413
|
+
: "Record material review before treating this source as considered for narrative readiness."
|
|
414
|
+
if (source.status === "text_only_read") return "Use Revela extraction and review before treating this source as complete intake."
|
|
415
|
+
if (source.status === "unsupported") return "Convert to a supported format or keep the source as an explicit intake gap."
|
|
416
|
+
if (source.status === "failed") return "Fix extraction failure or record the source as an intake gap."
|
|
417
|
+
return undefined
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function writeReviewMarkdown(
|
|
421
|
+
workspaceRoot: string,
|
|
422
|
+
input: {
|
|
423
|
+
sourcePath: string
|
|
424
|
+
fingerprint?: string
|
|
425
|
+
extraction: MaterialRegistryEntry["extraction"]
|
|
426
|
+
reviewedPaths: string[]
|
|
427
|
+
reviewSummary: string
|
|
428
|
+
narrativeDecisions: RecordMaterialReviewInput["narrativeDecisions"]
|
|
429
|
+
},
|
|
430
|
+
): string {
|
|
431
|
+
const dir = join(workspaceRoot, "researches", "local-materials")
|
|
432
|
+
mkdirSync(dir, { recursive: true })
|
|
433
|
+
const fileName = `${slugify(input.sourcePath)}-review.md`
|
|
434
|
+
const path = join(dir, fileName)
|
|
435
|
+
const lines = [
|
|
436
|
+
"---",
|
|
437
|
+
"type: local-material-review",
|
|
438
|
+
`sourcePath: ${JSON.stringify(input.sourcePath)}`,
|
|
439
|
+
input.fingerprint ? `fingerprint: ${JSON.stringify(input.fingerprint)}` : undefined,
|
|
440
|
+
input.extraction?.manifestPath ? `extractionManifestPath: ${JSON.stringify(input.extraction.manifestPath)}` : undefined,
|
|
441
|
+
input.extraction?.textPath ? `extractionTextPath: ${JSON.stringify(input.extraction.textPath)}` : undefined,
|
|
442
|
+
input.extraction?.readViewPath ? `readViewPath: ${JSON.stringify(input.extraction.readViewPath)}` : undefined,
|
|
443
|
+
`reviewedAt: ${JSON.stringify(new Date().toISOString())}`,
|
|
444
|
+
"status: reviewed",
|
|
445
|
+
"---",
|
|
446
|
+
"",
|
|
447
|
+
"# Local Material Review",
|
|
448
|
+
"",
|
|
449
|
+
"## Review Summary",
|
|
450
|
+
"",
|
|
451
|
+
input.reviewSummary.trim(),
|
|
452
|
+
"",
|
|
453
|
+
"## Reviewed Paths",
|
|
454
|
+
"",
|
|
455
|
+
...input.reviewedPaths.map((path) => `- ${path}`),
|
|
456
|
+
"",
|
|
457
|
+
"## Narrative Decisions",
|
|
458
|
+
"",
|
|
459
|
+
...input.narrativeDecisions.map((decision) => `- ${decision.kind}${decision.target ? `: ${decision.target}` : ""} - ${decision.rationale}`),
|
|
460
|
+
"",
|
|
461
|
+
"## Extracted Images",
|
|
462
|
+
"",
|
|
463
|
+
input.extraction?.imageCount ? `- ${input.extraction.imageCount} extracted image(s); do not treat as interpreted evidence without explicit image review.` : "- None recorded.",
|
|
464
|
+
"",
|
|
465
|
+
].filter((line): line is string => line !== undefined)
|
|
466
|
+
writeFileSync(path, lines.join("\n"), "utf-8")
|
|
467
|
+
return workspaceRelative(path, workspaceRoot)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function scanRootFor(workspaceRoot: string, path?: string): string {
|
|
471
|
+
if (!path) return workspaceRoot
|
|
472
|
+
if (isAbsolute(path)) throw new Error("path must be relative to workspace root")
|
|
473
|
+
const candidate = resolve(workspaceRoot, path)
|
|
474
|
+
const resolvedWorkspace = resolve(workspaceRoot)
|
|
475
|
+
if (candidate !== resolvedWorkspace && !candidate.startsWith(resolvedWorkspace + sep)) throw new Error("path must be within workspace")
|
|
476
|
+
return candidate
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function root(workspaceRoot?: string): string {
|
|
480
|
+
return resolve(workspaceRoot || process.cwd())
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function workspaceRelative(path: string, workspaceRoot: string): string {
|
|
484
|
+
return relative(resolve(workspaceRoot), resolve(path)).replace(/\\/g, "/")
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function normalizePath(path: string): string {
|
|
488
|
+
return path.replace(/\\/g, "/")
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function slugify(value: string): string {
|
|
492
|
+
const base = basename(value).replace(/\.[^.]+$/, "")
|
|
493
|
+
return base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "material"
|
|
494
|
+
}
|
package/lib/runtime/index.ts
CHANGED
|
@@ -4,25 +4,33 @@ import { dirname, resolve } from "path"
|
|
|
4
4
|
import {
|
|
5
5
|
activeDesign,
|
|
6
6
|
activateDesign,
|
|
7
|
+
createDesignDraftPackage,
|
|
7
8
|
createDesignPackage,
|
|
8
9
|
getDesignSection,
|
|
9
10
|
getDesignSkillMd,
|
|
11
|
+
installDesignDraftPackage,
|
|
10
12
|
listDesigns,
|
|
11
13
|
seedBuiltinDesigns,
|
|
14
|
+
validateDesignDraftPackage,
|
|
12
15
|
validateDesignPackage,
|
|
13
16
|
} from "../design/designs"
|
|
14
17
|
import { createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
|
|
15
|
-
import { activeDomain, activateDomain, getDomainSkillMd, listDomains, seedBuiltinDomains } from "../domain/domains"
|
|
18
|
+
import { activeDomain, activateDomain, createDomainDraftPackage, createDomainPackage, getDomainSkillMd, installDomainDraftPackage, listDomains, seedBuiltinDomains, validateDomainDraftPackage, validateDomainPackage } from "../domain/domains"
|
|
16
19
|
import { computeNarrativeHash } from "../narrative-state/hash"
|
|
17
20
|
import { compileNarrativeVault } from "../narrative-vault/compile"
|
|
21
|
+
import { autoCompileNarrativeVault } from "../narrative-vault/auto-compile"
|
|
22
|
+
import { extractNarrativeVaultMarkdownTargetsFromPatch } from "../narrative-vault/hook-targets"
|
|
18
23
|
import { runNarrativeMarkdownQa, type MarkdownQaOptions } from "../narrative-vault/markdown-qa"
|
|
24
|
+
import { formatArtifactQaUserNotice, formatMarkdownQaUserNotice } from "../hook-notifications"
|
|
19
25
|
import { readDeckPlanArtifact } from "../narrative-state/deck-plan-artifact"
|
|
20
26
|
import { extractDesignClasses } from "../design/designs"
|
|
21
27
|
import { recordRenderedArtifact, workspaceRelative } from "../workspace-state/rendered-artifacts"
|
|
28
|
+
import { checkMaterialIntake, extractMaterial, materialIntakeNoticeForCommand, prepareLocalMaterials, recordMaterialReview } from "../material-intake"
|
|
22
29
|
import type { ReviewDeckOpenInput, ReviewDeckReadInput } from "./review"
|
|
23
30
|
import pkg from "../../package.json"
|
|
24
31
|
export { bindResearchFindings, evaluateResearchFindings, researchSave, researchTargets } from "./research"
|
|
25
32
|
export { storyRead } from "./story"
|
|
33
|
+
export { checkMaterialIntake, extractMaterial, materialIntakeNoticeForCommand, prepareLocalMaterials, recordMaterialReview }
|
|
26
34
|
|
|
27
35
|
export interface RuntimeWorkspaceInput {
|
|
28
36
|
workspaceRoot?: string
|
|
@@ -32,6 +40,10 @@ export interface RuntimeFileInput extends RuntimeWorkspaceInput {
|
|
|
32
40
|
file: string
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
export interface RuntimeNarrativeAutoCompileInput extends RuntimeWorkspaceInput {
|
|
44
|
+
touched?: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
export interface RuntimeDeckFoundationInput extends RuntimeWorkspaceInput {
|
|
36
48
|
outputPath: string
|
|
37
49
|
title: string
|
|
@@ -55,12 +67,28 @@ export interface RuntimeDesignCreateInput {
|
|
|
55
67
|
overwrite?: boolean
|
|
56
68
|
}
|
|
57
69
|
|
|
70
|
+
export interface RuntimeDesignDraftCreateInput extends RuntimeDesignCreateInput, RuntimeWorkspaceInput {}
|
|
71
|
+
|
|
72
|
+
export interface RuntimeDomainCreateInput {
|
|
73
|
+
name: string
|
|
74
|
+
domainMd: string
|
|
75
|
+
overwrite?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RuntimeDomainDraftCreateInput extends RuntimeDomainCreateInput, RuntimeWorkspaceInput {}
|
|
79
|
+
|
|
80
|
+
export interface RuntimeDraftInstallInput extends RuntimeWorkspaceInput {
|
|
81
|
+
name: string
|
|
82
|
+
overwrite?: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
export interface RuntimeNameInput {
|
|
59
86
|
name: string
|
|
60
87
|
}
|
|
61
88
|
|
|
62
89
|
export function doctor(input: RuntimeWorkspaceInput = {}) {
|
|
63
90
|
const workspaceRoot = root(input.workspaceRoot)
|
|
91
|
+
const domain = activeDomainDoctorInfo()
|
|
64
92
|
return {
|
|
65
93
|
ok: true,
|
|
66
94
|
version: pkg.version,
|
|
@@ -69,6 +97,8 @@ export function doctor(input: RuntimeWorkspaceInput = {}) {
|
|
|
69
97
|
hasDeckPlan: existsSync(resolve(workspaceRoot, "deck-plan")),
|
|
70
98
|
hasDecksJson: existsSync(resolve(workspaceRoot, "DECKS.json")),
|
|
71
99
|
activeDesign: safe(activeDesign),
|
|
100
|
+
activeDomain: domain.name,
|
|
101
|
+
activeDomainDescription: domain.description,
|
|
72
102
|
}
|
|
73
103
|
}
|
|
74
104
|
|
|
@@ -86,6 +116,18 @@ export function markdownQa(input: RuntimeWorkspaceInput & MarkdownQaOptions = {}
|
|
|
86
116
|
})
|
|
87
117
|
}
|
|
88
118
|
|
|
119
|
+
export function autoCompileNarrative(input: RuntimeNarrativeAutoCompileInput = {}) {
|
|
120
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
121
|
+
return autoCompileNarrativeVault(workspaceRoot, input.touched ?? [])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function extractNarrativeVaultMarkdownPatchTargets(input: RuntimeWorkspaceInput & { patch: string }) {
|
|
125
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
126
|
+
return extractNarrativeVaultMarkdownTargetsFromPatch(input.patch, workspaceRoot)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { formatArtifactQaUserNotice, formatMarkdownQaUserNotice }
|
|
130
|
+
|
|
89
131
|
export function readDeckPlan(input: RuntimeWorkspaceInput = {}) {
|
|
90
132
|
const workspaceRoot = root(input.workspaceRoot)
|
|
91
133
|
const compiled = compileNarrativeVault(workspaceRoot)
|
|
@@ -220,6 +262,30 @@ export function designValidate(input: RuntimeNameInput) {
|
|
|
220
262
|
return validateDesignPackage(requiredName(input, "design"))
|
|
221
263
|
}
|
|
222
264
|
|
|
265
|
+
export function designDraftCreate(input: RuntimeDesignDraftCreateInput) {
|
|
266
|
+
return createDesignDraftPackage({
|
|
267
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
268
|
+
name: requiredString(input?.name, "design name"),
|
|
269
|
+
base: input.base,
|
|
270
|
+
designMd: requiredString(input?.designMd, "designMd"),
|
|
271
|
+
previewHtml: requiredString(input?.previewHtml, "previewHtml"),
|
|
272
|
+
overwrite: input.overwrite ?? false,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function designDraftValidate(input: RuntimeWorkspaceInput & RuntimeNameInput) {
|
|
277
|
+
return validateDesignDraftPackage(root(input.workspaceRoot), requiredName(input, "design draft"))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function designDraftInstall(input: RuntimeDraftInstallInput) {
|
|
281
|
+
seedBuiltinDesigns()
|
|
282
|
+
return installDesignDraftPackage({
|
|
283
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
284
|
+
name: requiredName(input, "design draft"),
|
|
285
|
+
overwrite: input.overwrite ?? false,
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
223
289
|
export interface DesignRulesReadinessResult {
|
|
224
290
|
ok: boolean
|
|
225
291
|
activeDesign: string
|
|
@@ -322,6 +388,42 @@ export function domainActivate(input: RuntimeNameInput) {
|
|
|
322
388
|
}
|
|
323
389
|
}
|
|
324
390
|
|
|
391
|
+
export function domainCreate(input: RuntimeDomainCreateInput) {
|
|
392
|
+
seedBuiltinDomains()
|
|
393
|
+
return createDomainPackage({
|
|
394
|
+
name: requiredString(input?.name, "domain name"),
|
|
395
|
+
domainMd: requiredString(input?.domainMd, "domainMd"),
|
|
396
|
+
overwrite: input.overwrite ?? false,
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function domainValidate(input: RuntimeNameInput) {
|
|
401
|
+
seedBuiltinDomains()
|
|
402
|
+
return validateDomainPackage(requiredName(input, "domain"))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function domainDraftCreate(input: RuntimeDomainDraftCreateInput) {
|
|
406
|
+
return createDomainDraftPackage({
|
|
407
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
408
|
+
name: requiredString(input?.name, "domain name"),
|
|
409
|
+
domainMd: requiredString(input?.domainMd, "domainMd"),
|
|
410
|
+
overwrite: input.overwrite ?? false,
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function domainDraftValidate(input: RuntimeWorkspaceInput & RuntimeNameInput) {
|
|
415
|
+
return validateDomainDraftPackage(root(input.workspaceRoot), requiredName(input, "domain draft"))
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function domainDraftInstall(input: RuntimeDraftInstallInput) {
|
|
419
|
+
seedBuiltinDomains()
|
|
420
|
+
return installDomainDraftPackage({
|
|
421
|
+
workspaceRoot: root(input.workspaceRoot),
|
|
422
|
+
name: requiredName(input, "domain draft"),
|
|
423
|
+
overwrite: input.overwrite ?? false,
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
325
427
|
function root(workspaceRoot: string | undefined): string {
|
|
326
428
|
return resolve(workspaceRoot || process.cwd())
|
|
327
429
|
}
|
|
@@ -334,6 +436,17 @@ function safe<T>(fn: () => T): T | undefined {
|
|
|
334
436
|
}
|
|
335
437
|
}
|
|
336
438
|
|
|
439
|
+
function activeDomainDoctorInfo(): { name: string; description: string } {
|
|
440
|
+
try {
|
|
441
|
+
seedBuiltinDomains()
|
|
442
|
+
const name = activeDomain()
|
|
443
|
+
const description = listDomains().find((domain) => domain.name === name)?.description ?? ""
|
|
444
|
+
return { name, description }
|
|
445
|
+
} catch {
|
|
446
|
+
return { name: safe(activeDomain) ?? "", description: "" }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
337
450
|
function requiredName(input: RuntimeNameInput, label: string): string {
|
|
338
451
|
const name = input?.name?.trim()
|
|
339
452
|
if (!name) throw new Error(`${label} name is required`)
|
package/package.json
CHANGED