@harness-engineering/cli 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +9 -8
  2. package/dist/agents/skills/claude-code/pre-merge-brief/SKILL.md +86 -0
  3. package/dist/agents/skills/claude-code/pre-merge-brief/skill.yaml +67 -0
  4. package/dist/agents/skills/claude-code/product-advisor/SKILL.md +202 -0
  5. package/dist/agents/skills/claude-code/product-advisor/skill.yaml +74 -0
  6. package/dist/agents/skills/codex/harness-autopilot/SKILL.md +9 -8
  7. package/dist/agents/skills/codex/pre-merge-brief/SKILL.md +86 -0
  8. package/dist/agents/skills/codex/pre-merge-brief/skill.yaml +67 -0
  9. package/dist/agents/skills/codex/product-advisor/SKILL.md +202 -0
  10. package/dist/agents/skills/codex/product-advisor/skill.yaml +74 -0
  11. package/dist/agents/skills/cursor/harness-autopilot/SKILL.md +9 -8
  12. package/dist/agents/skills/cursor/pre-merge-brief/SKILL.md +86 -0
  13. package/dist/agents/skills/cursor/pre-merge-brief/skill.yaml +67 -0
  14. package/dist/agents/skills/cursor/product-advisor/SKILL.md +202 -0
  15. package/dist/agents/skills/cursor/product-advisor/skill.yaml +74 -0
  16. package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +9 -8
  17. package/dist/agents/skills/gemini-cli/pre-merge-brief/SKILL.md +86 -0
  18. package/dist/agents/skills/gemini-cli/pre-merge-brief/skill.yaml +67 -0
  19. package/dist/agents/skills/gemini-cli/product-advisor/SKILL.md +202 -0
  20. package/dist/agents/skills/gemini-cli/product-advisor/skill.yaml +74 -0
  21. package/dist/{agents-md-7OSNWU52.js → agents-md-7L4PKKOM.js} +4 -4
  22. package/dist/analyzer-XRCV63KC-HLTEIKAJ.js +26 -0
  23. package/dist/{architecture-2XYGJGT3.js → architecture-RCHFDI7E.js} +5 -5
  24. package/dist/{assess-project-3KPP2D6M.js → assess-project-YUDPNRS6.js} +5 -3
  25. package/dist/bin/harness-mcp.js +16 -16
  26. package/dist/bin/harness.js +27 -27
  27. package/dist/{check-phase-gate-46U5AAXE.js → check-phase-gate-56GS3A72.js} +5 -5
  28. package/dist/{chunk-MSBJ445U.js → chunk-4SKWGVYD.js} +5 -5
  29. package/dist/{chunk-CLI4K2CH.js → chunk-4U5LDFCY.js} +1 -1
  30. package/dist/{chunk-6YXCZV33.js → chunk-ABRYIEWJ.js} +1090 -714
  31. package/dist/{chunk-AK67S3O4.js → chunk-AU4YPEIE.js} +3 -3
  32. package/dist/{chunk-BV45354Q.js → chunk-DGVT4DBI.js} +9 -9
  33. package/dist/{chunk-363V6GPR.js → chunk-GLHLXTKC.js} +9 -9
  34. package/dist/{chunk-VTUINXWR.js → chunk-GSYBGOY3.js} +1 -1
  35. package/dist/{chunk-FZ4Q73HA.js → chunk-IGWJRAJT.js} +1 -1
  36. package/dist/{chunk-S2RQPPL6.js → chunk-N4AI2WFH.js} +1 -1
  37. package/dist/{chunk-HXMAIVV2.js → chunk-NCMJXBEO.js} +1 -1
  38. package/dist/{chunk-ZEYAOYJL.js → chunk-OZSKLJQW.js} +1 -1
  39. package/dist/{chunk-VTU3QOJT.js → chunk-P4NA5OQX.js} +1 -1
  40. package/dist/{chunk-WL3O2PKJ.js → chunk-RY2U77OY.js} +8 -8
  41. package/dist/{chunk-TTWNGOBT.js → chunk-RZQZXWEL.js} +32 -7
  42. package/dist/{chunk-KLPX55TI.js → chunk-UACYZCW4.js} +239 -146
  43. package/dist/{chunk-IYWT3JJW.js → chunk-VG4G3R2V.js} +1 -1
  44. package/dist/{chunk-PP6ZRL5T.js → chunk-WZJR6SOI.js} +536 -422
  45. package/dist/{chunk-PHX45JYN.js → chunk-XW5XVVMI.js} +2 -2
  46. package/dist/{chunk-MYVTYFQD.js → chunk-XZNCONFG.js} +1 -1
  47. package/dist/{chunk-GXCPDAS5.js → chunk-Y33P35U3.js} +26 -8
  48. package/dist/{chunk-XFHZ7GCI.js → chunk-Y5HPJMTM.js} +19 -3
  49. package/dist/{chunk-N55WOGN3.js → chunk-YJYUJSWK.js} +59 -21
  50. package/dist/{chunk-SPE7P4GL.js → chunk-YWRPQZL7.js} +1 -1
  51. package/dist/{chunk-6NCREWWM.js → chunk-YXOPQEFZ.js} +3 -3
  52. package/dist/{chunk-2N4OENGE.js → chunk-YYFSFSB3.js} +1 -1
  53. package/dist/{chunk-ERILXQBP.js → chunk-Z4XWWNYX.js} +615 -433
  54. package/dist/{chunk-HMW3C5DP.js → chunk-ZU6T7W3U.js} +2 -2
  55. package/dist/{ci-workflow-XJN4UO3Q.js → ci-workflow-TFAA5BK7.js} +4 -4
  56. package/dist/{dist-OWHMNL4W.js → dist-DWBTZXR5.js} +1 -1
  57. package/dist/{dist-IPVFYGY2.js → dist-MCTEX5QH.js} +5 -3
  58. package/dist/{docs-RSFZTNPG.js → docs-BBMAN3CY.js} +5 -5
  59. package/dist/{engine-DJIHY4JT.js → engine-R2YH74AL.js} +4 -4
  60. package/dist/{entropy-EAEKJKRM.js → entropy-5UP5N6AF.js} +7 -4
  61. package/dist/{feedback-JT5X3DBC.js → feedback-6MDCCFZL.js} +1 -1
  62. package/dist/{generate-agent-definitions-CC6KED3E.js → generate-agent-definitions-KKAN5DB5.js} +5 -5
  63. package/dist/{glob-helper-GLZAURJC.js → glob-helper-VR6UCZZB.js} +1 -1
  64. package/dist/{graph-loader-HHXN5FLP.js → graph-loader-STFGYKZH.js} +1 -1
  65. package/dist/hooks/sentinel-pre.js +15 -138
  66. package/dist/index.d.ts +61 -0
  67. package/dist/index.js +27 -27
  68. package/dist/{loader-UWNVTYBY.js → loader-2WT2ZE2T.js} +4 -4
  69. package/dist/{mcp-TCLNK7HG.js → mcp-5V4LPMB3.js} +16 -16
  70. package/dist/{performance-R6O3N3UB.js → performance-2N3CFXLR.js} +5 -5
  71. package/dist/{review-pipeline-MOOWXWSQ.js → review-pipeline-XZIGBU3Q.js} +4 -4
  72. package/dist/{runtime-RIWQUKBQ.js → runtime-POME3GTR.js} +4 -4
  73. package/dist/{scan-P7YFKB77.js → scan-IYSVRPD2.js} +1 -1
  74. package/dist/{security-U76C54W6.js → security-QLJLMSIW.js} +1 -1
  75. package/dist/{state-events-F44CUUZG.js → state-events-YY5KBZTC.js} +4 -4
  76. package/dist/{validate-IUAV7MOU.js → validate-VZORVK43.js} +5 -5
  77. package/dist/{validate-cross-check-M2ZM6RFY.js → validate-cross-check-OYKEDCST.js} +4 -4
  78. package/package.json +5 -4
  79. package/dist/analyzer-V633GUHY-MBCQWKDU.js +0 -26
@@ -1128,34 +1128,42 @@ function emptyIngestResult() {
1128
1128
  durationMs: 0
1129
1129
  };
1130
1130
  }
1131
- function withRetry(client, options) {
1132
- const maxRetries = options?.maxRetries ?? 3;
1133
- const baseDelayMs = options?.baseDelayMs ?? 1e3;
1134
- const maxDelayMs = options?.maxDelayMs ?? 3e4;
1135
- return async (url, requestOptions) => {
1136
- let lastError;
1137
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1138
- try {
1139
- const response = await client(url, requestOptions);
1140
- if (response.ok || !RETRYABLE_STATUSES.has(response.status ?? 0)) {
1141
- return response;
1142
- }
1143
- if (attempt === maxRetries) {
1144
- return response;
1145
- }
1146
- } catch (err) {
1147
- lastError = err instanceof Error ? err : new Error(String(err));
1148
- if (attempt === maxRetries) {
1149
- throw lastError;
1150
- }
1151
- }
1152
- const exponentialDelay = baseDelayMs * 2 ** attempt;
1153
- const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
1154
- const jitteredDelay = cappedDelay * (0.5 + Math.random() * 0.5);
1155
- await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
1131
+ function computeBackoffDelay(config, attempt) {
1132
+ const exponentialDelay = config.baseDelayMs * 2 ** attempt;
1133
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
1134
+ return cappedDelay * (0.5 + Math.random() * 0.5);
1135
+ }
1136
+ function shouldReturnResponse(response, attempt, maxRetries) {
1137
+ if (response.ok || !RETRYABLE_STATUSES.has(response.status ?? 0)) {
1138
+ return true;
1139
+ }
1140
+ return attempt === maxRetries;
1141
+ }
1142
+ async function executeWithRetry(client, url, requestOptions, config) {
1143
+ let lastError;
1144
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
1145
+ try {
1146
+ const response = await client(url, requestOptions);
1147
+ if (shouldReturnResponse(response, attempt, config.maxRetries)) {
1148
+ return response;
1149
+ }
1150
+ } catch (err) {
1151
+ lastError = err instanceof Error ? err : new Error(String(err));
1152
+ if (attempt === config.maxRetries) {
1153
+ throw lastError;
1154
+ }
1156
1155
  }
1157
- throw lastError ?? new Error("Request failed after retries");
1156
+ await new Promise((resolve) => setTimeout(resolve, computeBackoffDelay(config, attempt)));
1157
+ }
1158
+ throw lastError ?? new Error("Request failed after retries");
1159
+ }
1160
+ function withRetry(client, options) {
1161
+ const config = {
1162
+ maxRetries: options?.maxRetries ?? 3,
1163
+ baseDelayMs: options?.baseDelayMs ?? 1e3,
1164
+ maxDelayMs: options?.maxDelayMs ?? 3e4
1158
1165
  };
1166
+ return (url, requestOptions) => executeWithRetry(client, url, requestOptions, config);
1159
1167
  }
1160
1168
  function sanitizeExternalText(text, maxLength = 2e3) {
1161
1169
  let sanitized = text;
@@ -1739,7 +1747,7 @@ function parseAntiPatterns(content) {
1739
1747
  }
1740
1748
  return patterns;
1741
1749
  }
1742
- var NODE_TYPES, EDGE_TYPES, OBSERVABILITY_TYPES, CURRENT_SCHEMA_VERSION, NODE_STABILITY, GraphNodeSchema, GraphEdgeSchema, POISONED_KEYS, GraphStore, VectorStore, DEFAULT_TTL_MS, PackedSummaryCache, ContextQL, TEST_TYPES, DOC_TYPES, CODE_TYPES, DEFAULT_SKIP_DIRS, SKIP_METHOD_NAMES, FUNCTION_PATTERNS, CLASS_PATTERNS, INTERFACE_PATTERNS, METHOD_PATTERNS, GO_METHOD_PATTERN, VARIABLE_PATTERNS, SUPPORTED_EXTENSIONS, SKIP_EXTENSIONS, CodeIngestor, execFileAsync, GitIngestor, TopologicalLinker, CODE_NODE_TYPES, DOCS_OWNED_BY_OTHER_INGESTORS, KnowledgeIngestor, BUG_TRACK_CATEGORIES, KNOWLEDGE_TRACK_CATEGORIES, ISO_DATE, SolutionBaseSchema, SolutionDocFrontmatterSchema, BUSINESS_KNOWLEDGE_TYPES, GOVERNS_SOURCE_TYPES, CODE_NODE_TYPES2, MEASURABLE_TYPES, STRATEGY_REQUIRED_SECTIONS, STRATEGY_OPTIONAL_SECTIONS, STRATEGY_KNOWN_SECTIONS, STRATEGY_PLACEHOLDER_RE, BusinessKnowledgeIngestor, CODE_NODE_TYPES3, ARCHITECTURE_ADR_H1, DecisionIngestor, REQUIREMENT_SECTIONS, SECTION_HEADING_RE, NUMBERED_ITEM_RE, CODE_NODE_TYPES4, RequirementIngestor, DEFAULT_PATTERNS, DEFAULT_BLOCKLIST, CODE_EXTENSIONS, MermaidParser, D2Parser, PlantUmlParser, DIAGRAM_EXTENSIONS, DiagramParser, HEURISTIC_PATTERNS, SCANNABLE_TYPES, KnowledgeLinker, StructuralDriftDetector, BUSINESS_NODE_TYPES, KnowledgeStagingAggregator, EXT_TO_LANGUAGE, SKIP_EXTENSIONS2, EXTRACTOR_EDGE_TYPE, ExtractionRunner, PATTERNS, TestDescriptionExtractor, EnumConstantExtractor, ValidationRuleExtractor, ApiPathExtractor, ALL_EXTRACTORS, ImageAnalysisExtractor, KNOWLEDGE_NODE_TYPES, SIMILARITY_THRESHOLD, SEVERITY_MAP, ContradictionDetector, KNOWLEDGE_TYPES, CODE_TYPES2, KNOWLEDGE_EDGE_TYPES, CoverageScorer, VALID_BUSINESS_TYPES, DEFAULT_MAX_DOCS, MAX_COLLISION_SUFFIX, KnowledgeDocMaterializer, BUSINESS_NODE_TYPES2, SNAPSHOT_NODE_TYPES, KnowledgePipelineRunner, CODE_NODE_TYPES5, RETRYABLE_STATUSES, SANITIZE_RULES, SyncManager, SUMMARIZE_PROMPT, JiraConnector, SlackConnector, ConfluenceConnector, CIConnector, CONSTRAINT_KEYWORDS, FigmaConnector, MIN_CONTENT_LENGTH, CONCEPT_ITEM_TYPES, MiroConnector, STOP_WORDS, FusionLayer, CODE_NODE_TYPES6, GraphEntropyAdapter, GraphComplexityAdapter, GraphCouplingAdapter, DEFAULT_THRESHOLD, DEFAULT_METRICS, RECOGNIZED_METRICS, GraphAnomalyAdapter, INTENTS, SIGNAL_WEIGHTS, INTENT_SIGNALS, IntentClassifier, INTENT_KEYWORDS, STOP_WORDS2, PASCAL_OR_CAMEL_RE, FILE_PATH_RE, QUOTED_RE, EntityExtractor, EntityResolver, ResponseFormatter, CompositeProbabilityStrategy, DEFAULT_PROBABILITY_FLOOR, DEFAULT_MAX_DEPTH, CascadeSimulator, ENTITY_REQUIRED_INTENTS, classifier, extractor, formatter, PHASE_NODE_TYPES, CODE_NODE_TYPES7, Assembler, GraphConstraintAdapter, DesignIngestor, CODE_PREFIX_LABELS, DesignConstraintAdapter, GraphFeedbackAdapter, DEFAULT_EDGE_TYPES, TaskIndependenceAnalyzer, ConflictPredictor, VERSION;
1750
+ var NODE_TYPES, EDGE_TYPES, OBSERVABILITY_TYPES, CURRENT_SCHEMA_VERSION, NODE_STABILITY, GraphNodeSchema, GraphEdgeSchema, POISONED_KEYS, GraphStore, VectorStore, DEFAULT_TTL_MS, PackedSummaryCache, ContextQL, TEST_TYPES, DOC_TYPES, CODE_TYPES, DEFAULT_SKIP_DIRS, SKIP_METHOD_NAMES, FUNCTION_PATTERNS, CLASS_PATTERNS, INTERFACE_PATTERNS, METHOD_PATTERNS, GO_METHOD_PATTERN, VARIABLE_PATTERNS, SUPPORTED_EXTENSIONS, SKIP_EXTENSIONS, CodeIngestor, execFileAsync, GitIngestor, TopologicalLinker, CODE_NODE_TYPES, DOCS_OWNED_BY_OTHER_INGESTORS, KnowledgeIngestor, BUG_TRACK_CATEGORIES, KNOWLEDGE_TRACK_CATEGORIES, ISO_DATE, SolutionBaseSchema, SolutionDocFrontmatterSchema, BUSINESS_KNOWLEDGE_TYPES, GOVERNS_SOURCE_TYPES, CODE_NODE_TYPES2, MEASURABLE_TYPES, STRATEGY_REQUIRED_SECTIONS, STRATEGY_OPTIONAL_SECTIONS, STRATEGY_KNOWN_SECTIONS, STRATEGY_PLACEHOLDER_RE, BusinessKnowledgeIngestor, CODE_NODE_TYPES3, ARCHITECTURE_ADR_H1, DecisionIngestor, REQUIREMENT_SECTIONS, SECTION_HEADING_RE, NUMBERED_ITEM_RE, CODE_NODE_TYPES4, RequirementIngestor, DEFAULT_PATTERNS, DEFAULT_BLOCKLIST, CODE_EXTENSIONS, MermaidParser, D2Parser, PlantUmlParser, DIAGRAM_EXTENSIONS, DiagramParser, HEURISTIC_PATTERNS, SCANNABLE_TYPES, KnowledgeLinker, StructuralDriftDetector, BUSINESS_NODE_TYPES, KnowledgeStagingAggregator, EXT_TO_LANGUAGE, SKIP_EXTENSIONS2, EXTRACTOR_EDGE_TYPE, ExtractionRunner, PATTERNS, RUST_FN_RE, JAVA_DISPLAY_RE, JAVA_METHOD_RE, TestDescriptionExtractor, TS_ENUM_RE, TS_AS_CONST_RE, TS_UNION_RE, JS_FREEZE_RE, JS_CONST_OBJECT_RE, JS_UPPER_KEY_RE, PY_ENUM_RE, PY_LITERAL_RE, GO_TYPE_RE, GO_CONST_BLOCK_RE, GO_IOTA_RE, GO_TYPED_RE, GO_BARE_RE, RUST_ENUM_RE, JAVA_ENUM_RE, EnumConstantExtractor, ValidationRuleExtractor, ApiPathExtractor, ALL_EXTRACTORS, ImageAnalysisExtractor, KNOWLEDGE_NODE_TYPES, SIMILARITY_THRESHOLD, SEVERITY_MAP, ContradictionDetector, KNOWLEDGE_TYPES, CODE_TYPES2, KNOWLEDGE_EDGE_TYPES, CoverageScorer, VALID_BUSINESS_TYPES, DEFAULT_MAX_DOCS, MAX_COLLISION_SUFFIX, KnowledgeDocMaterializer, BUSINESS_NODE_TYPES2, SNAPSHOT_NODE_TYPES, KnowledgePipelineRunner, CODE_NODE_TYPES5, RETRYABLE_STATUSES, SANITIZE_RULES, SyncManager, SUMMARIZE_PROMPT, JiraConnector, SlackConnector, ConfluenceConnector, CIConnector, CONSTRAINT_KEYWORDS, FigmaConnector, MIN_CONTENT_LENGTH, CONCEPT_ITEM_TYPES, MiroConnector, STOP_WORDS, FusionLayer, CODE_NODE_TYPES6, GraphEntropyAdapter, GraphComplexityAdapter, GraphCouplingAdapter, DEFAULT_THRESHOLD, DEFAULT_METRICS, RECOGNIZED_METRICS, GraphAnomalyAdapter, INTENTS, SIGNAL_WEIGHTS, INTENT_SIGNALS, IntentClassifier, INTENT_KEYWORDS, STOP_WORDS2, PASCAL_OR_CAMEL_RE, FILE_PATH_RE, QUOTED_RE, EntityExtractor, EntityResolver, ResponseFormatter, CompositeProbabilityStrategy, DEFAULT_PROBABILITY_FLOOR, DEFAULT_MAX_DEPTH, CascadeSimulator, ENTITY_REQUIRED_INTENTS, classifier, extractor, formatter, PHASE_NODE_TYPES, CODE_NODE_TYPES7, Assembler, GraphConstraintAdapter, DesignIngestor, CODE_PREFIX_LABELS, DesignConstraintAdapter, GraphFeedbackAdapter, DEFAULT_EDGE_TYPES, TaskIndependenceAnalyzer, ConflictPredictor, VERSION;
1743
1751
  var init_dist = __esm({
1744
1752
  "../graph/dist/index.mjs"() {
1745
1753
  NODE_TYPES = [
@@ -4622,29 +4630,41 @@ var init_dist = __esm({
4622
4630
  };
4623
4631
  StructuralDriftDetector = class {
4624
4632
  detect(current, fresh) {
4625
- const findings = [];
4626
4633
  const currentById = new Map(current.entries.map((e) => [e.id, e]));
4627
4634
  const freshById = new Map(fresh.entries.map((e) => [e.id, e]));
4635
+ const findings = [
4636
+ ...this.findNew(currentById, freshById),
4637
+ ...this.findStale(currentById, freshById),
4638
+ ...this.findDrifted(currentById, freshById)
4639
+ ];
4640
+ findings.push(...this.findContradicting(fresh.entries, findings));
4641
+ const totalEntries = (/* @__PURE__ */ new Set([...currentById.keys(), ...freshById.keys()])).size;
4642
+ const driftScore = totalEntries > 0 ? findings.length / totalEntries : 0;
4643
+ return { findings, driftScore, summary: this.summarize(findings) };
4644
+ }
4645
+ /** 1. NEW: entries in fresh but not in current */
4646
+ findNew(currentById, freshById) {
4647
+ const findings = [];
4628
4648
  for (const [id, entry] of freshById) {
4629
4649
  if (!currentById.has(id)) {
4630
- findings.push({
4631
- entryId: id,
4632
- classification: "new",
4633
- fresh: entry,
4634
- severity: "low"
4635
- });
4650
+ findings.push({ entryId: id, classification: "new", fresh: entry, severity: "low" });
4636
4651
  }
4637
4652
  }
4653
+ return findings;
4654
+ }
4655
+ /** 2. STALE: entries in current but not in fresh */
4656
+ findStale(currentById, freshById) {
4657
+ const findings = [];
4638
4658
  for (const [id, entry] of currentById) {
4639
4659
  if (!freshById.has(id)) {
4640
- findings.push({
4641
- entryId: id,
4642
- classification: "stale",
4643
- current: entry,
4644
- severity: "high"
4645
- });
4660
+ findings.push({ entryId: id, classification: "stale", current: entry, severity: "high" });
4646
4661
  }
4647
4662
  }
4663
+ return findings;
4664
+ }
4665
+ /** 3. DRIFTED: entries in both but contentHash differs */
4666
+ findDrifted(currentById, freshById) {
4667
+ const findings = [];
4648
4668
  for (const [id, freshEntry] of freshById) {
4649
4669
  const currentEntry = currentById.get(id);
4650
4670
  if (currentEntry && currentEntry.contentHash !== freshEntry.contentHash) {
@@ -4657,43 +4677,56 @@ var init_dist = __esm({
4657
4677
  });
4658
4678
  }
4659
4679
  }
4680
+ return findings;
4681
+ }
4682
+ /** 4. CONTRADICTING: same entity name from different sources with different content */
4683
+ findContradicting(entries, existing) {
4684
+ const result = [];
4685
+ const alreadyContradicting = new Set(
4686
+ existing.filter((f) => f.classification === "contradicting").map((f) => f.entryId)
4687
+ );
4688
+ for (const [, group] of this.groupByName(entries)) {
4689
+ const finding = this.contradictionForGroup(group, alreadyContradicting);
4690
+ if (finding) {
4691
+ result.push(finding);
4692
+ alreadyContradicting.add(finding.entryId);
4693
+ }
4694
+ }
4695
+ return result;
4696
+ }
4697
+ groupByName(entries) {
4660
4698
  const byName = /* @__PURE__ */ new Map();
4661
- for (const entry of fresh.entries) {
4699
+ for (const entry of entries) {
4662
4700
  const group = byName.get(entry.name) ?? [];
4663
4701
  group.push(entry);
4664
4702
  byName.set(entry.name, group);
4665
4703
  }
4666
- for (const [, group] of byName) {
4667
- if (group.length > 1) {
4668
- const sources = new Set(group.map((e) => e.source));
4669
- const hashes = new Set(group.map((e) => e.contentHash));
4670
- if (sources.size > 1 && hashes.size > 1) {
4671
- const alreadyContradicting = new Set(
4672
- findings.filter((f) => f.classification === "contradicting").map((f) => f.entryId)
4673
- );
4674
- for (const entry of group) {
4675
- if (!alreadyContradicting.has(entry.id)) {
4676
- findings.push({
4677
- entryId: entry.id,
4678
- classification: "contradicting",
4679
- fresh: entry,
4680
- severity: "critical"
4681
- });
4682
- break;
4683
- }
4684
- }
4685
- }
4704
+ return byName;
4705
+ }
4706
+ contradictionForGroup(group, alreadyContradicting) {
4707
+ if (group.length <= 1) return void 0;
4708
+ const sources = new Set(group.map((e) => e.source));
4709
+ const hashes = new Set(group.map((e) => e.contentHash));
4710
+ if (sources.size <= 1 || hashes.size <= 1) return void 0;
4711
+ for (const entry of group) {
4712
+ if (!alreadyContradicting.has(entry.id)) {
4713
+ return {
4714
+ entryId: entry.id,
4715
+ classification: "contradicting",
4716
+ fresh: entry,
4717
+ severity: "critical"
4718
+ };
4686
4719
  }
4687
4720
  }
4688
- const totalEntries = (/* @__PURE__ */ new Set([...currentById.keys(), ...freshById.keys()])).size;
4689
- const driftScore = totalEntries > 0 ? findings.length / totalEntries : 0;
4690
- const summary = {
4721
+ return void 0;
4722
+ }
4723
+ summarize(findings) {
4724
+ return {
4691
4725
  new: findings.filter((f) => f.classification === "new").length,
4692
4726
  drifted: findings.filter((f) => f.classification === "drifted").length,
4693
4727
  stale: findings.filter((f) => f.classification === "stale").length,
4694
4728
  contradicting: findings.filter((f) => f.classification === "contradicting").length
4695
4729
  };
4696
- return { findings, driftScore, summary };
4697
4730
  }
4698
4731
  };
4699
4732
  BUSINESS_NODE_TYPES = [
@@ -4738,6 +4771,29 @@ var init_dist = __esm({
4738
4771
  return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
4739
4772
  }
4740
4773
  async generateGapReport(knowledgeDir, store) {
4774
+ const { domainDocNames, domainEntryCounts, totalEntries } = await this.collectDocumentedEntries(
4775
+ knowledgeDir,
4776
+ store
4777
+ );
4778
+ if (!store) {
4779
+ return this.buildEntryOnlyReport(domainEntryCounts, totalEntries);
4780
+ }
4781
+ const extractedByDomain = this.collectExtractedByDomain(store);
4782
+ const { domains, totalExtracted, totalGaps } = this.buildDomainCoverage(
4783
+ domainEntryCounts,
4784
+ extractedByDomain,
4785
+ domainDocNames
4786
+ );
4787
+ return {
4788
+ domains,
4789
+ totalEntries,
4790
+ totalExtracted,
4791
+ totalGaps,
4792
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
4793
+ };
4794
+ }
4795
+ /** Step 1-2: count `.md` entries per domain, capturing normalized names when a store is present. */
4796
+ async collectDocumentedEntries(knowledgeDir, store) {
4741
4797
  const domainDocNames = /* @__PURE__ */ new Map();
4742
4798
  const domainEntryCounts = /* @__PURE__ */ new Map();
4743
4799
  let totalEntries = 0;
@@ -4748,33 +4804,41 @@ var init_dist = __esm({
4748
4804
  const domainPath = path9.join(knowledgeDir, dir.name);
4749
4805
  const files = await fs8.readdir(domainPath);
4750
4806
  const mdFiles = files.filter((f) => f.endsWith(".md"));
4751
- const entryCount = mdFiles.length;
4752
- totalEntries += entryCount;
4753
- domainEntryCounts.set(dir.name, entryCount);
4807
+ totalEntries += mdFiles.length;
4808
+ domainEntryCounts.set(dir.name, mdFiles.length);
4754
4809
  if (store) {
4755
- const names = [];
4756
- for (const file of mdFiles) {
4757
- const name = await this.extractDocName(path9.join(domainPath, file));
4758
- names.push(name.toLowerCase().trim());
4759
- }
4760
- domainDocNames.set(dir.name, names);
4810
+ domainDocNames.set(dir.name, await this.extractDocNames(domainPath, mdFiles));
4761
4811
  }
4762
4812
  }
4763
4813
  } catch {
4764
4814
  }
4765
- if (!store) {
4766
- const domains2 = [];
4767
- for (const [domain, entryCount] of domainEntryCounts) {
4768
- domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
4769
- }
4770
- return {
4771
- domains: domains2,
4772
- totalEntries,
4773
- totalExtracted: 0,
4774
- totalGaps: 0,
4775
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
4776
- };
4815
+ return { domainDocNames, domainEntryCounts, totalEntries };
4816
+ }
4817
+ /** Extract and normalize the documented title from each markdown file in a domain. */
4818
+ async extractDocNames(domainPath, mdFiles) {
4819
+ const names = [];
4820
+ for (const file of mdFiles) {
4821
+ const name = await this.extractDocName(path9.join(domainPath, file));
4822
+ names.push(name.toLowerCase().trim());
4777
4823
  }
4824
+ return names;
4825
+ }
4826
+ /** Step 3: backward-compatible report when no store is available. */
4827
+ buildEntryOnlyReport(domainEntryCounts, totalEntries) {
4828
+ const domains = [];
4829
+ for (const [domain, entryCount] of domainEntryCounts) {
4830
+ domains.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
4831
+ }
4832
+ return {
4833
+ domains,
4834
+ totalEntries,
4835
+ totalExtracted: 0,
4836
+ totalGaps: 0,
4837
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
4838
+ };
4839
+ }
4840
+ /** Step 4: query store for all business nodes, group by domain, deduplicate by name. */
4841
+ collectExtractedByDomain(store) {
4778
4842
  const extractedByDomain = /* @__PURE__ */ new Map();
4779
4843
  for (const nodeType of BUSINESS_NODE_TYPES) {
4780
4844
  const nodes = store.findNodes({ type: nodeType });
@@ -4786,15 +4850,22 @@ var init_dist = __esm({
4786
4850
  }
4787
4851
  }
4788
4852
  for (const [domain, nodes] of extractedByDomain) {
4789
- const seen = /* @__PURE__ */ new Set();
4790
- const deduped = nodes.filter((n) => {
4791
- const key = n.name.toLowerCase().trim();
4792
- if (seen.has(key)) return false;
4793
- seen.add(key);
4794
- return true;
4795
- });
4796
- extractedByDomain.set(domain, deduped);
4853
+ extractedByDomain.set(domain, this.dedupeByName(nodes));
4797
4854
  }
4855
+ return extractedByDomain;
4856
+ }
4857
+ /** Keep the first occurrence of each normalized name, preserving order. */
4858
+ dedupeByName(nodes) {
4859
+ const seen = /* @__PURE__ */ new Set();
4860
+ return nodes.filter((n) => {
4861
+ const key = n.name.toLowerCase().trim();
4862
+ if (seen.has(key)) return false;
4863
+ seen.add(key);
4864
+ return true;
4865
+ });
4866
+ }
4867
+ /** Step 5: build per-domain coverage with gap analysis and roll up totals. */
4868
+ buildDomainCoverage(domainEntryCounts, extractedByDomain, domainDocNames) {
4798
4869
  const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
4799
4870
  const domains = [];
4800
4871
  let totalExtracted = 0;
@@ -4805,29 +4876,28 @@ var init_dist = __esm({
4805
4876
  const extractedCount = extractedNodes.length;
4806
4877
  totalExtracted += extractedCount;
4807
4878
  const docNames = domainDocNames.get(domain) ?? [];
4808
- const gapEntries = [];
4809
- for (const node of extractedNodes) {
4810
- const normalizedName = node.name.toLowerCase().trim();
4811
- if (!docNames.includes(normalizedName)) {
4812
- gapEntries.push({
4813
- nodeId: node.id,
4814
- name: node.name,
4815
- nodeType: node.type,
4816
- source: node.metadata?.source ?? "unknown",
4817
- hasContent: Boolean(node.content && node.content.trim().length >= 10)
4818
- });
4819
- }
4820
- }
4879
+ const gapEntries = this.computeGapEntries(extractedNodes, docNames);
4821
4880
  totalGaps += gapEntries.length;
4822
4881
  domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
4823
4882
  }
4824
- return {
4825
- domains,
4826
- totalEntries,
4827
- totalExtracted,
4828
- totalGaps,
4829
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
4830
- };
4883
+ return { domains, totalExtracted, totalGaps };
4884
+ }
4885
+ /** Collect extracted nodes that have no matching documented name. */
4886
+ computeGapEntries(extractedNodes, docNames) {
4887
+ const gapEntries = [];
4888
+ for (const node of extractedNodes) {
4889
+ const normalizedName = node.name.toLowerCase().trim();
4890
+ if (!docNames.includes(normalizedName)) {
4891
+ gapEntries.push({
4892
+ nodeId: node.id,
4893
+ name: node.name,
4894
+ nodeType: node.type,
4895
+ source: node.metadata?.source ?? "unknown",
4896
+ hasContent: Boolean(node.content && node.content.trim().length >= 10)
4897
+ });
4898
+ }
4899
+ }
4900
+ return gapEntries;
4831
4901
  }
4832
4902
  async writeGapReport(report) {
4833
4903
  const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
@@ -5060,6 +5130,9 @@ var init_dist = __esm({
5060
5130
  /@Test\s*\n\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|[\w<>]+)\s+(\w+)\s*\(/g
5061
5131
  ]
5062
5132
  };
5133
+ RUST_FN_RE = /(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/;
5134
+ JAVA_DISPLAY_RE = /@DisplayName\s*\(\s*"([^"]+)"\s*\)/;
5135
+ JAVA_METHOD_RE = /^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:void|[\w<>[\]]+)\s+(\w+)\s*\(/;
5063
5136
  TestDescriptionExtractor = class {
5064
5137
  name = "test-descriptions";
5065
5138
  supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
@@ -5089,83 +5162,87 @@ var init_dist = __esm({
5089
5162
  const records = [];
5090
5163
  const describeStack = [];
5091
5164
  for (let i = 0; i < lines.length; i++) {
5092
- const line = lines[i];
5093
- const describeMatch = line.match(/describe\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
5094
- if (describeMatch) {
5095
- describeStack.push(describeMatch[2]);
5096
- }
5097
- const itMatch = line.match(/(?:it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
5098
- if (itMatch) {
5099
- const testName = itMatch[2];
5100
- const fullPath = [...describeStack, testName].join(" > ");
5101
- const patternKey = fullPath;
5102
- records.push({
5103
- id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
5104
- extractor: "test-descriptions",
5105
- language,
5106
- filePath,
5107
- line: i + 1,
5108
- nodeType: "business_rule",
5109
- name: testName,
5110
- content: fullPath,
5111
- confidence: 0.7,
5112
- metadata: {
5113
- suite: describeStack.length > 0 ? describeStack[describeStack.length - 1] : void 0,
5114
- framework: "vitest"
5115
- }
5116
- });
5117
- }
5118
- if (line.match(/^\s*\}\s*\)\s*;?\s*$/) && describeStack.length > 0) {
5119
- describeStack.pop();
5120
- }
5165
+ this.scanJsTsLine(lines[i], i, language, filePath, describeStack, records);
5121
5166
  }
5122
5167
  return records;
5123
5168
  }
5169
+ scanJsTsLine(line, index, language, filePath, describeStack, records) {
5170
+ const describeMatch = line.match(/describe\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
5171
+ if (describeMatch) describeStack.push(describeMatch[2]);
5172
+ const itMatch = line.match(/(?:it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
5173
+ if (itMatch) {
5174
+ records.push(this.buildJsTsRecord(itMatch[2], index, language, filePath, describeStack));
5175
+ }
5176
+ if (/^\s*\}\s*\)\s*;?\s*$/.test(line) && describeStack.length > 0) describeStack.pop();
5177
+ }
5178
+ buildJsTsRecord(testName, index, language, filePath, describeStack) {
5179
+ const fullPath = [...describeStack, testName].join(" > ");
5180
+ const patternKey = fullPath;
5181
+ return {
5182
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
5183
+ extractor: "test-descriptions",
5184
+ language,
5185
+ filePath,
5186
+ line: index + 1,
5187
+ nodeType: "business_rule",
5188
+ name: testName,
5189
+ content: fullPath,
5190
+ confidence: 0.7,
5191
+ metadata: {
5192
+ suite: describeStack.length > 0 ? describeStack[describeStack.length - 1] : void 0,
5193
+ framework: "vitest"
5194
+ }
5195
+ };
5196
+ }
5124
5197
  extractPython(_content, filePath, language, lines) {
5125
5198
  const records = [];
5126
5199
  let currentClass;
5127
5200
  for (let i = 0; i < lines.length; i++) {
5128
5201
  const line = lines[i];
5129
5202
  const classMatch = line.match(/^class\s+(Test\w+)/);
5130
- if (classMatch) {
5131
- currentClass = classMatch[1];
5132
- }
5203
+ if (classMatch) currentClass = classMatch[1];
5133
5204
  const funcMatch = line.match(/^\s*def\s+(test_\w+)\s*\(/);
5134
5205
  if (funcMatch) {
5135
- const testName = funcMatch[1];
5136
- const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
5137
- let docstring;
5138
- if (i + 1 < lines.length) {
5139
- const nextLine = lines[i + 1].trim();
5140
- const docMatch = nextLine.match(/^"""(.+?)"""/);
5141
- if (docMatch) {
5142
- docstring = docMatch[1];
5143
- }
5144
- }
5145
- const patternKey = currentClass ? `${currentClass}.${testName}` : testName;
5146
- records.push({
5147
- id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
5148
- extractor: "test-descriptions",
5149
- language,
5150
- filePath,
5151
- line: i + 1,
5152
- nodeType: "business_rule",
5153
- name: docstring ?? humanName,
5154
- content: patternKey,
5155
- confidence: docstring ? 0.7 : 0.5,
5156
- metadata: {
5157
- suite: currentClass,
5158
- framework: "pytest",
5159
- functionName: testName
5160
- }
5161
- });
5162
- }
5163
- if (currentClass && /^\S/.test(line) && !line.startsWith("class ") && !line.startsWith("#") && line.trim() !== "") {
5164
- currentClass = void 0;
5206
+ records.push(
5207
+ this.buildPythonRecord(funcMatch[1], i, language, filePath, currentClass, lines)
5208
+ );
5165
5209
  }
5210
+ if (this.isPythonClassReset(line, currentClass)) currentClass = void 0;
5166
5211
  }
5167
5212
  return records;
5168
5213
  }
5214
+ isPythonClassReset(line, currentClass) {
5215
+ return Boolean(
5216
+ currentClass && /^\S/.test(line) && !line.startsWith("class ") && !line.startsWith("#") && line.trim() !== ""
5217
+ );
5218
+ }
5219
+ findPythonDocstring(lines, index) {
5220
+ if (index + 1 >= lines.length) return void 0;
5221
+ const nextLine = lines[index + 1].trim();
5222
+ const docMatch = nextLine.match(/^"""(.+?)"""/);
5223
+ return docMatch ? docMatch[1] : void 0;
5224
+ }
5225
+ buildPythonRecord(testName, index, language, filePath, currentClass, lines) {
5226
+ const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
5227
+ const docstring = this.findPythonDocstring(lines, index);
5228
+ const patternKey = currentClass ? `${currentClass}.${testName}` : testName;
5229
+ return {
5230
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
5231
+ extractor: "test-descriptions",
5232
+ language,
5233
+ filePath,
5234
+ line: index + 1,
5235
+ nodeType: "business_rule",
5236
+ name: docstring ?? humanName,
5237
+ content: patternKey,
5238
+ confidence: docstring ? 0.7 : 0.5,
5239
+ metadata: {
5240
+ suite: currentClass,
5241
+ framework: "pytest",
5242
+ functionName: testName
5243
+ }
5244
+ };
5245
+ }
5169
5246
  extractGo(_content, filePath, language, lines) {
5170
5247
  const records = [];
5171
5248
  let currentTest;
@@ -5214,87 +5291,105 @@ var init_dist = __esm({
5214
5291
  extractRust(_content, filePath, language, lines) {
5215
5292
  const records = [];
5216
5293
  for (let i = 0; i < lines.length; i++) {
5217
- const line = lines[i];
5218
- if (line.trim() === "#[test]") {
5219
- for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
5220
- const fnLine = lines[j];
5221
- const fnMatch = fnLine.match(/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
5222
- if (fnMatch) {
5223
- const testName = fnMatch[1];
5224
- const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
5225
- let docComment;
5226
- if (i > 0) {
5227
- const prevLine = lines[i - 1].trim();
5228
- const docMatch = prevLine.match(/^\/\/\/\s*(.+)/);
5229
- if (docMatch) {
5230
- docComment = docMatch[1];
5231
- }
5232
- }
5233
- records.push({
5234
- id: `extracted:test-descriptions:${hash(filePath + ":" + testName)}`,
5235
- extractor: "test-descriptions",
5236
- language,
5237
- filePath,
5238
- line: j + 1,
5239
- nodeType: "business_rule",
5240
- name: docComment ?? humanName,
5241
- content: testName,
5242
- confidence: docComment ? 0.7 : 0.5,
5243
- metadata: { framework: "rust-test" }
5244
- });
5245
- break;
5246
- }
5247
- }
5248
- }
5294
+ if (lines[i].trim() !== "#[test]") continue;
5295
+ const record = this.buildRustRecord(lines, i, language, filePath);
5296
+ if (record) records.push(record);
5249
5297
  }
5250
5298
  return records;
5251
5299
  }
5300
+ buildRustRecord(lines, testIdx, language, filePath) {
5301
+ for (let j = testIdx + 1; j < lines.length && j <= testIdx + 3; j++) {
5302
+ const fnMatch = lines[j].match(RUST_FN_RE);
5303
+ if (!fnMatch) continue;
5304
+ const testName = fnMatch[1];
5305
+ const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
5306
+ const docComment = this.findRustDocComment(lines, testIdx);
5307
+ return {
5308
+ id: `extracted:test-descriptions:${hash(filePath + ":" + testName)}`,
5309
+ extractor: "test-descriptions",
5310
+ language,
5311
+ filePath,
5312
+ line: j + 1,
5313
+ nodeType: "business_rule",
5314
+ name: docComment ?? humanName,
5315
+ content: testName,
5316
+ confidence: docComment ? 0.7 : 0.5,
5317
+ metadata: { framework: "rust-test" }
5318
+ };
5319
+ }
5320
+ return void 0;
5321
+ }
5322
+ findRustDocComment(lines, testIdx) {
5323
+ if (testIdx <= 0) return void 0;
5324
+ const prevLine = lines[testIdx - 1].trim();
5325
+ const docMatch = prevLine.match(/^\/\/\/\s*(.+)/);
5326
+ return docMatch ? docMatch[1] : void 0;
5327
+ }
5252
5328
  extractJava(_content, filePath, language, lines) {
5253
5329
  const records = [];
5254
5330
  for (let i = 0; i < lines.length; i++) {
5255
- const line = lines[i];
5256
- const testMatch = line.match(/@Test\s*$/);
5257
- if (testMatch) {
5258
- let displayName;
5259
- for (let k = Math.max(0, i - 3); k < i; k++) {
5260
- const prevLine = lines[k];
5261
- const dm = prevLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
5262
- if (dm) displayName = dm[1];
5263
- }
5264
- for (let j = i + 1; j < lines.length && j <= i + 5; j++) {
5265
- const scanLine = lines[j];
5266
- const adjacentDisplay = scanLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
5267
- if (adjacentDisplay) {
5268
- displayName = adjacentDisplay[1];
5269
- continue;
5270
- }
5271
- const methodMatch = scanLine.match(
5272
- /^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:void|[\w<>[\]]+)\s+(\w+)\s*\(/
5273
- );
5274
- if (methodMatch) {
5275
- const methodName = methodMatch[1];
5276
- const name = displayName ?? methodName;
5277
- const patternKey = displayName ?? methodName;
5278
- records.push({
5279
- id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
5280
- extractor: "test-descriptions",
5281
- language,
5282
- filePath,
5283
- line: j + 1,
5284
- nodeType: "business_rule",
5285
- name,
5286
- content: patternKey,
5287
- confidence: displayName ? 0.7 : 0.5,
5288
- metadata: { framework: "junit5" }
5289
- });
5290
- break;
5291
- }
5292
- }
5293
- }
5331
+ if (!/@Test\s*$/.test(lines[i])) continue;
5332
+ const record = this.buildJavaRecord(lines, i, language, filePath);
5333
+ if (record) records.push(record);
5294
5334
  }
5295
5335
  return records;
5296
5336
  }
5337
+ buildJavaRecord(lines, testIdx, language, filePath) {
5338
+ let displayName = this.findJavaDisplayNameBefore(lines, testIdx);
5339
+ for (let j = testIdx + 1; j < lines.length && j <= testIdx + 5; j++) {
5340
+ const scanLine = lines[j];
5341
+ const adjacentDisplay = scanLine.match(JAVA_DISPLAY_RE);
5342
+ if (adjacentDisplay) {
5343
+ displayName = adjacentDisplay[1];
5344
+ continue;
5345
+ }
5346
+ const methodMatch = scanLine.match(JAVA_METHOD_RE);
5347
+ if (methodMatch) {
5348
+ return this.buildJavaRecordFromMethod(methodMatch[1], displayName, j, language, filePath);
5349
+ }
5350
+ }
5351
+ return void 0;
5352
+ }
5353
+ findJavaDisplayNameBefore(lines, testIdx) {
5354
+ let displayName;
5355
+ for (let k = Math.max(0, testIdx - 3); k < testIdx; k++) {
5356
+ const dm = lines[k].match(JAVA_DISPLAY_RE);
5357
+ if (dm) displayName = dm[1];
5358
+ }
5359
+ return displayName;
5360
+ }
5361
+ buildJavaRecordFromMethod(methodName, displayName, index, language, filePath) {
5362
+ const name = displayName ?? methodName;
5363
+ const patternKey = displayName ?? methodName;
5364
+ return {
5365
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
5366
+ extractor: "test-descriptions",
5367
+ language,
5368
+ filePath,
5369
+ line: index + 1,
5370
+ nodeType: "business_rule",
5371
+ name,
5372
+ content: patternKey,
5373
+ confidence: displayName ? 0.7 : 0.5,
5374
+ metadata: { framework: "junit5" }
5375
+ };
5376
+ }
5297
5377
  };
5378
+ TS_ENUM_RE = /(?:export\s+)?enum\s+(\w+)/;
5379
+ TS_AS_CONST_RE = /(?:export\s+)?const\s+(\w+)\s*=\s*\{/;
5380
+ TS_UNION_RE = /(?:export\s+)?type\s+(\w+)\s*=\s*(['"`][\w]+['"`](?:\s*\|\s*['"`][\w]+['"`])+)/;
5381
+ JS_FREEZE_RE = /(?:export\s+)?const\s+(\w+)\s*=\s*Object\.freeze\s*\(\s*\{/;
5382
+ JS_CONST_OBJECT_RE = /(?:export\s+)?const\s+([A-Z][A-Z_\d]*)\s*=\s*\{/;
5383
+ JS_UPPER_KEY_RE = /^[A-Z][A-Z_\d]*$/;
5384
+ PY_ENUM_RE = /^class\s+(\w+)\s*\(\s*(?:str\s*,\s*)?(?:Enum|StrEnum|IntEnum|Flag|IntFlag)\s*\)/;
5385
+ PY_LITERAL_RE = /(\w+)\s*=\s*Literal\s*\[([^\]]+)\]/;
5386
+ GO_TYPE_RE = /^type\s+(\w+)\s+(?:int|string|uint|int32|int64|uint32|uint64)/;
5387
+ GO_CONST_BLOCK_RE = /^const\s*\(/;
5388
+ GO_IOTA_RE = /^(\w+)\s+(\w+)\s*=\s*iota/;
5389
+ GO_TYPED_RE = /^(\w+)\s+(\w+)\s*=\s*"[^"]*"/;
5390
+ GO_BARE_RE = /^(\w+)\s*$/;
5391
+ RUST_ENUM_RE = /^(?:pub\s+)?enum\s+(\w+)/;
5392
+ JAVA_ENUM_RE = /(?:public\s+|private\s+|protected\s+)?enum\s+(\w+)/;
5298
5393
  EnumConstantExtractor = class {
5299
5394
  name = "enum-constants";
5300
5395
  supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
@@ -5318,213 +5413,235 @@ var init_dist = __esm({
5318
5413
  const records = [];
5319
5414
  const lines = content.split("\n");
5320
5415
  for (let i = 0; i < lines.length; i++) {
5321
- const line = lines[i];
5322
- const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
5323
- if (enumMatch) {
5324
- const enumName = enumMatch[1];
5325
- const members = this.collectEnumMembers(lines, i + 1);
5326
- records.push({
5327
- id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
5328
- extractor: "enum-constants",
5329
- language,
5330
- filePath,
5331
- line: i + 1,
5332
- nodeType: "business_term",
5333
- name: enumName,
5334
- content: `enum ${enumName} { ${members.join(", ")} }`,
5335
- confidence: 0.8,
5336
- metadata: { kind: "enum", members }
5337
- });
5338
- }
5339
- const constMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*\{/);
5340
- if (constMatch && content.includes("as const")) {
5341
- const blockEnd = this.findClosingBrace(lines, i);
5342
- const block = lines.slice(i, blockEnd + 1).join("\n");
5343
- if (block.includes("as const")) {
5344
- const constName = constMatch[1];
5345
- const members = this.collectObjectKeys(lines, i + 1);
5346
- records.push({
5347
- id: `extracted:enum-constants:${hash(filePath + ":" + constName)}`,
5348
- extractor: "enum-constants",
5349
- language,
5350
- filePath,
5351
- line: i + 1,
5352
- nodeType: "business_term",
5353
- name: constName,
5354
- content: `const ${constName} = { ${members.join(", ")} } as const`,
5355
- confidence: 0.8,
5356
- metadata: { kind: "as-const", members }
5357
- });
5358
- }
5359
- }
5360
- const unionMatch = line.match(
5361
- /(?:export\s+)?type\s+(\w+)\s*=\s*(['"`][\w]+['"`](?:\s*\|\s*['"`][\w]+['"`])+)/
5362
- );
5363
- if (unionMatch) {
5364
- const typeName = unionMatch[1];
5365
- const values = unionMatch[2].split("|").map((v) => v.trim().replace(/['"`]/g, ""));
5366
- records.push({
5367
- id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
5368
- extractor: "enum-constants",
5369
- language,
5370
- filePath,
5371
- line: i + 1,
5372
- nodeType: "business_term",
5373
- name: typeName,
5374
- content: `type ${typeName} = ${values.map((v) => `'${v}'`).join(" | ")}`,
5375
- confidence: 0.7,
5376
- metadata: { kind: "union-type", members: values }
5377
- });
5378
- }
5416
+ const enumRecord = this.matchTsEnum(lines, i, filePath, language);
5417
+ if (enumRecord) records.push(enumRecord);
5418
+ const asConstRecord = this.matchTsAsConst(content, lines, i, filePath, language);
5419
+ if (asConstRecord) records.push(asConstRecord);
5420
+ const unionRecord = this.matchTsUnion(lines, i, filePath, language);
5421
+ if (unionRecord) records.push(unionRecord);
5379
5422
  }
5380
5423
  return records;
5381
5424
  }
5425
+ matchTsEnum(lines, i, filePath, language) {
5426
+ const enumMatch = lines[i].match(TS_ENUM_RE);
5427
+ if (!enumMatch) return void 0;
5428
+ const enumName = enumMatch[1];
5429
+ const members = this.collectEnumMembers(lines, i + 1);
5430
+ return {
5431
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
5432
+ extractor: "enum-constants",
5433
+ language,
5434
+ filePath,
5435
+ line: i + 1,
5436
+ nodeType: "business_term",
5437
+ name: enumName,
5438
+ content: `enum ${enumName} { ${members.join(", ")} }`,
5439
+ confidence: 0.8,
5440
+ metadata: { kind: "enum", members }
5441
+ };
5442
+ }
5443
+ matchTsAsConst(content, lines, i, filePath, language) {
5444
+ const constMatch = lines[i].match(TS_AS_CONST_RE);
5445
+ if (!constMatch || !content.includes("as const")) return void 0;
5446
+ const blockEnd = this.findClosingBrace(lines, i);
5447
+ const block = lines.slice(i, blockEnd + 1).join("\n");
5448
+ if (!block.includes("as const")) return void 0;
5449
+ const constName = constMatch[1];
5450
+ const members = this.collectObjectKeys(lines, i + 1);
5451
+ return {
5452
+ id: `extracted:enum-constants:${hash(filePath + ":" + constName)}`,
5453
+ extractor: "enum-constants",
5454
+ language,
5455
+ filePath,
5456
+ line: i + 1,
5457
+ nodeType: "business_term",
5458
+ name: constName,
5459
+ content: `const ${constName} = { ${members.join(", ")} } as const`,
5460
+ confidence: 0.8,
5461
+ metadata: { kind: "as-const", members }
5462
+ };
5463
+ }
5464
+ matchTsUnion(lines, i, filePath, language) {
5465
+ const unionMatch = lines[i].match(TS_UNION_RE);
5466
+ if (!unionMatch) return void 0;
5467
+ const typeName = unionMatch[1];
5468
+ const values = unionMatch[2].split("|").map((v) => v.trim().replace(/['"`]/g, ""));
5469
+ return {
5470
+ id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
5471
+ extractor: "enum-constants",
5472
+ language,
5473
+ filePath,
5474
+ line: i + 1,
5475
+ nodeType: "business_term",
5476
+ name: typeName,
5477
+ content: `type ${typeName} = ${values.map((v) => `'${v}'`).join(" | ")}`,
5478
+ confidence: 0.7,
5479
+ metadata: { kind: "union-type", members: values }
5480
+ };
5481
+ }
5382
5482
  extractJavaScript(content, filePath, language) {
5383
5483
  const records = [];
5384
5484
  const lines = content.split("\n");
5385
5485
  for (let i = 0; i < lines.length; i++) {
5386
- const line = lines[i];
5387
- const freezeMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*Object\.freeze\s*\(\s*\{/);
5486
+ const freezeMatch = lines[i].match(JS_FREEZE_RE);
5388
5487
  if (freezeMatch) {
5389
- const name = freezeMatch[1];
5390
- const members = this.collectObjectKeys(lines, i + 1);
5391
- records.push({
5392
- id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
5393
- extractor: "enum-constants",
5394
- language,
5395
- filePath,
5396
- line: i + 1,
5397
- nodeType: "business_term",
5398
- name,
5399
- content: `const ${name} = Object.freeze({ ${members.join(", ")} })`,
5400
- confidence: 0.8,
5401
- metadata: { kind: "frozen-object", members }
5402
- });
5488
+ records.push(this.buildJsFreezeRecord(freezeMatch[1], lines, i, filePath, language));
5403
5489
  }
5404
- const constMatch = line.match(/(?:export\s+)?const\s+([A-Z][A-Z_\d]*)\s*=\s*\{/);
5405
- if (constMatch && !freezeMatch) {
5406
- const name = constMatch[1];
5407
- const members = this.collectObjectKeys(lines, i + 1);
5408
- if (members.length > 0 && members.every((m) => /^[A-Z][A-Z_\d]*$/.test(m))) {
5409
- records.push({
5410
- id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
5411
- extractor: "enum-constants",
5412
- language,
5413
- filePath,
5414
- line: i + 1,
5415
- nodeType: "business_term",
5416
- name,
5417
- content: `const ${name} = { ${members.join(", ")} }`,
5418
- confidence: 0.6,
5419
- metadata: { kind: "const-object", members }
5420
- });
5421
- }
5490
+ if (!freezeMatch) {
5491
+ const constRecord = this.matchJsConstObject(lines, i, filePath, language);
5492
+ if (constRecord) records.push(constRecord);
5422
5493
  }
5423
5494
  }
5424
5495
  return records;
5425
5496
  }
5497
+ buildJsFreezeRecord(name, lines, i, filePath, language) {
5498
+ const members = this.collectObjectKeys(lines, i + 1);
5499
+ return {
5500
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
5501
+ extractor: "enum-constants",
5502
+ language,
5503
+ filePath,
5504
+ line: i + 1,
5505
+ nodeType: "business_term",
5506
+ name,
5507
+ content: `const ${name} = Object.freeze({ ${members.join(", ")} })`,
5508
+ confidence: 0.8,
5509
+ metadata: { kind: "frozen-object", members }
5510
+ };
5511
+ }
5512
+ matchJsConstObject(lines, i, filePath, language) {
5513
+ const constMatch = lines[i].match(JS_CONST_OBJECT_RE);
5514
+ if (!constMatch) return void 0;
5515
+ const name = constMatch[1];
5516
+ const members = this.collectObjectKeys(lines, i + 1);
5517
+ if (members.length === 0 || !members.every((m) => JS_UPPER_KEY_RE.test(m))) return void 0;
5518
+ return {
5519
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
5520
+ extractor: "enum-constants",
5521
+ language,
5522
+ filePath,
5523
+ line: i + 1,
5524
+ nodeType: "business_term",
5525
+ name,
5526
+ content: `const ${name} = { ${members.join(", ")} }`,
5527
+ confidence: 0.6,
5528
+ metadata: { kind: "const-object", members }
5529
+ };
5530
+ }
5426
5531
  extractPython(content, filePath, language) {
5427
5532
  const records = [];
5428
5533
  const lines = content.split("\n");
5429
5534
  for (let i = 0; i < lines.length; i++) {
5430
- const line = lines[i];
5431
- const enumMatch = line.match(
5432
- /^class\s+(\w+)\s*\(\s*(?:str\s*,\s*)?(?:Enum|StrEnum|IntEnum|Flag|IntFlag)\s*\)/
5433
- );
5434
- if (enumMatch) {
5435
- const enumName = enumMatch[1];
5436
- const members = this.collectPythonEnumMembers(lines, i + 1);
5437
- records.push({
5438
- id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
5439
- extractor: "enum-constants",
5440
- language,
5441
- filePath,
5442
- line: i + 1,
5443
- nodeType: "business_term",
5444
- name: enumName,
5445
- content: `class ${enumName}(Enum) { ${members.join(", ")} }`,
5446
- confidence: 0.8,
5447
- metadata: { kind: "enum", members }
5448
- });
5449
- }
5450
- const literalMatch = line.match(/(\w+)\s*=\s*Literal\s*\[([^\]]+)\]/);
5451
- if (literalMatch) {
5452
- const typeName = literalMatch[1];
5453
- const values = literalMatch[2].split(",").map((v) => v.trim().replace(/["']/g, ""));
5454
- records.push({
5455
- id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
5456
- extractor: "enum-constants",
5457
- language,
5458
- filePath,
5459
- line: i + 1,
5460
- nodeType: "business_term",
5461
- name: typeName,
5462
- content: `${typeName} = Literal[${values.map((v) => `"${v}"`).join(", ")}]`,
5463
- confidence: 0.7,
5464
- metadata: { kind: "literal-type", members: values }
5465
- });
5466
- }
5535
+ const enumRecord = this.matchPythonEnum(lines, i, filePath, language);
5536
+ if (enumRecord) records.push(enumRecord);
5537
+ const literalRecord = this.matchPythonLiteral(lines, i, filePath, language);
5538
+ if (literalRecord) records.push(literalRecord);
5467
5539
  }
5468
5540
  return records;
5469
5541
  }
5542
+ matchPythonEnum(lines, i, filePath, language) {
5543
+ const enumMatch = lines[i].match(PY_ENUM_RE);
5544
+ if (!enumMatch) return void 0;
5545
+ const enumName = enumMatch[1];
5546
+ const members = this.collectPythonEnumMembers(lines, i + 1);
5547
+ return {
5548
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
5549
+ extractor: "enum-constants",
5550
+ language,
5551
+ filePath,
5552
+ line: i + 1,
5553
+ nodeType: "business_term",
5554
+ name: enumName,
5555
+ content: `class ${enumName}(Enum) { ${members.join(", ")} }`,
5556
+ confidence: 0.8,
5557
+ metadata: { kind: "enum", members }
5558
+ };
5559
+ }
5560
+ matchPythonLiteral(lines, i, filePath, language) {
5561
+ const literalMatch = lines[i].match(PY_LITERAL_RE);
5562
+ if (!literalMatch) return void 0;
5563
+ const typeName = literalMatch[1];
5564
+ const values = literalMatch[2].split(",").map((v) => v.trim().replace(/["']/g, ""));
5565
+ return {
5566
+ id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
5567
+ extractor: "enum-constants",
5568
+ language,
5569
+ filePath,
5570
+ line: i + 1,
5571
+ nodeType: "business_term",
5572
+ name: typeName,
5573
+ content: `${typeName} = Literal[${values.map((v) => `"${v}"`).join(", ")}]`,
5574
+ confidence: 0.7,
5575
+ metadata: { kind: "literal-type", members: values }
5576
+ };
5577
+ }
5470
5578
  extractGo(content, filePath, language) {
5471
5579
  const records = [];
5472
5580
  const lines = content.split("\n");
5473
5581
  for (let i = 0; i < lines.length; i++) {
5474
5582
  const line = lines[i];
5475
- const typeMatch = line.match(/^type\s+(\w+)\s+(?:int|string|uint|int32|int64|uint32|uint64)/);
5583
+ const typeMatch = line.match(GO_TYPE_RE);
5476
5584
  if (typeMatch) {
5477
5585
  }
5478
- const constBlockMatch = line.match(/^const\s*\(/);
5586
+ const constBlockMatch = line.match(GO_CONST_BLOCK_RE);
5479
5587
  if (constBlockMatch) {
5480
- const consts = [];
5481
- let typeName;
5482
- for (let j = i + 1; j < lines.length; j++) {
5483
- const constLine = lines[j].trim();
5484
- if (constLine === ")") break;
5485
- if (constLine === "" || constLine.startsWith("//")) continue;
5486
- const iotaMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*iota/);
5487
- if (iotaMatch) {
5488
- typeName = iotaMatch[2];
5489
- consts.push(iotaMatch[1]);
5490
- continue;
5491
- }
5492
- const typedMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*"[^"]*"/);
5493
- if (typedMatch) {
5494
- typeName = typeName ?? typedMatch[2];
5495
- consts.push(typedMatch[1]);
5496
- continue;
5497
- }
5498
- const bareMatch = constLine.match(/^(\w+)\s*$/);
5499
- if (bareMatch) {
5500
- consts.push(bareMatch[1]);
5501
- }
5502
- }
5503
- if (consts.length > 0) {
5504
- const name = typeName ?? consts[0];
5505
- records.push({
5506
- id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
5507
- extractor: "enum-constants",
5508
- language,
5509
- filePath,
5510
- line: i + 1,
5511
- nodeType: "business_term",
5512
- name,
5513
- content: `const ( ${consts.join(", ")} )`,
5514
- confidence: typeName ? 0.8 : 0.6,
5515
- metadata: { kind: typeName ? "typed-const" : "const-block", members: consts }
5516
- });
5517
- }
5588
+ const record = this.buildGoConstBlockRecord(lines, i, filePath, language);
5589
+ if (record) records.push(record);
5518
5590
  }
5519
5591
  }
5520
5592
  return records;
5521
5593
  }
5594
+ buildGoConstBlockRecord(lines, i, filePath, language) {
5595
+ const { consts, typeName } = this.collectGoConsts(lines, i + 1);
5596
+ if (consts.length === 0) return void 0;
5597
+ const name = typeName ?? consts[0];
5598
+ return {
5599
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
5600
+ extractor: "enum-constants",
5601
+ language,
5602
+ filePath,
5603
+ line: i + 1,
5604
+ nodeType: "business_term",
5605
+ name,
5606
+ content: `const ( ${consts.join(", ")} )`,
5607
+ confidence: typeName ? 0.8 : 0.6,
5608
+ metadata: { kind: typeName ? "typed-const" : "const-block", members: consts }
5609
+ };
5610
+ }
5611
+ collectGoConsts(lines, startLine) {
5612
+ const consts = [];
5613
+ let typeName;
5614
+ for (let j = startLine; j < lines.length; j++) {
5615
+ const constLine = lines[j].trim();
5616
+ if (constLine === ")") break;
5617
+ if (constLine === "" || constLine.startsWith("//")) continue;
5618
+ const parsed = this.parseGoConstLine(constLine);
5619
+ if (!parsed) continue;
5620
+ typeName = this.resolveGoTypeName(typeName, parsed);
5621
+ consts.push(parsed.name);
5622
+ }
5623
+ return { consts, typeName };
5624
+ }
5625
+ parseGoConstLine(constLine) {
5626
+ const iotaMatch = constLine.match(GO_IOTA_RE);
5627
+ if (iotaMatch) return { name: iotaMatch[1], typeName: iotaMatch[2], overwriteType: true };
5628
+ const typedMatch = constLine.match(GO_TYPED_RE);
5629
+ if (typedMatch) return { name: typedMatch[1], typeName: typedMatch[2], overwriteType: false };
5630
+ const bareMatch = constLine.match(GO_BARE_RE);
5631
+ if (bareMatch) return { name: bareMatch[1] };
5632
+ return void 0;
5633
+ }
5634
+ resolveGoTypeName(current, parsed) {
5635
+ if (parsed.overwriteType) return parsed.typeName;
5636
+ if (parsed.typeName !== void 0 && current === void 0) return parsed.typeName;
5637
+ return current;
5638
+ }
5522
5639
  extractRust(content, filePath, language) {
5523
5640
  const records = [];
5524
5641
  const lines = content.split("\n");
5525
5642
  for (let i = 0; i < lines.length; i++) {
5526
5643
  const line = lines[i];
5527
- const enumMatch = line.match(/^(?:pub\s+)?enum\s+(\w+)/);
5644
+ const enumMatch = line.match(RUST_ENUM_RE);
5528
5645
  if (enumMatch) {
5529
5646
  const enumName = enumMatch[1];
5530
5647
  const members = this.collectRustEnumVariants(lines, i + 1);
@@ -5549,7 +5666,7 @@ var init_dist = __esm({
5549
5666
  const lines = content.split("\n");
5550
5667
  for (let i = 0; i < lines.length; i++) {
5551
5668
  const line = lines[i];
5552
- const enumMatch = line.match(/(?:public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
5669
+ const enumMatch = line.match(JAVA_ENUM_RE);
5553
5670
  if (enumMatch) {
5554
5671
  const enumName = enumMatch[1];
5555
5672
  const members = this.collectJavaEnumConstants(lines, i + 1);
@@ -5597,10 +5714,7 @@ var init_dist = __esm({
5597
5714
  for (let i = startLine; i < lines.length; i++) {
5598
5715
  for (const ch of lines[i]) {
5599
5716
  if (ch === "{") depth++;
5600
- if (ch === "}") {
5601
- depth--;
5602
- if (depth === 0) return i;
5603
- }
5717
+ else if (ch === "}" && --depth === 0) return i;
5604
5718
  }
5605
5719
  }
5606
5720
  return lines.length - 1;