@nathanvale/chatline 0.0.1
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 +1 -0
- package/LICENSE +21 -0
- package/README.md +1535 -0
- package/dist/bin/index.js +5121 -0
- package/dist/cli/commands/clean.d.ts +17 -0
- package/dist/cli/commands/clean.d.ts.map +1 -0
- package/dist/cli/commands/clean.js +142 -0
- package/dist/cli/commands/clean.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +17 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +202 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/enrich-ai.d.ts +17 -0
- package/dist/cli/commands/enrich-ai.d.ts.map +1 -0
- package/dist/cli/commands/enrich-ai.js +371 -0
- package/dist/cli/commands/enrich-ai.js.map +1 -0
- package/dist/cli/commands/index.d.ts +16 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +16 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/ingest-csv.d.ts +17 -0
- package/dist/cli/commands/ingest-csv.d.ts.map +1 -0
- package/dist/cli/commands/ingest-csv.js +138 -0
- package/dist/cli/commands/ingest-csv.js.map +1 -0
- package/dist/cli/commands/ingest-db.d.ts +17 -0
- package/dist/cli/commands/ingest-db.d.ts.map +1 -0
- package/dist/cli/commands/ingest-db.js +159 -0
- package/dist/cli/commands/ingest-db.js.map +1 -0
- package/dist/cli/commands/init.d.ts +17 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +110 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/normalize-link.d.ts +16 -0
- package/dist/cli/commands/normalize-link.d.ts.map +1 -0
- package/dist/cli/commands/normalize-link.js +144 -0
- package/dist/cli/commands/normalize-link.js.map +1 -0
- package/dist/cli/commands/render-markdown.d.ts +17 -0
- package/dist/cli/commands/render-markdown.d.ts.map +1 -0
- package/dist/cli/commands/render-markdown.js +218 -0
- package/dist/cli/commands/render-markdown.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +17 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +175 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +17 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +152 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +121 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/types.d.ts +93 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +7 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/utils.d.ts +29 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +53 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1805 -0
- package/dist/config/generator.d.ts +90 -0
- package/dist/config/generator.d.ts.map +1 -0
- package/dist/config/generator.js +320 -0
- package/dist/config/generator.js.map +1 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +251 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +107 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +169 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/enrich/audio-transcription.d.ts +77 -0
- package/dist/enrich/audio-transcription.d.ts.map +1 -0
- package/dist/enrich/audio-transcription.js +370 -0
- package/dist/enrich/audio-transcription.js.map +1 -0
- package/dist/enrich/checkpoint.d.ts +137 -0
- package/dist/enrich/checkpoint.d.ts.map +1 -0
- package/dist/enrich/checkpoint.js +205 -0
- package/dist/enrich/checkpoint.js.map +1 -0
- package/dist/enrich/idempotency.d.ts +90 -0
- package/dist/enrich/idempotency.d.ts.map +1 -0
- package/dist/enrich/idempotency.js +188 -0
- package/dist/enrich/idempotency.js.map +1 -0
- package/dist/enrich/image-analysis.d.ts +62 -0
- package/dist/enrich/image-analysis.d.ts.map +1 -0
- package/dist/enrich/image-analysis.js +264 -0
- package/dist/enrich/image-analysis.js.map +1 -0
- package/dist/enrich/index.d.ts +60 -0
- package/dist/enrich/index.d.ts.map +1 -0
- package/dist/enrich/index.js +74 -0
- package/dist/enrich/index.js.map +1 -0
- package/dist/enrich/link-enrichment.d.ts +37 -0
- package/dist/enrich/link-enrichment.d.ts.map +1 -0
- package/dist/enrich/link-enrichment.js +202 -0
- package/dist/enrich/link-enrichment.js.map +1 -0
- package/dist/enrich/pdf-video-handling.d.ts +49 -0
- package/dist/enrich/pdf-video-handling.d.ts.map +1 -0
- package/dist/enrich/pdf-video-handling.js +325 -0
- package/dist/enrich/pdf-video-handling.js.map +1 -0
- package/dist/enrich/progress-tracker.d.ts +120 -0
- package/dist/enrich/progress-tracker.d.ts.map +1 -0
- package/dist/enrich/progress-tracker.js +220 -0
- package/dist/enrich/progress-tracker.js.map +1 -0
- package/dist/enrich/providers/firecrawl.d.ts +18 -0
- package/dist/enrich/providers/firecrawl.d.ts.map +1 -0
- package/dist/enrich/providers/firecrawl.js +48 -0
- package/dist/enrich/providers/firecrawl.js.map +1 -0
- package/dist/enrich/providers/generic.d.ts +16 -0
- package/dist/enrich/providers/generic.d.ts.map +1 -0
- package/dist/enrich/providers/generic.js +36 -0
- package/dist/enrich/providers/generic.js.map +1 -0
- package/dist/enrich/providers/index.d.ts +14 -0
- package/dist/enrich/providers/index.d.ts.map +1 -0
- package/dist/enrich/providers/index.js +13 -0
- package/dist/enrich/providers/index.js.map +1 -0
- package/dist/enrich/providers/instagram.d.ts +16 -0
- package/dist/enrich/providers/instagram.d.ts.map +1 -0
- package/dist/enrich/providers/instagram.js +43 -0
- package/dist/enrich/providers/instagram.js.map +1 -0
- package/dist/enrich/providers/spotify.d.ts +16 -0
- package/dist/enrich/providers/spotify.d.ts.map +1 -0
- package/dist/enrich/providers/spotify.js +45 -0
- package/dist/enrich/providers/spotify.js.map +1 -0
- package/dist/enrich/providers/twitter.d.ts +16 -0
- package/dist/enrich/providers/twitter.d.ts.map +1 -0
- package/dist/enrich/providers/twitter.js +43 -0
- package/dist/enrich/providers/twitter.js.map +1 -0
- package/dist/enrich/providers/types.d.ts +47 -0
- package/dist/enrich/providers/types.d.ts.map +1 -0
- package/dist/enrich/providers/types.js +15 -0
- package/dist/enrich/providers/types.js.map +1 -0
- package/dist/enrich/providers/youtube.d.ts +16 -0
- package/dist/enrich/providers/youtube.d.ts.map +1 -0
- package/dist/enrich/providers/youtube.js +43 -0
- package/dist/enrich/providers/youtube.js.map +1 -0
- package/dist/enrich/rate-limiting.d.ts +118 -0
- package/dist/enrich/rate-limiting.d.ts.map +1 -0
- package/dist/enrich/rate-limiting.js +258 -0
- package/dist/enrich/rate-limiting.js.map +1 -0
- package/dist/index.d.ts +688 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1729 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/dedup-merge.d.ts +82 -0
- package/dist/ingest/dedup-merge.d.ts.map +1 -0
- package/dist/ingest/dedup-merge.js +262 -0
- package/dist/ingest/dedup-merge.js.map +1 -0
- package/dist/ingest/ingest-csv.d.ts +62 -0
- package/dist/ingest/ingest-csv.d.ts.map +1 -0
- package/dist/ingest/ingest-csv.js +300 -0
- package/dist/ingest/ingest-csv.js.map +1 -0
- package/dist/ingest/ingest-db.d.ts +64 -0
- package/dist/ingest/ingest-db.d.ts.map +1 -0
- package/dist/ingest/ingest-db.js +172 -0
- package/dist/ingest/ingest-db.js.map +1 -0
- package/dist/ingest/link-replies-and-tapbacks.d.ts +53 -0
- package/dist/ingest/link-replies-and-tapbacks.d.ts.map +1 -0
- package/dist/ingest/link-replies-and-tapbacks.js +381 -0
- package/dist/ingest/link-replies-and-tapbacks.js.map +1 -0
- package/dist/normalize/date-converters.d.ts +45 -0
- package/dist/normalize/date-converters.d.ts.map +1 -0
- package/dist/normalize/date-converters.js +166 -0
- package/dist/normalize/date-converters.js.map +1 -0
- package/dist/normalize/path-validator.d.ts +65 -0
- package/dist/normalize/path-validator.d.ts.map +1 -0
- package/dist/normalize/path-validator.js +221 -0
- package/dist/normalize/path-validator.js.map +1 -0
- package/dist/normalize/validate-normalized.d.ts +45 -0
- package/dist/normalize/validate-normalized.d.ts.map +1 -0
- package/dist/normalize/validate-normalized.js +144 -0
- package/dist/normalize/validate-normalized.js.map +1 -0
- package/dist/render/embeds-blockquotes.d.ts +84 -0
- package/dist/render/embeds-blockquotes.d.ts.map +1 -0
- package/dist/render/embeds-blockquotes.js +204 -0
- package/dist/render/embeds-blockquotes.js.map +1 -0
- package/dist/render/grouping.d.ts +78 -0
- package/dist/render/grouping.d.ts.map +1 -0
- package/dist/render/grouping.js +134 -0
- package/dist/render/grouping.js.map +1 -0
- package/dist/render/index.d.ts +47 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +245 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/reply-rendering.d.ts +88 -0
- package/dist/render/reply-rendering.d.ts.map +1 -0
- package/dist/render/reply-rendering.js +196 -0
- package/dist/render/reply-rendering.js.map +1 -0
- package/dist/schema/message.d.ts +125 -0
- package/dist/schema/message.d.ts.map +1 -0
- package/dist/schema/message.js +331 -0
- package/dist/schema/message.js.map +1 -0
- package/dist/utils/delta-detection.d.ts +107 -0
- package/dist/utils/delta-detection.d.ts.map +1 -0
- package/dist/utils/delta-detection.js +199 -0
- package/dist/utils/delta-detection.js.map +1 -0
- package/dist/utils/enrichment-merge.d.ts +135 -0
- package/dist/utils/enrichment-merge.d.ts.map +1 -0
- package/dist/utils/enrichment-merge.js +280 -0
- package/dist/utils/enrichment-merge.js.map +1 -0
- package/dist/utils/human.d.ts +15 -0
- package/dist/utils/human.d.ts.map +1 -0
- package/dist/utils/human.js +27 -0
- package/dist/utils/human.js.map +1 -0
- package/dist/utils/incremental-state.d.ts +133 -0
- package/dist/utils/incremental-state.d.ts.map +1 -0
- package/dist/utils/incremental-state.js +237 -0
- package/dist/utils/incremental-state.js.map +1 -0
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +176 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +165 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { MediaProvenance, Message } from '../schema/message';
|
|
2
|
+
export type PathValidationConfig = {
|
|
3
|
+
attachmentRoots: string[];
|
|
4
|
+
source: 'csv' | 'db' | 'merged';
|
|
5
|
+
};
|
|
6
|
+
export type PathValidationStats = {
|
|
7
|
+
total: number;
|
|
8
|
+
found: number;
|
|
9
|
+
missing: number;
|
|
10
|
+
notAbsolute: number;
|
|
11
|
+
errors: Array<{
|
|
12
|
+
guid: string;
|
|
13
|
+
filename: string;
|
|
14
|
+
error: string;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
export type PathValidationResult = {
|
|
18
|
+
messages: Message[];
|
|
19
|
+
stats: PathValidationStats;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Validate that a path is absolute (starts with /)
|
|
23
|
+
* AC01: All media.path fields are absolute paths when files exist on disk
|
|
24
|
+
*/
|
|
25
|
+
export declare function isAbsolutePath(filePath: string | null | undefined): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Check if a file exists on the filesystem
|
|
28
|
+
*/
|
|
29
|
+
export declare function fileExists(filePath: string | null | undefined): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Expand tilde in paths to home directory
|
|
32
|
+
*/
|
|
33
|
+
export declare function expandTildeInPath(filePath: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Search for an attachment file in configured root directories
|
|
36
|
+
* AC04: Support multiple attachment root directories from config
|
|
37
|
+
* Tries multiple strategies:
|
|
38
|
+
* 1. If filename is absolute and exists, return it
|
|
39
|
+
* 2. Try direct join of root + filename
|
|
40
|
+
* 3. Search for basename in root
|
|
41
|
+
*/
|
|
42
|
+
export declare function searchAttachmentInRoots(filename: string, attachmentRoots: string[]): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Infer the source (csv/db) from message metadata or GUID pattern
|
|
45
|
+
*/
|
|
46
|
+
export declare function inferSource(message: Message, defaultSource: 'csv' | 'db' | 'merged'): 'csv' | 'db';
|
|
47
|
+
/**
|
|
48
|
+
* Create provenance metadata for a media file
|
|
49
|
+
* AC02: Missing files retain original filename with provenance metadata
|
|
50
|
+
*/
|
|
51
|
+
export declare function createProvenance(message: Message, source: 'csv' | 'db' | 'merged'): MediaProvenance;
|
|
52
|
+
/**
|
|
53
|
+
* Validate and enforce absolute paths for all media messages
|
|
54
|
+
*
|
|
55
|
+
* AC01: All media.path fields are absolute paths when files exist on disk
|
|
56
|
+
* AC02: Missing files retain original filename with provenance metadata
|
|
57
|
+
* AC03: Path validation errors reported with counters (found vs missing)
|
|
58
|
+
* AC04: Support multiple attachment root directories from config
|
|
59
|
+
*/
|
|
60
|
+
export declare function validateAndEnforcePaths(messages: Message[], config: PathValidationConfig): PathValidationResult;
|
|
61
|
+
/**
|
|
62
|
+
* Format path validation statistics as human-readable string for logging
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatPathValidationStats(stats: PathValidationStats): string;
|
|
65
|
+
//# sourceMappingURL=path-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-validator.d.ts","sourceRoot":"","sources":["../../src/normalize/path-validator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAIjE,MAAM,MAAM,oBAAoB,GAAG;IAClC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,MAAM,EAAE,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,QAAQ,EAAE,MAAM,CAAA;QAChB,KAAK,EAAE,MAAM,CAAA;KACb,CAAC,CAAA;CACF,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IAClC,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,KAAK,EAAE,mBAAmB,CAAA;CAC1B,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAK3E;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CASvE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK1D;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EAAE,GACvB,MAAM,GAAG,IAAI,CA4Bf;AAED;;GAEG;AACH,wBAAgB,WAAW,CAC1B,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,KAAK,GAAG,IAAI,GAAG,QAAQ,GACpC,KAAK,GAAG,IAAI,CAqBd;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,GAAG,IAAI,GAAG,QAAQ,GAC7B,eAAe,CAOjB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,OAAO,EAAE,EACnB,MAAM,EAAE,oBAAoB,GAC1B,oBAAoB,CA8FtB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,CAoB5E"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Validate that a path is absolute (starts with /)
|
|
6
|
+
* AC01: All media.path fields are absolute paths when files exist on disk
|
|
7
|
+
*/
|
|
8
|
+
export function isAbsolutePath(filePath) {
|
|
9
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return filePath.startsWith('/');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if a file exists on the filesystem
|
|
16
|
+
*/
|
|
17
|
+
export function fileExists(filePath) {
|
|
18
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return existsSync(filePath);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Expand tilde in paths to home directory
|
|
30
|
+
*/
|
|
31
|
+
export function expandTildeInPath(filePath) {
|
|
32
|
+
if (filePath.startsWith('~')) {
|
|
33
|
+
return filePath.replace('~', os.homedir());
|
|
34
|
+
}
|
|
35
|
+
return filePath;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Search for an attachment file in configured root directories
|
|
39
|
+
* AC04: Support multiple attachment root directories from config
|
|
40
|
+
* Tries multiple strategies:
|
|
41
|
+
* 1. If filename is absolute and exists, return it
|
|
42
|
+
* 2. Try direct join of root + filename
|
|
43
|
+
* 3. Search for basename in root
|
|
44
|
+
*/
|
|
45
|
+
export function searchAttachmentInRoots(filename, attachmentRoots) {
|
|
46
|
+
if (!filename || attachmentRoots.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
for (const root of attachmentRoots) {
|
|
50
|
+
// Expand tilde in root directory
|
|
51
|
+
const expandedRoot = expandTildeInPath(root);
|
|
52
|
+
// Try direct file join
|
|
53
|
+
const directPath = path.join(expandedRoot, filename);
|
|
54
|
+
if (fileExists(directPath)) {
|
|
55
|
+
return directPath;
|
|
56
|
+
}
|
|
57
|
+
// Try basename matching (file in any subdirectory of root)
|
|
58
|
+
try {
|
|
59
|
+
const basename = path.basename(filename);
|
|
60
|
+
const basenameOnly = path.join(expandedRoot, basename);
|
|
61
|
+
if (fileExists(basenameOnly)) {
|
|
62
|
+
return basenameOnly;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Continue to next root
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Infer the source (csv/db) from message metadata or GUID pattern
|
|
73
|
+
*/
|
|
74
|
+
export function inferSource(message, defaultSource) {
|
|
75
|
+
// Check guid pattern
|
|
76
|
+
if (message.guid?.startsWith('p:')) {
|
|
77
|
+
// Part GUID format: p:index/guid
|
|
78
|
+
return 'db';
|
|
79
|
+
}
|
|
80
|
+
if (message.guid?.startsWith('csv:')) {
|
|
81
|
+
return 'csv';
|
|
82
|
+
}
|
|
83
|
+
// Check if there's explicit metadata
|
|
84
|
+
const msgWithMeta = message;
|
|
85
|
+
if (msgWithMeta.exportMetadata?.source === 'csv') {
|
|
86
|
+
return 'csv';
|
|
87
|
+
}
|
|
88
|
+
if (msgWithMeta.exportMetadata?.source === 'db') {
|
|
89
|
+
return 'db';
|
|
90
|
+
}
|
|
91
|
+
// Default fallback
|
|
92
|
+
return defaultSource === 'csv' ? 'csv' : 'db';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create provenance metadata for a media file
|
|
96
|
+
* AC02: Missing files retain original filename with provenance metadata
|
|
97
|
+
*/
|
|
98
|
+
export function createProvenance(message, source) {
|
|
99
|
+
const now = new Date().toISOString();
|
|
100
|
+
return {
|
|
101
|
+
source: source,
|
|
102
|
+
lastSeen: message.date,
|
|
103
|
+
resolvedAt: now,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validate and enforce absolute paths for all media messages
|
|
108
|
+
*
|
|
109
|
+
* AC01: All media.path fields are absolute paths when files exist on disk
|
|
110
|
+
* AC02: Missing files retain original filename with provenance metadata
|
|
111
|
+
* AC03: Path validation errors reported with counters (found vs missing)
|
|
112
|
+
* AC04: Support multiple attachment root directories from config
|
|
113
|
+
*/
|
|
114
|
+
export function validateAndEnforcePaths(messages, config) {
|
|
115
|
+
const result = {
|
|
116
|
+
messages: [],
|
|
117
|
+
stats: {
|
|
118
|
+
total: 0,
|
|
119
|
+
found: 0,
|
|
120
|
+
missing: 0,
|
|
121
|
+
notAbsolute: 0,
|
|
122
|
+
errors: [],
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
for (const message of messages) {
|
|
126
|
+
// Only process media messages
|
|
127
|
+
if (message.messageKind !== 'media' || !message.media) {
|
|
128
|
+
result.messages.push(message);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const updatedMessage = { ...message };
|
|
132
|
+
const media = { ...message.media };
|
|
133
|
+
result.stats.total++;
|
|
134
|
+
let resolvedPath = media.path;
|
|
135
|
+
let pathAbsolute = isAbsolutePath(resolvedPath);
|
|
136
|
+
let pathExists = fileExists(resolvedPath);
|
|
137
|
+
// If path is not absolute, try to resolve it
|
|
138
|
+
if (!pathAbsolute && resolvedPath) {
|
|
139
|
+
// Try expanding tilde
|
|
140
|
+
const expanded = expandTildeInPath(resolvedPath);
|
|
141
|
+
if (fileExists(expanded)) {
|
|
142
|
+
resolvedPath = expanded;
|
|
143
|
+
pathAbsolute = true;
|
|
144
|
+
pathExists = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// If still not absolute, try searching in attachment roots
|
|
148
|
+
if (!pathAbsolute && media.filename) {
|
|
149
|
+
const found = searchAttachmentInRoots(media.filename, config.attachmentRoots);
|
|
150
|
+
if (found) {
|
|
151
|
+
resolvedPath = found;
|
|
152
|
+
pathAbsolute = true;
|
|
153
|
+
pathExists = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Update statistics and path
|
|
157
|
+
if (pathAbsolute && pathExists) {
|
|
158
|
+
result.stats.found++;
|
|
159
|
+
media.path = resolvedPath;
|
|
160
|
+
}
|
|
161
|
+
else if (pathAbsolute && !pathExists) {
|
|
162
|
+
// Path is absolute but file not found
|
|
163
|
+
result.stats.missing++;
|
|
164
|
+
const error = `File not found at path: ${resolvedPath}`;
|
|
165
|
+
result.stats.errors.push({
|
|
166
|
+
guid: message.guid,
|
|
167
|
+
filename: media.filename,
|
|
168
|
+
error,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else if (!pathAbsolute) {
|
|
172
|
+
// Path is not absolute
|
|
173
|
+
result.stats.notAbsolute++;
|
|
174
|
+
const error = `Path is not absolute: ${resolvedPath || '(empty)'}`;
|
|
175
|
+
result.stats.errors.push({
|
|
176
|
+
guid: message.guid,
|
|
177
|
+
filename: media.filename,
|
|
178
|
+
error,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Unknown state
|
|
183
|
+
result.stats.missing++;
|
|
184
|
+
const error = 'Could not resolve path';
|
|
185
|
+
result.stats.errors.push({
|
|
186
|
+
guid: message.guid,
|
|
187
|
+
filename: media.filename,
|
|
188
|
+
error,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// AC02: Add provenance metadata
|
|
192
|
+
const _source = inferSource(message, config.source);
|
|
193
|
+
media.provenance = createProvenance(message, config.source);
|
|
194
|
+
updatedMessage.media = media;
|
|
195
|
+
result.messages.push(updatedMessage);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Format path validation statistics as human-readable string for logging
|
|
201
|
+
*/
|
|
202
|
+
export function formatPathValidationStats(stats) {
|
|
203
|
+
const lines = [
|
|
204
|
+
'📁 Path Validation Report',
|
|
205
|
+
` Total media files: ${stats.total}`,
|
|
206
|
+
` ✓ Found (absolute & exists): ${stats.found}`,
|
|
207
|
+
` ✗ Missing (absolute but no file): ${stats.missing}`,
|
|
208
|
+
` ⚠ Not absolute: ${stats.notAbsolute}`,
|
|
209
|
+
];
|
|
210
|
+
if (stats.errors.length > 0) {
|
|
211
|
+
lines.push(` Errors (${stats.errors.length}):`);
|
|
212
|
+
stats.errors.slice(0, 5).forEach((err) => {
|
|
213
|
+
lines.push(` - ${err.filename} (${err.guid}): ${err.error}`);
|
|
214
|
+
});
|
|
215
|
+
if (stats.errors.length > 5) {
|
|
216
|
+
lines.push(` ... and ${stats.errors.length - 5} more`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=path-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-validator.js","sourceRoot":"","sources":["../../src/normalize/path-validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ZodIssue } from 'zod';
|
|
2
|
+
import type { Message } from '../schema/message';
|
|
3
|
+
export type ValidationError = {
|
|
4
|
+
index: number;
|
|
5
|
+
fieldPath: string;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* AC01: Validate all normalized messages against MessageSchema
|
|
10
|
+
* AC02: Include field paths in error messages
|
|
11
|
+
* AC03: Batch validation - collect all errors before throwing
|
|
12
|
+
* @param messages - Array of messages to validate
|
|
13
|
+
* @returns Array of validated messages if all pass
|
|
14
|
+
* @throws Error with formatted error messages if any validation fails
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateNormalizedMessages(messages: unknown[]): Message[];
|
|
17
|
+
/**
|
|
18
|
+
* AC02: Format Zod validation errors with field paths
|
|
19
|
+
* Converts Zod error array into readable messages with paths
|
|
20
|
+
* @param messageIndex - Index of the message in the array
|
|
21
|
+
* @param zodErrors - Zod validation errors
|
|
22
|
+
* @returns Formatted error message string
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatValidationErrors(_messageIndex: number, zodErrors: ZodIssue[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* AC04: Detect if message has snake_case fields
|
|
27
|
+
* Recursively checks for snake_case field names
|
|
28
|
+
* @param obj - Object to check
|
|
29
|
+
* @returns true if any snake_case fields found
|
|
30
|
+
*/
|
|
31
|
+
export declare function hasSnakeCaseFields(obj: unknown, depth?: number): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Get all snake_case fields in an object (for error reporting)
|
|
34
|
+
* @param obj - Object to check
|
|
35
|
+
* @returns Array of field names that use snake_case
|
|
36
|
+
*/
|
|
37
|
+
export declare function getSnakeCaseFields(obj: unknown, prefix?: string, depth?: number): string[];
|
|
38
|
+
/**
|
|
39
|
+
* AC03: Collect validation errors from multiple messages
|
|
40
|
+
* Helper to aggregate errors with message index
|
|
41
|
+
* @param errorList - List of validation errors
|
|
42
|
+
* @returns Formatted error messages with indices
|
|
43
|
+
*/
|
|
44
|
+
export declare function collectValidationErrors(errorList: ValidationError[]): string[];
|
|
45
|
+
//# sourceMappingURL=validate-normalized.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-normalized.d.ts","sourceRoot":"","sources":["../../src/normalize/validate-normalized.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAA;AACnC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAGhD,MAAM,MAAM,eAAe,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAuDzE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACrC,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,QAAQ,EAAE,GACnB,MAAM,CAeR;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,SAAI,GAAG,OAAO,CAsBnE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CACjC,GAAG,EAAE,OAAO,EACZ,MAAM,SAAK,EACX,KAAK,SAAI,GACP,MAAM,EAAE,CAuBV;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACtC,SAAS,EAAE,eAAe,EAAE,GAC1B,MAAM,EAAE,CAIV"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/normalize/validate-normalized.ts
|
|
2
|
+
// Zod validation layer for normalized messages
|
|
3
|
+
// Spec §4.3, §9: Schema validation with comprehensive error reporting
|
|
4
|
+
import { MessageSchema } from '../schema/message';
|
|
5
|
+
/**
|
|
6
|
+
* AC01: Validate all normalized messages against MessageSchema
|
|
7
|
+
* AC02: Include field paths in error messages
|
|
8
|
+
* AC03: Batch validation - collect all errors before throwing
|
|
9
|
+
* @param messages - Array of messages to validate
|
|
10
|
+
* @returns Array of validated messages if all pass
|
|
11
|
+
* @throws Error with formatted error messages if any validation fails
|
|
12
|
+
*/
|
|
13
|
+
export function validateNormalizedMessages(messages) {
|
|
14
|
+
if (!Array.isArray(messages)) {
|
|
15
|
+
throw new Error('Messages must be an array');
|
|
16
|
+
}
|
|
17
|
+
const validatedMessages = [];
|
|
18
|
+
const errors = [];
|
|
19
|
+
// Process each message and collect errors (AC03: no fail-fast)
|
|
20
|
+
for (let i = 0; i < messages.length; i++) {
|
|
21
|
+
const message = messages[i];
|
|
22
|
+
// AC04: Check for snake_case fields before schema validation
|
|
23
|
+
if (typeof message === 'object' &&
|
|
24
|
+
message !== null &&
|
|
25
|
+
hasSnakeCaseFields(message)) {
|
|
26
|
+
const snakeCaseFields = getSnakeCaseFields(message);
|
|
27
|
+
const fieldList = snakeCaseFields.join(', ');
|
|
28
|
+
errors.push({
|
|
29
|
+
index: i,
|
|
30
|
+
fieldPath: 'root',
|
|
31
|
+
message: `Message contains snake_case fields: ${fieldList}. Use camelCase instead.`,
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Validate against schema
|
|
36
|
+
const result = MessageSchema.safeParse(message);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
// AC02: Format Zod errors with field paths
|
|
39
|
+
const formattedErrors = formatValidationErrors(i, result.error.errors);
|
|
40
|
+
errors.push({
|
|
41
|
+
index: i,
|
|
42
|
+
fieldPath: 'multiple',
|
|
43
|
+
message: formattedErrors,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
validatedMessages.push(result.data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// If any errors occurred, throw with all collected errors (AC03)
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
const errorSummary = errors
|
|
53
|
+
.map((e) => `messages.${e.index}: ${e.message}`)
|
|
54
|
+
.join('\n');
|
|
55
|
+
const error = new Error(`Validation failed:\n${errorSummary}`);
|
|
56
|
+
error.name = 'ValidationError';
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return validatedMessages;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* AC02: Format Zod validation errors with field paths
|
|
63
|
+
* Converts Zod error array into readable messages with paths
|
|
64
|
+
* @param messageIndex - Index of the message in the array
|
|
65
|
+
* @param zodErrors - Zod validation errors
|
|
66
|
+
* @returns Formatted error message string
|
|
67
|
+
*/
|
|
68
|
+
export function formatValidationErrors(_messageIndex, zodErrors) {
|
|
69
|
+
if (!Array.isArray(zodErrors) || zodErrors.length === 0) {
|
|
70
|
+
return 'Unknown validation error';
|
|
71
|
+
}
|
|
72
|
+
const formatted = zodErrors
|
|
73
|
+
.map((error) => {
|
|
74
|
+
// Build path: ["media", "enrichment", 0, "createdAt"] -> "media.enrichment.0.createdAt"
|
|
75
|
+
const path = (error.path || []).join('.');
|
|
76
|
+
const pathPrefix = path ? `${path}: ` : '';
|
|
77
|
+
return `${pathPrefix}${error.message}`;
|
|
78
|
+
})
|
|
79
|
+
.join('; ');
|
|
80
|
+
return formatted;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* AC04: Detect if message has snake_case fields
|
|
84
|
+
* Recursively checks for snake_case field names
|
|
85
|
+
* @param obj - Object to check
|
|
86
|
+
* @returns true if any snake_case fields found
|
|
87
|
+
*/
|
|
88
|
+
export function hasSnakeCaseFields(obj, depth = 0) {
|
|
89
|
+
if (depth > 10)
|
|
90
|
+
return false; // Prevent infinite recursion
|
|
91
|
+
if (typeof obj !== 'object' || obj === null)
|
|
92
|
+
return false;
|
|
93
|
+
const record = obj;
|
|
94
|
+
for (const key of Object.keys(record)) {
|
|
95
|
+
// Check if key contains underscore (snake_case indicator)
|
|
96
|
+
if (key.includes('_') && !key.startsWith('__')) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
// Recursively check nested objects
|
|
100
|
+
const value = record[key];
|
|
101
|
+
if (typeof value === 'object' && value !== null) {
|
|
102
|
+
if (hasSnakeCaseFields(value, depth + 1)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get all snake_case fields in an object (for error reporting)
|
|
111
|
+
* @param obj - Object to check
|
|
112
|
+
* @returns Array of field names that use snake_case
|
|
113
|
+
*/
|
|
114
|
+
export function getSnakeCaseFields(obj, prefix = '', depth = 0) {
|
|
115
|
+
if (depth > 10)
|
|
116
|
+
return []; // Prevent infinite recursion
|
|
117
|
+
if (typeof obj !== 'object' || obj === null)
|
|
118
|
+
return [];
|
|
119
|
+
const snakeCaseFields = [];
|
|
120
|
+
const record = obj;
|
|
121
|
+
for (const key of Object.keys(record)) {
|
|
122
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
123
|
+
// Check if key contains underscore (snake_case indicator)
|
|
124
|
+
if (key.includes('_') && !key.startsWith('__')) {
|
|
125
|
+
snakeCaseFields.push(fullPath);
|
|
126
|
+
}
|
|
127
|
+
// Recursively check nested objects
|
|
128
|
+
const value = record[key];
|
|
129
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
130
|
+
snakeCaseFields.push(...getSnakeCaseFields(value, fullPath, depth + 1));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return snakeCaseFields;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* AC03: Collect validation errors from multiple messages
|
|
137
|
+
* Helper to aggregate errors with message index
|
|
138
|
+
* @param errorList - List of validation errors
|
|
139
|
+
* @returns Formatted error messages with indices
|
|
140
|
+
*/
|
|
141
|
+
export function collectValidationErrors(errorList) {
|
|
142
|
+
return errorList.map((e) => `messages.${e.index}.${e.fieldPath}: ${e.message}`);
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=validate-normalized.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-normalized.js","sourceRoot":"","sources":["../../src/normalize/validate-normalized.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeds and Blockquotes Rendering Module (RENDER--T03)
|
|
3
|
+
*
|
|
4
|
+
* Implements Obsidian-friendly embeds and blockquote rendering:
|
|
5
|
+
* - AC01: Image embeds with ![[path]] syntax
|
|
6
|
+
* - AC02: Preview images with links to originals (HEIC/TIFF)
|
|
7
|
+
* - AC03: Transcription blockquotes with speaker labels
|
|
8
|
+
* - AC04: Link context blockquotes with metadata
|
|
9
|
+
* - AC05: PDF summary blockquotes with formatting
|
|
10
|
+
*/
|
|
11
|
+
import type { MediaEnrichment, Message } from '#schema/message';
|
|
12
|
+
/**
|
|
13
|
+
* AC01: Render image embed with Obsidian wikilink syntax
|
|
14
|
+
* Format: ![[path]]
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderImageEmbed(path: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* AC02: Render preview image with link to original
|
|
19
|
+
* For HEIC/TIFF files, shows preview with link to original
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderPreviewImageWithLink(previewPath: string, originalPath: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* AC03: Render transcription as blockquote
|
|
24
|
+
* Handles multiline transcriptions with speaker labels and timestamps
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderTranscriptionBlockquote(enrichment: MediaEnrichment): string;
|
|
27
|
+
/**
|
|
28
|
+
* AC04: Render link context as blockquote
|
|
29
|
+
* Shows title as markdown link and summary as blockquote
|
|
30
|
+
*/
|
|
31
|
+
export declare function renderLinkContextBlockquote(enrichment: MediaEnrichment): string;
|
|
32
|
+
/**
|
|
33
|
+
* AC05: Render PDF summary as blockquote
|
|
34
|
+
* Preserves paragraph structure with blockquote formatting
|
|
35
|
+
*/
|
|
36
|
+
export declare function renderPdfSummaryBlockquote(enrichment: MediaEnrichment): string;
|
|
37
|
+
/**
|
|
38
|
+
* Determine if a message should render an embed
|
|
39
|
+
* Only renders embeds for image media
|
|
40
|
+
*/
|
|
41
|
+
export declare function shouldRenderEmbed(message: Message): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Get the path to embed for a media message
|
|
44
|
+
* Returns undefined if message cannot be embedded
|
|
45
|
+
*/
|
|
46
|
+
export declare function getEmbedPath(message: Message): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Get preview path for an image file
|
|
49
|
+
* For HEIC/TIFF, returns the .jpg preview path
|
|
50
|
+
* For JPG/PNG, returns the original path
|
|
51
|
+
*/
|
|
52
|
+
export declare function getPreviewPath(originalPath: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Get the original path from a preview path
|
|
55
|
+
* Handles HEIC.jpg -> HEIC and TIFF.jpg -> TIFF conversions
|
|
56
|
+
*/
|
|
57
|
+
export declare function getOriginalPath(previewPath: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Extract enrichments of a specific kind from a media message
|
|
60
|
+
*/
|
|
61
|
+
export declare function getEnrichmentsByKind(message: Message, kind: MediaEnrichment['kind']): MediaEnrichment[];
|
|
62
|
+
/**
|
|
63
|
+
* Get all transcription enrichments from a message
|
|
64
|
+
*/
|
|
65
|
+
export declare function getTranscriptions(message: Message): MediaEnrichment[];
|
|
66
|
+
/**
|
|
67
|
+
* Get all link context enrichments from a message
|
|
68
|
+
*/
|
|
69
|
+
export declare function getLinkContexts(message: Message): MediaEnrichment[];
|
|
70
|
+
/**
|
|
71
|
+
* Get all PDF summary enrichments from a message
|
|
72
|
+
*/
|
|
73
|
+
export declare function getPdfSummaries(message: Message): MediaEnrichment[];
|
|
74
|
+
/**
|
|
75
|
+
* Render all blockquotes for a message's enrichments
|
|
76
|
+
*/
|
|
77
|
+
export type RenderedEnrichments = {
|
|
78
|
+
embeds: string[];
|
|
79
|
+
transcriptions: string[];
|
|
80
|
+
linkContexts: string[];
|
|
81
|
+
pdfSummaries: string[];
|
|
82
|
+
};
|
|
83
|
+
export declare function renderAllEnrichments(message: Message): RenderedEnrichments;
|
|
84
|
+
//# sourceMappingURL=embeds-blockquotes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embeds-blockquotes.d.ts","sourceRoot":"","sources":["../../src/render/embeds-blockquotes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAE/D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACzC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GAClB,MAAM,CAGR;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC5C,UAAU,EAAE,eAAe,GACzB,MAAM,CAOR;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAC1C,UAAU,EAAE,eAAe,GACzB,MAAM,CA2BR;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACzC,UAAU,EAAE,eAAe,GACzB,MAAM,CAOR;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAO3D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAa3D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAkB3D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,GAC3B,eAAe,EAAE,CAMnB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,EAAE,CAErE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,EAAE,CAEnE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,EAAE,CAEnE;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IACjC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,YAAY,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,mBAAmB,CAwC1E"}
|