@tekyzinc/gsd-t 2.39.12 → 2.45.11
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 +17 -9
- package/bin/desktop.ini +2 -0
- package/bin/global-sync-manager.js +350 -0
- package/bin/gsd-t.js +592 -2
- package/bin/metrics-collector.js +167 -0
- package/bin/metrics-rollup.js +200 -0
- package/bin/patch-lifecycle.js +195 -0
- package/bin/rule-engine.js +160 -0
- package/commands/desktop.ini +2 -0
- package/commands/gsd-t-complete-milestone.md +212 -5
- package/commands/gsd-t-debug.md +28 -2
- package/commands/gsd-t-execute.md +269 -52
- package/commands/gsd-t-feature.md +18 -0
- package/commands/gsd-t-gap-analysis.md +17 -0
- package/commands/gsd-t-help.md +25 -10
- package/commands/gsd-t-integrate.md +35 -7
- package/commands/gsd-t-metrics.md +143 -0
- package/commands/gsd-t-partition.md +17 -0
- package/commands/gsd-t-plan.md +49 -2
- package/commands/gsd-t-quick.md +27 -3
- package/commands/gsd-t-scan.md +44 -0
- package/commands/gsd-t-status.md +78 -0
- package/commands/gsd-t-test-sync.md +2 -2
- package/commands/gsd-t-verify.md +140 -9
- package/commands/gsd-t-visualize.md +11 -1
- package/commands/gsd-t-wave.md +34 -19
- package/docs/GSD-T-README.md +9 -6
- package/docs/architecture.md +84 -2
- package/docs/ci-examples/desktop.ini +2 -0
- package/docs/ci-examples/github-actions.yml +104 -0
- package/docs/ci-examples/gitlab-ci.yml +116 -0
- package/docs/desktop.ini +2 -0
- package/docs/infrastructure.md +87 -1
- package/docs/prd-graph-engine.md +2 -2
- package/docs/prd-gsd2-hybrid.md +258 -135
- package/docs/requirements.md +63 -2
- package/examples/.gsd-t/contracts/desktop.ini +2 -0
- package/examples/.gsd-t/desktop.ini +2 -0
- package/examples/.gsd-t/domains/desktop.ini +2 -0
- package/examples/.gsd-t/domains/example-domain/desktop.ini +2 -0
- package/examples/desktop.ini +2 -0
- package/examples/rules/.gitkeep +0 -0
- package/package.json +40 -40
- package/scripts/desktop.ini +2 -0
- package/scripts/gsd-t-dashboard-server.js +19 -2
- package/scripts/gsd-t-dashboard.html +63 -0
- package/scripts/gsd-t-event-writer.js +1 -0
- package/templates/CLAUDE-global.md +30 -9
- package/templates/desktop.ini +2 -0
package/README.md
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
|
|
4
4
|
|
|
5
|
-
**
|
|
6
|
-
**
|
|
5
|
+
**Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
|
|
6
|
+
**Safe parallel execution** — worktree isolation gives each domain agent its own filesystem; sequential atomic merges prevent conflicts.
|
|
7
7
|
**Maintains test coverage** — automatically keeps tests aligned with code changes.
|
|
8
8
|
**Catches downstream effects** — analyzes impact before changes break things.
|
|
9
9
|
**Protects existing work** — destructive action guard prevents schema drops, architecture replacements, and data loss without explicit approval.
|
|
10
10
|
**Visualizes execution in real time** — live browser dashboard renders agent hierarchy, tool activity, and phase progression from the event stream.
|
|
11
11
|
**Generates visual scan reports** — every `/gsd-t-scan` produces a self-contained HTML report with 6 live architectural diagrams, a tech debt register, and domain health scores; optional DOCX/PDF export via `--export docx|pdf`.
|
|
12
|
+
**Self-learning rule engine** — declarative rules in rules.jsonl detect failure patterns from task metrics. Candidate patches progress through a 5-stage lifecycle (candidate, applied, measured, promoted, graduated) with >55% improvement gates before becoming permanent methodology artifacts.
|
|
13
|
+
**Cross-project learning** — proven rules propagate to `~/.claude/metrics/` and sync across all registered projects via `update-all`. Rules validated in 3+ projects become universal; 5+ projects qualify for npm distribution. Cross-project signal comparison and global ELO rankings available via `gsd-t-metrics --cross-project` and `gsd-t-status`.
|
|
12
14
|
|
|
13
15
|
---
|
|
14
16
|
|
|
@@ -20,7 +22,7 @@ A methodology for reliable, parallelizable development using Claude Code with op
|
|
|
20
22
|
npx @tekyzinc/gsd-t install
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
This installs 45 GSD-T commands +
|
|
25
|
+
This installs 45 GSD-T commands + 5 utility commands (50 total) to `~/.claude/commands/` and the global CLAUDE.md to `~/.claude/CLAUDE.md`. Works on Windows, Mac, and Linux.
|
|
24
26
|
|
|
25
27
|
### Start Using It
|
|
26
28
|
|
|
@@ -76,6 +78,11 @@ npx @tekyzinc/gsd-t status # Check installation + version
|
|
|
76
78
|
npx @tekyzinc/gsd-t doctor # Diagnose common issues
|
|
77
79
|
npx @tekyzinc/gsd-t changelog # Open changelog in the browser
|
|
78
80
|
npx @tekyzinc/gsd-t uninstall # Remove commands (keeps project files)
|
|
81
|
+
|
|
82
|
+
# Headless mode (CI/CD)
|
|
83
|
+
gsd-t headless verify --json --timeout=1200 # Run verify non-interactively
|
|
84
|
+
gsd-t headless query status # Get project state (no LLM, <100ms)
|
|
85
|
+
gsd-t headless query domains # List domains (no LLM)
|
|
79
86
|
```
|
|
80
87
|
|
|
81
88
|
### Updating
|
|
@@ -129,26 +136,27 @@ This will replace changed command files, back up your CLAUDE.md if customized, a
|
|
|
129
136
|
| `/user:gsd-t-milestone` | Define new milestone | Manual |
|
|
130
137
|
| `/user:gsd-t-partition` | Decompose into domains + contracts | In wave |
|
|
131
138
|
| `/user:gsd-t-discuss` | Multi-perspective design exploration | In wave |
|
|
132
|
-
| `/user:gsd-t-plan` | Create atomic task lists per domain | In wave |
|
|
139
|
+
| `/user:gsd-t-plan` | Create atomic task lists per domain (tasks auto-split to fit one context window) | In wave |
|
|
133
140
|
| `/user:gsd-t-impact` | Analyze downstream effects | In wave |
|
|
134
|
-
| `/user:gsd-t-execute` | Run tasks
|
|
141
|
+
| `/user:gsd-t-execute` | Run tasks — task-level fresh dispatch, worktree isolation, adaptive replanning | In wave |
|
|
135
142
|
| `/user:gsd-t-test-sync` | Sync tests with code changes | In wave |
|
|
136
143
|
| `/user:gsd-t-qa` | QA agent — test generation, execution, gap reporting | Auto-spawned |
|
|
137
144
|
| `/user:gsd-t-integrate` | Wire domains together | In wave |
|
|
138
|
-
| `/user:gsd-t-verify` | Run quality gates | In wave |
|
|
139
|
-
| `/user:gsd-t-complete-milestone` | Archive + git tag | In wave |
|
|
145
|
+
| `/user:gsd-t-verify` | Run quality gates + goal-backward behavior verification | In wave |
|
|
146
|
+
| `/user:gsd-t-complete-milestone` | Archive + git tag (goal-backward gate required) | In wave |
|
|
140
147
|
|
|
141
148
|
### Automation & Utilities
|
|
142
149
|
|
|
143
150
|
| Command | Purpose | Auto |
|
|
144
151
|
|---------|---------|------|
|
|
145
152
|
| `/user:gsd-t-wave` | Full cycle, auto-advances all phases | Manual |
|
|
146
|
-
| `/user:gsd-t-status` | Cross-domain progress view | Manual |
|
|
153
|
+
| `/user:gsd-t-status` | Cross-domain progress view with token breakdown by domain/task/phase | Manual |
|
|
147
154
|
| `/user:gsd-t-resume` | Restore context, continue | Manual |
|
|
148
155
|
| `/user:gsd-t-quick` | Fast task with GSD-T guarantees | Manual |
|
|
149
156
|
| `/user:gsd-t-reflect` | Generate retrospective from event stream, propose memory updates | Manual |
|
|
150
157
|
| `/user:gsd-t-visualize` | Launch browser dashboard — SSE server + React Flow agent visualization | Manual |
|
|
151
158
|
| `/user:gsd-t-debug` | Systematic debugging with state | Manual |
|
|
159
|
+
| `/user:gsd-t-metrics` | View task telemetry, process ELO, signal distribution, domain health, and cross-project comparison (`--cross-project`) | Manual |
|
|
152
160
|
| `/user:gsd-t-health` | Validate .gsd-t/ structure, optionally repair | Manual |
|
|
153
161
|
| `/user:gsd-t-pause` | Save exact position for reliable resume | Manual |
|
|
154
162
|
| `/user:gsd-t-log` | Sync progress Decision Log with recent git activity | Manual |
|
|
@@ -306,7 +314,7 @@ get-stuff-done-teams/
|
|
|
306
314
|
├── LICENSE
|
|
307
315
|
├── bin/
|
|
308
316
|
│ └── gsd-t.js # CLI installer
|
|
309
|
-
├── commands/ #
|
|
317
|
+
├── commands/ # 50 slash commands
|
|
310
318
|
│ ├── gsd-t-*.md # 44 GSD-T workflow commands
|
|
311
319
|
│ ├── gsd.md # GSD-T smart router
|
|
312
320
|
│ ├── branch.md # Git branch helper
|
package/bin/desktop.ini
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T Global Sync Manager — Cross-project metrics and rule propagation
|
|
5
|
+
*
|
|
6
|
+
* Reads local project metrics and writes global aggregated files to
|
|
7
|
+
* ~/.claude/metrics/. Provides APIs for global rollup aggregation,
|
|
8
|
+
* global rule storage, signal distribution comparison, and universal
|
|
9
|
+
* rule promotion logic.
|
|
10
|
+
*
|
|
11
|
+
* Zero external dependencies (Node.js built-ins only).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const os = require("os");
|
|
17
|
+
|
|
18
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const GLOBAL_METRICS_DIR = path.join(os.homedir(), ".claude", "metrics");
|
|
21
|
+
const GLOBAL_RULES_FILE = path.join(GLOBAL_METRICS_DIR, "global-rules.jsonl");
|
|
22
|
+
const GLOBAL_ROLLUP_FILE = path.join(GLOBAL_METRICS_DIR, "global-rollup.jsonl");
|
|
23
|
+
const GLOBAL_SIGNAL_FILE = path.join(GLOBAL_METRICS_DIR, "global-signal-distributions.jsonl");
|
|
24
|
+
|
|
25
|
+
const ELO_START = 1000;
|
|
26
|
+
const ELO_K = 32;
|
|
27
|
+
|
|
28
|
+
const UNIVERSAL_THRESHOLD = 3;
|
|
29
|
+
const NPM_CANDIDATE_THRESHOLD = 5;
|
|
30
|
+
|
|
31
|
+
// ── Exports ──────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
// Task 1: Core JSONL read/write + global rule management
|
|
35
|
+
readGlobalRules, writeGlobalRule,
|
|
36
|
+
readGlobalRollups, writeGlobalRollup,
|
|
37
|
+
readGlobalSignalDistributions, writeGlobalSignalDistribution,
|
|
38
|
+
// Task 2: Signal distribution comparison
|
|
39
|
+
compareSignalDistributions, getDomainTypeComparison,
|
|
40
|
+
// Task 3: Universal rule promotion + global ELO
|
|
41
|
+
checkUniversalPromotion, getGlobalELO, getProjectRankings,
|
|
42
|
+
// Internal (exposed for testing)
|
|
43
|
+
_setGlobalDir,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ── Overridable paths (for test isolation) ──────────────────────────────────
|
|
47
|
+
|
|
48
|
+
let _globalDir = GLOBAL_METRICS_DIR;
|
|
49
|
+
let _rulesFile = GLOBAL_RULES_FILE;
|
|
50
|
+
let _rollupFile = GLOBAL_ROLLUP_FILE;
|
|
51
|
+
let _signalFile = GLOBAL_SIGNAL_FILE;
|
|
52
|
+
|
|
53
|
+
/** @param {string} dir Override global metrics directory (for testing) */
|
|
54
|
+
function _setGlobalDir(dir) {
|
|
55
|
+
_globalDir = dir;
|
|
56
|
+
_rulesFile = path.join(dir, "global-rules.jsonl");
|
|
57
|
+
_rollupFile = path.join(dir, "global-rollup.jsonl");
|
|
58
|
+
_signalFile = path.join(dir, "global-signal-distributions.jsonl");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Task 1: Core JSONL read/write ───────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/** @returns {object[]} All global rules */
|
|
64
|
+
function readGlobalRules() {
|
|
65
|
+
return loadJsonl(_rulesFile);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Write or update a global rule. Dedup via trigger fingerprint.
|
|
70
|
+
* If rule already exists (same trigger), increments promotion_count and
|
|
71
|
+
* updates propagated_to. Otherwise appends new rule.
|
|
72
|
+
* @param {object} rule
|
|
73
|
+
* @returns {object} The written/updated rule
|
|
74
|
+
*/
|
|
75
|
+
function writeGlobalRule(rule) {
|
|
76
|
+
ensureDir(_globalDir);
|
|
77
|
+
const rules = loadJsonl(_rulesFile);
|
|
78
|
+
const fingerprint = JSON.stringify(rule.original_rule && rule.original_rule.trigger
|
|
79
|
+
? rule.original_rule.trigger : (rule.trigger || {}));
|
|
80
|
+
const existing = rules.find((r) => {
|
|
81
|
+
const fp = JSON.stringify(r.original_rule && r.original_rule.trigger
|
|
82
|
+
? r.original_rule.trigger : (r.trigger || {}));
|
|
83
|
+
return fp === fingerprint;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (existing) {
|
|
87
|
+
existing.promotion_count = (existing.promotion_count || 1) + 1;
|
|
88
|
+
const srcDir = rule.source_project_dir || rule.source_project || "";
|
|
89
|
+
if (srcDir && !existing.propagated_to.includes(srcDir)) {
|
|
90
|
+
existing.propagated_to.push(srcDir);
|
|
91
|
+
}
|
|
92
|
+
// Auto-check universal/npm thresholds
|
|
93
|
+
if (existing.promotion_count >= UNIVERSAL_THRESHOLD) existing.is_universal = true;
|
|
94
|
+
if (existing.promotion_count >= NPM_CANDIDATE_THRESHOLD) existing.is_npm_candidate = true;
|
|
95
|
+
atomicWriteJsonl(_rulesFile, rules);
|
|
96
|
+
return existing;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// New rule — assign global_id
|
|
100
|
+
const nextId = rules.length > 0
|
|
101
|
+
? Math.max(...rules.map((r) => parseInt((r.global_id || "grule-0").replace("grule-", ""), 10))) + 1
|
|
102
|
+
: 1;
|
|
103
|
+
const newRule = {
|
|
104
|
+
id: rule.id || null,
|
|
105
|
+
global_id: `grule-${String(nextId).padStart(3, "0")}`,
|
|
106
|
+
source_project: rule.source_project || getProjectName(),
|
|
107
|
+
source_project_dir: rule.source_project_dir || process.cwd(),
|
|
108
|
+
original_rule: rule.original_rule || null,
|
|
109
|
+
promoted_at: rule.promoted_at || new Date().toISOString(),
|
|
110
|
+
propagated_to: rule.propagated_to || [],
|
|
111
|
+
promotion_count: rule.promotion_count || 1,
|
|
112
|
+
is_universal: (rule.promotion_count || 1) >= UNIVERSAL_THRESHOLD,
|
|
113
|
+
is_npm_candidate: (rule.promotion_count || 1) >= NPM_CANDIDATE_THRESHOLD,
|
|
114
|
+
shipped_in_version: rule.shipped_in_version || null,
|
|
115
|
+
};
|
|
116
|
+
rules.push(newRule);
|
|
117
|
+
atomicWriteJsonl(_rulesFile, rules);
|
|
118
|
+
return newRule;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @returns {object[]} All global rollup entries */
|
|
122
|
+
function readGlobalRollups() {
|
|
123
|
+
return loadJsonl(_rollupFile);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Append a global rollup entry. Dedup by source_project + milestone pair.
|
|
128
|
+
* @param {object} entry
|
|
129
|
+
* @returns {object} The written entry
|
|
130
|
+
*/
|
|
131
|
+
function writeGlobalRollup(entry) {
|
|
132
|
+
ensureDir(_globalDir);
|
|
133
|
+
const rollups = loadJsonl(_rollupFile);
|
|
134
|
+
const existing = rollups.find((r) =>
|
|
135
|
+
r.source_project === entry.source_project && r.milestone === entry.milestone);
|
|
136
|
+
|
|
137
|
+
if (existing) {
|
|
138
|
+
// Update in place
|
|
139
|
+
Object.assign(existing, entry, { ts: new Date().toISOString() });
|
|
140
|
+
atomicWriteJsonl(_rollupFile, rollups);
|
|
141
|
+
return existing;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const newEntry = { ts: new Date().toISOString(), ...entry };
|
|
145
|
+
rollups.push(newEntry);
|
|
146
|
+
atomicWriteJsonl(_rollupFile, rollups);
|
|
147
|
+
return newEntry;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @returns {object[]} All global signal distribution entries */
|
|
151
|
+
function readGlobalSignalDistributions() {
|
|
152
|
+
return loadJsonl(_signalFile);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Write or update a global signal distribution entry.
|
|
157
|
+
* One entry per project (overwrites previous).
|
|
158
|
+
* @param {object} entry
|
|
159
|
+
* @returns {object} The written entry
|
|
160
|
+
*/
|
|
161
|
+
function writeGlobalSignalDistribution(entry) {
|
|
162
|
+
ensureDir(_globalDir);
|
|
163
|
+
const entries = loadJsonl(_signalFile);
|
|
164
|
+
const idx = entries.findIndex((e) => e.source_project === entry.source_project);
|
|
165
|
+
const newEntry = { ts: new Date().toISOString(), ...entry };
|
|
166
|
+
|
|
167
|
+
if (idx >= 0) {
|
|
168
|
+
entries[idx] = newEntry;
|
|
169
|
+
} else {
|
|
170
|
+
entries.push(newEntry);
|
|
171
|
+
}
|
|
172
|
+
atomicWriteJsonl(_signalFile, entries);
|
|
173
|
+
return newEntry;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Task 2: Signal distribution comparison ──────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Compare signal distributions across all projects.
|
|
180
|
+
* Returns all projects' signal rates sorted by pass-through rate descending,
|
|
181
|
+
* with the queried project highlighted.
|
|
182
|
+
* @param {string} projectName
|
|
183
|
+
* @returns {object}
|
|
184
|
+
*/
|
|
185
|
+
function compareSignalDistributions(projectName) {
|
|
186
|
+
const entries = readGlobalSignalDistributions();
|
|
187
|
+
if (entries.length < 2) {
|
|
188
|
+
return {
|
|
189
|
+
insufficient_data: true,
|
|
190
|
+
projects: entries.map(formatProjectSignals),
|
|
191
|
+
queried_project: projectName,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const sorted = entries
|
|
196
|
+
.map(formatProjectSignals)
|
|
197
|
+
.sort((a, b) => (b.signal_rates["pass-through"] || 0) - (a.signal_rates["pass-through"] || 0));
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
insufficient_data: false,
|
|
201
|
+
projects: sorted.map((p) => ({
|
|
202
|
+
...p,
|
|
203
|
+
is_queried: p.source_project === projectName,
|
|
204
|
+
})),
|
|
205
|
+
queried_project: projectName,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Compare signal distributions for a specific domain type across all projects.
|
|
211
|
+
* @param {string} domainType
|
|
212
|
+
* @returns {object}
|
|
213
|
+
*/
|
|
214
|
+
function getDomainTypeComparison(domainType) {
|
|
215
|
+
const entries = readGlobalSignalDistributions();
|
|
216
|
+
const matches = [];
|
|
217
|
+
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const domSignals = (entry.domain_type_signals || [])
|
|
220
|
+
.find((d) => d.domain_type === domainType);
|
|
221
|
+
if (domSignals) {
|
|
222
|
+
matches.push({
|
|
223
|
+
source_project: entry.source_project,
|
|
224
|
+
domain_type: domainType,
|
|
225
|
+
signal_counts: domSignals.signal_counts || {},
|
|
226
|
+
total_tasks: domSignals.total_tasks || 0,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (matches.length < 2) {
|
|
232
|
+
return { insufficient_data: true, domain_type: domainType, projects: matches };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { insufficient_data: false, domain_type: domainType, projects: matches };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Task 3: Universal rule promotion + global ELO ───────────────────────────
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if a global rule qualifies for universal or npm-candidate status.
|
|
242
|
+
* Updates the rule on disk if thresholds are met.
|
|
243
|
+
* @param {string} globalRuleId
|
|
244
|
+
* @returns {object|null} Updated rule or null if not found
|
|
245
|
+
*/
|
|
246
|
+
function checkUniversalPromotion(globalRuleId) {
|
|
247
|
+
const rules = loadJsonl(_rulesFile);
|
|
248
|
+
const rule = rules.find((r) => r.global_id === globalRuleId);
|
|
249
|
+
if (!rule) return null;
|
|
250
|
+
|
|
251
|
+
let changed = false;
|
|
252
|
+
if (rule.promotion_count >= UNIVERSAL_THRESHOLD && !rule.is_universal) {
|
|
253
|
+
rule.is_universal = true;
|
|
254
|
+
changed = true;
|
|
255
|
+
}
|
|
256
|
+
if (rule.promotion_count >= NPM_CANDIDATE_THRESHOLD && !rule.is_npm_candidate) {
|
|
257
|
+
rule.is_npm_candidate = true;
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (changed) {
|
|
262
|
+
atomicWriteJsonl(_rulesFile, rules);
|
|
263
|
+
}
|
|
264
|
+
return rule;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the latest global ELO for a project.
|
|
269
|
+
* @param {string} projectName
|
|
270
|
+
* @returns {number|null}
|
|
271
|
+
*/
|
|
272
|
+
function getGlobalELO(projectName) {
|
|
273
|
+
const rollups = readGlobalRollups();
|
|
274
|
+
const projectRollups = rollups.filter((r) => r.source_project === projectName);
|
|
275
|
+
if (projectRollups.length === 0) return null;
|
|
276
|
+
// Return latest elo_after
|
|
277
|
+
const latest = projectRollups[projectRollups.length - 1];
|
|
278
|
+
return latest.elo_after != null ? latest.elo_after : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get all projects ranked by latest elo_after, descending.
|
|
283
|
+
* @returns {object[]} Array of { source_project, elo_after, milestone }
|
|
284
|
+
*/
|
|
285
|
+
function getProjectRankings() {
|
|
286
|
+
const rollups = readGlobalRollups();
|
|
287
|
+
if (rollups.length === 0) return [];
|
|
288
|
+
|
|
289
|
+
// Get latest rollup per project
|
|
290
|
+
const latest = {};
|
|
291
|
+
for (const r of rollups) {
|
|
292
|
+
latest[r.source_project] = r;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return Object.values(latest)
|
|
296
|
+
.map((r) => ({
|
|
297
|
+
source_project: r.source_project,
|
|
298
|
+
elo_after: r.elo_after != null ? r.elo_after : ELO_START,
|
|
299
|
+
milestone: r.milestone,
|
|
300
|
+
}))
|
|
301
|
+
.sort((a, b) => b.elo_after - a.elo_after);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Internal helpers ────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function formatProjectSignals(entry) {
|
|
307
|
+
const rates = entry.signal_rates || {};
|
|
308
|
+
// Ensure normalization
|
|
309
|
+
const sum = Object.values(rates).reduce((s, v) => s + v, 0);
|
|
310
|
+
const normalized = {};
|
|
311
|
+
if (sum > 0) {
|
|
312
|
+
for (const [k, v] of Object.entries(rates)) {
|
|
313
|
+
normalized[k] = Math.round((v / sum) * 1000) / 1000;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
source_project: entry.source_project,
|
|
318
|
+
total_tasks: entry.total_tasks || 0,
|
|
319
|
+
signal_rates: sum > 0 ? normalized : rates,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getProjectName() {
|
|
324
|
+
try {
|
|
325
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
|
|
326
|
+
return pkg.name || path.basename(process.cwd());
|
|
327
|
+
} catch {
|
|
328
|
+
return path.basename(process.cwd());
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function loadJsonl(fp) {
|
|
333
|
+
if (!fs.existsSync(fp)) return [];
|
|
334
|
+
const c = fs.readFileSync(fp, "utf8").trim();
|
|
335
|
+
return c ? c.split("\n").map(safeParse).filter(Boolean) : [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function safeParse(l) { try { return JSON.parse(l); } catch { return null; } }
|
|
339
|
+
|
|
340
|
+
function atomicWriteJsonl(fp, records) {
|
|
341
|
+
const dir = path.dirname(fp);
|
|
342
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
343
|
+
const tmp = fp + ".tmp." + process.pid;
|
|
344
|
+
fs.writeFileSync(tmp, records.map((r) => JSON.stringify(r)).join("\n") + "\n");
|
|
345
|
+
fs.renameSync(tmp, fp);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function ensureDir(dir) {
|
|
349
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
350
|
+
}
|