@enhansome/core 1.9.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/github.d.ts CHANGED
@@ -77,11 +77,12 @@ export interface MakeOctokitOptions {
77
77
  export declare function makeOctokit(token: string, { log, maxRetries, maxWaitSeconds, throttle, }?: MakeOctokitOptions): GithubClient;
78
78
  export declare function getRepoInfo(octokit: GithubClient, owner: string, repo: string): Promise<RepoInfoDetails>;
79
79
  /**
80
- * The repo's numeric GitHub id, for the JSON metadata — consumers key stable
81
- * node ids on it. Returns null (logged) instead of failing the run: the id is
82
- * metadata, not a gate; consumers fall back when it is missing.
80
+ * The repo's full info, for the JSON metadata — the numeric id consumers key
81
+ * stable node ids on, plus the stars/language/last_commit the root's own
82
+ * metadata row is built from. Returns null (logged) instead of failing the
83
+ * run: metadata is not a gate; consumers fall back when it is missing.
83
84
  */
84
- export declare function getRepoId(octokit: GithubClient, owner: string, repo: string): Promise<null | number>;
85
+ export declare function getRepoInfoOrNull(octokit: GithubClient, owner: string, repo: string): Promise<null | RepoInfoDetails>;
85
86
  export declare function getReadme(octokit: GithubClient, owner: string, repo: string, format?: 'html' | 'raw'): Promise<string>;
86
87
  /** Root file/directory names — the compile-manifest gate reads them to tell a
87
88
  * directory of resources from a repo that IS the deliverable. A `path: ''`
package/dist/github.js CHANGED
@@ -83,16 +83,17 @@ export async function getRepoInfo(octokit, owner, repo) {
83
83
  };
84
84
  }
85
85
  /**
86
- * The repo's numeric GitHub id, for the JSON metadata — consumers key stable
87
- * node ids on it. Returns null (logged) instead of failing the run: the id is
88
- * metadata, not a gate; consumers fall back when it is missing.
86
+ * The repo's full info, for the JSON metadata — the numeric id consumers key
87
+ * stable node ids on, plus the stars/language/last_commit the root's own
88
+ * metadata row is built from. Returns null (logged) instead of failing the
89
+ * run: metadata is not a gate; consumers fall back when it is missing.
89
90
  */
90
- export async function getRepoId(octokit, owner, repo) {
91
+ export async function getRepoInfoOrNull(octokit, owner, repo) {
91
92
  try {
92
- return (await getRepoInfo(octokit, owner, repo)).id;
93
+ return await getRepoInfo(octokit, owner, repo);
93
94
  }
94
95
  catch (error) {
95
- octokit.log.error(`Failed to fetch repo id for ${owner}/${repo}: ${formatRequestError(error)}`);
96
+ octokit.log.error(`Failed to fetch repo info for ${owner}/${repo}: ${formatRequestError(error)}`);
96
97
  return null;
97
98
  }
98
99
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { formatRequestError, getLatestCommitSha, getReadme, getRepoId, getRepoInfo, getRootEntryNames, makeOctokit, parseGitHubUrl, parseOwnerRepo, } from './github.js';
1
+ export { formatRequestError, getLatestCommitSha, getReadme, getRepoInfo, getRepoInfoOrNull, getRootEntryNames, makeOctokit, parseGitHubUrl, parseOwnerRepo, } from './github.js';
2
2
  export type { GithubClient, MakeOctokitOptions, RepoIdentifier, RepoInfoDetails, ThrottleOptions, } from './github.js';
3
3
  export type { Logger } from './logger.js';
4
4
  export { consoleLog, silentLog } from './logger.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { formatRequestError, getLatestCommitSha, getReadme, getRepoId, getRepoInfo, getRootEntryNames, makeOctokit, parseGitHubUrl, parseOwnerRepo, } from './github.js';
1
+ export { formatRequestError, getLatestCommitSha, getReadme, getRepoInfo, getRepoInfoOrNull, getRootEntryNames, makeOctokit, parseGitHubUrl, parseOwnerRepo, } from './github.js';
2
2
  export { consoleLog, silentLog } from './logger.js';
3
3
  export { toRepoInfo } from './markdown.js';
4
4
  export { enhance } from './orchestrator.js';
@@ -45,8 +45,7 @@ export interface JsonMetadata {
45
45
  enhanced_repository: null | string;
46
46
  enhanced_repository_description: null | string;
47
47
  last_updated: string;
48
- original_repository: string;
49
- original_repository_id: null | number;
48
+ original_repository_info: null | RepoInfo;
50
49
  original_repository_sha: null | string;
51
50
  title: string;
52
51
  }
@@ -55,7 +54,7 @@ export interface JsonSection {
55
54
  items: JsonNode[];
56
55
  title: string;
57
56
  }
58
- export declare function processMarkdownContent(originalContent: string, token: string, replacements: ReplacementRule[] | undefined, sortOptions: SortOptions | undefined, originalRepository: string, relativeLinkPrefix?: string, enhancedRepository?: string, enhancedRepositoryDescription?: string, originalRepositorySha?: string, originalRepositoryId?: number, now?: Date, log?: Logger): Promise<{
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<{
59
58
  finalContent: string;
60
59
  jsonData: JsonOutput;
61
60
  }>;
package/dist/markdown.js CHANGED
@@ -79,7 +79,7 @@ async function fetchTargetData(urls, repos) {
79
79
  log.info(`Target fetch: ${repoInfoMap.size}/${urls.size} repo-info ok in ${Date.now() - fetchStart}ms (concurrency ${FETCH_CONCURRENCY}).`);
80
80
  return repoInfoMap;
81
81
  }
82
- export async function processMarkdownContent(originalContent, token, replacements = [], sortOptions = { by: '', minLinks: 2 }, originalRepository, relativeLinkPrefix = '', enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryId, now = new Date(), log = consoleLog) {
82
+ export async function processMarkdownContent(originalContent, token, replacements = [], sortOptions = { by: '', minLinks: 2 }, relativeLinkPrefix = '', enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, now = new Date(), log = consoleLog) {
83
83
  const repos = createRepoInfoLookup(token, log);
84
84
  const brandingEnabled = replacements.some(rule => rule.type === 'branding');
85
85
  const contentAfterReplacements = applyTextReplacements(originalContent, replacements.filter(rule => rule.type !== 'branding'), log);
@@ -88,9 +88,7 @@ export async function processMarkdownContent(originalContent, token, replacement
88
88
  normalizeGitHubUrls(tree);
89
89
  const githubUrls = collectGitHubLinks(tree);
90
90
  const repoInfoMap = await fetchTargetData(githubUrls, repos);
91
- // The title derives from the *source* repository (originalRepository), never
92
- // the enhanced/mirror repo — otherwise the org name doubles into the title.
93
- const { sections, title: rawTitle, titleHeadingIndex, } = processTree(tree, repoInfoMap, sortOptions, originalRepository);
91
+ const { sections, title: rawTitle, titleHeadingIndex, } = processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo);
94
92
  // Single source of truth for the document title: brand it once and use the
95
93
  // same value for the markdown H1 and metadata.title (parity).
96
94
  const title = brandingEnabled ? brandTitle(rawTitle) : rawTitle;
@@ -99,8 +97,9 @@ export async function processMarkdownContent(originalContent, token, replacement
99
97
  }
100
98
  const metadata = {
101
99
  last_updated: now.toISOString(),
102
- original_repository: originalRepository.trim(),
103
- original_repository_id: originalRepositoryId ?? null,
100
+ original_repository_info: originalRepositoryInfo
101
+ ? toRepoInfo(originalRepositoryInfo)
102
+ : null,
104
103
  original_repository_sha: (originalRepositorySha?.trim() ?? '') || null,
105
104
  enhanced_repository: (enhancedRepository?.trim() ?? '') || null,
106
105
  enhanced_repository_description: (enhancedRepositoryDescription?.trim() ?? '') || null,
@@ -519,14 +518,23 @@ sectionGateOpen) {
519
518
  listNode.children = entries.map(entry => entry.node);
520
519
  return entries.flatMap(entry => entry.emitted);
521
520
  }
522
- // Title with the fallbacks every entry source shares when the split yields no
523
- // text (e.g. an image-only card link): the own link's text, then the repo
524
- // name.
521
+ // Title with the fallbacks every entry source shares: the split's own text
522
+ // when it names something; else the own link's label when meaningful; else —
523
+ // for a degenerate base (rank/year cell, URL label, tag word) owner/name, for
524
+ // an absent base (an image-only link) the repo name. A degenerate base with
525
+ // no live repo link (a group) keeps its text: nothing better exists.
525
526
  function entryTitle(base, ownLink, repoInfo) {
526
- return (base ||
527
- (ownLink ? getInlineText(ownLink.children) : '') ||
528
- repoInfo?.repo ||
529
- '');
527
+ if (base && !isDegenerateTitle(base)) {
528
+ return base;
529
+ }
530
+ const linkText = ownLink ? getInlineText(ownLink.children) : '';
531
+ if (isMeaningfulLinkText(linkText)) {
532
+ return linkText;
533
+ }
534
+ if (base === '') {
535
+ return repoInfo?.repo ?? '';
536
+ }
537
+ return repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : base;
530
538
  }
531
539
  // Table rows are entries under the nearest open container. The common shape —
532
540
  // scala's `[name](repo) | description`, spec tables with the link in a later
@@ -560,15 +568,27 @@ function processTableRows(tableNode, repoInfoMap, gateOpen) {
560
568
  }
561
569
  const repoInfo = repoInfoMap.get(ownLink.url) ?? null;
562
570
  const firstCellText = splitEntryText(row.children[0]?.children ?? []);
563
- const restCellsText = row.children
564
- .slice(1)
565
- .map(cell => getNodeText(cell))
566
- .filter(Boolean);
571
+ // A degenerate first cell (a rank/year column) moves the title source to
572
+ // the link's own cell: that cell no longer feeds the description — its
573
+ // label became the title — and the degenerate cell drops as noise.
574
+ const linkCellIndex = isDegenerateTitle(firstCellText.title)
575
+ ? cellLinks.findIndex(link => link === ownLink)
576
+ : -1;
577
+ const titleText = linkCellIndex === -1
578
+ ? firstCellText
579
+ : splitEntryText(row.children[linkCellIndex].children);
580
+ const description = [
581
+ titleText.description,
582
+ ...row.children
583
+ .slice(1)
584
+ .filter((_cell, index) => index + 1 !== linkCellIndex)
585
+ .map(cell => getNodeText(cell)),
586
+ ]
587
+ .join(' ')
588
+ .trim();
567
589
  items.push(...emitEntryNodes(ownLink.url, repoInfo, {
568
- title: entryTitle(firstCellText.title, ownLink, repoInfo),
569
- description: [firstCellText.description, ...restCellsText]
570
- .join(' ')
571
- .trim(),
590
+ title: entryTitle(titleText.title, ownLink, repoInfo),
591
+ description,
572
592
  }, []));
573
593
  continue;
574
594
  }
@@ -605,6 +625,26 @@ const ENTRY_LABEL_MAX = 15;
605
625
  const ENTRY_TRAILING_MAX = 3;
606
626
  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;
607
627
  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
+ const URL_TITLE = /^(?:[a-z][a-z0-9+.-]*:\/\/|www\.)\S+$|^github\.com\/\S+$/i;
635
+ function isDegenerateTitle(title) {
636
+ const trimmed = title.trim();
637
+ return (!!trimmed &&
638
+ (URL_TITLE.test(trimmed) ||
639
+ TAG_LINK_TEXT.test(trimmed) ||
640
+ !/[\p{L}]/u.test(trimmed)));
641
+ }
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
+ function isMeaningfulLinkText(text) {
646
+ return text !== '' && !isDegenerateTitle(text);
647
+ }
608
648
  // Dated entry lines end their tag cluster with a publication date ("4 Feb
609
649
  // 2023", "19 Apr 2022", "2023") — a real corpus family (dated paper/tutorial
610
650
  // lists), not a sentence continuation.
@@ -771,22 +811,6 @@ const INVALID_TITLE_PATTERNS = [
771
811
  /^video$/i,
772
812
  /^science/i,
773
813
  ];
774
- function repoNameFromIdentifier(identifier) {
775
- const trimmed = identifier.trim();
776
- if (!trimmed) {
777
- return '';
778
- }
779
- if (/^https?:\/\//i.test(trimmed)) {
780
- const parts = trimmed
781
- .replace(/\.git$/i, '')
782
- .replace(/[?#].*$/, '')
783
- .split('/')
784
- .filter(Boolean);
785
- return parts[parts.length - 1] ?? '';
786
- }
787
- const slashIndex = trimmed.lastIndexOf('/');
788
- return slashIndex === -1 ? trimmed : trimmed.slice(slashIndex + 1);
789
- }
790
814
  function formatRepoNameAsTitle(repoName) {
791
815
  const cleaned = repoName.replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim();
792
816
  if (/^awesome\s+/i.test(cleaned)) {
@@ -870,7 +894,7 @@ function applyBrandingToTree(tree, title, titleHeadingIndex) {
870
894
  tree.children.unshift(heading);
871
895
  }
872
896
  }
873
- function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
897
+ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
874
898
  // Remember the title H1's index so branding replaces the exact same node;
875
899
  // scope matches branding/section-building so they can't drift apart.
876
900
  const titleHeadingIndex = findTitleHeadingIndex(tree);
@@ -881,11 +905,8 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
881
905
  // Derive a subject from the *source* repository name when no valid H1 is
882
906
  // present. Using the source — not the enhanced/mirror repo — keeps the org
883
907
  // name out of the title.
884
- if (documentTitle === '' && originalRepository) {
885
- const repoName = repoNameFromIdentifier(originalRepository);
886
- if (repoName) {
887
- documentTitle = formatRepoNameAsTitle(repoName);
888
- }
908
+ if (documentTitle === '' && originalRepositoryInfo) {
909
+ documentTitle = formatRepoNameAsTitle(originalRepositoryInfo.repo);
889
910
  }
890
911
  const sectionDepth = findSectionDepth(tree, titleSlotIndex);
891
912
  const gateForSection = sectionGatePasses(tree, titleSlotIndex, sectionDepth, repoInfoMap, sortOptions);
@@ -1,4 +1,5 @@
1
1
  import { Logger } from './logger.js';
2
+ import type { RepoInfoDetails } from './github.js';
2
3
  import { JsonOutput, ReplacementRule } from './markdown.js';
3
4
  export interface EnhanceOptions {
4
5
  content: string;
@@ -8,11 +9,9 @@ export interface EnhanceOptions {
8
9
  /** Defaults to the console sink; pass your own (e.g. an Actions workflow-command sink) to route diagnostics. */
9
10
  log?: Logger;
10
11
  now?: Date;
11
- originalRepository: string;
12
- originalRepositoryId?: number;
12
+ originalRepositoryInfo?: null | RepoInfoDetails;
13
13
  originalRepositorySha?: string;
14
14
  relativeLinkPrefix?: string;
15
- /** Text substitutions applied to the source before it is parsed. */
16
15
  replacements?: ReplacementRule[];
17
16
  sortBy?: '' | 'last_commit' | 'stars';
18
17
  token: string;
@@ -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(), originalRepository, originalRepositoryId, originalRepositorySha, relativeLinkPrefix = '', replacements = [], sortBy = '', enhancedRepository, enhancedRepositoryDescription, token, } = options;
3
+ const { content, disableBranding = false, log, now = new Date(), originalRepositoryInfo, originalRepositorySha, 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, originalRepository, relativeLinkPrefix, enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryId, now, log);
12
+ const { finalContent, jsonData } = await processMarkdownContent(content, token, rules, sortOptions, relativeLinkPrefix, enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, 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.9.0",
3
+ "version": "1.10.1",
4
4
  "description": "Library core for enhansome — enhance markdown with GitHub star counts.",
5
5
  "repository": {
6
6
  "type": "git",