@hraness/sponge 0.2.5 → 0.2.6
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 +2 -1
- package/dist/src/cli/digest.js +3 -3
- package/dist/src/cli/import.js +1 -1
- package/dist/src/digest/editorial.d.ts +5 -4
- package/dist/src/digest/editorial.js +19 -13
- package/dist/src/digest/pipeline.d.ts +6 -6
- package/dist/src/digest/pipeline.js +29 -40
- package/dist/src/digest/schema.d.ts +50 -9
- package/dist/src/digest/schema.js +55 -35
- package/dist/src/digest/store.d.ts +8 -8
- package/dist/src/digest/store.js +10 -8
- package/dist/src/importers/jungle-digest.d.ts +28 -18
- package/dist/src/importers/jungle-digest.js +40 -65
- package/dist/src/importers/jungle-store.d.ts +6 -6
- package/dist/src/importers/jungle-store.js +20 -19
- package/dist/src/importers/jungle.d.ts +27 -11
- package/dist/src/importers/jungle.js +21 -13
- package/dist/src/portability/backup.js +3 -1
- package/dist/src/publish/projection.js +2 -2
- package/native/runtime-manifest.json +4 -4
- package/package.json +1 -1
package/dist/src/digest/store.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { checkDigest, parseDigest } from './schema.js';
|
|
1
|
+
import { checkDigest, digestSchemaVersion, parseDigest } from './schema.js';
|
|
2
2
|
import { markdownFor } from '../annotations/store.js';
|
|
3
3
|
import { mutate } from '../store/changes.js';
|
|
4
4
|
import { StoreError } from '../store/types.js';
|
|
5
5
|
import { digest, hash } from '../store/files.js';
|
|
6
6
|
const now = () => new Date().toISOString();
|
|
7
7
|
const fail = (code = 'invalid-input') => { throw new StoreError(code); };
|
|
8
|
-
/** A parsed digest serialized in fixed key order, so key order never makes a different object. */
|
|
8
|
+
/** A parsed digest serialized in fixed key order, so key order never makes a different object. Optional fields appear only when present. */
|
|
9
9
|
export function canonicalDigest(d) {
|
|
10
|
-
return JSON.stringify({ schemaVersion: d.schemaVersion, form: d.form, gist: { lead: d.gist.lead, ...(d.gist.context !== undefined ? { context: d.gist.context } : {}) }, ideas: d.ideas.map(i => ({ claim: i.claim, detail: i.detail, kind: i.kind, voice: i.voice.type === 'author' ? { type: 'author' } : { type: i.voice.type, name: i.voice.name }, quotes: i.quotes.map(q => ({ text: q.text, ...(q.speaker !== undefined ? { speaker: q.speaker } : {}), ...(q.locator ? { locator: q.locator.type === 'timestamp' ? { type: 'timestamp', ms: q.locator.ms } : { type: 'page', page: q.locator.page } } : {}) })) })), tags: [...d.tags] });
|
|
10
|
+
return JSON.stringify({ schemaVersion: d.schemaVersion, form: d.form, gist: { lead: d.gist.lead, ...(d.gist.context !== undefined ? { context: d.gist.context } : {}) }, ideas: d.ideas.map(i => ({ claim: i.claim, detail: i.detail, ...(i.kind !== undefined ? { kind: i.kind } : {}), ...(i.voice ? { voice: i.voice.type === 'author' ? { type: 'author' } : { type: i.voice.type, name: i.voice.name } } : {}), quotes: i.quotes.map(q => ({ text: q.text, ...(q.speaker !== undefined ? { speaker: q.speaker } : {}), ...(q.attribution !== undefined ? { attribution: q.attribution } : {}), ...(q.locator ? { locator: q.locator.type === 'timestamp' ? { type: 'timestamp', ms: q.locator.ms } : { type: 'page', page: q.locator.page } } : {}) })) })), tags: [...d.tags] });
|
|
11
11
|
}
|
|
12
12
|
/** Content address of a derivation: the same output from the same input is the same immutable object on every device. */
|
|
13
13
|
export const derivationId = (revisionId, kind, inputHash, output) => digest(JSON.stringify(['derivation-v1', revisionId, kind, inputHash, output]));
|
|
14
14
|
/**
|
|
15
|
-
* Store a draft digest only when every
|
|
16
|
-
* Drafts are immutable data, not change-log entries; a different output is a new object.
|
|
15
|
+
* Store a draft digest only when it is Digest v2 and every v2 rule passes against the revision it was made from.
|
|
16
|
+
* Stored v1 digests stay readable; new ones are v2. Drafts are immutable data, not change-log entries; a different output is a new object.
|
|
17
17
|
*/
|
|
18
18
|
export function putDigest(db, input) {
|
|
19
19
|
hash(input.inputHash);
|
|
@@ -26,6 +26,8 @@ export function putDigest(db, input) {
|
|
|
26
26
|
const parsed = parseDigest(input.output);
|
|
27
27
|
if (!parsed.digest)
|
|
28
28
|
throw Object.assign(new StoreError('invalid-input'), { issues: parsed.issues });
|
|
29
|
+
if (parsed.digest.schemaVersion !== digestSchemaVersion)
|
|
30
|
+
throw Object.assign(new StoreError('invalid-input'), { issues: [{ path: 'schemaVersion', code: 'superseded' }] });
|
|
29
31
|
const check = checkDigest(parsed.digest, markdownFor(db, input.revisionId), input.options);
|
|
30
32
|
if (!check.ok)
|
|
31
33
|
throw Object.assign(new StoreError('invalid-input'), { issues: check.issues });
|
|
@@ -35,7 +37,7 @@ export function putDigest(db, input) {
|
|
|
35
37
|
const id = derivationId(input.revisionId, 'digest', input.inputHash, output);
|
|
36
38
|
if (db.prepare('SELECT 1 FROM derivations WHERE id=?').get(id))
|
|
37
39
|
return { id, duplicate: true };
|
|
38
|
-
db.prepare("INSERT INTO derivations(id,item_id,revision_id,kind,schema_version,producer_json,input_hash,output_json,created_at) VALUES(?,?,?,'digest'
|
|
40
|
+
db.prepare("INSERT INTO derivations(id,item_id,revision_id,kind,schema_version,producer_json,input_hash,output_json,created_at) VALUES(?,?,?,'digest',?,?,?,?,?)").run(id, input.itemId, input.revisionId, parsed.digest.schemaVersion, producer, input.inputHash, output, now());
|
|
39
41
|
return { id, duplicate: false };
|
|
40
42
|
}
|
|
41
43
|
function derivationRow(r) { return { id: String(r.id), itemId: String(r.item_id), revisionId: String(r.revision_id), kind: 'digest', producer: JSON.parse(String(r.producer_json)), inputHash: String(r.input_hash), output: JSON.parse(String(r.output_json)), review: JSON.parse(String(r.review_json)), createdAt: String(r.created_at) }; }
|
|
@@ -69,9 +71,9 @@ export function reviewDigest(db, id, review) {
|
|
|
69
71
|
mutate(db, 'derivation', id, path, next);
|
|
70
72
|
return derivationRow(db.prepare('SELECT * FROM derivations WHERE id=?').get(id));
|
|
71
73
|
}
|
|
72
|
-
/** The accepted subset of a digest. Rejected or unreviewed fields never leave the device. */
|
|
74
|
+
/** The accepted subset of a digest (v1 or v2). Rejected or unreviewed fields never leave the device; v1 ideas keep their kind. */
|
|
73
75
|
export function acceptedDigest(d) {
|
|
74
|
-
const ideas = d.output.ideas.flatMap((idea, i) => d.review.ideas?.[i] === 'accepted' ? [{ claim: idea.claim, detail: idea.detail, kind: idea.kind, quotes: idea.quotes.filter((_, j) => d.review.quotes?.[i]?.[j] === 'accepted').map(q => q.text) }] : []);
|
|
76
|
+
const ideas = d.output.ideas.flatMap((idea, i) => d.review.ideas?.[i] === 'accepted' ? [{ claim: idea.claim, detail: idea.detail, ...(idea.kind !== undefined ? { kind: idea.kind } : {}), quotes: idea.quotes.filter((_, j) => d.review.quotes?.[i]?.[j] === 'accepted').map(q => q.text) }] : []);
|
|
75
77
|
const lead = d.review.gist === 'accepted' ? d.output.gist.lead : undefined;
|
|
76
78
|
return lead || ideas.length ? { ...(lead ? { lead } : {}), ideas } : undefined;
|
|
77
79
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { type DigestIssue, type
|
|
1
|
+
import { type DigestIssue, type DigestV2 } from '../digest/schema.js';
|
|
2
2
|
import { type Form, type QuoteMatch } from '../digest/pipeline.js';
|
|
3
3
|
/**
|
|
4
|
-
* Pure conversion of one reviewed Jungle Reading digest note (`kb/notes/reading/<slug>.md`) into Digest
|
|
5
|
-
* quotes in their own section, so each quote is assigned to the idea it shares the most content words with.
|
|
4
|
+
* Pure conversion of one reviewed Jungle Reading digest note (`kb/notes/reading/<slug>.md`) into Digest v2. Jungle keeps
|
|
5
|
+
* quotes in their own section, so each verbatim quote is assigned to the idea it shares the most content words with.
|
|
6
|
+
* Owner text is never cut or rewritten: a note that does not fit v2 as written is reported, not converted.
|
|
6
7
|
* Nothing here reads files; callers pass the note and, when available, its capture Markdown.
|
|
7
8
|
*/
|
|
8
9
|
export declare const jungleTags: readonly ["agent-eng", "ai", "work", "business", "creative-coding", "finance", "music", "internet", "security", "history", "art", "hardware", "climate", "article", "interactive", "code", "x", "video"];
|
|
9
|
-
/** Tags that name how a source is consumed;
|
|
10
|
+
/** Tags that name how a source is consumed; a digest carries that as `form`. */
|
|
10
11
|
export declare const jungleFormatTags: readonly ["article", "interactive", "x", "video"];
|
|
11
12
|
export declare class JungleDigestError extends Error {
|
|
12
13
|
readonly code: 'not-digest' | 'malformed' | 'no-capture';
|
|
@@ -32,23 +33,25 @@ export interface JungleNote {
|
|
|
32
33
|
}[];
|
|
33
34
|
quotes: JungleQuote[];
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Why a Jungle quote is left out of the digest: not found as written in the capture (`snapped` matches differ in markup),
|
|
38
|
+
* under four words, over the quote budget (count, words or source share), or every idea already holds two.
|
|
39
|
+
*/
|
|
40
|
+
export type QuoteDrop = 'not-verbatim' | 'too-short' | 'budget' | 'overflow';
|
|
36
41
|
export interface JungleConversion {
|
|
37
42
|
note: JungleNote;
|
|
38
|
-
/**
|
|
39
|
-
gold:
|
|
40
|
-
/** Per Jungle quote: the
|
|
43
|
+
/** The note as Digest v2: the Gist's first sentence as lead and the rest as context, every idea, and the quotes that survive. */
|
|
44
|
+
gold: DigestV2;
|
|
45
|
+
/** Per Jungle quote, in note order: the idea it went to (null when dropped), how it located in the capture, and why it was dropped. */
|
|
41
46
|
quotes: {
|
|
42
47
|
idea: number | null;
|
|
43
48
|
match?: QuoteMatch;
|
|
49
|
+
dropped?: QuoteDrop;
|
|
44
50
|
}[];
|
|
45
|
-
|
|
46
|
-
/** checkDigest issues of the gold against the capture; empty without a capture. */
|
|
51
|
+
/** Why the gold is not a valid Digest v2: shape issues (an empty detail), then checkDigest issues against the capture when one is given. */
|
|
47
52
|
issues: DigestIssue[];
|
|
48
|
-
/** The gold
|
|
49
|
-
|
|
50
|
-
/** Gold passes every v1 rule against the capture with nothing dropped or trimmed. `kind` is never gold: Jungle has no kinds. */
|
|
51
|
-
clean: boolean;
|
|
53
|
+
/** The gold, when a capture was given and the gold passes every v2 rule against it. */
|
|
54
|
+
digest?: DigestV2;
|
|
52
55
|
}
|
|
53
56
|
export type Yaml = string | boolean | Yaml[] | {
|
|
54
57
|
[k: string]: Yaml;
|
|
@@ -58,7 +61,7 @@ export declare function captureBody(capture: string): string;
|
|
|
58
61
|
export declare function parseJungleNote(markdown: string): JungleNote;
|
|
59
62
|
/**
|
|
60
63
|
* One Jungle Reading note of any form (digest, social, note, conversation), kept whole: the exact Markdown and its hash,
|
|
61
|
-
* the parsed frontmatter, and the gist, ideas and quotes where the form has them. Nothing is trimmed to
|
|
64
|
+
* the parsed frontmatter, and the gist, ideas and quotes where the form has them. Nothing is trimmed to digest limits.
|
|
62
65
|
* `capture` is the evidence the note is about: the first `synthesizes` relation, else the first `evidenced-by`.
|
|
63
66
|
*/
|
|
64
67
|
export interface JungleEditorial {
|
|
@@ -84,14 +87,21 @@ export declare function parseJungleEditorial(markdown: string): JungleEditorial;
|
|
|
84
87
|
/** Content-word stems: lowercase, stopwords out, a crude plural/tense strip. Shared by idea assignment and lexical coverage. */
|
|
85
88
|
export declare function contentTerms(text: string): Set<string>;
|
|
86
89
|
export declare function termSimilarity(a: Set<string>, b: Set<string>): number;
|
|
87
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Each quote in order goes to the idea sharing the most content words with its claim and detail (ties: the earliest idea),
|
|
92
|
+
* else to the next best idea with room; null when every idea already holds `cap` quotes.
|
|
93
|
+
*/
|
|
88
94
|
export declare function assignQuotes(ideas: {
|
|
89
95
|
claim: string;
|
|
90
96
|
detail: string;
|
|
91
97
|
}[], quotes: {
|
|
92
98
|
text: string;
|
|
93
|
-
}[]): (number | null)[];
|
|
99
|
+
}[], cap?: number): (number | null)[];
|
|
94
100
|
export declare const firstSentence: (text: string) => readonly [string, string];
|
|
95
101
|
export declare function formOf(note: JungleNote): Form;
|
|
96
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Convert one Jungle digest note. With `capture` (the capture Markdown, frontmatter allowed), each quote is kept only when it
|
|
104
|
+
* occurs as written (folded typography only) and has at least four words; whole quotes then leave, longest and then latest in
|
|
105
|
+
* the note first, until the rest fit the quote budget (never the last one); survivors are assigned to ideas, and the gold is checked.
|
|
106
|
+
*/
|
|
97
107
|
export declare function convertJungleDigest(noteMarkdown: string, capture?: string): JungleConversion;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { foldText,
|
|
3
|
-
import { checkDigest, digestLimits as L } from '../digest/schema.js';
|
|
4
|
-
import {
|
|
2
|
+
import { foldText, words } from '../annotations/quote.js';
|
|
3
|
+
import { checkDigest, digestLimits as L, parseDigest } from '../digest/schema.js';
|
|
4
|
+
import { snapQuote } from '../digest/pipeline.js';
|
|
5
5
|
/**
|
|
6
|
-
* Pure conversion of one reviewed Jungle Reading digest note (`kb/notes/reading/<slug>.md`) into Digest
|
|
7
|
-
* quotes in their own section, so each quote is assigned to the idea it shares the most content words with.
|
|
6
|
+
* Pure conversion of one reviewed Jungle Reading digest note (`kb/notes/reading/<slug>.md`) into Digest v2. Jungle keeps
|
|
7
|
+
* quotes in their own section, so each verbatim quote is assigned to the idea it shares the most content words with.
|
|
8
|
+
* Owner text is never cut or rewritten: a note that does not fit v2 as written is reported, not converted.
|
|
8
9
|
* Nothing here reads files; callers pass the note and, when available, its capture Markdown.
|
|
9
10
|
*/
|
|
10
11
|
export const jungleTags = ['agent-eng', 'ai', 'work', 'business', 'creative-coding', 'finance', 'music', 'internet', 'security', 'history', 'art', 'hardware', 'climate', 'article', 'interactive', 'code', 'x', 'video'];
|
|
11
|
-
/** Tags that name how a source is consumed;
|
|
12
|
+
/** Tags that name how a source is consumed; a digest carries that as `form`. */
|
|
12
13
|
export const jungleFormatTags = ['article', 'interactive', 'x', 'video'];
|
|
13
14
|
export class JungleDigestError extends Error {
|
|
14
15
|
code;
|
|
@@ -148,21 +149,14 @@ export function termSimilarity(a, b) { if (!a.size || !b.size)
|
|
|
148
149
|
return 0; let n = 0; for (const t of a)
|
|
149
150
|
if (b.has(t))
|
|
150
151
|
n++; return n / Math.sqrt(a.size * b.size); }
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for (const cap of [1, L.maxQuotesPerIdea])
|
|
160
|
-
for (const p of pairs)
|
|
161
|
-
if (out[p.q] === null && load[p.i] < cap) {
|
|
162
|
-
out[p.q] = p.i;
|
|
163
|
-
load[p.i]++;
|
|
164
|
-
}
|
|
165
|
-
return out;
|
|
152
|
+
/**
|
|
153
|
+
* Each quote in order goes to the idea sharing the most content words with its claim and detail (ties: the earliest idea),
|
|
154
|
+
* else to the next best idea with room; null when every idea already holds `cap` quotes.
|
|
155
|
+
*/
|
|
156
|
+
export function assignQuotes(ideas, quotes, cap = L.maxQuotesPerIdea) {
|
|
157
|
+
const terms = ideas.map(i => contentTerms(`${i.claim} ${i.detail}`)), load = ideas.map(() => 0);
|
|
158
|
+
return quotes.map(q => { const t = contentTerms(q.text), pick = terms.map((it, i) => ({ i, s: termSimilarity(t, it) })).sort((a, b) => b.s - a.s || a.i - b.i).find(r => load[r.i] < cap); if (!pick)
|
|
159
|
+
return null; load[pick.i]++; return pick.i; });
|
|
166
160
|
}
|
|
167
161
|
export const firstSentence = (text) => { const m = /^(.+?[.!?]["”’)]*)\s+(?=["“(]?[A-Z0-9])/.exec(text); return m ? [m[1], text.slice(m[0].length)] : [text, '']; };
|
|
168
162
|
export function formOf(note) {
|
|
@@ -172,50 +166,31 @@ export function formOf(note) {
|
|
|
172
166
|
return 'thread';
|
|
173
167
|
return /\.pdf(?:$|[?#])|arxiv\.org\/pdf\//i.test(note.sourceUrl) ? 'pdf' : 'article';
|
|
174
168
|
}
|
|
175
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* Convert one Jungle digest note. With `capture` (the capture Markdown, frontmatter allowed), each quote is kept only when it
|
|
171
|
+
* occurs as written (folded typography only) and has at least four words; whole quotes then leave, longest and then latest in
|
|
172
|
+
* the note first, until the rest fit the quote budget (never the last one); survivors are assigned to ideas, and the gold is checked.
|
|
173
|
+
*/
|
|
176
174
|
export function convertJungleDigest(noteMarkdown, capture) {
|
|
177
|
-
const note = parseJungleNote(noteMarkdown),
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
r = next;
|
|
188
|
-
}
|
|
189
|
-
const assigned = assign(note.ideas, note.quotes), authors = new Set(note.authors.map(a => foldText(a).toLowerCase())), form = formOf(note);
|
|
190
|
-
const located = note.quotes.map(q => source === undefined ? undefined : snapQuote(source, q.text));
|
|
191
|
-
const ideas = note.ideas.map((idea, i) => {
|
|
192
|
-
const mine = note.quotes.map((q, k) => ({ q, k })).filter(x => assigned[x.k] === i);
|
|
193
|
-
const other = mine.find(x => !authors.has(foldText(x.q.speaker).toLowerCase()));
|
|
194
|
-
// A non-author in a talk is a speaker; in writing, the author reports them.
|
|
195
|
-
const voice = other ? { type: form === 'video' ? 'speaker' : 'reported', name: other.q.speaker } : { type: 'author' };
|
|
196
|
-
const quotes = mine.map(x => ({ text: located[x.k]?.text ?? x.q.text, speaker: x.q.speaker }));
|
|
197
|
-
return { claim: idea.claim, detail: idea.detail, kind: 'argument', voice, quotes };
|
|
198
|
-
});
|
|
199
|
-
const content = note.tags.filter(t => t !== 'reading' && !jungleFormatTags.includes(t)), tags = (content.length ? content : note.tags.filter(t => t !== 'reading')).slice(0, L.maxTags);
|
|
200
|
-
if (!content.length)
|
|
201
|
-
lossy.add('no-content-tag');
|
|
202
|
-
if (assigned.some(a => a === null))
|
|
203
|
-
lossy.add('quote-unassigned');
|
|
204
|
-
if (located.some(l => l?.match === 'missing'))
|
|
205
|
-
lossy.add('quote-missing');
|
|
206
|
-
if (ideas.some(i => !i.quotes.length))
|
|
207
|
-
lossy.add('idea-unquoted');
|
|
208
|
-
const gold = { schemaVersion: 1, form, gist: { lead, ...(context ? { context } : {}) }, ideas, tags };
|
|
209
|
-
const options = { tags: jungleTags };
|
|
210
|
-
const issues = source === undefined ? [] : checkDigest(gold, source, options).issues;
|
|
211
|
-
let importable;
|
|
212
|
-
if (source !== undefined) {
|
|
213
|
-
const kept = note.quotes.map((_, k) => assigned[k] !== null && located[k]?.match !== 'missing');
|
|
214
|
-
// Import keeps Sponge's ordering rule: ideas by their earliest quote in the capture, quotes of at least minQuoteWords.
|
|
215
|
-
const at = (t) => locateAll(source, t)[0]?.start ?? Number.MAX_SAFE_INTEGER;
|
|
216
|
-
const trimmed = { ...gold, ideas: note.ideas.map((_, i) => ({ ...ideas[i], quotes: note.quotes.flatMap((__, k) => assigned[k] === i && kept[k] && words(located[k].text) >= minQuoteWords ? [{ text: located[k].text, speaker: note.quotes[k].speaker }] : []).sort((a, b) => at(a.text) - at(b.text)) })).filter(i => i.quotes.length).sort((a, b) => at(a.quotes[0].text) - at(b.quotes[0].text)) };
|
|
217
|
-
if (checkDigest(trimmed, source, options).ok)
|
|
218
|
-
importable = trimmed;
|
|
175
|
+
const note = parseJungleNote(noteMarkdown), source = capture === undefined ? undefined : captureBody(capture), [lead, context] = firstSentence(note.gist);
|
|
176
|
+
const located = note.quotes.map(q => source === undefined ? undefined : snapQuote(source, q.text).match);
|
|
177
|
+
const dropped = note.quotes.map((q, k) => located[k] === 'missing' || located[k] === 'snapped' ? 'not-verbatim' : words(q.text) < L.minQuoteWords ? 'too-short' : undefined);
|
|
178
|
+
let kept = note.quotes.flatMap((q, k) => dropped[k] ? [] : [{ q, k, n: words(q.text) }]);
|
|
179
|
+
// The pipeline's budget rule, applied to owner quotes: drop whole quotes, never edit one. A single quote over budget still fails the check.
|
|
180
|
+
const budget = source === undefined ? Infinity : Math.min(L.totalQuoteWords, Math.max(L.quoteWords, Math.floor(words(source) * L.sourceQuoteShare)));
|
|
181
|
+
while (kept.length > 1 && (kept.length > L.maxQuotes || kept.reduce((n, x) => n + x.n, 0) > budget)) {
|
|
182
|
+
const x = [...kept].sort((a, b) => b.n - a.n || b.k - a.k)[0];
|
|
183
|
+
dropped[x.k] = 'budget';
|
|
184
|
+
kept = kept.filter(y => y !== x);
|
|
219
185
|
}
|
|
220
|
-
|
|
186
|
+
const assigned = note.quotes.map(() => null);
|
|
187
|
+
assignQuotes(note.ideas, kept.map(x => x.q)).forEach((idea, n) => { if (idea === null)
|
|
188
|
+
dropped[kept[n].k] = 'overflow';
|
|
189
|
+
else
|
|
190
|
+
assigned[kept[n].k] = idea; });
|
|
191
|
+
const ideas = note.ideas.map((idea, i) => ({ claim: idea.claim, detail: idea.detail, quotes: note.quotes.flatMap((q, k) => assigned[k] === i ? [{ text: q.text, ...(q.attribution ? { attribution: q.attribution } : {}) }] : []) }));
|
|
192
|
+
const tags = note.tags.filter(t => t !== 'reading' && !jungleFormatTags.includes(t));
|
|
193
|
+
const gold = { schemaVersion: 2, form: formOf(note), gist: { lead, ...(context ? { context } : {}) }, ideas, tags };
|
|
194
|
+
const issues = [...parseDigest(gold).issues, ...source === undefined ? [] : checkDigest(gold, source, { tags: jungleTags }).issues];
|
|
195
|
+
return { note, gold, quotes: note.quotes.map((_, k) => ({ idea: assigned[k] ?? null, ...(located[k] ? { match: located[k] } : {}), ...(dropped[k] ? { dropped: dropped[k] } : {}) })), issues, ...(source !== undefined && !issues.length ? { digest: gold } : {}) };
|
|
221
196
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import { type
|
|
2
|
+
import { type DigestV2 } from '../digest/schema.js';
|
|
3
3
|
import { type VisibilityState } from '../identity/visibility.js';
|
|
4
4
|
import { type JungleEditorial } from './jungle-digest.js';
|
|
5
5
|
/**
|
|
@@ -8,7 +8,7 @@ import { type JungleEditorial } from './jungle-digest.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const jungleNamespace = "jungle", jungleRealm = "import:jungle", jungleProducer: {
|
|
10
10
|
readonly importer: "sponge-jungle-reading";
|
|
11
|
-
readonly version: "
|
|
11
|
+
readonly version: "2";
|
|
12
12
|
};
|
|
13
13
|
export type EditorialAction = 'new' | 'unchanged' | 'user-review-wins';
|
|
14
14
|
export type DigestAction = 'new' | 'unchanged' | 'not-convertible' | 'not-digest' | 'user-review-wins' | 'user-digest-wins';
|
|
@@ -17,13 +17,13 @@ export interface JungleNoteInput {
|
|
|
17
17
|
itemId: string;
|
|
18
18
|
revisionId: string;
|
|
19
19
|
editorial: JungleEditorial;
|
|
20
|
-
|
|
20
|
+
digest?: DigestV2;
|
|
21
21
|
}
|
|
22
22
|
export interface JunglePlan {
|
|
23
23
|
editorialId: string;
|
|
24
24
|
editorial: EditorialAction;
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
digestId?: string;
|
|
26
|
+
digest: DigestAction;
|
|
27
27
|
visibility: VisibilityAction;
|
|
28
28
|
}
|
|
29
29
|
/** What Jungle's `reading.status` asks for: `published` is Jungle's explicit public approval, `draft` stays private. */
|
|
@@ -39,7 +39,7 @@ export declare function visibilityAction(state: Pick<VisibilityState, 'access' |
|
|
|
39
39
|
export declare function planJungleNote(db: DatabaseSync, input: JungleNoteInput): JunglePlan;
|
|
40
40
|
/**
|
|
41
41
|
* Apply one note; the caller holds the write transaction. A new editorial object is accepted through `review.note` unless
|
|
42
|
-
* the user already rejected or cleared an earlier one; a
|
|
42
|
+
* the user already rejected or cleared an earlier one; a converted Digest v2 is stored and accepted per field, linked to the
|
|
43
43
|
* editorial object by its input hash, unless the user owns a digest of this item or changed an imported digest's review.
|
|
44
44
|
*/
|
|
45
45
|
export declare function applyJungleNote(db: DatabaseSync, input: JungleNoteInput): JunglePlan;
|
|
@@ -8,7 +8,7 @@ import { jungleTags } from './jungle-digest.js';
|
|
|
8
8
|
* The library side of `sponge import jungle`: one Jungle Reading note onto the item holding its capture.
|
|
9
9
|
* `planJungleNote` only reads; `applyJungleNote` runs the same plan inside the caller's transaction, through the logged paths.
|
|
10
10
|
*/
|
|
11
|
-
export const jungleNamespace = 'jungle', jungleRealm = 'import:jungle', jungleProducer = { importer: 'sponge-jungle-reading', version: '
|
|
11
|
+
export const jungleNamespace = 'jungle', jungleRealm = 'import:jungle', jungleProducer = { importer: 'sponge-jungle-reading', version: '2' };
|
|
12
12
|
/** Receipts of the importer's own visibility writes (local, like every import receipt), so a later user override is recognisable. */
|
|
13
13
|
const visibilityReceipts = 'jungle-visibility';
|
|
14
14
|
/** What Jungle's `reading.status` asks for: `published` is Jungle's explicit public approval, `draft` stays private. */
|
|
@@ -49,29 +49,30 @@ export function planJungleNote(db, input) {
|
|
|
49
49
|
const { itemId, revisionId, editorial } = input, editorialId = derivationId(revisionId, 'editorial', editorial.markdownSha256, canonicalEditorial(editorial));
|
|
50
50
|
const editorials = db.prepare("SELECT id,review_json FROM derivations WHERE item_id=? AND kind='editorial'").all(itemId);
|
|
51
51
|
const editorialAction = editorials.some(r => r.id === editorialId) ? 'unchanged' : editorials.some(r => JSON.parse(String(r.review_json)).note !== 'accepted') ? 'user-review-wins' : 'new';
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
// 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
|
+
let action = 'not-digest', digestId;
|
|
54
|
+
if (editorial.form === 'digest' && !input.digest)
|
|
55
|
+
action = 'not-convertible';
|
|
56
|
+
else if (input.digest) {
|
|
57
|
+
const digests = db.prepare("SELECT id,producer_json,output_json,review_json FROM derivations WHERE item_id=? AND kind='digest'").all(itemId), parsed = parseDigest(input.digest).digest;
|
|
58
|
+
digestId = parsed && derivationId(revisionId, 'digest', editorialId, canonicalDigest(parsed));
|
|
59
|
+
if (!parsed || parsed.schemaVersion !== 2)
|
|
60
|
+
action = 'not-convertible';
|
|
61
|
+
else if (digests.some(r => r.id === digestId))
|
|
62
|
+
action = 'unchanged';
|
|
62
63
|
else if (digests.some(r => !imported(r)))
|
|
63
|
-
|
|
64
|
+
action = 'user-digest-wins';
|
|
64
65
|
else if (digests.some(r => JSON.stringify(JSON.parse(String(r.review_json))) !== JSON.stringify(allAccepted(JSON.parse(String(r.output_json))))))
|
|
65
|
-
|
|
66
|
+
action = 'user-review-wins';
|
|
66
67
|
else
|
|
67
|
-
|
|
68
|
+
action = editorialAction === 'user-review-wins' ? 'user-review-wins' : 'new';
|
|
68
69
|
}
|
|
69
70
|
const visibility = visibilityAction(visibilityState(db, itemId), editorial.status, userOverride(db, itemId)).action;
|
|
70
|
-
return { editorialId, editorial: editorialAction, ...(
|
|
71
|
+
return { editorialId, editorial: editorialAction, ...(digestId ? { digestId } : {}), digest: action, visibility };
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* Apply one note; the caller holds the write transaction. A new editorial object is accepted through `review.note` unless
|
|
74
|
-
* the user already rejected or cleared an earlier one; a
|
|
75
|
+
* the user already rejected or cleared an earlier one; a converted Digest v2 is stored and accepted per field, linked to the
|
|
75
76
|
* editorial object by its input hash, unless the user owns a digest of this item or changed an imported digest's review.
|
|
76
77
|
*/
|
|
77
78
|
export function applyJungleNote(db, input) {
|
|
@@ -81,9 +82,9 @@ export function applyJungleNote(db, input) {
|
|
|
81
82
|
if (plan.editorial === 'new')
|
|
82
83
|
reviewEditorial(db, plan.editorialId, 'accepted');
|
|
83
84
|
}
|
|
84
|
-
if (plan.
|
|
85
|
-
const stored = putDigest(db, { itemId: input.itemId, revisionId: input.revisionId, producer: { ...producer, editorial: plan.editorialId }, inputHash: plan.editorialId, output: input.
|
|
86
|
-
reviewDigest(db, stored.id, allAccepted(input.
|
|
85
|
+
if (plan.digest === 'new') {
|
|
86
|
+
const stored = putDigest(db, { itemId: input.itemId, revisionId: input.revisionId, producer: { ...producer, editorial: plan.editorialId }, inputHash: plan.editorialId, output: input.digest, options: { tags: jungleTags } });
|
|
87
|
+
reviewDigest(db, stored.id, allAccepted(input.digest));
|
|
87
88
|
}
|
|
88
89
|
const action = visibilityAction(visibilityState(db, input.itemId), input.editorial.status, userOverride(db, input.itemId));
|
|
89
90
|
if (action.target !== undefined) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { LibraryStore } from '../store/index.js';
|
|
2
|
-
import { type
|
|
3
|
-
import { type JungleEditorial } from './jungle-digest.js';
|
|
2
|
+
import { type DigestV2 } from '../digest/schema.js';
|
|
3
|
+
import { type JungleEditorial, type QuoteDrop } from './jungle-digest.js';
|
|
4
4
|
import { type DigestAction, type EditorialAction, type VisibilityAction } from './jungle-store.js';
|
|
5
5
|
import type { BundleVerifier } from './inspect.js';
|
|
6
6
|
/**
|
|
7
7
|
* `sponge import jungle DIR`: every Jungle Reading note (`kb/notes/reading/<slug>.md`) onto the item holding its capture.
|
|
8
8
|
* The capture goes through the verified bundle import (always as a private association); the note becomes an accepted
|
|
9
|
-
* editorial derivation, plus an accepted Digest
|
|
10
|
-
* item's visibility. Jungle is only read. A dry run reads the notes, captures and library and writes nothing.
|
|
9
|
+
* 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); `reading.status` becomes the item's visibility. Jungle is only read. A dry run reads the notes, captures and library and writes nothing.
|
|
11
11
|
*/
|
|
12
12
|
export interface JungleImportOptions {
|
|
13
13
|
verify?: BundleVerifier;
|
|
@@ -17,6 +17,14 @@ export interface JungleImportOptions {
|
|
|
17
17
|
signal?: AbortSignal;
|
|
18
18
|
}
|
|
19
19
|
export type JungleSkip = 'not-reading-note' | 'malformed-note' | 'no-capture' | 'capture-missing' | 'item-deleted' | 'capture-unsupported' | 'capture-failed';
|
|
20
|
+
/** Per note: quotes assigned to ideas, and each dropped quote by its 0-based position in the note's Quotes section. */
|
|
21
|
+
export interface JungleQuoteReport {
|
|
22
|
+
assigned: number;
|
|
23
|
+
dropped: {
|
|
24
|
+
quote: number;
|
|
25
|
+
reason: QuoteDrop;
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
20
28
|
export interface JungleEntry {
|
|
21
29
|
slug: string;
|
|
22
30
|
form?: string;
|
|
@@ -24,14 +32,16 @@ export interface JungleEntry {
|
|
|
24
32
|
item?: 'new' | 'existing';
|
|
25
33
|
itemId?: string;
|
|
26
34
|
editorial?: EditorialAction;
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
digest?: DigestAction;
|
|
36
|
+
digestIssues?: string[];
|
|
37
|
+
quotes?: JungleQuoteReport;
|
|
29
38
|
visibility?: VisibilityAction;
|
|
30
39
|
skipped?: JungleSkip;
|
|
31
40
|
code?: string;
|
|
32
41
|
}
|
|
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`. */
|
|
33
43
|
export interface JungleImportReport {
|
|
34
|
-
schemaVersion:
|
|
44
|
+
schemaVersion: 2;
|
|
35
45
|
dryRun: boolean;
|
|
36
46
|
captureVerification: 'deferred' | 'verified';
|
|
37
47
|
notes: number;
|
|
@@ -39,16 +49,22 @@ export interface JungleImportReport {
|
|
|
39
49
|
newItems: number;
|
|
40
50
|
existingItems: number;
|
|
41
51
|
editorial: Record<string, number>;
|
|
42
|
-
|
|
52
|
+
digest: Record<string, number>;
|
|
53
|
+
digestIssues: Record<string, number>;
|
|
54
|
+
quotes: {
|
|
55
|
+
assigned: number;
|
|
56
|
+
dropped: Record<string, number>;
|
|
57
|
+
};
|
|
43
58
|
visibility: Record<string, number>;
|
|
44
59
|
skipped: Record<string, number>;
|
|
45
60
|
};
|
|
46
61
|
entries: JungleEntry[];
|
|
47
62
|
nextOffset: number | null;
|
|
48
63
|
}
|
|
49
|
-
/** Digest
|
|
50
|
-
export declare function
|
|
51
|
-
|
|
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;
|
|
52
67
|
issues: string[];
|
|
68
|
+
quotes?: JungleQuoteReport;
|
|
53
69
|
};
|
|
54
70
|
export declare function importJungle(store: LibraryStore, jungle: string, options?: JungleImportOptions): Promise<JungleImportReport>;
|
|
@@ -13,12 +13,13 @@ const errorCode = (error) => String(error?.code ?? (error instanceof Error ? err
|
|
|
13
13
|
/** A capture link is `<dir>/<name>` under `kb/`, never escaping it; the bundle is its directory. */
|
|
14
14
|
function captureLink(link) { if (isAbsolute(link) || link.includes('\\') || link.split('/').length < 2 || link.split('/').some(p => !p || p === '.' || p === '..'))
|
|
15
15
|
throw new JungleDigestError('no-capture'); return link; }
|
|
16
|
-
/** Digest
|
|
17
|
-
export function
|
|
16
|
+
/** Digest v2 when the note converts and passes every rule against this revision's Markdown; otherwise the reasons. */
|
|
17
|
+
export function jungleDigest(editorial, markdown) {
|
|
18
18
|
if (editorial.form !== 'digest')
|
|
19
19
|
return { issues: [] };
|
|
20
|
-
const c = convertJungleDigest(editorial.markdown, markdown), check = checkDigest(c.gold, markdown, { tags: jungleTags }), issues = [...
|
|
21
|
-
|
|
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 = { assigned: c.quotes.filter(q => q.idea !== null).length, dropped: c.quotes.flatMap((q, k) => q.dropped ? [{ quote: k, reason: q.dropped }] : []) };
|
|
22
|
+
return c.digest && check.ok ? { digest: c.digest, issues: [], quotes } : { issues, quotes };
|
|
22
23
|
}
|
|
23
24
|
/** What a verified import would store, read from the bundle without the verifier: the same localization, from the manifest's asset list. */
|
|
24
25
|
async function previewCapture(kb, bundle, link) {
|
|
@@ -37,7 +38,7 @@ export async function importJungle(store, jungle, options = {}) {
|
|
|
37
38
|
throw Object.assign(Error('invalid-input'), { code: 'invalid-input' });
|
|
38
39
|
if (!dryRun && !options.verify)
|
|
39
40
|
throw Object.assign(Error('runtime-missing'), { code: 'runtime-missing' });
|
|
40
|
-
const report = { schemaVersion:
|
|
41
|
+
const report = { schemaVersion: 2, dryRun, captureVerification: dryRun ? 'deferred' : 'verified', notes: files.length, counts: { newItems: 0, existingItems: 0, editorial: {}, digest: {}, digestIssues: {}, quotes: { assigned: 0, dropped: {} }, visibility: {}, skipped: {} }, entries: [], nextOffset: offset + limit < files.length ? offset + limit : null };
|
|
41
42
|
const bump = (m, k) => { m[k] = (m[k] ?? 0) + 1; }, seen = new Set();
|
|
42
43
|
for (const file of files.slice(offset, offset + limit)) {
|
|
43
44
|
if (options.signal?.aborted)
|
|
@@ -82,12 +83,12 @@ export async function importJungle(store, jungle, options = {}) {
|
|
|
82
83
|
seen.add(link);
|
|
83
84
|
if (dryRun) {
|
|
84
85
|
if (prior?.revisionId) {
|
|
85
|
-
const markdown = store.revisionMarkdown(prior.revisionId), {
|
|
86
|
-
Object.assign(entry, { itemId: prior.itemId, editorial: plan.editorial,
|
|
86
|
+
const markdown = store.revisionMarkdown(prior.revisionId), { digest, issues, quotes } = jungleDigest(editorial, markdown), plan = store.planJungleNote({ itemId: prior.itemId, revisionId: prior.revisionId, editorial, ...(digest ? { digest } : {}) });
|
|
87
|
+
Object.assign(entry, { itemId: prior.itemId, editorial: plan.editorial, digest: plan.digest, ...(issues.length ? { digestIssues: issues } : {}), ...(quotes ? { quotes } : {}), visibility: plan.visibility });
|
|
87
88
|
}
|
|
88
89
|
else {
|
|
89
|
-
const preview = await previewCapture(kb, bundle, link), {
|
|
90
|
-
Object.assign(entry, { editorial: 'new',
|
|
90
|
+
const preview = await previewCapture(kb, bundle, link), { digest, issues, quotes } = jungleDigest(editorial, preview.markdown), access = accessClass(jungleRealm, preview.locator);
|
|
91
|
+
Object.assign(entry, { editorial: 'new', digest: editorial.form !== 'digest' ? 'not-digest' : digest ? 'new' : 'not-convertible', ...(issues.length ? { digestIssues: issues } : {}), ...(quotes ? { quotes } : {}), visibility: visibilityAction({ access, default: defaultVisibility(access, 'private'), override: null }, editorial.status, false).action });
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
else {
|
|
@@ -103,12 +104,19 @@ export async function importJungle(store, jungle, options = {}) {
|
|
|
103
104
|
entry.code = code;
|
|
104
105
|
throw entry;
|
|
105
106
|
}
|
|
106
|
-
const itemId = imported.itemId, revisionId = store.importedRevision(itemId, importBundleHash(imported.sourceHash)), markdown = store.revisionMarkdown(revisionId), {
|
|
107
|
-
const plan = store.applyJungleNote({ itemId, revisionId, editorial, ...(
|
|
108
|
-
Object.assign(entry, { itemId, editorial: plan.editorial,
|
|
107
|
+
const itemId = imported.itemId, revisionId = store.importedRevision(itemId, importBundleHash(imported.sourceHash)), markdown = store.revisionMarkdown(revisionId), { digest, issues, quotes } = jungleDigest(editorial, markdown);
|
|
108
|
+
const plan = store.applyJungleNote({ itemId, revisionId, editorial, ...(digest ? { digest } : {}) });
|
|
109
|
+
Object.assign(entry, { itemId, editorial: plan.editorial, digest: plan.digest, ...(issues.length ? { digestIssues: issues } : {}), ...(quotes ? { quotes } : {}), visibility: plan.visibility });
|
|
109
110
|
}
|
|
110
111
|
bump(report.counts.editorial, entry.editorial);
|
|
111
|
-
bump(report.counts.
|
|
112
|
+
bump(report.counts.digest, entry.digest);
|
|
113
|
+
for (const i of new Set((entry.digestIssues ?? []).map(i => i.replace(/\.\d+/g, '.N'))))
|
|
114
|
+
bump(report.counts.digestIssues, i);
|
|
115
|
+
if (entry.quotes) {
|
|
116
|
+
report.counts.quotes.assigned += entry.quotes.assigned;
|
|
117
|
+
for (const d of entry.quotes.dropped)
|
|
118
|
+
bump(report.counts.quotes.dropped, d.reason);
|
|
119
|
+
}
|
|
112
120
|
bump(report.counts.visibility, entry.visibility);
|
|
113
121
|
if (entry.item === 'new')
|
|
114
122
|
report.counts.newItems++;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { constants, openSync, closeSync, fstatSync, fsyncSync, unlinkSync } from 'node:fs';
|
|
1
|
+
import { constants, openSync, closeSync, fstatSync, fsyncSync, unlinkSync, rmSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { openLibrary } from '../store/index.js';
|
|
4
4
|
import { safeDirectory } from '../store/files.js';
|
|
@@ -74,6 +74,8 @@ export async function backupLibrary(store, output) {
|
|
|
74
74
|
closeSync(fd);
|
|
75
75
|
try {
|
|
76
76
|
unlinkSync(snapshot.path);
|
|
77
|
+
for (const side of ['-wal', '-shm'])
|
|
78
|
+
rmSync(snapshot.path + side, { force: true });
|
|
77
79
|
}
|
|
78
80
|
finally {
|
|
79
81
|
store.finishBackup(snapshot.id);
|
|
@@ -12,8 +12,8 @@ function selectedClaim(db, revisionId, fields) {
|
|
|
12
12
|
return undefined;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* The public digest: the accepted fields of the current
|
|
16
|
-
* note (lead, claims, verbatim quotes within the
|
|
15
|
+
* The public digest: the accepted fields of the current digest (v2, or a stored v1), else the public subset of an accepted
|
|
16
|
+
* editorial note (lead, claims, verbatim quotes within the v2 limits). Never both, never attributions, and never the note's other content.
|
|
17
17
|
*/
|
|
18
18
|
function publicDigest(db, id) { const d = currentDigest(db, id); if (d)
|
|
19
19
|
return acceptedDigest(d); const e = currentEditorial(db, id); return e && publicEditorial(db, e); }
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
29
|
"build": {
|
|
30
|
-
"asrSourceSha256": "
|
|
30
|
+
"asrSourceSha256": "e944e4e0b1c2cf571e78a83fcfbf56a91a14fd08dcc701562d32555cf090ec40",
|
|
31
31
|
"clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
|
|
32
|
-
"embeddingSourceSha256": "
|
|
32
|
+
"embeddingSourceSha256": "7d58718911725b3c3dce35a09a253704b4e5e732cf1e23c12389485127534b10",
|
|
33
33
|
"swiftVersion": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)"
|
|
34
34
|
},
|
|
35
35
|
"minimumMacOS": "26.0",
|
|
36
|
-
"packageVersion": "0.2.
|
|
36
|
+
"packageVersion": "0.2.6",
|
|
37
37
|
"platform": "darwin",
|
|
38
38
|
"schemaVersion": 1,
|
|
39
|
-
"sourceCommit": "
|
|
39
|
+
"sourceCommit": "1190f23ad793985b6934583c5d8b0957d79a4ce0"
|
|
40
40
|
}
|