@poltergeist-ai/cli 0.1.0 → 0.1.6
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 +204 -0
- package/dist/cli.js +494 -46
- package/dist/index.d.ts +36 -0
- package/dist/index.js +494 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -225,31 +225,31 @@ var LANG_MAP = {
|
|
|
225
225
|
var JS_TS = ["typescript", "javascript"];
|
|
226
226
|
var TS_ONLY = ["typescript"];
|
|
227
227
|
var PATTERN_RULES = [
|
|
228
|
-
// Imports
|
|
229
|
-
{ category: "import_style", choice: "named_import", pattern: /^import\s+\{/, languages: JS_TS },
|
|
230
|
-
{ category: "import_style", choice: "default_import", pattern: /^import\s+[A-Za-z_$][^\s{]*\s+from/, languages: JS_TS },
|
|
231
|
-
{ category: "import_style", choice: "path_alias", pattern: /from\s+['"][@~]\//, languages: JS_TS },
|
|
232
|
-
{ category: "import_style", choice: "relative_import", pattern: /from\s+['"]\.\.?\//, languages: JS_TS },
|
|
233
|
-
// Exports
|
|
234
|
-
{ category: "export_style", choice: "named_export", pattern: /^export\s+(?:const|function|class|type|interface|enum)\s/, languages: JS_TS },
|
|
235
|
-
{ category: "export_style", choice: "default_export", pattern: /^export\s+default\b/, languages: JS_TS },
|
|
236
|
-
{ category: "export_style", choice: "re_export", pattern: /^export\s+\{[^}]*\}\s+from/, languages: JS_TS },
|
|
237
|
-
// Functions
|
|
228
|
+
// Imports — typically project-level config (path aliases, import style)
|
|
229
|
+
{ category: "import_style", choice: "named_import", pattern: /^import\s+\{/, languages: JS_TS, lintEnforceable: true },
|
|
230
|
+
{ category: "import_style", choice: "default_import", pattern: /^import\s+[A-Za-z_$][^\s{]*\s+from/, languages: JS_TS, lintEnforceable: true },
|
|
231
|
+
{ category: "import_style", choice: "path_alias", pattern: /from\s+['"][@~]\//, languages: JS_TS, lintEnforceable: true },
|
|
232
|
+
{ category: "import_style", choice: "relative_import", pattern: /from\s+['"]\.\.?\//, languages: JS_TS, lintEnforceable: true },
|
|
233
|
+
// Exports — often enforced by barrel-file lint rules
|
|
234
|
+
{ category: "export_style", choice: "named_export", pattern: /^export\s+(?:const|function|class|type|interface|enum)\s/, languages: JS_TS, lintEnforceable: true },
|
|
235
|
+
{ category: "export_style", choice: "default_export", pattern: /^export\s+default\b/, languages: JS_TS, lintEnforceable: true },
|
|
236
|
+
{ category: "export_style", choice: "re_export", pattern: /^export\s+\{[^}]*\}\s+from/, languages: JS_TS, lintEnforceable: true },
|
|
237
|
+
// Functions — genuine opinion, not typically lint-enforced
|
|
238
238
|
{ category: "function_style", choice: "arrow_function", pattern: /(?:const|let)\s+\w+\s*=\s*(?:async\s+)?\(/, languages: JS_TS },
|
|
239
239
|
{ category: "function_style", choice: "function_declaration", pattern: /^(?:export\s+)?(?:async\s+)?function\s+\w/, languages: JS_TS },
|
|
240
|
-
// Async
|
|
240
|
+
// Async — genuine architectural choice
|
|
241
241
|
{ category: "async_style", choice: "async_await", pattern: /\bawait\s/, languages: [...JS_TS, "python"] },
|
|
242
242
|
{ category: "async_style", choice: "then_chain", pattern: /\.then\s*\(/, languages: JS_TS },
|
|
243
|
-
// Control flow
|
|
243
|
+
// Control flow — genuine opinion about code structure
|
|
244
244
|
{ category: "control_flow", choice: "early_return", pattern: /^\s+if\s*\(.*\)\s*return\b/, languages: JS_TS },
|
|
245
245
|
{ category: "control_flow", choice: "guard_clause", pattern: /^\s+if\s*\(!/, languages: JS_TS },
|
|
246
|
-
// Strings
|
|
247
|
-
{ category: "string_style", choice: "template_literal", pattern: /`[^`]*\$\{/, languages: JS_TS },
|
|
248
|
-
// Modern operators
|
|
246
|
+
// Strings — often lint-enforced (prefer-template)
|
|
247
|
+
{ category: "string_style", choice: "template_literal", pattern: /`[^`]*\$\{/, languages: JS_TS, lintEnforceable: true },
|
|
248
|
+
// Modern operators — genuine preference
|
|
249
249
|
{ category: "modern_operators", choice: "optional_chaining", pattern: /\?\.\w/, languages: JS_TS },
|
|
250
250
|
{ category: "modern_operators", choice: "nullish_coalescing", pattern: /\?\?/, languages: JS_TS },
|
|
251
251
|
{ category: "modern_operators", choice: "destructuring", pattern: /(?:const|let|var)\s+[\[{]/, languages: JS_TS },
|
|
252
|
-
// TypeScript types
|
|
252
|
+
// TypeScript types — genuine opinion about modelling
|
|
253
253
|
{ category: "type_definition", choice: "interface", pattern: /^(?:export\s+)?interface\s+\w/, languages: TS_ONLY },
|
|
254
254
|
{ category: "type_definition", choice: "type_alias", pattern: /^(?:export\s+)?type\s+\w+\s*=/, languages: TS_ONLY },
|
|
255
255
|
{ category: "enum_vs_union", choice: "enum", pattern: /^(?:export\s+)?(?:const\s+)?enum\s+\w/, languages: TS_ONLY },
|
|
@@ -257,15 +257,15 @@ var PATTERN_RULES = [
|
|
|
257
257
|
{ category: "type_features", choice: "as_const", pattern: /\bas\s+const\b/, languages: TS_ONLY },
|
|
258
258
|
{ category: "type_features", choice: "generic_usage", pattern: /<[A-Z]\w*(?:,\s*[A-Z]\w*)*>/, languages: TS_ONLY },
|
|
259
259
|
{ category: "type_features", choice: "explicit_return_type", pattern: /\)\s*:\s*(?:Promise<|void|string|number|boolean|\w+\[\])/, languages: TS_ONLY },
|
|
260
|
-
// Error handling
|
|
260
|
+
// Error handling — genuine architectural choice
|
|
261
261
|
{ category: "error_handling", choice: "try_catch", pattern: /^\s*(?:try\s*\{|\}\s*catch\s*\()/ },
|
|
262
262
|
{ category: "error_handling", choice: "custom_error", pattern: /class\s+\w+Error\s+extends/ },
|
|
263
|
-
// Testing
|
|
263
|
+
// Testing — genuine opinion about test structure
|
|
264
264
|
{ category: "test_structure", choice: "describe_it", pattern: /\b(?:describe|it)\s*\(/, languages: JS_TS },
|
|
265
265
|
{ category: "test_structure", choice: "test_fn", pattern: /\btest\s*\(/, languages: JS_TS },
|
|
266
266
|
{ category: "test_assertion", choice: "expect", pattern: /\bexpect\s*\(/, languages: JS_TS },
|
|
267
267
|
{ category: "test_assertion", choice: "assert", pattern: /\bassert\.\w/, languages: [...JS_TS, "python"] },
|
|
268
|
-
// Architecture
|
|
268
|
+
// Architecture — strong personal opinion territory
|
|
269
269
|
{ category: "composition_style", choice: "inheritance", pattern: /class\s+\w+\s+extends\s/, languages: JS_TS },
|
|
270
270
|
{ category: "composition_style", choice: "composition", pattern: /\buse[A-Z]\w+\s*\(/, languages: JS_TS },
|
|
271
271
|
{ category: "architecture", choice: "factory_function", pattern: /(?:create|make|build)[A-Z]\w+\s*\(/, languages: JS_TS },
|
|
@@ -274,7 +274,7 @@ var PATTERN_RULES = [
|
|
|
274
274
|
{ category: "python_style", choice: "type_hints", pattern: /def\s+\w+\(.*:\s*\w+/, languages: ["python"] },
|
|
275
275
|
{ category: "python_style", choice: "list_comprehension", pattern: /\[.*\bfor\b.*\bin\b.*\]/, languages: ["python"] },
|
|
276
276
|
{ category: "python_style", choice: "dataclass", pattern: /@dataclass/, languages: ["python"] },
|
|
277
|
-
{ category: "python_style", choice: "f_string", pattern: /f['"].*\{/, languages: ["python"] }
|
|
277
|
+
{ category: "python_style", choice: "f_string", pattern: /f['"].*\{/, languages: ["python"], lintEnforceable: true }
|
|
278
278
|
];
|
|
279
279
|
var CATEGORY_LABELS = {
|
|
280
280
|
import_style: "Import Style",
|
|
@@ -377,17 +377,31 @@ function extractCodeStyleFromDiff(diffOutput) {
|
|
|
377
377
|
}
|
|
378
378
|
function summariseCodeStyle(signals) {
|
|
379
379
|
const observations = [];
|
|
380
|
+
const categoryLintStatus = /* @__PURE__ */ new Map();
|
|
381
|
+
for (const rule of PATTERN_RULES) {
|
|
382
|
+
const current = categoryLintStatus.get(rule.category);
|
|
383
|
+
if (current === void 0) {
|
|
384
|
+
categoryLintStatus.set(rule.category, rule.lintEnforceable === true);
|
|
385
|
+
} else {
|
|
386
|
+
categoryLintStatus.set(
|
|
387
|
+
rule.category,
|
|
388
|
+
current && rule.lintEnforceable === true
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
380
392
|
for (const [category, choices] of Object.entries(signals.counters)) {
|
|
381
393
|
const entries = Object.entries(choices).sort((a, b) => b[1] - a[1]);
|
|
382
394
|
const total = entries.reduce((sum, [, c]) => sum + c, 0);
|
|
383
395
|
if (total < 3) continue;
|
|
384
396
|
const [topChoice, topCount] = entries[0];
|
|
397
|
+
const isLintEnforceable = categoryLintStatus.get(category) ?? false;
|
|
385
398
|
if (entries.length === 1) {
|
|
386
399
|
if (topCount >= 5) {
|
|
387
400
|
observations.push({
|
|
388
401
|
category: CATEGORY_LABELS[category] ?? category,
|
|
389
402
|
observation: `Frequently uses ${CHOICE_LABELS[topChoice] ?? topChoice} (${topCount} occurrences)`,
|
|
390
|
-
confidence: topCount >= 15 ? "strong" : "moderate"
|
|
403
|
+
confidence: topCount >= 15 ? "strong" : "moderate",
|
|
404
|
+
lintEnforceable: isLintEnforceable
|
|
391
405
|
});
|
|
392
406
|
}
|
|
393
407
|
continue;
|
|
@@ -401,7 +415,8 @@ function summariseCodeStyle(signals) {
|
|
|
401
415
|
observations.push({
|
|
402
416
|
category: CATEGORY_LABELS[category] ?? category,
|
|
403
417
|
observation: `Prefers ${topLabel} over ${runnerLabel} (${pct}% of ${total})`,
|
|
404
|
-
confidence
|
|
418
|
+
confidence,
|
|
419
|
+
lintEnforceable: isLintEnforceable
|
|
405
420
|
});
|
|
406
421
|
}
|
|
407
422
|
observations.sort((a, b) => {
|
|
@@ -419,6 +434,347 @@ function summariseCodeStyle(signals) {
|
|
|
419
434
|
import { readFileSync } from "fs";
|
|
420
435
|
|
|
421
436
|
// src/extractors/review-common.ts
|
|
437
|
+
var THEME_DEFS = [
|
|
438
|
+
{
|
|
439
|
+
theme: "decomposition",
|
|
440
|
+
label: "Decomposition / single responsibility",
|
|
441
|
+
patterns: [
|
|
442
|
+
/\bextract\b.*\b(?:into|to|out)\b/,
|
|
443
|
+
/\bsplit\b.*\b(?:into|up|out)\b/,
|
|
444
|
+
/\bseparate\b/,
|
|
445
|
+
/\bsingle.?responsib/,
|
|
446
|
+
/\bdoing\s+(?:too\s+)?(?:much|many|multiple)\b/,
|
|
447
|
+
/\btoo\s+(?:big|large|long|complex)\b/,
|
|
448
|
+
/\bbreak\b.*\b(?:into|up|out|down)\b/,
|
|
449
|
+
/\bpull\b.*\bout\b/
|
|
450
|
+
]
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
theme: "naming",
|
|
454
|
+
label: "Naming clarity",
|
|
455
|
+
patterns: [
|
|
456
|
+
/\brename\b/,
|
|
457
|
+
/\bnaming\b/,
|
|
458
|
+
/\bname\b.*\b(?:unclear|confusing|misleading|vague|generic|better)\b/,
|
|
459
|
+
/\bwhat\s+does\s+\w+\s+mean\b/,
|
|
460
|
+
/\bmore\s+descriptive\b/
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
theme: "error_handling",
|
|
465
|
+
label: "Error handling / edge cases",
|
|
466
|
+
patterns: [
|
|
467
|
+
/\berror\s+handl/,
|
|
468
|
+
/\bwhat\s+(?:if|happens\s+(?:if|when))\b/,
|
|
469
|
+
/\bedge\s+case/,
|
|
470
|
+
/\bfail(?:s|ure|ing)?\b.*\b(?:silent|graceful|handle|catch)\b/,
|
|
471
|
+
/\bunhandled\b/,
|
|
472
|
+
/\bmissing\b.*\b(?:error|catch|check|validation)\b/,
|
|
473
|
+
/\bcatch\b.*\b(?:this|here|error)\b/
|
|
474
|
+
]
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
theme: "testing",
|
|
478
|
+
label: "Testing / test coverage",
|
|
479
|
+
patterns: [
|
|
480
|
+
/\badd\b.*\btests?\b/,
|
|
481
|
+
/\btest\s+(?:for|this|that|coverage|case|missing)\b/,
|
|
482
|
+
/\buntested\b/,
|
|
483
|
+
/\bspec\b.*\b(?:for|missing|add)\b/,
|
|
484
|
+
/\bcoverage\b/,
|
|
485
|
+
/\bassert(?:ion)?\b.*\b(?:missing|add|should)\b/
|
|
486
|
+
]
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
theme: "performance",
|
|
490
|
+
label: "Performance awareness",
|
|
491
|
+
patterns: [
|
|
492
|
+
/\bperformance\b/,
|
|
493
|
+
/\bslow\b/,
|
|
494
|
+
/\boptimiz/,
|
|
495
|
+
/\bcache\b/,
|
|
496
|
+
/\bmemory\b/,
|
|
497
|
+
/\bcomplexity\b/,
|
|
498
|
+
/\bn\s*\+\s*1\b/,
|
|
499
|
+
/\bunnecessary\s+(?:re-?render|iteration|loop|allocation|copy)\b/,
|
|
500
|
+
/\bexpensive\b/
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
theme: "readability",
|
|
505
|
+
label: "Readability / clarity",
|
|
506
|
+
patterns: [
|
|
507
|
+
/\breadab/,
|
|
508
|
+
/\bclarity\b/,
|
|
509
|
+
/\bsimplif/,
|
|
510
|
+
/\bconfusing\b/,
|
|
511
|
+
/\bhard\s+to\s+(?:read|follow|understand|parse)\b/,
|
|
512
|
+
/\bcomplicated\b/,
|
|
513
|
+
/\bself[- ]?explanat/
|
|
514
|
+
]
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
theme: "type_safety",
|
|
518
|
+
label: "Type safety / strictness",
|
|
519
|
+
patterns: [
|
|
520
|
+
/\btype\s+safe/,
|
|
521
|
+
/\b(?:use|avoid|remove)\b.*\bany\b/,
|
|
522
|
+
/\bnarrow\b.*\btype/,
|
|
523
|
+
/\btype\s+assert/,
|
|
524
|
+
/\bas\s+unknown\b/,
|
|
525
|
+
/\bstrict(?:er|ly)?\s+typ/,
|
|
526
|
+
/\binfer\b.*\btype/
|
|
527
|
+
]
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
theme: "security",
|
|
531
|
+
label: "Security / input validation",
|
|
532
|
+
patterns: [
|
|
533
|
+
/\bsecur/,
|
|
534
|
+
/\bsaniti[sz]/,
|
|
535
|
+
/\bvalidat/,
|
|
536
|
+
/\binject/,
|
|
537
|
+
/\bescap(?:e|ing)\b/,
|
|
538
|
+
/\bauth(?:entication|orization)?\b/,
|
|
539
|
+
/\bxss\b/i
|
|
540
|
+
]
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
theme: "api_design",
|
|
544
|
+
label: "API / interface design",
|
|
545
|
+
patterns: [
|
|
546
|
+
/\bapi\s+(?:design|surface|contract|boundary)\b/i,
|
|
547
|
+
/\bpublic\s+(?:api|interface|surface)\b/,
|
|
548
|
+
/\bbreaking\s+change/,
|
|
549
|
+
/\bbackwards?\s+compat/,
|
|
550
|
+
/\bconsumer\b/,
|
|
551
|
+
/\bcaller\b/
|
|
552
|
+
]
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
theme: "consistency",
|
|
556
|
+
label: "Consistency with existing patterns",
|
|
557
|
+
patterns: [
|
|
558
|
+
/\bconsisten/,
|
|
559
|
+
/\belsewhere\s+(?:we|in)\b/,
|
|
560
|
+
/\bwe\s+(?:usually|always|typically|normally)\b/,
|
|
561
|
+
/\bexisting\s+(?:pattern|convention|approach|style)\b/,
|
|
562
|
+
/\bdoesn'?t\s+match\b/,
|
|
563
|
+
/\balign\b.*\bwith\b/,
|
|
564
|
+
/\bin\s+line\s+with\b/
|
|
565
|
+
]
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
theme: "documentation",
|
|
569
|
+
label: "Documentation / comments",
|
|
570
|
+
patterns: [
|
|
571
|
+
/\bdocument\b/,
|
|
572
|
+
/\bdocstring\b/,
|
|
573
|
+
/\bjsdoc\b/,
|
|
574
|
+
/\bcomment\b.*\b(?:explain|why|add|missing)\b/,
|
|
575
|
+
/\bexplain\b.*\b(?:why|what|how)\b/,
|
|
576
|
+
/\breadme\b/i
|
|
577
|
+
]
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
theme: "dependency_awareness",
|
|
581
|
+
label: "Dependency / coupling awareness",
|
|
582
|
+
patterns: [
|
|
583
|
+
/\bcoupl(?:ed|ing)\b/,
|
|
584
|
+
/\bdependen(?:cy|cies|t)\b/,
|
|
585
|
+
/\bcircular\b/,
|
|
586
|
+
/\bimport\b.*\b(?:heavy|large|unnecessary)\b/,
|
|
587
|
+
/\btight(?:ly)?\s+coupled\b/,
|
|
588
|
+
/\bdecouple\b/
|
|
589
|
+
]
|
|
590
|
+
}
|
|
591
|
+
];
|
|
592
|
+
var PRAISE_PATTERNS = [
|
|
593
|
+
/\bnice\b/,
|
|
594
|
+
/\bgreat\b/,
|
|
595
|
+
/\bgood\s+(?:call|catch|point|idea|work|job|stuff)\b/,
|
|
596
|
+
/\blove\s+(?:this|that|it|the)\b/,
|
|
597
|
+
/\bclean\b/,
|
|
598
|
+
/\belegant\b/,
|
|
599
|
+
/\blgtm\b/i,
|
|
600
|
+
/\bwell\s+done\b/,
|
|
601
|
+
/\bsolid\b/,
|
|
602
|
+
/\b(?:looks?\s+)?good\s+to\s+(?:me|go)\b/,
|
|
603
|
+
/\+1\b/,
|
|
604
|
+
/👍|🎉|✅/,
|
|
605
|
+
/\bneat\b/,
|
|
606
|
+
/\bawesome\b/
|
|
607
|
+
];
|
|
608
|
+
var EXPLANATION_PATTERNS = [
|
|
609
|
+
/\bbecause\b/,
|
|
610
|
+
/\bsince\b/,
|
|
611
|
+
/\bthe\s+reason\b/,
|
|
612
|
+
/\bso\s+that\b/,
|
|
613
|
+
/\bthis\s+(?:ensures?|prevents?|avoids?|allows?|makes?)\b/,
|
|
614
|
+
/\botherwise\b/,
|
|
615
|
+
/\bthe\s+(?:problem|issue|risk|concern)\s+(?:is|here|with)\b/
|
|
616
|
+
];
|
|
617
|
+
var SUGGESTIVE_PATTERNS = [
|
|
618
|
+
/\bconsider\b/,
|
|
619
|
+
/\bwhat\s+(?:about|if\s+we|do\s+you\s+think)\b/,
|
|
620
|
+
/\bmaybe\b/,
|
|
621
|
+
/\balternative(?:ly)?\b/,
|
|
622
|
+
/\bone\s+option\b/,
|
|
623
|
+
/\bcould\s+(?:we|you|this)\b/,
|
|
624
|
+
/\bhow\s+about\b/,
|
|
625
|
+
/\bwhat\s+(?:do\s+you|would\s+you)\s+think\b/,
|
|
626
|
+
/\bmight\s+(?:be|want)\b/,
|
|
627
|
+
/\bwould\s+(?:it\s+)?be\s+(?:better|cleaner|simpler|easier)\b/
|
|
628
|
+
];
|
|
629
|
+
var DIRECTIVE_PATTERNS = [
|
|
630
|
+
/^(?:rename|remove|delete|drop|change|move|replace|use|add|fix|update|revert|undo)\b/i,
|
|
631
|
+
/\bplease\s+(?:rename|remove|delete|change|move|replace|use|add|fix|update|revert)\b/,
|
|
632
|
+
/\bshould\s+be\b/,
|
|
633
|
+
/\bneeds?\s+to\s+be\b/,
|
|
634
|
+
/\bmust\s+be\b/,
|
|
635
|
+
/\bdon'?t\b.*\bhere\b/,
|
|
636
|
+
/\bthis\s+(?:should|must|needs?)\b/
|
|
637
|
+
];
|
|
638
|
+
function extractRecurringPhrases(comments) {
|
|
639
|
+
if (comments.length < 5) return [];
|
|
640
|
+
const phraseCounts = /* @__PURE__ */ new Map();
|
|
641
|
+
const MIN_PHRASE_LEN = 2;
|
|
642
|
+
const MAX_PHRASE_LEN = 5;
|
|
643
|
+
for (const comment of comments) {
|
|
644
|
+
const lower = comment.toLowerCase().replace(/[^\w\s]/g, " ");
|
|
645
|
+
const words = lower.split(/\s+/).filter((w) => w.length > 2);
|
|
646
|
+
const seenInComment = /* @__PURE__ */ new Set();
|
|
647
|
+
for (let start = 0; start < words.length; start++) {
|
|
648
|
+
for (let len = MIN_PHRASE_LEN; len <= MAX_PHRASE_LEN && start + len <= words.length; len++) {
|
|
649
|
+
const phrase = words.slice(start, start + len).join(" ");
|
|
650
|
+
if (!seenInComment.has(phrase)) {
|
|
651
|
+
seenInComment.add(phrase);
|
|
652
|
+
phraseCounts.set(phrase, (phraseCounts.get(phrase) ?? 0) + 1);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const minCount = Math.max(3, Math.floor(comments.length * 0.1));
|
|
658
|
+
const STOP_WORDS = /* @__PURE__ */ new Set([
|
|
659
|
+
"the",
|
|
660
|
+
"this",
|
|
661
|
+
"that",
|
|
662
|
+
"with",
|
|
663
|
+
"from",
|
|
664
|
+
"have",
|
|
665
|
+
"has",
|
|
666
|
+
"been",
|
|
667
|
+
"will",
|
|
668
|
+
"would",
|
|
669
|
+
"could",
|
|
670
|
+
"should",
|
|
671
|
+
"can",
|
|
672
|
+
"are",
|
|
673
|
+
"for",
|
|
674
|
+
"not",
|
|
675
|
+
"but",
|
|
676
|
+
"and",
|
|
677
|
+
"you",
|
|
678
|
+
"was",
|
|
679
|
+
"were",
|
|
680
|
+
"than",
|
|
681
|
+
"also",
|
|
682
|
+
"its",
|
|
683
|
+
"into",
|
|
684
|
+
"here",
|
|
685
|
+
"there",
|
|
686
|
+
"then",
|
|
687
|
+
"when",
|
|
688
|
+
"what",
|
|
689
|
+
"which",
|
|
690
|
+
"where",
|
|
691
|
+
"how"
|
|
692
|
+
]);
|
|
693
|
+
return [...phraseCounts.entries()].filter(([phrase, count]) => {
|
|
694
|
+
if (count < minCount) return false;
|
|
695
|
+
const words = phrase.split(" ");
|
|
696
|
+
return words.some((w) => !STOP_WORDS.has(w));
|
|
697
|
+
}).sort((a, b) => b[1] - a[1]).slice(0, 15).map(([phrase]) => phrase);
|
|
698
|
+
}
|
|
699
|
+
function extractRecurringQuestions(comments) {
|
|
700
|
+
const questions = comments.filter(
|
|
701
|
+
(c) => c.includes("?") && c.length > 15 && c.length < 300
|
|
702
|
+
);
|
|
703
|
+
if (questions.length < 2) return questions.slice(0, 5);
|
|
704
|
+
const normalise = (q) => q.toLowerCase().replace(/[^\w\s?]/g, "").replace(/\s+/g, " ").trim();
|
|
705
|
+
const seen = /* @__PURE__ */ new Map();
|
|
706
|
+
for (const q of questions) {
|
|
707
|
+
const norm = normalise(q);
|
|
708
|
+
let isDupe = false;
|
|
709
|
+
for (const existing of seen.keys()) {
|
|
710
|
+
if (norm === existing || norm.length > 20 && existing.startsWith(norm.slice(0, 20))) {
|
|
711
|
+
isDupe = true;
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (!isDupe) {
|
|
716
|
+
seen.set(norm, q);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return [...seen.values()].slice(0, 10);
|
|
720
|
+
}
|
|
721
|
+
function matchesAny(text, patterns) {
|
|
722
|
+
return patterns.some((p) => p.test(text));
|
|
723
|
+
}
|
|
724
|
+
function countMatching(comments, patterns) {
|
|
725
|
+
return comments.filter((c) => matchesAny(c.toLowerCase(), patterns)).length;
|
|
726
|
+
}
|
|
727
|
+
function sampleMatching(comments, patterns, max) {
|
|
728
|
+
const matches = [];
|
|
729
|
+
for (const c of comments) {
|
|
730
|
+
if (matchesAny(c.toLowerCase(), patterns)) {
|
|
731
|
+
matches.push(c.length > 200 ? c.slice(0, 200) + "..." : c);
|
|
732
|
+
if (matches.length >= max) break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return matches;
|
|
736
|
+
}
|
|
737
|
+
function extractThemes(comments) {
|
|
738
|
+
if (comments.length === 0) return [];
|
|
739
|
+
const themes = [];
|
|
740
|
+
for (const def of THEME_DEFS) {
|
|
741
|
+
const matchingComments = [];
|
|
742
|
+
for (const comment of comments) {
|
|
743
|
+
const lower = comment.toLowerCase();
|
|
744
|
+
if (def.patterns.some((p) => p.test(lower))) {
|
|
745
|
+
matchingComments.push(comment);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (matchingComments.length < 2) continue;
|
|
749
|
+
const ratio = Math.round(matchingComments.length / comments.length * 100) / 100;
|
|
750
|
+
const snippets = matchingComments.filter((c) => c.length > 20 && c.length < 300).slice(0, 3).map((c) => c.length > 150 ? c.slice(0, 150) + "..." : c);
|
|
751
|
+
themes.push({
|
|
752
|
+
theme: def.theme,
|
|
753
|
+
label: def.label,
|
|
754
|
+
count: matchingComments.length,
|
|
755
|
+
ratio,
|
|
756
|
+
exampleSnippets: snippets
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
themes.sort((a, b) => b.count - a.count);
|
|
760
|
+
return themes;
|
|
761
|
+
}
|
|
762
|
+
function buildToneProfile(comments) {
|
|
763
|
+
if (comments.length < 5) return void 0;
|
|
764
|
+
const n = comments.length;
|
|
765
|
+
const praiseCount = countMatching(comments, PRAISE_PATTERNS);
|
|
766
|
+
const explanationCount = countMatching(comments, EXPLANATION_PATTERNS);
|
|
767
|
+
const suggestiveCount = countMatching(comments, SUGGESTIVE_PATTERNS);
|
|
768
|
+
const directiveCount = countMatching(comments, DIRECTIVE_PATTERNS);
|
|
769
|
+
return {
|
|
770
|
+
praiseRatio: Math.round(praiseCount / n * 100) / 100,
|
|
771
|
+
explanationRatio: Math.round(explanationCount / n * 100) / 100,
|
|
772
|
+
suggestiveRatio: Math.round(suggestiveCount / n * 100) / 100,
|
|
773
|
+
directiveRatio: Math.round(directiveCount / n * 100) / 100,
|
|
774
|
+
praiseExamples: sampleMatching(comments, PRAISE_PATTERNS, 3),
|
|
775
|
+
explanationExamples: sampleMatching(comments, EXPLANATION_PATTERNS, 3)
|
|
776
|
+
};
|
|
777
|
+
}
|
|
422
778
|
function summariseReview(signals) {
|
|
423
779
|
const obs = { source: signals.source };
|
|
424
780
|
const comments = signals.reviewComments;
|
|
@@ -446,6 +802,10 @@ function summariseReview(signals) {
|
|
|
446
802
|
indices.filter((i) => i >= 0 && i < n).map((i) => sorted[i])
|
|
447
803
|
)
|
|
448
804
|
];
|
|
805
|
+
obs.reviewThemes = extractThemes(comments);
|
|
806
|
+
obs.toneProfile = buildToneProfile(comments);
|
|
807
|
+
obs.recurringQuestions = extractRecurringQuestions(comments);
|
|
808
|
+
obs.recurringPhrases = extractRecurringPhrases(comments);
|
|
449
809
|
return obs;
|
|
450
810
|
}
|
|
451
811
|
|
|
@@ -747,6 +1107,16 @@ function dominantNamingStyle(style) {
|
|
|
747
1107
|
if (style.PascalCase / total > 0.7) return "Strongly prefers PascalCase";
|
|
748
1108
|
return null;
|
|
749
1109
|
}
|
|
1110
|
+
function describeTone(profile) {
|
|
1111
|
+
const traits = [];
|
|
1112
|
+
if (profile.directiveRatio > 0.3) traits.push("direct");
|
|
1113
|
+
else if (profile.suggestiveRatio > 0.3) traits.push("collaborative");
|
|
1114
|
+
if (profile.explanationRatio > 0.3) traits.push("explanatory");
|
|
1115
|
+
if (profile.praiseRatio > 0.15) traits.push("encouraging");
|
|
1116
|
+
else if (profile.praiseRatio < 0.05) traits.push("critique-focused");
|
|
1117
|
+
if (traits.length === 0) traits.push("balanced");
|
|
1118
|
+
return traits.join(", ");
|
|
1119
|
+
}
|
|
750
1120
|
function buildGhostMarkdown(input) {
|
|
751
1121
|
const { contributor, slug, gitObs, codeStyleObs, reviewObs, slackObs, docsSignals, sourcesUsed } = input;
|
|
752
1122
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -765,23 +1135,37 @@ function buildGhostMarkdown(input) {
|
|
|
765
1135
|
"",
|
|
766
1136
|
"## Review Philosophy",
|
|
767
1137
|
"",
|
|
768
|
-
"### What they care about most (ranked)"
|
|
769
|
-
"_[Fill in manually after reviewing the data below \u2014 re-order based on review comment patterns]_",
|
|
770
|
-
"1. Correctness",
|
|
771
|
-
"2. Naming",
|
|
772
|
-
"3. Component / module boundaries",
|
|
773
|
-
"4. Test coverage",
|
|
774
|
-
"5. Consistency with existing patterns",
|
|
775
|
-
"",
|
|
776
|
-
"### What they tend to ignore",
|
|
777
|
-
"_[Fill in manually]_",
|
|
778
|
-
"",
|
|
779
|
-
"### Dealbreakers",
|
|
780
|
-
"_[Fill in manually]_",
|
|
781
|
-
"",
|
|
782
|
-
"### Recurring questions / phrases"
|
|
1138
|
+
"### What they care about most (ranked)"
|
|
783
1139
|
];
|
|
784
|
-
|
|
1140
|
+
const themes = reviewObs.reviewThemes;
|
|
1141
|
+
if (themes && themes.length > 0) {
|
|
1142
|
+
lines.push(
|
|
1143
|
+
`_Inferred from ${reviewObs.totalReviewComments ?? "?"} review comments \u2014 verify and re-order as needed_`
|
|
1144
|
+
);
|
|
1145
|
+
for (let i = 0; i < Math.min(themes.length, 8); i++) {
|
|
1146
|
+
const t = themes[i];
|
|
1147
|
+
lines.push(`${i + 1}. ${t.label} _(${t.count} comments, ${Math.round(t.ratio * 100)}%)_`);
|
|
1148
|
+
}
|
|
1149
|
+
} else {
|
|
1150
|
+
lines.push(
|
|
1151
|
+
"_[Fill in manually after reviewing the data below \u2014 re-order based on review comment patterns]_",
|
|
1152
|
+
"1. Correctness",
|
|
1153
|
+
"2. Naming",
|
|
1154
|
+
"3. Component / module boundaries",
|
|
1155
|
+
"4. Test coverage",
|
|
1156
|
+
"5. Consistency with existing patterns"
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
lines.push("", "### What they tend to ignore", "_[Fill in manually]_");
|
|
1160
|
+
lines.push("", "### Dealbreakers", "_[Fill in manually]_");
|
|
1161
|
+
lines.push("", "### Recurring questions / phrases");
|
|
1162
|
+
const recurringQs = reviewObs.recurringQuestions;
|
|
1163
|
+
if (recurringQs && recurringQs.length > 0) {
|
|
1164
|
+
for (const q of recurringQs.slice(0, 8)) {
|
|
1165
|
+
const truncated = q.length > 150 ? q.slice(0, 150) + "..." : q;
|
|
1166
|
+
lines.push(`- "${truncated}"`);
|
|
1167
|
+
}
|
|
1168
|
+
} else if (reviewObs.sampleComments && reviewObs.sampleComments.length > 0) {
|
|
785
1169
|
const questions = reviewObs.sampleComments.filter((c) => c.endsWith("?"));
|
|
786
1170
|
if (questions.length > 0) {
|
|
787
1171
|
for (const q of questions.slice(0, 5)) {
|
|
@@ -794,6 +1178,19 @@ function buildGhostMarkdown(input) {
|
|
|
794
1178
|
} else {
|
|
795
1179
|
lines.push("_[Fill in from sample comments below]_");
|
|
796
1180
|
}
|
|
1181
|
+
if (themes && themes.length > 0) {
|
|
1182
|
+
lines.push("");
|
|
1183
|
+
lines.push("### Review concern examples");
|
|
1184
|
+
lines.push("_Verbatim excerpts grouped by detected theme \u2014 use these to validate the ranking above_");
|
|
1185
|
+
for (const t of themes.slice(0, 5)) {
|
|
1186
|
+
if (t.exampleSnippets.length === 0) continue;
|
|
1187
|
+
lines.push("");
|
|
1188
|
+
lines.push(`**${t.label}** (${t.count} comments):`);
|
|
1189
|
+
for (const snippet of t.exampleSnippets) {
|
|
1190
|
+
lines.push(`> ${snippet}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
797
1194
|
lines.push("", "---", "", "## Communication Style", "");
|
|
798
1195
|
if (reviewObs.totalReviewComments != null) {
|
|
799
1196
|
lines.push(
|
|
@@ -808,9 +1205,28 @@ function buildGhostMarkdown(input) {
|
|
|
808
1205
|
}
|
|
809
1206
|
lines.push("");
|
|
810
1207
|
}
|
|
1208
|
+
lines.push("### Tone");
|
|
1209
|
+
const tone = reviewObs.toneProfile;
|
|
1210
|
+
if (tone) {
|
|
1211
|
+
const toneDesc = describeTone(tone);
|
|
1212
|
+
lines.push(
|
|
1213
|
+
`${toneDesc} _(inferred from comment language)_`,
|
|
1214
|
+
"",
|
|
1215
|
+
`- Praise ratio: ${Math.round(tone.praiseRatio * 100)}% of comments include positive reinforcement`,
|
|
1216
|
+
`- Explanation ratio: ${Math.round(tone.explanationRatio * 100)}% of comments explain "why"`,
|
|
1217
|
+
`- Suggestive ratio: ${Math.round(tone.suggestiveRatio * 100)}% offer alternatives ("consider", "what about")`,
|
|
1218
|
+
`- Directive ratio: ${Math.round(tone.directiveRatio * 100)}% use direct imperatives ("rename this", "remove this")`
|
|
1219
|
+
);
|
|
1220
|
+
if (tone.praiseExamples.length > 0) {
|
|
1221
|
+
lines.push("", "**Praise examples:**");
|
|
1222
|
+
for (const ex of tone.praiseExamples) {
|
|
1223
|
+
lines.push(`> ${ex.length > 200 ? ex.slice(0, 200) + "..." : ex}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
} else {
|
|
1227
|
+
lines.push("_[Fill in manually \u2014 direct? warm? collaborative? terse?]_");
|
|
1228
|
+
}
|
|
811
1229
|
lines.push(
|
|
812
|
-
"### Tone",
|
|
813
|
-
"_[Fill in manually \u2014 direct? warm? collaborative? terse?]_",
|
|
814
1230
|
"",
|
|
815
1231
|
"### Severity prefixes they use"
|
|
816
1232
|
);
|
|
@@ -821,10 +1237,17 @@ function buildGhostMarkdown(input) {
|
|
|
821
1237
|
} else {
|
|
822
1238
|
lines.push("_[Derived from comments above \u2014 fill in which they actually use]_");
|
|
823
1239
|
}
|
|
1240
|
+
lines.push("", "### Vocabulary / phrases they use");
|
|
1241
|
+
const phrases = reviewObs.recurringPhrases;
|
|
1242
|
+
if (phrases && phrases.length > 0) {
|
|
1243
|
+
lines.push("_Phrases that appear repeatedly across their review comments:_");
|
|
1244
|
+
for (const phrase of phrases.slice(0, 10)) {
|
|
1245
|
+
lines.push(`- "${phrase}"`);
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
lines.push("_[Fill in from sample comments below]_");
|
|
1249
|
+
}
|
|
824
1250
|
lines.push(
|
|
825
|
-
"",
|
|
826
|
-
"### Vocabulary / phrases they use",
|
|
827
|
-
"_[Fill in from sample comments below]_",
|
|
828
1251
|
"",
|
|
829
1252
|
"---",
|
|
830
1253
|
"",
|
|
@@ -865,15 +1288,18 @@ function buildGhostMarkdown(input) {
|
|
|
865
1288
|
if (gitObs.primaryExtensions || gitObs.primaryDirectories || gitObs.namingStyle || gitObs.fileTypeProfile) {
|
|
866
1289
|
lines.push("");
|
|
867
1290
|
}
|
|
868
|
-
|
|
1291
|
+
const personalObs = codeStyleObs.observations.filter((o) => !o.lintEnforceable);
|
|
1292
|
+
const lintObs = codeStyleObs.observations.filter((o) => o.lintEnforceable);
|
|
1293
|
+
if (personalObs.length > 0) {
|
|
869
1294
|
lines.push("### Detected Code Style Preferences");
|
|
1295
|
+
lines.push("_These reflect genuine contributor choices, not linter-enforced rules_");
|
|
870
1296
|
if (codeStyleObs.primaryLanguage) {
|
|
871
1297
|
lines.push(
|
|
872
1298
|
`_Primary language: ${codeStyleObs.primaryLanguage} \xB7 ${codeStyleObs.totalLinesAnalyzed} added lines analyzed_`
|
|
873
1299
|
);
|
|
874
1300
|
}
|
|
875
1301
|
lines.push("");
|
|
876
|
-
for (const obs of
|
|
1302
|
+
for (const obs of personalObs) {
|
|
877
1303
|
if (obs.confidence === "strong") {
|
|
878
1304
|
lines.push(`- **${obs.category}**: ${obs.observation}`);
|
|
879
1305
|
} else {
|
|
@@ -882,6 +1308,15 @@ function buildGhostMarkdown(input) {
|
|
|
882
1308
|
}
|
|
883
1309
|
lines.push("");
|
|
884
1310
|
}
|
|
1311
|
+
if (lintObs.length > 0) {
|
|
1312
|
+
lines.push("### Project-Level Patterns (likely lint-enforced)");
|
|
1313
|
+
lines.push("_These patterns are typically enforced by project linters/formatters \u2014 they describe the project, not the person_");
|
|
1314
|
+
lines.push("");
|
|
1315
|
+
for (const obs of lintObs) {
|
|
1316
|
+
lines.push(`- ~${obs.category}: ${obs.observation}~`);
|
|
1317
|
+
}
|
|
1318
|
+
lines.push("");
|
|
1319
|
+
}
|
|
885
1320
|
lines.push(
|
|
886
1321
|
"### Patterns they introduce / prefer"
|
|
887
1322
|
);
|
|
@@ -889,10 +1324,23 @@ function buildGhostMarkdown(input) {
|
|
|
889
1324
|
if (namingInsight) {
|
|
890
1325
|
lines.push(`- ${namingInsight}`);
|
|
891
1326
|
}
|
|
1327
|
+
if (personalObs.length > 0) {
|
|
1328
|
+
const strong = personalObs.filter((o) => o.confidence === "strong");
|
|
1329
|
+
for (const obs of strong.slice(0, 3)) {
|
|
1330
|
+
lines.push(`- ${obs.observation} _(inferred from code)_`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
892
1333
|
lines.push(
|
|
893
1334
|
"_[Fill in manually from code inspection and review comment themes]_",
|
|
894
1335
|
"",
|
|
895
|
-
"### Patterns they push back on"
|
|
1336
|
+
"### Patterns they push back on"
|
|
1337
|
+
);
|
|
1338
|
+
if (themes && themes.length > 0) {
|
|
1339
|
+
for (const t of themes.slice(0, 3)) {
|
|
1340
|
+
lines.push(`- Violations of ${t.label.toLowerCase()} _(${t.count} review comments)_`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
lines.push(
|
|
896
1344
|
"_[Fill in manually]_",
|
|
897
1345
|
"",
|
|
898
1346
|
"---",
|