github-portfolio-analyzer 1.1.0 → 1.2.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/CHANGELOG.md +13 -0
- package/README.md +122 -33
- package/analyzer.manifest.json +1 -1
- package/bin/github-portfolio-analyzer.js +0 -0
- package/package.json +2 -2
- package/schemas/portfolio-report.schema.json +9 -1
- package/src/cli.js +0 -0
- package/src/commands/analyze.js +19 -14
- package/src/commands/buildPortfolio.js +0 -0
- package/src/commands/ingestIdeas.js +0 -0
- package/src/commands/report.js +0 -0
- package/src/config.js +0 -0
- package/src/core/classification.js +0 -0
- package/src/core/ideas.js +0 -0
- package/src/core/portfolio.js +0 -0
- package/src/core/presentationOverrides.js +0 -0
- package/src/core/report.js +4 -2
- package/src/core/scoring.js +60 -19
- package/src/core/taxonomy.js +42 -2
- package/src/errors.js +0 -0
- package/src/github/client.js +0 -0
- package/src/github/repo-inspection.js +0 -0
- package/src/github/repos.js +0 -0
- package/src/io/csv.js +0 -0
- package/src/io/files.js +0 -0
- package/src/io/markdown.js +0 -0
- package/src/io/report.js +4 -2
- package/src/utils/args.js +0 -0
- package/src/utils/concurrency.js +0 -0
- package/src/utils/header.js +0 -0
- package/src/utils/nextAction.js +0 -0
- package/src/utils/output.js +0 -0
- package/src/utils/retry.js +0 -0
- package/src/utils/slug.js +0 -0
- package/src/utils/time.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,19 @@ All notable changes to this project will be documented in this file.
|
|
|
12
12
|
- Fallback count in analyze summary when structural inspection fails
|
|
13
13
|
- Fatal error messages for missing token, auth failure, and rate limit
|
|
14
14
|
|
|
15
|
+
## [1.2.0] — 2026-04-02
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `inferRepoCategory()` em `taxonomy.js`: inferência de categoria por heurística de nome, descrição e topics — detecta content, learning, template, library, infra, experiment, product; fallback: tooling
|
|
19
|
+
- `CATEGORY_WEIGHTS` em `scoring.js`: pesos de scoring distintos por categoria — `hasLicense` e `hasTests` zerados para content/learning/experiment, baselines altos para experiment (45), learning (35), template (30) e content (25)
|
|
20
|
+
- `category` exposto no output de `buildReportModel` em `report.js` — consumers do report (worker, frontend) agora recebem essa informação
|
|
21
|
+
- `docs/SCORING_MODEL.md`: documentação completa com tabela de pesos, exemplos end-to-end por categoria, e seção para agentes/LLMs
|
|
22
|
+
- Seção de scoring v2 no `AGENT_GUIDE.md`
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `scoreRepository` agora lê `repository.category` para selecionar os pesos corretos; fallback para `tooling` se category ausente ou inválida
|
|
26
|
+
- `sources.category` em `buildRepoTaxonomy` retorna `'inferred'` em vez de `'default'`
|
|
27
|
+
|
|
15
28
|
## [1.0.0] — 2026-03-31
|
|
16
29
|
|
|
17
30
|
### Added
|
package/README.md
CHANGED
|
@@ -98,14 +98,17 @@ Design goals:
|
|
|
98
98
|
|
|
99
99
|
## Installation
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
```bash
|
|
102
|
+
npm install -g github-portfolio-analyzer
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Verify the installation:
|
|
102
106
|
|
|
103
107
|
```bash
|
|
104
|
-
npm install
|
|
105
108
|
github-portfolio-analyzer --version
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
If the global binary is not available
|
|
111
|
+
If the global binary is not available, run directly:
|
|
109
112
|
|
|
110
113
|
```bash
|
|
111
114
|
node bin/github-portfolio-analyzer.js --version
|
|
@@ -497,7 +500,7 @@ Each `portfolio.json.items[]` entry includes:
|
|
|
497
500
|
- `effort`: `xs | s | m | l | xl`
|
|
498
501
|
- `value`: `low | medium | high | very-high`
|
|
499
502
|
- `nextAction`: `"<Verb> <target> — Done when: <measurable condition>"`
|
|
500
|
-
- `taxonomyMeta`: per-field provenance (`default | user | inferred`)
|
|
503
|
+
- `taxonomyMeta`: per-field provenance (`default | user | inferred`). For repositories, `sources.category` is always `user` (when set manually) or `inferred` (heuristic) — never `default`.
|
|
501
504
|
|
|
502
505
|
`inventory.json.items[]` includes the same taxonomy fields and `taxonomyMeta` for repositories.
|
|
503
506
|
|
|
@@ -508,50 +511,133 @@ Each `portfolio.json.items[]` entry includes:
|
|
|
508
511
|
- `meta` (generatedAt, asOfDate, owner, counts)
|
|
509
512
|
- `summary` (state counts, top10 by score, now/next/later/park)
|
|
510
513
|
- `matrix.completionByEffort` (`CL0..CL5` by `xs..xl`)
|
|
511
|
-
- `items[]` with decision fields (`completionLevel`, `effortEstimate`, `priorityBand`, `priorityWhy`)
|
|
514
|
+
- `items[]` with decision fields (`completionLevel`, `effortEstimate`, `priorityBand`, `priorityWhy`, `category`)
|
|
512
515
|
|
|
513
516
|
## Decision Model (Report)
|
|
514
517
|
|
|
518
|
+
Every repository passes through a deterministic scoring pipeline:
|
|
519
|
+
|
|
520
|
+
```mermaid
|
|
521
|
+
flowchart LR
|
|
522
|
+
subgraph top [ ]
|
|
523
|
+
direction LR
|
|
524
|
+
A([repo metadata]) --> B(inferRepoCategory) --> C([category]) --> D(scoreRepository) --> E([score 0–100])
|
|
525
|
+
end
|
|
526
|
+
subgraph mid [ ]
|
|
527
|
+
direction RL
|
|
528
|
+
J(computePriorityBand) <-- I([effort xs–xl]) <-- H(computeEffortEstimate) <-- G([CL 0–5]) <-- F(computeCompletionLevel)
|
|
529
|
+
end
|
|
530
|
+
E --> F
|
|
531
|
+
E -. feeds .-> J
|
|
532
|
+
G -. feeds .-> J
|
|
533
|
+
J --> park([park]) & later([later]) & next([next]) & now([now])
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Score
|
|
537
|
+
|
|
538
|
+
Each repository receives a score from 0 to 100 based on observable signals.
|
|
539
|
+
Signal weights depend on the project's **category**, inferred automatically
|
|
540
|
+
from its name, description, and GitHub topics.
|
|
541
|
+
|
|
542
|
+
| Signal | product | tooling | library | content | learning | infra | experiment | template |
|
|
543
|
+
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
544
|
+
| baseline | — | — | — | **25** | **35** | — | **45** | **30** |
|
|
545
|
+
| pushed (90d) | 25 | 25 | 20 | 25 | 20 | 25 | 20 | 10 |
|
|
546
|
+
| README | 15 | 15 | 20 | 15 | 15 | 20 | 15 | **25** |
|
|
547
|
+
| license | 10 | 10 | **20** | ✗ | ✗ | 10 | ✗ | 10 |
|
|
548
|
+
| tests | 25 | 20 | **25** | ✗ | ✗ | 10 | ✗ | 5 |
|
|
549
|
+
| stars > 1 | 5 | 5 | 10 | 5 | 5 | 5 | 5 | 10 |
|
|
550
|
+
| updated (180d) | 20 | 25 | 5 | **30** | **25** | **30** | 15 | 10 |
|
|
551
|
+
|
|
552
|
+
`✗` = irrelevant for this category (weight 0). `library` penalizes missing
|
|
553
|
+
license most heavily. `experiment` and `learning` skip tests and license entirely.
|
|
554
|
+
|
|
555
|
+
Example — a `content` repo with no license and no tests still scores 95:
|
|
556
|
+
|
|
557
|
+
```
|
|
558
|
+
"prompt-library" category: content
|
|
559
|
+
────────────────────────────────────
|
|
560
|
+
baseline +25
|
|
561
|
+
pushed 10d ago +25
|
|
562
|
+
has README +15
|
|
563
|
+
has license +0 (irrelevant for content)
|
|
564
|
+
has tests +0 (irrelevant for content)
|
|
565
|
+
updated this month +30
|
|
566
|
+
────────────────────────────────────
|
|
567
|
+
score 95
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
See [docs/SCORING_MODEL.md](docs/SCORING_MODEL.md) for the full weight table
|
|
571
|
+
and numeric examples for every category.
|
|
572
|
+
|
|
515
573
|
### Completion Level
|
|
516
574
|
|
|
517
|
-
|
|
518
|
-
- `CL1`: has README
|
|
519
|
-
- `CL2`: has package.json, or non-JS repo with size >= 500 KB
|
|
520
|
-
- `CL3`: CL2 + CI
|
|
521
|
-
- `CL4`: CL3 + tests
|
|
522
|
-
- `CL5`: CL4 + score >= 70
|
|
523
|
-
- Ideas default to `CL0`
|
|
575
|
+
Reflects structural maturity, regardless of category. Ideas always default to CL 0.
|
|
524
576
|
|
|
525
|
-
|
|
577
|
+
| CL | Label | Condition |
|
|
578
|
+
|---|---|---|
|
|
579
|
+
| 0 | Concept only | no README, or `type: idea` |
|
|
580
|
+
| 1 | Documented | has README |
|
|
581
|
+
| 2 | Structured baseline | has `package.json` (or non-JS repo ≥ 500 KB) |
|
|
582
|
+
| 3 | Automated workflow | CL 2 + CI |
|
|
583
|
+
| 4 | Tested workflow | CL 3 + tests |
|
|
584
|
+
| 5 | Production-ready candidate | CL 4 + score ≥ 70 |
|
|
526
585
|
|
|
527
|
-
|
|
528
|
-
If defaulted, infer by size and completion:
|
|
586
|
+
### Effort Estimate
|
|
529
587
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
-
|
|
533
|
-
- `l`: size < 20000 KB
|
|
534
|
-
- `xl`: size >= 20000 KB
|
|
588
|
+
How much work remains to bring a project to its next meaningful state.
|
|
589
|
+
Inferred automatically from repository size and completion level when not set manually.
|
|
590
|
+
`effortEstimate` is a report-only field — it never overwrites the taxonomy `effort`.
|
|
535
591
|
|
|
536
|
-
|
|
592
|
+
| Estimate | Size | CL | What it means |
|
|
593
|
+
|---|---|---|---|
|
|
594
|
+
| `xs` | < 100 KB | ≤ 2 | A few hours. Easy to restart from scratch. |
|
|
595
|
+
| `s` | < 500 KB | ≤ 3 | A day or two. Focused sprint. |
|
|
596
|
+
| `m` | < 5 MB | any | About a week. Needs planning. |
|
|
597
|
+
| `l` | < 20 MB | any | Multiple weeks. Real commitment required. |
|
|
598
|
+
| `xl` | ≥ 20 MB | any | A long-term project. Strategic investment. |
|
|
537
599
|
|
|
538
600
|
### Priority Band
|
|
539
601
|
|
|
540
|
-
|
|
602
|
+
The base score is adjusted by state, completion, and effort to produce a
|
|
603
|
+
final `priorityScore`, which determines the band.
|
|
541
604
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
-
|
|
605
|
+
| Modifier | Condition | Effect |
|
|
606
|
+
|---|---|---|
|
|
607
|
+
| State boost | `active` | +10 |
|
|
608
|
+
| State boost | `stale` | +5 |
|
|
609
|
+
| State penalty | `abandoned` or `archived` | −20 |
|
|
610
|
+
| Quick-win boost | CL 1, 2, or 3 | +10 |
|
|
611
|
+
| Effort penalty | `l` or `xl` | −10 |
|
|
612
|
+
|
|
613
|
+
`priorityScore` has no lower bound — it can go negative.
|
|
614
|
+
|
|
615
|
+
| Band | Range | Meaning |
|
|
616
|
+
|---|---|---|
|
|
617
|
+
| `park` | < 45 | Needs a decision before any investment. Abandoned, low signal, or intentionally paused. |
|
|
618
|
+
| `later` | 45–64 | Viable but not urgent. Can return when backlog has room. |
|
|
619
|
+
| `next` | 65–79 | Strong candidate. High score but large effort, or active with average score. |
|
|
620
|
+
| `now` | ≥ 80 | High confidence. Active project, good score, low effort — or manually pinned. |
|
|
548
621
|
|
|
549
|
-
|
|
622
|
+
Example — modifiers can push a `park`-bound project below zero:
|
|
550
623
|
|
|
551
|
-
|
|
552
|
-
-
|
|
553
|
-
|
|
554
|
-
|
|
624
|
+
```
|
|
625
|
+
"old-monolith" category: product
|
|
626
|
+
──────────────────────────────────
|
|
627
|
+
baseline 0
|
|
628
|
+
pushed 400d ago +0 (> 90 days)
|
|
629
|
+
has README +15
|
|
630
|
+
has license +10
|
|
631
|
+
no tests +0
|
|
632
|
+
updated 200d ago +0 (> 180 days)
|
|
633
|
+
──────────────────────────────────
|
|
634
|
+
score 25
|
|
635
|
+
|
|
636
|
+
state=abandoned −20
|
|
637
|
+
effort=xl −10
|
|
638
|
+
──────────────────────────────────
|
|
639
|
+
priorityScore −5 → park
|
|
640
|
+
```
|
|
555
641
|
|
|
556
642
|
## Determinism and Time Rules
|
|
557
643
|
|
|
@@ -654,10 +740,13 @@ npm test
|
|
|
654
740
|
Coverage includes:
|
|
655
741
|
|
|
656
742
|
- activity/maturity/scoring boundaries
|
|
743
|
+
- category inference from repository name, description, and topics
|
|
744
|
+
- category-aware scoring weights and category preservation for user-specified values
|
|
657
745
|
- taxonomy presence and provenance behavior
|
|
658
746
|
- `nextAction` validation and normalization
|
|
659
747
|
- portfolio merge determinism
|
|
660
748
|
- report completion logic, priority mapping, and deterministic model generation
|
|
749
|
+
- `category` propagation to report items and all summary bands
|
|
661
750
|
|
|
662
751
|
## Troubleshooting
|
|
663
752
|
|
package/analyzer.manifest.json
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-portfolio-analyzer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CLI tool to analyze GitHub repos and portfolio ideas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/paulo-raoni/github-portfolio-analyzer.git"
|
|
20
|
+
"url": "git+https://github.com/paulo-raoni/github-portfolio-analyzer.git"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"bin/",
|
|
@@ -176,7 +176,11 @@
|
|
|
176
176
|
"type": "array",
|
|
177
177
|
"items": { "type": "string" }
|
|
178
178
|
},
|
|
179
|
-
"nextAction": { "type": "string" }
|
|
179
|
+
"nextAction": { "type": "string" },
|
|
180
|
+
"category": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"enum": ["product", "tooling", "library", "learning", "content", "infra", "experiment", "template"]
|
|
183
|
+
}
|
|
180
184
|
}
|
|
181
185
|
},
|
|
182
186
|
"item": {
|
|
@@ -244,6 +248,10 @@
|
|
|
244
248
|
},
|
|
245
249
|
"htmlUrl": { "type": "string" },
|
|
246
250
|
"homepage": { "type": "string" },
|
|
251
|
+
"category": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"enum": ["product", "tooling", "library", "learning", "content", "infra", "experiment", "template"]
|
|
254
|
+
},
|
|
247
255
|
"presentationState": {
|
|
248
256
|
"type": "string",
|
|
249
257
|
"enum": ["featured", "complete", "in-progress", "salvageable", "learning", "archived", "hidden"]
|
package/src/cli.js
CHANGED
|
File without changes
|
package/src/commands/analyze.js
CHANGED
|
@@ -73,17 +73,22 @@ export async function runAnalyzeCommand(options = {}) {
|
|
|
73
73
|
const structuralHealth = await inspectRepositoryStructure(github, normalized);
|
|
74
74
|
const activity = classifyActivity(normalized._pushedAt, asOfDate);
|
|
75
75
|
const maturity = classifyMaturity(normalized.sizeKb);
|
|
76
|
-
const { score, scoreBreakdown } = scoreRepository(
|
|
77
|
-
{ ...normalized, structuralHealth, pushedAt: normalized._pushedAt, updatedAt: normalized._updatedAt },
|
|
78
|
-
asOfDate
|
|
79
|
-
);
|
|
80
76
|
const taxonomy = buildRepoTaxonomy({
|
|
81
77
|
...normalized,
|
|
82
78
|
structuralHealth,
|
|
83
79
|
activity,
|
|
84
|
-
maturity
|
|
85
|
-
score
|
|
80
|
+
maturity
|
|
86
81
|
});
|
|
82
|
+
const { score, scoreBreakdown } = scoreRepository(
|
|
83
|
+
{
|
|
84
|
+
...normalized,
|
|
85
|
+
structuralHealth,
|
|
86
|
+
pushedAt: normalized._pushedAt,
|
|
87
|
+
updatedAt: normalized._updatedAt,
|
|
88
|
+
category: taxonomy.category
|
|
89
|
+
},
|
|
90
|
+
asOfDate
|
|
91
|
+
);
|
|
87
92
|
|
|
88
93
|
return stripInternalFields({
|
|
89
94
|
...normalized,
|
|
@@ -106,22 +111,22 @@ export async function runAnalyzeCommand(options = {}) {
|
|
|
106
111
|
hasTests: false,
|
|
107
112
|
hasCi: false
|
|
108
113
|
};
|
|
114
|
+
const taxonomy = buildRepoTaxonomy({
|
|
115
|
+
...normalized,
|
|
116
|
+
structuralHealth: fallbackStructuralHealth,
|
|
117
|
+
activity,
|
|
118
|
+
maturity
|
|
119
|
+
});
|
|
109
120
|
const { score, scoreBreakdown } = scoreRepository(
|
|
110
121
|
{
|
|
111
122
|
...normalized,
|
|
112
123
|
structuralHealth: fallbackStructuralHealth,
|
|
113
124
|
pushedAt: normalized._pushedAt,
|
|
114
|
-
updatedAt: normalized._updatedAt
|
|
125
|
+
updatedAt: normalized._updatedAt,
|
|
126
|
+
category: taxonomy.category
|
|
115
127
|
},
|
|
116
128
|
asOfDate
|
|
117
129
|
);
|
|
118
|
-
const taxonomy = buildRepoTaxonomy({
|
|
119
|
-
...normalized,
|
|
120
|
-
structuralHealth: fallbackStructuralHealth,
|
|
121
|
-
activity,
|
|
122
|
-
maturity,
|
|
123
|
-
score
|
|
124
|
-
});
|
|
125
130
|
|
|
126
131
|
return stripInternalFields({
|
|
127
132
|
...normalized,
|
|
File without changes
|
|
File without changes
|
package/src/commands/report.js
CHANGED
|
File without changes
|
package/src/config.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/core/ideas.js
CHANGED
|
File without changes
|
package/src/core/portfolio.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/core/report.js
CHANGED
|
@@ -200,7 +200,8 @@ export function buildReportModel(portfolioData, inventoryData = null, options =
|
|
|
200
200
|
...(item.language != null ? { language: item.language } : {}),
|
|
201
201
|
...(Array.isArray(item.topics) && item.topics.length > 0 ? { topics: item.topics } : {}),
|
|
202
202
|
...(item.htmlUrl != null ? { htmlUrl: item.htmlUrl } : {}),
|
|
203
|
-
...(item.homepage != null ? { homepage: item.homepage } : {})
|
|
203
|
+
...(item.homepage != null ? { homepage: item.homepage } : {}),
|
|
204
|
+
...(item.category != null ? { category: item.category } : {})
|
|
204
205
|
};
|
|
205
206
|
});
|
|
206
207
|
|
|
@@ -362,7 +363,8 @@ function toSummaryItem(item) {
|
|
|
362
363
|
...(item.priorityTag ? { priorityTag: item.priorityTag } : {}),
|
|
363
364
|
priorityOverrides: item.priorityOverrides,
|
|
364
365
|
priorityWhy: item.priorityWhy,
|
|
365
|
-
nextAction: item.nextAction
|
|
366
|
+
nextAction: item.nextAction,
|
|
367
|
+
...(item.category != null ? { category: item.category } : {})
|
|
366
368
|
};
|
|
367
369
|
}
|
|
368
370
|
|
package/src/core/scoring.js
CHANGED
|
@@ -1,8 +1,49 @@
|
|
|
1
1
|
import { daysSince } from './classification.js';
|
|
2
2
|
|
|
3
|
+
const CATEGORY_WEIGHTS = {
|
|
4
|
+
product: {
|
|
5
|
+
pushedWithin90Days: 25, hasReadme: 15, hasLicense: 10,
|
|
6
|
+
hasTests: 25, starsOverOne: 5, updatedWithin180Days: 20, baseline: 0
|
|
7
|
+
},
|
|
8
|
+
tooling: {
|
|
9
|
+
pushedWithin90Days: 25, hasReadme: 15, hasLicense: 10,
|
|
10
|
+
hasTests: 20, starsOverOne: 5, updatedWithin180Days: 25, baseline: 0
|
|
11
|
+
},
|
|
12
|
+
library: {
|
|
13
|
+
pushedWithin90Days: 20, hasReadme: 20, hasLicense: 20,
|
|
14
|
+
hasTests: 25, starsOverOne: 10, updatedWithin180Days: 5, baseline: 0
|
|
15
|
+
},
|
|
16
|
+
content: {
|
|
17
|
+
pushedWithin90Days: 25, hasReadme: 15, hasLicense: 0,
|
|
18
|
+
hasTests: 0, starsOverOne: 5, updatedWithin180Days: 30, baseline: 25
|
|
19
|
+
},
|
|
20
|
+
learning: {
|
|
21
|
+
pushedWithin90Days: 20, hasReadme: 15, hasLicense: 0,
|
|
22
|
+
hasTests: 0, starsOverOne: 5, updatedWithin180Days: 25, baseline: 35
|
|
23
|
+
},
|
|
24
|
+
infra: {
|
|
25
|
+
pushedWithin90Days: 25, hasReadme: 20, hasLicense: 10,
|
|
26
|
+
hasTests: 10, starsOverOne: 5, updatedWithin180Days: 30, baseline: 0
|
|
27
|
+
},
|
|
28
|
+
experiment: {
|
|
29
|
+
pushedWithin90Days: 20, hasReadme: 15, hasLicense: 0,
|
|
30
|
+
hasTests: 0, starsOverOne: 5, updatedWithin180Days: 15, baseline: 45
|
|
31
|
+
},
|
|
32
|
+
template: {
|
|
33
|
+
pushedWithin90Days: 10, hasReadme: 25, hasLicense: 10,
|
|
34
|
+
hasTests: 5, starsOverOne: 10, updatedWithin180Days: 10, baseline: 30
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DEFAULT_WEIGHTS = CATEGORY_WEIGHTS.tooling;
|
|
39
|
+
|
|
3
40
|
export function scoreRepository(repository, asOfDate) {
|
|
4
|
-
|
|
41
|
+
const category = repository.category ?? 'tooling';
|
|
42
|
+
const weights = CATEGORY_WEIGHTS[category] ?? DEFAULT_WEIGHTS;
|
|
43
|
+
|
|
44
|
+
let score = weights.baseline ?? 0;
|
|
5
45
|
const breakdown = {
|
|
46
|
+
baseline: weights.baseline ?? 0,
|
|
6
47
|
pushedWithin90Days: 0,
|
|
7
48
|
hasReadme: 0,
|
|
8
49
|
hasLicense: 0,
|
|
@@ -11,34 +52,34 @@ export function scoreRepository(repository, asOfDate) {
|
|
|
11
52
|
updatedWithin180Days: 0
|
|
12
53
|
};
|
|
13
54
|
|
|
14
|
-
if (daysSince(repository.pushedAt, asOfDate) <= 90) {
|
|
15
|
-
score +=
|
|
16
|
-
breakdown.pushedWithin90Days =
|
|
55
|
+
if (weights.pushedWithin90Days > 0 && daysSince(repository.pushedAt, asOfDate) <= 90) {
|
|
56
|
+
score += weights.pushedWithin90Days;
|
|
57
|
+
breakdown.pushedWithin90Days = weights.pushedWithin90Days;
|
|
17
58
|
}
|
|
18
59
|
|
|
19
|
-
if (repository.structuralHealth?.hasReadme) {
|
|
20
|
-
score +=
|
|
21
|
-
breakdown.hasReadme =
|
|
60
|
+
if (weights.hasReadme > 0 && repository.structuralHealth?.hasReadme) {
|
|
61
|
+
score += weights.hasReadme;
|
|
62
|
+
breakdown.hasReadme = weights.hasReadme;
|
|
22
63
|
}
|
|
23
64
|
|
|
24
|
-
if (repository.structuralHealth?.hasLicense) {
|
|
25
|
-
score +=
|
|
26
|
-
breakdown.hasLicense =
|
|
65
|
+
if (weights.hasLicense > 0 && repository.structuralHealth?.hasLicense) {
|
|
66
|
+
score += weights.hasLicense;
|
|
67
|
+
breakdown.hasLicense = weights.hasLicense;
|
|
27
68
|
}
|
|
28
69
|
|
|
29
|
-
if (repository.structuralHealth?.hasTests) {
|
|
30
|
-
score +=
|
|
31
|
-
breakdown.hasTests =
|
|
70
|
+
if (weights.hasTests > 0 && repository.structuralHealth?.hasTests) {
|
|
71
|
+
score += weights.hasTests;
|
|
72
|
+
breakdown.hasTests = weights.hasTests;
|
|
32
73
|
}
|
|
33
74
|
|
|
34
|
-
if ((repository.stargazersCount ?? 0) > 1) {
|
|
35
|
-
score +=
|
|
36
|
-
breakdown.starsOverOne =
|
|
75
|
+
if (weights.starsOverOne > 0 && (repository.stargazersCount ?? 0) > 1) {
|
|
76
|
+
score += weights.starsOverOne;
|
|
77
|
+
breakdown.starsOverOne = weights.starsOverOne;
|
|
37
78
|
}
|
|
38
79
|
|
|
39
|
-
if (daysSince(repository.updatedAt, asOfDate) <= 180) {
|
|
40
|
-
score +=
|
|
41
|
-
breakdown.updatedWithin180Days =
|
|
80
|
+
if (weights.updatedWithin180Days > 0 && daysSince(repository.updatedAt, asOfDate) <= 180) {
|
|
81
|
+
score += weights.updatedWithin180Days;
|
|
82
|
+
breakdown.updatedWithin180Days = weights.updatedWithin180Days;
|
|
42
83
|
}
|
|
43
84
|
|
|
44
85
|
return {
|
package/src/core/taxonomy.js
CHANGED
|
@@ -1,17 +1,57 @@
|
|
|
1
1
|
import { formatNextAction, normalizeNextAction } from '../utils/nextAction.js';
|
|
2
2
|
|
|
3
|
+
function inferRepoCategory(repository) {
|
|
4
|
+
const name = String(repository.name ?? '').toLowerCase();
|
|
5
|
+
const desc = String(repository.description ?? '').toLowerCase();
|
|
6
|
+
const topics = Array.isArray(repository.topics)
|
|
7
|
+
? repository.topics.map((topic) => String(topic).toLowerCase())
|
|
8
|
+
: [];
|
|
9
|
+
const all = [name, desc, ...topics].join(' ');
|
|
10
|
+
|
|
11
|
+
if (/\b(prompt|note|notes|snippet|snippets|cheatsheet|doc|docs|documentation|knowledge|wiki|resource|resources|writing|content|guide|guides|cookbook)\b/.test(all)) {
|
|
12
|
+
return 'content';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (/\b(learn|learning|study|exercise|exercises|course|tutorial|tutorials|practice|training|bootcamp|challenge|challenges|kata)\b/.test(all)) {
|
|
16
|
+
return 'learning';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (/\b(template|templates|boilerplate|starter|scaffold|skeleton|seed|base|init)\b/.test(all)) {
|
|
20
|
+
return 'template';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (/\b(lib|library|sdk|package|npm|module|plugin|extension|addon|util|utils|helper|helpers)\b/.test(all)) {
|
|
24
|
+
return 'library';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (/\b(infra|infrastructure|docker|kubernetes|k8s|ci|cd|pipeline|deploy|devops|ansible|terraform|nginx|proxy)\b/.test(all)) {
|
|
28
|
+
return 'infra';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (/\b(poc|proof|experiment|spike|demo|prototype|sandbox|playground|try|trying)\b/.test(all)) {
|
|
32
|
+
return 'experiment';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (/\b(app|application|system|platform|service|api|backend|frontend|web|mobile|dashboard|portal|saas)\b/.test(all)) {
|
|
36
|
+
return 'product';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 'tooling';
|
|
40
|
+
}
|
|
41
|
+
|
|
3
42
|
export function buildRepoTaxonomy(repository) {
|
|
4
43
|
const activityState = repository.activity;
|
|
5
44
|
const state = repository.archived ? 'archived' : normalizeState(activityState, 'active');
|
|
6
45
|
|
|
7
|
-
const
|
|
46
|
+
const userCategory = normalizeCategory(repository.category);
|
|
47
|
+
const category = userCategory ?? inferRepoCategory(repository);
|
|
8
48
|
const strategy = 'maintenance';
|
|
9
49
|
const effort = 'm';
|
|
10
50
|
const value = 'medium';
|
|
11
51
|
const nextAction = defaultRepoNextAction(state);
|
|
12
52
|
|
|
13
53
|
const sources = {
|
|
14
|
-
category: '
|
|
54
|
+
category: userCategory ? 'user' : 'inferred',
|
|
15
55
|
state: repository.archived ? 'inferred' : 'inferred',
|
|
16
56
|
strategy: 'default',
|
|
17
57
|
effort: 'default',
|
package/src/errors.js
CHANGED
|
File without changes
|
package/src/github/client.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/github/repos.js
CHANGED
|
File without changes
|
package/src/io/csv.js
CHANGED
|
File without changes
|
package/src/io/files.js
CHANGED
|
File without changes
|
package/src/io/markdown.js
CHANGED
|
File without changes
|
package/src/io/report.js
CHANGED
|
@@ -108,7 +108,8 @@ function renderAsciiBandSection(title, items) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
items.slice(0, 5).forEach((item, index) => {
|
|
111
|
-
|
|
111
|
+
const categoryPrefix = item.category == null ? '' : `[${item.category}] `;
|
|
112
|
+
lines.push(`${index + 1}) ${categoryPrefix}${item.slug} — Score ${item.score} — CL${item.completionLevel} — Effort ${item.effortEstimate} — State ${item.state}`);
|
|
112
113
|
lines.push(` Why: ${item.priorityWhy?.join('; ') ?? ''}`);
|
|
113
114
|
lines.push(` Next: ${item.nextAction}`);
|
|
114
115
|
});
|
|
@@ -129,7 +130,8 @@ function renderMarkdownBandSection(title, items) {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
items.slice(0, 5).forEach((item, index) => {
|
|
132
|
-
|
|
133
|
+
const categoryPrefix = item.category == null ? '' : `\`${item.category}\` `;
|
|
134
|
+
lines.push(`${index + 1}. ${categoryPrefix}**${item.slug}** — Score ${item.score} — CL${item.completionLevel} — Effort ${item.effortEstimate} — State ${item.state}`);
|
|
133
135
|
lines.push(` - Why: ${item.priorityWhy?.join('; ') ?? ''}`);
|
|
134
136
|
lines.push(` - Next: ${item.nextAction}`);
|
|
135
137
|
});
|
package/src/utils/args.js
CHANGED
|
File without changes
|
package/src/utils/concurrency.js
CHANGED
|
File without changes
|
package/src/utils/header.js
CHANGED
|
File without changes
|
package/src/utils/nextAction.js
CHANGED
|
File without changes
|
package/src/utils/output.js
CHANGED
|
File without changes
|
package/src/utils/retry.js
CHANGED
|
File without changes
|
package/src/utils/slug.js
CHANGED
|
File without changes
|
package/src/utils/time.js
CHANGED
|
File without changes
|