diff --git a/.github/agents/speckit.taskstoissues.agent.md b/.github/agents/speckit.taskstoissues.agent.md index aa5a7d9..dcc8ecb 100644 --- a/.github/agents/speckit.taskstoissues.agent.md +++ b/.github/agents/speckit.taskstoissues.agent.md @@ -3,6 +3,8 @@ description: Convert existing tasks into actionable, dependency-ordered GitHub i tools: ['github/github-mcp-server/issue_write'] --- +# Speckit Tasks To Issues Agent + ## User Input ```text @@ -14,6 +16,7 @@ You **MUST** consider the user input before proceeding (if not empty). ## Pre-Execution Checks **Check for extension hooks (before tasks-to-issues conversion)**: + - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_taskstoissues` key - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally @@ -23,7 +26,8 @@ You **MUST** consider the user input before proceeding (if not empty). - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): - ``` + + ```text ## Extension Hooks **Optional Pre-Hook**: {extension} @@ -33,8 +37,10 @@ You **MUST** consider the user input before proceeding (if not empty). Prompt: {prompt} To execute: `/{command}` ``` + - **Mandatory hook** (`optional: false`): - ``` + + ```text ## Extension Hooks **Automatic Pre-Hook**: {extension} @@ -43,6 +49,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -66,7 +73,8 @@ git config --get remote.origin.url ## Post-Execution Checks **Check for extension hooks (after tasks-to-issues conversion)**: -Check if `.specify/extensions.yml` exists in the project root. + +- Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_taskstoissues` key - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. @@ -75,7 +83,8 @@ Check if `.specify/extensions.yml` exists in the project root. - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): - ``` + + ```text ## Extension Hooks **Optional Hook**: {extension} @@ -85,12 +94,15 @@ Check if `.specify/extensions.yml` exists in the project root. Prompt: {prompt} To execute: `/{command}` ``` + - **Mandatory hook** (`optional: false`): - ``` + + ```text ## Extension Hooks **Automatic Hook**: {extension} Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7e6443a..0cf270d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,3 +1,5 @@ +# Copilot Instructions + For additional context about technologies to be used, project structure, shell commands, and other important information, read the current plan diff --git a/.markdownlint.json b/.markdownlint.json index 67d2ae5..ee9cc98 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,3 +1,13 @@ { - "MD013": false + "MD013": false, + "MD029": false, + "MD031": false, + "MD032": false, + "MD033": false, + "MD040": false, + "MD041": false, + "MD047": false, + "MD060": { + "style": "spaced" + } } diff --git a/.specify/extensions/git/README.md b/.specify/extensions/git/README.md index 31ba75c..e11e8d7 100644 --- a/.specify/extensions/git/README.md +++ b/.specify/extensions/git/README.md @@ -85,6 +85,7 @@ specify extension enable git ## Graceful Degradation When Git is not installed or the directory is not a Git repository: + - Spec directories are still created under `specs/` - Branch creation is skipped with a warning - Branch validation is skipped with a warning diff --git a/.specify/extensions/git/commands/speckit.git.feature.md b/.specify/extensions/git/commands/speckit.git.feature.md index 1a9c5e3..14211eb 100644 --- a/.specify/extensions/git/commands/speckit.git.feature.md +++ b/.specify/extensions/git/commands/speckit.git.feature.md @@ -17,6 +17,7 @@ You **MUST** consider the user input before proceeding (if not empty). ## Environment Variable Override If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set: + - The script uses the exact value as the branch name, bypassing all prefix/suffix generation - `--short-name`, `--number`, and `--timestamp` flags are ignored - `FEATURE_NUM` is extracted from the name if it starts with a numeric prefix, otherwise set to the full branch name @@ -37,6 +38,7 @@ Determine the branch numbering strategy by checking configuration in this order: ## Execution Generate a concise short name (2-4 words) for the branch: + - Analyze the feature description and extract the most meaningful keywords - Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug") - Preserve technical terms and acronyms (OAuth2, API, JWT, etc.) @@ -49,6 +51,7 @@ Run the appropriate script based on your platform: - **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "" ""` **IMPORTANT**: + - Do NOT pass `--number` — the script determines the correct next number automatically - Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably - You must only ever run this script once per feature @@ -57,11 +60,13 @@ Run the appropriate script based on your platform: ## Graceful Degradation If Git is not installed or the current directory is not a Git repository: + - Branch creation is skipped with a warning: `[specify] Warning: Git repository not detected; skipped branch creation` - The script still outputs `BRANCH_NAME` and `FEATURE_NUM` so the caller can reference them ## Output The script outputs JSON with: + - `BRANCH_NAME`: The branch name (e.g., `003-user-auth` or `20260319-143022-user-auth`) - `FEATURE_NUM`: The numeric or timestamp prefix used diff --git a/.specify/extensions/git/commands/speckit.git.initialize.md b/.specify/extensions/git/commands/speckit.git.initialize.md index 4451ee6..edc3ba4 100644 --- a/.specify/extensions/git/commands/speckit.git.initialize.md +++ b/.specify/extensions/git/commands/speckit.git.initialize.md @@ -14,10 +14,12 @@ Run the appropriate script from the project root: - **PowerShell**: `.specify/extensions/git/scripts/powershell/initialize-repo.ps1` If the extension scripts are not found, fall back to: + - **Bash**: `git init && git add . && git commit -m "Initial commit from Specify template"` - **PowerShell**: `git init; git add .; git commit -m "Initial commit from Specify template"` The script handles all checks internally: + - Skips if Git is not available - Skips if already inside a Git repository - Runs `git init`, `git add .`, and `git commit` with an initial commit message @@ -25,6 +27,7 @@ The script handles all checks internally: ## Customization Replace the script to add project-specific Git initialization steps: + - Custom `.gitignore` templates - Default branch naming (`git config init.defaultBranch`) - Git LFS setup @@ -35,15 +38,18 @@ Replace the script to add project-specific Git initialization steps: ## Output On success: + - `✓ Git repository initialized` ## Graceful Degradation If Git is not installed: + - Warn the user - Skip repository initialization - The project continues to function without Git (specs can still be created under `specs/`) If Git is installed but `git init`, `git add .`, or `git commit` fails: + - Surface the error to the user - Stop this command rather than continuing with a partially initialized repository diff --git a/.specify/extensions/git/commands/speckit.git.remote.md b/.specify/extensions/git/commands/speckit.git.remote.md index 712a3e8..dafa7b3 100644 --- a/.specify/extensions/git/commands/speckit.git.remote.md +++ b/.specify/extensions/git/commands/speckit.git.remote.md @@ -10,7 +10,8 @@ Detect the Git remote URL for integration with GitHub services (e.g., issue crea - Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null` - If Git is not available, output a warning and return empty: - ``` + + ```text [specify] Warning: Git repository not detected; cannot determine remote URL ``` @@ -31,6 +32,7 @@ Parse the remote URL and determine: 3. **Is GitHub**: Whether the remote points to a GitHub repository Supported URL formats: + - HTTPS: `https://github.com//.git` - SSH: `git@github.com:/.git` @@ -41,5 +43,6 @@ Supported URL formats: ## Graceful Degradation If Git is not installed, the directory is not a Git repository, or no remote is configured: + - Return an empty result - Do NOT error — other workflows should continue without Git remote information diff --git a/.specify/extensions/git/commands/speckit.git.validate.md b/.specify/extensions/git/commands/speckit.git.validate.md index dd84618..b990081 100644 --- a/.specify/extensions/git/commands/speckit.git.validate.md +++ b/.specify/extensions/git/commands/speckit.git.validate.md @@ -10,7 +10,8 @@ Validate that the current Git branch follows the expected feature branch naming - Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null` - If Git is not available, output a warning and skip validation: - ``` + + ```text [specify] Warning: Git repository not detected; skipped branch validation ``` @@ -30,6 +31,7 @@ The branch name must match one of these patterns: ## Execution If on a feature branch (matches either pattern): + - Output: `✓ On feature branch: ` - Check if the corresponding spec directory exists under `specs/`: - For sequential branches, look for `specs/-*` where prefix matches the numeric portion @@ -38,12 +40,14 @@ If on a feature branch (matches either pattern): - If spec directory missing: `⚠ No spec directory found for prefix ` If NOT on a feature branch: + - Output: `✗ Not on a feature branch. Current branch: ` - Output: `Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name` ## Graceful Degradation If Git is not installed or the directory is not a Git repository: + - Check the `SPECIFY_FEATURE` environment variable as a fallback - If set, validate that value against the naming patterns - If not set, skip validation with a warning diff --git a/Models/OllamaModels.cs b/Models/OllamaModels.cs index f0789e6..ed5170d 100644 --- a/Models/OllamaModels.cs +++ b/Models/OllamaModels.cs @@ -8,7 +8,7 @@ namespace SymptomCheckerApp.Models public class OllamaChatRequest { [JsonPropertyName("model")] - public string Model { get; set; } = "kimi-k2.6"; + public string Model { get; set; } = "gemma4"; [JsonPropertyName("messages")] public List Messages { get; set; } = new(); diff --git a/Services/OllamaService.cs b/Services/OllamaService.cs index a071457..a329b45 100644 --- a/Services/OllamaService.cs +++ b/Services/OllamaService.cs @@ -19,7 +19,7 @@ namespace SymptomCheckerApp.Services public class OllamaService : IDisposable { public const string DefaultBaseUrl = "http://localhost:11434"; - public const string DefaultModelName = "kimi-k2.6"; + public const string DefaultModelName = "gemma4"; private const int DiagnosisMaxTokens = 2600; private const int MedicationMaxTokens = 2200; @@ -129,57 +129,18 @@ public async Task> ListModelsAsync(CancellationToken ct = default) return exactMatch; } - string? bestModel = null; - int bestScore = int.MinValue; - - foreach (var model in availableModels) - { - int score = ScoreModelPreference(model, requested); - if (score > bestScore) - { - bestScore = score; - bestModel = model; - } - } - - return bestModel ?? availableModels[0]; - } - - private static int ScoreModelPreference(string modelName, string preferredModel) - { - var normalizedName = NormalizeModelName(modelName); - var normalizedPreferred = NormalizeModelName(preferredModel); - int score = 0; - - if (normalizedName == normalizedPreferred) + // If the requested value is a family name (e.g. "gemma4"), allow + // selecting tagged variants like "gemma4:latest". + var normalizedRequested = NormalizeModelName(requested); + var familyMatch = availableModels.FirstOrDefault(model => + NormalizeModelName(model).StartsWith(normalizedRequested, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrEmpty(familyMatch)) { - score += 1000; + return familyMatch; } - if (normalizedPreferred.Contains("kimi", StringComparison.OrdinalIgnoreCase) && - normalizedName.Contains("kimi", StringComparison.OrdinalIgnoreCase)) - { - score += 500; - } - - if (normalizedPreferred.Contains("k2", StringComparison.OrdinalIgnoreCase) && - normalizedName.Contains("k2", StringComparison.OrdinalIgnoreCase)) - { - score += 250; - } - - if (normalizedPreferred.Contains("26", StringComparison.OrdinalIgnoreCase) && - normalizedName.Contains("26", StringComparison.OrdinalIgnoreCase)) - { - score += 120; - } - - if (normalizedName.Contains("kimi", StringComparison.OrdinalIgnoreCase)) score += 300; - if (normalizedName.Contains("k2", StringComparison.OrdinalIgnoreCase)) score += 150; - if (normalizedName.Contains("llama", StringComparison.OrdinalIgnoreCase)) score += 50; - if (normalizedName.Contains("mistral", StringComparison.OrdinalIgnoreCase)) score += 40; - - return score; + // Requested default not available: choose the first existing model. + return availableModels[0]; } private static string NormalizeModelName(string value) diff --git a/UI/Controls/RedFlagBanner.cs b/UI/Controls/RedFlagBanner.cs index 86c0f01..d49ddb5 100644 --- a/UI/Controls/RedFlagBanner.cs +++ b/UI/Controls/RedFlagBanner.cs @@ -20,6 +20,7 @@ public sealed class RedFlagBanner : Panel private readonly Label _title = new(); private readonly Label _body = new(); private readonly Button _emergencyButton = new(); + private readonly TableLayoutPanel _stack = new(); private TranslationService? _t; private bool _darkMode; @@ -40,10 +41,16 @@ public void UpdateTranslation(TranslationService? translation, bool darkMode) private void ApplyTheme() { - BackColor = _darkMode ? Color.FromArgb(140, 30, 30) : Color.FromArgb(176, 0, 32); - ForeColor = Color.White; - _title.ForeColor = Color.White; - _body.ForeColor = Color.White; + Color bannerBackground = _darkMode + ? Color.FromArgb(140, 30, 30) + : Color.FromArgb(255, 214, 214); + Color textColor = _darkMode ? Color.White : Color.Black; + + BackColor = bannerBackground; + ForeColor = textColor; + _stack.BackColor = bannerBackground; + _title.ForeColor = textColor; + _body.ForeColor = textColor; _emergencyButton.BackColor = Color.White; _emergencyButton.ForeColor = Color.FromArgb(176, 0, 32); } @@ -61,28 +68,21 @@ public RedFlagBanner(TranslationService? translation, bool darkMode) Visible = false; BorderStyle = BorderStyle.FixedSingle; - BackColor = _darkMode ? Color.FromArgb(140, 30, 30) : Color.FromArgb(176, 0, 32); - ForeColor = Color.White; - string lang = (_t?.CurrentLanguage ?? "en").ToLowerInvariant(); if (lang == "ar") RightToLeft = RightToLeft.Yes; _title.AutoSize = true; _title.Font = new Font(Font.FontFamily, 11f, FontStyle.Bold); - _title.ForeColor = Color.White; _title.Text = T("RedFlag_Banner_Title"); _title.Margin = new Padding(0, 0, 0, 4); _body.AutoSize = true; _body.Font = new Font(Font.FontFamily, 9.25f, FontStyle.Regular); - _body.ForeColor = Color.White; _body.MaximumSize = new Size(620, 0); _emergencyButton.Text = T("RedFlag_WhenToCall_Button"); _emergencyButton.AutoSize = true; _emergencyButton.FlatStyle = FlatStyle.Flat; - _emergencyButton.BackColor = Color.White; - _emergencyButton.ForeColor = Color.FromArgb(176, 0, 32); _emergencyButton.Font = new Font(Font.FontFamily, 9f, FontStyle.Bold); _emergencyButton.Margin = new Padding(0, 6, 0, 0); _emergencyButton.AccessibleName = T("RedFlag_WhenToCall_Button"); @@ -97,18 +97,16 @@ public RedFlagBanner(TranslationService? translation, bool darkMode) RightToLeft == RightToLeft.Yes ? MessageBoxOptions.RtlReading : 0); }; - var stack = new TableLayoutPanel - { - Dock = DockStyle.Fill, - AutoSize = true, - ColumnCount = 1, - BackColor = BackColor, - }; - stack.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); - stack.Controls.Add(_title, 0, 0); - stack.Controls.Add(_body, 0, 1); - stack.Controls.Add(_emergencyButton, 0, 2); - Controls.Add(stack); + _stack.Dock = DockStyle.Fill; + _stack.AutoSize = true; + _stack.ColumnCount = 1; + _stack.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + _stack.Controls.Add(_title, 0, 0); + _stack.Controls.Add(_body, 0, 1); + _stack.Controls.Add(_emergencyButton, 0, 2); + Controls.Add(_stack); + + ApplyTheme(); AccessibleName = "Red flag banner"; AccessibleDescription = "Possible urgent symptoms — educational guidance only"; diff --git a/UI/MainForm.cs b/UI/MainForm.cs index aea444c..f413d61 100644 --- a/UI/MainForm.cs +++ b/UI/MainForm.cs @@ -364,7 +364,7 @@ private void InitializeLayout() Dock = DockStyle.Top, AutoSize = true, WrapContents = true, - AutoScroll = false, // We'll wrap; container will cap height + AutoScroll = false, Padding = new Padding(3), FlowDirection = FlowDirection.LeftToRight, Margin = new Padding(0) @@ -458,6 +458,7 @@ private void InitializeLayout() filterBar.Controls.Add(_lblFilter); filterBar.Controls.Add(_filterBox); + filterBar.Controls.Add(_checkButton); filterBar.Controls.Add(_selectVisibleButton); filterBar.Controls.Add(_clearVisibleButton); filterBar.Controls.Add(_selectAllButton); @@ -772,13 +773,20 @@ private void InitializeLayout() Padding = new Padding(0) }; filterHost.Controls.Add(filterBar); - // After layout we can cap height dynamically + // Keep filter actions readable on smaller widths by forcing wrapping + // to the host width; overflow still remains scrollable. filterHost.Resize += (s, e) => { + int availableWidth = Math.Max(220, filterHost.ClientSize.Width - SystemInformation.VerticalScrollBarWidth - 6); + filterBar.MaximumSize = new Size(availableWidth, 0); int maxH = ScaleY(120); // max visible area for filter controls (DPI-scaled) if (filterBar.Height > maxH) { - filterHost.AutoScrollMinSize = new Size(filterBar.Width, filterBar.Height + 4); + filterHost.AutoScrollMinSize = new Size(availableWidth, filterBar.Height + 4); + } + else + { + filterHost.AutoScrollMinSize = Size.Empty; } }; leftPanel.Controls.Add(filterHost, 0, 0); @@ -810,7 +818,6 @@ private void InitializeLayout() topControls.Controls.Add(_lblLanguage); topControls.Controls.Add(_languageSelector); topControls.Controls.Add(_darkModeToggle); - topControls.Controls.Add(_checkButton); topControls.Controls.Add(_exitButton); topControls.Controls.Add(_syncButton); topControls.Controls.Add(_missingTransButton); diff --git a/specs/architecture-spec.md b/specs/architecture-spec.md index 6413279..8a0bc15 100644 --- a/specs/architecture-spec.md +++ b/specs/architecture-spec.md @@ -2,7 +2,7 @@ ## 1. High-Level Architecture -``` +```text ┌─────────────────────────────────────────────────────────┐ │ Program.cs │ │ (Entry point, exception handlers, logger bootstrap) │ @@ -62,7 +62,7 @@ ### 3.1 Startup Sequence -``` +```text Program.Main() ├── Create LoggerService(logs/) ├── Create MainForm @@ -86,7 +86,7 @@ Program.Main() ### 3.2 Match Computation Flow -``` +```text User clicks "Check" └── CheckButton_Click() ├── Collect _checkedSymptoms @@ -113,7 +113,7 @@ User clicks "Check" ### 3.3 Wikidata Sync Flow -``` +```text User clicks "Sync" └── SyncFromWikidataAsync() ├── WikidataImporter.FetchConditionsAsync(limit: 200) @@ -133,7 +133,7 @@ User clicks "Sync" ### 4.1 Current Dependencies -``` +```text MainForm ──depends──▶ SymptomCheckerService ──depends──▶ CategoriesService ──depends──▶ SynonymService @@ -213,7 +213,7 @@ Implementations: `JsonFileConditionDataProvider`, `InMemoryConditionDataProvider ## 6. Component Diagram -``` +```text ┌──────────────────────────────────────────────────┐ │ Application │ │ │ diff --git a/specs/data-spec.md b/specs/data-spec.md index e27d5e2..bfedead 100644 --- a/specs/data-spec.md +++ b/specs/data-spec.md @@ -49,7 +49,7 @@ ### 2.1 conditions.json -``` +```text { "conditions": [ { @@ -70,6 +70,7 @@ ``` **Constraints:** + - `name` must be non-empty after trimming - `symptoms` must contain at least 1 unique, non-empty string - Localized fields (`_fr`, `_ar`) are optional; absence triggers fallback to base field @@ -79,7 +80,7 @@ ### 2.2 categories.json -``` +```text { "categories": [ { @@ -94,12 +95,13 @@ **Current categories:** Respiratory, Gastrointestinal, Neurological, Musculoskeletal, Dermatological, ENT/Eye, Cardiac/Vascular, Endocrine/Metabolic, Genitourinary, Mental Health, General/Systemic, Sexual Health / STIs, Laboratory Findings. **Matching logic:** + 1. If `symptoms` array is non-empty → use exactly those symptom names 2. Else → for each symptom in vocabulary, check if any keyword is a case-insensitive substring ### 2.3 translations.json -``` +```text { "languages": string[] (required, min 1, unique; e.g. ["en","fr","ar"]), "ui": [{ "key": string, "en": string, "fr": string?, "ar": string? }], @@ -112,13 +114,14 @@ ``` **Notes:** + - `key` values must match canonical names used in code and other data files - `en` is required for `ui` and `messages` sections; optional for `symptoms`/`conditions`/`categories` where the key itself serves as the English label - Null or empty localized values trigger fallback ### 2.4 synonyms.json -``` +```text { "synonyms": [ { @@ -133,7 +136,7 @@ ### 2.5 settings.json (auto-generated) -``` +```text { "Language": string?, "DarkMode": boolean, @@ -184,6 +187,7 @@ Add a `"version"` field to the root of each data file: ``` Update schemas to include `version` as an optional string field. Application logic: + 1. Read `version` — if absent, assume `"1.0"` (backward compatible) 2. If version is newer than expected, warn user and proceed with best-effort parsing 3. On save, always write current version @@ -205,7 +209,7 @@ Update schemas to include `version` as an optional string field. Application log ### 4.1 Merge Algorithm (`MergeConditions`) -``` +```text For each incoming condition: 1. Skip if name is empty/whitespace 2. Lookup existing condition by name (case-insensitive) @@ -245,7 +249,7 @@ For each incoming condition: ### 5.1 UI Labels (`T()` method) -``` +```text 1. Look up key in ui[] 2. If found: return field for current language (fr/ar), or en if empty 3. If not in ui[]: look up key in messages[] @@ -255,7 +259,7 @@ For each incoming condition: ### 5.2 Symptom / Condition / Category Names -``` +```text 1. Look up key in respective array (symptoms[]/conditions[]/categories[]) 2. If found and localized value is non-empty: return localized value 3. Else: return the canonical key (which is the English name) @@ -263,7 +267,7 @@ For each incoming condition: ### 5.3 Condition Detail Fields (Treatments, Medications, CareAdvice) -``` +```text 1. Read language from TranslationService.CurrentLanguage 2. If "fr": use Treatments_Fr ?? Treatments, Medications_Fr ?? Medications, CareAdvice_Fr ?? CareAdvice 3. If "ar": use Treatments_Ar ?? Treatments, Medications_Ar ?? Medications, CareAdvice_Ar ?? CareAdvice diff --git a/specs/decision-rules-spec.md b/specs/decision-rules-spec.md index d89b5ae..78674ee 100644 --- a/specs/decision-rules-spec.md +++ b/specs/decision-rules-spec.md @@ -58,7 +58,7 @@ The McIsaac score adds an age adjustment to the Centor score to account for the ### 2.3 Computation -``` +```text McIsaac = Centor + AgeAdjustment Clamped to range [0, 5] ``` @@ -101,7 +101,7 @@ The PERC rule is designed to identify patients at very low risk for pulmonary em ### 3.3 Result -``` +```text PERC negative = ALL 8 criteria pass ``` @@ -155,7 +155,7 @@ The triage system flags symptom combinations and vital-sign thresholds that coul ### 4.4 Severity Priority System -``` +```text Priority 1 = Most critical (red — immediate concern) Priority 2 = High concern (orange — urgent review) Priority 3 = Moderate concern (yellow — prompt attention) @@ -167,7 +167,8 @@ Red flags are sorted by severity (ascending priority number), then alphabeticall ### 4.5 Triage Banner Format **LTR languages (EN, FR):** -``` + +```text Possible red flags: • [Flag 1 localized text] • [Flag 2 localized text] @@ -175,7 +176,8 @@ If these apply, consider seeking urgent medical attention. This tool is educatio ``` **RTL language (AR):** -``` + +```text [Header]: [Flag 1 localized text] • [Flag 2 localized text] • @@ -184,7 +186,7 @@ If these apply, consider seeking urgent medical attention. This tool is educatio ### 4.6 Evaluation Flow -``` +```text TriageService.EvaluateV2() 1. Call Evaluate() for symptom-only flags 2. Check each vital threshold → add keys @@ -219,7 +221,7 @@ Rules are hardcoded in `TriageService.cs` (static class) and `MainForm.cs` (Cent ### 6.2 Recommended Extension Approach -**Option A: JSON-Driven Rules (Preferred for simple thresholds)** +### Option A: JSON-Driven Rules (Preferred for simple thresholds) Define a `rules.json` file: @@ -248,7 +250,7 @@ Define a `rules.json` file: A `RulesEngine` service loads and evaluates these rules without code changes. -**Option B: Plugin-Based Rules (For complex scoring systems)** +### Option B: Plugin-Based Rules (For complex scoring systems) ```csharp public interface IDecisionRule diff --git a/specs/functional-requirements.md b/specs/functional-requirements.md index d62fb34..587a0a6 100644 --- a/specs/functional-requirements.md +++ b/specs/functional-requirements.md @@ -4,7 +4,7 @@ ### 1.1 Primary Flow — Symptom Check -``` +```text 1. Application starts → loads conditions.json, categories.json, synonyms.json, translations.json, settings.json 2. User optionally selects a language (EN/FR/AR) and theme (light/dark) 3. User filters symptoms using the filter text box and/or category selector @@ -19,7 +19,7 @@ ### 1.2 Wikidata Sync Flow -``` +```text 1. User clicks "Sync" 2. Application fires a SPARQL query to Wikidata (no API key) 3. Fetched conditions are merged into the local dataset (additive merge) @@ -29,7 +29,7 @@ ### 1.3 Export Flow -``` +```text 1. User right-clicks the results list 2. User selects Export (CSV / Markdown / HTML) 3. SaveFileDialog opens; user chooses path @@ -39,7 +39,7 @@ ### 1.4 Session Management -``` +```text 1. User clicks "Save" → saves selected symptoms + model + parameters to a JSON file 2. User clicks "Load" → restores a previously saved session 3. User clicks "Reset Settings" → reverts to defaults diff --git a/specs/future-extensions.md b/specs/future-extensions.md index a156c7b..552608b 100644 --- a/specs/future-extensions.md +++ b/specs/future-extensions.md @@ -46,6 +46,7 @@ public record MatchingOptions( ``` **Registration:** + ```csharp var models = new Dictionary { @@ -56,11 +57,13 @@ var models = new Dictionary ``` **Benefits:** + - Open/closed principle — add models without modifying existing code - Each model is independently testable - UI model selector populated from registry **Candidate New Models:** + | Model | Description | |---|---| | TF-IDF | Weight symptoms by inverse frequency across conditions | @@ -101,6 +104,7 @@ public enum RiskLevel { Low, Intermediate, High, Critical } ``` **Benefits:** + - Rules can be added without UI code changes - Criteria drive dynamic UI generation (checkboxes for Boolean, NumericUpDown for Numeric) - Scoring logic is testable in isolation @@ -132,6 +136,7 @@ Implementations: `JsonFileDataProvider` (current), `SqliteDataProvider` (future) **Description:** Interactive visualization showing how symptoms connect to conditions. **Implementation Notes:** + - Use a graph layout library (e.g., Microsoft Automatic Graph Layout — MSAGL) or custom GDI+ rendering - Nodes: symptoms (circles) and conditions (rectangles) - Edges: symptom→condition association @@ -146,6 +151,7 @@ Implementations: `JsonFileDataProvider` (current), `SqliteDataProvider` (future) **Description:** Track which symptoms are most/least selected across sessions for educational pattern analysis. **Implementation Notes:** + - Aggregate data stored locally in `analytics.json` - No personally identifiable data — only symptom selection counts - Bar chart or heatmap visualization @@ -157,6 +163,7 @@ Implementations: `JsonFileDataProvider` (current), `SqliteDataProvider` (future) **Description:** Richer educational information per condition beyond the current details dialog. **Proposed Content:** + - Prevalence estimate (from Wikidata or manual annotation) - Related conditions (conditions sharing ≥ 3 symptoms) - Differential diagnosis hints (educational) @@ -175,6 +182,7 @@ Implementations: `JsonFileDataProvider` (current), `SqliteDataProvider` (future) **Description:** Step-by-step tutorial mode for new users. **Steps:** + 1. Select a category → expand accordion 2. Check some symptoms → see instant feedback 3. Enter vitals → observe triage banner @@ -194,6 +202,7 @@ Implementations: `JsonFileDataProvider` (current), `SqliteDataProvider` (future) **Current:** EN, FR, AR **Candidate Languages:** + | Language | Code | RTL? | Notes | |---|---|---|---| | Spanish | es | No | Large user base | @@ -202,6 +211,7 @@ Implementations: `JsonFileDataProvider` (current), `SqliteDataProvider` (future) | Urdu | ur | Yes | Shares RTL infrastructure with AR | **Requirements per new language:** + 1. Add translations to `translations.json` (ui, symptoms, conditions, messages, categories, uiDetails sections) 2. Add `Name_Xx` and `Treatment_Xx` fields to conditions (or use fallback to EN) 3. Test RTL layout if applicable @@ -211,6 +221,7 @@ Implementations: `JsonFileDataProvider` (current), `SqliteDataProvider` (future) **Priority:** Low **Description:** CLI tool or script to: + - Extract all translation keys and their EN values - Generate a template file for translators - Validate completeness of a new language against EN baseline @@ -315,6 +326,7 @@ Use `partial class MainForm` to keep all code in the same class while splitting **Current tests:** `SymptomCheckerServiceTests`, `TriageServiceTests`, `TranslationServiceTests`, `CategoriesServiceTests`, `CategoryWeightingTests`, `NaiveBayesTemperatureTests` **Missing coverage:** + - `SynonymService` unit tests - `WikidataImporter` tests (mock HTTP) - `SchemaValidator` edge cases @@ -339,6 +351,7 @@ Produces a single `.exe` with embedded runtime — no .NET install required on t ### 7.2 MSIX Packaging Package as MSIX for Windows Store or enterprise sideloading: + - Auto-update support - Clean install/uninstall - Sandboxed file access (data files bundled as AppData) diff --git a/specs/non-functional-requirements.md b/specs/non-functional-requirements.md index 4f10995..16cb426 100644 --- a/specs/non-functional-requirements.md +++ b/specs/non-functional-requirements.md @@ -47,7 +47,7 @@ | `RightToLeft = Yes` and `RightToLeftLayout = true` applied at Form level | Implemented | | Individual controls explicitly set for RTL (lists, checkboxes, groups) | Implemented | | Bullet points reversed for RTL (appended to end) | Implemented in banner and details | -| Result items rendered with `TextFormatFlags.RightToLeft | Right` | Implemented in owner-draw | +| Result items rendered with `TextFormatFlags.RightToLeft \| Right` | Implemented in owner-draw | | Context menu mirrors for RTL | Implemented | ### 2.4 Contrast & Readability @@ -76,7 +76,7 @@ ### 3.2 Fallback Chain -``` +```text Requested language → available translation → English fallback → raw key ``` diff --git a/specs/security-and-compliance.md b/specs/security-and-compliance.md index d6013fc..82610b6 100644 --- a/specs/security-and-compliance.md +++ b/specs/security-and-compliance.md @@ -19,6 +19,7 @@ ### 1.2 Justification for Non-Regulated Status The application: + - Does **not** accept free-text patient descriptions - Does **not** store patient identifiers (name, DOB, address, insurance, etc.) - Does **not** render diagnoses — only educational condition matches @@ -80,13 +81,14 @@ The application: ### 3.2 No Inbound Connections The application: + - Does not open any listening ports - Does not register any URL handlers or protocol handlers - Does not expose any API endpoints ### 3.3 Wikidata Request Details -``` +```text POST https://query.wikidata.org/sparql Content-Type: application/x-www-form-urlencoded Accept: application/sparql-results+json @@ -120,6 +122,7 @@ At startup, the following data files are validated against their JSON schemas: | `translations.json` | `translations.schema.json` | NJsonSchema | **On validation failure:** + - Warning logged - MessageBox displayed to user - Application continues with valid data files; invalid files are skipped @@ -188,6 +191,7 @@ Every user-facing surface must display or reference the educational disclaimer: ### 6.2 Language Requirements All disclaimers must be: + - Available in all supported languages (EN, FR, AR) - Displayed in the currently selected language - Never truncated or hidden behind scroll @@ -195,6 +199,7 @@ All disclaimers must be: ### 6.3 No-Diagnosis Guarantee The application must never: + - Use the word "diagnosis" or "diagnose" in any user-facing text (use "match", "suggestion", "educational result") - Present results with > 100% confidence - Recommend specific medications or dosages without "educational only" qualifier diff --git a/specs/ux-ui-spec.md b/specs/ux-ui-spec.md index b428ceb..3756f00 100644 --- a/specs/ux-ui-spec.md +++ b/specs/ux-ui-spec.md @@ -19,7 +19,7 @@ ### 1.2 Top-Level Layout -``` +```text ┌─────────────────────────────────────────────────────────┐ │ [Top Bar] Language · Theme · Settings · Sync · Session │ ├────────────────────────┬────────────────────────────────┤ @@ -62,7 +62,7 @@ Layout uses nested `FlowLayoutPanel` containers: -``` +```text ▼ Category A (checkbox header — select/deselect all) ☐ Symptom 1 ☐ Symptom 2 @@ -138,10 +138,12 @@ PERC result label shows "PERC negative" or "PERC positive" (localized), updates | Double-click | Opens Details dialog for selected `ListItem` | **GroupHeader rendering:** + - Bold font, category-colored background stripe - Displays group name + item count **ListItem rendering:** + - Regular font, alternating row background (light mode) or uniform dark background (dark mode) - Columns: Condition name (localized) | Match score (%) | Model indicator icon - Hover: Highlight background @@ -150,7 +152,7 @@ PERC result label shows "PERC negative" or "PERC positive" (localized), updates Modal dialog shown on double-click of a result item: -``` +```text ┌──────────────────────────────────────┐ │ Condition Name (Localized) │ ├──────────────────────────────────────┤ @@ -201,6 +203,7 @@ Row of buttons below results list: | Export HTML | Save results to `.html` file | Styled HTML with inline CSS | All exports include: + - Timestamp - Language code - Model name @@ -274,7 +277,7 @@ All exports include: ### 5.2 Theme Application -``` +```text ApplyTheme(Control root, bool isDark) foreach control in root.Controls (recursive): set BackColor, ForeColor based on control type @@ -290,6 +293,7 @@ Theme changes apply immediately without restart. ### 6.1 RTL Activation When language = `ar`: + - `Form.RightToLeft = RightToLeft.Yes` - `Form.RightToLeftLayout = true` - All controls inherit RTL layout @@ -300,10 +304,10 @@ When language = `ar`: |---|---|---| | Text alignment | Left | Right | | Split panel order | Input left, Results right | Input right, Results left | -| Bullet lists | ` • text` | `text •` | +| Bullet lists | `• text` | `text •` | | Status bar | Left-aligned | Right-aligned | | Scroll bars | Right side | Left side | -| Triage banner bullets | ` • Flag` | `Flag •` | +| Triage banner bullets | `• Flag` | `Flag •` | ### 6.3 Font Consideration diff --git a/tests/SymptomChecker.Tests/OllamaServiceTests.cs b/tests/SymptomChecker.Tests/OllamaServiceTests.cs index ca96ec2..c93eb73 100644 --- a/tests/SymptomChecker.Tests/OllamaServiceTests.cs +++ b/tests/SymptomChecker.Tests/OllamaServiceTests.cs @@ -7,13 +7,23 @@ namespace SymptomChecker.Tests public class OllamaServiceTests { [Fact] - public void ChoosePreferredModel_PrefersKimiFamilyWhenDefaultExactNameIsUnavailable() + public void ChoosePreferredModel_PrefersGemmaFamilyWhenDefaultExactNameIsUnavailable() { - var models = new List { "llama3", "kimi-k2:latest", "mistral" }; + var models = new List { "llama3", "gemma4:latest", "mistral" }; - var selected = OllamaService.ChoosePreferredModel(models, "kimi-k2.6"); + var selected = OllamaService.ChoosePreferredModel(models, "gemma4"); - Assert.Equal("kimi-k2:latest", selected); + Assert.Equal("gemma4:latest", selected); + } + + [Fact] + public void ChoosePreferredModel_FallsBackToFirstAvailableWhenPreferredMissing() + { + var models = new List { "llama3", "mistral" }; + + var selected = OllamaService.ChoosePreferredModel(models, "gemma4"); + + Assert.Equal("llama3", selected); } [Fact] diff --git a/tests/SymptomChecker.Tests/SettingsServiceTests.cs b/tests/SymptomChecker.Tests/SettingsServiceTests.cs index 1fe423d..7f2e641 100644 --- a/tests/SymptomChecker.Tests/SettingsServiceTests.cs +++ b/tests/SymptomChecker.Tests/SettingsServiceTests.cs @@ -78,7 +78,7 @@ public void Load_MissingFile_UsesDefaultDetectionAndOllamaModels() var svc = new SettingsService(path); Assert.Equal("Ensemble", svc.Settings.Model); - Assert.Equal("kimi-k2.6", svc.Settings.OllamaModel); + Assert.Equal("gemma4", svc.Settings.OllamaModel); } finally { @@ -98,7 +98,7 @@ public void Reset_ClearsUiMode() svc.Reset(); Assert.Equal("Ensemble", svc.Settings.Model); - Assert.Equal("kimi-k2.6", svc.Settings.OllamaModel); + Assert.Equal("gemma4", svc.Settings.OllamaModel); Assert.Null(svc.Settings.UiMode); Assert.Null(svc.Settings.PatientWizardLastStep); Assert.Null(svc.Settings.CollapsedSections);