@enhansome/core 1.10.1 → 1.11.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.
@@ -0,0 +1,3 @@
1
+ import type { JsonOutput } from './markdown.js';
2
+ export type FirstSeen = (repoId: number) => string;
3
+ export declare function firstSeenFor(previous: JsonOutput | undefined, now: Date): FirstSeen;
@@ -0,0 +1,20 @@
1
+ export function firstSeenFor(previous, now) {
2
+ const index = new Map();
3
+ if (previous) {
4
+ for (const section of previous.items) {
5
+ collect(section.items, index);
6
+ }
7
+ }
8
+ return repoId => index.get(repoId) ?? now.toISOString();
9
+ }
10
+ function collect(nodes, index) {
11
+ for (const node of nodes) {
12
+ if (node.node_type === 'item' && node.first_seen) {
13
+ const existing = index.get(node.repo_info.id);
14
+ if (existing === undefined || node.first_seen < existing) {
15
+ index.set(node.repo_info.id, node.first_seen);
16
+ }
17
+ }
18
+ collect(node.children, index);
19
+ }
20
+ }
package/dist/github.d.ts CHANGED
@@ -8,8 +8,10 @@ export type GithubClient = InstanceType<typeof HardenedOctokit>;
8
8
  export interface RepoInfoDetails {
9
9
  archived: boolean;
10
10
  description: null | string;
11
+ homepage: null | string;
11
12
  id: number;
12
13
  language: null | string;
14
+ license: null | string;
13
15
  open_issues_count: number;
14
16
  owner: string;
15
17
  pushed_at: null | string;
package/dist/github.js CHANGED
@@ -72,8 +72,10 @@ export async function getRepoInfo(octokit, owner, repo) {
72
72
  return {
73
73
  archived: data.archived,
74
74
  description: data.description ?? null,
75
+ homepage: data.homepage || null,
75
76
  id: data.id,
76
77
  language: data.language,
78
+ license: data.license?.spdx_id ?? null,
77
79
  open_issues_count: data.open_issues_count,
78
80
  owner: data.owner.login,
79
81
  pushed_at: data.pushed_at,
@@ -18,18 +18,24 @@ export interface SortOptions {
18
18
  }
19
19
  export interface RepoInfo {
20
20
  archived: boolean;
21
+ description: null | string;
22
+ homepage: null | string;
21
23
  id: number;
22
24
  language: null | string;
25
+ license: null | string;
23
26
  last_commit: null | string;
27
+ open_issues: number;
24
28
  owner: string;
25
29
  repo: string;
26
30
  stars: number;
31
+ topics: string[];
27
32
  }
28
33
  /** The sole crossing from the API shape to the emitted one, so both agree on the renames. */
29
34
  export declare function toRepoInfo(details: RepoInfoDetails): RepoInfo;
30
35
  export interface JsonItem {
31
36
  children: JsonNode[];
32
37
  description: null | string;
38
+ first_seen?: string;
33
39
  node_type: 'item';
34
40
  repo_info: RepoInfo;
35
41
  title: string;
@@ -54,7 +60,7 @@ export interface JsonSection {
54
60
  items: JsonNode[];
55
61
  title: string;
56
62
  }
57
- export declare function processMarkdownContent(originalContent: string, token: string, replacements?: ReplacementRule[], sortOptions?: SortOptions, relativeLinkPrefix?: string, enhancedRepository?: string, enhancedRepositoryDescription?: string, originalRepositorySha?: string, originalRepositoryInfo?: null | RepoInfoDetails, now?: Date, log?: Logger): Promise<{
63
+ export declare function processMarkdownContent(originalContent: string, token: string, replacements?: ReplacementRule[], sortOptions?: SortOptions, relativeLinkPrefix?: string, enhancedRepository?: string, enhancedRepositoryDescription?: string, originalRepositorySha?: string, originalRepositoryInfo?: null | RepoInfoDetails, previousJson?: JsonOutput, now?: Date, log?: Logger): Promise<{
58
64
  finalContent: string;
59
65
  jsonData: JsonOutput;
60
66
  }>;
package/dist/markdown.js CHANGED
@@ -4,18 +4,24 @@ import remarkParse from 'remark-parse';
4
4
  import remarkStringify from 'remark-stringify';
5
5
  import { unified } from 'unified';
6
6
  import { visit } from 'unist-util-visit';
7
+ import { firstSeenFor } from './first-seen.js';
7
8
  import { formatRequestError, getRepoInfo as fetchRepoInfo, makeOctokit, parseGitHubUrl, } from './github.js';
8
9
  import { consoleLog } from './logger.js';
9
10
  /** The sole crossing from the API shape to the emitted one, so both agree on the renames. */
10
11
  export function toRepoInfo(details) {
11
12
  return {
12
13
  archived: details.archived,
14
+ description: details.description,
15
+ homepage: details.homepage,
13
16
  id: details.id,
14
17
  language: details.language,
18
+ license: details.license,
15
19
  last_commit: details.pushed_at,
20
+ open_issues: details.open_issues_count,
16
21
  owner: details.owner,
17
22
  repo: details.repo,
18
23
  stars: details.stargazers_count,
24
+ topics: details.topics,
19
25
  };
20
26
  }
21
27
  // The lookup's single throttled client coordinates rate limits across the whole
@@ -79,7 +85,7 @@ async function fetchTargetData(urls, repos) {
79
85
  log.info(`Target fetch: ${repoInfoMap.size}/${urls.size} repo-info ok in ${Date.now() - fetchStart}ms (concurrency ${FETCH_CONCURRENCY}).`);
80
86
  return repoInfoMap;
81
87
  }
82
- export async function processMarkdownContent(originalContent, token, replacements = [], sortOptions = { by: '', minLinks: 2 }, relativeLinkPrefix = '', enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, now = new Date(), log = consoleLog) {
88
+ export async function processMarkdownContent(originalContent, token, replacements = [], sortOptions = { by: '', minLinks: 2 }, relativeLinkPrefix = '', enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, previousJson, now = new Date(), log = consoleLog) {
83
89
  const repos = createRepoInfoLookup(token, log);
84
90
  const brandingEnabled = replacements.some(rule => rule.type === 'branding');
85
91
  const contentAfterReplacements = applyTextReplacements(originalContent, replacements.filter(rule => rule.type !== 'branding'), log);
@@ -88,7 +94,8 @@ export async function processMarkdownContent(originalContent, token, replacement
88
94
  normalizeGitHubUrls(tree);
89
95
  const githubUrls = collectGitHubLinks(tree);
90
96
  const repoInfoMap = await fetchTargetData(githubUrls, repos);
91
- const { sections, title: rawTitle, titleHeadingIndex, } = processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo);
97
+ const firstSeen = firstSeenFor(previousJson, now);
98
+ const { sections, title: rawTitle, titleHeadingIndex, } = processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo, firstSeen);
92
99
  // Single source of truth for the document title: brand it once and use the
93
100
  // same value for the markdown H1 and metadata.title (parity).
94
101
  const title = brandingEnabled ? brandTitle(rawTitle) : rawTitle;
@@ -439,7 +446,7 @@ function splitEntryText(inlines) {
439
446
  // group — never a `repo_info`, that's the identity-borrowing bug; no own link
440
447
  // and no children is a non-GitHub leaf, kept in markdown, dropped from JSON.
441
448
  // TODO(future): preserve non-GitHub leaves in a separate shape.
442
- function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
449
+ function emitEntryNodes(githubUrl, repoInfo, text, childrenJson, firstSeen) {
443
450
  if (githubUrl && repoInfo) {
444
451
  return [
445
452
  {
@@ -448,6 +455,7 @@ function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
448
455
  description: text.description || null,
449
456
  children: childrenJson,
450
457
  repo_info: toRepoInfo(repoInfo),
458
+ first_seen: firstSeen(repoInfo.id),
451
459
  },
452
460
  ];
453
461
  }
@@ -466,7 +474,7 @@ function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
466
474
  }
467
475
  return [];
468
476
  }
469
- function processListRecursively(listNode, repoInfoMap, sortOptions, isNested = false,
477
+ function processListRecursively(listNode, repoInfoMap, sortOptions, firstSeen, isNested = false,
470
478
  // The caller's section-scope gate decision (sectionGatePasses). Absent for
471
479
  // nested lists (emitted under their parent regardless) and for top-level
472
480
  // lists with no open container (preamble), which gate per list.
@@ -493,8 +501,8 @@ sectionGateOpen) {
493
501
  // regardless of the gate: the top-level call already gated the section.
494
502
  const nestedContent = itemNode.children.filter((child) => child.type === 'list' || child.type === 'table');
495
503
  const childrenJson = nestedContent.flatMap(child => child.type === 'list'
496
- ? processListRecursively(child, repoInfoMap, sortOptions, true)
497
- : processTableRows(child, repoInfoMap, true));
504
+ ? processListRecursively(child, repoInfoMap, sortOptions, firstSeen, true)
505
+ : processTableRows(child, repoInfoMap, true, firstSeen));
498
506
  // Title/description split on the FIRST paragraph only — a paper-list
499
507
  // entry's identity link may live in a later paragraph (findOwnGitHubLink)
500
508
  // while its title text stays the leading one.
@@ -508,7 +516,7 @@ sectionGateOpen) {
508
516
  // The shared title fallbacks (an inline-code link label carries no text
509
517
  // nodes, so the split alone can leave an empty title).
510
518
  entryText.title = entryTitle(entryText.title, ownLink, repoInfo);
511
- const emitted = emitEntryNodes(githubUrl, repoInfo, entryText, childrenJson);
519
+ const emitted = emitEntryNodes(githubUrl, repoInfo, entryText, childrenJson, firstSeen);
512
520
  entries.push({ emitted, node: itemNode, repoInfo });
513
521
  }
514
522
  if (sortOptions.by) {
@@ -536,18 +544,7 @@ function entryTitle(base, ownLink, repoInfo) {
536
544
  }
537
545
  return repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : base;
538
546
  }
539
- // Table rows are entries under the nearest open container. The common shape —
540
- // scala's `[name](repo) | description`, spec tables with the link in a later
541
- // column — holds ONE repo per row: the title is the first cell's text (the
542
- // own link's text, then the repo name, when the first cell is empty), the
543
- // description the remaining cells' text (badge images carry no text nodes, so
544
- // they never pollute it). Card grids pin several repos per row, each cell its
545
- // own card, so those emit one entry per link-bearing cell. A linked row above
546
- // the delimiter is content like any other (grid tables have no label header;
547
- // pure-label header rows carry no links and emit nothing). Rows stay in source
548
- // order — unlike the unranked lists the product sorts, a table's row order is
549
- // part of its meaning.
550
- function processTableRows(tableNode, repoInfoMap, gateOpen) {
547
+ function processTableRows(tableNode, repoInfoMap, gateOpen, firstSeen) {
551
548
  if (!gateOpen) {
552
549
  return [];
553
550
  }
@@ -589,7 +586,7 @@ function processTableRows(tableNode, repoInfoMap, gateOpen) {
589
586
  items.push(...emitEntryNodes(ownLink.url, repoInfo, {
590
587
  title: entryTitle(titleText.title, ownLink, repoInfo),
591
588
  description,
592
- }, []));
589
+ }, [], firstSeen));
593
590
  continue;
594
591
  }
595
592
  const emittedUrls = new Set();
@@ -603,34 +600,15 @@ function processTableRows(tableNode, repoInfoMap, gateOpen) {
603
600
  items.push(...emitEntryNodes(ownLink.url, repoInfo, {
604
601
  title: entryTitle(cellText.title, ownLink, repoInfo),
605
602
  description: cellText.description,
606
- }, []));
603
+ }, [], firstSeen));
607
604
  }
608
605
  }
609
606
  return items;
610
607
  }
611
- // A top-level paragraph can BE an entry, not only feed container prose. Two
612
- // corpus families qualify (progress/empty-tree-parses.md, step 4): a GitHub
613
- // link LEADING the paragraph behind a short entry label, or the paragraph
614
- // ending in a tag cluster whose GitHub link carries the identity — the
615
- // paper-list shape whose first link points at the paper and a [Code]/[Github]
616
- // tag at the end, name/author lines ending in that tag, and dated lines
617
- // ("… [Github] 4 Feb 2023"). Prose that mentions a repo ("Please see
618
- // CONTRIBUTING", "See also [repo]", intro text ending in a bare repo URL) is
619
- // neither and stays description. Calibrated on the 2,293-README corpus plus
620
- // the fixture fleet, not intuition: an entry label is a TAG (empty, CJK/emoji,
621
- // bracketed, or colon-terminated) — never bare English prose — and the
622
- // identity link must carry text, so the ubiquitous image-only awesome badge
623
- // is not an entry.
624
608
  const ENTRY_LABEL_MAX = 15;
625
609
  const ENTRY_TRAILING_MAX = 3;
626
610
  const TAG_LINK_TEXT = /^[\[\]()*\s:_-]*(?:source\s+code|code|github|repo|source|src|project|paper|page|web|site|home|official|notebook|demo|data|docs|implementation|arxiv)\b[\[\]()*\s:_-]*$/i;
627
611
  const URL_LINK_TEXT = /^https?:\/\/\S+$/i;
628
- // A title that names nothing — the degenerate families every entry source can
629
- // emit: a pure number/punctuation run (a table's rank or year column, "15."
630
- // / "2023" / "2025-05" / "-"), a URL (a link whose label is the URL itself,
631
- // scheme or scheme-less `github.com/…`), or a bare tag word ("GitHub",
632
- // "Source code" — best-of's generated lines). CJK labels are letters
633
- // (`\p{L}`) and stay titles.
634
612
  const URL_TITLE = /^(?:[a-z][a-z0-9+.-]*:\/\/|www\.)\S+$|^github\.com\/\S+$/i;
635
613
  function isDegenerateTitle(title) {
636
614
  const trimmed = title.trim();
@@ -639,9 +617,6 @@ function isDegenerateTitle(title) {
639
617
  TAG_LINK_TEXT.test(trimmed) ||
640
618
  !/[\p{L}]/u.test(trimmed)));
641
619
  }
642
- // A link label that can serve as a title. The degenerate shapes are excluded
643
- // twice over — a URL label stays a URL, and a tag label names the link's role
644
- // ("[Github](repo)"), not the repo.
645
620
  function isMeaningfulLinkText(text) {
646
621
  return text !== '' && !isDegenerateTitle(text);
647
622
  }
@@ -757,13 +732,13 @@ function blockquoteEntries(blockquote) {
757
732
  // The shared entry emission for the non-list sources (paragraph entries,
758
733
  // blockquote cards): resolve, split, title-fallback — the same decisions the
759
734
  // list and table paths make through the same helpers.
760
- function entryNodesFor(ownLink, inlines, repoInfoMap) {
735
+ function entryNodesFor(ownLink, inlines, repoInfoMap, firstSeen) {
761
736
  const repoInfo = repoInfoMap.get(ownLink.url) ?? null;
762
737
  const entryText = splitEntryText(inlines);
763
738
  return emitEntryNodes(ownLink.url, repoInfo, {
764
739
  title: entryTitle(entryText.title, ownLink, repoInfo),
765
740
  description: entryText.description,
766
- }, []);
741
+ }, [], firstSeen);
767
742
  }
768
743
  // The <details><summary>…</summary> collapsible-section idiom: the summary
769
744
  // text delimits structure like a heading would (java's generated README,
@@ -894,7 +869,7 @@ function applyBrandingToTree(tree, title, titleHeadingIndex) {
894
869
  tree.children.unshift(heading);
895
870
  }
896
871
  }
897
- function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
872
+ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo, firstSeen) {
898
873
  // Remember the title H1's index so branding replaces the exact same node;
899
874
  // scope matches branding/section-building so they can't drift apart.
900
875
  const titleHeadingIndex = findTitleHeadingIndex(tree);
@@ -925,7 +900,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
925
900
  openImplicitSection(stack, atIndex);
926
901
  }
927
902
  if (gateForSection(stack[0])) {
928
- stack[stack.length - 1].children.push(...entryNodesFor(ownLink, inlines, repoInfoMap));
903
+ stack[stack.length - 1].children.push(...entryNodesFor(ownLink, inlines, repoInfoMap, firstSeen));
929
904
  }
930
905
  };
931
906
  // A standalone entry restating the repo of an open item container (crypto's
@@ -952,7 +927,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
952
927
  const promotedEntry = headingEntry && getInlineText(headingEntry.link.children)
953
928
  ? headingEntry
954
929
  : null;
955
- closeContainers(stack, node.depth, sectionRecords, !!promotedEntry);
930
+ closeContainers(stack, node.depth, sectionRecords, firstSeen, !!promotedEntry);
956
931
  openContainer(stack, node, i, sectionDepth, promotedEntry, headingEntry);
957
932
  }
958
933
  else if (node.type === 'paragraph') {
@@ -1004,7 +979,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
1004
979
  // section, which keeps collecting after the collapsible block.
1005
980
  const summaryTitle = detailsSummaryTitle(node.value);
1006
981
  if (summaryTitle) {
1007
- closeInnermostDetails(stack, sectionRecords);
982
+ closeInnermostDetails(stack, sectionRecords, firstSeen);
1008
983
  // Container depths never decrease going up the stack. When the open
1009
984
  // containers sit deeper than sectionDepth (a mid-document H1 defines
1010
985
  // sectionDepth while the content sections run deeper), the
@@ -1025,7 +1000,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
1025
1000
  });
1026
1001
  }
1027
1002
  else if (DETAILS_CLOSE.test(node.value)) {
1028
- closeInnermostDetails(stack, sectionRecords);
1003
+ closeInnermostDetails(stack, sectionRecords, firstSeen);
1029
1004
  }
1030
1005
  }
1031
1006
  else if (node.type === 'list') {
@@ -1039,7 +1014,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
1039
1014
  // Every list inside the open container contributes items — a section is
1040
1015
  // not closed by its first list — and the minLinks gate is decided per
1041
1016
  // section, against the whole section subtree.
1042
- const items = processListRecursively(node, repoInfoMap, sortOptions, false, gateForSection(stack[0]));
1017
+ const items = processListRecursively(node, repoInfoMap, sortOptions, firstSeen, false, gateForSection(stack[0]));
1043
1018
  stack[stack.length - 1].children.push(...items);
1044
1019
  }
1045
1020
  else if (node.type === 'table') {
@@ -1049,11 +1024,11 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
1049
1024
  if (stack.length === 0) {
1050
1025
  openImplicitSection(stack, i);
1051
1026
  }
1052
- const items = processTableRows(node, repoInfoMap, gateForSection(stack[0]));
1027
+ const items = processTableRows(node, repoInfoMap, gateForSection(stack[0]), firstSeen);
1053
1028
  stack[stack.length - 1].children.push(...items);
1054
1029
  }
1055
1030
  }
1056
- closeContainers(stack, 0, sectionRecords);
1031
+ closeContainers(stack, 0, sectionRecords, firstSeen);
1057
1032
  const sections = sectionRecords
1058
1033
  .sort((a, b) => a.headingIndex - b.headingIndex)
1059
1034
  .map(record => record.section);
@@ -1314,7 +1289,7 @@ function sectionGatePasses(tree, titleSlotIndex, sectionDepth, repoInfoMap, sort
1314
1289
  // item-bearing nodes) is dropped; an item always survives, it IS the content.
1315
1290
  // Non-section containers always have an open parent (stack invariant), and
1316
1291
  // kind === 'item' exactly when repoInfo is set.
1317
- function finalizeContainer(container, stack, sectionRecords) {
1292
+ function finalizeContainer(container, stack, sectionRecords, firstSeen) {
1318
1293
  if (container.children.length === 0 && container.kind !== 'item') {
1319
1294
  return;
1320
1295
  }
@@ -1334,6 +1309,7 @@ function finalizeContainer(container, stack, sectionRecords) {
1334
1309
  node_type: 'item',
1335
1310
  repo_info: toRepoInfo(container.repoInfo),
1336
1311
  title: container.title,
1312
+ first_seen: firstSeen(container.repoInfo.id),
1337
1313
  });
1338
1314
  }
1339
1315
  else {
@@ -1351,13 +1327,13 @@ function finalizeContainer(container, stack, sectionRecords) {
1351
1327
  // group/item always has a parent to land in. stopAtSynthesized: the pop an
1352
1328
  // entry heading triggers must stop at the synthesized section wrapping its
1353
1329
  // run — the next entry heading of the run lands back inside it.
1354
- function closeContainers(stack, depth, sectionRecords, stopAtSynthesized = false) {
1330
+ function closeContainers(stack, depth, sectionRecords, firstSeen, stopAtSynthesized = false) {
1355
1331
  while (stack.length > 0 &&
1356
1332
  stack[stack.length - 1].headingDepth >= depth) {
1357
1333
  if (stopAtSynthesized && stack[stack.length - 1].openedBySynthesis) {
1358
1334
  break;
1359
1335
  }
1360
- finalizeContainer(stack.pop(), stack, sectionRecords);
1336
+ finalizeContainer(stack.pop(), stack, sectionRecords, firstSeen);
1361
1337
  }
1362
1338
  }
1363
1339
  // Ends the innermost open details-section and everything opened inside it,
@@ -1365,7 +1341,7 @@ function closeContainers(stack, depth, sectionRecords, stopAtSynthesized = false
1365
1341
  // details boundary never ends its parent section, so content after the
1366
1342
  // collapsible block keeps collecting under it. A stray </details> (no
1367
1343
  // details-section open) is a no-op.
1368
- function closeInnermostDetails(stack, sectionRecords) {
1344
+ function closeInnermostDetails(stack, sectionRecords, firstSeen) {
1369
1345
  let detailsIndex = -1;
1370
1346
  for (let s = stack.length - 1; s >= 0; s--) {
1371
1347
  if (stack[s].openedByDetails) {
@@ -1377,7 +1353,7 @@ function closeInnermostDetails(stack, sectionRecords) {
1377
1353
  return;
1378
1354
  }
1379
1355
  while (stack.length > detailsIndex) {
1380
- finalizeContainer(stack.pop(), stack, sectionRecords);
1356
+ finalizeContainer(stack.pop(), stack, sectionRecords, firstSeen);
1381
1357
  }
1382
1358
  }
1383
1359
  function serializeAst(tree, originalContent) {
@@ -6,11 +6,11 @@ export interface EnhanceOptions {
6
6
  disableBranding?: boolean;
7
7
  enhancedRepository?: string;
8
8
  enhancedRepositoryDescription?: string;
9
- /** Defaults to the console sink; pass your own (e.g. an Actions workflow-command sink) to route diagnostics. */
10
9
  log?: Logger;
11
10
  now?: Date;
12
11
  originalRepositoryInfo?: null | RepoInfoDetails;
13
12
  originalRepositorySha?: string;
13
+ previousJson?: JsonOutput;
14
14
  relativeLinkPrefix?: string;
15
15
  replacements?: ReplacementRule[];
16
16
  sortBy?: '' | 'last_commit' | 'stars';
@@ -1,6 +1,6 @@
1
1
  import { processMarkdownContent, } from './markdown.js';
2
2
  export async function enhance(options) {
3
- const { content, disableBranding = false, log, now = new Date(), originalRepositoryInfo, originalRepositorySha, relativeLinkPrefix = '', replacements = [], sortBy = '', enhancedRepository, enhancedRepositoryDescription, token, } = options;
3
+ const { content, disableBranding = false, log, now = new Date(), originalRepositoryInfo, originalRepositorySha, previousJson, relativeLinkPrefix = '', replacements = [], sortBy = '', enhancedRepository, enhancedRepositoryDescription, token, } = options;
4
4
  // Branding is an internal rule prepended to the caller's own; build a fresh
5
5
  // array so the caller's `replacements` is never mutated.
6
6
  const branding = { type: 'branding' };
@@ -9,7 +9,7 @@ export async function enhance(options) {
9
9
  by: sortBy,
10
10
  minLinks: 2,
11
11
  };
12
- const { finalContent, jsonData } = await processMarkdownContent(content, token, rules, sortOptions, relativeLinkPrefix, enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, now, log);
12
+ const { finalContent, jsonData } = await processMarkdownContent(content, token, rules, sortOptions, relativeLinkPrefix, enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, previousJson, now, log);
13
13
  return {
14
14
  finalContent,
15
15
  jsonData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enhansome/core",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "Library core for enhansome — enhance markdown with GitHub star counts.",
5
5
  "repository": {
6
6
  "type": "git",