@hraness/sponge 0.2.6 → 0.3.0
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 +21 -9
- package/bin/sponge.js +8 -7
- package/dist/contracts/index.js +24 -6
- package/dist/contracts/validate.js +106 -29
- package/dist/migrations/008.d.ts +3 -0
- package/dist/migrations/008.js +5 -0
- package/dist/migrations/009.d.ts +3 -0
- package/dist/migrations/009.js +8 -0
- package/dist/migrations/010.d.ts +3 -0
- package/dist/migrations/010.js +11 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +39 -0
- package/dist/src/acquisition/capture.js +87 -28
- package/dist/src/acquisition/cleanup.d.ts +0 -4
- package/dist/src/acquisition/cleanup.js +3 -5
- package/dist/src/acquisition/errors.js +17 -2
- package/dist/src/acquisition/files.d.ts +0 -1
- package/dist/src/acquisition/files.js +14 -11
- package/dist/src/acquisition/index.d.ts +2 -3
- package/dist/src/acquisition/index.js +1 -2
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/normalize.js +57 -18
- package/dist/src/acquisition/pins.js +22 -4
- package/dist/src/acquisition/process.js +67 -29
- package/dist/src/acquisition/runtime.d.ts +5 -7
- package/dist/src/acquisition/runtime.js +129 -68
- package/dist/src/acquisition/verify-helper.js +10 -3
- package/dist/src/annotations/quote.js +40 -12
- package/dist/src/annotations/store.d.ts +0 -1
- package/dist/src/annotations/store.js +66 -14
- package/dist/src/app/article.d.ts +2 -1
- package/dist/src/app/article.js +66 -70
- package/dist/src/app/bundle-files.d.ts +0 -4
- package/dist/src/app/bundle-files.js +11 -14
- package/dist/src/app/markdown.js +18 -6
- package/dist/src/app/source.d.ts +0 -4
- package/dist/src/app/source.js +18 -14
- package/dist/src/cli/arguments.d.ts +2 -8
- package/dist/src/cli/arguments.js +67 -80
- package/dist/src/cli/commands.d.ts +42 -0
- package/dist/src/cli/commands.js +795 -0
- package/dist/src/cli/config.js +12 -9
- package/dist/src/cli/digest.d.ts +1 -1
- package/dist/src/cli/digest.js +38 -16
- package/dist/src/cli/import.js +25 -4
- package/dist/src/cli/indexing.d.ts +2 -0
- package/dist/src/cli/indexing.js +26 -9
- package/dist/src/cli/main.js +47 -386
- package/dist/src/cli/portability.d.ts +0 -2
- package/dist/src/cli/portability.js +13 -8
- package/dist/src/cli/queue.js +12 -8
- package/dist/src/cli/runtimes.d.ts +1 -3
- package/dist/src/cli/runtimes.js +13 -6
- package/dist/src/cli/terminal.js +1 -1
- package/dist/src/cli/worker.d.ts +7 -0
- package/dist/src/cli/worker.js +54 -15
- package/dist/src/derivations/kinds.d.ts +34 -0
- package/dist/src/derivations/kinds.js +14 -0
- package/dist/src/derivations/types.d.ts +48 -0
- package/dist/src/derivations/types.js +1 -0
- package/dist/src/digest/derivation.d.ts +25 -0
- package/dist/src/digest/derivation.js +124 -0
- package/dist/src/digest/editorial.d.ts +9 -46
- package/dist/src/digest/editorial.js +57 -49
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +49 -28
- package/dist/src/digest/pipeline.js +343 -100
- package/dist/src/digest/schema.d.ts +11 -25
- package/dist/src/digest/schema.js +86 -18
- package/dist/src/errors.d.ts +46 -0
- package/dist/src/errors.js +117 -0
- package/dist/src/identity/query-policy.d.ts +0 -5
- package/dist/src/identity/query-policy.js +61 -9
- package/dist/src/identity/uri.d.ts +6 -5
- package/dist/src/identity/uri.js +37 -18
- package/dist/src/identity/visibility.d.ts +11 -1
- package/dist/src/identity/visibility.js +81 -17
- package/dist/src/importers/apply.js +104 -36
- package/dist/src/importers/custody.js +27 -9
- package/dist/src/importers/inspect.js +125 -38
- package/dist/src/importers/jungle-digest.d.ts +3 -6
- package/dist/src/importers/jungle-digest.js +196 -69
- package/dist/src/importers/jungle-store.d.ts +15 -6
- package/dist/src/importers/jungle-store.js +122 -28
- package/dist/src/importers/jungle.d.ts +14 -14
- package/dist/src/importers/jungle.js +136 -31
- package/dist/src/importers/manifest.js +57 -21
- package/dist/src/importers/verify-helper.js +18 -4
- package/dist/src/indexing/image.js +29 -10
- package/dist/src/indexing/index.d.ts +37 -6
- package/dist/src/indexing/index.js +360 -124
- package/dist/src/indexing/jobs.js +156 -47
- package/dist/src/indexing/prune-run.d.ts +2 -0
- package/dist/src/indexing/prune-run.js +69 -26
- package/dist/src/indexing/prune.d.ts +12 -3
- package/dist/src/indexing/prune.js +85 -35
- package/dist/src/indexing/runner.d.ts +2 -1
- package/dist/src/indexing/runner.js +83 -40
- package/dist/src/indexing/upgrade.d.ts +9 -0
- package/dist/src/indexing/upgrade.js +79 -0
- package/dist/src/inference/asr/bridge.js +6 -2
- package/dist/src/inference/asr/protocol.js +109 -19
- package/dist/src/inference/asr/runtime.js +58 -21
- package/dist/src/inference/asr/windows.js +9 -5
- package/dist/src/inference/embedding/index.d.ts +2 -6
- package/dist/src/inference/embedding/index.js +89 -29
- package/dist/src/ingest/attempt.d.ts +29 -0
- package/dist/src/ingest/attempt.js +55 -0
- package/dist/src/media/capture-helper.js +7 -2
- package/dist/src/media/capture.d.ts +2 -1
- package/dist/src/media/capture.js +124 -43
- package/dist/src/media/handler.d.ts +4 -2
- package/dist/src/media/handler.js +88 -70
- package/dist/src/media/transcribe.js +63 -21
- package/dist/src/metadata/article.js +49 -17
- package/dist/src/models/artifacts.d.ts +10 -8
- package/dist/src/models/artifacts.js +237 -106
- package/dist/src/models/catalog.d.ts +2 -5
- package/dist/src/models/catalog.js +94 -25
- package/dist/src/models/download.js +55 -15
- package/dist/src/models/runtime.d.ts +0 -2
- package/dist/src/models/runtime.js +69 -33
- package/dist/src/pdf/capture-helper.js +3 -1
- package/dist/src/pdf/capture.d.ts +2 -1
- package/dist/src/pdf/capture.js +95 -27
- package/dist/src/pdf/errors.js +12 -1
- package/dist/src/pdf/handler.d.ts +2 -1
- package/dist/src/pdf/handler.js +70 -71
- package/dist/src/pdf/index.d.ts +1 -3
- package/dist/src/pdf/index.js +0 -2
- package/dist/src/pdf/input.js +12 -7
- package/dist/src/pdf/normalize.js +80 -21
- package/dist/src/pdf/source-identity.js +3 -2
- package/dist/src/pdf/source.js +12 -8
- package/dist/src/pdf/verify-helper.js +15 -3
- package/dist/src/portability/backup.d.ts +2 -9
- package/dist/src/portability/backup.js +106 -22
- package/dist/src/portability/database.js +51 -17
- package/dist/src/portability/export.js +35 -9
- package/dist/src/portability/files.d.ts +1 -4
- package/dist/src/portability/files.js +127 -67
- package/dist/src/portability/gc.js +65 -22
- package/dist/src/publish/component.js +1 -1
- package/dist/src/publish/feed.d.ts +2 -2
- package/dist/src/publish/feed.js +75 -29
- package/dist/src/publish/identity.d.ts +5 -1
- package/dist/src/publish/identity.js +31 -11
- package/dist/src/publish/index.d.ts +2 -3
- package/dist/src/publish/index.js +34 -21
- package/dist/src/publish/projection.d.ts +2 -9
- package/dist/src/publish/projection.js +21 -14
- package/dist/src/search/extension.js +3 -1
- package/dist/src/search/hybrid.d.ts +3 -8
- package/dist/src/search/hybrid.js +45 -11
- package/dist/src/search/lexical.js +18 -4
- package/dist/src/search/query.js +56 -25
- package/dist/src/search/vector.js +37 -10
- package/dist/src/service/launch-agent.d.ts +5 -3
- package/dist/src/service/launch-agent.js +272 -83
- package/dist/src/service/lifetime.js +47 -14
- package/dist/src/service/loop.d.ts +3 -3
- package/dist/src/service/loop.js +47 -19
- package/dist/src/store/annotations.d.ts +32 -0
- package/dist/src/store/annotations.js +44 -0
- package/dist/src/store/backups.d.ts +18 -0
- package/dist/src/store/backups.js +110 -0
- package/dist/src/store/blobs.d.ts +24 -0
- package/dist/src/store/blobs.js +139 -0
- package/dist/src/store/changes.d.ts +5 -12
- package/dist/src/store/changes.js +108 -56
- package/dist/src/store/context.d.ts +15 -0
- package/dist/src/store/context.js +19 -0
- package/dist/src/store/derivations.d.ts +42 -0
- package/dist/src/store/derivations.js +104 -0
- package/dist/src/store/files.d.ts +0 -2
- package/dist/src/store/files.js +19 -24
- package/dist/src/store/ids.d.ts +30 -0
- package/dist/src/store/ids.js +34 -0
- package/dist/src/store/imports.d.ts +26 -0
- package/dist/src/store/imports.js +57 -0
- package/dist/src/store/index.d.ts +3 -186
- package/dist/src/store/index.js +2 -809
- package/dist/src/store/lease.d.ts +20 -0
- package/dist/src/store/lease.js +62 -0
- package/dist/src/store/library.d.ts +75 -0
- package/dist/src/store/library.js +174 -0
- package/dist/src/store/open.d.ts +7 -0
- package/dist/src/store/open.js +183 -0
- package/dist/src/store/queue.d.ts +65 -0
- package/dist/src/store/queue.js +327 -0
- package/dist/src/store/revisions.d.ts +12 -0
- package/dist/src/store/revisions.js +189 -0
- package/dist/src/store/search-index.d.ts +63 -0
- package/dist/src/store/search-index.js +190 -0
- package/dist/src/store/sync-ready.d.ts +6 -0
- package/dist/src/store/sync-ready.js +121 -0
- package/dist/src/store/types.d.ts +1 -5
- package/dist/src/store/types.js +1 -8
- package/dist/src/util/canonical.d.ts +2 -0
- package/dist/src/util/canonical.js +17 -0
- package/dist/src/util/exact.d.ts +2 -0
- package/dist/src/util/exact.js +9 -0
- package/dist/src/util/fsync.d.ts +4 -0
- package/dist/src/util/fsync.js +22 -0
- package/dist/src/util/hash.d.ts +4 -0
- package/dist/src/util/hash.js +8 -0
- package/dist/src/worker/basic.d.ts +0 -1
- package/dist/src/worker/basic.js +23 -7
- package/dist/src/worker/custody.d.ts +4 -1
- package/dist/src/worker/custody.js +111 -38
- package/dist/src/worker/engine.d.ts +2 -1
- package/dist/src/worker/engine.js +42 -20
- package/dist/src/worker/identity.js +13 -5
- package/dist/src/worker/journal.d.ts +1 -1
- package/dist/src/worker/journal.js +54 -24
- package/dist/src/worker/lock.d.ts +18 -0
- package/dist/src/worker/lock.js +196 -0
- package/dist/src/worker/runtime-gc.d.ts +4 -3
- package/dist/src/worker/runtime-gc.js +68 -41
- package/dist/src/worker/supervisor-child.js +19 -6
- package/dist/src/worker/supervisor.js +41 -8
- package/native/runtime-manifest.json +4 -4
- package/package.json +10 -2
- package/dist/src/digest/store.d.ts +0 -57
- package/dist/src/digest/store.js +0 -79
- package/dist/src/store/foundation.d.ts +0 -7
- package/dist/src/store/foundation.js +0 -7
|
@@ -6,28 +6,37 @@ import { type JungleEditorial } from './jungle-digest.js';
|
|
|
6
6
|
* The library side of `sponge import jungle`: one Jungle Reading note onto the item holding its capture.
|
|
7
7
|
* `planJungleNote` only reads; `applyJungleNote` runs the same plan inside the caller's transaction, through the logged paths.
|
|
8
8
|
*/
|
|
9
|
-
export declare const jungleNamespace = "jungle", jungleRealm = "import:jungle"
|
|
10
|
-
readonly importer: "sponge-jungle-reading";
|
|
11
|
-
readonly version: "2";
|
|
12
|
-
};
|
|
9
|
+
export declare const jungleNamespace = "jungle", jungleRealm = "import:jungle";
|
|
13
10
|
export type EditorialAction = 'new' | 'unchanged' | 'user-review-wins';
|
|
14
11
|
export type DigestAction = 'new' | 'unchanged' | 'not-convertible' | 'not-digest' | 'user-review-wins' | 'user-digest-wins';
|
|
15
12
|
export type VisibilityAction = 'set-public' | 'set-private' | 'restore-default' | 'unchanged' | 'kept-user-edit' | 'guarded' | 'default-policy';
|
|
13
|
+
/** Local captures only (no web URI of their own): `uri` records the note's `source_url`. Later notes on one capture leave it to the first (`other-note`). */
|
|
14
|
+
export type UriAction = 'new' | 'unchanged' | 'web-capture' | 'other-note' | 'no-source-url' | 'capability';
|
|
15
|
+
/** The first note's title is the item's default title (`noteTitle`); `kept-user-edit` records it while the user's override stays shown. */
|
|
16
|
+
export type TitleAction = 'set' | 'unchanged' | 'other-note' | 'kept-user-edit';
|
|
17
|
+
/** `firstNote` is false for a later note on a capture an earlier note in the same run already used; `firstNoteUri` is the URI that note records (a dry run has not written it). */
|
|
16
18
|
export interface JungleNoteInput {
|
|
17
19
|
itemId: string;
|
|
18
20
|
revisionId: string;
|
|
19
21
|
editorial: JungleEditorial;
|
|
20
22
|
digest?: DigestV2;
|
|
23
|
+
firstNote?: boolean;
|
|
24
|
+
firstNoteUri?: string;
|
|
21
25
|
}
|
|
22
26
|
export interface JunglePlan {
|
|
23
27
|
editorialId: string;
|
|
24
28
|
editorial: EditorialAction;
|
|
25
29
|
digestId?: string;
|
|
26
30
|
digest: DigestAction;
|
|
31
|
+
uri: UriAction;
|
|
32
|
+
title: TitleAction;
|
|
27
33
|
visibility: VisibilityAction;
|
|
28
34
|
}
|
|
29
|
-
/** What
|
|
30
|
-
export declare function
|
|
35
|
+
/** What a local capture's URI action would be, before or after its item exists. */
|
|
36
|
+
export declare function uriAction(local: boolean, firstNote: boolean, sourceUrl: string, recorded?: string): {
|
|
37
|
+
action: UriAction;
|
|
38
|
+
uri?: string;
|
|
39
|
+
};
|
|
31
40
|
/**
|
|
32
41
|
* Jungle's decision becomes an explicit override only while nobody else has set one: once the latest override was not
|
|
33
42
|
* written by this importer, the user's choice wins. A public decision never lifts the capability-URL or local-file guards.
|
|
@@ -1,18 +1,62 @@
|
|
|
1
|
-
import { canonicalDigest
|
|
2
|
-
import { canonicalEditorial
|
|
1
|
+
import { canonicalDigest } from '../digest/derivation.js';
|
|
2
|
+
import { canonicalEditorial } from '../digest/editorial.js';
|
|
3
|
+
import { derivationId, putDerivation, reviewDerivation } from '../store/derivations.js';
|
|
3
4
|
import { parseDigest } from '../digest/schema.js';
|
|
4
|
-
import { visibility as visibilityState, setVisibility } from '../identity/visibility.js';
|
|
5
|
-
import {
|
|
5
|
+
import { visibility as visibilityState, setVisibility, hasWebUri, metadataUri, setMetadataUri, } from '../identity/visibility.js';
|
|
6
|
+
import { isCapabilityUrl } from '../identity/query-policy.js';
|
|
7
|
+
import { isResolvable, maxUriBytes, shareableWebUri } from '../identity/uri.js';
|
|
8
|
+
import { mutate } from '../store/changes.js';
|
|
9
|
+
import { sha256 } from '../util/hash.js';
|
|
6
10
|
import { jungleTags } from './jungle-digest.js';
|
|
7
11
|
/**
|
|
8
12
|
* The library side of `sponge import jungle`: one Jungle Reading note onto the item holding its capture.
|
|
9
13
|
* `planJungleNote` only reads; `applyJungleNote` runs the same plan inside the caller's transaction, through the logged paths.
|
|
10
14
|
*/
|
|
11
|
-
export const jungleNamespace = 'jungle', jungleRealm = 'import:jungle'
|
|
15
|
+
export const jungleNamespace = 'jungle', jungleRealm = 'import:jungle';
|
|
16
|
+
const jungleProducer = { importer: 'sponge-jungle-reading', version: '2' };
|
|
12
17
|
/** Receipts of the importer's own visibility writes (local, like every import receipt), so a later user override is recognisable. */
|
|
13
18
|
const visibilityReceipts = 'jungle-visibility';
|
|
19
|
+
/**
|
|
20
|
+
* The note's `reading.source_url` as a shareable URI: http(s) only, through the same derivation as a captured locator.
|
|
21
|
+
* A capability URL (query-policy.ts) is refused outright, so its stripped form never becomes a public address either.
|
|
22
|
+
*/
|
|
23
|
+
function noteUri(sourceUrl) {
|
|
24
|
+
if (!/^https?:\/\//i.test(sourceUrl))
|
|
25
|
+
return { refused: 'no-source-url' };
|
|
26
|
+
if (isCapabilityUrl(sourceUrl))
|
|
27
|
+
return { refused: 'capability' };
|
|
28
|
+
let uri;
|
|
29
|
+
try {
|
|
30
|
+
uri = shareableWebUri(sourceUrl);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { refused: 'no-source-url' };
|
|
34
|
+
}
|
|
35
|
+
return Buffer.byteLength(uri) <= maxUriBytes && isResolvable(uri) && !isCapabilityUrl(uri)
|
|
36
|
+
? { uri }
|
|
37
|
+
: { refused: 'no-source-url' };
|
|
38
|
+
}
|
|
39
|
+
/** What a local capture's URI action would be, before or after its item exists. */
|
|
40
|
+
export function uriAction(local, firstNote, sourceUrl, recorded) {
|
|
41
|
+
if (!firstNote)
|
|
42
|
+
return { action: 'other-note' };
|
|
43
|
+
if (!local)
|
|
44
|
+
return { action: 'web-capture' };
|
|
45
|
+
const { uri, refused } = noteUri(sourceUrl);
|
|
46
|
+
return uri ? { action: uri === recorded ? 'unchanged' : 'new', uri } : { action: refused };
|
|
47
|
+
}
|
|
48
|
+
function titleAction(db, input) {
|
|
49
|
+
if (input.firstNote === false)
|
|
50
|
+
return 'other-note';
|
|
51
|
+
const title = input.editorial.title.trim(), item = db.prepare('SELECT title_note,title_override FROM items WHERE id=?').get(input.itemId);
|
|
52
|
+
if (item?.title_override !== null && item?.title_override !== undefined)
|
|
53
|
+
return 'kept-user-edit';
|
|
54
|
+
return !title || Buffer.byteLength(title) > 4096 || title === item?.title_note ? 'unchanged' : 'set';
|
|
55
|
+
}
|
|
14
56
|
/** What Jungle's `reading.status` asks for: `published` is Jungle's explicit public approval, `draft` stays private. */
|
|
15
|
-
|
|
57
|
+
function desiredVisibility(status) {
|
|
58
|
+
return status === 'published' ? 'public' : status === 'draft' ? 'private' : undefined;
|
|
59
|
+
}
|
|
16
60
|
/**
|
|
17
61
|
* Jungle's decision becomes an explicit override only while nobody else has set one: once the latest override was not
|
|
18
62
|
* written by this importer, the user's choice wins. A public decision never lifts the capability-URL or local-file guards.
|
|
@@ -30,45 +74,73 @@ export function visibilityAction(state, status, userOverride) {
|
|
|
30
74
|
return { action: 'unchanged' };
|
|
31
75
|
return { action: target === null ? 'restore-default' : target === 'public' ? 'set-public' : 'set-private', target };
|
|
32
76
|
}
|
|
33
|
-
const allAccepted = (o) => ({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
77
|
+
const allAccepted = (o) => ({
|
|
78
|
+
gist: 'accepted',
|
|
79
|
+
ideas: o.ideas.map(() => 'accepted'),
|
|
80
|
+
quotes: o.ideas.map((i) => i.quotes.map(() => 'accepted')),
|
|
81
|
+
});
|
|
82
|
+
const imported = (r) => {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(String(r.producer_json))?.importer === jungleProducer.importer;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const latestVisibility = (db, itemId) => db
|
|
91
|
+
.prepare("SELECT id FROM changes WHERE entity='item' AND entity_id=? AND field='visibility' ORDER BY seq DESC LIMIT 1")
|
|
92
|
+
.get(itemId);
|
|
41
93
|
/** The latest logged visibility write was not this importer's: the user chose, even when the choice was `default`. */
|
|
42
94
|
function userOverride(db, itemId) {
|
|
43
95
|
const latest = latestVisibility(db, itemId);
|
|
44
96
|
if (!latest)
|
|
45
97
|
return (db.prepare('SELECT visibility FROM items WHERE id=?').get(itemId)?.visibility ?? null) !== null;
|
|
46
|
-
return !db
|
|
98
|
+
return !db
|
|
99
|
+
.prepare('SELECT 1 FROM import_receipts WHERE namespace=? AND source_key=? AND source_hash=?')
|
|
100
|
+
.get(visibilityReceipts, itemId, sha256(String(latest.id)));
|
|
47
101
|
}
|
|
48
102
|
export function planJungleNote(db, input) {
|
|
49
103
|
const { itemId, revisionId, editorial } = input, editorialId = derivationId(revisionId, 'editorial', editorial.markdownSha256, canonicalEditorial(editorial));
|
|
50
|
-
const editorials = db
|
|
51
|
-
|
|
104
|
+
const editorials = db
|
|
105
|
+
.prepare("SELECT id,review_json FROM derivations WHERE item_id=? AND kind='editorial'")
|
|
106
|
+
.all(itemId);
|
|
107
|
+
const editorialAction = editorials.some((r) => r.id === editorialId)
|
|
108
|
+
? 'unchanged'
|
|
109
|
+
: editorials.some((r) => JSON.parse(String(r.review_json)).note !== 'accepted')
|
|
110
|
+
? 'user-review-wins'
|
|
111
|
+
: 'new';
|
|
52
112
|
// Any earlier digest of the item, v1 included, takes part: an unchanged imported one is superseded by the v2 import, a user's is never.
|
|
53
113
|
let action = 'not-digest', digestId;
|
|
54
114
|
if (editorial.form === 'digest' && !input.digest)
|
|
55
115
|
action = 'not-convertible';
|
|
56
116
|
else if (input.digest) {
|
|
57
|
-
const digests = db
|
|
117
|
+
const digests = db
|
|
118
|
+
.prepare("SELECT id,producer_json,output_json,review_json FROM derivations WHERE item_id=? AND kind='digest'")
|
|
119
|
+
.all(itemId), parsed = parseDigest(input.digest).digest;
|
|
58
120
|
digestId = parsed && derivationId(revisionId, 'digest', editorialId, canonicalDigest(parsed));
|
|
59
121
|
if (!parsed || parsed.schemaVersion !== 2)
|
|
60
122
|
action = 'not-convertible';
|
|
61
|
-
else if (digests.some(r => r.id === digestId))
|
|
123
|
+
else if (digests.some((r) => r.id === digestId))
|
|
62
124
|
action = 'unchanged';
|
|
63
|
-
else if (digests.some(r => !imported(r)))
|
|
125
|
+
else if (digests.some((r) => !imported(r)))
|
|
64
126
|
action = 'user-digest-wins';
|
|
65
|
-
else if (digests.some(r => JSON.stringify(JSON.parse(String(r.review_json))) !==
|
|
127
|
+
else if (digests.some((r) => JSON.stringify(JSON.parse(String(r.review_json))) !==
|
|
128
|
+
JSON.stringify(allAccepted(JSON.parse(String(r.output_json))))))
|
|
66
129
|
action = 'user-review-wins';
|
|
67
130
|
else
|
|
68
131
|
action = editorialAction === 'user-review-wins' ? 'user-review-wins' : 'new';
|
|
69
132
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
133
|
+
const local = !hasWebUri(db, itemId), uri = uriAction(local, input.firstNote !== false, editorial.sourceUrl, metadataUri(db, itemId));
|
|
134
|
+
const visibility = visibilityAction(visibilityState(db, itemId, uri.uri ?? (input.firstNote === false ? input.firstNoteUri : undefined) ?? metadataUri(db, itemId)), editorial.status, userOverride(db, itemId)).action;
|
|
135
|
+
return {
|
|
136
|
+
editorialId,
|
|
137
|
+
editorial: editorialAction,
|
|
138
|
+
...(digestId ? { digestId } : {}),
|
|
139
|
+
digest: action,
|
|
140
|
+
uri: uri.action,
|
|
141
|
+
title: titleAction(db, input),
|
|
142
|
+
visibility,
|
|
143
|
+
};
|
|
72
144
|
}
|
|
73
145
|
/**
|
|
74
146
|
* Apply one note; the caller holds the write transaction. A new editorial object is accepted through `review.note` unless
|
|
@@ -78,19 +150,41 @@ export function planJungleNote(db, input) {
|
|
|
78
150
|
export function applyJungleNote(db, input) {
|
|
79
151
|
const plan = planJungleNote(db, input), producer = { ...jungleProducer, note: input.editorial.slug };
|
|
80
152
|
if (plan.editorial !== 'unchanged') {
|
|
81
|
-
|
|
153
|
+
putDerivation(db, 'editorial', {
|
|
154
|
+
itemId: input.itemId,
|
|
155
|
+
revisionId: input.revisionId,
|
|
156
|
+
producer,
|
|
157
|
+
output: input.editorial,
|
|
158
|
+
});
|
|
82
159
|
if (plan.editorial === 'new')
|
|
83
|
-
|
|
160
|
+
reviewDerivation(db, 'editorial', plan.editorialId, 'accepted');
|
|
84
161
|
}
|
|
162
|
+
// A local capture takes the note's web address and title before visibility is decided, so the published state applies to it.
|
|
163
|
+
if (plan.uri === 'new')
|
|
164
|
+
setMetadataUri(db, input.itemId, uriAction(true, true, input.editorial.sourceUrl).uri);
|
|
165
|
+
// The note's title is the item's default; a refresh changes only the captured title, and the user's override wins over both.
|
|
166
|
+
const title = input.editorial.title.trim();
|
|
167
|
+
if (input.firstNote !== false &&
|
|
168
|
+
title &&
|
|
169
|
+
Buffer.byteLength(title) <= 4096 &&
|
|
170
|
+
title !== db.prepare('SELECT title_note FROM items WHERE id=?').get(input.itemId)?.title_note)
|
|
171
|
+
mutate(db, 'item', input.itemId, 'noteTitle', title);
|
|
85
172
|
if (plan.digest === 'new') {
|
|
86
|
-
const stored =
|
|
87
|
-
|
|
173
|
+
const stored = putDerivation(db, 'digest', {
|
|
174
|
+
itemId: input.itemId,
|
|
175
|
+
revisionId: input.revisionId,
|
|
176
|
+
producer: { ...producer, editorial: plan.editorialId },
|
|
177
|
+
inputHash: plan.editorialId,
|
|
178
|
+
output: input.digest,
|
|
179
|
+
options: { tags: jungleTags },
|
|
180
|
+
});
|
|
181
|
+
reviewDerivation(db, 'digest', stored.id, allAccepted(input.digest));
|
|
88
182
|
}
|
|
89
183
|
const action = visibilityAction(visibilityState(db, input.itemId), input.editorial.status, userOverride(db, input.itemId));
|
|
90
184
|
if (action.target !== undefined) {
|
|
91
185
|
setVisibility(db, input.itemId, action.target);
|
|
92
186
|
const latest = latestVisibility(db, input.itemId);
|
|
93
|
-
db.prepare('INSERT OR IGNORE INTO import_receipts VALUES(?,?,?,?,?,?)').run(visibilityReceipts, input.itemId,
|
|
187
|
+
db.prepare('INSERT OR IGNORE INTO import_receipts VALUES(?,?,?,?,?,?)').run(visibilityReceipts, input.itemId, sha256(String(latest.id)), jungleProducer.version, input.itemId, 'visibility');
|
|
94
188
|
}
|
|
95
189
|
return plan;
|
|
96
190
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { LibraryStore } from '../store/index.js';
|
|
2
|
-
import { type
|
|
3
|
-
import { type
|
|
4
|
-
import { type DigestAction, type EditorialAction, type VisibilityAction } from './jungle-store.js';
|
|
2
|
+
import { type QuoteDrop } from './jungle-digest.js';
|
|
3
|
+
import { type DigestAction, type EditorialAction, type TitleAction, type UriAction, type VisibilityAction } from './jungle-store.js';
|
|
5
4
|
import type { BundleVerifier } from './inspect.js';
|
|
6
5
|
/**
|
|
7
6
|
* `sponge import jungle DIR`: every Jungle Reading note (`kb/notes/reading/<slug>.md`) onto the item holding its capture.
|
|
8
7
|
* The capture goes through the verified bundle import (always as a private association); the note becomes an accepted
|
|
9
8
|
* editorial derivation, plus an accepted Digest v2 when it converts without altering owner text (non-verbatim, short and
|
|
10
|
-
* overflowing quotes are dropped and reported);
|
|
9
|
+
* overflowing quotes are dropped and reported); the note's title becomes the item's default title (a user override still wins), and a
|
|
10
|
+
* local capture (no web URI of its own) takes the note's `source_url`;
|
|
11
|
+
* `reading.status` becomes the item's visibility. Jungle is only read. A dry run reads the notes, captures and library and writes nothing.
|
|
11
12
|
*/
|
|
12
13
|
export interface JungleImportOptions {
|
|
13
14
|
verify?: BundleVerifier;
|
|
@@ -16,16 +17,16 @@ export interface JungleImportOptions {
|
|
|
16
17
|
offset?: number;
|
|
17
18
|
signal?: AbortSignal;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
type JungleSkip = 'not-reading-note' | 'malformed-note' | 'no-capture' | 'capture-missing' | 'item-deleted' | 'capture-unsupported' | 'capture-failed';
|
|
20
21
|
/** Per note: quotes assigned to ideas, and each dropped quote by its 0-based position in the note's Quotes section. */
|
|
21
|
-
|
|
22
|
+
interface JungleQuoteReport {
|
|
22
23
|
assigned: number;
|
|
23
24
|
dropped: {
|
|
24
25
|
quote: number;
|
|
25
26
|
reason: QuoteDrop;
|
|
26
27
|
}[];
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
interface JungleEntry {
|
|
29
30
|
slug: string;
|
|
30
31
|
form?: string;
|
|
31
32
|
status?: string;
|
|
@@ -35,11 +36,13 @@ export interface JungleEntry {
|
|
|
35
36
|
digest?: DigestAction;
|
|
36
37
|
digestIssues?: string[];
|
|
37
38
|
quotes?: JungleQuoteReport;
|
|
39
|
+
uri?: UriAction;
|
|
40
|
+
title?: TitleAction;
|
|
38
41
|
visibility?: VisibilityAction;
|
|
39
42
|
skipped?: JungleSkip;
|
|
40
43
|
code?: string;
|
|
41
44
|
}
|
|
42
|
-
/** Schema 2: digest actions are Digest v2 (`digest`, `digestIssues`; counts add notes per reason and quotes assigned or dropped); schema 1 reported Digest v1 as `v1`. */
|
|
45
|
+
/** Schema 2: digest actions are Digest v2 (`digest`, `digestIssues`; counts add notes per reason and quotes assigned or dropped); schema 1 reported Digest v1 as `v1`. `uri` and `title` were added within schema 2. */
|
|
43
46
|
export interface JungleImportReport {
|
|
44
47
|
schemaVersion: 2;
|
|
45
48
|
dryRun: boolean;
|
|
@@ -55,16 +58,13 @@ export interface JungleImportReport {
|
|
|
55
58
|
assigned: number;
|
|
56
59
|
dropped: Record<string, number>;
|
|
57
60
|
};
|
|
61
|
+
uri: Record<string, number>;
|
|
62
|
+
title: Record<string, number>;
|
|
58
63
|
visibility: Record<string, number>;
|
|
59
64
|
skipped: Record<string, number>;
|
|
60
65
|
};
|
|
61
66
|
entries: JungleEntry[];
|
|
62
67
|
nextOffset: number | null;
|
|
63
68
|
}
|
|
64
|
-
/** Digest v2 when the note converts and passes every rule against this revision's Markdown; otherwise the reasons. */
|
|
65
|
-
export declare function jungleDigest(editorial: JungleEditorial, markdown: string): {
|
|
66
|
-
digest?: DigestV2;
|
|
67
|
-
issues: string[];
|
|
68
|
-
quotes?: JungleQuoteReport;
|
|
69
|
-
};
|
|
70
69
|
export declare function importJungle(store: LibraryStore, jungle: string, options?: JungleImportOptions): Promise<JungleImportReport>;
|
|
70
|
+
export {};
|
|
@@ -1,48 +1,107 @@
|
|
|
1
1
|
import { readdir, realpath } from 'node:fs/promises';
|
|
2
2
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
3
|
-
import { readConfined
|
|
3
|
+
import { readConfined } from '../acquisition/files.js';
|
|
4
|
+
import { sha256 as hash } from '../util/hash.js';
|
|
4
5
|
import { object } from '../acquisition/normalize.js';
|
|
5
6
|
import { localizeMarkdown } from '../app/markdown.js';
|
|
6
7
|
import { checkDigest } from '../digest/schema.js';
|
|
7
8
|
import { accessClass, defaultVisibility } from '../identity/uri.js';
|
|
8
|
-
import { convertJungleDigest, parseJungleEditorial, jungleTags,
|
|
9
|
-
import { jungleNamespace, jungleRealm, visibilityAction } from './jungle-store.js';
|
|
9
|
+
import { convertJungleDigest, parseJungleEditorial, jungleTags, } from './jungle-digest.js';
|
|
10
|
+
import { jungleNamespace, jungleRealm, uriAction, visibilityAction, } from './jungle-store.js';
|
|
10
11
|
import { importBundleEntry, importBundleHash } from './apply.js';
|
|
12
|
+
import { SpongeError, codeOf } from '../errors.js';
|
|
11
13
|
const noteBytes = 4 * 1024 ** 2, captureBytes = 16 * 1024 ** 2;
|
|
12
|
-
const errorCode = (error) =>
|
|
14
|
+
const errorCode = (error) => codeOf(error);
|
|
13
15
|
/** A capture link is `<dir>/<name>` under `kb/`, never escaping it; the bundle is its directory. */
|
|
14
|
-
function captureLink(link) {
|
|
15
|
-
|
|
16
|
+
function captureLink(link) {
|
|
17
|
+
if (isAbsolute(link) ||
|
|
18
|
+
link.includes('\\') ||
|
|
19
|
+
link.split('/').length < 2 ||
|
|
20
|
+
link.split('/').some((p) => !p || p === '.' || p === '..'))
|
|
21
|
+
throw new SpongeError('no-capture');
|
|
22
|
+
return link;
|
|
23
|
+
}
|
|
16
24
|
/** Digest v2 when the note converts and passes every rule against this revision's Markdown; otherwise the reasons. */
|
|
17
|
-
|
|
25
|
+
function jungleDigest(editorial, markdown) {
|
|
18
26
|
if (editorial.form !== 'digest')
|
|
19
27
|
return { issues: [] };
|
|
20
|
-
const c = convertJungleDigest(editorial.markdown, markdown), check = checkDigest(c.gold, markdown, { tags: jungleTags }), issues = [...new Set([...c.issues, ...check.issues].map(i => `${i.path}:${i.code}`))];
|
|
21
|
-
const quotes = {
|
|
28
|
+
const c = convertJungleDigest(editorial.markdown, markdown), check = checkDigest(c.gold, markdown, { tags: jungleTags }), issues = [...new Set([...c.issues, ...check.issues].map((i) => `${i.path}:${i.code}`))];
|
|
29
|
+
const quotes = {
|
|
30
|
+
assigned: c.quotes.filter((q) => q.idea !== null).length,
|
|
31
|
+
dropped: c.quotes.flatMap((q, k) => (q.dropped ? [{ quote: k, reason: q.dropped }] : [])),
|
|
32
|
+
};
|
|
22
33
|
return c.digest && check.ok ? { digest: c.digest, issues: [], quotes } : { issues, quotes };
|
|
23
34
|
}
|
|
24
35
|
/** What a verified import would store, read from the bundle without the verifier: the same localization, from the manifest's asset list. */
|
|
25
36
|
async function previewCapture(kb, bundle, link) {
|
|
37
|
+
// An untrusted bundle fails as `integrity`, exactly as apply's `inspectImportBundle` reports it.
|
|
38
|
+
try {
|
|
39
|
+
return await previewBundle(kb, bundle, link);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof SpongeError)
|
|
43
|
+
throw error;
|
|
44
|
+
throw new SpongeError('integrity', { cause: error });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function previewBundle(kb, bundle, link) {
|
|
26
48
|
const manifest = object(JSON.parse((await readConfined(kb, join(bundle, 'capture.json'), captureBytes)).toString('utf8'))), pdf = manifest.kind === 'pdf';
|
|
27
|
-
const listed = (pdf
|
|
49
|
+
const listed = (pdf
|
|
50
|
+
? Array.isArray(manifest.images)
|
|
51
|
+
? manifest.images.map((i) => object(i).asset)
|
|
52
|
+
: []
|
|
53
|
+
: Array.isArray(manifest.assets)
|
|
54
|
+
? manifest.assets
|
|
55
|
+
: []).flatMap((raw) => {
|
|
56
|
+
const a = raw && typeof raw === 'object' ? raw : {};
|
|
57
|
+
return typeof a.path === 'string' && typeof a.sha256 === 'string'
|
|
58
|
+
? [{ relativePath: a.path, sha256: a.sha256, bytes: Number(a.bytes), mediaType: String(a.mimeType) }]
|
|
59
|
+
: [];
|
|
60
|
+
});
|
|
28
61
|
const text = (await readConfined(kb, link + '.md', captureBytes)).toString('utf8'), markdown = localizeMarkdown(pdf ? text.replace(/^<!-- pdf-page: ([1-9][0-9]*) -->$/gm, '## Page $1') : text, listed).markdown;
|
|
29
62
|
const locator = pdf ? String(object(manifest.source ?? {}).requestedUrl ?? '') : String(manifest.sourceUrl ?? '');
|
|
30
63
|
return { markdown, locator };
|
|
31
64
|
}
|
|
32
65
|
export async function importJungle(store, jungle, options = {}) {
|
|
33
66
|
if (!isAbsolute(jungle))
|
|
34
|
-
throw
|
|
35
|
-
const kb = await realpath(join(jungle, 'kb')), notesDir = join(kb, 'notes', 'reading'), files = (await readdir(notesDir)).filter(f => /^[a-z0-9][a-z0-9-]*\.md$/.test(f)).sort();
|
|
67
|
+
throw new SpongeError('invalid-input');
|
|
68
|
+
const kb = await realpath(join(jungle, 'kb')), notesDir = join(kb, 'notes', 'reading'), files = (await readdir(notesDir)).filter((f) => /^[a-z0-9][a-z0-9-]*\.md$/.test(f)).sort();
|
|
36
69
|
const limit = options.limit ?? 1000, offset = options.offset ?? 0, dryRun = options.dryRun !== false;
|
|
37
|
-
if (!Number.isSafeInteger(limit) ||
|
|
38
|
-
|
|
70
|
+
if (!Number.isSafeInteger(limit) ||
|
|
71
|
+
limit < 1 ||
|
|
72
|
+
limit > 1000 ||
|
|
73
|
+
!Number.isSafeInteger(offset) ||
|
|
74
|
+
offset < 0 ||
|
|
75
|
+
offset > files.length)
|
|
76
|
+
throw new SpongeError('invalid-input');
|
|
39
77
|
if (!dryRun && !options.verify)
|
|
40
|
-
throw
|
|
41
|
-
const report = {
|
|
42
|
-
|
|
78
|
+
throw new SpongeError('runtime-missing');
|
|
79
|
+
const report = {
|
|
80
|
+
schemaVersion: 2,
|
|
81
|
+
dryRun,
|
|
82
|
+
captureVerification: dryRun ? 'deferred' : 'verified',
|
|
83
|
+
notes: files.length,
|
|
84
|
+
counts: {
|
|
85
|
+
newItems: 0,
|
|
86
|
+
existingItems: 0,
|
|
87
|
+
editorial: {},
|
|
88
|
+
digest: {},
|
|
89
|
+
digestIssues: {},
|
|
90
|
+
quotes: { assigned: 0, dropped: {} },
|
|
91
|
+
uri: {},
|
|
92
|
+
title: {},
|
|
93
|
+
visibility: {},
|
|
94
|
+
skipped: {},
|
|
95
|
+
},
|
|
96
|
+
entries: [],
|
|
97
|
+
nextOffset: offset + limit < files.length ? offset + limit : null,
|
|
98
|
+
};
|
|
99
|
+
const bump = (m, k) => {
|
|
100
|
+
m[k] = (m[k] ?? 0) + 1;
|
|
101
|
+
}, seen = new Map();
|
|
43
102
|
for (const file of files.slice(offset, offset + limit)) {
|
|
44
103
|
if (options.signal?.aborted)
|
|
45
|
-
throw
|
|
104
|
+
throw new SpongeError('cancelled');
|
|
46
105
|
const slug = file.slice(0, -3);
|
|
47
106
|
let entry = { slug };
|
|
48
107
|
try {
|
|
@@ -51,8 +110,9 @@ export async function importJungle(store, jungle, options = {}) {
|
|
|
51
110
|
editorial = parseJungleEditorial((await readConfined(notesDir, file, noteBytes)).toString('utf8'));
|
|
52
111
|
}
|
|
53
112
|
catch (error) {
|
|
54
|
-
const
|
|
55
|
-
entry.skipped =
|
|
113
|
+
const raw = codeOf(error), code = raw === 'not-digest' || raw === 'no-capture' ? raw : 'malformed';
|
|
114
|
+
entry.skipped =
|
|
115
|
+
code === 'not-digest' ? 'not-reading-note' : code === 'no-capture' ? 'no-capture' : 'malformed-note';
|
|
56
116
|
throw entry;
|
|
57
117
|
}
|
|
58
118
|
entry = { slug, form: editorial.form, status: editorial.status };
|
|
@@ -72,23 +132,51 @@ export async function importJungle(store, jungle, options = {}) {
|
|
|
72
132
|
entry.skipped = 'capture-missing';
|
|
73
133
|
throw entry;
|
|
74
134
|
}
|
|
75
|
-
const prior = store.
|
|
135
|
+
const prior = store.imports.item(jungleRealm, hash(JSON.stringify([jungleNamespace, link])));
|
|
76
136
|
if (prior?.deleted) {
|
|
77
137
|
entry.skipped = 'item-deleted';
|
|
78
138
|
entry.itemId = prior.itemId;
|
|
79
139
|
throw entry;
|
|
80
140
|
}
|
|
81
141
|
// Two notes on one capture share its item, in a dry run too.
|
|
82
|
-
|
|
83
|
-
seen.
|
|
142
|
+
// The first note on a capture decides its default title and a local capture's URI; later notes see the URI it records.
|
|
143
|
+
const firstNote = !seen.has(link), firstNoteUri = seen.get(link);
|
|
144
|
+
entry.item = prior || !firstNote ? 'existing' : 'new';
|
|
145
|
+
if (firstNote)
|
|
146
|
+
seen.set(link, uriAction(true, true, editorial.sourceUrl).uri);
|
|
84
147
|
if (dryRun) {
|
|
85
148
|
if (prior?.revisionId) {
|
|
86
|
-
const markdown = store.
|
|
87
|
-
|
|
149
|
+
const markdown = store.revisions.markdown(prior.revisionId), { digest, issues, quotes } = jungleDigest(editorial, markdown), plan = store.imports.planJungleNote({
|
|
150
|
+
itemId: prior.itemId,
|
|
151
|
+
revisionId: prior.revisionId,
|
|
152
|
+
editorial,
|
|
153
|
+
...(digest ? { digest } : {}),
|
|
154
|
+
firstNote,
|
|
155
|
+
...(firstNoteUri ? { firstNoteUri } : {}),
|
|
156
|
+
});
|
|
157
|
+
Object.assign(entry, {
|
|
158
|
+
itemId: prior.itemId,
|
|
159
|
+
editorial: plan.editorial,
|
|
160
|
+
digest: plan.digest,
|
|
161
|
+
...(issues.length ? { digestIssues: issues } : {}),
|
|
162
|
+
...(quotes ? { quotes } : {}),
|
|
163
|
+
uri: plan.uri,
|
|
164
|
+
title: plan.title,
|
|
165
|
+
visibility: plan.visibility,
|
|
166
|
+
});
|
|
88
167
|
}
|
|
168
|
+
// A capture without a web locator is stored as a local file; predict the URI, title and access it would get.
|
|
89
169
|
else {
|
|
90
|
-
const preview = await previewCapture(kb, bundle, link), { digest, issues, quotes } = jungleDigest(editorial, preview.markdown), access = accessClass(jungleRealm, preview.locator);
|
|
91
|
-
Object.assign(entry, {
|
|
170
|
+
const preview = await previewCapture(kb, bundle, link), { digest, issues, quotes } = jungleDigest(editorial, preview.markdown), local = !/^https?:/i.test(preview.locator), uri = uriAction(local, firstNote, editorial.sourceUrl), access = accessClass(jungleRealm, local ? 'file:' : preview.locator, uri.uri ?? firstNoteUri), noteTitle = editorial.title.trim();
|
|
171
|
+
Object.assign(entry, {
|
|
172
|
+
editorial: 'new',
|
|
173
|
+
digest: editorial.form !== 'digest' ? 'not-digest' : digest ? 'new' : 'not-convertible',
|
|
174
|
+
...(issues.length ? { digestIssues: issues } : {}),
|
|
175
|
+
...(quotes ? { quotes } : {}),
|
|
176
|
+
uri: uri.action,
|
|
177
|
+
title: !firstNote ? 'other-note' : noteTitle && Buffer.byteLength(noteTitle) <= 4096 ? 'set' : 'unchanged',
|
|
178
|
+
visibility: visibilityAction({ access, default: defaultVisibility(access, 'private'), override: null }, editorial.status, false).action,
|
|
179
|
+
});
|
|
92
180
|
}
|
|
93
181
|
}
|
|
94
182
|
else {
|
|
@@ -104,19 +192,36 @@ export async function importJungle(store, jungle, options = {}) {
|
|
|
104
192
|
entry.code = code;
|
|
105
193
|
throw entry;
|
|
106
194
|
}
|
|
107
|
-
const itemId = imported.itemId, revisionId = store.
|
|
108
|
-
const plan = store.applyJungleNote({
|
|
109
|
-
|
|
195
|
+
const itemId = imported.itemId, revisionId = store.imports.revision(itemId, importBundleHash(imported.sourceHash)), markdown = store.revisions.markdown(revisionId), { digest, issues, quotes } = jungleDigest(editorial, markdown);
|
|
196
|
+
const plan = store.imports.applyJungleNote({
|
|
197
|
+
itemId,
|
|
198
|
+
revisionId,
|
|
199
|
+
editorial,
|
|
200
|
+
...(digest ? { digest } : {}),
|
|
201
|
+
firstNote,
|
|
202
|
+
});
|
|
203
|
+
Object.assign(entry, {
|
|
204
|
+
itemId,
|
|
205
|
+
editorial: plan.editorial,
|
|
206
|
+
digest: plan.digest,
|
|
207
|
+
...(issues.length ? { digestIssues: issues } : {}),
|
|
208
|
+
...(quotes ? { quotes } : {}),
|
|
209
|
+
uri: plan.uri,
|
|
210
|
+
title: plan.title,
|
|
211
|
+
visibility: plan.visibility,
|
|
212
|
+
});
|
|
110
213
|
}
|
|
111
214
|
bump(report.counts.editorial, entry.editorial);
|
|
112
215
|
bump(report.counts.digest, entry.digest);
|
|
113
|
-
for (const i of new Set((entry.digestIssues ?? []).map(i => i.replace(/\.\d+/g, '.N'))))
|
|
216
|
+
for (const i of new Set((entry.digestIssues ?? []).map((i) => i.replace(/\.\d+/g, '.N'))))
|
|
114
217
|
bump(report.counts.digestIssues, i);
|
|
115
218
|
if (entry.quotes) {
|
|
116
219
|
report.counts.quotes.assigned += entry.quotes.assigned;
|
|
117
220
|
for (const d of entry.quotes.dropped)
|
|
118
221
|
bump(report.counts.quotes.dropped, d.reason);
|
|
119
222
|
}
|
|
223
|
+
bump(report.counts.uri, entry.uri);
|
|
224
|
+
bump(report.counts.title, entry.title);
|
|
120
225
|
bump(report.counts.visibility, entry.visibility);
|
|
121
226
|
if (entry.item === 'new')
|
|
122
227
|
report.counts.newItems++;
|