@poltergeist-ai/cli 0.0.0 → 0.1.5

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