@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.
@@ -58,6 +58,7 @@ export type DocumentMaterialsResult = {
58
58
  cache_dir?: string
59
59
  manifest_path?: string
60
60
  text_path?: string
61
+ read_view_path?: string
61
62
  images?: DocumentMaterial[]
62
63
  skipped_assets?: SkippedAsset[]
63
64
  slides?: PptxSlide[]
@@ -74,6 +75,7 @@ type CachedManifest = {
74
75
  cache_dir: string
75
76
  manifest_path: string
76
77
  text_path: string
78
+ read_view_path?: string
77
79
  images: DocumentMaterial[]
78
80
  skipped_assets: SkippedAsset[]
79
81
  slides: PptxSlide[]
@@ -157,6 +159,145 @@ function materialPath(cacheDir: string, workspaceDir: string, ...segments: strin
157
159
  return workspaceRelative(join(cacheDir, ...segments), workspaceDir)
158
160
  }
159
161
 
162
+ function buildReadView(input: {
163
+ source: string
164
+ type: SupportedType
165
+ fingerprint: string
166
+ text: string
167
+ manifestPath: string
168
+ textPath: string
169
+ images: DocumentMaterial[]
170
+ skippedAssets: SkippedAsset[]
171
+ tables: DocumentMaterial[]
172
+ slides: PptxSlide[] | undefined
173
+ }): string {
174
+ const lines = [
175
+ `# Extracted Material: ${basename(input.source)}`,
176
+ "",
177
+ "## Source",
178
+ "",
179
+ `- sourcePath: ${input.source}`,
180
+ `- type: ${input.type}`,
181
+ `- fingerprint: ${input.fingerprint}`,
182
+ `- manifestPath: ${input.manifestPath}`,
183
+ `- textPath: ${input.textPath}`,
184
+ "",
185
+ "## Text",
186
+ "",
187
+ input.text.trim() || "No text extracted.",
188
+ "",
189
+ "## Extracted Images",
190
+ "",
191
+ ]
192
+
193
+ if (input.images.length === 0) lines.push("- None")
194
+ else {
195
+ for (const image of input.images) {
196
+ const parts = [
197
+ image.page_or_slide ? `page_or_slide: ${image.page_or_slide}` : null,
198
+ `source_ref: ${image.source_ref}`,
199
+ image.note ? `note: ${image.note}` : null,
200
+ ].filter(Boolean).join("; ")
201
+ lines.push(`- ${image.path}${parts ? ` (${parts})` : ""}`)
202
+ }
203
+ }
204
+
205
+ if (input.skippedAssets.length > 0) {
206
+ lines.push("", "## Skipped Or Unmapped Assets", "")
207
+ for (const asset of input.skippedAssets) {
208
+ const parts = [
209
+ asset.page_or_slide ? `page_or_slide: ${asset.page_or_slide}` : null,
210
+ `reason: ${asset.reason}`,
211
+ asset.kind ? `kind: ${asset.kind}` : null,
212
+ ].filter(Boolean).join("; ")
213
+ lines.push(`- ${asset.source_ref}${parts ? ` (${parts})` : ""}`)
214
+ }
215
+ }
216
+
217
+ if (input.tables.length > 0) {
218
+ lines.push("", "## Extracted Tables", "")
219
+ for (const table of input.tables) lines.push(`- ${table.path} (${table.note ?? table.source_ref})`)
220
+ }
221
+
222
+ if (input.slides?.length) {
223
+ lines.push("", "## Slide Structure", "")
224
+ for (const slide of input.slides) {
225
+ const textCount = slide.elements.filter((element) => element.kind === "text").length
226
+ const imageCount = slide.elements.filter((element) => element.kind === "image").length
227
+ const shapeCount = slide.elements.filter((element) => element.kind === "shape").length
228
+ lines.push(`- ${slide.slide}: ${textCount} text, ${imageCount} image, ${shapeCount} shape`)
229
+ }
230
+ }
231
+
232
+ lines.push(
233
+ "",
234
+ "## Intake Rules",
235
+ "",
236
+ "- Treat this extracted material as source context until a material review records what was considered.",
237
+ "- Do not treat extracted images as interpreted evidence unless an explicit image review or user-provided meaning exists.",
238
+ "- Canonical evidence still requires source trace, quote/snippet, support scope, unsupported scope, caveat, strength, and relations in `revela-narrative/`.",
239
+ )
240
+
241
+ return lines.join("\n")
242
+ }
243
+
244
+ function writeReadView(input: {
245
+ cacheDir: string
246
+ workspaceDir: string
247
+ source: string
248
+ type: SupportedType
249
+ fingerprint: string
250
+ text: string
251
+ manifestPath: string
252
+ textPath: string
253
+ images: DocumentMaterial[]
254
+ skippedAssets: SkippedAsset[]
255
+ tables: DocumentMaterial[]
256
+ slides?: PptxSlide[]
257
+ }): string {
258
+ const readViewPath = join(input.cacheDir, "read.md")
259
+ writeFileSync(readViewPath, buildReadView({
260
+ source: input.source,
261
+ type: input.type,
262
+ fingerprint: input.fingerprint,
263
+ text: input.text,
264
+ manifestPath: input.manifestPath,
265
+ textPath: input.textPath,
266
+ images: input.images,
267
+ skippedAssets: input.skippedAssets,
268
+ tables: input.tables,
269
+ slides: input.slides,
270
+ }), "utf-8")
271
+ return workspaceRelative(readViewPath, input.workspaceDir)
272
+ }
273
+
274
+ function ensureCachedReadView(
275
+ manifest: CachedManifest,
276
+ cacheDir: string,
277
+ workspaceDir: string,
278
+ ): string {
279
+ const existing = manifest.read_view_path
280
+ if (existing && existsSync(join(workspaceDir, existing))) return existing
281
+
282
+ const text = existsSync(join(workspaceDir, manifest.text_path))
283
+ ? readFileSync(join(workspaceDir, manifest.text_path), "utf-8").replace(/^\[Extracted from: .*?\]\n\n/, "")
284
+ : ""
285
+ return writeReadView({
286
+ cacheDir,
287
+ workspaceDir,
288
+ source: manifest.source,
289
+ type: manifest.type,
290
+ fingerprint: manifest.fingerprint,
291
+ text,
292
+ manifestPath: manifest.manifest_path,
293
+ textPath: manifest.text_path,
294
+ images: manifest.images,
295
+ skippedAssets: manifest.skipped_assets,
296
+ tables: manifest.tables,
297
+ slides: manifest.slides,
298
+ })
299
+ }
300
+
160
301
  function updateDecksSourceMaterialIndex(
161
302
  workspaceDir: string,
162
303
  filePath: string,
@@ -716,6 +857,7 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
716
857
 
717
858
  if (existsSync(manifestPath)) {
718
859
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as CachedManifest
860
+ const readViewPath = ensureCachedReadView(manifest, cacheDir, workspaceDir)
719
861
  return {
720
862
  status: "processed",
721
863
  cache_status: "hit",
@@ -724,6 +866,7 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
724
866
  cache_dir: manifest.cache_dir,
725
867
  manifest_path: manifest.manifest_path,
726
868
  text_path: manifest.text_path,
869
+ read_view_path: readViewPath,
727
870
  images: manifest.images,
728
871
  skipped_assets: manifest.skipped_assets,
729
872
  slides: manifest.slides,
@@ -740,6 +883,22 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
740
883
  writeFileSync(textPath, `[Extracted from: ${basename(filePath)}]\n\n${text}`, "utf-8")
741
884
 
742
885
  const images = await extractPdfImages(buf, cacheDir, workspaceDir)
886
+ const relativeManifestPath = workspaceRelative(manifestPath, workspaceDir)
887
+ const relativeTextPath = workspaceRelative(textPath, workspaceDir)
888
+ const readViewPath = writeReadView({
889
+ cacheDir,
890
+ workspaceDir,
891
+ source: relativeSource,
892
+ type: "pdf",
893
+ fingerprint,
894
+ text,
895
+ manifestPath: relativeManifestPath,
896
+ textPath: relativeTextPath,
897
+ images,
898
+ skippedAssets: [],
899
+ tables: [],
900
+ slides: [],
901
+ })
743
902
 
744
903
  const result: DocumentMaterialsResult = {
745
904
  status: "processed",
@@ -747,8 +906,9 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
747
906
  source: relativeSource,
748
907
  type: "pdf",
749
908
  cache_dir: workspaceRelative(cacheDir, workspaceDir),
750
- manifest_path: workspaceRelative(manifestPath, workspaceDir),
751
- text_path: workspaceRelative(textPath, workspaceDir),
909
+ manifest_path: relativeManifestPath,
910
+ text_path: relativeTextPath,
911
+ read_view_path: readViewPath,
752
912
  images,
753
913
  skipped_assets: [],
754
914
  slides: [],
@@ -762,6 +922,7 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
762
922
  cache_dir: result.cache_dir!,
763
923
  manifest_path: result.manifest_path!,
764
924
  text_path: result.text_path!,
925
+ read_view_path: result.read_view_path,
765
926
  images: result.images ?? [],
766
927
  skipped_assets: [],
767
928
  slides: [],
@@ -780,6 +941,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
780
941
 
781
942
  if (existsSync(manifestPath)) {
782
943
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as CachedManifest
944
+ const readViewPath = ensureCachedReadView(manifest, cacheDir, workspaceDir)
783
945
  return {
784
946
  status: "processed",
785
947
  cache_status: "hit",
@@ -788,6 +950,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
788
950
  cache_dir: manifest.cache_dir,
789
951
  manifest_path: manifest.manifest_path,
790
952
  text_path: manifest.text_path,
953
+ read_view_path: readViewPath,
791
954
  images: manifest.images,
792
955
  skipped_assets: manifest.skipped_assets,
793
956
  slides: manifest.slides,
@@ -821,6 +984,24 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
821
984
  const slides = type === "pptx"
822
985
  ? extractPptxSlides(files, images, pptxAssets!.skipped_assets)
823
986
  : undefined
987
+ const relativeManifestPath = workspaceRelative(manifestPath, workspaceDir)
988
+ const relativeTextPath = workspaceRelative(textPath, workspaceDir)
989
+ const tables = extractTables(type, relativeTextPath)
990
+ const skippedAssets = pptxAssets?.skipped_assets ?? []
991
+ const readViewPath = writeReadView({
992
+ cacheDir,
993
+ workspaceDir,
994
+ source: relativeSource,
995
+ type,
996
+ fingerprint,
997
+ text,
998
+ manifestPath: relativeManifestPath,
999
+ textPath: relativeTextPath,
1000
+ images,
1001
+ skippedAssets,
1002
+ tables,
1003
+ slides,
1004
+ })
824
1005
 
825
1006
  const result: DocumentMaterialsResult = {
826
1007
  status: "processed",
@@ -828,12 +1009,13 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
828
1009
  source: relativeSource,
829
1010
  type,
830
1011
  cache_dir: workspaceRelative(cacheDir, workspaceDir),
831
- manifest_path: workspaceRelative(manifestPath, workspaceDir),
832
- text_path: workspaceRelative(textPath, workspaceDir),
1012
+ manifest_path: relativeManifestPath,
1013
+ text_path: relativeTextPath,
1014
+ read_view_path: readViewPath,
833
1015
  images,
834
- skipped_assets: pptxAssets?.skipped_assets ?? [],
1016
+ skipped_assets: skippedAssets,
835
1017
  slides,
836
- tables: extractTables(type, workspaceRelative(textPath, workspaceDir)),
1018
+ tables,
837
1019
  }
838
1020
 
839
1021
  const manifest: CachedManifest = {
@@ -843,6 +1025,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
843
1025
  cache_dir: result.cache_dir!,
844
1026
  manifest_path: result.manifest_path!,
845
1027
  text_path: result.text_path!,
1028
+ read_view_path: result.read_view_path,
846
1029
  images: result.images ?? [],
847
1030
  skipped_assets: result.skipped_assets ?? [],
848
1031
  slides: result.slides ?? [],
@@ -22,7 +22,7 @@ import {
22
22
  statSync,
23
23
  writeFileSync,
24
24
  } from "fs"
25
- import { join, resolve, basename } from "path"
25
+ import { dirname, join, resolve, basename } from "path"
26
26
  import { tmpdir } from "os"
27
27
  import { parseFrontmatter } from "../frontmatter"
28
28
  import {
@@ -49,6 +49,44 @@ export interface DomainInfo {
49
49
  skillText: string
50
50
  }
51
51
 
52
+ export interface CreateDomainPackageArgs {
53
+ name: string
54
+ domainMd: string
55
+ overwrite?: boolean
56
+ }
57
+
58
+ export interface CreateDomainDraftArgs extends CreateDomainPackageArgs {
59
+ workspaceRoot: string
60
+ }
61
+
62
+ export interface CreateDomainPackageResult {
63
+ ok: true
64
+ name: string
65
+ path: string
66
+ files: string[]
67
+ overwritten: boolean
68
+ }
69
+
70
+ export interface InstallDomainDraftArgs {
71
+ workspaceRoot: string
72
+ name: string
73
+ overwrite?: boolean
74
+ }
75
+
76
+ export interface InstallDomainDraftResult extends CreateDomainPackageResult {
77
+ sourcePath: string
78
+ }
79
+
80
+ export interface ValidateDomainPackageResult {
81
+ ok: boolean
82
+ name: string
83
+ path: string
84
+ hasIndustryMd: boolean
85
+ hasRequiredFrontmatter: boolean
86
+ hasBody: boolean
87
+ errors: string[]
88
+ }
89
+
52
90
  // ---------------------------------------------------------------------------
53
91
  // Seed
54
92
  // ---------------------------------------------------------------------------
@@ -148,6 +186,188 @@ export function getDomainSkillMd(name?: string): string {
148
186
  return info.skillText
149
187
  }
150
188
 
189
+ /** Normalize and validate a domain package name. */
190
+ export function normalizeDomainName(name: string): string {
191
+ const normalized = name.trim().toLowerCase()
192
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(normalized)) {
193
+ throw new Error("Domain name must be kebab-case using lowercase letters, numbers, and hyphens")
194
+ }
195
+ return normalized
196
+ }
197
+
198
+ /** Create a local domain package in ~/.config/revela/domains/<name>/. */
199
+ export function createDomainPackage(args: CreateDomainPackageArgs): CreateDomainPackageResult {
200
+ const name = normalizeDomainName(args.name)
201
+ const domainMd = args.domainMd?.trim()
202
+
203
+ if (!domainMd) throw new Error("domainMd is required")
204
+
205
+ const target = join(DOMAINS_DIR, name)
206
+ const existed = existsSync(target)
207
+ if (existed && !args.overwrite) {
208
+ throw new Error(`Domain '${name}' already exists. Pass overwrite=true to replace it.`)
209
+ }
210
+
211
+ mkdirSync(DOMAINS_DIR, { recursive: true })
212
+ if (existed) {
213
+ rmSync(target, { recursive: true, force: true })
214
+ }
215
+ mkdirSync(target, { recursive: true })
216
+ writeFileSync(join(target, DOMAIN_FILE), `${domainMd}\n`, "utf-8")
217
+
218
+ const validation = validateDomainPackage(name)
219
+ if (!validation.ok) {
220
+ throw new Error(`Created domain package is invalid: ${validation.errors.join("; ")}`)
221
+ }
222
+
223
+ return {
224
+ ok: true,
225
+ name,
226
+ path: target,
227
+ files: [DOMAIN_FILE],
228
+ overwritten: existed,
229
+ }
230
+ }
231
+
232
+ /** Create a project-local domain draft under .revela/drafts/domains/<name>/. */
233
+ export function createDomainDraftPackage(args: CreateDomainDraftArgs): CreateDomainPackageResult {
234
+ const name = normalizeDomainName(args.name)
235
+ const domainMd = args.domainMd?.trim()
236
+
237
+ if (!domainMd) throw new Error("domainMd is required")
238
+
239
+ const target = domainDraftDir(args.workspaceRoot, name)
240
+ const existed = existsSync(target)
241
+ if (existed && !args.overwrite) {
242
+ throw new Error(`Domain draft '${name}' already exists. Pass overwrite=true to replace it.`)
243
+ }
244
+
245
+ mkdirSync(dirname(target), { recursive: true })
246
+ if (existed) {
247
+ rmSync(target, { recursive: true, force: true })
248
+ }
249
+ mkdirSync(target, { recursive: true })
250
+ writeFileSync(join(target, DOMAIN_FILE), `${domainMd}\n`, "utf-8")
251
+
252
+ const validation = validateDomainDraftPackage(args.workspaceRoot, name)
253
+ if (!validation.ok) {
254
+ throw new Error(`Created domain draft is invalid: ${validation.errors.join("; ")}`)
255
+ }
256
+
257
+ return {
258
+ ok: true,
259
+ name,
260
+ path: target,
261
+ files: [DOMAIN_FILE],
262
+ overwritten: existed,
263
+ }
264
+ }
265
+
266
+ /** Validate a project-local domain draft. */
267
+ export function validateDomainDraftPackage(workspaceRoot: string, nameInput: string): ValidateDomainPackageResult {
268
+ let name = nameInput
269
+ try {
270
+ name = normalizeDomainName(nameInput)
271
+ } catch {
272
+ // validateDomainPackageAt records the invalid-name error.
273
+ }
274
+ return validateDomainPackageAt(nameInput, domainDraftDir(workspaceRoot, name))
275
+ }
276
+
277
+ /** Install a validated project-local domain draft into the user-level domain registry. */
278
+ export function installDomainDraftPackage(args: InstallDomainDraftArgs): InstallDomainDraftResult {
279
+ const name = normalizeDomainName(args.name)
280
+ const sourcePath = domainDraftDir(args.workspaceRoot, name)
281
+ const validation = validateDomainDraftPackage(args.workspaceRoot, name)
282
+ if (!validation.ok) {
283
+ throw new Error(`Domain draft is invalid: ${validation.errors.join("; ")}`)
284
+ }
285
+
286
+ const target = join(DOMAINS_DIR, name)
287
+ const existed = existsSync(target)
288
+ if (existed && !args.overwrite) {
289
+ throw new Error(`Domain '${name}' already exists. Pass overwrite=true to replace it.`)
290
+ }
291
+
292
+ try {
293
+ mkdirSync(DOMAINS_DIR, { recursive: true })
294
+ if (existed) {
295
+ rmSync(target, { recursive: true, force: true })
296
+ }
297
+ cpSync(sourcePath, target, { recursive: true })
298
+ } catch (e) {
299
+ throw new Error(`Installing domain draft requires write access to Revela user config at ${DOMAINS_DIR}: ${e instanceof Error ? e.message : String(e)}`)
300
+ }
301
+
302
+ return {
303
+ ok: true,
304
+ name,
305
+ path: target,
306
+ sourcePath,
307
+ files: [DOMAIN_FILE],
308
+ overwritten: existed,
309
+ }
310
+ }
311
+
312
+ /** Validate a local domain package for the minimum Revela domain contract. */
313
+ export function validateDomainPackage(nameInput: string): ValidateDomainPackageResult {
314
+ let name = nameInput
315
+ try {
316
+ name = normalizeDomainName(nameInput)
317
+ } catch {
318
+ // validateDomainPackageAt records the invalid-name error.
319
+ }
320
+ return validateDomainPackageAt(nameInput, join(DOMAINS_DIR, name))
321
+ }
322
+
323
+ function validateDomainPackageAt(nameInput: string, dir: string): ValidateDomainPackageResult {
324
+ let name = nameInput
325
+ const errors: string[] = []
326
+ try {
327
+ name = normalizeDomainName(nameInput)
328
+ } catch (e) {
329
+ errors.push(e instanceof Error ? e.message : String(e))
330
+ }
331
+
332
+ const mdPath = join(dir, DOMAIN_FILE)
333
+ const hasIndustryMd = existsSync(mdPath)
334
+ let hasRequiredFrontmatter = false
335
+ let hasBody = false
336
+
337
+ if (!existsSync(dir)) errors.push(`Domain directory does not exist: ${dir}`)
338
+ if (!hasIndustryMd) errors.push(`${DOMAIN_FILE} is missing`)
339
+
340
+ if (hasIndustryMd) {
341
+ try {
342
+ const text = readFileSync(mdPath, "utf-8")
343
+ const { meta, body } = parseFrontmatter(text)
344
+ const required = ["name", "description", "author", "version"]
345
+ const missing = required.filter((field) => !meta[field]?.trim())
346
+ hasRequiredFrontmatter = missing.length === 0
347
+ hasBody = body.trim().length > 0
348
+ if (!hasRequiredFrontmatter) errors.push(`INDUSTRY.md frontmatter is missing required field(s): ${missing.join(", ")}`)
349
+ if (!hasBody) errors.push("INDUSTRY.md body/guidance is empty")
350
+ if (meta.name && meta.name.trim() !== name) errors.push(`INDUSTRY.md frontmatter name '${meta.name.trim()}' does not match package name '${name}'`)
351
+ } catch (e) {
352
+ errors.push(`INDUSTRY.md could not be parsed: ${e instanceof Error ? e.message : String(e)}`)
353
+ }
354
+ }
355
+
356
+ return {
357
+ ok: errors.length === 0,
358
+ name,
359
+ path: dir,
360
+ hasIndustryMd,
361
+ hasRequiredFrontmatter,
362
+ hasBody,
363
+ errors,
364
+ }
365
+ }
366
+
367
+ function domainDraftDir(workspaceRoot: string, name: string): string {
368
+ return resolve(workspaceRoot, ".revela", "drafts", "domains", name)
369
+ }
370
+
151
371
  /** Remove an installed domain. Throws if not found or is the protected default. */
152
372
  export function removeDomain(name: string): void {
153
373
  if (name === DEFAULT_DOMAIN) {