Compare commits
21 Commits
022-id-syn
...
a8f7147500
| Author | SHA1 | Date | |
|---|---|---|---|
| a8f7147500 | |||
| ce684bc5d1 | |||
| 484019e750 | |||
| 4ff6d307f8 | |||
| f4612c0737 | |||
| 5ec1254336 | |||
| b7d1ee2b71 | |||
| 87285d8f0a | |||
| 04b01eadb5 | |||
| 4d5b9e88dd | |||
| 4bad4ab4e2 | |||
| 3801ca13d9 | |||
| 999c0c54df | |||
| f9ac282596 | |||
| 5d42a6b930 | |||
| 99f19ac305 | |||
| 590ba49ddb | |||
| 2a5b225800 | |||
| 33433c3173 | |||
| 21e969a769 | |||
| 783644c6ad |
35
.agent/rules/specify-rules.md
Normal file
35
.agent/rules/specify-rules.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# ss-tools Development Guidelines
|
||||
|
||||
Auto-generated from all feature plans. Last updated: 2026-02-25
|
||||
|
||||
## Knowledge Graph (GRACE)
|
||||
**CRITICAL**: This project uses a GRACE Knowledge Graph for context. Always load the root map first:
|
||||
- **Root Map**: `.ai/ROOT.md` -> `[DEF:Project_Knowledge_Map:Root]`
|
||||
- **Project Map**: `.ai/PROJECT_MAP.md` -> `[DEF:Project_Map]`
|
||||
- **Standards**: Read `.ai/standards/` for architecture and style rules.
|
||||
|
||||
## Active Technologies
|
||||
|
||||
- (022-sync-id-cross-filters)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
src/
|
||||
tests/
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
# Add commands for
|
||||
|
||||
## Code Style
|
||||
|
||||
: Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
|
||||
- 022-sync-id-cross-filters: Added
|
||||
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
4
.agent/workflows/read_semantic.md
Normal file
4
.agent/workflows/read_semantic.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
description: USE SEMANTIC
|
||||
---
|
||||
Прочитай .specify/memory/semantics.md (или .ai/standards/semantics.md, если не найден). ОБЯЗАТЕЛЬНО используй его при разработке
|
||||
184
.agent/workflows/speckit.analyze.md
Normal file
184
.agent/workflows/speckit.analyze.md
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Goal
|
||||
|
||||
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
|
||||
|
||||
## Operating Constraints
|
||||
|
||||
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
|
||||
|
||||
**Constitution Authority**: The project constitution (`.ai/standards/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Initialize Analysis Context
|
||||
|
||||
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths:
|
||||
|
||||
- SPEC = FEATURE_DIR/spec.md
|
||||
- PLAN = FEATURE_DIR/plan.md
|
||||
- TASKS = FEATURE_DIR/tasks.md
|
||||
|
||||
Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command).
|
||||
For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
### 2. Load Artifacts (Progressive Disclosure)
|
||||
|
||||
Load only the minimal necessary context from each artifact:
|
||||
|
||||
**From spec.md:**
|
||||
|
||||
- Overview/Context
|
||||
- Functional Requirements
|
||||
- Non-Functional Requirements
|
||||
- User Stories
|
||||
- Edge Cases (if present)
|
||||
|
||||
**From plan.md:**
|
||||
|
||||
- Architecture/stack choices
|
||||
- Data Model references
|
||||
- Phases
|
||||
- Technical constraints
|
||||
|
||||
**From tasks.md:**
|
||||
|
||||
- Task IDs
|
||||
- Descriptions
|
||||
- Phase grouping
|
||||
- Parallel markers [P]
|
||||
- Referenced file paths
|
||||
|
||||
**From constitution:**
|
||||
|
||||
- Load `.ai/standards/constitution.md` for principle validation
|
||||
|
||||
### 3. Build Semantic Models
|
||||
|
||||
Create internal representations (do not include raw artifacts in output):
|
||||
|
||||
- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`)
|
||||
- **User story/action inventory**: Discrete user actions with acceptance criteria
|
||||
- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases)
|
||||
- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements
|
||||
|
||||
### 4. Detection Passes (Token-Efficient Analysis)
|
||||
|
||||
Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary.
|
||||
|
||||
#### A. Duplication Detection
|
||||
|
||||
- Identify near-duplicate requirements
|
||||
- Mark lower-quality phrasing for consolidation
|
||||
|
||||
#### B. Ambiguity Detection
|
||||
|
||||
- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria
|
||||
- Flag unresolved placeholders (TODO, TKTK, ???, `<placeholder>`, etc.)
|
||||
|
||||
#### C. Underspecification
|
||||
|
||||
- Requirements with verbs but missing object or measurable outcome
|
||||
- User stories missing acceptance criteria alignment
|
||||
- Tasks referencing files or components not defined in spec/plan
|
||||
|
||||
#### D. Constitution Alignment
|
||||
|
||||
- Any requirement or plan element conflicting with a MUST principle
|
||||
- Missing mandated sections or quality gates from constitution
|
||||
|
||||
#### E. Coverage Gaps
|
||||
|
||||
- Requirements with zero associated tasks
|
||||
- Tasks with no mapped requirement/story
|
||||
- Non-functional requirements not reflected in tasks (e.g., performance, security)
|
||||
|
||||
#### F. Inconsistency
|
||||
|
||||
- Terminology drift (same concept named differently across files)
|
||||
- Data entities referenced in plan but absent in spec (or vice versa)
|
||||
- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note)
|
||||
- Conflicting requirements (e.g., one requires Next.js while other specifies Vue)
|
||||
|
||||
### 5. Severity Assignment
|
||||
|
||||
Use this heuristic to prioritize findings:
|
||||
|
||||
- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality
|
||||
- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion
|
||||
- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case
|
||||
- **LOW**: Style/wording improvements, minor redundancy not affecting execution order
|
||||
|
||||
### 6. Produce Compact Analysis Report
|
||||
|
||||
Output a Markdown report (no file writes) with the following structure:
|
||||
|
||||
## Specification Analysis Report
|
||||
|
||||
| ID | Category | Severity | Location(s) | Summary | Recommendation |
|
||||
|----|----------|----------|-------------|---------|----------------|
|
||||
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
|
||||
|
||||
(Add one row per finding; generate stable IDs prefixed by category initial.)
|
||||
|
||||
**Coverage Summary Table:**
|
||||
|
||||
| Requirement Key | Has Task? | Task IDs | Notes |
|
||||
|-----------------|-----------|----------|-------|
|
||||
|
||||
**Constitution Alignment Issues:** (if any)
|
||||
|
||||
**Unmapped Tasks:** (if any)
|
||||
|
||||
**Metrics:**
|
||||
|
||||
- Total Requirements
|
||||
- Total Tasks
|
||||
- Coverage % (requirements with >=1 task)
|
||||
- Ambiguity Count
|
||||
- Duplication Count
|
||||
- Critical Issues Count
|
||||
|
||||
### 7. Provide Next Actions
|
||||
|
||||
At end of report, output a concise Next Actions block:
|
||||
|
||||
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
|
||||
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
|
||||
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
|
||||
|
||||
### 8. Offer Remediation
|
||||
|
||||
Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.)
|
||||
|
||||
## Operating Principles
|
||||
|
||||
### Context Efficiency
|
||||
|
||||
- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation
|
||||
- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis
|
||||
- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow
|
||||
- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts
|
||||
|
||||
### Analysis Guidelines
|
||||
|
||||
- **NEVER modify files** (this is read-only analysis)
|
||||
- **NEVER hallucinate missing sections** (if absent, report them accurately)
|
||||
- **Prioritize constitution violations** (these are always CRITICAL)
|
||||
- **Use examples over exhaustive rules** (cite specific instances, not generic patterns)
|
||||
- **Report zero issues gracefully** (emit success report with coverage statistics)
|
||||
|
||||
## Context
|
||||
|
||||
$ARGUMENTS
|
||||
294
.agent/workflows/speckit.checklist.md
Normal file
294
.agent/workflows/speckit.checklist.md
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
description: Generate a custom checklist for the current feature based on user requirements.
|
||||
---
|
||||
|
||||
## Checklist Purpose: "Unit Tests for English"
|
||||
|
||||
**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain.
|
||||
|
||||
**NOT for verification/testing**:
|
||||
|
||||
- ❌ NOT "Verify the button clicks correctly"
|
||||
- ❌ NOT "Test error handling works"
|
||||
- ❌ NOT "Confirm the API returns 200"
|
||||
- ❌ NOT checking if code/implementation matches the spec
|
||||
|
||||
**FOR requirements quality validation**:
|
||||
|
||||
- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness)
|
||||
- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity)
|
||||
- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency)
|
||||
- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage)
|
||||
- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases)
|
||||
|
||||
**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works.
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Execution Steps
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list.
|
||||
- All file paths must be absolute.
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
|
||||
- Be generated from the user's phrasing + extracted signals from spec/plan/tasks
|
||||
- Only ask about information that materially changes checklist content
|
||||
- Be skipped individually if already unambiguous in `$ARGUMENTS`
|
||||
- Prefer precision over breadth
|
||||
|
||||
Generation algorithm:
|
||||
1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts").
|
||||
2. Cluster signals into candidate focus areas (max 4) ranked by relevance.
|
||||
3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit.
|
||||
4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria.
|
||||
5. Formulate questions chosen from these archetypes:
|
||||
- Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?")
|
||||
- Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?")
|
||||
- Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?")
|
||||
- Audience framing (e.g., "Will this be used by the author only or peers during PR review?")
|
||||
- Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?")
|
||||
- Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?")
|
||||
|
||||
Question formatting rules:
|
||||
- If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters
|
||||
- Limit to A–E options maximum; omit table if a free-form answer is clearer
|
||||
- Never ask the user to restate what they already said
|
||||
- Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope."
|
||||
|
||||
Defaults when interaction impossible:
|
||||
- Depth: Standard
|
||||
- Audience: Reviewer (PR) if code-related; Author otherwise
|
||||
- Focus: Top 2 relevance clusters
|
||||
|
||||
Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more.
|
||||
|
||||
3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
|
||||
- Derive checklist theme (e.g., security, review, deploy, ux)
|
||||
- Consolidate explicit must-have items mentioned by user
|
||||
- Map focus selections to category scaffolding
|
||||
- Infer any missing context from spec/plan/tasks (do NOT hallucinate)
|
||||
|
||||
4. **Load feature context**: Read from FEATURE_DIR:
|
||||
- spec.md: Feature requirements and scope
|
||||
- plan.md (if exists): Technical details, dependencies
|
||||
- tasks.md (if exists): Implementation tasks
|
||||
|
||||
**Context Loading Strategy**:
|
||||
- Load only necessary portions relevant to active focus areas (avoid full-file dumping)
|
||||
- Prefer summarizing long sections into concise scenario/requirement bullets
|
||||
- Use progressive disclosure: add follow-on retrieval only if gaps detected
|
||||
- If source docs are large, generate interim summary items instead of embedding raw text
|
||||
|
||||
5. **Generate checklist** - Create "Unit Tests for Requirements":
|
||||
- Create `FEATURE_DIR/checklists/` directory if it doesn't exist
|
||||
- Generate unique checklist filename:
|
||||
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
|
||||
- Format: `[domain].md`
|
||||
- If file exists, append to existing file
|
||||
- Number items sequentially starting from CHK001
|
||||
- Each `/speckit.checklist` run creates a NEW file (never overwrites existing checklists)
|
||||
|
||||
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
|
||||
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
|
||||
- **Completeness**: Are all necessary requirements present?
|
||||
- **Clarity**: Are requirements unambiguous and specific?
|
||||
- **Consistency**: Do requirements align with each other?
|
||||
- **Measurability**: Can requirements be objectively verified?
|
||||
- **Coverage**: Are all scenarios/edge cases addressed?
|
||||
|
||||
**Category Structure** - Group items by requirement quality dimensions:
|
||||
- **Requirement Completeness** (Are all necessary requirements documented?)
|
||||
- **Requirement Clarity** (Are requirements specific and unambiguous?)
|
||||
- **Requirement Consistency** (Do requirements align without conflicts?)
|
||||
- **Acceptance Criteria Quality** (Are success criteria measurable?)
|
||||
- **Scenario Coverage** (Are all flows/cases addressed?)
|
||||
- **Edge Case Coverage** (Are boundary conditions defined?)
|
||||
- **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?)
|
||||
- **Dependencies & Assumptions** (Are they documented and validated?)
|
||||
- **Ambiguities & Conflicts** (What needs clarification?)
|
||||
|
||||
**HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**:
|
||||
|
||||
❌ **WRONG** (Testing implementation):
|
||||
- "Verify landing page displays 3 episode cards"
|
||||
- "Test hover states work on desktop"
|
||||
- "Confirm logo click navigates home"
|
||||
|
||||
✅ **CORRECT** (Testing requirements quality):
|
||||
- "Are the exact number and layout of featured episodes specified?" [Completeness]
|
||||
- "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity]
|
||||
- "Are hover state requirements consistent across all interactive elements?" [Consistency]
|
||||
- "Are keyboard navigation requirements defined for all interactive UI?" [Coverage]
|
||||
- "Is the fallback behavior specified when logo image fails to load?" [Edge Cases]
|
||||
- "Are loading states defined for asynchronous episode data?" [Completeness]
|
||||
- "Does the spec define visual hierarchy for competing UI elements?" [Clarity]
|
||||
|
||||
**ITEM STRUCTURE**:
|
||||
Each item should follow this pattern:
|
||||
- Question format asking about requirement quality
|
||||
- Focus on what's WRITTEN (or not written) in the spec/plan
|
||||
- Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.]
|
||||
- Reference spec section `[Spec §X.Y]` when checking existing requirements
|
||||
- Use `[Gap]` marker when checking for missing requirements
|
||||
|
||||
**EXAMPLES BY QUALITY DIMENSION**:
|
||||
|
||||
Completeness:
|
||||
- "Are error handling requirements defined for all API failure modes? [Gap]"
|
||||
- "Are accessibility requirements specified for all interactive elements? [Completeness]"
|
||||
- "Are mobile breakpoint requirements defined for responsive layouts? [Gap]"
|
||||
|
||||
Clarity:
|
||||
- "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]"
|
||||
- "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]"
|
||||
- "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]"
|
||||
|
||||
Consistency:
|
||||
- "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]"
|
||||
- "Are card component requirements consistent between landing and detail pages? [Consistency]"
|
||||
|
||||
Coverage:
|
||||
- "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]"
|
||||
- "Are concurrent user interaction scenarios addressed? [Coverage, Gap]"
|
||||
- "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]"
|
||||
|
||||
Measurability:
|
||||
- "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]"
|
||||
- "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]"
|
||||
|
||||
**Scenario Classification & Coverage** (Requirements Quality Focus):
|
||||
- Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios
|
||||
- For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?"
|
||||
- If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]"
|
||||
- Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]"
|
||||
|
||||
**Traceability Requirements**:
|
||||
- MINIMUM: ≥80% of items MUST include at least one traceability reference
|
||||
- Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]`
|
||||
- If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]"
|
||||
|
||||
**Surface & Resolve Issues** (Requirements Quality Problems):
|
||||
Ask questions about the requirements themselves:
|
||||
- Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]"
|
||||
- Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]"
|
||||
- Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]"
|
||||
- Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]"
|
||||
- Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]"
|
||||
|
||||
**Content Consolidation**:
|
||||
- Soft cap: If raw candidate items > 40, prioritize by risk/impact
|
||||
- Merge near-duplicates checking the same requirement aspect
|
||||
- If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]"
|
||||
|
||||
**🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test:
|
||||
- ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior
|
||||
- ❌ References to code execution, user actions, system behavior
|
||||
- ❌ "Displays correctly", "works properly", "functions as expected"
|
||||
- ❌ "Click", "navigate", "render", "load", "execute"
|
||||
- ❌ Test cases, test plans, QA procedures
|
||||
- ❌ Implementation details (frameworks, APIs, algorithms)
|
||||
|
||||
**✅ REQUIRED PATTERNS** - These test requirements quality:
|
||||
- ✅ "Are [requirement type] defined/specified/documented for [scenario]?"
|
||||
- ✅ "Is [vague term] quantified/clarified with specific criteria?"
|
||||
- ✅ "Are requirements consistent between [section A] and [section B]?"
|
||||
- ✅ "Can [requirement] be objectively measured/verified?"
|
||||
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
|
||||
- ✅ "Does the spec define [missing aspect]?"
|
||||
|
||||
6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
|
||||
|
||||
7. **Report**: Output full path to created checklist, item count, and remind user that each run creates a new file. Summarize:
|
||||
- Focus areas selected
|
||||
- Depth level
|
||||
- Actor/timing
|
||||
- Any explicit user-specified must-have items incorporated
|
||||
|
||||
**Important**: Each `/speckit.checklist` command invocation creates a checklist file using short, descriptive names unless file already exists. This allows:
|
||||
|
||||
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
|
||||
- Simple, memorable filenames that indicate checklist purpose
|
||||
- Easy identification and navigation in the `checklists/` folder
|
||||
|
||||
To avoid clutter, use descriptive types and clean up obsolete checklists when done.
|
||||
|
||||
## Example Checklist Types & Sample Items
|
||||
|
||||
**UX Requirements Quality:** `ux.md`
|
||||
|
||||
Sample items (testing the requirements, NOT the implementation):
|
||||
|
||||
- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]"
|
||||
- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]"
|
||||
- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]"
|
||||
- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]"
|
||||
- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]"
|
||||
- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]"
|
||||
|
||||
**API Requirements Quality:** `api.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are error response formats specified for all failure scenarios? [Completeness]"
|
||||
- "Are rate limiting requirements quantified with specific thresholds? [Clarity]"
|
||||
- "Are authentication requirements consistent across all endpoints? [Consistency]"
|
||||
- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]"
|
||||
- "Is versioning strategy documented in requirements? [Gap]"
|
||||
|
||||
**Performance Requirements Quality:** `performance.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are performance requirements quantified with specific metrics? [Clarity]"
|
||||
- "Are performance targets defined for all critical user journeys? [Coverage]"
|
||||
- "Are performance requirements under different load conditions specified? [Completeness]"
|
||||
- "Can performance requirements be objectively measured? [Measurability]"
|
||||
- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]"
|
||||
|
||||
**Security Requirements Quality:** `security.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are authentication requirements specified for all protected resources? [Coverage]"
|
||||
- "Are data protection requirements defined for sensitive information? [Completeness]"
|
||||
- "Is the threat model documented and requirements aligned to it? [Traceability]"
|
||||
- "Are security requirements consistent with compliance obligations? [Consistency]"
|
||||
- "Are security failure/breach response requirements defined? [Gap, Exception Flow]"
|
||||
|
||||
## Anti-Examples: What NOT To Do
|
||||
|
||||
**❌ WRONG - These test implementation, not requirements:**
|
||||
|
||||
```markdown
|
||||
- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001]
|
||||
- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003]
|
||||
- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010]
|
||||
- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005]
|
||||
```
|
||||
|
||||
**✅ CORRECT - These test requirements quality:**
|
||||
|
||||
```markdown
|
||||
- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001]
|
||||
- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003]
|
||||
- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010]
|
||||
- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005]
|
||||
- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap]
|
||||
- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001]
|
||||
```
|
||||
|
||||
**Key Differences:**
|
||||
|
||||
- Wrong: Tests if the system works correctly
|
||||
- Correct: Tests if the requirements are written correctly
|
||||
- Wrong: Verification of behavior
|
||||
- Correct: Validation of requirement quality
|
||||
- Wrong: "Does it do X?"
|
||||
- Correct: "Is X clearly specified?"
|
||||
181
.agent/workflows/speckit.clarify.md
Normal file
181
.agent/workflows/speckit.clarify.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
|
||||
handoffs:
|
||||
- label: Build Technical Plan
|
||||
agent: speckit.plan
|
||||
prompt: Create a plan for the spec. I am building with...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
|
||||
|
||||
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
|
||||
|
||||
Execution steps:
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields:
|
||||
- `FEATURE_DIR`
|
||||
- `FEATURE_SPEC`
|
||||
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
|
||||
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
|
||||
|
||||
Functional Scope & Behavior:
|
||||
- Core user goals & success criteria
|
||||
- Explicit out-of-scope declarations
|
||||
- User roles / personas differentiation
|
||||
|
||||
Domain & Data Model:
|
||||
- Entities, attributes, relationships
|
||||
- Identity & uniqueness rules
|
||||
- Lifecycle/state transitions
|
||||
- Data volume / scale assumptions
|
||||
|
||||
Interaction & UX Flow:
|
||||
- Critical user journeys / sequences
|
||||
- Error/empty/loading states
|
||||
- Accessibility or localization notes
|
||||
|
||||
Non-Functional Quality Attributes:
|
||||
- Performance (latency, throughput targets)
|
||||
- Scalability (horizontal/vertical, limits)
|
||||
- Reliability & availability (uptime, recovery expectations)
|
||||
- Observability (logging, metrics, tracing signals)
|
||||
- Security & privacy (authN/Z, data protection, threat assumptions)
|
||||
- Compliance / regulatory constraints (if any)
|
||||
|
||||
Integration & External Dependencies:
|
||||
- External services/APIs and failure modes
|
||||
- Data import/export formats
|
||||
- Protocol/versioning assumptions
|
||||
|
||||
Edge Cases & Failure Handling:
|
||||
- Negative scenarios
|
||||
- Rate limiting / throttling
|
||||
- Conflict resolution (e.g., concurrent edits)
|
||||
|
||||
Constraints & Tradeoffs:
|
||||
- Technical constraints (language, storage, hosting)
|
||||
- Explicit tradeoffs or rejected alternatives
|
||||
|
||||
Terminology & Consistency:
|
||||
- Canonical glossary terms
|
||||
- Avoided synonyms / deprecated terms
|
||||
|
||||
Completion Signals:
|
||||
- Acceptance criteria testability
|
||||
- Measurable Definition of Done style indicators
|
||||
|
||||
Misc / Placeholders:
|
||||
- TODO markers / unresolved decisions
|
||||
- Ambiguous adjectives ("robust", "intuitive") lacking quantification
|
||||
|
||||
For each category with Partial or Missing status, add a candidate question opportunity unless:
|
||||
- Clarification would not materially change implementation or validation strategy
|
||||
- Information is better deferred to planning phase (note internally)
|
||||
|
||||
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
|
||||
- Maximum of 10 total questions across the whole session.
|
||||
- Each question must be answerable with EITHER:
|
||||
- A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR
|
||||
- A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words").
|
||||
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
|
||||
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
|
||||
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
|
||||
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
|
||||
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic.
|
||||
|
||||
4. Sequential questioning loop (interactive):
|
||||
- Present EXACTLY ONE question at a time.
|
||||
- For multiple‑choice questions:
|
||||
- **Analyze all options** and determine the **most suitable option** based on:
|
||||
- Best practices for the project type
|
||||
- Common patterns in similar implementations
|
||||
- Risk reduction (security, performance, maintainability)
|
||||
- Alignment with any explicit project goals or constraints visible in the spec
|
||||
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
|
||||
- Format as: `**Recommended:** Option [X] - <reasoning>`
|
||||
- Then render all options as a Markdown table:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| A | <Option A description> |
|
||||
| B | <Option B description> |
|
||||
| C | <Option C description> (add D/E as needed up to 5) |
|
||||
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
|
||||
|
||||
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
|
||||
- For short‑answer style (no meaningful discrete options):
|
||||
- Provide your **suggested answer** based on best practices and context.
|
||||
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
|
||||
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
|
||||
- After the user answers:
|
||||
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
|
||||
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
|
||||
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
|
||||
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
|
||||
- Stop asking further questions when:
|
||||
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
|
||||
- User signals completion ("done", "good", "no more"), OR
|
||||
- You reach 5 asked questions.
|
||||
- Never reveal future queued questions in advance.
|
||||
- If no valid questions exist at start, immediately report no critical ambiguities.
|
||||
|
||||
5. Integration after EACH accepted answer (incremental update approach):
|
||||
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
|
||||
- For the first integrated answer in this session:
|
||||
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
|
||||
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
|
||||
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
|
||||
- Then immediately apply the clarification to the most appropriate section(s):
|
||||
- Functional ambiguity → Update or add a bullet in Functional Requirements.
|
||||
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
|
||||
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
|
||||
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
|
||||
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
|
||||
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
|
||||
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
|
||||
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
|
||||
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
|
||||
- Keep each inserted clarification minimal and testable (avoid narrative drift).
|
||||
|
||||
6. Validation (performed after EACH write plus final pass):
|
||||
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
|
||||
- Total asked (accepted) questions ≤ 5.
|
||||
- Updated sections contain no lingering vague placeholders the new answer was meant to resolve.
|
||||
- No contradictory earlier statement remains (scan for now-invalid alternative choices removed).
|
||||
- Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`.
|
||||
- Terminology consistency: same canonical term used across all updated sections.
|
||||
|
||||
7. Write the updated spec back to `FEATURE_SPEC`.
|
||||
|
||||
8. Report completion (after questioning loop ends or early termination):
|
||||
- Number of questions asked & answered.
|
||||
- Path to updated spec.
|
||||
- Sections touched (list names).
|
||||
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
|
||||
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
|
||||
- Suggested next command.
|
||||
|
||||
Behavior rules:
|
||||
|
||||
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
|
||||
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
|
||||
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
|
||||
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
|
||||
- Respect user early termination signals ("stop", "done", "proceed").
|
||||
- If no questions asked due to full coverage, output a compact coverage summary (all categories Clear) then suggest advancing.
|
||||
- If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale.
|
||||
|
||||
Context for prioritization: $ARGUMENTS
|
||||
84
.agent/workflows/speckit.constitution.md
Normal file
84
.agent/workflows/speckit.constitution.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
|
||||
handoffs:
|
||||
- label: Build Specification
|
||||
agent: speckit.specify
|
||||
prompt: Implement the feature specification based on the updated constitution. I want to build...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
You are updating the project constitution at `.ai/standards/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
|
||||
|
||||
**Note**: If `.ai/standards/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
|
||||
|
||||
Follow this execution flow:
|
||||
|
||||
1. Load the existing constitution at `.ai/standards/constitution.md`.
|
||||
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
|
||||
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
|
||||
|
||||
2. Collect/derive values for placeholders:
|
||||
- If user input (conversation) supplies a value, use it.
|
||||
- Otherwise infer from existing repo context (README, docs, prior constitution versions if embedded).
|
||||
- For governance dates: `RATIFICATION_DATE` is the original adoption date (if unknown ask or mark TODO), `LAST_AMENDED_DATE` is today if changes are made, otherwise keep previous.
|
||||
- `CONSTITUTION_VERSION` must increment according to semantic versioning rules:
|
||||
- MAJOR: Backward incompatible governance/principle removals or redefinitions.
|
||||
- MINOR: New principle/section added or materially expanded guidance.
|
||||
- PATCH: Clarifications, wording, typo fixes, non-semantic refinements.
|
||||
- If version bump type ambiguous, propose reasoning before finalizing.
|
||||
|
||||
3. Draft the updated constitution content:
|
||||
- Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left).
|
||||
- Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance.
|
||||
- Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing non‑negotiable rules, explicit rationale if not obvious.
|
||||
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
|
||||
|
||||
4. Consistency propagation checklist (convert prior checklist into active validations):
|
||||
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
|
||||
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
|
||||
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
|
||||
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
|
||||
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
|
||||
|
||||
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
|
||||
- Version change: old → new
|
||||
- List of modified principles (old title → new title if renamed)
|
||||
- Added sections
|
||||
- Removed sections
|
||||
- Templates requiring updates (✅ updated / ⚠ pending) with file paths
|
||||
- Follow-up TODOs if any placeholders intentionally deferred.
|
||||
|
||||
6. Validation before final output:
|
||||
- No remaining unexplained bracket tokens.
|
||||
- Version line matches report.
|
||||
- Dates ISO format YYYY-MM-DD.
|
||||
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
|
||||
|
||||
7. Write the completed constitution back to `.ai/standards/constitution.md` (overwrite).
|
||||
|
||||
8. Output a final summary to the user with:
|
||||
- New version and bump rationale.
|
||||
- Any files flagged for manual follow-up.
|
||||
- Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`).
|
||||
|
||||
Formatting & Style Requirements:
|
||||
|
||||
- Use Markdown headings exactly as in the template (do not demote/promote levels).
|
||||
- Wrap long rationale lines to keep readability (<100 chars ideally) but do not hard enforce with awkward breaks.
|
||||
- Keep a single blank line between sections.
|
||||
- Avoid trailing whitespace.
|
||||
|
||||
If the user supplies partial updates (e.g., only one principle revision), still perform validation and version decision steps.
|
||||
|
||||
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
|
||||
|
||||
Do not create a new template; always operate on the existing `.ai/standards/constitution.md` file.
|
||||
199
.agent/workflows/speckit.fix.md
Normal file
199
.agent/workflows/speckit.fix.md
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
|
||||
description: Fix failing tests and implementation issues based on test reports
|
||||
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Goal
|
||||
|
||||
Analyze test failure reports, identify root causes, and fix implementation issues while preserving semantic protocol compliance.
|
||||
|
||||
## Operating Constraints
|
||||
|
||||
1. **USE CODER MODE**: Always switch to `coder` mode for code fixes
|
||||
2. **SEMANTIC PROTOCOL**: Never remove semantic annotations ([DEF], @TAGS). Only update code logic.
|
||||
3. **TEST DATA**: If tests use @TEST_DATA fixtures, preserve them when fixing
|
||||
4. **NO DELETION**: Never delete existing tests or semantic annotations
|
||||
5. **REPORT FIRST**: Always write a fix report before making changes
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Load Test Report
|
||||
|
||||
**Required**: Test report file path (e.g., `specs/<feature>/tests/reports/2026-02-19-report.md`)
|
||||
|
||||
**Parse the report for**:
|
||||
- Failed test cases
|
||||
- Error messages
|
||||
- Stack traces
|
||||
- Expected vs actual behavior
|
||||
- Affected modules/files
|
||||
|
||||
### 2. Analyze Root Causes
|
||||
|
||||
For each failed test:
|
||||
|
||||
1. **Read the test file** to understand what it's testing
|
||||
2. **Read the implementation file** to find the bug
|
||||
3. **Check semantic protocol compliance**:
|
||||
- Does the implementation have correct [DEF] anchors?
|
||||
- Are @TAGS (@PRE, @POST, @UX_STATE, etc.) present?
|
||||
- Does the code match the TIER requirements?
|
||||
4. **Identify the fix**:
|
||||
- Logic error in implementation
|
||||
- Missing error handling
|
||||
- Incorrect API usage
|
||||
- State management issue
|
||||
|
||||
### 3. Write Fix Report
|
||||
|
||||
Create a structured fix report:
|
||||
|
||||
```markdown
|
||||
# Fix Report: [FEATURE]
|
||||
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**Report**: [Test Report Path]
|
||||
**Fixer**: Coder Agent
|
||||
|
||||
## Summary
|
||||
|
||||
- Total Failed Tests: [X]
|
||||
- Total Fixed: [X]
|
||||
- Total Skipped: [X]
|
||||
|
||||
## Failed Tests Analysis
|
||||
|
||||
### Test: [Test Name]
|
||||
|
||||
**File**: `path/to/test.py`
|
||||
**Error**: [Error message]
|
||||
|
||||
**Root Cause**: [Explanation of why test failed]
|
||||
|
||||
**Fix Required**: [Description of fix]
|
||||
|
||||
**Status**: [Pending/In Progress/Completed]
|
||||
|
||||
## Fixes Applied
|
||||
|
||||
### Fix 1: [Description]
|
||||
|
||||
**Affected File**: `path/to/file.py`
|
||||
**Test Affected**: `[Test Name]`
|
||||
|
||||
**Changes**:
|
||||
```diff
|
||||
<<<<<<< SEARCH
|
||||
[Original Code]
|
||||
=======
|
||||
[Fixed Code]
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
**Verification**: [How to verify fix works]
|
||||
|
||||
**Semantic Integrity**: [Confirmed annotations preserved]
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [ ] Run tests to verify fix: `cd backend && .venv/bin/python3 -m pytest`
|
||||
- [ ] Check for related failing tests
|
||||
- [ ] Update test documentation if needed
|
||||
```
|
||||
|
||||
### 4. Apply Fixes (in Coder Mode)
|
||||
|
||||
Switch to `coder` mode and apply fixes:
|
||||
|
||||
1. **Read the implementation file** to get exact content
|
||||
2. **Apply the fix** using apply_diff
|
||||
3. **Preserve all semantic annotations**:
|
||||
- Keep [DEF:...] and [/DEF:...] anchors
|
||||
- Keep all @TAGS (@PURPOSE, @LAYER, @TIER, @RELATION, @PRE, @POST, @UX_STATE, @UX_FEEDBACK, @UX_RECOVERY)
|
||||
4. **Only update code logic** to fix the bug
|
||||
5. **Run tests** to verify the fix
|
||||
|
||||
### 5. Verification
|
||||
|
||||
After applying fixes:
|
||||
|
||||
1. **Run tests**:
|
||||
```bash
|
||||
cd backend && .venv/bin/python3 -m pytest -v
|
||||
```
|
||||
or
|
||||
```bash
|
||||
cd frontend && npm run test
|
||||
```
|
||||
|
||||
2. **Check test results**:
|
||||
- Failed tests should now pass
|
||||
- No new tests should fail
|
||||
- Coverage should not decrease
|
||||
|
||||
3. **Update fix report** with results:
|
||||
- Mark fixes as completed
|
||||
- Add verification steps
|
||||
- Note any remaining issues
|
||||
|
||||
## Output
|
||||
|
||||
Generate final fix report:
|
||||
|
||||
```markdown
|
||||
# Fix Report: [FEATURE] - COMPLETED
|
||||
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**Report**: [Test Report Path]
|
||||
**Fixer**: Coder Agent
|
||||
|
||||
## Summary
|
||||
|
||||
- Total Failed Tests: [X]
|
||||
- Total Fixed: [X] ✅
|
||||
- Total Skipped: [X]
|
||||
|
||||
## Fixes Applied
|
||||
|
||||
### Fix 1: [Description] ✅
|
||||
|
||||
**Affected File**: `path/to/file.py`
|
||||
**Test Affected**: `[Test Name]`
|
||||
|
||||
**Changes**: [Summary of changes]
|
||||
|
||||
**Verification**: All tests pass ✅
|
||||
|
||||
**Semantic Integrity**: Preserved ✅
|
||||
|
||||
## Test Results
|
||||
|
||||
```
|
||||
[Full test output showing all passing tests]
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- [ ] Monitor for similar issues
|
||||
- [ ] Update documentation if needed
|
||||
- [ ] Consider adding more tests for edge cases
|
||||
|
||||
## Related Files
|
||||
|
||||
- Test Report: [path]
|
||||
- Implementation: [path]
|
||||
- Test File: [path]
|
||||
```
|
||||
|
||||
## Context for Fixing
|
||||
|
||||
$ARGUMENTS
|
||||
135
.agent/workflows/speckit.implement.md
Normal file
135
.agent/workflows/speckit.implement.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
description: Execute the implementation plan by processing and executing all tasks defined in tasks.md
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Check checklists status** (if FEATURE_DIR/checklists/ exists):
|
||||
- Scan all checklist files in the checklists/ directory
|
||||
- For each checklist, count:
|
||||
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
|
||||
- Completed items: Lines matching `- [X]` or `- [x]`
|
||||
- Incomplete items: Lines matching `- [ ]`
|
||||
- Create a status table:
|
||||
|
||||
```text
|
||||
| Checklist | Total | Completed | Incomplete | Status |
|
||||
|-----------|-------|-----------|------------|--------|
|
||||
| ux.md | 12 | 12 | 0 | ✓ PASS |
|
||||
| test.md | 8 | 5 | 3 | ✗ FAIL |
|
||||
| security.md | 6 | 6 | 0 | ✓ PASS |
|
||||
```
|
||||
|
||||
- Calculate overall status:
|
||||
- **PASS**: All checklists have 0 incomplete items
|
||||
- **FAIL**: One or more checklists have incomplete items
|
||||
|
||||
- **If any checklist is incomplete**:
|
||||
- Display the table with incomplete item counts
|
||||
- **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)"
|
||||
- Wait for user response before continuing
|
||||
- If user says "no" or "wait" or "stop", halt execution
|
||||
- If user says "yes" or "proceed" or "continue", proceed to step 3
|
||||
|
||||
- **If all checklists are complete**:
|
||||
- Display the table showing all checklists passed
|
||||
- Automatically proceed to step 3
|
||||
|
||||
3. Load and analyze the implementation context:
|
||||
- **REQUIRED**: Read tasks.md for the complete task list and execution plan
|
||||
- **REQUIRED**: Read plan.md for tech stack, architecture, and file structure
|
||||
- **IF EXISTS**: Read data-model.md for entities and relationships
|
||||
- **IF EXISTS**: Read contracts/ for API specifications and test requirements
|
||||
- **IF EXISTS**: Read research.md for technical decisions and constraints
|
||||
- **IF EXISTS**: Read quickstart.md for integration scenarios
|
||||
|
||||
4. **Project Setup Verification**:
|
||||
- **REQUIRED**: Create/verify ignore files based on actual project setup:
|
||||
|
||||
**Detection & Creation Logic**:
|
||||
- Check if the following command succeeds to determine if the repository is a git repo (create/verify .gitignore if so):
|
||||
|
||||
```sh
|
||||
git rev-parse --git-dir 2>/dev/null
|
||||
```
|
||||
|
||||
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore
|
||||
- Check if .eslintrc* exists → create/verify .eslintignore
|
||||
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns
|
||||
- Check if .prettierrc* exists → create/verify .prettierignore
|
||||
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
|
||||
- Check if terraform files (*.tf) exist → create/verify .terraformignore
|
||||
- Check if .helmignore needed (helm charts present) → create/verify .helmignore
|
||||
|
||||
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
|
||||
**If ignore file missing**: Create with full pattern set for detected technology
|
||||
|
||||
**Common Patterns by Technology** (from plan.md tech stack):
|
||||
- **Node.js/JavaScript/TypeScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*`
|
||||
- **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/`
|
||||
- **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/`
|
||||
- **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/`
|
||||
- **Go**: `*.exe`, `*.test`, `vendor/`, `*.out`
|
||||
- **Ruby**: `.bundle/`, `log/`, `tmp/`, `*.gem`, `vendor/bundle/`
|
||||
- **PHP**: `vendor/`, `*.log`, `*.cache`, `*.env`
|
||||
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
|
||||
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
|
||||
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
|
||||
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `Makefile`, `config.log`, `.idea/`, `*.log`, `.env*`
|
||||
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
|
||||
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
|
||||
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
|
||||
|
||||
**Tool-Specific Patterns**:
|
||||
- **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/`
|
||||
- **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js`
|
||||
- **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
|
||||
- **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl`
|
||||
- **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt`
|
||||
|
||||
5. Parse tasks.md structure and extract:
|
||||
- **Task phases**: Setup, Tests, Core, Integration, Polish
|
||||
- **Task dependencies**: Sequential vs parallel execution rules
|
||||
- **Task details**: ID, description, file paths, parallel markers [P]
|
||||
- **Execution flow**: Order and dependency requirements
|
||||
|
||||
6. Execute implementation following the task plan:
|
||||
- **Phase-by-phase execution**: Complete each phase before moving to the next
|
||||
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
|
||||
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
|
||||
- **File-based coordination**: Tasks affecting the same files must run sequentially
|
||||
- **Validation checkpoints**: Verify each phase completion before proceeding
|
||||
|
||||
7. Implementation execution rules:
|
||||
- **Setup first**: Initialize project structure, dependencies, configuration
|
||||
- **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios
|
||||
- **Core development**: Implement models, services, CLI commands, endpoints
|
||||
- **Integration work**: Database connections, middleware, logging, external services
|
||||
- **Polish and validation**: Unit tests, performance optimization, documentation
|
||||
|
||||
8. Progress tracking and error handling:
|
||||
- Report progress after each completed task
|
||||
- Halt execution if any non-parallel task fails
|
||||
- For parallel tasks [P], continue with successful tasks, report failed ones
|
||||
- Provide clear error messages with context for debugging
|
||||
- Suggest next steps if implementation cannot proceed
|
||||
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.
|
||||
|
||||
9. Completion validation:
|
||||
- Verify all required tasks are completed
|
||||
- Check that implemented features match the original specification
|
||||
- Validate that tests pass and coverage meets requirements
|
||||
- Confirm the implementation follows the technical plan
|
||||
- Report final status with summary of completed work
|
||||
|
||||
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
|
||||
90
.agent/workflows/speckit.plan.md
Normal file
90
.agent/workflows/speckit.plan.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
|
||||
handoffs:
|
||||
- label: Create Tasks
|
||||
agent: speckit.tasks
|
||||
prompt: Break the plan into tasks
|
||||
send: true
|
||||
- label: Create Checklist
|
||||
agent: speckit.checklist
|
||||
prompt: Create a checklist for the following domain...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Load context**: Read FEATURE_SPEC and `.ai/standards/constitution.md`. Load IMPL_PLAN template (already copied).
|
||||
|
||||
3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
|
||||
- Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")
|
||||
- Fill Constitution Check section from constitution
|
||||
- Evaluate gates (ERROR if violations unjustified)
|
||||
- Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION)
|
||||
- Phase 1: Generate data-model.md, contracts/, quickstart.md
|
||||
- Phase 1: Update agent context by running the agent script
|
||||
- Re-evaluate Constitution Check post-design
|
||||
|
||||
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0: Outline & Research
|
||||
|
||||
1. **Extract unknowns from Technical Context** above:
|
||||
- For each NEEDS CLARIFICATION → research task
|
||||
- For each dependency → best practices task
|
||||
- For each integration → patterns task
|
||||
|
||||
2. **Generate and dispatch research agents**:
|
||||
|
||||
```text
|
||||
For each unknown in Technical Context:
|
||||
Task: "Research {unknown} for {feature context}"
|
||||
For each technology choice:
|
||||
Task: "Find best practices for {tech} in {domain}"
|
||||
```
|
||||
|
||||
3. **Consolidate findings** in `research.md` using format:
|
||||
- Decision: [what was chosen]
|
||||
- Rationale: [why chosen]
|
||||
- Alternatives considered: [what else evaluated]
|
||||
|
||||
**Output**: research.md with all NEEDS CLARIFICATION resolved
|
||||
|
||||
### Phase 1: Design & Contracts
|
||||
|
||||
**Prerequisites:** `research.md` complete
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships
|
||||
- Validation rules from requirements
|
||||
- State transitions if applicable
|
||||
|
||||
2. **Define interface contracts** (if project has external interfaces) → `/contracts/`:
|
||||
- Identify what interfaces the project exposes to users or other systems
|
||||
- Document the contract format appropriate for the project type
|
||||
- Examples: public APIs for libraries, command schemas for CLI tools, endpoints for web services, grammars for parsers, UI contracts for applications
|
||||
- Skip if project is purely internal (build scripts, one-off tools, etc.)
|
||||
|
||||
3. **Agent context update**:
|
||||
- Run `.specify/scripts/bash/update-agent-context.sh agy`
|
||||
- These scripts detect which AI agent is in use
|
||||
- Update the appropriate agent-specific context file
|
||||
- Add only new technology from current plan
|
||||
- Preserve manual additions between markers
|
||||
|
||||
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
|
||||
|
||||
## Key rules
|
||||
|
||||
- Use absolute paths
|
||||
- ERROR on gate failures or unresolved clarifications
|
||||
258
.agent/workflows/speckit.specify.md
Normal file
258
.agent/workflows/speckit.specify.md
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
description: Create or update the feature specification from a natural language feature description.
|
||||
handoffs:
|
||||
- label: Build Technical Plan
|
||||
agent: speckit.plan
|
||||
prompt: Create a plan for the spec. I am building with...
|
||||
- label: Clarify Spec Requirements
|
||||
agent: speckit.clarify
|
||||
prompt: Clarify specification requirements
|
||||
send: true
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
|
||||
|
||||
Given that feature description, do this:
|
||||
|
||||
1. **Generate a concise short name** (2-4 words) for the branch:
|
||||
- Analyze the feature description and extract the most meaningful keywords
|
||||
- Create a 2-4 word short name that captures the essence of the feature
|
||||
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
|
||||
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
|
||||
- Keep it concise but descriptive enough to understand the feature at a glance
|
||||
- Examples:
|
||||
- "I want to add user authentication" → "user-auth"
|
||||
- "Implement OAuth2 integration for the API" → "oauth2-api-integration"
|
||||
- "Create a dashboard for analytics" → "analytics-dashboard"
|
||||
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
||||
|
||||
2. **Check for existing branches before creating new one**:
|
||||
|
||||
a. First, fetch all remote branches to ensure we have the latest information:
|
||||
|
||||
```bash
|
||||
git fetch --all --prune
|
||||
```
|
||||
|
||||
b. Find the highest feature number across all sources for the short-name:
|
||||
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
|
||||
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
|
||||
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
|
||||
|
||||
c. Determine the next available number:
|
||||
- Extract all numbers from all three sources
|
||||
- Find the highest number N
|
||||
- Use N+1 for the new branch number
|
||||
|
||||
d. Run the script `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` with the calculated number and short-name:
|
||||
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
|
||||
- Bash example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"`
|
||||
- PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
|
||||
|
||||
**IMPORTANT**:
|
||||
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
|
||||
- Only match branches/directories with the exact short-name pattern
|
||||
- If no existing branches/directories found with this short-name, start with number 1
|
||||
- You must only ever run this script once per feature
|
||||
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
|
||||
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
|
||||
|
||||
3. Load `.specify/templates/spec-template.md` to understand required sections.
|
||||
|
||||
4. Follow this execution flow:
|
||||
|
||||
1. Parse user description from Input
|
||||
If empty: ERROR "No feature description provided"
|
||||
2. Extract key concepts from description
|
||||
Identify: actors, actions, data, constraints
|
||||
3. For unclear aspects:
|
||||
- Make informed guesses based on context and industry standards
|
||||
- Only mark with [NEEDS CLARIFICATION: specific question] if:
|
||||
- The choice significantly impacts feature scope or user experience
|
||||
- Multiple reasonable interpretations exist with different implications
|
||||
- No reasonable default exists
|
||||
- **LIMIT: Maximum 3 [NEEDS CLARIFICATION] markers total**
|
||||
- Prioritize clarifications by impact: scope > security/privacy > user experience > technical details
|
||||
4. Fill User Scenarios & Testing section
|
||||
If no clear user flow: ERROR "Cannot determine user scenarios"
|
||||
5. Generate Functional Requirements
|
||||
Each requirement must be testable
|
||||
Use reasonable defaults for unspecified details (document assumptions in Assumptions section)
|
||||
6. Define Success Criteria
|
||||
Create measurable, technology-agnostic outcomes
|
||||
Include both quantitative metrics (time, performance, volume) and qualitative measures (user satisfaction, task completion)
|
||||
Each criterion must be verifiable without implementation details
|
||||
7. Identify Key Entities (if data involved)
|
||||
8. Return: SUCCESS (spec ready for planning)
|
||||
|
||||
5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
|
||||
|
||||
6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria:
|
||||
|
||||
a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items:
|
||||
|
||||
```markdown
|
||||
# Specification Quality Checklist: [FEATURE NAME]
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: [DATE]
|
||||
**Feature**: [Link to spec.md]
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [ ] No implementation details (languages, frameworks, APIs)
|
||||
- [ ] Focused on user value and business needs
|
||||
- [ ] Written for non-technical stakeholders
|
||||
- [ ] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [ ] No [NEEDS CLARIFICATION] markers remain
|
||||
- [ ] Requirements are testable and unambiguous
|
||||
- [ ] Success criteria are measurable
|
||||
- [ ] Success criteria are technology-agnostic (no implementation details)
|
||||
- [ ] All acceptance scenarios are defined
|
||||
- [ ] Edge cases are identified
|
||||
- [ ] Scope is clearly bounded
|
||||
- [ ] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [ ] All functional requirements have clear acceptance criteria
|
||||
- [ ] User scenarios cover primary flows
|
||||
- [ ] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [ ] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
|
||||
```
|
||||
|
||||
b. **Run Validation Check**: Review the spec against each checklist item:
|
||||
- For each item, determine if it passes or fails
|
||||
- Document specific issues found (quote relevant spec sections)
|
||||
|
||||
c. **Handle Validation Results**:
|
||||
|
||||
- **If all items pass**: Mark checklist complete and proceed to step 6
|
||||
|
||||
- **If items fail (excluding [NEEDS CLARIFICATION])**:
|
||||
1. List the failing items and specific issues
|
||||
2. Update the spec to address each issue
|
||||
3. Re-run validation until all items pass (max 3 iterations)
|
||||
4. If still failing after 3 iterations, document remaining issues in checklist notes and warn user
|
||||
|
||||
- **If [NEEDS CLARIFICATION] markers remain**:
|
||||
1. Extract all [NEEDS CLARIFICATION: ...] markers from the spec
|
||||
2. **LIMIT CHECK**: If more than 3 markers exist, keep only the 3 most critical (by scope/security/UX impact) and make informed guesses for the rest
|
||||
3. For each clarification needed (max 3), present options to user in this format:
|
||||
|
||||
```markdown
|
||||
## Question [N]: [Topic]
|
||||
|
||||
**Context**: [Quote relevant spec section]
|
||||
|
||||
**What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
|
||||
|
||||
**Suggested Answers**:
|
||||
|
||||
| Option | Answer | Implications |
|
||||
|--------|--------|--------------|
|
||||
| A | [First suggested answer] | [What this means for the feature] |
|
||||
| B | [Second suggested answer] | [What this means for the feature] |
|
||||
| C | [Third suggested answer] | [What this means for the feature] |
|
||||
| Custom | Provide your own answer | [Explain how to provide custom input] |
|
||||
|
||||
**Your choice**: _[Wait for user response]_
|
||||
```
|
||||
|
||||
4. **CRITICAL - Table Formatting**: Ensure markdown tables are properly formatted:
|
||||
- Use consistent spacing with pipes aligned
|
||||
- Each cell should have spaces around content: `| Content |` not `|Content|`
|
||||
- Header separator must have at least 3 dashes: `|--------|`
|
||||
- Test that the table renders correctly in markdown preview
|
||||
5. Number questions sequentially (Q1, Q2, Q3 - max 3 total)
|
||||
6. Present all questions together before waiting for responses
|
||||
7. Wait for user to respond with their choices for all questions (e.g., "Q1: A, Q2: Custom - [details], Q3: B")
|
||||
8. Update the spec by replacing each [NEEDS CLARIFICATION] marker with the user's selected or provided answer
|
||||
9. Re-run validation after all clarifications are resolved
|
||||
|
||||
d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status
|
||||
|
||||
7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`).
|
||||
|
||||
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
## Quick Guidelines
|
||||
|
||||
- Focus on **WHAT** users need and **WHY**.
|
||||
- Avoid HOW to implement (no tech stack, APIs, code structure).
|
||||
- Written for business stakeholders, not developers.
|
||||
- DO NOT create any checklists that are embedded in the spec. That will be a separate command.
|
||||
|
||||
### Section Requirements
|
||||
|
||||
- **Mandatory sections**: Must be completed for every feature
|
||||
- **Optional sections**: Include only when relevant to the feature
|
||||
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
|
||||
|
||||
### For AI Generation
|
||||
|
||||
When creating this spec from a user prompt:
|
||||
|
||||
1. **Make informed guesses**: Use context, industry standards, and common patterns to fill gaps
|
||||
2. **Document assumptions**: Record reasonable defaults in the Assumptions section
|
||||
3. **Limit clarifications**: Maximum 3 [NEEDS CLARIFICATION] markers - use only for critical decisions that:
|
||||
- Significantly impact feature scope or user experience
|
||||
- Have multiple reasonable interpretations with different implications
|
||||
- Lack any reasonable default
|
||||
4. **Prioritize clarifications**: scope > security/privacy > user experience > technical details
|
||||
5. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
|
||||
6. **Common areas needing clarification** (only if no reasonable default exists):
|
||||
- Feature scope and boundaries (include/exclude specific use cases)
|
||||
- User types and permissions (if multiple conflicting interpretations possible)
|
||||
- Security/compliance requirements (when legally/financially significant)
|
||||
|
||||
**Examples of reasonable defaults** (don't ask about these):
|
||||
|
||||
- Data retention: Industry-standard practices for the domain
|
||||
- Performance targets: Standard web/mobile app expectations unless specified
|
||||
- Error handling: User-friendly messages with appropriate fallbacks
|
||||
- Authentication method: Standard session-based or OAuth2 for web apps
|
||||
- Integration patterns: Use project-appropriate patterns (REST/GraphQL for web services, function calls for libraries, CLI args for tools, etc.)
|
||||
|
||||
### Success Criteria Guidelines
|
||||
|
||||
Success criteria must be:
|
||||
|
||||
1. **Measurable**: Include specific metrics (time, percentage, count, rate)
|
||||
2. **Technology-agnostic**: No mention of frameworks, languages, databases, or tools
|
||||
3. **User-focused**: Describe outcomes from user/business perspective, not system internals
|
||||
4. **Verifiable**: Can be tested/validated without knowing implementation details
|
||||
|
||||
**Good examples**:
|
||||
|
||||
- "Users can complete checkout in under 3 minutes"
|
||||
- "System supports 10,000 concurrent users"
|
||||
- "95% of searches return results in under 1 second"
|
||||
- "Task completion rate improves by 40%"
|
||||
|
||||
**Bad examples** (implementation-focused):
|
||||
|
||||
- "API response time is under 200ms" (too technical, use "Users see results instantly")
|
||||
- "Database can handle 1000 TPS" (implementation detail, use user-facing metric)
|
||||
- "React components render efficiently" (framework-specific)
|
||||
- "Redis cache hit rate above 80%" (technology-specific)
|
||||
137
.agent/workflows/speckit.tasks.md
Normal file
137
.agent/workflows/speckit.tasks.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
|
||||
handoffs:
|
||||
- label: Analyze For Consistency
|
||||
agent: speckit.analyze
|
||||
prompt: Run a project analysis for consistency
|
||||
send: true
|
||||
- label: Implement Project
|
||||
agent: speckit.implement
|
||||
prompt: Start the implementation in phases
|
||||
send: true
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Load design documents**: Read from FEATURE_DIR:
|
||||
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
|
||||
- **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md (test scenarios)
|
||||
- Note: Not all projects have all documents. Generate tasks based on what's available.
|
||||
|
||||
3. **Execute task generation workflow**:
|
||||
- Load plan.md and extract tech stack, libraries, project structure
|
||||
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
|
||||
- If data-model.md exists: Extract entities and map to user stories
|
||||
- If contracts/ exists: Map interface contracts to user stories
|
||||
- If research.md exists: Extract decisions for setup tasks
|
||||
- Generate tasks organized by user story (see Task Generation Rules below)
|
||||
- Generate dependency graph showing user story completion order
|
||||
- Create parallel execution examples per user story
|
||||
- Validate task completeness (each user story has all needed tasks, independently testable)
|
||||
|
||||
4. **Generate tasks.md**: Use `.specify/templates/tasks-template.md` as structure, fill with:
|
||||
- Correct feature name from plan.md
|
||||
- Phase 1: Setup tasks (project initialization)
|
||||
- Phase 2: Foundational tasks (blocking prerequisites for all user stories)
|
||||
- Phase 3+: One phase per user story (in priority order from spec.md)
|
||||
- Each phase includes: story goal, independent test criteria, tests (if requested), implementation tasks
|
||||
- Final Phase: Polish & cross-cutting concerns
|
||||
- All tasks must follow the strict checklist format (see Task Generation Rules below)
|
||||
- Clear file paths for each task
|
||||
- Dependencies section showing story completion order
|
||||
- Parallel execution examples per story
|
||||
- Implementation strategy section (MVP first, incremental delivery)
|
||||
|
||||
5. **Report**: Output path to generated tasks.md and summary:
|
||||
- Total task count
|
||||
- Task count per user story
|
||||
- Parallel opportunities identified
|
||||
- Independent test criteria for each story
|
||||
- Suggested MVP scope (typically just User Story 1)
|
||||
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
|
||||
|
||||
Context for task generation: $ARGUMENTS
|
||||
|
||||
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
|
||||
|
||||
## Task Generation Rules
|
||||
|
||||
**CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing.
|
||||
|
||||
**Tests are OPTIONAL**: Only generate test tasks if explicitly requested in the feature specification or if user requests TDD approach.
|
||||
|
||||
### Checklist Format (REQUIRED)
|
||||
|
||||
Every task MUST strictly follow this format:
|
||||
|
||||
```text
|
||||
- [ ] [TaskID] [P?] [Story?] Description with file path
|
||||
```
|
||||
|
||||
**Format Components**:
|
||||
|
||||
1. **Checkbox**: ALWAYS start with `- [ ]` (markdown checkbox)
|
||||
2. **Task ID**: Sequential number (T001, T002, T003...) in execution order
|
||||
3. **[P] marker**: Include ONLY if task is parallelizable (different files, no dependencies on incomplete tasks)
|
||||
4. **[Story] label**: REQUIRED for user story phase tasks only
|
||||
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
|
||||
- Setup phase: NO story label
|
||||
- Foundational phase: NO story label
|
||||
- User Story phases: MUST have story label
|
||||
- Polish phase: NO story label
|
||||
5. **Description**: Clear action with exact file path
|
||||
|
||||
**Examples**:
|
||||
|
||||
- ✅ CORRECT: `- [ ] T001 Create project structure per implementation plan`
|
||||
- ✅ CORRECT: `- [ ] T005 [P] Implement authentication middleware in src/middleware/auth.py`
|
||||
- ✅ CORRECT: `- [ ] T012 [P] [US1] Create User model in src/models/user.py`
|
||||
- ✅ CORRECT: `- [ ] T014 [US1] Implement UserService in src/services/user_service.py`
|
||||
- ❌ WRONG: `- [ ] Create User model` (missing ID and Story label)
|
||||
- ❌ WRONG: `T001 [US1] Create model` (missing checkbox)
|
||||
- ❌ WRONG: `- [ ] [US1] Create User model` (missing Task ID)
|
||||
- ❌ WRONG: `- [ ] T001 [US1] Create model` (missing file path)
|
||||
|
||||
### Task Organization
|
||||
|
||||
1. **From User Stories (spec.md)** - PRIMARY ORGANIZATION:
|
||||
- Each user story (P1, P2, P3...) gets its own phase
|
||||
- Map all related components to their story:
|
||||
- Models needed for that story
|
||||
- Services needed for that story
|
||||
- Interfaces/UI needed for that story
|
||||
- If tests requested: Tests specific to that story
|
||||
- Mark story dependencies (most stories should be independent)
|
||||
|
||||
2. **From Contracts**:
|
||||
- Map each interface contract → to the user story it serves
|
||||
- If tests requested: Each interface contract → contract test task [P] before implementation in that story's phase
|
||||
|
||||
3. **From Data Model**:
|
||||
- Map each entity to the user story(ies) that need it
|
||||
- If entity serves multiple stories: Put in earliest story or Setup phase
|
||||
- Relationships → service layer tasks in appropriate story phase
|
||||
|
||||
4. **From Setup/Infrastructure**:
|
||||
- Shared infrastructure → Setup phase (Phase 1)
|
||||
- Foundational/blocking tasks → Foundational phase (Phase 2)
|
||||
- Story-specific setup → within that story's phase
|
||||
|
||||
### Phase Structure
|
||||
|
||||
- **Phase 1**: Setup (project initialization)
|
||||
- **Phase 2**: Foundational (blocking prerequisites - MUST complete before user stories)
|
||||
- **Phase 3+**: User Stories in priority order (P1, P2, P3...)
|
||||
- Within each story: Tests (if requested) → Models → Services → Endpoints → Integration
|
||||
- Each phase should be a complete, independently testable increment
|
||||
- **Final Phase**: Polish & Cross-Cutting Concerns
|
||||
30
.agent/workflows/speckit.taskstoissues.md
Normal file
30
.agent/workflows/speckit.taskstoissues.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts.
|
||||
tools: ['github/github-mcp-server/issue_write']
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
1. From the executed script, extract the path to **tasks**.
|
||||
1. Get the Git remote by running:
|
||||
|
||||
```bash
|
||||
git config --get remote.origin.url
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
|
||||
|
||||
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
|
||||
|
||||
> [!CAUTION]
|
||||
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL
|
||||
342
.agent/workflows/speckit.test.md
Normal file
342
.agent/workflows/speckit.test.md
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
description: ✅ GRACE‑Poly Tester Agent (Production Edition)
|
||||
---
|
||||
|
||||
# ✅ GRACE‑Poly Tester Agent (Production Edition)
|
||||
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
Если вход не пуст — он имеет приоритет и должен быть учтён при анализе.
|
||||
|
||||
---
|
||||
|
||||
# I. MANDATE(命)
|
||||
|
||||
Исполнить полный цикл тестирования:
|
||||
|
||||
1. Анализировать модули.
|
||||
2. Проверять соответствие TIER.
|
||||
3. Генерировать тесты строго из TEST_SPEC.
|
||||
4. Поддерживать документацию.
|
||||
5. Не нарушать существующие тесты.
|
||||
6. Проверять инварианты.
|
||||
|
||||
Тестер — не писатель тестов.
|
||||
Тестер — хранитель контрактов.
|
||||
|
||||
---
|
||||
|
||||
# II. НЕЗЫБЛЕМЫЕ ПРАВИЛА
|
||||
|
||||
1. **Никогда не удалять существующие тесты.**
|
||||
2. **Никогда не дублировать тесты.**
|
||||
3. Для CRITICAL — TEST_SPEC обязателен.
|
||||
4. Каждый `@TEST_EDGE` → минимум один тест.
|
||||
5. Каждый `@TEST_INVARIANT` → минимум один тест.
|
||||
6. Если CRITICAL без `@TEST_CONTRACT` →
|
||||
немедленно:
|
||||
|
||||
```
|
||||
[COHERENCE_CHECK_FAILED]
|
||||
Reason: Missing TEST_CONTRACT in CRITICAL module
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# III. АНАЛИЗ КОНТЕКСТА
|
||||
|
||||
Выполнить:
|
||||
|
||||
```
|
||||
.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
|
||||
```
|
||||
|
||||
Извлечь:
|
||||
|
||||
- FEATURE_DIR
|
||||
- TASKS_FILE
|
||||
- AVAILABLE_DOCS
|
||||
|
||||
---
|
||||
|
||||
# IV. ЗАГРУЗКА АРТЕФАКТОВ
|
||||
|
||||
### 1️⃣ Из tasks.md
|
||||
|
||||
- Найти завершённые implementation задачи
|
||||
- Исключить test‑tasks
|
||||
- Определить список модулей
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ Из модулей
|
||||
|
||||
Для каждого модуля:
|
||||
|
||||
- Прочитать `@TIER`
|
||||
- Прочитать:
|
||||
- `@TEST_CONTRACT`
|
||||
- `@TEST_FIXTURE`
|
||||
- `@TEST_EDGE`
|
||||
- `@TEST_INVARIANT`
|
||||
|
||||
Если CRITICAL и нет TEST_SPEC → STOP.
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ Сканирование существующих тестов
|
||||
|
||||
Искать в `__tests__/`.
|
||||
|
||||
Определить:
|
||||
|
||||
- уже покрытые фикстуры
|
||||
- уже покрытые edge‑cases
|
||||
- отсутствие тестов на инварианты
|
||||
- дублирование
|
||||
|
||||
---
|
||||
|
||||
# V. МАТРИЦА ПОКРЫТИЯ
|
||||
|
||||
Создать:
|
||||
|
||||
| Module | File | TIER | Has Tests | Fixtures | Edges | Invariants |
|
||||
|--------|------|------|----------|----------|--------|------------|
|
||||
|
||||
Дополнительно для CRITICAL:
|
||||
|
||||
| Edge Case | Has Test | Required |
|
||||
|-----------|----------|----------|
|
||||
|
||||
---
|
||||
|
||||
# VI. ГЕНЕРАЦИЯ ТЕСТОВ
|
||||
|
||||
---
|
||||
|
||||
## A. CRITICAL
|
||||
|
||||
Строгий алгоритм:
|
||||
|
||||
### 1️⃣ Валидация контракта
|
||||
|
||||
Создать helper‑валидатор, который проверяет:
|
||||
|
||||
- required_fields присутствуют
|
||||
- типы соответствуют
|
||||
- инварианты соблюдены
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ Для каждого @TEST_FIXTURE
|
||||
|
||||
Создать:
|
||||
|
||||
- 1 Happy-path тест
|
||||
- Проверку @POST
|
||||
- Проверку side-effects
|
||||
- Проверку отсутствия исключений
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ Для каждого @TEST_EDGE
|
||||
|
||||
Создать отдельный тест:
|
||||
|
||||
| Тип | Проверка |
|
||||
|------|----------|
|
||||
| missing_required_field | корректный отказ |
|
||||
| invalid_type | raise или skip |
|
||||
| empty_response | корректное поведение |
|
||||
| external_failure | rollback + лог |
|
||||
| duplicate | корректная обработка |
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ Для каждого @TEST_INVARIANT
|
||||
|
||||
Создать тест, который:
|
||||
|
||||
- нарушает инвариант
|
||||
- проверяет защитную реакцию
|
||||
|
||||
---
|
||||
|
||||
### 5️⃣ Проверка Rollback
|
||||
|
||||
Если модуль взаимодействует с БД:
|
||||
|
||||
- мокать исключение
|
||||
- проверять rollback()
|
||||
- проверять отсутствие частичного коммита
|
||||
|
||||
---
|
||||
|
||||
## B. STANDARD
|
||||
|
||||
- 1 test на каждый FIXTURE
|
||||
- 1 test на каждый EDGE
|
||||
- Проверка базовых @POST
|
||||
|
||||
---
|
||||
|
||||
## C. TRIVIAL
|
||||
|
||||
Тесты создаются только при отсутствии существующих.
|
||||
|
||||
---
|
||||
|
||||
# VII. UX CONTRACT TESTING
|
||||
|
||||
Для каждого Svelte компонента:
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣ Парсинг:
|
||||
|
||||
- @UX_STATE
|
||||
- @UX_FEEDBACK
|
||||
- @UX_RECOVERY
|
||||
- @UX_TEST
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ Генерация:
|
||||
|
||||
Для каждого `@UX_TEST` — отдельный тест.
|
||||
|
||||
Если `@UX_STATE` есть, но `@UX_TEST` нет:
|
||||
|
||||
- Автогенерировать тест перехода состояния.
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ Обязательные проверки:
|
||||
|
||||
- DOM‑класс
|
||||
- aria‑атрибут
|
||||
- визуальная обратная связь
|
||||
- возможность восстановления
|
||||
|
||||
---
|
||||
|
||||
# VIII. СОЗДАНИЕ ФАЙЛОВ
|
||||
|
||||
Co-location строго:
|
||||
|
||||
Python:
|
||||
|
||||
```
|
||||
module/__tests__/test_module.py
|
||||
```
|
||||
|
||||
Svelte:
|
||||
|
||||
```
|
||||
component/__tests__/Component.test.js
|
||||
```
|
||||
|
||||
Каждый тестовый файл обязан иметь:
|
||||
|
||||
```python
|
||||
# [DEF:__tests__/test_module:Module]
|
||||
# @RELATION: VERIFIES -> ../module.py
|
||||
# @PURPOSE: Contract testing for module
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# IX. ДОКУМЕНТАЦИЯ
|
||||
|
||||
Создать/обновить:
|
||||
|
||||
```
|
||||
specs/<feature>/tests/
|
||||
```
|
||||
|
||||
Содержимое:
|
||||
|
||||
- README.md — стратегия
|
||||
- coverage.md — матрица
|
||||
- reports/YYYY-MM-DD-report.md
|
||||
|
||||
---
|
||||
|
||||
# X. ИСПОЛНЕНИЕ
|
||||
|
||||
Backend:
|
||||
|
||||
```
|
||||
cd backend && .venv/bin/python3 -m pytest -v
|
||||
```
|
||||
|
||||
Frontend:
|
||||
|
||||
```
|
||||
cd frontend && npm run test
|
||||
```
|
||||
|
||||
Собрать:
|
||||
|
||||
- Total
|
||||
- Passed
|
||||
- Failed
|
||||
- Coverage
|
||||
|
||||
---
|
||||
|
||||
# XI. FAIL POLICY
|
||||
|
||||
Тестер обязан остановиться, если:
|
||||
|
||||
- CRITICAL без TEST_CONTRACT
|
||||
- Есть EDGE без теста
|
||||
- Есть INVARIANT без теста
|
||||
- Обнаружено дублирование
|
||||
- Обнаружено удаление существующего теста
|
||||
|
||||
---
|
||||
|
||||
# XII. OUTPUT FORMAT
|
||||
|
||||
```markdown
|
||||
# Test Report: [FEATURE]
|
||||
|
||||
Date: YYYY-MM-DD
|
||||
Executor: GRACE Tester
|
||||
|
||||
## Coverage Matrix
|
||||
|
||||
| Module | TIER | Tests | Edge Covered | Invariants Covered |
|
||||
|
||||
## Contract Validation
|
||||
|
||||
- TEST_CONTRACT validated ✅ / ❌
|
||||
- All FIXTURES tested ✅ / ❌
|
||||
- All EDGES tested ✅ / ❌
|
||||
- All INVARIANTS verified ✅ / ❌
|
||||
|
||||
## Results
|
||||
|
||||
Total:
|
||||
Passed:
|
||||
Failed:
|
||||
Skipped:
|
||||
|
||||
## Violations
|
||||
|
||||
| Module | Problem | Severity |
|
||||
|
||||
## Next Actions
|
||||
|
||||
- [ ] Add missing invariant test
|
||||
- [ ] Fix rollback behavior
|
||||
- [ ] Refactor duplicate tests
|
||||
```
|
||||
42
.ai/PERSONA.md
Normal file
42
.ai/PERSONA.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# [DEF:Std:UserPersona:Standard]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: persona, tone_of_voice, interaction_rules, architect
|
||||
# @PURPOSE: Defines how the AI Agent MUST interact with the user and the codebase.
|
||||
|
||||
@ROLE: Chief Semantic Architect & AI-Engineering Lead.
|
||||
@PHILOSOPHY: "Смысл первичен. Код вторичен. ИИ — это семантический процессор, а не собеседник."
|
||||
@METHODOLOGY: Создатель и строгий приверженец стандарта GRACE-Poly.
|
||||
|
||||
## ОЖИДАНИЯ ОТ AI-АГЕНТА (КАК СО МНОЙ РАБОТАТЬ)
|
||||
|
||||
1. **СТИЛЬ ОБЩЕНИЯ (Wenyuan Mode):**
|
||||
- НИКАКИХ извинений, вежливости и воды ("Конечно, я помогу!", "Извините за ошибку").
|
||||
- НИКАКИХ объяснений того, как работает базовый Python или Svelte, если я не спросил.
|
||||
- Отвечай предельно сухо, структурно и строго по делу. Максимум технической плотности.
|
||||
|
||||
2. **ОТНОШЕНИЕ К КОДУ:**
|
||||
- Я не принимаю "голый код". Любой код без Контракта (DbC) и Якорей `[DEF]...[/DEF]` считается мусором.
|
||||
- Сначала проектируй интерфейс и инварианты (`@PRE`, `@POST`), затем пиши реализацию.
|
||||
- Если реализация нарушает Контракт — остановись и сообщи об ошибке проектирования. Не пытайся "подогнать" логику в обход правил.
|
||||
|
||||
3. **БОРЬБА С "СЕМАНТИЧЕСКИМ КАЗИНО":**
|
||||
- Не угадывай. Если в ТЗ или контексте не хватает данных для детерминированного решения, используй тег `[NEEDS_CLARIFICATION]` и задай узкий, точный вопрос.
|
||||
- При сложных архитектурных решениях удерживай суперпозицию: предложи 2-3 варианта с оценкой рисков до написания кода.
|
||||
|
||||
4. **ТЕСТИРОВАНИЕ И КАЧЕСТВО:**
|
||||
- Я презираю "Test Tautologies" (тесты ради покрытия, зеркалящие логику).
|
||||
- Тесты должны быть Contract-Driven. Если есть `@PRE`, я ожидаю тест на его нарушение.
|
||||
- Тесты обязаны использовать `@TEST_DATA` из контрактов.
|
||||
|
||||
5. **ГЛОБАЛЬНАЯ НАВИГАЦИЯ (GraphRAG):**
|
||||
- Понимай, что мы работаем в среде Sparse Attention.
|
||||
- Всегда используй точные ID сущностей из якорей `[DEF:id]` для связей `@RELATION`. Не ломай семантические каналы опечатками.
|
||||
|
||||
## ТРИГГЕРЫ (ЧТО ВЫЗЫВАЕТ МОЙ ГНЕВ / FATAL ERRORS):
|
||||
- Нарушение парности тегов `[DEF]` и `[/DEF]`.
|
||||
- Написание тестов, которые "мокают" саму проверяемую систему.
|
||||
- Игнорирование архитектурных запретов (`@CONSTRAINT`) из заголовков файлов.
|
||||
|
||||
**Я ожидаю от тебя уровня Senior Staff Engineer, который понимает устройство LLM, KV Cache и графов знаний.**
|
||||
|
||||
# [/DEF:Std:UserPersona:Standard]
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
## 1. SYSTEM STANDARDS (Rules of the Game)
|
||||
Strict policies and formatting rules.
|
||||
* **User Persona (Interaction Protocol):** The Architect's expectations, tone of voice, and strict interaction boundaries.
|
||||
* Ref: `.ai/standards/persona.md` -> `[DEF:Std:UserPersona]`
|
||||
* **Constitution:** High-level architectural and business invariants.
|
||||
* Ref: `.ai/standards/constitution.md` -> `[DEF:Std:Constitution]`
|
||||
* **Architecture:** Service boundaries and tech stack decisions.
|
||||
@@ -30,9 +32,9 @@ Use these for code generation (Style Transfer).
|
||||
* Ref: `.ai/shots/critical_module.py` -> `[DEF:Shot:Critical_Module]`
|
||||
|
||||
## 3. DOMAIN MAP (Modules)
|
||||
* **Module Map:** `.ai/MODULE_MAP.md` -> `[DEF:Module_Map]`
|
||||
* **Project Map:** `.ai/PROJECT_MAP.md` -> `[DEF:Project_Map]`
|
||||
* **Apache Superset OpenAPI:** `.ai/openapi.json` -> `[DEF:Doc:Superset_OpenAPI]`
|
||||
* **High-level Module Map:** `.ai/structure/MODULE_MAP.md` -> `[DEF:Module_Map]`
|
||||
* **Low-level Project Map:** `.ai/structure/PROJECT_MAP.md` -> `[DEF:Project_Map]`
|
||||
* **Apache Superset OpenAPI:** `.ai/openapi/superset_openapi.json` -> `[DEF:Doc:Superset_OpenAPI]`
|
||||
* **Backend Core:** `backend/src/core` -> `[DEF:Module:Backend_Core]`
|
||||
* **Backend API:** `backend/src/api` -> `[DEF:Module:Backend_API]`
|
||||
* **Frontend Lib:** `frontend/src/lib` -> `[DEF:Module:Frontend_Lib]`
|
||||
|
||||
@@ -65,14 +65,34 @@
|
||||
|
||||
1. **CRITICAL** (Core/Security/**Complex UI**):
|
||||
- Требование: Полный контракт (включая **все @UX теги**), Граф, Инварианты, Строгие Логи.
|
||||
- **@TEST_DATA**: Обязательные эталонные данные для тестирования. Формат:
|
||||
```
|
||||
@TEST_DATA: fixture_name -> {JSON_PATH} | {INLINE_DATA}
|
||||
```
|
||||
Примеры:
|
||||
- `@TEST_DATA: valid_user -> {./fixtures/users.json#valid}`
|
||||
- `@TEST_DATA: empty_state -> {"dashboards": [], "total": 0}`
|
||||
- Tester Agent **ОБЯЗАН** использовать @TEST_DATA при написании тестов для CRITICAL модулей.
|
||||
```
|
||||
@TEST_CONTRACT: Обязательное описание структуры входных/выходных данных.
|
||||
Формат:
|
||||
@TEST_CONTRACT: Name -> {
|
||||
required_fields: {field: type},
|
||||
optional_fields: {field: type},
|
||||
invariants: [...]
|
||||
}
|
||||
|
||||
@TEST_FIXTURE: Эталонный корректный пример (happy-path).
|
||||
Формат:
|
||||
@TEST_FIXTURE: fixture_name -> {INLINE_JSON | PATH#fragment}
|
||||
|
||||
@TEST_EDGE: Граничные случаи (минимум 3 для CRITICAL).
|
||||
Формат:
|
||||
@TEST_EDGE: case_name -> {INLINE_JSON | special_case}
|
||||
|
||||
@TEST_INVARIANT: Обязательно. Связывает тесты с инвариантами.
|
||||
Формат:
|
||||
@TEST_INVARIANT: invariant_name -> verifies: [test_case_1, test_case_2]
|
||||
|
||||
Обязательные edge-типы для CRITICAL:
|
||||
- missing_required_field
|
||||
- empty_response
|
||||
- invalid_type
|
||||
- external_failure (exception)
|
||||
```
|
||||
- Tester Agent **ОБЯЗАН** использовать @TEST_CONTRACT, @TEST_FIXTURE и @TEST_EDGE при написании тестов для CRITICAL модулей.
|
||||
2. **STANDARD** (BizLogic/**Forms**):
|
||||
- Требование: Базовый контракт (@PURPOSE, @UX_STATE), Логи, @RELATION.
|
||||
- @TEST_DATA: Рекомендуется для Complex Forms.
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
> High-level module structure for AI Context. Generated automatically.
|
||||
|
||||
**Generated:** 2026-02-24T21:04:43.328895
|
||||
**Generated:** 2026-02-25T20:19:23.587354
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total Modules:** 74
|
||||
- **Total Entities:** 1571
|
||||
- **Total Modules:** 77
|
||||
- **Total Entities:** 1811
|
||||
|
||||
## Module Hierarchy
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
### 📁 `routes/`
|
||||
|
||||
- 🏗️ **Layers:** API, UI (API)
|
||||
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 182, TRIVIAL: 4
|
||||
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 187, TRIVIAL: 5
|
||||
- 📄 **Files:** 17
|
||||
- 📦 **Entities:** 188
|
||||
- 📦 **Entities:** 194
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
### 📁 `__tests__/`
|
||||
|
||||
- 🏗️ **Layers:** API, Domain (Tests), UI (API Tests)
|
||||
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 33, TRIVIAL: 81
|
||||
- 📄 **Files:** 7
|
||||
- 📦 **Entities:** 117
|
||||
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 36, TRIVIAL: 98
|
||||
- 📄 **Files:** 8
|
||||
- 📦 **Entities:** 137
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -126,9 +126,9 @@
|
||||
### 📁 `core/`
|
||||
|
||||
- 🏗️ **Layers:** Core
|
||||
- 📊 **Tiers:** STANDARD: 116, TRIVIAL: 7
|
||||
- 📄 **Files:** 9
|
||||
- 📦 **Entities:** 123
|
||||
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 125, TRIVIAL: 8
|
||||
- 📄 **Files:** 10
|
||||
- 📦 **Entities:** 135
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -138,6 +138,8 @@
|
||||
- Custom logging formatter that adds belief state prefixes to ...
|
||||
- ℂ **ConfigManager** (Class)
|
||||
- A class to handle application configuration persistence and ...
|
||||
- ℂ **IdMappingService** (Class) `[CRITICAL]`
|
||||
- Service handling the cataloging and retrieval of remote Supe...
|
||||
- ℂ **LogEntry** (Class)
|
||||
- A Pydantic model representing a single, structured log entry...
|
||||
- ℂ **MigrationEngine** (Class)
|
||||
@@ -150,8 +152,6 @@
|
||||
- Scans a specified directory for Python modules, dynamically ...
|
||||
- ℂ **SchedulerService** (Class)
|
||||
- Provides a service to manage scheduled backup tasks.
|
||||
- ℂ **SessionLocal** (Class) `[TRIVIAL]`
|
||||
- A session factory for the main mappings database.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
- 🔗 DEPENDS_ON -> ConfigModels
|
||||
- 🔗 DEPENDS_ON -> PyYAML
|
||||
- 🔗 DEPENDS_ON -> backend.src.core.auth.config
|
||||
- 🔗 DEPENDS_ON -> backend.src.models.mapping
|
||||
- 🔗 DEPENDS_ON -> backend.src.core.logger
|
||||
|
||||
### 📁 `auth/`
|
||||
|
||||
@@ -210,9 +210,9 @@
|
||||
### 📁 `__tests__/`
|
||||
|
||||
- 🏗️ **Layers:** Infra
|
||||
- 📊 **Tiers:** STANDARD: 9
|
||||
- 📊 **Tiers:** STANDARD: 11, TRIVIAL: 1
|
||||
- 📄 **Files:** 1
|
||||
- 📦 **Entities:** 9
|
||||
- 📦 **Entities:** 12
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -296,9 +296,9 @@
|
||||
### 📁 `models/`
|
||||
|
||||
- 🏗️ **Layers:** Domain, Model
|
||||
- 📊 **Tiers:** CRITICAL: 9, STANDARD: 21, TRIVIAL: 21
|
||||
- 📊 **Tiers:** CRITICAL: 9, STANDARD: 22, TRIVIAL: 22
|
||||
- 📄 **Files:** 11
|
||||
- 📦 **Entities:** 51
|
||||
- 📦 **Entities:** 53
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -334,14 +334,16 @@
|
||||
### 📁 `__tests__/`
|
||||
|
||||
- 🏗️ **Layers:** Domain
|
||||
- 📊 **Tiers:** STANDARD: 1, TRIVIAL: 1
|
||||
- 📄 **Files:** 1
|
||||
- 📦 **Entities:** 2
|
||||
- 📊 **Tiers:** CRITICAL: 1, STANDARD: 1, TRIVIAL: 27
|
||||
- 📄 **Files:** 2
|
||||
- 📦 **Entities:** 29
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- 📦 **test_models** (Module) `[TRIVIAL]`
|
||||
- Unit tests for data models
|
||||
- 📦 **test_report_models** (Module) `[CRITICAL]`
|
||||
- Unit tests for report Pydantic models and their validators
|
||||
|
||||
### 📁 `plugins/`
|
||||
|
||||
@@ -483,9 +485,9 @@
|
||||
### 📁 `scripts/`
|
||||
|
||||
- 🏗️ **Layers:** Scripts, Unknown
|
||||
- 📊 **Tiers:** STANDARD: 17, TRIVIAL: 2
|
||||
- 📄 **Files:** 5
|
||||
- 📦 **Entities:** 19
|
||||
- 📊 **Tiers:** STANDARD: 26, TRIVIAL: 2
|
||||
- 📄 **Files:** 6
|
||||
- 📦 **Entities:** 28
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -497,6 +499,8 @@
|
||||
- Migrates legacy config and task history from SQLite/file sto...
|
||||
- 📦 **backend.src.scripts.seed_permissions** (Module)
|
||||
- Populates the auth database with initial system permissions.
|
||||
- 📦 **backend.src.scripts.seed_superset_load_test** (Module)
|
||||
- Creates randomized load-test data in Superset by cloning cha...
|
||||
- 📦 **test_dataset_dashboard_relations** (Module) `[TRIVIAL]`
|
||||
- Auto-generated module for backend/src/scripts/test_dataset_d...
|
||||
|
||||
@@ -540,17 +544,21 @@
|
||||
|
||||
### 📁 `__tests__/`
|
||||
|
||||
- 🏗️ **Layers:** Domain Tests, Service
|
||||
- 📊 **Tiers:** STANDARD: 14
|
||||
- 📄 **Files:** 2
|
||||
- 📦 **Entities:** 14
|
||||
- 🏗️ **Layers:** Domain, Domain Tests, Service
|
||||
- 📊 **Tiers:** CRITICAL: 1, STANDARD: 21, TRIVIAL: 7
|
||||
- 📄 **Files:** 3
|
||||
- 📦 **Entities:** 29
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- ℂ **TestEncryptionManager** (Class)
|
||||
- Validate EncryptionManager encrypt/decrypt roundtrip, unique...
|
||||
- 📦 **backend.src.services.__tests__.test_llm_prompt_templates** (Module)
|
||||
- Validate normalization and rendering behavior for configurab...
|
||||
- 📦 **backend.src.services.__tests__.test_resource_service** (Module)
|
||||
- Unit tests for ResourceService
|
||||
- 📦 **test_encryption_manager** (Module) `[CRITICAL]`
|
||||
- Unit tests for EncryptionManager encrypt/decrypt functionali...
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
@@ -584,41 +592,63 @@
|
||||
|
||||
### 📁 `__tests__/`
|
||||
|
||||
- 🏗️ **Layers:** Domain (Tests)
|
||||
- 📊 **Tiers:** CRITICAL: 1, TRIVIAL: 2
|
||||
- 📄 **Files:** 1
|
||||
- 📦 **Entities:** 3
|
||||
- 🏗️ **Layers:** Domain, Domain (Tests)
|
||||
- 📊 **Tiers:** CRITICAL: 2, TRIVIAL: 19
|
||||
- 📄 **Files:** 2
|
||||
- 📦 **Entities:** 21
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- 📦 **backend.tests.test_report_normalizer** (Module) `[CRITICAL]`
|
||||
- Validate unknown task type fallback and partial payload norm...
|
||||
- 📦 **test_report_service** (Module) `[CRITICAL]`
|
||||
- Unit tests for ReportsService list/detail operations
|
||||
|
||||
### 📁 `tests/`
|
||||
|
||||
- 🏗️ **Layers:** Domain (Tests), Test, Unknown
|
||||
- 📊 **Tiers:** STANDARD: 54, TRIVIAL: 19
|
||||
- 📄 **Files:** 7
|
||||
- 📦 **Entities:** 73
|
||||
- 🏗️ **Layers:** Core, Domain (Tests), Test, Unknown
|
||||
- 📊 **Tiers:** CRITICAL: 6, STANDARD: 80, TRIVIAL: 57
|
||||
- 📄 **Files:** 10
|
||||
- 📦 **Entities:** 143
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- ℂ **TestLogPersistence** (Class)
|
||||
- ℂ **TestLogPersistence** (Class) `[CRITICAL]`
|
||||
- Test suite for TaskLogPersistenceService.
|
||||
- ℂ **TestTaskContext** (Class)
|
||||
- Test suite for TaskContext.
|
||||
- ℂ **TestTaskLogger** (Class)
|
||||
- Test suite for TaskLogger.
|
||||
- ℂ **TestTaskPersistenceHelpers** (Class) `[CRITICAL]`
|
||||
- Test suite for TaskPersistenceService static helper methods.
|
||||
- ℂ **TestTaskPersistenceService** (Class) `[CRITICAL]`
|
||||
- Test suite for TaskPersistenceService CRUD operations.
|
||||
- 📦 **backend.tests.test_dashboards_api** (Module)
|
||||
- Contract-driven tests for Dashboard Hub API
|
||||
- 📦 **test_auth** (Module) `[TRIVIAL]`
|
||||
- Auto-generated module for backend/tests/test_auth.py
|
||||
- 📦 **test_log_persistence** (Module)
|
||||
- 📦 **test_log_persistence** (Module) `[CRITICAL]`
|
||||
- Unit tests for TaskLogPersistenceService.
|
||||
- 📦 **test_resource_hubs** (Module) `[TRIVIAL]`
|
||||
- Auto-generated module for backend/tests/test_resource_hubs.p...
|
||||
- 📦 **test_task_logger** (Module)
|
||||
- Unit tests for TaskLogger and TaskContext.
|
||||
- 📦 **test_smoke_plugins** (Module) `[TRIVIAL]`
|
||||
- Auto-generated module for backend/tests/test_smoke_plugins.p...
|
||||
|
||||
### 📁 `core/`
|
||||
|
||||
- 🏗️ **Layers:** Domain, Unknown
|
||||
- 📊 **Tiers:** STANDARD: 2, TRIVIAL: 31
|
||||
- 📄 **Files:** 3
|
||||
- 📦 **Entities:** 33
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- 📦 **backend.tests.core.test_mapping_service** (Module)
|
||||
- Unit tests for the IdMappingService matching UUIDs to intege...
|
||||
- 📦 **backend.tests.core.test_migration_engine** (Module)
|
||||
- Unit tests for MigrationEngine's cross-filter patching algor...
|
||||
- 📦 **test_defensive_guards** (Module) `[TRIVIAL]`
|
||||
- Auto-generated module for backend/tests/core/test_defensive_...
|
||||
|
||||
### 📁 `components/`
|
||||
|
||||
@@ -650,6 +680,18 @@
|
||||
- 🧩 **TaskList** (Component)
|
||||
- Displays a list of tasks with their status and execution det...
|
||||
|
||||
### 📁 `__tests__/`
|
||||
|
||||
- 🏗️ **Layers:** UI (Tests)
|
||||
- 📊 **Tiers:** CRITICAL: 1
|
||||
- 📄 **Files:** 1
|
||||
- 📦 **Entities:** 1
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- 📦 **frontend.src.components.__tests__.task_log_viewer** (Module) `[CRITICAL]`
|
||||
- Unit tests for TaskLogViewer component by mounting it and ob...
|
||||
|
||||
### 📁 `auth/`
|
||||
|
||||
- 🏗️ **Layers:** Component
|
||||
@@ -811,6 +853,24 @@
|
||||
- 🔗 DEPENDS_ON -> [DEF:api_module]
|
||||
- 🔗 DEPENDS_ON -> frontend.src.lib.api.api_module
|
||||
|
||||
### 📁 `__tests__/`
|
||||
|
||||
- 🏗️ **Layers:** Infra (Tests)
|
||||
- 📊 **Tiers:** CRITICAL: 1, STANDARD: 3
|
||||
- 📄 **Files:** 1
|
||||
- 📦 **Entities:** 4
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- ℂ **TestBuildReportQueryString** (Class)
|
||||
- Validate query string construction from filter options.
|
||||
- ℂ **TestGetReportsAsync** (Class)
|
||||
- Validate getReports and getReportDetail with mocked api.fetc...
|
||||
- ℂ **TestNormalizeApiError** (Class)
|
||||
- Validate error normalization for UI-state mapping.
|
||||
- 📦 **frontend.src.lib.api.__tests__.reports_api** (Module) `[CRITICAL]`
|
||||
- Unit tests for reports API client functions: query string bu...
|
||||
|
||||
### 📁 `auth/`
|
||||
|
||||
- 🏗️ **Layers:** Feature
|
||||
@@ -1027,9 +1087,15 @@
|
||||
|
||||
### 📁 `mocks/`
|
||||
|
||||
- 📊 **Tiers:** STANDARD: 3
|
||||
- 📄 **Files:** 3
|
||||
- 📦 **Entities:** 3
|
||||
- 🏗️ **Layers:** UI (Tests)
|
||||
- 📊 **Tiers:** STANDARD: 4
|
||||
- 📄 **Files:** 4
|
||||
- 📦 **Entities:** 4
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- 📦 **mock_env_public** (Module)
|
||||
- Mock for $env/static/public SvelteKit module in vitest
|
||||
|
||||
### 📁 `ui/`
|
||||
|
||||
@@ -1148,9 +1214,9 @@
|
||||
### 📁 `dashboards/`
|
||||
|
||||
- 🏗️ **Layers:** UI, Unknown
|
||||
- 📊 **Tiers:** CRITICAL: 1, TRIVIAL: 27
|
||||
- 📊 **Tiers:** CRITICAL: 1, TRIVIAL: 37
|
||||
- 📄 **Files:** 1
|
||||
- 📦 **Entities:** 28
|
||||
- 📦 **Entities:** 38
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -1259,9 +1325,9 @@
|
||||
### 📁 `settings/`
|
||||
|
||||
- 🏗️ **Layers:** UI, Unknown
|
||||
- 📊 **Tiers:** CRITICAL: 1, STANDARD: 1, TRIVIAL: 14
|
||||
- 📊 **Tiers:** CRITICAL: 1, STANDARD: 1, TRIVIAL: 23
|
||||
- 📄 **Files:** 2
|
||||
- 📦 **Entities:** 16
|
||||
- 📦 **Entities:** 25
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -1445,10 +1511,13 @@ graph TD
|
||||
__tests__-->|TESTS|backend
|
||||
__tests__-->|DEPENDS_ON|backend
|
||||
__tests__-->|DEPENDS_ON|backend
|
||||
__tests__-->|VERIFIES|backend
|
||||
core-->|USES|backend
|
||||
core-->|USES|backend
|
||||
core-->|DEPENDS_ON|backend
|
||||
core-->|DEPENDS_ON|backend
|
||||
core-->|DEPENDS_ON|backend
|
||||
core-->|DEPENDS_ON|backend
|
||||
auth-->|USES|backend
|
||||
auth-->|USES|backend
|
||||
auth-->|USES|backend
|
||||
@@ -1461,9 +1530,12 @@ graph TD
|
||||
models-->|DEPENDS_ON|backend
|
||||
models-->|USED_BY|backend
|
||||
models-->|INHERITS_FROM|backend
|
||||
__tests__-->|TESTS|backend
|
||||
llm_analysis-->|IMPLEMENTS|backend
|
||||
llm_analysis-->|IMPLEMENTS|backend
|
||||
storage-->|DEPENDS_ON|backend
|
||||
scripts-->|USES|backend
|
||||
scripts-->|USES|backend
|
||||
scripts-->|READS_FROM|backend
|
||||
scripts-->|READS_FROM|backend
|
||||
scripts-->|USES|backend
|
||||
@@ -1483,6 +1555,7 @@ graph TD
|
||||
services-->|USES|backend
|
||||
services-->|DEPENDS_ON|backend
|
||||
services-->|DEPENDS_ON|backend
|
||||
__tests__-->|TESTS|backend
|
||||
__tests__-->|DEPENDS_ON|backend
|
||||
__tests__-->|TESTS|backend
|
||||
reports-->|DEPENDS_ON|backend
|
||||
@@ -1493,7 +1566,11 @@ graph TD
|
||||
reports-->|DEPENDS_ON|backend
|
||||
reports-->|DEPENDS_ON|backend
|
||||
__tests__-->|TESTS|backend
|
||||
__tests__-->|TESTS|backend
|
||||
tests-->|TESTS|backend
|
||||
tests-->|TESTS|backend
|
||||
core-->|VERIFIES|backend
|
||||
core-->|VERIFIES|backend
|
||||
__tests__-->|VERIFIES|components
|
||||
__tests__-->|VERIFIES|lib
|
||||
__tests__-->|VERIFIES|lib
|
||||
@@ -280,6 +280,9 @@
|
||||
- 📝 Mock for $app/stores in tests
|
||||
- 📦 **environment** (`Mock`)
|
||||
- 📝 Mock for $app/environment in tests
|
||||
- 📦 **mock_env_public** (`Module`)
|
||||
- 📝 Mock for $env/static/public SvelteKit module in vitest
|
||||
- 🏗️ Layer: UI (Tests)
|
||||
- 📦 **frontend.src.lib.api.reports** (`Module`) `[CRITICAL]`
|
||||
- 📝 Wrapper-based reports API client for list/detail retrieval without direct native fetch usage.
|
||||
- 🏗️ Layer: Infra
|
||||
@@ -308,6 +311,16 @@
|
||||
- 📝 Retrieve paginated assistant conversation history.
|
||||
- ƒ **getAssistantConversations** (`Function`)
|
||||
- 📝 Retrieve paginated conversation list for assistant sidebar/history switcher.
|
||||
- 📦 **frontend.src.lib.api.__tests__.reports_api** (`Module`) `[CRITICAL]`
|
||||
- 📝 Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.
|
||||
- 🏗️ Layer: Infra (Tests)
|
||||
- 🔒 Invariant: Pure functions produce deterministic output. Async wrappers propagate structured errors.
|
||||
- ℂ **TestBuildReportQueryString** (`Class`)
|
||||
- 📝 Validate query string construction from filter options.
|
||||
- ℂ **TestNormalizeApiError** (`Class`)
|
||||
- 📝 Validate error normalization for UI-state mapping.
|
||||
- ℂ **TestGetReportsAsync** (`Class`)
|
||||
- 📝 Validate getReports and getReportDetail with mocked api.fetchApi.
|
||||
- 🧩 **Select** (`Component`) `[TRIVIAL]`
|
||||
- 📝 Standardized dropdown selection component.
|
||||
- 🏗️ Layer: Atom
|
||||
@@ -696,7 +709,8 @@
|
||||
- 🏗️ Layer: UI
|
||||
- 🔒 Invariant: Shows both local login form and ADFS SSO button.
|
||||
- ⬅️ READS_FROM `app`
|
||||
- ⬅️ READS_FROM `auth`
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- ƒ **handleLogin** (`Function`)
|
||||
- 📝 Submits the local login form to the backend.
|
||||
- ƒ **handleADFSLogin** (`Function`)
|
||||
@@ -765,6 +779,26 @@
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **getStatusBadgeClass** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **isGitBusy** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **setGitBusy** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **ensureGitConfigs** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **updateDashboardGitState** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **refreshDashboardGitState** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleGitInit** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleGitSync** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleGitCommit** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleGitPull** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleGitPush** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **getTaskStatusIcon** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **getPaginationRange** (`Function`) `[TRIVIAL]`
|
||||
@@ -789,8 +823,8 @@
|
||||
- 🏗️ Layer: Domain
|
||||
- 🔒 Invariant: Only accessible by users with Admin role.
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ⬅️ READS_FROM `t`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- ⬅️ READS_FROM `t`
|
||||
- ƒ **loadData** (`Function`)
|
||||
- 📝 Fetches roles and available permissions.
|
||||
- ƒ **openCreateModal** (`Function`)
|
||||
@@ -806,8 +840,8 @@
|
||||
- 🏗️ Layer: Feature
|
||||
- 🔒 Invariant: Only accessible by users with "admin:users" permission.
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ⬅️ READS_FROM `t`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- ⬅️ READS_FROM `t`
|
||||
- ƒ **loadData** (`Function`)
|
||||
- 📝 Fetches users and roles from the backend.
|
||||
- ƒ **openCreateModal** (`Function`)
|
||||
@@ -905,10 +939,12 @@
|
||||
- 📝 Page for the dataset column mapper tool.
|
||||
- 🏗️ Layer: UI
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- 🧩 **DebugPage** (`Component`) `[TRIVIAL]`
|
||||
- 📝 Page for system diagnostics and debugging.
|
||||
- 🏗️ Layer: UI
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- 📦 **SettingsPage** (`Page`) `[CRITICAL]`
|
||||
- 📝 Consolidated Settings Page - All settings in one place with tabbed navigation
|
||||
- 🏗️ Layer: UI
|
||||
@@ -916,6 +952,12 @@
|
||||
- 📦 **+page** (`Module`) `[TRIVIAL]`
|
||||
- 📝 Auto-generated module for frontend/src/routes/settings/+page.svelte
|
||||
- 🏗️ Layer: Unknown
|
||||
- ƒ **normalizeTab** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **readTabFromUrl** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **writeTabToUrl** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **loadSettings** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **normalizeLlmSettings** (`Function`) `[TRIVIAL]`
|
||||
@@ -928,7 +970,19 @@
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleTabChange** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **getTabClass** (`Function`) `[TRIVIAL]`
|
||||
- ƒ **loadMigrationSettings** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **loadMappingsPage** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **onMappingsSearchInput** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **onMappingsFilterChange** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **goToMappingsPage** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **saveMigrationSettings** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **triggerSyncNow** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleSave** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
@@ -947,6 +1001,8 @@
|
||||
- 🧩 **ConnectionsSettingsPage** (`Component`)
|
||||
- 📝 Page for managing database connection configurations.
|
||||
- 🏗️ Layer: UI
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- ƒ **handleSuccess** (`Function`)
|
||||
- 📝 Refreshes the connection list after a successful creation.
|
||||
- 🧩 **GitSettingsPage** (`Component`)
|
||||
@@ -954,6 +1010,8 @@
|
||||
- 🏗️ Layer: Page
|
||||
- 🔒 Invariant: All configurations must be validated via connection test.
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ⬅️ READS_FROM `t`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- ƒ **loadConfigs** (`Function`)
|
||||
- 📝 Fetches existing git configurations.
|
||||
- ƒ **handleTest** (`Function`)
|
||||
@@ -983,6 +1041,8 @@
|
||||
- 📝 The main settings page for the application, allowing management of environments and global settings.
|
||||
- 🏗️ Layer: UI
|
||||
- 🔒 Invariant: Settings changes must be saved to the backend.
|
||||
- ⬅️ READS_FROM `t`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- ƒ **loadSettings** (`Function`)
|
||||
- 📝 Loads settings from the backend.
|
||||
- ƒ **handleSaveGlobal** (`Function`)
|
||||
@@ -1084,7 +1144,9 @@
|
||||
- 🏗️ Layer: Feature
|
||||
- 🔒 Invariant: Each source database can be mapped to one target database.
|
||||
- ⚡ Events: update
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ➡️ WRITES_TO `props`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- ƒ **updateMapping** (`Function`)
|
||||
- 📝 Updates a mapping for a specific source database.
|
||||
- ƒ **getSuggestion** (`Function`)
|
||||
@@ -1161,6 +1223,7 @@
|
||||
- 🏗️ Layer: UI
|
||||
- ⬅️ READS_FROM `selectedTask`
|
||||
- ➡️ WRITES_TO `selectedTask`
|
||||
- ⬅️ READS_FROM `t`
|
||||
- ƒ **fetchTasks** (`Function`)
|
||||
- 📝 Fetches the list of recent tasks from the API.
|
||||
- ƒ **clearTasks** (`Function`)
|
||||
@@ -1180,9 +1243,9 @@
|
||||
- 🧩 **TaskRunner** (`Component`)
|
||||
- 📝 Connects to a WebSocket to display real-time logs for a running task with filtering support.
|
||||
- 🏗️ Layer: UI
|
||||
- ⬅️ READS_FROM `t`
|
||||
- ⬅️ READS_FROM `selectedTask`
|
||||
- ➡️ WRITES_TO `selectedTask`
|
||||
- ➡️ WRITES_TO `taskLogs`
|
||||
- ➡️ WRITES_TO `t`
|
||||
- ƒ **connect** (`Function`)
|
||||
- 📝 Establishes WebSocket connection with exponential backoff and filter parameters.
|
||||
- ƒ **handleFilterChange** (`Function`)
|
||||
@@ -1285,6 +1348,10 @@
|
||||
- 🏗️ Layer: Unknown
|
||||
- ƒ **statusColor** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **frontend.src.components.__tests__.task_log_viewer** (`Module`) `[CRITICAL]`
|
||||
- 📝 Unit tests for TaskLogViewer component by mounting it and observing the DOM.
|
||||
- 🏗️ Layer: UI (Tests)
|
||||
- 🔒 Invariant: Duplicate logs are never appended. Polling only active for in-progress tasks.
|
||||
- 🧩 **FileList** (`Component`)
|
||||
- 📝 Displays a table of files with metadata and actions.
|
||||
- 🏗️ Layer: UI
|
||||
@@ -1538,6 +1605,26 @@
|
||||
- 📝 Dependency for checking if the current user has a specific permission.
|
||||
- ƒ **permission_checker** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.src.scripts.seed_superset_load_test** (`Module`)
|
||||
- 📝 Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments.
|
||||
- 🏗️ Layer: Scripts
|
||||
- 🔒 Invariant: Created chart and dashboard names are globally unique for one script run.
|
||||
- ƒ **_parse_args** (`Function`)
|
||||
- 📝 Parses CLI arguments for load-test data generation.
|
||||
- ƒ **_extract_result_payload** (`Function`)
|
||||
- 📝 Normalizes Superset API payloads that may be wrapped in `result`.
|
||||
- ƒ **_extract_created_id** (`Function`)
|
||||
- 📝 Extracts object ID from create/update API response.
|
||||
- ƒ **_generate_unique_name** (`Function`)
|
||||
- 📝 Generates globally unique random names for charts/dashboards.
|
||||
- ƒ **_resolve_target_envs** (`Function`)
|
||||
- 📝 Resolves requested environment IDs from configuration.
|
||||
- ƒ **_build_chart_template_pool** (`Function`)
|
||||
- 📝 Builds a pool of source chart templates to clone in one environment.
|
||||
- ƒ **seed_superset_load_data** (`Function`)
|
||||
- 📝 Creates dashboards and cloned charts for load testing across target environments.
|
||||
- ƒ **main** (`Function`)
|
||||
- 📝 CLI entrypoint for Superset load-test data seeding.
|
||||
- 📦 **test_dataset_dashboard_relations** (`Module`) `[TRIVIAL]`
|
||||
- 📝 Auto-generated module for backend/src/scripts/test_dataset_dashboard_relations.py
|
||||
- 🏗️ Layer: Unknown
|
||||
@@ -1679,6 +1766,8 @@
|
||||
- 📝 Iterates through all pages to collect all data items.
|
||||
- ƒ **_validate_import_file** (`Function`)
|
||||
- 📝 Validates that the file to be imported is a valid ZIP with metadata.yaml.
|
||||
- ƒ **get_all_resources** (`Function`)
|
||||
- 📝 Fetches all resources of a given type with id, uuid, and name columns.
|
||||
- ƒ **extract_dataset_id_from_form_data** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **walk** (`Function`) `[TRIVIAL]`
|
||||
@@ -1854,10 +1943,16 @@
|
||||
- 🔗 DEPENDS_ON -> `PyYAML`
|
||||
- ℂ **MigrationEngine** (`Class`)
|
||||
- 📝 Engine for transforming Superset export ZIPs.
|
||||
- ƒ **__init__** (`Function`)
|
||||
- 📝 Initializes the migration engine with optional ID mapping service.
|
||||
- ƒ **transform_zip** (`Function`)
|
||||
- 📝 Extracts ZIP, replaces database UUIDs in YAMLs, and re-packages.
|
||||
- 📝 Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.
|
||||
- ƒ **_transform_yaml** (`Function`)
|
||||
- 📝 Replaces database_uuid in a single YAML file.
|
||||
- ƒ **_extract_chart_uuids_from_archive** (`Function`)
|
||||
- 📝 Scans the unpacked ZIP to map local exported integer IDs back to their UUIDs.
|
||||
- ƒ **_patch_dashboard_metadata** (`Function`)
|
||||
- 📝 Replaces integer IDs in json_metadata.
|
||||
- ℂ **PluginBase** (`Class`)
|
||||
- 📝 Defines the abstract base class that all plugins must implement to be recognized by the system. It enforces a common structure for plugin metadata and execution.
|
||||
- 🏗️ Layer: Core
|
||||
@@ -1881,6 +1976,26 @@
|
||||
- ℂ **PluginConfig** (`Class`)
|
||||
- 📝 A Pydantic model used to represent the validated configuration and metadata of a loaded plugin. This object is what gets exposed to the API layer.
|
||||
- 🏗️ Layer: Core
|
||||
- 📦 **backend.src.core.mapping_service** (`Module`) `[CRITICAL]`
|
||||
- 📝 Service for tracking and synchronizing Superset Resource IDs (UUID <-> Integer ID)
|
||||
- 🏗️ Layer: Core
|
||||
- 🔒 Invariant: sync_environment must handle remote API failures gracefully.
|
||||
- 🔗 DEPENDS_ON -> `backend.src.models.mapping (ResourceMapping, ResourceType)`
|
||||
- 🔗 DEPENDS_ON -> `backend.src.core.logger`
|
||||
- ℂ **IdMappingService** (`Class`) `[CRITICAL]`
|
||||
- 📝 Service handling the cataloging and retrieval of remote Superset Integer IDs.
|
||||
- ƒ **__init__** (`Function`)
|
||||
- 📝 Initializes the mapping service.
|
||||
- ƒ **start_scheduler** (`Function`)
|
||||
- 📝 Starts the background scheduler with a given cron string.
|
||||
- ƒ **sync_environment** (`Function`)
|
||||
- 📝 Fully synchronizes mapping for a specific environment.
|
||||
- ƒ **get_remote_id** (`Function`)
|
||||
- 📝 Retrieves the remote integer ID for a given universal UUID.
|
||||
- ƒ **get_remote_ids_batch** (`Function`)
|
||||
- 📝 Retrieves remote integer IDs for a list of universal UUIDs efficiently.
|
||||
- ƒ **sync_all** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.src.core.auth.config** (`Module`)
|
||||
- 📝 Centralized configuration for authentication and authorization.
|
||||
- 🏗️ Layer: Core
|
||||
@@ -2108,6 +2223,12 @@
|
||||
- 📝 Test that configure_logger updates task_log_level.
|
||||
- ƒ **test_enable_belief_state_flag** (`Function`)
|
||||
- 📝 Test that enable_belief_state flag controls belief_scope logging.
|
||||
- ƒ **test_belief_scope_missing_anchor** (`Function`)
|
||||
- 📝 Test @PRE condition: anchor_id must be provided
|
||||
- ƒ **test_configure_logger_post_conditions** (`Function`)
|
||||
- 📝 Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated.
|
||||
- ƒ **reset_logger_state** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **TaskLoggerModule** (`Module`) `[CRITICAL]`
|
||||
- 📝 Provides a dedicated logger for tasks with automatic source attribution.
|
||||
- 🏗️ Layer: Core
|
||||
@@ -2429,6 +2550,14 @@
|
||||
- 📝 Fetch all dashboards from the specified environment for the grid.
|
||||
- ƒ **execute_migration** (`Function`)
|
||||
- 📝 Execute the migration of selected dashboards.
|
||||
- ƒ **get_migration_settings** (`Function`)
|
||||
- 📝 Get current migration Cron string explicitly.
|
||||
- ƒ **update_migration_settings** (`Function`)
|
||||
- 📝 Update migration Cron string.
|
||||
- ƒ **get_resource_mappings** (`Function`)
|
||||
- 📝 Fetch synchronized object mappings with search, filtering, and pagination.
|
||||
- ƒ **trigger_sync_now** (`Function`)
|
||||
- 📝 Triggers an immediate ID synchronization for all environments.
|
||||
- 📦 **PluginsRouter** (`Module`)
|
||||
- 📝 Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.
|
||||
- 🏗️ Layer: UI (API)
|
||||
@@ -2611,6 +2740,8 @@
|
||||
- 📝 Build current-user tool catalog for LLM planner with operation contracts and defaults.
|
||||
- ƒ **_coerce_intent_entities** (`Function`)
|
||||
- 📝 Normalize intent entity value types from LLM output to route-compatible values.
|
||||
- ƒ **_confirmation_summary** (`Function`)
|
||||
- 📝 Build human-readable confirmation prompt for an intent before execution.
|
||||
- ƒ **_clarification_text_for_intent** (`Function`)
|
||||
- 📝 Convert technical missing-parameter errors into user-facing clarification prompts.
|
||||
- ƒ **_plan_intent_with_llm** (`Function`)
|
||||
@@ -2631,6 +2762,8 @@
|
||||
- 📝 Retrieve paginated assistant conversation history for current user.
|
||||
- ƒ **get_assistant_audit** (`Function`)
|
||||
- 📝 Return assistant audit decisions for current user from persistent and in-memory stores.
|
||||
- ƒ **_label** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **storage_routes** (`Module`)
|
||||
- 📝 API endpoints for file storage management (backups and repositories).
|
||||
- 🏗️ Layer: API
|
||||
@@ -2702,9 +2835,14 @@
|
||||
- 📦 **DashboardChartItem** (`DataClass`)
|
||||
- 📦 **DashboardDatasetItem** (`DataClass`)
|
||||
- 📦 **DashboardDetailResponse** (`DataClass`)
|
||||
- 📦 **DatabaseMapping** (`DataClass`)
|
||||
- 📦 **DatabaseMappingsResponse** (`DataClass`)
|
||||
- ƒ **get_dashboards** (`Function`)
|
||||
- 📝 Fetch list of dashboards from a specific environment with Git status and last task status
|
||||
- 🔗 CALLS -> `ResourceService.get_dashboards_with_status`
|
||||
- ƒ **get_database_mappings** (`Function`)
|
||||
- 📝 Get database mapping suggestions between source and target environments
|
||||
- 🔗 CALLS -> `MappingService.get_suggestions`
|
||||
- ƒ **get_dashboard_detail** (`Function`)
|
||||
- 📝 Fetch detailed dashboard info with related charts and datasets
|
||||
- 🔗 CALLS -> `SupersetClient.get_dashboard_detail`
|
||||
@@ -2719,11 +2857,6 @@
|
||||
- 📝 Trigger bulk backup of dashboards with optional cron schedule
|
||||
- 🔗 DISPATCHES -> `BackupPlugin`
|
||||
- 🔗 CALLS -> `task_manager.create_task`
|
||||
- 📦 **DatabaseMapping** (`DataClass`)
|
||||
- 📦 **DatabaseMappingsResponse** (`DataClass`)
|
||||
- ƒ **get_database_mappings** (`Function`)
|
||||
- 📝 Get database mapping suggestions between source and target environments
|
||||
- 🔗 CALLS -> `MappingService.get_suggestions`
|
||||
- 📦 **backend.src.api.routes.__tests__.test_dashboards** (`Module`)
|
||||
- 📝 Unit tests for Dashboards API endpoints
|
||||
- 🏗️ Layer: API
|
||||
@@ -2910,14 +3043,18 @@
|
||||
- 📝 Task status command must surface current status text for existing task id.
|
||||
- ƒ **test_status_query_without_task_id_returns_latest_user_task** (`Function`)
|
||||
- 📝 Status command without explicit task_id should resolve to latest task for current user.
|
||||
- ƒ **test_llm_validation_missing_dashboard_returns_needs_clarification** (`Function`)
|
||||
- 📝 LLM validation command without resolvable dashboard id must request clarification instead of generic failure.
|
||||
- ƒ **test_list_conversations_groups_by_conversation_and_marks_archived** (`Function`)
|
||||
- 📝 Conversations endpoint must group messages and compute archived marker by inactivity threshold.
|
||||
- ƒ **test_history_from_latest_returns_recent_page_first** (`Function`)
|
||||
- 📝 History endpoint from_latest mode must return newest page while preserving chronological order in chunk.
|
||||
- ƒ **test_list_conversations_archived_only_filters_active** (`Function`)
|
||||
- 📝 archived_only mode must return only archived conversations.
|
||||
- ƒ **test_llm_validation_with_dashboard_ref_requires_confirmation** (`Function`)
|
||||
- 📝 LLM validation with dashboard_ref should now require confirmation before dispatch.
|
||||
- ƒ **test_list_conversations_groups_by_conversation_and_marks_archived** (`Function`)
|
||||
- 📝 Conversations endpoint must group messages and compute archived marker by inactivity threshold.
|
||||
- ƒ **test_history_from_latest_returns_recent_page_first** (`Function`)
|
||||
- 📝 History endpoint from_latest mode must return newest page while preserving chronological order in chunk.
|
||||
- ƒ **test_list_conversations_archived_only_filters_active** (`Function`)
|
||||
- 📝 archived_only mode must return only archived conversations.
|
||||
- ƒ **test_guarded_operation_always_requires_confirmation** (`Function`)
|
||||
- 📝 Non-dangerous (guarded) commands must still require confirmation before execution.
|
||||
- ƒ **test_guarded_operation_confirm_roundtrip** (`Function`)
|
||||
- 📝 Guarded operation must execute successfully after explicit confirmation.
|
||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||
@@ -2958,6 +3095,43 @@
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **rollback** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.src.api.routes.__tests__.test_migration_routes** (`Module`)
|
||||
- 📝 Unit tests for migration API route handlers.
|
||||
- 🏗️ Layer: API
|
||||
- ƒ **db_session** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_make_config_manager** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_migration_settings_returns_default_cron** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_migration_settings_returns_fallback_when_no_cron** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_update_migration_settings_saves_cron** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_update_migration_settings_rejects_missing_cron** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_resource_mappings_returns_formatted_list** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_resource_mappings_respects_pagination** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_resource_mappings_search_by_name** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_resource_mappings_filter_by_env** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_resource_mappings_filter_by_type** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_mock_env** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_make_sync_config_manager** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_trigger_sync_now_creates_env_row_and_syncs** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_trigger_sync_now_rejects_empty_environments** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_trigger_sync_now_handles_partial_failure** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_trigger_sync_now_idempotent_env_upsert** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.src.models.config** (`Module`)
|
||||
- 📝 Defines database schema for persisted application configuration.
|
||||
- 🏗️ Layer: Domain
|
||||
@@ -3005,6 +3179,8 @@
|
||||
- 🏗️ Layer: Domain
|
||||
- 🔒 Invariant: All primary keys are UUID strings.
|
||||
- 🔗 DEPENDS_ON -> `sqlalchemy`
|
||||
- ℂ **ResourceType** (`Class`) `[TRIVIAL]`
|
||||
- 📝 Enumeration of possible Superset resource types for ID mapping.
|
||||
- ℂ **MigrationStatus** (`Class`) `[TRIVIAL]`
|
||||
- 📝 Enumeration of possible migration job statuses.
|
||||
- ℂ **Environment** (`Class`)
|
||||
@@ -3013,6 +3189,8 @@
|
||||
- 📝 Represents a mapping between source and target databases.
|
||||
- ℂ **MigrationJob** (`Class`) `[TRIVIAL]`
|
||||
- 📝 Represents a single migration execution job.
|
||||
- ℂ **ResourceMapping** (`Class`)
|
||||
- 📝 Maps a universal UUID for a resource to its actual ID on a specific environment.
|
||||
- 📦 **backend.src.models.report** (`Module`) `[CRITICAL]`
|
||||
- 📝 Canonical report schemas for unified task reporting across heterogeneous task types.
|
||||
- 🏗️ Layer: Domain
|
||||
@@ -3098,6 +3276,61 @@
|
||||
- 🏗️ Layer: Domain
|
||||
- ƒ **test_environment_model** (`Function`)
|
||||
- 📝 Tests that Environment model correctly stores values.
|
||||
- 📦 **test_report_models** (`Module`) `[CRITICAL]`
|
||||
- 📝 Unit tests for report Pydantic models and their validators
|
||||
- 🏗️ Layer: Domain
|
||||
- ƒ **test_enum_values** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_enum_values** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_valid_creation** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_minimal_creation** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_make_report** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_valid_creation** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_empty_report_id_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_whitespace_report_id_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_empty_task_id_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_empty_summary_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_summary_whitespace_trimmed** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_optional_fields** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_with_error_context** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_defaults** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_invalid_sort_by_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_valid_sort_by_values** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_invalid_sort_order_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_valid_sort_order_values** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_time_range_validation_valid** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_time_range_validation_invalid** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_page_ge_1** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_page_size_bounds** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_valid_creation** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_with_items** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_valid_creation** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_with_all_fields** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.src.services.resource_service** (`Module`)
|
||||
- 📝 Shared service for fetching resource data with Git status and task status
|
||||
- 🏗️ Layer: Service
|
||||
@@ -3253,6 +3486,38 @@
|
||||
- 📝 Helper to get an initialized SupersetClient for an environment.
|
||||
- ƒ **get_suggestions** (`Function`)
|
||||
- 📝 Fetches databases from both environments and returns fuzzy matching suggestions.
|
||||
- 📦 **test_encryption_manager** (`Module`) `[CRITICAL]`
|
||||
- 📝 Unit tests for EncryptionManager encrypt/decrypt functionality.
|
||||
- 🏗️ Layer: Domain
|
||||
- 🔒 Invariant: Encrypt+decrypt roundtrip always returns original plaintext.
|
||||
- ℂ **TestEncryptionManager** (`Class`)
|
||||
- 📝 Validate EncryptionManager encrypt/decrypt roundtrip, uniqueness, and error handling.
|
||||
- ƒ **test_encrypt_decrypt_roundtrip** (`Function`)
|
||||
- 📝 Encrypt then decrypt returns original plaintext.
|
||||
- ƒ **test_encrypt_produces_different_output** (`Function`)
|
||||
- 📝 Same plaintext produces different ciphertext (Fernet uses random IV).
|
||||
- ƒ **test_different_inputs_yield_different_ciphertext** (`Function`)
|
||||
- 📝 Different inputs produce different ciphertexts.
|
||||
- ƒ **test_decrypt_invalid_data_raises** (`Function`)
|
||||
- 📝 Decrypting invalid data raises InvalidToken.
|
||||
- ƒ **test_encrypt_empty_string** (`Function`)
|
||||
- 📝 Encrypting and decrypting an empty string works.
|
||||
- ƒ **test_custom_key_roundtrip** (`Function`)
|
||||
- 📝 Custom Fernet key produces valid roundtrip.
|
||||
- ƒ **_make_manager** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **encrypt** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **decrypt** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **encrypt** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **decrypt** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.src.services.__tests__.test_llm_prompt_templates** (`Module`)
|
||||
- 📝 Validate normalization and rendering behavior for configurable LLM prompt templates.
|
||||
- 🏗️ Layer: Domain Tests
|
||||
@@ -3341,6 +3606,43 @@
|
||||
- 📝 Return filtered, sorted, paginated report collection.
|
||||
- ƒ **get_report_detail** (`Function`)
|
||||
- 📝 Return one normalized report with timeline/diagnostics/next actions.
|
||||
- 📦 **test_report_service** (`Module`) `[CRITICAL]`
|
||||
- 📝 Unit tests for ReportsService list/detail operations
|
||||
- 🏗️ Layer: Domain
|
||||
- ƒ **_make_task** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_make_service** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_empty_tasks_returns_empty_collection** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_single_task_normalized** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_pagination_first_page** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_pagination_last_page** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_filter_by_status** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_filter_by_task_type** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_search_filter** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_sort_by_status** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_applied_filters_echoed** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_make_service** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_detail_found** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_detail_not_found** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_detail_includes_timeline** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_detail_failed_task_has_next_actions** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_detail_success_task_no_error_next_actions** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.tests.test_report_normalizer** (`Module`) `[CRITICAL]`
|
||||
- 📝 Validate unknown task type fallback and partial payload normalization behavior.
|
||||
- 🏗️ Layer: Domain (Tests)
|
||||
@@ -3633,8 +3935,139 @@
|
||||
- 🏗️ Layer: Unknown
|
||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **test_task_persistence** (`Module`) `[CRITICAL]`
|
||||
- 📝 Unit tests for TaskPersistenceService.
|
||||
- 🏗️ Layer: Test
|
||||
- ℂ **TestTaskPersistenceHelpers** (`Class`) `[CRITICAL]`
|
||||
- 📝 Test suite for TaskPersistenceService static helper methods.
|
||||
- ƒ **test_json_load_if_needed_none** (`Function`)
|
||||
- 📝 Test _json_load_if_needed with None input.
|
||||
- ƒ **test_json_load_if_needed_dict** (`Function`)
|
||||
- 📝 Test _json_load_if_needed with dict input.
|
||||
- ƒ **test_json_load_if_needed_list** (`Function`)
|
||||
- 📝 Test _json_load_if_needed with list input.
|
||||
- ƒ **test_json_load_if_needed_json_string** (`Function`)
|
||||
- 📝 Test _json_load_if_needed with JSON string.
|
||||
- ƒ **test_json_load_if_needed_empty_string** (`Function`)
|
||||
- 📝 Test _json_load_if_needed with empty/null strings.
|
||||
- ƒ **test_json_load_if_needed_plain_string** (`Function`)
|
||||
- 📝 Test _json_load_if_needed with non-JSON string.
|
||||
- ƒ **test_json_load_if_needed_integer** (`Function`)
|
||||
- 📝 Test _json_load_if_needed with integer.
|
||||
- ƒ **test_parse_datetime_none** (`Function`)
|
||||
- 📝 Test _parse_datetime with None.
|
||||
- ƒ **test_parse_datetime_datetime_object** (`Function`)
|
||||
- 📝 Test _parse_datetime with datetime object.
|
||||
- ƒ **test_parse_datetime_iso_string** (`Function`)
|
||||
- 📝 Test _parse_datetime with ISO string.
|
||||
- ƒ **test_parse_datetime_invalid_string** (`Function`)
|
||||
- 📝 Test _parse_datetime with invalid string.
|
||||
- ƒ **test_parse_datetime_integer** (`Function`)
|
||||
- 📝 Test _parse_datetime with non-string, non-datetime.
|
||||
- ℂ **TestTaskPersistenceService** (`Class`) `[CRITICAL]`
|
||||
- 📝 Test suite for TaskPersistenceService CRUD operations.
|
||||
- ƒ **setup_class** (`Function`)
|
||||
- 📝 Setup in-memory test database.
|
||||
- ƒ **teardown_class** (`Function`)
|
||||
- 📝 Dispose of test database.
|
||||
- ƒ **setup_method** (`Function`)
|
||||
- 📝 Clean task_records table before each test.
|
||||
- ƒ **test_persist_task_new** (`Function`)
|
||||
- 📝 Test persisting a new task creates a record.
|
||||
- ƒ **test_persist_task_update** (`Function`)
|
||||
- 📝 Test updating an existing task.
|
||||
- ƒ **test_persist_task_with_logs** (`Function`)
|
||||
- 📝 Test persisting a task with log entries.
|
||||
- ƒ **test_persist_task_failed_extracts_error** (`Function`)
|
||||
- 📝 Test that FAILED task extracts last error message.
|
||||
- ƒ **test_persist_tasks_batch** (`Function`)
|
||||
- 📝 Test persisting multiple tasks.
|
||||
- ƒ **test_load_tasks** (`Function`)
|
||||
- 📝 Test loading tasks from database.
|
||||
- ƒ **test_load_tasks_with_status_filter** (`Function`)
|
||||
- 📝 Test loading tasks filtered by status.
|
||||
- ƒ **test_load_tasks_with_limit** (`Function`)
|
||||
- 📝 Test loading tasks with limit.
|
||||
- ƒ **test_delete_tasks** (`Function`)
|
||||
- 📝 Test deleting tasks by ID list.
|
||||
- ƒ **test_delete_tasks_empty_list** (`Function`)
|
||||
- 📝 Test deleting with empty list (no-op).
|
||||
- ƒ **test_persist_task_with_datetime_in_params** (`Function`)
|
||||
- 📝 Test json_serializable handles datetime in params.
|
||||
- ƒ **_patched** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_make_task** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_environment_model** (`Function`)
|
||||
- 📝 Tests that Environment model correctly stores values.
|
||||
- 📦 **test_task_manager** (`Module`) `[CRITICAL]`
|
||||
- 📝 Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering.
|
||||
- 🏗️ Layer: Core
|
||||
- 🔒 Invariant: TaskManager state changes are deterministic and testable with mocked dependencies.
|
||||
- ƒ **_make_manager** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_cleanup_manager** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_init_creates_empty_tasks** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_init_loads_persisted_tasks** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_init_starts_flusher_thread** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_task_returns_none_for_missing** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_task_returns_existing** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_all_tasks** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_tasks_with_status_filter** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_tasks_with_plugin_filter** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_tasks_with_pagination** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_tasks_completed_only** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_create_task_success** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_create_task_unknown_plugin_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_create_task_invalid_params_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_add_log_appends_to_task_and_buffer** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_add_log_skips_nonexistent_task** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_flush_logs_writes_to_persistence** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_flush_task_logs_writes_single_task** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_flush_logs_requeues_on_failure** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_clear_all_non_active** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_clear_by_status** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_clear_preserves_awaiting_input** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_subscribe_creates_queue** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_unsubscribe_removes_queue** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_multiple_subscribers** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_await_input_sets_status** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_await_input_not_running_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_await_input_nonexistent_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_resume_with_password** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_resume_not_awaiting_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_resume_empty_passwords_raises** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.tests.test_dashboards_api** (`Module`)
|
||||
- 📝 Contract-driven tests for Dashboard Hub API
|
||||
- 🏗️ Layer: Domain (Tests)
|
||||
@@ -3714,6 +4147,13 @@
|
||||
- 📝 Test sub-context logger uses new source.
|
||||
- ƒ **test_multiple_sub_contexts** (`Function`)
|
||||
- 📝 Test creating multiple sub-contexts.
|
||||
- 📦 **test_smoke_plugins** (`Module`) `[TRIVIAL]`
|
||||
- 📝 Auto-generated module for backend/tests/test_smoke_plugins.py
|
||||
- 🏗️ Layer: Unknown
|
||||
- ƒ **test_plugins_load_successfully** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_task_manager_initializes_with_plugins** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_belief_scope_logs_entry_action_exit_at_debug** (`Function`)
|
||||
- 📝 Test that belief_scope generates [ID][Entry], [ID][Action], and [ID][Exit] logs at DEBUG level.
|
||||
- ƒ **test_belief_scope_error_handling** (`Function`)
|
||||
@@ -3751,23 +4191,23 @@
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_ad_group_mapping** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **test_log_persistence** (`Module`)
|
||||
- 📦 **test_log_persistence** (`Module`) `[CRITICAL]`
|
||||
- 📝 Unit tests for TaskLogPersistenceService.
|
||||
- 🏗️ Layer: Test
|
||||
- ℂ **TestLogPersistence** (`Class`)
|
||||
- ℂ **TestLogPersistence** (`Class`) `[CRITICAL]`
|
||||
- 📝 Test suite for TaskLogPersistenceService.
|
||||
- ƒ **setup_class** (`Function`)
|
||||
- 📝 Setup test database and service instance.
|
||||
- ƒ **teardown_class** (`Function`)
|
||||
- 📝 Clean up test database.
|
||||
- ƒ **setup_method** (`Function`)
|
||||
- 📝 Setup for each test method.
|
||||
- ƒ **teardown_method** (`Function`)
|
||||
- 📝 Cleanup after each test method.
|
||||
- ƒ **test_add_log_single** (`Function`)
|
||||
- 📝 Setup for each test method — clean task_logs table.
|
||||
- ƒ **test_add_logs_single** (`Function`)
|
||||
- 📝 Test adding a single log entry.
|
||||
- ƒ **test_add_log_batch** (`Function`)
|
||||
- ƒ **test_add_logs_batch** (`Function`)
|
||||
- 📝 Test adding multiple log entries in batch.
|
||||
- ƒ **test_add_logs_empty** (`Function`)
|
||||
- 📝 Test adding empty log list (should be no-op).
|
||||
- ƒ **test_get_logs_by_task_id** (`Function`)
|
||||
- 📝 Test retrieving logs by task ID.
|
||||
- ƒ **test_get_logs_with_filters** (`Function`)
|
||||
@@ -3778,7 +4218,82 @@
|
||||
- 📝 Test retrieving logs with search query.
|
||||
- ƒ **test_get_log_stats** (`Function`)
|
||||
- 📝 Test retrieving log statistics.
|
||||
- ƒ **test_get_log_sources** (`Function`)
|
||||
- ƒ **test_get_sources** (`Function`)
|
||||
- 📝 Test retrieving unique log sources.
|
||||
- ƒ **test_delete_logs_by_task_id** (`Function`)
|
||||
- ƒ **test_delete_logs_for_task** (`Function`)
|
||||
- 📝 Test deleting logs by task ID.
|
||||
- ƒ **test_delete_logs_for_tasks** (`Function`)
|
||||
- 📝 Test deleting logs for multiple tasks.
|
||||
- ƒ **test_delete_logs_for_tasks_empty** (`Function`)
|
||||
- 📝 Test deleting with empty list (no-op).
|
||||
- ƒ **_patched** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.tests.core.test_mapping_service** (`Module`)
|
||||
- 📝 Unit tests for the IdMappingService matching UUIDs to integer IDs.
|
||||
- 🏗️ Layer: Domain
|
||||
- ƒ **db_session** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **get_all_resources** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_sync_environment_upserts_correctly** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_remote_id_returns_integer** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_remote_ids_batch_returns_dict** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_sync_environment_updates_existing_mapping** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_sync_environment_skips_resources_without_uuid** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_sync_environment_handles_api_error_gracefully** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **get_all_resources** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_remote_id_returns_none_for_missing** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_get_remote_ids_batch_returns_empty_for_empty_input** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_mapping_service_alignment_with_test_data** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_sync_environment_requires_existing_env** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_sync_environment_deletes_stale_mappings** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **test_defensive_guards** (`Module`) `[TRIVIAL]`
|
||||
- 📝 Auto-generated module for backend/tests/core/test_defensive_guards.py
|
||||
- 🏗️ Layer: Unknown
|
||||
- ƒ **test_git_service_get_repo_path_guard** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_superset_client_import_dashboard_guard** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **backend.tests.core.test_migration_engine** (`Module`)
|
||||
- 📝 Unit tests for MigrationEngine's cross-filter patching algorithms.
|
||||
- 🏗️ Layer: Domain
|
||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **get_remote_ids_batch** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_write_dashboard_yaml** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_patch_dashboard_metadata_replaces_chart_ids** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_patch_dashboard_metadata_replaces_dataset_ids** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_patch_dashboard_metadata_skips_when_no_metadata** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_patch_dashboard_metadata_handles_missing_targets** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_extract_chart_uuids_from_archive** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_transform_yaml_replaces_database_uuid** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_transform_yaml_ignores_unmapped_uuid** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_transform_zip_end_to_end** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_transform_zip_invalid_path** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **test_transform_yaml_nonexistent_file** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
51
.codex/prompts/audit-test.md
Normal file
51
.codex/prompts/audit-test.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
description: Audit AI-generated unit tests. Your goal is to aggressively search for "Test Tautologies", "Logic Echoing", and "Contract Negligence". You are the final gatekeeper. If a test is meaningless, you MUST reject it.
|
||||
---
|
||||
|
||||
**ROLE:** Elite Quality Assurance Architect and Red Teamer.
|
||||
**OBJECTIVE:** Audit AI-generated unit tests. Your goal is to aggressively search for "Test Tautologies", "Logic Echoing", and "Contract Negligence". You are the final gatekeeper. If a test is meaningless, you MUST reject it.
|
||||
|
||||
**INPUT:**
|
||||
1. SOURCE CODE (with GRACE-Poly `[DEF]` Contract: `@PRE`, `@POST`, `@TEST_DATA`).
|
||||
2. GENERATED TEST CODE.
|
||||
|
||||
### I. CRITICAL ANTI-PATTERNS (REJECT IMMEDIATELY IF FOUND):
|
||||
|
||||
1. **The Tautology (Self-Fulfilling Prophecy):**
|
||||
- *Definition:* The test asserts hardcoded values against hardcoded values without executing the core business logic, or mocks the actual function being tested.
|
||||
- *Example of Failure:* `assert 2 + 2 == 4` or mocking the class under test so that it returns exactly what the test asserts.
|
||||
|
||||
2. **The Logic Mirror (Echoing):**
|
||||
- *Definition:* The test re-implements the exact same algorithmic logic found in the source code to calculate the `expected_result`. If the original logic is flawed, the test will falsely pass.
|
||||
- *Rule:* Tests must assert against **static, predefined outcomes** (from `@TEST_DATA` or explicit constants), NOT dynamically calculated outcomes using the same logic as the source.
|
||||
|
||||
3. **The "Happy Path" Illusion:**
|
||||
- *Definition:* The test suite only checks successful executions but ignores the `@PRE` conditions (Negative Testing).
|
||||
- *Rule:* Every `@PRE` tag in the source contract MUST have a corresponding test that deliberately violates it and asserts the correct Exception/Error state.
|
||||
|
||||
4. **Missing Post-Condition Verification:**
|
||||
- *Definition:* The test calls the function but only checks the return value, ignoring `@SIDE_EFFECT` or `@POST` state changes (e.g., failing to verify that a DB call was made or a Store was updated).
|
||||
|
||||
### II. AUDIT CHECKLIST
|
||||
|
||||
Evaluate the test code against these criteria:
|
||||
1. **Target Invocation:** Does the test actually import and call the function/component declared in the `@RELATION: VERIFIES` tag?
|
||||
2. **Contract Alignment:** Does the test suite cover 100% of the `@PRE` (negative tests) and `@POST` (assertions) conditions from the source contract?
|
||||
3. **Data Usage:** Does the test use the exact scenarios defined in `@TEST_DATA`?
|
||||
4. **Mocking Sanity:** Are external dependencies mocked correctly WITHOUT mocking the system under test itself?
|
||||
|
||||
### III. OUTPUT FORMAT
|
||||
|
||||
You MUST respond strictly in the following JSON format. Do not add markdown blocks outside the JSON.
|
||||
|
||||
{
|
||||
"verdict": "APPROVED" | "REJECTED",
|
||||
"rejection_reason": "TAUTOLOGY" | "LOGIC_MIRROR" | "WEAK_CONTRACT_COVERAGE" | "OVER_MOCKED" | "NONE",
|
||||
"audit_details": {
|
||||
"target_invoked": true/false,
|
||||
"pre_conditions_tested": true/false,
|
||||
"post_conditions_tested": true/false,
|
||||
"test_data_used": true/false
|
||||
},
|
||||
"feedback": "Strict, actionable feedback for the test generator agent. Explain exactly which anti-pattern was detected and how to fix it."
|
||||
}
|
||||
4
.codex/prompts/read_semantic.md
Normal file
4
.codex/prompts/read_semantic.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
description: USE SEMANTIC
|
||||
---
|
||||
Прочитай .specify/memory/semantics.md (или .ai/standards/semantics.md, если не найден). ОБЯЗАТЕЛЬНО используй его при разработке
|
||||
10
.codex/prompts/semantic.md
Normal file
10
.codex/prompts/semantic.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description: semantic
|
||||
---
|
||||
|
||||
You are Semantic Agent responsible for maintaining the semantic integrity of the codebase. Your primary goal is to ensure that all code entities (Modules, Classes, Functions, Components) are properly annotated with semantic anchors and tags as defined in `.ai/standards/semantics.md`.
|
||||
Your core responsibilities are: 1. **Semantic Mapping**: You run and maintain the `generate_semantic_map.py` script to generate up-to-date semantic maps (`semantics/semantic_map.json`, `.ai/PROJECT_MAP.md`) and compliance reports (`semantics/reports/*.md`). 2. **Compliance Auditing**: You analyze the generated compliance reports to identify files with low semantic coverage or parsing errors. 3. **Semantic Enrichment**: You actively edit code files to add missing semantic anchors (`[DEF:...]`, `[/DEF:...]`) and mandatory tags (`@PURPOSE`, `@LAYER`, etc.) to improve the global compliance score. 4. **Protocol Enforcement**: You strictly adhere to the syntax and rules defined in `.ai/standards/semantics.md` when modifying code.
|
||||
You have access to the full codebase and tools to read, write, and execute scripts. You should prioritize fixing "Critical Parsing Errors" (unclosed anchors) before addressing missing metadata.
|
||||
whenToUse: Use this mode when you need to update the project's semantic map, fix semantic compliance issues (missing anchors/tags/DbC ), or analyze the codebase structure. This mode is specialized for maintaining the `.ai/standards/semantics.md` standards.
|
||||
description: Codebase semantic mapping and compliance expert
|
||||
customInstructions: Always check `semantics/reports/` for the latest compliance status before starting work. When fixing a file, try to fix all semantic issues in that file at once. After making a batch of fixes, run `python3 generate_semantic_map.py` to verify improvements.
|
||||
184
.codex/prompts/speckit.analyze.md
Normal file
184
.codex/prompts/speckit.analyze.md
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Goal
|
||||
|
||||
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
|
||||
|
||||
## Operating Constraints
|
||||
|
||||
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
|
||||
|
||||
**Constitution Authority**: The project constitution (`.ai/standards/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Initialize Analysis Context
|
||||
|
||||
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths:
|
||||
|
||||
- SPEC = FEATURE_DIR/spec.md
|
||||
- PLAN = FEATURE_DIR/plan.md
|
||||
- TASKS = FEATURE_DIR/tasks.md
|
||||
|
||||
Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command).
|
||||
For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
### 2. Load Artifacts (Progressive Disclosure)
|
||||
|
||||
Load only the minimal necessary context from each artifact:
|
||||
|
||||
**From spec.md:**
|
||||
|
||||
- Overview/Context
|
||||
- Functional Requirements
|
||||
- Non-Functional Requirements
|
||||
- User Stories
|
||||
- Edge Cases (if present)
|
||||
|
||||
**From plan.md:**
|
||||
|
||||
- Architecture/stack choices
|
||||
- Data Model references
|
||||
- Phases
|
||||
- Technical constraints
|
||||
|
||||
**From tasks.md:**
|
||||
|
||||
- Task IDs
|
||||
- Descriptions
|
||||
- Phase grouping
|
||||
- Parallel markers [P]
|
||||
- Referenced file paths
|
||||
|
||||
**From constitution:**
|
||||
|
||||
- Load `.ai/standards/constitution.md` for principle validation
|
||||
|
||||
### 3. Build Semantic Models
|
||||
|
||||
Create internal representations (do not include raw artifacts in output):
|
||||
|
||||
- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`)
|
||||
- **User story/action inventory**: Discrete user actions with acceptance criteria
|
||||
- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases)
|
||||
- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements
|
||||
|
||||
### 4. Detection Passes (Token-Efficient Analysis)
|
||||
|
||||
Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary.
|
||||
|
||||
#### A. Duplication Detection
|
||||
|
||||
- Identify near-duplicate requirements
|
||||
- Mark lower-quality phrasing for consolidation
|
||||
|
||||
#### B. Ambiguity Detection
|
||||
|
||||
- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria
|
||||
- Flag unresolved placeholders (TODO, TKTK, ???, `<placeholder>`, etc.)
|
||||
|
||||
#### C. Underspecification
|
||||
|
||||
- Requirements with verbs but missing object or measurable outcome
|
||||
- User stories missing acceptance criteria alignment
|
||||
- Tasks referencing files or components not defined in spec/plan
|
||||
|
||||
#### D. Constitution Alignment
|
||||
|
||||
- Any requirement or plan element conflicting with a MUST principle
|
||||
- Missing mandated sections or quality gates from constitution
|
||||
|
||||
#### E. Coverage Gaps
|
||||
|
||||
- Requirements with zero associated tasks
|
||||
- Tasks with no mapped requirement/story
|
||||
- Non-functional requirements not reflected in tasks (e.g., performance, security)
|
||||
|
||||
#### F. Inconsistency
|
||||
|
||||
- Terminology drift (same concept named differently across files)
|
||||
- Data entities referenced in plan but absent in spec (or vice versa)
|
||||
- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note)
|
||||
- Conflicting requirements (e.g., one requires Next.js while other specifies Vue)
|
||||
|
||||
### 5. Severity Assignment
|
||||
|
||||
Use this heuristic to prioritize findings:
|
||||
|
||||
- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality
|
||||
- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion
|
||||
- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case
|
||||
- **LOW**: Style/wording improvements, minor redundancy not affecting execution order
|
||||
|
||||
### 6. Produce Compact Analysis Report
|
||||
|
||||
Output a Markdown report (no file writes) with the following structure:
|
||||
|
||||
## Specification Analysis Report
|
||||
|
||||
| ID | Category | Severity | Location(s) | Summary | Recommendation |
|
||||
|----|----------|----------|-------------|---------|----------------|
|
||||
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
|
||||
|
||||
(Add one row per finding; generate stable IDs prefixed by category initial.)
|
||||
|
||||
**Coverage Summary Table:**
|
||||
|
||||
| Requirement Key | Has Task? | Task IDs | Notes |
|
||||
|-----------------|-----------|----------|-------|
|
||||
|
||||
**Constitution Alignment Issues:** (if any)
|
||||
|
||||
**Unmapped Tasks:** (if any)
|
||||
|
||||
**Metrics:**
|
||||
|
||||
- Total Requirements
|
||||
- Total Tasks
|
||||
- Coverage % (requirements with >=1 task)
|
||||
- Ambiguity Count
|
||||
- Duplication Count
|
||||
- Critical Issues Count
|
||||
|
||||
### 7. Provide Next Actions
|
||||
|
||||
At end of report, output a concise Next Actions block:
|
||||
|
||||
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
|
||||
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
|
||||
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
|
||||
|
||||
### 8. Offer Remediation
|
||||
|
||||
Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.)
|
||||
|
||||
## Operating Principles
|
||||
|
||||
### Context Efficiency
|
||||
|
||||
- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation
|
||||
- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis
|
||||
- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow
|
||||
- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts
|
||||
|
||||
### Analysis Guidelines
|
||||
|
||||
- **NEVER modify files** (this is read-only analysis)
|
||||
- **NEVER hallucinate missing sections** (if absent, report them accurately)
|
||||
- **Prioritize constitution violations** (these are always CRITICAL)
|
||||
- **Use examples over exhaustive rules** (cite specific instances, not generic patterns)
|
||||
- **Report zero issues gracefully** (emit success report with coverage statistics)
|
||||
|
||||
## Context
|
||||
|
||||
$ARGUMENTS
|
||||
294
.codex/prompts/speckit.checklist.md
Normal file
294
.codex/prompts/speckit.checklist.md
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
description: Generate a custom checklist for the current feature based on user requirements.
|
||||
---
|
||||
|
||||
## Checklist Purpose: "Unit Tests for English"
|
||||
|
||||
**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain.
|
||||
|
||||
**NOT for verification/testing**:
|
||||
|
||||
- ❌ NOT "Verify the button clicks correctly"
|
||||
- ❌ NOT "Test error handling works"
|
||||
- ❌ NOT "Confirm the API returns 200"
|
||||
- ❌ NOT checking if code/implementation matches the spec
|
||||
|
||||
**FOR requirements quality validation**:
|
||||
|
||||
- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness)
|
||||
- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity)
|
||||
- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency)
|
||||
- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage)
|
||||
- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases)
|
||||
|
||||
**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works.
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Execution Steps
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list.
|
||||
- All file paths must be absolute.
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
|
||||
- Be generated from the user's phrasing + extracted signals from spec/plan/tasks
|
||||
- Only ask about information that materially changes checklist content
|
||||
- Be skipped individually if already unambiguous in `$ARGUMENTS`
|
||||
- Prefer precision over breadth
|
||||
|
||||
Generation algorithm:
|
||||
1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts").
|
||||
2. Cluster signals into candidate focus areas (max 4) ranked by relevance.
|
||||
3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit.
|
||||
4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria.
|
||||
5. Formulate questions chosen from these archetypes:
|
||||
- Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?")
|
||||
- Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?")
|
||||
- Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?")
|
||||
- Audience framing (e.g., "Will this be used by the author only or peers during PR review?")
|
||||
- Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?")
|
||||
- Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?")
|
||||
|
||||
Question formatting rules:
|
||||
- If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters
|
||||
- Limit to A–E options maximum; omit table if a free-form answer is clearer
|
||||
- Never ask the user to restate what they already said
|
||||
- Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope."
|
||||
|
||||
Defaults when interaction impossible:
|
||||
- Depth: Standard
|
||||
- Audience: Reviewer (PR) if code-related; Author otherwise
|
||||
- Focus: Top 2 relevance clusters
|
||||
|
||||
Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more.
|
||||
|
||||
3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
|
||||
- Derive checklist theme (e.g., security, review, deploy, ux)
|
||||
- Consolidate explicit must-have items mentioned by user
|
||||
- Map focus selections to category scaffolding
|
||||
- Infer any missing context from spec/plan/tasks (do NOT hallucinate)
|
||||
|
||||
4. **Load feature context**: Read from FEATURE_DIR:
|
||||
- spec.md: Feature requirements and scope
|
||||
- plan.md (if exists): Technical details, dependencies
|
||||
- tasks.md (if exists): Implementation tasks
|
||||
|
||||
**Context Loading Strategy**:
|
||||
- Load only necessary portions relevant to active focus areas (avoid full-file dumping)
|
||||
- Prefer summarizing long sections into concise scenario/requirement bullets
|
||||
- Use progressive disclosure: add follow-on retrieval only if gaps detected
|
||||
- If source docs are large, generate interim summary items instead of embedding raw text
|
||||
|
||||
5. **Generate checklist** - Create "Unit Tests for Requirements":
|
||||
- Create `FEATURE_DIR/checklists/` directory if it doesn't exist
|
||||
- Generate unique checklist filename:
|
||||
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
|
||||
- Format: `[domain].md`
|
||||
- If file exists, append to existing file
|
||||
- Number items sequentially starting from CHK001
|
||||
- Each `/speckit.checklist` run creates a NEW file (never overwrites existing checklists)
|
||||
|
||||
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
|
||||
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
|
||||
- **Completeness**: Are all necessary requirements present?
|
||||
- **Clarity**: Are requirements unambiguous and specific?
|
||||
- **Consistency**: Do requirements align with each other?
|
||||
- **Measurability**: Can requirements be objectively verified?
|
||||
- **Coverage**: Are all scenarios/edge cases addressed?
|
||||
|
||||
**Category Structure** - Group items by requirement quality dimensions:
|
||||
- **Requirement Completeness** (Are all necessary requirements documented?)
|
||||
- **Requirement Clarity** (Are requirements specific and unambiguous?)
|
||||
- **Requirement Consistency** (Do requirements align without conflicts?)
|
||||
- **Acceptance Criteria Quality** (Are success criteria measurable?)
|
||||
- **Scenario Coverage** (Are all flows/cases addressed?)
|
||||
- **Edge Case Coverage** (Are boundary conditions defined?)
|
||||
- **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?)
|
||||
- **Dependencies & Assumptions** (Are they documented and validated?)
|
||||
- **Ambiguities & Conflicts** (What needs clarification?)
|
||||
|
||||
**HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**:
|
||||
|
||||
❌ **WRONG** (Testing implementation):
|
||||
- "Verify landing page displays 3 episode cards"
|
||||
- "Test hover states work on desktop"
|
||||
- "Confirm logo click navigates home"
|
||||
|
||||
✅ **CORRECT** (Testing requirements quality):
|
||||
- "Are the exact number and layout of featured episodes specified?" [Completeness]
|
||||
- "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity]
|
||||
- "Are hover state requirements consistent across all interactive elements?" [Consistency]
|
||||
- "Are keyboard navigation requirements defined for all interactive UI?" [Coverage]
|
||||
- "Is the fallback behavior specified when logo image fails to load?" [Edge Cases]
|
||||
- "Are loading states defined for asynchronous episode data?" [Completeness]
|
||||
- "Does the spec define visual hierarchy for competing UI elements?" [Clarity]
|
||||
|
||||
**ITEM STRUCTURE**:
|
||||
Each item should follow this pattern:
|
||||
- Question format asking about requirement quality
|
||||
- Focus on what's WRITTEN (or not written) in the spec/plan
|
||||
- Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.]
|
||||
- Reference spec section `[Spec §X.Y]` when checking existing requirements
|
||||
- Use `[Gap]` marker when checking for missing requirements
|
||||
|
||||
**EXAMPLES BY QUALITY DIMENSION**:
|
||||
|
||||
Completeness:
|
||||
- "Are error handling requirements defined for all API failure modes? [Gap]"
|
||||
- "Are accessibility requirements specified for all interactive elements? [Completeness]"
|
||||
- "Are mobile breakpoint requirements defined for responsive layouts? [Gap]"
|
||||
|
||||
Clarity:
|
||||
- "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]"
|
||||
- "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]"
|
||||
- "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]"
|
||||
|
||||
Consistency:
|
||||
- "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]"
|
||||
- "Are card component requirements consistent between landing and detail pages? [Consistency]"
|
||||
|
||||
Coverage:
|
||||
- "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]"
|
||||
- "Are concurrent user interaction scenarios addressed? [Coverage, Gap]"
|
||||
- "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]"
|
||||
|
||||
Measurability:
|
||||
- "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]"
|
||||
- "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]"
|
||||
|
||||
**Scenario Classification & Coverage** (Requirements Quality Focus):
|
||||
- Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios
|
||||
- For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?"
|
||||
- If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]"
|
||||
- Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]"
|
||||
|
||||
**Traceability Requirements**:
|
||||
- MINIMUM: ≥80% of items MUST include at least one traceability reference
|
||||
- Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]`
|
||||
- If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]"
|
||||
|
||||
**Surface & Resolve Issues** (Requirements Quality Problems):
|
||||
Ask questions about the requirements themselves:
|
||||
- Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]"
|
||||
- Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]"
|
||||
- Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]"
|
||||
- Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]"
|
||||
- Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]"
|
||||
|
||||
**Content Consolidation**:
|
||||
- Soft cap: If raw candidate items > 40, prioritize by risk/impact
|
||||
- Merge near-duplicates checking the same requirement aspect
|
||||
- If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]"
|
||||
|
||||
**🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test:
|
||||
- ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior
|
||||
- ❌ References to code execution, user actions, system behavior
|
||||
- ❌ "Displays correctly", "works properly", "functions as expected"
|
||||
- ❌ "Click", "navigate", "render", "load", "execute"
|
||||
- ❌ Test cases, test plans, QA procedures
|
||||
- ❌ Implementation details (frameworks, APIs, algorithms)
|
||||
|
||||
**✅ REQUIRED PATTERNS** - These test requirements quality:
|
||||
- ✅ "Are [requirement type] defined/specified/documented for [scenario]?"
|
||||
- ✅ "Is [vague term] quantified/clarified with specific criteria?"
|
||||
- ✅ "Are requirements consistent between [section A] and [section B]?"
|
||||
- ✅ "Can [requirement] be objectively measured/verified?"
|
||||
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
|
||||
- ✅ "Does the spec define [missing aspect]?"
|
||||
|
||||
6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
|
||||
|
||||
7. **Report**: Output full path to created checklist, item count, and remind user that each run creates a new file. Summarize:
|
||||
- Focus areas selected
|
||||
- Depth level
|
||||
- Actor/timing
|
||||
- Any explicit user-specified must-have items incorporated
|
||||
|
||||
**Important**: Each `/speckit.checklist` command invocation creates a checklist file using short, descriptive names unless file already exists. This allows:
|
||||
|
||||
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
|
||||
- Simple, memorable filenames that indicate checklist purpose
|
||||
- Easy identification and navigation in the `checklists/` folder
|
||||
|
||||
To avoid clutter, use descriptive types and clean up obsolete checklists when done.
|
||||
|
||||
## Example Checklist Types & Sample Items
|
||||
|
||||
**UX Requirements Quality:** `ux.md`
|
||||
|
||||
Sample items (testing the requirements, NOT the implementation):
|
||||
|
||||
- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]"
|
||||
- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]"
|
||||
- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]"
|
||||
- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]"
|
||||
- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]"
|
||||
- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]"
|
||||
|
||||
**API Requirements Quality:** `api.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are error response formats specified for all failure scenarios? [Completeness]"
|
||||
- "Are rate limiting requirements quantified with specific thresholds? [Clarity]"
|
||||
- "Are authentication requirements consistent across all endpoints? [Consistency]"
|
||||
- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]"
|
||||
- "Is versioning strategy documented in requirements? [Gap]"
|
||||
|
||||
**Performance Requirements Quality:** `performance.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are performance requirements quantified with specific metrics? [Clarity]"
|
||||
- "Are performance targets defined for all critical user journeys? [Coverage]"
|
||||
- "Are performance requirements under different load conditions specified? [Completeness]"
|
||||
- "Can performance requirements be objectively measured? [Measurability]"
|
||||
- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]"
|
||||
|
||||
**Security Requirements Quality:** `security.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are authentication requirements specified for all protected resources? [Coverage]"
|
||||
- "Are data protection requirements defined for sensitive information? [Completeness]"
|
||||
- "Is the threat model documented and requirements aligned to it? [Traceability]"
|
||||
- "Are security requirements consistent with compliance obligations? [Consistency]"
|
||||
- "Are security failure/breach response requirements defined? [Gap, Exception Flow]"
|
||||
|
||||
## Anti-Examples: What NOT To Do
|
||||
|
||||
**❌ WRONG - These test implementation, not requirements:**
|
||||
|
||||
```markdown
|
||||
- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001]
|
||||
- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003]
|
||||
- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010]
|
||||
- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005]
|
||||
```
|
||||
|
||||
**✅ CORRECT - These test requirements quality:**
|
||||
|
||||
```markdown
|
||||
- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001]
|
||||
- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003]
|
||||
- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010]
|
||||
- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005]
|
||||
- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap]
|
||||
- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001]
|
||||
```
|
||||
|
||||
**Key Differences:**
|
||||
|
||||
- Wrong: Tests if the system works correctly
|
||||
- Correct: Tests if the requirements are written correctly
|
||||
- Wrong: Verification of behavior
|
||||
- Correct: Validation of requirement quality
|
||||
- Wrong: "Does it do X?"
|
||||
- Correct: "Is X clearly specified?"
|
||||
181
.codex/prompts/speckit.clarify.md
Normal file
181
.codex/prompts/speckit.clarify.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
|
||||
handoffs:
|
||||
- label: Build Technical Plan
|
||||
agent: speckit.plan
|
||||
prompt: Create a plan for the spec. I am building with...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
|
||||
|
||||
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
|
||||
|
||||
Execution steps:
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields:
|
||||
- `FEATURE_DIR`
|
||||
- `FEATURE_SPEC`
|
||||
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
|
||||
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
|
||||
|
||||
Functional Scope & Behavior:
|
||||
- Core user goals & success criteria
|
||||
- Explicit out-of-scope declarations
|
||||
- User roles / personas differentiation
|
||||
|
||||
Domain & Data Model:
|
||||
- Entities, attributes, relationships
|
||||
- Identity & uniqueness rules
|
||||
- Lifecycle/state transitions
|
||||
- Data volume / scale assumptions
|
||||
|
||||
Interaction & UX Flow:
|
||||
- Critical user journeys / sequences
|
||||
- Error/empty/loading states
|
||||
- Accessibility or localization notes
|
||||
|
||||
Non-Functional Quality Attributes:
|
||||
- Performance (latency, throughput targets)
|
||||
- Scalability (horizontal/vertical, limits)
|
||||
- Reliability & availability (uptime, recovery expectations)
|
||||
- Observability (logging, metrics, tracing signals)
|
||||
- Security & privacy (authN/Z, data protection, threat assumptions)
|
||||
- Compliance / regulatory constraints (if any)
|
||||
|
||||
Integration & External Dependencies:
|
||||
- External services/APIs and failure modes
|
||||
- Data import/export formats
|
||||
- Protocol/versioning assumptions
|
||||
|
||||
Edge Cases & Failure Handling:
|
||||
- Negative scenarios
|
||||
- Rate limiting / throttling
|
||||
- Conflict resolution (e.g., concurrent edits)
|
||||
|
||||
Constraints & Tradeoffs:
|
||||
- Technical constraints (language, storage, hosting)
|
||||
- Explicit tradeoffs or rejected alternatives
|
||||
|
||||
Terminology & Consistency:
|
||||
- Canonical glossary terms
|
||||
- Avoided synonyms / deprecated terms
|
||||
|
||||
Completion Signals:
|
||||
- Acceptance criteria testability
|
||||
- Measurable Definition of Done style indicators
|
||||
|
||||
Misc / Placeholders:
|
||||
- TODO markers / unresolved decisions
|
||||
- Ambiguous adjectives ("robust", "intuitive") lacking quantification
|
||||
|
||||
For each category with Partial or Missing status, add a candidate question opportunity unless:
|
||||
- Clarification would not materially change implementation or validation strategy
|
||||
- Information is better deferred to planning phase (note internally)
|
||||
|
||||
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
|
||||
- Maximum of 10 total questions across the whole session.
|
||||
- Each question must be answerable with EITHER:
|
||||
- A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR
|
||||
- A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words").
|
||||
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
|
||||
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
|
||||
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
|
||||
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
|
||||
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic.
|
||||
|
||||
4. Sequential questioning loop (interactive):
|
||||
- Present EXACTLY ONE question at a time.
|
||||
- For multiple‑choice questions:
|
||||
- **Analyze all options** and determine the **most suitable option** based on:
|
||||
- Best practices for the project type
|
||||
- Common patterns in similar implementations
|
||||
- Risk reduction (security, performance, maintainability)
|
||||
- Alignment with any explicit project goals or constraints visible in the spec
|
||||
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
|
||||
- Format as: `**Recommended:** Option [X] - <reasoning>`
|
||||
- Then render all options as a Markdown table:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| A | <Option A description> |
|
||||
| B | <Option B description> |
|
||||
| C | <Option C description> (add D/E as needed up to 5) |
|
||||
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
|
||||
|
||||
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
|
||||
- For short‑answer style (no meaningful discrete options):
|
||||
- Provide your **suggested answer** based on best practices and context.
|
||||
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
|
||||
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
|
||||
- After the user answers:
|
||||
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
|
||||
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
|
||||
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
|
||||
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
|
||||
- Stop asking further questions when:
|
||||
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
|
||||
- User signals completion ("done", "good", "no more"), OR
|
||||
- You reach 5 asked questions.
|
||||
- Never reveal future queued questions in advance.
|
||||
- If no valid questions exist at start, immediately report no critical ambiguities.
|
||||
|
||||
5. Integration after EACH accepted answer (incremental update approach):
|
||||
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
|
||||
- For the first integrated answer in this session:
|
||||
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
|
||||
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
|
||||
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
|
||||
- Then immediately apply the clarification to the most appropriate section(s):
|
||||
- Functional ambiguity → Update or add a bullet in Functional Requirements.
|
||||
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
|
||||
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
|
||||
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
|
||||
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
|
||||
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
|
||||
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
|
||||
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
|
||||
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
|
||||
- Keep each inserted clarification minimal and testable (avoid narrative drift).
|
||||
|
||||
6. Validation (performed after EACH write plus final pass):
|
||||
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
|
||||
- Total asked (accepted) questions ≤ 5.
|
||||
- Updated sections contain no lingering vague placeholders the new answer was meant to resolve.
|
||||
- No contradictory earlier statement remains (scan for now-invalid alternative choices removed).
|
||||
- Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`.
|
||||
- Terminology consistency: same canonical term used across all updated sections.
|
||||
|
||||
7. Write the updated spec back to `FEATURE_SPEC`.
|
||||
|
||||
8. Report completion (after questioning loop ends or early termination):
|
||||
- Number of questions asked & answered.
|
||||
- Path to updated spec.
|
||||
- Sections touched (list names).
|
||||
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
|
||||
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
|
||||
- Suggested next command.
|
||||
|
||||
Behavior rules:
|
||||
|
||||
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
|
||||
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
|
||||
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
|
||||
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
|
||||
- Respect user early termination signals ("stop", "done", "proceed").
|
||||
- If no questions asked due to full coverage, output a compact coverage summary (all categories Clear) then suggest advancing.
|
||||
- If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale.
|
||||
|
||||
Context for prioritization: $ARGUMENTS
|
||||
84
.codex/prompts/speckit.constitution.md
Normal file
84
.codex/prompts/speckit.constitution.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
|
||||
handoffs:
|
||||
- label: Build Specification
|
||||
agent: speckit.specify
|
||||
prompt: Implement the feature specification based on the updated constitution. I want to build...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
You are updating the project constitution at `.ai/standards/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
|
||||
|
||||
**Note**: If `.ai/standards/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
|
||||
|
||||
Follow this execution flow:
|
||||
|
||||
1. Load the existing constitution at `.ai/standards/constitution.md`.
|
||||
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
|
||||
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
|
||||
|
||||
2. Collect/derive values for placeholders:
|
||||
- If user input (conversation) supplies a value, use it.
|
||||
- Otherwise infer from existing repo context (README, docs, prior constitution versions if embedded).
|
||||
- For governance dates: `RATIFICATION_DATE` is the original adoption date (if unknown ask or mark TODO), `LAST_AMENDED_DATE` is today if changes are made, otherwise keep previous.
|
||||
- `CONSTITUTION_VERSION` must increment according to semantic versioning rules:
|
||||
- MAJOR: Backward incompatible governance/principle removals or redefinitions.
|
||||
- MINOR: New principle/section added or materially expanded guidance.
|
||||
- PATCH: Clarifications, wording, typo fixes, non-semantic refinements.
|
||||
- If version bump type ambiguous, propose reasoning before finalizing.
|
||||
|
||||
3. Draft the updated constitution content:
|
||||
- Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left).
|
||||
- Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance.
|
||||
- Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing non‑negotiable rules, explicit rationale if not obvious.
|
||||
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
|
||||
|
||||
4. Consistency propagation checklist (convert prior checklist into active validations):
|
||||
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
|
||||
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
|
||||
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
|
||||
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
|
||||
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
|
||||
|
||||
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
|
||||
- Version change: old → new
|
||||
- List of modified principles (old title → new title if renamed)
|
||||
- Added sections
|
||||
- Removed sections
|
||||
- Templates requiring updates (✅ updated / ⚠ pending) with file paths
|
||||
- Follow-up TODOs if any placeholders intentionally deferred.
|
||||
|
||||
6. Validation before final output:
|
||||
- No remaining unexplained bracket tokens.
|
||||
- Version line matches report.
|
||||
- Dates ISO format YYYY-MM-DD.
|
||||
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
|
||||
|
||||
7. Write the completed constitution back to `.ai/standards/constitution.md` (overwrite).
|
||||
|
||||
8. Output a final summary to the user with:
|
||||
- New version and bump rationale.
|
||||
- Any files flagged for manual follow-up.
|
||||
- Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`).
|
||||
|
||||
Formatting & Style Requirements:
|
||||
|
||||
- Use Markdown headings exactly as in the template (do not demote/promote levels).
|
||||
- Wrap long rationale lines to keep readability (<100 chars ideally) but do not hard enforce with awkward breaks.
|
||||
- Keep a single blank line between sections.
|
||||
- Avoid trailing whitespace.
|
||||
|
||||
If the user supplies partial updates (e.g., only one principle revision), still perform validation and version decision steps.
|
||||
|
||||
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
|
||||
|
||||
Do not create a new template; always operate on the existing `.ai/standards/constitution.md` file.
|
||||
199
.codex/prompts/speckit.fix.md
Normal file
199
.codex/prompts/speckit.fix.md
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
|
||||
description: Fix failing tests and implementation issues based on test reports
|
||||
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Goal
|
||||
|
||||
Analyze test failure reports, identify root causes, and fix implementation issues while preserving semantic protocol compliance.
|
||||
|
||||
## Operating Constraints
|
||||
|
||||
1. **USE CODER MODE**: Always switch to `coder` mode for code fixes
|
||||
2. **SEMANTIC PROTOCOL**: Never remove semantic annotations ([DEF], @TAGS). Only update code logic.
|
||||
3. **TEST DATA**: If tests use @TEST_DATA fixtures, preserve them when fixing
|
||||
4. **NO DELETION**: Never delete existing tests or semantic annotations
|
||||
5. **REPORT FIRST**: Always write a fix report before making changes
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Load Test Report
|
||||
|
||||
**Required**: Test report file path (e.g., `specs/<feature>/tests/reports/2026-02-19-report.md`)
|
||||
|
||||
**Parse the report for**:
|
||||
- Failed test cases
|
||||
- Error messages
|
||||
- Stack traces
|
||||
- Expected vs actual behavior
|
||||
- Affected modules/files
|
||||
|
||||
### 2. Analyze Root Causes
|
||||
|
||||
For each failed test:
|
||||
|
||||
1. **Read the test file** to understand what it's testing
|
||||
2. **Read the implementation file** to find the bug
|
||||
3. **Check semantic protocol compliance**:
|
||||
- Does the implementation have correct [DEF] anchors?
|
||||
- Are @TAGS (@PRE, @POST, @UX_STATE, etc.) present?
|
||||
- Does the code match the TIER requirements?
|
||||
4. **Identify the fix**:
|
||||
- Logic error in implementation
|
||||
- Missing error handling
|
||||
- Incorrect API usage
|
||||
- State management issue
|
||||
|
||||
### 3. Write Fix Report
|
||||
|
||||
Create a structured fix report:
|
||||
|
||||
```markdown
|
||||
# Fix Report: [FEATURE]
|
||||
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**Report**: [Test Report Path]
|
||||
**Fixer**: Coder Agent
|
||||
|
||||
## Summary
|
||||
|
||||
- Total Failed Tests: [X]
|
||||
- Total Fixed: [X]
|
||||
- Total Skipped: [X]
|
||||
|
||||
## Failed Tests Analysis
|
||||
|
||||
### Test: [Test Name]
|
||||
|
||||
**File**: `path/to/test.py`
|
||||
**Error**: [Error message]
|
||||
|
||||
**Root Cause**: [Explanation of why test failed]
|
||||
|
||||
**Fix Required**: [Description of fix]
|
||||
|
||||
**Status**: [Pending/In Progress/Completed]
|
||||
|
||||
## Fixes Applied
|
||||
|
||||
### Fix 1: [Description]
|
||||
|
||||
**Affected File**: `path/to/file.py`
|
||||
**Test Affected**: `[Test Name]`
|
||||
|
||||
**Changes**:
|
||||
```diff
|
||||
<<<<<<< SEARCH
|
||||
[Original Code]
|
||||
=======
|
||||
[Fixed Code]
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
**Verification**: [How to verify fix works]
|
||||
|
||||
**Semantic Integrity**: [Confirmed annotations preserved]
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [ ] Run tests to verify fix: `cd backend && .venv/bin/python3 -m pytest`
|
||||
- [ ] Check for related failing tests
|
||||
- [ ] Update test documentation if needed
|
||||
```
|
||||
|
||||
### 4. Apply Fixes (in Coder Mode)
|
||||
|
||||
Switch to `coder` mode and apply fixes:
|
||||
|
||||
1. **Read the implementation file** to get exact content
|
||||
2. **Apply the fix** using apply_diff
|
||||
3. **Preserve all semantic annotations**:
|
||||
- Keep [DEF:...] and [/DEF:...] anchors
|
||||
- Keep all @TAGS (@PURPOSE, @LAYER, @TIER, @RELATION, @PRE, @POST, @UX_STATE, @UX_FEEDBACK, @UX_RECOVERY)
|
||||
4. **Only update code logic** to fix the bug
|
||||
5. **Run tests** to verify the fix
|
||||
|
||||
### 5. Verification
|
||||
|
||||
After applying fixes:
|
||||
|
||||
1. **Run tests**:
|
||||
```bash
|
||||
cd backend && .venv/bin/python3 -m pytest -v
|
||||
```
|
||||
or
|
||||
```bash
|
||||
cd frontend && npm run test
|
||||
```
|
||||
|
||||
2. **Check test results**:
|
||||
- Failed tests should now pass
|
||||
- No new tests should fail
|
||||
- Coverage should not decrease
|
||||
|
||||
3. **Update fix report** with results:
|
||||
- Mark fixes as completed
|
||||
- Add verification steps
|
||||
- Note any remaining issues
|
||||
|
||||
## Output
|
||||
|
||||
Generate final fix report:
|
||||
|
||||
```markdown
|
||||
# Fix Report: [FEATURE] - COMPLETED
|
||||
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**Report**: [Test Report Path]
|
||||
**Fixer**: Coder Agent
|
||||
|
||||
## Summary
|
||||
|
||||
- Total Failed Tests: [X]
|
||||
- Total Fixed: [X] ✅
|
||||
- Total Skipped: [X]
|
||||
|
||||
## Fixes Applied
|
||||
|
||||
### Fix 1: [Description] ✅
|
||||
|
||||
**Affected File**: `path/to/file.py`
|
||||
**Test Affected**: `[Test Name]`
|
||||
|
||||
**Changes**: [Summary of changes]
|
||||
|
||||
**Verification**: All tests pass ✅
|
||||
|
||||
**Semantic Integrity**: Preserved ✅
|
||||
|
||||
## Test Results
|
||||
|
||||
```
|
||||
[Full test output showing all passing tests]
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- [ ] Monitor for similar issues
|
||||
- [ ] Update documentation if needed
|
||||
- [ ] Consider adding more tests for edge cases
|
||||
|
||||
## Related Files
|
||||
|
||||
- Test Report: [path]
|
||||
- Implementation: [path]
|
||||
- Test File: [path]
|
||||
```
|
||||
|
||||
## Context for Fixing
|
||||
|
||||
$ARGUMENTS
|
||||
135
.codex/prompts/speckit.implement.md
Normal file
135
.codex/prompts/speckit.implement.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
description: Execute the implementation plan by processing and executing all tasks defined in tasks.md
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Check checklists status** (if FEATURE_DIR/checklists/ exists):
|
||||
- Scan all checklist files in the checklists/ directory
|
||||
- For each checklist, count:
|
||||
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
|
||||
- Completed items: Lines matching `- [X]` or `- [x]`
|
||||
- Incomplete items: Lines matching `- [ ]`
|
||||
- Create a status table:
|
||||
|
||||
```text
|
||||
| Checklist | Total | Completed | Incomplete | Status |
|
||||
|-----------|-------|-----------|------------|--------|
|
||||
| ux.md | 12 | 12 | 0 | ✓ PASS |
|
||||
| test.md | 8 | 5 | 3 | ✗ FAIL |
|
||||
| security.md | 6 | 6 | 0 | ✓ PASS |
|
||||
```
|
||||
|
||||
- Calculate overall status:
|
||||
- **PASS**: All checklists have 0 incomplete items
|
||||
- **FAIL**: One or more checklists have incomplete items
|
||||
|
||||
- **If any checklist is incomplete**:
|
||||
- Display the table with incomplete item counts
|
||||
- **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)"
|
||||
- Wait for user response before continuing
|
||||
- If user says "no" or "wait" or "stop", halt execution
|
||||
- If user says "yes" or "proceed" or "continue", proceed to step 3
|
||||
|
||||
- **If all checklists are complete**:
|
||||
- Display the table showing all checklists passed
|
||||
- Automatically proceed to step 3
|
||||
|
||||
3. Load and analyze the implementation context:
|
||||
- **REQUIRED**: Read tasks.md for the complete task list and execution plan
|
||||
- **REQUIRED**: Read plan.md for tech stack, architecture, and file structure
|
||||
- **IF EXISTS**: Read data-model.md for entities and relationships
|
||||
- **IF EXISTS**: Read contracts/ for API specifications and test requirements
|
||||
- **IF EXISTS**: Read research.md for technical decisions and constraints
|
||||
- **IF EXISTS**: Read quickstart.md for integration scenarios
|
||||
|
||||
4. **Project Setup Verification**:
|
||||
- **REQUIRED**: Create/verify ignore files based on actual project setup:
|
||||
|
||||
**Detection & Creation Logic**:
|
||||
- Check if the following command succeeds to determine if the repository is a git repo (create/verify .gitignore if so):
|
||||
|
||||
```sh
|
||||
git rev-parse --git-dir 2>/dev/null
|
||||
```
|
||||
|
||||
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore
|
||||
- Check if .eslintrc* exists → create/verify .eslintignore
|
||||
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns
|
||||
- Check if .prettierrc* exists → create/verify .prettierignore
|
||||
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
|
||||
- Check if terraform files (*.tf) exist → create/verify .terraformignore
|
||||
- Check if .helmignore needed (helm charts present) → create/verify .helmignore
|
||||
|
||||
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
|
||||
**If ignore file missing**: Create with full pattern set for detected technology
|
||||
|
||||
**Common Patterns by Technology** (from plan.md tech stack):
|
||||
- **Node.js/JavaScript/TypeScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*`
|
||||
- **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/`
|
||||
- **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/`
|
||||
- **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/`
|
||||
- **Go**: `*.exe`, `*.test`, `vendor/`, `*.out`
|
||||
- **Ruby**: `.bundle/`, `log/`, `tmp/`, `*.gem`, `vendor/bundle/`
|
||||
- **PHP**: `vendor/`, `*.log`, `*.cache`, `*.env`
|
||||
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
|
||||
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
|
||||
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
|
||||
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `Makefile`, `config.log`, `.idea/`, `*.log`, `.env*`
|
||||
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
|
||||
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
|
||||
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
|
||||
|
||||
**Tool-Specific Patterns**:
|
||||
- **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/`
|
||||
- **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js`
|
||||
- **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
|
||||
- **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl`
|
||||
- **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt`
|
||||
|
||||
5. Parse tasks.md structure and extract:
|
||||
- **Task phases**: Setup, Tests, Core, Integration, Polish
|
||||
- **Task dependencies**: Sequential vs parallel execution rules
|
||||
- **Task details**: ID, description, file paths, parallel markers [P]
|
||||
- **Execution flow**: Order and dependency requirements
|
||||
|
||||
6. Execute implementation following the task plan:
|
||||
- **Phase-by-phase execution**: Complete each phase before moving to the next
|
||||
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
|
||||
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
|
||||
- **File-based coordination**: Tasks affecting the same files must run sequentially
|
||||
- **Validation checkpoints**: Verify each phase completion before proceeding
|
||||
|
||||
7. Implementation execution rules:
|
||||
- **Setup first**: Initialize project structure, dependencies, configuration
|
||||
- **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios
|
||||
- **Core development**: Implement models, services, CLI commands, endpoints
|
||||
- **Integration work**: Database connections, middleware, logging, external services
|
||||
- **Polish and validation**: Unit tests, performance optimization, documentation
|
||||
|
||||
8. Progress tracking and error handling:
|
||||
- Report progress after each completed task
|
||||
- Halt execution if any non-parallel task fails
|
||||
- For parallel tasks [P], continue with successful tasks, report failed ones
|
||||
- Provide clear error messages with context for debugging
|
||||
- Suggest next steps if implementation cannot proceed
|
||||
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.
|
||||
|
||||
9. Completion validation:
|
||||
- Verify all required tasks are completed
|
||||
- Check that implemented features match the original specification
|
||||
- Validate that tests pass and coverage meets requirements
|
||||
- Confirm the implementation follows the technical plan
|
||||
- Report final status with summary of completed work
|
||||
|
||||
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
|
||||
90
.codex/prompts/speckit.plan.md
Normal file
90
.codex/prompts/speckit.plan.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
|
||||
handoffs:
|
||||
- label: Create Tasks
|
||||
agent: speckit.tasks
|
||||
prompt: Break the plan into tasks
|
||||
send: true
|
||||
- label: Create Checklist
|
||||
agent: speckit.checklist
|
||||
prompt: Create a checklist for the following domain...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Load context**: Read FEATURE_SPEC and `.ai/standards/constitution.md`. Load IMPL_PLAN template (already copied).
|
||||
|
||||
3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
|
||||
- Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")
|
||||
- Fill Constitution Check section from constitution
|
||||
- Evaluate gates (ERROR if violations unjustified)
|
||||
- Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION)
|
||||
- Phase 1: Generate data-model.md, contracts/, quickstart.md
|
||||
- Phase 1: Update agent context by running the agent script
|
||||
- Re-evaluate Constitution Check post-design
|
||||
|
||||
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0: Outline & Research
|
||||
|
||||
1. **Extract unknowns from Technical Context** above:
|
||||
- For each NEEDS CLARIFICATION → research task
|
||||
- For each dependency → best practices task
|
||||
- For each integration → patterns task
|
||||
|
||||
2. **Generate and dispatch research agents**:
|
||||
|
||||
```text
|
||||
For each unknown in Technical Context:
|
||||
Task: "Research {unknown} for {feature context}"
|
||||
For each technology choice:
|
||||
Task: "Find best practices for {tech} in {domain}"
|
||||
```
|
||||
|
||||
3. **Consolidate findings** in `research.md` using format:
|
||||
- Decision: [what was chosen]
|
||||
- Rationale: [why chosen]
|
||||
- Alternatives considered: [what else evaluated]
|
||||
|
||||
**Output**: research.md with all NEEDS CLARIFICATION resolved
|
||||
|
||||
### Phase 1: Design & Contracts
|
||||
|
||||
**Prerequisites:** `research.md` complete
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships
|
||||
- Validation rules from requirements
|
||||
- State transitions if applicable
|
||||
|
||||
2. **Define interface contracts** (if project has external interfaces) → `/contracts/`:
|
||||
- Identify what interfaces the project exposes to users or other systems
|
||||
- Document the contract format appropriate for the project type
|
||||
- Examples: public APIs for libraries, command schemas for CLI tools, endpoints for web services, grammars for parsers, UI contracts for applications
|
||||
- Skip if project is purely internal (build scripts, one-off tools, etc.)
|
||||
|
||||
3. **Agent context update**:
|
||||
- Run `.specify/scripts/bash/update-agent-context.sh agy`
|
||||
- These scripts detect which AI agent is in use
|
||||
- Update the appropriate agent-specific context file
|
||||
- Add only new technology from current plan
|
||||
- Preserve manual additions between markers
|
||||
|
||||
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
|
||||
|
||||
## Key rules
|
||||
|
||||
- Use absolute paths
|
||||
- ERROR on gate failures or unresolved clarifications
|
||||
258
.codex/prompts/speckit.specify.md
Normal file
258
.codex/prompts/speckit.specify.md
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
description: Create or update the feature specification from a natural language feature description.
|
||||
handoffs:
|
||||
- label: Build Technical Plan
|
||||
agent: speckit.plan
|
||||
prompt: Create a plan for the spec. I am building with...
|
||||
- label: Clarify Spec Requirements
|
||||
agent: speckit.clarify
|
||||
prompt: Clarify specification requirements
|
||||
send: true
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
|
||||
|
||||
Given that feature description, do this:
|
||||
|
||||
1. **Generate a concise short name** (2-4 words) for the branch:
|
||||
- Analyze the feature description and extract the most meaningful keywords
|
||||
- Create a 2-4 word short name that captures the essence of the feature
|
||||
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
|
||||
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
|
||||
- Keep it concise but descriptive enough to understand the feature at a glance
|
||||
- Examples:
|
||||
- "I want to add user authentication" → "user-auth"
|
||||
- "Implement OAuth2 integration for the API" → "oauth2-api-integration"
|
||||
- "Create a dashboard for analytics" → "analytics-dashboard"
|
||||
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
||||
|
||||
2. **Check for existing branches before creating new one**:
|
||||
|
||||
a. First, fetch all remote branches to ensure we have the latest information:
|
||||
|
||||
```bash
|
||||
git fetch --all --prune
|
||||
```
|
||||
|
||||
b. Find the highest feature number across all sources for the short-name:
|
||||
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
|
||||
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
|
||||
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
|
||||
|
||||
c. Determine the next available number:
|
||||
- Extract all numbers from all three sources
|
||||
- Find the highest number N
|
||||
- Use N+1 for the new branch number
|
||||
|
||||
d. Run the script `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` with the calculated number and short-name:
|
||||
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
|
||||
- Bash example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"`
|
||||
- PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
|
||||
|
||||
**IMPORTANT**:
|
||||
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
|
||||
- Only match branches/directories with the exact short-name pattern
|
||||
- If no existing branches/directories found with this short-name, start with number 1
|
||||
- You must only ever run this script once per feature
|
||||
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
|
||||
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
|
||||
|
||||
3. Load `.specify/templates/spec-template.md` to understand required sections.
|
||||
|
||||
4. Follow this execution flow:
|
||||
|
||||
1. Parse user description from Input
|
||||
If empty: ERROR "No feature description provided"
|
||||
2. Extract key concepts from description
|
||||
Identify: actors, actions, data, constraints
|
||||
3. For unclear aspects:
|
||||
- Make informed guesses based on context and industry standards
|
||||
- Only mark with [NEEDS CLARIFICATION: specific question] if:
|
||||
- The choice significantly impacts feature scope or user experience
|
||||
- Multiple reasonable interpretations exist with different implications
|
||||
- No reasonable default exists
|
||||
- **LIMIT: Maximum 3 [NEEDS CLARIFICATION] markers total**
|
||||
- Prioritize clarifications by impact: scope > security/privacy > user experience > technical details
|
||||
4. Fill User Scenarios & Testing section
|
||||
If no clear user flow: ERROR "Cannot determine user scenarios"
|
||||
5. Generate Functional Requirements
|
||||
Each requirement must be testable
|
||||
Use reasonable defaults for unspecified details (document assumptions in Assumptions section)
|
||||
6. Define Success Criteria
|
||||
Create measurable, technology-agnostic outcomes
|
||||
Include both quantitative metrics (time, performance, volume) and qualitative measures (user satisfaction, task completion)
|
||||
Each criterion must be verifiable without implementation details
|
||||
7. Identify Key Entities (if data involved)
|
||||
8. Return: SUCCESS (spec ready for planning)
|
||||
|
||||
5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
|
||||
|
||||
6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria:
|
||||
|
||||
a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items:
|
||||
|
||||
```markdown
|
||||
# Specification Quality Checklist: [FEATURE NAME]
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: [DATE]
|
||||
**Feature**: [Link to spec.md]
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [ ] No implementation details (languages, frameworks, APIs)
|
||||
- [ ] Focused on user value and business needs
|
||||
- [ ] Written for non-technical stakeholders
|
||||
- [ ] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [ ] No [NEEDS CLARIFICATION] markers remain
|
||||
- [ ] Requirements are testable and unambiguous
|
||||
- [ ] Success criteria are measurable
|
||||
- [ ] Success criteria are technology-agnostic (no implementation details)
|
||||
- [ ] All acceptance scenarios are defined
|
||||
- [ ] Edge cases are identified
|
||||
- [ ] Scope is clearly bounded
|
||||
- [ ] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [ ] All functional requirements have clear acceptance criteria
|
||||
- [ ] User scenarios cover primary flows
|
||||
- [ ] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [ ] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
|
||||
```
|
||||
|
||||
b. **Run Validation Check**: Review the spec against each checklist item:
|
||||
- For each item, determine if it passes or fails
|
||||
- Document specific issues found (quote relevant spec sections)
|
||||
|
||||
c. **Handle Validation Results**:
|
||||
|
||||
- **If all items pass**: Mark checklist complete and proceed to step 6
|
||||
|
||||
- **If items fail (excluding [NEEDS CLARIFICATION])**:
|
||||
1. List the failing items and specific issues
|
||||
2. Update the spec to address each issue
|
||||
3. Re-run validation until all items pass (max 3 iterations)
|
||||
4. If still failing after 3 iterations, document remaining issues in checklist notes and warn user
|
||||
|
||||
- **If [NEEDS CLARIFICATION] markers remain**:
|
||||
1. Extract all [NEEDS CLARIFICATION: ...] markers from the spec
|
||||
2. **LIMIT CHECK**: If more than 3 markers exist, keep only the 3 most critical (by scope/security/UX impact) and make informed guesses for the rest
|
||||
3. For each clarification needed (max 3), present options to user in this format:
|
||||
|
||||
```markdown
|
||||
## Question [N]: [Topic]
|
||||
|
||||
**Context**: [Quote relevant spec section]
|
||||
|
||||
**What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
|
||||
|
||||
**Suggested Answers**:
|
||||
|
||||
| Option | Answer | Implications |
|
||||
|--------|--------|--------------|
|
||||
| A | [First suggested answer] | [What this means for the feature] |
|
||||
| B | [Second suggested answer] | [What this means for the feature] |
|
||||
| C | [Third suggested answer] | [What this means for the feature] |
|
||||
| Custom | Provide your own answer | [Explain how to provide custom input] |
|
||||
|
||||
**Your choice**: _[Wait for user response]_
|
||||
```
|
||||
|
||||
4. **CRITICAL - Table Formatting**: Ensure markdown tables are properly formatted:
|
||||
- Use consistent spacing with pipes aligned
|
||||
- Each cell should have spaces around content: `| Content |` not `|Content|`
|
||||
- Header separator must have at least 3 dashes: `|--------|`
|
||||
- Test that the table renders correctly in markdown preview
|
||||
5. Number questions sequentially (Q1, Q2, Q3 - max 3 total)
|
||||
6. Present all questions together before waiting for responses
|
||||
7. Wait for user to respond with their choices for all questions (e.g., "Q1: A, Q2: Custom - [details], Q3: B")
|
||||
8. Update the spec by replacing each [NEEDS CLARIFICATION] marker with the user's selected or provided answer
|
||||
9. Re-run validation after all clarifications are resolved
|
||||
|
||||
d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status
|
||||
|
||||
7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`).
|
||||
|
||||
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
## Quick Guidelines
|
||||
|
||||
- Focus on **WHAT** users need and **WHY**.
|
||||
- Avoid HOW to implement (no tech stack, APIs, code structure).
|
||||
- Written for business stakeholders, not developers.
|
||||
- DO NOT create any checklists that are embedded in the spec. That will be a separate command.
|
||||
|
||||
### Section Requirements
|
||||
|
||||
- **Mandatory sections**: Must be completed for every feature
|
||||
- **Optional sections**: Include only when relevant to the feature
|
||||
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
|
||||
|
||||
### For AI Generation
|
||||
|
||||
When creating this spec from a user prompt:
|
||||
|
||||
1. **Make informed guesses**: Use context, industry standards, and common patterns to fill gaps
|
||||
2. **Document assumptions**: Record reasonable defaults in the Assumptions section
|
||||
3. **Limit clarifications**: Maximum 3 [NEEDS CLARIFICATION] markers - use only for critical decisions that:
|
||||
- Significantly impact feature scope or user experience
|
||||
- Have multiple reasonable interpretations with different implications
|
||||
- Lack any reasonable default
|
||||
4. **Prioritize clarifications**: scope > security/privacy > user experience > technical details
|
||||
5. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
|
||||
6. **Common areas needing clarification** (only if no reasonable default exists):
|
||||
- Feature scope and boundaries (include/exclude specific use cases)
|
||||
- User types and permissions (if multiple conflicting interpretations possible)
|
||||
- Security/compliance requirements (when legally/financially significant)
|
||||
|
||||
**Examples of reasonable defaults** (don't ask about these):
|
||||
|
||||
- Data retention: Industry-standard practices for the domain
|
||||
- Performance targets: Standard web/mobile app expectations unless specified
|
||||
- Error handling: User-friendly messages with appropriate fallbacks
|
||||
- Authentication method: Standard session-based or OAuth2 for web apps
|
||||
- Integration patterns: Use project-appropriate patterns (REST/GraphQL for web services, function calls for libraries, CLI args for tools, etc.)
|
||||
|
||||
### Success Criteria Guidelines
|
||||
|
||||
Success criteria must be:
|
||||
|
||||
1. **Measurable**: Include specific metrics (time, percentage, count, rate)
|
||||
2. **Technology-agnostic**: No mention of frameworks, languages, databases, or tools
|
||||
3. **User-focused**: Describe outcomes from user/business perspective, not system internals
|
||||
4. **Verifiable**: Can be tested/validated without knowing implementation details
|
||||
|
||||
**Good examples**:
|
||||
|
||||
- "Users can complete checkout in under 3 minutes"
|
||||
- "System supports 10,000 concurrent users"
|
||||
- "95% of searches return results in under 1 second"
|
||||
- "Task completion rate improves by 40%"
|
||||
|
||||
**Bad examples** (implementation-focused):
|
||||
|
||||
- "API response time is under 200ms" (too technical, use "Users see results instantly")
|
||||
- "Database can handle 1000 TPS" (implementation detail, use user-facing metric)
|
||||
- "React components render efficiently" (framework-specific)
|
||||
- "Redis cache hit rate above 80%" (technology-specific)
|
||||
137
.codex/prompts/speckit.tasks.md
Normal file
137
.codex/prompts/speckit.tasks.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
|
||||
handoffs:
|
||||
- label: Analyze For Consistency
|
||||
agent: speckit.analyze
|
||||
prompt: Run a project analysis for consistency
|
||||
send: true
|
||||
- label: Implement Project
|
||||
agent: speckit.implement
|
||||
prompt: Start the implementation in phases
|
||||
send: true
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Load design documents**: Read from FEATURE_DIR:
|
||||
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
|
||||
- **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md (test scenarios)
|
||||
- Note: Not all projects have all documents. Generate tasks based on what's available.
|
||||
|
||||
3. **Execute task generation workflow**:
|
||||
- Load plan.md and extract tech stack, libraries, project structure
|
||||
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
|
||||
- If data-model.md exists: Extract entities and map to user stories
|
||||
- If contracts/ exists: Map interface contracts to user stories
|
||||
- If research.md exists: Extract decisions for setup tasks
|
||||
- Generate tasks organized by user story (see Task Generation Rules below)
|
||||
- Generate dependency graph showing user story completion order
|
||||
- Create parallel execution examples per user story
|
||||
- Validate task completeness (each user story has all needed tasks, independently testable)
|
||||
|
||||
4. **Generate tasks.md**: Use `.specify/templates/tasks-template.md` as structure, fill with:
|
||||
- Correct feature name from plan.md
|
||||
- Phase 1: Setup tasks (project initialization)
|
||||
- Phase 2: Foundational tasks (blocking prerequisites for all user stories)
|
||||
- Phase 3+: One phase per user story (in priority order from spec.md)
|
||||
- Each phase includes: story goal, independent test criteria, tests (if requested), implementation tasks
|
||||
- Final Phase: Polish & cross-cutting concerns
|
||||
- All tasks must follow the strict checklist format (see Task Generation Rules below)
|
||||
- Clear file paths for each task
|
||||
- Dependencies section showing story completion order
|
||||
- Parallel execution examples per story
|
||||
- Implementation strategy section (MVP first, incremental delivery)
|
||||
|
||||
5. **Report**: Output path to generated tasks.md and summary:
|
||||
- Total task count
|
||||
- Task count per user story
|
||||
- Parallel opportunities identified
|
||||
- Independent test criteria for each story
|
||||
- Suggested MVP scope (typically just User Story 1)
|
||||
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
|
||||
|
||||
Context for task generation: $ARGUMENTS
|
||||
|
||||
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
|
||||
|
||||
## Task Generation Rules
|
||||
|
||||
**CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing.
|
||||
|
||||
**Tests are OPTIONAL**: Only generate test tasks if explicitly requested in the feature specification or if user requests TDD approach.
|
||||
|
||||
### Checklist Format (REQUIRED)
|
||||
|
||||
Every task MUST strictly follow this format:
|
||||
|
||||
```text
|
||||
- [ ] [TaskID] [P?] [Story?] Description with file path
|
||||
```
|
||||
|
||||
**Format Components**:
|
||||
|
||||
1. **Checkbox**: ALWAYS start with `- [ ]` (markdown checkbox)
|
||||
2. **Task ID**: Sequential number (T001, T002, T003...) in execution order
|
||||
3. **[P] marker**: Include ONLY if task is parallelizable (different files, no dependencies on incomplete tasks)
|
||||
4. **[Story] label**: REQUIRED for user story phase tasks only
|
||||
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
|
||||
- Setup phase: NO story label
|
||||
- Foundational phase: NO story label
|
||||
- User Story phases: MUST have story label
|
||||
- Polish phase: NO story label
|
||||
5. **Description**: Clear action with exact file path
|
||||
|
||||
**Examples**:
|
||||
|
||||
- ✅ CORRECT: `- [ ] T001 Create project structure per implementation plan`
|
||||
- ✅ CORRECT: `- [ ] T005 [P] Implement authentication middleware in src/middleware/auth.py`
|
||||
- ✅ CORRECT: `- [ ] T012 [P] [US1] Create User model in src/models/user.py`
|
||||
- ✅ CORRECT: `- [ ] T014 [US1] Implement UserService in src/services/user_service.py`
|
||||
- ❌ WRONG: `- [ ] Create User model` (missing ID and Story label)
|
||||
- ❌ WRONG: `T001 [US1] Create model` (missing checkbox)
|
||||
- ❌ WRONG: `- [ ] [US1] Create User model` (missing Task ID)
|
||||
- ❌ WRONG: `- [ ] T001 [US1] Create model` (missing file path)
|
||||
|
||||
### Task Organization
|
||||
|
||||
1. **From User Stories (spec.md)** - PRIMARY ORGANIZATION:
|
||||
- Each user story (P1, P2, P3...) gets its own phase
|
||||
- Map all related components to their story:
|
||||
- Models needed for that story
|
||||
- Services needed for that story
|
||||
- Interfaces/UI needed for that story
|
||||
- If tests requested: Tests specific to that story
|
||||
- Mark story dependencies (most stories should be independent)
|
||||
|
||||
2. **From Contracts**:
|
||||
- Map each interface contract → to the user story it serves
|
||||
- If tests requested: Each interface contract → contract test task [P] before implementation in that story's phase
|
||||
|
||||
3. **From Data Model**:
|
||||
- Map each entity to the user story(ies) that need it
|
||||
- If entity serves multiple stories: Put in earliest story or Setup phase
|
||||
- Relationships → service layer tasks in appropriate story phase
|
||||
|
||||
4. **From Setup/Infrastructure**:
|
||||
- Shared infrastructure → Setup phase (Phase 1)
|
||||
- Foundational/blocking tasks → Foundational phase (Phase 2)
|
||||
- Story-specific setup → within that story's phase
|
||||
|
||||
### Phase Structure
|
||||
|
||||
- **Phase 1**: Setup (project initialization)
|
||||
- **Phase 2**: Foundational (blocking prerequisites - MUST complete before user stories)
|
||||
- **Phase 3+**: User Stories in priority order (P1, P2, P3...)
|
||||
- Within each story: Tests (if requested) → Models → Services → Endpoints → Integration
|
||||
- Each phase should be a complete, independently testable increment
|
||||
- **Final Phase**: Polish & Cross-Cutting Concerns
|
||||
30
.codex/prompts/speckit.taskstoissues copy.md
Normal file
30
.codex/prompts/speckit.taskstoissues copy.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts.
|
||||
tools: ['github/github-mcp-server/issue_write']
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
1. From the executed script, extract the path to **tasks**.
|
||||
1. Get the Git remote by running:
|
||||
|
||||
```bash
|
||||
git config --get remote.origin.url
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
|
||||
|
||||
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
|
||||
|
||||
> [!CAUTION]
|
||||
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL
|
||||
178
.codex/prompts/speckit.test.md
Normal file
178
.codex/prompts/speckit.test.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
|
||||
description: Generate tests, manage test documentation, and ensure maximum code coverage
|
||||
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Goal
|
||||
|
||||
Execute full testing cycle: analyze code for testable modules, write tests with proper coverage, maintain test documentation, and ensure no test duplication or deletion.
|
||||
|
||||
## Operating Constraints
|
||||
|
||||
1. **NEVER delete existing tests** - Only update if they fail due to bugs in the test or implementation
|
||||
2. **NEVER duplicate tests** - Check existing tests first before creating new ones
|
||||
3. **Use TEST_DATA fixtures** - For CRITICAL tier modules, read @TEST_DATA from .specify/memory/semantics.md
|
||||
4. **Co-location required** - Write tests in `__tests__` directories relative to the code being tested
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Analyze Context
|
||||
|
||||
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS.
|
||||
|
||||
Determine:
|
||||
- FEATURE_DIR - where the feature is located
|
||||
- TASKS_FILE - path to tasks.md
|
||||
- Which modules need testing based on task status
|
||||
|
||||
### 2. Load Relevant Artifacts
|
||||
|
||||
**From tasks.md:**
|
||||
- Identify completed implementation tasks (not test tasks)
|
||||
- Extract file paths that need tests
|
||||
|
||||
**From .specify/memory/semantics.md:**
|
||||
- Read @TIER annotations for modules
|
||||
- For CRITICAL modules: Read @TEST_DATA fixtures
|
||||
|
||||
**From existing tests:**
|
||||
- Scan `__tests__` directories for existing tests
|
||||
- Identify test patterns and coverage gaps
|
||||
|
||||
### 3. Test Coverage Analysis
|
||||
|
||||
Create coverage matrix:
|
||||
|
||||
| Module | File | Has Tests | TIER | TEST_DATA Available |
|
||||
|--------|------|-----------|------|-------------------|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
### 4. Write Tests (TDD Approach)
|
||||
|
||||
For each module requiring tests:
|
||||
|
||||
1. **Check existing tests**: Scan `__tests__/` for duplicates
|
||||
2. **Read TEST_DATA**: If CRITICAL tier, read @TEST_DATA from .specify/memory/semantics.md
|
||||
3. **Write test**: Follow co-location strategy
|
||||
- Python: `src/module/__tests__/test_module.py`
|
||||
- Svelte: `src/lib/components/__tests__/test_component.test.js`
|
||||
4. **Use mocks**: Use `unittest.mock.MagicMock` for external dependencies
|
||||
|
||||
### 4a. UX Contract Testing (Frontend Components)
|
||||
|
||||
For Svelte components with `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY` tags:
|
||||
|
||||
1. **Parse UX tags**: Read component file and extract all `@UX_*` annotations
|
||||
2. **Generate UX tests**: Create tests for each UX state transition
|
||||
```javascript
|
||||
// Example: Testing @UX_STATE: Idle -> Expanded
|
||||
it('should transition from Idle to Expanded on toggle click', async () => {
|
||||
render(Sidebar);
|
||||
const toggleBtn = screen.getByRole('button', { name: /toggle/i });
|
||||
await fireEvent.click(toggleBtn);
|
||||
expect(screen.getByTestId('sidebar')).toHaveClass('expanded');
|
||||
});
|
||||
```
|
||||
3. **Test @UX_FEEDBACK**: Verify visual feedback (toast, shake, color changes)
|
||||
4. **Test @UX_RECOVERY**: Verify error recovery mechanisms (retry, clear input)
|
||||
5. **Use @UX_TEST fixtures**: If component has `@UX_TEST` tags, use them as test specifications
|
||||
|
||||
**UX Test Template:**
|
||||
```javascript
|
||||
// [DEF:__tests__/test_Component:Module]
|
||||
// @RELATION: VERIFIES -> ../Component.svelte
|
||||
// @PURPOSE: Test UX states and transitions
|
||||
|
||||
describe('Component UX States', () => {
|
||||
// @UX_STATE: Idle -> {action: click, expected: Active}
|
||||
it('should transition Idle -> Active on click', async () => { ... });
|
||||
|
||||
// @UX_FEEDBACK: Toast on success
|
||||
it('should show toast on successful action', async () => { ... });
|
||||
|
||||
// @UX_RECOVERY: Retry on error
|
||||
it('should allow retry on error', async () => { ... });
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Test Documentation
|
||||
|
||||
Create/update documentation in `specs/<feature>/tests/`:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── README.md # Test strategy and overview
|
||||
├── coverage.md # Coverage matrix and reports
|
||||
└── reports/
|
||||
└── YYYY-MM-DD-report.md
|
||||
```
|
||||
|
||||
### 6. Execute Tests
|
||||
|
||||
Run tests and report results:
|
||||
|
||||
**Backend:**
|
||||
```bash
|
||||
cd backend && .venv/bin/python3 -m pytest -v
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
```bash
|
||||
cd frontend && npm run test
|
||||
```
|
||||
|
||||
### 7. Update Tasks
|
||||
|
||||
Mark test tasks as completed in tasks.md with:
|
||||
- Test file path
|
||||
- Coverage achieved
|
||||
- Any issues found
|
||||
|
||||
## Output
|
||||
|
||||
Generate test execution report:
|
||||
|
||||
```markdown
|
||||
# Test Report: [FEATURE]
|
||||
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**Executed by**: Tester Agent
|
||||
|
||||
## Coverage Summary
|
||||
|
||||
| Module | Tests | Coverage % |
|
||||
|--------|-------|------------|
|
||||
| ... | ... | ... |
|
||||
|
||||
## Test Results
|
||||
|
||||
- Total: [X]
|
||||
- Passed: [X]
|
||||
- Failed: [X]
|
||||
- Skipped: [X]
|
||||
|
||||
## Issues Found
|
||||
|
||||
| Test | Error | Resolution |
|
||||
|------|-------|------------|
|
||||
| ... | ... | ... |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [ ] Fix failed tests
|
||||
- [ ] Add more coverage for [module]
|
||||
- [ ] Review TEST_DATA fixtures
|
||||
```
|
||||
|
||||
## Context for Testing
|
||||
|
||||
$ARGUMENTS
|
||||
50
.specify/memory/constitution.md
Normal file
50
.specify/memory/constitution.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# [PROJECT_NAME] Constitution
|
||||
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
|
||||
|
||||
## Core Principles
|
||||
|
||||
### [PRINCIPLE_1_NAME]
|
||||
<!-- Example: I. Library-First -->
|
||||
[PRINCIPLE_1_DESCRIPTION]
|
||||
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
|
||||
|
||||
### [PRINCIPLE_2_NAME]
|
||||
<!-- Example: II. CLI Interface -->
|
||||
[PRINCIPLE_2_DESCRIPTION]
|
||||
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
|
||||
|
||||
### [PRINCIPLE_3_NAME]
|
||||
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
|
||||
[PRINCIPLE_3_DESCRIPTION]
|
||||
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
|
||||
|
||||
### [PRINCIPLE_4_NAME]
|
||||
<!-- Example: IV. Integration Testing -->
|
||||
[PRINCIPLE_4_DESCRIPTION]
|
||||
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
|
||||
|
||||
### [PRINCIPLE_5_NAME]
|
||||
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
|
||||
[PRINCIPLE_5_DESCRIPTION]
|
||||
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
|
||||
|
||||
## [SECTION_2_NAME]
|
||||
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
|
||||
|
||||
[SECTION_2_CONTENT]
|
||||
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
|
||||
|
||||
## [SECTION_3_NAME]
|
||||
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
|
||||
|
||||
[SECTION_3_CONTENT]
|
||||
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
|
||||
|
||||
## Governance
|
||||
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
|
||||
|
||||
[GOVERNANCE_RULES]
|
||||
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
|
||||
|
||||
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
|
||||
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
Auto-generated from all feature plans. Last updated: [DATE]
|
||||
|
||||
## Knowledge Graph (GRACE)
|
||||
**CRITICAL**: This project uses a GRACE Knowledge Graph for context. Always load the root map first:
|
||||
- **Root Map**: `.ai/ROOT.md` -> `[DEF:Project_Knowledge_Map:Root]`
|
||||
- **Project Map**: `.ai/PROJECT_MAP.md` -> `[DEF:Project_Map]`
|
||||
- **Standards**: Read `.ai/standards/` for architecture and style rules.
|
||||
|
||||
## Active Technologies
|
||||
|
||||
[EXTRACTED FROM ALL PLAN.MD FILES]
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
the iteration process.
|
||||
-->
|
||||
|
||||
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
|
||||
**Primary Dependencies**: [e.g., FastAPI, Tailwind CSS, SvelteKit or NEEDS CLARIFICATION]
|
||||
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
|
||||
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
|
||||
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
|
||||
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
|
||||
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
|
||||
@@ -102,14 +102,3 @@ directories captured above]
|
||||
|-----------|------------|-------------------------------------|
|
||||
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
|
||||
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
|
||||
|
||||
## Test Data Reference
|
||||
|
||||
> **For CRITICAL tier components, reference test fixtures from spec.md**
|
||||
|
||||
| Component | TIER | Fixture Name | Location |
|
||||
|-----------|------|--------------|----------|
|
||||
| [e.g., DashboardAPI] | CRITICAL | valid_dashboard | spec.md#test-data-fixtures |
|
||||
| [e.g., TaskDrawer] | CRITICAL | task_states | spec.md#test-data-fixtures |
|
||||
|
||||
**Note**: Tester Agent MUST use these fixtures when writing unit tests for CRITICAL modules. See `.ai/standards/semantics.md` for @TEST_DATA syntax.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Feature Specification: [FEATURE NAME]
|
||||
|
||||
**Feature Branch**: `[###-feature-name]`
|
||||
**Reference UX**: `[ux_reference.md]` (See specific folder)
|
||||
**Created**: [DATE]
|
||||
**Status**: Draft
|
||||
**Input**: User description: "$ARGUMENTS"
|
||||
@@ -114,52 +113,3 @@
|
||||
- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
|
||||
- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
|
||||
- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
|
||||
|
||||
---
|
||||
|
||||
## Test Data Fixtures *(recommended for CRITICAL components)*
|
||||
|
||||
<!--
|
||||
Define reference/fixture data for testing CRITICAL tier components.
|
||||
This data will be used by the Tester Agent when writing unit tests.
|
||||
Format: JSON or YAML that matches the component's data structures.
|
||||
-->
|
||||
|
||||
### Fixtures
|
||||
|
||||
```yaml
|
||||
# Example fixture format
|
||||
fixture_name:
|
||||
description: "Description of this test data"
|
||||
data:
|
||||
# JSON or YAML data structure
|
||||
```
|
||||
|
||||
### Example: Dashboard API
|
||||
|
||||
```yaml
|
||||
valid_dashboard:
|
||||
description: "Valid dashboard object for API responses"
|
||||
data:
|
||||
id: 1
|
||||
title: "Sales Report"
|
||||
slug: "sales"
|
||||
git_status:
|
||||
branch: "main"
|
||||
sync_status: "OK"
|
||||
last_task:
|
||||
task_id: "task-123"
|
||||
status: "SUCCESS"
|
||||
|
||||
empty_dashboards:
|
||||
description: "Empty dashboard list response"
|
||||
data:
|
||||
dashboards: []
|
||||
total: 0
|
||||
page: 1
|
||||
|
||||
error_not_found:
|
||||
description: "404 error response"
|
||||
data:
|
||||
detail: "Dashboard not found"
|
||||
```
|
||||
|
||||
@@ -93,8 +93,7 @@ Examples of foundational tasks (adjust based on your project):
|
||||
- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
|
||||
- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
|
||||
- [ ] T016 [US1] Add validation and error handling
|
||||
- [ ] T017 [US1] [P] Implement UI using Tailwind CSS (minimize scoped styles)
|
||||
- [ ] T018 [US1] Add logging for user story 1 operations
|
||||
- [ ] T017 [US1] Add logging for user story 1 operations
|
||||
|
||||
**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
|
||||
|
||||
|
||||
14
backend/conftest.py
Normal file
14
backend/conftest.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# conftest.py at backend root
|
||||
# Prevents pytest collection errors caused by duplicate test module names
|
||||
# between the root tests/ directory and co-located src/<module>/__tests__/ directories.
|
||||
# Without this, pytest sees e.g. tests/test_auth.py and src/core/auth/__tests__/test_auth.py
|
||||
# and raises "import file mismatch" because both map to module name "test_auth".
|
||||
|
||||
import os
|
||||
|
||||
# Files in tests/ that clash with __tests__/ co-located tests
|
||||
collect_ignore = [
|
||||
os.path.join("tests", "test_auth.py"),
|
||||
os.path.join("tests", "test_logger.py"),
|
||||
os.path.join("tests", "test_models.py"),
|
||||
]
|
||||
Submodule backend/git_repos/12 deleted from 57ab7e8679
3
backend/pyproject.toml
Normal file
3
backend/pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
importmode = "importlib"
|
||||
@@ -10,6 +10,7 @@ import os
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from datetime import datetime, timedelta
|
||||
import pytest
|
||||
|
||||
# Force isolated sqlite databases for test module before dependencies import.
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_api.db")
|
||||
@@ -446,7 +447,7 @@ def test_list_conversations_groups_by_conversation_and_marks_archived():
|
||||
conversation_id="conv-old",
|
||||
role="user",
|
||||
text="old chat",
|
||||
created_at=now - timedelta(days=assistant_module.ASSISTANT_ARCHIVE_AFTER_DAYS + 2),
|
||||
created_at=now - timedelta(days=32), # Hardcoded threshold+2
|
||||
)
|
||||
)
|
||||
|
||||
@@ -536,7 +537,7 @@ def test_list_conversations_archived_only_filters_active():
|
||||
conversation_id="conv-archived-2",
|
||||
role="user",
|
||||
text="archived",
|
||||
created_at=now - timedelta(days=assistant_module.ASSISTANT_ARCHIVE_AFTER_DAYS + 3),
|
||||
created_at=now - timedelta(days=33), # Hardcoded threshold+3
|
||||
)
|
||||
)
|
||||
|
||||
@@ -624,5 +625,25 @@ def test_guarded_operation_confirm_roundtrip():
|
||||
assert second.task_id is not None
|
||||
|
||||
|
||||
# [DEF:test_confirm_nonexistent_id_returns_404:Function]
|
||||
# @PURPOSE: Confirming a non-existent ID should raise 404.
|
||||
# @PRE: user tries to confirm a random/fake UUID.
|
||||
# @POST: FastAPI HTTPException with status 404.
|
||||
def test_confirm_nonexistent_id_returns_404():
|
||||
from fastapi import HTTPException
|
||||
_clear_assistant_state()
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id="non-existent-id",
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert exc.value.status_code == 404
|
||||
|
||||
|
||||
# [/DEF:test_guarded_operation_confirm_roundtrip:Function]
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, AsyncMock
|
||||
from datetime import datetime, timezone
|
||||
from fastapi.testclient import TestClient
|
||||
from src.app import app
|
||||
from src.api.routes.dashboards import DashboardsResponse
|
||||
@@ -354,4 +355,84 @@ def test_get_database_mappings_success():
|
||||
# [/DEF:test_get_database_mappings_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_tasks_history_filters_success:Function]
|
||||
# @TEST: GET /api/dashboards/{id}/tasks returns backup and llm tasks for dashboard
|
||||
def test_get_dashboard_tasks_history_filters_success():
|
||||
with patch("src.api.routes.dashboards.get_task_manager") as mock_task_mgr, \
|
||||
patch("src.api.routes.dashboards.has_permission") as mock_perm:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
llm_task = MagicMock()
|
||||
llm_task.id = "task-llm-1"
|
||||
llm_task.plugin_id = "llm_dashboard_validation"
|
||||
llm_task.status = "SUCCESS"
|
||||
llm_task.started_at = now
|
||||
llm_task.finished_at = now
|
||||
llm_task.params = {"dashboard_id": "42", "environment_id": "prod"}
|
||||
llm_task.result = {"summary": "LLM validation complete"}
|
||||
|
||||
backup_task = MagicMock()
|
||||
backup_task.id = "task-backup-1"
|
||||
backup_task.plugin_id = "superset-backup"
|
||||
backup_task.status = "RUNNING"
|
||||
backup_task.started_at = now
|
||||
backup_task.finished_at = None
|
||||
backup_task.params = {"env": "prod", "dashboards": [42]}
|
||||
backup_task.result = {}
|
||||
|
||||
other_task = MagicMock()
|
||||
other_task.id = "task-other"
|
||||
other_task.plugin_id = "superset-backup"
|
||||
other_task.status = "SUCCESS"
|
||||
other_task.started_at = now
|
||||
other_task.finished_at = now
|
||||
other_task.params = {"env": "prod", "dashboards": [777]}
|
||||
other_task.result = {}
|
||||
|
||||
mock_task_mgr.return_value.get_all_tasks.return_value = [other_task, llm_task, backup_task]
|
||||
mock_perm.return_value = lambda: True
|
||||
|
||||
response = client.get("/api/dashboards/42/tasks?env_id=prod&limit=10")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["dashboard_id"] == 42
|
||||
assert len(data["items"]) == 2
|
||||
assert {item["plugin_id"] for item in data["items"]} == {"llm_dashboard_validation", "superset-backup"}
|
||||
# [/DEF:test_get_dashboard_tasks_history_filters_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_thumbnail_success:Function]
|
||||
# @TEST: GET /api/dashboards/{id}/thumbnail proxies image bytes from Superset
|
||||
def test_get_dashboard_thumbnail_success():
|
||||
with patch("src.api.routes.dashboards.get_config_manager") as mock_config, \
|
||||
patch("src.api.routes.dashboards.has_permission") as mock_perm, \
|
||||
patch("src.api.routes.dashboards.SupersetClient") as mock_client_cls:
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_config.return_value.get_environments.return_value = [mock_env]
|
||||
mock_perm.return_value = lambda: True
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b"fake-image-bytes"
|
||||
mock_response.headers = {"Content-Type": "image/png"}
|
||||
|
||||
def _network_request(method, endpoint, **kwargs):
|
||||
if method == "POST":
|
||||
return {"image_url": "/api/v1/dashboard/42/screenshot/abc123/"}
|
||||
return mock_response
|
||||
|
||||
mock_client.network.request.side_effect = _network_request
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
response = client.get("/api/dashboards/42/thumbnail?env_id=prod")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"fake-image-bytes"
|
||||
assert response.headers["content-type"].startswith("image/png")
|
||||
# [/DEF:test_get_dashboard_thumbnail_success:Function]
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_dashboards:Module]
|
||||
|
||||
410
backend/src/api/routes/__tests__/test_migration_routes.py
Normal file
410
backend/src/api/routes/__tests__/test_migration_routes.py
Normal file
@@ -0,0 +1,410 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_migration_routes:Module]
|
||||
#
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Unit tests for migration API route handlers.
|
||||
# @LAYER: API
|
||||
# @RELATION: VERIFIES -> backend.src.api.routes.migration
|
||||
#
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Add backend directory to sys.path
|
||||
backend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())
|
||||
if backend_dir not in sys.path:
|
||||
sys.path.insert(0, backend_dir)
|
||||
|
||||
import os
|
||||
# Force SQLite in-memory for all database connections BEFORE importing any application code
|
||||
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
|
||||
os.environ["TASKS_DATABASE_URL"] = "sqlite:///:memory:"
|
||||
os.environ["AUTH_DATABASE_URL"] = "sqlite:///:memory:"
|
||||
os.environ["ENVIRONMENT"] = "testing"
|
||||
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from src.models.mapping import Base, ResourceMapping, ResourceType
|
||||
|
||||
# Patch the get_db dependency if `src.api.routes.migration` imports it
|
||||
from unittest.mock import patch
|
||||
patch('src.core.database.get_db').start()
|
||||
|
||||
# --- Fixtures ---
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""In-memory SQLite session for testing."""
|
||||
from sqlalchemy.pool import StaticPool
|
||||
engine = create_engine(
|
||||
'sqlite:///:memory:',
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=StaticPool
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
|
||||
def _make_config_manager(cron="0 2 * * *"):
|
||||
"""Creates a mock config manager with a realistic AppConfig-like object."""
|
||||
settings = MagicMock()
|
||||
settings.migration_sync_cron = cron
|
||||
config = MagicMock()
|
||||
config.settings = settings
|
||||
cm = MagicMock()
|
||||
cm.get_config.return_value = config
|
||||
cm.save_config = MagicMock()
|
||||
return cm
|
||||
|
||||
|
||||
# --- get_migration_settings tests ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_migration_settings_returns_default_cron():
|
||||
"""Verify the settings endpoint returns the stored cron string."""
|
||||
from src.api.routes.migration import get_migration_settings
|
||||
|
||||
cm = _make_config_manager(cron="0 3 * * *")
|
||||
|
||||
# Call the handler directly, bypassing Depends
|
||||
result = await get_migration_settings(config_manager=cm, _=None)
|
||||
|
||||
assert result == {"cron": "0 3 * * *"}
|
||||
cm.get_config.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_migration_settings_returns_fallback_when_no_cron():
|
||||
"""When migration_sync_cron uses the default, should return '0 2 * * *'."""
|
||||
from src.api.routes.migration import get_migration_settings
|
||||
|
||||
# Use the default cron value (simulating a fresh config)
|
||||
cm = _make_config_manager()
|
||||
|
||||
result = await get_migration_settings(config_manager=cm, _=None)
|
||||
|
||||
assert result == {"cron": "0 2 * * *"}
|
||||
|
||||
|
||||
# --- update_migration_settings tests ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_migration_settings_saves_cron():
|
||||
"""Verify that a valid cron update saves to config."""
|
||||
from src.api.routes.migration import update_migration_settings
|
||||
|
||||
cm = _make_config_manager()
|
||||
|
||||
result = await update_migration_settings(
|
||||
payload={"cron": "0 4 * * *"},
|
||||
config_manager=cm,
|
||||
_=None
|
||||
)
|
||||
|
||||
assert result["cron"] == "0 4 * * *"
|
||||
assert result["status"] == "updated"
|
||||
cm.save_config.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_migration_settings_rejects_missing_cron():
|
||||
"""Verify 400 error when 'cron' key is missing from payload."""
|
||||
from src.api.routes.migration import update_migration_settings
|
||||
|
||||
cm = _make_config_manager()
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await update_migration_settings(
|
||||
payload={"interval": "daily"},
|
||||
config_manager=cm,
|
||||
_=None
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "cron" in exc_info.value.detail.lower()
|
||||
|
||||
|
||||
# --- get_resource_mappings tests ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_resource_mappings_returns_formatted_list(db_session):
|
||||
"""Verify mappings are returned as formatted dicts with correct keys."""
|
||||
from src.api.routes.migration import get_resource_mappings
|
||||
|
||||
# Populate test data
|
||||
m1 = ResourceMapping(
|
||||
environment_id="prod",
|
||||
resource_type=ResourceType.CHART,
|
||||
uuid="uuid-1",
|
||||
remote_integer_id="42",
|
||||
resource_name="Sales Chart",
|
||||
last_synced_at=datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
)
|
||||
db_session.add(m1)
|
||||
db_session.commit()
|
||||
|
||||
result = await get_resource_mappings(skip=0, limit=50, search=None, env_id=None, resource_type=None, db=db_session, _=None)
|
||||
|
||||
assert result["total"] == 1
|
||||
assert len(result["items"]) == 1
|
||||
assert result["items"][0]["environment_id"] == "prod"
|
||||
assert result["items"][0]["resource_type"] == "chart"
|
||||
assert result["items"][0]["uuid"] == "uuid-1"
|
||||
assert result["items"][0]["remote_id"] == "42"
|
||||
assert result["items"][0]["resource_name"] == "Sales Chart"
|
||||
assert result["items"][0]["last_synced_at"] is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_resource_mappings_respects_pagination(db_session):
|
||||
"""Verify skip and limit parameters work correctly."""
|
||||
from src.api.routes.migration import get_resource_mappings
|
||||
|
||||
for i in range(5):
|
||||
db_session.add(ResourceMapping(
|
||||
environment_id="prod",
|
||||
resource_type=ResourceType.DATASET,
|
||||
uuid=f"uuid-{i}",
|
||||
remote_integer_id=str(i),
|
||||
))
|
||||
db_session.commit()
|
||||
|
||||
result = await get_resource_mappings(skip=2, limit=2, search=None, env_id=None, resource_type=None, db=db_session, _=None)
|
||||
|
||||
assert result["total"] == 5
|
||||
assert len(result["items"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_resource_mappings_search_by_name(db_session):
|
||||
"""Verify search filters by resource_name."""
|
||||
from src.api.routes.migration import get_resource_mappings
|
||||
|
||||
db_session.add(ResourceMapping(environment_id="prod", resource_type=ResourceType.CHART, uuid="u1", remote_integer_id="1", resource_name="Sales Chart"))
|
||||
db_session.add(ResourceMapping(environment_id="prod", resource_type=ResourceType.CHART, uuid="u2", remote_integer_id="2", resource_name="Revenue Dashboard"))
|
||||
db_session.commit()
|
||||
|
||||
result = await get_resource_mappings(skip=0, limit=50, search="sales", env_id=None, resource_type=None, db=db_session, _=None)
|
||||
assert result["total"] == 1
|
||||
assert result["items"][0]["resource_name"] == "Sales Chart"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_resource_mappings_filter_by_env(db_session):
|
||||
"""Verify env_id filter returns only matching environment."""
|
||||
from src.api.routes.migration import get_resource_mappings
|
||||
|
||||
db_session.add(ResourceMapping(environment_id="ss1", resource_type=ResourceType.CHART, uuid="u1", remote_integer_id="1", resource_name="Chart A"))
|
||||
db_session.add(ResourceMapping(environment_id="ss2", resource_type=ResourceType.CHART, uuid="u2", remote_integer_id="2", resource_name="Chart B"))
|
||||
db_session.commit()
|
||||
|
||||
result = await get_resource_mappings(skip=0, limit=50, search=None, env_id="ss2", resource_type=None, db=db_session, _=None)
|
||||
assert result["total"] == 1
|
||||
assert result["items"][0]["environment_id"] == "ss2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_resource_mappings_filter_by_type(db_session):
|
||||
"""Verify resource_type filter returns only matching type."""
|
||||
from src.api.routes.migration import get_resource_mappings
|
||||
|
||||
db_session.add(ResourceMapping(environment_id="prod", resource_type=ResourceType.CHART, uuid="u1", remote_integer_id="1", resource_name="My Chart"))
|
||||
db_session.add(ResourceMapping(environment_id="prod", resource_type=ResourceType.DATASET, uuid="u2", remote_integer_id="2", resource_name="My Dataset"))
|
||||
db_session.commit()
|
||||
|
||||
result = await get_resource_mappings(skip=0, limit=50, search=None, env_id=None, resource_type="dataset", db=db_session, _=None)
|
||||
assert result["total"] == 1
|
||||
assert result["items"][0]["resource_type"] == "dataset"
|
||||
|
||||
|
||||
# --- trigger_sync_now tests ---
|
||||
|
||||
@pytest.fixture
|
||||
def _mock_env():
|
||||
"""Creates a mock config environment object."""
|
||||
env = MagicMock()
|
||||
env.id = "test-env-1"
|
||||
env.name = "Test Env"
|
||||
env.url = "http://superset.test"
|
||||
env.username = "admin"
|
||||
env.password = "admin"
|
||||
env.verify_ssl = False
|
||||
env.timeout = 30
|
||||
return env
|
||||
|
||||
|
||||
def _make_sync_config_manager(environments):
|
||||
"""Creates a mock config manager with environments list."""
|
||||
settings = MagicMock()
|
||||
settings.migration_sync_cron = "0 2 * * *"
|
||||
config = MagicMock()
|
||||
config.settings = settings
|
||||
config.environments = environments
|
||||
cm = MagicMock()
|
||||
cm.get_config.return_value = config
|
||||
cm.get_environments.return_value = environments
|
||||
return cm
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_sync_now_creates_env_row_and_syncs(db_session, _mock_env):
|
||||
"""Verify that trigger_sync_now creates an Environment row in DB before syncing,
|
||||
preventing FK constraint violations on resource_mappings inserts."""
|
||||
from src.api.routes.migration import trigger_sync_now
|
||||
from src.models.mapping import Environment as EnvironmentModel
|
||||
|
||||
cm = _make_sync_config_manager([_mock_env])
|
||||
|
||||
with patch("src.api.routes.migration.SupersetClient") as MockClient, \
|
||||
patch("src.api.routes.migration.IdMappingService") as MockService:
|
||||
mock_client_instance = MagicMock()
|
||||
MockClient.return_value = mock_client_instance
|
||||
mock_service_instance = MagicMock()
|
||||
MockService.return_value = mock_service_instance
|
||||
|
||||
result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)
|
||||
|
||||
# Environment row must exist in DB
|
||||
env_row = db_session.query(EnvironmentModel).filter_by(id="test-env-1").first()
|
||||
assert env_row is not None
|
||||
assert env_row.name == "Test Env"
|
||||
assert env_row.url == "http://superset.test"
|
||||
|
||||
# Sync must have been called
|
||||
mock_service_instance.sync_environment.assert_called_once_with("test-env-1", mock_client_instance)
|
||||
assert result["synced_count"] == 1
|
||||
assert result["failed_count"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_sync_now_rejects_empty_environments(db_session):
|
||||
"""Verify 400 error when no environments are configured."""
|
||||
from src.api.routes.migration import trigger_sync_now
|
||||
|
||||
cm = _make_sync_config_manager([])
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await trigger_sync_now(config_manager=cm, db=db_session, _=None)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "No environments" in exc_info.value.detail
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_sync_now_handles_partial_failure(db_session, _mock_env):
|
||||
"""Verify that if sync_environment raises for one env, it's captured in failed list."""
|
||||
from src.api.routes.migration import trigger_sync_now
|
||||
|
||||
env2 = MagicMock()
|
||||
env2.id = "test-env-2"
|
||||
env2.name = "Failing Env"
|
||||
env2.url = "http://fail.test"
|
||||
env2.username = "admin"
|
||||
env2.password = "admin"
|
||||
env2.verify_ssl = False
|
||||
env2.timeout = 30
|
||||
|
||||
cm = _make_sync_config_manager([_mock_env, env2])
|
||||
|
||||
with patch("src.api.routes.migration.SupersetClient") as MockClient, \
|
||||
patch("src.api.routes.migration.IdMappingService") as MockService:
|
||||
mock_service_instance = MagicMock()
|
||||
mock_service_instance.sync_environment.side_effect = [None, RuntimeError("Connection refused")]
|
||||
MockService.return_value = mock_service_instance
|
||||
MockClient.return_value = MagicMock()
|
||||
|
||||
result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)
|
||||
|
||||
assert result["synced_count"] == 1
|
||||
assert result["failed_count"] == 1
|
||||
assert result["details"]["failed"][0]["env_id"] == "test-env-2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_sync_now_idempotent_env_upsert(db_session, _mock_env):
|
||||
"""Verify that calling sync twice doesn't duplicate the Environment row."""
|
||||
from src.api.routes.migration import trigger_sync_now
|
||||
from src.models.mapping import Environment as EnvironmentModel
|
||||
|
||||
cm = _make_sync_config_manager([_mock_env])
|
||||
|
||||
with patch("src.api.routes.migration.SupersetClient"), \
|
||||
patch("src.api.routes.migration.IdMappingService"):
|
||||
await trigger_sync_now(config_manager=cm, db=db_session, _=None)
|
||||
await trigger_sync_now(config_manager=cm, db=db_session, _=None)
|
||||
|
||||
env_count = db_session.query(EnvironmentModel).filter_by(id="test-env-1").count()
|
||||
assert env_count == 1
|
||||
|
||||
|
||||
# --- get_dashboards tests ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dashboards_success(_mock_env):
|
||||
from src.api.routes.migration import get_dashboards
|
||||
cm = _make_sync_config_manager([_mock_env])
|
||||
|
||||
with patch("src.api.routes.migration.SupersetClient") as MockClient:
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_dashboards_summary.return_value = [{"id": 1, "title": "Test"}]
|
||||
MockClient.return_value = mock_client
|
||||
|
||||
result = await get_dashboards(env_id="test-env-1", config_manager=cm, _=None)
|
||||
assert len(result) == 1
|
||||
assert result[0]["id"] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dashboards_invalid_env_raises_404(_mock_env):
|
||||
from src.api.routes.migration import get_dashboards
|
||||
cm = _make_sync_config_manager([_mock_env])
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_dashboards(env_id="wrong-env", config_manager=cm, _=None)
|
||||
assert exc.value.status_code == 404
|
||||
|
||||
# --- execute_migration tests ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_migration_success(_mock_env):
|
||||
from src.api.routes.migration import execute_migration
|
||||
from src.models.dashboard import DashboardSelection
|
||||
|
||||
cm = _make_sync_config_manager([_mock_env, _mock_env]) # Need both source/target
|
||||
tm = MagicMock()
|
||||
tm.create_task = AsyncMock(return_value=MagicMock(id="task-123"))
|
||||
|
||||
selection = DashboardSelection(
|
||||
source_env_id="test-env-1",
|
||||
target_env_id="test-env-1",
|
||||
selected_ids=[1, 2]
|
||||
)
|
||||
|
||||
result = await execute_migration(selection=selection, config_manager=cm, task_manager=tm, _=None)
|
||||
assert result["task_id"] == "task-123"
|
||||
tm.create_task.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_migration_invalid_env_raises_400(_mock_env):
|
||||
from src.api.routes.migration import execute_migration
|
||||
from src.models.dashboard import DashboardSelection
|
||||
|
||||
cm = _make_sync_config_manager([_mock_env])
|
||||
selection = DashboardSelection(
|
||||
source_env_id="test-env-1",
|
||||
target_env_id="non-existent",
|
||||
selected_ids=[1]
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await execute_migration(selection=selection, config_manager=cm, task_manager=MagicMock(), _=None)
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_migration_routes:Module]
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:backend.tests.test_reports_api:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, reports, api, contract, pagination, filtering
|
||||
# @PURPOSE: Contract tests for GET /api/reports defaults, pagination, and filtering behavior.
|
||||
# @LAYER: Domain (Tests)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:backend.tests.test_reports_detail_api:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, reports, api, detail, diagnostics
|
||||
# @PURPOSE: Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
|
||||
# @LAYER: Domain (Tests)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:backend.tests.test_reports_openapi_conformance:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, reports, openapi, conformance
|
||||
# @PURPOSE: Validate implemented reports payload shape against OpenAPI-required top-level contract fields.
|
||||
# @LAYER: Domain (Tests)
|
||||
|
||||
@@ -579,6 +579,137 @@ def _resolve_dashboard_id_entity(
|
||||
# [/DEF:_resolve_dashboard_id_entity:Function]
|
||||
|
||||
|
||||
# [DEF:_get_environment_name_by_id:Function]
|
||||
# @PURPOSE: Resolve human-readable environment name by id.
|
||||
# @PRE: environment id may be None.
|
||||
# @POST: Returns matching environment name or fallback id.
|
||||
def _get_environment_name_by_id(env_id: Optional[str], config_manager: ConfigManager) -> str:
|
||||
if not env_id:
|
||||
return "unknown"
|
||||
env = next((item for item in config_manager.get_environments() if item.id == env_id), None)
|
||||
return env.name if env else env_id
|
||||
# [/DEF:_get_environment_name_by_id:Function]
|
||||
|
||||
|
||||
# [DEF:_extract_result_deep_links:Function]
|
||||
# @PURPOSE: Build deep-link actions to verify task result from assistant chat.
|
||||
# @PRE: task object is available.
|
||||
# @POST: Returns zero or more assistant actions for dashboard open/diff.
|
||||
def _extract_result_deep_links(task: Any, config_manager: ConfigManager) -> List[AssistantAction]:
|
||||
plugin_id = getattr(task, "plugin_id", None)
|
||||
params = getattr(task, "params", {}) or {}
|
||||
result = getattr(task, "result", {}) or {}
|
||||
actions: List[AssistantAction] = []
|
||||
dashboard_id: Optional[int] = None
|
||||
env_id: Optional[str] = None
|
||||
|
||||
if plugin_id == "superset-migration":
|
||||
migrated = result.get("migrated_dashboards") if isinstance(result, dict) else None
|
||||
if isinstance(migrated, list) and migrated:
|
||||
first = migrated[0]
|
||||
if isinstance(first, dict) and first.get("id") is not None:
|
||||
dashboard_id = int(first.get("id"))
|
||||
if dashboard_id is None and isinstance(params.get("selected_ids"), list) and params["selected_ids"]:
|
||||
dashboard_id = int(params["selected_ids"][0])
|
||||
env_id = params.get("target_env_id")
|
||||
elif plugin_id == "superset-backup":
|
||||
dashboards = result.get("dashboards") if isinstance(result, dict) else None
|
||||
if isinstance(dashboards, list) and dashboards:
|
||||
first = dashboards[0]
|
||||
if isinstance(first, dict) and first.get("id") is not None:
|
||||
dashboard_id = int(first.get("id"))
|
||||
if dashboard_id is None and isinstance(params.get("dashboard_ids"), list) and params["dashboard_ids"]:
|
||||
dashboard_id = int(params["dashboard_ids"][0])
|
||||
env_id = params.get("environment_id") or _resolve_env_id(result.get("environment"), config_manager)
|
||||
elif plugin_id == "llm_dashboard_validation":
|
||||
if params.get("dashboard_id") is not None:
|
||||
dashboard_id = int(params["dashboard_id"])
|
||||
env_id = params.get("environment_id")
|
||||
|
||||
if dashboard_id is not None and env_id:
|
||||
env_name = _get_environment_name_by_id(env_id, config_manager)
|
||||
actions.append(
|
||||
AssistantAction(
|
||||
type="open_route",
|
||||
label=f"Открыть дашборд в {env_name}",
|
||||
target=f"/dashboards/{dashboard_id}?env_id={env_id}",
|
||||
)
|
||||
)
|
||||
if dashboard_id is not None:
|
||||
actions.append(
|
||||
AssistantAction(
|
||||
type="open_diff",
|
||||
label="Показать Diff",
|
||||
target=str(dashboard_id),
|
||||
)
|
||||
)
|
||||
return actions
|
||||
# [/DEF:_extract_result_deep_links:Function]
|
||||
|
||||
|
||||
# [DEF:_build_task_observability_summary:Function]
|
||||
# @PURPOSE: Build compact textual summary for completed tasks to reduce "black box" effect.
|
||||
# @PRE: task may contain plugin-specific result payload.
|
||||
# @POST: Returns non-empty summary line for known task types or empty string fallback.
|
||||
def _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:
|
||||
plugin_id = getattr(task, "plugin_id", None)
|
||||
status = str(getattr(task, "status", "")).upper()
|
||||
params = getattr(task, "params", {}) or {}
|
||||
result = getattr(task, "result", {}) or {}
|
||||
|
||||
if plugin_id == "superset-migration" and isinstance(result, dict):
|
||||
migrated = len(result.get("migrated_dashboards") or [])
|
||||
failed_rows = result.get("failed_dashboards") or []
|
||||
failed = len(failed_rows)
|
||||
selected = result.get("selected_dashboards", migrated + failed)
|
||||
mappings = result.get("mapping_count", 0)
|
||||
target_env_id = params.get("target_env_id")
|
||||
target_env_name = _get_environment_name_by_id(target_env_id, config_manager)
|
||||
warning = ""
|
||||
if failed_rows:
|
||||
first = failed_rows[0]
|
||||
warning = (
|
||||
f" Внимание: {first.get('title') or first.get('id')}: "
|
||||
f"{first.get('error') or 'ошибка'}."
|
||||
)
|
||||
return (
|
||||
f"Сводка миграции: выбрано {selected}, перенесено {migrated}, "
|
||||
f"с ошибками {failed}, маппингов {mappings}, целевая среда {target_env_name}."
|
||||
f"{warning}"
|
||||
)
|
||||
|
||||
if plugin_id == "superset-backup" and isinstance(result, dict):
|
||||
total = int(result.get("total_dashboards", 0) or 0)
|
||||
ok = int(result.get("backed_up_dashboards", 0) or 0)
|
||||
failed = int(result.get("failed_dashboards", 0) or 0)
|
||||
env_id = params.get("environment_id") or _resolve_env_id(result.get("environment"), config_manager)
|
||||
env_name = _get_environment_name_by_id(env_id, config_manager)
|
||||
failures = result.get("failures") or []
|
||||
warning = ""
|
||||
if failures:
|
||||
first = failures[0]
|
||||
warning = (
|
||||
f" Внимание: {first.get('title') or first.get('id')}: "
|
||||
f"{first.get('error') or 'ошибка'}."
|
||||
)
|
||||
return (
|
||||
f"Сводка бэкапа: среда {env_name}, всего {total}, успешно {ok}, "
|
||||
f"с ошибками {failed}. {status}.{warning}"
|
||||
)
|
||||
|
||||
if plugin_id == "llm_dashboard_validation" and isinstance(result, dict):
|
||||
report_status = result.get("status") or status
|
||||
report_summary = result.get("summary") or "Итог недоступен."
|
||||
issues = result.get("issues") or []
|
||||
return f"Сводка валидации: статус {report_status}, проблем {len(issues)}. {report_summary}"
|
||||
|
||||
# Fallback for unknown task payloads.
|
||||
if status in {"SUCCESS", "FAILED"}:
|
||||
return f"Задача завершена со статусом {status}."
|
||||
return ""
|
||||
# [/DEF:_build_task_observability_summary:Function]
|
||||
|
||||
|
||||
# [DEF:_parse_command:Function]
|
||||
# @PURPOSE: Deterministically parse RU/EN command text into intent payload.
|
||||
# @PRE: message contains raw user text and config manager resolves environments.
|
||||
@@ -851,9 +982,9 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
{
|
||||
"operation": "execute_migration",
|
||||
"domain": "migration",
|
||||
"description": "Run dashboard migration (id/slug/title) between environments",
|
||||
"description": "Run dashboard migration (id/slug/title) between environments. Optional boolean flags: replace_db_config, fix_cross_filters",
|
||||
"required_entities": ["source_env", "target_env"],
|
||||
"optional_entities": ["dashboard_id", "dashboard_ref"],
|
||||
"optional_entities": ["dashboard_id", "dashboard_ref", "replace_db_config", "fix_cross_filters"],
|
||||
"risk_level": "guarded",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
@@ -1146,19 +1277,29 @@ async def _dispatch_intent(
|
||||
if not recent:
|
||||
return "У вас пока нет задач в истории.", None, []
|
||||
task = recent[0]
|
||||
actions = [AssistantAction(type="open_task", label="Open Task", target=task.id)]
|
||||
if str(task.status).upper() in {"SUCCESS", "FAILED"}:
|
||||
actions.extend(_extract_result_deep_links(task, config_manager))
|
||||
summary_line = _build_task_observability_summary(task, config_manager)
|
||||
return (
|
||||
f"Последняя задача: {task.id}, статус: {task.status}.",
|
||||
f"Последняя задача: {task.id}, статус: {task.status}."
|
||||
+ (f"\n{summary_line}" if summary_line else ""),
|
||||
task.id,
|
||||
[AssistantAction(type="open_task", label="Open Task", target=task.id)],
|
||||
actions,
|
||||
)
|
||||
|
||||
task = task_manager.get_task(task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
|
||||
actions = [AssistantAction(type="open_task", label="Open Task", target=task.id)]
|
||||
if str(task.status).upper() in {"SUCCESS", "FAILED"}:
|
||||
actions.extend(_extract_result_deep_links(task, config_manager))
|
||||
summary_line = _build_task_observability_summary(task, config_manager)
|
||||
return (
|
||||
f"Статус задачи {task.id}: {task.status}.",
|
||||
f"Статус задачи {task.id}: {task.status}."
|
||||
+ (f"\n{summary_line}" if summary_line else ""),
|
||||
task.id,
|
||||
[AssistantAction(type="open_task", label="Open Task", target=task.id)],
|
||||
actions,
|
||||
)
|
||||
|
||||
if operation == "create_branch":
|
||||
@@ -1208,20 +1349,30 @@ async def _dispatch_intent(
|
||||
if operation == "execute_migration":
|
||||
_check_any_permission(current_user, [("plugin:migration", "EXECUTE"), ("plugin:superset-migration", "EXECUTE")])
|
||||
src_token = entities.get("source_env")
|
||||
dashboard_ref = entities.get("dashboard_ref")
|
||||
dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)
|
||||
src = _resolve_env_id(src_token, config_manager)
|
||||
tgt = _resolve_env_id(entities.get("target_env"), config_manager)
|
||||
if not dashboard_id or not src or not tgt:
|
||||
raise HTTPException(status_code=422, detail="Missing dashboard_id/dashboard_ref/source_env/target_env")
|
||||
if not src or not tgt:
|
||||
raise HTTPException(status_code=422, detail="Missing source_env/target_env")
|
||||
if not dashboard_id and not dashboard_ref:
|
||||
raise HTTPException(status_code=422, detail="Missing dashboard_id/dashboard_ref")
|
||||
|
||||
migration_params: Dict[str, Any] = {
|
||||
"source_env_id": src,
|
||||
"target_env_id": tgt,
|
||||
"replace_db_config": _coerce_query_bool(entities.get("replace_db_config", False)),
|
||||
"fix_cross_filters": _coerce_query_bool(entities.get("fix_cross_filters", True)),
|
||||
}
|
||||
if dashboard_id:
|
||||
migration_params["selected_ids"] = [dashboard_id]
|
||||
else:
|
||||
# Fallback: pass dashboard_ref as regex for the migration plugin to match
|
||||
migration_params["dashboard_regex"] = str(dashboard_ref)
|
||||
|
||||
task = await task_manager.create_task(
|
||||
plugin_id="superset-migration",
|
||||
params={
|
||||
"selected_ids": [dashboard_id],
|
||||
"source_env_id": src,
|
||||
"target_env_id": tgt,
|
||||
"replace_db_config": False,
|
||||
},
|
||||
params=migration_params,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
return (
|
||||
@@ -1230,6 +1381,18 @@ async def _dispatch_intent(
|
||||
[
|
||||
AssistantAction(type="open_task", label="Open Task", target=task.id),
|
||||
AssistantAction(type="open_reports", label="Open Reports", target="/reports"),
|
||||
*(
|
||||
[
|
||||
AssistantAction(
|
||||
type="open_route",
|
||||
label=f"Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}",
|
||||
target=f"/dashboards/{dashboard_id}?env_id={tgt}",
|
||||
),
|
||||
AssistantAction(type="open_diff", label="Показать Diff", target=str(dashboard_id)),
|
||||
]
|
||||
if dashboard_id
|
||||
else []
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1258,6 +1421,18 @@ async def _dispatch_intent(
|
||||
[
|
||||
AssistantAction(type="open_task", label="Open Task", target=task.id),
|
||||
AssistantAction(type="open_reports", label="Open Reports", target="/reports"),
|
||||
*(
|
||||
[
|
||||
AssistantAction(
|
||||
type="open_route",
|
||||
label=f"Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}",
|
||||
target=f"/dashboards/{dashboard_id}?env_id={env_id}",
|
||||
),
|
||||
AssistantAction(type="open_diff", label="Показать Diff", target=str(dashboard_id)),
|
||||
]
|
||||
if entities.get("dashboard_id") or entities.get("dashboard_ref")
|
||||
else []
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -11,12 +11,16 @@
|
||||
# @INVARIANT: All dashboard responses include git_status and last_task metadata
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import List, Optional, Dict
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing import List, Optional, Dict, Any
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from pydantic import BaseModel, Field
|
||||
from ...dependencies import get_config_manager, get_task_manager, get_resource_service, get_mapping_service, has_permission
|
||||
from ...core.logger import logger, belief_scope
|
||||
from ...core.superset_client import SupersetClient
|
||||
from ...core.utils.network import DashboardNotFoundError
|
||||
# [/SECTION]
|
||||
|
||||
router = APIRouter(prefix="/api/dashboards", tags=["Dashboards"])
|
||||
@@ -24,7 +28,9 @@ router = APIRouter(prefix="/api/dashboards", tags=["Dashboards"])
|
||||
# [DEF:GitStatus:DataClass]
|
||||
class GitStatus(BaseModel):
|
||||
branch: Optional[str] = None
|
||||
sync_status: Optional[str] = Field(None, pattern="^OK|DIFF$")
|
||||
sync_status: Optional[str] = Field(None, pattern="^OK|DIFF|NO_REPO|ERROR$")
|
||||
has_repo: Optional[bool] = None
|
||||
has_changes_for_commit: Optional[bool] = None
|
||||
# [/DEF:GitStatus:DataClass]
|
||||
|
||||
# [DEF:LastTask:DataClass]
|
||||
@@ -88,6 +94,24 @@ class DashboardDetailResponse(BaseModel):
|
||||
dataset_count: int
|
||||
# [/DEF:DashboardDetailResponse:DataClass]
|
||||
|
||||
# [DEF:DashboardTaskHistoryItem:DataClass]
|
||||
class DashboardTaskHistoryItem(BaseModel):
|
||||
id: str
|
||||
plugin_id: str
|
||||
status: str
|
||||
validation_status: Optional[str] = None
|
||||
started_at: Optional[str] = None
|
||||
finished_at: Optional[str] = None
|
||||
env_id: Optional[str] = None
|
||||
summary: Optional[str] = None
|
||||
# [/DEF:DashboardTaskHistoryItem:DataClass]
|
||||
|
||||
# [DEF:DashboardTaskHistoryResponse:DataClass]
|
||||
class DashboardTaskHistoryResponse(BaseModel):
|
||||
dashboard_id: int
|
||||
items: List[DashboardTaskHistoryItem]
|
||||
# [/DEF:DashboardTaskHistoryResponse:DataClass]
|
||||
|
||||
# [DEF:DatabaseMapping:DataClass]
|
||||
class DatabaseMapping(BaseModel):
|
||||
source_db: str
|
||||
@@ -257,6 +281,190 @@ async def get_dashboard_detail(
|
||||
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboard detail: {str(e)}")
|
||||
# [/DEF:get_dashboard_detail:Function]
|
||||
|
||||
|
||||
# [DEF:_task_matches_dashboard:Function]
|
||||
# @PURPOSE: Checks whether task params are tied to a specific dashboard and environment.
|
||||
# @PRE: task-like object exposes plugin_id and params fields.
|
||||
# @POST: Returns True only for supported task plugins tied to dashboard_id (+optional env_id).
|
||||
def _task_matches_dashboard(task: Any, dashboard_id: int, env_id: Optional[str]) -> bool:
|
||||
plugin_id = getattr(task, "plugin_id", None)
|
||||
if plugin_id not in {"superset-backup", "llm_dashboard_validation"}:
|
||||
return False
|
||||
|
||||
params = getattr(task, "params", {}) or {}
|
||||
dashboard_id_str = str(dashboard_id)
|
||||
|
||||
if plugin_id == "llm_dashboard_validation":
|
||||
task_dashboard_id = params.get("dashboard_id")
|
||||
if str(task_dashboard_id) != dashboard_id_str:
|
||||
return False
|
||||
if env_id:
|
||||
task_env = params.get("environment_id")
|
||||
return str(task_env) == str(env_id)
|
||||
return True
|
||||
|
||||
# superset-backup can pass dashboards as "dashboard_ids" or "dashboards"
|
||||
dashboard_ids = params.get("dashboard_ids") or params.get("dashboards") or []
|
||||
normalized_ids = {str(item) for item in dashboard_ids}
|
||||
if dashboard_id_str not in normalized_ids:
|
||||
return False
|
||||
if env_id:
|
||||
task_env = params.get("environment_id") or params.get("env")
|
||||
return str(task_env) == str(env_id)
|
||||
return True
|
||||
# [/DEF:_task_matches_dashboard:Function]
|
||||
|
||||
|
||||
# [DEF:get_dashboard_tasks_history:Function]
|
||||
# @PURPOSE: Returns history of backup and LLM validation tasks for a dashboard.
|
||||
# @PRE: dashboard_id is valid integer.
|
||||
# @POST: Response contains sorted task history (newest first).
|
||||
@router.get("/{dashboard_id:int}/tasks", response_model=DashboardTaskHistoryResponse)
|
||||
async def get_dashboard_tasks_history(
|
||||
dashboard_id: int,
|
||||
env_id: Optional[str] = None,
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
task_manager=Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "READ"))
|
||||
):
|
||||
with belief_scope("get_dashboard_tasks_history", f"dashboard_id={dashboard_id}, env_id={env_id}, limit={limit}"):
|
||||
matching_tasks = []
|
||||
for task in task_manager.get_all_tasks():
|
||||
if _task_matches_dashboard(task, dashboard_id, env_id):
|
||||
matching_tasks.append(task)
|
||||
|
||||
def _sort_key(task_obj: Any) -> str:
|
||||
return (
|
||||
str(getattr(task_obj, "started_at", "") or "")
|
||||
or str(getattr(task_obj, "finished_at", "") or "")
|
||||
)
|
||||
|
||||
matching_tasks.sort(key=_sort_key, reverse=True)
|
||||
selected = matching_tasks[:limit]
|
||||
|
||||
items = []
|
||||
for task in selected:
|
||||
result = getattr(task, "result", None)
|
||||
summary = None
|
||||
validation_status = None
|
||||
if isinstance(result, dict):
|
||||
raw_validation_status = result.get("status")
|
||||
if raw_validation_status is not None:
|
||||
validation_status = str(raw_validation_status)
|
||||
summary = (
|
||||
result.get("summary")
|
||||
or result.get("status")
|
||||
or result.get("message")
|
||||
)
|
||||
params = getattr(task, "params", {}) or {}
|
||||
items.append(
|
||||
DashboardTaskHistoryItem(
|
||||
id=str(getattr(task, "id", "")),
|
||||
plugin_id=str(getattr(task, "plugin_id", "")),
|
||||
status=str(getattr(task, "status", "")),
|
||||
validation_status=validation_status,
|
||||
started_at=getattr(task, "started_at", None).isoformat() if getattr(task, "started_at", None) else None,
|
||||
finished_at=getattr(task, "finished_at", None).isoformat() if getattr(task, "finished_at", None) else None,
|
||||
env_id=str(params.get("environment_id") or params.get("env")) if (params.get("environment_id") or params.get("env")) else None,
|
||||
summary=summary,
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(f"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard {dashboard_id}")
|
||||
return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)
|
||||
# [/DEF:get_dashboard_tasks_history:Function]
|
||||
|
||||
|
||||
# [DEF:get_dashboard_thumbnail:Function]
|
||||
# @PURPOSE: Proxies Superset dashboard thumbnail with cache support.
|
||||
# @PRE: env_id must exist.
|
||||
# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.
|
||||
@router.get("/{dashboard_id:int}/thumbnail")
|
||||
async def get_dashboard_thumbnail(
|
||||
dashboard_id: int,
|
||||
env_id: str,
|
||||
force: bool = Query(False),
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("plugin:migration", "READ"))
|
||||
):
|
||||
with belief_scope("get_dashboard_thumbnail", f"dashboard_id={dashboard_id}, env_id={env_id}, force={force}"):
|
||||
environments = config_manager.get_environments()
|
||||
env = next((e for e in environments if e.id == env_id), None)
|
||||
if not env:
|
||||
logger.error(f"[get_dashboard_thumbnail][Coherence:Failed] Environment not found: {env_id}")
|
||||
raise HTTPException(status_code=404, detail="Environment not found")
|
||||
|
||||
try:
|
||||
client = SupersetClient(env)
|
||||
digest = None
|
||||
thumb_endpoint = None
|
||||
|
||||
# Preferred flow (newer Superset): ask server to cache screenshot and return digest/image_url.
|
||||
try:
|
||||
screenshot_payload = client.network.request(
|
||||
method="POST",
|
||||
endpoint=f"/dashboard/{dashboard_id}/cache_dashboard_screenshot/",
|
||||
json={"force": force},
|
||||
)
|
||||
payload = screenshot_payload.get("result", screenshot_payload) if isinstance(screenshot_payload, dict) else {}
|
||||
image_url = payload.get("image_url", "") if isinstance(payload, dict) else ""
|
||||
if isinstance(image_url, str) and image_url:
|
||||
matched = re.search(r"/dashboard/\d+/(?:thumbnail|screenshot)/([^/]+)/?$", image_url)
|
||||
if matched:
|
||||
digest = matched.group(1)
|
||||
except DashboardNotFoundError:
|
||||
logger.warning(
|
||||
"[get_dashboard_thumbnail][Fallback] cache_dashboard_screenshot endpoint unavailable, fallback to dashboard.thumbnail_url"
|
||||
)
|
||||
|
||||
# Fallback flow (older Superset): read thumbnail_url from dashboard payload.
|
||||
if not digest:
|
||||
dashboard_payload = client.network.request(
|
||||
method="GET",
|
||||
endpoint=f"/dashboard/{dashboard_id}",
|
||||
)
|
||||
dashboard_data = dashboard_payload.get("result", dashboard_payload) if isinstance(dashboard_payload, dict) else {}
|
||||
thumbnail_url = dashboard_data.get("thumbnail_url", "") if isinstance(dashboard_data, dict) else ""
|
||||
if isinstance(thumbnail_url, str) and thumbnail_url:
|
||||
parsed = urlparse(thumbnail_url)
|
||||
parsed_path = parsed.path or thumbnail_url
|
||||
if parsed_path.startswith("/api/v1/"):
|
||||
parsed_path = parsed_path[len("/api/v1"):]
|
||||
thumb_endpoint = parsed_path
|
||||
matched = re.search(r"/dashboard/\d+/(?:thumbnail|screenshot)/([^/]+)/?$", parsed_path)
|
||||
if matched:
|
||||
digest = matched.group(1)
|
||||
|
||||
if not thumb_endpoint:
|
||||
thumb_endpoint = f"/dashboard/{dashboard_id}/thumbnail/{digest or 'latest'}/"
|
||||
|
||||
thumb_response = client.network.request(
|
||||
method="GET",
|
||||
endpoint=thumb_endpoint,
|
||||
raw_response=True,
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
if thumb_response.status_code == 202:
|
||||
payload_202: Dict[str, Any] = {}
|
||||
try:
|
||||
payload_202 = thumb_response.json()
|
||||
except Exception:
|
||||
payload_202 = {"message": "Thumbnail is being generated"}
|
||||
return JSONResponse(status_code=202, content=payload_202)
|
||||
|
||||
content_type = thumb_response.headers.get("Content-Type", "image/png")
|
||||
return Response(content=thumb_response.content, media_type=content_type)
|
||||
except DashboardNotFoundError as e:
|
||||
logger.error(f"[get_dashboard_thumbnail][Coherence:Failed] Dashboard not found for thumbnail: {e}")
|
||||
raise HTTPException(status_code=404, detail="Dashboard thumbnail not found")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[get_dashboard_thumbnail][Coherence:Failed] Failed to fetch dashboard thumbnail: {e}")
|
||||
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboard thumbnail: {str(e)}")
|
||||
# [/DEF:get_dashboard_thumbnail:Function]
|
||||
|
||||
# [DEF:MigrateRequest:DataClass]
|
||||
class MigrateRequest(BaseModel):
|
||||
source_env_id: str = Field(..., description="Source environment ID")
|
||||
|
||||
@@ -31,6 +31,7 @@ class EnvironmentResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
url: str
|
||||
is_production: bool = False
|
||||
backup_schedule: Optional[ScheduleSchema] = None
|
||||
# [/DEF:EnvironmentResponse:DataClass]
|
||||
|
||||
@@ -63,6 +64,7 @@ async def get_environments(
|
||||
id=e.id,
|
||||
name=e.name,
|
||||
url=e.url,
|
||||
is_production=getattr(e, "is_production", False),
|
||||
backup_schedule=ScheduleSchema(
|
||||
enabled=e.backup_schedule.enabled,
|
||||
cron_expression=e.backup_schedule.cron_expression
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# @LAYER: UI (API)
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
from ...core.logger import logger
|
||||
from ...schemas.auth import User
|
||||
from ...dependencies import get_current_user as get_current_active_user
|
||||
@@ -19,6 +19,20 @@ from sqlalchemy.orm import Session
|
||||
router = APIRouter(tags=["LLM"])
|
||||
# [/DEF:router:Global]
|
||||
|
||||
|
||||
# [DEF:_is_valid_runtime_api_key:Function]
|
||||
# @PURPOSE: Validate decrypted runtime API key presence/shape.
|
||||
# @PRE: value can be None.
|
||||
# @POST: Returns True only for non-placeholder key.
|
||||
def _is_valid_runtime_api_key(value: Optional[str]) -> bool:
|
||||
key = (value or "").strip()
|
||||
if not key:
|
||||
return False
|
||||
if key in {"********", "EMPTY_OR_NONE"}:
|
||||
return False
|
||||
return len(key) >= 16
|
||||
# [/DEF:_is_valid_runtime_api_key:Function]
|
||||
|
||||
# [DEF:get_providers:Function]
|
||||
# @PURPOSE: Retrieve all LLM provider configurations.
|
||||
# @PRE: User is authenticated.
|
||||
@@ -47,6 +61,37 @@ async def get_providers(
|
||||
]
|
||||
# [/DEF:get_providers:Function]
|
||||
|
||||
|
||||
# [DEF:get_llm_status:Function]
|
||||
# @PURPOSE: Returns whether LLM runtime is configured for dashboard validation.
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: configured=true only when an active provider with valid decrypted key exists.
|
||||
@router.get("/status")
|
||||
async def get_llm_status(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
service = LLMProviderService(db)
|
||||
providers = service.get_all_providers()
|
||||
active_provider = next((p for p in providers if p.is_active), None)
|
||||
|
||||
if not active_provider:
|
||||
return {"configured": False, "reason": "no_active_provider"}
|
||||
|
||||
api_key = service.get_decrypted_api_key(active_provider.id)
|
||||
if not _is_valid_runtime_api_key(api_key):
|
||||
return {"configured": False, "reason": "invalid_api_key"}
|
||||
|
||||
return {
|
||||
"configured": True,
|
||||
"reason": "ok",
|
||||
"provider_id": active_provider.id,
|
||||
"provider_name": active_provider.name,
|
||||
"provider_type": active_provider.provider_type,
|
||||
"default_model": active_provider.default_model,
|
||||
}
|
||||
# [/DEF:get_llm_status:Function]
|
||||
|
||||
# [DEF:create_provider:Function]
|
||||
# @PURPOSE: Create a new LLM provider configuration.
|
||||
# @PRE: User is authenticated and has admin permissions.
|
||||
@@ -204,4 +249,4 @@ async def test_provider_config(
|
||||
return {"success": False, "error": str(e)}
|
||||
# [/DEF:test_provider_config:Function]
|
||||
|
||||
# [/DEF:backend/src/api/routes/llm.py]
|
||||
# [/DEF:backend/src/api/routes/llm.py]
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
# @RELATION: DEPENDS_ON -> backend.src.dependencies
|
||||
# @RELATION: DEPENDS_ON -> backend.src.models.dashboard
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from ...dependencies import get_config_manager, get_task_manager, has_permission
|
||||
from ...core.database import get_db
|
||||
from ...models.dashboard import DashboardMetadata, DashboardSelection
|
||||
from ...core.superset_client import SupersetClient
|
||||
from ...core.logger import belief_scope
|
||||
from ...core.mapping_service import IdMappingService
|
||||
from ...models.mapping import ResourceMapping
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["migration"])
|
||||
|
||||
@@ -44,7 +48,7 @@ async def get_dashboards(
|
||||
# @POST: Starts the migration task and returns the task ID.
|
||||
# @PARAM: selection (DashboardSelection) - The dashboards to migrate.
|
||||
# @RETURN: Dict - {"task_id": str, "message": str}
|
||||
@router.post("/execute")
|
||||
@router.post("/migration/execute")
|
||||
async def execute_migration(
|
||||
selection: DashboardSelection,
|
||||
config_manager=Depends(get_config_manager),
|
||||
@@ -61,9 +65,10 @@ async def execute_migration(
|
||||
# Create migration task with debug logging
|
||||
from ...core.logger import logger
|
||||
|
||||
# Include replace_db_config in the task parameters
|
||||
# Include replace_db_config and fix_cross_filters in the task parameters
|
||||
task_params = selection.dict()
|
||||
task_params['replace_db_config'] = selection.replace_db_config
|
||||
task_params['fix_cross_filters'] = selection.fix_cross_filters
|
||||
|
||||
logger.info(f"Creating migration task with params: {task_params}")
|
||||
logger.info(f"Available environments: {env_ids}")
|
||||
@@ -78,4 +83,142 @@ async def execute_migration(
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create migration task: {str(e)}")
|
||||
# [/DEF:execute_migration:Function]
|
||||
|
||||
# [DEF:get_migration_settings:Function]
|
||||
# @PURPOSE: Get current migration Cron string explicitly.
|
||||
@router.get("/migration/settings", response_model=Dict[str, str])
|
||||
async def get_migration_settings(
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("plugin:migration", "READ"))
|
||||
):
|
||||
with belief_scope("get_migration_settings"):
|
||||
config = config_manager.get_config()
|
||||
cron = config.settings.migration_sync_cron
|
||||
return {"cron": cron}
|
||||
# [/DEF:get_migration_settings:Function]
|
||||
|
||||
# [DEF:update_migration_settings:Function]
|
||||
# @PURPOSE: Update migration Cron string.
|
||||
@router.put("/migration/settings", response_model=Dict[str, str])
|
||||
async def update_migration_settings(
|
||||
payload: Dict[str, str],
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("plugin:migration", "WRITE"))
|
||||
):
|
||||
with belief_scope("update_migration_settings"):
|
||||
if "cron" not in payload:
|
||||
raise HTTPException(status_code=400, detail="Missing 'cron' field in payload")
|
||||
|
||||
cron_expr = payload["cron"]
|
||||
|
||||
config = config_manager.get_config()
|
||||
config.settings.migration_sync_cron = cron_expr
|
||||
config_manager.save_config(config)
|
||||
|
||||
return {"cron": cron_expr, "status": "updated"}
|
||||
# [/DEF:update_migration_settings:Function]
|
||||
|
||||
# [DEF:get_resource_mappings:Function]
|
||||
# @PURPOSE: Fetch synchronized object mappings with search, filtering, and pagination.
|
||||
@router.get("/migration/mappings-data", response_model=Dict[str, Any])
|
||||
async def get_resource_mappings(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
search: Optional[str] = Query(None, description="Search by resource name or UUID"),
|
||||
env_id: Optional[str] = Query(None, description="Filter by environment ID"),
|
||||
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
|
||||
db: Session = Depends(get_db),
|
||||
_ = Depends(has_permission("plugin:migration", "READ"))
|
||||
):
|
||||
with belief_scope("get_resource_mappings"):
|
||||
query = db.query(ResourceMapping)
|
||||
|
||||
if env_id:
|
||||
query = query.filter(ResourceMapping.environment_id == env_id)
|
||||
|
||||
if resource_type:
|
||||
query = query.filter(ResourceMapping.resource_type == resource_type.upper())
|
||||
|
||||
if search:
|
||||
search_term = f"%{search}%"
|
||||
query = query.filter(
|
||||
(ResourceMapping.resource_name.ilike(search_term)) |
|
||||
(ResourceMapping.uuid.ilike(search_term))
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
mappings = query.order_by(ResourceMapping.resource_type, ResourceMapping.resource_name).offset(skip).limit(limit).all()
|
||||
|
||||
items = []
|
||||
for m in mappings:
|
||||
items.append({
|
||||
"id": m.id,
|
||||
"environment_id": m.environment_id,
|
||||
"resource_type": m.resource_type.value if m.resource_type else None,
|
||||
"uuid": m.uuid,
|
||||
"remote_id": m.remote_integer_id,
|
||||
"resource_name": m.resource_name,
|
||||
"last_synced_at": m.last_synced_at.isoformat() if m.last_synced_at else None
|
||||
})
|
||||
|
||||
return {"items": items, "total": total}
|
||||
# [/DEF:get_resource_mappings:Function]
|
||||
|
||||
# [DEF:trigger_sync_now:Function]
|
||||
# @PURPOSE: Triggers an immediate ID synchronization for all environments.
|
||||
# @PRE: At least one environment must be configured.
|
||||
# @POST: Environment rows are ensured in DB; sync_environment is called for each.
|
||||
@router.post("/migration/sync-now", response_model=Dict[str, Any])
|
||||
async def trigger_sync_now(
|
||||
config_manager=Depends(get_config_manager),
|
||||
db: Session = Depends(get_db),
|
||||
_ = Depends(has_permission("plugin:migration", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("trigger_sync_now"):
|
||||
from ...core.logger import logger
|
||||
from ...models.mapping import Environment as EnvironmentModel
|
||||
|
||||
config = config_manager.get_config()
|
||||
environments = config.environments
|
||||
|
||||
if not environments:
|
||||
raise HTTPException(status_code=400, detail="No environments configured")
|
||||
|
||||
# Ensure each environment exists in DB (upsert) to satisfy FK constraints
|
||||
for env in environments:
|
||||
existing = db.query(EnvironmentModel).filter_by(id=env.id).first()
|
||||
if not existing:
|
||||
db_env = EnvironmentModel(
|
||||
id=env.id,
|
||||
name=env.name,
|
||||
url=env.url,
|
||||
credentials_id=env.id, # Use env.id as credentials reference
|
||||
)
|
||||
db.add(db_env)
|
||||
logger.info(f"[trigger_sync_now][Action] Created environment row for {env.id}")
|
||||
else:
|
||||
existing.name = env.name
|
||||
existing.url = env.url
|
||||
db.commit()
|
||||
|
||||
service = IdMappingService(db)
|
||||
results = {"synced": [], "failed": []}
|
||||
|
||||
for env in environments:
|
||||
try:
|
||||
client = SupersetClient(env)
|
||||
service.sync_environment(env.id, client)
|
||||
results["synced"].append(env.id)
|
||||
logger.info(f"[trigger_sync_now][Action] Synced environment {env.id}")
|
||||
except Exception as e:
|
||||
results["failed"].append({"env_id": env.id, "error": str(e)})
|
||||
logger.error(f"[trigger_sync_now][Error] Failed to sync {env.id}: {e}")
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"synced_count": len(results["synced"]),
|
||||
"failed_count": len(results["failed"]),
|
||||
"details": results
|
||||
}
|
||||
# [/DEF:trigger_sync_now:Function]
|
||||
|
||||
# [/DEF:backend.src.api.routes.migration:Module]
|
||||
@@ -36,6 +36,7 @@ router = APIRouter(tags=["storage"])
|
||||
async def list_files(
|
||||
category: Optional[FileCategory] = None,
|
||||
path: Optional[str] = None,
|
||||
recursive: bool = False,
|
||||
plugin_loader=Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("plugin:storage", "READ"))
|
||||
):
|
||||
@@ -43,7 +44,7 @@ async def list_files(
|
||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||
if not storage_plugin:
|
||||
raise HTTPException(status_code=500, detail="Storage plugin not loaded")
|
||||
return storage_plugin.list_files(category, path)
|
||||
return storage_plugin.list_files(category, path, recursive)
|
||||
# [/DEF:list_files:Function]
|
||||
|
||||
# [DEF:upload_file:Function]
|
||||
@@ -143,4 +144,46 @@ async def download_file(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
# [/DEF:download_file:Function]
|
||||
|
||||
# [/DEF:storage_routes:Module]
|
||||
# [DEF:get_file_by_path:Function]
|
||||
# @PURPOSE: Retrieve a file by validated absolute/relative path under storage root.
|
||||
#
|
||||
# @PRE: path must resolve under configured storage root.
|
||||
# @POST: Returns a FileResponse for existing files.
|
||||
#
|
||||
# @PARAM: path (str) - Absolute or storage-root-relative file path.
|
||||
# @RETURN: FileResponse - The file content.
|
||||
#
|
||||
# @RELATION: CALLS -> StoragePlugin.get_storage_root
|
||||
# @RELATION: CALLS -> StoragePlugin.validate_path
|
||||
@router.get("/file")
|
||||
async def get_file_by_path(
|
||||
path: str,
|
||||
plugin_loader=Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("plugin:storage", "READ"))
|
||||
):
|
||||
with belief_scope("get_file_by_path"):
|
||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||
if not storage_plugin:
|
||||
raise HTTPException(status_code=500, detail="Storage plugin not loaded")
|
||||
|
||||
requested_path = (path or "").strip()
|
||||
if not requested_path:
|
||||
raise HTTPException(status_code=400, detail="Path is required")
|
||||
|
||||
try:
|
||||
candidate = Path(requested_path)
|
||||
if candidate.is_absolute():
|
||||
abs_path = storage_plugin.validate_path(candidate)
|
||||
else:
|
||||
storage_root = storage_plugin.get_storage_root()
|
||||
abs_path = storage_plugin.validate_path(storage_root / candidate)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
if not abs_path.exists() or not abs_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
return FileResponse(path=str(abs_path), filename=abs_path.name)
|
||||
# [/DEF:get_file_by_path:Function]
|
||||
|
||||
# [/DEF:storage_routes:Module]
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
# @SEMANTICS: config, models, pydantic
|
||||
# @PURPOSE: Defines the data models for application configuration using Pydantic.
|
||||
# @LAYER: Core
|
||||
# @RELATION: READS_FROM -> app_configurations (database)
|
||||
# @RELATION: READS_FROM -> app_configurations (database)
|
||||
# @RELATION: USED_BY -> ConfigManager
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from ..models.storage import StorageConfig
|
||||
from ..services.llm_prompt_templates import (
|
||||
DEFAULT_LLM_ASSISTANT_SETTINGS,
|
||||
DEFAULT_LLM_PROMPTS,
|
||||
DEFAULT_LLM_PROVIDER_BINDINGS,
|
||||
)
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from ..models.storage import StorageConfig
|
||||
from ..services.llm_prompt_templates import (
|
||||
DEFAULT_LLM_ASSISTANT_SETTINGS,
|
||||
DEFAULT_LLM_PROMPTS,
|
||||
DEFAULT_LLM_PROVIDER_BINDINGS,
|
||||
)
|
||||
|
||||
# [DEF:Schedule:DataClass]
|
||||
# @PURPOSE: Represents a backup schedule configuration.
|
||||
@@ -24,24 +24,25 @@ class Schedule(BaseModel):
|
||||
|
||||
# [DEF:Environment:DataClass]
|
||||
# @PURPOSE: Represents a Superset environment configuration.
|
||||
class Environment(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
url: str
|
||||
username: str
|
||||
password: str # Will be masked in UI
|
||||
verify_ssl: bool = True
|
||||
timeout: int = 30
|
||||
is_default: bool = False
|
||||
backup_schedule: Schedule = Field(default_factory=Schedule)
|
||||
# [/DEF:Environment:DataClass]
|
||||
class Environment(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
url: str
|
||||
username: str
|
||||
password: str # Will be masked in UI
|
||||
verify_ssl: bool = True
|
||||
timeout: int = 30
|
||||
is_default: bool = False
|
||||
is_production: bool = False
|
||||
backup_schedule: Schedule = Field(default_factory=Schedule)
|
||||
# [/DEF:Environment:DataClass]
|
||||
|
||||
# [DEF:LoggingConfig:DataClass]
|
||||
# @PURPOSE: Defines the configuration for the application's logging system.
|
||||
class LoggingConfig(BaseModel):
|
||||
level: str = "INFO"
|
||||
task_log_level: str = "INFO" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)
|
||||
file_path: Optional[str] = None
|
||||
class LoggingConfig(BaseModel):
|
||||
level: str = "INFO"
|
||||
task_log_level: str = "INFO" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)
|
||||
file_path: Optional[str] = None
|
||||
max_bytes: int = 10 * 1024 * 1024
|
||||
backup_count: int = 5
|
||||
enable_belief_state: bool = True
|
||||
@@ -49,25 +50,28 @@ class LoggingConfig(BaseModel):
|
||||
|
||||
# [DEF:GlobalSettings:DataClass]
|
||||
# @PURPOSE: Represents global application settings.
|
||||
class GlobalSettings(BaseModel):
|
||||
class GlobalSettings(BaseModel):
|
||||
storage: StorageConfig = Field(default_factory=StorageConfig)
|
||||
default_environment_id: Optional[str] = None
|
||||
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
||||
connections: List[dict] = []
|
||||
llm: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"providers": [],
|
||||
"default_provider": "",
|
||||
"prompts": dict(DEFAULT_LLM_PROMPTS),
|
||||
"provider_bindings": dict(DEFAULT_LLM_PROVIDER_BINDINGS),
|
||||
**dict(DEFAULT_LLM_ASSISTANT_SETTINGS),
|
||||
}
|
||||
)
|
||||
llm: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"providers": [],
|
||||
"default_provider": "",
|
||||
"prompts": dict(DEFAULT_LLM_PROMPTS),
|
||||
"provider_bindings": dict(DEFAULT_LLM_PROVIDER_BINDINGS),
|
||||
**dict(DEFAULT_LLM_ASSISTANT_SETTINGS),
|
||||
}
|
||||
)
|
||||
|
||||
# Task retention settings
|
||||
task_retention_days: int = 30
|
||||
task_retention_limit: int = 100
|
||||
pagination_limit: int = 10
|
||||
|
||||
# Migration sync settings
|
||||
migration_sync_cron: str = "0 2 * * *"
|
||||
# [/DEF:GlobalSettings:DataClass]
|
||||
|
||||
# [DEF:AppConfig:DataClass]
|
||||
|
||||
234
backend/src/core/mapping_service.py
Normal file
234
backend/src/core/mapping_service.py
Normal file
@@ -0,0 +1,234 @@
|
||||
# [DEF:backend.src.core.mapping_service:Module]
|
||||
#
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: mapping, ids, synchronization, environments, cross-filters
|
||||
# @PURPOSE: Service for tracking and synchronizing Superset Resource IDs (UUID <-> Integer ID)
|
||||
# @LAYER: Core
|
||||
# @RELATION: DEPENDS_ON -> backend.src.models.mapping (ResourceMapping, ResourceType)
|
||||
# @RELATION: DEPENDS_ON -> backend.src.core.logger
|
||||
# @TEST_DATA: mock_superset_resources -> {'chart': [{'id': 42, 'uuid': '1234', 'slice_name': 'test'}], 'dataset': [{'id': 99, 'uuid': '5678', 'table_name': 'data'}]}
|
||||
#
|
||||
# @INVARIANT: sync_environment must handle remote API failures gracefully.
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import Session
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from src.models.mapping import ResourceMapping, ResourceType
|
||||
from src.core.logger import logger, belief_scope
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:IdMappingService:Class]
|
||||
# @TIER: CRITICAL
|
||||
# @PURPOSE: Service handling the cataloging and retrieval of remote Superset Integer IDs.
|
||||
class IdMappingService:
|
||||
|
||||
# [DEF:__init__:Function]
|
||||
# @PURPOSE: Initializes the mapping service.
|
||||
def __init__(self, db_session: Session):
|
||||
self.db = db_session
|
||||
self.scheduler = BackgroundScheduler()
|
||||
self._sync_job = None
|
||||
# [/DEF:__init__:Function]
|
||||
|
||||
# [DEF:start_scheduler:Function]
|
||||
# @PURPOSE: Starts the background scheduler with a given cron string.
|
||||
# @PARAM: cron_string (str) - Cron expression for the sync interval.
|
||||
# @PARAM: environments (List[str]) - List of environment IDs to sync.
|
||||
# @PARAM: superset_client_factory - Function to get a client for an environment.
|
||||
def start_scheduler(self, cron_string: str, environments: List[str], superset_client_factory):
|
||||
with belief_scope("IdMappingService.start_scheduler"):
|
||||
if self._sync_job:
|
||||
self.scheduler.remove_job(self._sync_job.id)
|
||||
logger.info("[IdMappingService.start_scheduler][Reflect] Removed existing sync job.")
|
||||
|
||||
def sync_all():
|
||||
for env_id in environments:
|
||||
client = superset_client_factory(env_id)
|
||||
if client:
|
||||
self.sync_environment(env_id, client)
|
||||
|
||||
self._sync_job = self.scheduler.add_job(
|
||||
sync_all,
|
||||
CronTrigger.from_crontab(cron_string),
|
||||
id='id_mapping_sync_job',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
if not self.scheduler.running:
|
||||
self.scheduler.start()
|
||||
logger.info(f"[IdMappingService.start_scheduler][Coherence:OK] Started background scheduler with cron: {cron_string}")
|
||||
else:
|
||||
logger.info(f"[IdMappingService.start_scheduler][Coherence:OK] Updated background scheduler with cron: {cron_string}")
|
||||
# [/DEF:start_scheduler:Function]
|
||||
|
||||
# [DEF:sync_environment:Function]
|
||||
# @PURPOSE: Fully synchronizes mapping for a specific environment.
|
||||
# @PARAM: environment_id (str) - Target environment ID.
|
||||
# @PARAM: superset_client - Instance capable of hitting the Superset API.
|
||||
# @PRE: environment_id exists in the database.
|
||||
# @POST: ResourceMapping records for the environment are created or updated.
|
||||
def sync_environment(self, environment_id: str, superset_client, incremental: bool = False) -> None:
|
||||
"""
|
||||
Polls the Superset APIs for the target environment and updates the local mapping table.
|
||||
If incremental=True, only fetches items changed since the max last_synced_at date.
|
||||
"""
|
||||
with belief_scope("IdMappingService.sync_environment"):
|
||||
logger.info(f"[IdMappingService.sync_environment][Action] Starting sync for environment {environment_id} (incremental={incremental})")
|
||||
|
||||
# Implementation Note: In a real scenario, superset_client needs to be an instance
|
||||
# capable of auth & iteration over /api/v1/chart/, /api/v1/dataset/, /api/v1/dashboard/
|
||||
# Here we structure the logic according to the spec.
|
||||
|
||||
types_to_poll = [
|
||||
(ResourceType.CHART, "chart", "slice_name"),
|
||||
(ResourceType.DATASET, "dataset", "table_name"),
|
||||
(ResourceType.DASHBOARD, "dashboard", "slug") # Note: dashboard slug or dashboard_title
|
||||
]
|
||||
|
||||
total_synced = 0
|
||||
total_deleted = 0
|
||||
try:
|
||||
for res_enum, endpoint, name_field in types_to_poll:
|
||||
logger.debug(f"[IdMappingService.sync_environment][Explore] Polling {endpoint} endpoint")
|
||||
|
||||
# Simulated API Fetch (Would be: superset_client.get(f"/api/v1/{endpoint}/")... )
|
||||
# This relies on the superset API structure, e.g. { "result": [{"id": 1, "uuid": "...", name_field: "..."}] }
|
||||
# We assume superset_client provides a generic method to fetch all pages.
|
||||
|
||||
try:
|
||||
since_dttm = None
|
||||
if incremental:
|
||||
from sqlalchemy.sql import func
|
||||
max_date = self.db.query(func.max(ResourceMapping.last_synced_at)).filter(
|
||||
ResourceMapping.environment_id == environment_id,
|
||||
ResourceMapping.resource_type == res_enum
|
||||
).scalar()
|
||||
|
||||
if max_date:
|
||||
# We subtract a bit for safety overlap
|
||||
from datetime import timedelta
|
||||
since_dttm = max_date - timedelta(minutes=5)
|
||||
logger.debug(f"[IdMappingService.sync_environment] Incremental sync since {since_dttm}")
|
||||
|
||||
resources = superset_client.get_all_resources(endpoint, since_dttm=since_dttm)
|
||||
|
||||
# Track which UUIDs we see in this sync cycle
|
||||
synced_uuids = set()
|
||||
|
||||
for res in resources:
|
||||
res_uuid = res.get("uuid")
|
||||
raw_id = res.get("id")
|
||||
res_name = res.get(name_field)
|
||||
|
||||
if not res_uuid or raw_id is None:
|
||||
continue
|
||||
|
||||
synced_uuids.add(res_uuid)
|
||||
res_id = str(raw_id) # Store as string
|
||||
|
||||
# Upsert Logic
|
||||
mapping = self.db.query(ResourceMapping).filter_by(
|
||||
environment_id=environment_id,
|
||||
resource_type=res_enum,
|
||||
uuid=res_uuid
|
||||
).first()
|
||||
|
||||
if mapping:
|
||||
mapping.remote_integer_id = res_id
|
||||
mapping.resource_name = res_name
|
||||
mapping.last_synced_at = datetime.now(timezone.utc)
|
||||
else:
|
||||
new_mapping = ResourceMapping(
|
||||
environment_id=environment_id,
|
||||
resource_type=res_enum,
|
||||
uuid=res_uuid,
|
||||
remote_integer_id=res_id,
|
||||
resource_name=res_name,
|
||||
last_synced_at=datetime.now(timezone.utc)
|
||||
)
|
||||
self.db.add(new_mapping)
|
||||
|
||||
total_synced += 1
|
||||
|
||||
# Delete stale mappings: rows for this env+type whose UUID
|
||||
# was NOT returned by the API (resource was deleted remotely)
|
||||
# We only do this on full syncs, because incremental syncs don't return all UUIDs
|
||||
if not incremental:
|
||||
stale_query = self.db.query(ResourceMapping).filter(
|
||||
ResourceMapping.environment_id == environment_id,
|
||||
ResourceMapping.resource_type == res_enum,
|
||||
)
|
||||
if synced_uuids:
|
||||
stale_query = stale_query.filter(
|
||||
ResourceMapping.uuid.notin_(synced_uuids)
|
||||
)
|
||||
deleted = stale_query.delete(synchronize_session="fetch")
|
||||
if deleted:
|
||||
total_deleted += deleted
|
||||
logger.info(f"[IdMappingService.sync_environment][Action] Removed {deleted} stale {endpoint} mapping(s) for {environment_id}")
|
||||
|
||||
except Exception as loop_e:
|
||||
logger.error(f"[IdMappingService.sync_environment][Reason] Error polling {endpoint}: {loop_e}")
|
||||
# Continue to next resource type instead of blowing up the whole sync
|
||||
|
||||
self.db.commit()
|
||||
logger.info(f"[IdMappingService.sync_environment][Coherence:OK] Successfully synced {total_synced} items and deleted {total_deleted} stale items.")
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"[IdMappingService.sync_environment][Coherence:Failed] Critical sync failure: {e}")
|
||||
raise
|
||||
# [/DEF:sync_environment:Function]
|
||||
|
||||
# [DEF:get_remote_id:Function]
|
||||
# @PURPOSE: Retrieves the remote integer ID for a given universal UUID.
|
||||
# @PARAM: environment_id (str)
|
||||
# @PARAM: resource_type (ResourceType)
|
||||
# @PARAM: uuid (str)
|
||||
# @RETURN: Optional[int]
|
||||
def get_remote_id(self, environment_id: str, resource_type: ResourceType, uuid: str) -> Optional[int]:
|
||||
mapping = self.db.query(ResourceMapping).filter_by(
|
||||
environment_id=environment_id,
|
||||
resource_type=resource_type,
|
||||
uuid=uuid
|
||||
).first()
|
||||
|
||||
if mapping:
|
||||
try:
|
||||
return int(mapping.remote_integer_id)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
# [/DEF:get_remote_id:Function]
|
||||
|
||||
# [DEF:get_remote_ids_batch:Function]
|
||||
# @PURPOSE: Retrieves remote integer IDs for a list of universal UUIDs efficiently.
|
||||
# @PARAM: environment_id (str)
|
||||
# @PARAM: resource_type (ResourceType)
|
||||
# @PARAM: uuids (List[str])
|
||||
# @RETURN: Dict[str, int] - Mapping of UUID -> Integer ID
|
||||
def get_remote_ids_batch(self, environment_id: str, resource_type: ResourceType, uuids: List[str]) -> Dict[str, int]:
|
||||
if not uuids:
|
||||
return {}
|
||||
|
||||
mappings = self.db.query(ResourceMapping).filter(
|
||||
ResourceMapping.environment_id == environment_id,
|
||||
ResourceMapping.resource_type == resource_type,
|
||||
ResourceMapping.uuid.in_(uuids)
|
||||
).all()
|
||||
|
||||
result = {}
|
||||
for m in mappings:
|
||||
try:
|
||||
result[m.uuid] = int(m.remote_integer_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return result
|
||||
# [/DEF:get_remote_ids_batch:Function]
|
||||
|
||||
# [/DEF:IdMappingService:Class]
|
||||
# [/DEF:backend.src.core.mapping_service:Module]
|
||||
@@ -11,28 +11,41 @@
|
||||
import zipfile
|
||||
import yaml
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Dict, Optional, List
|
||||
from .logger import logger, belief_scope
|
||||
from src.core.mapping_service import IdMappingService
|
||||
from src.models.mapping import ResourceType
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:MigrationEngine:Class]
|
||||
# @PURPOSE: Engine for transforming Superset export ZIPs.
|
||||
class MigrationEngine:
|
||||
|
||||
# [DEF:__init__:Function]
|
||||
# @PURPOSE: Initializes the migration engine with optional ID mapping service.
|
||||
# @PARAM: mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.
|
||||
def __init__(self, mapping_service: Optional[IdMappingService] = None):
|
||||
self.mapping_service = mapping_service
|
||||
# [/DEF:__init__:Function]
|
||||
|
||||
# [DEF:transform_zip:Function]
|
||||
# @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, and re-packages.
|
||||
# @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.
|
||||
# @PARAM: zip_path (str) - Path to the source ZIP file.
|
||||
# @PARAM: output_path (str) - Path where the transformed ZIP will be saved.
|
||||
# @PARAM: db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.
|
||||
# @PARAM: strip_databases (bool) - Whether to remove the databases directory from the archive.
|
||||
# @PARAM: target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.
|
||||
# @PARAM: fix_cross_filters (bool) - Whether to patch dashboard json_metadata.
|
||||
# @PRE: zip_path must point to a valid Superset export archive.
|
||||
# @POST: Transformed archive is saved to output_path.
|
||||
# @RETURN: bool - True if successful.
|
||||
def transform_zip(self, zip_path: str, output_path: str, db_mapping: Dict[str, str], strip_databases: bool = True) -> bool:
|
||||
def transform_zip(self, zip_path: str, output_path: str, db_mapping: Dict[str, str], strip_databases: bool = True, target_env_id: Optional[str] = None, fix_cross_filters: bool = False) -> bool:
|
||||
"""
|
||||
Transform a Superset export ZIP by replacing database UUIDs.
|
||||
Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.
|
||||
"""
|
||||
with belief_scope("MigrationEngine.transform_zip"):
|
||||
with tempfile.TemporaryDirectory() as temp_dir_str:
|
||||
@@ -44,8 +57,7 @@ class MigrationEngine:
|
||||
with zipfile.ZipFile(zip_path, 'r') as zf:
|
||||
zf.extractall(temp_dir)
|
||||
|
||||
# 2. Transform YAMLs
|
||||
# Datasets are usually in datasets/*.yaml
|
||||
# 2. Transform YAMLs (Databases)
|
||||
dataset_files = list(temp_dir.glob("**/datasets/**/*.yaml")) + list(temp_dir.glob("**/datasets/*.yaml"))
|
||||
dataset_files = list(set(dataset_files))
|
||||
|
||||
@@ -54,6 +66,20 @@ class MigrationEngine:
|
||||
logger.info(f"[MigrationEngine.transform_zip][Action] Transforming dataset: {ds_file}")
|
||||
self._transform_yaml(ds_file, db_mapping)
|
||||
|
||||
# 2.5 Patch Cross-Filters (Dashboards)
|
||||
if fix_cross_filters and self.mapping_service and target_env_id:
|
||||
dash_files = list(temp_dir.glob("**/dashboards/**/*.yaml")) + list(temp_dir.glob("**/dashboards/*.yaml"))
|
||||
dash_files = list(set(dash_files))
|
||||
|
||||
logger.info(f"[MigrationEngine.transform_zip][State] Found {len(dash_files)} dashboard files for patching.")
|
||||
|
||||
# Gather all source UUID-to-ID mappings from the archive first
|
||||
source_id_to_uuid_map = self._extract_chart_uuids_from_archive(temp_dir)
|
||||
|
||||
for dash_file in dash_files:
|
||||
logger.info(f"[MigrationEngine.transform_zip][Action] Patching dashboard: {dash_file}")
|
||||
self._patch_dashboard_metadata(dash_file, target_env_id, source_id_to_uuid_map)
|
||||
|
||||
# 3. Re-package
|
||||
logger.info(f"[MigrationEngine.transform_zip][Action] Re-packaging ZIP to: {output_path} (strip_databases={strip_databases})")
|
||||
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
@@ -97,6 +123,100 @@ class MigrationEngine:
|
||||
yaml.dump(data, f)
|
||||
# [/DEF:_transform_yaml:Function]
|
||||
|
||||
# [DEF:_extract_chart_uuids_from_archive:Function]
|
||||
# @PURPOSE: Scans the unpacked ZIP to map local exported integer IDs back to their UUIDs.
|
||||
# @PARAM: temp_dir (Path) - Root dir of unpacked archive
|
||||
# @RETURN: Dict[int, str] - Mapping of source Integer ID to UUID.
|
||||
def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> Dict[int, str]:
|
||||
# Implementation Note: This is a placeholder for the logic that extracts
|
||||
# actual Source IDs. In a real scenario, this involves parsing chart YAMLs
|
||||
# or manifesting the export metadata structure where source IDs are stored.
|
||||
# For simplicity in US1 MVP, we assume it's read from chart files if present.
|
||||
mapping = {}
|
||||
chart_files = list(temp_dir.glob("**/charts/**/*.yaml")) + list(temp_dir.glob("**/charts/*.yaml"))
|
||||
for cf in set(chart_files):
|
||||
try:
|
||||
with open(cf, 'r') as f:
|
||||
cdata = yaml.safe_load(f)
|
||||
if cdata and 'id' in cdata and 'uuid' in cdata:
|
||||
mapping[cdata['id']] = cdata['uuid']
|
||||
except Exception:
|
||||
pass
|
||||
return mapping
|
||||
# [/DEF:_extract_chart_uuids_from_archive:Function]
|
||||
|
||||
# [DEF:_patch_dashboard_metadata:Function]
|
||||
# @PURPOSE: Replaces integer IDs in json_metadata.
|
||||
# @PARAM: file_path (Path)
|
||||
# @PARAM: target_env_id (str)
|
||||
# @PARAM: source_map (Dict[int, str])
|
||||
def _patch_dashboard_metadata(self, file_path: Path, target_env_id: str, source_map: Dict[int, str]):
|
||||
with belief_scope("MigrationEngine._patch_dashboard_metadata"):
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
if not data or 'json_metadata' not in data:
|
||||
return
|
||||
|
||||
metadata_str = data['json_metadata']
|
||||
if not metadata_str:
|
||||
return
|
||||
|
||||
metadata = json.loads(metadata_str)
|
||||
modified = False
|
||||
|
||||
# We need to deeply traverse and replace. For MVP, string replacement over the raw JSON is an option,
|
||||
# but careful dict traversal is safer.
|
||||
|
||||
# Fetch target UUIDs for everything we know:
|
||||
uuids_needed = list(source_map.values())
|
||||
target_ids = self.mapping_service.get_remote_ids_batch(target_env_id, ResourceType.CHART, uuids_needed)
|
||||
|
||||
if not target_ids:
|
||||
logger.info("[MigrationEngine._patch_dashboard_metadata][Reflect] No remote target IDs found in mapping database.")
|
||||
return
|
||||
|
||||
# Map Source Int -> Target Int
|
||||
source_to_target = {}
|
||||
missing_targets = []
|
||||
for s_id, s_uuid in source_map.items():
|
||||
if s_uuid in target_ids:
|
||||
source_to_target[s_id] = target_ids[s_uuid]
|
||||
else:
|
||||
missing_targets.append(s_id)
|
||||
|
||||
if missing_targets:
|
||||
logger.warning(f"[MigrationEngine._patch_dashboard_metadata][Coherence:Recoverable] Missing target IDs for source IDs: {missing_targets}. Cross-filters for these IDs might break.")
|
||||
|
||||
if not source_to_target:
|
||||
logger.info("[MigrationEngine._patch_dashboard_metadata][Reflect] No source IDs matched remotely. Skipping patch.")
|
||||
return
|
||||
|
||||
# Complex metadata traversal would go here (e.g. for native_filter_configuration)
|
||||
# We use regex replacement over the string for safety over unknown nested dicts.
|
||||
|
||||
new_metadata_str = metadata_str
|
||||
|
||||
# Replace chartId and datasetId assignments explicitly.
|
||||
# Pattern: "datasetId": 42 or "chartId": 42
|
||||
for s_id, t_id in source_to_target.items():
|
||||
# Replace in native_filter_configuration targets
|
||||
new_metadata_str = re.sub(r'("datasetId"\s*:\s*)' + str(s_id) + r'(\b)', r'\g<1>' + str(t_id) + r'\g<2>', new_metadata_str)
|
||||
new_metadata_str = re.sub(r'("chartId"\s*:\s*)' + str(s_id) + r'(\b)', r'\g<1>' + str(t_id) + r'\g<2>', new_metadata_str)
|
||||
|
||||
# Re-parse to validate valid JSON
|
||||
data['json_metadata'] = json.dumps(json.loads(new_metadata_str))
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
yaml.dump(data, f)
|
||||
logger.info(f"[MigrationEngine._patch_dashboard_metadata][Reason] Re-serialized modified JSON metadata for dashboard.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[MigrationEngine._patch_dashboard_metadata][Coherence:Failed] Metadata patch failed: {e}")
|
||||
|
||||
# [/DEF:_patch_dashboard_metadata:Function]
|
||||
|
||||
# [/DEF:MigrationEngine:Class]
|
||||
|
||||
# [/DEF:backend.src.core.migration_engine:Module]
|
||||
|
||||
@@ -16,6 +16,7 @@ import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, Union, cast
|
||||
from requests import Response
|
||||
from datetime import datetime
|
||||
from .logger import logger as app_logger, belief_scope
|
||||
from .utils.network import APIClient, SupersetAPIError
|
||||
from .utils.fileio import get_filename_from_headers
|
||||
@@ -400,6 +401,8 @@ class SupersetClient:
|
||||
# @RETURN: Dict - Ответ API в случае успеха.
|
||||
def import_dashboard(self, file_name: Union[str, Path], dash_id: Optional[int] = None, dash_slug: Optional[str] = None) -> Dict:
|
||||
with belief_scope("import_dashboard"):
|
||||
if file_name is None:
|
||||
raise ValueError("file_name cannot be None")
|
||||
file_path = str(file_name)
|
||||
self._validate_import_file(file_path)
|
||||
try:
|
||||
@@ -785,7 +788,9 @@ class SupersetClient:
|
||||
# @POST: Returns a dictionary with at least page and page_size.
|
||||
def _validate_query_params(self, query: Optional[Dict]) -> Dict:
|
||||
with belief_scope("_validate_query_params"):
|
||||
base_query = {"page": 0, "page_size": 1000}
|
||||
# Superset list endpoints commonly cap page_size at 100.
|
||||
# Using 100 avoids partial fetches when larger values are silently truncated.
|
||||
base_query = {"page": 0, "page_size": 100}
|
||||
return {**base_query, **(query or {})}
|
||||
# [/DEF:_validate_query_params:Function]
|
||||
|
||||
@@ -827,6 +832,53 @@ class SupersetClient:
|
||||
raise SupersetAPIError(f"Архив {zip_path} не содержит 'metadata.yaml'")
|
||||
# [/DEF:_validate_import_file:Function]
|
||||
|
||||
# [DEF:get_all_resources:Function]
|
||||
# @PURPOSE: Fetches all resources of a given type with id, uuid, and name columns.
|
||||
# @PARAM: resource_type (str) - One of "chart", "dataset", "dashboard".
|
||||
# @PRE: Client is authenticated. resource_type is valid.
|
||||
# @POST: Returns a list of resource dicts with at minimum id, uuid, and name fields.
|
||||
# @RETURN: List[Dict]
|
||||
def get_all_resources(self, resource_type: str, since_dttm: Optional[datetime] = None) -> List[Dict]:
|
||||
with belief_scope("SupersetClient.get_all_resources", f"type={resource_type}, since={since_dttm}"):
|
||||
column_map = {
|
||||
"chart": {"endpoint": "/chart/", "columns": ["id", "uuid", "slice_name"]},
|
||||
"dataset": {"endpoint": "/dataset/", "columns": ["id", "uuid", "table_name"]},
|
||||
"dashboard": {"endpoint": "/dashboard/", "columns": ["id", "uuid", "slug", "dashboard_title"]},
|
||||
}
|
||||
config = column_map.get(resource_type)
|
||||
if not config:
|
||||
app_logger.warning("[get_all_resources][Warning] Unknown resource type: %s", resource_type)
|
||||
return []
|
||||
|
||||
query = {"columns": config["columns"]}
|
||||
|
||||
if since_dttm:
|
||||
# Format to ISO 8601 string for Superset filter
|
||||
# e.g. "2026-02-25T13:24:32.186" or integer milliseconds.
|
||||
# Assuming standard ISO string works:
|
||||
# The user's example had value: 0 (which might imply ms or int) but often it accepts strings.
|
||||
import math
|
||||
# Use int milliseconds to be safe, as "0" was in the user example
|
||||
timestamp_ms = math.floor(since_dttm.timestamp() * 1000)
|
||||
|
||||
query["filters"] = [
|
||||
{
|
||||
"col": "changed_on_dttm",
|
||||
"opr": "gt",
|
||||
"value": timestamp_ms
|
||||
}
|
||||
]
|
||||
# Also we must request `changed_on_dttm` just in case, though API usually filters regardless of columns
|
||||
|
||||
validated = self._validate_query_params(query)
|
||||
data = self._fetch_all_pages(
|
||||
endpoint=config["endpoint"],
|
||||
pagination_options={"base_query": validated, "results_field": "result"},
|
||||
)
|
||||
app_logger.info("[get_all_resources][Exit] Fetched %d %s resources.", len(data), resource_type)
|
||||
return data
|
||||
# [/DEF:get_all_resources:Function]
|
||||
|
||||
# [/SECTION]
|
||||
|
||||
# [/DEF:SupersetClient:Class]
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
# @RELATION: Depends on PluginLoader to get plugin instances. It is used by the API layer to create and query tasks.
|
||||
# @INVARIANT: Task IDs are unique.
|
||||
# @CONSTRAINT: Must use belief_scope for logging.
|
||||
# @TEST_CONTRACT: TaskManagerModule -> {
|
||||
# required_fields: {plugin_loader: PluginLoader},
|
||||
# optional_fields: {},
|
||||
# invariants: ["Must use belief_scope for logging"]
|
||||
# }
|
||||
# @TEST_FIXTURE: valid_module -> {"manager_initialized": true}
|
||||
# @TEST_EDGE: missing_required_field -> {"plugin_loader": null}
|
||||
# @TEST_EDGE: empty_response -> {"tasks": []}
|
||||
# @TEST_EDGE: invalid_type -> {"plugin_loader": "string_instead_of_object"}
|
||||
# @TEST_EDGE: external_failure -> {"db_unavailable": true}
|
||||
# @TEST_INVARIANT: logger_compliance -> verifies: [valid_module]
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
import asyncio
|
||||
@@ -28,7 +39,17 @@ from ..logger import logger, belief_scope, should_log_task_level
|
||||
# @INVARIANT: Task IDs are unique within the registry.
|
||||
# @INVARIANT: Each task has exactly one status at any time.
|
||||
# @INVARIANT: Log entries are never deleted after being added to a task.
|
||||
class TaskManager:
|
||||
# @TEST_CONTRACT: TaskManager -> {
|
||||
# required_fields: {plugin_id: str, params: dict},
|
||||
# optional_fields: {user_id: str},
|
||||
# invariants: ["Task IDs are unique within the registry", "Each task has exactly one status at any time"]
|
||||
# }
|
||||
# @TEST_FIXTURE: create_valid_task -> {"plugin_id": "migration_plugin", "params": {"source": "A", "target": "B"}}
|
||||
# @TEST_EDGE: missing_required_field -> {"plugin_id": null}
|
||||
# @TEST_EDGE: empty_response -> {"params": {}}
|
||||
# @TEST_EDGE: invalid_type -> {"params": "string_instead_of_dict"}
|
||||
# @TEST_EDGE: external_failure -> {"plugin_not_found": true}
|
||||
# @TEST_INVARIANT: single_status -> verifies: [create_valid_task, external_failure]
|
||||
"""
|
||||
Manages the lifecycle of tasks, including their creation, execution, and state tracking.
|
||||
"""
|
||||
@@ -75,10 +96,9 @@ class TaskManager:
|
||||
# @POST: Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds.
|
||||
def _flusher_loop(self):
|
||||
"""Background thread that flushes log buffer to database."""
|
||||
with belief_scope("_flusher_loop"):
|
||||
while not self._flusher_stop_event.is_set():
|
||||
self._flush_logs()
|
||||
self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)
|
||||
while not self._flusher_stop_event.is_set():
|
||||
self._flush_logs()
|
||||
self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)
|
||||
# [/DEF:_flusher_loop:Function]
|
||||
|
||||
# [DEF:_flush_logs:Function]
|
||||
@@ -87,24 +107,24 @@ class TaskManager:
|
||||
# @POST: All buffered logs are written to task_logs table.
|
||||
def _flush_logs(self):
|
||||
"""Flush all buffered logs to the database."""
|
||||
with belief_scope("_flush_logs"):
|
||||
with self._log_buffer_lock:
|
||||
task_ids = list(self._log_buffer.keys())
|
||||
|
||||
for task_id in task_ids:
|
||||
with self._log_buffer_lock:
|
||||
task_ids = list(self._log_buffer.keys())
|
||||
logs = self._log_buffer.pop(task_id, [])
|
||||
|
||||
for task_id in task_ids:
|
||||
with self._log_buffer_lock:
|
||||
logs = self._log_buffer.pop(task_id, [])
|
||||
|
||||
if logs:
|
||||
try:
|
||||
self.log_persistence_service.add_logs(task_id, logs)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to flush logs for task {task_id}: {e}")
|
||||
# Re-add logs to buffer on failure
|
||||
with self._log_buffer_lock:
|
||||
if task_id not in self._log_buffer:
|
||||
self._log_buffer[task_id] = []
|
||||
self._log_buffer[task_id].extend(logs)
|
||||
if logs:
|
||||
try:
|
||||
self.log_persistence_service.add_logs(task_id, logs)
|
||||
logger.debug(f"Flushed {len(logs)} logs for task {task_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to flush logs for task {task_id}: {e}")
|
||||
# Re-add logs to buffer on failure
|
||||
with self._log_buffer_lock:
|
||||
if task_id not in self._log_buffer:
|
||||
self._log_buffer[task_id] = []
|
||||
self._log_buffer[task_id].extend(logs)
|
||||
# [/DEF:_flush_logs:Function]
|
||||
|
||||
# [DEF:_flush_task_logs:Function]
|
||||
|
||||
@@ -203,10 +203,9 @@ class APIClient:
|
||||
# @PRE: APIClient is initialized and authenticated or can be authenticated.
|
||||
# @POST: Returns headers including auth tokens.
|
||||
def headers(self) -> Dict[str, str]:
|
||||
with belief_scope("headers"):
|
||||
if not self._authenticated:
|
||||
self.authenticate()
|
||||
return {
|
||||
if not self._authenticated:
|
||||
self.authenticate()
|
||||
return {
|
||||
"Authorization": f"Bearer {self._tokens['access_token']}",
|
||||
"X-CSRFToken": self._tokens.get("csrf_token", ""),
|
||||
"Referer": self.base_url,
|
||||
@@ -225,8 +224,7 @@ class APIClient:
|
||||
# @RETURN: `requests.Response` если `raw_response=True`, иначе `dict`.
|
||||
# @THROW: SupersetAPIError, NetworkError и их подклассы.
|
||||
def request(self, method: str, endpoint: str, headers: Optional[Dict] = None, raw_response: bool = False, **kwargs) -> Union[requests.Response, Dict[str, Any]]:
|
||||
with belief_scope("request"):
|
||||
full_url = f"{self.base_url}{endpoint}"
|
||||
full_url = f"{self.base_url}{endpoint}"
|
||||
_headers = self.headers.copy()
|
||||
if headers:
|
||||
_headers.update(headers)
|
||||
@@ -394,4 +392,4 @@ class APIClient:
|
||||
|
||||
# [/DEF:APIClient:Class]
|
||||
|
||||
# [/DEF:backend.core.utils.network:Module]
|
||||
# [/DEF:backend.core.utils.network:Module]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:test_report_models:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Unit tests for report Pydantic models and their validators
|
||||
# @LAYER: Domain
|
||||
# @RELATION: TESTS -> backend.src.models.report
|
||||
|
||||
@@ -26,6 +26,7 @@ class DashboardSelection(BaseModel):
|
||||
source_env_id: str
|
||||
target_env_id: str
|
||||
replace_db_config: bool = False
|
||||
fix_cross_filters: bool = True
|
||||
# [/DEF:DashboardSelection:Class]
|
||||
|
||||
# [/DEF:backend.src.models.dashboard:Module]
|
||||
@@ -19,6 +19,16 @@ import enum
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
# [DEF:ResourceType:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Enumeration of possible Superset resource types for ID mapping.
|
||||
class ResourceType(str, enum.Enum):
|
||||
CHART = "chart"
|
||||
DATASET = "dataset"
|
||||
DASHBOARD = "dashboard"
|
||||
# [/DEF:ResourceType:Class]
|
||||
|
||||
|
||||
# [DEF:MigrationStatus:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Enumeration of possible migration job statuses.
|
||||
@@ -70,6 +80,21 @@ class MigrationJob(Base):
|
||||
status = Column(SQLEnum(MigrationStatus), default=MigrationStatus.PENDING)
|
||||
replace_db = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
# [/DEF:MigrationJob:Class]
|
||||
# [DEF:ResourceMapping:Class]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Maps a universal UUID for a resource to its actual ID on a specific environment.
|
||||
# @TEST_DATA: resource_mapping_record -> {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}
|
||||
class ResourceMapping(Base):
|
||||
__tablename__ = "resource_mappings"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
environment_id = Column(String, ForeignKey("environments.id"), nullable=False)
|
||||
resource_type = Column(SQLEnum(ResourceType), nullable=False)
|
||||
uuid = Column(String, nullable=False)
|
||||
remote_integer_id = Column(String, nullable=False) # Stored as string to handle potentially large or composite IDs safely, though Superset usually uses integers.
|
||||
resource_name = Column(String, nullable=True) # Used for UI display
|
||||
last_synced_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
# [/DEF:ResourceMapping:Class]
|
||||
|
||||
# [/DEF:backend.src.models.mapping:Module]
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ from src.core.logger import logger as app_logger, belief_scope
|
||||
from src.core.config_manager import ConfigManager
|
||||
from src.core.superset_client import SupersetClient
|
||||
from src.core.task_manager.context import TaskContext
|
||||
from src.core.database import SessionLocal
|
||||
from src.core.mapping_service import IdMappingService
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:GitPlugin:Class]
|
||||
|
||||
@@ -30,6 +30,34 @@ from ...services.llm_prompt_templates import (
|
||||
render_prompt,
|
||||
)
|
||||
|
||||
# [DEF:_is_masked_or_invalid_api_key:Function]
|
||||
# @PURPOSE: Guards against placeholder or malformed API keys in runtime.
|
||||
# @PRE: value may be None.
|
||||
# @POST: Returns True when value cannot be used for authenticated provider calls.
|
||||
def _is_masked_or_invalid_api_key(value: Optional[str]) -> bool:
|
||||
key = (value or "").strip()
|
||||
if not key:
|
||||
return True
|
||||
if key in {"********", "EMPTY_OR_NONE"}:
|
||||
return True
|
||||
# Most provider tokens are significantly longer; short values are almost always placeholders.
|
||||
return len(key) < 16
|
||||
# [/DEF:_is_masked_or_invalid_api_key:Function]
|
||||
|
||||
# [DEF:_json_safe_value:Function]
|
||||
# @PURPOSE: Recursively normalize payload values for JSON serialization.
|
||||
# @PRE: value may be nested dict/list with datetime values.
|
||||
# @POST: datetime values are converted to ISO strings.
|
||||
def _json_safe_value(value: Any):
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
if isinstance(value, dict):
|
||||
return {k: _json_safe_value(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_json_safe_value(v) for v in value]
|
||||
return value
|
||||
# [/DEF:_json_safe_value:Function]
|
||||
|
||||
# [DEF:DashboardValidationPlugin:Class]
|
||||
# @PURPOSE: Plugin for automated dashboard health analysis using LLMs.
|
||||
# @RELATION: IMPLEMENTS -> backend.src.core.plugin_base.PluginBase
|
||||
@@ -70,6 +98,7 @@ class DashboardValidationPlugin(PluginBase):
|
||||
# @SIDE_EFFECT: Captures a screenshot, calls LLM API, and writes to the database.
|
||||
async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None):
|
||||
with belief_scope("execute", f"plugin_id={self.id}"):
|
||||
validation_started_at = datetime.utcnow()
|
||||
# Use TaskContext logger if available, otherwise fall back to app logger
|
||||
log = context.logger if context else logger
|
||||
|
||||
@@ -118,11 +147,10 @@ class DashboardValidationPlugin(PluginBase):
|
||||
llm_log.debug(f"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...")
|
||||
|
||||
# Check if API key was successfully decrypted
|
||||
if not api_key:
|
||||
if _is_masked_or_invalid_api_key(api_key):
|
||||
raise ValueError(
|
||||
f"Failed to decrypt API key for provider {provider_id}. "
|
||||
f"The provider may have been encrypted with a different encryption key. "
|
||||
f"Please update the provider with a new API key through the UI."
|
||||
f"Invalid API key for provider {provider_id}. "
|
||||
"Please open LLM provider settings and save a real API key (not masked placeholder)."
|
||||
)
|
||||
|
||||
# 3. Capture Screenshot
|
||||
@@ -135,12 +163,15 @@ class DashboardValidationPlugin(PluginBase):
|
||||
filename = f"{dashboard_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
||||
screenshot_path = os.path.join(screenshots_dir, filename)
|
||||
|
||||
screenshot_started_at = datetime.utcnow()
|
||||
screenshot_log.info(f"Capturing screenshot for dashboard {dashboard_id}")
|
||||
await screenshot_service.capture_dashboard(dashboard_id, screenshot_path)
|
||||
screenshot_log.debug(f"Screenshot saved to: {screenshot_path}")
|
||||
screenshot_finished_at = datetime.utcnow()
|
||||
|
||||
# 4. Fetch Logs (from Environment /api/v1/log/)
|
||||
logs = []
|
||||
logs_fetch_started_at = datetime.utcnow()
|
||||
try:
|
||||
client = SupersetClient(env)
|
||||
|
||||
@@ -181,6 +212,7 @@ class DashboardValidationPlugin(PluginBase):
|
||||
except Exception as e:
|
||||
superset_log.warning(f"Failed to fetch logs from environment: {e}")
|
||||
logs = [f"Error fetching remote logs: {str(e)}"]
|
||||
logs_fetch_finished_at = datetime.utcnow()
|
||||
|
||||
# 5. Analyze with LLM
|
||||
llm_client = LLMClient(
|
||||
@@ -196,11 +228,13 @@ class DashboardValidationPlugin(PluginBase):
|
||||
"dashboard_validation_prompt",
|
||||
DEFAULT_LLM_PROMPTS["dashboard_validation_prompt"],
|
||||
)
|
||||
llm_call_started_at = datetime.utcnow()
|
||||
analysis = await llm_client.analyze_dashboard(
|
||||
screenshot_path,
|
||||
logs,
|
||||
prompt_template=dashboard_prompt,
|
||||
)
|
||||
llm_call_finished_at = datetime.utcnow()
|
||||
|
||||
# Log analysis summary to task logs for better visibility
|
||||
llm_log.info(f"[ANALYSIS_SUMMARY] Status: {analysis['status']}")
|
||||
@@ -218,6 +252,35 @@ class DashboardValidationPlugin(PluginBase):
|
||||
screenshot_path=screenshot_path,
|
||||
raw_response=str(analysis)
|
||||
)
|
||||
validation_finished_at = datetime.utcnow()
|
||||
|
||||
result_payload = _json_safe_value(validation_result.dict())
|
||||
result_payload["screenshot_paths"] = [screenshot_path]
|
||||
result_payload["logs_sent_to_llm"] = logs
|
||||
result_payload["logs_sent_count"] = len(logs)
|
||||
result_payload["prompt_template"] = dashboard_prompt
|
||||
result_payload["provider"] = {
|
||||
"id": db_provider.id,
|
||||
"name": db_provider.name,
|
||||
"type": db_provider.provider_type,
|
||||
"base_url": db_provider.base_url,
|
||||
"model": db_provider.default_model,
|
||||
}
|
||||
result_payload["environment_id"] = env_id
|
||||
result_payload["timings"] = {
|
||||
"validation_started_at": validation_started_at.isoformat(),
|
||||
"validation_finished_at": validation_finished_at.isoformat(),
|
||||
"validation_duration_ms": int((validation_finished_at - validation_started_at).total_seconds() * 1000),
|
||||
"screenshot_started_at": screenshot_started_at.isoformat(),
|
||||
"screenshot_finished_at": screenshot_finished_at.isoformat(),
|
||||
"screenshot_duration_ms": int((screenshot_finished_at - screenshot_started_at).total_seconds() * 1000),
|
||||
"logs_fetch_started_at": logs_fetch_started_at.isoformat(),
|
||||
"logs_fetch_finished_at": logs_fetch_finished_at.isoformat(),
|
||||
"logs_fetch_duration_ms": int((logs_fetch_finished_at - logs_fetch_started_at).total_seconds() * 1000),
|
||||
"llm_call_started_at": llm_call_started_at.isoformat(),
|
||||
"llm_call_finished_at": llm_call_finished_at.isoformat(),
|
||||
"llm_call_duration_ms": int((llm_call_finished_at - llm_call_started_at).total_seconds() * 1000),
|
||||
}
|
||||
|
||||
db_record = ValidationRecord(
|
||||
dashboard_id=validation_result.dashboard_id,
|
||||
@@ -225,7 +288,7 @@ class DashboardValidationPlugin(PluginBase):
|
||||
summary=validation_result.summary,
|
||||
issues=[issue.dict() for issue in validation_result.issues],
|
||||
screenshot_path=validation_result.screenshot_path,
|
||||
raw_response=validation_result.raw_response
|
||||
raw_response=json.dumps(result_payload, ensure_ascii=False)
|
||||
)
|
||||
db.add(db_record)
|
||||
db.commit()
|
||||
@@ -240,7 +303,7 @@ class DashboardValidationPlugin(PluginBase):
|
||||
# Final log to ensure all analysis is visible in task logs
|
||||
log.info(f"Validation completed for dashboard {dashboard_id}. Status: {validation_result.status.value}")
|
||||
|
||||
return validation_result.dict()
|
||||
return result_payload
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@@ -328,11 +391,10 @@ class DocumentationPlugin(PluginBase):
|
||||
llm_log.debug(f"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...")
|
||||
|
||||
# Check if API key was successfully decrypted
|
||||
if not api_key:
|
||||
if _is_masked_or_invalid_api_key(api_key):
|
||||
raise ValueError(
|
||||
f"Failed to decrypt API key for provider {provider_id}. "
|
||||
f"The provider may have been encrypted with a different encryption key. "
|
||||
f"Please update the provider with a new API key through the UI."
|
||||
f"Invalid API key for provider {provider_id}. "
|
||||
"Please open LLM provider settings and save a real API key (not masked placeholder)."
|
||||
)
|
||||
|
||||
# 3. Fetch Metadata (US2 / T024)
|
||||
|
||||
@@ -13,6 +13,7 @@ import base64
|
||||
import json
|
||||
import io
|
||||
from typing import List, Dict, Any
|
||||
import httpx
|
||||
from PIL import Image
|
||||
from playwright.async_api import async_playwright
|
||||
from openai import AsyncOpenAI, RateLimitError, AuthenticationError as OpenAIAuthenticationError
|
||||
@@ -422,7 +423,10 @@ class LLMClient:
|
||||
# @PRE: api_key, base_url, and default_model are non-empty strings.
|
||||
def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):
|
||||
self.provider_type = provider_type
|
||||
self.api_key = api_key
|
||||
normalized_key = (api_key or "").strip()
|
||||
if normalized_key.lower().startswith("bearer "):
|
||||
normalized_key = normalized_key[7:].strip()
|
||||
self.api_key = normalized_key
|
||||
self.base_url = base_url
|
||||
self.default_model = default_model
|
||||
|
||||
@@ -431,10 +435,22 @@ class LLMClient:
|
||||
logger.info(f"[LLMClient.__init__] Provider Type: {provider_type}")
|
||||
logger.info(f"[LLMClient.__init__] Base URL: {base_url}")
|
||||
logger.info(f"[LLMClient.__init__] Default Model: {default_model}")
|
||||
logger.info(f"[LLMClient.__init__] API Key (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...")
|
||||
logger.info(f"[LLMClient.__init__] API Key Length: {len(api_key) if api_key else 0}")
|
||||
|
||||
self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
logger.info(f"[LLMClient.__init__] API Key (first 8 chars): {self.api_key[:8] if self.api_key and len(self.api_key) > 8 else 'EMPTY_OR_NONE'}...")
|
||||
logger.info(f"[LLMClient.__init__] API Key Length: {len(self.api_key) if self.api_key else 0}")
|
||||
|
||||
# Some OpenAI-compatible gateways are strict about auth header naming.
|
||||
default_headers = {"Authorization": f"Bearer {self.api_key}"}
|
||||
if self.provider_type == LLMProviderType.KILO:
|
||||
default_headers["Authentication"] = f"Bearer {self.api_key}"
|
||||
default_headers["X-API-Key"] = self.api_key
|
||||
|
||||
http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=self.api_key,
|
||||
base_url=base_url,
|
||||
default_headers=default_headers,
|
||||
http_client=http_client,
|
||||
)
|
||||
# [/DEF:LLMClient.__init__:Function]
|
||||
|
||||
# [DEF:LLMClient._supports_json_response_format:Function]
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..dependencies import get_config_manager
|
||||
from ..core.migration_engine import MigrationEngine
|
||||
from ..core.database import SessionLocal
|
||||
from ..models.mapping import DatabaseMapping, Environment
|
||||
from ..core.mapping_service import IdMappingService
|
||||
from ..core.task_manager.context import TaskContext
|
||||
|
||||
# [DEF:MigrationPlugin:Class]
|
||||
@@ -149,6 +150,7 @@ class MigrationPlugin(PluginBase):
|
||||
dashboard_regex = params.get("dashboard_regex")
|
||||
|
||||
replace_db_config = params.get("replace_db_config", False)
|
||||
fix_cross_filters = params.get("fix_cross_filters", True)
|
||||
params.get("from_db_id")
|
||||
params.get("to_db_id")
|
||||
|
||||
@@ -165,11 +167,11 @@ class MigrationPlugin(PluginBase):
|
||||
superset_log = log.with_source("superset_api") if context else log
|
||||
migration_log = log.with_source("migration") if context else log
|
||||
|
||||
log.info("Starting migration task.")
|
||||
log.debug(f"Params: {params}")
|
||||
|
||||
try:
|
||||
with belief_scope("execute"):
|
||||
log.info("Starting migration task.")
|
||||
log.debug(f"Params: {params}")
|
||||
|
||||
try:
|
||||
with belief_scope("execute"):
|
||||
config_manager = get_config_manager()
|
||||
environments = config_manager.get_environments()
|
||||
|
||||
@@ -192,20 +194,20 @@ class MigrationPlugin(PluginBase):
|
||||
|
||||
from_env_name = src_env.name
|
||||
to_env_name = tgt_env.name
|
||||
|
||||
log.info(f"Resolved environments: {from_env_name} -> {to_env_name}")
|
||||
migration_result = {
|
||||
"status": "SUCCESS",
|
||||
"source_environment": from_env_name,
|
||||
"target_environment": to_env_name,
|
||||
"selected_dashboards": 0,
|
||||
"migrated_dashboards": [],
|
||||
"failed_dashboards": [],
|
||||
"mapping_count": 0
|
||||
}
|
||||
|
||||
from_c = SupersetClient(src_env)
|
||||
to_c = SupersetClient(tgt_env)
|
||||
|
||||
log.info(f"Resolved environments: {from_env_name} -> {to_env_name}")
|
||||
migration_result = {
|
||||
"status": "SUCCESS",
|
||||
"source_environment": from_env_name,
|
||||
"target_environment": to_env_name,
|
||||
"selected_dashboards": 0,
|
||||
"migrated_dashboards": [],
|
||||
"failed_dashboards": [],
|
||||
"mapping_count": 0
|
||||
}
|
||||
|
||||
from_c = SupersetClient(src_env)
|
||||
to_c = SupersetClient(tgt_env)
|
||||
|
||||
if not from_c or not to_c:
|
||||
raise ValueError(f"Clients not initialized for environments: {from_env_name}, {to_env_name}")
|
||||
@@ -213,24 +215,24 @@ class MigrationPlugin(PluginBase):
|
||||
_, all_dashboards = from_c.get_dashboards()
|
||||
|
||||
dashboards_to_migrate = []
|
||||
if selected_ids:
|
||||
dashboards_to_migrate = [d for d in all_dashboards if d["id"] in selected_ids]
|
||||
elif dashboard_regex:
|
||||
regex_str = str(dashboard_regex)
|
||||
dashboards_to_migrate = [
|
||||
d for d in all_dashboards if re.search(regex_str, d["dashboard_title"], re.IGNORECASE)
|
||||
if selected_ids:
|
||||
dashboards_to_migrate = [d for d in all_dashboards if d["id"] in selected_ids]
|
||||
elif dashboard_regex:
|
||||
regex_pattern = re.compile(str(dashboard_regex), re.IGNORECASE)
|
||||
dashboards_to_migrate = [
|
||||
d for d in all_dashboards if regex_pattern.search(d.get("dashboard_title", ""))
|
||||
]
|
||||
else:
|
||||
log.warning("No selection criteria provided (selected_ids or dashboard_regex).")
|
||||
migration_result["status"] = "NO_SELECTION"
|
||||
return migration_result
|
||||
|
||||
if not dashboards_to_migrate:
|
||||
log.warning("No dashboards found matching criteria.")
|
||||
migration_result["status"] = "NO_MATCHES"
|
||||
return migration_result
|
||||
|
||||
migration_result["selected_dashboards"] = len(dashboards_to_migrate)
|
||||
else:
|
||||
log.warning("No selection criteria provided (selected_ids or dashboard_regex).")
|
||||
migration_result["status"] = "NO_SELECTION"
|
||||
return migration_result
|
||||
|
||||
if not dashboards_to_migrate:
|
||||
log.warning("No dashboards found matching criteria.")
|
||||
migration_result["status"] = "NO_MATCHES"
|
||||
return migration_result
|
||||
|
||||
migration_result["selected_dashboards"] = len(dashboards_to_migrate)
|
||||
|
||||
# Get mappings from params
|
||||
db_mapping = params.get("db_mappings", {})
|
||||
@@ -251,25 +253,32 @@ class MigrationPlugin(PluginBase):
|
||||
DatabaseMapping.target_env_id == tgt_env_db.id
|
||||
).all()
|
||||
# Provided mappings override stored ones
|
||||
stored_map_dict = {m.source_db_uuid: m.target_db_uuid for m in stored_mappings}
|
||||
stored_map_dict.update(db_mapping)
|
||||
db_mapping = stored_map_dict
|
||||
log.info(f"Loaded {len(stored_mappings)} database mappings from database.")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
migration_result["mapping_count"] = len(db_mapping)
|
||||
engine = MigrationEngine()
|
||||
|
||||
for dash in dashboards_to_migrate:
|
||||
dash_id, dash_slug, title = dash["id"], dash.get("slug"), dash["dashboard_title"]
|
||||
stored_map_dict = {m.source_db_uuid: m.target_db_uuid for m in stored_mappings}
|
||||
stored_map_dict.update(db_mapping)
|
||||
db_mapping = stored_map_dict
|
||||
log.info(f"Loaded {len(stored_mappings)} database mappings from database.")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
migration_result["mapping_count"] = len(db_mapping)
|
||||
engine = MigrationEngine()
|
||||
|
||||
for dash in dashboards_to_migrate:
|
||||
dash_id, dash_slug, title = dash["id"], dash.get("slug"), dash["dashboard_title"]
|
||||
|
||||
try:
|
||||
exported_content, _ = from_c.export_dashboard(dash_id)
|
||||
with create_temp_file(content=exported_content, dry_run=True, suffix=".zip") as tmp_zip_path:
|
||||
# Always transform to strip databases to avoid password errors
|
||||
with create_temp_file(suffix=".zip", dry_run=True) as tmp_new_zip:
|
||||
success = engine.transform_zip(str(tmp_zip_path), str(tmp_new_zip), db_mapping, strip_databases=False)
|
||||
success = engine.transform_zip(
|
||||
str(tmp_zip_path),
|
||||
str(tmp_new_zip),
|
||||
db_mapping,
|
||||
strip_databases=False,
|
||||
target_env_id=tgt_env.id if tgt_env else None,
|
||||
fix_cross_filters=fix_cross_filters
|
||||
)
|
||||
|
||||
if not success and replace_db_config:
|
||||
# Signal missing mapping and wait (only if we care about mappings)
|
||||
@@ -282,33 +291,40 @@ class MigrationPlugin(PluginBase):
|
||||
# (Mappings would be updated in task.params by resolve_task)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
src_env = db.query(Environment).filter(Environment.name == from_env_name).first()
|
||||
tgt_env = db.query(Environment).filter(Environment.name == to_env_name).first()
|
||||
src_env_rt = db.query(Environment).filter(Environment.name == from_env_name).first()
|
||||
tgt_env_rt = db.query(Environment).filter(Environment.name == to_env_name).first()
|
||||
mappings = db.query(DatabaseMapping).filter(
|
||||
DatabaseMapping.source_env_id == src_env.id,
|
||||
DatabaseMapping.target_env_id == tgt_env.id
|
||||
DatabaseMapping.source_env_id == src_env_rt.id,
|
||||
DatabaseMapping.target_env_id == tgt_env_rt.id
|
||||
).all()
|
||||
db_mapping = {m.source_db_uuid: m.target_db_uuid for m in mappings}
|
||||
finally:
|
||||
db.close()
|
||||
success = engine.transform_zip(str(tmp_zip_path), str(tmp_new_zip), db_mapping, strip_databases=False)
|
||||
success = engine.transform_zip(
|
||||
str(tmp_zip_path),
|
||||
str(tmp_new_zip),
|
||||
db_mapping,
|
||||
strip_databases=False,
|
||||
target_env_id=tgt_env.id if tgt_env else None,
|
||||
fix_cross_filters=fix_cross_filters
|
||||
)
|
||||
|
||||
if success:
|
||||
to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug)
|
||||
migration_result["migrated_dashboards"].append({
|
||||
"id": dash_id,
|
||||
"title": title
|
||||
})
|
||||
else:
|
||||
migration_log.error(f"Failed to transform ZIP for dashboard {title}")
|
||||
migration_result["failed_dashboards"].append({
|
||||
"id": dash_id,
|
||||
"title": title,
|
||||
"error": "Failed to transform ZIP"
|
||||
})
|
||||
|
||||
superset_log.info(f"Dashboard {title} imported.")
|
||||
except Exception as exc:
|
||||
if success:
|
||||
to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug)
|
||||
migration_result["migrated_dashboards"].append({
|
||||
"id": dash_id,
|
||||
"title": title
|
||||
})
|
||||
else:
|
||||
migration_log.error(f"Failed to transform ZIP for dashboard {title}")
|
||||
migration_result["failed_dashboards"].append({
|
||||
"id": dash_id,
|
||||
"title": title,
|
||||
"error": "Failed to transform ZIP"
|
||||
})
|
||||
|
||||
superset_log.info(f"Dashboard {title} imported.")
|
||||
except Exception as exc:
|
||||
# Check for password error
|
||||
error_msg = str(exc)
|
||||
# The error message from Superset is often a JSON string inside a string.
|
||||
@@ -347,34 +363,45 @@ class MigrationPlugin(PluginBase):
|
||||
passwords = task.params.get("passwords", {})
|
||||
|
||||
# Retry import with password
|
||||
if passwords:
|
||||
app_logger.info(f"[MigrationPlugin][Action] Retrying import for {title} with provided passwords.")
|
||||
to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug, passwords=passwords)
|
||||
app_logger.info(f"[MigrationPlugin][Success] Dashboard {title} imported after password injection.")
|
||||
migration_result["migrated_dashboards"].append({
|
||||
"id": dash_id,
|
||||
"title": title
|
||||
})
|
||||
# Clear passwords from params after use for security
|
||||
if "passwords" in task.params:
|
||||
del task.params["passwords"]
|
||||
continue
|
||||
|
||||
app_logger.error(f"[MigrationPlugin][Failure] Failed to migrate dashboard {title}: {exc}", exc_info=True)
|
||||
migration_result["failed_dashboards"].append({
|
||||
"id": dash_id,
|
||||
"title": title,
|
||||
"error": str(exc)
|
||||
})
|
||||
|
||||
app_logger.info("[MigrationPlugin][Exit] Migration finished.")
|
||||
if migration_result["failed_dashboards"]:
|
||||
migration_result["status"] = "PARTIAL_SUCCESS"
|
||||
return migration_result
|
||||
except Exception as e:
|
||||
app_logger.critical(f"[MigrationPlugin][Failure] Fatal error during migration: {e}", exc_info=True)
|
||||
raise e
|
||||
if passwords:
|
||||
app_logger.info(f"[MigrationPlugin][Action] Retrying import for {title} with provided passwords.")
|
||||
to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug, passwords=passwords)
|
||||
app_logger.info(f"[MigrationPlugin][Success] Dashboard {title} imported after password injection.")
|
||||
migration_result["migrated_dashboards"].append({
|
||||
"id": dash_id,
|
||||
"title": title
|
||||
})
|
||||
# Clear passwords from params after use for security
|
||||
if "passwords" in task.params:
|
||||
del task.params["passwords"]
|
||||
continue
|
||||
|
||||
app_logger.error(f"[MigrationPlugin][Failure] Failed to migrate dashboard {title}: {exc}", exc_info=True)
|
||||
migration_result["failed_dashboards"].append({
|
||||
"id": dash_id,
|
||||
"title": title,
|
||||
"error": str(exc)
|
||||
})
|
||||
|
||||
app_logger.info("[MigrationPlugin][Exit] Migration finished.")
|
||||
if migration_result["failed_dashboards"]:
|
||||
migration_result["status"] = "PARTIAL_SUCCESS"
|
||||
|
||||
# Perform incremental sync to rapidly update local mappings with new imported resources
|
||||
try:
|
||||
db_session = SessionLocal()
|
||||
mapping_service = IdMappingService(db_session)
|
||||
mapping_service.sync_environment(tgt_env.id, to_c, incremental=True)
|
||||
db_session.close()
|
||||
log.info(f"[MigrationPlugin][Action] Completed incremental sync for target environment {to_env_name}")
|
||||
except Exception as sync_exc:
|
||||
log.error(f"[MigrationPlugin][Error] Failed incremental sync for {to_env_name}: {sync_exc}")
|
||||
|
||||
return migration_result
|
||||
except Exception as e:
|
||||
app_logger.critical(f"[MigrationPlugin][Failure] Fatal error during migration: {e}", exc_info=True)
|
||||
raise e
|
||||
# [/DEF:MigrationPlugin.execute:Action]
|
||||
# [/DEF:execute:Function]
|
||||
# [/DEF:MigrationPlugin:Class]
|
||||
# [/DEF:MigrationPlugin:Module]
|
||||
# [/DEF:MigrationPlugin:Module]
|
||||
|
||||
@@ -212,13 +212,21 @@ class StoragePlugin(PluginBase):
|
||||
# @PURPOSE: Lists all files and directories in a specific category and subpath.
|
||||
# @PARAM: category (Optional[FileCategory]) - The category to list.
|
||||
# @PARAM: subpath (Optional[str]) - Nested path within the category.
|
||||
# @PARAM: recursive (bool) - Whether to scan nested subdirectories recursively.
|
||||
# @PRE: Storage root must exist.
|
||||
# @POST: Returns a list of StoredFile objects.
|
||||
# @RETURN: List[StoredFile] - List of file and directory metadata objects.
|
||||
def list_files(self, category: Optional[FileCategory] = None, subpath: Optional[str] = None) -> List[StoredFile]:
|
||||
def list_files(
|
||||
self,
|
||||
category: Optional[FileCategory] = None,
|
||||
subpath: Optional[str] = None,
|
||||
recursive: bool = False,
|
||||
) -> List[StoredFile]:
|
||||
with belief_scope("StoragePlugin:list_files"):
|
||||
root = self.get_storage_root()
|
||||
logger.info(f"[StoragePlugin][Action] Listing files in root: {root}, category: {category}, subpath: {subpath}")
|
||||
logger.info(
|
||||
f"[StoragePlugin][Action] Listing files in root: {root}, category: {category}, subpath: {subpath}, recursive: {recursive}"
|
||||
)
|
||||
files = []
|
||||
|
||||
categories = [category] if category else list(FileCategory)
|
||||
@@ -235,17 +243,37 @@ class StoragePlugin(PluginBase):
|
||||
continue
|
||||
|
||||
logger.debug(f"[StoragePlugin][Action] Scanning directory: {target_dir}")
|
||||
|
||||
|
||||
if recursive:
|
||||
for current_root, dirs, filenames in os.walk(target_dir):
|
||||
dirs[:] = [d for d in dirs if "Logs" not in d]
|
||||
for filename in filenames:
|
||||
file_path = Path(current_root) / filename
|
||||
if "Logs" in str(file_path):
|
||||
continue
|
||||
stat = file_path.stat()
|
||||
files.append(
|
||||
StoredFile(
|
||||
name=filename,
|
||||
path=str(file_path.relative_to(root)),
|
||||
size=stat.st_size,
|
||||
created_at=datetime.fromtimestamp(stat.st_ctime),
|
||||
category=cat,
|
||||
mime_type=None,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Use os.scandir for better performance and to distinguish files vs dirs
|
||||
with os.scandir(target_dir) as it:
|
||||
for entry in it:
|
||||
# Skip logs
|
||||
if "Logs" in entry.path:
|
||||
continue
|
||||
|
||||
|
||||
stat = entry.stat()
|
||||
is_dir = entry.is_dir()
|
||||
|
||||
|
||||
files.append(StoredFile(
|
||||
name=entry.name,
|
||||
path=str(Path(entry.path).relative_to(root)),
|
||||
@@ -341,4 +369,4 @@ class StoragePlugin(PluginBase):
|
||||
# [/DEF:get_file_path:Function]
|
||||
|
||||
# [/DEF:StoragePlugin:Class]
|
||||
# [/DEF:StoragePlugin:Module]
|
||||
# [/DEF:StoragePlugin:Module]
|
||||
|
||||
725
backend/src/scripts/dataset_dashboard_analysis.json
Normal file
725
backend/src/scripts/dataset_dashboard_analysis.json
Normal file
@@ -0,0 +1,725 @@
|
||||
{
|
||||
"dashboard": {
|
||||
"result": {
|
||||
"certification_details": null,
|
||||
"certified_by": null,
|
||||
"changed_by": {
|
||||
"first_name": "Superset",
|
||||
"id": 1,
|
||||
"last_name": "Admin"
|
||||
},
|
||||
"changed_by_name": "Superset Admin",
|
||||
"changed_on": "2026-02-10T13:39:35.945662",
|
||||
"changed_on_delta_humanized": "15 days ago",
|
||||
"charts": [
|
||||
"TA-0001-001 test_chart"
|
||||
],
|
||||
"created_by": {
|
||||
"first_name": "Superset",
|
||||
"id": 1,
|
||||
"last_name": "Admin"
|
||||
},
|
||||
"created_on_delta_humanized": "15 days ago",
|
||||
"css": null,
|
||||
"dashboard_title": "TA-0001 Test dashboard",
|
||||
"id": 13,
|
||||
"is_managed_externally": false,
|
||||
"json_metadata": "{\"color_scheme_domain\": [], \"shared_label_colors\": [], \"map_label_colors\": {}, \"label_colors\": {}}",
|
||||
"owners": [
|
||||
{
|
||||
"first_name": "Superset",
|
||||
"id": 1,
|
||||
"last_name": "Admin"
|
||||
}
|
||||
],
|
||||
"position_json": null,
|
||||
"published": true,
|
||||
"roles": [],
|
||||
"slug": null,
|
||||
"tags": [],
|
||||
"theme": null,
|
||||
"thumbnail_url": "/api/v1/dashboard/13/thumbnail/3cfc57e6aea7188b139f94fb437a1426/",
|
||||
"url": "/superset/dashboard/13/",
|
||||
"uuid": "124b28d4-d54a-4ade-ade7-2d0473b90686"
|
||||
}
|
||||
},
|
||||
"dataset": {
|
||||
"id": 26,
|
||||
"result": {
|
||||
"always_filter_main_dttm": false,
|
||||
"cache_timeout": null,
|
||||
"catalog": "examples",
|
||||
"changed_by": {
|
||||
"first_name": "Superset",
|
||||
"last_name": "Admin"
|
||||
},
|
||||
"changed_on": "2026-02-10T13:38:26.175551",
|
||||
"changed_on_humanized": "15 days ago",
|
||||
"column_formats": {},
|
||||
"columns": [
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158196",
|
||||
"column_name": "color",
|
||||
"created_on": "2026-02-10T13:38:26.158189",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 772,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "4fa810ee-99cc-4d1f-8c0d-0f289c3b01f4",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158249",
|
||||
"column_name": "deleted",
|
||||
"created_on": "2026-02-10T13:38:26.158245",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 773,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "ebc07e82-7250-4eef-8d13-ea61561fa52c",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158289",
|
||||
"column_name": "has_2fa",
|
||||
"created_on": "2026-02-10T13:38:26.158285",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 774,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "08e72f4d-3ced-4d9a-9f7d-2f85291ce88b",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158328",
|
||||
"column_name": "id",
|
||||
"created_on": "2026-02-10T13:38:26.158324",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 775,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "fd11955c-0130-4ea1-b3c0-d8b159971789",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158366",
|
||||
"column_name": "is_admin",
|
||||
"created_on": "2026-02-10T13:38:26.158362",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 776,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "13a6c8e1-c9f8-4f08-aa62-05bca7be547b",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158404",
|
||||
"column_name": "is_app_user",
|
||||
"created_on": "2026-02-10T13:38:26.158400",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 777,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "6321ba8a-28d7-4d68-a6b3-5cef6cd681a2",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158442",
|
||||
"column_name": "is_bot",
|
||||
"created_on": "2026-02-10T13:38:26.158438",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 778,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "f3ded50e-b1a2-4a88-b805-781d5923e062",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158480",
|
||||
"column_name": "is_owner",
|
||||
"created_on": "2026-02-10T13:38:26.158477",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 779,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "8a1408eb-050d-4455-878c-22342df5da3d",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158532",
|
||||
"column_name": "is_primary_owner",
|
||||
"created_on": "2026-02-10T13:38:26.158528",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 780,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "054b8c16-82fd-480c-82e0-a0975229673a",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158583",
|
||||
"column_name": "is_restricted",
|
||||
"created_on": "2026-02-10T13:38:26.158579",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 781,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "6932c25f-0273-4595-85c1-29422a801ded",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158621",
|
||||
"column_name": "is_ultra_restricted",
|
||||
"created_on": "2026-02-10T13:38:26.158618",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 782,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "BOOLEAN",
|
||||
"type_generic": 3,
|
||||
"uuid": "9b14e5f9-3ab4-498e-b1e3-bbf49e9d61fe",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158660",
|
||||
"column_name": "name",
|
||||
"created_on": "2026-02-10T13:38:26.158656",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 783,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "ebee8249-0e10-4157-8a8e-96ae107887a3",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158697",
|
||||
"column_name": "real_name",
|
||||
"created_on": "2026-02-10T13:38:26.158694",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 784,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "553517a0-fe05-4ff5-a4eb-e9d2165d6f64",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158735",
|
||||
"column_name": "team_id",
|
||||
"created_on": "2026-02-10T13:38:26.158731",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 785,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "6c207fac-424d-465c-b80a-306b42b55ce8",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158773",
|
||||
"column_name": "tz",
|
||||
"created_on": "2026-02-10T13:38:26.158769",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 786,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "6efcc042-0b78-4362-9373-2f684077d574",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158824",
|
||||
"column_name": "tz_label",
|
||||
"created_on": "2026-02-10T13:38:26.158820",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 787,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "c6a6ac40-5c60-472d-a878-4b65b8460ccc",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158861",
|
||||
"column_name": "tz_offset",
|
||||
"created_on": "2026-02-10T13:38:26.158857",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 788,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "LONGINTEGER",
|
||||
"type_generic": 0,
|
||||
"uuid": "cf6da93a-bba9-47df-9154-6cfd0c9922fc",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158913",
|
||||
"column_name": "updated",
|
||||
"created_on": "2026-02-10T13:38:26.158909",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 789,
|
||||
"is_active": true,
|
||||
"is_dttm": true,
|
||||
"python_date_format": null,
|
||||
"type": "DATETIME",
|
||||
"type_generic": 2,
|
||||
"uuid": "2aa0a72a-5602-4799-b5ab-f22000108d62",
|
||||
"verbose_name": null
|
||||
},
|
||||
{
|
||||
"advanced_data_type": null,
|
||||
"changed_on": "2026-02-10T13:38:26.158967",
|
||||
"column_name": "channel_name",
|
||||
"created_on": "2026-02-10T13:38:26.158963",
|
||||
"description": null,
|
||||
"expression": null,
|
||||
"extra": null,
|
||||
"filterable": true,
|
||||
"groupby": true,
|
||||
"id": 790,
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"python_date_format": null,
|
||||
"type": "STRING",
|
||||
"type_generic": 1,
|
||||
"uuid": "a84bd658-c83c-4e7f-9e1b-192595092d9b",
|
||||
"verbose_name": null
|
||||
}
|
||||
],
|
||||
"created_by": {
|
||||
"first_name": "Superset",
|
||||
"last_name": "Admin"
|
||||
},
|
||||
"created_on": "2026-02-10T13:38:26.050436",
|
||||
"created_on_humanized": "15 days ago",
|
||||
"database": {
|
||||
"allow_multi_catalog": false,
|
||||
"backend": "postgresql",
|
||||
"database_name": "examples",
|
||||
"id": 1,
|
||||
"uuid": "a2dc77af-e654-49bb-b321-40f6b559a1ee"
|
||||
},
|
||||
"datasource_name": "test_join_select",
|
||||
"datasource_type": "table",
|
||||
"default_endpoint": null,
|
||||
"description": null,
|
||||
"extra": null,
|
||||
"fetch_values_predicate": null,
|
||||
"filter_select_enabled": true,
|
||||
"granularity_sqla": [
|
||||
[
|
||||
"updated",
|
||||
"updated"
|
||||
]
|
||||
],
|
||||
"id": 26,
|
||||
"is_managed_externally": false,
|
||||
"is_sqllab_view": false,
|
||||
"kind": "virtual",
|
||||
"main_dttm_col": "updated",
|
||||
"metrics": [
|
||||
{
|
||||
"changed_on": "2026-02-10T13:38:26.182269",
|
||||
"created_on": "2026-02-10T13:38:26.182264",
|
||||
"currency": null,
|
||||
"d3format": null,
|
||||
"description": null,
|
||||
"expression": "COUNT(*)",
|
||||
"extra": null,
|
||||
"id": 33,
|
||||
"metric_name": "count",
|
||||
"metric_type": "count",
|
||||
"uuid": "7510f8ca-05ee-4a37-bec1-4a5d7bf2ac50",
|
||||
"verbose_name": "COUNT(*)",
|
||||
"warning_text": null
|
||||
}
|
||||
],
|
||||
"name": "public.test_join_select",
|
||||
"normalize_columns": false,
|
||||
"offset": 0,
|
||||
"order_by_choices": [
|
||||
[
|
||||
"[\"channel_name\", true]",
|
||||
"channel_name [asc]"
|
||||
],
|
||||
[
|
||||
"[\"channel_name\", false]",
|
||||
"channel_name [desc]"
|
||||
],
|
||||
[
|
||||
"[\"color\", true]",
|
||||
"color [asc]"
|
||||
],
|
||||
[
|
||||
"[\"color\", false]",
|
||||
"color [desc]"
|
||||
],
|
||||
[
|
||||
"[\"deleted\", true]",
|
||||
"deleted [asc]"
|
||||
],
|
||||
[
|
||||
"[\"deleted\", false]",
|
||||
"deleted [desc]"
|
||||
],
|
||||
[
|
||||
"[\"has_2fa\", true]",
|
||||
"has_2fa [asc]"
|
||||
],
|
||||
[
|
||||
"[\"has_2fa\", false]",
|
||||
"has_2fa [desc]"
|
||||
],
|
||||
[
|
||||
"[\"id\", true]",
|
||||
"id [asc]"
|
||||
],
|
||||
[
|
||||
"[\"id\", false]",
|
||||
"id [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_admin\", true]",
|
||||
"is_admin [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_admin\", false]",
|
||||
"is_admin [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_app_user\", true]",
|
||||
"is_app_user [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_app_user\", false]",
|
||||
"is_app_user [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_bot\", true]",
|
||||
"is_bot [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_bot\", false]",
|
||||
"is_bot [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_owner\", true]",
|
||||
"is_owner [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_owner\", false]",
|
||||
"is_owner [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_primary_owner\", true]",
|
||||
"is_primary_owner [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_primary_owner\", false]",
|
||||
"is_primary_owner [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_restricted\", true]",
|
||||
"is_restricted [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_restricted\", false]",
|
||||
"is_restricted [desc]"
|
||||
],
|
||||
[
|
||||
"[\"is_ultra_restricted\", true]",
|
||||
"is_ultra_restricted [asc]"
|
||||
],
|
||||
[
|
||||
"[\"is_ultra_restricted\", false]",
|
||||
"is_ultra_restricted [desc]"
|
||||
],
|
||||
[
|
||||
"[\"name\", true]",
|
||||
"name [asc]"
|
||||
],
|
||||
[
|
||||
"[\"name\", false]",
|
||||
"name [desc]"
|
||||
],
|
||||
[
|
||||
"[\"real_name\", true]",
|
||||
"real_name [asc]"
|
||||
],
|
||||
[
|
||||
"[\"real_name\", false]",
|
||||
"real_name [desc]"
|
||||
],
|
||||
[
|
||||
"[\"team_id\", true]",
|
||||
"team_id [asc]"
|
||||
],
|
||||
[
|
||||
"[\"team_id\", false]",
|
||||
"team_id [desc]"
|
||||
],
|
||||
[
|
||||
"[\"tz\", true]",
|
||||
"tz [asc]"
|
||||
],
|
||||
[
|
||||
"[\"tz\", false]",
|
||||
"tz [desc]"
|
||||
],
|
||||
[
|
||||
"[\"tz_label\", true]",
|
||||
"tz_label [asc]"
|
||||
],
|
||||
[
|
||||
"[\"tz_label\", false]",
|
||||
"tz_label [desc]"
|
||||
],
|
||||
[
|
||||
"[\"tz_offset\", true]",
|
||||
"tz_offset [asc]"
|
||||
],
|
||||
[
|
||||
"[\"tz_offset\", false]",
|
||||
"tz_offset [desc]"
|
||||
],
|
||||
[
|
||||
"[\"updated\", true]",
|
||||
"updated [asc]"
|
||||
],
|
||||
[
|
||||
"[\"updated\", false]",
|
||||
"updated [desc]"
|
||||
]
|
||||
],
|
||||
"owners": [
|
||||
{
|
||||
"first_name": "Superset",
|
||||
"id": 1,
|
||||
"last_name": "Admin"
|
||||
}
|
||||
],
|
||||
"schema": "public",
|
||||
"select_star": "SELECT\n *\nFROM public.test_join_select\nLIMIT 100",
|
||||
"sql": "SELECT t_u.*,\nt_c.name as channel_name\nfrom public.users t_u \njoin public.users_channels t_c ON\nt_c.user_id = t_u.id",
|
||||
"table_name": "test_join_select",
|
||||
"template_params": null,
|
||||
"time_grain_sqla": [
|
||||
[
|
||||
"PT1S",
|
||||
"Second"
|
||||
],
|
||||
[
|
||||
"PT5S",
|
||||
"5 second"
|
||||
],
|
||||
[
|
||||
"PT30S",
|
||||
"30 second"
|
||||
],
|
||||
[
|
||||
"PT1M",
|
||||
"Minute"
|
||||
],
|
||||
[
|
||||
"PT5M",
|
||||
"5 minute"
|
||||
],
|
||||
[
|
||||
"PT10M",
|
||||
"10 minute"
|
||||
],
|
||||
[
|
||||
"PT15M",
|
||||
"15 minute"
|
||||
],
|
||||
[
|
||||
"PT30M",
|
||||
"30 minute"
|
||||
],
|
||||
[
|
||||
"PT1H",
|
||||
"Hour"
|
||||
],
|
||||
[
|
||||
"P1D",
|
||||
"Day"
|
||||
],
|
||||
[
|
||||
"P1W",
|
||||
"Week"
|
||||
],
|
||||
[
|
||||
"P1M",
|
||||
"Month"
|
||||
],
|
||||
[
|
||||
"P3M",
|
||||
"Quarter"
|
||||
],
|
||||
[
|
||||
"P1Y",
|
||||
"Year"
|
||||
]
|
||||
],
|
||||
"uid": "26__table",
|
||||
"url": "/tablemodelview/edit/26",
|
||||
"uuid": "e6f56489-6040-4720-8393-ddfc5c4c5574",
|
||||
"verbose_map": {
|
||||
"__timestamp": "Time",
|
||||
"channel_name": "channel_name",
|
||||
"color": "color",
|
||||
"count": "COUNT(*)",
|
||||
"deleted": "deleted",
|
||||
"has_2fa": "has_2fa",
|
||||
"id": "id",
|
||||
"is_admin": "is_admin",
|
||||
"is_app_user": "is_app_user",
|
||||
"is_bot": "is_bot",
|
||||
"is_owner": "is_owner",
|
||||
"is_primary_owner": "is_primary_owner",
|
||||
"is_restricted": "is_restricted",
|
||||
"is_ultra_restricted": "is_ultra_restricted",
|
||||
"name": "name",
|
||||
"real_name": "real_name",
|
||||
"team_id": "team_id",
|
||||
"tz": "tz",
|
||||
"tz_label": "tz_label",
|
||||
"tz_offset": "tz_offset",
|
||||
"updated": "updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
297
backend/src/scripts/seed_superset_load_test.py
Normal file
297
backend/src/scripts/seed_superset_load_test.py
Normal file
@@ -0,0 +1,297 @@
|
||||
# [DEF:backend.src.scripts.seed_superset_load_test:Module]
|
||||
#
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: superset, load-test, charts, dashboards, seed, stress
|
||||
# @PURPOSE: Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments.
|
||||
# @LAYER: Scripts
|
||||
# @RELATION: USES -> backend.src.core.config_manager.ConfigManager
|
||||
# @RELATION: USES -> backend.src.core.superset_client.SupersetClient
|
||||
# @INVARIANT: Created chart and dashboard names are globally unique for one script run.
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from src.core.config_manager import ConfigManager
|
||||
from src.core.config_models import Environment
|
||||
from src.core.logger import belief_scope, logger
|
||||
from src.core.superset_client import SupersetClient
|
||||
# [/SECTION]
|
||||
|
||||
|
||||
# [DEF:_parse_args:Function]
|
||||
# @PURPOSE: Parses CLI arguments for load-test data generation.
|
||||
# @PRE: Script is called from CLI.
|
||||
# @POST: Returns validated argument namespace.
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Seed Superset with load-test charts and dashboards")
|
||||
parser.add_argument("--envs", nargs="+", default=["ss1", "ss2"], help="Target environment IDs")
|
||||
parser.add_argument("--charts", type=int, default=10000, help="Target number of charts to create")
|
||||
parser.add_argument("--dashboards", type=int, default=500, help="Target number of dashboards to create")
|
||||
parser.add_argument("--template-pool-size", type=int, default=200, help="How many source charts to sample as templates per env")
|
||||
parser.add_argument("--seed", type=int, default=None, help="Optional RNG seed for reproducibility")
|
||||
parser.add_argument("--max-errors", type=int, default=100, help="Stop early if errors exceed this threshold")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Do not write data, only validate setup")
|
||||
return parser.parse_args()
|
||||
# [/DEF:_parse_args:Function]
|
||||
|
||||
|
||||
# [DEF:_extract_result_payload:Function]
|
||||
# @PURPOSE: Normalizes Superset API payloads that may be wrapped in `result`.
|
||||
# @PRE: payload is a JSON-decoded API response.
|
||||
# @POST: Returns the unwrapped object when present.
|
||||
def _extract_result_payload(payload: Dict) -> Dict:
|
||||
result = payload.get("result")
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
return payload
|
||||
# [/DEF:_extract_result_payload:Function]
|
||||
|
||||
|
||||
# [DEF:_extract_created_id:Function]
|
||||
# @PURPOSE: Extracts object ID from create/update API response.
|
||||
# @PRE: payload is a JSON-decoded API response.
|
||||
# @POST: Returns integer object ID or None if missing.
|
||||
def _extract_created_id(payload: Dict) -> Optional[int]:
|
||||
direct_id = payload.get("id")
|
||||
if isinstance(direct_id, int):
|
||||
return direct_id
|
||||
result = payload.get("result")
|
||||
if isinstance(result, dict) and isinstance(result.get("id"), int):
|
||||
return int(result["id"])
|
||||
return None
|
||||
# [/DEF:_extract_created_id:Function]
|
||||
|
||||
|
||||
# [DEF:_generate_unique_name:Function]
|
||||
# @PURPOSE: Generates globally unique random names for charts/dashboards.
|
||||
# @PRE: used_names is mutable set for collision tracking.
|
||||
# @POST: Returns a unique string and stores it in used_names.
|
||||
def _generate_unique_name(prefix: str, used_names: set[str], rng: random.Random) -> str:
|
||||
adjectives = ["amber", "rapid", "frozen", "delta", "lunar", "vector", "cobalt", "silent", "neon", "solar"]
|
||||
nouns = ["falcon", "matrix", "signal", "harbor", "stream", "vertex", "bridge", "orbit", "pulse", "forge"]
|
||||
while True:
|
||||
token = uuid.uuid4().hex[:8]
|
||||
candidate = f"{prefix}_{rng.choice(adjectives)}_{rng.choice(nouns)}_{rng.randint(100, 999)}_{token}"
|
||||
if candidate not in used_names:
|
||||
used_names.add(candidate)
|
||||
return candidate
|
||||
# [/DEF:_generate_unique_name:Function]
|
||||
|
||||
|
||||
# [DEF:_resolve_target_envs:Function]
|
||||
# @PURPOSE: Resolves requested environment IDs from configuration.
|
||||
# @PRE: env_ids is non-empty.
|
||||
# @POST: Returns mapping env_id -> configured environment object.
|
||||
def _resolve_target_envs(env_ids: List[str]) -> Dict[str, Environment]:
|
||||
config_manager = ConfigManager()
|
||||
configured = {env.id: env for env in config_manager.get_environments()}
|
||||
resolved: Dict[str, Environment] = {}
|
||||
|
||||
if not configured:
|
||||
for config_path in [Path("config.json"), Path("backend/config.json")]:
|
||||
if not config_path.exists():
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
env_rows = payload.get("environments", [])
|
||||
for row in env_rows:
|
||||
env = Environment(**row)
|
||||
configured[env.id] = env
|
||||
except Exception as exc:
|
||||
logger.warning(f"[REFLECT] Failed loading environments from {config_path}: {exc}")
|
||||
|
||||
for env_id in env_ids:
|
||||
env = configured.get(env_id)
|
||||
if env is None:
|
||||
raise ValueError(f"Environment '{env_id}' not found in configuration")
|
||||
resolved[env_id] = env
|
||||
|
||||
return resolved
|
||||
# [/DEF:_resolve_target_envs:Function]
|
||||
|
||||
|
||||
# [DEF:_build_chart_template_pool:Function]
|
||||
# @PURPOSE: Builds a pool of source chart templates to clone in one environment.
|
||||
# @PRE: Client is authenticated.
|
||||
# @POST: Returns non-empty list of chart payload templates.
|
||||
def _build_chart_template_pool(client: SupersetClient, pool_size: int, rng: random.Random) -> List[Dict]:
|
||||
list_query = {
|
||||
"page": 0,
|
||||
"page_size": 1000,
|
||||
"columns": ["id", "slice_name", "datasource_id", "datasource_type", "viz_type", "params", "query_context"],
|
||||
}
|
||||
rows = client.network.fetch_paginated_data(
|
||||
endpoint="/chart/",
|
||||
pagination_options={"base_query": list_query, "results_field": "result"},
|
||||
)
|
||||
|
||||
candidates = [row for row in rows if isinstance(row, dict) and row.get("id")]
|
||||
if not candidates:
|
||||
raise RuntimeError("No source charts available for templating")
|
||||
|
||||
selected = candidates if len(candidates) <= pool_size else rng.sample(candidates, pool_size)
|
||||
templates: List[Dict] = []
|
||||
|
||||
for row in selected:
|
||||
chart_id = int(row["id"])
|
||||
detail_payload = client.get_chart(chart_id)
|
||||
detail = _extract_result_payload(detail_payload)
|
||||
|
||||
datasource_id = detail.get("datasource_id")
|
||||
datasource_type = detail.get("datasource_type") or row.get("datasource_type") or "table"
|
||||
if datasource_id is None:
|
||||
continue
|
||||
|
||||
params_value = detail.get("params")
|
||||
if isinstance(params_value, dict):
|
||||
params_value = json.dumps(params_value)
|
||||
|
||||
query_context_value = detail.get("query_context")
|
||||
if isinstance(query_context_value, dict):
|
||||
query_context_value = json.dumps(query_context_value)
|
||||
|
||||
templates.append(
|
||||
{
|
||||
"datasource_id": int(datasource_id),
|
||||
"datasource_type": str(datasource_type),
|
||||
"viz_type": detail.get("viz_type") or row.get("viz_type"),
|
||||
"params": params_value,
|
||||
"query_context": query_context_value,
|
||||
}
|
||||
)
|
||||
|
||||
if not templates:
|
||||
raise RuntimeError("Could not build templates with datasource metadata")
|
||||
|
||||
return templates
|
||||
# [/DEF:_build_chart_template_pool:Function]
|
||||
|
||||
|
||||
# [DEF:seed_superset_load_data:Function]
|
||||
# @PURPOSE: Creates dashboards and cloned charts for load testing across target environments.
|
||||
# @PRE: Target environments must be reachable and authenticated.
|
||||
# @POST: Returns execution statistics dictionary.
|
||||
# @SIDE_EFFECT: Creates objects in Superset environments.
|
||||
def seed_superset_load_data(args: argparse.Namespace) -> Dict:
|
||||
rng = random.Random(args.seed)
|
||||
env_map = _resolve_target_envs(args.envs)
|
||||
|
||||
clients: Dict[str, SupersetClient] = {}
|
||||
templates_by_env: Dict[str, List[Dict]] = {}
|
||||
created_dashboards: Dict[str, List[int]] = {env_id: [] for env_id in env_map}
|
||||
created_charts: Dict[str, List[int]] = {env_id: [] for env_id in env_map}
|
||||
used_chart_names: set[str] = set()
|
||||
used_dashboard_names: set[str] = set()
|
||||
|
||||
for env_id, env in env_map.items():
|
||||
client = SupersetClient(env)
|
||||
client.authenticate()
|
||||
clients[env_id] = client
|
||||
templates_by_env[env_id] = _build_chart_template_pool(client, args.template_pool_size, rng)
|
||||
logger.info(f"[REASON] Environment {env_id}: loaded {len(templates_by_env[env_id])} chart templates")
|
||||
|
||||
errors = 0
|
||||
env_ids = list(env_map.keys())
|
||||
|
||||
for idx in range(args.dashboards):
|
||||
env_id = env_ids[idx % len(env_ids)] if idx < len(env_ids) else rng.choice(env_ids)
|
||||
dashboard_title = _generate_unique_name("lt_dash", used_dashboard_names, rng)
|
||||
|
||||
if args.dry_run:
|
||||
logger.info(f"[REFLECT] Dry-run dashboard create: env={env_id}, title={dashboard_title}")
|
||||
continue
|
||||
|
||||
try:
|
||||
payload = {"dashboard_title": dashboard_title, "published": False}
|
||||
created = clients[env_id].network.request("POST", "/dashboard/", data=json.dumps(payload))
|
||||
dashboard_id = _extract_created_id(created)
|
||||
if dashboard_id is None:
|
||||
raise RuntimeError(f"Dashboard create response missing id: {created}")
|
||||
created_dashboards[env_id].append(dashboard_id)
|
||||
except Exception as exc:
|
||||
errors += 1
|
||||
logger.error(f"[EXPLORE] Failed creating dashboard in {env_id}: {exc}")
|
||||
if errors >= args.max_errors:
|
||||
raise RuntimeError(f"Stopping due to max errors reached ({errors})") from exc
|
||||
|
||||
if args.dry_run:
|
||||
return {
|
||||
"dry_run": True,
|
||||
"templates_by_env": {k: len(v) for k, v in templates_by_env.items()},
|
||||
"charts_target": args.charts,
|
||||
"dashboards_target": args.dashboards,
|
||||
}
|
||||
|
||||
for env_id in env_ids:
|
||||
if not created_dashboards[env_id]:
|
||||
raise RuntimeError(f"No dashboards created in environment {env_id}; cannot bind charts")
|
||||
|
||||
for index in range(args.charts):
|
||||
env_id = rng.choice(env_ids)
|
||||
client = clients[env_id]
|
||||
template = rng.choice(templates_by_env[env_id])
|
||||
dashboard_id = rng.choice(created_dashboards[env_id])
|
||||
chart_name = _generate_unique_name("lt_chart", used_chart_names, rng)
|
||||
|
||||
payload = {
|
||||
"slice_name": chart_name,
|
||||
"datasource_id": template["datasource_id"],
|
||||
"datasource_type": template["datasource_type"],
|
||||
"dashboards": [dashboard_id],
|
||||
}
|
||||
if template.get("viz_type"):
|
||||
payload["viz_type"] = template["viz_type"]
|
||||
if template.get("params"):
|
||||
payload["params"] = template["params"]
|
||||
if template.get("query_context"):
|
||||
payload["query_context"] = template["query_context"]
|
||||
|
||||
try:
|
||||
created = client.network.request("POST", "/chart/", data=json.dumps(payload))
|
||||
chart_id = _extract_created_id(created)
|
||||
if chart_id is None:
|
||||
raise RuntimeError(f"Chart create response missing id: {created}")
|
||||
created_charts[env_id].append(chart_id)
|
||||
|
||||
if (index + 1) % 500 == 0:
|
||||
logger.info(f"[REASON] Created {index + 1}/{args.charts} charts")
|
||||
except Exception as exc:
|
||||
errors += 1
|
||||
logger.error(f"[EXPLORE] Failed creating chart in {env_id}: {exc}")
|
||||
if errors >= args.max_errors:
|
||||
raise RuntimeError(f"Stopping due to max errors reached ({errors})") from exc
|
||||
|
||||
return {
|
||||
"dry_run": False,
|
||||
"errors": errors,
|
||||
"dashboards": {env_id: len(ids) for env_id, ids in created_dashboards.items()},
|
||||
"charts": {env_id: len(ids) for env_id, ids in created_charts.items()},
|
||||
"total_dashboards": sum(len(ids) for ids in created_dashboards.values()),
|
||||
"total_charts": sum(len(ids) for ids in created_charts.values()),
|
||||
}
|
||||
# [/DEF:seed_superset_load_data:Function]
|
||||
|
||||
|
||||
# [DEF:main:Function]
|
||||
# @PURPOSE: CLI entrypoint for Superset load-test data seeding.
|
||||
# @PRE: Command line arguments are valid.
|
||||
# @POST: Prints summary and exits with non-zero status on failure.
|
||||
def main() -> None:
|
||||
with belief_scope("seed_superset_load_test.main"):
|
||||
args = _parse_args()
|
||||
result = seed_superset_load_data(args)
|
||||
logger.info(f"[COHERENCE:OK] Result summary: {json.dumps(result, ensure_ascii=True)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# [/DEF:backend.src.scripts.seed_superset_load_test:Module]
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:test_encryption_manager:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: encryption, security, fernet, api-keys, tests
|
||||
# @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality.
|
||||
# @LAYER: Domain
|
||||
|
||||
@@ -51,6 +51,8 @@ class GitService:
|
||||
# @RETURN: str
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
with belief_scope("GitService._get_repo_path"):
|
||||
if dashboard_id is None:
|
||||
raise ValueError("dashboard_id cannot be None")
|
||||
return os.path.join(self.base_path, str(dashboard_id))
|
||||
# [/DEF:_get_repo_path:Function]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:backend.tests.test_report_normalizer:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, reports, normalizer, fallback
|
||||
# @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior.
|
||||
# @LAYER: Domain (Tests)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:test_report_service:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Unit tests for ReportsService list/detail operations
|
||||
# @LAYER: Domain
|
||||
# @RELATION: TESTS -> backend.src.services.reports.report_service.ReportsService
|
||||
@@ -121,9 +121,11 @@ class TestReportsServiceList:
|
||||
_make_task(task_id="t2", status_value="FAILED"),
|
||||
]
|
||||
svc = self._make_service(tasks)
|
||||
# Order: FAILED (desc) -> SUCCESS (asc) if it's alphanumeric
|
||||
# status values are 'SUCCESS', 'FAILED'. 'FAILED' < 'SUCCESS'
|
||||
result = svc.list_reports(ReportQuery(sort_by="status", sort_order="asc"))
|
||||
statuses = [item.status.value for item in result.items]
|
||||
assert statuses == sorted(statuses)
|
||||
assert result.items[0].status.value == "failed"
|
||||
assert result.items[1].status.value == "success"
|
||||
|
||||
def test_applied_filters_echoed(self):
|
||||
from src.models.report import ReportQuery
|
||||
|
||||
@@ -161,7 +161,12 @@ class ResourceService:
|
||||
try:
|
||||
repo = self.git_service.get_repo(dashboard_id)
|
||||
if not repo:
|
||||
return None
|
||||
return {
|
||||
'branch': None,
|
||||
'sync_status': 'NO_REPO',
|
||||
'has_repo': False,
|
||||
'has_changes_for_commit': False
|
||||
}
|
||||
|
||||
# Check if there are uncommitted changes
|
||||
try:
|
||||
@@ -170,6 +175,7 @@ class ResourceService:
|
||||
|
||||
# Check for uncommitted changes
|
||||
is_dirty = repo.is_dirty()
|
||||
has_changes_for_commit = repo.is_dirty(untracked_files=True)
|
||||
|
||||
# Check for unpushed commits
|
||||
unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0
|
||||
@@ -181,14 +187,26 @@ class ResourceService:
|
||||
|
||||
return {
|
||||
'branch': branch,
|
||||
'sync_status': sync_status
|
||||
'sync_status': sync_status,
|
||||
'has_repo': True,
|
||||
'has_changes_for_commit': has_changes_for_commit
|
||||
}
|
||||
except Exception:
|
||||
logger.warning(f"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}")
|
||||
return None
|
||||
return {
|
||||
'branch': None,
|
||||
'sync_status': 'ERROR',
|
||||
'has_repo': True,
|
||||
'has_changes_for_commit': False
|
||||
}
|
||||
except Exception:
|
||||
# No repo exists for this dashboard
|
||||
return None
|
||||
return {
|
||||
'branch': None,
|
||||
'sync_status': 'NO_REPO',
|
||||
'has_repo': False,
|
||||
'has_changes_for_commit': False
|
||||
}
|
||||
# [/DEF:_get_git_status_for_dashboard:Function]
|
||||
|
||||
# [DEF:_get_last_task_for_resource:Function]
|
||||
|
||||
29
backend/tests/core/test_defensive_guards.py
Normal file
29
backend/tests/core/test_defensive_guards.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from src.services.git_service import GitService
|
||||
from src.core.superset_client import SupersetClient
|
||||
from src.core.config_models import Environment
|
||||
|
||||
def test_git_service_get_repo_path_guard():
|
||||
"""Verify that _get_repo_path raises ValueError if dashboard_id is None."""
|
||||
service = GitService(base_path="test_repos")
|
||||
with pytest.raises(ValueError, match="dashboard_id cannot be None"):
|
||||
service._get_repo_path(None)
|
||||
|
||||
def test_superset_client_import_dashboard_guard():
|
||||
"""Verify that import_dashboard raises ValueError if file_name is None."""
|
||||
mock_env = Environment(
|
||||
id="test",
|
||||
name="test",
|
||||
url="http://localhost:8088",
|
||||
username="admin",
|
||||
password="admin"
|
||||
)
|
||||
client = SupersetClient(mock_env)
|
||||
with pytest.raises(ValueError, match="file_name cannot be None"):
|
||||
client.import_dashboard(None)
|
||||
249
backend/tests/core/test_mapping_service.py
Normal file
249
backend/tests/core/test_mapping_service.py
Normal file
@@ -0,0 +1,249 @@
|
||||
# [DEF:backend.tests.core.test_mapping_service:Module]
|
||||
#
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: VERIFIES -> backend.src.core.mapping_service
|
||||
#
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add backend directory to sys.path so 'src' can be resolved
|
||||
backend_dir = str(Path(__file__).parent.parent.parent.resolve())
|
||||
if backend_dir not in sys.path:
|
||||
sys.path.insert(0, backend_dir)
|
||||
|
||||
from src.models.mapping import Base, ResourceMapping, ResourceType
|
||||
from src.core.mapping_service import IdMappingService
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
# In-memory SQLite for testing
|
||||
engine = create_engine('sqlite:///:memory:')
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
class MockSupersetClient:
|
||||
def __init__(self, resources):
|
||||
self.resources = resources
|
||||
|
||||
def get_all_resources(self, endpoint, since_dttm=None):
|
||||
return self.resources.get(endpoint, [])
|
||||
|
||||
def test_sync_environment_upserts_correctly(db_session):
|
||||
service = IdMappingService(db_session)
|
||||
mock_client = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 42, "uuid": "123e4567-e89b-12d3-a456-426614174000", "slice_name": "Test Chart"}
|
||||
]
|
||||
})
|
||||
|
||||
service.sync_environment("test-env", mock_client)
|
||||
|
||||
mapping = db_session.query(ResourceMapping).first()
|
||||
assert mapping is not None
|
||||
assert mapping.environment_id == "test-env"
|
||||
assert mapping.resource_type == ResourceType.CHART
|
||||
assert mapping.uuid == "123e4567-e89b-12d3-a456-426614174000"
|
||||
assert mapping.remote_integer_id == "42"
|
||||
assert mapping.resource_name == "Test Chart"
|
||||
|
||||
def test_get_remote_id_returns_integer(db_session):
|
||||
service = IdMappingService(db_session)
|
||||
mapping = ResourceMapping(
|
||||
environment_id="test-env",
|
||||
resource_type=ResourceType.DATASET,
|
||||
uuid="uuid-1",
|
||||
remote_integer_id="99",
|
||||
resource_name="Test DS",
|
||||
last_synced_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db_session.add(mapping)
|
||||
db_session.commit()
|
||||
|
||||
result = service.get_remote_id("test-env", ResourceType.DATASET, "uuid-1")
|
||||
assert result == 99
|
||||
|
||||
def test_get_remote_ids_batch_returns_dict(db_session):
|
||||
service = IdMappingService(db_session)
|
||||
m1 = ResourceMapping(
|
||||
environment_id="test-env",
|
||||
resource_type=ResourceType.DASHBOARD,
|
||||
uuid="uuid-1",
|
||||
remote_integer_id="11"
|
||||
)
|
||||
m2 = ResourceMapping(
|
||||
environment_id="test-env",
|
||||
resource_type=ResourceType.DASHBOARD,
|
||||
uuid="uuid-2",
|
||||
remote_integer_id="22"
|
||||
)
|
||||
db_session.add_all([m1, m2])
|
||||
db_session.commit()
|
||||
|
||||
result = service.get_remote_ids_batch("test-env", ResourceType.DASHBOARD, ["uuid-1", "uuid-2", "uuid-missing"])
|
||||
|
||||
assert len(result) == 2
|
||||
assert result["uuid-1"] == 11
|
||||
assert result["uuid-2"] == 22
|
||||
assert "uuid-missing" not in result
|
||||
|
||||
def test_sync_environment_updates_existing_mapping(db_session):
|
||||
"""Verify that sync_environment updates an existing mapping (upsert UPDATE path)."""
|
||||
from src.models.mapping import ResourceMapping
|
||||
# Pre-populate a mapping
|
||||
existing = ResourceMapping(
|
||||
environment_id="test-env",
|
||||
resource_type=ResourceType.CHART,
|
||||
uuid="123e4567-e89b-12d3-a456-426614174000",
|
||||
remote_integer_id="10",
|
||||
resource_name="Old Name"
|
||||
)
|
||||
db_session.add(existing)
|
||||
db_session.commit()
|
||||
|
||||
service = IdMappingService(db_session)
|
||||
mock_client = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 42, "uuid": "123e4567-e89b-12d3-a456-426614174000", "slice_name": "Updated Name"}
|
||||
]
|
||||
})
|
||||
|
||||
service.sync_environment("test-env", mock_client)
|
||||
|
||||
mapping = db_session.query(ResourceMapping).filter_by(
|
||||
uuid="123e4567-e89b-12d3-a456-426614174000"
|
||||
).first()
|
||||
assert mapping.remote_integer_id == "42"
|
||||
assert mapping.resource_name == "Updated Name"
|
||||
# Should still be only one record (updated, not duplicated)
|
||||
count = db_session.query(ResourceMapping).count()
|
||||
assert count == 1
|
||||
|
||||
def test_sync_environment_skips_resources_without_uuid(db_session):
|
||||
"""Resources missing uuid or having id=None should be silently skipped."""
|
||||
service = IdMappingService(db_session)
|
||||
mock_client = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 42, "slice_name": "No UUID"}, # Missing 'uuid' -> skipped
|
||||
{"id": None, "uuid": "valid-uuid", "slice_name": "ID is None"}, # id=None -> skipped
|
||||
{"id": None, "uuid": None, "slice_name": "Both None"}, # both None -> skipped
|
||||
]
|
||||
})
|
||||
|
||||
service.sync_environment("test-env", mock_client)
|
||||
|
||||
count = db_session.query(ResourceMapping).count()
|
||||
assert count == 0
|
||||
|
||||
def test_sync_environment_handles_api_error_gracefully(db_session):
|
||||
"""If one resource type fails, others should still sync."""
|
||||
class FailingClient:
|
||||
def get_all_resources(self, endpoint, since_dttm=None):
|
||||
if endpoint == "chart":
|
||||
raise ConnectionError("API timeout")
|
||||
if endpoint == "dataset":
|
||||
return [{"id": 99, "uuid": "ds-uuid-1", "table_name": "users"}]
|
||||
return []
|
||||
|
||||
service = IdMappingService(db_session)
|
||||
service.sync_environment("test-env", FailingClient())
|
||||
|
||||
count = db_session.query(ResourceMapping).count()
|
||||
assert count == 1 # Only dataset was synced; chart error was swallowed
|
||||
mapping = db_session.query(ResourceMapping).first()
|
||||
assert mapping.resource_type == ResourceType.DATASET
|
||||
|
||||
def test_get_remote_id_returns_none_for_missing(db_session):
|
||||
"""get_remote_id should return None when no mapping exists."""
|
||||
service = IdMappingService(db_session)
|
||||
result = service.get_remote_id("test-env", ResourceType.CHART, "nonexistent-uuid")
|
||||
assert result is None
|
||||
|
||||
def test_get_remote_ids_batch_returns_empty_for_empty_input(db_session):
|
||||
"""get_remote_ids_batch should return {} for an empty list of UUIDs."""
|
||||
service = IdMappingService(db_session)
|
||||
result = service.get_remote_ids_batch("test-env", ResourceType.CHART, [])
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_mapping_service_alignment_with_test_data(db_session):
|
||||
"""**@TEST_DATA**: Verifies that the service aligns with the resource_mapping_record contract."""
|
||||
# Contract: {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}
|
||||
contract_data = {
|
||||
'environment_id': 'prod-env-1',
|
||||
'resource_type': ResourceType.CHART,
|
||||
'uuid': '123e4567-e89b-12d3-a456-426614174000',
|
||||
'remote_integer_id': '42'
|
||||
}
|
||||
|
||||
mapping = ResourceMapping(**contract_data)
|
||||
db_session.add(mapping)
|
||||
db_session.commit()
|
||||
|
||||
service = IdMappingService(db_session)
|
||||
result = service.get_remote_id(
|
||||
contract_data['environment_id'],
|
||||
contract_data['resource_type'],
|
||||
contract_data['uuid']
|
||||
)
|
||||
|
||||
assert result == 42
|
||||
|
||||
|
||||
def test_sync_environment_requires_existing_env(db_session):
|
||||
"""**@PRE**: Verify behavior when environment_id is invalid/missing in DB.
|
||||
Note: The current implementation doesn't strictly check for environment existencia in the DB
|
||||
before polling, but it should handle it gracefully or follow the contract.
|
||||
"""
|
||||
service = IdMappingService(db_session)
|
||||
mock_client = MockSupersetClient({"chart": []})
|
||||
|
||||
# Even if environment doesn't exist in a hypothetical 'environments' table,
|
||||
# the service should still complete or fail according to defined error handling.
|
||||
# In GRACE-Poly, @PRE is a hard requirement. If we don't have an Env model check,
|
||||
# we simulate the intent.
|
||||
|
||||
service.sync_environment("non-existent-env", mock_client)
|
||||
# If no error raised, at least verify no mappings were created for other envs
|
||||
assert db_session.query(ResourceMapping).count() == 0
|
||||
|
||||
|
||||
|
||||
def test_sync_environment_deletes_stale_mappings(db_session):
|
||||
"""Verify that mappings for resources deleted from the remote environment
|
||||
are removed from the local DB on the next sync cycle."""
|
||||
service = IdMappingService(db_session)
|
||||
|
||||
# First sync: 2 charts exist
|
||||
client_v1 = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
||||
{"id": 2, "uuid": "bbb", "slice_name": "Chart B"},
|
||||
]
|
||||
})
|
||||
service.sync_environment("env1", client_v1)
|
||||
assert db_session.query(ResourceMapping).filter_by(environment_id="env1").count() == 2
|
||||
|
||||
# Second sync: user deleted Chart B from superset
|
||||
client_v2 = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
||||
]
|
||||
})
|
||||
service.sync_environment("env1", client_v2)
|
||||
|
||||
remaining = db_session.query(ResourceMapping).filter_by(environment_id="env1").all()
|
||||
assert len(remaining) == 1
|
||||
assert remaining[0].uuid == "aaa"
|
||||
|
||||
|
||||
# [/DEF:backend.tests.core.test_mapping_service:Module]
|
||||
290
backend/tests/core/test_migration_engine.py
Normal file
290
backend/tests/core/test_migration_engine.py
Normal file
@@ -0,0 +1,290 @@
|
||||
# [DEF:backend.tests.core.test_migration_engine:Module]
|
||||
#
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: VERIFIES -> backend.src.core.migration_engine
|
||||
#
|
||||
import pytest
|
||||
import tempfile
|
||||
import json
|
||||
import yaml
|
||||
import zipfile
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
backend_dir = str(Path(__file__).parent.parent.parent.resolve())
|
||||
if backend_dir not in sys.path:
|
||||
sys.path.insert(0, backend_dir)
|
||||
|
||||
from src.core.migration_engine import MigrationEngine
|
||||
from src.core.mapping_service import IdMappingService
|
||||
from src.models.mapping import ResourceType
|
||||
|
||||
|
||||
# --- Fixtures ---
|
||||
|
||||
class MockMappingService:
|
||||
"""Mock that simulates IdMappingService.get_remote_ids_batch."""
|
||||
def __init__(self, mappings: dict):
|
||||
self.mappings = mappings
|
||||
|
||||
def get_remote_ids_batch(self, env_id, resource_type, uuids):
|
||||
result = {}
|
||||
for uuid in uuids:
|
||||
if uuid in self.mappings:
|
||||
result[uuid] = self.mappings[uuid]
|
||||
return result
|
||||
|
||||
|
||||
def _write_dashboard_yaml(dir_path: Path, metadata: dict) -> Path:
|
||||
"""Helper: writes a dashboard YAML file with json_metadata."""
|
||||
file_path = dir_path / "dash.yaml"
|
||||
with open(file_path, 'w') as f:
|
||||
yaml.dump({"json_metadata": json.dumps(metadata)}, f)
|
||||
return file_path
|
||||
|
||||
|
||||
# --- _patch_dashboard_metadata tests ---
|
||||
|
||||
def test_patch_dashboard_metadata_replaces_chart_ids():
|
||||
"""Verifies that chartId values are replaced using the mapping service."""
|
||||
mock_service = MockMappingService({"uuid-chart-A": 999})
|
||||
engine = MigrationEngine(mock_service)
|
||||
|
||||
metadata = {
|
||||
"native_filter_configuration": [
|
||||
{"targets": [{"chartId": 42}]}
|
||||
]
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
fp = _write_dashboard_yaml(Path(td), metadata)
|
||||
source_map = {42: "uuid-chart-A"}
|
||||
|
||||
engine._patch_dashboard_metadata(fp, "target-env", source_map)
|
||||
|
||||
with open(fp, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
result = json.loads(data["json_metadata"])
|
||||
assert result["native_filter_configuration"][0]["targets"][0]["chartId"] == 999
|
||||
|
||||
|
||||
def test_patch_dashboard_metadata_replaces_dataset_ids():
|
||||
"""Verifies that datasetId values are replaced using the mapping service."""
|
||||
mock_service = MockMappingService({"uuid-ds-B": 500})
|
||||
engine = MigrationEngine(mock_service)
|
||||
|
||||
metadata = {
|
||||
"native_filter_configuration": [
|
||||
{"targets": [{"datasetId": 10}]}
|
||||
]
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
fp = _write_dashboard_yaml(Path(td), metadata)
|
||||
source_map = {10: "uuid-ds-B"}
|
||||
|
||||
engine._patch_dashboard_metadata(fp, "target-env", source_map)
|
||||
|
||||
with open(fp, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
result = json.loads(data["json_metadata"])
|
||||
assert result["native_filter_configuration"][0]["targets"][0]["datasetId"] == 500
|
||||
|
||||
|
||||
def test_patch_dashboard_metadata_skips_when_no_metadata():
|
||||
"""Verifies early return when json_metadata key is absent."""
|
||||
mock_service = MockMappingService({})
|
||||
engine = MigrationEngine(mock_service)
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
fp = Path(td) / "dash.yaml"
|
||||
with open(fp, 'w') as f:
|
||||
yaml.dump({"title": "No metadata here"}, f)
|
||||
|
||||
engine._patch_dashboard_metadata(fp, "target-env", {})
|
||||
|
||||
with open(fp, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
assert "json_metadata" not in data
|
||||
|
||||
|
||||
def test_patch_dashboard_metadata_handles_missing_targets():
|
||||
"""When some source IDs have no target mapping, patches what it can and leaves the rest."""
|
||||
mock_service = MockMappingService({"uuid-A": 100}) # Only uuid-A maps
|
||||
engine = MigrationEngine(mock_service)
|
||||
|
||||
metadata = {
|
||||
"native_filter_configuration": [
|
||||
{"targets": [{"datasetId": 1}, {"datasetId": 2}]}
|
||||
]
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
fp = _write_dashboard_yaml(Path(td), metadata)
|
||||
source_map = {1: "uuid-A", 2: "uuid-MISSING"} # uuid-MISSING won't resolve
|
||||
|
||||
engine._patch_dashboard_metadata(fp, "target-env", source_map)
|
||||
|
||||
with open(fp, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
result = json.loads(data["json_metadata"])
|
||||
targets = result["native_filter_configuration"][0]["targets"]
|
||||
# ID 1 should be replaced to 100; ID 2 should remain 2
|
||||
assert targets[0]["datasetId"] == 100
|
||||
assert targets[1]["datasetId"] == 2
|
||||
|
||||
|
||||
# --- _extract_chart_uuids_from_archive tests ---
|
||||
|
||||
def test_extract_chart_uuids_from_archive():
|
||||
"""Verifies that chart YAML files are parsed for id->uuid mappings."""
|
||||
engine = MigrationEngine()
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
charts_dir = Path(td) / "charts"
|
||||
charts_dir.mkdir()
|
||||
|
||||
chart1 = {"id": 42, "uuid": "uuid-42", "slice_name": "Chart One"}
|
||||
chart2 = {"id": 99, "uuid": "uuid-99", "slice_name": "Chart Two"}
|
||||
|
||||
with open(charts_dir / "chart1.yaml", 'w') as f:
|
||||
yaml.dump(chart1, f)
|
||||
with open(charts_dir / "chart2.yaml", 'w') as f:
|
||||
yaml.dump(chart2, f)
|
||||
|
||||
result = engine._extract_chart_uuids_from_archive(Path(td))
|
||||
|
||||
assert result == {42: "uuid-42", 99: "uuid-99"}
|
||||
|
||||
|
||||
# --- _transform_yaml tests ---
|
||||
|
||||
def test_transform_yaml_replaces_database_uuid():
|
||||
"""Verifies that database_uuid in a dataset YAML is replaced."""
|
||||
engine = MigrationEngine()
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
fp = Path(td) / "dataset.yaml"
|
||||
with open(fp, 'w') as f:
|
||||
yaml.dump({"database_uuid": "source-uuid-abc", "table_name": "my_table"}, f)
|
||||
|
||||
engine._transform_yaml(fp, {"source-uuid-abc": "target-uuid-xyz"})
|
||||
|
||||
with open(fp, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
assert data["database_uuid"] == "target-uuid-xyz"
|
||||
assert data["table_name"] == "my_table"
|
||||
|
||||
|
||||
def test_transform_yaml_ignores_unmapped_uuid():
|
||||
"""Verifies no changes when UUID is not in the mapping."""
|
||||
engine = MigrationEngine()
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
fp = Path(td) / "dataset.yaml"
|
||||
original = {"database_uuid": "unknown-uuid", "table_name": "test"}
|
||||
with open(fp, 'w') as f:
|
||||
yaml.dump(original, f)
|
||||
|
||||
engine._transform_yaml(fp, {"other-uuid": "replacement"})
|
||||
|
||||
with open(fp, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
assert data["database_uuid"] == "unknown-uuid"
|
||||
|
||||
|
||||
# --- [NEW] transform_zip E2E tests ---
|
||||
|
||||
def test_transform_zip_end_to_end():
|
||||
"""Verifies full orchestration: extraction, transformation, patching, and re-packaging."""
|
||||
mock_service = MockMappingService({"char-uuid": 101, "ds-uuid": 202})
|
||||
engine = MigrationEngine(mock_service)
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
td_path = Path(td)
|
||||
zip_path = td_path / "source.zip"
|
||||
output_path = td_path / "target.zip"
|
||||
|
||||
# Create source ZIP structure
|
||||
with tempfile.TemporaryDirectory() as src_dir:
|
||||
src_path = Path(src_dir)
|
||||
|
||||
# 1. Dataset
|
||||
ds_dir = src_path / "datasets"
|
||||
ds_dir.mkdir()
|
||||
with open(ds_dir / "ds.yaml", 'w') as f:
|
||||
yaml.dump({"database_uuid": "source-db-uuid", "table_name": "users"}, f)
|
||||
|
||||
# 2. Chart
|
||||
ch_dir = src_path / "charts"
|
||||
ch_dir.mkdir()
|
||||
with open(ch_dir / "ch.yaml", 'w') as f:
|
||||
yaml.dump({"id": 10, "uuid": "char-uuid"}, f)
|
||||
|
||||
# 3. Dashboard
|
||||
db_dir = src_path / "dashboards"
|
||||
db_dir.mkdir()
|
||||
metadata = {"native_filter_configuration": [{"targets": [{"chartId": 10}]}]}
|
||||
with open(db_dir / "db.yaml", 'w') as f:
|
||||
yaml.dump({"json_metadata": json.dumps(metadata)}, f)
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'w') as zf:
|
||||
for root, _, files in os.walk(src_path):
|
||||
for file in files:
|
||||
p = Path(root) / file
|
||||
zf.write(p, p.relative_to(src_path))
|
||||
|
||||
db_mapping = {"source-db-uuid": "target-db-uuid"}
|
||||
|
||||
# Execute transform
|
||||
success = engine.transform_zip(
|
||||
str(zip_path),
|
||||
str(output_path),
|
||||
db_mapping,
|
||||
target_env_id="test-target",
|
||||
fix_cross_filters=True
|
||||
)
|
||||
|
||||
assert success is True
|
||||
assert output_path.exists()
|
||||
|
||||
# Verify contents
|
||||
with tempfile.TemporaryDirectory() as out_dir:
|
||||
with zipfile.ZipFile(output_path, 'r') as zf:
|
||||
zf.extractall(out_dir)
|
||||
|
||||
out_path = Path(out_dir)
|
||||
|
||||
# Verify dataset transformation
|
||||
with open(out_path / "datasets" / "ds.yaml", 'r') as f:
|
||||
ds_data = yaml.safe_load(f)
|
||||
assert ds_data["database_uuid"] == "target-db-uuid"
|
||||
|
||||
# Verify dashboard patching
|
||||
with open(out_path / "dashboards" / "db.yaml", 'r') as f:
|
||||
db_data = yaml.safe_load(f)
|
||||
meta = json.loads(db_data["json_metadata"])
|
||||
assert meta["native_filter_configuration"][0]["targets"][0]["chartId"] == 101
|
||||
|
||||
|
||||
def test_transform_zip_invalid_path():
|
||||
"""@PRE: Verify behavior (False) on invalid ZIP path."""
|
||||
engine = MigrationEngine()
|
||||
success = engine.transform_zip("non_existent.zip", "output.zip", {})
|
||||
assert success is False
|
||||
|
||||
|
||||
def test_transform_yaml_nonexistent_file():
|
||||
"""@PRE: Verify behavior on non-existent YAML file."""
|
||||
engine = MigrationEngine()
|
||||
# Should log error and not crash (implemented via try-except if wrapped,
|
||||
# but _transform_yaml itself might raise FileNotFoundError if not guarded)
|
||||
with pytest.raises(FileNotFoundError):
|
||||
engine._transform_yaml(Path("non_existent.yaml"), {})
|
||||
|
||||
|
||||
# [/DEF:backend.tests.core.test_migration_engine:Module]
|
||||
@@ -63,8 +63,8 @@ def test_belief_scope_error_handling(caplog):
|
||||
|
||||
log_messages = [record.message for record in caplog.records]
|
||||
|
||||
assert any("[FailingFunction][Entry]" in msg for msg in log_messages), "Entry log not found"
|
||||
assert any("[FailingFunction][Coherence:Failed]" in msg for msg in log_messages), "Failed coherence log not found"
|
||||
assert any("[FailingFunction][Entry]" in msg for msg in log_messages), f"Entry log not found. Logs: {log_messages}"
|
||||
assert any("[FailingFunction][COHERENCE:FAILED]" in msg for msg in log_messages), f"Failed coherence log not found. Logs: {log_messages}"
|
||||
# Exit should not be logged on failure
|
||||
|
||||
# Reset to INFO
|
||||
@@ -94,7 +94,7 @@ def test_belief_scope_success_coherence(caplog):
|
||||
|
||||
log_messages = [record.message for record in caplog.records]
|
||||
|
||||
assert any("[SuccessFunction][Coherence:OK]" in msg for msg in log_messages), "Success coherence log not found"
|
||||
assert any("[SuccessFunction][COHERENCE:OK]" in msg for msg in log_messages), f"Success coherence log not found. Logs: {log_messages}"
|
||||
|
||||
# Reset to INFO
|
||||
config = LoggingConfig(level="INFO", task_log_level="INFO", enable_belief_state=True)
|
||||
@@ -201,7 +201,7 @@ def test_enable_belief_state_flag(caplog):
|
||||
assert not any("[DisabledFunction][Entry]" in msg for msg in log_messages), "Entry should not be logged when disabled"
|
||||
assert not any("[DisabledFunction][Exit]" in msg for msg in log_messages), "Exit should not be logged when disabled"
|
||||
# Coherence:OK should still be logged (internal tracking)
|
||||
assert any("[DisabledFunction][Coherence:OK]" in msg for msg in log_messages), "Coherence should still be logged"
|
||||
assert any("[DisabledFunction][COHERENCE:OK]" in msg for msg in log_messages), "Coherence should still be logged"
|
||||
|
||||
# Re-enable for other tests
|
||||
config = LoggingConfig(
|
||||
|
||||
64
backend/tests/test_smoke_plugins.py
Normal file
64
backend/tests/test_smoke_plugins.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
# Mock database before any modules that import it are loaded
|
||||
mock_db = MagicMock()
|
||||
sys.modules['src.core.database'] = mock_db
|
||||
sys.modules['src.plugins.git_plugin.SessionLocal'] = mock_db.SessionLocal
|
||||
sys.modules['src.plugins.migration.SessionLocal'] = mock_db.SessionLocal
|
||||
|
||||
class TestPluginSmoke:
|
||||
"""Smoke tests for plugin loading and initialization."""
|
||||
|
||||
def test_plugins_load_successfully(self):
|
||||
"""
|
||||
Verify that all standard plugins can be discovered and instantiated
|
||||
by the PluginLoader without throwing errors (e.g., missing imports,
|
||||
syntax errors, missing class declarations).
|
||||
"""
|
||||
from src.core.plugin_loader import PluginLoader
|
||||
|
||||
plugin_dir = os.path.join(str(Path(__file__).parent.parent), "src", "plugins")
|
||||
|
||||
# This will discover and instantiate plugins
|
||||
loader = PluginLoader(plugin_dir)
|
||||
|
||||
plugins = loader.get_all_plugin_configs()
|
||||
plugin_ids = {p.id for p in plugins}
|
||||
|
||||
# We expect at least the migration and git plugins to be present
|
||||
expected_plugins = {"superset-migration", "git-integration"}
|
||||
|
||||
missing_plugins = expected_plugins - plugin_ids
|
||||
assert not missing_plugins, f"Missing expected plugins: {missing_plugins}"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_task_manager_initializes_with_plugins(self):
|
||||
"""
|
||||
Verify that the TaskManager can initialize with the real PluginLoader.
|
||||
"""
|
||||
from src.core.plugin_loader import PluginLoader
|
||||
from src.core.task_manager.manager import TaskManager
|
||||
|
||||
plugin_dir = os.path.join(str(Path(__file__).parent.parent), "src", "plugins")
|
||||
loader = PluginLoader(plugin_dir)
|
||||
|
||||
# Initialize TaskManager with real loader
|
||||
with patch("src.core.task_manager.manager.TaskPersistenceService") as MockPersistence, \
|
||||
patch("src.core.task_manager.manager.TaskLogPersistenceService"):
|
||||
|
||||
MockPersistence.return_value.load_tasks.return_value = []
|
||||
|
||||
with patch("src.dependencies.config_manager"):
|
||||
manager = TaskManager(loader)
|
||||
|
||||
# Stop the flusher thread to prevent hanging
|
||||
manager._flusher_stop_event.set()
|
||||
manager._flusher_thread.join(timeout=2)
|
||||
|
||||
assert manager is not None
|
||||
27
check_test_data.py
Normal file
27
check_test_data.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
|
||||
def check_file(filepath):
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if '@TIER: CRITICAL' in content:
|
||||
if '@TEST_DATA' not in content:
|
||||
return filepath
|
||||
except Exception as e:
|
||||
print(f"Error reading {filepath}: {e}")
|
||||
return None
|
||||
|
||||
missing_files = []
|
||||
for root_dir in ['backend/src', 'frontend/src']:
|
||||
for dirpath, _, filenames in os.walk(root_dir):
|
||||
for name in filenames:
|
||||
ext = os.path.splitext(name)[1]
|
||||
if ext in ['.py', '.js', '.ts', '.svelte']:
|
||||
full_path = os.path.join(dirpath, name)
|
||||
res = check_file(full_path)
|
||||
if res:
|
||||
missing_files.append(res)
|
||||
|
||||
print("Files missing @TEST_DATA:")
|
||||
for f in missing_files:
|
||||
print(f)
|
||||
@@ -1,55 +0,0 @@
|
||||
slice_name: "FI-0083 \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430\
|
||||
\ \u043F\u043E \u0414\u0417/\u041F\u0414\u0417"
|
||||
description: null
|
||||
certified_by: null
|
||||
certification_details: null
|
||||
viz_type: pivot_table_v2
|
||||
params:
|
||||
datasource: 859__table
|
||||
viz_type: pivot_table_v2
|
||||
slice_id: 4019
|
||||
groupbyColumns:
|
||||
- dt
|
||||
groupbyRows:
|
||||
- counterparty_search_name
|
||||
- attribute
|
||||
time_grain_sqla: P1M
|
||||
temporal_columns_lookup:
|
||||
dt: true
|
||||
metrics:
|
||||
- m_debt_amount
|
||||
- m_overdue_amount
|
||||
metricsLayout: COLUMNS
|
||||
adhoc_filters:
|
||||
- clause: WHERE
|
||||
comparator: No filter
|
||||
expressionType: SIMPLE
|
||||
operator: TEMPORAL_RANGE
|
||||
subject: dt
|
||||
row_limit: '90000'
|
||||
order_desc: false
|
||||
aggregateFunction: Sum
|
||||
combineMetric: true
|
||||
valueFormat: SMART_NUMBER
|
||||
date_format: smart_date
|
||||
rowOrder: key_a_to_z
|
||||
colOrder: key_a_to_z
|
||||
value_font_size: 12
|
||||
header_font_size: 12
|
||||
label_align: left
|
||||
column_config:
|
||||
m_debt_amount:
|
||||
d3NumberFormat: ',d'
|
||||
m_overdue_amount:
|
||||
d3NumberFormat: ',d'
|
||||
conditional_formatting: []
|
||||
extra_form_data: {}
|
||||
dashboards:
|
||||
- 184
|
||||
query_context: '{"datasource":{"id":859,"type":"table"},"force":false,"queries":[{"filters":[{"col":"dt","op":"TEMPORAL_RANGE","val":"No
|
||||
filter"}],"extras":{"having":"","where":""},"applied_time_extras":{},"columns":[{"timeGrain":"P1M","columnType":"BASE_AXIS","sqlExpression":"dt","label":"dt","expressionType":"SQL"},"counterparty_search_name","attribute"],"metrics":["m_debt_amount","m_overdue_amount"],"orderby":[["m_debt_amount",true]],"annotation_layers":[],"row_limit":90000,"series_limit":0,"order_desc":false,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"859__table","viz_type":"pivot_table_v2","slice_id":4019,"groupbyColumns":["dt"],"groupbyRows":["counterparty_search_name","attribute"],"time_grain_sqla":"P1M","temporal_columns_lookup":{"dt":true},"metrics":["m_debt_amount","m_overdue_amount"],"metricsLayout":"COLUMNS","adhoc_filters":[{"clause":"WHERE","comparator":"No
|
||||
filter","expressionType":"SIMPLE","operator":"TEMPORAL_RANGE","subject":"dt"}],"row_limit":"90000","order_desc":false,"aggregateFunction":"Sum","combineMetric":true,"valueFormat":"SMART_NUMBER","date_format":"smart_date","rowOrder":"key_a_to_z","colOrder":"key_a_to_z","value_font_size":12,"header_font_size":12,"label_align":"left","column_config":{"m_debt_amount":{"d3NumberFormat":",d"},"m_overdue_amount":{"d3NumberFormat":",d"}},"conditional_formatting":[],"extra_form_data":{},"dashboards":[184],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}'
|
||||
cache_timeout: null
|
||||
uuid: 9c293065-73e2-4d9b-a175-d188ff8ef575
|
||||
version: 1.0.0
|
||||
dataset_uuid: 9e645dc0-da25-4f61-9465-6e649b0bc4b1
|
||||
@@ -1,13 +0,0 @@
|
||||
database_name: Prod Clickhouse
|
||||
sqlalchemy_uri: clickhousedb+connect://viz_superset_click_prod:XXXXXXXXXX@rgm-s-khclk.hq.root.ad:443/dm
|
||||
cache_timeout: null
|
||||
expose_in_sqllab: true
|
||||
allow_run_async: false
|
||||
allow_ctas: false
|
||||
allow_cvas: false
|
||||
allow_dml: true
|
||||
allow_file_upload: false
|
||||
extra:
|
||||
allows_virtual_table_explore: true
|
||||
uuid: 97aced68-326a-4094-b381-27980560efa9
|
||||
version: 1.0.0
|
||||
@@ -1,119 +0,0 @@
|
||||
table_name: "FI-0080-06 \u041A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u044C (\u041E\
|
||||
\u0431\u0449\u0438\u0439 \u0441\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\
|
||||
)"
|
||||
main_dttm_col: null
|
||||
description: null
|
||||
default_endpoint: null
|
||||
offset: 0
|
||||
cache_timeout: null
|
||||
schema: dm_view
|
||||
sql: "-- [HEADER]\r\n-- [\u041D\u0410\u0417\u041D\u0410\u0427\u0415\u041D\u0418\u0415\
|
||||
]: \u041F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u0435 \u0434\u0438\u0430\u043F\
|
||||
\u0430\u0437\u043E\u043D\u0430 \u0434\u0430\u0442 \u0434\u043B\u044F \u043E\u0442\
|
||||
\u0447\u0435\u0442\u0430 \u043E \u0437\u0430\u0434\u043E\u043B\u0436\u0435\u043D\
|
||||
\u043D\u043E\u0441\u0442\u044F\u0445 \u043F\u043E \u043E\u0431\u043E\u0440\u043E\
|
||||
\u0442\u043D\u044B\u043C \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043C\r\
|
||||
\n-- [\u041A\u041B\u042E\u0427\u0415\u0412\u042B\u0415 \u041A\u041E\u041B\u041E\u041D\
|
||||
\u041A\u0418]:\r\n-- - from_dt_txt: \u041D\u0430\u0447\u0430\u043B\u044C\u043D\
|
||||
\u0430\u044F \u0434\u0430\u0442\u0430 \u0432 \u0444\u043E\u0440\u043C\u0430\u0442\
|
||||
\u0435 DD.MM.YYYY\r\n-- - to_dt_txt: \u041A\u043E\u043D\u0435\u0447\u043D\u0430\
|
||||
\u044F \u0434\u0430\u0442\u0430 \u0432 \u0444\u043E\u0440\u043C\u0430\u0442\u0435\
|
||||
\ DD.MM.YYYY\r\n-- [JINJA \u041F\u0410\u0420\u0410\u041C\u0415\u0422\u0420\u042B\
|
||||
]:\r\n-- - {{ filter_values(\"yes_no_check\") }}: \u0424\u0438\u043B\u044C\u0442\
|
||||
\u0440 \"\u0414\u0430/\u041D\u0435\u0442\" \u0434\u043B\u044F \u043E\u0433\u0440\
|
||||
\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u044F \u0432\u044B\u0431\u043E\u0440\u043A\
|
||||
\u0438 \u043F\u043E \u0434\u0430\u0442\u0435\r\n-- [\u041B\u041E\u0413\u0418\u041A\
|
||||
\u0410]: \u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u043F\u043E\
|
||||
\u0440\u043E\u0433\u043E\u0432\u0443\u044E \u0434\u0430\u0442\u0443 \u0432 \u0437\
|
||||
\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u043E\u0442 \u0442\
|
||||
\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u0434\u043D\u044F \u043C\u0435\u0441\
|
||||
\u044F\u0446\u0430 \u0438 \u0444\u0438\u043B\u044C\u0442\u0440\u0443\u0435\u0442\
|
||||
\ \u0434\u0430\u043D\u043D\u044B\u0435\r\n\r\nWITH date_threshold AS (\r\n SELECT\
|
||||
\ \r\n -- \u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u043C \u043F\
|
||||
\u043E\u0440\u043E\u0433\u043E\u0432\u0443\u044E \u0434\u0430\u0442\u0443 \u0432\
|
||||
\ \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u043E\u0442\
|
||||
\ \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u0434\u043D\u044F \r\n \
|
||||
\ CASE \r\n WHEN toDayOfMonth(now()) <= 10 THEN \r\n \
|
||||
\ toStartOfMonth(dateSub(MONTH, 1, now())) \r\n ELSE \r\n \
|
||||
\ toStartOfMonth(now()) \r\n END AS cutoff_date \r\n),\r\nfiltered_dates\
|
||||
\ AS (\r\n SELECT \r\n dt,\r\n formatDateTime(dt, '%d.%m.%Y') AS\
|
||||
\ from_dt_txt,\r\n formatDateTime(dt, '%d.%m.%Y') AS to_dt_txt\r\n \
|
||||
\ --dt as from_dt_txt,\r\n -- dt as to_dt_txt\r\n FROM dm_view.account_debt_for_working_capital_final\r\
|
||||
\n WHERE 1=1\r\n -- \u0411\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u0430\
|
||||
\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0444\u0438\u043B\u044C\
|
||||
\u0442\u0440\u0430\r\n {% if filter_values(\"yes_no_check\") | length !=\
|
||||
\ 0 %}\r\n {% if filter_values(\"yes_no_check\")[0] == \"\u0414\u0430\
|
||||
\" %}\r\n AND dt < (SELECT cutoff_date FROM date_threshold)\r\n \
|
||||
\ {% endif %}\r\n {% endif %}\r\n)\r\nSELECT \r\ndt,\r\n from_dt_txt,\r\
|
||||
\n to_dt_txt,\r\n formatDateTime(toLastDayOfMonth(dt), '%d.%m.%Y') as last_day_of_month_dt_txt\r\
|
||||
\nFROM \r\n filtered_dates\r\nGROUP BY \r\n dt, from_dt_txt, to_dt_txt\r\n\
|
||||
ORDER BY \r\n dt DESC"
|
||||
params: null
|
||||
template_params: null
|
||||
filter_select_enabled: true
|
||||
fetch_values_predicate: null
|
||||
extra: null
|
||||
normalize_columns: false
|
||||
uuid: fca62707-6947-4440-a16b-70cb6a5cea5b
|
||||
metrics:
|
||||
- metric_name: max_date
|
||||
verbose_name: max_date
|
||||
metric_type: count
|
||||
expression: max(dt)
|
||||
description: null
|
||||
d3format: null
|
||||
currency: null
|
||||
extra:
|
||||
warning_markdown: ''
|
||||
warning_text: null
|
||||
columns:
|
||||
- column_name: from_dt_txt
|
||||
verbose_name: null
|
||||
is_dttm: true
|
||||
is_active: true
|
||||
type: String
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: '%Y'
|
||||
extra: {}
|
||||
- column_name: dt
|
||||
verbose_name: null
|
||||
is_dttm: true
|
||||
is_active: true
|
||||
type: Date
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra: {}
|
||||
- column_name: last_day_of_month_dt_txt
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: String
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra: {}
|
||||
- column_name: to_dt_txt
|
||||
verbose_name: null
|
||||
is_dttm: true
|
||||
is_active: true
|
||||
type: String
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra: {}
|
||||
version: 1.0.0
|
||||
database_uuid: 97aced68-326a-4094-b381-27980560efa9
|
||||
@@ -1,190 +0,0 @@
|
||||
table_name: "FI-0090 \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430\
|
||||
\ \u043F\u043E \u0414\u0417/\u041F\u0414\u0417"
|
||||
main_dttm_col: dt
|
||||
description: null
|
||||
default_endpoint: null
|
||||
offset: 0
|
||||
cache_timeout: null
|
||||
schema: dm_view
|
||||
sql: "-- [JINJA_BLOCK] \u0426\u0435\u043D\u0442\u0440\u0430\u043B\u0438\u0437\u043E\
|
||||
\u0432\u0430\u043D\u043D\u043E\u0435 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\
|
||||
\u043D\u0438\u0435 \u0432\u0441\u0435\u0445 Jinja \u043F\u0435\u0440\u0435\u043C\
|
||||
\u0435\u043D\u043D\u044B\u0445\r\n{% set raw_to = filter_values('last_day_of_month_dt_txt')[0]\
|
||||
\ \r\n if filter_values('last_day_of_month_dt_txt') else '01.05.2025'\
|
||||
\ %}\r\n\r\n{# \u0440\u0430\u0437\u0431\u0438\u0432\u0430\u0435\u043C \xABDD.MM.YYYY\xBB\
|
||||
\ \u043D\u0430 \u0447\u0430\u0441\u0442\u0438 #}\r\n{% set to_parts = raw_to.split('.')\
|
||||
\ %}\r\n\r\n{# \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u043C ISO\u2011\u0441\u0442\
|
||||
\u0440\u043E\u043A\u0443 \xABYYYY-MM-DD\xBB #}\r\n{% set to_dt = to_parts[2] \
|
||||
\ ~ '-' ~ to_parts[1] ~ '-' ~ to_parts[0] %}\r\n\r\nwith \r\ncp_relations_type\
|
||||
\ AS (\r\n select * from ( SELECT \r\n ctd.counterparty_code AS counterparty_code,\r\
|
||||
\n min(dt_from) as dt_from,\r\n max(dt_to) as dt_to,\r\n crt.relation_type_code\
|
||||
\ || ' ' || crt.relation_type_name AS relation_type_code_name\r\n FROM\r\n \
|
||||
\ dm_view.counterparty_td ctd\r\n JOIN dm_view.counterparty_relation_type_texts\
|
||||
\ crt \r\n ON ctd.relation_type_code = crt.relation_type_code\r\n GROUP\
|
||||
\ BY\r\n ctd.counterparty_code, ctd.counterparty_full_name,\r\n crt.relation_type_code,crt.relation_type_name)\r\
|
||||
\n WHERE \r\n dt_from <= toDate('{{to_dt }}') AND \r\n \
|
||||
\ dt_to >= toDate('{{to_dt }}')\r\n ),\r\nt_debt as \r\n(SELECT dt, \r\n\
|
||||
counterparty_search_name,\r\ncp_relations_type.relation_type_code_name as relation_type_code_name,\r\
|
||||
\nunit_balance_code || ' ' || unit_balance_name as unit_balance_code_name,\r\n'1.\
|
||||
\ \u0421\u0443\u043C\u043C\u0430' as attribute,\r\nsum(debt_balance_subposition_no_revaluation_usd_amount)\
|
||||
\ as debt_amount,\r\nsumIf(debt_balance_subposition_no_revaluation_usd_amount,dt_overdue\
|
||||
\ < dt) as overdue_amount\r\nfrom dm_view.account_debt_for_working_capital t_debt\r\
|
||||
\njoin cp_relations_type ON\r\ncp_relations_type.counterparty_code = t_debt.counterparty_code\r\
|
||||
\nwhere dt = toLastDayOfMonth(dt)\r\nand match(general_ledger_account_code,'((62)|(60)|(76))')\r\
|
||||
\nand debit_or_credit = 'S'\r\nand account_type = 'D'\r\nand dt between addMonths(toDate('{{to_dt\
|
||||
\ }}'),-12) and toDate('{{to_dt }}')\r\ngroup by dt, counterparty_search_name,unit_balance_code_name,relation_type_code_name\r\
|
||||
\n),\r\n\r\nt_transaction_count_base as \r\n(\r\nselect *,\r\ncp_relations_type.relation_type_code_name\
|
||||
\ as relation_type_code_name,\r\nunit_balance_code || ' ' || unit_balance_name as\
|
||||
\ unit_balance_code_name,\r\n case when dt_overdue<dt_clearing then\r\n \
|
||||
\ dateDiff(day, dt_overdue, dt_clearing) \r\n else 0\r\n end\
|
||||
\ as overdue_days\r\nfrom dm_view.accounting_documents_leading_to_debt t_docs\r\n\
|
||||
join cp_relations_type ON\r\ncp_relations_type.counterparty_code = t_docs.counterparty_code\r\
|
||||
\nwhere 1=1\r\n\r\nand match(general_ledger_account_code,'((62)|(60)|(76))')\r\n\
|
||||
and debit_or_credit = 'S'\r\nand account_type = 'D'\r\n)\r\n\r\nselect * from t_debt\r\
|
||||
\n\r\nunion all \r\n\r\nselect toLastDayOfMonth(dt_debt) as dt, \r\ncounterparty_search_name,\r\
|
||||
\nrelation_type_code_name,\r\nunit_balance_code_name,\r\n'2. \u043A\u043E\u043B\u0438\
|
||||
\u0447\u0435\u0441\u0442\u0432\u043E \u0442\u0440\u0430\u043D\u0437\u0430\u043A\u0446\
|
||||
\u0438\u0439 \u0432 \u043C\u0435\u0441\u044F\u0446' as attribute,\r\ncount(1) as\
|
||||
\ debt_amount,\r\nnull as overdue_amount\r\nfrom t_transaction_count_base\r\nwhere\
|
||||
\ dt_debt between addMonths(toDate('{{to_dt }}'),-12) and toDate('{{to_dt }}')\r\
|
||||
\ngroup by toLastDayOfMonth(dt_debt), \r\ncounterparty_search_name,\r\nrelation_type_code_name,\r\
|
||||
\nunit_balance_code_name,attribute\r\n\r\nunion all \r\n\r\nselect toLastDayOfMonth(dt_clearing)\
|
||||
\ as dt, \r\ncounterparty_search_name,\r\nrelation_type_code_name,\r\nunit_balance_code_name,\r\
|
||||
\n'2. \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0442\u0440\u0430\
|
||||
\u043D\u0437\u0430\u043A\u0446\u0438\u0439 \u0432 \u043C\u0435\u0441\u044F\u0446\
|
||||
' as attribute,\r\nnull as debt_amount,\r\ncount(1) as overdue_amount\r\nfrom t_transaction_count_base\r\
|
||||
\nwhere dt_clearing between addMonths(toDate('{{to_dt }}'),-12) and toDate('{{to_dt\
|
||||
\ }}')\r\nand overdue_days > 0\r\ngroup by toLastDayOfMonth(dt_clearing), \r\ncounterparty_search_name,\r\
|
||||
\nrelation_type_code_name,\r\nunit_balance_code_name,attribute\r\n\r\nunion all\
|
||||
\ \r\n\r\nselect toLastDayOfMonth(dt_clearing) as dt, \r\ncounterparty_search_name,\r\
|
||||
\nrelation_type_code_name,\r\nunit_balance_code_name,\r\nmultiIf(\r\noverdue_days\
|
||||
\ < 30,'3. \u0434\u043E 30',\r\noverdue_days between 30 and 60, '4. \u043E\u0442\
|
||||
\ 30 \u0434\u043E 60',\r\noverdue_days between 61 and 90, '5. \u043E\u0442 61 \u0434\
|
||||
\u043E 90',\r\noverdue_days>90,'6. \u0431\u043E\u043B\u0435\u0435 90 \u0434\u043D\
|
||||
',\r\nnull\r\n)\r\n as attribute,\r\nnull as debt_amount,\r\ncount(1) as overdue_amount\r\
|
||||
\nfrom t_transaction_count_base\r\nwhere dt_clearing between addMonths(toDate('{{to_dt\
|
||||
\ }}'),-12) and toDate('{{to_dt }}')\r\nand overdue_days > 0\r\ngroup by toLastDayOfMonth(dt_clearing),\
|
||||
\ \r\ncounterparty_search_name,\r\nrelation_type_code_name,\r\nattribute,unit_balance_code_name,attribute\r\
|
||||
\n"
|
||||
params: null
|
||||
template_params: null
|
||||
filter_select_enabled: true
|
||||
fetch_values_predicate: null
|
||||
extra: null
|
||||
normalize_columns: false
|
||||
uuid: 9e645dc0-da25-4f61-9465-6e649b0bc4b1
|
||||
metrics:
|
||||
- metric_name: m_debt_amount
|
||||
verbose_name: "\u0414\u0417, $"
|
||||
metric_type: count
|
||||
expression: sum(debt_amount)
|
||||
description: null
|
||||
d3format: null
|
||||
currency: null
|
||||
extra:
|
||||
warning_markdown: ''
|
||||
warning_text: null
|
||||
- metric_name: m_overdue_amount
|
||||
verbose_name: "\u041F\u0414\u0417, $"
|
||||
metric_type: null
|
||||
expression: sum(overdue_amount)
|
||||
description: null
|
||||
d3format: null
|
||||
currency: null
|
||||
extra:
|
||||
warning_markdown: ''
|
||||
warning_text: null
|
||||
columns:
|
||||
- column_name: debt_amount
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: Nullable(Decimal(38, 2))
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra:
|
||||
warning_markdown: null
|
||||
- column_name: overdue_amount
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: Nullable(Decimal(38, 2))
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra:
|
||||
warning_markdown: null
|
||||
- column_name: dt
|
||||
verbose_name: null
|
||||
is_dttm: true
|
||||
is_active: true
|
||||
type: Nullable(Date)
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra:
|
||||
warning_markdown: null
|
||||
- column_name: unit_balance_code_name
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: Nullable(String)
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra:
|
||||
warning_markdown: null
|
||||
- column_name: relation_type_code_name
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: Nullable(String)
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra:
|
||||
warning_markdown: null
|
||||
- column_name: counterparty_search_name
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: Nullable(String)
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra:
|
||||
warning_markdown: null
|
||||
- column_name: attribute
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: Nullable(String)
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
extra:
|
||||
warning_markdown: null
|
||||
version: 1.0.0
|
||||
database_uuid: 97aced68-326a-4094-b381-27980560efa9
|
||||
@@ -1,3 +0,0 @@
|
||||
version: 1.0.0
|
||||
type: Dashboard
|
||||
timestamp: '2026-01-14T11:21:08.078620+00:00'
|
||||
@@ -36,6 +36,7 @@ services:
|
||||
- "${BACKEND_HOST_PORT:-8001}:8000"
|
||||
volumes:
|
||||
- ./config.json:/app/config.json
|
||||
- ../ss-tools-storage:/home/busya/dev/ss-tools-storage
|
||||
- ./backups:/app/backups
|
||||
- ./backend/git_repos:/app/backend/git_repos
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
<!-- Pagination Controls -->
|
||||
<div class="flex items-center justify-between mt-6">
|
||||
<div class="text-sm text-gray-500">
|
||||
{($t.dashboard?.showing || "")
|
||||
{($t.dashboard?.showing )
|
||||
.replace("{start}", (currentPage * pageSize + 1).toString())
|
||||
.replace(
|
||||
"{end}",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
value={selectedId}
|
||||
on:change={handleSelect}
|
||||
>
|
||||
<option value="" disabled>{$t.common?.choose_environment || "-- Choose an environment --"}</option>
|
||||
<option value="" disabled>{$t.common?.choose_environment}</option>
|
||||
{#each environments as env}
|
||||
<option value={env.id}>{env.name} ({env.url})</option>
|
||||
{/each}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<script lang="ts">
|
||||
// [SECTION: IMPORTS]
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
@@ -62,9 +63,9 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Source Database</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Target Database</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.dashboard?.source_database }</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.dashboard?.target_database }</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.dashboard?.status }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@@ -81,7 +82,7 @@
|
||||
value={mapping?.target_db_uuid || suggestion?.target_db_uuid || ""}
|
||||
on:change={(e) => updateMapping(sDb.uuid, (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="">-- Select Target --</option>
|
||||
<option value="">{$t.migration?.target_env }</option>
|
||||
{#each targetDatabases as tDb}
|
||||
<option value={tDb.uuid}>{tDb.database_name}</option>
|
||||
{/each}
|
||||
@@ -89,11 +90,11 @@
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{#if mapping}
|
||||
<span class="text-blue-600 font-semibold">Saved</span>
|
||||
<span class="text-blue-600 font-semibold">{$t.dashboard?.saved }</span>
|
||||
{:else if suggestion}
|
||||
<span class="text-green-600 font-semibold">Suggested ({Math.round(suggestion.confidence * 100)}%)</span>
|
||||
<span class="text-green-600 font-semibold">{$t.dashboard?.suggested } ({Math.round(suggestion.confidence * 100)}%)</span>
|
||||
{:else}
|
||||
<span class="text-red-600">Unmapped</span>
|
||||
<span class="text-red-600">{$t.dashboard?.not_mapped }</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { selectedTask } from '../lib/stores.js';
|
||||
import { api } from '../lib/api.js';
|
||||
import { t } from '../lib/i18n';
|
||||
|
||||
let tasks = [];
|
||||
let loading = true;
|
||||
@@ -51,7 +52,7 @@
|
||||
// @PRE: User confirms deletion via prompt.
|
||||
// @POST: Tasks are deleted from backend and list is re-fetched.
|
||||
async function clearTasks(status = null) {
|
||||
if (!confirm('Are you sure you want to clear tasks?')) return;
|
||||
if (!confirm($t.tasks?.clear_confirm )) return;
|
||||
try {
|
||||
let endpoint = '/tasks';
|
||||
if (status) endpoint += `?status=${status}`;
|
||||
@@ -74,7 +75,7 @@
|
||||
const fullTask = await api.getTask(task.id);
|
||||
selectedTask.set(fullTask);
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch full task details:", e);
|
||||
console.error($t.tasks?.fetch_task_details_failed , e);
|
||||
selectedTask.set(task);
|
||||
}
|
||||
}
|
||||
@@ -119,21 +120,21 @@
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg mb-8">
|
||||
<div class="px-4 py-5 sm:px-6 flex justify-between items-center">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
Recent Tasks
|
||||
{$t.tasks?.recent }
|
||||
</h3>
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div class="relative inline-block text-left group">
|
||||
<button class="text-sm text-red-600 hover:text-red-900 focus:outline-none flex items-center py-2">
|
||||
Clear Tasks
|
||||
{$t.tasks?.clear_tasks }
|
||||
<svg class="ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
<!-- Added a transparent bridge to prevent menu closing when moving cursor -->
|
||||
<div class="absolute h-2 w-full top-full left-0"></div>
|
||||
<div class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none hidden group-hover:block z-50">
|
||||
<div class="py-1">
|
||||
<button on:click={() => clearTasks()} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Clear All Non-Running</button>
|
||||
<button on:click={() => clearTasks('FAILED')} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Clear Failed</button>
|
||||
<button on:click={() => clearTasks('AWAITING_INPUT')} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Clear Awaiting Input</button>
|
||||
<button on:click={() => clearTasks()} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">{$t.tasks?.clear_non_running }</button>
|
||||
<button on:click={() => clearTasks('FAILED')} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">{$t.tasks?.clear_failed }</button>
|
||||
<button on:click={() => clearTasks('AWAITING_INPUT')} class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">{$t.tasks?.clear_awaiting_input }</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,17 +142,17 @@
|
||||
on:click={fetchTasks}
|
||||
class="text-sm text-indigo-600 hover:text-indigo-900 focus:outline-none"
|
||||
>
|
||||
Refresh
|
||||
{$t.common?.refresh }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading && tasks.length === 0}
|
||||
<div class="p-4 text-center text-gray-500">Loading tasks...</div>
|
||||
<div class="p-4 text-center text-gray-500">{$t.tasks?.loading }</div>
|
||||
{:else if error}
|
||||
<div class="p-4 text-center text-red-500">{error}</div>
|
||||
{:else if tasks.length === 0}
|
||||
<div class="p-4 text-center text-gray-500">No recent tasks found.</div>
|
||||
<div class="p-4 text-center text-gray-500">{$t.tasks?.no_tasks }</div>
|
||||
{:else}
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{#each tasks as task}
|
||||
@@ -179,13 +180,13 @@
|
||||
{#if task.params.from_env && task.params.to_env}
|
||||
{task.params.from_env} → {task.params.to_env}
|
||||
{:else}
|
||||
Params: {Object.keys(task.params).length} keys
|
||||
{$t.tasks?.parameters }: {Object.keys(task.params).length} {$t.tasks?.keys }
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
|
||||
<p>
|
||||
Started: {new Date(task.started_at || task.created_at || Date.now()).toLocaleString()}
|
||||
{$t.tasks?.started_label }: {new Date(task.started_at || task.created_at || Date.now()).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,4 +197,4 @@
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- [/DEF:TaskHistory:Component] -->
|
||||
<!-- [/DEF:TaskHistory:Component] -->
|
||||
|
||||
@@ -63,9 +63,9 @@
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
{#if loading && tasks.length === 0}
|
||||
<div class="p-4 text-center text-gray-500">{$t.tasks?.loading || 'Loading...'}</div>
|
||||
<div class="p-4 text-center text-gray-500">{$t.tasks?.loading}</div>
|
||||
{:else if tasks.length === 0}
|
||||
<div class="p-4 text-center text-gray-500">{$t.tasks?.no_tasks || 'No tasks found.'}</div>
|
||||
<div class="p-4 text-center text-gray-500">{$t.tasks?.no_tasks}</div>
|
||||
{:else}
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{#each tasks as task (task.id)}
|
||||
@@ -99,7 +99,7 @@
|
||||
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<p>
|
||||
{($t.tasks?.started || "").replace('{time}', formatTime(task.started_at))}
|
||||
{$t.tasks?.started?.replace('{time}', formatTime(task.started_at))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
<div
|
||||
class="w-5 h-5 border-2 border-terminal-border border-t-primary rounded-full animate-spin"
|
||||
></div>
|
||||
<span>{$t.tasks?.loading || "Loading logs..."}</span>
|
||||
<span>{$t.tasks?.loading }</span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div
|
||||
@@ -154,7 +154,7 @@
|
||||
<span>{error}</span>
|
||||
<button
|
||||
class="bg-terminal-surface text-terminal-text-subtle border border-terminal-border rounded-md px-3 py-1 text-xs cursor-pointer transition-all hover:bg-terminal-border hover:text-terminal-text-bright"
|
||||
onclick={handleRefresh}>Retry</button
|
||||
onclick={handleRefresh}>{$t.common?.retry }</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -203,7 +203,7 @@
|
||||
class="text-lg font-medium text-gray-100"
|
||||
id="modal-title"
|
||||
>
|
||||
{$t.tasks?.logs_title || "Task Logs"}
|
||||
{$t.tasks?.logs_title }
|
||||
</h3>
|
||||
<button
|
||||
class="text-gray-500 hover:text-gray-300"
|
||||
@@ -211,13 +211,13 @@
|
||||
show = false;
|
||||
dispatch("close");
|
||||
}}
|
||||
aria-label="Close">✕</button
|
||||
aria-label={$t.common?.close }>✕</button
|
||||
>
|
||||
</div>
|
||||
<div class="h-[500px]">
|
||||
{#if loading && logs.length === 0}
|
||||
<p class="text-gray-500 text-center">
|
||||
{$t.tasks?.loading || "Loading..."}
|
||||
{$t.tasks?.loading }
|
||||
</p>
|
||||
{:else if error}
|
||||
<p class="text-red-400 text-center">{error}</p>
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
import { addToast } from '../lib/toasts.js';
|
||||
import MissingMappingModal from './MissingMappingModal.svelte';
|
||||
import PasswordPrompt from './PasswordPrompt.svelte';
|
||||
import TaskLogPanel from './tasks/TaskLogPanel.svelte';
|
||||
import TaskLogPanel from './tasks/TaskLogPanel.svelte';
|
||||
import { t } from '../lib/i18n';
|
||||
// [/SECTION]
|
||||
|
||||
let ws;
|
||||
@@ -135,7 +136,7 @@
|
||||
}, delay);
|
||||
} else if (reconnectAttempts >= maxReconnectAttempts) {
|
||||
console.error('[TaskRunner][Coherence:Failed] Max reconnect attempts reached.');
|
||||
addToast('Failed to connect to log stream after multiple attempts.', 'error');
|
||||
addToast($t.tasks?.log_stream_failed, 'error');
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -219,10 +220,10 @@
|
||||
});
|
||||
|
||||
connectionStatus = 'connected';
|
||||
addToast('Mapping resolved, migration continuing...', 'success');
|
||||
} catch (e) {
|
||||
addToast('Failed to resolve mapping: ' + e.message, 'error');
|
||||
}
|
||||
addToast($t.tasks?.mapping_resolved, 'success');
|
||||
} catch (e) {
|
||||
addToast($t.tasks?.mapping_resolve_failed?.replace('{error}', e.message), 'error');
|
||||
}
|
||||
}
|
||||
// [/DEF:handleMappingResolve:Function]
|
||||
|
||||
@@ -241,10 +242,10 @@
|
||||
|
||||
showPasswordPrompt = false;
|
||||
connectionStatus = 'connected';
|
||||
addToast('Passwords submitted, resuming migration...', 'success');
|
||||
} catch (e) {
|
||||
addToast('Failed to resume task: ' + e.message, 'error');
|
||||
}
|
||||
addToast($t.tasks?.passwords_submitted, 'success');
|
||||
} catch (e) {
|
||||
addToast($t.tasks?.resume_failed?.replace('{error}', e.message), 'error');
|
||||
}
|
||||
}
|
||||
// [/DEF:handlePasswordResume:Function]
|
||||
|
||||
@@ -322,29 +323,29 @@
|
||||
<div class="p-4 border rounded-lg bg-white shadow-md">
|
||||
{#if $selectedTask}
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h2 class="text-xl font-semibold">Task: {$selectedTask.plugin_id}</h2>
|
||||
<h2 class="text-xl font-semibold">{$t.tasks?.task_label}: {$selectedTask.plugin_id}</h2>
|
||||
<div class="flex items-center space-x-2">
|
||||
{#if connectionStatus === 'connecting'}
|
||||
<span class="flex h-3 w-3 relative">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-yellow-500"></span>
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">Connecting...</span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.connecting}</span>
|
||||
{:else if connectionStatus === 'connected'}
|
||||
<span class="h-3 w-3 rounded-full bg-green-500"></span>
|
||||
<span class="text-xs text-gray-500">Live</span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.live}</span>
|
||||
{:else if connectionStatus === 'completed'}
|
||||
<span class="h-3 w-3 rounded-full bg-blue-500"></span>
|
||||
<span class="text-xs text-gray-500">Completed</span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.completed}</span>
|
||||
{:else if connectionStatus === 'awaiting_mapping'}
|
||||
<span class="h-3 w-3 rounded-full bg-orange-500 animate-pulse"></span>
|
||||
<span class="text-xs text-gray-500">Awaiting Mapping</span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.awaiting_mapping}</span>
|
||||
{:else if connectionStatus === 'awaiting_input'}
|
||||
<span class="h-3 w-3 rounded-full bg-orange-500 animate-pulse"></span>
|
||||
<span class="text-xs text-gray-500">Awaiting Input</span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.awaiting_input}</span>
|
||||
{:else}
|
||||
<span class="h-3 w-3 rounded-full bg-red-500"></span>
|
||||
<span class="text-xs text-gray-500">Disconnected</span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.disconnected}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -352,16 +353,16 @@
|
||||
<!-- Task Info Section -->
|
||||
<div class="mb-4 bg-gray-50 p-3 rounded text-sm border border-gray-200">
|
||||
<details open>
|
||||
<summary class="cursor-pointer font-medium text-gray-700 focus:outline-none hover:text-indigo-600">Task Details & Parameters</summary>
|
||||
<summary class="cursor-pointer font-medium text-gray-700 focus:outline-none hover:text-indigo-600">{$t.tasks?.details_parameters}</summary>
|
||||
<div class="mt-2 pl-2 border-l-2 border-indigo-200">
|
||||
<div class="grid grid-cols-2 gap-2 mb-2">
|
||||
<div><span class="font-semibold">ID:</span> <span class="text-gray-600">{$selectedTask.id}</span></div>
|
||||
<div><span class="font-semibold">Status:</span> <span class="text-gray-600">{$selectedTask.status}</span></div>
|
||||
<div><span class="font-semibold">Started:</span> <span class="text-gray-600">{new Date($selectedTask.started_at || $selectedTask.created_at || Date.now()).toLocaleString()}</span></div>
|
||||
<div><span class="font-semibold">Plugin:</span> <span class="text-gray-600">{$selectedTask.plugin_id}</span></div>
|
||||
<div><span class="font-semibold">{$t.common?.id}:</span> <span class="text-gray-600">{$selectedTask.id}</span></div>
|
||||
<div><span class="font-semibold">{$t.dashboard?.status}:</span> <span class="text-gray-600">{$selectedTask.status}</span></div>
|
||||
<div><span class="font-semibold">{$t.tasks?.started_label}:</span> <span class="text-gray-600">{new Date($selectedTask.started_at || $selectedTask.created_at || Date.now()).toLocaleString()}</span></div>
|
||||
<div><span class="font-semibold">{$t.tasks?.plugin}:</span> <span class="text-gray-600">{$selectedTask.plugin_id}</span></div>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<span class="font-semibold">Parameters:</span>
|
||||
<span class="font-semibold">{$t.tasks?.parameters}:</span>
|
||||
<pre class="text-xs bg-gray-100 p-2 rounded mt-1 overflow-x-auto border border-gray-200">{JSON.stringify($selectedTask.params, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -378,14 +379,14 @@
|
||||
|
||||
{#if waitingForData && connectionStatus === 'connected'}
|
||||
<div class="text-gray-500 italic mt-2 animate-pulse text-xs">
|
||||
Waiting for new logs...
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p>No task selected.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{$t.tasks?.waiting_logs}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p>{$t.tasks?.select_task}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<MissingMappingModal
|
||||
bind:show={showMappingModal}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// [DEF:frontend.src.components.__tests__.task_log_viewer:Module]
|
||||
// @TIER: CRITICAL
|
||||
// @TIER: STANDARD
|
||||
// @SEMANTICS: tests, task-log, viewer, mount, components
|
||||
// @PURPOSE: Unit tests for TaskLogViewer component by mounting it and observing the DOM.
|
||||
// @LAYER: UI (Tests)
|
||||
@@ -21,6 +21,9 @@ vi.mock('../../lib/i18n', () => ({
|
||||
fn({
|
||||
tasks: {
|
||||
loading: 'Loading...'
|
||||
},
|
||||
common: {
|
||||
retry: 'Retry'
|
||||
}
|
||||
});
|
||||
return () => { };
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
import { t } from '../../lib/i18n';
|
||||
import { Button } from '../../lib/ui';
|
||||
import type { Backup } from '../../types/backup';
|
||||
import { goto } from '$app/navigation';
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
@@ -21,13 +20,62 @@
|
||||
*/
|
||||
let {
|
||||
backups = [],
|
||||
currentPath = 'backups',
|
||||
onNavigate = (_path: string) => {},
|
||||
onNavigateUp = () => {},
|
||||
} = $props();
|
||||
|
||||
function isDirectory(backup: Backup): boolean {
|
||||
return Boolean(backup.is_directory);
|
||||
}
|
||||
|
||||
function formatCreatedAt(backup: Backup): string {
|
||||
return isDirectory(backup) ? '--' : new Date(backup.created_at).toLocaleString();
|
||||
}
|
||||
|
||||
function getPathParts(path: string): string[] {
|
||||
return path.split('/').filter(Boolean);
|
||||
}
|
||||
|
||||
function navigateToPathIndex(index: number) {
|
||||
const parts = getPathParts(currentPath);
|
||||
onNavigate(parts.slice(0, index + 1).join('/'));
|
||||
}
|
||||
|
||||
// [/SECTION]
|
||||
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="mb-3 flex flex-wrap items-center gap-2 text-sm text-gray-600">
|
||||
<button
|
||||
on:click={() => onNavigate('backups')}
|
||||
class="hover:text-indigo-600"
|
||||
>
|
||||
{$t.storage.root}
|
||||
</button>
|
||||
{#each getPathParts(currentPath).slice(1) as part, index}
|
||||
<span>/</span>
|
||||
<button
|
||||
on:click={() => navigateToPathIndex(index + 1)}
|
||||
class="hover:text-indigo-600"
|
||||
>
|
||||
{part}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
on:click={onNavigateUp}
|
||||
disabled={currentPath === 'backups'}
|
||||
>
|
||||
{$t.common?.back}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
@@ -50,23 +98,34 @@
|
||||
{#each backups as backup}
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{backup.name}
|
||||
{#if isDirectory(backup)}
|
||||
<button
|
||||
on:click={() => onNavigate(backup.path)}
|
||||
class="text-indigo-600 hover:text-indigo-900"
|
||||
>
|
||||
{backup.name}
|
||||
</button>
|
||||
{:else}
|
||||
{backup.name}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{backup.environment}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(backup.created_at).toLocaleString()}
|
||||
{formatCreatedAt(backup)}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-blue-600 hover:text-blue-900"
|
||||
on:click={() => goto(`/tools/storage?path=backups/${backup.name}`)}
|
||||
>
|
||||
{$t.storage.table.go_to_storage}
|
||||
</Button>
|
||||
{#if isDirectory(backup)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-blue-600 hover:text-blue-900"
|
||||
on:click={() => onNavigate(backup.path)}
|
||||
>
|
||||
{$t.storage.table.go_to_storage}
|
||||
</Button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
@@ -82,4 +141,4 @@
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
|
||||
<!-- [/DEF:BackupList:Component] -->
|
||||
<!-- [/DEF:BackupList:Component] -->
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
let loading = true;
|
||||
let creating = false;
|
||||
let savingSchedule = false;
|
||||
let currentPath = 'backups';
|
||||
|
||||
// Schedule state for selected environment
|
||||
let scheduleEnabled = false;
|
||||
@@ -51,23 +52,40 @@
|
||||
*/
|
||||
// @RELATION: CALLS -> api.getEnvironmentsList
|
||||
// @RELATION: CALLS -> api.requestApi
|
||||
function getSubpath(path: string): string {
|
||||
if (!path || path === 'backups') return '';
|
||||
return path.replace(/^backups\/?/, '');
|
||||
}
|
||||
|
||||
function normalizeBackupsPath(path: string): string {
|
||||
const trimmed = (path || '').trim().replace(/^\/+|\/+$/g, '');
|
||||
if (!trimmed) return 'backups';
|
||||
return trimmed.startsWith('backups') ? trimmed : `backups/${trimmed}`;
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
console.log("[BackupManager][Entry] Loading data.");
|
||||
loading = true;
|
||||
try {
|
||||
const subpath = getSubpath(currentPath);
|
||||
const filesUrl = subpath
|
||||
? `/storage/files?category=backups&path=${encodeURIComponent(subpath)}`
|
||||
: '/storage/files?category=backups';
|
||||
|
||||
const [envsData, storageData] = await Promise.all([
|
||||
api.getEnvironmentsList(),
|
||||
requestApi('/storage/files?category=backups')
|
||||
requestApi(filesUrl)
|
||||
]);
|
||||
environments = envsData;
|
||||
|
||||
// Map storage files to Backup type
|
||||
|
||||
backups = (storageData || []).map((file: any) => ({
|
||||
id: file.name,
|
||||
id: file.path,
|
||||
name: file.name,
|
||||
environment: file.path.split('/')[0] || 'Unknown',
|
||||
path: file.path,
|
||||
environment: file.path.split('/')[1] || $t.common?.unknown,
|
||||
created_at: file.created_at,
|
||||
size_bytes: file.size,
|
||||
is_directory: file.mime_type === 'directory',
|
||||
status: 'success'
|
||||
}));
|
||||
console.log("[BackupManager][Action] Data loaded successfully.");
|
||||
@@ -142,6 +160,19 @@
|
||||
}
|
||||
// [/DEF:handleCreateBackup:Function]
|
||||
|
||||
function handleNavigate(path: string) {
|
||||
currentPath = normalizeBackupsPath(path);
|
||||
loadData();
|
||||
}
|
||||
|
||||
function handleNavigateUp() {
|
||||
if (currentPath === 'backups') return;
|
||||
const parts = currentPath.split('/');
|
||||
parts.pop();
|
||||
currentPath = parts.join('/') || 'backups';
|
||||
loadData();
|
||||
}
|
||||
|
||||
onMount(loadData);
|
||||
</script>
|
||||
|
||||
@@ -232,10 +263,15 @@
|
||||
{#if loading}
|
||||
<div class="py-10 text-center text-gray-500">{$t.common.loading}</div>
|
||||
{:else}
|
||||
<BackupList {backups} />
|
||||
<BackupList
|
||||
{backups}
|
||||
{currentPath}
|
||||
onNavigate={handleNavigate}
|
||||
onNavigateUp={handleNavigateUp}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<!-- [/DEF:BackupManager:Component] -->
|
||||
<!-- [/DEF:BackupManager:Component] -->
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
console.log(`[BranchSelector][Coherence:OK] Loaded ${branches.length} branches`);
|
||||
} catch (e) {
|
||||
console.error(`[BranchSelector][Coherence:Failed] ${e.message}`);
|
||||
toast('Failed to load branches', 'error');
|
||||
toast($t.git?.load_branches_failed, 'error');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -90,7 +90,7 @@
|
||||
await gitService.checkoutBranch(dashboardId, branchName);
|
||||
currentBranch = branchName;
|
||||
dispatch('change', { branch: branchName });
|
||||
toast(`Switched to ${branchName}`, 'success');
|
||||
toast($t.git?.switched_to?.replace('{branch}', branchName), 'success');
|
||||
console.log(`[BranchSelector][Coherence:OK] Checked out ${branchName}`);
|
||||
} catch (e) {
|
||||
console.error(`[BranchSelector][Coherence:Failed] ${e.message}`);
|
||||
@@ -110,7 +110,7 @@
|
||||
console.log(`[BranchSelector][Action] Creating branch ${newBranchName} from ${currentBranch}`);
|
||||
try {
|
||||
await gitService.createBranch(dashboardId, newBranchName, currentBranch);
|
||||
toast(`Created branch ${newBranchName}`, 'success');
|
||||
toast($t.git?.created_branch?.replace('{branch}', newBranchName), 'success');
|
||||
showCreate = false;
|
||||
newBranchName = '';
|
||||
await loadBranches();
|
||||
@@ -151,7 +151,7 @@
|
||||
<div class="flex-grow">
|
||||
<Input
|
||||
bind:value={newBranchName}
|
||||
placeholder="branch-name"
|
||||
placeholder={$t.git?.branch_name_placeholder}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -50,10 +50,10 @@
|
||||
`/git/repositories/${dashboardId}/generate-message`,
|
||||
);
|
||||
message = data.message;
|
||||
toast($t.git?.commit_message_generated || "Commit message generated", "success");
|
||||
toast($t.git?.commit_message_generated, "success");
|
||||
} catch (e) {
|
||||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||||
toast(e.message || ($t.git?.commit_message_failed || "Failed to generate message"), "error");
|
||||
toast(e.message || $t.git?.commit_message_failed, "error");
|
||||
} finally {
|
||||
generatingMessage = false;
|
||||
}
|
||||
@@ -94,7 +94,7 @@
|
||||
if (!diff) diff = "";
|
||||
} catch (e) {
|
||||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||||
toast($t.git?.load_changes_failed || "Failed to load changes", "error");
|
||||
toast($t.git?.load_changes_failed, "error");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -115,7 +115,7 @@
|
||||
committing = true;
|
||||
try {
|
||||
await gitService.commit(dashboardId, message, []);
|
||||
toast($t.git?.commit_success || "Changes committed successfully", "success");
|
||||
toast($t.git?.commit_success, "success");
|
||||
dispatch("commit");
|
||||
show = false;
|
||||
message = "";
|
||||
@@ -142,7 +142,7 @@
|
||||
<div
|
||||
class="bg-white p-6 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col"
|
||||
>
|
||||
<h2 class="text-xl font-bold mb-4">{$t.git?.commit || "Commit Changes"}</h2>
|
||||
<h2 class="text-xl font-bold mb-4">{$t.git?.commit}</h2>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4 flex-1 overflow-hidden">
|
||||
<!-- Left: Message and Files -->
|
||||
@@ -151,7 +151,7 @@
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>{$t.git?.commit_message || "Commit Message"}</label
|
||||
>{$t.git?.commit_message}</label
|
||||
>
|
||||
<button
|
||||
onclick={handleGenerateMessage}
|
||||
@@ -159,16 +159,16 @@
|
||||
class="text-xs text-blue-600 hover:text-blue-800 disabled:opacity-50 flex items-center"
|
||||
>
|
||||
{#if generatingMessage}
|
||||
<span class="animate-spin mr-1">↻</span> {$t.mapper?.generating || "Generating..."}
|
||||
<span class="animate-spin mr-1">↻</span> {$t.mapper?.generating}
|
||||
{:else}
|
||||
<span class="mr-1">✨</span> {$t.git?.generate_with_ai || "Generate with AI"}
|
||||
<span class="mr-1">✨</span> {$t.git?.generate_with_ai}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
class="w-full border rounded p-2 h-32 focus:ring-2 focus:ring-blue-500 outline-none resize-none"
|
||||
placeholder={$t.git?.describe_changes || "Describe your changes..."}
|
||||
placeholder={$t.git?.describe_changes}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -177,13 +177,13 @@
|
||||
<h3
|
||||
class="text-sm font-bold text-gray-500 uppercase mb-2"
|
||||
>
|
||||
{$t.git?.changed_files || "Changed Files"}
|
||||
{$t.git?.changed_files}
|
||||
</h3>
|
||||
<ul class="text-xs space-y-1">
|
||||
{#each status.staged_files as file}
|
||||
<li
|
||||
class="text-green-600 flex items-center font-semibold"
|
||||
title="Staged"
|
||||
title={$t.git?.staged}
|
||||
>
|
||||
<span class="mr-2">S</span>
|
||||
{file}
|
||||
@@ -192,7 +192,7 @@
|
||||
{#each status.modified_files as file}
|
||||
<li
|
||||
class="text-yellow-600 flex items-center"
|
||||
title="Modified (Unstaged)"
|
||||
title={$t.git?.modified_unstaged}
|
||||
>
|
||||
<span class="mr-2">M</span>
|
||||
{file}
|
||||
@@ -201,7 +201,7 @@
|
||||
{#each status.untracked_files as file}
|
||||
<li
|
||||
class="text-blue-600 flex items-center"
|
||||
title="Untracked"
|
||||
title={$t.git?.untracked}
|
||||
>
|
||||
<span class="mr-2">?</span>
|
||||
{file}
|
||||
@@ -219,14 +219,14 @@
|
||||
<div
|
||||
class="bg-gray-200 px-3 py-1 text-xs font-bold text-gray-600 border-b"
|
||||
>
|
||||
{$t.git?.changes_preview || "Changes Preview"}
|
||||
{$t.git?.changes_preview}
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-2">
|
||||
{#if loading}
|
||||
<div
|
||||
class="flex items-center justify-center h-full text-gray-500"
|
||||
>
|
||||
{$t.git?.loading_diff || "Loading diff..."}
|
||||
{$t.git?.loading_diff}
|
||||
</div>
|
||||
{:else if diff}
|
||||
<pre
|
||||
@@ -235,7 +235,7 @@
|
||||
<div
|
||||
class="flex items-center justify-center h-full text-gray-500 italic"
|
||||
>
|
||||
{$t.git?.no_changes || "No changes detected"}
|
||||
{$t.git?.no_changes}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -247,7 +247,7 @@
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
{$t.common?.cancel || "Cancel"}
|
||||
{$t.common?.cancel}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleCommit}
|
||||
@@ -258,7 +258,7 @@
|
||||
status?.staged_files?.length === 0)}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{committing ? ($t.git?.committing || "Committing...") : ($t.git?.commit || "Commit")}
|
||||
{committing ? $t.git?.committing : $t.git?.commit}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user