Skip to content
Back to skills

Ambiguity Detector

ASecurity

Takes surface scan results, optional LLM synthesis open-question entries, and optional tree-sitter AST signals to score project ambiguities (1-10). Outputs structured ambiguity_feed for smart-question-generator. Pure heuristic — no LLM calls. Designed for Stage 3.5 of the /gaai:bootstrap pipeline (between LLM synthesis and Q&A).

  • 162 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 23, 2026
ai-agentsjavascripttypescriptpythongojavareactvuenextjsgitfrontend

Works with

  • cli

Security analysis

A100/100

Scanned September 23, 2026

npx -y skills add digipulse-engineering/GAAI-framework --skill ambiguity-detector --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ambiguity Detector?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ambiguity Detector
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/digipulse-engineering-ambiguity-detector/badge)](https://www.skillsdirectory.com/skills/digipulse-engineering-ambiguity-detector)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ambiguity-detector
description: Takes surface scan results, optional LLM synthesis open-question entries, and optional tree-sitter AST signals to score project ambiguities (1-10). Outputs structured ambiguity_feed for smart-question-generator. Pure heuristic — no LLM calls. Designed for Stage 3.5 of the /gaai:bootstrap pipeline (between LLM synthesis and Q&A).
license: ELv2
compatibility: Works with any filesystem-based AI coding agent
metadata:
  author: gaai-framework
  version: "1.0"
  category: cross
  track: cross-cutting
  id: SKILL-AMBIGUITY-DETECTOR-001
  updated_at: 2026-04-29
  status: stable
inputs:
  - surface_scan_result      # from project-surface-scan (required)
  - synthesis_entries        # Array of open-question clarity entries from bootstrap-llm-synthesis (optional, may be [])
  - ast_signals              # from tree-sitter Stage 0c / E107a (optional, null if unavailable)
outputs:
  - ambiguity_feed           # Array<{topic, ambiguity_score, evidence_pro[], evidence_against[]}>
---

# Ambiguity Detector

## Purpose / When to Activate

Activate:
- As Stage 3.5 of the `/gaai:bootstrap` pipeline — after `bootstrap-llm-synthesis` returns results and before `smart-question-generator` is called
- The bootstrap orchestrator filters `synthesis_result.entries[]` to `clarity: "open-question"` and passes them as `synthesis_entries`
- When `synthesis_entries` is empty and `ast_signals` is null, the skill runs heuristic-only analysis on `surface_scan_result` and may still produce entries from language distribution

This skill is **pure heuristic** — it makes no LLM calls. It scores ambiguities deterministically from structural signals. All scoring logic executes client-side before the Q&A stage (consistent with any active "LLM stays client-side" and "instructions stay client-side" DECs in your registry).

---

## Input Schema

```yaml
surface_scan_result:              # required — output of project-surface-scan
  total_file_count: number
  languages: Array<{
    language: string
    file_count: number
    rank: number
  }>
  ext_counts: { [ext: string]: number }   # e.g. { ".ts": 142, ".vue": 3 }
  dir_counts: { [dir: string]: number }   # depth-1 directory file counts

synthesis_entries:                # optional — null or empty array acceptable
  # Array of entries from bootstrap-llm-synthesis filtered to clarity="open-question"
  # Pass [] or null when synthesis stage was skipped or produced no open-questions
  - topic: string                 # e.g. "What framework is this project using?"
    content: string               # the ambiguity question or description
    sources: Array<string>        # file:line references or descriptors

ast_signals:                      # optional — null if E107a stories not yet delivered
  import_patterns:
    has_esm: boolean              # ESM (import/export) patterns detected
    has_commonjs: boolean         # CommonJS (require/module.exports) patterns detected
    conflict_count: number        # files containing both ESM and CommonJS patterns
    conflict_files: Array<string> # sample of conflicting file paths
  directory_signals:
    has_src_dir: boolean          # src/ directory exists and is non-trivial
    has_app_dir: boolean          # app/ directory exists and is non-trivial
    src_file_count: number | null # files under src/ (null if not scanned)
    app_file_count: number | null # files under app/ (null if not scanned)
```

---

## Process

### Step 1 — Heuristic analysis from surface_scan_result

#### Step 1a — Language distribution analysis

Compute the ratio of each detected language over total language-attributed files:

```
total_lang_files = sum(l.file_count for l in surface_scan_result.languages)

if total_lang_files == 0:
  skip language heuristics   # no language data — not an error
  log: "[ambiguity-detector] no language data in surface_scan_result — skipping language heuristics"
else:
  lang_ratios = [
    { language: l.language, ratio: l.file_count / total_lang_files, count: l.file_count }
    for l in surface_scan_result.languages
  ]
  
  top1 = lang_ratios[0] if len >= 1 else null
  top2 = lang_ratios[1] if len >= 2 else null
  top3 = lang_ratios[2] if len >= 3 else null
```

Apply scoring rules in order (first match wins):

| Condition | Score | Topic | Rationale |
|---|---|---|---|
| top1.ratio >= 0.80 | 1–2 | `primary_language` | Dominant single language — low ambiguity |
| top1.ratio >= 0.50 AND top2 is null OR top2.ratio < 0.25 | 3 | `primary_language` | Clear majority — minor presence of secondary |
| top1 AND top2 both >= 0.30 AND abs(top1.ratio – top2.ratio) < 0.20 | 7–9 | `language_ecosystem` | Near-parity — ambiguous which is primary |
| top1 AND top2 AND top3 all >= 0.15 | 5–6 | `language_ecosystem` | Mixed polyglot project |
| default (top1 >= 0.50, top2 >= 0.25) | 4 | `language_ecosystem` | Moderate secondary presence |

Score refinements:
- For `primary_language` with ratio >= 0.80: `score = max(1, round((1 - top1.ratio) * 20))`  
  (e.g., 95% → 1, 85% → 3, 80% → 4 → capped at 2)  
  Use: `score = 1 if top1.ratio >= 0.90 else 2`
- For near-parity: `score = 7 + round((0.20 - abs(top1.ratio - top2.ratio)) * 10)`, clamped to 7–9

Build evidence arrays:
```
for primary_language low-ambiguity:
  evidence_pro = [{ source: "surface_scan.languages", snippet: "{top1.language}: {top1.ratio*100:.0f}% of detected files", weight: 1.0 }]
  evidence_against = []

for language_ecosystem high-ambiguity (near-parity):
  evidence_pro = [
    { source: "surface_scan.languages", snippet: "{top1.language}: {top1.ratio*100:.0f}%", weight: top1.ratio },
    { source: "surface_scan.languages", snippet: "{top2.language}: {top2.ratio*100:.0f}%", weight: top2.ratio }
  ]
  evidence_against = [
    { source: "surface_scan.languages", snippet: "Secondary language may be tooling/tests only", weight: 0.30 }
  ]
```

#### Step 1b — Framework conflict detection

Detect framework markers from `ext_counts`:

```
vue_count   = surface_scan_result.ext_counts[".vue"]  ?? 0
jsx_count   = surface_scan_result.ext_counts[".jsx"]  ?? 0
tsx_count   = surface_scan_result.ext_counts[".tsx"]  ?? 0
react_count = jsx_count + tsx_count
```

Framework conflict scoring:

```
if vue_count > 0 AND react_count > 0:
  # Both Vue (.vue files) and React (.jsx/.tsx) detected simultaneously
  score = 8
  topic = "frontend_framework"
  evidence_pro = [
    { source: "ext_counts[.vue]",      snippet: ".vue files detected: {vue_count}",       weight: 0.85 },
    { source: "ext_counts[.jsx/.tsx]", snippet: ".jsx/.tsx files detected: {react_count}", weight: 0.85 }
  ]
  evidence_against = [
    { source: "ext_counts", snippet: ".tsx may be non-React TypeScript components", weight: 0.25 }
  ]
  add to heuristic_topics

elif vue_count > 0 AND react_count == 0:
  # Vue-only — low ambiguity, skip (score ≤ 2; smart-question-generator filters score < 3)

elif react_count > 5 AND vue_count == 0:
  # React-only — low ambiguity, skip
```

**Note:** Framework detection intentionally limited to extension-based signals only. Config file inspection (webpack.config.js vs vite.config.ts) requires file reads outside this skill's scope. If synthesis open-question entries surface framework conflicts, those will be scored in Step 3.

---

### Step 2 — AST signal analysis (skipped if ast_signals is null)

```
if ast_signals == null:
  log: "[ambiguity-detector] ast_signals not available — skipping AST heuristics (E107a stories not yet delivered)"
  skip to Step 3
```

#### Step 2a — Module system conflict

```
if ast_signals.import_patterns.has_esm AND ast_signals.import_patterns.has_commonjs:
  conflict_count = ast_signals.import_patterns.conflict_count
  total_files = surface_scan_result.total_file_count
  conflict_ratio = conflict_count / max(1, total_files)
  
  # Score: base 5, +1 per 5% of conflicting files, capped at 8
  score = min(8, 5 + round(conflict_ratio * 20))
  
  topic = "module_system"
  evidence_pro = [
    { source: "ast_signals.import_patterns", snippet: "ESM (import/export) patterns detected",                        weight: 0.75 },
    { source: "ast_signals.import_patterns", snippet: "CommonJS (require/module.exports) patterns detected",           weight: 0.75 },
    { source: "ast_signals.import_patterns", snippet: "{conflict_count} files with mixed ESM+CJS patterns",            weight: min(1.0, conflict_ratio * 5) }
  ]
  
  # Sample up to 3 conflict files as evidence snippets
  for f in ast_signals.import_patterns.conflict_files[0..2]:
    evidence_pro.push({ source: f, snippet: "file contains both import and require", weight: 0.60 })
  
  evidence_against = [
    { source: "convention", snippet: "Config files (.cjs, jest.config.js) may legitimately use require()", weight: 0.30 }
  ]
  add to ast_topics
```

#### Step 2b — Directory structure conflict

```
d = ast_signals.directory_signals

if d.has_src_dir AND d.has_app_dir:
  src_count = d.src_file_count ?? 0
  app_count = d.app_file_count ?? 0
  
  if src_count > 5 AND app_count > 5:
    # Both directories are substantive — potential layout ambiguity
    ratio_diff = abs(src_count - app_count) / max(src_count, app_count)
    
    if ratio_diff < 0.25:
      score = 6    # nearly equal counts — genuinely ambiguous
    elif ratio_diff < 0.50:
      score = 4    # one is larger but both active
    else:
      score = 3    # significant size difference — one is likely primary
    
    topic = "source_layout"
    evidence_pro = [
      { source: "directory_signals.src", snippet: "src/ directory: {src_count} files", weight: 0.70 },
      { source: "directory_signals.app", snippet: "app/ directory: {app_count} files", weight: 0.70 }
    ]
    evidence_against = [
      { source: "convention", snippet: "One directory may be legacy code or framework convention (e.g., Next.js app/)", weight: 0.35 }
    ]
    add to ast_topics
```

---

### Step 3 — Score synthesis open-question entries

For each entry in `synthesis_entries` (skip if null or empty):

```
for entry in (synthesis_entries ?? []):
  topic_key = derive_topic_key(entry.topic)
  # Normalization: lowercase, replace spaces/special chars with underscores, max 30 chars
  # e.g. "What framework is this project using?" → "framework"
  # e.g. "Is this a SaaS or a CLI tool?"        → "project_type"
  # Simple approach: take significant nouns from the question if it's a question,
  # or use the content directly if it's a noun phrase (≤ 30 chars → keep as-is after normalization)
  
  # Check for existing entry with matching topic key from heuristic analysis
  existing = heuristic_topics.concat(ast_topics).find(t => t.topic == topic_key)
  
  if existing is found:
    # Synthesis confirms an existing heuristic ambiguity — boost score
    synthesis_score = base_synthesis_score(entry)
    existing.ambiguity_score = max(existing.ambiguity_score, synthesis_score)
    existing.evidence_pro.push({
      source: "llm_synthesis.open_question",
      snippet: entry.content[0..120],
      weight: 0.60
    })
    for s in entry.sources[0..2]:
      existing.evidence_pro.push({ source: s, snippet: "synthesis source", weight: 0.40 })
      
  else:
    # New topic from synthesis — create entry
    synthesis_score = base_synthesis_score(entry)
    new_entry = {
      topic: topic_key,
      ambiguity_score: synthesis_score,
      evidence_pro: [
        { source: "llm_synthesis.open_question", snippet: entry.content[0..120], weight: 0.60 }
      ] + entry.sources[0..2].map(s => ({ source: s, snippet: "synthesis source", weight: 0.40 })),
      evidence_against: []
    }
    synthesis_topics.push(new_entry)
```

**`base_synthesis_score` helper:**

```
base_synthesis_score(entry):
  # Open-question entries from LLM synthesis start at 5 (moderate ambiguity)
  # Adjust based on source evidence density
  source_count = entry.sources.length
  bonus = min(2, source_count)                 # 0-2 bonus based on cited sources
  return min(8, 5 + bonus)                     # range: 5-8
```

**`derive_topic_key` helper:**

```
derive_topic_key(topic_string):
  # If short noun phrase (≤ 30 chars, no question mark): normalize directly
  # If question string: extract key nouns
  # Fallback: lowercase + replace non-alphanumeric with underscores + truncate to 30 chars
  normalized = topic_string.toLowerCase()
                            .replace(/[^a-z0-9]+/g, '_')
                            .replace(/^_+|_+$/g, '')
                            .slice(0, 30)
  return normalized
```

---

### Step 4 — Merge, deduplicate, and finalize

Merge all topic sources:
```
all_topics = heuristic_topics + ast_topics + synthesis_topics
```

Deduplicate by topic key (keep the entry with the highest `ambiguity_score` when duplicates exist):
```
deduped = {}
for t in all_topics:
  if t.topic in deduped:
    if t.ambiguity_score > deduped[t.topic].ambiguity_score:
      deduped[t.topic] = t
    else:
      # Merge evidence from lower-score duplicate into winner
      deduped[t.topic].evidence_pro += t.evidence_pro
      deduped[t.topic].evidence_against += t.evidence_against
  else:
    deduped[t.topic] = t

ambiguity_feed = sorted(deduped.values(), key=lambda t: (-t.ambiguity_score, t.topic))
```

Secondary sort by `topic` alphabetically ensures deterministic output for equal scores.

Clamp all scores to [1, 10]:
```
for t in ambiguity_feed:
  t.ambiguity_score = max(1, min(10, round(t.ambiguity_score)))
```

---

### Step 5 — Observability (AC4)

Log score distribution to stdout:

```
high_count   = ambiguity_feed.filter(t => t.ambiguity_score >= 7).length
medium_count = ambiguity_feed.filter(t => t.ambiguity_score >= 4 AND t.ambiguity_score <= 6).length
low_count    = ambiguity_feed.filter(t => t.ambiguity_score <= 3).length

log (stdout):
[ambiguity-detector] analysis complete
  topics: {ambiguity_feed.length} total
    heuristic: {heuristic_count} (language: {lang_count}, framework: {fw_count})
    AST-derived: {ast_count}
    synthesis open-questions: {synthesis_count}
  score distribution:
    high   (7-10): {high_count}   topics
    medium (4-6):  {medium_count} topics
    low    (1-3):  {low_count}    topics
  ast_signals_available: {ast_signals != null}
  synthesis_entries_processed: {len(synthesis_entries ?? [])}
```

---

## Output Schema

```yaml
ambiguity_feed:
  - topic: string              # short key, e.g. "language_ecosystem", "frontend_framework"
    ambiguity_score: number    # 1-10 (integer after clamp); higher = more ambiguous
    evidence_pro:              # signals supporting the ambiguity claim
      - source: string         # e.g. "surface_scan.languages", "ext_counts[.vue]", "ast_signals.import_patterns"
        snippet: string        # short human-readable description of the evidence
        weight: number         # 0.0-1.0 relative confidence weight
    evidence_against:          # signals that temper or contradict the ambiguity claim
      - source: string
        snippet: string
        weight: number

# Example outputs:

# 1. Near-parity language ecosystem (high ambiguity)
ambiguity_feed:
  - topic: "language_ecosystem"
    ambiguity_score: 8
    evidence_pro:
      - source: "surface_scan.languages"
        snippet: "Python: 42%"
        weight: 0.42
      - source: "surface_scan.languages"
        snippet: "JavaScript: 40%"
        weight: 0.40
    evidence_against:
      - source: "surface_scan.languages"
        snippet: "Secondary language may be tooling/tests only"
        weight: 0.30

# 2. Framework conflict (high ambiguity)
  - topic: "frontend_framework"
    ambiguity_score: 8
    evidence_pro:
      - source: "ext_counts[.vue]"
        snippet: ".vue files detected: 12"
        weight: 0.85
      - source: "ext_counts[.jsx/.tsx]"
        snippet: ".jsx/.tsx files detected: 34"
        weight: 0.85
    evidence_against:
      - source: "ext_counts"
        snippet: ".tsx may be non-React TypeScript components"
        weight: 0.25

# 3. Module system conflict from AST signals
  - topic: "module_system"
    ambiguity_score: 6
    evidence_pro:
      - source: "ast_signals.import_patterns"
        snippet: "ESM (import/export) patterns detected"
        weight: 0.75
      - source: "ast_signals.import_patterns"
        snippet: "CommonJS (require/module.exports) patterns detected"
        weight: 0.75
      - source: "ast_signals.import_patterns"
        snippet: "3 files with mixed ESM+CJS patterns"
        weight: 0.60
    evidence_against:
      - source: "convention"
        snippet: "Config files (.cjs, jest.config.js) may legitimately use require()"
        weight: 0.30

# 4. Dominant single language (low ambiguity — may still appear in feed)
  - topic: "primary_language"
    ambiguity_score: 1
    evidence_pro:
      - source: "surface_scan.languages"
        snippet: "TypeScript: 95% of detected files"
        weight: 1.0
    evidence_against: []
```

---

## Quality Checks

Before returning `ambiguity_feed`:
- All `ambiguity_score` values are integers in [1, 10]
- All `topic` values are non-empty strings with no spaces (underscores used as separators)
- No duplicate `topic` values in the array
- `evidence_pro` is always an array (never null; may be empty for synthesis-only entries)
- `evidence_against` is always an array (never null; may be empty)
- All `weight` values are in [0.0, 1.0]
- Output is sorted by `ambiguity_score` descending
- `ambiguity_feed` is an array (never null; may be empty if no signals detected)

---

## Non-Goals

This skill MUST NOT:
- Make LLM calls (pure deterministic heuristic — no inference)
- Read files directly (receives structured data from prior stages)
- Generate questions (that is `smart-question-generator`'s responsibility)
- Write to memory (memory ingest is the orchestrator's responsibility post-consent gate)
- Apply the `score >= 3` threshold filter (that is `smart-question-generator`'s pre-filter)
- Rank by topic importance beyond score (that is `topic-importance-ranker` / E107bS04's concern)
- Substitute for `bootstrap-llm-synthesis` — it scores ambiguities, not synthesizes project context
- Perform retries or caching — stateless, called once per bootstrap session

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.