@tekyzinc/gsd-t 2.39.13 → 2.46.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/CHANGELOG.md +12 -0
- package/README.md +19 -10
- 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 +194 -6
- package/commands/gsd-t-debug.md +38 -3
- package/commands/gsd-t-doc-ripple.md +148 -0
- package/commands/gsd-t-execute.md +328 -54
- package/commands/gsd-t-help.md +32 -10
- package/commands/gsd-t-integrate.md +59 -7
- package/commands/gsd-t-metrics.md +143 -0
- package/commands/gsd-t-plan.md +49 -2
- package/commands/gsd-t-qa.md +26 -5
- package/commands/gsd-t-quick.md +36 -3
- package/commands/gsd-t-status.md +78 -0
- package/commands/gsd-t-test-sync.md +23 -2
- package/commands/gsd-t-verify.md +142 -10
- package/commands/gsd-t-visualize.md +11 -1
- package/commands/gsd-t-wave.md +64 -18
- package/docs/GSD-T-README.md +10 -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/framework-comparison-scorecard.md +160 -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 +66 -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/examples/rules/desktop.ini +2 -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 +92 -10
- package/templates/desktop.ini +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [2.46.11] - 2026-03-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **M28: Doc-Ripple Subagent** — automated document ripple enforcement agent. Threshold check (7 FIRE/3 SKIP conditions), blast radius analysis, manifest generation, parallel document updates. New command: `gsd-t-doc-ripple`. 43 new tests. Wired into execute, integrate, quick, debug, wave.
|
|
9
|
+
- **Orchestrator context self-check** — execute and wave orchestrators now check their own context utilization after every domain/phase. If >= 70%, saves progress and stops to prevent session breaks.
|
|
10
|
+
- **Functional E2E test quality standard (REQ-050)** — Playwright specs must verify functional behavior, not just element existence. Shallow test audit added to qa, test-sync, verify, complete-milestone commands.
|
|
11
|
+
- **Document Ripple Completion Gate (REQ-051)** — structural rule preventing "done" reports until all downstream documents are updated.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Command count: 50 → 51 (added `gsd-t-doc-ripple`)
|
|
15
|
+
- Package description updated to include doc-ripple enforcement
|
|
16
|
+
|
|
5
17
|
## [2.39.12] - 2026-03-19
|
|
6
18
|
|
|
7
19
|
### Added
|
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
|
|
25
|
+
This installs 46 GSD-T commands + 5 utility commands (51 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,28 @@ 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 |
|
|
144
|
+
| `/user:gsd-t-doc-ripple` | Automated document ripple — update downstream docs after code changes | Auto-spawned |
|
|
137
145
|
| `/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 |
|
|
146
|
+
| `/user:gsd-t-verify` | Run quality gates + goal-backward behavior verification | In wave |
|
|
147
|
+
| `/user:gsd-t-complete-milestone` | Archive + git tag (goal-backward gate required) | In wave |
|
|
140
148
|
|
|
141
149
|
### Automation & Utilities
|
|
142
150
|
|
|
143
151
|
| Command | Purpose | Auto |
|
|
144
152
|
|---------|---------|------|
|
|
145
153
|
| `/user:gsd-t-wave` | Full cycle, auto-advances all phases | Manual |
|
|
146
|
-
| `/user:gsd-t-status` | Cross-domain progress view | Manual |
|
|
154
|
+
| `/user:gsd-t-status` | Cross-domain progress view with token breakdown by domain/task/phase | Manual |
|
|
147
155
|
| `/user:gsd-t-resume` | Restore context, continue | Manual |
|
|
148
156
|
| `/user:gsd-t-quick` | Fast task with GSD-T guarantees | Manual |
|
|
149
157
|
| `/user:gsd-t-reflect` | Generate retrospective from event stream, propose memory updates | Manual |
|
|
150
158
|
| `/user:gsd-t-visualize` | Launch browser dashboard — SSE server + React Flow agent visualization | Manual |
|
|
151
159
|
| `/user:gsd-t-debug` | Systematic debugging with state | Manual |
|
|
160
|
+
| `/user:gsd-t-metrics` | View task telemetry, process ELO, signal distribution, domain health, and cross-project comparison (`--cross-project`) | Manual |
|
|
152
161
|
| `/user:gsd-t-health` | Validate .gsd-t/ structure, optionally repair | Manual |
|
|
153
162
|
| `/user:gsd-t-pause` | Save exact position for reliable resume | Manual |
|
|
154
163
|
| `/user:gsd-t-log` | Sync progress Decision Log with recent git activity | Manual |
|
|
@@ -306,8 +315,8 @@ get-stuff-done-teams/
|
|
|
306
315
|
├── LICENSE
|
|
307
316
|
├── bin/
|
|
308
317
|
│ └── gsd-t.js # CLI installer
|
|
309
|
-
├── commands/ #
|
|
310
|
-
│ ├── gsd-t-*.md #
|
|
318
|
+
├── commands/ # 51 slash commands
|
|
319
|
+
│ ├── gsd-t-*.md # 45 GSD-T workflow commands
|
|
311
320
|
│ ├── gsd.md # GSD-T smart router
|
|
312
321
|
│ ├── branch.md # Git branch helper
|
|
313
322
|
│ ├── checkin.md # Auto-version + commit/push 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
|
+
}
|